evolclaw-web 1.2.0 → 1.2.2

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.
@@ -5,6 +5,7 @@ import path from 'path';
5
5
  import fs from 'fs';
6
6
  import { createRequire } from 'module';
7
7
  import { resolvePaths } from '../paths.js';
8
+ import { encodeSegment } from '../fs-utils.js';
8
9
  const requireFromHere = createRequire(import.meta.url);
9
10
  let sqliteModule;
10
11
  function loadSqlite() {
@@ -36,97 +37,6 @@ function openDb() {
36
37
  return null;
37
38
  }
38
39
  }
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
40
  export function queryStatsForDashboard() {
131
41
  const db = openDb();
132
42
  if (!db)
@@ -135,26 +45,20 @@ export function queryStatsForDashboard() {
135
45
  const todayStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
136
46
  const h24ago = Date.now() - 24 * 60 * 60 * 1000;
137
47
  try {
138
- // Today summary
48
+ // Today summary with cost
139
49
  const todayRow = db.prepare(`
140
50
  SELECT
141
51
  COALESCE(SUM(input_tokens),0) AS input_tokens,
142
52
  COALESCE(SUM(output_tokens),0) AS output_tokens,
143
53
  COALESCE(SUM(cache_creation_tokens),0) AS cache_creation_tokens,
144
54
  COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
145
- COUNT(*) AS call_count
55
+ COUNT(*) AS call_count,
56
+ COALESCE(SUM(cost_gateway_usd),0) AS cost_usd,
57
+ COALESCE(SUM(cost_gateway_cny),0) AS cost_cny
146
58
  FROM usage_events WHERE ts >= ?
147
59
  `).get(todayStart);
148
60
  const totalIn = (todayRow.input_tokens ?? 0) + (todayRow.cache_read_tokens ?? 0);
149
61
  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
62
  // Hourly (last 24h)
159
63
  const hourly = db.prepare(`
160
64
  SELECT
@@ -179,7 +83,7 @@ export function queryStatsForDashboard() {
179
83
  GROUP BY peer_key ORDER BY total_tokens DESC LIMIT 5
180
84
  `).all(todayStart);
181
85
  return {
182
- today: { ...todayRow, cache_hit_rate: hitRate, cost_usd: costUsd, cost_cny: costCny },
86
+ today: { ...todayRow, cache_hit_rate: hitRate },
183
87
  hourly,
184
88
  top_models,
185
89
  top_peers,
@@ -258,62 +162,129 @@ export function queryStatsByPeer(params) {
258
162
  const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
259
163
  const limit = params.limit ?? 50;
260
164
  try {
261
- return db.prepare(`
165
+ const rows = db.prepare(`
262
166
  SELECT peer_key, peer_type,
263
167
  COALESCE(SUM(input_tokens),0) AS input_tokens,
264
168
  COALESCE(SUM(output_tokens),0) AS output_tokens,
265
169
  COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
266
170
  COUNT(*) AS call_count
267
171
  FROM usage_events ${where}
268
- GROUP BY peer_key ORDER BY (input_tokens+output_tokens) DESC LIMIT ${limit}
172
+ GROUP BY peer_key ORDER BY (COALESCE(SUM(input_tokens),0) + COALESCE(SUM(output_tokens),0)) DESC LIMIT ${limit}
269
173
  `).all(...p);
174
+ // 为每个peer添加名称和详细信息
175
+ return rows.map((row) => {
176
+ const peerInfo = getPeerInfo(row.peer_key);
177
+ return {
178
+ ...row,
179
+ peer_name: peerInfo.name,
180
+ peer_chat_type: peerInfo.chatType,
181
+ peer_group_member_count: peerInfo.memberCount
182
+ };
183
+ });
270
184
  }
271
185
  finally {
272
186
  db.close();
273
187
  }
274
188
  }
275
- export function queryStatsOverview() {
189
+ export function queryStatsOverview(params) {
276
190
  const db = openDb();
277
191
  if (!db)
278
192
  return null;
279
193
  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;
194
+ // 构建WHERE条件
195
+ const conds = [];
196
+ const p = [];
197
+ if (params?.from_ts) {
198
+ conds.push('ts >= ?');
199
+ p.push(params.from_ts);
200
+ }
201
+ if (params?.to_ts) {
202
+ conds.push('ts <= ?');
203
+ p.push(params.to_ts);
204
+ }
205
+ if (params?.agent_aid) {
206
+ conds.push('agent_aid = ?');
207
+ p.push(params.agent_aid);
208
+ }
209
+ if (params?.peer_key) {
210
+ conds.push('peer_key = ?');
211
+ p.push(params.peer_key);
305
212
  }
213
+ const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
214
+ // Token and cost aggregation with filters
215
+ const allRow = db.prepare(`
216
+ SELECT
217
+ COALESCE(SUM(input_tokens),0) AS input_tokens,
218
+ COALESCE(SUM(output_tokens),0) AS output_tokens,
219
+ COALESCE(SUM(cache_creation_tokens),0) AS cache_creation_tokens,
220
+ COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
221
+ COUNT(*) AS call_count,
222
+ COALESCE(SUM(cost_official_usd),0) AS cost_official_usd,
223
+ COALESCE(SUM(cost_official_cny),0) AS cost_official_cny,
224
+ COALESCE(SUM(cost_gateway_usd),0) AS cost_usd,
225
+ COALESCE(SUM(cost_gateway_cny),0) AS cost_cny
226
+ FROM usage_events ${where}
227
+ `).get(...p);
228
+ // By agent aggregation with filters
229
+ const byAgentRows = db.prepare(`
230
+ SELECT agent_aid,
231
+ COALESCE(SUM(input_tokens),0) AS input_tokens,
232
+ COALESCE(SUM(output_tokens),0) AS output_tokens,
233
+ COALESCE(SUM(cache_creation_tokens),0) AS cache_creation_tokens,
234
+ COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
235
+ COUNT(*) AS call_count,
236
+ COALESCE(SUM(cost_official_usd),0) AS cost_official_usd,
237
+ COALESCE(SUM(cost_official_cny),0) AS cost_official_cny,
238
+ COALESCE(SUM(cost_gateway_usd),0) AS cost_usd,
239
+ COALESCE(SUM(cost_gateway_cny),0) AS cost_cny
240
+ FROM usage_events ${where}
241
+ GROUP BY agent_aid
242
+ ORDER BY (COALESCE(SUM(input_tokens),0) + COALESCE(SUM(output_tokens),0)) DESC
243
+ `).all(...p);
244
+ // 为每个agent添加名称
245
+ const byAgentWithNames = byAgentRows.map((row) => ({
246
+ ...row,
247
+ agent_name: getAgentName(row.agent_aid)
248
+ }));
306
249
  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)),
250
+ all_time: allRow,
251
+ by_agent: byAgentWithNames,
311
252
  };
312
253
  }
313
254
  finally {
314
255
  db.close();
315
256
  }
316
257
  }
258
+ /** 获取所有本地agent列表 */
259
+ export function getAllLocalAgents() {
260
+ try {
261
+ const root = resolvePaths().root;
262
+ const aidsDir = path.join(root, 'AIDs');
263
+ if (!fs.existsSync(aidsDir))
264
+ return [];
265
+ const agentDirs = fs.readdirSync(aidsDir);
266
+ const agents = [];
267
+ for (const agentAid of agentDirs) {
268
+ // 跳过非目录项
269
+ const agentPath = path.join(aidsDir, agentAid);
270
+ if (!fs.statSync(agentPath).isDirectory())
271
+ continue;
272
+ // 获取agent名称
273
+ const agentName = getAgentName(agentAid);
274
+ agents.push({ agent_aid: agentAid, agent_name: agentName });
275
+ }
276
+ // 按名称排序
277
+ agents.sort((a, b) => {
278
+ const nameA = a.agent_name || a.agent_aid;
279
+ const nameB = b.agent_name || b.agent_aid;
280
+ return nameA.localeCompare(nameB);
281
+ });
282
+ return agents;
283
+ }
284
+ catch {
285
+ return [];
286
+ }
287
+ }
317
288
  /** 按 agent 分组聚合(支持时间范围过滤)。 */
318
289
  export function queryStatsByAgent(params) {
319
290
  const db = openDb();
@@ -332,17 +303,179 @@ export function queryStatsByAgent(params) {
332
303
  const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
333
304
  const limit = params.limit ?? 50;
334
305
  try {
335
- return db.prepare(`
306
+ const rows = db.prepare(`
336
307
  SELECT agent_aid,
337
308
  COALESCE(SUM(input_tokens),0) AS input_tokens,
338
309
  COALESCE(SUM(output_tokens),0) AS output_tokens,
339
310
  COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
340
311
  COUNT(*) AS call_count
341
312
  FROM usage_events ${where}
342
- GROUP BY agent_aid ORDER BY (input_tokens+output_tokens) DESC LIMIT ${limit}
313
+ GROUP BY agent_aid ORDER BY (COALESCE(SUM(input_tokens),0) + COALESCE(SUM(output_tokens),0)) DESC LIMIT ${limit}
314
+ `).all(...p);
315
+ // 为每个agent添加名称
316
+ return rows.map((row) => ({
317
+ ...row,
318
+ agent_name: getAgentName(row.agent_aid)
319
+ }));
320
+ }
321
+ finally {
322
+ db.close();
323
+ }
324
+ }
325
+ /** 查询模型访问明细(支持分页)*/
326
+ export function queryUsageDetail(params) {
327
+ const db = openDb();
328
+ if (!db)
329
+ return { data: [], total: 0 };
330
+ const conds = [];
331
+ const p = [];
332
+ if (params.from_ts) {
333
+ conds.push('ts >= ?');
334
+ p.push(params.from_ts);
335
+ }
336
+ if (params.to_ts) {
337
+ conds.push('ts <= ?');
338
+ p.push(params.to_ts);
339
+ }
340
+ if (params.agent_aid) {
341
+ conds.push('agent_aid = ?');
342
+ p.push(params.agent_aid);
343
+ }
344
+ if (params.model) {
345
+ conds.push('model = ?');
346
+ p.push(params.model);
347
+ }
348
+ const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
349
+ const limit = params.limit ?? 50;
350
+ const offset = params.offset ?? 0;
351
+ try {
352
+ // 获取总数
353
+ const countRow = db.prepare(`SELECT COUNT(*) as total FROM usage_events ${where}`).get(...p);
354
+ const total = countRow?.total || 0;
355
+ // 获取数据
356
+ const data = db.prepare(`
357
+ SELECT ts, agent_aid, peer_key, model,
358
+ input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
359
+ COALESCE(cost_official_usd, 0) AS cost_official_usd,
360
+ COALESCE(cost_official_cny, 0) AS cost_official_cny,
361
+ COALESCE(cost_gateway_usd, 0) AS cost_gateway_usd,
362
+ COALESCE(cost_gateway_cny, 0) AS cost_gateway_cny
363
+ FROM usage_events ${where}
364
+ ORDER BY ts DESC LIMIT ${limit} OFFSET ${offset}
343
365
  `).all(...p);
366
+ // 为每条记录添加agent_name (需要从文件系统读取agent.md)
367
+ const dataWithNames = data.map((row) => ({
368
+ ...row,
369
+ agent_name: getAgentName(row.agent_aid)
370
+ }));
371
+ return { data: dataWithNames, total };
344
372
  }
345
373
  finally {
346
374
  db.close();
347
375
  }
348
376
  }
377
+ /** 查询指定时间范围内使用过的模型列表 */
378
+ export function queryUsedModels(params) {
379
+ const db = openDb();
380
+ if (!db)
381
+ return [];
382
+ const conds = [];
383
+ const p = [];
384
+ if (params.from_ts) {
385
+ conds.push('ts >= ?');
386
+ p.push(params.from_ts);
387
+ }
388
+ if (params.to_ts) {
389
+ conds.push('ts <= ?');
390
+ p.push(params.to_ts);
391
+ }
392
+ const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
393
+ try {
394
+ const rows = db.prepare(`
395
+ SELECT DISTINCT model
396
+ FROM usage_events ${where}
397
+ ORDER BY model
398
+ `).all(...p);
399
+ return rows.map((row) => row.model).filter(Boolean);
400
+ }
401
+ finally {
402
+ db.close();
403
+ }
404
+ }
405
+ // 辅助函数:从agent.md获取agent名称
406
+ function getAgentName(agentAid) {
407
+ if (!agentAid)
408
+ return null;
409
+ try {
410
+ const root = resolvePaths().root;
411
+ const agentMdPath = path.join(root, 'AIDs', agentAid, 'agent.md');
412
+ if (!fs.existsSync(agentMdPath))
413
+ return null;
414
+ const content = fs.readFileSync(agentMdPath, 'utf-8');
415
+ // 解析YAML frontmatter中的name字段
416
+ const yamlMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
417
+ if (yamlMatch) {
418
+ const yamlContent = yamlMatch[1];
419
+ const nameMatch = yamlContent.match(/^name:\s*["']?([^"'\n]+)["']?$/m);
420
+ if (nameMatch) {
421
+ return nameMatch[1].trim();
422
+ }
423
+ }
424
+ // fallback: 尝试匹配第一个markdown标题
425
+ const titleMatch = content.match(/^#\s+(.+)$/m);
426
+ return titleMatch ? titleMatch[1].trim() : null;
427
+ }
428
+ catch {
429
+ return null;
430
+ }
431
+ }
432
+ // 辅助函数:从peer_key解析peer信息
433
+ // peer_key格式:
434
+ // 1. 群聊:aun#{agent_aid}#main#{group_id} (URL encoded)
435
+ // 2. 单聊:aun#{agent_aid}#main#{peer_agent_aid}
436
+ function getPeerInfo(peerKey) {
437
+ if (!peerKey)
438
+ return { name: null, chatType: null, memberCount: null };
439
+ try {
440
+ // peer_key格式:aun#{agent_aid}#main#{target}
441
+ const parts = peerKey.split('#');
442
+ if (parts.length < 4)
443
+ return { name: null, chatType: null, memberCount: null };
444
+ const agentAid = parts[1]; // 自己的agent_aid
445
+ const target = parts[3]; // 群ID或对端agent_aid
446
+ // 判断是群聊还是单聊
447
+ if (target.startsWith('group.')) {
448
+ // 群聊:读取群信息
449
+ const { sessionsDir } = resolvePaths();
450
+ const groupDir = path.join(sessionsDir, 'aun', encodeSegment(agentAid), encodeSegment(target));
451
+ const activeJsonPath = path.join(groupDir, 'active.json');
452
+ if (fs.existsSync(activeJsonPath)) {
453
+ const activeData = JSON.parse(fs.readFileSync(activeJsonPath, 'utf-8'));
454
+ // 使用 metadata.groupName 作为显示名称
455
+ const groupName = activeData.metadata?.groupName || null;
456
+ // 计算群人数:从groupName中的成员数量(以"、"分隔)+ "..."表示还有更多
457
+ let memberCount = null;
458
+ if (groupName) {
459
+ // groupName格式:"用户1、用户2、用户3..."
460
+ const members = groupName.split('、');
461
+ memberCount = members.length;
462
+ // 如果最后一个成员包含"...",说明还有更多成员
463
+ if (members[members.length - 1].includes('...')) {
464
+ memberCount = memberCount - 1; // 减去"..."那个元素
465
+ // 实际人数可能更多,但我们只能从显示的名字估算
466
+ }
467
+ }
468
+ return { name: groupName, chatType: 'group', memberCount };
469
+ }
470
+ return { name: null, chatType: 'group', memberCount: null };
471
+ }
472
+ else {
473
+ // 单聊:target是对端agent_aid,获取其名称
474
+ const name = getAgentName(target);
475
+ return { name, chatType: 'private', memberCount: null };
476
+ }
477
+ }
478
+ catch {
479
+ return { name: null, chatType: null, memberCount: null };
480
+ }
481
+ }