feat(overlay): 覆盖网络线 序㉔–㊸ 补提交 —— 源码与已部署产物对齐

把这批「已 scp 到 47 / 106 生产并在跑、但一直未入 git」的实现补进版本库
(其中 P2 的全部新源码此前一直是 untracked)。

分棒内容:
- 序㉔ 内容分发块级寻址:src/net/relay/content/{runtime,source}.ts
- 序㉖ 骨干稳定选路(jitter):src/net/relay/{index,network}.ts、src/web/server.ts
- 序㉘ 组密钥加密(GCM 确定性):src/net/relay/content/{runtime,source}.ts
- 序㉛ P1 一键加入 + 分组准入:src/net/relay/{join,registry}.ts、
  src/web/routes/overlay-nodes.ts、scripts/overlay-node-{join,admit}.cjs、
  test/overlay-join.test.mjs
- 序㊵/㊶ 直连打洞 + peer 档:src/net/relay/direct/{candidate,index,punch}.ts、
  scripts/overlay-direct-probe.cjs、test/overlay-direct.test.mjs
- 序㊷/㊸ 观测面:scripts/overlay-probe.cjs、scripts/overlay-failover-drill.cjs

零回归三件套(2026-09-18 16:2x 提交前复跑,全绿):
- npm test                                  201 tests / pass 200 / fail 0 / skipped 1(Node v22.22.2)
- overlay-probe.cjs --table                 28 PASS / 0 SKIP / 0 FAIL
- overlay-failover-drill.cjs --scene all --table  12 PASS / 0 SKIP / 0 FAIL

⛔ 未纳入:_tmp_seq24/、_tmp_seq40/、_中间产物_待清理/(本机临时产物,仍 untracked)
🔴 未实施:D8 云安全组乙-1(待人工在云控制台落地,见
   交接单_覆盖网络直连与P2P_20260918.md §8.11)
This commit is contained in:
admin committed 2026-09-18 16:37:09 +08:00
1 parent 09ce76f3af
commit c452013129
18 files changed
+6604 -78

No files matched your search

+88 -7
View File
@@ -41,8 +41,12 @@
* 3. **`systemctl is-active` 在 inactive 时退出码 = 3** ⇒ `execFileSync` 会抛 ⇒ 首轮**中断在幕1 半路**
* 且把 47 的 relay 留在停用态。凡是**读状态**的远端命令一律 `|| true`。
*
* ## 运行(cwd = 工作区根;⛔ 阈值零硬编码)
* ## 运行(🆕 序㊸:**cwd 不再受限**;⛔ 阈值零硬编码)
* `node "D:/github/dsh_shenxian/scripts/overlay-failover-drill.cjs" [--scene 1|2|3|all]`
* ⚠️ **参数表定位已与 cwd 解耦**(从代码仓根 / 任意目录都可跑):候选目录链 = `--table` > `--dir`
* > `DSHS_OVERLAY_TABLE_DIR` > **注册文件**(缺省 `~/.dshs/overlay-table-dir`)> `cwd` > 脚本目录及上两级。
* 🔴 `--scene trace` / `--scene sample` 的**明细落盘**仍按 `cwd` 写 `_中间产物_待清理/seq9-trace/`
* (那两幕是取证子命令,⛔ 不是本条约的范围)。
*
* ⚠️ **本脚本会真停生产单元**(`dshs-relay`);`finally` 里**一律复原**(失败也复原)。
*/
@@ -51,6 +55,7 @@
const { execFileSync } = require('node:child_process')
const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
/** 参数表文件名(⛔ 不写死日期数字)。 */
@@ -61,6 +66,14 @@ const KEY_ONLY_RE = /^`([A-Z0-9_]+)`$/
/* ─────────── 参数表装载(与 overlay-probe.cjs 同款;⛔ 脚本内无魔数) ─────────── */
/**
* 🆕 **序㊸:参数表定位与 cwd 解耦**(与 `overlay-probe.cjs` **同款**,改动理由见该文件同名注释)。
* 候选目录链 = `--table` > `--dir` > `DSHS_OVERLAY_TABLE_DIR` > **注册文件**
* (`DSHS_OVERLAY_TABLE_REGISTRY`,缺省 `~/.dshs/overlay-table-dir`,一行一个目录、`#` 注释)
* > `cwd` > 脚本自身目录及上两级。
* 🔴 **"恰好 1 个才合法"保留且更严**:任一候选目录 ≥2 份 ⇒ 立即报错;跨目录合计 ≥2 份 ⇒ 也报错。
* ⛔ 一份都没找到 ⇒ 报错并列出**搜过的目录**,⛔ **不取缺省**。
*/
function resolveTablePath(argv) {
const argOf = (name) => {
const i = argv.indexOf(name)
@@ -68,12 +81,78 @@ function resolveTablePath(argv) {
}
const explicit = argOf('--table')
if (explicit !== undefined) return explicit
const dir = argOf('--dir') ?? process.env.DSHS_OVERLAY_TABLE_DIR ?? process.cwd()
const hits = fs.readdirSync(dir).filter((n) => TABLE_RE.test(n))
if (hits.length !== 1) {
throw new Error(`在 ${dir} 下按 /${TABLE_RE.source}/ 找到 ${hits.length} 个参数表(要求恰好 1 个)`)
const registry = readTableRegistry()
const out = []
const push = (d) => {
if (typeof d === 'string' && d.trim() !== '') out.push(path.resolve(d.trim()))
}
return path.join(dir, hits[0])
push(argOf('--dir'))
push(process.env.DSHS_OVERLAY_TABLE_DIR)
for (const d of registry.dirs) push(d)
push(process.cwd())
push(__dirname)
push(path.join(__dirname, '..'))
push(path.join(__dirname, '..', '..'))
const seen = new Set()
const dirs = []
for (const d of out) {
if (seen.has(d)) continue
seen.add(d)
dirs.push(d)
}
const hits = []
const scanned = []
for (const dir of dirs) {
if (!fs.existsSync(dir)) continue
let names
try {
names = fs.readdirSync(dir).filter((n) => TABLE_RE.test(n))
} catch (err) {
continue // 目录存在但读不了(权限等)⇒ 不算"扫过",也不冒充"没有"
}
scanned.push(dir)
if (names.length > 1) {
throw new Error(
`在 ${dir} 下按 /${TABLE_RE.source}/ 找到 ${names.length} 个参数表(要求恰好 1 个):${names.join(' , ')}`,
)
}
if (names.length === 1) hits.push(path.join(dir, names[0]))
}
if (hits.length === 0) {
throw new Error(
`在 ${scanned.length} 个候选目录下按 /${TABLE_RE.source}/ **一份都没找到**(要求恰好 1 个)\n` +
` 搜过的目录:\n${scanned.map((d) => ` · ${d}`).join('\n')}\n` +
` 注册文件:${registry.file}${registry.readError === null ? `(${registry.dirs.length} 条)` : `(读取失败:${registry.readError})`}\n` +
` ⇒ 处置(三选一):① \`--table <file>\` 直接指定 ② 把参数表所在目录(一行一个)写进上面的注册文件 ③ 在参数表所在目录下运行`,
)
}
if (hits.length > 1) {
throw new Error(
`跨候选目录共找到 ${hits.length} 个参数表(要求恰好 1 个,⛔ 不猜哪一份是对的):\n` +
`${hits.map((h) => ` · ${h}`).join('\n')}`,
)
}
return hits[0]
}
/** 读**注册文件**(一行一个目录)。⛔ 读不到就返回**读取失败原因**(⛔ 不冒充"没有")。 */
function readTableRegistry() {
const envFile = process.env.DSHS_OVERLAY_TABLE_REGISTRY
const file =
typeof envFile === 'string' && envFile.trim() !== ''
? envFile.trim()
: path.join(os.homedir(), '.dshs', 'overlay-table-dir')
let text
try {
text = fs.readFileSync(file, 'utf8')
} catch (err) {
return { file, dirs: [], readError: (err && err.code) || String(err) }
}
const dirs = text
.split(/\r?\n/)
.map((l) => l.replace(/#.*$/, '').trim())
.filter((l) => l !== '')
return { file, dirs, readError: null }
}
const cleanValue = (raw) => String(raw).replace(/[*`]/g, '').trim()
@@ -173,7 +252,9 @@ async function main() {
const sshTo = r.num('SSH_TIMEOUT_MS')
const h47 = r.need('SSH_TARGET_47')
const h106 = r.need('SSH_TARGET_106')
const relayUnit = r.need('DRILL_RELAY_UNIT')
// ⚠️ 序㊳ 改名:`DRILL_RELAY_UNIT` → `RELAY_UNIT_NAME`(它记的是"两台机的 relay 单元名"这一
// **事实**、并非演练专用坐标 —— 探针 `OBS-08` 也读它 ⇒ 原 `DRILL_` 前缀与用途不相称)。
const relayUnit = r.need('RELAY_UNIT_NAME')
const managerUnit = r.need('DRILL_MANAGER_UNIT')
const deadlineMs = r.num('RELAY_FAILOVER_DEADLINE_MS')
const pollMs = r.num('DRILL_POLL_MS')