207 lines
6.3 KiB
JavaScript
207 lines
6.3 KiB
JavaScript
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 [
|
|
'<script id="app-boot">',
|
|
'window.APP_PROJECT = ' + safeJsonForScript(project) + ';',
|
|
"window.APP_MODE = '" + mode + "';",
|
|
'window.__INITIAL_CONFIG__ = ' + safeJsonForScript(config) + ';',
|
|
"document.documentElement.setAttribute('data-app-mode', window.APP_MODE);",
|
|
'</script>'
|
|
].join('\n');
|
|
}
|
|
|
|
function injectBoot(html, config, mode) {
|
|
return html.replace(/<script id="app-boot">[\s\S]*?<\/script>/, renderBootScript(config, mode));
|
|
}
|
|
|
|
function inlineAddressRegions(html) {
|
|
if (!fs.existsSync(ADDRESS_REGIONS_JS)) return html;
|
|
const js = fs.readFileSync(ADDRESS_REGIONS_JS, 'utf8');
|
|
return html.replace(
|
|
/<script src="address-regions\.js"><\/script>/,
|
|
'<script>\n' + js + '\n</script>'
|
|
);
|
|
}
|
|
|
|
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);
|
|
});
|