evolclaw 2.6.1 → 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.
- package/dist/channels/aun.js +223 -34
- package/dist/core/command-handler.js +120 -21
- package/dist/core/message/message-processor.js +53 -16
- package/dist/core/message/thought-emitter.js +16 -3
- package/dist/utils/init-channel.js +26 -21
- package/package.json +1 -1
package/dist/channels/aun.js
CHANGED
|
@@ -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';
|
|
@@ -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
|
|
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('
|
|
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)
|
|
@@ -113,12 +120,26 @@ export class AUNChannel {
|
|
|
113
120
|
if (messageId)
|
|
114
121
|
this.messageSeqMap.delete(messageId);
|
|
115
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
|
+
}
|
|
116
133
|
_aid;
|
|
134
|
+
_selfName; // 本地 agent.md 中的 name,首次 connect 时读取
|
|
117
135
|
_chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
|
|
118
136
|
seenMessages = new Map();
|
|
119
137
|
peerInfoCache = new Map();
|
|
120
138
|
messageSeqMap = new Map(); // messageId → seq (for ack)
|
|
121
139
|
sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
|
|
140
|
+
peerE2ee = new Map();
|
|
141
|
+
static E2EE_PROBE_TTL = 10 * 60 * 1000; // 10min
|
|
142
|
+
plaintextRecv = 0;
|
|
122
143
|
// Reconnect state (TS-layer fallback, on top of SDK auto_reconnect)
|
|
123
144
|
intentionalDisconnect = false;
|
|
124
145
|
reconnectAttempt = 0;
|
|
@@ -153,7 +174,7 @@ export class AUNChannel {
|
|
|
153
174
|
this.client = null;
|
|
154
175
|
}
|
|
155
176
|
this.connected = false;
|
|
156
|
-
const aunPath = this.config.keystorePath ||
|
|
177
|
+
const aunPath = this.config.keystorePath || path.join(os.homedir(), '.aun');
|
|
157
178
|
const aidName = this.config.aid;
|
|
158
179
|
const encryptionSeed = this.config.encryptionSeed || process.env.AUN_ENCRYPTION_SEED || undefined;
|
|
159
180
|
// Gateway URL 解析:优先用配置的 gatewayUrl,否则通过 well-known 自动发现
|
|
@@ -177,12 +198,12 @@ export class AUNChannel {
|
|
|
177
198
|
logger.info(`[AUN] Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
|
|
178
199
|
// Create client with FileSecretStore (AES-256-GCM)
|
|
179
200
|
// 不传 encryption_seed 时,SDK 自动从 {aun_path}/.seed 文件派生密钥(与 aun_cli.py 对齐)
|
|
180
|
-
const rootCaPath =
|
|
201
|
+
const rootCaPath = path.join(aunPath, 'CA', 'root', 'root.crt');
|
|
181
202
|
this.client = new AUNClient({
|
|
182
203
|
aun_path: aunPath,
|
|
183
204
|
root_ca_path: rootCaPath,
|
|
184
205
|
...(encryptionSeed && { encryption_seed: encryptionSeed }),
|
|
185
|
-
});
|
|
206
|
+
}, this.config.aunSdkLog ?? true);
|
|
186
207
|
// Set gateway URL (internal property, same as Python SDK)
|
|
187
208
|
this.client._gatewayUrl = gateway;
|
|
188
209
|
// Register event handlers before connecting
|
|
@@ -218,6 +239,16 @@ export class AUNChannel {
|
|
|
218
239
|
}
|
|
219
240
|
}
|
|
220
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
|
+
});
|
|
221
252
|
// Authenticate
|
|
222
253
|
// Workaround: SDK 0.3.x _loadIdentityOrRaise doesn't set identity.aid from requested aid,
|
|
223
254
|
// causing gateway "missing aid" error. Patch to backfill aid on loaded identity.
|
|
@@ -262,6 +293,7 @@ export class AUNChannel {
|
|
|
262
293
|
this._aid = this.client.aid ?? undefined;
|
|
263
294
|
const deviceId = this.client._device_id ?? '';
|
|
264
295
|
this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
|
|
296
|
+
this._selfName = this.loadSelfName(aidName);
|
|
265
297
|
this.connected = true;
|
|
266
298
|
this.reconnectAttempt = 0;
|
|
267
299
|
// Workaround: SDK e2ee uses _identity.cert for sender_cert_fingerprint;
|
|
@@ -388,7 +420,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
388
420
|
- 所有命令以 \`/\` 开头
|
|
389
421
|
|
|
390
422
|
现在,请先使用 \`/bind\` 命令绑定您的项目目录,然后就可以开始工作了!`;
|
|
391
|
-
|
|
423
|
+
// First contact with Owner races against Owner's async cert fetch from
|
|
424
|
+
// gateway PKI; a 3s pause lets the cert propagate. persist_required asks
|
|
425
|
+
// the gateway to durably store the message so Owner can recover it via
|
|
426
|
+
// pull if the initial E2EE push still arrives before the cert resolves.
|
|
427
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
428
|
+
if (!this.client) {
|
|
429
|
+
logger.warn('[AUN] Client disconnected before welcome message could be sent');
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
await this.client.call('message.send', {
|
|
433
|
+
to: owner,
|
|
434
|
+
payload: { type: 'text', text: welcomeText },
|
|
435
|
+
encrypt: true,
|
|
436
|
+
persist_required: true,
|
|
437
|
+
});
|
|
392
438
|
logger.info(`[AUN] Welcome message sent to owner: ${owner}`);
|
|
393
439
|
}
|
|
394
440
|
catch (e) {
|
|
@@ -471,6 +517,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
471
517
|
this.acknowledgeImmediately(messageId, seq);
|
|
472
518
|
return;
|
|
473
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
|
+
}
|
|
474
530
|
// Detect @mentions
|
|
475
531
|
const mentions = [];
|
|
476
532
|
if (this._aid && text.includes(`@${this._aid}`)) {
|
|
@@ -543,6 +599,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
543
599
|
this.acknowledgeImmediately(messageId, seq);
|
|
544
600
|
return;
|
|
545
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
|
+
}
|
|
546
612
|
// dispatch_mode from server tells agent how to work in this group
|
|
547
613
|
const dispatchMode = msg.dispatch_mode ?? payload?.dispatch_mode ?? 'mention';
|
|
548
614
|
const mentionedSelf = this._aid
|
|
@@ -725,28 +791,68 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
725
791
|
const payload = { type: 'text', text: finalText };
|
|
726
792
|
if (context?.threadId)
|
|
727
793
|
payload.thread_id = context.threadId;
|
|
728
|
-
|
|
794
|
+
if (context?.metadata?.taskId)
|
|
795
|
+
payload.task_id = context.metadata.taskId;
|
|
796
|
+
if (context?.metadata?.chatmode)
|
|
797
|
+
payload.chatmode = context.metadata.chatmode;
|
|
798
|
+
const isGroup = this.isGroupId(channelId);
|
|
729
799
|
// Multi-instance routing: channelId may be "aid:device_id:slot_id"
|
|
730
800
|
const colonIdx = channelId.indexOf(':');
|
|
731
801
|
const targetAid = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
|
|
732
802
|
if (colonIdx > 0) {
|
|
733
|
-
|
|
803
|
+
payload.chat_id = channelId;
|
|
734
804
|
}
|
|
805
|
+
const encryptTarget = isGroup ? channelId : targetAid;
|
|
806
|
+
const encrypt = this.shouldEncrypt(encryptTarget);
|
|
807
|
+
const params = { payload, encrypt };
|
|
735
808
|
try {
|
|
736
|
-
if (
|
|
809
|
+
if (isGroup) {
|
|
737
810
|
params.group_id = channelId;
|
|
738
811
|
this.trace('OUT', 'group.send', params);
|
|
739
|
-
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
|
+
}
|
|
740
816
|
}
|
|
741
817
|
else {
|
|
742
818
|
params.to = targetAid;
|
|
743
819
|
this.trace('OUT', 'message.send', params);
|
|
744
|
-
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
|
+
}
|
|
745
824
|
}
|
|
746
825
|
}
|
|
747
826
|
catch (e) {
|
|
748
|
-
|
|
749
|
-
|
|
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
|
+
}
|
|
750
856
|
}
|
|
751
857
|
}
|
|
752
858
|
/**
|
|
@@ -783,8 +889,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
783
889
|
}
|
|
784
890
|
}
|
|
785
891
|
catch (e) {
|
|
786
|
-
|
|
787
|
-
|
|
892
|
+
const err = e;
|
|
893
|
+
this.trace('OUT', 'thought.put.error', {
|
|
894
|
+
channelId,
|
|
895
|
+
errorName: err?.name,
|
|
896
|
+
errorCode: err?.code,
|
|
897
|
+
errorMessage: err?.message,
|
|
898
|
+
});
|
|
899
|
+
logger.debug(`[AUN] thought.put failed to ${channelId}: ${err?.name}(${err?.code})=${err?.message}`);
|
|
788
900
|
}
|
|
789
901
|
}
|
|
790
902
|
async sendFile(channelId, filePath, context) {
|
|
@@ -860,22 +972,61 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
860
972
|
};
|
|
861
973
|
if (context?.threadId)
|
|
862
974
|
filePayload.thread_id = context.threadId;
|
|
863
|
-
|
|
975
|
+
if (context?.metadata?.taskId)
|
|
976
|
+
filePayload.task_id = context.metadata.taskId;
|
|
977
|
+
if (context?.metadata?.chatmode)
|
|
978
|
+
filePayload.chatmode = context.metadata.chatmode;
|
|
979
|
+
const isGroup = this.isGroupId(channelId);
|
|
864
980
|
// Multi-instance routing
|
|
865
981
|
const fileColonIdx = channelId.indexOf(':');
|
|
866
982
|
const fileTargetAid = fileColonIdx > 0 ? channelId.substring(0, fileColonIdx) : channelId;
|
|
867
983
|
if (fileColonIdx > 0) {
|
|
868
|
-
|
|
984
|
+
filePayload.chat_id = channelId;
|
|
869
985
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
+
}
|
|
874
1006
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
+
}
|
|
879
1030
|
}
|
|
880
1031
|
logger.info(`[AUN] File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
|
|
881
1032
|
}
|
|
@@ -909,26 +1060,40 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
909
1060
|
};
|
|
910
1061
|
if (context?.threadId)
|
|
911
1062
|
payload.thread_id = context.threadId;
|
|
912
|
-
const
|
|
1063
|
+
const isGroup = this.isGroupId(channelId);
|
|
913
1064
|
// Multi-instance routing
|
|
914
1065
|
const statusColonIdx = channelId.indexOf(':');
|
|
915
1066
|
const statusTargetAid = statusColonIdx > 0 ? channelId.substring(0, statusColonIdx) : channelId;
|
|
916
1067
|
if (statusColonIdx > 0) {
|
|
917
1068
|
payload.chat_id = channelId;
|
|
918
1069
|
}
|
|
919
|
-
|
|
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
|
+
};
|
|
1088
|
+
if (isGroup) {
|
|
920
1089
|
params.group_id = channelId;
|
|
921
1090
|
this.trace('OUT', 'group.send.status', params);
|
|
922
|
-
|
|
923
|
-
logger.debug(`[AUN] Processing status failed: ${e}`);
|
|
924
|
-
});
|
|
1091
|
+
sendWithFallback('group.send');
|
|
925
1092
|
}
|
|
926
1093
|
else {
|
|
927
1094
|
params.to = statusTargetAid;
|
|
928
1095
|
this.trace('OUT', 'message.send.status', params);
|
|
929
|
-
|
|
930
|
-
logger.debug(`[AUN] Processing status failed: ${e}`);
|
|
931
|
-
});
|
|
1096
|
+
sendWithFallback('message.send');
|
|
932
1097
|
}
|
|
933
1098
|
}
|
|
934
1099
|
sendCustomPayload(channelId, payload) {
|
|
@@ -1036,8 +1201,30 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1036
1201
|
aid: this._aid,
|
|
1037
1202
|
reconnectAttempt: this.reconnectAttempt,
|
|
1038
1203
|
maxAttempts: AUNChannel.RECONNECT_DELAYS.length,
|
|
1204
|
+
plaintextRecv: this.plaintextRecv,
|
|
1039
1205
|
};
|
|
1040
1206
|
}
|
|
1207
|
+
/** 读取本地 agent.md 中的 name(用于身份上下文展示) */
|
|
1208
|
+
loadSelfName(aid) {
|
|
1209
|
+
try {
|
|
1210
|
+
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
1211
|
+
const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
|
|
1212
|
+
if (!fs.existsSync(agentMdPath))
|
|
1213
|
+
return undefined;
|
|
1214
|
+
const content = fs.readFileSync(agentMdPath, 'utf-8');
|
|
1215
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1216
|
+
if (!fmMatch)
|
|
1217
|
+
return undefined;
|
|
1218
|
+
const nameMatch = fmMatch[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
1219
|
+
return nameMatch?.[1]?.trim() || undefined;
|
|
1220
|
+
}
|
|
1221
|
+
catch {
|
|
1222
|
+
return undefined;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
getSelfName() {
|
|
1226
|
+
return this._selfName;
|
|
1227
|
+
}
|
|
1041
1228
|
async fetchPeerInfo(aid) {
|
|
1042
1229
|
const cached = this.peerInfoCache.get(aid);
|
|
1043
1230
|
if (cached !== undefined)
|
|
@@ -1097,6 +1284,7 @@ export class AUNChannelPlugin {
|
|
|
1097
1284
|
encryptionSeed: inst.encryptionSeed,
|
|
1098
1285
|
owner: inst.owner,
|
|
1099
1286
|
aunTrace: config.debug?.aunTrace,
|
|
1287
|
+
aunSdkLog: config.debug?.aunSdkLog,
|
|
1100
1288
|
});
|
|
1101
1289
|
const adapter = {
|
|
1102
1290
|
channelName: inst.name,
|
|
@@ -1109,6 +1297,7 @@ export class AUNChannelPlugin {
|
|
|
1109
1297
|
downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
|
|
1110
1298
|
putThought: (id, taskId, payload) => channel.sendThought(id, taskId, payload),
|
|
1111
1299
|
_selfAid: () => channel.getStatus().aid,
|
|
1300
|
+
_selfName: () => channel.getSelfName(),
|
|
1112
1301
|
};
|
|
1113
1302
|
const policy = {
|
|
1114
1303
|
canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
@@ -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
|
-
|
|
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
|
-
: [
|
|
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
|
-
|
|
541
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -1386,9 +1451,9 @@ export class CommandHandler {
|
|
|
1386
1451
|
return `✅ 中间输出模式: ${activityArg}(${label})`;
|
|
1387
1452
|
}
|
|
1388
1453
|
// /chatmode 命令:查看/切换 session 会话模式(interactive | proactive)
|
|
1454
|
+
// - 查看:所有人可用
|
|
1455
|
+
// - 设置:单聊任何角色可设置;群聊仅管理员可设置
|
|
1389
1456
|
if (normalizedContent === '/chatmode' || normalizedContent.startsWith('/chatmode ')) {
|
|
1390
|
-
if (!isAdmin)
|
|
1391
|
-
return '❌ 无权限:此命令仅限管理员使用';
|
|
1392
1457
|
if (!activeSession)
|
|
1393
1458
|
return '❌ 当前无活跃会话';
|
|
1394
1459
|
const lockedMode = getChannelSessionMode(this.config, channel);
|
|
@@ -1396,17 +1461,37 @@ export class CommandHandler {
|
|
|
1396
1461
|
const currentMode = activeSession.sessionMode || 'interactive';
|
|
1397
1462
|
if (!arg) {
|
|
1398
1463
|
const lockHint = lockedMode ? `(由通道配置锁定为 ${lockedMode})` : '';
|
|
1399
|
-
|
|
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`;
|
|
1403
1472
|
}
|
|
1473
|
+
if (activeChatType === 'group' && !isAdmin) {
|
|
1474
|
+
return '❌ 无权限:群聊中切换会话模式仅限管理员使用';
|
|
1475
|
+
}
|
|
1404
1476
|
if (lockedMode) {
|
|
1405
1477
|
return `❌ 会话模式由通道配置锁定为 ${lockedMode},无法切换`;
|
|
1406
1478
|
}
|
|
1407
1479
|
if (arg === currentMode) {
|
|
1408
1480
|
return `📋 当前会话模式已是 ${arg}`;
|
|
1409
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
|
+
}
|
|
1410
1495
|
await this.sessionManager.updateSession(activeSession.id, { sessionMode: arg });
|
|
1411
1496
|
return `✅ 会话模式已切换: ${arg}`;
|
|
1412
1497
|
}
|
|
@@ -1551,15 +1636,18 @@ export class CommandHandler {
|
|
|
1551
1636
|
}
|
|
1552
1637
|
}
|
|
1553
1638
|
const lines = [];
|
|
1639
|
+
const sessionMode = session.sessionMode || 'interactive';
|
|
1640
|
+
const lockedMode = getChannelSessionMode(this.config, channel);
|
|
1641
|
+
const chatModeLine = `会话模式: ${sessionMode}${lockedMode ? '(通道锁定)' : ''}`;
|
|
1554
1642
|
if (isAdmin) {
|
|
1555
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`);
|
|
1643
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${session.name || '(未命名)'}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`, `会话状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`);
|
|
1556
1644
|
if (health.consecutiveErrors > 0) {
|
|
1557
1645
|
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
1558
1646
|
}
|
|
1559
1647
|
lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
|
|
1560
1648
|
}
|
|
1561
1649
|
else {
|
|
1562
|
-
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
1650
|
+
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, chatModeLine, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
1563
1651
|
}
|
|
1564
1652
|
if (health.lastError) {
|
|
1565
1653
|
lines.push('');
|
|
@@ -1952,6 +2040,7 @@ export class CommandHandler {
|
|
|
1952
2040
|
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
1953
2041
|
const cardSent = await this.sendInteractionCard({
|
|
1954
2042
|
channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
|
|
2043
|
+
canWrite: isAdmin,
|
|
1955
2044
|
callback: async (action, _values, operatorId) => {
|
|
1956
2045
|
if (userId && operatorId && operatorId !== userId)
|
|
1957
2046
|
return;
|
|
@@ -2520,6 +2609,9 @@ export class CommandHandler {
|
|
|
2520
2609
|
if (!args) {
|
|
2521
2610
|
return await this.handleRewindList(session, rewindAgent);
|
|
2522
2611
|
}
|
|
2612
|
+
// 带参(执行回退,会删除文件/改对话)需 admin+
|
|
2613
|
+
if (!isAdmin)
|
|
2614
|
+
return '❌ 无权限:回退操作仅限管理员使用';
|
|
2523
2615
|
const parts = args.split(/\s+/);
|
|
2524
2616
|
const turnNum = parseInt(parts[0], 10);
|
|
2525
2617
|
if (isNaN(turnNum) || turnNum < 1) {
|
|
@@ -2696,10 +2788,13 @@ export class CommandHandler {
|
|
|
2696
2788
|
}
|
|
2697
2789
|
// ── Agent Ctl ──
|
|
2698
2790
|
static CTL_COMMANDS = [
|
|
2699
|
-
'/help', '/status', '/check',
|
|
2700
|
-
'/model', '/effort', '/perm',
|
|
2791
|
+
'/help', '/status', '/check', '/pwd',
|
|
2792
|
+
'/model', '/effort', '/perm', '/agent',
|
|
2701
2793
|
'/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd', '/bind', '/aid',
|
|
2794
|
+
'/rename', '/name',
|
|
2702
2795
|
];
|
|
2796
|
+
/** ctl 中仅允许查询形态的指令;写形态(带参)一律拒绝 */
|
|
2797
|
+
static CTL_READONLY = new Set(['/agent']);
|
|
2703
2798
|
/**
|
|
2704
2799
|
* 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
|
|
2705
2800
|
* - 群聊话题:metadata.replyContext.{threadId,peerId}
|
|
@@ -2726,6 +2821,10 @@ export class CommandHandler {
|
|
|
2726
2821
|
if (!CommandHandler.CTL_COMMANDS.includes(inputCmd)) {
|
|
2727
2822
|
return { ok: false, error: `不允许的指令: ${inputCmd}` };
|
|
2728
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
|
+
}
|
|
2729
2828
|
// 2. 通过 sessionId 查 session
|
|
2730
2829
|
const session = await this.sessionManager.getSessionById(sessionId);
|
|
2731
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
|
-
|
|
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 =
|
|
364
|
+
const capturedReplyContext = taskReplyContext();
|
|
354
365
|
// 设置权限审批的消息发送回调(指向当前渠道)
|
|
355
366
|
agent.setSendPrompt(async (text) => {
|
|
356
367
|
await adapter.sendText(capturedChannelId, text, capturedReplyContext);
|
|
@@ -389,15 +400,28 @@ export class MessageProcessor {
|
|
|
389
400
|
const peerLabel = session.identity?.role || 'unknown';
|
|
390
401
|
const peerName = message.peerName || session.metadata?.peerName;
|
|
391
402
|
const peerType = message.peerType;
|
|
403
|
+
const peerId = message.peerId;
|
|
404
|
+
const adapterAny = channelInfo.adapter;
|
|
405
|
+
const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
406
|
+
const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
|
|
407
|
+
const formatIdentity = (name, id) => {
|
|
408
|
+
if (name && id)
|
|
409
|
+
return `${name} (${id})`;
|
|
410
|
+
return name || id || undefined;
|
|
411
|
+
};
|
|
412
|
+
const selfIdentity = formatIdentity(selfName, selfAid);
|
|
413
|
+
const peerIdentity = formatIdentity(peerName, peerId);
|
|
392
414
|
const envParts = [
|
|
393
415
|
`会话通道: ${currentChannelType}`,
|
|
394
416
|
`当前项目: ${path.basename(absoluteProjectPath)}`,
|
|
395
417
|
];
|
|
396
418
|
if (session.name)
|
|
397
419
|
envParts.push(`会话名称: ${session.name}`);
|
|
420
|
+
if (selfIdentity)
|
|
421
|
+
envParts.push(`当前名称: ${selfIdentity}`);
|
|
398
422
|
envParts.push(`对端身份: ${peerLabel}`);
|
|
399
|
-
if (
|
|
400
|
-
envParts.push(`对端名称: ${
|
|
423
|
+
if (peerIdentity)
|
|
424
|
+
envParts.push(`对端名称: ${peerIdentity}`);
|
|
401
425
|
if (peerType && peerType !== 'unknown')
|
|
402
426
|
envParts.push(`对端类型: ${peerType}`);
|
|
403
427
|
if (session.chatType)
|
|
@@ -547,22 +571,22 @@ export class MessageProcessor {
|
|
|
547
571
|
&& targetSpec !== currentChannelType;
|
|
548
572
|
// 跨通道仅限 owner
|
|
549
573
|
if (isCrossChannel && session.identity?.role !== 'owner') {
|
|
550
|
-
await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`,
|
|
574
|
+
await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`, taskReplyContext());
|
|
551
575
|
continue;
|
|
552
576
|
}
|
|
553
577
|
const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
|
|
554
578
|
if (!fs.existsSync(resolvedPath)) {
|
|
555
579
|
logger.warn(`[${adapter.channelName}] File not found: ${resolvedPath}`);
|
|
556
|
-
await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`,
|
|
580
|
+
await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`, taskReplyContext());
|
|
557
581
|
continue;
|
|
558
582
|
}
|
|
559
583
|
// 找目标 adapter
|
|
560
584
|
if (!targetInfo) {
|
|
561
|
-
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`,
|
|
585
|
+
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`, taskReplyContext());
|
|
562
586
|
continue;
|
|
563
587
|
}
|
|
564
588
|
if (!targetInfo.adapter.sendFile) {
|
|
565
|
-
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 不支持文件发送`,
|
|
589
|
+
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 不支持文件发送`, taskReplyContext());
|
|
566
590
|
continue;
|
|
567
591
|
}
|
|
568
592
|
// 找目标 channelId
|
|
@@ -573,21 +597,21 @@ export class MessageProcessor {
|
|
|
573
597
|
const ownerPeerId = getOwner(this.config, targetAdapterName);
|
|
574
598
|
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
|
|
575
599
|
if (!targetChannelId) {
|
|
576
|
-
await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`,
|
|
600
|
+
await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, taskReplyContext());
|
|
577
601
|
continue;
|
|
578
602
|
}
|
|
579
603
|
}
|
|
580
604
|
logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
|
|
581
605
|
try {
|
|
582
|
-
await targetInfo.adapter.sendFile(targetChannelId, resolvedPath,
|
|
606
|
+
await targetInfo.adapter.sendFile(targetChannelId, resolvedPath, taskReplyContext());
|
|
583
607
|
this.eventBus.publish({ type: 'agent:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
|
|
584
608
|
if (isCrossChannel) {
|
|
585
|
-
await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetLabel} 发送`,
|
|
609
|
+
await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, taskReplyContext());
|
|
586
610
|
}
|
|
587
611
|
}
|
|
588
612
|
catch (error) {
|
|
589
613
|
logger.error(`[${adapter.channelName}] Failed to send file: ${resolvedPath}`, error);
|
|
590
|
-
await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`,
|
|
614
|
+
await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`, taskReplyContext());
|
|
591
615
|
}
|
|
592
616
|
}
|
|
593
617
|
} // end of !isProactive
|
|
@@ -671,7 +695,7 @@ export class MessageProcessor {
|
|
|
671
695
|
if (isFinallyBackground) {
|
|
672
696
|
const projectName = path.basename(session.projectPath);
|
|
673
697
|
const count = this.messageCache.getCount(session.id);
|
|
674
|
-
await adapter.sendText(message.channelId, `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)
|
|
698
|
+
await adapter.sendText(message.channelId, `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)`, taskReplyContext());
|
|
675
699
|
}
|
|
676
700
|
// 记录发送响应
|
|
677
701
|
logger.message({
|
|
@@ -746,11 +770,24 @@ export class MessageProcessor {
|
|
|
746
770
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
747
771
|
let sendOpts;
|
|
748
772
|
try {
|
|
749
|
-
|
|
773
|
+
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
|
|
750
774
|
sendOpts = this.getReplyContext(message);
|
|
751
775
|
}
|
|
752
776
|
catch { }
|
|
777
|
+
// 注入 taskId / chatmode(与任务主流程保持一致)
|
|
778
|
+
sendOpts = {
|
|
779
|
+
...(sendOpts ?? {}),
|
|
780
|
+
metadata: { ...(sendOpts?.metadata ?? {}), taskId, chatmode },
|
|
781
|
+
};
|
|
753
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
|
+
}
|
|
754
791
|
}
|
|
755
792
|
}
|
|
756
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
|
}
|
|
@@ -51,7 +57,7 @@ export class ThoughtEmitter {
|
|
|
51
57
|
return null;
|
|
52
58
|
return { type: 'thought', text: event.text, stage: 'thinking' };
|
|
53
59
|
case 'tool_use': {
|
|
54
|
-
const desc = this.summarizeInput(event.input);
|
|
60
|
+
const desc = this.summarizeInput(event.input, event.name);
|
|
55
61
|
return {
|
|
56
62
|
type: 'thought',
|
|
57
63
|
text: desc ? `🔧 ${event.name}: ${desc}` : `🔧 ${event.name}`,
|
|
@@ -109,9 +115,16 @@ export class ThoughtEmitter {
|
|
|
109
115
|
return null;
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
|
-
summarizeInput(input) {
|
|
118
|
+
summarizeInput(input, toolName) {
|
|
113
119
|
if (!input || typeof input !== 'object')
|
|
114
120
|
return '';
|
|
121
|
+
// Bash + ctl send/file: 显示完整命令内容(含发送的消息正文)
|
|
122
|
+
if (toolName === 'Bash' && typeof input.command === 'string') {
|
|
123
|
+
const cmd = input.command;
|
|
124
|
+
if (cmd.includes('evolclaw ctl send') || cmd.includes('evolclaw ctl file')) {
|
|
125
|
+
return cmd;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
115
128
|
return (input.description ||
|
|
116
129
|
input.file_path ||
|
|
117
130
|
input.pattern ||
|
|
@@ -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
|
-
//
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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