claw-subagent-service 0.0.0
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 +44 -0
- package/cli.js +254 -0
- package/command/linux/restart.sh +98 -0
- package/command/linux/start.sh +101 -0
- package/command/linux/status.sh +140 -0
- package/command/linux/stop.sh +112 -0
- package/command/win/restart.bat +39 -0
- package/command/win/start.bat +65 -0
- package/command/win/status.bat +52 -0
- package/command/win/stop.bat +55 -0
- package/command/win/windows/345/220/257/345/212/250/350/204/232/346/234/254 +0 -0
- package/package.json +37 -0
- package/scripts/install-silent.js +167 -0
- package/scripts/uninstall.js +61 -0
- package/service/daemon.js +189 -0
- package/service/logger.js +31 -0
- package/service/modules/auth.js +17 -0
- package/service/modules/business-message-handler.js +118 -0
- package/service/modules/command-handler.js +152 -0
- package/service/modules/config.js +44 -0
- package/service/modules/dashboard-collector.js +588 -0
- package/service/modules/heartbeat-dashboard.js +153 -0
- package/service/modules/mac-address.js +15 -0
- package/service/modules/message-processor-example.js +72 -0
- package/service/modules/message-processor.js +62 -0
- package/service/modules/normal-message-handler.js +60 -0
- package/service/modules/openclaw-control.js +128 -0
- package/service/modules/openclaw-enum.js +48 -0
- package/service/modules/opencode-service.js +199 -0
- package/service/modules/opencode-starter.js +194 -0
- package/service/modules/port-checker.js +31 -0
- package/service/modules/rongyun-message-handler.js +250 -0
- package/service/modules/rongyun-message-sender.js +157 -0
- package/service/modules/rongyun-message-types.js +28 -0
- package/service/modules/script-executor.js +550 -0
- package/service/modules/service-manager.js +319 -0
- package/service/modules/structured-message-router.js +118 -0
- package/service/rongcloud/env-polyfill.js +95 -0
- package/service/rongcloud/index.js +19 -0
- package/service/rongcloud/message-handler.js +147 -0
- package/service/rongcloud/message-types.js +22 -0
- package/service/rongcloud/openclaw-client.js +98 -0
- package/service/rongcloud/openclaw-config.js +108 -0
- package/service/rongcloud/rongcloud-client.js +273 -0
- package/service/rongcloud/types.js +9 -0
- package/service/updater.js +348 -0
- package/service/worker.js +376 -0
- package/version.json +4 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { createLogger } = require('./logger');
|
|
6
|
+
const { RongCloudClient, MessageHandler, ensurePluginsAllow } = require('./rongcloud');
|
|
7
|
+
const { RongyunMessageHandler } = require('./modules/rongyun-message-handler');
|
|
8
|
+
const { RongyunMessageSender } = require('./modules/rongyun-message-sender');
|
|
9
|
+
const { HeartbeatManager, DashboardReporter } = require('./modules/heartbeat-dashboard');
|
|
10
|
+
const { getOpenClawStatus } = require('./modules/port-checker');
|
|
11
|
+
const { getMacAddress } = require('./modules/mac-address');
|
|
12
|
+
const { startOpencodeService, stopOpencodeService } = require('./modules/opencode-starter');
|
|
13
|
+
|
|
14
|
+
const log = createLogger('worker');
|
|
15
|
+
const PORT = process.env.SILENT_SERVICE_PORT ? parseInt(process.env.SILENT_SERVICE_PORT, 10) : 38765;
|
|
16
|
+
|
|
17
|
+
// Timestamp 校验专用日志
|
|
18
|
+
const timestampLogPath = path.join(__dirname, '..', 'logs', 'timestamp-validation.log');
|
|
19
|
+
const logTimestampValidation = (message) => {
|
|
20
|
+
const line = `[${new Date().toISOString()}] [WARN] ${message}\n`;
|
|
21
|
+
try {
|
|
22
|
+
fs.appendFileSync(timestampLogPath, line);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// 忽略写入错误
|
|
25
|
+
}
|
|
26
|
+
log.warn(`[TIMESTAMP-VALIDATION] ${message}`); // 同时记录到主日志
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
log.info(`[WORKER] 业务进程启动,PID: ${process.pid}`);
|
|
30
|
+
|
|
31
|
+
const clawBridgeConfigPath = path.join(os.homedir(), '.claw-bridge', 'config.json');
|
|
32
|
+
const localConfigPath = path.join(__dirname, '..', 'rongcloud-config.json');
|
|
33
|
+
let rongcloudConfig = null;
|
|
34
|
+
|
|
35
|
+
function loadRongCloudConfig() {
|
|
36
|
+
let config = {};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (fs.existsSync(clawBridgeConfigPath)) {
|
|
40
|
+
const clawConfig = JSON.parse(fs.readFileSync(clawBridgeConfigPath, 'utf8'));
|
|
41
|
+
config.token = clawConfig.token;
|
|
42
|
+
config.accountId = clawConfig.nodeId;
|
|
43
|
+
config.nodeName = clawConfig.nodeName;
|
|
44
|
+
log.info(`[WORKER] 从 claw-bridge 加载配置: nodeId=${clawConfig.nodeId}, nodeName=${clawConfig.nodeName}`);
|
|
45
|
+
} else {
|
|
46
|
+
log.warn('[WORKER] 未找到 ~/.claw-bridge/config.json');
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
log.error(`[WORKER] 加载 claw-bridge 配置失败: ${err.message}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
if (fs.existsSync(localConfigPath)) {
|
|
54
|
+
const localConfig = JSON.parse(fs.readFileSync(localConfigPath, 'utf8'));
|
|
55
|
+
config.appKey = localConfig.appKey || config.appKey;
|
|
56
|
+
if (localConfig.token) config.token = localConfig.token;
|
|
57
|
+
if (localConfig.accountId) config.accountId = localConfig.accountId;
|
|
58
|
+
log.info(`[WORKER] 从本地配置加载: appKey=${config.appKey?.substring(0, 8)}...`);
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
log.error(`[WORKER] 加载本地配置失败: ${err.message}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!config.appKey) {
|
|
65
|
+
config.appKey = process.env.DM_APP_KEY || 'bmdehs6pbyyks';
|
|
66
|
+
log.info(`[WORKER] 使用默认 appKey: ${config.appKey}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 设置默认心跳间隔为20秒
|
|
70
|
+
if (!config.heartbeatInterval) {
|
|
71
|
+
config.heartbeatInterval = 20;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (config.token && config.accountId) {
|
|
75
|
+
return config;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
log.warn('[WORKER] 缺少必要配置(token/accountId),融云功能未启用');
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
rongcloudConfig = loadRongCloudConfig();
|
|
83
|
+
|
|
84
|
+
let rongcloudClient = null;
|
|
85
|
+
let messageHandler = null;
|
|
86
|
+
|
|
87
|
+
async function initRongCloud() {
|
|
88
|
+
if (!rongcloudConfig) return;
|
|
89
|
+
|
|
90
|
+
// 启动 opencode 服务(与桌面客户端对齐)
|
|
91
|
+
log.info('[WORKER] 启动 opencode 服务...');
|
|
92
|
+
try {
|
|
93
|
+
await startOpencodeService(log);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
log.error(`[WORKER] 启动 opencode 服务失败: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await ensurePluginsAllow(log);
|
|
99
|
+
|
|
100
|
+
rongcloudClient = new RongCloudClient(rongcloudConfig, log);
|
|
101
|
+
|
|
102
|
+
// 创建消息发送器
|
|
103
|
+
const messageSender = new RongyunMessageSender(rongcloudClient, rongcloudConfig, log);
|
|
104
|
+
|
|
105
|
+
// 创建新的融云消息处理器(与桌面客户端对齐)
|
|
106
|
+
const rongyunMessageHandler = new RongyunMessageHandler(rongcloudClient, rongcloudConfig, log);
|
|
107
|
+
rongyunMessageHandler.setMessageSender(messageSender);
|
|
108
|
+
|
|
109
|
+
messageHandler = new MessageHandler(
|
|
110
|
+
rongcloudConfig,
|
|
111
|
+
async (targetId, content, conversationType) => {
|
|
112
|
+
return rongcloudClient.sendMessage(targetId, content, conversationType);
|
|
113
|
+
},
|
|
114
|
+
log,
|
|
115
|
+
async (msg) => {
|
|
116
|
+
return rongcloudClient.sendReadReceipt(msg);
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// 包装 MessageHandler.handleMessage 以处理结构化消息
|
|
121
|
+
const originalHandleMessage = messageHandler.handleMessage.bind(messageHandler);
|
|
122
|
+
|
|
123
|
+
messageHandler.handleMessage = async (msg) => {
|
|
124
|
+
// 检查是否是结构化消息
|
|
125
|
+
if (msg.content && typeof msg.content === 'string') {
|
|
126
|
+
try {
|
|
127
|
+
const parsed = JSON.parse(msg.content);
|
|
128
|
+
|
|
129
|
+
if (parsed.msg_type) {
|
|
130
|
+
// 这是结构化消息,使用 RongyunMessageHandler 处理
|
|
131
|
+
log.info(`[WORKER] 收到结构化消息: type=${parsed.msg_type}, from=${parsed.source_im_id || msg.senderUserId}`);
|
|
132
|
+
|
|
133
|
+
// 忽略自己发送的消息
|
|
134
|
+
if (parsed.source_im_id === rongcloudConfig.accountId) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Timestamp 校验(5分钟有效期)
|
|
139
|
+
const msgTimestamp = parsed.timestamp;
|
|
140
|
+
if (msgTimestamp) {
|
|
141
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
142
|
+
const timeDiff = Math.abs(currentTime - msgTimestamp);
|
|
143
|
+
if (timeDiff > 300) { // 5分钟 = 300秒
|
|
144
|
+
logTimestampValidation(
|
|
145
|
+
`消息时间戳过期: msg_time=${msgTimestamp}, current_time=${currentTime}, ` +
|
|
146
|
+
`diff=${timeDiff}s, type=${parsed.msg_type}, source=${parsed.source_im_id}`
|
|
147
|
+
);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 解析 content 字段(它本身可能是 JSON 字符串)
|
|
153
|
+
let innerContent = parsed.content;
|
|
154
|
+
if (typeof innerContent === 'string') {
|
|
155
|
+
try {
|
|
156
|
+
innerContent = JSON.parse(innerContent);
|
|
157
|
+
} catch {
|
|
158
|
+
// 保持字符串
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 构建消息数据
|
|
163
|
+
// 注意:后端发送的 command 消息中,command/command_id/request_id 在 content 字段内
|
|
164
|
+
// 保留原始 content(用户消息内容),同时展开其他字段
|
|
165
|
+
const messageData = {
|
|
166
|
+
...parsed,
|
|
167
|
+
...innerContent, // 展开 content 中的字段(如 command, command_id 等)
|
|
168
|
+
content: typeof innerContent === 'object' ? innerContent.content : innerContent,
|
|
169
|
+
senderUserId: parsed.source_im_id || msg.senderUserId,
|
|
170
|
+
targetId: msg.targetId,
|
|
171
|
+
conversationType: msg.conversationType,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// 使用 RongyunMessageHandler 处理
|
|
175
|
+
try {
|
|
176
|
+
await rongyunMessageHandler.handle(messageData);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
log.error(`[WORKER] RongyunMessageHandler 处理异常: ${err.message}`);
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// 不是 JSON,是普通消息,继续传给原始 handler
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 调用原始的 handleMessage(处理普通消息)
|
|
188
|
+
return originalHandleMessage(msg);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// 添加调试日志:确认替换后的方法
|
|
192
|
+
log.info('[WORKER-DEBUG] 替换后 messageHandler.handleMessage 类型: ' + typeof messageHandler.handleMessage);
|
|
193
|
+
|
|
194
|
+
const connected = await rongcloudClient.connect(messageHandler);
|
|
195
|
+
if (connected) {
|
|
196
|
+
log.info('[WORKER] 融云连接成功');
|
|
197
|
+
|
|
198
|
+
// 发送 CLIENT_CONNECTED
|
|
199
|
+
try {
|
|
200
|
+
await messageSender.sendClientConnected();
|
|
201
|
+
log.info('[WORKER] CLIENT_CONNECTED 已发送');
|
|
202
|
+
} catch (err) {
|
|
203
|
+
log.error(`[WORKER] 发送 CLIENT_CONNECTED 失败: ${err.message}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 启动心跳管理器
|
|
207
|
+
const heartbeatManager = new HeartbeatManager(rongcloudClient, rongcloudConfig, log);
|
|
208
|
+
heartbeatManager.start(getMacAddress, getOpenClawStatus);
|
|
209
|
+
|
|
210
|
+
// 启动仪表盘上报
|
|
211
|
+
const dashboardReporter = new DashboardReporter(rongcloudClient, rongcloudConfig, log);
|
|
212
|
+
dashboardReporter.start(getMacAddress);
|
|
213
|
+
|
|
214
|
+
// 保存引用以便关闭时停止
|
|
215
|
+
global.heartbeatManager = heartbeatManager;
|
|
216
|
+
global.dashboardReporter = dashboardReporter;
|
|
217
|
+
|
|
218
|
+
} else {
|
|
219
|
+
log.error('[WORKER] 融云连接失败');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function shutdownRongCloud() {
|
|
224
|
+
// 停止心跳和仪表盘上报
|
|
225
|
+
if (global.heartbeatManager) {
|
|
226
|
+
global.heartbeatManager.stop();
|
|
227
|
+
}
|
|
228
|
+
if (global.dashboardReporter) {
|
|
229
|
+
global.dashboardReporter.stop();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (rongcloudClient) {
|
|
233
|
+
// 发送 CLIENT_DISCONNECTED
|
|
234
|
+
try {
|
|
235
|
+
const messageSender = new RongyunMessageSender(rongcloudClient, rongcloudConfig, log);
|
|
236
|
+
await messageSender.sendClientDisconnected();
|
|
237
|
+
log.info('[WORKER] CLIENT_DISCONNECTED 已发送');
|
|
238
|
+
} catch (err) {
|
|
239
|
+
log.error(`[WORKER] 发送 CLIENT_DISCONNECTED 失败: ${err.message}`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await rongcloudClient.disconnect();
|
|
243
|
+
log.info('[WORKER] 融云已断开');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// 停止 opencode 服务
|
|
247
|
+
stopOpencodeService(log);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
initRongCloud().catch(err => {
|
|
251
|
+
log.error(`[WORKER] 融云初始化异常: ${err.message}`);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const server = http.createServer((req, res) => {
|
|
255
|
+
if (req.url === '/health') {
|
|
256
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
257
|
+
res.end('alive');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (req.url === '/version') {
|
|
261
|
+
try {
|
|
262
|
+
const versionFile = path.join(__dirname, '..', 'version.json');
|
|
263
|
+
const data = JSON.parse(fs.readFileSync(versionFile, 'utf8'));
|
|
264
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
265
|
+
res.end(JSON.stringify(data));
|
|
266
|
+
} catch (e) {
|
|
267
|
+
res.writeHead(500);
|
|
268
|
+
res.end('version read error');
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (req.url === '/rongcloud/status') {
|
|
273
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
274
|
+
res.end(JSON.stringify({
|
|
275
|
+
enabled: !!rongcloudConfig,
|
|
276
|
+
connected: rongcloudClient?.isConnected || false
|
|
277
|
+
}));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
res.writeHead(404);
|
|
281
|
+
res.end('not found');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
285
|
+
log.info(`[WORKER] HTTP 服务已启动: http://127.0.0.1:${PORT}/health`);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
process.on('message', (msg) => {
|
|
289
|
+
if (msg?.type === 'prepare-shutdown') {
|
|
290
|
+
log.info(`[WORKER] 收到${msg.reason || 'unknown'}通知,准备优雅退出...`);
|
|
291
|
+
shutdownRongCloud().then(() => {
|
|
292
|
+
server.close(() => {
|
|
293
|
+
log.info('[WORKER] HTTP 服务已关闭');
|
|
294
|
+
setTimeout(() => process.exit(0), 1000);
|
|
295
|
+
});
|
|
296
|
+
}).catch(err => {
|
|
297
|
+
log.error(`[WORKER] 关闭融云异常: ${err.message}`);
|
|
298
|
+
server.close(() => process.exit(0));
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// 标记是否正在关闭,避免重复执行
|
|
304
|
+
let isShuttingDown = false;
|
|
305
|
+
|
|
306
|
+
// 优雅退出处理函数
|
|
307
|
+
async function gracefulShutdown(signal) {
|
|
308
|
+
if (isShuttingDown) {
|
|
309
|
+
log.warn(`[WORKER] 已经在关闭中,忽略 ${signal} 信号`);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
isShuttingDown = true;
|
|
313
|
+
|
|
314
|
+
log.info(`[WORKER] 收到 ${signal} 信号,开始优雅退出...`);
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
await shutdownRongCloud();
|
|
318
|
+
} catch (err) {
|
|
319
|
+
log.error(`[WORKER] 关闭融云异常: ${err.message}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 关闭 HTTP 服务
|
|
323
|
+
server.close(() => {
|
|
324
|
+
log.info('[WORKER] HTTP 服务已关闭');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// 给 3 秒时间完成关闭操作
|
|
328
|
+
setTimeout(() => {
|
|
329
|
+
log.info('[WORKER] 退出进程');
|
|
330
|
+
process.exit(0);
|
|
331
|
+
}, 3000);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 处理正常退出信号
|
|
335
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
336
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
337
|
+
|
|
338
|
+
// 处理 Windows 的 SIGINT(Ctrl+C)
|
|
339
|
+
if (process.platform === 'win32') {
|
|
340
|
+
process.on('SIGBREAK', () => gracefulShutdown('SIGBREAK'));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 处理未捕获的异常
|
|
344
|
+
process.on('uncaughtException', async (err) => {
|
|
345
|
+
log.error(`[WORKER] 未捕获异常: ${err.message}\n${err.stack}`);
|
|
346
|
+
if (!isShuttingDown) {
|
|
347
|
+
await gracefulShutdown('uncaughtException');
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// 处理未处理的 Promise 拒绝
|
|
352
|
+
process.on('unhandledRejection', async (reason) => {
|
|
353
|
+
log.error(`[WORKER] 未捕获 Promise: ${reason}`);
|
|
354
|
+
if (!isShuttingDown) {
|
|
355
|
+
await gracefulShutdown('unhandledRejection');
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// 处理进程退出事件(最后的机会)
|
|
360
|
+
process.on('exit', (code) => {
|
|
361
|
+
if (!isShuttingDown && rongcloudClient?.isConnected) {
|
|
362
|
+
log.warn(`[WORKER] 进程即将退出 (code=${code}),尝试发送 CLIENT_DISCONNECTED...`);
|
|
363
|
+
// 同步发送,因为 exit 事件不支持异步
|
|
364
|
+
try {
|
|
365
|
+
const messageSender = new RongyunMessageSender(rongcloudClient, rongcloudConfig, log);
|
|
366
|
+
// 使用同步方式发送(如果可能)
|
|
367
|
+
messageSender.sendClientDisconnected().catch(() => {});
|
|
368
|
+
} catch (e) {
|
|
369
|
+
// 忽略错误
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
setInterval(() => {
|
|
375
|
+
log.info('[WORKER] 业务心跳...');
|
|
376
|
+
}, 60000);
|
package/version.json
ADDED