#!/usr/bin/env node /** * Scan MindSpace public/*.html for mindspace-cover compliance (scheme A gate). * * Usage: * node scripts/check-mindspace-cover.mjs * node scripts/check-mindspace-cover.mjs --user * node scripts/check-mindspace-cover.mjs --root /path/to/MindSpace * node scripts/check-mindspace-cover.mjs --file public/thumbnail-demo-samples/vietnam-guide-good.html */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseMindspaceCoverMeta } from '../mindspace-cover-meta.mjs'; import { hasPlatformBrandMarker } from '../mindspace-page-tag.mjs'; import { hasMindspaceCoverMeta, hasRasterShareImageHint, hasShareDescription, } from '../wechat/verify/share-preview.mjs'; const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); const DEFAULT_PUBLISH_ROOT = 'MindSpace'; function parseArgs(argv) { let root = path.join(repoRoot, DEFAULT_PUBLISH_ROOT); let userId = null; let file = null; for (let i = 2; i < argv.length; i += 1) { if (argv[i] === '--root' && argv[i + 1]) { root = path.resolve(argv[i + 1]); i += 1; } else if (argv[i] === '--user' && argv[i + 1]) { userId = argv[i + 1]; i += 1; } else if (argv[i] === '--file' && argv[i + 1]) { file = path.resolve(argv[i + 1]); i += 1; } else if (argv[i] === '--help' || argv[i] === '-h') { console.log(`Usage: node scripts/check-mindspace-cover.mjs [--root ${DEFAULT_PUBLISH_ROOT}] [--user ] [--file ]`); process.exit(0); } } return { root, userId, file }; } function hasCoverImageHint(html) { if (hasRasterShareImageHint(html)) return true; const coverMeta = parseMindspaceCoverMeta(html); const cover = String(coverMeta?.cover ?? coverMeta?.image ?? '').trim(); if (!cover) return false; if (/^https?:\/\//i.test(cover)) return true; if (/\.svg(?:[?#]|$)/i.test(cover)) return false; return true; } function missingLocalCoverPath(htmlPath, coverMeta) { const cover = String(coverMeta?.cover ?? coverMeta?.image ?? '').trim(); if (!cover || /^https?:\/\//i.test(cover) || cover.startsWith('data:')) return null; const clean = cover.split('?')[0].split('#')[0].replace(/^\.\//, ''); if (!clean || clean.startsWith('/') || clean.includes('\\')) return null; const base = path.dirname(htmlPath); const target = path.resolve(base, clean); if (target !== base && !target.startsWith(`${base}${path.sep}`)) return clean; return fs.existsSync(target) && fs.statSync(target).isFile() ? null : clean; } function auditCoverHtml(htmlPath, html) { const issues = []; if (!hasMindspaceCoverMeta(html)) { issues.push({ level: 'error', code: 'missing_mindspace_cover', message: '缺少 ' }); } if (!hasShareDescription(html)) { issues.push({ level: 'error', code: 'missing_description', message: '缺少 description / og:description' }); } if (!hasPlatformBrandMarker(html)) { issues.push({ level: 'warn', code: 'missing_platform_brand', message: '缺少 data-mindspace-page-tag="platform-brand" 页脚' }); } const coverMeta = parseMindspaceCoverMeta(html); if (coverMeta) { if (!String(coverMeta.tag ?? '').trim()) { issues.push({ level: 'warn', code: 'missing_tag', message: 'mindspace-cover 缺少 tag' }); } if (!String(coverMeta.subtitle ?? '').trim()) { issues.push({ level: 'warn', code: 'missing_subtitle', message: 'mindspace-cover 缺少 subtitle' }); } if (!String(coverMeta.accent ?? '').trim()) { issues.push({ level: 'warn', code: 'missing_accent', message: 'mindspace-cover 缺少 accent' }); } const missingCover = missingLocalCoverPath(htmlPath, coverMeta); if (missingCover) { issues.push({ level: 'error', code: 'missing_cover_file', message: `mindspace-cover 引用的本地文件不存在:${missingCover}`, }); } } if (!hasCoverImageHint(html)) { issues.push({ level: 'warn', code: 'missing_cover_image', message: '无 raster 封面图(cover/image/og:image),缩略图会退化为纯色渐变', }); } return issues; } function collectHtmlFiles(root, userId) { const files = []; const walk = (dir) => { if (!fs.existsSync(dir)) return; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { if (entry.name === '.agents' || entry.name === 'node_modules' || entry.name.startsWith('.')) continue; walk(full); continue; } if (!entry.name.endsWith('.html') || entry.name.endsWith('.thumbnail.svg')) continue; files.push(full); } }; if (userId) { walk(path.join(root, userId, 'public')); return files; } if (!fs.existsSync(root)) return files; for (const entry of fs.readdirSync(root, { withFileTypes: true })) { if (!entry.isDirectory() || entry.name === 'wiki' || entry.name.startsWith('.')) continue; walk(path.join(root, entry.name, 'public')); } return files; } const { root, userId, file } = parseArgs(process.argv); const targets = file ? [file] : collectHtmlFiles(root, userId); const results = []; for (const htmlPath of targets) { const html = fs.readFileSync(htmlPath, 'utf8'); const issues = auditCoverHtml(htmlPath, html); if (issues.length) { results.push({ htmlPath, issues }); } } if (results.length === 0) { const scope = file ?? `${root}${userId ? ` (user ${userId})` : ''}`; console.log(`OK: ${targets.length} 个 HTML 页面均符合 mindspace-cover 规范 (${scope})`); process.exit(0); } let errors = 0; let warns = 0; console.error(`Found cover issues in ${results.length} / ${targets.length} HTML files:`); for (const { htmlPath, issues } of results) { const rel = file ? htmlPath : path.relative(root, htmlPath); console.error(`\n${rel}`); for (const issue of issues) { const prefix = issue.level === 'error' ? ' ✗' : ' ⚠'; console.error(`${prefix} [${issue.code}] ${issue.message}`); if (issue.level === 'error') errors += 1; else warns += 1; } } console.error(`\n合计: ${errors} 错误, ${warns} 警告`); process.exit(errors > 0 ? 1 : 0);