/** * OPC 培训站 · 独立启动服务 * ------------------------------------------------------------- * 用途:把构建产物 dist/ 静态托管 + 挂载模拟认证 API,一条命令跑起来。 * 启动:npm run build && node server/start.mjs * 默认端口 8091(避开 PineAgentsServer 的 8090)。 * 迁移:后期接真实后端后,本文件可替换为生产服务器/网关。 */ import http from 'node:http'; import { createReadStream, existsSync, statSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import { mockApi } from './mock-api.mjs'; const PORT = process.env.PORT || 8091; const DIST = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../dist'); const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.webp': 'image/webp', '.json': 'application/json; charset=utf-8', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ico': 'image/x-icon' }; function serveStatic(req, res) { let p = (req.url || '/').split('?')[0]; if (p === '/' || p === '') p = '/index.html'; // 防路径穿越 const file = path.normalize(path.join(DIST, p)); if (!file.startsWith(DIST)) return notFound(res); if (!existsSync(file) || statSync(file).isDirectory()) { // SPA 回退:非文件请求一律给 index.html(仅当请求的不是带扩展名的资源) if (!path.extname(file)) return sendFile(res, path.join(DIST, 'index.html')); return notFound(res); } sendFile(res, file); } function sendFile(res, file) { const ext = path.extname(file).toLowerCase(); res.setHeader('Content-Type', MIME[ext] || 'application/octet-stream'); res.setHeader('Cache-Control', ext === '.html' ? 'no-cache' : 'public, max-age=31536000, immutable'); createReadStream(file).pipe(res); } function notFound(res) { res.statusCode = 404; res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.end('Not Found'); } const server = http.createServer((req, res) => { mockApi(req, res, () => serveStatic(req, res)); }); server.listen(PORT, () => { console.log(`▶ OPC 培训站已启动: http://localhost:${PORT}`); console.log(` 静态资源: ${DIST}`); console.log(` 模拟登录: POST /api/auth/login (pine / 123456)`); });