claw-subagent-service 0.0.58 → 0.0.61

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.58",
3
+ "version": "0.0.61",
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,
@@ -72,7 +84,8 @@ function loadConfig() {
72
84
  openclawPort: localConfig.openclawPort || 18789,
73
85
  scriptTimeout: localConfig.scriptTimeout || 180,
74
86
  successKeyword: localConfig.successKeyword || 'Success',
75
- chatTimeout: localConfig.chatTimeout || 600
87
+ chatTimeout: localConfig.chatTimeout || 600,
88
+ apiBaseUrl
76
89
  };
77
90
  }
78
91
 
@@ -4,8 +4,8 @@ const { OpenClawClient } = require('./openclaw-client');
4
4
  const { RongCloudClient, ConversationType } = require('./rongcloud-client');
5
5
  const { ensurePluginsAllow } = require('./openclaw-config');
6
6
 
7
- function createRongCloudModule(config, sendFn, log) {
8
- return new MessageHandler(config, sendFn, log);
7
+ function createRongCloudModule(config, sendFn, log, sendReadReceiptFn) {
8
+ return new MessageHandler(config, sendFn, log, sendReadReceiptFn);
9
9
  }
10
10
 
11
11
  module.exports = {
@@ -1,172 +1,291 @@
1
- const { MessageType } = require('./types');
2
- const { OpenClawClient } = require('./openclaw-client');
3
- const { handleNormalMessage } = require('../modules/normal-message-handler');
4
- const MENTION_REGEX = /@(claw_[a-zA-Z0-9]+)/g;
5
-
6
- class MessageHandler {
7
- constructor(config, sendFn, log, sendReadReceiptFn) {
8
- this.config = config;
9
- this.sendFn = sendFn;
10
- this.log = log;
11
- this.sendReadReceiptFn = sendReadReceiptFn;
12
- this.openclawClient = new OpenClawClient(log);
13
- this.nodeId = config.accountId || '';
14
- this.handleNormalMessage = handleNormalMessage;
15
- }
16
-
17
- extractMentions(content) {
18
- const mentions = [];
19
- let match;
20
- while ((match = MENTION_REGEX.exec(content)) !== null) {
21
- mentions.push(match[1]);
22
- }
23
- MENTION_REGEX.lastIndex = 0;
24
- return mentions;
25
- }
26
-
27
- shouldHandleMessage(msg) {
28
- // 过滤离线消息:离线消息是历史记录,不需要重复处理
29
- if (msg.isOffLineMessage) {
30
- this.log?.info('[MessageHandler] 收到离线消息,忽略');
31
- return false;
32
- }
33
-
34
- const allowedTypes = ['RC:TxtMsg', 'claw'];
35
- if (!allowedTypes.includes(msg.messageType)) {
36
- this.log?.info(`[MessageHandler] 忽略非文本消息: ${msg.messageType}`);
37
- return false;
38
- }
39
-
40
- if (msg.senderUserId === this.config.accountId) {
41
- this.log?.info('[MessageHandler] 忽略自己发送的消息');
42
- return false;
43
- }
44
-
45
- // 优先从融云 mentionedInfo 提取被@用户列表(用户界面 @昵称,但融云底层存的是 userId)
46
- let mentions = [];
47
- if (msg.mentionedInfo && Array.isArray(msg.mentionedInfo.userIdList)) {
48
- mentions = msg.mentionedInfo.userIdList;
49
- if (mentions.length > 0) {
50
- this.log?.info(`[MessageHandler] 融云 mentionedInfo: ${mentions.join(', ')},本节点: ${this.nodeId}`);
51
- }
52
- }
53
-
54
- // 兜底:从文本内容中正则匹配 @claw_xxx
55
- if (mentions.length === 0) {
56
- const textContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
57
- mentions = this.extractMentions(textContent);
58
- }
59
-
60
- if (mentions.length > 0) {
61
- if (!mentions.includes(this.nodeId)) {
62
- this.log?.info(`[MessageHandler] 消息 @${mentions.join(', ')},非本节点(${this.nodeId}),忽略`);
63
- return false;
64
- }
65
- this.log?.info(`[MessageHandler] 消息提及本节点(${this.nodeId}),处理`);
66
- } else if (msg.conversationType === 3) {
67
- // 群聊消息未 @ 任何人 → 视为所有人参与,正常处理
68
- this.log?.info(`[MessageHandler] 群聊消息未 @ 任何人,本节点(${this.nodeId})参与处理`);
69
- } else {
70
- this.log?.info(`[MessageHandler] 单聊消息未指定节点,本节点(${this.nodeId})处理`);
71
- }
72
-
73
- return true;
74
- }
75
-
76
- async handleMessage(msg) {
77
- if (!this.shouldHandleMessage(msg)) {
78
- return;
79
- }
80
-
81
- try {
82
- const type = this.getMessageType(msg);
83
- const logContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
84
- this.log?.info(`[MessageHandler] 收到消息 from=${msg.senderUserId}, type=${type}, content=${logContent.substring(0, 50)}`);
85
- if (msg.messageType === 'claw') {
86
- this.log?.info(`收到龙虾消息,交由 OpenClawClient 处理`);
87
- await this.handleClaw(msg);
88
- } else {
89
- const reply = await this.handleNormalMessage(msg);
90
- if (reply) {
91
- const targetId = this.getReplyTarget(msg);
92
- await this.sendFn(targetId, reply, msg.conversationType);
93
- }
94
- }
95
- } catch (err) {
96
- this.log?.error(`[MessageHandler] 处理消息异常: ${err.message}`);
97
- const targetId = msg.conversationType === 3 ? msg.targetId : msg.senderUserId;
98
- await this.sendFn(targetId, `处理失败: ${err.message}`, msg.conversationType);
99
- }
100
- }
101
-
102
- getMessageType(msg) {
103
- if (msg.messageType === 'claw') {
104
- return MessageType.CLAW;
105
- }
106
- const text = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
107
- if (text.startsWith('/')) {
108
- return MessageType.COMMAND;
109
- }
110
- return MessageType.NORMAL;
111
- }
112
-
113
- getReplyTarget(msg) {
114
- if (msg.conversationType === 3) {
115
- return msg.targetId;
116
- }
117
- return msg.senderUserId;
118
- }
119
-
120
- async handleCommand(msg) {
121
- const payload = this.parseCommand(msg.content, msg.senderUserId);
122
- this.log?.info(`[MessageHandler] 指令消息: command=${payload.command}, args=${payload.args.join(', ')}`);
123
-
124
- let reply;
125
- if (this.config.onCommand) {
126
- try {
127
- reply = await this.config.onCommand(payload);
128
- } catch (err) {
129
- this.log?.error(`[MessageHandler] 指令处理回调异常: ${err.message}`);
130
- reply = `指令执行异常: ${err.message}`;
131
- }
132
- } else {
133
- reply = `指令 "${payload.command}" 暂未实现`;
134
- }
135
-
136
- const targetId = this.getReplyTarget(msg);
137
- await this.sendFn(targetId, reply, msg.conversationType);
138
- }
139
-
140
- async handleClaw(msg) {
141
- const targetId = this.getReplyTarget(msg);
142
-
143
- // 发送已读回执(fire-and-forget,不阻塞)
144
- if (this.sendReadReceiptFn) {
145
- this.sendReadReceiptFn(msg).catch(() => {});
146
- }
147
-
148
- // 后台执行 openclaw,不阻塞消息队列
149
- this.openclawClient.chat(msg.content, msg.senderUserId)
150
- .then(reply => {
151
- this.log?.info(`[MessageHandler] AI 回复: ${reply.substring(0, 50)}...`);
152
- this.sendFn(targetId, reply, msg.conversationType).catch(() => {});
153
- })
154
- .catch(err => {
155
- this.log?.error(`[MessageHandler] OpenClaw 调用失败: ${err.message}`);
156
- this.sendFn(targetId, `❌ 处理失败: ${err.message}`, msg.conversationType).catch(() => {});
157
- });
158
- }
159
-
160
- parseCommand(raw, senderId) {
161
- const trimmed = raw.slice(1).trim();
162
- const parts = trimmed.split(/\s+/);
163
- return {
164
- command: parts[0] || '',
165
- args: parts.slice(1),
166
- rawMessage: raw,
167
- senderId
168
- };
169
- }
170
- }
171
-
172
- module.exports = { MessageHandler };
1
+ const { MessageType } = require('./types');
2
+ const { OpenClawClient } = require('./openclaw-client');
3
+ const { handleNormalMessage } = require('../modules/normal-message-handler');
4
+ const axios = require('axios');
5
+ const MENTION_REGEX = /@(claw_[a-zA-Z0-9]+)/g;
6
+
7
+ class MessageHandler {
8
+ constructor(config, sendFn, log, sendReadReceiptFn) {
9
+ this.config = config;
10
+ this.sendFn = sendFn;
11
+ this.log = log;
12
+ this.sendReadReceiptFn = sendReadReceiptFn;
13
+ this.openclawClient = new OpenClawClient(log);
14
+ this.nodeId = config.accountId || '';
15
+ this.handleNormalMessage = handleNormalMessage;
16
+ }
17
+
18
+ /**
19
+ * 判断是否支持流式处理
20
+ */
21
+ get isStreamingEnabled() {
22
+ return !!this.config.apiBaseUrl;
23
+ }
24
+
25
+ extractMentions(content) {
26
+ const mentions = [];
27
+ let match;
28
+ while ((match = MENTION_REGEX.exec(content)) !== null) {
29
+ mentions.push(match[1]);
30
+ }
31
+ MENTION_REGEX.lastIndex = 0;
32
+ return mentions;
33
+ }
34
+
35
+ shouldHandleMessage(msg) {
36
+ // 过滤离线消息:离线消息是历史记录,不需要重复处理
37
+ if (msg.isOffLineMessage) {
38
+ this.log?.info('[MessageHandler] 收到离线消息,忽略');
39
+ return false;
40
+ }
41
+
42
+ const allowedTypes = ['RC:TxtMsg', 'claw'];
43
+ if (!allowedTypes.includes(msg.messageType)) {
44
+ this.log?.info(`[MessageHandler] 忽略非文本消息: ${msg.messageType}`);
45
+ return false;
46
+ }
47
+
48
+ if (msg.senderUserId === this.config.accountId) {
49
+ this.log?.info('[MessageHandler] 忽略自己发送的消息');
50
+ return false;
51
+ }
52
+
53
+ // 优先从融云 mentionedInfo 提取被@用户列表(用户界面 @昵称,但融云底层存的是 userId)
54
+ let mentions = [];
55
+ if (msg.mentionedInfo && Array.isArray(msg.mentionedInfo.userIdList)) {
56
+ mentions = msg.mentionedInfo.userIdList;
57
+ if (mentions.length > 0) {
58
+ this.log?.info(`[MessageHandler] 融云 mentionedInfo: ${mentions.join(', ')},本节点: ${this.nodeId}`);
59
+ }
60
+ }
61
+
62
+ // 兜底:从文本内容中正则匹配 @claw_xxx
63
+ if (mentions.length === 0) {
64
+ const textContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
65
+ mentions = this.extractMentions(textContent);
66
+ }
67
+
68
+ if (mentions.length > 0) {
69
+ if (!mentions.includes(this.nodeId)) {
70
+ this.log?.info(`[MessageHandler] 消息 @${mentions.join(', ')},非本节点(${this.nodeId}),忽略`);
71
+ return false;
72
+ }
73
+ this.log?.info(`[MessageHandler] 消息提及本节点(${this.nodeId}),处理`);
74
+ } else if (msg.conversationType === 3) {
75
+ // 群聊消息未 @ 任何人 → 视为所有人参与,正常处理
76
+ this.log?.info(`[MessageHandler] 群聊消息未 @ 任何人,本节点(${this.nodeId})参与处理`);
77
+ } else {
78
+ this.log?.info(`[MessageHandler] 单聊消息未指定节点,本节点(${this.nodeId})处理`);
79
+ }
80
+
81
+ return true;
82
+ }
83
+
84
+ async handleMessage(msg) {
85
+ if (!this.shouldHandleMessage(msg)) {
86
+ return;
87
+ }
88
+
89
+ try {
90
+ const type = this.getMessageType(msg);
91
+ const logContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
92
+ this.log?.info(`[MessageHandler] 收到消息 from=${msg.senderUserId}, type=${type}, content=${logContent.substring(0, 50)}`);
93
+
94
+ if (msg.messageType === 'claw') {
95
+ this.log?.info(`收到龙虾消息,交由 OpenClawClient 处理`);
96
+ await this.handleClaw(msg);
97
+ } else {
98
+ // 如果配置了代理地址,使用流式处理
99
+ if (this.isStreamingEnabled) {
100
+ this.handleNormalMessageStream(msg).catch(err => {
101
+ this.log?.error(`[MessageHandler] 流式处理异常: ${err.message}`);
102
+ });
103
+ } else {
104
+ // 降级到非流式处理
105
+ const reply = await this.handleNormalMessage(msg);
106
+ if (reply) {
107
+ const targetId = this.getReplyTarget(msg);
108
+ await this.sendFn(targetId, reply, msg.conversationType);
109
+ }
110
+ }
111
+ }
112
+ } catch (err) {
113
+ this.log?.error(`[MessageHandler] 处理消息异常: ${err.message}`);
114
+ const targetId = msg.conversationType === 3 ? msg.targetId : msg.senderUserId;
115
+ await this.sendFn(targetId, `处理失败: ${err.message}`, msg.conversationType);
116
+ }
117
+ }
118
+
119
+ getMessageType(msg) {
120
+ if (msg.messageType === 'claw') {
121
+ return MessageType.CLAW;
122
+ }
123
+ const text = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
124
+ if (text.startsWith('/')) {
125
+ return MessageType.COMMAND;
126
+ }
127
+ return MessageType.NORMAL;
128
+ }
129
+
130
+ getReplyTarget(msg) {
131
+ if (msg.conversationType === 3) {
132
+ return msg.targetId;
133
+ }
134
+ return msg.senderUserId;
135
+ }
136
+
137
+ async handleCommand(msg) {
138
+ const payload = this.parseCommand(msg.content, msg.senderUserId);
139
+ this.log?.info(`[MessageHandler] 指令消息: command=${payload.command}, args=${payload.args.join(', ')}`);
140
+
141
+ let reply;
142
+ if (this.config.onCommand) {
143
+ try {
144
+ reply = await this.config.onCommand(payload);
145
+ } catch (err) {
146
+ this.log?.error(`[MessageHandler] 指令处理回调异常: ${err.message}`);
147
+ reply = `指令执行异常: ${err.message}`;
148
+ }
149
+ } else {
150
+ reply = `指令 "${payload.command}" 暂未实现`;
151
+ }
152
+
153
+ const targetId = this.getReplyTarget(msg);
154
+ await this.sendFn(targetId, reply, msg.conversationType);
155
+ }
156
+
157
+ async handleClaw(msg) {
158
+ const targetId = this.getReplyTarget(msg);
159
+
160
+ // 发送已读回执(fire-and-forget,不阻塞)
161
+ if (this.sendReadReceiptFn) {
162
+ this.sendReadReceiptFn(msg).catch(() => {});
163
+ }
164
+
165
+ // 如果配置了代理地址,使用流式处理
166
+ if (this.isStreamingEnabled) {
167
+ this.handleNormalMessageStream(msg).catch(err => {
168
+ this.log?.error(`[MessageHandler] 流式处理异常: ${err.message}`);
169
+ });
170
+ return;
171
+ }
172
+
173
+ // 降级:后台执行 openclaw CLI,不阻塞消息队列
174
+ this.openclawClient.chat(msg.content, msg.senderUserId)
175
+ .then(reply => {
176
+ this.log?.info(`[MessageHandler] AI 回复: ${reply.substring(0, 50)}...`);
177
+ this.sendFn(targetId, reply, msg.conversationType).catch(() => {});
178
+ })
179
+ .catch(err => {
180
+ this.log?.error(`[MessageHandler] OpenClaw 调用失败: ${err.message}`);
181
+ this.sendFn(targetId, `❌ 处理失败: ${err.message}`, msg.conversationType).catch(() => {});
182
+ });
183
+ }
184
+
185
+ /**
186
+ * 流式处理普通消息
187
+ */
188
+ async handleNormalMessageStream(msg) {
189
+ const targetId = this.getReplyTarget(msg);
190
+ const streamId = `${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
191
+ let isFirstChunk = true;
192
+ let buffer = '';
193
+ const fromUserId = this.config.accountId;
194
+ const conversationType = msg.conversationType;
195
+
196
+ // 1. 发送 typing 状态
197
+ await this._sendTypingStatus(fromUserId, targetId, conversationType);
198
+
199
+ this.log?.info(`[MessageHandler] 开始流式处理,streamId=${streamId}`);
200
+
201
+ // 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);
224
+ }
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
+ );
232
+ }
233
+
234
+ /**
235
+ * 发送 typing 状态(通过 Python 后端代理)
236
+ */
237
+ async _sendTypingStatus(fromUserId, targetId, conversationType) {
238
+ if (!this.isStreamingEnabled) return;
239
+ try {
240
+ await axios.post(
241
+ `${this.config.apiBaseUrl}/im/api/proxy/stream/typing`,
242
+ {
243
+ fromUserId,
244
+ targetId,
245
+ conversationType
246
+ },
247
+ { timeout: 5000 }
248
+ );
249
+ this.log?.info(`[MessageHandler] typing 状态已发送: ${fromUserId} -> ${targetId}`);
250
+ } catch (err) {
251
+ this.log?.warn(`[MessageHandler] 发送 typing 状态失败: ${err.message}`);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * 发送流式消息片段(通过 Python 后端代理)
257
+ */
258
+ async _sendStreamChunk(fromUserId, targetId, conversationType, content, streamId, isFirstChunk, isLastChunk) {
259
+ if (!this.isStreamingEnabled) return;
260
+ try {
261
+ await axios.post(
262
+ `${this.config.apiBaseUrl}/im/api/proxy/stream/publish`,
263
+ {
264
+ fromUserId,
265
+ targetId,
266
+ content,
267
+ streamId,
268
+ isFirstChunk,
269
+ isLastChunk,
270
+ conversationType
271
+ },
272
+ { timeout: 10000 }
273
+ );
274
+ } catch (err) {
275
+ this.log?.warn(`[MessageHandler] 发送流式消息失败: ${err.message}`);
276
+ }
277
+ }
278
+
279
+ parseCommand(raw, senderId) {
280
+ const trimmed = raw.slice(1).trim();
281
+ const parts = trimmed.split(/\s+/);
282
+ return {
283
+ command: parts[0] || '',
284
+ args: parts.slice(1),
285
+ rawMessage: raw,
286
+ senderId
287
+ };
288
+ }
289
+ }
290
+
291
+ module.exports = { MessageHandler };
@@ -282,6 +282,129 @@ class OpenClawClient {
282
282
  return this.chatViaCLI(message, fromUser);
283
283
  }
284
284
 
285
+ /**
286
+ * 流式调用 OpenClaw(SSE)
287
+ * 调用 OpenClaw Gateway 的 /v1/chat/completions SSE 端点
288
+ *
289
+ * @param {string} message - 用户消息
290
+ * @param {string} fromUser - 用户ID(用于session隔离)
291
+ * @param {Function} onDelta - 每次收到内容块时的回调 (chunk: string) => void
292
+ * @param {Function} onDone - 流结束时的回调 (fullText: string) => void
293
+ * @param {Function} onError - 出错时的回调 (error: Error) => void
294
+ * @returns {Promise<void>}
295
+ */
296
+ async chatStream(message, fromUser, onDelta, onDone, onError) {
297
+ if (!message || !message.trim()) {
298
+ onError?.(new Error('消息内容为空'));
299
+ return;
300
+ }
301
+
302
+ const gatewayReady = await this.ensureGatewayRunning();
303
+ if (!gatewayReady) {
304
+ const err = new Error('OpenClaw gateway 启动失败');
305
+ this.log?.error('[OpenClawClient] ' + err.message);
306
+ onError?.(err);
307
+ return;
308
+ }
309
+
310
+ this.log?.info(`[OpenClawClient] 准备 SSE 流式调用 OpenClaw,from=${fromUser}`);
311
+
312
+ const gatewayToken = getGatewayToken();
313
+ const sessionId = `clawmessenger-${fromUser}`;
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?.(err);
337
+ return;
338
+ }
339
+ }
340
+ }
341
+
342
+ async _doChatStream(apiUrl, gatewayToken, sessionId, message, onDelta, onDone) {
343
+ const headers = {
344
+ 'Content-Type': 'application/json',
345
+ 'Accept': 'text/event-stream'
346
+ };
347
+ if (gatewayToken) {
348
+ headers['Authorization'] = `Bearer ${gatewayToken}`;
349
+ }
350
+
351
+ const payload = {
352
+ model: 'openclaw',
353
+ messages: [
354
+ { role: 'user', content: message }
355
+ ],
356
+ stream: true,
357
+ session_id: sessionId
358
+ };
359
+
360
+ let fullText = '';
361
+ let buffer = '';
362
+
363
+ const response = await axios.post(apiUrl, payload, {
364
+ headers,
365
+ responseType: 'stream',
366
+ timeout: 600000 // 10 分钟
367
+ });
368
+
369
+ return new Promise((resolve, reject) => {
370
+ response.data.on('data', (chunk) => {
371
+ buffer += chunk.toString();
372
+ const lines = buffer.split('\n');
373
+ buffer = lines.pop(); // 保留未完整的最后一行
374
+
375
+ for (const line of lines) {
376
+ const trimmed = line.trim();
377
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
378
+
379
+ const dataStr = trimmed.slice(6).trim();
380
+ if (dataStr === '[DONE]') continue;
381
+
382
+ try {
383
+ const data = JSON.parse(dataStr);
384
+ const delta = data.choices?.[0]?.delta?.content;
385
+ if (delta) {
386
+ fullText += delta;
387
+ onDelta?.(delta);
388
+ }
389
+ } catch {
390
+ // 忽略无法解析的 JSON 行
391
+ }
392
+ }
393
+ });
394
+
395
+ response.data.on('end', () => {
396
+ this.log?.info(`[OpenClawClient] SSE 流结束,总长度: ${fullText.length}`);
397
+ onDone?.(fullText);
398
+ resolve();
399
+ });
400
+
401
+ response.data.on('error', (err) => {
402
+ this.log?.error(`[OpenClawClient] SSE 流错误: ${err.message}`);
403
+ reject(err);
404
+ });
405
+ });
406
+ }
407
+
285
408
  async chatViaCLI(message, fromUser) {
286
409
  const sessionId = `clawmessenger-${fromUser}`;
287
410
 
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}`);
@@ -198,12 +199,29 @@ function loadRongCloudConfig() {
198
199
  config.appKey = localConfig.appKey || config.appKey;
199
200
  if (localConfig.token) config.token = localConfig.token;
200
201
  if (localConfig.accountId) config.accountId = localConfig.accountId;
202
+ if (localConfig.appSecret) config.appSecret = localConfig.appSecret;
203
+ if (localConfig.apiBaseUrl) config.apiBaseUrl = localConfig.apiBaseUrl;
201
204
  log.info(`[WORKER] 从本地配置加载: appKey=${config.appKey?.substring(0, 8)}...`);
202
205
  }
203
206
  } catch (err) {
204
207
  log.error(`[WORKER] 加载本地配置失败: ${err.message}`);
205
208
  }
206
209
 
210
+ // 加载 apiBaseUrl(Python 后端地址,用于代理发送流式消息)
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
+ }
224
+
207
225
  if (!config.appKey) {
208
226
  config.appKey = process.env.DM_APP_KEY || 'bmdehs6pbyyks';
209
227
  log.info(`[WORKER] 使用默认 appKey: ${config.appKey}`);