claw-subagent-service 0.0.37 → 0.0.39

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/service/worker.js CHANGED
@@ -3,6 +3,7 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
5
  const { execSync } = require('child_process');
6
+ const axios = require('axios');
6
7
  const { createLogger } = require('./logger');
7
8
  const { RongCloudClient, MessageHandler, ensurePluginsAllow } = require('./rongcloud');
8
9
  const { RongyunMessageHandler } = require('./modules/rongyun-message-handler');
@@ -14,6 +15,7 @@ const { startOpencodeService, stopOpencodeService } = require('./modules/opencod
14
15
 
15
16
  const log = createLogger('worker');
16
17
  const PORT = process.env.SILENT_SERVICE_PORT ? parseInt(process.env.SILENT_SERVICE_PORT, 10) : 28765;
18
+ const HOST = process.env.SILENT_SERVICE_HOST || '127.0.0.1';
17
19
 
18
20
 
19
21
  // 捕获所有异常,强制打印到控制台(绕过 logger)
@@ -53,9 +55,20 @@ function findPidOnPort(port) {
53
55
  }
54
56
  }
55
57
  } else {
56
- const out = execSync(`lsof -i :${port} -t 2>/dev/null`, { encoding: 'utf8', timeout: 5000 });
57
- const pid = parseInt(out.trim(), 10);
58
- if (!isNaN(pid)) return pid;
58
+ // 优先尝试 lsof,再兜底 ss / fuser / netstat(适配精简 Docker 镜像)
59
+ const commands = [
60
+ `lsof -i :${port} -t 2>/dev/null`,
61
+ `fuser ${port}/tcp 2>/dev/null`,
62
+ `ss -tlnp 2>/dev/null | grep ":${port}" | sed -n 's/.*pid=\\([0-9]*\\).*/\\1/p'`,
63
+ `netstat -tlnp 2>/dev/null | grep ":${port}" | sed -n 's/.*\\/\\([0-9]*\\).*/\\1/p'`,
64
+ ];
65
+ for (const cmd of commands) {
66
+ try {
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;
70
+ } catch { continue; }
71
+ }
59
72
  }
60
73
  } catch { /* port is free */ }
61
74
  return null;
@@ -198,6 +211,53 @@ rongcloudConfig = loadRongCloudConfig();
198
211
  let rongcloudClient = null;
199
212
  let messageHandler = null;
200
213
 
214
+ /**
215
+ * 向服务端刷新融云 token
216
+ */
217
+ async function refreshRongCloudToken() {
218
+ const nodeId = rongcloudConfig?.accountId;
219
+ if (!nodeId) {
220
+ log.error('[WORKER] 无法刷新 token: 缺少 nodeId');
221
+ return false;
222
+ }
223
+
224
+ const serverUrl = process.env.DM_SERVER_URL || 'https://newsradar.dreamdt.cn/im';
225
+ try {
226
+ log.info(`[WORKER] 正在向服务端刷新 token, nodeId=${nodeId}`);
227
+ const resp = await axios.get(`${serverUrl}/api/claw/token/${nodeId}`, { timeout: 15000 });
228
+ if (resp.data?.code === 200) {
229
+ const newToken = resp.data.data?.token || resp.data.token || '';
230
+ if (!newToken) {
231
+ log.error('[WORKER] 服务端返回了空 token');
232
+ return false;
233
+ }
234
+ log.info('[WORKER] token 刷新成功');
235
+
236
+ // 更新内存配置
237
+ rongcloudConfig.token = newToken;
238
+
239
+ // 保存到 config.json
240
+ try {
241
+ if (fs.existsSync(clawBridgeConfigPath)) {
242
+ const clawConfig = JSON.parse(fs.readFileSync(clawBridgeConfigPath, 'utf8'));
243
+ clawConfig.token = newToken;
244
+ clawConfig.expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7天
245
+ fs.writeFileSync(clawBridgeConfigPath, JSON.stringify(clawConfig, null, 2));
246
+ log.info('[WORKER] 新 token 已保存到 config.json');
247
+ }
248
+ } catch (err) {
249
+ log.error(`[WORKER] 保存新 token 失败: ${err.message}`);
250
+ }
251
+ return true;
252
+ }
253
+ log.error(`[WORKER] 刷新 token 失败: ${resp.data?.message || '未知错误'}`);
254
+ return false;
255
+ } catch (err) {
256
+ log.error(`[WORKER] 刷新 token 异常: ${err.message}`);
257
+ return false;
258
+ }
259
+ }
260
+
201
261
  async function initRongCloud() {
202
262
  if (!rongcloudConfig) return;
203
263
 
@@ -233,22 +293,22 @@ async function initRongCloud() {
233
293
 
234
294
  // 包装 MessageHandler.handleMessage 以处理结构化消息
235
295
  const originalHandleMessage = messageHandler.handleMessage.bind(messageHandler);
236
-
296
+
237
297
  messageHandler.handleMessage = async (msg) => {
238
298
  // 检查是否是结构化消息
239
299
  if (msg.content && typeof msg.content === 'string') {
240
300
  try {
241
301
  const parsed = JSON.parse(msg.content);
242
-
302
+
243
303
  if (parsed.msg_type) {
244
304
  // 这是结构化消息,使用 RongyunMessageHandler 处理
245
305
  log.info(`[WORKER] 收到结构化消息: type=${parsed.msg_type}, from=${parsed.source_im_id || msg.senderUserId}`);
246
-
306
+
247
307
  // 忽略自己发送的消息
248
308
  if (parsed.source_im_id === rongcloudConfig.accountId) {
249
309
  return;
250
310
  }
251
-
311
+
252
312
  // Timestamp 校验(5分钟有效期)
253
313
  const msgTimestamp = parsed.timestamp;
254
314
  if (msgTimestamp) {
@@ -262,7 +322,7 @@ async function initRongCloud() {
262
322
  return;
263
323
  }
264
324
  }
265
-
325
+
266
326
  // 解析 content 字段(它本身可能是 JSON 字符串)
267
327
  let innerContent = parsed.content;
268
328
  if (typeof innerContent === 'string') {
@@ -272,7 +332,7 @@ async function initRongCloud() {
272
332
  // 保持字符串
273
333
  }
274
334
  }
275
-
335
+
276
336
  // 构建消息数据
277
337
  // 注意:后端发送的 command 消息中,command/command_id/request_id 在 content 字段内
278
338
  // 保留原始 content(用户消息内容),同时展开其他字段
@@ -284,7 +344,7 @@ async function initRongCloud() {
284
344
  targetId: msg.targetId,
285
345
  conversationType: msg.conversationType,
286
346
  };
287
-
347
+
288
348
  // 使用 RongyunMessageHandler 处理
289
349
  try {
290
350
  await rongyunMessageHandler.handle(messageData);
@@ -297,18 +357,30 @@ async function initRongCloud() {
297
357
  // 不是 JSON,是普通消息,继续传给原始 handler
298
358
  }
299
359
  }
300
-
360
+
301
361
  // 调用原始的 handleMessage(处理普通消息)
302
362
  return originalHandleMessage(msg);
303
363
  };
304
-
364
+
305
365
  // 添加调试日志:确认替换后的方法
306
366
  log.info('[WORKER-DEBUG] 替换后 messageHandler.handleMessage 类型: ' + typeof messageHandler.handleMessage);
307
367
 
308
- const connected = await rongcloudClient.connect(messageHandler);
368
+ let connected = await rongcloudClient.connect(messageHandler);
369
+
370
+ // 连接失败时尝试刷新 token 并重试一次
371
+ if (!connected) {
372
+ log.warn('[WORKER] 首次融云连接失败,尝试刷新 token...');
373
+ const refreshed = await refreshRongCloudToken();
374
+ if (refreshed) {
375
+ // 使用新 token 重新创建客户端并连接
376
+ rongcloudClient = new RongCloudClient(rongcloudConfig, log);
377
+ connected = await rongcloudClient.connect(messageHandler);
378
+ }
379
+ }
380
+
309
381
  if (connected) {
310
382
  log.info('[WORKER] 融云连接成功');
311
-
383
+
312
384
  // 发送 CLIENT_CONNECTED
313
385
  try {
314
386
  await messageSender.sendClientConnected();
@@ -316,21 +388,21 @@ async function initRongCloud() {
316
388
  } catch (err) {
317
389
  log.error(`[WORKER] 发送 CLIENT_CONNECTED 失败: ${err.message}`);
318
390
  }
319
-
391
+
320
392
  // 启动心跳管理器
321
393
  const heartbeatManager = new HeartbeatManager(rongcloudClient, rongcloudConfig, log);
322
394
  heartbeatManager.start(getMacAddress, getOpenClawStatus);
323
-
395
+
324
396
  // 启动仪表盘上报
325
397
  const dashboardReporter = new DashboardReporter(rongcloudClient, rongcloudConfig, log);
326
398
  dashboardReporter.start(getMacAddress);
327
-
399
+
328
400
  // 保存引用以便关闭时停止
329
401
  global.heartbeatManager = heartbeatManager;
330
402
  global.dashboardReporter = dashboardReporter;
331
-
403
+
332
404
  } else {
333
- log.error('[WORKER] 融云连接失败');
405
+ log.error('[WORKER] 融云连接失败,token 刷新后仍无法连接');
334
406
  }
335
407
  }
336
408
 
@@ -415,15 +487,15 @@ server.on('error', (err) => {
415
487
  setTimeout(() => {
416
488
  log.info(`[WORKER] 重新尝试监听端口 ${PORT}...`);
417
489
  server.close(() => {});
418
- server.listen(PORT, '127.0.0.1');
490
+ server.listen(PORT, HOST);
419
491
  }, 2000);
420
492
  return;
421
493
  }
422
494
  log.error(`[WORKER] HTTP 服务错误: ${err.message}`);
423
495
  });
424
496
 
425
- server.listen(PORT, '127.0.0.1', () => {
426
- log.info(`[WORKER] HTTP 服务已启动: http://127.0.0.1:${PORT}/health`);
497
+ server.listen(PORT, HOST, () => {
498
+ log.info(`[WORKER] HTTP 服务已启动: http://${HOST}:${PORT}/health`);
427
499
  });
428
500
 
429
501
  process.on('message', (msg) => {