claw-subagent-service 0.0.177 → 0.0.179

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.177",
3
+ "version": "0.0.179",
4
4
  "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -78,12 +78,72 @@ class RongyunMessageHandler {
78
78
  this.commandLock = false;
79
79
  this.commandLockTimer = null;
80
80
  this.messageSender = null;
81
+ this.serverAPI = null;
82
+ // 流式消息队列:确保片段串行发送
83
+ this._streamQueue = Promise.resolve();
84
+ // 存储流式消息的 RongCloud messageUID:streamId -> messageUID
85
+ this._streamMessageUIDs = new Map();
81
86
  }
82
87
 
83
88
  setMessageSender(messageSender) {
84
89
  this.messageSender = messageSender;
85
90
  }
86
91
 
92
+ setServerAPI(serverAPI) {
93
+ this.serverAPI = serverAPI;
94
+ }
95
+
96
+ /**
97
+ * 发送流式消息片段(直接调用融云 API)
98
+ * @param {string} fromUserId - 发送者ID
99
+ * @param {string} targetId - 目标用户ID
100
+ * @param {string} content - 消息内容
101
+ * @param {string} streamId - 流式消息ID
102
+ * @param {boolean} isFirstChunk - 是否首流
103
+ * @param {boolean} isLastChunk - 是否尾流
104
+ * @param {number} seq - 片段序号
105
+ */
106
+ async _sendStreamChunk(fromUserId, targetId, content, streamId, isFirstChunk, isLastChunk, seq = 1) {
107
+ const contentPreview = typeof content === 'string' ? content.substring(0, 100) : JSON.stringify(content).substring(0, 100);
108
+ this.logInfo(`[RongyunMessageHandler] _sendStreamChunk: target=${targetId}, streamId=${streamId}, seq=${seq}, first=${isFirstChunk}, last=${isLastChunk}, content_len=${content?.length || 0}`);
109
+
110
+ if (!this.serverAPI) {
111
+ this.logWarn('[RongyunMessageHandler] _sendStreamChunk skipped: serverAPI not configured');
112
+ return;
113
+ }
114
+
115
+ // 使用队列确保流式消息片段串行发送,避免并发导致后端处理错乱
116
+ this._streamQueue = this._streamQueue.then(async () => {
117
+ try {
118
+ // 获取已存储的 RongCloud messageUID(首流响应返回的)
119
+ const messageUID = this._streamMessageUIDs.get(streamId);
120
+
121
+ const result = await this.serverAPI.sendStreamPrivate({
122
+ fromUserId,
123
+ toUserId: targetId,
124
+ content,
125
+ streamId,
126
+ isFirstChunk,
127
+ isLastChunk,
128
+ seq,
129
+ messageUID
130
+ });
131
+
132
+ // 首流时存储 RongCloud 返回的 messageUID
133
+ if (isFirstChunk && result?.messageUID) {
134
+ this._streamMessageUIDs.set(streamId, result.messageUID);
135
+ this.logInfo(`[RongyunMessageHandler] 首流 messageUID 已存储: ${result.messageUID}, streamId=${streamId}`);
136
+ }
137
+
138
+ this.logInfo(`[RongyunMessageHandler] _sendStreamChunk 成功: seq=${seq}`);
139
+ } catch (err) {
140
+ this.logWarn(`[RongyunMessageHandler] 发送流式消息失败: ${err.message}, seq=${seq}`);
141
+ }
142
+ });
143
+
144
+ await this._streamQueue;
145
+ }
146
+
87
147
  logInfo(message) {
88
148
  if (this.log?.info) {
89
149
  this.log.info(message);
@@ -312,11 +372,32 @@ class RongyunMessageHandler {
312
372
  }
313
373
 
314
374
  let fullResponse = '';
375
+ let buffer = ''; // 用于流式发送的缓冲区
315
376
  const chatTimeoutMs = (this.config.chatTimeout || 600) * 1000;
377
+
378
+ // 生成流式消息唯一ID
379
+ const streamId = `stream-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
380
+ let seq = 0;
381
+ let hasSentChunk = false;
382
+ const fromUserId = this.config.accountId || '';
316
383
 
317
384
  try {
318
385
  await forwardChatMessage(sessionId, content, async (delta) => {
319
386
  fullResponse += delta;
387
+ buffer += delta;
388
+
389
+ // 当缓冲区达到一定大小或包含标点时,发送流式片段
390
+ // 使用 50 字符作为触发阈值(与 message-handler.js 保持一致)
391
+ if (buffer.length >= 50) {
392
+ seq += 1;
393
+ const chunkToSend = buffer;
394
+ buffer = ''; // 清空缓冲区
395
+
396
+ // 首流时发送首流标记
397
+ const isFirstChunk = seq === 1;
398
+ await this._sendStreamChunk(fromUserId, sourceId, chunkToSend, streamId, isFirstChunk, false, seq);
399
+ hasSentChunk = true;
400
+ }
320
401
  }, (level, message) => {
321
402
  if (level === 'ERROR') {
322
403
  this.logError(`[CHAT-API] ${message}`);
@@ -327,21 +408,50 @@ class RongyunMessageHandler {
327
408
  }
328
409
  }, chatTimeoutMs);
329
410
 
411
+ // 发送剩余缓冲区内容
412
+ if (buffer.length > 0) {
413
+ seq += 1;
414
+ const isFirstChunk = seq === 1;
415
+ await this._sendStreamChunk(fromUserId, sourceId, buffer, streamId, isFirstChunk, false, seq);
416
+ hasSentChunk = true;
417
+ buffer = '';
418
+ }
419
+
420
+ // 发送尾流标记
421
+ if (hasSentChunk) {
422
+ seq += 1;
423
+ await this._sendStreamChunk(fromUserId, sourceId, '', streamId, false, true, seq);
424
+ }
425
+
426
+ // 同时发送完整的 command 消息作为历史记录(兼容旧前端)
330
427
  await this.sendResponse(RongyunMessageTypeEnum.CHAT_MESSAGE, {
331
428
  status: 'success',
332
429
  message: 'Response received',
333
430
  content: fullResponse,
334
431
  metadata: {}
335
432
  }, requestId, sourceId);
433
+
434
+ // 清理已存储的 messageUID
435
+ this._streamMessageUIDs.delete(streamId);
336
436
  } catch (e) {
337
437
  const msg = e instanceof Error ? e.message : String(e);
338
438
  this.logError(`聊天消息处理异常: ${msg}`);
439
+
440
+ // 如果已经开始流式发送,发送错误标记
441
+ if (hasSentChunk) {
442
+ seq += 1;
443
+ await this._sendStreamChunk(fromUserId, sourceId, `[错误] 转发失败: ${msg}`, streamId, false, true, seq);
444
+ }
445
+
339
446
  await this.sendResponse(RongyunMessageTypeEnum.CHAT_MESSAGE, {
340
447
  status: 'error',
341
448
  message: msg,
342
449
  content: `[错误] 转发失败: ${msg}`,
343
450
  metadata: {}
344
451
  }, requestId, sourceId);
452
+
453
+ // 清理已存储的 messageUID
454
+ this._streamMessageUIDs.delete(streamId);
345
455
  }
346
456
  }
347
457
 
@@ -176,9 +176,9 @@ class RongyunMessageSender {
176
176
 
177
177
  // 优先使用自定义消息类型发送 P2P 消息
178
178
  let result;
179
- if (this.rongcloudClient.ServiceChatMessage && msgType.includes('service')) {
180
- // 客服相关消息使用 service_chat 自定义消息类型
181
- // 对于客服消息,直接将业务内容放在顶层,方便前端解析
179
+ // 客服消息和聊天消息都将业务内容放在顶层,方便前端解析
180
+ if (this.rongcloudClient.ServiceChatMessage && (msgType.includes('service') || msgType === 'chat_message')) {
181
+ // 对于客服/聊天消息,直接将业务内容放在顶层,方便前端解析
182
182
  const serviceChatPayload = {
183
183
  msg_type: msgType,
184
184
  ...content, // 展开业务内容(status, content, sessionId, userId 等)
@@ -245,6 +245,65 @@ class RongyunMessageSender {
245
245
  requestId
246
246
  );
247
247
  }
248
+
249
+ /**
250
+ * 发送流式消息片段(P2P)
251
+ * 使用融云服务端API发送 RC:StreamMsg
252
+ * @param {Object} options - 流式消息选项
253
+ * @param {string} options.targetId - 目标用户ID
254
+ * @param {string} options.content - 消息片段内容
255
+ * @param {string} options.streamId - 流式消息ID
256
+ * @param {number} options.seq - 片段序号
257
+ * @param {boolean} options.isFirstChunk - 是否首流
258
+ * @param {boolean} options.isLastChunk - 是否尾流
259
+ * @param {string} options.messageUID - 首流返回的messageUID(后续流使用)
260
+ * @returns {Promise<Object>} 发送结果
261
+ */
262
+ async sendStreamToTarget({
263
+ targetId,
264
+ content,
265
+ streamId,
266
+ seq = 1,
267
+ isFirstChunk = false,
268
+ isLastChunk = false,
269
+ messageUID = null
270
+ }) {
271
+ // 需要 serverAPI 支持
272
+ if (!this.serverAPI) {
273
+ this.log?.error('[RongyunMessageSender] serverAPI 未设置,无法发送流式消息');
274
+ return false;
275
+ }
276
+
277
+ try {
278
+ const fromUserId = this.config.accountId || '';
279
+
280
+ const result = await this.serverAPI.sendStreamPrivate({
281
+ fromUserId,
282
+ toUserId: targetId,
283
+ content,
284
+ streamId,
285
+ isFirstChunk,
286
+ isLastChunk,
287
+ seq,
288
+ streamType: 'text',
289
+ messageUID
290
+ });
291
+
292
+ this.log?.info(`[RongyunMessageSender] 流式消息已发送: seq=${seq}, first=${isFirstChunk}, last=${isLastChunk}`);
293
+ return result;
294
+ } catch (err) {
295
+ this.log?.error(`[RongyunMessageSender] 发送流式消息失败: ${err.message}`);
296
+ return false;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * 设置 serverAPI(用于发送流式消息)
302
+ * @param {RongCloudServerAPI} serverAPI
303
+ */
304
+ setServerAPI(serverAPI) {
305
+ this.serverAPI = serverAPI;
306
+ }
248
307
  }
249
308
 
250
309
  module.exports = {
package/service/worker.js CHANGED
@@ -8,6 +8,8 @@ const { createLogger } = require('./logger');
8
8
  const { RongCloudClient, MessageHandler, ensurePluginsAllow } = require('./rongcloud');
9
9
  const { RongyunMessageHandler } = require('./modules/rongyun-message-handler');
10
10
  const { RongyunMessageSender } = require('./modules/rongyun-message-sender');
11
+ const { RongCloudServerAPI } = require('./rongcloud/rongcloud-server-api');
12
+ const { SystemConfigManager } = require('./modules/system-config');
11
13
  const { HeartbeatManager, DashboardReporter } = require('./modules/heartbeat-dashboard');
12
14
  const { getOpenClawStatus } = require('./modules/port-checker');
13
15
  const { getMacAddress } = require('./modules/mac-address');
@@ -340,12 +342,20 @@ async function initRongCloud() {
340
342
 
341
343
  rongcloudClient = new RongCloudClient(rongcloudConfig, log);
342
344
 
345
+ // 创建系统配置管理器(用于融云服务端API)
346
+ const configManager = new SystemConfigManager(rongcloudConfig, log);
347
+
348
+ // 创建融云服务端API客户端(用于发送流式消息)
349
+ const serverAPI = new RongCloudServerAPI(configManager, log);
350
+
343
351
  // 创建消息发送器
344
352
  const messageSender = new RongyunMessageSender(rongcloudClient, rongcloudConfig, log);
353
+ messageSender.setServerAPI(serverAPI); // 注入 serverAPI,支持发送流式消息
345
354
 
346
355
  // 创建新的融云消息处理器(与桌面客户端对齐)
347
356
  const rongyunMessageHandler = new RongyunMessageHandler(rongcloudClient, rongcloudConfig, log);
348
357
  rongyunMessageHandler.setMessageSender(messageSender);
358
+ rongyunMessageHandler.setServerAPI(serverAPI); // 注入 serverAPI
349
359
 
350
360
  messageHandler = new MessageHandler(
351
361
  rongcloudConfig,