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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-subagent-service",
3
- "version": "0.0.111",
3
+ "version": "0.0.114",
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
  }
@@ -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, 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
+
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, message, onDelta, onDone) {
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
- // 检测消息是否包含图片 URL
465
- const imageUrlMatch = message.match(/\[图片\]\s*(https?:\/\/[^\s]+)/);
466
- 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]+)/);
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 = message.replace(/\[图片\]\s*https?:\/\/[^\s]+/, '').trim();
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
- // 统一使用 messages 格式(OpenAI 兼容标准)
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: [{ role: 'user', content: message }],
585
+ messages: messages,
504
586
  stream: true,
505
587
  max_tokens: 2048
506
588
  };
507
589
  }
508
590
  } else {
509
- // 纯文本格式 - 统一使用 messages 格式
591
+ // 纯文本格式 - 使用包含历史记录的 messages
510
592
  payload = {
511
593
  model: 'openclaw',
512
- messages: [{ role: 'user', content: message }],
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
  }