evolclaw 3.1.0 → 3.1.2

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +407 -0
  2. package/README.md +1 -1
  3. package/SKILLS.md +311 -0
  4. package/dist/agents/claude-runner.js +40 -3
  5. package/dist/aun/aid/agentmd.js +7 -6
  6. package/dist/aun/aid/client.js +5 -11
  7. package/dist/aun/aid/identity.js +32 -13
  8. package/dist/aun/msg/group.js +1 -0
  9. package/dist/aun/msg/p2p.js +51 -0
  10. package/dist/aun/msg/upload.js +57 -18
  11. package/dist/channels/aun.js +124 -50
  12. package/dist/channels/dingtalk.js +2 -0
  13. package/dist/channels/feishu.js +15 -6
  14. package/dist/channels/qqbot.js +2 -0
  15. package/dist/channels/wechat.js +2 -0
  16. package/dist/channels/wecom.js +2 -0
  17. package/dist/cli/agent.js +130 -35
  18. package/dist/cli/index.js +221 -48
  19. package/dist/cli/init-channel.js +4 -2
  20. package/dist/cli/init.js +44 -23
  21. package/dist/cli/watch-msg.js +109 -30
  22. package/dist/config-store.js +67 -1
  23. package/dist/core/channel-loader.js +4 -4
  24. package/dist/core/command-handler.js +95 -84
  25. package/dist/core/evolagent-registry.js +45 -9
  26. package/dist/core/evolagent.js +4 -4
  27. package/dist/core/message/im-renderer.js +47 -8
  28. package/dist/core/message/message-bridge.js +30 -1
  29. package/dist/core/message/message-log.js +6 -1
  30. package/dist/core/message/message-processor.js +29 -35
  31. package/dist/core/relation/peer-identity.js +161 -0
  32. package/dist/core/session/session-fs-store.js +23 -0
  33. package/dist/core/session/session-manager.js +11 -4
  34. package/dist/core/trigger/manager.js +16 -0
  35. package/dist/core/trigger/parser.js +110 -0
  36. package/dist/core/trigger/scheduler.js +6 -0
  37. package/dist/index.js +64 -20
  38. package/dist/paths.js +35 -0
  39. package/dist/utils/cross-platform.js +2 -1
  40. package/dist/utils/error-utils.js +17 -13
  41. package/dist/utils/stats.js +216 -2
  42. package/kits/docs/INDEX.md +6 -0
  43. package/kits/docs/evolclaw/MSG_PRIVATE.md +53 -6
  44. package/kits/rules/06-channel.md +30 -0
  45. package/package.json +6 -3
@@ -3,21 +3,19 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import { guessMime, formatSize } from '../../utils/media-cache.js';
5
5
  import { inferPayloadType, isValidPayloadType } from './payload-type.js';
6
- /** 小文件阈值:≤64KB 走 storage.put_object 内联 base64;>64KB 走 create_upload_session + HTTP PUT。 */
7
- const INLINE_UPLOAD_LIMIT = 64 * 1024;
8
6
  /** 单次上传最大大小(与 daemon sendFile 一致)。 */
9
7
  const MAX_FILE_SIZE = 10 * 1024 * 1024;
8
+ /** server 端 inline 上限错误的识别特征。优先匹配错误消息里的“inline 上限”字样。 */
9
+ function isInlineLimitError(err) {
10
+ const msg = err?.message ?? '';
11
+ return /inline\s*上限|inline limit|create_upload_session/i.test(msg);
12
+ }
10
13
  /**
11
14
  * 上传本地文件并构造发送用的 payload。
12
15
  *
13
- * 流程:
14
- * 1. 读文件、算 sha256、推断 content_type
15
- * 2. 小文件 storage.put_object,大文件 storage.create_upload_session + HTTP PUT + storage.complete_upload
16
- * 3. 按 as / 扩展名 确定 payload.type
17
- * 4. 构造 payload(含 attachments 引用)
18
- *
19
- * 不做 outbox 持久化、不做 E2EE 加密兜底——这些是 daemon 的职责。
20
- * CLI 短连接场景假定网络稳定,失败抛异常给调用方处理。
16
+ * 策略:先无脑 storage.put_object(内联 base64)。server 报“超过 inline 上限”
17
+ * 时降级到 storage.create_upload_session + HTTP PUT + storage.complete_upload,
18
+ * 此时通过 onProgress 上报字节级进度。
21
19
  */
22
20
  export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts) {
23
21
  const absPath = path.resolve(filePath);
@@ -29,14 +27,19 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
29
27
  throw new Error(`文件为空: ${absPath}`);
30
28
  }
31
29
  if (stat.size > MAX_FILE_SIZE) {
32
- throw new Error(`文件过大 (${formatSize(stat.size)}, ${formatSize(MAX_FILE_SIZE)}): ${absPath}`);
30
+ throw new Error(`文件过大 (${formatSize(stat.size)}, 上限 ${formatSize(MAX_FILE_SIZE)}): ${absPath}`);
33
31
  }
34
32
  const filename = path.basename(absPath);
35
33
  const fileData = fs.readFileSync(absPath);
36
34
  const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
37
35
  const contentType = opts?.contentType ?? guessMime(filename);
38
36
  const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
39
- if (stat.size <= INLINE_UPLOAD_LIMIT) {
37
+ const total = stat.size;
38
+ const report = (phase, bytes) => opts?.onProgress?.({ phase, bytes, total });
39
+ // 1. 先按 inline 走
40
+ let inlineRejected = false;
41
+ try {
42
+ report('inline', 0);
40
43
  await conn.call('storage.put_object', {
41
44
  object_key: objectKey,
42
45
  content: fileData.toString('base64'),
@@ -44,32 +47,68 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
44
47
  is_private: false,
45
48
  overwrite: true,
46
49
  });
50
+ report('inline', total);
47
51
  }
48
- else {
52
+ catch (e) {
53
+ if (!isInlineLimitError(e))
54
+ throw e;
55
+ inlineRejected = true;
56
+ }
57
+ // 2. inline 被拒:降级到 session + HTTP PUT
58
+ if (inlineRejected) {
59
+ report('session-create', 0);
49
60
  const session = await conn.call('storage.create_upload_session', {
50
61
  object_key: objectKey,
51
- size_bytes: stat.size,
62
+ size_bytes: total,
52
63
  content_type: contentType,
53
64
  });
54
65
  const uploadUrl = session?.upload_url;
55
66
  if (!uploadUrl)
56
67
  throw new Error('storage.create_upload_session 未返回 upload_url');
57
- const uploadResp = await fetch(uploadUrl, { method: 'PUT', body: fileData });
68
+ // 简单 Buffer 上传 + 周期性进度(不用流,避免某些 storage 网关对 chunked / duplex 不友好)
69
+ report('http-put', 0);
70
+ const PROGRESS_TICK_MS = 250;
71
+ let lastBytes = 0;
72
+ const tickerStart = Date.now();
73
+ // 简单的“估算”进度:实际上一次性发,但在等待响应期间按 elapsed 模拟字节数,让用户看到在跑
74
+ const ticker = setInterval(() => {
75
+ const elapsed = Date.now() - tickerStart;
76
+ // 假设 2MB/s 估算;不超过 99%
77
+ const estimated = Math.min(total - 1, Math.floor((elapsed / 1000) * 2 * 1024 * 1024));
78
+ if (estimated > lastBytes) {
79
+ lastBytes = estimated;
80
+ report('http-put', estimated);
81
+ }
82
+ }, PROGRESS_TICK_MS);
83
+ let uploadResp;
84
+ try {
85
+ uploadResp = await fetch(uploadUrl, {
86
+ method: 'PUT',
87
+ body: new Blob([new Uint8Array(fileData)], { type: contentType }),
88
+ headers: { 'Content-Type': contentType },
89
+ });
90
+ }
91
+ finally {
92
+ clearInterval(ticker);
93
+ }
58
94
  if (!uploadResp.ok)
59
95
  throw new Error(`HTTP 上传失败: ${uploadResp.status}`);
96
+ report('http-put', total);
97
+ report('session-complete', total);
60
98
  await conn.call('storage.complete_upload', {
61
99
  object_key: objectKey,
62
100
  sha256,
63
101
  content_type: contentType,
64
102
  is_private: false,
65
- size_bytes: stat.size,
103
+ size_bytes: total,
66
104
  });
67
105
  }
106
+ report('done', total);
68
107
  const attachment = {
69
108
  owner_aid: ownerAid,
70
109
  object_key: objectKey,
71
110
  filename,
72
- size_bytes: stat.size,
111
+ size_bytes: total,
73
112
  sha256,
74
113
  content_type: contentType,
75
114
  };
@@ -91,7 +130,7 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
91
130
  if (opts?.text)
92
131
  payload.text = opts.text;
93
132
  else if (type === 'file')
94
- payload.text = `📎 ${filename} (${formatSize(stat.size)})`;
133
+ payload.text = `📎 ${filename} (${formatSize(total)})`;
95
134
  if (type === 'voice' && opts?.transcript)
96
135
  payload.transcript = opts.transcript;
97
136
  return { payload, type, attachment };
@@ -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, agentMdPath as agentMdPathFn, 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,10 +408,10 @@ export class AUNChannel {
404
408
  }
405
409
  return out;
406
410
  }
407
- buildGroupReplyContext(taskId, senderAid, encrypted, messageId) {
408
- const replyContext = { metadata: { encrypted } };
409
- if (taskId)
410
- replyContext.threadId = taskId;
411
+ buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode) {
412
+ const replyContext = { metadata: { encrypted, chatmode } };
413
+ if (threadId)
414
+ replyContext.threadId = threadId;
411
415
  replyContext.peerId = senderAid;
412
416
  if (messageId)
413
417
  replyContext.replyToMessageId = messageId;
@@ -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',
@@ -726,8 +731,8 @@ export class AUNChannel {
726
731
  logger.info(`${this.logPrefix()} No owner configured, skipping welcome message (will retry after auto-bind)`);
727
732
  return;
728
733
  }
729
- const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
730
- const existingAgentMd = fs.existsSync(agentMdPath) ? fs.readFileSync(agentMdPath, 'utf-8') : '';
734
+ const agentMdLocalPath = agentMdPathFn(aidName);
735
+ const existingAgentMd = fs.existsSync(agentMdLocalPath) ? fs.readFileSync(agentMdLocalPath, 'utf-8') : '';
731
736
  const existingFrontmatterMatch = existingAgentMd.match(/^---\n([\s\S]*?)\n---/);
732
737
  const existingFrontmatter = existingFrontmatterMatch?.[1] ?? '';
733
738
  // Fetch owner's agent.md to derive name and validate type
@@ -773,8 +778,8 @@ tags:
773
778
  EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
774
779
  `;
775
780
  // Write locally
776
- fs.mkdirSync(path.dirname(agentMdPath), { recursive: true });
777
- fs.writeFileSync(agentMdPath, newAgentMd, 'utf-8');
781
+ fs.mkdirSync(path.dirname(agentMdLocalPath), { recursive: true });
782
+ fs.writeFileSync(agentMdLocalPath, newAgentMd, 'utf-8');
778
783
  logger.info(`${this.logPrefix()} Updated agent.md for ${aidName}`);
779
784
  // Publish to AUN network via auth.uploadAgentMd
780
785
  try {
@@ -903,7 +908,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
903
908
  const fromAid = msg.from ?? '';
904
909
  const payload = msg.payload ?? '';
905
910
  const text = this.extractTextPayload(payload, fromAid);
906
- const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
911
+ const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
907
912
  const messageId = msg.message_id ?? '';
908
913
  const seq = msg.seq;
909
914
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
@@ -947,12 +952,14 @@ 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 selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
957
+ const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.client, false);
951
958
  const shortAid = this.getShortAid(fromAid);
952
- const displayName = peerInfo.name || shortAid;
959
+ const displayName = peerIdentity.name || shortAid;
953
960
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
954
961
  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))}`);
962
+ 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
963
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
957
964
  if (p2pPayloadType === 'action_card_reply')
958
965
  return;
@@ -964,7 +971,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
964
971
  text: JSON.stringify(payload),
965
972
  chatType: 'private', messageId, seq,
966
973
  peerName: displayName || undefined,
967
- peerType: peerInfo.type || undefined,
974
+ peerType: peerIdentity.type,
968
975
  });
969
976
  return;
970
977
  }
@@ -974,13 +981,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
974
981
  logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
975
982
  return;
976
983
  }
977
- logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} text=${finalText.slice(0, 60)}`);
984
+ const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
985
+ logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
978
986
  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
987
  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 } };
982
- if (taskId)
983
- replyContext.threadId = taskId;
988
+ this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode);
989
+ const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode } };
990
+ if (threadId)
991
+ replyContext.threadId = threadId;
984
992
  this.dispatchMessage({
985
993
  channelId: chatId,
986
994
  userId: fromAid,
@@ -988,10 +996,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
988
996
  chatType: 'private',
989
997
  messageId,
990
998
  seq,
991
- taskId,
999
+ threadId,
992
1000
  mentions,
993
1001
  peerName: displayName || undefined,
994
- peerType: peerInfo.type || 'unknown',
1002
+ peerType: peerIdentity.type,
995
1003
  replyContext,
996
1004
  });
997
1005
  }
@@ -1003,7 +1011,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1003
1011
  const senderAid = msg.sender_aid ?? '';
1004
1012
  const payload = msg.payload ?? '';
1005
1013
  const text = this.extractTextPayload(payload, groupId, senderAid);
1006
- const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1014
+ const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1007
1015
  const messageId = msg.message_id ?? '';
1008
1016
  const seq = msg.seq;
1009
1017
  // Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
@@ -1028,6 +1036,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1028
1036
  if (/echo/i.test(firstLineFast) && firstLineFast.trim().length <= 10 && !hasEvolClawTrace) {
1029
1037
  this.acknowledgeImmediately(messageId, seq);
1030
1038
  const msgEncryptedFast = !!(msg.e2ee);
1039
+ const msgChatmodeFast = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1031
1040
  const peerInfo = this.peerInfoCached(senderAid);
1032
1041
  const shortAid = this.getShortAid(senderAid);
1033
1042
  const displayName = peerInfo?.name || shortAid;
@@ -1043,7 +1052,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1043
1052
  peerName: displayName,
1044
1053
  peerType: peerInfo?.type || 'unknown',
1045
1054
  seq,
1046
- replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast, messageId),
1055
+ replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast, messageId, msgChatmodeFast),
1047
1056
  createdAt,
1048
1057
  });
1049
1058
  return;
@@ -1104,10 +1113,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1104
1113
  };
1105
1114
  let echoText = text;
1106
1115
  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'}`;
1116
+ const msgChatmodeEcho = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1107
1117
  this.pendingEchoMessages.set(messageId, {
1108
1118
  text: echoText,
1109
1119
  channelId: groupId,
1110
- context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId),
1120
+ context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho),
1111
1121
  receiveTs: Date.now(),
1112
1122
  });
1113
1123
  // 继续走正常 Agent 流程(下面的代码会 dispatch)
@@ -1159,9 +1169,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1159
1169
  finalText = parts.join('\n\n');
1160
1170
  }
1161
1171
  }
1162
- const peerInfo = await this.fetchPeerInfo(senderAid);
1172
+ const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
1173
+ const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.client, false);
1163
1174
  const shortAid = this.getShortAid(senderAid);
1164
- const displayName = peerInfo.name || shortAid;
1175
+ const displayName = peerIdentity.name || shortAid;
1165
1176
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
1166
1177
  const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
1167
1178
  const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
@@ -1173,26 +1184,27 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1173
1184
  : mentionedSelf
1174
1185
  ? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
1175
1186
  : `${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))}`);
1187
+ 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
1188
  // action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
1178
1189
  if (payloadType === 'action_card_reply')
1179
1190
  return;
1180
- logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} text=${finalText.slice(0, 60)}`);
1191
+ const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
1192
+ logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
1181
1193
  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');
1194
+ this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode);
1183
1195
  this.dispatchMessage({
1184
1196
  channelId: groupId,
1185
1197
  groupId,
1186
1198
  userId: senderAid,
1187
1199
  peerName: displayName || undefined,
1188
- peerType: peerInfo.type || 'unknown',
1200
+ peerType: peerIdentity.type,
1189
1201
  text: finalText,
1190
1202
  chatType: 'group',
1191
1203
  messageId,
1192
1204
  seq,
1193
- taskId,
1205
+ threadId,
1194
1206
  mentions,
1195
- replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted, messageId),
1207
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
1196
1208
  });
1197
1209
  }
1198
1210
  dispatchMessage(event) {
@@ -1242,8 +1254,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1242
1254
  // Use caller-supplied replyContext (group path builds mentionUserIds);
1243
1255
  // fall back to simple threadId-only context for private messages
1244
1256
  let replyContext = event.replyContext;
1245
- if (!replyContext && event.taskId) {
1246
- replyContext = { threadId: event.taskId };
1257
+ if (!replyContext && event.threadId) {
1258
+ replyContext = { threadId: event.threadId };
1247
1259
  }
1248
1260
  this.messageHandler({
1249
1261
  channelId: event.channelId || '',
@@ -1256,7 +1268,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1256
1268
  peerName: event.peerName,
1257
1269
  peerType: event.peerType,
1258
1270
  messageId: event.messageId,
1259
- threadId: event.taskId,
1271
+ threadId: event.threadId,
1260
1272
  mentions: mentionObjects,
1261
1273
  replyContext,
1262
1274
  }).catch(err => {
@@ -1624,9 +1636,6 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1624
1636
  await this.flushPendingEcho(channelId);
1625
1637
  }
1626
1638
  let finalText = text;
1627
- if (context?.title && (this.sentCount.get(channelId) || 0) > 0) {
1628
- finalText = '最终回复\n' + text;
1629
- }
1630
1639
  this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
1631
1640
  if (this.isGroupId(channelId) && context?.peerId) {
1632
1641
  if (!finalText.includes(`@${context.peerId}`)) {
@@ -1700,6 +1709,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1700
1709
  const channelId = entry.channelId;
1701
1710
  const finalText = entry.text;
1702
1711
  const context = entry.context;
1712
+ // 从 context.metadata.source 读取 source,默认为 'daemon'
1713
+ const source = context?.metadata?.source ?? 'daemon';
1703
1714
  const payload = { type: 'text', text: finalText };
1704
1715
  if (this.isGroupId(channelId)) {
1705
1716
  const extracted = this.extractMentionAidsFromText(finalText);
@@ -1713,7 +1724,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1713
1724
  if (context?.metadata?.chatmode)
1714
1725
  payload.chatmode = context.metadata.chatmode;
1715
1726
  // 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
1716
- logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} textLen=${finalText.length}`);
1727
+ logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${finalText.length}`);
1717
1728
  const isGroup = this.isGroupId(channelId);
1718
1729
  const targetAid = channelId;
1719
1730
  const encryptTarget = isGroup ? channelId : targetAid;
@@ -1738,7 +1749,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1738
1749
  else {
1739
1750
  logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1740
1751
  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);
1752
+ this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1753
+ this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
1742
1754
  }
1743
1755
  }
1744
1756
  else {
@@ -1750,7 +1762,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1750
1762
  else {
1751
1763
  logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1752
1764
  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);
1765
+ this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1766
+ this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
1754
1767
  }
1755
1768
  }
1756
1769
  return true;
@@ -1792,6 +1805,34 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1792
1805
  }
1793
1806
  }
1794
1807
  }
1808
+ /** 出站消息写入 messages.jsonl(message.send/group.send/thought.put 成功后调用) */
1809
+ appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text', source = 'daemon') {
1810
+ try {
1811
+ const sessionsDir = resolvePaths().sessionsDir;
1812
+ const selfId = this.config.aid;
1813
+ const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfId);
1814
+ const chatmode = context?.metadata?.chatmode;
1815
+ appendMessageLog(chatDir, buildOutboundEntry({
1816
+ from: selfId,
1817
+ to: channelId,
1818
+ chatType: isGroup ? 'group' : 'private',
1819
+ groupId: isGroup ? channelId : null,
1820
+ msgId,
1821
+ content: text,
1822
+ replyTo: null,
1823
+ agent: null,
1824
+ model: null,
1825
+ durationMs: null,
1826
+ encrypt,
1827
+ chatmode,
1828
+ msgType,
1829
+ source,
1830
+ }));
1831
+ }
1832
+ catch (e) {
1833
+ logger.debug(`${this.logPrefix()} appendOutboundJsonl failed: ${e}`);
1834
+ }
1835
+ }
1795
1836
  /**
1796
1837
  * 发送 thought 内容(Proactive 模式可观测)
1797
1838
  * 群聊:调用 group.thought.put
@@ -1818,17 +1859,45 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1818
1859
  try {
1819
1860
  const itemCount = Array.isArray(payload?.items) ? payload.items.length : 0;
1820
1861
  const stage = payload?.stage ?? `items=${itemCount}`;
1862
+ // 提取 thought 文本(只对 kind=text 的 item 写 jsonl,过滤 tool_use/tool_result 等结构化项)
1863
+ const items = payload?.items;
1864
+ let thoughtText;
1865
+ if (Array.isArray(items) && items.length > 0) {
1866
+ const lastItem = items[items.length - 1];
1867
+ // 优先 text 字段(kind=text 的 item),否则 content
1868
+ if (lastItem?.kind === 'text' && lastItem.text) {
1869
+ thoughtText = lastItem.text;
1870
+ }
1871
+ else if (lastItem?.text) {
1872
+ thoughtText = lastItem.text;
1873
+ }
1874
+ else if (lastItem?.content) {
1875
+ thoughtText = lastItem.content;
1876
+ }
1877
+ else if (typeof lastItem === 'string') {
1878
+ thoughtText = lastItem;
1879
+ }
1880
+ }
1821
1881
  if (this.isGroupId(channelId)) {
1822
1882
  params.group_id = targetId;
1823
1883
  const putRes = await this.callAndTrace('group.thought.put', params);
1824
1884
  const tid = putRes?.thought_id;
1825
1885
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1886
+ this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
1887
+ // 文本类 thought 写入 jsonl(只对有 text 的 item,过滤 tool 等结构化项)
1888
+ if (thoughtText) {
1889
+ this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
1890
+ }
1826
1891
  }
1827
1892
  else {
1828
1893
  params.to = targetId;
1829
1894
  const putRes = await this.callAndTrace('message.thought.put', params);
1830
1895
  const tid = putRes?.thought_id;
1831
1896
  logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1897
+ this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
1898
+ if (thoughtText) {
1899
+ this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
1900
+ }
1832
1901
  }
1833
1902
  }
1834
1903
  catch (e) {
@@ -2085,7 +2154,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2085
2154
  // to avoid duplicate "已送达" at the sender CLI
2086
2155
  this.messageSeqMap.delete(messageId);
2087
2156
  }
2088
- sendProcessingStatus(channelId, status, sessionId, taskId, context) {
2157
+ sendProcessingStatus(channelId, status, sessionId, taskId, context, extraMeta) {
2089
2158
  if (status === 'start')
2090
2159
  this.sentCount.delete(channelId); // 新任务开始,重置计数
2091
2160
  if (!this.client || !this.connected)
@@ -2098,6 +2167,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2098
2167
  error: 'error',
2099
2168
  timeout: 'timeout',
2100
2169
  queued: 'queued',
2170
+ progress: 'progress',
2101
2171
  };
2102
2172
  const statusPayload = {
2103
2173
  type: 'status',
@@ -2105,6 +2175,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2105
2175
  task_id: taskId,
2106
2176
  session_id: sessionId,
2107
2177
  severity,
2178
+ ...(extraMeta && Object.keys(extraMeta).length > 0 && { metadata: extraMeta }),
2108
2179
  };
2109
2180
  if (context?.threadId)
2110
2181
  statusPayload.thread_id = context.threadId;
@@ -2155,7 +2226,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2155
2226
  const chatmode = context?.metadata?.chatmode ?? '?';
2156
2227
  const initiator = statusPayload.initiator ?? '';
2157
2228
  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}`);
2229
+ const metaStr = statusPayload.metadata ? ` meta=${JSON.stringify(statusPayload.metadata)}` : '';
2230
+ logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel} initiator=${initiator} ref_msg=${refMsgId}${metaStr}`);
2159
2231
  }
2160
2232
  sendCustomPayload(channelId, payload) {
2161
2233
  if (!this.client || !this.connected)
@@ -2253,13 +2325,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2253
2325
  loadSelfName(aid) {
2254
2326
  try {
2255
2327
  const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
2256
- const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
2257
- if (!fs.existsSync(agentMdPath)) {
2328
+ const mdPath = agentMdPathFn(aidName);
2329
+ if (!fs.existsSync(mdPath)) {
2258
2330
  // 异步拉取,不阻塞连接流程
2259
2331
  this.fetchAndCacheSelfName(aidName);
2260
2332
  return undefined;
2261
2333
  }
2262
- const content = fs.readFileSync(agentMdPath, 'utf-8');
2334
+ const content = fs.readFileSync(mdPath, 'utf-8');
2263
2335
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
2264
2336
  if (!fmMatch)
2265
2337
  return undefined;
@@ -2370,6 +2442,7 @@ export class AUNChannelPlugin {
2370
2442
  });
2371
2443
  const adapter = {
2372
2444
  channelName: inst.name,
2445
+ channelKey: inst.name, // channelName 实际上就是 channelKey
2373
2446
  capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true },
2374
2447
  send: async (envelope, payload) => {
2375
2448
  const ctx = envelope.replyContext;
@@ -2414,13 +2487,14 @@ export class AUNChannelPlugin {
2414
2487
  await channel.sendThought(channelId, envelope.taskId, aunPayload, ctx);
2415
2488
  }
2416
2489
  else {
2417
- await Promise.all([
2418
- channel.sendThought(channelId, envelope.taskId, aunPayload, ctx),
2419
- channel.sendStructured(channelId, aunPayload, ctx),
2420
- ]);
2490
+ // interactive 模式不发 thought.put,只写入消息历史
2491
+ await channel.sendStructured(channelId, aunPayload, ctx);
2421
2492
  }
2422
2493
  return;
2423
2494
  }
2495
+ case 'status.progress':
2496
+ channel.sendProcessingStatus(channelId, 'progress', envelope.taskId, envelope.taskId, ctx, payload.metadata);
2497
+ return;
2424
2498
  case 'status.started':
2425
2499
  channel.sendProcessingStatus(channelId, 'start', envelope.taskId, envelope.taskId, ctx);
2426
2500
  return;
@@ -447,6 +447,7 @@ export class DingtalkChannelPlugin {
447
447
  });
448
448
  const adapter = {
449
449
  channelName: inst.name,
450
+ channelKey: inst.name,
450
451
  capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
451
452
  send: async (envelope, payload) => {
452
453
  const ctx = envelope.replyContext;
@@ -482,6 +483,7 @@ export class DingtalkChannelPlugin {
482
483
  case 'status.interrupted':
483
484
  case 'status.error':
484
485
  case 'status.timeout':
486
+ case 'status.progress':
485
487
  case 'custom':
486
488
  return;
487
489
  default: