357e26ab49
Finish/交付写盘前自动回填 mindspace-geo,扩展 storage publications 扫描与 geo meta 解析,并增加 143 生产回填脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
361 lines
13 KiB
JavaScript
361 lines
13 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { injectBeforeDocumentClosingBody } from './html-document-injection.mjs';
|
|
import { parseMindspaceCoverMeta, upsertMindspaceCoverMeta } from './mindspace-cover-meta.mjs';
|
|
import {
|
|
extractFaqFromHeadings,
|
|
parseMindspaceGeoMeta,
|
|
resolveGeoFaq,
|
|
resolveGeoKeywords,
|
|
} from './mindspace-geo-meta.mjs';
|
|
|
|
function htmlWithoutEmbeddedCode(html) {
|
|
return String(html ?? '')
|
|
.replace(/<script\b[\s\S]*?<\/script>/gi, ' ')
|
|
.replace(/<style\b[\s\S]*?<\/style>/gi, ' ');
|
|
}
|
|
import { ensurePlatformBrandFooter, injectPlatformBrandVisibilityStyle } from './mindspace-page-tag.mjs';
|
|
import { PLATFORM_BRAND_ICON_PATH } from './mindspace-og-tags.mjs';
|
|
import { extractCoverSignals } from './mindspace-thumbnails.mjs';
|
|
import { extractSeoDescription } from './mindspace-seo-tags.mjs';
|
|
import { hasRasterShareImageHint } from './wechat/verify/share-preview.mjs';
|
|
|
|
export const DEFAULT_RASTER_COVER_URL = 'https://m.tkmind.cn/brand/tkmind-icon.png';
|
|
|
|
function escapeMetaAttribute(value) {
|
|
return String(value ?? '')
|
|
.replaceAll('&', '&')
|
|
.replaceAll('"', '"')
|
|
.replaceAll('<', '<');
|
|
}
|
|
|
|
function stripHtml(value) {
|
|
return String(value ?? '')
|
|
.replace(/<[^>]+>/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
function rawTitleFromHtml(html) {
|
|
return String(html ?? '').match(/<title[^>]*>([^<]*)<\/title>/i)?.[1]?.trim() ?? '';
|
|
}
|
|
|
|
function rawH1FromHtml(html) {
|
|
const match = String(html ?? '').match(/<h1[^>]*>([\s\S]*?)<\/h1>/i);
|
|
return match ? stripHtml(match[1]) : '';
|
|
}
|
|
|
|
function rawFirstParagraph(html) {
|
|
const match = String(html ?? '').match(/<p[^>]*>([\s\S]*?)<\/p>/i);
|
|
return match ? stripHtml(match[1]) : '';
|
|
}
|
|
|
|
function hasMeta(html, name) {
|
|
return new RegExp(`<meta[^>]+name=["']${name}["']`, 'i').test(String(html ?? ''));
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
function countH2(html) {
|
|
return (String(html ?? '').match(/<h2\b/gi) ?? []).length;
|
|
}
|
|
|
|
function hasShareDescription(html) {
|
|
return Boolean(extractSeoDescription(html));
|
|
}
|
|
|
|
export function repairMindspaceGeoMeta(html) {
|
|
const source = String(html ?? '');
|
|
const parsed = parseMindspaceGeoMeta(source);
|
|
const hasValidSummary = String(parsed.summary ?? '').trim().length > 0;
|
|
const hasTag = /<meta[^>]*name=["']mindspace-geo["']/i.test(source);
|
|
if (hasTag && hasValidSummary) return source;
|
|
return upsertMindspaceGeoMeta(source, buildMindspaceGeoPayload(source), { forceReplace: true });
|
|
}
|
|
|
|
export function buildMindspaceGeoPayload(html) {
|
|
const source = htmlWithoutEmbeddedCode(html);
|
|
const title = rawH1FromHtml(source) || rawTitleFromHtml(source) || 'TKMind 页面';
|
|
const cover = extractCoverSignals(source);
|
|
const summary =
|
|
String(parseMindspaceGeoMeta(source).summary ?? '').trim() ||
|
|
extractSeoDescription(source) ||
|
|
String(cover.subtitle ?? '').trim() ||
|
|
rawFirstParagraph(source) ||
|
|
title;
|
|
|
|
let keywords = resolveGeoKeywords(source);
|
|
if (keywords.length < 2) {
|
|
keywords = [title, cover.tag, 'TKMind'].map((item) => String(item ?? '').trim()).filter(Boolean);
|
|
}
|
|
keywords = [...new Set(keywords)].slice(0, 8);
|
|
|
|
let faq = resolveGeoFaq(source);
|
|
if (faq.length < 1) {
|
|
faq = extractFaqFromHeadings(source);
|
|
}
|
|
if (faq.length < 1 && summary) {
|
|
faq = [{ q: `${title}讲什么?`, a: summary.slice(0, 220) }];
|
|
}
|
|
|
|
return {
|
|
summary: summary.slice(0, 320),
|
|
keywords,
|
|
faq: faq.slice(0, 6),
|
|
};
|
|
}
|
|
|
|
export function upsertMindspaceGeoMeta(html, geoPayload, { forceReplace = false } = {}) {
|
|
const payload = forceReplace
|
|
? { ...(geoPayload ?? {}) }
|
|
: {
|
|
...buildMindspaceGeoPayload(html),
|
|
...(geoPayload && typeof geoPayload === 'object' ? geoPayload : {}),
|
|
};
|
|
payload.summary = String(payload.summary ?? '').trim().slice(0, 320);
|
|
payload.keywords = Array.isArray(payload.keywords)
|
|
? payload.keywords.map((item) => String(item ?? '').trim()).filter(Boolean).slice(0, 12)
|
|
: [];
|
|
payload.faq = Array.isArray(payload.faq)
|
|
? payload.faq
|
|
.map((item) => ({
|
|
q: String(item?.q ?? '').trim().slice(0, 120),
|
|
a: String(item?.a ?? '').trim().slice(0, 500),
|
|
}))
|
|
.filter((item) => item.q && item.a && item.a.length >= 4)
|
|
.slice(0, 6)
|
|
: [];
|
|
if (payload.faq.length < 1 && payload.summary) {
|
|
const topic = String(rawH1FromHtml(html) || rawTitleFromHtml(html) || payload.summary).trim();
|
|
payload.faq = [{ q: `${topic}讲什么?`, a: payload.summary.slice(0, 220) }];
|
|
}
|
|
const metaTag = `<meta name="mindspace-geo" content="${escapeMetaAttribute(JSON.stringify(payload))}">`;
|
|
const source = String(html ?? '');
|
|
|
|
if (hasMeta(source, 'mindspace-geo')) {
|
|
return source.replace(/<meta[^>]*name=["']mindspace-geo["'][^>]*>\s*/i, `${metaTag.trim()}\n `);
|
|
}
|
|
if (/<meta[^>]*name=["']mindspace-cover["'][^>]*>/i.test(source)) {
|
|
return source.replace(
|
|
/(<meta[^>]*name=["']mindspace-cover["'][^>]*>)/i,
|
|
`$1\n ${metaTag.trim()}`,
|
|
);
|
|
}
|
|
if (/<meta[^>]*name=["']description["'][^>]*>/i.test(source)) {
|
|
return source.replace(
|
|
/(<meta[^>]*name=["']description["'][^>]*>)/i,
|
|
`$1\n ${metaTag.trim()}`,
|
|
);
|
|
}
|
|
if (/<head[^>]*>/i.test(source)) {
|
|
return source.replace(/<head([^>]*)>/i, `<head$1>\n ${metaTag.trim()}`);
|
|
}
|
|
return `${metaTag.trim()}\n${source}`;
|
|
}
|
|
|
|
export function upsertDescriptionMeta(html, description) {
|
|
const text = String(description ?? '').trim();
|
|
if (!text || hasMeta(html, 'description')) return String(html ?? '');
|
|
const metaTag = `<meta name="description" content="${escapeMetaAttribute(text)}">`;
|
|
const source = String(html ?? '');
|
|
if (/<title[^>]*>/i.test(source)) {
|
|
return source.replace(/(<title[^>]*>[^<]*<\/title>)/i, `$1\n ${metaTag}`);
|
|
}
|
|
if (/<head[^>]*>/i.test(source)) {
|
|
return source.replace(/<head([^>]*)>/i, `<head$1>\n ${metaTag}`);
|
|
}
|
|
return `${metaTag}\n${source}`;
|
|
}
|
|
|
|
export function buildMinimalCoverMeta(html, { title = '' } = {}) {
|
|
const cover = parseMindspaceCoverMeta(html);
|
|
if (cover) return cover;
|
|
const signals = extractCoverSignals(String(html ?? ''));
|
|
const resolvedTitle = title || rawH1FromHtml(html) || rawTitleFromHtml(html) || 'TKMind 页面';
|
|
const description = extractSeoDescription(html) || rawFirstParagraph(html) || resolvedTitle;
|
|
return {
|
|
tag: signals.tag || '报告',
|
|
emoji: '📄',
|
|
accent: signals.accent || '#3b82f6',
|
|
accent2: signals.accent2 || '#1e293b',
|
|
subtitle: signals.subtitle || description.slice(0, 80),
|
|
};
|
|
}
|
|
|
|
function humanizeFilename(filePath) {
|
|
const base = path.basename(String(filePath ?? ''), '.html');
|
|
const decoded = decodeURIComponent(base);
|
|
return decoded
|
|
.replace(/[-_]+/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
function isWeakTitle(title) {
|
|
const value = String(title ?? '').trim();
|
|
if (!value || value.length < 2) return true;
|
|
if (/^(页面|我的页面|未命名|demo|test|index|MindSpace 页面)$/i.test(value)) return true;
|
|
if (/^[\x00-\x7F]+$/.test(value) && value.length < 4) return true;
|
|
return false;
|
|
}
|
|
|
|
function buildFallbackTitle(html, htmlFilePath = '') {
|
|
const cover = parseMindspaceCoverMeta(html);
|
|
const fromCover = [cover?.tag, cover?.subtitle]
|
|
.map((item) => String(item ?? '').trim())
|
|
.filter(Boolean)
|
|
.join(' · ');
|
|
if (fromCover && !isWeakTitle(fromCover)) return fromCover;
|
|
const fromH1 = rawH1FromHtml(html);
|
|
if (fromH1 && !isWeakTitle(fromH1)) return fromH1;
|
|
const fromFile = humanizeFilename(htmlFilePath);
|
|
if (fromFile && !isWeakTitle(fromFile)) return fromFile;
|
|
if (fromFile) return `TKMind · ${fromFile}`;
|
|
return 'TKMind 精选页面';
|
|
}
|
|
|
|
export function improveTitleFromContext(html, htmlFilePath = '') {
|
|
const source = String(html ?? '');
|
|
const current = rawTitleFromHtml(source);
|
|
if (!isWeakTitle(current)) return source;
|
|
const candidate =
|
|
[rawH1FromHtml(source), parseMindspaceCoverMeta(source)?.subtitle, buildFallbackTitle(source, htmlFilePath)]
|
|
.map((item) => String(item ?? '').trim())
|
|
.find((item) => item && !isWeakTitle(item)) ?? '';
|
|
if (!candidate) return source;
|
|
if (/<title[^>]*>[^<]*<\/title>/i.test(source)) {
|
|
return source.replace(/<title[^>]*>[^<]*<\/title>/i, `<title>${candidate}</title>`);
|
|
}
|
|
if (/<head[^>]*>/i.test(source)) {
|
|
return source.replace(/<head([^>]*)>/i, `<head$1>\n <title>${candidate}</title>`);
|
|
}
|
|
return source;
|
|
}
|
|
|
|
export function ensureH1FromTitle(html) {
|
|
const source = String(html ?? '');
|
|
if (/<h1\b/i.test(source)) return source;
|
|
const title = rawTitleFromHtml(source);
|
|
if (!title) return source;
|
|
if (/<body[^>]*>/i.test(source)) {
|
|
return source.replace(/<body([^>]*)>/i, `<body$1>\n <h1>${title}</h1>`);
|
|
}
|
|
return `${source}\n<h1>${title}</h1>`;
|
|
}
|
|
|
|
export function fixBrokenLocalCover(html, htmlFilePath = '') {
|
|
const coverMeta = parseMindspaceCoverMeta(html);
|
|
if (!coverMeta || !htmlFilePath) return String(html ?? '');
|
|
const cover = String(coverMeta.cover ?? coverMeta.image ?? '').trim();
|
|
if (!cover || /^https?:\/\//i.test(cover) || cover.startsWith('data:')) {
|
|
return String(html ?? '');
|
|
}
|
|
const clean = cover.split('?')[0].split('#')[0].replace(/^\.\//, '');
|
|
const base = path.dirname(htmlFilePath);
|
|
const target = path.resolve(base, clean);
|
|
if (target.startsWith(`${base}${path.sep}`) && fs.existsSync(target)) {
|
|
return String(html ?? '');
|
|
}
|
|
const nextMeta = { ...coverMeta };
|
|
delete nextMeta.cover;
|
|
delete nextMeta.image;
|
|
return upsertMindspaceCoverMeta(html, nextMeta, { replace: true });
|
|
}
|
|
|
|
export function ensureDefaultRasterCover(html, { coverUrl = DEFAULT_RASTER_COVER_URL } = {}) {
|
|
const source = String(html ?? '');
|
|
if (hasRasterShareImageHint(source)) return source;
|
|
const coverMeta = parseMindspaceCoverMeta(source) ?? buildMinimalCoverMeta(source);
|
|
return upsertMindspaceCoverMeta(source, {
|
|
...coverMeta,
|
|
cover: coverUrl || DEFAULT_RASTER_COVER_URL,
|
|
});
|
|
}
|
|
|
|
export function buildSeoOutlineSection(html) {
|
|
const geo = parseMindspaceGeoMeta(html);
|
|
const title = rawH1FromHtml(html) || rawTitleFromHtml(html) || '本页';
|
|
const summary = String(geo.summary ?? extractSeoDescription(html) ?? title).trim();
|
|
const faq = resolveGeoFaq(html, geo);
|
|
const blocks = [];
|
|
if (summary) {
|
|
blocks.push(
|
|
`<h2 style="font-size:17px;color:#334155;margin:0 0 8px">内容概要</h2>`,
|
|
`<p style="margin:0 0 16px">${escapeHtml(summary)}</p>`,
|
|
);
|
|
}
|
|
for (const item of faq.slice(0, 2)) {
|
|
blocks.push(
|
|
`<h2 style="font-size:17px;color:#334155;margin:16px 0 8px">${escapeHtml(item.q)}</h2>`,
|
|
`<p style="margin:0 0 12px">${escapeHtml(item.a)}</p>`,
|
|
);
|
|
}
|
|
if (countH2(blocks.join('\n')) < 2) {
|
|
blocks.push(
|
|
`<h2 style="font-size:17px;color:#334155;margin:16px 0 8px">关于${escapeHtml(title)}</h2>`,
|
|
`<p style="margin:0 0 12px">${escapeHtml(summary || `${title} 由 TKMind 生成,可在浏览器中直接阅读与分享。`)}</p>`,
|
|
);
|
|
}
|
|
if (blocks.length === 0) return '';
|
|
return `<section data-mindspace-seo-outline="1" aria-label="页面提纲" style="margin:32px auto 20px;padding:20px 16px;max-width:920px;border-top:1px solid rgba(15,23,42,.08);color:#64748b;font-size:14px;line-height:1.75">${blocks.join('\n ')}</section>`;
|
|
}
|
|
|
|
export function ensureSeoHeadingStructure(html) {
|
|
const source = String(html ?? '');
|
|
if (countH2(source) >= 2 || /data-mindspace-seo-outline=/i.test(source)) return source;
|
|
const outline = buildSeoOutlineSection(source);
|
|
if (!outline) return source;
|
|
return injectBeforeDocumentClosingBody(source, `\n${outline}\n`);
|
|
}
|
|
|
|
export function ensureSeoGeoWarningsFixed(html, options = {}) {
|
|
let next = String(html ?? '');
|
|
if (options.fixPlatformBrand !== false) {
|
|
next = injectPlatformBrandVisibilityStyle(ensurePlatformBrandFooter(next));
|
|
}
|
|
if (options.fixRasterCover !== false) {
|
|
next = ensureDefaultRasterCover(next, { coverUrl: options.coverUrl });
|
|
}
|
|
if (options.fixHeadingStructure !== false) {
|
|
next = ensureSeoHeadingStructure(next);
|
|
}
|
|
return next;
|
|
}
|
|
|
|
export function backfillMindspaceSeoGeoHtml(html, options = {}) {
|
|
const htmlFilePath = String(options.htmlFilePath ?? '');
|
|
let next = String(html ?? '');
|
|
next = improveTitleFromContext(next, htmlFilePath);
|
|
next = fixBrokenLocalCover(next, htmlFilePath);
|
|
next = ensureH1FromTitle(next);
|
|
|
|
const title = rawH1FromHtml(next) || rawTitleFromHtml(next);
|
|
let geoPayload = buildMindspaceGeoPayload(next);
|
|
|
|
if (!parseMindspaceCoverMeta(next) && options.ensureCover !== false) {
|
|
next = upsertMindspaceCoverMeta(next, buildMinimalCoverMeta(next, { title }));
|
|
}
|
|
next = upsertDescriptionMeta(next, geoPayload.summary);
|
|
const existingGeo = parseMindspaceGeoMeta(next);
|
|
const needsGeoUpsert =
|
|
!hasMeta(next, 'mindspace-geo') ||
|
|
options.forceGeo ||
|
|
!String(existingGeo.summary ?? '').trim();
|
|
if (needsGeoUpsert) {
|
|
next = upsertMindspaceGeoMeta(next, geoPayload, {
|
|
forceReplace: options.forceGeo || !String(existingGeo.summary ?? '').trim(),
|
|
});
|
|
}
|
|
next = repairMindspaceGeoMeta(next);
|
|
if (options.fixWarnings !== false) {
|
|
next = ensureSeoGeoWarningsFixed(next, options);
|
|
}
|
|
return next;
|
|
}
|