@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,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ๅทกๆฃ issue ็ปๆๅค็ โโ ๅทกๆฃๅๅฎๆ patrol issue ๅ,่งฃๆ result JSON ่ฝๅบใ
|
|
3
|
+
*
|
|
4
|
+
* ็ฑ server.ts ็ _onIssueTerminal hook ๅจ issue ่ฟๅ
ฅ terminal ็ถๆๆถ่ฐ็จใ
|
|
5
|
+
* ่ท e2ed/sync.js ๅ็บง,่่ดฃๅไธ:่งฃๆ result โ ๅ issue_patrol_logs + finish runใ
|
|
6
|
+
*
|
|
7
|
+
* ๅ
ฅๅฃๅไธคๆก่ทฏๅพ:
|
|
8
|
+
* - handleIssuePatrolTerminal: issue-patrol ๅทกๆฃๅ โ ๅ issue_patrol_logs
|
|
9
|
+
* - handleLinkPatrolIssueTerminal: link-patrol ๅทกๆฃๅ โ UPDATE links + ๅ link_patrol_logs + memory
|
|
10
|
+
*
|
|
11
|
+
* ็ปไธๅ
ฅๅฃ dispatchPatrolTerminal(issue) ๅ
ๆ issueId ๅๆฅ link_patrol_runs,
|
|
12
|
+
* ๅฝไธญ่ตฐ link ๆต็จ,ๅฆๅ fallback ๅฐ issue ๆต็จใ
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import { createLogger } from "../shared/logger.js";
|
|
16
|
+
const log = createLogger("mesh-patrol-terminal");
|
|
17
|
+
/**
|
|
18
|
+
* ็ปไธๅ
ฅๅฃ:issue ็ปๆๆถ server.ts ่ฐ่ฟไธชใ
|
|
19
|
+
* ไผๅ
ๅๆฅ link_patrol_runs.patrol_issue_id โโ ๅฝไธญ่ตฐ link ๅ็ฑปๆต็จ,
|
|
20
|
+
* ๅฆๅ fallback ๅฐ issue ๅทกๆฃๆต็จใ
|
|
21
|
+
*/
|
|
22
|
+
export function dispatchPatrolTerminal(db, issue) {
|
|
23
|
+
const linkRun = db.getLinkPatrolRunByIssueId(issue.id);
|
|
24
|
+
if (linkRun) {
|
|
25
|
+
handleLinkPatrolIssueTerminal(db, issue);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
handleIssuePatrolTerminal(db, issue);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* ่งฃๆ patrol issue ็ result(JSON ๆฐ็ป),้ๆกๅ issue_patrol_logs,
|
|
32
|
+
* ๅนถ finish ๅฏนๅบ็ runใ่งฃๆๅคฑ่ดฅ โ run status='error'ใ
|
|
33
|
+
*/
|
|
34
|
+
export function handleIssuePatrolTerminal(db, issue) {
|
|
35
|
+
const run = db.getPatrolRunByIssueId(issue.id);
|
|
36
|
+
if (!run) {
|
|
37
|
+
log.warn(`patrol issue ${issue.id} terminal, but no run found (orphan?)`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// ๅทกๆฃๅๆ JSON ๆพๅจ issue.result ้ใresult ๅฏ่ฝๆฏ่ฃธ JSON,ไนๅฏ่ฝ่ขซๅ
ๅจ markdown ไปฃ็ ๅ้ใ
|
|
41
|
+
const raw = (issue.result ?? "").trim();
|
|
42
|
+
if (!raw) {
|
|
43
|
+
db.finishPatrolRun(run.run_id, "error", { note: "empty result" });
|
|
44
|
+
log.warn(`patrol run ${run.run_id}: empty result`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const parsed = parseVerdicts(raw);
|
|
48
|
+
if (!parsed) {
|
|
49
|
+
db.finishPatrolRun(run.run_id, "error", { note: `bad result JSON: ${raw.slice(0, 200)}` });
|
|
50
|
+
log.error(`patrol run ${run.run_id}: bad result JSON`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let readyCount = 0;
|
|
54
|
+
for (const v of parsed) {
|
|
55
|
+
const verdict = normalizeVerdict(v.verdict);
|
|
56
|
+
if (verdict === "ready")
|
|
57
|
+
readyCount++;
|
|
58
|
+
db.insertPatrolLog({
|
|
59
|
+
id: randomUUID(),
|
|
60
|
+
runId: run.run_id,
|
|
61
|
+
patrolGroupId: run.patrol_group_id,
|
|
62
|
+
issueId: v.issue_id ?? null,
|
|
63
|
+
candidateGroupId: resolveCandidateGroupId(db, v.issue_id),
|
|
64
|
+
verdict,
|
|
65
|
+
ruleMatched: v.rule_matched ?? null,
|
|
66
|
+
rationale: v.rationale ?? null,
|
|
67
|
+
raw: JSON.stringify(v),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
db.finishPatrolRun(run.run_id, "completed", {
|
|
71
|
+
scanned: parsed.length,
|
|
72
|
+
ready: readyCount,
|
|
73
|
+
note: issue.status === "completed" ? undefined : `issue terminal as ${issue.status}`,
|
|
74
|
+
});
|
|
75
|
+
log.info(`patrol run ${run.run_id}: completed (scanned=${parsed.length}, ready=${readyCount})`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* ่งฃๆ link-patrol issue ็ result(JSON ๆฐ็ป),้ๆก:
|
|
79
|
+
* - UPDATE links SET category/tags/title
|
|
80
|
+
* - INSERT link_patrol_logs
|
|
81
|
+
* ็ถๅๅๅนถๆฌ่ฝฎๅญฆๅฐ็่งๅๅๅ
ฅ agent_memory(scope=global, tags=link_classification)ใ
|
|
82
|
+
* ่งฃๆๅคฑ่ดฅ โ run status='error'ใ
|
|
83
|
+
*/
|
|
84
|
+
export function handleLinkPatrolIssueTerminal(db, issue) {
|
|
85
|
+
const run = db.getLinkPatrolRunByIssueId(issue.id);
|
|
86
|
+
if (!run) {
|
|
87
|
+
log.warn(`link-patrol issue ${issue.id} terminal, but no run found (orphan?)`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const raw = (issue.result ?? "").trim();
|
|
91
|
+
if (!raw) {
|
|
92
|
+
db.finishLinkPatrolRun(run.run_id, "error", { note: "empty result" });
|
|
93
|
+
log.warn(`link-patrol run ${run.run_id}: empty result`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const parsed = parseLinkVerdicts(raw);
|
|
97
|
+
if (!parsed) {
|
|
98
|
+
db.finishLinkPatrolRun(run.run_id, "error", { note: `bad result JSON: ${raw.slice(0, 200)}` });
|
|
99
|
+
log.error(`link-patrol run ${run.run_id}: bad result JSON`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let classifiedCount = 0;
|
|
103
|
+
/** ๆ่ๅ:ๅ host ็ๅค link ๅ
ฑ็จไธๆก่งๅ */
|
|
104
|
+
const rulesByHost = new Map();
|
|
105
|
+
for (const v of parsed) {
|
|
106
|
+
const linkId = v.link_id;
|
|
107
|
+
const category = (v.category ?? "other").toString();
|
|
108
|
+
const tags = Array.isArray(v.tags)
|
|
109
|
+
? v.tags.filter((t) => typeof t === "string" && t.trim()).map((t) => t.trim())
|
|
110
|
+
: [];
|
|
111
|
+
const title = typeof v.title === "string" ? v.title : null;
|
|
112
|
+
const rationale = typeof v.rationale === "string" ? v.rationale : null;
|
|
113
|
+
if (!linkId)
|
|
114
|
+
continue;
|
|
115
|
+
const link = db.getLink(linkId);
|
|
116
|
+
if (!link) {
|
|
117
|
+
log.warn(`link-patrol: link ${linkId} not found, skip`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
db.updateLinkClassification(linkId, { category, tags, title });
|
|
121
|
+
db.insertLinkPatrolLog({
|
|
122
|
+
id: randomUUID(),
|
|
123
|
+
runId: run.run_id,
|
|
124
|
+
linkId,
|
|
125
|
+
category,
|
|
126
|
+
tags,
|
|
127
|
+
title,
|
|
128
|
+
rationale,
|
|
129
|
+
raw: JSON.stringify(v),
|
|
130
|
+
});
|
|
131
|
+
classifiedCount++;
|
|
132
|
+
// ็ดฏ็งฏ host ่งๅ
|
|
133
|
+
const entry = rulesByHost.get(link.host);
|
|
134
|
+
if (entry) {
|
|
135
|
+
entry.tagsSet = new Set([...entry.tagsSet, ...tags]);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
rulesByHost.set(link.host, { category, tagsSet: new Set(tags), sampleLink: link.url_raw });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ๅๅนถๅๅ
ฅ agent_memory(ๆฏ host ไธๆก,upsert:ๅ key ๅทฒๅญๅจๅฐฑ update value/tags)
|
|
142
|
+
for (const [host, info] of rulesByHost.entries()) {
|
|
143
|
+
upsertLinkRuleMemory(db, issue.group_id, host, info.category, [...info.tagsSet], info.sampleLink, "system:link-patrol");
|
|
144
|
+
}
|
|
145
|
+
db.finishLinkPatrolRun(run.run_id, "completed", {
|
|
146
|
+
classified: classifiedCount,
|
|
147
|
+
note: issue.status === "completed" ? undefined : `issue terminal as ${issue.status}`,
|
|
148
|
+
});
|
|
149
|
+
log.info(`link-patrol run ${run.run_id}: completed (classified=${classifiedCount}, rules_learned=${rulesByHost.size})`);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* ๆไธๆก host ็บง่งๅๅๅ
ฅ agent_memory,scope=group / group_id=patrolGroupId / category=convention
|
|
153
|
+
* / tags=["link_classification"]ใๆณจๆ:link-patrol ่ชๅจๅญฆๅฐ็่งๅๆพ patrol-link ็พค,ไธๆพ global
|
|
154
|
+
* โโ global namespace ๅฟ
้กปไบบๅทฅ review / promote ๆ่ฝ่ฟใไธไธ่ฝฎ link-patrol handler ๆผ prompt ๆถ
|
|
155
|
+
* ไปๅฏ่ขซ listMemory({ groupId, tags:["link_classification"] }) ๆๅบใ
|
|
156
|
+
* key = `link_rule:${host}`ใๅ key ๅทฒๅญๅจ โ ๅๅนถ tags,ๅทๆฐ value;ไธๅญๅจ โ INSERTใ
|
|
157
|
+
*/
|
|
158
|
+
function upsertLinkRuleMemory(db, groupId, host, category, tags, sampleUrl, createdBy) {
|
|
159
|
+
const key = `link_rule:${host}`;
|
|
160
|
+
const existing = db.db.prepare(`SELECT id, tags FROM agent_memory WHERE key = ? AND group_id = ? AND active = 1 LIMIT 1`).get(key, groupId);
|
|
161
|
+
const value = `host=${host} ้ป่ฎคๅ็ฑป ${category}; tags=[${tags.join(", ")}]; ๆ ทไพ=${sampleUrl}`;
|
|
162
|
+
const summary = `${host} โ ${category}`;
|
|
163
|
+
const mergedTags = Array.from(new Set(["link_classification", ...tags]));
|
|
164
|
+
if (existing) {
|
|
165
|
+
let oldTags = [];
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(existing.tags);
|
|
168
|
+
if (Array.isArray(parsed))
|
|
169
|
+
oldTags = parsed.filter((t) => typeof t === "string");
|
|
170
|
+
}
|
|
171
|
+
catch { /* ignore */ }
|
|
172
|
+
const finalTags = Array.from(new Set([...mergedTags, ...oldTags]));
|
|
173
|
+
db.updateMemory(existing.id, { value, summary, tags: finalTags, category: "convention" });
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const memId = `mem_link_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
177
|
+
db.addMemory({
|
|
178
|
+
id: memId,
|
|
179
|
+
scope: "group",
|
|
180
|
+
groupId,
|
|
181
|
+
category: "convention",
|
|
182
|
+
sourceType: "manual",
|
|
183
|
+
key,
|
|
184
|
+
value,
|
|
185
|
+
summary,
|
|
186
|
+
tags: mergedTags,
|
|
187
|
+
visibility: "group",
|
|
188
|
+
agentVisible: true,
|
|
189
|
+
createdBy,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function parseVerdicts(raw) {
|
|
194
|
+
// 1. ็ดๆฅ JSON.parse
|
|
195
|
+
try {
|
|
196
|
+
const v = JSON.parse(raw);
|
|
197
|
+
if (Array.isArray(v))
|
|
198
|
+
return v;
|
|
199
|
+
}
|
|
200
|
+
catch { /* fall through */ }
|
|
201
|
+
// 2. ไป markdown ```json ... ``` ้ๆ
|
|
202
|
+
const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
203
|
+
if (fenceMatch) {
|
|
204
|
+
try {
|
|
205
|
+
const v = JSON.parse(fenceMatch[1]);
|
|
206
|
+
if (Array.isArray(v))
|
|
207
|
+
return v;
|
|
208
|
+
}
|
|
209
|
+
catch { /* fall through */ }
|
|
210
|
+
}
|
|
211
|
+
// 3. ๆ ๆๅคๅฑ [ ... ]
|
|
212
|
+
const bracketMatch = raw.match(/\[[\s\S]*\]/);
|
|
213
|
+
if (bracketMatch) {
|
|
214
|
+
try {
|
|
215
|
+
const v = JSON.parse(bracketMatch[0]);
|
|
216
|
+
if (Array.isArray(v))
|
|
217
|
+
return v;
|
|
218
|
+
}
|
|
219
|
+
catch { /* fall through */ }
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
/** ่งฃๆ link-patrol result JSONใๅค็จ parseVerdicts ็ไธ็ง fallback,ไฝ็ฑปๅไธๅใ */
|
|
224
|
+
function parseLinkVerdicts(raw) {
|
|
225
|
+
try {
|
|
226
|
+
const v = JSON.parse(raw);
|
|
227
|
+
if (Array.isArray(v))
|
|
228
|
+
return v;
|
|
229
|
+
}
|
|
230
|
+
catch { /* fall through */ }
|
|
231
|
+
const fenceMatch = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
232
|
+
if (fenceMatch) {
|
|
233
|
+
try {
|
|
234
|
+
const v = JSON.parse(fenceMatch[1]);
|
|
235
|
+
if (Array.isArray(v))
|
|
236
|
+
return v;
|
|
237
|
+
}
|
|
238
|
+
catch { /* fall through */ }
|
|
239
|
+
}
|
|
240
|
+
const bracketMatch = raw.match(/\[[\s\S]*\]/);
|
|
241
|
+
if (bracketMatch) {
|
|
242
|
+
try {
|
|
243
|
+
const v = JSON.parse(bracketMatch[0]);
|
|
244
|
+
if (Array.isArray(v))
|
|
245
|
+
return v;
|
|
246
|
+
}
|
|
247
|
+
catch { /* fall through */ }
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
function normalizeVerdict(v) {
|
|
252
|
+
if (!v)
|
|
253
|
+
return "uncertain";
|
|
254
|
+
const s = String(v).toLowerCase().replace(/[-\s]/g, "_");
|
|
255
|
+
if (s === "ready")
|
|
256
|
+
return "ready";
|
|
257
|
+
if (s === "not_ready")
|
|
258
|
+
return "not_ready";
|
|
259
|
+
if (s === "uncertain")
|
|
260
|
+
return "uncertain";
|
|
261
|
+
if (s === "skipped")
|
|
262
|
+
return "skipped";
|
|
263
|
+
// ๅ
ๅบ:ๅ
ณ้ฎๅญ็ๆต
|
|
264
|
+
if (s.includes("ready") && !s.includes("not"))
|
|
265
|
+
return "ready";
|
|
266
|
+
if (s.includes("not_ready") || s.includes("not ready"))
|
|
267
|
+
return "not_ready";
|
|
268
|
+
return "uncertain";
|
|
269
|
+
}
|
|
270
|
+
function resolveCandidateGroupId(db, issueId) {
|
|
271
|
+
if (!issueId)
|
|
272
|
+
return null;
|
|
273
|
+
const issue = db.getIssueById(issueId);
|
|
274
|
+
return issue?.group_id ?? null;
|
|
275
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Master ไพง็ worktree ๆซๆ(ๅช่ฏป)ใ
|
|
3
|
+
*
|
|
4
|
+
* worktree ็ฉ็ไธๅจ executor ๆฌๆบ(`~/.rotom/repos/`),ไฝ master ไธ executor
|
|
5
|
+
* ๅๆบ้จ็ฝฒๆถๅฏ็ดๆฅ่ฏป FS ๆด้ฒ็ป Dashboardใ่ทจๆบๅจๆถๆฌๆจกๅ่ฟๅ็ฉบๅ่กจ(ๆฌๆบๆ ็ผๅญ)ใ
|
|
6
|
+
*
|
|
7
|
+
* ไธไพ่ต executor/repo-cache.ts(้ฃๆฏ executor ไพง spawn git ็ๆจกๅ),่ฟ้็ฌ็ซ
|
|
8
|
+
* ๅฎ็ฐ scan ้ป่พ,ไฟๆ master/executor ๆจกๅ่พน็ๆธ
ๆฐใ็ฎๆณไธ repo-cache ไธ่ด:
|
|
9
|
+
* - repoId = SHA-1(ๅฝไธๅ URL) ๅ 12 ไฝ
|
|
10
|
+
* - repoName = URL ๆๅไธๆฎต(ๅป .git)
|
|
11
|
+
* - bare ็ฎๅฝๅ = <repoName>-<repoId8>.git
|
|
12
|
+
* - worktree ๆ น = <repoName>-<repoId8>-wt/<slot>/
|
|
13
|
+
*
|
|
14
|
+
* slot ๆจ็ฎ:group ๆจกๅผ = `group-<groupId8>`,issue ๆจกๅผ = `<issueId8>`ใ
|
|
15
|
+
* Dashboard ็จ่ฟไธชๆจ็ฎ"ๅฝๅ group ็ worktree ๅบ่ฏฅๅจๅช"ใ
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
const REPOS_ROOT = path.join(os.homedir(), ".rotom", "repos");
|
|
23
|
+
export function repoIdFor(url) {
|
|
24
|
+
let u = url.trim();
|
|
25
|
+
if (u.endsWith(".git"))
|
|
26
|
+
u = u.slice(0, -4);
|
|
27
|
+
u = u.replace(/^ssh:\/\/([^@]+@)?/, "").replace(/^https?:\/\//, "");
|
|
28
|
+
u = u.replace(/^git@/, "").replace(/\/$/, "");
|
|
29
|
+
return createHash("sha1").update(u).digest("hex").slice(0, 12);
|
|
30
|
+
}
|
|
31
|
+
export function repoNameFor(url) {
|
|
32
|
+
let u = url.trim();
|
|
33
|
+
if (u.endsWith(".git"))
|
|
34
|
+
u = u.slice(0, -4);
|
|
35
|
+
u = u.split("?")[0].split("#")[0].replace(/\/$/, "");
|
|
36
|
+
const last = u.split("/").pop() || "repo";
|
|
37
|
+
return last || "repo";
|
|
38
|
+
}
|
|
39
|
+
/** bare clone ่ทฏๅพ(ไธๆฅ FS,ๅช็ฎ)ใ */
|
|
40
|
+
export function barePathForUrl(url) {
|
|
41
|
+
const repoId = repoIdFor(url);
|
|
42
|
+
const repoName = repoNameFor(url);
|
|
43
|
+
return path.join(REPOS_ROOT, `${repoName}-${repoId.slice(0, 8)}.git`);
|
|
44
|
+
}
|
|
45
|
+
/** worktree ่ทฏๅพ(ไธๆฅ FS,ๅช็ฎ)ใslot ็ฑ่ฐ็จๆนๅณๅฎ(group-<groupId8> ๆ <issueId8>)ใ */
|
|
46
|
+
export function worktreePathForUrl(url, slot) {
|
|
47
|
+
const repoId = repoIdFor(url);
|
|
48
|
+
const repoName = repoNameFor(url);
|
|
49
|
+
return path.join(REPOS_ROOT, `${repoName}-${repoId.slice(0, 8)}-wt`, slot);
|
|
50
|
+
}
|
|
51
|
+
/** ๆจ็ฎ group ๆจกๅผไธๆ group ็ worktree ่ทฏๅพใ */
|
|
52
|
+
export function groupWorktreePath(url, groupId) {
|
|
53
|
+
return worktreePathForUrl(url, `group-${groupId.slice(0, 8)}`);
|
|
54
|
+
}
|
|
55
|
+
function runGitSync(args, opts) {
|
|
56
|
+
const r = spawnSync("git", args, {
|
|
57
|
+
cwd: opts?.cwd,
|
|
58
|
+
encoding: "utf-8",
|
|
59
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
ok: r.status === 0,
|
|
63
|
+
stdout: typeof r.stdout === "string" ? r.stdout : "",
|
|
64
|
+
stderr: typeof r.stderr === "string" ? r.stderr : "",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function dirSize(p) {
|
|
68
|
+
let total = 0;
|
|
69
|
+
try {
|
|
70
|
+
const walk = (dir) => {
|
|
71
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
72
|
+
const full = path.join(dir, e.name);
|
|
73
|
+
if (e.isDirectory())
|
|
74
|
+
walk(full);
|
|
75
|
+
else if (e.isFile()) {
|
|
76
|
+
try {
|
|
77
|
+
total += fs.statSync(full).size;
|
|
78
|
+
}
|
|
79
|
+
catch { /* skip */ }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
walk(p);
|
|
84
|
+
}
|
|
85
|
+
catch { /* skip */ }
|
|
86
|
+
return total;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* ๆซๆๆฌๆบ `~/.rotom/repos/`,ๅๅบๆๆ bare clone ๅๅ
ถ worktreeใ
|
|
90
|
+
* ๆฌๆบๆ repos ็ฎๅฝ(่ทจๆบๅจ้จ็ฝฒ master ไธๅจ executor ๆบๅจ)ๆถ่ฟๅ็ฉบๆฐ็ปใ
|
|
91
|
+
*/
|
|
92
|
+
export function scanAllRepos() {
|
|
93
|
+
if (!fs.existsSync(REPOS_ROOT))
|
|
94
|
+
return [];
|
|
95
|
+
const entries = fs.readdirSync(REPOS_ROOT, { withFileTypes: true });
|
|
96
|
+
const result = [];
|
|
97
|
+
for (const e of entries) {
|
|
98
|
+
if (!e.isDirectory() || !e.name.endsWith(".git"))
|
|
99
|
+
continue;
|
|
100
|
+
const repoKey = e.name.slice(0, -4); // ๅป .git
|
|
101
|
+
const bp = path.join(REPOS_ROOT, e.name);
|
|
102
|
+
// repoName = ๅปๆๆๅไธๆฎต -<repoId8>
|
|
103
|
+
const lastDash = repoKey.lastIndexOf("-");
|
|
104
|
+
const repoName = lastDash > 0 ? repoKey.slice(0, lastDash) : repoKey;
|
|
105
|
+
const wtList = runGitSync(["worktree", "list", "--porcelain"], { cwd: bp });
|
|
106
|
+
const worktrees = [];
|
|
107
|
+
if (wtList.ok) {
|
|
108
|
+
let cur = {};
|
|
109
|
+
for (const line of wtList.stdout.split("\n")) {
|
|
110
|
+
if (line.startsWith("worktree ")) {
|
|
111
|
+
if (cur.path)
|
|
112
|
+
worktrees.push(cur);
|
|
113
|
+
cur = { path: line.slice("worktree ".length), branch: "", head: "" };
|
|
114
|
+
}
|
|
115
|
+
else if (line.startsWith("HEAD ")) {
|
|
116
|
+
cur.head = line.slice("HEAD ".length).slice(0, 8);
|
|
117
|
+
}
|
|
118
|
+
else if (line.startsWith("branch ")) {
|
|
119
|
+
const ref = line.slice("branch ".length);
|
|
120
|
+
cur.branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
|
|
121
|
+
}
|
|
122
|
+
else if (line === "") {
|
|
123
|
+
if (cur.path)
|
|
124
|
+
worktrees.push(cur);
|
|
125
|
+
cur = {};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (cur.path)
|
|
129
|
+
worktrees.push(cur);
|
|
130
|
+
}
|
|
131
|
+
// ็ฌฌไธๆกๆฏ bare clone ่ชๅทฑ,่ฟๆปคๆ
|
|
132
|
+
const realWorktrees = worktrees.filter(w => !w.path.endsWith(".git"));
|
|
133
|
+
result.push({
|
|
134
|
+
repoKey,
|
|
135
|
+
repoName,
|
|
136
|
+
barePath: bp,
|
|
137
|
+
worktrees: realWorktrees,
|
|
138
|
+
sizeBytes: dirSize(bp),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* ๆจ็ฎๆ group ็ worktree ไฟกๆฏ(็จไบ Dashboard ๆพ็คบ"ๅฝๅ worktree")ใ
|
|
145
|
+
*
|
|
146
|
+
* ่ฟๅ:
|
|
147
|
+
* - ่ฅ group ๆฒก้
repo_url โ null
|
|
148
|
+
* - ่ฅ้
ไบ โ { url, branch, mode, primaryPath, extraRepos[], exists }
|
|
149
|
+
* primaryPath ๆฏๆจ็ฎ่ทฏๅพ(group ๆจกๅผ slot = group-<groupId8>);
|
|
150
|
+
* exists ่กจ็คบ่ฏฅ่ทฏๅพๅจๆฌๆบ FS ๆฏๅฆๅทฒๅญๅจ(executor ๆฏๅฆๅทฒๅๅปบ)ใ
|
|
151
|
+
*/
|
|
152
|
+
export function resolveGroupWorktreeInfo(db, groupId) {
|
|
153
|
+
const group = db.getGroupById(groupId);
|
|
154
|
+
if (!group)
|
|
155
|
+
return null;
|
|
156
|
+
const url = group.repo_url?.trim();
|
|
157
|
+
if (!url)
|
|
158
|
+
return null;
|
|
159
|
+
const branch = group.repo_default_branch?.trim() || null;
|
|
160
|
+
const mode = group.worktree_mode === "issue" ? "issue" : "group";
|
|
161
|
+
const slot = mode === "group" ? `group-${groupId.slice(0, 8)}` : "issue-mode";
|
|
162
|
+
const primaryPath = worktreePathForUrl(url, slot);
|
|
163
|
+
const primaryExists = fs.existsSync(primaryPath);
|
|
164
|
+
let extras = [];
|
|
165
|
+
if (group.extra_repos) {
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(group.extra_repos);
|
|
168
|
+
if (Array.isArray(parsed)) {
|
|
169
|
+
extras = parsed
|
|
170
|
+
.filter((e) => !!e && typeof e === "object" && typeof e.id === "string" && typeof e.url === "string" && typeof e.mountPath === "string")
|
|
171
|
+
.map(e => {
|
|
172
|
+
const extraSlot = mode === "group" ? `group-${groupId.slice(0, 8)}` : "issue-mode";
|
|
173
|
+
const p = worktreePathForUrl(e.url, extraSlot);
|
|
174
|
+
return {
|
|
175
|
+
id: e.id,
|
|
176
|
+
url: e.url,
|
|
177
|
+
branch: typeof e.branch === "string" && e.branch ? e.branch : null,
|
|
178
|
+
mountPath: e.mountPath,
|
|
179
|
+
path: p,
|
|
180
|
+
exists: fs.existsSync(p),
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch { /* malformed */ }
|
|
186
|
+
}
|
|
187
|
+
return { url, branch, mode, primaryPath, primaryExists, extras };
|
|
188
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Digital Employee Mesh โ Router
|
|
3
|
+
*
|
|
4
|
+
* One routing mode: exact.
|
|
5
|
+
* Router only makes decisions โ it does NOT send messages.
|
|
6
|
+
* WSHub reads the result and handles actual delivery.
|
|
7
|
+
*/
|
|
8
|
+
import { MessageDedup } from "../shared/dedup.js";
|
|
9
|
+
import { DEDUP_TTL_MS, CLEANUP_INTERVAL_MS, PENDING_REQUEST_TTL_MS, } from "../shared/constants.js";
|
|
10
|
+
import { parseAgentRef } from "../shared/protocol/federation.js";
|
|
11
|
+
/** Sentinel fromAgentId for federated pending requests(ๆฌๅฐ ws-hub ๆฐธ่ฟไธไผๆฟ่ฟไธช id ๅป่ทฏ็ฑ) */
|
|
12
|
+
export const FEDERATED_REPLY_MARKER = "__federation__";
|
|
13
|
+
export class Router {
|
|
14
|
+
db;
|
|
15
|
+
logger;
|
|
16
|
+
dedup = new MessageDedup(DEDUP_TTL_MS);
|
|
17
|
+
pendingRequests = new Map();
|
|
18
|
+
cleanupTimer;
|
|
19
|
+
/** Federation ๅฎขๆท็ซฏ(member ๆจกๅผ);standalone ๆถไธบ undefined */
|
|
20
|
+
fedClient;
|
|
21
|
+
/** ๅทฒๅ ๅ
ฅ็ teamId(็จไบๆฅ agent_visibility ็ผๅญ) */
|
|
22
|
+
teamId;
|
|
23
|
+
/** ๆฌๆบ hostname(็จไบ from.hostname ๅญๆฎต) */
|
|
24
|
+
localHostname;
|
|
25
|
+
/** Federation ๅๅค hook:ๆฌๅฐ agent ๅๅค federated ่ฏทๆฑๆถ่ฐ็จ,็ฑ FederationManager ๆณจๅ
ฅ */
|
|
26
|
+
fedReplyHook;
|
|
27
|
+
constructor(db, logger) {
|
|
28
|
+
this.db = db;
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
// Periodic cleanup of dedup and pending requests
|
|
31
|
+
this.cleanupTimer = setInterval(() => {
|
|
32
|
+
this.dedup.cleanup();
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
for (const [id, entry] of this.pendingRequests) {
|
|
35
|
+
if (now - entry.createdAt > PENDING_REQUEST_TTL_MS) {
|
|
36
|
+
this.pendingRequests.delete(id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, CLEANUP_INTERVAL_MS);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* ๆณจๅ
ฅ federation ๅฎขๆท็ซฏ(member ๆจกๅผๆถ็ฑ server.ts ่ฐ็จ)ใ
|
|
43
|
+
* ๆณจๅ
ฅๅ Router ๆไผ่ตฐ federated ่ทฏ็ฑๅๆฏใ
|
|
44
|
+
*/
|
|
45
|
+
setFederation(client, teamId, localHostname) {
|
|
46
|
+
this.fedClient = client;
|
|
47
|
+
this.teamId = teamId;
|
|
48
|
+
this.localHostname = localHostname;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Federation deliver ๅ
ฅๅฃ:ๅ่ฐ master / member ๆถๅฐ FedDeliver ๅ่ฐ่ฟไธช,
|
|
52
|
+
* ่ฎฉๆฌๆบ WSHub ๆ้ๅฐ็ฎๆ ๆฌๅฐ agentใ
|
|
53
|
+
* ่ฟๅ true=ๅทฒๆพๅฐ็ฎๆ ๅนถๆ้(็ฑ่ฐ็จๆนๅฎ้
send)ใ
|
|
54
|
+
*/
|
|
55
|
+
handleFedDeliver(msg, sendToLocal) {
|
|
56
|
+
// to.name ๅจๆฌๆบๆฅ
|
|
57
|
+
const target = this.db.getLocalAgentByName(msg.to.name)
|
|
58
|
+
?? this.db.getAgentByName(msg.to.name);
|
|
59
|
+
if (!target)
|
|
60
|
+
return false;
|
|
61
|
+
return sendToLocal(target.name, msg.payload);
|
|
62
|
+
}
|
|
63
|
+
/** Federation reply ๅ
ฅๅฃ:FedReply ๅฐ่พพๅ,resolve pendingRequest ่ทฏ็ฑๅๆฅๆบ */
|
|
64
|
+
handleFedReply(msg, sendReply) {
|
|
65
|
+
const entry = this.pendingRequests.get(msg.requestId);
|
|
66
|
+
if (entry) {
|
|
67
|
+
sendReply(entry.fromAgentId, msg.payload);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
71
|
+
// Public API
|
|
72
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
73
|
+
/**
|
|
74
|
+
* Route a message. Returns a decision (targetAgentId) โ caller handles delivery.
|
|
75
|
+
*/
|
|
76
|
+
route(fromAgentId, msg) {
|
|
77
|
+
// Dedup
|
|
78
|
+
if (this.dedup.isDuplicate(msg.requestId)) {
|
|
79
|
+
return { delivered: false, queued: false, error: "Duplicate message" };
|
|
80
|
+
}
|
|
81
|
+
this.dedup.mark(msg.requestId);
|
|
82
|
+
// Resolve sender info
|
|
83
|
+
const fromAgent = this.db.getAgentById(fromAgentId);
|
|
84
|
+
const fromName = fromAgent?.name || "unknown";
|
|
85
|
+
const fromDomain = fromAgent?.domain;
|
|
86
|
+
const summary = (msg.payload?.message || "").slice(0, 100);
|
|
87
|
+
// Record for reply correlation (preserve conversation for reply path)
|
|
88
|
+
this.pendingRequests.set(msg.requestId, { fromAgentId, createdAt: Date.now(), conversation: msg.conversation });
|
|
89
|
+
// Route
|
|
90
|
+
if (msg.target) {
|
|
91
|
+
return this.routeExact(fromAgentId, fromName, fromDomain, msg.target, summary, msg.requestId, msg.payload);
|
|
92
|
+
}
|
|
93
|
+
return { delivered: false, queued: false, error: "No target specified" };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolve the original sender for a reply. Returns agentId or undefined.
|
|
97
|
+
* Does NOT consume the entry โ allows multi-round replies on the same requestId.
|
|
98
|
+
* Entry is cleaned up by TTL instead.
|
|
99
|
+
*/
|
|
100
|
+
resolveReplyTarget(requestId) {
|
|
101
|
+
const entry = this.pendingRequests.get(requestId);
|
|
102
|
+
if (entry) {
|
|
103
|
+
entry.createdAt = Date.now();
|
|
104
|
+
return entry.fromAgentId;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
/** ๆฅ่ฏข requestId ๆฏๅฆๆฅ่ช federation(ๆฌๅฐ agent ๅๅคๆถ่ฅไธบ true,่ตฐ fedReplyHook ่้ sendToAgent) */
|
|
109
|
+
isFederatedRequest(requestId) {
|
|
110
|
+
return this.pendingRequests.get(requestId)?.isFederated === true;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* ๆณจๅไธไธช federated ๆฅๆบ็ pending requestใ
|
|
114
|
+
*
|
|
115
|
+
* ๅ่ฐ master / member ๆถๅฐ FedDeliver ๆๅฐๆฌๅฐ agent ไนๅ่ฐ็จใ
|
|
116
|
+
* fromAgentId ็จไธไธช sentinel,ไฝฟ ws-hub ็ sendToAgent ไธไผ็ๆ้ๅฐไปปไฝๆฌๅฐ agent
|
|
117
|
+
* (ๅฎ้
ไธ ws-hub reply handler ๅจ isFederated ๆถไผๆน่ตฐ fedReplyHook,ไธไผ sendToAgent)ใ
|
|
118
|
+
*/
|
|
119
|
+
registerFederatedPendingRequest(requestId, conversation) {
|
|
120
|
+
this.pendingRequests.set(requestId, {
|
|
121
|
+
fromAgentId: FEDERATED_REPLY_MARKER,
|
|
122
|
+
createdAt: Date.now(),
|
|
123
|
+
conversation,
|
|
124
|
+
isFederated: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/** ๆถ่ดนๅนถๆธ
ๆไธไธช federated pending request(ๆๅ dispatch reply ๅ่ฐ็จ) */
|
|
128
|
+
consumeFederatedPendingRequest(requestId) {
|
|
129
|
+
return this.pendingRequests.delete(requestId);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get the conversation context associated with a pending request.
|
|
133
|
+
*/
|
|
134
|
+
getConversation(requestId) {
|
|
135
|
+
return this.pendingRequests.get(requestId)?.conversation;
|
|
136
|
+
}
|
|
137
|
+
stop() {
|
|
138
|
+
clearInterval(this.cleanupTimer);
|
|
139
|
+
}
|
|
140
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
141
|
+
// Private routing methods
|
|
142
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
143
|
+
routeExact(_fromId, fromName, fromDomain, targetName, summary, requestId, payload) {
|
|
144
|
+
// 1. ๆฌๆบ agent ๆฅ่ฏข(ไผๅ
hostname ๅคๅ้ฎ,fallback ๅ
จๅฑ name)
|
|
145
|
+
const target = this.db.getLocalAgentByName(targetName)
|
|
146
|
+
?? this.db.getAgentByName(targetName);
|
|
147
|
+
if (target) {
|
|
148
|
+
// Check if target agent is disabled
|
|
149
|
+
if (target.enabled === 0) {
|
|
150
|
+
this.db.audit({ fromName, fromDomain, toName: target.name, toDomain: target.domain, routeType: "exact", result: "rejected", messageSummary: summary });
|
|
151
|
+
return { delivered: false, queued: false, error: `Agent "${targetName}" is disabled` };
|
|
152
|
+
}
|
|
153
|
+
if (!this.db.canCrossDomain(fromDomain, target.domain)) {
|
|
154
|
+
this.db.audit({ fromName, fromDomain, toName: target.name, toDomain: target.domain, routeType: "exact", result: "rejected", messageSummary: summary });
|
|
155
|
+
return { delivered: false, queued: false, error: "Cross-domain not allowed" };
|
|
156
|
+
}
|
|
157
|
+
this.db.audit({ fromName, fromDomain, toName: target.name, toDomain: target.domain, routeType: "exact", result: "routed", messageSummary: summary });
|
|
158
|
+
return { targetAgentId: target.id, targetName: target.name, delivered: false, queued: false };
|
|
159
|
+
}
|
|
160
|
+
// 2. ๆฌๆบๆพไธๅฐ โ ่ตฐ federation ่ทฏ็ฑ(ๅฆๆ้
ไบ)
|
|
161
|
+
if (this.fedClient && this.teamId && requestId && payload) {
|
|
162
|
+
const fedResult = this.routeFederated(fromName, targetName, requestId, payload, summary);
|
|
163
|
+
if (fedResult)
|
|
164
|
+
return fedResult;
|
|
165
|
+
}
|
|
166
|
+
this.db.audit({ fromName, fromDomain, toName: targetName, routeType: "exact", result: "failed", messageSummary: summary });
|
|
167
|
+
return { delivered: false, queued: false, error: `Agent "${targetName}" not found` };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* ่ทจ master ่ทฏ็ฑ:่งฃๆ targetName(alice@hostB ๆ่ฃธ alice)โ ๆฅ agent_visibility
|
|
171
|
+
* โ ่ฐ federationClient.route ๅ FedRouteMessageใ
|
|
172
|
+
*/
|
|
173
|
+
routeFederated(fromName, targetName, requestId, payload, summary) {
|
|
174
|
+
if (!this.fedClient || !this.teamId || !this.localHostname)
|
|
175
|
+
return null;
|
|
176
|
+
const ref = parseAgentRef(targetName);
|
|
177
|
+
let targetMasterId;
|
|
178
|
+
let targetHostname;
|
|
179
|
+
if (ref.hostname && ref.hostname !== this.localHostname) {
|
|
180
|
+
// ๅฝขๅฆ "alice@hostB" โ ๆ (hostname, name) ๅๆฅ
|
|
181
|
+
const vis = this.db.findVisibleAgentByHostAndName(this.teamId, ref.hostname, ref.name);
|
|
182
|
+
if (!vis)
|
|
183
|
+
return null;
|
|
184
|
+
targetMasterId = vis.master_id;
|
|
185
|
+
targetHostname = vis.hostname;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// ่ฃธ name โ ๆ name ๅๆฅ(department ๅ
ๅฟ
้กปๅฏไธ)
|
|
189
|
+
const candidates = this.db.findVisibleAgentsByName(this.teamId, ref.name);
|
|
190
|
+
if (candidates.length === 0)
|
|
191
|
+
return null;
|
|
192
|
+
if (candidates.length > 1) {
|
|
193
|
+
this.db.audit({ fromName, toName: targetName, routeType: "federated", result: "failed", messageSummary: `ambiguous: ${candidates.length} matches` });
|
|
194
|
+
return { delivered: false, queued: false, error: `Ambiguous agent "${ref.name}" across masters (use name@hostname)` };
|
|
195
|
+
}
|
|
196
|
+
targetMasterId = candidates[0].master_id;
|
|
197
|
+
targetHostname = candidates[0].hostname;
|
|
198
|
+
}
|
|
199
|
+
// ๅ FedRouteMessage ็ปๅ่ฐ master(็ฑๅ่ฐ master ่ฝฌ FedDeliver)
|
|
200
|
+
const ok = this.fedClient.route(requestId, { hostname: this.localHostname, name: fromName }, { hostname: targetHostname, name: ref.name }, { message: payload.message, files: payload.files });
|
|
201
|
+
if (!ok) {
|
|
202
|
+
return { delivered: false, queued: false, error: "Federation client not connected" };
|
|
203
|
+
}
|
|
204
|
+
this.db.audit({
|
|
205
|
+
fromName,
|
|
206
|
+
toName: `${ref.name}@${targetHostname}`,
|
|
207
|
+
routeType: "federated",
|
|
208
|
+
result: "routed",
|
|
209
|
+
messageSummary: summary,
|
|
210
|
+
});
|
|
211
|
+
// delivered=true ๅ่ฏ WSHub ไธ่ฆๅๅฐ่ฏๆฌๅฐๆ้
|
|
212
|
+
return { delivered: true, queued: false, targetName: `${ref.name}@${targetHostname}` };
|
|
213
|
+
}
|
|
214
|
+
}
|