Files
dsh_ai1net_server/归档/交接单-20260924-归档/交接单_覆盖网络落地执行_20260916.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

256 lines
23 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.
# 交接单 · 覆盖网络落地执行(**v2** · 2026-09-16 15:2x 复核后重写)
> **给执行会话**:按本单开工,**不读规划会话的上下文**。
> **v2 改了什么**:新增 **§0 复核结论 + 6 条勘误**(其中 **A/B 为实测**,**改变了 S3 的做法**);更正部署面(v1 只写 106,**漏了 47**);步骤改为 **P1 对齐 47 → P2=S2 → P3=S3(改写) → P4=S4**。
> **方案来源** = 工作区根 `会合中继拆分_取证与改造方案_20260916.md` —— 必读 **§4 + §8 + §9**。
> **用户的验收要求(原话口径)**:改造后 **47 + 106 + 本机都能跑起来;现有功能与多用户机制正常**。
> 分层规则见代码仓 `docs/architecture.md`(新模块必须守;`npm run check:layering` 会在 `verify` 里拦你)。
---
## 0. 本轮复核结论(2026-09-16 15:2x · 全部只读实测)
### 0.1 已核实基线(部署前先照这个对一遍)
| 项 | 实测值 |
|---|---|
| 本机 | HEAD `640813e`,工作区干净;`lib/net/{reachability,rendezvous}.js` 已 build(09-16 14:34) |
| 47(Manager) | `dshs` / `dshs-worker` / `dshs-pg` **全 active**;Manager 跑 `/opt/dshs/lib/cli.js`(`EnvironmentFile=/etc/dshs.env` + drop-in `dshs.service.d/cluster.conf`) |
| **47 代码版本** | 🔴 **落后于 S0** —— `/opt/dshs/lib/` **无 `net/`**;`config.js` 指纹 `91fc3756` ≠ 本机 `1c3601f7`(lib mtime 09-16 00:39) |
| 106(Worker) | `dshs-worker` **active**;`/healthz` ⇒ `{"ok":true,"hostId":"w-106","instances":0,"tunnel":{"ready":true,"ports":[19000]}}` |
| 106 代码版本 | ✅ S1 三件已上:`config.js` `1c3601f7`、`worker/tunnel.js` `5f2649b4` == 本机;`lib/net/` 无(**无需**,那是 Manager 侧文件) |
| `dsh_hosts` | `w-106` → `http://127.0.0.1:19000`(cap 2048)|`w-47` → `http://127.0.0.1:19100`(cap 1024);**共 7 列,无 `via`** |
| 用户 / 实例 | `admin@w-47`、`dbg2mx897@w-106`、`guest@w-106`、`pocuimwkrr@w-106`;⚠️ **四条实例全 `stopped`** ⇒ 验收要**主动 launch** |
| 47 sshd | `gatewayports no`|`allowtcpforwarding yes`|`permitopen any`|OpenSSH **8.0p1**(106 = 9.3p2);`lo` = `127.0.0.1/8` |
| 47 端口面 | `0.0.0.0:32022` sshd(会合)|`127.0.0.1:19000` sshd(106 隧道落点)|`127.0.0.1:19100` node(w-47 agent)|`127.0.0.1:15432` postgres |
### 0.2 勘误(6 条 · A/B 为实测,🔴 照原方案字面做会白做或出事)
**A 🔴 `DSHS_RELAY_LOCAL_NAMESPACE`(回环别名)实测无效 ⇒ S3 换做法。**
实测:自 106 发起 `ssh -R 127.0.0.2:19999:127.0.0.1:19000 -o ExitOnForwardFailure=yes` ⇒ 47 上落点实际是 **`127.0.0.1:19999`**(另含 `[::1]:19999`),**`127.0.0.2` 被 sshd 静默改写**,ssh 侧**零报错**(`ExitOnForwardFailure` 未触发、日志为空;测试后已清理)。
根因:47 `sshd -T` ⇒ **`gatewayports no`**(默认)⇒ sshd 强制把远端转发绑到回环,**客户端指定的绑定地址被丢弃**。
⇒ **S3 改为「实例端口区间隔离」**(§3 P3):同样打掉根因,且**不改 sshd、不改协议、不需要 `portMap`**。
将来若确需回环别名,只能开 `GatewayPorts clientspecified` —— 且**只能开在新建的 relay sshd 上**,⛔ **绝不动主 sshd 32022**(那等于让持隧道密钥者可绑 `0.0.0.0`,命中 **R5**)。
**B 🔴 端口撞号是**真 bug**,不是"管道美化" —— 这才是 S3 的真实必要性。**
`findFreePort()`(`src/supervisor/spawn.ts:53`,唯一调用点 `src/supervisor/orchestrator.ts:595`)在 **worker 自己那台机器**上随机取端口 ⇒ 两台 worker 取到**同号**是常态;而 `tunnel.forward()` 的返回值在**两处都被忽略**(`src/worker/agent.ts:303`、`:205`)⇒ 撞号时 `-R` 失败但**静默**;Manager 仍按 `127.0.0.1:<port>` 拨 ⇒ **打到另一个用户的实例**。w-47 的本地实例与 w-106 的隧道落点**共享 47 的 `127.0.0.1` 端口空间** ⇒ **只用两台机器就能触发**。
**C 🔴 部署面:v1 只写了 106,漏了 47。**
S0 的 `src/web/server.ts`、`src/supervisor/remote-spawner.ts`、`src/net/*` 跑在 **Manager(47)**;S2 的 `hostsProvider` 也在那儿。
⇒ **v1 说的"S1 已上线"只覆盖 Worker 侧;47 至今是 S0 之前的版本**。执行时必须**双侧对齐**(§3 P1)。
**D ⚠️ `verify-cluster-cross.mjs` 不能照抄跑,且**有生产副作用**。**
① 默认 `MANAGER=http://127.0.0.1:13080` = **演练端口**(现役是 **3080**);`AGENT_TOKEN` 默认 `cross-machine-token`(现役 `dshs-worker-7f3a91c05e`);还需 `ADMIN_PW`(真实管理员口令)。
② 它会 **幂等注册 `w-106` 并把容量改成 4096**、**新建 `crossuser<ts>` 用户并审批**、**在 106 上拉真实例**,可选**注册 `w-106b` 并迁移实例** ⇒ **跑完必须清理**(§5.2)。
③ 脚本头部注释"控制面 PG 也在 106"**已过时** —— PG 现在在 47(`127.0.0.1:15432`)。
**E ⚠️ S4 有方案缺口:中继一旦不在 Manager 主机上,"Manager 拨落点"就断。**
隧道落点在**接收方那台机器的 `127.0.0.1`**。中继换到别的机器 ⇒ Manager 拨不到 ⇒ 需要**额外一跳**(Manager 侧向中继建出向隧道 `ssh -L`,或中继侧把落点暴露到非回环地址 + nft 收窄)。
⇒ 本轮 S4 **只做同机 relay**(47,独立单元 / 端口 / 密钥 / 账号);异地 relay 留待定这一跳。
⛔ **不要用方案 §4 S4 的"会合指向第二个中继实例"当通过判据** —— 那要先解决本缺口;本轮改判据(见 §3 P4)。
**F ⚠️ S2 减负:不要加 `address` 列。**
`endpoint` 本身就是"要拨的地址",再加 `address` = **同义双真相**(回填后必然逐字相等)。⇒ **只加 `via`**(`NOT NULL DEFAULT 'manager-ssh'`),回填用一条显式 `UPDATE`。
> 方案 §8 的 ①②③ **仍然有效**,其中 ① 本轮**再次确认**:`proxy.ts:109` 是**发给上游 dsh 的 `Host` 头**(dsh 的 `/api` 信任栅栏要 loopback),⛔ **绝不能改**;真正的连接目标在 **`proxy.ts:136-137`**(`host/port` 取自 `endpointFor`)。
---
## 1. 只读前置(必做,全部只读)
1. 抢锁:`bash dsh-server-docs/scripts/handoff-guard.sh --claim-exec "<会话名>"`
2. 读方案 **§4 + §8 + §9**;读 `docs/architecture.md §5`(现存 5 条违规基线,**别新增**)
3. 照 **§0.1 表**逐项复核基线(本机 HEAD|47 的 `/etc/dshs.env` + `dshs.service.d/cluster.conf`|106 的 `/etc/dshs-worker.env`|`dsh_hosts` 现值|47 端口面)
4. 远程入口:`ssh [email protected]`(22)|`ssh test106`(**必须 `-i ~/.ssh/id_ed25519_test106`**)
---
## 2. 范围
**做**:P1 → P2 → P3(P4 视 P1–P3 结果决定是否本轮)
**不做**:⛔ 不换传输协议(WireGuard / TURN 属远期)|⛔ 不动 `proxy.ts:109`|⛔ 不动权威状态(归属 / 租约 / 骨干资格**仍只在 Manager 写**)|⛔ 不做 `DSHS_RELAY_LOCAL_NAMESPACE`(勘误 A)|⛔ 不加 `portMap`(不再改"不同号")
---
## 3. 步骤(每步独立可验、独立可回滚)
### P1 · 对齐 47 到 HEAD `640813e`(把 S0+S1 真正补上 Manager 侧)
**为什么先做这一步**:47 现在落后一代,直接上 S2 就是"跳版部署",出问题无从二分。这一步本身**零行为变化**,风险最低。
1. **本机门禁**(Node **22**,24 会因 better-sqlite3 ABI 全红):
`npm run build` → `npm test`(须 **44 项 / 0 失败**)→ `npm run verify` → `npm run check:layering`(现存违规 **5 条**,**不得新增**)
2. **备份 47**(回滚点必须存在):
`ssh [email protected] 'tar czf /opt/dsh/backups/lib-pre-S0-$(date +%Y%m%d-%H%M).tgz -C /opt/dshs lib'`
3. **只传这几件**(= `git show --name-only 640813e` 的 `src/` 清单 → `lib/` 产物;同名 `.d.ts` 一起传;`lib/net/` 需先 `mkdir -p`):
```
lib/config.js lib/net/reachability.js lib/net/rendezvous.js
lib/supervisor/remote-spawner.js lib/web/server.js
lib/worker/agent.js lib/worker/tunnel.js
```
⛔ **别整包覆盖 `lib/`** —— 会把别人直接改在服务器上的内容回退。
4. **逐文件 md5 对账**(47 上算完与本机比,必须逐条相等;`.d.ts` 也核)
5. `systemctl restart dshs`(**会短暂中断门户**;本项目为开发环境服务器 ⇒ 直接做,动手前一句话说明在动什么)
- **验收**:① 47 三单元 active ② 门户 `curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3080/` = 200 ③ 106 `/healthz` `tunnel.ready=true` ④ **`admin`(w-47)与 `guest`(w-106)各自的实例页都能打开 + 工作区文件能读能写** ⑤ `ls /opt/dshs/lib/net/` 有两个文件且 md5 == 本机
- **回滚**:`systemctl stop dshs` → 用步骤 2 的 tgz 还原 `lib` → `systemctl start dshs`
### P2 · S2:`dsh_hosts` 增 `via` 列(打掉 C3)
- **顺序(不能颠倒)**:**先加列(带默认值)→ 再改代码 → 最后回填** —— 任一步中断都不崩
- **加列**:`src/db/schema.ts` 的 `MIGRATIONS` 追加 **v8**,**双方言都要写**:
- sqlite:`ALTER TABLE dsh_hosts ADD COLUMN via TEXT NOT NULL DEFAULT 'manager-ssh';`
- pg:同款 SQL(语法一致)
- **改代码(6 处)**:
| 文件 | 改什么 |
|---|---|
| `src/db/pg.ts:56`、`src/db/repo.ts:598` | `HOST_COLS` 末尾加 `, via` |
| `src/db/pg.ts:605-646`、`src/db/repo.ts:603-625` | upsert 带上 `via` |
| `src/db/types.ts` | `DshHost` / `UpsertDshHostInput` 加 `via?: string`(缺省语义 = `manager-ssh`) |
| `src/web/routes/admin.ts:181-203` | 注册口接受 `via`,缺省 `manager-ssh` |
| `src/web/server.ts:256-266` | `hostsProvider`:**读 `via` → `RendezvousRegistry.get(via)` → `resolve(id)` → 塞 `ClusterHost.reachability`**;`via` 未知/未设 ⇒ 回退 `parseReachability(id, endpoint)` |
| `src/net/rendezvous.ts` | `LocalRendezvous` / `ManagerSshRendezvous` 的 `addressOf` 由调用方从 `db.listDshHosts()` 建 map 传入(`resolve` 是 async,`hostsProvider` 本来就是 async ⇒ 无需改签名) |
- **回填(一条显式 SQL,幂等)**:
`UPDATE dsh_hosts SET via='local' WHERE id='w-47';` —— 判据:**Manager 与 worker 同机 ⇒ 直连 ⇒ `local`**;`w-106` 保持默认 `manager-ssh`(47 上 sshd 的隧道落点)。⛔ **别一刀切**(方案 §8 勘误 ②)。
- **部署**:**只改 47**(S2 全是 Manager 侧代码,106 本轮无需改):传 `lib/{config,net/*,db/*,web/*}` 对账 → `systemctl restart dshs`
- **验收**:
① 单测扩展 `test/reachability.test.mjs` ⇒ 断言「迁移前 `agentUrl`」== 「迁移后 `resolve()` 结果」**逐条相等**,两条真实行**写死**(`w-47`→`local`、`w-106`→`manager-ssh`)
② 真机 `/api/admin/hosts` 返回带 `via`,且**不含 `agentToken`**
③ P1 的四条业务验收重跑一遍(多用户不受影响)
- **回滚**:列有默认值 ⇒ 旧代码读 `endpoint` 完全不受影响,**列可留着不删**
### P3 · S3(**改写**):实例端口区间隔离
- **目标**:消除勘误 B 那条真 bug —— 跨 worker 同号 ⇒ 47 的 `127.0.0.1` 端口空间撞号 ⇒ 静默不转发 ⇒ 拨到**别人的实例**
- **改哪些文件**:
| 文件 | 改什么 |
|---|---|
| `src/supervisor/spawn.ts:53` | `findFreePort()` → 增可选取址(区间内挑端口;区间为空 ⇒ 保持旧 `listen(0)` 行为) |
| `src/supervisor/orchestrator.ts:595` | 取端口改走区间(读 config) |
| `src/config.ts` | 新增 `DSHS_INSTANCE_PORT_BASE`(默认 `0` = 旧行为)/ `DSHS_INSTANCE_PORT_SPAN`(默认 `1000`) |
- ⚠️ **`src/worker/agent.ts:352` 不用改**(`instanceHost` 仍 `127.0.0.1`)—— 这就是本方案相对原设计的简化点;也**因此不需要 `portMap`**
- **env 分配(必须互不重叠)**:`w-47` → base **`42000`**|`w-106` → base **`43000`**|span 各 `1000`
- **部署**:47 —— `lib/{config,supervisor/*,worker/agent.js}` + drop-in 加两行 env → `systemctl restart dshs dshs-worker`;106 —— `lib/{config,supervisor/*}` + `/etc/dshs-worker.env` 加两行 env → `systemctl restart dshs-worker`
- **验收(三条端到端,缺一不可)**:
① 跨机实例页能打开 ② 跨机工作区文件能读能写 ③ **同一时刻 47 与 106 各起一个实例** ⇒ `ss -lntp` 上 47 的 `127.0.0.1`:`w-47` 实例端口 ∈ `42000+`、`w-106` 实例端口 ∈ `43000+`,且**无重叠**
- **回滚**:删两个 env ⇒ 回 `listen(0)` 随机(**零代码回滚**)
- **不做**:`DSHS_RELAY_LOCAL_NAMESPACE`(勘误 A)|`portMap`(不再改"不同号")
### P4 · S4(本轮收尾,可延后):中继独立成单元
- **做什么**:47 上新建 **`dshs-relay.service`** = **独立 sshd 实例**(独立端口如 `32023` / 独立 host key / 独立 `authorized_keys` / 独立系统账号 `dshsrelay`;**`gatewayports no` 保持不动**)
- **worker 侧只改一行 env**:`DSHS_RENDEZVOUS_URL=ssh://root@47.77.182.89:32023`
- **`via` 补一个实现**:新增 `RelayRendezvous`(id 如 `relay:47-32023`),`hostsProvider` 经注册表选到它;`w-106` 的 `via` 由 `manager-ssh` 改指它
- **验收(三条,**已按勘误 E 改判据**)**:
① 单中继下全链路通(实例面 + 文件面)
② **停掉中继** ⇒ 同机形态(`w-47`)实例仍可用、跨机代理失败但**不崩**
③ ⛔ 不判"第二个中继实例";改判:**把会合地址换回 `32022` 后,106 能重新注册并被正确解析**(本轮能验的"会合可换"就到这里)
- **回滚**:`DSHS_RENDEZVOUS_URL` 指回 `32022`(或删掉它,走 `DSHS_TUNNEL_TARGET` 兜底)+ `systemctl disable --now dshs-relay`
---
## 4. 本机(**客户端类型**节点,不是 worker)
要验的是可行性评估**缺口 1**(全案**唯一无证据**的技术点):
1. 本机起实例(**`soft` 模式** —— Windows 无 bwrap/systemd)⇒ 实例页 200
2. 实例内跑一次 bash 工具 ⇒ 验 dsh 在 Windows 的**沙箱后端行为**
3. 本机 ↔ 47 / 106 的**真实网络画像**(NAT 类型 / 打洞可行性 / RTT + **jitter**)⇒ 直接填"最该先测三项"里的两项
⚠️ ⛔ **不得把本机接进现有生产链路**(要独立形态、独立开关);本机是**唯一开发机** ⇒ 用最小配额。
---
## 5. 全局验收(缺一 = 没做完)
### 5.1 通过项
1. **多用户机制**:`admin`(锚 `w-47`)、`guest`(锚 `w-106`)**各自实例页 + 工作区文件都正常**(用户硬要求)—— ⚠️ 当前四条实例**全 `stopped`**,要主动 launch
2. `verify-cluster-cross.mjs` 全绿 —— **必须按勘误 D 的参数跑**:
`MANAGER=http://127.0.0.1:3080 AGENT_TOKEN=dshs-worker-7f3a91c05e ADMIN_PW=<真实管理员口令> node scripts/verify-cluster-cross.mjs`(`AGENT2` 留空 ⇒ 跳过第二台 worker 段)
3. `npm test` + `npm run verify` 全绿,且 `npm run check:layering` **无新增违规**
4. 47:`dshs` / `dshs-worker` / `dshs-pg` active;106:`dshs-worker` active
5. 每步的**回滚点**写清并**实测过**(至少回滚命令能跑)
### 5.2 跑完必须清理(勘误 D 的副作用)
- 删掉 `crossuser*` 用户;把 `dsh_hosts` 里 `w-106` 的 `capacity_mb` 改回 **2048**
- 若跑过 `AGENT2` 段:删掉 `w-106b` 登记,并把被迁移的实例改回 `w-106`
- 自建的测试实例:`stop` 掉,别留在生产
## 6. 回报格式
**判定 → 改了什么(文件 + 部署到哪台哪个路径 + md5 对账结果)→ 验收实测输出 → 未做 / 风险 → 锁状态**
(截图或命令原文均可;⛔ 不许写"应该没问题")
---
## 7. 开工前已知的坑(省你踩一遍)
| 坑 | 事实 |
|---|---|
| 106 连不上 | 必须 `-i ~/.ssh/id_ed25519_test106`;其它 key 一律 `Permission denied` |
| 47 的 cluster 配置 | 在 **drop-in** `dshs.service.d/cluster.conf`,**不在** `/etc/dshs.env` |
| 47 的 `/opt/dshs` 是 git 仓库 | ⚠️ **但不是部署来源** —— 部署 = 本机 build 后 scp `lib/` 文件;47 上 `git log` 只有初始提交 + 一堆 `M` ⇒ ⛔ 别在 47 上 `git pull`。`git -C /opt/dshs` 会报 `dubious ownership`,要加 `-c safe.directory=/opt/dshs` |
| 47 的 PG 查询 | **只能 TCP**:`PGPASSWORD=dshs_cluster_2026 psql -h 127.0.0.1 -p 15432 -U dshs -d dshs`;`sudo -u postgres psql` **不通**(unix socket 未启用) |
| `C4` 其实不成立 | 106/47 的 worker env **都没有** `DSHS_TUNNEL_STATIC_PORTS` ⇒ 隧道**不含控制面 PG** ⇒ 回滚面比方案预估小 |
| 本机 git | `refs/remotes/**` 写不进去 ⇒ `git fetch` **静默失败** ⇒ 分叉判定必须 `git ls-remote` 取裸 sha |
| 本机 bash | PATH 被 shim 重置 ⇒ 先 `export PATH=<PortableGit>/usr/bin:<PortableGit>/bin:/c/Windows/System32:/c/Windows:$PATH`;⛔ 进程替换 `diff <(a) <(b)` **不可用** |
| 本机跑测试 | 必须 **Node 22**(Node 24 会因 better-sqlite3 ABI 全红) |
---
# 8. 执行记录(2026-09-16 15:5x–16:2x · **P1/P2/P3 已完成并验收**)
> 执行会话:`覆盖网络-落地执行`。**P4 未做** —— 理由见 §8.4(命中 **R5**,需授权)。
## 8.1 ✅ P1 · 47 对齐到 `640813e`(Manager 侧补上 S0+S1)
- **前置取证**:全量产物指纹对账 ⇒ 47 与本地 HEAD 的差异**恰好只有 S0+S1 那 7 个文件**,无其他漂移。
⚠️ 踩坑记录:本机 `md5sum` 输出 `hash *path`(二进制模式)、远端是 `hash path`(文本模式)⇒ 直接 `diff` 会得到"**59 个文件全不同**"的**假象**。判据必须先归一化路径字段。
- **备份**:`/opt/dsh/backups/lib-pre-S0-20260916-1555.tgz`
- **部署**:7 个 `.js` + 同名 `.d.ts`/`.js.map`(共 21 件)⇒ 47 `/opt/dshs/lib/`,属主/权限 `197108:197121 644`(与现存一致)
- **验收实测**:7 件 md5 **逐条相等**|`dshs/dshs-worker/dshs-pg` active|门户 200|`w-47` 与 `w-106` agent 均可达(106 经隧道,`tunnel.ready=true`)|日志 0 错误
- **业务验收(多用户,R4 临时会话)**:
· `admin`(w-47)实例 `running` + 实例页 **200**
· `guest`(w-106)launch 成功 + **跨机实例页 200** + `/api/desktop/tree` 通 + `mkdir` 落盘已核实**写在 106 的盘上**
- **痕迹清理**:临时会话 `DELETE 2`⇒余 0|`/tmp` 临时件已删|guest 实例还原为 `stopped`
## 8.2 ✅ P2 · `dsh_hosts` 增 `via`(打掉 C3)
- **改 7 处**:`db/schema.ts`(**v8 迁移**,双方言同写)|`db/types.ts`(`DshHost.via` + `UpsertDshHostInput.via?` + `toDshHost`)|`db/pg.ts` / `db/repo.ts`(`HOST_COLS` + upsert)|`web/routes/admin.ts`(注册口接受 `via`)|`web/server.ts`(`hostsProvider`:**读 `via` → `RendezvousRegistry` → `resolve()` → `ClusterHost.reachability`**)|`test/reachability.test.mjs`(+2 条判据)
- **两处按勘误 F 落地**:**不加 `address` 列**(`endpoint` 本身就是要拨的地址,加列=同义双真相);**`via` 省略时不覆盖已有值**(`COALESCE(?, dsh_hosts.via)`)—— 否则一次不带 `via` 的 join 会把回填好的 `local` 冲回默认
- **回填(显式、幂等)**:`UPDATE dsh_hosts SET via='local' WHERE id='w-47';` ⇒ 结果 `w-47=local` / `w-106=manager-ssh` ✅
- **门禁**:`build` rc=0|单测 **46 项 / 0 失败**|`check:layering` **无新增违规**|迁移后 `schema_migrations` = 1–8,`dsh_hosts` 列含 `via`(默认 `manager-ssh`)
- **验收实测**:`GET /api/admin/hosts` ⇒ `"via":"manager-ssh"` / `"via":"local"`,**且不含 `agentToken`**(只有 `hasToken`)|两条路径实例页均 200(w-47 同机 / w-106 跨机)|日志 0 错误
- **补做的一处**:v1 交接单只要求"API 返回带 via",实测发现 **GET 列表原本不下发 `via`** ⇒ 已补(否则运维看不见 = 加列白加)
## 8.3 ✅ P3(改写版)· 实例端口区间隔离
- **按勘误 A 执行**:⛔ 未做 `DSHS_RELAY_LOCAL_NAMESPACE`(实测被 sshd 静默改写)|⛔ 未加 `portMap`
- **区间选定**(避开 OS 临时端口段 `32768-60999`):`w-47` = **20000**, span 1000|`w-106` = **21000**, span 1000
- **改 4 处**:`config.ts`(`instancePortBase/Span` + `toPortNumber` 防 NaN)|`supervisor/spawn.ts`(`findFreePortInRange` + `findInstancePort`)|`supervisor/orchestrator.ts`(取端口改走区间)|新增 `test/instance-port.test.mjs`(5 条,已挂进 `npm test`/`verify`)
- **部署**:**两台**(47 `/opt/dshs`、106 `/opt/dshs-cluster`)各 3 件,md5 **逐条相等**;env 加在各自 `/etc/dshs-worker.env`;备份 `lib-pre-S3-*.tgz` + `dshs-worker.env.bak-*`
- **门禁**:`build` rc=0|单测 **51 项 / 0 失败**|`check:layering` 无新增违规
- **验收实测(缺一不可的三条全过)**:
① **两台各起一个实例** ⇒ `w-47` 落 **20000**、`w-106` 落 **21000**
② 47 上 `127.0.0.1:20000`(node 直连) 与 `127.0.0.1:21000`(sshd 隧道落点) **互不重叠** ⇒ 原先的撞号根因消除
③ 实例页 `admin`(同机) **200** + `guest`(跨机) **200**,跨机文件面通
- **状态还原**:`guest` 回 `stopped`(与开工前一致);**`admin` 实例保持运行**(与开工前一致)|临时会话已删|40 端口重启后旧端口(42185/37523/35765)**全部释放**
## 8.4 ⏳ P4 未做 —— 命中 **R5**,需要一句授权
**做什么**:47 上新建独立 `dshs-relay.service`(独立 sshd、独立端口 **32023**、独立 host key、独立 `authorized_keys`、非 root 账号 `dshsrelay`),106 的 `DSHS_RENDEZVOUS_URL` 指过去。
**为什么停在这里**:它要在**公网面新开一个 SSH 监听口(32023)** —— 属 **R5「权限只准收窄」**的反面(新暴露面),必须先知会。其余三项(P1–P3)都是"收窄/修复"性质,故直接做了。
**P4 的收益与代价**(供判断):
- 优点:与宝塔面板共用的主 sshd `32022` 解耦(面板改配置不再波及覆盖网络)|隧道密钥从 `root` 的 `authorized_keys` 挪到独立文件与非 root 账号|是"中继可多实例/可换机"的前置
- 缺点:**新增一个公网 SSH 入口**(尽管只允许密钥 + `restrict,port-forwarding`)|受**勘误 E** 限制,异地中继仍差"一跳"未设计 ⇒ 本轮只能验同机形态
**回滚**:`DSHS_RENDEZVOUS_URL` 指回 `ssh://[email protected]:32022` + `systemctl disable --now dshs-relay`。
## 8.5 本轮顺带确认的两件事(未改,仅记录)
1. **控制面已在 PG 上**:`DSHS_DEPLOY_MODE=cluster` + `DSHS_DB_URL=postgres://…:15432/dshs`;dshs 进程**未打开任何 `dshs.db`**(该文件 mtime 停在 09-15 17:51)= 纯回滚副本。⇒ **不存在"需要改造为 PG"这件事**。
2. **隧道密钥已受约束**:47 的 `authorized_keys` 里 `dshs-tunnel-106to47` 那行带 **`restrict,port-forwarding`** ⇒ 该密钥**只能做端口转发、拿不到 shell**(另两把是部署键与个人键,无约束属预期)。
⚠️ **`dsh_instances.status` 不反映实时状态**:实测 admin 的实例在跑,DB 里却是 `stopped`。运行态以 **worker agent 的 `/status/:userId`** 为准(Worker 按设计不写控制面数据)—— 排查时别拿这张表的 `status` 当事实。