evolclaw-web 1.1.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.
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Monitor 数据源 — 复用 daemon 的 IPC socket(`monitor-snapshot` 命令)。
3
+ *
4
+ * 拉进程级 + 系统级运行指标(CPU/内存/uptime/loadAvg)+ 全局 stats(含最近错误)
5
+ * + per-agent 汇总。IPC 无推送能力,故 2s 轮询 + JSON diff,仅在变化时 push。
6
+ *
7
+ * 时序数据在源侧维护「三档分辨率」滚动缓冲区,随快照一并下发,前端按所选周期切换:
8
+ * - fine : 2s 桶 × 60 ≈ 近 2 分钟
9
+ * - mid : 10s 桶 × 60 ≈ 近 10 分钟
10
+ * - coarse : 60s 桶 × 60 ≈ 近 1 小时
11
+ * 每个桶内对 CPU/内存做均值聚合,避免长周期下点数爆炸。
12
+ */
13
+ import { resolvePaths } from '../paths.js';
14
+ import { ipcQuery } from '../ipc-client.js';
15
+ // 模块级缓冲区——所有订阅者共享同一份累积时序(单一事实源)。
16
+ const resolutions = {
17
+ fine: { bucketMs: 2_000, cap: 60, buf: [] },
18
+ mid: { bucketMs: 10_000, cap: 60, buf: [] },
19
+ coarse: { bucketMs: 60_000, cap: 60, buf: [] },
20
+ };
21
+ /** 把一个原始样本并入指定分辨率的桶(同桶内做增量均值,跨桶则新建)。 */
22
+ function ingest(res, sample) {
23
+ const bucketTs = Math.floor(sample.ts / res.bucketMs) * res.bucketMs;
24
+ const last = res.buf[res.buf.length - 1];
25
+ if (last && last.ts === bucketTs) {
26
+ const n = last._n + 1;
27
+ last.procCpu = last.procCpu + (sample.procCpu - last.procCpu) / n;
28
+ last.sysCpu = last.sysCpu + (sample.sysCpu - last.sysCpu) / n;
29
+ last.procRss = last.procRss + (sample.procRss - last.procRss) / n;
30
+ last.sysMemUsed = last.sysMemUsed + (sample.sysMemUsed - last.sysMemUsed) / n;
31
+ last._n = n;
32
+ }
33
+ else {
34
+ res.buf.push({ ts: bucketTs, procCpu: sample.procCpu, sysCpu: sample.sysCpu, procRss: sample.procRss, sysMemUsed: sample.sysMemUsed, _n: 1 });
35
+ if (res.buf.length > res.cap)
36
+ res.buf.shift();
37
+ }
38
+ }
39
+ /** 导出某分辨率的纯数据点(剥掉内部 _n 字段)。 */
40
+ function exportBuf(res) {
41
+ return res.buf.map((b) => ({ ts: b.ts, procCpu: round1(b.procCpu), sysCpu: round1(b.sysCpu), procRss: Math.round(b.procRss), sysMemUsed: Math.round(b.sysMemUsed) }));
42
+ }
43
+ function round1(n) { return Math.round(n * 10) / 10; }
44
+ async function fetchSnapshot() {
45
+ const p = resolvePaths();
46
+ const resp = await ipcQuery(p.socket, { type: 'monitor-snapshot' }, 3000);
47
+ // resp === null → daemon 离线或 socket 不可达;resp.ok === false → 命令不受支持(旧 daemon)
48
+ if (!resp?.ok) {
49
+ return { daemonRunning: resp !== null, snapshot: null, history: { fine: [], mid: [], coarse: [] } };
50
+ }
51
+ const s = resp.snapshot;
52
+ const sample = {
53
+ ts: s.ts,
54
+ procCpu: s.cpuPercent ?? 0,
55
+ sysCpu: s.system?.cpuPercent ?? 0,
56
+ procRss: s.memory?.rss ?? 0,
57
+ sysMemUsed: s.system?.memUsed ?? 0,
58
+ };
59
+ ingest(resolutions.fine, sample);
60
+ ingest(resolutions.mid, sample);
61
+ ingest(resolutions.coarse, sample);
62
+ return {
63
+ daemonRunning: true,
64
+ snapshot: s,
65
+ history: {
66
+ fine: exportBuf(resolutions.fine),
67
+ mid: exportBuf(resolutions.mid),
68
+ coarse: exportBuf(resolutions.coarse),
69
+ },
70
+ };
71
+ }
72
+ export const monitorSource = {
73
+ kind: 'monitor',
74
+ async snapshot() {
75
+ return fetchSnapshot();
76
+ },
77
+ subscribe(_params, push) {
78
+ let lastJson = '';
79
+ let stopped = false;
80
+ const tick = async () => {
81
+ if (stopped)
82
+ return;
83
+ try {
84
+ const snap = await fetchSnapshot();
85
+ const json = JSON.stringify(snap);
86
+ if (json !== lastJson) {
87
+ lastJson = json;
88
+ push(snap);
89
+ }
90
+ }
91
+ catch { /* ignore transient IPC errors */ }
92
+ };
93
+ const timer = setInterval(tick, 2000);
94
+ return () => { stopped = true; clearInterval(timer); };
95
+ },
96
+ };
@@ -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 {
@@ -212,7 +221,7 @@ function resolveProject(params, projects) {
212
221
  dlog(`[session] resolveProject: cwd project=${curEncoded.slice(-24)} not found → using first=${projects[0]?.encoded.slice(-24)}`);
213
222
  return projects[0] || null;
214
223
  }
215
- function buildBindMap() {
224
+ export function buildBindMap() {
216
225
  const map = new Map();
217
226
  try {
218
227
  const p = resolvePaths();
@@ -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
+ }
@@ -5,7 +5,7 @@
5
5
  * 另注入 ecwebVersion(ECWeb 进程自身 package.json 版本,由 server.ts 合并,不走 daemon IPC)。
6
6
  * check / upgrade 结果不在快照里——由前端按钮触发 menu.action 后写入 state。
7
7
  *
8
- * subscribe: 3s 轮询 + JSON diff,仅变化时 push。
8
+ * subscribe: 30s 轮询 + JSON diff,仅变化时 push。
9
9
  */
10
10
  import { resolvePaths } from '../paths.js';
11
11
  import { ipcQuery } from '../ipc-client.js';
@@ -45,7 +45,7 @@ export const systemSource = {
45
45
  }
46
46
  catch { /* ignore transient IPC errors */ }
47
47
  };
48
- const timer = setInterval(tick, 3000);
48
+ const timer = setInterval(tick, 30000);
49
49
  return () => { stopped = true; clearInterval(timer); };
50
50
  },
51
51
  };