claw-subagent-service 0.0.133 → 0.0.135

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.
@@ -246,14 +246,53 @@ start_docker() {
246
246
  cat /tmp/openclaw-run-help.txt | head -30
247
247
  fi
248
248
 
249
+ # 先停止所有已有的 openclaw 进程,避免端口冲突
250
+ log_info "检查并停止已有的 openclaw 进程..."
251
+ local existing_pids=""
252
+ if command -v pgrep &>/dev/null; then
253
+ existing_pids=$(pgrep -f "openclaw" | tr '\n' ' ')
254
+ elif command -v pidof &>/dev/null; then
255
+ existing_pids=$(pidof openclaw)
256
+ else
257
+ existing_pids=$(ps aux | grep -v grep | grep "openclaw" | awk '{print $2}' | tr '\n' ' ')
258
+ fi
259
+
260
+ if [ -n "$existing_pids" ]; then
261
+ log_warn "发现已有 openclaw 进程: $existing_pids,先停止它们..."
262
+ for ep in $existing_pids; do
263
+ log_info "停止进程 $ep..."
264
+ kill -15 "$ep" 2>/dev/null || true
265
+ done
266
+ # 等待 3 秒让进程优雅退出
267
+ sleep 3
268
+ # 检查是否还有残留进程,如果有则强制停止
269
+ local remaining_pids=""
270
+ if command -v pgrep &>/dev/null; then
271
+ remaining_pids=$(pgrep -f "openclaw" | tr '\n' ' ')
272
+ fi
273
+ if [ -n "$remaining_pids" ]; then
274
+ log_warn "强制停止残留进程: $remaining_pids"
275
+ for rp in $remaining_pids; do
276
+ kill -9 "$rp" 2>/dev/null || true
277
+ done
278
+ sleep 1
279
+ fi
280
+ fi
281
+
282
+ # 确保端口未被占用
283
+ if command -v fuser &>/dev/null; then
284
+ fuser -k "${PORT}/tcp" 2>/dev/null || true
285
+ fi
286
+
249
287
  # 尝试使用正确的参数启动
250
288
  # 注意:openclaw gateway 可能使用不同的参数名
251
- # 在 Docker 环境中,使用 run 子命令前台运行,而不是后台启动
252
289
  log_info "尝试启动: openclaw gateway run --port $PORT"
290
+
291
+ # 使用 nohup 后台启动,将输出重定向到日志文件
253
292
  nohup openclaw gateway run --port "$PORT" > "$log_file" 2>&1 &
254
293
 
255
- # 等待 5 秒检查进程是否启动(给更多时间)
256
- sleep 5
294
+ # 等待 15 秒检查进程是否启动(给更多时间初始化)
295
+ sleep 15
257
296
  local started_pid=$!
258
297
  log_info "启动的进程 PID: $started_pid"
259
298
 
@@ -272,7 +311,7 @@ start_docker() {
272
311
 
273
312
  # 检查进程监听的端口
274
313
  log_info "检查进程监听的端口..."
275
- sleep 2
314
+ sleep 5
276
315
  local listening_ports=$(netstat -tlnp 2>/dev/null | grep "$started_pid" || ss -tlnp 2>/dev/null | grep "$started_pid" || echo "")
277
316
  if [ -n "$listening_ports" ]; then
278
317
  log_info "进程 $started_pid 监听的端口:"
@@ -460,6 +460,12 @@ show_help() {
460
460
  main() {
461
461
  local force=""
462
462
 
463
+ # 检查 FORCE 环境变量(来自 script-executor 的强制停止模式)
464
+ if [ "${FORCE:-}" = "1" ]; then
465
+ force="1"
466
+ log_info "检测到 FORCE=1 环境变量,启用强制停止模式"
467
+ fi
468
+
463
469
  # 解析命令行参数
464
470
  while [ $# -gt 0 ]; do
465
471
  case $1 in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-subagent-service",
3
- "version": "0.0.133",
3
+ "version": "0.0.135",
4
4
  "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/service/daemon.js CHANGED
@@ -28,6 +28,31 @@ let crashCount = 0;
28
28
  let lastCrashTime = 0;
29
29
  const updater = new Updater();
30
30
 
31
+ // 检测是否是 npm 更新后的重启
32
+ const VERSION_FILE = path.join(os.tmpdir(), '.claw-subagent-service.version');
33
+ function detectUpdateRestart() {
34
+ try {
35
+ const currentVersion = updater.loadCurrentVersion();
36
+ let previousVersion = null;
37
+
38
+ if (fs.existsSync(VERSION_FILE)) {
39
+ previousVersion = fs.readFileSync(VERSION_FILE, 'utf8').trim();
40
+ }
41
+
42
+ // 写入当前版本
43
+ fs.writeFileSync(VERSION_FILE, currentVersion);
44
+
45
+ // 如果之前有版本记录,且当前版本不同,说明是更新后的重启
46
+ if (previousVersion && previousVersion !== currentVersion) {
47
+ log.info(`[DAEMON] 检测到版本变化: ${previousVersion} → ${currentVersion},这是更新后的重启`);
48
+ return true;
49
+ }
50
+ } catch (err) {
51
+ log.warn(`[DAEMON] 检测版本变化失败: ${err.message}`);
52
+ }
53
+ return false;
54
+ }
55
+
31
56
  process.chdir(__dirname);
32
57
 
33
58
  /**
@@ -359,7 +384,14 @@ process.on('message', (msg) => {
359
384
  if (msg === 'shutdown') gracefulShutdown();
360
385
  });
361
386
 
362
- startWorker(false, null);
387
+ // 检测是否是 npm 更新后的重启
388
+ const isAfterNpmUpdate = detectUpdateRestart();
389
+ if (isAfterNpmUpdate) {
390
+ log.info('[DAEMON] npm 更新后的重启,启动 Worker 并设置健康观察...');
391
+ startWorker(true, null);
392
+ } else {
393
+ startWorker(false, null);
394
+ }
363
395
  updater.startSchedule(restartWorkerWithUpdate);
364
396
 
365
397
  process.on('uncaughtException', (err) => {
@@ -112,14 +112,16 @@ async function manageWithServiceManager(command) {
112
112
 
113
113
  /**
114
114
  * 使用 ScriptExecutor 执行脚本(Docker 模式)
115
+ * @param {number} command - 命令代码
116
+ * @param {boolean} force - 是否强制停止
115
117
  * @returns {Object} { result, output }
116
118
  */
117
- async function executeWithScript(command) {
119
+ async function executeWithScript(command, force = false) {
118
120
  const executor = getExecutor();
119
121
  const scriptName = getScriptName(command);
120
122
 
121
123
  try {
122
- const result = await executor.executeWithStatus(command, scriptName);
124
+ const result = await executor.executeWithStatus(command, scriptName, force);
123
125
  // ScriptExecutor 现在返回 { status, message, output }
124
126
  return {
125
127
  result: { status: result.status, message: result.message },
@@ -200,10 +202,11 @@ async function verifyCommandResult(command, result, scriptOutput = '') {
200
202
  return result;
201
203
  }
202
204
 
203
- async function executeCommand(command, window, sendResponse) {
205
+ async function executeCommand(command, window, sendResponse, force = false) {
204
206
  const cmdName = getCommandName(command);
205
- console.log(`[OpenClawControl] ====== 开始执行 ${cmdName} 命令 ======`);
206
- console.log(`[OpenClawControl] 命令代码: ${command}`);
207
+ const actionName = force ? '强制' + cmdName : cmdName;
208
+ console.log(`[OpenClawControl] ====== 开始执行 ${actionName} 命令 ======`);
209
+ console.log(`[OpenClawControl] 命令代码: ${command}, force: ${force}`);
207
210
  console.log(`[OpenClawControl] 平台: ${process.platform}`);
208
211
 
209
212
  let result;
@@ -223,7 +226,7 @@ async function executeCommand(command, window, sendResponse) {
223
226
  let scriptOutput = '';
224
227
  if (!result) {
225
228
  console.log(`[OpenClawControl] 使用脚本方式执行...`);
226
- const scriptResult = await executeWithScript(command);
229
+ const scriptResult = await executeWithScript(command, force);
227
230
  result = scriptResult.result;
228
231
  scriptOutput = scriptResult.output;
229
232
  console.log(`[OpenClawControl] 脚本执行结果: ${result.status} - ${result.message}`);
@@ -242,13 +245,13 @@ async function executeCommand(command, window, sendResponse) {
242
245
  result.status === OpenClawServiceStatus.STOP_SUCCESS ||
243
246
  result.status === OpenClawServiceStatus.RESTART_SUCCESS ||
244
247
  result.status === OpenClawServiceStatus.CONFIG_FIX_SUCCESS) {
245
- console.log(`[OpenClawControl] ${cmdName} 成功: ${result.message}`);
248
+ console.log(`[OpenClawControl] ${actionName} 成功: ${result.message}`);
246
249
  } else if (result.status === OpenClawServiceStatus.ERROR) {
247
- console.log(`[OpenClawControl] ${cmdName} 失败: ${result.message}`);
250
+ console.log(`[OpenClawControl] ${actionName} 失败: ${result.message}`);
248
251
  } else if (result.status === OpenClawServiceStatus.NOT_INSTALL) {
249
- console.log(`[OpenClawControl] ${cmdName}: OpenClaw未安装`);
252
+ console.log(`[OpenClawControl] ${actionName}: OpenClaw未安装`);
250
253
  } else {
251
- console.log(`[OpenClawControl] ${cmdName} 状态: ${result.message}`);
254
+ console.log(`[OpenClawControl] ${actionName} 状态: ${result.message}`);
252
255
  }
253
256
 
254
257
  if (sendResponse) {
@@ -264,16 +267,19 @@ async function executeCommand(command, window, sendResponse) {
264
267
  const { getOpenClawStatus } = require('./port-checker');
265
268
  const portStatus = await getOpenClawStatus(18789);
266
269
  console.log(`[OpenClawControl] 端口状态: ${portStatus}`);
267
- // 更新真实状态
270
+ // 更新真实状态 - 始终以端口检测结果为准
268
271
  if (command === OpenClawCommandEnum.STOP) {
269
272
  realStatus = portStatus === 1 ? OpenClawServiceStatus.RUNNING : OpenClawServiceStatus.STOP_SUCCESS;
270
273
  realMessage = portStatus === 1 ? '停止失败: 服务仍在运行' : '服务已停止';
274
+ if (portStatus === 1) httpStatus = 'error';
271
275
  } else if (command === OpenClawCommandEnum.START) {
272
276
  realStatus = portStatus === 1 ? OpenClawServiceStatus.START_SUCCESS : OpenClawServiceStatus.ERROR;
273
277
  realMessage = portStatus === 1 ? '服务已启动' : '启动失败: 服务未运行';
278
+ if (portStatus !== 1) httpStatus = 'error';
274
279
  } else if (command === OpenClawCommandEnum.RESTART) {
275
280
  realStatus = portStatus === 1 ? OpenClawServiceStatus.RESTART_SUCCESS : OpenClawServiceStatus.ERROR;
276
281
  realMessage = portStatus === 1 ? '服务已重启' : '重启失败: 服务未运行';
282
+ if (portStatus !== 1) httpStatus = 'error';
277
283
  }
278
284
  console.log(`[OpenClawControl] 操作后真实状态: portStatus=${portStatus}, realStatus=${realStatus}, message=${realMessage}`);
279
285
  } catch (e) {
@@ -64,27 +64,40 @@ class RongyunMessageHandler {
64
64
  return;
65
65
  }
66
66
 
67
- const msgType = parsed.msg_type;
67
+ // 解析 content 字段(前端将业务数据放在 content 内)
68
+ let data = parsed;
69
+ if (parsed.content && typeof parsed.content === 'string') {
70
+ try {
71
+ const contentData = JSON.parse(parsed.content);
72
+ // 将 content 内的数据合并到顶层,方便处理器直接访问
73
+ // 保留原始 content 字段(用于聊天消息等场景)
74
+ data = { ...parsed, ...contentData, _raw_content: parsed.content };
75
+ } catch (e) {
76
+ this.logWarn(`解析 content 失败: ${e.message}`);
77
+ }
78
+ }
79
+
80
+ const msgType = data.msg_type;
68
81
  this.logInfo(`[RongyunMessageHandler] 处理消息类型: ${msgType}`);
69
82
 
70
83
  switch (msgType) {
71
84
  case RongyunMessageTypeEnum.COMMAND:
72
- await this.handleCommand(parsed);
85
+ await this.handleCommand(data);
73
86
  break;
74
87
  case RongyunMessageTypeEnum.CHAT_MESSAGE:
75
- await this.handleChatMessage(parsed);
88
+ await this.handleChatMessage(data);
76
89
  break;
77
90
  case RongyunMessageTypeEnum.CREATE_OPENCODE_SESSION:
78
- await this.handleCreateSession(parsed);
91
+ await this.handleCreateSession(data);
79
92
  break;
80
93
  case RongyunMessageTypeEnum.DELETE_OPENCODE_SESSION:
81
- await this.handleDeleteSession(parsed);
94
+ await this.handleDeleteSession(data);
82
95
  break;
83
96
  case RongyunMessageTypeEnum.DEVICE_CONTROL:
84
- await this.handleDeviceControl(parsed);
97
+ await this.handleDeviceControl(data);
85
98
  break;
86
99
  case RongyunMessageTypeEnum.DEVICE_STATUS_REQUEST:
87
- await this.handleDeviceStatusRequest(parsed);
100
+ await this.handleDeviceStatusRequest(data);
88
101
  break;
89
102
  default:
90
103
  this.logWarn(`未处理的消息类型: ${msgType}`);
@@ -100,8 +113,9 @@ class RongyunMessageHandler {
100
113
  const commandId = data.command_id;
101
114
  const requestId = data.request_id;
102
115
  const sourceId = data.source_im_id;
116
+ const force = data.force === true; // 是否强制停止
103
117
 
104
- this.logInfo(`[RongyunMessageHandler] 收到命令: command=${command}, command_id=${commandId}, from=${sourceId || 'guardserver'}`);
118
+ this.logInfo(`[RongyunMessageHandler] 收到命令: command=${command}, command_id=${commandId}, force=${force}, from=${sourceId || 'guardserver'}`);
105
119
 
106
120
  // 验证命令是否有效
107
121
  const validCommands = Object.values(OpenClawCommandEnum);
@@ -143,11 +157,12 @@ class RongyunMessageHandler {
143
157
 
144
158
  if (isAsyncCommand) {
145
159
  // 立即响应,告知前端命令已接收
160
+ const actionName = force ? '强制' + getCommandName(command) : getCommandName(command);
146
161
  await this.sendResponse(RongyunMessageTypeEnum.COMMAND_RESULT, {
147
162
  command,
148
163
  command_id: commandId,
149
164
  status: 'success',
150
- message: `${getCommandName(command)}命令已接收,正在执行...`
165
+ message: `${actionName}命令已接收,正在执行...`
151
166
  }, requestId, sourceId);
152
167
  }
153
168
 
@@ -163,8 +178,8 @@ class RongyunMessageHandler {
163
178
  }
164
179
  // 异步命令不在这里响应,因为已经提前响应了
165
180
  // 但我们可以在这里记录执行结果
166
- this.logInfo(`[RongyunMessageHandler] 命令执行完成: command=${command}, status=${response.status}, message=${response.message}`);
167
- }).then(() => {
181
+ this.logInfo(`[RongyunMessageHandler] 命令执行完成: command=${command}, force=${force}, status=${response.status}, message=${response.message}`);
182
+ }, force).then(() => {
168
183
  // 命令执行完成后,立即释放锁
169
184
  this.commandLock = false;
170
185
  if (this.commandLockTimer) {
@@ -206,7 +221,8 @@ class RongyunMessageHandler {
206
221
  async handleChatMessage(data) {
207
222
  const roomId = data.room_id;
208
223
  const sessionId = data.gateway_session_id || data.session_id;
209
- const content = data.content;
224
+ // 使用解析后的 content(聊天内容),如果没有则使用原始 content
225
+ const content = data.content || data._raw_content;
210
226
  const requestId = data.request_id;
211
227
 
212
228
  this.logInfo(`[RongyunMessageHandler] 收到聊天消息, roomId=${roomId}, sessionId=${sessionId}`);
@@ -79,7 +79,7 @@ class ScriptExecutor {
79
79
  }
80
80
  }
81
81
 
82
- async executeWithStatus(command, scriptName) {
82
+ async executeWithStatus(command, scriptName, force = false) {
83
83
  // 配置修复命令特殊处理:直接执行命令,不需要脚本
84
84
  if (command === OpenClawCommandEnum.CONFIG_FIX) {
85
85
  return await this.executeCommandDirect('openclaw doctor --fix', 120);
@@ -88,8 +88,8 @@ class ScriptExecutor {
88
88
  const scriptPath = path.join(this.scriptDir, scriptName);
89
89
 
90
90
  try {
91
- console.log(`[ScriptExecutor] 开始执行脚本: ${scriptPath}`);
92
- const { stdout, stderr, exitCode } = await this.runScript(scriptPath);
91
+ console.log(`[ScriptExecutor] 开始执行脚本: ${scriptPath}, force=${force}`);
92
+ const { stdout, stderr, exitCode } = await this.runScript(scriptPath, force);
93
93
  const fullOutput = stdout + stderr;
94
94
 
95
95
  // 调试日志:记录脚本实际输出
@@ -206,7 +206,7 @@ class ScriptExecutor {
206
206
  });
207
207
  }
208
208
 
209
- runScript(scriptPath) {
209
+ runScript(scriptPath, force = false) {
210
210
  // Linux/Mac 系统:自动修复 shell 脚本的 CRLF 换行符
211
211
  if (process.platform !== 'win32') {
212
212
  try {
@@ -233,6 +233,12 @@ class ScriptExecutor {
233
233
  cmd = 'bash';
234
234
  args = [scriptPath];
235
235
  }
236
+
237
+ // 如果是强制停止,添加 FORCE 环境变量
238
+ if (force) {
239
+ args.unshift('FORCE=1');
240
+ console.log(`[ScriptExecutor] 强制停止模式: FORCE=1`);
241
+ }
236
242
 
237
243
  // 调试日志:记录执行环境
238
244
  if (process.platform !== 'win32') {