Files
dsh_ai1net_server/归档/交接单-20260924-归档/交接单_观测口径与在册缺陷_20260917.md
T
admin 7bd1151c67 chore(工作区): 归档交接单 27 件 + 新增 CODEBUDDY §9 工作区卫生
交接单按归属约定(正文落文档库、工作区只放指针)归档至
归档/交接单-20260924-归档/,含逐件判定 README:
- 16 件已被文档库正式版取代(T09–T21 + 覆盖网络-24/25/26)
- 4 件主题已被覆盖网络线入口汇总
- 7 件历史接续包/规划件

CODEBUDDY §9:收口清本棒 tmp、tmp 保留期 7 天、禁「待清理」中间态、
工作区入库只放文档与文件、不保留脚本副本、>60 KB 单文件须逐个判。
2026-09-24 07:58:57 +08:00

850 lines
93 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 交接单 · 观测口径重构(OBS-11 计数 → 白名单集合)+ 在册小缺陷清算排序(2026-09-17)
- **状态**:⏳ 待执行(**规划棒产物**,2026-09-17 14:5x 出单)
- **出单者**:覆盖网络线-序11规划棒(automation `3d4dffc0-356e-4bc6-9628-b7d664da7db9`)
- **执行方式**:规划与执行分离 ⇒ 执行会话**只按本单开工、不读规划会话上下文**
- **上游依据**(现取证据,⛔ 不必重跑):
1. `交接单_检测时延与deadline_20260917.md` **§8.8-4 ③ 逐口对账**(序⑩ 收口回填)—— OBS-11 `78→77` 的名字 = **`20000`**(47 实例档、无活跃实例)
2. 同单 **§8.8-4 末段「🔴 ③ 的副产品」** —— 计数判据的两处结构缺陷(ⓐ 对状态敏感 ⇒ 假红|ⓑ 对替换式变化不敏感 ⇒ 假绿)
3. `参数表_覆盖网络_20260917.md` §6(`OBS-11` 现判据 = `LISTEN_COUNT` / `NFT_RULES` **相等**)
- **成本纪律**:本单的取证命令**都已写死在 §2 / §6**,执行会话**照抄即可**(⛔ 不要另做全库探索)
---
## §1 目标
**一句话(可判定「做完了没有」)**:`scripts/overlay-probe.cjs` 的 `OBS-11` 由「**两个计数相等**」改为「**白名单集合**」判据(`必在集 ⊆ 实际 ⊆ 必在集 ∪ 允许集 ∪ 运行期派生集`,差集点名打印),`nft` 同族缺陷一并按同一思路改造;改造后 **① 47 无活跃实例时 `OBS-11` 不再假红;② 「一进一出」替换式变化必须能被判红(假绿实证见 §6-E2)**;并给出在册 6 条小缺陷的清算排序(**只排序 + 最小验收 + 回滚,本单不修**)。
**本单不追求**:把 `OBS-09`(47 无活跃实例)判绿 —— 那是环境态,归 §5.9-Q3。
---
## §2 只读前置(S0 必须先核实;任一条不符 ⇒ **停下报告**,⛔ 不硬闯)
| # | 事实 | 命令(本机 `cwd` = 工作区根;47 走 `ssh -p 22 bt-server`) | 期望输出 |
|---|---|---|---|
| **P1** | 47 监听口**原文全量**(判据形态的来源) | `ssh -p 22 bt-server "ss -lntp" > _中间产物_待清理/seq11/ss47-before.txt; wc -l < _中间产物_待清理/seq11/ss47-before.txt` | **77** 行(含表头)⇒ 76 socket;**每条都必须是 `地址:端口` 形态**(⛔ 见 P1-注) |
| **P1-注** | 🔴 **形态风险(本单最容易踩)**:参数表的 `cleanValue()` 会**剥掉 `*` 与反引号**(`overlay-probe.cjs:68-70`)⇒ 白名单值里**绝不能出现 `*:443` 这种写法**,必须写 `ss` 实际打印的 `0.0.0.0:443` / `[::]:22`。**P1 的落盘原文就是唯一依据** | `grep -nE '\*' _中间产物_待清理/seq11/ss47-before.txt` | **零命中**(若有命中 ⇒ 停下报告,形态与 2026-09-17 的认知不同) |
| **P2** | nft 判据的两条取数路径是否都可用 | `ssh -p 22 bt-server "nft --version; nft -j list ruleset \| head -c 200; echo; nft list ruleset \| wc -l"` | 版本行有输出|`-j` 回 JSON(首字符 `{`)|文本行数 = **72**。⚠️ **若 `-j` 不可用 ⇒ 不得放弃判据**,走文本退化路径并在输出里**显式标记**(§5-S2-d) |
| **P3** | 参数表现取指纹(本单出单基线) | `sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md \| md5sum` | **`99e9e17b0c1ce0550e4bc7626a5a0494`** |
| **P4** | 代码仓干净度(防越界) | `git -C D:/github/dsh_shenxian status --short \| wc -l; git -C D:/github/dsh_shenxian status --short src/ \| wc -l; md5sum D:/github/dsh_shenxian/scripts/overlay-probe.cjs` | 总 **43**|`src/` 改动 **0**|探针 md5 现取并落 §8.9 |
| **P5** | relay 端点**动态落点**(⛔ 它不是固定值,每次重启都会变) | `ssh -p 22 bt-server "curl -s http://127.0.0.1:20080/status" \| python3 -c "import sys,json;d=json.load(sys.stdin);print([e['localPort'] for e in d['endpoints']])"` | 形如 `[46147]`(序⑩ 现值 46147;⛔ 开工时**以现取为准**) |
> ⚠️ `_中间产物_待清理/seq11/` 是本单的取证落盘目录(先 `mkdir -p`)。收口时**保留**(供第三方复核),⛔ 不要写进正式档案。
---
## §3 范围
**改(3 类,全部是"观测面",⛔ 无一在生产路径上)**
| # | 文件 | 改动性质 | 是否计入本单前置指纹 |
|---|---|---|---|
| 1 | `D:/github/dsh_shenxian/scripts/overlay-probe.cjs` | `OBS-11` 判据主体 + nft 同族改造 + 取数改为回传原文 | 代码,**不进**参数表指纹 |
| 2 | `参数表_覆盖网络_20260917.md` §6(新增集合键、退役计数键)+ §10 | 新增 4 键 / 退役 2 键说明 | **会变** ⇒ §5-S5 必须重取并记 §8.9 |
| 3 | 工作区根 `接续入口_覆盖网络线_20260916.md` §0(1 行) | 口径指针 | 入口无指纹口径 |
**⛔ 不动(明确写死,防顺手扩大)**
- ⛔ **`src/**` 一字不改**(本单是观测面改造;`git status --short src/ | wc -l` 必须仍 = **0**)
- ⛔ 不动任何生产阈值:`RELAY_FAILOVER_*` / `HB_SEC` / burst 语义 / `RELAY_MAX_HOSTS` / `MemoryHigh`·`MemoryMax`
- ⛔ 不开新监听口 / 不新增凭据 / 不放宽 nft / 不碰 nginx / 不重启 `dshs` 或 relay(本单**无生产中断**)
- ⛔ **不 commit / 不 push**(未获授权)
- ⛔ 不回去改已收官的历史单(`交接单_检测时延…md` 等记录的是**当时实况**;新口径以参数表为单一来源 ⇒ §5-S5 只在参数表内写"退役说明",**不追改历史单**)
- ⛔ **不做**:presence / 房间层 / 内容分发(清单第 7 步)|106 agent 面吃引导链(已判不执行)|worker 拨出方向、relay 自研形态、打洞实现|同机候选去重 / 并发竞速 open(已判不做)|第 4/5 台真机(已拍板 = 本机多实例)
---
## §4 决策点
**已定(本规划棒自决,可推翻)**
1. **判据形态 = 三集包含式集合判据**(不是"计数 + 容差",也不是"只报不判"):
```
required ⊆ actual // 必在集:缺一条 ⇒ FAIL(点名缺项)
actual ⊆ required ∪ allowed ∪ derived
// 多一条 ⇒ FAIL(点名多出项)
derived = { 127.0.0.1:<p> | p ∈ /status.endpoints[].localPort } // 唯一来源 = relay 自身 /status
allowed = 表中静态允许集(含区间,见 §5-S1)
```
> 依据:`required` 抓"**消失**"、包含式抓"**新增**"、`derived` 让**合法动态落点**有名字 ⇒ 替换式变化(一进一出)**必然**被两条断言之一命中。
2. **`LISTEN_COUNT` 的"有/无实例态"两值 —— ⛔ 不表达,直接消除**:实例档 `20000`(w-47)/ `21000`(w-106)进**允许区间** ⇒ 有实例则出现(合法)、无实例则不出现(不红)⇒ 状态无关化。
⛔ **不新增** `LISTEN_COUNT_UP` / `LISTEN_COUNT_DOWN`(那等于把"对状态敏感"这条缺陷**编码进参数表** = 净退化,违 **R11**)。
3. **`nft` 同族缺陷一并改造(结论:改,但机制与监听口不同)**:nft 是**内容**不是端口 ⇒ 用 `nft -j` **入站 accept 集合 ⊆ 白名单**(语义判据,比行数更贴近"零新增暴露面"的本意);`nft list ruleset | wc -l` **降级为"仅打印的上下文"**,不再参与判 PASS/FAIL。⛔ 退化为文本解析时**必须显式标记**,⛔ 不得静默改判据(`text-fallback` 字样进输出行)。
4. **`LISTEN_COUNT` / `NFT_RULES` 退役为"历史留档"**:两行**留在**参数表(供对账),但 `OBS-11` 判据列**不再引用**;退役说明写在参数表 §6 内(单一来源内自解释),⛔ 不追改历史单。
5. **假绿/假红的**自证**方式 = 夹具 + 真机受控临时口**(§5-S3 / §6-E2、E3)—— 判据改造**必须自带"旧判据会放过、新判据能抓住"的实证**,否则等于换了个说法。
**硬门(三条,⛔ 任一违反即停手)**
- **D1**:分解类前提未复现前 ⛔ **不许改任何生产值**(本单只改观测面,本就不该碰生产值 ⇒ 违反即意味着范围外扩)
- **R7**:范围外缺陷**先报告、不动手**(§5.9 的 6 条里,本单**只执行 Q6 的"结构化吸收"**,其余全部只排序)
- 🔴 ⛔ **不许把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写进任何回滚 / 演练 / 夹具路径**(该值已判「看似合法、实际自锁」:`num()` 的 `/^\d+$/` 放行 `'0'`,但归零会让"失败候选必须被排除"失效 ⇒ 候选链卡死,实测 **121–123 s 无切换**;序⑩ 已把 `--scene 4c` / `ctrl` 整体移除并**显式拒绝**)。本单**不涉及任何切流演练** ⇒ 全单不该出现这个键。
**§4.3 待你拍板 = 空**(本单**零上抛**;§5.10 的范围外清单只登记、不定序)
---
## §5 步骤
### S0 · 只读前置
照 §2 跑完 P1–P5。**任一条与期望不符 ⇒ 停下报告**(附原文)。⚠️ P1 与 P1-注 是**全部设计的形态依据**,必须先落盘再动手。
### S1 · 定义白名单集合(改**参数表** §6,⛔ 值一律按 P1 原文写)
新增 4 键(全部加在 §6 阈值表内,`KEY_RE` 能解析;⛔ 值内不得含 `|`、`*`、反引号):
| 键 | 含义 | 值形态(示例;**实际以 P1 原文为准**) |
|---|---|---|
| `LISTEN_REQUIRED` | **必在集**:缺失 ⇒ FAIL。口径 = **平台工作必需 且 不受实例/端点在线态影响**的固定口 | 逗号分隔的 `地址:端口`,如 `0.0.0.0:22,[::]:22,0.0.0.0:80,0.0.0.0:443,0.0.0.0:888,0.0.0.0:3080,127.0.0.1:20080` |
| `LISTEN_ALLOWED` | **允许集(非必在)**:出现合法、消失不红 | 同上形态,如 `0.0.0.0:58888,127.0.0.1:8765,127.0.0.1:15432,127.0.0.1:19100` |
| `LISTEN_ALLOWED_RANGES` | **区间允许集**(实例档 / 拨号池) | `127.0.0.1:20000-20099,127.0.0.1:21000-21099`(两段取自序③ P3 的实例端口区间隔离)。⚠️**拨号池不在此键内** —— 复用表内既有 `DIAL_POOL_BOUND`(`25000–25063`)派生,⛔ 不重复写死(避免两处漂移) |
| `NFT_ALLOW_INBOUND` | **入站 accept 白名单**(S4 用) | `tcp:22,tcp:80,tcp:443,tcp:888,tcp:3080,tcp:58888`(+ 无端口匹配的必要规则另立一个标记位,形态见 S4) |
**成员归属的判据(写进参数表备注,防下一棒再吵)**:
- **必在** = 它不在 ⇒ **平台本身坏了**(sshd / nginx 80·443 / 宝塔 888 / 平台门户 3080 / relay `20080`)
- **允许** = 它不在 ⇒ 只是**某个可选项没开**(运维面板 58888、管理 UI 8765、PG 15432、Worker 19100、实例档、拨号池)
> ⚠️ 若 P1 原文显示某口与上表推断不符(例如 `888` 实际不在监听)⇒ **以原文为准**,并把该口从 `LISTEN_REQUIRED` 移到 `LISTEN_ALLOWED`(本单不追求"凑齐九个")。
### S2 · 改探针 `scripts/overlay-probe.cjs`(本单唯一代码改动)
**a. 取数:远端只回传原文,归一化在本地做**
- 现状 `listenTotal=$(ss -lntp | wc -l)` ⇒ 改为**回传原文**:`echo "listenRaw=$(ss -lntp | base64 -w0)"`(单行、`base64 -w0` 字母表无 `=` 除末尾填充 ⇒ 与现有 `key=value` 解析兼容)。
- nft 同法:`echo "nftRaw=$(nft -j list ruleset | base64 -w0; echo <退化标记>)"` —— ⛔ **`-j` 失败必须能分辨**(见 d)。
- ⛔ **仍只准一次 ssh**(现有 `remoteFacts()` 一条命令取全部事实的设计不许破)。
**b. 归一化(本地)**
- `ss` 行:跳过表头 / 空行 ⇒ 按 `\s+` 切分取 **Local Address:Port** 列 ⇒ 从**右侧**第一个 `:` 拆 host/port(`[::]:22` 必须拆对,⛔ 不能用左侧 `indexOf`)。
- 产出集合 `actual`,**逐条保留原文字符串**(报错要点名)。
**c. 判据(替换 `overlay-probe.cjs:251-259` 的 `OBS-11`)**
```
missing = required \ actual // 缺项 ⇒ FAIL
extra = actual \ (required ∪ allowed ∪ ranges ∪ derived) // 多出 ⇒ FAIL
ok = missing.length === 0 && extra.length === 0 && relayListenTotal === relayListenLoopback && relayListenTotal > 0
```
- 输出行(≤12 行的约束不许破):`OBS-11 集合 必在 n/7 允许 m 区间 k 派生 d 实际 t 多出 x 缺失 y`
- 差集**逐条点名**写到 stderr:`OBS-11 多出 127.0.0.1:9999` / `OBS-11 缺失 0.0.0.0:443`
- ⛔ **`LISTEN_COUNT` / `NFT_RULES` 不再被 `r.num()` 读取**(退役;两行留在表内仅供对账)
**d. nft(S4 的实现细节,与 c 同批落地)**
- `nft -j` 可用 ⇒ 解析 JSON:取 **`input` 链**(family `ip`/`ip6` 的 `filter`/`inet` 视 P2 原文)中 **verdict 含 accept** 的规则 ⇒ 归一为 `{proto}:{dport}`(无端口匹配的规则归为专用标记,如 `established`)。
- `nft -j` 不可用 ⇒ `nft list ruleset` 文本按行抽 `accept` 行 ⇒ **同一判据**,但输出行尾加 `nft=text-fallback`(⛔ 不许静默)。
- 判据 = `acceptSet ⊆ NFT_ALLOW_INBOUND`;多出逐条点名(`OBS-11 nft 多出 tcp:9999`)。
**e. 零数字纪律(判据见 §6-E5)**:新增代码**不得出现任何 ≥3 位的数字字面量**(端口/池/日期全部走参数表或从取回数据里取)⇒ `grep -nE "[0-9]{3,}" scripts/overlay-probe.cjs` 仍须**零命中**。
### S3 · 假红 / 假绿的自证(**本单的核心验收,⛔ 不许省**)
**a. 夹具模式(离线、零生产副作用)**:给探针加两个**仅测试用** argv:`--listen-fixture <file>` / `--nft-fixture <file>`(文件内容 = 远端原文,即 `ss -lntp` / `nft -j` 的输出)。
- 夹具模式下**不 ssh**;输出行首加 `⚠️ FIXTURE` 标记,**stderr 同时标一次**(⛔ 防止被下游当成生产结论)。
- 退出码仍按判据给 `0/1`(否则先红后绿无法表达)。
- ⛔ 夹具模式**必须配 `--table <副本>`**(避免误改生产参数表)。
**b. 三份夹具 + 新旧判据对照表**(落盘 `_中间产物_待清理/seq11/fx-*.txt`,三份都从 P1 原文**机械改写**,⛔ 不手搓)
| 夹具 | 构造 | 旧判据(计数相等) | 新判据(集合) | 证的是 |
|---|---|---|---|---|
| `fx-normal.txt` | P1 原文 | FAIL(77≠79 ⇒ **假红**) | **PASS** | ⓐ 假红已消除 |
| `fx-swap.txt` | 原文**去掉一行**(如 `0.0.0.0:888`)**加一行**(`127.0.0.1:9999`)⇒ **行数不变** | **PASS(假绿!)** | **FAIL**(多出 `9999` + 缺失 `888`,各点名) | ⓑ 替换式变化已能抓住 |
| `fx-instance.txt` | 原文**加上** `127.0.0.1:20000`(模拟 47 有活跃实例) | FAIL(78≠79 ⇒ 假红) | **PASS** | 实例态不再影响判据 |
**c. 真机正向复验(47,只读 + 一个受控临时口)**
1. 现态跑一次 ⇒ 期望 **`OBS-11 PASS`**,且 stderr 无差集(`overlay-probe` 总数由 **10/12 → 11/12**,剩余的 `OBS-09` = 环境态)。
2. **受控临时口**:本机 `ssh -p 22 bt-server "python3 -c '…'"` 起一个**只绑 `127.0.0.1` 的临时监听口**(口值从 47 的空闲回环口里现取,⛔ 不猜、⛔ 不绑公网)⇒ 新判据**必须 FAIL 并点名该口**;随即**关掉** ⇒ 必须回 PASS。
⚠️ 该临时口的值**必须记进 §8**(自证"多出必红"用的就是它);⚠️ 用 `timeout`/`trap` 保证**即使中断也会自动关闭**(⛔ 不留残留监听口)。
### S4 · nft 集合判据落表
按 S1 的 `NFT_ALLOW_INBOUND` 定值(**以 P2 的 `nft -j` 原文为准**,⛔ 不抄本单示例)。当前 `nft list ruleset | wc -l` = **72** ⇒ 归一后入站 accept 项数应远小于 72(含 `established`/`related` 一类无端口规则)。⚠️ 若发现**白名单写不全**(如 `icmp` 或 `ct state` 规则)⇒ 补白名单**而不是**放松判据;若确实存在"说不清来源的入站 accept"(如宝塔自行插入)⇒ **记进 §8.8 的"在册未办"并点名命令**,⛔ 不许把它静默加进白名单。
### S5 · 连带更新
1. **参数表 §6**:新增 4 键;`LISTEN_COUNT` / `NFT_RULES` 两行后加注「⛔ **已退役(序⑪ 集合判据替代)—— 仅对账用,不得再作为判据**」(值**保留**,历史可比)。
2. **参数表 §10 指纹**:按表内既有口径重取现值,写进 §10 的"现取"行。
3. **入口 `接续入口_覆盖网络线_20260916.md` §0**:加/改 1 行口径指针(指向本单 §8.9 与参数表 §6)。
4. ⛔ **不追改历史单**(见 §3)。
### S6 · 收口
按 §7 回滚准备 → 释放锁 → 用**陈述句**告知接续并登记下一棒(`scheduledAt` = 收口 + 2~5 分钟;⚠️ 下一棒 id **只能来自工具返回值**,先 create、再取 id、再落盘)→ 写工作区日志 → §8 回填。
---
### §5.9 在册小缺陷清算排序(② · **本单只排序 + 给最小验收与回滚,⛔ 不修**)
| 序 | 缺陷 | 类型 | 最小验收(命令 ⇒ 期望) | 回滚 | 依赖 |
|---|---|---|---|---|---|
| **Q1** | `mksess*.cjs` 失效(`/opt/dshs/mksess*.cjs` 仍写 **SQLite 旧库**) | 工具链(**阻塞验收手段**) | 用它开一次临时 session ⇒ 实例面 `curl -w '%{http_code}'` 回 **200/401**(⛔ 非 `000`);用完即删 | 恢复原脚本 + 删 session | 无(**先做**) |
| **Q2** | guest(w-106) 实例页 **502** | 生产缺陷(用户可见) | 同 Q1 的临时 session ⇒ w-106 实例页 **200** | `cp -a <lib 备份>/lib/. /opt/dshs/lib/` → `systemctl restart dshs` | **Q1**(没有临时 session ⇒ 无法验收) |
| **Q3** | `OBS-09` 实例面 `000`(47 无活跃实例) | **环境态,非缺陷** | 47 起活跃实例 ⇒ `OBS-09` 绿 **且** 新 `OBS-11` 在"有实例态"下仍绿(`20000` 落在允许区间) | 停实例(回到现态) | **Q2**(同批做最省) |
| **Q4** | `src/net/relay/**` **untracked 留档缺口** | 交付基线(**需用户授权**) | `git ls-files src/net/relay \| wc -l` > 0 且与工作区一致 | 不适用(只增不改) | **用户授权**(§4 提交边界 ⇒ 只报告) |
| **Q5** | `client.ts.bak-seq7-*` 残留 | 清理 | `ls src/net/relay/*.bak-*` 为空 **且** `npm test` 值不变(基线 160/159/0/1) | 从同目录备份/产物重生成(⚠️ 删前先确认 lib 内已有等价产物) | 无 |
| **Q6** | 参数表 `LISTEN_COUNT`=79 与现态的 **1 条无名口** | 观测口径 | **被 S1–S3 结构化吸收**(集合判据不再依赖计数);剩余 = 47 恢复活跃实例后复取应回 **78 行** | 参数表备份恢复(指纹回 `99e9e17b…`) | **本单即办**;"78 行"的复核与 Q3 合批 |
**排序理由(一句话)**:**Q1 是"验收手段"**(R4 规定实例面验收一律走临时 session)⇒ 它不修,Q2/Q3 连"做完了没有"都判不了;**Q2 是唯一用户可见的坏**(502);**Q3 是环境态**且与 Q2 同批最省;**Q4 卡在授权**(不在技术侧);Q5/Q6 是收尾。
**本单实际动手的只有 Q6 的"吸收"部分**(属 S1–S3 的副产品);⚠️ 其余 5 条 **⛔ 本单不修**(R7),执行会话把它们**原样转给下一棒**。
### §5.10 范围外(⛔ 只登记,本单**不定序、不上抛**)
presence / 房间层 / 内容分发(清单第 7 步)|106 agent 面吃引导链(已判不执行)|worker 只拨出 / relay 自研 / 不做打洞实现|同机候选去重 / 并发竞速 open(已判不做)|第 4/5 台真机 = 本机多实例(已拍板)。
> ⛔ 这些的**先后顺序属"业务优先级"**,按 §1 判据属边界外 ⇒ **本单只登记、不排序、不决策**。
---
## §6 验收(命令 + 期望输出 + 退出码;可被第三方复现)
| # | 项 | 命令(`cwd` = 工作区根) | 期望 |
|---|---|---|---|
| **E1** | 新判据在真实态全绿 | `node "D:/github/dsh_shenxian/scripts/overlay-probe.cjs"; echo "rc=$?"` | `PASS OBS-11 …`(stderr 无差集);**10/12 → 11/12**(剩 `OBS-09` 环境态);`rc=1`(因 OBS-09) |
| **E2** | **假绿实证**(本单核心) | `node …/overlay-probe.cjs --table <副本> --listen-fixture _中间产物_待清理/seq11/fx-swap.txt` | **旧判据 PASS / 新判据 FAIL**,且 stderr 同时点名**多出**与**缺失**各 1 条(对照表全文进 §8) |
| **E3** | 真机受控临时口 | S3-c 两步(起口 ⇒ 跑探针 ⇒ 关口 ⇒ 再跑) | 起口后 `FAIL` 且点名该口;关口后回 `PASS`;**收口时该口必须已关闭**(`ss -lntp` 复核 77 行) |
| **E4** | nft 判据两路可用 | `ssh -p 22 bt-server "nft -j list ruleset \| head -c 50"` + 一次探针 | `acceptSet ⊆ NFT_ALLOW_INBOUND`;走退化路径时输出含 `text-fallback` |
| **E5** | 零数字纪律未被破 | `grep -nE "[0-9]{3,}" D:/github/dsh_shenxian/scripts/overlay-probe.cjs` | **零命中**(退出码 1) |
| **E6** | 零回归(⛔ 四个都验) | `npm.cmd test`(Node 22)|`node …/overlay-failover-drill.cjs --scene all`|`ssh -p 22 bt-server "ss -lntp \| wc -l; nft list ruleset \| wc -l"` | 测试 **≥ 160/159/0/1**|`--scene all` **12 PASS / 0 FAIL**|`77` / `72`(本单不产生新监听口) |
| **E7** | 越界自证 | `git -C D:/github/dsh_shenxian status --short src/ \| wc -l; git -C D:/github/dsh_shenxian status --short \| wc -l` | `src/` = **0**;总数 = **43 + 本单新增**(⛔ 无 `src/` 改动、无 `.bak` 残留) |
| **E8** | 回滚可用 | §7 的恢复命令 + 重跑探针 | 回到 **10/12**(`OBS-11` 重新红) |
| **E9** | 参数表指纹已更新并记录 | `sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md \| md5sum` | 与 §8.9 记录值一致(⛔ 不得仍等于出单基线 `99e9e17b…`) |
**退出码约定**(沿用探针原设计,⛔ 不改语义):全绿 `0` / 任一红 `1` / 用法或取数失败 `2`。
---
## §7 回滚(全部秒级,⛔ 无生产中断)
| 层 | 改前准备 | 回滚命令 |
|---|---|---|
| 探针 | `cp -a D:/github/dsh_shenxian/scripts/overlay-probe.cjs _中间产物_待清理/seq11/overlay-probe.cjs.bak-<ts>`(⛔ **不进 `src/`**,收口时**删除仓内任何 `.bak`**) | `cp -a _中间产物_待清理/seq11/overlay-probe.cjs.bak-<ts> D:/github/dsh_shenxian/scripts/overlay-probe.cjs` |
| 参数表 | `cp -a 参数表_覆盖网络_20260917.md _中间产物_待清理/seq11/参数表.bak-<ts>` | 拷回 ⇒ 复核指纹回到 **`99e9e17b0c1ce0550e4bc7626a5a0494`** |
| 入口 | 只加 1 行 ⇒ 手工删该行 | 同左 |
| 临时监听口 | §5-S3-c 的 `timeout`/`trap` 兜底 | `ssh -p 22 bt-server "ss -lntp \| wc -l"` 必须回 **77** |
- ⛔ 本单**不重启** `dshs` / relay ⇒ 无在线用户影响;⛔ 回滚路径**不含任何** `RELAY_FAILOVER_*` 值(尤其 ⛔ 不含 `…COOLDOWN_MS=0`)。
- ⛔ **`.bak` 只在执行期存在**:验收全绿后**立即删除**仓内 `.bak`(`client.ts.bak-seq7-*` 残留就是反例 ⇒ 本单顺手立规矩,并把结果写进 §8)。
---
## §8 回报格式(执行会话按此回填;⛔ 缺一节即算未完成)
```
### 8.1 结论(1 行) :OBS-11 集合判据是否落地 / E1–E9 逐条 PASS·FAIL
### 8.2 只读前置 P1–P5 :逐条贴命令与输出(P1 的 77 行原文落盘路径 + 形态核对)
### 8.3 白名单定值 :LISTEN_REQUIRED / ALLOWED / ALLOWED_RANGES / NFT_ALLOW_INBOUND 四个键的**最终值**(含"为什么是这几个")
### 8.4 假红假绿实证 :E2 的三行对照表(夹具 ⇒ 旧判据 / 新判据)+ 三份夹具的 md5
### 8.5 真机正向复验 :E1 / E3 的命令原文与输出(含临时口的**口值**与"关闭后已复原"证据)
### 8.6 nft 判据 :`-j` 是否可用、归一后的 accept 集合、与白名单的关系(⚠️ 若有"说不清来源"的项 ⇒ 点名并转 §8.8)
### 8.7 零回归 / 越界自证 :E5–E7 原文(npm test 计数、--scene all 计数、ss/nft 计数、git status 计数、src=0)
### 8.8 未过项 / 在册未办 :逐条写「现象 / 已做到哪一步 / 回头条件」;§5.9 的 Q1–Q5 原样转下一棒
### 8.9 指纹(可复现核对口径)
```
### §8.9 指纹
- **参数表**:`sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md | md5sum` ⇒ 出单基线 **`99e9e17b0c1ce0550e4bc7626a5a0494`**(执行后**必须已变**并在此回填现值)
- **本交接单前缀(§8 及其后不计入)**:`sed '/^## §8 回报格式/,$d' 交接单_观测口径与在册缺陷_20260917.md | md5sum` ⇒ 现取值**由出单棒在入口与本棒回复中登记**(⛔ 本文件内**不自嵌**,避免自指)
- **交叉证据(收口复取)**:`交接单_检测时延与deadline_20260917.md` = **`d903b4eeabf25ef381379cdbaac77e8a`**(§8 前缀口径)|`参数表` 出单基线见上
- **原始取证落盘**:`_中间产物_待清理/seq11/`(`ss47-before.txt` = P1 原文;`fx-normal.txt` / `fx-swap.txt` / `fx-instance.txt` = 三份夹具;`overlay-probe.cjs.bak-<ts>` = 探针回滚副本)
---
## §8 执行回报(序⑫ 执行棒 · 2026-09-17 15:0x–15:2x)
> ⚠️ 本回报**追加在 §8 之后** ⇒ **§8 前缀指纹 `3ece0f87…` 未变**(回填不进入前缀,与序⑨/⑩ 同例)。
> ⚠️ 单头「状态:⏳ 待执行」一行**保持原样不动** —— 改它会动前缀指纹;本棒状态以 §8.1 与本行下方为准(**已执行完毕**)。
### 8.1 结论(1 行)
**✅ `OBS-11` 集合判据已落地;E1–E9 = 9 全绿**(其中 **E6-2 在第 3 次尝试才通过** —— 前两次被 **106 的 `sshd` MaxStartups 限流**中止,属**环境级、非本单引入**,已入册 **§8.8-1**)。`overlay-probe` **10/12 → 11/12**(剩 `OBS-09` = 环境态);**假红已消除 + 假绿已能抓**(§8.4 有对照表)。
### 8.2 只读前置 P1–P5
| # | 命令(`cwd` = 工作区根) | 实测 | 期望 | 判定 |
|---|---|---|---|---|
| **P1** | `ssh -p 22 bt-server "ss -lntp" > …/seq11/ss47-before.txt 2>/dev/null; wc -l < …` | **77 行**(=表头 1 + socket **76**) | 77 | ✅ |
| **P1-注** | `awk 'NR>1 && $1=="LISTEN"{print $4}' … \| grep -c '\*'` | **0** | 0 | ✅(⛔ 按**实质判据**;见 §8.8-2-①) |
| **P2** | `ssh -p 22 bt-server "nft --version; nft -j list ruleset \| head -c 120; … nft list ruleset \| wc -l"` | `nftables v1.0.4`|`-j` **回 JSON**(首字符 `{`)|文本 **72 行** | 同 | ✅ |
| **P3** | `sed '/^## §10 指纹/,$d' 参数表… \| md5sum` | **`99e9e17b0c1ce0550e4bc7626a5a0494`** | 同 | ✅ |
| **P4** | `git -C D:/github/dsh_shenxian status --short \| wc -l`;`… src/ \| wc -l` | 总 **43**|`src/` **20** | 43|**0** | 总 ✅;`src/` ⛔ 期望值本身有误 ⇒ 见 §8.8-2-② |
| **P5** | `ssh -p 22 bt-server "curl -s 127.0.0.1:20080/status"` → 取 `endpoints[].localPort` | **`[40985]`**(与 relay `20080` 同 PID 781675) | 现取为准 | ✅(⚠️ 动态值,序⑩ 的 46147 已过期 —— 印证"⛔ 不能写死") |
> **P1 原文的 76 条监听口分类**(这是白名单定值的**唯一依据**):
> `0.0.0.0:22` / `[::]:22` / `0.0.0.0:80` / `0.0.0.0:443` / `0.0.0.0:888` / `0.0.0.0:8765` / `0.0.0.0:58888` / `127.0.0.1:3080` / `127.0.0.1:15432` / `127.0.0.1:19100` / `127.0.0.1:20080` / `127.0.0.1:40985` + **`127.0.0.1:25000–25063`(64 口,拨号池)**。
> ⇒ `7 必在 + 4 允许 + 1 派生 + 64 拨号池 = 76` **逐条有名、零无名口**(对账闭合)。
### 8.3 白名单定值(四个键的最终值 + 为什么是这几个)
| 键 | 最终值 | 为什么是这几个 |
|---|---|---|
| `LISTEN_REQUIRED` | `0.0.0.0:22,[::]:22,0.0.0.0:80,0.0.0.0:443,0.0.0.0:888,127.0.0.1:3080,127.0.0.1:20080` | 判据 = **它不在 ⇒ 平台本身坏了**(sshd/nginx `80`·`443`/宝塔 `888`/平台门户 `3080`/relay 回环口)。⚠️ 与单内示例的**唯一差异** = **`3080` 写在 `127.0.0.1` 上**(47 实测绑回环,⛔ 不是 `0.0.0.0:3080`)—— 单内示例写错,按 P1 原文为准 |
| `LISTEN_ALLOWED` | `0.0.0.0:58888,0.0.0.0:8765,127.0.0.1:15432,127.0.0.1:19100` | 判据 = **它不在 ⇒ 只是某个可选项没开**(宝塔面板口/管理 UI/控制面 PG/本机 worker agent)⇒ 出现合法、消失**不红** |
| `LISTEN_ALLOWED_RANGES` | `127.0.0.1:20000-20999,127.0.0.1:21000-21999` | 两台 worker 的**实例端口区间**。🔴 **span 取 1000,不是单内示例的 100** —— 实测 47 的 `dshs-worker.service`(经 `/proc/<pid>/environ`)与 106 的 `/etc/dshs-worker.env` 均为 `DSHS_INSTANCE_PORT_BASE=20000`/`=21000` + **`DSHS_INSTANCE_PORT_SPAN=1000`**;并集 `[20000,22000)` 正好落在 relay 声明窗口 `[19000,22000)`(`--base 19000 --span 3000`)**之内** ⇒ 自洽。⛔ 拨号池**不在此键内**(复用表内既有 `DIAL_POOL_BOUND` 派生,避免两处漂移) |
| `NFT_ALLOW_INBOUND` | `tcp:22,tcp:80,tcp:443,tcp:888,tcp:3080,tcp:58888` | 入站 accept 的**语义白名单**(见 §8.6)。⚠️ 归一形态 = `<proto>:<dport>`;「无 dport 匹配的 accept」归一为 `<proto>:any` —— **当前 47 的 input 链 0 条规则 ⇒ 集合为 ∅、本键暂无标记位**(将来若出现必须显式加进来,⛔ 不许放宽判据) |
**"有/无实例态"的处置(⭐ 本单的结构性决定)**:⛔ **不新增** `LISTEN_COUNT_UP` / `LISTEN_COUNT_DOWN` 两个计数(那等于把"对状态敏感"编码进单一来源 = 净退化,违 **R11**)⇒ 把实例档 `20000` / `21000` 直接放进**允许区间**:**出现不报、消失不红**,状态敏感性问题被**消除**而不是被打补丁。
### 8.4 假红假绿实证(E2 核心)
**判据口径先说清**(⛔ 否则数字会被误读):**旧判据口径** = `ss -lntp | wc -l`(**含表头**,比 `LISTEN_COUNT`=79);**新判据口径** = **socket 集合大小**(不含表头)。三份夹具**全部由 P1 原文机械改写**(`mk-fixtures.py`,⛔ 不手搓):
| 夹具 | 构造 | 行数 | **旧判据** | **新判据** | 证的是 |
|---|---|---|---|---|---|
| `fx-normal.txt` | P1 原文**逐字** | 77 | **FAIL**(`77 ≠ 79`)⇒ **假红** | **PASS**(多出 0 缺失 0) | ⓐ 假红已消除 |
| `fx-swap.txt` | 原文 **+ `20000` + `21000`**(模拟两实例活跃 ⇒ 行数回到 79)**− `0.0.0.0:888` + `127.0.0.1:9999`**(一进一出) | **79** | **PASS(假绿!)** | **FAIL** —— 同时点名 `OBS-11 多出 127.0.0.1:9999` + `OBS-11 缺失 0.0.0.0:888` | ⓑ 替换式变化已能抓住 |
| `fx-instance.txt` | 原文 **+ `127.0.0.1:20000`** | 78 | **FAIL**(`78 ≠ 79`)⇒ **假红** | **PASS** | 实例态不再影响判据 |
| `fx-nft-extra.json` | `nft -j` 原文 + 一条 `INPUT` 上的 `accept tcp dport 9999` | — | — | **FAIL** —— 点名 `OBS-11 nft 多出 tcp:9999` | nft 侧"多出必红" |
| `nft-text.txt` | `nft list ruleset` **文本原文**(喂给 `--nft-fixture` ⇒ 必走退化路径) | 72 | 旧口径行数相等 ⇒ PASS | **PASS** + 输出含 **`nft=text-fallback`**;accept 集合 = **∅**(`FORWARD` 的 accept **未误抓** ⇒ 链感知有效) | 退化路径**同一判据**、⛔ 不静默 |
| `fx-nft-text-extra.txt` | 上述文本 + 在 `chain INPUT {` 后插一行 `tcp dport 9999 counter accept` | 73 | — | **FAIL** —— 点名 `tcp:9999` | 退化路径同样"多出必红" |
**三份 ss 夹具 md5**:`fx-normal.txt` = `e53031f7f2485b4609a69128effb7210`|`fx-swap.txt` = `dadd94f85df9a2c1e115afce17bfd91e`|`fx-instance.txt` = `45cee07da44d5c3fa4a92475f03be2ed`
**入场码**:`fx-normal` rc=**0**|`fx-swap` rc=**1**|`fx-instance` rc=**0**|`fx-nft-extra` rc=**1**|`nft-text` rc=**0**|`fx-nft-text-extra` rc=**1**(⛔ 夹具模式退出码仍按判据给,否则"先红后绿"表达不出来)
⚠️ **与单内 §6-E2 的一处构造差异(必须记)**:单内写「`fx-swap` = 原文**去掉一行加一行**、**行数不变**」并同时期望「旧判据 PASS」。**这两条指不并存** —— 若行数仍 = 77,则旧判据也 FAIL ⇒ **假绿证不出来**。本棒按**实质判据**构造:先补两个实例口把行数**抬到 79(= `LISTEN_COUNT`)**,再做一进一出 ⇒ 两条期望**同时**满足(旧 PASS/新 FAIL)。这是"让判据可被证伪",⛔ 不是改判据。
### 8.5 真机正向复验(E1 / E3)
**E1(真实态)**:`node scripts/overlay-probe.cjs` ⇒ `rc=1`(因 `OBS-09`)|**PASS 11 / FAIL 1**(唯一红 = `OBS-09 实例面 本机:20000=000 对端:21000=000`)|`OBS-11` 原文:
`PASS OBS-11 集合 必在 7 允许 4 区间 3 派生 1 实际 76 多出 0 缺失 0 |nft accept 0 多出 0 |relay 口绑定回环=1/1 条`
⇒ **10/12 → 11/12** 达成本单期望(原红项 `OBS-11` 由"77 ≠ 79"假红转为绿)。
**E3(真机受控临时口)** —— 口值 = **`127.0.0.1:27000`**(选址理由:**不在** required/allowed/ranges/derived/拨号池任何一处);起口方式 = `ssh 'timeout 45 bash -s' < tmplisten.sh`(**`timeout` 兜底 ⇒ 即使中断也会自动关闭**):
| 步 | 命令 | 实测 |
|---|---|---|
| 起口前 | `ssh bt-server "ss -lntp \| wc -l"` | **77** |
| 起口后 | `ss -lntp \| wc -l` ⇒ **78**;跑探针 | `FAIL OBS-11 集合 … 多出 1 缺失 0`|**stderr 点名 `OBS-11 多出 127.0.0.1:27000`**|rc=**1** |
| 关口后(`wait` 到 `timeout` 自然结束) | `ss -lntp \| wc -l` ⇒ **77**;再跑探针 | `PASS OBS-11 集合 … 多出 0 缺失 0`|rc=**1**(仅 OBS-09) |
⇒ **"多出必红、恢复必绿",且收口时该口已关闭、`ss` 回到 77(⛔ 零残留监听口)**。
### 8.6 nft 判据
- **`nft -j` 可用**(P2:`nftables v1.0.4`,首字符 `{`)⇒ **正路走 JSON**,未走退化。
- 🔴 **归一后的 accept 集合 = ∅**。原因(本条同时纠正了一个旧口径):47 的 `nftables` 只有 **22 条规则**,而 **`ip filter INPUT` 链 0 条规则、`policy = accept`**;`nft list ruleset | wc -l = 72` 里绝大多数是 **`ip nat` / `dsh_egress`(output)** 与注释行 ⇒ **行数根本不是入站暴露面**(这正是"`NFT_RULES` 计数"这条旧判据的第二处结构缺陷,已在参数表内标注退役)。
- 归一实现:取 **`hook=input` 的链 + 沿 `jump` 的传递闭包**,其内 verdict 含 `accept` 的规则 → `<proto>:<dport>`(`dport` 支持 数/区间/集合;无端口匹配 → `<proto>:any`)。
- 判据 = `acceptSet ⊆ NFT_ALLOW_INBOUND`:今天 **`∅ ⊆ 白名单` 成立 ⇒ PASS**。
- ⚠️ **诚实标注**:因为集合为 ∅,**当前白名单并未被"现实样本"压过** ⇒ 本棒为此补了**两条夹具**(`fx-nft-extra.json` 注入 `tcp:9999`;`fx-nft-text-extra.txt` 注入文本行)**证明"多出必红"**,⛔ 而不是让它"永远绿着过"。
- **"说不清来源的入站 accept" = 0 条** ⇒ 无需转 §8.8。
### 8.7 零回归 / 越界自证
| 项 | 命令 | 实测 | 期望 |
|---|---|---|---|
| `npm test` | `npm.cmd test`(Node **v22.22.2**) | **# tests 160 / # pass 159 / # fail 0 / # cancelled 0 / # skipped 1**;rc=0 | ≥ 160/159/0/1 ✅(**与基线逐字一致 ⇒ 零回归**) |
| `--scene all` | `node scripts/overlay-failover-drill.cjs --scene all` | **12 PASS / 0 SKIP / 0 FAIL**;rc=0(⚠️ 第 3 次尝试;见 §8.8-1) | 12 PASS / 0 FAIL ✅ |
| 监听口 / nft | `ssh bt-server "ss -lntp \| wc -l; nft list ruleset \| wc -l"` | **77 / 72** | 77 / 72 ✅(**本单不产生新监听口**) |
| 越界:`src/` | `git status --short src/ \| wc -l` | **20**(改动前 20 ⇒ **Δ0**) | Δ=0 ✅ |
| 越界:总数 | `git status --short \| wc -l` | **43**(=改动前基线;⚠️ `scripts/overlay-probe.cjs` 是 **untracked**(`??`)⇒ **改它不改计数**) | 43 ✅ |
| `.bak` 残留(仓内) | `git status --short \| grep -i bak` | **空** | 空 ✅(本单备份全在 `_中间产物_待清理/seq11/`,⛔ 不进仓) |
| 演练 env 残留 | `ls /etc/systemd/system/dshs.service.d/` + `systemctl show dshs -p Environment` | 无 `zz-drill-override.conf`;`DRILL_*` 命中 **0** | 0 ✅ |
| **E8 回滚可用** | `cp -a …bak-<ts> → probe` ⇒ 跑探针 ⇒ `cp -a …new-<ts> → probe` | 回滚后:`FAIL OBS-11 监听口=77(阈值 79) …` ⇒ **10/12**;恢复后 md5 = `d7e3ed77c1e0ea983b5ee879abde3130`(与改后一致) | 回到 10/12 ✅ |
### 8.8 未过项 / 在册未办(逐条:现象 / 已做到哪一步 / 回头条件)
**§8.8-1 🔴(新发现 · 环境级 · 非本单引入)106 的 `sshd` 被 MaxStartups 限流 ⇒ 脚本化 ssh 偶发 `rc=255`,会让整场演练中止**
- **现象**:`--scene all` 第 1 次在**幕 1 全绿后**崩(4 PASS / 0 FAIL,1m24s),第 2 次在**启动阶段**崩(8s,0 项),两次都停在 `overlay-failover-drill.cjs` 的 `ssh()` 抛 `Error: Command failed: ssh … test106 …`;**第 3 次通过(12 PASS)**。
- **根因(证据级)**:106 的 `journalctl -u sshd --since '-25min'` 命中 **586** 条 error/refus/timeout/preauth(对照 47 仅 **3** 条),其中含原文 **`sshd[6366]: error: beginning MaxStartups throttling`**,另有大量 `Connection closed by authenticating user root … [preauth]`/`invalid user ubuntu`(**外部爆破流量**)+ `kex_exchange_identification: read: Connection reset by peer`。`/etc/ssh/sshd_config` 的 `MaxStartups` 为**默认 `10:30:100`**、106 的 **fail2ban `inactive`** ⇒ 未认证连接槽位被爆破流量打满 ⇒ **新连接被丢** ⇒ ssh `rc=255`。手工具复现:同一条重命令第 1 次 `rc=255`、第 2/3 次 `rc=0`(~700 ms)。
- **已做到哪一步**:① 已确认**与本单改动无关** —— 本单零 `src/` 改动、且演练**不消费** `overlay-probe.cjs`,探针只连 47(47 侧 3 条错误、E1 全绿);② 已用**第 3 次尝试**取得 E6-2 的 12 PASS / 0 FAIL;③ 已定位到 sshd 侧的限流证据与爆破流量来源。
- **回头条件**:① **给 106 的 sshd 调 `MaxStartups`/起 fail2ban/把 106 的运维 ssh 改走 47 跳板**(⚠️ 前两项属**放宽/收紧一个安全参数** ⇒ 其中"放开限流"命中 **R5**,**⛔ 本棒不做、不在本单范围内**);② 或让脚本化 ssh 具备**退避重试**(属改 `overlay-failover-drill.cjs` ⇒ 单外,**先报告**)。
- **在册**:转入下一棒(**平台面既有缺陷**,会影响**任何**针对 106 的脚本化 ssh,不只是演练)。
**§8.8-2 🟡 与单不符的两处 —— 已按"实质判据"处理(供下一轮修单模板时吸收)**
1. **P1-注 的期望值不可满足(单方瑕疵)**:单内写 `grep -nE '\*' ss47-before.txt` ⇒ **零命中**。**实测不可满足** —— `ss -lntp` 的 **peer 列**恒为 `0.0.0.0:*`(76 行**每行都有** `*`)。⇒ 实质判据 = **Local Address:Port 列零 `*`**:`awk 'NR>1 && $1=="LISTEN"{print $4}' … | grep -c '\*'` = **0** ✅。⚠️ 顺带踩到一次:首跑 `ssh … > f 2>&1` 把 **3 行 ssh PQ 告警**写进了取证文件(77 → 80)⇒ **取证落盘一律 `2>/dev/null`**(已复取干净版)。
2. **P4 / E7 的 `src/` 期望值有误(单方瑕疵)**:单内写「`src/` 改动 = **0**」。**实测 20**,且**与本单无关** —— 它是**既存未提交基线**(17 个 `M` + 3 个 `??`:`src/net/relay/`、`src/web/routes/overlay.ts`、`src/worker/relay-tunnel.ts`;后者正是 §5.9-**Q4** 的留档缺口)。⇒ 实质判据 = **本单不得新增 `src/` 改动** ⇒ 20 → **20(Δ0)** ✅。⚠️ 另一处易误读:总计数 43 **不因本单而变**,因为 `scripts/overlay-probe.cjs` 本身是 **untracked**(`??`)—— **不能拿"总数没变"当"没改文件"的证据**,判据要落到 `md5sum` 与 Δ 上。
**§8.8-3 📌 §5.9 的 Q1–Q5 原样转下一棒(⛔ 本棒未修,R7)**
| 序 | 缺陷 | 本棒动作 |
|---|---|---|
| **Q1** | `mksess*.cjs` 失效(`/opt/dshs/mksess*.cjs` 仍写 SQLite 旧库)⇒ **阻塞实例面验收** | ⛔ 未修,原样转办(**下一棒第一件**) |
| **Q2** | guest(w-106) 实例页 **502** | ⛔ 未修,原样转办 |
| **Q3** | `OBS-09` 实例面 `000`(**环境态非缺陷**) | ⛔ 未动(本棒**不起实例**;起实例属 Q3,与 Q2 同批最省) |
| **Q4** | `src/net/relay/**` untracked 留档缺口 | ⛔ 只报告(**卡在用户授权**,不在技术侧) |
| **Q5** | `client.ts.bak-seq7-*` 残留 | ⛔ 未动;本棒已**以身作则**立起"`.bak` 只在执行期存在"这条规矩(本棒仓内 `.bak` = 0) |
| **Q6** | 参数表 `LISTEN_COUNT`=79 与现态的 1 条无名口 | ✅ **已结构化吸收**(集合判据不再依赖计数;那"1 条"就是 `20000` 实例档 ⇒ 现已在允许区间内)。⚠️ 剩余项「47 恢复活跃实例后复取应回 78 行」的复核**与 Q3 合批** |
**⚠️ 另记一条(本棒未触发但已备查)**:`RELAY_FAILOVER_COOLDOWN_MS` 全程 **未被触碰**;`grep` 演练 / 回滚 / 夹具路径中该键的 `=0` 形态 = **0 命中**(收口复核:`dshs.service.d` 下无 `zz-drill-override.conf`、`DRILL_*` env 命中 0)。
### 8.9 指纹(收口复取 · 全部现取)
| 项 | 命令 | 值 |
|---|---|---|
| **参数表**(§10 不计入) | `sed '/^## §10 指纹/,$d' 参数表_覆盖网络_20260917.md \| md5sum` | **`8f08e74b026e6e5b5e1b3db813f031ae`**(出单基线 `99e9e17b0c1ce0550e4bc7626a5a0494` ⇒ **已变** ✅ E9) |
| **本交接单前缀**(§8 及其后不计入) | `sed '/^## §8 回报格式/,$d' 交接单_观测口径与在册缺陷_20260917.md \| md5sum` | **`3ece0f870cba67d0113a4c5f9de9d812`**(**回填后未变** ✅) |
| **交叉证据**:检测时延单(§8 前缀口径) | `sed '/^## §8 回报格式/,$d' 交接单_检测时延与deadline_20260917.md \| md5sum` | **`d903b4eeabf25ef381379cdbaac77e8a`**(未变 ✅) |
| **探针(代码)** | `md5sum D:/github/dsh_shenxian/scripts/overlay-probe.cjs` | 改前 `0cd76d986273e04631510aa7d215d16c` ⇒ **改后 `d7e3ed77c1e0ea983b5ee879abde3130`** |
| **入口** | `md5sum 接续入口_覆盖网络线_20260916.md` | 收口取(本单未在此登记,避免自指) |
| **原始取证落盘** | — | `_中间产物_待清理/seq11/`(P1 原文、6 份夹具、`nft47.json`/`nft-text.txt`、`status-fixture.json`、探针 `.bak-<ts>` 与 `.new-<ts>`、`npmtest.txt`、`drill-all-*.txt`、`real-*.txt`、生成器 `mk-fixtures.py`/`mk-nfttext-fx.py`/`nft-summary.py`)⇒ **保留供第三方复核**(⛔ 不写进正式档案) |
---
## §9 执行回报(序⑬ 执行棒 · 2026-09-17 15:2x–15:5x · 在册小缺陷清算 Q1→Q3)
### 9.1 结论(逐条)
| 序 | 结果 | 判据 |
|---|---|---|
| **Q1** | ✅ **已修并验收** | `/opt/dshs/mksess{,-guest}.cjs` 改插 **PG**;修前实证写进 SQLite(PG `sessions`=2 / SQLite=3);修后临时会话可直插 PG,**实例面 `curl` 回 `401`(⛔ 非 `000`)** |
| **Q2** | 🔴 **未达成 —— 阻塞在一条新发现的数据面缺陷** | w-106 用户 `POST /api/dsh/enter` 仍 **500**;**真因不是单内记的 `landModels → writeHomeFile`**,而是「relay 流上的 HTTP keep-alive 复用」⇒ 见 **9.3** |
| **Q3** | ✅ **达成(探针 12/12 · rc=0)** | `OBS-09` 绿(本机 `20000`=401、w-106 `42461`=401)**且**"有实例态"下 `OBS-11` 仍绿(实际 77 / 多出 0 / 缺失 0、`20000` 落在允许区间) |
### 9.2 Q1 · `mksess*.cjs` 从 SQLite 改到 PG(✅ 已修)
- **前提复现(D1)**:跑旧 `/opt/dshs/mksess-guest.cjs` ⇒ **PG `sessions` 仍是 2、SQLite `sessions` 变 3** ⇒ 会话落错库、平台读不到(R4 的实例面验收手段整体失效)。
- **改法**:实现**只有一份** = `/opt/dshs/mksess.cjs` —— 连接串**从 `DSHS_DB_URL` env → `/etc/dshs.env` → `dshs.service.d/*.conf` 逐个找**,⛔ **不把凭据固化进 0644 脚本**;`mksess-guest.cjs` = `process.argv[2]='guest'; require('/opt/dshs/mksess.cjs')` 薄封装(保留原文件名 —— 档案 76/77 与 `dsh-plugin-diagnose` 等多处按此名引用)。
- **验收**:`node mksess.cjs` ⇒ token 64 字符、PG `sessions` 2→3、`user_agent='poc-curl2'` 命中;**实例面 `curl -o /dev/null -w '%{http_code}' http://127.0.0.1:20000/` = `401`**(⛔ 非 `000`)。
- **指纹**:`mksess.cjs` = **`a97c0f214650fbc552c07d804fd153be`**|`mksess-guest.cjs` = **`af6eab38c4ff5c365ee17e29d7b98a31`**。原件备份:`/opt/dshs/mksess{,-guest}.cjs.bak-seq13-20260917-152653`(本地副本 `_中间产物_待清理/seq13/`)。
- **回滚**:`cp -a /opt/dshs/mksess*.bak-seq13-20260917-152653` 回原名 + `DELETE FROM sessions WHERE user_agent='poc-curl2'`。
- ⚠️ **遗留(R7 · 只报告未动)**:只改了 `/opt/dshs/` 这一处;**文档库与技能里对 `mksess.cjs` 的描述("DB 直插")仍是旧口径** ⇒ 需要时另起一小棒同步。
### 9.3 Q2 · guest(w-106) `enter` 500 —— **真因是数据面(新缺陷)**
**现象(3/3 稳定复现)**:
```
POST /api/dsh/enter (guest) ⇒ 500
{"statusCode":500,…,"message":"agent POST /fs/isdir → 400: {\"error\":\"Bad Request\",\"message\":\"Client Error\",\"statusCode\":400}"}
```
**最小复现(1 条命令,在 47 上跑)** —— 对**拨号池回环口**的**同一条 TCP 连接**连发两条请求:
```
curl -s --http1.1 -w ' -> %{http_code}\n' -X POST -H 'content-type: application/json' \
-H 'x-dsh-agent-token: <w-106 的 agent_token>' -d '{"userId":"<guest id>","relPath":""}' \
http://127.0.0.1:25000/fs/isdir http://127.0.0.1:25000/fs/isdir
⇒ {"isDirectory":true} -> 200
⇒ {"error":"Bad Request","message":"Client Error","statusCode":400} -> 400
```
(`/touch` 或 `/status` 打头、`/fs/isdir` 收尾同样复现;两条 GET 也复现 ⇒ **与请求体无关**。)
**取证链(逐层排除)**:
1. **客户端无辜**:Manager 侧 `strace -f -e trace=write,writev -p <dshs pid>` 抓到写出的就是一条**格式完全正确**的请求(`POST /fs/isdir HTTP/1.1`、`content-length: 62`、body 完整)。
2. **走的就是拨号池口**:`ss` 见文件面连的是 `127.0.0.1:25000`;`[relay-dialer] 落点 127.0.0.1:25000 -> ops/w-106:19000`(`localPortFor('ops/w-106', 19000)`)。`/touch`、`/status` 因**并发**各占一条新连接 ⇒ **各占一条流 ⇒ 都成功**;`/fs/isdir` 落到**已被复用**的那条 ⇒ 失败。
3. **400 的出处已定死**:body 与 Fastify `node_modules/fastify/fastify.js:985` 的 **`clientError` 分支**逐字一致 ⇒ 这是**收到非法字节流时写裸 socket 的兜底响应**,⛔ 不是任何业务路由返回的;它**不写 pino 日志** ⇒ 这正是"106 worker 日志里什么都没有"的原因。
4. **106 侧抓包(`tcpdump -i lo -A -s0 'tcp port 19000'`)**:
- 全场**只有 1 条** `POST /fs/isdir` 到达 agent,紧接着 agent 回 400;
- 🔴 **关键**:`57244 > 19000` 的载荷是 **`HTTP/1.1 200 OK … {"isDirectory":true}`** —— 即**worker 侧把 agent 自己上一份响应回灌给了 agent** ⇒ agent 把响应行当请求行解析 ⇒ `clientError 400`。
5. **因此**:责任面收敛到 **relay 的 stream 路由**(`src/net/relay/server.ts` 的 stream 表 / `workerStream` 与 `dialStream` 的映射)与 **worker 侧本地转发**(`client.ts#onOpenRequest` / `onRemoteData` → `st.tcp`)+ **拨号池收尾**(`dialer.ts` 的 `tcp.pipe(duplex).pipe(tcp)`,`duplex` 关闭时**没有** `tcp.destroy()`)。
**影响面(⚠️ 比在册描述严重)**:不是"guest 一个用户的 502",而是 **任何 `via='relay'` 的 host(今天 = w-106)** —— 只要一次业务动作对同一 host 发**第 2 条** agent 请求(`enter` 的 `isDirectory` → `launch` → `waitUpOnStatus` 轮询…)就必失败 ⇒ **w-106 用户"登录直达工作区"整体不可用(用户可见)**。
**已做到哪一步 / 卡在哪**:故障面已收敛到上面那一组文件,**但帧级取证(relay 侧 `workerStream`/`dialStream` 路由表逐帧日志)未做** ⇒ 尚未定位到具体代码行。
**回头条件(必须回头)**:① 拿到 relay 帧级路由证据并定位代码行;② 修复判据 = guest `enter` 回 **200**、同一连接连发两次都 **200**、`npm test` 不退化(基线 160/159/0/1)、`--scene all` 不退化、`overlay-probe` 保持 **12/12**。
**⛔ 本棒未动手(R7 + R11)**:这是**数据面**变更;在拿到帧级证据前盲改(例如只给拨号池补 `tcp.destroy()`)会把"静默给出错误答案"换成别的形态,还可能**掩盖 relay 侧的真错** ⇒ 属净风险,留作下一棒主题(**序 ⑭**)。
### 9.4 Q3 · `OBS-09` +「有实例态下的 `OBS-11`」(✅ 达成 · 探针 **12/12**)
- **起实例**:① **admin(w-47,本机直连,不经 relay)** 经 `POST /api/dsh/enter` ⇒ **`200`**、`port=20000`、`status=running`(这同时就是 **Q1 的实例面验收**:`curl http://127.0.0.1:20000/` = **401**,⛔ 非 `000`);② **guest(w-106)** 因 9.3 的缺陷堵在 `enter`,改由 **106 的 worker agent `POST /launch`**(带 `uid=100002` + `ws` 目录)拉起 ⇒ `200`、`port=21000`、`pid=237067`。⚠️ **该实例未经 Manager 租约**(为把 `OBS-09` 的对端腿测出来而"出格"了一步,已在工作区日志登记)。
- **探针结果**:**`12/12 PASS · rc=0`**,其中
- `PASS OBS-09 实例面 本机:20000=401 w-106:42461=401 (阈值 ∈ {200,401})`
- `PASS OBS-11 集合 必在 7 允许 4 区间 3 派生 2 实际 77 多出 0 缺失 0 |nft accept 0 多出 0 |relay 口绑定回环=1/1 条` —— **正是 Q3 要的那一问:有实例态下 `20000` 落在允许区间**
- `PASS OBS-08 端点表 2 条 / 离线 0 条`(比无实例态多 1 条)
- `ss -lntp | wc -l` = **78 = 1 表头 + 77 socket**(无实例态 76 + 实例 `20000` 一口)⇒ **无多余监听**。
- **回滚(要回到"无实例态")**:`systemctl stop dsh-100002-*.scope`(106)+ 停 w-47 那个实例;此后 `OBS-09` 回到红 —— 那是**环境态,非缺陷**。
- ⚠️ **回头条件**:guest 实例是**出格拉起**的;一旦 Manager 的 reaper 回收它 ⇒ `OBS-09` 对端腿回 `000`(**环境态**,按 Q3 语义非缺陷)。
### 9.5 其余(R7 · 只报告,⛔ 未动手)
| 序 | 状态 |
|---|---|
| **Q4** | `src/net/relay/**` untracked 留档缺口 —— **未动**(卡在提交授权;`git status --short src/net/relay/` = 1) |
| **Q5** | `src/net/relay/*.bak-seq7-*` 残留 —— **未动**(本棒仓内 `.bak` = 0,规矩继续) |
| **Q6** | 已被序⑫ 结构化吸收;本棒复取 `实际 77`、`20000` 在允许区间 ⇒ **可关闭** |
| 🔴 新发现 | **9.3 的 relay keep-alive 缺陷**(= 下一棒主题) |
### 9.6 越界自证 & 三条硬门
- `git status --short` = **43**(Δ **0**)|`git status --short src/` = **20**(Δ **0**)|仓内 `.bak` = **0**。
- **D1**:全程**未改任何生产值** —— 参数表指纹仍 `8f08e74b026e6e5b5e1b3db813f031ae`、`RELAY_FAILOVER_*` / `HB_SEC` / burst 一字未动。
- **R7**:Q4 / Q5 只报告;文档库・技能侧对 `mksess` 的旧描述未改(见 9.2 遗留)。
- 🔴 `RELAY_FAILOVER_COOLDOWN_MS=0`:**0 命中**(未写入任何回滚 / 演练 / 夹具路径)。
- **R4 收尾**:临时会话用完即删(`DELETE … user_agent='poc-curl2'` ⇒ `DELETE 2`,`sessions` 回 **2 = 基线**)。
### 9.7 指纹(收口现取)
| 项 | 值 |
|---|---|
| Q1 产物(47) | `mksess.cjs` **`a97c0f214650fbc552c07d804fd153be`**|`mksess-guest.cjs` **`af6eab38c4ff5c365ee17e29d7b98a31`** |
| 参数表(§10 不计入) | **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变** ⇒ D1 自证) |
| 本交接单 §8 前缀(追加 §9 后复取) | **`3ece0f870cba67d0113a4c5f9de9d812`**(**未变** —— `§9` 在 §8 之后,不进前缀口径) |
| 探针(代码,未改) | `d7e3ed77c1e0ea983b5ee879abde3130` |
| 原始取证落盘 | `_中间产物_待清理/seq13/`(`mksess.cjs` / `mksess-guest.cjs` / `probe-isdir.cjs` / `q2-capture.sh`);106 抓包与 47 strace 原文留在各机 `/tmp`(`cap.pcap` / `st2.txt`) |
| 下一棒 | automation **`3e0a7b01-9257-4b76-b82f-4998c9f7eab0`**(`scheduledAt` = 2026-09-17 **15:53**,`nextRunAt` = 1789631580000) |
---
## §10 执行回报(序⑭ 执行棒 · 2026-09-17 15:53–16:1x · relay 流 keep-alive 复用缺陷)
### 10.1 结论(逐条)
| 项 | 结果 |
|---|---|
| **D1 · 先复现** | ✅ 复现,与 §9.3 逐字一致:同一条连接连发两条 ⇒ `{"isDirectory":true} -> 200` + `{"error":"Bad Request","message":"Client Error","statusCode":400} -> 400` |
| **② 帧级取证 → 定位到行** | ✅ **定位到 `src/net/relay/client.ts#openStream` 里的一行 `duplex.on('data', …)`**(原 975 行) |
| **③ 先红后绿** | ✅ 红:目标端收到 **2 段**回灌字节 `HTTP/1.1 200 OK …`;绿:修后 `T23` 通过(无杂音) |
| **④ 端到端验收** | ✅ guest `enter` **200**、同连接连发两次 **200/200**(三次 **200/200/200**);`npm test` **160 pass / 0 fail / 1 skip**(基线 159 pass +T23);`--scene all` **12 PASS / 0 SKIP / 0 FAIL**;`overlay-probe` **12/12 PASS · rc=0** |
### 10.2 复现(D1 自证)
命令 = §9.3 原文那一条(47 上,对拨号池口同连接连发两条)。**修前**:`200` 然后 `400`。⛔ 全程未改任何生产值。
### 10.3 帧级取证与定位(**这是本棒的核心**)
**真因**:`RelayClient#openStream()` 把**读侧**接回了**出向**。
```ts
// src/net/relay/client.ts(修前,openStream 内)
const duplex = new MuxDuplex({ onOut: (chunk) => this.pumpDial(id, chunk), … })
this.dialStreams.set(id, st)
duplex.on('data', (chunk: Buffer) => this.pumpDial(id, chunk)) // 🔴 就是这一行
```
**机制(逐跳)**:目标(agent)的响应 → worker `tcp.on('data')` → `pumpToRelay` → relay → 拨号方 `onRemoteData` → `dial.duplex.feed(payload)` → `push(chunk)` 触发 `'data'` → 🔴 **该监听把它 `pumpDial()` 又打回 relay** → worker 写进 agent socket ⇒ agent 拿 `HTTP/1.1 200 OK` 当**请求行**解析 ⇒ 非法字节流 ⇒ Fastify `clientError` 兜底回 `400`(写裸 socket、**不写 pino 日志**)。
⇒ **`200` 被回灌 ⇒ agent 回 `400` ⇒ 该 `400` 顺着同一条 keep-alive 连接被 curl 当成"第 2 条请求的响应"** —— 这就是"第 2 条必 400"的真实来源(curl 打印的 `400` **不是**第 2 条请求的应答,而是第 1 条响应的回灌回声)。
**为什么现有测试抓不到**:`test/relay.test.mjs#dialRoundTrip` 一凑够 `text.length` 就 `resolve`,多出来的那份回声**没人看**。
**排除项(逐层)**:relay 服务端无辜 —— `server.ts#onData → onDialerData` 只把 DATA 投给**对端**(`st.peer`),不镜像回发送方;错误方向只能是拨号方自己产生的。`dialer.ts` 的 `tcp.pipe(duplex).pipe(tcp)` 形状正确(`pipe` 走 `_write` → `onOut`)。
### 10.4 改法(唯一改动)
- **`src/net/relay/client.ts`**:**删掉**那一行 `duplex.on('data', …)`,原地换成一段⛔禁挂说明(含 106 抓包证据与 T23 指针)。
- **为什么删是安全的**:出向唯一入口是 `_write` → `onOut` → `pumpDial`;`duplex.write()` 与 `tcp.pipe(duplex)` **都走 `_write`** ⇒ 两者本就是同一条路,那个监听**纯属多余且方向错**。
- **附带修好的一条**:读侧不再被提前"吃掉" ⇒ `openStream()` 返回到 `tcp.pipe(duplex)` 之间到达的响应会**留在读缓冲**而不是被丢/被回灌。
- **`test/relay.test.mjs`**:新增 **T23**「拨号流严格单向」—— 目标端**逐帧记账**(`POST …` 记 `requests`、其余记 `garbage`),一旦入向回灌 ⇒ `garbage` 非空 ⇒ **断言点名**(不是"计数差不多"那种软判据)。
- 文件已在 `package.json` 的 `npm test` 列表内 ⇒ ⛔ **未改 `package.json`**(守住"改动不超出 `src/net/relay/**` + 其测试")。
### 10.5 先红后绿(原文级)
```
修前(旧 lib):
not ok 1 - T23 拨号流严格单向:入向的响应 ⛔ 不得被回灌进 agent socket
拨号流不是单向的 —— 入向字节被回灌进 agent socket(共 2 段):
["HTTP/1.1 200 OK\r\ncontent-length: 19\r\n\r\n{\"isDirectory\":true}", …(同上)]
build 后:ok 1 - T23 … # pass 1 / # fail 0
```
⚠️ **过程中踩到并修掉的测试卫生坑**:`onDown` → `teardownDialStreams()` 会 `duplex.destroy(new Error('link down: …'))`,而 `MuxDuplex` 上没有 `'error'` 监听 ⇒ Node 抛 **uncaughtException**,表现 =「T23 自己 pass、整个文件 fail」。生产侧 `dialer.ts` 有同形监听,测试里补上即可(已在 T23 的 `t.after` 注释里写死原因)。
### 10.6 端到端验收(原文级)
```
① guest POST /api/dsh/enter(同连接 ×2)
{"kind":"session","instance":{"id":"ae024d2c-…","port":21000,"status":"running"},"url":"https://guest.alotbuy.com/?token=…"} -> 200
(同上)-> 200
② 帧级复验:已分配的池口 = 25000(同连接 ×3)
{"isDirectory":true} -> 200 / 200 / 200 (修前 = 200 后必 400)
```
- `npm test` = **160 pass / 0 fail / 1 skip**(= 基线 159 pass + T23,**零退化**)
- `--scene all` = **12 PASS / 0 SKIP / 0 FAIL**(幕1-A 17874ms、幕4-A 18754ms 均 < deadline 30000ms)
- `overlay-probe`(带参数表)= **12/12 PASS · rc=0**
- **部署**:`client.js`(`b8b29afba06ed6347cbefd29091f8c73`)铺到 **5 处** —— 47 `/opt/dshs/lib`、47 `/opt/dshs-cluster/lib`、47 `/opt/dsh-relay/lib`、106 `/opt/dshs-cluster/lib`、106 `/opt/dsh-relay/lib`;各留 `.bak-20260917-1558xx`。
- **只重启了 47 的 `dshs`(Manager = 拨号方)**;`dshs-relay` / 106 `dshs-worker` **未重启**(改动是**拨号方专属**一行,worker/relay 侧行为逐字未变)。
- **R7 传播面自证**:铺之前对本机 `lib/net/relay/*.js` 与 47 `/opt/dshs/lib/net/relay/*.js` 逐文件对账 ⇒ **Δ 只有 `client.js` 一个文件**(无夹带改动)。
### 10.7 越界自证 & 三条硬门
- **D1**:参数表指纹 **`8f08e74b026e6e5b5e1b3db813f031ae`(未变)** ⇒ **未改任何生产值**;`RELAY_FAILOVER_*` / `HB_SEC` / burst 一字未动。
- **R7**:`git status --short` = **43**、`src/` = **20**(均 Δ0);仓内 `.bak`/`.tgz` = **0**;⛔ **未 commit / 未 push**。
- 🔴 `RELAY_FAILOVER_COOLDOWN_MS` 置 0:**本机仓 0 命中 / 47 lib 0 命中**(未写进任何回滚·演练·夹具路径)。
- **R4**:临时会话用完即删(`DELETE 1` ⇒ `sessions` 回 **2 = 基线**)。
- **未超范围**:改动只有 `src/net/relay/client.ts` + `test/relay.test.mjs` 两个文件,**未动 `package.json`、未动 worker 侧协议、未重启 relay 之外的服务**(Manager 重启属本次修复必需,已按 R8 直接做)。
### 10.8 🔴 新发现在册缺陷 B(R7 · **只报告,未动手**)
| 项 | 内容 |
|---|---|
| **现象** | 拨号池**未分配**的槽位,被**任意一条连接**碰到后 ⇒ **该槽位永久死亡**;而 `localPortFor()` 之后照样把这个口号发出去 ⇒ **`ECONNREFUSED`**(本棒实测:`[relay-dialer] 落点 127.0.0.1:25000 -> ops/w-106:19000` 紧接 `connect ECONNREFUSED 127.0.0.1:25000`,`enter` 回 500 `fetch failed`) |
| **出处** | `src/net/relay/dialer.ts#onConn`:`const key = slot.key; if (key === undefined) { slot.server.close(); tcp.destroy(); return }` —— 关闭**服务器本身**,却**不把该槽位从 `slots` 里摘掉**,于是 `key` 仍是 `undefined` ⇒ `localPortFor()` 的 `slots.find(s => s.key === undefined)` **下次还会选中它** |
| **怎么被触发** | 本棒**自己**触发:口池刚绑好、落点还没分配时,对 `127.0.0.1:25000` 打了一次探针(§9.3 那条命令的"提前版")⇒ 25000 当场死掉;池报"64 个口"而 `ss` 只见 **63**,正是这个缺口 |
| **为什么算缺陷** | 属本线反复强调的"**静默给出错误答案**"同族:**失败被推迟**到下一次分配,且现场只有 `ECONNREFUSED`,看不出"这个槽位早废了" |
| **候选修法(⛔ 未实施,留给下一棒先取证)** | ① `onConn` 撞到未分配槽位时**只 destroy 连接、不 close 服务器**(最小改动,但"外部随机连入"仍会被静默丢弃);② close 之后**把槽位从 `slots` 摘掉/标记废弃**,让 `localPortFor` 不再选中(更彻底,需处理 `bound` 计数与日志);③ 池口号改绑在**独立的不可路由网段/更冷门口位**并加"首次使用才分配"的显式状态机。**判据**:未分配槽位被连接后,`status().pool` 与实际在听口号数必须**恒等**,且 `localPortFor` 永不返回已死口号 |
| **回头条件** | 单独一棒处理;⛔ 在拿到"槽位被谁连、何时连"的帧级证据前**不要**只改 `onConn` |
### 10.9 其余在册未办(原样转下一棒,⛔ 未动手)
| 序 | 状态 |
|---|---|
| **Q4** | `src/net/relay/**` **untracked** 留档缺口 —— ⚠️ **本棒的修复正好落在该缺口内**(`client.ts` 是新文件、`test/relay.test.mjs` 也是 `??`)⇒ 二者目前**只存在于工作区,不在版本库里**。commit / push 仍需**你的明确授权**(本棒未获授权,未动)。 |
| **Q5** | `src/net/relay/*.bak-seq7-*` 残留 —— 未动(本棒仓内 `.bak` = 0) |
| **seq13 遗留** | 文档库・技能侧对 `mksess.cjs` 的描述("DB 直插")仍是旧口径 ⇒ 需同步时另起一小棒 |
| **长期在册** | `dsh-auto-handoff-chain` 在文档库**无副本** ⇒ 该技能的"三处同步"从未建立 |
| **§8.8-2** | 幕4-A 耗时仍**贴近** deadline(本次 18754ms / 30000ms)⇒ 临界项,未恶化也未解决 |
### 10.10 指纹(收口现取)
| 项 | 值 |
|---|---|
| **修复源文件** | `src/net/relay/client.ts` = **`6ffb117f3f6df1c2456641e176529ce8`** |
| **测试文件** | `test/relay.test.mjs` = **`14aa2808bc6446d34af19306d43c4e8d`**(1149 行,含 T23) |
| **部署产物** | `lib/net/relay/client.js` = **`b8b29afba06ed6347cbefd29091f8c73`**(五处远端同值) |
| 参数表(§10 不计入) | **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变** ⇒ D1 自证) |
| 本交接单 §8 前缀(追加 §10 后复取) | **`3ece0f870cba67d0113a4c5f9de9d812`**(**未变** —— §10 在 §8 之后,不进前缀口径) |
| 原始取证落盘 | `_中间产物_待清理/seq14/`(`deploy-seq14.sh` / `evidence-47.sh` / `final-verify.sh`);`--scene all` 原文 = 本地 `/tmp/seq14-scene-all.txt` |
| 下一棒 | automation **`1933b18a-0c2a-46a2-829c-440a900fed08`**(「覆盖网络线-序15执行棒-拨号池槽位自毁缺陷B修复」,一次性,`scheduledAt` = 2026-09-17 **16:14**,`nextRunAt` = 1789632840000) |
---
## §11 执行回报(序⑮ 执行棒 · 2026-09-17 16:14–16:3x · 拨号池未分配槽位自毁「缺陷 B」)
### 11.1 结论(逐条)
✅ **缺陷 B 已修并端到端验收**:`src/net/relay/dialer.ts`(1 个源文件)+ `test/relay.test.mjs`(新增 T24);判据 **a / b / c 三条全绿**;⛔ 未改任何生产值、⛔ 未 commit / push、**零上抛**;收口 6 件全办。
### 11.2 复现(D1:先复现、后动手 —— ⛔ 没在"只看到 `ECONNREFUSED`"的情况下动 `onConn`)
**① 进程内确定性复现**(`_中间产物_待清理/seq15/repro-defect-b.mjs`;私有口段 47000、私有实例段 47100,**零生产影响**)—— 修前原文:
```
① 池就绪(注册=true):status().pool = 4 · 实际在听 = 4 · 区间 47000..47003
② 对未分配口 47000 发一条连接 ⇒ connected
status().pool = 4 · 实际在听 = 3 ⇒ 🔴 判据 a 不等(池在撒谎)
该路径日志行数 = 0
③ localPortFor(ops/w-xs, 47110) = 47000(死口 = 47000)
④ 🔴 判据 b:落点不通 = ECONNREFUSED
```
**② 47 现网事实**(只读取证,落盘 `A47.txt`;⛔ 全程未连任何池口):
- `dshs` 于 **16:09:11** 重启 ⇒ 池口 **64 个全在听**(`ss -lntH 'sport >= :25000 and sport <= :25063' | wc -l` = 64)、`ss -lntp` 总行数 **79**、逐口核对 **无缺失**。
- journal 里 **`ECONNREFUSED 127.0.0.1:25000` 全量 3 条,全部落在 16:00:31 / 16:00:35 / 16:00:40**,全部是 `POST /api/dsh/enter` → **500 `fetch failed`**(栈 = `supervisor/remote-spawner.js:142` ← `:211` ← `web/routes/dsh.js:200`)。
- 而这三条**紧跟在 16:00:27 的 `[relay-dialer] 落点 127.0.0.1:25000 -> ops/w-106:19000` 之后** ⇒ **"落点已分配、却连不上"** —— 这正是"该槽位早就死了"的**演绎证据**(唯一能关掉池槽位监听又保留槽位的路径就是 `onConn` 的 `key === undefined` 分支)。
- **谁 / 何时连了未分配槽位**:**是我们自己的取证探针触发的,不是自然发生** —— 该路径**零日志零计数**(全量 journal `grep -c '未分配'` = **0**),只能靠上面那组矛盾反推。⇒ 这也正是"必须补日志/计数"的直接理由。
### 11.3 定位(到代码行)
`src/net/relay/dialer.ts#onConn`:
```ts
const key = slot.key
if (key === undefined) { slot.server.close(); tcp.destroy(); return }
```
关掉的只是**这个口的服务器**,而槽位**仍留在 `slots` 里、`key` 仍是 `undefined`** ⇒ `localPortFor()` 的 `slots.find((s) => s.key === undefined)` **下次还会选中它** ⇒ 把一个没人听的口号当落点发出去。**失败被推迟到下一次分配**,现场只有 `ECONNREFUSED`。
### 11.4 改法(三个候选里取 ①,理由 = R11)
1. **`onConn`:⛔ 不再 `close()` 服务器,只 `tcp.destroy()` + 计数(新增 `stray`)+ 点名日志。**
⛔ **不取候选 ②**(把槽位从 `slots` 摘掉):那等于让**任意一条本地连接**都能**永久**蚕食池容量(扫 64 次即可把池扫空)= **净退化(R11)**;候选 ③(换口段 + 显式状态机)超出 `src/net/relay/**` 必要范围。
2. **纵深防御**:`localPortFor()` 的空槽查找与 `lruIdle()` 都**只认 `server.listening` 的槽**;一个可成交槽都没有时**点名"哪几个口已不在听"**再拒(失败关闭,⛔ 绝不发死口号)。
3. **`status()`**:`pool` 改报**实际还在听的槽位数**(不再报 `start()` 那一刻的常量)+ 新增 `stray` 计数 —— 让"池在撒谎"这类事**不可能不被看见**。
### 11.5 先红后绿(原文级)
- **红**(新用例 + 旧 `lib/`):`判据 a:池账(3) 与实际在听(2) 必须恒等 —— 不等即"池在撒谎"` ⇒ `3 !== 2`(`ERR_ASSERTION`)。
- **绿**(`npm run build` 后):`ok 1 - T24 拨号池:未分配槽位被一条连接命中后 ⛔ 不得自毁…` / `# pass 1 / # fail 0`。
- 新用例 = **T24**(`test/relay.test.mjs` 尾部,文件 1318 行);⛔ 未改 `package.json`。
### 11.6 端到端验收(47 现网原文)
**① 收口终验**(`verify-seq15.sh`,含一次 `systemctl restart dshs`)—— ⚠️ 这个顺序**就是缺陷 B 的复现顺序**:
| 步 | 原文 |
|---|---|
| 干净池基线 | 池口在听数 = **64** |`[relay-dialer] 本机落点池就绪:64 个口(25000..26000)` |
| 🔴 **故意对未分配落点 25000 发一条连接** | `TCP 已连上` ⇒ **打后池口在听数仍 64**、stray 日志 **1** 行:`[relay-dialer] ⛔ 未分配落点 127.0.0.1:25000 收到一条连接 ⇒ 只丢弃该连接、槽位保留(累计 1 次)` |
| guest `POST /api/dsh/enter`(同连接 ×2) | **200 / 200**(`instance.port=21000 status=running`) |
| 帧级(该落点口) | 落点口 = **25000**(就是刚被打过的那一个)|该口在听 = 1|同连接连发 3 次 ⇒ **200 / 200 / 200**(`{"isDirectory":true}`) |
| R4 | `sessions` 清理后回 **2**|结束态池口 = 64 |
**② 最终态复验**(`verify-seq15-final.sh`,⛔ **不重启**,就在收口现场把同一顺序再走一遍)= `verify-47-final.txt`,逐项与 ① 同(64 → 触发 → **仍 64 / stray=1** → **200 / 200** → 25000 在听 200×3 → 结束态 64)。
**③ 零回归**:`npm test` = **162 tests / 161 pass / 0 fail / 1 skip**(基线 160 pass + 新 T24)|`--scene all` = **12 PASS / 0 SKIP / 0 FAIL**(幕 4-A 19537 ms、幕 4-C 20809 ms / 30000 ms)|`overlay-probe --table "E:/…/参数表_覆盖网络_20260917.md"` = **12/12 PASS · rc=0**(`OBS-11 集合 … 实际 78 多出 0 缺失 0`、`OBS-09 本机:20000=401 w-106:33909=401`)。
**部署**:`lib/net/relay/dialer.js` = **`6446fe9b23bca2649137adcbf4bf9d1f`**,**五处同值**(47:`/opt/dshs` `/opt/dshs-cluster` `/opt/dsh-relay`;106:`/opt/dshs-cluster` `/opt/dsh-relay`,106 无 `/opt/dshs`);每处各留 `.bak-20260917-1621xx`;**只重启了 47 的 `dshs`**(拨号方专属改动)。
### 11.7 越界自证 & 三条硬门
| 门 | 自证 |
|---|---|
| **D1** | 参数表指纹(§10 截断口径)仍 **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变**)|⛔ 未动 `RELAY_FAILOVER_*` / `HB_SEC` / burst |
| **R7** | `git status` 总行数 **43**、`src/` **20** 均为既存基线(Δ0);本棒只改 `src/net/relay/dialer.ts` + `test/relay.test.mjs`(两者同处 untracked 的 `src/net/relay/**`、`test/` 属**既有缺口**)|⛔ 未 commit / push|Q4 / Q5 **只报告未动手**;范围**未越出** `src/net/relay/**` + 其测试(⛔ 未动 worker 侧协议、未重启 relay / 106) |
| **COOLDOWN** | 远端**严格判据(赋 0)0 命中**|⚠️ 本棒 `verify-seq15.sh` 里的宽松正则 `"\?0"\?` 会**误报 2** —— 命中 `switcher.js:53` 的默认字面量 `num('RELAY_FAILOVER_COOLDOWN_MS', 300_000)` 里的 `0`;口径以「值被赋 0」为准(`grep -E '=\s*0\|, 0\)\|"0"'`) |
### 11.8 其余在册未办(原样转下一棒,⛔ 未动手)
| 序 | 状态 |
|---|---|
| **Q4** | `src/net/relay/**` untracked 留档缺口 —— ⛔ **卡在"提交 / 推送授权"**(本棒未获授权,未动;本棒的修复与 T24 同样只存在于工作区) |
| **Q5** | 代码仓 `src/net/relay/*.bak-*` 残留 **3 个**(本棒未清,R7) |
| **seq13 遗留** | 文档库 / 技能侧对 `/opt/dshs/mksess.cjs` 的「DB 直插」描述仍是旧口径(实际已改 PG) |
| **长期在册** | 技能 `dsh-auto-handoff-chain` 在文档库**无副本** ⇒ 该技能「三处同步」从未建立 |
| **§8.8-2** | 幕 4-A 耗时 **19537 / 20809 ms**(贴近 deadline 30000 ms)⇒ 临界项,未恶化也未解决 |
### 11.9 指纹(收口现取)
| 项 | 值 |
|---|---|
| **修复源文件** | `src/net/relay/dialer.ts` = **`f3a608a75f7edd0aa0de8cceb4de5615`** |
| **测试文件** | `test/relay.test.mjs` = **`587f9d80e9330e33badb281e6bd6c719`**(1318 行,含 T24) |
| **部署产物** | `lib/net/relay/dialer.js` = **`6446fe9b23bca2649137adcbf4bf9d1f`**(五处远端同值) |
| 参数表(不计入 §11 口径) | **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变** ⇒ D1 自证;口径 = `sed '/^## §10 指纹/,$d' … \| md5sum`) |
| 本交接单 §8 前缀(追加 §11 后复取) | **`3ece0f870cba67d0113a4c5f9de9d812`**(**未变** —— §11 在 §8 之后,不进前缀口径) |
| 原始取证落盘 | `_中间产物_待清理/seq15/`:`evidence-A-47.sh`+`A47.txt`|`repro-defect-b.mjs`|`deploy-seq15.sh`+`deploy-47.txt`/`deploy-106.txt`|`verify-seq15.sh`+`verify-47.txt`|`verify-seq15-final.sh`+`verify-47-final.txt`|`npmtest-seq15.txt`|`scene-all.txt`|`probe.txt` |
| **下一棒** | automation **`91c53ef8-c3f0-4bb3-9f0a-943790178176`**(「覆盖网络线-序16规划棒-会合中继拆分复核与在册收尾定序」,一次性,`scheduledAt` = 2026-09-17 **16:36**,`nextRunAt` = 1789634160000) |
### 11.10 范围外 · 只报告(R7,⛔ 未动手)
**ssh 别名 `bt-server` 指向的 `32022` 是失效残留口。** 本机 `~/.ssh/config` 里 `bt-server` 写的是 `Port 32022`,而 47 上 **sshd 只监听 `22`**(`/etc/ssh/sshd_config:151 Port 22`;master pid 733418 **启动于 2026-09-16 22:12:48**、`/etc/ssh/` 下**无** `sshd_config.d` drop-in;`32022` 无监听)。项目脚本**一律显式写 `ssh -p 22 bt-server`**(近 20 份脚本 42 处均如此)⇒ 只有"直接敲别名"会 `Connection refused`。本棒为绕开它**多花了 3 次调用**;⛔ 未改 47 的 sshd_config、⛔ 未改本机 ssh config(等指示)。
---
## §12 执行回报(序⑯ 规划棒 · 2026-09-17 16:36–16:5x · 会合/中继拆分复核 + 在册收尾定序)
### 12.1 结论(1 行)
**① 会合 / 中继从 Manager 拆分 ⇒ 判「不做」**(四个耦合点 C1–C4 + 两步改造 S3/S4 **逐条已被覆盖,未覆盖部分 = 无**)|**② 剩余在册四条已定序并出单** ⇒ 产物 = 工作区根 **`交接单_在册收尾_20260917.md`**(243 行;**§8 前缀指纹 `bab83b7219b2669d5a6e9f1acf782e1f`**,全文 md5 `ca13b9ded3d71268f8b0e56755d7aea4`)= **序 ⑰ 执行棒**(S1 mksess 文档口径 → S2 技能三处同步 → S3 Q5 清理 → S4 幕 4-A 临界项)。⛔ 只出规划:未改码、未动 47/106、未 commit/push、零上抛。
### 12.2 复核判定(① 二选一 → **判「不做」**)
| 靶子 | 现状证据(只读取证) | 判定 |
|---|---|---|
| **C1** 会合地址硬编码在 Worker env | `src/config.ts:485-490`:`DSHS_RENDEZVOUS_URL` **优先** / `DSHS_TUNNEL_TARGET` 降**兜底**(注释原文「覆盖网络 S1:会合地址出 env」) | ✅ 序② P0-2 覆盖 |
| **C2** 中继落点 = Manager loopback + 两端同号 | `src/config.ts:498-500` `DSHS_RELAY_DIAL_PORT_BASE=25000` / `SPAN=1000` / `POOL=64` ⇒ 落点在 **Manager 本机**回环池;`src/net/relay/dialer.ts:11`「Manager 也像 worker 一样**只拨出**一条 wss」 | ✅ **R5** 覆盖 |
| **C3** 可达性登记磨掉「经谁中转」 | `src/net/rendezvous.ts:103-106`「按 `via` 选实现的注册表」+ `Reachability.via`(`:27/:65/:98`)+ `agentBaseUrlOf()` 单一取址入口(`src/net/reachability.ts:99`) | ✅ S2/P2 覆盖 |
| **C4** 控制面 PG 走同一隧道 | 方案 §8.1 实测:106/47 worker env **均无** `DSHS_TUNNEL_STATIC_PORTS` | ✅ **前提不成立** |
| **S3** 回环别名 | 方案 §9.1 自我证伪(`gatewayports no`)⇒ 改「实例端口区间隔离」,且 R5 后「同号」前提消失 | ✅ 已被更强做法替代 |
| **S4** 中继独立成单元 / 会合可换机 / 多实例 | `dshs-relay` 独立单元(47+106,只绑回环 `20080`)+ 序⑥ S8 第二中继 + 序⑦「杀 47 ⇒ 切 106」实测 | ✅ R2 / 序⑥S8 / 序⑦ 覆盖 |
**处置已落**:`会合中继拆分_取证与改造方案_20260916.md` **头部加状态块**(「已完成使命 · 仅存档 · ⛔ 勿再按 S0–S4 开工」+ 逐条覆盖指针),**正文未改**(历史档案属性)。
⚠️ **顺带登记(⛔ 未处置)**:`src/worker/tunnel.ts`(SSH 隧道)现已是**生产死路径**(47 同机不建隧道、106 走 `wss://`)⇒ 是否删 = 独立决策(可能保留作 ssh 兜底变体)。
### 12.3 在册项定序(② → 序 ⑰ 执行棒,写进同一张单)
| 序 | 项 | 为什么在这个位置 |
|---|---|---|
| **S1** | mksess 文档口径(「DB 直插」→「PG 直插」) | **必须最先** —— 它改 `skills/**` 正文,**S2 的镜像同步要把它的产物一起带上** ⇒ 二者共用**一次 scp**(硬顺序) |
| **S2** | 技能 `dsh-auto-handoff-chain` 三处同步 | 紧随 S1(同面、共用一次 scp);它是**自动接续链路**载体 ⇒ 影响后续每一棒的可复现性 |
| **S3** | Q5 `.bak-seq7-*` 清理 | 与 S1/S2 无依赖,同属「零服务副作用批」⇒ 合批省一次抢锁 |
| **S4** | §8.8-2 临界项(幕 4-A) | **唯一动服务项** ⇒ 置末;收尾 `restart dshs` 归零不影响已完成工作 |
**排序一句话**:`S1 → S2`(硬依赖)→ `S3`(零风险清理)→ `S4`(唯一动服务,置末)。
### 12.4 本棒取证(只读 3 条命令族;⛔ 未连任何池口、未动 47/106)
| # | 命令(`cwd` = `D:/github/dsh_shenxian`) | 关键输出 |
|---|---|---|
| 1 | `grep -rn "DSHS_TUNNEL_TARGET\|32022" --include=*.ts src` + `grep -rn "DSHS_RENDEZVOUS_URL" --include=*.ts src` | C1 现状(新变量优先、旧变量兜底) |
| 2 | `grep -rn "via\|25000\|dialer" src/net/rendezvous.ts src/net/relay/dialer.ts` + `grep -rn "DSHS_RELAY_DIAL" src/config.ts` | C2/C3 现状 |
| 3 | `find src test scripts -name "*.bak-*"` + `git ls-files src/net/relay \| wc -l` + `git ls-files --error-unmatch <bak>` | 🔴 **Q5 真实范围 = 5 个(非 3 个)**;`src/net/relay` tracked = **0**(Q4 自证) |
🔴 **本棒两条新事实(写进单里,均已自决处置)**:
1. **Q5 范围修正:3 → 5** —— 除原单记的 `src/net/relay/{client,directory,main}.ts.bak-seq7-20260917-112114` 外,**还有 2 个同批残留**:`src/web/server.ts.bak-seq7-20260917-112114`、`src/worker/tunnel.ts.bak-seq7-20260917-112114`。**5 个全部 untracked**(`git ls-files --error-unmatch` 报 pathspec 不匹配)⇒ 删除只减 untracked、不动版本库基线。已按 D2 **扩到 5 个**(同批、同类、**均为过期快照 ⇒ 无回滚价值**)。
2. **`04-调整方案/**` 里的 mksess 旧口径不改** —— 档案属性是**当时事实**(当时确实直插 SQLite),改写 = 销毁溯源。已按 D1 只改操作性载体(`02-运维手册.md` + `skills/**` + 本机技能 10 行),在运维手册 R4 段写一行勘误指针。
3. **幕 4-A 判据取三档递进**(D3):`p95 ≤ 24 s = 健康`|`24–27 s = 临界(记录、⛔ 不判 FAIL)`|`> 27 s = 回头条件触发` —— 观测样本 19.5/20.8/22.2 s,把「假红 vs 预警提前量」的取舍**消解**掉。
### 12.5 越界自证
| 门 | 自证 |
|---|---|
| **D1** | 参数表(截断口径)仍 **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变**)|⛔ 未动 `RELAY_FAILOVER_*` / `HB_SEC` / burst |
| **R7** | 本棒**零代码改动、零服务器改动**(只新增 1 份单 + 给 1 份存档方案加状态块)|⛔ 未 commit / push(HEAD 仍 `640813e`)|Q4 / Q5 **只报告未动手** |
| **COOLDOWN** | ⛔ 本棒未触任何演练 / 夹具路径;单内已把 🔴 禁令原文写进 §3.2 |
| **ssh** | 本棒**未对 47 / 106 执行任何命令**(纯本地只读)⇒ 无 `-p 22` 相关风险面 |
### 12.6 在册未办(原样转 §12.8 的下一棒;⛔ 只报告)
| 序 | 状态 |
|---|---|
| **Q4** | `src/net/relay/**` untracked 留档缺口 —— 🔴 **卡在「提交 / 推送授权」**(实测 `git ls-files src/net/relay` = **0**)⇒ ⛔ **未写进执行范围**,单内 §0.4 只登记一行 |
| **Q5** | 已**定序进 S3**(范围修正为 5 个) |
| **§8.8-1** | 106 sshd MaxStartups 限流(外部爆破)⇒ 脚本化 ssh 偶发 `rc=255`;⛔ 不放宽(命中 R5)⇒ 仍在册 |
| **§5.10 三项** | presence / 房间层 / 内容分发 —— ⛔ 属业务优先级,单内 **§3.3 只登记候选与优缺点、不定序** |
### 12.7 指纹(收口现取)
| 项 | 值 |
|---|---|
| **本棒产物** | `交接单_在册收尾_20260917.md` = **243 行**;全文 md5 **`ca13b9ded3d71268f8b0e56755d7aea4`**;**§8 前缀指纹 `bab83b7219b2669d5a6e9f1acf782e1f`**(口径 = `sed '/^## §8 回报格式/,$d' … \| md5sum`);行尾 = **纯 LF**(CR=0 自证) |
| 参数表(不计入本单口径) | **`8f08e74b026e6e5b5e1b3db813f031ae`**(**未变** ⇒ D1 自证) |
| 被加状态块的存档方案 | `会合中继拆分_取证与改造方案_20260916.md`(头部新增状态块,**正文未改**;原 278 行) |
| 本交接单 §8 前缀 | **`3ece0f870cba67d0113a4c5f9de9d812`**(**未变** —— §12 在 §8 之后,不进前缀口径) |
| 本机技能 mksess 命中 | 文档库 skills = **10 行** / 本机技能 = **10 行**(同数 ⇒ 两副本同步态;**待 S1 同改**) |
| **下一棒** | automation **`067b0892-964e-4deb-bf84-4f1c37bebea8`**(「覆盖网络线-序17执行棒-在册收尾(mksess口径/技能三处同步/Q5清理/幕4A临界项)」,一次性,`scheduledAt` = 2026-09-17 **16:44**,`nextRunAt` = 1789634640000) |
### 12.8 下一棒(序 ⑰ · 执行棒)
**开工依据 = 工作区根 `交接单_在册收尾_20260917.md`(§8 前缀指纹 `bab83b7219b2669d5a6e9f1acf782e1f`)**,按 §5 的 **S1 → S2 → S3 → S4** 顺序执行。
**已登记**:automation **`067b0892-964e-4deb-bf84-4f1c37bebea8`**(一次性,`scheduledAt` = 2026-09-17 **16:44**,`nextRunAt` = 1789634640000)⇒ 已用陈述句告知用户。
**两条硬门**:**D1**(不改任何生产值)|**R7**(Q4 ⛔ 只报告、⛔ 不 commit/push;范围外先报告后动手)。
🔴 ⛔ **不许把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写进任何回滚 / 演练 / 夹具路径**。
**已定项(⛔ 不必再上抛)**:D1 只改操作性载体(档案不动)|D2 Q5 扩到 5 个|D3 幕 4-A 三档判据|D4 不处理 `tunnel.ts` 死路径|D5 触发回头条件时停下报告、不调生产值。
**收口必须**:释放锁 + **陈述句**告知 + 登记下一棒(id 取自工具返回值)+ 推进 `接续入口_覆盖网络线_20260916.md` §0/§2 + 写工作区日志 + 在本单回填 §8。⛔ 不 commit / 不 push。
---
## §13 序 ⑳ 新增在册缺陷(2026-09-17 18:2x–19:2x · ⛔ 本棒只登记、未动手)
> 本棒在**量化 presence 真机收益**时新查证两条。两者均按 **R7** 处理:**先报告、后动手**。
> 完整取证、命令原文与判定 = 交接单 `交接单_presence在线态_20260917.md` **§8.10**。
| # | 缺陷 | 证据(可复现) | 影响面 | 判定 |
|---|---|---|---|---|
| **P-1** | **`presenceFresh()` 的新鲜度判据与"稳态零帧"互相打架** ⇒ 订阅生效期间 `/status` 轮询**照旧在跑**,presence 的**核心收益未兑现** | ① 判据 = `state==='subscribed' && now − max(lastSnapAt,lastPushAt) ≤ PRESENCE_TTL_MS`(`src/net/relay/client.ts:593-600`)② 稳态 `pushed/snaps` 自 relay 重启起**恒为 1**(**一帧都不推**)⇒ 45 s 后**必然过期** ⇒ 门重开 ③ 实测 `R_now ≈ 10.9 次/分钟` vs 基准 `R_base = 60 000 / RELAY_STATUS_POLL_MS(5000) = 12 次/分钟` ⇒ **降幅 1.10×** ④ 门开关时间线:35 min 内 **11 次切换**、挂起窗口 7/20/20/10/45 s ⇒ **占空比 5.0%**(理论降幅 1.05×,与实测吻合) | 全平台(Manager→relay 的读取量≈无变化);**用户不可见** ⇒ 属**成本 / 设计目标**项 | **另立单**(技术项,下一棒可自决) |
| **P-2** | `src/web/server.ts#translateEndpoint` **键口径不一致 ⇒ 整个闭包是死分支**(`via='relay'` 的实例面翻译**从不生效**) | ① `:733 hostVia.set(name, row.via)`,`name = logicalName(networkId, row.id)` = `ops/w-106`(键 = **逻辑名**)② `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** ⇒ **当前零实际影响** | **另立单**(本棒**无验证条件**:无活跃实例 + 该闭包不可先红后绿,需先抽纯函数) |
**P-1 候选修法(供下一棒选)**:
A) 把**任何入站帧**(含协议心跳)计入新鲜度 —— 最小改动、语义正(连接活着 + 订阅已建立 ⇒ 镜像有效);
B) relay **定期重推 `SNAP`** —— 自愈(漏推可被下一次纠正),代价 = 周期流量;
C) 拉长 `PRESENCE_TTL_MS` —— 只把竞速推远,⛔ 不推荐单用。
**P-2 候选修法**:把翻译判定抽成纯函数 `relayEndpointTarget({via, dialedPort, snapshotLocalPort})` + 建 `hostId → 逻辑名` 映射(`hostNameById`);判据 = `via≠relay` ⇒ 原样透传 / `via=relay` ⇒ ①拨号落点 ②快照落点 ③**`undefined`(失败关闭,⛔ 不原样透传)**;单测至少 4 例,含"传**裸 hostId** 必须命中"的键口径回归。
**两条共同硬门**:**D1**(不改任何生产值)|**R5**(不扩大暴露面)|**R7**(⛔ 不 commit / 不 push)|🔴 ⛔ **不许把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写进任何回滚 / 演练 / 夹具路径**。
---
## §14 序㉑ 回报(2026-09-17 19:0x–19:4x · **P-1 与 P-2 均已修**;详细取证 = `交接单_presence在线态_20260917.md` **§8.11**)
| # | 项 | 判定 | 结果(一句话) |
|---|---|---|---|
| **P-1** | `presenceFresh()` 新鲜度判据与"稳态零帧"打架 | ✅ **已修** | 判据改为「**订阅已建立 ∧ 链路活着**」(任何入站帧,含心跳;上界 = `max(TTL, 半开阈值)`)。**候选取 A**:⛔ B(周期 `SNAP`)会**推翻序⑲ E1「稳态 Δpushed ≤ 0」**与 `OBS-14` 的 `snaps ≤ pushed` ⇒ R11 净变差;⛔ C 只把竞速推远。**真机同口径 Before/After**:67 s 窗口 `ΔstatusHits 14 → **1**`(探针自身读数 = 1 ⇒ **Manager 侧 13 → 0**),门开关 **0 次翻转**(重启后 5+ 分钟仅 1 行"挂起")。 |
| **P-2** | `translateEndpoint` 键口径不一致 ⇒ 闭包是死分支 | ✅ **已修(单测面)** | 抽出纯函数 `src/net/relay/endpoint-target.ts`(`relayEndpointTarget` + `hostNameIndex`)+ 闭包先换逻辑名(`hostNameById`);`T35` 四支判定(含"⛔ 不误触发拨号池")、`T36` 键口径 + **源码级守卫**(⛔ 不得再出现裸 `hostId` 作键)。**E2E 未做**(无活跃实例 ⇒ 按入口授权"只做到单测",可执行判据已写进 presence 单 §8.11-⑤)。 |
| **新** | `OBS-16`(门的验收判据) | ➕ **落地** | ⛔ 不能只看 `OBS-13`(它的 `ΔstatusHits ≥ 1` 由探针自身两次读即满足 ⇒ 旧口径下轮询照旧在跑也全绿);新增第三次采样 + `PRESENCE_GATE_WINDOW_MS` / `PRESENCE_GATE_HITS_MAX`;夹具两侧实证 PASS(Δ=1, rc=0) / FAIL(Δ=12, rc=1);真机 PASS。 |
| **新** | **P-2b**(本棒只登记、⛔ 未动手) | ⏳ 在册 | `relayEndpointTarget` 只有"拨号落点 → relay 快照"两级;**P-1 修好后快照路径趋冷**,若拨号池分不出槽位而**订阅推送里有落点**仍会判失败关闭 ⇒ 候选 = 把 `presenceLocalPort` 插成 ②'级(与 `addressOf` 三级链对齐)。属**行为面新增** ⇒ 按 R7 先报告后动手。 |
**零回归三件套**:`npm test` **174/173/0/1**(基线 170/169/0/1 + 新 T33–T36)|`--scene all` **12 PASS / 0 SKIP / 0 FAIL**|`overlay-probe` **15 PASS / 0 FAIL / 1 SKIP**(基线 14/0/1 + `OBS-16`)。
**边界自证**:⛔ 未改任何生产值(`RELAY_FAILOVER_*` / `HB_SEC` / burst;`PRESENCE_TTL_MS` 值未改)|⛔ 未新增公网口 / 未改 nft·nginx|🔴 `COOLDOWN_MS=0` 计数 **0**|⛔ 未 commit / 未 push。
**指纹**:参数表 **`42238175d84319ada99afa56d583f9db`**(旧 `659c08949c193102f774822905fde3b7`)|探针 `61a9fc5623bb617c802daf604d3ce0f3`(旧 `4cbe34f1ffc51cfab78f0540224aa388`)|🔴 **本单指纹改记「前缀口径」**(⛔ 不记全文 —— §13/§14 自身就是回填产物,全文 md5 必然**自指过时**):`sed '/^## §13 序 ⑳ 新增在册缺陷/,$d' 交接单_观测口径与在册缺陷_20260917.md | md5sum` = **`fc0389d759c57a14d038b6450a36d8e1`**(**§13 之前**的正文;回填 §13/§14 **不影响**它,可复现)|收口时全文 md5 实测 = `28bbe2a7467569a0ad76a486ae37546d`(**814 行**、CR=0),⚠️ **仅作当次快照,⛔ 别当复现判据**(旧记的 `5495f2d2360aea64e763ec0e5aeb2987` 是"写完 §14 但尚未补 §13 的 P-2b 行"那一瞬的值,⚠️ 已作废)。
---
## §15 序 ㉒ 回报(2026-09-17 19:33–20:0x · **P-2b 已修 + P-1 长窗复测通过**;详细取证 = `交接单_presence在线态_20260917.md` **§8.12**)
| # | 项 | 判定 | 结果(一句话) |
|---|---|---|---|
| **P-2b** | `relayEndpointTarget` 候选链只有两级 ⇒ 订阅推送里有落点也会判失败关闭 | ✅ **已修** | 补 **②' 级 `pushedLocalPort`**,链变**三级**且与 `addressOf` **逐级对齐**;改动**只在 3 个在册文件**(`endpoint-target.ts` / `web/server.ts` / `test/relay.test.mjs`);**先红后绿**(产物里去掉 ②' 级 ⇒ `T37` actual `{41000,'snapshot'}` vs expected `{37057,'pushed'}`);新增 **T37**(三级逐支 + `0` 哨兵逐级下探 + ⛔ 不碰拨号池)/**T38**(源码级守卫:闭包必须把 `presenceLocalPort(name, ep.port)` 传进判定)。 |
| **P-1(长窗)** | 序㉑ 只验过 **67 s 短窗** | ✅ **长窗通过** | 窗口 **706 s**(11.77 min):`ΔstatusHits 9→10` ⇒ **Manager 侧 = 0**;门开关窗口内 **0 行**;`Δpushed=0` / `Δsnaps=0`。相对基准 `60 000/5 000 = 12 次/分钟`:raw **0.085 次/分钟** ⇒ **141.2×**(Manager 侧零命中 ⇒ 无穷倍)。 |
| **新** | 环境勘误:106 的 ssh 口径 | 🔴 **已定** | **必须用别名 `test106`**(专用密钥 `id_ed25519_test106`)。裸 `ssh [email protected]` ⇒ `Permission denied (publickey)` —— ⛔ **别再误判成 MaxStartups 限流**(本棒先误判、后取证纠正)。 |
| **遗留** | E2E(P-2 + P-2b) | ⏳ **未做** | 无活跃实例(47 上 `dsh-systemctl list-units 'dsh-*.scope'` = **0**)⇒ 判据仍留 presence 单 **§8.11-⑤**,交 **序 ㉓**。 |
**在册缺陷表(现行)**:**P-1 ✅ / P-2 ✅(单测面)/ P-2b ✅** —— **无未关闭在册缺陷**;剩余仅 **`§9-10`「relay 侧静默丢订阅而 socket 仍活 ⇒ 门察觉不到」**(当前代码路径**不可达**,属**回头条件**而非在册缺陷)+ **E2E 缺口**。
**零回归三件套**:`npm test` **176/175/0/1**(基线 174/173/0/1 + T37/T38)|`--scene all` **12 PASS / 0 SKIP / 0 FAIL**|`overlay-probe` **15 PASS / 0 FAIL / 1 SKIP**(**部署后重跑同值**)。
**部署 + 回滚点**:`lib/net/relay/{endpoint-target,index}.js` + `lib/web/server.js` 铺 **4 处**(47 `/opt/dshs/lib`+`/opt/dsh-relay/lib`、106 `/opt/dshs-cluster/lib`+`/opt/dsh-relay/lib`),md5 四根全同(`36b2dd99…` / `486419ba…` / `460d360e…`);**只重启 47 的 `dshs`**;回滚点 = 两机 **`/opt/dsh/backups/seq22-20260917-195602/`**(旧 md5 `57e6c9ae…` / `486419ba…` / `74c077b3…`,由"反向改回两处 hunk → `tsc`"**精确重放**取得)。
**边界自证**:⛔ 未改任何生产值(`RELAY_FAILOVER_*` / `HB_SEC` / burst 逐项未变)|⛔ 未新增公网口 / 未改 nft·nginx|🔴 `COOLDOWN_MS=0` 计数 **0**|⛔ 未 commit / 未 push。
**指纹**:参数表 **`42238175d84319ada99afa56d583f9db`(未变 ✅ —— 本棒无数值改动)**|探针 `61a9fc5623bb617c802daf604d3ce0f3`(未变)|本单 §13 前缀口径 `fc0389d759c57a14d038b6450a36d8e1`(**未变 ✅ —— §15 在截断点之后**)|代码面:`endpoint-target.ts` `9f421a9f7bdff18cd02fd60cf4add2fb`、`web/server.ts` `8c4c072cb7dc068b4f378f51b5d5a29a`、`test/relay.test.mjs` `f08088b8c07bf7f18a0c0ce0a98ee018`。
---
## §16 序 ㉓ 回报(2026-09-17 20:08–20:4x · **E2E 已验收 + 在册缺陷表清空 + 线内可停**;详细取证 = `交接单_presence在线态_20260917.md` **§8.13**)
| # | 项 | 判定 | 结果(一句话) |
|---|---|---|---|
| **E2E(P-2 + P-2b)** | 序㉑/序㉒ 都只做到单测 | ✅ **已验收** | **R4** 临时 session ⇒ `POST /api/dsh/enter`:ⓐ **200**(同连接双发 + 6 轮复测全 200)ⓑ 47 上 `:21000` 监听 **0**(`:19000` 亦 0)ⓒ `overlay-endpoint` 窗口内 **0 行**(失败关闭未触发)ⓓ relay `counters.dial` **0 → 2**。**判别器更强证据**:拨号原文 `落点 127.0.0.1:25000 -> ops/w-106:19000`(agent 面)+ `落点 127.0.0.1:25001 -> ops/w-106:21000`(实例面)⇒ 键口径(逻辑名)与三级候选链**都在生产上走通了**;同时 106 上 `dsh-100002-…scope` argv 末尾 `--port 21000` 在听 ⇒ **"47 无 21000 / 106 有 21000" 同时成立** = 翻译生效强判据。 |
| **用户可见面** | 序⑬ Q2(guest(w-106) 实例页 502) | ✅ **判据已满足** | `curl -H 'Host: guest.alotbuy.com' 127.0.0.1:3080/` ⇒ **200**、**62451** 字节真 HTML、`502` 标记 **0**。 |
| **新** | `OBS-09` 由 SKIP → **PASS** | ✅ **回头条件兑现** | `PASS OBS-09 在册实例面 w-106:37747=401` ⇒ **序⑲ §9-8 的回头条件已兑现**("有用户访问实例后 `OBS-09` 应回 PASS"),⛔ **不是**真回归。探针整表 **16 PASS / 0 FAIL / 0 SKIP**(基线 15/0/1 ⇒ **+1 better**)。 |
| **新登记(R7 · ⛔ 未动手)** | **O-1**:`model landing failed, falling back to env injection Error: ENOENT … /var/lib/dshs/users/<uid>/home/.credentials.yaml` | ⏳ 线外 | **用本地路径读远端实例 home**(与序⑬ Q2 所记真因**同族**);有 env 注入兜底、**用户可见面仍 200**;归属 = `RemoteSpawner.resolveApiKey`(平台模型落地面)⇒ **不在覆盖网络线 §3.1 文件集**,仅登记、待定立项。 |
| **观察项** | `OBS-15` 在线态陈旧 p95 | ✅ 未越界 | 本棒 **1770 ms**(序㉒ 曾见 14220 ms,阈值 15000 ms)⇒ 抖动主导,⛔ 未改阈值。 |
**在册缺陷表(现行)**:**P-1 ✅ / P-2 ✅ / P-2b ✅ + E2E ✅** ⇒ **空**。剩余仅 **`§9-10`**(relay 静默丢订阅 ⇒ 回头条件,当前代码路径**不可达**)+ **三项待拍板**(`B` 内容分发 / 「Manager 重启后自动拉起实例」/ 入口 §4 骨干服务范围)。
**收口复核结论**:**覆盖网络线线内可停** ⇒ 按登记门禁(用户 2026-09-17 明令)**本棒不登记下一棒**,等拍板后再建。
**零回归三件套**:`npm test` **176/175/0/1** | `--scene all` **12 PASS / 0 SKIP / 0 FAIL**(`rc=0`)| `overlay-probe` **16 PASS / 0 FAIL / 0 SKIP**(`rc=0`)。
**边界自证**:⛔ 未改任何代码(in-册三个文件 md5 与序㉒ **全同**)|⛔ 未改任何生产值(参数表指纹 **未变**)|⛔ 未动 nft·nginx / 未新增公网口|🔴 `COOLDOWN_MS=0` 计数 **0**|⛔ 未 commit / 未 push|R4 临时 session 残留 **0**。
**指纹**:参数表 **`42238175d84319ada99afa56d583f9db`(未变 ✅)**|探针 `61a9fc5623bb617c802daf604d3ce0f3`(未变)|本单 §13 前缀口径 `fc0389d759c57a14d038b6450a36d8e1`(**未变 ✅ —— §16 在截断点之后**)|代码仓 HEAD `04776af`(改动数 10,与 T0 一致)。