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,348 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { exec } = require('child_process');
|
|
7
|
+
const { createLogger } = require('./logger');
|
|
8
|
+
|
|
9
|
+
const log = createLogger('updater');
|
|
10
|
+
|
|
11
|
+
const CONFIG = {
|
|
12
|
+
UPDATE_URL: 'https://your-cdn.com/api/version',
|
|
13
|
+
CHECK_INTERVAL: 10000 * 60 * 60 * 6,
|
|
14
|
+
ROLLBACK_TIMEOUT: 10000 * 60 * 5,
|
|
15
|
+
CURRENT_VERSION_PATH: path.join(__dirname, '..', 'version.json'),
|
|
16
|
+
UPDATE_DIR: path.join(__dirname, '..', 'update'),
|
|
17
|
+
BACKUP_DIR: path.join(__dirname, '..', 'backup'),
|
|
18
|
+
SERVICE_DIR: path.join(__dirname)
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
[CONFIG.UPDATE_DIR, CONFIG.BACKUP_DIR].forEach(dir => {
|
|
22
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
class Updater {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.currentVersion = this.loadCurrentVersion();
|
|
28
|
+
this.isUpdating = false;
|
|
29
|
+
this.updateTimer = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
loadCurrentVersion() {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(CONFIG.CURRENT_VERSION_PATH, 'utf8');
|
|
35
|
+
return JSON.parse(raw).version;
|
|
36
|
+
} catch {
|
|
37
|
+
return '0.0.0';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
saveVersion(ver) {
|
|
42
|
+
fs.writeFileSync(CONFIG.CURRENT_VERSION_PATH, JSON.stringify({
|
|
43
|
+
version: ver,
|
|
44
|
+
updatedAt: new Date().toISOString()
|
|
45
|
+
}, null, 2));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async check() {
|
|
49
|
+
if (this.isUpdating) return null;
|
|
50
|
+
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const url = new URL(`${CONFIG.UPDATE_URL}?current=${this.currentVersion}&arch=${process.platform}`);
|
|
53
|
+
const client = url.protocol === 'https:' ? https : http;
|
|
54
|
+
|
|
55
|
+
const req = client.get(url, { timeout: 15000 }, (res) => {
|
|
56
|
+
let data = '';
|
|
57
|
+
res.on('data', chunk => data += chunk);
|
|
58
|
+
res.on('end', () => {
|
|
59
|
+
try {
|
|
60
|
+
if (res.statusCode === 204) {
|
|
61
|
+
resolve(null);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const info = JSON.parse(data);
|
|
65
|
+
if (this.compareVersion(info.version, this.currentVersion) > 0) {
|
|
66
|
+
log.info(`[UPDATER] 发现新版本: ${this.currentVersion} → ${info.version}`);
|
|
67
|
+
resolve(info);
|
|
68
|
+
} else {
|
|
69
|
+
resolve(null);
|
|
70
|
+
}
|
|
71
|
+
} catch (e) {
|
|
72
|
+
reject(new Error('版本接口返回非法JSON'));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
req.on('error', reject);
|
|
78
|
+
req.on('timeout', () => {
|
|
79
|
+
req.destroy();
|
|
80
|
+
reject(new Error('版本检测超时'));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
compareVersion(a, b) {
|
|
86
|
+
const pa = a.split('.').map(Number);
|
|
87
|
+
const pb = b.split('.').map(Number);
|
|
88
|
+
for (let i = 0; i < 3; i++) {
|
|
89
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
90
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
91
|
+
}
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async download(url, dest, expectedSize = 0) {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const tempPath = dest + '.tmp';
|
|
98
|
+
let startByte = 0;
|
|
99
|
+
|
|
100
|
+
if (fs.existsSync(tempPath)) {
|
|
101
|
+
startByte = fs.statSync(tempPath).size;
|
|
102
|
+
log.info(`[UPDATER] 断点续传: ${startByte} bytes`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const client = url.startsWith('https') ? https : http;
|
|
106
|
+
const req = client.get(url, {
|
|
107
|
+
headers: startByte > 0 ? { 'Range': `bytes=${startByte}-` } : {},
|
|
108
|
+
timeout: 30000
|
|
109
|
+
}, (res) => {
|
|
110
|
+
if (res.statusCode !== 200 && res.statusCode !== 206) {
|
|
111
|
+
return reject(new Error(`下载失败,HTTP ${res.statusCode}`));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const total = parseInt(res.headers['content-length'] || expectedSize) + startByte;
|
|
115
|
+
const file = fs.createWriteStream(tempPath, { flags: startByte > 0 ? 'a' : 'w' });
|
|
116
|
+
let downloaded = startByte;
|
|
117
|
+
|
|
118
|
+
res.pipe(file);
|
|
119
|
+
res.on('data', (chunk) => {
|
|
120
|
+
downloaded += chunk.length;
|
|
121
|
+
if (total && downloaded % (1024 * 1024 * 5) < chunk.length) {
|
|
122
|
+
log.info(`[UPDATER] 下载进度: ${(downloaded / total * 100).toFixed(1)}%`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
file.on('finish', () => {
|
|
127
|
+
file.close();
|
|
128
|
+
fs.renameSync(tempPath, dest);
|
|
129
|
+
resolve(dest);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
req.on('error', reject);
|
|
134
|
+
req.on('timeout', () => {
|
|
135
|
+
req.destroy();
|
|
136
|
+
reject(new Error('下载超时'));
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async verifyHash(filePath, expectedHash) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const hash = crypto.createHash('sha256');
|
|
144
|
+
const stream = fs.createReadStream(filePath);
|
|
145
|
+
stream.on('data', d => hash.update(d));
|
|
146
|
+
stream.on('end', () => {
|
|
147
|
+
const actual = 'sha256:' + hash.digest('hex');
|
|
148
|
+
if (actual === expectedHash) {
|
|
149
|
+
resolve(true);
|
|
150
|
+
} else {
|
|
151
|
+
reject(new Error(`哈希校验失败: ${actual} ≠ ${expectedHash}`));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
stream.on('error', reject);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async unzip(zipPath, extractTo) {
|
|
159
|
+
return new Promise((resolve, reject) => {
|
|
160
|
+
const psCmd = `powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${extractTo}' -Force"`;
|
|
161
|
+
exec(psCmd, { timeout: 600000 }, (err, stdout, stderr) => {
|
|
162
|
+
if (err) {
|
|
163
|
+
log.error(`[UPDATER] PowerShell 解压失败: ${stderr}`);
|
|
164
|
+
const sevenz = `7z x "${zipPath}" -o"${extractTo}" -y`;
|
|
165
|
+
exec(sevenz, { timeout: 600000 }, (err2) => {
|
|
166
|
+
if (err2) reject(new Error('解压失败,请确保系统有 PowerShell 或 7-Zip'));
|
|
167
|
+
else resolve();
|
|
168
|
+
});
|
|
169
|
+
} else {
|
|
170
|
+
resolve();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async atomicReplace(newDir) {
|
|
177
|
+
const serviceDir = CONFIG.SERVICE_DIR;
|
|
178
|
+
const backupDir = path.join(CONFIG.BACKUP_DIR, `bak-${Date.now()}`);
|
|
179
|
+
|
|
180
|
+
fs.mkdirSync(backupDir, { recursive: true });
|
|
181
|
+
|
|
182
|
+
const items = fs.readdirSync(serviceDir);
|
|
183
|
+
for (const item of items) {
|
|
184
|
+
if (item === 'node_modules') continue;
|
|
185
|
+
const src = path.join(serviceDir, item);
|
|
186
|
+
const dst = path.join(backupDir, item);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
fs.renameSync(src, dst);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
if (fs.statSync(src).isDirectory()) {
|
|
192
|
+
this.copyDir(src, dst);
|
|
193
|
+
} else {
|
|
194
|
+
fs.copyFileSync(src, dst);
|
|
195
|
+
const lockedTrash = src + '.old';
|
|
196
|
+
try {
|
|
197
|
+
fs.renameSync(src, lockedTrash);
|
|
198
|
+
} catch (renameErr) {
|
|
199
|
+
log.error(`[UPDATER] 文件被占用,无法重命名: ${src}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const newItems = fs.readdirSync(newDir);
|
|
206
|
+
for (const item of newItems) {
|
|
207
|
+
const src = path.join(newDir, item);
|
|
208
|
+
const dst = path.join(serviceDir, item);
|
|
209
|
+
fs.renameSync(src, dst);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.scheduleCleanup(backupDir);
|
|
213
|
+
|
|
214
|
+
log.info(`[UPDATER] 原子替换完成,备份: ${backupDir}`);
|
|
215
|
+
return backupDir;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
copyDir(src, dest) {
|
|
219
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
220
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
const s = path.join(src, entry.name);
|
|
223
|
+
const d = path.join(dest, entry.name);
|
|
224
|
+
entry.isDirectory() ? this.copyDir(s, d) : fs.copyFileSync(s, d);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
scheduleCleanup(dir) {
|
|
229
|
+
setTimeout(() => {
|
|
230
|
+
try {
|
|
231
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
232
|
+
const olds = fs.readdirSync(CONFIG.SERVICE_DIR).filter(f => f.endsWith('.old'));
|
|
233
|
+
olds.forEach(f => {
|
|
234
|
+
try {
|
|
235
|
+
fs.rmSync(path.join(CONFIG.SERVICE_DIR, f), { force: true });
|
|
236
|
+
} catch { }
|
|
237
|
+
});
|
|
238
|
+
} catch (e) {
|
|
239
|
+
log.error(`[UPDATER] 清理失败: ${e.message}`);
|
|
240
|
+
}
|
|
241
|
+
}, CONFIG.ROLLBACK_TIMEOUT + 5000);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async rollback(backupDir) {
|
|
245
|
+
log.error(`[UPDATER] 启动回滚...`);
|
|
246
|
+
const serviceDir = CONFIG.SERVICE_DIR;
|
|
247
|
+
|
|
248
|
+
const badDir = path.join(CONFIG.BACKUP_DIR, `bad-${Date.now()}`);
|
|
249
|
+
fs.mkdirSync(badDir, { recursive: true });
|
|
250
|
+
|
|
251
|
+
fs.readdirSync(serviceDir).forEach(item => {
|
|
252
|
+
if (item === 'node_modules' || item.endsWith('.old')) return;
|
|
253
|
+
try {
|
|
254
|
+
fs.renameSync(path.join(serviceDir, item), path.join(badDir, item));
|
|
255
|
+
} catch { }
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
fs.readdirSync(backupDir).forEach(item => {
|
|
259
|
+
try {
|
|
260
|
+
fs.renameSync(path.join(backupDir, item), path.join(serviceDir, item));
|
|
261
|
+
} catch { }
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
log.info(`[UPDATER] 回滚完成,已恢复旧版本`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async execute(info) {
|
|
268
|
+
if (this.isUpdating) return { success: false, error: '更新进行中' };
|
|
269
|
+
this.isUpdating = true;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
log.info(`[UPDATER] 开始更新: ${this.currentVersion} → ${info.version}`);
|
|
273
|
+
|
|
274
|
+
const zipFile = path.join(CONFIG.UPDATE_DIR, `v${info.version}.zip`);
|
|
275
|
+
await this.download(info.url, zipFile, info.size);
|
|
276
|
+
|
|
277
|
+
if (info.hash) {
|
|
278
|
+
await this.verifyHash(zipFile, info.hash);
|
|
279
|
+
log.info(`[UPDATER] 哈希校验通过`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const extractDir = path.join(CONFIG.UPDATE_DIR, `v${info.version}`);
|
|
283
|
+
if (fs.existsSync(extractDir)) fs.rmSync(extractDir, { recursive: true });
|
|
284
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
285
|
+
await this.unzip(zipFile, extractDir);
|
|
286
|
+
|
|
287
|
+
const extractSubDir = this.findServiceDir(extractDir);
|
|
288
|
+
const backupDir = await this.atomicReplace(extractSubDir);
|
|
289
|
+
|
|
290
|
+
this.saveVersion(info.version);
|
|
291
|
+
this.currentVersion = info.version;
|
|
292
|
+
|
|
293
|
+
fs.rmSync(zipFile, { force: true });
|
|
294
|
+
try { fs.rmSync(extractDir, { recursive: true, force: true }); } catch { }
|
|
295
|
+
|
|
296
|
+
log.info(`[UPDATER] 文件替换完成,准备重启 Worker`);
|
|
297
|
+
return { success: true, backupDir };
|
|
298
|
+
} catch (err) {
|
|
299
|
+
log.error(`[UPDATER] 更新失败: ${err.message}`);
|
|
300
|
+
try { fs.rmSync(CONFIG.UPDATE_DIR, { recursive: true, force: true }); } catch { }
|
|
301
|
+
return { success: false, error: err.message };
|
|
302
|
+
} finally {
|
|
303
|
+
this.isUpdating = false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
findServiceDir(extractDir) {
|
|
308
|
+
const items = fs.readdirSync(extractDir);
|
|
309
|
+
if (items.length === 1) {
|
|
310
|
+
const subPath = path.join(extractDir, items[0]);
|
|
311
|
+
if (fs.statSync(subPath).isDirectory()) {
|
|
312
|
+
const subItems = fs.readdirSync(subPath);
|
|
313
|
+
if (subItems.some(i => i.endsWith('.js'))) {
|
|
314
|
+
return subPath;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return extractDir;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
startSchedule(restartWorkerCallback) {
|
|
322
|
+
this.restartWorker = restartWorkerCallback;
|
|
323
|
+
|
|
324
|
+
const run = async () => {
|
|
325
|
+
try {
|
|
326
|
+
const info = await this.check();
|
|
327
|
+
if (info) {
|
|
328
|
+
const result = await this.execute(info);
|
|
329
|
+
if (result.success) {
|
|
330
|
+
this.restartWorker(result.backupDir);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
} catch (err) {
|
|
334
|
+
// log.error(`[UPDATER] 定时检查异常: ${err.message}`);
|
|
335
|
+
log.error(`[UPDATER] 定时检查异常: ${err}`);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
run();
|
|
340
|
+
this.updateTimer = setInterval(run, CONFIG.CHECK_INTERVAL);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
stopSchedule() {
|
|
344
|
+
if (this.updateTimer) clearInterval(this.updateTimer);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
module.exports = { Updater };
|