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/README.md +411 -60
- package/cli.js +16 -1
- package/command/linux/restart.sh +138 -21
- package/command/linux/start.sh +117 -24
- package/command/linux/status.sh +172 -43
- package/command/linux/stop.sh +143 -36
- package/package.json +1 -1
- package/scripts/post-install.js +15 -0
- package/service/daemon/clawsubagentservice.exe +0 -0
- package/service/daemon/clawsubagentservice.exe.config +6 -0
- package/service/daemon/clawsubagentservice.xml +33 -0
- package/service/modules/opencode-service.js +139 -66
- package/service/worker.js +94 -22
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
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
426
|
-
log.info(`[WORKER] HTTP 服务已启动: http
|
|
497
|
+
server.listen(PORT, HOST, () => {
|
|
498
|
+
log.info(`[WORKER] HTTP 服务已启动: http://${HOST}:${PORT}/health`);
|
|
427
499
|
});
|
|
428
500
|
|
|
429
501
|
process.on('message', (msg) => {
|