fde6503bdf
Memind CI / Test, build, and release guards (push) Has been cancelled
Enable optional SEO/GEO injection and discovery routes for confirmed public pages while keeping private pages noindex. Add premium page template skills, portal catalog API, template shop UI, and Baidu push gated by memind_adm config. Co-authored-by: Cursor <cursoragent@cursor.com>
192 lines
7.1 KiB
JavaScript
192 lines
7.1 KiB
JavaScript
#!/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 <uuid>
|
||
* 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;
|
||
let seo = false;
|
||
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] === '--seo') {
|
||
seo = true;
|
||
} else if (argv[i] === '--help' || argv[i] === '-h') {
|
||
console.log(`Usage: node scripts/check-mindspace-cover.mjs [--root ${DEFAULT_PUBLISH_ROOT}] [--user <uuid>] [--file <html>] [--seo]`);
|
||
process.exit(0);
|
||
}
|
||
}
|
||
return { root, userId, file, seo };
|
||
}
|
||
|
||
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: '缺少 <meta name="mindspace-cover">' });
|
||
}
|
||
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 auditSeoHtml(html) {
|
||
const issues = [];
|
||
const title = String(html).match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]?.replace(/\s+/g, ' ').trim() ?? '';
|
||
if (!title || title.length < 4) {
|
||
issues.push({ level: 'warn', code: 'weak_title', message: '缺少或过短的 <title>,SEO/GEO 质量会下降' });
|
||
} else if (/^(页面|我的页面|未命名|demo|test|MindSpace 页面)$/i.test(title)) {
|
||
issues.push({ level: 'warn', code: 'generic_title', message: `title 过于泛化:${title}` });
|
||
}
|
||
if (!/<h1\b/i.test(html)) {
|
||
issues.push({ level: 'warn', code: 'missing_h1', message: '缺少 <h1>,搜索引擎与 AI 抽取质量会下降' });
|
||
}
|
||
if (!hasShareDescription(html)) {
|
||
issues.push({ level: 'warn', code: 'seo_missing_description', message: '缺少 description,SEO/GEO 摘要来源不足' });
|
||
}
|
||
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, seo } = 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 (seo) issues.push(...auditSeoHtml(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);
|