claw-subagent-service 0.0.40 → 0.0.42

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.40",
3
+ "version": "0.0.42",
4
4
  "description": "虾说静态服务",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/service/daemon.js CHANGED
@@ -119,11 +119,38 @@ function freePortIfNeeded(port) {
119
119
  }
120
120
  }
121
121
  } else {
122
- const out = execSync(`lsof -i :${port} -t 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
123
- const pid = parseInt(out.trim(), 10);
124
- if (pid && pid > 0) {
122
+ // Linux / macOS:优先 lsof,兜底 fuser / ss / netstat / pkill
123
+ let pid = 0;
124
+ const commands = [
125
+ `lsof -i :${port} -t 2>/dev/null`,
126
+ `fuser ${port}/tcp 2>/dev/null`,
127
+ `ss -tlnp 2>/dev/null | grep -oP 'pid=\\K[0-9]+'`,
128
+ `netstat -tlnp 2>/dev/null | grep ":${port} " | awk '{print $7}' | cut -d'/' -f1`,
129
+ ];
130
+ for (const cmd of commands) {
131
+ try {
132
+ const out = execSync(cmd, { encoding: 'utf8', timeout: 5000 }).trim();
133
+ const firstLine = out.split(/\r?\n/)[0];
134
+ const candidate = parseInt(firstLine, 10);
135
+ if (candidate && candidate > 0 && candidate !== currentWorkerPid && candidate !== process.pid) {
136
+ pid = candidate;
137
+ break;
138
+ }
139
+ } catch { /* 命令不可用,继续下一个兜底 */ }
140
+ }
141
+
142
+ if (pid) {
125
143
  log.warn(`[DAEMON] 端口 ${port} 被进程 ${pid} 占用,正在释放...`);
126
- process.kill(pid, 'SIGKILL');
144
+ try { process.kill(pid, 'SIGKILL'); } catch (e) {
145
+ log.warn(`[DAEMON] 终止进程 ${pid} 失败: ${e.message}`);
146
+ }
147
+ } else {
148
+ // 所有命令都不可用,尝试按脚本名批量杀掉残留进程
149
+ try {
150
+ execSync('pkill -9 -f "daemon.js" 2>/dev/null || true', { timeout: 5000 });
151
+ execSync('pkill -9 -f "worker.js" 2>/dev/null || true', { timeout: 5000 });
152
+ log.warn(`[DAEMON] 已尝试按脚本名释放端口 ${port}`);
153
+ } catch { /* 忽略 */ }
127
154
  }
128
155
  }
129
156
  } catch { /* 端口已被释放或无法查询 */ }
@@ -63,8 +63,8 @@ class MessageHandler {
63
63
  }
64
64
  this.log?.info(`[MessageHandler] 消息提及本节点(${this.nodeId}),处理`);
65
65
  } else if (msg.conversationType === 3) {
66
- this.log?.info(`[MessageHandler] 群聊消息未 @ 本节点(${this.nodeId}),忽略`);
67
- return false;
66
+ // 群聊消息未 @ 任何人 → 视为所有人参与,正常处理
67
+ this.log?.info(`[MessageHandler] 群聊消息未 @ 任何人,本节点(${this.nodeId})参与处理`);
68
68
  } else {
69
69
  this.log?.info(`[MessageHandler] 单聊消息未指定节点,本节点(${this.nodeId})处理`);
70
70
  }
@@ -128,11 +128,12 @@ class RongCloudClient {
128
128
  try {
129
129
  const msgType = message.messageType;
130
130
  let rawContent = message.content;
131
- let mentionedInfo = null;
131
+ // 融云 SDK 中 mentionedInfo 通常在消息根级别
132
+ let mentionedInfo = message.mentionedInfo || null;
132
133
 
133
134
  // 自定义消息 content 可能是对象,提取文本内容并保留 mentionedInfo
134
135
  if (rawContent && typeof rawContent === 'object') {
135
- mentionedInfo = rawContent.mentionedInfo || null;
136
+ mentionedInfo = mentionedInfo || rawContent.mentionedInfo || null;
136
137
  rawContent = rawContent.content || rawContent.text || JSON.stringify(rawContent);
137
138
  }
138
139
 
package/service/worker.js CHANGED
@@ -65,8 +65,8 @@ function findPidOnPort(port) {
65
65
  for (const cmd of commands) {
66
66
  try {
67
67
  const out = execSync(cmd, { encoding: 'utf8', timeout: 5000 }).trim();
68
- const pid = parseInt(out.split('\n')[0], 10);
69
- if (!isNaN(pid) && pid > 0) return pid;
68
+ const candidate = parseInt(out.split('\n')[0], 10);
69
+ if (!isNaN(candidate) && candidate > 0 && candidate !== process.pid) return candidate;
70
70
  } catch { continue; }
71
71
  }
72
72
  }
@@ -95,7 +95,18 @@ function forceKill(pid) {
95
95
  function ensurePortFree(port) {
96
96
  for (let i = 0; i < 3; i++) {
97
97
  const pid = findPidOnPort(port);
98
- if (!pid) return true;
98
+ if (!pid) {
99
+ // 端口查询工具都不可用但端口仍可能被占用,按脚本名兜底清理一次
100
+ if (i === 0 && process.platform !== 'win32') {
101
+ try {
102
+ execSync('pkill -9 -f "daemon.js" 2>/dev/null || true', { timeout: 5000 });
103
+ execSync('pkill -9 -f "worker.js" 2>/dev/null || true', { timeout: 5000 });
104
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
105
+ } catch { /* 忽略 */ }
106
+ continue;
107
+ }
108
+ return true;
109
+ }
99
110
  log.warn(`[WORKER] 端口 ${port} 被进程 ${pid} 占用,正在释放...`);
100
111
  forceKill(pid);
101
112
  // 同步等待端口释放(最多 1.5s)
@@ -479,16 +490,23 @@ server.on('error', (err) => {
479
490
  log.error(`[WORKER] 端口 ${PORT} 被占用,尝试释放并重启监听...`);
480
491
  // 尝试杀死占用进程后重试
481
492
  const pid = findPidOnPort(PORT);
482
- if (pid) {
493
+ if (pid && pid !== process.pid) {
483
494
  log.warn(`[WORKER] 发现占用进程 ${pid},强制终止...`);
484
495
  forceKill(pid);
496
+ } else if (!pid) {
497
+ // 所有端口查询工具都不可用(常见于精简 Docker 镜像),按脚本名兜底清理
498
+ log.warn('[WORKER] 无法查询端口占用进程,尝试按脚本名清理残留...');
499
+ try {
500
+ execSync('pkill -9 -f "daemon.js" 2>/dev/null || true', { timeout: 5000 });
501
+ execSync('pkill -9 -f "worker.js" 2>/dev/null || true', { timeout: 5000 });
502
+ } catch { /* 忽略 */ }
485
503
  }
486
- // 延迟 2 秒后重试
504
+ // 延迟 3 秒后重试,给进程退出和端口释放留出足够时间
487
505
  setTimeout(() => {
488
506
  log.info(`[WORKER] 重新尝试监听端口 ${PORT}...`);
489
507
  server.close(() => {});
490
508
  server.listen(PORT, HOST);
491
- }, 2000);
509
+ }, 3000);
492
510
  return;
493
511
  }
494
512
  log.error(`[WORKER] HTTP 服务错误: ${err.message}`);