evolclaw 3.1.5 → 3.1.6
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 +53 -3
- package/dist/agents/claude-runner.js +69 -24
- package/dist/agents/kit-renderer.js +15 -4
- package/dist/aun/aid/agentmd.js +10 -3
- package/dist/aun/msg/group.js +2 -2
- package/dist/channels/aun.js +98 -12
- 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 +7 -3
- package/dist/core/message/message-processor.js +138 -35
- package/dist/core/relation/peer-identity.js +23 -11
- package/dist/core/trigger/parser.js +4 -4
- package/dist/core/trigger/scheduler.js +20 -6
- package/dist/index.js +55 -5
- 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/rules/06-channel.md +5 -1
- 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 +9 -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';
|
|
@@ -14,6 +15,22 @@ import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
|
|
|
14
15
|
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
15
16
|
import { formatPeerKey } from '../relation/peer-key.js';
|
|
16
17
|
import { resolveEffectiveModel } from '../model/model-scope.js';
|
|
18
|
+
/** OS 信息在进程生命周期内是常量,模块加载时算一次。例: "Windows 11 Pro (win32 10.0.26200)" */
|
|
19
|
+
const OS_INFO = (() => {
|
|
20
|
+
let label = '';
|
|
21
|
+
try {
|
|
22
|
+
label = os.version();
|
|
23
|
+
}
|
|
24
|
+
catch { /* 旧 Node 无 os.version */ }
|
|
25
|
+
return `${label ? label + ' ' : ''}(${os.platform()} ${os.release()})`;
|
|
26
|
+
})();
|
|
27
|
+
/** 当前 UTC 偏移,格式 +08:00 / -05:00。每条消息算(DST 安全)。 */
|
|
28
|
+
function currentTzOffset() {
|
|
29
|
+
const off = -new Date().getTimezoneOffset(); // 分钟,东区为正
|
|
30
|
+
const sign = off >= 0 ? '+' : '-';
|
|
31
|
+
const abs = Math.abs(off);
|
|
32
|
+
return `${sign}${String(Math.floor(abs / 60)).padStart(2, '0')}:${String(abs % 60).padStart(2, '0')}`;
|
|
33
|
+
}
|
|
17
34
|
function getContextTooLongHint(agent) {
|
|
18
35
|
if (canCompactAgent(agent)) {
|
|
19
36
|
return '上下文过长,请精简提问或使用 /compact 压缩上下文';
|
|
@@ -69,6 +86,8 @@ export class MessageProcessor {
|
|
|
69
86
|
agentMap;
|
|
70
87
|
primaryRunnerKey;
|
|
71
88
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
89
|
+
/** sessionId → 模型降级状态(带退避探测,进程重启清零) */
|
|
90
|
+
modelFallbackMap = new Map();
|
|
72
91
|
interactionRouter;
|
|
73
92
|
messageQueue;
|
|
74
93
|
/** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
|
|
@@ -208,15 +227,15 @@ export class MessageProcessor {
|
|
|
208
227
|
}
|
|
209
228
|
// 命令前缀列表(与 CommandHandler.quickCommandPrefixes 保持同步)
|
|
210
229
|
static COMMAND_PREFIXES = [
|
|
211
|
-
'/new', '/pwd', '/
|
|
230
|
+
'/new', '/pwd', '/help', '/status', '/restart',
|
|
212
231
|
'/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
|
|
213
232
|
'/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
|
|
214
|
-
'/
|
|
233
|
+
'/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
|
|
215
234
|
'/aid', '/upgrade', '/evolagent',
|
|
216
235
|
];
|
|
217
236
|
/** 判断消息内容是否为已知命令 */
|
|
218
237
|
isKnownCommand(content) {
|
|
219
|
-
return content === '/
|
|
238
|
+
return content === '/s' ||
|
|
220
239
|
MessageProcessor.COMMAND_PREFIXES.some(cmd => content.startsWith(cmd));
|
|
221
240
|
}
|
|
222
241
|
/**
|
|
@@ -227,6 +246,29 @@ export class MessageProcessor {
|
|
|
227
246
|
// 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
|
|
228
247
|
// message.channel 现在存实例名(channelName),可直接用于精确路由
|
|
229
248
|
const { session, absoluteProjectPath } = await this.resolveSession(message);
|
|
249
|
+
// thread(feishu) pending strategy: inject replyContext so first reply creates the thread
|
|
250
|
+
if (message.triggerMeta?.pendingThread && message.triggerMeta?.rootMessageId) {
|
|
251
|
+
const triggerId = message.triggerMeta.triggerId;
|
|
252
|
+
const channelKeyForAgent = session.metadata?.channelKey || message.channel;
|
|
253
|
+
const trigMgr = this.agentRegistry?.resolveByChannel(channelKeyForAgent)?.triggerManager;
|
|
254
|
+
const onThreadCreated = trigMgr
|
|
255
|
+
? (threadId) => {
|
|
256
|
+
try {
|
|
257
|
+
trigMgr.update(triggerId, { targetThreadId: threadId, pendingThread: false });
|
|
258
|
+
logger.info(`[MessageProcessor] Feishu thread created for trigger ${triggerId}: ${threadId}`);
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
logger.warn(`[MessageProcessor] Failed to write back thread_id for trigger ${triggerId}: ${e}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
: undefined;
|
|
265
|
+
message.replyContext = {
|
|
266
|
+
...(message.replyContext ?? {}),
|
|
267
|
+
replyToMessageId: message.triggerMeta.rootMessageId,
|
|
268
|
+
replyInThread: true,
|
|
269
|
+
...(onThreadCreated ? { metadata: { ...(message.replyContext?.metadata ?? {}), onThreadCreated } } : {}),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
230
272
|
const channelKey = session.metadata?.channelKey || message.channel;
|
|
231
273
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
232
274
|
if (!channelInfo) {
|
|
@@ -388,7 +430,7 @@ export class MessageProcessor {
|
|
|
388
430
|
};
|
|
389
431
|
};
|
|
390
432
|
const isProactive = session.sessionMode === 'proactive';
|
|
391
|
-
const isAutonomous = session.sessionMode === 'autonomous'
|
|
433
|
+
const isAutonomous = session.sessionMode === 'autonomous';
|
|
392
434
|
const envelope = buildEnvelope({
|
|
393
435
|
taskId,
|
|
394
436
|
channel: message.channel,
|
|
@@ -524,6 +566,9 @@ export class MessageProcessor {
|
|
|
524
566
|
let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
525
567
|
let effectiveSystemPrompt;
|
|
526
568
|
let modelOverride;
|
|
569
|
+
let usedFallback = false;
|
|
570
|
+
let skipEvolclawModel = false;
|
|
571
|
+
let agentModel;
|
|
527
572
|
try {
|
|
528
573
|
// 动态构建运行时上下文提示
|
|
529
574
|
const contextParts = [];
|
|
@@ -557,16 +602,35 @@ export class MessageProcessor {
|
|
|
557
602
|
// 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
|
|
558
603
|
// 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
|
|
559
604
|
// 多对端并发各自独立解析、各自传参,无共享状态可被污染。
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
605
|
+
let effectiveModel;
|
|
606
|
+
// 取降级状态,按退避策略决定是否跳过 evolclaw 作用域模型
|
|
607
|
+
const fbState = this.modelFallbackMap.get(session.id) ?? {
|
|
608
|
+
failCount: 0, fallbackActive: false,
|
|
609
|
+
messagesSinceFallback: 0, nextProbeAt: 2, hintShown: false,
|
|
610
|
+
};
|
|
611
|
+
// 退避期内递增消息计数,判断是否到探测点
|
|
612
|
+
if (fbState.fallbackActive) {
|
|
613
|
+
fbState.messagesSinceFallback++;
|
|
614
|
+
skipEvolclawModel = fbState.messagesSinceFallback < fbState.nextProbeAt;
|
|
615
|
+
this.modelFallbackMap.set(session.id, fbState);
|
|
564
616
|
}
|
|
565
|
-
|
|
566
|
-
|
|
617
|
+
// 非跳过时:尝试解析 evolclaw 作用域模型
|
|
618
|
+
let evolclawModelOverride;
|
|
619
|
+
if (!skipEvolclawModel) {
|
|
620
|
+
try {
|
|
621
|
+
const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
|
|
622
|
+
if (resolved.model) {
|
|
623
|
+
evolclawModelOverride = { model: resolved.model, effort: resolved.effort };
|
|
624
|
+
effectiveModel = resolved.model;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch (e) {
|
|
628
|
+
logger.warn(`[MessageProcessor] resolveEffectiveModel failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
629
|
+
}
|
|
630
|
+
modelOverride = evolclawModelOverride;
|
|
567
631
|
}
|
|
568
632
|
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
569
|
-
|
|
633
|
+
agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
570
634
|
// Kit renderer: 组装上下文
|
|
571
635
|
const pkgRoot = getPackageRoot();
|
|
572
636
|
const kitCtx = {
|
|
@@ -580,6 +644,8 @@ export class MessageProcessor {
|
|
|
580
644
|
KITS_DOCS: path.join(pkgRoot, 'kits', 'docs'),
|
|
581
645
|
KITS_TEMPLATES: path.join(pkgRoot, 'kits', 'templates'),
|
|
582
646
|
KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
|
|
647
|
+
// evolclaw 运行模式:dev=源码仓库 | install=全局安装包
|
|
648
|
+
evolclawMode: fs.existsSync(path.join(pkgRoot, 'src', 'index.ts')) ? 'dev' : 'install',
|
|
583
649
|
// 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
|
|
584
650
|
PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
|
|
585
651
|
RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
|
|
@@ -593,6 +659,9 @@ export class MessageProcessor {
|
|
|
593
659
|
peerName: peerName || undefined,
|
|
594
660
|
peerRole: session.identity?.role || 'anonymous',
|
|
595
661
|
peerType: message.peerType || undefined,
|
|
662
|
+
sameDevice: message.sameDevice || undefined,
|
|
663
|
+
sameNetwork: message.sameNetwork || undefined,
|
|
664
|
+
sameEgressIp: message.sameEgressIp || undefined,
|
|
596
665
|
groupId: session.metadata?.groupId || undefined,
|
|
597
666
|
chatType: session.chatType || null,
|
|
598
667
|
channel: currentChannelType || null,
|
|
@@ -606,6 +675,10 @@ export class MessageProcessor {
|
|
|
606
675
|
sessionId: session.id,
|
|
607
676
|
sessionName: session.name || undefined,
|
|
608
677
|
sessionCreatedAt: session.createdAt ? new Date(session.createdAt).toISOString() : undefined,
|
|
678
|
+
// 时区(把 ISO 时间戳转本地时间用)+ OS 环境
|
|
679
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || undefined,
|
|
680
|
+
tzOffset: currentTzOffset(),
|
|
681
|
+
osInfo: OS_INFO,
|
|
609
682
|
threadId: session.threadId || undefined,
|
|
610
683
|
// Stage 3: sessionKey 持久化字段
|
|
611
684
|
sessionKey: session.sessionKey,
|
|
@@ -614,6 +687,9 @@ export class MessageProcessor {
|
|
|
614
687
|
baseAgent: normalizedBaseagent.canonical,
|
|
615
688
|
baseAgentName: normalizedBaseagent.displayName,
|
|
616
689
|
baseAgentModel: agentModel || undefined,
|
|
690
|
+
effectiveModel: effectiveModel || agentModel || undefined,
|
|
691
|
+
modelFallbackActive: (fbState.fallbackActive || skipEvolclawModel) ? true : undefined,
|
|
692
|
+
modelFallbackModel: (fbState.fallbackActive || skipEvolclawModel) ? (agentModel || undefined) : undefined,
|
|
617
693
|
agentSessionId: session.agentSessionId || undefined,
|
|
618
694
|
},
|
|
619
695
|
sessionId: session.id,
|
|
@@ -632,12 +708,32 @@ export class MessageProcessor {
|
|
|
632
708
|
agent.registerStream(streamKey, stream);
|
|
633
709
|
streamRegistered = true;
|
|
634
710
|
streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
711
|
+
// 探测成功(退避期内到达探测点且用的是 evolclaw 模型)→ 清零降级状态
|
|
712
|
+
if (fbState.fallbackActive && !skipEvolclawModel && !usedFallback) {
|
|
713
|
+
this.modelFallbackMap.delete(session.id);
|
|
714
|
+
logger.info(`[MessageProcessor] Model probe succeeded, cleared fallback state for session=${session.id}`);
|
|
715
|
+
}
|
|
635
716
|
break; // 成功,跳出重试循环
|
|
636
717
|
}
|
|
637
718
|
catch (retryError) {
|
|
638
719
|
if (streamRegistered) {
|
|
639
720
|
agent.cleanupStream(streamKey);
|
|
640
721
|
}
|
|
722
|
+
// 模型不可用:累计计数,本次切换到 baseAgentModel 立即重试,不让用户看到失败
|
|
723
|
+
if (classifyError(retryError) === ErrorType.MODEL_UNAVAILABLE && evolclawModelOverride?.model) {
|
|
724
|
+
fbState.failCount++;
|
|
725
|
+
if (fbState.failCount >= 2) {
|
|
726
|
+
fbState.fallbackActive = true;
|
|
727
|
+
fbState.messagesSinceFallback = 0;
|
|
728
|
+
fbState.nextProbeAt = Math.min(Math.pow(2, fbState.failCount - 1), 8);
|
|
729
|
+
}
|
|
730
|
+
this.modelFallbackMap.set(session.id, fbState);
|
|
731
|
+
logger.warn(`[MessageProcessor] Model unavailable: ${evolclawModelOverride.model}, failCount=${fbState.failCount}, fallbackActive=${fbState.fallbackActive}`);
|
|
732
|
+
// 切换到 baseAgentModel 重试(清除 modelOverride,让 runQuery 使用 this.model)
|
|
733
|
+
modelOverride = undefined;
|
|
734
|
+
usedFallback = true;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
641
737
|
if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
|
|
642
738
|
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
|
|
643
739
|
logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
|
|
@@ -657,9 +753,6 @@ export class MessageProcessor {
|
|
|
657
753
|
await renderer.flush();
|
|
658
754
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
659
755
|
if (compacted) {
|
|
660
|
-
// compact 成功,清除第一次流中混入的错误文本,再重试
|
|
661
|
-
const ctxErrPattern = /prompt is too long|input is too long|上下文过长/i;
|
|
662
|
-
renderer.stripContextError(ctxErrPattern);
|
|
663
756
|
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
664
757
|
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
665
758
|
agent.registerStream(streamKey, retryStream);
|
|
@@ -682,7 +775,6 @@ export class MessageProcessor {
|
|
|
682
775
|
contextTooLongPattern.test(errorsText) ||
|
|
683
776
|
contextTooLongPattern.test(streamResult.fullText));
|
|
684
777
|
if (isPromptTooLong) {
|
|
685
|
-
renderer.stripContextError(contextTooLongPattern);
|
|
686
778
|
renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
687
779
|
await renderer.flush();
|
|
688
780
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
@@ -698,7 +790,6 @@ export class MessageProcessor {
|
|
|
698
790
|
contextTooLongPattern.test(retryErrorsText) ||
|
|
699
791
|
contextTooLongPattern.test(streamResult.fullText));
|
|
700
792
|
if (retryStillTooLong) {
|
|
701
|
-
renderer.stripContextError(contextTooLongPattern);
|
|
702
793
|
renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
|
|
703
794
|
}
|
|
704
795
|
}
|
|
@@ -815,6 +906,20 @@ export class MessageProcessor {
|
|
|
815
906
|
logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
|
|
816
907
|
this.sessionManager.clearProcessing(session.id);
|
|
817
908
|
logger.info(`[MessageProcessor] session ${session.id} processing cleared task=${taskId}`);
|
|
909
|
+
// 降级模型回复末尾追加标记(代码层硬注入,不依赖模型输出)
|
|
910
|
+
const usingFallback = usedFallback || (skipEvolclawModel && agentModel != null);
|
|
911
|
+
if (usingFallback && agentModel) {
|
|
912
|
+
const curFbState = this.modelFallbackMap.get(session.id);
|
|
913
|
+
const showHint = curFbState && curFbState.nextProbeAt >= 8 && !curFbState.hintShown;
|
|
914
|
+
const suffix = showHint
|
|
915
|
+
? `\n\n---\n⚠️ [降级模型: ${agentModel} | 可告诉我"帮我检查可用模型"来诊断]`
|
|
916
|
+
: `\n\n---\n⚠️ [降级模型: ${agentModel}]`;
|
|
917
|
+
renderer.addText(suffix);
|
|
918
|
+
if (showHint && curFbState) {
|
|
919
|
+
curFbState.hintShown = true;
|
|
920
|
+
this.modelFallbackMap.set(session.id, curFbState);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
818
923
|
// 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
|
|
819
924
|
const preFlushInterrupt = this.interruptedSessions.get(session.id);
|
|
820
925
|
if (preFlushInterrupt === 'new_message' || preFlushInterrupt === 'stop' || preFlushInterrupt === 'recalled') {
|
|
@@ -877,7 +982,7 @@ export class MessageProcessor {
|
|
|
877
982
|
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
878
983
|
}
|
|
879
984
|
else {
|
|
880
|
-
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, ttftMs: streamResult.ttftMs, numTurns: streamResult.numTurns,
|
|
985
|
+
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, ttftMs: streamResult.ttftMs, numTurns: streamResult.numTurns, tokenUsage: streamResult.tokenUsage, contextUsage: streamResult.contextUsage } }).catch(() => { });
|
|
881
986
|
}
|
|
882
987
|
}
|
|
883
988
|
if (message.triggerMeta) {
|
|
@@ -887,10 +992,6 @@ export class MessageProcessor {
|
|
|
887
992
|
else {
|
|
888
993
|
this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, messageId: messageId, durationMs });
|
|
889
994
|
}
|
|
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
995
|
}
|
|
895
996
|
await this.sessionManager.recordSuccess(session.id);
|
|
896
997
|
this.eventBus.publish({
|
|
@@ -1023,19 +1124,21 @@ export class MessageProcessor {
|
|
|
1023
1124
|
? { replyContext: message.replyContext }
|
|
1024
1125
|
: undefined;
|
|
1025
1126
|
const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
|
|
1026
|
-
//
|
|
1027
|
-
if (message.triggerMeta?.
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1127
|
+
// current strategy: resume bound session, make it active so output is not suppressed
|
|
1128
|
+
if (message.triggerMeta?.boundSessionId) {
|
|
1129
|
+
const bound = await this.sessionManager.getSessionById(message.triggerMeta.boundSessionId);
|
|
1130
|
+
if (bound) {
|
|
1131
|
+
const switched = await this.sessionManager.switchToSession(bound.channel, bound.channelId, bound.id);
|
|
1132
|
+
if (switched) {
|
|
1133
|
+
const absoluteProjectPath = path.isAbsolute(switched.projectPath)
|
|
1134
|
+
? switched.projectPath : path.resolve(process.cwd(), switched.projectPath);
|
|
1135
|
+
return { session: switched, absoluteProjectPath };
|
|
1136
|
+
}
|
|
1137
|
+
logger.warn(`[MessageProcessor] switchToSession failed for bound session ${bound.id}, falling back to latest`);
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
logger.warn(`[MessageProcessor] Bound session ${message.triggerMeta.boundSessionId} not found, falling back to latest`);
|
|
1034
1141
|
}
|
|
1035
|
-
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
1036
|
-
? session.projectPath
|
|
1037
|
-
: path.resolve(process.cwd(), session.projectPath);
|
|
1038
|
-
return { session, absoluteProjectPath };
|
|
1039
1142
|
}
|
|
1040
1143
|
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
1144
|
// 兜底纠正1:群聊强制 proactive
|
|
@@ -1240,7 +1343,7 @@ export class MessageProcessor {
|
|
|
1240
1343
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1241
1344
|
}
|
|
1242
1345
|
// 记录完成状态 + 最后一轮回复文本(后续 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,
|
|
1346
|
+
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
1347
|
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
1245
1348
|
// 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
|
|
1246
1349
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
@@ -1296,7 +1399,7 @@ export class MessageProcessor {
|
|
|
1296
1399
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1297
1400
|
}
|
|
1298
1401
|
// 记录完成状态
|
|
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,
|
|
1402
|
+
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
1403
|
if (event.subtype === 'success') {
|
|
1301
1404
|
this.messageCache.addEvent(session.id, {
|
|
1302
1405
|
type: 'completed',
|
|
@@ -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
|
}
|
|
@@ -209,12 +209,12 @@ export class TriggerScheduler {
|
|
|
209
209
|
this.inflightCron.delete(triggerId);
|
|
210
210
|
}
|
|
211
211
|
buildSyntheticMessage(trigger, messageId) {
|
|
212
|
-
|
|
212
|
+
const base = {
|
|
213
213
|
channel: trigger.targetChannel,
|
|
214
214
|
channelType: trigger.targetChannelType,
|
|
215
215
|
channelId: trigger.targetChannelId,
|
|
216
216
|
selfAID: this.aid,
|
|
217
|
-
threadId:
|
|
217
|
+
threadId: '',
|
|
218
218
|
agentId: trigger.agentId,
|
|
219
219
|
chatType: 'private',
|
|
220
220
|
peerId: `__trigger__:${trigger.id}`, // unique per trigger to prevent greedy merge
|
|
@@ -222,10 +222,24 @@ export class TriggerScheduler {
|
|
|
222
222
|
messageId,
|
|
223
223
|
timestamp: Date.now(),
|
|
224
224
|
source: 'trigger',
|
|
225
|
-
triggerMeta: {
|
|
226
|
-
triggerId: trigger.id,
|
|
227
|
-
silent: trigger.targetSessionStrategy === 'silent',
|
|
228
|
-
},
|
|
229
225
|
};
|
|
226
|
+
if (trigger.targetSessionStrategy === 'current') {
|
|
227
|
+
base.triggerMeta = { triggerId: trigger.id, boundSessionId: trigger.boundSessionId };
|
|
228
|
+
}
|
|
229
|
+
else if (trigger.targetSessionStrategy === 'thread') {
|
|
230
|
+
if (trigger.threadKind === 'feishu' && trigger.pendingThread) {
|
|
231
|
+
base.triggerMeta = { triggerId: trigger.id, pendingThread: true, rootMessageId: trigger.rootMessageId };
|
|
232
|
+
// threadId intentionally empty — first fire builds the thread via reply_in_thread
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
base.threadId = trigger.targetThreadId ?? '';
|
|
236
|
+
base.triggerMeta = { triggerId: trigger.id };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// latest
|
|
241
|
+
base.triggerMeta = { triggerId: trigger.id };
|
|
242
|
+
}
|
|
243
|
+
return base;
|
|
230
244
|
}
|
|
231
245
|
}
|
package/dist/index.js
CHANGED
|
@@ -452,11 +452,58 @@ async function main() {
|
|
|
452
452
|
continue;
|
|
453
453
|
const scheduler = agent.triggerScheduler;
|
|
454
454
|
const primaryProjectPath = agent.config.projects?.defaultPath ?? primaryAgent.projectPath;
|
|
455
|
+
function scheduleRetryWhenIdle(boundId, msg, trigger) {
|
|
456
|
+
let done = false;
|
|
457
|
+
const handler = (ev) => {
|
|
458
|
+
if (ev.sessionId !== boundId || done)
|
|
459
|
+
return;
|
|
460
|
+
done = true;
|
|
461
|
+
clearTimeout(timer);
|
|
462
|
+
eventBus.unsubscribe('task:completed', handler);
|
|
463
|
+
retry();
|
|
464
|
+
};
|
|
465
|
+
const timer = setTimeout(() => {
|
|
466
|
+
if (done)
|
|
467
|
+
return;
|
|
468
|
+
done = true;
|
|
469
|
+
eventBus.unsubscribe('task:completed', handler);
|
|
470
|
+
retry();
|
|
471
|
+
}, 30_000);
|
|
472
|
+
eventBus.subscribe('task:completed', handler);
|
|
473
|
+
function retry() {
|
|
474
|
+
if (messageQueue.isProcessing(boundId)) {
|
|
475
|
+
scheduleRetryWhenIdle(boundId, msg, trigger);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
sessionManager.getSessionById(boundId).then(bound => {
|
|
479
|
+
if (!bound) {
|
|
480
|
+
logger.warn(`[Trigger] Bound session ${boundId} deleted, aborting`);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
messageQueue.enqueue(boundId, msg, bound.projectPath, { interruptible: false })
|
|
484
|
+
.catch(err => logger.error(`[Trigger] Retry failed ${trigger.id}: ${err}`));
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
455
488
|
scheduler.setFireCallback((msg, trigger) => {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
489
|
+
if (trigger.targetSessionStrategy === 'current' && trigger.boundSessionId) {
|
|
490
|
+
const boundId = trigger.boundSessionId;
|
|
491
|
+
if (messageQueue.isProcessing(boundId)) {
|
|
492
|
+
scheduleRetryWhenIdle(boundId, msg, trigger);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
sessionManager.getSessionById(boundId).then(bound => {
|
|
496
|
+
if (!bound) {
|
|
497
|
+
logger.warn(`[Trigger] Bound session ${boundId} not found`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
messageQueue.enqueue(boundId, msg, bound.projectPath, { interruptible: false })
|
|
501
|
+
.catch(err => logger.error(`[Trigger] Enqueue failed ${trigger.id}: ${err}`));
|
|
502
|
+
});
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
messageQueue.enqueue(`${msg.channel}:${msg.channelId}`, msg, primaryProjectPath, { interruptible: false })
|
|
506
|
+
.catch(err => logger.error(`[Trigger] Enqueue failed ${trigger.id}: ${err}`));
|
|
460
507
|
});
|
|
461
508
|
// Subscribe to trigger:completed/failed/skipped to update cron inflight state
|
|
462
509
|
eventBus.subscribe('trigger:completed', (ev) => scheduler.onTriggerComplete(ev.triggerId, 'completed'));
|
|
@@ -504,7 +551,7 @@ async function main() {
|
|
|
504
551
|
nextFireAt: calcNextFireAt('cron', cronExpr),
|
|
505
552
|
targetChannel: firstChannel,
|
|
506
553
|
targetChannelId: '__system__',
|
|
507
|
-
targetSessionStrategy: '
|
|
554
|
+
targetSessionStrategy: 'latest',
|
|
508
555
|
prompt: '检查 evolclaw 是否有新版本可用。执行 `npm view evolclaw version` 获取最新版本,与当前版本(执行 `evolclaw --version`)对比。如果有新版本,执行 /restart 进行升级。如果已是最新版本,无需任何操作。',
|
|
509
556
|
createdByPeerId: '__system__',
|
|
510
557
|
createdByChannel: '__system__',
|
|
@@ -756,8 +803,11 @@ async function main() {
|
|
|
756
803
|
continue;
|
|
757
804
|
}
|
|
758
805
|
logger.info(`[Resume] Resuming session: ${session.id} (agent: ${evolName}::${baseagentName})`);
|
|
806
|
+
const parsedResumeKey = tryParseChannelKey(session.channel);
|
|
759
807
|
const resumeMessage = {
|
|
760
808
|
channel: session.channel,
|
|
809
|
+
channelType: session.channelType || parsedResumeKey?.type,
|
|
810
|
+
selfAID: parsedResumeKey?.selfAID,
|
|
761
811
|
channelId: session.channelId,
|
|
762
812
|
content: '服务已重启,请继续之前未完成的任务。',
|
|
763
813
|
timestamp: Date.now(),
|
package/dist/ipc.js
CHANGED
|
@@ -93,7 +93,7 @@ export class IpcServer {
|
|
|
93
93
|
case 'status':
|
|
94
94
|
return this.getStatus();
|
|
95
95
|
case 'ping':
|
|
96
|
-
return { pong: true, pid: process.pid };
|
|
96
|
+
return { pong: true, pid: process.pid, protocolVersion: 1 };
|
|
97
97
|
case 'aun-aids': {
|
|
98
98
|
const aids = this.aunAidProvider ? this.aunAidProvider() : [];
|
|
99
99
|
return { ok: true, aids };
|
|
@@ -10,6 +10,7 @@ export var ErrorType;
|
|
|
10
10
|
ErrorType["FILE_CORRUPT"] = "file_corrupt";
|
|
11
11
|
ErrorType["STREAM_ERROR"] = "stream_error";
|
|
12
12
|
ErrorType["CONTEXT_TOO_LONG"] = "context_too_long";
|
|
13
|
+
ErrorType["MODEL_UNAVAILABLE"] = "model_unavailable";
|
|
13
14
|
ErrorType["UNKNOWN"] = "unknown";
|
|
14
15
|
})(ErrorType || (ErrorType = {}));
|
|
15
16
|
/**
|
|
@@ -210,6 +211,11 @@ export function classifyError(error) {
|
|
|
210
211
|
|| msg.includes('上下文过长')) {
|
|
211
212
|
return ErrorType.CONTEXT_TOO_LONG;
|
|
212
213
|
}
|
|
214
|
+
if (msg.includes('invalid_model') || msg.includes('model_not_found')
|
|
215
|
+
|| msg.includes('no such model') || msg.includes('unknown model')
|
|
216
|
+
|| /api error: 404\b/.test(msg)) {
|
|
217
|
+
return ErrorType.MODEL_UNAVAILABLE;
|
|
218
|
+
}
|
|
213
219
|
if (msg.includes('401') || msg.includes('authentication_error')) {
|
|
214
220
|
return ErrorType.AUTH_ERROR;
|
|
215
221
|
}
|
|
@@ -51,18 +51,20 @@ function getStartTimeLinux(pid) {
|
|
|
51
51
|
const starttimeJiffies = parseInt(fields[19], 10);
|
|
52
52
|
if (isNaN(starttimeJiffies))
|
|
53
53
|
return null;
|
|
54
|
-
let
|
|
54
|
+
let btimeSec;
|
|
55
55
|
try {
|
|
56
|
-
|
|
56
|
+
const m = fs.readFileSync('/proc/stat', 'utf-8').match(/^btime (\d+)/m);
|
|
57
|
+
if (!m)
|
|
58
|
+
return null;
|
|
59
|
+
btimeSec = parseInt(m[1], 10);
|
|
57
60
|
}
|
|
58
61
|
catch {
|
|
59
62
|
return null;
|
|
60
63
|
}
|
|
61
|
-
if (isNaN(
|
|
64
|
+
if (isNaN(btimeSec))
|
|
62
65
|
return null;
|
|
63
66
|
const clkTck = 100;
|
|
64
|
-
|
|
65
|
-
return bootTimeMs + (starttimeJiffies / clkTck) * 1000;
|
|
67
|
+
return (btimeSec + starttimeJiffies / clkTck) * 1000;
|
|
66
68
|
}
|
|
67
69
|
// ── macOS ──
|
|
68
70
|
function getStartTimeMacOS(pid) {
|