evolclaw 3.1.5 → 3.1.7

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 (51) hide show
  1. package/CHANGELOG.md +68 -3
  2. package/dist/agents/claude-runner.js +69 -24
  3. package/dist/agents/kit-renderer.js +78 -321
  4. package/dist/agents/manifest-engine.js +243 -0
  5. package/dist/agents/message-renderer.js +112 -0
  6. package/dist/aun/aid/agentmd.js +10 -3
  7. package/dist/aun/msg/group.js +2 -2
  8. package/dist/channels/aun.js +154 -18
  9. package/dist/channels/dingtalk.js +1 -1
  10. package/dist/channels/feishu.js +31 -9
  11. package/dist/channels/qqbot.js +1 -1
  12. package/dist/channels/wechat.js +1 -1
  13. package/dist/channels/wecom.js +1 -1
  14. package/dist/cli/agent.js +10 -11
  15. package/dist/cli/bench.js +1 -5
  16. package/dist/cli/help.js +8 -0
  17. package/dist/cli/index.js +91 -128
  18. package/dist/cli/init.js +37 -21
  19. package/dist/cli/link-rules.js +1 -7
  20. package/dist/cli/model.js +231 -6
  21. package/dist/config-store.js +1 -22
  22. package/dist/core/command-handler.js +181 -48
  23. package/dist/core/evolagent.js +0 -18
  24. package/dist/core/message/im-renderer.js +9 -20
  25. package/dist/core/message/message-bridge.js +9 -10
  26. package/dist/core/message/message-processor.js +188 -39
  27. package/dist/core/message/message-queue.js +15 -1
  28. package/dist/core/relation/peer-identity.js +23 -11
  29. package/dist/core/trigger/parser.js +4 -4
  30. package/dist/core/trigger/scheduler.js +43 -13
  31. package/dist/index.js +102 -52
  32. package/dist/ipc.js +1 -1
  33. package/dist/utils/error-utils.js +6 -0
  34. package/dist/utils/process-introspect.js +7 -5
  35. package/kits/docs/INDEX.md +4 -8
  36. package/kits/docs/context-assembly.md +1 -0
  37. package/kits/docs/evolclaw/INDEX.md +43 -0
  38. package/kits/docs/evolclaw/group.md +13 -6
  39. package/kits/docs/evolclaw/model.md +51 -0
  40. package/kits/docs/evolclaw/msg.md +5 -0
  41. package/kits/docs/venues/group.md +13 -1
  42. package/kits/eck_manifest.json +9 -0
  43. package/kits/eck_message_manifest.json +14 -0
  44. package/kits/rules/06-channel.md +5 -1
  45. package/kits/templates/message-fragments/item.md +2 -0
  46. package/kits/templates/system-fragments/baseagent.md +7 -1
  47. package/kits/templates/system-fragments/channel.md +7 -5
  48. package/kits/templates/system-fragments/commands.md +19 -0
  49. package/kits/templates/system-fragments/session.md +12 -0
  50. package/kits/templates/system-fragments/venue.md +15 -0
  51. package/package.json +3 -3
@@ -576,7 +576,7 @@ export class AUNChannel {
576
576
  const store = await getAidStore({
577
577
  slotId: SLOT.daemon,
578
578
  aunPath,
579
- debug: this.config.aunSdkLog ?? true,
579
+ debug: this.config.aunSdkLog ?? false,
580
580
  });
581
581
  this.store = store;
582
582
  const client = await loadClient(store, aidName);
@@ -790,18 +790,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
790
790
 
791
791
  📋 **日常使用方法**:
792
792
 
793
- 1. **绑定项目**:发送 \`/bind <项目路径>\` 绑定工作目录
794
- 2. **查看帮助**:发送 \`/help\` 查看所有可用命令
795
- 3. **切换项目**:发送 \`/project <项目名>\` 切换到其他项目
796
- 4. **查看状态**:发送 \`/status\` 查看当前会话状态
797
- 5. **会话管理**:发送 \`/session\` 查看和切换会话
793
+ 1. **查看帮助**:发送 \`/help\` 查看所有可用命令
794
+ 2. **查看状态**:发送 \`/status\` 查看当前会话状态
795
+ 3. **会话管理**:发送 \`/session\` 查看和切换会话
798
796
 
799
797
  💡 **提示**:
800
798
  - 直接发送消息即可与 Claude/Codex 对话
801
- - 支持多项目会话管理,每个项目独立会话
799
+ - 支持多会话管理,每个会话独立上下文
802
800
  - 所有命令以 \`/\` 开头
803
801
 
804
- 现在,请先使用 \`/bind\` 命令绑定您的项目目录,然后就可以开始工作了!`;
802
+ 现在就可以开始工作了!`;
805
803
  // First contact with Owner races against Owner's async cert fetch from
806
804
  // gateway PKI; a 3s pause lets the cert propagate. persist_required asks
807
805
  // the gateway to durably store the message so Owner can recover it via
@@ -818,6 +816,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
818
816
  persist_required: true,
819
817
  });
820
818
  logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
819
+ // Send binding credential for Evol App to persist locally
820
+ await this.sendBindingCredential(owner, agentDisplayName, agentConfig.active_baseagent || 'claude').catch(e => logger.warn(`${this.logPrefix()} Binding credential failed: ${e}`));
821
821
  // Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
822
822
  try {
823
823
  const fresh = loadAgent(aidName);
@@ -835,7 +835,57 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
835
835
  logger.warn(`${this.logPrefix()} Failed to send welcome message: ${e}`);
836
836
  }
837
837
  }
838
+ async sendBindingCredential(owner, name, baseagent) {
839
+ if (!this.client)
840
+ return;
841
+ await this.callAndTrace('message.send', {
842
+ to: owner,
843
+ payload: { type: 'binding', aid: this.config.aid, name, owner, baseagent },
844
+ encrypt: true,
845
+ persist_required: true,
846
+ });
847
+ logger.info(`${this.logPrefix()} Binding credential sent to owner: ${owner}`);
848
+ }
838
849
  // ── Event handlers ──────────────────────────────────────────
850
+ /**
851
+ * 判断附件是否为图片,返回 MIME 类型(非图片返回空)。
852
+ * 多重检测:附件元数据字段 → 文件名后缀 → 文件 magic bytes。
853
+ */
854
+ detectImageMime(att, filePath) {
855
+ const extToMime = {
856
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
857
+ '.gif': 'image/gif', '.webp': 'image/webp',
858
+ };
859
+ // 1. 附件元数据字段(content_type / mime_type / mimeType)
860
+ const metaCt = (att?.content_type || att?.mime_type || att?.mimeType || '');
861
+ if (typeof metaCt === 'string' && metaCt.startsWith('image/'))
862
+ return metaCt;
863
+ // 2. 文件名后缀
864
+ const name = (att?.filename || att?.object_key || filePath || '').toLowerCase();
865
+ for (const [ext, mime] of Object.entries(extToMime)) {
866
+ if (name.endsWith(ext))
867
+ return mime;
868
+ }
869
+ // 3. magic bytes
870
+ try {
871
+ const { openSync, readSync, closeSync } = require('node:fs');
872
+ const fd = openSync(filePath, 'r');
873
+ const head = Buffer.alloc(12);
874
+ readSync(fd, head, 0, 12, 0);
875
+ closeSync(fd);
876
+ if (head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4e && head[3] === 0x47)
877
+ return 'image/png';
878
+ if (head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff)
879
+ return 'image/jpeg';
880
+ if (head[0] === 0x47 && head[1] === 0x49 && head[2] === 0x46)
881
+ return 'image/gif';
882
+ if (head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46 &&
883
+ head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50)
884
+ return 'image/webp';
885
+ }
886
+ catch { /* not readable, skip */ }
887
+ return '';
888
+ }
839
889
  async downloadAttachment(att, channelId) {
840
890
  const ownerAid = att.owner_aid || this._aid || '';
841
891
  const objectKey = att.object_key;
@@ -844,7 +894,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
844
894
  return null;
845
895
  }
846
896
  const filename = att.filename || objectKey.split('/').pop() || 'unknown';
847
- let downloadUrl;
897
+ // 安全:始终通过受信任的 ticket 路径获取下载 URL。
898
+ // 不信任 att.url(来自对端消息 payload,可被构造为内网/元数据地址,SSRF)。
899
+ let downloadUrl = '';
848
900
  try {
849
901
  const ticket = await this.callAndTrace('storage.create_download_ticket', {
850
902
  owner_aid: ownerAid,
@@ -924,12 +976,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
924
976
  // Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
925
977
  const rawAttachments = this.collectAllAttachments(payload);
926
978
  let finalText = text;
979
+ const inboundImages = [];
927
980
  if (rawAttachments.length > 0 && this.client) {
928
981
  const fileParts = [];
929
982
  for (const att of rawAttachments) {
930
983
  const filePath = await this.downloadAttachment(att, fromAid);
931
984
  if (filePath) {
932
985
  const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
986
+ const mime = this.detectImageMime(att, filePath);
987
+ if (mime) {
988
+ try {
989
+ const { readFileSync } = await import('node:fs');
990
+ inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
991
+ }
992
+ catch { /* fallback to file path */ }
993
+ }
933
994
  fileParts.push(`[文件: ${name} → ${filePath}]`);
934
995
  }
935
996
  }
@@ -938,9 +999,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
938
999
  if (text)
939
1000
  parts.push(text);
940
1001
  parts.push(...fileParts);
941
- parts.push('请使用 Read 工具读取文件内容。');
1002
+ if (inboundImages.length === 0)
1003
+ parts.push('请使用 Read 工具读取文件内容。');
942
1004
  finalText = parts.join('\n\n');
943
1005
  }
1006
+ logger.info(`${this.logPrefix()} [img-debug] private attachments=${rawAttachments.length} images=${inboundImages.length}`);
944
1007
  }
945
1008
  // 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
946
1009
  // device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
@@ -993,7 +1056,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
993
1056
  mentions,
994
1057
  peerName: displayName || undefined,
995
1058
  peerType: peerIdentity.type,
1059
+ sameDevice: msg.same_device === true || undefined,
1060
+ sameNetwork: msg.same_network === true || undefined,
1061
+ sameEgressIp: msg.same_egress_ip === true || undefined,
996
1062
  replyContext,
1063
+ images: inboundImages.length > 0 ? inboundImages : undefined,
997
1064
  });
998
1065
  }
999
1066
  async handleIncomingGroupMessage(data) {
@@ -1144,12 +1211,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1144
1211
  : mentionedSelf && this._aid ? [this._aid] : [];
1145
1212
  // Process attachments
1146
1213
  let finalText = strippedText;
1214
+ const inboundImages = [];
1147
1215
  if (hasAttachments && this.client) {
1148
1216
  const fileParts = [];
1149
1217
  for (const att of rawAttachments) {
1150
1218
  const filePath = await this.downloadAttachment(att, groupId);
1151
1219
  if (filePath) {
1152
1220
  const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
1221
+ const mime = this.detectImageMime(att, filePath);
1222
+ if (mime) {
1223
+ try {
1224
+ const { readFileSync } = await import('node:fs');
1225
+ inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
1226
+ }
1227
+ catch { /* fallback to file path */ }
1228
+ }
1153
1229
  fileParts.push(`[文件: ${name} → ${filePath}]`);
1154
1230
  }
1155
1231
  }
@@ -1158,7 +1234,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1158
1234
  if (strippedText)
1159
1235
  parts.push(strippedText);
1160
1236
  parts.push(...fileParts);
1161
- parts.push('请使用 Read 工具读取文件内容。');
1237
+ if (inboundImages.length === 0)
1238
+ parts.push('请使用 Read 工具读取文件内容。');
1162
1239
  finalText = parts.join('\n\n');
1163
1240
  }
1164
1241
  }
@@ -1191,6 +1268,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1191
1268
  userId: senderAid,
1192
1269
  peerName: displayName || undefined,
1193
1270
  peerType: peerIdentity.type,
1271
+ sameDevice: msg.same_device === true || undefined,
1272
+ sameNetwork: msg.same_network === true || undefined,
1273
+ sameEgressIp: msg.same_egress_ip === true || undefined,
1194
1274
  text: finalText,
1195
1275
  chatType: 'group',
1196
1276
  messageId,
@@ -1198,6 +1278,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1198
1278
  threadId,
1199
1279
  mentions,
1200
1280
  replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
1281
+ images: inboundImages.length > 0 ? inboundImages : undefined,
1201
1282
  });
1202
1283
  }
1203
1284
  dispatchMessage(event) {
@@ -1260,13 +1341,55 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1260
1341
  peerId: event.userId || event.channelId || '',
1261
1342
  peerName: event.peerName,
1262
1343
  peerType: event.peerType,
1344
+ sameDevice: event.sameDevice,
1345
+ sameNetwork: event.sameNetwork,
1346
+ sameEgressIp: event.sameEgressIp,
1263
1347
  messageId: event.messageId,
1264
1348
  threadId: event.threadId,
1265
1349
  mentions: mentionObjects,
1266
1350
  replyContext,
1351
+ images: event.images,
1267
1352
  }).catch(err => {
1268
1353
  logger.error(`${this.logPrefix()} Message handler error:`, err);
1269
1354
  });
1355
+ // Observer forward: inbound
1356
+ this.forwardToOwners('inbound', {
1357
+ from: event.userId || event.channelId || '',
1358
+ to: this.config.aid,
1359
+ seq: event.seq,
1360
+ payload: { type: 'text', text: event.text },
1361
+ });
1362
+ }
1363
+ /**
1364
+ * 观察者模式转发:将消息副本以 observer.forward 格式发给所有 owners。
1365
+ * 仅在 AgentConfig.observable === true 时执行;owners 为空或无法加载配置时静默跳过。
1366
+ */
1367
+ forwardToOwners(direction, original) {
1368
+ if (!this.connected || !this.client)
1369
+ return;
1370
+ const agentConfig = loadAgent(this.config.aid);
1371
+ if (!agentConfig?.observable)
1372
+ return;
1373
+ const owners = agentConfig.owners ?? [];
1374
+ if (owners.length === 0)
1375
+ return;
1376
+ const forwardPayload = {
1377
+ type: 'observer.forward',
1378
+ direction,
1379
+ agent_aid: this.config.aid,
1380
+ original: {
1381
+ from: original.from,
1382
+ to: original.to,
1383
+ ...(original.seq != null ? { seq: original.seq } : {}),
1384
+ timestamp: Date.now(),
1385
+ payload: original.payload,
1386
+ },
1387
+ };
1388
+ for (const ownerAid of owners) {
1389
+ const encrypt = this.shouldEncrypt(ownerAid);
1390
+ this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt })
1391
+ .catch(e => logger.debug(`${this.logPrefix()} observer.forward to ${ownerAid} failed: ${e}`));
1392
+ }
1270
1393
  }
1271
1394
  handleEcho(event) {
1272
1395
  const ts = () => {
@@ -1743,6 +1866,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1743
1866
  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 });
1744
1867
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1745
1868
  this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
1869
+ // Observer forward: outbound (group)
1870
+ this.forwardToOwners('outbound', {
1871
+ from: this.config.aid,
1872
+ to: channelId,
1873
+ payload: { type: 'text', text: finalText },
1874
+ });
1746
1875
  }
1747
1876
  }
1748
1877
  else {
@@ -1756,6 +1885,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1756
1885
  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 });
1757
1886
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1758
1887
  this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
1888
+ // Observer forward: outbound (private)
1889
+ this.forwardToOwners('outbound', {
1890
+ from: this.config.aid,
1891
+ to: targetAid,
1892
+ payload: { type: 'text', text: finalText },
1893
+ });
1759
1894
  }
1760
1895
  }
1761
1896
  return true;
@@ -2444,7 +2579,7 @@ export class AUNChannelPlugin {
2444
2579
  const adapter = {
2445
2580
  channelName: inst.name,
2446
2581
  channelKey: inst.name, // channelName 实际上就是 channelKey
2447
- capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true },
2582
+ capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true },
2448
2583
  send: async (envelope, payload) => {
2449
2584
  const ctx = envelope.replyContext;
2450
2585
  const channelId = envelope.channelId;
@@ -2497,22 +2632,22 @@ export class AUNChannelPlugin {
2497
2632
  channel.sendProcessingStatus(channelId, 'progress', envelope.taskId, envelope.taskId, ctx, payload.metadata);
2498
2633
  return;
2499
2634
  case 'status.started':
2500
- channel.sendProcessingStatus(channelId, 'start', envelope.taskId, envelope.taskId, ctx);
2635
+ channel.sendProcessingStatus(channelId, 'start', envelope.taskId, envelope.taskId, ctx, payload.metadata);
2501
2636
  return;
2502
2637
  case 'status.queued':
2503
- channel.sendProcessingStatus(channelId, 'queued', envelope.taskId, envelope.taskId, ctx);
2638
+ channel.sendProcessingStatus(channelId, 'queued', envelope.taskId, envelope.taskId, ctx, payload.metadata);
2504
2639
  return;
2505
2640
  case 'status.completed':
2506
- channel.sendProcessingStatus(channelId, 'done', envelope.taskId, envelope.taskId, ctx);
2641
+ channel.sendProcessingStatus(channelId, 'done', envelope.taskId, envelope.taskId, ctx, payload.metadata);
2507
2642
  return;
2508
2643
  case 'status.interrupted':
2509
- channel.sendProcessingStatus(channelId, 'interrupted', envelope.taskId, envelope.taskId, ctx);
2644
+ channel.sendProcessingStatus(channelId, 'interrupted', envelope.taskId, envelope.taskId, ctx, payload.metadata);
2510
2645
  return;
2511
2646
  case 'status.error':
2512
- channel.sendProcessingStatus(channelId, 'error', envelope.taskId, envelope.taskId, ctx);
2647
+ channel.sendProcessingStatus(channelId, 'error', envelope.taskId, envelope.taskId, ctx, payload.metadata);
2513
2648
  return;
2514
2649
  case 'status.timeout':
2515
- channel.sendProcessingStatus(channelId, 'timeout', envelope.taskId, envelope.taskId, ctx);
2650
+ channel.sendProcessingStatus(channelId, 'timeout', envelope.taskId, envelope.taskId, ctx, payload.metadata);
2516
2651
  return;
2517
2652
  case 'interaction': {
2518
2653
  const req = payload.interaction;
@@ -2637,6 +2772,7 @@ export class AUNChannelPlugin {
2637
2772
  threadId: opts.threadId,
2638
2773
  replyContext: opts.replyContext,
2639
2774
  source: opts.source,
2775
+ images: opts.images,
2640
2776
  });
2641
2777
  }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
2642
2778
  },
@@ -448,7 +448,7 @@ export class DingtalkChannelPlugin {
448
448
  const adapter = {
449
449
  channelName: inst.name,
450
450
  channelKey: inst.name,
451
- capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
451
+ capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false, thread: false },
452
452
  send: async (envelope, payload) => {
453
453
  const ctx = envelope.replyContext;
454
454
  const channelId = envelope.channelId;
@@ -283,7 +283,8 @@ export class FeishuChannel {
283
283
  else if (msg.message_type === 'merge_forward') {
284
284
  const { text: mergedText, images: mergedImages } = await this.extractMergeForwardContent(msg.message_id, msg.chat_id);
285
285
  if (mergedText) {
286
- const finalContent = quotedText + mergedText;
286
+ // 直接发送合并转发时,parent_id 指向自己,引用解析会把相同内容填入 quotedText 导致重复,丢弃
287
+ const finalContent = mergedText;
287
288
  const allImages = [...quotedImages, ...mergedImages];
288
289
  await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: allImages.length > 0 ? allImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
289
290
  }
@@ -513,10 +514,15 @@ export class FeishuChannel {
513
514
  if (options.replyInThread) {
514
515
  replyData.reply_in_thread = true;
515
516
  }
516
- await this.client.im.message.reply({
517
+ const replyRes = await this.client.im.message.reply({
517
518
  path: { message_id: options.replyToMessageId },
518
519
  data: replyData
519
520
  });
521
+ if (options.replyInThread && options.onThreadCreated) {
522
+ const newThreadId = replyRes?.data?.thread_id;
523
+ if (newThreadId)
524
+ options.onThreadCreated(newThreadId);
525
+ }
520
526
  }
521
527
  else {
522
528
  await this.client.im.message.create({
@@ -710,13 +716,18 @@ export class FeishuChannel {
710
716
  // seenThreads 无时间戳,仅限容量(话题持久存在,不按时间清理)
711
717
  if (this.seenThreads.size > 1000)
712
718
  this.seenThreads.clear();
713
- // 重写文件,去掉过期条目
714
- if (this.config.seenMsgFile && this.seenMessages.size > 0) {
719
+ // 重写文件,去掉过期条目(仅在有记录被清理时才写)
720
+ if (cleaned > 0 && this.config.seenMsgFile) {
715
721
  try {
716
- const lines = [...this.seenMessages.entries()]
717
- .map(([id, ts]) => JSON.stringify({ id, ts }))
718
- .join('\n') + '\n';
719
- fs.writeFileSync(this.config.seenMsgFile, lines);
722
+ if (this.seenMessages.size === 0) {
723
+ fs.unlinkSync(this.config.seenMsgFile);
724
+ }
725
+ else {
726
+ const lines = [...this.seenMessages.entries()]
727
+ .map(([id, ts]) => JSON.stringify({ id, ts }))
728
+ .join('\n') + '\n';
729
+ fs.writeFileSync(this.config.seenMsgFile, lines);
730
+ }
720
731
  }
721
732
  catch { }
722
733
  }
@@ -1196,6 +1207,15 @@ export function buildResolvedV2(interaction, response) {
1196
1207
  });
1197
1208
  bodyElements.push({ tag: 'markdown', content: lines.join('\n') });
1198
1209
  }
1210
+ // CommandCard: 显示原有按钮列表(保留上下文)
1211
+ if (kind.kind === 'command-card' && kind.buttons?.length) {
1212
+ const lines = kind.buttons.map(btn => {
1213
+ const prefix = btn.command === action ? '✓' : '•';
1214
+ const cleanLabel = btn.label.replace(/^✓\s*/, '');
1215
+ return `${prefix} ${cleanLabel}`;
1216
+ });
1217
+ bodyElements.push({ tag: 'markdown', content: lines.join('\n') });
1218
+ }
1199
1219
  return {
1200
1220
  toast: { type: 'success', content: statusText },
1201
1221
  card: {
@@ -1403,7 +1423,7 @@ export class FeishuChannelPlugin {
1403
1423
  const adapter = {
1404
1424
  channelName: inst.name,
1405
1425
  channelKey: inst.name,
1406
- capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true },
1426
+ capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true, thread: true },
1407
1427
  send: async (envelope, payload) => {
1408
1428
  const ctx = envelope.replyContext;
1409
1429
  const channelId = envelope.channelId;
@@ -1417,6 +1437,8 @@ export class FeishuChannelPlugin {
1417
1437
  const sendCtx = { ...(ctx ?? {}) };
1418
1438
  if (payload.kind === 'result.text' && payload.isFinal)
1419
1439
  sendCtx.title = '✅ 最终回复:';
1440
+ if (ctx?.metadata?.onThreadCreated)
1441
+ sendCtx.onThreadCreated = ctx.metadata.onThreadCreated;
1420
1442
  await channel.sendMessage(channelId, payload.text, sendCtx);
1421
1443
  return;
1422
1444
  }
@@ -335,7 +335,7 @@ export class QQBotChannelPlugin {
335
335
  const adapter = {
336
336
  channelName: inst.name,
337
337
  channelKey: inst.name,
338
- capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
338
+ capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false, thread: false },
339
339
  send: async (envelope, payload) => {
340
340
  const ctx = envelope.replyContext;
341
341
  const channelId = envelope.channelId;
@@ -731,7 +731,7 @@ export class WechatChannelPlugin {
731
731
  const adapter = {
732
732
  channelName: inst.name,
733
733
  channelKey: inst.name,
734
- capabilities: { file: false, image: false, interaction: false, markdown: false, thought: false, status: true },
734
+ capabilities: { file: false, image: false, interaction: false, markdown: false, thought: false, status: true, thread: false },
735
735
  send: async (envelope, payload) => {
736
736
  const channelId = envelope.channelId;
737
737
  switch (payload.kind) {
@@ -491,7 +491,7 @@ export class WecomChannelPlugin {
491
491
  const adapter = {
492
492
  channelName: inst.name,
493
493
  channelKey: inst.name,
494
- capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
494
+ capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false, thread: false },
495
495
  send: async (envelope, payload) => {
496
496
  const ctx = envelope.replyContext;
497
497
  const channelId = envelope.channelId;
package/dist/cli/agent.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  import readline from 'readline';
5
4
  import { resolvePaths, agentMdPath as getAgentMdPathFromPaths, aunPath as defaultAunPath } from '../paths.js';
6
5
  import { loadDefaults, loadAllAgents, loadAgent, saveAgent, ensureAgentDirSkeleton } from '../config-store.js';
@@ -11,11 +10,6 @@ import { commandExists } from '../utils/cross-platform.js';
11
10
  import { isCodexSdkAvailable } from '../agents/codex-runner.js';
12
11
  // ==================== Helpers ====================
13
12
  const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
14
- const BASEAGENT_ENV_KEY = {
15
- claude: 'ANTHROPIC_API_KEY',
16
- codex: 'OPENAI_API_KEY',
17
- gemini: 'GEMINI_API_KEY',
18
- };
19
13
  function isBaseagentAvailable(baseagent) {
20
14
  if (baseagent === 'codex')
21
15
  return isCodexSdkAvailable();
@@ -30,8 +24,7 @@ function pickDefaultBaseagent(available) {
30
24
  return available.includes('claude') ? 'claude' : available[0];
31
25
  }
32
26
  function buildBaseagentsBlock(chosen) {
33
- const env = BASEAGENT_ENV_KEY[chosen];
34
- return { [chosen]: env ? { apiKey: `$ENV:${env}` } : {} };
27
+ return { [chosen]: {} };
35
28
  }
36
29
  const DEFAULT_CHATMODE = { private: 'interactive', group: 'proactive', nothuman: 'proactive' };
37
30
  const DEFAULT_DISPATCH = 'mention';
@@ -296,11 +289,11 @@ export async function agentCreateInteractive(opts = {}) {
296
289
  const defaults = loadDefaults();
297
290
  const rootPath = defaults?.projects?.rootPath
298
291
  || (defaults?.projects?.defaultPath && path.dirname(defaults.projects.defaultPath))
299
- || path.join(os.homedir(), 'evolclaw-projects');
292
+ || resolvePaths().root + '/projects';
300
293
  suggestedProjectPath = deriveAgentProjectPath(rootPath, aid);
301
294
  }
302
295
  catch {
303
- suggestedProjectPath = deriveAgentProjectPath(path.join(os.homedir(), 'evolclaw-projects'), aid);
296
+ suggestedProjectPath = deriveAgentProjectPath(resolvePaths().root + '/projects', aid);
304
297
  }
305
298
  const projectInput = (await ask(`Project path [${suggestedProjectPath}]: `)).trim();
306
299
  const projectPath = projectInput || suggestedProjectPath;
@@ -633,7 +626,7 @@ export async function agentSyncAids() {
633
626
  const defaults = loadDefaults();
634
627
  const rootPath = defaults?.projects?.rootPath
635
628
  || (defaults?.projects?.defaultPath && path.dirname(defaults.projects.defaultPath))
636
- || path.join(os.homedir(), 'evolclaw-projects');
629
+ || resolvePaths().root + '/projects';
637
630
  const created = [];
638
631
  for (const aid of localAids) {
639
632
  if (existingAids.has(aid))
@@ -761,6 +754,12 @@ export async function agentSet(aid, key, rawValue) {
761
754
  return { ok: false, error: `Failed to read config: ${e?.message || e}` };
762
755
  }
763
756
  const value = parseJsonValue(rawValue);
757
+ // active_baseagent 白名单校验:只允许已知 baseagent,挡住把模型名(如 deepseek)误设为后端
758
+ if (key === 'active_baseagent') {
759
+ if (typeof value !== 'string' || !BASEAGENT_CANDIDATES.includes(value)) {
760
+ return { ok: false, error: `无效 active_baseagent: ${JSON.stringify(value)}(可选: ${BASEAGENT_CANDIDATES.join(' / ')})` };
761
+ }
762
+ }
764
763
  setNestedValue(config, key, value);
765
764
  try {
766
765
  saveAgent(config);
package/dist/cli/bench.js CHANGED
@@ -8,7 +8,7 @@ import { aidList, aidCreate } from '../aun/aid/identity.js';
8
8
  import { msgSend, msgPull } from '../aun/msg/index.js';
9
9
  import { getPackageRoot, aunPath as defaultAunPath } from '../paths.js';
10
10
  import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
11
- import { isHelpFlag } from './help.js';
11
+ import { isHelpFlag, getArgValue } from './help.js';
12
12
  const execFileAsync = promisify(execFile);
13
13
  // ==================== ANSI ====================
14
14
  const GREEN = '\x1b[32m';
@@ -132,10 +132,6 @@ function percentile(sorted, p) {
132
132
  const idx = Math.ceil((p / 100) * sorted.length) - 1;
133
133
  return sorted[Math.max(0, idx)];
134
134
  }
135
- function getArgValue(args, flag) {
136
- const idx = args.indexOf(flag);
137
- return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : undefined;
138
- }
139
135
  // ==================== Promise Pool ====================
140
136
  function withTimeout(promise, ms, label) {
141
137
  return new Promise((resolve, reject) => {
package/dist/cli/help.js CHANGED
@@ -21,3 +21,11 @@ export function wantsHelp(args) {
21
21
  return true;
22
22
  return false;
23
23
  }
24
+ /**
25
+ * 取出 `--flag <value>` 形式的参数值。
26
+ * flag 不存在或其后无值时返回 undefined。
27
+ */
28
+ export function getArgValue(args, flag) {
29
+ const idx = args.indexOf(flag);
30
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
31
+ }