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.
Files changed (83) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -2
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -27
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1069 -141
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +28 -0
  9. package/dist/aun/aid/store.js +1 -1
  10. package/dist/aun/storage/download.js +1 -1
  11. package/dist/aun/storage/upload.js +13 -1
  12. package/dist/channels/aun.js +406 -293
  13. package/dist/channels/dingtalk.js +77 -140
  14. package/dist/channels/feishu.js +97 -150
  15. package/dist/channels/qqbot.js +75 -138
  16. package/dist/channels/wechat.js +75 -136
  17. package/dist/channels/wecom.js +75 -138
  18. package/dist/cli/agent.js +8 -5
  19. package/dist/cli/index.js +177 -44
  20. package/dist/cli/init.js +33 -6
  21. package/dist/cli/model.js +1 -1
  22. package/dist/cli/stats.js +558 -0
  23. package/dist/cli/version.js +87 -0
  24. package/dist/cli/watch-msg.js +5 -2
  25. package/dist/config-store.js +12 -6
  26. package/dist/core/channel-loader.js +84 -82
  27. package/dist/core/command-handler.js +473 -114
  28. package/dist/core/evolagent-registry.js +1 -0
  29. package/dist/core/evolagent.js +1 -1
  30. package/dist/core/interaction-router.js +8 -0
  31. package/dist/core/message/command-handler-agent-control.js +63 -1
  32. package/dist/core/message/im-renderer.js +35 -13
  33. package/dist/core/message/items-formatter.js +9 -1
  34. package/dist/core/message/message-bridge.js +49 -21
  35. package/dist/core/message/message-log.js +1 -0
  36. package/dist/core/message/message-processor.js +295 -35
  37. package/dist/core/message/message-queue.js +2 -2
  38. package/dist/core/message/pending-hints.js +232 -0
  39. package/dist/core/message/response-depth.js +56 -0
  40. package/dist/core/model/model-catalog.js +1 -1
  41. package/dist/core/model/model-scope.js +2 -2
  42. package/dist/core/permission.js +9 -12
  43. package/dist/core/relation/peer-identity.js +16 -1
  44. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  45. package/dist/core/session/session-manager.js +27 -13
  46. package/dist/core/session/session-title.js +26 -0
  47. package/dist/core/stats/billing.js +151 -0
  48. package/dist/core/stats/budget.js +93 -0
  49. package/dist/core/stats/db.js +314 -0
  50. package/dist/core/stats/eck-vars.js +84 -0
  51. package/dist/core/stats/index.js +10 -0
  52. package/dist/core/stats/normalizer.js +78 -0
  53. package/dist/core/stats/query.js +760 -0
  54. package/dist/core/stats/writer.js +115 -0
  55. package/dist/core/trigger/manager.js +34 -0
  56. package/dist/core/trigger/parser.js +9 -3
  57. package/dist/core/trigger/scheduler.js +20 -17
  58. package/dist/{agents → eck}/manifest-engine.js +20 -1
  59. package/dist/{agents → eck}/message-renderer.js +24 -1
  60. package/dist/index.js +130 -8
  61. package/dist/ipc.js +17 -1
  62. package/dist/utils/cross-platform.js +23 -5
  63. package/dist/utils/ecweb-pair.js +20 -0
  64. package/dist/utils/stats.js +14 -0
  65. package/kits/docs/evolclaw/INDEX.md +3 -1
  66. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  67. package/kits/docs/evolclaw/fs.md +131 -0
  68. package/kits/docs/evolclaw/group-fs.md +209 -0
  69. package/kits/docs/evolclaw/stats.md +70 -0
  70. package/kits/docs/venues/aun-group.md +29 -6
  71. package/kits/docs/venues/group.md +5 -4
  72. package/kits/eck_manifest.json +12 -0
  73. package/kits/eck_message_manifest.json +30 -3
  74. package/kits/rules/05-venue.md +1 -1
  75. package/kits/templates/message-fragments/inject-default.md +2 -0
  76. package/kits/templates/system-fragments/response-depth.md +16 -0
  77. package/package.json +4 -4
  78. package/dist/agents/baseagent-normalize.js +0 -19
  79. package/dist/core/relation/peer-key.js +0 -16
  80. package/dist/evolclaw-config.js +0 -11
  81. package/dist/utils/channel-helpers.js +0 -46
  82. /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
  83. /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/claude-runner.js';
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 '../../agents/kit-renderer.js';
14
- import { renderMessageBody } from '../../agents/message-renderer.js';
15
- import { normalizeBaseagent } from '../../agents/baseagent-normalize.js';
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-key.js';
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 selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
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
- const normalizedBaseagent = normalizeBaseagent(agent.name);
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
- dispatch: session.metadata?.dispatchMode || undefined,
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: { durationMs, ttftMs: streamResult.ttftMs, numTurns: streamResult.numTurns, tokenUsage: streamResult.tokenUsage, contextUsage: streamResult.contextUsage } }).catch(() => { });
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
- // 话题会话创建时写入 replyContext(threadId 路由);主会话不写(避免群聊覆盖)
1170
- const metadata = (message.threadId && message.replyContext)
1171
- ? { replyContext: message.replyContext }
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, undefined, message.peerId, message.chatType, undefined, message.selfAID, message.channelType, message.peerType);
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).push({ message, projectPath, agentName, resolve, reject });
96
- // 根据 interruptible 选项决定是否触发中断
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
  // 单聊:保留中断行为