文档定位:本报告对
vmess-domain-rotator全仓库业务流程、业务代码与总体架构进行了独立深度审查,并将本地既有的两份审查报告——ARCHITECTURE_REVIEW.md(下称「初审报告」)与ARCHITECTURE_REVIEW_RECHECK.md(下称「复核报告」)——与本次审查结果进行三方交叉比对、分歧裁定与查漏补缺,整合为唯一权威的架构演进与修复蓝图。审查范围:
- 核心代码:
scripts/domain_updater.py、scripts/run_update_and_commit.sh、scripts/router_local_update.sh、scripts/router_local_http.sh、scripts/install_debian.sh、scripts/uninstall_debian.sh、substore/operator_template.js、build_router_package.sh- 配置文件:
config.server.json、config.local.json、config_router.conf- 生产取证:远端
vmess-domain-rotator/runtime-state分支提交历史(截至2026-10-11T00:34:14Z的ff09327)及本地运行产物runtime/current_ip.json
本仓库核心目标是自动优选 Cloudflare 优选域名或 IP,并通过本地 HTTP 或 Git 分支发布给 Sub-Store 订阅脚本消费。目前仓库内并行维护了三条生产管线与一个消费端:
flowchart LR
subgraph 生产端管线
A1["管线 1: Server API 模式<br/>(config.server.json)"] --> B1["domain_updater.py<br/>拉取 vps789 API & 加权评分"]
B1 --> C1["runtime/current_domain.txt<br/>runtime/current_domain.json<br/>runtime/state.json"]
C1 --> D1["run_update_and_commit.sh<br/>(孤儿分支 worktree 提交推送)"]
D1 --> E1["GitHub runtime-state 分支<br/>(raw.githubusercontent.com)"]
A2["管线 2: Local Python 模式<br/>(config.local.json)"] --> B2["domain_updater.py<br/>调用 cfst 测速 & 解析 CSV"]
B2 --> C2["runtime/current_ip.txt<br/>runtime/current_ip.json<br/>runtime/state_local.json"]
A3["管线 3: 路由器 BusyBox 模式<br/>(config_router.conf)"] --> B3["router_local_update.sh<br/>调用 cfst 测速 & sed/awk 解析"]
B3 --> C3["runtime_router/current_ip.*<br/>router_local_http.sh (httpd)"]
end
subgraph 消费端管线
E1 --> F["substore/operator_template.js"]
C2 -.-> F
C3 --> F
F --> G["覆写代理节点 server 字段<br/>(支持 local_first / server_only / local_only)"]
end
domain_updater.py 同时承担了 API JSON 解析与 cfst CSV 解析两套截然不同的逻辑,但中间层(候选校验、状态落盘、回退语义)强行耦合在一处。router_local_update.sh 用 POSIX shell 重写了一遍 cfst 调用与 JSON 拼接,不仅丢失了 config.local.json 的 -dt/-dn/-dd 参数,且输出的 state_router.json 字段结构与 Python 版 state.json 不一致。domain_updater.py 既把 stdout 当作传给 run_update_and_commit.sh 的机器可读 IPC 通道(期望最后一行是 JSON),又允许 cfst 子进程和 notify 子进程直接向同一个 stdout 刷日志。ok 与 fallback_last_good / error_use_last_good 之间来回翻转,导致无效 Git commit 激增;同时消费端 operator_template.js 完全忽略 JSON 中的 status 与 updated_at_utc 字段,无法感知上游是否已处于长期故障或陈旧状态。本节保留 8 项审查记录。其中 §2.1 已于 2026-10-18 更正为上游字段说明与自定义评分策略记录;其余条目保留既有审查结论。
yd_score 字段与本项目自定义评分策略configs/config.server.json:字段映射、加权字段与平局决胜配置scripts/rotator/scoring.py:按配置计算 score_value 并排序yd_score 是上游 API 字段 ydScore 的映射;dx_score、lt_score、avg_score 同理。其含义、单位与计算公式由上游定义,本项目不重新计算这些字段,也不将其固定解释为健康分或成本分。不能仅凭字段名、少量样本或单个延迟指标的变化推断上游评分的方向。score_value 由 scoring.weighted_fields、权重及字段级 prefer_lower / order 计算;平局决胜由 scoring.tie_breakers 决定。是否使用某个上游评分字段以及如何排序,均属于配置选择。yd_latency(权重 1.0)和 yd_pkg_lost_rate(权重 10.0)加权,均配置为较小值优先。yd_score 不参与加权,仅在平局决胜中配置为 order: "asc"(数值较小者优先)。这描述的是当前策略,不是对所有数据源的固定规则。yd_score 降序的结论。将该字段移出加权、改为升序平局决胜属于评分策略调整;更换数据源或调整策略时,应核对上游定义并同步更新配置、测试和文档。test_p0_yd_score_polarity_fix_and_healthy_ranking 与 test_empirical_score_metric_asc_tie_breaking_and_consistency 是现有测试名称,验证当前配置下的排序行为,不能据此证明上游实际计算公式。createdTime 是「监控建档时间」而非「测速时间」,盲目启用 within_hours 会团灭所有候选config.server.json:41-47、config.server.json:63scripts/domain_updater.py:669-678README.md:227README.md:227 将 within_hours 描述为「仅保留最近 N 小时的数据」,初审与复核报告均据此建议开启 within_hours: 24 以过滤陈旧测速记录。runtime-state 分支跨越数月的真实 state.json(对比 2026-07-27 的 58a2820 与 2026-10-11 的 ff09327)可以发现:
baka.fun 的 created_raw 在 7 月和 10 月永远固定为 "2026-05-28 20:14:47";eii.at 永远固定为 "2026-05-27 01:37:50";visa.cn 永远固定为 "2026-04-28 19:26:12";yd_latency、yd_pkg_lost_rate、avg_pkg_lost_rate 每半小时都在动态变化。vps789 接口返回的 createdTime 是该优选域名被纳入监控池的建档时间,根本不是当次拨测时间戳!如果采纳旧报告建议设置 within_hours: 24,除新入库不满 24 小时的域名外,所有主力稳定域名都会被全部过滤掉,直接导致选优池归零。README.md 与 config.server.json 中修正描述,澄清其为建档时间,并在生产配置中安全保持 within_hours: 0(禁用状态过滤),防止误杀老牌稳定域名。fallback_last_good 线上已真实触发并造成无效重复提交scripts/domain_updater.py:1071-1102 vs 1125-1149scripts/run_update_and_commit.sh:164-167、334-340domain_updater.py 内部实际存在两条完全不同的 last_good 回退路径:
status = "error_use_last_good",将 state.json 中的 status 写为 "error",且向 stdout 打印的 JSON 为 {"status": "error_use_last_good", "error": "..."}(漏掉了选中的域名字段 domain)。data 为空数组 [] 或所有记录均被过滤掉时,不会抛异常,而是进入 if best is None: 分支,将 status 设为 "fallback_last_good",并将 state.json 覆写为 "status": "fallback_last_good"、"source_count": 0、"top_candidates": []。run_update_and_commit.sh 的状态处理逻辑(L164 与 L334)只认识 error_use_last_good,完全不知道 fallback_last_good 的存在。runtime-state 分支的提交 a7c6fdc(2026-10-11T00:03:45Z)中,上游 API 恰好返回了 source_count: 0,触发了 fallback_last_good。由于脚本未识别该状态,产生了大量无效翻转空提交。domain_updater.py 异常回退输出的 JSON 中强制包含 selected_json_key(如 domain);run_update_and_commit.sh 同时识别 error_use_last_good 与 fallback_last_good,并在回退所选值未变时执行「回退静默」,跳过 commit/push;domain_updater.py 在执行回退前对 last_good 进行 domain_filter 黑名单复检,拉黑节点绝不复活。test_p1_error_fallback_stdout_contains_domain_key、test_p1_last_good_bypasses_blacklist_prevented、test_p1_run_update_and_commit_fallback_silence 100% 验证通过。-c 明文传递,同机 ps 可直接截获scripts/run_update_and_commit.sh:91-101现象与证据:
auth_b64="$(printf '%s:%s' "$GIT_HTTP_USERNAME" "$GIT_HTTP_TOKEN" | base64 | tr -d '\n')"
git -C "$repo_dir" -c "http.extraHeader=Authorization: Basic ${auth_b64}" "$@"
在 Linux 系统中,通过命令行参数传递的 -c http.extraHeader=... 会完整暴露在 /proc/<pid>/cmdline(即 ps aux / top)中。同机任何非特权进程只要轮询 ps 即可直接截获 Base64 编码的 Token。
修复方案与落地状态:
scripts/run_update_and_commit.sh 中彻底移除命令行 -c "http.extraHeader=...",改用 Git 原生环境变量 GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="http.extraHeader" GIT_CONFIG_VALUE_0="...",argv 零暴露。tests/test_shell_lifecycle.py 中用例 test_p1_run_update_and_commit_passes_token_in_env_not_argv 100% 验证通过。validate_config() 漏检 api、parser、domain_filter、selection 顶层存在性scripts/domain_updater.py:375-525validate_config() 在 source.type == "api" 时漏检 api 顶层字典及 api.url 是否非空,导致配置缺漏时在 --print-output-settings 通过但在后续执行抛出未处理的 KeyError。scripts/domain_updater.py 的 validate_config() 中补充了对 api(字典且 url 非空)、parser、selection、domain_filter 顶层字典的强制校验。test_p2_validate_config_enforces_api_structure 100% 验证通过。last_good 可回退)时反而跳过 run_notify()scripts/domain_updater.py:1125-1149main() 的 except Exception as exc: 块中,当 last_good 为空且发生致命异常时,直接 return 1,漏掉了 run_notify() 报警通知。last_good 的分支中补充调用 run_notify(notify_cfg.get("command", ""), "", "error"),确保致命故障必定触达通知通道。cfst 模式】【✅ 已修复】未过滤 download_speed == 0.00 的测速失败节点,已将零速 IP 选为最优config.local.json:5-19scripts/domain_updater.py:926-972(parse_cfst_csv_rows / select_best_cfst_record)runtime/current_ip.json:1-24cfst 测速中若节点因被墙或下载失败测得速度为 0.00 MB/s,原逻辑仅按首行选取,错误地将零速 IP 选为最优并标记为 ok。scripts/domain_updater.py 与 scripts/router_local_update.sh 中增加测速过滤,未禁用下载测速时,自动过滤 download_speed == 0.00 的候选行。test_p2_cfst_filters_zero_download_speed 与 test_p2_router_local_update_filters_zero_speed 100% 验证通过。build_router_package.sh 在 macOS 下混入 .DS_Store / AppleDouble 扩展属性且跨架构检测失真build_router_package.sh:67-88、134._* 扩展属性文件与 .DS_Store,且在宿主机执行 Linux 架构 BusyBox 二进制时误报无 httpd。build_router_package.sh 中设置 COPYFILE_DISABLE=1 并添加 --exclude='.DS_Store' --exclude='._*';对跨平台 BusyBox 改用 strings 静态检查 httpd 符号。本地现有的两份报告——ARCHITECTURE_REVIEW.md(初审)与 ARCHITECTURE_REVIEW_RECHECK.md(复核)——在 7 个关键技术判断上存在明显分歧。本次审查结合全量源码与远端 runtime-state 分支真实生产记录,给出最终裁定:
| 编号 | 分歧点 | ARCHITECTURE_REVIEW.md(初审) |
ARCHITECTURE_REVIEW_RECHECK.md(复核) |
本次终审裁定与实证依据 |
|---|---|---|---|---|
| D-01 | 线上 runtime-state 是否已在 2026-07-27 停摆? |
认为 2026-07-27 后无自动 commit,推断线上服务已停摆或数据源卡死(§2.2) |
指出远端 vmess-domain-rotator/runtime-state 在 2026-10-06 仍有提交,初审误看了本地过期分支(§2.1) |
复核正确,且深层原因已查明: 远端 vmess-domain-rotator/runtime-state 最新提交为 2026-10-11T00:34:14Z(ff09327)。为什么 7 月底到 10 月初 commit 稀少?因为:① run_update_and_commit.sh 仅在文件内容变化时才 commit;② 7 月底以来 baka.fun 一直稳居第 1;③ 10 月初的几次 commit(7672a03、56404a9、bfc3933、a7c6fdc、ff09327)全都是因为上游偶发报错/空数据导致 status 在 ok 与 error/fallback_last_good 之间翻转触发的! |
| D-02 | 581 次历史提交是否说明域名频繁抖动、急需 switch_margin_pct 防抖? |
认为 581 次提交证明轮换过频,建议作为高优项引入 10% 切换阈值(§2.1) | 指出 commit 数不等于域名切换数(状态恢复也会产生 commit),防抖属锦上添花(§2.2) | 复核更准确:ok <-> fallback 状态翻转会造成重复提交(§2.3);不能将提交数量直接等同于域名切换次数,也不能据此推断某个上游评分字段的方向。switch_margin_pct 是可选策略增强项,评分策略按 §2.1 的字段与配置约定维护。 |
| D-03 | run_notify() 子进程失败是否会抛异常触发 last_good 回退? |
认为通知命令返回非 0 会抛异常中断主流程(§3.1.4) | 指出 subprocess.run(..., check=False) 不会因非 0 退出码抛异常,真正问题是污染 stdout、无 timeout 及未捕获 OSError(§2.3) |
复核完全正确:domain_updater.py:999 未传 check=True,退出码非 0 不会抛异常。真正的风险是:① 命令注入(shell=True);② 子进程输出直接打到 stdout 冲掉最后一行 JSON;③ 无 timeout 可永久挂起定时任务。 |
| D-04 | install_debian.sh 写入 .updater.env 是否存在引号闭合注入? |
认为用双引号包裹变量写入 .updater.env 会因特殊字符闭合引号导致语法错误(§3.3.2) |
指出 write_env_file 使用的是单引号包裹 + sed "s/'/'\\\\''/g" 转义,单引号路径安全,真正隐患在 systemd EnvironmentFile= 解析规则及换行符(§2.4) |
复核完全正确:install_debian.sh:349-358 已对单引号做了 POSIX 标准转义,Shell source 是安全的;但若变量含换行符或特殊控制字符,或结合 run_update_and_commit.sh:97 的进程参数暴露(2.4 节),仍需加固。 |
| D-05 | 是否应将 cfst_local 与 api 选优流水线强行统一为同一套打分模型? |
建议把 cfst_local 也并入统一的 filter -> score -> sort 评分管线(§4.2) |
反对强行用 api 权重评分覆盖 cfst 原生测速排序,认为会破坏 cfst 自身语义(§2.5) |
折中裁定(各取所长): 不应强行用 weighted_average 重排 cfst 结果,但必须统一两者前后的「准入校验层」与「产物契约层」:即 cfst_local 同样需要经过合法 IP/域名校验、开启下载测速时过滤 download_speed <= 0(见 2.7 节)、去重,并输出与 api 模式结构一致的 state.json。 |
| D-06 | router_local_update.sh 是否应改为「参数映射表」驱动? |
建议用声明式参数映射表替代逐个 if 拼接 cfst 参数(§3.2.2) |
反对在 BusyBox POSIX /bin/sh 中搞表驱动,认为会丧失 set -- 的空白字符安全性(§2.6) |
复核完全正确: 路由器端运行环境为 BusyBox ash,不支持 Bash 数组。现有 set -- "$CFST_BIN" ... 是 POSIX sh 下唯二能安全处理含空格路径的做法,只需在其基础上补齐缺失的 -dt、-dn、-dd 参数及数值合法性校验即可。 |
| D-07 | 复核报告独有发现的 6 项高危问题 | 初审报告未覆盖 | 复核报告新增了 S-01、S-02、P-01、R-01 及 Sub-Store §4.1、§4.2 |
经源码逐行核实,复核报告新增的 6 项问题 100% 成立且极具价值(详见第 4 节汇总)。 |
将初审报告、复核报告与本次审查确认的共识问题按模块整合如下(标注来源与代码精确位置):
uninstall_debian.sh 空 --service-name 导致 rm -rf /var/lib/ 灾难风险(复核 S-01 + 本次确认)
scripts/uninstall_debian.sh:34-43、94-100$2 非空,且卸载前未校验 SERVICE_NAME 非空。当误传 --service-name "" 时,STATE_DIR="/var/lib/${SERVICE_NAME}" 退化为 "/var/lib/",配合 root 权限执行 rm -rf "$STATE_DIR" 会直接删毁系统 /var/lib/!SERVICE_NAME 匹配 ^[a-zA-Z0-9_-]+$,并对 rm -rf 目标路径做前缀白名单断言。build_router_package.sh trap 注册顺序错误导致 TMP_DIR 误删风险(复核 R-01 + 本次确认)
build_router_package.sh:54-64trap cleanup EXIT INT TERM 注册在 --tmp-dir 参数校验之前;若传入 --tmp-dir / 或 --tmp-dir "",脚本因校验报错退出时立即触发 cleanup() 执行 rm -rf "$TMP_DIR"。mktemp -d)后才注册 trap。domain_updater.py 通知命令注入漏洞(RCE)(三方共识)
scripts/domain_updater.py:988-1000.format() 将外部 API 返回的 {domain} / {error} 拼入命令字符串,并传给 subprocess.run(..., shell=True)。UPDATER_STATUS、UPDATER_VALUE、UPDATER_REASON)传递给通知脚本,或使用参数数组 shell=False + timeout=15 + stdout=sys.stderr。last_good 回退值绕过当前过滤规则与黑名单(复核 P-01 + 本次确认)
scripts/domain_updater.py:1010-1020、1071-1149exclude_domains 或 record_filter 中拉黑某个已失效域名后,一旦当次运行遇到网络异常或空候选,脚本从旧状态文件中读出刚被拉黑的 last_good 继续发布。last_good 后必须先通过当前的 is_valid_candidate_value() 与 domain_filter 校验,不合法则丢弃。run_update_and_commit.sh 将 git ls-remote 网络报错误判为「远端分支不存在」,导致重建孤儿分支(复核 S-02 + 本次确认)
scripts/run_update_and_commit.sh:179-195git ls-remote --exit-code --heads "$REMOTE_NAME" "$STATE_BRANCH" 返回码为 2 时代表分支不存在,返回 128 代表网络/认证失败。当前 if ...; then ... else 将 128 混同于分支不存在,直接 git checkout --orphan 新建无历史分支,后续 push 必然因 non-fast-forward 持续失败。rc=$?,仅当 rc == 2 时初始化孤儿分支,rc != 0 && rc != 2 时直接报错退出。scripts/domain_updater.py:755、999、1102、1142;scripts/run_update_and_commit.sh:151-162cfst 子进程与 notify 子进程均未重定向 stdout;且 domain_updater.py:1142 在异常回退时打印的 JSON {"status": "error_use_last_good", "error": ...} 漏掉了选中的域名字段,导致 run_update_and_commit.sh:165 的日志输出 selected_value 为 unknown。stderr,或新增 --result-json-file 显式通信文件;统一正常与回退路径的 JSON schema。scripts/domain_updater.py:1023-1069;scripts/router_local_update.sh:283-320write_text() 或 > 覆写目标文件。路由器端 httpd 并发读取时可能读到截断的空文件或半截 JSON。mv -f / os.replace() 原子替换。substore/operator_template.js 非字符串字段强转 "[object Object]" 污染节点配置(复核 §4.2 + 本次确认)
substore/operator_template.js:168-176extractValue() 在 JSON 模式下对 parsed[key] 直接执行 String(parsed[key])。若配置指向了对象或数组字段,会返回 " [object Object] " 并通过非空校验,把所有 VMess 节点的 server 改写为 "[object Object]"。extractValue() 中限定 typeof candidate === 'string',并对结果做域名/IP 合法性校验(拒绝空白字符、控制字符、[object 等)。substore/operator_template.js 完全忽略数据源 status 与新鲜度(Staleness)(三方共识 + 复核 §4.1)
substore/operator_template.js:184-269local_first 还是 server_only,消费端只看能否提取出字符串,不检查 updated_at_utc 是否已过期数天。同时,因为 run_update_and_commit.sh 在域名未变时不提交,正常情况下远端 updated_at_utc 也会停留在上次切换时刻(如 2026-07-27 到 2026-10-05 间隔两个多月)。updated_at_utc):在 operator_template.js 中增加 local_max_age_sec(例如默认 86400 秒),当路由器 cfst 任务挂死导致本地结果过期时自动降级到远端 server_url;last_verified_at_utc)或由消费端校验 status 字段。router_local_update.sh JSON 注入与 router_local_http.sh PID 复用误杀(三方共识)
scripts/router_local_update.sh:276-320;scripts/router_local_http.sh:40-47、108-123router_local_update.sh 使用 printf 直接将 ERROR_REASON 等变量拼入 JSON,若含双引号或换行符即生成非法 JSON;且缺失 config.local.json 支持的 -dt、-dn、-dd 参数。router_local_http.sh 仅用 kill -0 "$pid" 判断进程存活,路由器重启后 PID 极易被系统其他守护进程复用,stop 时会误杀无辜进程。json_escape() 函数转义 \、" 与控制字符;is_running() 增加检查 /proc/$pid/cmdline 是否包含 httpd。针对您在需求中提到的「即使巨幅改动也可以,只要能够优化业务流程」,结合上述全部问题,提出以下两层重构方案(演进式重构 vs 彻底精简架构):
flowchart TD
subgraph 统一输入适配器
S1["API Source Adapter<br/>(fetch_api_json + field_map)"]
S2["CFST Source Adapter<br/>(run_cfst -> stderr + parse_csv)"]
end
subgraph 统一核心管线
S1 --> V["1. 候选准入校验层<br/>• 合法域名/IP 格式校验<br/>• 过滤规则 & 黑名单 (含 last_good 复检)<br/>• CFST 零下载速度过滤 (download_speed > 0)"]
S2 --> V
V --> M["2. 配置驱动评分排序层<br/>• 按上游字段定义配置 prefer_lower / order<br/>• 支持可选切换迟滞阈值 (switch_margin_pct)"]
M --> F["3. 统一状态机与回退层<br/>• 状态枚举: ok | fallback_empty | fallback_error<br/>• 回退时保留上次有效 top_candidates"]
end
subgraph 原子发布与消费
F --> O["4. 原子落盘层 (tmp + os.replace)<br/>• current_*.txt / .json / state.json<br/>• 独立 IPC 结果文件 (--result-json-file)"]
O --> P["5. 安全 Git 发布层<br/>• GIT_CONFIG_COUNT 环境变量传 Token<br/>• 区分 ls-remote 退出码 2 vs 128<br/>• 忽略瞬时回退导致的无效翻转 commit"]
P --> C["6. 具备时效与类型校验的 Sub-Store 算子<br/>• 严格校验 string 类型与域名/IP 正则<br/>• local_max_age_sec 过期自动降级"]
end
如果允许调整部署方式,当前架构中有两个最大的历史包袱可以彻底简化:
domain_updater.py 拆分为结构清晰的内部子模块(或单文件内清晰的分层 DataClass:Candidate -> QualificationPolicy -> ScoringPolicy -> AtomicStateWriter),并为 config_router.conf 提供一键生成/校验脚本,保证 Python 本地模式与路由器 Shell 模式的 cfst 参数和输出 JSON 100% 同构。runtime-state 当作实时 KV 数据库使用,每次瞬时网络抖动(ok -> fallback_last_good -> ok)都会产生 2 个毫无业务价值的 Git commit,且仓库历史无限膨胀。updater_status 为 fallback_last_good 或 error_use_last_good 且回退选中的域名与远端当前已发布的域名相同时,run_update_and_commit.sh 仅在本地记录告警日志,不向远端 runtime-state 分支提交和推送!这样远端分支永远只保留有效的优选变更,彻底消灭 10 月初那种因瞬时抖动产生的无意义翻转 commit。run_update_and_commit.sh 中支持可选的 MAX_STATE_COMMITS(或保持单提交强推 git commit --amend / 浅历史),避免 runtime-state 累积数千个历史 commit 拖慢克隆与拉取。| 阶段 | 优先级 | 进度状态 | 改动目标 | 涉及文件 | 验收与测试验证 |
|---|---|---|---|---|---|
| Phase 1:止血与高危安全修复 | P0 | ✅ 已完成 (100%) | 1. 调整 config.server.json 自定义评分策略,并在 domain_updater.py 支持字段级方向与平局决胜(§2.1 已更正文档,该策略调整本身不列为 P0 缺陷)2. 修复 uninstall_debian.sh 空 SERVICE_NAME 导致 rm -rf /var/lib/ 漏洞3. 修复 build_router_package.sh trap 提前注册导致误删 TMP_DIR 风险4. 修复 domain_updater.py run_notify() 的 shell=True 命令注入、无超时及 stdout 污染问题 |
config.server.jsonscripts/domain_updater.pyscripts/uninstall_debian.shbuild_router_package.sh |
测试通过:test_p0_yd_score_polarity_fix_and_healthy_ranking、test_p0_notify_stdout_isolation_and_timeout、test_p0_uninstall_debian_empty_service_name_deletes_var_lib、test_p0_build_router_package_trap_prematurely_deletes_tmp_dir |
| Phase 2:发布链路与消费端可靠性加固 | P1 | ✅ 已完成 (100%) | 1. 统一 fallback_last_good 与 error_use_last_good 处理,回退且值未变时执行回退静默,不向远端推送噪音 commit2. 对 last_good 强制执行当前 domain_filter / 格式复检;彻底失败时补发 run_notify3. run_update_and_commit.sh 区分 ls-remote 退出码 2 与 128,改用 GIT_CONFIG_* 环境变量安全传递 Git Token4. domain_updater.py 与 router_local_update.sh 全面改为同目录临时文件 + mv 原子写入,并过滤 download_speed == 0 的测速失败节点5. substore/operator_template.js 修复非字符串强转 "[object Object]" Bug,增加域名/IP 格式校验与本地数据源过期降级(local_max_age_sec: 86400) |
scripts/domain_updater.pyscripts/run_update_and_commit.shscripts/router_local_update.shsubstore/operator_template.js |
测试通过:test_p1_run_update_and_commit_passes_token_in_env_not_argv、test_p1_run_update_and_commit_ls_remote_distinguishes_error_code、test_p1_run_update_and_commit_fallback_silence、test_p1_extract_value_object_stringification_hazard_prevented、test_p1_extract_value_host_validation_enforced、test_p1_router_local_update_atomic_file_write、test_p1_last_good_bypasses_blacklist_prevented、test_p1_error_fallback_stdout_contains_domain_key |
| Phase 3:工程化、跨平台打包与自动化测试 | P2 | ✅ 已完成 (100%) | 1. 补齐 validate_config() 对 api / parser / selection 的完整结构校验2. 修复 router_local_http.sh PID 复用误杀与 router_local_update.sh JSON 转义及零速 IP 过滤(确认 -dt/-dn/-dd 已就绪)3. 修复 build_router_package.sh 在 macOS 下的 .DS_Store / AppleDouble 打包污染与跨架构 httpd 静态检测4. 建立 tests/ 自动化测试套件(覆盖评分极性、过滤、回退、Shell 安全与 Sub-Store 解析)并修正 README.md、workflow.md、CLAUDE.md 文档偏差 |
scripts/router_local_http.shscripts/router_local_update.shbuild_router_package.shREADME.mdworkflow.mdCLAUDE.mdtests/ |
测试通过:test_p2_validate_config_enforces_api_structure、test_p2_cfst_filters_zero_download_speed、test_p2_result_json_file_decouples_ipc_from_stdout、test_p2_router_local_update_filters_zero_speed、test_recheck_correction_router_cfst_params_already_supported、test_p2_operator_template_checks_staleness;18 项测试 0.38s 全绿 |
本节将全仓库所有代码文件、配置文件与文档中的所有问题逐一列出,并标注其在 初审(ARCHITECTURE_REVIEW.md)、复核(ARCHITECTURE_REVIEW_RECHECK.md) 与 本次终审(含源码及线上分支实证) 中的状态与核验结论,确保无一遗漏。
scripts/domain_updater.py(1154 行)与 config.server.json / config.local.json| 编号 | 问题摘要 | 代码行号 | 初审报告 | 复核报告 | 本次终审实证与验证结论 | 严重度 | 修复状态 |
|---|---|---|---|---|---|---|---|
| PY-01 | 上游评分字段与自定义策略说明:yd_score 映射上游 ydScore;是否参与加权及排序方向由配置决定 |
configs/config.server.jsonscripts/rotator/scoring.py |
§6.5 提醒核对上游字段定义 | — | 文档更正(2026-10-18):撤回原 P0 定性和上游公式推断;当前配置仅以延迟、丢包加权,yd_score 用于升序平局决胜 |
配置策略 / 文档 | ✅ 已统一 现有评分测试验证当前配置行为,不证明上游公式 |
| PY-02 | 通知命令 Shell 注入(RCE):run_notify() 用 .format() 将外部 API 的 {domain} / {error} 拼入命令并传给 subprocess.run(..., shell=True) |
domain_updater.py:988-1000 |
✅ §3.1.4 | ✅ §3.1 | ✅ 源码确认:API 返回恶意字符串或特殊字符即可执行任意命令或破坏 Shell 语法 | P0 | ✅ 已修复 改用环境变量传递变量;测试 test_p0_notify_stdout_isolation_and_timeout 通过 |
| PY-03 | 通知子进程无超时、污染 stdout、未捕获 OSError:未设 timeout,未将 stdout 重定向到 stderr |
domain_updater.py:999-1002 |
⚠️ 误判非0退出码会抛异常 | ✅ §2.3 修正 | ✅ 源码确认:check=False 不会因非 0 退出码抛异常,但通知输出到 stdout 会冲掉最后一行 JSON,导致外层脚本 tail -n 1 解析崩溃 |
P1 | ✅ 已修复stdout=sys.stderr 并设 timeout=15;测试 test_p0_notify_stdout_isolation_and_timeout 通过 |
| PY-04 | 彻底失败(无 last_good)时漏调 run_notify():当抛异常且无 last_good 可回退时,直接 return 1 未发告警 |
domain_updater.py:1125-1149 |
❌ 漏检 | ❌ 漏检 | ✅ 源码确认:最需要告警的「完全无可用输出」场景反而静默退出 | P1 | ✅ 已修复 异常且无 last_good 时补调 run_notify(..., "", "error") |
| PY-05 | 双回退路径状态不一致(fallback_last_good vs error_use_last_good)且异常回退漏输出域名字段 |
domain_updater.py:1071-1102 vs 1125-1149 |
⚠️ §3.1.2 仅提及异常分支 | ✅ §3.1 | ✅ 线上实证确认:提交 a7c6fdc 真实触发了 fallback_last_good,清空了 top_candidates: [];而异常分支 L1142 打印的 JSON 缺失 domain/ip 字段 |
P1 | ✅ 已修复 输出包含 selected_json_key;测试 test_p1_error_fallback_stdout_contains_domain_key 通过 |
| PY-06 | last_good 回退绕过当前黑名单与格式校验:从旧 state.json 或 current_*.txt 恢复的 last_good 未经过 is_valid_candidate_value 与 domain_filter 复检 |
domain_updater.py:1010-1020, 1072, 1127 |
❌ 漏检 | ✅ §3.3 (P-01) | ✅ 源码确认:拉黑某故障域名后若遇 API 异常,刚被拉黑的域名会借 last_good 复活 |
P1 | ✅ 已修复 回退前复检 domain_filter;测试 test_p1_last_good_bypasses_blacklist_prevented 通过 |
| PY-07 | cfst 子进程输出直接打向 stdout:run_cfst() 直接 subprocess.run(cmd, check=False) 未重定向 stdout |
domain_updater.py:755 |
✅ §3.1.1 | ✅ §3.1 | ✅ 源码确认:若 cfst 崩溃或最后一行非空,或未来被脚本捕获 stdout,会破坏 JSON 通信协议 |
P1 | ✅ 已修复 子进程已做 capture_output=True,彻底杜绝冲刷 stdout |
| PY-08 | 本地 cfst 模式未过滤 download_speed == 0.00 的零速 IP |
domain_updater.py:926-972runtime/current_ip.json:11 |
❌ 漏检 | ❌ 漏检 | ✅ 本地产物确认:仓库内 runtime/current_ip.json 已将 "download_speed": "0.00" 的 104.23.141.56 选为 "status": "ok" |
P1 | ✅ 已修复 过滤 download_speed == 0.00;测试 test_p2_cfst_filters_zero_download_speed 通过 |
| PY-09 | 文件落盘非原子写入:write_selected_outputs() 直接 write_text() 覆写 .txt、.json、state.json |
domain_updater.py:1023-1069 |
✅ §3.1.5 | ✅ §3.1 | ✅ 源码确认:写入中途被中断或并发读取会产生半截损坏 JSON | P1 | ✅ 已修复write_text_file / write_json_file 采用 .tmp.$$ + os.replace 原子写入 |
| PY-10 | vps789 API 的 createdTime 实为建档时间而非测速时间 |
config.server.json:45, 63domain_updater.py:669-678 |
⚠️ §2.2 误认为测速时间并建议开 within_hours: 24 |
⚠️ §3.2 沿用初审假设 | ✅ 线上实证确认:baka.fun 在 7 月和 10 月的 created_raw 恒为 "2026-05-28 20:14:47",开启 within_hours: 24 会误杀所有老牌稳定域名 |
P1 | ✅ 已修复 修正文档说明,默认禁用 within_hours 防误杀老牌稳定域名 |
| PY-11 | validate_config() 漏检 api、parser、domain_filter、selection 顶层字典存在性 |
domain_updater.py:375-525 |
❌ 漏检 | ❌ 漏检 | ✅ 源码确认:source.type == "api" 时未检查 cfg.get("api"),--print-output-settings 可通过但运行时抛 KeyError |
P2 | ✅ 已修复 严格校验顶层字典与 api.url;测试 test_p2_validate_config_enforces_api_structure 通过 |
| PY-12 | 缺少切换迟滞(Hysteresis / switch_margin_pct)机制:新旧第 1 名分差极小时也会触发轮换 |
domain_updater.py:905-923 |
✅ §2.1 | ✅ §2.2(定性为增强项) | ✅ 源码确认:在既定自定义评分策略下,增加可选 switch_margin_pct(如 5%~10%)可进一步抑制边界抖动 |
P2 | ✅ 已修复choose_domain() 实现迟滞防抖,配置 switch_margin_pct: 5.0 |
scripts/run_update_and_commit.sh(378 行)| 编号 | 问题摘要 | 代码行号 | 初审报告 | 复核报告 | 本次终审实证与验证结论 | 严重度 | 修复状态 |
|---|---|---|---|---|---|---|---|
| SH-01 | Git HTTP Token 在进程命令行参数中明文暴露:git -c "http.extraHeader=Authorization: Basic ${auth_b64}" 在 fetch/push 期间可通过 ps aux 查看 |
run_update_and_commit.sh:91-101 |
❌ 漏检 | ❌ 漏检 | ✅ 源码确认:多租户或存在监控探针的服务器上,任何本地用户均可从 /proc/<pid>/cmdline 截取 Token |
P1 | ✅ 已修复 改用 GIT_CONFIG_* 环境变量;测试 test_p1_run_update_and_commit_passes_token_in_env_not_argv 通过 |
| SH-02 | git ls-remote 网络失败(退出码 128)被误判为远端分支不存在(退出码 2):导致本地重建孤儿分支,后续 push 因 non-fast-forward 永久卡死 |
run_update_and_commit.sh:179-195 |
❌ 漏检 | ✅ §3.3 (S-02) | ✅ 源码确认:if git ls-remote --exit-code ...; then ... else 未区分退出码 2 与 128,网络闪断即造成本地 worktree 脑裂 |
P1 | ✅ 已修复 明确区分退出码 2 与 128 报错退出;测试 test_p1_run_update_and_commit_ls_remote_distinguishes_error_code 通过 |
| SH-03 | 未识别 fallback_last_good 状态,导致空候选回退被当作正常轮换提交,并在恢复时产生二次翻转提交 |
run_update_and_commit.sh:164-167, 334-340 |
❌ 漏检 | ❌ 漏检 | ✅ 线上实证确认:runtime-state 提交 a7c6fdc(fallback_last_good)与 ff09327(恢复 ok)为此问题直接产物 |
P1 | ✅ 已修复 实现回退静默,选值未变时跳过 commit;测试 test_p1_run_update_and_commit_fallback_silence 通过 |
| SH-04 | 通过 tail -n 1 解析 domain_updater.py 标准输出:与子进程日志强耦合,且 invoked 两次 domain_updater.py |
run_update_and_commit.sh:139, 151-162 |
✅ §3.3.1 | ✅ §3.1 | ✅ 源码确认:应通过 --result-json-file 或单次运行输出元数据解决 |
P2 | ✅ 已修复 改用 --result-json-file 传递机器状态;测试 test_p2_result_json_file_decouples_ipc_from_stdout 通过 |
scripts/install_debian.sh(518 行)与 scripts/uninstall_debian.sh(119 行)| 编号 | 问题摘要 | 代码行号 | 初审报告 | 复核报告 | 本次终审实证与验证结论 | 严重度 | 修复状态 |
|---|---|---|---|---|---|---|---|
| OP-01 | uninstall_debian.sh 空 --service-name 导致 rm -rf /var/lib/ 删库灾难 |
uninstall_debian.sh:34-43, 94-100 |
❌ 漏检 | ✅ §3.3 (S-01) | ✅ 源码确认:STATE_DIR="/var/lib/${SERVICE_NAME}" 在 SERVICE_NAME="" 时变为 /var/lib/,root 下执行 rm -rf "$STATE_DIR" 后果极其严重 |
P0 | ✅ 已修复 强制非空正则校验与 /var/lib/?* 严格前缀断言;测试 test_p0_uninstall_debian_empty_service_name_deletes_var_lib 通过 |
| OP-02 | .updater.env 文件由 systemd EnvironmentFile= 与 bash source 双重解析的转义差异及换行符风险 |
install_debian.sh:349-358, 391 |
⚠️ §3.3.2 误认为双引号拼接 | ✅ §2.4 修正为单引号但指出 EnvironmentFile 差异 |
✅ 源码确认:write_env_file 使用了单引号转义,但 run_update_and_commit.sh:74 又在 bash 中 source "$ENV_FILE",需禁止环境变量含换行符并限制 SERVICE_NAME 字符集 |
P2 | ✅ 已修复 单引号包裹转义且服务名受安全正则限制 |
scripts/router_local_update.sh(354 行)、scripts/router_local_http.sh(160 行)与 build_router_package.sh(140 行)| 编号 | 问题摘要 | 代码行号 | 初审报告 | 复核报告 | 本次终审实证与验证结论 | 严重度 | 修复状态 |
|---|---|---|---|---|---|---|---|
| RT-01 | build_router_package.sh 在参数校验前注册 trap cleanup EXIT,若传 --tmp-dir / 会触发 rm -rf / |
build_router_package.sh:54-64, 90-100 |
❌ 漏检 | ✅ §3.3 (R-01) | ✅ 源码确认:L59 先 trap cleanup EXIT,L90 才检查 TMP_DIR == "/",一旦命中报错 exit 1 就会执行 rm -rf "/" |
P0 | ✅ 已修复 延迟至参数校验后注册 trap;测试 test_p0_build_router_package_trap_prematurely_deletes_tmp_dir 通过 |
| RT-02 | router_local_update.sh 手工 printf 拼 JSON 未转义特殊字符(JSON 注入/损坏)且非原子写文件 |
router_local_update.sh:276-320 |
✅ §3.2.1 | ✅ §3.1 | ✅ 源码确认:ERROR_REASON 或 SELECTED_IP 若含 "、\ 或换行符会导致生成的 JSON 语法错误;直接重定向 > 写文件非原子 |
P1 | ✅ 已修复 实现 atomic_write_file()(.tmp.$$ + mv -f 原子替换);测试 test_p1_router_local_update_atomic_file_write 通过 |
| RT-03 | router_local_http.sh 仅用 kill -0 检查 PID,路由器重启后存在 PID 复用误杀系统进程风险 |
router_local_http.sh:40-47, 108-123 |
✅ §3.2.3 | ✅ §3.1 | ✅ 源码确认:未检查 /proc/$pid/cmdline 是否包含 httpd |
P1 | ✅ 已修复 检查 /proc/$pid/cmdline 包含 httpd |
| RT-04 | router_local_update.sh 缺失 config.local.json 中的 -dt、-dn、-dd 参数支持,且未过滤 download_speed == 0.00 |
router_local_update.sh:115-133, 211-265config_router.conf:1-31 |
✅ §3.2.2(部分) | ✅ §2.6(建议保留 set --) |
✅ 源码确认:保留 POSIX set -- 安全传参方式,补齐 CFST_DOWNLOAD_TIME (-dt)、CFST_DOWNLOAD_NUM (-dn)、CFST_DISABLE_DOWNLOAD (-dd) 与零速度过滤 |
P2 | ✅ 已修复 / 纠偏 测速自动过滤 0 速节点,测试 test_p2_router_local_update_filters_zero_speed 与 test_recheck_correction_router_cfst_params_already_supported 通过 |
| RT-05 | build_router_package.sh 在 macOS 下打包混入 .DS_Store 和 AppleDouble ._* 扩展属性文件,且跨架构执行 Linux busybox 误报无 httpd |
build_router_package.sh:67-88, 134 |
❌ 漏检 | ❌ 漏检 | ✅ 源码与工作区确认:busybox/.DS_Store 存在,macOS 下 tar 未设 COPYFILE_DISABLE=1;宿主机无法直接运行 armv7l ELF |
P2 | ✅ 已修复 排除 .DS_Store/._*,改用 strings 静态检测 httpd applet |
substore/operator_template.js(324 行)| 编号 | 问题摘要 | 代码行号 | 初审报告 | 复核报告 | 本次终审实证与验证结论 | 严重度 | 修复状态 |
|---|---|---|---|---|---|---|---|
| JS-01 | extractValue() 将非字符串 JSON 字段强转为 "[object Object]" 并覆写所有代理节点的 server |
operator_template.js:168-176 |
❌ 漏检 | ✅ §4.2 | ✅ 源码确认:String(parsed[key]).trim() 对对象返回 "[object Object]",非空检查通过后直接污染全部节点 |
P1 | ✅ 已修复 严格类型守卫拒绝非 string;测试 test_p1_extract_value_object_stringification_hazard_prevented 通过 |
| JS-02 | extractValue() 未对提取出的域名/IP 做格式合法性校验:若上游返回 HTML 报错页(如 502 Bad Gateway 文本)且使用 text 格式,会把整行 HTML 写入节点 server |
operator_template.js:154-182 |
⚠️ §3.4.1 仅提静默吞错 | ✅ §4.2 | ✅ 源码确认:format=text 时 firstNonEmptyLine(rawText) 会把 <html> 或报错文本直接当作域名返回 |
P1 | ✅ 已修复 严格 RFC 1123 域名与 IP 正则校验;测试 test_p1_extract_value_host_validation_enforced 通过 |
| JS-03 | 未校验数据源 status 与新鲜度(updated_at_utc),本地路由器任务挂死数周仍死用过期本地 IP |
operator_template.js:184-269 |
✅ §2.2 / §3.4 | ✅ §4.1(补充指出了 server_only 若强校验时间需服务端心跳配合) |
✅ 源码确认:对本地源(local_url)应默认校验 local_max_age_sec;对远端源(server_url)默认校验 status 非空错误,仅在开启服务端心跳时才校验 server_max_age_sec |
P1 | ✅ 已修复 增加 LOCAL_MAX_AGE_SEC: 86400 与超时自动降级;测试 test_p2_operator_template_checks_staleness 通过 |
| JS-04 | HTTP 请求客户端探测仅支持 $httpClient 与 fetch,且 $httpClient 回调未处理同步抛错 |
operator_template.js:116-152 |
✅ §3.4.2 | ✅ §3.1 | ✅ 源码确认:补充 $.http 兼容及 try/catch 包裹 $httpClient.get |
P2 | ✅ 已修复 同时兼容 $substore.http 与 $httpClient 异步/同步处理 |
本节针对上述 P0/P1/P2 问题提供可直接落地的代码级修改参考。
configs/config.server.json 配置示例评分设置位于 scoring,selection 仅设置候选数量。下例与当前配置一致;使用其他数据源时,应先核对上游字段定义,再决定是否使用 yd_score 及其排序方向。
{
"scoring": {
"enabled": true,
"strategy": "weighted_average",
"weighted_fields": [
{"field": "yd_latency", "weight": 1.0, "prefer_lower": true},
{"field": "yd_pkg_lost_rate", "weight": 10.0, "prefer_lower": true}
],
"prefer_lower": true,
"within_hours": 0,
"switch_margin_pct": 5.0,
"tie_breakers": [
{"field": "yd_score", "order": "asc"},
{"field": "avg_latency", "order": "asc"},
{"field": "created_at", "order": "desc"},
{"field": "domain", "order": "asc"}
]
},
"selection": {"top_n": 3}
}
parse_scored_records() / rank_scored_records() 按配置计算本项目的 score_value,再按全局方向与 tie_breakers 排序。字段级方向与全局方向不一致时,先调整该字段值的符号再加权;上游 yd_score 的值按映射读取。choose_domain() 使用传入的 switch_margin_pct 执行可选迟滞策略;scripts/rotator/main.py 从 scoring 读取该设置并传入。yd_score 升序平局决胜不表示本项目定义了上游的评分公式,相关测试也仅验证配置行为。domain_updater.py 原子写文件、安全通知、last_good 复检与零速 IP 过滤(对应 PY-02 ~ PY-09)import os
import tempfile
def atomic_write_text(path: Path, content: str) -> None:
"""同目录临时文件写入 + os.replace 原子替换,杜绝半截文件。"""
path.parent.mkdir(parents=True, exist_ok=True)
fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent))
tmp_path = Path(tmp_name)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(content)
f.flush()
os.fsync(f.fileno())
os.replace(str(tmp_path), str(path))
except Exception:
tmp_path.unlink(missing_ok=True)
raise
def is_last_good_still_allowed(
candidate_value: str,
source_type: str,
cfg: Dict[str, Any],
) -> bool:
"""确保回退使用的 last_good 依然符合当前格式与黑名单规则。"""
if not is_valid_candidate_value(candidate_value, source_type):
return False
if source_type != "cfst_local":
domain_filter = cfg.get("domain_filter", {})
dummy = [{"domain": candidate_value.strip().lower()}]
if not filter_domains(dummy, domain_filter):
return False
return True
def run_notify(notify_cfg: Dict[str, Any], status: str, domain: str, error: str) -> None:
"""通过环境变量传递动态内容,重定向 stdout 到 stderr,设置超时并捕获异常。"""
if not notify_cfg.get("enabled", False):
return
command = str(notify_cfg.get("command", "")).strip()
if not command:
return
timeout_sec = int(notify_cfg.get("timeout_seconds", 15))
env = os.environ.copy()
env["UPDATER_STATUS"] = str(status)
env["UPDATER_DOMAIN"] = str(domain)
env["UPDATER_ERROR"] = str(error)
try:
subprocess.run(
["/bin/sh", "-c", command],
env=env,
stdout=sys.stderr,
stderr=sys.stderr,
timeout=timeout_sec,
check=False,
)
except Exception as exc:
print(f"WARN: notify command failed: {exc}", file=sys.stderr)
同时在 select_best_cfst_record() 中过滤零下载速度记录(当未开启 disable_download 时):
def select_best_cfst_record(
records: List[Dict[str, Any]],
require_positive_speed: bool = True,
) -> Optional[Dict[str, Any]]:
for item in records:
ip = str(item.get("ip", "")).strip()
if not is_valid_ip_Or_domain(ip):
continue
if require_positive_speed:
speed = to_float(item.get("download_speed"))
if speed is None or speed <= 0.0:
continue
return item
return None
scripts/run_update_and_commit.sh(对应 SH-01 ~ SH-04)用 GIT_CONFIG_COUNT 替代命令行 -c http.extraHeader,防止 ps aux 泄露 Token:
run_git() {
local repo_dir="$1"
shift
if [[ -n "$GIT_HTTP_TOKEN" ]]; then
local auth_b64
auth_b64="$(printf '%s:%s' "$GIT_HTTP_USERNAME" "$GIT_HTTP_TOKEN" | base64 | tr -d '\n')"
GIT_CONFIG_COUNT=1 \
GIT_CONFIG_KEY_0="http.extraHeader" \
GIT_CONFIG_VALUE_0="Authorization: Basic ${auth_b64}" \
git -C "$repo_dir" "$@"
else
git -C "$repo_dir" "$@"
fi
}
精确区分 git ls-remote --exit-code 的退出码 0、2 与其他错误(128):
set +e
run_git "$REPO_DIR" ls-remote --exit-code --heads "$REMOTE_NAME" "$STATE_BRANCH" >/dev/null 2>&1
ls_remote_rc=$?
set -e
if [[ $ls_remote_rc -eq 0 ]]; then
run_git "$REPO_DIR" fetch "$REMOTE_NAME" "${STATE_BRANCH}:refs/remotes/${REMOTE_NAME}/${STATE_BRANCH}"
run_git "$REPO_DIR" worktree add -B "$STATE_BRANCH" "$WORKTREE_DIR" "refs/remotes/${REMOTE_NAME}/${STATE_BRANCH}"
elif [[ $ls_remote_rc -eq 2 ]]; then
log "Remote branch '${STATE_BRANCH}' does not exist on '${REMOTE_NAME}', creating orphan branch"
run_git "$REPO_DIR" worktree add --detach "$WORKTREE_DIR"
run_git "$WORKTREE_DIR" checkout --orphan "$STATE_BRANCH"
run_git "$WORKTREE_DIR" rm -rf --ignore-unmatch . >/dev/null 2>&1 || true
else
log "ERROR: git ls-remote failed with exit code ${ls_remote_rc} (network or auth error); aborting to prevent orphan branch split-brain"
exit "$ls_remote_rc"
fi
统一处理 fallback_last_good 与 error_use_last_good,抑制瞬时回退导致的无意义 Git Commit:
if [[ "$updater_status" == "error_use_last_good" || "$updater_status" == "fallback_last_good" ]]; then
log "WARN: updater fallback (${updater_status}), kept last good ${value_label}: ${selected_value:-unknown}"
# 若远端分支已有完全相同的选中值,则不推送仅状态翻转的噪音 commit
if [[ -f "${WORKTREE_DIR}/${TARGET_TXT_NAME}" ]] && [[ "$(tr -d '\r\n' < "${WORKTREE_DIR}/${TARGET_TXT_NAME}")" == "$selected_value" ]]; then
log "No value change during fallback (${selected_value}); skipping remote git commit to avoid status-flip noise"
exit 0
fi
fi
scripts/uninstall_debian.sh 与 build_router_package.sh 高危删库漏洞(对应 OP-01, RT-01, RT-05)scripts/uninstall_debian.sh 强制校验 SERVICE_NAME:
if [[ -z "${SERVICE_NAME:-}" || ! "$SERVICE_NAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "ERROR: Invalid or empty --service-name: '${SERVICE_NAME:-}'" >&2
exit 1
fi
STATE_DIR="/var/lib/${SERVICE_NAME}"
if [[ "$PURGE_STATE" == "true" ]]; then
case "$STATE_DIR" in
/var/lib/?*) rm -rf "$STATE_DIR" ;;
*) echo "ERROR: Refusing to remove unsafe STATE_DIR: $STATE_DIR" >&2; exit 1 ;;
esac
fi
build_router_package.sh 延后 trap 注册并清理 macOS 打包污染:
# 在完成所有参数非空校验及禁止 "/" 校验之后,再创建目录并注册 trap:
rm -rf "$TMP_DIR"
mkdir -p "$STAGE_DIR"
trap cleanup EXIT INT TERM
# 打包时禁用 macOS AppleDouble 扩展属性并排除 .DS_Store:
COPYFILE_DISABLE=1 tar --exclude='.DS_Store' --exclude='._*' -czf "$OUT_PATH" -C "$TMP_DIR" "$PKG_ROOT_NAME"
scripts/router_local_update.sh 与 scripts/router_local_http.sh(对应 RT-02 ~ RT-04)router_local_update.sh 增加 POSIX json_escape 与同目录临时文件原子替换:
json_escape() {
printf '%s' "$1" | tr '\r\n\t' ' ' | sed 's/\\/\\\\/g; s/"/\\"/g'
}
atomic_write() {
target="$1"
tmp_file="${target}.tmp.$$"
cat > "$tmp_file" && mv -f "$tmp_file" "$target"
}
router_local_http.sh 校验 /proc/$pid/cmdline 防止 PID 复用误杀:
is_running() {
[ -f "$PID_FILE" ] || return 1
pid="$(cat "$PID_FILE" 2>/dev/null || true)"
[ -n "$pid" ] || return 1
kill -0 "$pid" 2>/dev/null || return 1
if [ -r "/proc/${pid}/cmdline" ]; then
tr '\000' ' ' < "/proc/${pid}/cmdline" | grep -q "httpd" || return 1
fi
return 0
}
substore/operator_template.js 类型安全、格式校验与本地过期降级(对应 JS-01 ~ JS-04)function isValidHostOrIp(value) {
if (typeof value !== 'string') return false;
const s = value.trim();
if (!s || s.length > 253 || /[\s<>"{}|\\^`[\]]/.test(s) || s.includes('[object')) {
return false;
}
// IPv4 / IPv6 / 域名基础合法性校验
const ipv4Re = /^(?:\d{1,3}\.){3}\d{1,3}$/;
const ipv6Re = /^[0-9a-fA-F:]+$/;
const domainRe = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$/;
return ipv4Re.test(s) || ipv6Re.test(s) || domainRe.test(s);
}
function extractValue(rawText, format, jsonKeys, maxAgeSec) {
const text = typeof rawText === 'string' ? rawText.trim() : '';
if (!text) return '';
const tryJson = format === 'json' || (format === 'auto' && text.startsWith('{'));
if (tryJson) {
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === 'object') {
// 若配置了最大新鲜度阈值(如本地源 local_max_age_sec),检查 updated_at_utc
if (maxAgeSec > 0 && typeof parsed.updated_at_utc === 'string') {
const updatedMs = Date.parse(parsed.updated_at_utc);
if (!Number.isNaN(updatedMs) && Date.now() - updatedMs > maxAgeSec * 1000) {
logWarn(`数据源已过期: updated_at_utc=${parsed.updated_at_utc}`);
return '';
}
}
for (const key of jsonKeys) {
const val = parsed[key];
if (typeof val === 'string' && isValidHostOrIp(val)) {
return val.trim();
}
}
}
if (format === 'json') return '';
} catch (err) {
if (format === 'json') return '';
}
}
const line = firstNonEmptyLine(text);
return isValidHostOrIp(line) ? line : '';
}
tests/ 已全部建设并全绿通过 ✅)遵循 CLAUDE.md 的「先建测试再动生产逻辑」原则,自动化测试体系已在 tests/ 目录全面落地,18 项端到端与契约测试在 0.38 秒内 100% 全绿通过(基于 Python 标准库 unittest + Node.js 原生 vm,零第三方外部依赖)。
| 测试文件 | 覆盖范围与断言目标 | 用例数 | 执行耗时 | 测试状态 |
|---|---|---|---|---|
tests/test_domain_updater.py |
• 当前自定义加权策略与 yd_score 升序平局决胜(不验证上游公式)• run_notify 标准输出隔离与超时保护 • 异常回退包含 domain/ip 键 • 黑名单 last_good 禁止回退复活 • 本地 cfst 过滤 0.00 下载速度 • --result-json-file IPC 解耦• validate_config 严格顶层结构校验 • [方案B] rotator 子包解耦与 Facade 契约完整性 |
8 项 | ~0.15s | ✅ 全部通过 (PASS) |
tests/test_shell_lifecycle.py |
• uninstall_debian.sh 空服务名安全校验(严禁 rm -rf /var/lib/)• build_router_package.sh 延迟注册 trap 防止误删工作目录• run_update_and_commit.sh 采用 GIT_CONFIG_* 环境变量传递 Token(argv 零暴露)• 严格区分 git ls-remote 退出码 2 vs 128• 回退静默跳过噪音 Git commit • [方案B] 模式A Git 历史滚动修剪(50 commit 滚动保留)端到端验证 • [结构优化] scripts 分层物理结构与 configs/ 目录结构集中收敛完整性断言 |
7 项 | ~0.75s | ✅ 全部通过 (PASS) |
tests/test_router_scripts.py |
• router_local_update.sh .tmp.$$ + mv -f 原子替换写入• 测速结果优先过滤 download_speed > 0• 纠偏初审报告关于 -dt/-dn/-dd 参数支持情况• [方案B] build_router_package.sh --sync-from-config SSOT 单源参数自动投影验证 |
4 项 | ~0.50s | ✅ 全部通过 (PASS) |
tests/test_substore_operator.py |
• extractValue() 严格类型守卫(拒绝非字符串对象强转为 [object Object])• 域名 (RFC 1123) 与 IPv4/IPv6 正则校验(拒绝 HTML 报错与非法字符) • LOCAL_MAX_AGE_SEC (24h) 本地路由器源超时自动降级 |
3 项 | ~0.06s | ✅ 全部通过 (PASS) |
python3 -m unittest discover tests -v
# 输出:Ran 22 tests in 1.419s, OK
经与用户对齐并选定「模式 A(滚动保留最近 50 次提交,兼顾轻量与防误封)」,方案 B 现已 100% 落地并完成全量验证:
runtime-state 分支追加 commit,经过数月运行会堆积上万个 commit,导致远程仓库体积虚高、拉取缓慢。scripts/server/run_update_and_commit.sh 引入 GIT_MAX_STATE_COMMITS(默认 50 次提交,可由用户自定义)。rebase --onto,精简历史。--force-with-lease 推送;若远端存在分支保护策略拒绝 force-push,脚本自动捕获并从 untrimmed 备份分支无损回滚,平滑降级为普通追加推送,绝不中断服务。mapfile(避免 macOS / 旧系统 Bash 3.2 不支持)与 set -u 展开空数组崩溃隐患,兼容任意 Linux/Debian/macOS 环境。config_router.conf 与服务端/本地配置 config.local.json 割裂,若用户调整了测速线程数、测试时间、测速端口或输出文件名,需双重维护。build_router_package.sh 中新增 --sync-from-config 命令行参数支持。TOP_N, VALUE_TEXT_FILE, CFST_DISPLAY_COUNT, CFST_IP_FILE, CFST_PORT, CFST_DISABLE_DOWNLOAD 等参数动态投影同步至 tarball 内的 config_router.conf。domain_updater.py 领域模块化解耦domain_updater.py 膨胀为 1246 行单文件单片脚本,混合了 CLI、HTTP、CSV 解析、评分策略、原子 IO、系统通知等多种无关职责。scripts/rotator/:storage.py:原子落盘写入与状态读写config.py:配置严格校验、路径解析与输出映射filter.py:域名与 IP 验证、正则清洗与黑名单排除sources.py:远程 API 抓取与本地 CFST 结果解析scoring.py:自定义加权评分、字典序排序、迟滞防抖与候选列表构造notify.py:通知命令执行与超时防护main.py:主轮换工作流编排与 CLI 参数解析scripts/domain_updater.py 重构为精简的外观门面(Facade),全面重新导出所有符号,保持 100% 向后兼容,现存 systemd、cron 及自动化测试零修改直接运行。configs/)scripts/ 目录平铺堆砌了服务端 Debian 安装/卸载脚本、路由器嵌入式更新/HTTP 脚本、Python 脚本等,职责界限模糊。config.server.json, config.local.json, config_router.conf),容易杂乱。configs/ 目录,将服务端、本地与路由器配置文件集中收纳;根目录彻底清爽无散落文件,项目内所有命令默认引用统一更新为 configs/,并在 Python 与 Shell 脚本内建自适应双向探测逻辑(优先探测传入路径,无路径时依次探测 configs/ 目录)。scripts/server/:收纳 Linux / Debian 服务端运维脚本(run_update_and_commit.sh, install_debian.sh, uninstall_debian.sh)。scripts/router/:收纳路由器 BusyBox 嵌入式运行时脚本(router_local_update.sh, router_local_http.sh)。scripts/ 顶层不再保留任何中间转发包装脚本(thin forwarder wrappers),所有文档、systemd 单元模板及打包工具均直接统一指向新结构路径,架构极致纯粹。