evolclaw 3.0.0 → 3.1.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 +1 -1
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +7 -9
- package/dist/agents/codex-runner.js +2 -0
- package/dist/agents/gemini-runner.js +9 -9
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/aun/aid/identity.js +28 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/msg/group.js +3 -1
- package/dist/aun/msg/p2p.js +4 -1
- package/dist/channels/aun.js +353 -125
- package/dist/channels/dingtalk.js +2 -1
- package/dist/channels/feishu.js +118 -5
- package/dist/channels/qqbot.js +2 -1
- package/dist/channels/wechat.js +3 -1
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +279 -19
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +589 -0
- package/dist/config-store.js +37 -5
- package/dist/core/channel-loader.js +23 -10
- package/dist/core/command-handler.js +46 -22
- package/dist/core/evolagent.js +5 -10
- package/dist/core/message/im-renderer.js +50 -44
- package/dist/core/message/items-formatter.js +11 -4
- package/dist/core/message/message-bridge.js +7 -2
- package/dist/core/message/message-log.js +2 -0
- package/dist/core/message/message-processor.js +150 -99
- package/dist/core/message/message-queue.js +10 -3
- package/dist/core/permission.js +95 -3
- package/dist/core/session/session-manager.js +98 -64
- package/dist/core/trigger/scheduler.js +1 -1
- package/dist/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +137 -16
- package/dist/net-check.js +640 -0
- package/dist/paths.js +31 -40
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +10 -0
- package/dist/utils/cross-platform.js +17 -8
- package/dist/utils/error-utils.js +10 -2
- package/dist/utils/instance-registry.js +6 -5
- package/dist/utils/log-writer.js +2 -1
- package/dist/utils/logger.js +10 -0
- package/dist/utils/npm-ops.js +35 -3
- package/dist/utils/process-introspect.js +16 -38
- package/dist/watch-msg.js +26 -11
- package/evolclaw-install-aun.md +14 -2
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +43 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +7 -5
- package/dist/agents/templates.js +0 -122
- package/dist/data/prompts.md +0 -137
- package/kits/aun/meta.md +0 -25
- package/kits/aun/role.md +0 -25
- package/kits/templates/group.md +0 -20
- package/kits/templates/private.md +0 -9
- package/kits/templates/system-fragments/personal-context.md +0 -3
- package/kits/templates/system-fragments/self-intro.md +0 -5
- package/kits/templates/system-fragments/speaker-intro.md +0 -5
- package/kits/templates/system-fragments/venue-intro.md +0 -5
- /package/kits/{channels → docs/channels}/aun.md +0 -0
- /package/kits/{evolclaw/commands.md → docs/evolclaw/AGENT_CMD.md} +0 -0
- /package/kits/{evolclaw → docs/evolclaw}/self-summary.md +0 -0
- /package/kits/{evolclaw → docs/evolclaw}/tools.md +0 -0
- /package/kits/{evolclaw → docs/identity}/identity-tools.md +0 -0
|
@@ -10,7 +10,8 @@ import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError,
|
|
|
10
10
|
import { summarizeToolInput } from '../permission.js';
|
|
11
11
|
import { DEFAULT_PERMISSION_MODE } from '../../types.js';
|
|
12
12
|
import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
13
|
-
import {
|
|
13
|
+
import { renderKitSections } from '../../agents/kit-renderer.js';
|
|
14
|
+
import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
|
|
14
15
|
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
15
16
|
/**
|
|
16
17
|
* 构造 OutboundEnvelope —— 出站三件套的信封部分。
|
|
@@ -124,7 +125,8 @@ export class MessageProcessor {
|
|
|
124
125
|
const agent = this.agentRegistry.resolveByChannel(channelName);
|
|
125
126
|
if (!agent)
|
|
126
127
|
return null;
|
|
127
|
-
|
|
128
|
+
// chatmode 解析优先级:agent.config.chatmode > globalSettings.chatmode
|
|
129
|
+
const globalCm = agent.config?.chatmode ?? this.globalSettings.chatmode;
|
|
128
130
|
return agent.getContext(channelName, chatType, globalCm);
|
|
129
131
|
}
|
|
130
132
|
/**
|
|
@@ -182,7 +184,7 @@ export class MessageProcessor {
|
|
|
182
184
|
'/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
|
|
183
185
|
'/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
|
|
184
186
|
'/p ', '/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
|
|
185
|
-
'/aid', '/agentmd',
|
|
187
|
+
'/aid', '/agentmd', '/upgrade',
|
|
186
188
|
];
|
|
187
189
|
/** 判断消息内容是否为已知命令 */
|
|
188
190
|
isKnownCommand(content) {
|
|
@@ -345,6 +347,8 @@ export class MessageProcessor {
|
|
|
345
347
|
// 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
|
|
346
348
|
const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
|
347
349
|
const chatmode = session.sessionMode ?? 'interactive';
|
|
350
|
+
// 诊断日志:记录 inbound message_id 和生成的 task_id 的对应关系
|
|
351
|
+
logger.info(`[MessageProcessor] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode}`);
|
|
348
352
|
// 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
|
|
349
353
|
const taskReplyContext = () => {
|
|
350
354
|
const base = this.getReplyContext(message);
|
|
@@ -416,6 +420,10 @@ export class MessageProcessor {
|
|
|
416
420
|
send: async (payload) => {
|
|
417
421
|
if (isAutonomous)
|
|
418
422
|
return; // autonomous session: never send to channel
|
|
423
|
+
// proactive 模式:activity.batch 是 thought 协议内容,只发给支持 thought 的 channel
|
|
424
|
+
// (不支持 thought 的 channel 静默丢弃,避免降级为普通消息)
|
|
425
|
+
if (isProactive && payload.kind === 'activity.batch' && !adapter.capabilities?.thought)
|
|
426
|
+
return;
|
|
419
427
|
const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
420
428
|
if (isCurrentlyBackground)
|
|
421
429
|
return;
|
|
@@ -431,7 +439,7 @@ export class MessageProcessor {
|
|
|
431
439
|
}
|
|
432
440
|
}
|
|
433
441
|
if (payload.kind === 'result.text' && payload.isFinal) {
|
|
434
|
-
opts.title = '\
|
|
442
|
+
opts.title = '\u2705 \u6700\u7ec8\u56de\u590d:';
|
|
435
443
|
}
|
|
436
444
|
opts.metadata = { ...(opts.metadata ?? {}), taskId, chatmode };
|
|
437
445
|
const enrichedEnvelope = { ...envelope, replyContext: opts };
|
|
@@ -488,32 +496,15 @@ export class MessageProcessor {
|
|
|
488
496
|
// 动态构建运行时上下文提示
|
|
489
497
|
const contextParts = [];
|
|
490
498
|
const currentChannelType = options?.channelType || message.channel;
|
|
491
|
-
//
|
|
492
|
-
const peerName = message.peerName || session.metadata?.peerName;
|
|
493
|
-
const peerType = message.peerType;
|
|
494
|
-
const peerId = message.peerId;
|
|
499
|
+
// 提取 self 信息
|
|
495
500
|
const adapterAny = channelInfo.adapter;
|
|
496
501
|
const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
497
502
|
const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
return `${name} (${id})`;
|
|
501
|
-
return name || id || undefined;
|
|
502
|
-
};
|
|
503
|
-
const selfIdentity = formatIdentity(selfName, selfAid);
|
|
504
|
-
const peerIdentity = formatIdentity(peerName, peerId);
|
|
505
|
-
// 文件发送能力(按 channelType 去重)
|
|
506
|
-
let crossChannelTypes = [];
|
|
503
|
+
const peerName = message.peerName || session.metadata?.peerName;
|
|
504
|
+
// 文件发送能力
|
|
507
505
|
let currentCanSend = false;
|
|
508
506
|
if (!isProactive) {
|
|
509
|
-
const fileChannelTypes = new Set();
|
|
510
507
|
currentCanSend = !!(channelInfo.adapter.capabilities?.file);
|
|
511
|
-
for (const [, info] of this.channels) {
|
|
512
|
-
if (info.adapter.capabilities?.file) {
|
|
513
|
-
fileChannelTypes.add(info.options?.channelType || info.adapter.channelName);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
crossChannelTypes = [...fileChannelTypes].filter(t => t !== currentChannelType);
|
|
517
508
|
}
|
|
518
509
|
// 通道能力
|
|
519
510
|
const capParts = [];
|
|
@@ -523,49 +514,53 @@ export class MessageProcessor {
|
|
|
523
514
|
capParts.push('图片输出');
|
|
524
515
|
if (channelInfo.adapter.capabilities?.file)
|
|
525
516
|
capParts.push('文件发送');
|
|
526
|
-
// Personal layer
|
|
517
|
+
// Personal layer
|
|
527
518
|
const owningAgent = this.agentRegistry?.resolveByChannel(channelKey);
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
519
|
+
const persona = owningAgent?.getPersona?.() || undefined;
|
|
520
|
+
const working = owningAgent?.getWorkingMemory?.() || undefined;
|
|
521
|
+
if (persona)
|
|
522
|
+
contextParts.push(persona);
|
|
523
|
+
if (working)
|
|
524
|
+
contextParts.push(`[当前关注]\n${working}`);
|
|
525
|
+
// 计算 peerKey: <channel>#<urlEncode(peerId)>
|
|
526
|
+
const peerIdRaw = message.peerId;
|
|
527
|
+
const peerKey = (currentChannelType && peerIdRaw)
|
|
528
|
+
? `${currentChannelType}#${encodeURIComponent(peerIdRaw)}`
|
|
529
|
+
: undefined;
|
|
530
|
+
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
531
|
+
// Kit renderer: 组装上下文
|
|
532
|
+
const kitCtx = {
|
|
533
|
+
vars: {
|
|
534
|
+
EVOLCLAW_HOME: resolveRoot(),
|
|
535
|
+
PACKAGE_ROOT: getPackageRoot(),
|
|
536
|
+
CURRENT_PROJECT: absoluteProjectPath,
|
|
537
|
+
selfAid: selfAid || undefined,
|
|
538
|
+
selfName: selfName || undefined,
|
|
539
|
+
hasPersona: !!persona,
|
|
540
|
+
hasWorkingMemory: !!working,
|
|
541
|
+
peerId: peerIdRaw || undefined,
|
|
542
|
+
peerKey,
|
|
543
|
+
peerName: peerName || undefined,
|
|
544
|
+
peerRole: session.identity?.role || 'unknown',
|
|
545
|
+
groupId: session.metadata?.groupId || undefined,
|
|
546
|
+
scene: session.chatType ? (session.chatType === 'group' ? 'group' : 'private') : 'coding',
|
|
547
|
+
chatType: session.chatType || null,
|
|
548
|
+
channel: currentChannelType || null,
|
|
549
|
+
venueUid: undefined,
|
|
550
|
+
project: path.basename(absoluteProjectPath),
|
|
551
|
+
sessionName: session.name || undefined,
|
|
552
|
+
sessionMode: isProactive ? 'proactive' : 'interactive',
|
|
553
|
+
readonly: session.metadata?.permissionMode === 'readonly',
|
|
554
|
+
canSendFile: !isProactive && currentCanSend,
|
|
555
|
+
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
556
|
+
baseAgent: normalizedBaseagent.canonical,
|
|
557
|
+
baseAgentName: normalizedBaseagent.displayName,
|
|
558
|
+
},
|
|
559
|
+
sessionId: session.id,
|
|
560
|
+
};
|
|
561
|
+
const kitContext = renderKitSections(kitCtx);
|
|
562
|
+
if (kitContext)
|
|
563
|
+
contextParts.push(kitContext);
|
|
569
564
|
effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
570
565
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
571
566
|
const MAX_RETRIES = 3;
|
|
@@ -616,6 +611,35 @@ export class MessageProcessor {
|
|
|
616
611
|
throw error;
|
|
617
612
|
}
|
|
618
613
|
}
|
|
614
|
+
// prompt_too_long:SDK 以 complete 事件(非异常)返回,需在此处触发 compact
|
|
615
|
+
// 检测条件:terminalReason 明确为 prompt_too_long,或文本/errors 包含相关错误文本
|
|
616
|
+
const contextTooLongPattern = /prompt is too long|input is too long|上下文过长/i;
|
|
617
|
+
const errorsText = streamResult.errors?.join(' ') || '';
|
|
618
|
+
const isPromptTooLong = streamResult.isError && session.agentSessionId && hasCompact(agent) && (streamResult.terminalReason === 'prompt_too_long' ||
|
|
619
|
+
contextTooLongPattern.test(streamResult.lastReplyText) ||
|
|
620
|
+
contextTooLongPattern.test(errorsText) ||
|
|
621
|
+
contextTooLongPattern.test(streamResult.fullText));
|
|
622
|
+
if (isPromptTooLong) {
|
|
623
|
+
renderer.addNotice('⚠️ 上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
624
|
+
await renderer.flush();
|
|
625
|
+
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
626
|
+
if (compacted) {
|
|
627
|
+
renderer.addNotice('✅ 压缩完成,正在重试...', 'info', 'compact-retry', true);
|
|
628
|
+
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
629
|
+
agent.registerStream(streamKey, retryStream);
|
|
630
|
+
streamResult = await this.processEventStream(retryStream, session, renderer, resetTimer, shouldSuppress);
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
throw new Error('CONTEXT_COMPACT_FAILED');
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
else if (streamResult.isError && !isPromptTooLong && (streamResult.terminalReason === 'prompt_too_long' ||
|
|
637
|
+
contextTooLongPattern.test(streamResult.lastReplyText) ||
|
|
638
|
+
contextTooLongPattern.test(errorsText) ||
|
|
639
|
+
contextTooLongPattern.test(streamResult.fullText))) {
|
|
640
|
+
// 上下文过长但无法 auto-compact(无 session ID 或 agent 不支持),显示友好提示
|
|
641
|
+
renderer.addNotice('⚠️ 上下文过长,请精简提问或使用 /compact 压缩上下文', 'warn', 'context-too-long', true);
|
|
642
|
+
}
|
|
619
643
|
// 处理文件标记 - 支持 [SEND_FILE:path] 和 [SEND_FILE:channel:path]
|
|
620
644
|
// 注意:始终扫描全部文本(含中间轮),因为文件标记可能出现在任意轮次
|
|
621
645
|
// suppressed 模式下 renderer 只有最后一轮文本,需要用 streamResult.fullText(SDK 全文)兜底
|
|
@@ -698,43 +722,35 @@ export class MessageProcessor {
|
|
|
698
722
|
}
|
|
699
723
|
}
|
|
700
724
|
} // end of !isProactive
|
|
701
|
-
//
|
|
702
|
-
// suppressed 模式:中间流式文本未推送,使用最后一轮回复(回退到全文)
|
|
703
|
-
// 非 suppressed 且无流式文本:同上
|
|
704
|
-
// 非 suppressed 且有流式文本:已经逐步推送过了,不重复添加
|
|
705
|
-
// 但如果 renderer 既未发送过内容也没有 pending 内容(如 text 事件全为空),仍需兜底
|
|
725
|
+
// 最终回复文本:suppressed 模式或无 text 事件时需要兜底添加
|
|
706
726
|
const finalReplyText = streamResult.lastReplyText || streamResult.fullText;
|
|
707
|
-
// 识别 Claude SDK 本地预处理兜底(如 "Unknown skill: xxx"):
|
|
708
|
-
// 特征:无流式 text + complete.result 匹配已知模式
|
|
709
|
-
// 这类输出不是 agent 的回复意图,而是 SDK 本地拦截到的"未知斜杠命令"提示。
|
|
710
|
-
// Proactive 模式下 renderer silent,需要兜底发出以告知用户,否则用户完全无反馈。
|
|
711
|
-
const isSdkFallbackMessage = !!finalReplyText
|
|
712
|
-
&& !streamResult.hasReceivedText
|
|
713
|
-
&& /^Unknown skill:\s+\S+/i.test(finalReplyText.trim());
|
|
714
727
|
if (finalReplyText) {
|
|
715
|
-
if (isProactive &&
|
|
716
|
-
// Proactive 模式 + SDK
|
|
728
|
+
if (isProactive && !streamResult.hasReceivedText && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim())) {
|
|
729
|
+
// Proactive 模式 + SDK 本地兜底:直接发送绕过 silent renderer
|
|
717
730
|
const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
|
|
718
731
|
if (!isCurrentlyBackground) {
|
|
719
732
|
await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text: finalReplyText, isFinal: true });
|
|
720
733
|
logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
|
|
721
734
|
}
|
|
722
735
|
}
|
|
723
|
-
else if (shouldSuppress()) {
|
|
724
|
-
renderer.addText(finalReplyText);
|
|
725
|
-
}
|
|
726
|
-
else if (!streamResult.hasReceivedText || (!renderer.hasSentContent() && !renderer.hasContent())) {
|
|
736
|
+
else if (shouldSuppress() || !streamResult.hasReceivedText) {
|
|
727
737
|
renderer.addText(finalReplyText);
|
|
728
738
|
}
|
|
729
739
|
}
|
|
730
|
-
//
|
|
731
|
-
await renderer.flush(true);
|
|
732
|
-
// 清理 activeStreams(正常完成)
|
|
740
|
+
// 先清理流和处理中状态(保证即使 flush 卡住,session 也不会永久处于"处理中")
|
|
733
741
|
agent.cleanupStream(streamKey);
|
|
734
742
|
logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
|
|
735
|
-
// 清除处理中状态
|
|
736
743
|
this.sessionManager.clearProcessing(session.id);
|
|
737
744
|
logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
|
|
745
|
+
// 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
|
|
746
|
+
const preFlushInterrupt = this.interruptedSessions.get(session.id);
|
|
747
|
+
if (preFlushInterrupt === 'new_message' || preFlushInterrupt === 'stop' || preFlushInterrupt === 'recalled') {
|
|
748
|
+
logger.info(`[MessageProcessor] Skipping flush for interrupted task=${taskId} reason=${preFlushInterrupt}`);
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
// Flush 剩余内容(文件标记已在 flush 时自动移除)
|
|
752
|
+
await renderer.flush(true);
|
|
753
|
+
}
|
|
738
754
|
// 更新 EvolAgent.lastActivity
|
|
739
755
|
if (this.agentRegistry) {
|
|
740
756
|
const owningAgent = this.agentRegistry.resolveByChannel(channelKey);
|
|
@@ -788,7 +804,7 @@ export class MessageProcessor {
|
|
|
788
804
|
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
789
805
|
}
|
|
790
806
|
else {
|
|
791
|
-
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs } }).catch(() => { });
|
|
807
|
+
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
|
|
792
808
|
}
|
|
793
809
|
}
|
|
794
810
|
if (message.triggerMeta) {
|
|
@@ -837,6 +853,8 @@ export class MessageProcessor {
|
|
|
837
853
|
agent: session.agentId || null,
|
|
838
854
|
model: agent.getModel?.() || null,
|
|
839
855
|
durationMs: Date.now() - startTime,
|
|
856
|
+
numTurns: streamResult.numTurns,
|
|
857
|
+
usage: streamResult.usage,
|
|
840
858
|
}));
|
|
841
859
|
}
|
|
842
860
|
}
|
|
@@ -961,7 +979,14 @@ export class MessageProcessor {
|
|
|
961
979
|
: path.resolve(process.cwd(), session.projectPath);
|
|
962
980
|
return { session, absoluteProjectPath };
|
|
963
981
|
}
|
|
964
|
-
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId);
|
|
982
|
+
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, undefined, undefined, undefined, undefined, message.peerType);
|
|
983
|
+
// 兜底纠正:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive。
|
|
984
|
+
// 新建场景已由 getOrCreateSession 内部 resolveDefaultSessionMode 处理,这里只兜底历史会话。
|
|
985
|
+
if (message.peerType && message.peerType !== 'human' && message.peerType !== 'unknown' && session.sessionMode !== 'proactive') {
|
|
986
|
+
logger.info(`[MessageProcessor] proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive (peerType=${message.peerType})`);
|
|
987
|
+
session.sessionMode = 'proactive';
|
|
988
|
+
await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
|
|
989
|
+
}
|
|
965
990
|
// replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
|
|
966
991
|
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
967
992
|
? session.projectPath
|
|
@@ -982,6 +1007,8 @@ export class MessageProcessor {
|
|
|
982
1007
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
983
1008
|
// 追踪最后一轮 assistant 回复文本(tool_use 之后的纯文本)
|
|
984
1009
|
let lastReplyText = '';
|
|
1010
|
+
// callId → description 映射,用于 tool_result 回显描述
|
|
1011
|
+
const toolDescByCallId = new Map();
|
|
985
1012
|
try {
|
|
986
1013
|
for await (const event of stream) {
|
|
987
1014
|
// 每收到事件重置空闲超时
|
|
@@ -1073,7 +1100,11 @@ export class MessageProcessor {
|
|
|
1073
1100
|
}
|
|
1074
1101
|
// 工具调用
|
|
1075
1102
|
if (event.type === 'tool_use') {
|
|
1076
|
-
//
|
|
1103
|
+
// 工具调用意味着当前 turn 结束,flush 已累积的文本作为独立消息
|
|
1104
|
+
if (renderer.hasTextPending()) {
|
|
1105
|
+
await renderer.flushText();
|
|
1106
|
+
}
|
|
1107
|
+
// 重置最后回复追踪
|
|
1077
1108
|
lastReplyText = '';
|
|
1078
1109
|
this.eventBus.publish({
|
|
1079
1110
|
type: 'tool:use',
|
|
@@ -1084,6 +1115,9 @@ export class MessageProcessor {
|
|
|
1084
1115
|
});
|
|
1085
1116
|
if (!shouldSuppress()) {
|
|
1086
1117
|
const desc = summarizeToolInput(event.name, event.input || {});
|
|
1118
|
+
if (event.callId) {
|
|
1119
|
+
toolDescByCallId.set(event.callId, desc);
|
|
1120
|
+
}
|
|
1087
1121
|
renderer.addToolCall(event.name, event.input, event.callId, desc);
|
|
1088
1122
|
}
|
|
1089
1123
|
}
|
|
@@ -1098,21 +1132,27 @@ export class MessageProcessor {
|
|
|
1098
1132
|
agentName: agentNameForStats,
|
|
1099
1133
|
timestamp: Date.now()
|
|
1100
1134
|
});
|
|
1135
|
+
// 从 tool_use 阶段缓存的描述中回溯
|
|
1136
|
+
const cachedDesc = event.callId ? toolDescByCallId.get(event.callId) : undefined;
|
|
1101
1137
|
if (event.isError && !shouldSuppress()) {
|
|
1102
1138
|
hasErrorResult = true;
|
|
1103
1139
|
let errorMsg = event.error || (typeof event.result === 'string' ? event.result : JSON.stringify(event.result)) || '\u6267\u884c\u5931\u8d25';
|
|
1104
1140
|
// 移除 XML 风格的错误标签
|
|
1105
1141
|
errorMsg = errorMsg.replace(/<tool_use_error>(.*?)<\/tool_use_error>/gs, '$1');
|
|
1106
|
-
renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId);
|
|
1142
|
+
renderer.addToolResult(event.name || '\u5de5\u5177', false, undefined, errorMsg, event.callId, undefined, cachedDesc);
|
|
1107
1143
|
}
|
|
1108
1144
|
else if (!event.isError && !shouldSuppress()) {
|
|
1109
|
-
renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId);
|
|
1145
|
+
renderer.addToolResult(event.name || '\u5de5\u5177', true, event.result, undefined, event.callId, undefined, cachedDesc);
|
|
1110
1146
|
}
|
|
1111
1147
|
}
|
|
1112
1148
|
// 运行时错误(Codex: turn.failed / item error)
|
|
1113
1149
|
if (event.type === 'error') {
|
|
1114
1150
|
logger.warn(`[MessageProcessor] error event: ${event.errorType}: ${event.error}`);
|
|
1115
|
-
|
|
1151
|
+
// 记录错误文本到 lastReplyText,供后续 isPromptTooLong 检测
|
|
1152
|
+
lastReplyText += event.error || '';
|
|
1153
|
+
// 上下文过长的错误不在此处输出 notice,留给外层 isPromptTooLong 触发 auto-compact
|
|
1154
|
+
const isContextError = /prompt is too long|input is too long|上下文过长/i.test(event.error || '');
|
|
1155
|
+
if (!isContextError && !hasErrorResult && !shouldSuppress()) {
|
|
1116
1156
|
hasErrorResult = true;
|
|
1117
1157
|
renderer.addNotice(`\u274c ${event.error}`, 'warn', 'runtime-error', true);
|
|
1118
1158
|
}
|
|
@@ -1121,19 +1161,23 @@ export class MessageProcessor {
|
|
|
1121
1161
|
// SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
|
|
1122
1162
|
// 仅记录状态,最终 flush(true) 在流结束后统一执行
|
|
1123
1163
|
if (event.type === 'complete') {
|
|
1124
|
-
logger.
|
|
1164
|
+
logger.info(`[MessageProcessor] complete event: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
|
|
1125
1165
|
// 自动回填会话名称
|
|
1126
1166
|
if (event.sessionTitle && session.name === '默认会话') {
|
|
1127
1167
|
await this.sessionManager.renameSession(session.id, event.sessionTitle);
|
|
1128
1168
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1129
1169
|
}
|
|
1130
1170
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
1131
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText };
|
|
1171
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
|
|
1132
1172
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
1133
1173
|
// 但用户主动中断(新消息打断 或 /stop 命令)时不显示错误提示
|
|
1174
|
+
// 上下文过长的错误留给外层 isPromptTooLong 触发 auto-compact,不在此处输出
|
|
1134
1175
|
const interruptReason = this.interruptedSessions.get(session.id);
|
|
1135
1176
|
const isUserInterrupt = interruptReason === 'new_message' || interruptReason === 'stop' || interruptReason === 'recalled';
|
|
1136
|
-
|
|
1177
|
+
const isContextTooLong = event.terminalReason === 'prompt_too_long'
|
|
1178
|
+
|| /prompt is too long|input is too long|上下文过长/i.test(event.errors?.join(' ') || '')
|
|
1179
|
+
|| /prompt is too long|input is too long|上下文过长/i.test(lastReplyText);
|
|
1180
|
+
if (event.isError && !hasErrorResult && !shouldSuppress() && !isUserInterrupt && !isContextTooLong) {
|
|
1137
1181
|
const errorSummary = event.errors?.join('; ') || '\u4efb\u52a1\u6267\u884c\u5931\u8d25';
|
|
1138
1182
|
// 使用 terminalReason 提供更友好的错误提示
|
|
1139
1183
|
const userFriendlyMessage = event.terminalReason
|
|
@@ -1165,7 +1209,7 @@ export class MessageProcessor {
|
|
|
1165
1209
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1166
1210
|
}
|
|
1167
1211
|
// 记录完成状态
|
|
1168
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText };
|
|
1212
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
|
|
1169
1213
|
if (event.subtype === 'success') {
|
|
1170
1214
|
this.messageCache.addEvent(session.id, {
|
|
1171
1215
|
type: 'completed',
|
|
@@ -1212,9 +1256,16 @@ export class MessageProcessor {
|
|
|
1212
1256
|
catch (error) {
|
|
1213
1257
|
// User interrupt (AbortError) is expected, log at info level
|
|
1214
1258
|
const catchInterruptReason = this.interruptedSessions.get(session.id);
|
|
1215
|
-
const catchIsUserInterrupt = catchInterruptReason === 'new_message' || catchInterruptReason === 'stop';
|
|
1259
|
+
const catchIsUserInterrupt = catchInterruptReason === 'new_message' || catchInterruptReason === 'stop' || catchInterruptReason === 'recalled';
|
|
1216
1260
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
1217
1261
|
logger.info('[MessageProcessor] Stream interrupted (AbortError)');
|
|
1262
|
+
// User-initiated interrupt: skip flush — new task takes over the channel,
|
|
1263
|
+
// flushing here would send a spurious "最终回复" before the new task's output
|
|
1264
|
+
if (catchIsUserInterrupt) {
|
|
1265
|
+
completeResult.isError = false;
|
|
1266
|
+
completeResult.hasReceivedText = hasReceivedText;
|
|
1267
|
+
return completeResult;
|
|
1268
|
+
}
|
|
1218
1269
|
}
|
|
1219
1270
|
else if (catchIsUserInterrupt) {
|
|
1220
1271
|
// SDK telemetry noise after user-initiated interrupt — not a real error
|
|
@@ -80,7 +80,8 @@ export class MessageQueue {
|
|
|
80
80
|
}
|
|
81
81
|
const queueKey = this.getQueueKey(sessionKey, projectPath);
|
|
82
82
|
const agentName = options?.agentName || DEFAULT_AGENT_NAME;
|
|
83
|
-
|
|
83
|
+
const isProcessing = this.processing.has(queueKey);
|
|
84
|
+
logger.info(`[Queue] enqueue: key=${queueKey} processing=${isProcessing} queueLen=${this.queues.get(queueKey)?.length ?? 0} agent=${agentName}`);
|
|
84
85
|
return new Promise((resolve, reject) => {
|
|
85
86
|
if (!this.queues.has(queueKey)) {
|
|
86
87
|
this.queues.set(queueKey, []);
|
|
@@ -104,6 +105,12 @@ export class MessageQueue {
|
|
|
104
105
|
else {
|
|
105
106
|
// 群聊:FIFO,不打断
|
|
106
107
|
logger.debug(`[Queue] ${queueKey} is processing, message queued (FIFO)`);
|
|
108
|
+
this.eventBus?.publish({
|
|
109
|
+
type: 'task:queued',
|
|
110
|
+
channel: message.channel,
|
|
111
|
+
channelId: message.channelId,
|
|
112
|
+
replyContext: message.replyContext,
|
|
113
|
+
});
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
else {
|
|
@@ -114,7 +121,7 @@ export class MessageQueue {
|
|
|
114
121
|
}
|
|
115
122
|
async processNext(queueKey) {
|
|
116
123
|
this.processing.add(queueKey);
|
|
117
|
-
logger.
|
|
124
|
+
logger.info(`[Queue] processNext: start key=${queueKey}`);
|
|
118
125
|
while (true) {
|
|
119
126
|
// 等待外部锁释放(/compact, /clear 等快速命令)
|
|
120
127
|
const lock = this.getExternalLock(queueKey);
|
|
@@ -124,7 +131,7 @@ export class MessageQueue {
|
|
|
124
131
|
}
|
|
125
132
|
const queue = this.queues.get(queueKey);
|
|
126
133
|
if (!queue || queue.length === 0) {
|
|
127
|
-
logger.
|
|
134
|
+
logger.info(`[Queue] processNext: queue empty, releasing key=${queueKey}`);
|
|
128
135
|
this.processing.delete(queueKey);
|
|
129
136
|
this.processingAgent.delete(queueKey);
|
|
130
137
|
this.currentSessionKey = undefined;
|
package/dist/core/permission.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
2
3
|
import { renderActionAsText } from './interaction-router.js';
|
|
3
4
|
import { buildEnvelope, sendInteractionPayload } from './message/message-processor.js';
|
|
4
5
|
// 危险命令黑名单(正则表达式)
|
|
@@ -89,9 +90,15 @@ export function summarizeToolInput(toolName, input) {
|
|
|
89
90
|
return '';
|
|
90
91
|
const extractors = {
|
|
91
92
|
'Read': (i) => i.file_path,
|
|
92
|
-
'Edit': (i) => i
|
|
93
|
+
'Edit': (i) => formatEditSummary(i),
|
|
93
94
|
'Write': (i) => i.file_path,
|
|
94
|
-
'Bash': (i) =>
|
|
95
|
+
'Bash': (i) => {
|
|
96
|
+
const cmd = i.command?.substring(0, 80) || '';
|
|
97
|
+
const desc = i.description;
|
|
98
|
+
if (desc && cmd)
|
|
99
|
+
return `${cmd} | ${desc}`;
|
|
100
|
+
return cmd || desc;
|
|
101
|
+
},
|
|
95
102
|
'Grep': (i) => `pattern: ${i.pattern}`,
|
|
96
103
|
'Glob': (i) => `pattern: ${i.pattern}`,
|
|
97
104
|
'Agent': (i) => i.description || i.prompt?.substring(0, 80),
|
|
@@ -110,6 +117,8 @@ export function summarizeToolInput(toolName, input) {
|
|
|
110
117
|
},
|
|
111
118
|
'TaskCreate': (i) => i.subject || i.description?.substring(0, 80),
|
|
112
119
|
'TaskUpdate': (i) => i.status ? `${i.taskId} → ${i.status}` : i.taskId,
|
|
120
|
+
'TaskOutput': (i) => `${i.task_id || '?'}${i.block === false ? ' (non-blocking)' : ''}${i.timeout ? ` timeout=${i.timeout}ms` : ''}`,
|
|
121
|
+
'TaskStop': (i) => i.task_id || i.shell_id || '?',
|
|
113
122
|
'NotebookEdit': (i) => i.notebook_path,
|
|
114
123
|
'WebFetch': (i) => i.url,
|
|
115
124
|
'WebSearch': (i) => i.query?.substring(0, 80),
|
|
@@ -131,6 +140,81 @@ export function summarizeToolInput(toolName, input) {
|
|
|
131
140
|
|| input.url
|
|
132
141
|
|| '';
|
|
133
142
|
}
|
|
143
|
+
/** 为 Edit 工具生成 diff 风格摘要 */
|
|
144
|
+
function formatEditSummary(input) {
|
|
145
|
+
const filePath = input.file_path || '';
|
|
146
|
+
const oldStr = typeof input.old_string === 'string' ? input.old_string : '';
|
|
147
|
+
const newStr = typeof input.new_string === 'string' ? input.new_string : '';
|
|
148
|
+
if (!oldStr && !newStr)
|
|
149
|
+
return filePath;
|
|
150
|
+
const MAX_DIFF_LINES = 14;
|
|
151
|
+
const oldLines = oldStr.split('\n');
|
|
152
|
+
const newLines = newStr.split('\n');
|
|
153
|
+
// 尝试从文件中定位 old_string 的起始行号
|
|
154
|
+
let startLine = 0; // 0-based; 0 means unknown
|
|
155
|
+
if (filePath && oldStr) {
|
|
156
|
+
try {
|
|
157
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
158
|
+
const idx = content.indexOf(oldStr);
|
|
159
|
+
if (idx >= 0) {
|
|
160
|
+
startLine = content.slice(0, idx).split('\n').length; // 1-based
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// 文件不可读,行号留空
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const diffLines = [];
|
|
168
|
+
// 找公共前缀行数
|
|
169
|
+
let prefixLen = 0;
|
|
170
|
+
while (prefixLen < oldLines.length && prefixLen < newLines.length && oldLines[prefixLen] === newLines[prefixLen]) {
|
|
171
|
+
prefixLen++;
|
|
172
|
+
}
|
|
173
|
+
// 找公共后缀行数
|
|
174
|
+
let suffixLen = 0;
|
|
175
|
+
while (suffixLen < oldLines.length - prefixLen &&
|
|
176
|
+
suffixLen < newLines.length - prefixLen &&
|
|
177
|
+
oldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]) {
|
|
178
|
+
suffixLen++;
|
|
179
|
+
}
|
|
180
|
+
const CONTEXT = 2;
|
|
181
|
+
// 计算行号宽度(用于对齐)
|
|
182
|
+
const maxLineNo = startLine > 0 ? startLine + oldLines.length - 1 : 0;
|
|
183
|
+
const newMaxLineNo = startLine > 0 ? startLine + prefixLen + (newLines.length - suffixLen - prefixLen) - 1 : 0;
|
|
184
|
+
const padWidth = startLine > 0 ? Math.max(maxLineNo, newMaxLineNo).toString().length : 0;
|
|
185
|
+
// 格式化一行:行号 + 标记 + 内容
|
|
186
|
+
// 使用 Unicode 符号避免飞书 Markdown 将 "- " 解析为列表
|
|
187
|
+
const fmtLine = (lineNo, marker, text) => {
|
|
188
|
+
if (startLine > 0) {
|
|
189
|
+
return `${lineNo.toString().padStart(padWidth)} ${marker} ${text}`;
|
|
190
|
+
}
|
|
191
|
+
return `${marker} ${text}`;
|
|
192
|
+
};
|
|
193
|
+
// 上下文前缀(最多 CONTEXT 行)
|
|
194
|
+
const ctxStart = Math.max(0, prefixLen - CONTEXT);
|
|
195
|
+
for (let i = ctxStart; i < prefixLen; i++) {
|
|
196
|
+
diffLines.push(fmtLine(startLine + i, ' ', oldLines[i]));
|
|
197
|
+
}
|
|
198
|
+
// 删除行
|
|
199
|
+
const removedEnd = oldLines.length - suffixLen;
|
|
200
|
+
for (let i = prefixLen; i < removedEnd && diffLines.length < MAX_DIFF_LINES; i++) {
|
|
201
|
+
diffLines.push(fmtLine(startLine + i, '−', oldLines[i]));
|
|
202
|
+
}
|
|
203
|
+
// 新增行(行号从 prefixLen 位置开始递增)
|
|
204
|
+
const addedEnd = newLines.length - suffixLen;
|
|
205
|
+
for (let i = prefixLen; i < addedEnd && diffLines.length < MAX_DIFF_LINES; i++) {
|
|
206
|
+
diffLines.push(fmtLine(startLine + i, '+', newLines[i]));
|
|
207
|
+
}
|
|
208
|
+
// 上下文后缀(最多 CONTEXT 行)
|
|
209
|
+
const ctxEnd = Math.min(oldLines.length, removedEnd + CONTEXT);
|
|
210
|
+
for (let i = removedEnd; i < ctxEnd && diffLines.length < MAX_DIFF_LINES + 2; i++) {
|
|
211
|
+
diffLines.push(fmtLine(startLine + i, ' ', oldLines[i]));
|
|
212
|
+
}
|
|
213
|
+
if (diffLines.length > MAX_DIFF_LINES + 2) {
|
|
214
|
+
diffLines.splice(MAX_DIFF_LINES, diffLines.length, ' ...');
|
|
215
|
+
}
|
|
216
|
+
return `${filePath}\n\`\`\`\n${diffLines.join('\n')}\n\`\`\``;
|
|
217
|
+
}
|
|
134
218
|
export class PermissionGateway {
|
|
135
219
|
pending = new Map();
|
|
136
220
|
timeout = 5 * 60 * 1000;
|
|
@@ -222,7 +306,15 @@ export class PermissionGateway {
|
|
|
222
306
|
await sendPrompt(renderActionAsText(interaction));
|
|
223
307
|
}
|
|
224
308
|
return new Promise((resolve) => {
|
|
225
|
-
|
|
309
|
+
const timer = setTimeout(() => {
|
|
310
|
+
const pending = this.pending.get(requestId);
|
|
311
|
+
if (!pending)
|
|
312
|
+
return;
|
|
313
|
+
this.pending.delete(requestId);
|
|
314
|
+
this.eventBus?.publish({ type: 'permission:timeout', sessionId, requestId, toolName });
|
|
315
|
+
pending.resolve('deny');
|
|
316
|
+
}, this.timeout);
|
|
317
|
+
this.pending.set(requestId, { sessionId, toolName, resolve, timer });
|
|
226
318
|
// 注册到 InteractionRouter(卡片和文本降级都注册,统一路由)
|
|
227
319
|
if (context?.interactionRouter) {
|
|
228
320
|
context.interactionRouter.register(requestId, sessionId, (action) => {
|