Spaces:
Running
Running
| 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}`)); | |