 cab8273bf6
			
		
	
	
		cab8273bf6
		
	
	
	
		
			
	
		
	
	
		
			All checks were successful
		
		
	
	Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m53s
				
			
		
			
				
	
	
		
			481 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			481 lines
		
	
	
		
			14 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, {
 | ||
|   pingTimeout: 60000,    // 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
 | ||
|   },
 | ||
|   {
 | ||
|     name: '开车',
 | ||
|     defaultVideo: 'kc-bd-3.mp4',
 | ||
|     interactionVideo: 'kc-sh-3.mp4',
 | ||
|     tag: 'driving',
 | ||
|     apiKey: 'bot-20250623140339-r8f8b' // 开车场景的API key
 | ||
|   },
 | ||
|   {
 | ||
|     name: '喝茶',
 | ||
|     defaultVideo: 'hc-bd-3.mp4',
 | ||
|     interactionVideo: 'hc-sh-3(1).mp4',
 | ||
|     tag: 'tea',
 | ||
|     apiKey: 'bot-20250804180724-4dgtk' // 喝茶场景的API key
 | ||
|   },
 | ||
|   {
 | ||
|     name: '睡觉',
 | ||
|     defaultVideo: '8-8-sj-bd.mp4',
 | ||
|     interactionVideo: '8-8-sj-sh-1.mp4',
 | ||
|     tag: 'sleep',
 | ||
|     apiKey: 'bot-20250808120704-lbxwj' // 睡觉场景的API key
 | ||
|   }
 | ||
| ];
 | ||
| 
 | ||
| // 获取当前场景
 | ||
| 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
 | ||
|   });
 | ||
| });
 | ||
| 
 | ||
| // 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} 开始使用`);
 |