Spaces:
Running
Running
| import http from 'http'; | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| import { fileURLToPath } from 'url'; | |
| import { createAiCore, UpstreamError } from './server/aiCore.js'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| const PORT = process.env.PORT || 7860; | |
| const HOST = process.env.HOST || '0.0.0.0'; | |
| const distDir = path.join(__dirname, 'dist'); | |
| const ai = createAiCore(process.env); | |
| function sendJson(res, statusCode, payload) { | |
| res.writeHead(statusCode, { | |
| 'Content-Type': 'application/json; charset=utf-8', | |
| 'Cache-Control': 'no-store', | |
| 'Access-Control-Allow-Origin': '*', | |
| }); | |
| res.end(JSON.stringify(payload)); | |
| } | |
| function readBody(req) { | |
| return new Promise((resolve, reject) => { | |
| let data = ''; | |
| req.on('data', (chunk) => (data += chunk)); | |
| req.on('end', () => resolve(data)); | |
| req.on('error', reject); | |
| }); | |
| } | |
| // MIME types mapping | |
| const mimeTypes = { | |
| '.html': 'text/html; charset=utf-8', | |
| '.js': 'application/javascript', | |
| '.mjs': 'application/javascript', | |
| '.css': 'text/css', | |
| '.json': 'application/json', | |
| '.png': 'image/png', | |
| '.jpg': 'image/jpeg', | |
| '.jpeg': 'image/jpeg', | |
| '.gif': 'image/gif', | |
| '.svg': 'image/svg+xml', | |
| '.woff': 'font/woff', | |
| '.woff2': 'font/woff2', | |
| '.ttf': 'font/ttf', | |
| '.eot': 'application/vnd.ms-fontobject', | |
| }; | |
| function getMimeType(filePath) { | |
| const ext = path.extname(filePath).toLowerCase(); | |
| return mimeTypes[ext] || 'application/octet-stream'; | |
| } | |
| function serveFile(res, filePath, statusCode = 200) { | |
| try { | |
| const content = fs.readFileSync(filePath); | |
| const mimeType = getMimeType(filePath); | |
| res.writeHead(statusCode, { | |
| 'Content-Type': mimeType, | |
| 'Cache-Control': 'public, max-age=3600', | |
| 'Access-Control-Allow-Origin': '*', | |
| }); | |
| res.end(content); | |
| } catch (err) { | |
| console.error(`Error serving file ${filePath}:`, err); | |
| res.writeHead(500, { 'Content-Type': 'text/plain' }); | |
| res.end('Internal Server Error'); | |
| } | |
| } | |
| const server = http.createServer((req, res) => { | |
| // Set CORS headers | |
| res.setHeader('Access-Control-Allow-Origin', '*'); | |
| res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); | |
| res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); | |
| if (req.method === 'OPTIONS') { | |
| res.writeHead(200); | |
| res.end(); | |
| return; | |
| } | |
| // Remove query string from URL | |
| const urlPath = req.url.split('?')[0]; | |
| // API routes (production) | |
| if (urlPath === '/api/ai/ping' && req.method === 'GET') { | |
| return sendJson(res, 200, ai.ping()); | |
| } | |
| if (urlPath === '/api/ai/mix' && req.method === 'POST') { | |
| return (async () => { | |
| try { | |
| const raw = await readBody(req); | |
| const parsed = raw ? JSON.parse(raw) : {}; | |
| const result = await ai.mix({ | |
| substances: parsed.substances || [], | |
| temperatureC: parsed.temperatureC, | |
| }); | |
| return sendJson(res, 200, result); | |
| } catch (err) { | |
| const triedModels = err?.triedModels || []; | |
| const lastModel = err?.lastModel || null; | |
| if (err instanceof UpstreamError) { | |
| if (err.status === 401 || err.status === 403) { | |
| return sendJson(res, 401, { error: 'API key Groq invalide ou non autorisee', triedModels, lastModel }); | |
| } | |
| if (err.status === 429) { | |
| return sendJson(res, 429, { error: 'Quota/limite Groq atteinte (429). Reessaye plus tard.', triedModels, lastModel }); | |
| } | |
| if (err.status >= 400 && err.status < 500) { | |
| return sendJson(res, err.status, { error: err.upstreamMessage || err.message, triedModels, lastModel }); | |
| } | |
| return sendJson(res, 502, { error: 'Erreur upstream Groq lors de la generation IA', details: err.message, triedModels, lastModel }); | |
| } | |
| return sendJson(res, 500, { error: 'Erreur serveur /api/ai/mix', details: err?.message || String(err), triedModels, lastModel }); | |
| } | |
| })(); | |
| } | |
| let filePath = path.join(distDir, urlPath === '/' ? 'index.html' : urlPath); | |
| // Normalize path to prevent directory traversal | |
| filePath = path.normalize(filePath); | |
| if (!filePath.startsWith(distDir)) { | |
| res.writeHead(403, { 'Content-Type': 'text/plain' }); | |
| res.end('Forbidden'); | |
| return; | |
| } | |
| // Check if file exists | |
| try { | |
| const stats = fs.statSync(filePath); | |
| if (stats.isFile()) { | |
| serveFile(res, filePath); | |
| } else if (stats.isDirectory()) { | |
| // Try to serve index.html from the directory | |
| const indexPath = path.join(filePath, 'index.html'); | |
| if (fs.existsSync(indexPath)) { | |
| serveFile(res, indexPath); | |
| } else { | |
| res.writeHead(404, { 'Content-Type': 'text/plain' }); | |
| res.end('Not Found'); | |
| } | |
| } | |
| } catch (err) { | |
| // File not found, serve index.html for SPA routing | |
| const indexPath = path.join(distDir, 'index.html'); | |
| if (fs.existsSync(indexPath)) { | |
| serveFile(res, indexPath, 200); | |
| } else { | |
| res.writeHead(404, { 'Content-Type': 'text/plain' }); | |
| res.end('Not Found'); | |
| } | |
| } | |
| }); | |
| server.listen(PORT, HOST, () => { | |
| console.log(`\n✓ Server is running!`); | |
| console.log(`✓ Listening on http://${HOST}:${PORT}`); | |
| console.log(`✓ Environment: ${process.env.NODE_ENV || 'development'}`); | |
| }); | |
| process.on('SIGTERM', () => { | |
| console.log('SIGTERM received, shutting down gracefully...'); | |
| server.close(() => { | |
| console.log('Server closed'); | |
| process.exit(0); | |
| }); | |
| }); | |
| process.on('SIGINT', () => { | |
| console.log('SIGINT received, shutting down gracefully...'); | |
| server.close(() => { | |
| console.log('Server closed'); | |
| process.exit(0); | |
| }); | |
| }); | |