Files
ykxmz/_tests/test_apk_contents.py
T
xmz7891 4295b482a1 2.0.0:安装时可设中转服务器 + 手表端新增设置页 + 中转默认留空
按用户定的版本规矩,这一轮既有新内容又有重构,所以升第二位:
1.9.0 -> 2.0.0(最后一位清 0)。

== 一、安装时就能设中转服务器(用户要求)==

安装向导那一页现在是两个输入框:配对码 + 中转服务器。
装的时候是"第一次设置",最该一次把地址填好,不然装完还得去
双击 exe 找设置窗口,很容易漏。

只填域名就行(自动补 https://),留空 = 只走局域网。
批量静默部署同样支持:
    ykxmz-setup-2.0.0.exe /VERYSILENT /CODE=246810 /RELAY=relay.你的域名

为此新增了一个 --relay-url 参数:
    --relay       连接一次、打印状态、然后退出(人工排查用)
    --relay-url   只写进配置,不连接、不退出(安装流程用)
用错那个的话,安装包的命令链会在中间就 return,后面的 --silent
根本跑不到 —— 所以必须分开。

== 二、手表端新增「设置」页(用户要求)==

用户原话:"安卓端加一个设置,各种设置就在这里面调"。
以前设置散在首页(只有一个"中转设置"按钮),现在统一收进设置页:
    * 中转服务器(填写 / 一键清空)
    * 记住的配对码(显示 / 清除)
    * 已配对的电脑(看数量 / 清空,带二次确认)
    * 关于(版本号从 APK 读,不会和代码对不上)

== 三、中转地址默认留空 + 一次性迁移(用户报的问题)==

用户原话:"安卓端为什么还是默认填的我的中转站,以后改成手动填"。

代码里的默认值早就是空的了,但用户看到的还是那个地址 ——
因为**老版本 1.5.0 把作者的域名硬编码成了默认值**,第一次运行就
存进了 SharedPreferences。装新 APK 不会清偏好,所以那个值一直留着。
用户其实从来没"选"过这个地址,是程序硬塞的。

所以加了 migrateLegacyRelay():存的值如果正好等于当时内置的域名,
就当成没设过、清掉。只清完全相等的,用户手填的不会动。

== 四、两个测试自身的毛病 ==

* test_lan_optimize 那条"没有把旧公网域名当默认值"太粗 ——
  它查的是源码里任何位置,于是迁移注释里引用那行老代码就把它判红了。
  改成先剥掉注释再查(断言要测的是"有没有把它当默认值")。

* test_image_modes 偶发红:它开真实 Tk 全屏窗口,在全量套件里跑
  偶尔起不来(单独跑 19/19 全过)。加了有界重试。
  偶发红的测试会让"全量通过"失去意义。

== 五、新增防复发断言 ==

_tests/test_relay_default_blank.py(15 项,回归阶段 3a3b):
    * 默认值必须是空串
    * 除了迁移清单,源码里不许有写死的中转域名
    * 迁移必须真的被调用(不是只定义)
    * 迁移必须是 remove,不能换成另一个硬编码值
    * Windows 端的默认也得是空

理由:用户说过"这个中转站我就自己用,不对外" —— 值得用断言钉死。

== 六、顺手修的 ==

* release_all.py 崩在 Gitee 那一步:Gitee 的
  GET /releases/tags/{tag} 在"标签存在、但还没发行版"时返回的是
  **HTTP 200 + JSON null**(不是 404),于是 .get("id") 直接
  AttributeError。发 1.9.0 时踩到,已改成容忍 None。

* installer/build_installer.py 里 license-combined.txt 是从
  docs/ 三份文档重新生成的,所以光改生成物没用(下次构建又变回去)。
  改成在 build_license() 里统一注入当前版本。

* ExE 文件属性里的版本(version_info.txt)从 1.7.1 起就一直没跟上。

全量回归 43 个阶段全部通过。
2026-10-07 23:02:44 +08:00

148 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""检查手表 APK 是否真的把「遥控诊断」合并进去了。
合并前是两个 APK(手表端 + 独立的遥控诊断 App)。
合并后只需要一个:主程序里有「设备诊断」按钮。
这里从 APK 的 dex 和清单里核实,而不是只信源码。
"""
from __future__ import annotations
import re
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# 别写死版本号 —— 一升级就坏(这坑踩过好几回了)。
# 直接扫 dist 里最新的手表 APK。
def _latest_apk() -> Path:
candidates = sorted(
(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.0.0.apk"
APK = _latest_apk()
DIAG_APK = ROOT / "dist" / "LanRemote-Diag-1.1.0.apk"
AAPT = (Path(r"C:\Users\shu\Desktop\deepseekagent\starklink\toolchain")
/ "android-sdk" / "build-tools" / "34.0.0" / "aapt.exe")
PASSED = 0
FAILED = 0
def check(name: str, ok: bool, detail: str = "") -> None:
global PASSED, FAILED
if ok:
PASSED += 1
print(f" [PASS] {name}")
else:
FAILED += 1
print(f" [FAIL] {name} {detail}")
def main() -> int:
print("=" * 68)
print("一、手表 APK 里有没有内置诊断")
print("=" * 68)
if not APK.is_file():
print(f" 找不到 {APK}")
print(" 请先运行 android\\build_apk.ps1")
return 1
with zipfile.ZipFile(APK) as archive:
dex = archive.read("classes.dex")
names = {item.decode() for item in
re.findall(rb"Lcom/lanremote/[A-Za-z0-9_/]+;", dex)}
print(f" dex 中的类: {len(names)} 个")
check("APK 里有 Diagnostics 类(诊断逻辑已内置)",
"Lcom/lanremote/watch/Diagnostics;" in names,
str(sorted(names)))
check("APK 里有 DiscoveryCore(诊断与主程序共用同一份发现实现)",
"Lcom/lanremote/watch/DiscoveryCore;" in names)
check("APK 里有 MainActivity", "Lcom/lanremote/watch/MainActivity;" in names)
# 注意:不能用 "diag" 做子串判断 —— Diagnostics 里也含 diag。
# 要查的是旧的独立包名 com/lanremote/diag/。
legacy = [n for n in names if n.startswith("Lcom/lanremote/diag/")]
check("不含旧的独立诊断包 com.lanremote.diag",
not legacy, str(legacy))
print()
print("=" * 68)
print("二、清单权限(诊断读 WiFi 名称需要定位权限)")
print("=" * 68)
# aapt 不认中文路径,先拷到纯 ASCII 路径
with tempfile.TemporaryDirectory() as tmp:
simple = Path(tmp) / "watch.apk"
simple.write_bytes(APK.read_bytes())
try:
proc = subprocess.run(
[str(AAPT), "dump", "badging", str(simple)],
capture_output=True, timeout=60)
badging = proc.stdout.decode("utf-8", errors="replace")
except Exception as exc: # noqa: BLE001
badging = ""
print(f" aapt 调用失败: {exc}")
if badging:
for permission, why in (
("android.permission.ACCESS_FINE_LOCATION", "读 WiFi 名称"),
("android.permission.INTERNET", "网络通信"),
("android.permission.CHANGE_WIFI_MULTICAST_STATE", "UDP 广播"),
):
check(f"声明了 {permission.split('.')[-1]}({why})",
permission in badging)
check("没有申请定位以外的敏感权限",
"READ_CONTACTS" not in badging
and "READ_SMS" not in badging
and "CAMERA" not in badging
and "RECORD_AUDIO" not in badging)
else:
check("能读取 APK 清单", False, "aapt 没有输出")
print()
print("=" * 68)
print("三、构建脚本不再依赖独立诊断 APK")
print("=" * 68)
package = (ROOT / "_build" / "package.py").read_text(encoding="utf-8")
check("package.py 不再要求 Diag APK",
"DIAG_APK" not in package, "仍引用 DIAG_APK")
check("package.py 说明里写明诊断已合并",
"合并" in package)
src = (ROOT / "android" / "src" / "com" / "lanremote" / "watch"
/ "MainActivity.java").read_text(encoding="utf-8")
# 用户要求:"安卓端中转设置下面,把那个设备诊断给删了,
# 换成输入配对码,输入对应配对码就可以连接对应电脑"
# 所以这里反过来断言:不该再有设备诊断入口,而是配对码输入。
import re as _re
code = _re.sub(r"//[^\n]*", "", src)
check("主界面不再有「设备诊断」入口", "设备诊断" not in code)
check("主界面不再调用 Diagnostics.build",
"Diagnostics.build(" not in code)
check("主界面不再调用 Diagnostics.manualTest",
"Diagnostics.manualTest(" not in code)
check("换成了「输入配对码连接」入口",
'bigButton("输入配对码连接"' in code)
check("配对码连接走中转(connectByCode)",
"private void connectByCode(" in code)
print()
print("=" * 68)
print(f"通过 {PASSED} 项,失败 {FAILED} 项")
print("=" * 68)
return 1 if FAILED else 0
if __name__ == "__main__":
sys.exit(main())