All checks were successful
		
		
	
	Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 3m57s
				
			
		
			
				
	
	
		
			325 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			325 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| // 用户输入文本后,进行大模型回答,并且合成音频,流式播放
 | ||
| 
 | ||
| import { requestLLMStream } from './llm_stream.js';
 | ||
| import { requestMinimaxi } from './minimaxi_stream.js';
 | ||
| import { getLLMConfig, getLLMConfigByScene, getMinimaxiConfig, getAudioConfig, validateConfig } from './config.js';
 | ||
| 
 | ||
| // 防止重复播放的标志
 | ||
| let isPlaying = false;
 | ||
| // 音频播放队列
 | ||
| let audioQueue = [];
 | ||
| let isProcessingQueue = false;
 | ||
| 
 | ||
| // 历史消息缓存
 | ||
| let historyMessage = [];
 | ||
| let isInitialized = false;
 | ||
| 
 | ||
| // 初始化历史消息
 | ||
| async function initializeHistoryMessage(recentCount = 5) {
 | ||
|     if (isInitialized) return historyMessage;
 | ||
|     
 | ||
|     try {
 | ||
|         const response = await fetch(`/api/messages/for-llm?includeSystem=true&recentCount=${recentCount}`);
 | ||
|         if (!response.ok) {
 | ||
|             throw new Error('获取历史消息失败');
 | ||
|         }
 | ||
|         const data = await response.json();
 | ||
|         historyMessage = data.messages || [];
 | ||
|         isInitialized = true;
 | ||
|         console.log("历史消息初始化完成:", historyMessage.length, "条消息");
 | ||
|         return historyMessage;
 | ||
|     } catch (error) {
 | ||
|         console.error('获取历史消息失败,使用默认格式:', error);
 | ||
|         historyMessage = [
 | ||
|             { role: 'system', content: 'You are a helpful assistant.' }
 | ||
|         ];
 | ||
|         isInitialized = true;
 | ||
|         return historyMessage;
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| // 获取当前历史消息(同步方法)
 | ||
| function getCurrentHistoryMessage() {
 | ||
|     if (!isInitialized) {
 | ||
|         console.warn('历史消息未初始化,返回默认消息');
 | ||
|         return [{ role: 'system', content: 'You are a helpful assistant.' }];
 | ||
|     }
 | ||
|     return [...historyMessage]; // 返回副本,避免外部修改
 | ||
| }
 | ||
| 
 | ||
| // 更新历史消息
 | ||
| function updateHistoryMessage(userInput, assistantResponse) {
 | ||
|     if (!isInitialized) {
 | ||
|         console.warn('历史消息未初始化,无法更新');
 | ||
|         return;
 | ||
|     }
 | ||
|     
 | ||
|     historyMessage.push(
 | ||
|         { role: 'user', content: userInput },
 | ||
|         { role: 'assistant', content: assistantResponse }
 | ||
|     );
 | ||
|     
 | ||
|     // 可选:限制历史消息数量,保持最近的对话
 | ||
|     // const maxMessages = 20; // 保留最近10轮对话(20条消息)
 | ||
|     // if (historyMessage.length > maxMessages) {
 | ||
|     //     // 保留系统消息和最近的对话
 | ||
|     //     const systemMessages = historyMessage.filter(msg => msg.role === 'system');
 | ||
|     //     const recentMessages = historyMessage.slice(-maxMessages + systemMessages.length);
 | ||
|     //     historyMessage = [...systemMessages, ...recentMessages.filter(msg => msg.role !== 'system')];
 | ||
|     // }
 | ||
| }
 | ||
| 
 | ||
| // 保存消息到服务端
 | ||
| // 保存消息到服务端
 | ||
| async function saveMessage(userInput, assistantResponse) {
 | ||
|     try {
 | ||
|         // 验证参数是否有效
 | ||
|         if (!userInput || !userInput.trim() || !assistantResponse || !assistantResponse.trim()) {
 | ||
|             console.warn('跳过保存消息:用户输入或助手回复为空');
 | ||
|             return;
 | ||
|         }
 | ||
|         
 | ||
|         const response = await fetch('/api/messages/save', {
 | ||
|             method: 'POST',
 | ||
|             headers: {
 | ||
|                 'Content-Type': 'application/json'
 | ||
|             },
 | ||
|             body: JSON.stringify({
 | ||
|                 userInput: userInput.trim(),
 | ||
|                 assistantResponse: assistantResponse.trim()
 | ||
|             })
 | ||
|         });
 | ||
|         
 | ||
|         if (!response.ok) {
 | ||
|             const errorData = await response.json().catch(() => ({}));
 | ||
|             throw new Error(`保存消息失败: ${response.status} ${errorData.error || response.statusText}`);
 | ||
|         }
 | ||
|         
 | ||
|         console.log('消息已保存到服务端');
 | ||
|     } catch (error) {
 | ||
|         console.error('保存消息失败:', error);
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| async function chatWithAudioStream(userInput) {
 | ||
|     // 确保历史消息已初始化
 | ||
|     if (!isInitialized) {
 | ||
|         await initializeHistoryMessage(100);
 | ||
|     }
 | ||
|     
 | ||
|     // 验证配置
 | ||
|     if (!validateConfig()) {
 | ||
|         throw new Error('配置不完整,请检查config.js文件中的API密钥设置');
 | ||
|     }
 | ||
|     
 | ||
|     console.log('用户输入:', userInput);
 | ||
|     
 | ||
|     // 获取当前场景对应的配置
 | ||
|     const llmConfig = await getLLMConfigByScene();
 | ||
|     const minimaxiConfig = getMinimaxiConfig();
 | ||
|     const audioConfig = getAudioConfig();
 | ||
|     
 | ||
|     console.log(`当前场景: ${llmConfig.sceneName} (${llmConfig.sceneTag})`);
 | ||
|     console.log(`使用API Key: ${llmConfig.model}...`);
 | ||
|     
 | ||
|     // 清空音频队列
 | ||
|     audioQueue = [];
 | ||
|     
 | ||
|     // 定义段落处理函数
 | ||
|     const handleSegment = async (segment, textPlay) => {
 | ||
|         console.log('\n=== 处理文本段落 ===');
 | ||
|         console.log('段落内容:', segment);
 | ||
|         
 | ||
|         try {
 | ||
|             // 为每个段落生成音频
 | ||
|             const audioResult = await requestMinimaxi({
 | ||
|                 apiKey: minimaxiConfig.apiKey,
 | ||
|                 groupId: minimaxiConfig.groupId,
 | ||
|                 body: {
 | ||
|                     model: audioConfig.model,
 | ||
|                     text: segment,
 | ||
|                     stream: audioConfig.stream,
 | ||
|                     language_boost: audioConfig.language_boost,
 | ||
|                     output_format: audioConfig.output_format,
 | ||
|                     voice_setting: audioConfig.voiceSetting,
 | ||
|                     audio_setting: audioConfig.audioSetting,
 | ||
|                 },
 | ||
|                 stream: true,
 | ||
|                 textPlay: textPlay
 | ||
|             });
 | ||
|             
 | ||
|             // 将音频添加到播放队列
 | ||
|             if (audioResult && audioResult.data && audioResult.data.audio) {
 | ||
|                 audioQueue.push({
 | ||
|                     text: segment,
 | ||
|                     audioHex: audioResult.data.audio
 | ||
|                 });
 | ||
|                 console.log('音频已添加到队列,队列长度:', audioQueue.length);
 | ||
|                 // this.socket.emit('audio-queue-updated', audioQueue.map(item => ({ text: item.text, hasAudio:!!item.audioHex })));
 | ||
|                 // 开始处理队列
 | ||
|                 // processAudioQueue();
 | ||
|             }
 | ||
|         } catch (error) {
 | ||
|             console.error('生成音频失败:', error);
 | ||
|         }
 | ||
|     };
 | ||
|     
 | ||
|     // 1. 获取包含历史的消息列表
 | ||
|     console.log('\n=== 获取历史消息 ===');
 | ||
|     const messages = getCurrentHistoryMessage();
 | ||
|     messages.push({role: 'user', content: userInput});
 | ||
|     console.log('发送的消息数量:', messages);
 | ||
|     
 | ||
|     // 2. 请求大模型回答
 | ||
|     console.log('\n=== 请求大模型回答 ===');
 | ||
|     const llmResponse = await requestLLMStream({
 | ||
|         apiKey: llmConfig.apiKey,
 | ||
|         model: llmConfig.model,
 | ||
|         messages: messages,
 | ||
|         onSegment: handleSegment
 | ||
|     });
 | ||
|     
 | ||
|     console.log('\n=== 大模型完整回答 ===');
 | ||
|     console.log("llmResponse: ", llmResponse);
 | ||
|     
 | ||
|     // 3. 保存对话到服务端
 | ||
|     await saveMessage(userInput, llmResponse);
 | ||
|     
 | ||
|     // 4. 更新本地历史消息
 | ||
|     updateHistoryMessage(userInput, llmResponse);
 | ||
|     console.log('历史消息数量:', historyMessage.length);
 | ||
|     
 | ||
|     return {
 | ||
|         userInput,
 | ||
|         llmResponse,
 | ||
|         audioQueue: audioQueue.map(item => ({ text: item.text, hasAudio: !!item.audioHex }))
 | ||
|     };
 | ||
| }
 | ||
| 
 | ||
| // 导出初始化函数,供外部调用
 | ||
| export { chatWithAudioStream, initializeHistoryMessage, getCurrentHistoryMessage };
 | ||
| 
 | ||
| // 处理音频播放队列
 | ||
| async function processAudioQueue() {
 | ||
|   if (isProcessingQueue) return;
 | ||
|   
 | ||
|   isProcessingQueue = true;
 | ||
|   
 | ||
|   // while (audioQueue.length > 0) {
 | ||
|   //   const audioItem = audioQueue.shift();
 | ||
|   //   console.log('\n=== 播放队列中的音频 ===');
 | ||
|   //   console.log('文本:', audioItem.text);
 | ||
|     
 | ||
|   //   try {
 | ||
|   //     await playAudioStream(audioItem.audioHex);
 | ||
|   //   } catch (error) {
 | ||
|   //     console.error('播放音频失败:', error);
 | ||
|   //   }
 | ||
|   // }
 | ||
|   
 | ||
|   isProcessingQueue = false;
 | ||
| }
 | ||
| 
 | ||
| // 流式播放音频
 | ||
| async function playAudioStream(audioHex) {
 | ||
|   console.log('=== 开始播放音频 ===');
 | ||
|   console.log('音频数据长度:', audioHex.length);
 | ||
|   
 | ||
|   // 将hex转换为ArrayBuffer
 | ||
|   const audioBuffer = hexToArrayBuffer(audioHex);
 | ||
|   
 | ||
|   // 创建AudioContext
 | ||
|   const audioContext = new (window.AudioContext || window.webkitAudioContext)();
 | ||
|   
 | ||
|   try {
 | ||
|     // 解码音频
 | ||
|     const audioData = await audioContext.decodeAudioData(audioBuffer);
 | ||
|     
 | ||
|     // 创建音频源
 | ||
|     const source = audioContext.createBufferSource();
 | ||
|     source.buffer = audioData;
 | ||
|     source.connect(audioContext.destination);
 | ||
|     
 | ||
|     // 播放
 | ||
|     source.start(0);
 | ||
|     
 | ||
|     console.log('音频播放开始,时长:', audioData.duration, '秒');
 | ||
|     
 | ||
|     // 等待播放完成
 | ||
|     return new Promise((resolve) => {
 | ||
|       source.onended = () => {
 | ||
|         console.log('音频播放完成');
 | ||
|         resolve();
 | ||
|       };
 | ||
|     });
 | ||
|   } catch (error) {
 | ||
|     console.error('音频播放失败:', error);
 | ||
|     throw error;
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| // 将hex字符串转换为ArrayBuffer
 | ||
| function hexToArrayBuffer(hex) {
 | ||
|   const bytes = new Uint8Array(hex.length / 2);
 | ||
|   for (let i = 0; i < hex.length; i += 2) {
 | ||
|     bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
 | ||
|   }
 | ||
|   return bytes.buffer;
 | ||
| }
 | ||
| 
 | ||
| // 在Node.js环境下的音频播放(使用play-sound库)
 | ||
| async function playAudioStreamNode(audioHex) {
 | ||
|   // 检查是否在Node.js环境中
 | ||
|   if (typeof window !== 'undefined') {
 | ||
|     console.warn('playAudioStreamNode 只能在Node.js环境中使用');
 | ||
|     return;
 | ||
|   }
 | ||
|   
 | ||
|   try {
 | ||
|     const fs = require('fs');
 | ||
|     const path = require('path');
 | ||
|     
 | ||
|     // 将hex转换为buffer
 | ||
|     const audioBuffer = Buffer.from(audioHex, 'hex');
 | ||
|     
 | ||
|     // 保存为临时文件
 | ||
|     const tempFile = path.join(process.cwd(), 'temp_audio.mp3');
 | ||
|     fs.writeFileSync(tempFile, audioBuffer);
 | ||
|     
 | ||
|     // 使用系统默认播放器播放
 | ||
|     const { exec } = require('child_process');
 | ||
|     const platform = process.platform;
 | ||
|     
 | ||
|     let command;
 | ||
|     if (platform === 'win32') {
 | ||
|       command = `start "" "${tempFile}"`;
 | ||
|     } else if (platform === 'darwin') {
 | ||
|       command = `open "${tempFile}"`;
 | ||
|     } else {
 | ||
|       command = `xdg-open "${tempFile}"`;
 | ||
|     }
 | ||
|     
 | ||
|     exec(command, (error) => {
 | ||
|       if (error) {
 | ||
|         console.error('播放音频失败:', error);
 | ||
|       } else {
 | ||
|         console.log('音频播放开始');
 | ||
|       }
 | ||
|     });
 | ||
|     
 | ||
|     // 等待一段时间后删除临时文件
 | ||
|     setTimeout(() => {
 | ||
|       if (fs.existsSync(tempFile)) {
 | ||
|         fs.unlinkSync(tempFile);
 | ||
|       }
 | ||
|     }, 10000);
 | ||
|     
 | ||
|   } catch (error) {
 | ||
|     console.error('音频播放失败:', error);
 | ||
|     throw error;
 | ||
|   }
 | ||
| }
 | ||
| 
 | ||
| 
 | ||
| 
 | ||
| // export { chatWithAudioStream, playAudioStream, playAudioStreamNode, initializeHistoryMessage, getCurrentHistoryMessage };
 |