/** * 门户主题 favicon 兜底资产生成器(4 套皮肤各一份) * - 输出 templates/themes//assets/favicon.svg + favicon.png(128×128) * - head.html 在 site.config['favicon'] 未配置时输出 url.asset('favicon.png')(见 fragments/head.html) * 图形语言对齐种子 logo-8 系(圆点+书脊),主色取各主题 theme.json 的 primary 参数默认值; * 小尺寸下高对比扁平化:无边框、无环形留白。 * 用法:node theme-favicons.cjs (在 front/ 下执行,依赖 ./node_modules/ws 与系统 Edge) */ const fs = require("fs"); const path = require("path"); const { spawn } = require("child_process"); const http = require("http"); const WebSocket = require("ws"); const EDGE = "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"; const ROOT = path.join(__dirname, "..", "..", "service", "src", "main", "resources", "templates", "themes"); const THEMES = [ { dir: "default", primary: "#1a5cb0" }, { dir: "classic-red", primary: "#b01a2e" }, { dir: "blue-white", primary: "#2f6bd8" }, { dir: "warm-kinder", primary: "#e8722a" }, ]; /** #rrggbb -> [r,g,b] */ function rgb(hex) { return [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16)); } function mix(hex, target, t) { const c = rgb(hex).map((v, i) => Math.round(v + (target[i] - v) * t)); return "#" + c.map(v => v.toString(16).padStart(2, "0")).join(""); } const light = hex => mix(hex, [255, 255, 255], 0.18); const dark = hex => mix(hex, [0, 0, 0], 0.30); function faviconSvg(primary) { return ` `; } /* ---- CDP 栅格化(Edge headless,128×128 视图内 img 渲染 svg 矢量,天然高清) ---- */ const sleep = ms => new Promise(r => setTimeout(r, ms)); function getJson(port, p) { return new Promise((resolve, reject) => { http.get({ host: "127.0.0.1", port, path: p }, res => { let d = ""; res.on("data", c => d += c); res.on("end", () => { try { resolve(JSON.parse(d)); } catch (e) { reject(e); } }); }).on("error", reject); }); } async function waitFor(fn, timeout, label) { const t0 = Date.now(); for (;;) { try { if (await fn()) return; } catch (e) { /* retry */ } if (Date.now() - t0 > timeout) throw new Error("timeout: " + label); await sleep(200); } } async function rasterizeSvg(svgPath, pngPath) { const PORT = 9400 + Math.floor(Math.random() * 300); const profile = path.join(require("os").tmpdir(), "fav-" + process.pid + "-" + Math.floor(Math.random() * 1e6)); const htmlPath = path.join(profile, "icon.html"); fs.mkdirSync(profile, { recursive: true }); fs.writeFileSync(htmlPath, ` `); const proc = spawn(EDGE, [ "--headless=new", "--disable-gpu", "--no-sandbox", "--hide-scrollbars", `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`, "--window-size=128,128", "--no-first-run", "--no-default-browser-check", "about:blank", ], { stdio: "ignore" }); try { await waitFor(async () => { try { await getJson(PORT, "/json/version"); return true; } catch (e) { return false; } }, 20000, "devtools"); const pages = await getJson(PORT, "/json"); const page = pages.find(p => p.type === "page"); const ws = new WebSocket(page.webSocketDebuggerUrl, { maxPayload: 64 * 1024 * 1024 }); await new Promise(r => ws.on("open", r)); const cdp = { id: 0, pending: new Map(), send(method, params = {}) { return new Promise((resolve, reject) => { const id = ++this.id; this.pending.set(id, { resolve, reject }); ws.send(JSON.stringify({ id, method, params })); }); }, }; ws.on("message", m => { const msg = JSON.parse(m); if (msg.id && cdp.pending.has(msg.id)) { const { resolve, reject } = cdp.pending.get(msg.id); cdp.pending.delete(msg.id); msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result); } }); await cdp.send("Page.enable"); await cdp.send("Emulation.setDeviceMetricsOverride", { width: 128, height: 128, deviceScaleFactor: 1, mobile: false }); const loaded = new Promise(r => { const on = m => { const d = JSON.parse(m); if (d.method === "Page.loadEventFired") { ws.off("message", on); r(); } }; ws.on("message", on); }); await cdp.send("Page.navigate", { url: "file:///" + htmlPath.replace(/\\/g, "/") }); await loaded; await sleep(500); const shot = await cdp.send("Page.captureScreenshot", { format: "png", captureBeyondViewport: false }); fs.writeFileSync(pngPath, Buffer.from(shot.data, "base64")); ws.close(); } finally { proc.kill(); } } (async () => { for (const t of THEMES) { const dir = path.join(ROOT, t.dir, "assets"); fs.writeFileSync(path.join(dir, "favicon.svg"), faviconSvg(t.primary)); await rasterizeSvg(path.join(dir, "favicon.svg"), path.join(dir, "favicon.png")); console.log("wrote", t.dir, "favicon.svg + favicon.png"); } console.log("done"); })().catch(e => { console.error(e); process.exit(1); });