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/server.js
CHANGED
|
@@ -20,21 +20,23 @@ import { ipcQuery } from './ipc-client.js';
|
|
|
20
20
|
import { setDebugLog, dlog } from './debug-log.js';
|
|
21
21
|
import { aidSource } from './sources/aid.js';
|
|
22
22
|
import { msgSource } from './sources/msg.js';
|
|
23
|
-
import { sessionSource } from './sources/session.js';
|
|
23
|
+
import { sessionSource, buildBindMap } from './sources/session.js';
|
|
24
24
|
import { cacheSource } from './sources/cache.js';
|
|
25
25
|
import { systemSource } from './sources/system.js';
|
|
26
26
|
import { triggersSource } from './sources/triggers.js';
|
|
27
27
|
import { monitorSource } from './sources/monitor.js';
|
|
28
|
-
import {
|
|
28
|
+
import { gatewaySource } from './sources/gateway.js';
|
|
29
|
+
import { queryStatsForDashboard, queryStatsExplorer, queryStatsByPeer, queryStatsOverview, queryUsageDetail, queryUsedModels } from './sources/stats.js';
|
|
29
30
|
import { getSessionsAunDir, listLocalAids, listPeers, readMessages } from './fs-utils.js';
|
|
30
31
|
import { ccProjectsDir } from './paths.js';
|
|
32
|
+
import { detectBaseAgents } from './sources/baseagent-detector.js';
|
|
31
33
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
34
|
const STATIC_DIR = path.join(__dirname, 'static');
|
|
33
35
|
const TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30天(滑动窗口:每次有效访问刷新 lastActive 自动续期)
|
|
34
36
|
const PAIRING_TTL_MS = 5 * 60 * 1000; // 5min
|
|
35
37
|
const DEFAULT_PORT = 42705;
|
|
36
38
|
const PROTOCOL_VERSION = 1; // 与 evolclaw ping response 对齐的软校验版本
|
|
37
|
-
const SOURCES = { agents: aidSource, msg: msgSource, session: sessionSource, cache: cacheSource, system: systemSource, triggers: triggersSource, monitor: monitorSource };
|
|
39
|
+
const SOURCES = { agents: aidSource, msg: msgSource, session: sessionSource, cache: cacheSource, system: systemSource, triggers: triggersSource, monitor: monitorSource, gateway: gatewaySource };
|
|
38
40
|
// ECWeb 自身版本:渲染 System 页时随快照下发(不走 daemon IPC,ECWeb 就是这个进程)。
|
|
39
41
|
function readEcwebVersion() {
|
|
40
42
|
try {
|
|
@@ -104,6 +106,26 @@ function validateAndRenew(token, now) {
|
|
|
104
106
|
return false;
|
|
105
107
|
}
|
|
106
108
|
// ── Static ──
|
|
109
|
+
// 经 AUN Service Proxy 转发时,proxy-server 注入 x-forwarded-prefix 头,
|
|
110
|
+
// 值为外部前缀(如 /evolai/ecweb)。把它作为 <base href> 注入 index.html,
|
|
111
|
+
// 使相对路径资源(style.css / app.js / api / ws)在带不带尾斜杠时都正确解析。
|
|
112
|
+
// 本地直连无此头,<base> 注入为 "/",行为不变。
|
|
113
|
+
function forwardedPrefix(req) {
|
|
114
|
+
const raw = req.headers['x-forwarded-prefix'];
|
|
115
|
+
const value = (Array.isArray(raw) ? raw[0] : raw || '').trim();
|
|
116
|
+
// 安全:只接受形如 /a/b 的简单路径,拒绝含协议、双斜杠、引号、尖括号等的值
|
|
117
|
+
if (!value || !/^\/[A-Za-z0-9._~%/-]*$/.test(value))
|
|
118
|
+
return '';
|
|
119
|
+
return value.replace(/\/+$/, ''); // 去尾斜杠,注入时统一补
|
|
120
|
+
}
|
|
121
|
+
function injectBaseHref(html, prefix) {
|
|
122
|
+
const base = prefix ? `${prefix}/` : '/';
|
|
123
|
+
const tag = `<base href="${base}">`;
|
|
124
|
+
// 若已有 <base> 则替换,否则插在 <head> 之后
|
|
125
|
+
if (/<base\b[^>]*>/i.test(html))
|
|
126
|
+
return html.replace(/<base\b[^>]*>/i, tag);
|
|
127
|
+
return html.replace(/<head[^>]*>/i, (m) => `${m}\n ${tag}`);
|
|
128
|
+
}
|
|
107
129
|
function serveStatic(req, res) {
|
|
108
130
|
let urlPath = (req.url || '/').split('?')[0];
|
|
109
131
|
if (urlPath === '/')
|
|
@@ -119,7 +141,16 @@ function serveStatic(req, res) {
|
|
|
119
141
|
res.writeHead(404).end('Not Found');
|
|
120
142
|
return;
|
|
121
143
|
}
|
|
122
|
-
|
|
144
|
+
const ext = path.extname(file);
|
|
145
|
+
// index.html 注入 <base href>,适配 AUN Service Proxy 前缀
|
|
146
|
+
if (ext === '.html') {
|
|
147
|
+
const prefix = forwardedPrefix(req);
|
|
148
|
+
const html = injectBaseHref(data.toString('utf-8'), prefix);
|
|
149
|
+
res.writeHead(200, { 'Content-Type': MIME[ext] });
|
|
150
|
+
res.end(html);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
|
|
123
154
|
res.end(data);
|
|
124
155
|
});
|
|
125
156
|
}
|
|
@@ -183,40 +214,97 @@ function handleStatsApi(req, res) {
|
|
|
183
214
|
res.end(JSON.stringify(data));
|
|
184
215
|
}
|
|
185
216
|
else if (urlPath === '/api/stats/agents') {
|
|
217
|
+
// 返回所有agent列表(与智能体管理页面一致,从aidSource获取)
|
|
218
|
+
aidSource.snapshot().then(snapshot => {
|
|
219
|
+
const agents = (snapshot?.agents || []).map((ag) => ({
|
|
220
|
+
agent_aid: ag.aid,
|
|
221
|
+
agent_name: ag.displayName || null
|
|
222
|
+
}));
|
|
223
|
+
res.writeHead(200);
|
|
224
|
+
res.end(JSON.stringify(agents));
|
|
225
|
+
}).catch(err => {
|
|
226
|
+
res.writeHead(500);
|
|
227
|
+
res.end(JSON.stringify({ error: 'Failed to fetch agents' }));
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
else if (urlPath === '/api/stats/overview') {
|
|
232
|
+
// 从查询参数中获取时间范围和筛选条件
|
|
186
233
|
const params = {};
|
|
187
234
|
if (query.from)
|
|
188
235
|
params.from_ts = Number(query.from);
|
|
189
236
|
if (query.to)
|
|
190
237
|
params.to_ts = Number(query.to);
|
|
191
|
-
if (query.
|
|
192
|
-
params.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
else if (urlPath === '/api/stats/overview') {
|
|
198
|
-
const tokenStats = queryStatsOverview();
|
|
199
|
-
// session count: scan all CC project dirs
|
|
238
|
+
if (query.agent)
|
|
239
|
+
params.agent_aid = String(query.agent);
|
|
240
|
+
if (query.peer)
|
|
241
|
+
params.peer_key = String(query.peer);
|
|
242
|
+
const tokenStats = queryStatsOverview(params);
|
|
243
|
+
// session count: 使用 bindMap 按 agent 和 peer 筛选
|
|
200
244
|
let sessionCount = 0;
|
|
201
245
|
try {
|
|
246
|
+
// 构建 agentSessionId → agent_aid 的映射
|
|
247
|
+
const bindMap = buildBindMap();
|
|
248
|
+
// 如果指定了 peer_key,提取并解码 channelId
|
|
249
|
+
let targetChannelId;
|
|
250
|
+
if (params.peer_key) {
|
|
251
|
+
const parts = params.peer_key.split('#');
|
|
252
|
+
if (parts.length >= 4) {
|
|
253
|
+
// aun#{agent}#main#{channelId},需要解码 URL 编码
|
|
254
|
+
targetChannelId = decodeURIComponent(parts[3]);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
202
257
|
const base = ccProjectsDir();
|
|
203
258
|
for (const d of fs.readdirSync(base, { withFileTypes: true })) {
|
|
204
259
|
if (!d.isDirectory())
|
|
205
260
|
continue;
|
|
261
|
+
const projectDir = path.join(base, d.name);
|
|
206
262
|
try {
|
|
207
|
-
|
|
263
|
+
const sessionFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
|
|
264
|
+
for (const sessionFile of sessionFiles) {
|
|
265
|
+
const sessionId = sessionFile.replace('.jsonl', '');
|
|
266
|
+
// 如果指定了 agent 或 peer,检查该会话是否匹配
|
|
267
|
+
if (params.agent_aid || targetChannelId) {
|
|
268
|
+
const bindInfo = bindMap.get(sessionId);
|
|
269
|
+
if (!bindInfo)
|
|
270
|
+
continue;
|
|
271
|
+
// 检查 agent
|
|
272
|
+
if (params.agent_aid && bindInfo.selfAID !== params.agent_aid)
|
|
273
|
+
continue;
|
|
274
|
+
// 检查 peer
|
|
275
|
+
if (targetChannelId && bindInfo.channelId !== targetChannelId)
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
// 如果有时间范围限制,检查会话文件的修改时间
|
|
279
|
+
if (params.from_ts || params.to_ts) {
|
|
280
|
+
const stat = fs.statSync(path.join(projectDir, sessionFile));
|
|
281
|
+
const mtime = stat.mtimeMs;
|
|
282
|
+
if (params.from_ts && mtime < params.from_ts)
|
|
283
|
+
continue;
|
|
284
|
+
if (params.to_ts && mtime > params.to_ts)
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
sessionCount++;
|
|
288
|
+
}
|
|
208
289
|
}
|
|
209
290
|
catch { }
|
|
210
291
|
}
|
|
211
292
|
}
|
|
212
293
|
catch { }
|
|
213
|
-
// message counts: scan aun dir
|
|
294
|
+
// message counts: scan aun dir, 根据时间范围和 agent/peer 过滤
|
|
214
295
|
let msgIn = 0, msgOut = 0;
|
|
215
296
|
try {
|
|
216
297
|
const aunDir = getSessionsAunDir();
|
|
217
|
-
|
|
218
|
-
|
|
298
|
+
const aids = params.agent_aid ? [params.agent_aid] : listLocalAids(aunDir);
|
|
299
|
+
for (const aid of aids) {
|
|
300
|
+
const peers = params.peer_key ? [params.peer_key] : listPeers(aunDir, aid);
|
|
301
|
+
for (const peer of peers) {
|
|
219
302
|
for (const m of readMessages(aunDir, aid, peer)) {
|
|
303
|
+
// 根据消息时间戳过滤
|
|
304
|
+
if (params.from_ts && m.ts < params.from_ts)
|
|
305
|
+
continue;
|
|
306
|
+
if (params.to_ts && m.ts > params.to_ts)
|
|
307
|
+
continue;
|
|
220
308
|
if (m.dir === 'in')
|
|
221
309
|
msgIn++;
|
|
222
310
|
else
|
|
@@ -229,6 +317,34 @@ function handleStatsApi(req, res) {
|
|
|
229
317
|
res.writeHead(200);
|
|
230
318
|
res.end(JSON.stringify({ token_stats: tokenStats, session_count: sessionCount, msg_in: msgIn, msg_out: msgOut }));
|
|
231
319
|
}
|
|
320
|
+
else if (urlPath === '/api/stats/detail') {
|
|
321
|
+
// 模型访问明细查询
|
|
322
|
+
const params = {};
|
|
323
|
+
if (query.from)
|
|
324
|
+
params.from_ts = Number(query.from);
|
|
325
|
+
if (query.to)
|
|
326
|
+
params.to_ts = Number(query.to);
|
|
327
|
+
if (query.agent)
|
|
328
|
+
params.agent_aid = query.agent;
|
|
329
|
+
if (query.model)
|
|
330
|
+
params.model = String(query.model);
|
|
331
|
+
params.limit = Number(query.limit) || 50; // 默认限制50条
|
|
332
|
+
params.offset = Number(query.offset) || 0; // 默认从0开始
|
|
333
|
+
const result = queryUsageDetail(params);
|
|
334
|
+
res.writeHead(200);
|
|
335
|
+
res.end(JSON.stringify(result));
|
|
336
|
+
}
|
|
337
|
+
else if (urlPath === '/api/stats/models') {
|
|
338
|
+
// 获取指定时间范围内使用过的模型列表
|
|
339
|
+
const params = {};
|
|
340
|
+
if (query.from)
|
|
341
|
+
params.from_ts = Number(query.from);
|
|
342
|
+
if (query.to)
|
|
343
|
+
params.to_ts = Number(query.to);
|
|
344
|
+
const models = queryUsedModels(params);
|
|
345
|
+
res.writeHead(200);
|
|
346
|
+
res.end(JSON.stringify(models));
|
|
347
|
+
}
|
|
232
348
|
else {
|
|
233
349
|
res.writeHead(404);
|
|
234
350
|
res.end(JSON.stringify({ error: 'not found' }));
|
|
@@ -246,6 +362,18 @@ function isLocalhost(req) {
|
|
|
246
362
|
const addr = req.socket.remoteAddress || '';
|
|
247
363
|
return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';
|
|
248
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* 判定「真本地直连」:socket 地址是回环 AND 无 x-aun-provider-aid 头。
|
|
367
|
+
* proxy-server 回连本地时 remoteAddress 也是 127.0.0.1,但会注入 x-aun-provider-aid
|
|
368
|
+
* 可信头(访客伪造的 x-aun-* 会被 proxy-server 剥掉)。只有真本地直连时两条件同时满足。
|
|
369
|
+
* 真本地直连免配对(自动发 token),隧道/远程需配对码。
|
|
370
|
+
*/
|
|
371
|
+
function isLocalDirect(req) {
|
|
372
|
+
if (!isLocalhost(req))
|
|
373
|
+
return false;
|
|
374
|
+
const providerAid = req.headers['x-aun-provider-aid'];
|
|
375
|
+
return !providerAid || (Array.isArray(providerAid) ? providerAid.length === 0 : !providerAid.trim());
|
|
376
|
+
}
|
|
249
377
|
function genPairingCode() {
|
|
250
378
|
return String(crypto.randomInt(0, 1000000)).padStart(6, '0');
|
|
251
379
|
}
|
|
@@ -281,6 +409,23 @@ function handlePair(req, res, pairingCode, pairingExpiry, log) {
|
|
|
281
409
|
log(`✓ 配对成功 from ${ip}(token 缓存 30 天,有访问自动续期)`);
|
|
282
410
|
});
|
|
283
411
|
}
|
|
412
|
+
/**
|
|
413
|
+
* 本地直连免配对:自动发 token。远程(隧道或真远程)需配对码。
|
|
414
|
+
* 本地直连判定:socket 来源是回环 AND 无 x-aun-provider-aid 头(非隧道)。
|
|
415
|
+
*/
|
|
416
|
+
function issueLocalDirectToken(req, log) {
|
|
417
|
+
if (!isLocalDirect(req))
|
|
418
|
+
return null;
|
|
419
|
+
const now = Date.now();
|
|
420
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
421
|
+
const store = loadTokens();
|
|
422
|
+
pruneExpired(store, now);
|
|
423
|
+
const ip = clientIp(req);
|
|
424
|
+
store.tokens.push({ token, createdAt: now, lastActive: now, label: `local-direct:${ip}` });
|
|
425
|
+
saveTokens(store);
|
|
426
|
+
log(`✓ 本地直连自动授权 from ${ip}`);
|
|
427
|
+
return token;
|
|
428
|
+
}
|
|
284
429
|
// ── WebSocket connection ──
|
|
285
430
|
function handleConnection(ws, req, log) {
|
|
286
431
|
const ip = clientIp(req);
|
|
@@ -342,7 +487,9 @@ function handleConnection(ws, req, log) {
|
|
|
342
487
|
params.project = msg.project;
|
|
343
488
|
if (msg.agent)
|
|
344
489
|
params.agent = msg.agent;
|
|
345
|
-
|
|
490
|
+
if (msg.baseagent)
|
|
491
|
+
params.baseagent = msg.baseagent;
|
|
492
|
+
dlog(`▸ 订阅 ${msg.view}${msg.aid ? ` aid=${String(msg.aid).split('.')[0]}` : ''}${msg.peer ? ` peer=${String(msg.peer).split('.')[0]}` : ''}${msg.sessionId ? ` session=${String(msg.sessionId).slice(0, 8)}` : ''}${msg.baseagent ? ` baseagent=${msg.baseagent}` : ''} from ${ip}`);
|
|
346
493
|
await switchSubscription(msg.view, params);
|
|
347
494
|
return;
|
|
348
495
|
}
|
|
@@ -470,9 +617,17 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
470
617
|
return { code: pairingCode, expiresAt: pairingExpiry };
|
|
471
618
|
}
|
|
472
619
|
const server = http.createServer((req, res) => {
|
|
473
|
-
if (req.method === 'GET' && (req.url || '') === '/api/
|
|
474
|
-
//
|
|
475
|
-
|
|
620
|
+
if (req.method === 'GET' && (req.url || '') === '/api/available-baseagents') {
|
|
621
|
+
// Base agent 可用性检测 API
|
|
622
|
+
const available = detectBaseAgents();
|
|
623
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
624
|
+
res.end(JSON.stringify(available));
|
|
625
|
+
}
|
|
626
|
+
else if (req.method === 'GET' && (req.url || '') === '/api/pair-code') {
|
|
627
|
+
// 取码 API:仅真本地直连可用(socket 回环 + 无 x-aun-provider-aid)。
|
|
628
|
+
// 隧道回连虽然 socket 也是 127.0.0.1,但有 x-aun-provider-aid 头 → 拒绝,
|
|
629
|
+
// 防止远程访客通过隧道拿到配对码(安全漏洞)。
|
|
630
|
+
if (!isLocalDirect(req)) {
|
|
476
631
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
477
632
|
res.end(JSON.stringify({ error: 'forbidden' }));
|
|
478
633
|
return;
|
|
@@ -482,15 +637,27 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
482
637
|
res.end(JSON.stringify({ code, expiresAt }));
|
|
483
638
|
}
|
|
484
639
|
else if (req.method === 'POST' && (req.url || '').startsWith('/api/pair')) {
|
|
640
|
+
// 配对 API:远程(隧道/真远程)需要配对码;本地直连自动发 token 跳过配对。
|
|
641
|
+
const autoToken = issueLocalDirectToken(req, log);
|
|
642
|
+
if (autoToken) {
|
|
643
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
644
|
+
res.end(JSON.stringify({ ok: true, token: autoToken }));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
485
647
|
handlePair(req, res, pairingCode, pairingExpiry, log);
|
|
486
648
|
}
|
|
487
649
|
else if (req.method === 'GET' && (req.url || '').startsWith('/api/stats/')) {
|
|
488
|
-
// Stats API —
|
|
650
|
+
// Stats API — 本地直连自动发 token(免鉴权),远程需鉴权
|
|
489
651
|
const authHeader = req.headers.authorization || '';
|
|
490
652
|
const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
|
|
491
653
|
const { query } = parseUrl(req.url || '');
|
|
492
|
-
|
|
493
|
-
if (!
|
|
654
|
+
let token = bearerToken || query.token || '';
|
|
655
|
+
if (!token) {
|
|
656
|
+
const autoToken = issueLocalDirectToken(req, log);
|
|
657
|
+
if (autoToken)
|
|
658
|
+
token = autoToken;
|
|
659
|
+
}
|
|
660
|
+
if (!token || !validateAndRenew(token, Date.now())) {
|
|
494
661
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
495
662
|
res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
496
663
|
return;
|
|
@@ -504,7 +671,8 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
504
671
|
const wss = new WebSocketServer({ noServer: true });
|
|
505
672
|
server.on('upgrade', (req, socket, head) => {
|
|
506
673
|
const { query } = parseUrl(req.url || '');
|
|
507
|
-
|
|
674
|
+
// 本地直连免 token;远程需 token。
|
|
675
|
+
const authed = isLocalDirect(req) || validateAndRenew(query.token || '', Date.now());
|
|
508
676
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
509
677
|
if (!authed) {
|
|
510
678
|
log(`✗ WS 拒绝(无效 token) from ${clientIp(req)}`);
|
package/dist/sources/aid.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AID 数据源 — 复用 daemon 的 IPC socket(与 evolclaw `watch aid` 同源)。
|
|
3
3
|
*
|
|
4
|
-
* daemon 运行时:拉 aun-aids / aun-aid-stats / status / evolagent.list。
|
|
4
|
+
* daemon 运行时:拉 aun-aids / aun-aid-stats / agent-stats / status / evolagent.list。
|
|
5
5
|
* daemon 未运行时:降级到读 instance/ 目录的 aid-*.jsonl(精简版 scanInstances)。
|
|
6
6
|
* IPC 无推送能力,故 1s 轮询 + JSON diff,仅在变化时 push。
|
|
7
7
|
*/
|
|
@@ -83,9 +83,10 @@ async function fetchVersion(socket) {
|
|
|
83
83
|
}
|
|
84
84
|
async function buildSnapshot() {
|
|
85
85
|
const p = resolvePaths();
|
|
86
|
-
const [aidsResp, statsResp, statusResp, agentsResp, version] = await Promise.all([
|
|
86
|
+
const [aidsResp, statsResp, agentStatsResp, statusResp, agentsResp, version] = await Promise.all([
|
|
87
87
|
ipcQuery(p.socket, { type: 'aun-aids' }),
|
|
88
88
|
ipcQuery(p.socket, { type: 'aun-aid-stats' }),
|
|
89
|
+
ipcQuery(p.socket, { type: 'agent-stats' }),
|
|
89
90
|
ipcQuery(p.socket, { type: 'status' }),
|
|
90
91
|
ipcQuery(p.socket, { type: 'evolagent.list' }),
|
|
91
92
|
fetchVersion(p.socket),
|
|
@@ -105,6 +106,7 @@ async function buildSnapshot() {
|
|
|
105
106
|
daemonRunning: true,
|
|
106
107
|
aids: aidsResp?.aids ?? [],
|
|
107
108
|
stats: statsResp?.stats ?? [],
|
|
109
|
+
agentStats: agentStatsResp?.stats ?? [],
|
|
108
110
|
status: statusResp ?? null,
|
|
109
111
|
agents: agentsResp?.agents ?? [],
|
|
110
112
|
version: version ?? null,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base agent 环境检测 — 检测 Claude 和 Codex 的可用性。
|
|
3
|
+
*
|
|
4
|
+
* 检测条件:
|
|
5
|
+
* - Claude: ~/.claude/projects/ 目录存在
|
|
6
|
+
* - Codex: ~/.codex/sessions/ 和 ~/.codex/state_*.sqlite 存在,且 Node 版本 >= 22.5
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { createRequire } from 'module';
|
|
12
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
13
|
+
let _cached = null;
|
|
14
|
+
/**
|
|
15
|
+
* 检测 node:sqlite 模块是否可用(Node 22.5+)
|
|
16
|
+
*/
|
|
17
|
+
function checkSqliteAvailable() {
|
|
18
|
+
try {
|
|
19
|
+
requireFromHere('node:sqlite');
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 检测 Codex state_*.sqlite 文件是否存在
|
|
28
|
+
*/
|
|
29
|
+
function checkCodexStateDb() {
|
|
30
|
+
const codexHome = path.join(os.homedir(), '.codex');
|
|
31
|
+
if (!fs.existsSync(codexHome))
|
|
32
|
+
return false;
|
|
33
|
+
try {
|
|
34
|
+
const files = fs.readdirSync(codexHome).filter(f => /^state_\d+\.sqlite$/.test(f));
|
|
35
|
+
return files.length > 0;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 检测 Claude projects 目录是否存在
|
|
43
|
+
*/
|
|
44
|
+
function checkClaudeProjects() {
|
|
45
|
+
const claudeProjects = path.join(os.homedir(), '.claude', 'projects');
|
|
46
|
+
return fs.existsSync(claudeProjects);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 检测 Codex sessions 目录是否存在
|
|
50
|
+
*/
|
|
51
|
+
function checkCodexSessions() {
|
|
52
|
+
const codexSessions = path.join(os.homedir(), '.codex', 'sessions');
|
|
53
|
+
return fs.existsSync(codexSessions);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 检测所有 base agent 的可用性
|
|
57
|
+
* 结果会缓存,避免重复检测
|
|
58
|
+
*/
|
|
59
|
+
export function detectBaseAgents() {
|
|
60
|
+
if (_cached)
|
|
61
|
+
return _cached;
|
|
62
|
+
const claude = checkClaudeProjects();
|
|
63
|
+
const codex = checkCodexSessions() && checkCodexStateDb() && checkSqliteAvailable();
|
|
64
|
+
_cached = { claude, codex };
|
|
65
|
+
return _cached;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 重置缓存(测试用)
|
|
69
|
+
*/
|
|
70
|
+
export function resetDetectionCache() {
|
|
71
|
+
_cached = null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gateway.ts — 网关(baseagent 后端)配置数据源(只读代理)。
|
|
3
|
+
*
|
|
4
|
+
* ECWeb 不直接读写配置文件。snapshot 通过 daemon 的 menu.exec IPC 调
|
|
5
|
+
* menu.query name=gateway,由 daemon 端的 command-handler-gateway-control 统一
|
|
6
|
+
* 出数据(apiKey 已掩码)。写操作(update/test/delete)走前端 menuSend → menu IPC,
|
|
7
|
+
* 不经过本 source。
|
|
8
|
+
*
|
|
9
|
+
* 连不上 daemon 时返回 { gateways: [], error } 让前端提示。
|
|
10
|
+
*/
|
|
11
|
+
import { resolvePaths } from '../paths.js';
|
|
12
|
+
import { ipcQuery } from '../ipc-client.js';
|
|
13
|
+
async function getSnapshot() {
|
|
14
|
+
const sock = resolvePaths().socket;
|
|
15
|
+
const resp = await ipcQuery(sock, { type: 'menu.exec', payload: { type: 'menu.query', name: 'gateway', id: 'ecw-gateway-snap' } }, 5000);
|
|
16
|
+
if (!resp) {
|
|
17
|
+
return { gateways: [], scopes: [], types: [], error: 'evolclaw 未运行或 socket 不可达' };
|
|
18
|
+
}
|
|
19
|
+
if (!resp.ok) {
|
|
20
|
+
return { gateways: [], scopes: [], types: [], error: resp.error || 'daemon 返回失败' };
|
|
21
|
+
}
|
|
22
|
+
const inner = resp.response;
|
|
23
|
+
if (inner?.error) {
|
|
24
|
+
return { gateways: [], scopes: [], types: [], error: inner.error.message };
|
|
25
|
+
}
|
|
26
|
+
const data = inner?.data ?? {};
|
|
27
|
+
return {
|
|
28
|
+
gateways: Array.isArray(data.gateways) ? data.gateways : [],
|
|
29
|
+
scopes: Array.isArray(data.scopes) ? data.scopes : [],
|
|
30
|
+
types: Array.isArray(data.types) ? data.types : [],
|
|
31
|
+
effective: Array.isArray(data.effective) ? data.effective : [],
|
|
32
|
+
envMismatch: data.envMismatch || { hasMismatch: false, mismatches: [] },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export const gatewaySource = {
|
|
36
|
+
kind: 'gateway',
|
|
37
|
+
async snapshot() {
|
|
38
|
+
return getSnapshot();
|
|
39
|
+
},
|
|
40
|
+
subscribe(_params, _push) {
|
|
41
|
+
// 网关配置变更不频繁,无文件监听;前端通过操作后主动 re-subscribe 刷新。
|
|
42
|
+
return () => { };
|
|
43
|
+
},
|
|
44
|
+
};
|