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.
- package/CHANGELOG.md +53 -0
- package/README.md +7 -4
- package/dist/agents/{resolve.js → baseagent.js} +34 -5
- package/dist/agents/claude-runner.js +120 -31
- package/dist/agents/codex-app-server-client.js +364 -0
- package/dist/agents/codex-runner.js +1152 -140
- package/dist/agents/gemini-runner.js +2 -2
- package/dist/agents/runner-types.js +58 -0
- package/dist/aun/aid/store.js +1 -1
- package/dist/aun/outbox.js +14 -2
- package/dist/aun/storage/download.js +1 -1
- package/dist/aun/storage/upload.js +13 -1
- package/dist/channels/aun.js +869 -358
- package/dist/channels/dingtalk.js +77 -140
- package/dist/channels/feishu.js +125 -154
- 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-command.js +591 -0
- package/dist/cli/agent.js +23 -8
- package/dist/cli/aun-commands.js +1444 -0
- package/dist/cli/ctl-command.js +78 -0
- package/dist/cli/daemon-commands.js +2707 -0
- package/dist/cli/index.js +23 -4905
- package/dist/cli/init.js +33 -6
- package/dist/cli/model.js +1 -1
- package/dist/cli/restart-monitor.js +539 -0
- package/dist/cli/stats.js +558 -0
- package/dist/cli/version.js +87 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +5 -2
- package/dist/config-store.js +12 -6
- package/dist/core/channel-loader.js +88 -83
- package/dist/core/command/command-handler.js +1189 -0
- package/dist/core/command/menu-handler.js +1478 -0
- package/dist/core/command/slash-gate.js +142 -0
- package/dist/core/command/slash-handler.js +2090 -0
- package/dist/core/evolagent-registry.js +82 -0
- package/dist/core/evolagent.js +17 -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 +91 -51
- package/dist/core/message/items-formatter.js +9 -1
- package/dist/core/message/message-bridge.js +73 -24
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-processor.js +432 -94
- package/dist/core/message/message-queue.js +70 -2
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-scope.js +2 -2
- package/dist/core/permission.js +25 -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 +86 -26
- 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 +334 -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/data/error-dict.json +7 -0
- package/dist/{agents → eck}/manifest-engine.js +20 -1
- package/dist/{agents → eck}/message-renderer.js +24 -1
- package/dist/index.js +174 -9
- package/dist/ipc.js +116 -1
- package/dist/utils/cross-platform.js +58 -5
- package/dist/utils/ecweb-launch.js +49 -0
- package/dist/utils/ecweb-pair.js +20 -0
- package/dist/utils/error-utils.js +18 -5
- package/dist/utils/npm-ops.js +38 -8
- package/dist/utils/stats.js +77 -6
- 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_message_manifest.json +30 -3
- package/kits/rules/05-venue.md +1 -1
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/package.json +5 -6
- package/dist/agents/baseagent-normalize.js +0 -19
- package/dist/core/command-handler.js +0 -3876
- 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, 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
|
-
|
|
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 = {
|
|
@@ -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/
|
|
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,
|