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.
- package/dist/index.js +9 -9
- package/dist/process-utils.js +20 -12
- package/dist/server.js +256 -36
- package/dist/sources/aid.js +20 -1
- package/dist/sources/gateway.js +44 -0
- package/dist/sources/monitor.js +96 -0
- package/dist/sources/session.js +11 -2
- package/dist/sources/stats.js +269 -136
- package/dist/sources/system.js +2 -2
- package/dist/static/app.js +2509 -327
- package/dist/static/index.html +145 -51
- package/dist/static/style.css +1016 -25
- package/package.json +2 -2
- package/dist/sources/control.js +0 -58
|
@@ -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
|
+
};
|
package/dist/sources/session.js
CHANGED
|
@@ -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
|
-
|
|
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();
|
package/dist/sources/stats.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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:
|
|
308
|
-
by_agent:
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/sources/system.js
CHANGED
|
@@ -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:
|
|
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,
|
|
48
|
+
const timer = setInterval(tick, 30000);
|
|
49
49
|
return () => { stopped = true; clearInterval(timer); };
|
|
50
50
|
},
|
|
51
51
|
};
|