覆盖网络线 S0+S1 落地:可达性单一入口 + 会合地址出 env

S0(本机):新增 src/net/reachability.ts(Reachability 描述 + agentBaseUrlOf 唯一取值入口)、src/net/rendezvous.ts、docs/architecture.md(四层划分 + 依赖方向 + R1-R4 判据)、scripts/check-layering.mjs + layering-baseline.json、test/reachability.test.mjs;src/supervisor/remote-spawner.ts 与 src/web/server.ts 改为统一走 agentBaseUrlOf(),agentUrl 降级为可选旧字段(向后兼容)。

S1(本机 + 106):新增 DSHS_RENDEZVOUS_URL 出 env(src/config.ts 解析 clusterRendezvousUrl,优先级 overrides > 新变量 > DSHS_TUNNEL_TARGET 兜底 > 空),src/worker/tunnel.ts 新增 normalizeTunnelTarget()、src/worker/agent.ts 改读新配置 ⇒ 双路径并存、可零代码回滚(删掉新 env 即走旧路径)。106 已上线,健康检查 tunnel.ready=true。

package.json 增加 check:layering 脚本并接入 verify;README 登记分层文档。

验收:npm run build 通过;npm test 44/44;check:layering 无新增违规(基线 5 条)。
This commit is contained in:
admin committed 2026-09-16 15:02:52 +08:00
1 parent 08219c99da
commit 640813e84e
13 files changed
+660 -18

No files matched your search

+7 -2
View File
@@ -28,7 +28,7 @@ import { userRoot } from '../fs/workspace.js'
import { isUserFsErrorCode, UserFsError } from '../fs/user-fs.js'
import { hashUid } from '../isolation.js'
import { LocalSpawner } from '../supervisor/orchestrator.js'
import { SshTunnel } from './tunnel.js'
import { normalizeTunnelTarget, SshTunnel } from './tunnel.js'
import type { Instance } from '../supervisor/spawner.js'
/** 绑定的头部名(Manager/agent 双方约定)。 */
@@ -150,8 +150,13 @@ export function buildWorkerAgent(
/**
* 反向隧道(可选)。静态转发 = **agent 自身端口** + `DSHS_TUNNEL_STATIC_PORTS`(如控制面 PG);
* 实例端口在 launch/stop 时动态加减,并在 `/healthz`(Manager 的心跳)里**对账自愈**。
*
* S1:会合地址**优先取 config**(`DSHS_RENDEZVOUS_URL` → 兜底 `DSHS_TUNNEL_TARGET`,在
* `config.ts` 里单点解析);显式 `options.tunnelTarget` 仍是最优先(测试/嵌入用)。
*/
const tunnelTarget = options.tunnelTarget ?? process.env.DSHS_TUNNEL_TARGET ?? ''
const tunnelTarget = normalizeTunnelTarget(
options.tunnelTarget ?? config.clusterRendezvousUrl ?? '',
)
const staticPorts = [
options.port,
...(process.env.DSHS_TUNNEL_STATIC_PORTS ?? '')
+31 -4
View File
@@ -13,7 +13,8 @@
* 在**同一条长连接**上加/减转发,不必为每个端口重开连接。
*
* ⚠️ 定位:这是**演练级**传输(生产长期方案见设计 §2.3:受控网段白名单或隧道服务)。
* ⚠️ 默认**关闭**:只有设了 `DSHS_TUNNEL_TARGET` 才启用 ⇒ 对同机/单机形态零影响。
* ⚠️ 默认**关闭**:只有设了 `DSHS_RENDEZVOUS_URL` 才启用 ⇒ 对同机/单机形态零影响。
* 旧变量 `DSHS_TUNNEL_TARGET` 作为**兜底**保留(新变量未设时才用它)。
*
* @module dshs/worker/tunnel
*/
@@ -23,8 +24,28 @@ import { promisify } from 'node:util'
const run = promisify(execFile)
/**
* 归一化会合地址(覆盖网络 S1)。
*
* 为什么要它:会合点从"硬写在 env 里的 `user@host:port`"升格为**带 scheme 的 URL**
* (`ssh://[email protected]:32022`)—— 以后换传输协议(中继/隧道服务)只改 scheme。
* 而本类其余代码如下按 `user@host:port` 切分 ⇒ 必须在**入口处**剥掉 scheme:
* 否则 `'ssh://root@h:32022'.split(':')` 会切成三截,把 `ssh` 当成主机名。
*
* 两种写法都接受(**单点归一,调用方不必判断**):
* · `ssh://[email protected]:32022` → `[email protected]:32022`
* · `[email protected]:32022` → 原样(兼容历史 env `DSHS_TUNNEL_TARGET`)
*/
export function normalizeTunnelTarget(raw: string): string {
const trimmed = raw.trim()
if (trimmed === '') return ''
return trimmed
.replace(/^[a-z][a-z0-9+.-]*:\/\//i, '') // 剥 scheme(ssh:// / dshs+ssh:// …)
.replace(/\/+$/, '') // 去掉可能的尾斜杠
}
export interface TunnelOptions {
/** 拨入目标,形如 `[email protected]:32022`。 */
/** 拨入目标,形如 `[email protected]:32022`(带 `ssh://` 前缀也接受,见 {@link normalizeTunnelTarget})。 */
target: string
/** 私钥路径(建议专用、且在 Manager 侧用 `restrict,port-forwarding` 限权)。 */
identity: string
@@ -52,11 +73,17 @@ export class SshTunnel {
constructor(options: TunnelOptions) {
// `user@host:port` 里的 port 是 **SSH 端口**(不是转发的端口)—— 47 上用 32022,
// 必须经 `-p` 传,否则会去连 22 而失败。
const [hostPart, portPart] = options.target.split(':')
// S1:先归一化(剥 `ssh://` scheme),再按 `user@host:port` 切分。
const normalized = normalizeTunnelTarget(options.target)
const [hostPart, portPart] = normalized.split(':')
if (portPart !== undefined && !/^\d+$/.test(portPart.trim())) {
// 宁可起不来也不要"静默连到 22 端口":地址写错必须吵。
throw new Error(`非法的会合地址(端口必须是数字):${options.target}`)
}
this.hostPart = hostPart
this.portPart = portPart === undefined ? undefined : Number(portPart)
this.opts = {
target: options.target,
target: normalized,
identity: options.identity,
controlPath: options.controlPath,
staticPorts: options.staticPorts ?? [],