evolclaw 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -14
- package/dist/agents/claude-runner.js +224 -23
- package/dist/agents/codex-runner.js +2 -8
- package/dist/agents/gemini-runner.js +1 -8
- package/dist/channels/aun.js +492 -82
- package/dist/channels/dingtalk.js +506 -0
- package/dist/channels/feishu.js +31 -231
- package/dist/channels/qqbot.js +391 -0
- package/dist/channels/wechat.js +36 -38
- package/dist/channels/wecom.js +549 -0
- package/dist/cli.js +170 -17
- package/dist/config.js +98 -2
- package/dist/core/command-handler.js +511 -146
- package/dist/core/message/message-bridge.js +10 -6
- package/dist/core/message/message-processor.js +176 -78
- package/dist/core/message/message-queue.js +48 -0
- package/dist/core/message/stream-flusher.js +2 -2
- package/dist/core/session/session-manager.js +25 -3
- package/dist/index.js +55 -21
- package/dist/ipc.js +14 -4
- package/dist/templates/skills.md +64 -0
- package/dist/utils/error-dict.js +63 -0
- package/dist/utils/error-utils.js +156 -56
- package/dist/utils/format.js +32 -0
- package/dist/utils/init-channel.js +734 -8
- package/dist/utils/init.js +33 -2
- package/dist/utils/media-cache.js +2 -0
- package/dist/utils/stats-collector.js +0 -8
- package/package.json +9 -3
|
@@ -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,33 +316,44 @@ 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 = [];
|
|
325
|
+
if (!isAdmin && chatType === 'group') {
|
|
326
|
+
return [
|
|
327
|
+
{
|
|
328
|
+
group: '其他',
|
|
329
|
+
commands: [
|
|
330
|
+
{ cmd: '/status', label: '显示会话状态' },
|
|
331
|
+
{ cmd: '/check', label: '检查渠道健康' },
|
|
332
|
+
{ cmd: '/help', label: '显示帮助信息' },
|
|
333
|
+
]
|
|
334
|
+
}
|
|
335
|
+
];
|
|
336
|
+
}
|
|
329
337
|
if (isAdmin) {
|
|
330
338
|
items.push({
|
|
331
339
|
group: '项目管理',
|
|
332
340
|
commands: [
|
|
333
341
|
{ cmd: '/pwd', label: '显示当前项目路径' },
|
|
334
|
-
{ cmd: '/
|
|
335
|
-
{ cmd: '/
|
|
336
|
-
{ cmd: '/bind', args: '<path>', label: '绑定新项目目录' },
|
|
342
|
+
{ cmd: '/p', args: '[name|path]', label: '列出或切换项目' },
|
|
343
|
+
...(isOwner ? [{ cmd: '/bind', args: '<path>', label: '绑定新项目目录' }] : []),
|
|
337
344
|
]
|
|
338
345
|
});
|
|
339
346
|
}
|
|
340
347
|
items.push({
|
|
341
348
|
group: '会话管理',
|
|
342
349
|
commands: [
|
|
343
|
-
{ cmd: '/new', args: '[name]', label: '
|
|
344
|
-
{ cmd: '/
|
|
345
|
-
{ cmd: '/slist', args: 'cli', label: '列出 CLI 会话(未导入的)' },
|
|
346
|
-
{ cmd: '/s', args: '<name|index|uuid>', label: '切换到指定会话' },
|
|
350
|
+
{ cmd: '/new', args: '[name]', label: '创建新会话(清空历史请用此命令)' },
|
|
351
|
+
{ cmd: '/s', args: '[cli|name|index|uuid]', label: '列出或切换会话(cli 查看未导入的 CLI 会话)' },
|
|
347
352
|
{ cmd: '/name', args: '<name>', label: '重命名当前会话' },
|
|
348
353
|
{ cmd: '/del', args: '<name>', label: '删除指定会话' },
|
|
349
354
|
...(isAdmin ? [
|
|
350
355
|
{ cmd: '/fork', args: '[name]', label: '分支当前会话' },
|
|
351
|
-
{ cmd: '/
|
|
356
|
+
{ cmd: '/rewind', args: '[N] [chat|file|all]', label: '查看历史/撤销指定轮次 (别名: /rw)' },
|
|
352
357
|
{ cmd: '/compact', label: '压缩会话上下文' },
|
|
353
358
|
] : []),
|
|
354
359
|
]
|
|
@@ -365,7 +370,7 @@ export class CommandHandler {
|
|
|
365
370
|
items.push({
|
|
366
371
|
group: '权限管理',
|
|
367
372
|
commands: [
|
|
368
|
-
{ cmd: '/perm', args: '[mode|allow|always|deny]', label: '权限模式管理' },
|
|
373
|
+
{ cmd: '/perm', args: isOwner ? '[mode|allow|always|deny]' : '[allow|always|deny]', label: '权限模式管理' },
|
|
369
374
|
]
|
|
370
375
|
});
|
|
371
376
|
items.push({
|
|
@@ -373,11 +378,16 @@ export class CommandHandler {
|
|
|
373
378
|
commands: [
|
|
374
379
|
{ cmd: '/status', label: '显示会话状态' },
|
|
375
380
|
{ cmd: '/stop', label: '中断当前任务' },
|
|
376
|
-
{ cmd: '/
|
|
377
|
-
{ cmd: '/
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
+
] : []),
|
|
381
391
|
]
|
|
382
392
|
});
|
|
383
393
|
}
|
|
@@ -386,6 +396,7 @@ export class CommandHandler {
|
|
|
386
396
|
group: '其他',
|
|
387
397
|
commands: [
|
|
388
398
|
{ cmd: '/status', label: '显示会话状态' },
|
|
399
|
+
{ cmd: '/check', label: '检查渠道健康' },
|
|
389
400
|
]
|
|
390
401
|
});
|
|
391
402
|
}
|
|
@@ -433,16 +444,23 @@ export class CommandHandler {
|
|
|
433
444
|
}
|
|
434
445
|
}
|
|
435
446
|
// 权限检查:区分用户级命令和管理级命令
|
|
436
|
-
const
|
|
447
|
+
const isOwner = identity.role === 'owner';
|
|
448
|
+
const isAdmin = identity.role === 'owner' || identity.role === 'admin';
|
|
449
|
+
const activeChatType = activeSession?.chatType || 'private';
|
|
437
450
|
if (normalizedContent.startsWith('/')) {
|
|
438
|
-
const
|
|
451
|
+
const guestGroupCommands = ['/status', '/help', '/check'];
|
|
452
|
+
const userCommands = activeChatType === 'group' && !isAdmin
|
|
453
|
+
? guestGroupCommands
|
|
454
|
+
: ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s ', '/check'];
|
|
439
455
|
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
440
456
|
if (!isUserCommand && !isAdmin) {
|
|
441
|
-
return
|
|
457
|
+
return activeChatType === 'group'
|
|
458
|
+
? '❌ 无权限:当前群聊仅支持 /status 和 /help'
|
|
459
|
+
: '❌ 无权限:此命令仅限管理员使用';
|
|
442
460
|
}
|
|
443
461
|
}
|
|
444
462
|
// 空闲检查:某些命令需要等待当前会话空闲
|
|
445
|
-
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'];
|
|
446
464
|
if (requiresIdle.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '))) {
|
|
447
465
|
if (threadId) {
|
|
448
466
|
// 话题中:检查话题 session 是否在处理(不创建)
|
|
@@ -480,41 +498,50 @@ export class CommandHandler {
|
|
|
480
498
|
return undefined;
|
|
481
499
|
// /help 命令不需要会话
|
|
482
500
|
if (normalizedContent === '/help') {
|
|
501
|
+
if (!isAdmin && activeChatType === 'group') {
|
|
502
|
+
const lines = [
|
|
503
|
+
'可用命令:',
|
|
504
|
+
'',
|
|
505
|
+
'其他:',
|
|
506
|
+
' /status - 显示会话状态',
|
|
507
|
+
' /check - 检查渠道健康',
|
|
508
|
+
' /help - 显示此帮助信息',
|
|
509
|
+
];
|
|
510
|
+
return lines.join('\n');
|
|
511
|
+
}
|
|
483
512
|
if (!isAdmin) {
|
|
484
513
|
const lines = [
|
|
485
514
|
'可用命令:',
|
|
486
515
|
'',
|
|
487
516
|
'🔄 会话管理:',
|
|
488
|
-
' /new [名称] -
|
|
489
|
-
' /
|
|
490
|
-
' /
|
|
491
|
-
' /s, /session <名称|序号|uuid> - 切换到指定会话',
|
|
492
|
-
' /name, /rename <新名称> - 重命名当前会话',
|
|
517
|
+
' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
|
|
518
|
+
' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
|
|
519
|
+
' /name <新名称> - 重命名当前会话',
|
|
493
520
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
494
521
|
' /status - 显示会话状态',
|
|
522
|
+
' /check - 检查渠道健康',
|
|
495
523
|
'',
|
|
496
524
|
'❓ 帮助:',
|
|
497
525
|
' /help - 显示此帮助信息',
|
|
498
526
|
];
|
|
499
527
|
return lines.join('\n');
|
|
500
528
|
}
|
|
529
|
+
// admin+ 基础命令
|
|
501
530
|
const lines = [
|
|
502
531
|
'可用命令:',
|
|
503
532
|
'',
|
|
504
533
|
'📁 项目管理:',
|
|
505
534
|
' /pwd - 显示当前项目路径',
|
|
506
|
-
' /
|
|
507
|
-
' /
|
|
508
|
-
' /bind <path> - 绑定新项目目录',
|
|
535
|
+
' /p [name|path] - 列出或切换项目',
|
|
536
|
+
...(isOwner ? [' /bind <path> - 绑定新项目目录'] : []),
|
|
509
537
|
'',
|
|
510
538
|
'🔄 会话管理:',
|
|
511
|
-
' /new [名称] -
|
|
512
|
-
' /
|
|
513
|
-
' /
|
|
514
|
-
' /name, /rename <新名称> - 重命名当前会话',
|
|
539
|
+
' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
|
|
540
|
+
' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
|
|
541
|
+
' /name <新名称> - 重命名当前会话',
|
|
515
542
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
516
543
|
' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
|
|
517
|
-
' /
|
|
544
|
+
' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
|
|
518
545
|
' /compact - 压缩会话上下文(减少 token 用量)',
|
|
519
546
|
'',
|
|
520
547
|
'🤖 Agent 与模型:',
|
|
@@ -524,16 +551,22 @@ export class CommandHandler {
|
|
|
524
551
|
'',
|
|
525
552
|
'🔐 权限管理:',
|
|
526
553
|
' /perm - 查看当前权限模式',
|
|
527
|
-
' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式',
|
|
554
|
+
...(isOwner ? [' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式'] : []),
|
|
528
555
|
' /perm allow|always|deny - 审批权限请求',
|
|
529
556
|
'',
|
|
530
557
|
'🛠️ 运维:',
|
|
531
558
|
' /status - 显示会话状态',
|
|
532
559
|
' /stop - 中断当前任务',
|
|
533
|
-
' /
|
|
534
|
-
' /
|
|
535
|
-
|
|
536
|
-
|
|
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
|
+
] : []),
|
|
537
570
|
'',
|
|
538
571
|
'❓ 帮助:',
|
|
539
572
|
' /help - 显示此帮助信息',
|
|
@@ -554,7 +587,8 @@ export class CommandHandler {
|
|
|
554
587
|
if (!hasPermissionController(permAgent)) {
|
|
555
588
|
return '❌ 权限控制不可用';
|
|
556
589
|
}
|
|
557
|
-
const
|
|
590
|
+
const defaultPermMode = identity.role === 'owner' ? 'bypass' : 'readonly';
|
|
591
|
+
const currentMode = permSession.metadata?.permissionMode ?? defaultPermMode;
|
|
558
592
|
const modes = permAgent.listModes();
|
|
559
593
|
// 尝试发送交互卡片
|
|
560
594
|
if (this.interactionRouter) {
|
|
@@ -568,7 +602,7 @@ export class CommandHandler {
|
|
|
568
602
|
kind: {
|
|
569
603
|
kind: 'action',
|
|
570
604
|
title: '🔐 权限模式',
|
|
571
|
-
body: availableModes.map(m => `${m.key === currentMode ? '
|
|
605
|
+
body: availableModes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.nameZh}) - ${m.description}`).join('\n'),
|
|
572
606
|
buttons: availableModes.map(m => ({
|
|
573
607
|
key: m.key,
|
|
574
608
|
label: m.key === currentMode ? `✓ ${m.key}` : m.key,
|
|
@@ -596,7 +630,7 @@ export class CommandHandler {
|
|
|
596
630
|
}
|
|
597
631
|
// 降级:文本
|
|
598
632
|
const modeList = modes.map(m => {
|
|
599
|
-
const prefix = m.key === currentMode ? '
|
|
633
|
+
const prefix = m.key === currentMode ? '✓' : ' ';
|
|
600
634
|
const suffix = m.available ? '' : ' ⚠️ 不可用';
|
|
601
635
|
return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
|
|
602
636
|
}).join('\n');
|
|
@@ -636,9 +670,9 @@ export class CommandHandler {
|
|
|
636
670
|
if (!matched.available) {
|
|
637
671
|
return `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}`;
|
|
638
672
|
}
|
|
639
|
-
// guest
|
|
640
|
-
if (
|
|
641
|
-
return '❌
|
|
673
|
+
// guest 和 admin 用户不能切换权限模式(仅 owner)
|
|
674
|
+
if (!isOwner) {
|
|
675
|
+
return '❌ 权限模式切换仅限 owner';
|
|
642
676
|
}
|
|
643
677
|
const metadata = permSession.metadata || {};
|
|
644
678
|
metadata.permissionMode = arg;
|
|
@@ -655,8 +689,9 @@ export class CommandHandler {
|
|
|
655
689
|
return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny`;
|
|
656
690
|
}
|
|
657
691
|
// /agent 命令:查看或切换 Agent 后端
|
|
658
|
-
if (normalizedContent.startsWith('/agent')) {
|
|
659
|
-
|
|
692
|
+
if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ')) {
|
|
693
|
+
// 群聊中 owner only,私聊中 admin+
|
|
694
|
+
if (activeChatType === 'group' ? !isOwner : !isAdmin)
|
|
660
695
|
return '❌ 无权限:此命令仅限管理员使用';
|
|
661
696
|
const args = normalizedContent.slice(6).trim();
|
|
662
697
|
const available = [...this.agentMap.keys()];
|
|
@@ -774,7 +809,7 @@ export class CommandHandler {
|
|
|
774
809
|
return null;
|
|
775
810
|
}
|
|
776
811
|
// 降级:文本
|
|
777
|
-
const modelList = models.map((m) =>
|
|
812
|
+
const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
|
|
778
813
|
const effortHint = efforts.length > 0
|
|
779
814
|
? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
|
|
780
815
|
: '';
|
|
@@ -798,7 +833,7 @@ export class CommandHandler {
|
|
|
798
833
|
newModel = arg;
|
|
799
834
|
}
|
|
800
835
|
else {
|
|
801
|
-
const modelList = models.map((m) =>
|
|
836
|
+
const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
|
|
802
837
|
const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
|
|
803
838
|
return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}`;
|
|
804
839
|
}
|
|
@@ -964,8 +999,9 @@ export class CommandHandler {
|
|
|
964
999
|
}
|
|
965
1000
|
// 降级:文本
|
|
966
1001
|
const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
|
|
967
|
-
const
|
|
968
|
-
|
|
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>`;
|
|
969
1005
|
}
|
|
970
1006
|
// /effort auto:恢复 SDK 默认
|
|
971
1007
|
if (args === 'auto') {
|
|
@@ -1034,6 +1070,151 @@ export class CommandHandler {
|
|
|
1034
1070
|
}
|
|
1035
1071
|
return `✓ 推理强度: ${newEffort}`;
|
|
1036
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
|
+
}
|
|
1037
1218
|
// /stop 命令:中断当前任务
|
|
1038
1219
|
if (normalizedContent === '/stop') {
|
|
1039
1220
|
const stopResult = await this.ensureSession(channel, channelId, threadId);
|
|
@@ -1180,22 +1361,11 @@ export class CommandHandler {
|
|
|
1180
1361
|
if (health.consecutiveErrors > 0) {
|
|
1181
1362
|
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
1182
1363
|
}
|
|
1183
|
-
if (health.safeMode) {
|
|
1184
|
-
lines.push(`安全模式: 是 ⚠️`);
|
|
1185
|
-
}
|
|
1186
1364
|
lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
|
|
1187
1365
|
}
|
|
1188
1366
|
else {
|
|
1189
1367
|
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
1190
1368
|
}
|
|
1191
|
-
if (health.safeMode) {
|
|
1192
|
-
lines.push('');
|
|
1193
|
-
lines.push('⚠️ 当前处于安全模式(历史上下文已禁用)');
|
|
1194
|
-
lines.push('');
|
|
1195
|
-
lines.push('退出方式:');
|
|
1196
|
-
lines.push('1. /repair - 检查并修复会话(推荐,保留历史)');
|
|
1197
|
-
lines.push('2. /new [名称] - 创建新会话(清空历史)');
|
|
1198
|
-
}
|
|
1199
1369
|
if (health.lastError) {
|
|
1200
1370
|
lines.push('');
|
|
1201
1371
|
lines.push(`最后错误: ${health.lastErrorType || 'unknown'}`);
|
|
@@ -1229,31 +1399,12 @@ export class CommandHandler {
|
|
|
1229
1399
|
await agent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
|
|
1230
1400
|
await agent.closeSession(session.id);
|
|
1231
1401
|
}
|
|
1232
|
-
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /
|
|
1402
|
+
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /s 查看`;
|
|
1233
1403
|
}
|
|
1234
|
-
// /check
|
|
1404
|
+
// /check 命令:检查渠道状态(guest 可用,详情仅 admin)/ 重连指定渠道(admin only)
|
|
1235
1405
|
if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
|
|
1236
|
-
if (!isAdmin)
|
|
1237
|
-
return '❌ 无权限:此命令仅限管理员使用';
|
|
1238
1406
|
const subCmd = normalizedContent.slice('/check'.length).trim();
|
|
1239
|
-
//
|
|
1240
|
-
if (subCmd.startsWith('rty')) {
|
|
1241
|
-
const target = subCmd.slice('rty'.length).trim();
|
|
1242
|
-
if (!target) {
|
|
1243
|
-
return '❌ 请指定渠道名称,例如:/check rty feishu';
|
|
1244
|
-
}
|
|
1245
|
-
const ch = this.channelObjects.get(target);
|
|
1246
|
-
if (!ch) {
|
|
1247
|
-
const available = [...this.channelObjects.keys()].join(', ') || '无';
|
|
1248
|
-
return `❌ 未找到渠道 "${target}",可用渠道:${available}`;
|
|
1249
|
-
}
|
|
1250
|
-
if (!ch.reconnect) {
|
|
1251
|
-
return `❌ 渠道 "${target}" 不支持重连`;
|
|
1252
|
-
}
|
|
1253
|
-
const result = await ch.reconnect();
|
|
1254
|
-
return `🔄 ${target} 重连: ${result}`;
|
|
1255
|
-
}
|
|
1256
|
-
// Default: show full system health check
|
|
1407
|
+
// Default: show system health check (non-admin 仅看摘要)
|
|
1257
1408
|
const lines = ['📡 渠道状态:'];
|
|
1258
1409
|
// Group by channelType
|
|
1259
1410
|
const groups = new Map();
|
|
@@ -1272,6 +1423,13 @@ export class CommandHandler {
|
|
|
1272
1423
|
groups.set(type, []);
|
|
1273
1424
|
groups.get(type).push({ name, status });
|
|
1274
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
|
+
}
|
|
1275
1433
|
for (const [type, instances] of groups) {
|
|
1276
1434
|
if (instances.length === 1) {
|
|
1277
1435
|
lines.push(` ${instances[0].name}: ${instances[0].status}`);
|
|
@@ -1291,7 +1449,6 @@ export class CommandHandler {
|
|
|
1291
1449
|
? this.statsCollector.getSnapshot().uptimeMs
|
|
1292
1450
|
: process.uptime() * 1000;
|
|
1293
1451
|
lines.push(` 运行时间: ${this.formatUptime(uptimeMs)}`);
|
|
1294
|
-
lines.push(` 当前安全模式会话: ${this.sessionManager.getSafeModeSessionCount()}`);
|
|
1295
1452
|
// 近 1 小时统计
|
|
1296
1453
|
if (this.statsCollector) {
|
|
1297
1454
|
const snap = this.statsCollector.getSnapshot();
|
|
@@ -1311,16 +1468,34 @@ export class CommandHandler {
|
|
|
1311
1468
|
lines.push(` 工具失败: ${h.toolErrors} (${toolBreakdown})`);
|
|
1312
1469
|
}
|
|
1313
1470
|
lines.push(` 被中断: ${h.interrupts}`);
|
|
1314
|
-
lines.push(` 进入安全模式: ${h.safeModeEntries}`);
|
|
1315
1471
|
if (h.completed > 0) {
|
|
1316
1472
|
lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
|
|
1317
1473
|
}
|
|
1318
1474
|
}
|
|
1319
|
-
lines.push('', '💡 /check rty <channel> — 重连指定渠道');
|
|
1320
1475
|
return lines.join('\n');
|
|
1321
1476
|
}
|
|
1322
|
-
// /restart
|
|
1323
|
-
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 使用';
|
|
1324
1499
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
1325
1500
|
const sessionsWithMessages = allSessions
|
|
1326
1501
|
.filter(s => this.messageCache.hasMessages(s.id))
|
|
@@ -1389,8 +1564,10 @@ export class CommandHandler {
|
|
|
1389
1564
|
}
|
|
1390
1565
|
return `当前项目: ${session.projectPath}`;
|
|
1391
1566
|
}
|
|
1392
|
-
// /send 命令:发送项目内文件,支持 /send path 和 /send channel path
|
|
1567
|
+
// /send 命令:发送项目内文件,支持 /send path 和 /send channel path(owner only)
|
|
1393
1568
|
if (normalizedContent.startsWith('/send')) {
|
|
1569
|
+
if (!isOwner)
|
|
1570
|
+
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1394
1571
|
// 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
|
|
1395
1572
|
// 还原: 将 [text](url) 替换为 text
|
|
1396
1573
|
const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
@@ -1562,7 +1739,7 @@ export class CommandHandler {
|
|
|
1562
1739
|
}));
|
|
1563
1740
|
const bodyLines = entries.map(e => {
|
|
1564
1741
|
const status = buildStatusText(e);
|
|
1565
|
-
const prefix = e.isCurrent ? '
|
|
1742
|
+
const prefix = e.isCurrent ? '✓' : '•';
|
|
1566
1743
|
return `${prefix} **${e.name}** (${e.projectPath}) ${status}`;
|
|
1567
1744
|
});
|
|
1568
1745
|
const interaction = {
|
|
@@ -1602,6 +1779,7 @@ export class CommandHandler {
|
|
|
1602
1779
|
const prefix = entry.isCurrent ? ' ✓' : ' ';
|
|
1603
1780
|
lines.push(`${prefix} ${entry.name} (${entry.projectPath}) - ${buildStatusText(entry)}`);
|
|
1604
1781
|
}
|
|
1782
|
+
lines.push('', '提示: 使用 /p <名称> 切换项目');
|
|
1605
1783
|
return lines.join('\n');
|
|
1606
1784
|
}
|
|
1607
1785
|
// /project(无参数):直接复用 /plist 逻辑(含卡片交互)
|
|
@@ -1641,7 +1819,7 @@ export class CommandHandler {
|
|
|
1641
1819
|
else {
|
|
1642
1820
|
projectPath = this.projects[arg];
|
|
1643
1821
|
if (!projectPath) {
|
|
1644
|
-
return `❌ 项目 "${arg}" 不存在\n提示: 使用 /
|
|
1822
|
+
return `❌ 项目 "${arg}" 不存在\n提示: 使用 /p 查看可用项目`;
|
|
1645
1823
|
}
|
|
1646
1824
|
projectName = arg;
|
|
1647
1825
|
}
|
|
@@ -1700,8 +1878,10 @@ export class CommandHandler {
|
|
|
1700
1878
|
}
|
|
1701
1879
|
return response;
|
|
1702
1880
|
}
|
|
1703
|
-
// /bind
|
|
1881
|
+
// /bind 命令:持久化项目到配置(不切换)(owner only)
|
|
1704
1882
|
if (normalizedContent.startsWith('/bind ')) {
|
|
1883
|
+
if (!isOwner)
|
|
1884
|
+
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1705
1885
|
const projectPath = normalizedContent.slice(6).trim();
|
|
1706
1886
|
if (!projectPath)
|
|
1707
1887
|
return '用法: /bind <路径>';
|
|
@@ -1746,7 +1926,7 @@ export class CommandHandler {
|
|
|
1746
1926
|
请先执行以下操作之一:
|
|
1747
1927
|
1. 发送任意消息 - 自动创建新会话
|
|
1748
1928
|
2. /new [名称] - 创建命名会话
|
|
1749
|
-
3. /
|
|
1929
|
+
3. /p <项目> - 切换到指定项目`;
|
|
1750
1930
|
}
|
|
1751
1931
|
const showCliOnly = normalizedContent === '/slist cli';
|
|
1752
1932
|
// /slist cli — 仅显示 CLI 会话
|
|
@@ -1876,7 +2056,7 @@ export class CommandHandler {
|
|
|
1876
2056
|
};
|
|
1877
2057
|
});
|
|
1878
2058
|
const bodyLines = displaySessions.map(ds => {
|
|
1879
|
-
const prefix = ds.isActive ? '
|
|
2059
|
+
const prefix = ds.isActive ? '✓' : '•';
|
|
1880
2060
|
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
1881
2061
|
const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
|
|
1882
2062
|
const fileMark = ds.fileMissing ? '❌ ' : '';
|
|
@@ -1940,13 +2120,17 @@ export class CommandHandler {
|
|
|
1940
2120
|
lines.push('');
|
|
1941
2121
|
}
|
|
1942
2122
|
lines.push('使用 /s <序号、name或8位uuid> 切换会话');
|
|
1943
|
-
lines.push('使用 /
|
|
2123
|
+
lines.push('使用 /s cli 查看 CLI 会话');
|
|
1944
2124
|
return lines.join('\n');
|
|
1945
2125
|
}
|
|
1946
2126
|
// /session(无参数):直接复用 /slist 逻辑(含卡片交互)
|
|
1947
2127
|
if (normalizedContent === '/session') {
|
|
1948
2128
|
return this.handle('/slist', channel, channelId, undefined, userId, threadId);
|
|
1949
2129
|
}
|
|
2130
|
+
// /session cli(= /s cli):列出未导入的 CLI 会话
|
|
2131
|
+
if (normalizedContent === '/session cli') {
|
|
2132
|
+
return this.handle('/slist cli', channel, channelId, undefined, userId, threadId);
|
|
2133
|
+
}
|
|
1950
2134
|
// /session 或 /s 命令:切换会话
|
|
1951
2135
|
if (normalizedContent.startsWith('/session ')) {
|
|
1952
2136
|
const sessionName = normalizedContent.slice(9).trim();
|
|
@@ -1967,7 +2151,7 @@ export class CommandHandler {
|
|
|
1967
2151
|
targetSession = visibleSessions[idx - 1];
|
|
1968
2152
|
}
|
|
1969
2153
|
else {
|
|
1970
|
-
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /
|
|
2154
|
+
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
|
|
1971
2155
|
}
|
|
1972
2156
|
}
|
|
1973
2157
|
if (!targetSession && sessionName.length === 8) {
|
|
@@ -1992,7 +2176,7 @@ export class CommandHandler {
|
|
|
1992
2176
|
}
|
|
1993
2177
|
}
|
|
1994
2178
|
if (!targetSession) {
|
|
1995
|
-
return `❌ 会话不存在: ${sessionName}\n使用 /
|
|
2179
|
+
return `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话`;
|
|
1996
2180
|
}
|
|
1997
2181
|
const lastInput = targetSession.agentSessionId
|
|
1998
2182
|
? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId, targetSession.agentId)
|
|
@@ -2071,14 +2255,14 @@ export class CommandHandler {
|
|
|
2071
2255
|
targetSession = visibleSessions[idx - 1];
|
|
2072
2256
|
}
|
|
2073
2257
|
else {
|
|
2074
|
-
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /
|
|
2258
|
+
return `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话`;
|
|
2075
2259
|
}
|
|
2076
2260
|
}
|
|
2077
2261
|
if (!targetSession && sessionName.length === 8) {
|
|
2078
2262
|
targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
|
|
2079
2263
|
}
|
|
2080
2264
|
if (!targetSession) {
|
|
2081
|
-
return `❌ 会话不存在: ${sessionName}\n使用 /
|
|
2265
|
+
return `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话`;
|
|
2082
2266
|
}
|
|
2083
2267
|
if (targetSession.id === session.id) {
|
|
2084
2268
|
return `❌ 无法删除当前活跃会话\n请先切换到其他会话`;
|
|
@@ -2109,38 +2293,66 @@ export class CommandHandler {
|
|
|
2109
2293
|
const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
|
|
2110
2294
|
const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
|
|
2111
2295
|
this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
|
|
2112
|
-
return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /
|
|
2296
|
+
return `✅ 会话已分支: ${newSession.name}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话`;
|
|
2113
2297
|
}
|
|
2114
2298
|
catch (error) {
|
|
2115
2299
|
logger.error('[CommandHandler] Fork session failed:', error);
|
|
2116
2300
|
return `❌ 会话分支失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
|
2117
2301
|
}
|
|
2118
2302
|
}
|
|
2119
|
-
// /
|
|
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 命令:检查并修复会话文件
|
|
2120
2338
|
if (normalizedContent === '/repair') {
|
|
2121
2339
|
const repairResult = await this.ensureSession(channel, channelId, threadId);
|
|
2122
2340
|
if ('error' in repairResult)
|
|
2123
2341
|
return repairResult.error;
|
|
2124
2342
|
const { session: repairSession } = repairResult;
|
|
2125
|
-
const health = await this.sessionManager.getHealthStatus(repairSession.id);
|
|
2126
|
-
if (!health.safeMode) {
|
|
2127
|
-
return `当前不在安全模式,无需修复\n\n如需进入安全模式,请使用 /safe`;
|
|
2128
|
-
}
|
|
2129
2343
|
const repairAgent = this.getAgent(repairSession.agentId);
|
|
2130
2344
|
const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
|
|
2131
2345
|
try {
|
|
2132
2346
|
if (!repairSession.agentSessionId) {
|
|
2133
2347
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2134
|
-
|
|
2135
|
-
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
2348
|
+
return `✓ 修复完成\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器`;
|
|
2136
2349
|
}
|
|
2137
2350
|
// 通过 agent 定位 session 文件
|
|
2138
2351
|
const sessionFile = repairAgent.resolveSessionFile?.(repairSession.agentSessionId, repairSession.projectPath) ?? null;
|
|
2139
2352
|
if (!sessionFile) {
|
|
2140
2353
|
// 文件不存在(已被删除或从未创建),直接重置
|
|
2141
2354
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2142
|
-
|
|
2143
|
-
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
2355
|
+
return `✓ 修复完成\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器`;
|
|
2144
2356
|
}
|
|
2145
2357
|
const healthCheck = await checkSessionFile(sessionFile);
|
|
2146
2358
|
if (healthCheck.corrupt) {
|
|
@@ -2150,42 +2362,195 @@ export class CommandHandler {
|
|
|
2150
2362
|
await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
|
|
2151
2363
|
repairAgent.updateSessionId(repairSession.id, '');
|
|
2152
2364
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2153
|
-
|
|
2154
|
-
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}`;
|
|
2155
2366
|
}
|
|
2156
2367
|
if (healthCheck.issues.length > 0) {
|
|
2157
2368
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2158
|
-
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
2159
2369
|
return `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n已重置异常计数器,可继续使用当前会话。`;
|
|
2160
2370
|
}
|
|
2161
2371
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2162
|
-
|
|
2163
|
-
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
2372
|
+
return `✓ 修复完成\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器`;
|
|
2164
2373
|
}
|
|
2165
2374
|
catch (error) {
|
|
2166
2375
|
logger.error('[Repair] Failed:', error);
|
|
2167
2376
|
return `❌ 修复失败: ${error.message}`;
|
|
2168
2377
|
}
|
|
2169
2378
|
}
|
|
2170
|
-
// /safe
|
|
2379
|
+
// /safe 命令:安全模式已禁用
|
|
2171
2380
|
if (normalizedContent === '/safe') {
|
|
2172
|
-
|
|
2173
|
-
if ('error' in safeResult)
|
|
2174
|
-
return safeResult.error;
|
|
2175
|
-
const { session: safeSession } = safeResult;
|
|
2176
|
-
await this.sessionManager.setSafeMode(safeSession.id, true);
|
|
2177
|
-
this.eventBus.publish({ type: 'session:safe-mode-entered', sessionId: safeSession.id, reason: 'manual' });
|
|
2178
|
-
return `✓ 已进入安全模式
|
|
2179
|
-
|
|
2180
|
-
当前行为:
|
|
2181
|
-
- 暂时不加载会话历史(每次对话独立)
|
|
2182
|
-
- 所有功能正常可用(读写文件、执行命令等)
|
|
2183
|
-
- 不会丢失历史数据(仍保存在 .claude/ 目录)
|
|
2184
|
-
|
|
2185
|
-
退出安全模式:
|
|
2186
|
-
- 使用 /repair 检查并修复会话
|
|
2187
|
-
- 使用 /new 创建全新会话`;
|
|
2381
|
+
return `ℹ️ 安全模式已禁用\n\n如需重置会话,请使用 /new 创建新会话。`;
|
|
2188
2382
|
}
|
|
2189
2383
|
return null;
|
|
2190
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
|
+
}
|
|
2191
2556
|
}
|