ec-web 0.0.1 → 0.0.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/README.md +15 -3
- package/dist/debug-log.js +7 -0
- package/dist/fs-utils.js +189 -0
- package/dist/index.js +137 -0
- package/dist/ipc-client.js +40 -0
- package/dist/paths.js +55 -0
- package/dist/process-utils.js +241 -0
- package/dist/server.js +820 -0
- package/dist/sources/aid.js +139 -0
- package/dist/sources/baseagent-detector.js +72 -0
- package/dist/sources/cache.js +43 -0
- package/dist/sources/gateway.js +44 -0
- package/dist/sources/logs.js +396 -0
- package/dist/sources/models.js +75 -0
- package/dist/sources/monitor.js +179 -0
- package/dist/sources/msg.js +625 -0
- package/dist/sources/parent-package.js +77 -0
- package/dist/sources/relation-roles.js +903 -0
- package/dist/sources/role-definitions.js +1195 -0
- package/dist/sources/session-codex.js +731 -0
- package/dist/sources/session.js +489 -0
- package/dist/sources/stats.js +481 -0
- package/dist/sources/system.js +83 -0
- package/dist/sources/triggers.js +138 -0
- package/dist/sources/types.js +10 -0
- package/dist/static/app.js +10753 -0
- package/dist/static/index.html +386 -0
- package/dist/static/style.css +4138 -0
- package/dist/types/cli-permissions.js +90 -0
- package/package.json +20 -11
- package/index.js +0 -7
|
@@ -0,0 +1,481 @@
|
|
|
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
|
+
import { encodeSegment } from '../fs-utils.js';
|
|
9
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
10
|
+
let sqliteModule;
|
|
11
|
+
function loadSqlite() {
|
|
12
|
+
if (sqliteModule !== undefined)
|
|
13
|
+
return sqliteModule;
|
|
14
|
+
try {
|
|
15
|
+
sqliteModule = requireFromHere('node:sqlite');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
sqliteModule = null;
|
|
19
|
+
}
|
|
20
|
+
return sqliteModule;
|
|
21
|
+
}
|
|
22
|
+
function getDbPath() {
|
|
23
|
+
const { root } = resolvePaths();
|
|
24
|
+
return path.join(root, 'data', 'stats', 'usage.db');
|
|
25
|
+
}
|
|
26
|
+
function openDb() {
|
|
27
|
+
const sqlite = loadSqlite();
|
|
28
|
+
if (!sqlite)
|
|
29
|
+
return null;
|
|
30
|
+
const dbPath = getDbPath();
|
|
31
|
+
if (!fs.existsSync(dbPath))
|
|
32
|
+
return null;
|
|
33
|
+
try {
|
|
34
|
+
return new sqlite.DatabaseSync(dbPath, { readOnly: true });
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function queryStatsForDashboard() {
|
|
41
|
+
const db = openDb();
|
|
42
|
+
if (!db)
|
|
43
|
+
return null;
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const todayStart = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
|
|
46
|
+
const h24ago = Date.now() - 24 * 60 * 60 * 1000;
|
|
47
|
+
try {
|
|
48
|
+
// Today summary with cost
|
|
49
|
+
const todayRow = db.prepare(`
|
|
50
|
+
SELECT
|
|
51
|
+
COALESCE(SUM(input_tokens),0) AS input_tokens,
|
|
52
|
+
COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
53
|
+
COALESCE(SUM(cache_creation_tokens),0) AS cache_creation_tokens,
|
|
54
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
|
|
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
|
|
58
|
+
FROM usage_events WHERE ts >= ?
|
|
59
|
+
`).get(todayStart);
|
|
60
|
+
const totalIn = (todayRow.input_tokens ?? 0) + (todayRow.cache_read_tokens ?? 0);
|
|
61
|
+
const hitRate = totalIn > 0 ? (todayRow.cache_read_tokens ?? 0) / totalIn : 0;
|
|
62
|
+
// Hourly (last 24h)
|
|
63
|
+
const hourly = db.prepare(`
|
|
64
|
+
SELECT
|
|
65
|
+
strftime('%Y-%m-%d %H:00', ts/1000, 'unixepoch', 'localtime') AS hour,
|
|
66
|
+
COALESCE(SUM(input_tokens),0) AS input_tokens,
|
|
67
|
+
COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
68
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
|
|
69
|
+
COUNT(*) AS call_count
|
|
70
|
+
FROM usage_events WHERE ts >= ?
|
|
71
|
+
GROUP BY hour ORDER BY hour
|
|
72
|
+
`).all(h24ago);
|
|
73
|
+
// Top models (today)
|
|
74
|
+
const top_models = db.prepare(`
|
|
75
|
+
SELECT model, SUM(input_tokens+output_tokens) AS total_tokens, COUNT(*) AS call_count
|
|
76
|
+
FROM usage_events WHERE ts >= ?
|
|
77
|
+
GROUP BY model ORDER BY total_tokens DESC LIMIT 10
|
|
78
|
+
`).all(todayStart);
|
|
79
|
+
// Top peers (today)
|
|
80
|
+
const top_peers = db.prepare(`
|
|
81
|
+
SELECT peer_key, SUM(input_tokens+output_tokens) AS total_tokens, COUNT(*) AS call_count
|
|
82
|
+
FROM usage_events WHERE ts >= ?
|
|
83
|
+
GROUP BY peer_key ORDER BY total_tokens DESC LIMIT 5
|
|
84
|
+
`).all(todayStart);
|
|
85
|
+
return {
|
|
86
|
+
today: { ...todayRow, cache_hit_rate: hitRate },
|
|
87
|
+
hourly,
|
|
88
|
+
top_models,
|
|
89
|
+
top_peers,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
db.close();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function queryStatsExplorer(params) {
|
|
97
|
+
const db = openDb();
|
|
98
|
+
if (!db)
|
|
99
|
+
return [];
|
|
100
|
+
const gran = params.granularity || 'day';
|
|
101
|
+
const fmt = { hour: '%Y-%m-%d %H:00', day: '%Y-%m-%d', week: '%Y-W%W', month: '%Y-%m' };
|
|
102
|
+
const strfmt = fmt[gran] || fmt.day;
|
|
103
|
+
const conds = [];
|
|
104
|
+
const p = [];
|
|
105
|
+
if (params.from_ts) {
|
|
106
|
+
conds.push('ts >= ?');
|
|
107
|
+
p.push(params.from_ts);
|
|
108
|
+
}
|
|
109
|
+
if (params.to_ts) {
|
|
110
|
+
conds.push('ts < ?');
|
|
111
|
+
p.push(params.to_ts);
|
|
112
|
+
}
|
|
113
|
+
if (params.agent_aid) {
|
|
114
|
+
conds.push('agent_aid = ?');
|
|
115
|
+
p.push(params.agent_aid);
|
|
116
|
+
}
|
|
117
|
+
if (params.peer_key) {
|
|
118
|
+
conds.push('peer_key = ?');
|
|
119
|
+
p.push(params.peer_key);
|
|
120
|
+
}
|
|
121
|
+
if (params.model) {
|
|
122
|
+
conds.push('model = ?');
|
|
123
|
+
p.push(params.model);
|
|
124
|
+
}
|
|
125
|
+
const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
|
|
126
|
+
try {
|
|
127
|
+
return db.prepare(`
|
|
128
|
+
SELECT
|
|
129
|
+
strftime('${strfmt}', ts/1000, 'unixepoch', 'localtime') AS period,
|
|
130
|
+
COALESCE(SUM(input_tokens),0) AS input_tokens,
|
|
131
|
+
COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
132
|
+
COALESCE(SUM(cache_creation_tokens),0) AS cache_creation_tokens,
|
|
133
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
|
|
134
|
+
COUNT(*) AS call_count
|
|
135
|
+
FROM usage_events ${where}
|
|
136
|
+
GROUP BY period ORDER BY period
|
|
137
|
+
`).all(...p);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
db.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** 按 peer 分组聚合(支持时间范围过滤)。 */
|
|
144
|
+
export function queryStatsByPeer(params) {
|
|
145
|
+
const db = openDb();
|
|
146
|
+
if (!db)
|
|
147
|
+
return [];
|
|
148
|
+
const conds = [];
|
|
149
|
+
const p = [];
|
|
150
|
+
if (params.from_ts) {
|
|
151
|
+
conds.push('ts >= ?');
|
|
152
|
+
p.push(params.from_ts);
|
|
153
|
+
}
|
|
154
|
+
if (params.to_ts) {
|
|
155
|
+
conds.push('ts < ?');
|
|
156
|
+
p.push(params.to_ts);
|
|
157
|
+
}
|
|
158
|
+
if (params.agent_aid) {
|
|
159
|
+
conds.push('agent_aid = ?');
|
|
160
|
+
p.push(params.agent_aid);
|
|
161
|
+
}
|
|
162
|
+
const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
|
|
163
|
+
const limit = params.limit ?? 50;
|
|
164
|
+
try {
|
|
165
|
+
const rows = db.prepare(`
|
|
166
|
+
SELECT peer_key, peer_type,
|
|
167
|
+
COALESCE(SUM(input_tokens),0) AS input_tokens,
|
|
168
|
+
COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
169
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
|
|
170
|
+
COUNT(*) AS call_count
|
|
171
|
+
FROM usage_events ${where}
|
|
172
|
+
GROUP BY peer_key ORDER BY (COALESCE(SUM(input_tokens),0) + COALESCE(SUM(output_tokens),0)) DESC LIMIT ${limit}
|
|
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
|
+
});
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
db.close();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export function queryStatsOverview(params) {
|
|
190
|
+
const db = openDb();
|
|
191
|
+
if (!db)
|
|
192
|
+
return null;
|
|
193
|
+
try {
|
|
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);
|
|
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
|
+
}));
|
|
249
|
+
return {
|
|
250
|
+
all_time: allRow,
|
|
251
|
+
by_agent: byAgentWithNames,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
db.close();
|
|
256
|
+
}
|
|
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
|
+
}
|
|
288
|
+
/** 按 agent 分组聚合(支持时间范围过滤)。 */
|
|
289
|
+
export function queryStatsByAgent(params) {
|
|
290
|
+
const db = openDb();
|
|
291
|
+
if (!db)
|
|
292
|
+
return [];
|
|
293
|
+
const conds = [];
|
|
294
|
+
const p = [];
|
|
295
|
+
if (params.from_ts) {
|
|
296
|
+
conds.push('ts >= ?');
|
|
297
|
+
p.push(params.from_ts);
|
|
298
|
+
}
|
|
299
|
+
if (params.to_ts) {
|
|
300
|
+
conds.push('ts < ?');
|
|
301
|
+
p.push(params.to_ts);
|
|
302
|
+
}
|
|
303
|
+
const where = conds.length ? 'WHERE ' + conds.join(' AND ') : '';
|
|
304
|
+
const limit = params.limit ?? 50;
|
|
305
|
+
try {
|
|
306
|
+
const rows = db.prepare(`
|
|
307
|
+
SELECT agent_aid,
|
|
308
|
+
COALESCE(SUM(input_tokens),0) AS input_tokens,
|
|
309
|
+
COALESCE(SUM(output_tokens),0) AS output_tokens,
|
|
310
|
+
COALESCE(SUM(cache_read_tokens),0) AS cache_read_tokens,
|
|
311
|
+
COUNT(*) AS call_count
|
|
312
|
+
FROM usage_events ${where}
|
|
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}
|
|
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 };
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
db.close();
|
|
375
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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 fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { resolvePaths } from '../paths.js';
|
|
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
|
+
}
|
|
37
|
+
async function menuExec(payload) {
|
|
38
|
+
const p = resolvePaths();
|
|
39
|
+
const r = await ipcQuery(p.socket, { type: 'menu.exec', payload, auth: { source: 'ecweb', localDirect: true } }, 5000);
|
|
40
|
+
return r?.ok ? r.response : null;
|
|
41
|
+
}
|
|
42
|
+
async function buildSnapshot() {
|
|
43
|
+
const [listResp, sysResp] = await Promise.all([
|
|
44
|
+
menuExec({ type: 'menu.list', id: 'sys-list' }),
|
|
45
|
+
menuExec({ type: 'menu.query', id: 'sys-q', name: 'system' }),
|
|
46
|
+
]);
|
|
47
|
+
const daemonRunning = listResp !== null;
|
|
48
|
+
// sysResp 形如 { type:'menu.response', id, name, data | error }
|
|
49
|
+
const system = sysResp?.data ?? null;
|
|
50
|
+
// baseagents:以 defaults.json 的 active/model/effort 为主,
|
|
51
|
+
// 再从后端返回的 baseagents([{name,version}])按 name 补上 CLI 版本号。
|
|
52
|
+
let baseagents = readDefaultBaseagents();
|
|
53
|
+
if (system && Array.isArray(system.baseagents)) {
|
|
54
|
+
const verByName = new Map(system.baseagents.map((b) => [b.name, b.version ?? null]));
|
|
55
|
+
baseagents = baseagents.map((b) => ({ ...b, version: verByName.get(b.name) ?? null }));
|
|
56
|
+
}
|
|
57
|
+
return { daemonRunning, system: system ? { ...system, baseagents } : system, upgrade: null, check: null };
|
|
58
|
+
}
|
|
59
|
+
export const systemSource = {
|
|
60
|
+
kind: 'system',
|
|
61
|
+
async snapshot() {
|
|
62
|
+
return buildSnapshot();
|
|
63
|
+
},
|
|
64
|
+
subscribe(_params, push) {
|
|
65
|
+
let lastJson = '';
|
|
66
|
+
let stopped = false;
|
|
67
|
+
const tick = async () => {
|
|
68
|
+
if (stopped)
|
|
69
|
+
return;
|
|
70
|
+
try {
|
|
71
|
+
const snap = await buildSnapshot();
|
|
72
|
+
const json = JSON.stringify(snap);
|
|
73
|
+
if (json !== lastJson) {
|
|
74
|
+
lastJson = json;
|
|
75
|
+
push(snap);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* ignore transient IPC errors */ }
|
|
79
|
+
};
|
|
80
|
+
const timer = setInterval(tick, 30000);
|
|
81
|
+
return () => { stopped = true; clearInterval(timer); };
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triggers 数据源 — 默认聚合全部 agent,也可按 agent 钻取触发器。
|
|
3
|
+
*
|
|
4
|
+
* snapshot:
|
|
5
|
+
* - agents: menu.options name=agent(agent 列表,同 Agents 页数据源)
|
|
6
|
+
* - triggers: 未选 agent 时遍历所有 agent 聚合;选中 agent 时只查该 agent
|
|
7
|
+
*
|
|
8
|
+
* subscribe: 2s 轮询 + JSON diff,仅变化时 push。
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { resolvePaths } from '../paths.js';
|
|
13
|
+
import { ipcQuery } from '../ipc-client.js';
|
|
14
|
+
async function menuExec(payload) {
|
|
15
|
+
const p = resolvePaths();
|
|
16
|
+
const r = await ipcQuery(p.socket, { type: 'menu.exec', payload, auth: { source: 'ecweb', localDirect: true } }, 5000);
|
|
17
|
+
return r?.ok ? r.response : null;
|
|
18
|
+
}
|
|
19
|
+
function readTriggerControlToken(instanceDir) {
|
|
20
|
+
try {
|
|
21
|
+
return fs.readFileSync(path.join(instanceDir, 'control.token'), 'utf8').trim() || undefined;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function normalizeAgent(item) {
|
|
28
|
+
const aid = String(item?.value || item?.aid || '').trim();
|
|
29
|
+
if (!aid)
|
|
30
|
+
return null;
|
|
31
|
+
const label = String(item?.label || item?.name || aid).trim() || aid;
|
|
32
|
+
return {
|
|
33
|
+
aid,
|
|
34
|
+
label,
|
|
35
|
+
channels: Array.isArray(item?.channels) ? item.channels.map(String).filter(Boolean) : [],
|
|
36
|
+
baseagent: typeof item?.baseagent === 'string' ? item.baseagent : undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function listTriggersForAgent(agentAid, agentLabel) {
|
|
40
|
+
const p = resolvePaths();
|
|
41
|
+
const controlToken = readTriggerControlToken(p.instanceDir);
|
|
42
|
+
const resp = await menuExec({
|
|
43
|
+
type: 'menu.options',
|
|
44
|
+
id: `tr-list-${agentAid}`,
|
|
45
|
+
name: 'trigger',
|
|
46
|
+
args: { options: 'all' },
|
|
47
|
+
agent: agentAid,
|
|
48
|
+
});
|
|
49
|
+
const items = Array.isArray(resp?.data) ? resp.data : [];
|
|
50
|
+
return Promise.all(items.map(async (trigger) => {
|
|
51
|
+
const effectiveAgentAid = trigger.agentAid || trigger.schedulerAid || agentAid;
|
|
52
|
+
let definition = null;
|
|
53
|
+
let revision = null;
|
|
54
|
+
let definitionError = null;
|
|
55
|
+
let subscription = null;
|
|
56
|
+
try {
|
|
57
|
+
const shown = await ipcQuery(p.socket, {
|
|
58
|
+
type: 'trigger.show',
|
|
59
|
+
agentAid: effectiveAgentAid,
|
|
60
|
+
triggerId: trigger.id ?? trigger.value,
|
|
61
|
+
controlToken,
|
|
62
|
+
includeScriptPreview: false,
|
|
63
|
+
}, 5000);
|
|
64
|
+
if (shown?.ok) {
|
|
65
|
+
definition = shown.definition ?? null;
|
|
66
|
+
revision = typeof shown.revision === 'string' && shown.revision ? shown.revision : null;
|
|
67
|
+
subscription = shown.subscription ?? null;
|
|
68
|
+
if (!definition)
|
|
69
|
+
definitionError = 'trigger.show did not return a definition';
|
|
70
|
+
else if (!revision)
|
|
71
|
+
definitionError = 'trigger.show did not return a definition revision';
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
definitionError = String(shown?.error || 'trigger.show failed');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
definitionError = error?.message || String(error);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
...trigger,
|
|
82
|
+
agentAid: effectiveAgentAid,
|
|
83
|
+
agentLabel: trigger.agentLabel || agentLabel,
|
|
84
|
+
definition,
|
|
85
|
+
revision,
|
|
86
|
+
definitionError,
|
|
87
|
+
subscription,
|
|
88
|
+
};
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
async function buildSnapshot(params) {
|
|
92
|
+
const p = resolvePaths();
|
|
93
|
+
const agent = typeof params?.agent === 'string' && params.agent ? params.agent : null;
|
|
94
|
+
const [agentsResp, runtimeAgentsResp] = await Promise.all([
|
|
95
|
+
menuExec({ type: 'menu.options', id: 'tr-agents', name: 'agent' }),
|
|
96
|
+
ipcQuery(p.socket, { type: 'evolagent.list' }, 5000),
|
|
97
|
+
]);
|
|
98
|
+
// menu.options 响应形如 { type:'menu.response', id, name, data: MenuItem[] }
|
|
99
|
+
const agents = Array.isArray(agentsResp?.data) ? agentsResp.data : [];
|
|
100
|
+
const runtimeByAid = new Map((Array.isArray(runtimeAgentsResp?.agents) ? runtimeAgentsResp.agents : [])
|
|
101
|
+
.map((item) => [String(item?.aid || ''), item]));
|
|
102
|
+
const agentInfos = agents
|
|
103
|
+
.map((item) => normalizeAgent({ ...runtimeByAid.get(String(item?.value || item?.aid || '')), ...item }))
|
|
104
|
+
.filter((x) => !!x);
|
|
105
|
+
if (agent) {
|
|
106
|
+
const found = agentInfos.find((x) => x.aid === agent);
|
|
107
|
+
const triggers = await listTriggersForAgent(agent, found?.label || agent);
|
|
108
|
+
return { agents: agentInfos, triggers, selectedAgent: agent };
|
|
109
|
+
}
|
|
110
|
+
const batches = await Promise.all(agentInfos.map((x) => listTriggersForAgent(x.aid, x.label)));
|
|
111
|
+
const triggers = batches.flat();
|
|
112
|
+
return { agents: agentInfos, triggers, selectedAgent: agent };
|
|
113
|
+
}
|
|
114
|
+
export const triggersSource = {
|
|
115
|
+
kind: 'triggers',
|
|
116
|
+
async snapshot(params) {
|
|
117
|
+
return buildSnapshot(params ?? {});
|
|
118
|
+
},
|
|
119
|
+
subscribe(params, push) {
|
|
120
|
+
let lastJson = '';
|
|
121
|
+
let stopped = false;
|
|
122
|
+
const tick = async () => {
|
|
123
|
+
if (stopped)
|
|
124
|
+
return;
|
|
125
|
+
try {
|
|
126
|
+
const snap = await buildSnapshot(params);
|
|
127
|
+
const json = JSON.stringify(snap);
|
|
128
|
+
if (json !== lastJson) {
|
|
129
|
+
lastJson = json;
|
|
130
|
+
push(snap);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch { /* ignore transient IPC errors */ }
|
|
134
|
+
};
|
|
135
|
+
const timer = setInterval(tick, 2000);
|
|
136
|
+
return () => { stopped = true; clearInterval(timer); };
|
|
137
|
+
},
|
|
138
|
+
};
|