lightclawbot 1.2.6 → 1.2.8

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 (102) hide show
  1. package/dist/src/config.js +30 -3
  2. package/dist/src/gateway.js +58 -12
  3. package/dist/src/group/constants/index.js +20 -0
  4. package/dist/src/group/inbound/index.js +254 -0
  5. package/dist/src/group/index.js +15 -0
  6. package/dist/src/group/orchestrator/execution/agent-runner.js +299 -0
  7. package/dist/src/group/orchestrator/execution/index.js +7 -0
  8. package/dist/src/group/orchestrator/execution/prompt-builder.js +288 -0
  9. package/dist/src/group/orchestrator/execution/soul-resolver.js +38 -0
  10. package/dist/src/group/orchestrator/execution/types.js +7 -0
  11. package/dist/src/group/orchestrator/index.js +14 -0
  12. package/dist/src/group/orchestrator/lifecycle/conversation-state.js +162 -0
  13. package/dist/src/group/orchestrator/lifecycle/index.js +7 -0
  14. package/dist/src/group/orchestrator/lifecycle/ledger-writer.js +96 -0
  15. package/dist/src/group/orchestrator/lifecycle/run-registry.js +174 -0
  16. package/dist/src/group/orchestrator/orchestrator.js +265 -0
  17. package/dist/src/group/orchestrator/planning/index.js +13 -0
  18. package/dist/src/group/orchestrator/planning/plan-validator.js +233 -0
  19. package/dist/src/group/orchestrator/planning/planning-parser.js +207 -0
  20. package/dist/src/group/orchestrator/planning/subtask-executor.js +345 -0
  21. package/dist/src/group/orchestrator/planning/summarizer-runner.js +224 -0
  22. package/dist/src/group/orchestrator/routes/index.js +9 -0
  23. package/dist/src/group/orchestrator/routes/leader-dispatch.js +229 -0
  24. package/dist/src/group/orchestrator/routes/leader-orchestration-route.js +179 -0
  25. package/dist/src/group/orchestrator/routes/leader-planning.js +92 -0
  26. package/dist/src/group/orchestrator/routes/leader-self-answer.js +223 -0
  27. package/dist/src/group/orchestrator/routes/mention-concurrent-route.js +226 -0
  28. package/dist/src/group/orchestrator/routes/route-helpers.js +186 -0
  29. package/dist/src/group/orchestrator/routes/types.js +8 -0
  30. package/dist/src/group/services/group-cleanup-service.js +183 -0
  31. package/dist/src/group/services/group-creation-service.js +122 -0
  32. package/dist/src/group/services/group-deletion-service.js +111 -0
  33. package/dist/src/group/services/group-history-service.js +73 -0
  34. package/dist/src/group/services/group-member-service.js +169 -0
  35. package/dist/src/group/services/group-query-service.js +133 -0
  36. package/dist/src/group/services/group-update-service.js +144 -0
  37. package/dist/src/group/services/index.js +20 -0
  38. package/dist/src/group/storage/concurrency-manager.js +119 -0
  39. package/dist/src/group/storage/group-storage-core.js +227 -0
  40. package/dist/src/group/storage/index.js +12 -0
  41. package/dist/src/group/storage/message-reader.js +213 -0
  42. package/dist/src/group/storage/message-writer.js +229 -0
  43. package/dist/src/group/storage/slice-manager.js +165 -0
  44. package/dist/src/group/types/common.js +5 -0
  45. package/dist/src/group/types/index.js +5 -0
  46. package/dist/src/group/types/message.js +5 -0
  47. package/dist/src/group/types/orchestrator.js +5 -0
  48. package/dist/src/group/types/storage.js +5 -0
  49. package/dist/src/group/utils/id-generator.js +15 -0
  50. package/dist/src/group/utils/index.js +12 -0
  51. package/dist/src/group/utils/mime.js +36 -0
  52. package/dist/src/group/utils/normalize.js +32 -0
  53. package/dist/src/group/utils/run-helpers.js +36 -0
  54. package/dist/src/history/session-reader.js +8 -2
  55. package/dist/src/outbound.js +12 -19
  56. package/dist/src/shared.js +4 -3
  57. package/dist/src/socket/events/agents-request.js +147 -0
  58. package/dist/src/socket/events/chat-request.js +67 -0
  59. package/dist/src/socket/events/file-download.js +121 -0
  60. package/dist/src/socket/events/group-abort.js +59 -0
  61. package/dist/src/socket/events/group-history.js +59 -0
  62. package/dist/src/socket/events/group-member.js +83 -0
  63. package/dist/src/socket/events/group-request.js +91 -0
  64. package/dist/src/socket/events/history-request.js +95 -0
  65. package/dist/src/socket/events/index.js +39 -0
  66. package/dist/src/socket/events/message-private.js +82 -0
  67. package/dist/src/socket/handlers.js +53 -568
  68. package/dist/src/socket/native-socket.js +21 -20
  69. package/dist/src/socket/registry.js +6 -3
  70. package/dist/src/socket/reliable-emitter.js +16 -13
  71. package/dist/src/socket/service/chat-common.js +36 -0
  72. package/dist/src/socket/service/chat-create.js +75 -0
  73. package/dist/src/socket/service/chat-delete.js +94 -0
  74. package/dist/src/socket/service/chat-list.js +82 -0
  75. package/dist/src/socket/service/chat-update.js +83 -0
  76. package/dist/src/socket/service/group-abort.js +104 -0
  77. package/dist/src/socket/service/group-history.js +140 -0
  78. package/dist/src/socket/service/group-member.js +209 -0
  79. package/dist/src/socket/service/group.js +233 -0
  80. package/dist/src/socket/service/history.js +102 -0
  81. package/dist/src/socket/service/index.js +14 -0
  82. package/dist/src/socket/types/index.js +7 -0
  83. package/dist/src/socket/types/request.js +8 -0
  84. package/dist/src/socket/types/service.js +8 -0
  85. package/dist/src/socket/utils/agent-soul.js +95 -0
  86. package/dist/src/socket/utils/index.js +8 -0
  87. package/dist/src/socket/utils/message.js +83 -0
  88. package/dist/src/socket/utils/validate.js +42 -0
  89. package/dist/src/streaming/index.js +1 -0
  90. package/dist/src/streaming/stream-reply-sink.js +367 -20
  91. package/dist/src/streaming/thinking-formatter.js +325 -0
  92. package/dist/src/streaming/types.js +20 -1
  93. package/dist/src/{download-tool.js → tools/download-tool.js} +41 -35
  94. package/dist/src/tools/group-history-tool.js +172 -0
  95. package/dist/src/{upload-tool.js → tools/upload-tool.js} +2 -2
  96. package/dist/src/tools.js +4 -3
  97. package/dist/src/utils/index.js +1 -0
  98. package/dist/src/utils/logger.js +38 -0
  99. package/openclaw.plugin.json +2 -1
  100. package/package.json +1 -1
  101. package/dist/src/socket/agent-soul.js +0 -41
  102. package/dist/src/socket/chat.js +0 -257
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @file chat-update.ts
3
+ * @description Chat handler — update(修改会话标题)
4
+ *
5
+ * 根据 chatId 修改 title(并置 titleLocked=true + 刷新 updatedAt);
6
+ * 文件或条目不存在时返回 error 响应,不隐式创建。
7
+ */
8
+ import * as fs from 'node:fs';
9
+ import { CHANNEL_KEY, EVENT_CHAT_RESPONSE } from '../../config.js';
10
+ import { readChatsFile, resolveChatsFilePath, writeChatsFile } from '../../utils/common.js';
11
+ import { log, generateMsgId, createChatErrorSender } from './chat-common.js';
12
+ /**
13
+ * 对目标会话应用标题更新
14
+ * @param chat - 原始会话元数据
15
+ * @param newTitle - 新标题(已 trim)
16
+ * @returns 更新后的会话元数据副本
17
+ */
18
+ function applyTitleUpdate(chat, newTitle) {
19
+ return {
20
+ ...chat,
21
+ title: newTitle,
22
+ titleLocked: true,
23
+ updatedAt: Date.now(),
24
+ };
25
+ }
26
+ /**
27
+ * 发送更新成功响应
28
+ * @param params - handler 基础参数
29
+ * @param responseMsgId - 响应消息 ID
30
+ * @param updatedChat - 更新后的会话元数据
31
+ */
32
+ function emitUpdateResponse(params, responseMsgId, updatedChat) {
33
+ const { botClientId, userId, agentId, reliableEmitter } = params;
34
+ reliableEmitter.emitWithAck(EVENT_CHAT_RESPONSE, {
35
+ msgId: responseMsgId,
36
+ from: botClientId,
37
+ to: userId,
38
+ type: 'update',
39
+ agentId,
40
+ chats: [updatedChat],
41
+ timestamp: Date.now(),
42
+ }, responseMsgId);
43
+ }
44
+ /**
45
+ * 处理 EVENT_CHAT_REQUEST(type=update)
46
+ *
47
+ * 流程:校验 title → 校验文件存在 → 读取列表 → 查找目标 → 更新写回 → 回包
48
+ *
49
+ * @param params - Chat update 操作入参(含 chatId、title)
50
+ */
51
+ export function handleChatUpdate(params) {
52
+ const { userId, agentId, chatId, title, botClientId, reliableEmitter } = params;
53
+ const chatsPath = resolveChatsFilePath(agentId, userId);
54
+ const responseMsgId = generateMsgId();
55
+ const sendError = createChatErrorSender({ responseMsgId, botClientId, userId, agentId, type: 'update', reliableEmitter }, { chatId });
56
+ // 前置校验:标题不能为空
57
+ const nextTitle = title?.trim();
58
+ if (!nextTitle) {
59
+ sendError('标题不能为空');
60
+ return;
61
+ }
62
+ // 前置校验:会话文件必须存在
63
+ if (!fs.existsSync(chatsPath)) {
64
+ sendError(`会话文件不存在: ${chatsPath}`);
65
+ return;
66
+ }
67
+ try {
68
+ const chats = readChatsFile(chatsPath);
69
+ const targetChat = chats.find((c) => c.chatId === chatId);
70
+ if (!targetChat) {
71
+ sendError(`会话不存在: chatId=${chatId}`);
72
+ return;
73
+ }
74
+ const updatedChat = applyTitleUpdate(targetChat, nextTitle);
75
+ const updatedChats = chats.map((c) => (c.chatId === chatId ? updatedChat : c));
76
+ writeChatsFile(chatsPath, updatedChats);
77
+ log.info(`[${CHANNEL_KEY}] 会话标题更新成功: userId=${userId}, agentId=${agentId}, chatId=${chatId}, title="${nextTitle}"`);
78
+ emitUpdateResponse(params, responseMsgId, updatedChat);
79
+ }
80
+ catch (err) {
81
+ sendError(err instanceof Error ? err.message : String(err));
82
+ }
83
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @file Socket 网关侧的群消息中止(Group Abort)请求处理器。
3
+ *
4
+ * @description
5
+ * 当群主需要中止某个正在进行的群聊会话时,前端通过 Socket 发送 abort 请求,
6
+ * 本模块负责接收并处理该请求。
7
+ *
8
+ * 职责:
9
+ * - 校验前端传入的 groupId / conversationId 是否合法;
10
+ * - 鉴权:验证请求用户是否为群 ownerIds 成员(仅群主可中止);
11
+ * - 调用 Orchestrator.abort() 级联中止整棵 run 树(包括所有子任务);
12
+ * - 通过 ReliableEmitter 以可靠投递方式回包(带 ACK 确认机制);
13
+ * - 幂等:重复 abort 同一 conversationId 不报错,返回 abortedRunIds: []。
14
+ *
15
+ * @module socket/group-abort
16
+ */
17
+ import { CHANNEL_KEY } from '../../config.js';
18
+ import { EVENT_GROUP_ABORT_RESPONSE } from '../../group/constants/index.js';
19
+ import { getModuleLogger } from '../../utils/logger.js';
20
+ /** 群消息中止模块日志器,模块标识为 'group.abort' */
21
+ const log = getModuleLogger('group.abort');
22
+ /**
23
+ * 统一发送群中止响应(成功/失败共用)。
24
+ *
25
+ * @description 构造标准的 abort 响应消息体,通过可靠发射器发送给请求方。
26
+ * 无论中止成功或失败,都通过此函数统一回包,保证响应格式一致。
27
+ *
28
+ * @param params - 回包所需的上下文参数(从请求参数中提取)
29
+ * @param abortedRunIds - 成功中止的 run ID 列表,失败时为空数组
30
+ * @param error - 可选的错误描述信息,存在时会附加到响应的 extra.error 字段
31
+ */
32
+ function emitAbortResponse(params, abortedRunIds, error) {
33
+ const { requestMsgId, botClientId, userId, groupId, conversationId, reliableEmitter } = params;
34
+ reliableEmitter.emitWithAck(EVENT_GROUP_ABORT_RESPONSE, {
35
+ msgId: requestMsgId,
36
+ from: botClientId,
37
+ to: userId,
38
+ groupId,
39
+ conversationId,
40
+ timestamp: Date.now(),
41
+ abortedRunIds,
42
+ ...(error ? { extra: { error } } : {}),
43
+ }, requestMsgId);
44
+ }
45
+ /**
46
+ * 处理「群消息中止」请求的核心函数。
47
+ *
48
+ * @description
49
+ * 完整处理流程如下:
50
+ * 1. **参数校验** — 确保 groupId 和 conversationId 均已提供;
51
+ * 2. **鉴权** — 从存储层读取群元数据,校验请求用户是否在 ownerIds 中;
52
+ * 3. **级联中止** — 调用 orchestrator.abort() 中止目标 conversationId 下的所有 run;
53
+ * 4. **回包** — 将中止结果通过可靠发射器返回给前端。
54
+ *
55
+ * @param params - 群消息中止请求的完整参数
56
+ * @returns Promise<void> 无返回值,结果通过 reliableEmitter 回包
57
+ */
58
+ export async function handleGroupAbort(params) {
59
+ const { userId, groupId, conversationId, orchestrator, storage } = params;
60
+ const logContext = `userId=${userId} groupId=${groupId} conversationId=${conversationId}`;
61
+ log.info(`[${CHANNEL_KEY}] 收到群消息中止请求: ${logContext}`);
62
+ /**
63
+ * 错误快捷回包辅助函数。
64
+ * 记录错误日志并向前端发送包含错误信息的空 abortedRunIds 响应。
65
+ *
66
+ * @param error - 错误描述文本
67
+ */
68
+ const sendError = (error) => {
69
+ log.error(`[${CHANNEL_KEY}] 群消息中止失败: ${logContext} 错误=${error}`);
70
+ emitAbortResponse(params, [], error);
71
+ };
72
+ // ① 参数校验
73
+ if (!groupId) {
74
+ sendError('缺少 groupId');
75
+ return;
76
+ }
77
+ if (!conversationId) {
78
+ sendError('缺少 conversationId');
79
+ return;
80
+ }
81
+ // ② 鉴权:读取群元数据,校验用户是否为群拥有者
82
+ // 只有 ownerIds 中的用户才有权限中止群会话,防止非授权用户恶意中止
83
+ try {
84
+ const meta = await storage.readGroupMeta(groupId);
85
+ if (!meta) {
86
+ sendError('群组不存在');
87
+ return;
88
+ }
89
+ if (!meta.ownerIds.includes(userId)) {
90
+ sendError('无操作权限');
91
+ return;
92
+ }
93
+ }
94
+ catch (err) {
95
+ // 存储层异常时记录完整错误信息,便于排查
96
+ sendError(`读取群元数据失败: ${err instanceof Error ? err.message : String(err)}`);
97
+ return;
98
+ }
99
+ // ③ 级联中止:中止该 conversationId 下所有正在运行的 run(包括子任务)
100
+ const abortedRunIds = orchestrator.abort(conversationId);
101
+ log.info(`[${CHANNEL_KEY}] 群消息中止成功: ${logContext} 已中止runIds=[${abortedRunIds.join(',')}]`);
102
+ // ④ 回包
103
+ emitAbortResponse(params, abortedRunIds);
104
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @file Socket 网关侧的群组历史消息(Group History)请求处理器。
3
+ *
4
+ * @description
5
+ * 当前端需要拉取群聊历史消息时,通过 Socket 发送 history 请求,
6
+ * 本模块负责接收并处理该请求,支持分页游标翻页。
7
+ *
8
+ * 职责:
9
+ * - 校验前端传入的分页参数(limit / before / chatOnly);
10
+ * - 调用 GroupHistoryService 拉取指定群组的历史消息;
11
+ * - 通过 ReliableEmitter 以可靠投递方式回包(带 ACK 确认机制);
12
+ * - 失败统一通过 sendError 输出错误响应与日志。
13
+ *
14
+ * @module socket/group-history
15
+ */
16
+ import { CHANNEL_KEY, DEFAULT_HISTORY_LIMIT } from '../../config.js';
17
+ import { EVENT_GROUP_HISTORY_RESPONSE } from '../../group/constants/index.js';
18
+ import { GroupHistoryService } from '../../group/services/index.js';
19
+ import { getModuleLogger } from '../../utils/logger.js';
20
+ /** 群组历史消息模块日志器,模块标识为 'group.history' */
21
+ const log = getModuleLogger('group.history');
22
+ /**
23
+ * 模块级共享的群组历史服务实例。
24
+ *
25
+ * @remarks
26
+ * - 采用单例模式,避免重复创建服务实例;
27
+ * - 同时导出供 group-member 模块引用,用于在成员变更时清除历史缓存。
28
+ */
29
+ export const historyService = new GroupHistoryService();
30
+ /**
31
+ * 统一发送群组历史响应(成功/失败共用)。
32
+ *
33
+ * @description 构造标准的 history 响应消息体,通过可靠发射器发送给请求方。
34
+ * 无论拉取成功或失败,都通过此函数统一回包,保证响应格式一致。
35
+ *
36
+ * @param params - 回包所需的上下文参数(从请求参数中 Pick)
37
+ * @param messages - 历史消息列表,失败时传空数组
38
+ * @param hasMore - 是否还有更早的消息可继续翻页
39
+ * @param options - 可选扩展参数
40
+ * @param options.nextCursor - 下一页游标(本批次最早消息的时间戳)
41
+ * @param options.error - 错误描述信息,存在时附加到 extra.error
42
+ */
43
+ function emitHistoryResponse(params, messages, hasMore, options) {
44
+ const { requestMsgId, botClientId, userId, groupId, reliableEmitter } = params;
45
+ // 构造响应 payload,使用展开运算符按需附加可选字段
46
+ reliableEmitter.emitWithAck(EVENT_GROUP_HISTORY_RESPONSE, {
47
+ msgId: requestMsgId,
48
+ from: botClientId,
49
+ to: userId,
50
+ groupId,
51
+ timestamp: Date.now(),
52
+ messages,
53
+ hasMore,
54
+ // 仅在有下一页游标时附加 nextCursor 字段
55
+ ...(options?.nextCursor != null ? { nextCursor: options.nextCursor } : {}),
56
+ // 仅在有错误时附加 extra.error 字段
57
+ ...(options?.error ? { extra: { error: options.error } } : {}),
58
+ },
59
+ // 使用 requestMsgId 作为 ACK 标识,实现幂等去重
60
+ requestMsgId);
61
+ }
62
+ /**
63
+ * 规范化 limit 参数。
64
+ *
65
+ * @description 校验规则:
66
+ * - 必须为 number 类型(排除 undefined)
67
+ * - 必须为有限数(排除 Infinity / NaN)
68
+ * - 必须为正数(排除 0 和负数)
69
+ * - 取整(排除小数,如 3.7 → 3)
70
+ * 不满足以上任一条件时,回退到 DEFAULT_HISTORY_LIMIT。
71
+ *
72
+ * @param raw - 前端传入的原始 limit 值
73
+ * @returns 合法的 limit 正整数
74
+ */
75
+ function normalizeLimit(raw) {
76
+ return typeof raw === 'number' && Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_HISTORY_LIMIT;
77
+ }
78
+ /**
79
+ * 规范化 before 游标。
80
+ *
81
+ * @description 校验规则与 normalizeLimit 类似:
82
+ * - 必须为有限正数
83
+ * - 不满足时返回 undefined,表示不指定游标(从最新消息开始拉取)
84
+ *
85
+ * @param raw - 前端传入的原始 before 时间戳
86
+ * @returns 合法的游标时间戳,或 undefined 表示取最新一页
87
+ */
88
+ function normalizeBefore(raw) {
89
+ return typeof raw === 'number' && Number.isFinite(raw) && raw > 0 ? raw : undefined;
90
+ }
91
+ /**
92
+ * 处理「群组历史消息」请求的核心函数。
93
+ *
94
+ * @description
95
+ * 完整处理流程如下:
96
+ * 1. **参数校验** — 确保 groupId 已提供;
97
+ * 2. **参数规范化** — 兜底 limit / before / chatOnly;
98
+ * 3. **拉取消息** — 调用 GroupHistoryService.getHistory;
99
+ * 4. **回包** — 将结果通过可靠发射器返回给前端。
100
+ *
101
+ * @param params - 群组历史消息请求的完整参数
102
+ * @returns Promise<void> 无返回值,结果通过 reliableEmitter 回包
103
+ */
104
+ export async function handleGroupHistory(params) {
105
+ const { userId, groupId } = params;
106
+ /**
107
+ * 错误快捷回包辅助闭包。
108
+ * 记录 error 级日志并向前端发送包含错误信息的空消息列表响应。
109
+ *
110
+ * @param error - 错误描述文本
111
+ */
112
+ const sendError = (error) => {
113
+ log.error(`[${CHANNEL_KEY}] 群组历史消息错误: groupId=${groupId} 错误=${error}`);
114
+ emitHistoryResponse(params, [], false, { error });
115
+ };
116
+ // ① 参数校验:groupId 为必填项,缺失时直接报错回包
117
+ if (!groupId) {
118
+ sendError('缺少 groupId');
119
+ return;
120
+ }
121
+ // ② 参数规范化:对前端传入的分页参数进行兜底处理
122
+ // 条数上限
123
+ const limit = normalizeLimit(params.limit);
124
+ // 翻页游标
125
+ const before = normalizeBefore(params.before);
126
+ // 是否仅聊天消息,默认 false
127
+ const chatOnly = params.chatOnly ?? false;
128
+ // ③ 拉取历史消息:调用 GroupHistoryService 获取分页数据
129
+ try {
130
+ const result = await historyService.getHistory({ groupId, userId, limit, before, chatOnly });
131
+ // 记录本次拉取的关键参数和结果摘要,便于线上排查
132
+ log.info(`[${CHANNEL_KEY}] 群组历史消息: groupId=${groupId} count=${result.messages.length}`);
133
+ // ④ 成功回包:携带消息列表、翻页状态和下一页游标
134
+ emitHistoryResponse(params, result.messages, result.hasMore, { nextCursor: result.nextCursor });
135
+ }
136
+ catch (err) {
137
+ // 服务层异常时提取错误信息并通过 sendError 统一回包
138
+ sendError(err instanceof Error ? err.message : String(err));
139
+ }
140
+ }
@@ -0,0 +1,209 @@
1
+ /**
2
+ * @file group-member.ts
3
+ * @description 群成员加入/退出请求的 Socket 网关处理器。
4
+ * 负责接收客户端的群成员变更请求,执行业务逻辑后通过可靠发射器回包。
5
+ */
6
+ import { CHANNEL_KEY } from '../../config.js';
7
+ import { EVENT_GROUP_MEMBER_RESPONSE } from '../../group/constants/index.js';
8
+ import { GroupMemberService, GroupUpdateService, GroupQueryService } from '../../group/services/index.js';
9
+ import { getLightclawRuntime } from '../../runtime.js';
10
+ import { historyService } from './group-history.js';
11
+ import { getModuleLogger } from '../../utils/logger.js';
12
+ const log = getModuleLogger('group.member');
13
+ /** 群成员服务实例,用于执行成员增删操作 */
14
+ const memberService = new GroupMemberService();
15
+ /** 群查询服务实例,用于管理查询缓存 */
16
+ const queryService = new GroupQueryService();
17
+ /**
18
+ * 构建群成员响应的基础载荷
19
+ * @description 统一构造响应数据结构,供成功和错误场景复用
20
+ * @param ctx - 请求上下文参数
21
+ * @param type - 操作类型(add/remove)
22
+ * @param members - 当前群成员列表
23
+ * @param extra - 附加信息(如错误详情)
24
+ * @returns 符合 EVENT_GROUP_MEMBER_RESPONSE 事件格式的载荷对象
25
+ */
26
+ function buildResponsePayload(ctx, type, members, extra) {
27
+ const { requestMsgId, botClientId, userId, groupId, agentId } = ctx;
28
+ return {
29
+ msgId: requestMsgId,
30
+ from: botClientId,
31
+ to: userId,
32
+ type,
33
+ timestamp: Date.now(),
34
+ groupId,
35
+ agentId,
36
+ members,
37
+ ...(extra && { extra }),
38
+ };
39
+ }
40
+ /**
41
+ * 创建错误回包发送函数(柯里化)
42
+ * @description 返回一个闭包函数,调用时自动记录错误日志并向客户端发送错误响应
43
+ * @param ctx - 包含操作类型的请求上下文
44
+ * @returns 接收错误信息字符串的发送函数
45
+ */
46
+ function createGroupMemberErrorSender(ctx) {
47
+ const { type, reliableEmitter, requestMsgId } = ctx;
48
+ return (error) => {
49
+ log.error(`[${CHANNEL_KEY}] 群成员${type}错误: ${error}`);
50
+ const payload = buildResponsePayload(ctx, type, [], { error });
51
+ reliableEmitter.emitWithAck(EVENT_GROUP_MEMBER_RESPONSE, payload, requestMsgId);
52
+ };
53
+ }
54
+ /**
55
+ * 发送成功响应
56
+ * @description 将操作成功后的群成员列表通过可靠发射器回包给客户端
57
+ * @param ctx - 请求上下文参数
58
+ * @param type - 操作类型(add/remove)
59
+ * @param members - 操作后的最新群成员列表
60
+ */
61
+ function emitGroupMemberResponse(ctx, type, members) {
62
+ const { reliableEmitter, requestMsgId } = ctx;
63
+ const payload = buildResponsePayload(ctx, type, members);
64
+ reliableEmitter.emitWithAck(EVENT_GROUP_MEMBER_RESPONSE, payload, requestMsgId);
65
+ }
66
+ /**
67
+ * 构建 agentId 校验器与元信息解析器
68
+ * @description 从运行时配置中加载 Agent 列表,生成校验和元信息查询工具。
69
+ * 优先使用 extra 中传入的 agentName/agentDesc,否则从配置列表中按优先级解析。
70
+ * @param extra - 外部传入的 Agent 名称和描述(优先级高于配置)
71
+ * @returns 包含 isValidAgentId 校验函数和 resolveAgentMeta 元信息解析函数的对象
72
+ */
73
+ function buildAgentRuntimeHelpers(extra) {
74
+ const pluginRuntime = getLightclawRuntime();
75
+ const currentCfg = pluginRuntime.config.loadConfig();
76
+ const list = currentCfg.agents?.list ?? [];
77
+ // 若配置列表为空,则使用默认 Agent ID 作为唯一合法值
78
+ const validIds = list.length > 0 ? list.map((a) => a.id) : [];
79
+ const validSet = new Set(validIds);
80
+ // 构建 agentId -> 元信息 的映射表,按优先级解析名称和描述
81
+ const metaMap = new Map();
82
+ for (const a of list) {
83
+ metaMap.set(a.id, {
84
+ agentName: a.displayName ?? a.identity?.name ?? a.name ?? a.id,
85
+ agentDesc: a.description ?? a.identity?.description ?? a.desc ?? a.routing ?? '',
86
+ });
87
+ }
88
+ return {
89
+ isValidAgentId: (agentId) => validSet.has(agentId),
90
+ resolveAgentMeta: (agentId) => {
91
+ // 若 extra 中提供了有效的 agentName 且不等于 agentId 本身,优先使用 extra
92
+ if (extra?.agentName && extra.agentName !== agentId) {
93
+ return { agentName: extra.agentName, agentDesc: extra.agentDesc ?? '' };
94
+ }
95
+ return metaMap.get(agentId);
96
+ },
97
+ };
98
+ }
99
+ /**
100
+ * 校验必填参数
101
+ * @description 类型守卫函数,校验 groupId 和 agentId 是否存在,不存在则发送错误响应
102
+ * @param groupId - 待校验的群组 ID
103
+ * @param agentId - 待校验的 Agent ID
104
+ * @param sendError - 错误发送函数
105
+ * @returns 校验通过返回 true(同时收窄 groupId 类型为 string)
106
+ */
107
+ function validateRequiredParams(groupId, agentId, sendError) {
108
+ if (!groupId) {
109
+ sendError('缺少 groupId');
110
+ return false;
111
+ }
112
+ if (!agentId) {
113
+ sendError('缺少 agentId');
114
+ return false;
115
+ }
116
+ return true;
117
+ }
118
+ /**
119
+ * 成员变更后的缓存清理
120
+ * @description 清除群查询缓存和历史消息阅读者缓存,确保后续查询获取最新数据
121
+ */
122
+ function invalidateCaches() {
123
+ queryService.clearCache();
124
+ historyService.clearReaderCache();
125
+ }
126
+ /**
127
+ * 处理「加入群」请求
128
+ * @description 校验参数后调用 memberService 添加成员,成功后清除缓存并回包
129
+ * @param params - 群成员操作入参
130
+ */
131
+ export async function handleGroupMemberAdd(params) {
132
+ const { userId, groupId, agentId } = params;
133
+ const sendError = createGroupMemberErrorSender({ ...params, type: 'add' });
134
+ if (!validateRequiredParams(groupId, agentId, sendError))
135
+ return;
136
+ try {
137
+ const { isValidAgentId, resolveAgentMeta } = buildAgentRuntimeHelpers(params.extra);
138
+ const result = await memberService.addMember(groupId, userId, agentId, {
139
+ isValidAgentId,
140
+ resolveAgentMeta,
141
+ });
142
+ invalidateCaches();
143
+ log.info(`[${CHANNEL_KEY}] 群成员添加: userId=${userId} groupId=${groupId} agentId=${agentId} members=${result.members.length}`);
144
+ emitGroupMemberResponse(params, 'add', result.members);
145
+ }
146
+ catch (err) {
147
+ sendError(err instanceof Error ? err.message : String(err));
148
+ }
149
+ }
150
+ /**
151
+ * 处理「退出群」请求
152
+ * @description 校验参数后调用 memberService 移除成员,成功后清除缓存并回包
153
+ * @param params - 群成员操作入参
154
+ */
155
+ export async function handleGroupMemberRemove(params) {
156
+ const { userId, groupId, agentId } = params;
157
+ const sendError = createGroupMemberErrorSender({ ...params, type: 'remove' });
158
+ if (!validateRequiredParams(groupId, agentId, sendError))
159
+ return;
160
+ try {
161
+ const result = await memberService.removeMember(groupId, userId, agentId);
162
+ invalidateCaches();
163
+ log.info(`[${CHANNEL_KEY}] 群成员移除: userId=${userId} groupId=${groupId} agentId=${agentId} members=${result.members.length}`);
164
+ emitGroupMemberResponse(params, 'remove', result.members);
165
+ }
166
+ catch (err) {
167
+ sendError(err instanceof Error ? err.message : String(err));
168
+ }
169
+ }
170
+ /**
171
+ * 处理「全量刷新成员元信息」请求
172
+ * @description 从客户端传入的 extra.agentMetaMap 中获取所有 Agent 的最新 agentName / agentDesc,
173
+ * 调用 GroupUpdateService.syncMembersMeta 全量刷新所有群组中的成员信息。
174
+ * 注意:此操作不需要 groupId 和 agentId,因为是全量刷新所有群组。
175
+ * @param params - 群成员操作入参
176
+ */
177
+ export async function handleGroupMemberUpdate(params) {
178
+ const { userId, reliableEmitter, requestMsgId, botClientId } = params;
179
+ const sendError = createGroupMemberErrorSender({ ...params, type: 'update' });
180
+ try {
181
+ // 从客户端传入的 extra 中获取 agentMetaMap
182
+ const agentMetaMap = params.extra?.agentMetaMap;
183
+ if (!agentMetaMap || Object.keys(agentMetaMap).length === 0) {
184
+ sendError('缺少 extra.agentMetaMap 或映射表为空');
185
+ return;
186
+ }
187
+ // 调用 syncMembersMeta 全量刷新
188
+ const updateService = new GroupUpdateService();
189
+ const result = await updateService.syncMembersMeta(agentMetaMap);
190
+ invalidateCaches();
191
+ log.info(`[${CHANNEL_KEY}] 群成员元信息同步: userId=${userId} scanned=${result.scannedCount} updated=${result.updatedGroupIds.length}`);
192
+ // 回包:返回被更新的群组 ID 列表
193
+ reliableEmitter.emitWithAck(EVENT_GROUP_MEMBER_RESPONSE, {
194
+ msgId: requestMsgId,
195
+ from: botClientId,
196
+ to: userId,
197
+ type: 'update',
198
+ timestamp: Date.now(),
199
+ members: [],
200
+ extra: {
201
+ updatedGroupIds: result.updatedGroupIds,
202
+ scannedCount: result.scannedCount,
203
+ },
204
+ }, requestMsgId);
205
+ }
206
+ catch (err) {
207
+ sendError(err instanceof Error ? err.message : String(err));
208
+ }
209
+ }