Spaces:
Running
Running
File size: 7,235 Bytes
ac9de5d 57073bc 720777f 57073bc 720777f 4859284 57073bc 720777f e20b9d1 57073bc 4859284 57073bc 720777f 4859284 720777f 57073bc 720777f cd87b40 720777f cd87b40 720777f 57073bc 720777f 4859284 720777f 4859284 720777f 40883e7 4859284 40883e7 ac70ee9 40883e7 ac70ee9 40883e7 4859284 0f591c7 ac70ee9 40883e7 4859284 40883e7 0f591c7 40883e7 4859284 0f591c7 4859284 40883e7 4859284 40883e7 4859284 40883e7 4859284 40883e7 4859284 0f591c7 40883e7 4859284 40883e7 0f591c7 40883e7 4859284 40883e7 4859284 40883e7 4859284 40883e7 0f591c7 40883e7 0f591c7 40883e7 4859284 57073bc 720777f 40883e7 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
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}`));
|