# -*- coding: utf-8 -*- """打「跨网络版」的三件套压缩包。 用户要求:压缩包里三个东西 1. 被控端(Windows 电脑) 2. 服务器端(随身 WiFi / Linux) 3. 控制端(手表 APK) 产物: dist/LanRemote-跨网络版-1.5.0.zip """ from __future__ import annotations import shutil import sys import zipfile from pathlib import Path ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") VERSION = "1.7.0" NAME = f"ykxmz-{VERSION}" AGENT_SRC = ROOT / "dist_exe" / "ykxmz" # 三份正式文档(许可协议 / 安装前必读 / 安装后须知) DOCS = ROOT / "docs" RELAY_SRC = ROOT / "relay" APK_SRC = ROOT / "dist" / f"ykxmz-watch-{VERSION}.apk" # 被控端目录里带的 .bat 工具 TOOLS = ("fxfhq.bat", "wlzj.bat", "xffhq.bat") TOP_README = f"""======================================================== ykxmz {VERSION} · 远程管理工具 ======================================================== 作者:小麦子 先看这三份: LICENSE.txt 许可协议(安装前必看,有使用红线) BEFORE-INSTALL.txt 安装前必读(前信息) AFTER-INSTALL.txt 安装后须知(后信息) 下面是这个包里有哪三个东西、怎么装。 这个包里三个东西,分别装在三台设备上: 1-被控端(电脑) 装在你**要控制的那台 Windows 电脑**上。 双击 ykxmz.exe 就行,不用装 Python。 2-服务器端(随身WiFi) 装在一台能上外网的 Linux 小机器上(你的随身 WiFi)。 它负责把"不在同一个网络"的电脑和手表接起来。 3-控制端(手表) 装在你的安卓手表上。一个 APK,直接装。 【怎么用】三步 第一步:架服务器端(只做一次) 把「2-服务器端」整个文件夹拷到随身 WiFi 上,执行: bash install.sh 它会装好中转服务 + Cloudflare 隧道,并打印一个**公网地址**。 记下这个地址(形如 https://relay.你的域名)。 小提示:公网地址每次重启会变。忘了地址就打开 http://<随身WiFi的IP>:8080/url —— 直接返回当前地址。 第二步:装被控端(电脑上,只做一次) 双击 ykxmz.exe → 输入 6 位配对码(自己定,比如 789113) → 勾上"开机自启"和"放行防火墙" → 点确定,会弹一次管理员确认,之后窗口全消失,后台静默运行 告诉它中转地址(这样才跨网络): ykxmz.exe --relay https://relay.你的域名 第三步:装控制端(手表上) 装 APK → 首页点「中转设置」→ 填同一个中转地址 → 点「自动搜索电脑」(同局域网)或直接输配对码 → 连上 【关于配对码:单设备单配对码】 一台电脑第一次设的配对码会被服务器记住。 以后同一台电脑再报别的码,服务器会**改回它原来那个**。 好处:你不会因为重装/误操作把配对码搞乱,手表永远认得出这台电脑。 一个配对码也不能同时属于两台电脑(服务器会拒绝)。 【服务器端能管什么】 浏览器打开: <中转地址>/?key=... 或者直接用管理密码登录 管理密码:120830(可在服务器上改) 能看:哪些设备在线、配对码、流量、会话数 能做:踢掉某个设备的会话、抹掉某个配对码绑定 注意:中转服务器**只是管道**,它不认识传输内容。 配对、令牌校验、权限判断全部由电脑端自己做 —— 所以就算中转机被攻破,也进不了你的电脑。 【每台设备分别看哪个说明】 电脑: 1-agent/README-agent.txt 随身 WiFi:2-server/README-server.txt 手表: 装完打开 App,首页有「连不上?点这里诊断」 """ AGENT_README = f"""======================================================== 被控端(Windows 电脑) 版本 {VERSION} ======================================================== *** 不需要安装 Python *** ykxmz.exe 是编译好的独立程序, 拷到任何一台 Windows 电脑上双击就能用。 【装】 1) 双击 ykxmz.exe 2) 弹出窗口,输入 6 位配对码(自己定个记得住的,比如 789113) 两个勾: [x] 设置开机自启 [x] 顺便放行防火墙(要管理员确认一次) 3) 点「确定,开始使用」 → 弹一次 UAC,点「是」 → 黑终端一闪,在修防火墙 → 窗口全消失,转入后台静默运行 (没有托盘图标、没有窗口、没有终端) 【开箱即用:一个地址都不用填】 程序里**已经内置了中转地址**,双击就能用。 内置的是两个,用分号隔开,按顺序尝试: 192.168.2.156:8080;https://relay.xxmxxs.top 电脑和随身 WiFi 在同一个网络 -> 走第一个 (局域网直连,几十毫秒,地址永远不变) 电脑在外面 -> 第一个连不上,自动切第二个 (Cloudflare 隧道,600 毫秒左右) 想改成别的当然也可以,见下一节。 【跨网络:告诉它中转地址】 支持**多个地址**,用分号隔开,按顺序尝试,第一个通的就用它。 推荐这样配(局域网优先,断了自动切隧道): ykxmz.exe --relay "192.168.2.156:8080;relay.xxmxxs.top" 为什么局域网优先? * 快:局域网几十毫秒,绕隧道要 600 毫秒左右 * 稳:不依赖隧道,隧道抽风也不影响 * 地址永远不变:不用管隧道地址变来变去 只走隧道也行: ykxmz.exe --relay https://relay.xxmxxs.top 地址**不用写 http:// 或 https://**,程序会自己判断: 192.168.x.x / 10.x.x.x / *.local -> 按 http 连(局域网是明文) 域名 -> 按 https 连 看看当前状态(会显示用了哪个地址、有几个候选): ykxmz.exe --relay-status 关掉中转(只走局域网直连): ykxmz.exe --relay "" 【忘了中转地址?】 如果随身 WiFi 在你身边(同一个网络),浏览器打开: http://<随身WiFi的IP>:8080/url 它会告诉你当前的公网地址(纯文本,不用登录)。 小技巧:把局域网地址也填进 --relay,电脑在家里就走局域网, 完全不用管隧道地址变没变: ykxmz.exe --relay "192.168.2.156:8080;relay.xxmxxs.top" 【配对码被别人占了会弹通知】 每台电脑第一次设的配对码会存到**中转服务器**上。 如果另一台电脑也想用同一个码: 1. 服务器拒绝它 2. 那台电脑上**弹通知**: 「该配对码已被使用,请换一个配对码」 3. 它停下等你换码(不会反复弹、不会刷屏) 4. 你在设置里换个新码,它自动接着连上 所以一个配对码同时只能有一台电脑用。 【配对码:单设备单配对码】 这台电脑第一次设的配对码会被服务器记住。 以后就算你重设成别的,服务器也会改回原来那个 —— 手表永远认得出这台电脑。 相关: --new-code 换一组随机配对码(服务器会按上面的规则处理) --reset-devices 清空已配对的手表 【目录里这几个文件】 ykxmz.exe 主程序 wlzj.bat 网络自检(连不上时跑它) xffhq.bat 一键修复防火墙(右键以管理员运行) fxfhq.bat 放行脚本(程序自动生成的) 【其它命令行】 --setup 重新打开设置窗口 --silent 直接静默运行 --add-device 10 静默运行 + 开放 10 分钟配对 --autostart on|off 开关开机自启 --status 看当前状态 --netcheck 90 网络自检(同 wlzj.bat) --fix-firewall 提权修复防火墙 【连不上?按这个顺序】 1) 任务管理器里能看到 ykxmz.exe 吗?没有就 --setup 2) 右键 xffhq.bat → 以管理员身份运行 3) 双击 wlzj.bat 做网络自检 4) 手表上点「连不上?点这里诊断」 5) 跨网络的话,确认 --relay-status 显示在线 """ RELAY_README = f"""======================================================== 服务器端(随身 WiFi / Linux) 版本 {VERSION} ======================================================== 作用:让**不在同一个网络**的电脑和手表连上。 手表 ──HTTPS──┐ ├──> Cloudflare ──> 本机 :8080 电脑 ──HTTPS──┘ 本程序是**纯标准库**的 Python,不装任何第三方包。 内存占用约 20MB,加 cloudflared 共约 60MB —— 512MB 的小机器跑得动。 【装】 把本文件夹拷到设备上,然后: bash install.sh 它会: 1. 检查 python3 / cloudflared(没有就下载) 2. 装两个 systemd 服务并设为开机自启: lanremote-relay 中转服务 lanremote-tunnel Cloudflare 隧道 3. 打印公网地址和管理页地址 【常用命令】 lanremote-url 打印当前公网地址 lanremote-key 打印管理钥匙 systemctl status lanremote-relay systemctl status lanremote-tunnel journalctl -u lanremote-relay -f 【地址老变怎么办?记住一个局域网地址就行】 匿名隧道的公网地址每次重启都会变。查当前地址: 浏览器打开 http://<随身WiFi的IP>:8080/url 比如 http://192.168.2.156:8080/url 它会直接返回当前的公网地址,纯文本,不用登录。 下面还有两个命令(SSH 上去用): lanremote-url 打印当前公网地址 systemctl restart lanremote-tunnel 换个地址(超时的时候用) 【管理】 管理页: <公网地址>/?key= 管理页顶部也会大字显示"当前公网地址"。 管理密码:120830(存在 data/admin_password.txt,可改) 能看到:设备列表、在线状态、配对码、流量、会话数 能操作:踢会话、抹掉配对码绑定 【重要:这几个参数都是实测调出来的】 install.sh 里给 cloudflared 加了: --edge-ip-version 4 只走 IPv4 --ha-connections 1 并发连接数 --retries 5 连边缘失败时多重试 为什么(都是踩出来的,不是拍脑袋): ① IPv6:这条线路**分配了 IPv6 地址但实际不通**, 而 Cloudflare 边缘默认优先 IPv6,会导致建隧道一直超时 (日志里是 context deadline exceeded)。 ② 协议:**用默认的 QUIC,不要写 --protocol http2**。 实测这条线路把 HTTP/2 用的 TCP 7844 挡了: TLS handshake with edge error: read tcp ...:7844: i/o timeout 而 QUIC 走 UDP 7844 是通的。 (早期版本这里误写成 --protocol http2,隧道完全起不来。) ③ 连接数:用 1(默认是 4)。这条线路对并发 UDP 很敏感: 链路好时 ha=4 只成功注册 1 条(9/10),ha=2 是 10/10; 链路退化时 ha=2 掉到 0/10(一条失败重试把另一条也拖下水), 而 ha=1 是 15/15 + 10/10(100%)。 多一条连接不是多一份冗余,而是多一份争抢。 冗余交给看门狗(每 30 秒查一次,断了就重启)更靠谱。 换了网络环境可以试着去掉这些参数用默认值。 【关于隧道地址会变】 默认用的是 Cloudflare **匿名快速隧道**,地址每次重启会变。 想固定地址要用**具名隧道**(需要你自己的域名): cloudflared tunnel login # 浏览器点授权 cloudflared tunnel create lanremote cloudflared tunnel route dns lanremote relay.你的域名 # 然后把 lanremote-tunnel.service 的 ExecStart 换成: # cloudflared tunnel --no-autoupdate run lanremote 具名隧道不受匿名隧道限流,稳定性完全不是一个级别。 【省内存】 默认给两个服务各设了 MemoryMax=128M。 要再省就把用不上的服务停掉: systemctl stop mariadb php8.2-fpm apache2 systemctl disable mariadb php8.2-fpm apache2 """ 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}") 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 都带上。 # 后缀白名单:.py / .sh 是服务本体,.ico 是管理页图标。 # 漏掉 .ico 的话,装到新设备上管理页又会变成没有图标的默认样式。 wanted = sorted( [f for f in RELAY_SRC.iterdir() if f.suffix in (".py", ".sh", ".ico") 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 if __name__ == "__main__": sys.exit(main())