evolclaw 3.1.5 → 3.1.7
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/CHANGELOG.md +68 -3
- package/dist/agents/claude-runner.js +69 -24
- package/dist/agents/kit-renderer.js +78 -321
- package/dist/agents/manifest-engine.js +243 -0
- package/dist/agents/message-renderer.js +112 -0
- package/dist/aun/aid/agentmd.js +10 -3
- package/dist/aun/msg/group.js +2 -2
- package/dist/channels/aun.js +154 -18
- package/dist/channels/dingtalk.js +1 -1
- package/dist/channels/feishu.js +31 -9
- package/dist/channels/qqbot.js +1 -1
- package/dist/channels/wechat.js +1 -1
- package/dist/channels/wecom.js +1 -1
- package/dist/cli/agent.js +10 -11
- package/dist/cli/bench.js +1 -5
- package/dist/cli/help.js +8 -0
- package/dist/cli/index.js +91 -128
- package/dist/cli/init.js +37 -21
- package/dist/cli/link-rules.js +1 -7
- package/dist/cli/model.js +231 -6
- package/dist/config-store.js +1 -22
- package/dist/core/command-handler.js +181 -48
- package/dist/core/evolagent.js +0 -18
- package/dist/core/message/im-renderer.js +9 -20
- package/dist/core/message/message-bridge.js +9 -10
- package/dist/core/message/message-processor.js +188 -39
- package/dist/core/message/message-queue.js +15 -1
- package/dist/core/relation/peer-identity.js +23 -11
- package/dist/core/trigger/parser.js +4 -4
- package/dist/core/trigger/scheduler.js +43 -13
- package/dist/index.js +102 -52
- package/dist/ipc.js +1 -1
- package/dist/utils/error-utils.js +6 -0
- package/dist/utils/process-introspect.js +7 -5
- package/kits/docs/INDEX.md +4 -8
- package/kits/docs/context-assembly.md +1 -0
- package/kits/docs/evolclaw/INDEX.md +43 -0
- package/kits/docs/evolclaw/group.md +13 -6
- package/kits/docs/evolclaw/model.md +51 -0
- package/kits/docs/evolclaw/msg.md +5 -0
- package/kits/docs/venues/group.md +13 -1
- package/kits/eck_manifest.json +9 -0
- package/kits/eck_message_manifest.json +14 -0
- package/kits/rules/06-channel.md +5 -1
- package/kits/templates/message-fragments/item.md +2 -0
- package/kits/templates/system-fragments/baseagent.md +7 -1
- package/kits/templates/system-fragments/channel.md +7 -5
- package/kits/templates/system-fragments/commands.md +19 -0
- package/kits/templates/system-fragments/session.md +12 -0
- package/kits/templates/system-fragments/venue.md +15 -0
- package/package.json +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
3
4
|
import crypto from 'crypto';
|
|
4
5
|
import { hasCompact } from '../../agents/claude-runner.js';
|
|
5
6
|
import { IMRenderer } from './im-renderer.js';
|
|
@@ -10,10 +11,41 @@ import { summarizeToolInput } from '../permission.js';
|
|
|
10
11
|
import { DEFAULT_PERMISSION_MODE } from '../../types.js';
|
|
11
12
|
import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
12
13
|
import { renderKitSections } from '../../agents/kit-renderer.js';
|
|
14
|
+
import { renderMessageBody } from '../../agents/message-renderer.js';
|
|
13
15
|
import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
|
|
14
16
|
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
15
17
|
import { formatPeerKey } from '../relation/peer-key.js';
|
|
16
18
|
import { resolveEffectiveModel } from '../model/model-scope.js';
|
|
19
|
+
/** OS 信息在进程生命周期内是常量,模块加载时算一次。例: "Windows 11 Pro (win32 10.0.26200)" */
|
|
20
|
+
const OS_INFO = (() => {
|
|
21
|
+
let label = '';
|
|
22
|
+
try {
|
|
23
|
+
label = os.version();
|
|
24
|
+
}
|
|
25
|
+
catch { /* 旧 Node 无 os.version */ }
|
|
26
|
+
return `${label ? label + ' ' : ''}(${os.platform()} ${os.release()})`;
|
|
27
|
+
})();
|
|
28
|
+
/** 当前 UTC 偏移,格式 +08:00 / -05:00。每条消息算(DST 安全)。 */
|
|
29
|
+
function currentTzOffset() {
|
|
30
|
+
const off = -new Date().getTimezoneOffset(); // 分钟,东区为正
|
|
31
|
+
const sign = off >= 0 ? '+' : '-';
|
|
32
|
+
const abs = Math.abs(off);
|
|
33
|
+
return `${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
|
|
34
|
+
}
|
|
35
|
+
/** 当前本地日期 YYYY-MM-DD(按运行环境时区)。系统提示词用,一天才变一次(缓存友好)。 */
|
|
36
|
+
function currentLocalDate() {
|
|
37
|
+
const d = new Date();
|
|
38
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
39
|
+
}
|
|
40
|
+
/** 当前本地星期几(中文,如「星期四」)。 */
|
|
41
|
+
function currentWeekday() {
|
|
42
|
+
try {
|
|
43
|
+
return new Intl.DateTimeFormat('zh-CN', { weekday: 'long' }).format(new Date());
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][new Date().getDay()];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
17
49
|
function getContextTooLongHint(agent) {
|
|
18
50
|
if (canCompactAgent(agent)) {
|
|
19
51
|
return '上下文过长,请精简提问或使用 /compact 压缩上下文';
|
|
@@ -69,6 +101,8 @@ export class MessageProcessor {
|
|
|
69
101
|
agentMap;
|
|
70
102
|
primaryRunnerKey;
|
|
71
103
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
104
|
+
/** sessionId → 模型降级状态(带退避探测,进程重启清零) */
|
|
105
|
+
modelFallbackMap = new Map();
|
|
72
106
|
interactionRouter;
|
|
73
107
|
messageQueue;
|
|
74
108
|
/** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
|
|
@@ -208,15 +242,15 @@ export class MessageProcessor {
|
|
|
208
242
|
}
|
|
209
243
|
// 命令前缀列表(与 CommandHandler.quickCommandPrefixes 保持同步)
|
|
210
244
|
static COMMAND_PREFIXES = [
|
|
211
|
-
'/new', '/pwd', '/
|
|
245
|
+
'/new', '/pwd', '/help', '/status', '/restart',
|
|
212
246
|
'/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
|
|
213
247
|
'/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
|
|
214
|
-
'/
|
|
248
|
+
'/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
|
|
215
249
|
'/aid', '/upgrade', '/evolagent',
|
|
216
250
|
];
|
|
217
251
|
/** 判断消息内容是否为已知命令 */
|
|
218
252
|
isKnownCommand(content) {
|
|
219
|
-
return content === '/
|
|
253
|
+
return content === '/s' ||
|
|
220
254
|
MessageProcessor.COMMAND_PREFIXES.some(cmd => content.startsWith(cmd));
|
|
221
255
|
}
|
|
222
256
|
/**
|
|
@@ -227,6 +261,29 @@ export class MessageProcessor {
|
|
|
227
261
|
// 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
|
|
228
262
|
// message.channel 现在存实例名(channelName),可直接用于精确路由
|
|
229
263
|
const { session, absoluteProjectPath } = await this.resolveSession(message);
|
|
264
|
+
// thread(feishu) pending strategy: inject replyContext so first reply creates the thread
|
|
265
|
+
if (message.triggerMeta?.pendingThread && message.triggerMeta?.rootMessageId) {
|
|
266
|
+
const triggerId = message.triggerMeta.triggerId;
|
|
267
|
+
const channelKeyForAgent = session.metadata?.channelKey || message.channel;
|
|
268
|
+
const trigMgr = this.agentRegistry?.resolveByChannel(channelKeyForAgent)?.triggerManager;
|
|
269
|
+
const onThreadCreated = trigMgr
|
|
270
|
+
? (threadId) => {
|
|
271
|
+
try {
|
|
272
|
+
trigMgr.update(triggerId, { targetThreadId: threadId, pendingThread: false });
|
|
273
|
+
logger.info(`[MessageProcessor] Feishu thread created for trigger ${triggerId}: ${threadId}`);
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
logger.warn(`[MessageProcessor] Failed to write back thread_id for trigger ${triggerId}: ${e}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
: undefined;
|
|
280
|
+
message.replyContext = {
|
|
281
|
+
...(message.replyContext ?? {}),
|
|
282
|
+
replyToMessageId: message.triggerMeta.rootMessageId,
|
|
283
|
+
replyInThread: true,
|
|
284
|
+
...(onThreadCreated ? { metadata: { ...(message.replyContext?.metadata ?? {}), onThreadCreated } } : {}),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
230
287
|
const channelKey = session.metadata?.channelKey || message.channel;
|
|
231
288
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
232
289
|
if (!channelInfo) {
|
|
@@ -388,7 +445,7 @@ export class MessageProcessor {
|
|
|
388
445
|
};
|
|
389
446
|
};
|
|
390
447
|
const isProactive = session.sessionMode === 'proactive';
|
|
391
|
-
const isAutonomous = session.sessionMode === 'autonomous'
|
|
448
|
+
const isAutonomous = session.sessionMode === 'autonomous';
|
|
392
449
|
const envelope = buildEnvelope({
|
|
393
450
|
taskId,
|
|
394
451
|
channel: message.channel,
|
|
@@ -518,12 +575,18 @@ export class MessageProcessor {
|
|
|
518
575
|
// 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
|
|
519
576
|
const prevInterruptReason = this.interruptedSessions.get(session.id);
|
|
520
577
|
this.interruptedSessions.delete(session.id);
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
578
|
+
const wasInterrupted = prevInterruptReason === 'new_message' && !!session.agentSessionId;
|
|
579
|
+
const wrapPrompt = (body) => wasInterrupted
|
|
580
|
+
? `【新消息插入】\n\n${body}\n\n【请无视之前中断继续处理】`
|
|
581
|
+
: body;
|
|
582
|
+
// 先用裸文本兜底;vars 构造完成后用消息渲染层重算(见下方 effectivePrompt 重赋值)。
|
|
583
|
+
let effectivePrompt = wrapPrompt(message.content);
|
|
524
584
|
let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
525
585
|
let effectiveSystemPrompt;
|
|
526
586
|
let modelOverride;
|
|
587
|
+
let usedFallback = false;
|
|
588
|
+
let skipEvolclawModel = false;
|
|
589
|
+
let agentModel;
|
|
527
590
|
try {
|
|
528
591
|
// 动态构建运行时上下文提示
|
|
529
592
|
const contextParts = [];
|
|
@@ -557,16 +620,35 @@ export class MessageProcessor {
|
|
|
557
620
|
// 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
|
|
558
621
|
// 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
|
|
559
622
|
// 多对端并发各自独立解析、各自传参,无共享状态可被污染。
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
623
|
+
let effectiveModel;
|
|
624
|
+
// 取降级状态,按退避策略决定是否跳过 evolclaw 作用域模型
|
|
625
|
+
const fbState = this.modelFallbackMap.get(session.id) ?? {
|
|
626
|
+
failCount: 0, fallbackActive: false,
|
|
627
|
+
messagesSinceFallback: 0, nextProbeAt: 2, hintShown: false,
|
|
628
|
+
};
|
|
629
|
+
// 退避期内递增消息计数,判断是否到探测点
|
|
630
|
+
if (fbState.fallbackActive) {
|
|
631
|
+
fbState.messagesSinceFallback++;
|
|
632
|
+
skipEvolclawModel = fbState.messagesSinceFallback < fbState.nextProbeAt;
|
|
633
|
+
this.modelFallbackMap.set(session.id, fbState);
|
|
564
634
|
}
|
|
565
|
-
|
|
566
|
-
|
|
635
|
+
// 非跳过时:尝试解析 evolclaw 作用域模型
|
|
636
|
+
let evolclawModelOverride;
|
|
637
|
+
if (!skipEvolclawModel) {
|
|
638
|
+
try {
|
|
639
|
+
const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
|
|
640
|
+
if (resolved.model) {
|
|
641
|
+
evolclawModelOverride = { model: resolved.model, effort: resolved.effort };
|
|
642
|
+
effectiveModel = resolved.model;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
catch (e) {
|
|
646
|
+
logger.warn(`[MessageProcessor] resolveEffectiveModel failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
647
|
+
}
|
|
648
|
+
modelOverride = evolclawModelOverride;
|
|
567
649
|
}
|
|
568
650
|
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
569
|
-
|
|
651
|
+
agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
570
652
|
// Kit renderer: 组装上下文
|
|
571
653
|
const pkgRoot = getPackageRoot();
|
|
572
654
|
const kitCtx = {
|
|
@@ -580,6 +662,9 @@ export class MessageProcessor {
|
|
|
580
662
|
KITS_DOCS: path.join(pkgRoot, 'kits', 'docs'),
|
|
581
663
|
KITS_TEMPLATES: path.join(pkgRoot, 'kits', 'templates'),
|
|
582
664
|
KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
|
|
665
|
+
KITS_MESSAGE_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'message-fragments'),
|
|
666
|
+
// evolclaw 运行模式:dev=源码仓库 | install=全局安装包
|
|
667
|
+
evolclawMode: fs.existsSync(path.join(pkgRoot, 'src', 'index.ts')) ? 'dev' : 'install',
|
|
583
668
|
// 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
|
|
584
669
|
PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
|
|
585
670
|
RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
|
|
@@ -593,6 +678,9 @@ export class MessageProcessor {
|
|
|
593
678
|
peerName: peerName || undefined,
|
|
594
679
|
peerRole: session.identity?.role || 'anonymous',
|
|
595
680
|
peerType: message.peerType || undefined,
|
|
681
|
+
sameDevice: message.sameDevice || undefined,
|
|
682
|
+
sameNetwork: message.sameNetwork || undefined,
|
|
683
|
+
sameEgressIp: message.sameEgressIp || undefined,
|
|
596
684
|
groupId: session.metadata?.groupId || undefined,
|
|
597
685
|
chatType: session.chatType || null,
|
|
598
686
|
channel: currentChannelType || null,
|
|
@@ -606,6 +694,12 @@ export class MessageProcessor {
|
|
|
606
694
|
sessionId: session.id,
|
|
607
695
|
sessionName: session.name || undefined,
|
|
608
696
|
sessionCreatedAt: session.createdAt ? new Date(session.createdAt).toISOString() : undefined,
|
|
697
|
+
// 时区(把 ISO 时间戳转本地时间用)+ OS 环境
|
|
698
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || undefined,
|
|
699
|
+
tzOffset: currentTzOffset(),
|
|
700
|
+
localDate: currentLocalDate(),
|
|
701
|
+
weekday: currentWeekday(),
|
|
702
|
+
osInfo: OS_INFO,
|
|
609
703
|
threadId: session.threadId || undefined,
|
|
610
704
|
// Stage 3: sessionKey 持久化字段
|
|
611
705
|
sessionKey: session.sessionKey,
|
|
@@ -614,6 +708,9 @@ export class MessageProcessor {
|
|
|
614
708
|
baseAgent: normalizedBaseagent.canonical,
|
|
615
709
|
baseAgentName: normalizedBaseagent.displayName,
|
|
616
710
|
baseAgentModel: agentModel || undefined,
|
|
711
|
+
effectiveModel: effectiveModel || agentModel || undefined,
|
|
712
|
+
modelFallbackActive: (fbState.fallbackActive || skipEvolclawModel) ? true : undefined,
|
|
713
|
+
modelFallbackModel: (fbState.fallbackActive || skipEvolclawModel) ? (agentModel || undefined) : undefined,
|
|
617
714
|
agentSessionId: session.agentSessionId || undefined,
|
|
618
715
|
},
|
|
619
716
|
sessionId: session.id,
|
|
@@ -622,22 +719,67 @@ export class MessageProcessor {
|
|
|
622
719
|
if (kitContext)
|
|
623
720
|
contextParts.push(kitContext);
|
|
624
721
|
effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
722
|
+
// 消息渲染层:用 message manifest 逐条渲染(时间 + 群聊发送者),组装成最终正文。
|
|
723
|
+
// 单条消息构造单元素 items;批量合并的消息 message.items 已由队列填充。
|
|
724
|
+
let renderResult;
|
|
725
|
+
const hasContent = message.content.trim() || (message.items && message.items.length > 0);
|
|
726
|
+
if (hasContent) {
|
|
727
|
+
try {
|
|
728
|
+
const renderItems = message.items && message.items.length > 0
|
|
729
|
+
? message.items
|
|
730
|
+
: [{
|
|
731
|
+
peerId: message.peerId, peerName: peerName || undefined,
|
|
732
|
+
peerType: message.peerType, content: message.content,
|
|
733
|
+
timestamp: message.timestamp,
|
|
734
|
+
images: message.images,
|
|
735
|
+
}];
|
|
736
|
+
renderResult = renderMessageBody(renderItems, kitCtx.vars, session.id);
|
|
737
|
+
if (renderResult.body.trim())
|
|
738
|
+
effectivePrompt = wrapPrompt(renderResult.body);
|
|
739
|
+
else
|
|
740
|
+
effectivePrompt = wrapPrompt(message.content);
|
|
741
|
+
}
|
|
742
|
+
catch (e) {
|
|
743
|
+
logger.warn(`[MessageProcessor] renderMessageBody failed, using raw content: ${e instanceof Error ? e.message : String(e)}`);
|
|
744
|
+
effectivePrompt = wrapPrompt(message.content);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
625
747
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
626
748
|
const MAX_RETRIES = 3;
|
|
627
749
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
628
750
|
let streamRegistered = false;
|
|
629
751
|
try {
|
|
630
752
|
logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
|
|
631
|
-
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
753
|
+
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, renderResult?.images.length ? renderResult.images : message.images, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
632
754
|
agent.registerStream(streamKey, stream);
|
|
633
755
|
streamRegistered = true;
|
|
634
756
|
streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
757
|
+
// 探测成功(退避期内到达探测点且用的是 evolclaw 模型)→ 清零降级状态
|
|
758
|
+
if (fbState.fallbackActive && !skipEvolclawModel && !usedFallback) {
|
|
759
|
+
this.modelFallbackMap.delete(session.id);
|
|
760
|
+
logger.info(`[MessageProcessor] Model probe succeeded, cleared fallback state for session=${session.id}`);
|
|
761
|
+
}
|
|
635
762
|
break; // 成功,跳出重试循环
|
|
636
763
|
}
|
|
637
764
|
catch (retryError) {
|
|
638
765
|
if (streamRegistered) {
|
|
639
766
|
agent.cleanupStream(streamKey);
|
|
640
767
|
}
|
|
768
|
+
// 模型不可用:累计计数,本次切换到 baseAgentModel 立即重试,不让用户看到失败
|
|
769
|
+
if (classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE && evolclawModelOverride?.model) {
|
|
770
|
+
fbState.failCount++;
|
|
771
|
+
if (fbState.failCount >= 2) {
|
|
772
|
+
fbState.fallbackActive = true;
|
|
773
|
+
fbState.messagesSinceFallback = 0;
|
|
774
|
+
fbState.nextProbeAt = Math.min(Math.pow(2, fbState.failCount - 1), 8);
|
|
775
|
+
}
|
|
776
|
+
this.modelFallbackMap.set(session.id, fbState);
|
|
777
|
+
logger.warn(`[MessageProcessor] Model unavailable: ${evolclawModelOverride.model}, failCount=${fbState.failCount}, fallbackActive=${fbState.fallbackActive}`);
|
|
778
|
+
// 切换到 baseAgentModel 重试(清除 modelOverride,让 runQuery 使用 this.model)
|
|
779
|
+
modelOverride = undefined;
|
|
780
|
+
usedFallback = true;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
641
783
|
if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
|
|
642
784
|
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
|
|
643
785
|
logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
|
|
@@ -657,9 +799,6 @@ export class MessageProcessor {
|
|
|
657
799
|
await renderer.flush();
|
|
658
800
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
659
801
|
if (compacted) {
|
|
660
|
-
// compact 成功,清除第一次流中混入的错误文本,再重试
|
|
661
|
-
const ctxErrPattern = /prompt is too long|input is too long|上下文过长/i;
|
|
662
|
-
renderer.stripContextError(ctxErrPattern);
|
|
663
802
|
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
664
803
|
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
665
804
|
agent.registerStream(streamKey, retryStream);
|
|
@@ -682,7 +821,6 @@ export class MessageProcessor {
|
|
|
682
821
|
contextTooLongPattern.test(errorsText) ||
|
|
683
822
|
contextTooLongPattern.test(streamResult.fullText));
|
|
684
823
|
if (isPromptTooLong) {
|
|
685
|
-
renderer.stripContextError(contextTooLongPattern);
|
|
686
824
|
renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
687
825
|
await renderer.flush();
|
|
688
826
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
@@ -698,7 +836,6 @@ export class MessageProcessor {
|
|
|
698
836
|
contextTooLongPattern.test(retryErrorsText) ||
|
|
699
837
|
contextTooLongPattern.test(streamResult.fullText));
|
|
700
838
|
if (retryStillTooLong) {
|
|
701
|
-
renderer.stripContextError(contextTooLongPattern);
|
|
702
839
|
renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
|
|
703
840
|
}
|
|
704
841
|
}
|
|
@@ -815,6 +952,20 @@ export class MessageProcessor {
|
|
|
815
952
|
logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
|
|
816
953
|
this.sessionManager.clearProcessing(session.id);
|
|
817
954
|
logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
|
|
955
|
+
// 降级模型回复末尾追加标记(代码层硬注入,不依赖模型输出)
|
|
956
|
+
const usingFallback = usedFallback || (skipEvolclawModel && agentModel != null);
|
|
957
|
+
if (usingFallback && agentModel) {
|
|
958
|
+
const curFbState = this.modelFallbackMap.get(session.id);
|
|
959
|
+
const showHint = curFbState && curFbState.nextProbeAt >= 8 && !curFbState.hintShown;
|
|
960
|
+
const suffix = showHint
|
|
961
|
+
? `\n\n---\n⚠️ [降级模型: ${agentModel} | 可告诉我"帮我检查可用模型"来诊断]`
|
|
962
|
+
: `\n\n---\n⚠️ [降级模型: ${agentModel}]`;
|
|
963
|
+
renderer.addText(suffix);
|
|
964
|
+
if (showHint && curFbState) {
|
|
965
|
+
curFbState.hintShown = true;
|
|
966
|
+
this.modelFallbackMap.set(session.id, curFbState);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
818
969
|
// 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
|
|
819
970
|
const preFlushInterrupt = this.interruptedSessions.get(session.id);
|
|
820
971
|
if (preFlushInterrupt === 'new_message' || preFlushInterrupt === 'stop' || preFlushInterrupt === 'recalled') {
|
|
@@ -877,7 +1028,7 @@ export class MessageProcessor {
|
|
|
877
1028
|
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
878
1029
|
}
|
|
879
1030
|
else {
|
|
880
|
-
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, ttftMs: streamResult.ttftMs, numTurns: streamResult.numTurns,
|
|
1031
|
+
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, ttftMs: streamResult.ttftMs, numTurns: streamResult.numTurns, tokenUsage: streamResult.tokenUsage, contextUsage: streamResult.contextUsage } }).catch(() => { });
|
|
881
1032
|
}
|
|
882
1033
|
}
|
|
883
1034
|
if (message.triggerMeta) {
|
|
@@ -887,10 +1038,6 @@ export class MessageProcessor {
|
|
|
887
1038
|
else {
|
|
888
1039
|
this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, messageId: messageId, durationMs });
|
|
889
1040
|
}
|
|
890
|
-
// Clean up autonomous sessions after completion to avoid accumulating orphaned sessions
|
|
891
|
-
if (session.sessionMode === 'autonomous') {
|
|
892
|
-
this.sessionManager.unbindSession(session.id).catch(() => { });
|
|
893
|
-
}
|
|
894
1041
|
}
|
|
895
1042
|
await this.sessionManager.recordSuccess(session.id);
|
|
896
1043
|
this.eventBus.publish({
|
|
@@ -1023,19 +1170,21 @@ export class MessageProcessor {
|
|
|
1023
1170
|
? { replyContext: message.replyContext }
|
|
1024
1171
|
: undefined;
|
|
1025
1172
|
const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
|
|
1026
|
-
//
|
|
1027
|
-
if (message.triggerMeta?.
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1173
|
+
// current strategy: resume bound session, make it active so output is not suppressed
|
|
1174
|
+
if (message.triggerMeta?.boundSessionId) {
|
|
1175
|
+
const bound = await this.sessionManager.getSessionById(message.triggerMeta.boundSessionId);
|
|
1176
|
+
if (bound) {
|
|
1177
|
+
const switched = await this.sessionManager.switchToSession(bound.channel, bound.channelId, bound.id);
|
|
1178
|
+
if (switched) {
|
|
1179
|
+
const absoluteProjectPath = path.isAbsolute(switched.projectPath)
|
|
1180
|
+
? switched.projectPath : path.resolve(process.cwd(), switched.projectPath);
|
|
1181
|
+
return { session: switched, absoluteProjectPath };
|
|
1182
|
+
}
|
|
1183
|
+
logger.warn(`[MessageProcessor] switchToSession failed for bound session ${bound.id}, falling back to latest`);
|
|
1184
|
+
}
|
|
1185
|
+
else {
|
|
1186
|
+
logger.warn(`[MessageProcessor] Bound session ${message.triggerMeta.boundSessionId} not found, falling back to latest`);
|
|
1034
1187
|
}
|
|
1035
|
-
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
1036
|
-
? session.projectPath
|
|
1037
|
-
: path.resolve(process.cwd(), session.projectPath);
|
|
1038
|
-
return { session, absoluteProjectPath };
|
|
1039
1188
|
}
|
|
1040
1189
|
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, message.chatType, undefined, message.selfAID, message.channelType, message.peerType);
|
|
1041
1190
|
// 兜底纠正1:群聊强制 proactive
|
|
@@ -1240,7 +1389,7 @@ export class MessageProcessor {
|
|
|
1240
1389
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1241
1390
|
}
|
|
1242
1391
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
1243
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs,
|
|
1392
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage };
|
|
1244
1393
|
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
1245
1394
|
// 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
|
|
1246
1395
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
@@ -1296,7 +1445,7 @@ export class MessageProcessor {
|
|
|
1296
1445
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1297
1446
|
}
|
|
1298
1447
|
// 记录完成状态
|
|
1299
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs,
|
|
1448
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage };
|
|
1300
1449
|
if (event.subtype === 'success') {
|
|
1301
1450
|
this.messageCache.addEvent(session.id, {
|
|
1302
1451
|
type: 'completed',
|
|
@@ -184,7 +184,8 @@ export class MessageQueue {
|
|
|
184
184
|
}
|
|
185
185
|
/**
|
|
186
186
|
* 合并多条同 peerId 消息:
|
|
187
|
-
* - content: \n
|
|
187
|
+
* - content: \n 连接(兜底用,渲染层优先用 items)
|
|
188
|
+
* - items: 保留每条子消息(含各自 peer/timestamp),供消息渲染层逐条渲染
|
|
188
189
|
* - images / mentions: 扁平合并
|
|
189
190
|
* - messageId: 取最新一条的 messageId(用于 thought 锚定与中断追踪)
|
|
190
191
|
* - replyContext / peerName / 其余字段: 取最后一条
|
|
@@ -193,6 +194,7 @@ export class MessageQueue {
|
|
|
193
194
|
const contents = [];
|
|
194
195
|
const allImages = [];
|
|
195
196
|
const allMentions = [];
|
|
197
|
+
const subMessages = [];
|
|
196
198
|
for (const item of items) {
|
|
197
199
|
const m = item.message;
|
|
198
200
|
contents.push(m.content);
|
|
@@ -200,6 +202,17 @@ export class MessageQueue {
|
|
|
200
202
|
allImages.push(...m.images);
|
|
201
203
|
if (m.mentions)
|
|
202
204
|
allMentions.push(...m.mentions);
|
|
205
|
+
// 逐条保留发送者、时刻、图片;若该条已自带 items(罕见),展开保留细粒度
|
|
206
|
+
if (m.items && m.items.length > 0) {
|
|
207
|
+
subMessages.push(...m.items);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
subMessages.push({
|
|
211
|
+
peerId: m.peerId, peerName: m.peerName, peerType: m.peerType,
|
|
212
|
+
content: m.content, timestamp: m.timestamp,
|
|
213
|
+
images: m.images && m.images.length > 0 ? m.images : undefined,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
203
216
|
}
|
|
204
217
|
const last = items[items.length - 1];
|
|
205
218
|
// 保留最新一条的 messageId(若最后一条无 ID 则回退到前面已有的 ID)
|
|
@@ -213,6 +226,7 @@ export class MessageQueue {
|
|
|
213
226
|
const merged = {
|
|
214
227
|
...last.message,
|
|
215
228
|
content: contents.join('\n'),
|
|
229
|
+
items: subMessages,
|
|
216
230
|
images: allImages.length > 0 ? allImages : undefined,
|
|
217
231
|
mentions: allMentions.length > 0 ? allMentions : undefined,
|
|
218
232
|
messageId: latestMessageId,
|
|
@@ -56,8 +56,10 @@ export class PeerIdentityCache {
|
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* 从 agent.md 更新身份信息
|
|
59
|
+
* @param source 'agentmd'(验签通过)或 'agentmd-unverified'(内容可解析但验签未过)
|
|
60
|
+
* @param verifiedAt 验签通过时间戳;未验签传 0
|
|
59
61
|
*/
|
|
60
|
-
static updateFromAgentMd(channelType, peerId, agentDir, agentMd, verifiedAt) {
|
|
62
|
+
static updateFromAgentMd(channelType, peerId, agentDir, agentMd, verifiedAt, source = 'agentmd') {
|
|
61
63
|
const typeMatch = agentMd.match(/^type:\s*["']?([^"'\n]+?)["']?\s*$/m);
|
|
62
64
|
const nameMatch = agentMd.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
63
65
|
const type = typeMatch?.[1] || 'unknown';
|
|
@@ -74,13 +76,13 @@ export class PeerIdentityCache {
|
|
|
74
76
|
agentMdUpdatedAt: now,
|
|
75
77
|
verifiedAt,
|
|
76
78
|
lastCheckedAt: now,
|
|
77
|
-
source
|
|
79
|
+
source,
|
|
78
80
|
};
|
|
79
81
|
const filePath = this.getFilePath(channelType, peerId, agentDir);
|
|
80
82
|
try {
|
|
81
83
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
82
84
|
fs.writeFileSync(filePath, JSON.stringify(identity, null, 2), 'utf-8');
|
|
83
|
-
logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type} isAgent=${isAgent}`);
|
|
85
|
+
logger.debug(`[PeerIdentityCache] Updated: ${channelType}#${peerId} type=${type} isAgent=${isAgent} source=${source}`);
|
|
84
86
|
}
|
|
85
87
|
catch (err) {
|
|
86
88
|
logger.warn(`[PeerIdentityCache] Failed to write cache: ${filePath} err=${err}`);
|
|
@@ -151,27 +153,37 @@ export class PeerIdentityCache {
|
|
|
151
153
|
if (!content) {
|
|
152
154
|
throw new Error('agent.md content unavailable');
|
|
153
155
|
}
|
|
154
|
-
//
|
|
156
|
+
// 验签通过 → 可信(source=agentmd);否则 type 仍解析但标记未验证
|
|
157
|
+
const verified = result.verification?.status === 'verified';
|
|
158
|
+
const source = verified ? 'agentmd' : 'agentmd-unverified';
|
|
159
|
+
if (!verified) {
|
|
160
|
+
logger.info(`[PeerIdentityCache] agent.md unverified for ${peerId}: status=${result.verification?.status ?? 'unknown'} reason=${result.verification?.reason ?? '-'} (type 仍按声明解析)`);
|
|
161
|
+
}
|
|
162
|
+
// 3. 比较 hash,内容与可信级别都未变时仅 touch
|
|
155
163
|
const newHash = 'sha256:' + crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
|
|
156
164
|
const cached = this.get(channelType, peerId, agentDir);
|
|
157
|
-
if (cached && cached.agentMdHash === newHash && cached.source ===
|
|
165
|
+
if (cached && cached.agentMdHash === newHash && cached.source === source) {
|
|
158
166
|
return this.touchLastChecked(channelType, peerId, agentDir, cached);
|
|
159
167
|
}
|
|
160
|
-
return this.updateFromAgentMd(channelType, peerId, agentDir, content, Date.now());
|
|
168
|
+
return this.updateFromAgentMd(channelType, peerId, agentDir, content, verified ? Date.now() : 0, source);
|
|
161
169
|
}
|
|
162
170
|
catch (err) {
|
|
163
|
-
// 4.
|
|
171
|
+
// 4. agentmdSync 抛错(非网络失败——网络失败时它内部已 fallback 返回本地内容;
|
|
172
|
+
// 这里通常是无内容或解析异常)。兜底读本地文件,但无法重新验签,
|
|
173
|
+
// 故沿用缓存里已有的可信级别,绝不凭空升级为已验签。
|
|
164
174
|
const localPath = agentMdPath(peerId);
|
|
165
175
|
try {
|
|
166
176
|
if (fs.existsSync(localPath)) {
|
|
167
177
|
const localContent = fs.readFileSync(localPath, 'utf-8');
|
|
168
|
-
logger.info(`[PeerIdentityCache] Network failed, using local agent.md for ${peerId}`);
|
|
169
|
-
const localHash = 'sha256:' + crypto.createHash('sha256').update(localContent, 'utf-8').digest('hex');
|
|
170
178
|
const cached = this.get(channelType, peerId, agentDir);
|
|
171
|
-
|
|
179
|
+
logger.info(`[PeerIdentityCache] Using local agent.md for ${peerId} (cached source=${cached?.source ?? 'none'})`);
|
|
180
|
+
const localHash = 'sha256:' + crypto.createHash('sha256').update(localContent, 'utf-8').digest('hex');
|
|
181
|
+
if (cached && cached.agentMdHash === localHash && (cached.source === 'agentmd' || cached.source === 'agentmd-unverified')) {
|
|
172
182
|
return this.touchLastChecked(channelType, peerId, agentDir, cached);
|
|
173
183
|
}
|
|
174
|
-
|
|
184
|
+
// 无匹配缓存可信级别 → 本地内容未经本次验签,标记为未验证
|
|
185
|
+
const fallbackSource = cached?.source === 'agentmd' ? 'agentmd' : 'agentmd-unverified';
|
|
186
|
+
return this.updateFromAgentMd(channelType, peerId, agentDir, localContent, cached?.verifiedAt ?? 0, fallbackSource);
|
|
175
187
|
}
|
|
176
188
|
}
|
|
177
189
|
catch { /* ignore fs errors */ }
|
|
@@ -127,8 +127,8 @@ export function parseTriggerUpdate(args) {
|
|
|
127
127
|
}
|
|
128
128
|
if (flags.has('session')) {
|
|
129
129
|
const sv = flags.get('session');
|
|
130
|
-
if (sv !== 'latest' && sv !== '
|
|
131
|
-
return { ok: false, error: '--session 只接受 latest 或
|
|
130
|
+
if (sv !== 'latest' && sv !== 'current' && sv !== 'thread')
|
|
131
|
+
return { ok: false, error: '--session 只接受 latest、current 或 thread' };
|
|
132
132
|
result.targetSessionStrategy = sv;
|
|
133
133
|
}
|
|
134
134
|
if (flags.has('agent')) {
|
|
@@ -216,8 +216,8 @@ export function parseTriggerSet(args) {
|
|
|
216
216
|
let targetSessionStrategy = 'latest';
|
|
217
217
|
if (hasSession) {
|
|
218
218
|
const sv = flags.get('session');
|
|
219
|
-
if (sv !== 'latest' && sv !== '
|
|
220
|
-
return { ok: false, error: '--session 只接受 latest 或
|
|
219
|
+
if (sv !== 'latest' && sv !== 'current' && sv !== 'thread') {
|
|
220
|
+
return { ok: false, error: '--session 只接受 latest、current 或 thread' };
|
|
221
221
|
}
|
|
222
222
|
targetSessionStrategy = sv;
|
|
223
223
|
}
|