WebRtc_QingGan/server.js
Song367 6ba96bc177
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 2m55s
添加更新历史对话
2025-08-13 13:51:14 +08:00

549 lines
18 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: [
'宝宝,一看到你,我这电量‘噌’就满了。准备关机前最后充会儿电…嗯,用眼睛充。',
'熄灯前最后一道光是你,真好。感觉今天积攒的烦心事,都被你眼睛里的星星照没了。',
'唉…手指头碰不到你屏幕都嫌凉。下次见面,这距离得用抱抱补回来,利息按秒算。',
'周围好安静就剩你的呼吸声当背景音乐了。比什么助眠App都好使…就是听久了心跳会抢拍子。',
'困不困?我眼皮在打架了…但就是想再多看你几秒。感觉多看一秒,梦里遇见你的概率就大一点。',
'好啦,我的小月亮,该哄世界睡觉了…但你先哄哄我?随便说句什么,我当睡前故事收藏。',
'捕捉到一只睡前小可爱…成功!',
'世界要静音了…但你的声音是白名单。多说几句?'
]
}
];
// 获取当前场景
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} 开始使用`);