import fs from 'node:fs';
import path from 'node:path';
import { parseMindspaceCoverMeta } from './mindspace-cover-meta.mjs';
import { parseMindspaceGeoMeta, resolveGeoFaq } from './mindspace-geo-meta.mjs';
import { hasPlatformBrandMarker } from './mindspace-page-tag.mjs';
import {
hasMindspaceCoverMeta,
hasRasterShareImageHint,
hasShareDescription,
} from './wechat/verify/share-preview.mjs';
export const AUDIT_SCOPES = ['mindspace', 'platform', 'all'];
const PLATFORM_SKIP_DIRS = new Set(['dev', 'thumbnail-demo', 'thumbnail-demo-samples']);
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;
}
export function auditCoverHtml(htmlPath, html, { requireCoverMeta = true } = {}) {
const issues = [];
if (requireCoverMeta && !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 (requireCoverMeta && !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 (requireCoverMeta && !hasCoverImageHint(html)) {
issues.push({
level: 'warn',
code: 'missing_cover_image',
message: '无 raster 封面图(cover/image/og:image),缩略图会退化为纯色渐变',
});
}
return issues;
}
export function auditSeoHtml(html, { strictGeo = true } = {}) {
const issues = [];
const title = String(html).match(/
]*>([^<]+)<\/title>/i)?.[1]?.replace(/\s+/g, ' ').trim() ?? '';
const weakLatinTitle = title && /^[\x00-\x7F]+$/.test(title) && title.length < 4;
if (!title || title.length < 2 || weakLatinTitle) {
issues.push({ level: 'error', code: 'weak_title', message: '缺少或过短的 ,SEO/GEO 无法达标' });
} else if (/^(页面|我的页面|未命名|demo|test|MindSpace 页面)$/i.test(title)) {
issues.push({ level: 'error', code: 'generic_title', message: `title 过于泛化:${title}` });
}
if (!/,搜索引擎与 AI 抽取质量会下降' });
}
if (!hasShareDescription(html)) {
issues.push({ level: 'error', code: 'seo_missing_description', message: '缺少 description,SEO/GEO 摘要来源不足' });
}
const geoMeta = parseMindspaceGeoMeta(html);
if (strictGeo && !/]*name=["']mindspace-geo["']/i.test(html)) {
issues.push({
level: 'error',
code: 'missing_mindspace_geo',
message: '缺少 ,GEO 摘要/FAQ 无法达标',
});
} else if (strictGeo && !String(geoMeta.summary ?? '').trim()) {
issues.push({
level: 'warn',
code: 'geo_missing_summary',
message: 'mindspace-geo 缺少 summary,建议写可被 AI 直接引用的摘要',
});
}
const h2Count = (String(html).match(/ 分节,便于 SEO/GEO 抽取',
});
}
const faq = resolveGeoFaq(html, geoMeta);
const parsedGeo = parseMindspaceGeoMeta(html);
const declaredFaq = Array.isArray(parsedGeo.faq) ? parsedGeo.faq.length : 0;
if (faq.length < 1 && declaredFaq < 1) {
issues.push({
level: 'warn',
code: 'geo_missing_faq',
message: '缺少 FAQ(mindspace-geo.faq 或 h2+p 结构),GEO 结构化数据会偏弱',
});
}
return issues;
}
export function auditHtmlFile(htmlPath, html, options = {}) {
const requireCoverMeta = options.requireCoverMeta !== false;
return [
...auditCoverHtml(htmlPath, html, { requireCoverMeta }),
...(options.seo === false ? [] : auditSeoHtml(html, options)),
];
}
function walkHtmlFiles(dir, files = []) {
if (!fs.existsSync(dir)) return files;
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;
walkHtmlFiles(full, files);
continue;
}
if (!entry.name.endsWith('.html') || entry.name.endsWith('.thumbnail.svg')) continue;
files.push(full);
}
return files;
}
export function collectMindspaceHtmlFiles(root, userId = null) {
if (userId) {
return walkHtmlFiles(path.join(root, userId, 'public'));
}
const 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;
walkHtmlFiles(path.join(root, entry.name, 'public'), files);
}
return files;
}
export function collectStoragePublicationHtmlFiles(storageRoot, userId = null) {
const files = [];
const usersDir = path.join(storageRoot, 'users');
if (!fs.existsSync(usersDir)) return files;
const userDirs = userId
? [path.join(usersDir, userId)]
: fs.readdirSync(usersDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
.map((entry) => path.join(usersDir, entry.name));
for (const userDir of userDirs) {
const publicationsDir = path.join(userDir, 'publications');
if (!fs.existsSync(publicationsDir)) continue;
for (const publication of fs.readdirSync(publicationsDir, { withFileTypes: true })) {
if (!publication.isDirectory() || publication.name.startsWith('.')) continue;
const indexHtml = path.join(publicationsDir, publication.name, 'index.html');
if (fs.existsSync(indexHtml)) files.push(indexHtml);
}
}
return files;
}
export function collectPlatformHtmlFiles(publicRoot) {
const files = [];
if (!fs.existsSync(publicRoot)) return files;
for (const entry of fs.readdirSync(publicRoot, { withFileTypes: true })) {
const full = path.join(publicRoot, entry.name);
if (entry.isDirectory()) {
if (PLATFORM_SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue;
walkHtmlFiles(full, files);
continue;
}
if (entry.name.endsWith('.html')) files.push(full);
}
return files;
}
export function collectHtmlTargets({
scope = 'mindspace',
mindspaceRoot,
publicRoot,
storageRoot = null,
userId = null,
file = null,
} = {}) {
if (file) return [path.resolve(file)];
const normalized = String(scope ?? 'mindspace').trim().toLowerCase();
const storageFiles = storageRoot
? collectStoragePublicationHtmlFiles(storageRoot, userId)
: [];
if (normalized === 'platform') {
return collectPlatformHtmlFiles(publicRoot);
}
if (normalized === 'storage') {
return storageFiles;
}
if (normalized === 'all') {
return [
...collectMindspaceHtmlFiles(mindspaceRoot, userId),
...storageFiles,
...collectPlatformHtmlFiles(publicRoot),
];
}
return collectMindspaceHtmlFiles(mindspaceRoot, userId);
}