evolclaw 3.1.4 → 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 +60 -0
- package/dist/agents/claude-runner.js +398 -161
- package/dist/agents/kit-renderer.js +191 -25
- package/dist/aun/aid/agentmd.js +75 -103
- package/dist/aun/aid/client.js +1 -29
- package/dist/aun/aid/identity.js +105 -64
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/msg/group.js +2 -2
- package/dist/aun/msg/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -30
- package/dist/channels/aun.js +174 -99
- package/dist/channels/dingtalk.js +2 -1
- package/dist/channels/feishu.js +301 -199
- package/dist/channels/qqbot.js +2 -1
- package/dist/channels/wechat.js +2 -1
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent.js +21 -16
- package/dist/cli/bench.js +41 -28
- package/dist/cli/help.js +8 -0
- package/dist/cli/index.js +176 -87
- package/dist/cli/init-channel.js +5 -1
- package/dist/cli/init.js +37 -21
- package/dist/cli/link-rules.js +1 -7
- package/dist/cli/model.js +549 -0
- package/dist/cli/net-check.js +133 -50
- package/dist/cli/watch-msg.js +7 -7
- package/dist/cli/watch-web/debug-log.js +18 -0
- package/dist/cli/watch-web/server.js +306 -0
- package/dist/cli/watch-web/sources/aid.js +63 -0
- package/dist/cli/watch-web/sources/msg.js +70 -0
- package/dist/cli/watch-web/sources/session.js +638 -0
- package/dist/cli/watch-web/sources/types.js +10 -0
- package/dist/cli/watch-web/static/app.js +546 -0
- package/dist/cli/watch-web/static/index.html +54 -0
- package/dist/cli/watch-web/static/style.css +247 -0
- package/dist/config-store.js +1 -22
- package/dist/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +261 -133
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -22
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/im-renderer.js +9 -20
- package/dist/core/message/message-bridge.js +13 -9
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +211 -123
- package/dist/core/message/stream-idle-monitor.js +21 -0
- package/dist/core/model/model-catalog.js +215 -0
- package/dist/core/model/model-scope.js +250 -0
- package/dist/core/relation/peer-identity.js +58 -55
- package/dist/core/relation/peer-key.js +16 -0
- package/dist/core/session/session-fs-store.js +34 -55
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +308 -251
- package/dist/core/session/session-mapper.js +9 -4
- package/dist/core/trigger/manager.js +3 -3
- package/dist/core/trigger/parser.js +4 -4
- package/dist/core/trigger/scheduler.js +22 -7
- package/dist/index.js +61 -7
- package/dist/ipc.js +23 -1
- package/dist/utils/error-utils.js +6 -0
- package/dist/utils/process-introspect.js +7 -5
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +8 -8
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +182 -0
- package/kits/docs/evolclaw/INDEX.md +43 -0
- package/kits/docs/evolclaw/agent.md +49 -0
- package/kits/docs/evolclaw/aid.md +49 -0
- package/kits/docs/evolclaw/ctl.md +46 -0
- package/kits/docs/evolclaw/group.md +89 -0
- package/kits/docs/evolclaw/model.md +51 -0
- package/kits/docs/evolclaw/msg.md +91 -0
- package/kits/docs/evolclaw/rpc.md +35 -0
- package/kits/docs/evolclaw/storage.md +49 -0
- package/kits/docs/venues/aun-group.md +10 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +23 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +81 -36
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/06-channel.md +34 -27
- 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 +19 -3
- package/kits/templates/system-fragments/venue.md +24 -0
- package/package.json +10 -5
- package/dist/aun/aid/lifecycle-log.js +0 -33
- package/dist/utils/aid-lifecycle-log.js +0 -33
- package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
- package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
- package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
- package/kits/docs/evolclaw/tools.md +0 -25
|
@@ -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';
|
|
@@ -12,6 +13,24 @@ import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
|
12
13
|
import { renderKitSections } from '../../agents/kit-renderer.js';
|
|
13
14
|
import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
|
|
14
15
|
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
16
|
+
import { formatPeerKey } from '../relation/peer-key.js';
|
|
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
|
+
}
|
|
15
34
|
function getContextTooLongHint(agent) {
|
|
16
35
|
if (canCompactAgent(agent)) {
|
|
17
36
|
return '上下文过长,请精简提问或使用 /compact 压缩上下文';
|
|
@@ -67,9 +86,12 @@ export class MessageProcessor {
|
|
|
67
86
|
agentMap;
|
|
68
87
|
primaryRunnerKey;
|
|
69
88
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
89
|
+
/** sessionId → 模型降级状态(带退避探测,进程重启清零) */
|
|
90
|
+
modelFallbackMap = new Map();
|
|
70
91
|
interactionRouter;
|
|
71
92
|
messageQueue;
|
|
72
|
-
|
|
93
|
+
/** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
|
|
94
|
+
activeMonitors = new Map();
|
|
73
95
|
/**
|
|
74
96
|
* Get the runner for a given (channel, baseagent) pair.
|
|
75
97
|
*
|
|
@@ -99,7 +121,7 @@ export class MessageProcessor {
|
|
|
99
121
|
if (session.threadId)
|
|
100
122
|
return false;
|
|
101
123
|
// 使用 session 自身的 channelType 精确定位 active.json,避免扫描误匹配
|
|
102
|
-
const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.
|
|
124
|
+
const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.selfAID);
|
|
103
125
|
return active ? session.id !== active.id : false;
|
|
104
126
|
}
|
|
105
127
|
constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
|
|
@@ -126,6 +148,16 @@ export class MessageProcessor {
|
|
|
126
148
|
}
|
|
127
149
|
setInteractionRouter(router) {
|
|
128
150
|
this.interactionRouter = router;
|
|
151
|
+
// 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
|
|
152
|
+
// 避免把「正在等用户点按钮」误判为「任务卡死」而中断任务。
|
|
153
|
+
router.setWaitHooks({
|
|
154
|
+
onWaitStart: (sessionId) => {
|
|
155
|
+
this.activeMonitors.get(sessionId)?.pause();
|
|
156
|
+
},
|
|
157
|
+
onWaitEnd: (sessionId) => {
|
|
158
|
+
this.activeMonitors.get(sessionId)?.resume();
|
|
159
|
+
},
|
|
160
|
+
});
|
|
129
161
|
}
|
|
130
162
|
setMessageQueue(queue) {
|
|
131
163
|
this.messageQueue = queue;
|
|
@@ -195,15 +227,15 @@ export class MessageProcessor {
|
|
|
195
227
|
}
|
|
196
228
|
// 命令前缀列表(与 CommandHandler.quickCommandPrefixes 保持同步)
|
|
197
229
|
static COMMAND_PREFIXES = [
|
|
198
|
-
'/new', '/pwd', '/
|
|
230
|
+
'/new', '/pwd', '/help', '/status', '/restart',
|
|
199
231
|
'/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
|
|
200
232
|
'/stop', '/clear', '/compact', '/safe', '/del', '/perm', '/file', '/check',
|
|
201
|
-
'/
|
|
233
|
+
'/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/chatmode',
|
|
202
234
|
'/aid', '/upgrade', '/evolagent',
|
|
203
235
|
];
|
|
204
236
|
/** 判断消息内容是否为已知命令 */
|
|
205
237
|
isKnownCommand(content) {
|
|
206
|
-
return content === '/
|
|
238
|
+
return content === '/s' ||
|
|
207
239
|
MessageProcessor.COMMAND_PREFIXES.some(cmd => content.startsWith(cmd));
|
|
208
240
|
}
|
|
209
241
|
/**
|
|
@@ -211,10 +243,33 @@ export class MessageProcessor {
|
|
|
211
243
|
*/
|
|
212
244
|
async processMessage(message) {
|
|
213
245
|
const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
|
|
214
|
-
// 先解析会话,再优先用 session.metadata.
|
|
246
|
+
// 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
|
|
215
247
|
// message.channel 现在存实例名(channelName),可直接用于精确路由
|
|
216
248
|
const { session, absoluteProjectPath } = await this.resolveSession(message);
|
|
217
|
-
|
|
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
|
+
}
|
|
272
|
+
const channelKey = session.metadata?.channelKey || message.channel;
|
|
218
273
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
219
274
|
if (!channelInfo) {
|
|
220
275
|
logger.error(`[MessageProcessor] Unknown channel: ${channelKey}`);
|
|
@@ -252,6 +307,7 @@ export class MessageProcessor {
|
|
|
252
307
|
if (!monitorEnabled)
|
|
253
308
|
return;
|
|
254
309
|
monitor = new StreamIdleMonitor(idleMs);
|
|
310
|
+
this.activeMonitors.set(streamKey, monitor);
|
|
255
311
|
monitorInterval = setInterval(() => {
|
|
256
312
|
// Drain all pending levels in one tick
|
|
257
313
|
let result = monitor.check();
|
|
@@ -332,6 +388,7 @@ export class MessageProcessor {
|
|
|
332
388
|
finally {
|
|
333
389
|
if (monitorInterval)
|
|
334
390
|
clearInterval(monitorInterval);
|
|
391
|
+
this.activeMonitors.delete(streamKey);
|
|
335
392
|
}
|
|
336
393
|
}
|
|
337
394
|
/** 获取回复上下文(跟着任务走) */
|
|
@@ -341,7 +398,7 @@ export class MessageProcessor {
|
|
|
341
398
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
342
399
|
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress) {
|
|
343
400
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
344
|
-
const channelKey = session.metadata?.
|
|
401
|
+
const channelKey = session.metadata?.channelKey || message.channel;
|
|
345
402
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
346
403
|
// Per-method agent name for stats bucketing (agent.name or '<unknown>')
|
|
347
404
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
|
|
@@ -373,7 +430,7 @@ export class MessageProcessor {
|
|
|
373
430
|
};
|
|
374
431
|
};
|
|
375
432
|
const isProactive = session.sessionMode === 'proactive';
|
|
376
|
-
const isAutonomous = session.sessionMode === 'autonomous'
|
|
433
|
+
const isAutonomous = session.sessionMode === 'autonomous';
|
|
377
434
|
const envelope = buildEnvelope({
|
|
378
435
|
taskId,
|
|
379
436
|
channel: message.channel,
|
|
@@ -448,7 +505,7 @@ export class MessageProcessor {
|
|
|
448
505
|
if (baseReplyCtx) {
|
|
449
506
|
Object.assign(opts, baseReplyCtx);
|
|
450
507
|
}
|
|
451
|
-
else if (firstReply && message.messageId) {
|
|
508
|
+
else if (firstReply && message.messageId && message.source !== 'trigger') {
|
|
452
509
|
if (payload.kind === 'result.text' && payload.text) {
|
|
453
510
|
opts.replyToMessageId = message.messageId;
|
|
454
511
|
firstReply = false;
|
|
@@ -508,6 +565,10 @@ export class MessageProcessor {
|
|
|
508
565
|
: message.content;
|
|
509
566
|
let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
510
567
|
let effectiveSystemPrompt;
|
|
568
|
+
let modelOverride;
|
|
569
|
+
let usedFallback = false;
|
|
570
|
+
let skipEvolclawModel = false;
|
|
571
|
+
let agentModel;
|
|
511
572
|
try {
|
|
512
573
|
// 动态构建运行时上下文提示
|
|
513
574
|
const contextParts = [];
|
|
@@ -533,19 +594,62 @@ export class MessageProcessor {
|
|
|
533
594
|
contextParts.push(persona);
|
|
534
595
|
if (working)
|
|
535
596
|
contextParts.push(`[当前关注]\n${working}`);
|
|
536
|
-
// 计算 peerKey: <
|
|
597
|
+
// 计算 peerKey: <channelType>#<urlEncode(peerId)>
|
|
537
598
|
const peerIdRaw = message.peerId;
|
|
538
599
|
const peerKey = (currentChannelType && peerIdRaw)
|
|
539
|
-
?
|
|
600
|
+
? formatPeerKey(currentChannelType, peerIdRaw)
|
|
540
601
|
: undefined;
|
|
602
|
+
// 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
|
|
603
|
+
// 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
|
|
604
|
+
// 多对端并发各自独立解析、各自传参,无共享状态可被污染。
|
|
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);
|
|
616
|
+
}
|
|
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;
|
|
631
|
+
}
|
|
541
632
|
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
542
|
-
|
|
633
|
+
agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
543
634
|
// Kit renderer: 组装上下文
|
|
635
|
+
const pkgRoot = getPackageRoot();
|
|
544
636
|
const kitCtx = {
|
|
545
637
|
vars: {
|
|
546
638
|
EVOLCLAW_HOME: resolveRoot(),
|
|
547
|
-
PACKAGE_ROOT:
|
|
639
|
+
PACKAGE_ROOT: pkgRoot,
|
|
548
640
|
CURRENT_PROJECT: absoluteProjectPath,
|
|
641
|
+
// ECK 派生路径(manifest 引用时需要展开)
|
|
642
|
+
KITS: path.join(pkgRoot, 'kits'),
|
|
643
|
+
KITS_RULES: path.join(pkgRoot, 'kits', 'rules'),
|
|
644
|
+
KITS_DOCS: path.join(pkgRoot, 'kits', 'docs'),
|
|
645
|
+
KITS_TEMPLATES: path.join(pkgRoot, 'kits', 'templates'),
|
|
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',
|
|
649
|
+
// 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
|
|
650
|
+
PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
|
|
651
|
+
RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
|
|
652
|
+
VENUES_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'venues') : undefined,
|
|
549
653
|
selfAid: selfAid || undefined,
|
|
550
654
|
selfName: selfName || undefined,
|
|
551
655
|
hasPersona: !!persona,
|
|
@@ -553,23 +657,39 @@ export class MessageProcessor {
|
|
|
553
657
|
peerId: peerIdRaw || undefined,
|
|
554
658
|
peerKey,
|
|
555
659
|
peerName: peerName || undefined,
|
|
556
|
-
peerRole: session.identity?.role || '
|
|
660
|
+
peerRole: session.identity?.role || 'anonymous',
|
|
557
661
|
peerType: message.peerType || undefined,
|
|
662
|
+
sameDevice: message.sameDevice || undefined,
|
|
663
|
+
sameNetwork: message.sameNetwork || undefined,
|
|
664
|
+
sameEgressIp: message.sameEgressIp || undefined,
|
|
558
665
|
groupId: session.metadata?.groupId || undefined,
|
|
559
666
|
chatType: session.chatType || null,
|
|
560
667
|
channel: currentChannelType || null,
|
|
561
668
|
venueUid: undefined,
|
|
669
|
+
// 群分发模式 / 客户端类型 / 权限模式
|
|
670
|
+
dispatch: session.metadata?.dispatchMode || undefined,
|
|
671
|
+
clientType: message.clientType || undefined,
|
|
672
|
+
permissionMode: session.metadata?.permissionMode || 'auto',
|
|
562
673
|
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
563
674
|
project: path.basename(absoluteProjectPath),
|
|
564
675
|
sessionId: session.id,
|
|
565
676
|
sessionName: session.name || undefined,
|
|
566
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,
|
|
567
682
|
threadId: session.threadId || undefined,
|
|
683
|
+
// Stage 3: sessionKey 持久化字段
|
|
684
|
+
sessionKey: session.sessionKey,
|
|
568
685
|
chatMode: isProactive ? 'proactive' : 'interactive',
|
|
569
686
|
readonly: session.metadata?.permissionMode === 'readonly',
|
|
570
687
|
baseAgent: normalizedBaseagent.canonical,
|
|
571
688
|
baseAgentName: normalizedBaseagent.displayName,
|
|
572
689
|
baseAgentModel: agentModel || undefined,
|
|
690
|
+
effectiveModel: effectiveModel || agentModel || undefined,
|
|
691
|
+
modelFallbackActive: (fbState.fallbackActive || skipEvolclawModel) ? true : undefined,
|
|
692
|
+
modelFallbackModel: (fbState.fallbackActive || skipEvolclawModel) ? (agentModel || undefined) : undefined,
|
|
573
693
|
agentSessionId: session.agentSessionId || undefined,
|
|
574
694
|
},
|
|
575
695
|
sessionId: session.id,
|
|
@@ -584,16 +704,36 @@ export class MessageProcessor {
|
|
|
584
704
|
let streamRegistered = false;
|
|
585
705
|
try {
|
|
586
706
|
logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
|
|
587
|
-
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
|
|
707
|
+
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
588
708
|
agent.registerStream(streamKey, stream);
|
|
589
709
|
streamRegistered = true;
|
|
590
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
|
+
}
|
|
591
716
|
break; // 成功,跳出重试循环
|
|
592
717
|
}
|
|
593
718
|
catch (retryError) {
|
|
594
719
|
if (streamRegistered) {
|
|
595
720
|
agent.cleanupStream(streamKey);
|
|
596
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
|
+
}
|
|
597
737
|
if (attempt < MAX_RETRIES && isRetryableError(retryError)) {
|
|
598
738
|
const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
|
|
599
739
|
logger.warn(`[MessageProcessor] Retryable error (attempt ${attempt}/${MAX_RETRIES}), retrying in ${delay}ms:`, retryError);
|
|
@@ -613,9 +753,8 @@ export class MessageProcessor {
|
|
|
613
753
|
await renderer.flush();
|
|
614
754
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
615
755
|
if (compacted) {
|
|
616
|
-
// compact 成功,带 resume 重试(不重复原始消息,让 Agent 继续未完成的工作)
|
|
617
756
|
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
618
|
-
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
757
|
+
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
619
758
|
agent.registerStream(streamKey, retryStream);
|
|
620
759
|
streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
621
760
|
}
|
|
@@ -641,7 +780,7 @@ export class MessageProcessor {
|
|
|
641
780
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
642
781
|
if (compacted) {
|
|
643
782
|
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
644
|
-
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
783
|
+
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
645
784
|
agent.registerStream(streamKey, retryStream);
|
|
646
785
|
streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
647
786
|
// 重试后仍然 prompt_too_long:清理 renderer 中可能混入的错误文本,显示友好提示
|
|
@@ -651,7 +790,6 @@ export class MessageProcessor {
|
|
|
651
790
|
contextTooLongPattern.test(retryErrorsText) ||
|
|
652
791
|
contextTooLongPattern.test(streamResult.fullText));
|
|
653
792
|
if (retryStillTooLong) {
|
|
654
|
-
renderer.stripContextError(contextTooLongPattern);
|
|
655
793
|
renderer.addNotice(getContextTooLongHint(agent), 'warn', 'context-too-long', true);
|
|
656
794
|
}
|
|
657
795
|
}
|
|
@@ -768,6 +906,20 @@ export class MessageProcessor {
|
|
|
768
906
|
logger.info(`[MessageProcessor] agent.cleanupStream ok: session=${session.id} task=${taskId}`);
|
|
769
907
|
this.sessionManager.clearProcessing(session.id);
|
|
770
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
|
+
}
|
|
771
923
|
// 被用户中断(新消息打断)时跳过 flush — 新 task 已接管渠道,旧 task 的 flush 无意义且可能卡住
|
|
772
924
|
const preFlushInterrupt = this.interruptedSessions.get(session.id);
|
|
773
925
|
if (preFlushInterrupt === 'new_message' || preFlushInterrupt === 'stop' || preFlushInterrupt === 'recalled') {
|
|
@@ -830,7 +982,7 @@ export class MessageProcessor {
|
|
|
830
982
|
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
831
983
|
}
|
|
832
984
|
else {
|
|
833
|
-
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, 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(() => { });
|
|
834
986
|
}
|
|
835
987
|
}
|
|
836
988
|
if (message.triggerMeta) {
|
|
@@ -840,10 +992,6 @@ export class MessageProcessor {
|
|
|
840
992
|
else {
|
|
841
993
|
this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, messageId: messageId, durationMs });
|
|
842
994
|
}
|
|
843
|
-
// Clean up autonomous sessions after completion to avoid accumulating orphaned sessions
|
|
844
|
-
if (session.sessionMode === 'autonomous') {
|
|
845
|
-
this.sessionManager.unbindSession(session.id).catch(() => { });
|
|
846
|
-
}
|
|
847
995
|
}
|
|
848
996
|
await this.sessionManager.recordSuccess(session.id);
|
|
849
997
|
this.eventBus.publish({
|
|
@@ -953,7 +1101,7 @@ export class MessageProcessor {
|
|
|
953
1101
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
954
1102
|
let sendOpts;
|
|
955
1103
|
try {
|
|
956
|
-
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId, undefined, undefined, message.peerId, message.chatType, undefined, message.
|
|
1104
|
+
await this.sessionManager.getOrCreateSession(message.channel, message.channelId, this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd(), message.threadId, undefined, undefined, message.peerId, message.chatType, undefined, message.selfAID, message.channelType, message.peerType);
|
|
957
1105
|
sendOpts = this.getReplyContext(message);
|
|
958
1106
|
}
|
|
959
1107
|
catch { }
|
|
@@ -976,21 +1124,23 @@ export class MessageProcessor {
|
|
|
976
1124
|
? { replyContext: message.replyContext }
|
|
977
1125
|
: undefined;
|
|
978
1126
|
const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
|
|
979
|
-
//
|
|
980
|
-
if (message.triggerMeta?.
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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`);
|
|
987
1141
|
}
|
|
988
|
-
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
989
|
-
? session.projectPath
|
|
990
|
-
: path.resolve(process.cwd(), session.projectPath);
|
|
991
|
-
return { session, absoluteProjectPath };
|
|
992
1142
|
}
|
|
993
|
-
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, message.chatType, undefined, message.
|
|
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);
|
|
994
1144
|
// 兜底纠正1:群聊强制 proactive
|
|
995
1145
|
if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
|
|
996
1146
|
logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
|
|
@@ -1004,6 +1154,13 @@ export class MessageProcessor {
|
|
|
1004
1154
|
session.sessionMode = 'proactive';
|
|
1005
1155
|
await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
|
|
1006
1156
|
}
|
|
1157
|
+
// Proactive→Interactive 模式切换提示:上一轮 proactive 使用了标志位,本轮已切换为 interactive
|
|
1158
|
+
if (session.sessionMode === 'interactive' && session.metadata?.lastProactiveFlag) {
|
|
1159
|
+
message.content = '本轮会话已切换为 interactive 模式,无需调用工具发送消息。\n\n' + message.content;
|
|
1160
|
+
delete session.metadata.lastProactiveFlag;
|
|
1161
|
+
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1162
|
+
logger.info(`[MessageProcessor] Injected interactive mode hint for session ${session.id}`);
|
|
1163
|
+
}
|
|
1007
1164
|
// replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
|
|
1008
1165
|
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
1009
1166
|
? session.projectPath
|
|
@@ -1018,7 +1175,7 @@ export class MessageProcessor {
|
|
|
1018
1175
|
*/
|
|
1019
1176
|
async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress) {
|
|
1020
1177
|
// Per-session agent name for stats bucketing
|
|
1021
|
-
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.
|
|
1178
|
+
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelKey || session.channel)?.name ?? '<unknown>';
|
|
1022
1179
|
let hasReceivedText = false;
|
|
1023
1180
|
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
1024
1181
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
@@ -1100,7 +1257,7 @@ export class MessageProcessor {
|
|
|
1100
1257
|
if (event.type === 'compact') {
|
|
1101
1258
|
this.eventBus.publish({ type: 'runner:compact-complete', sessionId: session.id, preTokens: event.preTokens });
|
|
1102
1259
|
if (!shouldSuppress()) {
|
|
1103
|
-
renderer.addNotice(`\ud83d\udca1
|
|
1260
|
+
renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...)`, 'info', 'compact');
|
|
1104
1261
|
}
|
|
1105
1262
|
}
|
|
1106
1263
|
// 子任务进度
|
|
@@ -1178,14 +1335,15 @@ export class MessageProcessor {
|
|
|
1178
1335
|
// SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
|
|
1179
1336
|
// 仅记录状态,最终 flush(true) 在流结束后统一执行
|
|
1180
1337
|
if (event.type === 'complete') {
|
|
1181
|
-
|
|
1338
|
+
const isAbort = event.terminalReason === 'aborted_streaming' || event.terminalReason === 'aborted_tools';
|
|
1339
|
+
logger.info(`[MessageProcessor] ${isAbort ? 'task interrupted' : 'complete event'}: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
|
|
1182
1340
|
// 自动回填会话名称
|
|
1183
1341
|
if (event.sessionTitle && session.name === '默认会话') {
|
|
1184
1342
|
await this.sessionManager.renameSession(session.id, event.sessionTitle);
|
|
1185
1343
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1186
1344
|
}
|
|
1187
1345
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
1188
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns,
|
|
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 };
|
|
1189
1347
|
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
1190
1348
|
// 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
|
|
1191
1349
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
@@ -1213,6 +1371,15 @@ export class MessageProcessor {
|
|
|
1213
1371
|
if (renderer.hasContent()) {
|
|
1214
1372
|
await renderer.flushActivitiesOnly();
|
|
1215
1373
|
}
|
|
1374
|
+
// 检测 proactive 标志位,设置 lastProactiveFlag 供模式切换提示使用
|
|
1375
|
+
if (session.sessionMode === 'proactive' && lastReplyText) {
|
|
1376
|
+
if (/\[PROACTIVE:REPLY_CONFIRMED_(SENT|NONE)\]/.test(lastReplyText)) {
|
|
1377
|
+
session.metadata = session.metadata || {};
|
|
1378
|
+
session.metadata.lastProactiveFlag = true;
|
|
1379
|
+
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1380
|
+
logger.debug(`[MessageProcessor] Set lastProactiveFlag for session ${session.id}`);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1216
1383
|
}
|
|
1217
1384
|
continue;
|
|
1218
1385
|
}
|
|
@@ -1232,7 +1399,7 @@ export class MessageProcessor {
|
|
|
1232
1399
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1233
1400
|
}
|
|
1234
1401
|
// 记录完成状态
|
|
1235
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns,
|
|
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 };
|
|
1236
1403
|
if (event.subtype === 'success') {
|
|
1237
1404
|
this.messageCache.addEvent(session.id, {
|
|
1238
1405
|
type: 'completed',
|
|
@@ -1345,85 +1512,6 @@ export class MessageProcessor {
|
|
|
1345
1512
|
// 都找不到,返回项目根目录路径
|
|
1346
1513
|
return rootPath;
|
|
1347
1514
|
}
|
|
1348
|
-
/**
|
|
1349
|
-
* 确保全局数据目录下有最新版本的 SKILLS.md
|
|
1350
|
-
* 目标:{EVOLCLAW_HOME}/data/SKILLS.md
|
|
1351
|
-
*/
|
|
1352
|
-
ensureSkillsFile() {
|
|
1353
|
-
try {
|
|
1354
|
-
const targetDir = path.join(resolveRoot(), 'data');
|
|
1355
|
-
const targetPath = path.join(targetDir, 'SKILLS.md');
|
|
1356
|
-
const templatePath = path.join(getPackageRoot(), 'src', 'templates', 'skills.md');
|
|
1357
|
-
// 模板不存在则跳过(构建环境可能没有 src/)
|
|
1358
|
-
if (!fs.existsSync(templatePath)) {
|
|
1359
|
-
// 尝试 dist/templates/skills.md
|
|
1360
|
-
const distTemplatePath = path.join(getPackageRoot(), 'dist', 'templates', 'skills.md');
|
|
1361
|
-
if (!fs.existsSync(distTemplatePath))
|
|
1362
|
-
return;
|
|
1363
|
-
this.copySkillsIfNeeded(distTemplatePath, targetDir, targetPath);
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
this.copySkillsIfNeeded(templatePath, targetDir, targetPath);
|
|
1367
|
-
}
|
|
1368
|
-
catch {
|
|
1369
|
-
// 静默失败,不影响正常消息处理
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
copySkillsIfNeeded(templatePath, targetDir, targetPath) {
|
|
1373
|
-
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
1374
|
-
const templateVersion = templateContent.match(/^version:\s*(.+)$/m)?.[1]?.trim() || '0';
|
|
1375
|
-
if (fs.existsSync(targetPath)) {
|
|
1376
|
-
const existing = fs.readFileSync(targetPath, 'utf-8');
|
|
1377
|
-
const existingVersion = existing.match(/^version:\s*(.+)$/m)?.[1]?.trim() || '0';
|
|
1378
|
-
if (this.compareSemver(existingVersion, templateVersion) >= 0)
|
|
1379
|
-
return; // 已是最新
|
|
1380
|
-
}
|
|
1381
|
-
if (!fs.existsSync(targetDir)) {
|
|
1382
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
1383
|
-
}
|
|
1384
|
-
fs.writeFileSync(targetPath, templateContent, 'utf-8');
|
|
1385
|
-
}
|
|
1386
|
-
/** 简易 semver 比较:支持 "1", "1.0", "1.0.0" 等格式,返回 -1/0/1 */
|
|
1387
|
-
compareSemver(a, b) {
|
|
1388
|
-
const pa = a.split('.').map(Number);
|
|
1389
|
-
const pb = b.split('.').map(Number);
|
|
1390
|
-
const len = Math.max(pa.length, pb.length);
|
|
1391
|
-
for (let i = 0; i < len; i++) {
|
|
1392
|
-
const na = pa[i] || 0;
|
|
1393
|
-
const nb = pb[i] || 0;
|
|
1394
|
-
if (na !== nb)
|
|
1395
|
-
return na > nb ? 1 : -1;
|
|
1396
|
-
}
|
|
1397
|
-
return 0;
|
|
1398
|
-
}
|
|
1399
|
-
/**
|
|
1400
|
-
* 从 data/SKILLS.md 读取 frontmatter 并生成提示。
|
|
1401
|
-
* 不缓存:每次读取保证用户编辑立即生效。
|
|
1402
|
-
* 调用前应确保 ensureSkillsFile() 已执行过(首次落盘)。
|
|
1403
|
-
*/
|
|
1404
|
-
getSkillsHint() {
|
|
1405
|
-
try {
|
|
1406
|
-
const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
|
|
1407
|
-
if (!fs.existsSync(skillsPath))
|
|
1408
|
-
return null;
|
|
1409
|
-
const content = fs.readFileSync(skillsPath, 'utf-8');
|
|
1410
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1411
|
-
if (!frontmatterMatch)
|
|
1412
|
-
return null;
|
|
1413
|
-
const fm = frontmatterMatch[1];
|
|
1414
|
-
const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
|
|
1415
|
-
const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
|
|
1416
|
-
const parts = [
|
|
1417
|
-
`可通过 Bash 指令管理运行时,${desc}。`,
|
|
1418
|
-
trigger ? `触发时机:${trigger}。` : '',
|
|
1419
|
-
`完整文档见 ${skillsPath}`,
|
|
1420
|
-
];
|
|
1421
|
-
return parts.filter(Boolean).join('');
|
|
1422
|
-
}
|
|
1423
|
-
catch {
|
|
1424
|
-
return null;
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
1515
|
/**
|
|
1428
1516
|
* 判断文件路径是否为占位符/示例文本
|
|
1429
1517
|
* 用于过滤大模型在说明文字中误写的 [SEND_FILE:...] 标记
|
|
@@ -10,6 +10,7 @@ export class StreamIdleMonitor {
|
|
|
10
10
|
state;
|
|
11
11
|
triggeredLevels = new Set();
|
|
12
12
|
idleMs;
|
|
13
|
+
paused = false;
|
|
13
14
|
constructor(idleMs) {
|
|
14
15
|
this.idleMs = idleMs;
|
|
15
16
|
this.state = {
|
|
@@ -22,6 +23,24 @@ export class StreamIdleMonitor {
|
|
|
22
23
|
hasReceivedText: false,
|
|
23
24
|
};
|
|
24
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* 暂停空闲计时(等待用户交互期间调用,如权限确认 / AskUserQuestion / PlanMode)。
|
|
28
|
+
* 暂停期间 check() 始终返回 null,等待时长不计入 idle。
|
|
29
|
+
*/
|
|
30
|
+
pause() {
|
|
31
|
+
this.paused = true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 恢复空闲计时。从恢复时刻重新起算 idle,并清空已触发级别——
|
|
35
|
+
* 用户应答后是一次全新的执行周期,不应继承等待前的 idle 状态。
|
|
36
|
+
*/
|
|
37
|
+
resume() {
|
|
38
|
+
if (!this.paused)
|
|
39
|
+
return;
|
|
40
|
+
this.paused = false;
|
|
41
|
+
this.state.lastEventTime = Date.now();
|
|
42
|
+
this.triggeredLevels.clear();
|
|
43
|
+
}
|
|
25
44
|
/**
|
|
26
45
|
* 记录 SDK 事件,更新状态并重置空闲计时
|
|
27
46
|
*/
|
|
@@ -44,6 +63,8 @@ export class StreamIdleMonitor {
|
|
|
44
63
|
* 检查空闲状态,返回 null(未空闲)或分级结果
|
|
45
64
|
*/
|
|
46
65
|
check() {
|
|
66
|
+
if (this.paused)
|
|
67
|
+
return null;
|
|
47
68
|
const now = Date.now();
|
|
48
69
|
const idleDuration = now - this.state.lastEventTime;
|
|
49
70
|
const notifyThreshold = this.idleMs * NOTIFY_MULTIPLIER;
|