WebRtc_QingGan/server.js
songjvcheng d34ee348b2
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m7s
修改开场白
2025-08-13 21:16:34 +08:00

546 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: '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} 开始使用`);