ec-web 0.0.18 → 0.0.20
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/index.js +5 -0
- package/dist/ipc-client.js +17 -4
- package/dist/paths.js +1 -6
- package/dist/sources/aid.js +96 -26
- package/dist/static/app.js +2 -2
- package/dist/static/index.html +1 -1
- package/ecweb-wrapper.cjs +64 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -33,6 +33,11 @@ if (home)
|
|
|
33
33
|
const { resolvePaths } = await import('./paths.js');
|
|
34
34
|
const { startWatchWebServer } = await import('./server.js');
|
|
35
35
|
const p = resolvePaths();
|
|
36
|
+
// 统一行为:无论是 supervisor 启动(已设 env)还是手动启动(未设 env),
|
|
37
|
+
// 都确保 EVOLCORE_HOME 环境变量存在,让所有依赖 env 的模块(如 ipc-client)行为一致
|
|
38
|
+
if (!process.env.EVOLCORE_HOME) {
|
|
39
|
+
process.env.EVOLCORE_HOME = p.root;
|
|
40
|
+
}
|
|
36
41
|
// 校验 home 目录存在
|
|
37
42
|
if (!fs.existsSync(p.root)) {
|
|
38
43
|
process.stderr.write(`❌ EVOLCORE_HOME 不存在: ${p.root}\n 用 --home 指定正确路径,或设置 EVOLCORE_HOME 环境变量。\n`);
|
package/dist/ipc-client.js
CHANGED
|
@@ -17,10 +17,23 @@ export function ipcQuery(socketPath, cmd, timeoutMs = 3000) {
|
|
|
17
17
|
let request = cmd;
|
|
18
18
|
if (LOCAL_CONTROL_COMMANDS.has(cmd.type) && typeof cmd.controlToken !== 'string') {
|
|
19
19
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// 入口会设置 EVOLCORE_HOME;保留 socket 旁路回退,便于独立调用和
|
|
21
|
+
// 旧版本地布局继续完成受保护 IPC 查询。
|
|
22
|
+
const tokenPaths = [
|
|
23
|
+
...(process.env.EVOLCORE_HOME
|
|
24
|
+
? [path.join(process.env.EVOLCORE_HOME, 'data', 'instance', 'control.token')]
|
|
25
|
+
: []),
|
|
26
|
+
path.join(path.dirname(socketPath), 'control.token'),
|
|
27
|
+
];
|
|
28
|
+
let controlToken = '';
|
|
29
|
+
for (const tokenPath of tokenPaths) {
|
|
30
|
+
try {
|
|
31
|
+
controlToken = fs.readFileSync(tokenPath, 'utf8').trim();
|
|
32
|
+
if (controlToken)
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
24
37
|
if (controlToken)
|
|
25
38
|
request = { ...cmd, controlToken };
|
|
26
39
|
}
|
package/dist/paths.js
CHANGED
|
@@ -7,12 +7,10 @@
|
|
|
7
7
|
* 根目录解析优先级:
|
|
8
8
|
* 1. --home 命令行参数(由 index.ts 写入 process.env.EVOLCORE_HOME)
|
|
9
9
|
* 2. EVOLCORE_HOME 环境变量
|
|
10
|
-
* 3.
|
|
11
|
-
* 4. ~/.evolcore
|
|
10
|
+
* 3. ~/.evolcore
|
|
12
11
|
*/
|
|
13
12
|
import path from 'path';
|
|
14
13
|
import os from 'os';
|
|
15
|
-
import fs from 'fs';
|
|
16
14
|
import crypto from 'crypto';
|
|
17
15
|
const isWindows = process.platform === 'win32';
|
|
18
16
|
let _root = null;
|
|
@@ -22,9 +20,6 @@ export function resolveRoot() {
|
|
|
22
20
|
if (process.env.EVOLCORE_HOME) {
|
|
23
21
|
_root = process.env.EVOLCORE_HOME;
|
|
24
22
|
}
|
|
25
|
-
else if (fs.existsSync(path.join(process.cwd(), 'agents', 'defaults.json'))) {
|
|
26
|
-
_root = process.cwd();
|
|
27
|
-
}
|
|
28
23
|
else {
|
|
29
24
|
_root = path.join(os.homedir(), '.evolcore');
|
|
30
25
|
}
|
package/dist/sources/aid.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* AID 数据源 — 复用 daemon 的 IPC socket(与 evolcore `watch aid` 同源)。
|
|
3
3
|
*
|
|
4
4
|
* daemon 运行时:拉 aun-aids / aun-aid-stats / agent-stats / status / evolagent.list。
|
|
5
|
-
* daemon
|
|
5
|
+
* daemon IPC 连续不可达时:降级到读 instance/ 目录的 aid-*.jsonl(精简版 scanInstances)。
|
|
6
6
|
* IPC 无推送能力,故 1s 轮询 + JSON diff,仅在变化时 push。
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { resolvePaths } from '../paths.js';
|
|
11
11
|
import { ipcQuery } from '../ipc-client.js';
|
|
12
|
-
/** 精简版实例扫描:仅在 daemon
|
|
12
|
+
/** 精简版实例扫描:仅在 daemon IPC 不可达时降级用,读各 alive main 进程的 aid 活动日志 */
|
|
13
13
|
function scanAidActivity() {
|
|
14
14
|
const result = new Map();
|
|
15
15
|
const dir = resolvePaths().instanceDir;
|
|
@@ -81,7 +81,7 @@ async function fetchVersion(socket) {
|
|
|
81
81
|
return null;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
async function buildSnapshot() {
|
|
84
|
+
async function buildSnapshot(fallback = null) {
|
|
85
85
|
const p = resolvePaths();
|
|
86
86
|
const [aidsResp, statsResp, agentStatsResp, statusResp, agentsResp, version] = await Promise.all([
|
|
87
87
|
ipcQuery(p.socket, { type: 'aun-aids' }),
|
|
@@ -91,7 +91,10 @@ async function buildSnapshot() {
|
|
|
91
91
|
ipcQuery(p.socket, { type: 'evolagent.list' }),
|
|
92
92
|
fetchVersion(p.socket),
|
|
93
93
|
]);
|
|
94
|
-
|
|
94
|
+
// 任一只读命令能返回就说明 IPC/daemon 仍可达。慢主机上某个较重的
|
|
95
|
+
// 统计命令单独超时,不应把整个 Agent 页面判成离线。
|
|
96
|
+
const daemonRunning = [aidsResp, statsResp, agentStatsResp, statusResp, agentsResp]
|
|
97
|
+
.some((response) => response !== null);
|
|
95
98
|
if (!daemonRunning) {
|
|
96
99
|
const activity = scanAidActivity();
|
|
97
100
|
const aids = Array.from(activity.entries()).map(([aid, info]) => ({
|
|
@@ -102,38 +105,105 @@ async function buildSnapshot() {
|
|
|
102
105
|
}));
|
|
103
106
|
return { daemonRunning: false, aids, stats: [], agents: [] };
|
|
104
107
|
}
|
|
108
|
+
const previous = fallback?.daemonRunning ? fallback : null;
|
|
105
109
|
return {
|
|
106
110
|
daemonRunning: true,
|
|
107
|
-
aids: aidsResp?.aids ?? [],
|
|
108
|
-
stats: statsResp?.stats ?? [],
|
|
109
|
-
agentStats: agentStatsResp?.stats ?? [],
|
|
110
|
-
status: statusResp ?? null,
|
|
111
|
-
agents: agentsResp?.agents ?? [],
|
|
112
|
-
version: version ?? null,
|
|
111
|
+
aids: Array.isArray(aidsResp?.aids) ? aidsResp.aids : (previous?.aids ?? []),
|
|
112
|
+
stats: Array.isArray(statsResp?.stats) ? statsResp.stats : (previous?.stats ?? []),
|
|
113
|
+
agentStats: Array.isArray(agentStatsResp?.stats) ? agentStatsResp.stats : (previous?.agentStats ?? []),
|
|
114
|
+
status: statusResp !== null ? statusResp : (previous?.status ?? null),
|
|
115
|
+
agents: Array.isArray(agentsResp?.agents) ? agentsResp.agents : (previous?.agents ?? []),
|
|
116
|
+
version: version ?? previous?.version ?? null,
|
|
113
117
|
};
|
|
114
118
|
}
|
|
119
|
+
const AID_POLL_INTERVAL_MS = 1000;
|
|
120
|
+
const DAEMON_FAILURE_THRESHOLD = 3;
|
|
121
|
+
// IPC 查询是整页共享的:每个浏览器标签页各自发起一组查询会在慢主机上
|
|
122
|
+
// 形成请求堆积。共享 promise 还可以让初始 snapshot 和轮询复用同一轮结果。
|
|
123
|
+
const subscribers = new Set();
|
|
124
|
+
let pollTimer = null;
|
|
125
|
+
let pollInFlight = null;
|
|
126
|
+
let lastSuccessfulSnapshot = null;
|
|
127
|
+
let lastResolvedSnapshot = null;
|
|
128
|
+
let consecutiveDaemonFailures = 0;
|
|
129
|
+
async function readStableSnapshot() {
|
|
130
|
+
if (!pollInFlight) {
|
|
131
|
+
pollInFlight = (async () => {
|
|
132
|
+
let snapshot;
|
|
133
|
+
try {
|
|
134
|
+
snapshot = await buildSnapshot(lastSuccessfulSnapshot);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
snapshot = { daemonRunning: false, aids: [], stats: [], agentStats: [], agents: [] };
|
|
138
|
+
}
|
|
139
|
+
if (snapshot?.daemonRunning) {
|
|
140
|
+
consecutiveDaemonFailures = 0;
|
|
141
|
+
lastSuccessfulSnapshot = snapshot;
|
|
142
|
+
lastResolvedSnapshot = snapshot;
|
|
143
|
+
return snapshot;
|
|
144
|
+
}
|
|
145
|
+
consecutiveDaemonFailures += 1;
|
|
146
|
+
// 单轮 IPC 超时不代表 daemon 已停止。保留最近一次成功数据,直到
|
|
147
|
+
// 连续多轮都失败,避免页面在慢主机上反复清空和恢复。
|
|
148
|
+
if (lastSuccessfulSnapshot && consecutiveDaemonFailures < DAEMON_FAILURE_THRESHOLD) {
|
|
149
|
+
lastResolvedSnapshot = lastSuccessfulSnapshot;
|
|
150
|
+
return lastSuccessfulSnapshot;
|
|
151
|
+
}
|
|
152
|
+
lastResolvedSnapshot = snapshot;
|
|
153
|
+
return snapshot;
|
|
154
|
+
})().finally(() => {
|
|
155
|
+
pollInFlight = null;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return pollInFlight;
|
|
159
|
+
}
|
|
160
|
+
function notifySubscribers(snapshot) {
|
|
161
|
+
const json = JSON.stringify(snapshot);
|
|
162
|
+
for (const subscriber of subscribers) {
|
|
163
|
+
if (json === subscriber.lastJson)
|
|
164
|
+
continue;
|
|
165
|
+
subscriber.lastJson = json;
|
|
166
|
+
try {
|
|
167
|
+
subscriber.push(snapshot);
|
|
168
|
+
}
|
|
169
|
+
catch { /* subscriber disconnected */ }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function schedulePoll(delayMs = AID_POLL_INTERVAL_MS) {
|
|
173
|
+
if (pollTimer !== null || subscribers.size === 0)
|
|
174
|
+
return;
|
|
175
|
+
pollTimer = setTimeout(async () => {
|
|
176
|
+
pollTimer = null;
|
|
177
|
+
if (subscribers.size === 0)
|
|
178
|
+
return;
|
|
179
|
+
try {
|
|
180
|
+
notifySubscribers(await readStableSnapshot());
|
|
181
|
+
}
|
|
182
|
+
catch { /* ignore transient IPC errors */ }
|
|
183
|
+
// 下一轮从本轮完成后开始计时,绝不与上一轮重叠。
|
|
184
|
+
schedulePoll();
|
|
185
|
+
}, delayMs);
|
|
186
|
+
}
|
|
115
187
|
export const aidSource = {
|
|
116
188
|
kind: 'agents',
|
|
117
189
|
async snapshot() {
|
|
118
|
-
return
|
|
190
|
+
return readStableSnapshot();
|
|
119
191
|
},
|
|
120
192
|
subscribe(_params, push) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
193
|
+
// server 会在 subscribe 前先发送 snapshot;以最近成功值作为基线,避免
|
|
194
|
+
// 第一轮轮询把同一份数据再推送一次。
|
|
195
|
+
const subscriber = {
|
|
196
|
+
push,
|
|
197
|
+
lastJson: lastResolvedSnapshot ? JSON.stringify(lastResolvedSnapshot) : '',
|
|
198
|
+
};
|
|
199
|
+
subscribers.add(subscriber);
|
|
200
|
+
schedulePoll();
|
|
201
|
+
return () => {
|
|
202
|
+
subscribers.delete(subscriber);
|
|
203
|
+
if (subscribers.size === 0 && pollTimer !== null) {
|
|
204
|
+
clearTimeout(pollTimer);
|
|
205
|
+
pollTimer = null;
|
|
133
206
|
}
|
|
134
|
-
catch { /* ignore transient IPC errors */ }
|
|
135
207
|
};
|
|
136
|
-
const timer = setInterval(tick, 1000);
|
|
137
|
-
return () => { stopped = true; clearInterval(timer); };
|
|
138
208
|
},
|
|
139
209
|
};
|
package/dist/static/app.js
CHANGED
|
@@ -139,7 +139,7 @@ const translations = {
|
|
|
139
139
|
// Agents view
|
|
140
140
|
'agents.subtitle.enabled': '启用',
|
|
141
141
|
'agents.subtitle.disabled': '禁用',
|
|
142
|
-
'agents.daemonStopped': '⚠ Daemon
|
|
142
|
+
'agents.daemonStopped': '⚠ Daemon IPC 暂时不可用,仅显示最近活动记录',
|
|
143
143
|
'agents.empty.disabled': '暂无禁用 Agent',
|
|
144
144
|
'agents.empty.enabled': '暂无启用 Agent',
|
|
145
145
|
'agents.stats.gateway': 'Daemon Gateway',
|
|
@@ -1045,7 +1045,7 @@ const translations = {
|
|
|
1045
1045
|
// Agents view
|
|
1046
1046
|
'agents.subtitle.enabled': 'Enabled',
|
|
1047
1047
|
'agents.subtitle.disabled': 'Disabled',
|
|
1048
|
-
'agents.daemonStopped': '⚠ Daemon
|
|
1048
|
+
'agents.daemonStopped': '⚠ Daemon IPC is temporarily unavailable, showing recent activity only',
|
|
1049
1049
|
'agents.empty.disabled': 'No disabled agents',
|
|
1050
1050
|
'agents.empty.enabled': 'No enabled agents',
|
|
1051
1051
|
'agents.stats.gateway': 'Daemon Gateway',
|
package/dist/static/index.html
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
</nav>
|
|
50
50
|
<span style="flex:1"></span>
|
|
51
|
-
2026-08-
|
|
51
|
+
2026-08-28 04:38
|
|
52
52
|
<span id="conn-status" class="conn-status" data-i18n="status.connecting">连接中…</span>
|
|
53
53
|
<button id="lang-btn" class="theme-btn" data-i18n-title="language.buttonTitle" title="切换语言">🌐</button>
|
|
54
54
|
<button id="theme-btn" class="theme-btn" data-i18n-title="theme.buttonTitle" title="主题">🌗</button>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ecweb 智能启动包装器
|
|
4
|
+
* 检测源码变化,需要时自动重新编译,然后启动
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { execSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const ECWEB_DIR = path.resolve(__dirname);
|
|
12
|
+
const SRC_DIR = path.join(ECWEB_DIR, 'src');
|
|
13
|
+
const DIST_DIR = path.join(ECWEB_DIR, 'dist');
|
|
14
|
+
const DIST_INDEX = path.join(DIST_DIR, 'index.js');
|
|
15
|
+
|
|
16
|
+
function needsBuild() {
|
|
17
|
+
if (!fs.existsSync(DIST_INDEX)) return true;
|
|
18
|
+
// Published packages contain the compiled dist/ tree but not the TypeScript
|
|
19
|
+
// source or devDependencies. Only source checkouts should trigger a build.
|
|
20
|
+
if (!fs.existsSync(SRC_DIR)) return false;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const distStat = fs.statSync(DIST_INDEX);
|
|
24
|
+
const distMtime = distStat.mtimeMs;
|
|
25
|
+
|
|
26
|
+
// 递归检查所有 src 文件
|
|
27
|
+
let newestSrcMtime = 0;
|
|
28
|
+
function walkDir(dir) {
|
|
29
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
const fullPath = path.join(dir, entry.name);
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
walkDir(fullPath);
|
|
34
|
+
} else if (entry.isFile() && /\.(ts|tsx|json)$/.test(entry.name)) {
|
|
35
|
+
const stat = fs.statSync(fullPath);
|
|
36
|
+
if (stat.mtimeMs > newestSrcMtime) {
|
|
37
|
+
newestSrcMtime = stat.mtimeMs;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
walkDir(SRC_DIR);
|
|
43
|
+
|
|
44
|
+
return newestSrcMtime > distMtime;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (needsBuild()) {
|
|
51
|
+
console.error('🔨 检测到代码变化,编译中...');
|
|
52
|
+
try {
|
|
53
|
+
execSync('npm run build', { cwd: ECWEB_DIR, stdio: 'inherit' });
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error('❌ 编译失败');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 执行真正的入口(ES module 需要用 import,Windows 路径需转 file:// URL)
|
|
61
|
+
(async () => {
|
|
62
|
+
const { pathToFileURL } = require('url');
|
|
63
|
+
await import(pathToFileURL(DIST_INDEX).href);
|
|
64
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ec-web",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "Web console and monitoring dashboard for EvolCore",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ec-web": "
|
|
7
|
+
"ec-web": "ecweb-wrapper.cjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/"
|