evolclaw 2.4.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 +438 -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 +69 -9
- package/dist/config.js +98 -2
- package/dist/core/command-handler.js +462 -112
- package/dist/core/message/message-bridge.js +8 -5
- package/dist/core/message/message-processor.js +146 -54
- 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 +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 +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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { hasModelSwitcher, hasPermissionController } from '../agents/claude-runner.js';
|
|
2
|
-
import { saveConfig, resolvePaths, getPackageRoot, getOwner } from '../config.js';
|
|
2
|
+
import { saveConfig, resolvePaths, getPackageRoot, getOwner, getChannelShowActivities, setChannelShowActivities } from '../config.js';
|
|
3
3
|
import { logger } from '../utils/logger.js';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import path from 'path';
|
|
@@ -22,10 +22,10 @@ function formatModelUsage(agent, model) {
|
|
|
22
22
|
const efforts = getAvailableEfforts(agent, model);
|
|
23
23
|
const lines = [
|
|
24
24
|
'用法:',
|
|
25
|
-
' /model
|
|
25
|
+
' /model <模型> 切换模型',
|
|
26
26
|
];
|
|
27
27
|
if (efforts.length > 0) {
|
|
28
|
-
lines.push(' /model
|
|
28
|
+
lines.push(' /model <模型> <强度> 切换模型+推理强度');
|
|
29
29
|
lines.push(' /effort [level] 查看或切换推理强度');
|
|
30
30
|
}
|
|
31
31
|
return lines.join('\n');
|
|
@@ -103,15 +103,16 @@ function formatIdleTime(ms) {
|
|
|
103
103
|
return '刚刚';
|
|
104
104
|
}
|
|
105
105
|
// 支持的命令列表
|
|
106
|
-
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/send', '/check'];
|
|
106
|
+
const commands = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/stop', '/clear', '/compact', '/repair', '/safe', '/fork', '/del', '/perm', '/send', '/check', '/rewind', '/activity', '/agentmd'];
|
|
107
107
|
// 命令别名映射
|
|
108
108
|
const aliases = {
|
|
109
109
|
'/p': '/project',
|
|
110
110
|
'/s': '/session',
|
|
111
|
-
'/name': '/rename'
|
|
111
|
+
'/name': '/rename',
|
|
112
|
+
'/rw': '/rewind'
|
|
112
113
|
};
|
|
113
114
|
// 命令快速路径前缀(所有命令都不进入消息队列)
|
|
114
|
-
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name '];
|
|
115
|
+
const quickCommandPrefixes = ['/new', '/pwd', '/plist', '/project', '/bind', '/help', '/status', '/restart', '/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork', '/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/send', '/check', '/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity'];
|
|
115
116
|
export class CommandHandler {
|
|
116
117
|
sessionManager;
|
|
117
118
|
config;
|
|
@@ -237,15 +238,8 @@ export class CommandHandler {
|
|
|
237
238
|
return false;
|
|
238
239
|
const wrappedCallback = async (action, values, operatorId) => {
|
|
239
240
|
await opts.callback(action, values, operatorId);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const disabledCard = {
|
|
243
|
-
config: { wide_screen_mode: true },
|
|
244
|
-
header: { template: 'grey', title: { tag: 'plain_text', content: '已过期' } },
|
|
245
|
-
elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
|
|
246
|
-
};
|
|
247
|
-
adapter.patchInteractionCard(messageId, disabledCard).catch(() => { });
|
|
248
|
-
}
|
|
241
|
+
// 已完成交互的卡片:保留原始内容,仅禁用按钮(不标记为"已过期")
|
|
242
|
+
// "已过期"仅用于被新卡片替代的旧卡片(invalidateOldCards)
|
|
249
243
|
};
|
|
250
244
|
this.interactionRouter.register(opts.requestId, opts.sessionId, wrappedCallback, { timeoutMs: 120_000, messageId });
|
|
251
245
|
return true;
|
|
@@ -322,9 +316,11 @@ export class CommandHandler {
|
|
|
322
316
|
}
|
|
323
317
|
/**
|
|
324
318
|
* 返回结构化命令菜单(供 menu.query 使用)
|
|
325
|
-
* admin
|
|
319
|
+
* owner 看到全部命令,admin 看到管理级命令(不含 owner-only),guest 仅看到用户级命令
|
|
326
320
|
*/
|
|
327
|
-
getMenuItems(
|
|
321
|
+
getMenuItems(role, chatType = 'private') {
|
|
322
|
+
const isOwner = role === 'owner';
|
|
323
|
+
const isAdmin = role === 'owner' || role === 'admin';
|
|
328
324
|
const items = [];
|
|
329
325
|
if (!isAdmin && chatType === 'group') {
|
|
330
326
|
return [
|
|
@@ -332,6 +328,7 @@ export class CommandHandler {
|
|
|
332
328
|
group: '其他',
|
|
333
329
|
commands: [
|
|
334
330
|
{ cmd: '/status', label: '显示会话状态' },
|
|
331
|
+
{ cmd: '/check', label: '检查渠道健康' },
|
|
335
332
|
{ cmd: '/help', label: '显示帮助信息' },
|
|
336
333
|
]
|
|
337
334
|
}
|
|
@@ -343,7 +340,7 @@ export class CommandHandler {
|
|
|
343
340
|
commands: [
|
|
344
341
|
{ cmd: '/pwd', label: '显示当前项目路径' },
|
|
345
342
|
{ cmd: '/p', args: '[name|path]', label: '列出或切换项目' },
|
|
346
|
-
{ cmd: '/bind', args: '<path>', label: '绑定新项目目录' },
|
|
343
|
+
...(isOwner ? [{ cmd: '/bind', args: '<path>', label: '绑定新项目目录' }] : []),
|
|
347
344
|
]
|
|
348
345
|
});
|
|
349
346
|
}
|
|
@@ -356,6 +353,7 @@ export class CommandHandler {
|
|
|
356
353
|
{ cmd: '/del', args: '<name>', label: '删除指定会话' },
|
|
357
354
|
...(isAdmin ? [
|
|
358
355
|
{ cmd: '/fork', args: '[name]', label: '分支当前会话' },
|
|
356
|
+
{ cmd: '/rewind', args: '[N] [chat|file|all]', label: '查看历史/撤销指定轮次 (别名: /rw)' },
|
|
359
357
|
{ cmd: '/compact', label: '压缩会话上下文' },
|
|
360
358
|
] : []),
|
|
361
359
|
]
|
|
@@ -372,7 +370,7 @@ export class CommandHandler {
|
|
|
372
370
|
items.push({
|
|
373
371
|
group: '权限管理',
|
|
374
372
|
commands: [
|
|
375
|
-
{ cmd: '/perm', args: '[mode|allow|always|deny]', label: '权限模式管理' },
|
|
373
|
+
{ cmd: '/perm', args: isOwner ? '[mode|allow|always|deny]' : '[allow|always|deny]', label: '权限模式管理' },
|
|
376
374
|
]
|
|
377
375
|
});
|
|
378
376
|
items.push({
|
|
@@ -380,9 +378,16 @@ export class CommandHandler {
|
|
|
380
378
|
commands: [
|
|
381
379
|
{ cmd: '/status', label: '显示会话状态' },
|
|
382
380
|
{ cmd: '/stop', label: '中断当前任务' },
|
|
383
|
-
{ cmd: '/
|
|
384
|
-
{ cmd: '/
|
|
385
|
-
|
|
381
|
+
{ cmd: '/check', label: '检查渠道状态' },
|
|
382
|
+
{ cmd: '/activity', args: '[all|dm|owner|none]', label: '查看/控制中间输出显示模式' },
|
|
383
|
+
...(isAdmin ? [
|
|
384
|
+
{ cmd: '/restart', args: '<channel>', label: '重连指定渠道' },
|
|
385
|
+
] : []),
|
|
386
|
+
...(isOwner ? [
|
|
387
|
+
{ cmd: '/restart', label: '重启服务' },
|
|
388
|
+
{ cmd: '/send', args: '[channel] <path>', label: '发送项目内文件' },
|
|
389
|
+
{ cmd: '/agentmd', args: '[put|set <内容>]', label: '管理 agent.md' },
|
|
390
|
+
] : []),
|
|
386
391
|
]
|
|
387
392
|
});
|
|
388
393
|
}
|
|
@@ -391,6 +396,7 @@ export class CommandHandler {
|
|
|
391
396
|
group: '其他',
|
|
392
397
|
commands: [
|
|
393
398
|
{ cmd: '/status', label: '显示会话状态' },
|
|
399
|
+
{ cmd: '/check', label: '检查渠道健康' },
|
|
394
400
|
]
|
|
395
401
|
});
|
|
396
402
|
}
|
|
@@ -438,20 +444,23 @@ export class CommandHandler {
|
|
|
438
444
|
}
|
|
439
445
|
}
|
|
440
446
|
// 权限检查:区分用户级命令和管理级命令
|
|
441
|
-
const
|
|
447
|
+
const isOwner = identity.role === 'owner';
|
|
448
|
+
const isAdmin = identity.role === 'owner' || identity.role === 'admin';
|
|
442
449
|
const activeChatType = activeSession?.chatType || 'private';
|
|
443
450
|
if (normalizedContent.startsWith('/')) {
|
|
444
|
-
const guestGroupCommands = ['/status', '/help'];
|
|
451
|
+
const guestGroupCommands = ['/status', '/help', '/check'];
|
|
445
452
|
const userCommands = activeChatType === 'group' && !isAdmin
|
|
446
453
|
? guestGroupCommands
|
|
447
|
-
: ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s '];
|
|
454
|
+
: ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s ', '/check'];
|
|
448
455
|
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
449
456
|
if (!isUserCommand && !isAdmin) {
|
|
450
|
-
return
|
|
457
|
+
return activeChatType === 'group'
|
|
458
|
+
? '❌ 无权限:当前群聊仅支持 /status 和 /help'
|
|
459
|
+
: '❌ 无权限:此命令仅限管理员使用';
|
|
451
460
|
}
|
|
452
461
|
}
|
|
453
462
|
// 空闲检查:某些命令需要等待当前会话空闲
|
|
454
|
-
const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent'];
|
|
463
|
+
const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent', '/rewind'];
|
|
455
464
|
if (requiresIdle.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '))) {
|
|
456
465
|
if (threadId) {
|
|
457
466
|
// 话题中:检查话题 session 是否在处理(不创建)
|
|
@@ -495,6 +504,7 @@ export class CommandHandler {
|
|
|
495
504
|
'',
|
|
496
505
|
'其他:',
|
|
497
506
|
' /status - 显示会话状态',
|
|
507
|
+
' /check - 检查渠道健康',
|
|
498
508
|
' /help - 显示此帮助信息',
|
|
499
509
|
];
|
|
500
510
|
return lines.join('\n');
|
|
@@ -509,19 +519,21 @@ export class CommandHandler {
|
|
|
509
519
|
' /name <新名称> - 重命名当前会话',
|
|
510
520
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
511
521
|
' /status - 显示会话状态',
|
|
522
|
+
' /check - 检查渠道健康',
|
|
512
523
|
'',
|
|
513
524
|
'❓ 帮助:',
|
|
514
525
|
' /help - 显示此帮助信息',
|
|
515
526
|
];
|
|
516
527
|
return lines.join('\n');
|
|
517
528
|
}
|
|
529
|
+
// admin+ 基础命令
|
|
518
530
|
const lines = [
|
|
519
531
|
'可用命令:',
|
|
520
532
|
'',
|
|
521
533
|
'📁 项目管理:',
|
|
522
534
|
' /pwd - 显示当前项目路径',
|
|
523
535
|
' /p [name|path] - 列出或切换项目',
|
|
524
|
-
' /bind <path> - 绑定新项目目录',
|
|
536
|
+
...(isOwner ? [' /bind <path> - 绑定新项目目录'] : []),
|
|
525
537
|
'',
|
|
526
538
|
'🔄 会话管理:',
|
|
527
539
|
' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
|
|
@@ -529,6 +541,7 @@ export class CommandHandler {
|
|
|
529
541
|
' /name <新名称> - 重命名当前会话',
|
|
530
542
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
531
543
|
' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
|
|
544
|
+
' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
|
|
532
545
|
' /compact - 压缩会话上下文(减少 token 用量)',
|
|
533
546
|
'',
|
|
534
547
|
'🤖 Agent 与模型:',
|
|
@@ -538,14 +551,22 @@ export class CommandHandler {
|
|
|
538
551
|
'',
|
|
539
552
|
'🔐 权限管理:',
|
|
540
553
|
' /perm - 查看当前权限模式',
|
|
541
|
-
' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式',
|
|
554
|
+
...(isOwner ? [' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式'] : []),
|
|
542
555
|
' /perm allow|always|deny - 审批权限请求',
|
|
543
556
|
'',
|
|
544
557
|
'🛠️ 运维:',
|
|
545
558
|
' /status - 显示会话状态',
|
|
546
559
|
' /stop - 中断当前任务',
|
|
547
|
-
' /
|
|
548
|
-
' /
|
|
560
|
+
' /check - 检查渠道状态',
|
|
561
|
+
' /activity [all|dm|owner|none] - 查看/控制中间输出显示模式',
|
|
562
|
+
...(isAdmin ? [
|
|
563
|
+
' /restart <channel> - 重连指定渠道',
|
|
564
|
+
] : []),
|
|
565
|
+
...(isOwner ? [
|
|
566
|
+
' /restart - 重启服务',
|
|
567
|
+
' /send [channel] <path> - 发送项目内文件',
|
|
568
|
+
' /agentmd [put|set <内容>] - 管理 agent.md',
|
|
569
|
+
] : []),
|
|
549
570
|
'',
|
|
550
571
|
'❓ 帮助:',
|
|
551
572
|
' /help - 显示此帮助信息',
|
|
@@ -581,7 +602,7 @@ export class CommandHandler {
|
|
|
581
602
|
kind: {
|
|
582
603
|
kind: 'action',
|
|
583
604
|
title: '🔐 权限模式',
|
|
584
|
-
body: availableModes.map(m => `${m.key === currentMode ? '
|
|
605
|
+
body: availableModes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.nameZh}) - ${m.description}`).join('\n'),
|
|
585
606
|
buttons: availableModes.map(m => ({
|
|
586
607
|
key: m.key,
|
|
587
608
|
label: m.key === currentMode ? `✓ ${m.key}` : m.key,
|
|
@@ -609,7 +630,7 @@ export class CommandHandler {
|
|
|
609
630
|
}
|
|
610
631
|
// 降级:文本
|
|
611
632
|
const modeList = modes.map(m => {
|
|
612
|
-
const prefix = m.key === currentMode ? '
|
|
633
|
+
const prefix = m.key === currentMode ? '✓' : ' ';
|
|
613
634
|
const suffix = m.available ? '' : ' ⚠️ 不可用';
|
|
614
635
|
return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
|
|
615
636
|
}).join('\n');
|
|
@@ -649,9 +670,9 @@ export class CommandHandler {
|
|
|
649
670
|
if (!matched.available) {
|
|
650
671
|
return `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}`;
|
|
651
672
|
}
|
|
652
|
-
// guest
|
|
653
|
-
if (
|
|
654
|
-
return '❌
|
|
673
|
+
// guest 和 admin 用户不能切换权限模式(仅 owner)
|
|
674
|
+
if (!isOwner) {
|
|
675
|
+
return '❌ 权限模式切换仅限 owner';
|
|
655
676
|
}
|
|
656
677
|
const metadata = permSession.metadata || {};
|
|
657
678
|
metadata.permissionMode = arg;
|
|
@@ -668,8 +689,9 @@ export class CommandHandler {
|
|
|
668
689
|
return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny`;
|
|
669
690
|
}
|
|
670
691
|
// /agent 命令:查看或切换 Agent 后端
|
|
671
|
-
if (normalizedContent.startsWith('/agent')) {
|
|
672
|
-
|
|
692
|
+
if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ')) {
|
|
693
|
+
// 群聊中 owner only,私聊中 admin+
|
|
694
|
+
if (activeChatType === 'group' ? !isOwner : !isAdmin)
|
|
673
695
|
return '❌ 无权限:此命令仅限管理员使用';
|
|
674
696
|
const args = normalizedContent.slice(6).trim();
|
|
675
697
|
const available = [...this.agentMap.keys()];
|
|
@@ -787,7 +809,7 @@ export class CommandHandler {
|
|
|
787
809
|
return null;
|
|
788
810
|
}
|
|
789
811
|
// 降级:文本
|
|
790
|
-
const modelList = models.map((m) =>
|
|
812
|
+
const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
|
|
791
813
|
const effortHint = efforts.length > 0
|
|
792
814
|
? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
|
|
793
815
|
: '';
|
|
@@ -811,7 +833,7 @@ export class CommandHandler {
|
|
|
811
833
|
newModel = arg;
|
|
812
834
|
}
|
|
813
835
|
else {
|
|
814
|
-
const modelList = models.map((m) =>
|
|
836
|
+
const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
|
|
815
837
|
const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
|
|
816
838
|
return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}`;
|
|
817
839
|
}
|
|
@@ -977,8 +999,9 @@ export class CommandHandler {
|
|
|
977
999
|
}
|
|
978
1000
|
// 降级:文本
|
|
979
1001
|
const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
|
|
980
|
-
const
|
|
981
|
-
|
|
1002
|
+
const allItems = [...efforts, 'auto'];
|
|
1003
|
+
const effortList = allItems.map(e => ` ${e === currentEffort ? '✓' : ' '} ${e}${e === 'auto' ? ' (SDK默认)' : ''}`).join('\n');
|
|
1004
|
+
return `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n\n用法: /effort <level>`;
|
|
982
1005
|
}
|
|
983
1006
|
// /effort auto:恢复 SDK 默认
|
|
984
1007
|
if (args === 'auto') {
|
|
@@ -1047,6 +1070,151 @@ export class CommandHandler {
|
|
|
1047
1070
|
}
|
|
1048
1071
|
return `✓ 推理强度: ${newEffort}`;
|
|
1049
1072
|
}
|
|
1073
|
+
// /activity 命令:控制中间输出显示模式
|
|
1074
|
+
if (normalizedContent === '/agentmd' || normalizedContent.startsWith('/agentmd ')) {
|
|
1075
|
+
if (!isOwner)
|
|
1076
|
+
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1077
|
+
const adapter = this.adapters.get(channel);
|
|
1078
|
+
if (!adapter?.uploadAgentMd)
|
|
1079
|
+
return '❌ 当前通道不支持 agent.md 操作';
|
|
1080
|
+
const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
|
|
1081
|
+
const arg = normalizedContent.slice(9).trim();
|
|
1082
|
+
// put — read local ~/.aun/AIDs/{aid}/agent.md and upload
|
|
1083
|
+
if (arg === 'put') {
|
|
1084
|
+
if (!selfAid)
|
|
1085
|
+
return '❌ 未连接,无法确定本地 AID';
|
|
1086
|
+
try {
|
|
1087
|
+
const { readFileSync } = await import('node:fs');
|
|
1088
|
+
const { join } = await import('node:path');
|
|
1089
|
+
const { homedir } = await import('node:os');
|
|
1090
|
+
const localPath = join(homedir(), '.aun', 'AIDs', selfAid, 'agent.md');
|
|
1091
|
+
const content = readFileSync(localPath, 'utf-8');
|
|
1092
|
+
await adapter.uploadAgentMd(content);
|
|
1093
|
+
return '✅ agent.md 已发布';
|
|
1094
|
+
}
|
|
1095
|
+
catch (e) {
|
|
1096
|
+
return `❌ 发布失败: ${String(e.message || e).slice(0, 100)}`;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
// set <content> — upload inline content and sync to local
|
|
1100
|
+
if (arg.startsWith('set ')) {
|
|
1101
|
+
const content = arg.slice(4).trim();
|
|
1102
|
+
if (!content)
|
|
1103
|
+
return '用法:/agentmd set <内容>';
|
|
1104
|
+
if (!selfAid)
|
|
1105
|
+
return '❌ 未连接,无法确定本地 AID';
|
|
1106
|
+
try {
|
|
1107
|
+
await adapter.uploadAgentMd(content);
|
|
1108
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
1109
|
+
const { join } = await import('node:path');
|
|
1110
|
+
const { homedir } = await import('node:os');
|
|
1111
|
+
const localDir = join(homedir(), '.aun', 'AIDs', selfAid);
|
|
1112
|
+
mkdirSync(localDir, { recursive: true });
|
|
1113
|
+
writeFileSync(join(localDir, 'agent.md'), content, 'utf-8');
|
|
1114
|
+
return '✅ agent.md 已更新并发布到AUN网络';
|
|
1115
|
+
}
|
|
1116
|
+
catch (e) {
|
|
1117
|
+
return `❌ 发布失败: ${String(e.message || e).slice(0, 100)}`;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
// view — /agentmd or /agentmd <aid>
|
|
1121
|
+
const aidToView = arg || selfAid;
|
|
1122
|
+
if (!aidToView)
|
|
1123
|
+
return '用法:/agentmd [<aid>] | put | set <内容>';
|
|
1124
|
+
try {
|
|
1125
|
+
const md = await adapter.downloadAgentMd(aidToView);
|
|
1126
|
+
if (!md || !md.trim())
|
|
1127
|
+
return `ℹ️ ${aidToView} 尚未设置 agent.md`;
|
|
1128
|
+
return `\`\`\`\n${md.slice(0, 1500)}\n\`\`\``;
|
|
1129
|
+
}
|
|
1130
|
+
catch (e) {
|
|
1131
|
+
const msg = String(e.message || e);
|
|
1132
|
+
if (msg.includes('not found') || msg.includes('404')) {
|
|
1133
|
+
return `ℹ️ ${aidToView} 尚未设置 agent.md`;
|
|
1134
|
+
}
|
|
1135
|
+
return `❌ 获取失败: ${msg.slice(0, 100)}`;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
|
|
1139
|
+
if (!isAdmin)
|
|
1140
|
+
return '❌ 无权限:此命令仅限管理员使用';
|
|
1141
|
+
const activityArg = normalizedContent.slice(9).trim();
|
|
1142
|
+
const modeMap = {
|
|
1143
|
+
all: 'all',
|
|
1144
|
+
dm: 'dm-only',
|
|
1145
|
+
owner: 'owner-dm-only',
|
|
1146
|
+
none: 'none',
|
|
1147
|
+
};
|
|
1148
|
+
const currentMode = getChannelShowActivities(this.config, channel);
|
|
1149
|
+
// 模式描述列表(用于 body 和文本降级)
|
|
1150
|
+
const modeDescriptions = [
|
|
1151
|
+
{ key: 'all', configVal: 'all', label: '全部显示' },
|
|
1152
|
+
{ key: 'dm', configVal: 'dm-only', label: '仅私聊显示' },
|
|
1153
|
+
{ key: 'owner', configVal: 'owner-dm-only', label: '仅 owner 私聊显示' },
|
|
1154
|
+
{ key: 'none', configVal: 'none', label: '全部静默' },
|
|
1155
|
+
];
|
|
1156
|
+
if (!activityArg) {
|
|
1157
|
+
// 无参数:显示当前模式 + Action 卡片
|
|
1158
|
+
if (this.interactionRouter) {
|
|
1159
|
+
const requestId = `activity-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1160
|
+
const body = modeDescriptions.map(m => `${m.configVal === currentMode ? '✓' : '•'} **${m.key}** (${m.label})`).join('\n');
|
|
1161
|
+
const buttons = modeDescriptions.map(m => ({
|
|
1162
|
+
key: m.key,
|
|
1163
|
+
label: m.configVal === currentMode ? `✓ ${m.key}` : m.key,
|
|
1164
|
+
style: m.configVal === currentMode ? 'primary' : 'default',
|
|
1165
|
+
}));
|
|
1166
|
+
const interaction = {
|
|
1167
|
+
type: 'interaction',
|
|
1168
|
+
id: requestId,
|
|
1169
|
+
channelId,
|
|
1170
|
+
sessionId: activeSession?.id || requestId,
|
|
1171
|
+
kind: {
|
|
1172
|
+
kind: 'action',
|
|
1173
|
+
title: '📋 中间输出模式',
|
|
1174
|
+
body,
|
|
1175
|
+
buttons,
|
|
1176
|
+
},
|
|
1177
|
+
};
|
|
1178
|
+
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
1179
|
+
const cardSent = await this.sendInteractionCard({
|
|
1180
|
+
channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
|
|
1181
|
+
callback: async (action, _values, operatorId) => {
|
|
1182
|
+
const newMode = modeMap[action];
|
|
1183
|
+
if (newMode && newMode !== currentMode) {
|
|
1184
|
+
if (userId && operatorId && operatorId !== userId)
|
|
1185
|
+
return;
|
|
1186
|
+
const result = await this.handle(`/activity ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1187
|
+
if (result) {
|
|
1188
|
+
const adapter = this.adapters.get(channel);
|
|
1189
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1193
|
+
});
|
|
1194
|
+
if (cardSent)
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
// 降级:文本
|
|
1198
|
+
const modeList = modeDescriptions.map(m => {
|
|
1199
|
+
const prefix = m.configVal === currentMode ? '✓' : ' ';
|
|
1200
|
+
return ` ${prefix} ${m.key} (${m.label})`;
|
|
1201
|
+
}).join('\n');
|
|
1202
|
+
return `📋 中间输出模式: ${currentMode}\n\n${modeList}\n\n用法:\n /activity <模式> 切换中间输出显示模式`;
|
|
1203
|
+
}
|
|
1204
|
+
const newMode = modeMap[activityArg];
|
|
1205
|
+
if (!newMode) {
|
|
1206
|
+
return `❌ 无效参数: ${activityArg}\n可选: all / dm / owner / none`;
|
|
1207
|
+
}
|
|
1208
|
+
const label = modeDescriptions.find(m => m.configVal === newMode)?.label || newMode;
|
|
1209
|
+
if (newMode === currentMode) {
|
|
1210
|
+
return `📋 中间输出模式已是 ${activityArg}(${label})`;
|
|
1211
|
+
}
|
|
1212
|
+
// 切换操作仅 owner
|
|
1213
|
+
if (!isOwner)
|
|
1214
|
+
return '❌ 中间输出模式切换仅限 owner';
|
|
1215
|
+
setChannelShowActivities(this.config, channel, newMode);
|
|
1216
|
+
return `✅ 中间输出模式: ${activityArg}(${label})`;
|
|
1217
|
+
}
|
|
1050
1218
|
// /stop 命令:中断当前任务
|
|
1051
1219
|
if (normalizedContent === '/stop') {
|
|
1052
1220
|
const stopResult = await this.ensureSession(channel, channelId, threadId);
|
|
@@ -1193,22 +1361,11 @@ export class CommandHandler {
|
|
|
1193
1361
|
if (health.consecutiveErrors > 0) {
|
|
1194
1362
|
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
1195
1363
|
}
|
|
1196
|
-
if (health.safeMode) {
|
|
1197
|
-
lines.push(`安全模式: 是 ⚠️`);
|
|
1198
|
-
}
|
|
1199
1364
|
lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
|
|
1200
1365
|
}
|
|
1201
1366
|
else {
|
|
1202
1367
|
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
1203
1368
|
}
|
|
1204
|
-
if (health.safeMode) {
|
|
1205
|
-
lines.push('');
|
|
1206
|
-
lines.push('⚠️ 当前处于安全模式(历史上下文已禁用)');
|
|
1207
|
-
lines.push('');
|
|
1208
|
-
lines.push('退出方式:');
|
|
1209
|
-
lines.push('1. /new [名称] - 创建新会话(清空历史)');
|
|
1210
|
-
lines.push('2. 联系管理员使用 /repair 检查并修复会话');
|
|
1211
|
-
}
|
|
1212
1369
|
if (health.lastError) {
|
|
1213
1370
|
lines.push('');
|
|
1214
1371
|
lines.push(`最后错误: ${health.lastErrorType || 'unknown'}`);
|
|
@@ -1244,29 +1401,10 @@ export class CommandHandler {
|
|
|
1244
1401
|
}
|
|
1245
1402
|
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /s 查看`;
|
|
1246
1403
|
}
|
|
1247
|
-
// /check
|
|
1404
|
+
// /check 命令:检查渠道状态(guest 可用,详情仅 admin)/ 重连指定渠道(admin only)
|
|
1248
1405
|
if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
|
|
1249
|
-
if (!isAdmin)
|
|
1250
|
-
return '❌ 无权限:此命令仅限管理员使用';
|
|
1251
1406
|
const subCmd = normalizedContent.slice('/check'.length).trim();
|
|
1252
|
-
//
|
|
1253
|
-
if (subCmd.startsWith('rty')) {
|
|
1254
|
-
const target = subCmd.slice('rty'.length).trim();
|
|
1255
|
-
if (!target) {
|
|
1256
|
-
return '❌ 请指定渠道名称,例如:/check rty feishu';
|
|
1257
|
-
}
|
|
1258
|
-
const ch = this.channelObjects.get(target);
|
|
1259
|
-
if (!ch) {
|
|
1260
|
-
const available = [...this.channelObjects.keys()].join(', ') || '无';
|
|
1261
|
-
return `❌ 未找到渠道 "${target}",可用渠道:${available}`;
|
|
1262
|
-
}
|
|
1263
|
-
if (!ch.reconnect) {
|
|
1264
|
-
return `❌ 渠道 "${target}" 不支持重连`;
|
|
1265
|
-
}
|
|
1266
|
-
const result = await ch.reconnect();
|
|
1267
|
-
return `🔄 ${target} 重连: ${result}`;
|
|
1268
|
-
}
|
|
1269
|
-
// Default: show full system health check
|
|
1407
|
+
// Default: show system health check (non-admin 仅看摘要)
|
|
1270
1408
|
const lines = ['📡 渠道状态:'];
|
|
1271
1409
|
// Group by channelType
|
|
1272
1410
|
const groups = new Map();
|
|
@@ -1285,6 +1423,13 @@ export class CommandHandler {
|
|
|
1285
1423
|
groups.set(type, []);
|
|
1286
1424
|
groups.get(type).push({ name, status });
|
|
1287
1425
|
}
|
|
1426
|
+
if (!isAdmin) {
|
|
1427
|
+
// guest/user: 仅显示渠道健康摘要
|
|
1428
|
+
const total = [...groups.values()].flat().length;
|
|
1429
|
+
const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
|
|
1430
|
+
lines.push(` ${healthy}/${total} 渠道正常`);
|
|
1431
|
+
return lines.join('\n');
|
|
1432
|
+
}
|
|
1288
1433
|
for (const [type, instances] of groups) {
|
|
1289
1434
|
if (instances.length === 1) {
|
|
1290
1435
|
lines.push(` ${instances[0].name}: ${instances[0].status}`);
|
|
@@ -1327,11 +1472,30 @@ export class CommandHandler {
|
|
|
1327
1472
|
lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
|
|
1328
1473
|
}
|
|
1329
1474
|
}
|
|
1330
|
-
lines.push('', '💡 /check rty <channel> — 重连指定渠道');
|
|
1331
1475
|
return lines.join('\n');
|
|
1332
1476
|
}
|
|
1333
|
-
// /restart
|
|
1334
|
-
if (normalizedContent === '/restart') {
|
|
1477
|
+
// /restart 命令:重启服务(owner only) / 重连指定渠道(admin+)
|
|
1478
|
+
if (normalizedContent === '/restart' || normalizedContent.startsWith('/restart ')) {
|
|
1479
|
+
const restartArg = normalizedContent.slice('/restart'.length).trim();
|
|
1480
|
+
// /restart <channel> — 重连指定渠道(admin only)
|
|
1481
|
+
if (restartArg) {
|
|
1482
|
+
if (!isAdmin)
|
|
1483
|
+
return '❌ 无权限:渠道重连仅限管理员使用';
|
|
1484
|
+
const target = restartArg;
|
|
1485
|
+
const ch = this.channelObjects.get(target);
|
|
1486
|
+
if (!ch) {
|
|
1487
|
+
const available = [...this.channelObjects.keys()].join(', ') || '无';
|
|
1488
|
+
return `❌ 未找到渠道 "${target}",可用渠道:${available}`;
|
|
1489
|
+
}
|
|
1490
|
+
if (!ch.reconnect) {
|
|
1491
|
+
return `❌ 渠道 "${target}" 不支持重连`;
|
|
1492
|
+
}
|
|
1493
|
+
const result = await ch.reconnect();
|
|
1494
|
+
return `🔄 ${target} 重连: ${result}`;
|
|
1495
|
+
}
|
|
1496
|
+
// /restart(无参数)— 重启整个服务(owner only)
|
|
1497
|
+
if (!isOwner)
|
|
1498
|
+
return '❌ 无权限:服务重启仅限 owner 使用';
|
|
1335
1499
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
1336
1500
|
const sessionsWithMessages = allSessions
|
|
1337
1501
|
.filter(s => this.messageCache.hasMessages(s.id))
|
|
@@ -1400,8 +1564,10 @@ export class CommandHandler {
|
|
|
1400
1564
|
}
|
|
1401
1565
|
return `当前项目: ${session.projectPath}`;
|
|
1402
1566
|
}
|
|
1403
|
-
// /send 命令:发送项目内文件,支持 /send path 和 /send channel path
|
|
1567
|
+
// /send 命令:发送项目内文件,支持 /send path 和 /send channel path(owner only)
|
|
1404
1568
|
if (normalizedContent.startsWith('/send')) {
|
|
1569
|
+
if (!isOwner)
|
|
1570
|
+
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1405
1571
|
// 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
|
|
1406
1572
|
// 还原: 将 [text](url) 替换为 text
|
|
1407
1573
|
const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
@@ -1573,7 +1739,7 @@ export class CommandHandler {
|
|
|
1573
1739
|
}));
|
|
1574
1740
|
const bodyLines = entries.map(e => {
|
|
1575
1741
|
const status = buildStatusText(e);
|
|
1576
|
-
const prefix = e.isCurrent ? '
|
|
1742
|
+
const prefix = e.isCurrent ? '✓' : '•';
|
|
1577
1743
|
return `${prefix} **${e.name}** (${e.projectPath}) ${status}`;
|
|
1578
1744
|
});
|
|
1579
1745
|
const interaction = {
|
|
@@ -1613,6 +1779,7 @@ export class CommandHandler {
|
|
|
1613
1779
|
const prefix = entry.isCurrent ? ' ✓' : ' ';
|
|
1614
1780
|
lines.push(`${prefix} ${entry.name} (${entry.projectPath}) - ${buildStatusText(entry)}`);
|
|
1615
1781
|
}
|
|
1782
|
+
lines.push('', '提示: 使用 /p <名称> 切换项目');
|
|
1616
1783
|
return lines.join('\n');
|
|
1617
1784
|
}
|
|
1618
1785
|
// /project(无参数):直接复用 /plist 逻辑(含卡片交互)
|
|
@@ -1711,8 +1878,10 @@ export class CommandHandler {
|
|
|
1711
1878
|
}
|
|
1712
1879
|
return response;
|
|
1713
1880
|
}
|
|
1714
|
-
// /bind
|
|
1881
|
+
// /bind 命令:持久化项目到配置(不切换)(owner only)
|
|
1715
1882
|
if (normalizedContent.startsWith('/bind ')) {
|
|
1883
|
+
if (!isOwner)
|
|
1884
|
+
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1716
1885
|
const projectPath = normalizedContent.slice(6).trim();
|
|
1717
1886
|
if (!projectPath)
|
|
1718
1887
|
return '用法: /bind <路径>';
|
|
@@ -1887,7 +2056,7 @@ export class CommandHandler {
|
|
|
1887
2056
|
};
|
|
1888
2057
|
});
|
|
1889
2058
|
const bodyLines = displaySessions.map(ds => {
|
|
1890
|
-
const prefix = ds.isActive ? '
|
|
2059
|
+
const prefix = ds.isActive ? '✓' : '•';
|
|
1891
2060
|
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
1892
2061
|
const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
|
|
1893
2062
|
const fileMark = ds.fileMissing ? '❌ ' : '';
|
|
@@ -2131,31 +2300,59 @@ export class CommandHandler {
|
|
|
2131
2300
|
return `❌ 会话分支失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
|
2132
2301
|
}
|
|
2133
2302
|
}
|
|
2134
|
-
// /
|
|
2303
|
+
// /rewind 命令:查看历史 / 回退会话
|
|
2304
|
+
if (normalizedContent === '/rewind' || normalizedContent.startsWith('/rewind ')) {
|
|
2305
|
+
const result = await this.ensureSession(channel, channelId, threadId);
|
|
2306
|
+
if ('error' in result)
|
|
2307
|
+
return result.error;
|
|
2308
|
+
const { session } = result;
|
|
2309
|
+
const rewindAgent = this.getAgent(session.agentId);
|
|
2310
|
+
if (rewindAgent.name !== 'claude') {
|
|
2311
|
+
return '❌ /rewind 仅支持 Claude 后端';
|
|
2312
|
+
}
|
|
2313
|
+
if (!session.agentSessionId) {
|
|
2314
|
+
return '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind';
|
|
2315
|
+
}
|
|
2316
|
+
if (!rewindAgent.getSessionMessages) {
|
|
2317
|
+
return '❌ 当前 Agent 不支持 /rewind';
|
|
2318
|
+
}
|
|
2319
|
+
const args = normalizedContent.slice('/rewind'.length).trim();
|
|
2320
|
+
if (!args) {
|
|
2321
|
+
return await this.handleRewindList(session, rewindAgent);
|
|
2322
|
+
}
|
|
2323
|
+
const parts = args.split(/\s+/);
|
|
2324
|
+
const turnNum = parseInt(parts[0], 10);
|
|
2325
|
+
if (isNaN(turnNum) || turnNum < 1) {
|
|
2326
|
+
return '❌ 无效轮次,用法:/rewind <N> chat|file|all(撤销第N轮)';
|
|
2327
|
+
}
|
|
2328
|
+
const mode = parts[1]?.toLowerCase();
|
|
2329
|
+
if (!mode) {
|
|
2330
|
+
return `❌ 请指定回退模式:/rewind ${turnNum} chat | file | all(撤销第${turnNum}轮)`;
|
|
2331
|
+
}
|
|
2332
|
+
if (!['chat', 'file', 'all'].includes(mode)) {
|
|
2333
|
+
return `❌ 无效模式 "${mode}",可选:chat | file | all`;
|
|
2334
|
+
}
|
|
2335
|
+
return await this.handleRewind(session, rewindAgent, turnNum, mode);
|
|
2336
|
+
}
|
|
2337
|
+
// /repair 命令:检查并修复会话文件
|
|
2135
2338
|
if (normalizedContent === '/repair') {
|
|
2136
2339
|
const repairResult = await this.ensureSession(channel, channelId, threadId);
|
|
2137
2340
|
if ('error' in repairResult)
|
|
2138
2341
|
return repairResult.error;
|
|
2139
2342
|
const { session: repairSession } = repairResult;
|
|
2140
|
-
const health = await this.sessionManager.getHealthStatus(repairSession.id);
|
|
2141
|
-
if (!health.safeMode) {
|
|
2142
|
-
return `当前不在安全模式,无需修复\n\n如需进入安全模式,请使用 /safe`;
|
|
2143
|
-
}
|
|
2144
2343
|
const repairAgent = this.getAgent(repairSession.agentId);
|
|
2145
2344
|
const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
|
|
2146
2345
|
try {
|
|
2147
2346
|
if (!repairSession.agentSessionId) {
|
|
2148
2347
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2149
|
-
|
|
2150
|
-
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
2348
|
+
return `✓ 修复完成\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器`;
|
|
2151
2349
|
}
|
|
2152
2350
|
// 通过 agent 定位 session 文件
|
|
2153
2351
|
const sessionFile = repairAgent.resolveSessionFile?.(repairSession.agentSessionId, repairSession.projectPath) ?? null;
|
|
2154
2352
|
if (!sessionFile) {
|
|
2155
2353
|
// 文件不存在(已被删除或从未创建),直接重置
|
|
2156
2354
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2157
|
-
|
|
2158
|
-
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
2355
|
+
return `✓ 修复完成\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器`;
|
|
2159
2356
|
}
|
|
2160
2357
|
const healthCheck = await checkSessionFile(sessionFile);
|
|
2161
2358
|
if (healthCheck.corrupt) {
|
|
@@ -2165,42 +2362,195 @@ export class CommandHandler {
|
|
|
2165
2362
|
await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
|
|
2166
2363
|
repairAgent.updateSessionId(repairSession.id, '');
|
|
2167
2364
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2168
|
-
|
|
2169
|
-
return `✓ 修复完成,已退出安全模式\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}`;
|
|
2365
|
+
return `✓ 修复完成\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}`;
|
|
2170
2366
|
}
|
|
2171
2367
|
if (healthCheck.issues.length > 0) {
|
|
2172
2368
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2173
|
-
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
2174
2369
|
return `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n已重置异常计数器,可继续使用当前会话。`;
|
|
2175
2370
|
}
|
|
2176
2371
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2177
|
-
|
|
2178
|
-
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
2372
|
+
return `✓ 修复完成\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器`;
|
|
2179
2373
|
}
|
|
2180
2374
|
catch (error) {
|
|
2181
2375
|
logger.error('[Repair] Failed:', error);
|
|
2182
2376
|
return `❌ 修复失败: ${error.message}`;
|
|
2183
2377
|
}
|
|
2184
2378
|
}
|
|
2185
|
-
// /safe
|
|
2379
|
+
// /safe 命令:安全模式已禁用
|
|
2186
2380
|
if (normalizedContent === '/safe') {
|
|
2187
|
-
|
|
2188
|
-
if ('error' in safeResult)
|
|
2189
|
-
return safeResult.error;
|
|
2190
|
-
const { session: safeSession } = safeResult;
|
|
2191
|
-
await this.sessionManager.setSafeMode(safeSession.id, true);
|
|
2192
|
-
this.eventBus.publish({ type: 'session:safe-mode-entered', sessionId: safeSession.id, reason: 'manual' });
|
|
2193
|
-
return `✓ 已进入安全模式
|
|
2194
|
-
|
|
2195
|
-
当前行为:
|
|
2196
|
-
- 暂时不加载会话历史(每次对话独立)
|
|
2197
|
-
- 所有功能正常可用(读写文件、执行命令等)
|
|
2198
|
-
- 不会丢失历史数据(仍保存在 .claude/ 目录)
|
|
2199
|
-
|
|
2200
|
-
退出安全模式:
|
|
2201
|
-
- 使用 /repair 检查并修复会话
|
|
2202
|
-
- 使用 /new 创建全新会话`;
|
|
2381
|
+
return `ℹ️ 安全模式已禁用\n\n如需重置会话,请使用 /new 创建新会话。`;
|
|
2203
2382
|
}
|
|
2204
2383
|
return null;
|
|
2205
2384
|
}
|
|
2385
|
+
// ── /rewind helpers ──
|
|
2386
|
+
async handleRewindList(session, agent) {
|
|
2387
|
+
try {
|
|
2388
|
+
const messages = await agent.getSessionMessages(session.agentSessionId, session.projectPath);
|
|
2389
|
+
const turns = this.buildTurnList(messages);
|
|
2390
|
+
if (turns.length === 0) {
|
|
2391
|
+
return '📋 当前会话暂无对话记录';
|
|
2392
|
+
}
|
|
2393
|
+
const lines = turns.map(t => `#${t.index} ${t.userContent}`);
|
|
2394
|
+
return [
|
|
2395
|
+
`📋 会话历史 (共 ${turns.length} 轮)`,
|
|
2396
|
+
'',
|
|
2397
|
+
...lines,
|
|
2398
|
+
'',
|
|
2399
|
+
'💡 /rewind <N> chat|file|all — 撤销第N轮',
|
|
2400
|
+
].join('\n');
|
|
2401
|
+
}
|
|
2402
|
+
catch (error) {
|
|
2403
|
+
logger.error('[CommandHandler] Failed to read session messages:', error);
|
|
2404
|
+
return `❌ 读取会话历史失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
async handleRewind(session, agent, turnNum, mode) {
|
|
2408
|
+
try {
|
|
2409
|
+
const messages = await agent.getSessionMessages(session.agentSessionId, session.projectPath);
|
|
2410
|
+
const turns = this.buildTurnList(messages);
|
|
2411
|
+
if (turnNum < 1 || turnNum > turns.length) {
|
|
2412
|
+
return `❌ 轮次超出范围,当前共 ${turns.length} 轮`;
|
|
2413
|
+
}
|
|
2414
|
+
// /rewind N = 撤销第N轮(及之后),保留 1..N-1
|
|
2415
|
+
const rewindTarget = turns[turnNum - 1]; // 被撤销的轮次(用于文件回退)
|
|
2416
|
+
const keepTarget = turnNum >= 2 ? turns[turnNum - 2] : null; // 保留到的轮次(用于对话回退)
|
|
2417
|
+
const results = [];
|
|
2418
|
+
// 文件回退(立即执行)
|
|
2419
|
+
if (mode === 'file' || mode === 'all') {
|
|
2420
|
+
if (!agent.rewindFiles) {
|
|
2421
|
+
return '❌ 当前 Agent 不支持文件回退';
|
|
2422
|
+
}
|
|
2423
|
+
const fileResult = await agent.rewindFiles(session.agentSessionId, session.projectPath, rewindTarget.userUuid);
|
|
2424
|
+
if (!fileResult.canRewind) {
|
|
2425
|
+
if (mode === 'file') {
|
|
2426
|
+
return `❌ 当前会话无文件快照,无法回退文件${fileResult.error ? `\n原因: ${fileResult.error}` : ''}`;
|
|
2427
|
+
}
|
|
2428
|
+
results.push(`⚠️ 文件回退失败${fileResult.error ? `: ${fileResult.error}` : '(无文件快照)'}`);
|
|
2429
|
+
}
|
|
2430
|
+
else {
|
|
2431
|
+
const detail = fileResult.filesChanged
|
|
2432
|
+
? `(恢复了 ${fileResult.filesChanged.length} 个文件)`
|
|
2433
|
+
: '';
|
|
2434
|
+
results.push(`✅ 已恢复文件到第 ${turnNum} 轮之前的状态${detail}`);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
// 对话回退(延迟执行 — 下次发消息时生效)
|
|
2438
|
+
if (mode === 'chat' || mode === 'all') {
|
|
2439
|
+
if (keepTarget) {
|
|
2440
|
+
const meta = { ...(session.metadata || {}), resumeAt: keepTarget.assistantUuid };
|
|
2441
|
+
await this.sessionManager.updateSession(session.id, { metadata: meta });
|
|
2442
|
+
}
|
|
2443
|
+
else {
|
|
2444
|
+
// N=1:撤销全部对话,清空 session 从头开始
|
|
2445
|
+
const meta = { ...(session.metadata || {}) };
|
|
2446
|
+
delete meta.resumeAt;
|
|
2447
|
+
await this.sessionManager.updateSession(session.id, {
|
|
2448
|
+
metadata: meta,
|
|
2449
|
+
agentSessionId: null,
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
const discarded = turns.length - turnNum + 1;
|
|
2453
|
+
const keepDesc = keepTarget
|
|
2454
|
+
? `回退到第 ${turnNum - 1} 轮:"${keepTarget.userContent}"`
|
|
2455
|
+
: '已清空全部对话历史';
|
|
2456
|
+
results.push(`✅ 已撤销第 ${turnNum} 轮${discarded > 1 ? `及后续共 ${discarded} 轮` : ''}`, keepTarget ? `下次发言将从第 ${turnNum - 1} 轮继续` : '下次发言将开始全新对话');
|
|
2457
|
+
}
|
|
2458
|
+
this.eventBus.publish({
|
|
2459
|
+
type: 'session:rewind',
|
|
2460
|
+
sessionId: session.id,
|
|
2461
|
+
turnNum,
|
|
2462
|
+
mode,
|
|
2463
|
+
});
|
|
2464
|
+
return results.join('\n');
|
|
2465
|
+
}
|
|
2466
|
+
catch (error) {
|
|
2467
|
+
logger.error('[CommandHandler] Rewind failed:', error);
|
|
2468
|
+
return `❌ 回退失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
buildTurnList(messages) {
|
|
2472
|
+
const turns = [];
|
|
2473
|
+
let pendingUser = null;
|
|
2474
|
+
for (const msg of messages) {
|
|
2475
|
+
if (msg.type === 'user') {
|
|
2476
|
+
const m = msg.message;
|
|
2477
|
+
if (Array.isArray(m?.content) && m.content.every((c) => c.type === 'tool_result')) {
|
|
2478
|
+
continue;
|
|
2479
|
+
}
|
|
2480
|
+
const content = this.extractUserContent(msg.message);
|
|
2481
|
+
if (content) {
|
|
2482
|
+
pendingUser = { content, uuid: msg.uuid };
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
else if (msg.type === 'assistant' && pendingUser) {
|
|
2486
|
+
turns.push({
|
|
2487
|
+
index: turns.length + 1,
|
|
2488
|
+
userContent: pendingUser.content,
|
|
2489
|
+
userUuid: pendingUser.uuid,
|
|
2490
|
+
assistantUuid: msg.uuid,
|
|
2491
|
+
});
|
|
2492
|
+
pendingUser = null;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
return turns;
|
|
2496
|
+
}
|
|
2497
|
+
// ── Agent Ctl ──
|
|
2498
|
+
static CTL_COMMANDS = [
|
|
2499
|
+
'/help', '/status', '/check',
|
|
2500
|
+
'/model', '/effort', '/perm',
|
|
2501
|
+
'/compact', '/activity', '/send', '/restart', '/agentmd',
|
|
2502
|
+
];
|
|
2503
|
+
/**
|
|
2504
|
+
* Agent ctl 入口:通过 IPC 接收 Agent 自主管理指令
|
|
2505
|
+
* 复用现有 slash cmd 逻辑,权限继承 session 用户角色
|
|
2506
|
+
*/
|
|
2507
|
+
async handleCtl(cmd, sessionId) {
|
|
2508
|
+
// 1. 白名单检查
|
|
2509
|
+
const inputCmd = cmd.split(' ')[0];
|
|
2510
|
+
if (!CommandHandler.CTL_COMMANDS.includes(inputCmd)) {
|
|
2511
|
+
return { ok: false, error: `不允许的指令: ${inputCmd}` };
|
|
2512
|
+
}
|
|
2513
|
+
// 2. 通过 sessionId 查 session
|
|
2514
|
+
const session = await this.sessionManager.getSessionById(sessionId);
|
|
2515
|
+
if (!session) {
|
|
2516
|
+
return { ok: false, error: '无效的 session' };
|
|
2517
|
+
}
|
|
2518
|
+
// 3. 从 session.metadata.peerId 获取 userId(用于权限判断)
|
|
2519
|
+
const userId = session.metadata?.peerId;
|
|
2520
|
+
// 4. send 路径限制:只允许 projectPath 下的文件
|
|
2521
|
+
if (cmd.startsWith('/send')) {
|
|
2522
|
+
const sendArgs = cmd.slice(5).trim();
|
|
2523
|
+
const parts = sendArgs.split(/\s+/);
|
|
2524
|
+
const filePath = parts[parts.length - 1];
|
|
2525
|
+
if (filePath) {
|
|
2526
|
+
const resolved = path.resolve(session.projectPath, filePath);
|
|
2527
|
+
if (!resolved.startsWith(session.projectPath)) {
|
|
2528
|
+
return { ok: false, error: '路径越界:只能发送项目目录下的文件' };
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
// 5. 调用现有 handle(),不传 sendMessage 回调(结果直接返回)
|
|
2533
|
+
try {
|
|
2534
|
+
const result = await this.handle(cmd, session.channel, session.channelId, undefined, // 不发送消息
|
|
2535
|
+
userId);
|
|
2536
|
+
return { ok: true, result: result ?? '(无输出)' };
|
|
2537
|
+
}
|
|
2538
|
+
catch (err) {
|
|
2539
|
+
return { ok: false, error: err.message };
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
extractUserContent(message) {
|
|
2543
|
+
const m = message;
|
|
2544
|
+
let text = '';
|
|
2545
|
+
if (typeof m?.content === 'string') {
|
|
2546
|
+
text = m.content;
|
|
2547
|
+
}
|
|
2548
|
+
else if (Array.isArray(m?.content)) {
|
|
2549
|
+
text = m.content.filter((b) => b.type === 'text').map((b) => b.text).join(' ');
|
|
2550
|
+
}
|
|
2551
|
+
text = text.trim().replace(/\s+/g, ' ');
|
|
2552
|
+
if (!text)
|
|
2553
|
+
return '';
|
|
2554
|
+
return text.length > 50 ? text.substring(0, 50) + '…' : text;
|
|
2555
|
+
}
|
|
2206
2556
|
}
|