初始化
This commit is contained in:
205
server.js
Normal file
205
server.js
Normal file
@@ -0,0 +1,205 @@
|
||||
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) + ';',
|
||||
'</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);
|
||||
});
|
||||
Reference in New Issue
Block a user