claw-subagent-service 0.0.4 → 0.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-subagent-service",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "虾说静态服务",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/service/daemon.js CHANGED
@@ -1,20 +1,70 @@
1
- const { fork } = require('child_process');
1
+ const { fork, execSync } = require('child_process');
2
2
  const path = require('path');
3
3
  const { createLogger } = require('./logger');
4
4
  const { Updater } = require('./updater');
5
+ const { checkPortListening } = require('./modules/port-checker');
5
6
 
6
7
  const log = createLogger('daemon');
7
8
  const WORKER_PATH = path.join(__dirname, 'worker.js');
9
+ const PORT = process.env.SILENT_SERVICE_PORT ? parseInt(process.env.SILENT_SERVICE_PORT, 10) : 38765;
8
10
 
9
11
  let worker = null;
10
12
  let stopping = false;
11
13
  let isRollingBack = false;
12
14
  let healthTimer = null;
13
15
  let currentBackupDir = null;
16
+ let crashCount = 0;
17
+ let lastCrashTime = 0;
14
18
  const updater = new Updater();
15
19
 
16
20
  process.chdir(__dirname);
17
21
 
22
+ /**
23
+ * 尝试释放占用的端口(杀死占用进程)
24
+ */
25
+ function freePortIfNeeded(port) {
26
+ try {
27
+ if (process.platform === 'win32') {
28
+ const out = execSync(`netstat -ano | findstr ":${port}"`, {
29
+ encoding: 'utf8', timeout: 5000, windowsHide: true,
30
+ });
31
+ for (const line of out.split('\n')) {
32
+ if (line.includes('LISTENING')) {
33
+ const pid = parseInt(line.trim().split(/\s+/).pop(), 10);
34
+ if (pid && pid > 0) {
35
+ log.warn(`[DAEMON] 端口 ${port} 被进程 ${pid} 占用,正在释放...`);
36
+ execSync(`taskkill /F /PID ${pid}`, { timeout: 5000, windowsHide: true });
37
+ }
38
+ }
39
+ }
40
+ } else {
41
+ const out = execSync(`lsof -i :${port} -t 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
42
+ const pid = parseInt(out.trim(), 10);
43
+ if (pid && pid > 0) {
44
+ log.warn(`[DAEMON] 端口 ${port} 被进程 ${pid} 占用,正在释放...`);
45
+ process.kill(pid, 'SIGKILL');
46
+ }
47
+ }
48
+ } catch { /* 端口已被释放或无法查询 */ }
49
+ }
50
+
51
+ /**
52
+ * 获取重启延迟(指数退避,最大 60 秒)
53
+ */
54
+ function getRestartDelay() {
55
+ const now = Date.now();
56
+ // 如果距离上次崩溃超过 30 秒,重置计数器
57
+ if (now - lastCrashTime > 30000) {
58
+ crashCount = 0;
59
+ }
60
+ lastCrashTime = now;
61
+ crashCount++;
62
+ // 指数退避: 1s, 2s, 4s, 8s, 16s, 32s, 60s(封顶)
63
+ const delay = Math.min(1000 * Math.pow(2, crashCount - 1), 60000);
64
+ log.info(`[DAEMON] Worker 连续崩溃 ${crashCount} 次,等待 ${delay/1000}s 后重启`);
65
+ return delay;
66
+ }
67
+
18
68
  function startWorker(isAfterUpdate = false, backupDirForRollback = null) {
19
69
  if (stopping || isRollingBack) return;
20
70
 
@@ -68,7 +118,10 @@ function startWorker(isAfterUpdate = false, backupDirForRollback = null) {
68
118
  }
69
119
 
70
120
  if (!stopping && !isRollingBack) {
71
- setTimeout(() => startWorker(false, null), 3000);
121
+ // 释放端口后使用指数退避重启
122
+ freePortIfNeeded(PORT);
123
+ const delay = getRestartDelay();
124
+ setTimeout(() => startWorker(false, null), delay);
72
125
  }
73
126
  });
74
127
 
package/service/worker.js CHANGED
@@ -2,6 +2,7 @@ const http = require('http');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
+ const { execSync } = require('child_process');
5
6
  const { createLogger } = require('./logger');
6
7
  const { RongCloudClient, MessageHandler, ensurePluginsAllow } = require('./rongcloud');
7
8
  const { RongyunMessageHandler } = require('./modules/rongyun-message-handler');
@@ -14,6 +15,65 @@ const { startOpencodeService, stopOpencodeService } = require('./modules/opencod
14
15
  const log = createLogger('worker');
15
16
  const PORT = process.env.SILENT_SERVICE_PORT ? parseInt(process.env.SILENT_SERVICE_PORT, 10) : 38765;
16
17
 
18
+ /**
19
+ * 查找占用指定端口的进程 PID
20
+ */
21
+ function findPidOnPort(port) {
22
+ try {
23
+ if (process.platform === 'win32') {
24
+ const out = execSync(`netstat -ano | findstr ":${port}"`, {
25
+ encoding: 'utf8', timeout: 5000, windowsHide: true,
26
+ });
27
+ for (const line of out.split('\n')) {
28
+ if (line.includes('LISTENING')) {
29
+ const pid = parseInt(line.trim().split(/\s+/).pop(), 10);
30
+ if (!isNaN(pid)) return pid;
31
+ }
32
+ }
33
+ } else {
34
+ const out = execSync(`lsof -i :${port} -t 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
35
+ const pid = parseInt(out.trim(), 10);
36
+ if (!isNaN(pid)) return pid;
37
+ }
38
+ } catch { /* port is free */ }
39
+ return null;
40
+ }
41
+
42
+ /**
43
+ * 强制终止进程
44
+ */
45
+ function forceKill(pid) {
46
+ try {
47
+ if (process.platform === 'win32') {
48
+ execSync(`taskkill /F /PID ${pid}`, { timeout: 5000, windowsHide: true });
49
+ } else {
50
+ process.kill(pid, 'SIGKILL');
51
+ }
52
+ return true;
53
+ } catch { return false; }
54
+ }
55
+
56
+ /**
57
+ * 确保端口未被占用:如果端口被占用,尝试杀死占用进程
58
+ * 最多重试 3 轮,每轮间隔 1 秒
59
+ */
60
+ function ensurePortFree(port) {
61
+ for (let i = 0; i < 3; i++) {
62
+ const pid = findPidOnPort(port);
63
+ if (!pid) return true;
64
+ log.warn(`[WORKER] 端口 ${port} 被进程 ${pid} 占用,正在释放...`);
65
+ forceKill(pid);
66
+ // 同步等待端口释放(最多 1.5s)
67
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1500);
68
+ }
69
+ const finalPid = findPidOnPort(port);
70
+ if (finalPid) {
71
+ log.error(`[WORKER] 端口 ${port} 被进程 ${finalPid} 占用,无法释放`);
72
+ return false;
73
+ }
74
+ return true;
75
+ }
76
+
17
77
  // Timestamp 校验专用日志
18
78
  const timestampLogPath = path.join(__dirname, '..', 'logs', 'timestamp-validation.log');
19
79
  const logTimestampValidation = (message) => {
@@ -281,10 +341,23 @@ const server = http.createServer((req, res) => {
281
341
  res.end('not found');
282
342
  });
283
343
 
344
+ // 启动前确保端口未被占用(防止 EADDRINUSE 导致崩溃循环)
345
+ if (!ensurePortFree(PORT)) {
346
+ log.error(`[WORKER] 端口 ${PORT} 无法使用,进程退出`);
347
+ process.exit(1);
348
+ }
349
+
284
350
  server.listen(PORT, '127.0.0.1', () => {
285
351
  log.info(`[WORKER] HTTP 服务已启动: http://127.0.0.1:${PORT}/health`);
286
352
  });
287
353
 
354
+ server.on('error', (err) => {
355
+ if (err.code === 'EADDRINUSE') {
356
+ log.error(`[WORKER] 端口 ${PORT} 被占用且无法释放,进程退出`);
357
+ process.exit(1);
358
+ }
359
+ });
360
+
288
361
  process.on('message', (msg) => {
289
362
  if (msg?.type === 'prepare-shutdown') {
290
363
  log.info(`[WORKER] 收到${msg.reason || 'unknown'}通知,准备优雅退出...`);