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.
- package/dist/src/config.js +30 -3
- package/dist/src/gateway.js +58 -12
- package/dist/src/group/constants/index.js +20 -0
- package/dist/src/group/inbound/index.js +254 -0
- package/dist/src/group/index.js +15 -0
- package/dist/src/group/orchestrator/execution/agent-runner.js +299 -0
- package/dist/src/group/orchestrator/execution/index.js +7 -0
- package/dist/src/group/orchestrator/execution/prompt-builder.js +288 -0
- package/dist/src/group/orchestrator/execution/soul-resolver.js +38 -0
- package/dist/src/group/orchestrator/execution/types.js +7 -0
- package/dist/src/group/orchestrator/index.js +14 -0
- package/dist/src/group/orchestrator/lifecycle/conversation-state.js +162 -0
- package/dist/src/group/orchestrator/lifecycle/index.js +7 -0
- package/dist/src/group/orchestrator/lifecycle/ledger-writer.js +96 -0
- package/dist/src/group/orchestrator/lifecycle/run-registry.js +174 -0
- package/dist/src/group/orchestrator/orchestrator.js +265 -0
- package/dist/src/group/orchestrator/planning/index.js +13 -0
- package/dist/src/group/orchestrator/planning/plan-validator.js +233 -0
- package/dist/src/group/orchestrator/planning/planning-parser.js +207 -0
- package/dist/src/group/orchestrator/planning/subtask-executor.js +345 -0
- package/dist/src/group/orchestrator/planning/summarizer-runner.js +224 -0
- package/dist/src/group/orchestrator/routes/index.js +9 -0
- package/dist/src/group/orchestrator/routes/leader-dispatch.js +229 -0
- package/dist/src/group/orchestrator/routes/leader-orchestration-route.js +179 -0
- package/dist/src/group/orchestrator/routes/leader-planning.js +92 -0
- package/dist/src/group/orchestrator/routes/leader-self-answer.js +223 -0
- package/dist/src/group/orchestrator/routes/mention-concurrent-route.js +226 -0
- package/dist/src/group/orchestrator/routes/route-helpers.js +186 -0
- package/dist/src/group/orchestrator/routes/types.js +8 -0
- package/dist/src/group/services/group-cleanup-service.js +183 -0
- package/dist/src/group/services/group-creation-service.js +122 -0
- package/dist/src/group/services/group-deletion-service.js +111 -0
- package/dist/src/group/services/group-history-service.js +73 -0
- package/dist/src/group/services/group-member-service.js +169 -0
- package/dist/src/group/services/group-query-service.js +133 -0
- package/dist/src/group/services/group-update-service.js +144 -0
- package/dist/src/group/services/index.js +20 -0
- package/dist/src/group/storage/concurrency-manager.js +119 -0
- package/dist/src/group/storage/group-storage-core.js +227 -0
- package/dist/src/group/storage/index.js +12 -0
- package/dist/src/group/storage/message-reader.js +213 -0
- package/dist/src/group/storage/message-writer.js +229 -0
- package/dist/src/group/storage/slice-manager.js +165 -0
- package/dist/src/group/types/common.js +5 -0
- package/dist/src/group/types/index.js +5 -0
- package/dist/src/group/types/message.js +5 -0
- package/dist/src/group/types/orchestrator.js +5 -0
- package/dist/src/group/types/storage.js +5 -0
- package/dist/src/group/utils/id-generator.js +15 -0
- package/dist/src/group/utils/index.js +12 -0
- package/dist/src/group/utils/mime.js +36 -0
- package/dist/src/group/utils/normalize.js +32 -0
- package/dist/src/group/utils/run-helpers.js +36 -0
- package/dist/src/history/session-reader.js +8 -2
- package/dist/src/outbound.js +12 -19
- package/dist/src/shared.js +4 -3
- package/dist/src/socket/events/agents-request.js +147 -0
- package/dist/src/socket/events/chat-request.js +67 -0
- package/dist/src/socket/events/file-download.js +121 -0
- package/dist/src/socket/events/group-abort.js +59 -0
- package/dist/src/socket/events/group-history.js +59 -0
- package/dist/src/socket/events/group-member.js +83 -0
- package/dist/src/socket/events/group-request.js +91 -0
- package/dist/src/socket/events/history-request.js +95 -0
- package/dist/src/socket/events/index.js +39 -0
- package/dist/src/socket/events/message-private.js +82 -0
- package/dist/src/socket/handlers.js +53 -568
- package/dist/src/socket/native-socket.js +21 -20
- package/dist/src/socket/registry.js +6 -3
- package/dist/src/socket/reliable-emitter.js +16 -13
- package/dist/src/socket/service/chat-common.js +36 -0
- package/dist/src/socket/service/chat-create.js +75 -0
- package/dist/src/socket/service/chat-delete.js +94 -0
- package/dist/src/socket/service/chat-list.js +82 -0
- package/dist/src/socket/service/chat-update.js +83 -0
- package/dist/src/socket/service/group-abort.js +104 -0
- package/dist/src/socket/service/group-history.js +140 -0
- package/dist/src/socket/service/group-member.js +209 -0
- package/dist/src/socket/service/group.js +233 -0
- package/dist/src/socket/service/history.js +102 -0
- package/dist/src/socket/service/index.js +14 -0
- package/dist/src/socket/types/index.js +7 -0
- package/dist/src/socket/types/request.js +8 -0
- package/dist/src/socket/types/service.js +8 -0
- package/dist/src/socket/utils/agent-soul.js +95 -0
- package/dist/src/socket/utils/index.js +8 -0
- package/dist/src/socket/utils/message.js +83 -0
- package/dist/src/socket/utils/validate.js +42 -0
- package/dist/src/streaming/index.js +1 -0
- package/dist/src/streaming/stream-reply-sink.js +367 -20
- package/dist/src/streaming/thinking-formatter.js +325 -0
- package/dist/src/streaming/types.js +20 -1
- package/dist/src/{download-tool.js → tools/download-tool.js} +41 -35
- package/dist/src/tools/group-history-tool.js +172 -0
- package/dist/src/{upload-tool.js → tools/upload-tool.js} +2 -2
- package/dist/src/tools.js +4 -3
- package/dist/src/utils/index.js +1 -0
- package/dist/src/utils/logger.js +38 -0
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/dist/src/socket/agent-soul.js +0 -41
- package/dist/src/socket/chat.js +0 -257
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Socket 网关侧的群组(Group)请求处理器集合。
|
|
3
|
+
* @description
|
|
4
|
+
* 提供对群组的「列表查询 / 创建 / 更新 / 解散 / 清空」五类 socket 请求的统一处理:
|
|
5
|
+
* - 参数校验
|
|
6
|
+
* - 调用对应的领域服务(GroupQueryService / GroupCreationService / GroupUpdateService / GroupDeletionService)
|
|
7
|
+
* - 通过 ReliableEmitter 以可靠投递的方式回包给前端
|
|
8
|
+
* - 失败时统一通过 `createGroupErrorSender` 输出错误响应与日志
|
|
9
|
+
*/
|
|
10
|
+
import { CHANNEL_KEY } from '../../config.js';
|
|
11
|
+
import { EVENT_GROUP_RESPONSE } from '../../group/constants/index.js';
|
|
12
|
+
import { getModuleLogger } from '../../utils/logger.js';
|
|
13
|
+
import { generateMsgId } from '../../dedup.js';
|
|
14
|
+
import { resolveEffectiveApiKey } from '../../config.js';
|
|
15
|
+
import { GroupQueryService, GroupCreationService, GroupUpdateService, GroupDeletionService, } from '../../group/services/index.js';
|
|
16
|
+
/** 模块级共享的群组查询服务实例,便于命中其内部缓存。 */
|
|
17
|
+
const queryService = new GroupQueryService();
|
|
18
|
+
/** 模块级日志器,通过公共方法获取,无需参数传递。 */
|
|
19
|
+
const logger = getModuleLogger('socket.group');
|
|
20
|
+
/** 将未知错误统一转换为字符串消息。 */
|
|
21
|
+
function toErrorMessage(err) {
|
|
22
|
+
return err instanceof Error ? err.message : String(err);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 构造统一的群组错误响应发射器。
|
|
26
|
+
* 调用返回的函数会输出 error 日志并通过 ReliableEmitter 发送错误响应。
|
|
27
|
+
* @param ctx - 错误响应的上下文(包含请求类型与回包必要字段)
|
|
28
|
+
* @param extra - 额外要合并到响应体的字段(例如 `groupId`)
|
|
29
|
+
*/
|
|
30
|
+
function createGroupErrorSender(ctx, extra = {}) {
|
|
31
|
+
const { requestMsgId, botClientId, userId, type, reliableEmitter } = ctx;
|
|
32
|
+
return (error) => {
|
|
33
|
+
logger.error(`[${CHANNEL_KEY}] 群组 ${type} 错误: ${error}`);
|
|
34
|
+
reliableEmitter.emitWithAck(EVENT_GROUP_RESPONSE, {
|
|
35
|
+
msgId: requestMsgId,
|
|
36
|
+
from: botClientId,
|
|
37
|
+
to: userId,
|
|
38
|
+
type,
|
|
39
|
+
timestamp: Date.now(),
|
|
40
|
+
groups: [],
|
|
41
|
+
...extra,
|
|
42
|
+
extra: { error },
|
|
43
|
+
}, requestMsgId);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 发送群组成功响应。
|
|
48
|
+
* @param ctx - 通用回包上下文
|
|
49
|
+
* @param type - 群组请求类型
|
|
50
|
+
* @param groups - 返回给前端的群组列表
|
|
51
|
+
*/
|
|
52
|
+
function emitGroupResponse(ctx, type, groups) {
|
|
53
|
+
const { requestMsgId, botClientId, userId, reliableEmitter } = ctx;
|
|
54
|
+
reliableEmitter.emitWithAck(EVENT_GROUP_RESPONSE, {
|
|
55
|
+
msgId: requestMsgId,
|
|
56
|
+
from: botClientId,
|
|
57
|
+
to: userId,
|
|
58
|
+
type,
|
|
59
|
+
groups,
|
|
60
|
+
timestamp: Date.now(),
|
|
61
|
+
}, requestMsgId);
|
|
62
|
+
}
|
|
63
|
+
/** 处理「群组列表」请求:查询当前用户加入的所有群组并回包。 */
|
|
64
|
+
export async function handleGroupList(params) {
|
|
65
|
+
const { userId } = params;
|
|
66
|
+
const sendError = createGroupErrorSender({ ...params, type: 'list' });
|
|
67
|
+
try {
|
|
68
|
+
const groups = await queryService.getUserGroups(userId);
|
|
69
|
+
logger.info(`[${CHANNEL_KEY}] 群组列表: userId=${userId} count=${groups.length}`);
|
|
70
|
+
emitGroupResponse(params, 'list', groups);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
sendError(toErrorMessage(err));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 处理「创建群组」请求。
|
|
78
|
+
* 校验 name / members / defaultAgentId 后调用 GroupCreationService 创建群组,
|
|
79
|
+
* 成功后清理查询缓存并回包新建群组的 meta 信息。
|
|
80
|
+
*/
|
|
81
|
+
export async function handleGroupCreate(params) {
|
|
82
|
+
const { userId, name, desc, members, defaultAgentId, orchestrator } = params;
|
|
83
|
+
const sendError = createGroupErrorSender({ ...params, type: 'create' });
|
|
84
|
+
if (!name || !name.trim()) {
|
|
85
|
+
sendError('缺少群名称');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (!Array.isArray(members) || members.length === 0) {
|
|
89
|
+
sendError('缺少群成员');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!defaultAgentId) {
|
|
93
|
+
sendError('缺少默认 Agent ID');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const creationService = new GroupCreationService();
|
|
98
|
+
const groupMeta = await creationService.createGroup(userId, name.trim(), (desc ?? '').trim(), members, defaultAgentId);
|
|
99
|
+
queryService.clearCache();
|
|
100
|
+
logger.info(`[${CHANNEL_KEY}] 创建群组: userId=${userId} groupId=${groupMeta.groupId} name="${groupMeta.name}" members=${members.length}`);
|
|
101
|
+
emitGroupResponse(params, 'create', [groupMeta]);
|
|
102
|
+
// 创建群成功后,唤醒主Agent生成欢迎消息
|
|
103
|
+
if (orchestrator) {
|
|
104
|
+
triggerWelcomeMessage(orchestrator, groupMeta, userId).catch((err) => {
|
|
105
|
+
logger.warn(`[${CHANNEL_KEY}] 唤醒主Agent生成欢迎消息失败: ${toErrorMessage(err)}`);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
sendError(toErrorMessage(err));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* 创建群成功后触发主Agent生成欢迎消息
|
|
115
|
+
*
|
|
116
|
+
* @remarks
|
|
117
|
+
* 构造一个特殊的 DispatchContext,让主Agent根据群目标生成欢迎消息。
|
|
118
|
+
* 采用「即发即忘」模式,不阻塞创建群的响应流程。
|
|
119
|
+
*
|
|
120
|
+
* @param orchestrator - 调度器实例
|
|
121
|
+
* @param groupMeta - 新创建的群元数据
|
|
122
|
+
* @param userId - 创建者用户 ID
|
|
123
|
+
*/
|
|
124
|
+
async function triggerWelcomeMessage(orchestrator, groupMeta, userId) {
|
|
125
|
+
const triggerMsgId = generateMsgId();
|
|
126
|
+
const conversationId = `conv:${groupMeta.groupId}:welcome:${triggerMsgId}`;
|
|
127
|
+
const abortSignal = orchestrator.runRegistry.ensureConversation(conversationId);
|
|
128
|
+
const effectiveApiKey = resolveEffectiveApiKey({ senderId: userId });
|
|
129
|
+
// 构造引导主Agent生成欢迎消息的用户消息
|
|
130
|
+
const welcomeUserMessage = [
|
|
131
|
+
`群「${groupMeta.name}」已创建成功。`,
|
|
132
|
+
groupMeta.desc ? `群目标:${groupMeta.desc}` : '',
|
|
133
|
+
`请作为领航员发送一条欢迎消息,向我介绍这个群的目标和建议的开始方向。`,
|
|
134
|
+
].filter(Boolean).join('\n');
|
|
135
|
+
const ctx = {
|
|
136
|
+
conversationId,
|
|
137
|
+
groupId: groupMeta.groupId,
|
|
138
|
+
userId,
|
|
139
|
+
meta: {
|
|
140
|
+
groupId: groupMeta.groupId,
|
|
141
|
+
members: groupMeta.members,
|
|
142
|
+
defaultAgentId: groupMeta.defaultAgentId,
|
|
143
|
+
ownerIds: groupMeta.ownerIds,
|
|
144
|
+
name: groupMeta.name,
|
|
145
|
+
desc: groupMeta.desc,
|
|
146
|
+
},
|
|
147
|
+
userMessage: welcomeUserMessage,
|
|
148
|
+
mentions: [],
|
|
149
|
+
abortSignal,
|
|
150
|
+
msgId: triggerMsgId,
|
|
151
|
+
effectiveApiKey,
|
|
152
|
+
welcomeMode: true,
|
|
153
|
+
};
|
|
154
|
+
const result = await orchestrator.dispatch(ctx);
|
|
155
|
+
logger.info(`[${CHANNEL_KEY}] 欢迎消息生成完成: groupId=${groupMeta.groupId} status=${result.finalStatus}`);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 处理「更新群组」请求。
|
|
159
|
+
* 仅把传入的有效字段合并进 updates,无可更新字段时返回错误。
|
|
160
|
+
*/
|
|
161
|
+
export async function handleGroupUpdate(params) {
|
|
162
|
+
const { userId, groupId, name, desc, pinned } = params;
|
|
163
|
+
const sendError = createGroupErrorSender({ ...params, type: 'update' }, { groupId });
|
|
164
|
+
if (!groupId) {
|
|
165
|
+
sendError('缺少群组 ID');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const updates = {};
|
|
169
|
+
if (typeof name === 'string')
|
|
170
|
+
updates.name = name.trim();
|
|
171
|
+
if (typeof desc === 'string')
|
|
172
|
+
updates.desc = desc.trim();
|
|
173
|
+
if (typeof pinned === 'boolean')
|
|
174
|
+
updates.pinned = pinned;
|
|
175
|
+
if (Object.keys(updates).length === 0) {
|
|
176
|
+
sendError('没有可更新的字段');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const updateService = new GroupUpdateService();
|
|
181
|
+
const updated = await updateService.updateGroup(groupId, userId, updates);
|
|
182
|
+
queryService.clearCache();
|
|
183
|
+
logger.info(`[${CHANNEL_KEY}] 更新群组: userId=${userId} groupId=${groupId} fields=${Object.keys(updates).join(',')}`);
|
|
184
|
+
emitGroupResponse(params, 'update', [updated]);
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
sendError(toErrorMessage(err));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 处理「解散群组」请求。
|
|
192
|
+
* 鉴权与删除逻辑由 GroupDeletionService 内部完成,
|
|
193
|
+
* 成功后清理查询缓存并回包被解散群组的 meta 信息。
|
|
194
|
+
*/
|
|
195
|
+
export async function handleGroupDelete(params) {
|
|
196
|
+
const { userId, groupId } = params;
|
|
197
|
+
const sendError = createGroupErrorSender({ ...params, type: 'delete' }, { groupId });
|
|
198
|
+
if (!groupId) {
|
|
199
|
+
sendError('缺少群组 ID');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const deletionService = new GroupDeletionService();
|
|
204
|
+
const dismissed = await deletionService.deleteGroup(groupId, userId);
|
|
205
|
+
queryService.clearCache();
|
|
206
|
+
logger.info(`[${CHANNEL_KEY}] 解散群组: userId=${userId} groupId=${groupId}`);
|
|
207
|
+
emitGroupResponse(params, 'delete', [dismissed]);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
sendError(toErrorMessage(err));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* 处理「清空群组聊天账本」请求。
|
|
215
|
+
* 仅清理消息账本与相关 Agent session,保留群组元数据(成员、置顶、名称等不变)。
|
|
216
|
+
*/
|
|
217
|
+
export async function handleGroupClear(params) {
|
|
218
|
+
const { userId, groupId } = params;
|
|
219
|
+
const sendError = createGroupErrorSender({ ...params, type: 'clear' }, { groupId });
|
|
220
|
+
if (!groupId) {
|
|
221
|
+
sendError('缺少群组 ID');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const deletionService = new GroupDeletionService();
|
|
226
|
+
const cleared = await deletionService.clearGroup(groupId, userId);
|
|
227
|
+
logger.info(`[${CHANNEL_KEY}] 清空群组: userId=${userId} groupId=${groupId}`);
|
|
228
|
+
emitGroupResponse(params, 'clear', [cleared]);
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
sendError(toErrorMessage(err));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 历史消息查询 Service
|
|
3
|
+
*
|
|
4
|
+
* 职责:封装私聊历史消息的查询逻辑,包括:
|
|
5
|
+
* - sessionKey 解析
|
|
6
|
+
* - agentId 合法性校验
|
|
7
|
+
* - 多模式读取(multi-session / single-session-with-cron)
|
|
8
|
+
* - sessionId 兜底登记
|
|
9
|
+
*
|
|
10
|
+
* 由 handlers/history-request.ts 调用,保持 handler 层薄而清晰。
|
|
11
|
+
*/
|
|
12
|
+
import { CHANNEL_KEY, DEFAULT_HISTORY_LIMIT, DEFAULT_AGENT_ID } from '../../config.js';
|
|
13
|
+
import { getLightclawRuntime } from '../../runtime.js';
|
|
14
|
+
import { readSessionHistoryWithCron, readSessionHistoriesByIds, loadSessionStore, } from '../../history/index.js';
|
|
15
|
+
import { ensureSessionInHistory, readChatsFile, resolveChatsFilePath } from '../../utils/common.js';
|
|
16
|
+
import { extractChatId } from '../utils/message.js';
|
|
17
|
+
import { getModuleLogger } from '../../utils/logger.js';
|
|
18
|
+
/** 模块级日志器 */
|
|
19
|
+
const log = getModuleLogger('socket.history-service');
|
|
20
|
+
/**
|
|
21
|
+
* 查询私聊历史消息
|
|
22
|
+
*
|
|
23
|
+
* @param data 前端请求数据
|
|
24
|
+
* @param account 当前账户配置
|
|
25
|
+
* @returns 查询结果(消息列表 + sessionKey + agentId)
|
|
26
|
+
*/
|
|
27
|
+
export function queryPrivateHistory(data, account) {
|
|
28
|
+
const pluginRuntime = getLightclawRuntime();
|
|
29
|
+
const currentCfg = pluginRuntime.config.loadConfig();
|
|
30
|
+
const baseRoute = pluginRuntime.channel.routing.resolveAgentRoute({
|
|
31
|
+
cfg: currentCfg,
|
|
32
|
+
channel: CHANNEL_KEY,
|
|
33
|
+
accountId: account.accountId,
|
|
34
|
+
peer: { kind: 'direct', id: data.from },
|
|
35
|
+
});
|
|
36
|
+
// agentId 合法性校验
|
|
37
|
+
const currentCfgTyped = currentCfg;
|
|
38
|
+
const validAgentIds = currentCfgTyped.agents?.list?.map((a) => a.id) ?? [DEFAULT_AGENT_ID];
|
|
39
|
+
log.info(`[${CHANNEL_KEY}] 当前合法的agentId为:${validAgentIds}`);
|
|
40
|
+
const resolvedAgentId = data.agentId && validAgentIds.includes(data.agentId) ? data.agentId : DEFAULT_AGENT_ID;
|
|
41
|
+
log.info(`[${CHANNEL_KEY}] 当前agentId为:${resolvedAgentId}`);
|
|
42
|
+
// 构建 sessionKey
|
|
43
|
+
const baseSessionKey = pluginRuntime.channel.routing.buildAgentSessionKey({
|
|
44
|
+
agentId: resolvedAgentId,
|
|
45
|
+
channel: CHANNEL_KEY,
|
|
46
|
+
accountId: baseRoute.accountId,
|
|
47
|
+
peer: { kind: 'direct', id: data.from },
|
|
48
|
+
dmScope: 'per-channel-peer',
|
|
49
|
+
});
|
|
50
|
+
const chatIdSuffix = extractChatId(data);
|
|
51
|
+
const sessionKey = chatIdSuffix ? `${baseSessionKey}:${chatIdSuffix}` : baseSessionKey;
|
|
52
|
+
log.info(`[${CHANNEL_KEY}] userId=${data.from},chatId=${chatIdSuffix || '-'},当前的sessionKey=${sessionKey}`);
|
|
53
|
+
// ① 兜底登记当前 sessionId 到 sessionIdHistory
|
|
54
|
+
try {
|
|
55
|
+
const store = loadSessionStore(resolvedAgentId);
|
|
56
|
+
const currentSessionId = store[sessionKey]?.sessionId;
|
|
57
|
+
if (currentSessionId) {
|
|
58
|
+
ensureSessionInHistory(resolvedAgentId, data.from, chatIdSuffix, currentSessionId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
log.warn(`[${CHANNEL_KEY}] ensureSessionInHistory(history:request) error: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
// ② 读取历史消息
|
|
65
|
+
const limit = data.limit ?? DEFAULT_HISTORY_LIMIT;
|
|
66
|
+
const chatOnly = data.chatOnly ?? true;
|
|
67
|
+
let messages = [];
|
|
68
|
+
let readMode = 'single-session-with-cron';
|
|
69
|
+
try {
|
|
70
|
+
const targetChatId = chatIdSuffix;
|
|
71
|
+
const chatsPath = resolveChatsFilePath(resolvedAgentId, data.from);
|
|
72
|
+
const matched = readChatsFile(chatsPath).find((c) => c.chatId === targetChatId);
|
|
73
|
+
const historyIds = matched?.sessionIdHistory ?? [];
|
|
74
|
+
if (historyIds.length > 0) {
|
|
75
|
+
messages = readSessionHistoriesByIds(historyIds, {
|
|
76
|
+
limit,
|
|
77
|
+
chatOnly,
|
|
78
|
+
agentId: resolvedAgentId,
|
|
79
|
+
});
|
|
80
|
+
readMode = 'multi-session';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
log.warn(`[${CHANNEL_KEY}] readSessionHistoriesByIds fallback to single-session: ${err instanceof Error ? err.message : String(err)}`);
|
|
85
|
+
}
|
|
86
|
+
if (messages.length === 0 && readMode !== 'multi-session') {
|
|
87
|
+
messages = readSessionHistoryWithCron(sessionKey, {
|
|
88
|
+
limit,
|
|
89
|
+
chatOnly,
|
|
90
|
+
includeCron: true,
|
|
91
|
+
agentId: resolvedAgentId,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
log.info(`[${CHANNEL_KEY}] History request: userId=${data.from} sessionKey=${sessionKey} mode=${readMode} found=${messages.length}`);
|
|
95
|
+
// 过滤掉内容为空的消息
|
|
96
|
+
const filteredMessages = messages.filter((msg) => !!msg.content.trim() || (msg.files && msg.files.length > 0));
|
|
97
|
+
return {
|
|
98
|
+
messages: filteredMessages,
|
|
99
|
+
sessionKey,
|
|
100
|
+
agentId: resolvedAgentId,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket 业务逻辑层统一导出
|
|
3
|
+
*
|
|
4
|
+
* 包含:会话管理、历史消息查询、群组管理、群成员管理、群历史、群中止等业务逻辑。
|
|
5
|
+
*/
|
|
6
|
+
export { handleChatList } from './chat-list.js';
|
|
7
|
+
export { handleChatCreate } from './chat-create.js';
|
|
8
|
+
export { handleChatUpdate } from './chat-update.js';
|
|
9
|
+
export { handleChatDelete } from './chat-delete.js';
|
|
10
|
+
export { queryPrivateHistory } from './history.js';
|
|
11
|
+
export { handleGroupList, handleGroupCreate, handleGroupUpdate, handleGroupDelete, handleGroupClear, } from './group.js';
|
|
12
|
+
export { handleGroupMemberAdd, handleGroupMemberRemove } from './group-member.js';
|
|
13
|
+
export { handleGroupHistory } from './group-history.js';
|
|
14
|
+
export { handleGroupAbort } from './group-abort.js';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file agent-soul.ts
|
|
3
|
+
* @description Agent SoulId 映射工具
|
|
4
|
+
*
|
|
5
|
+
* 负责从 lightsoul 配置文件中读取 agent 与 soulId 的映射关系,
|
|
6
|
+
* 并将 soulId 附加到 agent 配置列表中。
|
|
7
|
+
*
|
|
8
|
+
* 配置文件支持两种格式:
|
|
9
|
+
* - 扁平格式:`{ "agentId": "soulId" }`
|
|
10
|
+
* - 对象格式:`{ "agentId": { "soulId": "xxx" } }`
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { getModuleLogger } from '../../utils/logger.js';
|
|
15
|
+
/** 模块级日志器 */
|
|
16
|
+
const log = getModuleLogger('socket.agent-soul');
|
|
17
|
+
/** lightsoul 配置文件默认路径 */
|
|
18
|
+
export const LIGHTSOUL_CONFIG_PATH = path.join(process.env.HOME || '', '.config/lightsoul/config.json');
|
|
19
|
+
/**
|
|
20
|
+
* 获取非空 trim 字符串,空白视为无效
|
|
21
|
+
* @param value - 待检测值
|
|
22
|
+
* @returns trim 后的字符串,若无效则返回空字符串
|
|
23
|
+
*/
|
|
24
|
+
const getTrimmedString = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
25
|
+
/**
|
|
26
|
+
* 判断值是否为非数组的普通对象
|
|
27
|
+
* @param value - 待检测值
|
|
28
|
+
*/
|
|
29
|
+
const isPlainObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
+
/**
|
|
31
|
+
* 解析原始 JSON 字符串为 SoulIdMap
|
|
32
|
+
*
|
|
33
|
+
* 支持两种配置格式:
|
|
34
|
+
* - 字符串值:`{ "agentId": "soulId" }` → 直接映射
|
|
35
|
+
* - 对象值:`{ "agentId": { "soulId": "xxx" } }` → 提取 soulId 字段
|
|
36
|
+
*
|
|
37
|
+
* @param raw - lightsoul 配置文件的原始 JSON 字符串
|
|
38
|
+
* @returns 解析后的 agentId → soulId 映射表
|
|
39
|
+
*/
|
|
40
|
+
export function parseSoulIdMap(raw) {
|
|
41
|
+
const map = {};
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(raw);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
log.error(`解析 lightsoul 配置失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
return map;
|
|
49
|
+
}
|
|
50
|
+
if (!isPlainObject(parsed))
|
|
51
|
+
return map;
|
|
52
|
+
for (const [agentId, value] of Object.entries(parsed)) {
|
|
53
|
+
// 扁平格式:值直接为 soulId 字符串
|
|
54
|
+
const directValue = getTrimmedString(value);
|
|
55
|
+
if (directValue) {
|
|
56
|
+
map[agentId] = directValue;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// 对象格式:从 value.soulId 中提取
|
|
60
|
+
if (isPlainObject(value)) {
|
|
61
|
+
const soulId = getTrimmedString(value.soulId);
|
|
62
|
+
if (soulId) {
|
|
63
|
+
map[agentId] = soulId;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return map;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 从配置文件中读取 SoulIdMap
|
|
71
|
+
* @param configPath - 配置文件路径,默认为 LIGHTSOUL_CONFIG_PATH
|
|
72
|
+
* @returns 解析后的 agentId → soulId 映射表
|
|
73
|
+
* @throws 文件不存在或读取失败时抛出异常
|
|
74
|
+
*/
|
|
75
|
+
export function readSoulIdMap(configPath = LIGHTSOUL_CONFIG_PATH) {
|
|
76
|
+
return parseSoulIdMap(fs.readFileSync(configPath, 'utf8'));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 为 agents 列表附加 soulId
|
|
80
|
+
*
|
|
81
|
+
* 匹配规则:优先按 agent.id 查找,其次按 agent.name 查找。
|
|
82
|
+
* 若匹配到 soulId,则返回附加了 soulId 字段的新对象;否则原样返回。
|
|
83
|
+
*
|
|
84
|
+
* @param agents - agent 配置列表
|
|
85
|
+
* @param soulMap - agentId → soulId 映射表
|
|
86
|
+
* @returns 附加 soulId 后的 agents 列表(不修改原数组)
|
|
87
|
+
*/
|
|
88
|
+
export function attachSoulIdsToAgents(agents, soulMap) {
|
|
89
|
+
return agents.map((agent) => {
|
|
90
|
+
const id = getTrimmedString(agent.id);
|
|
91
|
+
const name = getTrimmedString(agent.name);
|
|
92
|
+
const soulId = (id && soulMap[id]) || (name && soulMap[name]) || undefined;
|
|
93
|
+
return soulId ? { ...agent, soulId } : agent;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket 工具函数层统一导出
|
|
3
|
+
*
|
|
4
|
+
* 包含:消息判断/提取工具、Agent Soul ID 解析工具。
|
|
5
|
+
*/
|
|
6
|
+
export { isGroupMessage, extractChatId, recordSessionInHistory } from './message.js';
|
|
7
|
+
export { parseSoulIdMap, readSoulIdMap, attachSoulIdsToAgents, LIGHTSOUL_CONFIG_PATH, } from './agent-soul.js';
|
|
8
|
+
export { hasRequiredFields, getMissingFields } from './validate.js';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket 事件处理器内部工具函数
|
|
3
|
+
*
|
|
4
|
+
* 包含:isGroupMessage、extractChatId、recordSessionInHistory
|
|
5
|
+
* 从 handlers.ts 中抽离,统一管理公共逻辑。
|
|
6
|
+
*/
|
|
7
|
+
import { CHANNEL_KEY, DEFAULT_AGENT_ID } from '../../config.js';
|
|
8
|
+
import { getLightclawRuntime } from '../../runtime.js';
|
|
9
|
+
import { loadSessionStore } from '../../history/index.js';
|
|
10
|
+
import { ensureSessionInHistory } from '../../utils/common.js';
|
|
11
|
+
import { getModuleLogger } from '../../utils/logger.js';
|
|
12
|
+
/** 模块级日志器 */
|
|
13
|
+
const log = getModuleLogger('socket.utils');
|
|
14
|
+
/**
|
|
15
|
+
* 判断一条 PrivateMessageData 是否为群聊消息
|
|
16
|
+
*
|
|
17
|
+
* 协议约定(docs/design.md §6.6.1):群聊复用 EVENT_MESSAGE_PRIVATE 事件,
|
|
18
|
+
* 通过顶层 chatKind === 'group' 区分,且 extra.groupId 必填为非空字符串。
|
|
19
|
+
* 容错:缺 chatKind / groupId / 字段类型不对 → 一律视为非群聊,回退 DM 路径。
|
|
20
|
+
*/
|
|
21
|
+
export function isGroupMessage(data) {
|
|
22
|
+
if (data.chatKind !== 'group')
|
|
23
|
+
return false;
|
|
24
|
+
const groupId = data.extra?.groupId;
|
|
25
|
+
return typeof groupId === 'string' && groupId.trim().length > 0;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 从 PrivateMessageData / HistoryRequestData 中统一提取 chatId
|
|
29
|
+
*
|
|
30
|
+
* 统一为 extractChatId 后:
|
|
31
|
+
* - 入参可能是 undefined / null / 非字符串 / 含空白字符串 / 合法字符串;
|
|
32
|
+
* - 一律 trim 后归一为字符串;undefined / null / 非字符串 → '';
|
|
33
|
+
* - 返回值要么是 ''(默认对话标识),要么是非空合法 chatId。
|
|
34
|
+
*/
|
|
35
|
+
export function extractChatId(data) {
|
|
36
|
+
const raw = data?.extra?.chatId;
|
|
37
|
+
if (typeof raw !== 'string')
|
|
38
|
+
return '';
|
|
39
|
+
return raw.trim();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 把当前 chat 在用的 sessionId 登记到 chats.json[chatId].sessionIdHistory
|
|
43
|
+
*
|
|
44
|
+
* 调用时机:
|
|
45
|
+
* - EVENT_MESSAGE_PRIVATE:在 setImmediate 中异步调用,确保框架已建好/rotate 好 session;
|
|
46
|
+
* - EVENT_HISTORY_REQUEST:在 sessionKey 解析后同步调用,作为兜底登记入口。
|
|
47
|
+
*/
|
|
48
|
+
export function recordSessionInHistory(params) {
|
|
49
|
+
const { userId, agentId, chatId, accountId } = params;
|
|
50
|
+
// 取最新配置,复用 history 分支同款 agentId 校验逻辑(防止伪造 agentId)
|
|
51
|
+
const pluginRuntime = getLightclawRuntime();
|
|
52
|
+
const currentCfg = pluginRuntime.config.loadConfig();
|
|
53
|
+
const currentCfgTyped = currentCfg;
|
|
54
|
+
const validAgentIds = currentCfgTyped.agents?.list?.map((a) => a.id) ?? [DEFAULT_AGENT_ID];
|
|
55
|
+
const resolvedAgentId = agentId && validAgentIds.includes(agentId) ? agentId : DEFAULT_AGENT_ID;
|
|
56
|
+
// 计算 sessionKey(必须与 history:request 一致,否则查不到 sessions.json 条目)
|
|
57
|
+
const baseRoute = pluginRuntime.channel.routing.resolveAgentRoute({
|
|
58
|
+
cfg: currentCfg,
|
|
59
|
+
channel: CHANNEL_KEY,
|
|
60
|
+
accountId,
|
|
61
|
+
peer: { kind: 'direct', id: userId },
|
|
62
|
+
});
|
|
63
|
+
const baseSessionKey = pluginRuntime.channel.routing.buildAgentSessionKey({
|
|
64
|
+
agentId: resolvedAgentId,
|
|
65
|
+
channel: CHANNEL_KEY,
|
|
66
|
+
accountId: baseRoute.accountId,
|
|
67
|
+
peer: { kind: 'direct', id: userId },
|
|
68
|
+
dmScope: 'per-channel-peer',
|
|
69
|
+
});
|
|
70
|
+
// 与 history:request 保持完全一致的 sessionKey 拼接规则:
|
|
71
|
+
// - 空 chatId → 不追加后缀,直接用 baseSessionKey(默认对话场景)
|
|
72
|
+
// - 非空 chatId → 追加 `:<chatId>` 后缀(多会话场景)
|
|
73
|
+
const sessionKey = chatId ? `${baseSessionKey}:${chatId}` : baseSessionKey;
|
|
74
|
+
// 查 sessions.json 拿当前生效 sessionId;首条消息时可能查不到 → 跳过,下一条消息会补登记
|
|
75
|
+
const store = loadSessionStore(resolvedAgentId);
|
|
76
|
+
const currentSessionId = store[sessionKey]?.sessionId;
|
|
77
|
+
if (!currentSessionId) {
|
|
78
|
+
log.info(`[${CHANNEL_KEY}] recordSessionInHistory: sessionId not found yet, skip. sessionKey=${sessionKey}`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// 登记 sessionId 到 chats.json,确保历史记录能被查到;已存在则跳过,新增则末尾追加并落盘
|
|
82
|
+
ensureSessionInHistory(resolvedAgentId, userId, chatId, currentSessionId);
|
|
83
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file validate.ts
|
|
3
|
+
* @description Socket 事件请求数据的通用字段校验工具
|
|
4
|
+
*
|
|
5
|
+
* 提供泛型的必填字段校验函数,避免各 handler 重复实现相同逻辑。
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 校验请求数据是否包含所有必填字段(非空判断)
|
|
9
|
+
*
|
|
10
|
+
* @typeParam T - 请求数据类型
|
|
11
|
+
* @param data - 请求数据(可能为 undefined)
|
|
12
|
+
* @param requiredFields - 必填字段名称数组
|
|
13
|
+
* @returns 当所有必填字段均存在且非空时返回 true(同时作为类型守卫)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const REQUIRED = ['from', 'msgId', 'type'] as const;
|
|
18
|
+
* if (!hasRequiredFields<GroupRequestData>(data, REQUIRED)) { ... }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function hasRequiredFields(data, requiredFields) {
|
|
22
|
+
if (!data)
|
|
23
|
+
return false;
|
|
24
|
+
return requiredFields.every((key) => !!data[key]);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 获取请求数据中缺失的必填字段名称列表
|
|
28
|
+
*
|
|
29
|
+
* @typeParam T - 请求数据类型
|
|
30
|
+
* @param data - 请求数据(可能为 undefined 或部分填充)
|
|
31
|
+
* @param requiredFields - 必填字段名称数组
|
|
32
|
+
* @returns 缺失字段名称数组,用于日志输出
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const missing = getMissingFields(data, REQUIRED);
|
|
37
|
+
* log.warn(`缺少字段: [${missing.join(', ')}]`);
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function getMissingFields(data, requiredFields) {
|
|
41
|
+
return requiredFields.filter((key) => !data?.[key]);
|
|
42
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* 使用方式:
|
|
7
7
|
* import { createStreamReplyConfig } from "./streaming/index.js";
|
|
8
8
|
*/
|
|
9
|
+
export { DEFAULT_PARTIAL_COALESCE } from "./types.js";
|
|
9
10
|
// 增量计算器
|
|
10
11
|
export { createDeltaTrackerState, toStreamDeltaText } from "./delta-tracker.js";
|
|
11
12
|
// 真流式回复配置(dispatcher + replyOptions,用于 dispatchReplyFromConfig)
|