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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-subagent-service",
3
- "version": "0.0.110",
3
+ "version": "0.0.113",
4
4
  "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -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/responses',
369
- 'http://127.0.0.1:18789/v1/chat/completions'
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, message, onDelta, onDone);
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
- const errorData = err.response.data;
398
- if (typeof errorData === 'string') {
399
- this.log?.error(`[OpenClawClient] 错误响应: ${errorData}`);
400
- } else if (errorData && typeof errorData === 'object') {
401
- // 提取常见错误字段
402
- const errorMsg = errorData.error?.message || errorData.message || errorData.error || JSON.stringify(errorData);
403
- this.log?.error(`[OpenClawClient] 错误响应: ${errorMsg}`);
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, message, onDelta, onDone) {
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
- // 检测消息是否包含图片 URL
450
- const imageUrlMatch = message.match(/\[图片\]\s*(https?:\/\/[^\s]+)/);
451
- let payload;
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 = message.replace(/\[图片\]\s*https?:\/\/[^\s]+/, '').trim();
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
- if (isResponsesEndpoint) {
466
- // /v1/responses 端点格式(推荐,支持多模态)
467
- payload = {
468
- model: 'openclaw',
469
- input: [
470
- { type: 'input_text', text: textContent || '描述这张图片' },
471
- {
472
- type: 'input_image',
473
- source: {
474
- type: 'base64',
475
- media_type: imageData.mediaType,
476
- data: imageData.base64 // 纯 base64,不带 data URI 前缀
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
- input: [
514
- { type: 'input_text', text: message }
515
- ],
576
+ messages: messagesWithImage,
516
577
  stream: true,
517
578
  max_tokens: 2048
518
579
  };
519
- } else {
580
+ } catch (err) {
581
+ this.log?.warn(`[OpenClawClient] 图片处理失败,回退到文本模式: ${err.message}`);
582
+ // 回退到纯文本模式
520
583
  payload = {
521
584
  model: 'openclaw',
522
- messages: [{ role: 'user', content: message }],
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
  }