Browse Source

fix: switch_margin_pct

Dew-OF-Aurora 6 ngày trước cách đây
mục cha
commit
844ec00519
2 tập tin đã thay đổi với 250 bổ sung và 1 xóa
  1. 1 1
      configs/config.server.json
  2. 249 0
      docs/reviews/argon-review.md

+ 1 - 1
configs/config.server.json

@@ -80,7 +80,7 @@
     ],
     "prefer_lower": true,
     "within_hours": 0,
-    "switch_margin_pct": 5.0,
+    "switch_margin_pct": 2.5,
     "tie_breakers": [
       {
         "field": "yd_score",

+ 249 - 0
docs/reviews/argon-review.md

@@ -0,0 +1,249 @@
+这里为您重新生成了一份单次完整输出、无任何截断的 Markdown 审查报告。您可以直接点击代码块右上角的“复制”按钮,一键保存为本地 .md 文件:
+
+# vmess-domain-rotator 全景架构与代码深度审查报告
+
+- **审查日期**:2026-10-23
+- **审查范围**:全量业务源码(Python / Bash / POSIX sh / Sub-Store JS)、三套配置文件、打包脚本、Claude Hooks/Skills 研发护栏、三份历史审查文档及 7 套自动化测试套件。
+- **审查目标**:全局梳理项目架构,交叉核验历史终审台账(`ARCHITECTURE_REVIEW_UNIFIED.md`)与当前实际代码的一致性,排查未闭环的逻辑漏洞、边界风险与测试泄漏。
+
+---
+
+## 一、 项目全景总览 (Project Overview)
+
+`vmess-domain-rotator` 是一个完成度极高、工程边界清晰的 **Cloudflare 优选域名/IP 自动化选优与分发系统**。其核心架构分为三层:
+
+1. **多源采集与选优层(Producers)**
+   - **Server 模式**(`configs/config.server.json` + `scripts/rotator/`):拉取第三方监控 API,经字段映射、黑名单过滤、加权评分、平局决胜及切换迟滞(Hysteresis)防抖后选出最优域名。
+   - **本地 Python 模式**(`configs/config.local.json` + `scripts/rotator/`):调用本地 `cfst` 二进制测速,解析 CSV,自动剔除零速死节点(除非显式配置 `-dd`)。
+   - **BusyBox 路由器模式**(`configs/config_router.conf` + `scripts/router/`):纯 POSIX `sh` + `awk` 实现,采用 **RAM (`/tmp`) 与 Flash (`/jffs`) 动静分离设计**,保护路由器闪存寿命,并通过 `busybox httpd` 暴露给局域网。
+2. **状态总线与发布层(Publishers)**
+   - `scripts/server/run_update_and_commit.sh`:双频自适应时间窗口(高峰/非高峰容差)、独立 Git Worktree 检出 `runtime-state` 孤儿分支、回退静默抑制(防状态翻转刷屏)、以及 **Mode A 历史滚动修剪(Squashed Rebase 保留最近 50 次提交)**。
+3. **下游消费层(Consumers)**
+   - `substore/operator_template.js`:Sub-Store 节点替换算子,支持手动参数最高优先级覆盖、多源降级(`default` / `server_only` / `router_only`)、RFC 1123 域名/IP 严格校验及本地数据 24 小时过期降级。
+
+---
+
+## 二、 架构与工程亮点 (Strengths)
+
+1. **极高水准的模块解耦与向后兼容**:`domain_updater.py` 拆分为 `scripts/rotator/` 下 7 个高内聚子模块(`config` / `filter` / `sources` / `scoring` / `storage` / `notify` / `main`),同时顶层保留完整 Facade 导出,兼顾了代码可维护性与外部调用兼容。
+2. **严密的 AI 研发护栏**:`.claude/hooks/check_syntax.py` 实现了零副作用的语法门禁(Python AST `compile`、Bash/sh `-n`、Node `--check`),配合 `verify-rotator` Skill 的隔离预检规范,工程化程度极高。
+3. **零外部依赖测试体系**:`tests/` 下 7 个测试文件完全基于标准库 `unittest` 与 Node 内置 `vm` 构建,执行速度快且可移植性强。
+
+---
+
+## 三、 深度代码审查:现存漏洞与“账实不符” (Findings)
+
+尽管 `docs/reviews/ARCHITECTURE_REVIEW_UNIFIED.md` 标记“所有 P0/P1/P2 问题 100% 修复完毕”,但通过**逐行比对当前实际代码与终审报告台账**,共发现 **8 处关键的实际代码与报告声明不符、未闭环的逻辑缺陷及安全隐患**:
+
+### 🔴 1. 【P1 消费端逻辑缺陷】Sub-Store `default` 模式依然会被路由器的故障回退值劫持
+
+- **位置**:`substore/operator_template.js:184-239` (`selectValueRouterFirst`)
+- **现象与根因**:
+  当路由器断网或测速失败时,`router_local_update.sh:188-196` 会写出 `"status": "error_use_last_good"`,且该 JSON **不包含 `top_candidates` 字段**,同时将 `updated_at` 刷新为当前报错时间。
+  此时在 `operator_template.js` 中:
+  1. `isRouterDataFresh(routerObj)` 检查通过(因为 `updated_at` 刚刚被错误回退刷新);
+  2. `parseRouterSpeedMBps(routerObj)` 因为没有 `top_candidates`,返回 `status: "missing"`;
+  3. 第 235 行低速拦截条件为 `if (telemetry.router.speedParse === "ok" && telemetry.router.speedMBps < ROUTER_MIN_SPEED_MB_PER_S)` —— 由于 `speedParse` 是 `"missing"`,**直接绕过了低速门禁**;
+  4. 最终在 `server` 完全健康(`status: "ok"`)的情况下,依然优先返回了路由器处于故障回退状态的旧 IP。
+- **修复建议**:
+  在 `selectValueRouterFirst()` 中,当 `telemetry.router.status !== "ok"` 且 `telemetry.server.ok && telemetry.server.status === "ok"` 时,优先降级选用健康的 `server`;或者在 `router_local_update.sh` 回退时不要刷新用于新鲜度判定的成功观测时间戳。
+
+### 🔴 2. 【P1 发布链路缺陷】Git Push 失败后,若下一轮优选域名未变,将永久跳过重试
+
+- **位置**:`scripts/server/run_update_and_commit.sh:277-318`
+- **现象与根因**:
+  初审报告 `P1-3` 和复审报告 `§3.2` 均指出了此问题,但**当前代码中依然未修复**。
+  脚本在第 277 行读取 `before="$(git -C "$work_dir" show "HEAD:${selected_rel}")"`(读取的是**本地** `runtime-state` 分支的 `HEAD`),并在第 314 行判断:如果 `after == before` 且 `status_unchanged == 1`,则直接 `log "... skip git commit and push"; exit 0`。
+  **故障推演**:第 1 轮选出新域名 `a.com`,本地 `git commit` 成功,但因网络抖动 `git push` 失败退出。第 2 轮(30 分钟后)运行,API 依然选出最优域名 `a.com` —— 此时本地 `HEAD` 已经是 `a.com`,触发 `after == before` 直接 `exit 0`。**本地与远端永久分叉,远端永远收不到这次更新**,直到未来某天域名再次变化或人工执行 `--force-commit`。
+- **修复建议**:
+  将“是否需要 `git commit`”与“是否需要 `git push`”解耦。在 `exit 0` 跳过前,检查当 `push_enabled == "1"` 时,本地 `HEAD` 是否领先于远端跟踪分支(例如比对 `git rev-parse HEAD` 与 `git rev-parse @{u}`),若存在未推送的 commit,则直接跳转至推送阶段重试。
+
+### 🔴 3. 【P0/S0 安全隐患】真实 API Token 仍在配置文件中明文入库
+
+- **位置**:`configs/config.server.json:6`
+- **现象与根因**:
+  `"url": "https://vps789.com/openApi/listMonitors?token=UR6SYEEATJGVOPIST6QHCPFNAJ8233S8"` 依然明文硬编码在配置文件中,且 `scripts/rotator/config.py` / `sources.py` 并未实现环境变量(如 `AUTODOMAIN_API_TOKEN`)或 `token_file` 的注入支持。
+- **修复建议**:
+  1. 在 `vps789.com` 后台轮换重置该 Token;
+  2. 在 `sources.py:fetch_api_json()` 中支持读取环境变量(如 `os.environ.get("AUTODOMAIN_API_TOKEN")`)或 `api.token_file` 注入查询参数,将 `config.server.json` 中的明文移除。
+
+### 🟠 4. 【P1 状态污染与策略穿透】`last_good` 回退与状态读取的三个漏洞
+
+- **位置**:`scripts/rotator/main.py:59, 125-130, 179-181` & `configs/config.local.json:37`
+- **现象与根因**:
+  1. **`state.json` 非字典类型崩溃风险 (`P-07`)**:若 `state.json` 内容被外部意外写成合法 JSON 数组 `[]`、`null` 或 `"string"`,`read_json_file` 原样返回非 `dict` 对象,随后 `main.py:59` 执行 `state.get(...)` 会直接在主 `try` 块外抛出 `AttributeError` 崩溃退出,导致正常的 API 采集也无法启动。
+  2. **`record_filter` 黑名单穿透**:如 `CLAUDE.md` 所述,当前回退仅复检了 `domain_filter`(正则),未复检 `record_filter`(例如在 `config.server.json` 的 `record_filter` 中拉黑了 `domain equals openai.com`,当 API 返回空列表触发 `fallback_last_good` 时,若上次 `last_good` 是 `openai.com`,它依然会绕过 `record_filter` 复活)。
+  3. **Server 与 Local 默认共用 `runtime/state.json`**:两份配置默认都指向 `./runtime/state.json`。在同一台机器交替运行 Server 和 Local 模式时,后运行者的 `write_json_file(state_file, new_state)` 会**全量覆盖** `state.json`,直接抹掉另一模式的 `last_good_domain` / `last_good_ip`。
+
+### 🟠 5. 【P1 参数映射错误】`build_router_package.sh` SSOT 同步将“下载速度下限”误映射为“丢包率上限”
+
+- **位置**:`build_router_package.sh:172-173` vs `scripts/router/router_local_update.sh:258-263`
+- **现象与根因**:
+  在 `build_router_package.sh:172-173` 中:
+  ```python
+  "-sl": "CFST_LOSS_LIMIT",
+  "-speedlimit": "CFST_SPEED_LIMIT",
+
+而在 router_local_update.sh:258-263(以及 cfst 官方参数定义)中,丢包率上限是 -tlr
+(CFST_LOSS_LIMIT),下载速度下限才是 -sl (CFST_SPEED_LIMIT)。 这意味着如果用户在 config.local.json
+里配置了 -sl 5(速度下限 5MB/s),执行 --sync-from-config 打包时会将其错误同步为路由器的
+CFST_LOSS_LIMIT="5"(丢包率上限),且漏掉了对 -tlr 的映射。
+
+  - 修复建议: 将 build_router_package.sh 中的映射修正为 "-tlr": "CFST_LOSS_LIMIT" 和 "-sl":
+    "CFST_SPEED_LIMIT"。
+
+### 🟡 6. 【P2 契约与文档漂移】终审报告宣称已修复,但实际代码未落地的 2 处细节
+
+1.  路由器 JSON 稀疏语义未统一(scripts/router/router_local_update.sh:123):
+      - README.md §1.2 与终审报告宣称“仅在字段非空时输出真实键值,彻底消除空字符串/空数组/null 占位污染”,但
+        router_local_update.sh:123 的 awk
+        依然硬编码输出:"location_country":"","location_city":"","host_provider":"","score_value":null,"scores":[],"created_raw":""。
+2.  终审文档对 router_local_http.sh (RT-03) 的修复描述与代码脱节:
+      - ARCHITECTURE_REVIEW_UNIFIED.md §7.4 RT-03 和 §8.5 声称已增加 is_running() 检查
+        /proc/$pid/cmdline 包含 httpd。核查 scripts/router/router_local_http.sh
+        全文,实际采用的是前台运行 httpd -f + wait "$httpd_pid" 模式,并无 PID_FILE 或
+        /proc/$pid/cmdline 逻辑。虽然前台 wait 模式本身规避了 PID 文件复用误杀,但终审台账的描述与代码事实不符。
+
+### 🟡 7. 【P2 测试隔离缺陷】test_server_mode_adaptive_interval_window 存在真实网络请求泄漏
+
+  - 位置:tests/test_server_mode_lifecycle.py:165-199
+  - 现象与根因: 正如 .claude/skills/verify-rotator/SKILL.md 中记录的那样,该测试在“场景 A(第 184
+    行)”首次调用 run_update_and_commit.sh 时,尚未将 config.server.json 中的 api.url 替换为本地
+    file:// 模拟路径(替换操作写在了第 205 行的场景 B 里)。 如果运行测试时恰好处于所设定的时间窗口或整点边界(例如 curr_min
+    == 0 或 curr_hour == 2),场景 A 不会被跳过,而是会直接向真实的 https://vps789.com/...
+    发起外部网络请求,破坏测试的离线隔离性。
+  - 修复建议: 在场景 A 执行前先完成本地模拟 file:// API 配置写入,并通过在测试环境 PATH 中注入 mock date
+    命令固定测试时钟,从而将此用例安全纳入常规回归测试。
+
+## 四、 问题汇总矩阵 (Summary Matrix)
+
+| 编号       | 严重度         | 模块                                            | 问题简述                                                                                                                                                                          | 当前状态     |
+| :------- | :---------- | :-------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- |
+| **F-01** | **P0 / S0** | `configs/config.server.json`                  | 真实 API Token 明文硬编码入库,缺乏环境变量/文件注入机制                                                                                                                                            | ❌ 未修复    |
+| **F-02** | **P1**      | `substore/operator_template.js`               | `default` 模式下路由器 `error_use_last_good` 因无 `top_candidates` 绕过测速门禁,压制健康 Server                                                                                                 | ❌ 未修复    |
+| **F-03** | **P1**      | `scripts/server/run_update_and_commit.sh`     | 本地 `git commit` 成功但 `push` 失败后,若下一轮优选值未变,将触发跳过判断导致永久不重试推送                                                                                                                     | ❌ 未修复    |
+| **F-04** | **P1**      | `scripts/rotator/main.py` / `storage.py`      | 1\. `state.json` 若为非字典 JSON(如 `[]` / `null`)将直接抛出 `AttributeError` 崩溃<br>2\. `last_good` 回退未复检 `record_filter` 静态域名黑名单<br>3\. Server 与 Local 配置默认共用 `runtime/state.json` 互相覆盖 | ❌ 未修复    |
+| **F-05** | **P1**      | `build_router_package.sh`                     | `--sync-from-config` 将 `-sl`(下载速度下限)误映射为 `CFST_LOSS_LIMIT`(丢包上限),且漏映射 `-tlr`                                                                                                  | ❌ 未修复    |
+| **F-06** | **P2**      | `scripts/router/router_local_update.sh`       | 路由器生成的 `top_candidates` 仍包含硬编码空字符串/`null`/`[]`,未对齐稀疏字段契约                                                                                                                      | ⚠️ 契约漂移  |
+| **F-07** | **P2**      | `docs/reviews/ARCHITECTURE_REVIEW_UNIFIED.md` | 终审报告 `RT-03` 声称 `router_local_http.sh` 检查 `/proc/$pid/cmdline`,与实际前台 `wait` 代码不符                                                                                              | ⚠️ 文档失真  |
+| **F-08** | **P2**      | `tests/test_server_mode_lifecycle.py`         | `test_server_mode_adaptive_interval_window` 在配置本地模拟 API 前先执行了一次脚本,存在真实网络请求泄漏风险                                                                                                | ⚠️ 测试未隔离 |
+
+## 五、 核心修复方案与代码级建议 (Actionable Fixes)
+
+### 5.1 修复 build_router_package.sh 参数映射错位(对应 F-05)
+
+修改位置:build_router_package.sh:160-175
+
+    arg_map = {
+        "-p": "CFST_DISPLAY_COUNT",
+        "-f": "CFST_IP_FILE",
+        "-n": "CFST_THREADS",
+        "-t": "CFST_TEST_COUNT",
+        "-dn": "CFST_DOWNLOAD_COUNT",
+        "-dt": "CFST_DOWNLOAD_TIME",
+        "-tp": "CFST_PORT",
+        "-url": "CFST_URL",
+        "-cfcolo": "CFST_CFCOLO",
+        "-tl": "CFST_LATENCY_LIMIT",
+        "-tll": "CFST_LATENCY_LOWER",
+        "-tlr": "CFST_LOSS_LIMIT",  # 修正:-tlr 对应丢包率上限
+        "-sl": "CFST_SPEED_LIMIT",  # 修正:-sl 对应下载速度下限
+    }
+
+### 5.2 修复 substore/operator_template.js 故障回退劫持(对应 F-02)
+
+修改位置:substore/operator_template.js:223-239 (selectValueRouterFirst)
+
+  if (!telemetry.router.ok && !telemetry.server.ok) {
+    throw new Error(`router failed: ${telemetry.router.error}; server failed: ${telemetry.server.error}`);
+  }
+
+  if (!telemetry.router.ok && telemetry.server.ok) {
+    return { value: telemetry.server.value, chosenSource: "server", reason: "router_fail", telemetry };
+  }
+
+  if (telemetry.router.ok && !telemetry.server.ok) {
+    return { value: telemetry.router.value, chosenSource: "router", reason: "server_fail", telemetry };
+  }
+
+  // 新增:当路由器处于故障回退状态(非 ok)而 Server 健康时,优先降级至健康的 Server
+  if (telemetry.router.status !== "ok" && telemetry.server.status === "ok") {
+    return { value: telemetry.server.value, chosenSource: "server", reason: "router_degraded_fallback", telemetry };
+  }
+
+  if (telemetry.router.speedParse === "ok" && telemetry.router.speedMBps < ROUTER_MIN_SPEED_MB_PER_S) {
+    return { value: telemetry.server.value, chosenSource: "server", reason: "router_low_speed", telemetry };
+  }
+
+  return { value: telemetry.router.value, chosenSource: "router", reason: "router_preferred", telemetry };
+
+### 5.3 修复 run_update_and_commit.sh 未推送提交永久跳过问题(对应 F-03)
+
+修改位置:scripts/server/run_update_and_commit.sh:313-318
+
+在判定“值未变化”准备跳过前,检查本地分支是否还有未推送到远端的 commit:
+
+has_unpushed="0"
+if [[ "$push_enabled" == "1" ]] && [[ -n "$push_remote" ]]; then
+  if git -C "$work_dir" rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
+    local_head="$(git -C "$work_dir" rev-parse HEAD 2>/dev/null || echo "")"
+    remote_head="$(git -C "$work_dir" rev-parse "@{u}" 2>/dev/null || echo "")"
+    if [[ -n "$local_head" ]] && [[ "$local_head" != "$remote_head" ]]; then
+      has_unpushed="1"
+    fi
+  else
+    has_unpushed="1"
+  fi
+fi
+
+if [[ "$force_commit" != "1" ]] && [[ -n "$before" ]] && [[ "$after" == "$before" ]]; then
+  if [[ "$status_unchanged" == "1" || "$is_fallback" == "1" ]]; then
+    if [[ "$has_unpushed" == "1" ]]; then
+      log "[vmess-domain-rotator] value unchanged (${after}), but local branch has unpushed commits; retrying push"
+      git_auth "$work_dir" push -u "$push_remote" "$runtime_branch:$runtime_branch" || {
+        log "[vmess-domain-rotator] retry git push failed"
+        [[ "$push_required" == "1" ]] && exit 1
+      }
+    else
+      log "[vmess-domain-rotator] selected value unchanged (${after}) [status=${updater_status:-unknown}], skip git commit and push"
+    fi
+    exit 0
+  fi
+fi
+
+### 5.4 加固 state.json 类型安全与回退黑名单检查(对应 F-04)
+
+修改位置:scripts/rotator/main.py:59
+
+    state = read_json_file(state_file, default={})
+    if not isinstance(state, dict):
+        state = {}
+    last_good = str(state.get(state_last_good_key, "") or "").strip()
+
+同时提取一个通用的 is_domain_blocked(domain, cfg) 辅助函数,在 last_good 回退时不仅调用
+apply_filter(),还对 record_filter 中针对 field == "domain" 的规则执行复检,防止被拉黑的域名通过
+last_good 复活。
+
+### 5.5 修复 test_server_mode_adaptive_interval_window 离线隔离(对应 F-08)
+
+修改位置:tests/test_server_mode_lifecycle.py:165-215
+
+将 payload_file = self._write_mock_api_payload(...) 与改写 cfg["api"]["url"] =
+f"file://{payload_file}" 的代码上移至场景 A 执行之前,并通过在 env 的 PATH 最前面注入一个临时 mock date
+脚本来固定测试时钟(例如场景 A 固定返回 04 时 15 分),使该测试 100% 确定性通过且绝不触发外网请求。
+
+### 六、 建议推进路线 (Next Steps)
+
+1.  第一优先级(立竿见影的正确性与测试闭环):
+      - 修复 build_router_package.sh 的 -sl / -tlr 参数映射错位并补充对应单元测试断言;
+      - 修复 tests/test_server_mode_lifecycle.py 的模拟 API 注入顺序,解除
+        .claude/skills/verify-rotator/SKILL.md 中对该测试的默认排除限制;
+      - 修复 scripts/rotator/main.py 对非字典 state.json 的防御性容错。
+2.  第二优先级(端到端容灾闭环):
+      - 修复 substore/operator_template.js 对 error_use_last_good 状态的降级判定;
+      - 修复 scripts/server/run_update_and_commit.sh 的未推送 commit 自动重试机制;
+      - 清理 router_local_update.sh 中 top_candidates 的冗余空字段,对齐稀疏 JSON 契约。
+3.  第三优先级(凭据安全与文档对齐):
+      - 支持通过环境变量或外部文件注入 API Token,轮换并移除 configs/config.server.json 中的明文 Token;
+      - 修正 ARCHITECTURE_REVIEW_UNIFIED.md 中的少量失真描述,保持文档与代码 100% 同步。
+