evolclaw-web 1.2.0 → 1.2.3
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/server.js +193 -25
- package/dist/sources/aid.js +4 -2
- package/dist/sources/baseagent-detector.js +72 -0
- package/dist/sources/gateway.js +44 -0
- package/dist/sources/msg.js +366 -31
- package/dist/sources/session-codex.js +618 -0
- package/dist/sources/session.js +25 -12
- package/dist/sources/stats.js +269 -136
- package/dist/sources/system.js +37 -2
- package/dist/static/app.js +2089 -321
- package/dist/static/index.html +122 -57
- package/dist/static/style.css +845 -19
- package/package.json +1 -1
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
|
@@ -7,22 +7,57 @@
|
|
|
7
7
|
*
|
|
8
8
|
* subscribe: 30s 轮询 + JSON diff,仅变化时 push。
|
|
9
9
|
*/
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
10
12
|
import { resolvePaths } from '../paths.js';
|
|
11
13
|
import { ipcQuery } from '../ipc-client.js';
|
|
14
|
+
function readDefaultBaseagents() {
|
|
15
|
+
try {
|
|
16
|
+
const p = resolvePaths();
|
|
17
|
+
const defaultsPath = path.join(p.root, 'agents', 'defaults.json');
|
|
18
|
+
const raw = JSON.parse(fs.readFileSync(defaultsPath, 'utf-8'));
|
|
19
|
+
const baseagents = raw?.baseagents;
|
|
20
|
+
if (!baseagents || typeof baseagents !== 'object')
|
|
21
|
+
return [];
|
|
22
|
+
const active = typeof raw.active_baseagent === 'string' ? raw.active_baseagent : null;
|
|
23
|
+
return Object.entries(baseagents).map(([name, cfg]) => {
|
|
24
|
+
const c = cfg && typeof cfg === 'object' ? cfg : {};
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
active: name === active,
|
|
28
|
+
model: c.model ?? null,
|
|
29
|
+
effort: c.effort ?? c.reasoning ?? null,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
12
37
|
async function menuExec(payload) {
|
|
13
38
|
const p = resolvePaths();
|
|
14
39
|
const r = await ipcQuery(p.socket, { type: 'menu.exec', payload }, 5000);
|
|
15
40
|
return r?.ok ? r.response : null;
|
|
16
41
|
}
|
|
17
42
|
async function buildSnapshot() {
|
|
18
|
-
const [listResp, sysResp] = await Promise.all([
|
|
43
|
+
const [listResp, sysResp, checkResp] = await Promise.all([
|
|
19
44
|
menuExec({ type: 'menu.list', id: 'sys-list' }),
|
|
20
45
|
menuExec({ type: 'menu.query', id: 'sys-q', name: 'system' }),
|
|
46
|
+
menuExec({ type: 'menu.action', id: 'sys-check', name: 'system', action: 'check' }),
|
|
21
47
|
]);
|
|
22
48
|
const daemonRunning = listResp !== null;
|
|
23
49
|
// sysResp 形如 { type:'menu.response', id, name, data | error }
|
|
24
50
|
const system = sysResp?.data ?? null;
|
|
25
|
-
|
|
51
|
+
// baseagents:以 defaults.json 的 active/model/effort 为主,
|
|
52
|
+
// 再从后端返回的 baseagents([{name,version}])按 name 补上 CLI 版本号。
|
|
53
|
+
let baseagents = readDefaultBaseagents();
|
|
54
|
+
if (system && Array.isArray(system.baseagents)) {
|
|
55
|
+
const verByName = new Map(system.baseagents.map((b) => [b.name, b.version ?? null]));
|
|
56
|
+
baseagents = baseagents.map((b) => ({ ...b, version: verByName.get(b.name) ?? null }));
|
|
57
|
+
}
|
|
58
|
+
// checkResp 包含 evolagents 等健康检查数据
|
|
59
|
+
const check = checkResp?.data ?? null;
|
|
60
|
+
return { daemonRunning, system: system ? { ...system, baseagents } : system, upgrade: null, check };
|
|
26
61
|
}
|
|
27
62
|
export const systemSource = {
|
|
28
63
|
kind: 'system',
|