claw-subagent-service 0.0.29 → 0.0.31
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/package.json
CHANGED
package/scripts/sync-local.js
CHANGED
|
@@ -10,13 +10,12 @@
|
|
|
10
10
|
* npm run sync (推荐)
|
|
11
11
|
* node scripts/sync-local.js
|
|
12
12
|
*
|
|
13
|
-
* 注意: Windows
|
|
13
|
+
* 注意: Windows 下若服务正在运行,会自动卸载服务、同步后再重新安装。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
const { execSync
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const fs = require('fs');
|
|
19
|
-
const os = require('os');
|
|
20
19
|
|
|
21
20
|
const isWindows = process.platform === 'win32';
|
|
22
21
|
const SERVICE_NAME = 'claw-subagent-service';
|
|
@@ -47,7 +46,7 @@ function getGlobalPackageDir() {
|
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
function
|
|
49
|
+
function stopAndUninstallService() {
|
|
51
50
|
if (!isWindows) {
|
|
52
51
|
try {
|
|
53
52
|
execSync('systemctl stop claw-subagent-service 2>/dev/null', {
|
|
@@ -59,7 +58,7 @@ function stopService() {
|
|
|
59
58
|
return;
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
console.log('🛑
|
|
61
|
+
console.log('🛑 正在停止并卸载 Windows 服务(释放文件锁)...');
|
|
63
62
|
|
|
64
63
|
// 1. 停止服务
|
|
65
64
|
try {
|
|
@@ -70,7 +69,7 @@ function stopService() {
|
|
|
70
69
|
console.log(' ✅ 服务已停止');
|
|
71
70
|
} catch {}
|
|
72
71
|
|
|
73
|
-
// 2. 杀掉 wrapper
|
|
72
|
+
// 2. 杀掉 wrapper 进程(node-windows 生成的 .exe)
|
|
74
73
|
try {
|
|
75
74
|
execSync('taskkill /f /im "claw-subagent-service.exe" 2>nul', {
|
|
76
75
|
stdio: 'ignore',
|
|
@@ -87,18 +86,36 @@ function stopService() {
|
|
|
87
86
|
);
|
|
88
87
|
} catch {}
|
|
89
88
|
|
|
90
|
-
// 4.
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
// 4. 通过 wmic 按命令行匹配杀掉相关 node 进程
|
|
90
|
+
try {
|
|
91
|
+
execSync('wmic process where "name=\'node.exe\' and commandline like \'%claw-subagent%\'" delete 2>nul', {
|
|
92
|
+
stdio: 'ignore',
|
|
93
|
+
timeout: 10000,
|
|
94
|
+
});
|
|
95
|
+
console.log(' ✅ 相关 node 进程已清理');
|
|
96
|
+
} catch {}
|
|
97
|
+
|
|
98
|
+
// 5. 从注册表删除服务(彻底释放 node-windows wrapper 的文件锁)
|
|
99
|
+
try {
|
|
100
|
+
execSync('sc.exe delete "claw-subagent-service" 2>nul', {
|
|
101
|
+
stdio: 'ignore',
|
|
102
|
+
timeout: 10000,
|
|
103
|
+
});
|
|
104
|
+
console.log(' ✅ 服务已从注册表删除');
|
|
105
|
+
} catch {}
|
|
106
|
+
|
|
107
|
+
// 6. 等待 Windows 回收文件句柄(关键!node-windows wrapper 需要足够时间释放)
|
|
108
|
+
console.log(' ⏳ 等待系统释放文件锁(10秒)...');
|
|
109
|
+
execSync('ping -n 11 127.0.0.1 >nul', { stdio: 'ignore', windowsHide: true });
|
|
93
110
|
}
|
|
94
111
|
|
|
95
|
-
function
|
|
96
|
-
console.log('🚀
|
|
112
|
+
function installAndStartService() {
|
|
113
|
+
console.log('🚀 正在安装并启动服务...');
|
|
97
114
|
try {
|
|
98
115
|
if (isWindows) {
|
|
99
|
-
execSync('
|
|
116
|
+
execSync('claw-subagent-service --install', {
|
|
100
117
|
stdio: 'inherit',
|
|
101
|
-
timeout:
|
|
118
|
+
timeout: 60000,
|
|
102
119
|
windowsHide: true,
|
|
103
120
|
});
|
|
104
121
|
} else {
|
|
@@ -107,23 +124,23 @@ function startService() {
|
|
|
107
124
|
timeout: 15000,
|
|
108
125
|
});
|
|
109
126
|
}
|
|
110
|
-
console.log('✅
|
|
127
|
+
console.log('✅ 服务已安装/启动');
|
|
111
128
|
} catch (e) {
|
|
112
|
-
console.error('⚠️
|
|
129
|
+
console.error('⚠️ 服务安装/启动失败:', e.message);
|
|
113
130
|
console.log(' 请手动运行: claw-subagent-service --install');
|
|
114
131
|
}
|
|
115
132
|
}
|
|
116
133
|
|
|
117
|
-
function
|
|
134
|
+
function isServiceInstalled() {
|
|
118
135
|
try {
|
|
119
136
|
if (isWindows) {
|
|
120
|
-
execSync('sc query "claw-subagent-service" | findstr
|
|
137
|
+
execSync('sc query "claw-subagent-service" | findstr SERVICE_NAME >nul', {
|
|
121
138
|
stdio: 'ignore',
|
|
122
139
|
windowsHide: true,
|
|
123
140
|
});
|
|
124
141
|
return true;
|
|
125
142
|
} else {
|
|
126
|
-
execSync('systemctl is-active --quiet claw-subagent-service', {
|
|
143
|
+
execSync('systemctl is-active --quiet claw-subagent-service || systemctl is-enabled --quiet claw-subagent-service', {
|
|
127
144
|
stdio: 'ignore',
|
|
128
145
|
});
|
|
129
146
|
return true;
|
|
@@ -135,21 +152,25 @@ function isServiceRunning() {
|
|
|
135
152
|
|
|
136
153
|
function copyDirSync(src, dest) {
|
|
137
154
|
if (isWindows) {
|
|
138
|
-
//
|
|
139
|
-
if (fs.existsSync(dest)) {
|
|
140
|
-
|
|
141
|
-
fs.rmSync(dest, { recursive: true, force: true, maxRetries: 5, retryDelay: 500 });
|
|
142
|
-
} catch (e) {
|
|
143
|
-
console.error(` ❌ 无法清空目录: ${dest}`);
|
|
144
|
-
console.error(` 错误: ${e.message}`);
|
|
145
|
-
console.error(` 请确保服务已完全停止,或尝试以管理员身份运行`);
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
155
|
+
// 确保目标目录存在
|
|
156
|
+
if (!fs.existsSync(dest)) {
|
|
157
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
148
158
|
}
|
|
149
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
150
159
|
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
// 使用 robocopy,遇到锁定的文件时跳过(/R:0 /W:0 表示不重试)
|
|
161
|
+
const cmd = `robocopy "${src}" "${dest}" /E /R:0 /W:0 /NJH /NJS /NDL /NC /NS /NFL`;
|
|
162
|
+
try {
|
|
163
|
+
execSync(cmd, { encoding: 'utf-8', windowsHide: true });
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// robocopy 返回码含义:
|
|
166
|
+
// 0 = 成功,1 = 有文件被跳过,2 = 有额外文件,4 = 有不匹配,8 = 有错误,16 = 严重错误
|
|
167
|
+
const status = e.status || 0;
|
|
168
|
+
if (status === 1) {
|
|
169
|
+
console.log(' ℹ️ 部分文件被跳过(可能正在使用)');
|
|
170
|
+
} else if (status >= 8) {
|
|
171
|
+
throw new Error(`robocopy 失败 (code ${status})`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
153
174
|
} else {
|
|
154
175
|
if (!fs.existsSync(dest)) {
|
|
155
176
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -222,11 +243,11 @@ function main() {
|
|
|
222
243
|
console.log('🦞 claw-subagent-service - 本地同步\n');
|
|
223
244
|
|
|
224
245
|
const targetDir = getGlobalPackageDir();
|
|
225
|
-
const
|
|
246
|
+
const wasInstalled = isServiceInstalled();
|
|
226
247
|
|
|
227
|
-
if (
|
|
228
|
-
console.log('ℹ️
|
|
229
|
-
|
|
248
|
+
if (wasInstalled) {
|
|
249
|
+
console.log('ℹ️ 检测到服务已安装,先卸载以释放文件锁\n');
|
|
250
|
+
stopAndUninstallService();
|
|
230
251
|
}
|
|
231
252
|
|
|
232
253
|
syncFiles(targetDir);
|
|
@@ -235,12 +256,12 @@ function main() {
|
|
|
235
256
|
console.log('\n✅ 同步完成!');
|
|
236
257
|
console.log(` 目标目录: ${targetDir}`);
|
|
237
258
|
|
|
238
|
-
if (
|
|
239
|
-
console.log('\n🔄
|
|
240
|
-
|
|
259
|
+
if (wasInstalled) {
|
|
260
|
+
console.log('\n🔄 正在重新安装并启动服务...');
|
|
261
|
+
installAndStartService();
|
|
241
262
|
} else {
|
|
242
263
|
console.log('\n💡 提示:');
|
|
243
|
-
console.log('
|
|
264
|
+
console.log(' 服务未安装,如需安装请运行:');
|
|
244
265
|
console.log(' claw-subagent-service --install');
|
|
245
266
|
}
|
|
246
267
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 普通消息处理器 - 接收 rongcloud 转发的普通消息并调用 AI 服务
|
|
3
|
-
*
|
|
4
|
-
* 被调用位置:rongcloud/message-handler.js
|
|
3
|
+
*
|
|
4
|
+
* 被调用位置:rongcloud/message-handler.js
|
|
5
5
|
* 调用方式:await this.handleNormalMessage(msg)
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* @param {Object} msg - 消息对象
|
|
8
8
|
* @param {string} msg.content - 消息内容
|
|
9
9
|
* @param {string} msg.senderUserId - 发送者ID
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* @param {number} msg.conversationType - 会话类型 (1=私聊, 3=群聊)
|
|
12
12
|
* @returns {string} 回复内容
|
|
13
13
|
*/
|
|
14
|
-
const {
|
|
14
|
+
const { OpenClawClient } = require('../rongcloud/openclaw-client');
|
|
15
|
+
|
|
16
|
+
const openclawClient = new OpenClawClient(console);
|
|
15
17
|
|
|
16
18
|
async function handleNormalMessage(msg) {
|
|
17
19
|
console.log(`[NormalMessageHandler] 收到普通消息:`, {
|
|
@@ -21,34 +23,14 @@ async function handleNormalMessage(msg) {
|
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
try {
|
|
24
|
-
// 使用 senderUserId 作为 session ID,确保每个用户有独立的会话
|
|
25
|
-
const sessionId = msg.senderUserId || 'default';
|
|
26
26
|
const content = msg.content;
|
|
27
|
-
|
|
28
27
|
if (!content || !content.trim()) {
|
|
29
28
|
return '消息内容为空';
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await forwardChatMessage(
|
|
36
|
-
sessionId,
|
|
37
|
-
content,
|
|
38
|
-
async (delta) => {
|
|
39
|
-
// 收集响应片段
|
|
40
|
-
fullResponse += delta;
|
|
41
|
-
},
|
|
42
|
-
(level, message) => {
|
|
43
|
-
// 日志回调
|
|
44
|
-
console.log(`[CHAT-${level}] ${message}`);
|
|
45
|
-
},
|
|
46
|
-
600000 // 10分钟超时
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
console.log(`[NormalMessageHandler] AI 回复: ${fullResponse.substring(0, 50)}...`);
|
|
50
|
-
return fullResponse;
|
|
51
|
-
|
|
31
|
+
const reply = await openclawClient.chat(content, msg.senderUserId);
|
|
32
|
+
console.log(`[NormalMessageHandler] AI 回复: ${reply.substring(0, 50)}...`);
|
|
33
|
+
return reply;
|
|
52
34
|
} catch (err) {
|
|
53
35
|
console.error(`[NormalMessageHandler] 处理异常:`, err.message);
|
|
54
36
|
return `抱歉,处理消息时出错: ${err.message}`;
|
|
@@ -41,7 +41,25 @@ class MessageHandler {
|
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
// claw 控制消息不需要 @mention 过滤
|
|
45
|
+
if (msg.messageType === 'claw') {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 优先从融云 mentionedInfo 提取被@用户列表(用户界面 @昵称,但融云底层存的是 userId)
|
|
50
|
+
let mentions = [];
|
|
51
|
+
if (msg.mentionedInfo && Array.isArray(msg.mentionedInfo.userIdList)) {
|
|
52
|
+
mentions = msg.mentionedInfo.userIdList;
|
|
53
|
+
if (mentions.length > 0) {
|
|
54
|
+
this.log?.info(`[MessageHandler] 融云 mentionedInfo: ${mentions.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 兜底:从文本内容中正则匹配 @claw_xxx
|
|
59
|
+
if (mentions.length === 0) {
|
|
60
|
+
const textContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
|
|
61
|
+
mentions = this.extractMentions(textContent);
|
|
62
|
+
}
|
|
45
63
|
|
|
46
64
|
if (mentions.length > 0) {
|
|
47
65
|
if (!mentions.includes(this.nodeId)) {
|
|
@@ -49,8 +67,11 @@ class MessageHandler {
|
|
|
49
67
|
return false;
|
|
50
68
|
}
|
|
51
69
|
this.log?.info(`[MessageHandler] 消息提及本节点(${this.nodeId}),处理`);
|
|
70
|
+
} else if (msg.conversationType === 3) {
|
|
71
|
+
this.log?.info(`[MessageHandler] 群聊消息未 @ 本节点(${this.nodeId}),忽略`);
|
|
72
|
+
return false;
|
|
52
73
|
} else {
|
|
53
|
-
this.log?.info(`[MessageHandler]
|
|
74
|
+
this.log?.info(`[MessageHandler] 单聊消息未指定节点,本节点(${this.nodeId})处理`);
|
|
54
75
|
}
|
|
55
76
|
|
|
56
77
|
return true;
|
|
@@ -63,12 +84,17 @@ class MessageHandler {
|
|
|
63
84
|
|
|
64
85
|
try {
|
|
65
86
|
const type = this.getMessageType(msg);
|
|
66
|
-
|
|
87
|
+
const logContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
|
|
88
|
+
this.log?.info(`[MessageHandler] 收到消息 from=${msg.senderUserId}, type=${type}, content=${logContent.substring(0, 50)}`);
|
|
67
89
|
if (msg.messageType === 'claw') {
|
|
68
90
|
this.log?.info(`收到龙虾消息,交由 OpenClawClient 处理`);
|
|
69
91
|
await this.handleClaw(msg);
|
|
70
92
|
} else {
|
|
71
|
-
await this.handleNormalMessage(msg);
|
|
93
|
+
const reply = await this.handleNormalMessage(msg);
|
|
94
|
+
if (reply) {
|
|
95
|
+
const targetId = this.getReplyTarget(msg);
|
|
96
|
+
await this.sendFn(targetId, reply, msg.conversationType);
|
|
97
|
+
}
|
|
72
98
|
}
|
|
73
99
|
} catch (err) {
|
|
74
100
|
this.log?.error(`[MessageHandler] 处理消息异常: ${err.message}`);
|
|
@@ -81,7 +107,8 @@ class MessageHandler {
|
|
|
81
107
|
if (msg.messageType === 'claw') {
|
|
82
108
|
return MessageType.CLAW;
|
|
83
109
|
}
|
|
84
|
-
|
|
110
|
+
const text = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
|
|
111
|
+
if (text.startsWith('/')) {
|
|
85
112
|
return MessageType.COMMAND;
|
|
86
113
|
}
|
|
87
114
|
return MessageType.NORMAL;
|
|
@@ -249,53 +249,7 @@ class OpenClawClient {
|
|
|
249
249
|
|
|
250
250
|
this.log?.info(`[OpenClawClient] 准备发送消息到 OpenClaw,from=${fromUser}, message=${message.substring(0, 50)}`);
|
|
251
251
|
|
|
252
|
-
//
|
|
253
|
-
const httpAvailable = await checkPort(18789);
|
|
254
|
-
if (httpAvailable) {
|
|
255
|
-
try {
|
|
256
|
-
const gatewayToken = getGatewayToken();
|
|
257
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
258
|
-
if (gatewayToken) {
|
|
259
|
-
headers['Authorization'] = `Bearer ${gatewayToken}`;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const response = await axios.post(
|
|
263
|
-
'http://127.0.0.1:18789/v1/chat/completions',
|
|
264
|
-
{
|
|
265
|
-
model: 'openclaw:main',
|
|
266
|
-
messages: [
|
|
267
|
-
{ role: 'user', content: message }
|
|
268
|
-
],
|
|
269
|
-
stream: false
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
headers,
|
|
273
|
-
timeout: 120000
|
|
274
|
-
}
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
if (response.status === 200 && response.data) {
|
|
278
|
-
const result = response.data;
|
|
279
|
-
const aiMessage = result.choices?.[0]?.message;
|
|
280
|
-
const content = aiMessage?.content;
|
|
281
|
-
if (content) {
|
|
282
|
-
this.log?.info(`[OpenClawClient] AI 回复: ${content.substring(0, 50)}...`);
|
|
283
|
-
return content;
|
|
284
|
-
}
|
|
285
|
-
this.log?.warn('[OpenClawClient] OpenClaw 返回了空响应');
|
|
286
|
-
return 'OpenClaw 未返回有效响应';
|
|
287
|
-
} else {
|
|
288
|
-
this.log?.error(`[OpenClawClient] OpenClaw 返回错误: ${response.status}`);
|
|
289
|
-
return `OpenClaw 返回错误: ${response.status}`;
|
|
290
|
-
}
|
|
291
|
-
} catch (err) {
|
|
292
|
-
this.log?.error(`[OpenClawClient] HTTP 调用失败: ${err.message}`);
|
|
293
|
-
}
|
|
294
|
-
} else {
|
|
295
|
-
this.log?.info('[OpenClawClient] Gateway HTTP API (18789) 未就绪,直接走 CLI');
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// fallback 到 CLI 调用
|
|
252
|
+
// 直接走 CLI 调用(OpenClaw Gateway 未暴露兼容的 HTTP REST API)
|
|
299
253
|
return this.chatViaCLI(message, fromUser);
|
|
300
254
|
}
|
|
301
255
|
|
|
@@ -317,12 +271,14 @@ class OpenClawClient {
|
|
|
317
271
|
this.log?.info('[OpenClawClient] 已读取到 gateway token');
|
|
318
272
|
}
|
|
319
273
|
|
|
320
|
-
const
|
|
274
|
+
const quoteArg = (s) => `"${s}"`;
|
|
275
|
+
const cmdParts = ['openclaw', 'agent', '-m', quoteArg(escapedMessage), '--session-id', quoteArg(sessionId)];
|
|
321
276
|
if (gatewayToken) {
|
|
322
|
-
|
|
277
|
+
cmdParts.push('--token', quoteArg(gatewayToken));
|
|
323
278
|
}
|
|
279
|
+
const command = cmdParts.join(' ');
|
|
324
280
|
|
|
325
|
-
this.log?.info(`[OpenClawClient] 执行:
|
|
281
|
+
this.log?.info(`[OpenClawClient] 执行: ${command}`);
|
|
326
282
|
|
|
327
283
|
return new Promise((resolve) => {
|
|
328
284
|
let stdout = '';
|
|
@@ -331,7 +287,7 @@ class OpenClawClient {
|
|
|
331
287
|
|
|
332
288
|
// 关键:不设置 OPENCLAW_GATEWAY_URL,避免触发 "gateway url override requires explicit credentials"
|
|
333
289
|
// 让 openclaw agent 通过默认方式自动发现本地 gateway
|
|
334
|
-
const child = spawn(
|
|
290
|
+
const child = spawn(command, {
|
|
335
291
|
shell: true,
|
|
336
292
|
windowsHide: true,
|
|
337
293
|
env: getOpenClawEnv(),
|
|
@@ -128,9 +128,11 @@ class RongCloudClient {
|
|
|
128
128
|
try {
|
|
129
129
|
const msgType = message.messageType;
|
|
130
130
|
let rawContent = message.content;
|
|
131
|
+
let mentionedInfo = null;
|
|
131
132
|
|
|
132
|
-
// 自定义消息 content
|
|
133
|
+
// 自定义消息 content 可能是对象,提取文本内容并保留 mentionedInfo
|
|
133
134
|
if (rawContent && typeof rawContent === 'object') {
|
|
135
|
+
mentionedInfo = rawContent.mentionedInfo || null;
|
|
134
136
|
rawContent = rawContent.content || rawContent.text || JSON.stringify(rawContent);
|
|
135
137
|
}
|
|
136
138
|
|
|
@@ -174,7 +176,8 @@ class RongCloudClient {
|
|
|
174
176
|
messageType: msgType || 'RC:TxtMsg',
|
|
175
177
|
isOffLineMessage: message.isOffLineMessage || false,
|
|
176
178
|
messageUId: message.messageUId || `local-${Date.now()}`,
|
|
177
|
-
sentTime: message.sentTime || Date.now()
|
|
179
|
+
sentTime: message.sentTime || Date.now(),
|
|
180
|
+
mentionedInfo
|
|
178
181
|
};
|
|
179
182
|
|
|
180
183
|
// 并行处理消息,不等待上一条完成(避免 openclaw 长耗时调用阻塞后续消息)
|