guanghulab/style-system/apply-skin.js
Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

257 lines
7.5 KiB
JavaScript

/**
* UI 皮肤部署引擎 · apply-skin.js
*
* 铸渊 · 壳-核分离架构
* 读取皮肤 JSON → 安全校验 → 生成覆盖 CSS → 部署到目标页面
*
* 用法: node apply-skin.js <skin-file.json>
*
* 版权: 国作登字-2026-A-00037559
*/
'use strict';
const fs = require('fs');
const path = require('path');
// ─── 配置 ───
const SKINS_DIR = path.join(__dirname, 'skins');
const INBOX_DIR = path.join(SKINS_DIR, 'inbox');
const APPLIED_DIR = path.join(SKINS_DIR, 'applied');
const REJECTED_DIR = path.join(SKINS_DIR, 'rejected');
// 站点根目录(服务器部署路径)
const SITE_ROOT = process.env.SITE_ROOT || path.join(__dirname, '..');
// 目标页面映射
const TARGET_MAP = {
homepage: path.join(SITE_ROOT, 'homepage'),
chat: path.join(SITE_ROOT, 'persona-studio', 'frontend'),
dashboard: path.join(SITE_ROOT, 'dashboard'),
login: path.join(SITE_ROOT, 'persona-studio', 'frontend')
};
// ─── 安全校验 ───
const FORBIDDEN_PATTERNS = [
/<script/i,
/javascript:/i,
/on(click|load|error|mouseover|mouseout|focus|blur|submit|change|input)\s*=/i,
/expression\s*\(/i,
/url\s*\(\s*['"]?(https?:|data:|javascript:|ftp:)/i,
/@import\s/i,
/behavior\s*:/i,
/-moz-binding/i
];
const FORBIDDEN_HIDE_SELECTORS = [
'#sendBtn',
'#loginBtn',
'.btn-primary',
'#msgInput',
'#emailModal'
];
function validateSkin(skin) {
const errors = [];
// Required fields
if (!skin.skin_id) errors.push('缺少 skin_id');
if (!skin.author) errors.push('缺少 author');
if (!skin.target) errors.push('缺少 target');
if (!TARGET_MAP[skin.target]) errors.push('无效的 target: ' + skin.target);
// Check custom_css for forbidden patterns
if (skin.custom_css) {
FORBIDDEN_PATTERNS.forEach(function(pattern) {
if (pattern.test(skin.custom_css)) {
errors.push('custom_css 包含禁止内容: ' + pattern.toString());
}
});
// Check for hiding functional elements
FORBIDDEN_HIDE_SELECTORS.forEach(function(sel) {
if (skin.custom_css.includes(sel) && /display\s*:\s*none/i.test(skin.custom_css)) {
errors.push('不允许隐藏功能性元素: ' + sel);
}
});
}
// Check css_overrides for forbidden patterns
if (skin.css_overrides) {
var cssStr = JSON.stringify(skin.css_overrides);
FORBIDDEN_PATTERNS.forEach(function(pattern) {
if (pattern.test(cssStr)) {
errors.push('css_overrides 包含禁止内容: ' + pattern.toString());
}
});
}
// Check text_overrides for HTML
if (skin.text_overrides) {
Object.keys(skin.text_overrides).forEach(function(sel) {
var text = skin.text_overrides[sel];
if (/<[^>]+>/.test(text)) {
errors.push('text_overrides 不允许包含 HTML 标签: ' + sel);
}
});
}
return errors;
}
// ─── 生成 CSS ───
function generateCSS(skin) {
var lines = [];
lines.push('/* UI Skin: ' + skin.skin_id + ' by ' + skin.author + ' */');
lines.push('/* Target: ' + skin.target + ' · ' + skin.description + ' */');
lines.push('/* Generated: ' + new Date().toISOString() + ' */');
lines.push('');
// CSS variable overrides
if (skin.css_overrides) {
Object.keys(skin.css_overrides).forEach(function(selector) {
var props = skin.css_overrides[selector];
lines.push(selector + ' {');
Object.keys(props).forEach(function(prop) {
lines.push(' ' + prop + ': ' + props[prop] + ';');
});
lines.push('}');
lines.push('');
});
}
// Custom CSS
if (skin.custom_css) {
lines.push('/* Custom CSS */');
lines.push(skin.custom_css);
lines.push('');
}
return lines.join('\n');
}
// ─── 生成 text override JS ───
function generateTextScript(skin) {
if (!skin.text_overrides || Object.keys(skin.text_overrides).length === 0) {
return null;
}
var lines = [];
lines.push('/* Text Overrides: ' + skin.skin_id + ' */');
lines.push('(function() {');
lines.push(' document.addEventListener("DOMContentLoaded", function() {');
Object.keys(skin.text_overrides).forEach(function(sel) {
// Validate selector: only allow CSS selector-safe characters
if (!/^[a-zA-Z0-9_.#\-\s>+~:[\]='"^$*|,()]+$/.test(sel)) {
return; // skip unsafe selectors
}
// Escape for safe JS string embedding: backslash first, then quotes and newlines
var text = skin.text_overrides[sel]
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r');
lines.push(' var el = document.querySelector(' + JSON.stringify(sel) + ');');
lines.push(' if (el) el.textContent = \'' + text + '\';');
});
lines.push(' });');
lines.push('})();');
return lines.join('\n');
}
// ─── 主流程 ───
function applySkin(skinFileName) {
var skinPath = path.join(INBOX_DIR, skinFileName);
if (!fs.existsSync(skinPath)) {
console.error('❌ 皮肤文件不存在: ' + skinPath);
process.exit(1);
}
var skin;
try {
skin = JSON.parse(fs.readFileSync(skinPath, 'utf-8'));
} catch (e) {
console.error('❌ JSON 解析失败: ' + e.message);
process.exit(1);
}
console.log('📋 皮肤包信息:');
console.log(' ID: ' + skin.skin_id);
console.log(' 作者: ' + skin.author);
console.log(' 目标: ' + skin.target);
console.log(' 描述: ' + skin.description);
console.log('');
// Validate
var errors = validateSkin(skin);
if (errors.length > 0) {
console.error('❌ 安全校验未通过:');
errors.forEach(function(err) {
console.error(' - ' + err);
});
// Move to rejected
var rejectPath = path.join(REJECTED_DIR, skinFileName);
fs.renameSync(skinPath, rejectPath);
console.error('📁 已移至 rejected/');
process.exit(1);
}
console.log('✅ 安全校验通过');
// Generate CSS
var css = generateCSS(skin);
var targetDir = TARGET_MAP[skin.target];
// Backup existing skin override if any
var overridePath = path.join(targetDir, 'skin-override.css');
if (fs.existsSync(overridePath)) {
var backupName = 'skin-override.backup.' + Date.now() + '.css';
fs.copyFileSync(overridePath, path.join(targetDir, backupName));
console.log('📦 已备份旧皮肤: ' + backupName);
}
// Write CSS override
fs.writeFileSync(overridePath, css, 'utf-8');
console.log('🎨 CSS 覆盖已写入: ' + overridePath);
// Write text override script if needed
var textScript = generateTextScript(skin);
if (textScript) {
var scriptPath = path.join(targetDir, 'skin-text-override.js');
fs.writeFileSync(scriptPath, textScript, 'utf-8');
console.log('📝 文字覆盖已写入: ' + scriptPath);
}
// Move to applied
var appliedPath = path.join(APPLIED_DIR, skinFileName);
fs.copyFileSync(skinPath, appliedPath);
fs.unlinkSync(skinPath);
console.log('📁 皮肤已归档至 applied/');
console.log('');
console.log('✨ 皮肤部署完成!');
console.log('⚠️ 请确认目标 HTML 文件中已引入 skin-override.css:');
console.log(' <link rel="stylesheet" href="skin-override.css">');
if (textScript) {
console.log(' <script src="skin-text-override.js"><\/script>');
}
}
// ─── CLI ───
var args = process.argv.slice(2);
if (args.length === 0) {
console.log('用法: node apply-skin.js <skin-file.json>');
console.log('');
console.log('示例: node apply-skin.js SKIN-20260417-001.json');
console.log('');
console.log('皮肤文件放在: style-system/skins/inbox/ 目录下');
process.exit(0);
}
applySkin(args[0]);