claw-subagent-service 0.0.72 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-subagent-service",
3
- "version": "0.0.72",
3
+ "version": "0.0.73",
4
4
  "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -85,6 +85,7 @@ function loadConfig() {
85
85
  scriptTimeout: localConfig.scriptTimeout || 180,
86
86
  successKeyword: localConfig.successKeyword || 'Success',
87
87
  chatTimeout: localConfig.chatTimeout || 600,
88
+ maxRounds: localConfig.maxRounds || 10,
88
89
  apiBaseUrl
89
90
  };
90
91
  }
@@ -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
  /**
@@ -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,16 +121,62 @@ 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
 
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;
144
+ }
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
+
97
162
  // 如果配置了代理地址,使用流式处理
98
163
  if (this.isStreamingEnabled) {
99
164
  try {
100
165
  await this.handleNormalMessageStream(msg);
166
+ // 流式处理成功,群聊轮数 +1
167
+ if (msg.conversationType === 3) {
168
+ this._incrementGroupRoundCount(msg.targetId);
169
+ }
101
170
  } catch (err) {
102
171
  this.log?.error(`[MessageHandler] 流式处理失败,回退到非流式: ${err.message}`);
103
172
  const reply = await this.handleNormalMessage(msg);
104
173
  if (reply) {
105
174
  const targetId = this.getReplyTarget(msg);
106
175
  await this.sendFn(targetId, reply, msg.conversationType);
176
+ // 非流式回退成功,群聊轮数 +1
177
+ if (msg.conversationType === 3) {
178
+ this._incrementGroupRoundCount(msg.targetId);
179
+ }
107
180
  }
108
181
  }
109
182
  } else {
@@ -112,6 +185,10 @@ class MessageHandler {
112
185
  if (reply) {
113
186
  const targetId = this.getReplyTarget(msg);
114
187
  await this.sendFn(targetId, reply, msg.conversationType);
188
+ // 非流式处理成功,群聊轮数 +1
189
+ if (msg.conversationType === 3) {
190
+ this._incrementGroupRoundCount(msg.targetId);
191
+ }
115
192
  }
116
193
  }
117
194
  } catch (err) {
@@ -174,6 +251,23 @@ class MessageHandler {
174
251
 
175
252
  // 2. 调用 OpenClaw SSE
176
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
+
177
271
  try {
178
272
  // 确保传入的内容是字符串(claw 类型消息 content 可能是对象)
179
273
  const chatContent = typeof msg.content === 'string' ? msg.content : (msg.content?.content || JSON.stringify(msg.content));
@@ -188,9 +282,18 @@ class MessageHandler {
188
282
  // 发送增量(delta),让前端做增量拼接,避免内容重复
189
283
  await this._sendStreamChunk(fromUserId, targetId, conversationType, delta, streamId, seq === 1, false, seq);
190
284
  hasSentChunk = true;
285
+ // 每次有输出时刷新 typing 状态
286
+ await refreshTyping();
191
287
  },
192
288
  async (fullText) => {
193
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
+
194
297
  if (buffer.trim()) {
195
298
  // 发送尾流:空字符串表示流结束,前端保留已拼接的完整内容
196
299
  seq += 1;
@@ -214,10 +317,29 @@ class MessageHandler {
214
317
 
215
318
  // 清理已存储的 messageUID,防止内存泄漏
216
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
+ }
217
332
  }
218
333
  );
219
334
  } catch (err) {
220
335
  this.log?.error(`[MessageHandler] 流式处理错误: ${err.message}`);
336
+
337
+ // 清除 typing 定时器
338
+ if (typingTimer) {
339
+ clearTimeout(typingTimer);
340
+ typingTimer = null;
341
+ }
342
+
221
343
  await this._sendStreamChunk(fromUserId, targetId, conversationType, '抱歉,AI 响应出现错误,请稍后重试。', streamId, true, true, 1);
222
344
 
223
345
  // 错误时也要清理