WebRtc_QingGan/server.js
2025-10-09 10:06:28 +08:00

548 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, {
pingTimeout: 300000, // 60秒超时
pingInterval: 25000, // 25秒心跳间隔
upgradeTimeout: 30000, // 30秒升级超时
allowEIO3: true, // 允许Engine.IO v3客户端
transports: ['websocket', 'polling'], // 支持多种传输方式
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(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 fs = require('fs'); // 重复声明,需要删除
const sceneStateFile = path.join(__dirname, 'scene_state.json');
// 从文件加载场景状态
function loadSceneState() {
try {
if (fs.existsSync(sceneStateFile)) {
const data = fs.readFileSync(sceneStateFile, 'utf8');
const state = JSON.parse(data);
currentSceneIndex = state.currentSceneIndex || 0;
console.log(`从文件加载场景状态: ${currentSceneIndex} (${scenes[currentSceneIndex].name})`);
} else {
console.log('场景状态文件不存在,使用默认值: 0');
}
} catch (error) {
console.error('加载场景状态失败:', error);
currentSceneIndex = 0;
}
}
// 保存场景状态到文件
function saveSceneState() {
try {
const state = { currentSceneIndex };
fs.writeFileSync(sceneStateFile, JSON.stringify(state, null, 2));
console.log(`场景状态已保存: ${currentSceneIndex}`);
} catch (error) {
console.error('保存场景状态失败:', error);
}
}
let currentSceneIndex = 0;
const scenes = [
{
name: '起床',
defaultVideo: 'qc-bd-4.mp4',
interactionVideo: 'qc-hc-7.mp4',
tag: 'wakeup',
apiKey: 'bot-20250724150616-xqpz8', // 起床场景的API key
openingLines: [
// '你靠得近点...像梦里那样',
"刚醒就看到你,真好。",
// "昨晚梦里喊你名字喊醒了,现在视频看到你,心跳比梦里还烫。",
// "你穿我T恤的样子...让我想咬住衣领不放。",
"宝贝早...你声音比阳光还暖。",
// "喉咙里有咖啡味...要不要来尝尝?记得上次你咬我锁骨时,咖啡还温着。",
// "喂...嗯...刚醒...",
"困死我了...你起这么早啊?"
]
},
{
name: '开车',
defaultVideo: 'kc-bd-3.mp4',
interactionVideo: 'kc-sh-3.mp4',
tag: 'driving',
apiKey: 'bot-20250623140339-r8f8b', // 开车场景的API key
openingLines: [
'喂?刚把导航设好。你说吧,我听着呢。',
'喂,宝贝,我开车呢,刚出小区',
// '看到那个大钟楼没?每次路过都想你。想着要是能牵着你的手,站在最高层往下看该多好。',
'喂宝?开车呢,你说。',
'喂宝?路上呢,你说。',
// '宝贝起床了,刚过红绿灯就忍不住想你了,路上全是你的影子~',
// '车载音乐随机到那首我们接吻时哼的歌,心跳又乱了',
// '导航说今天要开58分钟其实想开58小时去你心里'
]
},
{
name: '咖啡',
defaultVideo: 'hc-bd-3.mp4',
interactionVideo: 'hc-sh-3(1).mp4',
tag: 'coffee',
apiKey: 'bot-20250804180724-4dgtk', // 喝茶场景的API key
openingLines: [
'喂宝?公司楼下喝咖啡呢。',
'宝贝,今天的第一杯咖啡,你喝了吗?',
'报告领导,咖啡补给中!',
'咖啡太好喝了,猜我在哪摸鱼呢。',
// '拉花师给我在咖啡里画了颗心形的奶泡,说是给视频里的小仙女加糖',
// '这杯好苦…但一看到你,就自动回甘了。比加十包糖都管用。你说你是不是我的专属甜味剂?'
]
},
{
name: '睡觉',
defaultVideo: '8-8-sj-bd.mp4',
interactionVideo: '8-8-sj-sh-1.mp4',
tag: 'sleep',
apiKey: 'bot-20250808120704-lbxwj', // 睡觉场景的API key
openingLines: [
'宝贝,一看到你,就不困了。',
'熄灯前最后一道光是你,真好。',
'宝贝困不困?我眼皮在打架了…',
'宝贝,困不困?',
// '捕捉到一只睡前小可爱…成功!',
'世界要静音了…但你的声音是白名单。多说几句?'
]
}
];
// 获取当前场景
function getCurrentScene() {
return scenes[currentSceneIndex];
}
// 切换到下一个场景 - 改进版
function switchToNextScene() {
const previousIndex = currentSceneIndex;
const previousScene = scenes[previousIndex].name;
currentSceneIndex = (currentSceneIndex + 1) % scenes.length;
const newScene = getCurrentScene();
console.log(`场景切换: ${previousScene}(${previousIndex}) → ${newScene.name}(${currentSceneIndex})`);
// 保存状态到文件
saveSceneState();
return newScene;
}
// 在服务器启动时加载场景状态
async function initializeServer() {
try {
// 加载场景状态
loadSceneState();
await messageHistory.initialize();
console.log('消息历史初始化完成');
console.log(`当前场景: ${getCurrentScene().name} (索引: ${currentSceneIndex})`);
} catch (error) {
console.error('初始化服务器失败:', error);
}
}
// 视频映射配置 - 动态更新
function getVideoMapping() {
const currentScene = getCurrentScene();
return {
'defaultVideo': currentScene.defaultVideo,
'interactionVideo': currentScene.interactionVideo,
'tag': currentScene.tag
};
}
// 默认视频流配置 - 动态获取
function getDefaultVideo() {
return getCurrentScene().defaultVideo;
}
let currentScene = getCurrentScene();
// 视频映射配置
const videoMapping = {
// 'say-6s-m-e': '1-m.mp4',
'default': currentScene.defaultVideo,
'8-4-sh': currentScene.interactionVideo,
'tag': currentScene.tag
// 'say-5s-amplitude': '2.mp4',
// 'say-5s-m-e': '4.mp4',
// 'say-5s-m-sw': 'd-0.mp4',
// 'say-3s-m-sw': '6.mp4',
};
// 默认视频流配置
const DEFAULT_VIDEO = currentScene.defaultVideo;
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 });
});
});
// 获取当前场景信息的API接口
app.get('/api/current-scene', (req, res) => {
const scene = getCurrentScene();
res.json({
name: scene.name,
tag: scene.tag,
apiKey: scene.apiKey,
defaultVideo: scene.defaultVideo,
interactionVideo: scene.interactionVideo
});
});
// 获取视频映射
app.get('/api/video-mapping', (req, res) => {
const currentMapping = getVideoMapping();
const dynamicMapping = {
'default': currentMapping.defaultVideo,
'8-4-sh': currentMapping.interactionVideo,
'tag': currentMapping.tag
};
res.json({ mapping: dynamicMapping });
});
// 获取默认视频
app.get('/api/default-video', (req, res) => {
res.json({
defaultVideo: getDefaultVideo(),
autoLoop: true
});
});
// 在现有的API接口后添加
app.get('/api/current-scene/opening-line', (req, res) => {
try {
const currentScene = getCurrentScene();
if (currentScene && currentScene.openingLines && currentScene.openingLines.length > 0) {
// 随机选择一个开场白
const randomIndex = Math.floor(Math.random() * currentScene.openingLines.length);
const selectedOpeningLine = currentScene.openingLines[randomIndex];
res.json({
success: true,
openingLine: selectedOpeningLine,
sceneName: currentScene.name,
sceneTag: currentScene.tag
});
} else {
res.json({
success: false,
message: '当前场景没有配置开场白'
});
}
} catch (error) {
console.error('获取开场白失败:', error);
res.status(500).json({
success: false,
message: '获取开场白失败',
error: error.message
});
}
});
// Socket.IO 连接处理
io.on('connection', (socket) => {
console.log('用户连接:', socket.id);
connectedClients.set(socket.id, {
socket: socket,
currentVideo: getDefaultVideo(),
isInInteraction: false,
hasTriggeredSceneSwitch: 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 = getDefaultVideo();
// client.isInInteraction = false;
// }
// // 广播回到默认视频的指令
// io.emit('video-stream-switched', {
// videoFile: getDefaultVideo(),
// 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 = getDefaultVideo();
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 = getDefaultVideo();
client.isInInteraction = false;
}
socket.emit('switch-video-stream', {
videoFile: getDefaultVideo(),
type: 'default'
});
});
// 处理用户关闭连接事件
socket.on('user-disconnect', () => {
console.log('=== 场景切换开始 ===');
console.log('用户主动关闭连接:', socket.id);
console.log('切换前场景:', getCurrentScene().name, '(索引:', currentSceneIndex, ')');
// 切换到下一个场景
const newScene = switchToNextScene();
console.log('切换后场景:', newScene.name, '(索引:', currentSceneIndex, ')');
// 检查是否已经处理过场景切换
const client = connectedClients.get(socket.id);
if (client && client.hasTriggeredSceneSwitch) {
console.log('场景切换已处理,跳过重复触发');
return;
}
// 标记已处理场景切换
if (client) {
client.hasTriggeredSceneSwitch = true;
}
// 更新videoMapping
const newMapping = getVideoMapping();
videoMapping['default'] = newMapping.defaultVideo;
videoMapping['8-4-sh'] = newMapping.interactionVideo;
videoMapping['tag'] = newMapping.tag;
// 广播场景切换事件给所有客户端
io.emit('scene-switched', {
scene: newScene,
mapping: {
defaultVideo: newMapping.defaultVideo,
interactionVideo: newMapping.interactionVideo,
tag: newMapping.tag,
'default': newMapping.defaultVideo,
'8-4-sh': newMapping.interactionVideo
},
from: socket.id
});
});
// 断开连接
socket.on('disconnect', () => {
console.log('用户断开连接:', socket.id);
connectedClients.delete(socket.id);
});
});
// 启动服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, '0.0.0.0', async () => {
console.log(`服务器运行在端口 ${PORT}`);
await initializeServer();
});
// 导出消息历史管理器供其他模块使用
module.exports = { messageHistory };
console.log(`访问 http://localhost:${PORT} 开始使用`);