stud-manager / server.js
dvc890's picture
Update server.js
e20b9d1 verified
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}`));