claw-subagent-service 0.0.176 → 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.176",
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);
@@ -285,7 +345,7 @@ class RongyunMessageHandler {
285
345
  const roomId = data.room_id;
286
346
  const sessionId = data.gateway_session_id || data.session_id;
287
347
  // 使用解析后的 content(聊天内容),如果没有则使用原始 content
288
- const content = data.content || data._raw_content;
348
+ let content = data.content || data._raw_content;
289
349
  const requestId = data.request_id;
290
350
  const sourceId = data.source_im_id;
291
351
 
@@ -301,12 +361,43 @@ class RongyunMessageHandler {
301
361
  return;
302
362
  }
303
363
 
364
+ // 语音消息:先进行语音识别
365
+ if (data.voiceUrl) {
366
+ const voiceText = await this._recognizeVoice(data.voiceUrl, data.voiceDuration);
367
+ if (voiceText !== null) {
368
+ content = `[语音转文字] ${voiceText}`;
369
+ } else {
370
+ content = `[语音消息,转文字失败] ${content}`;
371
+ }
372
+ }
373
+
304
374
  let fullResponse = '';
375
+ let buffer = ''; // 用于流式发送的缓冲区
305
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 || '';
306
383
 
307
384
  try {
308
385
  await forwardChatMessage(sessionId, content, async (delta) => {
309
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
+ }
310
401
  }, (level, message) => {
311
402
  if (level === 'ERROR') {
312
403
  this.logError(`[CHAT-API] ${message}`);
@@ -317,21 +408,50 @@ class RongyunMessageHandler {
317
408
  }
318
409
  }, chatTimeoutMs);
319
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 消息作为历史记录(兼容旧前端)
320
427
  await this.sendResponse(RongyunMessageTypeEnum.CHAT_MESSAGE, {
321
428
  status: 'success',
322
429
  message: 'Response received',
323
430
  content: fullResponse,
324
431
  metadata: {}
325
432
  }, requestId, sourceId);
433
+
434
+ // 清理已存储的 messageUID
435
+ this._streamMessageUIDs.delete(streamId);
326
436
  } catch (e) {
327
437
  const msg = e instanceof Error ? e.message : String(e);
328
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
+
329
446
  await this.sendResponse(RongyunMessageTypeEnum.CHAT_MESSAGE, {
330
447
  status: 'error',
331
448
  message: msg,
332
449
  content: `[错误] 转发失败: ${msg}`,
333
450
  metadata: {}
334
451
  }, requestId, sourceId);
452
+
453
+ // 清理已存储的 messageUID
454
+ this._streamMessageUIDs.delete(streamId);
335
455
  }
336
456
  }
337
457
 
@@ -766,6 +886,53 @@ class RongyunMessageHandler {
766
886
  this.logError(`发送响应失败: ${msg}`);
767
887
  }
768
888
  }
889
+
890
+ /**
891
+ * 语音识别:调用后端百度语音 API 将语音转为文字
892
+ */
893
+ async _recognizeVoice(voiceUrl, voiceDuration) {
894
+ try {
895
+ if (!voiceUrl) {
896
+ this.logWarn('[_recognizeVoice] 语音 URL 为空,跳过识别');
897
+ return null;
898
+ }
899
+
900
+ // 从 URL 提取扩展名并映射为百度支持的格式
901
+ const urlPath = voiceUrl.split('?')[0];
902
+ const ext = urlPath.split('.').pop()?.toLowerCase() || '';
903
+ const fmtMap = { aac: 'm4a', ogg: 'mp3', oga: 'mp3', opus: 'mp3' };
904
+ let format = fmtMap[ext] || ext;
905
+ if (!['pcm', 'wav', 'amr', 'm4a', 'mp3'].includes(format)) {
906
+ format = 'mp3';
907
+ }
908
+
909
+ // 采样率修正:amr 强制 8000,其余兜底 16000
910
+ let sampleRate = 16000;
911
+ if (format === 'amr') sampleRate = 8000;
912
+
913
+ const axios = require('axios');
914
+ const apiUrl = `${this.config.apiBaseUrl}/im/api/voice/recognize`;
915
+ this.logInfo(`[_recognizeVoice] 调用语音识别 API: ${apiUrl}, format=${format}, sampleRate=${sampleRate}`);
916
+
917
+ const response = await axios.post(apiUrl, {
918
+ audioUrl: voiceUrl,
919
+ format,
920
+ sampleRate,
921
+ }, { timeout: 30000 });
922
+
923
+ if (response.data?.code === 200 && response.data?.data?.text !== undefined) {
924
+ const text = response.data.data.text;
925
+ this.logInfo(`[_recognizeVoice] 语音识别成功: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`);
926
+ return text;
927
+ } else {
928
+ this.logWarn(`[_recognizeVoice] 语音识别失败: ${JSON.stringify(response.data)}`);
929
+ return null;
930
+ }
931
+ } catch (err) {
932
+ this.logError(`[_recognizeVoice] 语音识别异常: ${err.message}`);
933
+ return null;
934
+ }
935
+ }
769
936
  }
770
937
 
771
938
  module.exports = {
@@ -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,