Files
dsh_shenxian/dsh-server-docs/交接单/archive/交接单-已完成/T15-presence在线态.md
T
admin 04776af4b1 docs: 工作区根项目文档批量入仓(04-调整方案 113–128 / 交接单 T09–T21 / ops / archive)
起因:用户 2026-09-17 明确「所有文档都要同步,都放在开发仓库 docs 对应文件夹下」。
判据:工作区根 *.md 中在仓库(git ls-files --quotepath=false)搜不到的那些。

入仓 33 份(一律复制,工作区根原件保留不动,避免引用断链):
- 04-调整方案/113–128(16 份 · 原子占号后落盘):覆盖网络 传输方案取舍 / 应用场景与待完善清单 /
  插件化vs改内核 / 问题逐条推演 / 参数表与观测口径;会合中继拆分取证与改造方案;
  集群化改造方案 Manager-Worker;跨节点迁移与节点自举;项目代码分层范式与迭代风险评估;
  搬运与共享重建方案 guest w47→w106;方案规划方法提炼;文档无效信息审计报告;
  会话接续机制复盘与修复;会话接续规范;dsh 客户端化部署方案;dsh 桌面客户端开发方案
- 交接单/archive/交接单-已完成/T09–T21(13 份 · 覆盖网络线已完成单归档)
- ops/(2 份运行态指针:接续入口 / 接续包 · 覆盖网络线)
- archive/(2 份临时与内部简报)

已排除(无需重复入仓):覆盖网络线 10 份方案正文已入档案 103–112(文件名不同)。

登记:INDEX.md §二 新增 04-113–128 共 16 行 + §四 追加 T09–T21 说明 + 机器摘要行刷新
(⛔ 未跑 docs-index-stats.py --write:该脚本会按 \r\n 归一化全文件行尾,故改为字节级单行替换);
README.md 追加 1 条入仓指针;docs-manifest.json 复跑 scripts/docs-manifest.py 刷新。

验收:工作区根 47 份 .md —— 同名已入仓 8 / 本次内容一致 29 / 已知改名映射 10 / 未入仓 0;
git status 待提交清单只含本次新增与登记 3 件(未涉 src/ 与 relay 代码面)。
2026-09-17 18:24:19 +08:00

41 KiB
Raw Blame History

交接单 · 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 明确不动的(⛔ 防止顺手扩大)

  1. ⛔ 不动任何生产值:RELAY_FAILOVER_*(6 键)/ HB_SEC / burst 语义 —— 这是 D1,也是本线长期硬门。
  2. ⛔ 不删 /status —— 它是回滚链的一环(§7-②)。
  3. ⛔ 不新增公网监听口、⛔ 不改 nft、⛔ 不改 nginx(本单是纯应用层改动,暴露面零变化 ⇒ §4.4 R5 未命中)。
  4. ⛔ 不改 src/net/relay/switcher.ts 的冷却语义 —— presence 抖动抑制独立成表(§4.1-D7)。
  5. ⛔ 不碰房间层 / 内容分发 / 打洞实现 / src/worker/tunnel.ts(死路径)。
  6. ⛔ 不动 04-调整方案/** 档案正文(历史档案属性)。
  7. ⛔ 不 commit / 不 push(未获明确授权;序 ⑰ 那次授权是当次一次性的)。
  8. 🔴 ⛔ 不许把 RELAY_FAILOVER_COOLDOWN_MS=0 写进任何回滚 / 演练 / 夹具路径(该值看似合法、实际自锁:归零会让"失败候选必须被排除"一起失效,实测 121–123 s 不切换)。

3.3 范围外登记(同 §0.4;执行棒只登记、⛔ 不动手)

  1. B 内容分发 / 块级内容寻址 —— 待拍板。
  2. 房间层 presence 专属做法(抽样降频 / idle / typing / bulk)—— 无载体。
  3. 骨干节点服务范围(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;106 dshs-relay→dshs-worker ⇒ dshs / dshs-relay / dshs-worker / dshs-pg 全 active。
  • 归零复跑三件套:npm test 169/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 遗留与回头条件

  1. 🔴 OBS-09 红 ⇒ 命中 §9-8(已停下报告)。卡在哪:OBS-09 要的是用户实例面(47 127.0.0.1:20000、对端 21000 经 relay),而 S7 规定的 systemctl restart dshs(+ 106 dshs-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 链路。
  2. ⚠️ 本棒实测踩到并已修的假死(重要,已补测):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)。
  3. ⚠️ 已登记的既存缺陷(本单范围外,⛔ 未动):web/server.ts 的 translateEndpoint 走了与 addressOf 不同的键口径(表键是逻辑名、传入裸 hostId ⇒ 恒 undefined)⇒ 该分支的订阅回退故意未加(加了也是死代码);要改须另立单(R7)。
  4. ✅ --scene all 本轮结果 = 12 PASS / 0 SKIP / 0 FAIL(唯一"附带影响":幕 4 前置含一次 Manager 重启 ⇒ 可能再次收掉实例 scope;未出现 FAIL)。
  5. ⏳ translateEndpoint 键口径既存缺陷(见上第 3 条)⇒ 建议并入序 ⑳ 或另立单。
  6. ⚠️ 动态值提醒:w-106 的 relay 落点 localPort 是动态值(本轮 = 35289),⛔ 不得写死进参数表。

8.9 本单指纹(可复现)

sed '/^## §8 回报格式/,$d' 交接单_presence在线态_20260917.md | md5sum = f612858344077420cff5f1f9ef1c942f(回填后保持不变 ✅ —— §8 之后的内容不影响前缀)。


§9 回头条件(一出现必须回头;⛔ 不许自行扩范围、⛔ 不许调生产值去凑)

  1. 要动生产值才能继续 —— 需要改 RELAY_FAILOVER_* / HB_SEC / burst 任一 ⇒ 停下报告(D1)。
  2. 要扩大暴露面 —— 需新增公网监听口 / 放开遮蔽 / 跨网络或跨租户订阅 ⇒ 命中 R5 ⇒ 停下出权限评估。
  3. 要超出 §3.1 的文件集 —— 除参数表 / 测试 / 探针 / 文档外,改动溢出到 src/** 的其他层 ⇒ 停下报告,别扩大。
  4. 批合并失效 —— 一次状态变化推不出 ≤1 帧,或稳态出现非零 presence 帧 ⇒ 停下报告(D4 的前提被证伪)。
  5. 观测跑不出单值 —— OBS-13/14/15 在真机拿不到可判定的数字 ⇒ 停下报告,改口径(⛔ 不许把未验证的项标 PASS)。
  6. 非静态失败 —— 跨网络订阅被静默放行(而非显式拒绝),或未知帧号被静默丢弃 ⇒ 立即停(假绿,本线头号教训)。
  7. 要求"先有房间层才能验收" ⇒ 停下报告:本单口径是节点层单值判据,房间层不在范围内。
  8. 零回归退化 —— npm test 低于 162/161/0/1、或 --scene all 出现 FAIL、或探针低于 12/12 ⇒ 停下报告。
  9. 🔴 任何回滚 / 演练 / 夹具路径要写 RELAY_FAILOVER_COOLDOWN_MS=0 ⇒ 禁止(不是"回头",是禁令)。

§10 指纹与状态

项 值
本单 §8 前缀指纹 f612858344077420cff5f1f9ef1c942f(落单时算出,2026-09-17 17:2x;口径 = sed '/^## §8 回报格式/,$d' … | md5sum)
参数表指纹(改前) 8f08e74b026e6e5b5e1b3db813f031ae(序⑰ 收口值,2026-09-17 17:2x)
代码仓 HEAD(落单时) bc0dd2c(已 push,远端 master 一致)
本单状态 待执行(下一棒 = 序 ⑲ 执行棒)
本棒边界自证 ⛔ 未改码 / ⛔ 未动 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)。