@konglx/rotom 2.21.0

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 (189) hide show
  1. package/README.md +417 -0
  2. package/bin/mesh-master.sh +439 -0
  3. package/bin/rotom +29 -0
  4. package/bin/rotom-link.sh +136 -0
  5. package/bin/rotom-send-with-status +57 -0
  6. package/bin/rotom-up.sh +428 -0
  7. package/dist/cli/ask.js +62 -0
  8. package/dist/cli/common.js +321 -0
  9. package/dist/cli/config.js +65 -0
  10. package/dist/cli/directory.js +17 -0
  11. package/dist/cli/executor.js +58 -0
  12. package/dist/cli/fed.js +91 -0
  13. package/dist/cli/group.js +273 -0
  14. package/dist/cli/identity.js +62 -0
  15. package/dist/cli/init.js +268 -0
  16. package/dist/cli/issue.js +202 -0
  17. package/dist/cli/join.js +170 -0
  18. package/dist/cli/link.js +47 -0
  19. package/dist/cli/master.js +51 -0
  20. package/dist/cli/memory.js +307 -0
  21. package/dist/cli/note.js +68 -0
  22. package/dist/cli/repo.js +77 -0
  23. package/dist/cli/rotom.js +277 -0
  24. package/dist/cli/routes.js +118 -0
  25. package/dist/cli/run.js +45 -0
  26. package/dist/cli/schedule.js +237 -0
  27. package/dist/cli/skill.js +173 -0
  28. package/dist/cli/team.js +106 -0
  29. package/dist/executor/claude-code-hook.cjs +80 -0
  30. package/dist/executor/cli-executor.js +8 -0
  31. package/dist/executor/executors/claude-code.js +780 -0
  32. package/dist/executor/executors/codex.js +719 -0
  33. package/dist/executor/executors/hermes-cli.js +855 -0
  34. package/dist/executor/executors/openclaw.js +467 -0
  35. package/dist/executor/executors/pi.js +514 -0
  36. package/dist/executor/index.js +269 -0
  37. package/dist/executor/jsonrpc-transport.js +125 -0
  38. package/dist/executor/process-runner.js +101 -0
  39. package/dist/executor/reasoning-status.js +83 -0
  40. package/dist/executor/repo-cache.js +502 -0
  41. package/dist/executor/session-store.js +188 -0
  42. package/dist/executor/worker-chat.js +257 -0
  43. package/dist/executor/worker-connection.js +89 -0
  44. package/dist/executor/worker-issue.js +264 -0
  45. package/dist/executor/worker.js +877 -0
  46. package/dist/link/pending-requests.js +72 -0
  47. package/dist/link/server.js +233 -0
  48. package/dist/link/visibility-store.js +58 -0
  49. package/dist/master/api/agents.js +333 -0
  50. package/dist/master/api/artifacts.js +271 -0
  51. package/dist/master/api/domains.js +64 -0
  52. package/dist/master/api/groups.js +635 -0
  53. package/dist/master/api/guidance-templates.js +147 -0
  54. package/dist/master/api/index.js +89 -0
  55. package/dist/master/api/issues-patrol.js +172 -0
  56. package/dist/master/api/issues.js +663 -0
  57. package/dist/master/api/links-patrol.js +168 -0
  58. package/dist/master/api/links.js +114 -0
  59. package/dist/master/api/memory.js +259 -0
  60. package/dist/master/api/messages.js +157 -0
  61. package/dist/master/api/notes.js +77 -0
  62. package/dist/master/api/schedule-patterns.js +133 -0
  63. package/dist/master/api/schedules.js +272 -0
  64. package/dist/master/api/sessions.js +158 -0
  65. package/dist/master/api/share.js +269 -0
  66. package/dist/master/api/skills.js +190 -0
  67. package/dist/master/api/teams.js +122 -0
  68. package/dist/master/api/uploads.js +245 -0
  69. package/dist/master/auth.js +134 -0
  70. package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
  71. package/dist/master/dashboard/animations/calico-error.apng +0 -0
  72. package/dist/master/dashboard/animations/calico-happy.apng +0 -0
  73. package/dist/master/dashboard/animations/calico-notification.apng +0 -0
  74. package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
  75. package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
  76. package/dist/master/dashboard/animations/calico-waking.apng +0 -0
  77. package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
  78. package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
  79. package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
  80. package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
  81. package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
  82. package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
  83. package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
  84. package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
  85. package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
  86. package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
  87. package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
  88. package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
  89. package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
  90. package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
  91. package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
  92. package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
  93. package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
  94. package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
  95. package/dist/master/dashboard/debug-auth.html +197 -0
  96. package/dist/master/dashboard/favicon.ico +0 -0
  97. package/dist/master/dashboard/index.html +20 -0
  98. package/dist/master/dashboard/rotom-avatar.png +0 -0
  99. package/dist/master/db/agent-sessions.js +60 -0
  100. package/dist/master/db/agent-visibility.js +64 -0
  101. package/dist/master/db/agents.js +119 -0
  102. package/dist/master/db/ask-bridges.js +157 -0
  103. package/dist/master/db/build-update.js +59 -0
  104. package/dist/master/db/core.js +82 -0
  105. package/dist/master/db/domains.js +80 -0
  106. package/dist/master/db/groups.js +316 -0
  107. package/dist/master/db/guidance-templates.js +58 -0
  108. package/dist/master/db/index.js +12 -0
  109. package/dist/master/db/internal.js +45 -0
  110. package/dist/master/db/issues-patrol.js +81 -0
  111. package/dist/master/db/issues.js +373 -0
  112. package/dist/master/db/links.js +221 -0
  113. package/dist/master/db/master-node.js +43 -0
  114. package/dist/master/db/memory.js +272 -0
  115. package/dist/master/db/messages.js +210 -0
  116. package/dist/master/db/notes.js +55 -0
  117. package/dist/master/db/schedule-patterns.js +56 -0
  118. package/dist/master/db/schedules.js +135 -0
  119. package/dist/master/db/skills.js +144 -0
  120. package/dist/master/db/team.js +88 -0
  121. package/dist/master/db/types.js +10 -0
  122. package/dist/master/db.js +12 -0
  123. package/dist/master/embedded.js +133 -0
  124. package/dist/master/federation/client.js +283 -0
  125. package/dist/master/federation/identity.js +133 -0
  126. package/dist/master/federation/manager.js +267 -0
  127. package/dist/master/federation/publisher.js +87 -0
  128. package/dist/master/federation/self-publisher.js +69 -0
  129. package/dist/master/federation/server.js +487 -0
  130. package/dist/master/group-paths.js +208 -0
  131. package/dist/master/offline-queue.js +38 -0
  132. package/dist/master/opc-bootstrap.js +245 -0
  133. package/dist/master/patrol-terminal.js +275 -0
  134. package/dist/master/repo-scan.js +188 -0
  135. package/dist/master/router.js +214 -0
  136. package/dist/master/scheduler-handlers.js +510 -0
  137. package/dist/master/scheduler.js +201 -0
  138. package/dist/master/server.js +203 -0
  139. package/dist/master/services/link-collector.js +82 -0
  140. package/dist/master/services/link-patrol-bootstrap.js +50 -0
  141. package/dist/master/services/memory-extract-prompt.js +34 -0
  142. package/dist/master/services/patrol-bootstrap.js +63 -0
  143. package/dist/master/share-tokens.js +56 -0
  144. package/dist/master/terminal-hub.js +300 -0
  145. package/dist/master/uploads.js +108 -0
  146. package/dist/master/util/fs.js +100 -0
  147. package/dist/master/util/paths.js +50 -0
  148. package/dist/master/util/persona.js +10 -0
  149. package/dist/master/ws-hub/connection.js +928 -0
  150. package/dist/master/ws-hub/conversation.js +290 -0
  151. package/dist/master/ws-hub/directory.js +70 -0
  152. package/dist/master/ws-hub/dispatch-enrich.js +34 -0
  153. package/dist/master/ws-hub/hub.js +136 -0
  154. package/dist/master/ws-hub/index.js +9 -0
  155. package/dist/master/ws-hub/internal.js +35 -0
  156. package/dist/master/ws-hub/routing.js +295 -0
  157. package/dist/master/ws-hub/sessions.js +130 -0
  158. package/dist/master/ws-hub.js +11 -0
  159. package/dist/shared/agent-profile.js +44 -0
  160. package/dist/shared/constants.js +55 -0
  161. package/dist/shared/dedup.js +33 -0
  162. package/dist/shared/group-context.js +62 -0
  163. package/dist/shared/json-codec.js +33 -0
  164. package/dist/shared/logger.js +136 -0
  165. package/dist/shared/mention.js +22 -0
  166. package/dist/shared/network.js +40 -0
  167. package/dist/shared/parse.js +18 -0
  168. package/dist/shared/prompt-composer.js +171 -0
  169. package/dist/shared/protocol/client-messages.js +8 -0
  170. package/dist/shared/protocol/enums.js +6 -0
  171. package/dist/shared/protocol/federation.js +62 -0
  172. package/dist/shared/protocol/guards.js +87 -0
  173. package/dist/shared/protocol/server-messages.js +8 -0
  174. package/dist/shared/protocol/types.js +8 -0
  175. package/dist/shared/protocol.js +19 -0
  176. package/dist/shared/readonly-allowlist.js +122 -0
  177. package/dist/shared/rotom-cli-prompt.js +23 -0
  178. package/dist/shared/skill-context.js +19 -0
  179. package/dist/shared/skill-md.js +43 -0
  180. package/dist/shared/slash-commands.js +50 -0
  181. package/dist/shared/time.js +80 -0
  182. package/dist/shared/title.js +46 -0
  183. package/dist/shared/url-extractor.js +99 -0
  184. package/migrations/001-schema.sql +942 -0
  185. package/package.json +68 -0
  186. package/scripts/fix-node-pty-perms.mjs +46 -0
  187. package/skill/rotom-a2a-communicate/SKILL.md +257 -0
  188. package/skill/rotom-bus-host/SKILL.md +78 -0
  189. package/skill/rotom-bus-host/scripts/poll-replies.sh +148 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Federation Publisher —— 把本机 agent 状态发布到协调 master。
3
+ *
4
+ * Phase 2 MVP:所有 online agent 都发布(用户接入部门就是为了协作)。
5
+ * Phase 3 会加 agents.published_to 字段(migration 058)+ 用户显式可见性控制。
6
+ *
7
+ * 触发:
8
+ * 1. 启动后立即发一次全量
9
+ * 2. 每 30s 全量同步一次(对齐协调 master 的 agent_visibility)
10
+ * 3. (预留)WSHub broadcastDirectory 钩子触发增量 —— Phase 3
11
+ *
12
+ * 真人 agent(profile.category="真人")的 isHuman=true,member 端 UI 可特殊渲染。
13
+ */
14
+ import { createLogger } from "../../shared/logger.js";
15
+ const log = createLogger("fed-publisher");
16
+ const PUBLISH_INTERVAL_MS = 30_000;
17
+ export class FedPublisher {
18
+ db;
19
+ client;
20
+ opts;
21
+ timer;
22
+ /** 等 client 握手成功的轮询(握手后切到正常 30s 间隔) */
23
+ waitTimer;
24
+ constructor(db, client, opts) {
25
+ this.db = db;
26
+ this.client = client;
27
+ this.opts = opts;
28
+ }
29
+ start() {
30
+ if (this.timer)
31
+ return;
32
+ log.info(`[fed-publisher] started (teamId=${this.opts.teamId}, interval=${PUBLISH_INTERVAL_MS}ms)`);
33
+ // 等 client 握手成功(client.start() 是异步,刚 start 时还没 connected)
34
+ this.waitUntilConnected();
35
+ }
36
+ waitUntilConnected() {
37
+ if (this.waitTimer)
38
+ return;
39
+ this.waitTimer = setInterval(() => {
40
+ if (this.client.isConnected()) {
41
+ clearInterval(this.waitTimer);
42
+ this.waitTimer = undefined;
43
+ // 握手成功 → 立即发一次 + 切到正常 30s 间隔
44
+ this.publishAll();
45
+ this.timer = setInterval(() => this.publishAll(), PUBLISH_INTERVAL_MS);
46
+ }
47
+ }, 1_000);
48
+ }
49
+ stop() {
50
+ if (this.timer) {
51
+ clearInterval(this.timer);
52
+ this.timer = undefined;
53
+ }
54
+ if (this.waitTimer) {
55
+ clearInterval(this.waitTimer);
56
+ this.waitTimer = undefined;
57
+ }
58
+ // 离开时撤销所有发布(让协调 master 清掉本 master 的 agent_visibility)
59
+ const agents = this.db.listAgents();
60
+ if (agents.length > 0) {
61
+ this.client.unpublish(agents.map((a) => a.name));
62
+ }
63
+ log.info("[fed-publisher] stopped");
64
+ }
65
+ /** 全量发布所有 online agent */
66
+ publishAll() {
67
+ if (!this.client.isConnected())
68
+ return;
69
+ const agents = this.db.listAgents();
70
+ if (agents.length === 0)
71
+ return;
72
+ const toPublish = agents.map((a) => {
73
+ const profile = a.profile ? JSON.parse(a.profile) : {};
74
+ const isHuman = profile.category === "真人";
75
+ return {
76
+ name: a.name,
77
+ displayName: profile.position || undefined,
78
+ isHuman,
79
+ online: a.status === "online",
80
+ };
81
+ });
82
+ const ok = this.client.publish(toPublish);
83
+ if (!ok) {
84
+ log.warn("[fed-publisher] publish skipped (client not connected or handshake pending)");
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * SelfPublisher —— 协调 master 把本机 agent 写入自己的 agent_visibility 表。
3
+ *
4
+ * 背景:FedPublisher 走 WS 把 member 的 agent 上报给协调 master,但协调
5
+ * master 自己也跑 agent(claude/codex workers 通过 /ws 接入),这些 agent
6
+ * 不会自动出现在 agent_visibility 里 —— 导致 link 节点(C)想直接对话协调
7
+ * master 上的 agent 时,FedServer.findVisibleAgentByHostAndName 查不到,
8
+ * route target not found 丢弃。
9
+ *
10
+ * 解法:协调 master 启动时也跑 SelfPublisher,定时把本机 online agent
11
+ * 用 (master_id=self.id, hostname=self.hostname) 写入 agent_visibility。
12
+ * FedServer.forwardDeliver 已有"目标是协调自己"分支(server.ts:347-354),
13
+ * 命中后走 deliverLocal → 本地 WSHub → 本机 worker,链路打通。
14
+ *
15
+ * 与 FedPublisher 的区别:
16
+ * - FedPublisher:读 db.listAgents → 走 WS(FedClient.publish) → 协调侧 UPSERT
17
+ * - SelfPublisher:读 db.listAgents → 直接 db.upsertVisibleAgent(无 WS)
18
+ */
19
+ import { createLogger } from "../../shared/logger.js";
20
+ const log = createLogger("fed-self-publisher");
21
+ const PUBLISH_INTERVAL_MS = 30_000;
22
+ export class SelfPublisher {
23
+ db;
24
+ identity;
25
+ timer;
26
+ constructor(db, identity) {
27
+ this.db = db;
28
+ this.identity = identity;
29
+ }
30
+ /** team_id 在协调侧等于 masterId(startCoordination: const teamId = identity.id) */
31
+ get teamId() {
32
+ return this.identity.id;
33
+ }
34
+ start() {
35
+ if (this.timer)
36
+ return;
37
+ log.info(`[fed-self-publisher] started (teamId=${this.teamId}, hostname=${this.identity.hostname})`);
38
+ this.publishAll();
39
+ this.timer = setInterval(() => this.publishAll(), PUBLISH_INTERVAL_MS);
40
+ }
41
+ stop() {
42
+ if (this.timer) {
43
+ clearInterval(this.timer);
44
+ this.timer = undefined;
45
+ }
46
+ // 协调 master 停止时清掉自己发布的可见 agent,避免 stale 记录
47
+ this.db.clearVisibleAgentsForMaster(this.teamId, this.identity.id);
48
+ log.info("[fed-self-publisher] stopped");
49
+ }
50
+ /** 全量同步本机 agent 到 agent_visibility */
51
+ publishAll() {
52
+ const agents = this.db.listAgents();
53
+ if (agents.length === 0)
54
+ return;
55
+ for (const a of agents) {
56
+ const profile = a.profile ? JSON.parse(a.profile) : {};
57
+ const isHuman = profile.category === "真人";
58
+ this.db.upsertVisibleAgent({
59
+ team_id: this.teamId,
60
+ master_id: this.identity.id,
61
+ agent_name: a.name,
62
+ hostname: this.identity.hostname,
63
+ display_name: profile.position ?? null,
64
+ is_human: isHuman,
65
+ online: a.status === "online",
66
+ });
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,487 @@
1
+ /**
2
+ * Federation Server —— 协调 master 端的 Federation WS server。
3
+ *
4
+ * 挂在 `/federation` 路径(与 agent 用的 `/ws` 区分),监听 member 的连接。
5
+ * 免认证但握手强制声明 masterId/hostname/role,协调侧持久化来源便于审计。
6
+ *
7
+ * 职责(Phase 2 星型):
8
+ * 1. 握手:校验 hostname 在 department 内不冲突 → fed_handshake_ack
9
+ * 2. 接收 member 的 FedAgentPublish → UPSERT agent_visibility + 广播 FedDirectorySync
10
+ * 3. 接收 member 的 FedRouteMessage → 查 team_peers 找 target master_id
11
+ * → 转 FedDeliver 投递到目标 member
12
+ * 4. 接收 member 的 FedReply → 路由回来源 member
13
+ * 5. 定期广播 FedDirectorySync(全量;Phase 3 加 diff 增量)
14
+ *
15
+ * 离线 member:消息暂存 offline_messages(扩展 target_hostname),member 重连后重投。
16
+ */
17
+ import { WebSocketServer, WebSocket } from "ws";
18
+ import { PENDING_REQUEST_TTL_MS, CLEANUP_INTERVAL_MS, } from "../../shared/constants.js";
19
+ import { FED_PROTOCOL_VERSION, isFedMessage, parseAgentRef, } from "../../shared/protocol/federation.js";
20
+ import { createLogger } from "../../shared/logger.js";
21
+ const log = createLogger("fed-server");
22
+ export class FedServer {
23
+ httpServer;
24
+ db;
25
+ opts;
26
+ wss;
27
+ peers; // masterId → ws
28
+ /** 反查:ws → masterId(用于 close 时清理) */
29
+ wsToMaster;
30
+ syncTimer;
31
+ /**
32
+ * requestId → 来源 member 的 masterId。
33
+ *
34
+ * FedRouteMessage 入口注册,FedReply 到达时按 requestId 反查 → 只发给来源 member(精确路由)。
35
+ * 取代 Phase 2 的广播兜底,避免 member 多了被 reply 噪声淹没 + 隐私泄漏。
36
+ * 失败出口(sendRouteFailed)和 reply 成功出口都 delete;TTL 兜底由 cleanupTimer 清。
37
+ */
38
+ pendingFedRequests = new Map();
39
+ cleanupTimer;
40
+ handlers = {};
41
+ /** 捕获的旧 upgrade listeners(start 时接管,非 /federation 的请求 delegate 回它们) */
42
+ delegatedUpgradeListeners = [];
43
+ upgradeHandler = null;
44
+ constructor(httpServer, db, opts) {
45
+ this.httpServer = httpServer;
46
+ this.db = db;
47
+ this.opts = opts;
48
+ this.peers = opts.peers ?? new Map();
49
+ this.wsToMaster = new Map();
50
+ // noServer 模式:手动分发 upgrade,避免与 WSHub 的 path=/ws 冲突
51
+ // (WSHub 用 WebSocketServer({ server, path: "/ws" }),其 handleUpgrade
52
+ // 会对非 /ws 路径 abortHandshake(socket, 400),破坏 /federation 握手)
53
+ this.wss = new WebSocketServer({ noServer: true });
54
+ }
55
+ start() {
56
+ // 接管 httpServer 的 upgrade 事件:匹配 /federation → 自己处理;
57
+ // 其他路径(/ws、/api/terminal)delegate 给原 listeners。
58
+ // pattern 借鉴 src/master/terminal-hub.ts。
59
+ this.delegatedUpgradeListeners = this.httpServer
60
+ .listeners("upgrade")
61
+ .slice();
62
+ this.httpServer.removeAllListeners("upgrade");
63
+ this.upgradeHandler = (req, socket, head) => {
64
+ const url = req.url || "";
65
+ if (url.includes("/federation")) {
66
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
67
+ this.wss.emit("connection", ws, req);
68
+ });
69
+ return;
70
+ }
71
+ // delegate 给原 listeners(WSHub、TerminalHub 等)
72
+ for (const listener of this.delegatedUpgradeListeners) {
73
+ listener.call(this.httpServer, req, socket, head);
74
+ }
75
+ };
76
+ this.httpServer.on("upgrade", this.upgradeHandler);
77
+ this.wss.on("connection", (ws, req) => this.handleConnection(ws, req.socket.remoteAddress));
78
+ // 30s 全量 directory sync 给所有 member
79
+ this.syncTimer = setInterval(() => this.broadcastDirectorySync(), 30_000);
80
+ // 60s 清理超时的 pendingFedRequests(对齐 Router.cleanupTimer 模式)
81
+ this.cleanupTimer = setInterval(() => this.cleanupPendingFedRequests(), CLEANUP_INTERVAL_MS);
82
+ log.info(`[fed-server] listening on /federation (teamId=${this.opts.teamId})`);
83
+ }
84
+ cleanupPendingFedRequests() {
85
+ const now = Date.now();
86
+ for (const [id, entry] of this.pendingFedRequests) {
87
+ if (now - entry.createdAt > PENDING_REQUEST_TTL_MS) {
88
+ this.pendingFedRequests.delete(id);
89
+ }
90
+ }
91
+ }
92
+ stop() {
93
+ if (this.syncTimer)
94
+ clearInterval(this.syncTimer);
95
+ if (this.cleanupTimer)
96
+ clearInterval(this.cleanupTimer);
97
+ this.pendingFedRequests.clear();
98
+ // 恢复原 listeners
99
+ if (this.upgradeHandler) {
100
+ this.httpServer.removeListener("upgrade", this.upgradeHandler);
101
+ for (const listener of this.delegatedUpgradeListeners) {
102
+ this.httpServer.on("upgrade", listener);
103
+ }
104
+ }
105
+ for (const ws of this.peers.values()) {
106
+ try {
107
+ ws.close(1000, "fed server stopping");
108
+ }
109
+ catch { /* ignore */ }
110
+ }
111
+ this.wss.close();
112
+ }
113
+ /** 注入路由处理器(由 server.ts 在 WSHub/Router 创建后调用) */
114
+ setHandlers(handlers) {
115
+ this.handlers = handlers;
116
+ }
117
+ /** 主动给指定 member 发消息 */
118
+ sendToMember(masterId, msg) {
119
+ const ws = this.peers.get(masterId);
120
+ if (!ws || ws.readyState !== WebSocket.OPEN)
121
+ return false;
122
+ ws.send(JSON.stringify(msg));
123
+ return true;
124
+ }
125
+ /** 广播 directory sync 给所有 member */
126
+ broadcastDirectorySync() {
127
+ const sync = this.buildDirectorySync();
128
+ for (const ws of this.peers.values()) {
129
+ if (ws.readyState === WebSocket.OPEN) {
130
+ ws.send(JSON.stringify(sync));
131
+ }
132
+ }
133
+ }
134
+ // ─── 内部 ─────────────────────────────────────────────────────────────────
135
+ handleConnection(ws, remoteAddr) {
136
+ log.info(`[fed-server] incoming connection from ${remoteAddr ?? "?"}`);
137
+ let authenticated = false;
138
+ let peerMasterId = null;
139
+ // 握手超时(复用 AUTH_TIMEOUT_MS 思路,10s)
140
+ const timeout = setTimeout(() => {
141
+ if (!authenticated) {
142
+ try {
143
+ ws.close(4401, "fed handshake timeout");
144
+ }
145
+ catch { /* ignore */ }
146
+ }
147
+ }, 10_000);
148
+ ws.on("message", (raw) => {
149
+ let msg;
150
+ try {
151
+ const parsed = JSON.parse(raw.toString());
152
+ if (!isFedMessage(parsed)) {
153
+ log.warn(`[fed-server] invalid fed message from ${remoteAddr}`);
154
+ ws.close(4400, "invalid fed message");
155
+ return;
156
+ }
157
+ msg = parsed;
158
+ }
159
+ catch {
160
+ ws.close(4400, "invalid JSON");
161
+ return;
162
+ }
163
+ if (!authenticated) {
164
+ if (msg.type !== "fed_handshake") {
165
+ ws.close(4401, "fed handshake required");
166
+ return;
167
+ }
168
+ const result = this.handleHandshake(ws, msg);
169
+ if (!result.accepted) {
170
+ // handleHandshake 已发 ack + close
171
+ return;
172
+ }
173
+ authenticated = true;
174
+ peerMasterId = result.masterId;
175
+ clearTimeout(timeout);
176
+ return;
177
+ }
178
+ // 已认证消息分发
179
+ this.handleFedMessage(msg, peerMasterId).catch((err) => {
180
+ log.error(`[fed-server] handle ${msg.type} error:`, err);
181
+ });
182
+ });
183
+ ws.on("close", () => {
184
+ clearTimeout(timeout);
185
+ if (peerMasterId) {
186
+ this.peers.delete(peerMasterId);
187
+ this.wsToMaster.delete(ws);
188
+ // 把该 member 在 agent_visibility 里的记录全部标 offline(不删,等重连)
189
+ this.db.setVisibleOnline(this.opts.teamId, peerMasterId, "*", false);
190
+ // setVisibleOnline 不支持通配符 — 改为遍历
191
+ this.markAllAgentsOffline(peerMasterId);
192
+ log.info(`[fed-server] peer ${peerMasterId} disconnected`);
193
+ }
194
+ });
195
+ }
196
+ handleHandshake(ws, msg) {
197
+ const ack = {
198
+ type: "fed_handshake_ack",
199
+ teamId: this.opts.teamId,
200
+ accepted: false,
201
+ serverMasterId: this.opts.identity.id,
202
+ serverHostname: this.opts.identity.hostname,
203
+ };
204
+ if (msg.protocol !== FED_PROTOCOL_VERSION) {
205
+ ack.error = "PROTOCOL_MISMATCH";
206
+ ws.send(JSON.stringify(ack));
207
+ ws.close(4402, "protocol mismatch");
208
+ return { accepted: false };
209
+ }
210
+ if (msg.role !== "member" && msg.role !== "coordination") {
211
+ ack.error = "ROLE_MISMATCH";
212
+ ws.send(JSON.stringify(ack));
213
+ ws.close(4403, "role mismatch");
214
+ return { accepted: false };
215
+ }
216
+ // hostname 冲突检测:department 内是否已有同 hostname 的 peer
217
+ const conflict = this.db.findPeerByHostname(this.opts.teamId, msg.hostname);
218
+ if (conflict && conflict.master_id !== msg.masterId) {
219
+ ack.error = "HOSTNAME_CONFLICT";
220
+ ws.send(JSON.stringify(ack));
221
+ ws.close(4404, "hostname conflict");
222
+ log.warn(`[fed-server] reject ${msg.masterId}: hostname "${msg.hostname}" already taken by ${conflict.master_id}`);
223
+ return { accepted: false };
224
+ }
225
+ // 注册 peer
226
+ this.db.upsertPeer({
227
+ team_id: this.opts.teamId,
228
+ master_id: msg.masterId,
229
+ hostname: msg.hostname,
230
+ role: msg.role,
231
+ });
232
+ this.peers.set(msg.masterId, ws);
233
+ this.wsToMaster.set(ws, msg.masterId);
234
+ ack.accepted = true;
235
+ ws.send(JSON.stringify(ack));
236
+ log.info(`[fed-server] peer joined: ${msg.masterId} (${msg.hostname}, ${msg.role})`);
237
+ // 握手后立即推一次 directory(让新 member 看到现有成员)
238
+ const sync = this.buildDirectorySync();
239
+ ws.send(JSON.stringify(sync));
240
+ // 重投该 member 离线期间暂存的 fed 消息(Phase 3 离线暂存重投)
241
+ this.replayOfflineFedMessages(msg.masterId);
242
+ return { accepted: true, masterId: msg.masterId };
243
+ }
244
+ /**
245
+ * 把 member 离线期间暂存的 FedDeliver 批量重投过去。
246
+ *
247
+ * 暂存由 forwardDeliver 在 target member 离线时调 enqueueFedOffline 写入。
248
+ * member 重连(握手成功)后立即重投 —— 顺序按 created_at,最早的先投。
249
+ * 单条重投失败(WS 又断了)→ 不回暂存,丢了;后续 reply 路由会 TTL 超时,member 端 PendingRequests 也同步超时。
250
+ */
251
+ replayOfflineFedMessages(masterId) {
252
+ const rows = this.db.popFedOfflineByMaster(masterId);
253
+ if (rows.length === 0)
254
+ return;
255
+ log.info(`[fed-server] replaying ${rows.length} offline fed messages to ${masterId}`);
256
+ for (const row of rows) {
257
+ try {
258
+ const deliver = JSON.parse(row.payload);
259
+ const ok = this.sendToMember(masterId, deliver);
260
+ if (!ok) {
261
+ log.warn(`[fed-server] replay deliver failed (member WS gone again?) requestId=${deliver.requestId}`);
262
+ break; // member 又断了,后面的也不投了
263
+ }
264
+ }
265
+ catch (e) {
266
+ log.warn(`[fed-server] replay parse failed for offline id=${row.id}: ${e.message}`);
267
+ }
268
+ }
269
+ }
270
+ async handleFedMessage(msg, fromMasterId) {
271
+ switch (msg.type) {
272
+ case "fed_agent_publish":
273
+ return this.handleAgentPublish(msg, fromMasterId);
274
+ case "fed_agent_unpublish":
275
+ return this.handleAgentUnpublish(msg);
276
+ case "fed_route":
277
+ return this.handleRouteMessage(msg, fromMasterId);
278
+ case "fed_reply":
279
+ return this.handleRouteReply(msg);
280
+ case "fed_deliver":
281
+ // 协调 master 不应该收到 fed_deliver(那是给 member 的)
282
+ log.warn(`[fed-server] unexpected fed_deliver from ${fromMasterId}`);
283
+ return;
284
+ case "fed_directory_sync":
285
+ // member 不会发这个给协调;忽略
286
+ return;
287
+ default:
288
+ log.warn(`[fed-server] unhandled fed message type: ${msg.type}`);
289
+ }
290
+ }
291
+ handleAgentPublish(msg, _fromMasterId) {
292
+ // msg.masterId 是 member 自报的(信任),_fromMasterId 是 ws 关联的(权威)
293
+ // 实际生产应该用 _fromMasterId 防伪造;Phase 2 简化用 msg.masterId
294
+ for (const a of msg.agents) {
295
+ this.db.upsertVisibleAgent({
296
+ team_id: msg.teamId,
297
+ master_id: msg.masterId,
298
+ agent_name: a.name,
299
+ hostname: a.hostname,
300
+ display_name: a.displayName,
301
+ is_human: a.isHuman,
302
+ online: a.online,
303
+ });
304
+ }
305
+ // 通知其他 member(增量 sync 简化为全量,因为 Phase 2 简单)
306
+ this.broadcastDirectorySync();
307
+ }
308
+ handleAgentUnpublish(msg) {
309
+ for (const a of msg.agents) {
310
+ this.db.removeVisibleAgent(msg.teamId, msg.masterId, a.name);
311
+ }
312
+ this.broadcastDirectorySync();
313
+ }
314
+ handleRouteMessage(msg, fromMasterId) {
315
+ // 记录来源 member,供 FedReply 精确路由回来源(取代广播)
316
+ this.pendingFedRequests.set(msg.requestId, { sourceMasterId: fromMasterId, createdAt: Date.now() });
317
+ // 查目标 agent 的 master_id(从 agent_visibility)
318
+ const visible = this.db.findVisibleAgentByHostAndName(msg.teamId, msg.to.hostname, msg.to.name);
319
+ if (!visible) {
320
+ // 也可能目标 name 在 department 内唯一(不带 hostname)
321
+ const candidates = this.db.findVisibleAgentsByName(msg.teamId, msg.to.name);
322
+ if (candidates.length === 1) {
323
+ this.forwardDeliver(candidates[0].master_id, msg);
324
+ return;
325
+ }
326
+ log.warn(`[fed-server] route target not found: ${msg.to.name}@${msg.to.hostname}`);
327
+ this.sendRouteFailed(fromMasterId, msg, "NOT_FOUND");
328
+ return;
329
+ }
330
+ this.forwardDeliver(visible.master_id, msg);
331
+ }
332
+ /**
333
+ * 给发起方 member / link 回 FedRouteFailed。
334
+ *
335
+ * 何时调用:
336
+ * - handleRouteMessage 找不到目标 agent(NOT_FOUND)
337
+ * - (后续)forwardDeliver 暂存失败或目标永久不可达(OFFLINE_DROPPED,Phase 4)
338
+ * 成功发送后删 pendingFedRequests entry(避免 reply 来时再发一次)。
339
+ * 来源 member 离线时 sendToMember 失败 → 静默丢弃(member 端 PendingRequests 自己 TTL 超时)。
340
+ */
341
+ sendRouteFailed(targetMasterId, route, reason) {
342
+ const msg = {
343
+ type: "fed_route_failed",
344
+ requestId: route.requestId,
345
+ reason,
346
+ from: route.from,
347
+ to: route.to,
348
+ };
349
+ const ok = this.sendToMember(targetMasterId, msg);
350
+ if (!ok) {
351
+ log.warn(`[fed-server] route_failed delivery to ${targetMasterId} failed (offline?), requestId=${route.requestId}`);
352
+ }
353
+ this.pendingFedRequests.delete(route.requestId);
354
+ }
355
+ forwardDeliver(targetMasterId, route) {
356
+ // 目标是协调自己?
357
+ if (targetMasterId === this.opts.identity.id) {
358
+ const deliver = {
359
+ type: "fed_deliver",
360
+ requestId: route.requestId,
361
+ from: route.from,
362
+ to: route.to,
363
+ payload: route.payload,
364
+ conversation: route.conversation,
365
+ };
366
+ const ok = this.handlers.deliverLocal?.(deliver) ?? false;
367
+ if (!ok)
368
+ log.warn(`[fed-server] local deliver failed for requestId=${route.requestId}`);
369
+ return;
370
+ }
371
+ // 转发到目标 member
372
+ const deliver = {
373
+ type: "fed_deliver",
374
+ requestId: route.requestId,
375
+ from: route.from,
376
+ to: route.to,
377
+ payload: route.payload,
378
+ conversation: route.conversation,
379
+ };
380
+ const ok = this.sendToMember(targetMasterId, deliver);
381
+ if (!ok) {
382
+ // 目标 member 离线 → 暂存到 offline_messages(member 重连时 replay)
383
+ const queued = this.db.enqueueFedOffline({
384
+ target_master_id: targetMasterId,
385
+ target_hostname: route.to.hostname,
386
+ target_agent: route.to.name,
387
+ source_master_id: this.opts.identity.id, // coord 自己记的 pendingFedRequests 用
388
+ source_hostname: route.from.hostname,
389
+ source_agent: route.from.name,
390
+ payload: JSON.stringify(deliver),
391
+ request_id: route.requestId,
392
+ });
393
+ if (queued) {
394
+ log.info(`[fed-server] target member ${targetMasterId} offline, queued requestId=${route.requestId} (will replay on reconnect)`);
395
+ }
396
+ else {
397
+ // per-member 100 条上限 → 丢老消息
398
+ log.warn(`[fed-server] offline queue full for member ${targetMasterId}, dropping requestId=${route.requestId}`);
399
+ }
400
+ // 注意:pendingFedRequests 不在这里删 —— member 重投成功后会回 FedReply,届时再删。
401
+ // route_failed 才删。
402
+ }
403
+ }
404
+ handleRouteReply(msg) {
405
+ // 精确路由:按 requestId 反查来源 member,只发给来源(取代广播兜底)
406
+ const entry = this.pendingFedRequests.get(msg.requestId);
407
+ if (!entry) {
408
+ // TTL 超时 / 重复 reply / 协调重启后丢了 → 兜底广播,member 端没 pending 就忽略
409
+ log.warn(`[fed-server] reply for unknown requestId=${msg.requestId} (TTL'd or already routed), broadcasting as fallback`);
410
+ this.broadcastReply(msg);
411
+ return;
412
+ }
413
+ const ok = this.sendToMember(entry.sourceMasterId, msg);
414
+ if (!ok) {
415
+ log.warn(`[fed-server] reply delivery to source ${entry.sourceMasterId} failed (offline?) requestId=${msg.requestId}`);
416
+ // 来源 member 离线 → 没法投递 reply。删 entry,member 端会自己 TTL 超时。
417
+ // 不再广播兜底(给其他 member 投也是噪声)。
418
+ }
419
+ this.pendingFedRequests.delete(msg.requestId);
420
+ }
421
+ /**
422
+ * 协调 master 把 FedReply 发给"发起方 member"。
423
+ *
424
+ * 由 FederationManager.fedReplyHook 调用:本机 agent 给一个 federated 请求回了消息,
425
+ * 把 reply 转回发起方。Phase 3 改为精确路由(查 pendingFedRequests 找 sourceMasterId);
426
+ * 若查不到(TTL 超时 / 协调重启后丢了),兜底广播。
427
+ */
428
+ sendReply(requestId, from, payload) {
429
+ const msg = {
430
+ type: "fed_reply",
431
+ requestId,
432
+ from,
433
+ payload,
434
+ };
435
+ const entry = this.pendingFedRequests.get(requestId);
436
+ if (!entry) {
437
+ log.warn(`[fed-server] sendReply: unknown requestId=${requestId} (TTL'd or coord restart), broadcasting as fallback`);
438
+ this.broadcastReply(msg);
439
+ return;
440
+ }
441
+ const ok = this.sendToMember(entry.sourceMasterId, msg);
442
+ if (!ok) {
443
+ log.warn(`[fed-server] sendReply to source ${entry.sourceMasterId} failed (offline?) requestId=${requestId}`);
444
+ }
445
+ this.pendingFedRequests.delete(requestId);
446
+ }
447
+ broadcastReply(msg) {
448
+ for (const [masterId, ws] of this.peers) {
449
+ if (ws.readyState === WebSocket.OPEN) {
450
+ ws.send(JSON.stringify(msg));
451
+ }
452
+ else {
453
+ this.peers.delete(masterId);
454
+ }
455
+ }
456
+ }
457
+ buildDirectorySync() {
458
+ const rows = this.db.listVisibleAgents(this.opts.teamId);
459
+ return {
460
+ type: "fed_directory_sync",
461
+ teamId: this.opts.teamId,
462
+ upsert: rows.map((r) => ({
463
+ masterId: r.master_id,
464
+ hostname: r.hostname,
465
+ name: r.agent_name,
466
+ displayName: r.display_name ?? undefined,
467
+ isHuman: r.is_human !== 0,
468
+ online: r.online !== 0,
469
+ lastHeartbeat: r.last_heartbeat ?? undefined,
470
+ })),
471
+ remove: [],
472
+ };
473
+ }
474
+ markAllAgentsOffline(masterId) {
475
+ // 简化:遍历该 master 的所有 visible agent,标 offline
476
+ const rows = this.db.listVisibleAgents(this.opts.teamId);
477
+ for (const r of rows) {
478
+ if (r.master_id === masterId && r.online === 1) {
479
+ this.db.setVisibleOnline(this.opts.teamId, masterId, r.agent_name, false);
480
+ }
481
+ }
482
+ }
483
+ }
484
+ /** 给 ws-hub 或 router 用:解析 "alice@hostB" 或裸 "alice" */
485
+ export function resolveAgentRef(ref) {
486
+ return parseAgentRef(ref);
487
+ }