claw-subagent-service 0.0.71 → 0.0.73
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
|
@@ -16,6 +16,9 @@ class MessageHandler {
|
|
|
16
16
|
this._streamQueue = Promise.resolve();
|
|
17
17
|
// 存储流式消息的 RongCloud messageUID:streamId -> messageUID
|
|
18
18
|
this._streamMessageUIDs = new Map();
|
|
19
|
+
// 群聊对话轮数统计:groupId -> currentRound
|
|
20
|
+
this._groupRoundCounts = new Map();
|
|
21
|
+
this._maxRounds = config.maxRounds || 10;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -42,7 +45,7 @@ class MessageHandler {
|
|
|
42
45
|
return false;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
|
-
const allowedTypes = ['RC:TxtMsg'
|
|
48
|
+
const allowedTypes = ['RC:TxtMsg'];
|
|
46
49
|
if (!allowedTypes.includes(msg.messageType)) {
|
|
47
50
|
this.log?.info(`[MessageHandler] 忽略非文本消息: ${msg.messageType}`);
|
|
48
51
|
return false;
|
|
@@ -84,6 +87,30 @@ class MessageHandler {
|
|
|
84
87
|
return true;
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
/**
|
|
91
|
+
* 获取群聊当前轮数
|
|
92
|
+
*/
|
|
93
|
+
_getGroupRoundCount(groupId) {
|
|
94
|
+
return this._groupRoundCounts.get(groupId) || 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 增加群聊轮数
|
|
99
|
+
*/
|
|
100
|
+
_incrementGroupRoundCount(groupId) {
|
|
101
|
+
const current = this._getGroupRoundCount(groupId);
|
|
102
|
+
this._groupRoundCounts.set(groupId, current + 1);
|
|
103
|
+
this.log?.info(`[MessageHandler] 群聊 ${groupId} 轮数 +1,当前: ${current + 1}/${this._maxRounds}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 重置群聊轮数
|
|
108
|
+
*/
|
|
109
|
+
_resetGroupRoundCount(groupId) {
|
|
110
|
+
this._groupRoundCounts.set(groupId, 0);
|
|
111
|
+
this.log?.info(`[MessageHandler] 群聊 ${groupId} 轮数已重置`);
|
|
112
|
+
}
|
|
113
|
+
|
|
87
114
|
async handleMessage(msg) {
|
|
88
115
|
if (!this.shouldHandleMessage(msg)) {
|
|
89
116
|
return;
|
|
@@ -94,28 +121,73 @@ class MessageHandler {
|
|
|
94
121
|
const logContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
|
|
95
122
|
this.log?.info(`[MessageHandler] 收到消息 from=${msg.senderUserId}, type=${type}, content=${logContent.substring(0, 50)}`);
|
|
96
123
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
124
|
+
// 群聊轮数控制(仅对群聊生效)
|
|
125
|
+
if (msg.conversationType === 3) {
|
|
126
|
+
const groupId = msg.targetId;
|
|
127
|
+
const currentRounds = this._getGroupRoundCount(groupId);
|
|
128
|
+
|
|
129
|
+
// 处理内置轮数相关命令
|
|
130
|
+
if (type === MessageType.COMMAND) {
|
|
131
|
+
const payload = this.parseCommand(msg.content, msg.senderUserId);
|
|
132
|
+
if (payload.command === 'newround') {
|
|
133
|
+
this._resetGroupRoundCount(groupId);
|
|
134
|
+
await this.sendFn(groupId, `✅ 新一轮对话已开始,最大对话轮数为 ${this._maxRounds} 轮。`, msg.conversationType);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (payload.command === 'roundstatus') {
|
|
138
|
+
const remaining = Math.max(0, this._maxRounds - currentRounds);
|
|
139
|
+
const statusMsg = currentRounds >= this._maxRounds
|
|
140
|
+
? `⛔ 本轮对话已结束(已达 ${this._maxRounds} 轮)。发送 /newround 开启新一轮。`
|
|
141
|
+
: `📊 当前对话进度:第 ${currentRounds + 1}/${this._maxRounds} 轮,剩余 ${remaining} 轮。`;
|
|
142
|
+
await this.sendFn(groupId, statusMsg, msg.conversationType);
|
|
143
|
+
return;
|
|
112
144
|
}
|
|
113
|
-
}
|
|
114
|
-
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 检查是否已达最大轮数
|
|
148
|
+
if (currentRounds >= this._maxRounds) {
|
|
149
|
+
this.log?.info(`[MessageHandler] 群聊 ${groupId} 已达到最大轮数 ${this._maxRounds},拒绝处理`);
|
|
150
|
+
await this.sendFn(groupId, `⛔ 本轮对话已达到最大轮数(${this._maxRounds} 轮),对话已结束。\n\n发送 /newround 可开启新一轮对话。`, msg.conversationType);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 发送已读回执(fire-and-forget,不阻塞消息处理)
|
|
156
|
+
if (this.sendReadReceiptFn && msg.messageUId) {
|
|
157
|
+
this.sendReadReceiptFn(msg).catch((err) => {
|
|
158
|
+
this.log?.warn(`[MessageHandler] 发送已读回执失败: ${err.message}`);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 如果配置了代理地址,使用流式处理
|
|
163
|
+
if (this.isStreamingEnabled) {
|
|
164
|
+
try {
|
|
165
|
+
await this.handleNormalMessageStream(msg);
|
|
166
|
+
// 流式处理成功,群聊轮数 +1
|
|
167
|
+
if (msg.conversationType === 3) {
|
|
168
|
+
this._incrementGroupRoundCount(msg.targetId);
|
|
169
|
+
}
|
|
170
|
+
} catch (err) {
|
|
171
|
+
this.log?.error(`[MessageHandler] 流式处理失败,回退到非流式: ${err.message}`);
|
|
115
172
|
const reply = await this.handleNormalMessage(msg);
|
|
116
173
|
if (reply) {
|
|
117
174
|
const targetId = this.getReplyTarget(msg);
|
|
118
175
|
await this.sendFn(targetId, reply, msg.conversationType);
|
|
176
|
+
// 非流式回退成功,群聊轮数 +1
|
|
177
|
+
if (msg.conversationType === 3) {
|
|
178
|
+
this._incrementGroupRoundCount(msg.targetId);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
// 降级到非流式处理
|
|
184
|
+
const reply = await this.handleNormalMessage(msg);
|
|
185
|
+
if (reply) {
|
|
186
|
+
const targetId = this.getReplyTarget(msg);
|
|
187
|
+
await this.sendFn(targetId, reply, msg.conversationType);
|
|
188
|
+
// 非流式处理成功,群聊轮数 +1
|
|
189
|
+
if (msg.conversationType === 3) {
|
|
190
|
+
this._incrementGroupRoundCount(msg.targetId);
|
|
119
191
|
}
|
|
120
192
|
}
|
|
121
193
|
}
|
|
@@ -127,9 +199,6 @@ class MessageHandler {
|
|
|
127
199
|
}
|
|
128
200
|
|
|
129
201
|
getMessageType(msg) {
|
|
130
|
-
if (msg.messageType === 'claw') {
|
|
131
|
-
return MessageType.CLAW;
|
|
132
|
-
}
|
|
133
202
|
const text = typeof msg.content === 'string' ? msg.content : (msg.content?.content || '');
|
|
134
203
|
if (text.startsWith('/')) {
|
|
135
204
|
return MessageType.COMMAND;
|
|
@@ -164,47 +233,6 @@ class MessageHandler {
|
|
|
164
233
|
await this.sendFn(targetId, reply, msg.conversationType);
|
|
165
234
|
}
|
|
166
235
|
|
|
167
|
-
async handleClaw(msg) {
|
|
168
|
-
const targetId = this.getReplyTarget(msg);
|
|
169
|
-
|
|
170
|
-
// 发送已读回执(fire-and-forget,不阻塞)
|
|
171
|
-
if (this.sendReadReceiptFn) {
|
|
172
|
-
this.sendReadReceiptFn(msg).catch(() => {});
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// 如果配置了代理地址,使用流式处理
|
|
176
|
-
if (this.isStreamingEnabled) {
|
|
177
|
-
try {
|
|
178
|
-
await this.handleNormalMessageStream(msg);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
this.log?.error(`[MessageHandler] 流式处理失败,回退到 CLI: ${err.message}`);
|
|
181
|
-
// 回退到非流式 CLI 调用
|
|
182
|
-
try {
|
|
183
|
-
const reply = await this.openclawClient.chat(msg.content, msg.senderUserId);
|
|
184
|
-
if (reply) {
|
|
185
|
-
this.log?.info(`[MessageHandler] AI 回复: ${reply.substring(0, 50)}...`);
|
|
186
|
-
await this.sendFn(targetId, reply, msg.conversationType);
|
|
187
|
-
}
|
|
188
|
-
} catch (cliErr) {
|
|
189
|
-
this.log?.error(`[MessageHandler] CLI 回退也失败: ${cliErr.message}`);
|
|
190
|
-
await this.sendFn(targetId, `❌ 处理失败: ${cliErr.message}`, msg.conversationType);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// 降级:后台执行 openclaw CLI,不阻塞消息队列
|
|
197
|
-
this.openclawClient.chat(msg.content, msg.senderUserId)
|
|
198
|
-
.then(reply => {
|
|
199
|
-
this.log?.info(`[MessageHandler] AI 回复: ${reply.substring(0, 50)}...`);
|
|
200
|
-
this.sendFn(targetId, reply, msg.conversationType).catch(() => {});
|
|
201
|
-
})
|
|
202
|
-
.catch(err => {
|
|
203
|
-
this.log?.error(`[MessageHandler] OpenClaw 调用失败: ${err.message}`);
|
|
204
|
-
this.sendFn(targetId, `❌ 处理失败: ${err.message}`, msg.conversationType).catch(() => {});
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
236
|
/**
|
|
209
237
|
* 流式处理普通消息
|
|
210
238
|
*/
|
|
@@ -223,6 +251,23 @@ class MessageHandler {
|
|
|
223
251
|
|
|
224
252
|
// 2. 调用 OpenClaw SSE
|
|
225
253
|
let hasSentChunk = false;
|
|
254
|
+
// typing 刷新定时器:每次 delta 时刷新 typing,超时后自动停止
|
|
255
|
+
let typingTimer = null;
|
|
256
|
+
const refreshTyping = async () => {
|
|
257
|
+
// 清除旧的超时
|
|
258
|
+
if (typingTimer) {
|
|
259
|
+
clearTimeout(typingTimer);
|
|
260
|
+
typingTimer = null;
|
|
261
|
+
}
|
|
262
|
+
// 发送 typing
|
|
263
|
+
await this._sendTypingStatus(fromUserId, targetId, conversationType);
|
|
264
|
+
// 设置 3 秒超时,如果 3 秒内没有新的 delta,typing 自动消失
|
|
265
|
+
typingTimer = setTimeout(() => {
|
|
266
|
+
this.log?.info(`[MessageHandler] typing 超时自动清除: streamId=${streamId}`);
|
|
267
|
+
typingTimer = null;
|
|
268
|
+
}, 3000);
|
|
269
|
+
};
|
|
270
|
+
|
|
226
271
|
try {
|
|
227
272
|
// 确保传入的内容是字符串(claw 类型消息 content 可能是对象)
|
|
228
273
|
const chatContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || JSON.stringify(msg.content));
|
|
@@ -237,9 +282,18 @@ class MessageHandler {
|
|
|
237
282
|
// 发送增量(delta),让前端做增量拼接,避免内容重复
|
|
238
283
|
await this._sendStreamChunk(fromUserId, targetId, conversationType, delta, streamId, seq === 1, false, seq);
|
|
239
284
|
hasSentChunk = true;
|
|
285
|
+
// 每次有输出时刷新 typing 状态
|
|
286
|
+
await refreshTyping();
|
|
240
287
|
},
|
|
241
288
|
async (fullText) => {
|
|
242
289
|
this.log?.info(`[MessageHandler] onDone 触发, fullText.length=${fullText.length}, buffer.length=${buffer.length}, hasSentChunk=${hasSentChunk}`);
|
|
290
|
+
|
|
291
|
+
// 清除 typing 定时器
|
|
292
|
+
if (typingTimer) {
|
|
293
|
+
clearTimeout(typingTimer);
|
|
294
|
+
typingTimer = null;
|
|
295
|
+
}
|
|
296
|
+
|
|
243
297
|
if (buffer.trim()) {
|
|
244
298
|
// 发送尾流:空字符串表示流结束,前端保留已拼接的完整内容
|
|
245
299
|
seq += 1;
|
|
@@ -263,10 +317,29 @@ class MessageHandler {
|
|
|
263
317
|
|
|
264
318
|
// 清理已存储的 messageUID,防止内存泄漏
|
|
265
319
|
this._streamMessageUIDs.delete(streamId);
|
|
320
|
+
|
|
321
|
+
// 发送持久化的普通文本消息作为历史记录(融云会保存 RC:TxtMsg)
|
|
322
|
+
if (buffer.trim()) {
|
|
323
|
+
try {
|
|
324
|
+
this.log?.info(`[MessageHandler] 发送历史记录文本消息: length=${buffer.length}`);
|
|
325
|
+
// 使用特殊前缀标记,前端识别后跳过渲染(避免与流式消息重复显示)
|
|
326
|
+
const historyContent = `__STREAM_HISTORY__:${buffer}`;
|
|
327
|
+
await this.sendFn(targetId, historyContent, conversationType);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
this.log?.error(`[MessageHandler] 发送历史记录失败: ${err.message}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
266
332
|
}
|
|
267
333
|
);
|
|
268
334
|
} catch (err) {
|
|
269
335
|
this.log?.error(`[MessageHandler] 流式处理错误: ${err.message}`);
|
|
336
|
+
|
|
337
|
+
// 清除 typing 定时器
|
|
338
|
+
if (typingTimer) {
|
|
339
|
+
clearTimeout(typingTimer);
|
|
340
|
+
typingTimer = null;
|
|
341
|
+
}
|
|
342
|
+
|
|
270
343
|
await this._sendStreamChunk(fromUserId, targetId, conversationType, '抱歉,AI 响应出现错误,请稍后重试。', streamId, true, true, 1);
|
|
271
344
|
|
|
272
345
|
// 错误时也要清理
|