@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,157 @@
1
+ import { createLogger } from "../../shared/logger.js";
2
+ const log = createLogger("mesh-api");
3
+ export function registerMessageRoutes(apiRouter, db, _auth, hub, router) {
4
+ apiRouter.get("/audit", (req, res) => {
5
+ const limit = Math.min(parseInt(req.query.limit) || 50, 500);
6
+ res.json(db.listAudit(limit));
7
+ });
8
+ apiRouter.get("/stats", (_req, res) => {
9
+ const basic = db.stats();
10
+ const msgStats = db.agentMessageStats();
11
+ res.json({ ...basic, agents: msgStats });
12
+ });
13
+ apiRouter.get("/messages", (req, res) => {
14
+ const agent = req.query.agent;
15
+ const from = req.query.from;
16
+ const to = req.query.to;
17
+ const status = req.query.status;
18
+ const keyword = req.query.keyword;
19
+ const groupId = req.query.groupId;
20
+ const limit = Math.min(parseInt(req.query.limit) || 100, 500);
21
+ const offset = Math.max(parseInt(req.query.offset) || 0, 0);
22
+ const before = req.query.before;
23
+ const messages = db.listMessages({ agent, from, to, status, keyword, groupId, limit, offset, before });
24
+ const total = db.countMessages({ agent, from, to, status, keyword, groupId, before });
25
+ res.json({ messages, total });
26
+ });
27
+ apiRouter.post("/messages/send", (req, res) => {
28
+ const { from, to, message } = req.body;
29
+ if (!from || !to || !message) {
30
+ res.status(400).json({ error: "from, to, and message are required" });
31
+ return;
32
+ }
33
+ const fromAgent = db.getAgentByName(from);
34
+ if (!fromAgent) {
35
+ res.status(404).json({ error: `Sender agent "${from}" not found` });
36
+ return;
37
+ }
38
+ const toAgent = db.getAgentByName(to);
39
+ if (!toAgent) {
40
+ res.status(404).json({ error: `Target agent "${to}" not found` });
41
+ return;
42
+ }
43
+ const requestId = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
44
+ if (!router) {
45
+ res.status(500).json({ error: "Message router not available" });
46
+ return;
47
+ }
48
+ const routeResult = router.route(fromAgent.id, {
49
+ requestId,
50
+ target: to,
51
+ payload: { message }
52
+ });
53
+ if (routeResult.error) {
54
+ res.status(500).json({ error: routeResult.error });
55
+ return;
56
+ }
57
+ let delivered = false;
58
+ let queued = false;
59
+ if (routeResult.targetAgentId && hub) {
60
+ delivered = hub.sendToAgent(routeResult.targetAgentId, {
61
+ type: "a2a_message",
62
+ requestId,
63
+ from: { name: from, domain: fromAgent.domain || undefined, status: "online" },
64
+ payload: { message },
65
+ routeType: "exact",
66
+ });
67
+ if (!delivered) {
68
+ queued = true;
69
+ }
70
+ }
71
+ db.logMessage({
72
+ requestId,
73
+ fromName: from,
74
+ fromDomain: fromAgent.domain || undefined,
75
+ toName: to,
76
+ toDomain: toAgent.domain || undefined,
77
+ routeType: "exact",
78
+ direction: "send",
79
+ payload: JSON.stringify({ message }),
80
+ status: routeResult.error ? "failed" : queued ? "queued" : delivered ? "delivered" : "failed",
81
+ source: "api",
82
+ });
83
+ res.json({
84
+ requestId,
85
+ delivered,
86
+ queued,
87
+ message: "Message sent successfully"
88
+ });
89
+ });
90
+ // Cancel an in-flight streaming chat reply. Body: { requestId, agentName, reason? }.
91
+ // agentName is the responder currently generating (the streaming bubble's `from`),
92
+ // not the original sender — master uses it to route the chat_cancelled WS message
93
+ // to that specific worker. Returns delivered=false when the responder is offline
94
+ // or unknown; the caller can treat that as "stream is already broken" and move on.
95
+ apiRouter.post("/messages/cancel", (req, res) => {
96
+ const { requestId, agentName, reason } = req.body || {};
97
+ if (!requestId || typeof requestId !== "string" ||
98
+ !agentName || typeof agentName !== "string") {
99
+ res.status(400).json({ error: "requestId and agentName are required" });
100
+ return;
101
+ }
102
+ if (!hub) {
103
+ res.status(500).json({ error: "WSHub not available" });
104
+ return;
105
+ }
106
+ const delivered = hub.pushChatCancel(agentName, requestId, typeof reason === "string" && reason ? reason : undefined);
107
+ res.json({ ok: true, delivered });
108
+ });
109
+ apiRouter.get("/whoami", (req, res) => {
110
+ const agentAuth = req.agentAuth;
111
+ if (agentAuth) {
112
+ const agent = db.getAgentById(agentAuth.id);
113
+ res.json({ kind: "agent", name: agentAuth.name, id: agentAuth.id, domain: agent?.domain || null });
114
+ return;
115
+ }
116
+ res.json({ kind: "dashboard" });
117
+ });
118
+ apiRouter.post("/cli/groups/:groupId/send", (req, res) => {
119
+ const agentAuth = req.agentAuth;
120
+ if (!agentAuth) {
121
+ res.status(403).json({ error: "Mesh token required (use a Bearer mesh_xxx token)" });
122
+ return;
123
+ }
124
+ if (!hub) {
125
+ res.status(500).json({ error: "WSHub not available" });
126
+ return;
127
+ }
128
+ const group = db.getGroupById(req.params.groupId);
129
+ if (!group) {
130
+ res.status(404).json({ error: "Group not found" });
131
+ return;
132
+ }
133
+ if (group.archived_at) {
134
+ res.status(403).json({ error: "Group is archived, cannot send messages" });
135
+ return;
136
+ }
137
+ const { target, message, noDispatch, needReply } = req.body || {};
138
+ if (!target || !message) {
139
+ res.status(400).json({ error: "target and message are required" });
140
+ return;
141
+ }
142
+ const r = hub.sendAsAgent({
143
+ fromName: agentAuth.name,
144
+ target,
145
+ message,
146
+ groupId: req.params.groupId,
147
+ groupName: group.name,
148
+ ...(noDispatch === true ? { noDispatch: true } : {}),
149
+ ...(needReply === true ? { needReply: true } : {}),
150
+ });
151
+ if (r.error) {
152
+ res.status(400).json(r);
153
+ return;
154
+ }
155
+ res.json(r);
156
+ });
157
+ }
@@ -0,0 +1,77 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createLogger } from "../../shared/logger.js";
3
+ const log = createLogger("mesh-api");
4
+ export function registerNoteRoutes(apiRouter, db) {
5
+ apiRouter.get("/groups/:groupId/notes", (req, res) => {
6
+ const group = db.getGroupById(req.params.groupId);
7
+ if (!group) {
8
+ res.status(404).json({ error: "Group not found" });
9
+ return;
10
+ }
11
+ res.json(db.listNotesByGroup(req.params.groupId));
12
+ });
13
+ apiRouter.post("/groups/:groupId/notes", (req, res) => {
14
+ const group = db.getGroupById(req.params.groupId);
15
+ if (!group) {
16
+ res.status(404).json({ error: "Group not found" });
17
+ return;
18
+ }
19
+ if (group.archived_at) {
20
+ res.status(403).json({ error: "Group is archived, cannot create notes" });
21
+ return;
22
+ }
23
+ const { title, description, createdBy } = req.body;
24
+ if (!title || !createdBy) {
25
+ res.status(400).json({ error: "title and createdBy are required" });
26
+ return;
27
+ }
28
+ const id = randomUUID();
29
+ db.createNote({
30
+ id, groupId: req.params.groupId,
31
+ title: String(title).trim(),
32
+ description: description == null ? "" : String(description),
33
+ createdBy,
34
+ });
35
+ log.info(`Note created: "${title}" (${id}) in group ${req.params.groupId}`);
36
+ res.status(201).json({ id, title });
37
+ });
38
+ apiRouter.get("/notes/:id", (req, res) => {
39
+ const note = db.getNoteById(req.params.id);
40
+ if (!note) {
41
+ res.status(404).json({ error: "Note not found" });
42
+ return;
43
+ }
44
+ res.json(note);
45
+ });
46
+ apiRouter.put("/notes/:id", (req, res) => {
47
+ const note = db.getNoteById(req.params.id);
48
+ if (!note) {
49
+ res.status(404).json({ error: "Note not found" });
50
+ return;
51
+ }
52
+ const { title, description } = req.body;
53
+ const fields = {};
54
+ if (title !== undefined) {
55
+ const t = String(title).trim();
56
+ if (!t) {
57
+ res.status(400).json({ error: "title cannot be empty" });
58
+ return;
59
+ }
60
+ fields.title = t;
61
+ }
62
+ if (description !== undefined) {
63
+ fields.description = description === null ? "" : String(description);
64
+ }
65
+ db.updateNote(req.params.id, fields);
66
+ res.json({ ok: true });
67
+ });
68
+ apiRouter.delete("/notes/:id", (req, res) => {
69
+ const note = db.getNoteById(req.params.id);
70
+ if (!note) {
71
+ res.status(404).json({ error: "Note not found" });
72
+ return;
73
+ }
74
+ db.deleteNote(req.params.id);
75
+ res.json({ ok: true });
76
+ });
77
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * /api/schedule-patterns — 调度模式参考库 CRUD
3
+ *
4
+ * - 列表: GET /api/schedule-patterns
5
+ * - 新建: POST /api/schedule-patterns
6
+ * - 改: PATCH /api/schedule-patterns/:id
7
+ * - 删: DELETE /api/schedule-patterns/:id (种子模式 is_default=1 返回 400)
8
+ *
9
+ * 身份验证沿用 api/index.ts 的 permissive middleware。
10
+ */
11
+ import { createLogger } from "../../shared/logger.js";
12
+ const log = createLogger("mesh-api-schedule-patterns");
13
+ function asString(v) {
14
+ return typeof v === "string" && v.trim() ? v.trim() : undefined;
15
+ }
16
+ function asInt(v) {
17
+ if (typeof v === "number" && Number.isFinite(v))
18
+ return Math.trunc(v);
19
+ if (typeof v === "string" && /^-?\d+$/.test(v.trim()))
20
+ return parseInt(v.trim(), 10);
21
+ return undefined;
22
+ }
23
+ /**
24
+ * schedule_config 接受对象或 JSON 字符串;null/空串/缺省=不设置(返回 null)。
25
+ * 返回 undefined 表示「格式非法」,由调用方返回 400。
26
+ */
27
+ function normalizeScheduleConfig(v) {
28
+ if (v === null || v === undefined)
29
+ return null;
30
+ if (typeof v === "string") {
31
+ if (v.trim() === "")
32
+ return null;
33
+ try {
34
+ JSON.parse(v);
35
+ return v;
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
41
+ if (typeof v === "object") {
42
+ return JSON.stringify(v);
43
+ }
44
+ return undefined;
45
+ }
46
+ export function registerSchedulePatternRoutes(apiRouter, db) {
47
+ // ── 列表 ─────────────────────────────────────────────────────────────────
48
+ apiRouter.get("/schedule-patterns", (_req, res) => {
49
+ res.json(db.listSchedulePatterns());
50
+ });
51
+ // ── 新建 ─────────────────────────────────────────────────────────────────
52
+ apiRouter.post("/schedule-patterns", (req, res) => {
53
+ const b = req.body;
54
+ const name = asString(b.name);
55
+ if (!name) {
56
+ res.status(400).json({ error: "name is required" });
57
+ return;
58
+ }
59
+ const scheduleConfig = normalizeScheduleConfig(b.schedule_config);
60
+ if (scheduleConfig === undefined) {
61
+ res.status(400).json({ error: "schedule_config must be a JSON object or JSON string" });
62
+ return;
63
+ }
64
+ const pat = db.createSchedulePattern({
65
+ name,
66
+ description: typeof b.description === "string" ? b.description : "",
67
+ schedule_config: scheduleConfig,
68
+ sort_order: asInt(b.sort_order) ?? 0,
69
+ });
70
+ log.info(`Schedule pattern created: #${pat.id} "${pat.name}"`);
71
+ res.status(201).json(pat);
72
+ });
73
+ // ── 改 ───────────────────────────────────────────────────────────────────
74
+ apiRouter.patch("/schedule-patterns/:id", (req, res) => {
75
+ const id = asInt(req.params.id);
76
+ if (id === undefined) {
77
+ res.status(400).json({ error: "id must be an integer" });
78
+ return;
79
+ }
80
+ const existing = db.getSchedulePattern(id);
81
+ if (!existing) {
82
+ res.status(404).json({ error: "Schedule pattern not found" });
83
+ return;
84
+ }
85
+ const b = req.body;
86
+ const patch = {};
87
+ if (b.name !== undefined) {
88
+ const s = asString(b.name);
89
+ if (!s) {
90
+ res.status(400).json({ error: "name cannot be empty" });
91
+ return;
92
+ }
93
+ patch.name = s;
94
+ }
95
+ if (b.description !== undefined) {
96
+ patch.description = typeof b.description === "string" ? b.description : "";
97
+ }
98
+ if (b.schedule_config !== undefined) {
99
+ const v = normalizeScheduleConfig(b.schedule_config);
100
+ if (v === undefined) {
101
+ res.status(400).json({ error: "schedule_config must be a JSON object or JSON string" });
102
+ return;
103
+ }
104
+ patch.schedule_config = v;
105
+ }
106
+ if (b.sort_order !== undefined) {
107
+ patch.sort_order = asInt(b.sort_order) ?? 0;
108
+ }
109
+ const pat = db.updateSchedulePattern(id, patch);
110
+ log.info(`Schedule pattern updated: #${id}`);
111
+ res.json(pat);
112
+ });
113
+ // ── 删 ───────────────────────────────────────────────────────────────────
114
+ apiRouter.delete("/schedule-patterns/:id", (req, res) => {
115
+ const id = asInt(req.params.id);
116
+ if (id === undefined) {
117
+ res.status(400).json({ error: "id must be an integer" });
118
+ return;
119
+ }
120
+ const existing = db.getSchedulePattern(id);
121
+ if (!existing) {
122
+ res.status(404).json({ error: "Schedule pattern not found" });
123
+ return;
124
+ }
125
+ if (existing.is_default === 1) {
126
+ res.status(400).json({ error: "Cannot delete default schedule pattern" });
127
+ return;
128
+ }
129
+ db.deleteSchedulePattern(id);
130
+ log.info(`Schedule pattern deleted: #${id}`);
131
+ res.json({ ok: true });
132
+ });
133
+ }
@@ -0,0 +1,272 @@
1
+ /**
2
+ * /api/schedules — 群内定时任务 CRUD + trigger
3
+ *
4
+ * - 列表:GET /api/schedules?group_id=<id>
5
+ * - 详情:GET /api/schedules/:id
6
+ * - 新建:POST /api/schedules
7
+ * - 改: PATCH /api/schedules/:id(改 schedule 字段会重算 next_run_at)
8
+ * - 删: DELETE /api/schedules/:id
9
+ * - 立刻触发:POST /api/schedules/:id/trigger
10
+ *
11
+ * 身份验证沿用 api/index.ts 的 permissive middleware(Dashboard cookie + agent Bearer 都允许)。
12
+ */
13
+ import { createLogger } from "../../shared/logger.js";
14
+ const log = createLogger("mesh-api-schedules");
15
+ function asString(v) {
16
+ return typeof v === "string" && v.trim() ? v.trim() : undefined;
17
+ }
18
+ function asInt(v) {
19
+ if (typeof v === "number" && Number.isFinite(v))
20
+ return Math.trunc(v);
21
+ if (typeof v === "string" && /^-?\d+$/.test(v.trim()))
22
+ return parseInt(v.trim(), 10);
23
+ return undefined;
24
+ }
25
+ function asBool(v) {
26
+ if (typeof v === "boolean")
27
+ return v;
28
+ if (v === "true" || v === 1 || v === "1")
29
+ return true;
30
+ if (v === "false" || v === 0 || v === "0")
31
+ return false;
32
+ return undefined;
33
+ }
34
+ function asMode(v) {
35
+ return v === "agent" || v === "message" ? v : undefined;
36
+ }
37
+ function asScheduleKind(v) {
38
+ return v === "once" || v === "interval" ? v : undefined;
39
+ }
40
+ export function registerScheduleRoutes(apiRouter, db) {
41
+ // ── 列表 ─────────────────────────────────────────────────────────────────
42
+ apiRouter.get("/schedules", (req, res) => {
43
+ const groupId = asString(req.query.group_id);
44
+ res.json(db.listScheduledTasks(groupId ? { groupId } : undefined));
45
+ });
46
+ // ── 详情 ─────────────────────────────────────────────────────────────────
47
+ apiRouter.get("/schedules/:id", (req, res) => {
48
+ const id = asInt(req.params.id);
49
+ if (id === undefined) {
50
+ res.status(400).json({ error: "id must be an integer" });
51
+ return;
52
+ }
53
+ const task = db.getScheduledTask(id);
54
+ if (!task) {
55
+ res.status(404).json({ error: "Scheduled task not found" });
56
+ return;
57
+ }
58
+ res.json(task);
59
+ });
60
+ // ── 新建 ─────────────────────────────────────────────────────────────────
61
+ apiRouter.post("/schedules", (req, res) => {
62
+ const b = req.body;
63
+ const name = asString(b.name);
64
+ const groupId = asString(b.group_id);
65
+ const mode = asMode(b.mode);
66
+ const scheduleKind = asScheduleKind(b.schedule_kind) ?? "interval";
67
+ const prompt = asString(b.prompt);
68
+ if (!name) {
69
+ res.status(400).json({ error: "name is required" });
70
+ return;
71
+ }
72
+ if (!groupId) {
73
+ res.status(400).json({ error: "group_id is required" });
74
+ return;
75
+ }
76
+ if (!prompt) {
77
+ res.status(400).json({ error: "prompt is required" });
78
+ return;
79
+ }
80
+ if (!mode) {
81
+ res.status(400).json({ error: "mode must be 'agent' or 'message'" });
82
+ return;
83
+ }
84
+ if (!db.getGroupById(groupId)) {
85
+ res.status(404).json({ error: `Group not found: ${groupId}` });
86
+ return;
87
+ }
88
+ let intervalSec = null;
89
+ let runAt = null;
90
+ if (scheduleKind === "interval") {
91
+ intervalSec = asInt(b.interval_sec) ?? null;
92
+ if (intervalSec === null || intervalSec < 30) {
93
+ res.status(400).json({ error: "interval_sec must be an integer >= 30 for schedule_kind=interval" });
94
+ return;
95
+ }
96
+ }
97
+ else {
98
+ runAt = asInt(b.run_at) ?? null;
99
+ if (runAt === null || runAt <= Date.now()) {
100
+ res.status(400).json({ error: "run_at must be a future epoch ms timestamp for schedule_kind=once" });
101
+ return;
102
+ }
103
+ }
104
+ let agentName = null;
105
+ if (mode === "agent") {
106
+ agentName = asString(b.agent_name) ?? null;
107
+ if (!agentName) {
108
+ res.status(400).json({ error: "agent_name is required for mode=agent" });
109
+ return;
110
+ }
111
+ }
112
+ else if (b.agent_name !== undefined) {
113
+ // message 模式也允许带 agent_name,但存也无所谓 —— 这里忽略,保持 NULL
114
+ agentName = asString(b.agent_name) ?? null;
115
+ }
116
+ const repeatTimes = b.repeat_times === null || b.repeat_times === undefined
117
+ ? null
118
+ : (() => {
119
+ const n = asInt(b.repeat_times);
120
+ return n !== undefined && n > 0 ? n : null;
121
+ })();
122
+ const enabled = asBool(b.enabled);
123
+ const task = db.createScheduledTask({
124
+ name,
125
+ groupId,
126
+ mode,
127
+ agentName,
128
+ scheduleKind,
129
+ intervalSec,
130
+ runAt,
131
+ prompt,
132
+ repeatTimes,
133
+ enabled: enabled !== undefined ? enabled : true,
134
+ });
135
+ log.info(`Schedule created: #${task.id} "${task.name}" (${task.mode}/${task.schedule_kind}) in group ${groupId}`);
136
+ res.status(201).json(task);
137
+ });
138
+ // ── 改 ───────────────────────────────────────────────────────────────────
139
+ apiRouter.patch("/schedules/:id", (req, res) => {
140
+ const id = asInt(req.params.id);
141
+ if (id === undefined) {
142
+ res.status(400).json({ error: "id must be an integer" });
143
+ return;
144
+ }
145
+ const existing = db.getScheduledTask(id);
146
+ if (!existing) {
147
+ res.status(404).json({ error: "Scheduled task not found" });
148
+ return;
149
+ }
150
+ const b = req.body;
151
+ const patch = {};
152
+ if (b.name !== undefined) {
153
+ const s = asString(b.name);
154
+ if (!s) {
155
+ res.status(400).json({ error: "name cannot be empty" });
156
+ return;
157
+ }
158
+ patch.name = s;
159
+ }
160
+ if (b.mode !== undefined) {
161
+ const m = asMode(b.mode);
162
+ if (!m) {
163
+ res.status(400).json({ error: "mode must be 'agent' or 'message'" });
164
+ return;
165
+ }
166
+ patch.mode = m;
167
+ }
168
+ if (b.agent_name !== undefined) {
169
+ patch.agentName = asString(b.agent_name) ?? null;
170
+ }
171
+ if (b.prompt !== undefined) {
172
+ const s = asString(b.prompt);
173
+ if (!s) {
174
+ res.status(400).json({ error: "prompt cannot be empty" });
175
+ return;
176
+ }
177
+ patch.prompt = s;
178
+ }
179
+ if (b.enabled !== undefined) {
180
+ const v = asBool(b.enabled);
181
+ if (v === undefined) {
182
+ res.status(400).json({ error: "enabled must be boolean" });
183
+ return;
184
+ }
185
+ patch.enabled = v;
186
+ }
187
+ if (b.repeat_times !== undefined) {
188
+ if (b.repeat_times === null) {
189
+ patch.repeatTimes = null;
190
+ }
191
+ else {
192
+ const n = asInt(b.repeat_times);
193
+ if (n === undefined || n <= 0) {
194
+ res.status(400).json({ error: "repeat_times must be a positive integer or null" });
195
+ return;
196
+ }
197
+ patch.repeatTimes = n;
198
+ }
199
+ }
200
+ // schedule 字段改动:三种字段互相影响,做一次合并校验
201
+ const newKind = b.schedule_kind !== undefined ? asScheduleKind(b.schedule_kind) : existing.schedule_kind;
202
+ const newInterval = b.interval_sec !== undefined ? asInt(b.interval_sec) : (existing.interval_sec ?? undefined);
203
+ const newRunAt = b.run_at !== undefined ? asInt(b.run_at) : (existing.run_at ?? undefined);
204
+ if (b.schedule_kind !== undefined && !newKind) {
205
+ res.status(400).json({ error: "schedule_kind must be 'once' or 'interval'" });
206
+ return;
207
+ }
208
+ if (newKind === "interval") {
209
+ if (newInterval === undefined || newInterval < 30) {
210
+ res.status(400).json({ error: "interval_sec must be an integer >= 30 for schedule_kind=interval" });
211
+ return;
212
+ }
213
+ patch.scheduleKind = "interval";
214
+ patch.intervalSec = newInterval;
215
+ }
216
+ else if (newKind === "once") {
217
+ if (newRunAt === undefined || newRunAt <= Date.now()) {
218
+ res.status(400).json({ error: "run_at must be a future epoch ms timestamp for schedule_kind=once" });
219
+ return;
220
+ }
221
+ patch.scheduleKind = "once";
222
+ patch.runAt = newRunAt;
223
+ }
224
+ const updated = db.updateScheduledTask(id, patch);
225
+ if (!updated) {
226
+ // shouldn't happen, we already checked existence
227
+ res.status(404).json({ error: "Scheduled task not found" });
228
+ return;
229
+ }
230
+ log.info(`Schedule #${id} updated`);
231
+ res.json(updated);
232
+ });
233
+ // ── 删 ───────────────────────────────────────────────────────────────────
234
+ apiRouter.delete("/schedules/:id", (req, res) => {
235
+ const id = asInt(req.params.id);
236
+ if (id === undefined) {
237
+ res.status(400).json({ error: "id must be an integer" });
238
+ return;
239
+ }
240
+ const ok = db.deleteScheduledTask(id);
241
+ if (!ok) {
242
+ res.status(404).json({ error: "Scheduled task not found" });
243
+ return;
244
+ }
245
+ log.info(`Schedule #${id} deleted`);
246
+ res.json({ ok: true });
247
+ });
248
+ // ── 立刻触发 ─────────────────────────────────────────────────────────────
249
+ apiRouter.post("/schedules/:id/trigger", (req, res) => {
250
+ const id = asInt(req.params.id);
251
+ if (id === undefined) {
252
+ res.status(400).json({ error: "id must be an integer" });
253
+ return;
254
+ }
255
+ const task = db.getScheduledTask(id);
256
+ if (!task) {
257
+ res.status(404).json({ error: "Scheduled task not found" });
258
+ return;
259
+ }
260
+ if (!task.enabled) {
261
+ res.status(409).json({ error: "Scheduled task is disabled — re-enable it first" });
262
+ return;
263
+ }
264
+ const ok = db.triggerScheduledTask(id);
265
+ if (!ok) {
266
+ res.status(409).json({ error: "Scheduled task is disabled" });
267
+ return;
268
+ }
269
+ log.info(`Schedule #${id} "${task.name}" manually triggered`);
270
+ res.json({ ok: true, id, next_run_at: Date.now() });
271
+ });
272
+ }