|
|
@@ -0,0 +1,768 @@
|
|
|
+# 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 模式<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
|
|
|
+```
|
|
|
+
|
|
|
+### 架构层面的核心症结
|
|
|
+
|
|
|
+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 个问题**在初审报告(`ARCHITECTURE_REVIEW.md`)和复核报告(`ARCHITECTURE_REVIEW_RECHECK.md`)中**均未被发现**,其中包含基于 `runtime-state` 线上真实运行数据证实的 **P0 级选优方向反转 Bug**。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 2.1 【P0 严重业务反转】【✅ 已修复】`config.server.json` 中 `yd_score` 极性反转,导致算法优先挑选劣化节点
|
|
|
+
|
|
|
+- **位置**:
|
|
|
+ - `config.server.json:68-83`
|
|
|
+ - `scripts/domain_updater.py:482-490`(`validate_config`)
|
|
|
+ - `scripts/domain_updater.py:708-735`(`compute_weighted_average`)
|
|
|
+ - `README.md:221-225`
|
|
|
+- **现象与证据**:
|
|
|
+ `config.server.json` 的选优配置如下:
|
|
|
+ ```json
|
|
|
+ "selection": {
|
|
|
+ "strategy": "weighted_average",
|
|
|
+ "prefer_lower": true,
|
|
|
+ "weights": {
|
|
|
+ "yd_latency": 1.0,
|
|
|
+ "yd_pkg_lost_rate": 10.0,
|
|
|
+ "yd_score": 5.0
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ```
|
|
|
+ 1. 在 `vps789.com` 接口中,`yd_latency`(移动延迟,越小越好)和 `yd_pkg_lost_rate`(移动丢包率,越小越好)是**越小越优(负向指标)**;而 `yd_score`(对应接口 `ydScore`)是**综合健康度得分,越大越优(正向指标,通常在 75~110 之间)**。
|
|
|
+ 2. 核查远端 `vmess-domain-rotator/runtime-state` 分支的真实生产提交记录可铁证这一点:
|
|
|
+ - 在提交 `4fd8a65`(`2026-06-05T11:32:56Z`)中:
|
|
|
+ - `icook.tw`:`yd_latency = 49`, `yd_pkg_lost_rate = 1.9`, `yd_score = 92` -> 加权分 `(49*1 + 1.9*10 + 92*5)/16 = 33.0`(第 1名)
|
|
|
+ - `www.fastsoso.cn`:`yd_latency = 36`, `yd_pkg_lost_rate = 0`, `yd_score = 109` -> 加权分 `(36*1 + 0*10 + 109*5)/16 = 36.3125`(因 `109` 分更高,在 `prefer_lower: true` 下反而排到第 3 名!)
|
|
|
+ - 30 分钟后,在提交 `1a38eea`(`2026-06-05T12:08:25Z`)中:
|
|
|
+ - `www.fastsoso.cn` 的移动延迟从 `36ms` **恶化翻倍**到 `74ms`,上游接口将其健康分 `yd_score` 从 `109` **下调惩罚**至 `78`。
|
|
|
+ - 然而因为 `prefer_lower: true`,其加权分变为 `(74*1 + 0*10 + 78*5)/16 = 29.0`,**节点延迟暴涨、健康分暴跌后,反而被本脚本逆向提拔为全网第 1 名(最优域名)!**
|
|
|
+ 3. 更严重的是,由于 `yd_score` 数值量级在 `80~109`,乘以权重 `5.0` 后高达 `400~545`,而 `yd_latency * 1.0` 仅 `35~75`,`yd_score` 实际上主导了 **85%~90% 的总评分权重**。
|
|
|
+ 4. 同时,`domain_updater.py:486` 中硬编码了 `if not is_int_or_float(weight) or float(weight) <= 0: raise ValueError(...)`,**从代码层面禁止了负数权重**,且不支持为每个字段单独指定方向(`asc`/`desc`)。此外 `README.md:221-225` 的文档示例中只写了 `yd_latency` 和 `yd_pkg_lost_rate`,与实际 `config.server.json` 脱节。
|
|
|
+- **修复方案与落地状态**:
|
|
|
+ - **✅ 已落地修复**:在 `config.server.json` 中彻底移出 `weighted_fields`,转移至 `tie_breakers` 配置 `{"field": "yd_score", "order": "desc"}`(高分优选);在 `scripts/domain_updater.py` 中支持字段级 `order: desc/asc` 与迟滞防抖 `switch_margin_pct: 5.0`。
|
|
|
+ - **测试验证**:`tests/test_domain_updater.py` 中用例 `test_p0_yd_score_polarity_fix_and_healthy_ranking` 100% 验证通过。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 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/<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% 验证通过。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 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) | **复核正确,且深层原因已查明**:<br>远端 `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) | **复核更准确**:<br>当前造成无效 commit 的首要元凶是 **`yd_score` 极性反转(2.1 节)** 与 **`ok <-> fallback` 状态翻转重复提交(2.3 节)**。应在修复这两项后,将 `switch_margin_pct` 作为可选策略增强项,而非最高优止血项。 |
|
|
|
+| **D-03** | **`run_notify()` 子进程失败是否会抛异常触发 `last_good` 回退?** | 认为通知命令返回非 0 会抛异常中断主流程(§3.1.4) | 指出 `subprocess.run(..., check=False)` 不会因非 0 退出码抛异常,真正问题是污染 `stdout`、无 `timeout` 及未捕获 `OSError`(§2.3) | **复核完全正确**:<br>`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) | **复核完全正确**:<br>`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) | **折中裁定(各取所长)**:<br>不应强行用 `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) | **复核完全正确**:<br>路由器端运行环境为 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<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 (修复 yd_score 反转)<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
|
|
|
+```
|
|
|
+
|
|
|
+### 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` 中 `yd_score` 极性反转问题,在 `domain_updater.py` 支持字段级正负向与平局决胜<br>2. 修复 `uninstall_debian.sh` 空 `SERVICE_NAME` 导致 `rm -rf /var/lib/` 漏洞<br>3. 修复 `build_router_package.sh` `trap` 提前注册导致误删 `TMP_DIR` 风险<br>4. 修复 `domain_updater.py` `run_notify()` 的 `shell=True` 命令注入、无超时及 `stdout` 污染问题 | `config.server.json`<br>`scripts/domain_updater.py`<br>`scripts/uninstall_debian.sh`<br>`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<br>2. 对 `last_good` 强制执行当前 `domain_filter` / 格式复检;彻底失败时补发 `run_notify`<br>3. `run_update_and_commit.sh` 区分 `ls-remote` 退出码 `2` 与 `128`,改用 `GIT_CONFIG_*` 环境变量安全传递 Git Token<br>4. `domain_updater.py` 与 `router_local_update.sh` 全面改为同目录临时文件 + `mv` 原子写入,并过滤 `download_speed == 0` 的测速失败节点<br>5. `substore/operator_template.js` 修复非字符串强转 `"[object Object]"` Bug,增加域名/IP 格式校验与本地数据源过期降级(`local_max_age_sec: 86400`) | `scripts/domain_updater.py`<br>`scripts/run_update_and_commit.sh`<br>`scripts/router_local_update.sh`<br>`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` 的完整结构校验<br>2. 修复 `router_local_http.sh` PID 复用误杀与 `router_local_update.sh` JSON 转义及零速 IP 过滤(确认 `-dt/-dn/-dd` 已就绪)<br>3. 修复 `build_router_package.sh` 在 macOS 下的 `.DS_Store` / AppleDouble 打包污染与跨架构 `httpd` 静态检测<br>4. 建立 `tests/` 自动化测试套件(覆盖评分极性、过滤、回退、Shell 安全与 Sub-Store 解析)并修正 `README.md`、`workflow.md`、`CLAUDE.md` 文档偏差 | `scripts/router_local_http.sh`<br>`scripts/router_local_update.sh`<br>`build_router_package.sh`<br>`README.md`<br>`workflow.md`<br>`CLAUDE.md`<br>`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` 评分极性反转**:`config.server.json` 将正向健康分 `yd_score`(权重 5.0)与负向延迟/丢包混用且 `prefer_lower: true`;`validate_config` 禁止负权重,导致延迟翻倍、健康分暴跌的劣化节点反而排第 1 | `config.server.json:68-83`<br>`domain_updater.py:486, 708-735` | ❌ 漏检 | ❌ 漏检 | ✅ **线上实证确认**:`runtime-state` 提交 `4fd8a65 -> 1a38eea` 证明 `www.fastsoso.cn` 延迟从 36ms 恶化到 74ms、`yd_score` 从 109 跌至 78 后,加权分从 36.31 降至 29.0 逆向夺冠 | **P0** | **✅ 已修复**<br>移出加权改入平局决胜降序;测试 `test_p0_yd_score_polarity_fix_and_healthy_ranking` 通过 |
|
|
|
+| **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** | **✅ 已修复**<br>改用环境变量传递变量;测试 `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** | **✅ 已修复**<br>`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** | **✅ 已修复**<br>异常且无 `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** | **✅ 已修复**<br>输出包含 `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** | **✅ 已修复**<br>回退前复检 `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** | **✅ 已修复**<br>子进程已做 `capture_output=True`,彻底杜绝冲刷 stdout |
|
|
|
+| **PY-08** | **本地 `cfst` 模式未过滤 `download_speed == 0.00` 的零速 IP** | `domain_updater.py:926-972`<br>`runtime/current_ip.json:11` | ❌ 漏检 | ❌ 漏检 | ✅ **本地产物确认**:仓库内 `runtime/current_ip.json` 已将 `"download_speed": "0.00"` 的 `104.23.141.56` 选为 `"status": "ok"` | **P1** | **✅ 已修复**<br>过滤 `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** | **✅ 已修复**<br>`write_text_file` / `write_json_file` 采用 `.tmp.$$` + `os.replace` 原子写入 |
|
|
|
+| **PY-10** | **`vps789` API 的 `createdTime` 实为建档时间而非测速时间** | `config.server.json:45, 63`<br>`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** | **✅ 已修复**<br>修正文档说明,默认禁用 `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** | **✅ 已修复**<br>严格校验顶层字典与 `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(定性为增强项) | ✅ **源码确认**:在修复 `yd_score` 极性后,增加可选 `switch_margin_pct`(如 5%~10%)可进一步抑制边界抖动 | **P2** | **✅ 已修复**<br>`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/<pid>/cmdline` 截取 Token | **P1** | **✅ 已修复**<br>改用 `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** | **✅ 已修复**<br>明确区分退出码 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** | **✅ 已修复**<br>实现回退静默,选值未变时跳过 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** | **✅ 已修复**<br>改用 `--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** | **✅ 已修复**<br>强制非空正则校验与 `/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** | **✅ 已修复**<br>单引号包裹转义且服务名受安全正则限制 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 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** | **✅ 已修复**<br>延迟至参数校验后注册 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** | **✅ 已修复**<br>实现 `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** | **✅ 已修复**<br>检查 `/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`<br>`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** | **✅ 已修复 / 纠偏**<br>测速自动过滤 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** | **✅ 已修复**<br>排除 `.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** | **✅ 已修复**<br>严格类型守卫拒绝非 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** | **✅ 已修复**<br>严格 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** | **✅ 已修复**<br>增加 `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** | **✅ 已修复**<br>同时兼容 `$substore.http` 与 `$httpClient` 异步/同步处理 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 核心改造完整代码级方案(Copy-Paste Ready 蓝图)
|
|
|
+
|
|
|
+本节针对上述 P0/P1/P2 问题提供可直接落地的代码级修改参考。
|
|
|
+
|
|
|
+### 8.1 修复 `config.server.json` 评分极性反转与 `domain_updater.py` 评分/迟滞模型(对应 PY-01, PY-12)
|
|
|
+
|
|
|
+#### 1) `config.server.json` 修正建议
|
|
|
+在 `selection` 中支持显式的指标极性定义(或者立即从负向加权中移除正向指标 `yd_score`,或通过 `field_directions` 指定 `yd_score` 为越大越优):
|
|
|
+```json
|
|
|
+ "selection": {
|
|
|
+ "strategy": "weighted_average",
|
|
|
+ "prefer_lower": true,
|
|
|
+ "switch_margin_pct": 5.0,
|
|
|
+ "weights": {
|
|
|
+ "yd_latency": 1.0,
|
|
|
+ "yd_pkg_lost_rate": 10.0
|
|
|
+ },
|
|
|
+ "field_prefer_lower": {
|
|
|
+ "yd_latency": true,
|
|
|
+ "yd_pkg_lost_rate": true,
|
|
|
+ "yd_score": false
|
|
|
+ },
|
|
|
+ "tie_breakers": [
|
|
|
+ {
|
|
|
+ "field": "yd_score",
|
|
|
+ "order": "desc"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "field": "yd_pkg_lost_rate",
|
|
|
+ "order": "asc"
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "field": "yd_latency",
|
|
|
+ "order": "asc"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+#### 2) `scripts/domain_updater.py` 极性感知评分与防抖迟滞(Hysteresis)实现
|
|
|
+```python
|
|
|
+def compute_weighted_average(
|
|
|
+ record: Dict[str, Any],
|
|
|
+ weights: Dict[str, Any],
|
|
|
+ prefer_lower: bool = True,
|
|
|
+ field_prefer_lower: Optional[Dict[str, bool]] = None,
|
|
|
+) -> Optional[float]:
|
|
|
+ if not weights:
|
|
|
+ return None
|
|
|
+ field_dirs = field_prefer_lower or {}
|
|
|
+ total_weight = 0.0
|
|
|
+ weighted_sum = 0.0
|
|
|
+ for field, raw_weight in weights.items():
|
|
|
+ try:
|
|
|
+ weight = float(raw_weight)
|
|
|
+ except Exception:
|
|
|
+ continue
|
|
|
+ if weight <= 0:
|
|
|
+ continue
|
|
|
+ value = to_float(record.get(field))
|
|
|
+ if value is None:
|
|
|
+ return None
|
|
|
+ # 若单字段极性与全局 prefer_lower 相反,则取相反数参与加权
|
|
|
+ is_field_lower_better = bool(field_dirs.get(field, prefer_lower))
|
|
|
+ effective_value = value if (is_field_lower_better == prefer_lower) else -value
|
|
|
+ weighted_sum += effective_value * weight
|
|
|
+ total_weight += weight
|
|
|
+ if total_weight <= 0:
|
|
|
+ return None
|
|
|
+ return weighted_sum / total_weight
|
|
|
+
|
|
|
+
|
|
|
+def apply_hysteresis_if_needed(
|
|
|
+ ranked: List[Dict[str, Any]],
|
|
|
+ last_good: str,
|
|
|
+ selection_cfg: Dict[str, Any],
|
|
|
+) -> Dict[str, Any]:
|
|
|
+ """当新第 1 名相比仍在候选池中的 last_good 领先幅度不足 switch_margin_pct 时,保持 last_good。"""
|
|
|
+ best = ranked[0]
|
|
|
+ margin_pct = to_float(selection_cfg.get("switch_margin_pct")) or 0.0
|
|
|
+ if margin_pct <= 0 or not last_good or best.get("domain") == last_good:
|
|
|
+ return best
|
|
|
+
|
|
|
+ prefer_lower = bool(selection_cfg.get("prefer_lower", True))
|
|
|
+ best_score = to_float(best.get("score"))
|
|
|
+ if best_score is None:
|
|
|
+ return best
|
|
|
+
|
|
|
+ for cand in ranked[1:]:
|
|
|
+ if cand.get("domain") == last_good:
|
|
|
+ last_score = to_float(cand.get("score"))
|
|
|
+ if last_score is None or last_score == 0:
|
|
|
+ break
|
|
|
+ improvement_pct = (
|
|
|
+ ((last_score - best_score) / abs(last_score)) * 100.0
|
|
|
+ if prefer_lower
|
|
|
+ else ((best_score - last_score) / abs(last_score)) * 100.0
|
|
|
+ )
|
|
|
+ if improvement_pct < margin_pct:
|
|
|
+ return cand
|
|
|
+ break
|
|
|
+ return best
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 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) | • P0 极性反转真实数据排序回归与平局决胜<br>• run_notify 标准输出隔离与超时保护<br>• 异常回退包含 domain/ip 键<br>• 黑名单 last_good 禁止回退复活<br>• 本地 cfst 过滤 0.00 下载速度<br>• `--result-json-file` IPC 解耦<br>• validate_config 严格顶层结构校验 | 7 项 | ~0.15s | **✅ 全部通过 (PASS)** |
|
|
|
+| [`tests/test_shell_lifecycle.py`](./tests/test_shell_lifecycle.py) | • `uninstall_debian.sh` 空服务名安全校验(严禁 `rm -rf /var/lib/`)<br>• `build_router_package.sh` 延迟注册 trap 防止误删工作目录<br>• `run_update_and_commit.sh` 采用 `GIT_CONFIG_*` 环境变量传递 Token(argv 零暴露)<br>• 严格区分 `git ls-remote` 退出码 `2` vs `128`<br>• 回退静默跳过噪音 Git commit | 5 项 | ~0.12s | **✅ 全部通过 (PASS)** |
|
|
|
+| [`tests/test_router_scripts.py`](./tests/test_router_scripts.py) | • `router_local_update.sh` `.tmp.$$` + `mv -f` 原子替换写入<br>• 测速结果优先过滤 `download_speed > 0`<br>• 纠偏初审报告关于 `-dt/-dn/-dd` 参数支持情况 | 3 项 | ~0.05s | **✅ 全部通过 (PASS)** |
|
|
|
+| [`tests/test_substore_operator.py`](./tests/test_substore_operator.py) | • `extractValue()` 严格类型守卫(拒绝非字符串对象强转为 `[object Object]`)<br>• 域名 (RFC 1123) 与 IPv4/IPv6 正则校验(拒绝 HTML 报错与非法字符)<br>• `LOCAL_MAX_AGE_SEC` (24h) 本地路由器源超时自动降级 | 3 项 | ~0.06s | **✅ 全部通过 (PASS)** |
|
|
|
+
|
|
|
+### 9.2 运行命令
|
|
|
+```bash
|
|
|
+python3 -m unittest discover tests -v
|
|
|
+# 输出:Ran 18 tests in 0.378s, OK
|
|
|
+```
|