All checks were successful
Deploy play.wenpai.net / deploy (push) Successful in 3s
- Schema validation (validate-plugins): field/type/enum/blueprint existence checks - Deep verification (play-verify): admin notice bar, plugin activation, JS error capture - Performance baseline (play-verify): navigation + playground load timing - Visual regression (play-visual): 3 viewports, ImageMagick pixel diff - Cross-browser (play-verify): --browser firefox/webkit, --mobile viewport - Accessibility (play-a11y): axe-core WCAG 2 AA audit - Error monitoring (play-smoke): blueprint URL reachability for all zip references - Fix: color contrast on featured card (opacity 0.45→0.7) for WCAG AA compliance Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
139 lines
4.3 KiB
JavaScript
Executable file
139 lines
4.3 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
// validate-plugins — plugins.json schema 校验 + 蓝图文件存在性检查
|
|
// 用法: validate-plugins [--fix-hints]
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const PLAY_DIR = path.join(process.env.HOME, 'play');
|
|
const PLUGINS_PATH = path.join(PLAY_DIR, 'plugins.json');
|
|
const SCHEMA_PATH = path.join(PLAY_DIR, 'plugins.schema.json');
|
|
|
|
const RED = '\x1b[31m', GREEN = '\x1b[32m', YELLOW = '\x1b[33m', NC = '\x1b[0m';
|
|
let errors = 0, warnings = 0;
|
|
|
|
function fail(msg) { console.error(` ${RED}[✗]${NC} ${msg}`); errors++; }
|
|
function pass(msg) { console.log(` ${GREEN}[✓]${NC} ${msg}`); }
|
|
function warn(msg) { console.log(` ${YELLOW}[!]${NC} ${msg}`); warnings++; }
|
|
|
|
// --- 1. 文件可读性 ---
|
|
console.log('\n=== plugins.json Schema 校验 ===\n');
|
|
|
|
let plugins, schema;
|
|
try {
|
|
plugins = JSON.parse(fs.readFileSync(PLUGINS_PATH, 'utf8'));
|
|
pass('plugins.json 可读且为合法 JSON');
|
|
} catch (e) {
|
|
fail(`plugins.json 读取/解析失败: ${e.message}`);
|
|
process.exit(1);
|
|
}
|
|
try {
|
|
schema = JSON.parse(fs.readFileSync(SCHEMA_PATH, 'utf8'));
|
|
pass('plugins.schema.json 可读');
|
|
} catch (e) {
|
|
fail(`plugins.schema.json 读取失败: ${e.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// --- 2. 结构校验 ---
|
|
if (!Array.isArray(plugins)) {
|
|
fail('plugins.json 顶层必须是数组');
|
|
process.exit(1);
|
|
}
|
|
pass(`共 ${plugins.length} 个条目`);
|
|
|
|
const requiredFields = schema.items.required;
|
|
const properties = schema.items.properties;
|
|
const seenIds = new Set();
|
|
|
|
for (const [i, item] of plugins.entries()) {
|
|
const label = `[${i}] ${item.id || '(无id)'}`;
|
|
|
|
// 必填字段
|
|
for (const field of requiredFields) {
|
|
if (item[field] === undefined || item[field] === null) {
|
|
fail(`${label}: 缺少必填字段 "${field}"`);
|
|
}
|
|
}
|
|
|
|
// 额外字段
|
|
const allowed = Object.keys(properties);
|
|
for (const key of Object.keys(item)) {
|
|
if (!allowed.includes(key)) {
|
|
fail(`${label}: 未知字段 "${key}"`);
|
|
}
|
|
}
|
|
|
|
// ID 唯一性
|
|
if (item.id) {
|
|
if (seenIds.has(item.id)) {
|
|
fail(`${label}: ID 重复`);
|
|
}
|
|
seenIds.add(item.id);
|
|
|
|
// ID 格式
|
|
if (properties.id.pattern && !new RegExp(properties.id.pattern).test(item.id)) {
|
|
fail(`${label}: ID 格式不合法 (应为 ${properties.id.pattern})`);
|
|
}
|
|
}
|
|
|
|
// 枚举校验
|
|
if (item.icon && properties.icon.enum && !properties.icon.enum.includes(item.icon)) {
|
|
fail(`${label}: icon "${item.icon}" 不在允许列表中`);
|
|
}
|
|
if (item.group && properties.group.enum && !properties.group.enum.includes(item.group)) {
|
|
fail(`${label}: group "${item.group}" 不在允许列表中`);
|
|
}
|
|
|
|
// 颜色格式
|
|
if (item.color && !(/^#[0-9a-fA-F]{6}$/).test(item.color)) {
|
|
fail(`${label}: color "${item.color}" 格式不合法`);
|
|
}
|
|
|
|
// 蓝图路径格式 + 文件存在性
|
|
if (item.blueprint) {
|
|
if (!(/^\/blueprints\/[a-z0-9-]+\.json$/).test(item.blueprint)) {
|
|
fail(`${label}: blueprint 路径格式不合法 "${item.blueprint}"`);
|
|
}
|
|
const bpFile = path.join(PLAY_DIR, item.blueprint.slice(1));
|
|
if (!fs.existsSync(bpFile)) {
|
|
fail(`${label}: 蓝图文件不存在 ${bpFile}`);
|
|
} else {
|
|
// 蓝图 JSON 合法性
|
|
try {
|
|
JSON.parse(fs.readFileSync(bpFile, 'utf8'));
|
|
} catch (e) {
|
|
fail(`${label}: 蓝图文件 JSON 解析失败`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 描述长度
|
|
if (item.desc) {
|
|
if (item.desc.length < 10) warn(`${label}: desc 过短 (${item.desc.length} 字符)`);
|
|
if (item.desc.length > 200) warn(`${label}: desc 过长 (${item.desc.length} 字符)`);
|
|
}
|
|
|
|
// repo URL 格式(非空时)
|
|
if (item.repo && item.repo.length > 0) {
|
|
if (!item.repo.startsWith('http://') && !item.repo.startsWith('https://')) {
|
|
fail(`${label}: repo URL 格式不合法`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 3. 蓝图反向检查:有蓝图文件但不在 plugins.json 中 ---
|
|
const bpDir = path.join(PLAY_DIR, 'blueprints');
|
|
if (fs.existsSync(bpDir)) {
|
|
const bpFiles = fs.readdirSync(bpDir).filter(f => f.endsWith('.json'));
|
|
const registeredBps = new Set(plugins.map(p => p.blueprint.replace('/blueprints/', '')));
|
|
for (const f of bpFiles) {
|
|
if (!registeredBps.has(f)) {
|
|
warn(`蓝图文件 ${f} 未在 plugins.json 中注册`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- 汇总 ---
|
|
console.log(`\n=== 结果: ${errors} 错误, ${warnings} 警告 ===`);
|
|
process.exit(errors > 0 ? 1 : 0);
|