const http = require('http'); const fs = require('fs'); const path = require('path'); const url = require('url'); const ROOT = __dirname; const CONFIG_PATH = path.join(ROOT, 'project.config.json'); const TEMPLATE_HTML = path.join(ROOT, '飞书审批配置文件_V1.0.html'); const ADDRESS_REGIONS_JS = path.join(ROOT, 'address-regions.js'); function loadProjectConfig() { try { if (fs.existsSync(CONFIG_PATH)) return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch (e) {} return { projectId: 'approval_of_design', port: 8080, host: '0.0.0.0', label: '飞书审批配置服务' }; } const PROJECT = loadProjectConfig(); const PORT = Number(process.env.PORT) || PROJECT.port || 8080; const HOST = process.env.HOST || PROJECT.host || '0.0.0.0'; const NODE_MAJOR = parseInt(String(process.versions.node).split('.')[0], 10); if (NODE_MAJOR < 16) { console.error('[ERROR] Node.js 16.x or later is required. Current: ' + process.version); process.exit(1); } let templateCache = null; function readTemplate() { if (!templateCache) { templateCache = fs.readFileSync(TEMPLATE_HTML, 'utf8'); } return templateCache; } function reloadTemplate() { templateCache = null; return readTemplate(); } function safeJsonForScript(value) { return JSON.stringify(value).replace(/<\//g, '\\u003c/'); } function renderBootScript(config, mode) { const project = { id: PROJECT.projectId || 'approval_of_design', label: PROJECT.label || '飞书审批配置服务' }; return [ '' ].join('\n'); } function injectBoot(html, config, mode) { return html.replace(/' ); } function prepareHtml(config, mode) { return inlineAddressRegions(injectBoot(readTemplate(), config, mode)); } function sanitizeFilename(name) { return String(name || '审批配置') .replace(/[<>:"/\\|?*\x00-\x1f]/g, '_') .replace(/\s+/g, '_') .slice(0, 80) || '审批配置'; } function sendJson(res, status, data) { res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(data)); } function readBody(req, limit) { const maxBytes = limit || 20 * 1024 * 1024; return new Promise(function (resolve, reject) { var body = ''; var size = 0; req.on('data', function (chunk) { size += chunk.length; if (size > maxBytes) { reject(new Error('request body too large')); req.destroy(); return; } body += chunk; }); req.on('end', function () { resolve(body); }); req.on('error', reject); }); } function parseInitialConfigFromHtml(html) { const match = html.match(/window\.__INITIAL_CONFIG__\s*=\s*([\s\S]*?);\s*(?:\n|<\/script>)/); if (!match) return null; try { return JSON.parse(match[1]); } catch (e) { return null; } } const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } const parsed = url.parse(req.url, true); const pathname = parsed.pathname; try { if (req.method === 'GET' && (pathname === '/' || pathname === '/editor')) { const initial = parsed.query.config ? JSON.parse(parsed.query.config) : null; const html = prepareHtml(initial, 'server'); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(html); return; } if (req.method === 'GET' && pathname === '/address-regions.js') { if (!fs.existsSync(ADDRESS_REGIONS_JS)) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('address-regions.js not found'); return; } res.writeHead(200, { 'Content-Type': 'application/javascript; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(fs.readFileSync(ADDRESS_REGIONS_JS, 'utf8')); return; } if (req.method === 'GET' && pathname === '/api/health') { sendJson(res, 200, { ok: true, service: PROJECT.label, projectId: PROJECT.projectId }); return; } if (req.method === 'POST' && pathname === '/api/export') { const body = await readBody(req); const config = JSON.parse(body); config.exportedAt = new Date().toISOString(); config.projectId = PROJECT.projectId; const html = prepareHtml(config, 'standalone'); const filename = sanitizeFilename(config.approvalName) + '.html'; res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Content-Disposition': 'attachment; filename="' + encodeURIComponent(filename) + '"', 'Cache-Control': 'no-store' }); res.end(html); return; } if (req.method === 'POST' && pathname === '/api/import') { const body = await readBody(req); const payload = JSON.parse(body); let config = null; if (payload.config) { config = payload.config; } else if (payload.html) { config = parseInitialConfigFromHtml(payload.html); } if (!config) { sendJson(res, 400, { ok: false, error: 'invalid import payload' }); return; } sendJson(res, 200, { ok: true, config }); return; } if (req.method === 'POST' && pathname === '/api/reload-template') { reloadTemplate(); sendJson(res, 200, { ok: true }); return; } res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Not Found'); } catch (err) { sendJson(res, 500, { ok: false, error: err.message || 'server error' }); } }); server.listen(PORT, HOST, () => { console.log('[' + (PROJECT.label || 'approval') + '] http://' + HOST + ':' + PORT); });