80 lines
2.7 KiB
JavaScript
80 lines
2.7 KiB
JavaScript
const http = require('http');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const port = Number(process.env.PORT || 8080);
|
|
const publicDir = path.join(__dirname, 'public');
|
|
const publicRoot = publicDir + path.sep;
|
|
|
|
const types = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.js': 'application/javascript; charset=utf-8',
|
|
'.json': 'application/json; charset=utf-8',
|
|
'.svg': 'image/svg+xml',
|
|
};
|
|
|
|
function readBody(req) {
|
|
return new Promise((resolve, reject) => {
|
|
let body = '';
|
|
req.on('data', chunk => {
|
|
body += chunk;
|
|
if (body.length > 12 * 1024 * 1024) {
|
|
req.destroy();
|
|
reject(new Error('Request body is too large'));
|
|
}
|
|
});
|
|
req.on('end', () => resolve(body));
|
|
req.on('error', reject);
|
|
});
|
|
}
|
|
|
|
function send(res, status, body, contentType = 'application/json; charset=utf-8', headOnly = false) {
|
|
res.writeHead(status, {
|
|
'content-type': contentType,
|
|
'cache-control': 'no-store',
|
|
});
|
|
res.end(headOnly ? '' : body);
|
|
}
|
|
|
|
function serveStatic(req, res) {
|
|
const cleanUrl = decodeURIComponent(req.url.split('?')[0]);
|
|
const file = cleanUrl === '/' ? '/index.html' : cleanUrl;
|
|
const target = path.normalize(path.join(publicDir, file));
|
|
if (!target.startsWith(publicRoot)) return send(res, 403, 'Forbidden', 'text/plain; charset=utf-8');
|
|
|
|
fs.readFile(target, (err, data) => {
|
|
if (err) return send(res, 404, 'Not found', 'text/plain; charset=utf-8');
|
|
send(res, 200, data, types[path.extname(target)] || 'application/octet-stream', req.method === 'HEAD');
|
|
});
|
|
}
|
|
|
|
async function proxyChat(req, res) {
|
|
try {
|
|
const payload = JSON.parse(await readBody(req));
|
|
const baseUrl = String(payload.baseUrl || '').replace(/\/+$/, '');
|
|
if (!/^https?:\/\//.test(baseUrl)) throw new Error('A valid API base URL is required');
|
|
if (!payload.body || typeof payload.body !== 'object') throw new Error('Missing chat completion body');
|
|
|
|
const headers = { 'content-type': 'application/json' };
|
|
if (payload.apiKey) headers.authorization = `Bearer ${payload.apiKey}`;
|
|
|
|
const upstream = await fetch(`${baseUrl}/chat/completions`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify(payload.body),
|
|
});
|
|
const text = await upstream.text();
|
|
send(res, upstream.status, text);
|
|
} catch (error) {
|
|
send(res, 400, JSON.stringify({ error: error.message }));
|
|
}
|
|
}
|
|
|
|
http.createServer((req, res) => {
|
|
if (req.method === 'POST' && req.url === '/api/chat') return proxyChat(req, res);
|
|
if (req.method === 'GET' || req.method === 'HEAD') return serveStatic(req, res);
|
|
send(res, 405, 'Method not allowed', 'text/plain; charset=utf-8');
|
|
}).listen(port, () => {
|
|
console.log(`Calorie AI web listening on ${port}`);
|
|
});
|