[!IMPORTANT] 终审与修复状态更新(2026-10-13):
本报告的所有发现、与复核报告的分歧裁定以及最新查漏补缺已全部整合进唯一权威终审报告:ARCHITECTURE_REVIEW_UNIFIED.md。
全仓所有 P0 / P1 / P2 问题均已全部修复完毕 ✅,并在tests/目录下建立了端到端自动化测试套件(18 项用例 100% 通过)。最新修复状态与代码台账请以终审报告为准。
审查范围:业务代码(Python / Bash / POSIX sh / Sub-Store JS)、三套配置、运行时输出、服务端 Git 发布链路、systemd 安装、BusyBox 路由器部署与 HTTP 暴露、下游消费。
审查日期:2026-10-09 审查方式:完整阅读源码与配置;对全部脚本做语法检查;通过临时目录、模拟 API 响应、模拟 cfst 结果和 JS 运行环境离线复现关键场景。本轮为只读审查,未修改业务代码,未执行真实 API、cfst 测速、systemd 安装卸载或 Git 推送。
本文件来源:由仓库内 BUSINESS_FLOW_REVIEW.md(原文档)与另一轮独立审查合并而成。每条结论标注来源:
[原] — 原文档已有[补] — 本次合并新增[合] — 两轮审查一致(原文档已提出,本次补充了实现细节、量化数据或额外验证)原文档在状态语义、发布正确性、BusyBox 健壮性和离线复现方面更深入;本次补充集中在密钥泄露、跨实现一致性、可维护性、构建与运维四个方面。两轮结论无相互矛盾之处,分歧仅在于深浅与覆盖面。
项目已经形成一条完整的动态优选链路:
API / 本地 cfst / BusyBox cfst
↓
解析候选
↓
过滤与评分
↓
选择当前值
↓
写入运行时文件和 fallback 状态
↓
服务器提交 runtime-state 或路由器 HTTP 暴露
↓
Sub-Store 获取并替换 VMess 节点
功能的完整性不是问题。核心问题是没有单一的事实来源(single source of truth),导致三层语义各自漂移:
domain_updater.py)、BusyBox awk(router_local_update.sh)、Sub-Store JS(operator_template.js)消费。三者对"字段存在性"、"状态含义"、"指标单位"的理解不一致,且没有任何文件固化契约。updated_at 同时承担尝试时间、成功时间、发布时间;error_use_last_good 在下游可能被当作健康;本地写入 ≠ 远端已发布。runtime/;多进程/多调度器可同时写同一批文件;无锁、无实例标识。这些漂移会产生三类后果([原]):
重构主线:统一输出契约 + 实例隔离 + 原子发布 + 可重试发布([原]),并叠加 消除重复实现 + 合并数据管线([补])。
纲领性建议(
[补]):新增docs/OUTPUT_CONTRACT.md作为唯一契约文件,声明 schema 版本、必需/可选字段、状态枚举、单位与时效语义。Python、shell、JS 三者都引用它,并各自带一条契约校验。这是把后面 P1/P2 从"逐个修 bug"变成"结构性消除"的关键。
[补]api.url 明文内嵌 token=UR6SYEEATJGVOPIST6QHCPFNAJ8233S8。已确认该值存在于 commit ea38258,且该 commit 是 main 的祖先 —— 因此不是历史深处或野分支的问题,任何 clone 都能拿到,并会随仓库推送到 git.dewofaurora.de。
影响:token 等同于公开,可被第三方消耗你的 API 配额、伪造请求,或(若该 API 有写能力)篡改你依赖的候选数据。
建议:
install_debian.sh 已经支持 --git-http-token-file + chmod 600(install_debian.sh:420-424)。给 API 加同样的入口:
api.token_file,或AUTODOMAIN_API_TOKEN,在 build_url 阶段注入。token_env / token_file 的引用,不保留字面值。.gitignore / CI 中加一条 secret 扫描,防止复发。判定标准:会导致「错误成功」、远端永久分叉、灾备失效或数据损坏。
[原](已独立复现)位置:scripts/domain_updater.py:271-295(兜底正则在 L283-286)
当配置的结构化字段路径解析不到任何候选时,parse_domains() 会退化为对整个 JSON 文本执行正则搜索。本次独立复现:
err = {"code": 401, "message": "unauthorized, see https://status.example.com/help for details"}
get_values_by_path(err, "data[].ip") # -> []
parse_domains(err, {"field_paths": ["data[].ip"]})
# -> ['status.example.com']
即:API 返回 401/403 错误页,更新器把 status.example.com 当作候选,以 status: ok 写入输出,fallback 不触发,错误目标覆盖上次正常值。
建议:
urllib 对 4xx/5xx 会抛异常,但业务层错误码如 {"code": 401} 不会)。api.success_path / success_equals)。parser.regex_fallback: false)。[合]位置:config.server.json:103-111、config.local.json:37-46、scripts/domain_updater.py:1101
两份 Python 配置都用 ./runtime,都用 state.json / substore_vars.json。但二者的 state_last_good_key 分别是 last_good_domain / last_good_ip,互不兼容。
影响(原文档已复现,本次补充一条):
last_good_domain → Local 写入 last_good_ip 覆盖同一文件 → Server 故障时读不到自己的 fallback。substore_vars.json 的键在 AUTO_DOMAIN / AUTO_CFIP 之间翻转,下游读到的键名取决于"谁最后跑的"。[补] runtime/state.json 与 runtime/substore_vars.json 正是 runtime-state 分支所跟踪的文件(已确认分支内容:current_domain.txt/json、state.json、substore_vars.json)。因此本地手工跑一次 config.local.json,下一次服务端提交就会把 IP 值推到 runtime-state,污染下游。建议:
config.local.json 的 runtime_dir 改为 ./cfip_runtime(这与 README §2「路由器相关模式:cfip_runtime/」一致,只有 §4.3 文档写错)。instance_id;状态文件带独立 schema 与实例标识,拒绝读取其他模式的状态。[原]位置:scripts/run_update_and_commit.sh:287
跳过判断在 push 之前,且 before 取自本地 runtime-state 分支:
before="$(git -C "$work_dir" show "HEAD:${selected_rel}" ...)"
# ...
if [[ "$force_commit" != "1" ]] && [[ -n "$before" ]] && [[ "$after" == "$before" ]] && [[ "$status_unchanged" == "1" ]]; then
log "... skip git commit and push"
exit 0
fi
work_dir 用的是本地分支(L222-L223 仅在本地分支不存在时才 fetch 远端)。因此:本地 commit 成功但 push 失败 → 下一轮本地 HEAD 已有该值 → after == before → 提前退出 → 永远不推。远端与本地永久分叉,Sub-Store 持续读旧域名,只能靠强制提交或下一次值变化恢复。
建议:拆成两个独立判断 ——
本地内容是否变化? 是 → 提交
远端是否包含当前本地提交? 否 → 重试推送
推送状态需持久化,或每轮比对远端 ref 与本地 HEAD。值不变 ≠ 发布成功。
[原]位置:substore/operator_template.js:118、substore/operator_template.js:159
default 模式下,selectValueRouterFirst 只判断路由器 JSON 能否取到、能否解析、是否有值(telemetry.router.ok)。status: error_use_last_good 只进 telemetry,不影响来源资格。
更关键的是速度闸门被绕过:parseRouterSpeedMBps 读 top_candidates[0].download_speed(L61-L62),而 fallback 路径写出的 JSON 不含 top_candidates,于是 speedParse === "missing",speedParse === "ok" && < 阈值 不成立,低速拦截失效。
结果:路由器断网/测速失败时,Sub-Store 仍选一个不可用的旧 IP,覆盖健康的服务器结果。
建议:
status: ok 且未过期的来源才参与正常优先级决策。last_success_at / valid_until。[原]位置:scripts/router_local_update.sh:148、scripts/router_local_update.sh:161
BusyBox 脚本用 cat > file 直接覆盖 JSON,而 HTTP 服务同时在读这些文件;state.json 还是路由器重启后唯一的 fallback 来源。
Python 侧已有 _atomic_write(domain_updater.py:37-69,temp + fsync + os.replace),BusyBox 侧完全没有对应实现 —— 能力不对等。
影响:HTTP 请求读到空文件/半截 JSON;更新中断、断电或写失败导致 state 被截断,重启后 fallback 丢失。
建议:同目录 temp → 写全 → fsync → rename。所有 HTTP 可见文件与持久化 state 共用同一套原子发布函数;更新过程加实例锁。
[原]位置:scripts/domain_updater.py:660、scripts/domain_updater.py:749
加权评分遇缺失字段时 score_value 留 None,排序靠后;但若全部记录都缺失,仍保留候选并选第一条,状态照样 ok。
建议定义候选资格:必需指标、缺失是否淘汰、取值范围与单位、无有效候选时是否 fallback(而非输出 ok)。
[合]位置:scripts/domain_updater.py:194、scripts/router_local_update.sh:337
两侧都可能接受任意非空字符串。原文档已复现 BusyBox 把 999.999.999.999 写入 current_ip.json 与 state.json —— 因为 BEST_IP 直接来自 sed -n '2p' | cut -d',' -f1,没有任何 IP 校验(Python 侧虽有 IPV4_RE,但 cfst 分支未用于校验选中值)。
[合] 补充:BusyBox 固定取第二行作选中值,而候选解析会跳过空行。若第二行首列为空、第三行有效,则选中值与候选列表不一致(选中值来自第 2 行,candidate_1 来自第 3 行)。
建议:统一 IPv4/IPv6 校验;过滤重复表头、空行、损坏行;选中值、候选列表、source count 必须来自同一次 CSV 解析。
[原]位置:scripts/router_local_update.sh:315、scripts/router_local_update.sh:327
运行前不清理旧结果,也不验证结果由本次运行产生。原文档离线复现了"命令成功但未生成新结果时读取旧 CSV"。
建议:每轮唯一临时结果路径 → 成功后 move 为正式结果;记录开始/结束时间或 inode/mtime 验证归属;无新结果时进入 fallback,不得返回 ok。
run_cfst 的 cd 失败后可能继续执行 [原]位置:scripts/router_local_update.sh:220
run_cfst 以 if ! run_cfst 调用,set -e 不保证 cd 失败后停止(最小 shell 示例已复现)。后果:在错误目录执行另一个同名 cfst,或读到错误输入。
建议:cd "$CFST_WORK_DIR" || return 1;并显式检查 cfst 可执行文件、输入文件、结果目录。
[原]位置:scripts/router_local_update.sh:315、scripts/run_update_and_commit.sh:161
手工运行 / cron / watchdog / systemd 可同时执行,共享结果 CSV、runtime 文件、state 文件。可能出现:A 读到 B 生成的 CSV;选中值来自一次运行而候选来自另一次;四个输出文件来自不同运行;Git 发布读到未完成快照。
建议:每实例锁文件(或 mkdir 锁),带超时与持有者信息;发布先整快照后一次性替换;服务器更新器与 Git 发布器共用同一把锁。
[原]位置:scripts/run_update_and_commit.sh:270-285
只要有一个可解析且匹配的 status 就判定"状态未变化"。若另一个 JSON 缺失或损坏,会提前跳过,无法自我修复缺失输出(原文档已复现 selected JSON 正常但 vars 缺失,及反向缺失)。
建议:统一发布文件清单;逐文件检查存在性、可解析性、schema、内容一致性;只有完整快照与远端一致才跳过。
[补] 非高峰调度对长间隔支持不正确(并修复 start == end 静默失效)位置:scripts/run_update_and_commit.sh:36-75
[原] 调度用小时内分钟数取模(curr_min % offpeak_int):2h 会在每小时边界运行而非每两小时;45min 产生不规则的小时内边界。
[补] 另有一处静默失效:当 peak_start == peak_end(如 --peak-start 19 --peak-end 19)时,L49-L51 把 in_peak 强制为 0,即视为"无高峰窗口",全部落入非高峰逻辑;而 install_debian.sh 的 validate_hour_range 会放行这个输入,不报错。
根源是:用一个固定周期 timer 模拟两个周期,再在脚本里用算术反推 —— 需要 40 行代码 + 文档专节(README §3.8.2)解释 Remainder / Tolerance 公式,这本身就是可维护性信号。
建议([补],比"修算术"更彻底):改用 两个 OnCalendar timer(systemd ≥ v235 支持时区后缀),把复杂度交给 systemd:
# vmess-domain-rotator-peak.timer
OnCalendar=*-*-* 19..23:0/10:00 Asia/Shanghai
# vmess-domain-rotator-offpeak.timer
OnCalendar=*-*-* 0..18:0/30:00 Asia/Shanghai
连带收益:run_update_and_commit.sh 删掉整个 peak 判定块与 PEAK_* 环境变量;TIMER_INTERVAL = min(...) 计算消失;README §3.8.2 整节删除。PEAK_* 从"每轮重新求值的逻辑"降级为"生成 timer 时的输入"。
[合]位置:scripts/domain_updater.py:919、scripts/domain_updater.py:1110
[原] 通知在输出写入后执行,但仍在主 try 内:通知失败会让整个运行进入异常分支,用旧值重新生成错误输出(把已成功的选优事务回滚了)。通知的 stdout 还会混入调度器解析的 JSON,使 updater_status 解析失败。
[补] 这条其实是 run_update_and_commit.sh 与 domain_updater.py 之间stdout JSON RPC 问题的实例。整条 shell ↔ Python 协议目前有三处:
| 调用 | 位置 | 用途 |
|---|---|---|
--print-output-settings |
L110-L118 | 取 5 个输出路径 |
updater stdout →json_field status |
L144-L159 | 取status |
repo_relpath 内联 heredoc |
L126-L142 | 路径转换(每个文件各启一次解释器) |
这是没有契约的进程间协议。具体隐患:只要 updater 往 stdout 多打一行日志,json_field status 返回空串 → L272 status_unchanged 保持 0 → 每次都强制提交。此降级是静默的。
建议([合]):
--result-file <path>,把 {selected, status, paths, ...} 写成文件;shell 只读文件,不再解析 stdout。update / commit),run_update_and_commit.sh 退化为薄薄的 git 封装。[合]位置:scripts/router_local_update.sh:18、scripts/router_local_http.sh:18
[原] 脚本直接 . "$CONFIG_PATH"。部分 BusyBox ash 中,不含 / 的路径只搜 PATH,不搜当前目录 —— 而 README §5.4 的手动命令正是这种形式(sh scripts/router_local_update.sh config_router.conf)。
[补] 这属于下面 P2-10「路径解析三实现不统一」的一个表现。建议:配置路径先规范化为绝对路径(或补 ./)。
[补] 三个"路径解析"实现语义各不相同| 实现 | 相对基准 | 规范化 |
|---|---|---|
| domain_updater.py:92-98 | 配置文件所在目录 | normpath |
| router_local_update.sh:63-70 | 当前工作目录(CWD) | 无 |
| router_local_http.sh:44-50 | 名义上配置文件目录,但RUNTIME_DIR 实走 CWD |
无 |
router_local_update.sh 的 resolve_path 只把文件名接到目录后;目录本身若为相对路径就保留为 CWD 相对 —— 即依赖调用者先 cd $VMESS_DIR。README §5.5 的 cron 恰好这么写,所以"恰好能用"。router_local_http.sh 里 resolve_path_from_config 只用于 BUSYBOX_BIN,RUNTIME_DIR 走另一条路。
风险:httpd -h 的目标目录与 update 的输出目录之间没有任何构造性保证,只靠约定。一旦调用方式变化(systemd、绝对路径调用、从别处 cd),就静默错位。
建议:统一为"所有相对路径相对配置文件所在目录"(Python 侧已是此语义),两脚本共用同一函数。
[原]位置:scripts/router_local_update.sh:25、scripts/router_local_http.sh:34
HTTP 脚本在 source 后修剪部分变量的 CR,更新脚本完全没处理。Windows / Web UI 编辑的配置可能携带回车,导致目录、二进制、state 路径异常。
建议:读取配置前统一转换或拒绝 CRLF;两脚本共用同一套加载 + 清洗逻辑;在校验阶段报出具体变量名,而不是等 cfst 执行失败。
[原]位置:scripts/router_local_http.sh:52-77
resolve_busybox() 在配置路径失效后还会依次尝试工作目录、打包目录、PATH。这掩盖配置错误,也违反 CLAUDE.md 的约定("使用配置中的 BusyBox 二进制的 httpd applet,不使用系统 httpd")。
建议:配置了 BUSYBOX_BIN 时只解析该路径;不存在/不可执行/无 httpd 直接失败;仅在未配置时才允许默认路径。
[补] BusyBox 路径的 schema 与 Python 不一致,且忽略 domain_filter位置:scripts/router_local_update.sh:123
awk 输出:
"location_country":"","location_city":"","host_provider":"","score_value":null,"scores":[],"created_raw":""
而 README §2 明确写着「有值才输出该字段,不再写空字符串/空数组/null 占位」,Python 侧 set_if_nonempty_text 也确实这么做了。同一个文件契约,两条实现路径产出两种 schema。
另有:BusyBox 路径完全没有实现 domain_filter(include_suffixes / exclude_regex 都没用到),只是 sed -n '2p' 取第一行。
建议:把这条并入 P1-5 / P2-2 的统一修复 —— 由共享契约与共享校验函数消除,而不是在两处分别对齐。
[补] router_local_update.sh 的 60 行 if 链位置:scripts/router_local_update.sh:220-281
每个 cfst 参数都要写一段 if [ -n "$CFST_X" ]; then set -- "$@" -flag "$CFST_X"; fi。当前 15 个参数 ≈ 60 行,且每加一个参数要改 3 处(config_router.conf、默认值表、这条 if 链)。
同时,Python 侧用 run_args 数组(config.local.json:8-14),shell 侧用散装变量 —— 同一件事两种表达。
建议:表驱动 + 与 Python 对齐。
# 变量名:cfst 参数:是否布尔开关
CFST_FLAG_MAP="
CFST_THREADS:-n:0
CFST_TEST_COUNT:-t:0
CFST_PORT:-tp:0
CFST_HTTPING:-httping:1
CFST_LATENCY_LIMIT:-tl:0
"
一个循环吃掉全部;加参数 = 表里加一行。进一步可让 config_router.conf 也用 CFST_RUN_ARGS,两模式共享同一份参数拼装规则。
[补] 更新器非零退出时无日志位置:scripts/run_update_and_commit.sh:161
set -euo pipefail 下,updater_output="$(...)" 若 Python 非零退出,脚本直接终止,没有任何 log_err 说明原因(stderr 里有 Python 的 JSON error,但在 journal 里不与调度日志关联)。建议包一层捕获,记录退出码与 stderr。
[原]位置:build_router_package.sh:115 — 修复了 BusyBox 与两个 shell 脚本的权限,没有修 cfst/cfst;最终校验只检查文件名存在。
建议:检查 cfst/cfst 是目标架构可执行文件;打包前 chmod +x 或权限不对直接失败;校验包内权限而非仅文件名。
位置:build_router_package.sh:109 — 支持自定义 PKG_ROOT_NAME,但配置原样复制,默认配置仍引用 /jffs/vmess/cfst、/jffs/vmess/state.json。包解压到其他目录后可能执行包外的旧安装。
建议:构建时生成与包根匹配的配置(或改为相对配置目录);包内加 manifest(版本、架构、配置根、schema 版本)。
位置:build_router_package.sh:120 — 先删旧归档再生成新归档;磁盘满/中断/校验失败会丢失原本可用的部署包。
建议:temp 生成 → 校验内容与权限 → 原子 rename 到 OUT_TAR。
[补] 构建不可复现位置:.gitignore
.gitignore 忽略 cfst*/、busybox*/,但 build_router_package.sh:16-17 的默认 BUSYBOX_SRC / CFST_SRC_DIR 正指向这两个被忽略的目录。新 clone 无法打包 —— 仓库里没有任何说明如何获取这些二进制,构建不可复现,也意味着 CI 无法验证打包脚本。
建议:二选一并写进文档 —— ①提供下载/获取步骤(带校验和与架构说明);②把二进制纳入版本控制(体积大但可靠,当前 vmess.tar.gz 已入库,说明体积可接受)。
[补] index.html 的维护逻辑在两条脚本里各写一遍位置:router_local_http.sh:118-125(ensure_index)与 router_local_update.sh:134-139(write_text_value 里 rm -f "$INDEX_PATH"; cp ... "$INDEX_PATH")
两处都在维护同一个 index.html 派生文件,逻辑重复且语义微妙(一个仅在文件缺失时补,一个每次覆盖)。建议并入统一发布函数,让 index.html 成为快照的一部分。
[原]将 Python 更新器拆分:
| 模块 | 职责 |
|---|---|
sources/ |
API、Python cfst、BusyBox 结果的采集适配器 |
models.py |
候选、观测、决策、发布状态的数据结构 |
policy.py |
资格过滤、去重、评分、排序、切换与回退策略 |
state.py |
实例隔离、锁、最后成功值、原子持久化 |
publishers/ |
本地快照、HTTP 文件发布、Git 提交与推送 |
cli.py |
配置校验、命令参数、退出码、日志格式 |
BusyBox 版可保留精简 shell 实现,但必须遵循同一份输出契约(状态、时间、指标单位)。
[补] 合并 cfst 与 api 到同一条管线当前 main() 是两个近乎独立的分支(domain_updater.py:1031-1070):cfst 分支完全跳过 record_filter / scoring / rank_scored_records,直接信任 cfst 自身的输出顺序。
但这套机制对 cfst 数据完全可用 —— to_float_or_none 能处理 "0.00" / "23.75" 这类字符串,loss_rate / avg_latency / download_speed 已在 load_cfst_rows 返回的 dict 里。
建议:让 cfst 也产出标准 record(field_map 为恒等映射),走同一条 filter → record_filter → score → rank → select 管线。
收益:
config.local.json 从此可声明"先看丢包率再看延迟",而不再依赖 cfst 内部排序这个隐式契约 —— 这是真实的能力缺口(目前无法表达"丢包率优先")。main() 两分支塌缩为一;choose_domain / choose_top_candidate_domains 的防御性 if ranked_scored: ... else: ... 一并消失。build_top_candidates 里 cfst/api 两套 row_map / record_map 逻辑合并。[原]updated_at 语义过宽。建议至少区分:
attempted_at — 最近一次尝试采集last_success_at — 最近一次取得有效观测selected_at — 当前值被选中published_at — 当前快照发布valid_until — 消费者允许使用该观测的截止时间status — ok / degraded / error / unavailablereason — api_timeout / last_good_fallback / invalid_payloadgeneration — 快照批次号schema_version — 输出契约版本失败回退不得刷新 last_success_at,否则过期数据看起来像刚刚成功测量。
[原]download_speed 可能是带单位的字符串,导致 Sub-Store 需要自行猜测 MB/s / Mbps / KB/s(见 operator_template.js:61-109 那段约 50 行的单位嗅探)。
建议输出统一数值字段:latency_ms、loss_ratio、download_mbps、score_value、measured_at。展示文本可另行保留,但业务决策只用数值字段。
[原]分三层:候选资格 → 排序评分 → 是否值得切换。
yd_score 是否越低越好,应以 API 语义为准,不能仅凭字段名猜测(当前 prefer_lower: true 是全局的)。[合] 迟滞(hysteresis):当前切换过于频繁原文档 §5.4 已提出"最小改善幅度"等概念。本次补充量化数据(来自 runtime-state 分支历史):
而每次切换的代价链很长:新值 → git commit → push → Git 服务器 → Sub-Store 拉取(5 分钟 TTL)→ 客户端重新解析。触发原因可能只是 yd_latency 变化 0.5ms。
建议:scoring 增加 switch_margin_pct: 10;main() 写文件前比对 last_good 是否仍在候选内且与新首选差距小于阈值 → 保持不动。这同时降低 commit 数量、Sub-Store 请求量与客户端重解析频率。这是对"业务流程"收益最集中的一条。
[原]采集本次结果
→ 生成 current value / state / vars / metadata
→ 校验所有文件与 schema
→ 原子替换本地运行时目录
→ Git / HTTP 发布
→ 记录发布结果
本地更新成功与远端发布成功必须是两个状态。远端发布失败不回滚已验证的当前值,但必须保留未发布版本并在下一轮重试(即 P1-3)。
[原]显式 domain 覆盖
→ 检查是否有匹配 VMess 节点
→ 并行获取允许的来源
→ 校验状态、时间、地址、有效期
→ 根据模式与策略选来源
→ 更新节点
具体:
default 模式下串行,两个 5s 超时可能叠加)。value / source / status / last_success_at / valid_until。error_use_last_good 默认不作为健康来源(即 P1-4)。$arguments.domain 最高优先级与 domain_override 开关。[补] 把硬编码常量改为 $arguments 参数位置:substore/operator_template.js:7-14
ROUTER_VALUE_JSON_URL / SERVER_VALUE_JSON_URL / ROUTER_MIN_SPEED_MB_PER_S / VALUE_SOURCE_MODE 全是常量,用户每次换模式或阈值都要编辑脚本内容。而仓库里已有正确模式 —— domain / domain_override 走 $arguments(L250-L276)。
建议同样参数化(带默认值):$arguments.mode / min_speed / router_url / server_url。CACHE_KEY 带上解析后的 mode 即保持缓存隔离语义。
附带信息:出厂默认
VALUE_SOURCE_MODE = "server_only",而router_url指向http://192.168.50.1:8080—— 意味着selectValueRouterFirst与parseRouterSpeedMBps这约 120 行在默认部署下是死代码。不算错误,但值得知道。
[补] top_candidates 被大量生产却几乎未被消费substore/operator_template.js 只读 domain / ip(L15),唯一例外是读 top_candidates[0].download_speed 一个标量(L61-L62)。
也就是说 base_top_candidate / build_cfst_candidate / build_api_candidate / build_top_candidates / set_if_nonempty_text / text_or_blank / maybe_resolve_field 这 7 个函数,加上 score_value / scores / created_raw 字段,本质上只为一个标量 + 人工排查服务。
两条路,选一条并写进文档:
项目当前主要依赖语法检查与手工运行(CLAUDE.md:「无专用测试框架」)。
[原] 回归测试清单2h、45min 和跨午夜调度符合配置语义。[补] 优先覆盖的纯函数零依赖(stdlib unittest 即可,符合本仓库不引三方库的风格)。这些函数目前完全没有测试,但它们是"选哪个域名"的唯一裁决者:
| 函数 | 值得测的点 |
|---|---|
rank_scored_records |
比较器很微妙:None 处理、asc/desc、数值与字符串的降级比较、tie_breaker 顺序 |
parse_scored_records |
加权求和的缺失字段分支;lexicographic 的 prefer_lower 默认方向 |
parse_created_time |
多格式 + 时区 offset + ISO 兜底 + 无时区时的 tz 注入 |
rule_matches |
contains / equals / regex × case_sensitive × 列表值展开 |
get_values_by_path |
[] 展开、缺失路径、非 dict 中间节点 |
apply_filter |
suffix / regex 组合 |
build_output_settings |
相对/绝对路径解析、legacy key 拒绝 |
[合][补]config.server.json 与 config.local.json 的 runtime/state/vars 目录(P1-2)。cd / cfst / 输入文件 / 结果文件(P2-4、P2-3)。error_use_last_good 的来源判断(P1-4)。[原]schema_version / last_success_at / valid_until / reason(6.3)。top_n 增加按域名去重与测量聚合规则。[合][补]docs/OUTPUT_CONTRACT.md。[补][补]router_local_update.sh 参数表驱动化(P2-14)。[补][补]validate / doctor 子命令,输出解析后路径、状态与契约检查结果。tests/(8.1、8.2)。[补]switch_margin_pct(6.6)。top_candidates 用于下游 failover(7.3)。yd_score 的真实业务方向(6.5)。build_output_settings 的 legacy key 拒绝逻辑(domain_updater.py:941-957)在配置迁移完成后是永久负担。runtime/ 与 cfip_runtime/ 的说法互相矛盾,且与出厂配置不完全一致。python3 -m py_compile scripts/domain_updater.py
bash -n scripts/run_update_and_commit.sh
bash -n scripts/install_debian.sh
bash -n scripts/uninstall_debian.sh
sh -n scripts/router_local_update.sh
sh -n scripts/router_local_http.sh
sh -n build_router_package.sh
node --check substore/operator_template.js
git diff --check
[合]原文档复现:
ok 候选。cd 失败后继续执行。本次合并复现/确认:
parse_domains 对 401 错误页返回 ['status.example.com']。UR6SYEEA… 存在于 commit ea38258,且 ea38258 是 main 的祖先。runtime-state 分支跟踪 state.json / substore_vars.json,与 config.local.json 输出路径冲突。tests/ 目录。[原][补] 运维观察:线上服务可能已停止runtime-state 分支 HEAD 停在 2026-09-15(一次 manual: 提交),上一次自动提交是 2026-07-27 —— 此后 2.5 个月无自动提交。
结合 P2-19 的提交分布(历史显示值平均每 ~3 小时一变),这段空白有三种解释,建议排查:
排查命令:
sudo systemctl status vmess-domain-rotator.timer # 定时器是否仍在运行
sudo journalctl -u vmess-domain-rotator.service -n 100 --no-pager # 近期是否报错
git ls-remote origin runtime-state # 远端是否与本地一致
| 主题 | 原文档 | 本次补充 | 状态 |
|---|---|---|---|
| API Token 泄露 | — | S0-1 | 新增 |
| API 错误响应被解析为域名 | P1 | 独立复现 | 一致 |
| Server/Local 状态互覆盖 | P1 | 补充"污染 runtime-state" | 一致并加深 |
| Git push 失败永久跳过 | P1 | 代码路径确认 | 一致 |
| Sub-Store 偏好过期 router fallback | P1 | 代码路径确认 | 一致 |
| BusyBox 非原子写入 | P1 | 指出与 Python_atomic_write 能力不对等 |
一致并加深 |
| 缺失评分字段仍被选中 | P2 | — | 原文档独有 |
| cfst 无地址校验 | P2 | 补充"选中值与候选列表不一致" | 一致并加深 |
| 旧 CSV 被当新结果 | P2 | — | 原文档独有 |
cd 失败后继续执行 |
P2 | — | 原文档独有 |
| 无互斥锁 | P2 | — | 原文档独有 |
| 发布同步检查不完整 | P2 | — | 原文档独有 |
| 调度长间隔语义 | P2 | 补充start==end 静默失效 + OnCalendar 方案 |
一致并加深 |
| 通知影响主事务 | P2 | 扩展为 shell↔Python stdout RPC 问题 | 一致并加深 |
| BusyBox 配置路径 / CRLF | P2 | 归并为"路径解析三实现不统一" | 一致并加深 |
| BusyBox 静默换二进制 | P2 | — | 原文档独有 |
| 打包(权限/路径/原子) | P2 ×3 | 补充"构建不可复现" | 一致并加深 |
| 状态字段语义 | §5.2 | — | 原文档独有 |
| 指标单位统一 | §5.3 | — | 原文档独有 |
| 选优三层规则 | §5.4 | 补充yd_score 方向存疑 |
一致并加深 |
| 迟滞/切换阈值 | §5.4 概念 | 量化数据 + switch_margin_pct |
一致并加深 |
| 统一快照发布 | §5.5 | — | 原文档独有 |
| Sub-Store 流程 | §6 | 补充参数化 + 节点预检 + 死代码说明 | 一致并加深 |
| 模块拆分 | §5.1 | — | 原文档独有 |
| 合并 cfst/api 管线 | — | 6.2 | 新增 |
| BusyBox schema 不一致 / 忽略 domain_filter | — | P2-13 | 新增 |
| 参数表驱动化 | — | P2-14 | 新增 |
top_candidates 未被消费 |
— | 7.3 | 新增 |
| Sub-Store 常量参数化 | — | 7.2 | 新增 |
| 构建不可复现 | — | P2-19 | 新增 |
index.html 逻辑重复 |
— | P2-20 | 新增 |
| 线上服务可能已停 | — | 10.4 | 新增 |
| 文档漂移 | — | 九.4/5 | 新增 |
| 回归测试清单 | §8 | 补充纯函数优先级表 | 一致并加深 |
两轮审查在以下判断上完全一致,不存在需要仲裁的分歧:
config.local.json → cfip_runtime/。本报告为只读审查结论,未修改仓库业务代码。建议第一轮只处理 S0-1(token)、P1-1~P1-5、P1-3(发布重试)与 10.4(线上状态排查),完成后再进入模块拆分。