4 Commits
Author SHA1 Message Date
xmz7891 31a2360031 删掉临时提交说明 2026-10-09 13:16:31 +08:00
xmz7891 be59a20036 2.1.5:修「Windows 7 装完报 dll 缺失」
用户报:学校的电脑是 Windows 7,装完提示缺 DLL。

== 根因不是漏打包,是 Python 版本越界 ==

    Python 3.8    -> 最后一个支持 Windows 7 的版本
    Python 3.9+   -> 只支持 Windows 8 及以上

Python 3.9 起用到了 api-ms-win-core-path-l1-1-0.dll 里的
PathCchCanonicalizeEx,而这个 DLL **Windows 7 上根本不存在**,
也没法用 api-set 转发(那套机制只在 Win8+ 生效)。

所以之前用 3.11 打出来的包,在 Win7 上必然报缺 DLL ——
**那个 DLL 不是"忘了放进包",是"放进去也没用"。**

顺带说明:包里那 47 个 api-ms-win-core-*.dll 转发库正是
Python 为兼容 Win7 才带上的 —— 上游本来就考虑过 Win7,
只是 3.11 这个版本号越界了。

== 改法 ==

1. **改用 Python 3.8 打包,而且选 32 位**
   32 位在 Win7 的 32 位和 64 位系统上都能跑,学校不用分两种发。
   (64 位的包在 32 位 Windows 上直接起不来。)

   依赖固定到 3.8 能用的版本:
       Pillow==9.5.0(最后一个支持 3.8 的)
       pyinstaller 6.x(保持 _internal 布局,和安装包/测试/卸载逻辑一致;
                       5.x 会把依赖摊在根目录,导致"_internal 在"那一项失败)

2. **打包脚本主动拦**
   windows\build_exe.ps1 打包前先校验 Python:
     * 找不到 3.8 -> 报错并给出安装命令
     * 版本不是 3.8 -> 直接 throw,而不是默默打一个别人装不上的包
     * 不是 32 位 -> 警告
   Python 从 YKXMZ_PY 环境变量或常见路径里找,不写死。

3. **新增 Win7 兼容校验** _build\check_win7_compat.py
   盯三件事:内嵌是不是 3.8 的运行库、**有没有**那个要命的 DLL、
   exe 是不是 32 位。打包时和回归套件(3a9 关)里都会跑,
   不过就拒绝出包。

== 顺手修的 ==

_build/test_installer_cycle.py 里写死了 ykxmz-setup-1.8.2.exe,
于是一直在**装一个不存在的旧包** -> 静默失败 -> 报出一堆
莫名其妙的失败("配对码实际是 xxx"、"版本显示 v1.8.2")。
改成自动找 dist 下版本号最新的包(按数字段排序,不能按字符串,
否则 1.9.0 会排到 2.1.4 前面)。

== 验证(用户要求:自己测一遍,没问题再上传)==

    全量回归 42 个阶段全 PASS(含新的 "Windows 7 兼容" 关)
    安装包完整周期 20/20
    Win7 校验:python38.dll / 无 api-ms-win-core-path / 32 位
    实际启动打包的 EXE:进程活着、TCP 监听、UDP 自动发现在听、
                        连接能建立(日志里有"设备接入")
    产物扫描:三件全部干净(无旧密码、无服务器 IP)
    提交前泄漏闸门通过

安装包体积 14.4 MB -> 9.0 MB。
2026-10-09 13:16:31 +08:00
xmz7891 7ffd56391e 删掉临时提交说明 2026-10-09 00:02:32 +08:00
xmz7891 0ad3f648c6 修上一轮清理留下的副作用:占位符把运维脚本弄连不上了
上一个提交把仓库里的服务器 IP 换成了占位符,但**那些脚本本身要用这个 IP
去连服务器** —— 换掉之后它们就跑不起来了。这个提交把取值方式理清楚。

== 取值统一走 _build/lssh.py ==

    YKXMZ_LS_HOST 环境变量
    或 _build/.lightsail_host(一行 IP,已 gitignore)
    都没有就报错,并告诉你怎么设

十个运维脚本改成 `from lssh import ... HOST as LS_HOST`,
公开仓库里一个具体地址都不留。

== 顺带修的两个自己造的 bug ==

  * 用正则往 `from lssh import ...` 后面追加东西时,
    追加到了行尾的 `# noqa: E402` **注释里面** ——
    十处导入全是废的,脚本一跑就 NameError。
    改成先剥掉注释、拼好导入、再把 noqa 加回去。

  * 同一个正则还把 `from lssh import HOST as LS_HOST` 加进了
    **lssh.py 自己**,造成自我导入。

教训:**正则批量改导入语句很危险**(注释、行尾、顺序都能坑到),
这种地方宁可逐个 edit。

== 为什么要重建产物 ==

docs/AFTER-INSTALL.txt 会被打进服务端 zip,而它上一轮改过
(去掉了写死的旧管理员密码)—— 不重打的话发出去的还是旧的。

重打后确认:三个产物里都没有旧密码、没有服务器 IP。

== 验证 ==

    全量回归退出码 0,41 个阶段全 PASS
    603 个 Python 文件语法通过
    ykxmz-agent / server / watch 三个产物扫描干净
    提交前泄漏闸门通过
2026-10-09 00:02:32 +08:00
39 changed files with 438 additions and 75 deletions
+5
View File
@@ -81,3 +81,8 @@ admin_password.txt
/_build/relay_info.json /_build/relay_info.json
# 后台管理密码(绝不进仓库) # 后台管理密码(绝不进仓库)
_build/.admin_password _build/.admin_password
# 服务器地址(绝不进仓库)
_build/.lightsail_host
_build/.gitee_token
_build/.admin_password
+8 -8
View File
@@ -45,9 +45,9 @@
| 装在哪 | 是什么 | 文件 | | 装在哪 | 是什么 | 文件 |
|---|---|---| |---|---|---|
| 教室电脑(Windows) | 被控端 | `ykxmz-setup-2.1.4.exe` | | 教室电脑(Windows) | 被控端 | `ykxmz-setup-2.1.5.exe` |
| 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-2.1.4.zip` | | 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-2.1.5.zip` |
| 手表(安卓) | 控制端 | `ykxmz-watch-2.1.4.apk` | | 手表(安卓) | 控制端 | `ykxmz-watch-2.1.5.apk` |
**只在一个教室用?** 那第三件(中转站)可以先不装,手表和电脑连同一个 WiFi 就能直连。 **只在一个教室用?** 那第三件(中转站)可以先不装,手表和电脑连同一个 WiFi 就能直连。
@@ -81,7 +81,7 @@
### 第一步:电脑上装被控端 ### 第一步:电脑上装被控端
双击 `ykxmz-setup-2.1.4.exe`,按提示装完(默认装到 `C:\Program Files\ykxmz`)。 双击 `ykxmz-setup-2.1.5.exe`,按提示装完(默认装到 `C:\Program Files\ykxmz`)。
第一次运行会弹出窗口,填两样: 第一次运行会弹出窗口,填两样:
@@ -94,12 +94,12 @@
> 想再打开窗口,**再双击一次程序图标**就行,不会起第二个进程。 > 想再打开窗口,**再双击一次程序图标**就行,不会起第二个进程。
> 想彻底退出,窗口上点「退出」。 > 想彻底退出,窗口上点「退出」。
> 不想用安装包的话,`ykxmz-agent-2.1.4.zip` 是绿色版: > 不想用安装包的话,`ykxmz-agent-2.1.5.zip` 是绿色版:
> 解压出来直接跑 `ykxmz.exe`,整个文件夹拷走就能换台机器用,不写注册表。 > 解压出来直接跑 `ykxmz.exe`,整个文件夹拷走就能换台机器用,不写注册表。
### 第二步:手表上装 App ### 第二步:手表上装 App
把 `ykxmz-watch-2.1.4.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。 把 `ykxmz-watch-2.1.5.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。
打开 App,首页有四个入口: 打开 App,首页有四个入口:
@@ -223,7 +223,7 @@
中转站就是**一台能上外网的 Linux 小机器**(随身 WiFi、树莓派、云主机都行, 中转站就是**一台能上外网的 Linux 小机器**(随身 WiFi、树莓派、云主机都行,
Debian 12 / Ubuntu 实测可用,512 MB 内存就够)。 Debian 12 / Ubuntu 实测可用,512 MB 内存就够)。
把 `ykxmz-server-2.1.4.zip` 解压到那台机器上,然后一条命令: 把 `ykxmz-server-2.1.5.zip` 解压到那台机器上,然后一条命令:
```bash ```bash
bash deploy.sh bash deploy.sh
@@ -456,7 +456,7 @@ windows\build_exe.ps1
```powershell ```powershell
android\build_apk.ps1 android\build_apk.ps1
# 流程:aapt2 → javac → d8 → zipalign → apksigner # 流程:aapt2 → javac → d8 → zipalign → apksigner
# 产物:dist\ykxmz-watch-2.1.4.apk # 产物:dist\ykxmz-watch-2.1.5.apk
``` ```
最低支持 Android 7.0(API 24),targetSdk 34。 最低支持 Android 7.0(API 24),targetSdk 34。
+38
View File
@@ -1,3 +1,41 @@
## 2.1.5
- **修「Windows 7 装完报 dll 缺失」** —— 学校电脑装不上。
这一条是用户报的:学校的电脑是 Windows 7,装完提示缺 DLL。
**根因不是漏打包,是 Python 版本越界**:
| Python | 支持的系统 |
|---|---|
| **3.8** | Windows 7 及以上 ← 最后一个支持 Win7 的版本 |
| 3.9 ~ 3.13 | 只支持 Windows 8 及以上 |
Python 3.9 起用到了 `api-ms-win-core-path-l1-1-0.dll` 里的
`PathCchCanonicalizeEx`,而这个 DLL **Windows 7 上根本不存在**,
也没法用 api-set 转发(那套机制只在 Win8+ 生效)。
所以之前用 3.11 打出来的包,在 Win7 上必然报缺 DLL ——
**那个 DLL 不是"忘了放进包",是"放进去也没用"。**
**改法:改用 Python 3.8 打包**,而且选 **32 位** ——
32 位在 Win7 的 32 位和 64 位系统上**都能跑**,学校不用分两种发。
顺带加了两道自动关卡,以后不会再退化:
- 打包脚本 `windows\build_exe.ps1` **先校验 Python 版本**,
不是 3.8 直接报错退出(而不是默默打一个别人装不上的包)
- `_build\check_win7_compat.py` 校验产物:
内嵌是不是 3.8 的运行库、**有没有**那个要命的 DLL、exe 是不是 32 位。
打包时和回归套件里都会跑,不过就拒绝出包
安装包体积也从 14.4 MB 降到 **9.0 MB**(32 位运行库小一圈)。
- **顺手修了安装包测试里一个藏了很久的坑**:
`_build/test_installer_cycle.py` 里写死了 `ykxmz-setup-1.8.2.exe`,
于是它一直在**装一个不存在的旧包** → 静默失败 → 报出一堆
莫名其妙的失败("配对码实际是 xxx"、"版本显示 v1.8.2")。
改成自动找 `dist` 下版本号最新的包,以后不会再过期。
## 2.1.4 ## 2.1.4
- **修一个"填了地址也连不上"的 bug**(手表端)。 - **修一个"填了地址也连不上"的 bug**(手表端)。
+4
View File
@@ -167,6 +167,10 @@ def main() -> int:
changed += 1 changed += 1
# ---- 会断言成品名的测试 ---- # ---- 会断言成品名的测试 ----
# 注意 test_installer_cycle.py 也曾经写死过成品名(ykxmz-setup-1.8.2.exe),
# 结果它去装一个不存在的包 -> 静默失败 -> 报出一堆莫名其妙的
# "配对码实际是 xxx" "版本显示 v1.8.2"。那个文件现在改成自动找
# dist 下最新的安装包了,不再需要这里维护。
for name in ("test_package_remote.py", "test_apk_contents.py"): for name in ("test_package_remote.py", "test_apk_contents.py"):
path = ROOT / "_tests" / name path = ROOT / "_tests" / name
if not path.is_file(): if not path.is_file():
+3 -3
View File
@@ -17,7 +17,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build")) sys.path.insert(0, str(ROOT / "_build"))
from lssh import run # noqa: E402 from lssh import run, HOST as LS_HOST # noqa: E402
PORT = "13245" PORT = "13245"
DOMAIN = "yk.xxmxxs.top" DOMAIN = "yk.xxmxxs.top"
@@ -28,8 +28,8 @@ RELAY_DIR = "/opt/ykxmz-relay"
# IP:80 —— 绕过 Cloudflare,直连 nginx(域名没配好时靠它) # IP:80 —— 绕过 Cloudflare,直连 nginx(域名没配好时靠它)
# :13245 —— **应该不通**(中转已只绑 127.0.0.1,不再对公网) # :13245 —— **应该不通**(中转已只绑 127.0.0.1,不再对公网)
DOMAIN_URL = "http://%s" % DOMAIN DOMAIN_URL = "http://%s" % DOMAIN
NGINX_URL = "http://<源站IP>:80" NGINX_URL = "http://%s:80" % LS_HOST
DIRECT_URL = "http://<源站IP>:%s" % PORT DIRECT_URL = "http://%s:%s" % (LS_HOST, PORT)
REMOTE = r''' REMOTE = r'''
R=__DIR__ R=__DIR__
+2 -2
View File
@@ -19,10 +19,10 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build")) sys.path.insert(0, str(ROOT / "_build"))
from lssh import run # noqa: E402 from lssh import run, HOST as LS_HOST # noqa: E402
DOMAIN = "yk.xxmxxs.top" DOMAIN = "yk.xxmxxs.top"
IP = "<源站IP>" IP = LS_HOST
def tcp(host: str, port: int, timeout: float = 8.0) -> bool: def tcp(host: str, port: int, timeout: float = 8.0) -> bool:
+137
View File
@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
r'''校验打出来的 EXE 能不能在 **Windows 7** 上跑。
== 为什么需要这个 ==
学校电脑是 Windows 7,装完报「dll 缺失」。
根因不是漏打包,是 **Python 版本越界**:
Python 3.8 -> 最后一个支持 Windows 7 的版本
Python 3.9+ -> 只支持 Windows 8 及以上
Python 3.9 起用到了 `api-ms-win-core-path-l1-1-0.dll` 里的
`PathCchCanonicalizeEx`,而这个 DLL **Windows 7 上根本不存在**,
也没法用 api-set 转发(那套机制只在 Win8+ 生效)。
**所以那个 DLL 不是"忘了放进包",是"放进去也没用"。**
唯一可行的办法是**用 3.8 打包**。
== 这个脚本查什么 ==
1. 有没有 python38.dll(而不是 python39/310/311/... )
2. **不该有** api-ms-win-core-path-l1-1-0.dll
—— 有它就说明是 3.9+ 打的,Win7 上必挂
3. PE 头是不是 32 位(0x14C)
—— 32 位在 Win7 的 32/64 位系统上都能跑;
64 位在 32 位 Windows 上直接起不来
4. 顺带报一下总大小和文件数
用 `python _build/check_win7_compat.py` 单独跑也行,
`build_exe.ps1` 里会调它,不过就直接拒绝出包。
'''
from __future__ import annotations
import struct
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DIST = ROOT / "dist_exe" / "ykxmz"
FAILS: list = []
def check(name: str, ok: bool, why: str = "") -> None:
print(" [%s] %s%s" % ("PASS" if ok else "FAIL", name,
(" —— " + why) if why else ""))
if not ok:
FAILS.append(name)
def pe_machine(path: Path) -> int:
"""读 PE 头里的 machine 字段。0x14C=32 位,0x8664=64 位。"""
with path.open("rb") as f:
if f.read(2) != b"MZ":
return 0
f.seek(0x3C)
off = struct.unpack("<I", f.read(4))[0]
f.seek(off)
if f.read(4) != b"PE\0\0":
return 0
return struct.unpack("<H", f.read(2))[0]
def main() -> int:
print("=" * 58)
print(" Windows 7 兼容性校验")
print("=" * 58)
print()
check("dist_exe/ykxmz 存在", DIST.is_dir(),
"先跑 windows\\build_exe.ps1")
if not DIST.is_dir():
return 1
dlls = {p.name.lower(): p for p in DIST.rglob("*.dll")}
exe = DIST / "ykxmz.exe"
check("ykxmz.exe 存在", exe.is_file())
if not exe.is_file():
return 1
# ---- 1. Python 运行库版本 ----
print()
print("一、Python 运行库")
py_dlls = sorted(n for n in dlls if n.startswith("python3") and n.endswith(".dll"))
for n in py_dlls:
print(" %s" % n)
check("有 python38.dll(3.8 打的)",
any("python38" in n for n in py_dlls),
"没有 python38.dll 说明不是 3.8 打的")
bad_py = [n for n in py_dlls
if any(v in n for v in ("python39", "python310", "python311",
"python312", "python313", "python314"))]
check("没有 3.9+ 的运行库", not bad_py,
"3.9+ 的包在 Win7 上会因为缺 DLL 启动失败:%s" % bad_py)
# ---- 2. 那个要命的 DLL ----
print()
print("二、Win7 上不存在的那个 DLL")
killer = "api-ms-win-core-path-l1-1-0.dll"
has = killer in dlls
check("**没有** %s" % killer, not has,
"有它就说明是 3.9+ 打的 —— Win7 上必然报缺 DLL")
print(" (这个 DLL 是 Win8+ 才有的。Python 3.9 起用到它,")
print(" 放进来也没用,Win7 上加载不了。)")
# ---- 3. 位数 ----
print()
print("三、位数")
m = pe_machine(exe)
arch = {0x14C: "32 位 (x86)", 0x8664: "64 位 (x64)"}.get(m, "未知 (0x%X)" % m)
print(" ykxmz.exe: %s" % arch)
check("是 32 位(Win7 32/64 位都能跑)", m == 0x14C,
"64 位的包在 32 位 Windows 上起不来;学校机器可能是 32 位")
# ---- 4. 体积概览 ----
print()
print("四、产物概览")
files = list(DIST.rglob("*"))
files = [f for f in files if f.is_file()]
total = sum(f.stat().st_size for f in files)
print(" 文件数: %d" % len(files))
print(" 总大小: %.1f MB" % (total / 1048576))
print(" exe 大小: %.1f MB" % (exe.stat().st_size / 1048576))
print()
print("=" * 58)
if FAILS:
print(" 失败 %d 项:%s" % (len(FAILS), ", ".join(FAILS)))
print(" 这个包学校电脑装不上,不该发布。")
return 1
print(" ✅ 可以在 Windows 7 / 8 / 10 / 11 上运行")
return 0
if __name__ == "__main__":
sys.exit(main())
+3 -3
View File
@@ -40,7 +40,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build")) sys.path.insert(0, str(ROOT / "_build"))
from lssh import run, upload # noqa: E402 from lssh import run, upload, HOST as LS_HOST # noqa: E402
# 用户指定的端口 # 用户指定的端口
PORT = "13245" PORT = "13245"
@@ -88,7 +88,7 @@ WantedBy=multi-user.target
def main() -> int: def main() -> int:
print("=" * 62) print("=" * 62)
print(" 部署到 Lightsail(<源站IP>),端口 %s" % PORT) print(" 部署到 Lightsail(LS_HOST),端口 %s" % PORT)
print("=" * 62) print("=" * 62)
print() print()
@@ -167,7 +167,7 @@ def main() -> int:
print() print()
print(" 提示:防火墙要先放行 TCP %s(用户来开)。" % PORT) print(" 提示:防火墙要先放行 TCP %s(用户来开)。" % PORT)
print(" 之后公网地址就是 http://<源站IP>:%s/" % PORT) print(" 之后公网地址就是 http://LS_HOST:%s/" % PORT)
return 0 return 0
+3 -3
View File
@@ -31,11 +31,11 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build")) sys.path.insert(0, str(ROOT / "_build"))
from lssh import run, upload # noqa: E402 from lssh import run, upload, HOST as LS_HOST # noqa: E402
RELAY_DIR = "/opt/ykxmz-relay" RELAY_DIR = "/opt/ykxmz-relay"
PORT = "13245" PORT = "13245"
PUBLIC_URL = "http://<源站IP>:%s" % PORT PUBLIC_URL = "http://%s:%s" % (LS_HOST, PORT)
MONITOR = r'''#!/bin/bash MONITOR = r'''#!/bin/bash
# ============================================================================ # ============================================================================
@@ -46,7 +46,7 @@ MONITOR = r'''#!/bin/bash
# #
# 为什么不查 127.0.0.1: # 为什么不查 127.0.0.1:
# 本机 200 只说明进程活着。用户真正关心的是"打开 # 本机 200 只说明进程活着。用户真正关心的是"打开
# http://<源站IP>:13245 能不能出页面" —— 那要经过 # http://LS_HOST:13245 能不能出页面" —— 那要经过
# Lightsail 的防火墙 / 安全组 / 上游线路。实测踩过: # Lightsail 的防火墙 / 安全组 / 上游线路。实测踩过:
# 隧道两条实例都掉了,本机 curl 照样 200,但公网 502。 # 隧道两条实例都掉了,本机 curl 照样 200,但公网 502。
# #
+2 -2
View File
@@ -20,11 +20,11 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build")) sys.path.insert(0, str(ROOT / "_build"))
from lssh import run, upload # noqa: E402 from lssh import run, upload, HOST as LS_HOST # noqa: E402
RELAY_DIR = "/opt/ykxmz-relay" RELAY_DIR = "/opt/ykxmz-relay"
PORT = "13245" PORT = "13245"
PUBLIC = "http://<源站IP>:%s" % PORT PUBLIC = "http://%s:%s" % (LS_HOST, PORT)
CODE = ["relay.py", "admin.py", "landing.py", "showcase.py"] CODE = ["relay.py", "admin.py", "landing.py", "showcase.py"]
ARTIFACTS = ["ykxmz-setup-{v}.exe", "ykxmz-watch-{v}.apk", ARTIFACTS = ["ykxmz-setup-{v}.exe", "ykxmz-watch-{v}.apk",
+2 -2
View File
@@ -33,7 +33,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build")) sys.path.insert(0, str(ROOT / "_build"))
from lssh import run # noqa: E402 from lssh import run, HOST as LS_HOST # noqa: E402
DOMAIN = "yk.xxmxxs.top" DOMAIN = "yk.xxmxxs.top"
@@ -86,7 +86,7 @@ done
echo echo
echo "════ 6. 对比:直连自己的 nginx ════" echo "════ 6. 对比:直连自己的 nginx ════"
printf ' %-30s %s\n' "127.0.0.1:443(带 SNI)" "$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 --resolve $D:443:127.0.0.1 https://$D/v1/health)" printf ' %-30s %s\n' "127.0.0.1:443(带 SNI)" "$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 --resolve $D:443:127.0.0.1 https://$D/v1/health)"
printf ' %-30s %s\n' "公网 IP:443" "$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 --resolve $D:443:<源站IP> https://$D/v1/health)" printf ' %-30s %s\n' "公网 IP:443" "$(curl -sk -o /dev/null -w '%{http_code}' --max-time 8 --resolve $D:443:LS_HOST https://$D/v1/health)"
''' '''
+5 -5
View File
@@ -43,11 +43,11 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build")) sys.path.insert(0, str(ROOT / "_build"))
from lssh import run # noqa: E402 from lssh import run, HOST as LS_HOST # noqa: E402
RELAY_DIR = "/opt/ykxmz-relay" RELAY_DIR = "/opt/ykxmz-relay"
DOMAIN = "yk.xxmxxs.top" DOMAIN = "yk.xxmxxs.top"
FALLBACK = "http://<源站IP>:80" FALLBACK = "http://%s:80" % LS_HOST
def main() -> int: def main() -> int:
@@ -58,7 +58,7 @@ def main() -> int:
print() print()
print(" ── 1. 改 monitor.sh 的健康检查(域名优先,IP 兜底)──") print(" ── 1. 改 monitor.sh 的健康检查(域名优先,IP 兜底)──")
# 把 PUBLIC_URL 的默认值换掉,并把 is_healthy 改成"两个都试" # 把 PUBLIC_URL 的默认值换掉,并把 is_healthy 改成"两个都试"
old_url = 'PUBLIC_URL="${PUBLIC_URL:-http://<源站IP>:13245}"' old_url = 'PUBLIC_URL="${PUBLIC_URL:-http://LS_HOST:13245}"'
new_url = ( new_url = (
'# 探测地址:域名优先(用户实际走的路),IP:80 兜底\n' '# 探测地址:域名优先(用户实际走的路),IP:80 兜底\n'
'# (域名 DNS 还没指过来时用它,否则监测会一直误报)\n' '# (域名 DNS 还没指过来时用它,否则监测会一直误报)\n'
@@ -151,10 +151,10 @@ def main() -> int:
'http://%s/v1/health)"; ' 'http://%s/v1/health)"; '
'printf " %-28s %s\\n" "IP:80 (兜底)" ' 'printf " %-28s %s\\n" "IP:80 (兜底)" '
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
'http://<源站IP>:80/v1/health)"; ' 'http://LS_HOST:80/v1/health)"; '
'printf " %-28s %s\\n" "IP:13245 (已关闭)" ' 'printf " %-28s %s\\n" "IP:13245 (已关闭)" '
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
'http://<源站IP>:13245/v1/health)"' % DOMAIN, 'http://LS_HOST:13245/v1/health)"' % DOMAIN,
timeout=120, quiet=True) timeout=120, quiet=True)
for ln in out[1].strip().splitlines(): for ln in out[1].strip().splitlines():
print(ln) print(ln)
+8 -1
View File
@@ -32,7 +32,14 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
IP = "<源站IP>" # ⚠ 这个脚本要"在所有发行版介绍里找这个 IP",所以它必须知道 IP。
# 但**不能在源码里写死** —— 写了就等于把 IP 又发到公开仓库里,
# 正是这个脚本要防的事。
# 从 lssh 取(那里会读环境变量 YKXMZ_LS_HOST 或
# _build/.lightsail_host,后者已 gitignore)。
sys.path.insert(0, str(Path(__file__).resolve().parent))
from lssh import HOST as IP, HOST as LS_HOST # noqa: E402
DOMAIN = "yk.xxmxxs.top" DOMAIN = "yk.xxmxxs.top"
OWNER_REPO = "xmz7891/ykxmz" OWNER_REPO = "xmz7891/ykxmz"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 109 KiB

+2 -2
View File
@@ -31,7 +31,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build")) sys.path.insert(0, str(ROOT / "_build"))
from lssh import run, upload # noqa: E402 from lssh import run, upload, HOST as LS_HOST # noqa: E402
RELAY_DIR = "/opt/ykxmz-relay" RELAY_DIR = "/opt/ykxmz-relay"
@@ -94,7 +94,7 @@ def main() -> int:
'https://yk.xxmxxs.top/v1/health)"; ' 'https://yk.xxmxxs.top/v1/health)"; '
'printf " %-36s %s\\n" "公网 443(Lightsail 防火墙)" ' 'printf " %-36s %s\\n" "公网 443(Lightsail 防火墙)" '
'"$(curl -sk -o /dev/null -w \'%{http_code}\' --max-time 8 ' '"$(curl -sk -o /dev/null -w \'%{http_code}\' --max-time 8 '
'https://<源站IP>/v1/health 2>/dev/null || echo 不通)"' 'https://LS_HOST/v1/health 2>/dev/null || echo 不通)"'
) )
out = run(probe, timeout=180, quiet=True) out = run(probe, timeout=180, quiet=True)
for ln in out[1].strip().splitlines(): for ln in out[1].strip().splitlines():
+25 -1
View File
@@ -23,7 +23,31 @@ from pathlib import Path
import paramiko import paramiko
HOST = os.environ.get("YKXMZ_LS_HOST", "<源站IP>")
def _host() -> str:
"""服务器公网 IP —— **不写死在代码里**。
这个仓库是公开的。IP 写进来等于把源站地址告诉所有人,
而源站躲在 Cloudflare 后面本来就不该被知道(直接打 IP 就绕过 CDN
和防护了)。2026-10-08 就因为 IP 泄漏返工过一次。
取值顺序:
1. 环境变量 YKXMZ_LS_HOST
2. _build/.lightsail_host(一行 IP,已 gitignore)
"""
env = os.environ.get("YKXMZ_LS_HOST", "").strip()
if env:
return env
f = Path(__file__).resolve().parent / ".lightsail_host"
if f.is_file():
return f.read_text(encoding="utf-8").strip()
raise SystemExit(
"没拿到服务器地址。二选一:\n"
" set YKXMZ_LS_HOST=<你的服务器IP>\n"
" 或把 IP 写进 _build/.lightsail_host(该文件已在 .gitignore 里)")
HOST = _host()
USER = os.environ.get("YKXMZ_LS_USER", "admin") USER = os.environ.get("YKXMZ_LS_USER", "admin")
PORT = int(os.environ.get("YKXMZ_LS_PORT", "22")) PORT = int(os.environ.get("YKXMZ_LS_PORT", "22"))
KEY = Path(os.environ.get( KEY = Path(os.environ.get(
+1 -1
View File
@@ -17,7 +17,7 @@ import zipfile
from pathlib import Path from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
VERSION = "2.1.4" VERSION = "2.1.5"
NAME = f"ykxmz-{VERSION}" NAME = f"ykxmz-{VERSION}"
AGENT_SRC = ROOT / "dist_exe" / "ykxmz" AGENT_SRC = ROOT / "dist_exe" / "ykxmz"
+5 -5
View File
@@ -4,7 +4,7 @@ r'''在 Lightsail 上装 nginx,把 yk.xxmxxs.top 反代到中转。
== 架构 == == 架构 ==
用户 ──HTTPS──> Cloudflare CDN ──HTTP:80──> nginx ──> 127.0.0.1:13245 用户 ──HTTPS──> Cloudflare CDN ──HTTP:80──> nginx ──> 127.0.0.1:13245
(yk.xxmxxs.top) <源站IP> (yk.xxmxxs.top) LS_HOST
== 为什么要加 nginx 这一层 == == 为什么要加 nginx 这一层 ==
@@ -24,7 +24,7 @@ r'''在 Lightsail 上装 nginx,把 yk.xxmxxs.top 反代到中转。
== 关于端口 80 的安全 == == 关于端口 80 的安全 ==
nginx 监听 80 是**必须的**(Cloudflare 的开源端口)。 nginx 监听 80 是**必须的**(Cloudflare 的开源端口)。
但要注意:如果有人知道 <源站IP>,也能直接打 80 绕过 Cloudflare。 但要注意:如果有人知道 LS_HOST,也能直接打 80 绕过 Cloudflare。
所以配置里加了: 所以配置里加了:
* 只接受来自 Cloudflare 网段的请求(可选,见 allowlist 那段) * 只接受来自 Cloudflare 网段的请求(可选,见 allowlist 那段)
@@ -41,7 +41,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "_build")) sys.path.insert(0, str(ROOT / "_build"))
from lssh import run, upload # noqa: E402 from lssh import run, upload, HOST as LS_HOST # noqa: E402
DOMAIN = "yk.xxmxxs.top" DOMAIN = "yk.xxmxxs.top"
RELAY_PORT = "13245" RELAY_PORT = "13245"
@@ -178,13 +178,13 @@ def main() -> int:
'-H \'Host: ' + DOMAIN + '\' http://127.0.0.1:80/v1/health)"\n' '-H \'Host: ' + DOMAIN + '\' http://127.0.0.1:80/v1/health)"\n'
'printf " %-22s %s\\n" "公网 80(IP)" ' 'printf " %-22s %s\\n" "公网 80(IP)" '
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 15 ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 15 '
'http://<源站IP>:80/v1/health)"\n' 'http://LS_HOST:80/v1/health)"\n'
'printf " %-22s %s\\n" "中转本机(13245)" ' 'printf " %-22s %s\\n" "中转本机(13245)" '
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
'http://127.0.0.1:13245/v1/health)"\n' 'http://127.0.0.1:13245/v1/health)"\n'
'printf " %-22s %s\\n" "中转是否还暴露公网" ' 'printf " %-22s %s\\n" "中转是否还暴露公网" '
'"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 ' '"$(curl -s -o /dev/null -w \'%{http_code}\' --max-time 8 '
'http://<源站IP>:13245/v1/health)"\n' 'http://LS_HOST:13245/v1/health)"\n'
) )
out = run(script, timeout=180, quiet=True) out = run(script, timeout=180, quiet=True)
for ln in out[1].strip().splitlines(): for ln in out[1].strip().splitlines():
+30 -1
View File
@@ -19,13 +19,42 @@ r'''安装包完整验收:装 → 验 → 卸 → 再验。
from __future__ import annotations from __future__ import annotations
import json import json
import re
import subprocess import subprocess
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
SETUP = ROOT / "dist" / "ykxmz-setup-1.8.2.exe"
def _newest_setup() -> Path:
"""自动找 dist 下最新的安装包。
⚠ 这里原来是写死的 `ykxmz-setup-1.8.2.exe`。后果很隐蔽:
测试去装一个**根本不存在的旧版本包** -> 安装静默失败 ->
读到的还是上一次残留的状态 -> 报出一堆莫名其妙的失败
("配对码实际是 xxx"、"版本显示 v1.8.2")。
版本号每次发版都在变,写死必然过期。
`bump_version_generic.py` 更新了别的测试,唯独漏了这个文件。
改成按版本号排序取最新的 —— 一劳永逸,以后不用再维护。
"""
cands = []
for p in (ROOT / "dist").glob("ykxmz-setup-*.exe"):
# 文件名里抠版本号,按数字段排序(不能按字符串,否则 1.9.0 > 2.1.4)
m = re.search(r"ykxmz-setup-(\d+)\.(\d+)\.(\d+)\.exe$", p.name)
if m:
cands.append((tuple(int(x) for x in m.groups()), p))
if not cands:
return ROOT / "dist" / "ykxmz-setup-不存在.exe"
cands.sort(key=lambda t: t[0])
return cands[-1][1]
SETUP = _newest_setup()
APP = Path(r"C:\Program Files\ykxmz") APP = Path(r"C:\Program Files\ykxmz")
DATA = Path(r"C:\ProgramData\ykxmz") DATA = Path(r"C:\ProgramData\ykxmz")
EXE = APP / "ykxmz.exe" EXE = APP / "ykxmz.exe"
+1 -1
View File
@@ -23,7 +23,7 @@ def _latest_apk() -> Path:
(ROOT / "dist").glob("*watch*.apk"), (ROOT / "dist").glob("*watch*.apk"),
key=lambda p: p.stat().st_mtime, reverse=True) key=lambda p: p.stat().st_mtime, reverse=True)
return candidates[0] if candidates else \ return candidates[0] if candidates else \
ROOT / "dist" / "ykxmz-watch-2.1.4.apk" ROOT / "dist" / "ykxmz-watch-2.1.5.apk"
APK = _latest_apk() APK = _latest_apk()
+4 -4
View File
@@ -26,9 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent
# 被控端 -> 一个 zip # 被控端 -> 一个 zip
# 服务器端 -> 一个 zip # 服务器端 -> 一个 zip
# 控制端 -> 直接给 APK(不打 zip) # 控制端 -> 直接给 APK(不打 zip)
AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.4.zip" AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.5.zip"
SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.4.zip" SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.5.zip"
WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.4.apk" WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.5.apk"
TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain") TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain")
JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe" JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe"
JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe" JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe"
@@ -89,7 +89,7 @@ def main() -> int:
for label, path in (("被控端", agent_dir / "README-agent.txt"), for label, path in (("被控端", agent_dir / "README-agent.txt"),
("服务器端", relay_dir / "README-server.txt"), ("服务器端", relay_dir / "README-server.txt"),
("控制端(旁边那份)", ("控制端(旁边那份)",
ROOT / "dist" / "ykxmz-watch-2.1.4-说明.txt")): ROOT / "dist" / "ykxmz-watch-2.1.5-说明.txt")):
raw = path.read_bytes() if path.is_file() else b"" raw = path.read_bytes() if path.is_file() else b""
check(f"{label}说明有 UTF-8 BOM", check(f"{label}说明有 UTF-8 BOM",
raw[:3] == b"\xef\xbb\xbf", raw[:3]) raw[:3] == b"\xef\xbb\xbf", raw[:3])
+21
View File
@@ -481,6 +481,27 @@ $stageOut = & python (Join-Path $root "_tests\check_version_consistency.py") 2>&
Show-Fails $stageOut Show-Fails $stageOut
Record "版本号一致性" ($LASTEXITCODE -eq 0) Record "版本号一致性" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3a9
Banner "3a9/7 Windows 7 兼容(学校电脑是 Win7)"
# 用户报:学校电脑是 Windows 7,装完报"dll 缺失"。
#
# 根因不是漏打包,是 **Python 版本越界**:
# Python 3.8 -> 最后一个支持 Windows 7 的版本
# Python 3.9+ -> 只支持 Windows 8 及以上
# Python 3.9 起用到 api-ms-win-core-path-l1-1-0.dll 里的
# PathCchCanonicalizeEx,而这个 DLL **Win7 上根本不存在**,
# 也没法用 api-set 转发 —— 那个 DLL 放进去也没用。
#
# 这个关卡盯三件事:
# 1. 内嵌的是 python38.dll(不是 python311.dll 之类)
# 2. **没有** api-ms-win-core-path-l1-1-0.dll
# 3. exe 是 32 位(Win7 的 32/64 位系统都能跑;64 位在 32 位系统上起不来)
# 打包脚本 windows\build_exe.ps1 里也调了同一个检查,
# 不过就直接拒绝出包 —— 这里是回归层面的双保险。
$stageOut = & python (Join-Path $root "_build\check_win7_compat.py") 2>&1
Show-Fails $stageOut
Record "Windows 7 兼容" ($LASTEXITCODE -eq 0)
# ---------------------------------------------------------------- 3a5 # ---------------------------------------------------------------- 3a5
Banner "3a5/7 局域网/中转对比基准(防测试自己骗人)" Banner "3a5/7 局域网/中转对比基准(防测试自己骗人)"
# 这个基准一度是红的,而且报告里写着「这是中转链路的产品 bug」—— # 这个基准一度是红的,而且报告里写着「这是中转链路的产品 bug」——
+1 -1
View File
@@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lanremote.watch" package="com.lanremote.watch"
android:versionCode="5" android:versionCode="5"
android:versionName="2.1.4"> android:versionName="2.1.5">
<!-- ⚠ 允许明文 HTTP。 <!-- ⚠ 允许明文 HTTP。
中转可以是"自己搭的、没有证书"的服务:局域网那台,或者 中转可以是"自己搭的、没有证书"的服务:局域网那台,或者
+2 -2
View File
@@ -27,7 +27,7 @@ $d8 = Join-Path $bt "d8.bat"
$work = Join-Path $android "build" $work = Join-Path $android "build"
$outDir = Join-Path $root "dist" $outDir = Join-Path $root "dist"
$apkName = "ykxmz-watch-2.1.4.apk" $apkName = "ykxmz-watch-2.1.5.apk"
function Step($text) { Write-Host "`n=== $text ===" -ForegroundColor Cyan } function Step($text) { Write-Host "`n=== $text ===" -ForegroundColor Cyan }
@@ -65,7 +65,7 @@ $unsigned = Join-Path $work "app-unsigned.apk"
--min-sdk-version 24 ` --min-sdk-version 24 `
--target-sdk-version 34 ` --target-sdk-version 34 `
--version-code 5 ` --version-code 5 `
--version-name "2.1.4" ` --version-name "2.1.5" `
--auto-add-overlay --auto-add-overlay
if ($LASTEXITCODE -ne 0) { throw "aapt2 link 失败" } if ($LASTEXITCODE -ne 0) { throw "aapt2 link 失败" }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 55 KiB

+3 -3
View File
@@ -3,7 +3,7 @@
======================================================== ========================================================
软件名称:ykxmz 软件名称:ykxmz
版本:2.1.4 版本:2.1.5
作者:小麦子 作者:小麦子
装好之后看这一页:怎么确认它在跑、怎么改设置、 装好之后看这一页:怎么确认它在跑、怎么改设置、
@@ -197,7 +197,7 @@
八、版本记录 八、版本记录
-------------------------------------------------------- --------------------------------------------------------
2.1.4 2.1.5
* 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍 * 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍
* 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2, * 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2,
合计 10 条连接),一种协议犯病另一种还在服务 合计 10 条连接),一种协议犯病另一种还在服务
@@ -218,6 +218,6 @@
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.4 软件:ykxmz 2.1.5
许可条款见《许可协议》(LICENSE.txt) 许可条款见《许可协议》(LICENSE.txt)
-------------------------------------------------------- --------------------------------------------------------
+2 -2
View File
@@ -3,7 +3,7 @@
======================================================== ========================================================
软件名称:ykxmz 软件名称:ykxmz
版本:2.1.4 版本:2.1.5
作者:小麦子 作者:小麦子
请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的 请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的
@@ -164,6 +164,6 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.4 软件:ykxmz 2.1.5
继续安装,即表示你已阅读并同意《许可协议》。 继续安装,即表示你已阅读并同意《许可协议》。
-------------------------------------------------------- --------------------------------------------------------
+3 -3
View File
@@ -3,7 +3,7 @@
======================================================== ========================================================
软件名称:ykxmz(远程管理工具) 软件名称:ykxmz(远程管理工具)
版本:2.1.4 版本:2.1.5
作者:小麦子 作者:小麦子
生效日期:2026 年 10 月 5 日 生效日期:2026 年 10 月 5 日
@@ -16,7 +16,7 @@
> 都不许拿去做下面第三条那些事。** > 都不许拿去做下面第三条那些事。**
> >
> (早先这版文件写的是"软件许可协议",里面有一条"不得修改"—— > (早先这版文件写的是"软件许可协议",里面有一条"不得修改"——
> 那和 AGPL 冲突,所以 2.1.4 起拆开了:代码授权归 LICENSE, > 那和 AGPL 冲突,所以 2.1.5 起拆开了:代码授权归 LICENSE,
> 使用约束归这份文件。) > 使用约束归这份文件。)
@@ -160,7 +160,7 @@
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.4 软件:ykxmz 2.1.5
如果你不同意本条款的任何一条,请不要安装或使用本软件。 如果你不同意本条款的任何一条,请不要安装或使用本软件。
代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。 代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。
+8 -8
View File
@@ -3,7 +3,7 @@
======================================================== ========================================================
软件名称:ykxmz(远程管理工具) 软件名称:ykxmz(远程管理工具)
版本:2.1.4 版本:2.1.5
作者:小麦子 作者:小麦子
生效日期:2026 年 10 月 5 日 生效日期:2026 年 10 月 5 日
@@ -16,7 +16,7 @@
> 都不许拿去做下面第三条那些事。** > 都不许拿去做下面第三条那些事。**
> >
> (早先这版文件写的是"软件许可协议",里面有一条"不得修改"—— > (早先这版文件写的是"软件许可协议",里面有一条"不得修改"——
> 那和 AGPL 冲突,所以 2.1.4 起拆开了:代码授权归 LICENSE, > 那和 AGPL 冲突,所以 2.1.5 起拆开了:代码授权归 LICENSE,
> 使用约束归这份文件。) > 使用约束归这份文件。)
@@ -160,7 +160,7 @@
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.4 软件:ykxmz 2.1.5
如果你不同意本条款的任何一条,请不要安装或使用本软件。 如果你不同意本条款的任何一条,请不要安装或使用本软件。
代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。 代码授权条款见仓库根目录 LICENSE(GNU AGPL-3.0)。
@@ -173,7 +173,7 @@
======================================================== ========================================================
软件名称:ykxmz 软件名称:ykxmz
版本:2.1.4 版本:2.1.5
作者:小麦子 作者:小麦子
请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的 请你**在安装之前**花三分钟读完这一页。里面有几条很要紧的
@@ -334,7 +334,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.4 软件:ykxmz 2.1.5
继续安装,即表示你已阅读并同意《许可协议》。 继续安装,即表示你已阅读并同意《许可协议》。
-------------------------------------------------------- --------------------------------------------------------
@@ -345,7 +345,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
======================================================== ========================================================
软件名称:ykxmz 软件名称:ykxmz
版本:2.1.4 版本:2.1.5
作者:小麦子 作者:小麦子
装好之后看这一页:怎么确认它在跑、怎么改设置、 装好之后看这一页:怎么确认它在跑、怎么改设置、
@@ -539,7 +539,7 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
八、版本记录 八、版本记录
-------------------------------------------------------- --------------------------------------------------------
2.1.4 2.1.5
* 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍 * 下载改走 Cloudflare 边缘缓存 —— 安装包和 APK 快了几十倍
* 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2, * 跨网络更稳:隧道**两种协议同时跑**(QUIC + HTTP/2,
合计 10 条连接),一种协议犯病另一种还在服务 合计 10 条连接),一种协议犯病另一种还在服务
@@ -560,6 +560,6 @@ ykxmz 是一套自己搭建的远程管理工具,由三部分组成:
-------------------------------------------------------- --------------------------------------------------------
作者:小麦子 作者:小麦子
软件:ykxmz 2.1.4 软件:ykxmz 2.1.5
许可条款见《许可协议》(LICENSE.txt) 许可条款见《许可协议》(LICENSE.txt)
-------------------------------------------------------- --------------------------------------------------------
+2 -2
View File
@@ -27,7 +27,7 @@
; ============================================================================ ; ============================================================================
#define MyAppName "ykxmz" #define MyAppName "ykxmz"
#define MyAppVersion "2.1.4" #define MyAppVersion "2.1.5"
#define MyAppPublisher "小麦子" #define MyAppPublisher "小麦子"
#define MyAppExeName "ykxmz.exe" #define MyAppExeName "ykxmz.exe"
@@ -163,7 +163,7 @@ Filename: "{app}\{#MyAppExeName}"; Parameters: "--autostart on"; \
; 代价是安装器和它是父子关系:用"等整个进程树"的方式调安装器会卡住 ; 代价是安装器和它是父子关系:用"等整个进程树"的方式调安装器会卡住
; (PowerShell 的 Start-Process -Wait 就是)。 ; (PowerShell 的 Start-Process -Wait 就是)。
; 学校批量部署请用 cmd: ; 学校批量部署请用 cmd:
; cmd /c ykxmz-setup-2.1.4.exe /VERYSILENT /CODE=123456 ; cmd /c ykxmz-setup-2.1.5.exe /VERYSILENT /CODE=123456
; cmd 只等安装器本身,不等孙子进程,不会卡。 ; cmd 只等安装器本身,不等孙子进程,不会卡。
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \ Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
Description: "立即启动(后台运行,不显示窗口)"; \ Description: "立即启动(后台运行,不显示窗口)"; \
+1 -1
View File
@@ -59,7 +59,7 @@ from collections import deque
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path from pathlib import Path
VERSION = "2.1.4" VERSION = "2.1.5"
# ---------------- 参数(都能用环境变量覆盖)---------------- # ---------------- 参数(都能用环境变量覆盖)----------------
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 165 KiB

+104 -6
View File
@@ -11,24 +11,115 @@
# #
# 注意:本版本**不再使用系统托盘**(pystray 已移除), # 注意:本版本**不再使用系统托盘**(pystray 已移除),
# 关窗后进程继续运行,靠单实例机制再次双击找回窗口。 # 关窗后进程继续运行,靠单实例机制再次双击找回窗口。
#
# ============================================================================
# ⚠⚠ 必须用 Python 3.8 打包 —— 这是学校电脑能用的前提 ⚠⚠
# ============================================================================
#
# 用户的学校电脑是 **Windows 7**,装完报「dll 缺失」。
#
# 根因不是漏打包,是 **Python 版本越界**:
#
# Python 3.8 -> 最后一个支持 Windows 7 的版本
# Python 3.9+ -> 只支持 Windows 8 及以上
#
# Python 3.9 起用到了 api-ms-win-core-path-l1-1-0.dll 里的
# PathCchCanonicalizeEx,而这个 DLL **Windows 7 上根本不存在**,
# 也没法转发(api set 那套只在 Win8+ 生效)。
#
# 所以用 3.11 打出来的 exe 在 Win7 上必然报缺 DLL。
# 那个 DLL 不是"忘了放进包",是"放进去也没用"——只能在 Win8+ 跑。
#
# 这里选 **32 位** 3.8,因为 32 位在 Win7 的 32 位和 64 位系统上**都能跑**,
# 一个包打天下,学校不用分两种发。
# (64 位的包在 32 位 Windows 上直接跑不起来。)
#
# 脚本会**主动校验**:拿到的 Python 不是 3.8 就直接报错退出,
# 而不是默默打出一个人家装不上的包。
# ============================================================================
$ErrorActionPreference = "Continue" $ErrorActionPreference = "Continue"
$windows = $PSScriptRoot # LanRemote/windows $windows = $PSScriptRoot # LanRemote/windows
$root = Split-Path -Parent $windows # LanRemote $root = Split-Path -Parent $windows # LanRemote
Write-Host "=== 1/3 生成图标 ===" -ForegroundColor Cyan # ---------------------------------------------------------------- 选 Python
& python (Join-Path $root "_build\make_ico.py") Write-Host "=== 0/4 选择 Python(必须是 3.8,为了 Windows 7)===" -ForegroundColor Cyan
function Find-Py38 {
# 1) 环境变量指定
if ($env:YKXMZ_PY -and (Test-Path $env:YKXMZ_PY)) { return $env:YKXMZ_PY }
# 2) 常见安装位置
$cands = @(
"C:\ykxmz-py38\python.exe",
"C:\Python38\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python38\python.exe",
"$env:LOCALAPPDATA\Programs\Python\Python38-32\python.exe",
"C:\Program Files (x86)\Python38-32\python.exe"
)
foreach ($c in $cands) { if (Test-Path $c) { return $c } }
# 3) py launcher
try {
$p = & py -3.8 -c "import sys;print(sys.executable)" 2>$null
if ($LASTEXITCODE -eq 0 -and $p -and (Test-Path $p.Trim())) {
return $p.Trim()
}
} catch { }
return $null
}
$py = Find-Py38
if (-not $py) {
Write-Host @"
✗ 找不到 Python 3.8。
这个项目**必须**用 3.8 打包,因为学校电脑是 Windows 7,
而 Python 3.9 及以上在 Win7 上会因为缺
api-ms-win-core-path-l1-1-0.dll 直接启动失败。
装一个(32 位,Win7 32/64 位都能用):
https://www.python.org/ftp/python/3.8.10/python-3.8.10.exe
静默安装到 C:\ykxmz-py38 :
python-3.8.10.exe /quiet InstallAllUsers=0 TargetDir=C:\ykxmz-py38 ``
Include_pip=1 Include_tcltk=1 Include_test=0 Include_launcher=0
再装依赖:
C:\ykxmz-py38\python.exe -m pip install Pillow==9.5.0 pyinstaller==5.13.2
或者设环境变量 YKXMZ_PY 指向已有的 3.8。
"@ -ForegroundColor Red
throw "需要 Python 3.8"
}
$ver = & $py -c "import sys;print('%d.%d'%sys.version_info[:2])"
$bits = & $py -c "import struct;print(struct.calcsize('P')*8)"
Write-Host " Python: $py"
Write-Host " 版本: $ver ($bits 位)"
if ($ver -ne "3.8") {
throw "打包必须用 Python 3.8(当前 $ver)。用 3.9+ 打出来的包在 Windows 7 上会因为缺 api-ms-win-core-path-l1-1-0.dll 启动失败。"
}
if ($bits -ne "32") {
Write-Host " ⚠ 不是 32 位 —— 64 位的包在 32 位 Windows(不少学校机器是)上跑不起来。" -ForegroundColor Yellow
Write-Host " 建议换成 32 位的 3.8。继续打包……" -ForegroundColor Yellow
}
Write-Host "`n=== 1/4 生成图标 ===" -ForegroundColor Cyan
& $py (Join-Path $root "_build\make_ico.py")
if ($LASTEXITCODE -ne 0) { throw "生成图标失败" } if ($LASTEXITCODE -ne 0) { throw "生成图标失败" }
$ico = Join-Path $windows "app.ico" $ico = Join-Path $windows "app.ico"
Write-Host "`n=== 2/3 PyInstaller 打包(--onedir 多文件)===" -ForegroundColor Cyan Write-Host "`n=== 2/4 PyInstaller 打包(--onedir 多文件)===" -ForegroundColor Cyan
# ⚠ lanremote.ui 必须显式 --hidden-import(见下面那行)。 # ⚠ lanremote.ui 必须显式 --hidden-import(见下面那行)。
# 它是"设置窗口"(tkinter)的实现,而静默那条路不需要它, # 它是"设置窗口"(tkinter)的实现,而静默那条路不需要它,
# PyInstaller 静态分析就找不到 —— 于是双击 exe 想弹设置窗口时 # PyInstaller 静态分析就找不到 —— 于是双击 exe 想弹设置窗口时
# 会 ModuleNotFoundError: No module named 'lanremote.ui', # 会 ModuleNotFoundError: No module named 'lanremote.ui',
# 进程直接退出,用户看到的就是"双击没反应"。 # 进程直接退出,用户看到的就是"双击没反应"。
# (PS 的续行符 ` 后面不能再跟 # 注释,所以说明写在这里。) # (PS 的续行符 ` 后面不能再跟 # 注释,所以说明写在这里。)
& python -m PyInstaller --noconfirm --clean --onedir --windowed ` & $py -m PyInstaller --noconfirm --clean --onedir --windowed `
--name "ykxmz" ` --name "ykxmz" `
--icon "$ico" ` --icon "$ico" `
--paths "$windows" ` --paths "$windows" `
@@ -50,9 +141,16 @@ Write-Host "`n=== 2/3 PyInstaller 打包(--onedir 多文件)===" -Foreground
$target = Join-Path $root "dist_exe\ykxmz\ykxmz.exe" $target = Join-Path $root "dist_exe\ykxmz\ykxmz.exe"
if (-not (Test-Path $target)) { throw "打包失败:未生成 $target" } if (-not (Test-Path $target)) { throw "打包失败:未生成 $target" }
Write-Host "`n=== 3/3 产物 ===" -ForegroundColor Cyan # ---------------------------------------------------------------- Win7 校验
Write-Host "`n=== 3/4 Windows 7 兼容性校验 ===" -ForegroundColor Cyan
& $py (Join-Path $root "_build\check_win7_compat.py")
if ($LASTEXITCODE -ne 0) {
throw "Win7 兼容性校验没过 —— 这个包学校电脑装不上,不该发布。"
}
Write-Host "`n=== 4/4 产物 ===" -ForegroundColor Cyan
Write-Host "目录: $(Split-Path -Parent $target)" Write-Host "目录: $(Split-Path -Parent $target)"
Write-Host ("exe : {0:N1} MB" -f ((Get-Item $target).Length / 1MB)) Write-Host ("exe : {0:N1} MB" -f ((Get-Item $target).Length / 1MB))
$all = Get-ChildItem (Split-Path -Parent $target) -Recurse -File $all = Get-ChildItem (Split-Path -Parent $target) -Recurse -File
Write-Host ("总计: {0} 个文件, {1:N1} MB" -f $all.Count, (($all | Measure-Object Length -Sum).Sum / 1MB)) Write-Host ("总计: {0} 个文件, {1:N1} MB" -f $all.Count, (($all | Measure-Object Length -Sum).Sum / 1MB))
Write-Host "`n完成" -ForegroundColor Green Write-Host "`n完成(Python $ver $bits 位,可在 Windows 7 及以上运行)" -ForegroundColor Green
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""局域网遥控 · Windows 被控端。""" """局域网遥控 · Windows 被控端。"""
__version__ = "2.1.4" __version__ = "2.1.5"
APP_NAME = "ykxmz" APP_NAME = "ykxmz"
+2 -2
View File
@@ -27,12 +27,12 @@ VSVersionInfo(
[ [
StringStruct("CompanyName", "ykxmz"), StringStruct("CompanyName", "ykxmz"),
StringStruct("FileDescription", "ykxmz"), StringStruct("FileDescription", "ykxmz"),
StringStruct("FileVersion", "2.1.4.0"), StringStruct("FileVersion", "2.1.5.0"),
StringStruct("InternalName", "ykxmz"), StringStruct("InternalName", "ykxmz"),
StringStruct("LegalCopyright", "ykxmz"), StringStruct("LegalCopyright", "ykxmz"),
StringStruct("OriginalFilename", "ykxmz.exe"), StringStruct("OriginalFilename", "ykxmz.exe"),
StringStruct("ProductName", "ykxmz"), StringStruct("ProductName", "ykxmz"),
StringStruct("ProductVersion", "2.1.4.0"), StringStruct("ProductVersion", "2.1.5.0"),
], ],
) )
] ]