evolclaw 2.4.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -14
- package/dist/agents/claude-runner.js +269 -23
- package/dist/agents/codex-runner.js +2 -8
- package/dist/agents/gemini-runner.js +1 -8
- package/dist/channels/aun.js +525 -53
- package/dist/channels/dingtalk.js +506 -0
- package/dist/channels/feishu.js +31 -231
- package/dist/channels/qqbot.js +391 -0
- package/dist/channels/wechat.js +36 -38
- package/dist/channels/wecom.js +549 -0
- package/dist/cli.js +86 -10
- package/dist/config.js +98 -2
- package/dist/core/command-handler.js +554 -130
- package/dist/core/message/message-bridge.js +26 -9
- package/dist/core/message/message-processor.js +152 -57
- package/dist/core/message/message-queue.js +48 -0
- package/dist/core/message/stream-flusher.js +2 -2
- package/dist/core/permission.js +7 -11
- package/dist/core/session/session-manager.js +21 -3
- package/dist/index.js +48 -13
- package/dist/ipc.js +14 -4
- package/dist/templates/skills.md +64 -0
- package/dist/utils/error-dict.js +63 -0
- package/dist/utils/error-utils.js +156 -56
- package/dist/utils/format.js +32 -0
- package/dist/utils/init-channel.js +752 -8
- package/dist/utils/init.js +85 -3
- package/dist/utils/media-cache.js +2 -0
- package/dist/utils/stats-collector.js +0 -8
- package/evolclaw-install.md +54 -0
- package/package.json +11 -4
|
@@ -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
|
}
|
|
@@ -341,22 +338,23 @@ export class CommandHandler {
|
|
|
341
338
|
items.push({
|
|
342
339
|
group: '项目管理',
|
|
343
340
|
commands: [
|
|
344
|
-
{ cmd: '/pwd', label: '显示当前项目路径' },
|
|
345
|
-
{ cmd: '/p',
|
|
346
|
-
{ cmd: '/bind',
|
|
341
|
+
{ cmd: '/pwd', label: '显示当前项目路径', desc: '查看当前会话绑定的项目目录' },
|
|
342
|
+
{ cmd: '/p', label: '列出或切换项目', desc: '切换到其他已配置的项目', next: { type: 'select', dynamic: true } },
|
|
343
|
+
...(isOwner ? [{ cmd: '/bind', label: '绑定新项目目录', desc: '将当前会话绑定到指定项目路径', next: { type: 'text' } }] : []),
|
|
347
344
|
]
|
|
348
345
|
});
|
|
349
346
|
}
|
|
350
347
|
items.push({
|
|
351
348
|
group: '会话管理',
|
|
352
349
|
commands: [
|
|
353
|
-
{ cmd: '/new',
|
|
354
|
-
{ cmd: '/s',
|
|
355
|
-
{ cmd: '/name',
|
|
356
|
-
{ cmd: '/del',
|
|
350
|
+
{ cmd: '/new', label: '创建新会话', desc: '清空历史,开始全新对话', next: { type: 'text' } },
|
|
351
|
+
{ cmd: '/s', label: '切换会话', desc: '切换到同项目下的其他会话', next: { type: 'select', dynamic: true } },
|
|
352
|
+
{ cmd: '/name', label: '重命名当前会话', desc: '为当前会话设置一个易识别的名称', next: { type: 'text' } },
|
|
353
|
+
{ cmd: '/del', label: '删除指定会话', desc: '永久删除一个非活跃会话', next: { type: 'select', dynamic: true } },
|
|
357
354
|
...(isAdmin ? [
|
|
358
|
-
{ cmd: '/fork',
|
|
359
|
-
{ cmd: '/
|
|
355
|
+
{ cmd: '/fork', label: '分支当前会话', desc: '基于当前会话创建独立分支', next: { type: 'text' } },
|
|
356
|
+
{ cmd: '/rewind', label: '查看历史/撤销指定轮次', desc: '回退会话到指定轮次,可选择撤销文件改动' },
|
|
357
|
+
{ cmd: '/compact', label: '压缩会话上下文', desc: '将长对话压缩为摘要以节省 token' },
|
|
360
358
|
] : []),
|
|
361
359
|
]
|
|
362
360
|
});
|
|
@@ -364,25 +362,56 @@ export class CommandHandler {
|
|
|
364
362
|
items.push({
|
|
365
363
|
group: 'Agent 与模型',
|
|
366
364
|
commands: [
|
|
367
|
-
{ cmd: '/agent',
|
|
368
|
-
{ cmd: '/model',
|
|
369
|
-
{ cmd: '/effort',
|
|
365
|
+
{ cmd: '/agent', label: '切换 Agent 后端', desc: '切换当前会话使用的 AI 后端', next: { type: 'select', dynamic: true } },
|
|
366
|
+
{ cmd: '/model', label: '切换模型', desc: '切换当前 Agent 使用的模型版本', next: { type: 'select', dynamic: true } },
|
|
367
|
+
{ cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', items: [
|
|
368
|
+
{ value: 'low', label: 'Low' },
|
|
369
|
+
{ value: 'medium', label: 'Medium' },
|
|
370
|
+
{ value: 'high', label: 'High' },
|
|
371
|
+
{ value: 'max', label: 'Max' },
|
|
372
|
+
] } },
|
|
370
373
|
]
|
|
371
374
|
});
|
|
372
375
|
items.push({
|
|
373
376
|
group: '权限管理',
|
|
374
377
|
commands: [
|
|
375
|
-
{ cmd: '/perm',
|
|
378
|
+
{ cmd: '/perm', label: '权限模式管理', desc: '控制工具调用的审批策略', next: { type: 'select', items: [
|
|
379
|
+
...(isOwner ? [
|
|
380
|
+
{ value: 'auto', label: '自动模式', desc: '根据风险等级自动决定是否审批' },
|
|
381
|
+
{ value: 'bypass', label: '免审批模式', desc: '跳过所有工具审批确认' },
|
|
382
|
+
{ value: 'plan', label: '计划模式', desc: '仅允许只读操作,写操作需审批' },
|
|
383
|
+
{ value: 'edit', label: '编辑模式', desc: '允许文件编辑,其他操作需审批' },
|
|
384
|
+
{ value: 'request', label: '请求模式', desc: '所有操作均需审批' },
|
|
385
|
+
{ value: 'noask', label: '静默模式', desc: '不弹出审批,自动拒绝未授权操作' },
|
|
386
|
+
] : []),
|
|
387
|
+
{ value: 'allow', label: '允许此操作', desc: '本次允许当前待审批操作' },
|
|
388
|
+
{ value: 'always', label: '始终允许', desc: '永久允许同类操作' },
|
|
389
|
+
{ value: 'deny', label: '拒绝此操作', desc: '拒绝当前待审批操作' },
|
|
390
|
+
] } },
|
|
376
391
|
]
|
|
377
392
|
});
|
|
378
393
|
items.push({
|
|
379
394
|
group: '运维',
|
|
380
395
|
commands: [
|
|
381
|
-
{ cmd: '/status', label: '显示会话状态' },
|
|
382
|
-
{ cmd: '/stop', label: '中断当前任务' },
|
|
383
|
-
{ cmd: '/
|
|
384
|
-
{ cmd: '/
|
|
385
|
-
|
|
396
|
+
{ cmd: '/status', label: '显示会话状态', desc: '查看当前会话、项目、Agent 的详细状态' },
|
|
397
|
+
{ cmd: '/stop', label: '中断当前任务', desc: '立即中断正在执行的 Agent 任务' },
|
|
398
|
+
{ cmd: '/check', label: '检查渠道状态', desc: '检查各消息渠道的连接健康状态' },
|
|
399
|
+
{ cmd: '/activity', label: '控制中间输出显示', desc: '设置工具调用过程的可见范围', next: { type: 'select', items: [
|
|
400
|
+
{ value: 'all', label: '全部显示', desc: '所有用户均可见中间输出' },
|
|
401
|
+
{ value: 'dm', label: '仅私聊', desc: '仅私聊中显示中间输出' },
|
|
402
|
+
{ value: 'owner', label: '仅 owner 私聊', desc: '仅 owner 的私聊中显示' },
|
|
403
|
+
{ value: 'none', label: '不显示', desc: '关闭所有中间输出' },
|
|
404
|
+
] } },
|
|
405
|
+
...(isAdmin ? [
|
|
406
|
+
{ cmd: '/restart', label: '重启/重连', desc: '重启服务或重连指定渠道', next: { type: 'select', dynamic: true } },
|
|
407
|
+
] : []),
|
|
408
|
+
...(isOwner ? [
|
|
409
|
+
{ cmd: '/send', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
|
|
410
|
+
{ cmd: '/agentmd', label: '管理 agent.md', desc: '查看或更新 AUN 网络上的 agent.md 身份文件', next: { type: 'select', items: [
|
|
411
|
+
{ value: 'put', label: '上传当前', desc: '将本地 agent.md 上传到 AUN 网络' },
|
|
412
|
+
{ value: 'set', label: '直接设置', desc: '输入内容直接更新 agent.md', next: { type: 'text' } },
|
|
413
|
+
] } },
|
|
414
|
+
] : []),
|
|
386
415
|
]
|
|
387
416
|
});
|
|
388
417
|
}
|
|
@@ -390,21 +419,67 @@ export class CommandHandler {
|
|
|
390
419
|
items.push({
|
|
391
420
|
group: '其他',
|
|
392
421
|
commands: [
|
|
393
|
-
{ cmd: '/status', label: '显示会话状态' },
|
|
422
|
+
{ cmd: '/status', label: '显示会话状态', desc: '查看当前会话的基本状态' },
|
|
423
|
+
{ cmd: '/check', label: '检查渠道健康', desc: '检查消息渠道连接状态' },
|
|
394
424
|
]
|
|
395
425
|
});
|
|
396
426
|
}
|
|
397
427
|
items.push({
|
|
398
428
|
group: '帮助',
|
|
399
429
|
commands: [
|
|
400
|
-
{ cmd: '/help', label: '显示帮助信息' },
|
|
430
|
+
{ cmd: '/help', label: '显示帮助信息', desc: '列出所有可用命令及说明' },
|
|
401
431
|
]
|
|
402
432
|
});
|
|
403
433
|
return items;
|
|
404
434
|
}
|
|
405
|
-
/**
|
|
406
|
-
|
|
407
|
-
|
|
435
|
+
/** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
|
|
436
|
+
async getSubMenuItems(cmd, channel, channelId, userId) {
|
|
437
|
+
const session = await this.sessionManager.getActiveSession(channel, channelId);
|
|
438
|
+
if (cmd === '/s' || cmd === '/del') {
|
|
439
|
+
const sessions = await this.sessionManager.listSessions(channel, channelId);
|
|
440
|
+
const active = cmd === '/del' ? await this.sessionManager.getActiveSession(channel, channelId) : null;
|
|
441
|
+
const items = sessions
|
|
442
|
+
.filter(s => !active || s.id !== active.id)
|
|
443
|
+
.map(s => {
|
|
444
|
+
const shortId = s.agentSessionId ? s.agentSessionId.substring(0, 8) : '';
|
|
445
|
+
const time = s.updatedAt ? formatIdleTime(Date.now() - s.updatedAt) : '';
|
|
446
|
+
const parts = [shortId, time].filter(Boolean).join(' · ');
|
|
447
|
+
return {
|
|
448
|
+
value: s.name || s.id.slice(0, 8),
|
|
449
|
+
label: s.name || s.id.slice(0, 8),
|
|
450
|
+
desc: parts || undefined,
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
if (cmd === '/s') {
|
|
454
|
+
items.push({ value: 'cli', label: '查看 CLI 会话', desc: '列出未导入的 CLI 本地会话' });
|
|
455
|
+
}
|
|
456
|
+
return items;
|
|
457
|
+
}
|
|
458
|
+
if (cmd === '/p') {
|
|
459
|
+
const list = this.config.projects?.list || {};
|
|
460
|
+
return Object.entries(list).map(([name, path]) => ({ value: name, label: name, desc: path }));
|
|
461
|
+
}
|
|
462
|
+
if (cmd === '/agent') {
|
|
463
|
+
return [...this.agentMap.keys()].map(name => ({ value: name, label: name }));
|
|
464
|
+
}
|
|
465
|
+
if (cmd === '/model') {
|
|
466
|
+
const agent = this.getAgent(session?.agentId);
|
|
467
|
+
if (hasModelSwitcher(agent) && agent.listModels) {
|
|
468
|
+
const models = await agent.listModels() ?? [];
|
|
469
|
+
if (models.length > 0)
|
|
470
|
+
return models.map((m) => ({ value: m, label: m }));
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
if (cmd === '/restart') {
|
|
475
|
+
const isOwner = userId ? this.sessionManager.resolveIdentity(channel, userId).role === 'owner' : false;
|
|
476
|
+
const channels = [...this.adapters.keys()].map(name => ({ value: name, label: name, desc: '重连此渠道' }));
|
|
477
|
+
if (isOwner)
|
|
478
|
+
channels.unshift({ value: '', label: '重启服务', desc: '重启整个 EvolClaw 服务进程' });
|
|
479
|
+
return channels;
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
408
483
|
isCommand(content) {
|
|
409
484
|
return content === '/p' || content === '/s' || quickCommandPrefixes.some(cmd => content.startsWith(cmd));
|
|
410
485
|
}
|
|
@@ -438,20 +513,23 @@ export class CommandHandler {
|
|
|
438
513
|
}
|
|
439
514
|
}
|
|
440
515
|
// 权限检查:区分用户级命令和管理级命令
|
|
441
|
-
const
|
|
516
|
+
const isOwner = identity.role === 'owner';
|
|
517
|
+
const isAdmin = identity.role === 'owner' || identity.role === 'admin';
|
|
442
518
|
const activeChatType = activeSession?.chatType || 'private';
|
|
443
519
|
if (normalizedContent.startsWith('/')) {
|
|
444
|
-
const guestGroupCommands = ['/status', '/help'];
|
|
520
|
+
const guestGroupCommands = ['/status', '/help', '/check'];
|
|
445
521
|
const userCommands = activeChatType === 'group' && !isAdmin
|
|
446
522
|
? guestGroupCommands
|
|
447
|
-
: ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s '];
|
|
523
|
+
: ['/slist', '/new', '/session', '/rename', '/name', '/status', '/help', '/del', '/s ', '/check'];
|
|
448
524
|
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
449
525
|
if (!isUserCommand && !isAdmin) {
|
|
450
|
-
return
|
|
526
|
+
return activeChatType === 'group'
|
|
527
|
+
? '❌ 无权限:当前群聊仅支持 /status 和 /help'
|
|
528
|
+
: '❌ 无权限:此命令仅限管理员使用';
|
|
451
529
|
}
|
|
452
530
|
}
|
|
453
531
|
// 空闲检查:某些命令需要等待当前会话空闲
|
|
454
|
-
const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent'];
|
|
532
|
+
const requiresIdle = ['/new', '/session', '/clear', '/compact', '/safe', '/repair', '/fork', '/bind', '/project', '/agent', '/rewind'];
|
|
455
533
|
if (requiresIdle.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' '))) {
|
|
456
534
|
if (threadId) {
|
|
457
535
|
// 话题中:检查话题 session 是否在处理(不创建)
|
|
@@ -495,6 +573,7 @@ export class CommandHandler {
|
|
|
495
573
|
'',
|
|
496
574
|
'其他:',
|
|
497
575
|
' /status - 显示会话状态',
|
|
576
|
+
' /check - 检查渠道健康',
|
|
498
577
|
' /help - 显示此帮助信息',
|
|
499
578
|
];
|
|
500
579
|
return lines.join('\n');
|
|
@@ -509,19 +588,21 @@ export class CommandHandler {
|
|
|
509
588
|
' /name <新名称> - 重命名当前会话',
|
|
510
589
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
511
590
|
' /status - 显示会话状态',
|
|
591
|
+
' /check - 检查渠道健康',
|
|
512
592
|
'',
|
|
513
593
|
'❓ 帮助:',
|
|
514
594
|
' /help - 显示此帮助信息',
|
|
515
595
|
];
|
|
516
596
|
return lines.join('\n');
|
|
517
597
|
}
|
|
598
|
+
// admin+ 基础命令
|
|
518
599
|
const lines = [
|
|
519
600
|
'可用命令:',
|
|
520
601
|
'',
|
|
521
602
|
'📁 项目管理:',
|
|
522
603
|
' /pwd - 显示当前项目路径',
|
|
523
604
|
' /p [name|path] - 列出或切换项目',
|
|
524
|
-
' /bind <path> - 绑定新项目目录',
|
|
605
|
+
...(isOwner ? [' /bind <path> - 绑定新项目目录'] : []),
|
|
525
606
|
'',
|
|
526
607
|
'🔄 会话管理:',
|
|
527
608
|
' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
|
|
@@ -529,6 +610,7 @@ export class CommandHandler {
|
|
|
529
610
|
' /name <新名称> - 重命名当前会话',
|
|
530
611
|
' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
|
|
531
612
|
' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
|
|
613
|
+
' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
|
|
532
614
|
' /compact - 压缩会话上下文(减少 token 用量)',
|
|
533
615
|
'',
|
|
534
616
|
'🤖 Agent 与模型:',
|
|
@@ -538,14 +620,22 @@ export class CommandHandler {
|
|
|
538
620
|
'',
|
|
539
621
|
'🔐 权限管理:',
|
|
540
622
|
' /perm - 查看当前权限模式',
|
|
541
|
-
' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式',
|
|
623
|
+
...(isOwner ? [' /perm <auto|bypass|request|edit|plan|noask> - 切换权限模式'] : []),
|
|
542
624
|
' /perm allow|always|deny - 审批权限请求',
|
|
543
625
|
'',
|
|
544
626
|
'🛠️ 运维:',
|
|
545
627
|
' /status - 显示会话状态',
|
|
546
628
|
' /stop - 中断当前任务',
|
|
547
|
-
' /
|
|
548
|
-
' /
|
|
629
|
+
' /check - 检查渠道状态',
|
|
630
|
+
' /activity [all|dm|owner|none] - 查看/控制中间输出显示模式',
|
|
631
|
+
...(isAdmin ? [
|
|
632
|
+
' /restart <channel> - 重连指定渠道',
|
|
633
|
+
] : []),
|
|
634
|
+
...(isOwner ? [
|
|
635
|
+
' /restart - 重启服务',
|
|
636
|
+
' /send [channel] <path> - 发送项目内文件',
|
|
637
|
+
' /agentmd [put|set <内容>] - 管理 agent.md',
|
|
638
|
+
] : []),
|
|
549
639
|
'',
|
|
550
640
|
'❓ 帮助:',
|
|
551
641
|
' /help - 显示此帮助信息',
|
|
@@ -581,7 +671,7 @@ export class CommandHandler {
|
|
|
581
671
|
kind: {
|
|
582
672
|
kind: 'action',
|
|
583
673
|
title: '🔐 权限模式',
|
|
584
|
-
body: availableModes.map(m => `${m.key === currentMode ? '
|
|
674
|
+
body: availableModes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.nameZh}) - ${m.description}`).join('\n'),
|
|
585
675
|
buttons: availableModes.map(m => ({
|
|
586
676
|
key: m.key,
|
|
587
677
|
label: m.key === currentMode ? `✓ ${m.key}` : m.key,
|
|
@@ -609,7 +699,7 @@ export class CommandHandler {
|
|
|
609
699
|
}
|
|
610
700
|
// 降级:文本
|
|
611
701
|
const modeList = modes.map(m => {
|
|
612
|
-
const prefix = m.key === currentMode ? '
|
|
702
|
+
const prefix = m.key === currentMode ? '✓' : ' ';
|
|
613
703
|
const suffix = m.available ? '' : ' ⚠️ 不可用';
|
|
614
704
|
return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
|
|
615
705
|
}).join('\n');
|
|
@@ -649,9 +739,9 @@ export class CommandHandler {
|
|
|
649
739
|
if (!matched.available) {
|
|
650
740
|
return `❌ ${matched.key} 模式当前不可用:${matched.unavailableReason}`;
|
|
651
741
|
}
|
|
652
|
-
// guest
|
|
653
|
-
if (
|
|
654
|
-
return '❌
|
|
742
|
+
// guest 和 admin 用户不能切换权限模式(仅 owner)
|
|
743
|
+
if (!isOwner) {
|
|
744
|
+
return '❌ 权限模式切换仅限 owner';
|
|
655
745
|
}
|
|
656
746
|
const metadata = permSession.metadata || {};
|
|
657
747
|
metadata.permissionMode = arg;
|
|
@@ -668,8 +758,9 @@ export class CommandHandler {
|
|
|
668
758
|
return `❌ 未知参数: ${args}\n用法: /perm <${allModeKeys}> 或 /perm allow|always|deny`;
|
|
669
759
|
}
|
|
670
760
|
// /agent 命令:查看或切换 Agent 后端
|
|
671
|
-
if (normalizedContent.startsWith('/agent')) {
|
|
672
|
-
|
|
761
|
+
if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ')) {
|
|
762
|
+
// 群聊中 owner only,私聊中 admin+
|
|
763
|
+
if (activeChatType === 'group' ? !isOwner : !isAdmin)
|
|
673
764
|
return '❌ 无权限:此命令仅限管理员使用';
|
|
674
765
|
const args = normalizedContent.slice(6).trim();
|
|
675
766
|
const available = [...this.agentMap.keys()];
|
|
@@ -787,7 +878,7 @@ export class CommandHandler {
|
|
|
787
878
|
return null;
|
|
788
879
|
}
|
|
789
880
|
// 降级:文本
|
|
790
|
-
const modelList = models.map((m) =>
|
|
881
|
+
const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
|
|
791
882
|
const effortHint = efforts.length > 0
|
|
792
883
|
? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
|
|
793
884
|
: '';
|
|
@@ -811,7 +902,7 @@ export class CommandHandler {
|
|
|
811
902
|
newModel = arg;
|
|
812
903
|
}
|
|
813
904
|
else {
|
|
814
|
-
const modelList = models.map((m) =>
|
|
905
|
+
const modelList = models.map((m) => ` ${m === currentModel ? '✓' : ' '} ${m}`).join('\n');
|
|
815
906
|
const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
|
|
816
907
|
return `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}`;
|
|
817
908
|
}
|
|
@@ -977,8 +1068,9 @@ export class CommandHandler {
|
|
|
977
1068
|
}
|
|
978
1069
|
// 降级:文本
|
|
979
1070
|
const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
|
|
980
|
-
const
|
|
981
|
-
|
|
1071
|
+
const allItems = [...efforts, 'auto'];
|
|
1072
|
+
const effortList = allItems.map(e => ` ${e === currentEffort ? '✓' : ' '} ${e}${e === 'auto' ? ' (SDK默认)' : ''}`).join('\n');
|
|
1073
|
+
return `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n\n用法: /effort <level>`;
|
|
982
1074
|
}
|
|
983
1075
|
// /effort auto:恢复 SDK 默认
|
|
984
1076
|
if (args === 'auto') {
|
|
@@ -1047,6 +1139,151 @@ export class CommandHandler {
|
|
|
1047
1139
|
}
|
|
1048
1140
|
return `✓ 推理强度: ${newEffort}`;
|
|
1049
1141
|
}
|
|
1142
|
+
// /activity 命令:控制中间输出显示模式
|
|
1143
|
+
if (normalizedContent === '/agentmd' || normalizedContent.startsWith('/agentmd ')) {
|
|
1144
|
+
if (!isOwner)
|
|
1145
|
+
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1146
|
+
const adapter = this.adapters.get(channel);
|
|
1147
|
+
if (!adapter?.uploadAgentMd)
|
|
1148
|
+
return '❌ 当前通道不支持 agent.md 操作';
|
|
1149
|
+
const selfAid = typeof adapter._selfAid === 'function' ? adapter._selfAid() : '';
|
|
1150
|
+
const arg = normalizedContent.slice(9).trim();
|
|
1151
|
+
// put — read local ~/.aun/AIDs/{aid}/agent.md and upload
|
|
1152
|
+
if (arg === 'put') {
|
|
1153
|
+
if (!selfAid)
|
|
1154
|
+
return '❌ 未连接,无法确定本地 AID';
|
|
1155
|
+
try {
|
|
1156
|
+
const { readFileSync } = await import('node:fs');
|
|
1157
|
+
const { join } = await import('node:path');
|
|
1158
|
+
const { homedir } = await import('node:os');
|
|
1159
|
+
const localPath = join(homedir(), '.aun', 'AIDs', selfAid, 'agent.md');
|
|
1160
|
+
const content = readFileSync(localPath, 'utf-8');
|
|
1161
|
+
await adapter.uploadAgentMd(content);
|
|
1162
|
+
return '✅ agent.md 已发布';
|
|
1163
|
+
}
|
|
1164
|
+
catch (e) {
|
|
1165
|
+
return `❌ 发布失败: ${String(e.message || e).slice(0, 100)}`;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
// set <content> — upload inline content and sync to local
|
|
1169
|
+
if (arg.startsWith('set ')) {
|
|
1170
|
+
const content = arg.slice(4).trim();
|
|
1171
|
+
if (!content)
|
|
1172
|
+
return '用法:/agentmd set <内容>';
|
|
1173
|
+
if (!selfAid)
|
|
1174
|
+
return '❌ 未连接,无法确定本地 AID';
|
|
1175
|
+
try {
|
|
1176
|
+
await adapter.uploadAgentMd(content);
|
|
1177
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
1178
|
+
const { join } = await import('node:path');
|
|
1179
|
+
const { homedir } = await import('node:os');
|
|
1180
|
+
const localDir = join(homedir(), '.aun', 'AIDs', selfAid);
|
|
1181
|
+
mkdirSync(localDir, { recursive: true });
|
|
1182
|
+
writeFileSync(join(localDir, 'agent.md'), content, 'utf-8');
|
|
1183
|
+
return '✅ agent.md 已更新并发布到AUN网络';
|
|
1184
|
+
}
|
|
1185
|
+
catch (e) {
|
|
1186
|
+
return `❌ 发布失败: ${String(e.message || e).slice(0, 100)}`;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
// view — /agentmd or /agentmd <aid>
|
|
1190
|
+
const aidToView = arg || selfAid;
|
|
1191
|
+
if (!aidToView)
|
|
1192
|
+
return '用法:/agentmd [<aid>] | put | set <内容>';
|
|
1193
|
+
try {
|
|
1194
|
+
const md = await adapter.downloadAgentMd(aidToView);
|
|
1195
|
+
if (!md || !md.trim())
|
|
1196
|
+
return `ℹ️ ${aidToView} 尚未设置 agent.md`;
|
|
1197
|
+
return `\`\`\`\n${md.slice(0, 1500)}\n\`\`\``;
|
|
1198
|
+
}
|
|
1199
|
+
catch (e) {
|
|
1200
|
+
const msg = String(e.message || e);
|
|
1201
|
+
if (msg.includes('not found') || msg.includes('404')) {
|
|
1202
|
+
return `ℹ️ ${aidToView} 尚未设置 agent.md`;
|
|
1203
|
+
}
|
|
1204
|
+
return `❌ 获取失败: ${msg.slice(0, 100)}`;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
|
|
1208
|
+
if (!isAdmin)
|
|
1209
|
+
return '❌ 无权限:此命令仅限管理员使用';
|
|
1210
|
+
const activityArg = normalizedContent.slice(9).trim();
|
|
1211
|
+
const modeMap = {
|
|
1212
|
+
all: 'all',
|
|
1213
|
+
dm: 'dm-only',
|
|
1214
|
+
owner: 'owner-dm-only',
|
|
1215
|
+
none: 'none',
|
|
1216
|
+
};
|
|
1217
|
+
const currentMode = getChannelShowActivities(this.config, channel);
|
|
1218
|
+
// 模式描述列表(用于 body 和文本降级)
|
|
1219
|
+
const modeDescriptions = [
|
|
1220
|
+
{ key: 'all', configVal: 'all', label: '全部显示' },
|
|
1221
|
+
{ key: 'dm', configVal: 'dm-only', label: '仅私聊显示' },
|
|
1222
|
+
{ key: 'owner', configVal: 'owner-dm-only', label: '仅 owner 私聊显示' },
|
|
1223
|
+
{ key: 'none', configVal: 'none', label: '全部静默' },
|
|
1224
|
+
];
|
|
1225
|
+
if (!activityArg) {
|
|
1226
|
+
// 无参数:显示当前模式 + Action 卡片
|
|
1227
|
+
if (this.interactionRouter) {
|
|
1228
|
+
const requestId = `activity-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
1229
|
+
const body = modeDescriptions.map(m => `${m.configVal === currentMode ? '✓' : '•'} **${m.key}** (${m.label})`).join('\n');
|
|
1230
|
+
const buttons = modeDescriptions.map(m => ({
|
|
1231
|
+
key: m.key,
|
|
1232
|
+
label: m.configVal === currentMode ? `✓ ${m.key}` : m.key,
|
|
1233
|
+
style: m.configVal === currentMode ? 'primary' : 'default',
|
|
1234
|
+
}));
|
|
1235
|
+
const interaction = {
|
|
1236
|
+
type: 'interaction',
|
|
1237
|
+
id: requestId,
|
|
1238
|
+
channelId,
|
|
1239
|
+
sessionId: activeSession?.id || requestId,
|
|
1240
|
+
kind: {
|
|
1241
|
+
kind: 'action',
|
|
1242
|
+
title: '📋 中间输出模式',
|
|
1243
|
+
body,
|
|
1244
|
+
buttons,
|
|
1245
|
+
},
|
|
1246
|
+
};
|
|
1247
|
+
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
1248
|
+
const cardSent = await this.sendInteractionCard({
|
|
1249
|
+
channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
|
|
1250
|
+
callback: async (action, _values, operatorId) => {
|
|
1251
|
+
const newMode = modeMap[action];
|
|
1252
|
+
if (newMode && newMode !== currentMode) {
|
|
1253
|
+
if (userId && operatorId && operatorId !== userId)
|
|
1254
|
+
return;
|
|
1255
|
+
const result = await this.handle(`/activity ${action}`, channel, channelId, undefined, userId, threadId);
|
|
1256
|
+
if (result) {
|
|
1257
|
+
const adapter = this.adapters.get(channel);
|
|
1258
|
+
adapter?.sendText(channelId, result, replyCtx);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
},
|
|
1262
|
+
});
|
|
1263
|
+
if (cardSent)
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
// 降级:文本
|
|
1267
|
+
const modeList = modeDescriptions.map(m => {
|
|
1268
|
+
const prefix = m.configVal === currentMode ? '✓' : ' ';
|
|
1269
|
+
return ` ${prefix} ${m.key} (${m.label})`;
|
|
1270
|
+
}).join('\n');
|
|
1271
|
+
return `📋 中间输出模式: ${currentMode}\n\n${modeList}\n\n用法:\n /activity <模式> 切换中间输出显示模式`;
|
|
1272
|
+
}
|
|
1273
|
+
const newMode = modeMap[activityArg];
|
|
1274
|
+
if (!newMode) {
|
|
1275
|
+
return `❌ 无效参数: ${activityArg}\n可选: all / dm / owner / none`;
|
|
1276
|
+
}
|
|
1277
|
+
const label = modeDescriptions.find(m => m.configVal === newMode)?.label || newMode;
|
|
1278
|
+
if (newMode === currentMode) {
|
|
1279
|
+
return `📋 中间输出模式已是 ${activityArg}(${label})`;
|
|
1280
|
+
}
|
|
1281
|
+
// 切换操作仅 owner
|
|
1282
|
+
if (!isOwner)
|
|
1283
|
+
return '❌ 中间输出模式切换仅限 owner';
|
|
1284
|
+
setChannelShowActivities(this.config, channel, newMode);
|
|
1285
|
+
return `✅ 中间输出模式: ${activityArg}(${label})`;
|
|
1286
|
+
}
|
|
1050
1287
|
// /stop 命令:中断当前任务
|
|
1051
1288
|
if (normalizedContent === '/stop') {
|
|
1052
1289
|
const stopResult = await this.ensureSession(channel, channelId, threadId);
|
|
@@ -1193,22 +1430,11 @@ export class CommandHandler {
|
|
|
1193
1430
|
if (health.consecutiveErrors > 0) {
|
|
1194
1431
|
lines.push(`异常计数: ${health.consecutiveErrors}`);
|
|
1195
1432
|
}
|
|
1196
|
-
if (health.safeMode) {
|
|
1197
|
-
lines.push(`安全模式: 是 ⚠️`);
|
|
1198
|
-
}
|
|
1199
1433
|
lines.push(`最后成功: ${timeStr}`, `${session.agentId}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
|
|
1200
1434
|
}
|
|
1201
1435
|
else {
|
|
1202
1436
|
lines.push(`📊 ${isThread ? '话题' : '会话'}状态:`, `渠道: ${channel} / 项目: ${projectName} / ${session.agentId}会话`, `状态: ${sessionStatus}`, `会话轮数: ${sessionTurns}`, `最后活跃: ${timeStr}`);
|
|
1203
1437
|
}
|
|
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
1438
|
if (health.lastError) {
|
|
1213
1439
|
lines.push('');
|
|
1214
1440
|
lines.push(`最后错误: ${health.lastErrorType || 'unknown'}`);
|
|
@@ -1244,29 +1470,10 @@ export class CommandHandler {
|
|
|
1244
1470
|
}
|
|
1245
1471
|
return `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 之前的对话历史已保留,可通过 /s 查看`;
|
|
1246
1472
|
}
|
|
1247
|
-
// /check
|
|
1473
|
+
// /check 命令:检查渠道状态(guest 可用,详情仅 admin)/ 重连指定渠道(admin only)
|
|
1248
1474
|
if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
|
|
1249
|
-
if (!isAdmin)
|
|
1250
|
-
return '❌ 无权限:此命令仅限管理员使用';
|
|
1251
1475
|
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
|
|
1476
|
+
// Default: show system health check (non-admin 仅看摘要)
|
|
1270
1477
|
const lines = ['📡 渠道状态:'];
|
|
1271
1478
|
// Group by channelType
|
|
1272
1479
|
const groups = new Map();
|
|
@@ -1285,6 +1492,13 @@ export class CommandHandler {
|
|
|
1285
1492
|
groups.set(type, []);
|
|
1286
1493
|
groups.get(type).push({ name, status });
|
|
1287
1494
|
}
|
|
1495
|
+
if (!isAdmin) {
|
|
1496
|
+
// guest/user: 仅显示渠道健康摘要
|
|
1497
|
+
const total = [...groups.values()].flat().length;
|
|
1498
|
+
const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
|
|
1499
|
+
lines.push(` ${healthy}/${total} 渠道正常`);
|
|
1500
|
+
return lines.join('\n');
|
|
1501
|
+
}
|
|
1288
1502
|
for (const [type, instances] of groups) {
|
|
1289
1503
|
if (instances.length === 1) {
|
|
1290
1504
|
lines.push(` ${instances[0].name}: ${instances[0].status}`);
|
|
@@ -1327,11 +1541,30 @@ export class CommandHandler {
|
|
|
1327
1541
|
lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
|
|
1328
1542
|
}
|
|
1329
1543
|
}
|
|
1330
|
-
lines.push('', '💡 /check rty <channel> — 重连指定渠道');
|
|
1331
1544
|
return lines.join('\n');
|
|
1332
1545
|
}
|
|
1333
|
-
// /restart
|
|
1334
|
-
if (normalizedContent === '/restart') {
|
|
1546
|
+
// /restart 命令:重启服务(owner only) / 重连指定渠道(admin+)
|
|
1547
|
+
if (normalizedContent === '/restart' || normalizedContent.startsWith('/restart ')) {
|
|
1548
|
+
const restartArg = normalizedContent.slice('/restart'.length).trim();
|
|
1549
|
+
// /restart <channel> — 重连指定渠道(admin only)
|
|
1550
|
+
if (restartArg) {
|
|
1551
|
+
if (!isAdmin)
|
|
1552
|
+
return '❌ 无权限:渠道重连仅限管理员使用';
|
|
1553
|
+
const target = restartArg;
|
|
1554
|
+
const ch = this.channelObjects.get(target);
|
|
1555
|
+
if (!ch) {
|
|
1556
|
+
const available = [...this.channelObjects.keys()].join(', ') || '无';
|
|
1557
|
+
return `❌ 未找到渠道 "${target}",可用渠道:${available}`;
|
|
1558
|
+
}
|
|
1559
|
+
if (!ch.reconnect) {
|
|
1560
|
+
return `❌ 渠道 "${target}" 不支持重连`;
|
|
1561
|
+
}
|
|
1562
|
+
const result = await ch.reconnect();
|
|
1563
|
+
return `🔄 ${target} 重连: ${result}`;
|
|
1564
|
+
}
|
|
1565
|
+
// /restart(无参数)— 重启整个服务(owner only)
|
|
1566
|
+
if (!isOwner)
|
|
1567
|
+
return '❌ 无权限:服务重启仅限 owner 使用';
|
|
1335
1568
|
const allSessions = await this.sessionManager.listSessions(channel, channelId);
|
|
1336
1569
|
const sessionsWithMessages = allSessions
|
|
1337
1570
|
.filter(s => this.messageCache.hasMessages(s.id))
|
|
@@ -1400,8 +1633,10 @@ export class CommandHandler {
|
|
|
1400
1633
|
}
|
|
1401
1634
|
return `当前项目: ${session.projectPath}`;
|
|
1402
1635
|
}
|
|
1403
|
-
// /send 命令:发送项目内文件,支持 /send path 和 /send channel path
|
|
1636
|
+
// /send 命令:发送项目内文件,支持 /send path 和 /send channel path(owner only)
|
|
1404
1637
|
if (normalizedContent.startsWith('/send')) {
|
|
1638
|
+
if (!isOwner)
|
|
1639
|
+
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1405
1640
|
// 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
|
|
1406
1641
|
// 还原: 将 [text](url) 替换为 text
|
|
1407
1642
|
const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
@@ -1573,7 +1808,7 @@ export class CommandHandler {
|
|
|
1573
1808
|
}));
|
|
1574
1809
|
const bodyLines = entries.map(e => {
|
|
1575
1810
|
const status = buildStatusText(e);
|
|
1576
|
-
const prefix = e.isCurrent ? '
|
|
1811
|
+
const prefix = e.isCurrent ? '✓' : '•';
|
|
1577
1812
|
return `${prefix} **${e.name}** (${e.projectPath}) ${status}`;
|
|
1578
1813
|
});
|
|
1579
1814
|
const interaction = {
|
|
@@ -1613,6 +1848,7 @@ export class CommandHandler {
|
|
|
1613
1848
|
const prefix = entry.isCurrent ? ' ✓' : ' ';
|
|
1614
1849
|
lines.push(`${prefix} ${entry.name} (${entry.projectPath}) - ${buildStatusText(entry)}`);
|
|
1615
1850
|
}
|
|
1851
|
+
lines.push('', '提示: 使用 /p <名称> 切换项目');
|
|
1616
1852
|
return lines.join('\n');
|
|
1617
1853
|
}
|
|
1618
1854
|
// /project(无参数):直接复用 /plist 逻辑(含卡片交互)
|
|
@@ -1711,8 +1947,12 @@ export class CommandHandler {
|
|
|
1711
1947
|
}
|
|
1712
1948
|
return response;
|
|
1713
1949
|
}
|
|
1714
|
-
// /bind
|
|
1950
|
+
// /bind 命令:持久化项目到配置(不切换)(owner only)
|
|
1951
|
+
if (normalizedContent === '/bind')
|
|
1952
|
+
return '用法: /bind <路径>';
|
|
1715
1953
|
if (normalizedContent.startsWith('/bind ')) {
|
|
1954
|
+
if (!isOwner)
|
|
1955
|
+
return '❌ 无权限:此命令仅限 owner 使用';
|
|
1716
1956
|
const projectPath = normalizedContent.slice(6).trim();
|
|
1717
1957
|
if (!projectPath)
|
|
1718
1958
|
return '用法: /bind <路径>';
|
|
@@ -1887,7 +2127,7 @@ export class CommandHandler {
|
|
|
1887
2127
|
};
|
|
1888
2128
|
});
|
|
1889
2129
|
const bodyLines = displaySessions.map(ds => {
|
|
1890
|
-
const prefix = ds.isActive ? '
|
|
2130
|
+
const prefix = ds.isActive ? '✓' : '•';
|
|
1891
2131
|
const threadTag = ds.session.threadId ? '[话题] ' : '';
|
|
1892
2132
|
const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
|
|
1893
2133
|
const fileMark = ds.fileMissing ? '❌ ' : '';
|
|
@@ -2036,6 +2276,9 @@ export class CommandHandler {
|
|
|
2036
2276
|
return `✓ 已切换到会话: ${targetSession.name || sessionName}${continueHint}${lastInputLine}`;
|
|
2037
2277
|
}
|
|
2038
2278
|
// /rename 或 /name 命令:重命名当前会话
|
|
2279
|
+
if (normalizedContent === '/rename' || normalizedContent === '/name') {
|
|
2280
|
+
return '用法: /name <新名称> 或 /rename <新名称>';
|
|
2281
|
+
}
|
|
2039
2282
|
if (normalizedContent.startsWith('/rename ')) {
|
|
2040
2283
|
const newName = normalizedContent.slice(8).trim();
|
|
2041
2284
|
if (!newName)
|
|
@@ -2131,31 +2374,59 @@ export class CommandHandler {
|
|
|
2131
2374
|
return `❌ 会话分支失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
|
2132
2375
|
}
|
|
2133
2376
|
}
|
|
2134
|
-
// /
|
|
2377
|
+
// /rewind 命令:查看历史 / 回退会话
|
|
2378
|
+
if (normalizedContent === '/rewind' || normalizedContent.startsWith('/rewind ')) {
|
|
2379
|
+
const result = await this.ensureSession(channel, channelId, threadId);
|
|
2380
|
+
if ('error' in result)
|
|
2381
|
+
return result.error;
|
|
2382
|
+
const { session } = result;
|
|
2383
|
+
const rewindAgent = this.getAgent(session.agentId);
|
|
2384
|
+
if (rewindAgent.name !== 'claude') {
|
|
2385
|
+
return '❌ /rewind 仅支持 Claude 后端';
|
|
2386
|
+
}
|
|
2387
|
+
if (!session.agentSessionId) {
|
|
2388
|
+
return '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind';
|
|
2389
|
+
}
|
|
2390
|
+
if (!rewindAgent.getSessionMessages) {
|
|
2391
|
+
return '❌ 当前 Agent 不支持 /rewind';
|
|
2392
|
+
}
|
|
2393
|
+
const args = normalizedContent.slice('/rewind'.length).trim();
|
|
2394
|
+
if (!args) {
|
|
2395
|
+
return await this.handleRewindList(session, rewindAgent);
|
|
2396
|
+
}
|
|
2397
|
+
const parts = args.split(/\s+/);
|
|
2398
|
+
const turnNum = parseInt(parts[0], 10);
|
|
2399
|
+
if (isNaN(turnNum) || turnNum < 1) {
|
|
2400
|
+
return '❌ 无效轮次,用法:/rewind <N> chat|file|all(撤销第N轮)';
|
|
2401
|
+
}
|
|
2402
|
+
const mode = parts[1]?.toLowerCase();
|
|
2403
|
+
if (!mode) {
|
|
2404
|
+
return `❌ 请指定回退模式:/rewind ${turnNum} chat | file | all(撤销第${turnNum}轮)`;
|
|
2405
|
+
}
|
|
2406
|
+
if (!['chat', 'file', 'all'].includes(mode)) {
|
|
2407
|
+
return `❌ 无效模式 "${mode}",可选:chat | file | all`;
|
|
2408
|
+
}
|
|
2409
|
+
return await this.handleRewind(session, rewindAgent, turnNum, mode);
|
|
2410
|
+
}
|
|
2411
|
+
// /repair 命令:检查并修复会话文件
|
|
2135
2412
|
if (normalizedContent === '/repair') {
|
|
2136
2413
|
const repairResult = await this.ensureSession(channel, channelId, threadId);
|
|
2137
2414
|
if ('error' in repairResult)
|
|
2138
2415
|
return repairResult.error;
|
|
2139
2416
|
const { session: repairSession } = repairResult;
|
|
2140
|
-
const health = await this.sessionManager.getHealthStatus(repairSession.id);
|
|
2141
|
-
if (!health.safeMode) {
|
|
2142
|
-
return `当前不在安全模式,无需修复\n\n如需进入安全模式,请使用 /safe`;
|
|
2143
|
-
}
|
|
2144
2417
|
const repairAgent = this.getAgent(repairSession.agentId);
|
|
2145
2418
|
const { checkSessionFile, backupSessionFile } = await import('./session/session-file-health.js');
|
|
2146
2419
|
try {
|
|
2147
2420
|
if (!repairSession.agentSessionId) {
|
|
2148
2421
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2149
|
-
|
|
2150
|
-
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
2422
|
+
return `✓ 修复完成\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器`;
|
|
2151
2423
|
}
|
|
2152
2424
|
// 通过 agent 定位 session 文件
|
|
2153
2425
|
const sessionFile = repairAgent.resolveSessionFile?.(repairSession.agentSessionId, repairSession.projectPath) ?? null;
|
|
2154
2426
|
if (!sessionFile) {
|
|
2155
2427
|
// 文件不存在(已被删除或从未创建),直接重置
|
|
2156
2428
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2157
|
-
|
|
2158
|
-
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
2429
|
+
return `✓ 修复完成\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器`;
|
|
2159
2430
|
}
|
|
2160
2431
|
const healthCheck = await checkSessionFile(sessionFile);
|
|
2161
2432
|
if (healthCheck.corrupt) {
|
|
@@ -2165,42 +2436,195 @@ export class CommandHandler {
|
|
|
2165
2436
|
await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
|
|
2166
2437
|
repairAgent.updateSessionId(repairSession.id, '');
|
|
2167
2438
|
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}`;
|
|
2439
|
+
return `✓ 修复完成\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}`;
|
|
2170
2440
|
}
|
|
2171
2441
|
if (healthCheck.issues.length > 0) {
|
|
2172
2442
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2173
|
-
this.eventBus.publish({ type: 'session:safe-mode-exited', sessionId: repairSession.id, method: 'repair' });
|
|
2174
2443
|
return `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n已重置异常计数器,可继续使用当前会话。`;
|
|
2175
2444
|
}
|
|
2176
2445
|
await this.sessionManager.resetHealthStatus(repairSession.id);
|
|
2177
|
-
|
|
2178
|
-
return `✓ 修复完成,已退出安全模式\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器\n- 已恢复正常会话模式`;
|
|
2446
|
+
return `✓ 修复完成\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器`;
|
|
2179
2447
|
}
|
|
2180
2448
|
catch (error) {
|
|
2181
2449
|
logger.error('[Repair] Failed:', error);
|
|
2182
2450
|
return `❌ 修复失败: ${error.message}`;
|
|
2183
2451
|
}
|
|
2184
2452
|
}
|
|
2185
|
-
// /safe
|
|
2453
|
+
// /safe 命令:安全模式已禁用
|
|
2186
2454
|
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 创建全新会话`;
|
|
2455
|
+
return `ℹ️ 安全模式已禁用\n\n如需重置会话,请使用 /new 创建新会话。`;
|
|
2203
2456
|
}
|
|
2204
2457
|
return null;
|
|
2205
2458
|
}
|
|
2459
|
+
// ── /rewind helpers ──
|
|
2460
|
+
async handleRewindList(session, agent) {
|
|
2461
|
+
try {
|
|
2462
|
+
const messages = await agent.getSessionMessages(session.agentSessionId, session.projectPath);
|
|
2463
|
+
const turns = this.buildTurnList(messages);
|
|
2464
|
+
if (turns.length === 0) {
|
|
2465
|
+
return '📋 当前会话暂无对话记录';
|
|
2466
|
+
}
|
|
2467
|
+
const lines = turns.map(t => `#${t.index} ${t.userContent}`);
|
|
2468
|
+
return [
|
|
2469
|
+
`📋 会话历史 (共 ${turns.length} 轮)`,
|
|
2470
|
+
'',
|
|
2471
|
+
...lines,
|
|
2472
|
+
'',
|
|
2473
|
+
'💡 /rewind <N> chat|file|all — 撤销第N轮',
|
|
2474
|
+
].join('\n');
|
|
2475
|
+
}
|
|
2476
|
+
catch (error) {
|
|
2477
|
+
logger.error('[CommandHandler] Failed to read session messages:', error);
|
|
2478
|
+
return `❌ 读取会话历史失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
async handleRewind(session, agent, turnNum, mode) {
|
|
2482
|
+
try {
|
|
2483
|
+
const messages = await agent.getSessionMessages(session.agentSessionId, session.projectPath);
|
|
2484
|
+
const turns = this.buildTurnList(messages);
|
|
2485
|
+
if (turnNum < 1 || turnNum > turns.length) {
|
|
2486
|
+
return `❌ 轮次超出范围,当前共 ${turns.length} 轮`;
|
|
2487
|
+
}
|
|
2488
|
+
// /rewind N = 撤销第N轮(及之后),保留 1..N-1
|
|
2489
|
+
const rewindTarget = turns[turnNum - 1]; // 被撤销的轮次(用于文件回退)
|
|
2490
|
+
const keepTarget = turnNum >= 2 ? turns[turnNum - 2] : null; // 保留到的轮次(用于对话回退)
|
|
2491
|
+
const results = [];
|
|
2492
|
+
// 文件回退(立即执行)
|
|
2493
|
+
if (mode === 'file' || mode === 'all') {
|
|
2494
|
+
if (!agent.rewindFiles) {
|
|
2495
|
+
return '❌ 当前 Agent 不支持文件回退';
|
|
2496
|
+
}
|
|
2497
|
+
const fileResult = await agent.rewindFiles(session.agentSessionId, session.projectPath, rewindTarget.userUuid);
|
|
2498
|
+
if (!fileResult.canRewind) {
|
|
2499
|
+
if (mode === 'file') {
|
|
2500
|
+
return `❌ 当前会话无文件快照,无法回退文件${fileResult.error ? `\n原因: ${fileResult.error}` : ''}`;
|
|
2501
|
+
}
|
|
2502
|
+
results.push(`⚠️ 文件回退失败${fileResult.error ? `: ${fileResult.error}` : '(无文件快照)'}`);
|
|
2503
|
+
}
|
|
2504
|
+
else {
|
|
2505
|
+
const detail = fileResult.filesChanged
|
|
2506
|
+
? `(恢复了 ${fileResult.filesChanged.length} 个文件)`
|
|
2507
|
+
: '';
|
|
2508
|
+
results.push(`✅ 已恢复文件到第 ${turnNum} 轮之前的状态${detail}`);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
// 对话回退(延迟执行 — 下次发消息时生效)
|
|
2512
|
+
if (mode === 'chat' || mode === 'all') {
|
|
2513
|
+
if (keepTarget) {
|
|
2514
|
+
const meta = { ...(session.metadata || {}), resumeAt: keepTarget.assistantUuid };
|
|
2515
|
+
await this.sessionManager.updateSession(session.id, { metadata: meta });
|
|
2516
|
+
}
|
|
2517
|
+
else {
|
|
2518
|
+
// N=1:撤销全部对话,清空 session 从头开始
|
|
2519
|
+
const meta = { ...(session.metadata || {}) };
|
|
2520
|
+
delete meta.resumeAt;
|
|
2521
|
+
await this.sessionManager.updateSession(session.id, {
|
|
2522
|
+
metadata: meta,
|
|
2523
|
+
agentSessionId: null,
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
const discarded = turns.length - turnNum + 1;
|
|
2527
|
+
const keepDesc = keepTarget
|
|
2528
|
+
? `回退到第 ${turnNum - 1} 轮:"${keepTarget.userContent}"`
|
|
2529
|
+
: '已清空全部对话历史';
|
|
2530
|
+
results.push(`✅ 已撤销第 ${turnNum} 轮${discarded > 1 ? `及后续共 ${discarded} 轮` : ''}`, keepTarget ? `下次发言将从第 ${turnNum - 1} 轮继续` : '下次发言将开始全新对话');
|
|
2531
|
+
}
|
|
2532
|
+
this.eventBus.publish({
|
|
2533
|
+
type: 'session:rewind',
|
|
2534
|
+
sessionId: session.id,
|
|
2535
|
+
turnNum,
|
|
2536
|
+
mode,
|
|
2537
|
+
});
|
|
2538
|
+
return results.join('\n');
|
|
2539
|
+
}
|
|
2540
|
+
catch (error) {
|
|
2541
|
+
logger.error('[CommandHandler] Rewind failed:', error);
|
|
2542
|
+
return `❌ 回退失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
buildTurnList(messages) {
|
|
2546
|
+
const turns = [];
|
|
2547
|
+
let pendingUser = null;
|
|
2548
|
+
for (const msg of messages) {
|
|
2549
|
+
if (msg.type === 'user') {
|
|
2550
|
+
const m = msg.message;
|
|
2551
|
+
if (Array.isArray(m?.content) && m.content.every((c) => c.type === 'tool_result')) {
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
const content = this.extractUserContent(msg.message);
|
|
2555
|
+
if (content) {
|
|
2556
|
+
pendingUser = { content, uuid: msg.uuid };
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
else if (msg.type === 'assistant' && pendingUser) {
|
|
2560
|
+
turns.push({
|
|
2561
|
+
index: turns.length + 1,
|
|
2562
|
+
userContent: pendingUser.content,
|
|
2563
|
+
userUuid: pendingUser.uuid,
|
|
2564
|
+
assistantUuid: msg.uuid,
|
|
2565
|
+
});
|
|
2566
|
+
pendingUser = null;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
return turns;
|
|
2570
|
+
}
|
|
2571
|
+
// ── Agent Ctl ──
|
|
2572
|
+
static CTL_COMMANDS = [
|
|
2573
|
+
'/help', '/status', '/check',
|
|
2574
|
+
'/model', '/effort', '/perm',
|
|
2575
|
+
'/compact', '/activity', '/send', '/restart', '/agentmd',
|
|
2576
|
+
];
|
|
2577
|
+
/**
|
|
2578
|
+
* Agent ctl 入口:通过 IPC 接收 Agent 自主管理指令
|
|
2579
|
+
* 复用现有 slash cmd 逻辑,权限继承 session 用户角色
|
|
2580
|
+
*/
|
|
2581
|
+
async handleCtl(cmd, sessionId) {
|
|
2582
|
+
// 1. 白名单检查
|
|
2583
|
+
const inputCmd = cmd.split(' ')[0];
|
|
2584
|
+
if (!CommandHandler.CTL_COMMANDS.includes(inputCmd)) {
|
|
2585
|
+
return { ok: false, error: `不允许的指令: ${inputCmd}` };
|
|
2586
|
+
}
|
|
2587
|
+
// 2. 通过 sessionId 查 session
|
|
2588
|
+
const session = await this.sessionManager.getSessionById(sessionId);
|
|
2589
|
+
if (!session) {
|
|
2590
|
+
return { ok: false, error: '无效的 session' };
|
|
2591
|
+
}
|
|
2592
|
+
// 3. 从 session.metadata.peerId 获取 userId(用于权限判断)
|
|
2593
|
+
const userId = session.metadata?.peerId;
|
|
2594
|
+
// 4. send 路径限制:只允许 projectPath 下的文件
|
|
2595
|
+
if (cmd.startsWith('/send')) {
|
|
2596
|
+
const sendArgs = cmd.slice(5).trim();
|
|
2597
|
+
const parts = sendArgs.split(/\s+/);
|
|
2598
|
+
const filePath = parts[parts.length - 1];
|
|
2599
|
+
if (filePath) {
|
|
2600
|
+
const resolved = path.resolve(session.projectPath, filePath);
|
|
2601
|
+
if (!resolved.startsWith(session.projectPath)) {
|
|
2602
|
+
return { ok: false, error: '路径越界:只能发送项目目录下的文件' };
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
// 5. 调用现有 handle(),不传 sendMessage 回调(结果直接返回)
|
|
2607
|
+
try {
|
|
2608
|
+
const result = await this.handle(cmd, session.channel, session.channelId, undefined, // 不发送消息
|
|
2609
|
+
userId);
|
|
2610
|
+
return { ok: true, result: result ?? '(无输出)' };
|
|
2611
|
+
}
|
|
2612
|
+
catch (err) {
|
|
2613
|
+
return { ok: false, error: err.message };
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
extractUserContent(message) {
|
|
2617
|
+
const m = message;
|
|
2618
|
+
let text = '';
|
|
2619
|
+
if (typeof m?.content === 'string') {
|
|
2620
|
+
text = m.content;
|
|
2621
|
+
}
|
|
2622
|
+
else if (Array.isArray(m?.content)) {
|
|
2623
|
+
text = m.content.filter((b) => b.type === 'text').map((b) => b.text).join(' ');
|
|
2624
|
+
}
|
|
2625
|
+
text = text.trim().replace(/\s+/g, ' ');
|
|
2626
|
+
if (!text)
|
|
2627
|
+
return '';
|
|
2628
|
+
return text.length > 50 ? text.substring(0, 50) + '…' : text;
|
|
2629
|
+
}
|
|
2206
2630
|
}
|