claw-subagent-service 0.0.111 → 0.0.114
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
|
@@ -185,6 +185,8 @@ class MessageHandler {
|
|
|
185
185
|
const payload = this.parseCommand(msg.content, msg.senderUserId);
|
|
186
186
|
if (payload.command === 'newround') {
|
|
187
187
|
this._resetGroupRoundCount(groupId);
|
|
188
|
+
// 清空 OpenClaw 对话历史,确保新一轮对话没有上下文
|
|
189
|
+
this.openclawClient.clearHistory(msg.senderUserId);
|
|
188
190
|
await this.sendFn(groupId, `✅ 新一轮对话已开始,最大对话轮数为 ${maxRounds} 轮。`, msg.conversationType);
|
|
189
191
|
return;
|
|
190
192
|
}
|
|
@@ -543,6 +545,44 @@ class MessageHandler {
|
|
|
543
545
|
return '[图片](无法获取图片地址)';
|
|
544
546
|
}
|
|
545
547
|
|
|
548
|
+
// 检查是否有文字内容(从 content 或 extra 中提取)
|
|
549
|
+
let textContent = '';
|
|
550
|
+
|
|
551
|
+
// 1. 从 content 对象中提取文字
|
|
552
|
+
if (typeof content === 'object' && content !== null && content.content) {
|
|
553
|
+
textContent = content.content;
|
|
554
|
+
} else if (typeof content === 'string') {
|
|
555
|
+
// 尝试解析 content 是否为 JSON
|
|
556
|
+
try {
|
|
557
|
+
const contentObj = JSON.parse(content);
|
|
558
|
+
if (contentObj.content) {
|
|
559
|
+
textContent = contentObj.content;
|
|
560
|
+
}
|
|
561
|
+
} catch (e) {
|
|
562
|
+
// content 不是 JSON,可能是纯文本
|
|
563
|
+
if (content && !content.startsWith('data:image')) {
|
|
564
|
+
textContent = content;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// 2. 从 extra 字段中提取文字
|
|
570
|
+
if (!textContent && msg.extra) {
|
|
571
|
+
try {
|
|
572
|
+
const extraData = JSON.parse(msg.extra);
|
|
573
|
+
if (extraData.textContent) {
|
|
574
|
+
textContent = extraData.textContent;
|
|
575
|
+
}
|
|
576
|
+
} catch (e) {
|
|
577
|
+
// extra 不是 JSON,忽略
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// 构建返回内容:如果有文字,返回文字+图片;否则只返回图片
|
|
582
|
+
if (textContent) {
|
|
583
|
+
return `${textContent}\n[图片] ${imageUri}`;
|
|
584
|
+
}
|
|
585
|
+
|
|
546
586
|
return `[图片] ${imageUri}`;
|
|
547
587
|
}
|
|
548
588
|
|
|
@@ -251,6 +251,12 @@ class OpenClawClient {
|
|
|
251
251
|
static waitQueue = [];
|
|
252
252
|
// Session 级串行锁:确保同一 session 不会并发 spawn 多个进程
|
|
253
253
|
static sessionLocks = new Map();
|
|
254
|
+
// 会话历史管理:为每个用户维护对话上下文
|
|
255
|
+
static conversationHistory = new Map();
|
|
256
|
+
// 最大历史轮数(用户+AI 各算一轮)
|
|
257
|
+
static maxHistoryRounds = 10;
|
|
258
|
+
// 单条消息最大长度(超过则截断)
|
|
259
|
+
static maxMessageLength = 2000;
|
|
254
260
|
|
|
255
261
|
static async acquireSlot() {
|
|
256
262
|
if (OpenClawClient.runningCount < OpenClawClient.maxConcurrency) {
|
|
@@ -275,6 +281,71 @@ class OpenClawClient {
|
|
|
275
281
|
this.gatewayStarted = false;
|
|
276
282
|
}
|
|
277
283
|
|
|
284
|
+
/**
|
|
285
|
+
* 获取用户的会话历史
|
|
286
|
+
* @param {string} fromUser - 用户ID
|
|
287
|
+
* @returns {Array} 消息历史数组
|
|
288
|
+
*/
|
|
289
|
+
_getConversationHistory(fromUser) {
|
|
290
|
+
return OpenClawClient.conversationHistory.get(fromUser) || [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 添加消息到会话历史
|
|
295
|
+
* @param {string} fromUser - 用户ID
|
|
296
|
+
* @param {string} role - 角色 ('user' 或 'assistant')
|
|
297
|
+
* @param {string} content - 消息内容
|
|
298
|
+
*/
|
|
299
|
+
_addToHistory(fromUser, role, content) {
|
|
300
|
+
let history = this._getConversationHistory(fromUser);
|
|
301
|
+
|
|
302
|
+
// 截断过长的消息
|
|
303
|
+
let truncatedContent = content;
|
|
304
|
+
if (content.length > OpenClawClient.maxMessageLength) {
|
|
305
|
+
truncatedContent = content.substring(0, OpenClawClient.maxMessageLength) + '...';
|
|
306
|
+
this.log?.warn(`[OpenClawClient] 消息过长已截断: ${content.length} -> ${truncatedContent.length}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
history.push({ role, content: truncatedContent });
|
|
310
|
+
|
|
311
|
+
// 保持历史记录在限制范围内(保留最近的 N 轮对话)
|
|
312
|
+
// 每轮包含 user + assistant 两条消息
|
|
313
|
+
const maxMessages = OpenClawClient.maxHistoryRounds * 2;
|
|
314
|
+
if (history.length > maxMessages) {
|
|
315
|
+
// 移除最旧的消息对
|
|
316
|
+
history = history.slice(history.length - maxMessages);
|
|
317
|
+
this.log?.info(`[OpenClawClient] 历史记录已裁剪,保留最近 ${OpenClawClient.maxHistoryRounds} 轮`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
OpenClawClient.conversationHistory.set(fromUser, history);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 清空用户的会话历史
|
|
325
|
+
* @param {string} fromUser - 用户ID
|
|
326
|
+
*/
|
|
327
|
+
clearHistory(fromUser) {
|
|
328
|
+
OpenClawClient.conversationHistory.delete(fromUser);
|
|
329
|
+
this.log?.info(`[OpenClawClient] 已清空用户 ${fromUser} 的对话历史`);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 构建包含历史记录的 messages 数组
|
|
334
|
+
* @param {string} fromUser - 用户ID
|
|
335
|
+
* @param {string} currentMessage - 当前用户消息
|
|
336
|
+
* @returns {Array} 完整的 messages 数组
|
|
337
|
+
*/
|
|
338
|
+
_buildMessagesWithHistory(fromUser, currentMessage) {
|
|
339
|
+
const history = this._getConversationHistory(fromUser);
|
|
340
|
+
const messages = [...history];
|
|
341
|
+
|
|
342
|
+
// 添加当前消息
|
|
343
|
+
messages.push({ role: 'user', content: currentMessage });
|
|
344
|
+
|
|
345
|
+
this.log?.info(`[OpenClawClient] 构建消息上下文: 历史 ${history.length} 条 + 当前消息, 总共 ${messages.length} 条`);
|
|
346
|
+
return messages;
|
|
347
|
+
}
|
|
348
|
+
|
|
278
349
|
/**
|
|
279
350
|
* 确保 OpenClaw gateway 在运行
|
|
280
351
|
*/
|
|
@@ -368,10 +439,18 @@ class OpenClawClient {
|
|
|
368
439
|
'http://127.0.0.1:18789/v1/responses'
|
|
369
440
|
];
|
|
370
441
|
|
|
442
|
+
// 构建包含历史记录的完整消息
|
|
443
|
+
const messagesWithHistory = this._buildMessagesWithHistory(fromUser, message);
|
|
444
|
+
|
|
371
445
|
for (let i = 0; i < endpoints.length; i++) {
|
|
372
446
|
const apiUrl = endpoints[i];
|
|
373
447
|
try {
|
|
374
|
-
await this._doChatStream(apiUrl, gatewayToken, sessionId,
|
|
448
|
+
const fullText = await this._doChatStream(apiUrl, gatewayToken, sessionId, messagesWithHistory, onDelta, onDone);
|
|
449
|
+
|
|
450
|
+
// 成功响应后,保存当前对话到历史记录
|
|
451
|
+
this._addToHistory(fromUser, 'user', message);
|
|
452
|
+
this._addToHistory(fromUser, 'assistant', fullText);
|
|
453
|
+
|
|
375
454
|
return; // 成功则直接返回
|
|
376
455
|
} catch (err) {
|
|
377
456
|
const is404 = err.response?.status === 404;
|
|
@@ -452,7 +531,7 @@ class OpenClawClient {
|
|
|
452
531
|
}
|
|
453
532
|
}
|
|
454
533
|
|
|
455
|
-
async _doChatStream(apiUrl, gatewayToken, sessionId,
|
|
534
|
+
async _doChatStream(apiUrl, gatewayToken, sessionId, messages, onDelta, onDone) {
|
|
456
535
|
const headers = {
|
|
457
536
|
'Content-Type': 'application/json',
|
|
458
537
|
'Accept': 'text/event-stream'
|
|
@@ -461,37 +540,40 @@ class OpenClawClient {
|
|
|
461
540
|
headers['Authorization'] = `Bearer ${gatewayToken}`;
|
|
462
541
|
}
|
|
463
542
|
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
543
|
+
// messages 现在是数组,包含历史记录和当前消息
|
|
544
|
+
// 检测最后一条消息是否包含图片 URL
|
|
545
|
+
const lastMessage = messages[messages.length - 1];
|
|
546
|
+
const messageContent = typeof lastMessage.content === 'string' ? lastMessage.content : '';
|
|
547
|
+
const imageUrlMatch = messageContent.match(/\[图片\]\s*(https?:\/\/[^\s]+)/);
|
|
467
548
|
|
|
468
|
-
|
|
469
|
-
const isResponsesEndpoint = apiUrl.includes('/v1/responses');
|
|
549
|
+
let payload;
|
|
470
550
|
|
|
471
551
|
if (imageUrlMatch) {
|
|
472
552
|
// 多模态格式:图片 + 文本
|
|
473
553
|
const imageUrl = imageUrlMatch[1];
|
|
474
|
-
const textContent =
|
|
554
|
+
const textContent = messageContent.replace(/\[图片\]\s*https?:\/\/[^\s]+/, '').trim();
|
|
475
555
|
|
|
476
556
|
try {
|
|
477
557
|
// 下载图片并转换为 base64
|
|
478
558
|
const imageData = await this._downloadImageAsBase64(imageUrl);
|
|
479
559
|
|
|
480
|
-
//
|
|
560
|
+
// 构建包含历史的 messages,最后一条替换为多模态格式
|
|
561
|
+
const messagesWithImage = messages.slice(0, -1).concat([{
|
|
562
|
+
role: 'user',
|
|
563
|
+
content: [
|
|
564
|
+
{ type: 'text', text: textContent || '' },
|
|
565
|
+
{
|
|
566
|
+
type: 'image_url',
|
|
567
|
+
image_url: {
|
|
568
|
+
url: `data:${imageData.mediaType};base64,${imageData.base64}`
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
}]);
|
|
573
|
+
|
|
481
574
|
payload = {
|
|
482
575
|
model: 'openclaw',
|
|
483
|
-
messages:
|
|
484
|
-
role: 'user',
|
|
485
|
-
content: [
|
|
486
|
-
{ type: 'text', text: textContent || '描述这张图片' },
|
|
487
|
-
{
|
|
488
|
-
type: 'image_url',
|
|
489
|
-
image_url: {
|
|
490
|
-
url: `data:${imageData.mediaType};base64,${imageData.base64}`
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
]
|
|
494
|
-
}],
|
|
576
|
+
messages: messagesWithImage,
|
|
495
577
|
stream: true,
|
|
496
578
|
max_tokens: 2048
|
|
497
579
|
};
|
|
@@ -500,16 +582,16 @@ class OpenClawClient {
|
|
|
500
582
|
// 回退到纯文本模式
|
|
501
583
|
payload = {
|
|
502
584
|
model: 'openclaw',
|
|
503
|
-
messages:
|
|
585
|
+
messages: messages,
|
|
504
586
|
stream: true,
|
|
505
587
|
max_tokens: 2048
|
|
506
588
|
};
|
|
507
589
|
}
|
|
508
590
|
} else {
|
|
509
|
-
// 纯文本格式 -
|
|
591
|
+
// 纯文本格式 - 使用包含历史记录的 messages
|
|
510
592
|
payload = {
|
|
511
593
|
model: 'openclaw',
|
|
512
|
-
messages:
|
|
594
|
+
messages: messages,
|
|
513
595
|
stream: true,
|
|
514
596
|
max_tokens: 2048
|
|
515
597
|
};
|
|
@@ -617,7 +699,7 @@ class OpenClawClient {
|
|
|
617
699
|
}
|
|
618
700
|
try {
|
|
619
701
|
await onDone?.(fullText);
|
|
620
|
-
resolve();
|
|
702
|
+
resolve(fullText); // 返回完整文本,用于保存到历史记录
|
|
621
703
|
} catch (err) {
|
|
622
704
|
reject(err);
|
|
623
705
|
}
|