# 全项目架构与业务流程复审 > [!IMPORTANT] > **终审与修复状态更新(2026-10-13)**: > 本复审报告的所有独有发现、与初审报告的分歧裁定已全部整合进唯一权威终审报告:[`ARCHITECTURE_REVIEW_UNIFIED.md`](./ARCHITECTURE_REVIEW_UNIFIED.md)。 > 全仓所有 **P0 / P1 / P2 问题均已全部修复完毕 ✅**,并在 `tests/` 目录下建立了端到端自动化测试套件(18 项用例 100% 通过)。最新修复状态与代码台账请以终审报告为准。 审查日期:2026-10-10 代码基线:`main@cedbe6c` 对照文档:[ARCHITECTURE_REVIEW.md](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](scripts/domain_updater.py#L919-L925) 使用 `subprocess.run(..., shell=True, check=False)`。 - 通知命令普通的非零退出码**不会抛出异常,也不会触发回滚**;原报告 P2-8 的普遍性表述不成立。 - 进程启动等真正抛异常的情况,才可能进入外层错误分支,并用运行开始时读到的旧 last-good 改写输出。 - stdout 未隔离、没有超时依然是实际缺陷:日志会污染调度器 JSON 协议,长时间不退出的通知会延迟发布。 - stdout 污染导致跳过判定失效,但 [run_update_and_commit.sh:322-325](scripts/run_update_and_commit.sh#L322-L325) 仍有 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](scripts/uninstall_debian.sh#L40-L68)、[uninstall_debian.sh:100-103](scripts/uninstall_debian.sh#L100-L103)。 `SERVICE_NAME` 没有非空、字符集、路径分隔符和删除目标归属校验。传入空服务名时,`STATE_DIR` 变为 `/var/lib/`;默认删除认证文件的分支会对该目录递归删除。空值可能来自部署变量或误用脚本参数。这不只是卸载失败,而是 root 权限下损坏其他服务状态。 **不要实际运行此输入来验证。** 应在任何 systemd/文件操作前拒绝无效服务名,限制为单一合法服务名称;删除前校验规范化路径必须位于预期的专属目录、且存在本安装器写入的归属标记。绝不能仅靠加引号防止此类目标错误。 #### S-02 / P1:远端探测失败被误判为没有发布分支 位置:[run_update_and_commit.sh:222-231](scripts/run_update_and_commit.sh#L222-L231)、[run_update_and_commit.sh:355-373](scripts/run_update_and_commit.sh#L355-L373)。 首次没有本地 runtime 分支时,认证失败、网络失败与“远端分支不存在”共用 orphan 初始化路径。如果远端实际已有发布历史,本地会建立另一套历史,push 非快进失败。之后因为本地分支已存在,不再 fetch;即使候选持续变化或者使用 `--force-commit`,也不能解决分叉。 区别于原报告 P1-3:旧问题是已提交但未推送的版本被跳过;此处是**历史拓扑已错误**,单纯增加重试仍不能恢复。 建议区分探测结果与错误,未知远端状态必须失败而非初始化;每轮发布前核对远端进度,明确单发布者约束和远端领先/分叉恢复政策。不要用无条件 force push 作为修复。 #### S-03 / P2:灾备 state 有发布路径,却没有冷启动恢复入口 位置:[run_update_and_commit.sh:161](scripts/run_update_and_commit.sh#L161)、[run_update_and_commit.sh:225-226](scripts/run_update_and_commit.sh#L225-L226)、[domain_updater.py:1023-1024](scripts/domain_updater.py#L1023-L1024)。 fresh clone 或本地 state 丢失后,updater 先运行,发布分支随后才获取。首次 API 失败时没有本地 last-good,流程直接失败;即使远端有可用 state,也不会读取它。 这是**灾备能力缺口**,不等于现有本地 fallback 完全失效。建议明确恢复流程:受控地从可信发布分支恢复属于本实例、版本兼容且未过期的状态,再开始采集。远端 state 不能未经校验就当成当前健康观测。 #### S-04 / P2:部署用户可写 runtime,不代表可完成 Git 发布 位置:[install_debian.sh:373-375](scripts/install_debian.sh#L373-L375)、[install_debian.sh:428](scripts/install_debian.sh#L428)、[install_debian.sh:466-473](scripts/install_debian.sh#L466-L473)。 root 所有的仓库指定非 root 服务用户,或者重装时更换用户,安装器只修正 runtime/服务 state 的归属。`safe.directory` 只是 Git 信任配置,不赋予 `.git`、refs、worktree 元数据写权限,配置文件的读取权限也未按服务用户验证。可能出现采集成功、发布阶段持续失败。 建议以实际服务用户检查配置、来源文件、输出、`.git` 与 worktree 操作权限;发现问题应拒绝安装或明确指导,不要默认递归 chown 整个用户仓库。 #### S-05 / P2:手动命令和 systemd 运行不是同一个部署实例 位置:[install_debian.sh:430-453](scripts/install_debian.sh#L430-L453)、[install_debian.sh:512-516](scripts/install_debian.sh#L512-L516)、[run_update_and_commit.sh:204-209](scripts/run_update_and_commit.sh#L204-L209)。 安装器输出的手工强制提交命令没有加载服务的 EnvironmentFile。自定义 remote、header-token 认证、禁用推送等配置会丢失,回落到脚本默认值。指定 remote 不存在时,脚本还会静默选第一个 remote,可能把输出发向非预期仓库。 建议提供遵循同一身份和配置的运维入口;明确 remote 配置错误应报错。禁用推送的部署不能因为改用手动入口就意外开启推送。 #### S-06 / P1(特定权限部署):root 重装可跟随服务用户可替换的 token 路径 位置:[install_debian.sh:387-389](scripts/install_debian.sh#L387-L389)、[install_debian.sh:419-423](scripts/install_debian.sh#L419-L423)。 header-token 模式下,token 所在目录归服务用户所有,root 重装使用普通重定向写入固定文件。若该非 root 用户控制此目录内的链接,重定向以及随后的 chown/chmod 会跟随链接影响目录之外的目标。此结论依赖“非 root 服务用户可操作目录,随后 root 执行带 token 的重装”,不等于默认单用户部署已经被利用。 建议将认证文件目录保留为 root 管理;凭据按服务读取需要设置属组/权限。写入使用安全临时文件与受控替换,拒绝不符合预期的链接和文件类型。本轮仅检查权限边界,未执行提权或破坏实验。 #### S-07 / P2:卸载的凭据清理声明超过实际行为 位置:[install_debian.sh:398-411](scripts/install_debian.sh#L398-L411)、[uninstall_debian.sh:95-103](scripts/uninstall_debian.sh#L95-L103)。 credential-store 模式把凭据写入服务用户家目录或指定文件,卸载却只清理环境文件、服务 state 目录和 safe.directory。已保存的凭据条目、credential helper 设置可能保留。因此不能承诺默认卸载清除所有关联 token。 修复应记录本安装器创建的凭据条目与配置,针对性移除或明确提示。不要删除服务用户共享的整个 Git 凭据文件。 #### S-08 / P2:显式支持的“当前已在发布分支”路径无法复制输出 位置:[run_update_and_commit.sh:216-217](scripts/run_update_and_commit.sh#L216-L217)、[run_update_and_commit.sh:301-304](scripts/run_update_and_commit.sh#L301-L304)。 当当前检出分支等于 runtime 分支,`work_dir=APP_DIR`;进入同步时源和目标是同一个文件,`cp` 报错后 `set -e` 终止。该分支正常运行还需具备脚本与配置等文件,所以不是默认 main 部署必现,但现有“直接复用”分支并不正确。可跳过同文件复制,或者明确拒绝不支持的运行位置。 ### 3.2 Server 原有结论:确认与补强 - **P1 / 未推送版本被永久跳过**:地址和状态比较在 push 前,且比较本地 HEAD;确认原报告 P1-3。[run_update_and_commit.sh:255-289](scripts/run_update_and_commit.sh#L255-L289) - **P2 / 发布完整性检查不全**:selected JSON 或 vars 只要其中一个 status 匹配便可能提前退出;state 不参加完整性判断。确认原报告 P2-6。[run_update_and_commit.sh:270-284](scripts/run_update_and_commit.sh#L270-L284) - **P2 / 稳定值不刷新远端观测**:地址、status 不变时,最新候选、指标和时间均不发布。与下游 TTL 必须一起修复,这是原报告修复路线中的关键遗漏。 - **P2 / 并发快照不受保护**:手工入口和其他调度器可与 updater/publisher 交错。正常的同一个 systemd oneshot 不会因为自己的 timer 激活就无限并发;不能把“缺锁”描述成默认 timer 必然重入。[run_update_and_commit.sh:161](scripts/run_update_and_commit.sh#L161)、[run_update_and_commit.sh:292-308](scripts/run_update_and_commit.sh#L292-L308) ### 3.3 调度:不止长间隔错误,默认配置也受启动相位影响 证据:[run_update_and_commit.sh:36-75](scripts/run_update_and_commit.sh#L36-L75)、[install_debian.sh:356-360](scripts/install_debian.sh#L356-L360)、[install_debian.sh:480-485](scripts/install_debian.sh#L480-L485)。以下是**算术推导**,不是目标 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](scripts/domain_updater.py#L1044-L1075)、[domain_updater.py:1124-1144](scripts/domain_updater.py#L1124-L1144)。**静态确认**。 state 保存 `blocked.example`,随后配置加入对该域名的明确排除;本轮没有其他合格候选时,流程直接重新选择 state 中的值。fallback 不再执行地址校验、硬性域名排除或实例归属检查。 需要区分:`record_filter` 全淘汰后并不会重新调用正则解析,但会取未经校验的 state。last-good 只说明过去被选中过,不能自动获得当前策略豁免。 建议把资格分为不可放宽的硬约束(合法地址、明确禁用、实例归属)和可按降级政策放宽的软约束(新鲜度、性能),所有 fallback 也必须满足硬约束。 #### P-02 / P2:`top_n=0` 不只隐藏候选,还会改变实际选择 位置:[domain_updater.py:816-820](scripts/domain_updater.py#L816-L820)、[domain_updater.py:928-936](scripts/domain_updater.py#L928-L936)、[domain_updater.py:1028-1042](scripts/domain_updater.py#L1028-L1042)。**纯函数离线复现**: ```text 原始顺序: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](scripts/domain_updater.py#L660-L686)、[domain_updater.py:704-738](scripts/domain_updater.py#L704-L738)、[domain_updater.py:72-74](scripts/domain_updater.py#L72-L74)。**离线复现**: - 三个有限权重各为 `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](scripts/domain_updater.py#L649-L677)、[domain_updater.py:753-764](scripts/domain_updater.py#L753-L764)。**静态确认;需要启用 `within_hours`,当前 Server 值为 0**。 一条昨天的有效观测、一条刚生成但缺指标的观测,开启一小时窗口后,只保留后者并可能输出 `ok`。所以问题不止原报告的“全部记录都缺指标”,也可能是存在完整观测却选中不合格新观测。 未来时间没有上界检查;没有任何近期记录时又会主动放宽到全部记录。放宽本身可以是业务策略,但必须显式表现为降级,并与“硬性有效期”区分。先建立候选资格,再应用新鲜度和排序。 #### P-05 / P2:Server 同样接受非法 IPv4 外观字符串 位置:[domain_updater.py:271-295](scripts/domain_updater.py#L271-L295)、[config.server.json:60-64](config.server.json#L60-L64)。**解析纯函数离线复现**:`999.999.999.999` 被保留。 原报告只强调 cfst 地址未校验;API 路径的域名正则也能接受这种数字串。当前 Server 排除的是“合法 IPv4”形式,并不能替代对非法数字地址的拒绝。应先区分地址类别并严格解析,不能因为非法 IPv4 不匹配合法 IP 正则,就把它当合法域名。 #### P-06 / P2:配置“通过校验”并不等于实际生效 位置:[domain_updater.py:444-460](scripts/domain_updater.py#L444-L460)、[domain_updater.py:510-523](scripts/domain_updater.py#L510-L523)、[domain_updater.py:562-570](scripts/domain_updater.py#L562-L570)。 **离线复现**: - 排除规则使用非法正则 `[`:配置校验通过,运行时返回不匹配,规则静默失效。 - `scoring.tie_breakers=null`:校验通过,解析时迭代 None 抛出 TypeError。 **静态确认的输出键冲突**:[domain_updater.py:973-975](scripts/domain_updater.py#L973-L975)、[domain_updater.py:1101-1108](scripts/domain_updater.py#L1101-L1108)。若将 `state_last_good_key` 设为保留键 `status`,地址被状态字符串覆盖;下一次异常可以把 `ok` 当作地址发布。 应在运行前编译规则、统一可空语义,并校验保留字段与输出路径冲突。不需要为此引入大型配置框架。 #### P-07 / P2:state 自身异常可能让正常采集也无法启动 位置:[domain_updater.py:25-34](scripts/domain_updater.py#L25-L34)、[domain_updater.py:1023-1027](scripts/domain_updater.py#L1023-L1027)、[domain_updater.py:1113-1144](scripts/domain_updater.py#L1113-L1144)。**静态确认**。 语法有效但类型为 `[]` 或 `null` 的 state 不会回到默认对象,随后在主 try 外调用 `.get()` 直接退出。state 的 last-good 类型与地址也未校验。 另外,current/vars 已更新而 state 写入失败时,异常分支首先再次写同一 state;如果仍失败,回退写出流程不会完成。Python 的单文件原子写已有效降低截断风险,但不能替代 state schema 校验或多文件提交边界。 #### P-08 / P2:旧 CSV 误作新观测不仅存在于 BusyBox 位置:[domain_updater.py:144-166](scripts/domain_updater.py#L144-L166)。**静态确认**。 `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](scripts/domain_updater.py#L816-L820)、[domain_updater.py:895-915](scripts/domain_updater.py#L895-L915) ### 3.6 Router 与打包:新增问题 #### R-01 / P1:打包清理会删除调用者提供的整个临时目录 位置:[build_router_package.sh:9](build_router_package.sh#L9)、[build_router_package.sh:46-49](build_router_package.sh#L46-L49)、[build_router_package.sh:87-91](build_router_package.sh#L87-L91)。**静态确认**。 环境变量 `TMP_DIR` 可指向已经存在、包含其他数据的目录。cleanup 无条件删除整个目录,并在输入验证前就注册退出 trap;因此即使依赖缺失、根本没有开始打包,也会删除该目录。 不能靠提醒“只传空目录”消除这个清理边界问题。应在指定临时根下用 `mktemp -d` 创建本轮独占子目录,仅清理自己创建的目录,并避免清理未经验证的外部路径。**不应实际执行危险目录输入验证此问题。** #### R-02 / P2:cfst 卡住会阻断重启后的 fallback 和 HTTP 恢复 位置:[router_local_update.sh:312-324](scripts/router_local_update.sh#L312-L324)、[README.md:388-403](README.md#L388-L403)。**静态确认**。 更新器已读到持久化 last-good,但必须等待没有外层 deadline 的 cfst 返回,才会写 fallback。README 启动流程又等待更新器后才启动 HTTP。重启清空 `/tmp` 后,一次迟迟不返回的测速即可让 HTTP 长期没有服务;“最多重试 5 次”不能约束单次耗时。 建议先按可接受的降级策略恢复一份完整快照、启动可用的读取服务,再进行有总超时的采集。正常“cfst 非零返回 + 可用持久 state”本来就能恢复,不能笼统说 Router 重启一定失去 fallback。 #### R-03 / P2:路径可配置,但输出文件与发布根的约束没有校验 位置:[router_local_update.sh:63-80](scripts/router_local_update.sh#L63-L80)、[router_local_update.sh:134-173](scripts/router_local_update.sh#L134-L173)、[router_local_http.sh:41](scripts/router_local_http.sh#L41)、[router_local_http.sh:118-124](scripts/router_local_http.sh#L118-L124)。**静态确认**,包含三个具体输入: 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](scripts/router_local_update.sh#L103-L123)。**审查子任务离线复现**:从源码提取 awk 程序,用合成 CSV 输入;普通 region 可解析,region 内部含 tab 时输出带未转义控制字符,JSON 解析失败。 脚本没有输出解析/契约检查,因而可以将不可消费的 current 文件标为 `ok`。这不是正常 cfst 数据必现故障,而是输入边界缺失。不要仅修 tab;应采用覆盖 JSON 控制字符的可靠转义,并在发布前校验。 #### R-05 / P2(条件性):state 是合法 JSON,但正则读不出 last-good 位置:[router_local_update.sh:82-88](scripts/router_local_update.sh#L82-L88)。**静态确认**。 外部工具把状态保存为 `"last_good_ip" : "..."`,冒号前有合法 JSON 空白,正则即无法提取值。测速失败时会被当作没有 last-good。自身生成的格式满足该正则,所以这是手工恢复、格式化和跨生产者兼容性问题,而非默认自写自读失败。 #### R-06 / P2(目标设备待验):watchdog 可能匹配到自己的命令 位置:[README.md:405-406](README.md#L405-L406)。若设备 `ps` 展示完整 cron shell 命令,启动分支里未隐藏的 `router_local_http.sh` 字符串可能使检测匹配自身,即使服务不在也不重启。`[r]` 技巧只能避免 grep 的搜索参数自匹配,不能解决同一命令另一处出现完整进程名。 应使用可靠的 PID/进程归属或服务监督机制。实际触发取决于目标 BusyBox 的 ps 输出和截断,不能宣布现有设备已经受影响。 #### R-07 / 条件性安全风险:HTTP 暴露的是目录,不是声明的文件清单 位置:[router_local_http.sh:130-131](scripts/router_local_http.sh#L130-L131)、[config_router.conf:25-36](config_router.conf#L25-L36)。数字端口配置未限定监听接口,默认暴露 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](workflow.md#L75-L100) 的失败状态写入/HTTP 状态输出展示、README 部分 `cfip_runtime/` 维护路径,与当前 `/tmp` runtime 加独立持久 state 不完全一致。 构建主机不能执行另一架构 BusyBox 时,也不能据此断言目标二进制缺少 httpd applet;应区分“无法在本机检查”和“目标明确不支持”。 ## 4. Sub-Store:已复现的消费端问题 ### 4.1 默认 Server-only 也缺少新鲜度与健康资格检查 证据:[operator_template.js:171-180](substore/operator_template.js#L171-L180)、[operator_template.js:282-291](substore/operator_template.js#L282-L291)。 **离线复现**:输入 `{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](substore/operator_template.js#L47-L58)、[operator_template.js:303-305](substore/operator_template.js#L303-L305)。 **离线复现**:`{domain: {bad: true}, status: "ok"}` 被字符串化成 `[object object]`,写入符合名称规则的 VMess 节点 `server`,同时进入缓存。 显式覆盖已有 IPv4/IPv6/域名校验,但 HTTP 获取的数据没有相同边界。错误类型、非法 IP、含协议或路径的字符串不应通过 `String(...)` 自动转成“地址”。生产端校验不能替代消费者校验。 建议:先验证顶层为对象、地址字段为非空字符串,再使用一致的地址解析器;坏来源进入失败分支,不污染节点或缓存。 ### 4.3 原报告 Router fallback 优先问题得到确认 证据:[operator_template.js:118-163](substore/operator_template.js#L118-L163)。 **离线复现**: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](substore/operator_template.js#L13)、[operator_template.js:282-291](substore/operator_template.js#L282-L291)。 **条件性风险;共享缓存模型下已复现**:两个同为 `server_only`、URL 不同的操作符使用同一 Map 模拟宿主缓存;A 写入后,B 不请求自己的来源,直接使用 A 的值。 影响取决于实际 Sub-Store 宿主的缓存命名空间;本轮未启动真实 Sub-Store,因此不宣称所有部署都跨实例串值。至少缓存键没有表达 URL、阈值和策略版本;编辑配置时同模式旧缓存可能继续生效。 建议:缓存键包括有效来源 URL、模式与影响选择的策略,缓存值保留来源、观测时间、状态和有效期,而不只保存裸地址。 ### 4.5 速度解析的次要边界风险 证据:[operator_template.js:62-76](substore/operator_template.js#L62-L76)。 - 数值 `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 本轮主会话实际执行并通过 ```text 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;本轮没有引入测试框架或改动业务代码。