xmz7891
|
e1c04d617a
|
清掉工作区最后两处明文密码 + 加 .gitignore 规则
演练 scrub_password_history.sh 时抓出来的:
1. _build/rotate_password.sh —— **我自己写的脚本**,
为了测「老密码应该登不上」,把老密码写进去了。
改成从 OLD_ADMIN_PASSWORD 环境变量取,不设就跳过。
(又一次证明:写测试的时候很容易顺手把秘密抄进去)
2. _build/_hdr_data/data/admin_password.txt —— 测试数据文件,
居然被 git 跟踪着。已 git rm --cached。
3. .gitignore 加了规则,以后 admin_password.txt 进不来:
admin_password.txt
*admin_password*.txt
/_build/_hdr_data/
/_build/relay_info.json
现在工作区搜不到明文密码了,只剩 git 历史里那 7 个提交
(等用户确认后再做历史重写 + 强制推送)。
|
2026-10-07 20:50:25 +08:00 |
|
xmz7891
|
237aeeb580
|
remove temp
|
2026-10-07 20:49:33 +08:00 |
|
xmz7891
|
430923dd84
|
🔐 修一个严重安全问题:管理密码被硬编码进公开仓库
== 问题 ==
relay/relay.py 第 138 行:
ADMIN_PASSWORD_DEFAULT = "@REVAedu120830"
而且 _tests/ 和 _build/ 下 11 个文件里也写着明文密码。
这个仓库是要推到**公开的 Gitee / EzHub** 的,等于:
git 仓库 = 公开的
管理页路径 /zhongzhuanzhan = 公开的(就写在代码里)
公网地址 = 公开的
密码 = 也在仓库里
**= 把管理台的门钥匙挂在门上。**
而且 git 历史删不掉 —— 只是在最新提交里删掉没用,
读过仓库的人已经从历史里拿到了。
== 修法 ==
### 1. 代码里不再有任何固定密码
relay.py 的 admin_password() 改成和 admin_key 一样的做法:
环境变量 RELAY_ADMIN_PASSWORD
-> data/admin_password.txt(600 权限,不进仓库)
-> 都没有就**首次启动随机生成**,写进那个文件并打印一次
这样全新部署的密码天然是随机的、每台不同。
### 2. 11 个文件里的明文密码清掉
_tests/ acceptance.py, test_relay_admin_login.py,
test_admin_batch_delete.py, test_admin_button.py,
test_admin_device_control.py, test_admin_js.py,
test_admin_netcheck.py
_build/ check_web.sh, verify_site_admin.sh,
split_site_admin2.py, test_response_headers.py
统一改成从 RELAY_ADMIN_PASSWORD 环境变量读,
拿不到就从部署的 data/admin_password.txt 读,
再拿不到就**跳过需要登录的用例**(而不是编个假密码硬试)。
### 3. 真机上的密码已经轮换
设备上的 data/admin_password.txt 已换成用户指定的新密码,
并实测:
✓ 新密码能登录(HTTP 302 + lr_admin cookie)
✓ 老密码登不上了
✓ 新密码无换行(printf 不带 \n,免得读出来多一个字符)
顺带:重启中断了 relay-stability 的 A/B 对照 A 组(69 个样本),
分析数据时会标注这一段。
== 新增测试 ==
_tests/test_admin_password_gen.py 验证六件事:
1. 全新部署会生成随机密码(不是那个老密码)
2. 密码长度 >= 12、纯字母数字
3. 用生成的密码能登录管理页
4. **老密码登不进去**
5. 重启后密码不变(从文件读回来)
6. **两次全新部署的密码不一样**(真的是随机的)
== 还没做的(等用户决定)==
git **历史**里还有那个密码。要彻底清掉得重写历史 + 强制推送,
这是个危险操作,按用户的规矩必须他确认之后才做。
而且**光删历史不够** —— 密码已经公开过,必须换(已换)。
|
2026-10-07 20:49:33 +08:00 |
|
xmz7891
|
571ea0cc76
|
remove temp
|
2026-10-07 20:34:06 +08:00 |
|
xmz7891
|
fa2e1ae43c
|
补充:那个 JS bug 在做完修复之后又踩了一次(同一个坑,第二次)
在真实中转上实测管理页时,node --check 报第 46 行语法错。
出事的是注释:
// ⚠ 这里必须是 \\n(反斜杠+n),不能是 \n ——
^^^^ 被 Python 变成真换行
**注释为了举例说明「不能写那个转义」,自己写了一个转义**,
于是注释被断成两行,整段 <script> 语法错误,
管理页所有按钮(改码/删除/批量删除/全选)全部失效。
修法:注释改成一个转义序列都不写,只用文字描述。
新增两道防线:
* _tests/test_admin_js_syntax.py(回归阶段 3a6)
本地渲染管理页 -> node --check。不连中转就能查出来。
* _tests/test_admin_live.py
真实中转上走 HTTP 实测两个功能(16/17 通过,唯一失败就是这个 bug)。
它依赖线上设备,不进回归套件。
原来那个「管理页 JS 合法性」阶段只查 token 在不在,查不出语法错。
「测过」和「能用」是两件事 —— 这句话这轮被验证了两次。
部署时机:发现时 relay-stability 正在跑隧道 A/B,部署要重启中转
会打断对照,所以故意押后到对照跑完。
|
2026-10-07 20:34:06 +08:00 |
|
xmz7891
|
d5a0bd951d
|
remove temp
|
2026-10-07 20:33:28 +08:00 |
|
xmz7891
|
d907fdcf6e
|
🚨 修一个严重 bug:管理页所有按钮又失效了(同一个坑踩第二次)
== 症状 ==
在**真实中转**上实测管理页时,用 node --check 检查页面内嵌的 JS:
SyntaxError: Invalid or unexpected token (第 46 行)
整段 <script> 不执行 -> 改码 / 删除 / 批量删除 / 全选 全部变幽灵按键。
**这正是用户之前报过的那个 bug,加新功能时又踩了一遍。**
== 根因(讽刺到极点)==
admin.py 的 ADMIN_JS 是**普通三引号字符串**,里面写一个反斜杠+n
会被 Python 解释成**真实换行**。而 JS 的行注释 // 不能跨行 ——
一断行,后面就成了裸文本,整段脚本语法错误。
出事的是这段注释:
// ⚠ 这里必须是 \\n(反斜杠+n),不能是 \n ——
^^^^
这个 \n 被 Python 变成真换行
**注释为了举例说明「不能写 \n」,自己写了一个 \n。**
于是注释被断成两行,把整段 JS 弄坏了。
== 修法 ==
注释改成**一个转义序列都不写**,只用文字描述:
// ⚠ 下面 prompt 里的换行,必须写成「反斜杠 + n」两个字符。
// 本文件是普通三引号字符串:只写反斜杠 + n,Python 会把它
// 变成**真实换行** ... (一个转义都不出现)
并把"踩过两次"的历史写进注释,提醒后人别再来第三次。
== 新增两个测试(防止再犯)==
1. `_tests/test_admin_js_syntax.py`(已注册为回归阶段 3a6)
**本地渲染管理页**,把 <script> 抠出来喂给 `node --check`,
还检查注释行里有没有裸控制字符。
为什么原来查不出来:已有的「管理页 JS 合法性」阶段只检查
token 在不在,不查语法 —— 「23 项测试通过」和「功能能用」
是两件事。
2. `_build/scan_admin_escapes.py`
扫源文件里"注释下一行不像 JS 代码"的可疑断行。
3. `_tests/test_admin_live.py`
在**真实中转**上走 HTTP 实测两个新功能(不能进回归套件,
因为它依赖线上设备):
一、管理页能打开(带 X-Admin 头绕开登录页)
二、页面里真的有 allowNew 和改码按钮
三、内嵌 JS 语法合法
四、关掉接入开关 -> 新设备被拒 -> 打开 -> 能注册(实测通过)
五、改配对码 -> 真的变了(实测通过)
六、不带 X-Admin 访问管理接口返回 401(实测通过)
实测结果:16/17 通过(唯一的失败就是上面那个 JS bug,
已修,本地渲染 6/6 通过)。
== 顺带纠正一个我自己的误解 ==
我一开始只带 `?key=` 访问管理页,拿到的是**登录页**,
还以为是设备上部署了旧版本。实际是管理页改成了
**账号 + 密码登录 + Cookie 会话**(用户要求),
脚本通道用 `X-Admin` 头(见 relay.py 的 _admin_gate)。
== 部署时机 ==
**故意没有立刻部署到设备上** —— 部署要重启中转服务,
会打断 relay-stability 正在跑的隧道配比 A/B 对照。
等对照跑完(约 22:00)再部署并复测。
|
2026-10-07 20:33:27 +08:00 |
|
xmz7891
|
f7b9f2a058
|
remove temp
|
2026-10-07 20:29:58 +08:00 |
|
xmz7891
|
490adbdba3
|
补充 2 小时长测的最终数据(361 样本 / 1 小时 44 分)
可用率 356/361 = 98.61%
耗时 最小 829 中位 1280 P90 2386 P99 14242
长测原定 2 小时,实际跑到 1 小时 44 分停了 ——
设备在 19:11 换网段(192.168.10.105 -> 192.168.2.156),
测量脚本随之断开。361 个样本足够说明问题。
改之前是 96~98%,现在 98.61%,且失败不集中在某个时段。
|
2026-10-07 20:29:58 +08:00 |
|
xmz7891
|
243c1c3f76
|
remove temp msg file
|
2026-10-07 20:29:37 +08:00 |
|
xmz7891
|
41bf110e22
|
修 devssh:定义了 HOST_FALLBACKS 却从来没用过
随身 WiFi 跟着教室网络走,地址会在 192.168.2.156 和
192.168.10.105 之间变。devssh.py 里明明定义了 HOST_FALLBACKS,
但 run / upload / download 三个函数**全都直接用 HOST** ——
于是设备一换网段就全部连不上,而且报的是连接超时,
看起来像设备挂了。
现在改成按候选列表顺序试:
环境变量 YKXMZ_DEV_HOST 优先
-> 192.168.2.156(当前)
-> 192.168.10.105(换网前用过)
并记住上次通的那个,后续调用不用每次从头试。
连上时打一行 `[devssh] 连上 x.x.x.x`,免得以后又搞不清连的是哪台。
还加了 `--probe` 子命令,一句话看现在哪个地址能用:
python _build/devssh.py --probe
实测:192.168.2.156 通、192.168.10.105 不通,自动选中前者。
|
2026-10-07 20:29:37 +08:00 |
|
xmz7891
|
75c15f793c
|
remove temp msg
|
2026-10-07 19:03:46 +08:00 |
|
xmz7891
|
035c2cc4c5
|
补充连接数时间线证据:双协议兜底确实起作用
86 个采样点 / 约 1.5 小时:
QUIC: 最低 1 最高 8 平均 7.5
HTTP/2: 最低 1 最高 2 平均 2.0
QUIC 塌陷(<= 2 条)只有 2 次,占 2.3%
塌陷时 HTTP/2 一直是 >= 1 条(2/2 = 100%),归零 0 次
QUIC 连接数分布:
8 条 76.7%
7 条 15.1%
6 条 3.5%
5 条 2.3%
1 条 2.3% <- 塌陷(1.5 小时里约 2 分钟)
一次典型塌陷的时间线也记下来了(watchdog 日志):
重启把中断从「可能几分钟」缩到 26 秒。
目标「0 掉线」做不到 —— 4G 链路上 QUIC 会周期性塌陷。
但影响被压到 2.3% 的窗口,而且 HTTP/2 全程兜住。
教训:多行中文提交信息里有引号时,PowerShell 的 -m 参数会被吃掉。
一律写进文件再用 -F 提交。
|
2026-10-07 19:03:45 +08:00 |
|
xmz7891
|
4cc368431c
|
新增 docs/优化实测数据.md:本轮所有优化的实测数字汇总
每一项都写清楚「改之前是多少、改之后是多少、怎么测的、受什么限制」,
以后有人问「这个改动到底有没有用」能直接翻数字,不用靠感觉。
内容包括:
* 测量方法上的两个坑(从哪台机器测差很多、curl -I 验证 GET 是错的)
* 下载吞吐 41 倍(含改之前/之后对照)
* 隧道稳定性:配比对照表、QUIC 72% 拨号率的真因、
MTU 1400 的效果、长时可用率
* 长轮询 30 次数据(中位 61439ms)
* 端到端延迟:局域网 0ms vs 本地中转 15ms
* 局域网优化的四个真问题
* 「按一下没反应」的完整证据链和根因
* 独立验证发现的 9 个问题
* 测试工具自己骗人的两次
* 服务端部署脚本的三套测试
* **没做到的部分**(98.5% 可用率的物理限制,如实记录)
这份是内部工程文档,已加进对外清洁度检查的允许清单
(和 docs/项目规矩.md、docs/双平台发布.md 同类)。
|
2026-10-07 19:02:57 +08:00 |
|
xmz7891
|
0ee91b68de
|
清理误提交的测试数据目录 + 扩展 .gitignore
|
2026-10-07 19:01:33 +08:00 |
|
xmz7891
|
e61bae80ca
|
移除提交时残留的临时消息文件
|
2026-10-07 18:42:27 +08:00 |
|
xmz7891
|
da40f7572c
|
同步部署脚本智能体的收尾改动 + 恢复被误删的文件
== 部署脚本(另一个智能体的收尾)==
* deploy.sh 默认两个隧道 unit 都 enable:
主 lanremote-tunnel quic + 8 条 + --metrics :20241
副本 lanremote-tunnel-h2 http2 + 2 条 + --metrics :20242
--protocol 支持 both(默认)/ quic / http2,非法值退出码 2
* metrics 端口在两个 unit 里都显式钉死 ——
实测停掉主实例再重启副实例时,副实例会抢 20241,
于是 watchdog 和所有按端口读指标的脚本全部读错实例
* MTU/MSS 收成单一出处:net-tune.sh 唯一负责 MSS 1360 + 接口 MTU 1400,
deploy-tune.sh 只管 keepalive + 开机加速并调用 net-tune.sh。
这样就不存在"谁后跑谁赢"
* 按需重启:程序和 unit 都用 cmp 比对,一样就不落盘、不重启、
不 daemon-reload(幂等重跑不再打断正在服务的连接)
* 顺手修了一个会咬人的坑:单独用 --port 9090 时原来只改 unit 不改
config.yml,隧道会回源到 8080,现象是"隧道通了但一访问就 502"
* 域名:不传 --hostname 且 config.yml 里没有 hostname 时明确停下来
要输入,不猜、不写 config、不启隧道
测试(三套全绿,实测数字):
_build/test_deploy_sh.sh 110 通过 / 0 失败(Windows 沙箱)
_build/test_zip_deploy.sh 30 通过 / 0 失败(从发布 zip 解压再跑)
_build/test_deploy_device.sh 39 通过 / 0 失败(真机 Debian aarch64)
真机那套桩掉了 systemctl/iptables/ip/sysctl/nmcli
(--prefix 只隔离写文件,不隔离命令副作用),
第 8 节专门验证线上零影响:relay/tunnel/h2 全 active、
MTU=1400、MSS 规则数、keepalive 测前测后完全一致。
== 恢复被误删的文件 ==
排查过程中有人用通配符清了 $env:TEMP\tmp.*,而 Git Bash 的 /tmp
正映射到那个 Windows 目录,导致两个被 git 跟踪的文件被删:
_build/insert_stub_checks.py
_build/inspect_pathtest.sh
已从 git 恢复。
教训:用通配符删临时文件很危险,尤其是 /tmp 在 Windows 上
映射到共享目录时。改成只删明确列出的文件名。
== 清理 ==
删掉这轮调试产生的临时输出(reg3.txt / td1.txt / td2.txt 等),
不让它们进仓库。
|
2026-10-07 18:42:21 +08:00 |
|
xmz7891
|
e611b07fb1
|
纠正一个传出去的错误结论:不是「运营商封了 UDP」
我在给部署脚本的指示里写过:
实测这条线路上运营商对 UDP 有限制 ——
12 个 Cloudflare edge IP:TCP 12/12 通、UDP 0/12 通
它照这个把注释写进了 relay/deploy.sh。
**但这个测量是无效的。** relay-stability 后来自己纠正了:
那个 UDP 探测包是手搓的简化包,格式不对,
Cloudflare 本来就不该回应它 —— 「0/12 无回应」什么都证明不了。
用 cloudflared --loglevel debug 跑临时实例,真相是:
UDP Connectivity region1/region2 PASS QUIC connection successful
SUMMARY: Environment is healthy. cloudflared will use 'quic'
Registered tunnel connection (握手 1 秒)
**UDP 完全通。**
真因是:QUIC 建连要 6+ 个包往返,而 TCP 的 SYN 只有 3 个包、
内核还会按 1s/2s/4s 激进重传。约 5.4%/包 丢包下:
(1 - 0.054)^6 ≈ 72% <- 和实测的 72% 对得上
所以 QUIC 的弱点在「连不上」,不在「连上之后不好」;
一旦连上,它没有 TCP 的跨流队头阻塞,反而更稳。
结论的**用途**没变(还是要两条协议一起跑),但留着错误的根因注释会:
* 误导后人以为 UDP 被封 -> 可能做出「干脆别用 QUIC」的错误决定
* 掩盖真正可优化的方向(QUIC 握手对丢包敏感是链路物理属性,
只能靠 HTTP/2 兜底来绕,不是「运营商限制」)
改动:relay/deploy.sh 两处、_build/test_deploy_sh.sh 一处。
教训记一笔:**手搓协议包去探测可达性非常容易得出错误结论** ——
那个包格式不对,对方根本没义务回应。要么用官方工具的自检
(cloudflared 自己会打 UDP Connectivity PASS/FAIL),
要么别下结论。
|
2026-10-07 18:22:19 +08:00 |
|
xmz7891
|
cc5891e6d0
|
修局域网/中转对比基准:它自己有两个 bug,害产品背了黑锅
这个基准一直是红的(中转那条路报「连接阶段失败: timed out」),
而且报告里写着"这是中转链路的产品 bug"。**查下来不是产品的问题。**
== bug 1:两条路用了同一个配对码 ==
基准为了排除互相干扰,给两条路**各起一台全新被控端**(设计是对的),
但两台都用了写死的 code="246813"。
中转有「一个配对码只属于一台设备」的规则:
第 1 台(跑局域网)注册成功
-> stop_agent 杀掉进程,但**中转还以为它在线**(last_seen 没过期)
第 2 台(跑中转)用同一个码注册 -> **被拒**
中转日志:配对码 246813 已被 ... 占用,拒绝 ...
而 start_agent 只检查"有没有设备在线" —— 第 1 台僵尸还挂着,
于是**以为新的一台登记成功了**
-> 没有任何进程在回包 -> 超时
对照实验(新增 _build/verify_code_reuse.py):
场景 A 同一个码 -> **不通**
场景 B 各用不同码 -> 通
修法:两条路各用不同码;start_agent 的判据从"有没有在线设备"
改成"有没有**配对码正好是这一台要用的码**的在线设备"。
== bug 2:attach 用的还是旧码(我第一版补丁也漏了这处)==
relay_connect() 里 `client.attach(code)` 用的还是局域网那个码 ——
attach 到一台已经死掉、但中转以为在线的设备上,pair 自然没人回。
修成 `attach(relay_code)`。
== 顺带更新一段过时注释 ==
原文写着「当前版本经中转只稳定支撑前 1~2 次交互……这是中转链路的
产品 bug」,并据此把中转的轮数调小、超时调短。
那个 bug **已经修好了**(根因:一条 HTTP/1.1 连接被长轮询和发命令
两个线程同时用,撞状态机抛 CannotSendRequest("Idle"),
异常冒到 _client_loop 外面把 client-relay 线程打死)。
注释不更新会误导后人以为产品还有这个问题。
== 修好之后的实测(本地中转,30 次/路)==
局域网 握手 16ms 成功 30/30 (100%) 中位 0ms P90 0ms 最大 1ms
中转 握手 46ms 成功 30/30 (100%) 中位 15ms P90 29ms 最大 32ms
这是本地中转,量的是**协议本身的开销**(长轮询往返 vs 直连 TCP);
真实跨网络那条腿的 1.0~1.5 秒来自隧道和 4G,是另一回事。
结论:局域网确实是"毫秒级、不耗流量"的那条根本路径 ——
中位 0ms vs 15ms,而且不走任何外网。
踩坑记录:这两处都是"看起来像产品 bug、实际是测试自己的 bug"。
先怀疑测量工具,比先怀疑产品更省时间 ——
上一次(管理页缓存头)也是我拿 curl -I 测 GET 白查了半天。
|
2026-10-07 18:20:57 +08:00 |
|
xmz7891
|
aac43e559d
|
修正 MTU 测试的过时断言(1500 -> 1400)
test_net_tune.py 原来断言 net-tune.sh 里写着 mtu 1500,
依据是更早一次实测「接口 MTU 降到 1400 会掉到 50%」。
那个结论后来被推翻了 —— 当时漏了一件事:
**MSS 钳制只管 TCP,QUIC 走 UDP 完全不受保护。**
路径 MTU 实测 1472(1500 的包不通),QUIC 默认发 ~1452 的包会被
中间设备静默丢掉,而 UDP 丢了不回 ICMP,只能等超时。
把接口 MTU 压到 1400 之后,QUIC 建齐 10 条连接从 28 秒缩到 12 秒。
所以现在是**故意**设 1400。断言跟着改,
不然它会永久假失败,还会诱导后人把真正的改进改回 1500。
顺带记录:全量回归 38 个阶段全部通过。
新增的三个阶段(局域网优化 / 手表局域网切换 / 编码与换行 /
管理页设备控制)都是这轮加的。
|
2026-10-07 18:10:30 +08:00 |
|
xmz7891
|
8880e7033d
|
修「经中转几次后永久哑掉」——「按一下没反应」的真因
== 现象 ==
用户报:
* 局域网能搜到电脑,IP/端口/配对码都对,连上后**没有任何反应**
* 走中转「已配对」,但点按钮没反应
局域网那一半已经修过(preferLan)。这次抓到的是**经中转**那一半,
而且比预想的严重得多。
== 证据链(插桩复现出来的)==
被控端日志里直接抛了未捕获异常,线程当场死:
Exception in thread client-relay:
server.py:407 session.send(reply)
relay.py:166 self._link.send_to_client(data)
relay.py:357 self._post("/v1/agent/send", ...)
relay.py:766 raise RelayError(f"请求失败: {last_error}")
lanremote.relay.RelayError: 请求失败: Idle
之后的状态:
* client-relay 线程没了 —— 电脑再也不往手表回任何东西
* relay-link 线程还活着、还在长轮询 —— 中转那边看着「设备在线」
* **手表看到的就是「连上了,但按一下毫无反应」**
写入仓库的测试 _tests/test_relay_local.py 一直是这个形状:
前面几项 PASS、跑到经中转发命令就卡死。之前没人深挖。
== 根因 1:一条 HTTP/1.1 连接被两个线程同时用 ==
_post() 里锁只包住「取连接」那一段:
with self._http_lock:
connection = self._http # 锁到这里就放了
try:
connection.request("POST", ...) # 锁外发请求
于是两个线程会同时用同一条 connection:
* relay-link 线程跑 /v1/agent/poll —— 长轮询,一条请求挂 180 秒
* client-relay 线程同时 POST /v1/agent/send
HTTP/1.1 一条连接同一时刻只能有一个在途请求,第二个线程一进来
就撞上 http.client 状态机,抛 CannotSendRequest("Idle")。
_post 的「重试一次」也救不了:它 _drop_http() 之后,另一个线程
手里还攥着那条旧连接对象在发,照样坏。
修法:
* **发命令用独立连接**(_post 加 dedicated 参数)。
长轮询那条常年被占着,发命令不该去挤它,各走各的。
* 两条连接各自一把锁**锁住整个「发请求 + 读响应」周期**,
不再只锁取连接那一下。
* 普通请求若发现缓存连接正忙(长轮询在途),临时另开一条用完即关。
* _drop_http(dedicated=) 区分清哪条;finally 里一定归还占用
(否则那条连接会被永久标记为忙)。
== 根因 2:回包失败会打死整个会话线程 ==
server.py 的 _client_loop 里:
if reply is not None:
session.send(reply) # <- 不在任何 try 里
...
except OSError: # <- 外层只捕获 OSError
pass
而中转链路上 send 抛的是 RelayError,直接冒出去把线程打死。
**死一次就永久哑**,代价太大。
修法:session.send() 单独兜住所有异常,失败就断开这个会话
(让手表重连),绝不让线程消失。外层也补一个兜底 except。
== 修复效果 ==
修复前:pair OK -> ping OK -> ping 卡死 -> 线程崩溃,永久哑
修复后:pair 21ms -> ping 82ms -> ping 4ms -> ping 6ms,零异常
_tests/test_relay_local.py:**之前完全卡死 -> 现在 21/21 全过**
这个测试之前一直是红的,说明「35 个阶段的回归全绿」这件事
本身就不成立过 —— 某个阶段卡住时看起来像环境问题而不是缺陷。
== 同批修掉验证阶段找到的其它问题 ==
1. **TTL 清理 + 关掉接入开关 = 设备永久锁死**(严重)
register() 用 `device_id in self.devices` 判断老设备,但超时清理
把设备从 devices 和 codes 都删了。电脑关机过夜(必然 > 6 小时)
第二天开机就会被自己的开关挡住,而且**重启也救不回来**
(清 codes 时没落盘,重启后又当孤儿清一遍,自我维系)。
管理员在页面上还看不到这台设备,不知道该放开谁。
修法:状态文件里单独存 knownDevices 设备历史,超时清理绝不动它;
register 改查这份历史;清理后补 _save();delete_device 才真正忘记。
2. **管理页 JS 语法错误 -> 所有按钮失效**(严重)
admin.py 里 ADMIN_JS 是普通三引号字符串,我在 Python 源码里写的
`\n` 被变成真实换行,拼进 JS 就是字符串里的裸换行 ->
整段 <script> 不执行 -> 改码/删除/批量删除/全选全变幽灵按键。
元凶是我加改码功能那次提交。改成 `\\n`。
3. **--relay / --relay-status 崩溃**(严重)
RelayLink.start(url) 的 url 是必填参数,我把默认地址改空白时
run_agent.py 两处调用没跟着改。一处被 try 吞成「测试失败:missing
1 required positional argument」(用户以为地址填错了),
一处直接报栈。两处都补上 url。
4. 非法 deviceId(控制字符 / 路径分隔符)现在被拒(白名单:
字母数字 + - _ . :)
5. enable_relay() 第二次调用不再谎报成功(加 RelayLink.running 属性,
返回值反映真实运行状态;不再用「对象建过没有」判断)
6. snapshot() 清孤儿码后补 _save(),磁盘和内存不再脱节
== 局域网全面优化(task-2,另一个智能体完成)==
4 个真问题:
* **网段判断写死 /24** —— 在 /16 网络上 10.8.1.5 和 10.8.2.5
被判成不同网络,明明能直连却去走中转还费流量。
改成按网卡真实掩码比较。
* **广播只发一个包** —— 丢一个就整轮搜不到。改成 5 个时间点重发。
* **只探一个网段** —— 多网卡手表搜不到另一网段的电脑。改成全探。
* **走中转之后再也回不到局域网** —— 出门再回家永远挂中转。
新增定期回切(先便宜地 TCP ping,通了才动真格,
不破坏正在工作的中转连接)。
实测:局域网握手 2ms、50/50 = 100%、中位 0ms。
(跨网络那半之前拿不到数字,就是被上面那个线程崩溃挡住的 ——
现已修复。)
顺带:instance.py 单实例锁端口加 YKXMZ_LOCK_PORT 覆盖(默认不变)。
用户自己那台被控端正跑在这台机器上占着 8767/TCP + 8766/UDP,
是这次排查里好几个「测试怎么都不通」的根因。
== 服务端一键部署脚本(task-1,另一个智能体完成)==
relay/deploy.sh + relay/deploy-tune.sh,随服务端 zip 发布:
* 自动检查并安装 cloudflared
* 部署程序、装 4 个 systemd 服务、网络调优、配隧道、自检打公网地址
* **不传 --hostname 且没有 TUNNEL_TOKEN 时明确提示要填**,
不会静默用一个写死的域名(用户明确说过这个中转站是他自己用的,
不对外开放,绝不能把私人域名烧成默认值)
* 重跑 = 升级,已有数据/配对码/管理密码/凭据都不动
顺带修了发布包一直缺 site_shots/ 的老 bug
(package_remote.py 用 iterdir()+is_file() 收文件,目录进不去,
新机器装完官网截图全是 404)。
== 默认中转地址改成空白 ==
用户要求:软件默认不带任何中转地址,由用户自己填。
四处硬编码全部清空(server.py / MainActivity.java / run_agent.py /
relay.py 的注释和文档示例),全仓已无非注释的硬编码地址。
== 文档同步(task-5,另一个智能体完成)==
README / BEFORE-INSTALL / AFTER-INSTALL / 打包模板:
* 新增「软件里不带任何中转地址」告示,讲清跨网络要自己部署
* 修正 Android 版本(写的是 9.0/API28,实际 API 24 / Android 7.0)
* 删掉「地址已经内置好了」(现在是错的)
* AGENT_README 原来把作者自己的中转地址当成「内置地址」发给用户,已重写
* RELAY_README 原来写「两条隧道同时跑会互相争抢」,
和真机/CHANGELOG 矛盾;按实测改成「两种协议同时在跑(故意的)」
|
2026-10-07 17:59:39 +08:00 |
|
xmz7891
|
b2bfa6ffbf
|
长轮询客户端超时 120 -> 180 秒(实测出现 126.6 秒的长尾)
relay-stability 跑了 30 次真实长轮询,第 1 次耗时 126604ms:
服务端 hold 是 60 秒
剩下 66 秒全是网络停顿(4G 抖动 + 重传退避)
而客户端超时写的是 120000ms
-> 126.6 > 120,那次会被**误判成失败**,尽管服务端已经正常返回
其余 29 次都是 61~62 秒,很正常。不是 hold 太长,是网络尾巴太长。
为什么放宽超时而不是缩短 hold:
A. hold 缩到 45 秒、超时保持 120 -> 最坏 111 秒,安全
但请求数多 33%,而每次请求都要吃隧道 1~2 秒开销
B. hold 保持 60 秒、超时放宽到 180 -> 最坏 180 秒,安全
选 B —— 这条链路的根本问题是丢包抖动,**请求越少越不容易撞上**。
放宽超时只是"别把慢但成功的请求冤枉掉",不增加任何请求。
客户端已有"连续失败 3 次才判离线"(OFFLINE_AFTER=3),
所以偶尔一次长等待不会让界面闪"已断开"。
超时余量的演进(全是实测逼出来的):
90 秒 -> 实测 93589ms 不够
120 秒 -> 实测 126604ms 不够
180 秒 -> 60 hold + 120 余量
两边同步改:
windows/lanremote/relay.py POLL_TIMEOUT 120 -> 180
android .../RelayTransport.java POLL_TIMEOUT_MS 120000 -> 180000
(Java 侧确认 setConnectTimeout 另有 15 秒上限、setReadTimeout 吃这个值,
没有别的超时会先把请求掐掉。)
|
2026-10-07 17:36:20 +08:00 |
|
xmz7891
|
facb156aca
|
服务端 zip 加一键部署脚本(cloudflared 安装 + 部署 + 公网,全自动)
用户要求:服务器端文件里放一个 .sh,双击就能检查 cloudflared 装没装、
自动部署、配好公网,以后全自动。
== 新增 ==
relay/deploy.sh 一键部署主脚本,7 步全自动:
环境检查 -> 装 cloudflared -> 部署程序 -> 网络调优
-> 写 4 个 systemd 服务 -> 配隧道 -> 启动自检打公网地址
relay/deploy-tune.sh 网络调优,可单独重跑:
keepalive 30/10/3 + 路径 MTU 探测 + MSS 1360
+ 接口 MTU 1400 + mask networkd-wait-online
每一项都写明了实测依据
用法:
bash deploy.sh 复用已有凭据,全自动
TUNNEL_TOKEN=eyJh... bash deploy.sh Token 模式
bash deploy.sh --hostname relay.你的域名 换域名
bash deploy.sh --skip-tunnel 只装中转
bash deploy.sh --help 全部参数
重跑 = 升级;已有数据/配对码/管理密码/凭据/手改过的 config.yml 都不动。
== 自动化到什么程度 ==
* cloudflared 不在就自动下载装上(并校验能执行)
* 隧道支持两种模式:已有凭据文件、或 TUNNEL_TOKEN
* **不传 --hostname 且没有 Token 时明确提示要填**,不会静默用一个写死的值
(用户明确说过:这个中转站是他自己用的,不对外开放,
绝不能把私人域名烧成默认值)
* deploy-tune.sh 的 MTU 取 1400:路径 MTU 实测 1472,
而 MSS 钳制只管 TCP、QUIC 走 UDP 完全不受保护;
接口 MTU 降到 1400 后 QUIC 建齐 10 条连接从 28 秒缩到 12 秒
== 顺带修掉两个真 bug ==
1. write_tunnel_unit 写的文件名漏了 .service 后缀 ——
文件写下去了但 systemd 完全看不见,enable 还会安静地失败。
2. 发布包一直缺 site_shots/(老 bug):
package_remote.py 用 iterdir() + is_file() 收文件,目录进不去。
新机器装完官网截图全是 404。只有从 zip 解压真跑一遍才暴露。
同时把 deploy.sh 里 site_shots 的检查降级成警告(缺截图不该让部署失败)。
另外给 deploy-tune.sh 清旧 MSS 规则的循环加了 10 次上限 ——
原写法在 -D 报成功但规则仍在时会死循环把整个部署卡死。
== 验证 ==
* _build/test_deploy_sh.sh 沙箱 65 项,全通过
* _build/test_zip_deploy.sh 从发布 zip 解压再跑,30 项,全通过
* _build/test_deploy_real.sh **真机 Debian 上实测**(新增):
zip 解出 17 个文件 -> 语法/LF/无 BOM 通过
--help 退出码 0 且无副作用、未知参数退出码 2
沙箱安装成功、幂等重跑哨兵不被冲
现有三个中转服务全程 active,自检 200
|
2026-10-07 17:28:09 +08:00 |
|
xmz7891
|
d4f335b8f9
|
管理页新增:允许/禁止新设备接入 + 修改设备配对码
== 功能一:允许新设备接入开关 ==
关掉之后只有已经登记过的电脑能继续上线,没见过的新设备一律拒绝。
状态存在 relay_state.json 里,重启不丢。
判断新设备用的是 deviceId,不是配对码 ——
这一点很关键:管理员给老设备改过码之后,
那台设备必须仍然能进来;按码判断的话它会被自己的开关挡在外面。
测试里专门有一条 A3b 守这个行为。
被拒时回 blocked=True,让客户端能区分被管理员挡了
和配对码冲突,而不是去匹配中文错误文案(文案一改就坏)。
== 功能二:修改任意设备的配对码 ==
管理页每台设备一个「改码」按钮,填 6 位数字即生效。
要点:
* 新旧码冲突检查
* 旧码映射必须删掉,否则变成孤儿条目,
管理页配对码占用的数字会虚高
* 改码踢掉该设备当前会话(会话绑在码上)
* 立刻持久化
== 顺手修掉两个真 bug ==
1. 改码功能一上线就 500:Session 用了 __slots__,
根本没有 session_id 属性(会话是以 token 为 key 存在 dict 里的),
写 s.session_id 直接 AttributeError。
改成遍历 sessions.items() 拿 key。
2. snapshot() 里有同样的错(清理超时设备那段):
平时 stale 列表是空的所以从没炸过,一旦真有设备超过 6 小时
没上报就会 500。这次一起修了。
== 管理页 UI ==
* 设备小节加了「允许新设备接入」勾选框
* 每行加「改码」按钮(走 prompt,不引入弹窗层)
* 改码按钮用 data-recode 而不是 data-device ——
现有单台删除按 button.danger[data-device] 绑,
带上 data-device 会被当成删除按钮,点改码变删设备。
测试 C6 专门断言这一点。
== 测试 ==
新增 _tests/test_admin_device_control.py(23 项),
已注册进 _tests/verify_all.ps1 第 3e 阶段。
|
2026-10-07 17:01:14 +08:00 |
|
xmz7891
|
25e7780272
|
单次请求失败不再立刻判离线,改成连续 3 次
实测单个请求有 2~4% 的偶发失败(4G 丢包打在一条隧道连接上,
或者那条连接正在重连)。原逻辑是一失败就 online = false,
于是界面上会闪一下已断开 —— 用户点按钮时正好撞上那一闪,
看起来就像又连不上了。
而链路其实 1.5~15 秒就自己恢复(客户端本来就有退避重试)。
改成连续失败 3 次才判离线(手表至少 4.5 秒、电脑至少 6 秒):
* 单次抖动完全不影响界面
* 真的断了仍然如实显示
这不是掩盖问题,而是把是否可用的判断从一次采样改成连续采样 ——
单点采样在有丢包的链路上噪声太大。
两边同步改:
android .../RelayTransport.java failStreak + OFFLINE_AFTER
windows/lanremote/relay.py _fail_streak + OFFLINE_AFTER
顺带记录 PC 端基准(真实用户视角,之前都是在设备上量的,
那等于把设备 4G 算了两遍):
公网可用率 98% 耗时 中位 911 P90 1889 P99 23868 ms
长轮询 5/6 耗时 60.8~62.1 秒
对比设备端(中位 1344 / P90 2563)—— 用户实际体验比设备端量出来的好。
|
2026-10-07 16:16:50 +08:00 |
|
xmz7891
|
aea78efe45
|
版本 1.8.1 -> 1.8.2
|
2026-10-07 15:44:49 +08:00 |
|
xmz7891
|
bf15a2ee61
|
静态资源交给 Cloudflare 边缘缓存(下载提速 41 倍)
实测基线:
ykxmz-setup-1.8.1.exe 只下到 237568/15067199 字节就停(17 KB/s)
ykxmz-watch-1.8.1.apk 201523 字节 / 66.8 秒 = 3 KB/s
ykxmz-server-1.8.1.zip 214900 字节 / 3.1 秒 = 67 KB/s
原因:relay 给所有响应都发 Cache-Control: no-store,
Cloudflare 完全不缓存 —— 每次下载/每张图都要穿隧道回 4G 源站。
改动:
/download/* no-store -> public, max-age=604800, immutable
文件名带版本号,同一名字内容永不改变,可以长缓存
/ -> public, max-age=60(挡住刷新重复回源)
/favicon.ico 已是 max-age=86400,保持不变
/shot/* 已是 max-age=86400,保持不变
/v1/* 保持 no-store(接口必须实时)
顺带修两个自己踩的坑:
1. _send() 无条件先发 no-store,再追加 extra 里的 Cache-Control,
结果是**两个 Cache-Control 头**,客户端只认第一个 ——
调用方传的缓存策略被吃掉。改成 extra 里有就不再发默认值。
2. do_HEAD 和 do_GET 的缓存头不一致:HEAD 对非 /shot/ 一律报 no-store。
下载器/代理用 HEAD 探测时会误判文件不可缓存。
现在 _head_info 给每个路由带上和 GET 完全相同的缓存头,
do_HEAD 也不再盲目覆盖。
结果(同一个测试口径):
setup.exe 17 KB/s(截断) -> 695 KB/s 完整 15 MB 41 倍
watch.apk 3 KB/s -> 101 KB/s 33 倍
server.zip 67 KB/s -> 87 KB/s
三个文件全部 cf-cache-status=HIT(不再穿隧道回源)
排查过程记一笔:我一开始用 curl -I 验证,
看到的全是 no-store —— 因为 **-I 发的是 HEAD**,
而 HEAD 走的是另一个分支。查了半天才发现是验证方法错了。
|
2026-10-07 15:44:48 +08:00 |
|
xmz7891
|
46196c3618
|
CHANGELOG 加 1.8.1
|
2026-10-07 14:24:06 +08:00 |
|
xmz7891
|
b1119d0da6
|
修「局域网连接被中转劫持」+ 长轮询 15s→60s
用户报两个问题:
1. 局域网能搜到电脑,IP/端口/配对码都对,点连接**没有任何反应**
2. 走中转 1426ms 延时,一会儿就 502
========== 一、局域网被劫持(真 bug)==========
MainActivity.connect() 里:
boolean usedLan = !relayUrl().isEmpty() ? false : true;
if (!relay.isEmpty()) {
reply = client.connectViaRelay(...); // 走中转
} else {
reply = client.connect(host, port, ...); // 才走局域网
}
**只看「中转设置里填没填地址」,填了就一律走中转,
完全无视用户填的局域网 IP 和端口。**
而中转单次超时是 40 秒 —— 用户点完就卡在「正在连接…」
四十秒,在他看来就是"没有任何反应"。
影响两条路:
* 「局域网连接」页面填 IP 直连(connectLanTyped)
* 首页「已配对的电脑」点一下重连(quickConnect)
修法:给 connect() 加 preferLan 参数。明确要走局域网时
**先直连(6 秒超时,局域网本该是毫秒级),失败再退回中转**,
保证最终能连上。
顺带两个体验改进:
* 设备列表里,保存的 IP 和手表不同网段时标「⚠ 地址可能已过期」
(用户截图里那个 192.168.2.64 就是过期地址,电脑早换成
192.168.10.103 了)
* quickConnect 判断出地址过期就跳过直连,直接走中转查最新地址 ——
省掉那 6 秒白等
========== 二、长轮询 15 秒 -> 60 秒 ==========
实测隧道每次请求要多花 1~2 秒(CF 边缘路由 + 回源):
本机直连 60 秒的 poll -> 60.0s
走公网同一个 poll -> 61.5~62.0s
这个开销是**按次数**收的:
15 秒一循环 -> 一天 5760 次/端,光路费 2.4~3.2 小时
60 秒一循环 -> 一天 1440 次/端,光费 0.6~0.8 小时
而且请求越频繁,撞上丢包/重连窗口的概率越大。
**不会让命令变慢**:长轮询是"有数据立刻返回"(device.event 唤醒),
拉长只是减少空转重开。实测 60 秒长轮询穿隧道 4/4 通过;
Cloudflare 单请求上限 100 秒,留了 40 秒余量。
三层同步改(客户端超时必须显著大于服务端 hold):
* relay/relay.py RELAY_POLL 默认 25 -> 60
* windows/lanremote/relay.py POLL_TIMEOUT 40 -> 90
* android .../RelayTransport.java POLL_TIMEOUT_MS 40000 -> 90000
========== 三、A/B 实测(还在跑更严谨的一轮)==========
短测发现基线 http2+4 虽然 100% 成功,但 **P90 是 16.8 秒**
(每 10 次有 1 次卡十几秒 —— 正是用户说的"没反应"),
而 quic+6 的 P90 只有 2.8 秒。正在用真实长轮询模式
跑 10 分钟对照确认。
|
2026-10-07 14:11:33 +08:00 |
|
xmz7891
|
94252ca156
|
加 EzHub SSH 诊断报告(给服务端运维看)
结论:EzHub 的 22 端口上是**系统 OpenSSH 9.6**,不是 Forgejo 内置的
SSH 服务。Forgejo 把公钥存在自己数据库里,不会写进
git 用户的 authorized_keys,而 sshd 也没配 AuthorizedKeysCommand
去问 Forgejo —— 所以网页上加了公钥也不生效。
判定依据(客户端就能测出来):
1. ssh -vv 显示 remote software version = OpenSSH_9.6p1 Ubuntu
(内置 SSH 会带自己的标识,且只支持 publickey,不提供 password)
2. 服务端声明 Authentications that can continue: publickey,password
—— password 是系统 sshd 的默认行为
3. 只有 22 端口开,2222/2022 都不通(内置 SSH 默认在 2222)
4. 网页注册的密钥指纹和客户端完全一致,客户端也确实送出了公钥,
但仍被拒
报告里给了两种修法(开启内置 SSH / 给 sshd 配 AuthorizedKeysCommand)、
先手动验证命令能不能跑通的办法、以及 /var/log/auth.log 该看什么。
也说明了这不紧急:HTTPS + 令牌目前完全可用。
|
2026-10-07 12:56:47 +08:00 |
|
xmz7891
|
9692ef1f32
|
修 Gitee 附件清理:assets 字段没 id,要用 attach_files 接口
Gitee 的坑:GET /releases/{id} 返回的 assets 里
**只有 name 和 download_url,没有 id**,
照着它删是删不掉的(之前静默失败,附件一路堆到 11 个)。
必须换接口才拿得到 id:
GET /releases/{id}/attach_files -> [{id, name, size}]
DELETE /releases/{id}/attach_files/{id}
已清理:Gitee 11 -> 5 个(含 2 个自动生成的源码包),
EzHub 5 -> 3 个。清理动作也加了重试(网络会抖)。
|
2026-10-07 12:54:33 +08:00 |
|
xmz7891
|
ae7e1cf269
|
Gitee 也做成幂等:已存在就复用 + 先删同名附件
重发时 Gitee 直接 400「该标签已经存在发行版」,
而且拿不到 id,后面的附件也传不上。
改成和 EzHub 一样的流程:
先按标签查已有的(查到就复用 id),复用前删掉同名旧附件,再传。
这样重跑多少次,两边的附件数都稳定。
|
2026-10-07 12:37:06 +08:00 |
|
xmz7891
|
2425f8694e
|
修发行版重复附件:重发前先删同名
Gitea 的发行版复用逻辑有 bug:发现标签已有发行版就复用,
但**没有先删同名附件**,于是每跑一次就多传一份。
调试期间同一个文件在 EzHub 上堆了 3 份(共 8 个附件,实际只要 3 个)。
修法:复用已有发行版时,先列出它的附件,
把和本次要传的同名的删掉,再传。
配套加了:
* clean_ezhub_assets.py 清理已堆积的重复(同名只留最新,支持 --dry)
* verify_releases.py 对账两个平台的发行版(标题/说明/附件/
管理页字眼/两边说明是否一致)
已清理:EzHub 从 8 个附件降到 3 个。
|
2026-10-07 12:36:35 +08:00 |
|
xmz7891
|
4b545ea324
|
加 EzHub SSH 密钥诊断脚本
诊断结论:EzHub 的 SSH 服务是**系统 OpenSSH 9.6**(banner 看出来的),
不是 Gitea/Forgejo 内置的 SSH 服务。
密钥在 EzHub 上注册的指纹和本机完全一致
(SHA256:Acmu49YQcjw3m5WefOXSBzYNY2WPHLL2vvhnlYRAttA),
但服务端一直返回 Permission denied (publickey,password)。
说明 git 用户的 authorized_keys 没有跟 Gitea 数据库同步 ——
这是服务端配置问题,客户端怎么试都过不去。
服务端要做的(任选其一):
a) 开启内置 SSH:app.ini 里 START_SSH_SERVER = true 并放行端口
b) 继续用系统 OpenSSH,但给 sshd 加 AuthorizedKeysCommand
指向 gitea/forgejo 的 keys 子命令
PS:这个实例的 UI 主题是 forgejo-auto,实际是 Forgejo。
|
2026-10-07 12:35:10 +08:00 |
|
xmz7891
|
f200240730
|
发布脚本:EzHub 走 HTTPS+令牌推送(SSH 还没配好)
EzHub 的 SSH 公钥在服务端一直被拒(Permission denied),
但令牌有 push 权限。所以发布脚本里 EzHub 改用
HTTPS + 令牌推,令牌只在进程内拼进 URL,不写进 .git/config。
顺带加了增量推送脚本 ezhub_push_all.py:
一次性推 44 个提交 = 117 MB,被 EzHub 的 nginx 413 拦了。
Git 推送是增量的,逐个提交推每包就小得多 —— 实测 44 个全部推成功。
脚本会先探测远端进度,只推缺的,并且对网络抖动自动重试。
|
2026-10-07 12:32:11 +08:00 |
|
xmz7891
|
9f9db53a06
|
文档补充 EzHub 令牌权限坑和 SSH 实际状态
|
2026-10-07 12:25:27 +08:00 |
|
xmz7891
|
d60dfcf671
|
发布脚本:提前检查 EzHub 令牌写权限
Gitea 令牌可以只给读权限。只读令牌能读仓库、能列发行版,
看起来一切正常,但一推代码就 403、一建发行版也 403。
加了 check_ezhub_scope():开头就用一个**无副作用**的探针确认权限
(对不存在的发行版发 DELETE —— 403 说明缺 write:repository,
404 说明权限过了只是资源不存在),缺权限就立刻停下并给出
明确的修复指引,不让它走到一半才炸。
文档里也写清了这个坑。
|
2026-10-07 12:25:12 +08:00 |
|
xmz7891
|
2d99c05865
|
加双平台发布配置指引(Gitee + EzHub)
|
2026-10-07 12:20:17 +08:00 |
|
xmz7891
|
ad8b73f990
|
清理临时文件
|
2026-10-07 12:19:58 +08:00 |
|
xmz7891
|
4de2255ee4
|
加 EzHub(Gitea)发布支持 + 一次发两个平台的脚本
用户要求:除了 Gitee,还要发到朋友自建的代码托管平台 EzHub
(ssh://git@git.261826.xyz/xmz/ykxmz.git),release 也要建。
探测结果:EzHub 是 **Gitea 1.22.0**
(/api/v1/version 返回 16.0.5+gitea-1.22.0),
仓库 xmz/ykxmz 存在,目前还没有任何发行版。
Gitea 和 Gitee 的接口差别(踩过的点):
项目 Gitee Gitea / EzHub
API 前缀 /api/v5/repos/... /api/v1/repos/...
认证 参数 access_token 请求头 Authorization: token
传附件路径 /releases/{id}/ /releases/{id}/
attach_files assets?name=<文件名>
附件字段名 file attachment
草稿字段 无 draft / prerelease
新增:
* _build/ezhub_release.py 单独发 EzHub 用
* _build/release_all.py 一次发两个平台(推荐)
- 前置检查:产物齐不齐、更新说明能不能取到、工作区干不干净
- **对外口径检查**:更新说明里出现管理页字眼直接拒绝发布
- 推代码到两个 remote,再分别在两边建发行版 + 传三个附件
- 支持 --dry 演练(不真的发),已经过通
修掉的 bug:
CHANGELOG 解析。按 "\n## " 切分时第一块带着自己的 "## 版本号"
(前面没换行),后面几块没有 —— 原来只写 startswith("1.8.0"),
对第一块永远匹配不上,全靠"取不到就退回第一块"的兜底碰巧对了。
现在先把开头的 # 去掉再比。
SSH:
为 EzHub 单独生成了一把密钥(~/.ssh/id_ed25519_ezhub),
不复用 Gitee 那把,权限隔离。已写好 ~/.ssh/config 的 Host 段。
**公钥需要用户加到 EzHub 账号里之后才能推。**
私钥在仓库外,不会进版本库。
|
2026-10-07 12:19:58 +08:00 |
|
xmz7891
|
2a0d9e967d
|
清理临时文件
|
2026-10-07 12:15:34 +08:00 |
|
xmz7891
|
351171e6ea
|
清理对外材料里的中转管理后台 + 把规矩变成测试
用户定的新规矩:
「你对外公开的数据里不要包含那个中转站管理的页面,你就只能提官网」
用户确认的范围:只清理「我对外的公开发言」
(发布说明 / 更新日志 / 官网 / README / 我给出的汇报),
服务端 zip 里的运维文档保留(装服务器的人需要它),
源码实现保留(路径是功能本身,藏起来没法维护)。
清掉的内容:
* CHANGELOG.md:1.8.0 条目里"管理后台重做"整段、
"管理页显示成一屏源码"、"管理页标题被切掉";
1.7.1 条目里**直接写出的路径 /zhongzhuanzhan**、
"管理入口从官网上移除";1.7.0 的"管理页加图标"
* RELEASE-1.8.0.md:第四节「中转管理后台完全重做」整节删掉,
改成「官网重新设计」
新增 _build/test_public_clean.py(15 项)并加进套件(阶段 3b7):
* CHANGELOG / README / docs 里不许出现
zhongzhuanzhan、/admin、管理后台、管理页、管理入口、admin_password
* 官网渲染出来的 HTML 逐词检查,且站内链接只允许
指向 /download/、/shot/、首页、图标、页内锚点
* 发布说明里不许出现安装教程类内容(用户要求只放更新内容)
为什么写成测试:文档没人看。这一天里我因为"以为记住了"
翻了三次车,所以把规矩变成会红的断言。
新增 docs/项目规矩.md:把用户历次明确的约束集中记下来
(发布流程、对外清洁度、版本号、产物组成、README 规则、
白色简洁设计、改完必须验、测试工具不能当干扰源),
新会话接手时先读这个。
顺带修的工程问题(今天反复踩的坑):
* 用 PowerShell 的 @"..."@ 嵌 Python 去改 .ps1 时,
PowerShell 会先把 $var 展开成空,写进去一堆反斜杠
-> verify_all.ps1 被写坏,& 语法报错
现在改 .ps1 一律用 Python 读写文件
* re.sub 的替换串会解释反斜杠转义,
`"\\test_public_clean.py"` 里的 \t 变成制表符,
路径成了 "_build<TAB>est_public_clean.py"
-> 用 lambda 替换,避开转义
验收:全量回归 34 个阶段全部通过(含新的清洁度守卫)。
|
2026-10-07 12:15:32 +08:00 |
|
xmz7891
|
2f70e5bb49
|
加 1.8.0 发布说明
|
2026-10-07 11:47:23 +08:00 |
|
xmz7891
|
49fdec1da8
|
1.8.0:隧道稳定性大修 + 中转管理后台重做
版本号 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
|
2026-10-07 11:47:12 +08:00 |
|
xmz7891
|
c6e2120ca8
|
修正免费托管方案:ClawCloud 已关停,换掉整个推荐
用户追问 ClawCloud 的 \ 是每月还是一次性。
核实过程中发现:**ClawCloud Run 已经在 2026-05-11 关停了**,
官网/文档/博客全部下线,免费套餐服务全部停止,只给付费用户退款。
至今已停运 5 个月。
我上一版文档推荐了它 —— 因为搜到的文章是 2025 年的,
我只看了内容没核对日期。这个错误必须纠正。
重新核实了整个免费托管生态(依据 2026-09-26 逐条验证过的
第三方对比 + 各厂商官方页),结论:
2026 年这一波集体缩水:
2026-02 Koyeb 取消免卡套餐
2026-04-23 Render 免费带宽 100GB -> 5GB
2026-06-15 Oracle 免费 ARM 砍半 4核24G -> 2核12G(无公告)
2026-07 Hugging Face Docker Space 转付费
2026-09-16 Vercel Hobby 只留最近 3 个部署
已经死掉的:ClawCloud、Fly.io、Glitch、Heroku、Deta、Cyclic。
现在的推荐(重排):
1. Oracle Always Free —— 唯一永久方案,2核12G 仍远超我们所需
(我们只用 23MB 内存 / 0.1% CPU),但需要实体信用卡,
且亚洲区 ARM 常年缺货
2. Render 免费版 —— 不用卡的最佳选择。750 小时/月
刚好够一个服务 24/7;自带 HTTPS 和 2 个自定义域名;
最关键:被控端每 25 秒轮询,永远不会触发 15 分钟休眠
3. Cloudflare Workers + DO —— 技术最优但要用 JS 重写
文档里还记了一条经验:免费、好用、长期是不可兼三角。
选免费方案要问商家靠什么赚钱、数据能不能搬走、备选是什么。
我们的 relay.py 零依赖随时能搬,这就是最大的抗风险能力。
|
2026-10-07 11:11:26 +08:00 |
|
xmz7891
|
494b6e5671
|
加一份免费托管方案调研文档
用户问:网上有没有免费办法托管中转服务。
查了协议架构:全是 HTTP 长轮询(agent/register、agent/poll、
agent/send、agent/bye),没有裸 TCP —— 所以托管选择面很宽。
另外 relay.py 是纯 Python 标准库、零第三方依赖、仅 150 KB、
实测内存 23 MB / CPU 0.1%,任何免费实例都装得下。
关键洞察:一旦有了公网 IP,**根本不需要 Cloudflare 隧道**,
现在所有 1033/502/530 都是隧道这一跳的问题,换过去就从根上消失。
文档里对比了 6 个方案(永久性、是否需公网 IP、是否要信用卡、
亚洲节点、能否免隧道),推荐 Oracle Cloud Always Free 首选
(永久免费 + 真公网 IP + 东京/首尔节点),
ClawCloud Run 作为不用信用卡的备选。
还写了具体的搬迁步骤和要删掉的组件。
|
2026-10-07 11:07:24 +08:00 |
|
xmz7891
|
5bf108f1f8
|
修管理页顶栏被切掉(标题看不见、徽章压在字上)
用户截图圈出顶栏:主标题「ykxmz 中转站」被切掉看不见,
「运行中」徽章压在副标题上。
根因:
.top-in 用了固定 height:62px。
但顶栏内容比 62px 高:
左边品牌是「标题 + 副标题」两行
右边是「版本信息 + 累计请求」两行
flex 容器固定高度装不下,内容就被往上顶出去了 ——
于是标题被切在容器外,徽章挤到文字上。
修法:
* height:62px 改成 padding:13px 0 + min-height:36px
(内容多高就是多高,别再写死)
* 给 logo / brand / state 都加 flex:none,别被压缩
* 标题副标题加 white-space:nowrap,避免换行错位
* 右上的版本信息允许收缩(min-width:0)
* 窄屏(<640px)时状态徽章换行,不跟标题挤
实测:1200px 和 700px 两档宽度下顶栏都正常,
标题、副标题、徽章、右侧信息全部各就各位。
|
2026-10-07 10:48:28 +08:00 |
|
xmz7891
|
2b204cae07
|
修看门狗重启死循环(我上一版改过头了)
用户报 502 Bad Gateway,Cloudflare 的图显示
「Host: relay.xxmxxs.top - Error」,也就是源站/隧道这一跳挂了。
查 watchdog 日志,发现它在**每 50 秒重启一次隧道**:
[10:31:57] 只剩 1 条连接 -> 重启隧道
[10:32:22] 重启完
[10:32:24] 又只剩 1 条 -> 距上次才 27 秒,先不重启
[10:32:47] 距上次 50 秒 -> 又重启
... 循环不停
根因(我上一版的错):
隧道在**质量差的 4G 上重建 4 条连接要 30~60 秒**。
而我上一版把 MIN_RESTART_GAP 设成 45 秒,并且
"公网不通 + 连接数<=1"就立刻重启 ——
结果每次隧道还没把连接建齐,就被下一次重启打断,
**永远停在"只剩 1 条"的状态**。
等于我自己制造了不稳定。
修法:
1. 重启后给 GRACE=150 秒宽限期,这期间绝不重启,
让 cloudflared 专心把连接建齐
2. MIN_RESTART_GAP 从 45 提到 150(和宽限期一致)
3. "立刻重启"的判定加一条:距上次重启必须超过宽限期
实测(停掉 watchdog -> 手动重启隧道 -> 观察):
10 秒内建齐 4 条连接,公网立刻 200
宽限期生效后公网 20/20 = 100%,延迟 ~690ms
顺带一个教训:之前测出 80%、20% 的 000,
是我自己后台还跑着一个观察任务在并发打接口造成的干扰。
**测稳定性的时候,测量工具本身不能成为干扰源。**
|
2026-10-07 10:37:26 +08:00 |
|
xmz7891
|
b630028112
|
看门狗检查间隔 20 秒 -> 10 秒
每次 530 窗口从"出现"到"被看门狗发现并重启",最长要等一个检查周期。
20 秒缩到 10 秒,用户撞上 1033 的概率窗口直接减半。
代价:公网探测从每 20 秒一次变成每 10 秒一次。
这台设备本来就在长轮询,这点流量可以忽略。
设备上已生效(OnUnitActiveSec=10s, AccuracySec=3s)。
|
2026-10-07 10:27:33 +08:00 |
|
xmz7891
|
f4a146056f
|
修开机慢 2 分钟:掉电重启后要 2 分 10 秒才能恢复
用户早上撞上 Cloudflare Error 1033。查出来是设备**掉电重启**:
上次开机到重启之间没有任何日志落盘(直接断电的特征),
dmesg 里还有 "l13: voltage operation not allowed"。
重启本身不是软件问题,但**恢复太慢**:
Startup finished in 3.846s (kernel) + 2min 6.358s (userspace) = 2分10秒
那 2 分钟里浏览器看到的就是 1033。
根因:
这台机器 NetworkManager 和 systemd-networkd 同时在跑,
但 wlan0 归 NetworkManager 管。
NetworkManager-wait-online -> 开机 9 秒成功
systemd-networkd-wait-online -> 等一个不归它管的网络,
干等 2 分钟超时,然后 failed
network-online.target 要等**两个都完成**,
被那个失败的拖了整整 2 分钟,
relay / tunnel / net-tune 全跟着等。
修法:
1. mask 掉 systemd-networkd-wait-online(网络不归它管,只会拖后腿)
2. watchdog 首次检查从"开机后 2 分钟"提前到 45 秒
实测(重启验证过):
修之前 Startup finished in 3.846s + 2min 6.358s = 2分10秒
修之后 Startup finished in 3.218s + 14.060s = 17.3秒
掉电重启后公网恢复时间:**24 秒**(快了 5 倍多)
配套加了 scripts/speed-up-boot.sh,换设备时直接跑一遍。
顺带确认重启后一切自动恢复:
三个服务 active + enabled ✓
keepalive 30/10/3、MTU 探测、MSS 钳制、slow_start=0 全部持久化 ✓
隧道 4 条连接 http2 ✓
官网 / 管理页 / 截图全部 200 ✓
|
2026-10-07 10:27:07 +08:00 |
|
xmz7891
|
bcb089eded
|
看门狗:连接数掉到 1 就立刻重启,不等攒够 3 次
排查公网间歇 530 时发现的:
watchdog 日志显示「公网不通(HTTP 530,ha_connections=1)」
—— 出问题那段时间隧道**只剩 1 条连接**。
原来 watchdog 只把 CONNS == 0 算"零连接",
CONNS == 1 不当回事。但实测**只剩一条连接时跨机房回源就会失败**
(请求落 SEA,隧道只剩 LAX 一条)。
改法:
* CONNS <= 1 都计入降级计数
* 公网不通 **且** 连接数已在危险区 = 确认的故障(不是短抖动)
-> 立刻重启,不等 3 次
每次检查间隔 20 秒,等 3 次就是白断 40 秒。现在能省下来。
实测:改完公网 15/15 = 100%,连接数 4 条。
顺带说明 watchdog 是怎么跑的:
systemd timer(lanremote-watchdog.timer),每 20 秒调一次
watchdog.service。之前查"服务是 inactive"一度以为它没在跑,
其实是 timer 触发型的 —— oneshot 服务跑完就是 inactive,正常。
|
2026-10-07 10:00:50 +08:00 |
|
xmz7891
|
fe3b9ada12
|
修管理页 Content-Type(浏览器显示成一屏源码)
用户截图:打开 relay.xxmxxs.top/zhongzhuanzhan 看到的**不是页面,
而是整页 HTML 源码**。
根因:_send() 的 content_type 参数默认是 application/json,
我重做管理页时调它没传 text/html —— 浏览器按 JSON/纯文本处理,
就把 HTML 原样显示了。
⚠ 更该反省的是:**我当时没发现。**
验证脚本只看了 HTTP 200 和字节数,两个都对,就以为没问题。
200 不等于页面能看 —— 响应头也得看。
顺手修的第二个问题:
HEAD 请求一律返回 501 Unsupported method(没实现 do_HEAD)。
浏览器用 GET 所以页面照开,但 Cloudflare 健康检查、
很多监控/代理探测都用 HEAD,curl -I 也是。
补了 do_HEAD:按路径算出类型和长度,只回头不回体。
新增测试 _build/test_response_headers.py(9 项):
* HEAD 各路径不能是 501
* 官网/管理页 Content-Type 必须是 text/html,不能是 json
* favicon、产品截图必须是 image/*
并加进全量套件(阶段 3b6)。
验证:该测试 9/9 通过;全量套件 33 个阶段全部通过。
|
2026-10-07 09:53:43 +08:00 |
|
xmz7891
|
160dc3765e
|
修管理页测试的引号敏感 + 补 netPanel id
重做管理页时把 HTML 从 class=pick(无引号)改成了
class="pick"(标准写法),但测试里写死了无引号形式,
于是 5 个断言集体误报失败。
HTML 两种写法都合法,测试不该关心这个 ——
给两个测试加了 _has(),带不带引号都认。
另外给新管理页的体检卡补回 id=netPanel(语义合理,
也顺手让测试钩子还在)。
同时删掉 _build/_missing_tail.py(恢复事故时的临时文件,
静态检查把它当成真模块查,报了 8 个 undefined name)。
验证:管理页网络体检 15/15、中转批量删除 24/24、
全局 32 个阶段全部通过。
|
2026-10-07 09:45:10 +08:00 |
|
xmz7891
|
1f05084f51
|
隧道"假活"真因修复 + 中转管理后台重做
用户反馈两件事:
1. 早上打开 relay.xxmxxs.top 看到 Cloudflare Error 1033 —— "这就是你说的优化好了?"
2. "中转管理那边 ui 界面大改,现在难看的要死"
============ 一、隧道 1033/502/530 的真因 ============
之前我只测 15~25 次就说"可用率 100%",太草率 ——
实际是**间歇性假活**:隧道显示 ha_connections=2 连着,
公网却 502/530/000。watchdog 日志把过程记了下来。
根因:**4G 链路静默断开 TCP 连接**(不发 FIN/RST,双方都以为还活着)。
Cloudflare 把请求发到这条僵尸连接上就回错误。
Linux 默认 tcp_keepalive_time = **7200 秒(2 小时)**才发现,
中间这段时间服务一直"看着在线、实际打不开"。
修法(都有实测数据支撑):
1. tcp_keepalive 30 秒 + 每 10 秒探测 3 次
=> 最多约 60 秒发现僵尸连接并重连
2. 隧道连接数 2 -> 4
(对照实验:http2+2 只有 1 条连上时公网 0/12;
http2+4 稳定 12/12。连接数太少会出现"只有 1 条"的危险窗口)
3. 协议定为 http2(cloudflared 自检说 TCP 7844 被墙,
但实测 http2 反而比 quic 稳)
实测:http2+4 之后公网 40/40 = 100%,最慢 1.3 秒,0 次重连。
============ 二、中转管理后台重做 ============
原来是几百行内联在 relay.py 的 f-string 里(HTML+CSS+JS 全挤一起),
改一处样式要在一堆转义花括号里翻。拆成独立模块 relay/admin.py。
新版设计(对齐官网那套白色简洁):
* 吸顶顶栏:品牌 + 运行状态 + 版本/内存/请求数
* 公网地址给最大权重(这是用户最需要复制的东西)
* 四个数字卡(在线/会话数带颜色区分)
* 设备表卡片:状态圆点、等宽配对码、行悬停
* 工具卡:网络体检 + 下载区并排
* 空状态改成图标+引导文案,不再是干巴巴一句
* 窄屏时表格自动变卡片
relay.py 从 2089 行缩到 1550 行。
============ 三、顺手修的两个真 bug ============
1. 设备记录只增不减
self.devices 从不清理,被控端每次换端口/重装就多一条,
实测一天攒了 75 条僵尸记录,/v1/health 从几 KB 涨到 20KB。
加 DEVICE_TTL(6 小时)在 snapshot() 里清。
2. 配对码映射也没清
RELAY.codes 和 devices 是一对孪生 dict,
只清 devices 会留下指向已删设备的孤儿条目 ——
管理页显示"配对码占用 131",实际一台设备都没有。
现在一起清。
============ 四、我自己造的两次事故(如实记录) ============
1. 删文件删过头
用"从 _admin_page 截到文件末尾"的方式替换方法,
但 _admin_page 后面**还有 _esc/_kb/_uptime/class Server/main()**。
结果 relay 起不来,systemd 重启 24 次,所有接口 000。
教训:**"删到文件末尾"之前必须先确认末尾有什么。**
2. ADMIN_PATH 引用了不存在的常量
路由里写的是字面量,我却在新代码里用了 ADMIN_PATH,
管理页一访问就 NameError。现在统一定义了常量。
二者都是"改完没立刻验"造成的。现在每次改完都跑一遍
管理页 + 官网 + 关键接口的回归。
|
2026-10-07 09:34:53 +08:00 |
|
xmz7891
|
8c4ee6df08
|
修真凶:4G 链路静默断连导致隧道"假活"(用户撞上 Error 1033)
现象:用户早上打开 relay.xxmxxs.top 看到 Cloudflare Error 1033。
watchdog 日志把过程记下来了:
公网不通(HTTP 000,ha_connections=2) <- 隧道显示连着,公网不通
公网不通(HTTP 502,ha_connections=2)
公网恢复(HTTP 200) <- 自己又好了
也就是说:**间歇性假活**,短样本测不出来。
我之前只测 15~25 次就说"可用率 100%",太草率了。
根因:4G 链路会**静默断开** TCP 连接 —— 不发 FIN/RST,双方都以为还连着。
Cloudflare 把请求发到这条僵尸连接上,就回 502/530/1033。
Linux 默认 tcp_keepalive_time = **7200 秒(2 小时)**才发现连接死了,
所以中间这段时间服务一直"看着在线、实际打不开"。
tunnel.log 最近 3000 行的佐证:
i/o timeout 212 次
Retrying connection 127 次
Lost connection 22 次
修法:
1. TCP keepalive 调到 30 秒 + 每 10 秒探测 3 次
=> 最多约 60 秒发现僵尸连接,cloudflared 立刻重连
(/etc/sysctl.d/97-ykxmz-keepalive.conf)
2. 隧道连接数 2 -> 4,多几条冗余
3. 修 relay 从不清理设备记录的问题:
self.devices 只增不减,被控端每换端口/重装就多一条,
实测一天攒了 75 条僵尸记录,/v1/health 从几 KB 涨到 20KB。
加 DEVICE_TTL(6 小时)在 snapshot() 里清掉。
实测(修完立刻验证):
设备侧 公网 15/15 HTTP 200
Windows 公网 25/25,中位 520 ms,最快 463 ms
health 20931 字节 / 75 台 -> 152 字节 / 0 台
|
2026-10-07 09:15:27 +08:00 |
|
xmz7891
|
2b40968d23
|
CHANGELOG 补上安装/卸载卡死的修复说明
|
2026-10-07 03:14:21 +08:00 |
|
xmz7891
|
de65654676
|
修 --quiet 卸载日志的两个问题(静态检查抓出来的)
1. run_agent.py 用了 Path 但没导入
静态检查报 "undefined name 'Path'"。它被 try/except 吞了,
所以卸载功能本身没坏,但**日志从来没写成过** ——
属于"看着没事、其实功能是死的",最容易被漏掉的那种。
2. 日志写到刚被删掉的数据目录里
原来是 Path(data_dir())/"uninstall.log",但 do_uninstall()
会把整个数据目录删掉,写完等于白写。
改到 %TEMP%\ykxmz-uninstall.log。
现在日志能正常落盘,内容长这样:
开始卸载 ykxmz
已停止其它 ykxmz.exe 实例(本进程 15572 除外)
没有开机自启项
已删除数据目录 ...
程序目录交给安装包的卸载器删除
卸载完成。
最终全量套件(33 个阶段):
30 PASS + 3 SKIP(锁屏) + 0 FAIL
新增的「安装包完整周期」PASS
|
2026-10-07 03:14:11 +08:00 |
|
xmz7891
|
c7c0484a27
|
修安装包卸载卡死的真凶:attach_console 在无控制台时阻塞
排查过程(一步步排除,记录下来免得以后重踩):
1. 现象:从控制面板卸载,进度条永远走不完。
2. 先怀疑 input():安装器用 runhidden 调 --uninstall,没有 stdin,
input() 会一直等。加了 isatty 保护 —— **没解决**。
3. 再怀疑 Popen 没重定向 stdio:_schedule_self_delete 起的 cmd
继承了父进程句柄,capture_output 会等管道 EOF。加了
DEVNULL 重定向 —— **还是卡**。
4. 又发现 _kill_running 用 taskkill /IM ykxmz.exe 把正在卸载的
自己杀了(--uninstall 就跑在 ykxmz.exe 里),
导致后面的"删自启/删数据"没执行。改成 /FI "PID ne self" ——
自启能删了,**卸载仍然卡**。
5. 最后看进程状态才定位:卡住的 ykxmz.exe 只有 **1 个线程、
0.16 秒 CPU、19MB 内存** —— 明显停在启动早期,根本没干活。
那就是 cli_uninstall 第一行的 attach_console():
它对 --windowed 程序调 AllocConsole(),再 open("CONIN$")。
在没有控制台的隐藏进程里,这一步**永久阻塞**。
修法:加 --quiet。安装包的卸载器用
ykxmz.exe --uninstall --keep-dir --quiet
--quiet 下完全不碰控制台(不 AllocConsole、不 input),
结果写进 uninstall.log 供排查。
实测:不带 --quiet 无限卡住;带了 **0.6 秒返回**。
顺带修的两处(也是实测发现的):
* 安装器关不掉以管理员身份运行的旧实例 -> Inno 弹"无法关闭所有
应用程序",静默下被压成"终止",**整个安装回滚**。
改成 PrepareToInstall 里自己 taskkill /F。
* 测试脚本把注册表路径写成 HKCU\...(少了冒号),
PowerShell 静默返回空,"开机自启"永远误报失败。
最终实测(test_installer_cycle.py,20 项):
静默安装 退出码 0,7.1 秒
配对码 正确写入 C:\ProgramData\ykxmz\data\settings.json
进程/端口 在跑,8765 监听
开机自启 已设置
静默卸载 1.2 秒
卸载清理 安装目录 / 数据目录 / 注册表 / 进程 / 自启 全部干净
20 项全通过
套件新增一个阶段:「安装包完整周期」。
|
2026-10-07 02:21:36 +08:00 |
|
xmz7891
|
1e73a789d1
|
测试套件:锁屏识别 + 退出码取法 + BOM 兜底
一、锁屏时截屏类测试误报
电脑锁屏后,test_agent_protocol 的 5 项截屏断言、以及两个
Java 端到端阶段必然失败 —— 但那是环境限制,不是代码问题。
不区分的话汇总表一片红,真正的失败反而被淹没。
现在启动时用 OpenInputDesktop 探测,锁屏时这些阶段记
SKIP(锁屏) 而不是 FAIL,并在开头黄字提示。
二、管道后读 $LASTEXITCODE 不可靠
形如
& python xxx.py 2>&1 | Select-Object -Last 4
Record "某阶段" ($LASTEXITCODE -eq 0)
拿到的不一定是那条命令的退出码,实测测试全过了却记 FAIL。
改成先把码存进变量再走管道。共修 4 处。
三、edit 工具会剥掉 .ps1 的 UTF-8 BOM
没有 BOM 的 .ps1 会被 PowerShell 按 GBK 读,中文全乱码,
接着一堆 "Unexpected token" 语法错误,脚本直接跑不了。
这个坑今天踩了两次。加了 _build/fix_ps1_bom.py 统一补 BOM,
并顺带检查 .bat 是不是 GBK。
跑出来:33 个阶段 —— 30 PASS + 3 SKIP(锁屏),0 FAIL。
|
2026-10-07 01:57:20 +08:00 |
|
xmz7891
|
df8056dd16
|
修四个安装/卸载卡死的 bug(都实测复现过)
1. 卸载里 input() 在无 stdin 时永久阻塞
从控制面板卸载**永远卡住不结束**。安装包的 [UninstallRun]
用 runhidden 调用,没有 stdin,input() 就一直等。
改成只在 sys.stdin.isatty() 时才等回车。
2. Popen 没重定向子进程 stdio,导致 capture_output 等管道 EOF
_schedule_self_delete 起的 cmd 继承了父进程的 stdout/stderr 句柄,
任何用 capture_output 调 --uninstall 的地方都会一直等,
表现为"程序卡死"(实测 90 秒不返回,改后 0.2 秒)。
Popen 加 stdin/stdout/stderr=DEVNULL。
顺带把延迟从 timeout 换成 ping —— timeout 在 DETACHED_PROCESS
下没有控制台会立刻失败,等于没延迟直接删。
3. _kill_running 用 taskkill /IM ykxmz.exe 把**自己**杀了
--uninstall 本身就跑在 ykxmz.exe 里,这条命令一发,
后面的"删开机自启""删数据目录"全都没执行。
现象:卸载看着完成了,HKCU 的自启注册表项还留着。
改成 /FI "PID ne <self>" 排除自己。
4. 安装器关不掉以管理员身份运行的旧实例 -> 整个安装回滚
Inno 自带机制只会"请求"对方退出,遇上常驻的服务进程关不掉,
弹"无法自动关闭所有应用程序",静默模式下被压成"终止",
安装直接回滚 —— 用户看到的是"装了没反应/还是旧版本"。
改成 PrepareToInstall 里自己 taskkill /F,关掉 Inno 那套。
另外修了两个卸载残留:
* 安装目录留个空壳 -> [UninstallDelete] 加 dirifempty
* ProgramData\ykxmz 没删 -> 加进 [UninstallDelete]
还有一处 Inno 写法陷阱(踩了三次):
注释里写 [区段名] 或 {常量} 会让编译直接失败,
因为 [Code] 解析器连注释内容也处理。
加了 _build/check_iss.py 专门扫这个,并进全量套件。
实测结果:
静默安装 5 秒内退出,退出码 0,配对码正确写入,程序自动跑起来
静默卸载 0.7 秒退出(原来无限卡住)
全量回归 29 项全通过
卸载逻辑 11 项全通过
|
2026-10-07 01:19:11 +08:00 |
|
xmz7891
|
e29b741ddf
|
收尾:CHANGELOG 补 1.7.1 + 清历史产物 + 修验证脚本路径
- _build/CHANGELOG.md 补上 1.7.1 条目(发版说明的唯一来源)
- 两个验证脚本还在用旧的管理页路径 zhingzhuanzhan,
跑起来会误报失败,改成 zhongzhuanzhan
- dist 里清掉 1.6.x 的历史产物(Gitee 发行版里有,删了能下回来)
|
2026-10-07 00:17:55 +08:00 |
|
xmz7891
|
7b7830d8c5
|
阶段3+4+5:管理页白色化 + 局域网诊断 + 全量验收
阶段3 管理页
- 清掉全部深色主题残留(地址框 #0b1220、按钮悬停 #262c36、
删除按钮 #2d1214 等 11 处),现在 0 处深色
- 接口全部正常:health / downloads / netcheck
阶段4 局域网诊断
- 查清楚"电脑不在同一网络"是怎么回事:被控端是**对的**
(lan_ips() 报 ['192.168.2.64', '169.254.83.107'],真实地址排第一),
是手表当时真不在 192.168.2.x —— 可能连了 5G 频段的另一个路由器,或开着移动数据
- 所以改了手表的提示:不再只说"不在同一网络",
而是把**双方地址都打出来**,用户一眼能看出是网段不同还是连错 WiFi
- 端到端验证局域网路径:认证 1 ms,功能列表 207 项
阶段1 结论修正(一天内推翻自己两次,如实记录)
- 第一次判断"优选 IP 无用":错。半小时内确实能避开超时 IP
- 第二次固定优选 IP:也错。那些实测 138ms 的 IP 半小时后全不通了
—— Cloudflare anycast 可达性随运营商路由变化,固定 IP 等于钉死到会失效的地址
- 最终:不固定,靠 GeoDNS + cloudflared 重试 + ha-connections 2 冗余
- 撤销后立即变好:连接稳定 2 条、超时告警 0 次、设备侧延迟 4.40s -> 1.66s
- **真正的瓶颈**:分段实测发现本机到 Cloudflare 的**裸 RTT 就 1439 ms**,
比经隧道的请求(1215 ms)还慢。说明瓶颈是这条网络的 4G 上行本身,
不是设备的隧道。公网侧已无优化空间。
- 局域网 118 ms vs 公网 1215 ms,快 10 倍 —— 这才是该走的路
阶段5 验收
- 全量回归 29 项全通过
- 设备侧验收 38 项全通过
- 成品:setup exe 14.37 MB / server zip / watch apk,已同步到设备下载区
|
2026-10-07 00:13:10 +08:00 |
|
xmz7891
|
566d9e384f
|
阶段1+2:恢复 HTTP/2 加速 + 重做官网
阶段1 网络加速
- 发现隧道在跑 QUIC(ExecStart 丢了 --protocol http2),
实测 QUIC 可用率 48% vs HTTP/2 80%+,是重大性能回退。
恢复后公网延迟从 4141ms 降到 690ms(快 6 倍),可用率 92%。
- 优选 IP 结论:无效。hosts 覆盖确实生效了(日志里连的就是指定 IP),
但落点仍是 lax —— 运营商把所有 Cloudflare anycast 都路由到美西。
已清掉这段无效配置。
- BBR 不可用:设备是自定义内核 6.4.0-rc4-jsbsbxjxh66,
没编 tcp_bbr 模块也没配置项。改用 cubic + 调优:
TCP 缓冲 4MB、tcp_fastopen=3、tcp_slow_start_after_idle=0
(长轮询场景关键)、tcp_tw_reuse=1。
阶段2 官网重做
- 拆出独立模块 relay/landing.py(relay.py 已两千多行,
再塞 HTML 没法看)
- 信息结构参考成熟产品站:吸顶导航 / Hero / 九宫格功能 /
三步上手 / 下载 / FAQ / 页脚
- 保持白色简洁:底白、浅灰面板、细边框、大量留白
- logo 用内联 SVG(ICO 放 img 里实测是破图)
- 管理入口从官网移除(管理不对外开放)
过程中被自己的测试抓到两个 bug:
- 模块叫 site.py 会和 Python 标准库重名,import site 拿到的是标准库
- _logo() 里把 % 格式化和字符串拼接混用,直接抛 TypeError
|
2026-10-06 23:53:19 +08:00 |
|
xmz7891
|
9b1edbf378
|
chore: 清掉临时的提交消息文件
|
2026-10-06 23:10:51 +08:00 |
|
xmz7891
|
caf774791f
|
修官网 .lead 的文字色
那一处还是深色主题配的浅灰(#c9d1d9),白底上几乎看不见。
上一版脚本没改成功,因为判断条件写成了新值已存在就跳过,
而 color:#1f2328 在别处已经出现过,导致整个替换被跳过。
|
2026-10-06 23:10:49 +08:00 |
|
xmz7891
|
51565112b9
|
网页改成白色简洁 + 修官网两个 bug
配色
- 官网 / 登录页 / 管理页从 GitHub 深色风改成白色简洁
(用户的设计理念:所有东西都要白色简洁)
- 白底上把浅灰文字换成深色,链接蓝也调深一档保证对比度
修 bug
- 管理入口的链接拼接写坏了,页面上显示成 href= + ADMIN_PATH +,
点了打不开管理页
- .lead 用的是深色主题的浅灰,白底上几乎看不见
其他
- 清掉提交信息里的 BOM(PowerShell 的 Set-Content 加的)
|
2026-10-06 23:10:08 +08:00 |
|
xmz7891
|
81e923a0aa
|
1.7.1:修白屏 + 配对 + 官网管理页分家
手表端
- 修「连上后一片白屏」:initTabs() 从来没被调用过,
导致 tabDefs 为 null、selectTab 直接 return,
标签栏里只剩一个孤立的滑动指示器
- 应用名改成 ykxmz
被控端
- 修「设备已关闭新设备配对」:配对码本身就是密钥,
拿着正确的码就该放行。老逻辑"首台配对后自动关窗"会把
手表重装的人永久挡在外面
- 应用名改成 ykxmz
服务器端
- / 改成官网(公开,简洁介绍 + 下载)
- /zhingzhuanzhan 中转站管理(要登录),/admin 自动跳转
- 管理账号 xmz7891,可改
- 官网 / 登录页 / 管理页都加上 favicon
|
2026-10-06 23:08:07 +08:00 |
|
xmz7891
|
bc7a022c6e
|
chore: 发行版说明更新脚本(补 tag_name 字段)
|
2026-10-06 23:08:07 +08:00 |
|
xmz7891
|
a136f9d2e4
|
chore: 发行版说明只放更新内容 + 下载区改用安装包
|
2026-10-06 23:08:07 +08:00 |
|
xmz7891
|
ce87ec1650
|
1.7.0:常规发布
- 版本号 1.7.0 -> 1.7.0
- 重建三端并打包
- 关键测试全过
|
2026-10-06 23:08:07 +08:00 |
|
xmz7891
|
4c88ca901f
|
1.6.2:常规发布
- 版本号 1.6.2 -> 1.6.2
- 重建三端并打包
- 关键测试全过
|
2026-10-06 23:08:07 +08:00 |
|
xmz7891
|
65dc2fad65
|
1.6.1:常规发布
- 版本号 1.6.1 -> 1.6.1
- 重建三端并打包
- 关键测试全过
|
2026-10-06 23:08:07 +08:00 |
|
xmz7891
|
07e96a9595
|
1.6.0:重写 README,面向老师 / 电教 / 开发者 / 普通用户
README
- 原来的 README 是开发者视角(在解释我为什么没做某功能),
而且通篇 AI 腔。这次按用户视角重写,讲怎么用。
- 开头就分流:老师 / 电教 / 开发者 / 普通用户,各看各的
- 定位改成教室电脑手表遥控,主场景是老师上课管教室那台电脑
- 加了电教专用一节:机房同传部署、脚本批量推、网络规划、权限收口
- 加了开发者一节:三端怎么编译、怎么跑测试、怎么打包
- 加了通信协议一节
- 常见问题、排错顺序、错误对照表都补全
版本号
- 1.5.0 -> 1.6.0(versionCode 107)
- 顺手写了通用的版本升级脚本 _build/bump_version_generic.py,
以后升版本一条命令就行,不用手改十几个文件
|
2026-10-06 23:08:07 +08:00 |
|
xmz7891
|
60d1bdfe58
|
中转站加下载区 + 大文件流式发送
下载区
- 管理页加一张「下载最新版」卡片,就一个按钮
自动指向 /v1/downloads 里最新的被控端整包,不用每次改页面
- GET /v1/downloads 列出成品
- GET /download/<文件> 直接给文件
只允许白名单后缀(.apk/.zip/.txt/.exe/.idsig)
文件名严格校验,挡目录穿越(../ 和 ..%2F 都试过)
- 卡片下面附一个 Gitee 历史版本入口
大文件
- 原来 read_bytes() 一次性读进内存再 write。
19MB 在 460MB 的设备上勉强,更要命的是 Cloudflare
有 100 秒的源站响应上限,数据越晚开始流动越容易被掐。
- 改成 64KB 分块流式发送,边读边发
实测:19MB 公网下载 40 秒完成,字节数完全一致
版本
- 三端分开打包(原来是一个合并 zip)
ykxmz-agent-1.5.0.zip 被控端
ykxmz-server-1.5.0.zip 服务器端
ykxmz-watch-1.5.0.apk 控制端(直接给 APK)
|
2026-10-06 23:08:06 +08:00 |
|
xmz7891
|
e86679294a
|
chore: 排除 cloudflared 二进制与测试临时数据
|
2026-10-06 23:08:06 +08:00 |
|
xmz7891
|
ebd87300b9
|
chore: 排除 PyInstaller / Android 构建中间产物
|
2026-10-06 23:08:06 +08:00 |
|
xmz7891
|
8111fb6d97
|
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 控制端
|
2026-10-06 23:08:05 +08:00 |
|
xmz7891
|
edac13dbe2
|
chore: 排除 dist/ 构建产物(改用 release 附件分发)
|
2026-10-06 19:01:39 +08:00 |
|
xmz7891
|
b62ac865ea
|
ykxmz 1.5.0:修复手表端 TLS 证书校验 + MTU 修复网络稳定性
被控端
- 修复 MTU 问题:夹 MSS + 路径 MTU 探测(公网可用率 48% -> 100%)
- 复用 HTTP 连接(免掉每次新建 TLS 的 30% 握手失败)
- 每分钟回头找局域网,自动切回快 400 倍的路径
- 画面心跳帧:静止时省流量,客户端仍知道流是活的
- 锁屏时给出可操作的提示
手表端
- 内置 GTS Root R4 / GlobalSign Root CA,修 SSLHandshakeException
- 连接错误翻译成人话(证书/时间/解析/超时分别提示)
- 经中转连上后用服务器报的地址刷新已配对记录
- 新增局域网连接页面、服务器延迟检测
服务器端
- 支持 chunked 请求体(修每 16 秒一次的连接错乱与大面积 502)
- 网络调优脚本 net-tune.sh(开机自动应用)
- 管理页:批量删除、网络体检、下载区
- 隧道改 HTTP/2 + 双连接
文档
- 许可协议 / 安装前必读 / 安装后须知(作者:小麦子)
|
2026-10-06 19:01:05 +08:00 |
|