ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性

被控端
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
- 每分钟回头找局域网,自动切回快 400 倍的路径
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
- 锁屏时给出可操作的提示

手表端
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
- 经中转连上后用服务器报的地址刷新已配对记录
- 新增局域网连接页面、服务器延迟检测

服务器端
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
- 网络调优脚本 net-tune.sh(开机自动应用)
- 管理页:批量删除、网络体检、下载区
- 隧道改 HTTP/2 + 双连接

文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
This commit is contained in:
xmz7891
2026-10-06 19:01:05 +08:00
commit b62ac865ea
3195 changed files with 203292 additions and 0 deletions
+273
View File
@@ -0,0 +1,273 @@
# -*- coding: utf-8 -*-
"""端到端验证:启动**打包后的 EXE**,配对,然后调 feature_list。
这是唯一能验出"Features 没被构造"这个 bug 的方式 ——
之前的测试都自己 new 一个 Features,所以永远测不出来(真机上功能页是死的)。
"""
from __future__ import annotations
import json
import os
import shutil
import socket
import subprocess
import sys
import tempfile
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
EXE = ROOT / "dist_exe" / "ykxmz" / "ykxmz.exe"
PORT = 8871
PASSED = 0
FAILED = 0
def check(name, ok, detail=""):
global PASSED, FAILED
if ok:
PASSED += 1
print(f" [PASS] {name}")
else:
FAILED += 1
print(f" [FAIL] {name} {detail}")
class Client:
def __init__(self, host, port, timeout=10.0, source_ip=None):
if source_ip:
# 绑定一个具体的源地址,用来模拟"另一台机器"
# (Windows 上整个 127.0.0.0/8 都是回环)
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(timeout)
self.sock.bind((source_ip, 0))
self.sock.connect((host, port))
else:
self.sock = socket.create_connection((host, port), timeout=timeout)
self.sock.settimeout(timeout)
self.buf = b""
def send(self, obj):
self.sock.sendall((json.dumps(obj, ensure_ascii=False) + "\n").encode("utf-8"))
def recv(self, timeout=12.0):
deadline = time.time() + timeout
while b"\n" not in self.buf:
if time.time() > deadline:
raise TimeoutError("等回复超时")
chunk = self.sock.recv(65536)
if not chunk:
raise ConnectionError("连接断开")
self.buf += chunk
line, _, self.buf = self.buf.partition(b"\n")
return json.loads(line.decode("utf-8"))
def call(self, obj, timeout=12.0):
self.send(obj)
return self.recv(timeout)
def close(self):
try:
self.sock.close()
except OSError:
pass
def main() -> int:
if not EXE.is_file():
print(f" 找不到打包好的 EXE: {EXE}")
print(" 先跑 windows\\build_exe.ps1")
return 1
# 用独立的数据目录,别碰用户真实配置
work = Path(tempfile.mkdtemp(prefix="lr_e2e_"))
exe_dir = EXE.parent
# 先把真实 data 目录挪开(EXE 的 CWD 决定 data 位置)
backup = None
real_data = exe_dir / "data"
if real_data.exists():
backup = exe_dir / f"data_backup_{int(time.time())}"
shutil.move(str(real_data), str(backup))
print(f" 已备份真实 data 到 {backup.name}")
proc = None
try:
proc = subprocess.Popen(
# --add-device:静默运行 + 临时开放配对。
# 不能用 --minimized —— 全新数据目录会弹设置窗口卡住。
[str(EXE), "--port", str(PORT), "--add-device", "10"],
cwd=str(exe_dir),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print(f" 已启动 EXE(PID {proc.pid}),等待监听…")
code = None
for _ in range(40):
time.sleep(0.5)
settings = exe_dir / "data" / "settings.json"
if settings.is_file():
try:
code = json.loads(settings.read_text(encoding="utf-8")).get(
"pairing_code")
except Exception: # noqa: BLE001
pass
if code:
try:
probe = socket.create_connection(("127.0.0.1", PORT), timeout=1)
probe.close()
break
except OSError:
continue
check("EXE 起来了并读到配对码", bool(code), f"code={code}")
if not code:
return 1
print()
print("一、配对")
client = Client("127.0.0.1", PORT)
print()
print("一b、★ 配对防爆破(必须在成功配对之前测,"
"否则配对窗口已经关了)")
# 从 127.0.0.2 发起 —— 限速是按 IP 的,
# 用同一个 IP 会把后面的合法配对一起锁掉
breaker = Client("127.0.0.1", PORT, source_ip="127.0.0.2")
wrong = "000000" if code != "000000" else "111111"
limited = False
for i in range(12):
r = breaker.call({"cmd": "pair", "deviceId": f"brute-{i}",
"deviceName": "爆破", "code": wrong}, timeout=10)
if not r.get("ok") and "过多" in str(r.get("error", "")):
limited = True
print(f" 第 {i + 1} 次开始被限速")
break
check("连续错码会被限速", limited,
f"12 次都没触发限速,最后一次: {r}")
breaker.close()
# 限速状态在内存里,重启就清空。
# 重启既能让后续的合法配对不被挡住,也顺带验证了
# "限速不会写进磁盘、重启即恢复"。
print(" 重启被控端以清空限速…")
proc.terminate()
try:
proc.wait(timeout=10)
except Exception: # noqa: BLE001
proc.kill()
time.sleep(1.5)
proc = subprocess.Popen(
[str(EXE), "--port", str(PORT), "--add-device", "10"],
cwd=str(exe_dir),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
restarted = False
for _ in range(30):
time.sleep(0.5)
try:
s = socket.create_connection(("127.0.0.1", PORT), timeout=1)
s.close()
restarted = True
break
except OSError:
continue
check("重启后被控端重新在监听", restarted)
time.sleep(1.0)
# 重启之后必须重新建连接(旧的 socket 已经断了)
client = Client("127.0.0.1", PORT)
reply = client.call({"cmd": "pair", "deviceId": "e2e-test",
"deviceName": "端到端测试", "code": code})
check("配对成功", reply.get("ok") is True, str(reply)[:120])
print()
print("二、★ 功能列表(这是之前完全失败的那一项)")
reply = client.call({"cmd": "feature_list"}, timeout=25)
ok = reply.get("ok") is True
features = reply.get("features") or []
check("feature_list 返回 ok", ok, str(reply.get("error"))[:120])
check("功能数量 > 100", len(features) > 100, f"{len(features)} 个")
if features:
groups = sorted({f.get("group", "?") for f in features})
print(f" 共 {len(features)} 个功能,{len(groups)} 个分组")
print(f" 分组: {'、'.join(groups)}")
print()
print("三、★ 执行一个功能")
reply = client.call({"cmd": "feature_run", "name": "world_clock",
"params": {}}, timeout=20)
check("feature_run 返回 ok", reply.get("ok") is True, str(reply)[:120])
note = str(reply.get("note") or "")
check("世界时钟返回了内容", "北京" in note, note[:80])
if note:
print(f" {note.replace(chr(10), ' | ')}")
print()
print("四、★ 文件功能 + 越权拒绝")
reply = client.call({"cmd": "feature_run", "name": "file_info",
"params": {"path": "C:\\Windows\\win.ini"}}, timeout=15)
check("越权路径被拒绝", reply.get("ok") is False,
f"竟然成功了: {str(reply)[:100]}")
reply = client.call({"cmd": "feature_run", "name": "note_add",
"params": {"text": "端到端测试笔记"}}, timeout=15)
check("记笔记成功", reply.get("ok") is True, str(reply)[:100])
print()
print("五、★ 相对路径不能拉走 settings.json")
reply = client.call({"cmd": "file_pull", "path": "data/settings.json"},
timeout=15)
check("拒绝拉取 data/settings.json", reply.get("ok") is False,
f"竟然成功了: {str(reply)[:100]}")
print()
print("七、★ 分块超限")
big = "A" * (300 * 1024)
try:
client.send({"cmd": "file_push", "seq": 0, "last": False,
"name": "big.bin", "data": big})
client.send({"cmd": "file_push", "seq": 1, "last": False,
"name": "big.bin", "data": big})
r = client.recv(timeout=8)
except Exception as exc: # noqa: BLE001
r = {"error": str(exc)}
check("超限不会让服务端崩(还活着)", True)
reply = client.call({"cmd": "ping"}, timeout=10)
check("服务端仍然响应 ping", reply.get("ok") is True, str(reply)[:80])
client.close()
finally:
if proc is not None:
try:
proc.terminate()
proc.wait(timeout=8)
except Exception: # noqa: BLE001
try:
proc.kill()
except Exception: # noqa: BLE001
pass
time.sleep(1.0)
# 还原真实 data
try:
test_data = exe_dir / "data"
if test_data.exists():
shutil.rmtree(test_data, ignore_errors=True)
if backup is not None and backup.exists():
shutil.move(str(backup), str(real_data))
print(f" 已还原真实 data")
except Exception as exc: # noqa: BLE001
print(f" !! 还原 data 失败: {exc}")
shutil.rmtree(work, ignore_errors=True)
print()
print("=" * 62)
print(f"通过 {PASSED} 项,失败 {FAILED} 项")
return 1 if FAILED else 0
if __name__ == "__main__":
sys.exit(main())