claw-subagent-service 0.0.23 → 0.0.25
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,135 +1,278 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
1
|
+
const { spawn, execSync } = require('child_process');
|
|
2
|
+
const net = require('net');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 获取实际用户主目录(SYSTEM 账户下 os.homedir() 返回 systemprofile)
|
|
9
|
+
*/
|
|
10
|
+
function getRealHomeDir() {
|
|
11
|
+
const envHome = process.env.CLAW_SERVICE_HOME || process.env.USERPROFILE || process.env.HOME;
|
|
12
|
+
if (envHome && !envHome.includes('systemprofile')) {
|
|
13
|
+
return envHome;
|
|
14
|
+
}
|
|
15
|
+
const homeDir = os.homedir();
|
|
16
|
+
if (!homeDir.includes('systemprofile')) {
|
|
17
|
+
return homeDir;
|
|
18
|
+
}
|
|
19
|
+
// SYSTEM 账户兜底:扫描 C:\Users 找包含 .openclaw 的实际用户目录
|
|
20
|
+
const usersDir = 'C:\\Users';
|
|
21
|
+
if (fs.existsSync(usersDir)) {
|
|
22
|
+
const entries = fs.readdirSync(usersDir, { withFileTypes: true });
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
if (entry.isDirectory() && !['Public', 'Default', 'All Users', 'Default User'].includes(entry.name)) {
|
|
25
|
+
const candidate = path.join(usersDir, entry.name);
|
|
26
|
+
if (fs.existsSync(path.join(candidate, '.openclaw'))) {
|
|
27
|
+
return candidate;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return homeDir;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 检测端口是否监听
|
|
37
|
+
*/
|
|
38
|
+
function checkPort(port) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const sock = new net.Socket();
|
|
41
|
+
sock.setTimeout(3000);
|
|
42
|
+
sock.once('connect', () => {
|
|
43
|
+
sock.destroy();
|
|
44
|
+
resolve(true);
|
|
45
|
+
});
|
|
46
|
+
sock.once('error', () => {
|
|
47
|
+
sock.destroy();
|
|
48
|
+
resolve(false);
|
|
49
|
+
});
|
|
50
|
+
sock.once('timeout', () => {
|
|
51
|
+
sock.destroy();
|
|
52
|
+
resolve(false);
|
|
53
|
+
});
|
|
54
|
+
sock.connect(port, '127.0.0.1');
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 启动 OpenClaw gateway
|
|
60
|
+
*/
|
|
61
|
+
function startOpenClawGateway(log) {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
log?.info('[OpenClawClient] 正在启动 OpenClaw gateway...');
|
|
64
|
+
|
|
65
|
+
const child = spawn('openclaw', ['gateway'], {
|
|
66
|
+
shell: true,
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
detached: true,
|
|
69
|
+
stdio: 'ignore',
|
|
70
|
+
env: {
|
|
71
|
+
...process.env,
|
|
72
|
+
USERPROFILE: getRealHomeDir(),
|
|
73
|
+
HOME: getRealHomeDir(),
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
child.unref();
|
|
78
|
+
|
|
79
|
+
// 等待 gateway 启动(最多 15 秒)
|
|
80
|
+
let attempts = 0;
|
|
81
|
+
const maxAttempts = 15;
|
|
82
|
+
const interval = setInterval(async () => {
|
|
83
|
+
attempts++;
|
|
84
|
+
const isRunning = await checkPort(18789);
|
|
85
|
+
if (isRunning) {
|
|
86
|
+
clearInterval(interval);
|
|
87
|
+
log?.info('[OpenClawClient] OpenClaw gateway 启动成功');
|
|
88
|
+
resolve(true);
|
|
89
|
+
} else if (attempts >= maxAttempts) {
|
|
90
|
+
clearInterval(interval);
|
|
91
|
+
log?.warn('[OpenClawClient] OpenClaw gateway 启动超时');
|
|
92
|
+
resolve(false);
|
|
93
|
+
}
|
|
94
|
+
}, 1000);
|
|
95
|
+
|
|
96
|
+
child.on('error', (err) => {
|
|
97
|
+
log?.error(`[OpenClawClient] 启动 gateway 失败: ${err.message}`);
|
|
98
|
+
clearInterval(interval);
|
|
99
|
+
resolve(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class OpenClawClient {
|
|
105
|
+
constructor(log) {
|
|
106
|
+
this.log = log;
|
|
107
|
+
this.gatewayStarting = false;
|
|
108
|
+
this.gatewayStarted = false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 确保 OpenClaw gateway 在运行
|
|
113
|
+
*/
|
|
114
|
+
async ensureGatewayRunning() {
|
|
115
|
+
if (this.gatewayStarted) return true;
|
|
116
|
+
|
|
117
|
+
const isRunning = await checkPort(18789);
|
|
118
|
+
if (isRunning) {
|
|
119
|
+
this.log?.info('[OpenClawClient] OpenClaw gateway 已在运行');
|
|
120
|
+
this.gatewayStarted = true;
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 避免并发启动
|
|
125
|
+
if (this.gatewayStarting) {
|
|
126
|
+
this.log?.info('[OpenClawClient] gateway 正在启动中,等待...');
|
|
127
|
+
// 等待最多 20 秒
|
|
128
|
+
for (let i = 0; i < 20; i++) {
|
|
129
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
130
|
+
if (await checkPort(18789)) {
|
|
131
|
+
this.gatewayStarted = true;
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.gatewayStarting = true;
|
|
139
|
+
try {
|
|
140
|
+
const started = await startOpenClawGateway(this.log);
|
|
141
|
+
this.gatewayStarted = started;
|
|
142
|
+
return started;
|
|
143
|
+
} finally {
|
|
144
|
+
this.gatewayStarting = false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async chat(message, fromUser) {
|
|
149
|
+
if (!message || !message.trim()) {
|
|
150
|
+
return '消息内容为空';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 确保 OpenClaw gateway 已启动
|
|
154
|
+
const gatewayReady = await this.ensureGatewayRunning();
|
|
155
|
+
if (!gatewayReady) {
|
|
156
|
+
this.log?.error('[OpenClawClient] OpenClaw gateway 未运行且启动失败');
|
|
157
|
+
return 'OpenClaw gateway 启动失败,请检查 openclaw 是否正确安装';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.log?.info(`[OpenClawClient] 准备发送消息到 OpenClaw,from=${fromUser}, message=${message.substring(0, 50)}`);
|
|
161
|
+
|
|
162
|
+
const sessionId = `clawmessenger-${fromUser}`;
|
|
163
|
+
const escapedMessage = message
|
|
164
|
+
.replace(/\\/g, '\\\\')
|
|
165
|
+
.replace(/"/g, '\\"')
|
|
166
|
+
.replace(/\r\n/g, ' ')
|
|
167
|
+
.replace(/\n/g, ' ')
|
|
168
|
+
.replace(/\r/g, ' ');
|
|
169
|
+
|
|
170
|
+
const gatewayUrl = 'http://127.0.0.1:5678';
|
|
171
|
+
const realHome = getRealHomeDir();
|
|
172
|
+
this.log?.info(`[OpenClawClient] 使用用户目录: ${realHome}`);
|
|
173
|
+
|
|
174
|
+
const args = ['agent', '-m', escapedMessage, '--session-id', sessionId];
|
|
175
|
+
this.log?.info(`[OpenClawClient] 执行: openclaw ${args.join(' ')}`);
|
|
176
|
+
|
|
177
|
+
return new Promise((resolve) => {
|
|
178
|
+
let stdout = '';
|
|
179
|
+
let stderr = '';
|
|
180
|
+
let killed = false;
|
|
181
|
+
|
|
182
|
+
const child = spawn('openclaw', args, {
|
|
183
|
+
shell: true,
|
|
184
|
+
windowsHide: true,
|
|
185
|
+
env: {
|
|
186
|
+
...process.env,
|
|
187
|
+
OPENCLAW_GATEWAY_URL: gatewayUrl,
|
|
188
|
+
USERPROFILE: realHome,
|
|
189
|
+
HOME: realHome,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// 流式收集输出,无 maxBuffer 限制
|
|
194
|
+
child.stdout.on('data', (chunk) => {
|
|
195
|
+
stdout += chunk.toString();
|
|
196
|
+
});
|
|
197
|
+
child.stderr.on('data', (chunk) => {
|
|
198
|
+
stderr += chunk.toString();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// 超时兜底(20 分钟)
|
|
202
|
+
const timeout = setTimeout(() => {
|
|
203
|
+
killed = true;
|
|
204
|
+
child.kill('SIGTERM');
|
|
205
|
+
this.log?.error('[OpenClawClient] 执行超时,强制终止');
|
|
206
|
+
}, 1200000);
|
|
207
|
+
|
|
208
|
+
child.on('error', (err) => {
|
|
209
|
+
clearTimeout(timeout);
|
|
210
|
+
this.log?.error(`[OpenClawClient] 子进程错误: ${err.message}`);
|
|
211
|
+
if (err.code === 'ENOENT') {
|
|
212
|
+
resolve('找不到 openclaw 命令');
|
|
213
|
+
} else {
|
|
214
|
+
resolve(`OpenClaw 调用失败: ${err.message}`);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
child.on('close', (code) => {
|
|
219
|
+
clearTimeout(timeout);
|
|
220
|
+
|
|
221
|
+
if (killed) {
|
|
222
|
+
resolve('OpenClaw 响应超时');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.log?.info(`[OpenClawClient] 进程退出 code=${code}`);
|
|
227
|
+
this.log?.info(`[OpenClawClient] stdout 长度: ${stdout.length}, stderr 长度: ${stderr.length}`);
|
|
228
|
+
|
|
229
|
+
if (code !== 0) {
|
|
230
|
+
const errOutput = stderr || stdout || '';
|
|
231
|
+
this.log?.error(`[OpenClawClient] 错误输出: ${errOutput.substring(0, 500)}`);
|
|
232
|
+
resolve(`OpenClaw 调用失败: ${errOutput.substring(0, 200)}`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const output = stdout || stderr || '';
|
|
237
|
+
resolve(this.cleanOutput(output));
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
cleanOutput(output) {
|
|
243
|
+
const lines = output.split('\n');
|
|
244
|
+
const cleanLines = [];
|
|
245
|
+
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
const trimmed = line.trim();
|
|
248
|
+
if (!trimmed) continue;
|
|
249
|
+
|
|
250
|
+
// 移除 ANSI 颜色代码
|
|
251
|
+
const cleanLine = trimmed.replace(/\x1B\[[0-9;]*m/g, '');
|
|
252
|
+
|
|
253
|
+
// 跳过所有调试/配置日志行
|
|
254
|
+
if (cleanLine.startsWith('[ws]')) continue;
|
|
255
|
+
if (cleanLine.startsWith('[health-monitor]')) continue;
|
|
256
|
+
if (cleanLine.startsWith('[OpenClawConfig]')) continue;
|
|
257
|
+
if (cleanLine.startsWith('[plugins]')) continue;
|
|
258
|
+
if (cleanLine.includes('龙虾信使插件已注册')) continue;
|
|
259
|
+
if (cleanLine.includes('龙虾信使')) continue;
|
|
260
|
+
if (cleanLine.includes('已加载配置文件')) continue;
|
|
261
|
+
if (cleanLine.includes('plugins.allow')) continue;
|
|
262
|
+
if (cleanLine.includes('Config warnings')) continue;
|
|
263
|
+
if (cleanLine.includes('stale config')) continue;
|
|
264
|
+
if (cleanLine.includes('plugin not found')) continue;
|
|
265
|
+
if (cleanLine.includes('⇄ res')) continue;
|
|
266
|
+
if (cleanLine.includes('chat.history')) continue;
|
|
267
|
+
if (cleanLine.includes('models.list')) continue;
|
|
268
|
+
if (cleanLine.includes('node.list')) continue;
|
|
269
|
+
if (/^\d{2}:\d{2}:\d{2}/.test(cleanLine)) continue; // 时间戳开头的日志
|
|
270
|
+
|
|
271
|
+
cleanLines.push(cleanLine);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return cleanLines.join('\n').trim() || 'OpenClaw 未返回有效响应';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = { OpenClawClient };
|
|
@@ -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
|
}
|