ec-web 0.0.19 → 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 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`);
@@ -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
- const tokenPath = process.platform === 'win32'
21
- ? path.join(process.env.EVOLCORE_HOME || '', 'data', 'instance', 'control.token')
22
- : path.join(path.dirname(socketPath), 'control.token');
23
- const controlToken = fs.readFileSync(tokenPath, 'utf8').trim();
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
  }
@@ -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 未运行时:降级到读 instance/ 目录的 aid-*.jsonl(精简版 scanInstances)。
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 离线时降级用,读各 alive main 进程的 aid 活动日志 */
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
- const daemonRunning = aidsResp !== null || statusResp !== null;
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 buildSnapshot();
190
+ return readStableSnapshot();
119
191
  },
120
192
  subscribe(_params, push) {
121
- let lastJson = '';
122
- let stopped = false;
123
- const tick = async () => {
124
- if (stopped)
125
- return;
126
- try {
127
- const snap = await buildSnapshot();
128
- const json = JSON.stringify(snap);
129
- if (json !== lastJson) {
130
- lastJson = json;
131
- push(snap);
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
  };
@@ -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 process is not running, showing recent activity only',
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',
@@ -48,7 +48,7 @@
48
48
 
49
49
  </nav>
50
50
  <span style="flex:1"></span>
51
- 2026-08-27 15:27
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.19",
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": "dist/index.js"
7
+ "ec-web": "ecweb-wrapper.cjs"
8
8
  },
9
9
  "files": [
10
10
  "dist/"