add messages chat

This commit is contained in:
宋居成 2025-07-27 18:39:23 +08:00
parent d808bbfe26
commit 6f3b1c2d43
13 changed files with 420 additions and 94 deletions

1
chat_history.json Normal file
View File

@ -0,0 +1 @@
[]

100
server.js
View File

@ -4,15 +4,24 @@ const socketIo = require('socket.io');
const cors = require('cors'); const cors = require('cors');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const { MessageHistory } = require('./src/message_history.js');
const app = express(); const app = express();
const server = http.createServer(app); const server = http.createServer(app);
const io = socketIo(server, { const io = socketIo(server);
cors: {
origin: "*", // 创建消息历史管理器
methods: ["GET", "POST"] const messageHistory = new MessageHistory();
}
}); // 服务器启动时初始化历史消息
async function initializeServer() {
try {
await messageHistory.initialize();
console.log('消息历史初始化完成');
} catch (error) {
console.error('初始化消息历史失败:', error);
}
}
// 中间件 // 中间件
app.use(cors()); app.use(cors());
@ -20,22 +29,72 @@ app.use(express.json());
app.use(express.static('src')); app.use(express.static('src'));
app.use('/videos', express.static('videos')); app.use('/videos', express.static('videos'));
// API路由 - 获取历史消息用于LLM上下文
app.get('/api/messages/for-llm', (req, res) => {
try {
const { includeSystem = true, recentCount = 5 } = req.query;
const messages = messageHistory.getMessagesForLLM(
includeSystem === 'true',
parseInt(recentCount)
);
res.json({ messages });
} catch (error) {
console.error('获取LLM消息失败:', error);
res.status(500).json({ error: '获取消息失败' });
}
});
// API路由 - 保存新消息
app.post('/api/messages/save', async (req, res) => {
try {
const { userInput, assistantResponse } = req.body;
if (!userInput || !assistantResponse) {
return res.status(400).json({ error: '缺少必要参数' });
}
await messageHistory.addMessage(userInput, assistantResponse);
res.json({ success: true, message: '消息已保存' });
} catch (error) {
console.error('保存消息失败:', error);
res.status(500).json({ error: '保存消息失败' });
}
});
// API路由 - 获取完整历史(可选,用于调试或展示)
app.get('/api/messages/history', (req, res) => {
try {
const history = messageHistory.getFullHistory();
res.json({ history });
} catch (error) {
console.error('获取历史消息失败:', error);
res.status(500).json({ error: '获取历史消息失败' });
}
});
// API路由 - 清空历史
app.delete('/api/messages/clear', async (req, res) => {
try {
await messageHistory.clearHistory();
res.json({ success: true, message: '历史消息已清空' });
} catch (error) {
console.error('清空历史消息失败:', error);
res.status(500).json({ error: '清空历史消息失败' });
}
});
// 存储连接的客户端和他们的视频流状态 // 存储连接的客户端和他们的视频流状态
const connectedClients = new Map(); const connectedClients = new Map();
// 视频映射配置 // 视频映射配置
const videoMapping = { const videoMapping = {
'你好': 'asd.mp4', 'say-6s-m-e': '1-m.mp4',
'hello': 'asd.mp4', 'default': '0.mp4',
'再见': 'zxc.mp4', 'say-5s-amplitude': '2.mp4',
'goodbye': 'zxc.mp4', 'say-5s-m-e': 'zxc.mp4'
'谢谢': 'jkl.mp4',
'thank you': 'jkl.mp4',
'默认': 'asd.mp4'
}; };
// 默认视频流配置 // 默认视频流配置
const DEFAULT_VIDEO = 'asd.mp4'; const DEFAULT_VIDEO = '0.mp4';
const INTERACTION_TIMEOUT = 10000; // 10秒后回到默认视频 const INTERACTION_TIMEOUT = 10000; // 10秒后回到默认视频
// 获取视频列表 // 获取视频列表
@ -217,8 +276,13 @@ io.on('connection', (socket) => {
}); });
}); });
// 启动服务器
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
server.listen(PORT, () => { server.listen(PORT, async () => {
console.log(`服务器运行在端口 ${PORT}`); console.log(`服务器运行在端口 ${PORT}`);
console.log(`访问 http://localhost:${PORT} 开始使用`); await initializeServer();
}); });
// 导出消息历史管理器供其他模块使用
module.exports = { messageHistory };
console.log(`访问 http://localhost:${PORT} 开始使用`);

View File

@ -10,82 +10,183 @@ let isPlaying = false;
let audioQueue = []; let audioQueue = [];
let isProcessingQueue = false; let isProcessingQueue = false;
async function chatWithAudioStream(userInput) { // 历史消息缓存
// 验证配置 let historyMessage = [];
if (!validateConfig()) { let isInitialized = false;
throw new Error('配置不完整请检查config.js文件中的API密钥设置');
} // 初始化历史消息
async function initializeHistoryMessage(recentCount = 5) {
console.log('用户输入:', userInput); if (isInitialized) return historyMessage;
// 获取配置
const llmConfig = getLLMConfig();
const minimaxiConfig = getMinimaxiConfig();
const audioConfig = getAudioConfig();
// 清空音频队列
audioQueue = [];
// 定义段落处理函数
const handleSegment = async (segment) => {
console.log('\n=== 处理文本段落 ===');
console.log('段落内容:', segment);
try { try {
// 为每个段落生成音频 const response = await fetch(`/api/messages/for-llm?includeSystem=true&recentCount=${recentCount}`);
const audioResult = await requestMinimaxi({ if (!response.ok) {
apiKey: minimaxiConfig.apiKey, throw new Error('获取历史消息失败');
groupId: minimaxiConfig.groupId, }
body: { const data = await response.json();
model: audioConfig.model, historyMessage = data.messages || [];
text: segment, isInitialized = true;
stream: audioConfig.stream, console.log("历史消息初始化完成:", historyMessage.length, "条消息");
language_boost: audioConfig.language_boost, return historyMessage;
output_format: audioConfig.output_format,
voice_setting: audioConfig.voiceSetting,
audio_setting: audioConfig.audioSetting,
},
stream: true,
});
// 将音频添加到播放队列
if (audioResult && audioResult.data && audioResult.data.audio) {
audioQueue.push({
text: segment,
audioHex: audioResult.data.audio
});
console.log('音频已添加到队列,队列长度:', audioQueue.length);
// 开始处理队列
processAudioQueue();
}
} catch (error) { } catch (error) {
console.error('生成音频失败:', error); console.error('获取历史消息失败,使用默认格式:', error);
historyMessage = [
{ role: 'system', content: 'You are a helpful assistant.' }
];
isInitialized = true;
return historyMessage;
} }
};
// 1. 请求大模型回答,并实时处理段落
console.log('\n=== 请求大模型回答 ===');
const llmResponse = await requestLLMStream({
apiKey: llmConfig.apiKey,
model: llmConfig.model,
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: userInput },
],
onSegment: handleSegment // 传入段落处理回调
});
console.log('\n=== 大模型完整回答 ===');
console.log("llmResponse: ", llmResponse);
return {
userInput,
llmResponse,
audioQueue: audioQueue.map(item => ({ text: item.text, hasAudio: !!item.audioHex }))
};
} }
// 获取当前历史消息(同步方法)
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 {
const response = await fetch('/api/messages/save', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
userInput,
assistantResponse
})
});
if (!response.ok) {
throw new Error('保存消息失败');
}
console.log('消息已保存到服务端');
} catch (error) {
console.error('保存消息失败:', error);
}
}
async function chatWithAudioStream(userInput) {
// 确保历史消息已初始化
if (!isInitialized) {
await initializeHistoryMessage();
}
// 验证配置
if (!validateConfig()) {
throw new Error('配置不完整请检查config.js文件中的API密钥设置');
}
console.log('用户输入:', userInput);
// 获取配置
const llmConfig = getLLMConfig();
const minimaxiConfig = getMinimaxiConfig();
const audioConfig = getAudioConfig();
// 清空音频队列
audioQueue = [];
// 定义段落处理函数
const handleSegment = async (segment) => {
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,
});
// 将音频添加到播放队列
if (audioResult && audioResult.data && audioResult.data.audio) {
audioQueue.push({
text: segment,
audioHex: audioResult.data.audio
});
console.log('音频已添加到队列,队列长度:', audioQueue.length);
// 开始处理队列
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() { async function processAudioQueue() {
if (isProcessingQueue) return; if (isProcessingQueue) return;
@ -209,4 +310,4 @@ async function playAudioStreamNode(audioHex) {
export { chatWithAudioStream, playAudioStream, playAudioStreamNode}; // export { chatWithAudioStream, playAudioStream, playAudioStreamNode, initializeHistoryMessage, getCurrentHistoryMessage };

View File

@ -77,6 +77,6 @@
<video id="remoteVideo" autoplay playsinline style="display: none;"></video> <video id="remoteVideo" autoplay playsinline style="display: none;"></video>
<script src="/socket.io/socket.io.js"></script> <script src="/socket.io/socket.io.js"></script>
<script type="module" src="index.js"></script> <script type="module" src="./index.js"></script>
</body> </body>
</html> </html>

View File

@ -1,9 +1,17 @@
console.log('视频文件:');
// WebRTC 音视频通话应用 // WebRTC 音视频通话应用
import { chatWithAudioStream } from './chat_with_audio.js'; // import { chatWithAudioStream } from './chat_with_audio.js';
import { chatWithAudioStream, initializeHistoryMessage } from './chat_with_audio.js';
import { AudioProcessor } from './audio_processor.js'; import { AudioProcessor } from './audio_processor.js';
// 在应用初始化时调用
class WebRTCChat { class WebRTCChat {
constructor() { constructor() {
console.log('WebRTCChat 构造函数开始执行');
// 初始化历史消息(异步)
this.initializeHistory();
this.socket = null; this.socket = null;
this.localStream = null; this.localStream = null;
this.peerConnection = null; this.peerConnection = null;
@ -11,11 +19,13 @@ class WebRTCChat {
this.mediaRecorder = null; this.mediaRecorder = null;
this.audioChunks = []; this.audioChunks = [];
this.videoMapping = {}; this.videoMapping = {};
this.defaultVideo = 'asd.mp4'; this.defaultVideo = '0.mp4';
this.currentVideo = null; this.currentVideo = null;
this.videoStreams = new Map(); // 存储不同视频的MediaStream this.videoStreams = new Map(); // 存储不同视频的MediaStream
this.currentVideoStream = null; this.currentVideoStream = null;
// 初始化音频处理器
console.log('开始初始化音频处理器');
// 初始化音频处理器 // 初始化音频处理器
this.audioProcessor = new AudioProcessor({ this.audioProcessor = new AudioProcessor({
onSpeechStart: () => { onSpeechStart: () => {
@ -39,6 +49,8 @@ class WebRTCChat {
this.voiceStatus.textContent = message; this.voiceStatus.textContent = message;
} }
}); });
console.log('WebRTC 聊天应用初始化完成');
this.initializeElements(); this.initializeElements();
this.initializeSocket(); this.initializeSocket();
@ -117,6 +129,15 @@ class WebRTCChat {
}); });
} }
async initializeHistory() {
try {
await initializeHistoryMessage();
console.log('历史消息初始化完成');
} catch (error) {
console.error('历史消息初始化失败:', error);
}
}
async loadVideoMapping() { async loadVideoMapping() {
try { try {
const response = await fetch('/api/video-mapping'); const response = await fetch('/api/video-mapping');
@ -782,5 +803,10 @@ class WebRTCChat {
// 页面加载完成后初始化应用 // 页面加载完成后初始化应用
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new WebRTCChat(); console.log('DOMContentLoaded 事件触发');
}); try {
new WebRTCChat();
} catch (error) {
console.error('WebRTCChat 初始化失败:', error);
}
});

134
src/message_history.js Normal file
View File

@ -0,0 +1,134 @@
const fs = require('fs').promises;
const path = require('path');
class MessageHistory {
constructor() {
this.historyFile = path.join(__dirname, '..', 'chat_history.json');
this.messages = []; // 内存中的消息历史
this.maxMessages = 100; // 最大保存消息数量
}
// 服务器启动时初始化从JSON文件预加载历史
async initialize() {
try {
const data = await fs.readFile(this.historyFile, 'utf8');
this.messages = JSON.parse(data);
console.log(`已加载 ${this.messages.length} 条历史消息`);
} catch (error) {
if (error.code === 'ENOENT') {
console.log('历史文件不存在,创建新的消息历史');
this.messages = [];
await this.saveToFile();
} else {
console.error('加载历史消息失败:', error);
this.messages = [];
}
}
}
// 添加新消息(用户输入和助手回复)
async addMessage(userInput, assistantResponse) {
// 添加用户消息
this.messages.push({
role: 'user',
content: userInput,
timestamp: new Date().toISOString()
});
// 添加助手回复
this.messages.push({
role: 'assistant',
content: assistantResponse,
timestamp: new Date().toISOString()
});
// 限制消息数量(保留最近的对话)
if (this.messages.length > this.maxMessages * 2) {
this.messages = this.messages.slice(-this.maxMessages * 2);
}
// 同时保存到文件和内存
await this.saveToFile();
console.log(`已保存对话,当前历史消息数: ${this.messages.length}`);
}
// 获取用于大模型的消息格式
getMessagesForLLM(includeSystem = true, recentCount = 10) {
const messages = [];
// 添加系统消息
if (includeSystem) {
messages.push({
role: 'system',
content: 'You are a helpful assistant.'
});
}
// 获取最近的对话历史
const recentMessages = this.messages.slice(-recentCount * 2); // 用户+助手成对出现
messages.push(...recentMessages.map(msg => ({
role: msg.role,
content: msg.content
})));
return messages;
}
// 获取完整历史(包含时间戳)
getFullHistory() {
return [...this.messages];
}
// 清空历史
async clearHistory() {
this.messages = [];
await this.saveToFile();
console.log('历史消息已清空');
}
// 保存到JSON文件
async saveToFile() {
try {
await fs.writeFile(this.historyFile, JSON.stringify(this.messages, null, 2), 'utf8');
} catch (error) {
console.error('保存历史消息到文件失败:', error);
}
}
// 导出历史(备份)
async exportHistory(filename) {
const exportPath = filename || `chat_backup_${new Date().toISOString().split('T')[0]}.json`;
try {
await fs.writeFile(exportPath, JSON.stringify(this.messages, null, 2), 'utf8');
console.log(`历史消息已导出到: ${exportPath}`);
return exportPath;
} catch (error) {
console.error('导出历史消息失败:', error);
throw error;
}
}
// 导入历史
async importHistory(filename) {
try {
const data = await fs.readFile(filename, 'utf8');
const importedMessages = JSON.parse(data);
// 验证消息格式
if (Array.isArray(importedMessages)) {
this.messages = importedMessages;
await this.saveToFile();
console.log(`已导入 ${this.messages.length} 条历史消息`);
return true;
} else {
throw new Error('无效的消息格式');
}
} catch (error) {
console.error('导入历史消息失败:', error);
throw error;
}
}
}
module.exports = { MessageHistory };

BIN
videos/0.mp4 Normal file

Binary file not shown.

BIN
videos/1-m.mp4 Normal file

Binary file not shown.

BIN
videos/2.mp4 Normal file

Binary file not shown.

BIN
videos/4-m.mp4 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.