Files
ykxmz/_build/package_remote.py
T
xmz7891 5fc2f97be7 2.1.3:搬到 Lightsail 真服务器 + 支持"没有证书的中转"
用户买了 AWS Lightsail(2G 内存 / 2 vCPU / 60G SSD / 公网 IP
13.54.30.196),中转从随身 WiFi 那台小机器搬过去。

== 为什么这次搬迁解决了一堆老问题 ==

原机器(Debian aarch64、460MB、走 4G)一直有几件搞不定的:

  * 线路对 Cloudflare 隧道的 **7844 端口间歇性阻断**(443 通、7844 不通),
    隧道每 3~10 分钟掉一次、公网跟着 530 —— 查了很久才从
    cloudflared 自己的 precheck 里看清
  * 只有 460MB 内存
  * 会掉电硬挂(实测 2026-10-08 18:42 一次非正常关机)

新机器**有公网 IP**,中转直接监听,不经隧道、不过 7844。
实测公网延迟从 **1000~2000 ms 降到 1.1 ms**。

== 代码改动 ==

1. **relay.py 的 DATA_DIR 默认值不再写死**
   原来是 /root/lanremote-relay。新机器上服务以 admin 普通用户跑
   (它没理由要 root),而 /root 是 0700 —— 部署时直接
   PermissionError: '/root/lanremote-relay'。
   改成"本脚本所在目录下的 data/",搬到哪都跟着走。

2. **客户端支持"没有证书的中转"**
   新服务器是明文 HTTP(没签证书),而两个客户端原来都往 https 走:
     * 手表端:裸 IP 会被强制补 https:// -> 永远连不上,
       报的还是 SSL 错误,很容易让人白查半天证书
     * Android 9+ 默认禁止明文流量,没 usesCleartextTraffic
       请求会被系统直接掐掉
   Windows 端本来就是对的(正则里把任意纯 IP 都当"用明文")。
   现在手表端规则和它一致,并加了 usesCleartextTraffic。

3. 清掉电脑端一段函数内的重复代码(return 之后还有一份拷贝,
   缩进仍在函数体内所以不报错,但读起来很迷惑)。

== 新服务器上的部署 ==

    /opt/ykxmz-relay/            服务端(relay/admin/landing/showcase)
    ykxmz-relay.service          监听 0.0.0.0:13245
    ykxmz-monitor.service/.timer 每 60 秒查公网,连续 3 次不通就叫预案
    rescue-lightsail.sh          四级阶梯抢救(重启中转 / 强杀重拉 /
                                 查防火墙 / 重启整机[默认关])

  预案针对新架构重写过:原来围着隧道转(重启隧道、重施 MSS/MTU),
  现在没有隧道,改成重启中转 + 查端口/防火墙。

全量回归 46 个阶段全部通过。
2026-10-08 22:29:56 +08:00

685 lines
26 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 -*-
"""打「跨网络版」的三件套压缩包。
用户要求:压缩包里三个东西
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 = "2.1.3"
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,直接装。
【怎么用】四步
**先说最要紧的一条:本软件不带任何中转地址,也不提供官方服务器。**
只在同一个 WiFi 下用(手表 + 电脑连同一个路由器)的话,
跳过第一步和第四步,装完就能连。
要跨网络(在办公室管教室),必须自己搭一台中转站。
第一步:架服务器端(只做一次,**跨网络才需要**)
把「2-服务器端」整个文件夹拷到那台能上外网的 Linux 小机器上,执行:
bash deploy.sh
它会装好中转服务 + 网络调优 + Cloudflare 隧道,并打印一个
**属于你自己的公网地址**。记下这个地址。
(形如 https://你的域名 —— 域名是你自己的,脚本里没有默认域名。)
小提示:忘了地址就在那台机器上执行 lanremote-url,或者浏览器打开
http://<那台机器的IP>:8080/url —— 直接返回当前地址。
第二步:装被控端(电脑上,只做一次)
双击安装包
→ 输入 6 位配对码(自己定,比如 789113)
→ 勾上"开机自启"和"放行防火墙"
→ 点确定,会弹一次管理员确认,之后窗口全消失,后台静默运行
第三步:装控制端(手表上)
装 APK
→ 点「自动搜索电脑」(同一个 WiFi 下用这个,不用填地址)
→ 或者点「输入配对码连接」填电脑上那个 6 位码
第四步:跨网络的话,把中转地址填上(**自己搭的那个**)
电脑: ykxmz.exe --relay https://你的域名
手表: 首页点「中转设置」→ 填同一个地址
然后回首页点「输入配对码连接」,输那 6 位数字,连上。
【关于配对码:单设备单配对码】
一台电脑第一次设的配对码会被服务器记住。
以后同一台电脑再报别的码,服务器会**改回它原来那个**。
好处:你不会因为重装/误操作把配对码搞乱,手表永远认得出这台电脑。
一个配对码也不能同时属于两台电脑(服务器会拒绝)。
【服务器端能管什么】
浏览器打开: <中转地址>/?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,点「是」
→ 黑终端一闪,在修防火墙
→ 窗口全消失,转入后台静默运行
(没有托盘图标、没有窗口、没有终端)
【开箱即用:一个地址都不用填(同一个 WiFi 下)】
手表和这台电脑连**同一个路由器**时,直接就能连上,
配对码对上就行 —— 不需要任何服务器、不需要填任何地址。
程序会自己往局域网广播找电脑,所以电脑上装了几块虚拟网卡、
IP 是多少,都不用管。
【跨网络:必须自己搭一台中转站,然后填地址】
⚠ **本软件没有内置任何中转地址,作者也不对外提供中转服务。**
要跨网络用(比如在办公室管教室),你得自己部署一台中转站:
服务端压缩包里有 deploy.sh,一条命令装好,它会打印一个
属于你自己的公网地址。然后把这个地址填给被控端:
ykxmz.exe --relay https://你的域名
只在同一个 WiFi 下用的话,这一节完全不用管。
【支持多个地址,按顺序试】
用分号隔开,第一个通的就用它。推荐把局域网地址也写进去:
ykxmz.exe --relay "192.168.1.50:8080;https://你的域名"
为什么带上局域网地址?
* 快:局域网几十毫秒,绕隧道要一秒多
* 稳:不依赖隧道,隧道抽风也不影响
* 那个地址永远不变,不用管公网地址变没变
地址**不用写 http:// 或 https://**,程序会自己判断:
192.168.x.x / 10.x.x.x / *.local -> 按 http 连(局域网是明文)
域名 -> 按 https 连
看看当前状态(会显示用了哪个地址、有几个候选):
ykxmz.exe --relay-status
关掉中转(只走局域网直连):
ykxmz.exe --relay ""
【忘了中转地址?】
如果那台小机器在你身边(同一个网络),浏览器打开:
http://<它的IP>:8080/url
它会直接返回当前的公网地址(纯文本,不用登录)。
或者在那台机器上执行:
lanremote-url
小技巧:把局域网地址也填进 --relay,就永远不用管公网地址变没变:
ykxmz.exe --relay "192.168.1.50:8080;https://你的域名"
【配对码被别人占了会弹通知】
每台电脑第一次设的配对码会存到**中转服务器**上。
如果另一台电脑也想用同一个码:
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 deploy.sh
它会一口气做完这些,不用你一步步敲:
1. 检查并安装 cloudflared(已经装了就用现成的)
2. 把中转程序装到 /root/lanremote-relay/
3. 网络调优(keepalive / MSS / MTU / 开机加速)
4. 注册 systemd 服务并设成开机自启
(中转、网络调优、两份隧道、看门狗)
5. 配好 Cloudflare 隧道
6. 自检,然后把**公网地址**打印出来
── 三种常见情形 ──
① 机器上已经有隧道凭据(/root/.cloudflared/*.json)
直接: bash deploy.sh
② 有 Cloudflare 给的隧道 Token
TUNNEL_TOKEN=eyJhIjoi... bash deploy.sh
(用环境变量而不是 --token,Token 就不会留在 shell 历史里)
③ 换域名 / 只装中转不要隧道
bash deploy.sh --hostname relay.你的域名
bash deploy.sh --skip-tunnel
看全部参数: bash deploy.sh --help
**重跑 deploy.sh = 升级**,不会把已有配置冲掉:
设备数据、配对码、管理密码、隧道凭据、手工改过的 config.yml
都会原样保留。
【常用命令】
lanremote-status 看整体状态(服务 / 端口 / 公网 / 内存)
lanremote-url 打印当前公网地址
lanremote-key 打印管理钥匙
systemctl status lanremote-relay
systemctl status lanremote-tunnel
journalctl -u lanremote-relay -f
【关于 install.sh(旧版脚本,仍在包里)】
装匿名快速隧道,公网地址每次重启都变。
新机器建议用 deploy.sh(具名隧道,地址固定)。
install.sh 保留是为了兼容已经用它装过的机器。
【地址老变怎么办?记住一个局域网地址就行】
匿名隧道的公网地址每次重启都会变。查当前地址:
浏览器打开 http://<随身WiFi的IP>:8080/url
它会直接返回当前的公网地址,纯文本,不用登录。
具名隧道(deploy.sh 装的)地址是固定的,不用管这个。
【管理】
官网: <公网地址>/
管理页: <公网地址>/?key=<lanremote-key 的输出>
管理密码: 120830(存在 data/admin_password.txt,可改)
能看到:设备列表、在线状态、配对码、流量、会话数
能操作:踢会话、抹掉配对码绑定
【下载区:静态文件走 Cloudflare 边缘缓存】
官网页面上的三个文件(安装包 / 服务端 zip / APK)和产品截图,
都由 Cloudflare 的边缘节点**直接发送**,不用回源到这台小机器。
这不是默认行为 —— relay.py 给这些路径发了
`Cache-Control: public, max-age=604800, immutable`
(下载文件名带版本号,同一名字内容永不改变,所以能长缓存)。
为什么重要:这台机器通常在 4G 上,回源吞吐实测只有 3~67 KB/s,
15 MB 的安装包根本下不动。走边缘之后实测 695 KB/s,快四十多倍,
而且**不消耗这台机器的流量**。
换了新版本的安装包怎么办?
文件名里带版本号(ykxmz-setup-1.8.2.exe),新文件是新 URL,
自然就是新的缓存条目,不会拿到旧的。
旧版本的条目到期(7 天)后自动消失。
【网络调优:这些参数都是实测调出来的】
deploy-tune.sh 会做这五件事,改动之前先看它开头的说明:
① TCP keepalive 30/10/3
这条 4G 线路会**静默丢连接**(不发明文 FIN/RST)。
Linux 默认要 7200 秒才发现死链,表现就是"过几分钟突然 502"。
改成 30 秒首发探测之后,最快 60 秒就能发现并重建。
② tcp_mtu_probing = 1
接口写着 MTU 1500,但实际路径扛不住(1472 通、1500 全丢)。
不打开探测的话,新建 TLS 连接约 30% 失败。
③ MSS 钳制到 1360
只限制 TCP 段,不动链路层。
实测:不夹 87%,夹到 1360/1400 都是 100%。
④ 接口 MTU 1400
**这一条是专门给 QUIC 隧道的**:MSS 钳制走 iptables 的
TCPMSS 目标,只对 TCP 生效;QUIC 跑 UDP,完全不受它保护。
QUIC 的包超过路径 MTU 就会被丢,而 UDP 丢包不回 ICMP,
只能干等握手超时(日志里那句
"failed to dial to edge with quic: timeout: no recent network activity")。
实测把 MTU 从 1500 降到 1400:建齐 6 条连接 28 秒 -> 13 秒。
⑤ mask 掉 systemd-networkd-wait-online
这台机器 wlan0 归 NetworkManager 管,
systemd-networkd-wait-online 傻等一个不归它的网络,
干等 2 分钟超时,把 relay / tunnel 全拖住。
实测:掉电重启后公网恢复 2 分 10 秒 -> 24 秒。
【隧道:两种协议同时在跑(这是故意的)】
deploy.sh 会装**两份**隧道服务,而且**两份都启用**:
lanremote-tunnel QUIC
lanremote-tunnel-h2 HTTP/2
它们跑的是**同一个隧道 ID**,Cloudflare 会把请求分摊到两个实例
注册上来的全部连接上(合计 10 条,横跨两种协议)。
为什么要这么干 —— 两种协议各有各的病,实测:
QUIC 尾部延迟好(P90 约 2.5 秒),
但建连会 "no recent network activity" 超时,偶发失败
HTTP/2 建连可靠(走 TCP,受 MSS 钳制保护),
但尾部延迟差(P90 约 14 秒)——
TCP 队头阻塞:一条连接上丢一个包,所有流一起卡
两个同时跑 = 协议级故障隔离。QUIC 那半边犯病时 HTTP/2 还在服务,
反过来也一样。实测连续 25 分钟可用率 100%(单协议时是 96~98%)。
所以**不要**为了"省内存"随手停掉其中一个 —— 停了就退化成单协议了。
真想只留一种(比如这台机器的线路对 UDP 特别不友好,
日志里大量 "Failed to dial a quic connection"):
systemctl disable --now lanremote-tunnel-h2 # 只留 QUIC
systemctl disable --now lanremote-tunnel # 只留 HTTP/2
【改端口 / 改协议 / 换域名】
重跑 deploy.sh,带上参数就行(幂等,不会冲掉已有数据):
bash deploy.sh --protocol http2 # 只跑 HTTP/2(停掉 QUIC 那半边)
bash deploy.sh --protocol quic # 只跑 QUIC
bash deploy.sh # 不传 = 两条一起跑(推荐)
bash deploy.sh --port 9090 # 换监听端口
bash deploy.sh --hostname relay.你的域名
【省内存】
默认给服务设了 MemoryMax(relay 128M / tunnel 160M)。
要再省就把用不上的服务停掉:
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 里没有内置任何中转地址,作者也不对外提供中转服务。**
只在同一个 WiFi 下用的话,不用填任何地址 —— 点「局域网连接」就行。
要跨网络,你必须先自己部署一台中转站(服务端 zip 里有 deploy.sh
一键脚本),拿到它打印的公网地址,填进「中转设置」。
电脑那边也要填同一个地址:ykxmz.exe --relay https://你的域名
两个地址填得不一样是连不上的 —— 这是最常见的坑。
【连不上时看这个】
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 / .ico / .service / .timer 都带上。
# 后缀白名单:
# .py 服务本体
# .sh 部署/运维脚本
# .ico 管理页图标(漏了的话新设备上管理页是默认样式)
# .service /
# .timer systemd 单元。**这两个是后加的** ——
# 加监测单元的时候才发现白名单里没有它们,
# 于是 monitor.sh 进了包、拉它的 timer 却没进,
# 装到新机器上监测根本不会启动(而且不会报错,只是静默不跑)。
wanted = sorted(
[f for f in RELAY_SRC.iterdir()
if f.suffix in (".py", ".sh", ".ico", ".service", ".timer")
and f.is_file()],
key=lambda f: f.name)
for src in wanted:
shutil.copy2(src, server_dir / src.name)
# ⚠ 产品截图是个**目录**,上面那个 iterdir() 只收文件、而且不递归,
# 所以 site_shots/ 一直没被打进服务端包 ——
# 装到新机器上官网的截图全是 404。
# 这个是拿「从 zip 解压出来真跑一遍」的端到端测试才发现的。
shots_src = RELAY_SRC / "site_shots"
if shots_src.is_dir():
shutil.copytree(shots_src, server_dir / "site_shots")
else:
print(" [警告] relay/site_shots 不存在,官网截图会缺失")
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}")
# 一键部署脚本必须进包 —— 用户拿到的 zip 里要有它才能双击/一条命令跑起来
for must in ("deploy.sh", "deploy-tune.sh"):
if not (server_dir / must).is_file():
print(f" [失败] 服务器端少了一键部署脚本 {must}")
return 1
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",
"deploy.sh", "deploy-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())