@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.
Files changed (101) hide show
  1. package/lib/client.js +2157 -567
  2. package/lib/client.js.map +1 -1
  3. package/lib/core/index.js +72 -4
  4. package/lib/core/index.js.map +1 -1
  5. package/lib/core/node-registry.d.ts +2 -108
  6. package/lib/core/node-registry.js +43 -2
  7. package/lib/core/node-registry.js.map +1 -1
  8. package/lib/core/routes.d.ts +4 -3
  9. package/lib/core/routes.js +67 -23
  10. package/lib/core/routes.js.map +1 -1
  11. package/lib/core/sync/coordinator.d.ts +48 -0
  12. package/lib/core/sync/coordinator.js +247 -0
  13. package/lib/core/sync/coordinator.js.map +1 -0
  14. package/lib/core/sync/host-key.d.ts +2 -0
  15. package/lib/core/sync/host-key.js +71 -0
  16. package/lib/core/sync/host-key.js.map +1 -0
  17. package/lib/core/sync/internal-routes.d.ts +9 -0
  18. package/lib/core/sync/internal-routes.js +236 -0
  19. package/lib/core/sync/internal-routes.js.map +1 -0
  20. package/lib/core/sync/model.d.ts +2 -0
  21. package/lib/core/sync/model.js +106 -0
  22. package/lib/core/sync/model.js.map +1 -0
  23. package/lib/core/sync/node-sync.d.ts +2 -0
  24. package/lib/core/sync/node-sync.js +660 -0
  25. package/lib/core/sync/node-sync.js.map +1 -0
  26. package/lib/core/sync/offline-archive.d.ts +55 -0
  27. package/lib/core/sync/offline-archive.js +673 -0
  28. package/lib/core/sync/offline-archive.js.map +1 -0
  29. package/lib/core/sync/offline-journal.d.ts +22 -0
  30. package/lib/core/sync/offline-journal.js +74 -0
  31. package/lib/core/sync/offline-journal.js.map +1 -0
  32. package/lib/core/sync/offline-routes.d.ts +7 -0
  33. package/lib/core/sync/offline-routes.js +192 -0
  34. package/lib/core/sync/offline-routes.js.map +1 -0
  35. package/lib/core/sync/peer-manager.d.ts +56 -0
  36. package/lib/core/sync/peer-manager.js +258 -0
  37. package/lib/core/sync/peer-manager.js.map +1 -0
  38. package/lib/core/sync/protocol.d.ts +2 -0
  39. package/lib/core/sync/protocol.js +124 -0
  40. package/lib/core/sync/protocol.js.map +1 -0
  41. package/lib/core/sync/remote-helper.d.ts +5 -0
  42. package/lib/core/sync/remote-helper.js +160 -0
  43. package/lib/core/sync/remote-helper.js.map +1 -0
  44. package/lib/core/sync/revision.d.ts +2 -0
  45. package/lib/core/sync/revision.js +77 -0
  46. package/lib/core/sync/revision.js.map +1 -0
  47. package/lib/core/sync/routes.d.ts +9 -0
  48. package/lib/core/sync/routes.js +339 -0
  49. package/lib/core/sync/routes.js.map +1 -0
  50. package/lib/core/sync/scheduler.d.ts +14 -0
  51. package/lib/core/sync/scheduler.js +81 -0
  52. package/lib/core/sync/scheduler.js.map +1 -0
  53. package/lib/core/sync/session-journal.d.ts +2 -0
  54. package/lib/core/sync/session-journal.js +191 -0
  55. package/lib/core/sync/session-journal.js.map +1 -0
  56. package/lib/core/sync/session-routes.d.ts +7 -0
  57. package/lib/core/sync/session-routes.js +67 -0
  58. package/lib/core/sync/session-routes.js.map +1 -0
  59. package/lib/core/sync/session-sync.d.ts +2 -0
  60. package/lib/core/sync/session-sync.js +2 -0
  61. package/lib/core/sync/ssh-transport.d.ts +2 -0
  62. package/lib/core/sync/ssh-transport.js +408 -0
  63. package/lib/core/sync/ssh-transport.js.map +1 -0
  64. package/lib/core/sync/stores.d.ts +3 -0
  65. package/lib/core/sync/stores.js +385 -0
  66. package/lib/core/sync/stores.js.map +1 -0
  67. package/lib/core/test.d.ts +1 -1
  68. package/lib/core/workspace-bindings.d.ts +2 -28
  69. package/lib/core/workspace-bindings.js +14 -14
  70. package/lib/core/workspace-bindings.js.map +1 -1
  71. package/lib/core/workspace-instruction-context.d.ts +3 -2260
  72. package/lib/core/workspace-instruction-context.js +104 -2
  73. package/lib/core/workspace-instruction-context.js.map +1 -0
  74. package/lib/core/workspace-instruction-routes.d.ts +1 -1
  75. package/lib/core/workspace-instructions.d.ts +2 -68
  76. package/lib/core/workspace-instructions.js +13 -0
  77. package/lib/core/workspace-instructions.js.map +1 -1
  78. package/lib/core/workspaces.d.ts +4 -3
  79. package/lib/core/workspaces.js +17 -1
  80. package/lib/core/workspaces.js.map +1 -1
  81. package/lib/host-key-C5afhAUd.d.ts +30 -0
  82. package/lib/index-VF8bcBeX.d.ts +2293 -0
  83. package/lib/{workspace-instruction-context-DytAmaPY.js → lib-gdtU_WKT.js} +159 -102
  84. package/lib/lib-gdtU_WKT.js.map +1 -0
  85. package/lib/model-BP7Gw1yk.d.ts +104 -0
  86. package/lib/node-registry-BTnnM5-k.d.ts +112 -0
  87. package/lib/node-sync-3Z9QLz2n.d.ts +89 -0
  88. package/lib/protocol-CV7LGE44.d.ts +35 -0
  89. package/lib/revision-C9sOlDZ6.d.ts +25 -0
  90. package/lib/router/router-fs.d.ts +12 -3
  91. package/lib/router/router-fs.js.map +1 -1
  92. package/lib/session-journal-pgPqcWn9.d.ts +40 -0
  93. package/lib/session-sync-EdfwUON5.js +2177 -0
  94. package/lib/session-sync-EdfwUON5.js.map +1 -0
  95. package/lib/session-sync-g6qKZzjV.d.ts +1252 -0
  96. package/lib/ssh-transport-BcIiLxsX.d.ts +55 -0
  97. package/lib/stores-CKyEt0yk.d.ts +31 -0
  98. package/lib/workspace-bindings-CsB0TIJB.d.ts +28 -0
  99. package/lib/workspace-instructions-DirYYiku.d.ts +69 -0
  100. package/package.json +20 -1
  101. 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 { a as registerWorkspaceInstructionInjector } from "../workspace-instruction-context-DytAmaPY.js";
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);
@@ -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 { CredentialKey } from "@deepseek-ai/dsh-credentials";
2
- import { Context } from "@deepseek-ai/cordis";
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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"}
@@ -1,6 +1,7 @@
1
1
  import { KnownHostsStore } from "./known-hosts.js";
2
- import { NodeRegistry } from "./node-registry.js";
3
- import { WorkspaceBindingsStore } from "./workspace-bindings.js";
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 };
@@ -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
- ...n,
186
- hasSecret: await registry.hasSecret(n.id),
187
- status: registry.status(n.id),
188
- provision: provisioner?.status(n.id)
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
- if ((node.type === "local-docker" || node.type === "remote-docker") && node.docker?.containerId) {
336
- const runner = await resolveHostRunner(ctx, node);
337
- if (!runner) throw new Error("Docker 节点执行环境不可用,节点未删除");
338
- const result = await runner.run([
339
- "docker",
340
- "rm",
341
- "-f",
342
- node.docker.containerId
343
- ]);
344
- if (result.code !== 0) throw new Error((result.stderr || result.stdout).trim() || "删除容器失败,节点未删除");
345
- }
346
- await registry.remove(resourceId);
347
- const sshWorlds = ctx.get("dshbSshWorlds");
348
- const dockerWorlds = ctx.get("dshbDockerWorlds");
349
- sshWorlds?.remove?.(resourceId);
350
- dockerWorlds?.remove?.(resourceId);
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)