evolclaw 2.6.2 → 2.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/claude-runner.js +2 -1
- package/dist/agents/codex-runner.js +0 -1
- package/dist/agents/gemini-runner.js +3 -1
- package/dist/channels/aun.js +166 -27
- package/dist/core/command-handler.js +111 -18
- package/dist/core/message/message-processor.js +93 -80
- package/dist/core/message/thought-emitter.js +8 -1
- package/dist/core/session/session-manager.js +22 -2
- package/dist/index.js +3 -0
- package/dist/prompts/templates.js +122 -0
- package/dist/templates/prompts.md +103 -0
- package/dist/utils/init-channel.js +26 -21
- package/package.json +1 -1
|
@@ -233,11 +233,23 @@ export class CommandHandler {
|
|
|
233
233
|
async sendInteractionCard(opts) {
|
|
234
234
|
if (!this.interactionRouter)
|
|
235
235
|
return false;
|
|
236
|
+
// 无写权限 → 走文本降级(由调用点 fall through 输出只读信息)
|
|
237
|
+
if (opts.canWrite === false)
|
|
238
|
+
return false;
|
|
239
|
+
// 有写权限但此刻忙碌 → 也走文本降级(避免诱导用户在忙碌状态下触发带参写操作)
|
|
240
|
+
if (this.isSessionBusy(opts.sessionId))
|
|
241
|
+
return false;
|
|
236
242
|
await this.invalidateOldCards(opts.channel, opts.sessionId);
|
|
237
243
|
const messageId = await this.trySendInteraction(opts.channel, opts.channelId, opts.interaction, opts.replyCtx);
|
|
238
244
|
if (!messageId)
|
|
239
245
|
return false;
|
|
240
246
|
const wrappedCallback = async (action, values, operatorId) => {
|
|
247
|
+
// 点击回调时二次校验:若会话此刻忙碌,忽略本次点击(防止已弹卡片被用于带参切换)
|
|
248
|
+
if (this.isSessionBusy(opts.sessionId)) {
|
|
249
|
+
const adapter = this.adapters.get(opts.channel);
|
|
250
|
+
adapter?.sendText(opts.channelId, '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试', opts.replyCtx);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
241
253
|
await opts.callback(action, values, operatorId);
|
|
242
254
|
// 已完成交互的卡片:保留原始内容,仅禁用按钮(不标记为"已过期")
|
|
243
255
|
// "已过期"仅用于被新卡片替代的旧卡片(invalidateOldCards)
|
|
@@ -245,6 +257,14 @@ export class CommandHandler {
|
|
|
245
257
|
this.interactionRouter.register(opts.requestId, opts.sessionId, wrappedCallback, { timeoutMs: 120_000, messageId });
|
|
246
258
|
return true;
|
|
247
259
|
}
|
|
260
|
+
/** 判断指定 session 是否有活跃流(用于 idle 守卫和卡片降级) */
|
|
261
|
+
isSessionBusy(sessionId) {
|
|
262
|
+
for (const agent of this.agentMap.values()) {
|
|
263
|
+
if (agent.hasActiveStream(sessionId))
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
248
268
|
/** 获取活跃会话,无会话时返回统一错误提示 */
|
|
249
269
|
async ensureSession(channel, channelId, threadId) {
|
|
250
270
|
if (threadId) {
|
|
@@ -525,10 +545,18 @@ export class CommandHandler {
|
|
|
525
545
|
const isAdmin = identity.role === 'owner' || identity.role === 'admin';
|
|
526
546
|
const activeChatType = activeSession?.chatType || 'private';
|
|
527
547
|
if (normalizedContent.startsWith('/')) {
|
|
528
|
-
|
|
548
|
+
// guest 在群聊和私聊中均可访问的只读命令:纯查询形态(带参写操作由各 handler 内部守卫拦截)
|
|
549
|
+
const guestGroupCommands = [
|
|
550
|
+
'/status', '/help', '/check', '/chatmode',
|
|
551
|
+
'/model', '/effort', '/agent', '/perm', '/activity', '/safe',
|
|
552
|
+
];
|
|
529
553
|
const userCommands = activeChatType === 'group' && !isAdmin
|
|
530
554
|
? guestGroupCommands
|
|
531
|
-
: [
|
|
555
|
+
: [
|
|
556
|
+
...guestGroupCommands,
|
|
557
|
+
// 私聊 guest 额外可用:会话自管理 + 私聊专属的 /rewind 历史查看
|
|
558
|
+
'/slist', '/new', '/session', '/rename', '/name', '/del', '/s ', '/rewind',
|
|
559
|
+
];
|
|
532
560
|
const isUserCommand = userCommands.some(cmd => normalizedContent === cmd.trimEnd() || normalizedContent.startsWith(cmd));
|
|
533
561
|
if (!isUserCommand && !isAdmin) {
|
|
534
562
|
return activeChatType === 'group'
|
|
@@ -537,8 +565,16 @@ export class CommandHandler {
|
|
|
537
565
|
}
|
|
538
566
|
}
|
|
539
567
|
// 空闲检查:某些命令需要等待当前会话空闲
|
|
540
|
-
|
|
541
|
-
|
|
568
|
+
// 原则:仅对"写/破坏性"形态拦截,纯读/用法提示的无参形态始终放行
|
|
569
|
+
// - 始终需要 idle(无参即写):/new /clear /compact /repair /fork
|
|
570
|
+
// - 仅带参时需要 idle(无参是列表/用法):/session /bind /project /agent /rewind
|
|
571
|
+
// - /chatmode:在 handler 内部自行做写操作的 idle 检查
|
|
572
|
+
// - /safe:已禁用 no-op,不再要求 idle
|
|
573
|
+
const idleAlways = ['/new', '/clear', '/compact', '/repair', '/fork'];
|
|
574
|
+
const idleWhenArg = ['/session', '/bind', '/project', '/agent', '/rewind'];
|
|
575
|
+
const needsIdle = idleAlways.some(cmd => normalizedContent === cmd || normalizedContent.startsWith(cmd + ' ')) ||
|
|
576
|
+
idleWhenArg.some(cmd => normalizedContent.startsWith(cmd + ' '));
|
|
577
|
+
if (needsIdle) {
|
|
542
578
|
if (threadId) {
|
|
543
579
|
// 话题中:检查话题 session 是否在处理(不创建)
|
|
544
580
|
const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
@@ -665,7 +701,7 @@ export class CommandHandler {
|
|
|
665
701
|
if (!hasPermissionController(permAgent)) {
|
|
666
702
|
return '❌ 权限控制不可用';
|
|
667
703
|
}
|
|
668
|
-
const defaultPermMode = identity.role === 'owner' ? 'bypass' : '
|
|
704
|
+
const defaultPermMode = identity.role === 'owner' ? 'bypass' : identity.role === 'admin' ? 'auto' : 'noask';
|
|
669
705
|
const currentMode = permSession.metadata?.permissionMode ?? defaultPermMode;
|
|
670
706
|
const modes = permAgent.listModes();
|
|
671
707
|
// 尝试发送交互卡片
|
|
@@ -691,6 +727,7 @@ export class CommandHandler {
|
|
|
691
727
|
const replyCtx = this.getReplyContext(permSession);
|
|
692
728
|
const cardSent = await this.sendInteractionCard({
|
|
693
729
|
channel, channelId, sessionId: permSession.id, requestId, interaction, replyCtx,
|
|
730
|
+
canWrite: isOwner,
|
|
694
731
|
callback: async (action, _values, operatorId) => {
|
|
695
732
|
if (action !== currentMode) {
|
|
696
733
|
if (userId && operatorId && operatorId !== userId)
|
|
@@ -712,7 +749,10 @@ export class CommandHandler {
|
|
|
712
749
|
const suffix = m.available ? '' : ' ⚠️ 不可用';
|
|
713
750
|
return ` ${prefix} ${m.key} (${m.nameZh}) - ${m.description}${suffix}`;
|
|
714
751
|
}).join('\n');
|
|
715
|
-
|
|
752
|
+
if (isOwner) {
|
|
753
|
+
return `🔐 当前权限模式: ${currentMode}\n\n${modeList}\n\n用法:\n /perm <模式> 切换权限模式\n /perm allow|always|deny 审批权限请求`;
|
|
754
|
+
}
|
|
755
|
+
return `🔐 当前权限模式: ${currentMode}`;
|
|
716
756
|
}
|
|
717
757
|
const parts = args.split(/\s+/);
|
|
718
758
|
// /perm <mode> 或 /perm allow|always|deny:切换模式 / 快捷审批
|
|
@@ -768,10 +808,11 @@ export class CommandHandler {
|
|
|
768
808
|
}
|
|
769
809
|
// /agent 命令:查看或切换 Agent 后端
|
|
770
810
|
if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ')) {
|
|
771
|
-
// 群聊中 owner only,私聊中 admin+
|
|
772
|
-
if (activeChatType === 'group' ? !isOwner : !isAdmin)
|
|
773
|
-
return '❌ 无权限:此命令仅限管理员使用';
|
|
774
811
|
const args = normalizedContent.slice(6).trim();
|
|
812
|
+
// 切换(带参)需权限:群聊 owner only,私聊 admin+;无参查询对所有人放开
|
|
813
|
+
if (args && (activeChatType === 'group' ? !isOwner : !isAdmin)) {
|
|
814
|
+
return '❌ 无权限:此命令仅限管理员使用';
|
|
815
|
+
}
|
|
775
816
|
const available = [...this.agentMap.keys()];
|
|
776
817
|
if (!args) {
|
|
777
818
|
const currentAgent = activeSession?.agentId || this.defaultAgentId;
|
|
@@ -796,6 +837,7 @@ export class CommandHandler {
|
|
|
796
837
|
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
797
838
|
const cardSent = await this.sendInteractionCard({
|
|
798
839
|
channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
|
|
840
|
+
canWrite: activeChatType === 'group' ? isOwner : isAdmin,
|
|
799
841
|
callback: async (action, _values, operatorId) => {
|
|
800
842
|
if (action !== currentAgent) {
|
|
801
843
|
if (userId && operatorId && operatorId !== userId)
|
|
@@ -813,7 +855,11 @@ export class CommandHandler {
|
|
|
813
855
|
}
|
|
814
856
|
// 降级:文本
|
|
815
857
|
const list = available.map(a => `${a === currentAgent ? ' ✓' : ' '} ${a}`).join('\n');
|
|
816
|
-
|
|
858
|
+
const canSwitchAgent = activeChatType === 'group' ? isOwner : isAdmin;
|
|
859
|
+
if (canSwitchAgent) {
|
|
860
|
+
return `当前 Agent: ${currentAgent}\n\n可用:\n${list}\n\n用法: /agent <name>`;
|
|
861
|
+
}
|
|
862
|
+
return `当前 Agent: ${currentAgent}`;
|
|
817
863
|
}
|
|
818
864
|
if (!this.agentMap.has(args)) {
|
|
819
865
|
return `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}`;
|
|
@@ -871,6 +917,7 @@ export class CommandHandler {
|
|
|
871
917
|
const replyCtx = this.getReplyContext(modelSession);
|
|
872
918
|
const cardSent = await this.sendInteractionCard({
|
|
873
919
|
channel, channelId, sessionId: modelSession.id, requestId, interaction, replyCtx,
|
|
920
|
+
canWrite: isAdmin,
|
|
874
921
|
callback: async (action, _values, operatorId) => {
|
|
875
922
|
if (action !== currentModel) {
|
|
876
923
|
if (userId && operatorId && operatorId !== userId)
|
|
@@ -891,8 +938,14 @@ export class CommandHandler {
|
|
|
891
938
|
const effortHint = efforts.length > 0
|
|
892
939
|
? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
|
|
893
940
|
: '';
|
|
894
|
-
|
|
941
|
+
if (isAdmin) {
|
|
942
|
+
return `当前模型: ${currentModel}${effortHint}\n\n可用模型:\n${modelList}\n\n${formatModelUsage(modelAgent, currentModel)}`;
|
|
943
|
+
}
|
|
944
|
+
return `当前模型: ${currentModel}${effortHint}`;
|
|
895
945
|
}
|
|
946
|
+
// 带参(切换/调整)需 admin+;无参查询已在上方返回
|
|
947
|
+
if (!isAdmin)
|
|
948
|
+
return '❌ 无权限:切换模型仅限管理员使用';
|
|
896
949
|
const parts = args.split(/\s+/);
|
|
897
950
|
let newModel;
|
|
898
951
|
let newEffort;
|
|
@@ -1060,6 +1113,7 @@ export class CommandHandler {
|
|
|
1060
1113
|
const replyCtx = this.getReplyContext(effortSession);
|
|
1061
1114
|
const cardSent = await this.sendInteractionCard({
|
|
1062
1115
|
channel, channelId, sessionId: effortSession.id, requestId, interaction, replyCtx,
|
|
1116
|
+
canWrite: isAdmin,
|
|
1063
1117
|
callback: async (action, _values, operatorId) => {
|
|
1064
1118
|
if (action !== currentEffort) {
|
|
1065
1119
|
if (userId && operatorId && operatorId !== userId)
|
|
@@ -1079,8 +1133,14 @@ export class CommandHandler {
|
|
|
1079
1133
|
const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
|
|
1080
1134
|
const allItems = [...efforts, 'auto'];
|
|
1081
1135
|
const effortList = allItems.map(e => ` ${e === currentEffort ? '✓' : ' '} ${e}${e === 'auto' ? ' (SDK默认)' : ''}`).join('\n');
|
|
1082
|
-
|
|
1136
|
+
if (isAdmin) {
|
|
1137
|
+
return `⚡ 推理强度: ${effortDisplay}\n\n可选:\n${effortList}\n\n用法: /effort <level>`;
|
|
1138
|
+
}
|
|
1139
|
+
return `⚡ 推理强度: ${effortDisplay}`;
|
|
1083
1140
|
}
|
|
1141
|
+
// 带参(切换)需 admin+;无参查询已在上方返回
|
|
1142
|
+
if (!isAdmin)
|
|
1143
|
+
return '❌ 无权限:切换推理强度仅限管理员使用';
|
|
1084
1144
|
// /effort auto:恢复 SDK 默认
|
|
1085
1145
|
if (args === 'auto') {
|
|
1086
1146
|
effortAgent.setEffort?.(undefined);
|
|
@@ -1302,13 +1362,14 @@ export class CommandHandler {
|
|
|
1302
1362
|
}
|
|
1303
1363
|
}
|
|
1304
1364
|
if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
|
|
1305
|
-
|
|
1365
|
+
const activityArg = normalizedContent.slice(9).trim();
|
|
1366
|
+
// 带参(写操作)需 admin+;无参查询对所有人开放(owner 门在具体切换点还有一道)
|
|
1367
|
+
if (activityArg && !isAdmin)
|
|
1306
1368
|
return '❌ 无权限:此命令仅限管理员使用';
|
|
1307
1369
|
// proactive 模式下流式输出全部静默,activity 配置无意义
|
|
1308
1370
|
if (activeSession?.sessionMode === 'proactive') {
|
|
1309
1371
|
return '❌ 当前会话为 proactive 模式,不支持 activity 配置(流式输出已全部静默)';
|
|
1310
1372
|
}
|
|
1311
|
-
const activityArg = normalizedContent.slice(9).trim();
|
|
1312
1373
|
const modeMap = {
|
|
1313
1374
|
all: 'all',
|
|
1314
1375
|
dm: 'dm-only',
|
|
@@ -1348,6 +1409,7 @@ export class CommandHandler {
|
|
|
1348
1409
|
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
1349
1410
|
const cardSent = await this.sendInteractionCard({
|
|
1350
1411
|
channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
|
|
1412
|
+
canWrite: isOwner,
|
|
1351
1413
|
callback: async (action, _values, operatorId) => {
|
|
1352
1414
|
const newMode = modeMap[action];
|
|
1353
1415
|
if (newMode && newMode !== currentMode) {
|
|
@@ -1369,7 +1431,10 @@ export class CommandHandler {
|
|
|
1369
1431
|
const prefix = m.configVal === currentMode ? '✓' : ' ';
|
|
1370
1432
|
return ` ${prefix} ${m.key} (${m.label})`;
|
|
1371
1433
|
}).join('\n');
|
|
1372
|
-
|
|
1434
|
+
if (isOwner) {
|
|
1435
|
+
return `📋 中间输出模式: ${currentMode}\n\n${modeList}\n\n用法:\n /activity <模式> 切换中间输出显示模式`;
|
|
1436
|
+
}
|
|
1437
|
+
return `📋 中间输出模式: ${currentMode}`;
|
|
1373
1438
|
}
|
|
1374
1439
|
const newMode = modeMap[activityArg];
|
|
1375
1440
|
if (!newMode) {
|
|
@@ -1396,7 +1461,11 @@ export class CommandHandler {
|
|
|
1396
1461
|
const currentMode = activeSession.sessionMode || 'interactive';
|
|
1397
1462
|
if (!arg) {
|
|
1398
1463
|
const lockHint = lockedMode ? `(由通道配置锁定为 ${lockedMode})` : '';
|
|
1399
|
-
|
|
1464
|
+
const canSwitch = activeChatType !== 'group' || isAdmin;
|
|
1465
|
+
if (canSwitch && !lockedMode) {
|
|
1466
|
+
return `📋 当前会话模式: ${currentMode}${lockHint}\n可选: interactive / proactive\n用法: /chatmode <模式>`;
|
|
1467
|
+
}
|
|
1468
|
+
return `📋 当前会话模式: ${currentMode}${lockHint}`;
|
|
1400
1469
|
}
|
|
1401
1470
|
if (arg !== 'interactive' && arg !== 'proactive') {
|
|
1402
1471
|
return `❌ 无效模式: ${arg}\n可选: interactive / proactive`;
|
|
@@ -1410,6 +1479,19 @@ export class CommandHandler {
|
|
|
1410
1479
|
if (arg === currentMode) {
|
|
1411
1480
|
return `📋 当前会话模式已是 ${arg}`;
|
|
1412
1481
|
}
|
|
1482
|
+
// 仅在真正需要切换时才要求会话空闲
|
|
1483
|
+
if (threadId) {
|
|
1484
|
+
const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
|
|
1485
|
+
if (threadSession) {
|
|
1486
|
+
const threadAgent = this.getAgent(threadSession.agentId);
|
|
1487
|
+
if (threadAgent.hasActiveStream(threadSession.id)) {
|
|
1488
|
+
return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
else if (agent.hasActiveStream(activeSession.id)) {
|
|
1493
|
+
return '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试';
|
|
1494
|
+
}
|
|
1413
1495
|
await this.sessionManager.updateSession(activeSession.id, { sessionMode: arg });
|
|
1414
1496
|
return `✅ 会话模式已切换: ${arg}`;
|
|
1415
1497
|
}
|
|
@@ -1958,6 +2040,7 @@ export class CommandHandler {
|
|
|
1958
2040
|
const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
|
|
1959
2041
|
const cardSent = await this.sendInteractionCard({
|
|
1960
2042
|
channel, channelId, sessionId: activeSession?.id || requestId, requestId, interaction, replyCtx,
|
|
2043
|
+
canWrite: isAdmin,
|
|
1961
2044
|
callback: async (action, _values, operatorId) => {
|
|
1962
2045
|
if (userId && operatorId && operatorId !== userId)
|
|
1963
2046
|
return;
|
|
@@ -2526,6 +2609,9 @@ export class CommandHandler {
|
|
|
2526
2609
|
if (!args) {
|
|
2527
2610
|
return await this.handleRewindList(session, rewindAgent);
|
|
2528
2611
|
}
|
|
2612
|
+
// 带参(执行回退,会删除文件/改对话)需 admin+
|
|
2613
|
+
if (!isAdmin)
|
|
2614
|
+
return '❌ 无权限:回退操作仅限管理员使用';
|
|
2529
2615
|
const parts = args.split(/\s+/);
|
|
2530
2616
|
const turnNum = parseInt(parts[0], 10);
|
|
2531
2617
|
if (isNaN(turnNum) || turnNum < 1) {
|
|
@@ -2702,10 +2788,13 @@ export class CommandHandler {
|
|
|
2702
2788
|
}
|
|
2703
2789
|
// ── Agent Ctl ──
|
|
2704
2790
|
static CTL_COMMANDS = [
|
|
2705
|
-
'/help', '/status', '/check',
|
|
2706
|
-
'/model', '/effort', '/perm',
|
|
2791
|
+
'/help', '/status', '/check', '/pwd',
|
|
2792
|
+
'/model', '/effort', '/perm', '/agent',
|
|
2707
2793
|
'/compact', '/activity', '/file', '/send', '/chatmode', '/restart', '/agentmd', '/bind', '/aid',
|
|
2794
|
+
'/rename', '/name',
|
|
2708
2795
|
];
|
|
2796
|
+
/** ctl 中仅允许查询形态的指令;写形态(带参)一律拒绝 */
|
|
2797
|
+
static CTL_READONLY = new Set(['/agent']);
|
|
2709
2798
|
/**
|
|
2710
2799
|
* 从 session 恢复 ReplyContext,用于 ctl send 主动发送文本时的路由
|
|
2711
2800
|
* - 群聊话题:metadata.replyContext.{threadId,peerId}
|
|
@@ -2732,6 +2821,10 @@ export class CommandHandler {
|
|
|
2732
2821
|
if (!CommandHandler.CTL_COMMANDS.includes(inputCmd)) {
|
|
2733
2822
|
return { ok: false, error: `不允许的指令: ${inputCmd}` };
|
|
2734
2823
|
}
|
|
2824
|
+
// 1.1 只读守卫:带参形态(写操作)在 ctl 中禁止
|
|
2825
|
+
if (CommandHandler.CTL_READONLY.has(inputCmd) && cmd.trimEnd().length > inputCmd.length) {
|
|
2826
|
+
return { ok: false, error: `${inputCmd} 在 ctl 中仅支持查询形态,不支持带参切换` };
|
|
2827
|
+
}
|
|
2735
2828
|
// 2. 通过 sessionId 查 session
|
|
2736
2829
|
const session = await this.sessionManager.getSessionById(sessionId);
|
|
2737
2830
|
if (!session) {
|
|
@@ -10,6 +10,7 @@ import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError,
|
|
|
10
10
|
import { summarizeToolInput } from '../permission.js';
|
|
11
11
|
import { getOwner } from '../../config.js';
|
|
12
12
|
import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
13
|
+
import { renderPromptSection } from '../../prompts/templates.js';
|
|
13
14
|
/**
|
|
14
15
|
* 统一消息处理器
|
|
15
16
|
* 负责处理来自不同渠道的消息,协调事件流处理
|
|
@@ -282,6 +283,17 @@ export class MessageProcessor {
|
|
|
282
283
|
const streamKey = session.id;
|
|
283
284
|
// 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
|
|
284
285
|
const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
|
286
|
+
const chatmode = session.sessionMode ?? 'interactive';
|
|
287
|
+
// 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
|
|
288
|
+
const taskReplyContext = () => {
|
|
289
|
+
const base = this.getReplyContext(message);
|
|
290
|
+
return {
|
|
291
|
+
...(base ?? {}),
|
|
292
|
+
metadata: { ...(base?.metadata ?? {}), taskId, chatmode },
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
// Proactive 模式可观测:ThoughtEmitter 声明在 try 外,catch 块也能透传错误为 thought
|
|
296
|
+
let thoughtEmitter = null;
|
|
285
297
|
try {
|
|
286
298
|
const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
287
299
|
// 记录收到消息
|
|
@@ -302,7 +314,7 @@ export class MessageProcessor {
|
|
|
302
314
|
const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
|
|
303
315
|
const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
|
|
304
316
|
logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}`);
|
|
305
|
-
logger.info(`[MessageProcessor] session=${session.id} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
317
|
+
logger.info(`[MessageProcessor] session=${session.id} task=${taskId} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
306
318
|
// 记录开始处理
|
|
307
319
|
this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
|
|
308
320
|
adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId, this.getReplyContext(message));
|
|
@@ -335,22 +347,25 @@ export class MessageProcessor {
|
|
|
335
347
|
firstReply = false;
|
|
336
348
|
}
|
|
337
349
|
}
|
|
338
|
-
|
|
350
|
+
opts.metadata = { ...(opts.metadata ?? {}), taskId, chatmode };
|
|
351
|
+
await adapter.sendText(message.channelId, text, opts);
|
|
339
352
|
}
|
|
340
353
|
// 后台任务:静默,不发送输出
|
|
341
354
|
}, (options?.flushDelay ?? this.config.flushDelay ?? 3) * 1000, options?.fileMarkerPattern, this.config.debug?.flusherDiag, isProactive);
|
|
342
355
|
// 保存当前 flusher,用于 compact 事件
|
|
343
356
|
this.currentFlusher = flusher;
|
|
357
|
+
if (isProactive) {
|
|
358
|
+
logger.info(`[MessageProcessor] proactive mode: flusher silent, outputs via thought.put task=${taskId}`);
|
|
359
|
+
}
|
|
344
360
|
// Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
|
|
345
361
|
// selector: context = { type: 'task', id: taskId }
|
|
346
|
-
let thoughtEmitter = null;
|
|
347
362
|
if (isProactive && adapter.putThought) {
|
|
348
|
-
thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId);
|
|
363
|
+
thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId, chatmode);
|
|
349
364
|
}
|
|
350
365
|
// 调用 AgentRunner(含上下文过长自动 compact 重试)
|
|
351
366
|
// 捕获当前消息的上下文(闭包),避免后续消息处理时串台
|
|
352
367
|
const capturedChannelId = message.channelId;
|
|
353
|
-
const capturedReplyContext =
|
|
368
|
+
const capturedReplyContext = taskReplyContext();
|
|
354
369
|
// 设置权限审批的消息发送回调(指向当前渠道)
|
|
355
370
|
agent.setSendPrompt(async (text) => {
|
|
356
371
|
await adapter.sendText(capturedChannelId, text, capturedReplyContext);
|
|
@@ -368,9 +383,9 @@ export class MessageProcessor {
|
|
|
368
383
|
? (sessionKey) => this.messageQueue.cancelIntercept(sessionKey)
|
|
369
384
|
: undefined,
|
|
370
385
|
});
|
|
371
|
-
// 设置 per-session 权限模式(动态默认值:owner → bypass,admin → auto,guest →
|
|
386
|
+
// 设置 per-session 权限模式(动态默认值:owner → bypass,admin → auto,guest → noask)
|
|
372
387
|
const role = session.identity?.role;
|
|
373
|
-
const defaultPermMode = role === 'owner' ? 'bypass' : role === 'admin' ? 'auto' : '
|
|
388
|
+
const defaultPermMode = role === 'owner' ? 'bypass' : role === 'admin' ? 'auto' : 'noask';
|
|
374
389
|
agent.setMode(session.metadata?.permissionMode ?? defaultPermMode);
|
|
375
390
|
// 标记会话为处理中(实时持久化,重启后可恢复)
|
|
376
391
|
this.sessionManager.markProcessing(session.id);
|
|
@@ -385,8 +400,7 @@ export class MessageProcessor {
|
|
|
385
400
|
// 动态构建运行时上下文提示
|
|
386
401
|
const contextParts = [];
|
|
387
402
|
const currentChannelType = options?.channelType || message.channel;
|
|
388
|
-
// 1.
|
|
389
|
-
const peerLabel = session.identity?.role || 'unknown';
|
|
403
|
+
// 1. 构建模板变量并渲染 runtime 段
|
|
390
404
|
const peerName = message.peerName || session.metadata?.peerName;
|
|
391
405
|
const peerType = message.peerType;
|
|
392
406
|
const peerId = message.peerId;
|
|
@@ -400,52 +414,20 @@ export class MessageProcessor {
|
|
|
400
414
|
};
|
|
401
415
|
const selfIdentity = formatIdentity(selfName, selfAid);
|
|
402
416
|
const peerIdentity = formatIdentity(peerName, peerId);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
];
|
|
407
|
-
if (session.name)
|
|
408
|
-
envParts.push(`会话名称: ${session.name}`);
|
|
409
|
-
if (selfIdentity)
|
|
410
|
-
envParts.push(`当前名称: ${selfIdentity}`);
|
|
411
|
-
envParts.push(`对端身份: ${peerLabel}`);
|
|
412
|
-
if (peerIdentity)
|
|
413
|
-
envParts.push(`对端名称: ${peerIdentity}`);
|
|
414
|
-
if (peerType && peerType !== 'unknown')
|
|
415
|
-
envParts.push(`对端类型: ${peerType}`);
|
|
416
|
-
if (session.chatType)
|
|
417
|
-
envParts.push(`聊天类型: ${session.chatType}`);
|
|
418
|
-
if (session.agentId && session.agentId !== 'claude')
|
|
419
|
-
envParts.push(`当前Agent: ${session.agentId}`);
|
|
420
|
-
contextParts.push(`[当前环境] ${envParts.join(' | ')}`);
|
|
421
|
-
// 只读模式提示
|
|
422
|
-
if (session.metadata?.permissionMode === 'readonly') {
|
|
423
|
-
const sendHint = isProactive
|
|
424
|
-
? '使用 evolclaw ctl file 发送'
|
|
425
|
-
: '使用 [SEND_FILE:] 发送';
|
|
426
|
-
contextParts.push(`[只读模式] 禁止修改项目文件。如需生成文件供用户下载,请写入 .evolclaw/tmp/ 目录后${sendHint}`);
|
|
427
|
-
}
|
|
428
|
-
// 2. 文件发送能力(按 channelType 去重,提示词只展示第一级通道名)
|
|
429
|
-
// proactive 模式:不推送 [SEND_FILE:] 提示,统一通过 evolclaw ctl file 显式发送(与 ctl send 契约一致)
|
|
417
|
+
// 文件发送能力(按 channelType 去重)
|
|
418
|
+
let crossChannelTypes = [];
|
|
419
|
+
let currentCanSend = false;
|
|
430
420
|
if (!isProactive) {
|
|
431
421
|
const fileChannelTypes = new Set();
|
|
432
|
-
|
|
422
|
+
currentCanSend = !!channelInfo.adapter.sendFile;
|
|
433
423
|
for (const [, info] of this.channels) {
|
|
434
424
|
if (info.adapter.sendFile) {
|
|
435
425
|
fileChannelTypes.add(info.options?.channelType || info.adapter.channelName);
|
|
436
426
|
}
|
|
437
427
|
}
|
|
438
|
-
|
|
439
|
-
if (currentCanSend || crossChannelTypes.length > 0) {
|
|
440
|
-
const hints = [];
|
|
441
|
-
if (currentCanSend)
|
|
442
|
-
hints.push(`[SEND_FILE:路径] 发送文件到当前通道`);
|
|
443
|
-
if (crossChannelTypes.length > 0)
|
|
444
|
-
hints.push(`[SEND_FILE:${crossChannelTypes[0]}:路径] 发送文件到指定通道(可用: ${crossChannelTypes.join('/')})`);
|
|
445
|
-
contextParts.push(hints.join(','));
|
|
446
|
-
}
|
|
428
|
+
crossChannelTypes = [...fileChannelTypes].filter(t => t !== currentChannelType);
|
|
447
429
|
}
|
|
448
|
-
//
|
|
430
|
+
// 通道能力
|
|
449
431
|
const capParts = [];
|
|
450
432
|
if (options?.supportsImages)
|
|
451
433
|
capParts.push('图片输入');
|
|
@@ -453,30 +435,32 @@ export class MessageProcessor {
|
|
|
453
435
|
capParts.push('图片输出');
|
|
454
436
|
if (channelInfo.adapter.sendFile)
|
|
455
437
|
capParts.push('文件发送');
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
438
|
+
contextParts.push(renderPromptSection('runtime', {
|
|
439
|
+
channel: currentChannelType,
|
|
440
|
+
project: path.basename(absoluteProjectPath),
|
|
441
|
+
sessionName: session.name || '',
|
|
442
|
+
selfIdentity: selfIdentity || '',
|
|
443
|
+
peerRole: session.identity?.role || 'unknown',
|
|
444
|
+
peerIdentity: peerIdentity || '',
|
|
445
|
+
peerType: peerType && peerType !== 'unknown' ? peerType : '',
|
|
446
|
+
chatType: session.chatType || '',
|
|
447
|
+
agent: session.agentId && session.agentId !== 'claude' ? session.agentId : '',
|
|
448
|
+
readonly: session.metadata?.permissionMode === 'readonly',
|
|
449
|
+
readonlySendHint: isProactive ? '使用 evolclaw ctl file 发送' : '使用 [SEND_FILE:] 发送',
|
|
450
|
+
fileSendCurrent: !isProactive && currentCanSend,
|
|
451
|
+
fileSendCross: !isProactive && crossChannelTypes.length > 0,
|
|
452
|
+
crossPrimary: crossChannelTypes[0] || '',
|
|
453
|
+
crossTypes: crossChannelTypes.join('/'),
|
|
454
|
+
capability: capParts.length > 0,
|
|
455
|
+
capabilities: capParts.join('、'),
|
|
456
|
+
}));
|
|
457
|
+
// 2. 群聊 @ 规则
|
|
460
458
|
if (message.chatType === 'group' && message.peerId) {
|
|
461
|
-
contextParts.push(
|
|
459
|
+
contextParts.push(renderPromptSection('group', { peerId: message.peerId }));
|
|
462
460
|
}
|
|
463
|
-
//
|
|
464
|
-
// 暂时注释:排查 proactive 模式下 agent 未调用 Bash 工具的问题,
|
|
465
|
-
// 怀疑此段与 [Proactive 模式] 语义重合稀释了后者的权重
|
|
466
|
-
// if (!this.skillsEnsured) {
|
|
467
|
-
// this.ensureSkillsFile();
|
|
468
|
-
// this.skillsEnsured = true;
|
|
469
|
-
// }
|
|
470
|
-
// const skillsHint = this.getSkillsHint();
|
|
471
|
-
// if (skillsHint) {
|
|
472
|
-
// contextParts.push(`[EvolClaw 自管理] ${skillsHint}`);
|
|
473
|
-
// }
|
|
474
|
-
// 6. Proactive 模式提示词:agent 的输出不会自动发送,必须主动调用 ctl send/file
|
|
461
|
+
// 3. Proactive 模式提示词
|
|
475
462
|
if (isProactive) {
|
|
476
|
-
contextParts.push('
|
|
477
|
-
'- 发送文本:evolclaw ctl send "<消息内容>"\n' +
|
|
478
|
-
'- 发送文件:evolclaw ctl file <路径>\n' +
|
|
479
|
-
'可多次调用。如不调用,用户将看不到任何回复。');
|
|
463
|
+
contextParts.push(renderPromptSection('proactive', {}));
|
|
480
464
|
}
|
|
481
465
|
const effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
482
466
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
@@ -560,22 +544,22 @@ export class MessageProcessor {
|
|
|
560
544
|
&& targetSpec !== currentChannelType;
|
|
561
545
|
// 跨通道仅限 owner
|
|
562
546
|
if (isCrossChannel && session.identity?.role !== 'owner') {
|
|
563
|
-
await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`,
|
|
547
|
+
await adapter.sendText(message.channelId, `\u274c 跨通道发送仅限管理员`, taskReplyContext());
|
|
564
548
|
continue;
|
|
565
549
|
}
|
|
566
550
|
const resolvedPath = this.resolveFilePath(filePath, absoluteProjectPath);
|
|
567
551
|
if (!fs.existsSync(resolvedPath)) {
|
|
568
552
|
logger.warn(`[${adapter.channelName}] File not found: ${resolvedPath}`);
|
|
569
|
-
await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`,
|
|
553
|
+
await adapter.sendText(message.channelId, `\u26a0\ufe0f 文件未找到: ${filePath}`, taskReplyContext());
|
|
570
554
|
continue;
|
|
571
555
|
}
|
|
572
556
|
// 找目标 adapter
|
|
573
557
|
if (!targetInfo) {
|
|
574
|
-
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`,
|
|
558
|
+
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 未启用或不存在`, taskReplyContext());
|
|
575
559
|
continue;
|
|
576
560
|
}
|
|
577
561
|
if (!targetInfo.adapter.sendFile) {
|
|
578
|
-
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 不支持文件发送`,
|
|
562
|
+
await adapter.sendText(message.channelId, `\u274c 通道 ${targetLabel} 不支持文件发送`, taskReplyContext());
|
|
579
563
|
continue;
|
|
580
564
|
}
|
|
581
565
|
// 找目标 channelId
|
|
@@ -586,21 +570,21 @@ export class MessageProcessor {
|
|
|
586
570
|
const ownerPeerId = getOwner(this.config, targetAdapterName);
|
|
587
571
|
targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannelType, ownerPeerId) ?? '') : '';
|
|
588
572
|
if (!targetChannelId) {
|
|
589
|
-
await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`,
|
|
573
|
+
await adapter.sendText(message.channelId, `\u274c 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息`, taskReplyContext());
|
|
590
574
|
continue;
|
|
591
575
|
}
|
|
592
576
|
}
|
|
593
577
|
logger.info(`[${adapter.channelName}] Sending file via ${targetInfo.adapter.channelName}: ${resolvedPath}`);
|
|
594
578
|
try {
|
|
595
|
-
await targetInfo.adapter.sendFile(targetChannelId, resolvedPath,
|
|
579
|
+
await targetInfo.adapter.sendFile(targetChannelId, resolvedPath, taskReplyContext());
|
|
596
580
|
this.eventBus.publish({ type: 'agent:file-sent', sessionId: session.id, filePath: resolvedPath, channel: targetInfo.adapter.channelName });
|
|
597
581
|
if (isCrossChannel) {
|
|
598
|
-
await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetLabel} 发送`,
|
|
582
|
+
await adapter.sendText(message.channelId, `\ud83d\udcce 文件已通过 ${targetLabel} 发送`, taskReplyContext());
|
|
599
583
|
}
|
|
600
584
|
}
|
|
601
585
|
catch (error) {
|
|
602
586
|
logger.error(`[${adapter.channelName}] Failed to send file: ${resolvedPath}`, error);
|
|
603
|
-
await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`,
|
|
587
|
+
await adapter.sendText(message.channelId, `\u274c 文件发送失败: ${filePath}`, taskReplyContext());
|
|
604
588
|
}
|
|
605
589
|
}
|
|
606
590
|
} // end of !isProactive
|
|
@@ -684,7 +668,7 @@ export class MessageProcessor {
|
|
|
684
668
|
if (isFinallyBackground) {
|
|
685
669
|
const projectName = path.basename(session.projectPath);
|
|
686
670
|
const count = this.messageCache.getCount(session.id);
|
|
687
|
-
await adapter.sendText(message.channelId, `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)
|
|
671
|
+
await adapter.sendText(message.channelId, `[\u540e\u53f0-${projectName}] \u2713 任务完成 (${count}条消息已缓存)`, taskReplyContext());
|
|
688
672
|
}
|
|
689
673
|
// 记录发送响应
|
|
690
674
|
logger.message({
|
|
@@ -759,11 +743,24 @@ export class MessageProcessor {
|
|
|
759
743
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
760
744
|
let sendOpts;
|
|
761
745
|
try {
|
|
762
|
-
|
|
746
|
+
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.config.projects?.defaultPath || process.cwd(), message.threadId);
|
|
763
747
|
sendOpts = this.getReplyContext(message);
|
|
764
748
|
}
|
|
765
749
|
catch { }
|
|
750
|
+
// 注入 taskId / chatmode(与任务主流程保持一致)
|
|
751
|
+
sendOpts = {
|
|
752
|
+
...(sendOpts ?? {}),
|
|
753
|
+
metadata: { ...(sendOpts?.metadata ?? {}), taskId, chatmode },
|
|
754
|
+
};
|
|
766
755
|
await adapter.sendText(message.channelId, userMessage, sendOpts);
|
|
756
|
+
// Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
|
|
757
|
+
if (thoughtEmitter) {
|
|
758
|
+
const thoughtErrorType = errType === ErrorType.CONTEXT_TOO_LONG ? 'context_too_long' :
|
|
759
|
+
errType === ErrorType.AUTH_ERROR ? 'auth' :
|
|
760
|
+
(errType === ErrorType.SDK_TIMEOUT || errType === ErrorType.STREAM_ERROR) ? 'network' :
|
|
761
|
+
'unknown';
|
|
762
|
+
thoughtEmitter.emit({ type: 'error', error: userMessage, errorType: thoughtErrorType }).catch(() => { });
|
|
763
|
+
}
|
|
767
764
|
}
|
|
768
765
|
}
|
|
769
766
|
}
|
|
@@ -799,8 +796,24 @@ export class MessageProcessor {
|
|
|
799
796
|
// 每收到事件重置空闲超时
|
|
800
797
|
const toolName = event.type === 'tool_use' ? event.name : undefined;
|
|
801
798
|
resetTimer(event.type, toolName);
|
|
802
|
-
//
|
|
803
|
-
|
|
799
|
+
// 记录所有事件类型(text / tool_use 附带摘要,便于排查)
|
|
800
|
+
let eventDetail = '';
|
|
801
|
+
if (event.type === 'text' && event.text) {
|
|
802
|
+
const preview = event.text.replace(/\s+/g, ' ').slice(0, 80);
|
|
803
|
+
eventDetail = ` text="${preview}${event.text.length > 80 ? '…' : ''}"`;
|
|
804
|
+
}
|
|
805
|
+
else if (event.type === 'tool_use') {
|
|
806
|
+
const input = event.input;
|
|
807
|
+
const desc = input?.description
|
|
808
|
+
|| input?.file_path
|
|
809
|
+
|| input?.pattern
|
|
810
|
+
|| (typeof input?.command === 'string' ? input.command.slice(0, 80) : '')
|
|
811
|
+
|| (typeof input?.prompt === 'string' ? input.prompt.slice(0, 80) : '')
|
|
812
|
+
|| (typeof input?.query === 'string' ? input.query.slice(0, 80) : '')
|
|
813
|
+
|| '';
|
|
814
|
+
eventDetail = ` tool=${event.name}${desc ? ` desc="${desc}"` : ''}`;
|
|
815
|
+
}
|
|
816
|
+
logger.info(`[MessageProcessor] Event: type=${event.type}${eventDetail}`);
|
|
804
817
|
// Proactive 可观测:将事件实时透传为 thought(fire-and-forget)
|
|
805
818
|
if (thoughtEmitter) {
|
|
806
819
|
thoughtEmitter.emit(event).catch(() => { });
|