evolclaw 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -14
- package/dist/agents/claude-runner.js +224 -23
- package/dist/agents/codex-runner.js +2 -8
- package/dist/agents/gemini-runner.js +1 -8
- package/dist/channels/aun.js +492 -82
- package/dist/channels/dingtalk.js +506 -0
- package/dist/channels/feishu.js +31 -231
- package/dist/channels/qqbot.js +391 -0
- package/dist/channels/wechat.js +36 -38
- package/dist/channels/wecom.js +549 -0
- package/dist/cli.js +170 -17
- package/dist/config.js +98 -2
- package/dist/core/command-handler.js +511 -146
- package/dist/core/message/message-bridge.js +10 -6
- package/dist/core/message/message-processor.js +176 -78
- package/dist/core/message/message-queue.js +48 -0
- package/dist/core/message/stream-flusher.js +2 -2
- package/dist/core/session/session-manager.js +25 -3
- package/dist/index.js +55 -21
- package/dist/ipc.js +14 -4
- package/dist/templates/skills.md +64 -0
- package/dist/utils/error-dict.js +63 -0
- package/dist/utils/error-utils.js +156 -56
- package/dist/utils/format.js +32 -0
- package/dist/utils/init-channel.js +734 -8
- package/dist/utils/init.js +33 -2
- package/dist/utils/media-cache.js +2 -0
- package/dist/utils/stats-collector.js +0 -8
- package/package.json +9 -3
package/dist/channels/aun.js
CHANGED
|
@@ -1,21 +1,66 @@
|
|
|
1
|
-
import { AUNClient } from '@eleans/aun-core-
|
|
1
|
+
import { AUNClient, GatewayDiscovery } from '@eleans/aun-core-sdk';
|
|
2
|
+
import crypto from 'crypto';
|
|
2
3
|
import fs from 'fs';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import { logger, localTimestamp } from '../utils/logger.js';
|
|
5
|
-
import { normalizeChannelInstances } from '../config.js';
|
|
6
|
+
import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
|
|
6
7
|
import { resolvePaths } from '../paths.js';
|
|
8
|
+
import { saveToUploads, sanitizeFileName } from '../utils/media-cache.js';
|
|
9
|
+
function guessMime(filename) {
|
|
10
|
+
const ext = path.extname(filename).toLowerCase();
|
|
11
|
+
const map = {
|
|
12
|
+
'.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json',
|
|
13
|
+
'.js': 'text/javascript', '.ts': 'text/typescript', '.py': 'text/x-python',
|
|
14
|
+
'.html': 'text/html', '.css': 'text/css', '.csv': 'text/csv',
|
|
15
|
+
'.pdf': 'application/pdf', '.zip': 'application/zip', '.gz': 'application/gzip',
|
|
16
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
17
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
18
|
+
'.xml': 'application/xml', '.yaml': 'application/x-yaml', '.yml': 'application/x-yaml',
|
|
19
|
+
};
|
|
20
|
+
return map[ext] || 'application/octet-stream';
|
|
21
|
+
}
|
|
22
|
+
function formatSize(bytes) {
|
|
23
|
+
if (bytes < 1024)
|
|
24
|
+
return `${bytes} B`;
|
|
25
|
+
if (bytes < 1048576)
|
|
26
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
27
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
28
|
+
}
|
|
7
29
|
export class AUNChannel {
|
|
8
30
|
config;
|
|
9
31
|
client = null;
|
|
32
|
+
projectPathProvider;
|
|
10
33
|
messageHandler;
|
|
34
|
+
recallHandler;
|
|
11
35
|
connected = false;
|
|
12
36
|
traceStream = null;
|
|
37
|
+
traceDate = ''; // 当前 trace 文件对应的日期 (YYYYMMDD)
|
|
13
38
|
trace(dir, event, data) {
|
|
39
|
+
if (!this.config.aunTrace)
|
|
40
|
+
return;
|
|
41
|
+
this.rotateTraceIfNeeded();
|
|
14
42
|
if (!this.traceStream)
|
|
15
43
|
return;
|
|
16
44
|
const line = JSON.stringify({ ts: localTimestamp(), dir, event, data });
|
|
17
45
|
this.traceStream.write(line + '\n');
|
|
18
46
|
}
|
|
47
|
+
rotateTraceIfNeeded() {
|
|
48
|
+
const d = new Date();
|
|
49
|
+
const today = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
|
|
50
|
+
if (this.traceDate === today && this.traceStream)
|
|
51
|
+
return;
|
|
52
|
+
if (this.traceStream) {
|
|
53
|
+
this.traceStream.end();
|
|
54
|
+
this.traceStream = null;
|
|
55
|
+
}
|
|
56
|
+
this.traceDate = today;
|
|
57
|
+
const logPath = path.join(resolvePaths().logs, `aun-${today}.log`);
|
|
58
|
+
this.traceStream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
59
|
+
}
|
|
60
|
+
/** 判断 channelId 是否为群组 ID(g-xxx.agentid.pub 或 grp_ 前缀) */
|
|
61
|
+
isGroupId(id) {
|
|
62
|
+
return id.startsWith('grp_') || (id.startsWith('g-') && id.includes('.'));
|
|
63
|
+
}
|
|
19
64
|
getShortAid(aid) {
|
|
20
65
|
if (!aid)
|
|
21
66
|
return undefined;
|
|
@@ -39,25 +84,23 @@ export class AUNChannel {
|
|
|
39
84
|
const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
40
85
|
return new RegExp(`(^|\\s)@${escaped}(?=$|\\s|[.,!?;:,。!?;:]|[\\u4e00-\\u9fff])`).test(text);
|
|
41
86
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return
|
|
87
|
+
stripSelfMentionIfOnly(text, selfAid) {
|
|
88
|
+
if (!selfAid)
|
|
89
|
+
return text;
|
|
90
|
+
const mentions = text.match(/@[\w.-]+/g) || [];
|
|
91
|
+
if (mentions.length !== 1)
|
|
92
|
+
return text;
|
|
93
|
+
const escapedAid = selfAid.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
94
|
+
return text
|
|
95
|
+
.replace(new RegExp(`(^|\\s)@${escapedAid}(?=$|\\s|[.,!?;:,。!?;:]|[\\u4e00-\\u9fff])`, 'g'), '$1')
|
|
96
|
+
.replace(/[ \t]+/g, ' ')
|
|
97
|
+
.trim();
|
|
50
98
|
}
|
|
51
|
-
buildGroupReplyContext(taskId, senderAid
|
|
99
|
+
buildGroupReplyContext(taskId, senderAid) {
|
|
52
100
|
const replyContext = {};
|
|
53
101
|
if (taskId)
|
|
54
102
|
replyContext.threadId = taskId;
|
|
55
|
-
|
|
56
|
-
replyContext.mentionUserIds = ['all'];
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
replyContext.mentionUserIds = [senderAid];
|
|
60
|
-
}
|
|
103
|
+
replyContext.peerId = senderAid;
|
|
61
104
|
return replyContext;
|
|
62
105
|
}
|
|
63
106
|
acknowledgeImmediately(messageId, seq) {
|
|
@@ -70,7 +113,9 @@ export class AUNChannel {
|
|
|
70
113
|
this.messageSeqMap.delete(messageId);
|
|
71
114
|
}
|
|
72
115
|
_aid;
|
|
116
|
+
_chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
|
|
73
117
|
seenMessages = new Map();
|
|
118
|
+
peerInfoCache = new Map();
|
|
74
119
|
messageSeqMap = new Map(); // messageId → seq (for ack)
|
|
75
120
|
sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
|
|
76
121
|
// Reconnect state (TS-layer fallback, on top of SDK auto_reconnect)
|
|
@@ -79,12 +124,17 @@ export class AUNChannel {
|
|
|
79
124
|
reconnectTimer = null;
|
|
80
125
|
static RECONNECT_DELAYS = [60, 120, 300, 600]; // seconds
|
|
81
126
|
onChannelDown;
|
|
127
|
+
// SDK reconnect throttling — avoid log spam when SDK enters tight reconnect loop
|
|
128
|
+
lastReconnectLogTime = 0;
|
|
129
|
+
lastReconnectLogAttempt = 0;
|
|
130
|
+
static RECONNECT_LOG_INTERVAL = 60_000; // log at most every 60s
|
|
131
|
+
static RECONNECT_LOG_STEP = 100; // or every 100 attempts
|
|
132
|
+
static SDK_RECONNECT_GIVEUP = 50; // force TS-layer fallback after this many SDK attempts
|
|
82
133
|
constructor(config) {
|
|
83
134
|
this.config = config;
|
|
84
135
|
if (config.aunTrace) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
logger.info(`[AUN] Trace logging enabled: ${logPath}`);
|
|
136
|
+
this.rotateTraceIfNeeded();
|
|
137
|
+
logger.info(`[AUN] Trace logging enabled (daily rotation): ${resolvePaths().logs}/aun-YYYYMMDD.log`);
|
|
88
138
|
}
|
|
89
139
|
}
|
|
90
140
|
async connect() {
|
|
@@ -105,18 +155,22 @@ export class AUNChannel {
|
|
|
105
155
|
const aunPath = this.config.keystorePath || `${process.env.HOME || '~'}/.aun`;
|
|
106
156
|
const aidName = this.config.aid;
|
|
107
157
|
const encryptionSeed = this.config.encryptionSeed || process.env.AUN_ENCRYPTION_SEED || undefined;
|
|
108
|
-
// Gateway URL
|
|
158
|
+
// Gateway URL 解析:优先用配置的 gatewayUrl,否则通过 well-known 自动发现
|
|
109
159
|
let gateway = this.config.gatewayUrl || '';
|
|
110
160
|
if (!gateway) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
gateway =
|
|
161
|
+
// AID 本身即域名(如 evolai.agentid.pub),用其查询 well-known,与 Python SDK 行为对齐
|
|
162
|
+
const wellKnownUrl = `https://${aidName}/.well-known/aun-gateway`;
|
|
163
|
+
try {
|
|
164
|
+
const discovery = new GatewayDiscovery({});
|
|
165
|
+
gateway = await discovery.discover(wellKnownUrl);
|
|
166
|
+
logger.info(`[AUN] Gateway discovered: ${gateway}`);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
logger.warn(`[AUN] Well-known discovery failed (${e}), no fallback available`);
|
|
116
170
|
}
|
|
117
171
|
}
|
|
118
172
|
if (!gateway) {
|
|
119
|
-
logger.error('[AUN] Cannot
|
|
173
|
+
logger.error('[AUN] Cannot resolve gateway URL from AID');
|
|
120
174
|
return;
|
|
121
175
|
}
|
|
122
176
|
logger.info(`[AUN] Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
|
|
@@ -146,10 +200,36 @@ export class AUNChannel {
|
|
|
146
200
|
this.handleIncomingGroupMessage(data);
|
|
147
201
|
});
|
|
148
202
|
this.client.on('connection.state', (data) => {
|
|
149
|
-
|
|
203
|
+
// trace is handled inside handleConnectionState with throttling
|
|
150
204
|
this.handleConnectionState(data);
|
|
151
205
|
});
|
|
206
|
+
this.client.on('message.recalled', (data) => {
|
|
207
|
+
this.trace('IN', 'message.recalled', data);
|
|
208
|
+
if (data && typeof data === 'object') {
|
|
209
|
+
const ids = data.message_ids;
|
|
210
|
+
if (Array.isArray(ids)) {
|
|
211
|
+
for (const id of ids) {
|
|
212
|
+
if (typeof id === 'string') {
|
|
213
|
+
logger.info(`[AUN] Message recalled: ${id}`);
|
|
214
|
+
this.recallHandler?.(id);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
152
220
|
// Authenticate
|
|
221
|
+
// Workaround: SDK 0.3.x _loadIdentityOrRaise doesn't set identity.aid from requested aid,
|
|
222
|
+
// causing gateway "missing aid" error. Patch to backfill aid on loaded identity.
|
|
223
|
+
const authFlow = this.client._auth;
|
|
224
|
+
if (authFlow && typeof authFlow._loadIdentityOrRaise === 'function') {
|
|
225
|
+
const origLoad = authFlow._loadIdentityOrRaise.bind(authFlow);
|
|
226
|
+
authFlow._loadIdentityOrRaise = (aid) => {
|
|
227
|
+
const identity = origLoad(aid);
|
|
228
|
+
if (identity && !identity.aid)
|
|
229
|
+
identity.aid = aid ?? authFlow._aid;
|
|
230
|
+
return identity;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
153
233
|
let accessToken;
|
|
154
234
|
try {
|
|
155
235
|
logger.info(`[AUN] Authenticating as ${aidName}...`);
|
|
@@ -169,7 +249,8 @@ export class AUNChannel {
|
|
|
169
249
|
// Fallback: try direct token from env/config (legacy)
|
|
170
250
|
accessToken = this.config.accessToken || process.env.AUN_ACCESS_TOKEN || '';
|
|
171
251
|
if (!accessToken) {
|
|
172
|
-
logger.error(`[AUN] No accessToken fallback available,
|
|
252
|
+
logger.error(`[AUN] No accessToken fallback available, scheduling retry`);
|
|
253
|
+
this.scheduleReconnect();
|
|
173
254
|
return;
|
|
174
255
|
}
|
|
175
256
|
logger.warn(`[AUN] Using accessToken fallback`);
|
|
@@ -178,16 +259,88 @@ export class AUNChannel {
|
|
|
178
259
|
try {
|
|
179
260
|
await this.client.connect({ access_token: accessToken, gateway: this.client._gatewayUrl }, { auto_reconnect: true, retry: { max_attempts: 5, initial_delay: 1.0, max_delay: 30.0 } });
|
|
180
261
|
this._aid = this.client.aid ?? undefined;
|
|
262
|
+
const deviceId = this.client._device_id ?? '';
|
|
263
|
+
this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
|
|
181
264
|
this.connected = true;
|
|
182
265
|
this.reconnectAttempt = 0;
|
|
266
|
+
// Workaround: SDK e2ee uses _identity.cert for sender_cert_fingerprint;
|
|
267
|
+
// if cert is missing, it falls back to public key SPKI fingerprint which
|
|
268
|
+
// causes peer cert lookup failures. Backfill from keystore if needed.
|
|
269
|
+
const clientAny = this.client;
|
|
270
|
+
if (clientAny._identity && !clientAny._identity.cert) {
|
|
271
|
+
const cert = clientAny._keystore?.loadCert?.(aidName);
|
|
272
|
+
if (cert) {
|
|
273
|
+
clientAny._identity.cert = cert;
|
|
274
|
+
logger.info('[AUN] Backfilled identity.cert from keystore for e2ee fingerprint');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
183
277
|
logger.info(`[AUN] Connected as ${this._aid}`);
|
|
184
278
|
}
|
|
185
279
|
catch (e) {
|
|
186
280
|
logger.error(`[AUN] Connection failed: ${e}`);
|
|
281
|
+
this.scheduleReconnect();
|
|
187
282
|
return;
|
|
188
283
|
}
|
|
189
284
|
}
|
|
190
285
|
// ── Event handlers ──────────────────────────────────────────
|
|
286
|
+
async downloadAttachment(att, channelId) {
|
|
287
|
+
const ownerAid = att.owner_aid || this._aid || '';
|
|
288
|
+
const objectKey = att.object_key;
|
|
289
|
+
const filename = att.filename || objectKey.split('/').pop() || 'unknown';
|
|
290
|
+
if (!objectKey) {
|
|
291
|
+
logger.warn('[AUN] Attachment missing object_key, skipping');
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
let downloadUrl;
|
|
295
|
+
try {
|
|
296
|
+
const ticket = await this.client.call('storage.create_download_ticket', {
|
|
297
|
+
owner_aid: ownerAid,
|
|
298
|
+
object_key: objectKey,
|
|
299
|
+
});
|
|
300
|
+
downloadUrl = ticket.download_url || '';
|
|
301
|
+
if (!downloadUrl) {
|
|
302
|
+
logger.warn(`[AUN] No download_url for attachment: ${filename}`);
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
logger.warn(`[AUN] create_download_ticket failed for ${filename}: ${e}`);
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
let buffer;
|
|
311
|
+
try {
|
|
312
|
+
const res = await fetch(downloadUrl);
|
|
313
|
+
if (!res.ok) {
|
|
314
|
+
logger.warn(`[AUN] Download failed for ${filename}: HTTP ${res.status}`);
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
buffer = Buffer.from(await res.arrayBuffer());
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
logger.warn(`[AUN] Download error for ${filename}: ${e}`);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
if (att.sha256) {
|
|
324
|
+
const { createHash } = await import('node:crypto');
|
|
325
|
+
const actual = createHash('sha256').update(buffer).digest('hex');
|
|
326
|
+
if (actual !== att.sha256) {
|
|
327
|
+
logger.warn(`[AUN] SHA256 mismatch for ${filename}: expected ${att.sha256.slice(0, 8)}… got ${actual.slice(0, 8)}…`);
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const projectPath = this.projectPathProvider
|
|
332
|
+
? await this.projectPathProvider(channelId)
|
|
333
|
+
: process.cwd();
|
|
334
|
+
try {
|
|
335
|
+
const result = saveToUploads(buffer, filename, projectPath);
|
|
336
|
+
logger.info(`[AUN] Saved attachment: ${result.filePath} (${result.size} bytes)`);
|
|
337
|
+
return result.filePath;
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
logger.warn(`[AUN] saveToUploads failed for ${filename}: ${e}`);
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
191
344
|
async handleIncomingPrivateMessage(data) {
|
|
192
345
|
if (!data || typeof data !== 'object')
|
|
193
346
|
return;
|
|
@@ -195,23 +348,62 @@ export class AUNChannel {
|
|
|
195
348
|
const fromAid = msg.from ?? '';
|
|
196
349
|
const payload = msg.payload ?? '';
|
|
197
350
|
const text = this.extractTextPayload(payload);
|
|
198
|
-
const taskId =
|
|
351
|
+
const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
199
352
|
const messageId = msg.message_id ?? '';
|
|
200
353
|
const seq = msg.seq;
|
|
354
|
+
// 回声过滤:自己发出的消息会被 gateway fanout 回来,
|
|
355
|
+
// 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
|
|
356
|
+
const msgChatId = typeof payload === 'object' && payload !== null && payload.chat_id;
|
|
357
|
+
if (this._aid && fromAid === this._aid && (!msgChatId || !this._chatId || msgChatId !== this._chatId)) {
|
|
358
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
201
361
|
// Detect @mentions
|
|
202
362
|
const mentions = [];
|
|
203
363
|
if (this._aid && text.includes(`@${this._aid}`)) {
|
|
204
364
|
mentions.push(this._aid);
|
|
205
365
|
}
|
|
366
|
+
// Process attachments
|
|
367
|
+
const rawAttachments = Array.isArray(payload?.attachments)
|
|
368
|
+
? payload.attachments
|
|
369
|
+
: [];
|
|
370
|
+
let finalText = text;
|
|
371
|
+
if (rawAttachments.length > 0 && this.client) {
|
|
372
|
+
const fileParts = [];
|
|
373
|
+
for (const att of rawAttachments) {
|
|
374
|
+
const filePath = await this.downloadAttachment(att, fromAid);
|
|
375
|
+
if (filePath) {
|
|
376
|
+
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
377
|
+
fileParts.push(`[文件: ${name} → ${filePath}]`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (fileParts.length > 0) {
|
|
381
|
+
const parts = [];
|
|
382
|
+
if (text)
|
|
383
|
+
parts.push(text);
|
|
384
|
+
parts.push(...fileParts);
|
|
385
|
+
parts.push('请使用 Read 工具读取文件内容。');
|
|
386
|
+
finalText = parts.join('\n\n');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Extract chat_id from payload for multi-instance routing (falls back to fromAid)
|
|
390
|
+
const chatId = (typeof payload === 'object' && payload !== null && payload.chat_id)
|
|
391
|
+
? String(payload.chat_id)
|
|
392
|
+
: fromAid;
|
|
393
|
+
const peerInfo = await this.fetchPeerInfo(fromAid);
|
|
394
|
+
const shortAid = this.getShortAid(fromAid);
|
|
395
|
+
const displayName = peerInfo.name || shortAid;
|
|
206
396
|
this.dispatchMessage({
|
|
207
|
-
channelId:
|
|
397
|
+
channelId: chatId,
|
|
208
398
|
userId: fromAid,
|
|
209
|
-
text,
|
|
399
|
+
text: finalText,
|
|
210
400
|
chatType: 'private',
|
|
211
401
|
messageId,
|
|
212
402
|
seq,
|
|
213
403
|
taskId,
|
|
214
404
|
mentions,
|
|
405
|
+
peerName: displayName || undefined,
|
|
406
|
+
peerType: peerInfo.type || 'unknown',
|
|
215
407
|
});
|
|
216
408
|
}
|
|
217
409
|
async handleIncomingGroupMessage(data) {
|
|
@@ -219,12 +411,16 @@ export class AUNChannel {
|
|
|
219
411
|
return;
|
|
220
412
|
const msg = data;
|
|
221
413
|
const groupId = msg.group_id ?? '';
|
|
222
|
-
const senderAid = msg.sender_aid ??
|
|
414
|
+
const senderAid = msg.sender_aid ?? '';
|
|
223
415
|
const payload = msg.payload ?? '';
|
|
224
416
|
const text = this.extractTextPayload(payload);
|
|
225
|
-
const taskId =
|
|
417
|
+
const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
226
418
|
const messageId = msg.message_id ?? '';
|
|
227
419
|
const seq = msg.seq;
|
|
420
|
+
// Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
|
|
421
|
+
const payloadMentions = Array.isArray(payload?.mentions)
|
|
422
|
+
? payload.mentions.filter((m) => typeof m === 'string')
|
|
423
|
+
: [];
|
|
228
424
|
logger.info(`[AUN][DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
|
|
229
425
|
if (!groupId || !senderAid) {
|
|
230
426
|
this.acknowledgeImmediately(messageId, seq);
|
|
@@ -234,29 +430,61 @@ export class AUNChannel {
|
|
|
234
430
|
this.acknowledgeImmediately(messageId, seq);
|
|
235
431
|
return;
|
|
236
432
|
}
|
|
237
|
-
const mentionedSelf = this._aid
|
|
238
|
-
|
|
433
|
+
const mentionedSelf = this._aid
|
|
434
|
+
? (this.hasExplicitMention(text, this._aid) || payloadMentions.includes(this._aid))
|
|
435
|
+
: false;
|
|
436
|
+
const mentionedAll = this.hasExplicitMention(text, 'all') || payloadMentions.includes('all');
|
|
239
437
|
if (!mentionedSelf && !mentionedAll) {
|
|
240
438
|
this.acknowledgeImmediately(messageId, seq);
|
|
241
439
|
return;
|
|
242
440
|
}
|
|
243
|
-
const strippedText = this.
|
|
244
|
-
|
|
441
|
+
const strippedText = this.stripSelfMentionIfOnly(text, this._aid);
|
|
442
|
+
// Detect attachments before the empty-text guard
|
|
443
|
+
const rawAttachments = Array.isArray(payload?.attachments)
|
|
444
|
+
? payload.attachments
|
|
445
|
+
: [];
|
|
446
|
+
const hasAttachments = rawAttachments.length > 0;
|
|
447
|
+
// Allow through if there's text OR attachments; both-empty messages are silently dropped
|
|
448
|
+
if (!strippedText && !hasAttachments) {
|
|
245
449
|
this.acknowledgeImmediately(messageId, seq);
|
|
246
450
|
return;
|
|
247
451
|
}
|
|
248
452
|
const mentions = mentionedAll ? ['all'] : (this._aid ? [this._aid] : []);
|
|
453
|
+
// Process attachments
|
|
454
|
+
let finalText = strippedText;
|
|
455
|
+
if (hasAttachments && this.client) {
|
|
456
|
+
const fileParts = [];
|
|
457
|
+
for (const att of rawAttachments) {
|
|
458
|
+
const filePath = await this.downloadAttachment(att, groupId);
|
|
459
|
+
if (filePath) {
|
|
460
|
+
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
461
|
+
fileParts.push(`[文件: ${name} → ${filePath}]`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (fileParts.length > 0) {
|
|
465
|
+
const parts = [];
|
|
466
|
+
if (strippedText)
|
|
467
|
+
parts.push(strippedText);
|
|
468
|
+
parts.push(...fileParts);
|
|
469
|
+
parts.push('请使用 Read 工具读取文件内容。');
|
|
470
|
+
finalText = parts.join('\n\n');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const peerInfo = await this.fetchPeerInfo(senderAid);
|
|
474
|
+
const shortAid = this.getShortAid(senderAid);
|
|
475
|
+
const displayName = peerInfo.name || shortAid;
|
|
249
476
|
this.dispatchMessage({
|
|
250
477
|
channelId: groupId,
|
|
251
478
|
userId: senderAid,
|
|
252
|
-
peerName:
|
|
253
|
-
|
|
479
|
+
peerName: displayName || undefined,
|
|
480
|
+
peerType: peerInfo.type || 'unknown',
|
|
481
|
+
text: finalText,
|
|
254
482
|
chatType: 'group',
|
|
255
483
|
messageId,
|
|
256
484
|
seq,
|
|
257
485
|
taskId,
|
|
258
486
|
mentions,
|
|
259
|
-
replyContext: this.buildGroupReplyContext(taskId, senderAid
|
|
487
|
+
replyContext: this.buildGroupReplyContext(taskId, senderAid),
|
|
260
488
|
});
|
|
261
489
|
}
|
|
262
490
|
dispatchMessage(event) {
|
|
@@ -286,6 +514,7 @@ export class AUNChannel {
|
|
|
286
514
|
chatType: event.chatType,
|
|
287
515
|
peerId: event.userId || event.channelId || '',
|
|
288
516
|
peerName: event.peerName,
|
|
517
|
+
peerType: event.peerType,
|
|
289
518
|
messageId: event.messageId,
|
|
290
519
|
threadId: event.taskId,
|
|
291
520
|
mentions: mentionObjects,
|
|
@@ -301,6 +530,8 @@ export class AUNChannel {
|
|
|
301
530
|
if (state === 'connected') {
|
|
302
531
|
this.connected = true;
|
|
303
532
|
this.reconnectAttempt = 0;
|
|
533
|
+
this.lastReconnectLogTime = 0;
|
|
534
|
+
this.lastReconnectLogAttempt = 0;
|
|
304
535
|
logger.info('[AUN] Connected');
|
|
305
536
|
}
|
|
306
537
|
else if (state === 'disconnected') {
|
|
@@ -308,21 +539,50 @@ export class AUNChannel {
|
|
|
308
539
|
logger.warn(`[AUN] Disconnected: ${data.error ?? 'unknown'}`);
|
|
309
540
|
}
|
|
310
541
|
else if (state === 'reconnecting') {
|
|
311
|
-
|
|
542
|
+
const attempt = data.attempt ?? 0;
|
|
543
|
+
const now = Date.now();
|
|
544
|
+
// Throttled logging: first attempt, every N attempts, or every M seconds
|
|
545
|
+
const isFirst = attempt <= 1;
|
|
546
|
+
const isStep = attempt - this.lastReconnectLogAttempt >= AUNChannel.RECONNECT_LOG_STEP;
|
|
547
|
+
const isInterval = now - this.lastReconnectLogTime >= AUNChannel.RECONNECT_LOG_INTERVAL;
|
|
548
|
+
if (isFirst || isStep || isInterval) {
|
|
549
|
+
const suppressed = attempt - this.lastReconnectLogAttempt - 1;
|
|
550
|
+
const suffix = suppressed > 0 ? `, ${suppressed} suppressed since last log` : '';
|
|
551
|
+
logger.info(`[AUN] SDK reconnecting (attempt ${attempt}${suffix})`);
|
|
552
|
+
this.lastReconnectLogTime = now;
|
|
553
|
+
this.lastReconnectLogAttempt = attempt;
|
|
554
|
+
this.trace('IN', 'connection.state', data);
|
|
555
|
+
}
|
|
556
|
+
// Detect runaway SDK reconnect loop: force disconnect and use TS-layer backoff
|
|
557
|
+
if (attempt >= AUNChannel.SDK_RECONNECT_GIVEUP && !this.intentionalDisconnect) {
|
|
558
|
+
logger.warn(`[AUN] SDK reconnect stuck at attempt ${attempt}, forcing TS-layer reconnect with backoff`);
|
|
559
|
+
this.connected = false;
|
|
560
|
+
if (this.client) {
|
|
561
|
+
this.client.close().catch(() => { });
|
|
562
|
+
this.client = null;
|
|
563
|
+
}
|
|
564
|
+
this.scheduleReconnect();
|
|
565
|
+
}
|
|
312
566
|
}
|
|
313
567
|
else if (state === 'terminal_failed') {
|
|
314
568
|
this.connected = false;
|
|
315
|
-
|
|
316
|
-
|
|
569
|
+
const reason = data.reason ?? '';
|
|
570
|
+
logger.error(`[AUN] Terminal failure: ${data.error ?? 'unknown'}${reason ? ` (${reason})` : ''}`);
|
|
317
571
|
if (!this.intentionalDisconnect) {
|
|
318
572
|
this.scheduleReconnect();
|
|
319
573
|
}
|
|
320
574
|
}
|
|
321
575
|
}
|
|
322
576
|
// ── Public API (same interface as before) ───────────────────
|
|
577
|
+
onProjectPathRequest(provider) {
|
|
578
|
+
this.projectPathProvider = provider;
|
|
579
|
+
}
|
|
323
580
|
onMessage(handler) {
|
|
324
581
|
this.messageHandler = handler;
|
|
325
582
|
}
|
|
583
|
+
onRecall(handler) {
|
|
584
|
+
this.recallHandler = handler;
|
|
585
|
+
}
|
|
326
586
|
async sendMessage(channelId, text, context) {
|
|
327
587
|
if (!this.connected || !this.client) {
|
|
328
588
|
logger.warn('[AUN] Cannot send: not connected');
|
|
@@ -338,24 +598,30 @@ export class AUNChannel {
|
|
|
338
598
|
finalText = '最终回复\n' + text;
|
|
339
599
|
}
|
|
340
600
|
this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
|
|
341
|
-
//
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
const params = { payload: { text: finalText }, encrypt: true };
|
|
601
|
+
// 群聊 @ 兜底:提示词已告知 agent 要 @,但如果 agent 没写,系统自动补上
|
|
602
|
+
if (this.isGroupId(channelId) && context?.peerId) {
|
|
603
|
+
if (!finalText.includes(`@${context.peerId}`)) {
|
|
604
|
+
finalText = `@${context.peerId} ` + finalText;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const payload = { type: 'text', text: finalText };
|
|
349
608
|
if (context?.threadId)
|
|
350
|
-
|
|
609
|
+
payload.thread_id = context.threadId;
|
|
610
|
+
const params = { payload, encrypt: true };
|
|
611
|
+
// Multi-instance routing: channelId may be "aid:device_id:slot_id"
|
|
612
|
+
const colonIdx = channelId.indexOf(':');
|
|
613
|
+
const targetAid = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
|
|
614
|
+
if (colonIdx > 0) {
|
|
615
|
+
params.payload.chat_id = channelId;
|
|
616
|
+
}
|
|
351
617
|
try {
|
|
352
|
-
if (
|
|
618
|
+
if (this.isGroupId(channelId)) {
|
|
353
619
|
params.group_id = channelId;
|
|
354
620
|
this.trace('OUT', 'group.send', params);
|
|
355
621
|
await this.client.call('group.send', params);
|
|
356
622
|
}
|
|
357
623
|
else {
|
|
358
|
-
params.to =
|
|
624
|
+
params.to = targetAid;
|
|
359
625
|
this.trace('OUT', 'message.send', params);
|
|
360
626
|
await this.client.call('message.send', params);
|
|
361
627
|
}
|
|
@@ -365,33 +631,136 @@ export class AUNChannel {
|
|
|
365
631
|
logger.error(`[AUN] Send failed to ${channelId}: ${e}`);
|
|
366
632
|
}
|
|
367
633
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
634
|
+
async sendFile(channelId, filePath, context) {
|
|
635
|
+
if (!this.connected || !this.client) {
|
|
636
|
+
logger.warn('[AUN] Cannot sendFile: not connected');
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const absPath = path.resolve(filePath);
|
|
640
|
+
if (!fs.existsSync(absPath)) {
|
|
641
|
+
logger.warn(`[AUN] sendFile: file not found: ${absPath}`);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const stat = fs.statSync(absPath);
|
|
645
|
+
if (stat.size === 0) {
|
|
646
|
+
logger.warn('[AUN] sendFile: file is empty');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (stat.size > 10 * 1024 * 1024) {
|
|
650
|
+
logger.warn(`[AUN] sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const filename = path.basename(absPath);
|
|
654
|
+
const fileData = fs.readFileSync(absPath);
|
|
655
|
+
const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
|
|
656
|
+
const contentType = guessMime(filename);
|
|
657
|
+
const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
|
|
658
|
+
try {
|
|
659
|
+
// Upload to storage
|
|
660
|
+
if (stat.size <= 64 * 1024) {
|
|
661
|
+
// Inline upload for small files (≤64KB)
|
|
662
|
+
await this.client.call('storage.put_object', {
|
|
663
|
+
object_key: objectKey,
|
|
664
|
+
content: fileData.toString('base64'),
|
|
665
|
+
content_type: contentType,
|
|
666
|
+
is_private: false,
|
|
667
|
+
overwrite: true,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
// Ticket upload for large files
|
|
672
|
+
const session = await this.client.call('storage.create_upload_session', {
|
|
673
|
+
object_key: objectKey,
|
|
674
|
+
size_bytes: stat.size,
|
|
675
|
+
content_type: contentType,
|
|
676
|
+
});
|
|
677
|
+
const uploadUrl = session.upload_url;
|
|
678
|
+
if (!uploadUrl)
|
|
679
|
+
throw new Error('No upload_url in session response');
|
|
680
|
+
const uploadResp = await fetch(uploadUrl, { method: 'PUT', body: fileData });
|
|
681
|
+
if (!uploadResp.ok)
|
|
682
|
+
throw new Error(`HTTP upload failed: ${uploadResp.status}`);
|
|
683
|
+
await this.client.call('storage.complete_upload', {
|
|
684
|
+
object_key: objectKey,
|
|
685
|
+
sha256,
|
|
686
|
+
content_type: contentType,
|
|
687
|
+
is_private: false,
|
|
688
|
+
size_bytes: stat.size,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
// Send message with attachment
|
|
692
|
+
const attachment = {
|
|
693
|
+
owner_aid: this._aid || '',
|
|
694
|
+
object_key: objectKey,
|
|
695
|
+
filename,
|
|
696
|
+
size_bytes: stat.size,
|
|
697
|
+
sha256,
|
|
698
|
+
content_type: contentType,
|
|
699
|
+
};
|
|
700
|
+
const filePayload = {
|
|
701
|
+
type: 'file',
|
|
702
|
+
text: `📎 ${filename} (${formatSize(stat.size)})`,
|
|
703
|
+
attachments: [attachment],
|
|
704
|
+
};
|
|
705
|
+
if (context?.threadId)
|
|
706
|
+
filePayload.thread_id = context.threadId;
|
|
707
|
+
const params = { payload: filePayload, encrypt: true };
|
|
708
|
+
// Multi-instance routing
|
|
709
|
+
const fileColonIdx = channelId.indexOf(':');
|
|
710
|
+
const fileTargetAid = fileColonIdx > 0 ? channelId.substring(0, fileColonIdx) : channelId;
|
|
711
|
+
if (fileColonIdx > 0) {
|
|
712
|
+
params.payload.chat_id = channelId;
|
|
713
|
+
}
|
|
714
|
+
if (this.isGroupId(channelId)) {
|
|
715
|
+
params.group_id = channelId;
|
|
716
|
+
this.trace('OUT', 'group.send.file', params);
|
|
717
|
+
await this.client.call('group.send', params);
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
params.to = fileTargetAid;
|
|
721
|
+
this.trace('OUT', 'message.send.file', params);
|
|
722
|
+
await this.client.call('message.send', params);
|
|
723
|
+
}
|
|
724
|
+
logger.info(`[AUN] File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
|
|
725
|
+
}
|
|
726
|
+
catch (e) {
|
|
727
|
+
this.trace('OUT', 'sendFile.error', { channelId, filePath, error: String(e) });
|
|
728
|
+
logger.error(`[AUN] sendFile failed for ${channelId}: ${e}`);
|
|
375
729
|
}
|
|
376
730
|
}
|
|
731
|
+
acknowledge(messageId) {
|
|
732
|
+
// Gateway auto-delivery-ack is sufficient; skip explicit message.ack RPC
|
|
733
|
+
// to avoid duplicate "已送达" at the sender CLI
|
|
734
|
+
this.messageSeqMap.delete(messageId);
|
|
735
|
+
}
|
|
377
736
|
sendProcessingStatus(channelId, status, sessionId, context) {
|
|
378
737
|
if (status === 'start')
|
|
379
738
|
this.sentCount.delete(channelId); // 新任务开始,重置计数
|
|
380
739
|
if (!this.client || !this.connected)
|
|
381
740
|
return;
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
741
|
+
const eventMap = {
|
|
742
|
+
start: 'task.started',
|
|
743
|
+
done: 'task.completed',
|
|
744
|
+
interrupted: 'task.interrupted',
|
|
745
|
+
error: 'task.error',
|
|
746
|
+
timeout: 'task.timeout',
|
|
387
747
|
};
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
748
|
+
const payload = {
|
|
749
|
+
type: 'event',
|
|
750
|
+
event: eventMap[status] ?? `task.${status}`,
|
|
751
|
+
data: { session_id: sessionId },
|
|
752
|
+
severity: status === 'error' || status === 'timeout' ? 'error' : 'info',
|
|
391
753
|
};
|
|
392
754
|
if (context?.threadId)
|
|
393
|
-
|
|
394
|
-
|
|
755
|
+
payload.thread_id = context.threadId;
|
|
756
|
+
const params = { payload, encrypt: true };
|
|
757
|
+
// Multi-instance routing
|
|
758
|
+
const statusColonIdx = channelId.indexOf(':');
|
|
759
|
+
const statusTargetAid = statusColonIdx > 0 ? channelId.substring(0, statusColonIdx) : channelId;
|
|
760
|
+
if (statusColonIdx > 0) {
|
|
761
|
+
payload.chat_id = channelId;
|
|
762
|
+
}
|
|
763
|
+
if (this.isGroupId(channelId)) {
|
|
395
764
|
params.group_id = channelId;
|
|
396
765
|
this.trace('OUT', 'group.send.status', params);
|
|
397
766
|
this.client.call('group.send', params).catch(e => {
|
|
@@ -399,7 +768,7 @@ export class AUNChannel {
|
|
|
399
768
|
});
|
|
400
769
|
}
|
|
401
770
|
else {
|
|
402
|
-
params.to =
|
|
771
|
+
params.to = statusTargetAid;
|
|
403
772
|
this.trace('OUT', 'message.send.status', params);
|
|
404
773
|
this.client.call('message.send', params).catch(e => {
|
|
405
774
|
logger.debug(`[AUN] Processing status failed: ${e}`);
|
|
@@ -419,9 +788,15 @@ export class AUNChannel {
|
|
|
419
788
|
catch {
|
|
420
789
|
payloadObj = { text: payload };
|
|
421
790
|
}
|
|
791
|
+
// Multi-instance routing
|
|
792
|
+
const customColonIdx = channelId.indexOf(':');
|
|
793
|
+
const customTargetAid = customColonIdx > 0 ? channelId.substring(0, customColonIdx) : channelId;
|
|
794
|
+
if (customColonIdx > 0) {
|
|
795
|
+
payloadObj.chat_id = channelId;
|
|
796
|
+
}
|
|
422
797
|
const sendParams = {
|
|
423
|
-
to:
|
|
424
|
-
encrypt: true,
|
|
798
|
+
to: customTargetAid, payload: payloadObj,
|
|
799
|
+
encrypt: true,
|
|
425
800
|
};
|
|
426
801
|
this.trace('OUT', 'message.send.custom', sendParams);
|
|
427
802
|
this.client.call('message.send', sendParams).catch(e => {
|
|
@@ -507,6 +882,37 @@ export class AUNChannel {
|
|
|
507
882
|
maxAttempts: AUNChannel.RECONNECT_DELAYS.length,
|
|
508
883
|
};
|
|
509
884
|
}
|
|
885
|
+
async fetchPeerInfo(aid) {
|
|
886
|
+
const cached = this.peerInfoCache.get(aid);
|
|
887
|
+
if (cached !== undefined)
|
|
888
|
+
return cached;
|
|
889
|
+
if (!this.client)
|
|
890
|
+
return { type: null };
|
|
891
|
+
try {
|
|
892
|
+
const md = await this.client.auth.downloadAgentMd(aid);
|
|
893
|
+
const typeMatch = md.match(/^type:\s*["']?(\w+)["']?/m);
|
|
894
|
+
const nameMatch = md.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
895
|
+
const type = typeMatch?.[1] === 'human' ? 'human' : 'ai';
|
|
896
|
+
const name = nameMatch?.[1]?.trim() || undefined;
|
|
897
|
+
const info = { type, name };
|
|
898
|
+
this.peerInfoCache.set(aid, info);
|
|
899
|
+
setTimeout(() => this.peerInfoCache.delete(aid), 30 * 60 * 1000);
|
|
900
|
+
return info;
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
return { type: null }; // no agent.md → unknown
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async uploadAgentMd(content) {
|
|
907
|
+
if (!this.client)
|
|
908
|
+
throw new Error('not connected');
|
|
909
|
+
await this.client.auth.uploadAgentMd(content);
|
|
910
|
+
}
|
|
911
|
+
async downloadAgentMd(aid) {
|
|
912
|
+
if (!this.client)
|
|
913
|
+
throw new Error('not connected');
|
|
914
|
+
return this.client.auth.downloadAgentMd(aid);
|
|
915
|
+
}
|
|
510
916
|
}
|
|
511
917
|
// Plugin implementation
|
|
512
918
|
export class AUNChannelPlugin {
|
|
@@ -529,7 +935,6 @@ export class AUNChannelPlugin {
|
|
|
529
935
|
const channel = new AUNChannel({
|
|
530
936
|
aid: inst.aid,
|
|
531
937
|
keystorePath: inst.keystorePath,
|
|
532
|
-
gatewayPort: inst.gatewayPort,
|
|
533
938
|
gatewayUrl: inst.gatewayUrl,
|
|
534
939
|
accessToken: inst.accessToken,
|
|
535
940
|
flushDelay: inst.flushDelay,
|
|
@@ -539,19 +944,23 @@ export class AUNChannelPlugin {
|
|
|
539
944
|
const adapter = {
|
|
540
945
|
channelName: inst.name,
|
|
541
946
|
sendText: (id, text, context) => channel.sendMessage(id, text, context),
|
|
947
|
+
sendFile: (id, filePath, context) => channel.sendFile(id, filePath, context),
|
|
542
948
|
acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); },
|
|
543
949
|
sendProcessingStatus: (id, status, sessionId, context) => channel.sendProcessingStatus(id, status, sessionId, context),
|
|
544
950
|
sendCustomPayload: (id, payload) => channel.sendCustomPayload(id, payload),
|
|
951
|
+
uploadAgentMd: (content) => channel.uploadAgentMd(content),
|
|
952
|
+
downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
|
|
953
|
+
_selfAid: () => channel.getStatus().aid,
|
|
545
954
|
};
|
|
546
955
|
const policy = {
|
|
547
|
-
canSwitchProject: (chatType, identity) => identity === 'owner',
|
|
548
|
-
canListProjects: (chatType, identity) => identity === 'owner',
|
|
956
|
+
canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
957
|
+
canListProjects: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
549
958
|
canCreateSession: (chatType, identity) => true,
|
|
550
959
|
canDeleteSession: (chatType, identity) => true,
|
|
551
|
-
canImportCliSession: (chatType, identity) => identity === 'owner',
|
|
552
|
-
messagePrefix: () => '',
|
|
960
|
+
canImportCliSession: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
961
|
+
messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
|
|
553
962
|
showMiddleResult: (chatType, identity) => {
|
|
554
|
-
const mode = inst.
|
|
963
|
+
const mode = getChannelShowActivities(config, inst.name);
|
|
555
964
|
if (mode === 'none')
|
|
556
965
|
return false;
|
|
557
966
|
if (mode === 'dm-only')
|
|
@@ -561,7 +970,7 @@ export class AUNChannelPlugin {
|
|
|
561
970
|
return true;
|
|
562
971
|
},
|
|
563
972
|
showIdleMonitor: (chatType, identity) => {
|
|
564
|
-
const mode = inst.
|
|
973
|
+
const mode = getChannelShowActivities(config, inst.name);
|
|
565
974
|
if (mode === 'none')
|
|
566
975
|
return false;
|
|
567
976
|
if (mode === 'dm-only')
|
|
@@ -584,6 +993,7 @@ export class AUNChannelPlugin {
|
|
|
584
993
|
options,
|
|
585
994
|
connect: () => channel.connect(),
|
|
586
995
|
disconnect: () => channel.disconnect(),
|
|
996
|
+
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
587
997
|
});
|
|
588
998
|
}
|
|
589
999
|
return result;
|