覆盖网络线 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

+22 -8
View File
@@ -18,14 +18,26 @@
* @module dshs/supervisor/remote-spawner
*/
import { randomUUID } from 'node:crypto'
import { agentBaseUrlOf, type Reachability } from '../net/reachability.js'
import { AGENT_TOKEN_HEADER } from '../worker/agent.js'
import type { Endpoint, Instance, Spawner, UserStatus } from './spawner.js'
/** 一台 worker 的接入信息。 */
/**
* 一台 worker 的接入信息。
*
* ⚠️ `agentUrl` 与 `reachability` **至少要有一个** —— 取址一律走 `agentBaseUrlOf()`
* (唯一入口,别在调用点自己拼字符串):
* · `reachability` = S0 引入的**可达性描述**,比 `agentUrl` 多一层语义 ——
* **经谁中转**(`via`)。现网两类 host 的 `endpoint` 字符串同形但语义完全不同
* (`w-47` 是直连本机、`w-106` 是 Manager 上的隧道落点),只有它能表达。
* · `agentUrl` = 旧字段,保留向后兼容。
*/
export interface ClusterHost {
hostId: string
/** agent 基址。 */
agentUrl: string
/** agent 基址(旧字段;与 `reachability` 至少给一个)。 */
agentUrl?: string
/** **可达性**:经谁中转 + 真实地址。给了就优先于 `agentUrl`(S2 起由 `dsh_hosts.via` 驱动)。 */
reachability?: Reachability
/** 与 agent 约定的共享密钥。 */
token: string
/** 代理时使用的主机(同机 1a = `127.0.0.1`;跨机填 Worker 内网 IP)。 */
@@ -81,15 +93,17 @@ export class RemoteSpawner implements Spawner {
private directoryLoadedAt = 0
constructor(options: RemoteSpawnerOptions) {
// ⚠️ 这里**不再做** `replace(/\/$/,'')` 归一化 —— 归一化统一在 `agentBaseUrlOf()`
// 里做(幂等)。存归一化值会让"可达性与 agentUrl 两套表示"混在一起,S2 之后难拆。
this.defaultHost = {
hostId: options.defaultHostId ?? 'local',
agentUrl: options.agentUrl.replace(/\/$/, ''),
agentUrl: options.agentUrl,
token: options.token,
instanceHost: options.instanceHost ?? '127.0.0.1',
}
this.hosts.set(this.defaultHost.hostId, this.defaultHost)
for (const host of options.hosts ?? []) {
this.hosts.set(host.hostId, { ...host, agentUrl: host.agentUrl.replace(/\/$/, '') })
this.hosts.set(host.hostId, { ...host })
}
this.timeoutMs = options.timeoutMs ?? 10_000
this.doFetch = options.fetchImpl ?? fetch
@@ -110,7 +124,7 @@ export class RemoteSpawner implements Spawner {
this.directoryLoadedAt = Date.now()
try {
for (const host of await this.hostsProvider()) {
this.hosts.set(host.hostId, { ...host, agentUrl: host.agentUrl.replace(/\/$/, '') })
this.hosts.set(host.hostId, { ...host })
}
this.hosts.set(this.defaultHost.hostId, this.defaultHost)
} catch {
@@ -177,7 +191,7 @@ export class RemoteSpawner implements Spawner {
for (let attempt = 0; attempt <= RETRY_DELAYS_MS.length; attempt += 1) {
if (attempt > 0) await new Promise((r) => setTimeout(r, RETRY_DELAYS_MS[attempt - 1]))
try {
const res = await this.doFetch(`${host.agentUrl}${path}`, {
const res = await this.doFetch(`${agentBaseUrlOf(host)}${path}`, {
method,
headers: {
[AGENT_TOKEN_HEADER]: host.token,
@@ -314,7 +328,7 @@ export class RemoteSpawner implements Spawner {
touch(userId: string): void {
void this.hostFor(userId)
.then((host) =>
this.doFetch(`${host.agentUrl}/touch/${encodeURIComponent(userId)}`, {
this.doFetch(`${agentBaseUrlOf(host)}/touch/${encodeURIComponent(userId)}`, {
method: 'POST',
headers: { [AGENT_TOKEN_HEADER]: host.token },
}),