lightclawbot 1.2.6 → 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 (99) 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 -568
  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/agent-soul.js +0 -41
  99. package/dist/src/socket/chat.js +0 -257
@@ -0,0 +1,121 @@
1
+ /**
2
+ * 文件下载处理器(kind=file:download, status=download_req)
3
+ *
4
+ * 流程:
5
+ * 1. 从 data.extra.transferData 解析 transferId / localPath
6
+ * 2. 基础校验:必须是绝对路径,文件必须存在
7
+ * 3. 先回告 download_ready 帧(transferId + 文件元数据),前端据此更新下载按钮状态
8
+ * 4. 通过 uploadFileToServer 将本机文件上传到 ai-server,拿到下载 URL
9
+ * 5. 回告 download_url 帧(transferId + url),前端触发浏览器原生下载
10
+ * 6. 任一阶段失败 → 回告 download_error 帧(transferId + error message)
11
+ */
12
+ import { CHANNEL_KEY, EVENT_MESSAGE_PRIVATE, KIND_FILE_DOWNLOAD, FILE_DOWNLOAD_STATUS, } from '../../config.js';
13
+ import { generateMsgId } from '../../dedup.js';
14
+ import { uploadFileToServer } from '../../file-storage.js';
15
+ import { guessMimeByExt } from '../../media.js';
16
+ import { extractChatId } from '../utils/message.js';
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+ import { getModuleLogger } from '../../utils/logger.js';
20
+ /** 模块级日志器 */
21
+ const log = getModuleLogger('socket.file-download');
22
+ /**
23
+ * 处理文件下载请求
24
+ */
25
+ export async function handleFileDownloadReq(data, botClientId, reliableEmitter, account) {
26
+ const transferData = data.extra?.transferData;
27
+ const transferId = transferData?.transferId;
28
+ const localPath = transferData?.localPath;
29
+ log.info(`[${CHANNEL_KEY}] file:download(req) received: transferId=${transferId}, localPath=${localPath}, from=${data.from}`);
30
+ const sendError = (error) => {
31
+ const msgId = generateMsgId();
32
+ reliableEmitter.emitWithAck(EVENT_MESSAGE_PRIVATE, {
33
+ msgId,
34
+ from: botClientId,
35
+ to: data.from,
36
+ content: '',
37
+ timestamp: Date.now(),
38
+ kind: KIND_FILE_DOWNLOAD,
39
+ extra: {
40
+ chatId: extractChatId(data),
41
+ transferData: { transferId, status: FILE_DOWNLOAD_STATUS.ERROR, error },
42
+ },
43
+ }, msgId);
44
+ log.error(`[${CHANNEL_KEY}] file:download(error) sent: transferId=${transferId}, error=${error}`);
45
+ };
46
+ if (!transferId || !localPath) {
47
+ sendError('Missing transferId or localPath in extra.transferData');
48
+ return;
49
+ }
50
+ // 基础校验:必须是绝对路径
51
+ if (!path.isAbsolute(localPath)) {
52
+ sendError(`localPath must be an absolute path: ${localPath}`);
53
+ return;
54
+ }
55
+ const resolvedPath = path.resolve(localPath);
56
+ if (!fs.existsSync(resolvedPath)) {
57
+ sendError(`File not found: ${resolvedPath}`);
58
+ return;
59
+ }
60
+ const stat = fs.statSync(resolvedPath);
61
+ if (!stat.isFile()) {
62
+ sendError(`Not a regular file: ${resolvedPath}`);
63
+ return;
64
+ }
65
+ const fileName = path.basename(resolvedPath);
66
+ const mimeType = guessMimeByExt(path.extname(fileName).toLowerCase()) || 'application/octet-stream';
67
+ // 回告 download_ready(文件确认存在 + 元数据)
68
+ const readyMsgId = generateMsgId();
69
+ reliableEmitter.emitWithAck(EVENT_MESSAGE_PRIVATE, {
70
+ msgId: readyMsgId,
71
+ from: botClientId,
72
+ to: data.from,
73
+ content: '',
74
+ timestamp: Date.now(),
75
+ kind: KIND_FILE_DOWNLOAD,
76
+ extra: {
77
+ chatId: extractChatId(data),
78
+ transferData: {
79
+ transferId,
80
+ status: FILE_DOWNLOAD_STATUS.READY,
81
+ name: fileName,
82
+ size: stat.size,
83
+ contentType: mimeType,
84
+ },
85
+ },
86
+ }, readyMsgId);
87
+ log.info(`[${CHANNEL_KEY}] file:download(ready) sent: transferId=${transferId}, name=${fileName}, size=${stat.size}`);
88
+ // 上传本机文件到 ai-server(含审核),成功后把 URL 回告前端
89
+ try {
90
+ const apiKey = account.apiKey;
91
+ const uploadResult = await uploadFileToServer(resolvedPath, { apiKey });
92
+ if (!uploadResult.isUploaded || !uploadResult.url) {
93
+ sendError('Upload to ai-server failed');
94
+ return;
95
+ }
96
+ const urlMsgId = generateMsgId();
97
+ reliableEmitter.emitWithAck(EVENT_MESSAGE_PRIVATE, {
98
+ msgId: urlMsgId,
99
+ from: botClientId,
100
+ to: data.from,
101
+ content: '',
102
+ timestamp: Date.now(),
103
+ kind: KIND_FILE_DOWNLOAD,
104
+ extra: {
105
+ chatId: extractChatId(data),
106
+ transferData: {
107
+ transferId,
108
+ status: FILE_DOWNLOAD_STATUS.URL,
109
+ url: uploadResult.url,
110
+ name: fileName,
111
+ size: stat.size,
112
+ contentType: mimeType,
113
+ },
114
+ },
115
+ }, urlMsgId);
116
+ log.info(`[${CHANNEL_KEY}] file:download(url) sent: transferId=${transferId}, url=${uploadResult.url}`);
117
+ }
118
+ catch (err) {
119
+ sendError(err instanceof Error ? err.message : String(err));
120
+ }
121
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @file group-abort.ts
3
+ * @description 群消息中止请求处理器
4
+ *
5
+ * 监听 group:abort:request 事件,对请求进行必填字段校验、
6
+ * 自身消息过滤、去重判断后,委托给 handleGroupAbort 执行中止逻辑。
7
+ */
8
+ import { CHANNEL_KEY } from '../../config.js';
9
+ import { isDuplicate } from '../../dedup.js';
10
+ import { handleGroupAbort } from '../service/group-abort.js';
11
+ import { EVENT_GROUP_ABORT_REQUEST } from '../../group/constants/index.js';
12
+ import { getModuleLogger } from '../../utils/logger.js';
13
+ import { hasRequiredFields, getMissingFields } from '../utils/validate.js';
14
+ /** 模块级日志器 */
15
+ const log = getModuleLogger('socket.group-abort-handler');
16
+ /** 中止请求的必填字段列表 */
17
+ const REQUIRED_FIELDS = ['from', 'msgId', 'groupId', 'conversationId'];
18
+ /**
19
+ * 注册 group:abort:request 事件监听器
20
+ *
21
+ * 处理流程:确认消息 → 校验必填字段 → 过滤自身消息 → 去重 → 委托 service 处理
22
+ *
23
+ * @param socket - 原生 Socket 实例
24
+ * @param deps - 依赖项(botClientId、reliableEmitter、orchestrator 等)
25
+ */
26
+ export function bindGroupAbortHandler(socket, deps) {
27
+ const { botClientId, reliableEmitter, onEvent, orchestrator, groupStorage } = deps;
28
+ socket.on(EVENT_GROUP_ABORT_REQUEST, (data, ack) => {
29
+ // 立即确认收到消息
30
+ ack?.();
31
+ // 校验必填字段,明确记录缺失项
32
+ if (!hasRequiredFields(data, REQUIRED_FIELDS)) {
33
+ const missing = getMissingFields(data, REQUIRED_FIELDS);
34
+ log.warn(`[${CHANNEL_KEY}] 中止群消息请求缺少必填字段: [${missing.join(', ')}],已忽略`);
35
+ return;
36
+ }
37
+ // 忽略自身发出的请求,防止回环
38
+ if (data.from === botClientId)
39
+ return;
40
+ // 去重:同一 msgId 的请求只处理一次
41
+ if (isDuplicate(`group:abort:${data.msgId}`)) {
42
+ log.warn(`[${CHANNEL_KEY}] 重复的群中止请求 (msgId=${data.msgId}),已忽略`);
43
+ return;
44
+ }
45
+ log.info(`[${CHANNEL_KEY}] 收到中止群消息请求: from=${data.from}, groupId=${data.groupId}, conversationId=${data.conversationId}`);
46
+ // 触发外部事件回调(如心跳续期)
47
+ onEvent?.();
48
+ void handleGroupAbort({
49
+ userId: data.from,
50
+ botClientId,
51
+ reliableEmitter,
52
+ requestMsgId: data.msgId,
53
+ groupId: data.groupId,
54
+ conversationId: data.conversationId,
55
+ orchestrator,
56
+ storage: groupStorage,
57
+ });
58
+ });
59
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @file group-history.ts
3
+ * @description 群历史消息请求处理器
4
+ *
5
+ * 监听 group:history:request 事件,对请求进行必填字段校验(并报告缺失字段)、
6
+ * 自身消息过滤、去重判断后,委托给 handleGroupHistory 查询并返回历史消息。
7
+ */
8
+ import { CHANNEL_KEY } from '../../config.js';
9
+ import { isDuplicate } from '../../dedup.js';
10
+ import { handleGroupHistory } from '../service/group-history.js';
11
+ import { EVENT_GROUP_HISTORY_REQUEST } from '../../group/constants/index.js';
12
+ import { getModuleLogger } from '../../utils/logger.js';
13
+ import { hasRequiredFields, getMissingFields } from '../utils/validate.js';
14
+ /** 模块级日志器 */
15
+ const log = getModuleLogger('socket.group-history-handler');
16
+ /** 群历史消息请求的必填字段列表 */
17
+ const REQUIRED_FIELDS = ['from', 'msgId', 'groupId'];
18
+ /**
19
+ * 注册 group:history:request 事件监听器
20
+ *
21
+ * 处理流程:确认消息 → 校验必填字段(报告缺失详情) → 过滤自身消息 → 去重 → 委托 service 查询历史
22
+ *
23
+ * @param socket - 原生 Socket 实例
24
+ * @param deps - 依赖项(botClientId、reliableEmitter、onEvent 回调)
25
+ */
26
+ export function bindGroupHistoryHandler(socket, deps) {
27
+ const { botClientId, reliableEmitter, onEvent } = deps;
28
+ socket.on(EVENT_GROUP_HISTORY_REQUEST, (data, ack) => {
29
+ // 立即确认收到消息
30
+ ack?.();
31
+ // 校验必填字段,缺失时输出具体字段名称便于排查
32
+ if (!hasRequiredFields(data, REQUIRED_FIELDS)) {
33
+ const missing = getMissingFields(data, REQUIRED_FIELDS);
34
+ log.warn(`[${CHANNEL_KEY}] 群历史消息请求缺少必填字段: [${missing.join(', ')}],已忽略`);
35
+ return;
36
+ }
37
+ // 忽略自身发出的请求,防止回环
38
+ if (data.from === botClientId)
39
+ return;
40
+ // 去重:同一 msgId 的请求只处理一次
41
+ if (isDuplicate(`group:history:${data.msgId}`)) {
42
+ log.warn(`[${CHANNEL_KEY}] 重复的群历史消息请求 (msgId=${data.msgId}),已忽略`);
43
+ return;
44
+ }
45
+ log.info(`[${CHANNEL_KEY}] 收到群历史消息请求: from=${data.from}, groupId=${data.groupId}, limit=${data.limit ?? '-'}, before=${data.before ?? '-'}, chatOnly=${data.chatOnly ?? '-'}`);
46
+ // 触发外部事件回调(如心跳续期)
47
+ onEvent?.();
48
+ void handleGroupHistory({
49
+ userId: data.from,
50
+ botClientId,
51
+ reliableEmitter,
52
+ requestMsgId: data.msgId,
53
+ groupId: data.groupId,
54
+ limit: data.limit,
55
+ before: data.before,
56
+ chatOnly: data.chatOnly,
57
+ });
58
+ });
59
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @file group-member.ts
3
+ * @description 群成员管理请求处理器(group:member:request)
4
+ *
5
+ * 监听 group:member:request 事件,对请求进行必填字段校验(并报告缺失字段)、
6
+ * 自身消息过滤、去重判断后,根据 type 分发到 add/remove handler。
7
+ */
8
+ import { CHANNEL_KEY } from '../../config.js';
9
+ import { isDuplicate } from '../../dedup.js';
10
+ import { handleGroupMemberAdd, handleGroupMemberRemove, handleGroupMemberUpdate } from '../service/group-member.js';
11
+ import { EVENT_GROUP_MEMBER_REQUEST } from '../../group/constants/index.js';
12
+ import { getModuleLogger } from '../../utils/logger.js';
13
+ import { hasRequiredFields, getMissingFields } from '../utils/validate.js';
14
+ /** 模块级日志器 */
15
+ const log = getModuleLogger('socket.group-member-handler');
16
+ /** 基础必填字段列表(所有操作类型共用) */
17
+ const BASE_REQUIRED_FIELDS = ['from', 'msgId', 'type'];
18
+ /** add/remove 操作额外必填字段 */
19
+ const MEMBER_OP_REQUIRED_FIELDS = ['from', 'msgId', 'type', 'groupId', 'agentId'];
20
+ /**
21
+ * 根据成员操作类型分发到对应的 service handler
22
+ * @param data - 请求数据
23
+ * @param botClientId - Bot 客户端 ID
24
+ * @param reliableEmitter - 可靠消息发射器
25
+ */
26
+ function dispatchMemberHandler(data, botClientId, reliableEmitter) {
27
+ const baseCtx = {
28
+ userId: data.from,
29
+ botClientId,
30
+ reliableEmitter,
31
+ requestMsgId: data.msgId,
32
+ groupId: data.groupId,
33
+ agentId: data.agentId,
34
+ extra: data.extra,
35
+ };
36
+ switch (data.type) {
37
+ case 'add':
38
+ void handleGroupMemberAdd(baseCtx);
39
+ break;
40
+ case 'remove':
41
+ void handleGroupMemberRemove(baseCtx);
42
+ break;
43
+ case 'update':
44
+ void handleGroupMemberUpdate(baseCtx);
45
+ break;
46
+ default:
47
+ log.warn(`[${CHANNEL_KEY}] 未知的群成员操作类型: ${data.type}`);
48
+ }
49
+ }
50
+ /**
51
+ * 注册 group:member:request 事件监听器
52
+ *
53
+ * 处理流程:确认消息 → 校验必填字段(报告缺失详情) → 过滤自身消息 → 去重 → 分发到对应 handler
54
+ *
55
+ * @param socket - 原生 Socket 实例
56
+ * @param deps - 依赖项(botClientId、reliableEmitter、onEvent 回调)
57
+ */
58
+ export function bindGroupMemberHandler(socket, deps) {
59
+ const { botClientId, reliableEmitter, onEvent } = deps;
60
+ socket.on(EVENT_GROUP_MEMBER_REQUEST, (data, ack) => {
61
+ // 立即确认收到消息
62
+ ack?.();
63
+ // 校验必填字段,update 类型不需要 groupId 和 agentId
64
+ const requiredFields = data.type === 'update' ? BASE_REQUIRED_FIELDS : MEMBER_OP_REQUIRED_FIELDS;
65
+ if (!hasRequiredFields(data, requiredFields)) {
66
+ const missing = getMissingFields(data, requiredFields);
67
+ log.warn(`[${CHANNEL_KEY}] 群成员请求缺少必填字段: [${missing.join(', ')}],已忽略`);
68
+ return;
69
+ }
70
+ // 忽略自身发出的请求,防止回环
71
+ if (data.from === botClientId)
72
+ return;
73
+ // 去重:同一 msgId 的请求只处理一次
74
+ if (isDuplicate(`group:member:${data.msgId}`)) {
75
+ log.warn(`[${CHANNEL_KEY}] 重复的群成员请求 (msgId=${data.msgId}),已忽略`);
76
+ return;
77
+ }
78
+ log.info(`[${CHANNEL_KEY}] 收到群成员请求: from=${data.from}, type=${data.type}, groupId=${data.groupId}, agentId=${data.agentId}`);
79
+ // 触发外部事件回调(如心跳续期)
80
+ onEvent?.();
81
+ dispatchMemberHandler(data, botClientId, reliableEmitter);
82
+ });
83
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @file group-request.ts
3
+ * @description 群组管理请求处理器(group:request)
4
+ *
5
+ * 监听 group:request 事件,对请求进行必填字段校验(并报告缺失字段)、
6
+ * 自身消息过滤、去重判断后,根据 type 分发到 list/create/update/delete/clear handler。
7
+ */
8
+ import { CHANNEL_KEY } from '../../config.js';
9
+ import { isDuplicate } from '../../dedup.js';
10
+ import { handleGroupList, handleGroupCreate, handleGroupUpdate, handleGroupDelete, handleGroupClear, } from '../service/group.js';
11
+ import { EVENT_GROUP_REQUEST } from '../../group/constants/index.js';
12
+ import { getModuleLogger } from '../../utils/logger.js';
13
+ import { hasRequiredFields, getMissingFields } from '../utils/validate.js';
14
+ /** 模块级日志器 */
15
+ const log = getModuleLogger('socket.group-request');
16
+ /** 必填字段列表 */
17
+ const REQUIRED_FIELDS = ['from', 'msgId', 'type'];
18
+ /**
19
+ * 根据群组操作类型分发到对应的 service handler
20
+ * @param data - 请求数据
21
+ * @param botClientId - Bot 客户端 ID
22
+ * @param reliableEmitter - 可靠消息发射器
23
+ */
24
+ function dispatchGroupHandler(data, botClientId, reliableEmitter, orchestrator) {
25
+ const baseCtx = { userId: data.from, botClientId, reliableEmitter, requestMsgId: data.msgId };
26
+ switch (data.type) {
27
+ case 'list':
28
+ void handleGroupList(baseCtx);
29
+ break;
30
+ case 'create':
31
+ void handleGroupCreate({
32
+ ...baseCtx,
33
+ name: data.extra?.name,
34
+ desc: data.extra?.desc,
35
+ members: data.extra?.members,
36
+ defaultAgentId: data.extra?.defaultAgentId,
37
+ orchestrator,
38
+ });
39
+ break;
40
+ case 'update':
41
+ void handleGroupUpdate({
42
+ ...baseCtx,
43
+ groupId: data.groupId,
44
+ name: data.extra?.name,
45
+ desc: data.extra?.desc,
46
+ pinned: data.extra?.pinned,
47
+ });
48
+ break;
49
+ case 'delete':
50
+ void handleGroupDelete({ ...baseCtx, groupId: data.groupId });
51
+ break;
52
+ case 'clear':
53
+ void handleGroupClear({ ...baseCtx, groupId: data.groupId });
54
+ break;
55
+ default:
56
+ log.warn(`[${CHANNEL_KEY}] 未知的群组操作类型: ${data.type}`);
57
+ }
58
+ }
59
+ /**
60
+ * 注册 group:request 事件监听器
61
+ *
62
+ * 处理流程:确认消息 → 校验必填字段(报告缺失详情) → 过滤自身消息 → 去重 → 分发到对应 handler
63
+ *
64
+ * @param socket - 原生 Socket 实例
65
+ * @param deps - 依赖项(botClientId、reliableEmitter、onEvent 回调)
66
+ */
67
+ export function bindGroupRequestHandler(socket, deps) {
68
+ const { botClientId, reliableEmitter, onEvent, orchestrator } = deps;
69
+ socket.on(EVENT_GROUP_REQUEST, (data, ack) => {
70
+ // 立即确认收到消息
71
+ ack?.();
72
+ // 校验必填字段,缺失时输出具体字段名称便于排查
73
+ if (!hasRequiredFields(data, REQUIRED_FIELDS)) {
74
+ const missing = getMissingFields(data, REQUIRED_FIELDS);
75
+ log.warn(`[${CHANNEL_KEY}] 群组请求缺少必填字段: [${missing.join(', ')}],已忽略`);
76
+ return;
77
+ }
78
+ // 忽略自身发出的请求,防止回环
79
+ if (data.from === botClientId)
80
+ return;
81
+ // 去重:同一 msgId 的请求只处理一次
82
+ if (isDuplicate(`group:${data.msgId}`)) {
83
+ log.warn(`[${CHANNEL_KEY}] 重复的群组请求 (msgId=${data.msgId}),已忽略`);
84
+ return;
85
+ }
86
+ log.info(`[${CHANNEL_KEY}] 收到群组请求: from=${data.from}, type=${data.type}, groupId=${data.groupId ?? '-'}`);
87
+ // 触发外部事件回调(如心跳续期)
88
+ onEvent?.();
89
+ dispatchGroupHandler(data, botClientId, reliableEmitter, orchestrator);
90
+ });
91
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @file history-request.ts
3
+ * @description 私聊历史消息请求处理器(history:request)
4
+ *
5
+ * 监听 history:request 事件,对请求进行必填字段校验、
6
+ * 自身消息过滤、去重与防抖后,委托给 queryPrivateHistory 执行查询,
7
+ * 并通过 reliableEmitter 回包(成功/失败统一格式)。
8
+ */
9
+ import { CHANNEL_KEY, EVENT_HISTORY_REQUEST, EVENT_HISTORY_RESPONSE, DEFAULT_AGENT_ID } from '../../config.js';
10
+ import { isDuplicate, debounceHistoryRequest, generateMsgId } from '../../dedup.js';
11
+ import { queryPrivateHistory } from '../service/history.js';
12
+ import { getModuleLogger } from '../../utils/logger.js';
13
+ /** 模块级日志器 */
14
+ const log = getModuleLogger('socket.history-request');
15
+ /**
16
+ * 发送历史消息查询成功响应
17
+ * @param reliableEmitter - 可靠消息发射器
18
+ * @param botClientId - Bot 客户端 ID
19
+ * @param userId - 请求方用户 ID
20
+ * @param result - 查询结果
21
+ */
22
+ function emitSuccessResponse(reliableEmitter, botClientId, userId, result) {
23
+ const msgId = generateMsgId();
24
+ const payload = {
25
+ msgId,
26
+ from: botClientId,
27
+ to: userId,
28
+ sessionKey: result.sessionKey,
29
+ messages: result.messages,
30
+ agentId: result.agentId,
31
+ };
32
+ reliableEmitter.emitWithAck(EVENT_HISTORY_RESPONSE, payload, msgId);
33
+ }
34
+ /**
35
+ * 发送历史消息查询失败响应
36
+ * @param reliableEmitter - 可靠消息发射器
37
+ * @param botClientId - Bot 客户端 ID
38
+ * @param userId - 请求方用户 ID
39
+ * @param agentId - 请求中的 agentId(兜底为默认值)
40
+ * @param err - 错误对象
41
+ */
42
+ function emitErrorResponse(reliableEmitter, botClientId, userId, agentId, err) {
43
+ const msgId = generateMsgId();
44
+ const payload = {
45
+ msgId,
46
+ from: botClientId,
47
+ to: userId,
48
+ sessionKey: '',
49
+ messages: [],
50
+ agentId,
51
+ error: err instanceof Error ? err.message : String(err),
52
+ };
53
+ reliableEmitter.emitWithAck(EVENT_HISTORY_RESPONSE, payload, msgId);
54
+ }
55
+ /**
56
+ * 注册 history:request 事件监听器
57
+ *
58
+ * 处理流程:确认消息 → 校验 from 字段 → 过滤自身消息 → 去重 → 防抖 → 查询 → 回包
59
+ *
60
+ * @param socket - 原生 Socket 实例
61
+ * @param deps - 依赖项
62
+ */
63
+ export function bindHistoryRequestHandler(socket, deps) {
64
+ const { account, botClientId, reliableEmitter, onEvent } = deps;
65
+ socket.on(EVENT_HISTORY_REQUEST, (data, ack) => {
66
+ // 立即确认收到消息
67
+ ack?.();
68
+ // 校验必填字段
69
+ if (!data?.from) {
70
+ log.warn(`[${CHANNEL_KEY}] 历史消息请求缺少 from 字段,已忽略`);
71
+ return;
72
+ }
73
+ // 忽略自身发出的请求,防止回环
74
+ if (data.from === botClientId)
75
+ return;
76
+ // 去重:同一 msgId 的请求只处理一次
77
+ if (data.msgId && isDuplicate(`history:${data.msgId}`)) {
78
+ log.warn(`[${CHANNEL_KEY}] 重复的历史消息请求 (msgId=${data.msgId}),已忽略`);
79
+ return;
80
+ }
81
+ // 防抖:同一用户高频请求只处理最后一条
82
+ debounceHistoryRequest(data.from, () => {
83
+ // 触发外部事件回调(如心跳续期)
84
+ onEvent?.();
85
+ try {
86
+ const result = queryPrivateHistory(data, account);
87
+ emitSuccessResponse(reliableEmitter, botClientId, data.from, result);
88
+ }
89
+ catch (err) {
90
+ log.error(`[${CHANNEL_KEY}] 历史消息查询失败: ${err instanceof Error ? err.message : String(err)}`);
91
+ emitErrorResponse(reliableEmitter, botClientId, data.from, data.agentId || DEFAULT_AGENT_ID, err);
92
+ }
93
+ });
94
+ });
95
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @file index.ts
3
+ * @description Socket 事件处理器子模块统一导出入口
4
+ *
5
+ * 本模块作为 socket/events 目录的桶文件(barrel),集中导出:
6
+ * - 类型定义:各事件处理器所需的依赖接口和请求数据类型
7
+ * - 工具函数:消息判断、会话 ID 提取、历史记录等通用工具
8
+ * - 事件绑定器:各类 Socket 事件的监听注册函数
9
+ */
10
+ /**
11
+ * 工具函数导出 —— 消息处理相关的通用工具
12
+ *
13
+ * - isGroupMessage: 判断消息是否为群消息
14
+ * - extractChatId: 从消息中提取会话 ID
15
+ * - recordSessionInHistory: 将当前会话记录到历史存储
16
+ */
17
+ export { isGroupMessage, extractChatId, recordSessionInHistory } from '../utils/message.js';
18
+ /**
19
+ * 事件绑定器导出 —— 各 Socket 事件的监听注册函数
20
+ *
21
+ * - bindAgentsRequestHandler: 注册 agents 列表请求事件监听
22
+ * - handleFileDownloadReq: 处理文件下载请求
23
+ * - bindMessagePrivateHandler: 注册私聊消息事件监听
24
+ * - bindHistoryRequestHandler: 注册私聊历史消息请求事件监听
25
+ * - bindChatRequestHandler: 注册聊天请求事件监听
26
+ * - bindGroupRequestHandler: 注册群聊请求事件监听
27
+ * - bindGroupHistoryHandler: 注册群历史消息请求事件监听
28
+ * - bindGroupMemberHandler: 注册群成员变更事件监听
29
+ * - bindGroupAbortHandler: 注册群中止请求事件监听
30
+ */
31
+ export { bindAgentsRequestHandler } from './agents-request.js';
32
+ export { handleFileDownloadReq } from './file-download.js';
33
+ export { bindMessagePrivateHandler } from './message-private.js';
34
+ export { bindHistoryRequestHandler } from './history-request.js';
35
+ export { bindChatRequestHandler } from './chat-request.js';
36
+ export { bindGroupRequestHandler } from './group-request.js';
37
+ export { bindGroupHistoryHandler } from './group-history.js';
38
+ export { bindGroupMemberHandler } from './group-member.js';
39
+ export { bindGroupAbortHandler } from './group-abort.js';
@@ -0,0 +1,82 @@
1
+ /**
2
+ * 私信消息处理器(message:private)
3
+ *
4
+ * 职责:对消息做多层过滤,将合法消息放入处理队列
5
+ */
6
+ import { CHANNEL_KEY, EVENT_MESSAGE_PRIVATE, KIND_FILE_DOWNLOAD, FILE_DOWNLOAD_STATUS } from '../../config.js';
7
+ import { isDuplicate } from '../../dedup.js';
8
+ import { isGroupMessage, extractChatId, recordSessionInHistory } from '../utils/message.js';
9
+ import { handleFileDownloadReq } from './file-download.js';
10
+ import { getModuleLogger } from '../../utils/logger.js';
11
+ const log = getModuleLogger('socket.message-private');
12
+ /** 生成私聊消息去重 key */
13
+ const dedupKey = (msgId) => `private:${msgId}`;
14
+ /** 判断消息是否包含有效内容(文字或附件) */
15
+ function hasValidPayload(data) {
16
+ return !!(data.content?.trim() || (data.files && data.files.length > 0));
17
+ }
18
+ /** 处理私聊文本消息(非群聊分支) */
19
+ function handlePrivateTextMessage(data, deps) {
20
+ const { account, handleMessage } = deps;
21
+ const chatId = extractChatId(data);
22
+ handleMessage({
23
+ senderId: data.from,
24
+ text: data.content || '',
25
+ messageId: data.msgId,
26
+ files: data.files ?? [],
27
+ timestamp: data.timestamp,
28
+ agentId: data.agentId,
29
+ chatId,
30
+ });
31
+ // 异步登记 sessionId 到历史记录
32
+ setImmediate(() => {
33
+ try {
34
+ recordSessionInHistory({
35
+ userId: data.from,
36
+ agentId: data.agentId,
37
+ chatId,
38
+ accountId: account.accountId,
39
+ });
40
+ }
41
+ catch (err) {
42
+ log.warn(`[${CHANNEL_KEY}] recordSessionInHistory 失败: ${err instanceof Error ? err.message : String(err)}`);
43
+ }
44
+ });
45
+ }
46
+ /** 注册 message:private 事件监听 */
47
+ export function bindMessagePrivateHandler(socket, deps) {
48
+ const { botClientId, onEvent, reliableEmitter, account, handleGroupMessage } = deps;
49
+ socket.on(EVENT_MESSAGE_PRIVATE, (data, ack) => {
50
+ ack?.();
51
+ // ① 回环防御:过滤 bot 自身发出的消息
52
+ if (data.from === botClientId)
53
+ return;
54
+ // ② 文件下载信令分发(独立链路,不入 AI 处理队列)
55
+ if (data.kind === KIND_FILE_DOWNLOAD) {
56
+ const status = data.extra?.transferData?.status;
57
+ if (status === FILE_DOWNLOAD_STATUS.REQ && !isDuplicate(dedupKey(data.msgId))) {
58
+ onEvent?.();
59
+ void handleFileDownloadReq(data, botClientId, reliableEmitter, account);
60
+ }
61
+ return;
62
+ }
63
+ // ③ 跳过非文本控制消息
64
+ if (data.kind && data.kind !== 'text')
65
+ return;
66
+ // ④ 内容校验 + 去重
67
+ if (!hasValidPayload(data))
68
+ return;
69
+ if (isDuplicate(dedupKey(data.msgId)))
70
+ return;
71
+ onEvent?.();
72
+ // ⑤ 群聊/私聊分流
73
+ if (isGroupMessage(data)) {
74
+ handleGroupMessage(data);
75
+ log.info(`[${CHANNEL_KEY}] 处理群聊消息: groupId=${data?.extra?.groupId}, from=${data.from}, content="${(data.content || '').slice(0, 60)}", files=${data.files?.length ?? 0}`);
76
+ }
77
+ else {
78
+ log.info(`[${CHANNEL_KEY}] 处理私聊消息: agent=${data.agentId}, from=${data.from}, content="${(data.content || '').slice(0, 60)}", files=${data.files?.length ?? 0}`);
79
+ handlePrivateTextMessage(data, deps);
80
+ }
81
+ });
82
+ }