@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,201 @@
1
+ import { toBeijing } from "../shared/time.js";
2
+ /**
3
+ * Scheduler — 群内定时任务调度器
4
+ *
5
+ * 设计要点(参考 hermes-agent cron/scheduler.py + cron/jobs.py,简化后落到 rotom):
6
+ *
7
+ * - `next_run_at` 字段驱动:不靠 `last_run_at + interval` 算,而是显式维护下次运行时间。
8
+ * - schedule 两形态:
9
+ * - schedule_kind='interval' + interval_sec: 每 N 秒跑一次
10
+ * - schedule_kind='once' + run_at: 在指定时间戳跑一次,跑完自动 enabled=0
11
+ * - grace window 防宕机堆积:Master 宕机后重启,如果 now - next_run_at > grace 就
12
+ * fast-forward 到下一个未来时间点,不补跑历史。recurring 用 `computeGraceSec`
13
+ * (max(120, min(interval_sec/2, 7200)));一次性任务用 ONESHOT_GRACE_SEC=120。
14
+ * - at-most-once:执行前先把 next_run_at 推进到下一个时间点,再派 Issue / 发消息,
15
+ * 崩溃后重启不会重跑。
16
+ * - 两种触发模式:
17
+ * - mode='agent': 创建 Issue + hub.pushIssueAssignment(group_id, agent_name),
18
+ * agent 离线或上一轮 Issue 仍 in_progress 就跳过,但 next_run_at 仍推进。
19
+ * - mode='message': 直接调 hub.postSystemToGroup(group_id, prompt),无需 agent。
20
+ * - 串行 tick:30s 一次,无需并行池;Issue 在 worker 进程跑,不阻塞 scheduler。
21
+ *
22
+ * 不在本期:
23
+ * - tryClaimNextIssue 的 poller(plan 已选 push 路径)
24
+ * - Dashboard UI / WS 协议变更 / 跨 Master 协调 / cron 表达式 / file lock
25
+ */
26
+ import { randomUUID } from "node:crypto";
27
+ import { resolveGroupAgentWorkingDir } from "./group-paths.js";
28
+ import { createLogger } from "../shared/logger.js";
29
+ import { getSchedulerHandler } from "./scheduler-handlers.js";
30
+ const log = createLogger("mesh-scheduler");
31
+ /** 调度器扫描周期。每 20s 看一眼:扫 scheduled_tasks(含 ask-bridge handler)。 */
32
+ const TICK_MS = 20_000;
33
+ /** 一次性任务的 grace window,固定 120s。 */
34
+ const ONESHOT_GRACE_SEC = 120;
35
+ /** recurring 任务 grace 下界 / 上界。 */
36
+ const MIN_GRACE_SEC = 120;
37
+ const MAX_GRACE_SEC = 7200;
38
+ function computeGraceSec(intervalSec) {
39
+ return Math.max(MIN_GRACE_SEC, Math.min(Math.floor(intervalSec / 2), MAX_GRACE_SEC));
40
+ }
41
+ export class Scheduler {
42
+ db;
43
+ hub;
44
+ timer = null;
45
+ ticking = false;
46
+ constructor(db, hub) {
47
+ this.db = db;
48
+ this.hub = hub;
49
+ }
50
+ start() {
51
+ if (this.timer)
52
+ return;
53
+ log.info(`Scheduler started (tick=${TICK_MS}ms)`);
54
+ this.timer = setInterval(() => this.tick().catch((err) => log.error("scheduler tick failed", err)), TICK_MS);
55
+ // 启动时立刻扫一次,避免冷启动后等满 30s
56
+ this.tick().catch((err) => log.error("scheduler initial tick failed", err));
57
+ }
58
+ stop() {
59
+ if (this.timer) {
60
+ clearInterval(this.timer);
61
+ this.timer = null;
62
+ }
63
+ log.info("Scheduler stopped");
64
+ }
65
+ async tick() {
66
+ if (this.ticking)
67
+ return; // 上一轮还没跑完,跳过(避免 tick 重叠)
68
+ this.ticking = true;
69
+ try {
70
+ const now = Date.now();
71
+ const due = this.db.getDueScheduledTasks(now);
72
+ if (due.length === 0)
73
+ return;
74
+ log.info(`tick: ${due.length} task(s) due`);
75
+ for (const task of due) {
76
+ await this.runOne(task, now);
77
+ }
78
+ }
79
+ finally {
80
+ this.ticking = false;
81
+ }
82
+ }
83
+ async runOne(task, now) {
84
+ // 1. 宕机堆积保护:recurring 任务错过超过 grace 窗口,fast-forward 不补跑
85
+ if (task.schedule_kind === "interval") {
86
+ const graceSec = computeGraceSec(task.interval_sec);
87
+ if (now - task.next_run_at > graceSec * 1000) {
88
+ const next = now + task.interval_sec * 1000;
89
+ this.db.rescheduleTask(task.id, next);
90
+ log.info(`task #${task.id} "${task.name}" stale, fast-forward to ${toBeijing(next)}`);
91
+ return;
92
+ }
93
+ }
94
+ else if (task.schedule_kind === "once") {
95
+ // 一次性任务用更小的 grace;过了太久没跑就当过期,直接 disable
96
+ if (now - task.next_run_at > ONESHOT_GRACE_SEC * 1000) {
97
+ this.db.disableScheduledTask(task.id);
98
+ log.info(`task #${task.id} "${task.name}" oneshot expired, disabled`);
99
+ return;
100
+ }
101
+ }
102
+ // 2. at-most-once:先把 next_run_at 推进,再执行。崩溃后重启不会重跑
103
+ const nextRun = this.computeNextRun(task, now);
104
+ if (nextRun !== null) {
105
+ this.db.rescheduleTask(task.id, nextRun);
106
+ }
107
+ const newRepeatCount = task.repeat_count + 1;
108
+ try {
109
+ // handler 模式:跑硬编码逻辑(ask-bridge-check 等),不走 prompt/agent 路径
110
+ if (task.handler_key) {
111
+ const handler = getSchedulerHandler(task.handler_key);
112
+ if (!handler) {
113
+ this.db.markScheduledTaskRun(task.id, now, "error", `unknown handler: ${task.handler_key}`, null, newRepeatCount);
114
+ log.error(`task #${task.id} "${task.name}" unknown handler: ${task.handler_key}`);
115
+ this.autoDisableIfDone(task, newRepeatCount);
116
+ return;
117
+ }
118
+ let payload;
119
+ try {
120
+ payload = task.handler_payload ? JSON.parse(task.handler_payload) : {};
121
+ }
122
+ catch (e) {
123
+ this.db.markScheduledTaskRun(task.id, now, "error", `bad handler_payload JSON: ${e.message}`, null, newRepeatCount);
124
+ log.error(`task #${task.id} "${task.name}" bad handler_payload`, e);
125
+ this.autoDisableIfDone(task, newRepeatCount);
126
+ return;
127
+ }
128
+ const result = await handler(payload, { db: this.db, hub: this.hub });
129
+ this.db.markScheduledTaskRun(task.id, now, result.status, result.error ?? null, result.issueId ?? null, newRepeatCount);
130
+ log.info(`task #${task.id} "${task.name}" handler "${task.handler_key}" → ${result.status}${result.issueId ? ` (issue ${result.issueId})` : ""}`);
131
+ this.autoDisableIfDone(task, newRepeatCount);
132
+ return;
133
+ }
134
+ if (task.mode === "message") {
135
+ this.hub.postSystemToGroup(task.group_id, task.prompt);
136
+ this.db.markScheduledTaskRun(task.id, now, "ok", null, null, newRepeatCount);
137
+ log.info(`task #${task.id} "${task.name}" message posted to group ${task.group_id}`);
138
+ }
139
+ else {
140
+ // agent 模式:防堆积,上一轮 Issue 仍 in_progress 就跳过(但 next_run_at 已推进)
141
+ if (task.last_issue_id) {
142
+ const prev = this.db.getIssueById(task.last_issue_id);
143
+ if (prev && prev.status === "in_progress") {
144
+ this.db.markScheduledTaskRun(task.id, now, "skipped", "prev issue in_progress", null, newRepeatCount);
145
+ log.info(`task #${task.id} "${task.name}" skipped: prev issue still in_progress`);
146
+ this.autoDisableIfDone(task, newRepeatCount);
147
+ return;
148
+ }
149
+ }
150
+ const agent = this.db.getAgentByName(task.agent_name);
151
+ if (!agent || agent.status !== "online") {
152
+ const reason = !agent ? "agent not found" : "agent offline";
153
+ this.db.markScheduledTaskRun(task.id, now, "skipped", reason, null, newRepeatCount);
154
+ log.info(`task #${task.id} "${task.name}" skipped: ${reason}`);
155
+ this.autoDisableIfDone(task, newRepeatCount);
156
+ return;
157
+ }
158
+ const issueId = randomUUID();
159
+ this.db.createIssue({
160
+ id: issueId,
161
+ groupId: task.group_id,
162
+ title: `[定时] ${task.name}`,
163
+ description: task.prompt,
164
+ createdBy: "system:scheduler",
165
+ workingDir: resolveGroupAgentWorkingDir(this.db, task.group_id, task.agent_name),
166
+ assignedTo: task.agent_name,
167
+ });
168
+ const pushed = this.hub.pushIssueAssignment(issueId, task.agent_name);
169
+ if (!pushed) {
170
+ this.db.markScheduledTaskRun(task.id, now, "error", "pushIssueAssignment failed", null, newRepeatCount);
171
+ log.warn(`task #${task.id} "${task.name}" push failed`);
172
+ this.autoDisableIfDone(task, newRepeatCount);
173
+ return;
174
+ }
175
+ this.db.markScheduledTaskRun(task.id, now, "ok", null, issueId, newRepeatCount);
176
+ log.info(`task #${task.id} "${task.name}" dispatched issue ${issueId} → ${task.agent_name}`);
177
+ }
178
+ this.autoDisableIfDone(task, newRepeatCount);
179
+ }
180
+ catch (err) {
181
+ this.db.markScheduledTaskRun(task.id, now, "error", String(err?.message ?? err), null, newRepeatCount);
182
+ log.error(`task #${task.id} "${task.name}" threw`, err);
183
+ this.autoDisableIfDone(task, newRepeatCount);
184
+ }
185
+ }
186
+ /** 一次性任务或 repeat_times 用尽,自动 enabled=0。 */
187
+ autoDisableIfDone(task, newCount) {
188
+ if (task.schedule_kind === "once") {
189
+ this.db.disableScheduledTask(task.id);
190
+ }
191
+ else if (task.repeat_times != null && newCount >= task.repeat_times) {
192
+ this.db.disableScheduledTask(task.id);
193
+ log.info(`task #${task.id} "${task.name}" reached repeat_times=${task.repeat_times}, disabled`);
194
+ }
195
+ }
196
+ computeNextRun(task, now) {
197
+ if (task.schedule_kind === "once")
198
+ return null; // 一次性跑完靠 disable 兜底
199
+ return now + task.interval_sec * 1000;
200
+ }
201
+ }
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Digital Employee Mesh — Master server (standalone entry point)
4
+ *
5
+ * Usage:
6
+ * node dist/master/server.js [--port 28800] [--host 0.0.0.0] [--data ~/.rotom]
7
+ *
8
+ * Or via package.json bin:
9
+ * mesh-master [--port 28800] [--data ~/.rotom]
10
+ */
11
+ import express from "express";
12
+ import fs from "node:fs";
13
+ import http from "node:http";
14
+ import path from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { MeshDb } from "./db.js";
17
+ import { AuthService } from "./auth.js";
18
+ import { WSHub } from "./ws-hub.js";
19
+ import { Router } from "./router.js";
20
+ import { OfflineQueue } from "./offline-queue.js";
21
+ import { createApi } from "./api/index.js";
22
+ import { TerminalHub } from "./terminal-hub.js";
23
+ import { Scheduler } from "./scheduler.js";
24
+ import { ShareTokenStore } from "./share-tokens.js";
25
+ import { handleIssuePatrolTerminal, handleLinkPatrolIssueTerminal } from "./patrol-terminal.js";
26
+ import { DEFAULT_MASTER_PORT, DEFAULT_MASTER_HOST } from "../shared/constants.js";
27
+ import os from "node:os";
28
+ import { createLogger, enableFileLogging, closeFileLogging } from "../shared/logger.js";
29
+ import { getMasterIdentity } from "./federation/identity.js";
30
+ import { runOpcBootstrap, ensureLocalExecutor } from "./opc-bootstrap.js";
31
+ import { initFederationManager } from "./federation/manager.js";
32
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
33
+ const log = createLogger("mesh-master");
34
+ function resolveDataDir() {
35
+ return process.env.ROTOM_HOME || path.join(os.homedir(), ".rotom");
36
+ }
37
+ function parseArgs() {
38
+ const args = process.argv.slice(2);
39
+ const config = {
40
+ port: DEFAULT_MASTER_PORT,
41
+ host: DEFAULT_MASTER_HOST,
42
+ dataDir: resolveDataDir(),
43
+ };
44
+ for (let i = 0; i < args.length; i++) {
45
+ switch (args[i]) {
46
+ case "--port":
47
+ case "-p":
48
+ config.port = parseInt(args[++i]) || DEFAULT_MASTER_PORT;
49
+ break;
50
+ case "--host":
51
+ case "-h":
52
+ config.host = args[++i] || DEFAULT_MASTER_HOST;
53
+ break;
54
+ case "--data":
55
+ case "-d":
56
+ config.dataDir = args[++i] || resolveDataDir();
57
+ break;
58
+ }
59
+ }
60
+ return config;
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // Start
64
+ // ---------------------------------------------------------------------------
65
+ async function main() {
66
+ const config = parseArgs();
67
+ // Enable daily-rotated file logging under data directory
68
+ enableFileLogging(path.join(path.resolve(config.dataDir), "logs"));
69
+ log.info("Starting...");
70
+ log.info(`Data directory: ${path.resolve(config.dataDir)}`);
71
+ // Database
72
+ const db = new MeshDb(path.join(config.dataDir, "mesh.db"));
73
+ // OPC bootstrap — 解析本机 master 身份 + 首次启动建默认 agent / group。
74
+ // 失败(hostname 校验等)直接终止启动 —— OPC 是底层身份,不能没有。
75
+ const identity = getMasterIdentity({ rotomHome: config.dataDir });
76
+ const opcResult = runOpcBootstrap(db, identity);
77
+ // Patrol auto-sync: when an issue reaches terminal state, advance patrol state
78
+ db._onIssueTerminal = (issueId) => {
79
+ const issue = db.getIssueById(issueId);
80
+ if (!issue)
81
+ return;
82
+ const group = db.getGroupByIdFull(issue.group_id);
83
+ if (group?.type === "patrol") {
84
+ handleIssuePatrolTerminal(db, issue);
85
+ return;
86
+ }
87
+ if (group?.type === "patrol-link") {
88
+ handleLinkPatrolIssueTerminal(db, issue);
89
+ return;
90
+ }
91
+ };
92
+ // Reset stale online status from previous run
93
+ const resetCount = db.resetAllOnline();
94
+ if (resetCount > 0) {
95
+ log.info(`Reset ${resetCount} stale online agent(s) to offline`);
96
+ }
97
+ // Services — single AuthService shared between WSHub and API
98
+ const auth = new AuthService(db);
99
+ const offlineQueue = new OfflineQueue(db);
100
+ const router = new Router(db, log);
101
+ const shareTokens = new ShareTokenStore();
102
+ // HTTP + Express
103
+ const app = express();
104
+ // 15mb to allow base64-encoded image uploads via /api/uploads (see
105
+ // uploads.ts MAX_UPLOAD_BYTES — kept in sync). Regular JSON endpoints
106
+ // don't approach this; the limit is a ceiling, not a default allocation.
107
+ app.use(express.json({ limit: "15mb" }));
108
+ // Dashboard (static files)
109
+ // Prod (running from dist/master): build:master copies React dashboard
110
+ // build output to dist/master/dashboard.
111
+ // Dev (running src/master via tsx): fall back to packages/dashboard build
112
+ // output — run `pnpm dashboard:build` first.
113
+ let dashboardDir = path.resolve(__dirname, "dashboard");
114
+ if (!fs.existsSync(dashboardDir)) {
115
+ dashboardDir = path.resolve(__dirname, "../../packages/dashboard/dist/src/master/dashboard");
116
+ }
117
+ if (!fs.existsSync(dashboardDir)) {
118
+ log.warn(`Dashboard files not found. Run \`pnpm dashboard:build\` then retry. Looked in: ${dashboardDir}`);
119
+ }
120
+ else {
121
+ log.info(`Dashboard files: ${dashboardDir}`);
122
+ }
123
+ app.use("/dashboard", express.static(dashboardDir));
124
+ // SPA fallback — serve index.html for all /dashboard/* routes (client-side routing)
125
+ app.get("/dashboard/*", (_req, res) => {
126
+ res.sendFile(path.join(dashboardDir, "index.html"));
127
+ });
128
+ // Root redirect
129
+ app.get("/", (_req, res) => res.redirect("/dashboard"));
130
+ // Health check (unauthenticated)
131
+ app.get("/health", (_req, res) => {
132
+ res.json({ status: "ok", ...db.stats() });
133
+ });
134
+ // Create HTTP server first — needed by WSHub
135
+ const httpServer = http.createServer(app);
136
+ // WebSocket Hub (attaches to HTTP server) — shares auth service
137
+ const hub = new WSHub(httpServer, db, auth, router, offlineQueue, log);
138
+ hub.start();
139
+ // Federation:FederationManager 封装 fedClient/fedPublisher/fedServer 生命周期。
140
+ // API 层(POST /api/teams/join)可通过 getFederationManager() runtime 切换 federation 状态。
141
+ const federationManager = initFederationManager({
142
+ db, hub, router, httpServer, identity,
143
+ rotomHome: config.dataDir,
144
+ masterPort: config.port,
145
+ });
146
+ federationManager.initFromRole();
147
+ // Web terminal hub — mounts on /api/terminal alongside the agent /ws.
148
+ // Lazy-loads node-pty; no-op if the optional dep isn't installed.
149
+ const terminalHub = new TerminalHub(httpServer, db, log);
150
+ await terminalHub.start();
151
+ // Scheduled-task scheduler — 30s tick interval, drives scheduled_tasks rows
152
+ // that trigger pushIssueAssignment (agent mode) or postSystemToGroup (message mode).
153
+ const scheduler = new Scheduler(db, hub);
154
+ scheduler.start();
155
+ // REST API — shares auth service and hub with WSHub
156
+ app.use("/api", createApi(db, auth, hub, router, config.port, shareTokens));
157
+ // Listen
158
+ let localExecutor = null;
159
+ await new Promise((resolve) => {
160
+ httpServer.listen(config.port, config.host, () => {
161
+ log.info(`Running on http://${config.host}:${config.port}`);
162
+ log.info(`Dashboard: http://localhost:${config.port}/dashboard`);
163
+ log.info(`WebSocket: ws://localhost:${config.port}/ws`);
164
+ log.info(`Terminal: ws://localhost:${config.port}/api/terminal`);
165
+ log.info(`API: http://localhost:${config.port}/api`);
166
+ log.warn("API authentication is DISABLED (internal network mode)");
167
+ // master 监听 ready 后再 spawn 本机 executor —— 避免 executor 比 master 早起来连不上。
168
+ // 注意:即使用户已有 agents(OPC bootstrap 没建 defaultAgent),也要 spawn executor,
169
+ // 否则用户的 agents 上不了线。defaultAgentName 仅用于 .auto-executor.json 的兜底,
170
+ // scanClis 模式下用不到。
171
+ localExecutor = ensureLocalExecutor({
172
+ rotomHome: config.dataDir,
173
+ masterPort: config.port,
174
+ defaultAgentName: opcResult.defaultAgent?.name,
175
+ });
176
+ resolve();
177
+ });
178
+ });
179
+ // Graceful shutdown
180
+ const shutdown = () => {
181
+ log.info("Shutting down...");
182
+ federationManager.stop();
183
+ localExecutor?.stop();
184
+ scheduler.stop();
185
+ hub.stop();
186
+ terminalHub.stop();
187
+ router.stop();
188
+ db.close();
189
+ httpServer.close(() => {
190
+ log.info("Goodbye.");
191
+ closeFileLogging();
192
+ process.exit(0);
193
+ });
194
+ // Force exit after 5s
195
+ setTimeout(() => process.exit(1), 5000);
196
+ };
197
+ process.on("SIGINT", shutdown);
198
+ process.on("SIGTERM", shutdown);
199
+ }
200
+ main().catch((err) => {
201
+ log.error("Fatal:", err);
202
+ process.exit(1);
203
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Link collector —— 消息发送路径上的 inline hook。
3
+ *
4
+ * 用法:在 4 个 addGroupMessage 调用点之后,直接调
5
+ * collectLinksFromText(text, ctx, db)
6
+ * 函数内部纯函数式抽 URL → 规范化 → dedup by url_norm → 入库。
7
+ *
8
+ * 错误隔离:整个流程包 try/catch,失败只 log.warn,绝不影响消息发送主路径。
9
+ * 不抓 system sender 的消息(系统消息不含真实业务链接,且会重复触发)。
10
+ */
11
+ import { randomUUID } from "node:crypto";
12
+ import { extractUrls, normalizeUrl, extractContextSnippet, } from "../../shared/url-extractor.js";
13
+ import { createLogger } from "../../shared/logger.js";
14
+ const log = createLogger("link-collector");
15
+ /**
16
+ * 从消息正文抽 URL 入库。
17
+ * 同一消息内多次出现的同一 URL 也各算一次 occurrence(防漏抓上下文)。
18
+ */
19
+ export function collectLinksFromText(text, ctx, db) {
20
+ if (!text)
21
+ return;
22
+ // system sender 跳过
23
+ if (ctx.sourceSender && ctx.sourceSender === "system")
24
+ return;
25
+ let extracted;
26
+ try {
27
+ extracted = extractUrls(text);
28
+ }
29
+ catch (err) {
30
+ log.warn(`extractUrls failed: ${err.message}`);
31
+ return;
32
+ }
33
+ if (extracted.length === 0)
34
+ return;
35
+ for (const item of extracted) {
36
+ try {
37
+ const norm = normalizeUrl(item.raw);
38
+ if (!norm)
39
+ continue; // 非法 / 非 http(s) 丢弃
40
+ const snippet = extractContextSnippet(text, item.index, item.raw.length);
41
+ const existing = db.getLinkByUrlNorm(norm.norm);
42
+ if (existing) {
43
+ db.addLinkOccurrence(existing.id, {
44
+ sourceType: ctx.sourceType,
45
+ sourceId: ctx.sourceId,
46
+ sourceGroupId: ctx.sourceGroupId,
47
+ sourceSender: ctx.sourceSender,
48
+ contextSnippet: snippet,
49
+ });
50
+ db.touchLinkLastSeen(existing.id);
51
+ if (ctx.sourceGroupId)
52
+ db.addLinkSourceGroup(existing.id, ctx.sourceGroupId);
53
+ }
54
+ else {
55
+ const linkId = randomUUID();
56
+ db.createLink({
57
+ id: linkId,
58
+ urlNorm: norm.norm,
59
+ urlRaw: norm.raw,
60
+ host: norm.host,
61
+ });
62
+ // 并发情况:另一个 collector tick 可能在我们 createLink 之前抢先 INSERT,
63
+ // INSERT OR IGNORE 后再查一次确保有 row(若已被抢先,existing 就拿到那个 id)
64
+ const again = db.getLinkByUrlNorm(norm.norm);
65
+ const finalId = again?.id ?? linkId;
66
+ db.addLinkOccurrence(finalId, {
67
+ sourceType: ctx.sourceType,
68
+ sourceId: ctx.sourceId,
69
+ sourceGroupId: ctx.sourceGroupId,
70
+ sourceSender: ctx.sourceSender,
71
+ contextSnippet: snippet,
72
+ });
73
+ if (ctx.sourceGroupId)
74
+ db.addLinkSourceGroup(finalId, ctx.sourceGroupId);
75
+ }
76
+ }
77
+ catch (err) {
78
+ // 单条失败不影响其他链接
79
+ log.warn(`collectLinksFromText: URL "${item.raw}" 失败: ${err.message}`);
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Link-patrol group bootstrap —— type=patrol-link 群建群时的副作用。
3
+ *
4
+ * 仿 services/patrol-bootstrap.ts:
5
+ * - 创建 recurring link-patrol scheduled task(interval 3600s,handler_key="link-patrol")
6
+ * - 绑定 link-patrol-rules skill 到巡检员 agent(skill seed 在 migration 053 里)
7
+ *
8
+ * Failed skill binding 非致命 —— schedule 仍跑,agent 没规则 prompt 时用兜底判断。
9
+ */
10
+ export function buildLinkPatrolPayload(groupId, agentName) {
11
+ return {
12
+ patrolGroupId: groupId,
13
+ patrolAgentName: agentName,
14
+ scanBatch: 20,
15
+ };
16
+ }
17
+ // 默认 5 小时一次:链接分类的 few-shot 增量慢,1h 太频会浪费 token,4h 以上更稳。
18
+ // 改这块要 dashboard 那边的 "Link 分类" tab 也跟着(intervalSec 默认值)。
19
+ const LINK_PATROL_DEFAULT_INTERVAL_SEC = 5 * 60 * 60;
20
+ export function bootstrapLinkPatrolGroup(db, log, groupId, agentName) {
21
+ if (!agentName)
22
+ return;
23
+ const payload = buildLinkPatrolPayload(groupId, agentName);
24
+ db.createScheduledTask({
25
+ name: "链接智能分类",
26
+ groupId,
27
+ mode: "agent",
28
+ agentName,
29
+ scheduleKind: "interval",
30
+ intervalSec: LINK_PATROL_DEFAULT_INTERVAL_SEC,
31
+ prompt: "",
32
+ enabled: true,
33
+ handlerKey: "link-patrol",
34
+ handlerPayload: JSON.stringify(payload),
35
+ });
36
+ const skill = db.getSkillByName("link-patrol-rules");
37
+ if (skill) {
38
+ db.bindSkill({
39
+ groupId,
40
+ agentName,
41
+ skillId: skill.id,
42
+ createdBy: "system:link-patrol-bootstrap",
43
+ });
44
+ log.info(`Link-patrol group ${groupId}: bound link-patrol-rules to ${agentName}`);
45
+ }
46
+ else {
47
+ log.warn(`Link-patrol group ${groupId}: link-patrol-rules skill not found, skip binding`);
48
+ }
49
+ log.info(`Link-patrol group ${groupId}: auto-created link-patrol schedule (interval 3600s, enabled)`);
50
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Memory-extract prompt builder — extracted from api/issues.ts so the API
3
+ * handler stays thin and the prompt text is testable in isolation.
4
+ *
5
+ * When the dashboard triggers "extract memory from this issue", the API
6
+ * creates a child issue assigned to a chosen agent with this prompt as its
7
+ * description. The agent reads the parent issue, distills 0..N memory
8
+ * entries (fact / decision / convention / pitfall / todo / playbook), and
9
+ * writes them via `rotom memory add ... --pending` for human review.
10
+ */
11
+ /**
12
+ * Build the Chinese prompt that instructs the agent to extract durable
13
+ * memory entries from a finished issue.
14
+ */
15
+ export function buildMemoryExtractPrompt(sourceIssue, sourceShortId) {
16
+ return [
17
+ `[记忆提取任务] 请从 Issue #${sourceShortId} 的产出中提炼值得长期记住的经验。`,
18
+ ``,
19
+ `原 Issue 标题:${sourceIssue.title}`,
20
+ `原 Issue 描述:`,
21
+ sourceIssue.description || "(无)",
22
+ ``,
23
+ `步骤:`,
24
+ `1. 用 \`rotom issue show ${sourceIssue.id}\` 或读 issue 详情,了解这次任务做了什么、关键决策、踩过的坑、用到的技术栈/约定`,
25
+ `2. 提炼 0~N 条记忆,每条选定 category(fact/decision/convention/pitfall/todo/playbook)`,
26
+ `3. 每条用 \`rotom memory add ${sourceIssue.group_id} --key <主题> --value <内容> --category <cat> --summary <一句话> --pending\` 写入`,
27
+ ` - --pending 必须加,写入后处于待审核状态,由人在 dashboard 审核`,
28
+ ` - key 用 "decision:xxx" / "pitfall:xxx" / "fact:xxx" 等带前缀的形式`,
29
+ ` - 只提炼真正值得长期记住的,无关细节不要记。没有值得记的就一条都不写`,
30
+ `4. 完成后在群里回复"已提取 N 条记忆,待审核"`,
31
+ ``,
32
+ `重要:不要记临时性的、任务特定的、下次不会复用的信息。`,
33
+ ].join("\n");
34
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Patrol-group bootstrap — extracted from api/groups.ts createGroup handler
3
+ * so the API layer stays thin. Owns the side-effects that fire when a group
4
+ * with `type === "patrol"` is created:
5
+ * - create the recurring `issue-patrol` scheduled task (default interval 7200s = 2h)
6
+ * - bind the `issue-patrol-rules` skill to the patrol agent (if it exists)
7
+ *
8
+ * Failed skill binding is non-fatal — the schedule still runs, the agent
9
+ * just doesn't get the rules prompt injected.
10
+ */
11
+ // 默认 2 小时一次:巡检本质是观察 + 预警,1h 太频反而是噪音;2h 留出足够的处理窗口
12
+ // 又不至于错过新 issue(常见新 issue 在群聊触发,2h 内会被人工认领)。
13
+ const PATROL_DEFAULT_INTERVAL_SEC = 2 * 60 * 60;
14
+ /**
15
+ * Build the handler payload used by both the scheduled task and the
16
+ * dashboard's "what does this patrol do?" view. Pulled out so the shape
17
+ * stays in one place if the patrol handler evolves.
18
+ */
19
+ export function buildPatrolPayload(groupId, agentName) {
20
+ return {
21
+ patrolGroupId: groupId,
22
+ patrolAgentName: agentName,
23
+ throughputCap: 3,
24
+ candidateCap: 3,
25
+ scanBatch: 10,
26
+ };
27
+ }
28
+ /**
29
+ * Create the recurring `issue-patrol` schedule and bind the
30
+ * `issue-patrol-rules` skill to the patrol agent. No-op if `agentName`
31
+ * is empty.
32
+ */
33
+ export function bootstrapPatrolGroup(db, log, groupId, agentName) {
34
+ if (!agentName)
35
+ return;
36
+ const payload = buildPatrolPayload(groupId, agentName);
37
+ db.createScheduledTask({
38
+ name: "Issue 巡检",
39
+ groupId,
40
+ mode: "agent", // handler 模式下 mode 不被使用,但 schema NOT NULL,保留 agent
41
+ agentName,
42
+ scheduleKind: "interval",
43
+ intervalSec: PATROL_DEFAULT_INTERVAL_SEC,
44
+ prompt: "", // handler 模式不用 prompt,但 schema NOT NULL
45
+ enabled: true,
46
+ handlerKey: "issue-patrol",
47
+ handlerPayload: JSON.stringify(payload),
48
+ });
49
+ const skill = db.getSkillByName("issue-patrol-rules");
50
+ if (skill) {
51
+ db.bindSkill({
52
+ groupId,
53
+ agentName,
54
+ skillId: skill.id,
55
+ createdBy: "system:patrol-bootstrap",
56
+ });
57
+ log.info(`Patrol group ${groupId}: bound issue-patrol-rules to ${agentName}`);
58
+ }
59
+ else {
60
+ log.warn(`Patrol group ${groupId}: issue-patrol-rules skill not found, skip binding`);
61
+ }
62
+ log.info(`Patrol group ${groupId}: auto-created issue-patrol schedule (interval ${PATROL_DEFAULT_INTERVAL_SEC}s, enabled)`);
63
+ }