Some checks failed
Backend Maven CI / backend-local (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
3.0 KiB
JavaScript
89 lines
3.0 KiB
JavaScript
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}`);
|
||
});
|