ec-web 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ import { resolveParentDistModule, toFileUrl } from './parent-package.js';
2
+ async function getCatalogModule() {
3
+ const catalogPath = resolveParentDistModule('core', 'model', 'model-catalog.js');
4
+ const mod = await import(toFileUrl(catalogPath));
5
+ if (typeof mod.getCatalog !== 'function') {
6
+ throw new Error('getCatalog not found in model-catalog.js');
7
+ }
8
+ return { getCatalog: mod.getCatalog };
9
+ }
10
+ export function inferModelFamily(id) {
11
+ if (['opus', 'sonnet', 'haiku'].includes(id))
12
+ return id;
13
+ const claude = id.match(/^claude-([a-z0-9]+)-/i);
14
+ if (claude)
15
+ return claude[1].toLowerCase();
16
+ const vendor = id.match(/^([a-z0-9]+)[-_]/i);
17
+ return vendor ? vendor[1].toLowerCase() : 'other';
18
+ }
19
+ export function normalizeModelEntries(models) {
20
+ const seen = new Set();
21
+ const out = [];
22
+ for (const model of models || []) {
23
+ if (!model || typeof model.id !== 'string')
24
+ continue;
25
+ const id = model.id.trim();
26
+ if (!id || seen.has(id))
27
+ continue;
28
+ seen.add(id);
29
+ const isAlias = model.owned_by === 'alias';
30
+ out.push({
31
+ id,
32
+ owned_by: model.owned_by,
33
+ created: model.created,
34
+ family: inferModelFamily(id),
35
+ status: isAlias ? 'alias' : 'available',
36
+ isAlias
37
+ });
38
+ }
39
+ return out;
40
+ }
41
+ export async function getModelCatalogSnapshot(baseagent = 'claude') {
42
+ const { getCatalog } = await getCatalogModule();
43
+ const catalog = await getCatalog(undefined, baseagent);
44
+ const normalized = normalizeModelEntries(catalog.models || []);
45
+ return {
46
+ models: normalized,
47
+ catalogModels: normalized,
48
+ source: catalog.source || 'unknown',
49
+ lastUpdate: new Date().toISOString()
50
+ };
51
+ }
52
+ function sendJson(res, status, payload) {
53
+ res.writeHead(status, { 'Content-Type': 'application/json' });
54
+ res.end(JSON.stringify(payload));
55
+ }
56
+ export async function handleModelsApi(req, res) {
57
+ try {
58
+ const url = new URL(req.url || '', 'http://localhost');
59
+ if (req.method === 'GET' && url.pathname === '/api/models/catalog') {
60
+ const baseagent = url.searchParams.get('baseagent') || 'claude';
61
+ if (!/^[a-z0-9_-]+$/i.test(baseagent)) {
62
+ sendJson(res, 400, { success: false, error: 'Invalid baseagent' });
63
+ return;
64
+ }
65
+ const data = await getModelCatalogSnapshot(baseagent);
66
+ sendJson(res, 200, { success: true, data });
67
+ return;
68
+ }
69
+ sendJson(res, 404, { success: false, error: 'Not found' });
70
+ }
71
+ catch (err) {
72
+ console.error('[models] API error:', err);
73
+ sendJson(res, 500, { success: false, error: err.message });
74
+ }
75
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Monitor 数据源 — 复用 daemon 的 IPC socket(`monitor-snapshot` 命令)。
3
+ *
4
+ * 拉 daemon 进程树 + 系统级运行指标(CPU/内存/uptime/loadAvg)+ 全局 stats(含最近错误)
5
+ * + per-agent 汇总。IPC 无推送能力,故 2s 轮询 + JSON diff,仅在变化时 push。
6
+ *
7
+ * 时序数据在源侧维护「三档分辨率」滚动缓冲区,随快照一并下发,前端按所选周期切换:
8
+ * - fine : 2s 桶 × 60 ≈ 近 2 分钟
9
+ * - mid : 10s 桶 × 60 ≈ 近 10 分钟
10
+ * - coarse : 60s 桶 × 60 ≈ 近 1 小时
11
+ * 每个桶内对 CPU/内存做均值聚合,避免长周期下点数爆炸。
12
+ */
13
+ import fs from 'node:fs';
14
+ import { resolvePaths } from '../paths.js';
15
+ import { ipcQuery } from '../ipc-client.js';
16
+ // 模块级缓冲区——所有订阅者共享同一份累积时序(单一事实源)。
17
+ const resolutions = {
18
+ fine: { bucketMs: 2_000, cap: 60, buf: [] },
19
+ mid: { bucketMs: 10_000, cap: 60, buf: [] },
20
+ coarse: { bucketMs: 60_000, cap: 60, buf: [] },
21
+ };
22
+ /** 把一个原始样本并入指定分辨率的桶(同桶内做增量均值,跨桶则新建)。 */
23
+ function ingest(res, sample) {
24
+ const bucketTs = Math.floor(sample.ts / res.bucketMs) * res.bucketMs;
25
+ const last = res.buf[res.buf.length - 1];
26
+ if (last && last.ts === bucketTs) {
27
+ const n = last._n + 1;
28
+ last.procCpu = last.procCpu + (sample.procCpu - last.procCpu) / n;
29
+ last.sysCpu = last.sysCpu + (sample.sysCpu - last.sysCpu) / n;
30
+ last.procRss = last.procRss + (sample.procRss - last.procRss) / n;
31
+ last.sysMemUsed = last.sysMemUsed + (sample.sysMemUsed - last.sysMemUsed) / n;
32
+ last.netRxBps = last.netRxBps + (sample.netRxBps - last.netRxBps) / n;
33
+ last.netTxBps = last.netTxBps + (sample.netTxBps - last.netTxBps) / n;
34
+ for (const process of sample.processes) {
35
+ const current = last.processes.find(item => item.pid === process.pid);
36
+ if (current) {
37
+ const processSamples = current._n + 1;
38
+ current.cpuPercent += (process.cpuPercent - current.cpuPercent) / processSamples;
39
+ current.rss += (process.rss - current.rss) / processSamples;
40
+ current.name = process.name;
41
+ current.kind = process.kind;
42
+ current._n = processSamples;
43
+ }
44
+ else {
45
+ last.processes.push({ ...process, _n: 1 });
46
+ }
47
+ }
48
+ last._n = n;
49
+ }
50
+ else {
51
+ res.buf.push({ ...sample, ts: bucketTs, processes: sample.processes.map(process => ({ ...process, _n: 1 })), _n: 1 });
52
+ if (res.buf.length > res.cap)
53
+ res.buf.shift();
54
+ }
55
+ }
56
+ /** 导出某分辨率的纯数据点(剥掉内部 _n 字段)。 */
57
+ function exportBuf(res) {
58
+ return res.buf.map((b) => ({
59
+ ts: b.ts,
60
+ procCpu: round1(b.procCpu),
61
+ sysCpu: round1(b.sysCpu),
62
+ procRss: Math.round(b.procRss),
63
+ sysMemUsed: Math.round(b.sysMemUsed),
64
+ netRxBps: Math.round(b.netRxBps),
65
+ netTxBps: Math.round(b.netTxBps),
66
+ processes: b.processes.map(process => ({
67
+ pid: process.pid,
68
+ name: process.name,
69
+ kind: process.kind,
70
+ cpuPercent: round1(process.cpuPercent),
71
+ rss: Math.round(process.rss),
72
+ })),
73
+ }));
74
+ }
75
+ function round1(n) { return Math.round(n * 10) / 10; }
76
+ let previousNetworkSample = null;
77
+ let latestNetworkRate = { rxBps: 0, txBps: 0 };
78
+ function readNetworkTotals() {
79
+ try {
80
+ const lines = fs.readFileSync('/proc/net/dev', 'utf8').split('\n').slice(2);
81
+ let rxBytes = 0;
82
+ let txBytes = 0;
83
+ for (const line of lines) {
84
+ const separator = line.indexOf(':');
85
+ if (separator < 0)
86
+ continue;
87
+ const name = line.slice(0, separator).trim();
88
+ if (!name || /^(lo|docker\d*|br-|veth|virbr|vmnet|ifb)/.test(name))
89
+ continue;
90
+ const fields = line.slice(separator + 1).trim().split(/\s+/);
91
+ rxBytes += Number(fields[0]) || 0;
92
+ txBytes += Number(fields[8]) || 0;
93
+ }
94
+ return { rxBytes, txBytes };
95
+ }
96
+ catch {
97
+ return { rxBytes: 0, txBytes: 0 };
98
+ }
99
+ }
100
+ function sampleNetworkRate(ts) {
101
+ const totals = readNetworkTotals();
102
+ if (!previousNetworkSample) {
103
+ previousNetworkSample = { ...totals, ts };
104
+ return latestNetworkRate;
105
+ }
106
+ const elapsedMs = ts - previousNetworkSample.ts;
107
+ if (elapsedMs < 250)
108
+ return latestNetworkRate;
109
+ latestNetworkRate = {
110
+ rxBps: Math.max(0, (totals.rxBytes - previousNetworkSample.rxBytes) * 1000 / elapsedMs),
111
+ txBps: Math.max(0, (totals.txBytes - previousNetworkSample.txBytes) * 1000 / elapsedMs),
112
+ };
113
+ previousNetworkSample = { ...totals, ts };
114
+ return latestNetworkRate;
115
+ }
116
+ async function fetchSnapshot() {
117
+ const p = resolvePaths();
118
+ const resp = await ipcQuery(p.socket, { type: 'monitor-snapshot' }, 3000);
119
+ // resp === null → daemon 离线或 socket 不可达;resp.ok === false → 命令不受支持(旧 daemon)
120
+ if (!resp?.ok) {
121
+ return { daemonRunning: resp !== null, snapshot: null, history: { fine: [], mid: [], coarse: [] } };
122
+ }
123
+ const s = resp.snapshot;
124
+ const ts = Number(s.ts) || Date.now();
125
+ const network = sampleNetworkRate(ts);
126
+ const sample = {
127
+ ts,
128
+ procCpu: s.cpuPercent ?? 0,
129
+ sysCpu: s.system?.cpuPercent ?? 0,
130
+ procRss: s.memory?.rss ?? 0,
131
+ sysMemUsed: s.system?.memUsed ?? 0,
132
+ netRxBps: network.rxBps,
133
+ netTxBps: network.txBps,
134
+ processes: Array.isArray(s.processTree?.processes) ? s.processTree.processes.map((process) => ({
135
+ pid: Number(process.pid),
136
+ name: String(process.name || 'process'),
137
+ kind: String(process.kind || 'child'),
138
+ cpuPercent: Number(process.cpuPercent) || 0,
139
+ rss: Number(process.rss) || 0,
140
+ })) : [],
141
+ };
142
+ ingest(resolutions.fine, sample);
143
+ ingest(resolutions.mid, sample);
144
+ ingest(resolutions.coarse, sample);
145
+ return {
146
+ daemonRunning: true,
147
+ snapshot: s,
148
+ history: {
149
+ fine: exportBuf(resolutions.fine),
150
+ mid: exportBuf(resolutions.mid),
151
+ coarse: exportBuf(resolutions.coarse),
152
+ },
153
+ };
154
+ }
155
+ export const monitorSource = {
156
+ kind: 'monitor',
157
+ async snapshot() {
158
+ return fetchSnapshot();
159
+ },
160
+ subscribe(_params, push) {
161
+ let lastJson = '';
162
+ let stopped = false;
163
+ const tick = async () => {
164
+ if (stopped)
165
+ return;
166
+ try {
167
+ const snap = await fetchSnapshot();
168
+ const json = JSON.stringify(snap);
169
+ if (json !== lastJson) {
170
+ lastJson = json;
171
+ push(snap);
172
+ }
173
+ }
174
+ catch { /* ignore transient IPC errors */ }
175
+ };
176
+ const timer = setInterval(tick, 2000);
177
+ return () => { stopped = true; clearInterval(timer); };
178
+ },
179
+ };