evolclaw-web 1.0.1 → 1.2.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.
@@ -25,7 +25,16 @@ function dlog(line) { if (_dlog)
25
25
  const CACHE_VERSION = 2;
26
26
  const _metaCache = new Map();
27
27
  function cacheDir(encoded) {
28
- return path.join(resolvePaths().dataDir, 'watch-web-cache', encoded);
28
+ const dataDir = resolvePaths().dataDir;
29
+ const currentRoot = path.join(dataDir, 'ecweb-cache');
30
+ const legacyRoot = path.join(dataDir, 'watch-web-cache');
31
+ if (!fs.existsSync(currentRoot) && fs.existsSync(legacyRoot)) {
32
+ try {
33
+ fs.renameSync(legacyRoot, currentRoot);
34
+ }
35
+ catch { }
36
+ }
37
+ return path.join(currentRoot, encoded);
29
38
  }
30
39
  function readDiskCache(encoded, id, mtime, size) {
31
40
  try {
@@ -0,0 +1,348 @@
1
+ /**
2
+ * ecweb/src/sources/stats.ts — Stats 数据源,直接只读查 usage.db。
3
+ */
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+ import { createRequire } from 'module';
7
+ import { resolvePaths } from '../paths.js';
8
+ const requireFromHere = createRequire(import.meta.url);
9
+ let sqliteModule;
10
+ function loadSqlite() {
11
+ if (sqliteModule !== undefined)
12
+ return sqliteModule;
13
+ try {
14
+ sqliteModule = requireFromHere('node:sqlite');
15
+ }
16
+ catch {
17
+ sqliteModule = null;
18
+ }
19
+ return sqliteModule;
20
+ }
21
+ function getDbPath() {
22
+ const { root } = resolvePaths();
23
+ return path.join(root, 'data', 'stats', 'usage.db');
24
+ }
25
+ function openDb() {
26
+ const sqlite = loadSqlite();
27
+ if (!sqlite)
28
+ return null;
29
+ const dbPath = getDbPath();
30
+ if (!fs.existsSync(dbPath))
31
+ return null;
32
+ try {
33
+ return new sqlite.DatabaseSync(dbPath, { readOnly: true });
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ let _priceCache = null;
40
+ let _aliasCache = null;
41
+ let _priceCacheTs = 0;
42
+ const PRICE_CACHE_TTL = 5 * 60 * 1000;
43
+ function _loadPrices() {
44
+ const now = Date.now();
45
+ if (_priceCache && now - _priceCacheTs < PRICE_CACHE_TTL)
46
+ return _priceCache;
47
+ const { root } = resolvePaths();
48
+ const file = path.join(root, 'data', 'stats', 'model-prices.jsonl');
49
+ if (!fs.existsSync(file)) {
50
+ _priceCache = [];
51
+ _priceCacheTs = now;
52
+ return [];
53
+ }
54
+ try {
55
+ _priceCache = fs.readFileSync(file, 'utf-8').split('\n').filter(Boolean).map(l => JSON.parse(l));
56
+ _priceCacheTs = now;
57
+ return _priceCache;
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }
63
+ function _loadAliases() {
64
+ const now = Date.now();
65
+ if (_aliasCache && now - _priceCacheTs < PRICE_CACHE_TTL)
66
+ return _aliasCache;
67
+ const { root } = resolvePaths();
68
+ const file = path.join(root, 'data', 'stats', 'model-aliases.jsonl');
69
+ if (!fs.existsSync(file)) {
70
+ _aliasCache = [];
71
+ return [];
72
+ }
73
+ try {
74
+ _aliasCache = fs.readFileSync(file, 'utf-8').split('\n').filter(Boolean).map(l => JSON.parse(l));
75
+ return _aliasCache;
76
+ }
77
+ catch {
78
+ return [];
79
+ }
80
+ }
81
+ function _resolvePrice(model, ts) {
82
+ const prices = _loadPrices();
83
+ let candidates = prices.filter(p => p.model === model && p.effective_from <= ts);
84
+ if (!candidates.length) {
85
+ const aliases = _loadAliases();
86
+ const entry = aliases.find(a => a.alias === model);
87
+ if (entry)
88
+ candidates = prices.filter(p => p.model === entry.canonical && p.effective_from <= ts);
89
+ }
90
+ if (!candidates.length)
91
+ return null;
92
+ return candidates.reduce((a, b) => a.effective_from >= b.effective_from ? a : b);
93
+ }
94
+ function _calcRowCost(row) {
95
+ const p = _resolvePrice(row.model, row.ts);
96
+ if (!p)
97
+ return { usd: 0, cny: 0 };
98
+ let cost = 0;
99
+ switch (p.billing_fn) {
100
+ case 'per_token_v1':
101
+ cost = ((p.price_input ?? 0) * (row.input_tokens ?? 0)
102
+ + (p.price_output ?? 0) * (row.output_tokens ?? 0)
103
+ + (p.price_cache_creation ?? 0) * (row.cache_creation_tokens ?? 0)
104
+ + (p.price_cache_read ?? 0) * (row.cache_read_tokens ?? 0)) / 1e6;
105
+ break;
106
+ case 'per_token_deepseek_v1':
107
+ cost = ((p.price_cache_hit ?? 0) * (row.cache_hit_tokens ?? 0)
108
+ + (p.price_cache_miss ?? 0) * (row.cache_miss_tokens ?? 0)
109
+ + (p.price_output ?? 0) * (row.output_tokens ?? 0)) / 1e6;
110
+ break;
111
+ case 'per_token_tiered_v1': {
112
+ const tiers = p.tiers;
113
+ if (!Array.isArray(tiers))
114
+ break;
115
+ const ctx = row.total_context_tokens ?? row.input_tokens ?? 0;
116
+ const tier = tiers.find(t => t.up_to_tokens == null || ctx <= t.up_to_tokens) ?? tiers[tiers.length - 1];
117
+ cost = ((tier.price_input ?? 0) * (row.input_tokens ?? 0)
118
+ + (tier.price_output ?? 0) * (row.output_tokens ?? 0)
119
+ + (tier.price_cache_read ?? 0) * (row.cache_read_tokens ?? 0)) / 1e6;
120
+ break;
121
+ }
122
+ case 'per_token_image_v1':
123
+ cost = ((p.price_input ?? 0) * (row.input_tokens ?? 0)
124
+ + (p.price_output ?? 0) * (row.output_tokens ?? 0)
125
+ + (p.price_image ?? 0) * (row.image_tokens ?? 0)) / 1e6;
126
+ break;
127
+ }
128
+ return p.currency === 'CNY' ? { usd: 0, cny: cost } : { usd: cost, cny: 0 };
129
+ }
130
+ export function queryStatsForDashboard() {
131
+ const db = openDb();
132
+ if (!db)
133
+ return null;
134
+ const now = new Date();
135
+ const todayStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
136
+ const h24ago = Date.now() - 24 * 60 * 60 * 1000;
137
+ try {
138
+ // Today summary
139
+ const todayRow = db.prepare(`
140
+ SELECT
141
+ COALESCE(SUM(input_tokens),0) AS input_tokens,
142
+ COALESCE(SUM(output_tokens),0) AS output_tokens,
143
+ COALESCE(SUM(cache_creation_tokens),0) AS cache_creation_tokens,
144
+ COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
145
+ COUNT(*) AS call_count
146
+ FROM usage_events WHERE ts >= ?
147
+ `).get(todayStart);
148
+ const totalIn = (todayRow.input_tokens ?? 0) + (todayRow.cache_read_tokens ?? 0);
149
+ const hitRate = totalIn > 0 ? (todayRow.cache_read_tokens ?? 0) / totalIn : 0;
150
+ // Today cost (逐行计算)
151
+ let costUsd = 0, costCny = 0;
152
+ const costRows = db.prepare(`SELECT * FROM usage_events WHERE ts >= ?`).all(todayStart);
153
+ for (const r of costRows) {
154
+ const c = _calcRowCost(r);
155
+ costUsd += c.usd;
156
+ costCny += c.cny;
157
+ }
158
+ // Hourly (last 24h)
159
+ const hourly = db.prepare(`
160
+ SELECT
161
+ strftime('%Y-%m-%d %H:00', ts/1000, 'unixepoch', 'localtime') AS hour,
162
+ COALESCE(SUM(input_tokens),0) AS input_tokens,
163
+ COALESCE(SUM(output_tokens),0) AS output_tokens,
164
+ COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
165
+ COUNT(*) AS call_count
166
+ FROM usage_events WHERE ts >= ?
167
+ GROUP BY hour ORDER BY hour
168
+ `).all(h24ago);
169
+ // Top models (today)
170
+ const top_models = db.prepare(`
171
+ SELECT model, SUM(input_tokens+output_tokens) AS total_tokens, COUNT(*) AS call_count
172
+ FROM usage_events WHERE ts >= ?
173
+ GROUP BY model ORDER BY total_tokens DESC LIMIT 10
174
+ `).all(todayStart);
175
+ // Top peers (today)
176
+ const top_peers = db.prepare(`
177
+ SELECT peer_key, SUM(input_tokens+output_tokens) AS total_tokens, COUNT(*) AS call_count
178
+ FROM usage_events WHERE ts >= ?
179
+ GROUP BY peer_key ORDER BY total_tokens DESC LIMIT 5
180
+ `).all(todayStart);
181
+ return {
182
+ today: { ...todayRow, cache_hit_rate: hitRate, cost_usd: costUsd, cost_cny: costCny },
183
+ hourly,
184
+ top_models,
185
+ top_peers,
186
+ };
187
+ }
188
+ finally {
189
+ db.close();
190
+ }
191
+ }
192
+ export function queryStatsExplorer(params) {
193
+ const db = openDb();
194
+ if (!db)
195
+ return [];
196
+ const gran = params.granularity || 'day';
197
+ const fmt = { hour: '%Y-%m-%d %H:00', day: '%Y-%m-%d', week: '%Y-W%W', month: '%Y-%m' };
198
+ const strfmt = fmt[gran] || fmt.day;
199
+ const conds = [];
200
+ const p = [];
201
+ if (params.from_ts) {
202
+ conds.push('ts >= ?');
203
+ p.push(params.from_ts);
204
+ }
205
+ if (params.to_ts) {
206
+ conds.push('ts < ?');
207
+ p.push(params.to_ts);
208
+ }
209
+ if (params.agent_aid) {
210
+ conds.push('agent_aid = ?');
211
+ p.push(params.agent_aid);
212
+ }
213
+ if (params.peer_key) {
214
+ conds.push('peer_key = ?');
215
+ p.push(params.peer_key);
216
+ }
217
+ if (params.model) {
218
+ conds.push('model = ?');
219
+ p.push(params.model);
220
+ }
221
+ const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
222
+ try {
223
+ return db.prepare(`
224
+ SELECT
225
+ strftime('${strfmt}', ts/1000, 'unixepoch', 'localtime') AS period,
226
+ COALESCE(SUM(input_tokens),0) AS input_tokens,
227
+ COALESCE(SUM(output_tokens),0) AS output_tokens,
228
+ COALESCE(SUM(cache_creation_tokens),0) AS cache_creation_tokens,
229
+ COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
230
+ COUNT(*) AS call_count
231
+ FROM usage_events ${where}
232
+ GROUP BY period ORDER BY period
233
+ `).all(...p);
234
+ }
235
+ finally {
236
+ db.close();
237
+ }
238
+ }
239
+ /** 按 peer 分组聚合(支持时间范围过滤)。 */
240
+ export function queryStatsByPeer(params) {
241
+ const db = openDb();
242
+ if (!db)
243
+ return [];
244
+ const conds = [];
245
+ const p = [];
246
+ if (params.from_ts) {
247
+ conds.push('ts >= ?');
248
+ p.push(params.from_ts);
249
+ }
250
+ if (params.to_ts) {
251
+ conds.push('ts < ?');
252
+ p.push(params.to_ts);
253
+ }
254
+ if (params.agent_aid) {
255
+ conds.push('agent_aid = ?');
256
+ p.push(params.agent_aid);
257
+ }
258
+ const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
259
+ const limit = params.limit ?? 50;
260
+ try {
261
+ return db.prepare(`
262
+ SELECT peer_key, peer_type,
263
+ COALESCE(SUM(input_tokens),0) AS input_tokens,
264
+ COALESCE(SUM(output_tokens),0) AS output_tokens,
265
+ COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
266
+ COUNT(*) AS call_count
267
+ FROM usage_events ${where}
268
+ GROUP BY peer_key ORDER BY (input_tokens+output_tokens) DESC LIMIT ${limit}
269
+ `).all(...p);
270
+ }
271
+ finally {
272
+ db.close();
273
+ }
274
+ }
275
+ export function queryStatsOverview() {
276
+ const db = openDb();
277
+ if (!db)
278
+ return null;
279
+ try {
280
+ const allRows = db.prepare('SELECT * FROM usage_events').all();
281
+ let totIn = 0, totOut = 0, totCc = 0, totCr = 0, totCalls = 0, totUsd = 0, totCny = 0;
282
+ const byAgent = new Map();
283
+ for (const r of allRows) {
284
+ totIn += r.input_tokens ?? 0;
285
+ totOut += r.output_tokens ?? 0;
286
+ totCc += r.cache_creation_tokens ?? 0;
287
+ totCr += r.cache_read_tokens ?? 0;
288
+ totCalls++;
289
+ const { usd, cny } = _calcRowCost(r);
290
+ totUsd += usd;
291
+ totCny += cny;
292
+ const aid = r.agent_aid || '';
293
+ let a = byAgent.get(aid);
294
+ if (!a) {
295
+ a = { input_tokens: 0, output_tokens: 0, cache_creation_tokens: 0, cache_read_tokens: 0, call_count: 0, cost_usd: 0, cost_cny: 0 };
296
+ byAgent.set(aid, a);
297
+ }
298
+ a.input_tokens += r.input_tokens ?? 0;
299
+ a.output_tokens += r.output_tokens ?? 0;
300
+ a.cache_creation_tokens += r.cache_creation_tokens ?? 0;
301
+ a.cache_read_tokens += r.cache_read_tokens ?? 0;
302
+ a.call_count++;
303
+ a.cost_usd += usd;
304
+ a.cost_cny += cny;
305
+ }
306
+ return {
307
+ all_time: { input_tokens: totIn, output_tokens: totOut, cache_creation_tokens: totCc, cache_read_tokens: totCr, call_count: totCalls, cost_usd: totUsd, cost_cny: totCny },
308
+ by_agent: Array.from(byAgent.entries())
309
+ .map(([agent_aid, v]) => ({ agent_aid, ...v }))
310
+ .sort((a, b) => (b.input_tokens + b.output_tokens) - (a.input_tokens + a.output_tokens)),
311
+ };
312
+ }
313
+ finally {
314
+ db.close();
315
+ }
316
+ }
317
+ /** 按 agent 分组聚合(支持时间范围过滤)。 */
318
+ export function queryStatsByAgent(params) {
319
+ const db = openDb();
320
+ if (!db)
321
+ return [];
322
+ const conds = [];
323
+ const p = [];
324
+ if (params.from_ts) {
325
+ conds.push('ts >= ?');
326
+ p.push(params.from_ts);
327
+ }
328
+ if (params.to_ts) {
329
+ conds.push('ts < ?');
330
+ p.push(params.to_ts);
331
+ }
332
+ const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
333
+ const limit = params.limit ?? 50;
334
+ try {
335
+ return db.prepare(`
336
+ SELECT agent_aid,
337
+ COALESCE(SUM(input_tokens),0) AS input_tokens,
338
+ COALESCE(SUM(output_tokens),0) AS output_tokens,
339
+ COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
340
+ COUNT(*) AS call_count
341
+ FROM usage_events ${where}
342
+ GROUP BY agent_aid ORDER BY (input_tokens+output_tokens) DESC LIMIT ${limit}
343
+ `).all(...p);
344
+ }
345
+ finally {
346
+ db.close();
347
+ }
348
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * System 数据源 — 纯进程级看板。
3
+ *
4
+ * snapshot: menu.list(探活)+ menu.query name=system(version/fastaunVersion/uptime/pid/node)。
5
+ * 另注入 ecwebVersion(ECWeb 进程自身 package.json 版本,由 server.ts 合并,不走 daemon IPC)。
6
+ * check / upgrade 结果不在快照里——由前端按钮触发 menu.action 后写入 state。
7
+ *
8
+ * subscribe: 30s 轮询 + JSON diff,仅变化时 push。
9
+ */
10
+ import { resolvePaths } from '../paths.js';
11
+ import { ipcQuery } from '../ipc-client.js';
12
+ async function menuExec(payload) {
13
+ const p = resolvePaths();
14
+ const r = await ipcQuery(p.socket, { type: 'menu.exec', payload }, 5000);
15
+ return r?.ok ? r.response : null;
16
+ }
17
+ async function buildSnapshot() {
18
+ const [listResp, sysResp] = await Promise.all([
19
+ menuExec({ type: 'menu.list', id: 'sys-list' }),
20
+ menuExec({ type: 'menu.query', id: 'sys-q', name: 'system' }),
21
+ ]);
22
+ const daemonRunning = listResp !== null;
23
+ // sysResp 形如 { type:'menu.response', id, name, data | error }
24
+ const system = sysResp?.data ?? null;
25
+ return { daemonRunning, system, upgrade: null, check: null };
26
+ }
27
+ export const systemSource = {
28
+ kind: 'system',
29
+ async snapshot() {
30
+ return buildSnapshot();
31
+ },
32
+ subscribe(_params, push) {
33
+ let lastJson = '';
34
+ let stopped = false;
35
+ const tick = async () => {
36
+ if (stopped)
37
+ return;
38
+ try {
39
+ const snap = await buildSnapshot();
40
+ const json = JSON.stringify(snap);
41
+ if (json !== lastJson) {
42
+ lastJson = json;
43
+ push(snap);
44
+ }
45
+ }
46
+ catch { /* ignore transient IPC errors */ }
47
+ };
48
+ const timer = setInterval(tick, 30000);
49
+ return () => { stopped = true; clearInterval(timer); };
50
+ },
51
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Triggers 数据源 — 按 agent 钻取触发器。
3
+ *
4
+ * snapshot:
5
+ * - agents: menu.options name=agent(agent 列表,同 Agents 页数据源)
6
+ * - triggers: 选中 agent 时 menu.options name=trigger(带 options=all),并带 agent 参数解析其 triggerManager
7
+ *
8
+ * subscribe: 2s 轮询 + JSON diff,仅变化时 push。
9
+ */
10
+ import { resolvePaths } from '../paths.js';
11
+ import { ipcQuery } from '../ipc-client.js';
12
+ async function menuExec(payload) {
13
+ const p = resolvePaths();
14
+ const r = await ipcQuery(p.socket, { type: 'menu.exec', payload }, 5000);
15
+ return r?.ok ? r.response : null;
16
+ }
17
+ async function buildSnapshot(params) {
18
+ const agent = params?.agent ?? null;
19
+ const [agentsResp, triggersResp] = await Promise.all([
20
+ menuExec({ type: 'menu.options', id: 'tr-agents', name: 'agent' }),
21
+ agent
22
+ ? menuExec({ type: 'menu.options', id: 'tr-list', name: 'trigger', args: { options: 'all' }, agent })
23
+ : Promise.resolve(null),
24
+ ]);
25
+ // menu.options 响应形如 { type:'menu.response', id, name, data: MenuItem[] }
26
+ const agents = Array.isArray(agentsResp?.data) ? agentsResp.data : [];
27
+ const triggers = Array.isArray(triggersResp?.data) ? triggersResp.data : [];
28
+ return { agents, triggers, selectedAgent: agent };
29
+ }
30
+ export const triggersSource = {
31
+ kind: 'triggers',
32
+ async snapshot(params) {
33
+ return buildSnapshot(params ?? {});
34
+ },
35
+ subscribe(params, push) {
36
+ let lastJson = '';
37
+ let stopped = false;
38
+ const tick = async () => {
39
+ if (stopped)
40
+ return;
41
+ try {
42
+ const snap = await buildSnapshot(params);
43
+ const json = JSON.stringify(snap);
44
+ if (json !== lastJson) {
45
+ lastJson = json;
46
+ push(snap);
47
+ }
48
+ }
49
+ catch { /* ignore transient IPC errors */ }
50
+ };
51
+ const timer = setInterval(tick, 2000);
52
+ return () => { stopped = true; clearInterval(timer); };
53
+ },
54
+ };
@@ -5,6 +5,6 @@
5
5
  * - snapshot(params): 当前全量快照
6
6
  * - subscribe(params, push): 注册变更回调,返回取消订阅函数
7
7
  *
8
- * aid 走 IPC 轮询(无推送),msg/session 走 fs.watch 文件监听。
8
+ * aid/cache 走 IPC 轮询(无推送),msg/session 走 fs.watch 文件监听。
9
9
  */
10
10
  export {};