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,189 @@
|
|
|
1
|
+
const { fork } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { createLogger } = require('./logger');
|
|
4
|
+
const { Updater } = require('./updater');
|
|
5
|
+
|
|
6
|
+
const log = createLogger('daemon');
|
|
7
|
+
const WORKER_PATH = path.join(__dirname, 'worker.js');
|
|
8
|
+
|
|
9
|
+
let worker = null;
|
|
10
|
+
let stopping = false;
|
|
11
|
+
let isRollingBack = false;
|
|
12
|
+
let healthTimer = null;
|
|
13
|
+
let currentBackupDir = null;
|
|
14
|
+
const updater = new Updater();
|
|
15
|
+
|
|
16
|
+
process.chdir(__dirname);
|
|
17
|
+
|
|
18
|
+
function startWorker(isAfterUpdate = false, backupDirForRollback = null) {
|
|
19
|
+
if (stopping || isRollingBack) return;
|
|
20
|
+
|
|
21
|
+
log.info(`[DAEMON] 启动 Worker,PID: ${process.pid},更新后重启: ${isAfterUpdate}`);
|
|
22
|
+
|
|
23
|
+
// Windows 服务中需要使用 detached: true 避免控制台关联问题
|
|
24
|
+
const forkOptions = {
|
|
25
|
+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
|
26
|
+
detached: process.platform === 'win32',
|
|
27
|
+
windowsHide: process.platform === 'win32'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
worker = fork(WORKER_PATH, [], forkOptions);
|
|
31
|
+
|
|
32
|
+
worker.stdout?.on('data', d => log.info(`[WORKER] ${d.toString().trim()}`));
|
|
33
|
+
worker.stderr?.on('data', d => log.error(`[WORKER] ${d.toString().trim()}`));
|
|
34
|
+
|
|
35
|
+
if (isAfterUpdate && backupDirForRollback) {
|
|
36
|
+
currentBackupDir = backupDirForRollback;
|
|
37
|
+
healthTimer = setTimeout(() => {
|
|
38
|
+
log.info('[DAEMON] 新版 Worker 健康观察通过(5分钟未崩溃),更新成功');
|
|
39
|
+
currentBackupDir = null;
|
|
40
|
+
}, 5 * 60 * 1000);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
worker.on('exit', (code, signal) => {
|
|
44
|
+
if (healthTimer) {
|
|
45
|
+
clearTimeout(healthTimer);
|
|
46
|
+
healthTimer = null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
log.error(`[DAEMON] Worker 退出,code=${code}, signal=${signal}`);
|
|
50
|
+
worker = null;
|
|
51
|
+
|
|
52
|
+
if (isAfterUpdate && code !== 0 && currentBackupDir && !isRollingBack) {
|
|
53
|
+
isRollingBack = true;
|
|
54
|
+
log.error('[DAEMON] 新版 Worker 启动后异常退出,触发自动回滚!');
|
|
55
|
+
|
|
56
|
+
updater.rollback(currentBackupDir).then(() => {
|
|
57
|
+
isRollingBack = false;
|
|
58
|
+
currentBackupDir = null;
|
|
59
|
+
log.info('[DAEMON] 回滚完成,重新启动旧版 Worker...');
|
|
60
|
+
startWorker(false, null);
|
|
61
|
+
}).catch(err => {
|
|
62
|
+
log.error(`[DAEMON] 回滚失败: ${err.message}`);
|
|
63
|
+
isRollingBack = false;
|
|
64
|
+
currentBackupDir = null;
|
|
65
|
+
startWorker(false, null);
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!stopping && !isRollingBack) {
|
|
71
|
+
setTimeout(() => startWorker(false, null), 3000);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
worker.on('error', (err) => {
|
|
76
|
+
log.error(`[DAEMON] Worker 启动错误: ${err.message}`);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function restartWorkerWithUpdate(backupDir) {
|
|
81
|
+
if (!worker) {
|
|
82
|
+
startWorker(true, backupDir);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
log.info('[DAEMON] 收到更新重启指令,优雅终止当前 Worker...');
|
|
87
|
+
|
|
88
|
+
worker.send({ type: 'prepare-shutdown', reason: 'update' });
|
|
89
|
+
|
|
90
|
+
// Windows 上等待时间稍长
|
|
91
|
+
const waitTime = process.platform === 'win32' ? 5000 : 3000;
|
|
92
|
+
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
if (worker) {
|
|
95
|
+
worker.removeAllListeners('exit');
|
|
96
|
+
|
|
97
|
+
// Windows 上使用 taskkill 确保进程树被终止
|
|
98
|
+
if (process.platform === 'win32' && worker.pid) {
|
|
99
|
+
try {
|
|
100
|
+
const { execSync } = require('child_process');
|
|
101
|
+
execSync(`taskkill /pid ${worker.pid} /T /F 2>nul`, { windowsHide: true });
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// 忽略错误,使用 kill 作为后备
|
|
104
|
+
worker.kill('SIGTERM');
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
worker.kill('SIGTERM');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const waitKill = setInterval(() => {
|
|
111
|
+
if (!worker || worker.killed) {
|
|
112
|
+
clearInterval(waitKill);
|
|
113
|
+
worker = null;
|
|
114
|
+
setTimeout(() => startWorker(true, backupDir), 1000);
|
|
115
|
+
} else {
|
|
116
|
+
// 再次尝试强制终止
|
|
117
|
+
if (process.platform === 'win32' && worker.pid) {
|
|
118
|
+
try {
|
|
119
|
+
const { execSync } = require('child_process');
|
|
120
|
+
execSync(`taskkill /pid ${worker.pid} /T /F 2>nul`, { windowsHide: true });
|
|
121
|
+
} catch (e) {
|
|
122
|
+
worker.kill('SIGKILL');
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
worker.kill('SIGKILL');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}, 500);
|
|
129
|
+
} else {
|
|
130
|
+
startWorker(true, backupDir);
|
|
131
|
+
}
|
|
132
|
+
}, waitTime);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function gracefulShutdown() {
|
|
136
|
+
stopping = true;
|
|
137
|
+
updater.stopSchedule();
|
|
138
|
+
log.info('[DAEMON] 收到停止信号,正在终止 Worker...');
|
|
139
|
+
|
|
140
|
+
if (worker) {
|
|
141
|
+
// 先尝试优雅地通知 Worker 退出
|
|
142
|
+
try {
|
|
143
|
+
worker.send({ type: 'prepare-shutdown', reason: 'daemon-stopping' });
|
|
144
|
+
} catch (e) {
|
|
145
|
+
// 忽略发送失败
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Windows 上等待时间稍长,给子进程时间清理
|
|
149
|
+
const waitTime = process.platform === 'win32' ? 8000 : 5000;
|
|
150
|
+
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
if (worker && !worker.killed) {
|
|
153
|
+
log.error('[DAEMON] Worker 未响应,强制杀死');
|
|
154
|
+
// Windows 上使用 taskkill 确保进程树被终止
|
|
155
|
+
if (process.platform === 'win32' && worker.pid) {
|
|
156
|
+
try {
|
|
157
|
+
const { execSync } = require('child_process');
|
|
158
|
+
execSync(`taskkill /pid ${worker.pid} /T /F 2>nul`, { windowsHide: true });
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// 忽略错误,使用 kill 作为后备
|
|
161
|
+
worker.kill('SIGKILL');
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
worker.kill('SIGKILL');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
process.exit(0);
|
|
168
|
+
}, waitTime);
|
|
169
|
+
} else {
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
process.on('SIGINT', gracefulShutdown);
|
|
175
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
176
|
+
process.on('message', (msg) => {
|
|
177
|
+
if (msg === 'shutdown') gracefulShutdown();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
startWorker(false, null);
|
|
181
|
+
updater.startSchedule(restartWorkerWithUpdate);
|
|
182
|
+
|
|
183
|
+
process.on('uncaughtException', (err) => {
|
|
184
|
+
log.error(`[DAEMON] 未捕获异常: ${err.message}`);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
process.on('unhandledRejection', (reason) => {
|
|
188
|
+
log.error(`[DAEMON] 未捕获 Promise: ${reason}`);
|
|
189
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const LOG_DIR = path.join(__dirname, '..', 'logs');
|
|
5
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
6
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function createLogger(name) {
|
|
10
|
+
const getFile = () => {
|
|
11
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
12
|
+
return path.join(LOG_DIR, `${name}-${date}.log`);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const write = (level, msg) => {
|
|
16
|
+
const line = `[${new Date().toISOString()}] [${level}] ${msg}\n`;
|
|
17
|
+
try {
|
|
18
|
+
fs.appendFileSync(getFile(), line);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
// ignore write errors
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
info: (msg) => write('INFO', msg),
|
|
26
|
+
error: (msg) => write('ERROR', msg),
|
|
27
|
+
warn: (msg) => write('WARN', msg)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { createLogger };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
function generateSecret(mac, secretKey) {
|
|
4
|
+
return crypto.createHash('md5')
|
|
5
|
+
.update(mac + secretKey)
|
|
6
|
+
.digest('hex');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function verifySecret(mac, secretKey, expectedSecret) {
|
|
10
|
+
const calculated = generateSecret(mac, secretKey);
|
|
11
|
+
return calculated === expectedSecret;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
generateSecret,
|
|
16
|
+
verifySecret
|
|
17
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息业务处理器 - 处理 rongcloud 转发过来的消息
|
|
3
|
+
*
|
|
4
|
+
* 被调用位置:rongcloud/message-handler.js
|
|
5
|
+
* - handleNormal() 第68行:处理普通消息(AI聊天)
|
|
6
|
+
* - handleCommand() 第70行:处理命令消息(以/开头)
|
|
7
|
+
*
|
|
8
|
+
* 注入方式:通过 config.onCommand 回调
|
|
9
|
+
* 位置:rongcloud/message-handler.js 第93行
|
|
10
|
+
*/
|
|
11
|
+
const { CommandHandler, OpenClawCommandEnum } = require('./command-handler');
|
|
12
|
+
const { RongyunMessageSender } = require('./rongyun-message-sender');
|
|
13
|
+
|
|
14
|
+
class BusinessMessageHandler {
|
|
15
|
+
constructor(config, rongcloudClient, log) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.rongcloudClient = rongcloudClient;
|
|
18
|
+
this.log = log;
|
|
19
|
+
this.commandHandler = new CommandHandler(config, log);
|
|
20
|
+
this.messageSender = new RongyunMessageSender(rongcloudClient, config, log);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 处理命令消息
|
|
25
|
+
* 被调用位置:rongcloud/message-handler.js 第93行
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} payload - 命令对象
|
|
28
|
+
* @param {string} payload.command - 命令名称
|
|
29
|
+
* @param {string[]} payload.args - 命令参数
|
|
30
|
+
* @param {string} payload.rawMessage - 原始消息
|
|
31
|
+
* @param {string} payload.senderId - 发送者ID
|
|
32
|
+
* @returns {string} 回复内容
|
|
33
|
+
*/
|
|
34
|
+
async handleCommand(payload) {
|
|
35
|
+
this.log?.info(`[BusinessMessageHandler] 收到命令: ${payload.command}, args=${JSON.stringify(payload.args)}`);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// 将命令名称或枚举值映射到枚举值
|
|
39
|
+
// 后端可能发送字符串(如 "start")或整数(如 1)
|
|
40
|
+
const commandMap = {
|
|
41
|
+
'start': OpenClawCommandEnum.START,
|
|
42
|
+
'stop': OpenClawCommandEnum.STOP,
|
|
43
|
+
'status': OpenClawCommandEnum.STATUS,
|
|
44
|
+
'restart': OpenClawCommandEnum.RESTART,
|
|
45
|
+
1: OpenClawCommandEnum.START,
|
|
46
|
+
2: OpenClawCommandEnum.STOP,
|
|
47
|
+
3: OpenClawCommandEnum.RESTART,
|
|
48
|
+
4: OpenClawCommandEnum.STATUS
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// 支持字符串和整数命令
|
|
52
|
+
const commandKey = typeof payload.command === 'string'
|
|
53
|
+
? payload.command.toLowerCase()
|
|
54
|
+
: payload.command;
|
|
55
|
+
|
|
56
|
+
const command = commandMap[commandKey];
|
|
57
|
+
if (!command) {
|
|
58
|
+
return `未知命令: ${payload.command}\n可用命令: start, stop, status, restart`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 执行命令
|
|
62
|
+
const result = await this.commandHandler.execute(command);
|
|
63
|
+
|
|
64
|
+
// 发送命令结果到服务端
|
|
65
|
+
try {
|
|
66
|
+
await this.messageSender.sendCommandResult(
|
|
67
|
+
command,
|
|
68
|
+
payload.senderId,
|
|
69
|
+
result.status,
|
|
70
|
+
result.message
|
|
71
|
+
);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
this.log?.error(`[BusinessMessageHandler] 发送命令结果失败: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result.message;
|
|
77
|
+
|
|
78
|
+
} catch (err) {
|
|
79
|
+
this.log?.error(`[BusinessMessageHandler] 命令处理异常: ${err.message}`);
|
|
80
|
+
return `执行失败: ${err.message}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 处理普通消息(AI聊天)
|
|
86
|
+
* 被调用位置:rongcloud/message-handler.js 第68行
|
|
87
|
+
*
|
|
88
|
+
* @param {Object} msg - 消息对象
|
|
89
|
+
* @param {string} msg.content - 消息内容
|
|
90
|
+
* @param {string} msg.senderUserId - 发送者ID
|
|
91
|
+
* @returns {string} 回复内容
|
|
92
|
+
*/
|
|
93
|
+
async handleNormalMessage(msg) {
|
|
94
|
+
this.log?.info(`[BusinessMessageHandler] 收到普通消息 from=${msg.senderUserId}: ${msg.content?.substring(0, 50)}`);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// TODO: 调用 AI 服务进行回复
|
|
98
|
+
// 这里可以调用 opencode 服务或其他 AI 服务
|
|
99
|
+
|
|
100
|
+
const reply = `收到您的消息: ${msg.content}`;
|
|
101
|
+
|
|
102
|
+
// 发送聊天消息回复到服务端
|
|
103
|
+
try {
|
|
104
|
+
await this.messageSender.sendChatMessage(reply);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
this.log?.error(`[BusinessMessageHandler] 发送聊天回复失败: ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return reply;
|
|
110
|
+
|
|
111
|
+
} catch (err) {
|
|
112
|
+
this.log?.error(`[BusinessMessageHandler] 处理普通消息异常: ${err.message}`);
|
|
113
|
+
return `抱歉,处理消息时出错: ${err.message}`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { BusinessMessageHandler };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 命令处理器 - 处理 start/stop/status/restart 命令
|
|
3
|
+
*
|
|
4
|
+
* 使用 script-executor.js 执行实际的脚本文件
|
|
5
|
+
*/
|
|
6
|
+
const { ScriptExecutor, OpenClawCommandEnum, OpenClawServiceStatus } = require('./script-executor');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
class CommandHandler {
|
|
10
|
+
constructor(config, log) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.log = log;
|
|
13
|
+
|
|
14
|
+
// 初始化脚本执行器
|
|
15
|
+
const scriptDir = this.getScriptDir();
|
|
16
|
+
this.executor = new ScriptExecutor(
|
|
17
|
+
scriptDir,
|
|
18
|
+
config.successKeyword || 'Success',
|
|
19
|
+
config.scriptTimeout || 180
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 获取脚本目录
|
|
25
|
+
*/
|
|
26
|
+
getScriptDir() {
|
|
27
|
+
const system = process.platform;
|
|
28
|
+
const subDir = system === 'win32' ? path.join('command', 'win') : path.join('command', 'linux');
|
|
29
|
+
return path.join(__dirname, '..', '..', subDir);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 获取脚本文件名
|
|
34
|
+
*/
|
|
35
|
+
getScriptName(command) {
|
|
36
|
+
const system = process.platform;
|
|
37
|
+
const ext = system === 'win32' ? '.bat' : '.sh';
|
|
38
|
+
const names = {
|
|
39
|
+
[OpenClawCommandEnum.START]: 'start',
|
|
40
|
+
[OpenClawCommandEnum.STOP]: 'stop',
|
|
41
|
+
[OpenClawCommandEnum.RESTART]: 'restart',
|
|
42
|
+
[OpenClawCommandEnum.STATUS]: 'status',
|
|
43
|
+
[OpenClawCommandEnum.CONFIG_FIX]: null // 配置修复不需要脚本,直接执行命令
|
|
44
|
+
};
|
|
45
|
+
const name = names[command];
|
|
46
|
+
return name ? name + ext : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 执行命令
|
|
51
|
+
* @param {number} command - 命令枚举值 (1=start, 2=stop, 3=restart, 4=status, 5=config_fix)
|
|
52
|
+
* @returns {Object} { status, message }
|
|
53
|
+
*/
|
|
54
|
+
async execute(command) {
|
|
55
|
+
const scriptName = this.getScriptName(command);
|
|
56
|
+
const commandName = this.getCommandName(command);
|
|
57
|
+
|
|
58
|
+
// 配置修复命令特殊处理:直接执行 openclaw doctor --fix
|
|
59
|
+
if (command === OpenClawCommandEnum.CONFIG_FIX) {
|
|
60
|
+
return await this.executeConfigFix();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.log?.info(`[CommandHandler] 执行 ${commandName} 命令,脚本: ${scriptName}`);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const result = await this.executor.executeWithStatus(command, scriptName);
|
|
67
|
+
this.log?.info(`[CommandHandler] ${commandName} 结果: ${result.status} - ${result.message}`);
|
|
68
|
+
return result;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
this.log?.error(`[CommandHandler] ${commandName} 异常: ${err.message}`);
|
|
71
|
+
return {
|
|
72
|
+
status: OpenClawServiceStatus.ERROR,
|
|
73
|
+
message: `执行异常: ${err.message}`
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 执行配置修复命令
|
|
80
|
+
* 直接运行 openclaw doctor --fix
|
|
81
|
+
*/
|
|
82
|
+
async executeConfigFix() {
|
|
83
|
+
this.log?.info('[CommandHandler] 执行配置修复命令: openclaw doctor --fix');
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result = await this.executor.executeCommandDirect('openclaw doctor --fix', 120); // 2分钟超时
|
|
87
|
+
this.log?.info(`[CommandHandler] 配置修复结果: ${result.status} - ${result.message}`);
|
|
88
|
+
return result;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
this.log?.error(`[CommandHandler] 配置修复异常: ${err.message}`);
|
|
91
|
+
return {
|
|
92
|
+
status: OpenClawServiceStatus.ERROR,
|
|
93
|
+
message: `配置修复异常: ${err.message}`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 获取命令名称
|
|
100
|
+
*/
|
|
101
|
+
getCommandName(command) {
|
|
102
|
+
const names = {
|
|
103
|
+
[OpenClawCommandEnum.START]: '启动',
|
|
104
|
+
[OpenClawCommandEnum.STOP]: '停止',
|
|
105
|
+
[OpenClawCommandEnum.RESTART]: '重启',
|
|
106
|
+
[OpenClawCommandEnum.STATUS]: '状态检查',
|
|
107
|
+
[OpenClawCommandEnum.CONFIG_FIX]: '配置修复'
|
|
108
|
+
};
|
|
109
|
+
return names[command] || '未知命令';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 启动服务
|
|
114
|
+
*/
|
|
115
|
+
async start() {
|
|
116
|
+
return await this.execute(OpenClawCommandEnum.START);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 停止服务
|
|
121
|
+
*/
|
|
122
|
+
async stop() {
|
|
123
|
+
return await this.execute(OpenClawCommandEnum.STOP);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 重启服务
|
|
128
|
+
*/
|
|
129
|
+
async restart() {
|
|
130
|
+
return await this.execute(OpenClawCommandEnum.RESTART);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 检查状态
|
|
135
|
+
*/
|
|
136
|
+
async status() {
|
|
137
|
+
return await this.execute(OpenClawCommandEnum.STATUS);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 配置修复
|
|
142
|
+
*/
|
|
143
|
+
async configFix() {
|
|
144
|
+
return await this.execute(OpenClawCommandEnum.CONFIG_FIX);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
CommandHandler,
|
|
150
|
+
OpenClawCommandEnum,
|
|
151
|
+
OpenClawServiceStatus
|
|
152
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
function loadConfig() {
|
|
6
|
+
// Read from ~/.claw-bridge/config.json
|
|
7
|
+
const clawBridgePath = path.join(os.homedir(), '.claw-bridge', 'config.json');
|
|
8
|
+
let clawBridgeConfig = {};
|
|
9
|
+
if (fs.existsSync(clawBridgePath)) {
|
|
10
|
+
try {
|
|
11
|
+
clawBridgeConfig = JSON.parse(fs.readFileSync(clawBridgePath, 'utf-8'));
|
|
12
|
+
} catch (e) {
|
|
13
|
+
console.error('读取 claw-bridge 配置失败:', e);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Read from local rongcloud-config.json
|
|
18
|
+
const localConfigPath = path.join(__dirname, '..', '..', 'rongcloud-config.json');
|
|
19
|
+
let localConfig = {};
|
|
20
|
+
if (fs.existsSync(localConfigPath)) {
|
|
21
|
+
try {
|
|
22
|
+
localConfig = JSON.parse(fs.readFileSync(localConfigPath, 'utf-8'));
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error('读取本地配置失败:', e);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
appKey: process.env.DM_APP_KEY || localConfig.appKey || 'bmdehs6pbyyks',
|
|
30
|
+
token: localConfig.token || clawBridgeConfig.token,
|
|
31
|
+
accountId: localConfig.accountId || clawBridgeConfig.nodeId,
|
|
32
|
+
nodeName: clawBridgeConfig.nodeName || 'cli-client',
|
|
33
|
+
secretKey: localConfig.secretKey || 'secret_key',
|
|
34
|
+
nickname: localConfig.nickname || 'CLI客户端',
|
|
35
|
+
reconnectInterval: localConfig.reconnectInterval || 60,
|
|
36
|
+
heartbeatInterval: localConfig.heartbeatInterval || 20,
|
|
37
|
+
openclawPort: localConfig.openclawPort || 18789,
|
|
38
|
+
scriptTimeout: localConfig.scriptTimeout || 180,
|
|
39
|
+
successKeyword: localConfig.successKeyword || 'Success',
|
|
40
|
+
chatTimeout: localConfig.chatTimeout || 600
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { loadConfig };
|