diff --git a/README.md b/README.md index 0402706..14bbf11 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,9 @@ | 装在哪 | 是什么 | 文件 | |---|---|---| -| 教室电脑(Windows) | 被控端 | `ykxmz-setup-2.1.4.exe` | -| 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-2.1.4.zip` | -| 手表(安卓) | 控制端 | `ykxmz-watch-2.1.4.apk` | +| 教室电脑(Windows) | 被控端 | `ykxmz-setup-2.1.5.exe` | +| 小服务器(随身 WiFi / 云主机 / 树莓派) | 中转站 | `ykxmz-server-2.1.5.zip` | +| 手表(安卓) | 控制端 | `ykxmz-watch-2.1.5.apk` | **只在一个教室用?** 那第三件(中转站)可以先不装,手表和电脑连同一个 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`,整个文件夹拷走就能换台机器用,不写注册表。 ### 第二步:手表上装 App -把 `ykxmz-watch-2.1.4.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。 +把 `ykxmz-watch-2.1.5.apk` 传到手表装上。手表可能提示"未知来源",去设置里允许一下。 打开 App,首页有四个入口: @@ -223,7 +223,7 @@ 中转站就是**一台能上外网的 Linux 小机器**(随身 WiFi、树莓派、云主机都行, Debian 12 / Ubuntu 实测可用,512 MB 内存就够)。 -把 `ykxmz-server-2.1.4.zip` 解压到那台机器上,然后一条命令: +把 `ykxmz-server-2.1.5.zip` 解压到那台机器上,然后一条命令: ```bash bash deploy.sh @@ -456,7 +456,7 @@ windows\build_exe.ps1 ```powershell android\build_apk.ps1 # 流程: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。 diff --git a/_build/CHANGELOG.md b/_build/CHANGELOG.md index f575115..610cf23 100644 --- a/_build/CHANGELOG.md +++ b/_build/CHANGELOG.md @@ -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 - **修一个"填了地址也连不上"的 bug**(手表端)。 diff --git a/_build/bump_version_generic.py b/_build/bump_version_generic.py index d786013..139f56b 100644 --- a/_build/bump_version_generic.py +++ b/_build/bump_version_generic.py @@ -167,6 +167,10 @@ def main() -> int: 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"): path = ROOT / "_tests" / name if not path.is_file(): diff --git a/_build/check_win7_compat.py b/_build/check_win7_compat.py new file mode 100644 index 0000000..5d0a9d0 --- /dev/null +++ b/_build/check_win7_compat.py @@ -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(" 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()) diff --git a/_build/commit_msg_215.txt b/_build/commit_msg_215.txt new file mode 100644 index 0000000..9ace06c --- /dev/null +++ b/_build/commit_msg_215.txt @@ -0,0 +1,62 @@ +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。 diff --git a/_build/icon_preview.png b/_build/icon_preview.png index b292ba9..da3f247 100644 Binary files a/_build/icon_preview.png and b/_build/icon_preview.png differ diff --git a/_build/package_remote.py b/_build/package_remote.py index ba93d45..940d59c 100644 --- a/_build/package_remote.py +++ b/_build/package_remote.py @@ -17,7 +17,7 @@ import zipfile from pathlib import Path ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") -VERSION = "2.1.4" +VERSION = "2.1.5" NAME = f"ykxmz-{VERSION}" AGENT_SRC = ROOT / "dist_exe" / "ykxmz" diff --git a/_build/test_installer_cycle.py b/_build/test_installer_cycle.py index 1d79629..3855f33 100644 --- a/_build/test_installer_cycle.py +++ b/_build/test_installer_cycle.py @@ -19,13 +19,42 @@ r'''安装包完整验收:装 → 验 → 卸 → 再验。 from __future__ import annotations import json +import re import subprocess import sys import time from pathlib import Path 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") DATA = Path(r"C:\ProgramData\ykxmz") EXE = APP / "ykxmz.exe" diff --git a/_tests/test_apk_contents.py b/_tests/test_apk_contents.py index 280b94c..7fda053 100644 --- a/_tests/test_apk_contents.py +++ b/_tests/test_apk_contents.py @@ -23,7 +23,7 @@ def _latest_apk() -> Path: (ROOT / "dist").glob("*watch*.apk"), key=lambda p: p.stat().st_mtime, reverse=True) 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() diff --git a/_tests/test_package_remote.py b/_tests/test_package_remote.py index ccd7387..a3ed3bd 100644 --- a/_tests/test_package_remote.py +++ b/_tests/test_package_remote.py @@ -26,9 +26,9 @@ ROOT = Path(__file__).resolve().parent.parent # 被控端 -> 一个 zip # 服务器端 -> 一个 zip # 控制端 -> 直接给 APK(不打 zip) -AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.4.zip" -SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.4.zip" -WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.4.apk" +AGENT_ZIP = ROOT / "dist" / "ykxmz-agent-2.1.5.zip" +SERVER_ZIP = ROOT / "dist" / "ykxmz-server-2.1.5.zip" +WATCH_APK = ROOT / "dist" / "ykxmz-watch-2.1.5.apk" TOOLCHAIN = Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain") JAVAC = TOOLCHAIN / "jdk17" / "bin" / "javac.exe" JAVA = TOOLCHAIN / "jdk17" / "bin" / "java.exe" @@ -89,7 +89,7 @@ def main() -> int: for label, path in (("被控端", agent_dir / "README-agent.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"" check(f"{label}说明有 UTF-8 BOM", raw[:3] == b"\xef\xbb\xbf", raw[:3]) diff --git a/_tests/verify_all.ps1 b/_tests/verify_all.ps1 index 8f704ac..4931573 100644 --- a/_tests/verify_all.ps1 +++ b/_tests/verify_all.ps1 @@ -481,6 +481,27 @@ $stageOut = & python (Join-Path $root "_tests\check_version_consistency.py") 2>& Show-Fails $stageOut 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 Banner "3a5/7 局域网/中转对比基准(防测试自己骗人)" # 这个基准一度是红的,而且报告里写着「这是中转链路的产品 bug」—— diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 44f5e61..474e7ba 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -2,7 +2,7 @@ + android:versionName="2.1.5">