ykxmz 1.5.0:手表端 TLS 证书兼容 + 三端分开打包 + 下载区

手表端
- 内置 GTS Root R4 / GlobalSign Root CA
  修 SSLHandshakeException: Chain validation failed
- 连接错误翻译成人话:证书 / 时间 / 解析 / 超时分别给提示
- 经中转连上后用服务器报的地址刷新已配对记录
- 版本号 1.3.0 -> 1.5.0(AndroidManifest 之前漏改了)

服务器端
- 支持 chunked 请求体,修每 16 秒一次的连接错乱与大面积 502
- 网络调优 net-tune.sh(开机自动应用)
- 新增下载区,管理页加下载卡片
- 隧道改 HTTP/2 + 双连接

打包方式变更
- 从一个合并 zip 改成三个独立产物:
    ykxmz-agent-*.zip    被控端
    ykxmz-server-*.zip   服务器端
    ykxmz-watch-*.apk    控制端
This commit is contained in:
xmz7891
2026-10-06 23:08:05 +08:00
parent edac13dbe2
commit 8111fb6d97
20 changed files with 3770 additions and 2943 deletions
+154 -135
View File
@@ -352,11 +352,88 @@ RELAY_README = f"""========================================================
"""
WATCH_README = f"""========================================================
控制端(安卓手表) 版本 {VERSION}
========================================================
【装】
把 ykxmz-watch-{VERSION}.apk 传到手表上安装。
手表如果提示"未知来源",在设置里允许一下。
【连】
1) 打开 App,首页有四个东西:
中转设置(跨网络用) 填服务器端给的地址
局域网连接(同一个WiFi)同一个 WiFi 时走这条,最快
输入配对码连接 经中转连
服务器:还没测 点右边「检查服务器」看延迟
2) 电脑和手表在同一个 WiFi -> 点「局域网连接」,自动搜到电脑,点一下就连
电脑在外面 -> 点「输入配对码连接」,填电脑上那个 6 位码
3) 连上后是 10 个标签页:
媒体 屏幕 文件 语音 电脑 网络 系统 教室 效率 娱乐
【连不上时看这个】
App 会把错误翻译成人话:
"证书校验失败" -> 手表系统太旧或网络被劫持
"证书时间不对" -> 把手表的日期时间调成自动获取
"连接超时" -> 网络慢或被防火墙挡
"电脑拒绝连接" -> 电脑上 ykxmz 没开
"域名解析不了" -> 手表没联网
首页的「检查服务器」能测出到中转的延迟(快/一般/慢三档配色)。
【回退】
系统页最上面三个按钮:
回退上一步 撤销刚才那一步
回退全部 把能撤的都撤了
看能回退什么 列出还能撤哪些
能撤的:所有开关、所有画面(全屏大字/投图/通知等)
撤不了的:关机、重启、删文件、杀进程 —— 这些**不会**出现在回退列表里,
免得你以为什么都能退回去。
"""
def write_readme(path: Path, text: str) -> None:
"""说明文档统一写成 UTF-8 with BOM —— Windows 记事本不加 BOM 会认成 GBK。"""
path.write_bytes(b"\xef\xbb\xbf" + text.replace("\n", "\r\n").encode("utf-8"))
def _zip_dir(zip_path: Path, source: Path, arc_root: str) -> int:
"""把一个目录打成一个 zip(内部带一层 arc_root 目录名)。"""
if zip_path.exists():
zip_path.unlink()
count = 0
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED,
compresslevel=6) as archive:
for item in sorted(source.rglob("*")):
if item.is_file():
archive.write(item, Path(arc_root) / item.relative_to(source))
count += 1
return count
def _copy_docs(target: Path) -> int:
"""把三份正式文档拷到目标目录(UTF-8 BOM + CRLF)。"""
done = 0
for name in ("LICENSE.txt", "BEFORE-INSTALL.txt", "AFTER-INSTALL.txt"):
src = DOCS / name
if src.is_file():
write_readme(target / name, src.read_text(encoding="utf-8"))
done += 1
else:
print(f" [警告] 缺少文档 {src}")
return done
def main() -> int:
if not AGENT_SRC.is_dir():
print(f" [失败] 找不到被控端: {AGENT_SRC}")
@@ -365,13 +442,18 @@ def main() -> int:
print(f" [失败] 找不到 APK: {APK_SRC}")
return 1
build = ROOT / "dist" / NAME
if build.exists():
shutil.rmtree(build, ignore_errors=True)
dist = ROOT / "dist"
dist.mkdir(parents=True, exist_ok=True)
work = dist / "_build_tmp"
if work.exists():
shutil.rmtree(work, ignore_errors=True)
# ---------- 1. 被控端 ----------
agent_dir = build / "1-agent"
print(" 复制被控端…")
results = []
# ================= 1. 被控端 =================
print(" [1/3] 被控端(Windows)")
agent_name = f"ykxmz-agent-{VERSION}"
agent_dir = work / agent_name
shutil.copytree(AGENT_SRC, agent_dir,
ignore=shutil.ignore_patterns("data", "received",
"*.idsig", "*.log"))
@@ -381,155 +463,92 @@ def main() -> int:
if src.is_file():
shutil.copy2(src, agent_dir / tool)
write_readme(agent_dir / "README-agent.txt", AGENT_README)
# 老版本往被控端目录塞过一份顶层说明的副本,清掉
for junk in ("先看这个.txt", "使用说明.txt"):
target = agent_dir / junk
if target.is_file():
target.unlink()
_copy_docs(agent_dir)
if not (agent_dir / "ykxmz.exe").is_file():
print(" [失败] 被控端里没有 ykxmz.exe")
return 1
agent_zip = dist / f"{agent_name}.zip"
n = _zip_dir(agent_zip, agent_dir, agent_name)
results.append(("被控端", agent_zip, n))
# ---------- 2. 服务器端 ----------
relay_dir = build / "2-server"
relay_dir.mkdir(parents=True)
print(" 复制服务器端…")
# 文件清单写死容易漏 —— watchdog.sh 就漏过一次。
# ================= 2. 服务器端 =================
print(" [2/3] 服务器端(随身 WiFi / Linux)")
server_name = f"ykxmz-server-{VERSION}"
server_dir = work / server_name
server_dir.mkdir(parents=True)
# 文件清单写死容易漏 —— watchdog.sh / net-tune.sh 都漏过一次。
# 改成:relay 目录里所有 .py / .sh 都带上。
wanted = sorted(
[f for f in RELAY_SRC.iterdir()
if f.suffix in (".py", ".sh") and f.is_file()],
key=lambda f: f.name)
for src in wanted:
shutil.copy2(src, relay_dir / src.name)
write_readme(relay_dir / "README-server.txt", RELAY_README)
shutil.copy2(src, server_dir / src.name)
write_readme(server_dir / "README-server.txt", RELAY_README)
_copy_docs(server_dir)
for must in ("relay.py", "install.sh", "watchdog.sh", "net-tune.sh"):
if not (server_dir / must).is_file():
print(f" [警告] 服务器端少了 {must}")
server_zip = dist / f"{server_name}.zip"
n = _zip_dir(server_zip, server_dir, server_name)
results.append(("服务器端", server_zip, n))
# ---------- 3. 控制端 ----------
watch_dir = build / "3-watch"
watch_dir.mkdir(parents=True)
print(" 复制控制端…")
shutil.copy2(APK_SRC, watch_dir / APK_SRC.name)
write_readme(watch_dir / "README-watch.txt", f"""========================================================
控制端(安卓手表) 版本 {VERSION}
========================================================
# ================= 3. 控制端(直接给 APK)=================
print(" [3/3] 控制端(手表)—— 不打 zip,直接给 APK")
# 顺带把说明也放一份在旁边,方便看
watch_note = dist / f"ykxmz-watch-{VERSION}-说明.txt"
write_readme(watch_note, WATCH_README)
results.append(("控制端", APK_SRC, 1))
【装】
shutil.rmtree(work, ignore_errors=True)
把 {APK_SRC.name} 传到手表上安装。
手表如果提示"未知来源",在设置里允许一下。
【连】
1) 打开 App,首页点「中转设置(跨网络用)」
填服务器端给你的地址,形如 https://relay.你的域名
(不填也能用,但只在同一个 WiFi 下)
2) 点「自动搜索电脑」—— 同一个局域网时能直接搜到
搜不到就点「连不上?点这里诊断」
3) 输 6 位配对码(电脑上设的那个)
4) 连上后是 10 个标签页:
媒体 屏幕 文件 语音 电脑 网络 系统 教室 效率 娱乐
每个页面上半部分是常用按钮,往下滑是更多功能(共 206 个)。
【回退】
系统页最上面三个按钮:
回退上一步 撤销刚才那一步
回退全部 把能撤的都撤了
看能回退什么 列出还能撤哪些
能撤的:所有开关(15 个)、所有画面(26 个,全屏大字/投图/通知等)
撤不了的:关机、重启、删文件、杀进程 —— 这些**不会**出现在回退列表里,
免得你以为什么都能退回去。
""")
# ---------- 4. 顶层说明 ----------
write_readme(build / "README.txt", TOP_README)
# ---------- 4b. 三份正式文档(作者:小麦子)----------
# 放顶层,和 README.txt 并排,解压第一眼就能看到。
# 统一转成 UTF-8 BOM + CRLF —— 不带 BOM 记事本会按 GBK 读,全乱。
doc_files = [
("LICENSE.txt", "许可协议"),
("BEFORE-INSTALL.txt", "安装前必读"),
("AFTER-INSTALL.txt", "安装后须知"),
]
for name, label in doc_files:
src = DOCS / name
if not src.is_file():
print(f" [警告] 缺少文档 {src}")
# ================= 汇总 =================
print()
print(" ================ 产物 ================")
ok = True
for label, path, count in results:
if not path.is_file():
print(f" {label}: ✗ 缺失!")
ok = False
continue
# 读进来(原文件是无 BOM 的 UTF-8),交给 write_readme
# 统一加 BOM + CRLF 后写出
write_readme(build / name, src.read_text(encoding="utf-8"))
print(f" {label}: {name}")
# ---------- 5. 检查 ----------
print()
print(" 目录结构:")
total = 0
for item in sorted(build.rglob("*")):
if item.is_file():
total += 1
for item in sorted(build.iterdir()):
if item.is_dir():
count = sum(1 for _ in item.rglob("*") if _.is_file())
print(f" {item.name}/ ({count} 个文件)")
else:
print(f" {item.name}")
print(f" 合计 {total} 个文件")
# 必须有的东西
required = [
agent_dir / "ykxmz.exe",
relay_dir / "relay.py",
relay_dir / "install.sh",
watch_dir / APK_SRC.name,
build / "README.txt",
build / "LICENSE.txt",
build / "BEFORE-INSTALL.txt",
build / "AFTER-INSTALL.txt",
]
missing = [str(p.relative_to(build)) for p in required if not p.is_file()]
if missing:
print(f" [失败] 少了: {missing}")
size = path.stat().st_size / 1024 / 1024
extra = f"({count} 个文件)" if count > 1 else ""
print(f" {label:6} {path.name:34} {size:6.2f} MB {extra}")
if not ok:
return 1
print(" 三个部分都在")
# ---------- 6. 打包 ----------
# ================= 验证 =================
print()
print(" 压缩中…")
zip_path = ROOT / "dist" / f"{NAME}.zip"
if zip_path.exists():
zip_path.unlink()
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED,
compresslevel=6) as archive:
for item in sorted(build.rglob("*")):
if item.is_file():
archive.write(item, item.relative_to(build.parent))
size = zip_path.stat().st_size / 1024 / 1024
print(f" {zip_path.name} {size:.2f} MB")
print(" 验证压缩包内容:")
for label, path, _count in results:
if path.suffix != ".zip":
continue
with zipfile.ZipFile(path) as archive:
names = archive.namelist()
bad = archive.testzip()
if bad:
print(f" [失败] {path.name} 里有坏文件: {bad}")
return 1
print(f" {path.name}: {len(names)} 个条目,完整")
# ---------- 7. 验证压缩包 ----------
with zipfile.ZipFile(zip_path) as archive:
with zipfile.ZipFile(results[0][1]) as archive:
names = archive.namelist()
bad = archive.testzip()
if bad:
print(f" [失败] 压缩包里有坏文件: {bad}")
return 1
has = {
"被控端": any("ykxmz.exe" in n for n in names),
"服务器端": any("relay.py" in n for n in names),
"控制端": any(".apk" in n for n in names),
"顶层说明": any("README.txt" in n for n in names),
}
for label, ok in has.items():
print(f" {label}: {'✓' if ok else '✗'}")
if not all(has.values()):
return 1
print(f" 共 {len(names)} 个条目,完整")
if not any("ykxmz.exe" in n for n in names):
print(" [失败] 被控端包里没有 ykxmz.exe")
return 1
with zipfile.ZipFile(results[1][1]) as archive:
names = archive.namelist()
for must in ("relay.py", "install.sh", "watchdog.sh", "net-tune.sh"):
if not any(must in n for n in names):
print(f" [失败] 服务器端包里没有 {must}")
return 1
print()
print(" 三个产物齐全")
return 0