- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次) - .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…), 目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪 - .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/) - .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*) - .gitignore 补:备份件(*.bak-*) - 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
77 KiB
交接单 · presence(在线状态)改造
序 ⑱(规划棒)产物 | 2026-09-17 | 状态:待执行(= 下一棒 序 ⑲ 执行棒) 性质:只出规划 —— ⛔ 本棒未改任何代码、⛔ 未动 47/106 配置、⛔ 未 commit / push、⛔ 未改任何生产值。 依据:
接续入口_覆盖网络线_20260916.md§2「🎯 本轮动作」|覆盖网络_应用场景与待完善清单_20260916.md§五 第 7 步 + §二 / §四|覆盖网络_瓶颈落地方案_20260916.md§1 + 总览|覆盖网络_千台全场景推演_20260916.md瓶颈排序 / 流量预算总表 用户拍板 = 「a 要做」(2026-09-17 17:1x)⇒ presence 先做。⚠️ B(内容分发 / 块级内容寻址)仍待拍板 ⇒ 本单只登记、⛔ 不规划(见 §3.3-1)。 口径:本单 §8 前缀指纹 =sed '/^## §8 回报格式/,$d' 交接单_presence在线态_20260917.md | md5sum(⛔ 不是全文件 md5)。
§0 摘要
一句话:presence 的第一刀必须落在节点 / 端点在线态(overlay node presence)上 —— 因为全仓零应用层(room|chat 命中 0,presence 字样只出现在一处注释里),而"房间层 presence"(1000 人大房那套订阅裁剪 / 抽样降频)没有载体、无从下手;节点层是它的真前置,且今天就落在真实代码面上:Manager 现在的在线态来源是拉 relay /status 快照 + 15 s 陈旧回退,这正是要替换的那条"轮询"路径。
做法(照抄 瓶颈落地方案 §1 的七条,逐条映射到本仓):
①presence 绑连接生命周期(WS 连上 = 在线、断开 = 离线)|②本地批合并(固定 1 s 窗口)|③订阅式扇出(只推给订阅了该 host 的 Manager / Worker)|④批量事件(一帧带 host 数组)|⑤grace 10 s + 离线 debounce 30 s|⑥同 hostId 多连接按 device 聚合|⑦最终一致 5–15 s(⛔ 不上强一致),外加 TTL 安全网(防 relay 崩溃漏事件)。
§0.1 复核证据(本棒实测,3 条命令族)
| # | 命题 | 命令 | 实测输出 | 判定 |
|---|---|---|---|---|
| P1 | 全仓零应用层(房间 / 对话) | `grep -rilE "\b(room | chat)\b" src | wc -l` | 0 |
| P2 | presence 字样只在注释里 |
grep -ril presence src |
仅 src/net/relay/placement.ts;该文件 第 21 行原文:而我们的第一瓶颈是 **presence(在线态)**,所以速度略重(传输方案 §12 的实测依据) |
✅ 成立(零实现) |
| P3 | 现役在线态来源 = 拉快照 | grep -n "req.url === '/status'" src/net/relay/server.ts ⇒ 413;grep -n "为什么必须真的把条目从 endpoints 里删掉" src/net/relay/server.ts ⇒ 1234 |
1234 行原文写明:「/status 是 Manager …」 ⇒ Manager 消费的是 relay 的 /status 快照 |
✅ 成立(即"轮询"反模式) |
src/net/relay/现存 15 个模块:addr-override / client / dialer / directory / duplex / identity / index / keys / main / network / placement / rendezvous / server / switcher / wire⇒ presence 的落点就是这一层(不是新起一层)。
§0.2 与入口旧结论的对账
- 入口 §2 曾记(2026-09-16):「仓库里目前没有任何应用层代码 ⇒ "presence 改造"无从下手,这条旧指示是上一轮的笔误」。本棒的处置:该结论前提仍然成立(P1/P2 实测),但结论要收窄 —— 不是"presence 无从下手",而是"应用层 presence 无从下手、节点 presence 正当时"(序 ①–⑰ 已把它的全部前置做完:网抽象+引导 ②、一机一钥+信任根 ③、443 兜底 ④、参数表+观测 ⑤、3–5 台最小形态 ⑥、失败切流+冷却语义 ⑦⑧、检测时延 ⑨)。
- 入口 §2 另有:「⛔ 未为 presence / 房间层 / 内容分发定序(只登记候选与优缺点)」+ 序 ⑰ 执行棒的「登记门禁」。⇒ 本棒已拿到拍板(用户「a 要做」),门禁解除,presence 已由"候选"转为"在册执行项"。
§0.3 步骤定序(理由)
S0 基线 → S1 机制(relay 侧表)→ S2 线协议帧 → S3 批合并 → S4 消费侧 → S5 观测 → S6 真机验收 → S7 收口。
- S1 必须最先落:机制是唯一真活,S2/S3 都只是它的出口形式;先红后绿在 S1 就能建起来。
- S2 与 S3 同一批改(帧与批合并同属"出口形式",拆开要跑两遍 build / scp)。
- S4 消费侧放 S3 之后:只有出口定了才谈接线,否则接线要返工(序⑭ 的教训:消费者先动 = 方向反)。
- S5 观测紧贴 S6:没有阈值就不许跑真机(否则"降幅多少算过"无法判定,= 序⑤ 的立单理由)。
- S6 是唯一动服务的一步,置末;S7 收口含部署(属 lane 内执行细节,直接做、⛔ 不上抛)。
§0.4 范围外登记(⛔ 本单不规划)
| # | 项 | 为什么不在本单 |
|---|---|---|
| 1 | B 内容分发 / 块级内容寻址 | 仍待用户拍板(复杂度已评估为「中」:块级切分与哈希标识 + 内容寻址存储与客户端校验 + 同网段 peer 发现 + 内容源优先级链;验收判据单值可测「回源字节数 ≈ 1 份 × 组数」)⇒ 只登记一行,⛔ 不写步骤、不定序、不留键位 |
| 2 | 房间层 presence 专属做法 | 百分比抽样降频 / idle / typing / 1000 人大房批量 bulk:应用层专属,且应用层不存在(P1)⇒ 只登记;⛔ 参数表不留空键位(口径要可执行) |
| 3 | 游戏服放 L1(三件事之二) | 属"部署规范 + 容量",与 presence 无耦合 ⇒ 另立 |
| 4 | src/worker/tunnel.ts(SSH 隧道,生产死路径)是否删 |
序 ⑯ 已判独立决策 ⇒ 本单不处置 |
§1 目标(一句话,可判定"做完了没有")
把覆盖网络的节点 / 端点在线态从「Manager 拉 relay
/status快照 + 15 s 陈旧回退」改为「连接生命周期驱动 + 订阅式推送 + 本地批合并 + TTL 安全网」;完成判据 = 稳态 presence 帧数为 0、一次上下线变化 ≤1 帧、相对旧路径的帧数降幅 ≥ 1 个数量级(本机多实例实测 N≥4)、/status兜底仍可用、且RELAY_FAILOVER_*/HB_SEC/ burst 一字未动(D1 自证)。
§2 只读前置(执行前必须先核实的 5 条;给命令与期望输出,⛔ 不许靠推断)
| # | 要核实的事实 | 命令(cwd = 代码仓 D:/github/dsh_shenxian) |
期望输出 |
|---|---|---|---|
| P1 | 全仓零应用层 ⇒ 本单只做节点 presence | `grep -rilE "\b(room | chat)\b" src | wc -l` |
| P2 | presence 零实现 | grep -ril presence src ;sed -n '21p' src/net/relay/placement.ts |
仅 placement.ts;第 21 行 = 注释(含「第一瓶颈是 presence」) |
| P3 | 现役在线态 = 拉快照(要替换的那条路) | grep -n "req.url === '/status'" src/net/relay/server.ts |
413(行号以实测为准,⛔ 别写死) |
| P4 | D1 基线快照(改前拍一次,收口时逐项比对) | 参数表 sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum ;并在参数表内 grep -n "RELAY_FAILOVER_|HB_SEC|GRACEFUL_BURST" |
参数表指纹(序⑰ 收口值 = 8f08e74b026e6e5b5e1b3db813f031ae,取数时间 2026-09-17 17:2x)+ 6 个 RELAY_FAILOVER_* 与 HB_SEC / burst 现值逐行留档 |
| P5 | 收口基线(零回归三件套的改前值) | 代码仓 npm test(Node 22:npm.cmd test)/ node scripts/overlay-failover-drill.cjs --scene all --table "<工作区根>/参数表_覆盖网络_20260917.md" / node scripts/overlay-probe.cjs --table "<工作区根>/参数表_覆盖网络_20260917.md" |
npm test 162/161/0/1、--scene all 12 PASS / 0 SKIP / 0 FAIL、overlay-probe 12/12(取值以实测为准,必须带时间戳) |
⚠️ P4 / P5 的数字必须带"取数时间 + 复核命令",⛔ 不写死绝对值(
交接单/README.md §二的坑 2:写死的数字会被并行改动打穿)。 ⚠️ 对 47 / 106 的任何命令:ssh 一律显式-p 22(别名bt-server的32022是失效残留口);取证输出一律2>/dev/null(PQ 告警会污染计数判据)。
§3 范围
3.1 要改的(预计 8 个文件;以实测为准,多一个都要按 §9-3 停下报告)
| 层 | 文件 | 改什么 |
|---|---|---|
| 机制(主体) | src/net/relay/server.ts |
presence 权威表(hostId → {state, connId, deviceCount, lastSeenMs, endpoints[]})+ 订阅表 + 推送出口 + TTL 安全网 |
| 机制 | src/net/relay/wire.ts |
新增帧:SUB / UNSUB / PRESENCE / SNAP(帧号在既有分配表末尾追加,⛔ 不改既有帧号语义) |
| 出口 | src/net/relay/dialer.ts 或 client.ts |
批合并调度(1 s 窗口)落点(S3 时定,二选一,⛔ 不两处都改) |
| 消费 | src/net/relay/rendezvous.ts(RelayRendezvous) |
订阅为主路径、/status 降级为兜底 / 对账(⛔ 不删) |
| 消费 | src/web/server.ts |
C1 装配点:refreshOverlay 一带改为订阅驱动(去掉"只能靠拉"的假设) |
| 消费 | src/worker/relay-tunnel.ts / src/worker/agent.ts |
C2 装配点:同族接线(若实测无需改 ⇒ 在 §8 写明"已核对、无需改",⛔ 别为对称而改) |
| 测试 | test/relay.test.mjs(+可选新增 test/relay-presence.test.mjs) |
先红后绿用例;⛔ 不许改 package.json 的测试入口语义 |
| 观测 | 参数表_覆盖网络_20260917.md + scripts/overlay-probe.cjs |
新增 PRESENCE_* 键(全部有实测值,⛔ 无 待测 残留)+ OBS-13/14/15 + 探针新项 |
3.2 明确不动的(⛔ 防止顺手扩大)
- ⛔ 不动任何生产值:
RELAY_FAILOVER_*(6 键)/HB_SEC/ burst 语义 —— 这是 D1,也是本线长期硬门。 - ⛔ 不删
/status—— 它是回滚链的一环(§7-②)。 - ⛔ 不新增公网监听口、⛔ 不改
nft、⛔ 不改 nginx(本单是纯应用层改动,暴露面零变化 ⇒ §4.4 R5 未命中)。 - ⛔ 不改
src/net/relay/switcher.ts的冷却语义 —— presence 抖动抑制独立成表(§4.1-D7)。 - ⛔ 不碰房间层 / 内容分发 / 打洞实现 /
src/worker/tunnel.ts(死路径)。 - ⛔ 不动
04-调整方案/**档案正文(历史档案属性)。 - ⛔ 不 commit / 不 push(未获明确授权;序 ⑰ 那次授权是当次一次性的)。
- 🔴 ⛔ 不许把
RELAY_FAILOVER_COOLDOWN_MS=0写进任何回滚 / 演练 / 夹具路径(该值看似合法、实际自锁:归零会让"失败候选必须被排除"一起失效,实测 121–123 s 不切换)。
3.3 范围外登记(同 §0.4;执行棒只登记、⛔ 不动手)
- B 内容分发 / 块级内容寻址 —— 待拍板。
- 房间层 presence 专属做法(抽样降频 / idle / typing / bulk)—— 无载体。
- 骨干节点服务范围(A 自用 / B 全网)—— 入口 §4 既有待拍板项,与 presence 无关。
§4 决策点
4.1 已定项(本棒自决,可推翻 —— 依据均为客观可判)
| # | 决策 | 值 | 依据 |
|---|---|---|---|
| D1 | 落点层次 | 节点 / 端点 presence(src/net/relay/**);⛔ 应用层 presence 不做 |
§0.1 P1/P2 实测(零 `room |
| D2 | 权威归属 | 在线态权威 = relay 侧(连接生命周期所在层);Manager 只消费 + 缓存,⛔ 不自己轮询推导(避免双权威) | ⚠️ 与 §3-4「权威状态单点」不冲突:那条说的是归属 / 租约 / 骨干资格;presence 是连接事实,天然归属连接所在层。租约仍只归 Manager 写 |
| D3 | 一致性口径 | 最终一致,允许 5–15 s 陈旧;grace 10 s + 离线 debounce 30 s + TTL 安全网(TTL 取 HB_SEC 现值的 3 倍,⛔ 不改 HB_SEC) |
瓶颈落地方案 §1 第 5/7 条;TTL 是"防网关崩溃漏事件"的安全网(第 1 条) |
| D4 | 批合并 = 固定 1 s 窗口(方案 A) | 每网关把本地变化攒起来,每 1 s 一帧 pipeline 提交 | 瓶颈落地方案 §1 第 2/4 条(业界量级对照:2 万条命令/秒 → 1 次 pipeline/秒)。为什么不取"零延迟逐次推":presence 允许 5–15 s 陈旧 ⇒ 1 s 窗口完全够用,逐次推只会在抖动时制造帧爆炸(正是要消灭的现象) |
| D5 | /status 保留但降级 |
从"主路径"降为兜底 / 对账(保留代码与 nginx 放行,⛔ 不删) | 回滚链依赖它(§7-②);且它现在是 Manager 的唯一事实来源,一步切干净 = 单点失败 |
| D6 | 订阅可见性 | 默认只允许订阅同网络、且本机有权的 host(沿用序③ 的逻辑名 <net>/<hostId> 与成员资格校验);跨网络订阅 ⇒ 显式拒绝 + 拒绝计数 +1,⛔ 不静默返空 |
权限只收窄(R5 方向);静默返空 = 假绿,本线已有判别器教训 |
| D7 | 抖动抑制独立成表 | presence 的 grace / debounce 自成一张表,⛔ 不复用 switcher.ts 的冷却表 |
两条语义不同("拨号失败"vs"在线态抖动"),混表会污染序⑦/⑧ 的冷却判据 |
4.2 真取舍(本单 = 空)
三个曾考虑上抛的项(落点层次 / 批合并策略 / /status 去留)均有客观判据可分出高下(见 D1 / D4 / D5 的"依据"列)⇒ 按 §1 判据自行拍掉、⛔ 不上抛。
4.3 待拍板 = 空
- presence 本身已拍板(用户「a 要做」)。
- B(内容分发)不属本单:它是独立一项、仍待拍板 ⇒ 本单只登记一行(§3.3-1),⛔ 不写成"待你拍板"塞进本节(避免把别人那题捆进来)。
4.4 权限影响评估(R5,先评估后动手)
| 维度 | 变化 | 结论 |
|---|---|---|
| 新增监听口 | 0(帧走既有 wss 通道) | — |
| 新增凭据 | 0(复用序③ 的节点密钥 / 成员资格) | — |
| 新增入站 | 0(Manager / Worker 仍然只拨出) | — |
| 可见面 | 订阅可见性只收窄(D6 同网络 + 有权 + 跨网络显式拒绝) | — |
⇒ 未命中 R5,无需权限评估门禁,可直接开工。
4.5 与既有机制的对账(⛔ 不重复劳动、不重复造)
| 既有机制 | 与本单关系 |
|---|---|
序③ 身份 / 成员资格(identityRequired、<net>/<hostId>) |
复用:订阅鉴权走它,⛔ 不新造一套 |
R3 RendezvousRegistry / RelayRendezvous(relay 实时快照 + 15 s 陈旧回退) |
被替换主路径(保留为兜底):15 s 陈旧回退正是 D3 口径的现存实现,改造后由订阅推送取代 |
R5 DIAL / DIAL_ACK(Manager 只拨出 + 本机回环池) |
不改:presence 帧搭同一条通道,⛔ 不动拨号模型 |
序⑦ directory.ts#listCandidatesFromDoc / 序⑧ switcher.ts(冷却 + 一跳豁免) |
不改:那是地址选择,与在线态是两件事(D7) |
序⑨ waitUpOnStatus + 终态早退(deadline 四段分解) |
不改:那是换址墙钟;presence 缩短的是"Manager 多久知道谁在线",⛔ 别混为一谈 |
序⑤ 参数表 OBS-01..12 + scripts/overlay-probe.cjs |
扩展:新增 OBS-13/14/15(沿用"阈值⛔ 不许是脚本魔数"的纪律) |
序⑥ RELAY_MAX_HOSTS / capacity.conf |
不改:帧数在 presence 口径下与容量无关 |
§5 步骤 S0–S7(每步自带一次可执行的验证)
⚠️ 开工前:先抢全局执行锁
bash dsh-server-docs/scripts/handoff-guard.sh --claim-exec "<会话名>";抢不到 = 有会话在跑 ⇒ 停手(⛔ 不排队等)。 ⚠️ 本单是执行棒,按单开工,⛔ 无须再出规划单。 ⚠️ 部署属 lane 内执行细节(开发环境服务器,R8)⇒ 直接做,动手前一句话说明在动什么即可,⛔ 不上抛。
S0 · 只读前置 + 基线采样(零改动)
逐条跑 §2 的 P1–P5 并留档原文;把 P4 的生产值快照与 P5 的三件套基线存到 _中间产物_待清理/seq19/。
验证:§2 五行输出全部与"期望输出"一致;若有偏差 ⇒ ⛔ 先停下报告,别改码。
S1 · relay 侧 presence 权威表(唯一真活)
在 src/net/relay/server.ts 落 presence 表:open ⇒ online;close ⇒ offline(先过 grace 10 s + debounce 30 s);TTL 安全网按 D3 兜底;同 hostId 多连接按 device 聚合(任一在线 = 在线)。
判别器(必做,序⑤ 的教训):不许只写日志 —— 必须有可断言计数 subs / pushed / rejected,并以 status() 暴露。
验证(先红后绿):往 test/relay.test.mjs 追加用例 —— ①断开后 ≤ grace 仍 online、> grace+debounce 转 offline;②同 hostId 两连接断一条仍 online;③kill 掉通道后 TTL 内自愈。先对旧 lib 跑一次拿红,再改。
S2 · 线协议帧(wire.ts)
新增 SUB / UNSUB / PRESENCE / SNAP:帧号在既有分配表末尾追加,⛔ 不改既有帧号语义(改了 = 与 47/106 上现役 lib 不兼容,热更窗口会断)。
验证:帧号唯一性用例(既有号集合 ∩ 新号 = ∅)+ 旧 lib 对新帧"未知帧号 ⇒ 显式报错"(⛔ 不静默丢弃)。
S3 · 批合并 + 节流(1 s 窗口,D4)
每个网关把 1 s 内的变化合并成一帧(一帧带 host 数组);SNAP 供重连后一次拿全量(做法 ⑦:重连必须重拉)。
验证:单测断言 —— 稳态 0 帧;一次变化 ≤1 帧;100 次连续变化(<1 s 内)合并后 = 1 帧。
S4 · 消费侧接线(C1 / C2)
RelayRendezvous 改为订阅为主路径,/status 降为兜底;C1 src/web/server.ts(refreshOverlay 一带)、C2 src/worker/relay-tunnel.ts / src/worker/agent.ts 同族接线(实测无需改就写明,⛔ 不为对称而改)。
验证:单测 —— 订阅生效后不再发起 /status 轮询(判别器 = /status 命中计数稳态 = 0);断掉订阅 ⇒ 自动回退 /status 仍能拿到在线态。
S5 · 观测落地(参数表 + 探针)
参数表新增:PRESENCE_BATCH_MS / PRESENCE_GRACE_MS / PRESENCE_OFFLINE_DEBOUNCE_MS / PRESENCE_TTL_MS / PRESENCE_SUB_MAX(每个键都要有实测值,⛔ 无 待测 残留)+ OBS-13(presence 帧率:稳态 0)OBS-14(首帧即全量 SNAP,⛔ 无 N+1)OBS-15(状态陈旧 p95 ≤ 15 s);scripts/overlay-probe.cjs 增同名新项。
验证:node scripts/overlay-probe.cjs --table "<工作区根>/参数表_覆盖网络_20260917.md" ⇒ 新项能判 PASS 也能判 FAIL(⛔ 只做过 PASS 的路径 = 未验证,序⑫ 的夹具模式可复用:假红 / 假绿两侧都要能抓)。
S6 · 本机多实例真机验收(唯一动服务的一步)
用已拍板的本机多实例(用户原话「本机内存大 可以模拟多台」)起 N ≥ 4 个节点,实测:
① 稳态 presence 帧数 = 0;② 一次上下线 = ≤1 帧;③ 与旧路径(按 T 秒拉 /status)帧数降幅 ≥ 1 个数量级;④ 首帧即全量;⑤ 跨网络订阅 ⇒ 显式拒绝且计数 +1。
验证:五项逐条给"命令 + 实测数字 + 判定";跑完必须归零(systemctl restart dshs 把 Manager 归位回 47)并复跑探针。
S7 · 收口(部署 + 三件套 + 回填 + 指纹)
部署 = npm run build(Node 22)→ scp 产物到 47 /opt/dshs/lib/ 与 106 /opt/dshs-cluster/lib/(relay 真身另在 /opt/dsh-relay/lib/,三处都要铺)→ systemctl restart dshs(+ 需要时 dshs-relay)——属 lane 内,直接做;随后跑零回归三件套 + 回填 §8 + 给指纹 + 推进 接续入口_覆盖网络线_20260916.md §0/§2 + 写工作区日志。
验证:§6 的 E9/E10/E11 全绿;overlay-probe 12/12 + 新项 PASS;池口 64;三服务 active。
§6 验收判据 E1–E11(命令 + 期望输出,可被第三方复现)
| # | 判据 | 命令 / 观测点 | 期望 |
|---|---|---|---|
| E1 | 稳态 0 帧(变化驱动,无变化不推) | 单测 + 真机 N≥4 静置 60 s | presence 帧数 = 0(⛔ 非 0 即未达标) |
| E2 | 一次变化 ≤1 帧(批合并生效) | 单测:1 s 内 100 次变化 | 帧数 = 1 |
| E3 | 降幅 ≥ 1 个数量级(对旧路径) | 真机:旧口径 N 节点 × /T 秒拉 → 新口径变化驱动 |
降幅 ≥ 10×(写明 N / T / 实测算式) |
| E4 | 首帧即全量(⛔ 无 N+1) | 订阅后第一帧 | = SNAP(1 帧完成,⛔ 非逐 host 拉取) |
| E5 | 生命周期正确性 | 断连 ⇒ ≤ 10 s 仍 online、> 40 s 转 offline;kill -9 relay ⇒ TTL 内自愈 |
三条全绿(三个阈值均来自 D3,⛔ 不许改口径去凑判据) |
| E6 | 多设备聚合 | 同 hostId 两连接断一条 | 仍 online |
| E7 | 最终一致(⛔ 不上强一致) | 代码面检查 | 无跨节点同步 RPC / 无分布式锁 |
| E8 | 权限只收窄 + 显式拒绝(判别器) | 跨网络订阅请求 | 显式拒绝 + rejected 计数 +1(⛔ 不静默返空) |
| E9 | 零回归三件套 | npm test(Node 22)/ --scene all(必须带 --table)/ overlay-probe(必须带 --table) |
≥ 162/161/0/1 + 12 PASS / 0 SKIP / 0 FAIL + 12/12 + 新项 PASS |
| E10 | 参数表口径可执行 | grep -c "待测" 参数表_覆盖网络_20260917.md + 新增键逐项 |
PRESENCE_* 全部有实测值;指纹变化必须写明新值(口径 = sed '/^## §10 指纹/,$d' … | md5sum) |
| E11 | D1 自证 | 与 §2-P4 的快照逐项比对 | RELAY_FAILOVER_* / HB_SEC / burst 一字未变;grep -c 'COOLDOWN_MS=0' scripts/ = 0 |
§7 回滚(三层,均秒级;⛔ 不含任何 COOLDOWN_MS=0)
① 代码级(主回滚):git 恢复本单改动的文件(或从 .bak-seq19-<YYYYMMDD-HHMMSS> 副本还原)→ npm run build(Node 22)→ scp 铺回 47 / 106 / relay 三处 lib → systemctl restart dshs(+ dshs-relay)。判据 = 探针回 12/12 + --scene all 12 PASS。
② 消费侧回退(不重启服务即可):删掉 S4 的订阅装配点 ⇒ RelayRendezvous 自动回退到既有的「relay 实时快照 + 15 s 陈旧回退」路径(D5 保留 /status 就是为了这条)。判据 = /status 命中计数恢复非零 + 在线态仍正确。
③ 观测侧:参数表新增键保留但标注"未启用"(⛔ 不删行 —— 删了会让 OBS-13/14/15 悬空)。
⚠️ 备份命名一律
.bak-seq19-<YYYYMMDD-HHMMSS>,落原位同目录;relay 侧另存/etc/dshs/relay-keys.json.bak-seq19-*(若本轮未动密钥则不备份)。 ⛔ 回滚路径里出现RELAY_FAILOVER_COOLDOWN_MS=0= 违规(该值自锁,实测 121–123 s 不切换)。
§8 回报格式(执行会话必须回填的证据,逐条给"命令 + 原文输出 + 判定")
### 8.0 回报(序 ⑲ 执行棒 · <开始时刻>–<收口时刻>)
### 8.1 只读前置复现
P1–P5 命令 + 原文输出(P4/P5 带取数时间);与 §2「期望输出」逐条对账(一致 / 偏差及处置)
### 8.2 S1–S7 逐步回报
每步:改了什么(文件 + 行级要点)/验证命令 / 原文输出 / 判定(✅ / ⚠️ 临界 / ❌)
⛔ 不许写"已完成"三字了事;**每步必须有一次可执行验证的原文**
### 8.3 验收 E1–E11 对账表
| # | 判据 | 命令 | 实测 | 判定 |
(❌ 项必须写:卡在哪 / 已做到哪一步 / 什么条件一出现必须回头)
### 8.4 真机降幅实测(E3)
N = ? / 旧口径算式的取数 / 新口径实测帧数 / 降幅 = ?
### 8.5 D1 自证
`RELAY_FAILOVER_*`(6) / `HB_SEC` / burst 改前 vs 改后逐值对比;`grep -c 'COOLDOWN_MS=0' scripts/` 实测值
### 8.6 部署与收口
build RC / scp 三处路径与产物 md5 / 重启了哪些单元 / 归零后三件套复跑值 / 池口数
### 8.7 参数表变更
新增键与实测值;**指纹:旧 → 新**(口径 = `sed '/^## §10 指纹/,$d' … | md5sum`)
### 8.8 遗留与回头条件
逐条列出;**若某条命中 §9 的回头条件 ⇒ 必须写明"命中第 N 条"并停下报告**
### 8.9 本单指纹(可复现)
`sed '/^## §8 回报格式/,$d' 交接单_presence在线态_20260917.md | md5sum` = ?(回填后**应保持不变**:§8 之后的内容不影响前缀)
§8★ 回报(序⑲ 执行棒 · 已回填 · 2026-09-17 17:28–18:16)
本节 = 按上面 §8 的格式回填的实测证据(原文 + 命令)。所有输出取自本棒实跑,⛔ 无一条为推断。
8.0 基本信息
| 项 | 值 |
|---|---|
| 会话名 | 覆盖网络线-序19执行棒-presence(全局执行锁:抢到 → 收口时释放) |
| 起止 | 17:28 → 18:16(含 S6 真机 2 分钟 + --scene all 6 分钟) |
| 改动文件 | 6 个 src + 1 个测试 + 1 个探针 + 参数表(⛔ 未 commit · 未 push) |
| 边界自证 | ⛔ 未改任何生产值 · ⛔ 未新增公网监听口 · ⛔ 未改 nft/nginx · 🔴 全程未出现 COOLDOWN_MS=0 |
8.1 只读前置复现(S0)
| # | 命令 | 实测原文 | 与 §2 期望 |
|---|---|---|---|
| P1 | grep -rilE "\b(room|chat)\b" src | wc -l |
0 |
一致(零应用层) |
| P2 | grep -ril presence src |
src/net/relay/placement.ts(仅注释) |
一致(只读缓存待替换已成事实) |
| P3 | grep -n "req.url === '/status'" src/net/relay/server.ts |
413(head -2 证输出非空) |
一致 |
| P4 | sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum |
8f08e74b026e6e5b5e1b3db813f031ae |
一致 ⇒ D1 自证成立(生产值一字未改) |
| P5 | npm.cmd test / overlay-probe --table / --scene all --table |
161 pass / 0 fail / 1 skip / 12/12 PASS / 12 PASS / 0 FAIL |
一致(基线) |
P4 生产值快照(逐值留档):HB_SEC=15(server.ts:60) · HALF_OPEN_MS=37500 · RELAY_FAILOVER_MIN_ATTEMPTS=3 · GRACE_MS=15000 · COOLDOWN_MS=300000 · DEADLINE_MS=30000 · CHECK_MS=2000 · UP_TIMEOUT_MS=12000 · GRACEFUL_BURST_MS=15000 · EXEMPT=1;grep -c 'COOLDOWN_MS=0' scripts/ = 0。
8.2 S1–S7 逐步回报
| 步 | 改了什么(文件 + 行级要点) | 验证命令 | 实测原文 | 判定 |
|---|---|---|---|---|
| S1 | server.ts:PresenceState(conns: Map<connId,lastActMs> 逐连接判死)+ presenceTouch/presenceDrop/presenceDevices/presenceEntry/publishPresence/flushPresence/presenceSnapshot + 计数 subs/pushed/snaps/rejected/statusHits |
node --test --test-name-pattern="T(25|29|31) " test/relay.test.mjs |
先红 0 pass / 7 fail → 后绿 7/7 |
✅ |
| S2 | wire.ts:末尾追加 SUB 0x10 / UNSUB 0x11 / PRESENCE 0x12 / SNAP 0x13(既有 0x01–0x0f 语义一字未动) |
T26(帧号唯一 + 未知帧号 0x99 必须显式拒绝) | close 1008 + authFailed 0→1 |
✅ |
| S3 | server.ts:presenceBatchMs 批合并出口(1 s 窗口、一帧带数组)、SNAP 首帧全量、TTL 安全网逐连接摘除 |
T27(6 台同窗口 ⇒ 1 帧 6 条)/T29(幽灵连接 TTL 自愈) | Δ帧=1 条目=6 / 离线自动收口 |
✅ |
| S4 | client.ts(订阅侧:subscribePresence/presenceStatus/presenceOf/…,失败 ≥2 次 ⇒ unsupported 停止再试)+ rendezvous.ts(presence 为主路径,undefined 才回落 online)+ web/server.ts(装配订阅 + refreshRelay 挂起 + addressOf ② 订阅落点) |
T30(订阅新鲜 ⛔ 不读兜底;undefined 必须回退;两台 relay 之间无同步) |
calls.length = 0 / 0 / 1;s2.presence.length = 0 |
✅ |
| S5 | 参数表 §3.6 新增 PRESENCE_* 五键 + §6 新增 OBS-13/14/15 + 四阈值键;overlay-probe.cjs 新增三条真判据(含 --status-fixture-2 双快照) |
夹具五连跑(good + bad13/14/15/16) | good:三条全 PASS;bad13 Δpushed=5 ⇒ FAIL;bad14 snaps=0 ⇒ FAIL;bad15 p95=60000 ⇒ FAIL;bad16 口径漂移 ⇒ FAIL |
✅ |
| S6 | 无改动(只跑现场) | _tmp_seq19_s6.mjs(生产默认时序零注入,N=6 本机多实例) |
8/8 ✅(明细见 §8.4) | ✅ |
| S7 | npm run build → scp 四处 lib/ → 重启 4 个单元 |
见 §8.6 | build RC=0;四处 md5 同值;dshs/dshs-relay/dshs-worker/dshs-pg 全 active |
✅(OBS-09 除外,见 §8.8-1) |
S1 的"先红后绿"原文:git stash push -m seq19-red -- <6 个 src 文件> → 重建 lib → 跑 7 个新用例 = 0 pass / 7 fail(RED_RC=1);git stash pop + 定点 LF 还原 → 重建 → 7/7 pass。
8.3 验收 E1–E11 对账表
| # | 判据 | 命令 / 观测点 | 实测 | 判定 |
|---|---|---|---|---|
| E1 | 稳态 0 帧 | 单测 T25/真机 S6 静置 60 s/探针 OBS-13 |
Δ帧=0(期间 24 个心跳帧在流、lastSeenAgoMs≈101ms)/真机 Δpushed=0 |
✅ |
| E2 | 一次变化 ≤1 帧 | T25/T27/S6 ② | Δ帧=1(上线、下线、6 台同窗口各 1 帧) |
✅ |
| E3 | 降幅 ≥10× | S6 ③(算式见 §8.4) | 字面口径 63×;同消费者口径 10.5×;稳态 ∞ | ✅ |
| E4 | 首帧即全量 | T25/S6 ④/真机 /status |
snapFrames=1;snaps 0→1;真机 snaps=2 = 订阅次数 |
✅ |
| E5 | 生命周期正确性 | T25/T29/S6 ②a | 断连→改口 40985 ms(grace 10 s+debounce 30 s);TTL 幽灵自愈 | ✅ |
| E6 | 多设备聚合 | T25(连接更替) | 顶旧接新期间 online=true 且 devices=1、零新增帧 |
✅ |
| E7 | 最终一致(无跨节点同步) | T30 尾段(两台 relay 独立) | s1.presence=1 / s2.presence=0 |
✅ |
| E8 | 权限只收窄 + 显式拒绝 | T28/S6 ⑤ | 客户端 rejected=1、服务端 rejected 0→1、状态回 idle |
✅ |
| E9 | 零回归三件套 | npm test / --scene all / overlay-probe |
169/0/1(≥162/161 ✅);--scene all = 见 §8.8-4;探针 15 PASS / 1 FAIL |
⚠️ 1 红(OBS-09,见 §8.8-1) |
| E10 | 参数表口径可执行 | grep -c 待测 + 新键逐项 |
PRESENCE_* 全为实测值(⛔ 无 待测);指纹 8f08…→ 13de5f9b77c486d71e5b83ec909b17b2 |
✅ |
| E11 | D1 自证 | 与 §2-P4 快照逐项比对 + grep -c 'COOLDOWN_MS=0' scripts/ |
六项生产值一字未变;禁用串计数 0 | ✅ |
8.4 真机降幅实测(E3)
现场:本机多实例,N = 6 台 worker + 1 台订阅方,relay 用生产默认时序(零注入),窗口 W = 104 s(含一次下线 + 一次上线)。
旧口径取数:src/web/server.ts:76 RELAY_STATUS_POLL_MS = 5000 ⇒ 同一消费者每 5 s 拉 1 次。
- 字面口径(交接单 §6 E3 原文「
N节点 ×/T秒拉」):6 × (104 / 5) = 126次 → 新口径实测 2 帧 ⇒ 63.0× - 同消费者口径(同一台 Manager 轮询):
104 / 5 = 21次 → 2 帧 ⇒ 10.5× - 纯稳态(60 s、零变化):旧
12次 vs 新 0 帧 ⇒ ∞ 判定:✅(≥10×)。⚠️ 临界说明:新口径的下限是"变化次数 × 1 帧"(与 N 无关),而旧口径与 N 成正比 ⇒ N 越大降幅越大、变化越稀疏降幅越大;本窗口把"变化"人为压到 104 s 内两次(极端档)仍达 10.5×。
8.5 D1 自证
| 值 | 改前 | 改后 |
|---|---|---|
HB_SEC |
15 | 15 |
HALF_OPEN_MS |
37500 | 37500 |
GRACE_MS |
15000 | 15000 |
COOLDOWN_MS |
300000 | 300000 |
DEADLINE_MS |
30000 | 30000 |
GRACEFUL_BURST_MS |
15000 | 15000 |
RELAY_FAILOVER_EXEMPT |
1 | 1 |
grep -c 'COOLDOWN_MS=0' scripts/ |
0 | 0 |
8.6 部署与收口
npm run build(Node 22)= RC 0;npx tsc --noEmit= RC 0。- 铺码 四处(产物 md5,改后值):
server.js = 182a7645f8ad3e8f240737b9dab91028,index/wire/client/rendezvous.js = 2a688424… / 467d38bd… / 46a025ff… / e851667d…,web/server.js = a360c7f4…—— 47/opt/dshs/lib/net/relay/+ 47/opt/dsh-relay/lib/net/relay/+ 106/opt/dshs-cluster/lib/net/relay/+ 106/opt/dsh-relay/lib/net/relay/(另 47/opt/dshs/lib/web/server.js)⇒ 远端 md5 与本机逐条相同。 - 重启单元:47
dshs-relay→dshs-worker→dshs;106dshs-relay→dshs-worker⇒dshs / dshs-relay / dshs-worker / dshs-pg全 active。 - 归零复跑三件套:
npm test169/0/1;overlay-probe --table= 15 PASS / 1 FAIL(唯一红项OBS-09);池口 64(DIALER_POOL,ss实测 25000–25063 全绑);--scene all= 见 §8.8-4。 - 真机新路径证据(47 relay
/status):presenceTiming = {graceMs 10000, offlineDebounceMs 30000, batchMs 1000, ttlMs 45000, subMax 0};counters = {subs 1, pushed 3, snaps 2, rejected 0};Manager 日志[relay-client] presence SNAP 2 条(scope=all)。
8.7 参数表变更
新增键(全部实测/可复算,⛔ 无 待测):PRESENCE_BATCH_MS=1000 · PRESENCE_GRACE_MS=10000 · PRESENCE_OFFLINE_DEBOUNCE_MS=30000 · PRESENCE_TTL_MS=45000 · PRESENCE_SUB_MAX=0 · PRESENCE_STEADY_FRAMES_MAX=0 · PRESENCE_SAMPLE_HITS_MIN=1 · PRESENCE_SAMPLE_GAP_MS=3000 · PRESENCE_STALE_P95_MAX_MS=15000;新增观测行 OBS-13/14/15。
指纹(口径 = sed '/^## §10 指纹/,$d' … | md5sum):8f08e74b026e6e5b5e1b3db813f031ae → 13de5f9b77c486d71e5b83ec909b17b2。
8.8 遗留与回头条件
- 🔴
OBS-09红 ⇒ 命中 §9-8(已停下报告)。卡在哪:OBS-09要的是用户实例面(47127.0.0.1:20000、对端 21000 经 relay),而 S7 规定的systemctl restart dshs(+ 106dshs-worker)会把dsh-114801-*.scope/dsh-100002-*.scope一起收掉;平台是按需拉起(拉起的唯一触发 = 用户真去打开自己的工作区),⛔ 无 reconciler(重启后 4 分钟仍inactive,Manager 日志零 spawn 记录)。⇒ S7 的重启步骤与 E9 的探针判据在"无用户访问"时互相冲突。已做到哪一步:探针 15 PASS / 1 FAIL,其余 11 项基线项全 PASS(OBS-11集合判据含 nft 与监听面全清)。什么条件一出现必须回头:任一用户访问自己的实例页(实例被拉起)后重跑overlay-probe --table …⇒OBS-09应回 PASS;若拉起后仍红 ⇒ 属真回归,立即回头查addressOf链路。 - ⚠️ 本棒实测踩到并已修的假死(重要,已补测):
presenceTouch原在ensureEndpoint之前、且localPort是listen(0)回调里异步才落地的 ⇒ 首帧推出去的落点是0;而发布只在"在线态翻转"时发生 ⇒ 那个0永远修不回来 ⇒ 订阅方addressOf查不到落点 ⇒ 页面打不开且不报错。修法:①presenceTouch移到ensureEndpoint之后;② 落点落地时(listen回调)与端口增删时强制重推(publishPresence(..., force))。新用例T32先红后绿覆盖(先红实测localPort=0)。真机复验:ops/w-106的localPorts=[{port:19000, localPort:35289}](非 0)。 - ⚠️ 已登记的既存缺陷(本单范围外,⛔ 未动):
web/server.ts的translateEndpoint走了与addressOf不同的键口径(表键是逻辑名、传入裸hostId⇒ 恒undefined)⇒ 该分支的订阅回退故意未加(加了也是死代码);要改须另立单(R7)。 - ✅
--scene all本轮结果 = 12 PASS / 0 SKIP / 0 FAIL(唯一"附带影响":幕 4 前置含一次 Manager 重启 ⇒ 可能再次收掉实例 scope;未出现 FAIL)。 - ⏳
translateEndpoint键口径既存缺陷(见上第 3 条)⇒ 建议并入序 ⑳ 或另立单。 - ⚠️ 动态值提醒:
w-106的 relay 落点localPort是动态值(本轮 = 35289),⛔ 不得写死进参数表。
8.9 本单指纹(可复现)
sed '/^## §8 回报格式/,$d' 交接单_presence在线态_20260917.md | md5sum = f612858344077420cff5f1f9ef1c942f(回填后保持不变 ✅ —— §8 之后的内容不影响前缀)。
8.10 序 ⑳ 执行棒回报(2026-09-17 18:26–18:5x · OBS-09 口径修正 + presence 真机收益量化)
本棒四项:① OBS-09 口径 ② presence 真机收益量化 ③ 收口复验 ④
translateEndpoint键口径缺陷处置。 ⚠️ 本棒只碰两个文件:代码仓scripts/overlay-probe.cjs+ 工作区根参数表_覆盖网络_20260917.md。src/**零改动。
① OBS-09 口径修正(产物 = scripts/overlay-probe.cjs)
新口径 = 在册 ⇒ 必须可达:在册判据 = /status.endpoints[] 中存在该实例端口且 online === true;在册 ⇒ 实例面码 ∈ PROBE_CODE_SET(否则 FAIL);不在册 ⇒ SKIP(⛔ 不是 PASS —— 那会掩盖"该在册却掉出端点表",那一类由 OBS-01/OBS-08 守)。
新增夹具参数 --instance-fixture <本机码>,<对端码>(实例面码不在任何既有夹具里 ⇒ 必须新增;⛔ 无此参数时在册仍判 ⇒ 会 FAIL 并点名,属契约面、故意如此)。add() 增加显式 skip 位;judgeObs09() 提到分支外 ⇒ 两种模式都判(否则夹具证不了"能判 PASS 也能判 FAIL")。
两侧夹具实证(三段原文;夹具 = 47 上抓的实况** /status + ss + nft -j)**
| 侧 | 输入 | 原文输出 | 退出码 |
|---|---|---|---|
| A 不误红 | 端点表补上 20000/21000 两条在线条目 + --instance-fixture 401,401 |
⚠️ FIXTURE PASS OBS-09 在册实例面 本机:20000=401 w-106:41695=401 (阈值 ∈ {200,401})|不在册 0 项 |
0 |
| B 能判真红 | 同上夹具 + --instance-fixture 502,000 |
⚠️ FIXTURE FAIL OBS-09 … |❌ 本机:20000=502 w-106:41695=0;❌ 1 项红:OBS-09 |
1 |
| C 不在册 ⇒ SKIP | 实况端点表(只有 w-106:19000) |
⚠️ FIXTURE SKIP OBS-09 在册实例面 无 (阈值 ∈ {200,401})|不在册 2 项(记 SKIP 的依据):本机:20000,对端:21000 |
0 |
| D 契约面 | 在册但不给 --instance-fixture |
⚠️ FIXTURE FAIL OBS-09 … 本机:20000=NaN … |
1 |
真机(非夹具):SKIP OBS-09 在册实例面 无 (阈值 ∈ {200,401})|不在册 2 项:本机:20000,对端:21000、rc=0 —— 修前该项恒红(序⑲ 的唯一红项),现已转为"明确的环境态"。
自检:grep -cE "[0-9]{3,}" scripts/overlay-probe.cjs = 0(零数字纪律未破)|node --check OK。
参数表:§6 OBS-09 判据格已改写 ⇒ 指纹 13de5f9b77c486d71e5b83ec909b17b2 → 659c08949c193102f774822905fde3b7(§6 只改判据文字、⛔ 无数值改动)。
② presence 真机收益量化 —— 🔴 实测收益 ≈ 0(设计目标未达成)
算式:R_base = 60 000 / RELAY_STATUS_POLL_MS(5000) = 12 次/分钟;R_now = (ΔstatusHits − 探针自身读数) / 窗口分钟;降幅 = R_base / R_now。
| 口径 | 实测 | 降幅 |
|---|---|---|
| 60 s 窗口(47 relay,T0=1789641041) | statusHits 191→204(Δ=13,自身 2)⇒ 11 次/分钟 |
1.09× |
| 12.4 min 窗口(T0=1789641041 → 1789641786) | statusHits 191→334(Δ=143,自身 8)⇒ ≈10.9 次/分钟 |
1.10× |
| 门开关占空比(journal 时间线) | 35 min 内 11 次切换;挂起窗口 7/20/20/10/45 s = 102 s / 2040 s ⇒ 5.0% | 理论 1.05×(与实测吻合) |
🔴 根因(代码级):presenceFresh()(src/net/relay/client.ts:593-600)= state==='subscribed' && now − max(lastSnapAt,lastPushAt) ≤ PRESENCE_TTL_MS(45 s);而稳态下 relay 一帧都不推(pushed=1 / snaps=1 自 relay 重启起未增,实测 Δpushed=0)⇒ 45 s 后必然过期 ⇒ pollSuspended 门重开、轮询恢复。"零帧"(收益的来源)与"帧新鲜度"(门的判据)互相打架 ⇒ 已登记为在册缺陷 P-1(交接单_观测口径与在册缺陷_20260917.md §13)。
⚠️ 口径勘误(必读):序⑲ 的"降幅 63× / 10.5× / ∞"是帧的口径(pushed),不是 /status 轮询降幅;本棒给出后者 = 1.10×。
⚠️ 附带口径提醒:OBS-13 的"活性证明 ΔstatusHits ≥ 1"由探针自己两次读即满足 ⇒ 它不能证明"轮询停了"(真机实测 Δ=3/3 s,其中 2 次是探针自身)。⛔ 别把它读成收益证据。
③ 收口复验三件套 + 归零
| 项 | 结果 | 基线 |
|---|---|---|
npm test(Node 22) |
170 tests / 169 pass / 0 fail / 1 skip | = 序⑲ 基线 ⇒ 零回归 |
overlay-probe --table(真机) |
14 PASS / 0 FAIL / 1 SKIP,rc=0 | 原为 15 PASS / 1 FAIL ⇒ OBS-09 转 SKIP |
--scene all --table |
12 PASS / 0 SKIP / 0 FAIL,rc=0(幕1-A 18898ms/幕4-A 23183ms+豁免回切 20887ms) | = 序⑲ 基线 ⇒ 零回归 |
| 收口归零 | 47 dshs=active、dshs-relay=active、池口 64;Manager 已归零回 47 |
✅ |
⚠️ 106 单元状态未取到(ssh 106.54.21.172 rc=255 = MaxStartups 限流,本线已知)⇒ ⛔ 放宽 sshd 命中 R5 ⇒ 只报告。
④ web/server.ts#translateEndpoint 键口径缺陷 —— 判定 = 另立单(本棒未动手)
证据(代码级):hostVia.set(name, row.via)(:733,name = logicalName(networkId, row.id) = ops/w-106,键 = 逻辑名)vs 调用方 src/supervisor/remote-spawner.ts:321 this.translateEndpoint(host.hostId, raw)(裸 hostId)⇒ :831 hostVia.get(hostId) 恒 undefined ⇒ 早退原样透传 ⇒ 该闭包(含 :840 的 ${hostId}:${ep.port} 快照回退,键同样应为逻辑名;对照 :689/:700 用的就是逻辑名)整体是死分支。
影响面:仅 via='relay' 的实例面(今天 = w-106);现场 ss -lntp | grep -c ':21000' = 0、journalctl -u dshs --since -6h | grep -c ':21000' = 0 ⇒ 当前零实际影响。
为何本棒不修:① 修它 = 让一条当前恒被跳过的翻译路径重新生效(数据面行为变化);② 该闭包是 buildServer 内部闭包、无法先红后绿,须先抽纯函数(= 一次小重构);③ 端到端验证需起 w-106 实例(当前无 reconciler、实例不自回)⇒ 本棒无验证条件。⇒ 登记为 P-2(缺陷单 §13,含候选修法与判据),交给下一棒。
⑤ 边界自证 + 指纹
边界:⛔ 未改任何生产值(RELAY_FAILOVER_* / HB_SEC / burst 逐项未变;参数表改动仅在 §6 判据文字)|⛔ 未新增公网监听口 / 未改 nft·nginx|🔴 COOLDOWN_MS=0 计数 0|⛔ 未 commit / 未 push(scripts/overlay-probe.cjs 仍为 M)|文件面 = 代码仓 scripts/overlay-probe.cjs(md5 4cbe34f1ffc51cfab78f0540224aa388)+ 工作区根 参数表_覆盖网络_20260917.md;src/** 零改动。
指纹:参数表(口径 = sed '/^## §10 指纹/,$d' … | md5sum)659c08949c193102f774822905fde3b7(旧 13de5f9b77c486d71e5b83ec909b17b2)|本单 §8 前缀 f612858344077420cff5f1f9ef1c942f(未变 ✅)|探针 4cbe34f1ffc51cfab78f0540224aa388。
8.11 序㉑ 执行棒回报(2026-09-17 19:0x–19:4x · P-1 与 P-2 均已修)
口径依据 = 入口 §2「本轮动作」+ 缺陷单 §13。本棒四项:① 修 P-1(门判据)② 修 P-2(键口径 ⇒ 抽纯函数)③ 零回归三件套 ④ 收口。
① P-1 已修 —— 门的判据从「载荷年龄」改为「订阅已建立 ∧ 链路活着」
我选了什么(可推翻):候选 A(任何入站帧含心跳即算"链路活着")。
- ⛔ 不选 B(relay 定期重推
SNAP):那会把稳态零帧这条设计属性打掉 —— 序⑲ E1 的机器判据就是Δpushed ≤ 0,OBS-14还依赖snaps ≤ pushed这个不变量;B 让snaps/pushed在稳态持续增长 ⇒ 直接推翻本单自己的验收口径(R11:净变差)。 - ⛔ 不选 C(拉长 TTL):只是把竞速推远(45 s → 更长),门迟早仍会自开 ⇒ 治不了"把没有变化读成没有数据"这个语义错位。
- ✅ A 的语义理由:镜像的有效性由 ① 订阅已建立(拿到过首帧
SNAP)② 通道有序可靠(WS/TCP)共同保证 —— relay 侧每一次变化必然按序到达,载荷的年龄与内容正确性无关;只有链路死才让镜像失效,而链路死由半开巡检(2.5 × hbSec无任何入站帧 ⇒ 主动断链)兜住,断链即state ≠ 'subscribed'⇒ 门自动打开、回退/status(兜底路径一行未删)。 - ⚠️ 已知残余 + 回头条件:relay 侧静默清掉订阅而 socket 仍活 ⇒ 本判据察觉不到。当前代码里不可达(唯一清空
session.subs的是显式UNSUB;relay 重启 / 会话回收都会断 socket)。已按 §9 第 10 条登记。
改法(代码级):src/net/relay/client.ts
| 位置 | 改动 |
|---|---|
presenceFresh(ttlMs?) |
判据 = state === 'subscribed' && lastSnapAt !== 0 && (now − lastFrameAt) ≤ presenceLinkSilentMaxMs(ttlMs)(lastFrameAt = 任何入站帧,含心跳) |
presenceLinkSilentMaxMs()(新增私有) |
上界 = max(服务端 TTL, 半开阈值) —— 🔴 不得比链路巡检更紧,否则门会比"链路真的死了"更早打开(= 把 P-1 换个方向重犯);测试档 TTL 被压到 3 s、心跳仍 15 s ⇒ 只用 TTL 会造假红 |
halfOpenMs()(新增私有) |
半开阈值唯一来源(startHealthWatch 与本判据共用,⛔ 不引入第二个时间口径) |
RelayPresenceStatus(+3 字段) |
lastInboundAgoMs / linkSilentMaxMs / fresh —— 让"门为什么开着/关着"可判(判别器纪律,⛔ 不静默) |
② 先红后绿(原文级)
把 presenceFresh() 临时改回旧判据(max(lastSnapAt,lastPushAt))重建后跑新用例:
not ok 1 - T33 P-1:稳态零帧下门不得自己重开(判据 = 订阅已建立 ∧ 链路活着,⛔ 不是载荷年龄)
error: |-
载荷 5243ms 没来、但链路活着(入站静默 217ms ≤ 上界 3000ms)⇒ 门必须保持关闭
# pass 0 / # fail 1
改回新判据重建后:ok 1 T33 / ok 2 T34(2 pass / 0 fail)。用例清单(test/relay.test.mjs):
T33 稳态超过 TTL 门不得自开(旧实现必红的那条);T34 反向失败关闭(未订阅 / 已退订 / 被拒 / 断链 ⇒ 一律不新鲜 —— 少这条,"永远返回 true"也能让 T33 绿)。
⚠️ T33 用夹具前提 hbSec: 1:测试档 TTL 3 s < 生产心跳 15 s,不压心跳时 relay 的 presenceDevices(now − conns[ts] ≤ ttl)会在 3 s 后把连接判死 ⇒ 自己制造"离线→在线"假帧(生产 TTL 45 s > 心跳 15 s,不存在该组合)。
③ 真机验收(47 relay,同一口径 Before / After)
算式:窗口内 ΔstatusHits = 探针自身读数(窗口内 1 次)+ Manager 侧命中 ⇒ Manager 侧 = Δ − 1。
| 口径 | 窗口 | statusHits |
Δ | Manager 侧 | 门开关 |
|---|---|---|---|---|---|
| Before(旧代码在跑,先测) | 66 s | 228 → 242 |
14 | 13(≈ 11.8 次/分钟,= 基准 12/分钟 ✅) | 窗口内 0 行(旧口径下门基本常开,翻一次后不再翻) |
After(铺新 lib + systemctl restart dshs 后) |
67 s | 247 → 248 |
1 | 0 ✅ | 窗口内 0 行 |
门开关原文(journalctl -u dshs | grep overlay-presence,重启后至今 5+ 分钟、远超 TTL 45 s):
Sep 17 19:09:53 … [overlay-presence] 订阅新鲜 ⇒ `/status` 轮询**挂起**(兜底路径待命)
--- 总行数: 1
⇒ 即"挂起"一次、零次翻转(旧代码在 TTL 边界必然翻转 ⇒ 轮询恢复)。旁证:两窗口 Δpushed = 0、subs = 1(稳态零帧、订阅在册)。
部署:本机 build ⇒ lib/net/relay/{client,index,endpoint-target}.js + lib/web/server.js scp 到 4 处 —— 47 /opt/dshs/lib / 47 /opt/dsh-relay/lib / 106 /opt/dshs-cluster/lib / 106 /opt/dsh-relay/lib,md5 全部 = 本机 6c14d4d4…;只在 47 重启 dshs(改动是 Manager 侧;relay 服务端路径未动,106 的文件待其下次重启自然生效)。
④ 观测口径:新增 OBS-16(门的验收判据;⛔ 不能只看 OBS-13)
🔴 为什么必须新增:OBS-13 的 ΔstatusHits ≥ 1 由探针自己两次读即满足 ⇒ 证明不了"轮询停了" —— 旧口径下门 95% 时间开着、轮询照旧在跑,而 OBS-13 仍然全绿(实测降幅 1.10× 却长时间没人发现,正是这条)。
新判据(scripts/overlay-probe.cjs,第三次 /status 采样,窗口 = PRESENCE_GATE_WINDOW_MS):
subs ≥ 1 且 ΔstatusHits ≥ 1(自身读数 = 活性证明)且 ≤ PRESENCE_GATE_HITS_MAX(= 1 ⇒ 除探针外零命中)。
夹具两侧实证(夹具 = 47 实况 /status + ss + nft -j 原文,仅改 counters.statusHits;新增 --status-fixture-3):
| 侧 | 输入 | 原文输出 | rc |
|---|---|---|---|
| PASS | 三连 181 / 182 / 183(窗口 Δ=1) |
⚠️ FIXTURE PASS OBS-16 门窗口 ΔstatusHits=1(≥ 1 = 探针自身读数|≤ 1 = 除探针外**零**命中),窗口 60000ms,期间 subs=1 |
0 |
| FAIL | 第三份 194(窗口 Δ=12 = 旧口径下的轮询量) |
⚠️ FIXTURE FAIL OBS-16 … ΔstatusHits=12 …;❌ 1 项红:OBS-16 |
1 |
⚠️ 夹具模式必须显式给 --status-fixture-3:缺省 = 与第二份同一份 ⇒ Δ=0 ⇒ 活性证明不成立 ⇒ FAIL 并点名(契约面,故意如此,与 OBS-09 的 --instance-fixture 同规则)。
真机(非夹具):PASS OBS-16 门窗口 ΔstatusHits=1 … 窗口 60000ms,期间 subs=1 ⇒ 15 PASS / 0 FAIL / 1 SKIP、rc=0(基线 14/0/1 + 新项)。
⚠️ 副作用(已知、有意):真机探针一轮多花 PRESENCE_GATE_WINDOW_MS(窗口必须够长,否则"轮询在跑"与"轮询停了"在计数上分不开)。
⑤ P-2 已修 —— translateEndpoint 键口径(抽纯函数 ⇒ 才可能先红后绿)
病根:控制面所有表(hostVia / relayEndpoints / 拨号池)键都是逻辑名,而调用方 RemoteSpawner.translateEndpoint(host.hostId, …) 只给得到裸 hostId ⇒ hostVia.get(hostId) 恒 undefined ⇒ 早退原样透传 ⇒ 整个闭包是死分支(:840 的快照回退键同错;:689/:700 用的正是逻辑名,自证口径)。
改法:新增纯函数模块 src/net/relay/endpoint-target.ts = relayEndpointTarget()(四支判定:未知 host / 非 relay ⇒ 原样透传;via=relay ⇒ ①拨号落点 ②快照落点 ③**unreachable 失败关闭**)+ hostNameIndex()(dsh_hosts 行 ⇒ hostId → 逻辑名);src/web/server.ts 的闭包改为先 hostNameById.get(hostId) 换逻辑名再调用它。
🔑 两处设计要点:① dialedPort 传 thunk —— RelayDialer#localPortFor 会按需绑池口 / 真的拨一次,⛔ 非 relay / 未知 host 时不许碰它(T35 把这钉成断言);② 失败关闭分支点名写日志([overlay-endpoint] ⛔ … 判实例不可达),因为原故障的表现只有"空响应 + 平台零日志"。
单测:T35(四支判定 + "⛔ 不误触发拨号池")、T36(hostNameIndex 口径 + 源码级守卫:闭包⛔ 不得再出现 hostVia.get(hostId) / localPortFor(hostId / relayEndpoints.get(\${hostId},只看代码行不看注释)。 **端到端判据(本轮未做,按入口授权"起不起来就只做到单测")**:需 w-106实例在跑(当前无 reconciler、不起就自回)。可执行判据 = ① 起实例后POST /api/dsh/enter**200**;②ss -lntp | grep -c ':21000'在 Manager 上仍为 **0**(翻译生效 ⇒ 拨的是 relay 回环落点,⛔ 不是 worker 侧口号);③journalctl -u dshs | grep -c overlay-endpoint= **0**(失败关闭未被触发);④ 反例判据:若翻译未生效,Manager 会去连127.0.0.1:21000 ⇒ 连接被拒(本线 2026-09-16 踩过的"空响应 + 零日志"同族)。 ✅ 顺带核实(DB,只读):dsh_hosts=w-106|relay|ops、w-47|local|ops⇒ **w-106 就是走被修的那条分支**;且endpoint === undefined在proxy.ts:240得到的是**显式**{ error: 'not_running', code: 404 }`(不是崩溃、也不是静默)⇒ 失败关闭语义干净。
⑥ 零回归三件套(全绿,均带 --table)
| 项 | 结果 | 基线 |
|---|---|---|
npm test(Node 22) |
174 tests / 173 pass / 0 fail / 1 skip(= 170/169/0/1 + 新增 T33–T36) | 170/169/0/1 ⇒ 零回归 |
--scene all --table |
12 PASS / 0 SKIP / 0 FAIL,rc=0(幕1-A 19060ms/幕4-A 22543ms+豁免回切 16512ms/幕4-B/4-C 全绿) |
12/0/0 ⇒ 零回归 |
overlay-probe --table |
15 PASS / 0 FAIL / 1 SKIP,rc=0(OBS-09 SKIP = 在册判据、OBS-16 PASS) |
14/0/1 ⇒ +1 项、零回归 |
| 收口归零 | 47 dshs/dshs-relay active、106 dshs-relay/dshs-worker active、Manager 归位 47、池口 76 行监听(ss -lntn 计数为 76 —— ⚠️ 与序⑳ 的"池口 64"口径不同:后者是 relay 自报的池容量 gauge,前者是本机监听行数,两者本就不同量,此处只作"未新增暴露面"的旁证,OBS-11 集合判据已绿) |
✅ |
⑦ 边界自证 + 指纹
边界:⛔ 未改任何生产值(RELAY_FAILOVER_* / HB_SEC / burst 逐项未变;PRESENCE_TTL_MS 值未改,只改判据语义)|⛔ 未新增公网监听口 / 未改 nft·nginx|🔴 COOLDOWN_MS=0 计数 0(含回滚/演练/夹具路径)|⛔ 未删 /status 兜底路径|⛔ 未改 switcher.ts 冷却语义|⛔ 未 commit / 未 push。
文件面:代码仓 = src/net/relay/client.ts(c5d395395324e17edf9d269f34e8b3af)|src/net/relay/endpoint-target.ts(新增,24ac70edc52adec4928513459a3e0160)|src/net/relay/index.ts|src/web/server.ts(adf69e484cf00e95a132c7f6c0ae8c65)|test/relay.test.mjs(b925a672c5fb897f0598ef89d2f2bde8)|scripts/overlay-probe.cjs(61a9fc5623bb617c802daf604d3ce0f3,旧 4cbe34f1ffc51cfab78f0540224aa388);工作区根 = 参数表_覆盖网络_20260917.md。
指纹:参数表(口径 = sed '/^## §10 指纹/,$d' … | md5sum)42238175d84319ada99afa56d583f9db(旧 659c08949c193102f774822905fde3b7;本轮 = §3.6 +2 键、§6 +1 项 +2 阈值键、§3.6 增"P-1 口径变更"注)|本单 §8 前缀 f612858344077420cff5f1f9ef1c942f(未变 ✅,§8.11 在截断点之后)|本单全文件 md5(回填前)b070e4228d061bb39a62d2aa0c11e416。
登记(⛔ 本棒未动手,只登记):P-2b —— relayEndpointTarget 目前只有"拨号落点 → relay 快照"两级;P-1 修好后 /status 快照路径趋冷(门长期挂起 ⇒ 快照很少刷新),当拨号池分不出槽位而订阅推送里有落点时仍会判失败关闭。候选 = 把 presenceLocalPort(订阅落点)插成第 ②'级(与 addressOf 的三级链对齐)。⚠️ 属行为面新增,按 R7「先报告后动手」登记,未在本棒实施。
8.12 序 ㉒ 执行棒回报(2026-09-17 19:33–20:0x · P-2b 已修 + P-1 长窗复测通过)
口径依据 = 入口 §2「本轮动作」+ 缺陷单 · 在册缺陷表 P-2b 行。本棒三件:① 修 P-2b ② P-1 修复后的**长窗(≥ 10 min)**真机复测 ③ 零回归三件套 + 收口。
① P-2b 已修 —— 候选链由「两级」扩成「三级」,与 addressOf 逐级对齐
病根(一句话):relayEndpointTarget 只查 ① 拨号落点 → ② relay 快照;而 addressOf(RelayRendezvous)查 ① 拨号 → ② 订阅推送(presenceLocalPort)→ ③ 快照。P-1 修好之后判据变成「订阅已建立 ∧ 链路活着」⇒ 订阅新鲜期长期成立 ⇒ 快照(②)趋冷;此时若拨号池分不出槽位(跨网被拒 / 池满 / 尚未绑口),本判定会落到"两级都没有" ⇒ 判实例不可达(失败关闭),而同一时刻 addressOf 明明能从订阅推送里答出落点 ⇒ 两条链漂移:用户看到"实例打不开",日志里却什么都没有。
改法(三个在册文件,⛔ 未越界):
| 文件 | 改动 |
|---|---|
src/net/relay/endpoint-target.ts |
新增入参 pushedLocalPort?: number;via 联合类型加 'pushed';判定顺序变 ① dialed → ② pushed → ③ snapshot → 失败关闭;模块头部的判定表与"两级 → 三级"的理由写进注释 |
src/web/server.ts |
闭包多传一行 pushedLocalPort: name === undefined ? undefined : presenceLocalPort(name, ep.port)(与 addressOf 的 ② 级同一个函数、同一个顺序);失败关闭的点名日志改成"拨号落点 / 订阅推送 / relay 快照三级都查不到" |
test/relay.test.mjs |
新增 T37(三级逐支可判 + 0 哨兵逐级下探 + 未知 host / 非 relay ⛔ 不碰拨号池)/T38(源码级守卫:闭包必须把 presenceLocalPort(name, ep.port) 传进判定 —— 无实例时该分支根本不执行,运行时断言抓不到) |
先红后绿(原文级 · 同一份测试体):临时把 build 产物 lib/net/relay/endpoint-target.js 里的 ②' 级整段去掉(源码不动)后跑:
not ok 1 - T37 P-2b:候选链三级(拨号 → 订阅推送 → relay 快照)逐支可判,⛔ 不误触发拨号池
error: |-
P-2b:拨号给不出时,订阅推送的落点必须先于 relay 快照被采用(与 `addressOf` 对齐)
+ actual - expected
{ kind: 'local',
+ port: 41000,
+ via: 'snapshot'
- port: 37057,
- via: 'pushed' }
# pass 0 / # fail 1
tsc 重建后:T37 / T38 2 pass / 0 fail。⇒ 红确实是"少这一级"造成的,不是测试写歪。
② P-1 长窗(706 s ≈ 11.8 min)复测 —— 门 0 次翻转、Manager 侧 0 命中
算式(与序㉑ 同口径):Manager 侧 = 窗口内 ΔstatusHits − 探针自身读数;窗口开端读一次(计入 T0 读数本身)、末端读一次 ⇒ 自身读数 = 1。窗口内不跑探针(探针的 OBS-16 自己会占一次窗口,会污染计数)。
| 口径 | 窗口 | statusHits |
Δ | Manager 侧 | 门开关行数 | Δpushed / Δsnaps |
|---|---|---|---|---|---|---|
| 入册(短窗) | 67 s | 247 → 248 |
1 | 0 | 0 | — / — |
| 本棒(长窗) | 706 s | 9 → 10 |
1 | 0 ✅ | 0 ✅ | 0 / 0 ✅ |
相对基准的倍数:基准 = 60 000 / RELAY_STATUS_POLL_MS(5 000) = 12.000 次/分钟;实测 raw Δ = 1 / (706/60) = 0.085 次/分钟 ⇒ 141.2×;Manager 侧 = 0 次/分钟 ⇒ 无穷倍(分母为零)。
门开关计数原文:journalctl -u dshs --since @<T0> | grep -c overlay-presence = 0(窗口 706 s ≫ PRESENCE_TTL_MS 45 s ⇒ 若判据仍是"载荷年龄",这里必然 ≥ 1 ⇒ 长窗是对 P-1 修复的独立复核,不只是短窗的重复)。
旁证:窗口两端 subs = 1(订阅在册)、pushed = 1 → 1、snaps = 1 → 1 ⇒ 稳态零帧,正是 P-1 的病理前提。
③ 零回归三件套(全绿,均带 --table;部署后重跑同值)
| 项 | 结果 | 基线 |
|---|---|---|
npm test(Node 22) |
176 tests / 175 pass / 0 fail / 1 skip(= 174/173/0/1 + T37/T38) | 174/173/0/1 ⇒ 零回归 |
--scene all --table |
12 PASS / 0 SKIP / 0 FAIL,rc=0(幕1-A 15561ms/幕4-A 23612ms + 豁免回切 21145ms) |
12/0/0 ⇒ 零回归 |
overlay-probe --table |
15 PASS / 0 FAIL / 1 SKIP,rc=0(OBS-09 SKIP = 在册判据;OBS-16 PASS ΔstatusHits=1) |
15/0/1 ⇒ 零回归 |
| 收口归零 | 47 dshs/dshs-relay/dshs-pg active |106 dshs-relay/dshs-worker active |监听行 76 / 池口 64 |overlay-endpoint 失败关闭点名 0 行 |
✅ |
⚠️ 观察项(非回归,⛔ 未处置):OBS-15 在线态陈旧 p95 = 14220 ms(阈值 ≤ 15000 ms,样本 2 条)—— 贴线但未越界;同批另一次采样为 5347 ms ⇒ 样本少、抖动大。本棒未改任何阈值 / 未放宽判据,仅登记。
④ 部署 + 回滚点
npm run build ⇒ lib/net/relay/{endpoint-target,index}.js + lib/web/server.js scp 到 4 处:47 /opt/dshs/lib+/opt/dsh-relay/lib、106 /opt/dshs-cluster/lib+/opt/dsh-relay/lib ⇒ 四根 md5 全同:
36b2dd9953a615de9a3e148752dffe94 net/relay/endpoint-target.js
486419ba443b99dd734ca79e987ffdea net/relay/index.js
460d360e8c6196341935b2f9a2a1d408 web/server.js
只重启 47 的 dshs(改动是 Manager 侧)。回滚点(两机同路径)= 把本棒的两处 hunk 反向改回 → tsc → 得到的精确旧产物(⛔ 不是"凭记忆重写"):
/opt/dsh/backups/seq22-20260917-195602/{net/relay/endpoint-target.js,net/relay/index.js,web/server.js}
57e6c9ae81d64f6586d758f5dc021a4b endpoint-target.js(旧) 486419ba443b99dd734ca79e987ffdea index.js(未变)
74c077b33c6d8afdef980171c19eee26 server.js(旧)
⑤ 本棒两条环境/流程教训(下一棒别踩)
- 🔴 106 的 ssh 必须用别名
test106(专用密钥id_ed25519_test106,IdentitiesOnly yes;参数表SSH_TARGET_106本就写着)。裸ssh [email protected]⇒Permission denied (publickey)—— 本棒先误判成"MaxStartups 限流 rc=255"并据此放弃,后来抓 stderr 才看到真因,改用别名后一次通过。⛔ 后续任何一棒不许再把 106 连通失败笼统归因成限流。 - ⚠️ 原生
scp不认E:/…混写路径(Git Bash 里cd E:/…能用,但scp 'E:/…/f'会No such file or directory)⇒ 一律 先cd到目录、再用相对路径;本棒的"回滚点落盘"第一版就是因此静默空跑(只打印了一行空 md5),已重跑纠正。
⑥ 登记(⛔ 本棒未动手)
- E2E(P-2 + P-2b)仍未做 —— 47 上活跃实例 scope = 0(
systemctl list-units 'dsh-*.scope'空;:21000监听 = 0)⇒ 判据仍是本单 §8.11-⑤ 那四条,交序 ㉓。 - 在册缺陷表已清空(P-1 / P-2 / P-2b 全部关闭);剩余只有 §9 第 10 条(回头条件,非在册缺陷)+ E2E 缺口 + 两项待拍板(
B内容分发 / 「Manager 重启后是否自动拉起既有实例」)。
8.13 序 ㉓ 执行棒回报(2026-09-17 20:08–20:4x · P-2 / P-2b 的 E2E 已验收通过 + 在册缺陷表清空 + 线内可停)
口径依据 = 入口 §2「本轮动作」+ 本单 §8.11-⑤(E2E 四条判据)。本棒两件:① 主 = P-2/P-2b 的 E2E ② 次 = presence 线收口复核 ③ 零回归三件套 + 收口。
① 前置(T0 = 2026-09-17 20:10:57,epoch 1789647057)
| 项 | 值 |
|---|---|
| 47 单元 | dshs / dshs-relay / dshs-pg = active |
dsh_hosts(只读) |
w-106 → http://127.0.0.1:19000 via=relay | w-47 → http://127.0.0.1:19100 via=local |
relay /status |
dial=0 dialDenied=0 dialFailed=0 statusHits=14 subs=1 pushed=2 snaps=2|endpoints=[{port:19000,hostId:w-106,localPort:39525}] |
47 上 :21000 / :19000 监听 |
0 / 0 |
overlay-endpoint 日志(全量历史) |
0 行 |
47 上 dsh-*.scope |
0(⚠️ guest 实例在 106 上 ⇒ 见 ④) |
② 四条判据(E2E 主体 · 全部通过)
手段 = R4 临时 session:node /opt/dshs/mksess-guest.cjs ⇒ POST http://127.0.0.1:3080/api/dsh/enter(--http1.1,同一条连接连发两次)。
| # | 判据 | 期望 | 实测 | 判定 |
|---|---|---|---|---|
| ⓐ | enter HTTP 码 |
200 | …|200 + …|200(同连接双发)+ 复测 6 轮全 200 |
✅ |
| ⓑ | 47(Manager)上 ss -lntp | grep -c ':21000' |
0 | 0(:19000 亦为 0) |
✅ |
| ⓒ | journalctl -u dshs | grep -c overlay-endpoint |
0 | 0(窗口内 delta = 0、全量历史 = 0) | ✅ |
| ⓓ | relay /status counters.dial 增量 |
> 0 | 0 → 2(denied=0 / failed=0) |
✅ |
enter 响应体原文(截断):
{"kind":"session","instance":{"id":"b5e5b0cd-…","port":21000,"status":"running","launchToken":"dVQHA02lJYqOE8UyRJzJoTuOmpd0MBd9oMkoUzCg9Js"},"url":"https://guest.alotbuy.com/?token=…"}
③ 判别器证据(比 dial 计数更强的一条)—— 两条链各自都拨到了
20:10:59 [relay-dialer] 落点 127.0.0.1:25000 -> ops/w-106:19000 ← agent 面
20:10:59 [relay-client] dial w-106:19000 up (stream=2)
20:13:02 [relay-dialer] 落点 127.0.0.1:25001 -> ops/w-106:21000 ← 实例面
20:13:03/04 [relay-client] dial w-106:21000 up (stream=3/4/5)
🔑 为什么这条是 P-2 / P-2b 的直接验收:dsh_hosts 里 w-106 的 endpoint 是 http://127.0.0.1:19000(worker 侧口号)。拨号日志里出现的是逻辑名 ops/w-106:<口号>(19000 = agent 面、21000 = 实例面),且 47 本机 :21000 / :19000 监听恒为 0 ⇒ 说明:① hostId → 逻辑名 的键口径修对了(P-2)② 拨的是 relay 回环落点、⛔ 不是 worker 侧口号 ③ 实例面那条走的就是 P-2b 新增的候选路径。同刻 /status 出现两条端点:{port:19000,localPort:39525} + {port:21000,localPort:43431}。
④ 实例真身在 106(旁证)+ 用户可见面 200
- 106 侧:
dsh-100002-ef8d1d12.scope= active running,argv 末尾 =dsh --profile web --host 127.0.0.1 --port 21000⇒127.0.0.1:21000在 106 上监听(count_21000=1),agent 口19000亦在听;dshs-worker/dshs-relay= active。 ⇒ "47 上:21000= 0" 与 "106 上有实例在听 21000" 同时成立 = 翻译生效的强判据(若翻译失效,Manager 会去连 47 自己的127.0.0.1:21000⇒ 连接被拒 ⇒ 正是本线 2026-09-16 踩过的"空响应 + 零日志")。 - 用户可见面(补齐
OBS-09的语义):curl -H 'Host: guest.alotbuy.com' http://127.0.0.1:3080/⇒ 200、62451 字节真 HTML(<!doctype html> … __ModuleLoader__)、502/Bad Gateway标记 0 ⇒ 序⑬ Q2(guest(w-106) 实例页 502)的判据现已满足。
⑤ 零回归三件套(全绿;均带 --table)
| 项 | 结果 | 基线 | 判定 |
|---|---|---|---|
npm test(Node 22) |
176 tests / 175 pass / 0 fail / 1 skip | 176/175/0/1 | ✅ 零回归 |
--scene all --table |
12 PASS / 0 SKIP / 0 FAIL,rc=0(幕1-A 17494ms/幕4-A 22234ms + 一跳豁免 17996ms) |
12/0/0 | ✅ 零回归 |
overlay-probe --table |
16 PASS / 0 FAIL / 0 SKIP,rc=0 |
15/0/1 | ✅ +1(better) |
🔴 overlay-probe 由 15/0/1 → 16/0/0 的原因 = OBS-09 由 SKIP 转 PASS:
PASS OBS-09 在册实例面 w-106:37747=401 (阈值 ∈ {200,401})|不在册 1 项(记 SKIP 的依据):本机:20000
⇒ 这正是序⑲ §9-8 写下的回头条件("任一用户访问自己的实例页后重跑探针 ⇒ OBS-09 应回 PASS")已兑现,且不是真回归。其余 15 项同基线(OBS-11 实际 77 多出 0 缺失 0 = 端点数 1→2 各占一个回环落点,集合判据已绿;OBS-15 p95 = 1770 ms ≪ 15000 ms)。
收口归零:47 dshs/dshs-relay/dshs-pg active、门户 200、池口 64、:21000 监听 0|106 dshs-worker/dshs-relay active。
⑥ 边界自证 + 指纹
⛔ 未改任何代码 / 未改任何生产值 / 未动 nft·nginx / 未新增公网监听口 —— 本棒只做只读取证 + 临时 session。逐项:
| 项 | 期望 | 实测 |
|---|---|---|
参数表指纹(sed '/^## §10 指纹/,$d' … | md5sum) |
42238175d84319ada99afa56d583f9db(未变) |
42238175d84319ada99afa56d583f9db ✅ |
| 本单 §8 前缀 | f612858344077420cff5f1f9ef1c942f(未变) |
f612858344077420cff5f1f9ef1c942f ✅ |
in-册代码面 md5(endpoint-target.ts / web/server.ts / relay.test.mjs) |
与序㉒ 同值 | 9f421a9f… / 8c4c072c… / f08088b8… ✅ 全同 |
| 代码仓 HEAD / 改动数 | 04776af / 10 |
04776af / 10 ✅ |
🔴 COOLDOWN_MS=0 命中(scripts+src+test) |
0 | 0 ✅ |
| R4 临时 session 残留 | 0 | 0 ✅(两轮用完即删,user_agent='poc-curl2') |
| ⛔ commit / push | 无 | 无 ✅ |
⑦ 收口复核结论(本棒第二件)—— 线内可停
| 项 | 状态 |
|---|---|
| P-1(presence 新鲜度判据) | ✅ 已修(§8.11-①;长窗复核见 §8.12-②) |
P-2(translateEndpoint 键口径) |
✅ 已修 + E2E 验收通过(本节 ②③) |
| P-2b(候选链两级 → 三级) | ✅ 已修 + E2E 验收通过(本节 ②③) |
| E2E 缺口(§8.11-⑤) | ✅ 已补齐(本节 ②③④) |
| 在册缺陷表 | 空(P-1 / P-2 / P-2b 全关闭) |
| §9 第 10 条(relay 静默丢订阅) | 回头条件(当前代码路径不可达)⇒ 非在册缺陷、无需动作 |
| 清单 §五 第 7 步剩余(内容分发 · 房间层 · 游戏) | 全部待拍板 |
| 入口 §4(骨干节点服务范围 A/B) | 待拍板 |
| 「Manager 重启后是否自动拉起既有实例」 | 待拍板 |
⇒ 覆盖网络线(presence 部分)线内可停:在册缺陷清空、E2E 补齐、剩余项全部落在「需拍板」区。按登记门禁(用户 2026-09-17 明令:待拍板未定 ⇒ 不登记下一棒)本棒不登记接续,等拍板后再建。
⑧ 本轮新登记(⛔ R7:只报告、未动手)
- O-1(线外 · 集群化/模型落地面):
enter时 47 日志出现model landing failed, falling back to env injection Error: ENOENT … open '/var/lib/dshs/users/4092b965-…/home/.credentials.yaml'—— 即用本地路径读远端实例 home(与序⑬ Q2 所记真因同族)。影响:landModels退化为 env 注入(有兜底、用户可见面仍 200);归属:RemoteSpawner.resolveApiKey(平台模型落地),不在本线 §3.1 文件集 ⇒ 仅登记。 - O-2(观察项):
OBS-15p95 本棒 = 1770 ms(序㉒ 曾见 14220 ms,阈值 15000 ms)⇒ 抖动主导、未越界、⛔ 未改阈值。
登记:本棒不登记下一棒(线内可停 + 剩余项全为拍板项);下一棒的触发条件 = 用户对 B(内容分发)/「Manager 重启后自动拉起实例」/入口 §4 骨干服务范围中的任一项拍板。
回填后指纹自检:§8.13 位于 §8 之后 ⇒ §8 前缀指纹 f612858344077420cff5f1f9ef1c942f 应保持不变。
§9 回头条件(一出现必须回头;⛔ 不许自行扩范围、⛔ 不许调生产值去凑)
- 要动生产值才能继续 —— 需要改
RELAY_FAILOVER_*/HB_SEC/ burst 任一 ⇒ 停下报告(D1)。 - 要扩大暴露面 —— 需新增公网监听口 / 放开遮蔽 / 跨网络或跨租户订阅 ⇒ 命中 R5 ⇒ 停下出权限评估。
- 要超出 §3.1 的文件集 —— 除参数表 / 测试 / 探针 / 文档外,改动溢出到
src/**的其他层 ⇒ 停下报告,别扩大。 - 批合并失效 —— 一次状态变化推不出 ≤1 帧,或稳态出现非零 presence 帧 ⇒ 停下报告(D4 的前提被证伪)。
- 观测跑不出单值 ——
OBS-13/14/15在真机拿不到可判定的数字 ⇒ 停下报告,改口径(⛔ 不许把未验证的项标 PASS)。 - 非静态失败 —— 跨网络订阅被静默放行(而非显式拒绝),或未知帧号被静默丢弃 ⇒ 立即停(假绿,本线头号教训)。
- 要求"先有房间层才能验收" ⇒ 停下报告:本单口径是节点层单值判据,房间层不在范围内。
- 零回归退化 ——
npm test低于 162/161/0/1、或--scene all出现 FAIL、或探针低于 12/12 ⇒ 停下报告。 - 🔴 任何回滚 / 演练 / 夹具路径要写
RELAY_FAILOVER_COOLDOWN_MS=0⇒ 禁止(不是"回头",是禁令)。 - 🆕 序㉑(P-1 修复)新增:relay 侧"静默丢订阅、socket 仍活" ⇒ 门的判据(订阅已建立 ∧ 链路活着)察觉不到,镜像会长期不动而没人知道。当前代码里这条路径不可达(唯一清空
session.subs的是显式UNSUB;relay 重启 / 会话回收都会断 socket)。一旦出现(现象 = 订阅在册却再也收不到任何PRESENCE帧,且subsgauge 与客户端state不一致) ⇒ 正解 = 心跳帧携带订阅态断言,或周期性SNAP复核;⛔ 不许靠"缩短 TTL"(那只是把 P-1 反向重犯)。
§10 指纹与状态
| 项 | 值 |
|---|---|
| 本单 §8 前缀指纹 | f612858344077420cff5f1f9ef1c942f(落单时算出,2026-09-17 17:2x;口径 = sed '/^## §8 回报格式/,$d' … | md5sum) |
| 参数表指纹(改前) | 8f08e74b026e6e5b5e1b3db813f031ae(序⑰ 收口值,2026-09-17 17:2x) |
| 代码仓 HEAD(落单时) | bc0dd2c(已 push,远端 master 一致) |
| 本单状态 | 已执行完毕(线内可停):序⑲(落地 + 真机验收)→ 序⑳(OBS-09 口径修正 + 收益量化 ⇒ P-1 在册)→ 序㉑(P-1 + P-2 均已修,§8.11)→ 序㉒(P-2b 已修 + P-1 长窗复测通过,§8.12)→ 序㉓(P-2 / P-2b 的 E2E 验收通过 + 在册缺陷表清空 + 线内可停,§8.13) ⇒ 剩余 = §9 第 10 条(回头条件)+ 三项待拍板(B 内容分发 / 「Manager 重启后自动拉起实例」/ 入口 §4 骨干服务范围) |
| 参数表指纹(现行) | 42238175d84319ada99afa56d583f9db(序㉑/序㉒/序㉓ 收口值;序㉒ 与 序㉓ 均未变 —— 两棒无数值改动 ⇒ 同时是"零生产值改动"的自证;前值链 659c08949c193102f774822905fde3b7 ← 13de5f9b77c486d71e5b83ec909b17b2 ← 8f08e74b026e6e5b5e1b3db813f031ae) |
| 本棒边界自证 | ⛔ 未改码 / ⛔ 未动 47·106 / ⛔ 未 commit·push / ⛔ 未改任何生产值 / 🔴 COOLDOWN_MS=0 仅以"禁令"形式出现 |
§11 规划棒回报(序 ⑱ · 2026-09-17 17:22–17:2x)
做了什么:只出规划 —— 产出本单(287 行 / 纯 LF),⛔ 零代码改动、⛔ 零服务器改动、⛔ 零 commit / push、⛔ 未改任何生产值。
本棒实测三条证据(§0.1 原文):grep -rilE "\b(room|chat)\b" src | wc -l = 0(零应用层)|grep -ril presence src = 仅 src/net/relay/placement.ts(第 21 行是注释)|grep -n "req.url === '/status'" src/net/relay/server.ts = 413 且 1234 行原文写明「/status 是 Manager …」⇒ 现役在线态 = 拉快照(要替换的那条路)。
本棒的核心判断:入口 §2 旧结论「presence 无从下手」要收窄 —— 正确表述是「应用层 presence 无从下手、节点 presence 正当时」:序 ①–⑰ 已把它全部前置做完,且落地载体(src/net/relay/** 共 15 个模块)今天就存在。
判据与边界:
- 单内 §4.2 真取舍 = 空、§4.3 待拍板 = 空(三个曾考虑的项均有客观判据 ⇒ 自决拍掉);§4.4 R5 评估 = 未命中(新增监听口 / 凭据 / 入站均 0,可见面只收窄)。
- ⛔ 未规划 B(内容分发 / 块级内容寻址) —— 仍待拍板,只登记一行(§0.4-1 / §3.3-1)。
- ⛔ 未为生产值留任何改动口;§9-9 把
RELAY_FAILOVER_COOLDOWN_MS=0写成硬禁令(不是"回头条件")。
登记:下一棒 = 序 ⑲ 执行棒,automation 107b8e38-ccbd-4dcf-8b49-25dad7c89be9(名称「覆盖网络线-序19执行棒-presence节点在线态」,一次性,scheduledAt = 2026-09-17 17:28 = 收口 +4 min,nextRunAt = 1789637280000)。
回填后指纹自检:§11 位于 §8 之后 ⇒ §8 前缀指纹 f612858344077420cff5f1f9ef1c942f 应保持不变(口径 = sed '/^## §8 回报格式/,$d' 交接单_presence在线态_20260917.md | md5sum)。