290 lines
8.1 KiB
JavaScript
290 lines
8.1 KiB
JavaScript
const express = require('express');
|
||
const http = require('http');
|
||
const socketIo = require('socket.io');
|
||
const cors = require('cors');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const { MessageHistory } = require('./src/message_history.js');
|
||
|
||
const app = express();
|
||
const server = http.createServer(app);
|
||
const io = socketIo(server);
|
||
|
||
// 创建消息历史管理器
|
||
const messageHistory = new MessageHistory();
|
||
|
||
// 服务器启动时初始化历史消息
|
||
async function initializeServer() {
|
||
try {
|
||
await messageHistory.initialize();
|
||
console.log('消息历史初始化完成');
|
||
} catch (error) {
|
||
console.error('初始化消息历史失败:', error);
|
||
}
|
||
}
|
||
|
||
// 中间件
|
||
app.use(cors());
|
||
app.use(express.json());
|
||
app.use(express.static('src'));
|
||
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 videoMapping = {
|
||
// 'say-6s-m-e': '1-m.mp4',
|
||
'default': 'd-3s.mp4',
|
||
// 'say-5s-amplitude': '2.mp4',
|
||
// 'say-5s-m-e': '4.mp4',
|
||
// 'say-5s-m-sw': '5.mp4',
|
||
'say-3s-m-sw': 's-1.mp4',
|
||
};
|
||
|
||
// 默认视频流配置
|
||
const DEFAULT_VIDEO = 'd-3s.mp4';
|
||
const INTERACTION_TIMEOUT = 10000; // 10秒后回到默认视频
|
||
|
||
// 获取视频列表
|
||
app.get('/api/videos', (req, res) => {
|
||
const videosDir = path.join(__dirname, 'videos');
|
||
fs.readdir(videosDir, (err, files) => {
|
||
if (err) {
|
||
return res.status(500).json({ error: '无法读取视频目录' });
|
||
}
|
||
const videoFiles = files.filter(file =>
|
||
file.endsWith('.mp4') || file.endsWith('.webm') || file.endsWith('.avi')
|
||
);
|
||
res.json({ videos: videoFiles });
|
||
});
|
||
});
|
||
|
||
// 获取视频映射
|
||
app.get('/api/video-mapping', (req, res) => {
|
||
res.json({ mapping: videoMapping });
|
||
});
|
||
|
||
// 获取默认视频
|
||
app.get('/api/default-video', (req, res) => {
|
||
res.json({
|
||
defaultVideo: DEFAULT_VIDEO,
|
||
autoLoop: true
|
||
});
|
||
});
|
||
|
||
// Socket.IO 连接处理
|
||
io.on('connection', (socket) => {
|
||
console.log('用户连接:', socket.id);
|
||
connectedClients.set(socket.id, {
|
||
socket: socket,
|
||
currentVideo: DEFAULT_VIDEO,
|
||
isInInteraction: false
|
||
});
|
||
|
||
// 处理WebRTC信令 - 用于传输视频流
|
||
socket.on('offer', (data) => {
|
||
console.log('收到offer:', socket.id);
|
||
socket.broadcast.emit('offer', {
|
||
...data,
|
||
from: socket.id
|
||
});
|
||
});
|
||
|
||
socket.on('answer', (data) => {
|
||
console.log('收到answer:', socket.id);
|
||
socket.broadcast.emit('answer', {
|
||
...data,
|
||
from: socket.id
|
||
});
|
||
});
|
||
|
||
socket.on('ice-candidate', (data) => {
|
||
console.log('收到ice-candidate:', socket.id);
|
||
socket.broadcast.emit('ice-candidate', {
|
||
...data,
|
||
from: socket.id
|
||
});
|
||
});
|
||
|
||
// 处理视频流切换请求
|
||
socket.on('switch-video-stream', (data) => {
|
||
const { videoFile, type, text } = data;
|
||
console.log(`用户 ${socket.id} 请求切换视频流: ${videoFile} (${type})`);
|
||
|
||
// 更新客户端状态
|
||
const client = connectedClients.get(socket.id);
|
||
if (client) {
|
||
client.currentVideo = videoFile;
|
||
client.isInInteraction = true;
|
||
}
|
||
|
||
// 广播视频流切换指令给所有用户
|
||
io.emit('video-stream-switched', {
|
||
videoFile,
|
||
type,
|
||
text,
|
||
from: socket.id
|
||
});
|
||
|
||
// 如果是交互类型,设置定时器回到默认视频
|
||
if (type === 'text' || type === 'voice') {
|
||
setTimeout(() => {
|
||
console.log(`交互超时,用户 ${socket.id} 回到默认视频`);
|
||
if (client) {
|
||
client.currentVideo = DEFAULT_VIDEO;
|
||
client.isInInteraction = false;
|
||
}
|
||
// 广播回到默认视频的指令
|
||
io.emit('video-stream-switched', {
|
||
videoFile: DEFAULT_VIDEO,
|
||
type: 'default',
|
||
from: socket.id
|
||
});
|
||
}, INTERACTION_TIMEOUT);
|
||
}
|
||
});
|
||
|
||
// 处理通话开始
|
||
socket.on('call-started', () => {
|
||
console.log('通话开始,用户:', socket.id);
|
||
const client = connectedClients.get(socket.id);
|
||
if (client) {
|
||
client.currentVideo = DEFAULT_VIDEO;
|
||
client.isInInteraction = false;
|
||
}
|
||
io.emit('call-started', { from: socket.id });
|
||
});
|
||
|
||
// 处理文本输入
|
||
socket.on('text-input', (data) => {
|
||
const { text } = data;
|
||
console.log('收到文本输入:', text, '来自用户:', socket.id);
|
||
|
||
// 根据文本查找对应视频
|
||
let videoFile = videoMapping['default'];
|
||
for (const [key, value] of Object.entries(videoMapping)) {
|
||
if (text.toLowerCase().includes(key.toLowerCase())) {
|
||
videoFile = value;
|
||
break;
|
||
}
|
||
}
|
||
|
||
console.log(`用户 ${socket.id} 文本输入 "${text}" 对应视频: ${videoFile}`);
|
||
|
||
// 发送视频流切换请求
|
||
socket.emit('switch-video-stream', {
|
||
videoFile,
|
||
type: 'text',
|
||
text
|
||
});
|
||
});
|
||
|
||
// 处理语音输入
|
||
socket.on('voice-input', (data) => {
|
||
const { audioData, text } = data;
|
||
console.log('收到语音输入:', text, '来自用户:', socket.id);
|
||
|
||
// 根据语音识别的文本查找对应视频
|
||
let videoFile = videoMapping['default'];
|
||
for (const [key, value] of Object.entries(videoMapping)) {
|
||
if (text.toLowerCase().includes(key.toLowerCase())) {
|
||
videoFile = value;
|
||
break;
|
||
}
|
||
}
|
||
|
||
console.log(`用户 ${socket.id} 语音输入 "${text}" 对应视频: ${videoFile}`);
|
||
|
||
// 发送视频流切换请求
|
||
socket.emit('switch-video-stream', {
|
||
videoFile,
|
||
type: 'voice',
|
||
text
|
||
});
|
||
});
|
||
|
||
// 处理回到默认视频请求
|
||
socket.on('return-to-default', () => {
|
||
console.log('用户请求回到默认视频:', socket.id);
|
||
const client = connectedClients.get(socket.id);
|
||
if (client) {
|
||
client.currentVideo = DEFAULT_VIDEO;
|
||
client.isInInteraction = false;
|
||
}
|
||
socket.emit('switch-video-stream', {
|
||
videoFile: DEFAULT_VIDEO,
|
||
type: 'default'
|
||
});
|
||
});
|
||
|
||
// 断开连接
|
||
socket.on('disconnect', () => {
|
||
console.log('用户断开连接:', socket.id);
|
||
connectedClients.delete(socket.id);
|
||
});
|
||
});
|
||
|
||
// 启动服务器
|
||
const PORT = process.env.PORT || 3000;
|
||
server.listen(PORT, async () => {
|
||
console.log(`服务器运行在端口 ${PORT}`);
|
||
await initializeServer();
|
||
});
|
||
|
||
// 导出消息历史管理器供其他模块使用
|
||
module.exports = { messageHistory };
|
||
console.log(`访问 http://localhost:${PORT} 开始使用`); |