@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,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ask_bridges —— Agent A 提问 B 后的「等回复 + 超时兜底」bridge 记录。
|
|
3
|
+
*
|
|
4
|
+
* 生命周期:pending → answered(B @ A 被检测) / timed_out(5min 到点创建 Issue
|
|
5
|
+
* 给 A) / cancelled(A 主动撤销)。
|
|
6
|
+
*
|
|
7
|
+
* scheduler 每 30s tick 扫 pending 行:先查 B 是否 @ 过 A(json_each 解析
|
|
8
|
+
* mentions JSON 数组),命中则 answered;否则若 expires_at < now,查 B 是否有
|
|
9
|
+
* 非 @ 回复,scheduler 据此创建复述 Issue 或升级 Issue,mark timed_out。
|
|
10
|
+
*
|
|
11
|
+
* 详细设计见 docs/AGENT_ASK_REPLY_TIMER.md 方案 C。
|
|
12
|
+
*/
|
|
13
|
+
export const askBridgeMethods = {
|
|
14
|
+
createAskBridge(input) {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
this.db.prepare(`
|
|
17
|
+
INSERT INTO ask_bridges
|
|
18
|
+
(id, group_id, asker, target, question_msg_id, escalate_to,
|
|
19
|
+
timeout_ms, created_at, expires_at, status)
|
|
20
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')
|
|
21
|
+
`).run(input.id, input.groupId, input.asker, input.target, input.questionMsgId, input.escalateTo, input.timeoutMs, now, now + input.timeoutMs);
|
|
22
|
+
return this.db.prepare("SELECT * FROM ask_bridges WHERE id = ?")
|
|
23
|
+
.get(input.id);
|
|
24
|
+
},
|
|
25
|
+
getAskBridge(id) {
|
|
26
|
+
return this.db.prepare("SELECT * FROM ask_bridges WHERE id = ?")
|
|
27
|
+
.get(id);
|
|
28
|
+
},
|
|
29
|
+
listAskBridges(filter) {
|
|
30
|
+
const where = [];
|
|
31
|
+
const params = [];
|
|
32
|
+
if (filter?.groupId) {
|
|
33
|
+
where.push("group_id = ?");
|
|
34
|
+
params.push(filter.groupId);
|
|
35
|
+
}
|
|
36
|
+
if (filter?.asker) {
|
|
37
|
+
where.push("asker = ?");
|
|
38
|
+
params.push(filter.asker);
|
|
39
|
+
}
|
|
40
|
+
if (filter?.status) {
|
|
41
|
+
where.push("status = ?");
|
|
42
|
+
params.push(filter.status);
|
|
43
|
+
}
|
|
44
|
+
const sql = `SELECT * FROM ask_bridges ${where.length ? "WHERE " + where.join(" AND ") : ""} ORDER BY created_at DESC`;
|
|
45
|
+
return this.db.prepare(sql).all(...params);
|
|
46
|
+
},
|
|
47
|
+
/** scheduler tick 用:返回所有 pending bridge(不论是否到期,都要先查 @ 回复)。 */
|
|
48
|
+
getPendingAskBridges() {
|
|
49
|
+
return this.db.prepare("SELECT * FROM ask_bridges WHERE status = 'pending' ORDER BY expires_at ASC").all();
|
|
50
|
+
},
|
|
51
|
+
/**
|
|
52
|
+
* 查 B 是否在 question_msg_id 之后 @ 过 A。
|
|
53
|
+
* mentions 是 JSON 数组字符串,用 json_each 精确匹配(避免子串误命中)。
|
|
54
|
+
* 返回最早一条 @ 回复(scheduler 据此 mark answered)。
|
|
55
|
+
*/
|
|
56
|
+
findAtReplyForBridge(bridge) {
|
|
57
|
+
return this.db.prepare(`
|
|
58
|
+
SELECT m.id, m.group_id, m.sender, m.content, m.mentions, m.created_at, m.cancelled_at
|
|
59
|
+
FROM group_messages m, json_each(m.mentions)
|
|
60
|
+
WHERE m.group_id = ?
|
|
61
|
+
AND m.id > ?
|
|
62
|
+
AND m.sender = ?
|
|
63
|
+
AND json_each.value = ?
|
|
64
|
+
ORDER BY m.id ASC
|
|
65
|
+
LIMIT 1
|
|
66
|
+
`).get(bridge.group_id, bridge.question_msg_id, bridge.target, bridge.asker);
|
|
67
|
+
},
|
|
68
|
+
/**
|
|
69
|
+
* 查 B 在 question_msg_id 之后的最新一条非 @ 回复(超时复述用)。
|
|
70
|
+
* 不过滤 mentions——@ 的也包含,因为 @ 的回复若 timer 还没 tick 到,A 的 worker
|
|
71
|
+
* 已被 master 正常 dispatch 触发处理;若 timer tick 到了还没 mark answered,
|
|
72
|
+
* 说明 @ 检测没命中(理论上不应发生,但兜底),仍取最新一条作复述。
|
|
73
|
+
*/
|
|
74
|
+
findLatestReplyForBridge(bridge) {
|
|
75
|
+
return this.db.prepare(`
|
|
76
|
+
SELECT m.id, m.group_id, m.sender, m.content, m.mentions, m.created_at, m.cancelled_at
|
|
77
|
+
FROM group_messages m
|
|
78
|
+
WHERE m.group_id = ?
|
|
79
|
+
AND m.id > ?
|
|
80
|
+
AND m.sender = ?
|
|
81
|
+
AND m.content != ''
|
|
82
|
+
AND m.cancelled_at IS NULL
|
|
83
|
+
ORDER BY m.id DESC
|
|
84
|
+
LIMIT 1
|
|
85
|
+
`).get(bridge.group_id, bridge.question_msg_id, bridge.target);
|
|
86
|
+
},
|
|
87
|
+
markBridgeAnswered(id, replyMsgId) {
|
|
88
|
+
this.db.prepare(`
|
|
89
|
+
UPDATE ask_bridges
|
|
90
|
+
SET status = 'answered', reply_msg_id = ?, resolved_at = ?
|
|
91
|
+
WHERE id = ? AND status = 'pending'
|
|
92
|
+
`).run(replyMsgId, Date.now(), id);
|
|
93
|
+
},
|
|
94
|
+
markBridgeTimedOut(id, issueId, replyMsgId) {
|
|
95
|
+
this.db.prepare(`
|
|
96
|
+
UPDATE ask_bridges
|
|
97
|
+
SET status = 'timed_out', issue_id = ?, reply_msg_id = ?, resolved_at = ?
|
|
98
|
+
WHERE id = ? AND status = 'pending'
|
|
99
|
+
`).run(issueId, replyMsgId, Date.now(), id);
|
|
100
|
+
},
|
|
101
|
+
cancelBridge(id) {
|
|
102
|
+
const result = this.db.prepare(`
|
|
103
|
+
UPDATE ask_bridges
|
|
104
|
+
SET status = 'cancelled', resolved_at = ?
|
|
105
|
+
WHERE id = ? AND status = 'pending'
|
|
106
|
+
`).run(Date.now(), id);
|
|
107
|
+
return result.changes > 0;
|
|
108
|
+
},
|
|
109
|
+
/** 取 group_messages.content;scheduler 创建超时 Issue 时复述原问题用。 */
|
|
110
|
+
getGroupMessageContent(msgId) {
|
|
111
|
+
const row = this.db.prepare("SELECT content FROM group_messages WHERE id = ?")
|
|
112
|
+
.get(msgId);
|
|
113
|
+
return row?.content;
|
|
114
|
+
},
|
|
115
|
+
/**
|
|
116
|
+
* 事件式检测:给定一条新群消息(sender, mentions),返回这条消息"答中"的 pending bridge。
|
|
117
|
+
* 答中条件:bridge.target = sender AND bridge.asker ∈ mentions AND status=pending。
|
|
118
|
+
* ws-hub 在 addGroupMessage 后调这个,命中即 markBridgeAnswered + disable 对应 schedule。
|
|
119
|
+
* 返回 bridge 列表(理论上最多 1 条,因为同 target 同时只该有 1 个 pending;但
|
|
120
|
+
* 万一有并发,全部返回让上层处理)。
|
|
121
|
+
*/
|
|
122
|
+
findBridgesAnsweredByMessage(groupId, sender, mentions) {
|
|
123
|
+
if (mentions.length === 0)
|
|
124
|
+
return [];
|
|
125
|
+
// 用 json_each 反查不太合适(我们要的是 bridge.asker ∈ mentions,不是 mentions ∈ bridge)
|
|
126
|
+
// 直接 SELECT pending bridges where target=sender,然后内存里过滤 asker ∈ mentions
|
|
127
|
+
const placeholders = mentions.map(() => "?").join(",");
|
|
128
|
+
return this.db.prepare(`
|
|
129
|
+
SELECT * FROM ask_bridges
|
|
130
|
+
WHERE group_id = ?
|
|
131
|
+
AND target = ?
|
|
132
|
+
AND status = 'pending'
|
|
133
|
+
AND asker IN (${placeholders})
|
|
134
|
+
`).all(groupId, sender, ...mentions);
|
|
135
|
+
},
|
|
136
|
+
/**
|
|
137
|
+
* 按 name 查 scheduled_tasks(legacy,仍保留给历史 task name 兜底)。
|
|
138
|
+
* 新代码请用 findAskBridgeScheduledTask —— name 已改成"星期五 · …"友好文案,
|
|
139
|
+
* 不再适合做 lookup key。
|
|
140
|
+
*/
|
|
141
|
+
findScheduledTaskByName(name) {
|
|
142
|
+
return this.db.prepare("SELECT id, enabled FROM scheduled_tasks WHERE name = ? ORDER BY id DESC LIMIT 1")
|
|
143
|
+
.get(name);
|
|
144
|
+
},
|
|
145
|
+
/**
|
|
146
|
+
* 按 bridgeId 查对应的 ask-bridge scheduled_task(handler_key='ask-bridge-check'
|
|
147
|
+
* 且 handler_payload 里含该 bridgeId)。name 改成"星期五 · 等待 X 回复"友好文案后,
|
|
148
|
+
* 这是 cancel 路径的权威查找方式。bridgeId 是 UUID,LIKE 不会误匹配。
|
|
149
|
+
*/
|
|
150
|
+
findAskBridgeScheduledTask(bridgeId) {
|
|
151
|
+
return this.db.prepare("SELECT id, enabled FROM scheduled_tasks WHERE handler_key = 'ask-bridge-check' AND handler_payload LIKE ? ORDER BY id DESC LIMIT 1").get(`%"bridgeId":"${bridgeId}"%`);
|
|
152
|
+
},
|
|
153
|
+
/** 查是否有 pending bridge where asker+target 匹配(用于 autoCreate 防重 + 区分提问/回复)。 */
|
|
154
|
+
findPendingBridge(groupId, asker, target) {
|
|
155
|
+
return this.db.prepare("SELECT * FROM ask_bridges WHERE group_id = ? AND asker = ? AND target = ? AND status = 'pending' ORDER BY created_at DESC LIMIT 1").get(groupId, asker, target);
|
|
156
|
+
},
|
|
157
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic `UPDATE ... SET` builder.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the 8+ scattered `const sets = []; if (field !== undefined) { ... }`
|
|
5
|
+
* patterns in db/issues.ts, db/memory.ts, db/skills.ts, db/agents.ts,
|
|
6
|
+
* db/domains.ts, db/guidance-templates.ts, db/schedules.ts,
|
|
7
|
+
* db/schedule-patterns.ts, db/notes.ts.
|
|
8
|
+
*
|
|
9
|
+
* Returns `null` when there are no set clauses (caller can short-circuit
|
|
10
|
+
* without running a no-op UPDATE). Otherwise returns the SQL string and the
|
|
11
|
+
* ordered bind params (set params first, then where params).
|
|
12
|
+
*
|
|
13
|
+
* `updatedAt` controls the auto-pushed `updated_at` column shape so each
|
|
14
|
+
* table sticks to its existing convention:
|
|
15
|
+
* - "beijing" → `?` bound to `nowBeijing()` (Beijing "YYYY-MM-DD HH:MM:SS.mmm")
|
|
16
|
+
* - "datetime-now" → `datetime('now')` (SQLite UTC, no param)
|
|
17
|
+
* - "epoch" → `?` bound to `Date.now()` (epoch ms)
|
|
18
|
+
* - false / undefined → no auto-pushed updated_at
|
|
19
|
+
*/
|
|
20
|
+
import { nowBeijing } from "../../shared/time.js";
|
|
21
|
+
export function buildUpdate(opts) {
|
|
22
|
+
const setClauses = [];
|
|
23
|
+
const setParams = [];
|
|
24
|
+
for (const [column, value] of Object.entries(opts.sets)) {
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
continue;
|
|
27
|
+
setClauses.push(`${column} = ?`);
|
|
28
|
+
setParams.push(value);
|
|
29
|
+
}
|
|
30
|
+
if (opts.extraSets) {
|
|
31
|
+
for (const ex of opts.extraSets) {
|
|
32
|
+
if ("sql" in ex) {
|
|
33
|
+
setClauses.push(ex.sql);
|
|
34
|
+
if (ex.params)
|
|
35
|
+
setParams.push(...ex.params);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
setClauses.push(`${ex.column} = ?`);
|
|
39
|
+
setParams.push(ex.value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const mode = opts.updatedAt ?? false;
|
|
44
|
+
if (mode === "beijing") {
|
|
45
|
+
setClauses.push("updated_at = ?");
|
|
46
|
+
setParams.push(nowBeijing());
|
|
47
|
+
}
|
|
48
|
+
else if (mode === "epoch") {
|
|
49
|
+
setClauses.push("updated_at = ?");
|
|
50
|
+
setParams.push(Date.now());
|
|
51
|
+
}
|
|
52
|
+
else if (mode === "datetime-now") {
|
|
53
|
+
setClauses.push("updated_at = datetime('now')");
|
|
54
|
+
}
|
|
55
|
+
if (setClauses.length === 0)
|
|
56
|
+
return null;
|
|
57
|
+
const sql = `UPDATE ${opts.table} SET ${setClauses.join(", ")} WHERE ${opts.where}`;
|
|
58
|
+
return { sql, params: [...setParams, ...opts.whereParams] };
|
|
59
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeshDbCore — constructor, schema migration, lifecycle hooks.
|
|
3
|
+
*
|
|
4
|
+
* Domain modules (./agents.ts, ./issues.ts, ...) attach their methods to the
|
|
5
|
+
* `MeshDb` class via `Object.assign(this, ...)` in the subclass constructor.
|
|
6
|
+
* They type their methods against this interface so cross-domain calls
|
|
7
|
+
* (e.g. messages.enqueueOffline → agents.getAgentById) type-check.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
// better-sqlite3 is optionalDependency (only needed for Master).
|
|
14
|
+
// Dynamic import keeps dashboard builds from pulling it in.
|
|
15
|
+
let Database;
|
|
16
|
+
try {
|
|
17
|
+
Database = (await import("better-sqlite3")).default;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Will remain undefined — MeshDb constructor will throw a clear error.
|
|
21
|
+
}
|
|
22
|
+
export class MeshDbCore {
|
|
23
|
+
db;
|
|
24
|
+
/** Hook fired when an issue transitions to a terminal state. */
|
|
25
|
+
_onIssueTerminal;
|
|
26
|
+
constructor(dbPath) {
|
|
27
|
+
if (!Database) {
|
|
28
|
+
throw new Error("better-sqlite3 is required for Master mode but not installed.\n" +
|
|
29
|
+
"Run: pnpm install better-sqlite3 (or npm install better-sqlite3)");
|
|
30
|
+
}
|
|
31
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
32
|
+
this.db = new Database(dbPath);
|
|
33
|
+
this.db.pragma("journal_mode = WAL");
|
|
34
|
+
this.db.pragma("synchronous = NORMAL");
|
|
35
|
+
this.db.pragma("foreign_keys = ON");
|
|
36
|
+
this.migrate();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Migration with version tracking.
|
|
40
|
+
*
|
|
41
|
+
* Walks ./migrations/ at the project root, applies any *.sql files whose
|
|
42
|
+
* numeric prefix is not yet recorded in schema_version. Falls back to
|
|
43
|
+
* ../migrations if the file was moved (kept for the build artifacts layout
|
|
44
|
+
* where dist/master/db/internal.js resolves one level deeper).
|
|
45
|
+
*/
|
|
46
|
+
migrate() {
|
|
47
|
+
this.db.exec(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
49
|
+
version INTEGER PRIMARY KEY,
|
|
50
|
+
applied_at TEXT DEFAULT (datetime('now'))
|
|
51
|
+
)
|
|
52
|
+
`);
|
|
53
|
+
const applied = new Set(this.db.prepare("SELECT version FROM schema_version").all()
|
|
54
|
+
.map(r => r.version));
|
|
55
|
+
let migDir = path.resolve(__dirname, "../../../../migrations");
|
|
56
|
+
if (!fs.existsSync(migDir)) {
|
|
57
|
+
migDir = path.resolve(__dirname, "../../../migrations");
|
|
58
|
+
}
|
|
59
|
+
const files = fs.readdirSync(migDir).filter(f => f.endsWith(".sql")).sort();
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const match = file.match(/^(\d+)/);
|
|
62
|
+
if (!match)
|
|
63
|
+
continue;
|
|
64
|
+
const version = parseInt(match[1], 10);
|
|
65
|
+
if (applied.has(version))
|
|
66
|
+
continue;
|
|
67
|
+
const sql = fs.readFileSync(path.join(migDir, file), "utf-8");
|
|
68
|
+
this.db.exec(sql);
|
|
69
|
+
this.db.prepare("INSERT INTO schema_version (version) VALUES (?)").run(version);
|
|
70
|
+
}
|
|
71
|
+
// Inline migration: add endpoint column if missing (safe to re-run).
|
|
72
|
+
try {
|
|
73
|
+
this.db.exec("ALTER TABLE agents ADD COLUMN endpoint TEXT");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Column already exists — ignore.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
close() {
|
|
80
|
+
this.db.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domains — department CRUD + cross-domain routing rules.
|
|
3
|
+
*
|
|
4
|
+
* Methods attach to a `MeshDb` instance via `Object.assign`. Cross-module
|
|
5
|
+
* mutation: `renameDomainInAgents` writes the agents table (owned by
|
|
6
|
+
* ./agents.ts) — that's just a SQL write, no method call, so no cross-domain
|
|
7
|
+
* coupling beyond the SQL schema.
|
|
8
|
+
*/
|
|
9
|
+
import { buildUpdate } from "./build-update.js";
|
|
10
|
+
export const domainMethods = {
|
|
11
|
+
listDomains() {
|
|
12
|
+
return this.db.prepare("SELECT * FROM domains ORDER BY name").all();
|
|
13
|
+
},
|
|
14
|
+
getDomainByName(name) {
|
|
15
|
+
return this.db.prepare("SELECT * FROM domains WHERE name = ?").get(name);
|
|
16
|
+
},
|
|
17
|
+
getDomainById(id) {
|
|
18
|
+
return this.db.prepare("SELECT * FROM domains WHERE id = ?").get(id);
|
|
19
|
+
},
|
|
20
|
+
insertDomain(id, name, description) {
|
|
21
|
+
this.db.prepare("INSERT INTO domains (id, name, description) VALUES (?, ?, ?)").run(id, name, description || null);
|
|
22
|
+
},
|
|
23
|
+
updateDomain(id, meta) {
|
|
24
|
+
const built = buildUpdate({
|
|
25
|
+
table: "domains",
|
|
26
|
+
sets: { name: meta.name, description: meta.description },
|
|
27
|
+
where: "id = ?",
|
|
28
|
+
whereParams: [id],
|
|
29
|
+
updatedAt: false,
|
|
30
|
+
});
|
|
31
|
+
if (built)
|
|
32
|
+
this.db.prepare(built.sql).run(...built.params);
|
|
33
|
+
},
|
|
34
|
+
deleteDomain(id) {
|
|
35
|
+
this.db.prepare("DELETE FROM domains WHERE id = ?").run(id);
|
|
36
|
+
},
|
|
37
|
+
/** Rename domain in all agents (used when domain name changes). */
|
|
38
|
+
renameDomainInAgents(oldName, newName) {
|
|
39
|
+
this.db.prepare("UPDATE agents SET domain = ?, updated_at = datetime('now') WHERE domain = ?").run(newName, oldName);
|
|
40
|
+
// Also update cross_domain_rules
|
|
41
|
+
this.db.prepare("UPDATE cross_domain_rules SET from_domain = ? WHERE from_domain = ?").run(newName, oldName);
|
|
42
|
+
this.db.prepare("UPDATE cross_domain_rules SET to_domain = ? WHERE to_domain = ?").run(newName, oldName);
|
|
43
|
+
},
|
|
44
|
+
/** Count agents belonging to a domain (by domain name). */
|
|
45
|
+
countAgentsByDomain(domainName) {
|
|
46
|
+
const row = this.db.prepare("SELECT COUNT(*) as c FROM agents WHERE domain = ?").get(domainName);
|
|
47
|
+
return row.c;
|
|
48
|
+
},
|
|
49
|
+
canCrossDomain(from, to) {
|
|
50
|
+
// No domain set → no isolation
|
|
51
|
+
if (!from || !to)
|
|
52
|
+
return true;
|
|
53
|
+
// Same domain → always OK
|
|
54
|
+
if (from === to)
|
|
55
|
+
return true;
|
|
56
|
+
// Check explicit rule
|
|
57
|
+
return !!this.db.prepare("SELECT 1 FROM cross_domain_rules WHERE from_domain = ? AND to_domain = ?").get(from, to);
|
|
58
|
+
},
|
|
59
|
+
/** Add cross-domain rule. Set bidirectional=true to create both A→B and B→A. */
|
|
60
|
+
addCrossDomainRule(from, to, bidirectional = false) {
|
|
61
|
+
this.db.prepare("INSERT OR IGNORE INTO cross_domain_rules (from_domain, to_domain) VALUES (?, ?)").run(from, to);
|
|
62
|
+
if (bidirectional) {
|
|
63
|
+
this.db.prepare("INSERT OR IGNORE INTO cross_domain_rules (from_domain, to_domain) VALUES (?, ?)").run(to, from);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
/** List all cross-domain rules. */
|
|
67
|
+
listCrossDomainRules() {
|
|
68
|
+
return this.db.prepare("SELECT from_domain, to_domain FROM cross_domain_rules ORDER BY from_domain, to_domain").all();
|
|
69
|
+
},
|
|
70
|
+
/** Count cross-domain rules referencing a domain (as source or target). */
|
|
71
|
+
countCrossDomainRulesByDomain(domainName) {
|
|
72
|
+
const row = this.db.prepare("SELECT COUNT(*) as c FROM cross_domain_rules WHERE from_domain = ? OR to_domain = ?").get(domainName, domainName);
|
|
73
|
+
return row.c;
|
|
74
|
+
},
|
|
75
|
+
/** Delete a cross-domain rule. */
|
|
76
|
+
deleteCrossDomainRule(from, to) {
|
|
77
|
+
const result = this.db.prepare("DELETE FROM cross_domain_rules WHERE from_domain = ? AND to_domain = ?").run(from, to);
|
|
78
|
+
return result.changes > 0;
|
|
79
|
+
},
|
|
80
|
+
};
|