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
@@ -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, 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.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 = {
@@ -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,
package/dist/index.js CHANGED
@@ -2,9 +2,8 @@ import { ClaudeSessionFileAdapter } from './core/session/adapters/claude-session
2
2
  import { CodexSessionFileAdapter } from './core/session/adapters/codex-session-file-adapter.js';
3
3
  import { GeminiSessionFileAdapter } from './core/session/adapters/gemini-session-file-adapter.js';
4
4
  import { ensureDataDirs, resolvePaths, getPackageRoot, agentMdPath } from './paths.js';
5
- import { resolveAnthropicConfig } from './agents/resolve.js';
6
- import { loadDefaults, autoMigrateIfNeeded, migrateIdentitiesIfNeeded, migrateProcessConfigIfNeeded } from './config-store.js';
7
- import { loadEvolclawConfig } from './evolclaw-config.js';
5
+ import { resolveAnthropicConfig } from './agents/baseagent.js';
6
+ import { loadDefaults, autoMigrateIfNeeded, migrateIdentitiesIfNeeded, migrateProcessConfigIfNeeded, loadEvolclawConfig } from './config-store.js';
8
7
  import { CONFIG_SCHEMA_VERSION } from './types.js';
9
8
  import dotenv from 'dotenv';
10
9
  import { SessionManager } from './core/session/session-manager.js';
@@ -21,7 +20,7 @@ import { MessageProcessor, buildEnvelope } from './core/message/message-processo
21
20
  import { MessageQueue } from './core/message/message-queue.js';
22
21
  import { MessageBridge } from './core/message/message-bridge.js';
23
22
  import { MessageCache } from './core/message/message-cache.js';
24
- import { CommandHandler } from './core/command-handler.js';
23
+ import { CommandHandler, isProcessLevelOwner } from './core/command-handler.js';
25
24
  import { EventBus } from './core/event-bus.js';
26
25
  import { StatsCollector } from './utils/stats.js';
27
26
  import { AidStatsCollector } from './utils/stats.js';
@@ -33,9 +32,10 @@ import { EvolAgentRegistry } from './core/evolagent-registry.js';
33
32
  import { buildReloadHooks } from './core/channel-loader.js';
34
33
  import { IpcServer } from './ipc.js';
35
34
  import { logger, setLogLevel } from './utils/logger.js';
35
+ import { fetchEcwebPairCode } from './utils/ecweb-pair.js';
36
36
  import { writeMain, removeAll, isMainWinner, scanInstances } from './utils/instance-registry.js';
37
37
  import { detectDuplicates } from './core/evolagent-registry.js';
38
- import { loadKitManifest, cleanEckDebug, invalidateKitCache } from './agents/kit-renderer.js';
38
+ import { loadKitManifest, cleanEckDebug, invalidateKitCache } from './eck/kit-renderer.js';
39
39
  import { initEck } from './eck/init.js';
40
40
  import { TriggerManager } from './core/trigger/manager.js';
41
41
  import { TriggerScheduler, calcNextFireAt } from './core/trigger/scheduler.js';
@@ -316,6 +316,40 @@ async function main() {
316
316
  // Per-AID 消息统计收集器(累计,供 watch aid 实时展示)
317
317
  const aidStatsCollector = new AidStatsCollector(eventBus);
318
318
  aidStatsCollector.setSessionsDir(paths.sessionsDir);
319
+ // 持久化网络流量到 message_events 表
320
+ aidStatsCollector.onMessage = (ev) => {
321
+ import('./core/stats/writer.js').then(({ insertMessageEvent }) => {
322
+ insertMessageEvent(paths.root, ev);
323
+ }).catch(() => { });
324
+ };
325
+ // 日聚合表 usage_daily:首次启动回填 + 每日自愈。
326
+ // 首次:表为空但明细非空时全量回填历史数据;之后靠 writer 写时增量维护。
327
+ // 自愈:每日全量重建一次,纠正任何写时漂移。
328
+ import('./core/stats/db.js').then(({ getDb, rebuildDailyRollup }) => {
329
+ const db = getDb(paths.root);
330
+ if (!db)
331
+ return;
332
+ try {
333
+ const daily = db.prepare('SELECT COUNT(*) AS n FROM usage_daily').get();
334
+ const events = db.prepare('SELECT COUNT(*) AS n FROM usage_events').get();
335
+ if (daily.n === 0 && events.n > 0) {
336
+ logger.info('[Stats] usage_daily 为空,回填历史数据…');
337
+ rebuildDailyRollup(paths.root);
338
+ }
339
+ }
340
+ catch (e) {
341
+ logger.warn(`[Stats] usage_daily 回填检测失败(非致命): ${e}`);
342
+ }
343
+ // 每日自愈(24h),纠正写时增量漂移。
344
+ setInterval(() => {
345
+ try {
346
+ rebuildDailyRollup(paths.root);
347
+ }
348
+ catch (e) {
349
+ logger.warn(`[Stats] usage_daily 自愈失败(非致命): ${e}`);
350
+ }
351
+ }, 24 * 60 * 60 * 1000);
352
+ }).catch(() => { });
319
353
  // 初始化 SessionManager(文件系统后端)
320
354
  const sessionManager = new SessionManager(paths.sessionsDir, eventBus, (channel, userId) => agentRegistry.isOwner(channel, userId), (channel, userId) => agentRegistry.isAdmin(channel, userId));
321
355
  // sessionMode 解析:从 channel 路由到具体 agent,按 agent.config.chatmode
@@ -449,7 +483,7 @@ async function main() {
449
483
  const evol = evolagentName || primaryAgent.aid;
450
484
  const agent = agentMap.get(`${evol}::${baseagent}`)
451
485
  || agentMap.get(primaryRunnerKey);
452
- if (agent?.hasActiveStream(sessionKey)) {
486
+ if (agent) {
453
487
  await agent.interrupt(sessionKey);
454
488
  }
455
489
  });
@@ -497,6 +531,17 @@ async function main() {
497
531
  }
498
532
  }
499
533
  scheduler.setFireCallback((msg, trigger) => {
534
+ const onEnqueueFailed = (err) => {
535
+ const error = err instanceof Error ? err.message : String(err);
536
+ logger.error(`[Trigger] Enqueue failed ${trigger.id}: ${error}`);
537
+ eventBus.publish({
538
+ type: 'trigger:failed', triggerId: trigger.id, name: trigger.name,
539
+ messageId: msg.messageId || '', error,
540
+ targetChannel: trigger.targetChannel, targetChannelId: trigger.targetChannelId,
541
+ fireTime: msg.triggerMeta?.fireTime ?? Date.now(), phase: 'enqueue',
542
+ });
543
+ scheduler.onTriggerComplete(trigger.id, 'failed');
544
+ };
500
545
  if (trigger.targetSessionStrategy === 'current' && trigger.boundSessionId) {
501
546
  const boundId = trigger.boundSessionId;
502
547
  if (messageQueue.isProcessing(boundId)) {
@@ -509,12 +554,12 @@ async function main() {
509
554
  return;
510
555
  }
511
556
  messageQueue.enqueue(boundId, msg, bound.projectPath, { interruptible: false })
512
- .catch(err => logger.error(`[Trigger] Enqueue failed ${trigger.id}: ${err}`));
557
+ .catch(onEnqueueFailed);
513
558
  });
514
559
  return;
515
560
  }
516
561
  messageQueue.enqueue(`${msg.channel}:${msg.channelId}`, msg, primaryProjectPath, { interruptible: false })
517
- .catch(err => logger.error(`[Trigger] Enqueue failed ${trigger.id}: ${err}`));
562
+ .catch(onEnqueueFailed);
518
563
  });
519
564
  // Subscribe to trigger:completed/failed/skipped to update cron inflight state
520
565
  eventBus.subscribe('trigger:completed', (ev) => scheduler.onTriggerComplete(ev.triggerId, 'completed'));
@@ -523,6 +568,41 @@ async function main() {
523
568
  if (ev.reason === 'interrupted')
524
569
  scheduler.onTriggerComplete(ev.triggerId, 'interrupted');
525
570
  });
571
+ // ── Trigger 失败/跳过通知:向 targetChannel 发送告警消息 ──
572
+ eventBus.subscribe('trigger:failed', (ev) => {
573
+ const adapter = processor.getAdapter(ev.targetChannel);
574
+ if (!adapter)
575
+ return;
576
+ const phaseLabel = ev.phase === 'enqueue' ? '入队' : '执行';
577
+ const timeStr = ev.fireTime ? new Date(ev.fireTime).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) : '未知';
578
+ const text = `⚠️ 定时任务 [${ev.name || ev.triggerId}] 执行失败\n阶段:${phaseLabel}\n原因:${ev.error}\n触发时间:${timeStr}`;
579
+ const envelope = {
580
+ taskId: `trigger-notify:${ev.triggerId}`,
581
+ channel: ev.targetChannel,
582
+ channelId: ev.targetChannelId,
583
+ agentName: 'system',
584
+ chatmode: 'interactive',
585
+ timestamp: Date.now(),
586
+ };
587
+ adapter.send(envelope, { kind: 'result.text', text, isFinal: true, format: 'plain' }).catch(() => { });
588
+ });
589
+ eventBus.subscribe('trigger:skipped', (ev) => {
590
+ if (ev.reason !== 'overlap')
591
+ return;
592
+ const adapter = processor.getAdapter(ev.targetChannel);
593
+ if (!adapter)
594
+ return;
595
+ const text = `⚠️ 定时任务 [${ev.name || ev.triggerId}] 本次跳过(上次执行仍在进行中)`;
596
+ const envelope = {
597
+ taskId: `trigger-notify:${ev.triggerId}`,
598
+ channel: ev.targetChannel,
599
+ channelId: ev.targetChannelId,
600
+ agentName: 'system',
601
+ chatmode: 'interactive',
602
+ timestamp: Date.now(),
603
+ };
604
+ adapter.send(envelope, { kind: 'result.text', text, isFinal: true, format: 'plain' }).catch(() => { });
605
+ });
526
606
  // Note: only the primary agent's scheduler is wired to cmdHandler.
527
607
  // Non-primary agent channels will receive "⚠️ 触发器功能未启用" when using /trigger.
528
608
  // Full per-channel scheduler routing is a future improvement.
@@ -651,6 +731,7 @@ async function main() {
651
731
  createdByPeerId: '__system__',
652
732
  createdByChannel: '__system__',
653
733
  fireCount: 0,
734
+ failCount: 0,
654
735
  createdAt: Date.now(),
655
736
  updatedAt: Date.now(),
656
737
  };
@@ -700,6 +781,15 @@ async function main() {
700
781
  });
701
782
  }
702
783
  // ── 控制 AID(daemon 进程身份):pureIdentity 接入 AUN,独立于 evolagent ──
784
+ // 证书缺失检测/生成在 CLI 侧(evolclaw start)完成。daemon 是后台进程无终端,
785
+ // 这里只做兜底:证书缺失时 warn 并继续(AUNChannel 内部后台重连),绝不阻塞。
786
+ if (evolclawCfg.aid) {
787
+ const aunPath = resolvePaths().root;
788
+ const certKey = path.join(aunPath, 'AIDs', evolclawCfg.aid, 'private', 'key.json');
789
+ if (!fs.existsSync(certKey)) {
790
+ logger.warn(`控制 AID 证书缺失:${evolclawCfg.aid}(AUN 控制通道后台重连;如需重建运行 evolclaw init)`);
791
+ }
792
+ }
703
793
  let controlChannel;
704
794
  if (evolclawCfg.aid) {
705
795
  controlChannel = new AUNChannel({
@@ -719,6 +809,37 @@ async function main() {
719
809
  catch (e) {
720
810
  logger.warn(`控制 AID 首连失败(后台自动重连,不影响 daemon 主流程): ${e?.message || e}`);
721
811
  }
812
+ // 控制 AID 接收 owner 指令:本轮只做 ECWeb 登录入口(/pair 取配对码)。
813
+ // 发送方身份由 AUN X.509 证书链验证,非 owner 完全静默。daemon 直接处理,
814
+ // 不转回 ecweb——仅「配对码是 ecweb 持有的状态」需经 localhost 取一次。
815
+ controlChannel.onMessage(async (opts) => {
816
+ try {
817
+ if (!isProcessLevelOwner(opts.peerId, evolclawCfg.owners)) {
818
+ logger.debug(`控制 AID 收到非 owner 消息,忽略: from=${opts.peerId}`);
819
+ return;
820
+ }
821
+ const text = (opts.content || '').trim();
822
+ if (text.toLowerCase() === '/pair') {
823
+ const port = evolclawCfg.ecweb?.port ?? 42705;
824
+ const pair = await fetchEcwebPairCode(port);
825
+ let reply;
826
+ if (pair) {
827
+ const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
828
+ reply = `ECWeb 配对码:${pair.code}(约 ${mins} 分钟内有效)\n在浏览器打开 ECWeb 后输入此码登录`;
829
+ }
830
+ else {
831
+ reply = 'ECWeb 未运行或暂不可达。请在主机运行 ec watch web 启动后重试。';
832
+ }
833
+ await controlChannel.sendMessage(opts.channelId, reply);
834
+ return;
835
+ }
836
+ // owner 发的其他内容:提示可用指令,避免无响应让 owner 困惑
837
+ await controlChannel.sendMessage(opts.channelId, '可用指令:/pair(获取 ECWeb 登录配对码)');
838
+ }
839
+ catch (e) {
840
+ logger.warn(`控制 AID 消息处理失败: ${e?.message || e}`);
841
+ }
842
+ });
722
843
  }
723
844
  // 上线通知:延迟 1-3 秒后向 owner 发送上线消息(带 name + 工作目录)
724
845
  // 需在配置中 debug.upmsg: true 手动开启
@@ -934,6 +1055,7 @@ async function main() {
934
1055
  }, async (cmd, sessionId) => cmdHandler.handleCtl(cmd, sessionId));
935
1056
  // M3: direct call (not cast) — wire EvolAgentRegistry into IPC for evolagent.* handlers
936
1057
  ipcServer.setAgentRegistry(agentRegistry);
1058
+ ipcServer.setMenuExecutor((payload) => cmdHandler.execMenuForEcweb(payload));
937
1059
  // 注入 AUN AID 状态聚合器:遍历所有 aun 类型 channel,调 getAidState() 收集
938
1060
  ipcServer.setAunAidProvider(() => {
939
1061
  const out = [];
package/dist/ipc.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import net from 'net';
2
2
  import fs from 'fs';
3
3
  import { logger } from './utils/logger.js';
4
- import { fileCache } from './core/cache/file-cache.js';
4
+ import { fileCache } from './core/daemon-file-cache.js';
5
5
  const isWindows = process.platform === 'win32';
6
6
  const isNamedPipe = (p) => isWindows && p.startsWith('\\\\.\\pipe\\');
7
7
  export class IpcServer {
@@ -13,6 +13,7 @@ export class IpcServer {
13
13
  aunAidProvider;
14
14
  aunAidStatsProvider;
15
15
  aunAidStatsRecorder;
16
+ menuExecutor;
16
17
  constructor(socketPath, getStatus, commandExecutor) {
17
18
  this.socketPath = socketPath;
18
19
  this.getStatus = getStatus;
@@ -22,6 +23,10 @@ export class IpcServer {
22
23
  setAgentRegistry(registry) {
23
24
  this.agentRegistry = registry;
24
25
  }
26
+ /** Inject menu.* executor (ECWeb Control proxies menu requests through this) */
27
+ setMenuExecutor(executor) {
28
+ this.menuExecutor = executor;
29
+ }
25
30
  /** Inject AUN AID state aggregator for aun-aids IPC handler */
26
31
  setAunAidProvider(provider) {
27
32
  this.aunAidProvider = provider;
@@ -206,6 +211,17 @@ export class IpcServer {
206
211
  return { ok: false, error: e?.message || String(e) };
207
212
  }
208
213
  }
214
+ case 'menu.exec': {
215
+ if (!this.menuExecutor)
216
+ return { ok: false, error: 'menu.exec not configured' };
217
+ try {
218
+ const response = await this.menuExecutor(cmd.payload);
219
+ return { ok: true, response };
220
+ }
221
+ catch (e) {
222
+ return { ok: false, error: e?.message ?? String(e) };
223
+ }
224
+ }
209
225
  default:
210
226
  return { error: `unknown command: ${cmd.type}` };
211
227
  }