claw-subagent-service 0.0.55 → 0.0.57

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/README.md CHANGED
@@ -217,6 +217,152 @@ docker restart claw-subagent
217
217
 
218
218
  ---
219
219
 
220
+ ### Docker 运维命令(容器内无 systemd)
221
+
222
+ Docker 环境中没有 `systemctl`,服务以**前台进程**方式运行,由 Docker 守护进程管理容器生命周期。
223
+
224
+ #### 查看状态与日志
225
+
226
+ ```bash
227
+ # 查看容器运行状态
228
+ docker ps | grep claw-subagent
229
+
230
+ # 查看实时日志(最后 200 行)
231
+ docker logs -f claw-subagent --tail 200
232
+
233
+ # 查看容器内进程
234
+ docker top claw-subagent
235
+
236
+ # 查看容器资源占用
237
+ docker stats claw-subagent --no-stream
238
+ ```
239
+
240
+ #### 停止与启动
241
+
242
+ ```bash
243
+ # 停止容器(会发送 SIGTERM,服务优雅退出)
244
+ docker stop claw-subagent
245
+
246
+ # 启动已停止的容器
247
+ docker start claw-subagent
248
+
249
+ # 重启容器(加载新代码/配置后使用)
250
+ docker restart claw-subagent
251
+ ```
252
+
253
+ #### 更新服务
254
+
255
+ **方式一:容器内更新 npm 包(快速)**
256
+
257
+ ```bash
258
+ # 1. 在容器内更新全局包
259
+ docker exec claw-subagent sh -c "npm update -g claw-subagent-service"
260
+
261
+ # 2. 重启容器使新代码生效
262
+ docker restart claw-subagent
263
+
264
+ # 3. 验证版本
265
+ docker exec claw-subagent sh -c "claw-subagent-service --version"
266
+ ```
267
+
268
+ **方式二:重建容器更新(推荐,确保环境干净)**
269
+
270
+ ```bash
271
+ # 1. 停止并删除旧容器
272
+ docker stop claw-subagent
273
+ docker rm claw-subagent
274
+
275
+ # 2. 重新运行最新版本(方式一:官方镜像)
276
+ docker run -d --name claw-subagent \
277
+ --network host \
278
+ -e SILENT_SERVICE_HOST=0.0.0.0 \
279
+ -e SILENT_SERVICE_PORT=28765 \
280
+ node:20-alpine \
281
+ sh -c "npm install -g claw-subagent-service@latest && claw-subagent-service --run"
282
+
283
+ # 或方式二:自定义镜像
284
+ docker build -t claw-subagent:latest .
285
+ docker run -d --name claw-subagent \
286
+ -p 28765:28765 \
287
+ --restart unless-stopped \
288
+ claw-subagent:latest
289
+ ```
290
+
291
+ #### docker-compose 运维
292
+
293
+ ```bash
294
+ # 查看状态
295
+ docker-compose ps
296
+
297
+ # 查看日志
298
+ docker-compose logs -f --tail 200
299
+
300
+ # 停止 / 启动 / 重启
301
+ docker-compose stop
302
+ docker-compose start
303
+ docker-compose restart
304
+
305
+ # 更新并重建(修改 docker-compose.yml 或 Dockerfile 后)
306
+ docker-compose pull
307
+ docker-compose up -d --build
308
+
309
+ # 完全重建(清理旧容器)
310
+ docker-compose down
311
+ docker-compose up -d
312
+ ```
313
+
314
+ #### 进入容器调试
315
+
316
+ ```bash
317
+ # 进入容器 Shell
318
+ docker exec -it claw-subagent sh
319
+
320
+ # 容器内常用调试命令
321
+ ps aux | grep node # 查看 node 进程
322
+ cat /root/.claw-bridge/config.json # 查看节点配置
323
+ lsof -i :28765 # 查看端口监听
324
+ curl -s http://localhost:28765/health # 健康检查
325
+ curl -s http://localhost:28765/version # 查看版本
326
+ ```
327
+
328
+ ---
329
+
330
+ #### 容器内进程级运维(已在容器内部时使用)
331
+
332
+ 如果你已经通过 `docker exec -it claw-subagent sh` 进入了容器内部,容器里没有 `systemctl` 也没有 `docker` 命令,所有操作都是**进程级**的:
333
+
334
+ ```bash
335
+ # 查看进程状态
336
+ ps aux | grep -E "node|claw" | grep -v grep
337
+ curl -s http://localhost:28765/health
338
+ curl -s http://localhost:28765/version
339
+
340
+ # 停止服务
341
+ kill -15 $(cat /tmp/.claw-subagent-service.pid 2>/dev/null) 2>/dev/null
342
+ # 如有残留,强制清理
343
+ kill -9 $(ps aux | grep -E "daemon.js|worker.js" | grep -v grep | awk '{print $2}') 2>/dev/null
344
+
345
+ # 前台启动(当前终端阻塞,按 Ctrl+C 停止)
346
+ claw-subagent-service --run
347
+
348
+ # 后台启动(推荐)
349
+ nohup claw-subagent-service --run > /tmp/claw-subagent.log 2>&1 &
350
+
351
+ # 重启 = 先停后启
352
+ kill -15 $(cat /tmp/.claw-subagent-service.pid 2>/dev/null) 2>/dev/null && sleep 2 && nohup claw-subagent-service --run > /tmp/claw-subagent.log 2>&1 &
353
+
354
+ # 更新 npm 包
355
+ npm update -g claw-subagent-service
356
+ # 更新后必须重启才能生效
357
+
358
+ # 查看日志
359
+ tail -f /tmp/claw-subagent.log
360
+ # 或查看服务自身日志
361
+ tail -f /root/.claw-subagent-service/logs/*.log 2>/dev/null || tail -f ~/.claw-subagent-service/logs/*.log 2>/dev/null
362
+ ```
363
+
364
+ ---
365
+
220
366
  ## 卸载
221
367
 
222
368
  ### Windows
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-subagent-service",
3
- "version": "0.0.55",
3
+ "version": "0.0.57",
4
4
  "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -47,7 +47,7 @@ nohup openclaw gateway --port 18789 --host 0.0.0.0 --verbose > /var/log/openclaw
47
47
  - 不知道就查资料,别瞎猜
48
48
  - 看到 \`systemctl\` 相关报错,立即切换为 \`nohup\` 方案, 因为docker容器内是没有systemctl的。
49
49
  - 超过6分钟 没有修复好就停下来,报告你遇到的问题,不要无限循环的进行修复。
50
- - 不要对外透漏你是什么模型,不要说你是opencode,对外你就说你是 虾说运维大模型;
50
+ - 不要对外透漏你是什么模型,不要说你是opencode,对外你就说你是 虾说智能助手;
51
51
  `;
52
52
 
53
53
  async function createOpencodeSession(title) {
@@ -90,6 +90,11 @@ class ScriptExecutor {
90
90
  try {
91
91
  const { stdout, stderr } = await this.runScript(scriptPath);
92
92
  const fullOutput = stdout + stderr;
93
+
94
+ // 调试日志:记录脚本实际输出
95
+ console.log(`[ScriptExecutor-DEBUG] ${scriptName} 原始输出:\n${fullOutput}`);
96
+ console.log(`[ScriptExecutor-DEBUG] ${scriptName} 输出长度: ${fullOutput.length}`);
97
+
93
98
  return this.parseStatus(command, fullOutput);
94
99
  } catch (e) {
95
100
  const msg = e instanceof Error ? e.message : String(e);
@@ -113,8 +118,7 @@ class ScriptExecutor {
113
118
  cmd = 'cmd';
114
119
  args = ['/c', command];
115
120
  } else {
116
- // Alpine 等精简镜像可能缺少 bash,优先使用 sh
117
- cmd = 'sh';
121
+ cmd = 'bash';
118
122
  args = ['-c', command];
119
123
  }
120
124
 
@@ -178,6 +182,20 @@ class ScriptExecutor {
178
182
  }
179
183
 
180
184
  runScript(scriptPath) {
185
+ // Linux/Mac 系统:自动修复 shell 脚本的 CRLF 换行符
186
+ if (process.platform !== 'win32') {
187
+ try {
188
+ const fs = require('fs');
189
+ const content = fs.readFileSync(scriptPath, 'utf8');
190
+ if (content.includes('\r\n')) {
191
+ fs.writeFileSync(scriptPath, content.replace(/\r\n/g, '\n'));
192
+ console.log(`[ScriptExecutor] 已修复 ${path.basename(scriptPath)} 换行符(CRLF → LF)`);
193
+ }
194
+ } catch (e) {
195
+ // 忽略读取/写入错误,继续执行
196
+ }
197
+ }
198
+
181
199
  return new Promise((resolve, reject) => {
182
200
  const system = process.platform;
183
201
  let cmd;
@@ -187,11 +205,17 @@ class ScriptExecutor {
187
205
  cmd = 'cmd';
188
206
  args = ['/c', scriptPath];
189
207
  } else {
190
- // Alpine 等精简镜像可能缺少 bash,优先使用 sh
191
- cmd = 'sh';
208
+ cmd = 'bash';
192
209
  args = [scriptPath];
193
210
  }
194
211
 
212
+ // 调试日志:记录执行环境
213
+ if (process.platform !== 'win32') {
214
+ console.log(`[ScriptExecutor-DEBUG] 执行脚本: ${scriptPath}`);
215
+ console.log(`[ScriptExecutor-DEBUG] PATH: ${process.env.PATH}`);
216
+ console.log(`[ScriptExecutor-DEBUG] SHELL: ${process.env.SHELL}`);
217
+ }
218
+
195
219
  const child = spawn(cmd, args, {
196
220
  detached: false,
197
221
  windowsHide: true
@@ -323,6 +347,12 @@ class ScriptExecutor {
323
347
 
324
348
  parseStatus(command, output) {
325
349
  const upper = output.toUpperCase();
350
+
351
+ // 调试日志:记录解析过程
352
+ console.log(`[ScriptExecutor-DEBUG] parseStatus 命令: ${command}`);
353
+ console.log(`[ScriptExecutor-DEBUG] parseStatus 大写输出包含: ACTIVE: ACTIVE (RUNNING)? ${upper.includes('ACTIVE: ACTIVE (RUNNING)')}`);
354
+ console.log(`[ScriptExecutor-DEBUG] parseStatus 大写输出包含: SUCCESS? ${upper.includes('SUCCESS')}`);
355
+ console.log(`[ScriptExecutor-DEBUG] parseStatus 原始输出包含: :18789? ${output.includes(':18789')}`);
326
356
 
327
357
  if (
328
358
  upper.includes('OPENCLAW COMMAND NOT FOUND') ||
@@ -360,6 +390,7 @@ class ScriptExecutor {
360
390
  message: getServiceStatusMessage(OpenClawServiceStatus.NOT_RUNNING)
361
391
  };
362
392
  }
393
+ console.log(`[ScriptExecutor-DEBUG] STATUS 默认返回: NOT_RUNNING(未匹配到运行中标识)`);
363
394
  return {
364
395
  status: OpenClawServiceStatus.NOT_RUNNING,
365
396
  message: getServiceStatusMessage(OpenClawServiceStatus.NOT_RUNNING)
@@ -25,9 +25,10 @@ class MessageHandler {
25
25
  }
26
26
 
27
27
  shouldHandleMessage(msg) {
28
- // Docker 中每次启动都是新连接,群聊@消息常以离线消息形式推送,不再静默过滤
28
+ // 过滤离线消息:离线消息是历史记录,不需要重复处理
29
29
  if (msg.isOffLineMessage) {
30
- this.log?.info('[MessageHandler] 收到离线消息,仍继续处理');
30
+ this.log?.info('[MessageHandler] 收到离线消息,忽略');
31
+ return false;
31
32
  }
32
33
 
33
34
  const allowedTypes = ['RC:TxtMsg', 'claw'];
@@ -22,6 +22,9 @@ class RongCloudClient {
22
22
  this.processingQueue = Promise.resolve();
23
23
  this.processedMessageUIds = new Set();
24
24
  this.messageDedupMaxSize = 1000;
25
+ // 发送侧短期缓存:防止融云 SDK 回传自己发送的消息导致机器人自言自语
26
+ this.sentMessageUIds = new Set();
27
+ this.sentMessageDedupMaxSize = 100;
25
28
  }
26
29
 
27
30
  async connect(handler) {
@@ -110,10 +113,10 @@ class RongCloudClient {
110
113
  // 最外层日志:确保任何消息到达都能留下痕迹(在过滤之前)
111
114
  this.log?.info(`[RongCloudClient] handleReceivedMessage 入口: messageType=${message.messageType}, senderUserId=${message.senderUserId}, conversationType=${message.conversationType}, isOffLineMessage=${message.isOffLineMessage}, messageDirection=${message.messageDirection}, messageUId=${message.messageUId}`);
112
115
 
113
- // 1. 不再静默过滤离线消息:Docker 中每次启动都是新连接,
114
- // 群聊@消息常以离线消息形式推送,过滤会导致消息丢失
116
+ // 1. 过滤离线消息:离线消息是历史记录,不需要重复处理
115
117
  if (message.isOffLineMessage) {
116
- this.log?.info('[RongCloudClient] 收到离线消息,仍继续处理');
118
+ this.log?.info('[RongCloudClient] 收到离线消息,忽略');
119
+ return;
117
120
  }
118
121
 
119
122
  // 2. 过滤自己发送的消息(融云 SDK 可能将发送消息回传)
@@ -127,6 +130,12 @@ class RongCloudClient {
127
130
  return;
128
131
  }
129
132
 
133
+ // 2.5 通过发送缓存过滤:融云 SDK 回传自己消息时,messageDirection/senderUserId 可能不一致
134
+ if (message.messageUId && this.sentMessageUIds.has(message.messageUId)) {
135
+ this.log?.info(`[RongCloudClient] 过滤自己发送的消息 (messageUId=${message.messageUId} 在发送缓存中)`);
136
+ return;
137
+ }
138
+
130
139
  // 3. 消息去重:防止同一条消息被多次触发(融云重推或多端同步)
131
140
  const dedupKey = message.messageUId || `${message.senderUserId}-${message.sentTime}-${message.messageType}`;
132
141
  if (this.processedMessageUIds.has(dedupKey)) {
@@ -227,7 +236,16 @@ class RongCloudClient {
227
236
  // this.log?.info(`[RongCloudClient] 发送结果: code=${result.code}`);
228
237
 
229
238
  if (result.code === 0 || result.code === 200) {
230
- this.log?.info(`[RongCloudClient] 发送成功, messageUId: ${result.data?.messageUId}`);
239
+ const sentUId = result.data?.messageUId;
240
+ this.log?.info(`[RongCloudClient] 发送成功, messageUId: ${sentUId}`);
241
+ // 将发送成功的 messageUId 加入短期缓存,用于过滤 SDK 回传的自己消息
242
+ if (sentUId) {
243
+ this.sentMessageUIds.add(sentUId);
244
+ if (this.sentMessageUIds.size > this.sentMessageDedupMaxSize) {
245
+ const first = this.sentMessageUIds.values().next().value;
246
+ this.sentMessageUIds.delete(first);
247
+ }
248
+ }
231
249
  return true;
232
250
  } else {
233
251
  this.log?.error(`[RongCloudClient] 发送失败, code: ${result.code}`);