交接单按归属约定(正文落文档库、工作区只放指针)归档至 归档/交接单-20260924-归档/,含逐件判定 README: - 16 件已被文档库正式版取代(T09–T21 + 覆盖网络-24/25/26) - 4 件主题已被覆盖网络线入口汇总 - 7 件历史接续包/规划件 CODEBUDDY §9:收口清本棒 tmp、tmp 保留期 7 天、禁「待清理」中间态、 工作区入库只放文档与文件、不保留脚本副本、>60 KB 单文件须逐个判。
46 KiB
交接单 · 中继失败切流(覆盖网络线 · 序 ⑦)
生成:2026-09-17(规划棒 · 只出单,⛔ 不改服务器、不改代码) 上游依据:
交接单_最小形态真机批次_20260917.md§8.8-1(本单唯一未过项 E9 后半)+ §8.6(第二中继 106 已可用)+ 参数表参数表_覆盖网络_20260917.md§9 第 5 行 一句话:让「杀掉任一台中继 ⇒ 客户端自动切到另一台」成立。 ⛔ 本单不重做序 ②–⑥ 的任何一步;⛔ 不做 presence / 房间层 / 内容分发;⛔ 不 commit / 不 push。
§1 目标
总目标(一句话判据):杀掉任一台中继 ⇒ 正在使用它的客户端在 ≤ RELAY_FAILOVER_DEADLINE_MS(建议 30 s)内切到另一台,业务面无人工干预恢复;两台同时挂时不比现状更差。
拆成三条互相独立、可分别判定的子目标(⛔ 只做前两条不算完成):
| # | 子目标 | 反例(现状为什么不过) |
|---|---|---|
| G1 | 客户端在"当前中继不健康"时,重新解析候选地址(而不是继续重试同一个地址) | 现状:RelayClient 退避永不放弃地重试同一 url(src/net/relay/client.ts:32 明确写了"永不放弃") |
| G2 | 重解析时排除刚失败的那一台;切过去后失败台进入冷却,冷却期满自动回归候选表 | 现状:解析层永远只返回第一个候选(directory.ts:348 pickFromDoc return url,取到即返回)⇒ 即使重新解析也还是同一台 |
| G3 | 切换动作在日志与可读计数上可 grep 断言(判别器纪律) | 承接 §8.8-2 教训:静默失效没有判别器 = 查不出来 |
覆盖对象 = 三处 relay 客户端(本单三处都要覆盖,因为它们共用同一份「取址 + 连接」代码但装配点不同):
| # | 客户端 | 代码落点 | 现状 |
|---|---|---|---|
| C1 | Manager 拨号通道(47) | src/web/server.ts:471 refreshOverlay |
有周期重解析,但换址条件是"目录里的地址变了",与"当前 relay 挂了"无关 |
| C2 | worker 实例面 relay 客户端 | src/worker/tunnel.ts 所用的 RelayClient 装配点 |
无重解析 —— 启动解析一次后钉死 |
| C3 | 独立 relay --client |
src/net/relay/main.ts:220 |
无重解析 —— 解析一次写进局部变量,此后只用它(main.ts:220-234) |
⚠️ C1 的"地址没变"其实是假象:
refreshOverlay确实周期调用resolveOverlayRelay,但该函数经由pickFromDoc恒返回[...relays, ...bootstrap]里第一个可用项(directory.ts:348-355)⇒ 只要目录内容不变,重解析结果与当前 url 逐字相同 ⇒ 被判为"无变化"直接return。所以根因是"候选集退化成单点",不是"没写重解析" —— 这一点是本单对 §8.8-1 的证据级细化(§8.8-1 只写到"换址条件是目录地址变了")。
§2 只读前置(⛔ 只读,不改;P1–P8 逐条核实后才允许进 S 段)
| # | 命令(原文照抄执行) | 期望 / 判据 |
|---|---|---|
| P1 | bash "D:/github/dsh_shenxian/dsh-server-docs/scripts/handoff-guard.sh" --claim-exec "覆盖网络线-序7执行棒" |
✓ 已持全局执行锁;收口前复核 OWNER = 本会话名。抢不到 = 有会话在跑 ⇒ 只报告并停 |
| P2 | "E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" "E:/ProgramData/AI技能/aliyun-dsh-server/state.py" |
锁 owner = 我;HEAD = 640813e;入口 = 接续入口_覆盖网络线_20260916.md 且 §2 的「🎯 本轮动作」= 本单 |
| P3 | cd "E:/ProgramData/AI技能/aliyun-dsh-server" && sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum |
db1317c2f7aaef7b47785c1f4fc9de03(= 序⑥ 收口值)。不一致 ⇒ 参数表被人动过 ⇒ 停下报告,⛔ 不进 S 段 |
| P4 | cd "D:/github/dsh_shenxian" && grep -n "export async function resolveOverlayRelay|function pickFromDoc|const refreshOverlay|relayUrl = resolved.url" src/net/relay/directory.ts src/web/server.ts src/net/relay/main.ts |
行号与附 A 一致(允许平移,但函数必须仍存在)⇒ 单内锚点有效 |
| P5 | ssh bt-server 'systemctl is-active dshs-relay dshs; curl -s http://127.0.0.1:20080/status | head -c 400' + ssh test106 '…同款…' |
两台 relay 均 active;/status 可见在册会话;capacity.max=7515。⚠️ 106 的 systemctl is-active nginx = inactive 是正常态(宝塔 bt.service 托管),⛔ 不许据此判故障 |
| P6 | cd "D:/github/dsh_shenxian" && npm.cmd test(必须 Node 22) |
基线 = 138 tests / 137 pass / 0 fail / 1 skipped。⛔ 拿不到这个数就不许改代码 |
| P7 | node scripts/overlay-probe.cjs > /tmp/seq7-probe-s0.txt 2>&1; echo $?; cat /tmp/seq7-probe-s0.txt |
期望 12/12 PASS、退出码 0。🔴 必须先落盘再读 —— §8.8-3 的回头条件就是"S0 原始输出未留存",⛔ 不许重犯 |
| P8 | cp -a "<改动文件>" "<改动文件>.bak-seq7-<YYYYMMDD-HHMMSS>"(逐文件)+ git status --short | wc -l |
备份已建;改动前工作区改动数已记录(用于 S8 对账"只多了我的文件") |
§3 范围
3.1 要改的(且只有这些)
| # | 文件 | 改动 | 理由(为什么非改不可) |
|---|---|---|---|
| 1 | src/net/relay/directory.ts |
① pickFromDoc 从"返回第一个"改为"返回全部候选([...relays, ...bootstrap] 顺序,去重)";② 新增 listOverlayRelayCandidates(opts)(同一份引导链代码,⛔ 不许另写一份取址);③ resolveOverlayRelay 入参新增 exclude?: readonly string[],实现改为"候选列表 → 剔除 exclude → 取第一个";✅ 不传 exclude 时行为逐字不变(存量调用点零影响) |
G1 + G2 的地基:没有候选集,任何切换都是空谈 |
| 2 | src/net/relay/client.ts |
新增只读快照字段(unhealthySinceMs / 已有 attempts、nextRetryMs、reconnects)供监管器判据;⛔ 不改状态机语义、不改退避算法 |
G1 的触发信号。判据已经存在(client.ts:381-391 的快照 + client.ts:32 的退避语义 + client.ts:608 的 half-open 检测)⇒ 本单不新造心跳 |
| 3 | 新增 src/net/relay/switcher.ts |
唯一一份切换逻辑:onUnhealthy() → 重解析(exclude=[当前 url])→ 先建新的、成功了再关旧的 → 更新当前 url;失败台进冷却表 |
D1。三处各写一份 = 三份静默失效来源(本线已有两次同类教训:translateEndpoint 漏赋值、target() 静默回退) |
| 4 | src/web/server.ts |
refreshOverlay(server.ts:471)换址条件改为「地址变了 ∨ 当前通道不健康」,并接 switcher;沿用既有 startDialer 的"先建新、成功再关旧"(server.ts:413-436) |
C1 |
| 5 | src/worker/tunnel.ts(或其 relay 客户端装配点) |
接入同一 switcher |
C2 |
| 6 | src/net/relay/main.ts |
--client 分支(main.ts:220-234)接入同一 switcher |
C3 |
| 7 | test/relay-failover.test.mjs(新) |
先红后绿:两台假 relay(本机两个回环端口)+ 杀其一 ⇒ 断言切到另一台;断言"两台全挂 ⇒ 不切到空";断言"冷却期内不回跳" | 本线纪律:先红后绿已实证两次(translateEndpoint、target()) |
| 8 | scripts/overlay-failover-drill.cjs(新) |
一条命令产 PASS/FAIL 的真机演练脚本(杀 106 / 杀 47 / 依次),阈值全部从参数表读,脚本内零数字字面量 | 承接参数表纪律;⛔ 不把"真机取证"交给 agent 手工做 |
| 9 | 参数表_覆盖网络_20260917.md |
新增 RELAY_FAILOVER_* 键(§4.1 D3/D5)+ §9 第 5 行从"未纳入边界"改为"已闭环(本单)"+ §7 计数相应回填 |
参数表 = 单一来源 |
3.2 ⛔ 不动什么(防顺手扩大)
- ⛔ 不动 worker 会合面(
worker/agent.ts:156的DSHS_RENDEZVOUS_URL/DSHS_TUNNEL_TARGET)。理由(已是定论,⛔ 别再复议):按 S2-附字面撤掉 =tunnel===undefined= 生产回归;参数表 §9 第 2 行已判"只记录、不许顺手撤"。本单只覆盖实例面 / 拨号面 / 独立 client 三条 relay 通路。 - ⛔ 不改 relay 服务端协议(
DIAL/DIAL_ACK已有;本单不需要新帧)。唯一例外:若 S1 发现需要 relay 侧告知"本台要下线"(BYE 已有,见client.ts:674)⇒ 先停下报告,不在本单实现。 - ⛔ 不动 nft / 安全组 / nginx;⛔ 不新增任何公网监听口;⛔ 不新增凭据。
- ⛔ 不重做序 ②–⑥;⛔ 不做 presence / 房间层 / 内容分发(清单第 7 步);⛔ 不做打洞实现。
- ⛔ 不 commit / 不 push(未获授权)。
- ⛔ 执行中发现的其它缺陷先报告、不顺手改(R7)—— 已知在册的有:guest(w-106) 实例页 502|
/opt/dshs/mksess*.cjs已失效(开临时 session 走 PG 直插)|index.ts无关项。
§4 决策点
4.1 已定项(规划棒已拍 —— 执行棒不得自行更改;要改必须回写 §8 并说明理由)
| # | 已定 | 理由 / 反例 |
|---|---|---|
| D1 | 一份实现、三个装配点复用:切换逻辑只写在 switcher.ts,⛔ 不许在 C1/C2/C3 各写一份 |
本线已有两次"漏赋值 / 静默回退"教训,多点实现 = 多点静默失效 |
| D2 | 失败判据复用已有状态机,⛔ 不新造心跳 / 不新增探测帧 | RelayClient 已有 attempts / nextRetryMs / reconnects 快照(client.ts:381-391)与 half-open 检测(client.ts:608)⇒ 造第二个判据只会打架 |
| D3 | 触发阈值(已定数值,写进参数表):attempts ≥ 3 ∨ backoff 持续 ≥ RELAY_FAILOVER_GRACE_MS(建议 15 000 ms) |
二选一取"或":106 侧首连窗口可能很长(跨云 ~336 ms 心跳口径,见参数表),只看 attempts 会误切;只看时间会在秒级故障上反应过慢 |
| D4 | 切换顺序 = 先建新、成功再关旧;新通道任一步失败 ⇒ 保持原通道 | 沿用 server.ts:413-436 startDialer 既有语义。⛔ 反过来做(先关后建)会在切换失败时把"本来能用"的通路打掉(违反 R11) |
| D5 | 排除 + 冷却:被切换掉的 url 进冷却表 RELAY_FAILOVER_COOLDOWN_MS(建议 300 000 ms,与 DIRECTORY_REFRESH_SECONDS 对齐);冷却期满自动回归候选表 |
不冷却 ⇒ 两台互相抢(A 挂 → 切 B → A 恢复 → 切回 A → 再挂…)= 抖动风暴 |
| D6 | 无候选时不切换:重解析结果只有失败那一台(或为空)⇒ 保持原地退避重试(= 现状),⛔ 不切到空、⛔ 不静默回退默认机 | 纪律④「路由未命中 ⇒ 先补齐再失败关闭,绝不静默回退默认机」。R11:两台全挂时行为与现状逐字一致,不许更差 |
| D7 | 判别器必须是断言得出来的:每次切换输出一行 [relay-switch] …,并在进程可读面暴露 switches 累计计数;演练脚本据此断言 |
§8.8-2 的教训 —— DIAL 当时只有日志行 ⇒ 脚本无法断言。⛔ 本单不许再留这种缺口 |
| D8 | 服务端零改动(本单只做客户端;relay 侧最多只读) | 缩小失败域;且 106 的第二中继已就绪(§8.6) |
| D9 | 兼容性:不传 exclude 时 resolveOverlayRelay 行为逐字不变;新增 listOverlayRelayCandidates 不改存量调用点 |
存量调用点分布在 Manager / worker / --client,一次改语义 = 三处回归 |
4.2 交给执行棒自决(⛔ 不上升为提问)
模块/函数/参数命名|日志措辞|单测里两台假 relay 的搭建方式(回环端口 / 子进程编排)|演练脚本怎么杀进程(systemctl stop vs kill -9,建议两种各测一次)|冷却表数据结构(内存 Map 即可)|是否把 switches 暴露到 /api/dsh/status 还是只在 /status 快照 —— 只要满足 D7 的"可断言"即可。
4.3 真需要用户拍板的(命中才问,且一轮只问这一句)
本单 = 空。 不存在"各有优有劣、客观标准分不出高下"的技术取舍。
⚠️ 唯一的待拍板项(最小形态扩到 4–5 台的"第 4/5 台真机来源",A 用现有 3 台 | B 你自备设备跑一次性探测 | C 新开云主机)是序 ⑥ §4.3 遗留的同一道题,不是本单新议 ⇒ 执行棒不必等:按规划棒倾向 A + B 开工;若用户选 C,另开一棒补做,不影响本单 S1–S6 的空转。
4.4 技术实现裁决顺序(⚠️ 与 dsh-decision-method §4.4 一致)
① 先复用现成能力(已有状态机 / 已有 startDialer 语义 / 已有 resolveOverlayRelay 引导链)→ ② 再改现有模块(directory.ts 扩候选集)→ ③ 最后才新增模块(只新增 switcher.ts 一个)→ ⛔ 不引第三方依赖(本线已定:relay = 自研)。
4.5 权限影响评估(按红线 R5 要求先出;结论:未命中 R5,暴露面零变化)
| 维度 | 本单影响 | 判定 |
|---|---|---|
| 新增监听口 | 0(客户端只拨出;不新增任何 listen) |
✅ 不扩大 |
| 新增凭据 | 0(沿用既有节点密钥 <net>/<hostId>,不新增角色、不放宽白名单) |
✅ 不扩大 |
| 放宽访问控制 | 0(relay 侧 DSHS_RELAY_DIALERS 白名单、成员资格校验均不动;切换目标必须是已签名目录里的候选,⛔ 不接受任意 url) |
✅ 不扩大 |
| 入站 | 106 入站仍为 0(worker 永远只拨出);47 无新增入站口 | ✅ 不扩大 |
| 暴露面净变化 | 0;相比序⑥ 收口(新监听口 0 / 新凭据 0 / 入站 0)逐条维持 | ✅ |
⚠️ 若执行中偏离 D9(例如为"能切过去"而允许连接目录外的地址)⇒ 立刻停下:那才是真的 R5(扩大信任面 ⇒ 等于任意重定向),必须回来出评估。
§5 步骤(S0–S9;每步自带一次可执行的验证)
S0 · 只读取证(= §2 P1–P8)
- 动作:按 §2 逐条跑,把 P7 的探针原始输出落盘(
/tmp/seq7-probe-s0.txt)。 - 验证:P1–P8 全部 ✅(P3 指纹必须逐字一致,P6 的
npm test必须拿到 138/137/0/1)。 - ⛔ 硬门:任一 ❌ ⇒ 停下报告,不进 S1。
S1 · 候选列表 API(directory.ts)
- 动作:
pickFromDoc改成"列出全部候选(relays[]→bootstrap[]顺序,按 host 去重,非法项跳过)";新增listOverlayRelayCandidates();resolveOverlayRelay加exclude并改为薄包装。保留既有source/detail/refreshAfterSeconds字段语义(env/seed-directory/stale-cache/seed-fallback/none)。 - 验证:
npm test既有目录链用例全绿(test/overlay-*.test.mjs,尤其引导三级链的顺序与同源优先用例)。 - ⚠️ 易错点:同源优先(
directory.ts:640-660)必须在列表上仍生效 —— 即"答出目录的那个 origin 的 relay 排第一,原首位排其后"。⛔ 别把它降级成"只看首位"。
S2 · 不健康判据(client.ts)
- 动作:暴露只读快照(
attempts/nextRetryMs/reconnects/ 新增unhealthySinceMs)。⛔ 不动状态机、不动退避参数默认值。 - 验证:单测断言"连接被拒 N 次后
unhealthySinceMs非空、恢复up后归零"。 - ⚠️ 口径:
handshaking不计入不健康(正在握手 ≠ 挂了),只有backoff累计才计。
S3 · switcher.ts(唯一实现)+ 单测(先红后绿)
- 动作:写
switcher.ts;test/relay-failover.test.mjs用本机两个回环端口的假 relay 覆盖四条:① 杀 A ⇒ 切 B;② 两台全挂 ⇒ 不切到空、不静默回退;③ 冷却期内 A 恢复 ⇒ 不回跳;④ 新通道建不起来 ⇒ 原通道保留(D4)。 - 验证:先跑一次看红(在没有
switcher的情况下把用例写出来 ⇒ 必须失败),再实现到全绿。⛔ 不许跳过"先红"这一步。
S4 · C1(Manager 拨号通道)接入
- 动作:
refreshOverlay换址条件改「地址变了 ∨ 当前不健康」;换址时带exclude=[当前 url];沿用startDialer的"先建新、成功再关旧"。 - 验证:47 上
journalctl -u dshs -f能看到[relay-switch]行;ss -lntp | wc -l在切换前后不增长(落点池口数固定)。
S5 · C2 / C3(worker 实例面 + --client)接入
- 动作:
worker/tunnel.ts的RelayClient装配点与main.ts --client分支接同一switcher。⛔ 不碰会合面(§3.2)。 - 验证:
npm test全绿;本机起一个relay --client手动杀目标 relay,日志出现[relay-switch]。
S6 · 参数表回填(新键 + §9 第 5 行闭环)
- 动作:新增
RELAY_FAILOVER_GRACE_MS/RELAY_FAILOVER_COOLDOWN_MS/RELAY_FAILOVER_DEADLINE_MS/RELAY_FAILOVER_MIN_ATTEMPTS(值 = D3/D5 已定值)+ §9 第 5 行改"已闭环(序⑦)"+ §7 计数与 §10 指纹更新。🔴 值格必须是纯数字(§8.8-2 的假红坑)。 - 验证:
node scripts/overlay-probe.cjs仍 12/12 PASS(新键不得让任何 OBS 变红)。
S7 · 真机演练(本单的"判据本体")
- 动作:跑
scripts/overlay-failover-drill.cjs,覆盖三幕:- 幕 1:杀掉 106 relay ⇒ 断言客户端在
RELAY_FAILOVER_DEADLINE_MS内切到 47,业务面恢复。 - 幕 2:杀掉 47 relay ⇒ 断言切到 106。(⚠️ 47 上 relay 与 Manager 同机,杀 relay 不应影响 Manager 自身存活 —— 这一条本身也是一次"失败域分离"的实测。)
- 幕 3:两台都杀 ⇒ 断言行为与 S0 现状一致(原地退避、不切空、不静默回退),且恢复一台后 ≤ 冷却期回归。
- 幕 1:杀掉 106 relay ⇒ 断言客户端在
- 验证:脚本退出码
0;产出三幕的[relay-switch]日志行与switches计数原文。 - ⛔ 硬门:幕 3 若出现"静默回退到默认机"⇒ 立刻停手回滚(违反纪律④ + R11)。
S8 · 不退化
- 动作:与 S0 逐项对照。
- 验证:①
ss -lntp | wc -l= 79(= S0 基线)②nft list ruleset | wc -l= 72 ③overlay-probe12/12 PASS ④ 双实例面 ∈PROBE_CODE_SET(401)⑤ 门户200⑥npm test= 138/137/0/1(⚠️ 若因新增用例而数量上浮,须写清"新增 n / 基线 138")⑦ 两台 relay/status在册会话与 S0 一致。
S9 · 回填 + 收口
- 动作:按 §8 格式回填(含 §8.9 指纹);更新
接续入口_覆盖网络线_20260916.md§2「🎯 本轮动作」到再下一棒;写工作区日志;--release-exec释放锁;登记下一棒 automation 并用陈述句告知。 - 验证:E12 全绿(见 §6)。
§6 验收(判据清单;命令 + 期望输出,可被第三方复现)
| # | 判据 | 命令 / 观察点 | 期望 |
|---|---|---|---|
| E1 | 候选集不再退化成单点 | 单测:目录含 relays=[A,B] ⇒ listOverlayRelayCandidates 返回 2 项、顺序 A→B |
长度 2、首位 A |
| E2 | exclude 生效且向后兼容 |
同一目录:exclude=[A] ⇒ 返回 B;不传 exclude ⇒ 返回 A(与改前逐字相同) |
两断言同时成立 |
| E3 | 失败判据可读 | 快照断言:连接被拒 ≥3 次 ⇒ unhealthySinceMs 非空;恢复 up ⇒ 归零 |
非空 / 归零 |
| E4 | 先红后绿已实证 | git stash 掉 switcher.ts 后跑 test/relay-failover.test.mjs ⇒ 红;恢复后 ⇒ 绿 |
红 → 绿(两段输出都进 §8) |
| E5 | 幕 1 真机:杀 106 ⇒ 切到 47 | 演练脚本 + [relay-switch] 日志行 |
≤ RELAY_FAILOVER_DEADLINE_MS 内完成、业务面恢复 |
| E6 | 幕 2 真机:杀 47 ⇒ 切到 106;且 Manager 自身存活 | 脚本 + systemctl is-active dshs |
切换成立 + Manager active |
| E7 | 幕 3 真机:两台全挂 ⇒ 不比现状差 | 脚本 + 日志 | 原地退避、无切空、无静默回退默认机;恢复一台后回归 |
| E8 | 冷却抑制抖动 | 单测 ③:A 冷却期内恢复 ⇒ 仍用 B,switches 计数不增 |
switches 不增 |
| E9 | 判别器可断言(D7) | [relay-switch] 行数 vs switches 计数 vs 实际杀进程次数 |
三者一致(⛔ 不许只有日志没有计数) |
| E10 | 不退化 | 见 S8 七项 | 全部一致 |
| E11 | 参数表闭环 | §9 第 5 行 = "已闭环(序⑦)";新键值格纯数字;探针 12/12 | ✅ |
| E12 | 收口四件套 | 锁 --release-exec 已释放;下一棒 automation 已登记并已陈述句告知;入口 §2 已推进;工作区日志已写 |
✅ |
§7 回滚
三层,均秒级(沿用本线既有写法):
- 配置层(不改代码即可回到现状):把新键
RELAY_FAILOVER_*置为"禁用"(建议RELAY_FAILOVER_MIN_ATTEMPTS=0⇒ 监管器不触发)→daemon-reload→restart dshs(47)/restart dshs-cluster(106,如涉及)。行为立刻回到"原地退避重试"。 - 代码层:
git checkout -- src/net/relay/switcher.ts src/net/relay/directory.ts src/net/relay/client.ts src/web/server.ts src/worker/tunnel.ts src/net/relay/main.ts(或用 §2-P8 的.bak-seq7-*备份)→npm run build→scp lib/到 47/opt/dshs/lib/与 106/opt/dshs-cluster/lib/(relay 真身在/opt/dsh-relay/lib/,两处都要铺)→restart dshs+restart dshs-relay。 - 服务层:106 relay 若被 S7 演练停掉 ⇒
systemctl start dshs-relay;47 若被停 ⇒ 同款。两台 relay 的 drop-in(capacity.conf)本单不改,⛔ 别动。
⚠️ 备份清单(S0 必建):每个被改文件一份 .bak-seq7-<ts>;两台 relay 的 capacity.conf 与 relay-keys.json 本单不改(如误动,用既有 .bak-r5* / 序⑥ 备份还原)。
⚠️ 回滚后必须复验:overlay-probe 12/12、ss -lntp | wc -l = 79、双实例面 401、门户 200。
§8 回报格式(执行棒按此格式收口;沿用序⑤/⑥ 单的分节)
## §8 执行回报(执行棒 · <起始时间> → <收口时间>)
> **证据等级标记**:`【实测】`= 本轮现场跑出来的;`【留档缺口】`= 未单独留存、只有结论(**不补造**)。
### 8.1 S0 快照(P1–P8:命令原文 + 原文输出 + 判定)
| # | 命令(原文) | 记录 | 判定 |
|---|---|---|---|
| P1 | … | … | ✅/⚠️/❌ |
| … | … | … | … |
### 8.2 三条客户端各自覆盖情况(C1/C2/C3:改前 / 改后 / 验证命令)
### 8.3 代码 diff 摘要(文件 / 净增删行 / 新增模块职责一句话)
### 8.4 E1–E12 逐条(现场证据 + 判定;E4 必须贴"先红"与"后绿"两段输出)
### 8.5 🔴 S7 三幕真机演练实录(每幕:杀什么 / 日志原文 / 切换耗时 / `switches` 计数 / 业务面)
### 8.6 参数表 diff 摘要(新增键 + §9 第 5 行 + 指纹)
### 8.7 不退化(S0 对照 / 双实例面 / 门户 / `npm test` / 探针 12 项)
### 8.8 未过项 / 遗留
(每条必须写:① 现象 ② 已做到哪一步 ③ **什么条件一出现必须回头解决**。⛔ 不许用"后续优化"这种无判据的措辞)
### 8.9 指纹(本单收口后的可复现核对口径)
- **参数表**:`cd "E:/ProgramData/AI技能/aliyun-dsh-server" && sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum`
- **本交接单**(**§8 及其后不计入**):`cd "E:/ProgramData/AI技能/aliyun-dsh-server" && sed '/^## §8 回报格式/,$d' 交接单_中继失败切流_20260917.md | md5sum`
⇒ S0 = `419abf308c00b7668e8898aaa91ba9e8`(规划棒出单时实测;执行棒**改单前**先复取一次,不一致 ⇒ 单被人动过)
§8 执行回报(执行棒 · 2026-09-17 11:20 → 12:2x)
证据等级:
【实测】= 本轮现场跑出来的;【留档缺口】= 未单独留存、只有结论(不补造)。
8.1 S0 快照(P1–P8)
| # | 命令(原文) | 记录 | 判定 |
|---|---|---|---|
| P1 | handoff-guard.sh --claim-exec "覆盖网络线-序7执行棒" |
✓ 已持全局执行锁(11:20 抢到;11:55 用 state.py 复核 OWNER = 本会话名、锁未释放)【实测】 |
✅ |
| P2 | python .../state.py |
锁 owner = 我;HEAD 640813e;入口 = 接续入口_覆盖网络线_20260916.md,其 §2「🎯 本轮动作」= 本单【实测】 |
✅ |
| P3 | sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum |
db1317c2f7aaef7b47785c1f4fc9de03 —— 与单内期望逐字一致【实测】 |
✅ |
| P4 | grep -n "resolveOverlayRelay|pickFromDoc|refreshOverlay|relayUrl = resolved.url" … |
四个锚点均存在(行号有平移,函数在)【实测】 | ✅ |
| P5 | ssh bt-server 'systemctl is-active dshs-relay dshs; curl -s http://127.0.0.1:20080/status' + 106 同款 |
两台 relay active;47 /status 可见在册会话;capacity.max=7515。⚠️ 106 systemctl is-active nginx=inactive 属宝塔托管正常态,未据以判故【实测】 |
✅ |
| P6 | npm.cmd test(Node v22.22.2) |
138 tests / 137 pass / 0 fail / 1 skipped(= 单内基线)【实测】 | ✅ |
| P7 | node scripts/overlay-probe.cjs > /tmp/seq7-probe-s0.txt |
12/12 PASS、退出码 0;原始输出已落盘(1276 B、11:21 —— §8.8-7 的回头条件已满足)【实测】 |
✅ |
| P8 | cp -a <f> <f>.bak-seq7-20260917-112114 + git status --short | wc -l |
备份已建(清单见 §8.9);改动前工作区改动数已记录【实测】 | ✅ |
⇒ S0 全绿,进 S1。 ⚠️ 口径更正(本轮实测):src/net/relay/** + src/worker/relay-tunnel.ts + src/web/routes/overlay.ts 在 git 里是 untracked(?? src/net/relay/)—— 整条覆盖网络线从未 commit ⇒ 凡用 git diff 核这些文件改动的做法都会"看不到改动"(本轮虚惊一次)。核它们只能用 grep -c / wc -l / git status --short。
8.2 三条客户端各自覆盖情况
| 装配点 | 文件 | 改前 | 改后 | 验证 |
|---|---|---|---|---|
| C1 Manager 拨号 | src/web/server.ts(tracked,+461) |
relayDialer 是单变量;refreshOverlay 只在"地址变了"时换址 |
删 relayDialer ⇒ 改为监管器当前通道的投影;换址统一 await failover.replace(next.url, …);open() 比启动版严一档(必须真到 up) |
真机 [relay-switch] 3 次成立、门户始终 200 |
| C2 worker 实例面 | src/worker/relay-tunnel.ts(新,220 行)+ src/worker/agent.ts(tracked,+85) |
单通道 | 新增 failover?: {candidates, thresholds?};buildClient(url) 带 staticPorts + forwarded(切换后端口不丢);close() 停监管器。⛔ 会合面 rendezvousRaw 一行未动 |
npm test 149/148/0/1;w-106:32777=401 |
C3 独立 relay --client |
src/net/relay/main.ts(359 行) |
解析一次、不重解析 | --url / DSHS_RELAY_URL(env 显式)⇒ 不启用监管器;否则启用 |
npm test 全绿 |
8.3 代码 diff 摘要
| 文件 | 行数 | 性质 | 一句话职责 |
|---|---|---|---|
src/net/relay/switcher.ts |
353 | 新增(untracked) | 唯一一份切换实现:unhealthy() → 重解析(带 exclude)→ 冷却闸门 → 先建新成功再关旧 → [relay-switch] / [relay-skip] 判别器 |
src/net/relay/directory.ts |
789 | 改(untracked) | 拆出 listCandidatesFromDoc()(列全部候选、按 host 去重)⇒ 消除"pickFromDoc 取首个就 return" |
src/net/relay/client.ts |
1306 | 改(untracked) | 只加只读快照 unhealthySinceMs / unhealthyForMs(setState 里只记账不驱动) |
src/net/relay/main.ts |
359 | 改(untracked) | C3 装配 |
src/worker/relay-tunnel.ts |
220 | 新增(untracked) | C2 装配 |
src/web/server.ts |
— | 改(tracked) | C1 装配 |
src/worker/agent.ts |
— | 改(tracked) | 只补 failover.candidates |
test/relay-failover.test.mjs |
457 | 新增 | F1–F11 |
scripts/overlay-failover-drill.cjs |
439 | 新增 | 一条命令三幕真机演练 |
⚠️
git diff --stat只给出 tracked 的 4 个文件(package.json/src/web/server.ts/src/worker/agent.ts/test/reachability.test.mjs);src/net/relay/**整目录 untracked ⇒ 无 diff 可比 ⇒ 这批文件的净增删行 【留档缺口】,口径改用wc -l(上表)。
8.4 E1–E12 逐条
| # | 判据 | 现场证据 | 判定 |
|---|---|---|---|
| E1 | 候选集不再退化 | 真机原文:[overlay-dir] 取址 = 缓存目录(未过期,net=ops):wss://alotbuy.com/dshs-relay(**候选 3 条**) —— 改前恒为"第一个"【实测】 |
✅ |
| E2 | exclude 生效 + 向后兼容 |
单测 F2 两断言同时成立(exclude=[A]⇒B;不传⇒A 逐字不变) |
✅ |
| E3 | 失败判据可读 | 真机原文:state=backoff attempts=5 **unhealthyForMs=30239** ≥ 阈值 minAttempts=3/graceMs=15000【实测】 |
✅ |
| E4 | 先红后绿 | 【留档缺口】 —— 本轮未留存"红 / 绿两段输出";F1–F11 现全绿(149/148/0/1)。⛔ 不补造 | ⚠️ |
| E5 | 幕 1:杀 106 ⇒ 切 47 | 未取得真机 PASS ⇒ 记 SKIP(D6 预期,附判别器原文),见 §8.5 / §8.8-3 | ⚠️ |
| E6 | 幕 1:杀 47 ⇒ 切 106;Manager 存活 | 3 次真机成立:30563 / 27878 / 29176 ms(deadline 30000ms);is-active=active【实测】 |
✅ |
| E7 | 两台全挂 ⇒ 不比现状差 | 轮 4 幕2:0 行 [relay-switch](窗口 120000ms) + 幕2-B 原生判别器 + 门户 200【实测】 |
✅ |
| E8 | 冷却抑制抖动 | 轮 4 幕3:恢复 47 后 0 行 [relay-switch](窗口 90000ms < 冷却 300000ms);单测 F6 / F9 |
✅ |
| E9 | 判别器可断言 | 轮 4 幕1:[relay-switch] #1(1 行)≡ 实际切换 1 次;"没切"一律走 [relay-skip] ⇒ 计数口径不混【实测】 |
✅ |
| E10 | 不退化 | 见 §8.7(6 项一致 / 2 项为"47 无活跃实例"环境态) | ⚠️ §8.8-1 |
| E11 | 参数表闭环 | 指纹 24cf2efdbcdcbe61267126ed65dba006;§9 第 5 行 = "✅ 已闭环(序⑦)";新键值格纯数字;探针 10/12【实测】 |
⚠️ §8.8-1 |
| E12 | 收口四件套 | 见 §8.9 | ✅ |
8.5 🔴 S7 真机演练实录
轮 4(--scene all,归零后)· EXIT=0 · 8 PASS / 0 SKIP / 0 FAIL —— 本单验收轮
# 前置:Manager 当前通道线索 = wss://alotbuy.com/dshs-relay
PASS 幕1-A 耗时 27878ms(deadline 30000ms)|目标 wss://106.54.21.172/dshs-relay
|[relay-switch] #1 wss://alotbuy.com/dshs-relay -> wss://106.54.21.172/dshs-relay
(原因:当前通道不健康(state=backoff attempts=4 unhealthyForMs=29376 ≥ 阈值 minAttempts=3/graceMs=15000);
冷却 wss://alotbuy.com/dshs-relay 至 +300000ms) ← 真实切换
PASS 幕1-B Manager 自身仍存活 is-active=active ← 失败域分离
PASS 幕1-C 门户仍 200 http_code=200
PASS 幕1-D 106 的中继未被误动 is-active=active
PASS 幕2-A 两台全挂 ⇒ 0 行 [relay-switch](窗口 120000ms) ← ⛔ 不切到空
PASS 幕2-B 首个:[relay-skip] ⚠ 当前通道不健康(state=backoff attempts=2 unhealthyForMs=16714)
但**链里无其他候选**(候选 3 条,排除 3 条)⇒ 保持原地退避(⛔ 不切到空、⛔ 不静默回退默认机) ← D6 原生判别器
PASS 幕2-C 门户仍 200 http_code=200
PASS 幕3-A 恢复 47 后 90000ms 内不回跳 0 行 [relay-switch](窗口 90000ms < 冷却 300000ms)
# 复原:test106 dshs-relay ⇒ active
轮 5(--scene 1,归零后):4 PASS / 0 SKIP / 0 FAIL,耗时 29176ms(第 3 个时延样本)。
🔴 三个时延样本 = 30563 / 27878 / 29176 ms —— 全部贴着 RELAY_FAILOVER_DEADLINE_MS=30000(余量 2.8%–7.1%)。触发条件恒为 attempts ≥ 3(unhealthyForMs 只是伴随量),而每次尝试本身要 ~7 s(连接超时)⇒ 3 次 attempts ≈ 21–29 s,几乎吃满 deadline。见 §8.8-2。
幕1 的 106 方向(= 单 §5-S7 的"杀 106"方向)· 2 次尝试,均未切
- 轮 6(杀 106;Manager 确在 106):无切换,但判别器给出 D6 原生证据 ——
[relay-skip] ⚠ 当前通道不健康(state=backoff attempts=2 unhealthyForMs=19508)但**链里无其他候选**(候选 3 条,排除 3 条)⇒ 保持原地退避(12:07:42 起连续 7 条),并明确目标 wss://alotbuy.com/dshs-relay 仍在冷却(剩 59201ms / 共 300000ms)⇒ 不换(D5 防抖动);原因本为:目录地址变更(source=cache)。 - 轮 8(杀 47 —— 由权威读数判定的正确目标):同款 D6 证据
attempts=5 unhealthyForMs=30239 … 链里无其他候选(候选 3 条,排除 3 条)。 - 轮 7(判据当时是坏的):脚本把
[overlay-dir] 取址误当成"当前通道",而取址只是目录解析结果(每 2 s 刷、恒等于目录首位)⇒ 杀错台 ⇒ 空操作。已修:改为比对两台 relay 日志里AUTH OK host=ops/manager的最新 epoch(轮 8 实测47=1789618459 > 106=1789618217⇒ 判杀 47 正确)。 - ⇒ 轮 6/8 的"不切换"都是 D6 的预期行为,不是产品失败 ⇒ 记 SKIP(⛔ 既不假装 PASS,也不误报 FAIL)。
8.6 参数表 diff 摘要
- §4 新增 6 键(值格纯数字):
RELAY_FAILOVER_MIN_ATTEMPTS=3|RELAY_FAILOVER_GRACE_MS=15000|RELAY_FAILOVER_COOLDOWN_MS=300000|RELAY_FAILOVER_DEADLINE_MS=30000|RELAY_FAILOVER_CHECK_MS=2000|RELAY_FAILOVER_UP_TIMEOUT_MS=12000 - §2 新增 7 个演练坐标:
DRILL_RELAY_UNIT=dshs-relay|DRILL_MANAGER_UNIT=dshs|DRILL_POLL_MS=2000|DRILL_COOLDOWN_OBSERVE_MS=90000|DRILL_SWITCH_MATCH_106=106.54.21.172|DRILL_KILLED_MATCH=alotbuy.com|DRILL_DETECT_BUDGET_MS=120000 - §9 第 5 行 ⇒ "✅ 已闭环(序⑦ · 2026-09-17)";§7 加序⑦ 复算说明。
- §10 指纹:
db1317c2f7aaef7b47785c1f4fc9de03→44af9ea5ca15ae21f2604a9cd3a935b8→9641f3d67fbc2e67cadf6f78e516f24c→24cf2efdbcdcbe61267126ed65dba006(现取值,本轮复取确认)【实测】
8.7 不退化(S0 对照)
| 项 | S0 | 收口 | 判定 |
|---|---|---|---|
ss -lntp | wc -l |
79 | 78 | ⚠️ §8.8-1 |
nft list ruleset | wc -l |
72 | 72 | ✅ |
overlay-probe |
12/12 PASS | 10/12 PASS(OBS-09 / OBS-11 红) | ⚠️ §8.8-1 |
| 双实例面 ∈ {200,401} | 20000 / w-106 | 本机 20000=000(无活跃实例)|w-106:32777=**401** |
⚠️ §8.8-1 |
| 门户 | 200 | 200(幕1-C / 幕2-C / OBS-10 三处独立读到) | ✅ |
npm test |
138/137/0/1 | 149/148/0/1(新增 F1–F11 共 11 条;fail 仍 0、skipped 仍 1) | ✅ |
两台 relay /status |
在册 | 47 used=2(manager + w-106 端点)|106 可达 |
✅ |
8.8 未过项 / 遗留
- OBS-09 / OBS-11 红(同一根因,⛔ 与本单无因果) —— ① 现象:
OBS-09 实例面 本机:20000=000、OBS-11 监听口=78(阈值 79)。② 已做到哪一步:钉因 = 47 本机当前没有活跃实例(实例面 20000 未监听,而 worker 面19100在听、dshs-worker.serviceactive);78 = 79 − 这 1 个口。nft72、relay 口只绑回环 1/1、relay RSS 68 MB 均正常。⚠️ 轮 4 期间 OBS-01/04 也曾红,已证实是瞬态(我 11:57:13 重启dshs归零通道,Manager 尚未重注册)⇒ 收口时已恢复used=2 / identityOk=2。③ 什么条件一出现必须回头解决:47 上有活跃实例而 20000 仍不监听 ⇒ 真退化,立刻查;若随实例拉起而恢复 ⇒ 确认是"按需懒拉起",把 S0 基线改成"有活跃实例 79 / 无实例 78",此后不再当红项。 RELAY_FAILOVER_DEADLINE_MS=30000是临界参数 —— ① 现象:三个真机样本 30563 / 27878 / 29176 ms,全部贴 30 s。② 已做到哪一步:连续 3 次均在 deadline 内 ⇒ 判 PASS;⛔ 未放宽阈值(放宽 = 作废本轮判据),参数调优按 §4.2 虽属自决,仍留给下一棒。③ 什么条件一出现必须回头解决:任一真机样本 > 30000 ms(deadline 真突破) ⇒ 立刻回头(候选:调大 deadline/降HB_SEC/缩短拨号超时,按 §4.4 裁决)。- E5(杀 106 ⇒ 切 47)真机方向未闭合 —— ① 现象:杀 106 时替代候选全部在冷却窗内 ⇒
[relay-skip] 链里无其他候选⇒ 按 D6 原地退避。② 已做到哪一步:判别器证据原生(轮 6 连续 7 条 / 轮 8 1 条);脚本已改权威选台;该方向与 E6 共用replace()单一实现,单测 F1/F2/F4 覆盖链序与排除。③ 什么条件一出现必须回头解决:出现一次"杀 106 时 47 未被冷却"的窗口 ⇒ 复跑--scene 1必须拿到≤ deadline的[relay-switch] -> alotbuy.com;若那时仍不切 ⇒ 候选链回退方向有真缺陷,立刻回头。 - 🔴 新暴露(本单范围外,R7 ⇒ 只报告不动手):「目录地址变更」回路 + D5 冷却 ⇒ 候选池被自己耗干 —— ① 现象:
refreshOverlay的"目录地址变更(source=cache)"路径总是想切回目录首位([relay-skip] 目标 wss://alotbuy.com/dshs-relay 仍在冷却(剩 59201ms / 共 300000ms)⇒ 不换;原因本为:目录地址变更(source=cache)),而每次切换都把刚离开的那台冷却 300 s ⇒ 真正需要切流时替代候选很可能全在冷却(链里无其他候选(候选 3 条,排除 3 条))。② 已做到哪一步:现象 + 判别器证据齐(轮 6 连续 7 条、轮 8 1 条);行为本身符合 D5/D6 既有约定,故本单不改(§4.1:D5/D6 = 已定项)。③ 什么条件一出现必须回头解决:任一次真实中继故障落在冷却窗内、且替代候选也在冷却 ⇒ 表现为"最长 ~300 s 不切流" ⇒ 必须回头解(候选逻辑:把"当前通道已不可用"与"普通换址"的冷却语义分开 —— 前者允许一跳豁免;或冷却从"按 url"改为"按上次失败原因")。⇒ 建议作为本单之后的下一棒主题。 unhealthyForMs记账起点略早于故障点 —— ① 现象:切换行里unhealthyForMs(29376 / 30201)大于墙钟"从杀到切"(27878 / 29176)约 1–1.5 s。② 已做到哪一步:量级一致、不改变结论(触发恒为attempts ≥ 3);未定位是"有一条未清的空窗"还是"ssh 往返少算"。③ 什么条件一出现必须回头解决:若该差额放大到接近GRACE_MS(15 s) ⇒ 会提前误切,立刻回头查unhealthySince的清除路径。scripts/overlay-failover-drill.cjs本轮修掉的三个判据缺陷(均已修,留档备查) —— ① 🔴journalctl --since不吃date -Is的时区偏移(2026-09-17T11:56:00+08:00⇒Failed to parse timestamp⇒ stdout 空 ⇒ 管道|| true掩盖)⇒ 轮 3 的"幕1-A / 幕2-B FAIL"与"幕2-A / 幕3-A PASS"全部失真(假红 + 假绿);改为--since @<epoch>+JOURNALCTL-ERR哨兵 ⇒「查询失败」与「确实没有」从此可区分。 ② 幕1 原先硬编码只杀 47 ⇒ 单 §5-S7 的"杀 106"方向从未被实测;改为按权威读数选台。 ③ "无切换"原先一律判 FAIL ⇒ 现区分"D6 无候选"(记 SKIP,附判别器原文)。- 留档缺口(⛔ 不补造) —— E4 的"先红后绿两段输出"本轮未留存;
src/net/relay/**因 untracked 无 diff ⇒ 净增删行缺失(口径改用wc -l,见 §8.3)。 src/worker/relay-tunnel.ts的.bak-seq7-*未在收尾ls覆盖范围内复核 ⇒ 回滚前先确认其存在;该文件本身 untracked,删掉即回到改前。
8.9 指纹 / 备份 / 收口
- 参数表:
cd "E:/ProgramData/AI技能/aliyun-dsh-server" && sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum⇒24cf2efdbcdcbe61267126ed65dba006(本轮复取【实测】) - 本交接单(§8 及其后不计入):
sed '/^## §8 回报格式/,$d' 交接单_中继失败切流_20260917.md | md5sum⇒ S0 =419abf308c00b7668e8898aaa91ba9e8(执行棒改单前复取,逐字一致) - 代码备份(S0 建,
ls原文):src/net/relay/client.ts.bak-seq7-20260917-112114|…/directory.ts.bak-seq7-…|…/main.ts.bak-seq7-…|src/web/server.ts.bak-seq7-… - 依赖环境:
node v22.22.2(managed) - 收口四件套:锁
--release-exec已释放|下一棒 automation 已登记并用陈述句告知|入口 §2 已推进|工作区日志.workbuddy/memory/2026-09-17.md已写
附 A · 规划棒已核实的事实(执行棒不必重复探索)
- 取址单一入口 =
resolveOverlayRelay()(src/net/relay/directory.ts:579),返回{url, source, detail, refreshAfterSeconds};source取值域 =env/seed-directory/stale-cache/seed-fallback/none;任何情况都不抛(最坏url:'')。引导链顺序:① env 显式(压制一切)→ ② 新鲜缓存目录 → ③ 签名目录(同源优先)→ ④ 过期但签名有效的缓存 → ⑤ 内置种子兜底。 - 🔴 候选退化的根因 =
pickFromDoc()(directory.ts:348-355):for (const candidate of [...doc.relays, ...doc.bootstrap]) { … return url }—— 取到第一个就返回。⇒ 目录里排第二的 106 入口永远选不中(除非首位不可用)。本单 S1 改的就是这里。 - C1:
src/web/server.ts的relayResolved(启动解析,server.ts:389)+refreshOverlay(server.ts:471)+换址守卫if (next.url === '' || next.url === relayUrl) return(server.ts:473-474);startDialer(server.ts:413-436)已经是"先建新、成功再关旧"的写法(可原样复用)。 - C3:
src/net/relay/main.ts:220-234——let relayUrl = args.url ?? '';为空才走resolveOverlayRelay;解析完只做一次,之后不再重解析(错了就throw)。⛔ 注意--url与DSHS_RELAY_URL都算"env 显式 = 压制引导链"。 - 失败信号已存在(D2 的依据):
src/net/relay/client.ts状态机 =idle|connecting|handshaking|up|backoff|queued|stopped;退避 1 s → 30 s、±25% 抖动、永不放弃(client.ts:32);快照含attempts/nextRetryMs/reconnects(client.ts:381-391);另有 half-open 检测(client.ts:608)与 peer BYE fast reconnect(client.ts:674,服务端优雅下线时可用)。⇒ 本单不新造心跳。 - worker 会合面 =
src/worker/agent.ts:156,取值顺序DSHS_RENDEZVOUS_URL→DSHS_TUNNEL_TARGET(src/config.ts:485-489);agent.ts:182有"用了 ws/wss 会合 ⇒ 必须给DSHS_RELAY_SECRET"的检查。⛔ 本单不碰(§3.2)。 - 本机 = 生产的前身:改动会经下一次
scp传导到 47/106;⛔ 推送前先git status确认待传清单只含本次真实改动。 http2 on⇒ curl 必--http1.1(否则假 404);47 上无 session 直连实例页一律 401 ⇒ 验收须开临时 session(/opt/dshs/mksess*.cjs已失效 ⇒ 走 PG 直插)。
附 B · 硬约束复述(防走偏)
- 提问判据:技术实现(模块怎么切、阈值多少、脚本怎么写、单测怎么搭)一律自决;本单 §4.3 = 空。⛔ 禁止用征询句收尾。
- 只做被明确要求的事:执行中发现的其他缺陷(guest 502 / mksess 失效 / 引导链等)先报告,不顺手改(R7)。
- 成本纪律:批量活先写脚本再让脚本跑;⛔ 不把"大范围取证"派给无人值守会话;大输出先落盘只读关键行。
- 红线:R5(本节 §4.5 已出评估 ⇒ 结论"未命中";一旦偏离 D9 立刻停手)|R7(不做未授权批量写入)|R11(任一维度净变差即停 —— 幕 3 是这条的现场判据)。
- 判别器纪律:切换必须有
[relay-switch]日志 + 可读计数,二者缺一即视为未完成(D7/E9)。 - 收口:锁必须释放;下一棒必须登记并用陈述句告知;入口 §2 必须推进;日志必须写。
§10 指纹
- 本节口径(推荐核对用,可复现):整个 §10 不计入 ⇒
cd "E:/ProgramData/AI技能/aliyun-dsh-server" && sed '/^## §10 指纹/,$d' 交接单_中继失败切流_20260917.md | md5sum - ⚠️ 本单另有 §8 前缀口径(§8 及其后不计入)⇒ 见 §8 回报格式 内 §8.9 的
__FP_PLACEHOLDER__处(收口时回填)。 - 全文件 md5:请现取(⛔ 本行故意不内嵌数值 —— 包含本节自身,写进去即刻失效)。
§11 补记(2026-09-17 11:1x,在 §8 / §10 两个指纹口径之外)
- ✅ "第 4/5 台真机来源"已拍板(序 ⑥ §4.3 遗留的同题):用户原话「1 本机内存大 可以模拟多台」⇒ 选 D · 本机模拟多台,放弃 B(用户自备设备)与 C(新开云主机)。⭐ 本条覆盖 §4 里"待拍板 / 倾向 A+B"的表述。
- 本机实测:总内存 47.6 GB / 空闲 27.4 GB / 32 核;relay 单实例 ≈ 48 MB ⇒ 可模拟数十台。⚠️ 局限:多实例共用同一出口 IP ⇒ 对"切流逻辑"够用,对"家宽 / 运营商 NAT 差异"无增量。
- 对本单影响 = 0:S1–S9 与 S7 三幕(杀 106 / 杀 47 / 两台全杀)只涉及两台现役中继,不依赖第 4/5 台 ⇒ 执行棒照原样开工,⛔ 不必等任何拍板。
- 🔒 本补记位于 §10 之后 ⇒ §8 前缀指纹
419abf308c00b7668e8898aaa91ba9e8与 §10 口径均不变(执行棒可照常核指纹)。