import http from 'node:http'; import { createReadStream, existsSync, statSync } from 'node:fs'; import { extname, join, normalize, resolve } from 'node:path'; // 这是 mini-desktop RC 部署的轻量静态服务:同时把前端相对 API 路径反代到 muse-server。 // 生产化部署应换成 Nginx/Caddy;RC smoke 用 Node 单文件可减少额外系统依赖和端口冲突。 const args = Object.fromEntries( process.argv.slice(2).map((arg) => { const [key, ...rest] = arg.replace(/^--/, '').split('='); return [key, rest.join('=') || '']; }), ); const root = resolve(args.root || '.'); const port = Number(args.port || 4175); const backend = new URL(args.backend || 'http://127.0.0.1:48081'); const proxyPrefixes = (args.proxy || '/app-api,/admin-api,/sse').split(',').filter(Boolean); const contentTypes = new Map([ ['.html', 'text/html; charset=utf-8'], ['.js', 'text/javascript; charset=utf-8'], ['.mjs', 'text/javascript; charset=utf-8'], ['.css', 'text/css; charset=utf-8'], ['.json', 'application/json; charset=utf-8'], ['.png', 'image/png'], ['.jpg', 'image/jpeg'], ['.jpeg', 'image/jpeg'], ['.svg', 'image/svg+xml'], ['.ico', 'image/x-icon'], ['.woff', 'font/woff'], ['.woff2', 'font/woff2'], ['.ttf', 'font/ttf'], ['.eot', 'application/vnd.ms-fontobject'], ]); function sendNotFound(res) { res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' }); res.end('Not Found'); } function proxy(req, res) { const target = new URL(req.url || '/', backend); const headers = { ...req.headers, host: backend.host }; const upstream = http.request(target, { method: req.method, headers }, (proxyRes) => { res.writeHead(proxyRes.statusCode || 502, proxyRes.headers); proxyRes.pipe(res); }); upstream.on('error', (error) => { res.writeHead(502, { 'content-type': 'application/json; charset=utf-8' }); res.end(JSON.stringify({ code: 502, msg: `backend unavailable: ${error.code || error.message}` })); }); req.pipe(upstream); } function serve(req, res) { const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`); if (proxyPrefixes.some((prefix) => url.pathname === prefix || url.pathname.startsWith(`${prefix}/`))) { proxy(req, res); return; } const decodedPath = decodeURIComponent(url.pathname); const relative = normalize(decodedPath).replace(/^\/+/, ''); let file = resolve(join(root, relative)); if (!file.startsWith(root)) { sendNotFound(res); return; } // SPA 深链统一回落到 index.html,避免刷新路由时 404。 if (!existsSync(file) || statSync(file).isDirectory()) { file = join(root, 'index.html'); } if (!existsSync(file) || statSync(file).isDirectory()) { sendNotFound(res); return; } const type = contentTypes.get(extname(file).toLowerCase()) || 'application/octet-stream'; res.writeHead(200, { 'content-type': type }); createReadStream(file).pipe(res); } http.createServer(serve).listen(port, '0.0.0.0', () => { console.log(`static proxy listening on ${port}, root=${root}, backend=${backend.origin}`); });