evolclaw 2.4.0 → 2.5.1
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 +269 -23
- package/dist/agents/codex-runner.js +2 -8
- package/dist/agents/gemini-runner.js +1 -8
- package/dist/channels/aun.js +525 -53
- 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 +86 -10
- package/dist/config.js +98 -2
- package/dist/core/command-handler.js +554 -130
- package/dist/core/message/message-bridge.js +26 -9
- package/dist/core/message/message-processor.js +152 -57
- package/dist/core/message/message-queue.js +48 -0
- package/dist/core/message/stream-flusher.js +2 -2
- package/dist/core/permission.js +7 -11
- package/dist/core/session/session-manager.js +21 -3
- package/dist/index.js +48 -13
- 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 +752 -8
- package/dist/utils/init.js +85 -3
- package/dist/utils/media-cache.js +2 -0
- package/dist/utils/stats-collector.js +0 -8
- package/evolclaw-install.md +54 -0
- package/package.json +11 -4
package/dist/channels/aun.js
CHANGED
|
@@ -1,21 +1,63 @@
|
|
|
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';
|
|
5
|
+
import os from 'os';
|
|
4
6
|
import { logger, localTimestamp } from '../utils/logger.js';
|
|
5
|
-
import { normalizeChannelInstances } from '../config.js';
|
|
7
|
+
import { normalizeChannelInstances, getChannelShowActivities } from '../config.js';
|
|
6
8
|
import { resolvePaths } from '../paths.js';
|
|
9
|
+
import { saveToUploads, sanitizeFileName } from '../utils/media-cache.js';
|
|
10
|
+
function guessMime(filename) {
|
|
11
|
+
const ext = path.extname(filename).toLowerCase();
|
|
12
|
+
const map = {
|
|
13
|
+
'.txt': 'text/plain', '.md': 'text/markdown', '.json': 'application/json',
|
|
14
|
+
'.js': 'text/javascript', '.ts': 'text/typescript', '.py': 'text/x-python',
|
|
15
|
+
'.html': 'text/html', '.css': 'text/css', '.csv': 'text/csv',
|
|
16
|
+
'.pdf': 'application/pdf', '.zip': 'application/zip', '.gz': 'application/gzip',
|
|
17
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
18
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
19
|
+
'.xml': 'application/xml', '.yaml': 'application/x-yaml', '.yml': 'application/x-yaml',
|
|
20
|
+
};
|
|
21
|
+
return map[ext] || 'application/octet-stream';
|
|
22
|
+
}
|
|
23
|
+
function formatSize(bytes) {
|
|
24
|
+
if (bytes < 1024)
|
|
25
|
+
return `${bytes} B`;
|
|
26
|
+
if (bytes < 1048576)
|
|
27
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
28
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
29
|
+
}
|
|
7
30
|
export class AUNChannel {
|
|
8
31
|
config;
|
|
9
32
|
client = null;
|
|
33
|
+
projectPathProvider;
|
|
10
34
|
messageHandler;
|
|
35
|
+
recallHandler;
|
|
11
36
|
connected = false;
|
|
12
37
|
traceStream = null;
|
|
38
|
+
traceDate = ''; // 当前 trace 文件对应的日期 (YYYYMMDD)
|
|
13
39
|
trace(dir, event, data) {
|
|
40
|
+
if (!this.config.aunTrace)
|
|
41
|
+
return;
|
|
42
|
+
this.rotateTraceIfNeeded();
|
|
14
43
|
if (!this.traceStream)
|
|
15
44
|
return;
|
|
16
45
|
const line = JSON.stringify({ ts: localTimestamp(), dir, event, data });
|
|
17
46
|
this.traceStream.write(line + '\n');
|
|
18
47
|
}
|
|
48
|
+
rotateTraceIfNeeded() {
|
|
49
|
+
const d = new Date();
|
|
50
|
+
const today = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
|
|
51
|
+
if (this.traceDate === today && this.traceStream)
|
|
52
|
+
return;
|
|
53
|
+
if (this.traceStream) {
|
|
54
|
+
this.traceStream.end();
|
|
55
|
+
this.traceStream = null;
|
|
56
|
+
}
|
|
57
|
+
this.traceDate = today;
|
|
58
|
+
const logPath = path.join(resolvePaths().logs, `aun-${today}.log`);
|
|
59
|
+
this.traceStream = fs.createWriteStream(logPath, { flags: 'a' });
|
|
60
|
+
}
|
|
19
61
|
/** 判断 channelId 是否为群组 ID(g-xxx.agentid.pub 或 grp_ 前缀) */
|
|
20
62
|
isGroupId(id) {
|
|
21
63
|
return id.startsWith('grp_') || (id.startsWith('g-') && id.includes('.'));
|
|
@@ -43,14 +85,17 @@ export class AUNChannel {
|
|
|
43
85
|
const escaped = target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
44
86
|
return new RegExp(`(^|\\s)@${escaped}(?=$|\\s|[.,!?;:,。!?;:]|[\\u4e00-\\u9fff])`).test(text);
|
|
45
87
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return
|
|
88
|
+
stripSelfMentionIfOnly(text, selfAid) {
|
|
89
|
+
if (!selfAid)
|
|
90
|
+
return text;
|
|
91
|
+
const mentions = text.match(/@[\w.-]+/g) || [];
|
|
92
|
+
if (mentions.length !== 1)
|
|
93
|
+
return text;
|
|
94
|
+
const escapedAid = selfAid.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
95
|
+
return text
|
|
96
|
+
.replace(new RegExp(`(^|\\s)@${escapedAid}(?=$|\\s|[.,!?;:,。!?;:]|[\\u4e00-\\u9fff])`, 'g'), '$1')
|
|
97
|
+
.replace(/[ \t]+/g, ' ')
|
|
98
|
+
.trim();
|
|
54
99
|
}
|
|
55
100
|
buildGroupReplyContext(taskId, senderAid) {
|
|
56
101
|
const replyContext = {};
|
|
@@ -69,7 +114,9 @@ export class AUNChannel {
|
|
|
69
114
|
this.messageSeqMap.delete(messageId);
|
|
70
115
|
}
|
|
71
116
|
_aid;
|
|
117
|
+
_chatId = ''; // aid:device_id:slot_id — 多实例回声过滤
|
|
72
118
|
seenMessages = new Map();
|
|
119
|
+
peerInfoCache = new Map();
|
|
73
120
|
messageSeqMap = new Map(); // messageId → seq (for ack)
|
|
74
121
|
sentCount = new Map(); // channelId → 已发消息计数(用于判断最终回复)
|
|
75
122
|
// Reconnect state (TS-layer fallback, on top of SDK auto_reconnect)
|
|
@@ -78,12 +125,17 @@ export class AUNChannel {
|
|
|
78
125
|
reconnectTimer = null;
|
|
79
126
|
static RECONNECT_DELAYS = [60, 120, 300, 600]; // seconds
|
|
80
127
|
onChannelDown;
|
|
128
|
+
// SDK reconnect throttling — avoid log spam when SDK enters tight reconnect loop
|
|
129
|
+
lastReconnectLogTime = 0;
|
|
130
|
+
lastReconnectLogAttempt = 0;
|
|
131
|
+
static RECONNECT_LOG_INTERVAL = 60_000; // log at most every 60s
|
|
132
|
+
static RECONNECT_LOG_STEP = 100; // or every 100 attempts
|
|
133
|
+
static SDK_RECONNECT_GIVEUP = 50; // force TS-layer fallback after this many SDK attempts
|
|
81
134
|
constructor(config) {
|
|
82
135
|
this.config = config;
|
|
83
136
|
if (config.aunTrace) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
logger.info(`[AUN] Trace logging enabled: ${logPath}`);
|
|
137
|
+
this.rotateTraceIfNeeded();
|
|
138
|
+
logger.info(`[AUN] Trace logging enabled (daily rotation): ${resolvePaths().logs}/aun-YYYYMMDD.log`);
|
|
87
139
|
}
|
|
88
140
|
}
|
|
89
141
|
async connect() {
|
|
@@ -104,18 +156,22 @@ export class AUNChannel {
|
|
|
104
156
|
const aunPath = this.config.keystorePath || `${process.env.HOME || '~'}/.aun`;
|
|
105
157
|
const aidName = this.config.aid;
|
|
106
158
|
const encryptionSeed = this.config.encryptionSeed || process.env.AUN_ENCRYPTION_SEED || undefined;
|
|
107
|
-
// Gateway URL
|
|
159
|
+
// Gateway URL 解析:优先用配置的 gatewayUrl,否则通过 well-known 自动发现
|
|
108
160
|
let gateway = this.config.gatewayUrl || '';
|
|
109
161
|
if (!gateway) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
gateway =
|
|
162
|
+
// AID 本身即域名(如 evolai.agentid.pub),用其查询 well-known,与 Python SDK 行为对齐
|
|
163
|
+
const wellKnownUrl = `https://${aidName}/.well-known/aun-gateway`;
|
|
164
|
+
try {
|
|
165
|
+
const discovery = new GatewayDiscovery({});
|
|
166
|
+
gateway = await discovery.discover(wellKnownUrl);
|
|
167
|
+
logger.info(`[AUN] Gateway discovered: ${gateway}`);
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
logger.warn(`[AUN] Well-known discovery failed (${e}), no fallback available`);
|
|
115
171
|
}
|
|
116
172
|
}
|
|
117
173
|
if (!gateway) {
|
|
118
|
-
logger.error('[AUN] Cannot
|
|
174
|
+
logger.error('[AUN] Cannot resolve gateway URL from AID');
|
|
119
175
|
return;
|
|
120
176
|
}
|
|
121
177
|
logger.info(`[AUN] Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
|
|
@@ -145,9 +201,23 @@ export class AUNChannel {
|
|
|
145
201
|
this.handleIncomingGroupMessage(data);
|
|
146
202
|
});
|
|
147
203
|
this.client.on('connection.state', (data) => {
|
|
148
|
-
|
|
204
|
+
// trace is handled inside handleConnectionState with throttling
|
|
149
205
|
this.handleConnectionState(data);
|
|
150
206
|
});
|
|
207
|
+
this.client.on('message.recalled', (data) => {
|
|
208
|
+
this.trace('IN', 'message.recalled', data);
|
|
209
|
+
if (data && typeof data === 'object') {
|
|
210
|
+
const ids = data.message_ids;
|
|
211
|
+
if (Array.isArray(ids)) {
|
|
212
|
+
for (const id of ids) {
|
|
213
|
+
if (typeof id === 'string') {
|
|
214
|
+
logger.info(`[AUN] Message recalled: ${id}`);
|
|
215
|
+
this.recallHandler?.(id);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
});
|
|
151
221
|
// Authenticate
|
|
152
222
|
// Workaround: SDK 0.3.x _loadIdentityOrRaise doesn't set identity.aid from requested aid,
|
|
153
223
|
// causing gateway "missing aid" error. Patch to backfill aid on loaded identity.
|
|
@@ -190,6 +260,8 @@ export class AUNChannel {
|
|
|
190
260
|
try {
|
|
191
261
|
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 } });
|
|
192
262
|
this._aid = this.client.aid ?? undefined;
|
|
263
|
+
const deviceId = this.client._device_id ?? '';
|
|
264
|
+
this._chatId = this._aid ? `${this._aid}:${deviceId}:` : '';
|
|
193
265
|
this.connected = true;
|
|
194
266
|
this.reconnectAttempt = 0;
|
|
195
267
|
// Workaround: SDK e2ee uses _identity.cert for sender_cert_fingerprint;
|
|
@@ -204,6 +276,8 @@ export class AUNChannel {
|
|
|
204
276
|
}
|
|
205
277
|
}
|
|
206
278
|
logger.info(`[AUN] Connected as ${this._aid}`);
|
|
279
|
+
// Send welcome message to owner after first connection
|
|
280
|
+
await this.sendWelcomeMessage();
|
|
207
281
|
}
|
|
208
282
|
catch (e) {
|
|
209
283
|
logger.error(`[AUN] Connection failed: ${e}`);
|
|
@@ -211,7 +285,149 @@ export class AUNChannel {
|
|
|
211
285
|
return;
|
|
212
286
|
}
|
|
213
287
|
}
|
|
288
|
+
async sendWelcomeMessage() {
|
|
289
|
+
try {
|
|
290
|
+
const owner = this.config.owner;
|
|
291
|
+
if (!owner) {
|
|
292
|
+
logger.info('[AUN] No owner configured, skipping welcome message');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Check agent.md initialized field
|
|
296
|
+
const aid = this.config.aid;
|
|
297
|
+
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
298
|
+
const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
|
|
299
|
+
if (!fs.existsSync(agentMdPath)) {
|
|
300
|
+
logger.warn('[AUN] agent.md not found, skipping welcome message');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const agentMdContent = fs.readFileSync(agentMdPath, 'utf-8');
|
|
304
|
+
const match = agentMdContent.match(/^---\n([\s\S]*?)\n---/);
|
|
305
|
+
if (!match) {
|
|
306
|
+
logger.warn('[AUN] agent.md frontmatter not found');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const frontmatter = match[1];
|
|
310
|
+
const initializedMatch = frontmatter.match(/^initialized:\s*(true|false)/m);
|
|
311
|
+
if (!initializedMatch || initializedMatch[1] === 'true') {
|
|
312
|
+
logger.info('[AUN] Agent already initialized, skipping welcome message');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// Generate new agent.md with proper fields
|
|
316
|
+
const ownerShortId = owner.split('@')[0].slice(0, 8);
|
|
317
|
+
const newAgentMd = `---
|
|
318
|
+
aid: "${aid}"
|
|
319
|
+
name: "${ownerShortId}的Evol助手"
|
|
320
|
+
type: "codeagent"
|
|
321
|
+
version: "1.0.0"
|
|
322
|
+
description: "EvolClaw AI Agent Gateway - 连接 Claude/Codex 到消息通道"
|
|
323
|
+
tags:
|
|
324
|
+
- evolclaw
|
|
325
|
+
- ai-agent
|
|
326
|
+
- gateway
|
|
327
|
+
initialized: true
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
# ${ownerShortId}的Evol助手
|
|
331
|
+
|
|
332
|
+
EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
333
|
+
`;
|
|
334
|
+
// Write locally
|
|
335
|
+
fs.writeFileSync(agentMdPath, newAgentMd, 'utf-8');
|
|
336
|
+
logger.info('[AUN] Updated agent.md with initialized=true');
|
|
337
|
+
// Publish to AUN network via auth.uploadAgentMd
|
|
338
|
+
try {
|
|
339
|
+
await this.client.auth.uploadAgentMd(newAgentMd);
|
|
340
|
+
logger.info('[AUN] Published agent.md to AUN network');
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
logger.warn(`[AUN] Failed to publish agent.md: ${e}`);
|
|
344
|
+
}
|
|
345
|
+
// Send welcome message
|
|
346
|
+
const welcomeText = `🎉 欢迎使用 EvolClaw!
|
|
347
|
+
|
|
348
|
+
我是您的 AI Agent 网关,已成功连接到 AUN 网络。
|
|
349
|
+
|
|
350
|
+
📋 **日常使用方法**:
|
|
351
|
+
|
|
352
|
+
1. **绑定项目**:发送 \`/bind <项目路径>\` 绑定工作目录
|
|
353
|
+
2. **查看帮助**:发送 \`/help\` 查看所有可用命令
|
|
354
|
+
3. **切换项目**:发送 \`/project <项目名>\` 切换到其他项目
|
|
355
|
+
4. **查看状态**:发送 \`/status\` 查看当前会话状态
|
|
356
|
+
5. **查看 Agent 信息**:发送 \`/agentmd\` 查看 agent.md 内容
|
|
357
|
+
6. **会话管理**:发送 \`/session\` 查看和切换会话
|
|
358
|
+
|
|
359
|
+
💡 **提示**:
|
|
360
|
+
- 直接发送消息即可与 Claude/Codex 对话
|
|
361
|
+
- 支持多项目会话管理,每个项目独立会话
|
|
362
|
+
- 所有命令以 \`/\` 开头
|
|
363
|
+
|
|
364
|
+
现在,请先使用 \`/bind\` 命令绑定您的项目目录,然后就可以开始工作了!`;
|
|
365
|
+
await this.sendMessage(owner, welcomeText);
|
|
366
|
+
logger.info(`[AUN] Welcome message sent to owner: ${owner}`);
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
logger.warn(`[AUN] Failed to send welcome message: ${e}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
214
372
|
// ── Event handlers ──────────────────────────────────────────
|
|
373
|
+
async downloadAttachment(att, channelId) {
|
|
374
|
+
const ownerAid = att.owner_aid || this._aid || '';
|
|
375
|
+
const objectKey = att.object_key;
|
|
376
|
+
const filename = att.filename || objectKey.split('/').pop() || 'unknown';
|
|
377
|
+
if (!objectKey) {
|
|
378
|
+
logger.warn('[AUN] Attachment missing object_key, skipping');
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
let downloadUrl;
|
|
382
|
+
try {
|
|
383
|
+
const ticket = await this.client.call('storage.create_download_ticket', {
|
|
384
|
+
owner_aid: ownerAid,
|
|
385
|
+
object_key: objectKey,
|
|
386
|
+
});
|
|
387
|
+
downloadUrl = ticket.download_url || '';
|
|
388
|
+
if (!downloadUrl) {
|
|
389
|
+
logger.warn(`[AUN] No download_url for attachment: ${filename}`);
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (e) {
|
|
394
|
+
logger.warn(`[AUN] create_download_ticket failed for ${filename}: ${e}`);
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
let buffer;
|
|
398
|
+
try {
|
|
399
|
+
const res = await fetch(downloadUrl);
|
|
400
|
+
if (!res.ok) {
|
|
401
|
+
logger.warn(`[AUN] Download failed for ${filename}: HTTP ${res.status}`);
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
buffer = Buffer.from(await res.arrayBuffer());
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
logger.warn(`[AUN] Download error for ${filename}: ${e}`);
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
if (att.sha256) {
|
|
411
|
+
const { createHash } = await import('node:crypto');
|
|
412
|
+
const actual = createHash('sha256').update(buffer).digest('hex');
|
|
413
|
+
if (actual !== att.sha256) {
|
|
414
|
+
logger.warn(`[AUN] SHA256 mismatch for ${filename}: expected ${att.sha256.slice(0, 8)}… got ${actual.slice(0, 8)}…`);
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const projectPath = this.projectPathProvider
|
|
419
|
+
? await this.projectPathProvider(channelId)
|
|
420
|
+
: process.cwd();
|
|
421
|
+
try {
|
|
422
|
+
const result = saveToUploads(buffer, filename, projectPath);
|
|
423
|
+
logger.info(`[AUN] Saved attachment: ${result.filePath} (${result.size} bytes)`);
|
|
424
|
+
return result.filePath;
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
logger.warn(`[AUN] saveToUploads failed for ${filename}: ${e}`);
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
215
431
|
async handleIncomingPrivateMessage(data) {
|
|
216
432
|
if (!data || typeof data !== 'object')
|
|
217
433
|
return;
|
|
@@ -219,23 +435,62 @@ export class AUNChannel {
|
|
|
219
435
|
const fromAid = msg.from ?? '';
|
|
220
436
|
const payload = msg.payload ?? '';
|
|
221
437
|
const text = this.extractTextPayload(payload);
|
|
222
|
-
const taskId =
|
|
438
|
+
const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
223
439
|
const messageId = msg.message_id ?? '';
|
|
224
440
|
const seq = msg.seq;
|
|
441
|
+
// 回声过滤:自己发出的消息会被 gateway fanout 回来,
|
|
442
|
+
// 只有 from_aid == self 且 chat_id 不匹配时才丢弃(说明是其它实例发的)
|
|
443
|
+
const msgChatId = typeof payload === 'object' && payload !== null && payload.chat_id;
|
|
444
|
+
if (this._aid && fromAid === this._aid && (!msgChatId || !this._chatId || msgChatId !== this._chatId)) {
|
|
445
|
+
this.acknowledgeImmediately(messageId, seq);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
225
448
|
// Detect @mentions
|
|
226
449
|
const mentions = [];
|
|
227
450
|
if (this._aid && text.includes(`@${this._aid}`)) {
|
|
228
451
|
mentions.push(this._aid);
|
|
229
452
|
}
|
|
453
|
+
// Process attachments
|
|
454
|
+
const rawAttachments = Array.isArray(payload?.attachments)
|
|
455
|
+
? payload.attachments
|
|
456
|
+
: [];
|
|
457
|
+
let finalText = text;
|
|
458
|
+
if (rawAttachments.length > 0 && this.client) {
|
|
459
|
+
const fileParts = [];
|
|
460
|
+
for (const att of rawAttachments) {
|
|
461
|
+
const filePath = await this.downloadAttachment(att, fromAid);
|
|
462
|
+
if (filePath) {
|
|
463
|
+
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
464
|
+
fileParts.push(`[文件: ${name} → ${filePath}]`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
if (fileParts.length > 0) {
|
|
468
|
+
const parts = [];
|
|
469
|
+
if (text)
|
|
470
|
+
parts.push(text);
|
|
471
|
+
parts.push(...fileParts);
|
|
472
|
+
parts.push('请使用 Read 工具读取文件内容。');
|
|
473
|
+
finalText = parts.join('\n\n');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// Extract chat_id from payload for multi-instance routing (falls back to fromAid)
|
|
477
|
+
const chatId = (typeof payload === 'object' && payload !== null && payload.chat_id)
|
|
478
|
+
? String(payload.chat_id)
|
|
479
|
+
: fromAid;
|
|
480
|
+
const peerInfo = await this.fetchPeerInfo(fromAid);
|
|
481
|
+
const shortAid = this.getShortAid(fromAid);
|
|
482
|
+
const displayName = peerInfo.name || shortAid;
|
|
230
483
|
this.dispatchMessage({
|
|
231
|
-
channelId:
|
|
484
|
+
channelId: chatId,
|
|
232
485
|
userId: fromAid,
|
|
233
|
-
text,
|
|
486
|
+
text: finalText,
|
|
234
487
|
chatType: 'private',
|
|
235
488
|
messageId,
|
|
236
489
|
seq,
|
|
237
490
|
taskId,
|
|
238
491
|
mentions,
|
|
492
|
+
peerName: displayName || undefined,
|
|
493
|
+
peerType: peerInfo.type || 'unknown',
|
|
239
494
|
});
|
|
240
495
|
}
|
|
241
496
|
async handleIncomingGroupMessage(data) {
|
|
@@ -243,10 +498,10 @@ export class AUNChannel {
|
|
|
243
498
|
return;
|
|
244
499
|
const msg = data;
|
|
245
500
|
const groupId = msg.group_id ?? '';
|
|
246
|
-
const senderAid = msg.sender_aid ??
|
|
501
|
+
const senderAid = msg.sender_aid ?? '';
|
|
247
502
|
const payload = msg.payload ?? '';
|
|
248
503
|
const text = this.extractTextPayload(payload);
|
|
249
|
-
const taskId =
|
|
504
|
+
const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
250
505
|
const messageId = msg.message_id ?? '';
|
|
251
506
|
const seq = msg.seq;
|
|
252
507
|
// Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
|
|
@@ -270,17 +525,47 @@ export class AUNChannel {
|
|
|
270
525
|
this.acknowledgeImmediately(messageId, seq);
|
|
271
526
|
return;
|
|
272
527
|
}
|
|
273
|
-
const strippedText = this.
|
|
274
|
-
|
|
528
|
+
const strippedText = this.stripSelfMentionIfOnly(text, this._aid);
|
|
529
|
+
// Detect attachments before the empty-text guard
|
|
530
|
+
const rawAttachments = Array.isArray(payload?.attachments)
|
|
531
|
+
? payload.attachments
|
|
532
|
+
: [];
|
|
533
|
+
const hasAttachments = rawAttachments.length > 0;
|
|
534
|
+
// Allow through if there's text OR attachments; both-empty messages are silently dropped
|
|
535
|
+
if (!strippedText && !hasAttachments) {
|
|
275
536
|
this.acknowledgeImmediately(messageId, seq);
|
|
276
537
|
return;
|
|
277
538
|
}
|
|
278
539
|
const mentions = mentionedAll ? ['all'] : (this._aid ? [this._aid] : []);
|
|
540
|
+
// Process attachments
|
|
541
|
+
let finalText = strippedText;
|
|
542
|
+
if (hasAttachments && this.client) {
|
|
543
|
+
const fileParts = [];
|
|
544
|
+
for (const att of rawAttachments) {
|
|
545
|
+
const filePath = await this.downloadAttachment(att, groupId);
|
|
546
|
+
if (filePath) {
|
|
547
|
+
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
548
|
+
fileParts.push(`[文件: ${name} → ${filePath}]`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (fileParts.length > 0) {
|
|
552
|
+
const parts = [];
|
|
553
|
+
if (strippedText)
|
|
554
|
+
parts.push(strippedText);
|
|
555
|
+
parts.push(...fileParts);
|
|
556
|
+
parts.push('请使用 Read 工具读取文件内容。');
|
|
557
|
+
finalText = parts.join('\n\n');
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const peerInfo = await this.fetchPeerInfo(senderAid);
|
|
561
|
+
const shortAid = this.getShortAid(senderAid);
|
|
562
|
+
const displayName = peerInfo.name || shortAid;
|
|
279
563
|
this.dispatchMessage({
|
|
280
564
|
channelId: groupId,
|
|
281
565
|
userId: senderAid,
|
|
282
|
-
peerName:
|
|
283
|
-
|
|
566
|
+
peerName: displayName || undefined,
|
|
567
|
+
peerType: peerInfo.type || 'unknown',
|
|
568
|
+
text: finalText,
|
|
284
569
|
chatType: 'group',
|
|
285
570
|
messageId,
|
|
286
571
|
seq,
|
|
@@ -316,6 +601,7 @@ export class AUNChannel {
|
|
|
316
601
|
chatType: event.chatType,
|
|
317
602
|
peerId: event.userId || event.channelId || '',
|
|
318
603
|
peerName: event.peerName,
|
|
604
|
+
peerType: event.peerType,
|
|
319
605
|
messageId: event.messageId,
|
|
320
606
|
threadId: event.taskId,
|
|
321
607
|
mentions: mentionObjects,
|
|
@@ -331,6 +617,8 @@ export class AUNChannel {
|
|
|
331
617
|
if (state === 'connected') {
|
|
332
618
|
this.connected = true;
|
|
333
619
|
this.reconnectAttempt = 0;
|
|
620
|
+
this.lastReconnectLogTime = 0;
|
|
621
|
+
this.lastReconnectLogAttempt = 0;
|
|
334
622
|
logger.info('[AUN] Connected');
|
|
335
623
|
}
|
|
336
624
|
else if (state === 'disconnected') {
|
|
@@ -338,21 +626,50 @@ export class AUNChannel {
|
|
|
338
626
|
logger.warn(`[AUN] Disconnected: ${data.error ?? 'unknown'}`);
|
|
339
627
|
}
|
|
340
628
|
else if (state === 'reconnecting') {
|
|
341
|
-
|
|
629
|
+
const attempt = data.attempt ?? 0;
|
|
630
|
+
const now = Date.now();
|
|
631
|
+
// Throttled logging: first attempt, every N attempts, or every M seconds
|
|
632
|
+
const isFirst = attempt <= 1;
|
|
633
|
+
const isStep = attempt - this.lastReconnectLogAttempt >= AUNChannel.RECONNECT_LOG_STEP;
|
|
634
|
+
const isInterval = now - this.lastReconnectLogTime >= AUNChannel.RECONNECT_LOG_INTERVAL;
|
|
635
|
+
if (isFirst || isStep || isInterval) {
|
|
636
|
+
const suppressed = attempt - this.lastReconnectLogAttempt - 1;
|
|
637
|
+
const suffix = suppressed > 0 ? `, ${suppressed} suppressed since last log` : '';
|
|
638
|
+
logger.info(`[AUN] SDK reconnecting (attempt ${attempt}${suffix})`);
|
|
639
|
+
this.lastReconnectLogTime = now;
|
|
640
|
+
this.lastReconnectLogAttempt = attempt;
|
|
641
|
+
this.trace('IN', 'connection.state', data);
|
|
642
|
+
}
|
|
643
|
+
// Detect runaway SDK reconnect loop: force disconnect and use TS-layer backoff
|
|
644
|
+
if (attempt >= AUNChannel.SDK_RECONNECT_GIVEUP && !this.intentionalDisconnect) {
|
|
645
|
+
logger.warn(`[AUN] SDK reconnect stuck at attempt ${attempt}, forcing TS-layer reconnect with backoff`);
|
|
646
|
+
this.connected = false;
|
|
647
|
+
if (this.client) {
|
|
648
|
+
this.client.close().catch(() => { });
|
|
649
|
+
this.client = null;
|
|
650
|
+
}
|
|
651
|
+
this.scheduleReconnect();
|
|
652
|
+
}
|
|
342
653
|
}
|
|
343
654
|
else if (state === 'terminal_failed') {
|
|
344
655
|
this.connected = false;
|
|
345
|
-
|
|
346
|
-
|
|
656
|
+
const reason = data.reason ?? '';
|
|
657
|
+
logger.error(`[AUN] Terminal failure: ${data.error ?? 'unknown'}${reason ? ` (${reason})` : ''}`);
|
|
347
658
|
if (!this.intentionalDisconnect) {
|
|
348
659
|
this.scheduleReconnect();
|
|
349
660
|
}
|
|
350
661
|
}
|
|
351
662
|
}
|
|
352
663
|
// ── Public API (same interface as before) ───────────────────
|
|
664
|
+
onProjectPathRequest(provider) {
|
|
665
|
+
this.projectPathProvider = provider;
|
|
666
|
+
}
|
|
353
667
|
onMessage(handler) {
|
|
354
668
|
this.messageHandler = handler;
|
|
355
669
|
}
|
|
670
|
+
onRecall(handler) {
|
|
671
|
+
this.recallHandler = handler;
|
|
672
|
+
}
|
|
356
673
|
async sendMessage(channelId, text, context) {
|
|
357
674
|
if (!this.connected || !this.client) {
|
|
358
675
|
logger.warn('[AUN] Cannot send: not connected');
|
|
@@ -374,9 +691,16 @@ export class AUNChannel {
|
|
|
374
691
|
finalText = `@${context.peerId} ` + finalText;
|
|
375
692
|
}
|
|
376
693
|
}
|
|
377
|
-
const
|
|
694
|
+
const payload = { type: 'text', text: finalText };
|
|
378
695
|
if (context?.threadId)
|
|
379
|
-
|
|
696
|
+
payload.thread_id = context.threadId;
|
|
697
|
+
const params = { payload, encrypt: true };
|
|
698
|
+
// Multi-instance routing: channelId may be "aid:device_id:slot_id"
|
|
699
|
+
const colonIdx = channelId.indexOf(':');
|
|
700
|
+
const targetAid = colonIdx > 0 ? channelId.substring(0, colonIdx) : channelId;
|
|
701
|
+
if (colonIdx > 0) {
|
|
702
|
+
params.payload.chat_id = channelId;
|
|
703
|
+
}
|
|
380
704
|
try {
|
|
381
705
|
if (this.isGroupId(channelId)) {
|
|
382
706
|
params.group_id = channelId;
|
|
@@ -384,7 +708,7 @@ export class AUNChannel {
|
|
|
384
708
|
await this.client.call('group.send', params);
|
|
385
709
|
}
|
|
386
710
|
else {
|
|
387
|
-
params.to =
|
|
711
|
+
params.to = targetAid;
|
|
388
712
|
this.trace('OUT', 'message.send', params);
|
|
389
713
|
await this.client.call('message.send', params);
|
|
390
714
|
}
|
|
@@ -394,6 +718,103 @@ export class AUNChannel {
|
|
|
394
718
|
logger.error(`[AUN] Send failed to ${channelId}: ${e}`);
|
|
395
719
|
}
|
|
396
720
|
}
|
|
721
|
+
async sendFile(channelId, filePath, context) {
|
|
722
|
+
if (!this.connected || !this.client) {
|
|
723
|
+
logger.warn('[AUN] Cannot sendFile: not connected');
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
const absPath = path.resolve(filePath);
|
|
727
|
+
if (!fs.existsSync(absPath)) {
|
|
728
|
+
logger.warn(`[AUN] sendFile: file not found: ${absPath}`);
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const stat = fs.statSync(absPath);
|
|
732
|
+
if (stat.size === 0) {
|
|
733
|
+
logger.warn('[AUN] sendFile: file is empty');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (stat.size > 10 * 1024 * 1024) {
|
|
737
|
+
logger.warn(`[AUN] sendFile: file too large (${formatSize(stat.size)}, max 10 MB)`);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const filename = path.basename(absPath);
|
|
741
|
+
const fileData = fs.readFileSync(absPath);
|
|
742
|
+
const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
|
|
743
|
+
const contentType = guessMime(filename);
|
|
744
|
+
const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
|
|
745
|
+
try {
|
|
746
|
+
// Upload to storage
|
|
747
|
+
if (stat.size <= 64 * 1024) {
|
|
748
|
+
// Inline upload for small files (≤64KB)
|
|
749
|
+
await this.client.call('storage.put_object', {
|
|
750
|
+
object_key: objectKey,
|
|
751
|
+
content: fileData.toString('base64'),
|
|
752
|
+
content_type: contentType,
|
|
753
|
+
is_private: false,
|
|
754
|
+
overwrite: true,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
// Ticket upload for large files
|
|
759
|
+
const session = await this.client.call('storage.create_upload_session', {
|
|
760
|
+
object_key: objectKey,
|
|
761
|
+
size_bytes: stat.size,
|
|
762
|
+
content_type: contentType,
|
|
763
|
+
});
|
|
764
|
+
const uploadUrl = session.upload_url;
|
|
765
|
+
if (!uploadUrl)
|
|
766
|
+
throw new Error('No upload_url in session response');
|
|
767
|
+
const uploadResp = await fetch(uploadUrl, { method: 'PUT', body: fileData });
|
|
768
|
+
if (!uploadResp.ok)
|
|
769
|
+
throw new Error(`HTTP upload failed: ${uploadResp.status}`);
|
|
770
|
+
await this.client.call('storage.complete_upload', {
|
|
771
|
+
object_key: objectKey,
|
|
772
|
+
sha256,
|
|
773
|
+
content_type: contentType,
|
|
774
|
+
is_private: false,
|
|
775
|
+
size_bytes: stat.size,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
// Send message with attachment
|
|
779
|
+
const attachment = {
|
|
780
|
+
owner_aid: this._aid || '',
|
|
781
|
+
object_key: objectKey,
|
|
782
|
+
filename,
|
|
783
|
+
size_bytes: stat.size,
|
|
784
|
+
sha256,
|
|
785
|
+
content_type: contentType,
|
|
786
|
+
};
|
|
787
|
+
const filePayload = {
|
|
788
|
+
type: 'file',
|
|
789
|
+
text: `📎 ${filename} (${formatSize(stat.size)})`,
|
|
790
|
+
attachments: [attachment],
|
|
791
|
+
};
|
|
792
|
+
if (context?.threadId)
|
|
793
|
+
filePayload.thread_id = context.threadId;
|
|
794
|
+
const params = { payload: filePayload, encrypt: true };
|
|
795
|
+
// Multi-instance routing
|
|
796
|
+
const fileColonIdx = channelId.indexOf(':');
|
|
797
|
+
const fileTargetAid = fileColonIdx > 0 ? channelId.substring(0, fileColonIdx) : channelId;
|
|
798
|
+
if (fileColonIdx > 0) {
|
|
799
|
+
params.payload.chat_id = channelId;
|
|
800
|
+
}
|
|
801
|
+
if (this.isGroupId(channelId)) {
|
|
802
|
+
params.group_id = channelId;
|
|
803
|
+
this.trace('OUT', 'group.send.file', params);
|
|
804
|
+
await this.client.call('group.send', params);
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
params.to = fileTargetAid;
|
|
808
|
+
this.trace('OUT', 'message.send.file', params);
|
|
809
|
+
await this.client.call('message.send', params);
|
|
810
|
+
}
|
|
811
|
+
logger.info(`[AUN] File sent: ${filename} (${formatSize(stat.size)}) → ${channelId}`);
|
|
812
|
+
}
|
|
813
|
+
catch (e) {
|
|
814
|
+
this.trace('OUT', 'sendFile.error', { channelId, filePath, error: String(e) });
|
|
815
|
+
logger.error(`[AUN] sendFile failed for ${channelId}: ${e}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
397
818
|
acknowledge(messageId) {
|
|
398
819
|
// Gateway auto-delivery-ack is sufficient; skip explicit message.ack RPC
|
|
399
820
|
// to avoid duplicate "已送达" at the sender CLI
|
|
@@ -404,18 +825,28 @@ export class AUNChannel {
|
|
|
404
825
|
this.sentCount.delete(channelId); // 新任务开始,重置计数
|
|
405
826
|
if (!this.client || !this.connected)
|
|
406
827
|
return;
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
828
|
+
const eventMap = {
|
|
829
|
+
start: 'task.started',
|
|
830
|
+
done: 'task.completed',
|
|
831
|
+
interrupted: 'task.interrupted',
|
|
832
|
+
error: 'task.error',
|
|
833
|
+
timeout: 'task.timeout',
|
|
412
834
|
};
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
835
|
+
const payload = {
|
|
836
|
+
type: 'event',
|
|
837
|
+
event: eventMap[status] ?? `task.${status}`,
|
|
838
|
+
data: { session_id: sessionId },
|
|
839
|
+
severity: status === 'error' || status === 'timeout' ? 'error' : 'info',
|
|
416
840
|
};
|
|
417
841
|
if (context?.threadId)
|
|
418
|
-
|
|
842
|
+
payload.thread_id = context.threadId;
|
|
843
|
+
const params = { payload, encrypt: true };
|
|
844
|
+
// Multi-instance routing
|
|
845
|
+
const statusColonIdx = channelId.indexOf(':');
|
|
846
|
+
const statusTargetAid = statusColonIdx > 0 ? channelId.substring(0, statusColonIdx) : channelId;
|
|
847
|
+
if (statusColonIdx > 0) {
|
|
848
|
+
payload.chat_id = channelId;
|
|
849
|
+
}
|
|
419
850
|
if (this.isGroupId(channelId)) {
|
|
420
851
|
params.group_id = channelId;
|
|
421
852
|
this.trace('OUT', 'group.send.status', params);
|
|
@@ -424,7 +855,7 @@ export class AUNChannel {
|
|
|
424
855
|
});
|
|
425
856
|
}
|
|
426
857
|
else {
|
|
427
|
-
params.to =
|
|
858
|
+
params.to = statusTargetAid;
|
|
428
859
|
this.trace('OUT', 'message.send.status', params);
|
|
429
860
|
this.client.call('message.send', params).catch(e => {
|
|
430
861
|
logger.debug(`[AUN] Processing status failed: ${e}`);
|
|
@@ -444,8 +875,14 @@ export class AUNChannel {
|
|
|
444
875
|
catch {
|
|
445
876
|
payloadObj = { text: payload };
|
|
446
877
|
}
|
|
878
|
+
// Multi-instance routing
|
|
879
|
+
const customColonIdx = channelId.indexOf(':');
|
|
880
|
+
const customTargetAid = customColonIdx > 0 ? channelId.substring(0, customColonIdx) : channelId;
|
|
881
|
+
if (customColonIdx > 0) {
|
|
882
|
+
payloadObj.chat_id = channelId;
|
|
883
|
+
}
|
|
447
884
|
const sendParams = {
|
|
448
|
-
to:
|
|
885
|
+
to: customTargetAid, payload: payloadObj,
|
|
449
886
|
encrypt: true,
|
|
450
887
|
};
|
|
451
888
|
this.trace('OUT', 'message.send.custom', sendParams);
|
|
@@ -532,6 +969,37 @@ export class AUNChannel {
|
|
|
532
969
|
maxAttempts: AUNChannel.RECONNECT_DELAYS.length,
|
|
533
970
|
};
|
|
534
971
|
}
|
|
972
|
+
async fetchPeerInfo(aid) {
|
|
973
|
+
const cached = this.peerInfoCache.get(aid);
|
|
974
|
+
if (cached !== undefined)
|
|
975
|
+
return cached;
|
|
976
|
+
if (!this.client)
|
|
977
|
+
return { type: null };
|
|
978
|
+
try {
|
|
979
|
+
const md = await this.client.auth.downloadAgentMd(aid);
|
|
980
|
+
const typeMatch = md.match(/^type:\s*["']?(\w+)["']?/m);
|
|
981
|
+
const nameMatch = md.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
982
|
+
const type = typeMatch?.[1] === 'human' ? 'human' : 'ai';
|
|
983
|
+
const name = nameMatch?.[1]?.trim() || undefined;
|
|
984
|
+
const info = { type, name };
|
|
985
|
+
this.peerInfoCache.set(aid, info);
|
|
986
|
+
setTimeout(() => this.peerInfoCache.delete(aid), 30 * 60 * 1000);
|
|
987
|
+
return info;
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
return { type: null }; // no agent.md → unknown
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async uploadAgentMd(content) {
|
|
994
|
+
if (!this.client)
|
|
995
|
+
throw new Error('not connected');
|
|
996
|
+
await this.client.auth.uploadAgentMd(content);
|
|
997
|
+
}
|
|
998
|
+
async downloadAgentMd(aid) {
|
|
999
|
+
if (!this.client)
|
|
1000
|
+
throw new Error('not connected');
|
|
1001
|
+
return this.client.auth.downloadAgentMd(aid);
|
|
1002
|
+
}
|
|
535
1003
|
}
|
|
536
1004
|
// Plugin implementation
|
|
537
1005
|
export class AUNChannelPlugin {
|
|
@@ -554,7 +1022,6 @@ export class AUNChannelPlugin {
|
|
|
554
1022
|
const channel = new AUNChannel({
|
|
555
1023
|
aid: inst.aid,
|
|
556
1024
|
keystorePath: inst.keystorePath,
|
|
557
|
-
gatewayPort: inst.gatewayPort,
|
|
558
1025
|
gatewayUrl: inst.gatewayUrl,
|
|
559
1026
|
accessToken: inst.accessToken,
|
|
560
1027
|
flushDelay: inst.flushDelay,
|
|
@@ -564,19 +1031,23 @@ export class AUNChannelPlugin {
|
|
|
564
1031
|
const adapter = {
|
|
565
1032
|
channelName: inst.name,
|
|
566
1033
|
sendText: (id, text, context) => channel.sendMessage(id, text, context),
|
|
1034
|
+
sendFile: (id, filePath, context) => channel.sendFile(id, filePath, context),
|
|
567
1035
|
acknowledge: (messageId) => { channel.acknowledge(messageId); return Promise.resolve(); },
|
|
568
1036
|
sendProcessingStatus: (id, status, sessionId, context) => channel.sendProcessingStatus(id, status, sessionId, context),
|
|
569
1037
|
sendCustomPayload: (id, payload) => channel.sendCustomPayload(id, payload),
|
|
1038
|
+
uploadAgentMd: (content) => channel.uploadAgentMd(content),
|
|
1039
|
+
downloadAgentMd: (aid) => channel.downloadAgentMd(aid),
|
|
1040
|
+
_selfAid: () => channel.getStatus().aid,
|
|
570
1041
|
};
|
|
571
1042
|
const policy = {
|
|
572
|
-
canSwitchProject: (chatType, identity) => identity === 'owner',
|
|
573
|
-
canListProjects: (chatType, identity) => identity === 'owner',
|
|
1043
|
+
canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
1044
|
+
canListProjects: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
574
1045
|
canCreateSession: (chatType, identity) => true,
|
|
575
1046
|
canDeleteSession: (chatType, identity) => true,
|
|
576
|
-
canImportCliSession: (chatType, identity) => identity === 'owner',
|
|
1047
|
+
canImportCliSession: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
577
1048
|
messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
|
|
578
1049
|
showMiddleResult: (chatType, identity) => {
|
|
579
|
-
const mode = inst.
|
|
1050
|
+
const mode = getChannelShowActivities(config, inst.name);
|
|
580
1051
|
if (mode === 'none')
|
|
581
1052
|
return false;
|
|
582
1053
|
if (mode === 'dm-only')
|
|
@@ -586,7 +1057,7 @@ export class AUNChannelPlugin {
|
|
|
586
1057
|
return true;
|
|
587
1058
|
},
|
|
588
1059
|
showIdleMonitor: (chatType, identity) => {
|
|
589
|
-
const mode = inst.
|
|
1060
|
+
const mode = getChannelShowActivities(config, inst.name);
|
|
590
1061
|
if (mode === 'none')
|
|
591
1062
|
return false;
|
|
592
1063
|
if (mode === 'dm-only')
|
|
@@ -609,6 +1080,7 @@ export class AUNChannelPlugin {
|
|
|
609
1080
|
options,
|
|
610
1081
|
connect: () => channel.connect(),
|
|
611
1082
|
disconnect: () => channel.disconnect(),
|
|
1083
|
+
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
612
1084
|
});
|
|
613
1085
|
}
|
|
614
1086
|
return result;
|