evolclaw 2.6.2 → 2.6.3

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.
@@ -1,4 +1,4 @@
1
- import { AUNClient, GatewayDiscovery } from '@agentunion/fastaun';
1
+ import { AUNClient, GatewayDiscovery, E2EEError } from '@agentunion/fastaun';
2
2
  import crypto from 'crypto';
3
3
  import fs from 'fs';
4
4
  import path from 'path';
@@ -120,6 +120,16 @@ export class AUNChannel {
120
120
  if (messageId)
121
121
  this.messageSeqMap.delete(messageId);
122
122
  }
123
+ shouldEncrypt(peerId) {
124
+ const cached = this.peerE2ee.get(peerId);
125
+ if (!cached)
126
+ return true;
127
+ if (Date.now() - cached.ts > AUNChannel.E2EE_PROBE_TTL) {
128
+ this.peerE2ee.delete(peerId);
129
+ return true;
130
+ }
131
+ return cached.ok;
132
+ }
123
133
  _aid;
124
134
  _selfName; // 本地 agent.md 中的 name,首次 connect 时读取
125
135
  _chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
@@ -127,6 +137,9 @@ export class AUNChannel {
127
137
  peerInfoCache = new Map();
128
138
  messageSeqMap = new Map(); // messageId → seq (for ack)
129
139
  sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
140
+ peerE2ee = new Map();
141
+ static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
142
+ plaintextRecv = 0;
130
143
  // Reconnect state (TS-layer fallback, on top of SDK auto_reconnect)
131
144
  intentionalDisconnect = false;
132
145
  reconnectAttempt = 0;
@@ -161,7 +174,7 @@ export class AUNChannel {
161
174
  this.client = null;
162
175
  }
163
176
  this.connected = false;
164
- const aunPath = this.config.keystorePath || `${process.env.HOME || '~'}/.aun`;
177
+ const aunPath = this.config.keystorePath || path.join(os.homedir(), '.aun');
165
178
  const aidName = this.config.aid;
166
179
  const encryptionSeed = this.config.encryptionSeed || process.env.AUN_ENCRYPTION_SEED || undefined;
167
180
  // Gateway URL 解析:优先用配置的 gatewayUrl,否则通过 well-known 自动发现
@@ -185,12 +198,12 @@ export class AUNChannel {
185
198
  logger.info(`[AUN] Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
186
199
  // Create client with FileSecretStore (AES-256-GCM)
187
200
  // 不传 encryption_seed 时,SDK 自动从 {aun_path}/.seed 文件派生密钥(与 aun_cli.py 对齐)
188
- const rootCaPath = `${aunPath}/CA/root/root.crt`;
201
+ const rootCaPath = path.join(aunPath, 'CA', 'root', 'root.crt');
189
202
  this.client = new AUNClient({
190
203
  aun_path: aunPath,
191
204
  root_ca_path: rootCaPath,
192
205
  ...(encryptionSeed && { encryption_seed: encryptionSeed }),
193
- });
206
+ }, this.config.aunSdkLog ?? true);
194
207
  // Set gateway URL (internal property, same as Python SDK)
195
208
  this.client._gatewayUrl = gateway;
196
209
  // Register event handlers before connecting
@@ -226,6 +239,16 @@ export class AUNChannel {
226
239
  }
227
240
  }
228
241
  });
242
+ this.client.on('message.undecryptable', (data) => {
243
+ this.trace('IN', 'message.undecryptable', data);
244
+ const d = data;
245
+ logger.warn(`[AUN] Message undecryptable: from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
246
+ });
247
+ this.client.on('group.message_undecryptable', (data) => {
248
+ this.trace('IN', 'group.message_undecryptable', data);
249
+ const d = data;
250
+ logger.warn(`[AUN] Group message undecryptable: group=${d.group_id} from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
251
+ });
229
252
  // Authenticate
230
253
  // Workaround: SDK 0.3.x _loadIdentityOrRaise doesn't set identity.aid from requested aid,
231
254
  // causing gateway "missing aid" error. Patch to backfill aid on loaded identity.
@@ -494,6 +517,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
494
517
  this.acknowledgeImmediately(messageId, seq);
495
518
  return;
496
519
  }
520
+ // E2EE 能力探测:收到加密消息则标记对端支持,明文则计数审计
521
+ const msgEncrypted = !!(msg.e2ee);
522
+ if (fromAid) {
523
+ if (msgEncrypted) {
524
+ this.peerE2ee.set(fromAid, { ok: true, ts: Date.now() });
525
+ }
526
+ else {
527
+ this.plaintextRecv++;
528
+ }
529
+ }
497
530
  // Detect @mentions
498
531
  const mentions = [];
499
532
  if (this._aid && text.includes(`@${this._aid}`)) {
@@ -566,6 +599,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
566
599
  this.acknowledgeImmediately(messageId, seq);
567
600
  return;
568
601
  }
602
+ // E2EE 能力探测:收到加密群消息则标记发送者支持
603
+ const msgEncrypted = !!(msg.e2ee);
604
+ if (senderAid) {
605
+ if (msgEncrypted) {
606
+ this.peerE2ee.set(senderAid, { ok: true, ts: Date.now() });
607
+ }
608
+ else {
609
+ this.plaintextRecv++;
610
+ }
611
+ }
569
612
  // dispatch_mode from server tells agent how to work in this group
570
613
  const dispatchMode = msg.dispatch_mode ?? payload?.dispatch_mode ?? 'mention';
571
614
  const mentionedSelf = this._aid
@@ -748,29 +791,68 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
748
791
  const payload = { type: 'text', text: finalText };
749
792
  if (context?.threadId)
750
793
  payload.thread_id = context.threadId;
794
+ if (context?.metadata?.taskId)
795
+ payload.task_id = context.metadata.taskId;
796
+ if (context?.metadata?.chatmode)
797
+ payload.chatmode = context.metadata.chatmode;
751
798
  const isGroup = this.isGroupId(channelId);
752
- const params = { payload, encrypt: !isGroup };
753
799
  // Multi-instance routing: channelId may be "aid:device_id:slot_id"
754
800
  const colonIdx = channelId.indexOf(':');
755
801
  const targetAid = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
756
802
  if (colonIdx > 0) {
757
- params.payload.chat_id = channelId;
803
+ payload.chat_id = channelId;
758
804
  }
805
+ const encryptTarget = isGroup ? channelId : targetAid;
806
+ const encrypt = this.shouldEncrypt(encryptTarget);
807
+ const params = { payload, encrypt };
759
808
  try {
760
809
  if (isGroup) {
761
810
  params.group_id = channelId;
762
811
  this.trace('OUT', 'group.send', params);
763
- await this.client.call('group.send', params);
812
+ const result = await this.client.call('group.send', params);
813
+ if (!result || !result.message_id) {
814
+ logger.warn(`[AUN] group.send returned no message_id: ${JSON.stringify(result)}`);
815
+ }
764
816
  }
765
817
  else {
766
818
  params.to = targetAid;
767
819
  this.trace('OUT', 'message.send', params);
768
- await this.client.call('message.send', params);
820
+ const result = await this.client.call('message.send', params);
821
+ if (!result || !result.message_id) {
822
+ logger.warn(`[AUN] message.send returned no message_id: ${JSON.stringify(result)}`);
823
+ }
769
824
  }
770
825
  }
771
826
  catch (e) {
772
- this.trace('OUT', 'send.error', { channelId, error: String(e) });
773
- logger.error(`[AUN] Send failed to ${channelId}: ${e}`);
827
+ if (encrypt && e instanceof E2EEError) {
828
+ this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
829
+ logger.warn(`[AUN] E2EE send failed to ${channelId}, retrying plaintext: ${e}`);
830
+ params.encrypt = false;
831
+ try {
832
+ if (isGroup) {
833
+ this.trace('OUT', 'group.send.fallback', params);
834
+ const result = await this.client.call('group.send', params);
835
+ if (!result || !result.message_id) {
836
+ logger.warn(`[AUN] group.send fallback returned no message_id: ${JSON.stringify(result)}`);
837
+ }
838
+ }
839
+ else {
840
+ this.trace('OUT', 'message.send.fallback', params);
841
+ const result = await this.client.call('message.send', params);
842
+ if (!result || !result.message_id) {
843
+ logger.warn(`[AUN] message.send fallback returned no message_id: ${JSON.stringify(result)}`);
844
+ }
845
+ }
846
+ }
847
+ catch (e2) {
848
+ this.trace('OUT', 'send.fallback.error', { channelId, error: String(e2) });
849
+ logger.error(`[AUN] Plaintext fallback also failed to ${channelId}: ${e2}`);
850
+ }
851
+ }
852
+ else {
853
+ this.trace('OUT', 'send.error', { channelId, error: String(e) });
854
+ logger.error(`[AUN] Send failed to ${channelId}: ${e}`);
855
+ }
774
856
  }
775
857
  }
776
858
  /**
@@ -890,23 +972,61 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
890
972
  };
891
973
  if (context?.threadId)
892
974
  filePayload.thread_id = context.threadId;
975
+ if (context?.metadata?.taskId)
976
+ filePayload.task_id = context.metadata.taskId;
977
+ if (context?.metadata?.chatmode)
978
+ filePayload.chatmode = context.metadata.chatmode;
893
979
  const isGroup = this.isGroupId(channelId);
894
- const params = { payload: filePayload, encrypt: !isGroup };
895
980
  // Multi-instance routing
896
981
  const fileColonIdx = channelId.indexOf(':');
897
982
  const fileTargetAid = fileColonIdx > 0 ? channelId.substring(0, fileColonIdx) : channelId;
898
983
  if (fileColonIdx > 0) {
899
- params.payload.chat_id = channelId;
984
+ filePayload.chat_id = channelId;
900
985
  }
901
- if (isGroup) {
902
- params.group_id = channelId;
903
- this.trace('OUT', 'group.send.file', params);
904
- await this.client.call('group.send', params);
986
+ const encryptTarget = isGroup ? channelId : fileTargetAid;
987
+ const encrypt = this.shouldEncrypt(encryptTarget);
988
+ const params = { payload: filePayload, encrypt };
989
+ try {
990
+ if (isGroup) {
991
+ params.group_id = channelId;
992
+ this.trace('OUT', 'group.send.file', params);
993
+ const result = await this.client.call('group.send', params);
994
+ if (!result || !result.message_id) {
995
+ logger.warn(`[AUN] group.send.file returned no message_id: ${JSON.stringify(result)}`);
996
+ }
997
+ }
998
+ else {
999
+ params.to = fileTargetAid;
1000
+ this.trace('OUT', 'message.send.file', params);
1001
+ const result = await this.client.call('message.send', params);
1002
+ if (!result || !result.message_id) {
1003
+ logger.warn(`[AUN] message.send.file returned no message_id: ${JSON.stringify(result)}`);
1004
+ }
1005
+ }
905
1006
  }
906
- else {
907
- params.to = fileTargetAid;
908
- this.trace('OUT', 'message.send.file', params);
909
- await this.client.call('message.send', params);
1007
+ catch (sendErr) {
1008
+ if (encrypt && sendErr instanceof E2EEError) {
1009
+ this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
1010
+ logger.warn(`[AUN] E2EE sendFile failed to ${channelId}, retrying plaintext: ${sendErr}`);
1011
+ params.encrypt = false;
1012
+ if (isGroup) {
1013
+ this.trace('OUT', 'group.send.file.fallback', params);
1014
+ const result = await this.client.call('group.send', params);
1015
+ if (!result || !result.message_id) {
1016
+ logger.warn(`[AUN] group.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
1017
+ }
1018
+ }
1019
+ else {
1020
+ this.trace('OUT', 'message.send.file.fallback', params);
1021
+ const result = await this.client.call('message.send', params);
1022
+ if (!result || !result.message_id) {
1023
+ logger.warn(`[AUN] message.send.file fallback returned no message_id: ${JSON.stringify(result)}`);
1024
+ }
1025
+ }
1026
+ }
1027
+ else {
1028
+ throw sendErr;
1029
+ }
910
1030
  }
911
1031
  logger.info(`[AUN] File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
912
1032
  }
@@ -941,26 +1061,39 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
941
1061
  if (context?.threadId)
942
1062
  payload.thread_id = context.threadId;
943
1063
  const isGroup = this.isGroupId(channelId);
944
- const params = { payload, encrypt: !isGroup };
945
1064
  // Multi-instance routing
946
1065
  const statusColonIdx = channelId.indexOf(':');
947
1066
  const statusTargetAid = statusColonIdx > 0 ? channelId.substring(0, statusColonIdx) : channelId;
948
1067
  if (statusColonIdx > 0) {
949
1068
  payload.chat_id = channelId;
950
1069
  }
1070
+ const encryptTarget = isGroup ? channelId : statusTargetAid;
1071
+ const encrypt = this.shouldEncrypt(encryptTarget);
1072
+ const params = { payload, encrypt };
1073
+ const sendWithFallback = (method) => {
1074
+ this.client.call(method, params).catch(e => {
1075
+ if (encrypt && e instanceof E2EEError) {
1076
+ this.peerE2ee.set(encryptTarget, { ok: false, ts: Date.now() });
1077
+ logger.warn(`[AUN] E2EE status send failed to ${channelId}, retrying plaintext`);
1078
+ params.encrypt = false;
1079
+ this.client.call(method, params).catch(e2 => {
1080
+ logger.debug(`[AUN] Processing status fallback failed: ${e2}`);
1081
+ });
1082
+ }
1083
+ else {
1084
+ logger.debug(`[AUN] Processing status failed: ${e}`);
1085
+ }
1086
+ });
1087
+ };
951
1088
  if (isGroup) {
952
1089
  params.group_id = channelId;
953
1090
  this.trace('OUT', 'group.send.status', params);
954
- this.client.call('group.send', params).catch(e => {
955
- logger.debug(`[AUN] Processing status failed: ${e}`);
956
- });
1091
+ sendWithFallback('group.send');
957
1092
  }
958
1093
  else {
959
1094
  params.to = statusTargetAid;
960
1095
  this.trace('OUT', 'message.send.status', params);
961
- this.client.call('message.send', params).catch(e => {
962
- logger.debug(`[AUN] Processing status failed: ${e}`);
963
- });
1096
+ sendWithFallback('message.send');
964
1097
  }
965
1098
  }
966
1099
  sendCustomPayload(channelId, payload) {
@@ -1068,6 +1201,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1068
1201
  aid: this._aid,
1069
1202
  reconnectAttempt: this.reconnectAttempt,
1070
1203
  maxAttempts: AUNChannel.RECONNECT_DELAYS.length,
1204
+ plaintextRecv: this.plaintextRecv,
1071
1205
  };
1072
1206
  }
1073
1207
  /** 读取本地 agent.md 中的 name(用于身份上下文展示) */
@@ -1150,6 +1284,7 @@ export class AUNChannelPlugin {
1150
1284
  encryptionSeed: inst.encryptionSeed,
1151
1285
  owner: inst.owner,
1152
1286
  aunTrace: config.debug?.aunTrace,
1287
+ aunSdkLog: config.debug?.aunSdkLog,
1153
1288
  });
1154
1289
  const adapter = {
1155
1290
  channelName: inst.name,
@@ -233,11 +233,23 @@ export class CommandHandler {
233
233
  async sendInteractionCard(opts) {
234
234
  if (!this.interactionRouter)
235
235
  return false;
236
+ // 无写权限 → 走文本降级(由调用点 fall through 输出只读信息)
237
+ if (opts.canWrite === false)
238
+ return false;
239
+ // 有写权限但此刻忙碌 → 也走文本降级(避免诱导用户在忙碌状态下触发带参写操作)
240
+ if (this.isSessionBusy(opts.sessionId))
241
+ return false;
236
242
  await this.invalidateOldCards(opts.channel, opts.sessionId);
237
243
  const messageId = await this.trySendInteraction(opts.channel, opts.channelId, opts.interaction, opts.replyCtx);
238
244
  if (!messageId)
239
245
  return false;
240
246
  const wrappedCallback = async (action, values, operatorId) => {
247
+ // 点击回调时二次校验:若会话此刻忙碌,忽略本次点击(防止已弹卡片被用于带参切换)
248
+ if (this.isSessionBusy(opts.sessionId)) {
249
+ const adapter = this.adapters.get(opts.channel);
250
+ adapter?.sendText(opts.channelId, '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试', opts.replyCtx);
251
+ return;
252
+ }
241
253
  await opts.callback(action, values, operatorId);
242
254
  // 已完成交互的卡片:保留原始内容,仅禁用按钮(不标记为"已过期")
243
255
  // "已过期"仅用于被新卡片替代的旧卡片(invalidateOldCards)
@@ -245,6 +257,14 @@ export class CommandHandler {
245
257
  this.interactionRouter.register(opts.requestId, opts.sessionId, wrappedCallback, { timeoutMs: 120_000, messageId });
246
258
  return true;
247
259
  }
260
+ /** 判断指定 session 是否有活跃流(用于 idle 守卫和卡片降级) */
261
+ isSessionBusy(sessionId) {
262
+ for (const agent of this.agentMap.values()) {
263
+ if (agent.hasActiveStream(sessionId))
264
+ return true;
265
+ }
266
+ return false;
267
+ }
248
268
  /** 获取活跃会话,无会话时返回统一错误提示 */
249
269
  async ensureSession(channel, channelId, threadId) {
250
270
  if (threadId) {
@@ -525,10 +545,18 @@ export class CommandHandler {
525
545
  const isAdmin = identity.role === 'owner' || identity.role === 'admin';
526
546
  const activeChatType = activeSession?.chatType || 'private';
527
547
  if (normalizedContent.startsWith('/')) {
528
- const guestGroupCommands = ['/status', '/help', '/check', '/chatmode'];
548
+ // guest 在群聊和私聊中均可访问的只读命令:纯查询形态(带参写操作由各 handler 内部守卫拦截)
549
+ const guestGroupCommands = [
550
+ '/status', '/help', '/check', '/chatmode',
551
+ '/model', '/effort', '/agent', '/perm', '/activity', '/safe',
552
+ ];
529
553
  const userCommands = activeChatType === 'group' && !isAdmin
530
554
  ? guestGroupCommands
531
- : ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s ', '/check', '/chatmode'];
555
+ : [
556
+ ...guestGroupCommands,
557
+ // 私聊 guest 额外可用:会话自管理 + 私聊专属的 /rewind 历史查看
558
+ '/slist', '/new', '/session', '/rename', '/name', '/del', '/s ', '/rewind',
559
+ ];
532
560
  const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
533
561
  if (!isUserCommand && !isAdmin) {
534
562
  return activeChatType === 'group'
@@ -537,8 +565,16 @@ export class CommandHandler {
537
565
  }
538
566
  }
539
567
  // 空闲检查:某些命令需要等待当前会话空闲
540
- const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent', '/rewind', '/chatmode'];
541
- if (requiresIdle.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '))) {
568
+ // 原则:仅对"写/破坏性"形态拦截,纯读/用法提示的无参形态始终放行
569
+ // - 始终需要 idle(无参即写):/new /clear /compact /repair /fork
570
+ // - 仅带参时需要 idle(无参是列表/用法):/session /bind /project /agent /rewind
571
+ // - /chatmode:在 handler 内部自行做写操作的 idle 检查
572
+ // - /safe:已禁用 no-op,不再要求 idle
573
+ const idleAlways = ['/new', '/clear', '/compact', '/repair', '/fork'];
574
+ const idleWhenArg = ['/session', '/bind', '/project', '/agent', '/rewind'];
575
+ const needsIdle = idleAlways.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' ')) ||
576
+ idleWhenArg.some(cmd => normalizedContent.startsWith(cmd + ' '));
577
+ if (needsIdle) {
542
578
  if (threadId) {
543
579
  // 话题中:检查话题 session 是否在处理(不创建)
544
580
  const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
@@ -691,6 +727,7 @@ export class CommandHandler {
691
727
  const replyCtx = this.getReplyContext(permSession);
692
728
  const cardSent = await this.sendInteractionCard({
693
729
  channel, channelId, sessionId: permSession.id, requestId, interaction, replyCtx,
730
+ canWrite: isOwner,
694
731
  callback: async (action, _values, operatorId) => {
695
732
  if (action !== currentMode) {
696
733
  if (userId && operatorId && operatorId !== userId)
@@ -712,7 +749,10 @@ export class CommandHandler {
712
749
  const suffix = m.available ? '' : ' ⚠️ 不可用';
713
750
  return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
714
751
  }).join('\n');
715
- return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|always|deny 审批权限请求`;
752
+ if (isOwner) {
753
+ return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|always|deny 审批权限请求`;
754
+ }
755
+ return `🔐 当前权限模式: ${currentMode}`;
716
756
  }
717
757
  const parts = args.split(/\s+/);
718
758
  // /perm <mode> 或 /perm allow|always|deny:切换模式 / 快捷审批
@@ -768,10 +808,11 @@ export class CommandHandler {
768
808
  }
769
809
  // /agent 命令:查看或切换 Agent 后端
770
810
  if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ')) {
771
- // 群聊中 owner only,私聊中 admin+
772
- if (activeChatType === 'group' ? !isOwner : !isAdmin)
773
- return '❌ 无权限:此命令仅限管理员使用';
774
811
  const args = normalizedContent.slice(6).trim();
812
+ // 切换(带参)需权限:群聊 owner only,私聊 admin+;无参查询对所有人放开
813
+ if (args && (activeChatType === 'group' ? !isOwner : !isAdmin)) {
814
+ return '❌ 无权限:此命令仅限管理员使用';
815
+ }
775
816
  const available = [...this.agentMap.keys()];
776
817
  if (!args) {
777
818
  const currentAgent = activeSession?.agentId || this.defaultAgentId;
@@ -796,6 +837,7 @@ export class CommandHandler {
796
837
  const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
797
838
  const cardSent = await this.sendInteractionCard({
798
839
  channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
840
+ canWrite: activeChatType === 'group' ? isOwner : isAdmin,
799
841
  callback: async (action, _values, operatorId) => {
800
842
  if (action !== currentAgent) {
801
843
  if (userId && operatorId && operatorId !== userId)
@@ -813,7 +855,11 @@ export class CommandHandler {
813
855
  }
814
856
  // 降级:文本
815
857
  const list = available.map(a => `${a === currentAgent ? ' ✓' : ' '} ${a}`).join('\n');
816
- return `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name>`;
858
+ const canSwitchAgent = activeChatType === 'group' ? isOwner : isAdmin;
859
+ if (canSwitchAgent) {
860
+ return `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name>`;
861
+ }
862
+ return `当前 Agent: ${currentAgent}`;
817
863
  }
818
864
  if (!this.agentMap.has(args)) {
819
865
  return `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}`;
@@ -871,6 +917,7 @@ export class CommandHandler {
871
917
  const replyCtx = this.getReplyContext(modelSession);
872
918
  const cardSent = await this.sendInteractionCard({
873
919
  channel, channelId, sessionId: modelSession.id, requestId, interaction, replyCtx,
920
+ canWrite: isAdmin,
874
921
  callback: async (action, _values, operatorId) => {
875
922
  if (action !== currentModel) {
876
923
  if (userId && operatorId && operatorId !== userId)
@@ -891,8 +938,14 @@ export class CommandHandler {
891
938
  const effortHint = efforts.length > 0
892
939
  ? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
893
940
  : '';
894
- return `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n${formatModelUsage(modelAgent, currentModel)}`;
941
+ if (isAdmin) {
942
+ return `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n${formatModelUsage(modelAgent, currentModel)}`;
943
+ }
944
+ return `当前模型: ${currentModel}${effortHint}`;
895
945
  }
946
+ // 带参(切换/调整)需 admin+;无参查询已在上方返回
947
+ if (!isAdmin)
948
+ return '❌ 无权限:切换模型仅限管理员使用';
896
949
  const parts = args.split(/\s+/);
897
950
  let newModel;
898
951
  let newEffort;
@@ -1060,6 +1113,7 @@ export class CommandHandler {
1060
1113
  const replyCtx = this.getReplyContext(effortSession);
1061
1114
  const cardSent = await this.sendInteractionCard({
1062
1115
  channel, channelId, sessionId: effortSession.id, requestId, interaction, replyCtx,
1116
+ canWrite: isAdmin,
1063
1117
  callback: async (action, _values, operatorId) => {
1064
1118
  if (action !== currentEffort) {
1065
1119
  if (userId && operatorId && operatorId !== userId)
@@ -1079,8 +1133,14 @@ export class CommandHandler {
1079
1133
  const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
1080
1134
  const allItems = [...efforts, 'auto'];
1081
1135
  const effortList = allItems.map(e => ` ${e === currentEffort ? '✓' : ' '} ${e}${e === 'auto' ? ' (SDK默认)' : ''}`).join('\n');
1082
- return `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n\n用法: /effort <level>`;
1136
+ if (isAdmin) {
1137
+ return `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n\n用法: /effort <level>`;
1138
+ }
1139
+ return `⚡ 推理强度: ${effortDisplay}`;
1083
1140
  }
1141
+ // 带参(切换)需 admin+;无参查询已在上方返回
1142
+ if (!isAdmin)
1143
+ return '❌ 无权限:切换推理强度仅限管理员使用';
1084
1144
  // /effort auto:恢复 SDK 默认
1085
1145
  if (args === 'auto') {
1086
1146
  effortAgent.setEffort?.(undefined);
@@ -1302,13 +1362,14 @@ export class CommandHandler {
1302
1362
  }
1303
1363
  }
1304
1364
  if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
1305
- if (!isAdmin)
1365
+ const activityArg = normalizedContent.slice(9).trim();
1366
+ // 带参(写操作)需 admin+;无参查询对所有人开放(owner 门在具体切换点还有一道)
1367
+ if (activityArg && !isAdmin)
1306
1368
  return '❌ 无权限:此命令仅限管理员使用';
1307
1369
  // proactive 模式下流式输出全部静默,activity 配置无意义
1308
1370
  if (activeSession?.sessionMode === 'proactive') {
1309
1371
  return '❌ 当前会话为 proactive 模式,不支持 activity 配置(流式输出已全部静默)';
1310
1372
  }
1311
- const activityArg = normalizedContent.slice(9).trim();
1312
1373
  const modeMap = {
1313
1374
  all: 'all',
1314
1375
  dm: 'dm-only',
@@ -1348,6 +1409,7 @@ export class CommandHandler {
1348
1409
  const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
1349
1410
  const cardSent = await this.sendInteractionCard({
1350
1411
  channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
1412
+ canWrite: isOwner,
1351
1413
  callback: async (action, _values, operatorId) => {
1352
1414
  const newMode = modeMap[action];
1353
1415
  if (newMode && newMode !== currentMode) {
@@ -1369,7 +1431,10 @@ export class CommandHandler {
1369
1431
  const prefix = m.configVal === currentMode ? '✓' : ' ';
1370
1432
  return ` ${prefix} ${m.key} (${m.label})`;
1371
1433
  }).join('\n');
1372
- return `📋 中间输出模式: ${currentMode}\n\n${modeList}\n\n用法:\n /activity <模式> 切换中间输出显示模式`;
1434
+ if (isOwner) {
1435
+ return `📋 中间输出模式: ${currentMode}\n\n${modeList}\n\n用法:\n /activity <模式> 切换中间输出显示模式`;
1436
+ }
1437
+ return `📋 中间输出模式: ${currentMode}`;
1373
1438
  }
1374
1439
  const newMode = modeMap[activityArg];
1375
1440
  if (!newMode) {
@@ -1396,7 +1461,11 @@ export class CommandHandler {
1396
1461
  const currentMode = activeSession.sessionMode || 'interactive';
1397
1462
  if (!arg) {
1398
1463
  const lockHint = lockedMode ? `(由通道配置锁定为 ${lockedMode})` : '';
1399
- return `📋 当前会话模式: ${currentMode}${lockHint}\n可选: interactive / proactive\n用法: /chatmode <模式>`;
1464
+ const canSwitch = activeChatType !== 'group' || isAdmin;
1465
+ if (canSwitch && !lockedMode) {
1466
+ return `📋 当前会话模式: ${currentMode}${lockHint}\n可选: interactive / proactive\n用法: /chatmode <模式>`;
1467
+ }
1468
+ return `📋 当前会话模式: ${currentMode}${lockHint}`;
1400
1469
  }
1401
1470
  if (arg !== 'interactive' && arg !== 'proactive') {
1402
1471
  return `❌ 无效模式: ${arg}\n可选: interactive / proactive`;
@@ -1410,6 +1479,19 @@ export class CommandHandler {
1410
1479
  if (arg === currentMode) {
1411
1480
  return `📋 当前会话模式已是 ${arg}`;
1412
1481
  }
1482
+ // 仅在真正需要切换时才要求会话空闲
1483
+ if (threadId) {
1484
+ const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
1485
+ if (threadSession) {
1486
+ const threadAgent = this.getAgent(threadSession.agentId);
1487
+ if (threadAgent.hasActiveStream(threadSession.id)) {
1488
+ return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
1489
+ }
1490
+ }
1491
+ }
1492
+ else if (agent.hasActiveStream(activeSession.id)) {
1493
+ return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
1494
+ }
1413
1495
  await this.sessionManager.updateSession(activeSession.id, { sessionMode: arg });
1414
1496
  return `✅ 会话模式已切换: ${arg}`;
1415
1497
  }
@@ -1958,6 +2040,7 @@ export class CommandHandler {
1958
2040
  const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
1959
2041
  const cardSent = await this.sendInteractionCard({
1960
2042
  channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
2043
+ canWrite: isAdmin,
1961
2044
  callback: async (action, _values, operatorId) => {
1962
2045
  if (userId && operatorId && operatorId !== userId)
1963
2046
  return;
@@ -2526,6 +2609,9 @@ export class CommandHandler {
2526
2609
  if (!args) {
2527
2610
  return await this.handleRewindList(session, rewindAgent);
2528
2611
  }
2612
+ // 带参(执行回退,会删除文件/改对话)需 admin+
2613
+ if (!isAdmin)
2614
+ return '❌ 无权限:回退操作仅限管理员使用';
2529
2615
  const parts = args.split(/\s+/);
2530
2616
  const turnNum = parseInt(parts[0], 10);
2531
2617
  if (isNaN(turnNum) || turnNum < 1) {
@@ -2702,10 +2788,13 @@ export class CommandHandler {
2702
2788
  }
2703
2789
  // ── Agent Ctl ──
2704
2790
  static CTL_COMMANDS = [
2705
- '/help', '/status', '/check',
2706
- '/model', '/effort', '/perm',
2791
+ '/help', '/status', '/check', '/pwd',
2792
+ '/model', '/effort', '/perm', '/agent',
2707
2793
  '/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd', '/bind', '/aid',
2794
+ '/rename', '/name',
2708
2795
  ];
2796
+ /** ctl 中仅允许查询形态的指令;写形态(带参)一律拒绝 */
2797
+ static CTL_READONLY = new Set(['/agent']);
2709
2798
  /**
2710
2799
  * 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
2711
2800
  * - 群聊话题:metadata.replyContext.{threadId,peerId}
@@ -2732,6 +2821,10 @@ export class CommandHandler {
2732
2821
  if (!CommandHandler.CTL_COMMANDS.includes(inputCmd)) {
2733
2822
  return { ok: false, error: `不允许的指令: ${inputCmd}` };
2734
2823
  }
2824
+ // 1.1 只读守卫:带参形态(写操作)在 ctl 中禁止
2825
+ if (CommandHandler.CTL_READONLY.has(inputCmd) && cmd.trimEnd().length > inputCmd.length) {
2826
+ return { ok: false, error: `${inputCmd} 在 ctl 中仅支持查询形态,不支持带参切换` };
2827
+ }
2735
2828
  // 2. 通过 sessionId 查 session
2736
2829
  const session = await this.sessionManager.getSessionById(sessionId);
2737
2830
  if (!session) {
@@ -282,6 +282,17 @@ export class MessageProcessor {
282
282
  const streamKey = session.id;
283
283
  // 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
284
284
  const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
285
+ const chatmode = session.sessionMode ?? 'interactive';
286
+ // 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
287
+ const taskReplyContext = () => {
288
+ const base = this.getReplyContext(message);
289
+ return {
290
+ ...(base ?? {}),
291
+ metadata: { ...(base?.metadata ?? {}), taskId, chatmode },
292
+ };
293
+ };
294
+ // Proactive 模式可观测:ThoughtEmitter 声明在 try 外,catch 块也能透传错误为 thought
295
+ let thoughtEmitter = null;
285
296
  try {
286
297
  const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
287
298
  // 记录收到消息
@@ -335,7 +346,8 @@ export class MessageProcessor {
335
346
  firstReply = false;
336
347
  }
337
348
  }
338
- await adapter.sendText(message.channelId, text, Object.keys(opts).length ? opts : undefined);
349
+ opts.metadata = { ...(opts.metadata ?? {}), taskId, chatmode };
350
+ await adapter.sendText(message.channelId, text, opts);
339
351
  }
340
352
  // 后台任务:静默,不发送输出
341
353
  }, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag, isProactive);
@@ -343,14 +355,13 @@ export class MessageProcessor {
343
355
  this.currentFlusher = flusher;
344
356
  // Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
345
357
  // selector: context = { type: 'task', id: taskId }
346
- let thoughtEmitter = null;
347
358
  if (isProactive && adapter.putThought) {
348
- thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId);
359
+ thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId, chatmode);
349
360
  }
350
361
  // 调用 AgentRunner(含上下文过长自动 compact 重试)
351
362
  // 捕获当前消息的上下文(闭包),避免后续消息处理时串台
352
363
  const capturedChannelId = message.channelId;
353
- const capturedReplyContext = this.getReplyContext(message);
364
+ const capturedReplyContext = taskReplyContext();
354
365
  // 设置权限审批的消息发送回调(指向当前渠道)
355
366
  agent.setSendPrompt(async (text) => {
356
367
  await adapter.sendText(capturedChannelId, text, capturedReplyContext);
@@ -560,22 +571,22 @@ export class MessageProcessor {
560
571
  && targetSpec !== currentChannelType;
561
572
  // 跨通道仅限 owner
562
573
  if (isCrossChannel && session.identity?.role !== 'owner') {
563
- await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`, this.getReplyContext(message));
574
+ await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`, taskReplyContext());
564
575
  continue;
565
576
  }
566
577
  const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
567
578
  if (!fs.existsSync(resolvedPath)) {
568
579
  logger.warn(`[${adapter.channelName}] File not found: ${resolvedPath}`);
569
- await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`, this.getReplyContext(message));
580
+ await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`, taskReplyContext());
570
581
  continue;
571
582
  }
572
583
  // 找目标 adapter
573
584
  if (!targetInfo) {
574
- await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`, this.getReplyContext(message));
585
+ await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`, taskReplyContext());
575
586
  continue;
576
587
  }
577
588
  if (!targetInfo.adapter.sendFile) {
578
- await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 不支持文件发送`, this.getReplyContext(message));
589
+ await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 不支持文件发送`, taskReplyContext());
579
590
  continue;
580
591
  }
581
592
  // 找目标 channelId
@@ -586,21 +597,21 @@ export class MessageProcessor {
586
597
  const ownerPeerId = getOwner(this.config, targetAdapterName);
587
598
  targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
588
599
  if (!targetChannelId) {
589
- await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, this.getReplyContext(message));
600
+ await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, taskReplyContext());
590
601
  continue;
591
602
  }
592
603
  }
593
604
  logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
594
605
  try {
595
- await targetInfo.adapter.sendFile(targetChannelId, resolvedPath, this.getReplyContext(message));
606
+ await targetInfo.adapter.sendFile(targetChannelId, resolvedPath, taskReplyContext());
596
607
  this.eventBus.publish({ type: 'agent:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
597
608
  if (isCrossChannel) {
598
- await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, this.getReplyContext(message));
609
+ await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, taskReplyContext());
599
610
  }
600
611
  }
601
612
  catch (error) {
602
613
  logger.error(`[${adapter.channelName}] Failed to send file: ${resolvedPath}`, error);
603
- await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`, this.getReplyContext(message));
614
+ await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`, taskReplyContext());
604
615
  }
605
616
  }
606
617
  } // end of !isProactive
@@ -684,7 +695,7 @@ export class MessageProcessor {
684
695
  if (isFinallyBackground) {
685
696
  const projectName = path.basename(session.projectPath);
686
697
  const count = this.messageCache.getCount(session.id);
687
- await adapter.sendText(message.channelId, `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)`);
698
+ await adapter.sendText(message.channelId, `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)`, taskReplyContext());
688
699
  }
689
700
  // 记录发送响应
690
701
  logger.message({
@@ -759,11 +770,24 @@ export class MessageProcessor {
759
770
  // 获取 session 用于话题回复(如果 resolveSession 已执行)
760
771
  let sendOpts;
761
772
  try {
762
- const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
773
+ await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
763
774
  sendOpts = this.getReplyContext(message);
764
775
  }
765
776
  catch { }
777
+ // 注入 taskId / chatmode(与任务主流程保持一致)
778
+ sendOpts = {
779
+ ...(sendOpts ?? {}),
780
+ metadata: { ...(sendOpts?.metadata ?? {}), taskId, chatmode },
781
+ };
766
782
  await adapter.sendText(message.channelId, userMessage, sendOpts);
783
+ // Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
784
+ if (thoughtEmitter) {
785
+ const thoughtErrorType = errType === ErrorType.CONTEXT_TOO_LONG ? 'context_too_long' :
786
+ errType === ErrorType.AUTH_ERROR ? 'auth' :
787
+ (errType === ErrorType.SDK_TIMEOUT || errType === ErrorType.STREAM_ERROR) ? 'network' :
788
+ 'unknown';
789
+ thoughtEmitter.emit({ type: 'error', error: userMessage, errorType: thoughtErrorType }).catch(() => { });
790
+ }
767
791
  }
768
792
  }
769
793
  }
@@ -6,20 +6,23 @@ import { logger } from '../../utils/logger.js';
6
6
  * - 不做聚合/batching,逐事件调用 adapter.putThought()
7
7
  * - 不感知 group vs P2P,通道差异由 adapter 内部处理
8
8
  * - taskId 映射为 context: { type: 'task', id: taskId }(协议 selector)
9
+ * 同时写入 payload.task_id / payload.chatmode,与 message.send/group.send 保持一致
9
10
  * - fire-and-forget:调用方不 await emit(),错误被内部捕获
10
11
  */
11
12
  export class ThoughtEmitter {
12
13
  adapter;
13
14
  channelId;
14
15
  taskId;
16
+ chatmode;
15
17
  hasEmittedText = false;
16
- constructor(adapter, channelId, taskId) {
18
+ constructor(adapter, channelId, taskId, chatmode = 'proactive') {
17
19
  if (!taskId) {
18
20
  throw new Error('[ThoughtEmitter] taskId is required at construction');
19
21
  }
20
22
  this.adapter = adapter;
21
23
  this.channelId = channelId;
22
24
  this.taskId = taskId;
25
+ this.chatmode = chatmode;
23
26
  }
24
27
  async emit(event) {
25
28
  // 对齐 interactive 的 dedup:流式 text 已推过时,complete.result 不再重复发 summary
@@ -37,6 +40,9 @@ export class ThoughtEmitter {
37
40
  if (payload.stage === 'thinking') {
38
41
  this.hasEmittedText = true;
39
42
  }
43
+ // payload 也带上 task_id / chatmode(与 message.send/group.send 对齐)
44
+ payload.task_id = this.taskId;
45
+ payload.chatmode = this.chatmode;
40
46
  try {
41
47
  await this.adapter.putThought(this.channelId, this.taskId, payload);
42
48
  }
@@ -728,7 +728,7 @@ export async function createAidSilent(opts) {
728
728
  if (fs.existsSync(aidDir) && fs.existsSync(path.join(aidDir, 'private'))) {
729
729
  return { aid: opts.aid, alreadyExisted: true };
730
730
  }
731
- const { AUNClient } = await import('@agentunion/fastaun');
731
+ const { AUNClient, GatewayDiscovery } = await import('@agentunion/fastaun');
732
732
  let client = new AUNClient({ aun_path: aunPath });
733
733
  const result = await client.auth.createAid({ aid: opts.aid });
734
734
  // Download CA root cert (if not already present)
@@ -742,6 +742,18 @@ export async function createAidSilent(opts) {
742
742
  catch { /* ignore */ }
743
743
  client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath, aid: opts.aid });
744
744
  }
745
+ // Set gateway URL for uploadAgentMd
746
+ let gatewayUrl = result.gateway || '';
747
+ if (!gatewayUrl) {
748
+ try {
749
+ const discovery = new GatewayDiscovery({});
750
+ gatewayUrl = await discovery.discover(`https://${opts.aid}/.well-known/aun-gateway`);
751
+ }
752
+ catch { /* fall through */ }
753
+ }
754
+ if (gatewayUrl) {
755
+ client._gatewayUrl = gatewayUrl;
756
+ }
745
757
  // Write initial agent.md (initialized: false, name = aid first label)
746
758
  const agentName = opts.aid.split('.')[0];
747
759
  const agentMdContent = `---\naid: "${opts.aid}"\nname: "${agentName}"\ntype: "ai"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
@@ -790,7 +802,6 @@ export function appendAunInstance(config, inst) {
790
802
  }
791
803
  export async function setupAunAid(rl, _config) {
792
804
  let aid = '';
793
- let gatewayPort; // only used locally for AID creation, not written to config
794
805
  // Outer loop: allows retrying with a different AID
795
806
  while (true) {
796
807
  // Ask AID with format validation
@@ -806,12 +817,6 @@ export async function setupAunAid(rl, _config) {
806
817
  aid = '';
807
818
  }
808
819
  }
809
- const portStr = (await ask(rl, ' Gateway 端口 [留空使用默认 443]: ')).trim();
810
- gatewayPort = portStr ? parseInt(portStr, 10) : undefined;
811
- if (gatewayPort !== undefined && (isNaN(gatewayPort) || gatewayPort < 1 || gatewayPort > 65535)) {
812
- console.log(' ⚠ 端口号无效,使用默认 443');
813
- gatewayPort = undefined;
814
- }
815
820
  // Check if AID exists locally
816
821
  const aunPath = path.join(os.homedir(), '.aun');
817
822
  const aidDir = path.join(aunPath, 'AIDs', aid);
@@ -828,21 +833,13 @@ export async function setupAunAid(rl, _config) {
828
833
  console.log(' 正在创建 AID...');
829
834
  let failed = false;
830
835
  try {
831
- const { AUNClient } = await import('@agentunion/fastaun');
836
+ const { AUNClient, GatewayDiscovery } = await import('@agentunion/fastaun');
832
837
  let client = new AUNClient({ aun_path: aunPath });
833
- // 如果用户指定了自定义端口,手动设置 gateway URL;否则让 SDK 自动发现
834
- if (gatewayPort) {
835
- const domain = aid.split('.').slice(1).join('.');
836
- client._gatewayUrl = `wss://gateway.${domain}:${gatewayPort}/aun`;
837
- }
838
838
  const result = await client.auth.createAid({ aid });
839
839
  console.log(` ✓ AID ${result.aid} 创建成功`);
840
840
  // 下载 CA 根证书(如果本地不存在),从 SDK 返回的实际网关 URL 派生
841
841
  const caDownloaded = await downloadCaRoot(aunPath, result.gateway || '', ' ');
842
- // 关键:SDK 默认 rootCaPath=null,只读取包内 bundled certs。
843
- // 必须显式传 root_ca_path 指向刚下载的 root.crt,uploadAgentMd 才能验证 server cert。
844
- // 同时传 aid,否则新 client 不知道该加载哪个身份,uploadAgentMd 会报
845
- // "no local identity found, call auth.createAid() first"。
842
+ // 重建 client:传 root_ca_path 以验证 server cert,传 aid 以加载身份
846
843
  const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
847
844
  if (caDownloaded && fs.existsSync(caCertPath)) {
848
845
  try {
@@ -850,10 +847,18 @@ export async function setupAunAid(rl, _config) {
850
847
  }
851
848
  catch { /* ignore */ }
852
849
  client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath, aid });
853
- if (gatewayPort) {
854
- const domain = aid.split('.').slice(1).join('.');
855
- client._gatewayUrl = `wss://gateway.${domain}:${gatewayPort}/aun`;
850
+ }
851
+ // 设置 gateway URL(从 createAid 返回值或 well-known 自动发现)
852
+ let gatewayUrl = result.gateway || '';
853
+ if (!gatewayUrl) {
854
+ try {
855
+ const discovery = new GatewayDiscovery({});
856
+ gatewayUrl = await discovery.discover(`https://${aid}/.well-known/aun-gateway`);
856
857
  }
858
+ catch { /* fall through */ }
859
+ }
860
+ if (gatewayUrl) {
861
+ client._gatewayUrl = gatewayUrl;
857
862
  }
858
863
  // Collect agent.md info and publish
859
864
  const typeInput = (await ask(rl, ' Agent 类型 human/ai [ai]: ')).trim().toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw",
3
- "version": "2.6.2",
3
+ "version": "2.6.3",
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",