@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,168 @@
1
+ /**
2
+ * Link 智能分类 REST —— 工具箱 Link 分类 tab 用。
3
+ *
4
+ * - GET /api/links-patrol/state — 当前 patrol-link 群 + scheduled_task 配置
5
+ * - PATCH /api/links-patrol/config — 改 enabled / intervalSec / scanBatch
6
+ * - GET /api/links-patrol/runs — 最近 runs
7
+ * - GET /api/links-patrol/runs/:runId/logs — 单轮日志
8
+ * - GET /api/links-patrol/stats — 采集 / 分类统计(总链接数 / 未分类数 / 分类 host 数)
9
+ *
10
+ * 仿 src/master/api/issues-patrol.ts(issue 巡检)的同名端点。type=patrol-link 群由建群时
11
+ * 自动建,这里只读 + 改 schedule config + 看历史。
12
+ */
13
+ import { createLogger } from "../../shared/logger.js";
14
+ const log = createLogger("mesh-api-links-patrol");
15
+ function parsePayload(raw) {
16
+ if (!raw)
17
+ return {};
18
+ try {
19
+ return JSON.parse(raw);
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ function findLinkPatrolGroup(db) {
26
+ const groups = db.listGroupsByType("patrol-link").filter((g) => g.archived_at == null);
27
+ if (groups.length === 0)
28
+ return null;
29
+ const group = groups[0];
30
+ const task = db.listScheduledTasks({ groupId: group.id }).find((t) => t.handler_key === "link-patrol");
31
+ const agentName = task?.agent_name ?? "";
32
+ return { groupId: group.id, groupName: group.name, agentName };
33
+ }
34
+ export function registerLinkPatrolRoutes(apiRouter, db) {
35
+ // ── state ────────────────────────────────────────────────────────────────
36
+ apiRouter.get("/links-patrol/state", (_req, res) => {
37
+ const patrol = findLinkPatrolGroup(db);
38
+ if (!patrol) {
39
+ res.json({ enabled: false, hasPatrolGroup: false });
40
+ return;
41
+ }
42
+ const task = db.listScheduledTasks({ groupId: patrol.groupId }).find((t) => t.handler_key === "link-patrol");
43
+ if (!task) {
44
+ res.json({
45
+ hasPatrolGroup: true,
46
+ patrolGroupId: patrol.groupId,
47
+ patrolGroupName: patrol.groupName,
48
+ patrolAgentName: patrol.agentName,
49
+ enabled: false,
50
+ });
51
+ return;
52
+ }
53
+ const payload = parsePayload(task.handler_payload);
54
+ res.json({
55
+ hasPatrolGroup: true,
56
+ patrolGroupId: patrol.groupId,
57
+ patrolGroupName: patrol.groupName,
58
+ patrolAgentName: patrol.agentName,
59
+ taskId: task.id,
60
+ enabled: task.enabled === 1,
61
+ intervalSec: task.interval_sec,
62
+ nextRunAt: task.next_run_at,
63
+ lastRunAt: task.last_run_at,
64
+ lastStatus: task.last_status,
65
+ lastError: task.last_error,
66
+ scanBatch: payload.scanBatch ?? 20,
67
+ });
68
+ });
69
+ // ── config ────────────────────────────────────────────────────────────────
70
+ apiRouter.patch("/links-patrol/config", (req, res) => {
71
+ const patrol = findLinkPatrolGroup(db);
72
+ if (!patrol) {
73
+ res.status(400).json({ error: "未创建链接分类巡检群,请先建一个 type=patrol-link 的群" });
74
+ return;
75
+ }
76
+ const task = db.listScheduledTasks({ groupId: patrol.groupId }).find((t) => t.handler_key === "link-patrol");
77
+ if (!task) {
78
+ res.status(404).json({ error: "链接分类定时任务不存在" });
79
+ return;
80
+ }
81
+ const body = req.body ?? {};
82
+ const patch = {};
83
+ const payload = parsePayload(task.handler_payload);
84
+ if (typeof body.enabled === "boolean") {
85
+ patch.enabled = body.enabled;
86
+ }
87
+ if (typeof body.intervalSec === "number") {
88
+ if (body.intervalSec < 60) {
89
+ res.status(400).json({ error: "intervalSec 必须 >= 60" });
90
+ return;
91
+ }
92
+ patch.intervalSec = Math.floor(body.intervalSec);
93
+ patch.scheduleKind = "interval";
94
+ }
95
+ if (typeof body.scanBatch === "number") {
96
+ if (body.scanBatch < 1 || body.scanBatch > 100) {
97
+ res.status(400).json({ error: "scanBatch 取值 1-100" });
98
+ return;
99
+ }
100
+ payload.scanBatch = Math.floor(body.scanBatch);
101
+ }
102
+ payload.patrolGroupId = payload.patrolGroupId ?? patrol.groupId;
103
+ payload.patrolAgentName = payload.patrolAgentName ?? patrol.agentName;
104
+ patch.handlerPayload = JSON.stringify(payload);
105
+ const updated = db.updateScheduledTask(task.id, patch);
106
+ if (!updated) {
107
+ res.status(500).json({ error: "更新失败" });
108
+ return;
109
+ }
110
+ log.info(`Link-patrol config updated (task #${task.id}): ${JSON.stringify(patch)}`);
111
+ res.json({
112
+ ok: true,
113
+ enabled: updated.enabled === 1,
114
+ intervalSec: updated.interval_sec,
115
+ scanBatch: payload.scanBatch,
116
+ nextRunAt: updated.next_run_at,
117
+ });
118
+ });
119
+ // ── runs ──────────────────────────────────────────────────────────────────
120
+ apiRouter.get("/links-patrol/runs", (req, res) => {
121
+ const limit = parseLimit(req.query.limit, 50);
122
+ const patrol = findLinkPatrolGroup(db);
123
+ const runs = db.listLinkPatrolRuns({
124
+ patrolGroupId: patrol?.groupId,
125
+ limit,
126
+ });
127
+ res.json({ runs });
128
+ });
129
+ apiRouter.get("/links-patrol/runs/:runId/logs", (req, res) => {
130
+ const logs = db.listLinkPatrolLogsForRun(req.params.runId);
131
+ res.json({ logs });
132
+ });
133
+ // ── stats ─────────────────────────────────────────────────────────────────
134
+ apiRouter.get("/links-patrol/stats", (_req, res) => {
135
+ const totalRow = db.db.prepare("SELECT COUNT(*) as n FROM links").get();
136
+ const unclassRow = db.db.prepare("SELECT COUNT(*) as n FROM links WHERE category IS NULL").get();
137
+ const occRow = db.db.prepare("SELECT COUNT(*) as n FROM link_occurrences").get();
138
+ const hostRow = db.db.prepare("SELECT COUNT(DISTINCT host) as n FROM links WHERE category IS NOT NULL").get();
139
+ const patrol = findLinkPatrolGroup(db);
140
+ const runs = patrol
141
+ ? db.listLinkPatrolRuns({ patrolGroupId: patrol.groupId, limit: 1 })
142
+ : [];
143
+ const lastRun = runs[0] ?? null;
144
+ res.json({
145
+ totalLinks: totalRow.n,
146
+ unclassified: unclassRow.n,
147
+ totalOccurrences: occRow.n,
148
+ classifiedHosts: hostRow.n,
149
+ lastRun: lastRun
150
+ ? {
151
+ run_id: lastRun.run_id,
152
+ started_at: lastRun.started_at,
153
+ finished_at: lastRun.finished_at,
154
+ status: lastRun.status,
155
+ candidates_scanned: lastRun.candidates_scanned,
156
+ candidates_classified: lastRun.candidates_classified,
157
+ note: lastRun.note,
158
+ }
159
+ : null,
160
+ });
161
+ });
162
+ }
163
+ function parseLimit(v, fallback) {
164
+ const n = typeof v === "string" ? parseInt(v, 10) : Number(v);
165
+ if (!Number.isFinite(n) || n <= 0)
166
+ return fallback;
167
+ return Math.min(n, 1000);
168
+ }
@@ -0,0 +1,114 @@
1
+ import { createLogger } from "../../shared/logger.js";
2
+ import { extractUrls, normalizeUrl } from "../../shared/url-extractor.js";
3
+ const log = createLogger("mesh-api-links");
4
+ /**
5
+ * PATCH /api/links/:id 时,若改了 category/tags,要顺手在 memory 强化一条
6
+ * link_rule:<host> 规则(放在 link 出现过的 source_group,不放 global namespace)。
7
+ * global memory 必须走人工 promote(POST /api/memory/:id/promote),不能由 link override 直接写。
8
+ */
9
+ function overrideMemoryForLink(db, linkId, fields) {
10
+ const link = db.getLink(linkId);
11
+ if (!link)
12
+ return;
13
+ if (fields.category === undefined && fields.tags === undefined)
14
+ return;
15
+ const sourceGroups = db.listSourceGroupsForLink(linkId);
16
+ if (sourceGroups.length === 0) {
17
+ log.info(`Link ${linkId} override: no source_group, skip memory reinforce`);
18
+ return;
19
+ }
20
+ // 多群共享的 host 规则:取最近出现群(按 source_groups 写入顺序等价于发现顺序)
21
+ const groupId = sourceGroups[0];
22
+ const category = fields.category ?? "other";
23
+ const tags = Array.isArray(fields.tags) ? fields.tags : [];
24
+ const key = `link_rule:${link.host}`;
25
+ const value = `[人工 override] host=${link.host} 默认分类 ${category}; tags=[${tags.join(", ")}]`;
26
+ const summary = `${link.host} → ${category} (override)`;
27
+ const existing = db.db.prepare(`SELECT id FROM agent_memory WHERE key = ? AND group_id = ? AND active = 1 LIMIT 1`).get(key, groupId);
28
+ if (existing) {
29
+ db.updateMemory(existing.id, { value, summary, tags: ["link_classification", ...tags, "manual"], category: "convention" });
30
+ }
31
+ else {
32
+ const memId = `mem_link_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
33
+ db.addMemory({
34
+ id: memId,
35
+ scope: "group",
36
+ groupId,
37
+ category: "convention",
38
+ sourceType: "manual",
39
+ key,
40
+ value,
41
+ summary,
42
+ tags: ["link_classification", ...tags, "manual"],
43
+ visibility: "group",
44
+ agentVisible: true,
45
+ createdBy: "dashboard:link-override",
46
+ });
47
+ }
48
+ log.info(`Link ${linkId} override → memory ${key} written in group ${groupId}`);
49
+ }
50
+ export function registerLinkRoutes(apiRouter, db) {
51
+ /** POST /api/links/extract — 从文本抽 URL,返回 [{raw, norm, host}]。供调试 + 单测用。 */
52
+ apiRouter.post("/links/extract", (req, res) => {
53
+ const { text } = req.body ?? {};
54
+ if (typeof text !== "string") {
55
+ res.status(400).json({ error: "text (string) is required" });
56
+ return;
57
+ }
58
+ const items = extractUrls(text);
59
+ const urls = items.map((it) => {
60
+ const n = normalizeUrl(it.raw);
61
+ return n ? { raw: n.raw, norm: n.norm, host: n.host } : { raw: it.raw, norm: null, host: null };
62
+ }).filter((u) => u.norm !== null);
63
+ res.json({ urls });
64
+ });
65
+ /** GET /api/links — 列表 + 过滤(category / tag / search / group_id / host)。 */
66
+ apiRouter.get("/links", (req, res) => {
67
+ const category = typeof req.query.category === "string" ? req.query.category : undefined;
68
+ const tag = typeof req.query.tag === "string" ? req.query.tag : undefined;
69
+ const search = typeof req.query.search === "string" ? req.query.search : undefined;
70
+ const groupId = typeof req.query.group_id === "string" ? req.query.group_id : undefined;
71
+ const host = typeof req.query.host === "string" ? req.query.host : undefined;
72
+ const limit = Math.min(parseInt(req.query.limit) || 50, 500);
73
+ const offset = Math.max(parseInt(req.query.offset) || 0, 0);
74
+ const items = db.listLinks({ category, tag, search, groupId, host, limit, offset });
75
+ const total = db.countLinks({ category, tag, search, groupId, host });
76
+ res.json({ items, total, limit, offset });
77
+ });
78
+ /** GET /api/links/:id — 链接详情 + tags + occurrences + source_groups */
79
+ apiRouter.get("/links/:id", (req, res) => {
80
+ const link = db.getLink(req.params.id);
81
+ if (!link) {
82
+ res.status(404).json({ error: "Link not found" });
83
+ return;
84
+ }
85
+ const tags = db.listTagsForLink(link.id);
86
+ const occurrences = db.listOccurrencesForLink(link.id, 50);
87
+ const sourceGroups = db.listSourceGroupsForLink(link.id);
88
+ res.json({ ...link, tags, occurrences, source_groups: sourceGroups });
89
+ });
90
+ /** PATCH /api/links/:id — 人工 override(category/tags/title/summary)+ memory 强化。 */
91
+ apiRouter.patch("/links/:id", (req, res) => {
92
+ const link = db.getLink(req.params.id);
93
+ if (!link) {
94
+ res.status(404).json({ error: "Link not found" });
95
+ return;
96
+ }
97
+ const { category, tags, title, summary } = req.body ?? {};
98
+ const fields = {};
99
+ if (typeof category === "string")
100
+ fields.category = category;
101
+ if (Array.isArray(tags)) {
102
+ fields.tags = tags.filter((t) => typeof t === "string" && t.trim()).map((t) => t.trim());
103
+ }
104
+ if (typeof title === "string")
105
+ fields.title = title;
106
+ if (typeof summary === "string")
107
+ fields.summary = summary;
108
+ db.updateLinkClassification(link.id, fields);
109
+ overrideMemoryForLink(db, link.id, fields);
110
+ log.info(`Link ${link.id} patched: ${JSON.stringify(fields)}`);
111
+ const updated = db.getLink(link.id);
112
+ res.json({ ok: true, link: updated });
113
+ });
114
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Memory REST API —— agent_memory 表的 CRUD + search + 审核 + 统计。
3
+ *
4
+ * Dashboard 端点开放(无登录),与 notes 一致。agent-token 端点走 Bearer header。
5
+ * 旧 /groups/:id/notes 路由保留在 notes.ts,作为兼容层。
6
+ */
7
+ import { randomUUID } from "node:crypto";
8
+ import { createLogger } from "../../shared/logger.js";
9
+ const log = createLogger("mesh-api");
10
+ const CATEGORIES = ["fact", "decision", "convention", "pitfall", "todo", "playbook", "note"];
11
+ function isCategory(v) {
12
+ return typeof v === "string" && CATEGORIES.includes(v);
13
+ }
14
+ export function registerMemoryRoutes(apiRouter, db) {
15
+ // ── 列表(支持 type=note|memory|all)─────────────────────────────────
16
+ apiRouter.get("/groups/:groupId/memory", (req, res) => {
17
+ const group = db.getGroupById(req.params.groupId);
18
+ if (!group) {
19
+ res.status(404).json({ error: "Group not found" });
20
+ return;
21
+ }
22
+ const { category, key, tags, includePending, type } = req.query;
23
+ const tagArr = typeof tags === "string" ? tags.split(",").map(t => t.trim()).filter(Boolean) : undefined;
24
+ let agentVisible;
25
+ if (type === "note")
26
+ agentVisible = 0;
27
+ else if (type === "memory")
28
+ agentVisible = 1;
29
+ // type=all 或未指定 → undefined(两者都查)
30
+ res.json(db.listMemory({
31
+ scope: "group",
32
+ groupId: req.params.groupId,
33
+ category: isCategory(category) ? category : undefined,
34
+ key: typeof key === "string" ? key : undefined,
35
+ tags: tagArr,
36
+ includePending: includePending === "true" || includePending === "1",
37
+ agentVisible,
38
+ }));
39
+ });
40
+ // ── 全局记忆列表 ─────────────────────────────────────────────────────
41
+ apiRouter.get("/memory/global", (req, res) => {
42
+ const { category, key, tags, includePending, type } = req.query;
43
+ const tagArr = typeof tags === "string" ? tags.split(",").map(t => t.trim()).filter(Boolean) : undefined;
44
+ let agentVisible;
45
+ if (type === "note")
46
+ agentVisible = 0;
47
+ else if (type === "memory")
48
+ agentVisible = 1;
49
+ res.json(db.listMemory({
50
+ scope: "global",
51
+ category: isCategory(category) ? category : undefined,
52
+ key: typeof key === "string" ? key : undefined,
53
+ tags: tagArr,
54
+ includePending: includePending === "true" || includePending === "1",
55
+ agentVisible,
56
+ }));
57
+ });
58
+ // ── 关键词搜索(强制 agent_visible=1)──────────────────────────────────
59
+ apiRouter.get("/groups/:groupId/memory/search", (req, res) => {
60
+ const group = db.getGroupById(req.params.groupId);
61
+ if (!group) {
62
+ res.status(404).json({ error: "Group not found" });
63
+ return;
64
+ }
65
+ const q = typeof req.query.q === "string" ? req.query.q.trim() : "";
66
+ if (!q) {
67
+ res.status(400).json({ error: "q (keyword) is required" });
68
+ return;
69
+ }
70
+ const { category } = req.query;
71
+ // 群内 + 全局都搜
72
+ const groupHits = db.searchMemory(q, { scope: "group", groupId: req.params.groupId, category: isCategory(category) ? category : undefined });
73
+ const globalHits = db.searchMemory(q, { scope: "global", category: isCategory(category) ? category : undefined });
74
+ res.json({ group: groupHits, global: globalHits });
75
+ });
76
+ apiRouter.get("/memory/search", (req, res) => {
77
+ const q = typeof req.query.q === "string" ? req.query.q.trim() : "";
78
+ if (!q) {
79
+ res.status(400).json({ error: "q (keyword) is required" });
80
+ return;
81
+ }
82
+ const { category, scope, groupId } = req.query;
83
+ res.json(db.searchMemory(q, {
84
+ scope: scope === "global" || scope === "group" ? scope : undefined,
85
+ groupId: typeof groupId === "string" ? groupId : undefined,
86
+ category: isCategory(category) ? category : undefined,
87
+ }));
88
+ });
89
+ // ── 详情(memory 读时计 view_count;note 不计)────────────────────────
90
+ apiRouter.get("/memory/:id", (req, res) => {
91
+ const row = db.getMemory(req.params.id);
92
+ if (!row) {
93
+ res.status(404).json({ error: "Memory not found" });
94
+ return;
95
+ }
96
+ res.json(row);
97
+ });
98
+ // ── 新建(支持 agent_visible,默认 true=memory)───────────────────────
99
+ apiRouter.post("/groups/:groupId/memory", (req, res) => {
100
+ const group = db.getGroupById(req.params.groupId);
101
+ if (!group) {
102
+ res.status(404).json({ error: "Group not found" });
103
+ return;
104
+ }
105
+ if (group.archived_at) {
106
+ res.status(403).json({ error: "Group is archived" });
107
+ return;
108
+ }
109
+ const { key, value, summary, tags, category, visibility, agentVisible, createdBy, expiresAt, pendingReview } = req.body;
110
+ if (!key || !value || !createdBy) {
111
+ res.status(400).json({ error: "key, value, createdBy are required" });
112
+ return;
113
+ }
114
+ if (!isCategory(category)) {
115
+ res.status(400).json({ error: `category must be one of: ${CATEGORIES.join(",")}` });
116
+ return;
117
+ }
118
+ const id = randomUUID();
119
+ db.addMemory({
120
+ id, scope: "group", groupId: req.params.groupId,
121
+ category, key: String(key).trim(), value: String(value),
122
+ summary: summary == null ? undefined : String(summary),
123
+ tags: Array.isArray(tags) ? tags.map(String) : [],
124
+ visibility: visibility === "private" || visibility === "global" ? visibility : "group",
125
+ agentVisible: agentVisible === false ? false : true,
126
+ createdBy,
127
+ expiresAt: expiresAt == null ? null : String(expiresAt),
128
+ pendingReview: pendingReview === true,
129
+ });
130
+ log.info(`Memory created: "${key}" (${id}) cat=${category} pending=${pendingReview === true} in group ${req.params.groupId}`);
131
+ res.status(201).json({ id });
132
+ });
133
+ apiRouter.post("/memory/global", (req, res) => {
134
+ const { key, value, summary, tags, category, visibility, createdBy, expiresAt } = req.body;
135
+ if (!key || !value || !createdBy) {
136
+ res.status(400).json({ error: "key, value, createdBy are required" });
137
+ return;
138
+ }
139
+ if (!isCategory(category)) {
140
+ res.status(400).json({ error: `category must be one of: ${CATEGORIES.join(",")}` });
141
+ return;
142
+ }
143
+ // 全局 memory 强制走审核:agent_visible=0 + pending_review=1,等人工 approve。
144
+ // 已有 memory 想升级到 global,走 PATCH /memory/:id body { visibility: "global" }(走 promoteMemoryVisibility 路径)。
145
+ // 原因:全局 memory 直接对所有 agent 可见影响面大,必须有真人拍板。
146
+ const id = randomUUID();
147
+ db.addMemory({
148
+ id, scope: "global", groupId: null,
149
+ category, key: String(key).trim(), value: String(value),
150
+ summary: summary == null ? undefined : String(summary),
151
+ tags: Array.isArray(tags) ? tags.map(String) : [],
152
+ visibility: visibility === "private" || visibility === "group" ? visibility : "global",
153
+ agentVisible: false, // 强制:global memory 创建时对 agent 不可见
154
+ createdBy,
155
+ expiresAt: expiresAt == null ? null : String(expiresAt),
156
+ pendingReview: true, // 强制:必须等人工 approve
157
+ });
158
+ log.info(`Global memory created (pending review): "${key}" (${id}) cat=${category}`);
159
+ res.status(201).json({ id, pendingReview: true });
160
+ });
161
+ // ── 更新(可切换 agent_visible:note↔memory)───────────────────────────
162
+ apiRouter.patch("/memory/:id", (req, res) => {
163
+ const row = db.getMemory(req.params.id);
164
+ if (!row) {
165
+ res.status(404).json({ error: "Memory not found" });
166
+ return;
167
+ }
168
+ const { value, summary, tags, category, visibility, agentVisible, expiresAt } = req.body;
169
+ const fields = {};
170
+ if (value !== undefined)
171
+ fields.value = String(value);
172
+ if (summary !== undefined)
173
+ fields.summary = String(summary);
174
+ if (tags !== undefined)
175
+ fields.tags = Array.isArray(tags) ? tags.map(String) : [];
176
+ if (isCategory(category))
177
+ fields.category = category;
178
+ if (visibility === "private" || visibility === "group" || visibility === "global")
179
+ fields.visibility = visibility;
180
+ if (agentVisible !== undefined)
181
+ fields.agentVisible = !!agentVisible;
182
+ if (expiresAt !== undefined)
183
+ fields.expiresAt = expiresAt == null ? null : String(expiresAt);
184
+ db.updateMemory(req.params.id, fields);
185
+ res.json({ ok: true });
186
+ });
187
+ apiRouter.delete("/memory/:id", (req, res) => {
188
+ const row = db.getMemory(req.params.id);
189
+ if (!row) {
190
+ res.status(404).json({ error: "Memory not found" });
191
+ return;
192
+ }
193
+ db.deactivateMemory(req.params.id);
194
+ res.json({ ok: true });
195
+ });
196
+ apiRouter.post("/memory/:id/promote", (req, res) => {
197
+ const row = db.getMemory(req.params.id);
198
+ if (!row) {
199
+ res.status(404).json({ error: "Memory not found" });
200
+ return;
201
+ }
202
+ const target = req.body?.visibility === "global" ? "global" : req.body?.visibility === "private" ? "private" : "global";
203
+ db.promoteMemoryVisibility(req.params.id, target);
204
+ res.json({ ok: true });
205
+ });
206
+ apiRouter.post("/memory/:id/expire", (req, res) => {
207
+ const row = db.getMemory(req.params.id);
208
+ if (!row) {
209
+ res.status(404).json({ error: "Memory not found" });
210
+ return;
211
+ }
212
+ db.expireMemory(req.params.id);
213
+ res.json({ ok: true });
214
+ });
215
+ // ── 审核 ──────────────────────────────────────────────────────────────
216
+ apiRouter.get("/groups/:groupId/memory/pending", (req, res) => {
217
+ res.json(db.listPendingMemory("group", req.params.groupId));
218
+ });
219
+ apiRouter.get("/memory/pending", (req, res) => {
220
+ const { scope } = req.query;
221
+ res.json(db.listPendingMemory(scope === "global" || scope === "group" ? scope : undefined));
222
+ });
223
+ apiRouter.post("/memory/:id/approve", (req, res) => {
224
+ if (!db.getMemory(req.params.id)) {
225
+ res.status(404).json({ error: "Memory not found" });
226
+ return;
227
+ }
228
+ db.approveMemory(req.params.id);
229
+ res.json({ ok: true });
230
+ });
231
+ apiRouter.post("/memory/:id/reject", (req, res) => {
232
+ if (!db.getMemory(req.params.id)) {
233
+ res.status(404).json({ error: "Memory not found" });
234
+ return;
235
+ }
236
+ db.rejectMemory(req.params.id);
237
+ res.json({ ok: true });
238
+ });
239
+ // ── 统计 ──────────────────────────────────────────────────────────────
240
+ apiRouter.get("/groups/:groupId/memory/stats", (req, res) => {
241
+ res.json(db.memoryStats("group", req.params.groupId));
242
+ });
243
+ apiRouter.get("/memory/stats", (req, res) => {
244
+ const { scope } = req.query;
245
+ res.json(db.memoryStats(scope === "global" || scope === "group" ? scope : undefined));
246
+ });
247
+ // ── count(供 prompt 注入,轻量)──────────────────────────────────────
248
+ apiRouter.get("/groups/:groupId/memory/count", (req, res) => {
249
+ const group = db.getGroupById(req.params.groupId);
250
+ if (!group) {
251
+ res.status(404).json({ error: "Group not found" });
252
+ return;
253
+ }
254
+ res.json({
255
+ group: db.countMemory("group", req.params.groupId),
256
+ global: db.countMemory("global"),
257
+ });
258
+ });
259
+ }