argon-review.md 20 KB

这里为您重新生成了一份单次完整输出、无任何截断的 Markdown 审查报告。您可以直接点击代码块右上角的“复制”按钮,一键保存为本地 .md 文件:

vmess-domain-rotator 全景架构与代码深度审查报告

  • 审查日期:2026-10-23
  • 审查范围:全量业务源码(Python / Bash / POSIX sh / Sub-Store JS)、三套配置文件、打包脚本、Claude Hooks/Skills 研发护栏、三份历史审查文档及 7 套自动化测试套件。
  • 审查目标:全局梳理项目架构,交叉核验历史终审台账(ARCHITECTURE_REVIEW_UNIFIED.md)与当前实际代码的一致性,排查未闭环的逻辑漏洞、边界风险与测试泄漏。

一、 项目全景总览 (Project Overview)

vmess-domain-rotator 是一个完成度极高、工程边界清晰的 Cloudflare 优选域名/IP 自动化选优与分发系统。其核心架构分为三层:

  1. 多源采集与选优层(Producers)
    • Server 模式(configs/config.server.json + scripts/rotator/):拉取第三方监控 API,经字段映射、黑名单过滤、加权评分、平局决胜及切换迟滞(Hysteresis)防抖后选出最优域名。
    • 本地 Python 模式(configs/config.local.json + scripts/rotator/):调用本地 cfst 二进制测速,解析 CSV,自动剔除零速死节点(除非显式配置 -dd)。
    • BusyBox 路由器模式(configs/config_router.conf + scripts/router/):纯 POSIX sh + awk 实现,采用 RAM (/tmp) 与 Flash (/jffs) 动静分离设计,保护路由器闪存寿命,并通过 busybox httpd 暴露给局域网。
  2. 状态总线与发布层(Publishers)
    • scripts/server/run_update_and_commit.sh:双频自适应时间窗口(高峰/非高峰容差)、独立 Git Worktree 检出 runtime-state 孤儿分支、回退静默抑制(防状态翻转刷屏)、以及 Mode A 历史滚动修剪(Squashed Rebase 保留最近 50 次提交)。
  3. 下游消费层(Consumers)
    • substore/operator_template.js:Sub-Store 节点替换算子,支持手动参数最高优先级覆盖、多源降级(default / server_only / router_only)、RFC 1123 域名/IP 严格校验及本地数据 24 小时过期降级。

二、 架构与工程亮点 (Strengths)

  1. 极高水准的模块解耦与向后兼容:domain_updater.py 拆分为 scripts/rotator/ 下 7 个高内聚子模块(config / filter / sources / scoring / storage / notify / main),同时顶层保留完整 Facade 导出,兼顾了代码可维护性与外部调用兼容。
  2. 严密的 AI 研发护栏:.claude/hooks/check_syntax.py 实现了零副作用的语法门禁(Python AST compile、Bash/sh -n、Node --check),配合 verify-rotator Skill 的隔离预检规范,工程化程度极高。
  3. 零外部依赖测试体系:tests/ 下 7 个测试文件完全基于标准库 unittest 与 Node 内置 vm 构建,执行速度快且可移植性强。

三、 深度代码审查:现存漏洞与“账实不符” (Findings)

尽管 docs/reviews/ARCHITECTURE_REVIEW_UNIFIED.md 标记“所有 P0/P1/P2 问题 100% 修复完毕”,但通过逐行比对当前实际代码与终审报告台账,共发现 8 处关键的实际代码与报告声明不符、未闭环的逻辑缺陷及安全隐患:

🔴 1. 【P1 消费端逻辑缺陷】Sub-Store default 模式依然会被路由器的故障回退值劫持

  • 位置:substore/operator_template.js:184-239 (selectValueRouterFirst)
  • 现象与根因: 当路由器断网或测速失败时,router_local_update.sh:188-196 会写出 "status": "error_use_last_good",且该 JSON 不包含 top_candidates 字段,同时将 updated_at 刷新为当前报错时间。 此时在 operator_template.js 中:
    1. isRouterDataFresh(routerObj) 检查通过(因为 updated_at 刚刚被错误回退刷新);
    2. parseRouterSpeedMBps(routerObj) 因为没有 top_candidates,返回 status: "missing";
    3. 第 235 行低速拦截条件为 if (telemetry.router.speedParse === "ok" && telemetry.router.speedMBps < ROUTER_MIN_SPEED_MB_PER_S) —— 由于 speedParse 是 "missing",直接绕过了低速门禁;
    4. 最终在 server 完全健康(status: "ok")的情况下,依然优先返回了路由器处于故障回退状态的旧 IP。
  • 修复建议: 在 selectValueRouterFirst() 中,当 telemetry.router.status !== "ok" 且 telemetry.server.ok && telemetry.server.status === "ok" 时,优先降级选用健康的 server;或者在 router_local_update.sh 回退时不要刷新用于新鲜度判定的成功观测时间戳。

🔴 2. 【P1 发布链路缺陷】Git Push 失败后,若下一轮优选域名未变,将永久跳过重试

  • 位置:scripts/server/run_update_and_commit.sh:277-318
  • 现象与根因: 初审报告 P1-3 和复审报告 §3.2 均指出了此问题,但当前代码中依然未修复。 脚本在第 277 行读取 before="$(git -C "$work_dir" show "HEAD:${selected_rel}")"(读取的是本地 runtime-state 分支的 HEAD),并在第 314 行判断:如果 after == before 且 status_unchanged == 1,则直接 log "... skip git commit and push"; exit 0。 故障推演:第 1 轮选出新域名 a.com,本地 git commit 成功,但因网络抖动 git push 失败退出。第 2 轮(30 分钟后)运行,API 依然选出最优域名 a.com —— 此时本地 HEAD 已经是 a.com,触发 after == before 直接 exit 0。本地与远端永久分叉,远端永远收不到这次更新,直到未来某天域名再次变化或人工执行 --force-commit。
  • 修复建议: 将“是否需要 git commit”与“是否需要 git push”解耦。在 exit 0 跳过前,检查当 push_enabled == "1" 时,本地 HEAD 是否领先于远端跟踪分支(例如比对 git rev-parse HEAD 与 git rev-parse @{u}),若存在未推送的 commit,则直接跳转至推送阶段重试。

🔴 3. 【P0/S0 安全隐患】真实 API Token 仍在配置文件中明文入库

  • 位置:configs/config.server.json:6
  • 现象与根因: "url": "https://vps789.com/openApi/listMonitors?token=UR6SYEEATJGVOPIST6QHCPFNAJ8233S8" 依然明文硬编码在配置文件中,且 scripts/rotator/config.py / sources.py 并未实现环境变量(如 AUTODOMAIN_API_TOKEN)或 token_file 的注入支持。
  • 修复建议:
    1. 在 vps789.com 后台轮换重置该 Token;
    2. 在 sources.py:fetch_api_json() 中支持读取环境变量(如 os.environ.get("AUTODOMAIN_API_TOKEN"))或 api.token_file 注入查询参数,将 config.server.json 中的明文移除。

🟠 4. 【P1 状态污染与策略穿透】last_good 回退与状态读取的三个漏洞

  • 位置:scripts/rotator/main.py:59, 125-130, 179-181 & configs/config.local.json:37
  • 现象与根因:
    1. state.json 非字典类型崩溃风险 (P-07):若 state.json 内容被外部意外写成合法 JSON 数组 []、null 或 "string",read_json_file 原样返回非 dict 对象,随后 main.py:59 执行 state.get(...) 会直接在主 try 块外抛出 AttributeError 崩溃退出,导致正常的 API 采集也无法启动。
    2. record_filter 黑名单穿透:如 CLAUDE.md 所述,当前回退仅复检了 domain_filter(正则),未复检 record_filter(例如在 config.server.json 的 record_filter 中拉黑了 domain equals openai.com,当 API 返回空列表触发 fallback_last_good 时,若上次 last_good 是 openai.com,它依然会绕过 record_filter 复活)。
    3. Server 与 Local 默认共用 runtime/state.json:两份配置默认都指向 ./runtime/state.json。在同一台机器交替运行 Server 和 Local 模式时,后运行者的 write_json_file(state_file, new_state) 会全量覆盖 state.json,直接抹掉另一模式的 last_good_domain / last_good_ip。

🟠 5. 【P1 参数映射错误】build_router_package.sh SSOT 同步将“下载速度下限”误映射为“丢包率上限”

  • 位置:build_router_package.sh:172-173 vs scripts/router/router_local_update.sh:258-263
  • 现象与根因: 在 build_router_package.sh:172-173 中: ``python "-sl": "CFST_LOSS_LIMIT", "-speedlimit": "CFST_SPEED_LIMIT", 而在 router_local_update.sh:258-263(以及 cfst 官方参数定义)中,丢包率上限是 -tlr (CFST_LOSS_LIMIT),下载速度下限才是 -sl (CFST_SPEED_LIMIT)。 这意味着如果用户在 config.local.json 里配置了 -sl 5(速度下限 5MB/s),执行 --sync-from-config 打包时会将其错误同步为路由器的 CFST_LOSS_LIMIT="5"(丢包率上限),且漏掉了对 -tlr 的映射。 - 修复建议: 将 build_router_package.sh 中的映射修正为 "-tlr": "CFST_LOSS_LIMIT" 和 "-sl": "CFST_SPEED_LIMIT"。 ### 🟡 6. 【P2 契约与文档漂移】终审报告宣称已修复,但实际代码未落地的 2 处细节 1. 路由器 JSON 稀疏语义未统一(scripts/router/router_local_update.sh:123): - README.md §1.2 与终审报告宣称“仅在字段非空时输出真实键值,彻底消除空字符串/空数组/null 占位污染”,但 router_local_update.sh:123 的 awk 依然硬编码输出:"location_country":"","location_city":"","host_provider":"","score_value":null,"scores":[],"created_raw":""。 2. 终审文档对 router_local_http.sh (RT-03) 的修复描述与代码脱节: - ARCHITECTURE_REVIEW_UNIFIED.md §7.4 RT-03 和 §8.5 声称已增加 is_running() 检查 /proc/$pid/cmdline 包含 httpd。核查 scripts/router/router_local_http.sh 全文,实际采用的是前台运行 httpd -f + wait "$httpd_pid" 模式,并无 PID_FILE 或 /proc/$pid/cmdline 逻辑。虽然前台 wait 模式本身规避了 PID 文件复用误杀,但终审台账的描述与代码事实不符。 ### 🟡 7. 【P2 测试隔离缺陷】test_server_mode_adaptive_interval_window 存在真实网络请求泄漏 - 位置:tests/test_server_mode_lifecycle.py:165-199 - 现象与根因: 正如 .claude/skills/verify-rotator/SKILL.md 中记录的那样,该测试在“场景 A(第 184 行)”首次调用 run_update_and_commit.sh 时,尚未将 config.server.json 中的 api.url 替换为本地 file:// 模拟路径(替换操作写在了第 205 行的场景 B 里)。 如果运行测试时恰好处于所设定的时间窗口或整点边界(例如 curr_min == 0 或 curr_hour == 2),场景 A 不会被跳过,而是会直接向真实的 https://vps789.com/... 发起外部网络请求,破坏测试的离线隔离性。 - 修复建议: 在场景 A 执行前先完成本地模拟 file:// API 配置写入,并通过在测试环境 PATH 中注入 mock date 命令固定测试时钟,从而将此用例安全纳入常规回归测试。 ## 四、 问题汇总矩阵 (Summary Matrix) | 编号 | 严重度 | 模块 | 问题简述 | 当前状态 | | :------- | :---------- | :-------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | | **F-01** | **P0 / S0** |configs/config.server.json| 真实 API Token 明文硬编码入库,缺乏环境变量/文件注入机制 | ❌ 未修复 | | **F-02** | **P1** |substore/operator_template.js|default模式下路由器error_use_last_good因无top_candidates绕过测速门禁,压制健康 Server | ❌ 未修复 | | **F-03** | **P1** |scripts/server/run_update_and_commit.sh| 本地git commit成功但push失败后,若下一轮优选值未变,将触发跳过判断导致永久不重试推送 | ❌ 未修复 | | **F-04** | **P1** |scripts/rotator/main.py/storage.py| 1\.state.json若为非字典 JSON(如[]/null)将直接抛出AttributeError崩溃<br>2\.last_good回退未复检record_filter静态域名黑名单<br>3\. Server 与 Local 配置默认共用runtime/state.json互相覆盖 | ❌ 未修复 | | **F-05** | **P1** |build_router_package.sh|--sync-from-config将-sl(下载速度下限)误映射为CFST_LOSS_LIMIT(丢包上限),且漏映射-tlr| ❌ 未修复 | | **F-06** | **P2** |scripts/router/router_local_update.sh| 路由器生成的top_candidates仍包含硬编码空字符串/null/[],未对齐稀疏字段契约 | ⚠️ 契约漂移 | | **F-07** | **P2** |docs/reviews/ARCHITECTURE_REVIEW_UNIFIED.md| 终审报告RT-03声称router_local_http.sh检查/proc/$pid/cmdline,与实际前台wait代码不符 | ⚠️ 文档失真 | | **F-08** | **P2** |tests/test_server_mode_lifecycle.py|test_server_mode_adaptive_interval_window在配置本地模拟 API 前先执行了一次脚本,存在真实网络请求泄漏风险 | ⚠️ 测试未隔离 | ## 五、 核心修复方案与代码级建议 (Actionable Fixes) ### 5.1 修复 build_router_package.sh 参数映射错位(对应 F-05) 修改位置:build_router_package.sh:160-175 arg_map = { "-p": "CFST_DISPLAY_COUNT", "-f": "CFST_IP_FILE", "-n": "CFST_THREADS", "-t": "CFST_TEST_COUNT", "-dn": "CFST_DOWNLOAD_COUNT", "-dt": "CFST_DOWNLOAD_TIME", "-tp": "CFST_PORT", "-url": "CFST_URL", "-cfcolo": "CFST_CFCOLO", "-tl": "CFST_LATENCY_LIMIT", "-tll": "CFST_LATENCY_LOWER", "-tlr": "CFST_LOSS_LIMIT", # 修正:-tlr 对应丢包率上限 "-sl": "CFST_SPEED_LIMIT", # 修正:-sl 对应下载速度下限 } ### 5.2 修复 substore/operator_template.js 故障回退劫持(对应 F-02) 修改位置:substore/operator_template.js:223-239 (selectValueRouterFirst) if (!telemetry.router.ok && !telemetry.server.ok) { throw new Error(router failed: ${telemetry.router.error}; server failed: ${telemetry.server.error}`); } if (!telemetry.router.ok && telemetry.server.ok) { return { value: telemetry.server.value, chosenSource: "server", reason: "router_fail", telemetry }; } if (telemetry.router.ok && !telemetry.server.ok) { return { value: telemetry.router.value, chosenSource: "router", reason: "server_fail", telemetry }; } // 新增:当路由器处于故障回退状态(非 ok)而 Server 健康时,优先降级至健康的 Server if (telemetry.router.status !== "ok" && telemetry.server.status === "ok") { return { value: telemetry.server.value, chosenSource: "server", reason: "router_degraded_fallback", telemetry }; } if (telemetry.router.speedParse === "ok" && telemetry.router.speedMBps < ROUTER_MIN_SPEED_MB_PER_S) { return { value: telemetry.server.value, chosenSource: "server", reason: "router_low_speed", telemetry }; } return { value: telemetry.router.value, chosenSource: "router", reason: "router_preferred", telemetry }; ### 5.3 修复 run_update_and_commit.sh 未推送提交永久跳过问题(对应 F-03) 修改位置:scripts/server/run_update_and_commit.sh:313-318 在判定“值未变化”准备跳过前,检查本地分支是否还有未推送到远端的 commit: has_unpushed="0" if [[ "$push_enabled" == "1" ]] && [[ -n "$push_remote" ]]; then if git -C "$work_dir" rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then local_head="$(git -C "$work_dir" rev-parse HEAD 2>/dev/null || echo "")" remote_head="$(git -C "$work_dir" rev-parse "@{u}" 2>/dev/null || echo "")" if [[ -n "$local_head" ]] && [[ "$local_head" != "$remote_head" ]]; then has_unpushed="1" fi else has_unpushed="1" fi fi if [[ "$force_commit" != "1" ]] && [[ -n "$before" ]] && [[ "$after" == "$before" ]]; then if [[ "$status_unchanged" == "1" || "$is_fallback" == "1" ]]; then if [[ "$has_unpushed" == "1" ]]; then log "[vmess-domain-rotator] value unchanged (${after}), but local branch has unpushed commits; retrying push" git_auth "$work_dir" push -u "$push_remote" "$runtime_branch:$runtime_branch" || { log "[vmess-domain-rotator] retry git push failed" [[ "$push_required" == "1" ]] && exit 1 } else log "[vmess-domain-rotator] selected value unchanged (${after}) [status=${updater_status:-unknown}], skip git commit and push" fi exit 0 fi fi ### 5.4 加固 state.json 类型安全与回退黑名单检查(对应 F-04) 修改位置:scripts/rotator/main.py:59 state = read_json_file(state_file, default={}) if not isinstance(state, dict): state = {} last_good = str(state.get(state_last_good_key, "") or "").strip() 同时提取一个通用的 is_domain_blocked(domain, cfg) 辅助函数,在 last_good 回退时不仅调用 apply_filter(),还对 record_filter 中针对 field == "domain" 的规则执行复检,防止被拉黑的域名通过 last_good 复活。 ### 5.5 修复 test_server_mode_adaptive_interval_window 离线隔离(对应 F-08) 修改位置:tests/test_server_mode_lifecycle.py:165-215 将 payload_file = self._write_mock_api_payload(...) 与改写 cfg["api"]["url"] = f"file://{payload_file}" 的代码上移至场景 A 执行之前,并通过在 env 的 PATH 最前面注入一个临时 mock date 脚本来固定测试时钟(例如场景 A 固定返回 04 时 15 分),使该测试 100% 确定性通过且绝不触发外网请求。 ### 六、 建议推进路线 (Next Steps) 1. 第一优先级(立竿见影的正确性与测试闭环): - 修复 build_router_package.sh 的 -sl / -tlr 参数映射错位并补充对应单元测试断言; - 修复 tests/test_server_mode_lifecycle.py 的模拟 API 注入顺序,解除 .claude/skills/verify-rotator/SKILL.md 中对该测试的默认排除限制; - 修复 scripts/rotator/main.py 对非字典 state.json 的防御性容错。 2. 第二优先级(端到端容灾闭环): - 修复 substore/operator_template.js 对 error_use_last_good 状态的降级判定; - 修复 scripts/server/run_update_and_commit.sh 的未推送 commit 自动重试机制; - 清理 router_local_update.sh 中 top_candidates 的冗余空字段,对齐稀疏 JSON 契约。 3. 第三优先级(凭据安全与文档对齐): - 支持通过环境变量或外部文件注入 API Token,轮换并移除 configs/config.server.json 中的明文 Token; - 修正 ARCHITECTURE_REVIEW_UNIFIED.md 中的少量失真描述,保持文档与代码 100% 同步。