import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; const PORT = Number(process.env.PORT || 80); const DIST_DIR = path.resolve(process.cwd(), 'dist'); const DASHSCOPE_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1'; function getContentType(filePath) { const ext = path.extname(filePath).toLowerCase(); switch (ext) { case '.html': return 'text/html; charset=utf-8'; case '.js': return 'text/javascript; charset=utf-8'; case '.css': return 'text/css; charset=utf-8'; case '.svg': return 'image/svg+xml'; case '.png': return 'image/png'; case '.jpg': case '.jpeg': return 'image/jpeg'; case '.ico': return 'image/x-icon'; case '.json': return 'application/json; charset=utf-8'; case '.map': return 'application/json; charset=utf-8'; case '.woff2': return 'font/woff2'; case '.txt': return 'text/plain; charset=utf-8'; default: return 'application/octet-stream'; } } function sendJson(res, statusCode, body) { const payload = JSON.stringify(body); res.statusCode = statusCode; res.setHeader('Content-Type', 'application/json; charset=utf-8'); res.setHeader('Cache-Control', 'no-store'); res.end(payload); } async function readJsonBody(req) { const chunks = []; for await (const chunk of req) chunks.push(chunk); const raw = Buffer.concat(chunks).toString('utf-8'); if (!raw) return null; return JSON.parse(raw); } function applyApiCorsHeaders(req, res) { const origin = req.headers.origin; if (!origin) return; res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary', 'Origin'); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); } async function handleChatCompletions(req, res) { applyApiCorsHeaders(req, res); if (req.method === 'OPTIONS') { res.statusCode = 204; res.end(); return; } if (req.method !== 'POST') { sendJson(res, 405, { error: 'Method Not Allowed' }); return; } const apiKey = process.env.DASHSCOPE_API_KEY || process.env.VITE_API_KEY || process.env.DASHSCOPE_KEY; if (!apiKey) { sendJson(res, 500, { error: 'Missing DASHSCOPE_API_KEY in server environment' }); return; } let body; try { body = await readJsonBody(req); } catch (e) { sendJson(res, 400, { error: 'Invalid JSON request body' }); return; } const upstreamUrl = `${DASHSCOPE_BASE_URL}/chat/completions`; const abortController = new AbortController(); req.on('close', () => { abortController.abort(); }); let upstream; try { upstream = await fetch(upstreamUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify(body ?? {}), signal: abortController.signal, }); } catch (e) { sendJson(res, 502, { error: 'Upstream request failed' }); return; } const isStream = Boolean(body && body.stream); res.statusCode = upstream.status; res.setHeader( 'Content-Type', upstream.headers.get('content-type') || (isStream ? 'text/event-stream; charset=utf-8' : 'application/json; charset=utf-8') ); // Help prevent buffering in common reverse proxies. res.setHeader('Cache-Control', isStream ? 'no-cache, no-transform' : 'no-store'); res.setHeader('X-Accel-Buffering', 'no'); // For streaming: flush headers ASAP so the browser can start rendering. if (isStream) { res.setHeader('Connection', 'keep-alive'); } if (typeof res.flushHeaders === 'function') res.flushHeaders(); if (!upstream.body) { const text = await upstream.text().catch(() => ''); res.end(text); return; } try { const reader = upstream.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; if (value) res.write(Buffer.from(value)); } } catch (e) { // If client disconnected, ignore; otherwise just end response. } finally { res.end(); } } function serveStatic(req, res, url) { if (req.method !== 'GET' && req.method !== 'HEAD') { res.statusCode = 405; res.end('Method Not Allowed'); return; } const pathname = decodeURIComponent(url.pathname); // Prevent path traversal. const safePath = path.normalize(pathname).replace(/^(\.\.(\/|\\|$))+/, ''); const fsPath = path.join(DIST_DIR, safePath); let resolved = fsPath; try { if (pathname.endsWith('/')) { resolved = path.join(fsPath, 'index.html'); } else if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) { resolved = path.join(resolved, 'index.html'); } } catch { // ignore } try { if (!fs.existsSync(resolved) || !fs.statSync(resolved).isFile()) { // SPA history fallback. resolved = path.join(DIST_DIR, 'index.html'); } } catch { resolved = path.join(DIST_DIR, 'index.html'); } const isAsset = pathname.startsWith('/assets/'); const contentType = getContentType(resolved); res.statusCode = 200; res.setHeader('Content-Type', contentType); res.setHeader( 'Cache-Control', isAsset ? 'public, max-age=31536000, immutable' : 'no-cache' ); if (req.method === 'HEAD') { res.end(); return; } fs.createReadStream(resolved).pipe(res); } const server = http.createServer(async (req, res) => { try { const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`); if (url.pathname === '/api/chat/completions') { await handleChatCompletions(req, res); return; } serveStatic(req, res, url); } catch (e) { res.statusCode = 500; res.end('Internal Server Error'); } }); server.listen(PORT, () => { console.log(`Server listening on :${PORT}`); });