evolclaw 3.1.11 → 3.2.0

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.
@@ -58,6 +58,37 @@ function getEvolclawVersion() {
58
58
  }
59
59
  return _cachedVersion;
60
60
  }
61
+ /**
62
+ * 把 AUNChannel 投递的 opts 映射成渠道无关的 InboundMessage。
63
+ *
64
+ * registerBridge 适配回调用它替代手抄字段——历史上手抄漏掉了
65
+ * sameDevice/sameNetwork/sameEgressIp,proximity 在此被吞,eck-debug 永远 false。
66
+ * 抽成纯函数后可单测锁字段,杜绝再漏。
67
+ */
68
+ export function aunOptsToInbound(opts, channel, channelType) {
69
+ return {
70
+ channel,
71
+ channelType,
72
+ channelId: opts.channelId,
73
+ selfAID: opts.selfAID,
74
+ groupId: opts.groupId,
75
+ content: opts.content,
76
+ chatType: opts.chatType || 'private',
77
+ peerId: opts.peerId || '',
78
+ peerName: opts.peerName,
79
+ peerType: opts.peerType,
80
+ sameDevice: opts.sameDevice,
81
+ sameNetwork: opts.sameNetwork,
82
+ sameEgressIp: opts.sameEgressIp,
83
+ messageId: opts.messageId,
84
+ mentions: opts.mentions,
85
+ mentionAids: opts.mentionAids,
86
+ threadId: opts.threadId,
87
+ replyContext: opts.replyContext,
88
+ source: opts.source,
89
+ images: opts.images,
90
+ };
91
+ }
61
92
  export class AUNChannel {
62
93
  config;
63
94
  client = null;
@@ -427,6 +458,7 @@ export class AUNChannel {
427
458
  _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
428
459
  _chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
429
460
  seenMessages = new Map();
461
+ groupNameCache = new Map(); // groupId → 群显示名(进程内缓存,群名极少变)
430
462
  peerInfoCache = new Map();
431
463
  messageSeqMap = new Map(); // messageId → seq (for ack)
432
464
  sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
@@ -591,13 +623,16 @@ export class AUNChannel {
591
623
  logger.debug(`${this.logPrefix()}[DIAG] message.received: kind=${kind} keys=${keys}`);
592
624
  this.handleIncomingPrivateMessage(data);
593
625
  });
594
- client.on('group.message_created', (data) => {
595
- this.trace('IN', 'group.message_created', data);
596
- const gid = (data && typeof data === 'object') ? data.group_id ?? '' : '';
597
- const sender = (data && typeof data === 'object') ? data.sender_aid ?? '' : '';
598
- logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
599
- this.handleIncomingGroupMessage(data);
600
- });
626
+ // pureIdentity(控制 AID):协议层不接群消息,不注册 group 创建监听
627
+ if (!this.config.pureIdentity) {
628
+ client.on('group.message_created', (data) => {
629
+ this.trace('IN', 'group.message_created', data);
630
+ const gid = (data && typeof data === 'object') ? data.group_id ?? '' : '';
631
+ const sender = (data && typeof data === 'object') ? data.sender_aid ?? '' : '';
632
+ logger.debug(`${this.logPrefix()}[DIAG] group.message_created: group_id=${gid} sender=${sender}`);
633
+ this.handleIncomingGroupMessage(data);
634
+ });
635
+ }
601
636
  client.on('connection.state', (data) => {
602
637
  // trace is handled inside handleConnectionState with throttling
603
638
  this.handleConnectionState(data);
@@ -626,11 +661,14 @@ export class AUNChannel {
626
661
  const d = data;
627
662
  logger.warn(`${this.logPrefix()} Message undecryptable: from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
628
663
  });
629
- client.on('group.message_undecryptable', (data) => {
630
- this.trace('IN', 'group.message_undecryptable', data);
631
- const d = data;
632
- logger.warn(`${this.logPrefix()} Group message undecryptable: group=${d.group_id} from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
633
- });
664
+ // pureIdentity(控制 AID):不注册 group 解密失败监听
665
+ if (!this.config.pureIdentity) {
666
+ client.on('group.message_undecryptable', (data) => {
667
+ this.trace('IN', 'group.message_undecryptable', data);
668
+ const d = data;
669
+ logger.warn(`${this.logPrefix()} Group message undecryptable: group=${d.group_id} from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
670
+ });
671
+ }
634
672
  // Authenticate(拿权威 gateway 用于日志/状态;connect 内部也会复用 token)
635
673
  try {
636
674
  logger.info(`${this.logPrefix()} Authenticating as ${aidName}...`);
@@ -676,7 +714,8 @@ export class AUNChannel {
676
714
  this._aid = this.client.aid ?? undefined;
677
715
  const deviceId = this.client._device_id ?? '';
678
716
  this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
679
- this._selfName = this.loadSelfName(aidName);
717
+ // pureIdentity(控制 AID):无 agent.md,跳过自身 agent.md 拉取,省一次 404
718
+ this._selfName = this.config.pureIdentity ? undefined : this.loadSelfName(aidName);
680
719
  if (this._selfName && this.aidStatsCollector)
681
720
  this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
682
721
  this.connected = true;
@@ -697,7 +736,10 @@ export class AUNChannel {
697
736
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
698
737
  appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
699
738
  // Send welcome message to owner after first connection
700
- await this.sendWelcomeMessage();
739
+ // pureIdentity(控制 AID):跳过 evolagent onboarding(根除 warn 噪声 + 永不 agentmdPut)
740
+ if (!this.config.pureIdentity) {
741
+ await this.sendWelcomeMessage();
742
+ }
701
743
  }
702
744
  catch (e) {
703
745
  this.trace('OUT', 'client.connect.error', { error: String(e) });
@@ -968,6 +1010,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
968
1010
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
969
1011
  const messageId = msg.message_id ?? '';
970
1012
  const seq = msg.seq;
1013
+ // Observer forward (inbound):在所有过滤之前转发原始明文 payload。
1014
+ // forwardInbound 内部排除 self-echo 与 from-owner。
1015
+ this.forwardInbound(fromAid, seq, payload);
971
1016
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
972
1017
  // 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
973
1018
  const msgChatId = typeof payload === 'object' && payload !== null && payload.chat_id;
@@ -1056,6 +1101,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1056
1101
  const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1057
1102
  const messageId = msg.message_id ?? '';
1058
1103
  const seq = msg.seq;
1104
+ // Observer forward (inbound):群聊消息在所有过滤之前转发原始明文 payload。
1105
+ // forwardInbound 内部排除 self-echo 与 from-owner。
1106
+ this.forwardInbound(senderAid, seq, payload);
1059
1107
  // Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
1060
1108
  const payloadMentions = Array.isArray(payload?.mentions)
1061
1109
  ? payload.mentions.filter((m) => typeof m === 'string')
@@ -1216,6 +1264,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1216
1264
  logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
1217
1265
  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 });
1218
1266
  this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode);
1267
+ // 渲染用完整 @ 列表:结构化 payload.mentions + 正文 @aid 兜底,去重(含 self / "all")。
1268
+ // 与上面用于过滤/回复的精简 mentions 独立——这份不丢任何被 @ 的 AID。
1269
+ const renderMentionAids = Array.from(new Set([
1270
+ ...payloadMentions,
1271
+ ...this.extractMentionAidsFromText(text),
1272
+ ]));
1219
1273
  this.dispatchMessage({
1220
1274
  channelId: groupId,
1221
1275
  groupId,
@@ -1231,6 +1285,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1231
1285
  seq,
1232
1286
  threadId,
1233
1287
  mentions,
1288
+ mentionAids: renderMentionAids.length > 0 ? renderMentionAids : undefined,
1234
1289
  replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
1235
1290
  images: inboundImages.length > 0 ? inboundImages : undefined,
1236
1291
  });
@@ -1301,32 +1356,68 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1301
1356
  messageId: event.messageId,
1302
1357
  threadId: event.threadId,
1303
1358
  mentions: mentionObjects,
1359
+ mentionAids: event.mentionAids,
1304
1360
  replyContext,
1305
1361
  images: event.images,
1306
1362
  }).catch(err => {
1307
1363
  logger.error(`${this.logPrefix()} Message handler error:`, err);
1308
1364
  });
1309
- // Observer forward: inbound
1310
- this.forwardToOwners('inbound', {
1311
- from: event.userId || event.channelId || '',
1312
- to: this.config.aid,
1313
- seq: event.seq,
1314
- payload: { type: 'text', text: event.text },
1315
- });
1365
+ }
1366
+ // ── 观察者模式(Observer Mode) ──────────────────────────────
1367
+ //
1368
+ // observable=true 时,Agent 收发的每条 AUN 消息(原始信封 + 解密后明文
1369
+ // payload)镜像一份给 owners[]。入站在所有过滤之前转发;出站在真实发送
1370
+ // 成功后转发。外层一律明文。详见 docs/observer-mode-design.md。
1371
+ // observable / owners 不在此处缓存——由 daemon 注入 resolver,从 EvolAgent 的
1372
+ // in-memory merged config(启动/重启/热重载时统一更新的唯一缓存)读取,避免重复缓存。
1373
+ observerConfigResolver;
1374
+ /** 注入观察者配置读取器(daemon 侧从 EvolAgent merged config 读)。 */
1375
+ setObserverConfigResolver(fn) {
1376
+ this.observerConfigResolver = fn;
1377
+ }
1378
+ /** 读取 observable 开关 + owners;无 resolver(如未接入 daemon)时视为关闭。 */
1379
+ getObserverConfig() {
1380
+ return this.observerConfigResolver?.() ?? { observable: false, owners: [] };
1381
+ }
1382
+ /**
1383
+ * 入站转发:到达本 AID 的消息全部转发,排除 self-echo。
1384
+ * 若消息发送方本身是 owner,则不转发给该 owner,但仍转发给其他 owner。
1385
+ * 调用点须在所有过滤逻辑之前,payload 为 SDK 解密后的明文。
1386
+ */
1387
+ forwardInbound(from, seq, payload) {
1388
+ if (!this.connected || !this.client)
1389
+ return;
1390
+ const { observable, owners } = this.getObserverConfig();
1391
+ if (!observable || owners.length === 0)
1392
+ return;
1393
+ if (this._aid && from === this._aid)
1394
+ return; // self-echo:已在出站转过
1395
+ // 排除来源 owner(不把"owner A 发来的"再转回 A),但仍转给其他 owner。
1396
+ const recipientOwners = owners.filter(o => o !== from);
1397
+ if (recipientOwners.length === 0)
1398
+ return;
1399
+ this.emitForward('inbound', { from, to: this.config.aid, seq, payload }, recipientOwners);
1316
1400
  }
1317
1401
  /**
1318
- * 观察者模式转发:将消息副本以 observer.forward 格式发给所有 owners。
1319
- * 仅在 AgentConfig.observable === true 时执行;owners 为空或无法加载配置时静默跳过。
1402
+ * 出站转发:Agent AUN 真实发出的消息全部转发。
1403
+ * to 为对端(私聊 AID / ID),payload 为实际发送的明文 payload。
1404
+ * 若 to 本身是 owner,排除该 owner(不把"回复 A"转发给 A 自己)。
1320
1405
  */
1321
- forwardToOwners(direction, original) {
1406
+ forwardOutbound(to, payload) {
1322
1407
  if (!this.connected || !this.client)
1323
1408
  return;
1324
- const agentConfig = loadAgent(this.config.aid);
1325
- if (!agentConfig?.observable)
1409
+ const { observable, owners } = this.getObserverConfig();
1410
+ if (!observable || owners.length === 0)
1326
1411
  return;
1327
- const owners = agentConfig.owners ?? [];
1328
- if (owners.length === 0)
1412
+ // 过滤:若 to 本身是 owner,不转发给该 owner(避免"回复你"转给你自己);
1413
+ // 但仍转发给其他 owner。
1414
+ const recipientOwners = owners.filter(o => o !== to);
1415
+ if (recipientOwners.length === 0)
1329
1416
  return;
1417
+ this.emitForward('outbound', { from: this.config.aid, to, payload }, recipientOwners);
1418
+ }
1419
+ /** 实际投递 observer.forward 给每个 owner,外层一律明文。 */
1420
+ emitForward(direction, original, owners) {
1330
1421
  const forwardPayload = {
1331
1422
  type: 'observer.forward',
1332
1423
  direction,
@@ -1340,8 +1431,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1340
1431
  },
1341
1432
  };
1342
1433
  for (const ownerAid of owners) {
1343
- const encrypt = this.shouldEncrypt(ownerAid);
1344
- this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt })
1434
+ this.callAndTrace('message.send', { to: ownerAid, payload: forwardPayload, encrypt: false })
1345
1435
  .catch(e => logger.debug(`${this.logPrefix()} observer.forward to ${ownerAid} failed: ${e}`));
1346
1436
  }
1347
1437
  }
@@ -1820,12 +1910,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1820
1910
  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 });
1821
1911
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1822
1912
  this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
1823
- // Observer forward: outbound (group)
1824
- this.forwardToOwners('outbound', {
1825
- from: this.config.aid,
1826
- to: channelId,
1827
- payload: { type: 'text', text: finalText },
1828
- });
1913
+ // Observer forward: outbound (group) — 转发实际发出的明文 payload
1914
+ this.forwardOutbound(channelId, payload);
1829
1915
  }
1830
1916
  }
1831
1917
  else {
@@ -1839,12 +1925,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1839
1925
  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 });
1840
1926
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
1841
1927
  this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
1842
- // Observer forward: outbound (private)
1843
- this.forwardToOwners('outbound', {
1844
- from: this.config.aid,
1845
- to: targetAid,
1846
- payload: { type: 'text', text: finalText },
1847
- });
1928
+ // Observer forward: outbound (private) — 转发实际发出的明文 payload
1929
+ this.forwardOutbound(targetAid, payload);
1848
1930
  }
1849
1931
  }
1850
1932
  return true;
@@ -1862,6 +1944,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1862
1944
  if (!result || !result.message_id) {
1863
1945
  logger.warn(`${this.logPrefix()} group.send fallback returned no message_id: ${JSON.stringify(result)}`);
1864
1946
  }
1947
+ this.forwardOutbound(channelId, payload);
1865
1948
  }
1866
1949
  else {
1867
1950
  this.trace('OUT', 'message.send.fallback', params);
@@ -1870,6 +1953,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1870
1953
  if (!result || !result.message_id) {
1871
1954
  logger.warn(`${this.logPrefix()} message.send fallback returned no message_id: ${JSON.stringify(result)}`);
1872
1955
  }
1956
+ this.forwardOutbound(targetAid, payload);
1873
1957
  }
1874
1958
  return true;
1875
1959
  }
@@ -1965,6 +2049,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1965
2049
  const tid = putRes?.thought_id;
1966
2050
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1967
2051
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
2052
+ this.forwardOutbound(channelId, payload);
1968
2053
  if (thoughtText) {
1969
2054
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
1970
2055
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
@@ -1976,6 +2061,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1976
2061
  const tid = putRes?.thought_id;
1977
2062
  logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1978
2063
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
2064
+ this.forwardOutbound(channelId, payload);
1979
2065
  if (thoughtText) {
1980
2066
  this.aidStatsCollector?.recordOutbound(this.config.aid, targetId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
1981
2067
  this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
@@ -2012,12 +2098,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2012
2098
  const result = await this.callAndTrace('group.send', params);
2013
2099
  const mid = result?.message?.message_id ?? result?.message_id ?? null;
2014
2100
  logger.info(`${this.logPrefix()} group.send (${payload.type}) ok: group=${channelId} mid=${mid} encrypt=${encrypt}`);
2101
+ this.forwardOutbound(channelId, finalPayload);
2015
2102
  return mid;
2016
2103
  }
2017
2104
  else {
2018
2105
  params.to = targetAid;
2019
2106
  const result = await this.callAndTrace('message.send', params);
2020
2107
  logger.info(`${this.logPrefix()} message.send (${payload.type}) ok: to=${this.peerLabel(targetAid)} mid=${result?.message_id} encrypt=${encrypt}`);
2108
+ this.forwardOutbound(targetAid, finalPayload);
2021
2109
  return result?.message_id ?? null;
2022
2110
  }
2023
2111
  }
@@ -2187,6 +2275,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2187
2275
  }
2188
2276
  }
2189
2277
  logger.info(`${this.logPrefix()} File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
2278
+ this.forwardOutbound(channelId, filePayload);
2190
2279
  return true;
2191
2280
  }
2192
2281
  catch (e) {
@@ -2302,6 +2391,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2302
2391
  };
2303
2392
  const method = isGroup ? 'group.send' : 'message.send';
2304
2393
  sendOne(method, statusPayload, 'status');
2394
+ this.forwardOutbound(channelId, statusPayload);
2305
2395
  this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, JSON.stringify(statusPayload).length, undefined, true);
2306
2396
  // 群聊显示 group id 简称,P2P 显示 peer label;从 context.metadata 读取 chatmode
2307
2397
  const targetLabel = this.isGroupId(channelId) ? channelId : this.peerLabel(channelId);
@@ -2499,6 +2589,32 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2499
2589
  const result = await agentmdSync(aid, { store: this.store ?? undefined });
2500
2590
  return result.content ?? '';
2501
2591
  }
2592
+ /**
2593
+ * 取群显示名(group.get → group.name),进程内缓存。
2594
+ * 走长连接 callAndTrace,失败/未连接返回 undefined —— 绝不抛出阻塞消息处理。
2595
+ */
2596
+ async getGroupName(groupId) {
2597
+ if (!groupId)
2598
+ return undefined;
2599
+ const cached = this.groupNameCache.get(groupId);
2600
+ if (cached !== undefined)
2601
+ return cached || undefined;
2602
+ if (!this.client)
2603
+ return undefined;
2604
+ try {
2605
+ const result = await this.callAndTrace('group.get', { group_id: groupId });
2606
+ const name = result?.group?.name;
2607
+ if (typeof name === 'string' && name) {
2608
+ this.groupNameCache.set(groupId, name);
2609
+ return name;
2610
+ }
2611
+ this.groupNameCache.set(groupId, ''); // 负缓存:避免反复 RPC(空串视为无名)
2612
+ return undefined;
2613
+ }
2614
+ catch {
2615
+ return undefined; // 不写缓存,下次仍可重试
2616
+ }
2617
+ }
2502
2618
  }
2503
2619
  // Plugin implementation
2504
2620
  export class AUNChannelPlugin {
@@ -2663,7 +2779,8 @@ export class AUNChannelPlugin {
2663
2779
  logger.warn(`[AUN] Unhandled payload kind: ${payload.kind}`);
2664
2780
  }
2665
2781
  }, acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); }, onInteraction: (cb) => { channel.interactionCallback = cb; }, uploadAgentMd: (content) => channel.uploadAgentMd(content),
2666
- downloadAgentMd: (aid) => channel.downloadAgentMd(aid), _selfAid: () => channel.getStatus().aid,
2782
+ downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
2783
+ getGroupName: (groupId) => channel.getGroupName(groupId), _selfAid: () => channel.getStatus().aid,
2667
2784
  _selfName: () => channel.getSelfName(),
2668
2785
  };
2669
2786
  const policy = {
@@ -2710,24 +2827,7 @@ export class AUNChannelPlugin {
2710
2827
  onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
2711
2828
  registerBridge(bridge, channelType) {
2712
2829
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async (opts) => {
2713
- handler({
2714
- channel: adapter.channelName,
2715
- channelType,
2716
- channelId: opts.channelId,
2717
- selfAID: opts.selfAID,
2718
- groupId: opts.groupId,
2719
- content: opts.content,
2720
- chatType: opts.chatType || 'private',
2721
- peerId: opts.peerId || '',
2722
- peerName: opts.peerName,
2723
- peerType: opts.peerType,
2724
- messageId: opts.messageId,
2725
- mentions: opts.mentions,
2726
- threadId: opts.threadId,
2727
- replyContext: opts.replyContext,
2728
- source: opts.source,
2729
- images: opts.images,
2730
- });
2830
+ handler(aunOptsToInbound(opts, adapter.channelName, channelType));
2731
2831
  }), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
2732
2832
  },
2733
2833
  registerHooks(ctx) {
@@ -207,7 +207,7 @@ export class FeishuChannel {
207
207
  // 清理残留的 mention 占位符(@_user_N 代表机器人)
208
208
  content = content.replace(/@_user_\d+/g, '').trim();
209
209
  const finalContent = quotedText + content;
210
- await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: quotedImages.length > 0 ? quotedImages : undefined, peerId, peerName, messageId: msg.message_id, mentions: mentions.length > 0 ? mentions : undefined, threadId, rootId, chatType });
210
+ await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: quotedImages.length > 0 ? quotedImages : undefined, peerId, peerName, messageId: msg.message_id, mentions: mentions.length > 0 ? mentions : undefined, mentionAids: mentions.length > 0 ? mentions.map((m) => m.userId) : undefined, threadId, rootId, chatType });
211
211
  }
212
212
  // 处理图片消息
213
213
  else if (msg.message_type === 'image') {
@@ -1519,12 +1519,12 @@ export class FeishuChannelPlugin {
1519
1519
  disconnect: () => channel.disconnect(),
1520
1520
  onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
1521
1521
  registerBridge(bridge, channelType) {
1522
- bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType, source }) => {
1522
+ bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, mentionAids, threadId, rootId, chatType, source }) => {
1523
1523
  await handler({
1524
1524
  channel: adapter.channelName, channelType, channelId: chatId, content, images,
1525
1525
  selfAID: inst.agentName,
1526
1526
  chatType: chatType || 'private',
1527
- peerId: peerId || '', peerName, messageId, mentions, threadId,
1527
+ peerId: peerId || '', peerName, messageId, mentions, mentionAids, threadId,
1528
1528
  replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
1529
1529
  source,
1530
1530
  });
package/dist/cli/agent.js CHANGED
@@ -450,26 +450,32 @@ export async function agentCreateInteractive(opts = {}) {
450
450
  export async function agentCreateNonInteractive(opts) {
451
451
  const p = resolvePaths();
452
452
  const { isValidAid, aidCreate } = await import('../aun/aid/index.js');
453
+ opts.onPhase?.('validating', 'begin');
454
+ /** 校验失败:透出 failed 进度并返回原结构(控制流不变)。 */
455
+ const failValidating = (error) => {
456
+ opts.onPhase?.('validating', 'failed', error);
457
+ return { ok: false, error };
458
+ };
453
459
  if (!isValidAid(opts.aid)) {
454
- return { ok: false, error: `Invalid AID "${opts.aid}": must be a valid multi-level domain (e.g. mybot.agentid.pub)` };
460
+ return failValidating(`Invalid AID "${opts.aid}": must be a valid multi-level domain (e.g. mybot.agentid.pub)`);
455
461
  }
456
462
  const agentDirPath = path.join(p.agentsDir, opts.aid);
457
463
  const configExists = fs.existsSync(path.join(agentDirPath, 'config.json'));
458
464
  if (configExists && !opts.force) {
459
- return { ok: false, error: `Agent "${opts.aid}" already exists: ${agentDirPath}/config.json (use --force to overwrite)` };
465
+ return failValidating(`Agent "${opts.aid}" already exists: ${agentDirPath}/config.json (use --force to overwrite)`);
460
466
  }
461
467
  // Baseagent
462
468
  const available = detectAvailableBaseagents();
463
469
  if (available.length === 0) {
464
- return { ok: false, error: `No usable baseagent detected. Install claude/gemini CLI or optional dependency @openai/codex-sdk.` };
470
+ return failValidating(`No usable baseagent detected. Install claude/gemini CLI or optional dependency @openai/codex-sdk.`);
465
471
  }
466
472
  let baseagent;
467
473
  if (opts.baseagent) {
468
474
  if (!BASEAGENT_CANDIDATES.includes(opts.baseagent)) {
469
- return { ok: false, error: `Invalid baseagent: ${opts.baseagent} (options: ${BASEAGENT_CANDIDATES.join('/')})` };
475
+ return failValidating(`Invalid baseagent: ${opts.baseagent} (options: ${BASEAGENT_CANDIDATES.join('/')})`);
470
476
  }
471
477
  if (!available.includes(opts.baseagent)) {
472
- return { ok: false, error: `${opts.baseagent} is not available in the current environment (available: ${available.join('/')})` };
478
+ return failValidating(`${opts.baseagent} is not available in the current environment (available: ${available.join('/')})`);
473
479
  }
474
480
  baseagent = opts.baseagent;
475
481
  }
@@ -477,20 +483,22 @@ export async function agentCreateNonInteractive(opts) {
477
483
  baseagent = pickDefaultBaseagent(available);
478
484
  }
479
485
  if (!path.isAbsolute(opts.project)) {
480
- return { ok: false, error: `--project must be absolute: ${opts.project}` };
486
+ return failValidating(`--project must be absolute: ${opts.project}`);
481
487
  }
482
488
  if (!fs.existsSync(opts.project)) {
483
489
  try {
484
490
  fs.mkdirSync(opts.project, { recursive: true });
485
491
  }
486
492
  catch (e) {
487
- return { ok: false, error: `Failed to create ${opts.project}: ${e?.message || e}` };
493
+ return failValidating(`Failed to create ${opts.project}: ${e?.message || e}`);
488
494
  }
489
495
  }
490
496
  if (opts.owner && !isValidAid(opts.owner)) {
491
- return { ok: false, error: `Invalid owner: ${opts.owner}` };
497
+ return failValidating(`Invalid owner: ${opts.owner}`);
492
498
  }
499
+ opts.onPhase?.('validating', 'done');
493
500
  // Register AID
501
+ opts.onPhase?.('registering_aid', 'begin');
494
502
  let aidCreated = false;
495
503
  try {
496
504
  const result = await aidCreate(opts.aid);
@@ -499,9 +507,12 @@ export async function agentCreateNonInteractive(opts) {
499
507
  }
500
508
  catch { /* ignore */ }
501
509
  aidCreated = !result.alreadyExisted;
510
+ opts.onPhase?.('registering_aid', 'done', aidCreated ? 'created' : 'existed');
502
511
  }
503
512
  catch (e) {
504
- return { ok: false, error: `AID creation failed: ${e?.message || e}` };
513
+ const error = `AID creation failed: ${e?.message || e}`;
514
+ opts.onPhase?.('registering_aid', 'failed', error);
515
+ return { ok: false, error };
505
516
  }
506
517
  // Force 模式下若 agent 已存在且已 initialized,保留该状态(避免重复发欢迎)
507
518
  let preservedInitialized = false;
@@ -526,9 +537,12 @@ export async function agentCreateNonInteractive(opts) {
526
537
  chatmode: { ...DEFAULT_CHATMODE },
527
538
  dispatch: DEFAULT_DISPATCH,
528
539
  };
540
+ opts.onPhase?.('config_saved', 'begin');
529
541
  saveAgent(agentConfig);
530
542
  ensureAgentDirSkeleton(opts.aid);
543
+ opts.onPhase?.('config_saved', 'done');
531
544
  // Generate and upload agent.md
545
+ opts.onPhase?.('uploading_agentmd', 'begin');
532
546
  let agentmdUploaded = false;
533
547
  try {
534
548
  const { buildInitialAgentMd, agentmdPut } = await import('../aun/aid/index.js');
@@ -550,6 +564,7 @@ export async function agentCreateNonInteractive(opts) {
550
564
  await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
551
565
  await agentmdPut(content, { aid: opts.aid, aunPath });
552
566
  agentmdUploaded = true;
567
+ opts.onPhase?.('uploading_agentmd', 'done');
553
568
  break;
554
569
  }
555
570
  catch (e) {
@@ -559,11 +574,13 @@ export async function agentCreateNonInteractive(opts) {
559
574
  if (!agentmdUploaded) {
560
575
  console.warn(`⚠ agent.md upload failed: ${lastError?.message || lastError}`);
561
576
  console.warn(` Retry later with: evolclaw aid agentmd put ${opts.aid}`);
577
+ opts.onPhase?.('uploading_agentmd', 'warn', `upload failed: ${lastError?.message || lastError}`);
562
578
  }
563
579
  await new Promise(r => setTimeout(r, 0));
564
580
  }
565
581
  catch (e) {
566
582
  console.warn(`⚠ agent.md generation failed: ${e?.message || e}`);
583
+ opts.onPhase?.('uploading_agentmd', 'warn', `generation failed: ${e?.message || e}`);
567
584
  }
568
585
  // Attempt hot-load via IPC (if daemon is running).
569
586
  // Cold-starting a new agent (connecting AUN WebSocket) routinely takes
@@ -571,16 +588,24 @@ export async function agentCreateNonInteractive(opts) {
571
588
  // report while the daemon actually finishes bringing the agent online.
572
589
  let hotLoaded = false;
573
590
  let hotLoadError;
591
+ opts.onPhase?.('hot_loading', 'begin');
574
592
  try {
575
593
  const ipcResult = await ipcQuery(p.socket, { type: 'evolagent.load', aid: opts.aid }, 30_000);
576
594
  if (ipcResult?.ok) {
577
595
  hotLoaded = true;
596
+ opts.onPhase?.('hot_loading', 'done');
578
597
  }
579
598
  else if (ipcResult) {
580
599
  hotLoadError = ipcResult.error;
600
+ opts.onPhase?.('hot_loading', 'warn', hotLoadError);
601
+ }
602
+ else {
603
+ opts.onPhase?.('hot_loading', 'warn', 'daemon not running');
581
604
  }
582
605
  }
583
- catch { /* daemon not running */ }
606
+ catch {
607
+ opts.onPhase?.('hot_loading', 'warn', 'daemon not running'); /* daemon not running */
608
+ }
584
609
  return {
585
610
  ok: true,
586
611
  aid: opts.aid,
@@ -858,6 +883,9 @@ export async function agentDelete(aid, purge = false) {
858
883
  }
859
884
  else {
860
885
  fs.unlinkSync(configPath);
886
+ // 清理构建进度文件(非 purge 删除只移除 config.json,需显式清理 create-status.json)
887
+ const { removeCreateStatus } = await import('../core/message/create-status.js');
888
+ removeCreateStatus(agentDir);
861
889
  }
862
890
  // Trigger resync so daemon drops the agent
863
891
  try {