evolclaw 2.6.1 → 2.6.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.
@@ -58,9 +58,16 @@ export class AUNChannel {
58
58
  const logPath = path.join(resolvePaths().logs, `aun-${today}.log`);
59
59
  this.traceStream = fs.createWriteStream(logPath, { flags: 'a' });
60
60
  }
61
- /** 判断 channelId 是否为群组 ID(g-xxx.agentid.pub 或 grp_ 前缀) */
61
+ /** 判断 channelId 是否为群组 ID
62
+ * - 新格式:group.{issuer}/{group_no|group_name}
63
+ * - 数字群号:{group_no}.{issuer}(如 11117.agentid.pub)
64
+ * - 兼容旧格式:grp_xxx、g-xxx.agentid.pub
65
+ */
62
66
  isGroupId(id) {
63
- return id.startsWith('grp_') || (id.startsWith('g-') && id.includes('.'));
67
+ return (id.startsWith('group.') && id.includes('/'))
68
+ || /^\d+\./.test(id)
69
+ || id.startsWith('grp_')
70
+ || (id.startsWith('g-') && id.includes('.'));
64
71
  }
65
72
  getShortAid(aid) {
66
73
  if (!aid)
@@ -114,6 +121,7 @@ export class AUNChannel {
114
121
  this.messageSeqMap.delete(messageId);
115
122
  }
116
123
  _aid;
124
+ _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
117
125
  _chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
118
126
  seenMessages = new Map();
119
127
  peerInfoCache = new Map();
@@ -262,6 +270,7 @@ export class AUNChannel {
262
270
  this._aid = this.client.aid ?? undefined;
263
271
  const deviceId = this.client._device_id ?? '';
264
272
  this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
273
+ this._selfName = this.loadSelfName(aidName);
265
274
  this.connected = true;
266
275
  this.reconnectAttempt = 0;
267
276
  // Workaround: SDK e2ee uses _identity.cert for sender_cert_fingerprint;
@@ -388,7 +397,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
388
397
  - 所有命令以 \`/\` 开头
389
398
 
390
399
  现在,请先使用 \`/bind\` 命令绑定您的项目目录,然后就可以开始工作了!`;
391
- await this.sendMessage(owner, welcomeText);
400
+ // First contact with Owner races against Owner's async cert fetch from
401
+ // gateway PKI; a 3s pause lets the cert propagate. persist_required asks
402
+ // the gateway to durably store the message so Owner can recover it via
403
+ // pull if the initial E2EE push still arrives before the cert resolves.
404
+ await new Promise(resolve => setTimeout(resolve, 3000));
405
+ if (!this.client) {
406
+ logger.warn('[AUN] Client disconnected before welcome message could be sent');
407
+ return;
408
+ }
409
+ await this.client.call('message.send', {
410
+ to: owner,
411
+ payload: { type: 'text', text: welcomeText },
412
+ encrypt: true,
413
+ persist_required: true,
414
+ });
392
415
  logger.info(`[AUN] Welcome message sent to owner: ${owner}`);
393
416
  }
394
417
  catch (e) {
@@ -725,7 +748,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
725
748
  const payload = { type: 'text', text: finalText };
726
749
  if (context?.threadId)
727
750
  payload.thread_id = context.threadId;
728
- const params = { payload, encrypt: true };
751
+ const isGroup = this.isGroupId(channelId);
752
+ const params = { payload, encrypt: !isGroup };
729
753
  // Multi-instance routing: channelId may be "aid:device_id:slot_id"
730
754
  const colonIdx = channelId.indexOf(':');
731
755
  const targetAid = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
@@ -733,7 +757,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
733
757
  params.payload.chat_id = channelId;
734
758
  }
735
759
  try {
736
- if (this.isGroupId(channelId)) {
760
+ if (isGroup) {
737
761
  params.group_id = channelId;
738
762
  this.trace('OUT', 'group.send', params);
739
763
  await this.client.call('group.send', params);
@@ -783,8 +807,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
783
807
  }
784
808
  }
785
809
  catch (e) {
786
- this.trace('OUT', 'thought.put.error', { channelId, error: String(e) });
787
- logger.debug(`[AUN] thought.put failed to ${channelId}: ${e}`);
810
+ const err = e;
811
+ this.trace('OUT', 'thought.put.error', {
812
+ channelId,
813
+ errorName: err?.name,
814
+ errorCode: err?.code,
815
+ errorMessage: err?.message,
816
+ });
817
+ logger.debug(`[AUN] thought.put failed to ${channelId}: ${err?.name}(${err?.code})=${err?.message}`);
788
818
  }
789
819
  }
790
820
  async sendFile(channelId, filePath, context) {
@@ -860,14 +890,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
860
890
  };
861
891
  if (context?.threadId)
862
892
  filePayload.thread_id = context.threadId;
863
- const params = { payload: filePayload, encrypt: true };
893
+ const isGroup = this.isGroupId(channelId);
894
+ const params = { payload: filePayload, encrypt: !isGroup };
864
895
  // Multi-instance routing
865
896
  const fileColonIdx = channelId.indexOf(':');
866
897
  const fileTargetAid = fileColonIdx > 0 ? channelId.substring(0, fileColonIdx) : channelId;
867
898
  if (fileColonIdx > 0) {
868
899
  params.payload.chat_id = channelId;
869
900
  }
870
- if (this.isGroupId(channelId)) {
901
+ if (isGroup) {
871
902
  params.group_id = channelId;
872
903
  this.trace('OUT', 'group.send.file', params);
873
904
  await this.client.call('group.send', params);
@@ -909,14 +940,15 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
909
940
  };
910
941
  if (context?.threadId)
911
942
  payload.thread_id = context.threadId;
912
- const params = { payload, encrypt: true };
943
+ const isGroup = this.isGroupId(channelId);
944
+ const params = { payload, encrypt: !isGroup };
913
945
  // Multi-instance routing
914
946
  const statusColonIdx = channelId.indexOf(':');
915
947
  const statusTargetAid = statusColonIdx > 0 ? channelId.substring(0, statusColonIdx) : channelId;
916
948
  if (statusColonIdx > 0) {
917
949
  payload.chat_id = channelId;
918
950
  }
919
- if (this.isGroupId(channelId)) {
951
+ if (isGroup) {
920
952
  params.group_id = channelId;
921
953
  this.trace('OUT', 'group.send.status', params);
922
954
  this.client.call('group.send', params).catch(e => {
@@ -1038,6 +1070,27 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1038
1070
  maxAttempts: AUNChannel.RECONNECT_DELAYS.length,
1039
1071
  };
1040
1072
  }
1073
+ /** 读取本地 agent.md 中的 name(用于身份上下文展示) */
1074
+ loadSelfName(aid) {
1075
+ try {
1076
+ const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
1077
+ const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
1078
+ if (!fs.existsSync(agentMdPath))
1079
+ return undefined;
1080
+ const content = fs.readFileSync(agentMdPath, 'utf-8');
1081
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
1082
+ if (!fmMatch)
1083
+ return undefined;
1084
+ const nameMatch = fmMatch[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
1085
+ return nameMatch?.[1]?.trim() || undefined;
1086
+ }
1087
+ catch {
1088
+ return undefined;
1089
+ }
1090
+ }
1091
+ getSelfName() {
1092
+ return this._selfName;
1093
+ }
1041
1094
  async fetchPeerInfo(aid) {
1042
1095
  const cached = this.peerInfoCache.get(aid);
1043
1096
  if (cached !== undefined)
@@ -1109,6 +1162,7 @@ export class AUNChannelPlugin {
1109
1162
  downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
1110
1163
  putThought: (id, taskId, payload) => channel.sendThought(id, taskId, payload),
1111
1164
  _selfAid: () => channel.getStatus().aid,
1165
+ _selfName: () => channel.getSelfName(),
1112
1166
  };
1113
1167
  const policy = {
1114
1168
  canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
@@ -525,10 +525,10 @@ export class CommandHandler {
525
525
  const isAdmin = identity.role === 'owner' || identity.role === 'admin';
526
526
  const activeChatType = activeSession?.chatType || 'private';
527
527
  if (normalizedContent.startsWith('/')) {
528
- const guestGroupCommands = ['/status', '/help', '/check'];
528
+ const guestGroupCommands = ['/status', '/help', '/check', '/chatmode'];
529
529
  const userCommands = activeChatType === 'group' && !isAdmin
530
530
  ? guestGroupCommands
531
- : ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s ', '/check'];
531
+ : ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s ', '/check', '/chatmode'];
532
532
  const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
533
533
  if (!isUserCommand && !isAdmin) {
534
534
  return activeChatType === 'group'
@@ -1386,9 +1386,9 @@ export class CommandHandler {
1386
1386
  return `✅ 中间输出模式: ${activityArg}(${label})`;
1387
1387
  }
1388
1388
  // /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
1389
+ // - 查看:所有人可用
1390
+ // - 设置:单聊任何角色可设置;群聊仅管理员可设置
1389
1391
  if (normalizedContent === '/chatmode' || normalizedContent.startsWith('/chatmode ')) {
1390
- if (!isAdmin)
1391
- return '❌ 无权限:此命令仅限管理员使用';
1392
1392
  if (!activeSession)
1393
1393
  return '❌ 当前无活跃会话';
1394
1394
  const lockedMode = getChannelSessionMode(this.config, channel);
@@ -1401,6 +1401,9 @@ export class CommandHandler {
1401
1401
  if (arg !== 'interactive' && arg !== 'proactive') {
1402
1402
  return `❌ 无效模式: ${arg}\n可选: interactive / proactive`;
1403
1403
  }
1404
+ if (activeChatType === 'group' && !isAdmin) {
1405
+ return '❌ 无权限:群聊中切换会话模式仅限管理员使用';
1406
+ }
1404
1407
  if (lockedMode) {
1405
1408
  return `❌ 会话模式由通道配置锁定为 ${lockedMode},无法切换`;
1406
1409
  }
@@ -1551,15 +1554,18 @@ export class CommandHandler {
1551
1554
  }
1552
1555
  }
1553
1556
  const lines = [];
1557
+ const sessionMode = session.sessionMode || 'interactive';
1558
+ const lockedMode = getChannelSessionMode(this.config, channel);
1559
+ const chatModeLine = `会话模式: ${sessionMode}${lockedMode ? '(通道锁定)' : ''}`;
1554
1560
  if (isAdmin) {
1555
- lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`);
1561
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`);
1556
1562
  if (health.consecutiveErrors > 0) {
1557
1563
  lines.push(`异常计数: ${health.consecutiveErrors}`);
1558
1564
  }
1559
1565
  lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
1560
1566
  }
1561
1567
  else {
1562
- lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
1568
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
1563
1569
  }
1564
1570
  if (health.lastError) {
1565
1571
  lines.push('');
@@ -389,15 +389,28 @@ export class MessageProcessor {
389
389
  const peerLabel = session.identity?.role || 'unknown';
390
390
  const peerName = message.peerName || session.metadata?.peerName;
391
391
  const peerType = message.peerType;
392
+ const peerId = message.peerId;
393
+ const adapterAny = channelInfo.adapter;
394
+ const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
395
+ const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
396
+ const formatIdentity = (name, id) => {
397
+ if (name && id)
398
+ return `${name} (${id})`;
399
+ return name || id || undefined;
400
+ };
401
+ const selfIdentity = formatIdentity(selfName, selfAid);
402
+ const peerIdentity = formatIdentity(peerName, peerId);
392
403
  const envParts = [
393
404
  `会话通道: ${currentChannelType}`,
394
405
  `当前项目: ${path.basename(absoluteProjectPath)}`,
395
406
  ];
396
407
  if (session.name)
397
408
  envParts.push(`会话名称: ${session.name}`);
409
+ if (selfIdentity)
410
+ envParts.push(`当前名称: ${selfIdentity}`);
398
411
  envParts.push(`对端身份: ${peerLabel}`);
399
- if (peerName)
400
- envParts.push(`对端名称: ${peerName}`);
412
+ if (peerIdentity)
413
+ envParts.push(`对端名称: ${peerIdentity}`);
401
414
  if (peerType && peerType !== 'unknown')
402
415
  envParts.push(`对端类型: ${peerType}`);
403
416
  if (session.chatType)
@@ -51,7 +51,7 @@ export class ThoughtEmitter {
51
51
  return null;
52
52
  return { type: 'thought', text: event.text, stage: 'thinking' };
53
53
  case 'tool_use': {
54
- const desc = this.summarizeInput(event.input);
54
+ const desc = this.summarizeInput(event.input, event.name);
55
55
  return {
56
56
  type: 'thought',
57
57
  text: desc ? `🔧 ${event.name}: ${desc}` : `🔧 ${event.name}`,
@@ -109,9 +109,16 @@ export class ThoughtEmitter {
109
109
  return null;
110
110
  }
111
111
  }
112
- summarizeInput(input) {
112
+ summarizeInput(input, toolName) {
113
113
  if (!input || typeof input !== 'object')
114
114
  return '';
115
+ // Bash + ctl send/file: 显示完整命令内容(含发送的消息正文)
116
+ if (toolName === 'Bash' && typeof input.command === 'string') {
117
+ const cmd = input.command;
118
+ if (cmd.includes('evolclaw ctl send') || cmd.includes('evolclaw ctl file')) {
119
+ return cmd;
120
+ }
121
+ }
115
122
  return (input.description ||
116
123
  input.file_path ||
117
124
  input.pattern ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.6.1",
3
+ "version": "2.6.2",
4
4
  "description": "Lightweight AI Agent gateway connecting Claude Agent SDK to messaging channels (Feishu, ACP) with multi-project session management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",