claw-subagent-service 0.0.24 → 0.0.26
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
|
@@ -115,21 +115,26 @@ class MessageHandler {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
async handleClaw(msg) {
|
|
118
|
-
|
|
118
|
+
const targetId = this.getReplyTarget(msg);
|
|
119
|
+
|
|
120
|
+
// 发送已读回执(fire-and-forget,不阻塞)
|
|
119
121
|
if (this.sendReadReceiptFn) {
|
|
120
|
-
|
|
121
|
-
await this.sendReadReceiptFn(msg);
|
|
122
|
-
this.log?.info(`[MessageHandler] 已读回执已发送`);
|
|
123
|
-
} catch (err) {
|
|
124
|
-
this.log?.error(`[MessageHandler] 发送已读回执失败: ${err.message}`);
|
|
125
|
-
}
|
|
122
|
+
this.sendReadReceiptFn(msg).catch(() => {});
|
|
126
123
|
}
|
|
127
124
|
|
|
128
|
-
|
|
129
|
-
this.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
// 先回复"正在处理",让用户知道消息已被接收
|
|
126
|
+
this.sendFn(targetId, '🤖 正在思考中,请稍候...', msg.conversationType).catch(() => {});
|
|
127
|
+
|
|
128
|
+
// 后台执行 openclaw,不阻塞消息队列
|
|
129
|
+
this.openclawClient.chat(msg.content, msg.senderUserId)
|
|
130
|
+
.then(reply => {
|
|
131
|
+
this.log?.info(`[MessageHandler] AI 回复: ${reply.substring(0, 50)}...`);
|
|
132
|
+
this.sendFn(targetId, reply, msg.conversationType).catch(() => {});
|
|
133
|
+
})
|
|
134
|
+
.catch(err => {
|
|
135
|
+
this.log?.error(`[MessageHandler] OpenClaw 调用失败: ${err.message}`);
|
|
136
|
+
this.sendFn(targetId, `❌ 处理失败: ${err.message}`, msg.conversationType).catch(() => {});
|
|
137
|
+
});
|
|
133
138
|
}
|
|
134
139
|
|
|
135
140
|
parseCommand(raw, senderId) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
const { spawn } = require('child_process');
|
|
1
|
+
const { spawn, execSync } = require('child_process');
|
|
2
|
+
const net = require('net');
|
|
2
3
|
const os = require('os');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const path = require('path');
|
|
6
|
+
const axios = require('axios');
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* 获取实际用户主目录(SYSTEM 账户下 os.homedir() 返回 systemprofile)
|
|
@@ -31,17 +33,174 @@ function getRealHomeDir() {
|
|
|
31
33
|
return homeDir;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 检测端口是否监听
|
|
38
|
+
*/
|
|
39
|
+
function checkPort(port) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const sock = new net.Socket();
|
|
42
|
+
sock.setTimeout(3000);
|
|
43
|
+
sock.once('connect', () => {
|
|
44
|
+
sock.destroy();
|
|
45
|
+
resolve(true);
|
|
46
|
+
});
|
|
47
|
+
sock.once('error', () => {
|
|
48
|
+
sock.destroy();
|
|
49
|
+
resolve(false);
|
|
50
|
+
});
|
|
51
|
+
sock.once('timeout', () => {
|
|
52
|
+
sock.destroy();
|
|
53
|
+
resolve(false);
|
|
54
|
+
});
|
|
55
|
+
sock.connect(port, '127.0.0.1');
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 启动 OpenClaw gateway
|
|
61
|
+
*/
|
|
62
|
+
function startOpenClawGateway(log) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
log?.info('[OpenClawClient] 正在启动 OpenClaw gateway...');
|
|
65
|
+
|
|
66
|
+
const child = spawn('openclaw', ['gateway'], {
|
|
67
|
+
shell: true,
|
|
68
|
+
windowsHide: true,
|
|
69
|
+
detached: true,
|
|
70
|
+
stdio: 'ignore',
|
|
71
|
+
env: {
|
|
72
|
+
...process.env,
|
|
73
|
+
USERPROFILE: getRealHomeDir(),
|
|
74
|
+
HOME: getRealHomeDir(),
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
child.unref();
|
|
79
|
+
|
|
80
|
+
// 等待 gateway 启动(最多 15 秒)
|
|
81
|
+
let attempts = 0;
|
|
82
|
+
const maxAttempts = 15;
|
|
83
|
+
const interval = setInterval(async () => {
|
|
84
|
+
attempts++;
|
|
85
|
+
const isRunning = await checkPort(18789);
|
|
86
|
+
if (isRunning) {
|
|
87
|
+
clearInterval(interval);
|
|
88
|
+
log?.info('[OpenClawClient] OpenClaw gateway 启动成功');
|
|
89
|
+
resolve(true);
|
|
90
|
+
} else if (attempts >= maxAttempts) {
|
|
91
|
+
clearInterval(interval);
|
|
92
|
+
log?.warn('[OpenClawClient] OpenClaw gateway 启动超时');
|
|
93
|
+
resolve(false);
|
|
94
|
+
}
|
|
95
|
+
}, 1000);
|
|
96
|
+
|
|
97
|
+
child.on('error', (err) => {
|
|
98
|
+
log?.error(`[OpenClawClient] 启动 gateway 失败: ${err.message}`);
|
|
99
|
+
clearInterval(interval);
|
|
100
|
+
resolve(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
34
105
|
class OpenClawClient {
|
|
35
106
|
constructor(log) {
|
|
36
107
|
this.log = log;
|
|
108
|
+
this.gatewayStarting = false;
|
|
109
|
+
this.gatewayStarted = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 确保 OpenClaw gateway 在运行
|
|
114
|
+
*/
|
|
115
|
+
async ensureGatewayRunning() {
|
|
116
|
+
if (this.gatewayStarted) return true;
|
|
117
|
+
|
|
118
|
+
const isRunning = await checkPort(18789);
|
|
119
|
+
if (isRunning) {
|
|
120
|
+
this.log?.info('[OpenClawClient] OpenClaw gateway 已在运行');
|
|
121
|
+
this.gatewayStarted = true;
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 避免并发启动
|
|
126
|
+
if (this.gatewayStarting) {
|
|
127
|
+
this.log?.info('[OpenClawClient] gateway 正在启动中,等待...');
|
|
128
|
+
// 等待最多 20 秒
|
|
129
|
+
for (let i = 0; i < 20; i++) {
|
|
130
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
131
|
+
if (await checkPort(18789)) {
|
|
132
|
+
this.gatewayStarted = true;
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.gatewayStarting = true;
|
|
140
|
+
try {
|
|
141
|
+
const started = await startOpenClawGateway(this.log);
|
|
142
|
+
this.gatewayStarted = started;
|
|
143
|
+
return started;
|
|
144
|
+
} finally {
|
|
145
|
+
this.gatewayStarting = false;
|
|
146
|
+
}
|
|
37
147
|
}
|
|
38
148
|
|
|
39
149
|
async chat(message, fromUser) {
|
|
40
150
|
if (!message || !message.trim()) {
|
|
41
151
|
return '消息内容为空';
|
|
42
152
|
}
|
|
153
|
+
|
|
154
|
+
// 确保 OpenClaw gateway 已启动
|
|
155
|
+
const gatewayReady = await this.ensureGatewayRunning();
|
|
156
|
+
if (!gatewayReady) {
|
|
157
|
+
this.log?.error('[OpenClawClient] OpenClaw gateway 未运行且启动失败');
|
|
158
|
+
return 'OpenClaw gateway 启动失败,请检查 openclaw 是否正确安装';
|
|
159
|
+
}
|
|
160
|
+
|
|
43
161
|
this.log?.info(`[OpenClawClient] 准备发送消息到 OpenClaw,from=${fromUser}, message=${message.substring(0, 50)}`);
|
|
44
162
|
|
|
163
|
+
const gatewayUrl = 'http://127.0.0.1:5678';
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const response = await axios.post(
|
|
167
|
+
`${gatewayUrl}/v1/chat/completions`,
|
|
168
|
+
{
|
|
169
|
+
model: 'claw-local',
|
|
170
|
+
messages: [
|
|
171
|
+
{ role: 'user', content: message }
|
|
172
|
+
],
|
|
173
|
+
stream: false
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
timeout: 120000
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (response.status === 200 && response.data) {
|
|
182
|
+
const result = response.data;
|
|
183
|
+
const aiMessage = result.choices?.[0]?.message;
|
|
184
|
+
const content = aiMessage?.content;
|
|
185
|
+
if (content) {
|
|
186
|
+
this.log?.info(`[OpenClawClient] AI 回复: ${content.substring(0, 50)}...`);
|
|
187
|
+
return content;
|
|
188
|
+
}
|
|
189
|
+
this.log?.warn('[OpenClawClient] OpenClaw 返回了空响应');
|
|
190
|
+
return 'OpenClaw 未返回有效响应';
|
|
191
|
+
} else {
|
|
192
|
+
this.log?.error(`[OpenClawClient] OpenClaw 返回错误: ${response.status}`);
|
|
193
|
+
return `OpenClaw 返回错误: ${response.status}`;
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this.log?.error(`[OpenClawClient] HTTP 调用失败: ${err.message}`);
|
|
197
|
+
// fallback 到 CLI 调用
|
|
198
|
+
this.log?.info('[OpenClawClient] HTTP 失败,尝试 CLI fallback');
|
|
199
|
+
return this.chatViaCLI(message, fromUser);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async chatViaCLI(message, fromUser) {
|
|
45
204
|
const sessionId = `clawmessenger-${fromUser}`;
|
|
46
205
|
const escapedMessage = message
|
|
47
206
|
.replace(/\\/g, '\\\\')
|
|
@@ -73,7 +232,6 @@ class OpenClawClient {
|
|
|
73
232
|
},
|
|
74
233
|
});
|
|
75
234
|
|
|
76
|
-
// 流式收集输出,无 maxBuffer 限制
|
|
77
235
|
child.stdout.on('data', (chunk) => {
|
|
78
236
|
stdout += chunk.toString();
|
|
79
237
|
});
|
|
@@ -81,20 +239,19 @@ class OpenClawClient {
|
|
|
81
239
|
stderr += chunk.toString();
|
|
82
240
|
});
|
|
83
241
|
|
|
84
|
-
// 超时兜底(20 分钟)
|
|
85
242
|
const timeout = setTimeout(() => {
|
|
86
243
|
killed = true;
|
|
87
244
|
child.kill('SIGTERM');
|
|
88
|
-
this.log?.error('[OpenClawClient] 执行超时,强制终止');
|
|
245
|
+
this.log?.error('[OpenClawClient] CLI 执行超时,强制终止');
|
|
89
246
|
}, 1200000);
|
|
90
247
|
|
|
91
248
|
child.on('error', (err) => {
|
|
92
249
|
clearTimeout(timeout);
|
|
93
|
-
this.log?.error(`[OpenClawClient] 子进程错误: ${err.message}`);
|
|
250
|
+
this.log?.error(`[OpenClawClient] CLI 子进程错误: ${err.message}`);
|
|
94
251
|
if (err.code === 'ENOENT') {
|
|
95
252
|
resolve('找不到 openclaw 命令');
|
|
96
253
|
} else {
|
|
97
|
-
resolve(`OpenClaw 调用失败: ${err.message}`);
|
|
254
|
+
resolve(`OpenClaw CLI 调用失败: ${err.message}`);
|
|
98
255
|
}
|
|
99
256
|
});
|
|
100
257
|
|
|
@@ -102,17 +259,17 @@ class OpenClawClient {
|
|
|
102
259
|
clearTimeout(timeout);
|
|
103
260
|
|
|
104
261
|
if (killed) {
|
|
105
|
-
resolve('OpenClaw 响应超时');
|
|
262
|
+
resolve('OpenClaw CLI 响应超时');
|
|
106
263
|
return;
|
|
107
264
|
}
|
|
108
265
|
|
|
109
|
-
this.log?.info(`[OpenClawClient] 进程退出 code=${code}`);
|
|
266
|
+
this.log?.info(`[OpenClawClient] CLI 进程退出 code=${code}`);
|
|
110
267
|
this.log?.info(`[OpenClawClient] stdout 长度: ${stdout.length}, stderr 长度: ${stderr.length}`);
|
|
111
268
|
|
|
112
269
|
if (code !== 0) {
|
|
113
270
|
const errOutput = stderr || stdout || '';
|
|
114
|
-
this.log?.error(`[OpenClawClient] 错误输出: ${errOutput.substring(0, 500)}`);
|
|
115
|
-
resolve(`OpenClaw 调用失败: ${errOutput.substring(0, 200)}`);
|
|
271
|
+
this.log?.error(`[OpenClawClient] CLI 错误输出: ${errOutput.substring(0, 500)}`);
|
|
272
|
+
resolve(`OpenClaw CLI 调用失败: ${errOutput.substring(0, 200)}`);
|
|
116
273
|
return;
|
|
117
274
|
}
|
|
118
275
|
|
|
@@ -154,13 +154,12 @@ class RongCloudClient {
|
|
|
154
154
|
sentTime: message.sentTime || Date.now()
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
});
|
|
157
|
+
// 并行处理消息,不等待上一条完成(避免 openclaw 长耗时调用阻塞后续消息)
|
|
158
|
+
if (this.handler) {
|
|
159
|
+
this.handler.handleMessage(rongCloudMsg).catch(err => {
|
|
160
|
+
this.log?.error(`[RongCloudClient] 消息处理异常: ${err.message}`);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
164
163
|
} catch (err) {
|
|
165
164
|
this.log?.error(`[RongCloudClient] 解析消息失败: ${err.message}`);
|
|
166
165
|
}
|