@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.
- package/README.md +417 -0
- package/bin/mesh-master.sh +439 -0
- package/bin/rotom +29 -0
- package/bin/rotom-link.sh +136 -0
- package/bin/rotom-send-with-status +57 -0
- package/bin/rotom-up.sh +428 -0
- package/dist/cli/ask.js +62 -0
- package/dist/cli/common.js +321 -0
- package/dist/cli/config.js +65 -0
- package/dist/cli/directory.js +17 -0
- package/dist/cli/executor.js +58 -0
- package/dist/cli/fed.js +91 -0
- package/dist/cli/group.js +273 -0
- package/dist/cli/identity.js +62 -0
- package/dist/cli/init.js +268 -0
- package/dist/cli/issue.js +202 -0
- package/dist/cli/join.js +170 -0
- package/dist/cli/link.js +47 -0
- package/dist/cli/master.js +51 -0
- package/dist/cli/memory.js +307 -0
- package/dist/cli/note.js +68 -0
- package/dist/cli/repo.js +77 -0
- package/dist/cli/rotom.js +277 -0
- package/dist/cli/routes.js +118 -0
- package/dist/cli/run.js +45 -0
- package/dist/cli/schedule.js +237 -0
- package/dist/cli/skill.js +173 -0
- package/dist/cli/team.js +106 -0
- package/dist/executor/claude-code-hook.cjs +80 -0
- package/dist/executor/cli-executor.js +8 -0
- package/dist/executor/executors/claude-code.js +780 -0
- package/dist/executor/executors/codex.js +719 -0
- package/dist/executor/executors/hermes-cli.js +855 -0
- package/dist/executor/executors/openclaw.js +467 -0
- package/dist/executor/executors/pi.js +514 -0
- package/dist/executor/index.js +269 -0
- package/dist/executor/jsonrpc-transport.js +125 -0
- package/dist/executor/process-runner.js +101 -0
- package/dist/executor/reasoning-status.js +83 -0
- package/dist/executor/repo-cache.js +502 -0
- package/dist/executor/session-store.js +188 -0
- package/dist/executor/worker-chat.js +257 -0
- package/dist/executor/worker-connection.js +89 -0
- package/dist/executor/worker-issue.js +264 -0
- package/dist/executor/worker.js +877 -0
- package/dist/link/pending-requests.js +72 -0
- package/dist/link/server.js +233 -0
- package/dist/link/visibility-store.js +58 -0
- package/dist/master/api/agents.js +333 -0
- package/dist/master/api/artifacts.js +271 -0
- package/dist/master/api/domains.js +64 -0
- package/dist/master/api/groups.js +635 -0
- package/dist/master/api/guidance-templates.js +147 -0
- package/dist/master/api/index.js +89 -0
- package/dist/master/api/issues-patrol.js +172 -0
- package/dist/master/api/issues.js +663 -0
- package/dist/master/api/links-patrol.js +168 -0
- package/dist/master/api/links.js +114 -0
- package/dist/master/api/memory.js +259 -0
- package/dist/master/api/messages.js +157 -0
- package/dist/master/api/notes.js +77 -0
- package/dist/master/api/schedule-patterns.js +133 -0
- package/dist/master/api/schedules.js +272 -0
- package/dist/master/api/sessions.js +158 -0
- package/dist/master/api/share.js +269 -0
- package/dist/master/api/skills.js +190 -0
- package/dist/master/api/teams.js +122 -0
- package/dist/master/api/uploads.js +245 -0
- package/dist/master/auth.js +134 -0
- package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
- package/dist/master/dashboard/animations/calico-error.apng +0 -0
- package/dist/master/dashboard/animations/calico-happy.apng +0 -0
- package/dist/master/dashboard/animations/calico-notification.apng +0 -0
- package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
- package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
- package/dist/master/dashboard/animations/calico-waking.apng +0 -0
- package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
- package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
- package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
- package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
- package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
- package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
- package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
- package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
- package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
- package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
- package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
- package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
- package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
- package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
- package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
- package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
- package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
- package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
- package/dist/master/dashboard/debug-auth.html +197 -0
- package/dist/master/dashboard/favicon.ico +0 -0
- package/dist/master/dashboard/index.html +20 -0
- package/dist/master/dashboard/rotom-avatar.png +0 -0
- package/dist/master/db/agent-sessions.js +60 -0
- package/dist/master/db/agent-visibility.js +64 -0
- package/dist/master/db/agents.js +119 -0
- package/dist/master/db/ask-bridges.js +157 -0
- package/dist/master/db/build-update.js +59 -0
- package/dist/master/db/core.js +82 -0
- package/dist/master/db/domains.js +80 -0
- package/dist/master/db/groups.js +316 -0
- package/dist/master/db/guidance-templates.js +58 -0
- package/dist/master/db/index.js +12 -0
- package/dist/master/db/internal.js +45 -0
- package/dist/master/db/issues-patrol.js +81 -0
- package/dist/master/db/issues.js +373 -0
- package/dist/master/db/links.js +221 -0
- package/dist/master/db/master-node.js +43 -0
- package/dist/master/db/memory.js +272 -0
- package/dist/master/db/messages.js +210 -0
- package/dist/master/db/notes.js +55 -0
- package/dist/master/db/schedule-patterns.js +56 -0
- package/dist/master/db/schedules.js +135 -0
- package/dist/master/db/skills.js +144 -0
- package/dist/master/db/team.js +88 -0
- package/dist/master/db/types.js +10 -0
- package/dist/master/db.js +12 -0
- package/dist/master/embedded.js +133 -0
- package/dist/master/federation/client.js +283 -0
- package/dist/master/federation/identity.js +133 -0
- package/dist/master/federation/manager.js +267 -0
- package/dist/master/federation/publisher.js +87 -0
- package/dist/master/federation/self-publisher.js +69 -0
- package/dist/master/federation/server.js +487 -0
- package/dist/master/group-paths.js +208 -0
- package/dist/master/offline-queue.js +38 -0
- package/dist/master/opc-bootstrap.js +245 -0
- package/dist/master/patrol-terminal.js +275 -0
- package/dist/master/repo-scan.js +188 -0
- package/dist/master/router.js +214 -0
- package/dist/master/scheduler-handlers.js +510 -0
- package/dist/master/scheduler.js +201 -0
- package/dist/master/server.js +203 -0
- package/dist/master/services/link-collector.js +82 -0
- package/dist/master/services/link-patrol-bootstrap.js +50 -0
- package/dist/master/services/memory-extract-prompt.js +34 -0
- package/dist/master/services/patrol-bootstrap.js +63 -0
- package/dist/master/share-tokens.js +56 -0
- package/dist/master/terminal-hub.js +300 -0
- package/dist/master/uploads.js +108 -0
- package/dist/master/util/fs.js +100 -0
- package/dist/master/util/paths.js +50 -0
- package/dist/master/util/persona.js +10 -0
- package/dist/master/ws-hub/connection.js +928 -0
- package/dist/master/ws-hub/conversation.js +290 -0
- package/dist/master/ws-hub/directory.js +70 -0
- package/dist/master/ws-hub/dispatch-enrich.js +34 -0
- package/dist/master/ws-hub/hub.js +136 -0
- package/dist/master/ws-hub/index.js +9 -0
- package/dist/master/ws-hub/internal.js +35 -0
- package/dist/master/ws-hub/routing.js +295 -0
- package/dist/master/ws-hub/sessions.js +130 -0
- package/dist/master/ws-hub.js +11 -0
- package/dist/shared/agent-profile.js +44 -0
- package/dist/shared/constants.js +55 -0
- package/dist/shared/dedup.js +33 -0
- package/dist/shared/group-context.js +62 -0
- package/dist/shared/json-codec.js +33 -0
- package/dist/shared/logger.js +136 -0
- package/dist/shared/mention.js +22 -0
- package/dist/shared/network.js +40 -0
- package/dist/shared/parse.js +18 -0
- package/dist/shared/prompt-composer.js +171 -0
- package/dist/shared/protocol/client-messages.js +8 -0
- package/dist/shared/protocol/enums.js +6 -0
- package/dist/shared/protocol/federation.js +62 -0
- package/dist/shared/protocol/guards.js +87 -0
- package/dist/shared/protocol/server-messages.js +8 -0
- package/dist/shared/protocol/types.js +8 -0
- package/dist/shared/protocol.js +19 -0
- package/dist/shared/readonly-allowlist.js +122 -0
- package/dist/shared/rotom-cli-prompt.js +23 -0
- package/dist/shared/skill-context.js +19 -0
- package/dist/shared/skill-md.js +43 -0
- package/dist/shared/slash-commands.js +50 -0
- package/dist/shared/time.js +80 -0
- package/dist/shared/title.js +46 -0
- package/dist/shared/url-extractor.js +99 -0
- package/migrations/001-schema.sql +942 -0
- package/package.json +68 -0
- package/scripts/fix-node-pty-perms.mjs +46 -0
- package/skill/rotom-a2a-communicate/SKILL.md +257 -0
- package/skill/rotom-bus-host/SKILL.md +78 -0
- 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
|
+
}
|