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
@@ -19,6 +19,7 @@ import { createInboundHandler } from './inbound.js';
19
19
  import { bindSocketHandlers, registerSocket, unregisterSocket, flushPendingMessages } from './socket/index.js';
20
20
  import { ReliableEmitter } from './socket/reliable-emitter.js';
21
21
  import { buildAuthHeaders } from './utils/index.js';
22
+ import { GroupStorageCore, Orchestrator, createGroupInboundHandler, } from './group/index.js';
22
23
  // ============================================================
23
24
  // Gateway 实例追踪(防止同一 accountId 创建多个连接)
24
25
  // ============================================================
@@ -125,7 +126,7 @@ export async function startGateway(ctx) {
125
126
  let healthHeartbeatTimer = null;
126
127
  // ---- 可靠发送器(所有出站 emit 都通过此实例) ----
127
128
  // ReliableEmitter 负责:断线时缓冲消息、重连后 flush、ACK 超时重试
128
- const reliableEmitter = new ReliableEmitter(() => currentSocket, log);
129
+ const reliableEmitter = new ReliableEmitter(() => currentSocket);
129
130
  // ---- WebSocket 发送抽象 ----
130
131
  /**
131
132
  * 底层 emit:将 PrivateMessageData 通过 ReliableEmitter 发送给客户端。
@@ -196,10 +197,24 @@ export async function startGateway(ctx) {
196
197
  };
197
198
  /** SocketEmitter 抽象:将 emit / sendReply / sendFiles 和 botClientId 打包传给 inbound 处理器 */
198
199
  const emitter = { emit, sendReply, sendFiles, botClientId };
200
+ // ---- 群聊调度器装配(路径 A 全链路服务于 chatKind === 'group' 的消息) ----
201
+ // 多 Gateway 进程共享同一份 ~/.openclaw/groups 目录,但每 account 一份 Orchestrator
202
+ // (RunRegistry 是进程内存态,跨 account 互不干扰)。
203
+ const groupStorage = new GroupStorageCore();
204
+ const orchestrator = new Orchestrator({
205
+ accountId: account.accountId,
206
+ storage: groupStorage,
207
+ emitter,
208
+ });
209
+ const groupInboundHandler = createGroupInboundHandler({
210
+ emitter,
211
+ storage: groupStorage,
212
+ orchestrator,
213
+ });
199
214
  // ---- 入站消息处理器 ----
200
215
  // createInboundHandler 返回一个异步函数,负责处理单条用户消息(文件处理、路由、AI 分发、回复)
201
216
  const handleInboundMessage = createInboundHandler(account, emitter, log);
202
- // ---- 消息处理(fire-and-forget) ----
217
+ // ---- 私聊消息处理(fire-and-forget) ----
203
218
  // 不做串行队列:并发/abort 由 openclaw 的 session 写锁 + followup queue 负责。
204
219
  // 这里仅兜底 inbound 自身未捕获的异常。
205
220
  const handleMessage = (msg) => {
@@ -231,6 +246,35 @@ export async function startGateway(ctx) {
231
246
  }
232
247
  })();
233
248
  };
249
+ // ---- 群聊消息处理 ----
250
+ const handleGroupMessage = (data) => {
251
+ if (isAborted)
252
+ return;
253
+ void (async () => {
254
+ try {
255
+ await groupInboundHandler(data);
256
+ }
257
+ catch (err) {
258
+ log?.error(`[${CHANNEL_KEY}] Group handler error: ${err}`);
259
+ try {
260
+ const groupId = data.extra?.groupId;
261
+ emitter.emit({
262
+ msgId: generateMsgId(),
263
+ from: emitter.botClientId,
264
+ to: data.from,
265
+ content: '群消息处理异常,请稍后重试。',
266
+ timestamp: Date.now(),
267
+ replyToMsgId: data.msgId,
268
+ chatKind: 'group',
269
+ extra: { groupId: typeof groupId === 'string' ? groupId : undefined },
270
+ });
271
+ }
272
+ catch (notifyErr) {
273
+ log?.error(`[${CHANNEL_KEY}] Failed to notify user about group handler error: ${notifyErr}`);
274
+ }
275
+ }
276
+ })();
277
+ };
234
278
  // ---- 生命周期 ----
235
279
  /**
236
280
  * 销毁当前 Gateway 实例,释放所有资源:
@@ -268,8 +312,6 @@ export async function startGateway(ctx) {
268
312
  const socket = new NativeSocketClient(WS_URL, {
269
313
  // 认证:ticket 作为 URL query 参数,替代 Authorization header
270
314
  path: `${SOCKET_PATH}${ticketQuery}&enableMultiLogin=false`,
271
- // 注入日志对象,便于 NativeSocketClient 内部打印连接/重连/错误等诊断信息
272
- log,
273
315
  logPrefix: `[${CHANNEL_KEY}:${account.accountId}:NativeSocket]`,
274
316
  });
275
317
  currentSocket = socket;
@@ -291,7 +333,7 @@ export async function startGateway(ctx) {
291
333
  // 恢复可靠发送器(断线时已 pause,重连后需要 resume 才能继续发送)
292
334
  reliableEmitter.resume();
293
335
  // 重连后 flush 断线期间缓冲的 outbound 消息(主动发送场景)
294
- const { sent, failed } = flushPendingMessages(account.accountId, log);
336
+ const { sent, failed } = flushPendingMessages(account.accountId);
295
337
  if (sent > 0 || failed > 0) {
296
338
  log?.info(`[${CHANNEL_KEY}] Flushed buffered outbound messages: sent=${sent}, failed=${failed}`);
297
339
  }
@@ -311,10 +353,12 @@ export async function startGateway(ctx) {
311
353
  bindSocketHandlers(socket, {
312
354
  account,
313
355
  botClientId,
314
- log,
315
356
  handleMessage,
316
357
  onEvent,
317
358
  reliableEmitter,
359
+ handleGroupMessage,
360
+ orchestrator,
361
+ groupStorage,
318
362
  });
319
363
  /**
320
364
  * 连接错误事件处理。
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @file 群聊事件常量定义
3
+ * @description 定义群聊模块中所有的事件类型和对应的消息通道名称
4
+ */
5
+ /** 群组基础操作请求事件 */
6
+ export const EVENT_GROUP_REQUEST = 'group:request';
7
+ /** 群组基础操作响应事件 */
8
+ export const EVENT_GROUP_RESPONSE = 'group:response';
9
+ /** 群组成员管理请求事件 */
10
+ export const EVENT_GROUP_MEMBER_REQUEST = 'group:member:request';
11
+ /** 群组成员管理响应事件 */
12
+ export const EVENT_GROUP_MEMBER_RESPONSE = 'group:member:response';
13
+ /** 群组历史消息请求事件 */
14
+ export const EVENT_GROUP_HISTORY_REQUEST = 'group:history:request';
15
+ /** 群组历史消息响应事件 */
16
+ export const EVENT_GROUP_HISTORY_RESPONSE = 'group:history:response';
17
+ /** 群组操作中止请求事件 */
18
+ export const EVENT_GROUP_ABORT_REQUEST = 'group:abort:request';
19
+ /** 群组操作中止响应事件 */
20
+ export const EVENT_GROUP_ABORT_RESPONSE = 'group:abort:response';
@@ -0,0 +1,254 @@
1
+ /**
2
+ * @file 群聊消息入站处理器
3
+ * @description
4
+ * 接收 Socket 私信消息,基于 extra.groupId 路由为群聊消息,
5
+ * 完成权限校验后写入账本并交由 Orchestrator 调度。
6
+ * 所有依赖通过 {@link GroupInboundDeps} 注入,错误通过 {@link sendError} 回发。
7
+ */
8
+ import { generateMsgId } from '../../dedup.js';
9
+ import { CHANNEL_KEY, MEDIA_MAX_BYTES, LOCALFILE_SCHEME, resolveEffectiveApiKey } from '../../config.js';
10
+ import { normalizeMentions, guessMimeType } from '../utils/index.js';
11
+ import { getModuleLogger } from '../../utils/logger.js';
12
+ import { parseDataUrl, formatFileSize } from '../../media.js';
13
+ import { downloadFileFromServer, uploadFileToServer, getFileDownloadUrl } from '../../file-storage.js';
14
+ import { getLightclawRuntime } from '../../runtime.js';
15
+ /**
16
+ * 工厂函数:基于注入的依赖创建一个群聊入站处理器。
17
+ *
18
+ * @param deps 依赖注入对象,参见 {@link GroupInboundDeps}
19
+ * @returns 一个可直接挂载到 Socket 私信事件上的异步处理函数
20
+ */
21
+ export function createGroupInboundHandler(deps) {
22
+ const { emitter, storage, orchestrator } = deps;
23
+ const logger = getModuleLogger('group.inbound');
24
+ /**
25
+ * 以群聊系统消息的形式向指定用户回发错误提示。
26
+ * 内部捕获 emit 异常,确保错误回发本身不会再抛出影响主流程。
27
+ *
28
+ * @param userId 目标用户 ID
29
+ * @param message 错误文案
30
+ * @param extra 附加 extra 字段(通常包含 groupId)
31
+ * @param replyToMsgId 关联的原始消息 ID(用于在 UI 上展示回复关系)
32
+ */
33
+ const sendError = (userId, message, extra, replyToMsgId) => {
34
+ // 错误回发统一带上 streamStatus: 'failed',便于前端流式状态机识别终态
35
+ const finalExtra = { ...extra, streamStatus: 'failed' };
36
+ try {
37
+ const ok = emitter.emit({
38
+ msgId: generateMsgId(),
39
+ from: emitter.botClientId,
40
+ to: userId,
41
+ content: message,
42
+ timestamp: Date.now(),
43
+ replyToMsgId,
44
+ chatKind: 'group',
45
+ extra: finalExtra,
46
+ });
47
+ if (!ok) {
48
+ logger.warn(`[${CHANNEL_KEY}] sendError 发送失败(socket 可能已断开)。to=${userId}`);
49
+ }
50
+ }
51
+ catch (err) {
52
+ logger.warn(`[${CHANNEL_KEY}] sendError 执行异常: ${err}`);
53
+ }
54
+ };
55
+ /**
56
+ * 实际的消息处理函数。整体流程:
57
+ * 1. 从 extra 解析 groupId,校验合法性;
58
+ * 2. 读取群元数据 meta,校验群存在且用户为拥有者;
59
+ * 3. 规范化 mentions;
60
+ * 4. 将用户消息写入 Ledger;
61
+ * 5. 构造 DispatchContext 并交由 Orchestrator 异步调度。
62
+ */
63
+ return async function handle(data) {
64
+ const userId = data.from;
65
+ // 统一原始触发消息 ID:缺失时本地生成一个,使 conversationId / replyToMsgId / ctx.msgId 保持一致
66
+ const triggerMsgId = data.msgId ?? generateMsgId();
67
+ // 注意:data.extra 来源于客户端,字段类型不可信,统一以 unknown 承载,由下游做严格的运行时校验。
68
+ const extraIn = (data.extra ?? {});
69
+ const groupId = extraIn.groupId;
70
+ // 统一日志前缀
71
+ const tag = `[${CHANNEL_KEY}][group=${groupId}][user=${userId}]`;
72
+ // —— 1. 入参校验:必须携带合法的 groupId ——
73
+ if (typeof groupId !== 'string' || !groupId.trim()) {
74
+ logger.warn(`[${tag}] 群消息缺少 groupId,无法处理。msgId=${triggerMsgId} from=${userId}`);
75
+ sendError(userId, '群消息缺少 groupId,无法处理', {}, triggerMsgId);
76
+ return;
77
+ }
78
+ // —— 2. 读取群元数据 ——
79
+ let meta;
80
+ try {
81
+ meta = await storage.readGroupMeta(groupId);
82
+ }
83
+ catch (err) {
84
+ logger.error(`${tag} 读取群元数据失败: ${err}`);
85
+ sendError(userId, '读取群信息失败,请稍后重试', { groupId }, triggerMsgId);
86
+ return;
87
+ }
88
+ // 群不存在/已解散
89
+ if (!meta) {
90
+ logger.warn(`${tag} 群不存在或已解散`);
91
+ sendError(userId, '群不存在或已解散', { groupId }, triggerMsgId);
92
+ return;
93
+ }
94
+ // —— 3. 权限校验:仅群拥有者可在群内发言 ——
95
+ if (!meta.ownerIds.includes(userId)) {
96
+ logger.warn(`${tag} 你不是该群的拥有者,无法在此群发言`);
97
+ sendError(userId, '你不是该群的拥有者,无法在此群发言', { groupId }, triggerMsgId);
98
+ return;
99
+ }
100
+ // —— 4. 规范化业务数据 ——
101
+ const content = (data.content ?? '').trim();
102
+ const memberIds = meta.members.map((m) => m.agentId);
103
+ const mentions = normalizeMentions(extraIn.mentions, memberIds);
104
+ // —— 4.5 处理文件附件(参考私聊 inbound.ts 的文件处理模式) ——
105
+ const rawFiles = data.files ?? [];
106
+ const pluginRuntime = getLightclawRuntime();
107
+ // 获取当前用户的 apiKey,用于从云端下载文件(需要认证)
108
+ const effectiveApiKey = resolveEffectiveApiKey({ senderId: userId });
109
+ /** 群聊文件附件(与私聊 FileAttachment 对齐:name + mimeType + size + uri) */
110
+ const groupFiles = [];
111
+ /** 传递给 AI 上下文的 Attachments(name + mimeType + url + localPath) */
112
+ const ctxAttachments = [];
113
+ /** 本地媒体路径列表(供 SDK vision 模型直接读取文件内容) */
114
+ const localMediaPaths = [];
115
+ /** 本地文件 MIME 类型列表,与 localMediaPaths 一一对应 */
116
+ const localMediaTypes = [];
117
+ /** 附加到用户消息末尾的文件描述文本(告知 AI 用户发送了哪些文件) */
118
+ let attachmentDescription = '';
119
+ for (const file of rawFiles) {
120
+ // 前端发送 FileAttachment 格式:{ name, mimeType, size, uri, bytes }
121
+ const fileName = file.name || 'unknown';
122
+ const fileMimeType = file.mimeType || guessMimeType(fileName);
123
+ const fileUri = file.uri || '';
124
+ const fileSize = file.size ?? 0;
125
+ groupFiles.push({ name: fileName, mimeType: fileMimeType, size: fileSize, uri: fileUri || undefined });
126
+ try {
127
+ let buffer;
128
+ let mimeType;
129
+ let uploadPublicUrl;
130
+ // 来源 1:data URL(base64 编码的文件内容)
131
+ const parsed = file.bytes ? parseDataUrl(file.bytes) : null;
132
+ if (parsed) {
133
+ buffer = parsed.buffer;
134
+ mimeType = parsed.mimeType;
135
+ }
136
+ else if (fileUri) {
137
+ // 来源 2:云端 URI(CDN 地址或文件路径)
138
+ logger.info(`${tag} 文件有 URI,从云端下载: ${fileUri}`);
139
+ const downloaded = await downloadFileFromServer(fileUri, { apiKey: effectiveApiKey });
140
+ buffer = downloaded.buffer;
141
+ mimeType = fileMimeType;
142
+ uploadPublicUrl = fileUri.startsWith('http') ? fileUri : getFileDownloadUrl(fileUri);
143
+ }
144
+ else {
145
+ // 来源 3:既无 bytes 也无 uri,无法处理,跳过此文件
146
+ logger.warn(`${tag} 文件无 bytes 或 uri: ${fileName},跳过`);
147
+ attachmentDescription += `\n用户发送了文件: ${fileName} (${formatFileSize(fileSize)})`;
148
+ continue;
149
+ }
150
+ // 保存到本地媒体存储(框架统一管理)
151
+ const saved = await pluginRuntime.channel.media.saveMediaBuffer(buffer, mimeType, 'inbound', MEDIA_MAX_BYTES, fileName);
152
+ localMediaPaths.push(saved.path);
153
+ localMediaTypes.push(mimeType);
154
+ const localPath = `${LOCALFILE_SCHEME}${saved.path}`;
155
+ if (uploadPublicUrl) {
156
+ // URI 来源:已有公网 URL,直接使用
157
+ ctxAttachments.push({ name: fileName, mimeType, url: localPath, localPath: saved.path });
158
+ }
159
+ else {
160
+ // data URL 来源:上传到 COS 获取公网 URL
161
+ try {
162
+ const uploadResult = await uploadFileToServer(saved.path, { apiKey: effectiveApiKey });
163
+ ctxAttachments.push({ name: fileName, mimeType, url: localPath, localPath: saved.path });
164
+ logger.info(`${tag} 文件已上传到 COS: ${saved.path} → ${uploadResult.url}`);
165
+ }
166
+ catch (uploadErr) {
167
+ logger.warn(`${tag} COS 上传失败,降级使用本地路径: ${uploadErr}`);
168
+ ctxAttachments.push({ name: fileName, mimeType, url: localPath, localPath: saved.path });
169
+ }
170
+ }
171
+ logger.info(`${tag} 文件已保存: ${saved.path} (${mimeType}, ${formatFileSize(saved.size)})`);
172
+ }
173
+ catch (err) {
174
+ // 单个文件处理失败不影响整条消息的处理,记录错误后继续
175
+ logger.error(`${tag} 文件处理失败 ${fileName}: ${err}`);
176
+ if (fileUri) {
177
+ // 降级:仅传 CDN URL
178
+ ctxAttachments.push({ name: fileName, mimeType: fileMimeType, url: fileUri });
179
+ }
180
+ }
181
+ attachmentDescription += `\n用户发送了文件: ${fileName} (${formatFileSize(fileSize)})`;
182
+ logger.info(`${tag} 文件附件: ${fileName} (${fileMimeType}, ${formatFileSize(fileSize)}) uri=${fileUri ? '有' : '无'}`);
183
+ }
184
+ // 将文件描述追加到用户消息中(与私聊一致,让 AI 知道用户发送了哪些文件)
185
+ const userMessage = content + attachmentDescription;
186
+ // —— 5. 用户消息入账(Ledger)——
187
+ try {
188
+ await orchestrator.ledger.appendUser({
189
+ groupId,
190
+ userId,
191
+ content,
192
+ mentions,
193
+ files: groupFiles.length > 0 ? groupFiles : undefined,
194
+ msgId: triggerMsgId,
195
+ });
196
+ }
197
+ catch (err) {
198
+ logger.error(`${tag} 用户消息入账失败: ${err}`);
199
+ sendError(userId, '消息入账失败,请稍后重试', { groupId }, triggerMsgId);
200
+ return;
201
+ }
202
+ // —— 6. 构造调度上下文并触发 dispatch ——
203
+ const conversationId = `conv:${groupId}:${triggerMsgId}`;
204
+ // 提前 ensure 以获取 abortSignal(dispatch 内部会幂等处理)
205
+ const abortSignal = orchestrator.runRegistry.ensureConversation(conversationId);
206
+ // 从 openclaw.json 配置中读取 agents.list,过滤掉不在配置中的成员
207
+ const currentCfg = pluginRuntime.config.loadConfig();
208
+ const configAgentIds = new Set((currentCfg.agents?.list ?? []).map((a) => a.id));
209
+ // 仅保留在 openclaw.json agents 列表中存在的成员
210
+ const validMembers = configAgentIds.size > 0 ? meta.members.filter((m) => configAgentIds.has(m.agentId)) : meta.members;
211
+ const ctx = {
212
+ conversationId,
213
+ groupId,
214
+ userId,
215
+ meta: {
216
+ groupId: meta.groupId,
217
+ members: validMembers,
218
+ defaultAgentId: meta.defaultAgentId,
219
+ ownerIds: meta.ownerIds,
220
+ },
221
+ userMessage,
222
+ mentions,
223
+ abortSignal,
224
+ msgId: triggerMsgId,
225
+ files: ctxAttachments.length > 0
226
+ ? ctxAttachments.map((a) => ({
227
+ name: a.name,
228
+ mimeType: a.mimeType,
229
+ size: groupFiles.find((f) => f.name === a.name)?.size ?? 0,
230
+ uri: a.url,
231
+ localPath: a.localPath,
232
+ }))
233
+ : undefined,
234
+ mediaPaths: localMediaPaths.length > 0 ? localMediaPaths : undefined,
235
+ mediaTypes: localMediaTypes.length > 0 ? localMediaTypes : undefined,
236
+ effectiveApiKey,
237
+ };
238
+ logger.info(`${tag} 开始调度 conv=${conversationId} mentions=[${mentions.join(',')}]`);
239
+ // dispatch 采用「即发即忘」模式,结果通过 .then 处理
240
+ orchestrator.dispatch(ctx).then((result) => {
241
+ logger.info(`${tag} 调度完成 status=${result.finalStatus} route=${result.route}`);
242
+ }, (err) => {
243
+ logger.error(`${tag} 调度异常: ${err instanceof Error ? err.message : String(err)}`);
244
+ // 主动 dispose 防止注册表泄漏
245
+ try {
246
+ orchestrator.runRegistry.disposeConversation(conversationId);
247
+ }
248
+ catch (cleanupErr) {
249
+ logger.warn(`${tag} 调度异常后清理会话失败: ${cleanupErr}`);
250
+ }
251
+ sendError(userId, '群消息处理异常,请稍后重试', { groupId }, triggerMsgId);
252
+ });
253
+ };
254
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @file 群聊功能入口文件
3
+ * @description 群聊模块的公共 API 出口,外部模块应从此文件导入所需功能
4
+ */
5
+ // ── 统一常量 ──
6
+ export { EVENT_GROUP_REQUEST, EVENT_GROUP_RESPONSE, EVENT_GROUP_MEMBER_REQUEST, EVENT_GROUP_MEMBER_RESPONSE, EVENT_GROUP_HISTORY_REQUEST, EVENT_GROUP_HISTORY_RESPONSE, EVENT_GROUP_ABORT_REQUEST, EVENT_GROUP_ABORT_RESPONSE, } from './constants/index.js';
7
+ // ── 统一工具函数 ──
8
+ export { generatePreRunId, normalizeRunStatus, errorToString, } from './utils/index.js';
9
+ // ── 存储层 ──
10
+ export { GroupStorageCore } from './storage/index.js';
11
+ export { ConcurrencyManager } from './storage/concurrency-manager.js';
12
+ // ── 调度器 ──
13
+ export { Orchestrator } from './orchestrator/orchestrator.js';
14
+ // ── 群入站处理器 ──
15
+ export { createGroupInboundHandler } from './inbound/index.js';