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 CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * 文件系统工具层(独立版)— 内联自 evolclaw session-fs-store + watch-msg 数据层。
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
- // ── 编解码(与 evolclaw encodeSegment / decodeDirSegment 一致)──
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
- /** 与 evolclaw cross-platform.ts encodePath 一致(CC transcript 目录名) */
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 <EVOLCLAW_HOME>]
6
+ * ec-web [--port 42705] [--home <EVOLCORE_HOME>]
7
7
  *
8
- * 通过 EVOLCLAW_HOME 定位 evolclaw 数据目录,启动 HTTP+WS 服务,浏览器配对码登录。
9
- * 与 evolclaw daemon 通过 IPC socket(live 状态)+ 文件系统(历史数据)旁路通信。
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 <EVOLCLAW_HOME>]\n\n选项:\n --port, -p 监听端口(默认 42705,占用则自动 +1)\n --home 当前运行时数据目录(现阶段默认读 EVOLCLAW_HOME 或 ~/.evolclaw)\n`);
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.EVOLCLAW_HOME = path.resolve(home);
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(`❌ EVOLCLAW_HOME 不存在: ${p.root}\n 用 --home 指定正确路径,或设置 EVOLCLAW_HOME 环境变量。\n`);
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;
@@ -1,8 +1,8 @@
1
1
  /**
2
- * evolclaw IPC client(独立版)— 通过 Unix socket / 命名管道查询运行中的 daemon。
2
+ * evolcore IPC client(独立版)— 通过 Unix socket / 命名管道查询运行中的 daemon。
3
3
  *
4
4
  * 协议:newline-delimited JSON,一问一答即断。
5
- * 与 evolclaw src/ipc.ts 的 ipcQuery 一致;连不上返回 null(daemon 未运行)。
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
- * evolclaw 路径解析(独立版)。
2
+ * evolcore 路径解析(独立版)。
3
3
  *
4
- * watch 进程只需要 EVOLCLAW_HOME 根目录,所有地址从它派生。
5
- * 逻辑与 evolclaw src/paths.ts 对齐,但不依赖 evolclaw 代码。
4
+ * watch 进程只需要 EVOLCORE_HOME 根目录,所有地址从它派生。
5
+ * 逻辑与 evolcore src/paths.ts 对齐,但不依赖 evolcore 代码。
6
6
  *
7
7
  * 根目录解析优先级:
8
- * 1. --home 命令行参数(由 index.ts 写入 process.env.EVOLCLAW_HOME)
9
- * 2. EVOLCLAW_HOME 环境变量
8
+ * 1. --home 命令行参数(由 index.ts 写入 process.env.EVOLCORE_HOME)
9
+ * 2. EVOLCORE_HOME 环境变量
10
10
  * 3. cwd/agents/defaults.json 存在 → cwd(开发模式)
11
- * 4. ~/.evolclaw
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.EVOLCLAW_HOME) {
23
- _root = process.env.EVOLCLAW_HOME;
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(), '.evolclaw');
29
+ _root = path.join(os.homedir(), '.evolcore');
30
30
  }
31
31
  return _root;
32
32
  }
33
- /** socket 路径派生:与 evolclaw resolveInstanceSocketPath 完全一致 */
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\\evolclaw-${hash}`;
37
+ return `\\\\.\\pipe\\daemon-${hash}`;
38
38
  }
39
- return path.join(root, 'data', 'instance', 'evolclaw.sock');
39
+ return path.join(root, 'data', 'instance', 'daemon.sock');
40
40
  }
41
41
  export function resolvePaths() {
42
42
  const root = resolveRoot();
@@ -1,7 +1,7 @@
1
1
  /**
2
- * 进程工具(独立版)— 内联自 evolclaw cross-platform + process-introspect + instance-registry。
2
+ * 进程工具(独立版)— 内联自 evolcore cross-platform + process-introspect + instance-registry。
3
3
  *
4
- * 只包含 ecweb 单实例保护实际用到的函数,无任何 evolclaw 代码依赖。
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
- * 与 evolclaw 的唯一通信:启动时发 ping 检查 protocolVersion(soft 校验)。
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; // 与 evolclaw ping response 对齐的软校验版本
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
- send({ type: 'snapshot', view, data: decorate(await source.snapshot(sourceParams)) });
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
- unsubscribe = source.subscribe(sourceParams, (data) => {
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', () => { clearInterval(heartbeat); if (unsubscribe) {
567
- unsubscribe();
568
- unsubscribe = null;
569
- } log(`◇ WS 断开 from ${ip}`); });
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(`⚠️ evolclaw protocolVersion=${pingResp.protocolVersion},watch 期望 >=${PROTOCOL_VERSION},部分功能可能异常`);
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;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * AID 数据源 — 复用 daemon 的 IPC socket(与 evolclaw `watch aid` 同源)。
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
- // evolclaw 版本:与 watch aid 状态栏一致。daemon 进程级、整轮 watch 不变,
67
+ // evolcore 版本:与 watch aid 状态栏一致。daemon 进程级、整轮 watch 不变,
68
68
  // 故缓存一次(首次 menu.query name=system 拿到后复用),避免每秒多发一次 IPC。
69
69
  let _cachedVersion = null;
70
70
  async function fetchVersion(socket) {
@@ -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: 'evolclaw 未运行或 socket 不可达' };
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 返回失败' };
@@ -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: 'evolclaw', label: 'Daemon', pattern: /^evolclaw(?:-.+)?\.log$/ },
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 === 'evolclaw') ? 'evolclaw' : (sources[0]?.id || null));
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) : [];
@@ -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.pid === process.pid);
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 ?? 0,
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: Number(process.cpuPercent) || 0,
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);
@@ -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
- const lastMsgAt = item.messages.reduce((max, message) => Math.max(max, message.ts || 0), 0);
320
- const updatedAt = Math.max(Number(partitionActive.updatedAt || 0), lastMsgAt);
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
- evolclawSessionId: active.id || null,
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 isEvolclawPackageRoot(dir) {
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 === 'evolclaw';
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 (isEvolclawPackageRoot(dir))
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.EVOLCLAW_PACKAGE_ROOT,
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 resolveEvolclawPackageRoot() {
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('evolclaw/package.json');
53
+ const pkgJson = require.resolve('evolcore/package.json');
54
54
  const root = path.dirname(pkgJson);
55
- if (isEvolclawPackageRoot(root)) {
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 = resolveEvolclawPackageRoot();
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, push) {
665
- let lastJson = '';
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
- * - 读取:从 evolclaw/dist/config/roles.ts 导入 readRolesConfig
11
- * - 写入:保存到 .evolclaw/roles.json
12
- * - 推送:2秒轮询 + JSON diff
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
- // 动态导入 evolclaw 主项目的角色配置模块
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 userRoles = roleOptions.data.filter(item => item.kind === 'user');
129
- const details = await Promise.all(userRoles.map(item => execRoleMenu(aid, {
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', 'evolclawHome', 'none'].includes(String(value))) {
428
- errors.push(`${label} must be agentProject, evolclawHome, or none`);
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, push) {
638
- let lastJson = '';
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) {