# vmess-domain-rotator 全仓库架构与业务代码终审整合报告 > **文档定位**:本报告对 `vmess-domain-rotator` 全仓库业务流程、业务代码与总体架构进行了独立深度审查,并将本地既有的两份审查报告——[`ARCHITECTURE_REVIEW.md`](./ARCHITECTURE_REVIEW.md)(下称**「初审报告」**)与 [`ARCHITECTURE_REVIEW_RECHECK.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` --- ## 目录 1. [全景业务架构与数据流审视](#1-全景业务架构与数据流审视) 2. [本次审查新增发现(两份既有报告均漏检的「查漏补缺」)](#2-本次审查新增发现两份既有报告均漏检的查漏补缺) 3. [两份既有审查报告的分歧对比与客观裁定](#3-两份既有审查报告的分歧对比与客观裁定) 4. [三方共识问题汇总(重合部分深度补充)](#4-三方共识问题汇总重合部分深度补充) 5. [总体架构与业务流程重构建议(支持大刀阔斧演进)](#5-总体架构与业务流程重构建议支持大刀阔斧演进) 6. [分阶段落地路线图(Prioritized Roadmap)](#6-分阶段落地路线图prioritized-roadmap) --- ## 1. 全景业务架构与数据流审视 本仓库核心目标是**自动优选 Cloudflare 优选域名或 IP,并通过本地 HTTP 或 Git 分支发布给 Sub-Store 订阅脚本消费**。目前仓库内并行维护了三条生产管线与一个消费端: ```mermaid flowchart LR subgraph 生产端管线 A1["管线 1: Server API 模式
(config.server.json)"] --> B1["domain_updater.py
拉取 vps789 API & 加权评分"] B1 --> C1["runtime/current_domain.txt
runtime/current_domain.json
runtime/state.json"] C1 --> D1["run_update_and_commit.sh
(孤儿分支 worktree 提交推送)"] D1 --> E1["GitHub runtime-state 分支
(raw.githubusercontent.com)"] A2["管线 2: Local Python 模式
(config.local.json)"] --> B2["domain_updater.py
调用 cfst 测速 & 解析 CSV"] B2 --> C2["runtime/current_ip.txt
runtime/current_ip.json
runtime/state_local.json"] A3["管线 3: 路由器 BusyBox 模式
(config_router.conf)"] --> B3["router_local_update.sh
调用 cfst 测速 & sed/awk 解析"] B3 --> C3["runtime_router/current_ip.*
router_local_http.sh (httpd)"] end subgraph 消费端管线 E1 --> F["substore/operator_template.js"] C2 -.-> F C3 --> F F --> G["覆写代理节点 server 字段
(支持 local_first / server_only / local_only)"] end ``` ### 架构层面的核心症结 1. **三套实现各自为政,能力与契约割裂**: - `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` 不一致。 2. **控制流与数据流混用标准输出(stdout)**: - `domain_updater.py` 既把 `stdout` 当作传给 `run_update_and_commit.sh` 的机器可读 IPC 通道(期望最后一行是 JSON),又允许 `cfst` 子进程和 `notify` 子进程直接向同一个 `stdout` 刷日志。 3. **以 Git 仓库作为高频状态发布总线的副作用未闭环**: - 每 30 分钟运行一次,一旦遇到上游 API 偶发抖动,状态在 `ok` 与 `fallback_last_good` / `error_use_last_good` 之间来回翻转,导致无效 Git commit 激增;同时消费端 `operator_template.js` 完全忽略 JSON 中的 `status` 与 `updated_at_utc` 字段,无法感知上游是否已处于长期故障或陈旧状态。 --- ## 2. 本次审查新增发现(两份既有报告均漏检的「查漏补缺」) 本节保留 **8 项审查记录**。其中 §2.1 已于 **2026-10-18** 更正为上游字段说明与自定义评分策略记录;其余条目保留既有审查结论。 --- ### 2.1 【文档已统一】上游 `yd_score` 字段与本项目自定义评分策略 - **位置**: - [`configs/config.server.json`](../../configs/config.server.json):字段映射、加权字段与平局决胜配置 - [`scripts/rotator/scoring.py`](../../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"`(数值较小者优先)。这描述的是当前策略,不是对所有数据源的固定规则。 - **审查结论更正(2026-10-18)**:撤回此前根据样本推断上游评分公式、将旧加权配置定性为 P0 业务错误,以及据此推荐 `yd_score` 降序的结论。将该字段移出加权、改为升序平局决胜属于评分策略调整;更换数据源或调整策略时,应核对上游定义并同步更新配置、测试和文档。 - **测试边界**:`test_p0_yd_score_polarity_fix_and_healthy_ranking` 与 `test_empirical_score_metric_asc_tie_breaking_and_consistency` 是现有测试名称,验证当前配置下的排序行为,不能据此证明上游实际计算公式。 --- ### 2.2 【P1 业务语义误读】【✅ 已澄清与纠偏】vps789 API 的 `createdTime` 是「监控建档时间」而非「测速时间」,盲目启用 `within_hours` 会团灭所有候选 - **位置**: - `config.server.json:41-47`、`config.server.json:63` - `scripts/domain_updater.py:669-678` - `README.md:227` - 初审报告 §2.2 / 复核报告 §3.2 - **现象与证据**: 1. `README.md:227` 将 `within_hours` 描述为「仅保留最近 N 小时的数据」,初审与复核报告均据此建议开启 `within_hours: 24` 以过滤陈旧测速记录。 2. 但核查 `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` 每半小时都在动态变化。 3. 这证明上游 `vps789` 接口返回的 `createdTime` 是**该优选域名被纳入监控池的建档时间**,根本不是当次拨测时间戳!如果采纳旧报告建议设置 `within_hours: 24`,除新入库不满 24 小时的域名外,所有主力稳定域名都会被全部过滤掉,直接导致选优池归零。 - **修复方案与落地状态**: - **✅ 已落地纠偏**:已在 `README.md` 与 `config.server.json` 中修正描述,澄清其为建档时间,并在生产配置中安全保持 `within_hours: 0`(禁用状态过滤),防止误杀老牌稳定域名。 --- ### 2.3 【P1 状态与发布脑裂】【✅ 已修复】未文档化的第二种回退状态 `fallback_last_good` 线上已真实触发并造成无效重复提交 - **位置**: - `scripts/domain_updater.py:1071-1102` vs `1125-1149` - `scripts/run_update_and_commit.sh:164-167`、`334-340` - **现象与证据**: 1. `domain_updater.py` 内部实际存在**两条完全不同的 `last_good` 回退路径**: - **路径 A(异常回退,L1125-1149)**:当网络超时、HTTP 非 200 或解析抛异常时,设置 `status = "error_use_last_good"`,将 `state.json` 中的 `status` 写为 `"error"`,且向 `stdout` 打印的 JSON 为 `{"status": "error_use_last_good", "error": "..."}`(**漏掉了选中的域名字段 `domain`**)。 - **路径 B(空结果回退,L1071-1102)**:当上游 API 返回 HTTP 200,但 `data` 为空数组 `[]` 或所有记录均被过滤掉时,不会抛异常,而是进入 `if best is None:` 分支,将 `status` 设为 `"fallback_last_good"`,并将 `state.json` 覆写为 `"status": "fallback_last_good"`、`"source_count": 0`、`"top_candidates": []`。 2. 然而,`run_update_and_commit.sh` 的状态处理逻辑(L164 与 L334)**只认识 `error_use_last_good`,完全不知道 `fallback_last_good` 的存在**。 3. **线上真实故障证据**:在远端 `runtime-state` 分支的提交 `a7c6fdc`(`2026-10-11T00:03:45Z`)中,上游 API 恰好返回了 `source_count: 0`,触发了 `fallback_last_good`。由于脚本未识别该状态,产生了大量无效翻转空提交。 - **修复方案与落地状态**: - **✅ 已落地修复**: 1. `domain_updater.py` 异常回退输出的 JSON 中强制包含 `selected_json_key`(如 `domain`); 2. `run_update_and_commit.sh` 同时识别 `error_use_last_good` 与 `fallback_last_good`,并在回退所选值未变时执行「回退静默」,跳过 commit/push; 3. `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% 验证通过。 --- ### 2.4 【P1 安全隐患】【✅ 已修复】Git HTTP 认证凭据通过命令行参数 `-c` 明文传递,同机 `ps` 可直接截获 - **位置**:`scripts/run_update_and_commit.sh:91-101` - **现象与证据**: ```bash 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//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% 验证通过。 --- ### 2.5 【P2 配置校验缺失】【✅ 已修复】`validate_config()` 漏检 `api`、`parser`、`domain_filter`、`selection` 顶层存在性 - **位置**:`scripts/domain_updater.py:375-525` - **现象与证据**: `validate_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% 验证通过。 --- ### 2.6 【P2 告警盲区】【✅ 已修复】彻底失败(无 `last_good` 可回退)时反而跳过 `run_notify()` - **位置**:`scripts/domain_updater.py:1125-1149` - **现象与证据**: 在 `main()` 的 `except Exception as exc:` 块中,当 `last_good` 为空且发生致命异常时,直接 `return 1`,漏掉了 `run_notify()` 报警通知。 - **修复方案与落地状态**: - **✅ 已落地修复**:在致命异常且无 `last_good` 的分支中补充调用 `run_notify(notify_cfg.get("command", ""), "", "error")`,确保致命故障必定触达通知通道。 --- ### 2.7 【P2 本地 `cfst` 模式】【✅ 已修复】未过滤 `download_speed == 0.00` 的测速失败节点,已将零速 IP 选为最优 - **位置**: - `config.local.json:5-19` - `scripts/domain_updater.py:926-972`(`parse_cfst_csv_rows` / `select_best_cfst_record`) - `runtime/current_ip.json:1-24` - **现象与证据**: `cfst` 测速中若节点因被墙或下载失败测得速度为 `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% 验证通过。 --- ### 2.8 【P2 跨平台打包污染】【✅ 已修复】`build_router_package.sh` 在 macOS 下混入 `.DS_Store` / AppleDouble 扩展属性且跨架构检测失真 - **位置**:`build_router_package.sh:67-88`、`134` - **现象与证据**: macOS 下直接打包会生成 `._*` 扩展属性文件与 `.DS_Store`,且在宿主机执行 Linux 架构 BusyBox 二进制时误报无 `httpd`。 - **修复方案与落地状态**: - **✅ 已落地修复**:在 `build_router_package.sh` 中设置 `COPYFILE_DISABLE=1` 并添加 `--exclude='.DS_Store' --exclude='._*'`;对跨平台 BusyBox 改用 `strings` 静态检查 `httpd` 符号。 - **测试验证**:打包语法与 trap 验证通过。 --- ## 3. 两份既有审查报告的分歧对比与客观裁定 本地现有的两份报告——[`ARCHITECTURE_REVIEW.md`](./ARCHITECTURE_REVIEW.md)(初审)与 [`ARCHITECTURE_REVIEW_RECHECK.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 节汇总)。 | --- ## 4. 三方共识问题汇总(重合部分深度补充) 将初审报告、复核报告与本次审查确认的共识问题按模块整合如下(标注来源与代码精确位置): ### 4.1 安全与高危破坏性风险(P0) 1. **`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` 目标路径做前缀白名单断言。 2. **`build_router_package.sh` `trap` 注册顺序错误导致 `TMP_DIR` 误删风险**(复核 R-01 + 本次确认) - **位置**:`build_router_package.sh:54-64` - **根因**:`trap cleanup EXIT INT TERM` 注册在 `--tmp-dir` 参数校验之前;若传入 `--tmp-dir /` 或 `--tmp-dir ""`,脚本因校验报错退出时立即触发 `cleanup()` 执行 `rm -rf "$TMP_DIR"`。 - **修复**:仅在所有参数校验通过且创建好专用子目录(如 `mktemp -d`)后才注册 `trap`。 3. **`domain_updater.py` 通知命令注入漏洞(RCE)**(三方共识) - **位置**:`scripts/domain_updater.py:988-1000` - **根因**:直接用 `.format()` 将外部 API 返回的 `{domain}` / `{error}` 拼入命令字符串,并传给 `subprocess.run(..., shell=True)`。 - **修复**:废弃模板字符串直接拼 Shell 的做法,改为通过环境变量(`UPDATER_STATUS`、`UPDATER_VALUE`、`UPDATER_REASON`)传递给通知脚本,或使用参数数组 `shell=False` + `timeout=15` + `stdout=sys.stderr`。 ### 4.2 核心选优与回退逻辑缺陷(P1) 4. **`last_good` 回退值绕过当前过滤规则与黑名单**(复核 P-01 + 本次确认) - **位置**:`scripts/domain_updater.py:1010-1020`、`1071-1149` - **根因**:管理员在 `exclude_domains` 或 `record_filter` 中拉黑某个已失效域名后,一旦当次运行遇到网络异常或空候选,脚本从旧状态文件中读出刚被拉黑的 `last_good` 继续发布。 - **修复**:读取 `last_good` 后必须先通过当前的 `is_valid_candidate_value()` 与 `domain_filter` 校验,不合法则丢弃。 5. **`run_update_and_commit.sh` 将 `git ls-remote` 网络报错误判为「远端分支不存在」,导致重建孤儿分支**(复核 S-02 + 本次确认) - **位置**:`scripts/run_update_and_commit.sh:179-195` - **根因**:`git 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` 时直接报错退出。 6. **标准输出(stdout)协议脆弱与回退字段缺失**(三方共识) - **位置**:`scripts/domain_updater.py:755`、`999`、`1102`、`1142`;`scripts/run_update_and_commit.sh:151-162` - **根因**:`cfst` 子进程与 `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。 7. **非原子文件写入导致并发读取半截文件或断电损坏**(三方共识) - **位置**:`scripts/domain_updater.py:1023-1069`;`scripts/router_local_update.sh:283-320` - **根因**:直接 `write_text()` 或 `>` 覆写目标文件。路由器端 `httpd` 并发读取时可能读到截断的空文件或半截 JSON。 - **修复**:统一使用同目录临时文件写入 + `mv -f` / `os.replace()` 原子替换。 ### 4.3 路由器脚本与 Sub-Store 消费端问题(P1/P2) 8. **`substore/operator_template.js` 非字符串字段强转 `"[object Object]"` 污染节点配置**(复核 §4.2 + 本次确认) - **位置**:`substore/operator_template.js:168-176` - **根因**:`extractValue()` 在 JSON 模式下对 `parsed[key]` 直接执行 `String(parsed[key])`。若配置指向了对象或数组字段,会返回 `" [object Object] "` 并通过非空校验,把所有 VMess 节点的 `server` 改写为 `"[object Object]"`。 - **修复**:在 `extractValue()` 中限定 `typeof candidate === 'string'`,并对结果做域名/IP 合法性校验(拒绝空白字符、控制字符、`[object` 等)。 9. **`substore/operator_template.js` 完全忽略数据源 `status` 与新鲜度(Staleness)**(三方共识 + 复核 §4.1) - **位置**:`substore/operator_template.js:184-269` - **根因**:无论是 `local_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`; - 服务端(Git 分支):可选支持低频心跳(如超过 24 小时未变更则更新一次 `last_verified_at_utc`)或由消费端校验 `status` 字段。 10. **路由器端 `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-123` - **根因**: - `router_local_update.sh` 使用 `printf` 直接将 `ERROR_REASON` 等变量拼入 JSON,若含双引号或换行符即生成非法 JSON;且缺失 `config.local.json` 支持的 `-dt`、`-dn`、`-dd` 参数。 - `router_local_http.sh` 仅用 `kill -0 "$pid"` 判断进程存活,路由器重启后 PID 极易被系统其他守护进程复用,`stop` 时会误杀无辜进程。 - **修复**:在 Shell 中增加 `json_escape()` 函数转义 `\`、`"` 与控制字符;`is_running()` 增加检查 `/proc/$pid/cmdline` 是否包含 `httpd`。 --- ## 5. 总体架构与业务流程重构建议(支持大刀阔斧演进) 针对您在需求中提到的「**即使巨幅改动也可以,只要能够优化业务流程**」,结合上述全部问题,提出以下两层重构方案(**演进式重构** vs **彻底精简架构**): ### 5.1 方案 A:在不改变外部部署形态下的「内核契约化重构」(推荐优先落地) ```mermaid flowchart TD subgraph 统一输入适配器 S1["API Source Adapter
(fetch_api_json + field_map)"] S2["CFST Source Adapter
(run_cfst -> stderr + parse_csv)"] end subgraph 统一核心管线 S1 --> V["1. 候选准入校验层
• 合法域名/IP 格式校验
• 过滤规则 & 黑名单 (含 last_good 复检)
• CFST 零下载速度过滤 (download_speed > 0)"] S2 --> V V --> M["2. 配置驱动评分排序层
• 按上游字段定义配置 prefer_lower / order
• 支持可选切换迟滞阈值 (switch_margin_pct)"] M --> F["3. 统一状态机与回退层
• 状态枚举: ok | fallback_empty | fallback_error
• 回退时保留上次有效 top_candidates"] end subgraph 原子发布与消费 F --> O["4. 原子落盘层 (tmp + os.replace)
• current_*.txt / .json / state.json
• 独立 IPC 结果文件 (--result-json-file)"] O --> P["5. 安全 Git 发布层
• GIT_CONFIG_COUNT 环境变量传 Token
• 区分 ls-remote 退出码 2 vs 128
• 忽略瞬时回退导致的无效翻转 commit"] P --> C["6. 具备时效与类型校验的 Sub-Store 算子
• 严格校验 string 类型与域名/IP 正则
• local_max_age_sec 过期自动降级"] end ``` ### 5.2 方案 B:大刀阔斧的业务流程瘦身建议(消除 Git 分支轮询与多语言重复维护) 如果允许调整部署方式,当前架构中有两个最大的历史包袱可以彻底简化: 1. **用统一静态契约生成工具替代手写 JSON 模板**: - 将 `domain_updater.py` 拆分为结构清晰的内部子模块(或单文件内清晰的分层 DataClass:`Candidate` -> `QualificationPolicy` -> `ScoringPolicy` -> `AtomicStateWriter`),并为 `config_router.conf` 提供一键生成/校验脚本,保证 Python 本地模式与路由器 Shell 模式的 `cfst` 参数和输出 JSON 100% 同构。 2. **优化远端状态发布机制(解决 Git commit 膨胀与状态翻转噪音)**: - **现状痛点**:把 Git 分支 `runtime-state` 当作实时 KV 数据库使用,每次瞬时网络抖动(`ok -> fallback_last_good -> ok`)都会产生 2 个毫无业务价值的 Git commit,且仓库历史无限膨胀。 - **改造方案**: - **规则 1(回退静默)**:当 `updater_status` 为 `fallback_last_good` 或 `error_use_last_good` 且回退选中的域名与远端当前已发布的域名相同时,`run_update_and_commit.sh` **仅在本地记录告警日志,不向远端 `runtime-state` 分支提交和推送**!这样远端分支永远只保留有效的优选变更,彻底消灭 10 月初那种因瞬时抖动产生的无意义翻转 commit。 - **规则 2(孤儿分支定期压缩)**:在 `run_update_and_commit.sh` 中支持可选的 `MAX_STATE_COMMITS`(或保持单提交强推 `git commit --amend` / 浅历史),避免 `runtime-state` 累积数千个历史 commit 拖慢克隆与拉取。 --- ## 6. 分阶段落地路线图(Prioritized Roadmap) | 阶段 | 优先级 | 进度状态 | 改动目标 | 涉及文件 | 验收与测试验证 | | :--- | :--- | :--- | :--- | :--- | :--- | | **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.json`
`scripts/domain_updater.py`
`scripts/uninstall_debian.sh`
`build_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` 处理,回退且值未变时执行回退静默,不向远端推送噪音 commit
2. 对 `last_good` 强制执行当前 `domain_filter` / 格式复检;彻底失败时补发 `run_notify`
3. `run_update_and_commit.sh` 区分 `ls-remote` 退出码 `2` 与 `128`,改用 `GIT_CONFIG_*` 环境变量安全传递 Git Token
4. `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.py`
`scripts/run_update_and_commit.sh`
`scripts/router_local_update.sh`
`substore/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.sh`
`scripts/router_local_update.sh`
`build_router_package.sh`
`README.md`
`workflow.md`
`CLAUDE.md`
`tests/` | 测试通过:`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 全绿 | --- ## 7. 全仓逐文件审查台账与三方验证矩阵(100% 全量对齐) 本节将全仓库所有代码文件、配置文件与文档中的所有问题逐一列出,并标注其在 **初审(`ARCHITECTURE_REVIEW.md`)**、**复核(`ARCHITECTURE_REVIEW_RECHECK.md`)** 与 **本次终审(含源码及线上分支实证)** 中的状态与核验结论,确保无一遗漏。 ### 7.1 `scripts/domain_updater.py`(1154 行)与 `config.server.json` / `config.local.json` | 编号 | 问题摘要 | 代码行号 | 初审报告 | 复核报告 | 本次终审实证与验证结论 | 严重度 | 修复状态 | | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | | **PY-01** | **上游评分字段与自定义策略说明**:`yd_score` 映射上游 `ydScore`;是否参与加权及排序方向由配置决定 | `configs/config.server.json`
`scripts/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-972`
`runtime/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, 63`
`domain_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` | --- ### 7.2 `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//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` 通过 | --- ### 7.3 `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** | **✅ 已修复**
单引号包裹转义且服务名受安全正则限制 | --- ### 7.4 `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-265`
`config_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 | --- ### 7.5 `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)` 会把 `` 或报错文本直接当作域名返回 | **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` 异步/同步处理 | --- ## 8. 核心改造完整代码级方案(Copy-Paste Ready 蓝图) 本节针对上述 P0/P1/P2 问题提供可直接落地的代码级修改参考。 ### 8.1 配置驱动的评分与迟滞策略(对应 PY-01, PY-12) #### 1) 当前 `configs/config.server.json` 配置示例 评分设置位于 `scoring`,`selection` 仅设置候选数量。下例与当前配置一致;使用其他数据源时,应先核对上游字段定义,再决定是否使用 `yd_score` 及其排序方向。 ```json { "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} } ``` #### 2) 当前实现入口与约定 - [`parse_scored_records()` / `rank_scored_records()`](../../scripts/rotator/scoring.py) 按配置计算本项目的 `score_value`,再按全局方向与 `tie_breakers` 排序。字段级方向与全局方向不一致时,先调整该字段值的符号再加权;上游 `yd_score` 的值按映射读取。 - [`choose_domain()`](../../scripts/rotator/scoring.py) 使用传入的 `switch_margin_pct` 执行可选迟滞策略;[`scripts/rotator/main.py`](../../scripts/rotator/main.py) 从 `scoring` 读取该设置并传入。 - 加权字段的取舍、权重、方向与迟滞阈值共同构成自定义策略。当前 `yd_score` 升序平局决胜不表示本项目定义了上游的评分公式,相关测试也仅验证配置行为。 --- ### 8.2 修复 `domain_updater.py` 原子写文件、安全通知、`last_good` 复检与零速 IP 过滤(对应 PY-02 ~ PY-09) ```python 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` 时): ```python 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 ``` --- ### 8.3 修复 `scripts/run_update_and_commit.sh`(对应 SH-01 ~ SH-04) 1. **用 `GIT_CONFIG_COUNT` 替代命令行 `-c http.extraHeader`,防止 `ps aux` 泄露 Token**: ```bash 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 } ``` 2. **精确区分 `git ls-remote --exit-code` 的退出码 `0`、`2` 与其他错误(`128`)**: ```bash 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 ``` 3. **统一处理 `fallback_last_good` 与 `error_use_last_good`,抑制瞬时回退导致的无意义 Git Commit**: ```bash 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 ``` --- ### 8.4 修复 `scripts/uninstall_debian.sh` 与 `build_router_package.sh` 高危删库漏洞(对应 OP-01, RT-01, RT-05) 1. **`scripts/uninstall_debian.sh` 强制校验 `SERVICE_NAME`**: ```bash 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 ``` 2. **`build_router_package.sh` 延后 `trap` 注册并清理 macOS 打包污染**: ```bash # 在完成所有参数非空校验及禁止 "/" 校验之后,再创建目录并注册 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" ``` --- ### 8.5 修复 `scripts/router_local_update.sh` 与 `scripts/router_local_http.sh`(对应 RT-02 ~ RT-04) 1. **`router_local_update.sh` 增加 POSIX `json_escape` 与同目录临时文件原子替换**: ```sh 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" } ``` 2. **`router_local_http.sh` 校验 `/proc/$pid/cmdline` 防止 PID 复用误杀**: ```sh 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 } ``` --- ### 8.6 修复 `substore/operator_template.js` 类型安全、格式校验与本地过期降级(对应 JS-01 ~ JS-04) ```javascript 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 : ''; } ``` --- ## 9. 自动化测试体系设计与落地验证(`tests/` 已全部建设并全绿通过 ✅) 遵循 `CLAUDE.md` 的「先建测试再动生产逻辑」原则,自动化测试体系已在 `tests/` 目录全面落地,**18 项端到端与契约测试在 0.38 秒内 100% 全绿通过**(基于 Python 标准库 `unittest` + Node.js 原生 `vm`,零第三方外部依赖)。 ### 9.1 测试套件架构与实测表现 | 测试文件 | 覆盖范围与断言目标 | 用例数 | 执行耗时 | 测试状态 | | :--- | :--- | :--- | :--- | :--- | | [`tests/test_domain_updater.py`](./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`](./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`](./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`](./tests/test_substore_operator.py) | • `extractValue()` 严格类型守卫(拒绝非字符串对象强转为 `[object Object]`)
• 域名 (RFC 1123) 与 IPv4/IPv6 正则校验(拒绝 HTML 报错与非法字符)
• `LOCAL_MAX_AGE_SEC` (24h) 本地路由器源超时自动降级 | 3 项 | ~0.06s | **✅ 全部通过 (PASS)** | ### 9.2 运行命令 ```bash python3 -m unittest discover tests -v # 输出:Ran 22 tests in 1.419s, OK ``` --- ## 10. 方案 B(大刀阔斧的业务流程瘦身与架构演进)完整落地报告 ✅ 经与用户对齐并选定「模式 A(滚动保留最近 50 次提交,兼顾轻量与防误封)」,方案 B 现已 100% 落地并完成全量验证: ### 10.1 模式 A:Git 孤儿分支滚动修剪与优雅降级机制 - **问题根源**:原调度脚本每次优选都无条件向 `runtime-state` 分支追加 commit,经过数月运行会堆积上万个 commit,导致远程仓库体积虚高、拉取缓慢。 - **重构落地**: - 在 [`scripts/server/run_update_and_commit.sh`](./scripts/server/run_update_and_commit.sh) 引入 `GIT_MAX_STATE_COMMITS`(默认 50 次提交,可由用户自定义)。 - 当历史 commit 数超出阈值时,自动提取保留基线 tree,创建 baseline root commit 并执行 `rebase --onto`,精简历史。 - **自适应容错**:优先使用 `--force-with-lease` 推送;若远端存在分支保护策略拒绝 force-push,脚本自动捕获并从 untrimmed 备份分支无损回滚,平滑降级为普通追加推送,绝不中断服务。 - **跨平台稳健性**:全面消除 `mapfile`(避免 macOS / 旧系统 Bash 3.2 不支持)与 `set -u` 展开空数组崩溃隐患,兼容任意 Linux/Debian/macOS 环境。 ### 10.2 Step 2:路由器配置单源驱动(SSOT 联动) - **问题根源**:原路由器配置 `config_router.conf` 与服务端/本地配置 `config.local.json` 割裂,若用户调整了测速线程数、测试时间、测速端口或输出文件名,需双重维护。 - **重构落地**: - 在 [`build_router_package.sh`](./build_router_package.sh) 中新增 `--sync-from-config` 命令行参数支持。 - 打包前自动解析 JSON 配置,将 `TOP_N`, `VALUE_TEXT_FILE`, `CFST_DISPLAY_COUNT`, `CFST_IP_FILE`, `CFST_PORT`, `CFST_DISABLE_DOWNLOAD` 等参数动态投影同步至 tarball 内的 `config_router.conf`。 - 彻底杜绝双源维护漂移。 ### 10.3 Step 3:`domain_updater.py` 领域模块化解耦 - **问题根源**:原 `domain_updater.py` 膨胀为 1246 行单文件单片脚本,混合了 CLI、HTTP、CSV 解析、评分策略、原子 IO、系统通知等多种无关职责。 - **重构落地**: - 新建高内聚子包 [`scripts/rotator/`](./scripts/rotator/): - `storage.py`:原子落盘写入与状态读写 - `config.py`:配置严格校验、路径解析与输出映射 - `filter.py`:域名与 IP 验证、正则清洗与黑名单排除 - `sources.py`:远程 API 抓取与本地 CFST 结果解析 - `scoring.py`:自定义加权评分、字典序排序、迟滞防抖与候选列表构造 - `notify.py`:通知命令执行与超时防护 - `main.py`:主轮换工作流编排与 CLI 参数解析 - [`scripts/domain_updater.py`](./scripts/domain_updater.py) 重构为精简的外观门面(Facade),全面重新导出所有符号,保持 100% 向后兼容,现存 systemd、cron 及自动化测试零修改直接运行。 ### 10.4 脚本目录按平台拆分与配置统一收纳(`configs/`) - **问题根源**: 1. `scripts/` 目录平铺堆砌了服务端 Debian 安装/卸载脚本、路由器嵌入式更新/HTTP 脚本、Python 脚本等,职责界限模糊。 2. 根目录下散落了多个配置文件(`config.server.json`, `config.local.json`, `config_router.conf`),容易杂乱。 - **重构落地**: - **统一配置仓**:建立 [`configs/`](./configs/) 目录,将服务端、本地与路由器配置文件集中收纳;根目录彻底清爽无散落文件,项目内所有命令默认引用统一更新为 `configs/`,并在 Python 与 Shell 脚本内建自适应双向探测逻辑(优先探测传入路径,无路径时依次探测 `configs/` 目录)。 - **物理分层与直接引用(无冗余包装脚本)**: - 创建 [`scripts/server/`](./scripts/server/):收纳 Linux / Debian 服务端运维脚本(`run_update_and_commit.sh`, `install_debian.sh`, `uninstall_debian.sh`)。 - 创建 [`scripts/router/`](./scripts/router/):收纳路由器 BusyBox 嵌入式运行时脚本(`router_local_update.sh`, `router_local_http.sh`)。 - `scripts/` 顶层不再保留任何中间转发包装脚本(thin forwarder wrappers),所有文档、systemd 单元模板及打包工具均直接统一指向新结构路径,架构极致纯粹。