架构与设计

三层,二十四个模块,一条单向的数据流

这一页写给想改这个工具的人。它讲三件事:模块怎么分层、 一轮采样里数据怎么流、以及七条决定了它长成什么样的取舍。 要动手改代码,配合 CONTRIBUTING 一起读 —— 那份讲的是规矩,这份讲的是形状。

一、分层:采集 / 计算 / 呈现

分层的判据只有一条:这个模块碰不碰外界。碰外界的(发请求、跑命令、 读文件)归采集层;不碰的归计算层,全是纯函数,可以喂固定样本做离线测试; 呈现层只负责整形,不做判断。

这条线画得很硬,好处是测试套件可以完全离线 —— 一个需要联网才能跑的测试套件,在没网的时候就等于没有。 而这个工具恰恰是给网络出问题的人用的。

采集层 碰外界:发请求 / 跑命令 / 读文件 net.py 分段计时的 HTTPS paths.py 三个入口的路径 resolve.py DNS 对账 / fake-ip sockets.py lsof 实时连接 asn.py Cymru / ip-api cloudranges.py 厂商官方 IP 段 rdap.py RIR 注册信息 pathtrace.py mtr 逐跳(可选) telemetry.py 静态提取字段 clash.py 分流器控制接口 拿不到就返回 None —— 一个可选增强 不该让主流程失败。 计算层 不碰外界:纯函数,可离线测试 model.py 全部数据结构,frozen dataclass,不可变 probe.py 拼成一轮采样 · claude_traces() 在这里过滤对照组 diagnose.py 出口画像 / 稳定性 / 现象→判据→成因→方案 checks.py 环境级检查 endpoints.py 域名清单 + 取证 store.py 内存环形缓冲 history.py 按天归档 / 可删 176 个测试全部落在这一层, 一个请求都不发。 呈现层 只整形,不判断 view.py 整形成界面要的形状 · 只出事实,不出文案 sampler.py 后台循环 + 开关 server.py HTTP + SSE cli.py 命令行入口 demo.py 虚构一轮采样 web/index.html · web/app.js 九个面板 · 双语文案只写在这里 scripts/build_docs.py docs/*.md → 这个站 + demo 页 界面不发明数字:没有的值渲染成 破折号,绝不用 0 顶上。
数据只往一个方向流。呈现层拿不到采集层的对象,只拿计算层整形过的字典 —— 所以换一个前端不需要碰任何采集代码,加一个数据源也不需要碰界面。

二、一轮采样里发生了什么

按下「立刻采一轮」或者定时器到点时,走的是下面这条链。整条链在一个后台线程里跑, 界面通过 SSE 收结果 —— 采样卡住不会让界面卡住。

1 确定路径 读环境变量 + 系统代理, 得出三条等价探测路径。 2 并行探测 每条路径 × 每个域名问一次 cdn-cgi/trace,四段计时。 3 解析对账 本机解析 vs 两个独立 DoH, 对不上就是 DNS 被改写了。 4 列实时连接 lsof 抓 Claude 的 socket, fake-ip 反查回域名。 5 归属与置信度 先查厂商官方 IP 段(确证), 没命中再退 ASN / rDNS。 6 诊断整形 跑诊断规则,整形成 界面要的字典。 采集 · 每轮都跑 带缓存 纯计算 落盘:一天一个 JSONL 文件 每轮一条精简记录(约 1 KB),只留结论要用的字段。 在 .gitignore 里 —— 采样产物就是你的网络长什么样。 推送:SSE 一条长连接 采样在后台线程,界面不阻塞。 断了自己重连,不丢面板状态。 第 2 步是唯一发出网请求的一步。第 5 步会打第三方数据源,但结果缓存 24 小时。 路径质量(mtr)不在这条链上 —— 它要跑好几秒,做成了按需触发的独立接口。
整条链最长的一步是第 2 步,因为要等真实的网络往返。三条路径之间是并行的, 所以一轮的耗时接近「最慢的那一条路径」,而不是三条相加。

三、七条设计取舍

每一条都是「选了 A 而不是 B」,以及不这么做会怎样。 改代码时如果要推翻其中一条,先看看这里写的代价还在不在。

1

监控默认关闭,而不是打开页面就开始采

一个专门监控网络流量的工具,在你没同意之前就开始发请求,它的可信度当场就没了。 所以空状态不是占位符,它是页面在如实说「我到目前为止什么都没做」。 代价是多一次点击,换来的是这个工具可以被放心地装在一台不属于你的机器上。

2

三张卡,而不是一个「你的出口 IP」大标题

任何装了代理的机器上,三个入口读的是不同的配置文件,可以从不同国家出去。 一个大标题必须挑一个显示,那它对另外两个就是错的 —— 而这个工具存在的全部理由,就是那个「不一样」。

3

对遥测接入点只做 TLS 握手,绝不发 HTTP 请求

往别人的遥测接入点写数据,等于污染他们的数据,也把这个监控工具变成了一个上报源。 握手已经能回答「通不通、多快」,那就到此为止。 界面上这一句也写出来了 —— 使用者有权知道这个工具对第三方做了什么。

4

四段延迟分开量,而不是给一个总数

「慢」有四种完全不同的原因,修法也完全不同:DNS 慢换解析器,TCP 慢换出口节点, TLS 慢多半是链路丢包重传,首字节慢是服务端或中间代理在等。 一个总数无法告诉你是哪一种,于是人只能瞎换节点。

5

静态提取遥测字段,不解密 TLS

静态提取只能知道「会发哪些字段」,不知道「某一次具体发了什么值」。 后者要装一个根 CA —— 而根 CA 私钥一旦泄露,本机所有 HTTPS 都可被解密。 这个代价大于收益,所以本仓不做,也不提供做这件事的工具。 能给的答案边界写清楚,比给一个越界的答案更有用。

6

历史存精简记录,不存整份采样

一份完整采样约 15 KB,精简记录约 1 KB。挂一个月的差别是 400 MB 和 30 MB。 精简记录只留结论要用的字段,代价是历史看板答不了「三周前那一轮的第七个域名 TTFB 是多少」—— 但那个问题没人问。想留全量的人可以加 --persist,那份文件在 gitignore 里。

7

demo 页复用真实界面,不另写一套

这个站上的 demo 用的就是 web/index.html 和 web/app.js 本身, 只是把三个接口换成了内置的一轮虚构采样。所以它不会随时间和真实界面走偏 —— 一个和产品长得不一样的 demo 比没有 demo 更糟。 代价是生成脚本要做一次 fetch 替换,那段代码在 scripts/build_docs.py 里,有注释。

四、想加东西的话,入口在哪

你想加改哪个文件要注意
一个 Claude 域名 cem/endpoints.py 必须带 evidence(怎么知道的),不是 Claude 的要标 baseline=True,否则会污染结论
一个数据源 采集层新建一个模块 拿不到返回 None 不抛异常;并在 数据源里加一行,写明能撑到哪一档置信度
一条诊断规则 cem/diagnose.py 判据 / 成因 / 下一步三样缺一不可;测试要同时断言「该触发时触发」和「不该触发时不触发」
一个界面面板 web/index.html + web/app.js 节点用 el() 造(外部输入拼 innerHTML 就是 XSS);文案双语;atl- 前缀的样式别改
Linux 支持 cem/paths.py · cem/sockets.py 核心探测本来就能跑,卡住的是「读系统代理」和「列进程连接」两块 macOS 命令
一章文档 docs/*.md 写完跑 python3 scripts/build_docs.py,把 .md 和生成的 .html 一起提交

为什么没有 CI

文档站的 HTML 是生成后提交进仓库的,不靠 GitHub Actions 构建。 这样 fork 的人开个 Pages 就能用,不需要配任何东西,也不需要给 token 加权限。 代价是改 .md 之后必须记得跑一次生成器 —— python3 scripts/build_docs.py --check 可以检查有没有忘。