claw-subagent-service 0.0.58 → 0.0.60
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
|
@@ -72,7 +72,8 @@ function loadConfig() {
|
|
|
72
72
|
openclawPort: localConfig.openclawPort || 18789,
|
|
73
73
|
scriptTimeout: localConfig.scriptTimeout || 180,
|
|
74
74
|
successKeyword: localConfig.successKeyword || 'Success',
|
|
75
|
-
chatTimeout: localConfig.chatTimeout || 600
|
|
75
|
+
chatTimeout: localConfig.chatTimeout || 600,
|
|
76
|
+
apiBaseUrl: process.env.API_BASE_URL || localConfig.apiBaseUrl || clawBridgeConfig.apiBaseUrl || 'http://127.0.0.1:5000'
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
this.
|
|
10
|
-
this.
|
|
11
|
-
this.
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
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
|
-
if (
|
|
56
|
-
|
|
57
|
-
mentions
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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,108 @@ 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
|
+
const apiUrl = 'http://127.0.0.1:18789/v1/chat/completions';
|
|
315
|
+
|
|
316
|
+
const headers = {
|
|
317
|
+
'Content-Type': 'application/json',
|
|
318
|
+
'Accept': 'text/event-stream'
|
|
319
|
+
};
|
|
320
|
+
if (gatewayToken) {
|
|
321
|
+
headers['Authorization'] = `Bearer ${gatewayToken}`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const payload = {
|
|
325
|
+
model: 'openclaw',
|
|
326
|
+
messages: [
|
|
327
|
+
{ role: 'user', content: message }
|
|
328
|
+
],
|
|
329
|
+
stream: true,
|
|
330
|
+
session_id: sessionId
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
let fullText = '';
|
|
334
|
+
let buffer = '';
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const response = await axios.post(apiUrl, payload, {
|
|
338
|
+
headers,
|
|
339
|
+
responseType: 'stream',
|
|
340
|
+
timeout: 600000 // 10 分钟
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
response.data.on('data', (chunk) => {
|
|
344
|
+
buffer += chunk.toString();
|
|
345
|
+
const lines = buffer.split('\n');
|
|
346
|
+
buffer = lines.pop(); // 保留未完整的最后一行
|
|
347
|
+
|
|
348
|
+
for (const line of lines) {
|
|
349
|
+
const trimmed = line.trim();
|
|
350
|
+
if (!trimmed || !trimmed.startsWith('data: ')) continue;
|
|
351
|
+
|
|
352
|
+
const dataStr = trimmed.slice(6).trim();
|
|
353
|
+
if (dataStr === '[DONE]') continue;
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const data = JSON.parse(dataStr);
|
|
357
|
+
const delta = data.choices?.[0]?.delta?.content;
|
|
358
|
+
if (delta) {
|
|
359
|
+
fullText += delta;
|
|
360
|
+
onDelta?.(delta);
|
|
361
|
+
}
|
|
362
|
+
} catch {
|
|
363
|
+
// 忽略无法解析的 JSON 行
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
return new Promise((resolve, reject) => {
|
|
369
|
+
response.data.on('end', () => {
|
|
370
|
+
this.log?.info(`[OpenClawClient] SSE 流结束,总长度: ${fullText.length}`);
|
|
371
|
+
onDone?.(fullText);
|
|
372
|
+
resolve();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
response.data.on('error', (err) => {
|
|
376
|
+
this.log?.error(`[OpenClawClient] SSE 流错误: ${err.message}`);
|
|
377
|
+
onError?.(err);
|
|
378
|
+
reject(err);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
} catch (err) {
|
|
382
|
+
this.log?.error(`[OpenClawClient] SSE 请求失败: ${err.message}`);
|
|
383
|
+
onError?.(err);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
285
387
|
async chatViaCLI(message, fromUser) {
|
|
286
388
|
const sessionId = `clawmessenger-${fromUser}`;
|
|
287
389
|
|
package/service/worker.js
CHANGED
|
@@ -198,12 +198,16 @@ function loadRongCloudConfig() {
|
|
|
198
198
|
config.appKey = localConfig.appKey || config.appKey;
|
|
199
199
|
if (localConfig.token) config.token = localConfig.token;
|
|
200
200
|
if (localConfig.accountId) config.accountId = localConfig.accountId;
|
|
201
|
+
if (localConfig.appSecret) config.appSecret = localConfig.appSecret;
|
|
201
202
|
log.info(`[WORKER] 从本地配置加载: appKey=${config.appKey?.substring(0, 8)}...`);
|
|
202
203
|
}
|
|
203
204
|
} catch (err) {
|
|
204
205
|
log.error(`[WORKER] 加载本地配置失败: ${err.message}`);
|
|
205
206
|
}
|
|
206
207
|
|
|
208
|
+
// 加载 apiBaseUrl(Python 后端地址,用于代理发送流式消息)
|
|
209
|
+
config.apiBaseUrl = process.env.API_BASE_URL || config.apiBaseUrl || 'http://127.0.0.1:5000';
|
|
210
|
+
|
|
207
211
|
if (!config.appKey) {
|
|
208
212
|
config.appKey = process.env.DM_APP_KEY || 'bmdehs6pbyyks';
|
|
209
213
|
log.info(`[WORKER] 使用默认 appKey: ${config.appKey}`);
|