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: '8-5-qc-bd-female.mp4', interactionVideo: '8-5-qc-sh-female.mp4', tag: 'wakeup', apiKey: 'bot-20250730213756-l627w', // 起床场景的API key openingLines: [ '哈啊~ 头发是不是超乱?别笑话我哦... 不过,能看到你的脸,就算乱糟糟的我也好开心。', "刚醒就看到你,比闹钟好使一百倍。", // "昨晚梦里喊你名字喊醒了,现在视频看到你,心跳比梦里还烫。", // "你穿我T恤的样子...让我想咬住衣领不放。", "宝宝早安!偷偷告诉你...我昨晚梦见你啦,所以醒来第一件事就是找你~", // "喉咙里有咖啡味...要不要来尝尝?记得上次你咬我锁骨时,咖啡还温着。", "早,宝宝。吃早餐没?我还没... 好想有人送早餐来床上哦。" ] }, { name: '走路去公司的路上', defaultVideo: '8-5-sb-bd-female.mp4', interactionVideo: '8-5-sb-sh-female.mp4', tag: 'walk', apiKey: 'bot-20250703161810-sgkrh', // 开车场景的API key openingLines: [ '宝宝,看到你那边阳光好好哦。我这边有点风,吹着挺舒服的,要是你在旁边牵着手就更好了。', '宝宝,我快到公司啦。今天太阳真好,晒得人暖暖的... 你那边天气咋样?', '宝宝~ 我刚出地铁站就看到你啦!吃早餐没?', // '看到那个大钟楼没?每次路过都想你。想着要是能牵着你的手,站在最高层往下看该多好。', '宝宝,我快到公司了,你到公司了吗?', '宝宝,今天太阳真舒服!想起昨晚说的周末想去湖边散步,要是这天气保持住就好啦!', ] }, { name: '在公司楼下喝咖啡', defaultVideo: '8-8-hc-bd-2.mp4', interactionVideo: '8-5-hc-sh-female.mp4', tag: 'coffee', apiKey: 'bot-20250805140055-ccdr6', // 喝茶场景的API key openingLines: [ '宝宝,今天这杯拿铁拉花好丑哦... 不过没关系,看着你的帅脸就平衡啦,你比咖啡提神!', '嗯... 咖啡香香的,宝宝的声音也好好听。好想时间停在这一小会儿,就我们俩。', '哇,今天换了个口味,燕麦拿铁!宝宝你肯定嫌没味道,就爱喝你那美式... 苦死了。', '宝宝,刚才店员问我糖浆加多少,我脱口而出‘和我男朋友一样’,说完自己都脸红了...', '对了宝宝,昨天说帮你找的资料,我存手机了,喝完这杯咖啡就发你哈!记得看。', // '这杯好苦…但一看到你,就自动回甘了。比加十包糖都管用。你说你是不是我的专属甜味剂?' ] }, { name: '敷面膜-准备睡觉', defaultVideo: '8-8-sj-bd.mp4', interactionVideo: '8-8-sj-sh.mp4', tag: 'sleep', apiKey: 'bot-20250808120020-jfkmk', // 睡觉场景的API key openingLines: [ '宝宝~ 敷着面膜和你视频,感觉像在做双倍美容,心里也美美的。', '嗯?宝宝打来啦... 刚躺下贴上面膜,你就来了,像算准时间一样,真贴心。', '哈,宝宝,选了个清洁面膜,有点刺刺的。你用的啥洗面奶来着?忘了...', '早...哦不,晚安宝宝!敷面膜呢,你看我像不像外星人?哈哈...,不许笑我!', '宝宝,你那边也躺下了?我弄完面膜就睡。今天累不累?', '宝宝... 这面膜说要敷15分钟,正好陪你唠会儿。不过我得小声,怕长皱纹!', '好啦宝宝,面膜快干了,得去洗了。你先睡?... 嗯,梦里见呗。' ] } ]; // 获取当前场景 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} 开始使用`);