# -*- coding: utf-8 -*- r"""把打包改成"三端分开出"。 用户要求: "三端分别打包,控制端不用打包成 zip,直接上传 apk" 所以产物是三个文件(原来是一个大 zip): dist/ykxmz-agent-1.5.0.zip 被控端(Windows) dist/ykxmz-server-1.5.0.zip 服务器端(随身 WiFi / Linux) dist/ykxmz-watch-1.5.0.apk 控制端(手表,直接给 APK 不打 zip) 三个包里都带上三份文档(许可协议 / 安装前必读 / 安装后须知), 这样用户拿到哪一个都知道自己在装什么、有什么红线。 """ from __future__ import annotations import re import sys from pathlib import Path ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") PKG = ROOT / "_build" / "package_remote.py" # 从 "def main() -> int:" 起,到文件末尾的 if __name__ 之前,全换成新的 NEW_MAIN = '''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}") return 1 if not APK_SRC.is_file(): print(f" [失败] 找不到 APK: {APK_SRC}") return 1 dist = ROOT / "dist" dist.mkdir(parents=True, exist_ok=True) work = dist / "_build_tmp" if work.exists(): shutil.rmtree(work, ignore_errors=True) 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")) for tool in TOOLS: if not (agent_dir / tool).is_file(): src = ROOT / "tools" / tool 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. 服务器端 ================= 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, 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. 控制端(直接给 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) # ================= 汇总 ================= print() print(" ================ 产物 ================") ok = True for label, path, count in results: if not path.is_file(): print(f" {label}: ✗ 缺失!") ok = False continue 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() 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)} 个条目,完整") with zipfile.ZipFile(results[0][1]) as archive: names = archive.namelist() 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 ''' # 手表说明单独抽出来(原来嵌在 main 里) WATCH_README_BLOCK = '''WATCH_README = f"""======================================================== 控制端(安卓手表) 版本 {VERSION} ======================================================== 【装】 把 ykxmz-watch-{VERSION}.apk 传到手表上安装。 手表如果提示"未知来源",在设置里允许一下。 【连】 1) 打开 App,首页有四个东西: 中转设置(跨网络用) 填服务器端给的地址 局域网连接(同一个WiFi)同一个 WiFi 时走这条,最快 输入配对码连接 经中转连 服务器:还没测 点右边「检查服务器」看延迟 2) 电脑和手表在同一个 WiFi -> 点「局域网连接」,自动搜到电脑,点一下就连 电脑在外面 -> 点「输入配对码连接」,填电脑上那个 6 位码 3) 连上后是 10 个标签页: 媒体 屏幕 文件 语音 电脑 网络 系统 教室 效率 娱乐 【连不上时看这个】 App 会把错误翻译成人话: "证书校验失败" -> 手表系统太旧或网络被劫持 "证书时间不对" -> 把手表的日期时间调成自动获取 "连接超时" -> 网络慢或被防火墙挡 "电脑拒绝连接" -> 电脑上 ykxmz 没开 "域名解析不了" -> 手表没联网 首页的「检查服务器」能测出到中转的延迟(快/一般/慢三档配色)。 【回退】 系统页最上面三个按钮: 回退上一步 撤销刚才那一步 回退全部 把能撤的都撤了 看能回退什么 列出还能撤哪些 能撤的:所有开关、所有画面(全屏大字/投图/通知等) 撤不了的:关机、重启、删文件、杀进程 —— 这些**不会**出现在回退列表里, 免得你以为什么都能退回去。 """ ''' WATCH_PATTERN = re.compile(r'WATCH_README = f""".*?"""\n\n\n', re.S) def main() -> int: text = PKG.read_text(encoding="utf-8") if "ykxmz-agent-" in text: print(" 已经改过") return 0 # 1. 把 WATCH_README 提到顶层的常量 # 原来它是嵌在 main() 里 write_readme 的第三个参数 old_watch = re.search( r' write_readme\(watch_dir / "README-watch\.txt", f"""' r'.*?"""\)\n', text, re.S) if old_watch: text = text[:old_watch.start()] + text[old_watch.end():] print(" 摘掉了 main 里内嵌的手表说明") else: print(" [提示] 没找到内嵌的手表说明(可能已改)") # 2. 插入 WATCH_README 常量(放在 RELAY_README 之后) anchor = 'def write_readme(path: Path, text: str) -> None:' if "WATCH_README =" not in text and anchor in text: text = text.replace(anchor, WATCH_README_BLOCK + anchor, 1) print(" 加了 WATCH_README 常量") # 3. 换掉 main() start = text.find("def main() -> int:") end = text.find('if __name__ == "__main__":') if start < 0 or end < 0 or end < start: print(" [失败] 找不到 main() 的边界") return 1 text = text[:start] + NEW_MAIN + text[end:] print(" 替换了 main()(改成出三个产物)") PKG.write_text(text, encoding="utf-8", newline="\n") import ast try: ast.parse(text) print(" package_remote.py 语法 OK") except SyntaxError as exc: print(f" 语法错误: 行 {exc.lineno}: {exc.msg}") return 1 return 0 if __name__ == "__main__": sys.exit(main())