ec-web 0.0.3 → 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/server.js +26 -7
- package/dist/sources/monitor.js +90 -7
- package/dist/sources/msg.js +6 -2
- package/dist/sources/relation-roles.js +2 -24
- package/dist/sources/role-definitions.js +11 -29
- package/dist/static/app.js +136 -109
- package/dist/static/index.html +5 -5
- package/dist/static/style.css +26 -0
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -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 ──
|
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,
|
|
@@ -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,12 +4,12 @@
|
|
|
4
4
|
* 功能:
|
|
5
5
|
* 1. 读取 roles.json 或内置角色配置
|
|
6
6
|
* 2. 提供角色定义的 CRUD API
|
|
7
|
-
* 3. WebSocket
|
|
7
|
+
* 3. WebSocket 提供按需快照
|
|
8
8
|
*
|
|
9
9
|
* 数据流:
|
|
10
10
|
* - 读取:从 evolcore/dist/config/roles.ts 导入 readRolesConfig
|
|
11
11
|
* - 写入:保存到 .evolcore/roles.json
|
|
12
|
-
* -
|
|
12
|
+
* - 刷新:进入页面、切换 Agent 或完成写操作时按需加载
|
|
13
13
|
*/
|
|
14
14
|
import { getModelCatalogSnapshot } from './models.js';
|
|
15
15
|
import { resolveParentDistModule, toFileUrl } from './parent-package.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)));
|
|
@@ -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) {
|
package/dist/static/app.js
CHANGED
|
@@ -374,13 +374,11 @@ const translations = {
|
|
|
374
374
|
'monitor.card.systemMemory': '系统内存',
|
|
375
375
|
'monitor.chart.cpu': 'CPU 占用',
|
|
376
376
|
'monitor.chart.memory': '内存占用',
|
|
377
|
-
'monitor.chart.
|
|
377
|
+
'monitor.chart.disk': '磁盘读写',
|
|
378
378
|
'monitor.chart.network': '网络占用',
|
|
379
|
-
'monitor.
|
|
380
|
-
'monitor.
|
|
381
|
-
'monitor.
|
|
382
|
-
'monitor.activity.interrupts': '中断',
|
|
383
|
-
'monitor.activity.toolErrors': '工具错误',
|
|
379
|
+
'monitor.series.diskRead': '读取',
|
|
380
|
+
'monitor.series.diskWrite': '写入',
|
|
381
|
+
'monitor.series.processTotal': '关联进程',
|
|
384
382
|
'monitor.series.processRss': 'Daemon 进程组 RSS',
|
|
385
383
|
'monitor.processes.daemon': '主进程',
|
|
386
384
|
'monitor.processes.ecweb': 'ECWeb',
|
|
@@ -394,7 +392,7 @@ const translations = {
|
|
|
394
392
|
'monitor.processes.sandbox': '沙箱',
|
|
395
393
|
'monitor.processes.runner-helper': 'Runner 辅助',
|
|
396
394
|
'monitor.processes.child': '关联进程',
|
|
397
|
-
'monitor.series.systemUsed': '
|
|
395
|
+
'monitor.series.systemUsed': '整机系统(已用)',
|
|
398
396
|
'monitor.series.networkDownload': '下载',
|
|
399
397
|
'monitor.series.networkUpload': '上传',
|
|
400
398
|
'monitor.th.received': '收',
|
|
@@ -705,7 +703,7 @@ const translations = {
|
|
|
705
703
|
// Usage view
|
|
706
704
|
'usage.subtab.overview': '总览',
|
|
707
705
|
'usage.subtab.explorer': '详细统计',
|
|
708
|
-
'usage.overview.range.
|
|
706
|
+
'usage.overview.range.last24': '最近24小时',
|
|
709
707
|
'usage.overview.range.week': '本周',
|
|
710
708
|
'usage.overview.range.lastWeek': '上周',
|
|
711
709
|
'usage.overview.range.month': '本月',
|
|
@@ -1232,13 +1230,11 @@ const translations = {
|
|
|
1232
1230
|
'monitor.card.systemMemory': 'System Memory',
|
|
1233
1231
|
'monitor.chart.cpu': 'CPU Usage',
|
|
1234
1232
|
'monitor.chart.memory': 'Memory Usage',
|
|
1235
|
-
'monitor.chart.
|
|
1233
|
+
'monitor.chart.disk': 'Disk I/O',
|
|
1236
1234
|
'monitor.chart.network': 'Network Usage',
|
|
1237
|
-
'monitor.
|
|
1238
|
-
'monitor.
|
|
1239
|
-
'monitor.
|
|
1240
|
-
'monitor.activity.interrupts': 'Interrupts',
|
|
1241
|
-
'monitor.activity.toolErrors': 'ToolErr',
|
|
1235
|
+
'monitor.series.diskRead': 'Read',
|
|
1236
|
+
'monitor.series.diskWrite': 'Write',
|
|
1237
|
+
'monitor.series.processTotal': 'Related Processes',
|
|
1242
1238
|
'monitor.series.processRss': 'Daemon process group RSS',
|
|
1243
1239
|
'monitor.processes.daemon': 'Daemon',
|
|
1244
1240
|
'monitor.processes.ecweb': 'ECWeb',
|
|
@@ -1563,7 +1559,7 @@ const translations = {
|
|
|
1563
1559
|
// Usage view
|
|
1564
1560
|
'usage.subtab.overview': 'Overview',
|
|
1565
1561
|
'usage.subtab.explorer': 'Detailed Statistics',
|
|
1566
|
-
'usage.overview.range.
|
|
1562
|
+
'usage.overview.range.last24': 'Last 24 Hours',
|
|
1567
1563
|
'usage.overview.range.week': 'This Week',
|
|
1568
1564
|
'usage.overview.range.lastWeek': 'Last Week',
|
|
1569
1565
|
'usage.overview.range.month': 'This Month',
|
|
@@ -7002,7 +6998,7 @@ function disposeWindowChart(key) {
|
|
|
7002
6998
|
}
|
|
7003
6999
|
|
|
7004
7000
|
function refreshThemeSensitiveViews() {
|
|
7005
|
-
['_hourlyChart', '_modelChart', '_monCpu', '_monMem', '
|
|
7001
|
+
['_hourlyChart', '_modelChart', '_monCpu', '_monMem', '_monDisk', '_monNet'].forEach(disposeWindowChart);
|
|
7006
7002
|
if (typeof _explorerChart !== 'undefined' && _explorerChart) {
|
|
7007
7003
|
_explorerChart.dispose();
|
|
7008
7004
|
_explorerChart = null;
|
|
@@ -7060,7 +7056,12 @@ function fmtTokens(n) {
|
|
|
7060
7056
|
}
|
|
7061
7057
|
|
|
7062
7058
|
// ── Usage Overview(总览,支持日期范围筛选)──
|
|
7063
|
-
let _ovCurrentRange = '
|
|
7059
|
+
let _ovCurrentRange = 'last24'; // 当前选择的范围
|
|
7060
|
+
|
|
7061
|
+
function rollingLast24HourRange(now) {
|
|
7062
|
+
const toTs = (now || new Date()).getTime();
|
|
7063
|
+
return { fromTs: toTs - 24 * 60 * 60 * 1000, toTs };
|
|
7064
|
+
}
|
|
7064
7065
|
|
|
7065
7066
|
async function loadUsageOverview(rangeType, customFrom, customTo) {
|
|
7066
7067
|
rangeType = rangeType || _ovCurrentRange;
|
|
@@ -7073,9 +7074,8 @@ async function loadUsageOverview(rangeType, customFrom, customTo) {
|
|
|
7073
7074
|
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
|
|
7074
7075
|
|
|
7075
7076
|
switch (rangeType) {
|
|
7076
|
-
case '
|
|
7077
|
-
fromTs =
|
|
7078
|
-
toTs = todayEnd;
|
|
7077
|
+
case 'last24':
|
|
7078
|
+
({ fromTs, toTs } = rollingLast24HourRange(now));
|
|
7079
7079
|
break;
|
|
7080
7080
|
case 'week': // 本周(周一到今天)
|
|
7081
7081
|
const dayOfWeek = now.getDay() || 7; // 周日=7
|
|
@@ -7104,8 +7104,7 @@ async function loadUsageOverview(rangeType, customFrom, customTo) {
|
|
|
7104
7104
|
fromTs = new Date(customFrom).getTime();
|
|
7105
7105
|
toTs = new Date(customTo).getTime();
|
|
7106
7106
|
} else {
|
|
7107
|
-
fromTs =
|
|
7108
|
-
toTs = todayEnd;
|
|
7107
|
+
({ fromTs, toTs } = rollingLast24HourRange(now));
|
|
7109
7108
|
}
|
|
7110
7109
|
break;
|
|
7111
7110
|
default:
|
|
@@ -7559,7 +7558,7 @@ var _usageSubtabsBound = false;
|
|
|
7559
7558
|
|
|
7560
7559
|
function activeOverviewRange() {
|
|
7561
7560
|
const active = document.querySelector('.ov-range-btn.active');
|
|
7562
|
-
return active ? active.getAttribute('data-range') || '
|
|
7561
|
+
return active ? active.getAttribute('data-range') || 'last24' : 'last24';
|
|
7563
7562
|
}
|
|
7564
7563
|
|
|
7565
7564
|
function refreshUsageView() {
|
|
@@ -7609,16 +7608,16 @@ function initUsageSubtabs() {
|
|
|
7609
7608
|
});
|
|
7610
7609
|
});
|
|
7611
7610
|
|
|
7612
|
-
//
|
|
7611
|
+
// 初始化总览页面的过滤器并加载默认数据(最近 24 小时)
|
|
7613
7612
|
initOverviewFilters();
|
|
7614
|
-
loadUsageOverview('
|
|
7613
|
+
loadUsageOverview('last24');
|
|
7615
7614
|
}
|
|
7616
7615
|
|
|
7617
7616
|
// ── Explorer ──
|
|
7618
7617
|
var _explorerChart = null;
|
|
7619
7618
|
var _explorerInited = false;
|
|
7620
7619
|
var _expSelection = { type: null, key: null }; // { type: 'agent'|'peer', key: string } or null
|
|
7621
|
-
var _expCurrentRange = '
|
|
7620
|
+
var _expCurrentRange = 'last24'; // Explorer 当前选择的时间范围
|
|
7622
7621
|
var _expTimeRange = { fromTs: null, toTs: null }; // Explorer 的时间范围
|
|
7623
7622
|
|
|
7624
7623
|
function initExplorer() {
|
|
@@ -7653,6 +7652,7 @@ function initExplorerTimeFilters() {
|
|
|
7653
7652
|
if (customDateEl) customDateEl.style.display = 'none';
|
|
7654
7653
|
_expCurrentRange = range;
|
|
7655
7654
|
calculateExplorerTimeRange(range);
|
|
7655
|
+
setExplorerGranularityForRange(range);
|
|
7656
7656
|
loadExplorerModels(); // 加载可用模型
|
|
7657
7657
|
runExplorerQuery();
|
|
7658
7658
|
}
|
|
@@ -7675,15 +7675,22 @@ function initExplorerTimeFilters() {
|
|
|
7675
7675
|
});
|
|
7676
7676
|
}
|
|
7677
7677
|
|
|
7678
|
-
//
|
|
7678
|
+
// 设置默认时间范围(最近 24 小时)并初始化日期选择器
|
|
7679
7679
|
const now = new Date();
|
|
7680
|
+
const last24 = rollingLast24HourRange(now);
|
|
7680
7681
|
const fromEl = $('#exp-from');
|
|
7681
7682
|
const toEl = $('#exp-to');
|
|
7682
|
-
if (fromEl) fromEl.value = formatDatetimeLocal(new Date(
|
|
7683
|
+
if (fromEl) fromEl.value = formatDatetimeLocal(new Date(last24.fromTs));
|
|
7683
7684
|
if (toEl) toEl.value = formatDatetimeLocal(now);
|
|
7684
7685
|
|
|
7685
|
-
//
|
|
7686
|
-
calculateExplorerTimeRange('
|
|
7686
|
+
// 计算默认时间范围(最近 24 小时)
|
|
7687
|
+
calculateExplorerTimeRange('last24');
|
|
7688
|
+
setExplorerGranularityForRange('last24');
|
|
7689
|
+
}
|
|
7690
|
+
|
|
7691
|
+
function setExplorerGranularityForRange(rangeType) {
|
|
7692
|
+
const granularityEl = $('#exp-granularity');
|
|
7693
|
+
if (rangeType === 'last24' && granularityEl) granularityEl.value = 'hour';
|
|
7687
7694
|
}
|
|
7688
7695
|
|
|
7689
7696
|
// 计算 Explorer 的时间范围
|
|
@@ -7693,9 +7700,8 @@ function calculateExplorerTimeRange(rangeType) {
|
|
|
7693
7700
|
const todayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999).getTime();
|
|
7694
7701
|
|
|
7695
7702
|
switch (rangeType) {
|
|
7696
|
-
case '
|
|
7697
|
-
_expTimeRange
|
|
7698
|
-
_expTimeRange.toTs = todayEnd;
|
|
7703
|
+
case 'last24':
|
|
7704
|
+
_expTimeRange = rollingLast24HourRange(now);
|
|
7699
7705
|
break;
|
|
7700
7706
|
case 'week':
|
|
7701
7707
|
const dayOfWeek = now.getDay() || 7;
|
|
@@ -8108,12 +8114,12 @@ function monitorLayoutHtml() {
|
|
|
8108
8114
|
'</div>' +
|
|
8109
8115
|
`<div id="mon-cards" class="usage-cards"><div class="empty" style="grid-column:1/-1">${t('common.loading')}</div></div>` +
|
|
8110
8116
|
'<div class="mon-row2"><div id="mon-cpu-chart" class="mon-chart"></div><div id="mon-mem-chart" class="mon-chart"></div></div>' +
|
|
8111
|
-
'<div class="mon-row2"><div id="mon-
|
|
8117
|
+
'<div class="mon-row2"><div id="mon-disk-chart" class="mon-chart"></div><div id="mon-net-chart" class="mon-chart"></div></div>' +
|
|
8112
8118
|
'</div>';
|
|
8113
8119
|
}
|
|
8114
8120
|
|
|
8115
8121
|
function disposeMonitorCharts() {
|
|
8116
|
-
['_monCpu', '_monMem', '
|
|
8122
|
+
['_monCpu', '_monMem', '_monDisk', '_monNet'].forEach(disposeWindowChart);
|
|
8117
8123
|
}
|
|
8118
8124
|
|
|
8119
8125
|
function bindMonRangeTabs(wrap) {
|
|
@@ -8136,36 +8142,65 @@ function formatMonitorResponse(ms) {
|
|
|
8136
8142
|
return value >= 10000 ? (value / 1000).toFixed(1) + 's' : Math.round(value) + 'ms';
|
|
8137
8143
|
}
|
|
8138
8144
|
|
|
8139
|
-
function
|
|
8145
|
+
function monitorProcessKind(process) {
|
|
8140
8146
|
var knownKinds = ['daemon', 'ecweb', 'watch-web', 'restart-monitor', 'watch', 'watch-aid', 'codex-app-server', 'claude-runner', 'gemini-runner', 'sandbox', 'runner-helper', 'child'];
|
|
8141
|
-
|
|
8142
|
-
|
|
8147
|
+
return knownKinds.includes(process.kind) ? process.kind : 'child';
|
|
8148
|
+
}
|
|
8149
|
+
|
|
8150
|
+
function monitorProcessKey(process) {
|
|
8151
|
+
return `${monitorProcessKind(process)}\u0000${process.name || 'process'}`;
|
|
8152
|
+
}
|
|
8153
|
+
|
|
8154
|
+
function monitorProcessLabel(process) {
|
|
8155
|
+
return `${t('monitor.processes.' + monitorProcessKind(process))} · ${process.name || 'process'}`;
|
|
8143
8156
|
}
|
|
8144
8157
|
|
|
8145
|
-
function monitorProcessColor(
|
|
8158
|
+
function monitorProcessColor(key) {
|
|
8146
8159
|
var colors = ['#4f6ef7', '#38a169', '#dd6b20', '#805ad5', '#d53f8c', '#319795', '#e53e3e', '#3182ce', '#718096', '#b7791f'];
|
|
8147
|
-
var value =
|
|
8160
|
+
var value = 0;
|
|
8161
|
+
for (var i = 0; i < key.length; i += 1) value = ((value * 31) + key.charCodeAt(i)) | 0;
|
|
8162
|
+
value = Math.abs(value);
|
|
8148
8163
|
return colors[value % colors.length];
|
|
8149
8164
|
}
|
|
8150
8165
|
|
|
8151
8166
|
function monitorProcessSeries(history, currentProcesses, metric) {
|
|
8152
8167
|
var catalog = new Map();
|
|
8153
|
-
|
|
8168
|
+
function addProcess(process) {
|
|
8169
|
+
var key = monitorProcessKey(process);
|
|
8170
|
+
if (!catalog.has(key)) catalog.set(key, { key: key, kind: monitorProcessKind(process), name: process.name || 'process' });
|
|
8171
|
+
}
|
|
8172
|
+
for (const process of (currentProcesses || [])) addProcess(process);
|
|
8154
8173
|
for (const point of history) {
|
|
8155
|
-
for (const process of (point.processes || []))
|
|
8174
|
+
for (const process of (point.processes || [])) addProcess(process);
|
|
8156
8175
|
}
|
|
8157
|
-
return [...catalog.values()].sort(function (a, b) { return a.
|
|
8176
|
+
return [...catalog.values()].sort(function (a, b) { return monitorProcessLabel(a).localeCompare(monitorProcessLabel(b)); }).map(function (process) {
|
|
8158
8177
|
return {
|
|
8159
8178
|
name: monitorProcessLabel(process),
|
|
8160
8179
|
data: history.map(function (point) {
|
|
8161
|
-
var
|
|
8162
|
-
|
|
8180
|
+
var value = 0;
|
|
8181
|
+
var hasSample = false;
|
|
8182
|
+
for (const sample of (point.processes || [])) {
|
|
8183
|
+
if (monitorProcessKey(sample) !== process.key) continue;
|
|
8184
|
+
value += Number(sample[metric]) || 0;
|
|
8185
|
+
hasSample = true;
|
|
8186
|
+
}
|
|
8187
|
+
return hasSample ? value : null;
|
|
8163
8188
|
}),
|
|
8164
|
-
color: monitorProcessColor(process.
|
|
8189
|
+
color: monitorProcessColor(process.key),
|
|
8165
8190
|
};
|
|
8166
8191
|
});
|
|
8167
8192
|
}
|
|
8168
8193
|
|
|
8194
|
+
function monitorAggregateSeries(name, history, metric, color) {
|
|
8195
|
+
return {
|
|
8196
|
+
name: name,
|
|
8197
|
+
data: history.map(function (point) { return point[metric] != null ? point[metric] : null; }),
|
|
8198
|
+
color: color,
|
|
8199
|
+
width: 3,
|
|
8200
|
+
areaOpacity: 0.12,
|
|
8201
|
+
};
|
|
8202
|
+
}
|
|
8203
|
+
|
|
8169
8204
|
function renderMonitor(data) {
|
|
8170
8205
|
var wrap = $('#sys-content .mon-layout');
|
|
8171
8206
|
if (!wrap) return;
|
|
@@ -8187,6 +8222,8 @@ function renderMonitor(data) {
|
|
|
8187
8222
|
var lh = (s.stats && s.stats.lastHour) || {};
|
|
8188
8223
|
var errRate = (lh.received > 0) ? ((lh.errors / lh.received) * 100).toFixed(1) + '%' : '0%';
|
|
8189
8224
|
var processTree = s.processTree || {};
|
|
8225
|
+
var cpuCount = Math.max(1, Number(s.cpuCount) || 1);
|
|
8226
|
+
var processCpuPercent = (Number(s.cpuPercent) || 0) / cpuCount;
|
|
8190
8227
|
|
|
8191
8228
|
// ── Stat cards ──
|
|
8192
8229
|
var sysMemPct = (sys.memTotal > 0) ? Math.round((sys.memUsed / sys.memTotal) * 100) : 0;
|
|
@@ -8195,7 +8232,7 @@ function renderMonitor(data) {
|
|
|
8195
8232
|
[t('monitor.card.messages1h'), lh.received || 0],
|
|
8196
8233
|
[t('monitor.card.avgResponse'), formatMonitorResponse(lh.avgResponseMs)],
|
|
8197
8234
|
[t('monitor.card.errorRate'), errRate],
|
|
8198
|
-
[t('monitor.card.processCpu'), (
|
|
8235
|
+
[t('monitor.card.processCpu'), processCpuPercent.toFixed(1) + '%'],
|
|
8199
8236
|
[t('monitor.card.systemCpu'), (sys.cpuPercent != null ? sys.cpuPercent : 0) + '%'],
|
|
8200
8237
|
[t('monitor.card.processMemory'), fmtBytes(s.memory ? s.memory.rss : 0)],
|
|
8201
8238
|
[t('monitor.card.processCount'), processTree.processCount || 1],
|
|
@@ -8211,52 +8248,32 @@ function renderMonitor(data) {
|
|
|
8211
8248
|
var cProc = css('--accent'), cSys = css('--orange');
|
|
8212
8249
|
|
|
8213
8250
|
var currentProcesses = Array.isArray(processTree.processes) ? processTree.processes : [];
|
|
8214
|
-
var cpuSeries =
|
|
8215
|
-
|
|
8251
|
+
var cpuSeries = [
|
|
8252
|
+
monitorAggregateSeries(t('monitor.legend.system'), h, 'sysCpu', cSys),
|
|
8253
|
+
monitorAggregateSeries(t('monitor.series.processTotal'), h, 'procCpu', cProc),
|
|
8254
|
+
].concat(monitorProcessSeries(h, currentProcesses, 'cpuPercent'));
|
|
8216
8255
|
|
|
8217
|
-
// ── CPU
|
|
8256
|
+
// ── CPU:整机系统、关联进程、每类关联进程 ──
|
|
8218
8257
|
monDualLine('mon-cpu-chart', '_monCpu', ts, isDark, t('monitor.chart.cpu'),
|
|
8219
8258
|
cpuSeries,
|
|
8220
8259
|
function (v) { return Number(v).toFixed(1) + '%'; }, null);
|
|
8221
8260
|
|
|
8222
|
-
// ── Memory
|
|
8261
|
+
// ── Memory:整机系统、关联进程、每类关联进程 ──
|
|
8262
|
+
var memorySeries = [
|
|
8263
|
+
monitorAggregateSeries(t('monitor.series.systemUsed'), h, 'sysMemUsed', cSys),
|
|
8264
|
+
monitorAggregateSeries(t('monitor.series.processTotal'), h, 'procRss', cProc),
|
|
8265
|
+
].concat(monitorProcessSeries(h, currentProcesses, 'rss'));
|
|
8223
8266
|
monDualLine('mon-mem-chart', '_monMem', ts, isDark, t('monitor.chart.memory'),
|
|
8224
|
-
|
|
8267
|
+
memorySeries,
|
|
8225
8268
|
function (v) { return fmtBytes(v); }, null);
|
|
8226
8269
|
|
|
8227
|
-
// ──
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
|
|
8234
|
-
grid: { top: 36, bottom: 24, left: 44, right: 12 },
|
|
8235
|
-
xAxis: {
|
|
8236
|
-
type: 'category',
|
|
8237
|
-
data: [
|
|
8238
|
-
t('monitor.activity.received'),
|
|
8239
|
-
t('monitor.activity.completed'),
|
|
8240
|
-
t('monitor.activity.errors'),
|
|
8241
|
-
t('monitor.activity.interrupts'),
|
|
8242
|
-
t('monitor.activity.toolErrors'),
|
|
8243
|
-
],
|
|
8244
|
-
axisLabel: { fontSize: 9 },
|
|
8245
|
-
},
|
|
8246
|
-
yAxis: { type: 'value', minInterval: 1 },
|
|
8247
|
-
series: [{
|
|
8248
|
-
type: 'bar', barWidth: '45%',
|
|
8249
|
-
data: [
|
|
8250
|
-
{ value: lh.received || 0, itemStyle: { color: css('--accent') } },
|
|
8251
|
-
{ value: lh.completed || 0, itemStyle: { color: css('--green') } },
|
|
8252
|
-
{ value: lh.errors || 0, itemStyle: { color: css('--red') } },
|
|
8253
|
-
{ value: lh.interrupts || 0, itemStyle: { color: css('--orange') } },
|
|
8254
|
-
{ value: lh.toolErrors || 0, itemStyle: { color: css('--blue') } },
|
|
8255
|
-
],
|
|
8256
|
-
}],
|
|
8257
|
-
animation: false,
|
|
8258
|
-
});
|
|
8259
|
-
}
|
|
8270
|
+
// ── Disk throughput ──
|
|
8271
|
+
monDualLine('mon-disk-chart', '_monDisk', ts, isDark, t('monitor.chart.disk'),
|
|
8272
|
+
[
|
|
8273
|
+
{ name: t('monitor.series.diskRead'), data: h.map(function (p) { return p.diskReadBps || 0; }), color: css('--blue') || cProc },
|
|
8274
|
+
{ name: t('monitor.series.diskWrite'), data: h.map(function (p) { return p.diskWriteBps || 0; }), color: cSys },
|
|
8275
|
+
],
|
|
8276
|
+
function (v) { return fmtBytes(v) + '/s'; }, null);
|
|
8260
8277
|
|
|
8261
8278
|
// ── Network throughput ──
|
|
8262
8279
|
monDualLine('mon-net-chart', '_monNet', ts, isDark, t('monitor.chart.network'),
|
|
@@ -8268,7 +8285,7 @@ function renderMonitor(data) {
|
|
|
8268
8285
|
|
|
8269
8286
|
}
|
|
8270
8287
|
|
|
8271
|
-
//
|
|
8288
|
+
// 时序图。series: [{name,data,color,width?,areaOpacity?}]
|
|
8272
8289
|
function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
|
|
8273
8290
|
var el = $('#' + elId);
|
|
8274
8291
|
if (!el) return;
|
|
@@ -8307,8 +8324,8 @@ function monDualLine(elId, varKey, times, isDark, title, series, fmtY, yRange) {
|
|
|
8307
8324
|
return {
|
|
8308
8325
|
name: sr.name, type: 'line', data: sr.data, smooth: true, symbol: 'none',
|
|
8309
8326
|
connectNulls: true,
|
|
8310
|
-
lineStyle: { width: 2, color: sr.color },
|
|
8311
|
-
areaStyle: { color: sr.color, opacity: 0.08 },
|
|
8327
|
+
lineStyle: { width: sr.width || 2, color: sr.color },
|
|
8328
|
+
areaStyle: { color: sr.color, opacity: sr.areaOpacity != null ? sr.areaOpacity : 0.08 },
|
|
8312
8329
|
itemStyle: { color: sr.color },
|
|
8313
8330
|
};
|
|
8314
8331
|
}),
|
|
@@ -8769,8 +8786,7 @@ function renderRoleDefinitions(data) {
|
|
|
8769
8786
|
}
|
|
8770
8787
|
|
|
8771
8788
|
if (!data) {
|
|
8772
|
-
|
|
8773
|
-
grid.innerHTML = '<div class="role-error">❌ 未收到角色定义数据</div>';
|
|
8789
|
+
grid.innerHTML = `<div class="role-error">${esc(t('common.loading'))}</div>`;
|
|
8774
8790
|
return;
|
|
8775
8791
|
}
|
|
8776
8792
|
|
|
@@ -8877,6 +8893,8 @@ function renderDefaultRoleSelector(data) {
|
|
|
8877
8893
|
alert('保存默认角色失败' + (detail ? ': ' + detail : ''));
|
|
8878
8894
|
privateSelect.value = defaults.private;
|
|
8879
8895
|
groupSelect.value = defaults.group;
|
|
8896
|
+
} else {
|
|
8897
|
+
subscribe('roleDefinitions', roleDefinitionsParams());
|
|
8880
8898
|
}
|
|
8881
8899
|
} catch (err) {
|
|
8882
8900
|
console.error('[roleDefinitions] Failed to save defaultRoles:', err);
|
|
@@ -9154,6 +9172,7 @@ function renderCommandPermissionsList(roleDef, operations, readonly = false) {
|
|
|
9154
9172
|
|
|
9155
9173
|
function renderCommandPermissionsEditor(roleDef, operations) {
|
|
9156
9174
|
const stats = getCommandPermissionStats(roleDef?.commandPermissions);
|
|
9175
|
+
const rawPermissions = formatCommandPermissions(roleDef?.commandPermissions);
|
|
9157
9176
|
return `
|
|
9158
9177
|
<div class="form-section command-permissions-section">
|
|
9159
9178
|
<h4>Command / CLI Permissions</h4>
|
|
@@ -9168,6 +9187,18 @@ function renderCommandPermissionsEditor(roleDef, operations) {
|
|
|
9168
9187
|
? '清单来自后端 operation registry。每行显示当前有效权限和命中的规则;修改后会写入精确 operation 规则。若要撤销内置继承规则,请对同名 operation 设置“拒绝”。'
|
|
9169
9188
|
: 'The list comes from the backend operation registry. Each row shows the current effective permission and matched rule. Changes are saved as exact operation rules.'}
|
|
9170
9189
|
</div>
|
|
9190
|
+
<details class="cli-raw-editor">
|
|
9191
|
+
<summary>${currentLang === 'zh-CN' ? '编辑原始规则 JSON' : 'Edit raw rules JSON'}</summary>
|
|
9192
|
+
<div class="cli-help">
|
|
9193
|
+
${currentLang === 'zh-CN'
|
|
9194
|
+
? '用于编辑通配符或未出现在 operation 清单中的规则。下方结构化修改会合并到这份 JSON。'
|
|
9195
|
+
: 'Use this for wildcard rules or operations missing from the catalog. Structured changes below are merged into this JSON.'}
|
|
9196
|
+
</div>
|
|
9197
|
+
<textarea id="edit-commandPermissions"
|
|
9198
|
+
class="form-textarea cli-raw-textarea"
|
|
9199
|
+
rows="12"
|
|
9200
|
+
spellcheck="false">${esc(rawPermissions)}</textarea>
|
|
9201
|
+
</details>
|
|
9171
9202
|
${renderCommandPermissionsList(roleDef, operations, false)}
|
|
9172
9203
|
</div>
|
|
9173
9204
|
`;
|
|
@@ -9209,21 +9240,24 @@ function validateCommandPermissionsClient(commandPermissions) {
|
|
|
9209
9240
|
|
|
9210
9241
|
function collectCommandPermissions(container, fallback = {}) {
|
|
9211
9242
|
const rows = Array.from(container.querySelectorAll('[data-command-operation]'));
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9243
|
+
const input = container.querySelector('#edit-commandPermissions');
|
|
9244
|
+
let commandPermissions = getCommandPermissionsObject(fallback);
|
|
9245
|
+
|
|
9246
|
+
if (input) {
|
|
9215
9247
|
try {
|
|
9216
9248
|
const parsed = JSON.parse(input.value.trim() || '{}');
|
|
9217
9249
|
const validationError = validateCommandPermissionsClient(parsed);
|
|
9218
9250
|
if (validationError) return { ok: false, error: validationError };
|
|
9219
|
-
|
|
9251
|
+
commandPermissions = parsed;
|
|
9220
9252
|
} catch (err) {
|
|
9221
9253
|
return { ok: false, error: `Command permissions JSON is invalid: ${err.message}` };
|
|
9222
9254
|
}
|
|
9223
9255
|
}
|
|
9224
9256
|
|
|
9257
|
+
if (rows.length === 0) return { ok: true, value: commandPermissions };
|
|
9258
|
+
|
|
9225
9259
|
const operationsById = new Map((roleOperationsCache || []).map(operation => [operation.id, operation]));
|
|
9226
|
-
|
|
9260
|
+
commandPermissions = { ...commandPermissions };
|
|
9227
9261
|
|
|
9228
9262
|
for (const row of rows) {
|
|
9229
9263
|
const operationId = row.dataset.commandOperation;
|
|
@@ -9435,7 +9469,7 @@ function createRoleCard(roleName, roleDef) {
|
|
|
9435
9469
|
const commandStats = getCommandPermissionStats(roleDef.commandPermissions);
|
|
9436
9470
|
|
|
9437
9471
|
// 判断是否为内置角色
|
|
9438
|
-
const builtinRoles = ['member', 'visitor'];
|
|
9472
|
+
const builtinRoles = ['admin', 'member', 'visitor'];
|
|
9439
9473
|
const isBuiltin = builtinRoles.includes(roleName);
|
|
9440
9474
|
|
|
9441
9475
|
card.innerHTML = `
|
|
@@ -9530,6 +9564,7 @@ function initRoleDefinitionsTab() {
|
|
|
9530
9564
|
alert(t('roleDefs.saveFailed'));
|
|
9531
9565
|
} else {
|
|
9532
9566
|
alert(t('roleDefs.saveSuccess'));
|
|
9567
|
+
subscribe('roleDefinitions', roleDefinitionsParams());
|
|
9533
9568
|
}
|
|
9534
9569
|
} catch (err) {
|
|
9535
9570
|
alert(t('pair.error.network'));
|
|
@@ -9575,6 +9610,7 @@ function initRoleDefinitionsTab() {
|
|
|
9575
9610
|
alert(t('roleDefs.saveFailed') + ': ' + (err.error || 'unknown'));
|
|
9576
9611
|
} else {
|
|
9577
9612
|
alert('角色已删除');
|
|
9613
|
+
subscribe('roleDefinitions', roleDefinitionsParams());
|
|
9578
9614
|
}
|
|
9579
9615
|
} catch (err) {
|
|
9580
9616
|
alert(t('pair.error.network'));
|
|
@@ -10244,14 +10280,8 @@ async function showRoleEditModal(roleName, roleDef) {
|
|
|
10244
10280
|
formHtml += renderRoleUsageLimitsEditor(roleDef);
|
|
10245
10281
|
|
|
10246
10282
|
formHtml += `
|
|
10247
|
-
<div class="
|
|
10248
|
-
<
|
|
10249
|
-
<button type="button" class="role-editor-tab" data-role-tab="cli">CLI Permissions</button>
|
|
10250
|
-
</div>
|
|
10251
|
-
|
|
10252
|
-
<div class="role-editor-tab-panel active" data-role-tab-panel="fields">
|
|
10253
|
-
<div class="form-section">
|
|
10254
|
-
<h4>权限配置</h4>
|
|
10283
|
+
<div class="form-section field-permissions-section">
|
|
10284
|
+
<h4>Field Permissions</h4>
|
|
10255
10285
|
`;
|
|
10256
10286
|
|
|
10257
10287
|
const permissions = roleDef.permissions || {};
|
|
@@ -10421,16 +10451,13 @@ async function showRoleEditModal(roleName, roleDef) {
|
|
|
10421
10451
|
formHtml += `</div>`;
|
|
10422
10452
|
});
|
|
10423
10453
|
|
|
10424
|
-
formHtml += `</div
|
|
10425
|
-
|
|
10426
|
-
${renderCommandPermissionsEditor(roleDef, operations)}
|
|
10427
|
-
</div>
|
|
10454
|
+
formHtml += `</div>
|
|
10455
|
+
${renderCommandPermissionsEditor(roleDef, operations)}
|
|
10428
10456
|
</div>`;
|
|
10429
10457
|
|
|
10430
10458
|
body.innerHTML = formHtml;
|
|
10431
10459
|
|
|
10432
10460
|
bindTagsInputEvents(body);
|
|
10433
|
-
initRoleEditorTabs(body);
|
|
10434
10461
|
initCommandPermissionEditor(body);
|
|
10435
10462
|
initRoleModelPermissionEditor(body, roleName, roleDef);
|
|
10436
10463
|
|
package/dist/static/index.html
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
</nav>
|
|
49
49
|
<span style="flex:1"></span>
|
|
50
|
-
2026-07-
|
|
50
|
+
2026-07-25 03:26
|
|
51
51
|
<span id="conn-status" class="conn-status" data-i18n="status.connecting">连接中…</span>
|
|
52
52
|
<button id="lang-btn" class="theme-btn" data-i18n-title="language.buttonTitle" title="切换语言">🌐</button>
|
|
53
53
|
<button id="theme-btn" class="theme-btn" data-i18n-title="theme.buttonTitle" title="主题">🌗</button>
|
|
@@ -141,7 +141,7 @@
|
|
|
141
141
|
<div id="usage-overview" class="usage-subpanel active">
|
|
142
142
|
<div class="overview-filters">
|
|
143
143
|
<div class="ov-range-tabs">
|
|
144
|
-
<button class="ov-range-btn active" data-range="
|
|
144
|
+
<button class="ov-range-btn active" data-range="last24" data-i18n="usage.overview.range.last24">Last 24 Hours</button>
|
|
145
145
|
<button class="ov-range-btn" data-range="week" data-i18n="usage.overview.range.week">This Week</button>
|
|
146
146
|
<button class="ov-range-btn" data-range="lastWeek" data-i18n="usage.overview.range.lastWeek">Last Week</button>
|
|
147
147
|
<button class="ov-range-btn" data-range="month" data-i18n="usage.overview.range.month">This Month</button>
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
<!-- 时间范围选择(与总览类似,但支持精确到时分秒) -->
|
|
219
219
|
<div class="exp-time-filters">
|
|
220
220
|
<div class="exp-range-tabs">
|
|
221
|
-
<button class="exp-range-btn active" data-range="
|
|
221
|
+
<button class="exp-range-btn active" data-range="last24" data-i18n="usage.overview.range.last24">Last 24 Hours</button>
|
|
222
222
|
<button class="exp-range-btn" data-range="week" data-i18n="usage.overview.range.week">This Week</button>
|
|
223
223
|
<button class="exp-range-btn" data-range="lastWeek" data-i18n="usage.overview.range.lastWeek">Last Week</button>
|
|
224
224
|
<button class="exp-range-btn" data-range="month" data-i18n="usage.overview.range.month">This Month</button>
|
|
@@ -245,8 +245,8 @@
|
|
|
245
245
|
<label>
|
|
246
246
|
<span data-i18n="usage.explorer.filter.granularity">Granularity</span>
|
|
247
247
|
<select id="exp-granularity">
|
|
248
|
-
<option value="hour" data-i18n="usage.explorer.filter.granularity.hour">Hour</option>
|
|
249
|
-
<option value="day"
|
|
248
|
+
<option value="hour" selected data-i18n="usage.explorer.filter.granularity.hour">Hour</option>
|
|
249
|
+
<option value="day" data-i18n="usage.explorer.filter.granularity.day">Day</option>
|
|
250
250
|
<option value="week" data-i18n="usage.explorer.filter.granularity.week">Week</option>
|
|
251
251
|
<option value="month" data-i18n="usage.explorer.filter.granularity.month">Month</option>
|
|
252
252
|
</select>
|
package/dist/static/style.css
CHANGED
|
@@ -3643,6 +3643,32 @@ tr.ag-disabled .ag-name { color: var(--dim); }
|
|
|
3643
3643
|
line-height: 1.5;
|
|
3644
3644
|
}
|
|
3645
3645
|
|
|
3646
|
+
.cli-raw-editor {
|
|
3647
|
+
border: 1px solid var(--border);
|
|
3648
|
+
border-radius: 6px;
|
|
3649
|
+
background: var(--bg2);
|
|
3650
|
+
padding: 10px 12px;
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
.cli-raw-editor summary {
|
|
3654
|
+
color: var(--fg);
|
|
3655
|
+
cursor: pointer;
|
|
3656
|
+
font-size: 13px;
|
|
3657
|
+
font-weight: 600;
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
.cli-raw-editor[open] summary {
|
|
3661
|
+
margin-bottom: 8px;
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
.cli-raw-textarea {
|
|
3665
|
+
margin-top: 10px;
|
|
3666
|
+
min-height: 180px;
|
|
3667
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
3668
|
+
font-size: 12px;
|
|
3669
|
+
line-height: 1.5;
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3646
3672
|
.cli-category-section {
|
|
3647
3673
|
display: flex;
|
|
3648
3674
|
flex-direction: column;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ec-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Web console and monitoring dashboard for EvolCore",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"author": "",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"evolcore": ">=0.0.
|
|
25
|
+
"evolcore": ">=0.0.3"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"ws": "^8.18.0"
|