evolclaw 3.1.3 → 3.1.5
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 +27 -0
- package/assets/.env.template +4 -0
- package/assets/config.json.template +6 -0
- package/assets/wechat-group-qr.jpeg +0 -0
- package/dist/agents/claude-runner.js +348 -156
- package/dist/agents/kit-renderer.js +211 -42
- package/dist/aun/aid/agentmd.js +75 -139
- package/dist/aun/aid/client.js +1 -14
- package/dist/aun/aid/identity.js +381 -54
- package/dist/aun/aid/index.js +3 -2
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/msg/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -35
- package/dist/channels/aun.js +92 -144
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +270 -190
- package/dist/channels/qqbot.js +1 -0
- package/dist/channels/wechat.js +1 -0
- package/dist/channels/wecom.js +1 -0
- package/dist/cli/agent.js +26 -27
- package/dist/cli/bench.js +45 -34
- package/dist/cli/help.js +23 -0
- package/dist/cli/index.js +538 -77
- package/dist/cli/init-channel.js +7 -4
- package/dist/cli/link-rules.js +2 -1
- package/dist/cli/model.js +324 -0
- package/dist/cli/net-check.js +138 -56
- 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/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +87 -93
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -4
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/message-bridge.js +6 -6
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +104 -118
- 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 +78 -44
- 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 +312 -251
- package/dist/core/session/session-mapper.js +9 -4
- package/dist/core/trigger/manager.js +37 -0
- package/dist/core/trigger/scheduler.js +2 -1
- package/dist/index.js +10 -3
- package/dist/ipc.js +22 -0
- package/dist/paths.js +87 -16
- package/dist/utils/npm-ops.js +18 -11
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +11 -7
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +181 -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 +82 -0
- package/kits/docs/evolclaw/msg.md +86 -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 +11 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +75 -39
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/05-venue.md +2 -2
- package/kits/rules/06-channel.md +30 -27
- package/kits/templates/system-fragments/baseagent.md +7 -1
- package/kits/templates/system-fragments/channel.md +4 -1
- package/kits/templates/system-fragments/identity.md +4 -4
- package/kits/templates/system-fragments/relation.md +8 -5
- package/kits/templates/system-fragments/session.md +27 -0
- package/kits/templates/system-fragments/venue.md +13 -1
- package/package.json +13 -6
- package/dist/aun/aid/lifecycle-log.js +0 -33
- package/dist/net-check.js +0 -640
- package/dist/utils/aid-lifecycle-log.js +0 -33
- package/dist/watch-msg.js +0 -544
- 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
- package/kits/templates/system-fragments/eckruntime.md +0 -14
|
@@ -12,6 +12,8 @@ import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
|
12
12
|
import { renderKitSections } from '../../agents/kit-renderer.js';
|
|
13
13
|
import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
|
|
14
14
|
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
15
|
+
import { formatPeerKey } from '../relation/peer-key.js';
|
|
16
|
+
import { resolveEffectiveModel } from '../model/model-scope.js';
|
|
15
17
|
function getContextTooLongHint(agent) {
|
|
16
18
|
if (canCompactAgent(agent)) {
|
|
17
19
|
return '上下文过长,请精简提问或使用 /compact 压缩上下文';
|
|
@@ -69,7 +71,8 @@ export class MessageProcessor {
|
|
|
69
71
|
interruptedSessions = new Map(); // sessionId → reason ('new_message' | 'stop' | ...)
|
|
70
72
|
interactionRouter;
|
|
71
73
|
messageQueue;
|
|
72
|
-
|
|
74
|
+
/** sessionId → 活跃的空闲监控器,用于等待用户交互期间暂停/恢复计时 */
|
|
75
|
+
activeMonitors = new Map();
|
|
73
76
|
/**
|
|
74
77
|
* Get the runner for a given (channel, baseagent) pair.
|
|
75
78
|
*
|
|
@@ -95,10 +98,11 @@ export class MessageProcessor {
|
|
|
95
98
|
return [...this.agentMap.keys()];
|
|
96
99
|
}
|
|
97
100
|
/** 判断是否为后台会话(仅主会话参与判断,话题会话独立) */
|
|
98
|
-
|
|
101
|
+
isBackgroundSession(session, _channel, _channelId) {
|
|
99
102
|
if (session.threadId)
|
|
100
103
|
return false;
|
|
101
|
-
|
|
104
|
+
// 使用 session 自身的 channelType 精确定位 active.json,避免扫描误匹配
|
|
105
|
+
const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.selfAID);
|
|
102
106
|
return active ? session.id !== active.id : false;
|
|
103
107
|
}
|
|
104
108
|
constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
|
|
@@ -125,6 +129,16 @@ export class MessageProcessor {
|
|
|
125
129
|
}
|
|
126
130
|
setInteractionRouter(router) {
|
|
127
131
|
this.interactionRouter = router;
|
|
132
|
+
// 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
|
|
133
|
+
// 避免把「正在等用户点按钮」误判为「任务卡死」而中断任务。
|
|
134
|
+
router.setWaitHooks({
|
|
135
|
+
onWaitStart: (sessionId) => {
|
|
136
|
+
this.activeMonitors.get(sessionId)?.pause();
|
|
137
|
+
},
|
|
138
|
+
onWaitEnd: (sessionId) => {
|
|
139
|
+
this.activeMonitors.get(sessionId)?.resume();
|
|
140
|
+
},
|
|
141
|
+
});
|
|
128
142
|
}
|
|
129
143
|
setMessageQueue(queue) {
|
|
130
144
|
this.messageQueue = queue;
|
|
@@ -210,10 +224,10 @@ export class MessageProcessor {
|
|
|
210
224
|
*/
|
|
211
225
|
async processMessage(message) {
|
|
212
226
|
const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
|
|
213
|
-
// 先解析会话,再优先用 session.metadata.
|
|
227
|
+
// 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
|
|
214
228
|
// message.channel 现在存实例名(channelName),可直接用于精确路由
|
|
215
229
|
const { session, absoluteProjectPath } = await this.resolveSession(message);
|
|
216
|
-
const channelKey = session.metadata?.
|
|
230
|
+
const channelKey = session.metadata?.channelKey || message.channel;
|
|
217
231
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
218
232
|
if (!channelInfo) {
|
|
219
233
|
logger.error(`[MessageProcessor] Unknown channel: ${channelKey}`);
|
|
@@ -245,12 +259,13 @@ export class MessageProcessor {
|
|
|
245
259
|
monitor?.recordEvent(eventType || 'unknown', toolName);
|
|
246
260
|
};
|
|
247
261
|
// Cache background status to avoid async call inside setInterval
|
|
248
|
-
const isBackground =
|
|
262
|
+
const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
249
263
|
const timeoutPromise = new Promise((_, reject) => {
|
|
250
264
|
rejectFn = reject;
|
|
251
265
|
if (!monitorEnabled)
|
|
252
266
|
return;
|
|
253
267
|
monitor = new StreamIdleMonitor(idleMs);
|
|
268
|
+
this.activeMonitors.set(streamKey, monitor);
|
|
254
269
|
monitorInterval = setInterval(() => {
|
|
255
270
|
// Drain all pending levels in one tick
|
|
256
271
|
let result = monitor.check();
|
|
@@ -331,6 +346,7 @@ export class MessageProcessor {
|
|
|
331
346
|
finally {
|
|
332
347
|
if (monitorInterval)
|
|
333
348
|
clearInterval(monitorInterval);
|
|
349
|
+
this.activeMonitors.delete(streamKey);
|
|
334
350
|
}
|
|
335
351
|
}
|
|
336
352
|
/** 获取回复上下文(跟着任务走) */
|
|
@@ -340,7 +356,7 @@ export class MessageProcessor {
|
|
|
340
356
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
341
357
|
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress) {
|
|
342
358
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
343
|
-
const channelKey = session.metadata?.
|
|
359
|
+
const channelKey = session.metadata?.channelKey || message.channel;
|
|
344
360
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
345
361
|
// Per-method agent name for stats bucketing (agent.name or '<unknown>')
|
|
346
362
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
|
|
@@ -382,7 +398,7 @@ export class MessageProcessor {
|
|
|
382
398
|
replyContext: taskReplyContext(),
|
|
383
399
|
});
|
|
384
400
|
try {
|
|
385
|
-
const isBackground =
|
|
401
|
+
const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
386
402
|
// 记录收到消息
|
|
387
403
|
logger.message({
|
|
388
404
|
msgId: messageId,
|
|
@@ -439,7 +455,7 @@ export class MessageProcessor {
|
|
|
439
455
|
// (不支持 thought 的 channel 静默丢弃,避免降级为普通消息)
|
|
440
456
|
if (isProactive && payload.kind === 'activity.batch' && !adapter.capabilities?.thought)
|
|
441
457
|
return;
|
|
442
|
-
const isCurrentlyBackground =
|
|
458
|
+
const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
443
459
|
if (isCurrentlyBackground)
|
|
444
460
|
return;
|
|
445
461
|
const opts = {};
|
|
@@ -447,7 +463,7 @@ export class MessageProcessor {
|
|
|
447
463
|
if (baseReplyCtx) {
|
|
448
464
|
Object.assign(opts, baseReplyCtx);
|
|
449
465
|
}
|
|
450
|
-
else if (firstReply && message.messageId) {
|
|
466
|
+
else if (firstReply && message.messageId && message.source !== 'trigger') {
|
|
451
467
|
if (payload.kind === 'result.text' && payload.text) {
|
|
452
468
|
opts.replyToMessageId = message.messageId;
|
|
453
469
|
firstReply = false;
|
|
@@ -507,6 +523,7 @@ export class MessageProcessor {
|
|
|
507
523
|
: message.content;
|
|
508
524
|
let streamResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
509
525
|
let effectiveSystemPrompt;
|
|
526
|
+
let modelOverride;
|
|
510
527
|
try {
|
|
511
528
|
// 动态构建运行时上下文提示
|
|
512
529
|
const contextParts = [];
|
|
@@ -516,18 +533,13 @@ export class MessageProcessor {
|
|
|
516
533
|
const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
517
534
|
const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
|
|
518
535
|
const peerName = message.peerName || session.metadata?.peerName;
|
|
519
|
-
// 文件发送能力
|
|
520
|
-
let currentCanSend = false;
|
|
521
|
-
if (!isProactive) {
|
|
522
|
-
currentCanSend = !!(channelInfo.adapter.capabilities?.file);
|
|
523
|
-
}
|
|
524
536
|
// 通道能力
|
|
525
537
|
const capParts = [];
|
|
526
538
|
if (options?.supportsImages)
|
|
527
539
|
capParts.push('图片输入');
|
|
528
540
|
if (channelInfo.adapter.capabilities?.image)
|
|
529
541
|
capParts.push('图片输出');
|
|
530
|
-
if (channelInfo.adapter.capabilities?.file)
|
|
542
|
+
if (!isProactive && channelInfo.adapter.capabilities?.file)
|
|
531
543
|
capParts.push('文件发送');
|
|
532
544
|
// Personal layer
|
|
533
545
|
const owningAgent = this.agentRegistry?.resolveByChannel(channelKey);
|
|
@@ -537,18 +549,41 @@ export class MessageProcessor {
|
|
|
537
549
|
contextParts.push(persona);
|
|
538
550
|
if (working)
|
|
539
551
|
contextParts.push(`[当前关注]\n${working}`);
|
|
540
|
-
// 计算 peerKey: <
|
|
552
|
+
// 计算 peerKey: <channelType>#<urlEncode(peerId)>
|
|
541
553
|
const peerIdRaw = message.peerId;
|
|
542
554
|
const peerKey = (currentChannelType && peerIdRaw)
|
|
543
|
-
?
|
|
555
|
+
? formatPeerKey(currentChannelType, peerIdRaw)
|
|
544
556
|
: undefined;
|
|
557
|
+
// 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
|
|
558
|
+
// 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
|
|
559
|
+
// 多对端并发各自独立解析、各自传参,无共享状态可被污染。
|
|
560
|
+
try {
|
|
561
|
+
const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
|
|
562
|
+
if (resolved.model)
|
|
563
|
+
modelOverride = { model: resolved.model, effort: resolved.effort };
|
|
564
|
+
}
|
|
565
|
+
catch (e) {
|
|
566
|
+
logger.warn(`[MessageProcessor] resolveEffectiveModel failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
567
|
+
}
|
|
545
568
|
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
569
|
+
const agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
546
570
|
// Kit renderer: 组装上下文
|
|
571
|
+
const pkgRoot = getPackageRoot();
|
|
547
572
|
const kitCtx = {
|
|
548
573
|
vars: {
|
|
549
574
|
EVOLCLAW_HOME: resolveRoot(),
|
|
550
|
-
PACKAGE_ROOT:
|
|
575
|
+
PACKAGE_ROOT: pkgRoot,
|
|
551
576
|
CURRENT_PROJECT: absoluteProjectPath,
|
|
577
|
+
// ECK 派生路径(manifest 引用时需要展开)
|
|
578
|
+
KITS: path.join(pkgRoot, 'kits'),
|
|
579
|
+
KITS_RULES: path.join(pkgRoot, 'kits', 'rules'),
|
|
580
|
+
KITS_DOCS: path.join(pkgRoot, 'kits', 'docs'),
|
|
581
|
+
KITS_TEMPLATES: path.join(pkgRoot, 'kits', 'templates'),
|
|
582
|
+
KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
|
|
583
|
+
// 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
|
|
584
|
+
PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
|
|
585
|
+
RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
|
|
586
|
+
VENUES_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'venues') : undefined,
|
|
552
587
|
selfAid: selfAid || undefined,
|
|
553
588
|
selfName: selfName || undefined,
|
|
554
589
|
hasPersona: !!persona,
|
|
@@ -556,20 +591,30 @@ export class MessageProcessor {
|
|
|
556
591
|
peerId: peerIdRaw || undefined,
|
|
557
592
|
peerKey,
|
|
558
593
|
peerName: peerName || undefined,
|
|
559
|
-
peerRole: session.identity?.role || '
|
|
594
|
+
peerRole: session.identity?.role || 'anonymous',
|
|
595
|
+
peerType: message.peerType || undefined,
|
|
560
596
|
groupId: session.metadata?.groupId || undefined,
|
|
561
|
-
scene: session.chatType ? (session.chatType === 'group' ? 'group' : 'private') : 'coding',
|
|
562
597
|
chatType: session.chatType || null,
|
|
563
598
|
channel: currentChannelType || null,
|
|
564
599
|
venueUid: undefined,
|
|
600
|
+
// 群分发模式 / 客户端类型 / 权限模式
|
|
601
|
+
dispatch: session.metadata?.dispatchMode || undefined,
|
|
602
|
+
clientType: message.clientType || undefined,
|
|
603
|
+
permissionMode: session.metadata?.permissionMode || 'auto',
|
|
604
|
+
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
565
605
|
project: path.basename(absoluteProjectPath),
|
|
606
|
+
sessionId: session.id,
|
|
566
607
|
sessionName: session.name || undefined,
|
|
567
|
-
|
|
608
|
+
sessionCreatedAt: session.createdAt ? new Date(session.createdAt).toISOString() : undefined,
|
|
609
|
+
threadId: session.threadId || undefined,
|
|
610
|
+
// Stage 3: sessionKey 持久化字段
|
|
611
|
+
sessionKey: session.sessionKey,
|
|
612
|
+
chatMode: isProactive ? 'proactive' : 'interactive',
|
|
568
613
|
readonly: session.metadata?.permissionMode === 'readonly',
|
|
569
|
-
canSendFile: !isProactive && currentCanSend,
|
|
570
|
-
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
571
614
|
baseAgent: normalizedBaseagent.canonical,
|
|
572
615
|
baseAgentName: normalizedBaseagent.displayName,
|
|
616
|
+
baseAgentModel: agentModel || undefined,
|
|
617
|
+
agentSessionId: session.agentSessionId || undefined,
|
|
573
618
|
},
|
|
574
619
|
sessionId: session.id,
|
|
575
620
|
};
|
|
@@ -583,7 +628,7 @@ export class MessageProcessor {
|
|
|
583
628
|
let streamRegistered = false;
|
|
584
629
|
try {
|
|
585
630
|
logger.info(`[MessageProcessor] agent.runQuery start: agent=${agent.name} session=${session.id} task=${taskId} attempt=${attempt}/${MAX_RETRIES} agentSessionId=${session.agentSessionId ?? 'none'}`);
|
|
586
|
-
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager);
|
|
631
|
+
const stream = await agent.runQuery(session.id, effectivePrompt, absoluteProjectPath, session.agentSessionId, message.images, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
587
632
|
agent.registerStream(streamKey, stream);
|
|
588
633
|
streamRegistered = true;
|
|
589
634
|
streamResult = await this.processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
@@ -612,9 +657,11 @@ export class MessageProcessor {
|
|
|
612
657
|
await renderer.flush();
|
|
613
658
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
614
659
|
if (compacted) {
|
|
615
|
-
// compact
|
|
660
|
+
// compact 成功,清除第一次流中混入的错误文本,再重试
|
|
661
|
+
const ctxErrPattern = /prompt is too long|input is too long|上下文过长/i;
|
|
662
|
+
renderer.stripContextError(ctxErrPattern);
|
|
616
663
|
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
617
|
-
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
664
|
+
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
618
665
|
agent.registerStream(streamKey, retryStream);
|
|
619
666
|
streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
620
667
|
}
|
|
@@ -635,12 +682,13 @@ export class MessageProcessor {
|
|
|
635
682
|
contextTooLongPattern.test(errorsText) ||
|
|
636
683
|
contextTooLongPattern.test(streamResult.fullText));
|
|
637
684
|
if (isPromptTooLong) {
|
|
685
|
+
renderer.stripContextError(contextTooLongPattern);
|
|
638
686
|
renderer.addNotice('上下文过长,正在压缩会话...', 'warn', 'compact-trigger', true);
|
|
639
687
|
await renderer.flush();
|
|
640
688
|
const compacted = await agent.compact(session.id, session.agentSessionId, absoluteProjectPath);
|
|
641
689
|
if (compacted) {
|
|
642
690
|
renderer.addNotice('✅ 压缩完成,继续处理...', 'info', 'compact-retry', true);
|
|
643
|
-
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager);
|
|
691
|
+
const retryStream = await agent.runQuery(session.id, '上下文已自动压缩,请继续之前未完成的任务。', absoluteProjectPath, session.agentSessionId, undefined, effectiveSystemPrompt, this.sessionManager, modelOverride);
|
|
644
692
|
agent.registerStream(streamKey, retryStream);
|
|
645
693
|
streamResult = await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress);
|
|
646
694
|
// 重试后仍然 prompt_too_long:清理 renderer 中可能混入的错误文本,显示友好提示
|
|
@@ -752,7 +800,7 @@ export class MessageProcessor {
|
|
|
752
800
|
if (finalReplyText) {
|
|
753
801
|
if (isProactive && !streamResult.hasReceivedText && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim())) {
|
|
754
802
|
// Proactive 模式 + SDK 本地兜底:直接发送绕过 silent renderer
|
|
755
|
-
const isCurrentlyBackground =
|
|
803
|
+
const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
756
804
|
if (!isCurrentlyBackground) {
|
|
757
805
|
await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text: finalReplyText, isFinal: true });
|
|
758
806
|
logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
|
|
@@ -829,7 +877,7 @@ export class MessageProcessor {
|
|
|
829
877
|
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
830
878
|
}
|
|
831
879
|
else {
|
|
832
|
-
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
|
|
880
|
+
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs, ttftMs: streamResult.ttftMs, numTurns: streamResult.numTurns, usage: streamResult.usage } }).catch(() => { });
|
|
833
881
|
}
|
|
834
882
|
}
|
|
835
883
|
if (message.triggerMeta) {
|
|
@@ -868,7 +916,7 @@ export class MessageProcessor {
|
|
|
868
916
|
// 写入消息记录(出方向)已下沉到 aun.ts:deliverTextEntry,
|
|
869
917
|
// 所有 message.send 成功后统一写入 messages.jsonl,此处不再重复写入。
|
|
870
918
|
}
|
|
871
|
-
const isFinallyBackground =
|
|
919
|
+
const isFinallyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
872
920
|
if (isFinallyBackground && session.sessionMode !== 'autonomous') {
|
|
873
921
|
const projectName = path.basename(session.projectPath);
|
|
874
922
|
const count = this.messageCache.getCount(session.id);
|
|
@@ -952,7 +1000,7 @@ export class MessageProcessor {
|
|
|
952
1000
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
953
1001
|
let sendOpts;
|
|
954
1002
|
try {
|
|
955
|
-
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.
|
|
1003
|
+
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);
|
|
956
1004
|
sendOpts = this.getReplyContext(message);
|
|
957
1005
|
}
|
|
958
1006
|
catch { }
|
|
@@ -989,7 +1037,7 @@ export class MessageProcessor {
|
|
|
989
1037
|
: path.resolve(process.cwd(), session.projectPath);
|
|
990
1038
|
return { session, absoluteProjectPath };
|
|
991
1039
|
}
|
|
992
|
-
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, undefined, message.peerId, message.chatType, undefined, message.
|
|
1040
|
+
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);
|
|
993
1041
|
// 兜底纠正1:群聊强制 proactive
|
|
994
1042
|
if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
|
|
995
1043
|
logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
|
|
@@ -1003,6 +1051,13 @@ export class MessageProcessor {
|
|
|
1003
1051
|
session.sessionMode = 'proactive';
|
|
1004
1052
|
await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
|
|
1005
1053
|
}
|
|
1054
|
+
// Proactive→Interactive 模式切换提示:上一轮 proactive 使用了标志位,本轮已切换为 interactive
|
|
1055
|
+
if (session.sessionMode === 'interactive' && session.metadata?.lastProactiveFlag) {
|
|
1056
|
+
message.content = '本轮会话已切换为 interactive 模式,无需调用工具发送消息。\n\n' + message.content;
|
|
1057
|
+
delete session.metadata.lastProactiveFlag;
|
|
1058
|
+
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1059
|
+
logger.info(`[MessageProcessor] Injected interactive mode hint for session ${session.id}`);
|
|
1060
|
+
}
|
|
1006
1061
|
// replyContext 不再写入 session.metadata(跟着 message 走,避免群聊多人覆盖)
|
|
1007
1062
|
const absoluteProjectPath = path.isAbsolute(session.projectPath)
|
|
1008
1063
|
? session.projectPath
|
|
@@ -1017,7 +1072,7 @@ export class MessageProcessor {
|
|
|
1017
1072
|
*/
|
|
1018
1073
|
async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress) {
|
|
1019
1074
|
// Per-session agent name for stats bucketing
|
|
1020
|
-
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.
|
|
1075
|
+
const agentNameForStats = this.agentRegistry?.resolveByChannel(session.metadata?.channelKey || session.channel)?.name ?? '<unknown>';
|
|
1021
1076
|
let hasReceivedText = false;
|
|
1022
1077
|
let hasErrorResult = false; // 是否已有 tool_result/error 事件输出过错误
|
|
1023
1078
|
let completeResult = { isError: false, lastReplyText: '', fullText: '', hasReceivedText: false };
|
|
@@ -1083,7 +1138,7 @@ export class MessageProcessor {
|
|
|
1083
1138
|
});
|
|
1084
1139
|
continue;
|
|
1085
1140
|
}
|
|
1086
|
-
const isCurrentlyBackground =
|
|
1141
|
+
const isCurrentlyBackground = this.isBackgroundSession(session, session.channel, session.channelId);
|
|
1087
1142
|
// === 前台任务:正常处理所有事件 ===
|
|
1088
1143
|
if (!isCurrentlyBackground) {
|
|
1089
1144
|
// 流式文本
|
|
@@ -1099,7 +1154,7 @@ export class MessageProcessor {
|
|
|
1099
1154
|
if (event.type === 'compact') {
|
|
1100
1155
|
this.eventBus.publish({ type: 'runner:compact-complete', sessionId: session.id, preTokens: event.preTokens });
|
|
1101
1156
|
if (!shouldSuppress()) {
|
|
1102
|
-
renderer.addNotice(`\ud83d\udca1
|
|
1157
|
+
renderer.addNotice(`\ud83d\udca1 会话压缩完成,继续执行...)`, 'info', 'compact');
|
|
1103
1158
|
}
|
|
1104
1159
|
}
|
|
1105
1160
|
// 子任务进度
|
|
@@ -1177,14 +1232,15 @@ export class MessageProcessor {
|
|
|
1177
1232
|
// SDK 可能产生多个 complete 事件(如 subagent 或 auto-compact 二次查询),
|
|
1178
1233
|
// 仅记录状态,最终 flush(true) 在流结束后统一执行
|
|
1179
1234
|
if (event.type === 'complete') {
|
|
1180
|
-
|
|
1235
|
+
const isAbort = event.terminalReason === 'aborted_streaming' || event.terminalReason === 'aborted_tools';
|
|
1236
|
+
logger.info(`[MessageProcessor] ${isAbort ? 'task interrupted' : 'complete event'}: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
|
|
1181
1237
|
// 自动回填会话名称
|
|
1182
1238
|
if (event.sessionTitle && session.name === '默认会话') {
|
|
1183
1239
|
await this.sessionManager.renameSession(session.id, event.sessionTitle);
|
|
1184
1240
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1185
1241
|
}
|
|
1186
1242
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
1187
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
|
|
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, usage: event.usage };
|
|
1188
1244
|
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
1189
1245
|
// 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
|
|
1190
1246
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
@@ -1212,6 +1268,15 @@ export class MessageProcessor {
|
|
|
1212
1268
|
if (renderer.hasContent()) {
|
|
1213
1269
|
await renderer.flushActivitiesOnly();
|
|
1214
1270
|
}
|
|
1271
|
+
// 检测 proactive 标志位,设置 lastProactiveFlag 供模式切换提示使用
|
|
1272
|
+
if (session.sessionMode === 'proactive' && lastReplyText) {
|
|
1273
|
+
if (/\[PROACTIVE:REPLY_CONFIRMED_(SENT|NONE)\]/.test(lastReplyText)) {
|
|
1274
|
+
session.metadata = session.metadata || {};
|
|
1275
|
+
session.metadata.lastProactiveFlag = true;
|
|
1276
|
+
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1277
|
+
logger.debug(`[MessageProcessor] Set lastProactiveFlag for session ${session.id}`);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1215
1280
|
}
|
|
1216
1281
|
continue;
|
|
1217
1282
|
}
|
|
@@ -1231,7 +1296,7 @@ export class MessageProcessor {
|
|
|
1231
1296
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1232
1297
|
}
|
|
1233
1298
|
// 记录完成状态
|
|
1234
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, usage: event.usage };
|
|
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, usage: event.usage };
|
|
1235
1300
|
if (event.subtype === 'success') {
|
|
1236
1301
|
this.messageCache.addEvent(session.id, {
|
|
1237
1302
|
type: 'completed',
|
|
@@ -1344,85 +1409,6 @@ export class MessageProcessor {
|
|
|
1344
1409
|
// 都找不到,返回项目根目录路径
|
|
1345
1410
|
return rootPath;
|
|
1346
1411
|
}
|
|
1347
|
-
/**
|
|
1348
|
-
* 确保全局数据目录下有最新版本的 SKILLS.md
|
|
1349
|
-
* 目标:{EVOLCLAW_HOME}/data/SKILLS.md
|
|
1350
|
-
*/
|
|
1351
|
-
ensureSkillsFile() {
|
|
1352
|
-
try {
|
|
1353
|
-
const targetDir = path.join(resolveRoot(), 'data');
|
|
1354
|
-
const targetPath = path.join(targetDir, 'SKILLS.md');
|
|
1355
|
-
const templatePath = path.join(getPackageRoot(), 'src', 'templates', 'skills.md');
|
|
1356
|
-
// 模板不存在则跳过(构建环境可能没有 src/)
|
|
1357
|
-
if (!fs.existsSync(templatePath)) {
|
|
1358
|
-
// 尝试 dist/templates/skills.md
|
|
1359
|
-
const distTemplatePath = path.join(getPackageRoot(), 'dist', 'templates', 'skills.md');
|
|
1360
|
-
if (!fs.existsSync(distTemplatePath))
|
|
1361
|
-
return;
|
|
1362
|
-
this.copySkillsIfNeeded(distTemplatePath, targetDir, targetPath);
|
|
1363
|
-
return;
|
|
1364
|
-
}
|
|
1365
|
-
this.copySkillsIfNeeded(templatePath, targetDir, targetPath);
|
|
1366
|
-
}
|
|
1367
|
-
catch {
|
|
1368
|
-
// 静默失败,不影响正常消息处理
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
copySkillsIfNeeded(templatePath, targetDir, targetPath) {
|
|
1372
|
-
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
1373
|
-
const templateVersion = templateContent.match(/^version:\s*(.+)$/m)?.[1]?.trim() || '0';
|
|
1374
|
-
if (fs.existsSync(targetPath)) {
|
|
1375
|
-
const existing = fs.readFileSync(targetPath, 'utf-8');
|
|
1376
|
-
const existingVersion = existing.match(/^version:\s*(.+)$/m)?.[1]?.trim() || '0';
|
|
1377
|
-
if (this.compareSemver(existingVersion, templateVersion) >= 0)
|
|
1378
|
-
return; // 已是最新
|
|
1379
|
-
}
|
|
1380
|
-
if (!fs.existsSync(targetDir)) {
|
|
1381
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
1382
|
-
}
|
|
1383
|
-
fs.writeFileSync(targetPath, templateContent, 'utf-8');
|
|
1384
|
-
}
|
|
1385
|
-
/** 简易 semver 比较:支持 "1", "1.0", "1.0.0" 等格式,返回 -1/0/1 */
|
|
1386
|
-
compareSemver(a, b) {
|
|
1387
|
-
const pa = a.split('.').map(Number);
|
|
1388
|
-
const pb = b.split('.').map(Number);
|
|
1389
|
-
const len = Math.max(pa.length, pb.length);
|
|
1390
|
-
for (let i = 0; i < len; i++) {
|
|
1391
|
-
const na = pa[i] || 0;
|
|
1392
|
-
const nb = pb[i] || 0;
|
|
1393
|
-
if (na !== nb)
|
|
1394
|
-
return na > nb ? 1 : -1;
|
|
1395
|
-
}
|
|
1396
|
-
return 0;
|
|
1397
|
-
}
|
|
1398
|
-
/**
|
|
1399
|
-
* 从 data/SKILLS.md 读取 frontmatter 并生成提示。
|
|
1400
|
-
* 不缓存:每次读取保证用户编辑立即生效。
|
|
1401
|
-
* 调用前应确保 ensureSkillsFile() 已执行过(首次落盘)。
|
|
1402
|
-
*/
|
|
1403
|
-
getSkillsHint() {
|
|
1404
|
-
try {
|
|
1405
|
-
const skillsPath = path.join(resolveRoot(), 'data', 'SKILLS.md');
|
|
1406
|
-
if (!fs.existsSync(skillsPath))
|
|
1407
|
-
return null;
|
|
1408
|
-
const content = fs.readFileSync(skillsPath, 'utf-8');
|
|
1409
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1410
|
-
if (!frontmatterMatch)
|
|
1411
|
-
return null;
|
|
1412
|
-
const fm = frontmatterMatch[1];
|
|
1413
|
-
const desc = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() || 'EvolClaw 运行时管理指令';
|
|
1414
|
-
const trigger = fm.match(/^trigger:\s*(.+)$/m)?.[1]?.trim() || '';
|
|
1415
|
-
const parts = [
|
|
1416
|
-
`可通过 Bash 指令管理运行时,${desc}。`,
|
|
1417
|
-
trigger ? `触发时机:${trigger}。` : '',
|
|
1418
|
-
`完整文档见 ${skillsPath}`,
|
|
1419
|
-
];
|
|
1420
|
-
return parts.filter(Boolean).join('');
|
|
1421
|
-
}
|
|
1422
|
-
catch {
|
|
1423
|
-
return null;
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
1412
|
/**
|
|
1427
1413
|
* 判断文件路径是否为占位符/示例文本
|
|
1428
1414
|
* 用于过滤大模型在说明文字中误写的 [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;
|