evolclaw 3.1.0 → 3.1.1

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.
@@ -398,6 +398,25 @@ export class AgentRunner {
398
398
  // 尝试发送交互卡片
399
399
  let cardSent = false;
400
400
  if (permCtx.adapter?.send) {
401
+ // 发送计划内容:找 plans 目录中最新修改的 .md 文件
402
+ if (sendPrompt) {
403
+ try {
404
+ const plansDir = path.join(process.env.HOME || '/root', '.claude', 'plans');
405
+ const files = fs.readdirSync(plansDir)
406
+ .filter((f) => f.endsWith('.md'))
407
+ .map((f) => ({ name: f, mtime: fs.statSync(path.join(plansDir, f)).mtimeMs }))
408
+ .sort((a, b) => b.mtime - a.mtime);
409
+ if (files.length > 0) {
410
+ const planContent = fs.readFileSync(path.join(plansDir, files[0].name), 'utf-8');
411
+ if (planContent.trim()) {
412
+ await sendPrompt(`📋 **计划内容**\n\n${planContent}`);
413
+ }
414
+ }
415
+ }
416
+ catch {
417
+ // 读取失败不影响后续审批流程
418
+ }
419
+ }
401
420
  const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
402
421
  const interaction = {
403
422
  type: 'interaction',
@@ -497,6 +516,8 @@ export class AgentRunner {
497
516
  let lastSessionId;
498
517
  // tool_use_id → tool_name 映射,用于从 SDKUserMessage 的 tool_result 块中还原工具名
499
518
  const toolUseNames = new Map();
519
+ let turnCount = 0;
520
+ const seenMessageIds = new Set();
500
521
  for await (const event of sdkStream) {
501
522
  // 提取 session_id(任意 SDK 事件都可能携带)
502
523
  if (event.session_id && event.session_id !== lastSessionId) {
@@ -523,15 +544,31 @@ export class AgentRunner {
523
544
  }
524
545
  // assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
525
546
  if (event.type === 'assistant' && event.message?.content) {
547
+ const msgId = event.message.id;
548
+ if (!msgId || !seenMessageIds.has(msgId)) {
549
+ if (msgId)
550
+ seenMessageIds.add(msgId);
551
+ turnCount++;
552
+ }
553
+ // 统计本轮 base agent 全部输出字符数(text + tool_use input)
554
+ let turnOutputChars = 0;
555
+ for (const content of event.message.content) {
556
+ if (content.type === 'tool_use') {
557
+ const inputStr = typeof content.input === 'string' ? content.input : JSON.stringify(content.input || '');
558
+ turnOutputChars += inputStr.length;
559
+ }
560
+ else if (content.type === 'text' && content.text) {
561
+ turnOutputChars += content.text.length;
562
+ }
563
+ }
526
564
  for (const content of event.message.content) {
527
565
  if (content.type === 'tool_use') {
528
- // 记录 id → name 映射,供后续 tool_result 使用
529
566
  if (content.id)
530
567
  toolUseNames.set(content.id, content.name);
531
- yield { type: 'tool_use', name: content.name, input: content.input, callId: content.id };
568
+ yield { type: 'tool_use', name: content.name, input: content.input, callId: content.id, turn: turnCount, outputTokens: turnOutputChars };
532
569
  }
533
570
  else if (content.type === 'text' && content.text) {
534
- yield { type: 'text', text: content.text };
571
+ yield { type: 'text', text: content.text, outputTokens: turnOutputChars, turn: turnCount };
535
572
  }
536
573
  }
537
574
  }
@@ -1,8 +1,21 @@
1
+ import path from 'path';
1
2
  import { createShortConnection } from '../rpc/index.js';
2
3
  import { uploadFileAndBuildPayload } from './upload.js';
4
+ import { appendMessageLog, buildOutboundEntry } from '../../core/message/message-log.js';
5
+ import { chatDirPath } from '../../core/session/session-fs-store.js';
6
+ import { resolvePaths } from '../../paths.js';
3
7
  export async function msgSend(args) {
4
8
  const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
5
9
  try {
10
+ // 1. 解析对端身份(30天缓存)
11
+ const { agentsDir } = resolvePaths();
12
+ const selfAgentDir = path.join(agentsDir, args.from);
13
+ const { PeerIdentityCache } = await import('../../core/relation/peer-identity.js');
14
+ const peerIdentity = await PeerIdentityCache.resolve('aun', args.to, selfAgentDir, conn, false);
15
+ // 2. 决定 chatmode(遵循来源1-3)
16
+ // 私聊:非 human 对端 → proactive,human 对端 → interactive
17
+ const chatmode = peerIdentity.isAgent ? 'proactive' : 'interactive';
18
+ // 3. 构建 payload
6
19
  let payload;
7
20
  switch (args.body.mode) {
8
21
  case 'text':
@@ -29,10 +42,35 @@ export async function msgSend(args) {
29
42
  break;
30
43
  }
31
44
  }
45
+ // 4. 写入 payload.chatmode
46
+ payload.chatmode = chatmode;
32
47
  const sendParams = { to: args.to, payload };
33
48
  // Default: plaintext. Set encrypt: true to enable E2EE.
34
49
  sendParams.encrypt = args.encrypt === true;
35
50
  const result = await conn.call('message.send', sendParams);
51
+ // 5. 写出方向 jsonl(与 daemon 一致格式,标记 source=cli)
52
+ if (result?.message_id) {
53
+ try {
54
+ const sessionsDir = resolvePaths().sessionsDir;
55
+ const chatDir = chatDirPath(sessionsDir, 'aun', args.to, args.from);
56
+ const textContent = args.body.mode === 'text' ? args.body.text
57
+ : args.body.mode === 'link' ? `[link] ${args.body.url}`
58
+ : args.body.mode === 'file' ? `[file] ${args.body.filePath}`
59
+ : `[payload]`;
60
+ appendMessageLog(chatDir, buildOutboundEntry({
61
+ from: args.from,
62
+ to: args.to,
63
+ chatType: 'private',
64
+ msgId: result.message_id,
65
+ content: textContent,
66
+ encrypt: args.encrypt === true,
67
+ chatmode, // 使用解析出的 chatmode
68
+ msgType: 'text',
69
+ source: 'cli',
70
+ }));
71
+ }
72
+ catch { }
73
+ }
36
74
  return {
37
75
  ok: true,
38
76
  message_id: result?.message_id,
@@ -6,14 +6,17 @@ import os from 'os';
6
6
  import { logger, localTimestamp } from '../utils/logger.js';
7
7
  import { LogWriter } from '../utils/log-writer.js';
8
8
  import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
9
- import { resolvePaths, getPackageRoot } from '../paths.js';
9
+ import { resolvePaths, getPackageRoot, agentDir as agentDirPath } from '../paths.js';
10
10
  import { saveToUploads, sanitizeFileName } from '../utils/media-cache.js';
11
11
  import { appendAidEvent } from '../utils/instance-registry.js';
12
+ import { appendMessageLog, buildOutboundEntry } from '../core/message/message-log.js';
13
+ import { chatDirPath } from '../core/session/session-fs-store.js';
12
14
  import { appendAidLifecycle } from '../aun/aid/identity.js';
13
15
  import { loadAgent, saveAgent } from '../config-store.js';
14
16
  import { getProcessStartTime } from '../utils/process-introspect.js';
15
17
  import * as outbox from '../aun/outbox.js';
16
18
  import { guessMime, formatSize } from '../utils/media-cache.js';
19
+ import { PeerIdentityCache } from '../core/relation/peer-identity.js';
17
20
  /**
18
21
  * 构造 connect extra_info:自描述本进程身份。
19
22
  *
@@ -67,6 +70,7 @@ export class AUNChannel {
67
70
  queuedHandler = null;
68
71
  pendingEchoMessages = new Map();
69
72
  isEchoSending = false;
73
+ agentDir;
70
74
  trace(dir, event, data) {
71
75
  if (!this.config.aunTrace)
72
76
  return;
@@ -404,8 +408,8 @@ export class AUNChannel {
404
408
  }
405
409
  return out;
406
410
  }
407
- buildGroupReplyContext(taskId, senderAid, encrypted, messageId) {
408
- const replyContext = { metadata: { encrypted } };
411
+ buildGroupReplyContext(taskId, senderAid, encrypted, messageId, chatmode) {
412
+ const replyContext = { metadata: { encrypted, chatmode } };
409
413
  if (taskId)
410
414
  replyContext.threadId = taskId;
411
415
  replyContext.peerId = senderAid;
@@ -470,6 +474,7 @@ export class AUNChannel {
470
474
  aidStatsCollector;
471
475
  constructor(config) {
472
476
  this.config = config;
477
+ this.agentDir = agentDirPath(config.aid);
473
478
  if (config.aunTrace) {
474
479
  this.traceWriter = new LogWriter({
475
480
  baseName: 'aun',
@@ -947,12 +952,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
947
952
  // 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
948
953
  // device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
949
954
  const chatId = fromAid;
950
- const peerInfo = await this.fetchPeerInfo(fromAid);
955
+ // 解析对端身份(30天缓存)
956
+ const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, this.agentDir, this.client, false);
951
957
  const shortAid = this.getShortAid(fromAid);
952
- const displayName = peerInfo.name || shortAid;
958
+ const displayName = peerIdentity.name || shortAid;
953
959
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
954
960
  const p2pPayloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
955
- logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${peerInfo.type || 'unknown'} payloadType=${p2pPayloadType} chatId=${chatId} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
961
+ logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${p2pPayloadType} chatId=${chatId} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
956
962
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
957
963
  if (p2pPayloadType === 'action_card_reply')
958
964
  return;
@@ -964,7 +970,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
964
970
  text: JSON.stringify(payload),
965
971
  chatType: 'private', messageId, seq,
966
972
  peerName: displayName || undefined,
967
- peerType: peerInfo.type || undefined,
973
+ peerType: peerIdentity.type,
968
974
  });
969
975
  return;
970
976
  }
@@ -974,11 +980,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
974
980
  logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
975
981
  return;
976
982
  }
977
- logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} text=${finalText.slice(0, 60)}`);
983
+ const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
984
+ logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
978
985
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
979
986
  const isSystemP2P = p2pPayloadType === 'event';
980
- this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P);
981
- const replyContext = { metadata: { encrypted: msgEncrypted } };
987
+ this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode);
988
+ const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode } };
982
989
  if (taskId)
983
990
  replyContext.threadId = taskId;
984
991
  this.dispatchMessage({
@@ -991,7 +998,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
991
998
  taskId,
992
999
  mentions,
993
1000
  peerName: displayName || undefined,
994
- peerType: peerInfo.type || 'unknown',
1001
+ peerType: peerIdentity.type,
995
1002
  replyContext,
996
1003
  });
997
1004
  }
@@ -1028,6 +1035,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1028
1035
  if (/echo/i.test(firstLineFast) && firstLineFast.trim().length <= 10 && !hasEvolClawTrace) {
1029
1036
  this.acknowledgeImmediately(messageId, seq);
1030
1037
  const msgEncryptedFast = !!(msg.e2ee);
1038
+ const msgChatmodeFast = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1031
1039
  const peerInfo = this.peerInfoCached(senderAid);
1032
1040
  const shortAid = this.getShortAid(senderAid);
1033
1041
  const displayName = peerInfo?.name || shortAid;
@@ -1043,7 +1051,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1043
1051
  peerName: displayName,
1044
1052
  peerType: peerInfo?.type || 'unknown',
1045
1053
  seq,
1046
- replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast, messageId),
1054
+ replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast, messageId, msgChatmodeFast),
1047
1055
  createdAt,
1048
1056
  });
1049
1057
  return;
@@ -1104,10 +1112,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1104
1112
  };
1105
1113
  let echoText = text;
1106
1114
  echoText += `\n${echoTs()} [EvolClaw.receive] from=${senderAid} mid=${messageId} chat=group self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
1115
+ const msgChatmodeEcho = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1107
1116
  this.pendingEchoMessages.set(messageId, {
1108
1117
  text: echoText,
1109
1118
  channelId: groupId,
1110
- context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId),
1119
+ context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho),
1111
1120
  receiveTs: Date.now(),
1112
1121
  });
1113
1122
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
@@ -1159,9 +1168,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1159
1168
  finalText = parts.join('\n\n');
1160
1169
  }
1161
1170
  }
1162
- const peerInfo = await this.fetchPeerInfo(senderAid);
1171
+ const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, this.agentDir, this.client, false);
1163
1172
  const shortAid = this.getShortAid(senderAid);
1164
- const displayName = peerInfo.name || shortAid;
1173
+ const displayName = peerIdentity.name || shortAid;
1165
1174
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
1166
1175
  const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1167
1176
  const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
@@ -1173,26 +1182,27 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1173
1182
  : mentionedSelf
1174
1183
  ? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
1175
1184
  : `${dispatchMode}.no-mention`;
1176
- logger.info(`${this.logPrefix()} Group dispatch decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerInfo.type || 'unknown'} payloadType=${payloadType} dispatchMode=${dispatchMode} reason=${reason} structMentions=${JSON.stringify(payloadMentions)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
1185
+ logger.info(`${this.logPrefix()} Group dispatch decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} dispatchMode=${dispatchMode} reason=${reason} structMentions=${JSON.stringify(payloadMentions)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
1177
1186
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1178
1187
  if (payloadType === 'action_card_reply')
1179
1188
  return;
1180
- logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} text=${finalText.slice(0, 60)}`);
1189
+ const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1190
+ logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
1181
1191
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: senderAid, msgId: messageId, kind: 'text', len: finalText.length, groupId });
1182
- this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event');
1192
+ this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode);
1183
1193
  this.dispatchMessage({
1184
1194
  channelId: groupId,
1185
1195
  groupId,
1186
1196
  userId: senderAid,
1187
1197
  peerName: displayName || undefined,
1188
- peerType: peerInfo.type || 'unknown',
1198
+ peerType: peerIdentity.type,
1189
1199
  text: finalText,
1190
1200
  chatType: 'group',
1191
1201
  messageId,
1192
1202
  seq,
1193
1203
  taskId,
1194
1204
  mentions,
1195
- replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted, messageId),
1205
+ replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted, messageId, msgChatmode),
1196
1206
  });
1197
1207
  }
1198
1208
  dispatchMessage(event) {
@@ -1624,9 +1634,6 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1624
1634
  await this.flushPendingEcho(channelId);
1625
1635
  }
1626
1636
  let finalText = text;
1627
- if (context?.title && (this.sentCount.get(channelId) || 0) > 0) {
1628
- finalText = '最终回复\n' + text;
1629
- }
1630
1637
  this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
1631
1638
  if (this.isGroupId(channelId) && context?.peerId) {
1632
1639
  if (!finalText.includes(`@${context.peerId}`)) {
@@ -1738,7 +1745,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1738
1745
  else {
1739
1746
  logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1740
1747
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
1741
- this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText);
1748
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1749
+ this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true);
1742
1750
  }
1743
1751
  }
1744
1752
  else {
@@ -1750,7 +1758,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1750
1758
  else {
1751
1759
  logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1752
1760
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId: result.message_id, kind: 'text', len: finalText.length });
1753
- this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText);
1761
+ this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1762
+ this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false);
1754
1763
  }
1755
1764
  }
1756
1765
  return true;
@@ -1792,6 +1801,33 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1792
1801
  }
1793
1802
  }
1794
1803
  }
1804
+ /** 出站消息写入 messages.jsonl(message.send/group.send/thought.put 成功后调用) */
1805
+ appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text') {
1806
+ try {
1807
+ const sessionsDir = resolvePaths().sessionsDir;
1808
+ const selfId = this.config.aid;
1809
+ const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfId);
1810
+ const chatmode = context?.metadata?.chatmode;
1811
+ appendMessageLog(chatDir, buildOutboundEntry({
1812
+ from: selfId,
1813
+ to: channelId,
1814
+ chatType: isGroup ? 'group' : 'private',
1815
+ groupId: isGroup ? channelId : null,
1816
+ msgId,
1817
+ content: text,
1818
+ replyTo: null,
1819
+ agent: null,
1820
+ model: null,
1821
+ durationMs: null,
1822
+ encrypt,
1823
+ chatmode,
1824
+ msgType,
1825
+ }));
1826
+ }
1827
+ catch (e) {
1828
+ logger.debug(`${this.logPrefix()} appendOutboundJsonl failed: ${e}`);
1829
+ }
1830
+ }
1795
1831
  /**
1796
1832
  * 发送 thought 内容(Proactive 模式可观测)
1797
1833
  * 群聊:调用 group.thought.put
@@ -1818,17 +1854,28 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1818
1854
  try {
1819
1855
  const itemCount = Array.isArray(payload?.items) ? payload.items.length : 0;
1820
1856
  const stage = payload?.stage ?? `items=${itemCount}`;
1857
+ // 提取 thought 文本(取最后一项的 text 或 content 字段)
1858
+ const items = payload?.items;
1859
+ let thoughtText;
1860
+ if (Array.isArray(items) && items.length > 0) {
1861
+ const lastItem = items[items.length - 1];
1862
+ thoughtText = lastItem?.text || lastItem?.content || (typeof lastItem === 'string' ? lastItem : undefined);
1863
+ }
1821
1864
  if (this.isGroupId(channelId)) {
1822
1865
  params.group_id = targetId;
1823
1866
  const putRes = await this.callAndTrace('group.thought.put', params);
1824
1867
  const tid = putRes?.thought_id;
1825
1868
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1869
+ this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
1870
+ // thought jsonl 写入已改为按 LLM 调用次数统计(在 complete 事件处写入),此处不再写
1826
1871
  }
1827
1872
  else {
1828
1873
  params.to = targetId;
1829
1874
  const putRes = await this.callAndTrace('message.thought.put', params);
1830
1875
  const tid = putRes?.thought_id;
1831
1876
  logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1877
+ this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
1878
+ // thought jsonl 写入已改为按 LLM 调用次数统计(在 complete 事件处写入),此处不再写
1832
1879
  }
1833
1880
  }
1834
1881
  catch (e) {
@@ -2085,7 +2132,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2085
2132
  // to avoid duplicate "已送达" at the sender CLI
2086
2133
  this.messageSeqMap.delete(messageId);
2087
2134
  }
2088
- sendProcessingStatus(channelId, status, sessionId, taskId, context) {
2135
+ sendProcessingStatus(channelId, status, sessionId, taskId, context, extraMeta) {
2089
2136
  if (status === 'start')
2090
2137
  this.sentCount.delete(channelId); // 新任务开始,重置计数
2091
2138
  if (!this.client || !this.connected)
@@ -2098,6 +2145,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2098
2145
  error: 'error',
2099
2146
  timeout: 'timeout',
2100
2147
  queued: 'queued',
2148
+ progress: 'progress',
2101
2149
  };
2102
2150
  const statusPayload = {
2103
2151
  type: 'status',
@@ -2105,6 +2153,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2105
2153
  task_id: taskId,
2106
2154
  session_id: sessionId,
2107
2155
  severity,
2156
+ ...(extraMeta && Object.keys(extraMeta).length > 0 && { metadata: extraMeta }),
2108
2157
  };
2109
2158
  if (context?.threadId)
2110
2159
  statusPayload.thread_id = context.threadId;
@@ -2155,7 +2204,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2155
2204
  const chatmode = context?.metadata?.chatmode ?? '?';
2156
2205
  const initiator = statusPayload.initiator ?? '';
2157
2206
  const refMsgId = statusPayload.ref_message_id ?? '';
2158
- logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel} initiator=${initiator} ref_msg=${refMsgId}`);
2207
+ const metaStr = statusPayload.metadata ? ` meta=${JSON.stringify(statusPayload.metadata)}` : '';
2208
+ logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel} initiator=${initiator} ref_msg=${refMsgId}${metaStr}`);
2159
2209
  }
2160
2210
  sendCustomPayload(channelId, payload) {
2161
2211
  if (!this.client || !this.connected)
@@ -2421,6 +2471,9 @@ export class AUNChannelPlugin {
2421
2471
  }
2422
2472
  return;
2423
2473
  }
2474
+ case 'status.progress':
2475
+ channel.sendProcessingStatus(channelId, 'progress', envelope.taskId, envelope.taskId, ctx, payload.metadata);
2476
+ return;
2424
2477
  case 'status.started':
2425
2478
  channel.sendProcessingStatus(channelId, 'start', envelope.taskId, envelope.taskId, ctx);
2426
2479
  return;
@@ -482,6 +482,7 @@ export class DingtalkChannelPlugin {
482
482
  case 'status.interrupted':
483
483
  case 'status.error':
484
484
  case 'status.timeout':
485
+ case 'status.progress':
485
486
  case 'custom':
486
487
  return;
487
488
  default:
@@ -219,7 +219,7 @@ export class FeishuChannel {
219
219
  const imageData = await this.downloadAndSaveImage(imageKey, msg.chat_id, msg.message_id, projectPath);
220
220
  if (imageData) {
221
221
  const allImages = [...quotedImages, imageData];
222
- const prompt = quotedText + '用户发送了一张图片,请分析这张图片的内容。';
222
+ const prompt = quotedText + '用户发送了一张图片,请结合上下文理解用户意图并回应。';
223
223
  await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: allImages, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
224
224
  }
225
225
  else {
@@ -573,7 +573,14 @@ export class FeishuChannel {
573
573
  const truncated = content.slice(0, 28000) + '\n\n⚠️ 消息过长,已截断';
574
574
  return this.sendMessage(chatId, truncated, options);
575
575
  }
576
- logger.error('[Feishu] Failed to send message:', error);
576
+ const respData = error?.response?.data;
577
+ const code = respData?.code;
578
+ logger.error('[Feishu] Failed to send message:', respData ? JSON.stringify(respData) : error?.message ?? error);
579
+ // post 格式相关错误(400/230001):降级为纯文本重试
580
+ if (!options?.forceText && (error?.response?.status === 400 || code === 230001)) {
581
+ logger.warn('[Feishu] Retrying as plain text (forceText)');
582
+ return this.sendMessage(chatId, content, { ...options, forceText: true });
583
+ }
577
584
  throw error;
578
585
  }
579
586
  }
@@ -1357,6 +1364,7 @@ export class FeishuChannelPlugin {
1357
1364
  case 'status.interrupted':
1358
1365
  case 'status.error':
1359
1366
  case 'status.timeout':
1367
+ case 'status.progress':
1360
1368
  // Feishu 通过 acknowledge (✓ 表情) 表达状态,由 channel 自行处理
1361
1369
  return;
1362
1370
  case 'interaction':
@@ -369,6 +369,7 @@ export class QQBotChannelPlugin {
369
369
  case 'status.interrupted':
370
370
  case 'status.error':
371
371
  case 'status.timeout':
372
+ case 'status.progress':
372
373
  case 'custom':
373
374
  return;
374
375
  default:
@@ -766,6 +766,7 @@ export class WechatChannelPlugin {
766
766
  case 'status.interrupted':
767
767
  case 'status.error':
768
768
  case 'status.timeout':
769
+ case 'status.progress':
769
770
  case 'custom':
770
771
  return;
771
772
  default:
@@ -525,6 +525,7 @@ export class WecomChannelPlugin {
525
525
  case 'status.interrupted':
526
526
  case 'status.error':
527
527
  case 'status.timeout':
528
+ case 'status.progress':
528
529
  case 'custom':
529
530
  return;
530
531
  default: