evolclaw-web 1.0.0 → 1.0.1

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
@@ -3,7 +3,7 @@
3
3
  * ecweb — EvolClaw 监控面板独立程序。
4
4
  *
5
5
  * 用法:
6
- * ecweb [--port 20030] [--home <EVOLCLAW_HOME>]
6
+ * ecweb [--port 42705] [--home <EVOLCLAW_HOME>]
7
7
  *
8
8
  * 通过 EVOLCLAW_HOME 定位 evolclaw 数据目录,启动 HTTP+WS 服务,浏览器配对码登录。
9
9
  * 与 evolclaw daemon 通过 IPC socket(live 状态)+ 文件系统(历史数据)旁路通信。
@@ -22,7 +22,7 @@ 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(`ecweb — EvolClaw 监控面板\n\n用法:\n ecweb [--port 20030] [--home <EVOLCLAW_HOME>]\n\n选项:\n --port, -p 监听端口(默认 20030,占用则自动 +1)\n --home EVOLCLAW_HOME 数据目录(默认读环境变量或 ~/.evolclaw)\n`);
25
+ process.stdout.write(`ecweb — EvolClaw 监控面板\n\n用法:\n ecweb [--port 42705] [--home <EVOLCLAW_HOME>]\n\n选项:\n --port, -p 监听端口(默认 42705,占用则自动 +1)\n --home EVOLCLAW_HOME 数据目录(默认读环境变量或 ~/.evolclaw)\n`);
26
26
  process.exit(0);
27
27
  }
28
28
  }
@@ -73,7 +73,7 @@ const killedWebs = cleanupWatchWebs();
73
73
  for (const r of killedWebs)
74
74
  logLine(`${YELLOW}↺ 已清理旧 watch 进程 PID ${r.pid}(端口 ${r.port})${RST}`);
75
75
  // 2) 兜底:按端口杀掉 instance 文件已丢失的孤儿进程(杀不掉的僵尸)
76
- const WATCH_WEB_PORT = port ?? 20030;
76
+ const WATCH_WEB_PORT = port ?? 42705;
77
77
  const killedByPort = cleanupWatchWebByPort(WATCH_WEB_PORT);
78
78
  for (const pid of killedByPort)
79
79
  logLine(`${YELLOW}↺ 已强占端口 ${WATCH_WEB_PORT}:杀掉占用进程 PID ${pid}${RST}`);
@@ -110,8 +110,16 @@ if (handle.displaced) {
110
110
  }
111
111
  process.stdout.write(`\n ${DIM}绑定 0.0.0.0,远程可访问。Ctrl-C 退出。${RST}\n`);
112
112
  process.stdout.write(` ${DIM}调试日志: ${logFile}${RST}\n\n`);
113
+ let cleaningUp = false;
113
114
  const cleanup = () => {
115
+ if (cleaningUp)
116
+ return; // 幂等:raw 模式下连按 Ctrl-C/q 不应重复触发
117
+ cleaningUp = true;
118
+ logLine(`${YELLOW}退出中…${RST}`);
114
119
  removeWatchWeb();
120
+ // 兜底:close() 万一卡住也强制退出,避免进程挂死
121
+ const force = setTimeout(() => process.exit(0), 2000);
122
+ force.unref();
115
123
  handle.close().finally(() => process.exit(0));
116
124
  };
117
125
  process.on('exit', () => removeWatchWeb());
@@ -123,7 +131,6 @@ if (process.stdin.isTTY) {
123
131
  process.stdin.on('data', (key) => {
124
132
  // Ctrl-C (0x03) 或 q 退出
125
133
  if (key[0] === 0x03 || key.toString() === 'q') {
126
- logLine(`${YELLOW}退出中…${RST}`);
127
134
  cleanup();
128
135
  }
129
136
  });
package/dist/server.js CHANGED
@@ -24,7 +24,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
24
24
  const STATIC_DIR = path.join(__dirname, 'static');
25
25
  const TOKEN_TTL_MS = 24 * 60 * 60 * 1000; // 24h
26
26
  const PAIRING_TTL_MS = 5 * 60 * 1000; // 5min
27
- const DEFAULT_PORT = 20030;
27
+ const DEFAULT_PORT = 42705;
28
28
  const PROTOCOL_VERSION = 1; // 与 evolclaw ping response 对齐的软校验版本
29
29
  const SOURCES = { aid: aidSource, msg: msgSource, session: sessionSource };
30
30
  const MIME = {
@@ -210,7 +210,22 @@ function handleConnection(ws, req, log) {
210
210
  await switchSubscription(msg.view, params);
211
211
  }
212
212
  });
213
- ws.on('close', () => { if (unsubscribe) {
213
+ // NAT keepalive: ping every 25s to prevent middlebox from cutting the connection
214
+ let alive = true;
215
+ const heartbeat = setInterval(() => {
216
+ if (ws.readyState !== ws.OPEN) {
217
+ clearInterval(heartbeat);
218
+ return;
219
+ }
220
+ if (!alive) {
221
+ ws.terminate();
222
+ return;
223
+ }
224
+ alive = false;
225
+ ws.ping();
226
+ }, 25000);
227
+ ws.on('pong', () => { alive = true; });
228
+ ws.on('close', () => { clearInterval(heartbeat); if (unsubscribe) {
214
229
  unsubscribe();
215
230
  unsubscribe = null;
216
231
  } log(`◇ WS 断开 from ${ip}`); });
@@ -287,13 +302,20 @@ export async function startWatchWebServer(opts = {}) {
287
302
  pairingCode,
288
303
  close() {
289
304
  return new Promise((resolve) => {
305
+ // 强制断开所有 WS 客户端(graceful close 握手可能永不完成)
290
306
  for (const client of wss.clients)
291
307
  try {
292
- client.close();
308
+ client.terminate();
293
309
  }
294
310
  catch { }
295
311
  wss.close();
312
+ // server.close() 仅停止接受新连接,会等待存量连接(含 HTTP keep-alive、已升级的 WS)排空,
313
+ // 否则回调永不触发 → 进程挂起。Node 18.2+ 用 closeAllConnections() 强制关闭。
296
314
  server.close(() => resolve());
315
+ try {
316
+ server.closeAllConnections();
317
+ }
318
+ catch { }
297
319
  });
298
320
  },
299
321
  };
@@ -259,6 +259,7 @@ function readTranscriptFile(file) {
259
259
  return empty;
260
260
  }
261
261
  const turns = [];
262
+ const counts = { userInput: 0, modelOutput: 0, toolCall: 0, toolResult: 0, msgSend: 0 };
262
263
  let inTok = 0, outTok = 0, model = '', branch = '', version = '', title = '', cwd = '', userMsgs = 0, totalMsgs = 0, contextTokens = 0, costUsd = 0, lastUsageKey = '';
263
264
  for (const line of raw.split('\n')) {
264
265
  if (!line.trim())
@@ -304,12 +305,45 @@ function readTranscriptFile(file) {
304
305
  if (o.message.model)
305
306
  model = o.message.model;
306
307
  }
307
- // 简化:turns 仅记录必要字段(省略完整 block 解析)
308
- if (o.type === 'user' || o.type === 'assistant')
309
- turns.push({ role: o.type, ts: o.timestamp ? Date.parse(o.timestamp) : 0, uuid: o.uuid });
308
+ if (o.type === 'user' || o.type === 'assistant') {
309
+ const content = o.message?.content;
310
+ const arr = typeof content === 'string' ? [{ type: 'text', text: content }] : (Array.isArray(content) ? content : []);
311
+ const blocks = [];
312
+ let hasToolUse = false, hasToolResult = false;
313
+ for (const item of arr) {
314
+ if (!item || typeof item !== 'object')
315
+ continue;
316
+ if (item.type === 'text' && item.text) {
317
+ blocks.push({ kind: 'text', text: item.text });
318
+ }
319
+ else if (item.type === 'thinking' && item.thinking) {
320
+ blocks.push({ kind: 'thinking', text: item.thinking });
321
+ }
322
+ else if (item.type === 'tool_use') {
323
+ const inputStr = item.input ? JSON.stringify(item.input, null, 2) : '';
324
+ blocks.push({ kind: 'tool_use', name: item.name || '', input: item.input || {}, inputStr });
325
+ hasToolUse = true;
326
+ }
327
+ else if (item.type === 'tool_result') {
328
+ const c = item.content;
329
+ const text = typeof c === 'string' ? c : (Array.isArray(c) ? c.filter((x) => x?.type === 'text').map((x) => x.text).join('\n') : '');
330
+ blocks.push({ kind: 'tool_result', text, isError: !!item.is_error });
331
+ hasToolResult = true;
332
+ }
333
+ }
334
+ let category;
335
+ if (o.type === 'user') {
336
+ category = hasToolResult ? 'tool_result' : 'user_input';
337
+ }
338
+ else {
339
+ category = hasToolUse ? 'tool_call' : 'model_output';
340
+ }
341
+ counts[category === 'user_input' ? 'userInput' : category === 'model_output' ? 'modelOutput' : category === 'tool_call' ? 'toolCall' : 'toolResult']++;
342
+ turns.push({ role: o.type, ts: o.timestamp ? Date.parse(o.timestamp) : 0, uuid: o.uuid, category, blocks });
343
+ }
310
344
  }
311
345
  const shown = turns.length > 500 ? turns.slice(-500) : turns;
312
- return { turns: shown, totalTurns: turns.length, userMsgs, totalMsgs, counts: { userInput: 0, modelOutput: 0, toolCall: 0, toolResult: 0, msgSend: 0 }, inputTokens: inTok, outputTokens: outTok, contextTokens, costUsd, model, gitBranch: branch, version, title, cwd };
346
+ return { turns: shown, totalTurns: turns.length, userMsgs, totalMsgs, counts, inputTokens: inTok, outputTokens: outTok, contextTokens, costUsd, model, gitBranch: branch, version, title, cwd };
313
347
  }
314
348
  function buildSnapshot(params) {
315
349
  const projects = listProjects();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evolclaw-web",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Web-based monitoring dashboard for EvolClaw",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,11 @@
13
13
  "build": "tsc && chmod +x dist/index.js && cp -r src/static dist/",
14
14
  "prepublishOnly": "npm run build"
15
15
  },
16
- "keywords": ["evolclaw", "monitoring", "debug"],
16
+ "keywords": [
17
+ "evolclaw",
18
+ "monitoring",
19
+ "debug"
20
+ ],
17
21
  "author": "",
18
22
  "license": "MIT",
19
23
  "peerDependencies": {