evolclaw 2.3.0 → 2.4.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/dist/channels/aun.js +54 -29
- package/dist/cli.js +101 -8
- package/dist/core/command-handler.js +57 -42
- package/dist/core/message/message-bridge.js +3 -2
- package/dist/core/message/message-processor.js +37 -31
- package/dist/core/session/session-manager.js +4 -0
- package/dist/index.js +7 -8
- package/package.json +1 -1
package/dist/channels/aun.js
CHANGED
|
@@ -16,6 +16,10 @@ export class AUNChannel {
|
|
|
16
16
|
const line = JSON.stringify({ ts: localTimestamp(), dir, event, data });
|
|
17
17
|
this.traceStream.write(line + '\n');
|
|
18
18
|
}
|
|
19
|
+
/** 判断 channelId 是否为群组 ID(g-xxx.agentid.pub 或 grp_ 前缀) */
|
|
20
|
+
isGroupId(id) {
|
|
21
|
+
return id.startsWith('grp_') || (id.startsWith('g-') && id.includes('.'));
|
|
22
|
+
}
|
|
19
23
|
getShortAid(aid) {
|
|
20
24
|
if (!aid)
|
|
21
25
|
return undefined;
|
|
@@ -48,16 +52,11 @@ export class AUNChannel {
|
|
|
48
52
|
result = result.replace(/(^|\s)@all(?=$|\s|[.,!?;:,。!?;:]|[\u4e00-\u9fff])/gi, '$1');
|
|
49
53
|
return result.replace(/[ \t]+/g, ' ').trim();
|
|
50
54
|
}
|
|
51
|
-
buildGroupReplyContext(taskId, senderAid
|
|
55
|
+
buildGroupReplyContext(taskId, senderAid) {
|
|
52
56
|
const replyContext = {};
|
|
53
57
|
if (taskId)
|
|
54
58
|
replyContext.threadId = taskId;
|
|
55
|
-
|
|
56
|
-
replyContext.mentionUserIds = ['all'];
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
replyContext.mentionUserIds = [senderAid];
|
|
60
|
-
}
|
|
59
|
+
replyContext.peerId = senderAid;
|
|
61
60
|
return replyContext;
|
|
62
61
|
}
|
|
63
62
|
acknowledgeImmediately(messageId, seq) {
|
|
@@ -150,6 +149,18 @@ export class AUNChannel {
|
|
|
150
149
|
this.handleConnectionState(data);
|
|
151
150
|
});
|
|
152
151
|
// Authenticate
|
|
152
|
+
// Workaround: SDK 0.3.x _loadIdentityOrRaise doesn't set identity.aid from requested aid,
|
|
153
|
+
// causing gateway "missing aid" error. Patch to backfill aid on loaded identity.
|
|
154
|
+
const authFlow = this.client._auth;
|
|
155
|
+
if (authFlow && typeof authFlow._loadIdentityOrRaise === 'function') {
|
|
156
|
+
const origLoad = authFlow._loadIdentityOrRaise.bind(authFlow);
|
|
157
|
+
authFlow._loadIdentityOrRaise = (aid) => {
|
|
158
|
+
const identity = origLoad(aid);
|
|
159
|
+
if (identity && !identity.aid)
|
|
160
|
+
identity.aid = aid ?? authFlow._aid;
|
|
161
|
+
return identity;
|
|
162
|
+
};
|
|
163
|
+
}
|
|
153
164
|
let accessToken;
|
|
154
165
|
try {
|
|
155
166
|
logger.info(`[AUN] Authenticating as ${aidName}...`);
|
|
@@ -169,7 +180,8 @@ export class AUNChannel {
|
|
|
169
180
|
// Fallback: try direct token from env/config (legacy)
|
|
170
181
|
accessToken = this.config.accessToken || process.env.AUN_ACCESS_TOKEN || '';
|
|
171
182
|
if (!accessToken) {
|
|
172
|
-
logger.error(`[AUN] No accessToken fallback available,
|
|
183
|
+
logger.error(`[AUN] No accessToken fallback available, scheduling retry`);
|
|
184
|
+
this.scheduleReconnect();
|
|
173
185
|
return;
|
|
174
186
|
}
|
|
175
187
|
logger.warn(`[AUN] Using accessToken fallback`);
|
|
@@ -180,10 +192,22 @@ export class AUNChannel {
|
|
|
180
192
|
this._aid = this.client.aid ?? undefined;
|
|
181
193
|
this.connected = true;
|
|
182
194
|
this.reconnectAttempt = 0;
|
|
195
|
+
// Workaround: SDK e2ee uses _identity.cert for sender_cert_fingerprint;
|
|
196
|
+
// if cert is missing, it falls back to public key SPKI fingerprint which
|
|
197
|
+
// causes peer cert lookup failures. Backfill from keystore if needed.
|
|
198
|
+
const clientAny = this.client;
|
|
199
|
+
if (clientAny._identity && !clientAny._identity.cert) {
|
|
200
|
+
const cert = clientAny._keystore?.loadCert?.(aidName);
|
|
201
|
+
if (cert) {
|
|
202
|
+
clientAny._identity.cert = cert;
|
|
203
|
+
logger.info('[AUN] Backfilled identity.cert from keystore for e2ee fingerprint');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
183
206
|
logger.info(`[AUN] Connected as ${this._aid}`);
|
|
184
207
|
}
|
|
185
208
|
catch (e) {
|
|
186
209
|
logger.error(`[AUN] Connection failed: ${e}`);
|
|
210
|
+
this.scheduleReconnect();
|
|
187
211
|
return;
|
|
188
212
|
}
|
|
189
213
|
}
|
|
@@ -225,6 +249,10 @@ export class AUNChannel {
|
|
|
225
249
|
const taskId = msg.task_id;
|
|
226
250
|
const messageId = msg.message_id ?? '';
|
|
227
251
|
const seq = msg.seq;
|
|
252
|
+
// Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
|
|
253
|
+
const payloadMentions = Array.isArray(payload?.mentions)
|
|
254
|
+
? payload.mentions.filter((m) => typeof m === 'string')
|
|
255
|
+
: [];
|
|
228
256
|
logger.info(`[AUN][DIAG-GRP] full_msg=${JSON.stringify(msg).substring(0, 500)}`);
|
|
229
257
|
if (!groupId || !senderAid) {
|
|
230
258
|
this.acknowledgeImmediately(messageId, seq);
|
|
@@ -234,8 +262,10 @@ export class AUNChannel {
|
|
|
234
262
|
this.acknowledgeImmediately(messageId, seq);
|
|
235
263
|
return;
|
|
236
264
|
}
|
|
237
|
-
const mentionedSelf = this._aid
|
|
238
|
-
|
|
265
|
+
const mentionedSelf = this._aid
|
|
266
|
+
? (this.hasExplicitMention(text, this._aid) || payloadMentions.includes(this._aid))
|
|
267
|
+
: false;
|
|
268
|
+
const mentionedAll = this.hasExplicitMention(text, 'all') || payloadMentions.includes('all');
|
|
239
269
|
if (!mentionedSelf && !mentionedAll) {
|
|
240
270
|
this.acknowledgeImmediately(messageId, seq);
|
|
241
271
|
return;
|
|
@@ -256,7 +286,7 @@ export class AUNChannel {
|
|
|
256
286
|
seq,
|
|
257
287
|
taskId,
|
|
258
288
|
mentions,
|
|
259
|
-
replyContext: this.buildGroupReplyContext(taskId, senderAid
|
|
289
|
+
replyContext: this.buildGroupReplyContext(taskId, senderAid),
|
|
260
290
|
});
|
|
261
291
|
}
|
|
262
292
|
dispatchMessage(event) {
|
|
@@ -338,18 +368,17 @@ export class AUNChannel {
|
|
|
338
368
|
finalText = '最终回复\n' + text;
|
|
339
369
|
}
|
|
340
370
|
this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
|
|
341
|
-
//
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
finalText = mentionPrefix + finalText;
|
|
371
|
+
// 群聊 @ 兜底:提示词已告知 agent 要 @,但如果 agent 没写,系统自动补上
|
|
372
|
+
if (this.isGroupId(channelId) && context?.peerId) {
|
|
373
|
+
if (!finalText.includes(`@${context.peerId}`)) {
|
|
374
|
+
finalText = `@${context.peerId} ` + finalText;
|
|
375
|
+
}
|
|
347
376
|
}
|
|
348
377
|
const params = { payload: { text: finalText }, encrypt: true };
|
|
349
378
|
if (context?.threadId)
|
|
350
379
|
params.task_id = context.threadId;
|
|
351
380
|
try {
|
|
352
|
-
if (
|
|
381
|
+
if (this.isGroupId(channelId)) {
|
|
353
382
|
params.group_id = channelId;
|
|
354
383
|
this.trace('OUT', 'group.send', params);
|
|
355
384
|
await this.client.call('group.send', params);
|
|
@@ -366,13 +395,9 @@ export class AUNChannel {
|
|
|
366
395
|
}
|
|
367
396
|
}
|
|
368
397
|
acknowledge(messageId) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
logger.debug(`[AUN] Ack failed: ${e}`);
|
|
373
|
-
});
|
|
374
|
-
this.messageSeqMap.delete(messageId);
|
|
375
|
-
}
|
|
398
|
+
// Gateway auto-delivery-ack is sufficient; skip explicit message.ack RPC
|
|
399
|
+
// to avoid duplicate "已送达" at the sender CLI
|
|
400
|
+
this.messageSeqMap.delete(messageId);
|
|
376
401
|
}
|
|
377
402
|
sendProcessingStatus(channelId, status, sessionId, context) {
|
|
378
403
|
if (status === 'start')
|
|
@@ -387,11 +412,11 @@ export class AUNChannel {
|
|
|
387
412
|
};
|
|
388
413
|
const params = {
|
|
389
414
|
payload,
|
|
390
|
-
encrypt: true,
|
|
415
|
+
encrypt: true,
|
|
391
416
|
};
|
|
392
417
|
if (context?.threadId)
|
|
393
418
|
params.task_id = context.threadId;
|
|
394
|
-
if (
|
|
419
|
+
if (this.isGroupId(channelId)) {
|
|
395
420
|
params.group_id = channelId;
|
|
396
421
|
this.trace('OUT', 'group.send.status', params);
|
|
397
422
|
this.client.call('group.send', params).catch(e => {
|
|
@@ -421,7 +446,7 @@ export class AUNChannel {
|
|
|
421
446
|
}
|
|
422
447
|
const sendParams = {
|
|
423
448
|
to: channelId, payload: payloadObj,
|
|
424
|
-
encrypt: true,
|
|
449
|
+
encrypt: true,
|
|
425
450
|
};
|
|
426
451
|
this.trace('OUT', 'message.send.custom', sendParams);
|
|
427
452
|
this.client.call('message.send', sendParams).catch(e => {
|
|
@@ -549,7 +574,7 @@ export class AUNChannelPlugin {
|
|
|
549
574
|
canCreateSession: (chatType, identity) => true,
|
|
550
575
|
canDeleteSession: (chatType, identity) => true,
|
|
551
576
|
canImportCliSession: (chatType, identity) => identity === 'owner',
|
|
552
|
-
messagePrefix: () => '',
|
|
577
|
+
messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
|
|
553
578
|
showMiddleResult: (chatType, identity) => {
|
|
554
579
|
const mode = inst.showActivities ?? config.showActivities ?? 'all';
|
|
555
580
|
if (mode === 'none')
|
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { promisify } from 'util';
|
|
|
6
6
|
import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from './paths.js';
|
|
7
7
|
import { loadConfig, validateConfigIntegrity, resolveAnthropicConfig } from './config.js';
|
|
8
8
|
import { migrateProject } from './utils/migrate-project.js';
|
|
9
|
+
import readline from 'readline';
|
|
9
10
|
import { cmdInit } from './utils/init.js';
|
|
10
11
|
import { ipcQuery } from './ipc.js';
|
|
11
12
|
import { cmdInitWechat, cmdInitFeishu, cmdInitAun } from './utils/init-channel.js';
|
|
@@ -568,22 +569,111 @@ async function cmdStatus() {
|
|
|
568
569
|
console.log(' (no log file yet)');
|
|
569
570
|
}
|
|
570
571
|
}
|
|
571
|
-
|
|
572
|
+
// Log line pattern: [timestamp] [LEVEL] [Module?] message
|
|
573
|
+
const LOG_RE = /^(\[[^\]]+\]) (\[(?:INFO|WARN|ERROR|DEBUG)\]) ((?:\[[^\]]+\] )*)(.*)$/;
|
|
574
|
+
const MAX_MSG = 200; // truncate long messages
|
|
575
|
+
function makeColors(enabled) {
|
|
576
|
+
const e = (code) => enabled ? code : '';
|
|
577
|
+
return {
|
|
578
|
+
reset: e('\x1b[0m'), dim: e('\x1b[2m'), bold: e('\x1b[1m'),
|
|
579
|
+
red: e('\x1b[31m'), yellow: e('\x1b[33m'), cyan: e('\x1b[36m'),
|
|
580
|
+
magenta: e('\x1b[35m'), gray: e('\x1b[90m'),
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function renderLogLine(line, opts) {
|
|
584
|
+
const m = line.match(LOG_RE);
|
|
585
|
+
if (!m)
|
|
586
|
+
return line; // passthrough non-standard lines (stack traces etc.)
|
|
587
|
+
const [, ts, levelTag, modulePart, msg] = m;
|
|
588
|
+
const level = levelTag.slice(1, -1); // strip brackets
|
|
589
|
+
// Level filter
|
|
590
|
+
if (opts.level) {
|
|
591
|
+
const want = opts.level.toUpperCase();
|
|
592
|
+
if (want === 'ERROR' && level !== 'ERROR')
|
|
593
|
+
return null;
|
|
594
|
+
if (want === 'WARN' && level !== 'WARN' && level !== 'ERROR')
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
// Module filter (case-insensitive substring match)
|
|
598
|
+
if (opts.module) {
|
|
599
|
+
const mod = modulePart.toLowerCase();
|
|
600
|
+
if (!mod.includes(opts.module.toLowerCase()))
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
// Truncate long messages (always, regardless of color)
|
|
604
|
+
const truncated = msg.length > MAX_MSG ? msg.slice(0, MAX_MSG) + '…' : msg;
|
|
605
|
+
const C = makeColors(opts.color);
|
|
606
|
+
// Color by level
|
|
607
|
+
const levelColor = level === 'ERROR' ? C.red : level === 'WARN' ? C.yellow : level === 'DEBUG' ? C.gray : '';
|
|
608
|
+
// Highlight user messages: [channel] channelId: text
|
|
609
|
+
const isUserMsg = modulePart && /^\S+: .+$/.test(truncated);
|
|
610
|
+
const renderedMsg = isUserMsg
|
|
611
|
+
? C.cyan + truncated + C.reset
|
|
612
|
+
: levelColor + truncated + C.reset;
|
|
613
|
+
return (C.dim + ts + C.reset + ' ' +
|
|
614
|
+
levelColor + C.bold + levelTag + C.reset + ' ' +
|
|
615
|
+
C.magenta + modulePart.trimEnd() + C.reset +
|
|
616
|
+
(modulePart ? ' ' : '') +
|
|
617
|
+
renderedMsg);
|
|
618
|
+
}
|
|
619
|
+
function cmdLogs(args) {
|
|
620
|
+
const raw = args.includes('--raw');
|
|
621
|
+
const noColor = args.includes('--no-color');
|
|
622
|
+
const levelIdx = args.indexOf('--level');
|
|
623
|
+
const moduleIdx = args.indexOf('--module');
|
|
624
|
+
const level = levelIdx !== -1 ? args[levelIdx + 1] : undefined;
|
|
625
|
+
const module = moduleIdx !== -1 ? args[moduleIdx + 1] : undefined;
|
|
572
626
|
const p = resolvePaths();
|
|
573
627
|
const mainLog = path.join(p.logs, 'evolclaw.log');
|
|
574
628
|
if (!fs.existsSync(mainLog)) {
|
|
575
629
|
console.log(`❌ Log file not found: ${mainLog}`);
|
|
576
630
|
process.exit(1);
|
|
577
631
|
}
|
|
632
|
+
if (raw) {
|
|
633
|
+
// Raw mode: plain tail -f, no rendering at all
|
|
634
|
+
if (platform.isWindows) {
|
|
635
|
+
const tail = platform.tailFile(mainLog);
|
|
636
|
+
platform.onShutdown(() => tail.abort());
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
const child = spawn('tail', ['-f', '-n', '50', mainLog], { stdio: 'inherit' });
|
|
640
|
+
child.on('exit', (code) => process.exit(code || 0));
|
|
641
|
+
}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
// Rendered mode: always filter+truncate, color depends on TTY
|
|
645
|
+
const useColor = !noColor && !!process.stdout.isTTY;
|
|
646
|
+
const opts = { level, module, color: useColor };
|
|
647
|
+
function processLine(line) {
|
|
648
|
+
const rendered = renderLogLine(line, opts);
|
|
649
|
+
if (rendered !== null)
|
|
650
|
+
process.stdout.write(rendered + '\n');
|
|
651
|
+
}
|
|
578
652
|
if (platform.isWindows) {
|
|
579
|
-
// Windows:
|
|
580
|
-
const
|
|
581
|
-
|
|
653
|
+
// Windows: read existing content + watch
|
|
654
|
+
const existing = fs.readFileSync(mainLog, 'utf-8').split('\n').slice(-50);
|
|
655
|
+
existing.forEach(processLine);
|
|
656
|
+
let size = fs.statSync(mainLog).size;
|
|
657
|
+
const watcher = fs.watch(mainLog, () => {
|
|
658
|
+
const newSize = fs.statSync(mainLog).size;
|
|
659
|
+
if (newSize <= size)
|
|
660
|
+
return;
|
|
661
|
+
const buf = Buffer.alloc(newSize - size);
|
|
662
|
+
const fd = fs.openSync(mainLog, 'r');
|
|
663
|
+
fs.readSync(fd, buf, 0, buf.length, size);
|
|
664
|
+
fs.closeSync(fd);
|
|
665
|
+
size = newSize;
|
|
666
|
+
buf.toString().split('\n').forEach(l => l && processLine(l));
|
|
667
|
+
});
|
|
668
|
+
platform.onShutdown(() => watcher.close());
|
|
582
669
|
}
|
|
583
670
|
else {
|
|
584
|
-
// Unix:
|
|
585
|
-
const child = spawn('tail', ['-f',
|
|
671
|
+
// Unix: spawn tail -f, pipe through renderer
|
|
672
|
+
const child = spawn('tail', ['-f', '-n', '50', mainLog]);
|
|
673
|
+
const rl = readline.createInterface({ input: child.stdout });
|
|
674
|
+
rl.on('line', processLine);
|
|
586
675
|
child.on('exit', (code) => process.exit(code || 0));
|
|
676
|
+
platform.onShutdown(() => { child.kill(); });
|
|
587
677
|
}
|
|
588
678
|
}
|
|
589
679
|
/**
|
|
@@ -1201,7 +1291,7 @@ export async function main(args) {
|
|
|
1201
1291
|
await cmdStatus();
|
|
1202
1292
|
break;
|
|
1203
1293
|
case 'logs':
|
|
1204
|
-
cmdLogs();
|
|
1294
|
+
cmdLogs(args.slice(1));
|
|
1205
1295
|
break;
|
|
1206
1296
|
case 'restart-monitor':
|
|
1207
1297
|
await cmdRestartMonitor();
|
|
@@ -1227,7 +1317,10 @@ Commands:
|
|
|
1227
1317
|
stop 停止服务
|
|
1228
1318
|
restart 重启服务
|
|
1229
1319
|
status 查看状态
|
|
1230
|
-
logs 查看日志 (tail -f)
|
|
1320
|
+
logs 查看日志 (tail -f, 着色渲染)
|
|
1321
|
+
--level error|warn 只显示指定级别及以上
|
|
1322
|
+
--module <name> 只显示指定模块(如 feishu、AgentRunner)
|
|
1323
|
+
--raw 原始输出,不着色
|
|
1231
1324
|
tui 启动 AUN TUI 客户端
|
|
1232
1325
|
diagnose 诊断启动环境(配置、数据库、进程)
|
|
1233
1326
|
mv <old> <new> 迁移项目目录(保留 Claude/Codex/EvolClaw 会话)
|
|
@@ -324,15 +324,25 @@ export class CommandHandler {
|
|
|
324
324
|
* 返回结构化命令菜单(供 menu.query 使用)
|
|
325
325
|
* admin 看到全部命令分组,guest 仅看到用户级命令
|
|
326
326
|
*/
|
|
327
|
-
getMenuItems(isAdmin) {
|
|
327
|
+
getMenuItems(isAdmin, chatType = 'private') {
|
|
328
328
|
const items = [];
|
|
329
|
+
if (!isAdmin && chatType === 'group') {
|
|
330
|
+
return [
|
|
331
|
+
{
|
|
332
|
+
group: '其他',
|
|
333
|
+
commands: [
|
|
334
|
+
{ cmd: '/status', label: '显示会话状态' },
|
|
335
|
+
{ cmd: '/help', label: '显示帮助信息' },
|
|
336
|
+
]
|
|
337
|
+
}
|
|
338
|
+
];
|
|
339
|
+
}
|
|
329
340
|
if (isAdmin) {
|
|
330
341
|
items.push({
|
|
331
342
|
group: '项目管理',
|
|
332
343
|
commands: [
|
|
333
344
|
{ cmd: '/pwd', label: '显示当前项目路径' },
|
|
334
|
-
{ cmd: '/
|
|
335
|
-
{ cmd: '/p', args: '<name|path>', label: '切换项目' },
|
|
345
|
+
{ cmd: '/p', args: '[name|path]', label: '列出或切换项目' },
|
|
336
346
|
{ cmd: '/bind', args: '<path>', label: '绑定新项目目录' },
|
|
337
347
|
]
|
|
338
348
|
});
|
|
@@ -340,15 +350,12 @@ export class CommandHandler {
|
|
|
340
350
|
items.push({
|
|
341
351
|
group: '会话管理',
|
|
342
352
|
commands: [
|
|
343
|
-
{ cmd: '/new', args: '[name]', label: '
|
|
344
|
-
{ cmd: '/
|
|
345
|
-
{ cmd: '/slist', args: 'cli', label: '列出 CLI 会话(未导入的)' },
|
|
346
|
-
{ cmd: '/s', args: '<name|index|uuid>', label: '切换到指定会话' },
|
|
353
|
+
{ cmd: '/new', args: '[name]', label: '创建新会话(清空历史请用此命令)' },
|
|
354
|
+
{ cmd: '/s', args: '[cli|name|index|uuid]', label: '列出或切换会话(cli 查看未导入的 CLI 会话)' },
|
|
347
355
|
{ cmd: '/name', args: '<name>', label: '重命名当前会话' },
|
|
348
356
|
{ cmd: '/del', args: '<name>', label: '删除指定会话' },
|
|
349
357
|
...(isAdmin ? [
|
|
350
358
|
{ cmd: '/fork', args: '[name]', label: '分支当前会话' },
|
|
351
|
-
{ cmd: '/clear', label: '清空会话对话历史' },
|
|
352
359
|
{ cmd: '/compact', label: '压缩会话上下文' },
|
|
353
360
|
] : []),
|
|
354
361
|
]
|
|
@@ -374,9 +381,7 @@ export class CommandHandler {
|
|
|
374
381
|
{ cmd: '/status', label: '显示会话状态' },
|
|
375
382
|
{ cmd: '/stop', label: '中断当前任务' },
|
|
376
383
|
{ cmd: '/restart', label: '重启服务' },
|
|
377
|
-
{ cmd: '/
|
|
378
|
-
{ cmd: '/safe', label: '进入安全模式' },
|
|
379
|
-
{ cmd: '/send', args: '[渠道] <path>', label: '发送项目内文件' },
|
|
384
|
+
{ cmd: '/send', args: '[channel] <path>', label: '发送项目内文件' },
|
|
380
385
|
{ cmd: '/check', args: '[rty <channel>]', label: '检查渠道状态或重连指定渠道' },
|
|
381
386
|
]
|
|
382
387
|
});
|
|
@@ -434,11 +439,15 @@ export class CommandHandler {
|
|
|
434
439
|
}
|
|
435
440
|
// 权限检查:区分用户级命令和管理级命令
|
|
436
441
|
const isAdmin = identity.role === 'owner';
|
|
442
|
+
const activeChatType = activeSession?.chatType || 'private';
|
|
437
443
|
if (normalizedContent.startsWith('/')) {
|
|
438
|
-
const
|
|
444
|
+
const guestGroupCommands = ['/status', '/help'];
|
|
445
|
+
const userCommands = activeChatType === 'group' && !isAdmin
|
|
446
|
+
? guestGroupCommands
|
|
447
|
+
: ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s '];
|
|
439
448
|
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
440
449
|
if (!isUserCommand && !isAdmin) {
|
|
441
|
-
return '❌
|
|
450
|
+
return '❌ 无权限:当前群聊仅支持 /status 和 /help';
|
|
442
451
|
}
|
|
443
452
|
}
|
|
444
453
|
// 空闲检查:某些命令需要等待当前会话空闲
|
|
@@ -480,16 +489,24 @@ export class CommandHandler {
|
|
|
480
489
|
return undefined;
|
|
481
490
|
// /help 命令不需要会话
|
|
482
491
|
if (normalizedContent === '/help') {
|
|
492
|
+
if (!isAdmin && activeChatType === 'group') {
|
|
493
|
+
const lines = [
|
|
494
|
+
'可用命令:',
|
|
495
|
+
'',
|
|
496
|
+
'其他:',
|
|
497
|
+
' /status - 显示会话状态',
|
|
498
|
+
' /help - 显示此帮助信息',
|
|
499
|
+
];
|
|
500
|
+
return lines.join('\n');
|
|
501
|
+
}
|
|
483
502
|
if (!isAdmin) {
|
|
484
503
|
const lines = [
|
|
485
504
|
'可用命令:',
|
|
486
505
|
'',
|
|
487
506
|
'🔄 会话管理:',
|
|
488
|
-
' /new [名称] -
|
|
489
|
-
' /
|
|
490
|
-
' /
|
|
491
|
-
' /s, /session <名称|序号|uuid> - 切换到指定会话',
|
|
492
|
-
' /name, /rename <新名称> - 重命名当前会话',
|
|
507
|
+
' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
|
|
508
|
+
' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
|
|
509
|
+
' /name <新名称> - 重命名当前会话',
|
|
493
510
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
494
511
|
' /status - 显示会话状态',
|
|
495
512
|
'',
|
|
@@ -503,18 +520,15 @@ export class CommandHandler {
|
|
|
503
520
|
'',
|
|
504
521
|
'📁 项目管理:',
|
|
505
522
|
' /pwd - 显示当前项目路径',
|
|
506
|
-
' /
|
|
507
|
-
' /p, /project <name|path> - 切换项目',
|
|
523
|
+
' /p [name|path] - 列出或切换项目',
|
|
508
524
|
' /bind <path> - 绑定新项目目录',
|
|
509
525
|
'',
|
|
510
526
|
'🔄 会话管理:',
|
|
511
|
-
' /new [名称] -
|
|
512
|
-
' /
|
|
513
|
-
' /
|
|
514
|
-
' /name, /rename <新名称> - 重命名当前会话',
|
|
527
|
+
' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
|
|
528
|
+
' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
|
|
529
|
+
' /name <新名称> - 重命名当前会话',
|
|
515
530
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
516
531
|
' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
|
|
517
|
-
' /clear - 清空当前会话的对话历史',
|
|
518
532
|
' /compact - 压缩会话上下文(减少 token 用量)',
|
|
519
533
|
'',
|
|
520
534
|
'🤖 Agent 与模型:',
|
|
@@ -531,9 +545,7 @@ export class CommandHandler {
|
|
|
531
545
|
' /status - 显示会话状态',
|
|
532
546
|
' /stop - 中断当前任务',
|
|
533
547
|
' /restart - 重启服务',
|
|
534
|
-
' /
|
|
535
|
-
' /safe - 进入安全模式',
|
|
536
|
-
' /send [渠道] <路径> - 发送项目内文件',
|
|
548
|
+
' /send [channel] <path> - 发送项目内文件',
|
|
537
549
|
'',
|
|
538
550
|
'❓ 帮助:',
|
|
539
551
|
' /help - 显示此帮助信息',
|
|
@@ -554,7 +566,8 @@ export class CommandHandler {
|
|
|
554
566
|
if (!hasPermissionController(permAgent)) {
|
|
555
567
|
return '❌ 权限控制不可用';
|
|
556
568
|
}
|
|
557
|
-
const
|
|
569
|
+
const defaultPermMode = identity.role === 'owner' ? 'bypass' : 'readonly';
|
|
570
|
+
const currentMode = permSession.metadata?.permissionMode ?? defaultPermMode;
|
|
558
571
|
const modes = permAgent.listModes();
|
|
559
572
|
// 尝试发送交互卡片
|
|
560
573
|
if (this.interactionRouter) {
|
|
@@ -1193,8 +1206,8 @@ export class CommandHandler {
|
|
|
1193
1206
|
lines.push('⚠️ 当前处于安全模式(历史上下文已禁用)');
|
|
1194
1207
|
lines.push('');
|
|
1195
1208
|
lines.push('退出方式:');
|
|
1196
|
-
lines.push('1. /
|
|
1197
|
-
lines.push('2. /
|
|
1209
|
+
lines.push('1. /new [名称] - 创建新会话(清空历史)');
|
|
1210
|
+
lines.push('2. 联系管理员使用 /repair 检查并修复会话');
|
|
1198
1211
|
}
|
|
1199
1212
|
if (health.lastError) {
|
|
1200
1213
|
lines.push('');
|
|
@@ -1229,7 +1242,7 @@ export class CommandHandler {
|
|
|
1229
1242
|
await agent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
|
|
1230
1243
|
await agent.closeSession(session.id);
|
|
1231
1244
|
}
|
|
1232
|
-
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /
|
|
1245
|
+
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /s 查看`;
|
|
1233
1246
|
}
|
|
1234
1247
|
// /check 命令:检查渠道状态 / 手动重连指定渠道
|
|
1235
1248
|
if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
|
|
@@ -1291,7 +1304,6 @@ export class CommandHandler {
|
|
|
1291
1304
|
? this.statsCollector.getSnapshot().uptimeMs
|
|
1292
1305
|
: process.uptime() * 1000;
|
|
1293
1306
|
lines.push(` 运行时间: ${this.formatUptime(uptimeMs)}`);
|
|
1294
|
-
lines.push(` 当前安全模式会话: ${this.sessionManager.getSafeModeSessionCount()}`);
|
|
1295
1307
|
// 近 1 小时统计
|
|
1296
1308
|
if (this.statsCollector) {
|
|
1297
1309
|
const snap = this.statsCollector.getSnapshot();
|
|
@@ -1311,7 +1323,6 @@ export class CommandHandler {
|
|
|
1311
1323
|
lines.push(` 工具失败: ${h.toolErrors} (${toolBreakdown})`);
|
|
1312
1324
|
}
|
|
1313
1325
|
lines.push(` 被中断: ${h.interrupts}`);
|
|
1314
|
-
lines.push(` 进入安全模式: ${h.safeModeEntries}`);
|
|
1315
1326
|
if (h.completed > 0) {
|
|
1316
1327
|
lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
|
|
1317
1328
|
}
|
|
@@ -1641,7 +1652,7 @@ export class CommandHandler {
|
|
|
1641
1652
|
else {
|
|
1642
1653
|
projectPath = this.projects[arg];
|
|
1643
1654
|
if (!projectPath) {
|
|
1644
|
-
return `❌ 项目 "${arg}" 不存在\n提示: 使用 /
|
|
1655
|
+
return `❌ 项目 "${arg}" 不存在\n提示: 使用 /p 查看可用项目`;
|
|
1645
1656
|
}
|
|
1646
1657
|
projectName = arg;
|
|
1647
1658
|
}
|
|
@@ -1746,7 +1757,7 @@ export class CommandHandler {
|
|
|
1746
1757
|
请先执行以下操作之一:
|
|
1747
1758
|
1. 发送任意消息 - 自动创建新会话
|
|
1748
1759
|
2. /new [名称] - 创建命名会话
|
|
1749
|
-
3. /
|
|
1760
|
+
3. /p <项目> - 切换到指定项目`;
|
|
1750
1761
|
}
|
|
1751
1762
|
const showCliOnly = normalizedContent === '/slist cli';
|
|
1752
1763
|
// /slist cli — 仅显示 CLI 会话
|
|
@@ -1940,13 +1951,17 @@ export class CommandHandler {
|
|
|
1940
1951
|
lines.push('');
|
|
1941
1952
|
}
|
|
1942
1953
|
lines.push('使用 /s <序号、name或8位uuid> 切换会话');
|
|
1943
|
-
lines.push('使用 /
|
|
1954
|
+
lines.push('使用 /s cli 查看 CLI 会话');
|
|
1944
1955
|
return lines.join('\n');
|
|
1945
1956
|
}
|
|
1946
1957
|
// /session(无参数):直接复用 /slist 逻辑(含卡片交互)
|
|
1947
1958
|
if (normalizedContent === '/session') {
|
|
1948
1959
|
return this.handle('/slist', channel, channelId, undefined, userId, threadId);
|
|
1949
1960
|
}
|
|
1961
|
+
// /session cli(= /s cli):列出未导入的 CLI 会话
|
|
1962
|
+
if (normalizedContent === '/session cli') {
|
|
1963
|
+
return this.handle('/slist cli', channel, channelId, undefined, userId, threadId);
|
|
1964
|
+
}
|
|
1950
1965
|
// /session 或 /s 命令:切换会话
|
|
1951
1966
|
if (normalizedContent.startsWith('/session ')) {
|
|
1952
1967
|
const sessionName = normalizedContent.slice(9).trim();
|
|
@@ -1967,7 +1982,7 @@ export class CommandHandler {
|
|
|
1967
1982
|
targetSession = visibleSessions[idx - 1];
|
|
1968
1983
|
}
|
|
1969
1984
|
else {
|
|
1970
|
-
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /
|
|
1985
|
+
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
|
|
1971
1986
|
}
|
|
1972
1987
|
}
|
|
1973
1988
|
if (!targetSession && sessionName.length === 8) {
|
|
@@ -1992,7 +2007,7 @@ export class CommandHandler {
|
|
|
1992
2007
|
}
|
|
1993
2008
|
}
|
|
1994
2009
|
if (!targetSession) {
|
|
1995
|
-
return `❌ 会话不存在: ${sessionName}\n使用 /
|
|
2010
|
+
return `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话`;
|
|
1996
2011
|
}
|
|
1997
2012
|
const lastInput = targetSession.agentSessionId
|
|
1998
2013
|
? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId, targetSession.agentId)
|
|
@@ -2071,14 +2086,14 @@ export class CommandHandler {
|
|
|
2071
2086
|
targetSession = visibleSessions[idx - 1];
|
|
2072
2087
|
}
|
|
2073
2088
|
else {
|
|
2074
|
-
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /
|
|
2089
|
+
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
|
|
2075
2090
|
}
|
|
2076
2091
|
}
|
|
2077
2092
|
if (!targetSession && sessionName.length === 8) {
|
|
2078
2093
|
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
2079
2094
|
}
|
|
2080
2095
|
if (!targetSession) {
|
|
2081
|
-
return `❌ 会话不存在: ${sessionName}\n使用 /
|
|
2096
|
+
return `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话`;
|
|
2082
2097
|
}
|
|
2083
2098
|
if (targetSession.id === session.id) {
|
|
2084
2099
|
return `❌ 无法删除当前活跃会话\n请先切换到其他会话`;
|
|
@@ -2109,7 +2124,7 @@ export class CommandHandler {
|
|
|
2109
2124
|
const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
|
|
2110
2125
|
const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
|
|
2111
2126
|
this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
|
|
2112
|
-
return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /
|
|
2127
|
+
return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话`;
|
|
2113
2128
|
}
|
|
2114
2129
|
catch (error) {
|
|
2115
2130
|
logger.error('[CommandHandler] Fork session failed:', error);
|
|
@@ -78,7 +78,8 @@ export class MessageBridge {
|
|
|
78
78
|
// 3. session 解析(使用 Channel 层填充的 chatType)
|
|
79
79
|
const chatType = msg.chatType || 'private';
|
|
80
80
|
const metadata = {};
|
|
81
|
-
|
|
81
|
+
// 话题会话创建时写入 replyContext(用于 threadId 路由);主会话不写(避免群聊覆盖)
|
|
82
|
+
if (msg.threadId && msg.replyContext)
|
|
82
83
|
metadata.replyContext = msg.replyContext;
|
|
83
84
|
// 写入实例名(审计 + 精确出站路由)
|
|
84
85
|
metadata.channelName = channelName;
|
|
@@ -151,7 +152,7 @@ export class MessageBridge {
|
|
|
151
152
|
if (parsed.type === 'menu.query') {
|
|
152
153
|
const identity = this.sessionManager.resolveIdentity(channel, msg.peerId);
|
|
153
154
|
const isAdmin = identity.role === 'owner';
|
|
154
|
-
const items = this.cmdHandler.getMenuItems(isAdmin);
|
|
155
|
+
const items = this.cmdHandler.getMenuItems(isAdmin, msg.chatType || 'private');
|
|
155
156
|
const response = JSON.stringify({ type: 'menu.response', items });
|
|
156
157
|
if (adapter?.sendCustomPayload) {
|
|
157
158
|
adapter.sendCustomPayload(msg.channelId, response);
|
|
@@ -181,7 +181,7 @@ export class MessageProcessor {
|
|
|
181
181
|
const msg = showIdleMonitor
|
|
182
182
|
? result.message
|
|
183
183
|
: `\u26a0\ufe0f 任务超时(${result.idleSec}秒无响应),已自动中断`;
|
|
184
|
-
channelInfo.adapter.sendText(message.channelId, msg, this.getReplyContext(
|
|
184
|
+
channelInfo.adapter.sendText(message.channelId, msg, this.getReplyContext(message)).catch(e => {
|
|
185
185
|
logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
|
|
186
186
|
});
|
|
187
187
|
}
|
|
@@ -196,7 +196,7 @@ export class MessageProcessor {
|
|
|
196
196
|
logger.info(`[MessageProcessor] Idle monitor: ${result.action} after ${result.idleSec}s idle, stream: ${streamKey}`);
|
|
197
197
|
if (channelInfo && showIdleMonitor && !shouldSuppress()) {
|
|
198
198
|
if (!isBackground) {
|
|
199
|
-
channelInfo.adapter.sendText(message.channelId, result.message, this.getReplyContext(
|
|
199
|
+
channelInfo.adapter.sendText(message.channelId, result.message, this.getReplyContext(message)).catch(e => {
|
|
200
200
|
logger.debug(`[MessageProcessor] Failed to send idle monitor message:`, e);
|
|
201
201
|
});
|
|
202
202
|
}
|
|
@@ -237,7 +237,7 @@ export class MessageProcessor {
|
|
|
237
237
|
else {
|
|
238
238
|
const prefixed = prefixErrorType(ERROR_PREFIX.INFRA, errorType);
|
|
239
239
|
const newCount = await this.sessionManager.recordError(session.id, prefixed, error.message);
|
|
240
|
-
await this.checkSafeMode(session, message.channelId, channelInfo.adapter, safeModeThreshold, newCount);
|
|
240
|
+
await this.checkSafeMode(session, message.channelId, channelInfo.adapter, safeModeThreshold, newCount, message);
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
catch (statusError) {
|
|
@@ -251,18 +251,18 @@ export class MessageProcessor {
|
|
|
251
251
|
clearInterval(monitorInterval);
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
|
-
/**
|
|
255
|
-
getReplyContext(
|
|
256
|
-
return
|
|
254
|
+
/** 获取回复上下文(跟着任务走) */
|
|
255
|
+
getReplyContext(message) {
|
|
256
|
+
return message.replyContext;
|
|
257
257
|
}
|
|
258
258
|
/**
|
|
259
259
|
* 检查是否需要进入安全模式(safeModeThreshold 为 0 时跳过)
|
|
260
260
|
*/
|
|
261
|
-
async checkSafeMode(session, channelId, adapter, safeModeThreshold, consecutiveErrors) {
|
|
261
|
+
async checkSafeMode(session, channelId, adapter, safeModeThreshold, consecutiveErrors, message) {
|
|
262
262
|
if (safeModeThreshold <= 0)
|
|
263
263
|
return;
|
|
264
264
|
const health = await this.sessionManager.getHealthStatus(session.id);
|
|
265
|
-
const sendOpts = this.getReplyContext(
|
|
265
|
+
const sendOpts = this.getReplyContext(message);
|
|
266
266
|
const isThread = !!session.threadId;
|
|
267
267
|
if (consecutiveErrors >= safeModeThreshold && !health.safeMode) {
|
|
268
268
|
await this.sessionManager.setSafeMode(session.id, true);
|
|
@@ -324,7 +324,7 @@ ${suggestions}`, sendOpts);
|
|
|
324
324
|
logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}`);
|
|
325
325
|
// 记录开始处理
|
|
326
326
|
this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
|
|
327
|
-
adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, this.getReplyContext(
|
|
327
|
+
adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, this.getReplyContext(message));
|
|
328
328
|
logger.message({
|
|
329
329
|
msgId: messageId,
|
|
330
330
|
sessionId: session.id,
|
|
@@ -341,8 +341,8 @@ ${suggestions}`, sendOpts);
|
|
|
341
341
|
const opts = {};
|
|
342
342
|
if (isFinal)
|
|
343
343
|
opts.title = '\u2713 \u6700\u7ec8\u56de\u590d:';
|
|
344
|
-
//
|
|
345
|
-
const replyCtx =
|
|
344
|
+
// replyContext 跟着任务走:优先用当前 message 的,兜底用 session 的(话题会话创建时写入)
|
|
345
|
+
const replyCtx = this.getReplyContext(message);
|
|
346
346
|
if (replyCtx) {
|
|
347
347
|
Object.assign(opts, replyCtx);
|
|
348
348
|
}
|
|
@@ -362,17 +362,18 @@ ${suggestions}`, sendOpts);
|
|
|
362
362
|
// 调用 AgentRunner(含上下文过长自动 compact 重试)
|
|
363
363
|
// 设置权限审批的消息发送回调(指向当前渠道)
|
|
364
364
|
agent.setSendPrompt(async (text) => {
|
|
365
|
-
await adapter.sendText(message.channelId, text, this.getReplyContext(
|
|
365
|
+
await adapter.sendText(message.channelId, text, this.getReplyContext(message));
|
|
366
366
|
});
|
|
367
367
|
// 设置权限审批的交互上下文(支持交互卡片)
|
|
368
368
|
agent.setPermissionContext?.({
|
|
369
369
|
adapter,
|
|
370
370
|
channelId: message.channelId,
|
|
371
|
-
replyContext: this.getReplyContext(
|
|
371
|
+
replyContext: this.getReplyContext(message),
|
|
372
372
|
interactionRouter: this.interactionRouter,
|
|
373
373
|
});
|
|
374
|
-
// 设置 per-session
|
|
375
|
-
|
|
374
|
+
// 设置 per-session 权限模式(动态默认值:owner → bypass,guest → readonly)
|
|
375
|
+
const defaultPermMode = session.identity?.role === 'owner' ? 'bypass' : 'readonly';
|
|
376
|
+
agent.setMode(session.metadata?.permissionMode ?? defaultPermMode);
|
|
376
377
|
// 标记会话为处理中(实时持久化,重启后可恢复)
|
|
377
378
|
this.sessionManager.markProcessing(session.id);
|
|
378
379
|
// 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
|
|
@@ -435,6 +436,10 @@ ${suggestions}`, sendOpts);
|
|
|
435
436
|
if (capParts.length > 0) {
|
|
436
437
|
contextParts.push(`[通道能力] ${capParts.join('、')}`);
|
|
437
438
|
}
|
|
439
|
+
// 4. 群聊 @ 规则:告知 agent 应该 @ 谁,由 agent 自行在回复中添加
|
|
440
|
+
if (message.chatType === 'group' && message.peerId) {
|
|
441
|
+
contextParts.push(`[群聊回复规则] 回复时必须在开头添加 @${message.peerId} 来通知对方`);
|
|
442
|
+
}
|
|
438
443
|
const effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
439
444
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
440
445
|
const MAX_RETRIES = 3;
|
|
@@ -515,22 +520,22 @@ ${suggestions}`, sendOpts);
|
|
|
515
520
|
&& targetSpec !== currentChannelType;
|
|
516
521
|
// 跨通道仅限 owner
|
|
517
522
|
if (isCrossChannel && session.identity?.role !== 'owner') {
|
|
518
|
-
await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`, this.getReplyContext(
|
|
523
|
+
await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`, this.getReplyContext(message));
|
|
519
524
|
continue;
|
|
520
525
|
}
|
|
521
526
|
const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
|
|
522
527
|
if (!fs.existsSync(resolvedPath)) {
|
|
523
528
|
logger.warn(`[${adapter.channelName}] File not found: ${resolvedPath}`);
|
|
524
|
-
await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`, this.getReplyContext(
|
|
529
|
+
await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`, this.getReplyContext(message));
|
|
525
530
|
continue;
|
|
526
531
|
}
|
|
527
532
|
// 找目标 adapter
|
|
528
533
|
if (!targetInfo) {
|
|
529
|
-
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`, this.getReplyContext(
|
|
534
|
+
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`, this.getReplyContext(message));
|
|
530
535
|
continue;
|
|
531
536
|
}
|
|
532
537
|
if (!targetInfo.adapter.sendFile) {
|
|
533
|
-
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 不支持文件发送`, this.getReplyContext(
|
|
538
|
+
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 不支持文件发送`, this.getReplyContext(message));
|
|
534
539
|
continue;
|
|
535
540
|
}
|
|
536
541
|
// 找目标 channelId
|
|
@@ -541,21 +546,21 @@ ${suggestions}`, sendOpts);
|
|
|
541
546
|
const ownerPeerId = getOwner(this.config, targetAdapterName);
|
|
542
547
|
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
|
|
543
548
|
if (!targetChannelId) {
|
|
544
|
-
await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, this.getReplyContext(
|
|
549
|
+
await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, this.getReplyContext(message));
|
|
545
550
|
continue;
|
|
546
551
|
}
|
|
547
552
|
}
|
|
548
553
|
logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
|
|
549
554
|
try {
|
|
550
|
-
await targetInfo.adapter.sendFile(targetChannelId, resolvedPath, this.getReplyContext(
|
|
555
|
+
await targetInfo.adapter.sendFile(targetChannelId, resolvedPath, this.getReplyContext(message));
|
|
551
556
|
this.eventBus.publish({ type: 'agent:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
|
|
552
557
|
if (isCrossChannel) {
|
|
553
|
-
await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, this.getReplyContext(
|
|
558
|
+
await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, this.getReplyContext(message));
|
|
554
559
|
}
|
|
555
560
|
}
|
|
556
561
|
catch (error) {
|
|
557
562
|
logger.error(`[${adapter.channelName}] Failed to send file: ${resolvedPath}`, error);
|
|
558
|
-
await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`, this.getReplyContext(
|
|
563
|
+
await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`, this.getReplyContext(message));
|
|
559
564
|
}
|
|
560
565
|
}
|
|
561
566
|
// 最终回复文本添加到 flusher(统一在流结束后处理,避免多 complete 事件重复发送)
|
|
@@ -580,7 +585,7 @@ ${suggestions}`, sendOpts);
|
|
|
580
585
|
const hint = session.threadId
|
|
581
586
|
? '\n\n\u26a0\ufe0f 当前处于安全模式(无上下文记忆)。使用 /repair 修复 或 /clear 清空会话'
|
|
582
587
|
: '\n\n\u26a0\ufe0f 当前处于安全模式(无上下文记忆)。使用 /repair 修复 或 /new 新建会话';
|
|
583
|
-
await adapter.sendText(message.channelId, hint, this.getReplyContext(
|
|
588
|
+
await adapter.sendText(message.channelId, hint, this.getReplyContext(message));
|
|
584
589
|
}
|
|
585
590
|
// 清理 activeStreams(正常完成)
|
|
586
591
|
agent.cleanupStream(streamKey);
|
|
@@ -593,7 +598,7 @@ ${suggestions}`, sendOpts);
|
|
|
593
598
|
const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
|
|
594
599
|
const rawSubtype = streamResult.subtype || 'agent_error';
|
|
595
600
|
const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
|
|
596
|
-
adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, this.getReplyContext(
|
|
601
|
+
adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, this.getReplyContext(message));
|
|
597
602
|
this.eventBus.publish({
|
|
598
603
|
type: 'message:error',
|
|
599
604
|
sessionId: session.id,
|
|
@@ -609,7 +614,7 @@ ${suggestions}`, sendOpts);
|
|
|
609
614
|
const { policy } = channelInfo;
|
|
610
615
|
if (policy.accumulateErrors(chatType, identityRole)) {
|
|
611
616
|
const newCount = await this.sessionManager.recordError(session.id, errorType, errorSummary);
|
|
612
|
-
await this.checkSafeMode(session, message.channelId, adapter, safeModeThreshold, newCount);
|
|
617
|
+
await this.checkSafeMode(session, message.channelId, adapter, safeModeThreshold, newCount, message);
|
|
613
618
|
}
|
|
614
619
|
}
|
|
615
620
|
logger.message({
|
|
@@ -623,7 +628,7 @@ ${suggestions}`, sendOpts);
|
|
|
623
628
|
}
|
|
624
629
|
else {
|
|
625
630
|
// 真正的成功
|
|
626
|
-
adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, this.getReplyContext(
|
|
631
|
+
adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, this.getReplyContext(message));
|
|
627
632
|
await this.sessionManager.recordSuccess(session.id);
|
|
628
633
|
this.eventBus.publish({
|
|
629
634
|
type: 'message:completed',
|
|
@@ -676,7 +681,7 @@ ${suggestions}`, sendOpts);
|
|
|
676
681
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
677
682
|
if (!isUserInterrupt) {
|
|
678
683
|
try {
|
|
679
|
-
adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, this.getReplyContext(
|
|
684
|
+
adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, this.getReplyContext(message));
|
|
680
685
|
}
|
|
681
686
|
catch { }
|
|
682
687
|
}
|
|
@@ -724,7 +729,7 @@ ${suggestions}`, sendOpts);
|
|
|
724
729
|
let sendOpts;
|
|
725
730
|
try {
|
|
726
731
|
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
|
|
727
|
-
sendOpts = this.getReplyContext(
|
|
732
|
+
sendOpts = this.getReplyContext(message);
|
|
728
733
|
}
|
|
729
734
|
catch { }
|
|
730
735
|
await adapter.sendText(message.channelId, userMessage, sendOpts);
|
|
@@ -735,11 +740,12 @@ ${suggestions}`, sendOpts);
|
|
|
735
740
|
* 解析会话和项目路径
|
|
736
741
|
*/
|
|
737
742
|
async resolveSession(message) {
|
|
738
|
-
//
|
|
739
|
-
const metadata = message.replyContext
|
|
743
|
+
// 话题会话创建时写入 replyContext(threadId 路由);主会话不写(避免群聊覆盖)
|
|
744
|
+
const metadata = (message.threadId && message.replyContext)
|
|
740
745
|
? { replyContext: message.replyContext }
|
|
741
746
|
: undefined;
|
|
742
747
|
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId, metadata, undefined, message.peerId);
|
|
748
|
+
// replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
|
|
743
749
|
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
744
750
|
? session.projectPath
|
|
745
751
|
: path.resolve(process.cwd(), session.projectPath);
|
|
@@ -798,6 +798,10 @@ export class SessionManager {
|
|
|
798
798
|
.run(JSON.stringify(metadata), Date.now(), targetSessionId);
|
|
799
799
|
return { ...this.rowToSession(target), metadata, updatedAt: Date.now() };
|
|
800
800
|
}
|
|
801
|
+
updateMetadata(sessionId, metadata) {
|
|
802
|
+
this.db.prepare(`UPDATE sessions SET metadata = ?, updated_at = ? WHERE id = ?`)
|
|
803
|
+
.run(JSON.stringify(metadata), Date.now(), sessionId);
|
|
804
|
+
}
|
|
801
805
|
async renameSession(sessionId, newName) {
|
|
802
806
|
const result = this.db.prepare(`
|
|
803
807
|
UPDATE sessions SET name = ?, updated_at = ? WHERE id = ?
|
package/dist/index.js
CHANGED
|
@@ -214,11 +214,11 @@ async function main() {
|
|
|
214
214
|
await handler({
|
|
215
215
|
channel: channelType, channelId: chatId, content, images, chatType,
|
|
216
216
|
peerId: peerId || '', peerName, messageId, mentions, threadId,
|
|
217
|
-
replyContext: rootId ? { replyToMessageId: rootId, replyInThread:
|
|
217
|
+
replyContext: rootId ? { replyToMessageId: rootId, replyInThread: !!threadId } : undefined,
|
|
218
218
|
});
|
|
219
219
|
}), (channelId, text, replyContext) => inst.channel.sendMessage(channelId, text, {
|
|
220
220
|
replyToMessageId: replyContext?.replyToMessageId,
|
|
221
|
-
replyInThread:
|
|
221
|
+
replyInThread: replyContext?.replyInThread,
|
|
222
222
|
}), inst.adapter, channelType);
|
|
223
223
|
inst.channel.onRecall?.((messageId) => {
|
|
224
224
|
msgBridge.cancel(messageId);
|
|
@@ -303,18 +303,17 @@ async function main() {
|
|
|
303
303
|
const sourceChannelName = event.channelName || sourceChannelType;
|
|
304
304
|
const msg = event.message;
|
|
305
305
|
logger.error(`[ChannelHealth] ${sourceChannelName} auth_error: ${msg}`);
|
|
306
|
-
const notified = new Set(); // channelType
|
|
306
|
+
const notified = new Set(); // channelType 去重(同类型只通知一次)
|
|
307
307
|
for (const other of channelInstances) {
|
|
308
308
|
const otherType = other.channelType || other.adapter.channelName;
|
|
309
309
|
if (otherType === sourceChannelType)
|
|
310
310
|
continue; // 跳过同类型通道
|
|
311
|
+
if (notified.has(otherType))
|
|
312
|
+
continue; // 同类型已通知过
|
|
311
313
|
const ownerId = getOwner(config, other.adapter.channelName);
|
|
312
314
|
if (!ownerId)
|
|
313
315
|
continue;
|
|
314
|
-
|
|
315
|
-
if (notified.has(key))
|
|
316
|
-
continue; // 同类型已通知过此 owner
|
|
317
|
-
notified.add(key);
|
|
316
|
+
notified.add(otherType);
|
|
318
317
|
other.adapter.sendText(ownerId, msg).catch(err => {
|
|
319
318
|
logger.error(`[ChannelHealth] Failed to notify ${other.adapter.channelName} owner:`, err);
|
|
320
319
|
});
|
|
@@ -380,7 +379,7 @@ async function main() {
|
|
|
380
379
|
const adapter = cmdHandler.getAdapter(pending.channel);
|
|
381
380
|
if (adapter) {
|
|
382
381
|
const replyContext = pending.rootId
|
|
383
|
-
? { replyToMessageId: pending.rootId, replyInThread:
|
|
382
|
+
? { replyToMessageId: pending.rootId, replyInThread: !!pending.threadId }
|
|
384
383
|
: undefined;
|
|
385
384
|
await adapter.sendText(pending.channelId, '✅ 服务重启成功!', replyContext);
|
|
386
385
|
logger.info(`[Restart] Notification sent via ${pending.channel}`);
|
package/package.json
CHANGED