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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-subagent-service",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "虾说静态服务",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -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
- try {
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
- 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);
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
- this.processingQueue = this.processingQueue.then(async () => {
158
- if (this.handler) {
159
- await this.handler.handleMessage(rongCloudMsg);
160
- }
161
- }).catch(err => {
162
- this.log?.error(`[RongCloudClient] 消息处理异常: ${err.message}`);
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
  }