evolclaw 3.2.0 → 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 +17 -0
- package/README.md +1 -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/store.js +1 -1
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +406 -293
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +97 -150
- 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 +8 -5
- package/dist/cli/index.js +177 -44
- package/dist/cli/init.js +33 -6
- 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 +12 -6
- package/dist/core/channel-loader.js +84 -82
- package/dist/core/command-handler.js +473 -114
- package/dist/core/evolagent-registry.js +1 -0
- package/dist/core/evolagent.js +1 -1
- package/dist/core/interaction-router.js +8 -0
- package/dist/core/message/command-handler-agent-control.js +63 -1
- 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 +49 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +295 -35
- package/dist/core/message/message-queue.js +2 -2
- 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 +2 -2
- package/dist/core/permission.js +9 -12
- package/dist/core/relation/peer-identity.js +16 -1
- 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}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +130 -8
- package/dist/ipc.js +17 -1
- 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/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/evolclaw-config.js +0 -11
- package/dist/utils/channel-helpers.js +0 -46
- /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
- /package/dist/{agents → eck}/kit-renderer.js +0 -0
|
@@ -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;
|
|
@@ -351,7 +386,7 @@ export class MessageProcessor {
|
|
|
351
386
|
});
|
|
352
387
|
try {
|
|
353
388
|
await Promise.race([
|
|
354
|
-
this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, () => lastIdleSec),
|
|
389
|
+
this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, () => lastIdleSec, responseDepth),
|
|
355
390
|
timeoutPromise
|
|
356
391
|
]);
|
|
357
392
|
}
|
|
@@ -399,7 +434,7 @@ export class MessageProcessor {
|
|
|
399
434
|
return message.replyContext;
|
|
400
435
|
}
|
|
401
436
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
402
|
-
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec) {
|
|
437
|
+
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, responseDepth) {
|
|
403
438
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
404
439
|
const channelKey = session.metadata?.channelKey || message.channel;
|
|
405
440
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
@@ -423,7 +458,7 @@ export class MessageProcessor {
|
|
|
423
458
|
const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
|
424
459
|
const chatmode = session.sessionMode ?? 'interactive';
|
|
425
460
|
// 诊断日志:记录 inbound message_id 和生成的 task_id 的对应关系
|
|
426
|
-
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}` : ''}`);
|
|
427
462
|
// 构建带 taskId/chatmode 的 ReplyContext(本次任务所有出站消息共用)
|
|
428
463
|
const taskReplyContext = () => {
|
|
429
464
|
const base = this.getReplyContext(message);
|
|
@@ -461,6 +496,17 @@ export class MessageProcessor {
|
|
|
461
496
|
agentName: agentNameForStats,
|
|
462
497
|
timestamp: Date.now()
|
|
463
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
|
+
}
|
|
464
510
|
const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
|
|
465
511
|
const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
|
|
466
512
|
const e2eeInfo = message.replyContext?.metadata?.encrypted != null ? ` encrypt=${message.replyContext.metadata.encrypted}` : '';
|
|
@@ -546,6 +592,9 @@ export class MessageProcessor {
|
|
|
546
592
|
agentName: agentNameForStats,
|
|
547
593
|
taskId,
|
|
548
594
|
chatmode: isProactive ? 'proactive' : 'interactive',
|
|
595
|
+
flushPending: async () => {
|
|
596
|
+
await renderer.flush(false);
|
|
597
|
+
},
|
|
549
598
|
interceptNextMessage: this.messageQueue
|
|
550
599
|
? (sessionKey, handler) => this.messageQueue.interceptNext(sessionKey, handler)
|
|
551
600
|
: undefined,
|
|
@@ -582,7 +631,8 @@ export class MessageProcessor {
|
|
|
582
631
|
const currentChannelType = options?.channelType || message.channel;
|
|
583
632
|
// 提取 self 信息
|
|
584
633
|
const adapterAny = channelInfo.adapter;
|
|
585
|
-
const
|
|
634
|
+
const adapterSelfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
635
|
+
const selfAid = adapterSelfAid || message.selfAID || session.selfAID || undefined;
|
|
586
636
|
const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
|
|
587
637
|
const peerName = message.peerName || session.metadata?.peerName;
|
|
588
638
|
// 通道能力
|
|
@@ -606,6 +656,7 @@ export class MessageProcessor {
|
|
|
606
656
|
const peerKey = (currentChannelType && peerIdRaw)
|
|
607
657
|
? formatPeerKey(currentChannelType, peerIdRaw)
|
|
608
658
|
: undefined;
|
|
659
|
+
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
609
660
|
// 按 关系级 > agent级 > 全局 解析本次调用的模型/强度,作为 per-call 入参传入 runQuery。
|
|
610
661
|
// 不缓存、不绑会话——改关系级/agent级后该范围所有会话的下条消息即时生效;
|
|
611
662
|
// 多对端并发各自独立解析、各自传参,无共享状态可被污染。
|
|
@@ -625,7 +676,7 @@ export class MessageProcessor {
|
|
|
625
676
|
let evolclawModelOverride;
|
|
626
677
|
if (!skipEvolclawModel) {
|
|
627
678
|
try {
|
|
628
|
-
const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey });
|
|
679
|
+
const resolved = resolveEffectiveModel({ self: selfAid || undefined, peerKey }, normalizedBaseagent.canonical);
|
|
629
680
|
if (resolved.model) {
|
|
630
681
|
evolclawModelOverride = { model: resolved.model, effort: resolved.effort };
|
|
631
682
|
effectiveModel = resolved.model;
|
|
@@ -636,7 +687,20 @@ export class MessageProcessor {
|
|
|
636
687
|
}
|
|
637
688
|
modelOverride = evolclawModelOverride;
|
|
638
689
|
}
|
|
639
|
-
|
|
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
|
+
}
|
|
640
704
|
agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
|
|
641
705
|
// Kit renderer: 组装上下文
|
|
642
706
|
const pkgRoot = getPackageRoot();
|
|
@@ -680,7 +744,9 @@ export class MessageProcessor {
|
|
|
680
744
|
channel: currentChannelType || null,
|
|
681
745
|
venueUid: undefined,
|
|
682
746
|
// 群分发模式 / 客户端类型 / 权限模式
|
|
683
|
-
|
|
747
|
+
// 优先本地 session 覆盖(/dispatch 命令),fallback 到服务器 dispatch_mode
|
|
748
|
+
dispatch: session.metadata?.dispatchMode || message.dispatchMode || undefined,
|
|
749
|
+
responseDepth: responseDepth || undefined,
|
|
684
750
|
clientType: message.clientType || undefined,
|
|
685
751
|
permissionMode: session.metadata?.permissionMode || 'auto',
|
|
686
752
|
capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
|
|
@@ -706,6 +772,8 @@ export class MessageProcessor {
|
|
|
706
772
|
modelFallbackActive: (fbState.fallbackActive || skipEvolclawModel) ? true : undefined,
|
|
707
773
|
modelFallbackModel: (fbState.fallbackActive || skipEvolclawModel) ? (agentModel || undefined) : undefined,
|
|
708
774
|
agentSessionId: session.agentSessionId || undefined,
|
|
775
|
+
// 渲染模式:各类型当前激活的 modeName(从内存 config 读,渲染层据此选 manifest section)。
|
|
776
|
+
renderModes: this.agentRegistry?.resolveByChannel(channelKey)?.config?.render ?? undefined,
|
|
709
777
|
},
|
|
710
778
|
sessionId: session.id,
|
|
711
779
|
};
|
|
@@ -713,31 +781,65 @@ export class MessageProcessor {
|
|
|
713
781
|
if (kitContext)
|
|
714
782
|
contextParts.push(kitContext);
|
|
715
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 */ }
|
|
716
813
|
// 消息渲染层:用 message manifest 逐条渲染(时间 + 群聊发送者),组装成最终正文。
|
|
717
814
|
// 单条消息构造单元素 items;批量合并的消息 message.items 已由队列填充。
|
|
718
815
|
let renderResult;
|
|
719
816
|
const hasContent = message.content.trim() || (message.items && message.items.length > 0);
|
|
720
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;
|
|
721
833
|
try {
|
|
722
|
-
const renderItems = message.items && message.items.length > 0
|
|
723
|
-
? message.items
|
|
724
|
-
: [{
|
|
725
|
-
peerId: message.peerId, peerName: peerName || undefined,
|
|
726
|
-
peerType: message.peerType,
|
|
727
|
-
sameDevice: message.sameDevice, sameNetwork: message.sameNetwork, sameEgressIp: message.sameEgressIp,
|
|
728
|
-
content: message.content, timestamp: message.timestamp,
|
|
729
|
-
images: message.images,
|
|
730
|
-
mentionAids: message.mentionAids,
|
|
731
|
-
}];
|
|
732
834
|
renderResult = renderMessageBody(renderItems, kitCtx.vars, session.id);
|
|
733
835
|
if (renderResult.body.trim())
|
|
734
836
|
effectivePrompt = wrapPrompt(renderResult.body);
|
|
735
837
|
else
|
|
736
|
-
effectivePrompt = wrapPrompt(message.content);
|
|
838
|
+
effectivePrompt = wrapPrompt(composeHintFallback(hintItems, message.content));
|
|
737
839
|
}
|
|
738
840
|
catch (e) {
|
|
739
841
|
logger.warn(`[MessageProcessor] renderMessageBody failed, using raw content: ${e instanceof Error ? e.message : String(e)}`);
|
|
740
|
-
effectivePrompt = wrapPrompt(message.content);
|
|
842
|
+
effectivePrompt = wrapPrompt(composeHintFallback(hintItems, message.content));
|
|
741
843
|
}
|
|
742
844
|
}
|
|
743
845
|
// 可重试错误(403/429/5xx)指数退避重试,最多 3 次
|
|
@@ -988,7 +1090,7 @@ export class MessageProcessor {
|
|
|
988
1090
|
adapter.send(envelope, { kind: 'status.error', metadata: { errorType: rawSubtype } }).catch(() => { });
|
|
989
1091
|
}
|
|
990
1092
|
if (message.triggerMeta) {
|
|
991
|
-
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' });
|
|
992
1094
|
}
|
|
993
1095
|
this.eventBus.publish({
|
|
994
1096
|
type: 'task:error',
|
|
@@ -1019,20 +1121,131 @@ export class MessageProcessor {
|
|
|
1019
1121
|
else {
|
|
1020
1122
|
// 真正的成功
|
|
1021
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 */ }
|
|
1022
1219
|
if (message.source !== 'trigger') {
|
|
1023
1220
|
if (interruptReason) {
|
|
1024
1221
|
adapter.send(envelope, { kind: 'status.interrupted', metadata: { reason: interruptReason } }).catch(() => { });
|
|
1025
1222
|
}
|
|
1026
1223
|
else {
|
|
1027
|
-
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(() => { });
|
|
1028
1241
|
}
|
|
1029
1242
|
}
|
|
1030
1243
|
if (message.triggerMeta) {
|
|
1031
1244
|
if (interruptReason) {
|
|
1032
|
-
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 });
|
|
1033
1246
|
}
|
|
1034
1247
|
else {
|
|
1035
|
-
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 });
|
|
1036
1249
|
}
|
|
1037
1250
|
}
|
|
1038
1251
|
await this.sessionManager.recordSuccess(session.id);
|
|
@@ -1166,11 +1379,24 @@ export class MessageProcessor {
|
|
|
1166
1379
|
* 解析会话和项目路径
|
|
1167
1380
|
*/
|
|
1168
1381
|
async resolveSession(message) {
|
|
1169
|
-
//
|
|
1170
|
-
const metadata =
|
|
1171
|
-
? {
|
|
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
|
+
}
|
|
1172
1389
|
: undefined;
|
|
1173
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
|
+
}
|
|
1174
1400
|
// current strategy: resume bound session, make it active so output is not suppressed
|
|
1175
1401
|
if (message.triggerMeta?.boundSessionId) {
|
|
1176
1402
|
const bound = await this.sessionManager.getSessionById(message.triggerMeta.boundSessionId);
|
|
@@ -1187,7 +1413,7 @@ export class MessageProcessor {
|
|
|
1187
1413
|
logger.warn(`[MessageProcessor] Bound session ${message.triggerMeta.boundSessionId} not found, falling back to latest`);
|
|
1188
1414
|
}
|
|
1189
1415
|
}
|
|
1190
|
-
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);
|
|
1191
1417
|
// 兜底纠正1:群聊强制 proactive
|
|
1192
1418
|
if (message.chatType === 'group' && session.sessionMode !== 'proactive') {
|
|
1193
1419
|
logger.info(`[MessageProcessor] group proactive upgrade: sessionId=${session.id} ${session.sessionMode} -> proactive`);
|
|
@@ -1204,6 +1430,14 @@ export class MessageProcessor {
|
|
|
1204
1430
|
await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
|
|
1205
1431
|
}
|
|
1206
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
|
+
}
|
|
1207
1441
|
// 兜底纠正2:旧 session 创建时没传 peerType(建为 interactive),后续非 human 消息进来时升级为 proactive。
|
|
1208
1442
|
// 新建场景已由 getOrCreateSession 内部 resolveDefaultSessionMode 处理,这里只兜底历史会话。
|
|
1209
1443
|
if (message.peerType && message.peerType !== 'human' && message.peerType !== 'unknown' && session.sessionMode !== 'proactive') {
|
|
@@ -1224,6 +1458,32 @@ export class MessageProcessor {
|
|
|
1224
1458
|
: path.resolve(process.cwd(), session.projectPath);
|
|
1225
1459
|
return { session, absoluteProjectPath };
|
|
1226
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
|
+
}
|
|
1227
1487
|
/**
|
|
1228
1488
|
* 处理标准事件流(AgentEvent)
|
|
1229
1489
|
*
|
|
@@ -1400,7 +1660,7 @@ export class MessageProcessor {
|
|
|
1400
1660
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1401
1661
|
}
|
|
1402
1662
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
1403
|
-
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 };
|
|
1404
1664
|
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
1405
1665
|
// 由那里按 LLM 输出的每个 text item 单独写一条,此处不再写。
|
|
1406
1666
|
// 失败且无前置错误输出:显示 errors 摘要
|
|
@@ -1456,7 +1716,7 @@ export class MessageProcessor {
|
|
|
1456
1716
|
logger.info(`[MessageProcessor] Auto-filled session name: ${event.sessionTitle}`);
|
|
1457
1717
|
}
|
|
1458
1718
|
// 记录完成状态
|
|
1459
|
-
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 };
|
|
1460
1720
|
if (event.subtype === 'success') {
|
|
1461
1721
|
this.messageCache.addEvent(session.id, {
|
|
1462
1722
|
type: 'completed',
|
|
@@ -92,8 +92,8 @@ export class MessageQueue {
|
|
|
92
92
|
if (!this.queues.has(queueKey)) {
|
|
93
93
|
this.queues.set(queueKey, []);
|
|
94
94
|
}
|
|
95
|
-
this.queues.get(queueKey)
|
|
96
|
-
|
|
95
|
+
const queue = this.queues.get(queueKey);
|
|
96
|
+
queue.push({ message, projectPath, agentName, resolve, reject });
|
|
97
97
|
if (this.processing.has(queueKey)) {
|
|
98
98
|
if (options?.interruptible !== false) {
|
|
99
99
|
// 单聊:保留中断行为
|