@artenx/dshb 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +2157 -567
- package/lib/client.js.map +1 -1
- package/lib/core/index.js +72 -4
- package/lib/core/index.js.map +1 -1
- package/lib/core/node-registry.d.ts +2 -108
- package/lib/core/node-registry.js +43 -2
- package/lib/core/node-registry.js.map +1 -1
- package/lib/core/routes.d.ts +4 -3
- package/lib/core/routes.js +67 -23
- package/lib/core/routes.js.map +1 -1
- package/lib/core/sync/coordinator.d.ts +48 -0
- package/lib/core/sync/coordinator.js +247 -0
- package/lib/core/sync/coordinator.js.map +1 -0
- package/lib/core/sync/host-key.d.ts +2 -0
- package/lib/core/sync/host-key.js +71 -0
- package/lib/core/sync/host-key.js.map +1 -0
- package/lib/core/sync/internal-routes.d.ts +9 -0
- package/lib/core/sync/internal-routes.js +236 -0
- package/lib/core/sync/internal-routes.js.map +1 -0
- package/lib/core/sync/model.d.ts +2 -0
- package/lib/core/sync/model.js +106 -0
- package/lib/core/sync/model.js.map +1 -0
- package/lib/core/sync/node-sync.d.ts +2 -0
- package/lib/core/sync/node-sync.js +660 -0
- package/lib/core/sync/node-sync.js.map +1 -0
- package/lib/core/sync/offline-archive.d.ts +55 -0
- package/lib/core/sync/offline-archive.js +673 -0
- package/lib/core/sync/offline-archive.js.map +1 -0
- package/lib/core/sync/offline-journal.d.ts +22 -0
- package/lib/core/sync/offline-journal.js +74 -0
- package/lib/core/sync/offline-journal.js.map +1 -0
- package/lib/core/sync/offline-routes.d.ts +7 -0
- package/lib/core/sync/offline-routes.js +192 -0
- package/lib/core/sync/offline-routes.js.map +1 -0
- package/lib/core/sync/peer-manager.d.ts +56 -0
- package/lib/core/sync/peer-manager.js +258 -0
- package/lib/core/sync/peer-manager.js.map +1 -0
- package/lib/core/sync/protocol.d.ts +2 -0
- package/lib/core/sync/protocol.js +124 -0
- package/lib/core/sync/protocol.js.map +1 -0
- package/lib/core/sync/remote-helper.d.ts +5 -0
- package/lib/core/sync/remote-helper.js +160 -0
- package/lib/core/sync/remote-helper.js.map +1 -0
- package/lib/core/sync/revision.d.ts +2 -0
- package/lib/core/sync/revision.js +77 -0
- package/lib/core/sync/revision.js.map +1 -0
- package/lib/core/sync/routes.d.ts +9 -0
- package/lib/core/sync/routes.js +339 -0
- package/lib/core/sync/routes.js.map +1 -0
- package/lib/core/sync/scheduler.d.ts +14 -0
- package/lib/core/sync/scheduler.js +81 -0
- package/lib/core/sync/scheduler.js.map +1 -0
- package/lib/core/sync/session-journal.d.ts +2 -0
- package/lib/core/sync/session-journal.js +191 -0
- package/lib/core/sync/session-journal.js.map +1 -0
- package/lib/core/sync/session-routes.d.ts +7 -0
- package/lib/core/sync/session-routes.js +67 -0
- package/lib/core/sync/session-routes.js.map +1 -0
- package/lib/core/sync/session-sync.d.ts +2 -0
- package/lib/core/sync/session-sync.js +2 -0
- package/lib/core/sync/ssh-transport.d.ts +2 -0
- package/lib/core/sync/ssh-transport.js +408 -0
- package/lib/core/sync/ssh-transport.js.map +1 -0
- package/lib/core/sync/stores.d.ts +3 -0
- package/lib/core/sync/stores.js +385 -0
- package/lib/core/sync/stores.js.map +1 -0
- package/lib/core/test.d.ts +1 -1
- package/lib/core/workspace-bindings.d.ts +2 -28
- package/lib/core/workspace-bindings.js +14 -14
- package/lib/core/workspace-bindings.js.map +1 -1
- package/lib/core/workspace-instruction-context.d.ts +3 -2260
- package/lib/core/workspace-instruction-context.js +104 -2
- package/lib/core/workspace-instruction-context.js.map +1 -0
- package/lib/core/workspace-instruction-routes.d.ts +1 -1
- package/lib/core/workspace-instructions.d.ts +2 -68
- package/lib/core/workspace-instructions.js +13 -0
- package/lib/core/workspace-instructions.js.map +1 -1
- package/lib/core/workspaces.d.ts +4 -3
- package/lib/core/workspaces.js +17 -1
- package/lib/core/workspaces.js.map +1 -1
- package/lib/host-key-C5afhAUd.d.ts +30 -0
- package/lib/index-VF8bcBeX.d.ts +2293 -0
- package/lib/{workspace-instruction-context-DytAmaPY.js → lib-gdtU_WKT.js} +159 -102
- package/lib/lib-gdtU_WKT.js.map +1 -0
- package/lib/model-BP7Gw1yk.d.ts +104 -0
- package/lib/node-registry-BTnnM5-k.d.ts +112 -0
- package/lib/node-sync-3Z9QLz2n.d.ts +89 -0
- package/lib/protocol-CV7LGE44.d.ts +35 -0
- package/lib/revision-C9sOlDZ6.d.ts +25 -0
- package/lib/router/router-fs.d.ts +12 -3
- package/lib/router/router-fs.js.map +1 -1
- package/lib/session-journal-pgPqcWn9.d.ts +40 -0
- package/lib/session-sync-EdfwUON5.js +2177 -0
- package/lib/session-sync-EdfwUON5.js.map +1 -0
- package/lib/session-sync-g6qKZzjV.d.ts +1252 -0
- package/lib/ssh-transport-BcIiLxsX.d.ts +55 -0
- package/lib/stores-CKyEt0yk.d.ts +31 -0
- package/lib/workspace-bindings-CsB0TIJB.d.ts +28 -0
- package/lib/workspace-instructions-DirYYiku.d.ts +69 -0
- package/package.json +20 -1
- package/lib/workspace-instruction-context-DytAmaPY.js.map +0 -1
package/lib/core/index.js
CHANGED
|
@@ -3,12 +3,24 @@ import { sharedAuditLogger } from "./audit.js";
|
|
|
3
3
|
import { KnownHostsStore } from "./known-hosts.js";
|
|
4
4
|
import { NodeRegistry } from "./node-registry.js";
|
|
5
5
|
import { testNode } from "./test.js";
|
|
6
|
+
import { SyncNodeService } from "./sync/node-sync.js";
|
|
6
7
|
import { registerNodeRoutes, registerSshConfigRoutes } from "./routes.js";
|
|
7
8
|
import { registerRemoteDownloadRoutes, registerWorkspaceRoutes } from "./workspaces.js";
|
|
8
|
-
import { sharedWorkspaceInstructionStore } from "./workspace-instructions.js";
|
|
9
|
+
import { sharedSessionInstructionSnapshotStore, sharedWorkspaceInstructionStore } from "./workspace-instructions.js";
|
|
9
10
|
import { registerWorkspaceInstructionRoutes } from "./workspace-instruction-routes.js";
|
|
10
11
|
import { WorkspaceBindingsStore } from "./workspace-bindings.js";
|
|
11
|
-
import {
|
|
12
|
+
import { registerWorkspaceInstructionInjector } from "./workspace-instruction-context.js";
|
|
13
|
+
import { SyncPeerManager } from "./sync/peer-manager.js";
|
|
14
|
+
import { n as SyncSessionService } from "../session-sync-EdfwUON5.js";
|
|
15
|
+
import { registerSyncInternalRoutes } from "./sync/internal-routes.js";
|
|
16
|
+
import { SyncSshTransport } from "./sync/ssh-transport.js";
|
|
17
|
+
import { SyncCoordinator } from "./sync/coordinator.js";
|
|
18
|
+
import { registerSyncPeerRoutes } from "./sync/routes.js";
|
|
19
|
+
import { SyncScheduler } from "./sync/scheduler.js";
|
|
20
|
+
import { registerSyncSessionRoutes } from "./sync/session-routes.js";
|
|
21
|
+
import { SyncOfflineArchiveService } from "./sync/offline-archive.js";
|
|
22
|
+
import { registerSyncOfflineRoutes } from "./sync/offline-routes.js";
|
|
23
|
+
import { SyncPeerStore, SyncStateStore } from "./sync/stores.js";
|
|
12
24
|
//#region src/core/index.ts
|
|
13
25
|
const name = "dshb-core";
|
|
14
26
|
const inject = ["webServer", "credentials"];
|
|
@@ -16,20 +28,76 @@ function apply(ctx) {
|
|
|
16
28
|
const knownHosts = new KnownHostsStore();
|
|
17
29
|
const registry = new NodeRegistry(ctx);
|
|
18
30
|
const bindings = new WorkspaceBindingsStore();
|
|
31
|
+
const syncPeers = new SyncPeerStore();
|
|
32
|
+
const syncState = new SyncStateStore();
|
|
33
|
+
const syncPeerManager = new SyncPeerManager(syncPeers, syncState, ctx.credentials);
|
|
34
|
+
const syncSshTransport = new SyncSshTransport(syncPeerManager);
|
|
19
35
|
let workspaceRegistry;
|
|
20
36
|
if (typeof ctx.inject === "function") ctx.inject(["workspaceRegistry"], (workspaceCtx) => {
|
|
21
37
|
workspaceRegistry = workspaceCtx.workspaceRegistry;
|
|
22
38
|
});
|
|
39
|
+
const syncNodes = new SyncNodeService(registry, bindings, syncState, () => workspaceRegistry, (nodeId) => {
|
|
40
|
+
const sshWorlds = ctx.get("dshbSshWorlds");
|
|
41
|
+
const dockerWorlds = ctx.get("dshbDockerWorlds");
|
|
42
|
+
sshWorlds?.remove?.(nodeId);
|
|
43
|
+
dockerWorlds?.remove?.(nodeId);
|
|
44
|
+
});
|
|
45
|
+
const syncSessions = new SyncSessionService(syncState, syncNodes, registry, bindings, () => workspaceRegistry, () => ctx.get("sessionPersistence"), () => ctx.get("attachments"), () => ctx.get("spillStore"), sharedSessionInstructionSnapshotStore());
|
|
46
|
+
const syncOfflineArchives = new SyncOfflineArchiveService(syncNodes, syncSessions, void 0, void 0, syncState);
|
|
47
|
+
try {
|
|
48
|
+
syncOfflineArchives.cleanupStagingDirectories();
|
|
49
|
+
syncSessions.cleanupOrphanedStaging();
|
|
50
|
+
} catch {
|
|
51
|
+
ctx.logger.warn("sync staging cleanup failed");
|
|
52
|
+
}
|
|
53
|
+
const syncCoordinator = new SyncCoordinator(syncPeerManager, syncState, syncNodes, syncSshTransport, Date.now, syncSessions);
|
|
54
|
+
const syncScheduler = new SyncScheduler(syncPeerManager, syncCoordinator);
|
|
23
55
|
ctx.provide("nodeRegistry", registry);
|
|
24
56
|
ctx.provide("knownHosts", knownHosts);
|
|
25
57
|
ctx.provide("workspaceBindings", bindings);
|
|
58
|
+
ctx.provide("syncPeerStore", syncPeers);
|
|
59
|
+
ctx.provide("syncStateStore", syncState);
|
|
60
|
+
ctx.provide("syncNodeService", syncNodes);
|
|
61
|
+
ctx.provide("syncSessionService", syncSessions);
|
|
62
|
+
ctx.provide("syncOfflineArchiveService", syncOfflineArchives);
|
|
63
|
+
ctx.provide("syncPeerManager", syncPeerManager);
|
|
64
|
+
ctx.provide("syncSshTransport", syncSshTransport);
|
|
65
|
+
ctx.provide("syncCoordinator", syncCoordinator);
|
|
66
|
+
ctx.provide("syncScheduler", syncScheduler);
|
|
26
67
|
ctx.provide("dshbAudit", sharedAuditLogger());
|
|
27
68
|
sharedWorldResolver().setBindings(bindings);
|
|
28
|
-
registerNodeRoutes(ctx, registry, bindings, () => workspaceRegistry);
|
|
69
|
+
registerNodeRoutes(ctx, registry, bindings, () => workspaceRegistry, syncNodes);
|
|
29
70
|
registerSshConfigRoutes(ctx, knownHosts);
|
|
30
|
-
registerWorkspaceRoutes(ctx, registry, bindings);
|
|
71
|
+
registerWorkspaceRoutes(ctx, registry, bindings, syncNodes);
|
|
31
72
|
registerRemoteDownloadRoutes(ctx, registry, bindings);
|
|
32
73
|
registerWorkspaceInstructionRoutes(ctx, sharedWorkspaceInstructionStore());
|
|
74
|
+
registerSyncPeerRoutes(ctx, syncPeerManager, syncSshTransport, syncCoordinator);
|
|
75
|
+
registerSyncInternalRoutes(ctx, syncState, syncNodes, syncSessions);
|
|
76
|
+
registerSyncSessionRoutes(ctx, syncSessions);
|
|
77
|
+
registerSyncOfflineRoutes(ctx, syncOfflineArchives);
|
|
78
|
+
ctx.effect(() => {
|
|
79
|
+
const stopScheduler = syncScheduler.start();
|
|
80
|
+
let disposed = false;
|
|
81
|
+
setImmediate(() => {
|
|
82
|
+
if (disposed) return;
|
|
83
|
+
(async () => {
|
|
84
|
+
try {
|
|
85
|
+
await syncSessions.recoverPending();
|
|
86
|
+
} catch {
|
|
87
|
+
ctx.logger.warn("sync session recovery remains pending");
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await syncOfflineArchives.recoverPending();
|
|
91
|
+
} catch {
|
|
92
|
+
ctx.logger.warn("offline archive recovery remains pending");
|
|
93
|
+
}
|
|
94
|
+
})();
|
|
95
|
+
});
|
|
96
|
+
return () => {
|
|
97
|
+
disposed = true;
|
|
98
|
+
stopScheduler();
|
|
99
|
+
};
|
|
100
|
+
});
|
|
33
101
|
registerWorkspaceInstructionInjector(ctx);
|
|
34
102
|
warmupWorlds(ctx, registry, bindings);
|
|
35
103
|
startHeartbeat(ctx, registry);
|
package/lib/core/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/core/index.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis'\nimport { sharedWorldResolver } from '../router/resolve.js'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { sharedAuditLogger } from './audit.js'\nimport { KnownHostsStore } from './known-hosts.js'\nimport { NodeRegistry } from './node-registry.js'\nimport { registerNodeRoutes, registerSshConfigRoutes } from './routes.js'\nimport { registerWorkspaceRoutes, registerRemoteDownloadRoutes } from './workspaces.js'\nimport { registerWorkspaceInstructionRoutes } from './workspace-instruction-routes.js'\nimport { testNode, type DockerNodeTester, type SshHandshakeTester } from './test.js'\nimport { WorkspaceBindingsStore } from './workspace-bindings.js'\nimport { sharedWorkspaceInstructionStore } from './workspace-instructions.js'\nimport { registerWorkspaceInstructionInjector } from './workspace-instruction-context.js'\n\nexport const name = 'dshb-core'\n\nexport const inject = ['webServer', 'credentials']\n\nexport function apply(ctx: Context): void {\n const knownHosts = new KnownHostsStore()\n const registry = new NodeRegistry(ctx)\n const bindings = new WorkspaceBindingsStore()\n let workspaceRegistry: { list(): Array<{ path: string; title: string }> } | undefined\n if (typeof ctx.inject === 'function') {\n ctx.inject(['workspaceRegistry'], (workspaceCtx: Context) => {\n workspaceRegistry = (workspaceCtx as unknown as { workspaceRegistry?: typeof workspaceRegistry }).workspaceRegistry\n })\n }\n ctx.provide('nodeRegistry', registry)\n ctx.provide('knownHosts', knownHosts)\n ctx.provide('workspaceBindings', bindings)\n ctx.provide('dshbAudit', sharedAuditLogger())\n sharedWorldResolver().setBindings(bindings)\n void registerNodeRoutes(ctx, registry, bindings, () => workspaceRegistry)\n void registerSshConfigRoutes(ctx, knownHosts)\n void registerWorkspaceRoutes(ctx, registry, bindings)\n void registerRemoteDownloadRoutes(ctx, registry, bindings)\n void registerWorkspaceInstructionRoutes(ctx, sharedWorkspaceInstructionStore())\n registerWorkspaceInstructionInjector(ctx)\n warmupWorlds(ctx, registry, bindings)\n startHeartbeat(ctx, registry)\n reprovisionDockerNodes(ctx, registry)\n}\n\nconst HEARTBEAT_MS = 5 * 60 * 1000\n\nfunction startHeartbeat(ctx: Context, registry: NodeRegistry): void {\n const tick = async (): Promise<void> => {\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n const dockerTester = ctx.get('dshbDockerTester') as DockerNodeTester | undefined\n for (const node of registry.list()) {\n try {\n await testNode(registry, node.id, sshTester, undefined, dockerTester)\n } catch {\n // 单节点心跳失败不影响其他节点\n }\n }\n }\n const timer = setInterval(() => void tick(), HEARTBEAT_MS)\n ctx.effect(() => () => clearInterval(timer))\n setImmediate(() => void tick())\n}\n\nfunction reprovisionDockerNodes(ctx: Context, registry: NodeRegistry): void {\n setImmediate(() => {\n void (async () => {\n const provisioner = ctx.get('dshbDockerProvisioner') as { provision(nodeId: string): Promise<unknown> } | undefined\n if (!provisioner) return\n for (const node of registry.list()) {\n if (node.type !== 'local-docker' && node.type !== 'remote-docker') continue\n if (!node.docker?.image) continue\n if (node.docker?.containerId) continue\n try {\n await provisioner.provision(node.id)\n } catch {\n // 失败已记录到 provisionStatuses\n }\n }\n })()\n })\n}\n\ninterface WarmupWorldsLike {\n ensure(nodeId: string): Promise<{ ensureDir(path: string): Promise<void> } | undefined>\n}\n\nfunction warmupWorlds(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore): void {\n setImmediate(() => {\n void (async () => {\n for (const b of bindings.list()) {\n const node = registry.get(b.nodeId)\n if (!node || node.type === 'local-host') continue\n try {\n const isDocker = node.type === 'local-docker' || node.type === 'remote-docker'\n const worlds = isDocker\n ? (ctx.get('dshbDockerWorldsGeneric') as WarmupWorldsLike | undefined)\n : (ctx.get('dshbWorlds') as WarmupWorldsLike | undefined)\n if (!worlds) continue\n const world = await worlds.ensure(b.nodeId)\n if (world) await world.ensureDir(b.remotePath)\n } catch {\n // 预热失败不应阻塞启动;运行时可再次 ensure\n }\n }\n })()\n })\n}\n"],"mappings":";;;;;;;;;;;;AAcA,MAAa,OAAO;AAEpB,MAAa,SAAS,CAAC,aAAa,aAAa;AAEjD,SAAgB,MAAM,KAAoB;CACxC,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,WAAW,IAAI,aAAa,GAAG;CACrC,MAAM,WAAW,IAAI,uBAAuB;CAC5C,IAAI;CACJ,IAAI,OAAO,IAAI,WAAW,YACxB,IAAI,OAAO,CAAC,mBAAmB,IAAI,iBAA0B;EAC3D,oBAAqB,aAA6E;CACpG,CAAC;CAEH,IAAI,QAAQ,gBAAgB,QAAQ;CACpC,IAAI,QAAQ,cAAc,UAAU;CACpC,IAAI,QAAQ,qBAAqB,QAAQ;CACzC,IAAI,QAAQ,aAAa,kBAAkB,CAAC;CAC5C,oBAAoB,CAAC,CAAC,YAAY,QAAQ;CAC1C,mBAAwB,KAAK,UAAU,gBAAgB,iBAAiB;CACxE,wBAA6B,KAAK,UAAU;CAC5C,wBAA6B,KAAK,UAAU,QAAQ;CACpD,6BAAkC,KAAK,UAAU,QAAQ;CACzD,mCAAwC,KAAK,gCAAgC,CAAC;CAC9E,qCAAqC,GAAG;CACxC,aAAa,KAAK,UAAU,QAAQ;CACpC,eAAe,KAAK,QAAQ;CAC5B,uBAAuB,KAAK,QAAQ;AACtC;AAEA,MAAM,eAAe;AAErB,SAAS,eAAe,KAAc,UAA8B;CAClE,MAAM,OAAO,YAA2B;EACtC,MAAM,YAAY,IAAI,IAAI,aAAa;EACvC,MAAM,eAAe,IAAI,IAAI,kBAAkB;EAC/C,KAAK,MAAM,QAAQ,SAAS,KAAK,GAC/B,IAAI;GACF,MAAM,SAAS,UAAU,KAAK,IAAI,WAAW,KAAA,GAAW,YAAY;EACtE,QAAQ,CAER;CAEJ;CACA,MAAM,QAAQ,kBAAkB,KAAK,KAAK,GAAG,YAAY;CACzD,IAAI,mBAAmB,cAAc,KAAK,CAAC;CAC3C,mBAAmB,KAAK,KAAK,CAAC;AAChC;AAEA,SAAS,uBAAuB,KAAc,UAA8B;CAC1E,mBAAmB;EACjB,CAAM,YAAY;GAChB,MAAM,cAAc,IAAI,IAAI,uBAAuB;GACnD,IAAI,CAAC,aAAa;GAClB,KAAK,MAAM,QAAQ,SAAS,KAAK,GAAG;IAClC,IAAI,KAAK,SAAS,kBAAkB,KAAK,SAAS,iBAAiB;IACnE,IAAI,CAAC,KAAK,QAAQ,OAAO;IACzB,IAAI,KAAK,QAAQ,aAAa;IAC9B,IAAI;KACF,MAAM,YAAY,UAAU,KAAK,EAAE;IACrC,QAAQ,CAER;GACF;EACF,EAAA,CAAG;CACL,CAAC;AACH;AAMA,SAAS,aAAa,KAAc,UAAwB,UAAwC;CAClG,mBAAmB;EACjB,CAAM,YAAY;GAChB,KAAK,MAAM,KAAK,SAAS,KAAK,GAAG;IAC/B,MAAM,OAAO,SAAS,IAAI,EAAE,MAAM;IAClC,IAAI,CAAC,QAAQ,KAAK,SAAS,cAAc;IACzC,IAAI;KAEF,MAAM,SADW,KAAK,SAAS,kBAAkB,KAAK,SAAS,kBAE1D,IAAI,IAAI,yBAAyB,IACjC,IAAI,IAAI,YAAY;KACzB,IAAI,CAAC,QAAQ;KACb,MAAM,QAAQ,MAAM,OAAO,OAAO,EAAE,MAAM;KAC1C,IAAI,OAAO,MAAM,MAAM,UAAU,EAAE,UAAU;IAC/C,QAAQ,CAER;GACF;EACF,EAAA,CAAG;CACL,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/core/index.ts"],"sourcesContent":["import type { Context } from '@deepseek-ai/cordis'\nimport { sharedWorldResolver } from '../router/resolve.js'\nimport type {} from '@deepseek-ai/dsh-host-webserver'\nimport { sharedAuditLogger } from './audit.js'\nimport { KnownHostsStore } from './known-hosts.js'\nimport { NodeRegistry } from './node-registry.js'\nimport { registerNodeRoutes, registerSshConfigRoutes } from './routes.js'\nimport { registerWorkspaceRoutes, registerRemoteDownloadRoutes } from './workspaces.js'\nimport { registerWorkspaceInstructionRoutes } from './workspace-instruction-routes.js'\nimport { testNode, type DockerNodeTester, type SshHandshakeTester } from './test.js'\nimport { WorkspaceBindingsStore } from './workspace-bindings.js'\nimport { sharedWorkspaceInstructionStore } from './workspace-instructions.js'\nimport { registerWorkspaceInstructionInjector } from './workspace-instruction-context.js'\nimport { SyncPeerManager } from './sync/peer-manager.js'\nimport { registerSyncInternalRoutes } from './sync/internal-routes.js'\nimport { registerSyncPeerRoutes } from './sync/routes.js'\nimport { SyncSshTransport } from './sync/ssh-transport.js'\nimport { SyncCoordinator } from './sync/coordinator.js'\nimport { SyncScheduler } from './sync/scheduler.js'\nimport { SyncNodeService } from './sync/node-sync.js'\nimport { SyncSessionService, type WorkspaceRegistryLike } from './sync/session-sync.js'\nimport { registerSyncSessionRoutes } from './sync/session-routes.js'\nimport { SyncOfflineArchiveService } from './sync/offline-archive.js'\nimport { registerSyncOfflineRoutes } from './sync/offline-routes.js'\nimport { SyncPeerStore, SyncStateStore } from './sync/stores.js'\nimport { sharedSessionInstructionSnapshotStore } from './workspace-instructions.js'\n\nexport const name = 'dshb-core'\n\nexport const inject = ['webServer', 'credentials']\n\nexport function apply(ctx: Context): void {\n const knownHosts = new KnownHostsStore()\n const registry = new NodeRegistry(ctx)\n const bindings = new WorkspaceBindingsStore()\n const syncPeers = new SyncPeerStore()\n const syncState = new SyncStateStore()\n const syncPeerManager = new SyncPeerManager(syncPeers, syncState, ctx.credentials as never)\n const syncSshTransport = new SyncSshTransport(syncPeerManager)\n let workspaceRegistry: WorkspaceRegistryLike | undefined\n if (typeof ctx.inject === 'function') {\n ctx.inject(['workspaceRegistry'], (workspaceCtx: Context) => {\n workspaceRegistry = (workspaceCtx as unknown as { workspaceRegistry?: typeof workspaceRegistry }).workspaceRegistry\n })\n }\n const syncNodes = new SyncNodeService(registry, bindings, syncState, () => workspaceRegistry, (nodeId) => {\n const sshWorlds = ctx.get('dshbSshWorlds') as { remove?: (id: string) => void } | undefined\n const dockerWorlds = ctx.get('dshbDockerWorlds') as { remove?: (id: string) => void } | undefined\n sshWorlds?.remove?.(nodeId)\n dockerWorlds?.remove?.(nodeId)\n })\n const syncSessions = new SyncSessionService(\n syncState,\n syncNodes,\n registry,\n bindings,\n () => workspaceRegistry,\n () => ctx.get('sessionPersistence') as never,\n () => ctx.get('attachments') as never,\n () => ctx.get('spillStore') as never,\n sharedSessionInstructionSnapshotStore(),\n )\n const syncOfflineArchives = new SyncOfflineArchiveService(syncNodes, syncSessions, undefined, undefined, syncState)\n try {\n syncOfflineArchives.cleanupStagingDirectories()\n syncSessions.cleanupOrphanedStaging()\n } catch {\n ctx.logger.warn('sync staging cleanup failed')\n }\n const syncCoordinator = new SyncCoordinator(syncPeerManager, syncState, syncNodes, syncSshTransport, Date.now, syncSessions)\n const syncScheduler = new SyncScheduler(syncPeerManager, syncCoordinator)\n ctx.provide('nodeRegistry', registry)\n ctx.provide('knownHosts', knownHosts)\n ctx.provide('workspaceBindings', bindings)\n ctx.provide('syncPeerStore', syncPeers)\n ctx.provide('syncStateStore', syncState)\n ctx.provide('syncNodeService', syncNodes)\n ctx.provide('syncSessionService', syncSessions)\n ctx.provide('syncOfflineArchiveService', syncOfflineArchives)\n ctx.provide('syncPeerManager', syncPeerManager)\n ctx.provide('syncSshTransport', syncSshTransport)\n ctx.provide('syncCoordinator', syncCoordinator)\n ctx.provide('syncScheduler', syncScheduler)\n ctx.provide('dshbAudit', sharedAuditLogger())\n sharedWorldResolver().setBindings(bindings)\n void registerNodeRoutes(ctx, registry, bindings, () => workspaceRegistry, syncNodes)\n void registerSshConfigRoutes(ctx, knownHosts)\n void registerWorkspaceRoutes(ctx, registry, bindings, syncNodes)\n void registerRemoteDownloadRoutes(ctx, registry, bindings)\n void registerWorkspaceInstructionRoutes(ctx, sharedWorkspaceInstructionStore())\n registerSyncPeerRoutes(ctx, syncPeerManager, syncSshTransport, syncCoordinator)\n registerSyncInternalRoutes(ctx, syncState, syncNodes, syncSessions)\n registerSyncSessionRoutes(ctx, syncSessions)\n registerSyncOfflineRoutes(ctx, syncOfflineArchives)\n ctx.effect(() => {\n const stopScheduler = syncScheduler.start()\n let disposed = false\n setImmediate(() => {\n if (disposed) return\n void (async () => {\n try {\n await syncSessions.recoverPending()\n } catch {\n ctx.logger.warn('sync session recovery remains pending')\n }\n try {\n await syncOfflineArchives.recoverPending()\n } catch {\n ctx.logger.warn('offline archive recovery remains pending')\n }\n })()\n })\n return () => {\n disposed = true\n stopScheduler()\n }\n })\n registerWorkspaceInstructionInjector(ctx)\n warmupWorlds(ctx, registry, bindings)\n startHeartbeat(ctx, registry)\n reprovisionDockerNodes(ctx, registry)\n}\n\nconst HEARTBEAT_MS = 5 * 60 * 1000\n\nfunction startHeartbeat(ctx: Context, registry: NodeRegistry): void {\n const tick = async (): Promise<void> => {\n const sshTester = ctx.get('dshbSshPool') as SshHandshakeTester | undefined\n const dockerTester = ctx.get('dshbDockerTester') as DockerNodeTester | undefined\n for (const node of registry.list()) {\n try {\n await testNode(registry, node.id, sshTester, undefined, dockerTester)\n } catch {\n // 单节点心跳失败不影响其他节点\n }\n }\n }\n const timer = setInterval(() => void tick(), HEARTBEAT_MS)\n ctx.effect(() => () => clearInterval(timer))\n setImmediate(() => void tick())\n}\n\nfunction reprovisionDockerNodes(ctx: Context, registry: NodeRegistry): void {\n setImmediate(() => {\n void (async () => {\n const provisioner = ctx.get('dshbDockerProvisioner') as { provision(nodeId: string): Promise<unknown> } | undefined\n if (!provisioner) return\n for (const node of registry.list()) {\n if (node.type !== 'local-docker' && node.type !== 'remote-docker') continue\n if (!node.docker?.image) continue\n if (node.docker?.containerId) continue\n try {\n await provisioner.provision(node.id)\n } catch {\n // 失败已记录到 provisionStatuses\n }\n }\n })()\n })\n}\n\ninterface WarmupWorldsLike {\n ensure(nodeId: string): Promise<{ ensureDir(path: string): Promise<void> } | undefined>\n}\n\nfunction warmupWorlds(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore): void {\n setImmediate(() => {\n void (async () => {\n for (const b of bindings.list()) {\n const node = registry.get(b.nodeId)\n if (!node || node.type === 'local-host') continue\n try {\n const isDocker = node.type === 'local-docker' || node.type === 'remote-docker'\n const worlds = isDocker\n ? (ctx.get('dshbDockerWorldsGeneric') as WarmupWorldsLike | undefined)\n : (ctx.get('dshbWorlds') as WarmupWorldsLike | undefined)\n if (!worlds) continue\n const world = await worlds.ensure(b.nodeId)\n if (world) await world.ensureDir(b.remotePath)\n } catch {\n // 预热失败不应阻塞启动;运行时可再次 ensure\n }\n }\n })()\n })\n}\n"],"mappings":"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"}
|
|
@@ -1,108 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
//#region src/core/node-registry.d.ts
|
|
4
|
-
type NodeType = 'local-host' | 'local-docker' | 'remote-ssh' | 'remote-docker';
|
|
5
|
-
interface NodeSshAuth {
|
|
6
|
-
kind: 'password' | 'key' | 'agent';
|
|
7
|
-
keyPath?: string;
|
|
8
|
-
}
|
|
9
|
-
interface NodeJumpHop {
|
|
10
|
-
host: string;
|
|
11
|
-
port?: number;
|
|
12
|
-
username?: string;
|
|
13
|
-
keyPath?: string;
|
|
14
|
-
}
|
|
15
|
-
interface NodeSsh {
|
|
16
|
-
host: string;
|
|
17
|
-
port: number;
|
|
18
|
-
username: string;
|
|
19
|
-
auth: NodeSshAuth;
|
|
20
|
-
jump?: NodeJumpHop[];
|
|
21
|
-
hostKeyFingerprint?: string;
|
|
22
|
-
}
|
|
23
|
-
interface NodeDocker {
|
|
24
|
-
mode: 'existing' | 'managed';
|
|
25
|
-
containerId?: string;
|
|
26
|
-
image?: string;
|
|
27
|
-
resources?: {
|
|
28
|
-
cpus?: number;
|
|
29
|
-
memoryMB?: number;
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
interface NodeProfile {
|
|
33
|
-
id: string;
|
|
34
|
-
name: string;
|
|
35
|
-
type: NodeType;
|
|
36
|
-
ssh?: NodeSsh;
|
|
37
|
-
docker?: NodeDocker;
|
|
38
|
-
createdAt: string;
|
|
39
|
-
updatedAt: string;
|
|
40
|
-
}
|
|
41
|
-
interface NodeSecrets {
|
|
42
|
-
password?: string;
|
|
43
|
-
privateKey?: string;
|
|
44
|
-
passphrase?: string;
|
|
45
|
-
}
|
|
46
|
-
interface NodeStatus {
|
|
47
|
-
reachable?: boolean;
|
|
48
|
-
lastCheckedAt?: string;
|
|
49
|
-
error?: string;
|
|
50
|
-
}
|
|
51
|
-
interface NodeTestReport {
|
|
52
|
-
ok: boolean;
|
|
53
|
-
reachable?: boolean;
|
|
54
|
-
error?: string;
|
|
55
|
-
category?: string;
|
|
56
|
-
}
|
|
57
|
-
interface NodeCreateInput {
|
|
58
|
-
name: string;
|
|
59
|
-
type: NodeType;
|
|
60
|
-
ssh?: Omit<NodeSsh, 'hostKeyFingerprint'> & {
|
|
61
|
-
hostKeyFingerprint?: string;
|
|
62
|
-
};
|
|
63
|
-
docker?: NodeDocker;
|
|
64
|
-
secrets?: NodeSecrets;
|
|
65
|
-
}
|
|
66
|
-
interface NodeUpdateInput {
|
|
67
|
-
name?: string;
|
|
68
|
-
ssh?: Partial<Omit<NodeSsh, 'hostKeyFingerprint'>>;
|
|
69
|
-
docker?: Partial<NodeDocker>;
|
|
70
|
-
secrets?: NodeSecrets;
|
|
71
|
-
}
|
|
72
|
-
interface CredentialLike {
|
|
73
|
-
readRecord(key: CredentialKey): Promise<unknown>;
|
|
74
|
-
modifyRecord(key: CredentialKey, mutate: (current: unknown) => Promise<unknown>): Promise<unknown>;
|
|
75
|
-
deleteRecord(key: CredentialKey): Promise<void>;
|
|
76
|
-
}
|
|
77
|
-
declare function slugifyName(name: string): string;
|
|
78
|
-
declare function dshHome(): string;
|
|
79
|
-
declare class NodeRegistry {
|
|
80
|
-
private readonly ctx;
|
|
81
|
-
private profiles;
|
|
82
|
-
private loaded;
|
|
83
|
-
private readonly statuses;
|
|
84
|
-
constructor(ctx: Context);
|
|
85
|
-
private get creds();
|
|
86
|
-
private file;
|
|
87
|
-
private load;
|
|
88
|
-
private ensureLocalHost;
|
|
89
|
-
private save;
|
|
90
|
-
list(): NodeProfile[];
|
|
91
|
-
get(id: string): NodeProfile | undefined;
|
|
92
|
-
hasSecret(id: string): Promise<{
|
|
93
|
-
hasPassword: boolean;
|
|
94
|
-
hasKey: boolean;
|
|
95
|
-
hasPassphrase: boolean;
|
|
96
|
-
}>;
|
|
97
|
-
getSecrets(id: string): Promise<NodeSecrets | undefined>;
|
|
98
|
-
create(input: NodeCreateInput): Promise<NodeProfile>;
|
|
99
|
-
update(id: string, input: NodeUpdateInput): Promise<NodeProfile>;
|
|
100
|
-
remove(id: string): Promise<void>;
|
|
101
|
-
status(id: string): NodeStatus;
|
|
102
|
-
setStatus(id: string, status: NodeStatus): void;
|
|
103
|
-
private credKey;
|
|
104
|
-
private writeSecrets;
|
|
105
|
-
}
|
|
106
|
-
//#endregion
|
|
107
|
-
export { CredentialLike, NodeCreateInput, NodeDocker, NodeJumpHop, NodeProfile, NodeRegistry, NodeSecrets, NodeSsh, NodeSshAuth, NodeStatus, NodeTestReport, NodeType, NodeUpdateInput, dshHome, slugifyName };
|
|
108
|
-
//# sourceMappingURL=node-registry.d.ts.map
|
|
1
|
+
import { a as NodeProfile, c as NodeSsh, d as NodeTestReport, f as NodeType, h as slugifyName, i as NodeJumpHop, l as NodeSshAuth, m as dshHome, n as NodeCreateInput, o as NodeRegistry, p as NodeUpdateInput, r as NodeDocker, s as NodeSecrets, t as CredentialLike, u as NodeStatus } from "../node-registry-BTnnM5-k.js";
|
|
2
|
+
export { CredentialLike, NodeCreateInput, NodeDocker, NodeJumpHop, NodeProfile, NodeRegistry, NodeSecrets, NodeSsh, NodeSshAuth, NodeStatus, NodeTestReport, NodeType, NodeUpdateInput, dshHome, slugifyName };
|
|
@@ -109,11 +109,19 @@ var NodeRegistry = class {
|
|
|
109
109
|
return (await this.creds.readRecord(key))?.payload;
|
|
110
110
|
}
|
|
111
111
|
async create(input) {
|
|
112
|
+
return this.createProfile(input, false);
|
|
113
|
+
}
|
|
114
|
+
async createImported(input, preferredId) {
|
|
115
|
+
if (input.type === "local-host" || input.type === "local-docker") throw new Error("sync imports only support remote nodes");
|
|
116
|
+
return this.createProfile(input, true, preferredId);
|
|
117
|
+
}
|
|
118
|
+
async createProfile(input, allowDuplicateName, preferredId) {
|
|
112
119
|
this.load();
|
|
113
120
|
const name = slugifyName(input.name);
|
|
114
|
-
if (this.profiles.some((p) => sameName(p.name, name))) throw new Error(`node name "${name}" already exists`);
|
|
115
|
-
const id = newId();
|
|
121
|
+
if (!allowDuplicateName && this.profiles.some((p) => sameName(p.name, name))) throw new Error(`node name "${name}" already exists`);
|
|
122
|
+
const id = preferredId ?? newId();
|
|
116
123
|
if (!isLowerHyphenatedId(id)) throw new Error("internal: invalid generated id");
|
|
124
|
+
if (this.profiles.some((profile) => profile.id === id)) throw new Error(`node ${id} already exists`);
|
|
117
125
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
118
126
|
const profile = {
|
|
119
127
|
id,
|
|
@@ -129,6 +137,27 @@ var NodeRegistry = class {
|
|
|
129
137
|
this.save();
|
|
130
138
|
return { ...profile };
|
|
131
139
|
}
|
|
140
|
+
async updateImported(id, input) {
|
|
141
|
+
if (id === LOCAL_NODE_ID || input.type === "local-host" || input.type === "local-docker") throw new Error("sync imports only support remote nodes");
|
|
142
|
+
this.load();
|
|
143
|
+
const idx = this.profiles.findIndex((profile) => profile.id === id);
|
|
144
|
+
if (idx < 0) throw new Error(`node ${id} not found`);
|
|
145
|
+
const current = this.profiles[idx];
|
|
146
|
+
const next = {
|
|
147
|
+
id,
|
|
148
|
+
name: slugifyName(input.name),
|
|
149
|
+
type: input.type,
|
|
150
|
+
ssh: input.ssh,
|
|
151
|
+
docker: input.docker,
|
|
152
|
+
createdAt: current.createdAt,
|
|
153
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
154
|
+
};
|
|
155
|
+
if (input.secrets && (input.secrets.password || input.secrets.privateKey || input.secrets.passphrase)) await this.writeSecrets(id, input.secrets);
|
|
156
|
+
this.profiles[idx] = next;
|
|
157
|
+
this.statuses.delete(id);
|
|
158
|
+
this.save();
|
|
159
|
+
return { ...next };
|
|
160
|
+
}
|
|
132
161
|
async update(id, input) {
|
|
133
162
|
this.load();
|
|
134
163
|
const idx = this.profiles.findIndex((p) => p.id === id);
|
|
@@ -163,6 +192,18 @@ var NodeRegistry = class {
|
|
|
163
192
|
const key = this.credKey(id);
|
|
164
193
|
if (key) await this.creds.deleteRecord(key);
|
|
165
194
|
}
|
|
195
|
+
async removeImported(id) {
|
|
196
|
+
if (id === LOCAL_NODE_ID) throw new Error("默认环境节点不可删除");
|
|
197
|
+
this.load();
|
|
198
|
+
const idx = this.profiles.findIndex((p) => p.id === id);
|
|
199
|
+
if (idx >= 0) {
|
|
200
|
+
this.profiles.splice(idx, 1);
|
|
201
|
+
this.save();
|
|
202
|
+
}
|
|
203
|
+
this.statuses.delete(id);
|
|
204
|
+
const key = this.credKey(id);
|
|
205
|
+
if (key) await this.creds.deleteRecord(key);
|
|
206
|
+
}
|
|
166
207
|
status(id) {
|
|
167
208
|
return this.statuses.get(id) ?? {};
|
|
168
209
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-registry.js","names":[],"sources":["../../src/core/node-registry.ts"],"sourcesContent":["import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'\nimport { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { credentialKey, type CredentialKey } from '@deepseek-ai/dsh-credentials'\nimport type { Context } from '@deepseek-ai/cordis'\n\nexport type NodeType = 'local-host' | 'local-docker' | 'remote-ssh' | 'remote-docker'\n\nexport interface NodeSshAuth {\n kind: 'password' | 'key' | 'agent'\n keyPath?: string\n}\n\nexport interface NodeJumpHop {\n host: string\n port?: number\n username?: string\n keyPath?: string\n}\n\nexport interface NodeSsh {\n host: string\n port: number\n username: string\n auth: NodeSshAuth\n jump?: NodeJumpHop[]\n hostKeyFingerprint?: string\n}\n\nexport interface NodeDocker {\n mode: 'existing' | 'managed'\n containerId?: string\n image?: string\n resources?: { cpus?: number; memoryMB?: number }\n}\n\nexport interface NodeProfile {\n id: string\n name: string\n type: NodeType\n ssh?: NodeSsh\n docker?: NodeDocker\n createdAt: string\n updatedAt: string\n}\n\nexport interface NodeSecrets {\n password?: string\n privateKey?: string\n passphrase?: string\n}\n\nexport interface NodeStatus {\n reachable?: boolean\n lastCheckedAt?: string\n error?: string\n}\n\nexport interface NodeTestReport {\n ok: boolean\n reachable?: boolean\n error?: string\n category?: string\n}\n\nexport interface NodeCreateInput {\n name: string\n type: NodeType\n ssh?: Omit<NodeSsh, 'hostKeyFingerprint'> & { hostKeyFingerprint?: string }\n docker?: NodeDocker\n secrets?: NodeSecrets\n}\n\nexport interface NodeUpdateInput {\n name?: string\n ssh?: Partial<Omit<NodeSsh, 'hostKeyFingerprint'>>\n docker?: Partial<NodeDocker>\n secrets?: NodeSecrets\n}\n\nexport interface CredentialLike {\n readRecord(key: CredentialKey): Promise<unknown>\n modifyRecord(key: CredentialKey, mutate: (current: unknown) => Promise<unknown>): Promise<unknown>\n deleteRecord(key: CredentialKey): Promise<void>\n}\n\nfunction newId(): string {\n return `n${randomBytes(6).toString('hex')}`\n}\n\nexport function slugifyName(name: string): string {\n return name.trim().slice(0, 64) || 'node'\n}\n\nfunction isLowerHyphenatedId(value: string): boolean {\n return /^[a-z][a-z0-9-]*$/.test(value)\n}\n\nfunction sameName(a: string, b: string): boolean {\n const ha = createHash('sha256').update(a).digest()\n const hb = createHash('sha256').update(b).digest()\n return timingSafeEqual(ha, hb)\n}\n\nexport function dshHome(): string {\n return process.env.DSH_HOME ?? join(homedir(), '.dsh')\n}\n\nfunction join(...parts: string[]): string {\n return parts.join('/')\n}\n\nconst LOCAL_NODE_ID = 'local'\n\nexport class NodeRegistry {\n private profiles: NodeProfile[] = []\n private loaded = false\n private readonly statuses = new Map<string, NodeStatus>()\n\n constructor(private readonly ctx: Context) {}\n\n private get creds(): CredentialLike {\n return this.ctx.credentials as unknown as CredentialLike\n }\n\n private file(): string {\n return `${dshHome()}/dshb/nodes.json`\n }\n\n private load(): void {\n if (this.loaded) return\n this.loaded = true\n try {\n this.profiles = JSON.parse(readFileSync(this.file(), 'utf8')) as NodeProfile[]\n } catch {\n this.profiles = []\n }\n this.ensureLocalHost()\n }\n\n private ensureLocalHost(): void {\n const existing = this.profiles.find((p) => p.type === 'local-host')\n if (existing) {\n if (existing.name !== '默认环境') {\n existing.name = '默认环境'\n existing.updatedAt = new Date().toISOString()\n this.save()\n }\n return\n }\n const now = new Date().toISOString()\n this.profiles.unshift({\n id: LOCAL_NODE_ID,\n name: '默认环境',\n type: 'local-host',\n createdAt: now,\n updatedAt: now,\n })\n this.save()\n }\n\n private save(): void {\n const file = this.file()\n const dir = file.slice(0, file.lastIndexOf('/'))\n mkdirSync(dir, { recursive: true, mode: 0o700 })\n const tmp = `${file}.${process.pid}.tmp`\n writeFileSync(tmp, JSON.stringify(this.profiles, null, 2), { mode: 0o600 })\n renameSync(tmp, file)\n chmodSync(file, 0o600)\n }\n\n list(): NodeProfile[] {\n this.load()\n return this.profiles.map((p) => ({ ...p }))\n }\n\n get(id: string): NodeProfile | undefined {\n this.load()\n const found = this.profiles.find((p) => p.id === id)\n return found ? { ...found } : undefined\n }\n\n async hasSecret(id: string): Promise<{ hasPassword: boolean; hasKey: boolean; hasPassphrase: boolean }> {\n const key = this.credKey(id)\n if (!key) return { hasPassword: false, hasKey: false, hasPassphrase: false }\n const record = (await this.creds.readRecord(key)) as { payload?: NodeSecrets } | undefined\n const payload = record?.payload\n return {\n hasPassword: Boolean(payload?.password),\n hasKey: Boolean(payload?.privateKey),\n hasPassphrase: Boolean(payload?.passphrase),\n }\n }\n\n async getSecrets(id: string): Promise<NodeSecrets | undefined> {\n const key = this.credKey(id)\n if (!key) return undefined\n const record = (await this.creds.readRecord(key)) as { payload?: NodeSecrets } | undefined\n return record?.payload\n }\n\n async create(input: NodeCreateInput): Promise<NodeProfile> {\n this.load()\n const name = slugifyName(input.name)\n if (this.profiles.some((p) => sameName(p.name, name))) {\n throw new Error(`node name \"${name}\" already exists`)\n }\n const id = newId()\n if (!isLowerHyphenatedId(id)) throw new Error('internal: invalid generated id')\n const now = new Date().toISOString()\n const profile: NodeProfile = {\n id,\n name,\n type: input.type,\n ssh: input.ssh as NodeSsh | undefined,\n docker: input.docker,\n createdAt: now,\n updatedAt: now,\n }\n if (input.secrets && (input.secrets.password || input.secrets.privateKey || input.secrets.passphrase)) {\n await this.writeSecrets(id, input.secrets)\n }\n this.profiles.push(profile)\n this.save()\n return { ...profile }\n }\n\n async update(id: string, input: NodeUpdateInput): Promise<NodeProfile> {\n this.load()\n const idx = this.profiles.findIndex((p) => p.id === id)\n if (idx < 0) throw new Error(`node ${id} not found`)\n const current = this.profiles[idx]\n const next: NodeProfile = { ...current, updatedAt: new Date().toISOString() }\n if (input.name !== undefined) next.name = slugifyName(input.name)\n if (input.ssh) next.ssh = { ...current.ssh, ...input.ssh } as NodeSsh\n if (input.docker) next.docker = { ...current.docker, ...input.docker } as NodeDocker\n this.profiles[idx] = next\n if (input.secrets && (input.secrets.password || input.secrets.privateKey || input.secrets.passphrase)) {\n await this.writeSecrets(id, input.secrets)\n }\n this.save()\n return { ...next }\n }\n\n async remove(id: string): Promise<void> {\n if (id === LOCAL_NODE_ID) throw new Error('默认环境节点不可删除')\n this.load()\n const idx = this.profiles.findIndex((p) => p.id === id)\n if (idx < 0) return\n this.profiles.splice(idx, 1)\n this.statuses.delete(id)\n this.save()\n const key = this.credKey(id)\n if (key) await this.creds.deleteRecord(key)\n }\n\n status(id: string): NodeStatus {\n return this.statuses.get(id) ?? {}\n }\n\n setStatus(id: string, status: NodeStatus): void {\n this.statuses.set(id, status)\n }\n\n private credKey(id: string): CredentialKey | undefined {\n try {\n return credentialKey('dshb-core', id)\n } catch {\n return undefined\n }\n }\n\n private async writeSecrets(id: string, secrets: NodeSecrets): Promise<void> {\n const key = this.credKey(id)\n if (!key) throw new Error('credentials service unavailable')\n await this.creds.modifyRecord(key, async () => ({\n kind: 'grant' as const,\n payload: { ...secrets },\n }))\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"node-registry.js","names":[],"sources":["../../src/core/node-registry.ts"],"sourcesContent":["import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'\nimport { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { credentialKey, type CredentialKey } from '@deepseek-ai/dsh-credentials'\nimport type { Context } from '@deepseek-ai/cordis'\n\nexport type NodeType = 'local-host' | 'local-docker' | 'remote-ssh' | 'remote-docker'\n\nexport interface NodeSshAuth {\n kind: 'password' | 'key' | 'agent'\n keyPath?: string\n}\n\nexport interface NodeJumpHop {\n host: string\n port?: number\n username?: string\n keyPath?: string\n}\n\nexport interface NodeSsh {\n host: string\n port: number\n username: string\n auth: NodeSshAuth\n jump?: NodeJumpHop[]\n hostKeyFingerprint?: string\n}\n\nexport interface NodeDocker {\n mode: 'existing' | 'managed'\n containerId?: string\n image?: string\n resources?: { cpus?: number; memoryMB?: number }\n}\n\nexport interface NodeProfile {\n id: string\n name: string\n type: NodeType\n ssh?: NodeSsh\n docker?: NodeDocker\n createdAt: string\n updatedAt: string\n}\n\nexport interface NodeSecrets {\n password?: string\n privateKey?: string\n passphrase?: string\n}\n\nexport interface NodeStatus {\n reachable?: boolean\n lastCheckedAt?: string\n error?: string\n}\n\nexport interface NodeTestReport {\n ok: boolean\n reachable?: boolean\n error?: string\n category?: string\n}\n\nexport interface NodeCreateInput {\n name: string\n type: NodeType\n ssh?: Omit<NodeSsh, 'hostKeyFingerprint'> & { hostKeyFingerprint?: string }\n docker?: NodeDocker\n secrets?: NodeSecrets\n}\n\nexport interface NodeUpdateInput {\n name?: string\n ssh?: Partial<Omit<NodeSsh, 'hostKeyFingerprint'>>\n docker?: Partial<NodeDocker>\n secrets?: NodeSecrets\n}\n\nexport interface CredentialLike {\n readRecord(key: CredentialKey): Promise<unknown>\n modifyRecord(key: CredentialKey, mutate: (current: unknown) => Promise<unknown>): Promise<unknown>\n deleteRecord(key: CredentialKey): Promise<void>\n}\n\nfunction newId(): string {\n return `n${randomBytes(6).toString('hex')}`\n}\n\nexport function slugifyName(name: string): string {\n return name.trim().slice(0, 64) || 'node'\n}\n\nfunction isLowerHyphenatedId(value: string): boolean {\n return /^[a-z][a-z0-9-]*$/.test(value)\n}\n\nfunction sameName(a: string, b: string): boolean {\n const ha = createHash('sha256').update(a).digest()\n const hb = createHash('sha256').update(b).digest()\n return timingSafeEqual(ha, hb)\n}\n\nexport function dshHome(): string {\n return process.env.DSH_HOME ?? join(homedir(), '.dsh')\n}\n\nfunction join(...parts: string[]): string {\n return parts.join('/')\n}\n\nconst LOCAL_NODE_ID = 'local'\n\nexport class NodeRegistry {\n private profiles: NodeProfile[] = []\n private loaded = false\n private readonly statuses = new Map<string, NodeStatus>()\n\n constructor(private readonly ctx: Context) {}\n\n private get creds(): CredentialLike {\n return this.ctx.credentials as unknown as CredentialLike\n }\n\n private file(): string {\n return `${dshHome()}/dshb/nodes.json`\n }\n\n private load(): void {\n if (this.loaded) return\n this.loaded = true\n try {\n this.profiles = JSON.parse(readFileSync(this.file(), 'utf8')) as NodeProfile[]\n } catch {\n this.profiles = []\n }\n this.ensureLocalHost()\n }\n\n private ensureLocalHost(): void {\n const existing = this.profiles.find((p) => p.type === 'local-host')\n if (existing) {\n if (existing.name !== '默认环境') {\n existing.name = '默认环境'\n existing.updatedAt = new Date().toISOString()\n this.save()\n }\n return\n }\n const now = new Date().toISOString()\n this.profiles.unshift({\n id: LOCAL_NODE_ID,\n name: '默认环境',\n type: 'local-host',\n createdAt: now,\n updatedAt: now,\n })\n this.save()\n }\n\n private save(): void {\n const file = this.file()\n const dir = file.slice(0, file.lastIndexOf('/'))\n mkdirSync(dir, { recursive: true, mode: 0o700 })\n const tmp = `${file}.${process.pid}.tmp`\n writeFileSync(tmp, JSON.stringify(this.profiles, null, 2), { mode: 0o600 })\n renameSync(tmp, file)\n chmodSync(file, 0o600)\n }\n\n list(): NodeProfile[] {\n this.load()\n return this.profiles.map((p) => ({ ...p }))\n }\n\n get(id: string): NodeProfile | undefined {\n this.load()\n const found = this.profiles.find((p) => p.id === id)\n return found ? { ...found } : undefined\n }\n\n async hasSecret(id: string): Promise<{ hasPassword: boolean; hasKey: boolean; hasPassphrase: boolean }> {\n const key = this.credKey(id)\n if (!key) return { hasPassword: false, hasKey: false, hasPassphrase: false }\n const record = (await this.creds.readRecord(key)) as { payload?: NodeSecrets } | undefined\n const payload = record?.payload\n return {\n hasPassword: Boolean(payload?.password),\n hasKey: Boolean(payload?.privateKey),\n hasPassphrase: Boolean(payload?.passphrase),\n }\n }\n\n async getSecrets(id: string): Promise<NodeSecrets | undefined> {\n const key = this.credKey(id)\n if (!key) return undefined\n const record = (await this.creds.readRecord(key)) as { payload?: NodeSecrets } | undefined\n return record?.payload\n }\n\n async create(input: NodeCreateInput): Promise<NodeProfile> {\n return this.createProfile(input, false)\n }\n\n async createImported(input: NodeCreateInput, preferredId?: string): Promise<NodeProfile> {\n if (input.type === 'local-host' || input.type === 'local-docker') {\n throw new Error('sync imports only support remote nodes')\n }\n return this.createProfile(input, true, preferredId)\n }\n\n private async createProfile(input: NodeCreateInput, allowDuplicateName: boolean, preferredId?: string): Promise<NodeProfile> {\n this.load()\n const name = slugifyName(input.name)\n if (!allowDuplicateName && this.profiles.some((p) => sameName(p.name, name))) {\n throw new Error(`node name \"${name}\" already exists`)\n }\n const id = preferredId ?? newId()\n if (!isLowerHyphenatedId(id)) throw new Error('internal: invalid generated id')\n if (this.profiles.some((profile) => profile.id === id)) throw new Error(`node ${id} already exists`)\n const now = new Date().toISOString()\n const profile: NodeProfile = {\n id,\n name,\n type: input.type,\n ssh: input.ssh as NodeSsh | undefined,\n docker: input.docker,\n createdAt: now,\n updatedAt: now,\n }\n if (input.secrets && (input.secrets.password || input.secrets.privateKey || input.secrets.passphrase)) {\n await this.writeSecrets(id, input.secrets)\n }\n this.profiles.push(profile)\n this.save()\n return { ...profile }\n }\n\n async updateImported(id: string, input: NodeCreateInput): Promise<NodeProfile> {\n if (id === LOCAL_NODE_ID || input.type === 'local-host' || input.type === 'local-docker') {\n throw new Error('sync imports only support remote nodes')\n }\n this.load()\n const idx = this.profiles.findIndex((profile) => profile.id === id)\n if (idx < 0) throw new Error(`node ${id} not found`)\n const current = this.profiles[idx]\n const next: NodeProfile = {\n id,\n name: slugifyName(input.name),\n type: input.type,\n ssh: input.ssh as NodeSsh | undefined,\n docker: input.docker,\n createdAt: current.createdAt,\n updatedAt: new Date().toISOString(),\n }\n if (input.secrets && (input.secrets.password || input.secrets.privateKey || input.secrets.passphrase)) {\n await this.writeSecrets(id, input.secrets)\n }\n this.profiles[idx] = next\n this.statuses.delete(id)\n this.save()\n return { ...next }\n }\n\n async update(id: string, input: NodeUpdateInput): Promise<NodeProfile> {\n this.load()\n const idx = this.profiles.findIndex((p) => p.id === id)\n if (idx < 0) throw new Error(`node ${id} not found`)\n const current = this.profiles[idx]\n const next: NodeProfile = { ...current, updatedAt: new Date().toISOString() }\n if (input.name !== undefined) next.name = slugifyName(input.name)\n if (input.ssh) next.ssh = { ...current.ssh, ...input.ssh } as NodeSsh\n if (input.docker) next.docker = { ...current.docker, ...input.docker } as NodeDocker\n this.profiles[idx] = next\n if (input.secrets && (input.secrets.password || input.secrets.privateKey || input.secrets.passphrase)) {\n await this.writeSecrets(id, input.secrets)\n }\n this.save()\n return { ...next }\n }\n\n async remove(id: string): Promise<void> {\n if (id === LOCAL_NODE_ID) throw new Error('默认环境节点不可删除')\n this.load()\n const idx = this.profiles.findIndex((p) => p.id === id)\n if (idx < 0) return\n this.profiles.splice(idx, 1)\n this.statuses.delete(id)\n this.save()\n const key = this.credKey(id)\n if (key) await this.creds.deleteRecord(key)\n }\n\n async removeImported(id: string): Promise<void> {\n if (id === LOCAL_NODE_ID) throw new Error('默认环境节点不可删除')\n this.load()\n const idx = this.profiles.findIndex((p) => p.id === id)\n if (idx >= 0) {\n this.profiles.splice(idx, 1)\n this.save()\n }\n this.statuses.delete(id)\n const key = this.credKey(id)\n if (key) await this.creds.deleteRecord(key)\n }\n\n status(id: string): NodeStatus {\n return this.statuses.get(id) ?? {}\n }\n\n setStatus(id: string, status: NodeStatus): void {\n this.statuses.set(id, status)\n }\n\n private credKey(id: string): CredentialKey | undefined {\n try {\n return credentialKey('dshb-core', id)\n } catch {\n return undefined\n }\n }\n\n private async writeSecrets(id: string, secrets: NodeSecrets): Promise<void> {\n const key = this.credKey(id)\n if (!key) throw new Error('credentials service unavailable')\n await this.creds.modifyRecord(key, async () => ({\n kind: 'grant' as const,\n payload: { ...secrets },\n }))\n }\n}\n"],"mappings":"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"}
|
package/lib/core/routes.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { KnownHostsStore } from "./known-hosts.js";
|
|
2
|
-
import { NodeRegistry } from "
|
|
3
|
-
import { WorkspaceBindingsStore } from "
|
|
2
|
+
import { o as NodeRegistry } from "../node-registry-BTnnM5-k.js";
|
|
3
|
+
import { r as WorkspaceBindingsStore } from "../workspace-bindings-CsB0TIJB.js";
|
|
4
|
+
import { s as SyncNodeService } from "../node-sync-3Z9QLz2n.js";
|
|
4
5
|
import { Context } from "@deepseek-ai/cordis";
|
|
5
6
|
//#region src/core/routes.d.ts
|
|
6
7
|
interface WorkspaceRegistryLike {
|
|
@@ -9,7 +10,7 @@ interface WorkspaceRegistryLike {
|
|
|
9
10
|
title: string;
|
|
10
11
|
}>;
|
|
11
12
|
}
|
|
12
|
-
declare function registerNodeRoutes(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore, getWorkspaceRegistry?: () => WorkspaceRegistryLike | undefined): Promise<void>;
|
|
13
|
+
declare function registerNodeRoutes(ctx: Context, registry: NodeRegistry, bindings: WorkspaceBindingsStore, getWorkspaceRegistry?: () => WorkspaceRegistryLike | undefined, syncNodes?: SyncNodeService): Promise<void>;
|
|
13
14
|
declare function registerSshConfigRoutes(ctx: Context, knownHosts: KnownHostsStore): Promise<void>;
|
|
14
15
|
//#endregion
|
|
15
16
|
export { registerNodeRoutes, registerSshConfigRoutes };
|
package/lib/core/routes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveHostRunner } from "../exec-docker/host-runner.js";
|
|
2
2
|
import { readSshConfig, resolveSshConfigEntry } from "./ssh-config.js";
|
|
3
3
|
import { testNode } from "./test.js";
|
|
4
|
+
import { SyncNodeInUseError } from "./sync/node-sync.js";
|
|
4
5
|
import { mkdirSync, readdirSync } from "node:fs";
|
|
5
6
|
//#region src/core/routes.ts
|
|
6
7
|
function resolveWorlds(ctx, nodeType) {
|
|
@@ -59,7 +60,7 @@ function maybeProvision(ctx, node) {
|
|
|
59
60
|
provisioner.provision(node.id).catch(() => {});
|
|
60
61
|
return true;
|
|
61
62
|
}
|
|
62
|
-
async function registerNodeRoutes(ctx, registry, bindings, getWorkspaceRegistry) {
|
|
63
|
+
async function registerNodeRoutes(ctx, registry, bindings, getWorkspaceRegistry, syncNodes) {
|
|
63
64
|
const webServer = ctx.webServer;
|
|
64
65
|
ctx.effect(() => webServer.register({
|
|
65
66
|
kind: "prefix",
|
|
@@ -76,6 +77,14 @@ async function registerNodeRoutes(ctx, registry, bindings, getWorkspaceRegistry)
|
|
|
76
77
|
});
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
80
|
+
if (syncNodes?.isPendingRemoval(resourceId)) {
|
|
81
|
+
sendJson(res, 409, {
|
|
82
|
+
ok: false,
|
|
83
|
+
code: "node/pending-removal",
|
|
84
|
+
error: "节点等待工作区解绑后清理"
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
79
88
|
const path = new URL(req.url ?? "/", "http://x").searchParams.get("path") ?? (node.type === "local-host" ? process.env.HOME ?? "/" : "/");
|
|
80
89
|
try {
|
|
81
90
|
if (node.type === "local-host") {
|
|
@@ -129,6 +138,14 @@ async function registerNodeRoutes(ctx, registry, bindings, getWorkspaceRegistry)
|
|
|
129
138
|
});
|
|
130
139
|
return;
|
|
131
140
|
}
|
|
141
|
+
if (syncNodes?.isPendingRemoval(resourceId)) {
|
|
142
|
+
sendJson(res, 409, {
|
|
143
|
+
ok: false,
|
|
144
|
+
code: "node/pending-removal",
|
|
145
|
+
error: "节点等待工作区解绑后清理"
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
132
149
|
const body = await readBody(req);
|
|
133
150
|
const path = body && typeof body.path === "string" ? body.path : void 0;
|
|
134
151
|
if (!path) {
|
|
@@ -178,15 +195,29 @@ async function registerNodeRoutes(ctx, registry, bindings, getWorkspaceRegistry)
|
|
|
178
195
|
}
|
|
179
196
|
if (req.method === "GET" && resourceId === void 0) {
|
|
180
197
|
const provisioner = ctx.get("dshbDockerProvisioner");
|
|
198
|
+
try {
|
|
199
|
+
await syncNodes?.reconcilePendingRemovals();
|
|
200
|
+
} catch {
|
|
201
|
+
sendJson(res, 503, {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: "同步删除状态暂不可用"
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
181
207
|
const nodes = registry.list();
|
|
208
|
+
const syncViews = syncNodes?.views(nodes.map((node) => node.id));
|
|
182
209
|
sendJson(res, 200, {
|
|
183
210
|
ok: true,
|
|
184
|
-
nodes: await Promise.all(nodes.map(async (n) =>
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
211
|
+
nodes: await Promise.all(nodes.map(async (n) => {
|
|
212
|
+
const sync = syncViews?.get(n.id);
|
|
213
|
+
return {
|
|
214
|
+
...n,
|
|
215
|
+
hasSecret: await registry.hasSecret(n.id),
|
|
216
|
+
status: registry.status(n.id),
|
|
217
|
+
provision: provisioner?.status(n.id),
|
|
218
|
+
...sync ? { sync } : {}
|
|
219
|
+
};
|
|
220
|
+
}))
|
|
190
221
|
});
|
|
191
222
|
return;
|
|
192
223
|
}
|
|
@@ -332,24 +363,37 @@ async function registerNodeRoutes(ctx, registry, bindings, getWorkspaceRegistry)
|
|
|
332
363
|
return;
|
|
333
364
|
}
|
|
334
365
|
try {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
366
|
+
const removeNode = async () => {
|
|
367
|
+
if ((node.type === "local-docker" || node.type === "remote-docker") && node.docker?.containerId) {
|
|
368
|
+
const runner = await resolveHostRunner(ctx, node);
|
|
369
|
+
if (!runner) throw new Error("Docker 节点执行环境不可用,节点未删除");
|
|
370
|
+
const result = await runner.run([
|
|
371
|
+
"docker",
|
|
372
|
+
"rm",
|
|
373
|
+
"-f",
|
|
374
|
+
node.docker.containerId
|
|
375
|
+
]);
|
|
376
|
+
if (result.code !== 0) throw new Error((result.stderr || result.stdout).trim() || "删除容器失败,节点未删除");
|
|
377
|
+
}
|
|
378
|
+
await registry.remove(resourceId);
|
|
379
|
+
const sshWorlds = ctx.get("dshbSshWorlds");
|
|
380
|
+
const dockerWorlds = ctx.get("dshbDockerWorlds");
|
|
381
|
+
sshWorlds?.remove?.(resourceId);
|
|
382
|
+
dockerWorlds?.remove?.(resourceId);
|
|
383
|
+
};
|
|
384
|
+
if (syncNodes) await syncNodes.removeLocalNode(resourceId, removeNode);
|
|
385
|
+
else await removeNode();
|
|
351
386
|
sendJson(res, 200, { ok: true });
|
|
352
387
|
} catch (err) {
|
|
388
|
+
if (err instanceof SyncNodeInUseError) {
|
|
389
|
+
sendJson(res, 409, {
|
|
390
|
+
ok: false,
|
|
391
|
+
code: "node/in-use",
|
|
392
|
+
error: "节点仍被工作区使用,请先移除或迁移这些工作区。",
|
|
393
|
+
workspaces: err.workspaces
|
|
394
|
+
});
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
353
397
|
sendJson(res, 502, {
|
|
354
398
|
ok: false,
|
|
355
399
|
error: err instanceof Error ? err.message : String(err)
|