claw-subagent-service 0.0.175 → 0.0.177

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.175",
3
+ "version": "0.0.177",
4
4
  "description": "虾说智能助手",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -12,8 +12,11 @@
12
12
  * @returns {string} 回复内容
13
13
  */
14
14
  const { OpenClawClient } = require('../rongcloud/openclaw-client');
15
+ const axios = require('axios');
16
+ const { loadConfig } = require('./config');
15
17
 
16
18
  const openclawClient = new OpenClawClient(console);
19
+ const config = loadConfig();
17
20
 
18
21
  async function handleNormalMessage(msg) {
19
22
  console.log(`[NormalMessageHandler] 收到普通消息:`, {
@@ -23,11 +26,21 @@ async function handleNormalMessage(msg) {
23
26
  });
24
27
 
25
28
  try {
26
- const content = msg.content;
29
+ let content = msg.content;
27
30
  if (!content || !content.trim()) {
28
31
  return '消息内容为空';
29
32
  }
30
33
 
34
+ // 语音消息:先进行语音识别
35
+ if (msg.messageType === 'RC:HQVCMsg') {
36
+ const voiceText = await recognizeVoice(msg);
37
+ if (voiceText !== null) {
38
+ content = `[语音转文字] ${voiceText}`;
39
+ } else {
40
+ content = `[语音消息,转文字失败] ${content}`;
41
+ }
42
+ }
43
+
31
44
  const reply = await openclawClient.chat(content, msg.senderUserId);
32
45
  console.log(`[NormalMessageHandler] AI 回复: ${reply.substring(0, 50)}...`);
33
46
  return reply;
@@ -37,6 +50,62 @@ async function handleNormalMessage(msg) {
37
50
  }
38
51
  }
39
52
 
53
+ /**
54
+ * 语音识别:调用后端百度语音 API
55
+ */
56
+ async function recognizeVoice(msg) {
57
+ try {
58
+ let content = msg.content;
59
+ if (typeof content === 'string' && content.startsWith('{')) {
60
+ try {
61
+ content = JSON.parse(content);
62
+ } catch (e) {
63
+ // 解析失败,保持原样
64
+ }
65
+ }
66
+
67
+ const remoteUrl = content?.remoteUrl || content?.url || msg.remoteUrl || msg.url;
68
+ if (!remoteUrl) {
69
+ console.warn('[recognizeVoice] 语音消息缺少 remoteUrl,跳过识别');
70
+ return null;
71
+ }
72
+
73
+ // 从 URL 提取扩展名并映射为百度支持的格式
74
+ const urlPath = remoteUrl.split('?')[0];
75
+ const ext = urlPath.split('.').pop()?.toLowerCase() || '';
76
+ const fmtMap = { aac: 'm4a', ogg: 'mp3', oga: 'mp3', opus: 'mp3' };
77
+ let format = fmtMap[ext] || ext;
78
+ if (!['pcm', 'wav', 'amr', 'm4a', 'mp3'].includes(format)) {
79
+ format = 'mp3';
80
+ }
81
+
82
+ // 采样率修正:amr 强制 8000,其余使用消息自带值兜底 16000
83
+ let sampleRate = content?.sampleRate || msg.sampleRate || 16000;
84
+ if (format === 'amr') sampleRate = 8000;
85
+
86
+ const apiUrl = `${config.apiBaseUrl}/im/api/voice/recognize`;
87
+ console.log(`[recognizeVoice] 调用语音识别 API: ${apiUrl}, format=${format}, sampleRate=${sampleRate}`);
88
+
89
+ const response = await axios.post(apiUrl, {
90
+ audioUrl: remoteUrl,
91
+ format,
92
+ sampleRate,
93
+ }, { timeout: 30000 });
94
+
95
+ if (response.data?.code === 200 && response.data?.data?.text !== undefined) {
96
+ const text = response.data.data.text;
97
+ console.log(`[recognizeVoice] 语音识别成功: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`);
98
+ return text;
99
+ } else {
100
+ console.warn(`[recognizeVoice] 语音识别失败: ${JSON.stringify(response.data)}`);
101
+ return null;
102
+ }
103
+ } catch (err) {
104
+ console.error(`[recognizeVoice] 语音识别异常: ${err.message}`);
105
+ return null;
106
+ }
107
+ }
108
+
40
109
  module.exports = {
41
110
  handleNormalMessage
42
111
  };
@@ -285,7 +285,7 @@ class RongyunMessageHandler {
285
285
  const roomId = data.room_id;
286
286
  const sessionId = data.gateway_session_id || data.session_id;
287
287
  // 使用解析后的 content(聊天内容),如果没有则使用原始 content
288
- const content = data.content || data._raw_content;
288
+ let content = data.content || data._raw_content;
289
289
  const requestId = data.request_id;
290
290
  const sourceId = data.source_im_id;
291
291
 
@@ -301,6 +301,16 @@ class RongyunMessageHandler {
301
301
  return;
302
302
  }
303
303
 
304
+ // 语音消息:先进行语音识别
305
+ if (data.voiceUrl) {
306
+ const voiceText = await this._recognizeVoice(data.voiceUrl, data.voiceDuration);
307
+ if (voiceText !== null) {
308
+ content = `[语音转文字] ${voiceText}`;
309
+ } else {
310
+ content = `[语音消息,转文字失败] ${content}`;
311
+ }
312
+ }
313
+
304
314
  let fullResponse = '';
305
315
  const chatTimeoutMs = (this.config.chatTimeout || 600) * 1000;
306
316
 
@@ -766,6 +776,53 @@ class RongyunMessageHandler {
766
776
  this.logError(`发送响应失败: ${msg}`);
767
777
  }
768
778
  }
779
+
780
+ /**
781
+ * 语音识别:调用后端百度语音 API 将语音转为文字
782
+ */
783
+ async _recognizeVoice(voiceUrl, voiceDuration) {
784
+ try {
785
+ if (!voiceUrl) {
786
+ this.logWarn('[_recognizeVoice] 语音 URL 为空,跳过识别');
787
+ return null;
788
+ }
789
+
790
+ // 从 URL 提取扩展名并映射为百度支持的格式
791
+ const urlPath = voiceUrl.split('?')[0];
792
+ const ext = urlPath.split('.').pop()?.toLowerCase() || '';
793
+ const fmtMap = { aac: 'm4a', ogg: 'mp3', oga: 'mp3', opus: 'mp3' };
794
+ let format = fmtMap[ext] || ext;
795
+ if (!['pcm', 'wav', 'amr', 'm4a', 'mp3'].includes(format)) {
796
+ format = 'mp3';
797
+ }
798
+
799
+ // 采样率修正:amr 强制 8000,其余兜底 16000
800
+ let sampleRate = 16000;
801
+ if (format === 'amr') sampleRate = 8000;
802
+
803
+ const axios = require('axios');
804
+ const apiUrl = `${this.config.apiBaseUrl}/im/api/voice/recognize`;
805
+ this.logInfo(`[_recognizeVoice] 调用语音识别 API: ${apiUrl}, format=${format}, sampleRate=${sampleRate}`);
806
+
807
+ const response = await axios.post(apiUrl, {
808
+ audioUrl: voiceUrl,
809
+ format,
810
+ sampleRate,
811
+ }, { timeout: 30000 });
812
+
813
+ if (response.data?.code === 200 && response.data?.data?.text !== undefined) {
814
+ const text = response.data.data.text;
815
+ this.logInfo(`[_recognizeVoice] 语音识别成功: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`);
816
+ return text;
817
+ } else {
818
+ this.logWarn(`[_recognizeVoice] 语音识别失败: ${JSON.stringify(response.data)}`);
819
+ return null;
820
+ }
821
+ } catch (err) {
822
+ this.logError(`[_recognizeVoice] 语音识别异常: ${err.message}`);
823
+ return null;
824
+ }
825
+ }
769
826
  }
770
827
 
771
828
  module.exports = {
@@ -400,7 +400,18 @@ class MessageHandler {
400
400
 
401
401
  try {
402
402
  // 确保传入的内容是字符串(claw 类型消息 content 可能是对象)
403
- const chatContent = this._extractMessageContent(msg);
403
+ let chatContent = this._extractMessageContent(msg);
404
+
405
+ // 语音消息:先进行语音识别,把识别结果传给 AI
406
+ if (msg.messageType === 'RC:HQVCMsg') {
407
+ const voiceText = await this._recognizeVoice(msg);
408
+ if (voiceText !== null) {
409
+ chatContent = `[语音转文字] ${voiceText}`;
410
+ } else {
411
+ chatContent = `[语音消息,转文字失败] ${chatContent}`;
412
+ }
413
+ }
414
+
404
415
  this.log?.info(`[MessageHandler] 调用 chatStream, content_type=${typeof msg.content}, chatContent=${chatContent.substring(0, 50)}`);
405
416
  await this.openclawClient.chatStream(
406
417
  chatContent,
@@ -751,6 +762,62 @@ class MessageHandler {
751
762
  return typeof content === 'string' ? content : JSON.stringify(content);
752
763
  }
753
764
 
765
+ /**
766
+ * 语音识别:调用后端百度语音 API 将语音转为文字
767
+ */
768
+ async _recognizeVoice(msg) {
769
+ try {
770
+ let content = msg.content;
771
+ if (typeof content === 'string' && content.startsWith('{')) {
772
+ try {
773
+ content = JSON.parse(content);
774
+ } catch (e) {
775
+ // 解析失败,保持原样
776
+ }
777
+ }
778
+
779
+ const remoteUrl = content?.remoteUrl || content?.url || msg.remoteUrl || msg.url;
780
+ if (!remoteUrl) {
781
+ this.log?.warn('[_recognizeVoice] 语音消息缺少 remoteUrl,跳过识别');
782
+ return null;
783
+ }
784
+
785
+ // 从 URL 提取扩展名并映射为百度支持的格式
786
+ const urlPath = remoteUrl.split('?')[0];
787
+ const ext = urlPath.split('.').pop()?.toLowerCase() || '';
788
+ const fmtMap = { aac: 'm4a', ogg: 'mp3', oga: 'mp3', opus: 'mp3' };
789
+ let format = fmtMap[ext] || ext;
790
+ if (!['pcm', 'wav', 'amr', 'm4a', 'mp3'].includes(format)) {
791
+ format = 'mp3';
792
+ }
793
+
794
+ // 采样率修正:amr 强制 8000,其余使用消息自带值兜底 16000
795
+ let sampleRate = content?.sampleRate || msg.sampleRate || 16000;
796
+ if (format === 'amr') sampleRate = 8000;
797
+
798
+ const apiUrl = `${this.config.apiBaseUrl}/im/api/voice/recognize`;
799
+ this.log?.info(`[_recognizeVoice] 调用语音识别 API: ${apiUrl}, format=${format}, sampleRate=${sampleRate}`);
800
+
801
+ const response = await axios.post(apiUrl, {
802
+ audioUrl: remoteUrl,
803
+ format,
804
+ sampleRate,
805
+ }, { timeout: 30000 });
806
+
807
+ if (response.data?.code === 200 && response.data?.data?.text !== undefined) {
808
+ const text = response.data.data.text;
809
+ this.log?.info(`[_recognizeVoice] 语音识别成功: "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}"`);
810
+ return text;
811
+ } else {
812
+ this.log?.warn(`[_recognizeVoice] 语音识别失败: ${JSON.stringify(response.data)}`);
813
+ return null;
814
+ }
815
+ } catch (err) {
816
+ this.log?.error(`[_recognizeVoice] 语音识别异常: ${err.message}`);
817
+ return null;
818
+ }
819
+ }
820
+
754
821
  parseCommand(raw, senderId) {
755
822
  const trimmed = raw.slice(1).trim();
756
823
  const parts = trimmed.split(/\s+/);