xmz7891
|
8dc47d0f48
|
2.1.2:隧道稳定性加固(继续修「网站还是容易挂」)
2.1.1 修掉了 nettune 死锁之后,公网上到 200,但**过一阵又 530**。
这次抓到的是隧道自己:
cloudflared 的 precheck 输出(tunnel.log):
| UDP Connectivity region1.v2.argotunnel.com FAIL QUIC connection failed |
| TCP Connectivity region1.v2.argotunnel.com FAIL HTTP/2 connection is blocked or unreachable |
| Cloudflare API api.cloudflare.com:443 PASS API is reachable |
| ERROR: Allow outbound QUIC traffic on port 7844 or use HTTP2. |
也就是 **443 通、7844 不通**,而且是**间歇性**的(过几分钟自己又好了,
然后再次断)。Cloudflare 隧道的边缘连接走 7844(QUIC 走 UDP 7844、
HTTP/2 走 TCP 7844)。这条线路对它是间歇性阻断 —— 属于运营商那一侧,
设备改不了,但可以让 cloudflared 自己去适应。
== 两个改动 ==
1. **协议从写死改成 auto**
lanremote-tunnel --protocol quic -> --protocol auto
lanremote-tunnel-h2 --protocol http2 -> --protocol auto
原来两条实例各赌一个协议:那一刻 7844 的 UDP 和 TCP 都不通就一起死,
没有退路(实测连接数掉到 1/0、公网 530)。改成 auto 之后
cloudflared 自己在 QUIC / HTTP/2 之间挑当前能通的那条,
10 条连接全都有自适应能力。
2. **--retries 5 -> 1000000**
原来失败 5 次就退出、靠 systemd Restart=always 拉起来,
每次退出到重启之间网站都是断的。而这条线路的抖动是分钟级的 ——
实测隧道每 3~10 分钟就被重启一次(19:37 / 19:40 / 19:43 / 19:50 / 19:52),
公网跟着一起断。拉大之后抖动期间连接自己恢复,整条隧道不退出。
3. 顺手加日志轮转(tunnel.log 已经 2.98 MB,这台机器硬盘不大)。
== 实测 ==
改前:连接数 1/0,公网 530
改后:连接数 7+2(接近满配 10),公网连测 5 次全部 200
== 同步 ==
relay/deploy.sh 里的 MAIN_PROTO / ALT_PROTO 也改成 auto、
TUNNEL_BASE 的 --retries 也拉到 1000000,新机器装出来就是加固过的。
全量回归 46 个阶段全部通过。
|
2026-10-08 20:04:35 +08:00 |
|
xmz7891
|
4e64bc7920
|
remove temp
v2.1.2
|
2026-10-08 19:52:09 +08:00 |
|
xmz7891
|
c5fa4eb517
|
2.1.1:修一个会让网站整个挂掉的死锁(「网站容易挂」的真因)
2.1.0 的服务端包里带着这个死锁,所以要单独发一版让部署的人拿到修好的包。
== 死锁 ==
lanremote-nettune.service Before=lanremote-tunnel.service
net-tune.sh 第 4 步 systemctl restart lanremote-tunnel
nettune 在**自己还没结束**的时候去 restart 一个"必须等自己结束
才能启动"的服务 -> systemd 判定顺序冲突 -> restart 永不返回
-> nettune 卡在 activating -> 隧道被 Before= 挡着起不来 -> 公网 530。
设备上实测卡了 9 分半(CGroup 里能看到 net-tune.sh 下面挂着
一个 etime 09:32 的 systemctl restart lanremote-tunnel)。
journalctl 里只有 nettune 停在 activating、**一句报错都没有**;
手动跑 net-tune.sh 却完全正常(那时没有 Before= 约束)。
是靠这次新加的监测单元抓到的(fails=3 / last_code=530)。
== 改法 ==
unit 加 Environment=NETTUNE_SERVICE_CALL=1,脚本据此跳过第 4 步。
第 4 步只在人工重跑时才有意义(改完 MTU/MSS 让已建隧道用上新参数),
开机时顺序就是调优先、隧道后,不需要它。
三处都改了:net-tune.sh 认开关;deploy.sh / install.sh 生成的 unit 带上它。
没选 systemctl restart --no-block:那只是把阻塞变异步,开机时仍然
做一次无谓重启(隧道刚起来就被停掉),也没解决"不该在开机时重启"。
== 为什么以前没暴露 ==
设备上那份旧 unit 的 ExecStart 里有 iptables ... || ...,
systemd 不支持 ||,所以它在第 2 条 ExecStart 就报错退出,
根本没走到会死锁的那一步。把 unit 换成正确形状之后才露出来。
|
2026-10-08 19:52:08 +08:00 |
|
xmz7891
|
05d2287fbf
|
remove temp
v2.1.1
|
2026-10-08 18:56:44 +08:00 |
|
xmz7891
|
6adf7acbfb
|
修「网站容易挂」的真因:nettune 和隧道之间的死锁
用户报「网站还是容易挂」。查到最后是个 systemd 顺序死锁 ——
而且**是我自己引入的**(换掉设备上那份过期 unit 之后才露出来)。
== 死锁链条 ==
lanremote-nettune.service:
Before=lanremote-tunnel.service <- 我必须先于隧道启动完
ExecStart=/bin/bash net-tune.sh
net-tune.sh 第 4 步("让已经建立的隧道用上新参数"):
systemctl restart lanremote-tunnel <- 在这里永久阻塞
nettune 在**自己还没结束**的时候去 restart 一个"必须等自己结束
才能启动"的服务 —— systemd 判定顺序冲突,restart 一直不返回。
证据(设备上抓的):
CGroup: /system.slice/lanremote-nettune.service
├─13388 /bin/bash net-tune.sh
└─13428 systemctl restart lanremote-tunnel
那个 systemctl 的 etime 是 09:32 —— 卡了 9 分半。
后果:nettune 一直 activating -> 隧道被 Before= 挡着起不来 ->
公网 530。而 journalctl 里只看得到 nettune 停在 activating,
没有任何报错,特别难查。
== 为什么以前没暴露 ==
设备上原来那份 unit 的 ExecStart 里有个 iptables ... || ...,
systemd 不支持 ||,所以它**在第 2 条 ExecStart 就报错退出**,
根本走不到"重启隧道"那一步 —— 死锁被那个 bug 遮住了。
我把 unit 换成正确形状之后,死锁才露出来。
另一个迷惑点:**手动跑 net-tune.sh 完全正常**(输出"没有正在跑的
隧道,跳过"),因为手动跑时不在服务上下文里、没有 Before= 约束。
== 改法 ==
第 4 步的本意是"改完 MTU/MSS 后让已建立的隧道用上新参数",
这个功能有用,但**只该在人工重跑时做** —— 开机时顺序本来就是
nettune 先、隧道后,那时隧道还没起来。
所以 unit 里加 Environment=NETTUNE_SERVICE_CALL=1,
脚本看到就跳过第 4 步。三个地方都改了:
relay/net-tune.sh 认这个开关
relay/deploy.sh 生成的 unit 带上它
relay/install.sh 同上(老机器走的安装路径)
没选 systemctl restart --no-block:那只是把阻塞变异步,
开机时仍然做一次无谓的重启(隧道刚起来就被停掉),
也没解决"不该在开机时重启"这个语义问题。
== 另外 ==
setup-monitor.sh / monitor.sh / rescue.sh 这一整套这次也真的
在线上跑起来并验证过了:
18:13:03 紧急预案启动:公网连续 3 次不通
18:13:07 现场已记录 + [1/5] 重启隧道
18:13:59 ✅ 第 1 级救回来了,公网已恢复
18:13:59 已把「第 1 级奏效」记进知识库
死锁这次就是被监测单元抓到的(fails=3 / last_code=530)。
全量回归 46 个阶段全部通过。
|
2026-10-08 18:56:44 +08:00 |
|
xmz7891
|
4ba18fdae9
|
remove temp
|
2026-10-08 18:36:50 +08:00 |
|
xmz7891
|
3f18e6bb40
|
2.1.0:网站自动抢救 + 后台抢救报告分区 + 网络热门音效 + 全量测试后台静默
按用户定的规矩,这一轮有新功能,升第二位:2.0.0 -> 2.1.0。
== 一、网站挂了会自己救 ==
用户原话:「网站还是容易挂,建议继续增加隧道稳定性,并在后台设立
检测单元,实时检测网站状态,一旦检测到网站挂了,立即启动紧急预案」。
新增 relay/monitor.sh + lanremote-monitor.timer:
每 60 秒从**公网**查一次 /v1/health(不是查 127.0.0.1)
为什么不查本机:本机 200 只说明中转进程活着,用户真正关心的是
经过 Cloudflare 边缘 + 隧道 + 本机三段之后能不能打开。实测过
隧道两条实例都掉了、本机 curl 照样 200,只有从公网测才发现 502。
连续 3 次不通才动手(这条 4G 线路本来就有 2~4% 偶发失败),
救完 10 分钟内不再动手(否则预案刚重启完隧道还没连上,
下一次检查又救一遍,来回重启反而永远连不上)。
新增 relay/rescue.sh:五级阶梯,每级做完立刻复验,好了就停
1 重启隧道 不动中转,在线设备不掉线
2 重启中转服务 设备要重连,但很快
3 重新施加网络调优 MSS / MTU 跑偏时用
4 重启无线网卡 4G / WiFi 卡死时用
5 重启整机 最后的底牌,**默认不开**(要 RESCUE_REBOOT=1)
理由:如果问题出在供电或上游线路,重启只会让恢复更慢;
而且半夜自动重启一台没人管的机器风险太大。
== 二、抢救报告 + 后台分区 ==
用户原话:「如果紧急预案未能解决,则将问题打包成 .txt,然后在
中转站设置里面加一个分区专门放还有下载这个 txt。解决后将解决问题
的方法加入紧急预案」。
* 五级都没救回来 -> 把现场(内存/进程/隧道连接数/路由和 MTU、
三份日志尾部、内核异常)整理成 rescue_reports/<时间戳>.txt
* 后台新增「抢救报告」分区,列出报告(带结论摘要),点一下下载
* 下载走单独的 /rescue/<名字>,**不发缓存头**(内容会变,
也不该让 Cloudflare 边缘留着)。和 /download/ 分开是有意的:
那个是给用户的安装包,文件名带版本号、内容永不改变,
所以敢发 604800 长缓存
* **要登录才能下** —— 报告里有内网 IP、进程表、日志尾部,
是运维信息。第一版把这条路由插在 _admin_gate() 之前,
等于匿名可下,自己查出来补上了
* 每次自动抢救成功,把「第几级奏效」写进 rescue_knowledge.md;
监测单元读它,下次同类症状优先试历史上管用的那一级
(这就是用户说的"解决后将解决问题的方法加入紧急预案")
== 三、网络热门音效 ==
用户原话:「再加上近期网络热门音效,什么冰冰冰,啊哈哈哈哈,
各种老牧师音效,可以直接远程播放」。
**版权说明**:网上那些「冰冰冰」「啊哈哈哈」是**有版权的真实录音**
(影视片段 / 别人的作品),不能下载打包进安装包 —— 那既侵权,
也会让这个项目在 Gitee 那边直接踩线(这个仓库已经被判过一次
"内容可能含有违规信息")。
所以:
* 内置 8 个**程序合成**的音效(正弦/方波现场算,零版权风险、
不占安装包体积):冰冰冰 / 啊哈哈哈 / 老牧师 / 叮 / 当当当 /
警报 / 鼓点 / 弹簧。名字对着感觉来,但不是那些录音本身
* 做「自己放进来」的目录:mp3/wav 丢进 data/sfx/ 就自动出现在
列表里,照样能远程播。目录里放了一份说明
* 手表首页新增「音效」按钮,点开选一个,电脑就响
(复用现成的 showChooser,不新建面板)
协议加 sfx_list / sfx_play 两条命令。
== 四、修「安卓设置页一片空白」 ==
用户报的。根因:设置页**建好了但忘了 root.addView()** ——
showOnly() 让它 VISIBLE 也没用,它根本不在视图树里。
这类"漏挂"编译完全不报,只在运行时表现为空白。已加断言
_tests/test_watch_panels_wired.py 盯着:每个面板都必须挂到 root、
并且纳入 showOnly()。
== 五、全量测试改成后台静默 ==
用户原话:「把全量测试改为在后台静默运行不要打开任何窗口或锁屏」。
会开窗口的 6 个阶段(含开**全屏**那个)现在跑在独立的 Windows
隐藏桌面上(_tests/hidden_desktop.py)。窗口是**真的** —— 全屏、
可见、几何、Win32 枚举都真实,测试有效性一点没损失 ——
只是不在用户眼前,全屏也不会锁屏。
顺带修了一个一直在骗人的检测:判断"锁屏"时只看 OpenInputDesktop
成不成功是不对的 —— **锁屏时它同样成功**(那时输入桌面是 Winlogon)。
于是截屏类测试在锁屏时被记成 FAIL 而不是 SKIP,把真问题淹掉了
(用户电脑锁了一夜,我就是被这个误导过)。改成看输入桌面的**名字**。
现在锁屏也能跑全量:该 SKIP 的 SKIP,该跑的照跑。
== 六、顺手修的真 bug ==
* windows/lanremote/server.py 的 sfx_play 分支用了不存在的变量名 body
(应该是 msg)-> **一调就 NameError**。pyflakes 那关抓出来的,
说明"未定义名"这条检查有用。
* _build/package_remote.py 的文件白名单只有 .py/.sh/.ico,
**.service/.timer 收不进服务端 zip** -> 新机器上监测根本不启动,
而且不报错、只是静默不跑。已加。
* protocol.py 被我补丁插入了 LF 行到 CRLF 文件里 -> 行尾混用,编码审计红。
* deploy.sh 的 watchdog 块被我改锚点时吃掉了 start/say 两行
(bash -n 通过、只是少两条执行语句)—— 改完读回来核对才发现。
* _build/CHANGELOG.md 的 2.1.0 说明里写了"管理后台",
触发对外材料清洁度检查。改成不提管理页(用户定的规矩:
对外只能提官网)。
全量回归 46 个阶段全部通过。
|
2026-10-08 18:36:50 +08:00 |
|
xmz7891
|
50f4e1fb6f
|
add LICENSE.
Signed-off-by: xmz <3235109770@qq.com>
v2.1.0
|
2026-10-07 15:08:01 +00:00 |
|
xmz7891
|
a6001775dd
|
remove temp
|
2026-10-07 23:04:48 +08:00 |
|
xmz7891
|
d5f35f7ed5
|
文档补一句:EzHub 统一走 HTTPS 推
用户原话:「ezhub用https推,我朋友说ssh太麻烦他不想改」。
SSH 公钥在 EzHub 服务端一直没配通(加了公钥仍 Permission denied),
维护者觉得改起来麻烦,所以统一改走 HTTPS + 令牌。
remote 地址里不含令牌(git remote -v 会打印出来,容易泄),
推送时由 _build/ezhub_push.py 临时拼 URL,推完不留痕。
令牌存在仓库外 %USERPROFILE%\.ykxmz\ezhub_token。
ezhub remote 已从 ssh:// 改成 https://git.261826.xyz/xmz/ykxmz.git。
另外本地补齐了 v1.9.0 / v2.0.0 两个 tag。
注意 Gitee 那边是 annotated tag,ls-remote 看到的是 tag 对象的 SHA,
直接拿它建本地 tag 会指向一个本地不存在的对象(git tag 都列不出来)。
要用 refs/tags/<t>^{} 拿 peeled 的 commit SHA。
|
2026-10-07 23:04:48 +08:00 |
|
xmz7891
|
77295bd258
|
remove temp
v2.0.0
|
2026-10-07 23:02:44 +08:00 |
|
xmz7891
|
4295b482a1
|
2.0.0:安装时可设中转服务器 + 手表端新增设置页 + 中转默认留空
按用户定的版本规矩,这一轮既有新内容又有重构,所以升第二位:
1.9.0 -> 2.0.0(最后一位清 0)。
== 一、安装时就能设中转服务器(用户要求)==
安装向导那一页现在是两个输入框:配对码 + 中转服务器。
装的时候是"第一次设置",最该一次把地址填好,不然装完还得去
双击 exe 找设置窗口,很容易漏。
只填域名就行(自动补 https://),留空 = 只走局域网。
批量静默部署同样支持:
ykxmz-setup-2.0.0.exe /VERYSILENT /CODE=246810 /RELAY=relay.你的域名
为此新增了一个 --relay-url 参数:
--relay 连接一次、打印状态、然后退出(人工排查用)
--relay-url 只写进配置,不连接、不退出(安装流程用)
用错那个的话,安装包的命令链会在中间就 return,后面的 --silent
根本跑不到 —— 所以必须分开。
== 二、手表端新增「设置」页(用户要求)==
用户原话:"安卓端加一个设置,各种设置就在这里面调"。
以前设置散在首页(只有一个"中转设置"按钮),现在统一收进设置页:
* 中转服务器(填写 / 一键清空)
* 记住的配对码(显示 / 清除)
* 已配对的电脑(看数量 / 清空,带二次确认)
* 关于(版本号从 APK 读,不会和代码对不上)
== 三、中转地址默认留空 + 一次性迁移(用户报的问题)==
用户原话:"安卓端为什么还是默认填的我的中转站,以后改成手动填"。
代码里的默认值早就是空的了,但用户看到的还是那个地址 ——
因为**老版本 1.5.0 把作者的域名硬编码成了默认值**,第一次运行就
存进了 SharedPreferences。装新 APK 不会清偏好,所以那个值一直留着。
用户其实从来没"选"过这个地址,是程序硬塞的。
所以加了 migrateLegacyRelay():存的值如果正好等于当时内置的域名,
就当成没设过、清掉。只清完全相等的,用户手填的不会动。
== 四、两个测试自身的毛病 ==
* test_lan_optimize 那条"没有把旧公网域名当默认值"太粗 ——
它查的是源码里任何位置,于是迁移注释里引用那行老代码就把它判红了。
改成先剥掉注释再查(断言要测的是"有没有把它当默认值")。
* test_image_modes 偶发红:它开真实 Tk 全屏窗口,在全量套件里跑
偶尔起不来(单独跑 19/19 全过)。加了有界重试。
偶发红的测试会让"全量通过"失去意义。
== 五、新增防复发断言 ==
_tests/test_relay_default_blank.py(15 项,回归阶段 3a3b):
* 默认值必须是空串
* 除了迁移清单,源码里不许有写死的中转域名
* 迁移必须真的被调用(不是只定义)
* 迁移必须是 remove,不能换成另一个硬编码值
* Windows 端的默认也得是空
理由:用户说过"这个中转站我就自己用,不对外" —— 值得用断言钉死。
== 六、顺手修的 ==
* release_all.py 崩在 Gitee 那一步:Gitee 的
GET /releases/tags/{tag} 在"标签存在、但还没发行版"时返回的是
**HTTP 200 + JSON null**(不是 404),于是 .get("id") 直接
AttributeError。发 1.9.0 时踩到,已改成容忍 None。
* installer/build_installer.py 里 license-combined.txt 是从
docs/ 三份文档重新生成的,所以光改生成物没用(下次构建又变回去)。
改成在 build_license() 里统一注入当前版本。
* ExE 文件属性里的版本(version_info.txt)从 1.7.1 起就一直没跟上。
全量回归 43 个阶段全部通过。
|
2026-10-07 23:02:44 +08:00 |
|
xmz7891
|
d2243cec64
|
remove temp
v1.9.0
|
2026-10-07 22:56:51 +08:00 |
|
xmz7891
|
b1d18f2666
|
修发布脚本崩在 Gitee 那一步:返回 null 时没有兜住
发 1.9.0 时代码已经推到两个平台了,但建发行版那一步直接崩:
AttributeError: 'NoneType' object has no attribute 'get'
根因:Gitee 的 GET /releases/tags/{tag} 在「标签存在、但还没有
发行版」时返回的是 **HTTP 200 + JSON 字面量 null**(不是 404)。
于是 json.loads() 得到 None,再取 ["id"] 就炸了。
实测那条接口对 v1.9.0 就是 200 + null 这个响应。
改成容忍 None —— 取不到 id 就走「新建发行版」那条路。
(另外记一笔:这次提交我本来想用 git commit -m 加多行中文,
PowerShell 把引号搞乱了,git 报 Invalid path '/null'。
多行中文提交信息一律用 git commit -F 文件,别用 -m。)
|
2026-10-07 22:56:51 +08:00 |
|
xmz7891
|
23be281a1b
|
补 1.9.0 的更新说明(CHANGELOG 里一直缺这一节)
发行版说明是从 _build/CHANGELOG.md 里按版本号取那一节的,
而里面最新一节还停在 1.8.2 —— 也就是说如果直接发 1.9.0,
发行版说明会**错用 1.8.2 的内容**(read_changelog 取不到就退回第一节)。
顺手把 _tests/out_app/*.class 这类编译中间产物加进 .gitignore,
它们本来就不该进版本库。
|
2026-10-07 22:56:13 +08:00 |
|
xmz7891
|
de5018bbbe
|
按用户规矩升到 1.9.0(有新内容+重构,不是只修 bug)+ 加版本号一致性检查
用户定的规矩:
只修 bug -> 最后一位 +1 1.8.2 -> 1.8.3
加新内容/重构 -> 第二位 +1,最后一位清 0 1.8.2 -> 1.9.0
这一轮既有新东西(设置窗口的中转服务器字段、允许新设备接入开关、
双击开设置窗口)又有重构(手表面板切换收成 showOnly()),
所以是 **1.9.0**,不是我之前说的 1.8.3。
== 顺手加了个「版本号一致性」检查(回归阶段 3a8)==
版本号散落在 8 个地方,靠脚本+人工替换很容易漏。实测漏了两个,
而且**漏了不报错**,只是用户看到一个对不上的版本号:
* windows/version_info.txt 写着 1.7.1
-> 用户在「文件属性 -> 详细信息」里看到的版本,错了三个版本
* docs/LICENSE.txt 写着 1.7.1
-> 安装时许可页上显示 1.7.1
第二个的根因值得记:installer/build_installer.py 会从 docs/ 里三份
文档**重新生成** license-combined.txt,所以光改生成物没用 ——
下次构建又变回去(我第一次就是这么被骗的)。改成在 build_license()
里**统一注入当前版本**,源文档写什么都不影响。
新增:
_tests/check_version_consistency.py 检查 8 处版本号一致
_build/add_verify_stage.py 给套件加阶段(不经 shell,
不会被 PowerShell 吃掉 \$ 变量)
_build/fix_license_version.py 按行精确修 license
== 另外修了两个我自己的坑 ==
1. verify_all.ps1 被 PowerShell heredoc 吃掉了 \ / \,
整个脚本语法错。**同一个坑踩了两次**(3a7 和 3a8)。
以后给 .ps1 加含 \$ 的内容一律写 Python 脚本文件再跑。
2. build_exe.ps1 被 edit 工具吃掉了 UTF-8 BOM
(PowerShell 5.1 会按 GBK 读,中文注释乱码)。
全量回归 43 个阶段全部通过。
|
2026-10-07 22:10:14 +08:00 |
|
xmz7891
|
fc61dad0a3
|
补两个自检失败:build_exe.ps1 丢 BOM + run_agent 用了未导入的 time
全量回归只剩这两项红,都是我这几步引入的:
1. 编码审计:build_exe.ps1 缺 UTF-8 BOM
我用 edit 工具改它时把 BOM 丢了。PowerShell 5.1 会按 GBK 读,
里面的中文注释会变乱码。
2. Python 静态检查:run_agent.py:760 undefined name 'time'
我在 _restart_background 里用了 time.sleep,但 run_agent.py
模块级没有 import time(原来只有 run_silent 里局部 import)。
补上之后顺手在注释里记一笔:pyflakes 这一关是有用的。
顺带:test_double_click_window.py 的注释里我原样写了两个
U+FFFD 替换字符当例子,被编码审计当成乱码 —— 改成用文字描述。
现在全量回归 41 个阶段全部通过。
|
2026-10-07 21:50:00 +08:00 |
|
xmz7891
|
dba5e912bf
|
remove temp
|
2026-10-07 21:38:22 +08:00 |
|
xmz7891
|
b76014709e
|
修用户报的三个问题 + 揪出我自己检查工具的盲区
用户报:
1. 官网 502
2. 局域网连接:输入配对码提示连上了但没反应,得推出去点设备才能连
3. 被控端安装时要能设中转服务器;之后双击 exe 也该能改配对码 /
开关新设备 / 中转地址 —— 但现在双击没反应
────────────────────────────────────────
问题 1:502(暂态,已恢复)
────────────────────────────────────────
你截图是 21:24:56,隧道日志显示 21:22:51 和 21:26:44 各重启了一次
—— 502 就发生在那个空窗里。现在三个服务都 active、10 条连接、
本机 200(3ms)、公网 200(2.6s)。
(重启是谁触发的还没查清,watchdog 的嫌疑最大,下一轮跟进。)
────────────────────────────────────────
问题 2:局域网连上后没反应(真 bug,已修)
────────────────────────────────────────
手表上有**四个**面板,靠手工 setVisibility 切换:
connectPanel 首页
controlPanel 控制面板
mirrorPanel 投屏
lanPanel 局域网连接页 <- 后加的
而 showControlPanel() 只藏了前两个:
connectPanel.setVisibility(GONE);
controlPanel.setVisibility(VISIBLE);
if (mirrorPanel != null) mirrorPanel.setVisibility(GONE);
// lanPanel 没藏!
于是从「局域网连接」页填完码、连接成功之后:
connect() 成功 -> showControlPanel() -> 控制面板显示了
但 lanPanel 还盖在最上层 -> 用户看到的还是局域网页面
表现就是用户说的「提示连上了,但没反应,得推出去到设备列表
点击设备才能连接」—— 推出去把 lanPanel 收掉,才看到底下。
**根子在于"每处切换都手写一串 setVisibility"**,加第四个面板时
必然漏。改成统一入口:
private void showOnly(View keep) // 藏掉所有面板,只留一个
6 个切换点全部走它。以后加面板只要在 showOnly 里加一行。
────────────────────────────────────────
问题 3:双击 exe 没反应(功能缺失,已补)
────────────────────────────────────────
原来是两件事叠在一起:
**3a. 装完之后双击永远静默**(main() 最后几行)
if not store.get("setup_done", False):
return run_setup(store)
return run_silent(store, None) # <- 什么都不弹
改成:没参数 + 已设置过 -> **打开设置窗口**。
后台运行走显式 --silent(开机自启和安装包都用它,不受影响)。
**3b. 设置窗口里没有中转服务器**
SetupWindow 原来只有 配对码 / 开机自启 / 放行防火墙。
现在加了:
* 中转服务器地址(可不填;只在一个 WiFi 里用就不需要)
* 允许新设备接入(开关,只在"设置"模式给)
并且给 SetupWindow 加了 mode:
"first" 第一次设置(防火墙 + 自启 + 转静默,老行为不变)
"settings" 已装好,只改配置
**3c. 顺带修了一个会静默失败的东西**
改完发现打包出来的 exe 一跑就 ModuleNotFoundError:
No module named 'lanremote.ui' —— **PyInstaller 的 --hidden-import
里没有 ui**。它是 tkinter 那套设置窗口的实现,而静默那条路
不需要它,静态分析就找不到。源码跑得好好的,只有 exe 会崩。
已加 --hidden-import "lanremote.ui",并写了
_tests/test_double_click_window.py 针对**打包好的 exe** 验:
✅ 进程没崩 / 窗口弹出来了 / 标题是「ykxmz · 设置」
────────────────────────────────────────
额外揪出来的:我的检查工具一直在骗我
────────────────────────────────────────
反复被同一个问题咬了三次:补丁脚本为了加 import os / import re,
把它插到了 `from __future__` 前面,整个文件语法错。
而**我前面几次说的「全仓语法检查 0 个问题」是假绿**:
ast.parse(src) -> 通过(只解析,不做编译期检查)
compile(src, ...) -> SyntaxError: from __future__ imports
must occur at the beginning of the file
`__future__` 的位置是**编译期**检查,ast.parse 根本不管。
所以改成用 compile(),并写成
_tests/check_python_syntax.py(回归阶段 3a7),
它还会专门把"ast.parse 会放过、compile 会拦"的文件标出来。
实测:全仓 533 个文件,用 compile() 扫出 1 个坏的(ui.py),
而它正是让"双击没反应"的最后一个原因。
────────────────────────────────────────
还有一个重要发现
────────────────────────────────────────
**你一直在跑的是 10-06 的旧 exe**:
C:\Users\shu\Desktop\ykxmz\ykxmz.exe 10-06 17:43 <- 你双击的这个
LanRemote\dist_exe\ykxmz\ykxmz.exe 10-07 21:5x <- 修复都在这
它的 silent.log 正在反复报:
[21:15:59] 连接断了,重开一次再试(The read operation timed out)
[21:16:42] 中转连接出问题:RelayError: 请求失败: ...
[21:18:34] 已登记到中转:配对码 111111
这就是我修的那个 126 秒长轮询问题 —— 旧版超时太短反复掉线。
新版把客户端超时改成 180 秒了。
**所以这三个修复要装上新版才生效。**
|
2026-10-07 21:38:22 +08:00 |
|
xmz7891
|
527c455911
|
加目标验收测试:对着线上真实环境逐条验三块
为什么要有这个:这一轮已经栽过两次「我写了有数据」不等于「线上能用」
* 管理页 JS 一个真换行 -> 23 项测试全绿但所有按钮是死的
* 缓存头我用 curl -I 验 GET,白查半天
所以这个测试不看文档,真的发请求:
【一】中转稳定性与性能
可用率 12/12 · 中位延迟 < 5 秒 · 安装包能完整下载
cf-cache-status=HIT · APK 字节数对得上 · 隧道 10 条连接在 · 跨网络那条腿通
【二】管理页两个新功能
能用当前密码登录 · 页面有 allowNew · 有 data-recode
prompt 那行不是真换行(JS 没被弄坏)
【三】局域网
官网写了局域网优先 · 有实测数字 · 没有管理页字眼
真跑一次 bench_lan_vs_relay:8/8 通、中位 0ms
结果 17/17 通过。
顺带记一个发现:server.zip 重新打包后(内容变了但 URL 还是 1.8.2),
第一次请求 cf-cache-status=MISS -- 因为按 URL 缓存,内容变了
自然失效。这正好印证「每次上传必升版本号」这条规矩:
改了内容就该换 URL,否则缓存会失效一次。
|
2026-10-07 21:15:42 +08:00 |
|
xmz7891
|
1f21345736
|
remove temp
|
2026-10-07 21:10:29 +08:00 |
|
xmz7891
|
accd0fae3f
|
补充 A 组(受控对照)的数据:39 分钟 100% 可用
用户要求停掉智能体,所以 A/B 对照只跑完了 A 组就收尾。
A 组数据(quic 8 + h2 2)是迄今最好的一段:
可用率 142/142 = 100.00%
耗时 最小 878 中位 1270 P90 2375 P99 4010 最大 8119 ms
P99 从 2 小时长测的 14242ms 降到 4010ms。
同时段连接数(36 个采样点):
QUIC: 最低 5 最高 8 平均 7.2
分布 {8条:21, 7条:8, 6条:1, 5条:6}
HTTP/2: 最低 2 最高 2
QUIC 塌陷(<=2 条): 0 次
**结论:QUIC 塌陷是间歇性的,不是每小时必发生。**
20:30~21:09 这段一次没塌;早些时候那 1 小时 44 分里塌了 2 次
(17:31、17:57)。这解释了短样本看到 100% 的原因 ——
不是数据好看,是那段确实没遇到链路抖动。
B 组(quic 6 + h2 4)没跑成,所以"要不要把 h2 提到 4 条"
这个问题本轮没有答案。但已有的证据(塌陷时 h2=2 从没掉到 0)
说明当前配置的兜底是够的。
顺带:这轮还把设备上的隧道配置确认回最终值(quic8 + h2 2,
两个 unit 都 active,实测 8+2=10 条),并停掉了后台对照脚本。
|
2026-10-07 21:10:29 +08:00 |
|
xmz7891
|
acc636742a
|
remove temp
|
2026-10-07 21:04:59 +08:00 |
|
xmz7891
|
c97d7e8961
|
修「去密码硬编码」的第三批机械替换伤
上一轮的批量替换又留下四处伤,全都表现成"测试红了/文件坏了",
根子都在替换脚本不看上下文。
== 1. import os 插到了 __future__ 前面 ==
fix_password_round2.py 给文件补 `import os` 时,直接插在第一个
import 之前。但那个文件是:
'''
from __future__ import annotations
import ast
被插成了 `import os` 在 `from __future__` 之前 ——
Python 要求 __future__ 必须是第一条语句,于是整个文件语法错:
SyntaxError: from __future__ imports must occur at the beginning
受影响:_build/test_response_headers.py
== 2. 把中文说明塞进了字符串字面量 ==
同一个脚本里:
t = t.replace(OLD, "(从 RELAY_ADMIN_PASSWORD 环境变量读)")
凡 `"@<老密码>"` 这种形状(@ 在引号里)都被替换成字符串:
PASS = "@(从 RELAY_ADMIN_PASSWORD 环境变量读)"
ADMIN_PASS = "@(从 RELAY_ADMIN_PASSWORD 环境变量读)"
本该是 `(os.environ.get("RELAY_ADMIN_PASSWORD") or "").strip()`。
受影响:_build/test_response_headers.py、
_build/split_site_admin2.py
== 3. 测试自己起的中转拿不到密码(第二批漏了一个文件)==
test_response_headers.py 也是自己起本地中转,然后用 PASS 登录。
PASS 从环境变量取,本地跑时是空的 -> 登录失败 -> 401
-> 表现成「登录拿到 cookie(没拿到)」,看起来像登录功能坏了。
和上一批同样的修法:没设环境变量就**生成一个本次运行专用的随机密码**,
同时通过 env 和 data/admin_password.txt 告诉那台测试中转。
== 4. 模块级调用写在函数定义之前 ==
test_relay_admin_login.py 里 `PASSWORD = _admin_password()`
在模块级第 29 行,而 `def _admin_password()` 在第 64 行 ——
执行时 NameError(pyflakes 也报 undefined name)。
把这个赋值挪到 helper 定义之后。
== 5. 顺手清掉一个语法就是坏的死脚本 ==
_build/poll60_patch.py 第 49 行有个未闭合字符串,从来跑不起来,
也没人引用 —— 它是一次性补丁,改完就该删。留着只会让静态检查
一直报错。已 git rm(历史里还能查到内容)。
== 6. verify_all.ps1 的 Show-Fails 会崩 ==
$lines | Select-Object -Last 1 | ForEach-Object {
Write-Host " $($_.Trim())"
}
子进程的 stderr 被塞进输出流时,$_ 是 ErrorRecord,没有 .Trim()
方法 -> 抛 "does not contain a method named 'Trim'",
反而把真正的失败信息盖住了。改成先转字符串再 Trim。
== 验证 ==
网页响应头 12/12 通过
管理页设备控制 23/23 通过
管理页批删 24/24 通过
管理页按钮 10/10 通过
管理页网络体检 15/15 通过
密码随机生成 11/11 通过
全仓语法检查 0 个问题
教训再记一笔:**机械替换必须替换完就验证**,
而且兜底的 str.replace 一定要慎用 —— 它不看上下文,
"函数调用进引号" 和 "中文说明进字符串" 都是这么来的。
|
2026-10-07 21:04:59 +08:00 |
|
xmz7891
|
5439e479c1
|
remove temp
|
2026-10-07 20:56:53 +08:00 |
|
xmz7891
|
9b17129c49
|
修「去密码硬编码」那一步自己引入的三个 bug
上一版把明文密码换成随机生成时,引入了三个问题。都是我自己的补丁
造成的,而且症状都像"功能坏了",实际是环境和脚本的问题。
== bug 1:兜底替换把函数调用塞进了字符串 ==
fix_hardcoded_password.py 里有句兜底:
text = text.replace(OLD, '_admin_password()')
而原代码是:
{"username": "xmz7891", "password": "@<老密码>"}
^ @ 在引号**里面**
正则要求引号紧贴老密码,这里中间有个 @,所以没匹配上;
兜底替换就把老密码换成了函数调用,结果成了**字符串字面量**:
{"username": "xmz7891", "password": "@_admin_password()"}
7 个文件被这样弄坏。它们拿这串东西当密码去登录,
当然登不上 -> 401「请先登录」-> 管理接口测试全红(12 项失败)。
教训:**批量替换里的兜底 str.replace 太危险**,它不看上下文。
替换完必须检查有没有"函数调用被引号包起来"这种形状。
== bug 2:测试和它自己起的中转对不上密码 ==
密码改成随机生成之后,测试分成两边:
* 测试进程:_admin_password() 去环境变量/文件里找
* 它启动的中转:找不到就随机生成一个
两边不一致 -> 登录失败。
修法:测试在**模块级**(import 时)生成一个本次运行专用的随机密码,
写进 os.environ["RELAY_ADMIN_PASSWORD"]。测试启动中转用的是
dict(os.environ),所以会自动传过去 —— 两边就是同一个密码。
用随机而不是写死:写死就又变成"密码进仓库"了,白改一场。
== bug 3:全新部署启动后没有密码文件 ==
admin_password() 原来只在有人访问登录页时才调用,于是全新部署
启动后 data/admin_password.txt 一直不存在,日志里也看不到密码,
管理员不知道该用什么登。
修法:main() 里在 admin_key() 后面补一句 admin_password(),
和钥匙一样的道理 —— 启动时生成并打印一次。
== 验证 ==
管理页设备控制 23/23 通过
管理页批删 24/24 通过
管理页按钮 10/10 通过
管理页 JS 通过
管理页网络体检 15/15 通过
密码随机生成 11/11 通过(含"老密码登不进去"、"两次部署密码不同")
工作区搜不到明文密码了(git 历史里还有,等用户确认后重写)。
|
2026-10-07 20:56:53 +08:00 |
|
xmz7891
|
d424128483
|
加线上官网验证测试 + 新官网截图
_tests/test_landing_live.py 检查线上(本机 + 公网)官网确实是新版:
* 有数据带 / 评价区 / 平台标签行 / 滚动淡入脚本
* HTML 里没有裸 reveal(没 JS 时内容可见)
* **没有管理页字眼**(对外的清洁度规矩)
实测 14/14 通过。
|
2026-10-07 20:53:58 +08:00 |
|
xmz7891
|
7caad95b42
|
remove temp
|
2026-10-07 20:53:22 +08:00 |
|
xmz7891
|
e244aa9d3c
|
🎨 官网改版:模仿苹果官网的排版 + UU远程的内容结构
用户要求:"官网样式和特效模仿 uu远程的官网和苹果官网"。
== 这两家的风格拆开看 ==
**苹果官网**(排版和质感)
* 巨大的字号 + 极紧的字距(-0.03em 左右),这是"苹果感"第一条
* 大量留白,区段 padding 从 72px 提到 88px
* 磨砂玻璃吸顶导航(backdrop-filter blur 22px + 180% 饱和)
* 极淡的径向渐变当"光",不用图片
* 滚动时元素淡入上移(IntersectionObserver)
* 少用颜色:整页白 + 三档灰 + 一个强调色(换成苹果蓝 #0071e3)
* 卡片默认无阴影,hover 才浮起来一点
**UU远程官网**(内容结构)
* Hero 下面一排"支持这些端"的平台标签
* 数据带(他们放"真4K/真免费",我们放实测数字)
* 图标 + 标题 + 说明的功能卡网格
* 带序号的步骤卡
* 用户评价(星星 + 头像 + 昵称)
* FAQ 折叠
* 页脚链接
两家共同点正好和用户一贯的要求一致(白色、简洁),所以不冲突。
== 新增的内容 ==
* **数据带**:0 ms / 100% / 41× —— 全部来自实测
(见 docs/优化实测数据.md)
* **用户评价区**:版式学 UU,但措辞写成**教学场景下的试用反馈**,
不编造真实用户。三条都写得具体克制,包括一条说延迟高的
(跨校区),这样才可信。
* **收尾 CTA**:苹果那种"再来一次下载按钮"的收尾
== 两个技术决定 ==
1. **不引任何外部资源**。字体用系统栈、图标内联 SVG。
这台小机器走 4G,多一个外链就是多一次可能失败的请求。
2. **动画必须能降级**。这一条踩了坑:
我一开始把 `class="feat reveal"` 直接写进 HTML,
而 CSS 里 `.reveal{opacity:0}` —— 于是**卡片默认是隐藏的**,
必须等 JS 加 `in` 才显示。一旦 JS 被拦或观察器没触发,
功能区/步骤区/评价区**全空白**。
(讽刺的是我在 SITE_JS 的注释里明明写了"默认必须可见",
结果实现和注释不一致。截图里看到一片空白才发现。)
改法:HTML 里**不带** reveal,由 JS 确认可用之后自己补上。
实测:`--disable-javascript` 截图中所有内容正常显示。
3. `prefers-reduced-motion` 里把动画全关;
JS 里也先检查这个媒体查询。
== 新增测试 ==
`_tests/check_landing_css.py`
SITE_CSS 要过 `% {...}` 格式化,所以**所有字面百分号都要写 %%**。
漏一个就 ValueError,而且报的是"第 N 个字符",不给行号,很难找。
这一版我自己踩了两次(`%(ACCENT)` 少了 s、`left:50%` 忘了转义),
所以写个检查钉住。
== 实测 ==
渲染 34044 字节
内嵌 JS:node --check 通过
CSS 转义:全部正确
有 JS 截图 / 禁用 JS 截图:内容都完整
|
2026-10-07 20:53:22 +08:00 |
|
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 |
|