claw-subagent-service 0.0.60 → 0.0.62

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.60",
3
+ "version": "0.0.62",
4
4
  "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -60,6 +60,18 @@ function loadConfig() {
60
60
  }
61
61
  }
62
62
 
63
+ // 计算 apiBaseUrl:环境变量 > 配置文件 > 推导值 > 默认值
64
+ let apiBaseUrl = process.env.API_BASE_URL || localConfig.apiBaseUrl || clawBridgeConfig.apiBaseUrl;
65
+ if (!apiBaseUrl) {
66
+ const serverUrl = process.env.DM_SERVER_URL || 'https://newsradar.dreamdt.cn/im';
67
+ try {
68
+ const url = new URL(serverUrl);
69
+ apiBaseUrl = `${url.protocol}//${url.host}`;
70
+ } catch {
71
+ apiBaseUrl = 'http://127.0.0.1:5000';
72
+ }
73
+ }
74
+
63
75
  return {
64
76
  appKey: process.env.DM_APP_KEY || localConfig.appKey || 'bmdehs6pbyyks',
65
77
  token: localConfig.token || clawBridgeConfig.token,
@@ -73,7 +85,7 @@ function loadConfig() {
73
85
  scriptTimeout: localConfig.scriptTimeout || 180,
74
86
  successKeyword: localConfig.successKeyword || 'Success',
75
87
  chatTimeout: localConfig.chatTimeout || 600,
76
- apiBaseUrl: process.env.API_BASE_URL || localConfig.apiBaseUrl || clawBridgeConfig.apiBaseUrl || 'http://127.0.0.1:5000'
88
+ apiBaseUrl
77
89
  };
78
90
  }
79
91
 
@@ -97,9 +97,16 @@ class MessageHandler {
97
97
  } else {
98
98
  // 如果配置了代理地址,使用流式处理
99
99
  if (this.isStreamingEnabled) {
100
- this.handleNormalMessageStream(msg).catch(err => {
101
- this.log?.error(`[MessageHandler] 流式处理异常: ${err.message}`);
102
- });
100
+ try {
101
+ await this.handleNormalMessageStream(msg);
102
+ } catch (err) {
103
+ this.log?.error(`[MessageHandler] 流式处理失败,回退到非流式: ${err.message}`);
104
+ const reply = await this.handleNormalMessage(msg);
105
+ if (reply) {
106
+ const targetId = this.getReplyTarget(msg);
107
+ await this.sendFn(targetId, reply, msg.conversationType);
108
+ }
109
+ }
103
110
  } else {
104
111
  // 降级到非流式处理
105
112
  const reply = await this.handleNormalMessage(msg);
@@ -164,9 +171,22 @@ class MessageHandler {
164
171
 
165
172
  // 如果配置了代理地址,使用流式处理
166
173
  if (this.isStreamingEnabled) {
167
- this.handleNormalMessageStream(msg).catch(err => {
168
- this.log?.error(`[MessageHandler] 流式处理异常: ${err.message}`);
169
- });
174
+ try {
175
+ await this.handleNormalMessageStream(msg);
176
+ } catch (err) {
177
+ this.log?.error(`[MessageHandler] 流式处理失败,回退到 CLI: ${err.message}`);
178
+ // 回退到非流式 CLI 调用
179
+ try {
180
+ const reply = await this.openclawClient.chat(msg.content, msg.senderUserId);
181
+ if (reply) {
182
+ this.log?.info(`[MessageHandler] AI 回复: ${reply.substring(0, 50)}...`);
183
+ await this.sendFn(targetId, reply, msg.conversationType);
184
+ }
185
+ } catch (cliErr) {
186
+ this.log?.error(`[MessageHandler] CLI 回退也失败: ${cliErr.message}`);
187
+ await this.sendFn(targetId, `❌ 处理失败: ${cliErr.message}`, msg.conversationType);
188
+ }
189
+ }
170
190
  return;
171
191
  }
172
192
 
@@ -199,36 +219,48 @@ class MessageHandler {
199
219
  this.log?.info(`[MessageHandler] 开始流式处理,streamId=${streamId}`);
200
220
 
201
221
  // 2. 调用 OpenClaw SSE
202
- return this.openclawClient.chatStream(
203
- msg.content,
204
- msg.senderUserId,
205
- async (delta) => {
206
- buffer += delta;
207
- // 策略:每 30 个字符或遇到句末标点发送一次片段
208
- if (buffer.length >= 30 || /[。!?.!?\n]$/.test(delta)) {
209
- await this._sendStreamChunk(fromUserId, targetId, conversationType, buffer, streamId, isFirstChunk, false);
210
- isFirstChunk = false;
211
- buffer = '';
212
- }
213
- },
214
- async (fullText) => {
215
- // 发送剩余缓冲区和结束标记
216
- if (buffer.trim()) {
217
- await this._sendStreamChunk(fromUserId, targetId, conversationType, buffer, streamId, isFirstChunk, true);
218
- } else if (!isFirstChunk) {
219
- // 已经发送过内容,单独发送结束标记
220
- await this._sendStreamChunk(fromUserId, targetId, conversationType, '', streamId, false, true);
221
- } else {
222
- // 完全没有收到内容,发送错误提示
223
- await this._sendStreamChunk(fromUserId, targetId, conversationType, '抱歉,AI 暂时没有回复内容。', streamId, true, true);
222
+ let hasSentChunk = false;
223
+ try {
224
+ await this.openclawClient.chatStream(
225
+ msg.content,
226
+ msg.senderUserId,
227
+ async (delta) => {
228
+ buffer += delta;
229
+ // 策略:每 30 个字符或遇到句末标点发送一次片段
230
+ if (buffer.length >= 30 || /[。!?.!?\n]$/.test(delta)) {
231
+ await this._sendStreamChunk(fromUserId, targetId, conversationType, buffer, streamId, isFirstChunk, false);
232
+ isFirstChunk = false;
233
+ hasSentChunk = true;
234
+ buffer = '';
235
+ }
236
+ },
237
+ async (fullText) => {
238
+ // 发送剩余缓冲区和结束标记
239
+ if (buffer.trim()) {
240
+ await this._sendStreamChunk(fromUserId, targetId, conversationType, buffer, streamId, isFirstChunk, true);
241
+ hasSentChunk = true;
242
+ } else if (!isFirstChunk) {
243
+ // 已经发送过内容,单独发送结束标记
244
+ await this._sendStreamChunk(fromUserId, targetId, conversationType, '', streamId, false, true);
245
+ hasSentChunk = true;
246
+ } else {
247
+ // 完全没有收到内容,发送错误提示
248
+ await this._sendStreamChunk(fromUserId, targetId, conversationType, '抱歉,AI 暂时没有回复内容。', streamId, true, true);
249
+ hasSentChunk = true;
250
+ }
251
+
252
+ if (!hasSentChunk) {
253
+ throw new Error('流式消息发送失败,没有任何片段成功送达');
254
+ }
255
+
256
+ this.log?.info(`[MessageHandler] 流式消息完成,streamId=${streamId}, 总长度: ${fullText.length}`);
224
257
  }
225
- this.log?.info(`[MessageHandler] 流式消息完成,streamId=${streamId}, 总长度: ${fullText.length}`);
226
- },
227
- async (err) => {
228
- this.log?.error(`[MessageHandler] 流式处理错误: ${err.message}`);
229
- await this._sendStreamChunk(fromUserId, targetId, conversationType, '抱歉,AI 响应出现错误,请稍后重试。', streamId, isFirstChunk, true);
230
- }
231
- );
258
+ );
259
+ } catch (err) {
260
+ this.log?.error(`[MessageHandler] 流式处理错误: ${err.message}`);
261
+ await this._sendStreamChunk(fromUserId, targetId, conversationType, '抱歉,AI 响应出现错误,请稍后重试。', streamId, isFirstChunk, true);
262
+ throw err;
263
+ }
232
264
  }
233
265
 
234
266
  /**
@@ -248,7 +280,9 @@ class MessageHandler {
248
280
  );
249
281
  this.log?.info(`[MessageHandler] typing 状态已发送: ${fromUserId} -> ${targetId}`);
250
282
  } catch (err) {
251
- this.log?.warn(`[MessageHandler] 发送 typing 状态失败: ${err.message}`);
283
+ const url = `${this.config.apiBaseUrl}/im/api/proxy/stream/typing`;
284
+ const status = err.response?.status;
285
+ this.log?.warn(`[MessageHandler] 发送 typing 状态失败: ${err.message}, url=${url}, status=${status || 'N/A'}`);
252
286
  }
253
287
  }
254
288
 
@@ -272,7 +306,9 @@ class MessageHandler {
272
306
  { timeout: 10000 }
273
307
  );
274
308
  } catch (err) {
275
- this.log?.warn(`[MessageHandler] 发送流式消息失败: ${err.message}`);
309
+ const url = `${this.config.apiBaseUrl}/im/api/proxy/stream/publish`;
310
+ const status = err.response?.status;
311
+ this.log?.warn(`[MessageHandler] 发送流式消息失败: ${err.message}, url=${url}, status=${status || 'N/A'}`);
276
312
  }
277
313
  }
278
314
 
@@ -311,8 +311,35 @@ class OpenClawClient {
311
311
 
312
312
  const gatewayToken = getGatewayToken();
313
313
  const sessionId = `clawmessenger-${fromUser}`;
314
- const apiUrl = 'http://127.0.0.1:18789/v1/chat/completions';
315
314
 
315
+ // 尝试多个可能的 SSE 端点,兼容不同版本 OpenClaw Gateway
316
+ const endpoints = [
317
+ 'http://127.0.0.1:18789/v1/chat/completions',
318
+ 'http://127.0.0.1:18789/v1/responses'
319
+ ];
320
+
321
+ for (let i = 0; i < endpoints.length; i++) {
322
+ const apiUrl = endpoints[i];
323
+ try {
324
+ await this._doChatStream(apiUrl, gatewayToken, sessionId, message, onDelta, onDone);
325
+ return; // 成功则直接返回
326
+ } catch (err) {
327
+ const is404 = err.response?.status === 404;
328
+ const isLast = i === endpoints.length - 1;
329
+
330
+ if (is404 && !isLast) {
331
+ this.log?.warn(`[OpenClawClient] SSE 端点 ${apiUrl} 返回 404,尝试备用端点`);
332
+ continue;
333
+ }
334
+
335
+ this.log?.error(`[OpenClawClient] SSE 请求失败: ${err.message}`);
336
+ // 不再内部调用 onError,让错误通过 Promise reject 向上传播,便于调用方回退
337
+ throw err;
338
+ }
339
+ }
340
+ }
341
+
342
+ async _doChatStream(apiUrl, gatewayToken, sessionId, message, onDelta, onDone) {
316
343
  const headers = {
317
344
  'Content-Type': 'application/json',
318
345
  'Accept': 'text/event-stream'
@@ -333,14 +360,14 @@ class OpenClawClient {
333
360
  let fullText = '';
334
361
  let buffer = '';
335
362
 
336
- try {
337
- const response = await axios.post(apiUrl, payload, {
338
- headers,
339
- responseType: 'stream',
340
- timeout: 600000 // 10 分钟
341
- });
363
+ const response = await axios.post(apiUrl, payload, {
364
+ headers,
365
+ responseType: 'stream',
366
+ timeout: 600000 // 10 分钟
367
+ });
342
368
 
343
- response.data.on('data', (chunk) => {
369
+ return new Promise((resolve, reject) => {
370
+ response.data.on('data', async (chunk) => {
344
371
  buffer += chunk.toString();
345
372
  const lines = buffer.split('\n');
346
373
  buffer = lines.pop(); // 保留未完整的最后一行
@@ -357,7 +384,12 @@ class OpenClawClient {
357
384
  const delta = data.choices?.[0]?.delta?.content;
358
385
  if (delta) {
359
386
  fullText += delta;
360
- onDelta?.(delta);
387
+ try {
388
+ await onDelta?.(delta);
389
+ } catch (err) {
390
+ reject(err);
391
+ return;
392
+ }
361
393
  }
362
394
  } catch {
363
395
  // 忽略无法解析的 JSON 行
@@ -365,23 +397,21 @@ class OpenClawClient {
365
397
  }
366
398
  });
367
399
 
368
- return new Promise((resolve, reject) => {
369
- response.data.on('end', () => {
370
- this.log?.info(`[OpenClawClient] SSE 流结束,总长度: ${fullText.length}`);
371
- onDone?.(fullText);
400
+ response.data.on('end', async () => {
401
+ this.log?.info(`[OpenClawClient] SSE 流结束,总长度: ${fullText.length}`);
402
+ try {
403
+ await onDone?.(fullText);
372
404
  resolve();
373
- });
374
-
375
- response.data.on('error', (err) => {
376
- this.log?.error(`[OpenClawClient] SSE 流错误: ${err.message}`);
377
- onError?.(err);
405
+ } catch (err) {
378
406
  reject(err);
379
- });
407
+ }
380
408
  });
381
- } catch (err) {
382
- this.log?.error(`[OpenClawClient] SSE 请求失败: ${err.message}`);
383
- onError?.(err);
384
- }
409
+
410
+ response.data.on('error', (err) => {
411
+ this.log?.error(`[OpenClawClient] SSE 流错误: ${err.message}`);
412
+ reject(err);
413
+ });
414
+ });
385
415
  }
386
416
 
387
417
  async chatViaCLI(message, fromUser) {
package/service/worker.js CHANGED
@@ -184,6 +184,7 @@ function loadRongCloudConfig() {
184
184
  config.token = clawConfig.token;
185
185
  config.accountId = clawConfig.nodeId;
186
186
  config.nodeName = clawConfig.nodeName;
187
+ if (clawConfig.apiBaseUrl) config.apiBaseUrl = clawConfig.apiBaseUrl;
187
188
  log.info(`[WORKER] 从 claw-bridge 加载配置: nodeId=${clawConfig.nodeId}, nodeName=${clawConfig.nodeName}`);
188
189
  } else {
189
190
  log.warn(`[WORKER] 未找到 ${clawBridgeConfigPath}`);
@@ -199,6 +200,7 @@ function loadRongCloudConfig() {
199
200
  if (localConfig.token) config.token = localConfig.token;
200
201
  if (localConfig.accountId) config.accountId = localConfig.accountId;
201
202
  if (localConfig.appSecret) config.appSecret = localConfig.appSecret;
203
+ if (localConfig.apiBaseUrl) config.apiBaseUrl = localConfig.apiBaseUrl;
202
204
  log.info(`[WORKER] 从本地配置加载: appKey=${config.appKey?.substring(0, 8)}...`);
203
205
  }
204
206
  } catch (err) {
@@ -206,7 +208,19 @@ function loadRongCloudConfig() {
206
208
  }
207
209
 
208
210
  // 加载 apiBaseUrl(Python 后端地址,用于代理发送流式消息)
209
- config.apiBaseUrl = process.env.API_BASE_URL || config.apiBaseUrl || 'http://127.0.0.1:5000';
211
+ // 优先级:环境变量 > 配置文件 > 推导值(DM_SERVER_URL) > 默认值
212
+ config.apiBaseUrl = process.env.API_BASE_URL || config.apiBaseUrl;
213
+
214
+ if (!config.apiBaseUrl) {
215
+ const serverUrl = process.env.DM_SERVER_URL || 'https://newsradar.dreamdt.cn/im';
216
+ try {
217
+ const url = new URL(serverUrl);
218
+ config.apiBaseUrl = `${url.protocol}//${url.host}`;
219
+ log.info(`[WORKER] 从 serverUrl 推导 apiBaseUrl: ${config.apiBaseUrl}`);
220
+ } catch {
221
+ config.apiBaseUrl = 'http://127.0.0.1:5000';
222
+ }
223
+ }
210
224
 
211
225
  if (!config.appKey) {
212
226
  config.appKey = process.env.DM_APP_KEY || 'bmdehs6pbyyks';
@@ -218,6 +232,8 @@ function loadRongCloudConfig() {
218
232
  config.heartbeatInterval = 20;
219
233
  }
220
234
 
235
+ log.info(`[WORKER] 最终 apiBaseUrl: ${config.apiBaseUrl}`);
236
+
221
237
  if (config.token && config.accountId) {
222
238
  return config;
223
239
  }