evolclaw 3.2.0 → 3.4.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 (95) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +7 -4
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -31
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1152 -140
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +58 -0
  9. package/dist/aun/aid/store.js +1 -1
  10. package/dist/aun/outbox.js +14 -2
  11. package/dist/aun/storage/download.js +1 -1
  12. package/dist/aun/storage/upload.js +13 -1
  13. package/dist/channels/aun.js +869 -358
  14. package/dist/channels/dingtalk.js +77 -140
  15. package/dist/channels/feishu.js +125 -154
  16. package/dist/channels/qqbot.js +75 -138
  17. package/dist/channels/wechat.js +75 -136
  18. package/dist/channels/wecom.js +75 -138
  19. package/dist/cli/agent-command.js +591 -0
  20. package/dist/cli/agent.js +23 -8
  21. package/dist/cli/aun-commands.js +1444 -0
  22. package/dist/cli/ctl-command.js +78 -0
  23. package/dist/cli/daemon-commands.js +2707 -0
  24. package/dist/cli/index.js +23 -4905
  25. package/dist/cli/init.js +33 -6
  26. package/dist/cli/model.js +1 -1
  27. package/dist/cli/restart-monitor.js +539 -0
  28. package/dist/cli/stats.js +558 -0
  29. package/dist/cli/version.js +87 -0
  30. package/dist/cli/watch-logs.js +33 -0
  31. package/dist/cli/watch-msg.js +5 -2
  32. package/dist/config-store.js +12 -6
  33. package/dist/core/channel-loader.js +88 -83
  34. package/dist/core/command/command-handler.js +1189 -0
  35. package/dist/core/command/menu-handler.js +1478 -0
  36. package/dist/core/command/slash-gate.js +142 -0
  37. package/dist/core/command/slash-handler.js +2090 -0
  38. package/dist/core/evolagent-registry.js +82 -0
  39. package/dist/core/evolagent.js +17 -1
  40. package/dist/core/interaction-router.js +8 -0
  41. package/dist/core/message/command-handler-agent-control.js +63 -1
  42. package/dist/core/message/im-renderer.js +91 -51
  43. package/dist/core/message/items-formatter.js +9 -1
  44. package/dist/core/message/message-bridge.js +73 -24
  45. package/dist/core/message/message-log.js +1 -0
  46. package/dist/core/message/message-processor.js +432 -94
  47. package/dist/core/message/message-queue.js +70 -2
  48. package/dist/core/message/pending-hints.js +232 -0
  49. package/dist/core/model/model-catalog.js +1 -1
  50. package/dist/core/model/model-scope.js +2 -2
  51. package/dist/core/permission.js +25 -12
  52. package/dist/core/relation/peer-identity.js +16 -1
  53. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  54. package/dist/core/session/session-manager.js +86 -26
  55. package/dist/core/session/session-title.js +26 -0
  56. package/dist/core/stats/billing.js +151 -0
  57. package/dist/core/stats/budget.js +93 -0
  58. package/dist/core/stats/db.js +334 -0
  59. package/dist/core/stats/eck-vars.js +84 -0
  60. package/dist/core/stats/index.js +10 -0
  61. package/dist/core/stats/normalizer.js +78 -0
  62. package/dist/core/stats/query.js +760 -0
  63. package/dist/core/stats/writer.js +115 -0
  64. package/dist/core/trigger/manager.js +34 -0
  65. package/dist/core/trigger/parser.js +9 -3
  66. package/dist/core/trigger/scheduler.js +20 -17
  67. package/dist/data/error-dict.json +7 -0
  68. package/dist/{agents → eck}/manifest-engine.js +20 -1
  69. package/dist/{agents → eck}/message-renderer.js +24 -1
  70. package/dist/index.js +174 -9
  71. package/dist/ipc.js +116 -1
  72. package/dist/utils/cross-platform.js +58 -5
  73. package/dist/utils/ecweb-launch.js +49 -0
  74. package/dist/utils/ecweb-pair.js +20 -0
  75. package/dist/utils/error-utils.js +18 -5
  76. package/dist/utils/npm-ops.js +38 -8
  77. package/dist/utils/stats.js +77 -6
  78. package/kits/docs/evolclaw/INDEX.md +3 -1
  79. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  80. package/kits/docs/evolclaw/fs.md +131 -0
  81. package/kits/docs/evolclaw/group-fs.md +209 -0
  82. package/kits/docs/evolclaw/stats.md +70 -0
  83. package/kits/docs/venues/aun-group.md +29 -6
  84. package/kits/docs/venues/group.md +5 -4
  85. package/kits/eck_message_manifest.json +30 -3
  86. package/kits/rules/05-venue.md +1 -1
  87. package/kits/templates/message-fragments/inject-default.md +2 -0
  88. package/package.json +5 -6
  89. package/dist/agents/baseagent-normalize.js +0 -19
  90. package/dist/core/command-handler.js +0 -3876
  91. package/dist/core/relation/peer-key.js +0 -16
  92. package/dist/evolclaw-config.js +0 -11
  93. package/dist/utils/channel-helpers.js +0 -46
  94. /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
  95. /package/dist/{agents → eck}/kit-renderer.js +0 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * writer.ts — 写入 usage_events 和 context_breakdown。
3
+ */
4
+ import { getDb } from './db.js';
5
+ export function insertUsageEvent(evolclawHome, event) {
6
+ const db = getDb(evolclawHome);
7
+ if (!db)
8
+ return;
9
+ try {
10
+ // 明细 INSERT + rollup UPSERT 包进同一事务:进程在两者间崩溃也不会让 rollup 与明细漂移。
11
+ db.exec('BEGIN');
12
+ db.prepare(`
13
+ INSERT INTO usage_events
14
+ (ts, agent_aid, peer_key, peer_type, session_id, model, billing_fn,
15
+ input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
16
+ cache_hit_tokens, cache_miss_tokens, image_tokens, total_context_tokens,
17
+ turns, duration_ms, context_window_pct)
18
+ VALUES
19
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
20
+ `).run(event.ts, event.agent_aid, event.peer_key, event.peer_type ?? null, event.session_id ?? null, event.model, event.billing_fn, event.input_tokens, event.output_tokens, event.cache_creation_tokens, event.cache_read_tokens, event.cache_hit_tokens ?? null, event.cache_miss_tokens ?? null, event.image_tokens ?? null, event.total_context_tokens ?? null, event.turns, event.duration_ms ?? null, event.context_window_pct ?? null);
21
+ // 写时增量:累加到日级预聚合表(grain 与 db.ts rebuildDailyRollup 一致)。
22
+ db.prepare(`
23
+ INSERT INTO usage_daily
24
+ (day, agent_aid, peer_key, peer_type, session_id, model, billing_fn,
25
+ input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
26
+ cache_hit_tokens, cache_miss_tokens, image_tokens, total_context_tokens,
27
+ turns, calls)
28
+ VALUES
29
+ (strftime('%Y-%m-%d', ?/1000, 'unixepoch', 'localtime'),
30
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
31
+ ON CONFLICT(day, agent_aid, peer_key, session_id, model, billing_fn) DO UPDATE SET
32
+ peer_type = excluded.peer_type,
33
+ input_tokens = input_tokens + excluded.input_tokens,
34
+ output_tokens = output_tokens + excluded.output_tokens,
35
+ cache_creation_tokens = cache_creation_tokens + excluded.cache_creation_tokens,
36
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
37
+ cache_hit_tokens = cache_hit_tokens + excluded.cache_hit_tokens,
38
+ cache_miss_tokens = cache_miss_tokens + excluded.cache_miss_tokens,
39
+ image_tokens = image_tokens + excluded.image_tokens,
40
+ total_context_tokens = total_context_tokens + excluded.total_context_tokens,
41
+ turns = turns + excluded.turns,
42
+ calls = calls + 1
43
+ `).run(event.ts, event.agent_aid, event.peer_key, event.peer_type ?? '', event.session_id ?? '', event.model, event.billing_fn, event.input_tokens, event.output_tokens, event.cache_creation_tokens, event.cache_read_tokens, event.cache_hit_tokens ?? 0, event.cache_miss_tokens ?? 0, event.image_tokens ?? 0, event.total_context_tokens ?? 0, event.turns);
44
+ db.exec('COMMIT');
45
+ }
46
+ catch (e) {
47
+ // 写入失败不影响主流程
48
+ try {
49
+ db.exec('ROLLBACK');
50
+ }
51
+ catch { }
52
+ import('../../utils/logger.js').then(({ logger }) => logger.warn(`[StatsWriter] insertUsageEvent failed: ${e}`));
53
+ }
54
+ }
55
+ export function insertContextBreakdown(evolclawHome, bd) {
56
+ const db = getDb(evolclawHome);
57
+ if (!db)
58
+ return;
59
+ try {
60
+ db.prepare(`
61
+ INSERT INTO context_breakdown
62
+ (ts, agent_aid, session_id, turn_count, model, max_tokens,
63
+ system_prompt, system_tools, mcp_tools, custom_agents,
64
+ memory_files, skills, messages, free_space, total_estimated)
65
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
66
+ `).run(bd.ts, bd.agent_aid, bd.session_id, bd.turn_count, bd.model, bd.max_tokens, bd.system_prompt ?? null, bd.system_tools ?? null, bd.mcp_tools ?? null, bd.custom_agents ?? null, bd.memory_files ?? null, bd.skills ?? null, bd.messages ?? null, bd.free_space ?? null, bd.total_estimated ?? null);
67
+ }
68
+ catch (e) {
69
+ import('../../utils/logger.js').then(({ logger }) => logger.warn(`[StatsWriter] insertContextBreakdown failed: ${e}`));
70
+ }
71
+ }
72
+ export function insertMessageEvent(evolclawHome, event) {
73
+ const db = getDb(evolclawHome);
74
+ if (!db)
75
+ return;
76
+ try {
77
+ db.prepare(`
78
+ INSERT INTO message_events
79
+ (ts, agent_aid, peer_key, direction, msg_type, bytes, encrypted, chatmode)
80
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
81
+ `).run(event.ts, event.agent_aid, event.peer_key, event.direction, event.msg_type ?? null, event.bytes, event.encrypted ? 1 : 0, event.chatmode ?? null);
82
+ }
83
+ catch (e) {
84
+ import('../../utils/logger.js').then(({ logger }) => logger.warn(`[StatsWriter] insertMessageEvent failed: ${e}`));
85
+ }
86
+ }
87
+ /** 批量写入大模型调用明细。单事务;失败不影响主流程。 */
88
+ export function insertModelCalls(evolclawHome, rows) {
89
+ if (!rows.length)
90
+ return;
91
+ const db = getDb(evolclawHome);
92
+ if (!db)
93
+ return;
94
+ try {
95
+ const stmt = db.prepare(`
96
+ INSERT INTO model_calls
97
+ (ts, task_id, session_id, agent_session_id, agent_aid, peer_key, call_index, model,
98
+ request_id, message_id, input_tokens, output_tokens, cache_creation_tokens,
99
+ cache_read_tokens, context_tokens, max_tokens, auto_compact_tokens, degraded)
100
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
101
+ `);
102
+ db.exec('BEGIN');
103
+ for (const r of rows) {
104
+ stmt.run(r.ts, r.task_id, r.session_id ?? null, r.agent_session_id ?? null, r.agent_aid, r.peer_key, r.call_index, r.model, r.request_id ?? null, r.message_id ?? null, r.input_tokens, r.output_tokens, r.cache_creation_tokens, r.cache_read_tokens, r.context_tokens ?? null, r.max_tokens ?? null, r.auto_compact_tokens ?? null, r.degraded);
105
+ }
106
+ db.exec('COMMIT');
107
+ }
108
+ catch (e) {
109
+ try {
110
+ db.exec('ROLLBACK');
111
+ }
112
+ catch { }
113
+ import('../../utils/logger.js').then(({ logger }) => logger.warn(`[StatsWriter] insertModelCalls failed: ${e}`));
114
+ }
115
+ }
@@ -90,6 +90,30 @@ export class TriggerManager {
90
90
  getById(id) {
91
91
  return this.triggers.get(id);
92
92
  }
93
+ /**
94
+ * 按 id 从磁盘重读单条 trigger(不走内存缓存)。
95
+ * 用于触发时刻取最新数据,消除内存/磁盘漂移。
96
+ * 读盘失败或不存在时返回 undefined(调用方回退到内存副本)。
97
+ */
98
+ getByIdFresh(id) {
99
+ if (!fs.existsSync(this.triggersPath))
100
+ return undefined;
101
+ try {
102
+ const raw = fs.readFileSync(this.triggersPath, 'utf8');
103
+ const data = JSON.parse(raw);
104
+ return data.triggers?.[id];
105
+ }
106
+ catch {
107
+ return undefined;
108
+ }
109
+ }
110
+ /**
111
+ * 从磁盘重新加载到内存 Map(外部编辑 triggers.json 后调用)。
112
+ * 等价于 load(),语义上强调"丢弃内存副本、以磁盘为准"。
113
+ */
114
+ reloadFromDisk() {
115
+ return this.load();
116
+ }
93
117
  getByName(name) {
94
118
  for (const t of this.triggers.values()) {
95
119
  if (t.name === name)
@@ -154,6 +178,16 @@ export class TriggerManager {
154
178
  t.updatedAt = Date.now();
155
179
  this.save();
156
180
  }
181
+ updateResult(id, outcome) {
182
+ const t = this.triggers.get(id);
183
+ if (!t)
184
+ return;
185
+ t.lastResult = outcome;
186
+ if (outcome === 'failed')
187
+ t.failCount = (t.failCount ?? 0) + 1;
188
+ t.updatedAt = Date.now();
189
+ this.save();
190
+ }
157
191
  updateNextFireAt(id, nextFireAt) {
158
192
  const t = this.triggers.get(id);
159
193
  if (!t)
@@ -105,8 +105,11 @@ export function parseTriggerUpdate(args) {
105
105
  }
106
106
  else if (hasCron) {
107
107
  const raw = flags.get('cron');
108
- if (!validateCronExpr(raw))
109
- return { ok: false, error: `无效的 cron 表达式:"${raw}"` };
108
+ if (!validateCronExpr(raw)) {
109
+ const truncated = /^[\d*/,-]+$/.test(raw) && /--cron\s+\S+\s+[*\d]/.test(args);
110
+ const hint = truncated ? '(看起来 cron 表达式被空格截断了,请用引号包裹,如 --cron \'*/15 * * * *\')' : '(需 5 段:分 时 日 月 周,如 */15 * * * *)';
111
+ return { ok: false, error: `无效的 cron 表达式:"${raw}" ${hint}` };
112
+ }
110
113
  result.scheduleType = 'cron';
111
114
  result.scheduleValue = raw;
112
115
  }
@@ -191,7 +194,10 @@ export function parseTriggerSet(args) {
191
194
  else {
192
195
  const raw = flags.get('cron');
193
196
  if (!validateCronExpr(raw)) {
194
- return { ok: false, error: `无效的 cron 表达式:"${raw}"` };
197
+ // Detect likely space-truncation: raw looks like one cron segment and args contains space-separated * or digits after it
198
+ const truncated = /^[\d*/,-]+$/.test(raw) && /--cron\s+\S+\s+[*\d]/.test(args);
199
+ const hint = truncated ? '(看起来 cron 表达式被空格截断了,请用引号包裹,如 --cron \'*/15 * * * *\')' : '(需 5 段:分 时 日 月 周,如 */15 * * * *)';
200
+ return { ok: false, error: `无效的 cron 表达式:"${raw}" ${hint}` };
195
201
  }
196
202
  scheduleType = 'cron';
197
203
  scheduleValue = raw;
@@ -130,7 +130,7 @@ export class TriggerScheduler {
130
130
  register(trigger) {
131
131
  this.heap.push(trigger);
132
132
  this.resetTimer();
133
- this.eventBus.publish({ type: 'trigger:registered', triggerId: trigger.id, name: trigger.name, peerId: trigger.createdByPeerId });
133
+ this.eventBus.publish({ type: 'trigger:registered', triggerId: trigger.id, name: trigger.name, peerId: trigger.createdByPeerId, targetChannel: trigger.targetChannel, targetChannelId: trigger.targetChannelId, scheduleType: trigger.scheduleType, scheduleValue: trigger.scheduleValue });
134
134
  }
135
135
  cancel(id) {
136
136
  this.heap.remove(id);
@@ -141,7 +141,7 @@ export class TriggerScheduler {
141
141
  this.heap.remove(trigger.id);
142
142
  this.heap.push(trigger);
143
143
  this.resetTimer();
144
- this.eventBus.publish({ type: 'trigger:updated', triggerId: trigger.id, name: trigger.name, peerId: trigger.createdByPeerId });
144
+ this.eventBus.publish({ type: 'trigger:updated', triggerId: trigger.id, name: trigger.name, peerId: trigger.createdByPeerId, scheduleType: trigger.scheduleType, scheduleValue: trigger.scheduleValue });
145
145
  }
146
146
  stop() {
147
147
  if (this.timer) {
@@ -172,7 +172,7 @@ export class TriggerScheduler {
172
172
  if (trigger.scheduleType === 'cron' && this.inflightCron.has(trigger.id)) {
173
173
  // Previous run still in flight — skip this occurrence, reschedule the next one.
174
174
  logger.warn(`[${this.aid}] Cron trigger ${trigger.name} still running, skipping`);
175
- this.eventBus.publish({ type: 'trigger:skipped', triggerId: trigger.id, reason: 'overlap' });
175
+ this.eventBus.publish({ type: 'trigger:skipped', triggerId: trigger.id, name: trigger.name, reason: 'overlap', targetChannel: trigger.targetChannel, targetChannelId: trigger.targetChannelId });
176
176
  const next = this.nextCronFireAt(trigger.scheduleValue, Date.now());
177
177
  this.manager.updateNextFireAt(trigger.id, next);
178
178
  this.heap.push({ ...trigger, nextFireAt: next });
@@ -198,31 +198,34 @@ export class TriggerScheduler {
198
198
  return next;
199
199
  }
200
200
  fireTrigger(trigger, now) {
201
- const messageId = `trigger:${trigger.id}:${now}`;
202
- const msg = this.buildSyntheticMessage(trigger, messageId);
203
- logger.info(`[${this.aid}] Firing trigger: ${trigger.name} (${trigger.id})`);
201
+ // 触发时刻以磁盘为真相源:heap 里的副本可能因外部编辑而过期。
202
+ // id 重读磁盘最新版,读盘失败则回退到 heap 副本。
203
+ const fresh = this.manager.getByIdFresh(trigger.id) ?? trigger;
204
+ const messageId = `trigger:${fresh.id}:${now}`;
205
+ const msg = this.buildSyntheticMessage(fresh, messageId);
206
+ logger.info(`[${this.aid}] Firing trigger: ${fresh.name} (${fresh.id})`);
204
207
  // Update stats before moving to done so history captures the updated count
205
- this.manager.updateFireStats(trigger.id, now);
206
- if (trigger.scheduleType === 'cron') {
207
- this.inflightCron.add(trigger.id);
208
+ this.manager.updateFireStats(fresh.id, now);
209
+ if (fresh.scheduleType === 'cron') {
210
+ this.inflightCron.add(fresh.id);
208
211
  // Re-schedule next occurrence (outside the firing window — see nextCronFireAt)
209
- const next = this.nextCronFireAt(trigger.scheduleValue, now);
210
- this.manager.updateNextFireAt(trigger.id, next);
211
- this.heap.push({ ...trigger, nextFireAt: next });
212
+ const next = this.nextCronFireAt(fresh.scheduleValue, now);
213
+ this.manager.updateNextFireAt(fresh.id, next);
214
+ this.heap.push({ ...fresh, nextFireAt: next });
212
215
  }
213
216
  else {
214
217
  // delay/at: one-shot, move to done
215
- this.manager.moveToDone(trigger.id, 'fired');
218
+ this.manager.moveToDone(fresh.id, 'fired');
216
219
  }
217
- this.eventBus.publish({ type: 'trigger:fired', triggerId: trigger.id, name: trigger.name, fireTime: now });
220
+ this.eventBus.publish({ type: 'trigger:fired', triggerId: fresh.id, name: fresh.name, fireTime: now, targetChannel: fresh.targetChannel, targetChannelId: fresh.targetChannelId, scheduleType: fresh.scheduleType });
218
221
  if (this.fireCallback) {
219
- this.fireCallback(msg, trigger);
222
+ this.fireCallback(msg, fresh);
220
223
  }
221
224
  }
222
225
  // Called by MessageProcessor when a trigger message completes/fails/is interrupted
223
- onTriggerComplete(triggerId, _outcome) {
224
- // Only clear inflight state — message-processor already published the relevant events
226
+ onTriggerComplete(triggerId, outcome) {
225
227
  this.inflightCron.delete(triggerId);
228
+ this.manager.updateResult(triggerId, outcome);
226
229
  }
227
230
  buildSyntheticMessage(trigger, messageId) {
228
231
  const base = {
@@ -95,6 +95,13 @@
95
95
  "type": "api_error",
96
96
  "message": "⚠️ API 返回异常响应,正在重试..."
97
97
  },
98
+ {
99
+ "id": "json-parse-error",
100
+ "match": "json parse error",
101
+ "action": "retry",
102
+ "type": "api_error",
103
+ "message": "⚠️ API 返回 JSON 解析异常,正在重试..."
104
+ },
98
105
  {
99
106
  "id": "feishu-permission",
100
107
  "match": "im:resource",
@@ -5,7 +5,7 @@ import fs from 'fs';
5
5
  import path from 'path';
6
6
  import { kitsDir, resolveRoot, getPackageRoot } from '../paths.js';
7
7
  import { logger } from '../utils/logger.js';
8
- import { fileCache } from '../core/cache/file-cache.js';
8
+ import { fileCache } from '../core/daemon-file-cache.js';
9
9
  // ── Manifest loading / cache ──
10
10
  // manifest 定义随包发布、运行期靠 reload/重启刷新 → on-reload(group 'kits')。
11
11
  // base + eck override 合成结果以 base 文件路径为键缓存;loader 内读两个文件。
@@ -59,10 +59,29 @@ function loadAndMergeManifest(filename) {
59
59
  function sortSections(sections) {
60
60
  return sections.slice().sort((a, b) => a.order - b.order);
61
61
  }
62
+ /**
63
+ * 从 manifest 抽出每个 modeType 标了 isDefault 的 modeName。
64
+ * 供 message-renderer 在 agent config.render 未配某类型时回退。
65
+ * 同一 modeType 有多个 isDefault 时取 order 最小(已排序)的第一个。
66
+ */
67
+ export function defaultModeNames(sections) {
68
+ const out = {};
69
+ for (const s of sections) {
70
+ if (s.modeType && s.isDefault && s.modeName && out[s.modeType] === undefined) {
71
+ out[s.modeType] = s.modeName;
72
+ }
73
+ }
74
+ return out;
75
+ }
62
76
  // ── When condition evaluation ──
63
77
  export function evaluateWhen(when, vars) {
64
78
  if (when === 'always')
65
79
  return true;
80
+ // 复合条件优先:and / or 递归求值。
81
+ if (when.and)
82
+ return when.and.every(c => evaluateWhen(c, vars));
83
+ if (when.or)
84
+ return when.or.some(c => evaluateWhen(c, vars));
66
85
  if (when.var !== undefined) {
67
86
  const val = vars[when.var];
68
87
  if (when.eq !== undefined) {
@@ -10,7 +10,7 @@ import path from 'path';
10
10
  import { randomUUID } from 'crypto';
11
11
  import { eckDebugDir } from '../paths.js';
12
12
  import { logger } from '../utils/logger.js';
13
- import { loadManifest, evaluateWhen, renderTemplate, loadSectionFiles, } from './manifest-engine.js';
13
+ import { loadManifest, evaluateWhen, renderTemplate, loadSectionFiles, defaultModeNames, } from './manifest-engine.js';
14
14
  const MESSAGE_MANIFEST_FILE = 'eck_message_manifest.json';
15
15
  // ── time formatting (per IANA timezone) ──
16
16
  function timeParts(epochMs, timeZone, opts) {
@@ -35,6 +35,19 @@ export function formatLocalTime(epochMs, timeZone) {
35
35
  // ── single item render ──
36
36
  function renderOneItem(item, sessionVars, sessionCache, contentSentinel) {
37
37
  const sections = loadManifest(MESSAGE_MANIFEST_FILE);
38
+ // 各 modeType 当前激活的 modeName:agent config.render(经 sessionVars.renderModes 透传)
39
+ // 覆盖 manifest 里标 isDefault 的缺省。详见 docs/observer-insert-design.md 第二部分。
40
+ const defaults = defaultModeNames(sections);
41
+ const configured = (sessionVars.renderModes && typeof sessionVars.renderModes === 'object' && !Array.isArray(sessionVars.renderModes))
42
+ ? sessionVars.renderModes
43
+ : {};
44
+ const activeMode = (type) => {
45
+ const c = configured[type];
46
+ if (typeof c === 'string' && c)
47
+ return c;
48
+ return defaults[type];
49
+ };
50
+ const isOwnerHint = item.kind === 'owner-hint';
38
51
  // item-level vars: session vars overlaid with this message's own sender/timestamp.
39
52
  const itemVars = {
40
53
  ...sessionVars,
@@ -47,6 +60,16 @@ function renderOneItem(item, sessionVars, sessionCache, contentSentinel) {
47
60
  // 模板引擎不支持数组循环:被 @ 的 AID 预先 join 成串,空则 undefined 使 {{?mentionAids}} 落空。
48
61
  mentionAids: (item.mentionAids && item.mentionAids.length > 0) ? item.mentionAids.join(',') : undefined,
49
62
  now: formatLocalTime(item.timestamp ?? Date.now(), sessionVars.timezone ? String(sessionVars.timezone) : undefined),
63
+ // 渲染模式命中变量(manifest section 的 when 用它选中唯一模式)
64
+ renderMode_private: activeMode('private'),
65
+ renderMode_group: activeMode('group'),
66
+ renderMode_inject: activeMode('inject'),
67
+ // owner 插话提示标记 + 信封头字段
68
+ isOwnerHint,
69
+ ownerAid: isOwnerHint ? (item.ownerAid ?? undefined) : undefined,
70
+ injectTime: isOwnerHint
71
+ ? formatLocalTime(item.injectTime ?? item.timestamp ?? Date.now(), sessionVars.timezone ? String(sessionVars.timezone) : undefined)
72
+ : undefined,
50
73
  // content held as a per-call random sentinel, swapped back post-render.
51
74
  // Using a UUID means no real message can collide with it.
52
75
  content: contentSentinel,