evolclaw 3.1.11 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +27 -2
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -27
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1069 -141
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +28 -0
- package/dist/aun/aid/control-aid.js +67 -0
- package/dist/aun/aid/identity.js +20 -7
- package/dist/aun/aid/store.js +2 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +538 -325
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +98 -151
- package/dist/channels/qqbot.js +75 -138
- package/dist/channels/wechat.js +75 -136
- package/dist/channels/wecom.js +75 -138
- package/dist/cli/agent.js +44 -13
- package/dist/cli/index.js +207 -46
- package/dist/cli/init-channel.js +38 -148
- package/dist/cli/init.js +192 -85
- package/dist/cli/model.js +1 -1
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +48 -11
- package/dist/core/channel-loader.js +84 -82
- package/dist/core/command-handler.js +754 -172
- package/dist/core/daemon-file-cache.js +216 -0
- package/dist/core/evolagent-registry.js +4 -0
- package/dist/core/evolagent.js +28 -23
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +215 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/im-renderer.js +35 -13
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +52 -22
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +336 -68
- package/dist/core/message/message-queue.js +15 -8
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +56 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +40 -7
- package/dist/core/permission.js +9 -12
- package/dist/core/relation/peer-identity.js +16 -1
- package/dist/core/session/adapters/claude-session-file-adapter.js +48 -5
- package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
- package/dist/core/session/session-manager.js +27 -13
- package/dist/core/session/session-title.js +26 -0
- package/dist/core/stats/billing.js +151 -0
- package/dist/core/stats/budget.js +93 -0
- package/dist/core/stats/db.js +314 -0
- package/dist/core/stats/eck-vars.js +84 -0
- package/dist/core/stats/index.js +10 -0
- package/dist/core/stats/normalizer.js +78 -0
- package/dist/core/stats/query.js +760 -0
- package/dist/core/stats/writer.js +115 -0
- package/dist/core/trigger/manager.js +34 -0
- package/dist/core/trigger/parser.js +9 -3
- package/dist/core/trigger/scheduler.js +20 -17
- package/dist/{agents → eck}/kit-renderer.js +5 -1
- package/dist/{agents → eck}/manifest-engine.js +127 -35
- package/dist/{agents → eck}/message-renderer.js +26 -1
- package/dist/index.js +185 -8
- package/dist/ipc.js +22 -0
- package/dist/paths.js +7 -3
- package/dist/utils/cross-platform.js +23 -5
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/stats.js +14 -0
- package/kits/docs/evolclaw/INDEX.md +3 -1
- package/kits/docs/evolclaw/fs-architecture.md +1215 -0
- package/kits/docs/evolclaw/fs.md +131 -0
- package/kits/docs/evolclaw/group-fs.md +209 -0
- package/kits/docs/evolclaw/stats.md +70 -0
- package/kits/docs/venues/aun-group.md +29 -6
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/message-fragments/item.md +1 -1
- package/kits/templates/system-fragments/response-depth.md +16 -0
- package/package.json +4 -4
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/relation/peer-key.js +0 -16
- package/dist/utils/channel-helpers.js +0 -46
|
@@ -2,20 +2,25 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
|
-
import { hasCompact } from '../../agents/
|
|
5
|
+
import { hasCompact } from '../../agents/runner-types.js';
|
|
6
6
|
import { IMRenderer } from './im-renderer.js';
|
|
7
7
|
import { StreamIdleMonitor } from './stream-idle-monitor.js';
|
|
8
8
|
import { logger } from '../../utils/logger.js';
|
|
9
9
|
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, prefixErrorType, isRetryableError } from '../../utils/error-utils.js';
|
|
10
10
|
import { summarizeToolInput } from '../permission.js';
|
|
11
11
|
import { DEFAULT_PERMISSION_MODE } from '../../types.js';
|
|
12
|
-
import { getPackageRoot, resolveRoot } from '../../paths.js';
|
|
13
|
-
import { renderKitSections } from '../../
|
|
14
|
-
import { renderMessageBody } from '../../
|
|
15
|
-
import {
|
|
12
|
+
import { getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
13
|
+
import { renderKitSections } from '../../eck/kit-renderer.js';
|
|
14
|
+
import { renderMessageBody } from '../../eck/message-renderer.js';
|
|
15
|
+
import { consumeHints, hintsToSubMessages, composeHintFallback } from './pending-hints.js';
|
|
16
|
+
import { resolveResponseDepth as computeResponseDepth } from './response-depth.js';
|
|
17
|
+
import { normalizeBaseagent } from '../../agents/baseagent.js';
|
|
16
18
|
import { renderActionAsText, renderCommandCardAsText } from '../interaction-router.js';
|
|
17
|
-
import { formatPeerKey } from '../relation/peer-
|
|
19
|
+
import { formatPeerKey } from '../relation/peer-identity.js';
|
|
18
20
|
import { resolveEffectiveModel } from '../model/model-scope.js';
|
|
21
|
+
import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../stats/writer.js';
|
|
22
|
+
import { normalizeUsage } from '../stats/normalizer.js';
|
|
23
|
+
import { getBudgetStatus } from '../stats/budget.js';
|
|
19
24
|
/** OS 信息在进程生命周期内是常量,模块加载时算一次。例: "Windows 11 Pro (win32 10.0.26200)" */
|
|
20
25
|
const OS_INFO = (() => {
|
|
21
26
|
let label = '';
|
|
@@ -192,6 +197,34 @@ export class MessageProcessor {
|
|
|
192
197
|
const globalCm = agent.config?.chatmode ?? this.globalSettings.chatmode;
|
|
193
198
|
return agent.getContext(channelName, chatType, globalCm);
|
|
194
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* 观察者插话(v0.3):消费当前 (对端, thread) 的待用提示,转成 owner-hint SubMessage。
|
|
202
|
+
* 一次性语义:consumeHints 回放算有效集后清该 thread(其它 thread 残留则保留,否则删文件)。
|
|
203
|
+
* 仅 aun 渠道(pending-hints 落在 sessions/aun/<self>/<对端>/)。
|
|
204
|
+
*/
|
|
205
|
+
consumeOwnerHints(session, message) {
|
|
206
|
+
const channelType = session.channelType || message.channelType || session.channel;
|
|
207
|
+
if (channelType !== 'aun')
|
|
208
|
+
return [];
|
|
209
|
+
const selfAID = session.selfAID || message.selfAID;
|
|
210
|
+
if (!selfAID)
|
|
211
|
+
return [];
|
|
212
|
+
// 会话定位键:私聊=对端 AID,群聊=groupId(均为 session.channelId)。
|
|
213
|
+
const peerChannelId = session.channelId;
|
|
214
|
+
if (!peerChannelId)
|
|
215
|
+
return [];
|
|
216
|
+
try {
|
|
217
|
+
const hints = consumeHints(resolvePaths().sessionsDir, 'aun', peerChannelId, selfAID, session.threadId);
|
|
218
|
+
if (hints.length === 0)
|
|
219
|
+
return [];
|
|
220
|
+
logger.info(`[MessageProcessor] consumed ${hints.length} owner-hint(s) for ${peerChannelId} thread=${session.threadId || 'main'}`);
|
|
221
|
+
return hintsToSubMessages(hints);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
logger.warn(`[MessageProcessor] consumeOwnerHints failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
195
228
|
/**
|
|
196
229
|
* 注册渠道适配器
|
|
197
230
|
*/
|
|
@@ -262,6 +295,8 @@ export class MessageProcessor {
|
|
|
262
295
|
// 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
|
|
263
296
|
// message.channel 现在存实例名(channelName),可直接用于精确路由
|
|
264
297
|
const { session, absoluteProjectPath } = await this.resolveSession(message);
|
|
298
|
+
// 群聊响应深度决策(resolveSession 之后、_processMessageInternal 之前)
|
|
299
|
+
const responseDepth = await this.resolveResponseDepth(message, session);
|
|
265
300
|
// thread(feishu) pending strategy: inject replyContext so first reply creates the thread
|
|
266
301
|
if (message.triggerMeta?.pendingThread && message.triggerMeta?.rootMessageId) {
|
|
267
302
|
const triggerId = message.triggerMeta.triggerId;
|
|
@@ -295,16 +330,9 @@ export class MessageProcessor {
|
|
|
295
330
|
const streamKey = session.id;
|
|
296
331
|
const chatType = message.chatType || 'private';
|
|
297
332
|
const identityRole = session.identity?.role || 'anonymous';
|
|
298
|
-
const agentNameForMonitor = this.agentRegistry?.resolveByChannel(channelKey)?.name ?? '<unknown>';
|
|
299
|
-
// Resolve agent context from registry (Phase 2 foundation)
|
|
300
|
-
const agentContext = this.getAgentContext(channelKey, chatType);
|
|
301
|
-
if (agentContext) {
|
|
302
|
-
logger.debug(`[MessageProcessor] Agent context resolved: ${agentContext.name} (${agentContext.baseagent})`);
|
|
303
|
-
}
|
|
304
|
-
// 按 session.agentId 选择 agent 后端
|
|
305
|
-
const agent = this.getAgent(channelKey, session.agentId);
|
|
306
333
|
const monitorEnabled = this.globalSettings.idleMonitor?.enabled !== false;
|
|
307
|
-
|
|
334
|
+
// 按 session.agentId 选择 agent 后端(idle-kill 路径需要 interrupt)
|
|
335
|
+
const agent = this.getAgent(channelKey, session.agentId);
|
|
308
336
|
// 计算是否抑制中间输出(工具活动 + 流式文本)
|
|
309
337
|
const shouldSuppress = () => {
|
|
310
338
|
return !policy.showMiddleResult(chatType, identityRole);
|
|
@@ -313,6 +341,7 @@ export class MessageProcessor {
|
|
|
313
341
|
let monitor;
|
|
314
342
|
let monitorInterval;
|
|
315
343
|
let rejectFn;
|
|
344
|
+
let lastIdleSec = 0;
|
|
316
345
|
const resetTimer = (eventType, toolName) => {
|
|
317
346
|
monitor?.recordEvent(eventType || 'unknown', toolName);
|
|
318
347
|
};
|
|
@@ -329,17 +358,9 @@ export class MessageProcessor {
|
|
|
329
358
|
let result = monitor.check();
|
|
330
359
|
while (result) {
|
|
331
360
|
if (result.action === 'kill') {
|
|
361
|
+
lastIdleSec = result.idleSec;
|
|
332
362
|
logger.warn(`[MessageProcessor] Idle monitor: kill after ${result.idleSec}s idle, stream: ${streamKey}`);
|
|
333
363
|
this.eventBus.publish({ type: 'runner:idle-timeout', sessionId: streamKey, idleSec: result.idleSec });
|
|
334
|
-
// 后台任务也需要中断(释放资源),但不发送通知
|
|
335
|
-
if (channelInfo && !isBackground) {
|
|
336
|
-
const msg = showIdleMonitor
|
|
337
|
-
? result.message
|
|
338
|
-
: `\u26a0\ufe0f 任务超时(${result.idleSec}秒无响应),已自动中断`;
|
|
339
|
-
channelInfo.adapter.send(buildEnvelope({ channel: channelInfo.adapter.channelName, channelId: message.channelId, agentName: agentNameForMonitor }), { kind: 'system.notice', text: msg, subtype: 'health' }).catch(e => {
|
|
340
|
-
logger.debug(`[MessageProcessor] Failed to send kill diagnostic message:`, e);
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
364
|
logger.info(`[MessageProcessor] agent.interrupt invoked (idle-kill) stream=${streamKey}`);
|
|
344
365
|
agent.interrupt(streamKey).catch(e => {
|
|
345
366
|
logger.debug(`[MessageProcessor] Interrupt failed (may already be cleaned up):`, e);
|
|
@@ -348,15 +369,16 @@ export class MessageProcessor {
|
|
|
348
369
|
return;
|
|
349
370
|
}
|
|
350
371
|
else {
|
|
351
|
-
// notify or warn:
|
|
372
|
+
// notify or warn: publish event, task continues
|
|
352
373
|
logger.info(`[MessageProcessor] Idle monitor: ${result.action} after ${result.idleSec}s idle, stream: ${streamKey}`);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
374
|
+
this.eventBus.publish({
|
|
375
|
+
type: result.action === 'notify' ? 'runner:idle-notify' : 'runner:idle-warn',
|
|
376
|
+
sessionId: streamKey,
|
|
377
|
+
idleSec: result.idleSec,
|
|
378
|
+
totalEvents: result.state.totalEvents,
|
|
379
|
+
totalToolCalls: result.state.totalToolCalls,
|
|
380
|
+
lastToolName: result.state.lastToolName,
|
|
381
|
+
});
|
|
360
382
|
}
|
|
361
383
|
result = monitor.check();
|
|
362
384
|
}
|
|
@@ -364,7 +386,7 @@ export class MessageProcessor {
|
|
|
364
386
|
});
|
|
365
387
|
try {
|
|
366
388
|
await Promise.race([
|
|
367
|
-
this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress),
|
|
389
|
+
this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, () => lastIdleSec, responseDepth),
|
|
368
390
|
timeoutPromise
|
|
369
391
|
]);
|
|
370
392
|
}
|
|
@@ -412,7 +434,7 @@ export class MessageProcessor {
|
|
|
412
434
|
return message.replyContext;
|
|
413
435
|
}
|
|
414
436
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
415
|
-
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress) {
|
|
437
|
+
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, responseDepth) {
|
|
416
438
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
417
439
|
const channelKey = session.metadata?.channelKey || message.channel;
|
|
418
440
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
@@ -436,7 +458,7 @@ export class MessageProcessor {
|
|
|
436
458
|
const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
|
437
459
|
const chatmode = session.sessionMode ?? 'interactive';
|
|
438
460
|
// 诊断日志:记录 inbound message_id 和生成的 task_id 的对应关系
|
|
439
|
-
logger.info(`[MessageProcessor] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode}`);
|
|
461
|
+
logger.info(`[MessageProcessor] Task created: inboundMsgId=${message.messageId ?? 'none'} taskId=${taskId} sessionId=${session.id} chatmode=${chatmode}${responseDepth && responseDepth !== 'standard' ? ` depth=${responseDepth}` : ''}`);
|
|
440
462
|
// 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
|
|
441
463
|
const taskReplyContext = () => {
|
|
442
464
|
const base = this.getReplyContext(message);
|
|
@@ -474,6 +496,17 @@ export class MessageProcessor {
|
|
|
474
496
|
agentName: agentNameForStats,
|
|
475
497
|
timestamp: Date.now()
|
|
476
498
|
});
|
|
499
|
+
// ── 硬上限检查:超限直接返回提示,不调模型 ──
|
|
500
|
+
{
|
|
501
|
+
const budgetAgentAid = session.selfAID || message.selfAID || '';
|
|
502
|
+
const budgetPeerKey = formatPeerKey(message.channel, message.channelId);
|
|
503
|
+
const budgetStatus = getBudgetStatus(resolveRoot(), budgetAgentAid, budgetPeerKey);
|
|
504
|
+
if (budgetStatus.hard_blocked) {
|
|
505
|
+
logger.warn(`[MessageProcessor] Budget hard limit reached: agent=${budgetAgentAid} peer=${budgetPeerKey} pct=${budgetStatus.pct_used.toFixed(1)}%`);
|
|
506
|
+
adapter.send(envelope, { kind: 'status.completed', metadata: { durationMs: 0 } }).catch(() => { });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
477
510
|
const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
|
|
478
511
|
const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
|
|
479
512
|
const e2eeInfo = message.replyContext?.metadata?.encrypted != null ? ` encrypt=${message.replyContext.metadata.encrypted}` : '';
|
|
@@ -559,6 +592,9 @@ export class MessageProcessor {
|
|
|
559
592
|
agentName: agentNameForStats,
|
|
560
593
|
taskId,
|
|
561
594
|
chatmode: isProactive ? 'proactive' : 'interactive',
|
|
595
|
+
flushPending: async () => {
|
|
596
|
+
await renderer.flush(false);
|
|
597
|
+
},
|
|
562
598
|
interceptNextMessage: this.messageQueue
|
|
563
599
|
? (sessionKey, handler) => this.messageQueue.interceptNext(sessionKey, handler)
|
|
564
600
|
: undefined,
|
|
@@ -595,7 +631,8 @@ export class MessageProcessor {
|
|
|
595
631
|
const currentChannelType = options?.channelType || message.channel;
|
|
596
632
|
// 提取 self 信息
|
|
597
633
|
const adapterAny = channelInfo.adapter;
|
|
598
|
-
const
|
|
634
|
+
const adapterSelfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
635
|
+
const selfAid = adapterSelfAid || message.selfAID || session.selfAID || undefined;
|
|
599
636
|
const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
|
|
600
637
|
const peerName = message.peerName || session.metadata?.peerName;
|
|
601
638
|
// 通道能力
|
|
@@ -619,6 +656,7 @@ export class MessageProcessor {
|
|
|
619
656
|
const peerKey = (currentChannelType && peerIdRaw)
|
|
620
657
|
? formatPeerKey(currentChannelType, peerIdRaw)
|
|
621
658
|
: undefined;
|
|
659
|
+
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
622
660
|
// 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
|
|
623
661
|
// 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
|
|
624
662
|
// 多对端并发各自独立解析、各自传参,无共享状态可被污染。
|
|
@@ -638,7 +676,7 @@ export class MessageProcessor {
|
|
|
638
676
|
let evolclawModelOverride;
|
|
639
677
|
if (!skipEvolclawModel) {
|
|
640
678
|
try {
|
|
641
|
-
const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
|
|
679
|
+
const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey }, normalizedBaseagent.canonical);
|
|
642
680
|
if (resolved.model) {
|
|
643
681
|
evolclawModelOverride = { model: resolved.model, effort: resolved.effort };
|
|
644
682
|
effectiveModel = resolved.model;
|
|
@@ -649,7 +687,20 @@ export class MessageProcessor {
|
|
|
649
687
|
}
|
|
650
688
|
modelOverride = evolclawModelOverride;
|
|
651
689
|
}
|
|
652
|
-
|
|
690
|
+
// ④ 群聊 responseDepth → effort 动态映射
|
|
691
|
+
// 仅当群聊且 evolclaw 作用域未显式指定 effort 时生效(显式配置优先)
|
|
692
|
+
if (message.chatType === 'group' && responseDepth && !(modelOverride?.effort)) {
|
|
693
|
+
const depthEffortMap = {
|
|
694
|
+
lightweight: 'low',
|
|
695
|
+
standard: 'medium',
|
|
696
|
+
deep: 'high',
|
|
697
|
+
};
|
|
698
|
+
const mappedEffort = depthEffortMap[responseDepth];
|
|
699
|
+
if (mappedEffort) {
|
|
700
|
+
modelOverride = { ...(modelOverride || {}), effort: mappedEffort };
|
|
701
|
+
logger.info(`[MessageProcessor] Group depth→effort: ${responseDepth} → ${mappedEffort} session=${session.id}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
653
704
|
agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
654
705
|
// Kit renderer: 组装上下文
|
|
655
706
|
const pkgRoot = getPackageRoot();
|
|
@@ -684,11 +735,18 @@ export class MessageProcessor {
|
|
|
684
735
|
sameNetwork: message.sameNetwork ?? false,
|
|
685
736
|
sameEgressIp: message.sameEgressIp ?? false,
|
|
686
737
|
groupId: session.metadata?.groupId || undefined,
|
|
738
|
+
groupName: session.metadata?.groupName || undefined,
|
|
739
|
+
// 信封展示用:有群名则「名<ID>」,否则纯 ID。规避模板引擎无 not/else 的限制。
|
|
740
|
+
groupLabel: session.metadata?.groupId
|
|
741
|
+
? (session.metadata?.groupName ? `${session.metadata.groupName}<${session.metadata.groupId}>` : session.metadata.groupId)
|
|
742
|
+
: undefined,
|
|
687
743
|
chatType: session.chatType || null,
|
|
688
744
|
channel: currentChannelType || null,
|
|
689
745
|
venueUid: undefined,
|
|
690
746
|
// 群分发模式 / 客户端类型 / 权限模式
|
|
691
|
-
|
|
747
|
+
// 优先本地 session 覆盖(/dispatch 命令),fallback 到服务器 dispatch_mode
|
|
748
|
+
dispatch: session.metadata?.dispatchMode || message.dispatchMode || undefined,
|
|
749
|
+
responseDepth: responseDepth || undefined,
|
|
692
750
|
clientType: message.clientType || undefined,
|
|
693
751
|
permissionMode: session.metadata?.permissionMode || 'auto',
|
|
694
752
|
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
@@ -714,6 +772,8 @@ export class MessageProcessor {
|
|
|
714
772
|
modelFallbackActive: (fbState.fallbackActive || skipEvolclawModel) ? true : undefined,
|
|
715
773
|
modelFallbackModel: (fbState.fallbackActive || skipEvolclawModel) ? (agentModel || undefined) : undefined,
|
|
716
774
|
agentSessionId: session.agentSessionId || undefined,
|
|
775
|
+
// 渲染模式:各类型当前激活的 modeName(从内存 config 读,渲染层据此选 manifest section)。
|
|
776
|
+
renderModes: this.agentRegistry?.resolveByChannel(channelKey)?.config?.render ?? undefined,
|
|
717
777
|
},
|
|
718
778
|
sessionId: session.id,
|
|
719
779
|
};
|
|
@@ -721,30 +781,65 @@ export class MessageProcessor {
|
|
|
721
781
|
if (kitContext)
|
|
722
782
|
contextParts.push(kitContext);
|
|
723
783
|
effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
|
|
784
|
+
// ── Stats: context_breakdown 旁路采集(各段估算 token 数,字符数/4 近似) ──
|
|
785
|
+
try {
|
|
786
|
+
const estTokens = (s) => s ? Math.ceil(s.length / 4) : 0;
|
|
787
|
+
const cbModel = effectiveModel || agentModel || 'unknown';
|
|
788
|
+
const cbMaxTokens = 200000; // 保守默认,后续可从 model-catalog 取
|
|
789
|
+
const systemPromptTokens = estTokens(options?.systemPromptAppend);
|
|
790
|
+
const personaTokens = estTokens(persona);
|
|
791
|
+
const workingTokens = estTokens(working);
|
|
792
|
+
const kitTokens = estTokens(kitContext);
|
|
793
|
+
const totalEst = estTokens(effectiveSystemPrompt);
|
|
794
|
+
insertContextBreakdown(resolveRoot(), {
|
|
795
|
+
ts: Date.now(),
|
|
796
|
+
agent_aid: selfAid || session.selfAID || '',
|
|
797
|
+
session_id: session.id,
|
|
798
|
+
turn_count: 0, // 按 ts 排序得轮次
|
|
799
|
+
model: cbModel,
|
|
800
|
+
max_tokens: cbMaxTokens,
|
|
801
|
+
system_prompt: systemPromptTokens + personaTokens + workingTokens,
|
|
802
|
+
system_tools: 0, // 工具 schema 不在此层,留 0(后续 runner 层补)
|
|
803
|
+
mcp_tools: 0,
|
|
804
|
+
custom_agents: 0,
|
|
805
|
+
memory_files: kitTokens, // ECK 渲染的所有段(含 memory/skills/rules)
|
|
806
|
+
skills: 0,
|
|
807
|
+
messages: 0, // messages 段在 runner 层才知道
|
|
808
|
+
free_space: Math.max(0, cbMaxTokens - totalEst),
|
|
809
|
+
total_estimated: totalEst,
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
catch { /* non-fatal */ }
|
|
724
813
|
// 消息渲染层:用 message manifest 逐条渲染(时间 + 群聊发送者),组装成最终正文。
|
|
725
814
|
// 单条消息构造单元素 items;批量合并的消息 message.items 已由队列填充。
|
|
726
815
|
let renderResult;
|
|
727
816
|
const hasContent = message.content.trim() || (message.items && message.items.length > 0);
|
|
728
817
|
if (hasContent) {
|
|
818
|
+
const peerItems = message.items && message.items.length > 0
|
|
819
|
+
? message.items
|
|
820
|
+
: [{
|
|
821
|
+
peerId: message.peerId, peerName: peerName || undefined,
|
|
822
|
+
peerType: message.peerType,
|
|
823
|
+
sameDevice: message.sameDevice, sameNetwork: message.sameNetwork, sameEgressIp: message.sameEgressIp,
|
|
824
|
+
content: message.content, timestamp: message.timestamp,
|
|
825
|
+
images: message.images,
|
|
826
|
+
mentionAids: message.mentionAids,
|
|
827
|
+
}];
|
|
828
|
+
// 观察者插话(v0.3):消费 (对端, thread) 的待用提示,包成 owner-hint item 排在对端消息前。
|
|
829
|
+
// 一次性语义:consumeOwnerHints 读取并删除(见 pending-hints.ts)。在 try 外消费,
|
|
830
|
+
// 这样即便 renderMessageBody 抛错走 raw 兜底,也把提示原文拼进去——绝不静默丢提示。
|
|
831
|
+
const hintItems = this.consumeOwnerHints(session, message);
|
|
832
|
+
const renderItems = hintItems.length > 0 ? [...hintItems, ...peerItems] : peerItems;
|
|
729
833
|
try {
|
|
730
|
-
const renderItems = message.items && message.items.length > 0
|
|
731
|
-
? message.items
|
|
732
|
-
: [{
|
|
733
|
-
peerId: message.peerId, peerName: peerName || undefined,
|
|
734
|
-
peerType: message.peerType,
|
|
735
|
-
sameDevice: message.sameDevice, sameNetwork: message.sameNetwork, sameEgressIp: message.sameEgressIp,
|
|
736
|
-
content: message.content, timestamp: message.timestamp,
|
|
737
|
-
images: message.images,
|
|
738
|
-
}];
|
|
739
834
|
renderResult = renderMessageBody(renderItems, kitCtx.vars, session.id);
|
|
740
835
|
if (renderResult.body.trim())
|
|
741
836
|
effectivePrompt = wrapPrompt(renderResult.body);
|
|
742
837
|
else
|
|
743
|
-
effectivePrompt = wrapPrompt(message.content);
|
|
838
|
+
effectivePrompt = wrapPrompt(composeHintFallback(hintItems, message.content));
|
|
744
839
|
}
|
|
745
840
|
catch (e) {
|
|
746
841
|
logger.warn(`[MessageProcessor] renderMessageBody failed, using raw content: ${e instanceof Error ? e.message : String(e)}`);
|
|
747
|
-
effectivePrompt = wrapPrompt(message.content);
|
|
842
|
+
effectivePrompt = wrapPrompt(composeHintFallback(hintItems, message.content));
|
|
748
843
|
}
|
|
749
844
|
}
|
|
750
845
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
@@ -995,7 +1090,7 @@ export class MessageProcessor {
|
|
|
995
1090
|
adapter.send(envelope, { kind: 'status.error', metadata: { errorType: rawSubtype } }).catch(() => { });
|
|
996
1091
|
}
|
|
997
1092
|
if (message.triggerMeta) {
|
|
998
|
-
this.eventBus.publish({ type: 'trigger:failed', triggerId: message.triggerMeta.triggerId, messageId: messageId, error: errorSummary });
|
|
1093
|
+
this.eventBus.publish({ type: 'trigger:failed', triggerId: message.triggerMeta.triggerId, name: message.triggerMeta.triggerName ?? '', messageId: messageId, error: errorSummary, targetChannel: message.channel, targetChannelId: message.channelId, fireTime: message.triggerMeta.fireTime ?? 0, phase: 'execute' });
|
|
999
1094
|
}
|
|
1000
1095
|
this.eventBus.publish({
|
|
1001
1096
|
type: 'task:error',
|
|
@@ -1026,20 +1121,131 @@ export class MessageProcessor {
|
|
|
1026
1121
|
else {
|
|
1027
1122
|
// 真正的成功
|
|
1028
1123
|
const durationMs = Date.now() - startTime;
|
|
1124
|
+
// ── Stats: 写入 usage_events(在 status.completed 之前,以便带上 cost) ──
|
|
1125
|
+
let statsCostUsd = 0;
|
|
1126
|
+
let statsCostCny = 0;
|
|
1127
|
+
let statsCacheHitRate = 0;
|
|
1128
|
+
if (streamResult.tokenUsage) {
|
|
1129
|
+
try {
|
|
1130
|
+
const statsAgentAid = session.selfAID || message.selfAID || '';
|
|
1131
|
+
const statsPeerKey = formatPeerKey(message.channel, message.channelId);
|
|
1132
|
+
const statsModel = streamResult.contextUsage?.model || 'unknown';
|
|
1133
|
+
const ctxPct = streamResult.contextUsage?.percentage;
|
|
1134
|
+
const event = normalizeUsage(streamResult.tokenUsage, {
|
|
1135
|
+
ts: Date.now(),
|
|
1136
|
+
agent_aid: statsAgentAid,
|
|
1137
|
+
peer_key: statsPeerKey,
|
|
1138
|
+
peer_type: session.chatType || undefined,
|
|
1139
|
+
session_id: session.id,
|
|
1140
|
+
model: statsModel,
|
|
1141
|
+
turns: streamResult.numTurns,
|
|
1142
|
+
duration_ms: durationMs,
|
|
1143
|
+
context_window_pct: ctxPct,
|
|
1144
|
+
});
|
|
1145
|
+
insertUsageEvent(resolveRoot(), event);
|
|
1146
|
+
// 逐次大模型调用明细落库(model_calls 表)
|
|
1147
|
+
if (streamResult.modelCalls?.length) {
|
|
1148
|
+
const mcRows = streamResult.modelCalls.map(mc => ({
|
|
1149
|
+
ts: event.ts,
|
|
1150
|
+
task_id: taskId,
|
|
1151
|
+
session_id: session.id,
|
|
1152
|
+
agent_session_id: session.agentSessionId ?? undefined,
|
|
1153
|
+
agent_aid: statsAgentAid,
|
|
1154
|
+
peer_key: statsPeerKey,
|
|
1155
|
+
call_index: mc.call_index,
|
|
1156
|
+
model: mc.model || statsModel,
|
|
1157
|
+
request_id: mc.request_id,
|
|
1158
|
+
message_id: mc.message_id,
|
|
1159
|
+
input_tokens: mc.tokenUsage.input_tokens ?? 0,
|
|
1160
|
+
output_tokens: mc.tokenUsage.output_tokens ?? 0,
|
|
1161
|
+
cache_creation_tokens: mc.tokenUsage.cache_creation_input_tokens ?? 0,
|
|
1162
|
+
cache_read_tokens: mc.tokenUsage.cache_read_input_tokens ?? 0,
|
|
1163
|
+
degraded: mc.degraded ? 1 : 0,
|
|
1164
|
+
}));
|
|
1165
|
+
insertModelCalls(resolveRoot(), mcRows);
|
|
1166
|
+
}
|
|
1167
|
+
// 计算费用(用于合入 status.completed)
|
|
1168
|
+
const { calcCost } = await import('../stats/billing.js');
|
|
1169
|
+
const cost = calcCost(resolveRoot(), { ...event, ts: event.ts, model: event.model, billing_fn: event.billing_fn });
|
|
1170
|
+
statsCostUsd = cost.usd ?? 0;
|
|
1171
|
+
statsCostCny = cost.cny ?? 0;
|
|
1172
|
+
const totalIn = event.input_tokens + event.cache_read_tokens;
|
|
1173
|
+
statsCacheHitRate = totalIn > 0 ? Math.round((event.cache_read_tokens / totalIn) * 100) / 100 : 0;
|
|
1174
|
+
}
|
|
1175
|
+
catch (e) {
|
|
1176
|
+
logger.debug(`[MessageProcessor] Stats write failed (non-fatal): ${e}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
// 会话累计 + model spec(用于 status.completed 统计细目)
|
|
1180
|
+
let sessionStats;
|
|
1181
|
+
let modelSpec;
|
|
1182
|
+
try {
|
|
1183
|
+
const { openReadonlyDb, getDbPath } = await import('../stats/db.js');
|
|
1184
|
+
const { resolveModelSpec } = await import('../stats/billing.js');
|
|
1185
|
+
const statsModel = streamResult.contextUsage?.model || 'unknown';
|
|
1186
|
+
modelSpec = resolveModelSpec(resolveRoot(), statsModel);
|
|
1187
|
+
const rdb = openReadonlyDb(getDbPath(resolveRoot()));
|
|
1188
|
+
if (rdb) {
|
|
1189
|
+
try {
|
|
1190
|
+
const row = rdb.prepare(`SELECT COALESCE(SUM(input_tokens),0) AS input_tokens, COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
1191
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens, COALESCE(SUM(cache_creation_tokens),0) AS cache_creation_tokens,
|
|
1192
|
+
COUNT(*) AS call_count FROM usage_events WHERE session_id = ?`).get(session.id);
|
|
1193
|
+
if (row) {
|
|
1194
|
+
// 逐行算费用太贵,用近似:最后一轮的 cost 乘以次数不准,所以这里用累加 token 近似
|
|
1195
|
+
sessionStats = {
|
|
1196
|
+
input_tokens: row.input_tokens,
|
|
1197
|
+
output_tokens: row.output_tokens,
|
|
1198
|
+
cache_read_tokens: row.cache_read_tokens,
|
|
1199
|
+
cache_creation_tokens: row.cache_creation_tokens,
|
|
1200
|
+
cost_usd: 0, cost_cny: 0,
|
|
1201
|
+
call_count: row.call_count,
|
|
1202
|
+
};
|
|
1203
|
+
// 快速费用估算:用会话所有行逐行算
|
|
1204
|
+
const rows = rdb.prepare(`SELECT * FROM usage_events WHERE session_id = ?`).all(session.id);
|
|
1205
|
+
const { calcCost: cc } = await import('../stats/billing.js');
|
|
1206
|
+
for (const r of rows) {
|
|
1207
|
+
const c = cc(resolveRoot(), r);
|
|
1208
|
+
sessionStats.cost_usd += c.usd ?? 0;
|
|
1209
|
+
sessionStats.cost_cny += c.cny ?? 0;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
finally {
|
|
1214
|
+
rdb.close();
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
catch { /* non-fatal */ }
|
|
1029
1219
|
if (message.source !== 'trigger') {
|
|
1030
1220
|
if (interruptReason) {
|
|
1031
1221
|
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
1032
1222
|
}
|
|
1033
1223
|
else {
|
|
1034
|
-
adapter.send(envelope, { kind: 'status.completed', metadata: {
|
|
1224
|
+
adapter.send(envelope, { kind: 'status.completed', metadata: {
|
|
1225
|
+
durationMs,
|
|
1226
|
+
ttftMs: streamResult.ttftMs,
|
|
1227
|
+
numTurns: streamResult.numTurns,
|
|
1228
|
+
tokenUsage: streamResult.tokenUsage,
|
|
1229
|
+
contextUsage: streamResult.contextUsage,
|
|
1230
|
+
lastModelCall: streamResult.lastModelCall,
|
|
1231
|
+
cost_usd: statsCostUsd,
|
|
1232
|
+
cost_cny: statsCostCny,
|
|
1233
|
+
cache_hit_rate: statsCacheHitRate,
|
|
1234
|
+
model_spec: modelSpec,
|
|
1235
|
+
session_total: sessionStats,
|
|
1236
|
+
queue: {
|
|
1237
|
+
pending: this.messageQueue?.getQueueLength(session.id) ?? 0,
|
|
1238
|
+
processing: this.messageQueue?.isProcessing(session.id) ? 1 : 0,
|
|
1239
|
+
},
|
|
1240
|
+
} }).catch(() => { });
|
|
1035
1241
|
}
|
|
1036
1242
|
}
|
|
1037
1243
|
if (message.triggerMeta) {
|
|
1038
1244
|
if (interruptReason) {
|
|
1039
|
-
this.eventBus.publish({ type: 'trigger:skipped', triggerId: message.triggerMeta.triggerId, reason: 'interrupted' });
|
|
1245
|
+
this.eventBus.publish({ type: 'trigger:skipped', triggerId: message.triggerMeta.triggerId, name: message.triggerMeta.triggerName ?? '', reason: 'interrupted', targetChannel: message.channel, targetChannelId: message.channelId });
|
|
1040
1246
|
}
|
|
1041
1247
|
else {
|
|
1042
|
-
this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, messageId: messageId, durationMs });
|
|
1248
|
+
this.eventBus.publish({ type: 'trigger:completed', triggerId: message.triggerMeta.triggerId, name: message.triggerMeta.triggerName ?? '', messageId: messageId, durationMs, targetChannel: message.channel, targetChannelId: message.channelId, fireTime: message.triggerMeta.fireTime ?? 0 });
|
|
1043
1249
|
}
|
|
1044
1250
|
}
|
|
1045
1251
|
await this.sessionManager.recordSuccess(session.id);
|
|
@@ -1100,7 +1306,7 @@ export class MessageProcessor {
|
|
|
1100
1306
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
|
|
1101
1307
|
if (!isUserInterrupt) {
|
|
1102
1308
|
const statusPayload = procStatus === 'timeout'
|
|
1103
|
-
? { kind: 'status.timeout' }
|
|
1309
|
+
? { kind: 'status.timeout', metadata: { idleSec: getLastIdleSec?.() || undefined } }
|
|
1104
1310
|
: procStatus === 'interrupted'
|
|
1105
1311
|
? { kind: 'status.interrupted', metadata: { reason: 'stream_error' } }
|
|
1106
1312
|
: { kind: 'status.error' };
|
|
@@ -1133,20 +1339,22 @@ export class MessageProcessor {
|
|
|
1133
1339
|
if (error instanceof Error && !isUserInterrupt) {
|
|
1134
1340
|
logger.error(`[${message.channel}] Error stack:`, error.stack);
|
|
1135
1341
|
}
|
|
1136
|
-
//
|
|
1342
|
+
// 发送用户友好的错误消息
|
|
1137
1343
|
// 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送错误提示
|
|
1138
1344
|
// processEventStream 已通过 renderer 发过错误时也跳过
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
}
|
|
1142
|
-
else if (isUserInterrupt) {
|
|
1345
|
+
const isTimeout = error instanceof Error && error.message === 'SDK_TIMEOUT';
|
|
1346
|
+
if (isUserInterrupt) {
|
|
1143
1347
|
logger.info(`[MessageProcessor] User interrupt by new_message, skip sending error message`);
|
|
1144
1348
|
}
|
|
1145
1349
|
else if (error?._errorAlreadySent) {
|
|
1146
1350
|
logger.info(`[MessageProcessor] Error already sent via renderer, skip sending duplicate message`);
|
|
1147
1351
|
}
|
|
1148
1352
|
else {
|
|
1149
|
-
|
|
1353
|
+
// SDK_TIMEOUT:status.timeout 已发结构化状态,此处再补一条用户可见的错误文本(result.error)
|
|
1354
|
+
const idleSec = getLastIdleSec?.() || 0;
|
|
1355
|
+
const userMessage = isTimeout
|
|
1356
|
+
? (idleSec > 0 ? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断` : '⚠️ 任务超时,已自动中断')
|
|
1357
|
+
: getErrorMessage(error, undefined);
|
|
1150
1358
|
// 获取 session 用于话题回复(如果 resolveSession 已执行)
|
|
1151
1359
|
let sendOpts;
|
|
1152
1360
|
try {
|
|
@@ -1159,7 +1367,10 @@ export class MessageProcessor {
|
|
|
1159
1367
|
...(sendOpts ?? {}),
|
|
1160
1368
|
metadata: { ...(sendOpts?.metadata ?? {}), taskId, chatmode },
|
|
1161
1369
|
};
|
|
1162
|
-
|
|
1370
|
+
const errorPayload = isTimeout
|
|
1371
|
+
? { kind: 'result.error', text: userMessage, reason: 'timeout' }
|
|
1372
|
+
: { kind: 'result.text', text: userMessage, isFinal: true };
|
|
1373
|
+
await adapter.send({ ...envelope, replyContext: sendOpts }, errorPayload);
|
|
1163
1374
|
// Proactive 可观测:catch 块的基础设施错误也透传为 thought,保证按 task_id 聚合完整
|
|
1164
1375
|
}
|
|
1165
1376
|
}
|
|
@@ -1168,11 +1379,24 @@ export class MessageProcessor {
|
|
|
1168
1379
|
* 解析会话和项目路径
|
|
1169
1380
|
*/
|
|
1170
1381
|
async resolveSession(message) {
|
|
1171
|
-
//
|
|
1172
|
-
const metadata =
|
|
1173
|
-
? {
|
|
1382
|
+
// 话题会话创建时写入创建者和 replyContext(threadId 路由);主会话不写(避免群聊覆盖)
|
|
1383
|
+
const metadata = message.threadId
|
|
1384
|
+
? {
|
|
1385
|
+
...(message.replyContext ? { replyContext: message.replyContext } : {}),
|
|
1386
|
+
...(message.peerId ? { peerId: message.peerId } : {}),
|
|
1387
|
+
...(message.peerName ? { peerName: message.peerName } : {}),
|
|
1388
|
+
}
|
|
1174
1389
|
: undefined;
|
|
1175
1390
|
const projectPath = this.agentRegistry?.resolveByChannel(message.channel)?.projectPath || process.cwd();
|
|
1391
|
+
if (message.chatType === 'group' && message.threadId && message.source !== 'trigger' && message.source !== 'owner-inject') {
|
|
1392
|
+
const existing = await this.sessionManager.getThreadSession(message.channel, message.channelId, message.threadId);
|
|
1393
|
+
if (!existing) {
|
|
1394
|
+
const role = this.sessionManager.resolveIdentity(message.channel, message.peerId).role;
|
|
1395
|
+
if (role !== 'owner' && role !== 'admin') {
|
|
1396
|
+
throw new Error('群聊中无权限创建话题');
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1176
1400
|
// current strategy: resume bound session, make it active so output is not suppressed
|
|
1177
1401
|
if (message.triggerMeta?.boundSessionId) {
|
|
1178
1402
|
const bound = await this.sessionManager.getSessionById(message.triggerMeta.boundSessionId);
|
|
@@ -1189,13 +1413,31 @@ export class MessageProcessor {
|
|
|
1189
1413
|
logger.warn(`[MessageProcessor] Bound session ${message.triggerMeta.boundSessionId} not found, falling back to latest`);
|
|
1190
1414
|
}
|
|
1191
1415
|
}
|
|
1192
|
-
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata,
|
|
1416
|
+
const session = await this.sessionManager.getOrCreateSession(message.channel, message.channelId, projectPath, message.threadId, metadata, message.topicName, message.peerId, message.chatType, undefined, message.selfAID, message.channelType, message.peerType);
|
|
1193
1417
|
// 兜底纠正1:群聊强制 proactive
|
|
1194
1418
|
if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
|
|
1195
1419
|
logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
|
|
1196
1420
|
session.sessionMode = 'proactive';
|
|
1197
1421
|
await this.sessionManager.updateSession(session.id, { sessionMode: 'proactive' });
|
|
1198
1422
|
}
|
|
1423
|
+
// 群名解析:群会话首次取群显示名(group.get),缓存到 metadata,供信封渲染。
|
|
1424
|
+
// 渠道私有方法 getGroupName 自带进程缓存 + 容错;取不到不阻塞(groupName 保持空,模板回退 groupId)。
|
|
1425
|
+
if (message.chatType === 'group' && session.metadata?.groupId && !session.metadata.groupName) {
|
|
1426
|
+
const adapter = this.resolveChannelInfo(message.channel)?.adapter;
|
|
1427
|
+
const groupName = await adapter?.getGroupName?.(session.metadata.groupId).catch(() => undefined);
|
|
1428
|
+
if (groupName) {
|
|
1429
|
+
session.metadata.groupName = groupName;
|
|
1430
|
+
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
// 群聊分发模式同步:aun.ts 从服务器信封解析的 dispatchMode 注入到 message,
|
|
1434
|
+
// 此处写入 session.metadata,确保 ECK 上下文的 venue fragment 正确渲染 dispatch 变量。
|
|
1435
|
+
// 仅当 message.dispatchMode 有值且与 session 记录不一致时更新。
|
|
1436
|
+
if (message.chatType === 'group' && message.dispatchMode && session.metadata?.dispatchMode !== message.dispatchMode) {
|
|
1437
|
+
logger.info(`[MessageProcessor] dispatchMode sync: sessionId=${session.id} ${session.metadata?.dispatchMode ?? 'none'} -> ${message.dispatchMode}`);
|
|
1438
|
+
session.metadata = { ...(session.metadata || {}), dispatchMode: message.dispatchMode };
|
|
1439
|
+
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1440
|
+
}
|
|
1199
1441
|
// 兜底纠正2:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive。
|
|
1200
1442
|
// 新建场景已由 getOrCreateSession 内部 resolveDefaultSessionMode 处理,这里只兜底历史会话。
|
|
1201
1443
|
if (message.peerType && message.peerType !== 'human' && message.peerType !== 'unknown' && session.sessionMode !== 'proactive') {
|
|
@@ -1216,6 +1458,32 @@ export class MessageProcessor {
|
|
|
1216
1458
|
: path.resolve(process.cwd(), session.projectPath);
|
|
1217
1459
|
return { session, absoluteProjectPath };
|
|
1218
1460
|
}
|
|
1461
|
+
/**
|
|
1462
|
+
* 群聊响应深度决策。根据 dispatch 模式、消息特征、话题轮次综合判断。
|
|
1463
|
+
* 返回 per-message 的瞬时深度枚举,不持久化到 session.metadata。
|
|
1464
|
+
* 同时更新 session.metadata 中的 topicRounds/lastTopicHash(话题追踪状态)。
|
|
1465
|
+
*/
|
|
1466
|
+
async resolveResponseDepth(message, session) {
|
|
1467
|
+
const result = computeResponseDepth({
|
|
1468
|
+
chatType: message.chatType,
|
|
1469
|
+
content: message.content,
|
|
1470
|
+
selfAid: session.selfAID || message.selfAID,
|
|
1471
|
+
mentionAids: message.mentionAids,
|
|
1472
|
+
dispatch: session.metadata?.dispatchMode || message.dispatchMode,
|
|
1473
|
+
topicRounds: session.metadata?.topicRounds ?? 0,
|
|
1474
|
+
lastTopicHash: session.metadata?.lastTopicHash,
|
|
1475
|
+
});
|
|
1476
|
+
// 持久化话题追踪状态(仅群聊时有意义)
|
|
1477
|
+
if (message.chatType === 'group') {
|
|
1478
|
+
session.metadata = {
|
|
1479
|
+
...(session.metadata || {}),
|
|
1480
|
+
topicRounds: result.topicRounds,
|
|
1481
|
+
lastTopicHash: result.topicHash,
|
|
1482
|
+
};
|
|
1483
|
+
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1484
|
+
}
|
|
1485
|
+
return result.depth;
|
|
1486
|
+
}
|
|
1219
1487
|
/**
|
|
1220
1488
|
* 处理标准事件流(AgentEvent)
|
|
1221
1489
|
*
|
|
@@ -1392,7 +1660,7 @@ export class MessageProcessor {
|
|
|
1392
1660
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1393
1661
|
}
|
|
1394
1662
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
1395
|
-
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 };
|
|
1663
|
+
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, lastModelCall: event.lastModelCall, modelCalls: event.modelCalls };
|
|
1396
1664
|
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
1397
1665
|
// 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
|
|
1398
1666
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
@@ -1448,7 +1716,7 @@ export class MessageProcessor {
|
|
|
1448
1716
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1449
1717
|
}
|
|
1450
1718
|
// 记录完成状态
|
|
1451
|
-
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 };
|
|
1719
|
+
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, lastModelCall: event.lastModelCall };
|
|
1452
1720
|
if (event.subtype === 'success') {
|
|
1453
1721
|
this.messageCache.addEvent(session.id, {
|
|
1454
1722
|
type: 'completed',
|