claw-subagent-service 0.0.110 → 0.0.113
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
|
}
|
|
@@ -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
|
*/
|
|
@@ -363,16 +434,23 @@ class OpenClawClient {
|
|
|
363
434
|
const sessionId = `clawmessenger-${fromUser}`;
|
|
364
435
|
|
|
365
436
|
// 尝试多个可能的 SSE 端点,兼容不同版本 OpenClaw Gateway
|
|
366
|
-
// 注意:多模态图片必须使用 /v1/responses 端点
|
|
367
437
|
const endpoints = [
|
|
368
|
-
'http://127.0.0.1:18789/v1/
|
|
369
|
-
'http://127.0.0.1:18789/v1/
|
|
438
|
+
'http://127.0.0.1:18789/v1/chat/completions',
|
|
439
|
+
'http://127.0.0.1:18789/v1/responses'
|
|
370
440
|
];
|
|
371
441
|
|
|
442
|
+
// 构建包含历史记录的完整消息
|
|
443
|
+
const messagesWithHistory = this._buildMessagesWithHistory(fromUser, message);
|
|
444
|
+
|
|
372
445
|
for (let i = 0; i < endpoints.length; i++) {
|
|
373
446
|
const apiUrl = endpoints[i];
|
|
374
447
|
try {
|
|
375
|
-
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
|
+
|
|
376
454
|
return; // 成功则直接返回
|
|
377
455
|
} catch (err) {
|
|
378
456
|
const is404 = err.response?.status === 404;
|
|
@@ -394,13 +472,29 @@ class OpenClawClient {
|
|
|
394
472
|
if (err.response) {
|
|
395
473
|
this.log?.error(`[OpenClawClient] 错误状态码: ${err.response.status}`);
|
|
396
474
|
// 安全地提取错误信息
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
475
|
+
try {
|
|
476
|
+
const errorData = err.response.data;
|
|
477
|
+
if (typeof errorData === 'string') {
|
|
478
|
+
this.log?.error(`[OpenClawClient] 错误响应: ${errorData}`);
|
|
479
|
+
} else if (errorData && typeof errorData === 'object') {
|
|
480
|
+
// 检查是否是 IncomingMessage 对象(流)
|
|
481
|
+
if (errorData._readableState || errorData.socket) {
|
|
482
|
+
// 这是一个流对象,尝试读取其中的数据
|
|
483
|
+
const buffer = errorData._readableState?.buffer;
|
|
484
|
+
if (buffer && buffer.length > 0) {
|
|
485
|
+
const dataStr = buffer[0].toString('utf8');
|
|
486
|
+
this.log?.error(`[OpenClawClient] 错误响应(流): ${dataStr}`);
|
|
487
|
+
} else {
|
|
488
|
+
this.log?.error(`[OpenClawClient] 错误响应(流对象,无法读取)`);
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
// 提取常见错误字段
|
|
492
|
+
const errorMsg = errorData.error?.message || errorData.message || errorData.error || JSON.stringify(errorData);
|
|
493
|
+
this.log?.error(`[OpenClawClient] 错误响应: ${errorMsg}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} catch (e) {
|
|
497
|
+
this.log?.error(`[OpenClawClient] 无法解析错误响应: ${e.message}`);
|
|
404
498
|
}
|
|
405
499
|
}
|
|
406
500
|
}
|
|
@@ -437,7 +531,7 @@ class OpenClawClient {
|
|
|
437
531
|
}
|
|
438
532
|
}
|
|
439
533
|
|
|
440
|
-
async _doChatStream(apiUrl, gatewayToken, sessionId,
|
|
534
|
+
async _doChatStream(apiUrl, gatewayToken, sessionId, messages, onDelta, onDone) {
|
|
441
535
|
const headers = {
|
|
442
536
|
'Content-Type': 'application/json',
|
|
443
537
|
'Accept': 'text/event-stream'
|
|
@@ -446,84 +540,61 @@ class OpenClawClient {
|
|
|
446
540
|
headers['Authorization'] = `Bearer ${gatewayToken}`;
|
|
447
541
|
}
|
|
448
542
|
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
|
|
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]+)/);
|
|
452
548
|
|
|
453
|
-
|
|
454
|
-
const isResponsesEndpoint = apiUrl.includes('/v1/responses');
|
|
549
|
+
let payload;
|
|
455
550
|
|
|
456
551
|
if (imageUrlMatch) {
|
|
457
552
|
// 多模态格式:图片 + 文本
|
|
458
553
|
const imageUrl = imageUrlMatch[1];
|
|
459
|
-
const textContent =
|
|
554
|
+
const textContent = messageContent.replace(/\[图片\]\s*https?:\/\/[^\s]+/, '').trim();
|
|
460
555
|
|
|
461
556
|
try {
|
|
462
557
|
// 下载图片并转换为 base64
|
|
463
558
|
const imageData = await this._downloadImageAsBase64(imageUrl);
|
|
464
559
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
],
|
|
480
|
-
stream: true,
|
|
481
|
-
max_tokens: 2048
|
|
482
|
-
};
|
|
483
|
-
} else {
|
|
484
|
-
// /v1/chat/completions 端点格式(兼容旧版)
|
|
485
|
-
payload = {
|
|
486
|
-
model: 'openclaw',
|
|
487
|
-
messages: [{
|
|
488
|
-
role: 'user',
|
|
489
|
-
content: [
|
|
490
|
-
{ type: 'text', text: textContent || '描述这张图片' },
|
|
491
|
-
{ type: 'image_url', image_url: { url: `data:${imageData.mediaType};base64,${imageData.base64}` } }
|
|
492
|
-
]
|
|
493
|
-
}],
|
|
494
|
-
stream: true,
|
|
495
|
-
max_tokens: 2048
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
} catch (err) {
|
|
499
|
-
this.log?.warn(`[OpenClawClient] 图片处理失败,回退到文本模式: ${err.message}`);
|
|
500
|
-
// 回退到纯文本模式
|
|
501
|
-
payload = {
|
|
502
|
-
model: 'openclaw',
|
|
503
|
-
messages: [{ role: 'user', content: message }],
|
|
504
|
-
stream: true,
|
|
505
|
-
max_tokens: 2048
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
} else {
|
|
509
|
-
// 纯文本格式
|
|
510
|
-
if (isResponsesEndpoint) {
|
|
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
|
+
|
|
511
574
|
payload = {
|
|
512
575
|
model: 'openclaw',
|
|
513
|
-
|
|
514
|
-
{ type: 'input_text', text: message }
|
|
515
|
-
],
|
|
576
|
+
messages: messagesWithImage,
|
|
516
577
|
stream: true,
|
|
517
578
|
max_tokens: 2048
|
|
518
579
|
};
|
|
519
|
-
}
|
|
580
|
+
} catch (err) {
|
|
581
|
+
this.log?.warn(`[OpenClawClient] 图片处理失败,回退到文本模式: ${err.message}`);
|
|
582
|
+
// 回退到纯文本模式
|
|
520
583
|
payload = {
|
|
521
584
|
model: 'openclaw',
|
|
522
|
-
messages:
|
|
585
|
+
messages: messages,
|
|
523
586
|
stream: true,
|
|
524
587
|
max_tokens: 2048
|
|
525
588
|
};
|
|
526
589
|
}
|
|
590
|
+
} else {
|
|
591
|
+
// 纯文本格式 - 使用包含历史记录的 messages
|
|
592
|
+
payload = {
|
|
593
|
+
model: 'openclaw',
|
|
594
|
+
messages: messages,
|
|
595
|
+
stream: true,
|
|
596
|
+
max_tokens: 2048
|
|
597
|
+
};
|
|
527
598
|
}
|
|
528
599
|
|
|
529
600
|
this.log?.info(`[OpenClawClient] SSE 请求 payload: ${JSON.stringify(payload)}`);
|
|
@@ -628,7 +699,7 @@ class OpenClawClient {
|
|
|
628
699
|
}
|
|
629
700
|
try {
|
|
630
701
|
await onDone?.(fullText);
|
|
631
|
-
resolve();
|
|
702
|
+
resolve(fullText); // 返回完整文本,用于保存到历史记录
|
|
632
703
|
} catch (err) {
|
|
633
704
|
reject(err);
|
|
634
705
|
}
|