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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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(
|
|
206
|
-
if (
|
|
207
|
-
this.inflightCron.add(
|
|
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(
|
|
210
|
-
this.manager.updateNextFireAt(
|
|
211
|
-
this.heap.push({ ...
|
|
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(
|
|
218
|
+
this.manager.moveToDone(fresh.id, 'fired');
|
|
216
219
|
}
|
|
217
|
-
this.eventBus.publish({ type: 'trigger:fired', triggerId:
|
|
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,
|
|
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,
|
|
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/
|
|
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/
|
|
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 './
|
|
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
|
|
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(
|
|
557
|
+
.catch(onEnqueueFailed);
|
|
513
558
|
});
|
|
514
559
|
return;
|
|
515
560
|
}
|
|
516
561
|
messageQueue.enqueue(`${msg.channel}:${msg.channelId}`, msg, primaryProjectPath, { interruptible: false })
|
|
517
|
-
.catch(
|
|
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/
|
|
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
|
}
|