# -*- coding: utf-8 -*- r'''最终验证 + 提交这一轮(隧道修复 + 管理页重做)。''' from __future__ import annotations import os import subprocess import sys from pathlib import Path ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote") GIT = r"C:\Program Files\Git\bin\git.exe" sys.path.insert(0, str(ROOT / "_build")) from devssh import run # noqa: E402 MSG = """隧道"假活"真因修复 + 中转管理后台重做 用户反馈两件事: 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。现在统一定义了常量。 二者都是"改完没立刻验"造成的。现在每次改完都跑一遍 管理页 + 官网 + 关键接口的回归。 """ def verify() -> None: print(" 一、验证配对码清理") out = run( "curl -s -b /tmp/ck.txt --max-time 20 " "http://127.0.0.1:8080/zhongzhuanzhan | " "grep -A2 '配对码占用' | head -3", timeout=120)[1] print(" " + (out or "").strip()) code = run( "curl -s --max-time 10 http://127.0.0.1:8080/v1/health | " "python3 -c \"import sys,json; d=json.load(sys.stdin); " "print('设备', len(d.get('devices',[])))\"", timeout=60)[1] print(" " + (code or "").strip()) def commit() -> None: print() print(" 二、提交") subprocess.run([GIT, "add", "-A"], cwd=str(ROOT), check=False) msg_file = ROOT / "_build" / "_cmsg7.txt" msg_file.write_bytes(MSG.encode("utf-8")) r = subprocess.run([GIT, "commit", "-q", "-F", str(msg_file)], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8", errors="replace") msg_file.unlink(missing_ok=True) print(" commit exit=%s" % r.returncode) show = subprocess.run([GIT, "log", "--oneline", "-1"], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8", errors="replace") print(" " + (show.stdout or "").strip()) env = dict(os.environ) env["GIT_SSH_COMMAND"] = r'"C:\Program Files\Git\usr\bin\ssh.exe"' push = subprocess.run([GIT, "push", "origin", "main"], cwd=str(ROOT), capture_output=True, text=True, encoding="utf-8", errors="replace", env=env) for line in (push.stderr or push.stdout or "").strip().splitlines()[-2:]: print(" " + line) def main() -> int: verify() commit() return 0 if __name__ == "__main__": sys.exit(main())