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
|
@@ -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
|
|
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
|
-
|
|
101
|
-
this.
|
|
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
|
-
|
|
168
|
-
this.
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
});
|
|
363
|
+
const response = await axios.post(apiUrl, payload, {
|
|
364
|
+
headers,
|
|
365
|
+
responseType: 'stream',
|
|
366
|
+
timeout: 600000 // 10 分钟
|
|
367
|
+
});
|
|
342
368
|
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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
|
}
|