lightclawbot 1.2.6-beta.0 → 1.2.7

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 (98) hide show
  1. package/dist/src/gateway.js +50 -6
  2. package/dist/src/group/constants/index.js +20 -0
  3. package/dist/src/group/inbound/index.js +254 -0
  4. package/dist/src/group/index.js +15 -0
  5. package/dist/src/group/orchestrator/execution/agent-runner.js +299 -0
  6. package/dist/src/group/orchestrator/execution/index.js +7 -0
  7. package/dist/src/group/orchestrator/execution/prompt-builder.js +288 -0
  8. package/dist/src/group/orchestrator/execution/soul-resolver.js +38 -0
  9. package/dist/src/group/orchestrator/execution/types.js +7 -0
  10. package/dist/src/group/orchestrator/index.js +14 -0
  11. package/dist/src/group/orchestrator/lifecycle/conversation-state.js +162 -0
  12. package/dist/src/group/orchestrator/lifecycle/index.js +7 -0
  13. package/dist/src/group/orchestrator/lifecycle/ledger-writer.js +96 -0
  14. package/dist/src/group/orchestrator/lifecycle/run-registry.js +174 -0
  15. package/dist/src/group/orchestrator/orchestrator.js +265 -0
  16. package/dist/src/group/orchestrator/planning/index.js +13 -0
  17. package/dist/src/group/orchestrator/planning/plan-validator.js +233 -0
  18. package/dist/src/group/orchestrator/planning/planning-parser.js +207 -0
  19. package/dist/src/group/orchestrator/planning/subtask-executor.js +345 -0
  20. package/dist/src/group/orchestrator/planning/summarizer-runner.js +224 -0
  21. package/dist/src/group/orchestrator/routes/index.js +9 -0
  22. package/dist/src/group/orchestrator/routes/leader-dispatch.js +229 -0
  23. package/dist/src/group/orchestrator/routes/leader-orchestration-route.js +179 -0
  24. package/dist/src/group/orchestrator/routes/leader-planning.js +92 -0
  25. package/dist/src/group/orchestrator/routes/leader-self-answer.js +223 -0
  26. package/dist/src/group/orchestrator/routes/mention-concurrent-route.js +226 -0
  27. package/dist/src/group/orchestrator/routes/route-helpers.js +186 -0
  28. package/dist/src/group/orchestrator/routes/types.js +8 -0
  29. package/dist/src/group/services/group-cleanup-service.js +183 -0
  30. package/dist/src/group/services/group-creation-service.js +122 -0
  31. package/dist/src/group/services/group-deletion-service.js +111 -0
  32. package/dist/src/group/services/group-history-service.js +73 -0
  33. package/dist/src/group/services/group-member-service.js +169 -0
  34. package/dist/src/group/services/group-query-service.js +133 -0
  35. package/dist/src/group/services/group-update-service.js +144 -0
  36. package/dist/src/group/services/index.js +20 -0
  37. package/dist/src/group/storage/concurrency-manager.js +119 -0
  38. package/dist/src/group/storage/group-storage-core.js +227 -0
  39. package/dist/src/group/storage/index.js +12 -0
  40. package/dist/src/group/storage/message-reader.js +213 -0
  41. package/dist/src/group/storage/message-writer.js +229 -0
  42. package/dist/src/group/storage/slice-manager.js +165 -0
  43. package/dist/src/group/types/common.js +5 -0
  44. package/dist/src/group/types/index.js +5 -0
  45. package/dist/src/group/types/message.js +5 -0
  46. package/dist/src/group/types/orchestrator.js +5 -0
  47. package/dist/src/group/types/storage.js +5 -0
  48. package/dist/src/group/utils/id-generator.js +15 -0
  49. package/dist/src/group/utils/index.js +12 -0
  50. package/dist/src/group/utils/mime.js +36 -0
  51. package/dist/src/group/utils/normalize.js +32 -0
  52. package/dist/src/group/utils/run-helpers.js +36 -0
  53. package/dist/src/outbound.js +12 -19
  54. package/dist/src/shared.js +4 -3
  55. package/dist/src/socket/events/agents-request.js +147 -0
  56. package/dist/src/socket/events/chat-request.js +67 -0
  57. package/dist/src/socket/events/file-download.js +121 -0
  58. package/dist/src/socket/events/group-abort.js +59 -0
  59. package/dist/src/socket/events/group-history.js +59 -0
  60. package/dist/src/socket/events/group-member.js +83 -0
  61. package/dist/src/socket/events/group-request.js +91 -0
  62. package/dist/src/socket/events/history-request.js +95 -0
  63. package/dist/src/socket/events/index.js +39 -0
  64. package/dist/src/socket/events/message-private.js +82 -0
  65. package/dist/src/socket/handlers.js +53 -517
  66. package/dist/src/socket/native-socket.js +21 -20
  67. package/dist/src/socket/registry.js +6 -3
  68. package/dist/src/socket/reliable-emitter.js +16 -13
  69. package/dist/src/socket/service/chat-common.js +36 -0
  70. package/dist/src/socket/service/chat-create.js +75 -0
  71. package/dist/src/socket/service/chat-delete.js +94 -0
  72. package/dist/src/socket/service/chat-list.js +82 -0
  73. package/dist/src/socket/service/chat-update.js +83 -0
  74. package/dist/src/socket/service/group-abort.js +104 -0
  75. package/dist/src/socket/service/group-history.js +140 -0
  76. package/dist/src/socket/service/group-member.js +209 -0
  77. package/dist/src/socket/service/group.js +233 -0
  78. package/dist/src/socket/service/history.js +102 -0
  79. package/dist/src/socket/service/index.js +14 -0
  80. package/dist/src/socket/types/index.js +7 -0
  81. package/dist/src/socket/types/request.js +8 -0
  82. package/dist/src/socket/types/service.js +8 -0
  83. package/dist/src/socket/utils/agent-soul.js +95 -0
  84. package/dist/src/socket/utils/index.js +8 -0
  85. package/dist/src/socket/utils/message.js +83 -0
  86. package/dist/src/socket/utils/validate.js +42 -0
  87. package/dist/src/streaming/index.js +1 -0
  88. package/dist/src/streaming/stream-reply-sink.js +270 -14
  89. package/dist/src/streaming/types.js +20 -1
  90. package/dist/src/{download-tool.js → tools/download-tool.js} +41 -35
  91. package/dist/src/tools/group-history-tool.js +172 -0
  92. package/dist/src/{upload-tool.js → tools/upload-tool.js} +2 -2
  93. package/dist/src/tools.js +4 -3
  94. package/dist/src/utils/index.js +1 -0
  95. package/dist/src/utils/logger.js +38 -0
  96. package/openclaw.plugin.json +2 -1
  97. package/package.json +1 -1
  98. package/dist/src/socket/chat.js +0 -257
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @fileoverview Leader Planning 阶段执行器
3
+ *
4
+ * @description
5
+ * 负责 Planning 阶段:以 JSON 模式调用 Leader Agent 输出决策,
6
+ * 支持重试(JSON 解析失败时自动重试)。
7
+ *
8
+ * @module leader-planning
9
+ */
10
+ import { generatePreRunId } from '../../utils/id-generator.js';
11
+ import { getModuleLogger } from '../../../utils/logger.js';
12
+ import { parsePlanningDecision } from '../planning/index.js';
13
+ import { DENY_TOOLS } from './route-helpers.js';
14
+ const log = getModuleLogger('group.leader-route');
15
+ /**
16
+ * 执行 Planning 阶段:强制 JSON 输出决策,支持重试
17
+ *
18
+ * @param deps - 路由依赖
19
+ * @param ctx - 路由上下文
20
+ * @param agentId - Leader Agent ID
21
+ * @param memberCards - 群成员能力卡
22
+ * @param tag - 日志标签
23
+ * @param maxRetries - 最大重试次数
24
+ */
25
+ export async function executePlanning(deps, ctx, agentId, memberCards, tag, maxRetries) {
26
+ const { runner, promptBuilder, runRegistry } = deps;
27
+ const planningPrompt = promptBuilder.buildPlanner({
28
+ leaderAgentId: agentId,
29
+ memberCards,
30
+ userMessage: ctx.userMessage,
31
+ });
32
+ const planningRunId = generatePreRunId(ctx.conversationId, 'planning');
33
+ const planningAbort = runRegistry.registerRun(ctx.conversationId, planningRunId, agentId);
34
+ // Planning 不推送给前端,不传 sinkContext,AgentRunner 内部会使用静默 sink
35
+ // Planning 阶段不传 attachments/媒体文件:
36
+ // Planning 只需要知道"用户发了文件"这个事实(已在 userMessage 的 attachmentDescription 中体现),
37
+ // 如果传入图片等媒体文件,AI 会被图片内容"分心",直接描述图片而忽略 JSON 格式要求。
38
+ // 真正需要"看到"文件内容的是自答阶段(leader-self-answer)和子任务阶段。
39
+ let planningOutput;
40
+ let lastError;
41
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
42
+ if (planningAbort.aborted)
43
+ break;
44
+ try {
45
+ planningOutput = await runner.run({
46
+ // 使用独立的 sessionKey(加 :planning 后缀),避免 Planning 阶段的 JSON 决策
47
+ // 输出被写入主 session transcript,污染后续自答/派活阶段的 LLM 上下文
48
+ groupId: `${ctx.groupId}:planning`,
49
+ agentId,
50
+ userId: ctx.userId,
51
+ role: 'leader',
52
+ message: ctx.userMessage,
53
+ systemPrompt: planningPrompt,
54
+ abortSignal: planningAbort,
55
+ preassignedRunId: attempt === 0 ? planningRunId : `${planningRunId}:retry${attempt}`,
56
+ denyTools: DENY_TOOLS,
57
+ enableAgentToAgent: true,
58
+ jsonMode: true,
59
+ });
60
+ if (planningOutput.status !== 'ok')
61
+ break;
62
+ // 尝试解析 JSON 决策
63
+ const parseResult = parsePlanningDecision(planningOutput.finalText);
64
+ if (parseResult.ok) {
65
+ log.info(`${tag} Planning 决策完成: action=${parseResult.decision.action} ` +
66
+ `尝试次数=${attempt} 字节数=${planningOutput.finalText.length}`);
67
+ runRegistry.unregisterRun(ctx.conversationId, planningRunId);
68
+ return { aborted: false, decision: parseResult.decision };
69
+ }
70
+ // JSON 解析失败,记录并决定是否重试
71
+ lastError = parseResult.error;
72
+ log.warn(`${tag} Planning JSON 解析失败 (第 ${attempt} 次): ${parseResult.error}`);
73
+ if (attempt < maxRetries) {
74
+ planningOutput = undefined;
75
+ }
76
+ }
77
+ catch (err) {
78
+ const e = err instanceof Error ? err : new Error(String(err));
79
+ log.error(`${tag} Planning 执行异常 (第 ${attempt} 次): ${e.message}`);
80
+ lastError = e.message;
81
+ if (attempt >= maxRetries) {
82
+ planningOutput = undefined;
83
+ }
84
+ }
85
+ }
86
+ runRegistry.unregisterRun(ctx.conversationId, planningRunId);
87
+ // 判断是否为 abort
88
+ if (planningOutput?.status === 'aborted' || planningAbort.aborted) {
89
+ return { aborted: true };
90
+ }
91
+ return { aborted: false, lastError };
92
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * @fileoverview Leader 自答路径执行器
3
+ *
4
+ * @description
5
+ * 当 Planning 阶段决策为 self_answer(Leader 自身有能力回答)或
6
+ * Planning 失败需要降级时,走此路径。
7
+ *
8
+ * 核心行为:
9
+ * - 构造流式 SocketStreamSink,实时推送 token 到前端
10
+ * - 调用 runner.run 让 Leader Agent 直接生成回复
11
+ * - 根据 run 终态(ok / aborted / failed)写入账本并转移状态机
12
+ *
13
+ * 与 dispatch 路径的区别:
14
+ * - 自答路径不涉及子任务拆分和 DAG 执行
15
+ * - 自答路径只产生一个 run(Leader 自身)
16
+ *
17
+ * @module leader-self-answer
18
+ */
19
+ // ─── 工具函数 ────────────────────────────────────────────────────────────────
20
+ import { generatePreRunId } from '../../utils/id-generator.js';
21
+ import { errorToString, normalizeRunStatus } from '../../utils/run-helpers.js';
22
+ import { getModuleLogger } from '../../../utils/logger.js';
23
+ import { generateMsgId } from '../../../dedup.js';
24
+ // ─── 路由层公共工具 ──────────────────────────────────────────────────────────
25
+ import { DENY_TOOLS, buildLeaderPrompt, createSinkForAgent, safeAppendAssistant, safeAppendSystem, } from './route-helpers.js';
26
+ /** 模块级日志记录器,统一使用 leader-route 命名空间 */
27
+ const log = getModuleLogger('group.leader-route');
28
+ /**
29
+ * 执行自答路径:正常流式 run,Leader 直接回答用户
30
+ *
31
+ * @remarks
32
+ * 执行流程:
33
+ * 1. 预分配 runId + 注册 AbortSignal
34
+ * 2. 构造 SocketStreamSink 并推送首帧
35
+ * 3. 调用 runner.run 执行 Agent
36
+ * 4. 根据 run 结果写入账本并转移状态机
37
+ *
38
+ * @param deps - 路由依赖
39
+ * @param input - 路由运行输入(ctx + state)
40
+ * @param agentId - Leader Agent ID
41
+ * @param memberCards - 群成员能力卡(用于构造 system prompt)
42
+ * @param tag - 日志标签
43
+ * @param routeName - 路由名称(透传到返回结果中)
44
+ * @param options - 可选配置(如 welcomeMode)
45
+ */
46
+ export async function executeSelfAnswer(deps, input, agentId, memberCards, tag, routeName, options) {
47
+ const { ctx, state } = input;
48
+ const { runner, runRegistry } = deps;
49
+ // ── 1. 预分配 runId 并注册 AbortSignal ──
50
+ // preRunId 在 run 开始前生成,确保首帧推送时前端就能拿到稳定的 runId
51
+ const preRunId = generatePreRunId(ctx.conversationId, 'leader-answer');
52
+ // 注册到 RunRegistry,支持用户中途 abort 时级联取消
53
+ const abortSignal = runRegistry.registerRun(ctx.conversationId, preRunId, agentId);
54
+ // 兜底生成 msgId,确保前端气泡能正确关联到触发消息
55
+ const originalMsgId = ctx.msgId ?? generateMsgId();
56
+ // ── 2. 构造流式 sink 并发送首帧 ──
57
+ // sinkContext 负责将 runner 产出的 token 实时推送到前端 WebSocket
58
+ // emitStart() 发送首帧,前端据此创建气泡占位
59
+ const sinkContext = createSinkForAgent(deps, ctx, agentId, preRunId, originalMsgId);
60
+ sinkContext.emitStart();
61
+ // ── 3. 执行 Agent run ──
62
+ // 根据是否为欢迎消息模式,选择不同的 system prompt
63
+ let extraSystemPrompt;
64
+ if (options?.welcomeMode) {
65
+ // 欢迎消息模式:使用专门的 buildWelcome prompt
66
+ try {
67
+ extraSystemPrompt = deps.promptBuilder.buildWelcome({
68
+ leaderAgentId: agentId,
69
+ groupName: ctx.meta.name || ctx.meta.groupId,
70
+ groupDesc: ctx.meta.desc || '',
71
+ memberCards,
72
+ });
73
+ }
74
+ catch (err) {
75
+ log.warn(`${tag} 构建 Welcome 提示词失败: ${err},降级为 Leader prompt`);
76
+ extraSystemPrompt = buildLeaderPrompt(deps, agentId, memberCards, tag);
77
+ }
78
+ }
79
+ else {
80
+ // 普通自答模式:使用 Leader prompt
81
+ extraSystemPrompt = buildLeaderPrompt(deps, agentId, memberCards, tag);
82
+ }
83
+ try {
84
+ const runOutput = await runner.run({
85
+ groupId: ctx.groupId,
86
+ agentId,
87
+ userId: ctx.userId,
88
+ role: 'leader', // 标识为 leader 角色,影响模型选择和 token 限制
89
+ message: ctx.userMessage,
90
+ systemPrompt: extraSystemPrompt,
91
+ abortSignal, // 用户 abort 时自动取消请求
92
+ sinkContext, // 统一流式输出上下文
93
+ preassignedRunId: preRunId, // 确保 runId 全程一致
94
+ denyTools: DENY_TOOLS, // 禁止 Agent 调用会话管理类工具
95
+ enableAgentToAgent: true, // 允许 Agent 间通信(如引用其他 Agent 的输出)
96
+ // 传递文件附件,使 AI 能感知用户上传的文件
97
+ attachments: ctx.files?.filter((f) => f.uri).map((f) => ({
98
+ name: f.name,
99
+ mimeType: f.mimeType || 'application/octet-stream',
100
+ url: f.uri,
101
+ localPath: f.localPath,
102
+ })),
103
+ });
104
+ // ── 4. 处理 run 结果并写入账本 ──
105
+ // runner 可能在内部重新分配 runId(如重试场景),优先使用实际 runId
106
+ const realRunId = runOutput.runId || preRunId;
107
+ const summary = {
108
+ runId: realRunId,
109
+ agentId,
110
+ status: normalizeRunStatus(runOutput.status),
111
+ };
112
+ // 根据 run 终态填充 summary 的可选字段,供上层日志/metrics 使用
113
+ if (runOutput.status === 'ok') {
114
+ summary.finalText = runOutput.finalText; // 完整回复文本
115
+ }
116
+ else if (runOutput.status === 'aborted') {
117
+ // 被中止时也保留已输出的部分内容,供 flushLedger 持久化
118
+ summary.finalText = runOutput.finalText || undefined;
119
+ summary.errorMessage = 'aborted';
120
+ }
121
+ else {
122
+ summary.errorMessage = errorToString(runOutput.error); // 错误描述
123
+ }
124
+ // 确定终态并转移状态机(DONE / ABORTED / FAILED)
125
+ const finalStatus = resolveTerminalState(runOutput.status, summary, { state, tag });
126
+ // 将结果持久化到群账本(assistant 消息或 system 事件)
127
+ await flushLedger(finalStatus, deps, ctx, agentId, realRunId, tag, summary);
128
+ // 释放 RunRegistry 中的注册,允许同一 conversation 发起新 run
129
+ runRegistry.unregisterRun(ctx.conversationId, preRunId);
130
+ return { route: routeName, finalStatus, runs: [summary], error: runOutput.error };
131
+ }
132
+ catch (err) {
133
+ // runner.run 抛出未预期异常(如网络超时、SDK 内部错误)的兜底处理
134
+ // 此分支确保即使 runner 崩溃,状态机也能正确终结,不会卡在 RUNNING 态
135
+ const e = err instanceof Error ? err : new Error(String(err));
136
+ log.error(`${tag} 自答执行异常: ${e.message}`);
137
+ state.forceTerminal('FAILED');
138
+ runRegistry.unregisterRun(ctx.conversationId, preRunId);
139
+ return {
140
+ route: routeName,
141
+ finalStatus: 'FAILED',
142
+ runs: [{ runId: preRunId, agentId, status: 'failed', errorMessage: e.message }],
143
+ error: e,
144
+ };
145
+ }
146
+ }
147
+ // ─── 私有辅助函数 ────────────────────────────────────────────────────────────
148
+ /**
149
+ * 根据 run 输出状态确定终态并转移状态机
150
+ *
151
+ * @remarks
152
+ * 状态转移规则:
153
+ * - ok → transitionTo('DONE') 正常完成
154
+ * - aborted → forceTerminal('ABORTED') 用户主动取消
155
+ * - 其他 → forceTerminal('FAILED') 执行失败
156
+ *
157
+ * 注意:forceTerminal 与 transitionTo 的区别在于前者跳过中间态校验,
158
+ * 适用于异常终结场景。
159
+ *
160
+ * @param outputStatus - runner 返回的原始状态字符串
161
+ * @param summary - 当前 run 的摘要对象(可能被修改 errorMessage)
162
+ * @param opts.state - 会话状态机实例
163
+ * @param opts.tag - 日志标签
164
+ * @returns 终态值(DONE / ABORTED / FAILED)
165
+ */
166
+ function resolveTerminalState(outputStatus, summary, opts) {
167
+ const { state, tag } = opts;
168
+ if (outputStatus === 'ok') {
169
+ state.transitionTo('DONE');
170
+ log.info(`${tag} 自答完成,字节数=${summary.finalText?.length ?? 0}`);
171
+ return 'DONE';
172
+ }
173
+ if (outputStatus === 'aborted') {
174
+ // aborted 时 runner 不一定填充 error,手动标记
175
+ summary.errorMessage = 'aborted';
176
+ state.forceTerminal('ABORTED');
177
+ return 'ABORTED';
178
+ }
179
+ // 其他状态(如 'error'、'timeout' 等)统一视为失败
180
+ state.forceTerminal('FAILED');
181
+ return 'FAILED';
182
+ }
183
+ /**
184
+ * 根据终态将结果写入对应账本
185
+ *
186
+ * @remarks
187
+ * 账本写入策略:
188
+ * - DONE → appendAssistant:记录 Agent 的完整回复内容
189
+ * - ABORTED → appendSystem('user_aborted'):记录用户取消事件
190
+ * - FAILED → appendSystem('run_failed'):记录失败事件及错误信息
191
+ *
192
+ * 所有写入操作通过 safe* 包装,失败时仅 warn 不抛异常,
193
+ * 确保账本写入失败不影响主流程返回。
194
+ *
195
+ * @param finalStatus - 已确定的终态
196
+ * @param deps - 路由依赖(使用 ledger)
197
+ * @param ctx - 调度上下文
198
+ * @param agentId - 发言 Agent ID
199
+ * @param runId - 关联的 runId
200
+ * @param tag - 日志标签
201
+ * @param summary - run 摘要(提供 finalText / errorMessage)
202
+ */
203
+ async function flushLedger(finalStatus, deps, ctx, agentId, runId, tag, summary) {
204
+ switch (finalStatus) {
205
+ case 'DONE':
206
+ // 将 Agent 完整回复持久化,供历史消息回放使用
207
+ await safeAppendAssistant(deps, ctx.groupId, agentId, summary.finalText ?? '', runId, tag);
208
+ break;
209
+ case 'ABORTED':
210
+ // 被中止时:如果已有部分输出内容,将其作为 assistant 条目持久化,
211
+ // 刷新页面后用户仍能看到中断前的内容。
212
+ if (summary.finalText) {
213
+ await safeAppendAssistant(deps, ctx.groupId, agentId, summary.finalText, runId, tag);
214
+ }
215
+ // 在 assistant 条目之后写 user_aborted,保证前端按 ts 排序后终止提示在部分内容之后
216
+ await safeAppendSystem(deps, ctx.groupId, 'user_aborted', agentId, ctx.userId, runId, tag);
217
+ break;
218
+ case 'FAILED':
219
+ // 记录失败事件及错误详情,便于排查问题
220
+ await safeAppendSystem(deps, ctx.groupId, 'run_failed', agentId, ctx.userId, runId, tag, summary.errorMessage);
221
+ break;
222
+ }
223
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * @file MentionConcurrentRoute —— 路径 B:@ 命中成员串行回复
3
+ * @description
4
+ * 适用场景:用户消息 @ 了群内成员,被 @ 的成员按顺序串行回复,无需 Leader 参与决策。
5
+ *
6
+ * 执行流程:
7
+ * 1. 状态转移:PENDING → ROUTING → RUNNING_CHILDREN
8
+ * 2. 为每个被 @ 的成员创建独立的 run(预分配 runId + AbortSignal)
9
+ * 3. 按顺序串行执行所有被 @ 成员的 Agent run
10
+ * 4. 每个成员独立流式输出回复(通过 SocketStreamSink 推流)
11
+ * 5. 所有成员完成后,根据结果汇总终态 → DONE / FAILED / ABORTED
12
+ *
13
+ * 设计要点:
14
+ * - 不涉及 Leader 决策,直接由被 @ 的成员回答
15
+ * - 每个被 @ 的成员按顺序串行执行,前一个完成后再执行下一个
16
+ * - 不进行结果汇总,每个成员直接回复用户
17
+ */
18
+ // ─── 工具函数 / 常量导入 ───────────────────────────────────────────────────────
19
+ import { generatePreRunId } from '../../utils/id-generator.js';
20
+ import { errorToString, normalizeRunStatus } from '../../utils/run-helpers.js';
21
+ import { getModuleLogger } from '../../../utils/logger.js';
22
+ import { generateMsgId } from '../../../dedup.js';
23
+ // ─── 路由层公共工具 ─────────────────────────────────────────────────────────
24
+ import { buildTag, createSinkForAgent, safeAppendAssistant, safeAppendSystem } from './route-helpers.js';
25
+ /**
26
+ * 路径 B —— Mention 串行层
27
+ *
28
+ * @remarks
29
+ * 实现 DispatchRoute 接口,由 Orchestrator 在 mentions 非空时选择调用。
30
+ * 内部为每个被 @ 的 agentId 创建独立的 run,按顺序串行执行后汇总终态。
31
+ */
32
+ export class MentionConcurrentRoute {
33
+ deps;
34
+ /** 路径标识名,用于日志和 RouteResult 中标记来源 */
35
+ name = 'mention_concurrent';
36
+ /** 模块级日志实例 */
37
+ log = getModuleLogger('group.mention-route');
38
+ /**
39
+ * @param deps - 由 Orchestrator 注入的共享依赖(runner、ledger、emitter 等)
40
+ */
41
+ constructor(deps) {
42
+ this.deps = deps;
43
+ }
44
+ /**
45
+ * 执行路径 B(Mention 串行层)
46
+ *
47
+ * @param input - 路径入参,包含调度上下文和状态机
48
+ * @returns 路径执行结果,包含终态和各 run 摘要
49
+ */
50
+ async run(input) {
51
+ const { ctx, state } = input;
52
+ const tag = buildTag(ctx);
53
+ // ── 1. 状态转移:PENDING → ROUTING → RUNNING_CHILDREN ──
54
+ // 若当前仍为初始态 PENDING,先过渡到 ROUTING 表示正在选路
55
+ if (state.getStatus() === 'PENDING') {
56
+ state.transitionTo('ROUTING');
57
+ }
58
+ // 进入 RUNNING_CHILDREN 表示子 run 开始执行
59
+ state.transitionTo('RUNNING_CHILDREN');
60
+ // ── 2. 串行执行所有被 @ 成员的 run ──
61
+ // 按 mentions 数组顺序逐个执行,前一个完成后再启动下一个
62
+ const runResults = [];
63
+ for (const agentId of ctx.mentions) {
64
+ const result = await this.executeMentionRun(ctx, agentId, tag);
65
+ runResults.push(result);
66
+ }
67
+ // ── 3. 汇总终态 ──
68
+ // 根据所有 run 的结果决定整体状态(DONE / FAILED / ABORTED)
69
+ const finalStatus = this.determineFinalStatus(runResults, state);
70
+ // ── 4. ABORTED 时写入 user_aborted 系统事件 ──
71
+ // 在所有 run 结束(部分内容已 appendAssistant)之后再写,
72
+ // 保证前端按 ts 排序后终止提示在 agent 部分回复内容之后。
73
+ if (finalStatus === 'ABORTED') {
74
+ try {
75
+ await this.deps.ledger.appendSystem({
76
+ groupId: ctx.groupId,
77
+ event: 'user_aborted',
78
+ userId: ctx.userId,
79
+ });
80
+ }
81
+ catch (err) {
82
+ this.log.warn(`${tag} appendSystem(user_aborted) 失败: ${err}`);
83
+ }
84
+ }
85
+ this.log.info(`${tag} finished status=${finalStatus} mentionCount=${ctx.mentions.length} ` +
86
+ `successCount=${runResults.filter((r) => r.status === 'done').length}`);
87
+ return {
88
+ route: this.name,
89
+ finalStatus,
90
+ runs: runResults,
91
+ error: finalStatus === 'FAILED' ? new Error('一个或多个 Mention run 执行失败') : undefined,
92
+ };
93
+ }
94
+ /**
95
+ * 为单个被 @ 的成员执行 Agent run
96
+ *
97
+ * @param ctx - 调度上下文
98
+ * @param agentId - 被 @ 的成员 Agent ID
99
+ * @param tag - 日志前缀
100
+ * @returns 该 run 的执行摘要(不会抛出异常)
101
+ */
102
+ async executeMentionRun(ctx, agentId, tag) {
103
+ const { runner, runRegistry } = this.deps;
104
+ const individualTag = `${tag} agent=${agentId}`;
105
+ // ── 1. 预分配 runId 并注册 AbortSignal ──
106
+ // 生成唯一 runId,同时在 RunRegistry 中注册以支持级联 abort
107
+ const preRunId = generatePreRunId(ctx.conversationId, 'mention');
108
+ const abortSignal = runRegistry.registerRun(ctx.conversationId, preRunId, agentId);
109
+ // ── 2. 构造独立的流式输出上下文(每个成员独立推流) ──
110
+ // 每个被 @ 成员拥有独立的 replyMsgId,前端据此区分不同成员的流式回复
111
+ const originalMsgId = ctx.msgId ?? generateMsgId();
112
+ const sinkContext = createSinkForAgent(this.deps, ctx, agentId, preRunId, originalMsgId);
113
+ // 在 runner.run 之前显式发 typing_start,通知前端该成员即将开始输出
114
+ sinkContext.emitStart();
115
+ // ── 3. 执行 Agent run ──
116
+ let runOutput;
117
+ try {
118
+ runOutput = await runner.run({
119
+ groupId: ctx.groupId,
120
+ agentId,
121
+ userId: ctx.userId,
122
+ role: 'mention', // 标记为 mention 角色
123
+ message: ctx.userMessage,
124
+ systemPrompt: '', // Mention 场景直接使用 Agent 默认配置,不注入额外 system prompt
125
+ abortSignal, // 传入 abort 信号以支持用户中止
126
+ sinkContext, // 统一流式输出上下文
127
+ preassignedRunId: preRunId, // 使用预分配的 runId 保证一致性
128
+ // 传递文件附件,使 AI 能感知用户上传的文件
129
+ attachments: ctx.files?.filter((f) => f.uri).map((f) => ({
130
+ name: f.name,
131
+ mimeType: f.mimeType || 'application/octet-stream',
132
+ url: f.uri,
133
+ localPath: f.localPath,
134
+ })),
135
+ });
136
+ }
137
+ catch (err) {
138
+ // runner.run 内部已兜底,此处为极端异常(如 OOM)的最终防线
139
+ const e = err instanceof Error ? err : new Error(String(err));
140
+ this.log.error(`${individualTag} runner 异常: ${e.message}`);
141
+ // 异常时也需注销 run,避免 RunRegistry 泄漏
142
+ runRegistry.unregisterRun(ctx.conversationId, preRunId);
143
+ return { runId: preRunId, agentId, status: 'failed', errorMessage: errorToString(e) };
144
+ }
145
+ // ── 4. 处理 run 结果 ──
146
+ // 优先使用 runner 返回的真实 runId(SDK 可能重新分配),兜底用预分配 ID
147
+ const realRunId = runOutput.runId || preRunId;
148
+ const status = normalizeRunStatus(runOutput.status);
149
+ const summary = { runId: realRunId, agentId, status };
150
+ if (runOutput.status === 'ok') {
151
+ // 成功:记录最终文本并写入 assistant 账本,供历史消息回放
152
+ summary.finalText = runOutput.finalText;
153
+ await safeAppendAssistant(this.deps, ctx.groupId, agentId, runOutput.finalText, realRunId, individualTag);
154
+ this.log.info(`${individualTag} 回复完成 bytes=${runOutput.finalText.length}`);
155
+ }
156
+ else if (runOutput.status === 'aborted') {
157
+ // 被中止:如果已有部分输出内容,将其作为 assistant 条目持久化,
158
+ // 刷新页面后用户仍能看到中断前的内容。
159
+ // user_aborted 系统事件由 run 方法在所有 run 结束后统一写入,保证 ts 顺序正确。
160
+ summary.errorMessage = 'aborted';
161
+ if (runOutput.finalText) {
162
+ summary.finalText = runOutput.finalText;
163
+ await safeAppendAssistant(this.deps, ctx.groupId, agentId, runOutput.finalText, realRunId, individualTag);
164
+ this.log.info(`${individualTag} 中止但已有部分内容,已持久化 bytes=${runOutput.finalText.length}`);
165
+ }
166
+ }
167
+ else {
168
+ // 失败:记录错误信息并写入 system 账本,便于排查问题
169
+ summary.errorMessage = errorToString(runOutput.error);
170
+ await this.safeAppendSystemEvent(ctx, agentId, runOutput, realRunId, individualTag);
171
+ }
172
+ // ── 5. 收尾:从注册表中注销 run,释放 abort 监听资源 ──
173
+ runRegistry.unregisterRun(ctx.conversationId, preRunId);
174
+ return summary;
175
+ }
176
+ /**
177
+ * 根据所有 run 的结果汇总终态并转移状态机
178
+ *
179
+ * @remarks
180
+ * 优先级:ABORTED > FAILED > DONE
181
+ * 只要有一个 run 被 abort,整体标记为 ABORTED;
182
+ * 只要有一个 run 失败(且无 abort),整体标记为 FAILED。
183
+ */
184
+ determineFinalStatus(results, state) {
185
+ // 检查是否存在被用户主动中止的 run
186
+ const hasAborted = results.some((r) => r.status === 'aborted');
187
+ // 检查是否存在执行失败的 run
188
+ const hasFailed = results.some((r) => r.status === 'failed');
189
+ // 优先级:ABORTED > FAILED > DONE
190
+ if (hasAborted) {
191
+ state.forceTerminal('ABORTED');
192
+ return 'ABORTED';
193
+ }
194
+ if (hasFailed) {
195
+ state.forceTerminal('FAILED');
196
+ return 'FAILED';
197
+ }
198
+ // 全部成功,正常转移到 DONE
199
+ state.transitionTo('DONE');
200
+ return 'DONE';
201
+ }
202
+ /**
203
+ * 安全写入 system 事件账本
204
+ *
205
+ * @remarks
206
+ * 将非成功状态的 run 信息记录到系统账本,便于运维排查和审计。
207
+ * 根据 run 状态写入不同事件类型:
208
+ * - aborted → user_aborted 事件(用户主动中止)
209
+ * - 其他失败 → run_failed 事件(Agent 执行异常)
210
+ *
211
+ * 异常仅 warn 级别记录,不影响主流程。
212
+ */
213
+ async safeAppendSystemEvent(ctx, agentId, runOutput, runId, tag) {
214
+ try {
215
+ // 注意:aborted 分支已在上层处理(保存部分内容为 assistant 条目),
216
+ // 此方法现在仅处理非 abort 的失败场景。
217
+ if (runOutput.status !== 'aborted') {
218
+ // Agent 执行失败:记录 run_failed 事件及错误详情
219
+ await safeAppendSystem(this.deps, ctx.groupId, 'run_failed', agentId, ctx.userId, runId, tag, errorToString(runOutput.error));
220
+ }
221
+ }
222
+ catch (err) {
223
+ this.log.warn(`${tag} appendSystem 失败: ${err}`);
224
+ }
225
+ }
226
+ }