ARCHITECTURE_REVIEW_RECHECK.md 43 KB

全项目架构与业务流程复审

[!IMPORTANT] 终审与修复状态更新(2026-10-13):
本复审报告的所有独有发现、与初审报告的分歧裁定已全部整合进唯一权威终审报告:ARCHITECTURE_REVIEW_UNIFIED.md。
全仓所有 P0 / P1 / P2 问题均已全部修复完毕 ✅,并在 tests/ 目录下建立了端到端自动化测试套件(18 项用例 100% 通过)。最新修复状态与代码台账请以终审报告为准。

审查日期:2026-10-10
代码基线:main@cedbe6c
对照文档:ARCHITECTURE_REVIEW.md

本文是独立复审,不覆盖原报告、不修改业务代码。原报告含有凭据和部署信息;本报告不重复凭据原文,也不上传外部服务。原报告中声称的历史复现,不自动视为本轮已经验证。

0. 结论先行

原报告的主线方向正确,但不能原样作为实施清单。 它抓到了错误成功、状态串用、push 重试和消费端降级问题;同时遗漏了危险清理路径、首次发布分叉、冷启动依赖及 fallback 绕过当前排除规则,并把若干可选重构或条件性现象写成了确定缺陷。

当前最重要的架构问题不是文件太长,而是没有把以下事实分开:

本次拿到了合法观测 ≠ 该地址符合当前策略 ≠ 本地快照写完 ≠ 远端发布成功 ≠ 下游拿到新鲜且可用的值。

建议优先处理:

优先级 问题 本文位置
P1 卸载空服务名、打包外部临时目录都可扩大递归删除范围 S-01、R-01
凭据处置 配置历史和原报告含完整 token;需轮换并脱敏 §2.2
P1 API 业务错误文本可作为有效候选;两份 Python 配置共享 state/vars §3.5
P1 fallback 可重新启用当前策略明确禁用的地址 P-01
P1 push 失败不重试,以及首次远端探测失败形成不兼容历史 S-02、§3.2
P1 Router 原地覆盖持久 state 和 HTTP 输出 §3.7
P1(特定权限部署) root 重装跟随服务用户可替换的 token 文件路径 S-06
P2 / 跨链路 数据新鲜度、稳定值心跳发布与消费 TTL 必须一起设计 §3.2、§4.1、§5.3
P2 配置/指标边界可绕过排名、禁用规则或 JSON 契约 P-02~P-07
P2 冷启动恢复、服务用户权限及调度实际间隔缺口 S-03~S-05、§3.3、R-02

“静态确认”不代表问题较轻;危险删除路径无需通过实际删除才能确认。条件性问题也不代表默认部署已触发,本文在各条中写明了前提。

1. 审查范围与证据标准

范围包含 Python 更新器、Server Git 发布与 systemd 生命周期、BusyBox Router 更新与 HTTP 服务、打包、三份配置、Sub-Store 和配套文档。

证据分级:

  • 离线复现:本轮以临时数据或内存 mock 触发实际代码路径。
  • 静态确认:从具体分支与调用关系可确定,但未在目标机器运行。
  • 条件性风险:依赖宿主行为、配置或目标设备,不能直接当作默认部署故障。
  • 设计建议:改善结构或业务策略,不算已发生的缺陷。

未进行真实 API 请求、cfst 测速、HTTP 服务启动、systemd 安装卸载、Git fetch/push。未执行 run_update_and_commit.sh,包括离线方式也未执行整个调度脚本。

2. 原报告首先需要修正的结论

2.1 撤回“线上可能已经停止”的现有证据链

原报告 §10.4 用本地 runtime-state 的 HEAD 日期推断线上停更,但本轮读取已有引用发现:

引用 本地可见最新提交时间
refs/heads/runtime-state 2026-09-15T01:40:44Z
refs/remotes/vmess-domain-rotator/runtime-state 2026-10-06T07:04:59Z

远端跟踪引用最新三条记录为 56404a9、1b89e6b、9e96c09,提交说明分别呈现正常、fallback、正常发布,且选中值相同。因此:

  1. “2026-07-27 后没有自动提交”被已有本地证据否定。
  2. 未执行 fetch,不能进一步宣称 2026-10-10 线上正常或异常。
  3. 提交次数不等于域名切换次数,原报告 §6.6 的切换频率不能直接由提交数推导,也不能据此给迟滞设置默认阈值。

2.2 凭据暴露属实,但不要扩大已知影响

本轮脱敏验证确认:配置含 token、对应历史提交存在且属于 main 祖先、原报告本身重复了完整 token。

建议作废并轮换、改用环境变量或受限文件注入;原 review 文档也要脱敏后才考虑提交或分享。凭据是否仍有效、API 是否有写权限、是否已被第三方使用,本轮均未验证,不能把“可能篡改候选”写成确定影响。

2.3 通知失败的后果被原报告扩大

domain_updater.py:919-925 使用 subprocess.run(..., shell=True, check=False)。

  • 通知命令普通的非零退出码不会抛出异常,也不会触发回滚;原报告 P2-8 的普遍性表述不成立。
  • 进程启动等真正抛异常的情况,才可能进入外层错误分支,并用运行开始时读到的旧 last-good 改写输出。
  • stdout 未隔离、没有超时依然是实际缺陷:日志会污染调度器 JSON 协议,长时间不退出的通知会延迟发布。
  • stdout 污染导致跳过判定失效,但 run_update_and_commit.sh:322-325 仍有 staged diff 检查。因此应写成“可能产生多余提交并丢失正确状态判断”,不能写成“必定每次强制提交”。

2.4 暂不把重构建议当作修复前提

以下事项必须与真实故障分开:

  • 显式的 POSIX if ... set -- 参数拼接虽长,但不天然比表驱动差;避免为减少行数引入 eval、不受控分词或新的配置语言。
  • server_only 下 Router 分支未执行,不等于死代码;它是可配置模式的正常实现。
  • top_candidates 有诊断、人工选择和未来消费价值,暂未全部被当前 JS 使用并不构成错误。不应直接增加“第二候选自动 failover”:仓库没有可靠的目标连通性/隧道兼容性验证机制。
  • API 与 cfst 可以统一候选接口、校验和状态契约,但不能未确认指标语义就强制采用同一套排名规则。cfst 原生排序也是一种可保留的明确策略。
  • 保留废弃配置键的明确拒绝有助于防止静默误配置,不应仅因它是迁移逻辑就删除。
  • vmess.tar.gz 当前不在 Git 跟踪文件中,原报告“该归档已入库”的论据不适用于当前基线。

3. 本轮业务链路核查

3.1 Server 发布与部署:新增问题

下列均为静态确认;没有执行整个调度器、安装器或卸载器。

S-01 / P1:卸载参数可使删除范围扩大到整个系统状态目录

位置:uninstall_debian.sh:40-68、uninstall_debian.sh:100-103。

SERVICE_NAME 没有非空、字符集、路径分隔符和删除目标归属校验。传入空服务名时,STATE_DIR 变为 /var/lib/;默认删除认证文件的分支会对该目录递归删除。空值可能来自部署变量或误用脚本参数。这不只是卸载失败,而是 root 权限下损坏其他服务状态。

不要实际运行此输入来验证。 应在任何 systemd/文件操作前拒绝无效服务名,限制为单一合法服务名称;删除前校验规范化路径必须位于预期的专属目录、且存在本安装器写入的归属标记。绝不能仅靠加引号防止此类目标错误。

S-02 / P1:远端探测失败被误判为没有发布分支

位置:run_update_and_commit.sh:222-231、run_update_and_commit.sh:355-373。

首次没有本地 runtime 分支时,认证失败、网络失败与“远端分支不存在”共用 orphan 初始化路径。如果远端实际已有发布历史,本地会建立另一套历史,push 非快进失败。之后因为本地分支已存在,不再 fetch;即使候选持续变化或者使用 --force-commit,也不能解决分叉。

区别于原报告 P1-3:旧问题是已提交但未推送的版本被跳过;此处是历史拓扑已错误,单纯增加重试仍不能恢复。

建议区分探测结果与错误,未知远端状态必须失败而非初始化;每轮发布前核对远端进度,明确单发布者约束和远端领先/分叉恢复政策。不要用无条件 force push 作为修复。

S-03 / P2:灾备 state 有发布路径,却没有冷启动恢复入口

位置:run_update_and_commit.sh:161、run_update_and_commit.sh:225-226、domain_updater.py:1023-1024。

fresh clone 或本地 state 丢失后,updater 先运行,发布分支随后才获取。首次 API 失败时没有本地 last-good,流程直接失败;即使远端有可用 state,也不会读取它。

这是灾备能力缺口,不等于现有本地 fallback 完全失效。建议明确恢复流程:受控地从可信发布分支恢复属于本实例、版本兼容且未过期的状态,再开始采集。远端 state 不能未经校验就当成当前健康观测。

S-04 / P2:部署用户可写 runtime,不代表可完成 Git 发布

位置:install_debian.sh:373-375、install_debian.sh:428、install_debian.sh:466-473。

root 所有的仓库指定非 root 服务用户,或者重装时更换用户,安装器只修正 runtime/服务 state 的归属。safe.directory 只是 Git 信任配置,不赋予 .git、refs、worktree 元数据写权限,配置文件的读取权限也未按服务用户验证。可能出现采集成功、发布阶段持续失败。

建议以实际服务用户检查配置、来源文件、输出、.git 与 worktree 操作权限;发现问题应拒绝安装或明确指导,不要默认递归 chown 整个用户仓库。

S-05 / P2:手动命令和 systemd 运行不是同一个部署实例

位置:install_debian.sh:430-453、install_debian.sh:512-516、run_update_and_commit.sh:204-209。

安装器输出的手工强制提交命令没有加载服务的 EnvironmentFile。自定义 remote、header-token 认证、禁用推送等配置会丢失,回落到脚本默认值。指定 remote 不存在时,脚本还会静默选第一个 remote,可能把输出发向非预期仓库。

建议提供遵循同一身份和配置的运维入口;明确 remote 配置错误应报错。禁用推送的部署不能因为改用手动入口就意外开启推送。

S-06 / P1(特定权限部署):root 重装可跟随服务用户可替换的 token 路径

位置:install_debian.sh:387-389、install_debian.sh:419-423。

header-token 模式下,token 所在目录归服务用户所有,root 重装使用普通重定向写入固定文件。若该非 root 用户控制此目录内的链接,重定向以及随后的 chown/chmod 会跟随链接影响目录之外的目标。此结论依赖“非 root 服务用户可操作目录,随后 root 执行带 token 的重装”,不等于默认单用户部署已经被利用。

建议将认证文件目录保留为 root 管理;凭据按服务读取需要设置属组/权限。写入使用安全临时文件与受控替换,拒绝不符合预期的链接和文件类型。本轮仅检查权限边界,未执行提权或破坏实验。

S-07 / P2:卸载的凭据清理声明超过实际行为

位置:install_debian.sh:398-411、uninstall_debian.sh:95-103。

credential-store 模式把凭据写入服务用户家目录或指定文件,卸载却只清理环境文件、服务 state 目录和 safe.directory。已保存的凭据条目、credential helper 设置可能保留。因此不能承诺默认卸载清除所有关联 token。

修复应记录本安装器创建的凭据条目与配置,针对性移除或明确提示。不要删除服务用户共享的整个 Git 凭据文件。

S-08 / P2:显式支持的“当前已在发布分支”路径无法复制输出

位置:run_update_and_commit.sh:216-217、run_update_and_commit.sh:301-304。

当当前检出分支等于 runtime 分支,work_dir=APP_DIR;进入同步时源和目标是同一个文件,cp 报错后 set -e 终止。该分支正常运行还需具备脚本与配置等文件,所以不是默认 main 部署必现,但现有“直接复用”分支并不正确。可跳过同文件复制,或者明确拒绝不支持的运行位置。

3.2 Server 原有结论:确认与补强

  • P1 / 未推送版本被永久跳过:地址和状态比较在 push 前,且比较本地 HEAD;确认原报告 P1-3。run_update_and_commit.sh:255-289
  • P2 / 发布完整性检查不全:selected JSON 或 vars 只要其中一个 status 匹配便可能提前退出;state 不参加完整性判断。确认原报告 P2-6。run_update_and_commit.sh:270-284
  • P2 / 稳定值不刷新远端观测:地址、status 不变时,最新候选、指标和时间均不发布。与下游 TTL 必须一起修复,这是原报告修复路线中的关键遗漏。
  • P2 / 并发快照不受保护:手工入口和其他调度器可与 updater/publisher 交错。正常的同一个 systemd oneshot 不会因为自己的 timer 激活就无限并发;不能把“缺锁”描述成默认 timer 必然重入。run_update_and_commit.sh:161、run_update_and_commit.sh:292-308

3.3 调度:不止长间隔错误,默认配置也受启动相位影响

证据:run_update_and_commit.sh:36-75、install_debian.sh:356-360、install_debian.sh:480-485。以下是算术推导,不是目标 systemd 运行实验:

参数与 tick 相位 门禁实际放行示例 问题
peak=10,offpeak=30;分钟 05/15/25/35/45/55 05、25、35、55 默认间隔也可能在边界两侧各运行一次
peak=10,offpeak=45;分钟 00/10/20/30/40/50 00、40、50 不是每 45 分钟
peak=10,offpeak=120;分钟 00/10/20/30/40/50 每小时 00 不是每 2 小时
peak=10,offpeak=120;分钟 07/17/27/37/47/57 非高峰全部跳过 容差完全错过边界
peak=30,offpeak=10 timer 每 10 分钟;高峰全部放行 高峰并非设置的 30 分钟

OnUnitActiveSec 不保证整点对齐;Persistent=true 也不能为纯 monotonic timer 提供 OnCalendar 式停机补跑。初次手动启动 service 仍经过门禁,不能把安装完成等同于已生成首份快照。

原报告还有两项需要纠正:

  1. start == end 在代码中明确注释为“无高峰窗口”,没有相反业务约定时不能判为错误,应补文档或显式校验政策。
  2. 两个固定 OnCalendar 示例只解决该示例的窗口和间隔,并不自动支持任意 45min/2h 或跨午夜配置。

应先确定“日历时刻运行”还是“距上次成功运行的间隔”语义,再生成正确 timer 或持久化调度状态。首次初始化、手工诊断和常规节流也应分开。

3.4 Python 选择与回退:新增问题

P-01 / P1:last-good 可以重新启用当前策略已明确禁用的地址

位置:domain_updater.py:1044-1075、domain_updater.py:1124-1144。静态确认。

state 保存 blocked.example,随后配置加入对该域名的明确排除;本轮没有其他合格候选时,流程直接重新选择 state 中的值。fallback 不再执行地址校验、硬性域名排除或实例归属检查。

需要区分:record_filter 全淘汰后并不会重新调用正则解析,但会取未经校验的 state。last-good 只说明过去被选中过,不能自动获得当前策略豁免。

建议把资格分为不可放宽的硬约束(合法地址、明确禁用、实例归属)和可按降级政策放宽的软约束(新鲜度、性能),所有 fallback 也必须满足硬约束。

P-02 / P2:top_n=0 不只隐藏候选,还会改变实际选择

位置:domain_updater.py:816-820、domain_updater.py:928-936、domain_updater.py:1028-1042。纯函数离线复现:

原始顺序:slow.example, fast.example
评分顺序:fast.example, slow.example
top_n=0
实际结果:('slow.example', [])

评分后的切片为空,代码回退到原始顺序第一项。Local 模式同样允许有选中值、空候选列表;下游速度门禁因而没有指标可读。足够小的负数也有类似问题,不能笼统声称每个负数都得到空列表。

应校验正整数,拒绝布尔值和被 int() 截断的浮点数。正常正整数条件下,本轮没有发现 Python 把不同候选的指标错配给选中值;不要虚构 random/round-robin 路径,当前代码没有实现它们。

P-03 / P2:非有限数仍可越过评分计算,破坏 JSON 契约

位置:domain_updater.py:660-686、domain_updater.py:704-738、domain_updater.py:72-74。离线复现:

  • 三个有限权重各为 1e308、指标各为 1,配置校验通过,但总分和总权重溢出,得到 NaN;经实际 build_top_candidates() 生成的候选 JSON 仍含裸 NaN。
  • 混合数值和文本比较产生循环:升序比较 2 < 10、10 < "1x"、"1x" < 2 都成立,不满足排序所需的一致关系。

另经静态检查,lexicographic 路径数值解析失败后保留原值,非有限数或错误类型可能转为文本参与排序。

极端权重不是默认配置故障,但数据/配置进入边界后不能产生宣称成功却无法被 JS JSON.parse() 读取的文件。建议明确字段类型,先淘汰无效指标;计算后再次验证有限性;序列化设置严格 JSON,并在写入前验证整个快照。

P-04 / P2:时间窗口会先淘汰有效旧记录,再保留无指标的新记录

位置:domain_updater.py:649-677、domain_updater.py:753-764。静态确认;需要启用 within_hours,当前 Server 值为 0。

一条昨天的有效观测、一条刚生成但缺指标的观测,开启一小时窗口后,只保留后者并可能输出 ok。所以问题不止原报告的“全部记录都缺指标”,也可能是存在完整观测却选中不合格新观测。

未来时间没有上界检查;没有任何近期记录时又会主动放宽到全部记录。放宽本身可以是业务策略,但必须显式表现为降级,并与“硬性有效期”区分。先建立候选资格,再应用新鲜度和排序。

P-05 / P2:Server 同样接受非法 IPv4 外观字符串

位置:domain_updater.py:271-295、config.server.json:60-64。解析纯函数离线复现:999.999.999.999 被保留。

原报告只强调 cfst 地址未校验;API 路径的域名正则也能接受这种数字串。当前 Server 排除的是“合法 IPv4”形式,并不能替代对非法数字地址的拒绝。应先区分地址类别并严格解析,不能因为非法 IPv4 不匹配合法 IP 正则,就把它当合法域名。

P-06 / P2:配置“通过校验”并不等于实际生效

位置:domain_updater.py:444-460、domain_updater.py:510-523、domain_updater.py:562-570。

离线复现:

  • 排除规则使用非法正则 [:配置校验通过,运行时返回不匹配,规则静默失效。
  • scoring.tie_breakers=null:校验通过,解析时迭代 None 抛出 TypeError。

静态确认的输出键冲突:domain_updater.py:973-975、domain_updater.py:1101-1108。若将 state_last_good_key 设为保留键 status,地址被状态字符串覆盖;下一次异常可以把 ok 当作地址发布。

应在运行前编译规则、统一可空语义,并校验保留字段与输出路径冲突。不需要为此引入大型配置框架。

P-07 / P2:state 自身异常可能让正常采集也无法启动

位置:domain_updater.py:25-34、domain_updater.py:1023-1027、domain_updater.py:1113-1144。静态确认。

语法有效但类型为 [] 或 null 的 state 不会回到默认对象,随后在主 try 外调用 .get() 直接退出。state 的 last-good 类型与地址也未校验。

另外,current/vars 已更新而 state 写入失败时,异常分支首先再次写同一 state;如果仍失败,回退写出流程不会完成。Python 的单文件原子写已有效降低截断风险,但不能替代 state schema 校验或多文件提交边界。

P-08 / P2:旧 CSV 误作新观测不仅存在于 BusyBox

位置:domain_updater.py:144-166。静态确认。

skip_run=false 时,Python 也只检查 cfst 退出码和结果文件是否存在。旧 CSV 已存在、命令返回 0 却未刷新结果时,会读取旧文件并给本次时间及 ok。默认 cfst 是否会在什么情景下这样退出仍需版本确认,但运行器没有建立文件属于本次采集的保证。

显式 skip_run=true 主动复用文件是另一种模式,不应一律认定为错误。正常测速应使用本轮唯一结果路径,成功后验证并发布。

3.5 Python 原有结论:确认与补强

  • 错误 JSON 文本成为候选已在本轮复现:业务错误消息中的 status.example.com 被正则兜底提取。准确前提是 HTTP 成功返回了业务错误;真实 HTTP 401/403 通常由 urllib 抛异常,而非照常交给解析器。
  • Server/Local 状态目录冲突已通过两次实际 --print-output-settings 确认:都使用相同的 runtime/state.json、runtime/substore_vars.json。约定与 README 不能替代当前真实配置。
  • 缺指标仍可输出 ok静态确认;修复应在“是否有资格参选”阶段,而非仅修改排序默认值。
  • top-N 按观测而非独立地址去重:重复域名的多条观测可能占满名额,候选构建又映射到该域名最佳观测,产生重复条目。原报告实施清单已提过去重,复审补充了实际形成机制。domain_updater.py:816-820、domain_updater.py:895-915

3.6 Router 与打包:新增问题

R-01 / P1:打包清理会删除调用者提供的整个临时目录

位置:build_router_package.sh:9、build_router_package.sh:46-49、build_router_package.sh:87-91。静态确认。

环境变量 TMP_DIR 可指向已经存在、包含其他数据的目录。cleanup 无条件删除整个目录,并在输入验证前就注册退出 trap;因此即使依赖缺失、根本没有开始打包,也会删除该目录。

不能靠提醒“只传空目录”消除这个清理边界问题。应在指定临时根下用 mktemp -d 创建本轮独占子目录,仅清理自己创建的目录,并避免清理未经验证的外部路径。不应实际执行危险目录输入验证此问题。

R-02 / P2:cfst 卡住会阻断重启后的 fallback 和 HTTP 恢复

位置:router_local_update.sh:312-324、README.md:388-403。静态确认。

更新器已读到持久化 last-good,但必须等待没有外层 deadline 的 cfst 返回,才会写 fallback。README 启动流程又等待更新器后才启动 HTTP。重启清空 /tmp 后,一次迟迟不返回的测速即可让 HTTP 长期没有服务;“最多重试 5 次”不能约束单次耗时。

建议先按可接受的降级策略恢复一份完整快照、启动可用的读取服务,再进行有总超时的采集。正常“cfst 非零返回 + 可用持久 state”本来就能恢复,不能笼统说 Router 重启一定失去 fallback。

R-03 / P2:路径可配置,但输出文件与发布根的约束没有校验

位置:router_local_update.sh:63-80、router_local_update.sh:134-173、router_local_http.sh:41、router_local_http.sh:118-124。静态确认,包含三个具体输入:

  1. VALUE_TEXT_FILE 为绝对路径:更新器按绝对路径写,HTTP 却拼成 runtime 加该路径,找不到源文件或保留旧 index。
  2. VALUE_TEXT_FILE=index.html:先写当前值,随后把同一文件当派生 index 删除,再复制失败;成功与回退发布都可被中断。
  3. state/vars/current 使用尚不存在的嵌套父目录:仅 runtime 根会创建,写入在半途失败,形成新 current、旧 state 或旧 vars。

建议统一相对路径基准;逐一预检目录、可写性和规范化路径是否相互冲突。位于 docroot 外的文件不会因为配置了绝对路径就自动获得 HTTP 地址,必须显式区分持久化路径和公开路径。

R-04 / P2:候选字符串转义不足,可写出非法 JSON

位置:router_local_update.sh:103-123。审查子任务离线复现:从源码提取 awk 程序,用合成 CSV 输入;普通 region 可解析,region 内部含 tab 时输出带未转义控制字符,JSON 解析失败。

脚本没有输出解析/契约检查,因而可以将不可消费的 current 文件标为 ok。这不是正常 cfst 数据必现故障,而是输入边界缺失。不要仅修 tab;应采用覆盖 JSON 控制字符的可靠转义,并在发布前校验。

R-05 / P2(条件性):state 是合法 JSON,但正则读不出 last-good

位置:router_local_update.sh:82-88。静态确认。

外部工具把状态保存为 "last_good_ip" : "...",冒号前有合法 JSON 空白,正则即无法提取值。测速失败时会被当作没有 last-good。自身生成的格式满足该正则,所以这是手工恢复、格式化和跨生产者兼容性问题,而非默认自写自读失败。

R-06 / P2(目标设备待验):watchdog 可能匹配到自己的命令

位置:README.md:405-406。若设备 ps 展示完整 cron shell 命令,启动分支里未隐藏的 router_local_http.sh 字符串可能使检测匹配自身,即使服务不在也不重启。[r] 技巧只能避免 grep 的搜索参数自匹配,不能解决同一命令另一处出现完整进程名。

应使用可靠的 PID/进程归属或服务监督机制。实际触发取决于目标 BusyBox 的 ps 输出和截断,不能宣布现有设备已经受影响。

R-07 / 条件性安全风险:HTTP 暴露的是目录,不是声明的文件清单

位置:router_local_http.sh:130-131、config_router.conf:25-36。数字端口配置未限定监听接口,默认暴露 docroot 中可读文件;可达性与认证还受设备防火墙/BusyBox 配置影响。

不能直接宣称公网泄露;默认持久 state 位于 docroot 外,也不能声称它被默认 HTTP 暴露。建议明确 LAN 绑定、允许访问方以及只放可公开输出的 docroot,避免部署时把凭据或其他文件放入同目录。

3.7 Router 原报告核对:确认与纠偏

原报告主题 本轮结论
P1-5 原地写 state/HTTP 输出 确认。文本/index、current、state、vars 顺序覆盖,可能截断或混批。
P2-2 非法地址进入 state 确认非空即接受。但“第二行空、第三行有效”实际进入 fallback/失败,不是成功发布空地址;原报告的例子需修正。
P2-3 旧 CSV 误作本轮结果 静态确认,具体 cfst 无结果行为待目标版本验证;显式 skip-run 不算错误。
P2-4 cd 失败仍执行 源码确认;审查子任务在条件调用的最小 sh 逻辑中复现 set -e 不会可靠中止。
P2-5 无互斥 确认。CSV 的首选、计数、候选分次读取,重叠更新会错配。watchdog 自身只启动 HTTP,不是直接启动更新器。
P2-9 配置不含斜杠 可移植性问题成立,点命令的 PATH 查找不等于前置 -r 检查;具体 shell 行为待设备确认。
P2-10 相对路径基准 确认 CWD 与配置目录语义混用;当前默认绝对路径和 README 的 cd 掩盖部分触发条件。
P2-11 CRLF 确认处理不统一。source 期间的回车空行可能先触发失败,不能指望之后修剪变量补救。
P2-12 BusyBox fallback 配置失效后会尝试其他 BusyBox,确认;但不是调用系统独立 httpd,成功日志会打印选中的二进制。
P2-13 忽略 domain_filter 撤回这一缺陷表述:shell 配置没有该选项,也已转发 cfst 原生过滤参数。新增相同功能是能力扩展。空字段输出与稀疏字段文档不一致则仍成立。
P2-14 60 行参数拼接 可维护性讨论,不独立列 P2;现有带引号的 set -- 是正确 POSIX 做法。
P2-16 cfst 可执行权限 确认,打包只验证文件名且未补 cfst 执行权限。
P2-17 自定义安装根 确认,配置原样复制仍可能指向旧安装。
P2-18 先删旧归档 确认,生成或校验失败会丢失旧可用包。
P2-19 构建依赖完全未说明 修正:README 已列架构/组件要求和来源目录覆盖方式;缺的是获取步骤、版本与校验和。CI 可提供依赖或 mock,不能说完全无法验证。
P2-20 index 只缺失才补 修正:HTTP 在源文本存在时每次删掉并重建 index,不是仅缺失时补建。

另有两处重要文档/状态语义漂移:

  • Router 正常 fallback 通常保留原有 state;同一地址持续成功时也可能不刷新持久 state。其 updated_at 不能直接当“最近一次成功测量”。这与减少闪存写入有关,应明确 checkpoint 策略,而非机械要求每个派生文件每次都写。
  • workflow.md:75-100 的失败状态写入/HTTP 状态输出展示、README 部分 cfip_runtime/ 维护路径,与当前 /tmp runtime 加独立持久 state 不完全一致。

构建主机不能执行另一架构 BusyBox 时,也不能据此断言目标二进制缺少 httpd applet;应区分“无法在本机检查”和“目标明确不支持”。

4. Sub-Store:已复现的消费端问题

4.1 默认 Server-only 也缺少新鲜度与健康资格检查

证据:operator_template.js:171-180、operator_template.js:282-291。

离线复现:输入 {domain: "stale.example", status: "ok", updated_at: "2000-01-01T00:00:00Z"},节点被改写成 stale.example。

原报告 P1-4 只强调 Router fallback 抢占健康 Server,遗漏了默认部署:生产任务停止而静态 JSON 仍可访问时,5 分钟缓存到期只是重读同一份旧文件,不能证明数据新鲜。即使最近请求成功,也不代表最近测速成功。

建议:按来源校验 last_success_at 与有效期,明确模式内的降级政策。server_only 不应擅自切到 Router;没有合格值时可以保持原订阅节点,或者在显式配置的期限内使用降级值。

重要约束:不能孤立给 JS 加 TTL;必须同时处理发布端的心跳/新鲜度传播,否则健康且长期不换域名的 Server 会被误判过期。

4.2 动态来源缺少地址类型和格式校验

证据:operator_template.js:47-58、operator_template.js:303-305。

离线复现:{domain: {bad: true}, status: "ok"} 被字符串化成 [object object],写入符合名称规则的 VMess 节点 server,同时进入缓存。

显式覆盖已有 IPv4/IPv6/域名校验,但 HTTP 获取的数据没有相同边界。错误类型、非法 IP、含协议或路径的字符串不应通过 String(...) 自动转成“地址”。生产端校验不能替代消费者校验。

建议:先验证顶层为对象、地址字段为非空字符串,再使用一致的地址解析器;坏来源进入失败分支,不污染节点或缓存。

4.3 原报告 Router fallback 优先问题得到确认

证据:operator_template.js:118-163。

离线复现:Router 返回 ip=192.0.2.1, status=error_use_last_good 且没有测速指标,Server 返回健康值;default 仍选择 Router。状态只记入 telemetry,缺少速度不触发低速条件。

修复应按“健康且新鲜 → 有期限的降级值 → 保留原节点”的明确政策排序,而不是简单把所有 fallback 永久禁用。Router-only、Server-only、default 必须各自有明确降级边界。

4.4 缓存隔离不覆盖来源和策略

证据:operator_template.js:13、operator_template.js:282-291。

条件性风险;共享缓存模型下已复现:两个同为 server_only、URL 不同的操作符使用同一 Map 模拟宿主缓存;A 写入后,B 不请求自己的来源,直接使用 A 的值。

影响取决于实际 Sub-Store 宿主的缓存命名空间;本轮未启动真实 Sub-Store,因此不宣称所有部署都跨实例串值。至少缓存键没有表达 URL、阈值和策略版本;编辑配置时同模式旧缓存可能继续生效。

建议:缓存键包括有效来源 URL、模式与影响选择的策略,缓存值保留来源、观测时间、状态和有效期,而不只保存裸地址。

4.5 速度解析的次要边界风险

证据:operator_template.js:62-76。

  • 数值 0 被 || "" 转为空,绕过低速判断;当前生产者通常输出字符串指标,因此这是数字化契约迁移时必须防止的回归,不列为默认部署阻断项。
  • 文本 -10 MB/s 的正则只取 10,被视作正速度;这是损坏输入的健壮性缺陷,不证明 cfst 正常输出会产生负速度。

应统一数值及单位契约,并校验完整输入,而非提取其中第一个数字。

4.6 已验证必须保持的正确行为

  • $arguments.domain 合法时最高优先级生效,且不访问网络。
  • domain_override=false 只关闭显式覆盖,恢复正常来源选择。
  • 无符合条件的节点时仍会请求来源:确认原报告建议有效,但这是可优化的无效工作,不应与数据损坏混列同一优先级。

5. 架构结论与修复顺序

5.1 优先建立四条业务不变量,而不是先拆目录

  1. 选择资格:任何写入 ok 或 last-good 的值,都来自本次合法、通过业务筛选的观测;错误响应文本、旧 CSV 和损坏数据不能补成“成功”。
  2. 状态归属:一个实例只能读取/更新自己的状态;采集失败不得刷新 last_success_at,通知失败不得撤销已完成的选择。
  3. 快照一致性:当前地址、候选、指标和状态描述同一批观测;单文件原子替换不等于多个文件形成事务。
  4. 交付闭环:本地写入、Git commit、远端 push、下游获取是不同阶段;未确认发布的版本必须可重试,“地址没变”不能代表交付成功或数据新鲜。

5.2 保留两种运行环境,统一可验证的契约

Python Server、Python cfst 和 BusyBox cfst 是三个采集/运行路径,但运行时数据的生产实现主要是 Python 与 shell 两套,JS 是消费者。Router 无 Python、POSIX sh、闪存与内存分离等约束应保留。

建议先约定最小公共快照:版本、实例、批次、选中地址、状态、最近成功观测时间、有效期、原因。指标明确单位并绑定具体候选;不要把完整诊断数据塞进 substore_vars.json。

随后再按已有职责提取配置校验、来源适配、选择策略、状态持久化与发布边界。不要为了消除代码重复而让路由器依赖 Python,也不要把 cfst 原生排序强制转换成 Server 的业务评分规则。跨实现的一致性主要用共享 fixture 和契约测试保证。

5.3 发布修复必须成套设计

  • 锁:以实例为粒度,覆盖采集临时结果、状态读取、快照写入和发布器取快照;仅锁某个 JSON 不足够。
  • 原子性:HTTP 的首要消费 JSON 可以作为权威快照,用同目录临时文件加 rename 发布;其余派生文件带相同 generation 并校验一致性。需要多文件同时切换时再设计版本目录加指针。
  • 持久化边界:Router 的 /tmp 输出和持久 state 可能跨文件系统,不能声称一次 rename 能原子提交两者。持久 state 要单独保证完整写入和可恢复性。
  • Git:内容变化判断、完整性修复、未推送重试、定期新鲜度发布是四个独立条件。先对待发布快照校验,再确认远端包含对应提交。
  • 有效期:根据正常采集周期、允许错过的轮数和分发延迟设定;不能用任意的 5 分钟消费缓存 TTL 代替观测寿命。稳定地址也要有可传播的健康心跳。

不要直接承诺“原子替换整个非空运行时目录”:跨平台目录替换、HTTP 正在访问和外置 state 都需要单独设计。

5.4 分阶段落地

阶段 A:先止住错误成功和永久未发布

先封住卸载与打包的越界清理入口;修复特定权限部署下的凭据写入边界并轮换暴露凭据;隔离实例;严格校验输入并保留筛选失败语义;修复回退与输出写入;修复 pending push 重试及错误分支初始化;给当前最高风险路径补离线回归。

阶段 B:打通状态到消费者的闭环

明确成功观测时间与有效期;补健康心跳发布;统一来源资格与降级政策;锁住快照边界;补冷启动、重启和输出缺失恢复测试。

阶段 C:收敛结构与运维

修复部署参数、权限、目录和生命周期问题;补配置校验及诊断入口;提取稳定边界,而非一次性重写全部脚本。最后再讨论迟滞、并行请求与更多下游能力。

迟滞需要实际的候选排名变化、测量噪声和连接影响数据,不能仅根据 Git 提交数决定阈值。

6. 验证清单与未覆盖边界

6.1 本轮主会话实际执行并通过

Python py_compile
两份 Python 配置 --print-output-settings
三个 Server Bash 脚本 bash -n
两个 Router 脚本及打包脚本 sh -n
Sub-Store 脚本 node --check

路径检查实际证实 Server 与 Local 的 state/vars 重合,而不是根据旧报告推测。

纯函数/内存模拟实际观察到:

  • Python top_n=0 改选原始第一项;非法数字地址通过解析;数值/文本比较形成循环。
  • 非法排除正则校验通过、匹配时返回 False;tie_breakers=null 校验通过后触发 TypeError。
  • 有限权重运算后得到 NaN,实际候选输出结构序列化仍含裸 NaN。
  • API 业务错误消息中的域名成为候选。
  • Sub-Store 将对象字段变成 [object object] 地址;接受多年以前的 Server 快照;Router fallback 优先于健康 Server。
  • 共享缓存 mock 下不同来源 URL 串值;数值零与负速度文本解析边界。
  • 合法显式覆盖不访问网络,关闭覆盖恢复正常选择;没有合格节点仍发请求。

这些模拟以暴露当前缺陷为目标;复现断言通过不等于业务行为正确。

6.2 审查子任务提供的验证

Router 子任务完成脚本语法检查,以及两个最小离线案例:条件上下文中的 set -e 不可靠中止、awk 候选字段内部 tab 生成非法 JSON。主会话另外重跑了所有规定的 shell/JS 语法检查。

Server/Python 子任务的部分命令受工具超时阻止,未把未运行的测试计入成果;主会话随后补做了上面列明的语法、路径及 Python 纯函数验证。

6.3 失败尝试与证据边界

  • 一次 NaN 实验最初直接序列化内部评分记录,因其含 datetime 而失败,报 TypeError: Object of type datetime is not JSON serializable。这是测试夹具选错层级,不是生产输出的新增缺陷;改为调用实际候选构造函数后验证了 NaN 问题。
  • 尝试运行含完整 main() mock 的实验时工具超时,因此 fallback 硬排除绕过、异常 state 在主 try 外崩溃等仍标为静态确认,不称已端到端复现。
  • 危险清理、安装权限边界只审查源码,未执行删除或提权验证。
  • 没有访问实时远端,因此本地 remote-tracking ref 只能纠正原报告的历史推断,不能证明当前线上服务状态。
  • 没有测试真实 API 成功码/指标含义、具体 cfst 版本的无结果行为、目标 BusyBox ash/ps/httpd、systemd 时序、路由器掉电持久性或真实 Sub-Store 缓存隔离。

6.4 建议固化的最小回归集合

  1. 资格过滤、评分缺失、非有限数、非法配置全部在写 state 前失败或明确降级。
  2. last-good 经过硬约束校验;不同实例不互读;损坏/错误类型 state 不阻止新的正常采集。
  3. 顶层地址、第一候选、指标身份一致;top-N 按独立地址聚合。
  4. cfst 超时、非零、零退出但无新文件、冷启动 RAM 空目录都有明确结果。
  5. 任何一个写入阶段失败不向消费者暴露伪完整快照;并发更新有互斥。
  6. Git push 失败后值不变仍重试;远端探测失败不建 orphan;远端领先、缺输出、稳定值心跳都有测试。
  7. 调度覆盖任意启动相位、默认 10/30、45min、2h、跨午夜及 peak 比 offpeak 更慢的情况。
  8. Sub-Store 覆盖健康/降级/过期/格式错误来源、不同 URL 缓存、无匹配节点、手工覆盖最高优先级。
  9. 安装/卸载/打包仅在自建隔离 fixture 上验证参数和清理范围,绝不以真实系统目录作测试目标。

可以使用标准库 unittest、Node 内存 VM 和受控 shell fixture;本轮没有引入测试框架或改动业务代码。