claw-subagent-service 0.0.115 → 0.0.117
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/package.json
CHANGED
|
@@ -29,6 +29,10 @@ class MessageHandler {
|
|
|
29
29
|
this._groupConfigCache = new Map();
|
|
30
30
|
this._defaultMaxRounds = 10;
|
|
31
31
|
this._groupConfigCacheTTL = config.groupConfigCacheTTL || 60000; // 默认缓存 60 秒
|
|
32
|
+
|
|
33
|
+
// 消息合并相关
|
|
34
|
+
this._pendingMessages = new Map(); // 待合并的消息
|
|
35
|
+
this._messageMergeTimeout = 500; // 合并等待时间(毫秒)
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -215,42 +219,102 @@ class MessageHandler {
|
|
|
215
219
|
});
|
|
216
220
|
}
|
|
217
221
|
|
|
218
|
-
//
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
222
|
+
// 消息合并逻辑:如果是图片消息,等待一段时间看是否有文字消息跟随
|
|
223
|
+
if (msg.messageType === 'RC:ImgMsg') {
|
|
224
|
+
await this._handleImageMessageWithMerge(msg, maxRounds);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 普通消息直接处理
|
|
229
|
+
await this._processMessage(msg, maxRounds);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
this.log?.error(`[MessageHandler] 处理消息异常: ${err.message}`);
|
|
232
|
+
const targetId = msg.conversationType === 3 ? msg.targetId : msg.senderUserId;
|
|
233
|
+
await this.sendFn(targetId, `处理失败: ${err.message}`, msg.conversationType);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 处理图片消息,支持合并后续文字消息
|
|
239
|
+
*/
|
|
240
|
+
async _handleImageMessageWithMerge(msg, maxRounds) {
|
|
241
|
+
const userId = msg.senderUserId;
|
|
242
|
+
const conversationKey = `${msg.conversationType}-${msg.targetId}-${userId}`;
|
|
243
|
+
|
|
244
|
+
// 设置待处理图片消息
|
|
245
|
+
this._pendingMessages.set(conversationKey, {
|
|
246
|
+
imageMsg: msg,
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// 等待一段时间,看是否有文字消息跟随
|
|
251
|
+
await new Promise(resolve => setTimeout(resolve, this._messageMergeTimeout));
|
|
252
|
+
|
|
253
|
+
// 获取待处理消息
|
|
254
|
+
const pending = this._pendingMessages.get(conversationKey);
|
|
255
|
+
this._pendingMessages.delete(conversationKey);
|
|
256
|
+
|
|
257
|
+
if (!pending) {
|
|
258
|
+
return; // 消息已被处理
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 构建合并后的消息内容
|
|
262
|
+
const imageContent = this._extractMessageContent(pending.imageMsg);
|
|
263
|
+
let mergedContent = imageContent;
|
|
264
|
+
|
|
265
|
+
if (pending.textMsg) {
|
|
266
|
+
const textContent = typeof pending.textMsg.content === 'string'
|
|
267
|
+
? pending.textMsg.content
|
|
268
|
+
: (pending.textMsg.content?.content || '');
|
|
269
|
+
mergedContent = `${textContent}\n${imageContent}`;
|
|
270
|
+
this.log?.info(`[MessageHandler] 合并消息: 图片+文字`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 创建合并后的消息对象
|
|
274
|
+
const mergedMsg = {
|
|
275
|
+
...pending.imageMsg,
|
|
276
|
+
content: mergedContent,
|
|
277
|
+
messageType: 'RC:TxtMsg', // 转为文本消息处理
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
await this._processMessage(mergedMsg, maxRounds);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 处理普通消息(包括合并后的消息)
|
|
285
|
+
*/
|
|
286
|
+
async _processMessage(msg, maxRounds) {
|
|
287
|
+
// 如果配置了代理地址,使用流式处理
|
|
288
|
+
if (this.isStreamingEnabled) {
|
|
289
|
+
try {
|
|
290
|
+
await this.handleNormalMessageStream(msg);
|
|
291
|
+
// 流式处理成功,群聊轮数 +1
|
|
292
|
+
if (msg.conversationType === 3) {
|
|
293
|
+
this._incrementGroupRoundCount(msg.targetId, maxRounds);
|
|
237
294
|
}
|
|
238
|
-
}
|
|
239
|
-
|
|
295
|
+
} catch (err) {
|
|
296
|
+
this.log?.error(`[MessageHandler] 流式处理失败,回退到非流式: ${err.message}`);
|
|
240
297
|
const reply = await this.handleNormalMessage(msg);
|
|
241
298
|
if (reply) {
|
|
242
299
|
const targetId = this.getReplyTarget(msg);
|
|
243
300
|
await this.sendFn(targetId, reply, msg.conversationType);
|
|
244
|
-
//
|
|
301
|
+
// 非流式回退成功,群聊轮数 +1
|
|
245
302
|
if (msg.conversationType === 3) {
|
|
246
303
|
this._incrementGroupRoundCount(msg.targetId, maxRounds);
|
|
247
304
|
}
|
|
248
305
|
}
|
|
249
306
|
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
307
|
+
} else {
|
|
308
|
+
// 降级到非流式处理
|
|
309
|
+
const reply = await this.handleNormalMessage(msg);
|
|
310
|
+
if (reply) {
|
|
311
|
+
const targetId = this.getReplyTarget(msg);
|
|
312
|
+
await this.sendFn(targetId, reply, msg.conversationType);
|
|
313
|
+
// 非流式处理成功,群聊轮数 +1
|
|
314
|
+
if (msg.conversationType === 3) {
|
|
315
|
+
this._incrementGroupRoundCount(msg.targetId, maxRounds);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
254
318
|
}
|
|
255
319
|
}
|
|
256
320
|
|
|
@@ -254,9 +254,11 @@ class OpenClawClient {
|
|
|
254
254
|
// 会话历史管理:为每个用户维护对话上下文
|
|
255
255
|
static conversationHistory = new Map();
|
|
256
256
|
// 最大历史轮数(用户+AI 各算一轮)
|
|
257
|
-
static maxHistoryRounds =
|
|
257
|
+
static maxHistoryRounds = 50;
|
|
258
258
|
// 单条消息最大长度(超过则截断)
|
|
259
259
|
static maxMessageLength = 2000;
|
|
260
|
+
// 活跃请求管理:为每个用户跟踪当前正在进行的请求
|
|
261
|
+
static activeRequests = new Map();
|
|
260
262
|
|
|
261
263
|
static async acquireSlot() {
|
|
262
264
|
if (OpenClawClient.runningCount < OpenClawClient.maxConcurrency) {
|
|
@@ -414,6 +416,21 @@ class OpenClawClient {
|
|
|
414
416
|
* @param {Function} onError - 出错时的回调 (error: Error) => void
|
|
415
417
|
* @returns {Promise<void>}
|
|
416
418
|
*/
|
|
419
|
+
/**
|
|
420
|
+
* 取消用户的活跃请求
|
|
421
|
+
* @param {string} fromUser - 用户ID
|
|
422
|
+
*/
|
|
423
|
+
cancelActiveRequest(fromUser) {
|
|
424
|
+
const activeRequest = OpenClawClient.activeRequests.get(fromUser);
|
|
425
|
+
if (activeRequest) {
|
|
426
|
+
this.log?.info(`[OpenClawClient] 取消用户 ${fromUser} 的活跃请求`);
|
|
427
|
+
activeRequest.abort();
|
|
428
|
+
OpenClawClient.activeRequests.delete(fromUser);
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
|
|
417
434
|
async chatStream(message, fromUser, onDelta, onDone, onError) {
|
|
418
435
|
if (!message || !message.trim()) {
|
|
419
436
|
onError?.(new Error('消息内容为空'));
|
|
@@ -428,6 +445,12 @@ class OpenClawClient {
|
|
|
428
445
|
return;
|
|
429
446
|
}
|
|
430
447
|
|
|
448
|
+
// 取消该用户之前的活跃请求(如果存在)
|
|
449
|
+
const hasCancelled = this.cancelActiveRequest(fromUser);
|
|
450
|
+
if (hasCancelled) {
|
|
451
|
+
this.log?.info(`[OpenClawClient] 用户 ${fromUser} 的新消息到达,已取消前一条消息的回复`);
|
|
452
|
+
}
|
|
453
|
+
|
|
431
454
|
this.log?.info(`[OpenClawClient] 准备 SSE 流式调用 OpenClaw,from=${fromUser}`);
|
|
432
455
|
|
|
433
456
|
const gatewayToken = getGatewayToken();
|
|
@@ -445,7 +468,7 @@ class OpenClawClient {
|
|
|
445
468
|
for (let i = 0; i < endpoints.length; i++) {
|
|
446
469
|
const apiUrl = endpoints[i];
|
|
447
470
|
try {
|
|
448
|
-
const fullText = await this._doChatStream(apiUrl, gatewayToken, sessionId, messagesWithHistory, onDelta, onDone);
|
|
471
|
+
const fullText = await this._doChatStream(apiUrl, gatewayToken, sessionId, fromUser, messagesWithHistory, onDelta, onDone);
|
|
449
472
|
|
|
450
473
|
// 成功响应后,保存当前对话到历史记录
|
|
451
474
|
this._addToHistory(fromUser, 'user', message);
|
|
@@ -453,6 +476,12 @@ class OpenClawClient {
|
|
|
453
476
|
|
|
454
477
|
return; // 成功则直接返回
|
|
455
478
|
} catch (err) {
|
|
479
|
+
// 检查是否是取消错误
|
|
480
|
+
if (err.name === 'AbortError' || err.message?.includes('aborted')) {
|
|
481
|
+
this.log?.info(`[OpenClawClient] 请求被用户取消: ${fromUser}`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
456
485
|
const is404 = err.response?.status === 404;
|
|
457
486
|
const isLast = i === endpoints.length - 1;
|
|
458
487
|
|
|
@@ -531,7 +560,7 @@ class OpenClawClient {
|
|
|
531
560
|
}
|
|
532
561
|
}
|
|
533
562
|
|
|
534
|
-
async _doChatStream(apiUrl, gatewayToken, sessionId, messages, onDelta, onDone) {
|
|
563
|
+
async _doChatStream(apiUrl, gatewayToken, sessionId, fromUser, messages, onDelta, onDone) {
|
|
535
564
|
const headers = {
|
|
536
565
|
'Content-Type': 'application/json',
|
|
537
566
|
'Accept': 'text/event-stream'
|
|
@@ -605,10 +634,17 @@ class OpenClawClient {
|
|
|
605
634
|
let hasError = false;
|
|
606
635
|
let errorMsg = '';
|
|
607
636
|
|
|
637
|
+
// 创建 AbortController 用于取消请求
|
|
638
|
+
const abortController = new AbortController();
|
|
639
|
+
|
|
640
|
+
// 注册活跃请求
|
|
641
|
+
OpenClawClient.activeRequests.set(fromUser, abortController);
|
|
642
|
+
|
|
608
643
|
const response = await axios.post(apiUrl, payload, {
|
|
609
644
|
headers,
|
|
610
645
|
responseType: 'stream',
|
|
611
|
-
timeout: 600000 // 10 分钟
|
|
646
|
+
timeout: 600000, // 10 分钟
|
|
647
|
+
signal: abortController.signal
|
|
612
648
|
});
|
|
613
649
|
|
|
614
650
|
return new Promise((resolve, reject) => {
|
|
@@ -677,6 +713,9 @@ class OpenClawClient {
|
|
|
677
713
|
});
|
|
678
714
|
|
|
679
715
|
response.data.on('end', async () => {
|
|
716
|
+
// 清理活跃请求
|
|
717
|
+
OpenClawClient.activeRequests.delete(fromUser);
|
|
718
|
+
|
|
680
719
|
this.log?.info(`[OpenClawClient] SSE 流结束,总长度: ${fullText.length}`);
|
|
681
720
|
|
|
682
721
|
if (hasError) {
|
|
@@ -706,6 +745,9 @@ class OpenClawClient {
|
|
|
706
745
|
});
|
|
707
746
|
|
|
708
747
|
response.data.on('error', (err) => {
|
|
748
|
+
// 清理活跃请求
|
|
749
|
+
OpenClawClient.activeRequests.delete(fromUser);
|
|
750
|
+
|
|
709
751
|
this.log?.error(`[OpenClawClient] SSE 流错误: ${err.message}`);
|
|
710
752
|
reject(err);
|
|
711
753
|
});
|