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,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 系统服务管理器
|
|
3
|
+
* 支持 Windows/Linux/macOS 系统服务注册
|
|
4
|
+
*/
|
|
5
|
+
const { spawn, exec } = require('child_process');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
class ServiceManager {
|
|
10
|
+
constructor(serviceName, serviceDesc, scriptPath, log) {
|
|
11
|
+
this.serviceName = serviceName || 'claw-subagent-service';
|
|
12
|
+
this.serviceDesc = serviceDesc || 'OpenClaw Guard CLI Client';
|
|
13
|
+
this.scriptPath = scriptPath || process.argv[1];
|
|
14
|
+
this.log = log;
|
|
15
|
+
this.platform = process.platform;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 安装系统服务
|
|
20
|
+
*/
|
|
21
|
+
async install() {
|
|
22
|
+
this.log?.info(`[ServiceManager] 安装系统服务: ${this.serviceName}`);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
switch (this.platform) {
|
|
26
|
+
case 'win32':
|
|
27
|
+
return await this.installWindows();
|
|
28
|
+
case 'linux':
|
|
29
|
+
return await this.installLinux();
|
|
30
|
+
case 'darwin':
|
|
31
|
+
return await this.installMacOS();
|
|
32
|
+
default:
|
|
33
|
+
throw new Error(`不支持的平台: ${this.platform}`);
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
this.log?.error(`[ServiceManager] 安装服务失败: ${err.message}`);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 卸载系统服务
|
|
43
|
+
*/
|
|
44
|
+
async uninstall() {
|
|
45
|
+
this.log?.info(`[ServiceManager] 卸载系统服务: ${this.serviceName}`);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
switch (this.platform) {
|
|
49
|
+
case 'win32':
|
|
50
|
+
return await this.uninstallWindows();
|
|
51
|
+
case 'linux':
|
|
52
|
+
return await this.uninstallLinux();
|
|
53
|
+
case 'darwin':
|
|
54
|
+
return await this.uninstallMacOS();
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`不支持的平台: ${this.platform}`);
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
this.log?.error(`[ServiceManager] 卸载服务失败: ${err.message}`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 启动服务
|
|
66
|
+
*/
|
|
67
|
+
async start() {
|
|
68
|
+
this.log?.info(`[ServiceManager] 启动服务: ${this.serviceName}`);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
switch (this.platform) {
|
|
72
|
+
case 'win32':
|
|
73
|
+
return await this.execCommand(`net start ${this.serviceName}`);
|
|
74
|
+
case 'linux':
|
|
75
|
+
return await this.execCommand(`systemctl start ${this.serviceName}`);
|
|
76
|
+
case 'darwin':
|
|
77
|
+
return await this.execCommand(`launchctl start ${this.serviceName}`);
|
|
78
|
+
default:
|
|
79
|
+
throw new Error(`不支持的平台: ${this.platform}`);
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
this.log?.error(`[ServiceManager] 启动服务失败: ${err.message}`);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 停止服务
|
|
89
|
+
*/
|
|
90
|
+
async stop() {
|
|
91
|
+
this.log?.info(`[ServiceManager] 停止服务: ${this.serviceName}`);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
switch (this.platform) {
|
|
95
|
+
case 'win32':
|
|
96
|
+
return await this.execCommand(`net stop ${this.serviceName}`);
|
|
97
|
+
case 'linux':
|
|
98
|
+
return await this.execCommand(`systemctl stop ${this.serviceName}`);
|
|
99
|
+
case 'darwin':
|
|
100
|
+
return await this.execCommand(`launchctl stop ${this.serviceName}`);
|
|
101
|
+
default:
|
|
102
|
+
throw new Error(`不支持的平台: ${this.platform}`);
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
this.log?.error(`[ServiceManager] 停止服务失败: ${err.message}`);
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 重启服务
|
|
112
|
+
*/
|
|
113
|
+
async restart() {
|
|
114
|
+
this.log?.info(`[ServiceManager] 重启服务: ${this.serviceName}`);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
switch (this.platform) {
|
|
118
|
+
case 'win32':
|
|
119
|
+
await this.execCommand(`net stop ${this.serviceName}`);
|
|
120
|
+
return await this.execCommand(`net start ${this.serviceName}`);
|
|
121
|
+
case 'linux':
|
|
122
|
+
return await this.execCommand(`systemctl restart ${this.serviceName}`);
|
|
123
|
+
case 'darwin':
|
|
124
|
+
await this.execCommand(`launchctl stop ${this.serviceName}`);
|
|
125
|
+
return await this.execCommand(`launchctl start ${this.serviceName}`);
|
|
126
|
+
default:
|
|
127
|
+
throw new Error(`不支持的平台: ${this.platform}`);
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
this.log?.error(`[ServiceManager] 重启服务失败: ${err.message}`);
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 查看服务状态
|
|
137
|
+
*/
|
|
138
|
+
async status() {
|
|
139
|
+
this.log?.info(`[ServiceManager] 查看服务状态: ${this.serviceName}`);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
switch (this.platform) {
|
|
143
|
+
case 'win32':
|
|
144
|
+
return await this.execCommand(`sc query ${this.serviceName}`);
|
|
145
|
+
case 'linux':
|
|
146
|
+
return await this.execCommand(`systemctl status ${this.serviceName}`);
|
|
147
|
+
case 'darwin':
|
|
148
|
+
return await this.execCommand(`launchctl list | grep ${this.serviceName}`);
|
|
149
|
+
default:
|
|
150
|
+
throw new Error(`不支持的平台: ${this.platform}`);
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
this.log?.error(`[ServiceManager] 查看状态失败: ${err.message}`);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Windows 服务安装
|
|
159
|
+
async installWindows() {
|
|
160
|
+
// 使用 node-windows 或手动创建服务
|
|
161
|
+
const nodeWindowsPath = path.join(__dirname, '..', '..', 'node_modules', 'node-windows');
|
|
162
|
+
|
|
163
|
+
if (!fs.existsSync(nodeWindowsPath)) {
|
|
164
|
+
this.log?.warn('[ServiceManager] node-windows 未安装,尝试安装...');
|
|
165
|
+
await this.execCommand('npm install node-windows --save');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const Service = require('node-windows').Service;
|
|
169
|
+
const svc = new Service({
|
|
170
|
+
name: this.serviceName,
|
|
171
|
+
description: this.serviceDesc,
|
|
172
|
+
script: this.scriptPath,
|
|
173
|
+
nodeOptions: ['--harmony', '--max_old_space_size=4096']
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
svc.on('install', () => {
|
|
178
|
+
this.log?.info('[ServiceManager] Windows 服务安装成功');
|
|
179
|
+
svc.start();
|
|
180
|
+
resolve(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
svc.on('error', (err) => {
|
|
184
|
+
this.log?.error(`[ServiceManager] Windows 服务安装失败: ${err.message}`);
|
|
185
|
+
reject(err);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
svc.install();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Linux 服务安装 (systemd)
|
|
193
|
+
async installLinux() {
|
|
194
|
+
const serviceFile = `/etc/systemd/system/${this.serviceName}.service`;
|
|
195
|
+
const serviceContent = `[Unit]
|
|
196
|
+
Description=${this.serviceDesc}
|
|
197
|
+
After=network.target
|
|
198
|
+
|
|
199
|
+
[Service]
|
|
200
|
+
Type=simple
|
|
201
|
+
User=root
|
|
202
|
+
ExecStart=/usr/bin/node ${this.scriptPath}
|
|
203
|
+
Restart=always
|
|
204
|
+
RestartSec=10
|
|
205
|
+
Environment=NODE_ENV=production
|
|
206
|
+
|
|
207
|
+
[Install]
|
|
208
|
+
WantedBy=multi-user.target
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
fs.writeFileSync(serviceFile, serviceContent);
|
|
212
|
+
await this.execCommand('systemctl daemon-reload');
|
|
213
|
+
await this.execCommand(`systemctl enable ${this.serviceName}`);
|
|
214
|
+
await this.execCommand(`systemctl start ${this.serviceName}`);
|
|
215
|
+
|
|
216
|
+
this.log?.info('[ServiceManager] Linux 服务安装成功');
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// macOS 服务安装 (launchd)
|
|
221
|
+
async installMacOS() {
|
|
222
|
+
const plistFile = `/Library/LaunchDaemons/${this.serviceName}.plist`;
|
|
223
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
224
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
225
|
+
<plist version="1.0">
|
|
226
|
+
<dict>
|
|
227
|
+
<key>Label</key>
|
|
228
|
+
<string>${this.serviceName}</string>
|
|
229
|
+
<key>ProgramArguments</key>
|
|
230
|
+
<array>
|
|
231
|
+
<string>/usr/local/bin/node</string>
|
|
232
|
+
<string>${this.scriptPath}</string>
|
|
233
|
+
</array>
|
|
234
|
+
<key>RunAtLoad</key>
|
|
235
|
+
<true/>
|
|
236
|
+
<key>KeepAlive</key>
|
|
237
|
+
<true/>
|
|
238
|
+
<key>StandardOutPath</key>
|
|
239
|
+
<string>/var/log/${this.serviceName}.log</string>
|
|
240
|
+
<key>StandardErrorPath</key>
|
|
241
|
+
<string>/var/log/${this.serviceName}.error.log</string>
|
|
242
|
+
</dict>
|
|
243
|
+
</plist>`;
|
|
244
|
+
|
|
245
|
+
fs.writeFileSync(plistFile, plistContent);
|
|
246
|
+
await this.execCommand(`launchctl load ${plistFile}`);
|
|
247
|
+
await this.execCommand(`launchctl start ${this.serviceName}`);
|
|
248
|
+
|
|
249
|
+
this.log?.info('[ServiceManager] macOS 服务安装成功');
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Windows 服务卸载
|
|
254
|
+
async uninstallWindows() {
|
|
255
|
+
const Service = require('node-windows').Service;
|
|
256
|
+
const svc = new Service({
|
|
257
|
+
name: this.serviceName,
|
|
258
|
+
script: this.scriptPath
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
svc.on('uninstall', () => {
|
|
263
|
+
this.log?.info('[ServiceManager] Windows 服务卸载成功');
|
|
264
|
+
resolve(true);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
svc.on('error', (err) => {
|
|
268
|
+
this.log?.error(`[ServiceManager] Windows 服务卸载失败: ${err.message}`);
|
|
269
|
+
reject(err);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
svc.uninstall();
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Linux 服务卸载
|
|
277
|
+
async uninstallLinux() {
|
|
278
|
+
await this.execCommand(`systemctl stop ${this.serviceName}`);
|
|
279
|
+
await this.execCommand(`systemctl disable ${this.serviceName}`);
|
|
280
|
+
const serviceFile = `/etc/systemd/system/${this.serviceName}.service`;
|
|
281
|
+
if (fs.existsSync(serviceFile)) {
|
|
282
|
+
fs.unlinkSync(serviceFile);
|
|
283
|
+
}
|
|
284
|
+
await this.execCommand('systemctl daemon-reload');
|
|
285
|
+
|
|
286
|
+
this.log?.info('[ServiceManager] Linux 服务卸载成功');
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// macOS 服务卸载
|
|
291
|
+
async uninstallMacOS() {
|
|
292
|
+
const plistFile = `/Library/LaunchDaemons/${this.serviceName}.plist`;
|
|
293
|
+
await this.execCommand(`launchctl stop ${this.serviceName}`);
|
|
294
|
+
await this.execCommand(`launchctl unload ${plistFile}`);
|
|
295
|
+
if (fs.existsSync(plistFile)) {
|
|
296
|
+
fs.unlinkSync(plistFile);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this.log?.info('[ServiceManager] macOS 服务卸载成功');
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 执行系统命令
|
|
304
|
+
execCommand(command) {
|
|
305
|
+
return new Promise((resolve, reject) => {
|
|
306
|
+
exec(command, (error, stdout, stderr) => {
|
|
307
|
+
if (error) {
|
|
308
|
+
reject(error);
|
|
309
|
+
} else {
|
|
310
|
+
resolve(stdout || stderr);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = {
|
|
318
|
+
ServiceManager
|
|
319
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 结构化消息路由器
|
|
3
|
+
*
|
|
4
|
+
* 由于不能修改 rongcloud/ 文件夹的代码,此模块在 worker.js 中拦截消息,
|
|
5
|
+
* 在传给 MessageHandler 之前解析结构化消息并路由到正确的处理器。
|
|
6
|
+
*
|
|
7
|
+
* 问题背景:
|
|
8
|
+
* - rongcloud-client.js 现在会传递所有消息(SYSTEM_MSG_TYPES 已清空)
|
|
9
|
+
* - 但 message-handler.js 的 getMessageType() 只检查 content 是否以 '/' 开头
|
|
10
|
+
* - 结构化消息(如 command)的 content 是 JSON 字符串,不以 '/' 开头
|
|
11
|
+
* - 导致所有结构化消息被当作 NORMAL 消息处理
|
|
12
|
+
*
|
|
13
|
+
* 解决方案:
|
|
14
|
+
* - 在 worker.js 中包装 MessageHandler.handleMessage()
|
|
15
|
+
* - 在消息到达业务处理器之前,解析 JSON 并检查 msg_type
|
|
16
|
+
* - 根据 msg_type 路由到正确的处理器
|
|
17
|
+
*/
|
|
18
|
+
const { RongyunMessageTypeEnum } = require('./rongyun-message-types');
|
|
19
|
+
|
|
20
|
+
class StructuredMessageRouter {
|
|
21
|
+
constructor(config, log) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.log = log;
|
|
24
|
+
this.handlers = new Map();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 注册消息处理器
|
|
29
|
+
* @param {string} msgType - 消息类型(来自 RongyunMessageTypeEnum)
|
|
30
|
+
* @param {Function} handler - 处理函数 async (parsedMessage) => result
|
|
31
|
+
*/
|
|
32
|
+
registerHandler(msgType, handler) {
|
|
33
|
+
this.handlers.set(msgType, handler);
|
|
34
|
+
this.log?.info(`[StructuredMessageRouter] 注册处理器: ${msgType}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 解析并路由消息
|
|
39
|
+
* @param {Object} msg - rongcloud-client.js 传来的消息对象
|
|
40
|
+
* @returns {Object|null} - 如果是结构化消息,返回解析后的对象;否则返回 null
|
|
41
|
+
*/
|
|
42
|
+
async routeMessage(msg) {
|
|
43
|
+
if (!msg || !msg.content) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 尝试解析 JSON
|
|
48
|
+
let parsed = null;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(msg.content);
|
|
51
|
+
} catch {
|
|
52
|
+
// 不是 JSON,可能是普通文本消息
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 检查是否是结构化协议消息
|
|
57
|
+
if (!parsed.msg_type) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.log?.info(`[StructuredMessageRouter] 收到结构化消息: type=${parsed.msg_type}, from=${parsed.source_im_id}`);
|
|
62
|
+
|
|
63
|
+
// 忽略自己发送的消息
|
|
64
|
+
if (parsed.source_im_id === this.config.accountId) {
|
|
65
|
+
this.log?.info(`[StructuredMessageRouter] 忽略自己发送的消息`);
|
|
66
|
+
return { handled: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 查找处理器
|
|
70
|
+
const handler = this.handlers.get(parsed.msg_type);
|
|
71
|
+
if (handler) {
|
|
72
|
+
try {
|
|
73
|
+
// 解析 content 字段(它本身可能是 JSON 字符串)
|
|
74
|
+
let innerContent = parsed.content;
|
|
75
|
+
if (typeof innerContent === 'string') {
|
|
76
|
+
try {
|
|
77
|
+
innerContent = JSON.parse(innerContent);
|
|
78
|
+
} catch {
|
|
79
|
+
// 保持字符串
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const messageData = {
|
|
84
|
+
...parsed,
|
|
85
|
+
content: innerContent,
|
|
86
|
+
rawMessage: msg,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
await handler(messageData);
|
|
90
|
+
return { handled: true };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
this.log?.error(`[StructuredMessageRouter] 处理器异常: ${err.message}`);
|
|
93
|
+
return { handled: true, error: err.message };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 没有注册处理器,返回 null 让上层处理
|
|
98
|
+
this.log?.warn(`[StructuredMessageRouter] 未找到处理器: ${parsed.msg_type}`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 检查消息是否是结构化消息
|
|
104
|
+
*/
|
|
105
|
+
isStructuredMessage(msg) {
|
|
106
|
+
if (!msg || !msg.content) return false;
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(msg.content);
|
|
109
|
+
return !!parsed.msg_type;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
StructuredMessageRouter
|
|
118
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 环境模拟 (Polyfill)
|
|
4
|
+
* 让融云 Web SDK (@rongcloud/imlib-next) 能在 Node.js 环境中运行
|
|
5
|
+
*/
|
|
6
|
+
require("fake-indexeddb/auto");
|
|
7
|
+
|
|
8
|
+
const { JSDOM } = require("jsdom");
|
|
9
|
+
const WebSocket = require("ws");
|
|
10
|
+
const { Blob, File } = require("node:buffer");
|
|
11
|
+
const crypto = require("crypto");
|
|
12
|
+
|
|
13
|
+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
14
|
+
url: 'http://localhost',
|
|
15
|
+
pretendToBeVisual: true,
|
|
16
|
+
resources: 'usable',
|
|
17
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const window = dom.window;
|
|
21
|
+
const g = global;
|
|
22
|
+
const win = window;
|
|
23
|
+
|
|
24
|
+
function defineWinProp(key, value) {
|
|
25
|
+
try {
|
|
26
|
+
Object.defineProperty(win, key, {
|
|
27
|
+
value: value,
|
|
28
|
+
writable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
enumerable: true
|
|
31
|
+
});
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function defineGlobalProp(key, value) {
|
|
36
|
+
try {
|
|
37
|
+
Object.defineProperty(g, key, {
|
|
38
|
+
value: value,
|
|
39
|
+
writable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
enumerable: true
|
|
42
|
+
});
|
|
43
|
+
} catch {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (g.indexedDB) {
|
|
47
|
+
defineWinProp('indexedDB', g.indexedDB);
|
|
48
|
+
defineWinProp('IDBKeyRange', g.IDBKeyRange);
|
|
49
|
+
defineWinProp('IDBRequest', g.IDBRequest);
|
|
50
|
+
defineWinProp('IDBDatabase', g.IDBDatabase);
|
|
51
|
+
defineWinProp('IDBTransaction', g.IDBTransaction);
|
|
52
|
+
defineWinProp('IDBCursor', g.IDBCursor);
|
|
53
|
+
defineWinProp('IDBIndex', g.IDBIndex);
|
|
54
|
+
defineWinProp('IDBFactory', g.IDBFactory);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const storageMock = {
|
|
58
|
+
getItem: (k) => null,
|
|
59
|
+
setItem: (k, v) => {},
|
|
60
|
+
removeItem: (k) => {},
|
|
61
|
+
clear: () => {},
|
|
62
|
+
length: 0,
|
|
63
|
+
key: (i) => null
|
|
64
|
+
};
|
|
65
|
+
defineWinProp('localStorage', storageMock);
|
|
66
|
+
defineWinProp('sessionStorage', storageMock);
|
|
67
|
+
defineGlobalProp('localStorage', storageMock);
|
|
68
|
+
defineGlobalProp('sessionStorage', storageMock);
|
|
69
|
+
|
|
70
|
+
if (!win.crypto) {
|
|
71
|
+
defineWinProp('crypto', crypto.webcrypto);
|
|
72
|
+
}
|
|
73
|
+
if (!g.crypto) {
|
|
74
|
+
defineGlobalProp('crypto', crypto.webcrypto);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
defineWinProp('onLine', true);
|
|
78
|
+
defineWinProp('language', 'zh-CN');
|
|
79
|
+
defineGlobalProp('navigator', win.navigator);
|
|
80
|
+
|
|
81
|
+
defineGlobalProp('WebSocket', WebSocket);
|
|
82
|
+
defineGlobalProp('XMLHttpRequest', win.XMLHttpRequest);
|
|
83
|
+
defineGlobalProp('window', win);
|
|
84
|
+
defineGlobalProp('document', win.document);
|
|
85
|
+
defineGlobalProp('location', win.location);
|
|
86
|
+
|
|
87
|
+
if (!win.Blob) defineWinProp('Blob', Blob);
|
|
88
|
+
if (!win.File) defineWinProp('File', File);
|
|
89
|
+
if (!win.URL) defineWinProp('URL', { createObjectURL: () => '', revokeObjectURL: () => {} });
|
|
90
|
+
|
|
91
|
+
if (!win.requestAnimationFrame) defineWinProp('requestAnimationFrame', (cb) => setTimeout(cb, 16));
|
|
92
|
+
if (!win.cancelAnimationFrame) defineWinProp('cancelAnimationFrame', (id) => clearTimeout(id));
|
|
93
|
+
if (!win.performance) defineWinProp('performance', { now: () => Date.now() });
|
|
94
|
+
|
|
95
|
+
module.exports = { window };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const { MessageType } = require('./types');
|
|
2
|
+
const { MessageHandler } = require('./message-handler');
|
|
3
|
+
const { OpenClawClient } = require('./openclaw-client');
|
|
4
|
+
const { RongCloudClient, ConversationType } = require('./rongcloud-client');
|
|
5
|
+
const { ensurePluginsAllow } = require('./openclaw-config');
|
|
6
|
+
|
|
7
|
+
function createRongCloudModule(config, sendFn, log) {
|
|
8
|
+
return new MessageHandler(config, sendFn, log);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
MessageType,
|
|
13
|
+
MessageHandler,
|
|
14
|
+
OpenClawClient,
|
|
15
|
+
RongCloudClient,
|
|
16
|
+
ConversationType,
|
|
17
|
+
ensurePluginsAllow,
|
|
18
|
+
createRongCloudModule
|
|
19
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const { MessageType } = require('./types');
|
|
2
|
+
const { OpenClawClient } = require('./openclaw-client');
|
|
3
|
+
const { handleNormalMessage } = require('../modules/normal-message-handler');
|
|
4
|
+
const MENTION_REGEX = /@(claw_[a-zA-Z0-9]+)/g;
|
|
5
|
+
|
|
6
|
+
class MessageHandler {
|
|
7
|
+
constructor(config, sendFn, log, sendReadReceiptFn) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.sendFn = sendFn;
|
|
10
|
+
this.log = log;
|
|
11
|
+
this.sendReadReceiptFn = sendReadReceiptFn;
|
|
12
|
+
this.openclawClient = new OpenClawClient(log);
|
|
13
|
+
this.nodeId = config.accountId || '';
|
|
14
|
+
this.handleNormalMessage = handleNormalMessage;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
extractMentions(content) {
|
|
18
|
+
const mentions = [];
|
|
19
|
+
let match;
|
|
20
|
+
while ((match = MENTION_REGEX.exec(content)) !== null) {
|
|
21
|
+
mentions.push(match[1]);
|
|
22
|
+
}
|
|
23
|
+
MENTION_REGEX.lastIndex = 0;
|
|
24
|
+
return mentions;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
shouldHandleMessage(msg) {
|
|
28
|
+
if (msg.isOffLineMessage) {
|
|
29
|
+
this.log?.info('[MessageHandler] 忽略离线消息');
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const allowedTypes = ['RC:TxtMsg', 'claw'];
|
|
34
|
+
if (!allowedTypes.includes(msg.messageType)) {
|
|
35
|
+
this.log?.info(`[MessageHandler] 忽略非文本消息: ${msg.messageType}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (msg.senderUserId === this.config.accountId) {
|
|
40
|
+
this.log?.info('[MessageHandler] 忽略自己发送的消息');
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const mentions = this.extractMentions(msg.content);
|
|
45
|
+
|
|
46
|
+
if (mentions.length > 0) {
|
|
47
|
+
if (!mentions.includes(this.nodeId)) {
|
|
48
|
+
this.log?.info(`[MessageHandler] 消息 @${mentions.join(', ')},非本节点(${this.nodeId}),忽略`);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
this.log?.info(`[MessageHandler] 消息提及本节点(${this.nodeId}),处理`);
|
|
52
|
+
} else {
|
|
53
|
+
this.log?.info(`[MessageHandler] 消息未指定节点,本节点(${this.nodeId})处理`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async handleMessage(msg) {
|
|
60
|
+
if (!this.shouldHandleMessage(msg)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const type = this.getMessageType(msg);
|
|
66
|
+
this.log?.info(`[MessageHandler] 收到消息 from=${msg.senderUserId}, type=${type}, content=${msg.content.substring(0, 50)}`);
|
|
67
|
+
if (msg.messageType === 'claw') {
|
|
68
|
+
this.log?.info(`收到龙虾消息,交由 OpenClawClient 处理`);
|
|
69
|
+
await this.handleClaw(msg);
|
|
70
|
+
} else {
|
|
71
|
+
await this.handleNormalMessage(msg);
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
this.log?.error(`[MessageHandler] 处理消息异常: ${err.message}`);
|
|
75
|
+
const targetId = msg.conversationType === 3 ? msg.targetId : msg.senderUserId;
|
|
76
|
+
await this.sendFn(targetId, `处理失败: ${err.message}`, msg.conversationType);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getMessageType(msg) {
|
|
81
|
+
if (msg.messageType === 'claw') {
|
|
82
|
+
return MessageType.CLAW;
|
|
83
|
+
}
|
|
84
|
+
if (msg.content && msg.content.startsWith('/')) {
|
|
85
|
+
return MessageType.COMMAND;
|
|
86
|
+
}
|
|
87
|
+
return MessageType.NORMAL;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getReplyTarget(msg) {
|
|
91
|
+
if (msg.conversationType === 3) {
|
|
92
|
+
return msg.targetId;
|
|
93
|
+
}
|
|
94
|
+
return msg.senderUserId;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async handleCommand(msg) {
|
|
98
|
+
const payload = this.parseCommand(msg.content, msg.senderUserId);
|
|
99
|
+
this.log?.info(`[MessageHandler] 指令消息: command=${payload.command}, args=${payload.args.join(', ')}`);
|
|
100
|
+
|
|
101
|
+
let reply;
|
|
102
|
+
if (this.config.onCommand) {
|
|
103
|
+
try {
|
|
104
|
+
reply = await this.config.onCommand(payload);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
this.log?.error(`[MessageHandler] 指令处理回调异常: ${err.message}`);
|
|
107
|
+
reply = `指令执行异常: ${err.message}`;
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
reply = `指令 "${payload.command}" 暂未实现`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const targetId = this.getReplyTarget(msg);
|
|
114
|
+
await this.sendFn(targetId, reply, msg.conversationType);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async handleClaw(msg) {
|
|
118
|
+
// 先发送已读回执(表示消息已被接收和处理)
|
|
119
|
+
if (this.sendReadReceiptFn) {
|
|
120
|
+
try {
|
|
121
|
+
await this.sendReadReceiptFn(msg);
|
|
122
|
+
this.log?.info(`[MessageHandler] 已读回执已发送`);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
this.log?.error(`[MessageHandler] 发送已读回执失败: ${err.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const reply = await this.openclawClient.chat(msg.content, msg.senderUserId);
|
|
129
|
+
this.log?.info(`[MessageHandler] AI 回复: ${reply.substring(0, 50)}...`);
|
|
130
|
+
|
|
131
|
+
const targetId = this.getReplyTarget(msg);
|
|
132
|
+
await this.sendFn(targetId, reply, msg.conversationType);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
parseCommand(raw, senderId) {
|
|
136
|
+
const trimmed = raw.slice(1).trim();
|
|
137
|
+
const parts = trimmed.split(/\s+/);
|
|
138
|
+
return {
|
|
139
|
+
command: parts[0] || '',
|
|
140
|
+
args: parts.slice(1),
|
|
141
|
+
rawMessage: raw,
|
|
142
|
+
senderId
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { MessageHandler };
|