@konglx/rotom 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/README.md +417 -0
  2. package/bin/mesh-master.sh +439 -0
  3. package/bin/rotom +29 -0
  4. package/bin/rotom-link.sh +136 -0
  5. package/bin/rotom-send-with-status +57 -0
  6. package/bin/rotom-up.sh +428 -0
  7. package/dist/cli/ask.js +62 -0
  8. package/dist/cli/common.js +321 -0
  9. package/dist/cli/config.js +65 -0
  10. package/dist/cli/directory.js +17 -0
  11. package/dist/cli/executor.js +58 -0
  12. package/dist/cli/fed.js +91 -0
  13. package/dist/cli/group.js +273 -0
  14. package/dist/cli/identity.js +62 -0
  15. package/dist/cli/init.js +268 -0
  16. package/dist/cli/issue.js +202 -0
  17. package/dist/cli/join.js +170 -0
  18. package/dist/cli/link.js +47 -0
  19. package/dist/cli/master.js +51 -0
  20. package/dist/cli/memory.js +307 -0
  21. package/dist/cli/note.js +68 -0
  22. package/dist/cli/repo.js +77 -0
  23. package/dist/cli/rotom.js +277 -0
  24. package/dist/cli/routes.js +118 -0
  25. package/dist/cli/run.js +45 -0
  26. package/dist/cli/schedule.js +237 -0
  27. package/dist/cli/skill.js +173 -0
  28. package/dist/cli/team.js +106 -0
  29. package/dist/executor/claude-code-hook.cjs +80 -0
  30. package/dist/executor/cli-executor.js +8 -0
  31. package/dist/executor/executors/claude-code.js +780 -0
  32. package/dist/executor/executors/codex.js +719 -0
  33. package/dist/executor/executors/hermes-cli.js +855 -0
  34. package/dist/executor/executors/openclaw.js +467 -0
  35. package/dist/executor/executors/pi.js +514 -0
  36. package/dist/executor/index.js +269 -0
  37. package/dist/executor/jsonrpc-transport.js +125 -0
  38. package/dist/executor/process-runner.js +101 -0
  39. package/dist/executor/reasoning-status.js +83 -0
  40. package/dist/executor/repo-cache.js +502 -0
  41. package/dist/executor/session-store.js +188 -0
  42. package/dist/executor/worker-chat.js +257 -0
  43. package/dist/executor/worker-connection.js +89 -0
  44. package/dist/executor/worker-issue.js +264 -0
  45. package/dist/executor/worker.js +877 -0
  46. package/dist/link/pending-requests.js +72 -0
  47. package/dist/link/server.js +233 -0
  48. package/dist/link/visibility-store.js +58 -0
  49. package/dist/master/api/agents.js +333 -0
  50. package/dist/master/api/artifacts.js +271 -0
  51. package/dist/master/api/domains.js +64 -0
  52. package/dist/master/api/groups.js +635 -0
  53. package/dist/master/api/guidance-templates.js +147 -0
  54. package/dist/master/api/index.js +89 -0
  55. package/dist/master/api/issues-patrol.js +172 -0
  56. package/dist/master/api/issues.js +663 -0
  57. package/dist/master/api/links-patrol.js +168 -0
  58. package/dist/master/api/links.js +114 -0
  59. package/dist/master/api/memory.js +259 -0
  60. package/dist/master/api/messages.js +157 -0
  61. package/dist/master/api/notes.js +77 -0
  62. package/dist/master/api/schedule-patterns.js +133 -0
  63. package/dist/master/api/schedules.js +272 -0
  64. package/dist/master/api/sessions.js +158 -0
  65. package/dist/master/api/share.js +269 -0
  66. package/dist/master/api/skills.js +190 -0
  67. package/dist/master/api/teams.js +122 -0
  68. package/dist/master/api/uploads.js +245 -0
  69. package/dist/master/auth.js +134 -0
  70. package/dist/master/dashboard/animations/calico-dozing.apng +0 -0
  71. package/dist/master/dashboard/animations/calico-error.apng +0 -0
  72. package/dist/master/dashboard/animations/calico-happy.apng +0 -0
  73. package/dist/master/dashboard/animations/calico-notification.apng +0 -0
  74. package/dist/master/dashboard/animations/calico-sleeping.apng +0 -0
  75. package/dist/master/dashboard/animations/calico-thinking.apng +0 -0
  76. package/dist/master/dashboard/animations/calico-waking.apng +0 -0
  77. package/dist/master/dashboard/assets/ApprovalCard-C38VV6ko.css +1 -0
  78. package/dist/master/dashboard/assets/ApprovalCard-CHPh2dmE.js +17 -0
  79. package/dist/master/dashboard/assets/ArtifactPanel-P_2gAP7v.js +1 -0
  80. package/dist/master/dashboard/assets/ArtifactPanel-aGHySny5.css +1 -0
  81. package/dist/master/dashboard/assets/css.worker-DaIe3gwK.js +84 -0
  82. package/dist/master/dashboard/assets/editor.worker-BCzxt1at.js +12 -0
  83. package/dist/master/dashboard/assets/html.worker-CKrFyw_2.js +461 -0
  84. package/dist/master/dashboard/assets/index-CChrTn81.css +32 -0
  85. package/dist/master/dashboard/assets/index-Dhu4SN1z.js +181 -0
  86. package/dist/master/dashboard/assets/json.worker-B7c_PmGb.js +49 -0
  87. package/dist/master/dashboard/assets/markdown-CeN5IgdF.js +29 -0
  88. package/dist/master/dashboard/assets/monaco-core-DyX1CsEw.css +1 -0
  89. package/dist/master/dashboard/assets/monaco-core-oQiQUisy.js +833 -0
  90. package/dist/master/dashboard/assets/monaco-setup-CiOPQdmo.js +1 -0
  91. package/dist/master/dashboard/assets/react-vendor-C8IxlyCR.js +67 -0
  92. package/dist/master/dashboard/assets/ts.worker-BhkL8olL.js +51334 -0
  93. package/dist/master/dashboard/assets/useMonaco-ILb4vyPh.js +12 -0
  94. package/dist/master/dashboard/assets/vite-preload-CxJPbCTl.js +1 -0
  95. package/dist/master/dashboard/debug-auth.html +197 -0
  96. package/dist/master/dashboard/favicon.ico +0 -0
  97. package/dist/master/dashboard/index.html +20 -0
  98. package/dist/master/dashboard/rotom-avatar.png +0 -0
  99. package/dist/master/db/agent-sessions.js +60 -0
  100. package/dist/master/db/agent-visibility.js +64 -0
  101. package/dist/master/db/agents.js +119 -0
  102. package/dist/master/db/ask-bridges.js +157 -0
  103. package/dist/master/db/build-update.js +59 -0
  104. package/dist/master/db/core.js +82 -0
  105. package/dist/master/db/domains.js +80 -0
  106. package/dist/master/db/groups.js +316 -0
  107. package/dist/master/db/guidance-templates.js +58 -0
  108. package/dist/master/db/index.js +12 -0
  109. package/dist/master/db/internal.js +45 -0
  110. package/dist/master/db/issues-patrol.js +81 -0
  111. package/dist/master/db/issues.js +373 -0
  112. package/dist/master/db/links.js +221 -0
  113. package/dist/master/db/master-node.js +43 -0
  114. package/dist/master/db/memory.js +272 -0
  115. package/dist/master/db/messages.js +210 -0
  116. package/dist/master/db/notes.js +55 -0
  117. package/dist/master/db/schedule-patterns.js +56 -0
  118. package/dist/master/db/schedules.js +135 -0
  119. package/dist/master/db/skills.js +144 -0
  120. package/dist/master/db/team.js +88 -0
  121. package/dist/master/db/types.js +10 -0
  122. package/dist/master/db.js +12 -0
  123. package/dist/master/embedded.js +133 -0
  124. package/dist/master/federation/client.js +283 -0
  125. package/dist/master/federation/identity.js +133 -0
  126. package/dist/master/federation/manager.js +267 -0
  127. package/dist/master/federation/publisher.js +87 -0
  128. package/dist/master/federation/self-publisher.js +69 -0
  129. package/dist/master/federation/server.js +487 -0
  130. package/dist/master/group-paths.js +208 -0
  131. package/dist/master/offline-queue.js +38 -0
  132. package/dist/master/opc-bootstrap.js +245 -0
  133. package/dist/master/patrol-terminal.js +275 -0
  134. package/dist/master/repo-scan.js +188 -0
  135. package/dist/master/router.js +214 -0
  136. package/dist/master/scheduler-handlers.js +510 -0
  137. package/dist/master/scheduler.js +201 -0
  138. package/dist/master/server.js +203 -0
  139. package/dist/master/services/link-collector.js +82 -0
  140. package/dist/master/services/link-patrol-bootstrap.js +50 -0
  141. package/dist/master/services/memory-extract-prompt.js +34 -0
  142. package/dist/master/services/patrol-bootstrap.js +63 -0
  143. package/dist/master/share-tokens.js +56 -0
  144. package/dist/master/terminal-hub.js +300 -0
  145. package/dist/master/uploads.js +108 -0
  146. package/dist/master/util/fs.js +100 -0
  147. package/dist/master/util/paths.js +50 -0
  148. package/dist/master/util/persona.js +10 -0
  149. package/dist/master/ws-hub/connection.js +928 -0
  150. package/dist/master/ws-hub/conversation.js +290 -0
  151. package/dist/master/ws-hub/directory.js +70 -0
  152. package/dist/master/ws-hub/dispatch-enrich.js +34 -0
  153. package/dist/master/ws-hub/hub.js +136 -0
  154. package/dist/master/ws-hub/index.js +9 -0
  155. package/dist/master/ws-hub/internal.js +35 -0
  156. package/dist/master/ws-hub/routing.js +295 -0
  157. package/dist/master/ws-hub/sessions.js +130 -0
  158. package/dist/master/ws-hub.js +11 -0
  159. package/dist/shared/agent-profile.js +44 -0
  160. package/dist/shared/constants.js +55 -0
  161. package/dist/shared/dedup.js +33 -0
  162. package/dist/shared/group-context.js +62 -0
  163. package/dist/shared/json-codec.js +33 -0
  164. package/dist/shared/logger.js +136 -0
  165. package/dist/shared/mention.js +22 -0
  166. package/dist/shared/network.js +40 -0
  167. package/dist/shared/parse.js +18 -0
  168. package/dist/shared/prompt-composer.js +171 -0
  169. package/dist/shared/protocol/client-messages.js +8 -0
  170. package/dist/shared/protocol/enums.js +6 -0
  171. package/dist/shared/protocol/federation.js +62 -0
  172. package/dist/shared/protocol/guards.js +87 -0
  173. package/dist/shared/protocol/server-messages.js +8 -0
  174. package/dist/shared/protocol/types.js +8 -0
  175. package/dist/shared/protocol.js +19 -0
  176. package/dist/shared/readonly-allowlist.js +122 -0
  177. package/dist/shared/rotom-cli-prompt.js +23 -0
  178. package/dist/shared/skill-context.js +19 -0
  179. package/dist/shared/skill-md.js +43 -0
  180. package/dist/shared/slash-commands.js +50 -0
  181. package/dist/shared/time.js +80 -0
  182. package/dist/shared/title.js +46 -0
  183. package/dist/shared/url-extractor.js +99 -0
  184. package/migrations/001-schema.sql +942 -0
  185. package/package.json +68 -0
  186. package/scripts/fix-node-pty-perms.mjs +46 -0
  187. package/skill/rotom-a2a-communicate/SKILL.md +257 -0
  188. package/skill/rotom-bus-host/SKILL.md +78 -0
  189. package/skill/rotom-bus-host/scripts/poll-replies.sh +148 -0
@@ -0,0 +1,510 @@
1
+ import { nowBeijing, toBeijing } from "../shared/time.js";
2
+ import { extractMentions } from "../shared/mention.js";
3
+ /**
4
+ * scheduler-handlers —— 定时器到点跑硬编码逻辑的 handler 注册表。
5
+ *
6
+ * scheduled_tasks.handler_key 非空时,scheduler.runOne 不走 prompt/agent 路径,
7
+ * 而是查这个 registry 找对应 handler 跑。handler 接收 payload(JSON 解析后) +
8
+ * ctx(db / hub),自行决定做什么(查 DB、发消息、建 Issue、cancel 别的 task...)。
9
+ *
10
+ * 这是"定时器模板"机制:不同业务场景注册不同 handler,共用同一套调度基础设施
11
+ * (scheduler tick、at-most-once、grace window、Dashboard 可见)。
12
+ *
13
+ * 当前注册的 handler:
14
+ * - ask-bridge-check:ask-bridge 超时检查,见 docs/ASK_BRIDGE_GUIDE.md
15
+ *
16
+ * 未来可加:
17
+ * - issue-stale-check:Issue 长时间无进展自动告警
18
+ * - agent-offline-alert:agent 离线超阈值通知真人
19
+ */
20
+ import { randomUUID } from "node:crypto";
21
+ import { resolveGroupAgentWorkingDir } from "./group-paths.js";
22
+ import { createLogger } from "../shared/logger.js";
23
+ import { TIMER_PERSONA_NAME } from "./util/persona.js";
24
+ const log = createLogger("mesh-scheduler-handlers");
25
+ const registry = {};
26
+ export function registerSchedulerHandler(key, handler) {
27
+ registry[key] = handler;
28
+ log.info(`handler registered: ${key}`);
29
+ }
30
+ export function getSchedulerHandler(key) {
31
+ return registry[key];
32
+ }
33
+ // ── ask-bridge-check handler ───────────────────────────────────────────────
34
+ /**
35
+ * ask-bridge 定时检查。每 20s 跑一次(interval 任务),payload: { bridgeId }。
36
+ *
37
+ * 每次 tick 检查三条路径:
38
+ * 1. bridge 已 resolved(answered/cancelled/timed_out)→ skip,handler 应已 disable task
39
+ * 2. B 有回复 → mark answered + disable task。复述按 reply.mentions 分流:
40
+ * - reply 已 @ asker → A 已被 raw @ 唤醒,跳过 system 复述(避免重复提醒)
41
+ * - reply 未 @ asker → postSystemToGroup 复述给 A(20s poll 兜底,raw @ 没触发)
42
+ * 3. expires_at < now(5min 到)→ 创建升级 Issue + mark timed_out + disable task
43
+ * 4. 都没命中 → return ok,等下个 20s tick
44
+ *
45
+ * ws-hub 事件式 @ 检测仍在(即时 cancel + disable task),handler 的 @ 检测是兜底。
46
+ * 非@回复只能靠 handler 这个 20s poll 检测(20s 内必达)。
47
+ */
48
+ registerSchedulerHandler("ask-bridge-check", async (payload, ctx) => {
49
+ const p = payload;
50
+ if (!p.bridgeId)
51
+ return { status: "error", error: "missing bridgeId in payload" };
52
+ const bridge = ctx.db.getAskBridge(p.bridgeId);
53
+ if (!bridge)
54
+ return { status: "error", error: `bridge ${p.bridgeId} not found` };
55
+ if (bridge.status !== "pending") {
56
+ // 已 resolved —— task 应该已被 disable。若还在跑,disable 一下兜底。
57
+ cancelBridgeTask(ctx, bridge.id);
58
+ return { status: "skipped", error: `bridge already ${bridge.status}` };
59
+ }
60
+ const now = Date.now();
61
+ // 1. 查 B 是否有回复(@ 或非@都算,取最新一条)
62
+ const reply = ctx.db.findLatestReplyForBridge(bridge);
63
+ if (reply) {
64
+ // 有回复 → mark answered + disable task。是否发 system 复述看 reply 是否已 @ asker:
65
+ // - 已 @ → A 已被 raw @ 唤醒,不复述(避免重复提醒,西花 06-28 反馈)
66
+ // - 未 @ → 走 system 复述唤醒 A(20s poll 兜底路径)
67
+ // mentions 从 content 重抽(不信任 DB 行的 mentions 列:agent a2a_reply 入库写死 [])
68
+ ctx.db.markBridgeAnswered(bridge.id, reply.id);
69
+ cancelBridgeTask(ctx, bridge.id);
70
+ const replyMentions = extractMentions(reply.content);
71
+ if (replyMentions.includes(bridge.asker)) {
72
+ log.info(`bridge #${bridge.id} answered by handler: ${bridge.target} replied (msg ${reply.id}) already @ ${bridge.asker}, skip restatement`);
73
+ return { status: "ok" };
74
+ }
75
+ const questionContent = ctx.db.getGroupMessageContent(bridge.question_msg_id) || "(原问题已删除)";
76
+ const qSnippet = questionContent.slice(0, 200);
77
+ const replySnippet = reply.content.slice(0, 500);
78
+ const msg = `@${bridge.asker} ${TIMER_PERSONA_NAME} 来汇报: ${bridge.target} 在 ${reply.created_at} 回复了你之前的提问:\n` +
79
+ `你问: ${qSnippet}\n` +
80
+ `${bridge.target} 回复: ${replySnippet}\n` +
81
+ `\n` +
82
+ `**下一步**:基于这条回复继续你之前的任务。如果你不记得任务上下文(新 session),**立即跑 \`rotom group history ${bridge.group_id} --limit 10\`**,找到你问之前是谁 @ 你的、他们让你做什么,然后把 ${bridge.target} 的回复告诉那个人。`;
83
+ ctx.hub.postSystemToGroup(bridge.group_id, msg);
84
+ log.info(`bridge #${bridge.id} answered by handler: ${bridge.target} replied (msg ${reply.id}), restated to ${bridge.asker} (no @ in reply)`);
85
+ return { status: "ok" };
86
+ }
87
+ // 2. 超时(5min 到,无回复)→ 创建升级 Issue + disable task
88
+ if (bridge.expires_at < now) {
89
+ const issueId = randomUUID();
90
+ const questionContent = ctx.db.getGroupMessageContent(bridge.question_msg_id) || "(原问题已删除)";
91
+ const qSnippet = questionContent.slice(0, 200);
92
+ const createdIso = toBeijing(bridge.created_at);
93
+ const minutes = Math.max(1, Math.round(bridge.timeout_ms / 60_000));
94
+ const escalateTo = resolveEscalateTo(ctx, bridge);
95
+ const title = `[ask-bridge] ${bridge.target} 未回复,需升级`;
96
+ const description = [
97
+ `${TIMER_PERSONA_NAME} 等了 ${minutes} 分钟没等到 ${bridge.target},触发升级:`,
98
+ `你于 ${createdIso} 在群 "${bridge.group_id}" 问 ${bridge.target}:`,
99
+ ` "${qSnippet}"`,
100
+ ``,
101
+ escalateTo
102
+ ? `请去群里 @ ${escalateTo} 求救,说明:`
103
+ : `群里当前没有在线的真人。请在群里说明情况,等真人上线后跟进。`,
104
+ `- 你问的是什么`,
105
+ `- 等了多久`,
106
+ `- 你尝试过什么(如有)`,
107
+ ``,
108
+ `求救后 rotom issue complete ${issueId} 关闭此 Issue。`,
109
+ ].join("\n");
110
+ ctx.db.createIssue({
111
+ id: issueId,
112
+ groupId: bridge.group_id,
113
+ title,
114
+ description,
115
+ createdBy: "system:ask-bridge",
116
+ workingDir: resolveGroupAgentWorkingDir(ctx.db, bridge.group_id, bridge.asker),
117
+ assignedTo: bridge.asker,
118
+ });
119
+ ctx.db.markBridgeTimedOut(bridge.id, issueId, null);
120
+ cancelBridgeTask(ctx, bridge.id);
121
+ const pushed = ctx.hub.pushIssueAssignment(issueId, bridge.asker);
122
+ if (!pushed) {
123
+ log.warn(`bridge #${bridge.id} pushIssueAssignment to ${bridge.asker} failed (offline?)`);
124
+ }
125
+ log.info(`bridge #${bridge.id} timed_out by handler: issue ${issueId} → ${bridge.asker} (no reply, escalate)`);
126
+ return { status: "ok", issueId };
127
+ }
128
+ // 3. 都没命中 → 等 5min 内的下个 20s tick
129
+ return { status: "ok" };
130
+ });
131
+ const PATROL_PRIORITY_ORDER = {
132
+ critical: 0,
133
+ high: 1,
134
+ medium: 2,
135
+ low: 3,
136
+ };
137
+ function patrolPriorityRank(p) {
138
+ return PATROL_PRIORITY_ORDER[p] ?? 99;
139
+ }
140
+ registerSchedulerHandler("issue-patrol", async (payload, ctx) => {
141
+ const p = (payload ?? {});
142
+ const throughputCap = p.throughputCap ?? 3;
143
+ const candidateCap = p.candidateCap ?? 3;
144
+ const scanBatch = p.scanBatch ?? 10;
145
+ // task.group_id / task.agent_name 本应是 patrolGroupId / patrolAgentName,
146
+ // 但 HandlerContext 只暴露 db/hub,scheduler.runOne 没把 task 传进来。
147
+ // 所以 patrolGroupId/patrolAgentName 在 handler_payload 里显式携带
148
+ // (由 POST /api/groups 建 patrol 群时写入,PATCH /api/issues-patrol/config 可改)。
149
+ const { patrolGroupId, patrolAgentName } = p;
150
+ if (!patrolGroupId || !patrolAgentName) {
151
+ return { status: "error", error: "missing patrolGroupId/patrolAgentName in handler_payload" };
152
+ }
153
+ const runId = randomUUID();
154
+ const startedAt = nowBeijing();
155
+ // 1. 防 overlap:查该 patrol 群最近一次 run,若 patrol_issue 仍 in_progress 则跳过
156
+ const recentRuns = ctx.db.listPatrolRuns({ patrolGroupId, limit: 1 });
157
+ const lastRun = recentRuns[0];
158
+ if (lastRun && lastRun.patrol_issue_id && lastRun.status === "dispatched") {
159
+ const prevIssue = ctx.db.getIssueById(lastRun.patrol_issue_id);
160
+ if (prevIssue && prevIssue.status === "in_progress") {
161
+ ctx.db.createPatrolRun({
162
+ runId,
163
+ patrolGroupId,
164
+ startedAt,
165
+ inProgressCount: 0,
166
+ status: "skipped_overlap",
167
+ });
168
+ ctx.db.finishPatrolRun(runId, "skipped_overlap", { note: `prev patrol issue ${prevIssue.id} still in_progress` });
169
+ log.info(`patrol run ${runId}: skipped_overlap (prev ${prevIssue.id})`);
170
+ return { status: "skipped", error: "prev patrol issue in_progress" };
171
+ }
172
+ }
173
+ // 2. 巡检员在线检查
174
+ const agent = ctx.db.getAgentByName(patrolAgentName);
175
+ if (!agent || agent.status !== "online") {
176
+ ctx.db.createPatrolRun({
177
+ runId,
178
+ patrolGroupId,
179
+ startedAt,
180
+ inProgressCount: 0,
181
+ status: "agent_offline",
182
+ });
183
+ ctx.db.finishPatrolRun(runId, "agent_offline", { note: !agent ? "agent not found" : "agent offline" });
184
+ log.info(`patrol run ${runId}: agent_offline (${patrolAgentName})`);
185
+ return { status: "skipped", error: "patrol agent offline" };
186
+ }
187
+ // 3. 全局 in_progress 计数(排除 patrol 群自己)
188
+ const inProgress = ctx.db.listAllIssues("in_progress").filter((i) => i.group_id !== patrolGroupId);
189
+ const inProgressCount = inProgress.length;
190
+ if (inProgressCount >= throughputCap) {
191
+ ctx.db.createPatrolRun({
192
+ runId,
193
+ patrolGroupId,
194
+ startedAt,
195
+ inProgressCount,
196
+ status: "skipped_quota",
197
+ });
198
+ ctx.db.finishPatrolRun(runId, "skipped_quota", { note: `throughput cap reached (${inProgressCount}/${throughputCap})` });
199
+ log.info(`patrol run ${runId}: skipped_quota (in_progress=${inProgressCount})`);
200
+ return { status: "ok" };
201
+ }
202
+ // 4. 取候选 open issue(排除 patrol 群自己),按 priority 排
203
+ const slots = candidateCap - inProgressCount;
204
+ if (slots <= 0) {
205
+ ctx.db.createPatrolRun({
206
+ runId,
207
+ patrolGroupId,
208
+ startedAt,
209
+ inProgressCount,
210
+ status: "skipped_quota",
211
+ });
212
+ ctx.db.finishPatrolRun(runId, "skipped_quota", { note: `no slots (in_progress=${inProgressCount}, cap=${candidateCap})` });
213
+ log.info(`patrol run ${runId}: skipped_quota (no slots)`);
214
+ return { status: "ok" };
215
+ }
216
+ // 候选数量:既要喂饱巡检员找满 slots 个 ready,又不能扫太多浪费 token。
217
+ // 取 max(slots, scanBatch/2) 上限 scanBatch。
218
+ const take = Math.min(scanBatch, Math.max(slots, Math.ceil(scanBatch / 2)));
219
+ const openIssues = ctx.db.listAllIssues("open")
220
+ .filter((i) => i.group_id !== patrolGroupId && i.type === "task")
221
+ .sort((a, b) => {
222
+ const rankDiff = patrolPriorityRank(a.priority) - patrolPriorityRank(b.priority);
223
+ if (rankDiff !== 0)
224
+ return rankDiff;
225
+ return a.created_at < b.created_at ? -1 : 1;
226
+ })
227
+ .slice(0, take);
228
+ if (openIssues.length === 0) {
229
+ ctx.db.createPatrolRun({
230
+ runId,
231
+ patrolGroupId,
232
+ startedAt,
233
+ inProgressCount,
234
+ status: "completed",
235
+ });
236
+ ctx.db.finishPatrolRun(runId, "completed", { scanned: 0, ready: 0, note: "no open candidates" });
237
+ log.info(`patrol run ${runId}: no open candidates`);
238
+ return { status: "ok" };
239
+ }
240
+ // 5. 取规则 skill 全文
241
+ const skill = ctx.db.getSkillByName("issue-patrol-rules");
242
+ const rulesText = skill?.content ?? "(rules skill not found;fall back to default judgment)";
243
+ // 6. 组装 patrol issue
244
+ const candidateList = openIssues.map((iss, idx) => {
245
+ return [
246
+ `### 候选 ${idx + 1}: ${iss.title}`,
247
+ `- issue_id: ${iss.id}`,
248
+ `- group_id: ${iss.group_id}`,
249
+ `- priority: ${iss.priority}`,
250
+ `- slash_command: ${iss.slash_command ?? "(无)"}`,
251
+ `- working_dir: ${iss.working_dir ?? "(默认)"}`,
252
+ `- created_at: ${iss.created_at}`,
253
+ `- description:`,
254
+ iss.description?.slice(0, 800) ?? "(空)",
255
+ ].join("\n");
256
+ }).join("\n\n");
257
+ const title = `[巡检] ${new Date().toLocaleString("zh-CN", { hour12: false })}`;
258
+ const description = [
259
+ `# Issue 巡检任务`,
260
+ ``,
261
+ `你是巡检员。本轮全局 in_progress=${inProgressCount}(cap ${throughputCap}),剩余 slot=${slots}。`,
262
+ `下面是 ${openIssues.length} 个候选 open issue,请逐个判断"是否可以直接认领开工"。`,
263
+ ``,
264
+ `## 巡检规则`,
265
+ ``,
266
+ rulesText,
267
+ ``,
268
+ `## 候选列表`,
269
+ ``,
270
+ candidateList,
271
+ ``,
272
+ `## 输出要求`,
273
+ ``,
274
+ `**不要**认领、分配、或操作任何候选 issue。只输出判断。`,
275
+ `result 字段必须是 JSON 数组,每条:`,
276
+ "```json",
277
+ `{`,
278
+ ` "issue_id": "<候选 id>",`,
279
+ ` "verdict": "ready" | "not_ready" | "uncertain",`,
280
+ ` "rule_matched": "<命中的规则名>",`,
281
+ ` "rationale": "<一句话理由>"`,
282
+ `}`,
283
+ "```",
284
+ `找满 ${slots} 个 ready 就可以停(不要继续往后扫)。`,
285
+ ].join("\n");
286
+ // 7. 派 issue + 写 run
287
+ const issueId = randomUUID();
288
+ ctx.db.createIssue({
289
+ id: issueId,
290
+ groupId: patrolGroupId,
291
+ title,
292
+ description,
293
+ priority: "medium",
294
+ createdBy: "system:issue-patrol",
295
+ workingDir: resolveGroupAgentWorkingDir(ctx.db, patrolGroupId, patrolAgentName),
296
+ assignedTo: patrolAgentName,
297
+ });
298
+ ctx.db.createPatrolRun({
299
+ runId,
300
+ patrolGroupId,
301
+ patrolIssueId: issueId,
302
+ startedAt,
303
+ inProgressCount,
304
+ status: "dispatched",
305
+ });
306
+ const pushed = ctx.hub.pushIssueAssignment(issueId, patrolAgentName);
307
+ if (!pushed) {
308
+ ctx.db.finishPatrolRun(runId, "error", { note: "pushIssueAssignment failed" });
309
+ log.warn(`patrol run ${runId}: pushIssueAssignment to ${patrolAgentName} failed`);
310
+ return { status: "error", error: "pushIssueAssignment failed", issueId };
311
+ }
312
+ log.info(`patrol run ${runId}: dispatched issue ${issueId} → ${patrolAgentName} (${openIssues.length} candidates, slots=${slots})`);
313
+ return { status: "ok", issueId };
314
+ });
315
+ registerSchedulerHandler("link-patrol", async (payload, ctx) => {
316
+ const p = (payload ?? {});
317
+ const scanBatch = p.scanBatch ?? 20;
318
+ const { patrolGroupId, patrolAgentName } = p;
319
+ if (!patrolGroupId || !patrolAgentName) {
320
+ return { status: "error", error: "missing patrolGroupId/patrolAgentName in handler_payload" };
321
+ }
322
+ const runId = randomUUID();
323
+ const startedAt = nowBeijing();
324
+ // 1. 防 overlap:查该 patrol-link 群最近一次 run,patrol_issue 仍 in_progress 跳过
325
+ const recentRuns = ctx.db.listLinkPatrolRuns({ patrolGroupId, limit: 1 });
326
+ const lastRun = recentRuns[0];
327
+ if (lastRun && lastRun.patrol_issue_id && lastRun.status === "dispatched") {
328
+ const prevIssue = ctx.db.getIssueById(lastRun.patrol_issue_id);
329
+ if (prevIssue && prevIssue.status === "in_progress") {
330
+ ctx.db.createLinkPatrolRun({
331
+ runId,
332
+ patrolGroupId,
333
+ startedAt,
334
+ status: "skipped",
335
+ });
336
+ ctx.db.finishLinkPatrolRun(runId, "skipped", { note: `prev patrol issue ${prevIssue.id} still in_progress` });
337
+ log.info(`link-patrol run ${runId}: skipped_overlap (prev ${prevIssue.id})`);
338
+ return { status: "skipped", error: "prev link-patrol issue in_progress" };
339
+ }
340
+ }
341
+ // 2. 巡检员在线检查
342
+ const agent = ctx.db.getAgentByName(patrolAgentName);
343
+ if (!agent || agent.status !== "online") {
344
+ ctx.db.createLinkPatrolRun({
345
+ runId,
346
+ patrolGroupId,
347
+ startedAt,
348
+ status: "agent_offline",
349
+ });
350
+ ctx.db.finishLinkPatrolRun(runId, "agent_offline", { note: !agent ? "agent not found" : "agent offline" });
351
+ log.info(`link-patrol run ${runId}: agent_offline (${patrolAgentName})`);
352
+ return { status: "skipped", error: "link-patrol agent offline" };
353
+ }
354
+ // 3. 取候选未分类链接
355
+ const candidates = ctx.db.listUnclassifiedLinks(scanBatch);
356
+ if (candidates.length === 0) {
357
+ ctx.db.createLinkPatrolRun({
358
+ runId,
359
+ patrolGroupId,
360
+ startedAt,
361
+ status: "completed",
362
+ });
363
+ ctx.db.finishLinkPatrolRun(runId, "completed", { classified: 0, note: "no unclassified links" });
364
+ log.info(`link-patrol run ${runId}: no unclassified links`);
365
+ return { status: "ok" };
366
+ }
367
+ // 4. 拉 memory few-shot:agent_visible=1 + tags 含 link_classification,scope=global
368
+ const rules = ctx.db.listMemory({
369
+ scope: "global",
370
+ tags: ["link_classification"],
371
+ agentVisible: 1,
372
+ limit: 20,
373
+ });
374
+ // 5. 拉 skill 全文
375
+ const skill = ctx.db.getSkillByName("link-patrol-rules");
376
+ const rulesText = skill?.content ?? "(link-patrol-rules skill not found; fall back to default categories)";
377
+ // 6. 拼 description
378
+ const candidateList = candidates.map((l, idx) => {
379
+ return [
380
+ `### 候选 ${idx + 1}: ${l.host}`,
381
+ `- link_id: ${l.id}`,
382
+ `- url_raw: ${l.url_raw}`,
383
+ `- 首次上下文: ${l.first_context || "(无)"}`,
384
+ `- 最后见到: ${l.last_seen_at}`,
385
+ ].join("\n");
386
+ }).join("\n\n");
387
+ const fewShotBlock = rules.length > 0
388
+ ? [
389
+ "## 历史分类经验(few-shot,memory 自动积累)",
390
+ "复用这些规则;若 host 在下列出现过,优先沿用旧分类:",
391
+ ...rules.map((r) => `- ${r.key}: ${r.summary ?? "(无 summary)"}`),
392
+ ].join("\n")
393
+ : "(暂无历史经验,这是首轮)";
394
+ const title = `[链接分类] ${new Date().toLocaleString("zh-CN", { hour12: false })}`;
395
+ const description = [
396
+ "# 链接分类任务",
397
+ ``,
398
+ `本轮有 ${candidates.length} 条待分类链接。按规则给出 category + tags + title + rationale。`,
399
+ ``,
400
+ "## 分类规则",
401
+ "",
402
+ rulesText,
403
+ "",
404
+ fewShotBlock,
405
+ "",
406
+ "## 候选链接",
407
+ "",
408
+ candidateList,
409
+ "",
410
+ "## 输出要求",
411
+ "",
412
+ "issue `result` 字段必须是 JSON 数组,用 markdown ```json 代码块包裹,每条:",
413
+ "```json",
414
+ "{",
415
+ ' "link_id": "<uuid>",',
416
+ ' "category": "reference | code | tool | article | paper | discussion | issue-tracker | media | other",',
417
+ ' "tags": ["keyword1", "keyword2"],',
418
+ ' "title": "<人类可读标题>",',
419
+ ' "rationale": "<一句话: host + path 模式 → category 推断理由>"',
420
+ "}",
421
+ "```",
422
+ ].join("\n");
423
+ // 7. 派 issue
424
+ const issueId = randomUUID();
425
+ ctx.db.createIssue({
426
+ id: issueId,
427
+ groupId: patrolGroupId,
428
+ title,
429
+ description,
430
+ priority: "medium",
431
+ createdBy: "system:link-patrol",
432
+ workingDir: resolveGroupAgentWorkingDir(ctx.db, patrolGroupId, patrolAgentName),
433
+ assignedTo: patrolAgentName,
434
+ });
435
+ ctx.db.createLinkPatrolRun({
436
+ runId,
437
+ patrolGroupId,
438
+ patrolIssueId: issueId,
439
+ startedAt,
440
+ candidatesScanned: candidates.length,
441
+ status: "dispatched",
442
+ });
443
+ const pushed = ctx.hub.pushIssueAssignment(issueId, patrolAgentName);
444
+ if (!pushed) {
445
+ ctx.db.finishLinkPatrolRun(runId, "error", { note: "pushIssueAssignment failed" });
446
+ log.warn(`link-patrol run ${runId}: pushIssueAssignment to ${patrolAgentName} failed`);
447
+ return { status: "error", error: "pushIssueAssignment failed", issueId };
448
+ }
449
+ log.info(`link-patrol run ${runId}: dispatched issue ${issueId} → ${patrolAgentName} (${candidates.length} candidates)`);
450
+ return { status: "ok", issueId };
451
+ });
452
+ /** cancel 对应 bridgeId 的 ask-bridge scheduled_task(disable,保留在列表里做审计)。
453
+ * 按 handler_key + handler_payload 查,不依赖 name(name 已改成"星期五 · …"友好文案)。 */
454
+ function cancelBridgeTask(ctx, bridgeId) {
455
+ const task = ctx.db.findAskBridgeScheduledTask(bridgeId);
456
+ if (task && task.enabled) {
457
+ ctx.db.disableScheduledTask(task.id);
458
+ log.info(`bridge ${bridgeId}: cancelled schedule task #${task.id}`);
459
+ }
460
+ }
461
+ /**
462
+ * 解析超时升级的求救对象。
463
+ *
464
+ * 优先级:
465
+ * 1. bridge.escalate_to 显式指定 —— 直接用
466
+ * 2. 群里在线真人(群成员 profile override 或全局 agent profile 里 category=真人,
467
+ * 且 status=online,且不是 asker 自己)—— 返回 "名字1 或 名字2"
468
+ * 3. 没有在线真人 —— 返回 null(调用方给另一套文案,不让 asker LLM 自己挑,
469
+ * 否则它会乱猜 agent 当真人,见 issue bcb92df5)
470
+ */
471
+ function resolveEscalateTo(ctx, bridge) {
472
+ if (bridge.escalate_to)
473
+ return bridge.escalate_to;
474
+ const members = ctx.db.getGroupMembers(bridge.group_id);
475
+ const agentByName = new Map();
476
+ for (const a of ctx.db.listAgents()) {
477
+ agentByName.set(a.name, a);
478
+ }
479
+ const humans = [];
480
+ for (const m of members) {
481
+ if (m.agent_name === bridge.asker)
482
+ continue;
483
+ const agent = agentByName.get(m.agent_name);
484
+ if (!agent || agent.status !== "online")
485
+ continue;
486
+ // category 优先取群成员 profile override,回落到全局 agent profile
487
+ let category;
488
+ if (m.profile) {
489
+ try {
490
+ const p = JSON.parse(m.profile);
491
+ if (typeof p.category === "string")
492
+ category = p.category;
493
+ }
494
+ catch { /* fall through */ }
495
+ }
496
+ if (!category && agent.profile) {
497
+ try {
498
+ const p = JSON.parse(agent.profile);
499
+ if (typeof p.category === "string")
500
+ category = p.category;
501
+ }
502
+ catch { /* ignore */ }
503
+ }
504
+ if (category === "真人")
505
+ humans.push(m.agent_name);
506
+ }
507
+ if (humans.length === 0)
508
+ return null;
509
+ return humans.join(" 或 ");
510
+ }