const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); const bodyParser = require('body-parser'); const path = require('path'); const compression = require('compression'); const WebSocket = require('ws'); const http = require('http'); const { ConfigModel } = require('./models'); // Route Imports const authRoutes = require('./routes/auth'); const coreRoutes = require('./routes/core'); const academicRoutes = require('./routes/academic'); const featuresRoutes = require('./routes/features'); const aiRoutes = require('./ai-routes'); const PORT = 7860; const MONGO_URI = process.env.MONGO_URI; const app = express(); const server = http.createServer(app); app.use(compression({ filter: (req, res) => { if (req.originalUrl && req.originalUrl.includes('/api/ai/chat')) return false; return compression.filter(req, res); } })); app.use(cors()); app.use(bodyParser.json({ limit: '50mb' })); // 静态资源托管配置优化 app.use(express.static(path.join(__dirname, 'dist'), { setHeaders: (res, filePath) => { // 关键修复:入口文件、Service Worker 和 Manifest 必须禁止缓存,确保每次都获取最新版本 if ( filePath.endsWith('.html') || filePath.endsWith('sw.js') || filePath.endsWith('manifest.webmanifest') ) { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); } else { // 其他带 Hash 的静态资源可以强缓存 1 年 res.setHeader('Cache-Control', 'public, max-age=31536000, immutable'); } } })); const connectDB = async () => { try { await mongoose.connect(MONGO_URI, { serverSelectionTimeoutMS: 30000 }); console.log('✅ MongoDB Connected'); } catch (err) { console.error('❌ MongoDB Connection Error:', err.message); } }; connectDB(); // --- WebSocket Proxy for Gemini Live API --- const wss = new WebSocket.Server({ noServer: true }); server.on('upgrade', (request, socket, head) => { if (request.url.startsWith('/ws/live')) { wss.handleUpgrade(request, socket, head, (ws) => { wss.emit('connection', ws, request); }); } else { socket.destroy(); } }); wss.on('connection', async (ws, req) => { // Extract User Info from Query Params for Logging & Future Context const params = new URLSearchParams(req.url.split('?')[1]); const userId = params.get('userId') || 'anon'; const username = params.get('username') || 'Anonymous'; console.log(`🔌 Client connected: ${username} (${userId})`); let geminiSession = null; let isGeminiConnected = false; try { // 1. Get API Key (Server-side Config) const config = await ConfigModel.findOne({ key: 'main' }); let apiKey = process.env.API_KEY; if (config && config.apiKeys && config.apiKeys.gemini && config.apiKeys.gemini.length > 0) { apiKey = config.apiKeys.gemini[0]; } if (!apiKey) { ws.send(JSON.stringify({ type: 'error', message: 'No Server API Key Configured' })); ws.close(); return; } // 2. Initialize Gemini SDK const { GoogleGenAI } = await import("@google/genai"); const client = new GoogleGenAI({ apiKey }); // 3. Connect to Gemini (Isolated Session per Connection) geminiSession = await client.live.connect({ model: 'gemini-2.5-flash-native-audio-preview-09-2025', config: { responseModalities: ['AUDIO'], speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: 'Kore' } } }, systemInstruction: { parts: [{ text: "你是一位乐于助人的校园AI助手。请必须使用中文(普通话)回答。回答要简短、自然、口语化。不要使用Markdown格式。" }] }, outputAudioTranscription: { model: true } }, callbacks: { onopen: () => { console.log(`Gemini Stream Open for ${username}`); isGeminiConnected = true; if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'status', content: 'CONNECTED' })); }, onmessage: (msg) => { const serverContent = msg.serverContent; if (serverContent?.modelTurn?.parts?.[0]?.inlineData?.data) { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'audio', data: serverContent.modelTurn.parts[0].inlineData.data })); } if (serverContent?.modelTurn?.parts?.[0]?.text) { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'text', content: serverContent.modelTurn.parts[0].text })); } if (serverContent?.turnComplete) { if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'turnComplete' })); } }, onclose: () => { console.log(`Gemini Stream Closed for ${username}`); isGeminiConnected = false; if (ws.readyState === WebSocket.OPEN) ws.close(); }, onerror: (err) => { console.error(`Gemini Error for ${username}:`, err); if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'error', message: 'Upstream Error' })); } } }); // 4. Forward Client -> Gemini ws.on('message', (message) => { if (!isGeminiConnected || !geminiSession) return; try { const parsed = JSON.parse(message); if (parsed.type === 'audio') { geminiSession.sendRealtimeInput({ media: { mimeType: 'audio/pcm;rate=16000', data: parsed.data } }); } } catch (e) { console.error('Parse error', e); } }); ws.on('close', () => { console.log(`Client disconnected: ${username}`); if (geminiSession) { try { geminiSession.close(); } catch(e){} geminiSession = null; isGeminiConnected = false; } }); } catch (e) { console.error('WebSocket Handshake Error:', e); if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'error', message: e.message })); ws.close(); } } }); // Mount Routes app.use('/api/auth', authRoutes); app.use('/api', coreRoutes); // Schools, Config, Students, Stats app.use('/api', academicRoutes); // Classes, Subjects, Courses, Scores, Schedules app.use('/api', featuresRoutes); // Games, Attendance, Wishes, etc. app.use('/api/users', require('./routes/auth')); // Re-map users root if needed or handle inside auth app.use('/api/ai', aiRoutes); app.get('*', (req, res) => { res.sendFile(path.join(__dirname, 'dist', 'index.html')); }); server.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));