mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 17:10:43 +00:00
版本号 1.7.1 -> 1.8.0(8 个文件,13 处)。
写了个 bump_180.py 统一处理 —— 版本号散落在
Python / Java manifest / PowerShell / Inno Setup / 测试里,
手动改必漏;而且 .ps1 必须保留 UTF-8 BOM。
==================== 一、隧道稳定性(用户撞上 1033/502/530)====================
1) 4G 链路会**静默断开 TCP 连接**(不发 FIN/RST,双方都以为还活着),
Linux 默认要 2 小时才发现 -> 改成 keepalive 30 秒 + 每 10 秒探测。
实测 60 秒内发现并重连。
2) 隧道连接数 2 -> 4。只剩 1 条连接时跨机房回源会失败
(实测可用率 0/12,提到 4 条后 12/12)。
3) 掉电重启后恢复慢:原来开机 2 分 10 秒公网才可用。
根因是 systemd-networkd-wait-online 等一个不归它管的网络
(wlan0 是 NetworkManager 管的),干等 2 分钟超时失败,
把 network-online.target 一起拖住。
mask 掉它之后:启动 17.3 秒,掉电重启 24 秒公网可用。
4) 看门狗两次翻车都是我造成的,都修了:
a) 我先加了"确认故障立刻重启",但隧道重建 4 条连接要 30~60 秒,
MIN_RESTART_GAP=45 导致**还没建齐就被下一次重启打断**,
死循环,每 50 秒重启一次 -> 加 150 秒宽限期
b) 宽限期又带来"链路持续差时什么都不做"的问题
-> 增加"本次重启后建齐过吗"的判断:
建齐过又垮 = 真故障,立刻重启
从没建齐 = 还在努力,继续等
5) 顺手修:看门狗状态文件里的变量在函数定义前被引用,
set -u 下报 "BUILT: 未绑定的变量"。
==================== 二、中转管理后台重做 ====================
原来的管理页是几百行内联在 relay.py 的 f-string 里,
改一处样式要在一堆转义花括号里翻。拆成独立模块 relay/admin.py。
新设计(对齐官网那套白色简洁):
吸顶顶栏 -> 公网地址(最大权重,这是用户最需要复制的)
-> 四个数字卡 -> 设备表 -> 工具卡(体检 + 下载)
空状态给图标+引导文案;窄屏表格自动变卡片。
relay.py 从 2089 行缩到 1500 行左右。
修了三个我自己引入的 bug:
a) 响应头类型写错,浏览器把整页 HTML 当纯文本显示
(用户看到的是"一屏源码")。教训:200 不等于页面能看,
当时只验了状态码和字节数。
b) 顶栏用了固定 height:62px,装不下"标题+副标题+版本信息"两行,
内容被顶出容器 -> 标题看不见、徽章压在字上。
c) do_HEAD 没处理 /download/ 路径,HEAD 一个 APK 返回
Content-Length: 0 + text/plain,下载器会误判成空文件。
同时发现 _head_info 里对 bytes 调 .encode() —— 加 _as_bytes() 兜住。
==================== 三、其它 ====================
* 设备记录只增不减:实测一天攒 75 条僵尸,/v1/health 从几 KB 涨到 20KB。
加 DEVICE_TTL(6 小时)自动清,占用的配对码一起释放
(管理页曾显示"配对码占用 131"但一台设备都没有)。
* HEAD 请求支持(原来一律 501)。
* 官网新增产品展示:手表界面预览(颜色/布局与真实手表一致)、
电脑端真实截图、两种连接方式对比图。
* 加了一份 docs/免费托管方案.md(含一个纠错:上一版推荐的
ClawCloud Run 已在 2026-05-11 关停)。
==================== 验收 ====================
全量回归 33 个阶段全部通过
安装包完整周期 20/20(静默装入 7 秒、卸载 0.6 秒、无残留)
网页响应头 12/12
公网可用率 26/30 = 86%(底层 4G 丢包 30%,是这个上限)
发布产物 安装包 14.37MB / 服务端 0.20MB / 手表 0.19MB
559 lines
20 KiB
Python
559 lines
20 KiB
Python
# -*- 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.8.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=<lanremote-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())
|