ec-web 0.0.2 → 0.0.4
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/fs-utils.js +3 -3
- package/dist/index.js +6 -6
- package/dist/ipc-client.js +2 -2
- package/dist/paths.js +12 -12
- package/dist/process-utils.js +2 -2
- package/dist/server.js +29 -10
- package/dist/sources/aid.js +2 -2
- package/dist/sources/gateway.js +1 -1
- package/dist/sources/logs.js +2 -2
- package/dist/sources/monitor.js +90 -7
- package/dist/sources/msg.js +7 -3
- package/dist/sources/parent-package.js +8 -8
- package/dist/sources/relation-roles.js +2 -24
- package/dist/sources/role-definitions.js +16 -34
- package/dist/sources/session-codex.js +1 -1
- package/dist/sources/session.js +1 -1
- package/dist/static/app.js +142 -115
- package/dist/static/index.html +5 -5
- package/dist/static/style.css +26 -0
- package/package.json +2 -2
package/dist/fs-utils.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 文件系统工具层(独立版)— 内联自
|
|
2
|
+
* 文件系统工具层(独立版)— 内联自 evolcore session-fs-store + watch-msg 数据层。
|
|
3
3
|
*
|
|
4
4
|
* 只包含 watch-web 实际用到的函数,无任何 npm 依赖。
|
|
5
5
|
*/
|
|
6
6
|
import fs from 'fs';
|
|
7
7
|
import path from 'path';
|
|
8
8
|
import { resolvePaths } from './paths.js';
|
|
9
|
-
// ── 编解码(与
|
|
9
|
+
// ── 编解码(与 evolcore encodeSegment / decodeDirSegment 一致)──
|
|
10
10
|
const UNSAFE_CHARS_RE = /[<>:"/\\|?*\x00-\x1F%]/g;
|
|
11
11
|
export function encodeSegment(s) {
|
|
12
12
|
return s.replace(UNSAFE_CHARS_RE, ch => '%' + ch.charCodeAt(0).toString(16).toUpperCase().padStart(2, '0'));
|
|
@@ -14,7 +14,7 @@ export function encodeSegment(s) {
|
|
|
14
14
|
export function decodeDirSegment(seg) {
|
|
15
15
|
return seg.replace(/%([0-9A-Fa-f]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
16
16
|
}
|
|
17
|
-
/** 与
|
|
17
|
+
/** 与 evolcore cross-platform.ts encodePath 一致(CC transcript 目录名) */
|
|
18
18
|
export function encodePath(p) {
|
|
19
19
|
const isWindows = process.platform === 'win32';
|
|
20
20
|
const norm = isWindows ? p.replace(/\\/g, '/') : p;
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* ec-web — EvolCore 监控面板独立程序。
|
|
4
4
|
*
|
|
5
5
|
* 用法:
|
|
6
|
-
* ec-web [--port 42705] [--home <
|
|
6
|
+
* ec-web [--port 42705] [--home <EVOLCORE_HOME>]
|
|
7
7
|
*
|
|
8
|
-
* 通过
|
|
9
|
-
* 与
|
|
8
|
+
* 通过 EVOLCORE_HOME 定位 evolcore 数据目录,启动 HTTP+WS 服务,浏览器配对码登录。
|
|
9
|
+
* 与 evolcore daemon 通过 IPC socket(live 状态)+ 文件系统(历史数据)旁路通信。
|
|
10
10
|
*/
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
@@ -22,20 +22,20 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
22
22
|
else if (a === '--home' || a === '-h')
|
|
23
23
|
home = argv[++i];
|
|
24
24
|
else if (a === '--help') {
|
|
25
|
-
process.stdout.write(`ec-web — EvolCore 监控面板\n\n用法:\n ec-web [--port 42705] [--home <
|
|
25
|
+
process.stdout.write(`ec-web — EvolCore 监控面板\n\n用法:\n ec-web [--port 42705] [--home <EVOLCORE_HOME>]\n\n选项:\n --port, -p 监听端口(默认 42705,占用则自动 +1)\n --home 当前运行时数据目录(现阶段默认读 EVOLCORE_HOME 或 ~/.evolcore)\n`);
|
|
26
26
|
process.exit(0);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
// --home 优先级最高:写入环境变量,paths.ts 读取
|
|
30
30
|
if (home)
|
|
31
|
-
process.env.
|
|
31
|
+
process.env.EVOLCORE_HOME = path.resolve(home);
|
|
32
32
|
// 延迟到环境变量设置后再导入(paths.ts 缓存 root)
|
|
33
33
|
const { resolvePaths } = await import('./paths.js');
|
|
34
34
|
const { startWatchWebServer } = await import('./server.js');
|
|
35
35
|
const p = resolvePaths();
|
|
36
36
|
// 校验 home 目录存在
|
|
37
37
|
if (!fs.existsSync(p.root)) {
|
|
38
|
-
process.stderr.write(`❌
|
|
38
|
+
process.stderr.write(`❌ EVOLCORE_HOME 不存在: ${p.root}\n 用 --home 指定正确路径,或设置 EVOLCORE_HOME 环境变量。\n`);
|
|
39
39
|
process.exit(1);
|
|
40
40
|
}
|
|
41
41
|
const useColor = !!process.stdout.isTTY;
|
package/dist/ipc-client.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* evolcore IPC client(独立版)— 通过 Unix socket / 命名管道查询运行中的 daemon。
|
|
3
3
|
*
|
|
4
4
|
* 协议:newline-delimited JSON,一问一答即断。
|
|
5
|
-
* 与
|
|
5
|
+
* 与 evolcore src/ipc.ts 的 ipcQuery 一致;连不上返回 null(daemon 未运行)。
|
|
6
6
|
*
|
|
7
7
|
* watch 用到的只读查询:aun-aids / aun-aid-stats / status / evolagent.list / ping
|
|
8
8
|
*/
|
package/dist/paths.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* evolcore 路径解析(独立版)。
|
|
3
3
|
*
|
|
4
|
-
* watch 进程只需要
|
|
5
|
-
* 逻辑与
|
|
4
|
+
* watch 进程只需要 EVOLCORE_HOME 根目录,所有地址从它派生。
|
|
5
|
+
* 逻辑与 evolcore src/paths.ts 对齐,但不依赖 evolcore 代码。
|
|
6
6
|
*
|
|
7
7
|
* 根目录解析优先级:
|
|
8
|
-
* 1. --home 命令行参数(由 index.ts 写入 process.env.
|
|
9
|
-
* 2.
|
|
8
|
+
* 1. --home 命令行参数(由 index.ts 写入 process.env.EVOLCORE_HOME)
|
|
9
|
+
* 2. EVOLCORE_HOME 环境变量
|
|
10
10
|
* 3. cwd/agents/defaults.json 存在 → cwd(开发模式)
|
|
11
|
-
* 4. ~/.
|
|
11
|
+
* 4. ~/.evolcore
|
|
12
12
|
*/
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import os from 'os';
|
|
@@ -19,24 +19,24 @@ let _root = null;
|
|
|
19
19
|
export function resolveRoot() {
|
|
20
20
|
if (_root)
|
|
21
21
|
return _root;
|
|
22
|
-
if (process.env.
|
|
23
|
-
_root = process.env.
|
|
22
|
+
if (process.env.EVOLCORE_HOME) {
|
|
23
|
+
_root = process.env.EVOLCORE_HOME;
|
|
24
24
|
}
|
|
25
25
|
else if (fs.existsSync(path.join(process.cwd(), 'agents', 'defaults.json'))) {
|
|
26
26
|
_root = process.cwd();
|
|
27
27
|
}
|
|
28
28
|
else {
|
|
29
|
-
_root = path.join(os.homedir(), '.
|
|
29
|
+
_root = path.join(os.homedir(), '.evolcore');
|
|
30
30
|
}
|
|
31
31
|
return _root;
|
|
32
32
|
}
|
|
33
|
-
/** socket 路径派生:与
|
|
33
|
+
/** socket 路径派生:与 evolcore resolveInstanceSocketPath 完全一致 */
|
|
34
34
|
function resolveInstanceSocketPath(root) {
|
|
35
35
|
if (isWindows) {
|
|
36
36
|
const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
|
|
37
|
-
return `\\\\.\\pipe\\
|
|
37
|
+
return `\\\\.\\pipe\\daemon-${hash}`;
|
|
38
38
|
}
|
|
39
|
-
return path.join(root, 'data', 'instance', '
|
|
39
|
+
return path.join(root, 'data', 'instance', 'daemon.sock');
|
|
40
40
|
}
|
|
41
41
|
export function resolvePaths() {
|
|
42
42
|
const root = resolveRoot();
|
package/dist/process-utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 进程工具(独立版)— 内联自
|
|
2
|
+
* 进程工具(独立版)— 内联自 evolcore cross-platform + process-introspect + instance-registry。
|
|
3
3
|
*
|
|
4
|
-
* 只包含 ecweb 单实例保护实际用到的函数,无任何
|
|
4
|
+
* 只包含 ecweb 单实例保护实际用到的函数,无任何 evolcore 代码依赖。
|
|
5
5
|
* 关键不变量:杀进程前用启动时间比对防 PID 复用,按端口兜底时只杀确认是 ecweb 的进程。
|
|
6
6
|
*/
|
|
7
7
|
import fs from 'fs';
|
package/dist/server.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - 鉴权: 6 位配对码(5 分钟有效)→ token(30 天,有访问自动续期),持久化到磁盘
|
|
7
7
|
* - 安全: 绑定 0.0.0.0(支持远程访问),token 校验,只读
|
|
8
8
|
*
|
|
9
|
-
* 与
|
|
9
|
+
* 与 evolcore 的唯一通信:启动时发 ping 检查 protocolVersion(soft 校验)。
|
|
10
10
|
*/
|
|
11
11
|
import http from 'http';
|
|
12
12
|
import fs from 'fs';
|
|
@@ -39,7 +39,7 @@ const STATIC_DIR = path.join(__dirname, 'static');
|
|
|
39
39
|
const TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30天(滑动窗口:每次有效访问刷新 lastActive 自动续期)
|
|
40
40
|
const PAIRING_TTL_MS = 5 * 60 * 1000; // 5min
|
|
41
41
|
const DEFAULT_PORT = 42705;
|
|
42
|
-
const PROTOCOL_VERSION = 1; // 与
|
|
42
|
+
const PROTOCOL_VERSION = 1; // 与 evolcore ping response 对齐的软校验版本
|
|
43
43
|
function daemonOwnerMenuAuth() {
|
|
44
44
|
return { localDirect: true, actorAid: null };
|
|
45
45
|
}
|
|
@@ -446,6 +446,7 @@ function handleConnection(ws, req, log) {
|
|
|
446
446
|
const ip = clientIp(req);
|
|
447
447
|
let unsubscribe = null;
|
|
448
448
|
let currentView = null;
|
|
449
|
+
let subscriptionGeneration = 0;
|
|
449
450
|
log(`◆ WS 连接 from ${ip}`);
|
|
450
451
|
const send = (obj) => {
|
|
451
452
|
if (ws.readyState === ws.OPEN)
|
|
@@ -455,6 +456,7 @@ function handleConnection(ws, req, log) {
|
|
|
455
456
|
catch { }
|
|
456
457
|
};
|
|
457
458
|
const switchSubscription = async (view, params) => {
|
|
459
|
+
const generation = ++subscriptionGeneration;
|
|
458
460
|
if (unsubscribe) {
|
|
459
461
|
unsubscribe();
|
|
460
462
|
unsubscribe = null;
|
|
@@ -468,16 +470,28 @@ function handleConnection(ws, req, log) {
|
|
|
468
470
|
const sourceParams = { ...params, __menuAuth: daemonOwnerMenuAuth() };
|
|
469
471
|
// System 视图:把 ECWeb 自身版本合并进快照(前端版本卡用)
|
|
470
472
|
const decorate = (data) => (view === 'system' && data ? { ...data, ecwebVersion: ECWEB_VERSION } : data);
|
|
473
|
+
let initialSnapshot;
|
|
471
474
|
try {
|
|
472
|
-
|
|
475
|
+
initialSnapshot = await source.snapshot(sourceParams);
|
|
476
|
+
if (generation !== subscriptionGeneration)
|
|
477
|
+
return;
|
|
478
|
+
send({ type: 'snapshot', view, data: decorate(initialSnapshot) });
|
|
473
479
|
}
|
|
474
480
|
catch (e) {
|
|
481
|
+
if (generation !== subscriptionGeneration)
|
|
482
|
+
return;
|
|
475
483
|
send({ type: 'error', message: `snapshot failed: ${e?.message || e}` });
|
|
476
484
|
}
|
|
477
|
-
|
|
478
|
-
if (currentView === view)
|
|
485
|
+
const nextUnsubscribe = source.subscribe(sourceParams, (data) => {
|
|
486
|
+
if (generation === subscriptionGeneration && currentView === view) {
|
|
479
487
|
send({ type: 'delta', view, data: decorate(data) });
|
|
488
|
+
}
|
|
480
489
|
});
|
|
490
|
+
if (generation !== subscriptionGeneration) {
|
|
491
|
+
nextUnsubscribe();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
unsubscribe = nextUnsubscribe;
|
|
481
495
|
};
|
|
482
496
|
ws.on('message', async (raw) => {
|
|
483
497
|
let msg;
|
|
@@ -563,10 +577,15 @@ function handleConnection(ws, req, log) {
|
|
|
563
577
|
ws.ping();
|
|
564
578
|
}, 25000);
|
|
565
579
|
ws.on('pong', () => { alive = true; });
|
|
566
|
-
ws.on('close', () => {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
580
|
+
ws.on('close', () => {
|
|
581
|
+
subscriptionGeneration += 1;
|
|
582
|
+
clearInterval(heartbeat);
|
|
583
|
+
if (unsubscribe) {
|
|
584
|
+
unsubscribe();
|
|
585
|
+
unsubscribe = null;
|
|
586
|
+
}
|
|
587
|
+
log(`◇ WS 断开 from ${ip}`);
|
|
588
|
+
});
|
|
570
589
|
ws.on('error', () => { });
|
|
571
590
|
}
|
|
572
591
|
// ── Port binding ──
|
|
@@ -644,7 +663,7 @@ export async function startWatchWebServer(opts = {}) {
|
|
|
644
663
|
const p = resolvePaths();
|
|
645
664
|
const pingResp = await ipcQuery(p.socket, { type: 'ping' }, 1000);
|
|
646
665
|
if (pingResp && pingResp.protocolVersion !== undefined && pingResp.protocolVersion < PROTOCOL_VERSION) {
|
|
647
|
-
log(`⚠️
|
|
666
|
+
log(`⚠️ evolcore protocolVersion=${pingResp.protocolVersion},watch 期望 >=${PROTOCOL_VERSION},部分功能可能异常`);
|
|
648
667
|
}
|
|
649
668
|
let pairingCode = genPairingCode();
|
|
650
669
|
let pairingExpiry = Date.now() + PAIRING_TTL_MS;
|
package/dist/sources/aid.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AID 数据源 — 复用 daemon 的 IPC socket(与
|
|
2
|
+
* AID 数据源 — 复用 daemon 的 IPC socket(与 evolcore `watch aid` 同源)。
|
|
3
3
|
*
|
|
4
4
|
* daemon 运行时:拉 aun-aids / aun-aid-stats / agent-stats / status / evolagent.list。
|
|
5
5
|
* daemon 未运行时:降级到读 instance/ 目录的 aid-*.jsonl(精简版 scanInstances)。
|
|
@@ -64,7 +64,7 @@ function scanAidActivity() {
|
|
|
64
64
|
}
|
|
65
65
|
return result;
|
|
66
66
|
}
|
|
67
|
-
//
|
|
67
|
+
// evolcore 版本:与 watch aid 状态栏一致。daemon 进程级、整轮 watch 不变,
|
|
68
68
|
// 故缓存一次(首次 menu.query name=system 拿到后复用),避免每秒多发一次 IPC。
|
|
69
69
|
let _cachedVersion = null;
|
|
70
70
|
async function fetchVersion(socket) {
|
package/dist/sources/gateway.js
CHANGED
|
@@ -14,7 +14,7 @@ async function getSnapshot() {
|
|
|
14
14
|
const sock = resolvePaths().socket;
|
|
15
15
|
const resp = await ipcQuery(sock, { type: 'menu.exec', payload: { type: 'menu.query', name: 'gateway', id: 'ecw-gateway-snap' }, auth: { source: 'ecweb', localDirect: true } }, 5000);
|
|
16
16
|
if (!resp) {
|
|
17
|
-
return { gateways: [], scopes: [], types: [], error: '
|
|
17
|
+
return { gateways: [], scopes: [], types: [], error: 'evolcore 未运行或 socket 不可达' };
|
|
18
18
|
}
|
|
19
19
|
if (!resp.ok) {
|
|
20
20
|
return { gateways: [], scopes: [], types: [], error: resp.error || 'daemon 返回失败' };
|
package/dist/sources/logs.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { resolvePaths } from '../paths.js';
|
|
4
4
|
const LOG_SOURCES = [
|
|
5
|
-
{ id: '
|
|
5
|
+
{ id: 'daemon', label: 'Daemon', pattern: /^daemon(?:-.+)?\.log$/ },
|
|
6
6
|
{ id: 'ecweb', label: 'ECWeb', pattern: /^(?:watch-web|ecweb[^/]*)\.(?:log|out)$/ },
|
|
7
7
|
{ id: 'stdout', label: 'Standard output', pattern: /^stdout\.log(?:\..+)?$/ },
|
|
8
8
|
{ id: 'aun', label: 'AUN', pattern: /^aun(?:-.+)?\.log$/ },
|
|
@@ -350,7 +350,7 @@ function buildSnapshot(params = {}) {
|
|
|
350
350
|
const requested = String(params.source || '');
|
|
351
351
|
const selectedSource = sources.some(source => source.id === requested)
|
|
352
352
|
? requested
|
|
353
|
-
: (sources.some(source => source.id === '
|
|
353
|
+
: (sources.some(source => source.id === 'daemon') ? 'daemon' : (sources[0]?.id || null));
|
|
354
354
|
const source = LOG_SOURCES.find(item => item.id === selectedSource);
|
|
355
355
|
const limit = Math.min(MAX_LIMIT, Math.max(100, Number(params.limit) || DEFAULT_LIMIT));
|
|
356
356
|
const lines = source ? linesFor(files, source, limit) : [];
|
package/dist/sources/monitor.js
CHANGED
|
@@ -8,11 +8,44 @@
|
|
|
8
8
|
* - fine : 2s 桶 × 60 ≈ 近 2 分钟
|
|
9
9
|
* - mid : 10s 桶 × 60 ≈ 近 10 分钟
|
|
10
10
|
* - coarse : 60s 桶 × 60 ≈ 近 1 小时
|
|
11
|
-
* 每个桶内对 CPU
|
|
11
|
+
* 每个桶内对 CPU/内存做均值聚合,关联进程先按类型和名称合并,
|
|
12
|
+
* 避免长周期下点数爆炸,也不让 PID 变化拆散同一条曲线。
|
|
12
13
|
*/
|
|
13
14
|
import fs from 'node:fs';
|
|
14
15
|
import { resolvePaths } from '../paths.js';
|
|
15
16
|
import { ipcQuery } from '../ipc-client.js';
|
|
17
|
+
function processGroupKey(process) {
|
|
18
|
+
return `${process.kind}\u0000${process.name}`;
|
|
19
|
+
}
|
|
20
|
+
/** 同一类、同一可执行名的多个实例在一个采样点中汇总。 */
|
|
21
|
+
export function mergeProcessesByIdentity(processes) {
|
|
22
|
+
const merged = new Map();
|
|
23
|
+
for (const process of processes) {
|
|
24
|
+
const name = String(process.name || 'process');
|
|
25
|
+
const kind = String(process.kind || 'child');
|
|
26
|
+
const key = processGroupKey({ name, kind });
|
|
27
|
+
const current = merged.get(key);
|
|
28
|
+
if (current) {
|
|
29
|
+
current.cpuPercent += Number(process.cpuPercent) || 0;
|
|
30
|
+
current.rss += Number(process.rss) || 0;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
merged.set(key, {
|
|
34
|
+
name,
|
|
35
|
+
kind,
|
|
36
|
+
cpuPercent: Number(process.cpuPercent) || 0,
|
|
37
|
+
rss: Number(process.rss) || 0,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...merged.values()].sort((left, right) => processGroupKey(left).localeCompare(processGroupKey(right)));
|
|
42
|
+
}
|
|
43
|
+
/** 将进程的单核 CPU 百分比换算为整机全部逻辑 CPU 的容量占比。 */
|
|
44
|
+
export function normalizeProcessCpuPercent(cpuPercent, cpuCount) {
|
|
45
|
+
const count = Number(cpuCount);
|
|
46
|
+
const divisor = Number.isFinite(count) && count > 0 ? count : 1;
|
|
47
|
+
return (Number(cpuPercent) || 0) / divisor;
|
|
48
|
+
}
|
|
16
49
|
// 模块级缓冲区——所有订阅者共享同一份累积时序(单一事实源)。
|
|
17
50
|
const resolutions = {
|
|
18
51
|
fine: { bucketMs: 2_000, cap: 60, buf: [] },
|
|
@@ -29,10 +62,12 @@ function ingest(res, sample) {
|
|
|
29
62
|
last.sysCpu = last.sysCpu + (sample.sysCpu - last.sysCpu) / n;
|
|
30
63
|
last.procRss = last.procRss + (sample.procRss - last.procRss) / n;
|
|
31
64
|
last.sysMemUsed = last.sysMemUsed + (sample.sysMemUsed - last.sysMemUsed) / n;
|
|
65
|
+
last.diskReadBps = last.diskReadBps + (sample.diskReadBps - last.diskReadBps) / n;
|
|
66
|
+
last.diskWriteBps = last.diskWriteBps + (sample.diskWriteBps - last.diskWriteBps) / n;
|
|
32
67
|
last.netRxBps = last.netRxBps + (sample.netRxBps - last.netRxBps) / n;
|
|
33
68
|
last.netTxBps = last.netTxBps + (sample.netTxBps - last.netTxBps) / n;
|
|
34
69
|
for (const process of sample.processes) {
|
|
35
|
-
const current = last.processes.find(item => item
|
|
70
|
+
const current = last.processes.find(item => processGroupKey(item) === processGroupKey(process));
|
|
36
71
|
if (current) {
|
|
37
72
|
const processSamples = current._n + 1;
|
|
38
73
|
current.cpuPercent += (process.cpuPercent - current.cpuPercent) / processSamples;
|
|
@@ -61,10 +96,11 @@ function exportBuf(res) {
|
|
|
61
96
|
sysCpu: round1(b.sysCpu),
|
|
62
97
|
procRss: Math.round(b.procRss),
|
|
63
98
|
sysMemUsed: Math.round(b.sysMemUsed),
|
|
99
|
+
diskReadBps: Math.round(b.diskReadBps),
|
|
100
|
+
diskWriteBps: Math.round(b.diskWriteBps),
|
|
64
101
|
netRxBps: Math.round(b.netRxBps),
|
|
65
102
|
netTxBps: Math.round(b.netTxBps),
|
|
66
103
|
processes: b.processes.map(process => ({
|
|
67
|
-
pid: process.pid,
|
|
68
104
|
name: process.name,
|
|
69
105
|
kind: process.kind,
|
|
70
106
|
cpuPercent: round1(process.cpuPercent),
|
|
@@ -73,6 +109,49 @@ function exportBuf(res) {
|
|
|
73
109
|
}));
|
|
74
110
|
}
|
|
75
111
|
function round1(n) { return Math.round(n * 10) / 10; }
|
|
112
|
+
const DISK_SECTOR_BYTES = 512;
|
|
113
|
+
let previousDiskSample = null;
|
|
114
|
+
let latestDiskRate = { readBps: 0, writeBps: 0 };
|
|
115
|
+
/** 解析 Linux diskstats,只累计整盘,避免分区和虚拟映射重复计数。 */
|
|
116
|
+
export function parseLinuxDiskStats(input, isPartition = name => fs.existsSync(`/sys/class/block/${name}/partition`)) {
|
|
117
|
+
let readBytes = 0;
|
|
118
|
+
let writeBytes = 0;
|
|
119
|
+
for (const line of input.split('\n')) {
|
|
120
|
+
const fields = line.trim().split(/\s+/);
|
|
121
|
+
if (fields.length < 10)
|
|
122
|
+
continue;
|
|
123
|
+
const name = fields[2];
|
|
124
|
+
if (!name || /^(loop|ram|zram|fd|sr|dm-|md|zd)/.test(name) || isPartition(name))
|
|
125
|
+
continue;
|
|
126
|
+
readBytes += (Number(fields[5]) || 0) * DISK_SECTOR_BYTES;
|
|
127
|
+
writeBytes += (Number(fields[9]) || 0) * DISK_SECTOR_BYTES;
|
|
128
|
+
}
|
|
129
|
+
return { readBytes, writeBytes };
|
|
130
|
+
}
|
|
131
|
+
function readDiskTotals() {
|
|
132
|
+
try {
|
|
133
|
+
return parseLinuxDiskStats(fs.readFileSync('/proc/diskstats', 'utf8'));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { readBytes: 0, writeBytes: 0 };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function sampleDiskRate(ts) {
|
|
140
|
+
const totals = readDiskTotals();
|
|
141
|
+
if (!previousDiskSample) {
|
|
142
|
+
previousDiskSample = { ...totals, ts };
|
|
143
|
+
return latestDiskRate;
|
|
144
|
+
}
|
|
145
|
+
const elapsedMs = ts - previousDiskSample.ts;
|
|
146
|
+
if (elapsedMs < 250)
|
|
147
|
+
return latestDiskRate;
|
|
148
|
+
latestDiskRate = {
|
|
149
|
+
readBps: Math.max(0, (totals.readBytes - previousDiskSample.readBytes) * 1000 / elapsedMs),
|
|
150
|
+
writeBps: Math.max(0, (totals.writeBytes - previousDiskSample.writeBytes) * 1000 / elapsedMs),
|
|
151
|
+
};
|
|
152
|
+
previousDiskSample = { ...totals, ts };
|
|
153
|
+
return latestDiskRate;
|
|
154
|
+
}
|
|
76
155
|
let previousNetworkSample = null;
|
|
77
156
|
let latestNetworkRate = { rxBps: 0, txBps: 0 };
|
|
78
157
|
function readNetworkTotals() {
|
|
@@ -122,22 +201,26 @@ async function fetchSnapshot() {
|
|
|
122
201
|
}
|
|
123
202
|
const s = resp.snapshot;
|
|
124
203
|
const ts = Number(s.ts) || Date.now();
|
|
204
|
+
const cpuCount = Number(s.cpuCount) || 1;
|
|
205
|
+
const disk = sampleDiskRate(ts);
|
|
125
206
|
const network = sampleNetworkRate(ts);
|
|
126
207
|
const sample = {
|
|
127
208
|
ts,
|
|
128
|
-
procCpu: s.cpuPercent
|
|
209
|
+
procCpu: normalizeProcessCpuPercent(s.cpuPercent, cpuCount),
|
|
129
210
|
sysCpu: s.system?.cpuPercent ?? 0,
|
|
130
211
|
procRss: s.memory?.rss ?? 0,
|
|
131
212
|
sysMemUsed: s.system?.memUsed ?? 0,
|
|
213
|
+
diskReadBps: disk.readBps,
|
|
214
|
+
diskWriteBps: disk.writeBps,
|
|
132
215
|
netRxBps: network.rxBps,
|
|
133
216
|
netTxBps: network.txBps,
|
|
134
|
-
processes: Array.isArray(s.processTree?.processes) ? s.processTree.processes.map((process) => ({
|
|
217
|
+
processes: mergeProcessesByIdentity(Array.isArray(s.processTree?.processes) ? s.processTree.processes.map((process) => ({
|
|
135
218
|
pid: Number(process.pid),
|
|
136
219
|
name: String(process.name || 'process'),
|
|
137
220
|
kind: String(process.kind || 'child'),
|
|
138
|
-
cpuPercent:
|
|
221
|
+
cpuPercent: normalizeProcessCpuPercent(process.cpuPercent, cpuCount),
|
|
139
222
|
rss: Number(process.rss) || 0,
|
|
140
|
-
})) : [],
|
|
223
|
+
})) : []),
|
|
141
224
|
};
|
|
142
225
|
ingest(resolutions.fine, sample);
|
|
143
226
|
ingest(resolutions.mid, sample);
|
package/dist/sources/msg.js
CHANGED
|
@@ -67,6 +67,9 @@ function readLastJsonlObject(file) {
|
|
|
67
67
|
const rows = readAllJsonlLines(file);
|
|
68
68
|
return rows.length ? rows[rows.length - 1] : null;
|
|
69
69
|
}
|
|
70
|
+
export function latestMessageAt(messages) {
|
|
71
|
+
return messages.reduce((latest, message) => Math.max(latest, Number(message.ts) || 0), 0);
|
|
72
|
+
}
|
|
70
73
|
function loadThreadSessions(dirPath) {
|
|
71
74
|
const threadsDir = path.join(dirPath, '_threads');
|
|
72
75
|
const index = readJsonFile(path.join(threadsDir, 'thread-index.json')) || {};
|
|
@@ -316,8 +319,9 @@ function loadChats() {
|
|
|
316
319
|
continue;
|
|
317
320
|
const thread = item.threadId ? threadById.get(item.threadId) : undefined;
|
|
318
321
|
const partitionActive = item.partition === 'thread' ? thread?.active || {} : item.partition === 'main' ? active : {};
|
|
319
|
-
|
|
320
|
-
|
|
322
|
+
// active.updatedAt also changes for session metadata maintenance (for example,
|
|
323
|
+
// a project-path migration), so it is not a conversation activity timestamp.
|
|
324
|
+
const updatedAt = latestMessageAt(item.messages);
|
|
321
325
|
chats.push({
|
|
322
326
|
id: safeId(`${selfAID}\n${channelType}\n${channelId}\n${chatType}\n${item.partition}\n${item.threadId || ''}`),
|
|
323
327
|
dirPath,
|
|
@@ -550,7 +554,7 @@ async function buildSessionContext(chat) {
|
|
|
550
554
|
catch { }
|
|
551
555
|
return {
|
|
552
556
|
sessionId: active.agentSessionId || active.id || null,
|
|
553
|
-
|
|
557
|
+
evolcoreSessionId: active.id || null,
|
|
554
558
|
name: active.name || null,
|
|
555
559
|
baseagent: active.baseagent || active.agentType || null,
|
|
556
560
|
chatMode,
|
|
@@ -3,13 +3,13 @@ import path from 'path';
|
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
let packageRootCache = null;
|
|
6
|
-
function
|
|
6
|
+
function isEvolcorePackageRoot(dir) {
|
|
7
7
|
const pkgPath = path.join(dir, 'package.json');
|
|
8
8
|
if (!fs.existsSync(pkgPath))
|
|
9
9
|
return false;
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
12
|
-
return pkg?.name === '
|
|
12
|
+
return pkg?.name === 'evolcore';
|
|
13
13
|
}
|
|
14
14
|
catch {
|
|
15
15
|
return false;
|
|
@@ -18,7 +18,7 @@ function isEvolclawPackageRoot(dir) {
|
|
|
18
18
|
function findUp(start) {
|
|
19
19
|
let dir = path.resolve(start);
|
|
20
20
|
while (true) {
|
|
21
|
-
if (
|
|
21
|
+
if (isEvolcorePackageRoot(dir))
|
|
22
22
|
return dir;
|
|
23
23
|
const parent = path.dirname(dir);
|
|
24
24
|
if (parent === dir)
|
|
@@ -29,14 +29,14 @@ function findUp(start) {
|
|
|
29
29
|
function candidateStarts() {
|
|
30
30
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
31
31
|
const starts = [
|
|
32
|
-
process.env.
|
|
32
|
+
process.env.EVOLCORE_PACKAGE_ROOT,
|
|
33
33
|
here,
|
|
34
34
|
process.cwd(),
|
|
35
35
|
process.argv[1] ? path.dirname(process.argv[1]) : undefined,
|
|
36
36
|
];
|
|
37
37
|
return [...new Set(starts.filter((item) => !!item))];
|
|
38
38
|
}
|
|
39
|
-
export function
|
|
39
|
+
export function resolveEvolcorePackageRoot() {
|
|
40
40
|
if (packageRootCache)
|
|
41
41
|
return packageRootCache;
|
|
42
42
|
const searched = [];
|
|
@@ -50,9 +50,9 @@ export function resolveEvolclawPackageRoot() {
|
|
|
50
50
|
}
|
|
51
51
|
try {
|
|
52
52
|
const require = createRequire(import.meta.url);
|
|
53
|
-
const pkgJson = require.resolve('
|
|
53
|
+
const pkgJson = require.resolve('evolcore/package.json');
|
|
54
54
|
const root = path.dirname(pkgJson);
|
|
55
|
-
if (
|
|
55
|
+
if (isEvolcorePackageRoot(root)) {
|
|
56
56
|
packageRootCache = root;
|
|
57
57
|
return root;
|
|
58
58
|
}
|
|
@@ -63,7 +63,7 @@ export function resolveEvolclawPackageRoot() {
|
|
|
63
63
|
throw new Error(`Main package root not found. searched=${searched.join(', ')}`);
|
|
64
64
|
}
|
|
65
65
|
export function resolveParentDistModule(...segments) {
|
|
66
|
-
const root =
|
|
66
|
+
const root = resolveEvolcorePackageRoot();
|
|
67
67
|
const modulePath = path.join(root, 'dist', ...segments);
|
|
68
68
|
if (!fs.existsSync(modulePath)) {
|
|
69
69
|
throw new Error(`Main package dist module not found: ${modulePath}. Run npm run build in ${root}.`);
|
|
@@ -661,30 +661,8 @@ export const relationRolesSource = {
|
|
|
661
661
|
return { agents: [], conversations: [], error: String(err) };
|
|
662
662
|
}
|
|
663
663
|
},
|
|
664
|
-
subscribe(_params,
|
|
665
|
-
|
|
666
|
-
let stopped = false;
|
|
667
|
-
const tick = async () => {
|
|
668
|
-
if (stopped)
|
|
669
|
-
return;
|
|
670
|
-
try {
|
|
671
|
-
const snap = await buildSnapshot(_params.__menuAuth ?? {});
|
|
672
|
-
const json = JSON.stringify(snap);
|
|
673
|
-
if (json !== lastJson) {
|
|
674
|
-
lastJson = json;
|
|
675
|
-
push(snap);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
catch (err) {
|
|
679
|
-
console.error('[relation-roles] polling error:', err);
|
|
680
|
-
}
|
|
681
|
-
};
|
|
682
|
-
tick();
|
|
683
|
-
const timer = setInterval(tick, 2000);
|
|
684
|
-
return () => {
|
|
685
|
-
stopped = true;
|
|
686
|
-
clearInterval(timer);
|
|
687
|
-
};
|
|
664
|
+
subscribe(_params, _push) {
|
|
665
|
+
return () => { };
|
|
688
666
|
},
|
|
689
667
|
};
|
|
690
668
|
function requireScope(body) {
|
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
* 功能:
|
|
5
5
|
* 1. 读取 roles.json 或内置角色配置
|
|
6
6
|
* 2. 提供角色定义的 CRUD API
|
|
7
|
-
* 3. WebSocket
|
|
7
|
+
* 3. WebSocket 提供按需快照
|
|
8
8
|
*
|
|
9
9
|
* 数据流:
|
|
10
|
-
* - 读取:从
|
|
11
|
-
* - 写入:保存到 .
|
|
12
|
-
* -
|
|
10
|
+
* - 读取:从 evolcore/dist/config/roles.ts 导入 readRolesConfig
|
|
11
|
+
* - 写入:保存到 .evolcore/roles.json
|
|
12
|
+
* - 刷新:进入页面、切换 Agent 或完成写操作时按需加载
|
|
13
13
|
*/
|
|
14
14
|
import { getModelCatalogSnapshot } from './models.js';
|
|
15
15
|
import { resolveParentDistModule, toFileUrl } from './parent-package.js';
|
|
16
16
|
import { resolvePaths } from '../paths.js';
|
|
17
17
|
import { ipcQuery } from '../ipc-client.js';
|
|
18
|
-
// 动态导入
|
|
18
|
+
// 动态导入 evolcore 主项目的角色配置模块
|
|
19
19
|
async function getRolesModule() {
|
|
20
20
|
const rolesPath = resolveParentDistModule('config', 'roles.js');
|
|
21
21
|
const cmPath = resolveParentDistModule('config', 'config-manager.js');
|
|
@@ -116,6 +116,11 @@ async function rolePolicyRevision(aid, auth) {
|
|
|
116
116
|
throw Object.assign(new Error(response.error.message), { menuResponse: response });
|
|
117
117
|
return response.data.policyRevision;
|
|
118
118
|
}
|
|
119
|
+
export function visibleRoleDefinitionOptions(roleOptions) {
|
|
120
|
+
return roleOptions
|
|
121
|
+
.filter(item => item?.kind === 'user' || item?.value === 'admin')
|
|
122
|
+
.sort((left, right) => Number(right.value === 'admin') - Number(left.value === 'admin'));
|
|
123
|
+
}
|
|
119
124
|
async function buildMenuSnapshot(aid, agents, auth) {
|
|
120
125
|
const [roleOptions, defaults] = await Promise.all([
|
|
121
126
|
execRoleMenu(aid, { type: 'menu.options', id: `ecweb-role-options-${Date.now()}`, args: { option: 'roles' } }, auth),
|
|
@@ -125,8 +130,8 @@ async function buildMenuSnapshot(aid, agents, auth) {
|
|
|
125
130
|
throw Object.assign(new Error(roleOptions.error.message), { menuResponse: roleOptions });
|
|
126
131
|
if (defaults.error)
|
|
127
132
|
throw Object.assign(new Error(defaults.error.message), { menuResponse: defaults });
|
|
128
|
-
const
|
|
129
|
-
const details = await Promise.all(
|
|
133
|
+
const visibleRoles = visibleRoleDefinitionOptions(roleOptions.data);
|
|
134
|
+
const details = await Promise.all(visibleRoles.map(item => execRoleMenu(aid, {
|
|
130
135
|
type: 'menu.query', id: `ecweb-role-${item.value}-${Date.now()}`,
|
|
131
136
|
args: { view: 'role', roleId: item.value },
|
|
132
137
|
}, auth)));
|
|
@@ -424,8 +429,8 @@ function validateCommandPermissionConstraints(rule, constraints, errors) {
|
|
|
424
429
|
if (!Number.isInteger(value) || value < 0)
|
|
425
430
|
errors.push(`${label} must be a non-negative integer`);
|
|
426
431
|
}
|
|
427
|
-
else if (key === 'cwdPolicy' && !['agentProject', '
|
|
428
|
-
errors.push(`${label} must be agentProject,
|
|
432
|
+
else if (key === 'cwdPolicy' && !['agentProject', 'evolcoreHome', 'none'].includes(String(value))) {
|
|
433
|
+
errors.push(`${label} must be agentProject, evolcoreHome, or none`);
|
|
429
434
|
}
|
|
430
435
|
else if (key === 'allowedArgs' || key === 'deniedArgs') {
|
|
431
436
|
validateScalarArrayMap(value, label, errors);
|
|
@@ -634,31 +639,8 @@ export const roleDefinitionsSource = {
|
|
|
634
639
|
async snapshot(params = {}) {
|
|
635
640
|
return buildSnapshot(params, params.__menuAuth ?? {});
|
|
636
641
|
},
|
|
637
|
-
subscribe(_params,
|
|
638
|
-
|
|
639
|
-
let stopped = false;
|
|
640
|
-
const tick = async () => {
|
|
641
|
-
if (stopped)
|
|
642
|
-
return;
|
|
643
|
-
try {
|
|
644
|
-
const snap = await buildSnapshot(_params, _params.__menuAuth ?? {});
|
|
645
|
-
const json = JSON.stringify(snap);
|
|
646
|
-
if (json !== lastJson) {
|
|
647
|
-
lastJson = json;
|
|
648
|
-
push(snap);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
catch (err) {
|
|
652
|
-
console.error('[role-definitions] Polling error:', err);
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
// 立即执行一次
|
|
656
|
-
tick();
|
|
657
|
-
const timer = setInterval(tick, 2000); // 2秒轮询
|
|
658
|
-
return () => {
|
|
659
|
-
stopped = true;
|
|
660
|
-
clearInterval(timer);
|
|
661
|
-
};
|
|
642
|
+
subscribe(_params, _push) {
|
|
643
|
+
return () => { };
|
|
662
644
|
}
|
|
663
645
|
};
|
|
664
646
|
async function handleRoleMenuCompatibility(req, res, auth, aid, agents, parsedUrl) {
|