357e26ab49
Finish/交付写盘前自动回填 mindspace-geo,扩展 storage publications 扫描与 geo meta 解析,并增加 143 生产回填脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
147 lines
4.1 KiB
JavaScript
147 lines
4.1 KiB
JavaScript
import { extractCoverSignals } from './mindspace-thumbnails.mjs';
|
|
|
|
function stripHtml(value) {
|
|
return String(value ?? '')
|
|
.replace(/<[^>]+>/g, ' ')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
}
|
|
|
|
function htmlWithoutEmbeddedCode(html) {
|
|
return String(html ?? '')
|
|
.replace(/<script\b[\s\S]*?<\/script>/gi, ' ')
|
|
.replace(/<style\b[\s\S]*?<\/style>/gi, ' ');
|
|
}
|
|
|
|
function sanitizeFaqItem(item) {
|
|
const q = stripHtml(item?.q ?? item?.question).slice(0, 120);
|
|
const a = stripHtml(item?.a ?? item?.answer).slice(0, 500);
|
|
if (!q || !a || q.length < 2 || a.length < 4) return null;
|
|
if (/document\.|function\s*\(|createElement|innerHTML|querySelector/i.test(`${q} ${a}`)) return null;
|
|
return { q, a };
|
|
}
|
|
|
|
function decodeMetaAttribute(value) {
|
|
return String(value ?? '').replaceAll('"', '"').replaceAll('&', '&').replaceAll('<', '<');
|
|
}
|
|
|
|
function extractGeoMetaJsonRaw(html) {
|
|
const source = String(html ?? '');
|
|
const marker = source.match(/<meta[^>]*name=["']mindspace-geo["'][^>]*content\s*=\s*/i);
|
|
if (!marker) return null;
|
|
let rest = source.slice(marker.index + marker[0].length);
|
|
if (rest[0] === '"') {
|
|
let raw = '';
|
|
for (let i = 1; i < rest.length; i += 1) {
|
|
if (rest[i] === '&' && rest.slice(i, i + 6) === '"') {
|
|
raw += '"';
|
|
i += 5;
|
|
continue;
|
|
}
|
|
if (rest[i] === '"') return raw;
|
|
raw += rest[i];
|
|
}
|
|
return null;
|
|
}
|
|
const jsonStart = rest.indexOf('{');
|
|
if (jsonStart < 0) return null;
|
|
let depth = 0;
|
|
let inString = false;
|
|
let escape = false;
|
|
for (let i = jsonStart; i < rest.length; i += 1) {
|
|
const ch = rest[i];
|
|
if (inString) {
|
|
if (escape) {
|
|
escape = false;
|
|
continue;
|
|
}
|
|
if (ch === '\\') {
|
|
escape = true;
|
|
continue;
|
|
}
|
|
if (ch === '"') inString = false;
|
|
continue;
|
|
}
|
|
if (ch === '"') {
|
|
inString = true;
|
|
continue;
|
|
}
|
|
if (ch === '{') depth += 1;
|
|
else if (ch === '}') {
|
|
depth -= 1;
|
|
if (depth === 0) return rest.slice(jsonStart, i + 1);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function parseMindspaceGeoMeta(html) {
|
|
const raw = extractGeoMetaJsonRaw(html);
|
|
if (!raw) return {};
|
|
try {
|
|
return JSON.parse(decodeMetaAttribute(raw));
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
export function extractFaqFromHeadings(html, { maxItems = 6 } = {}) {
|
|
const source = htmlWithoutEmbeddedCode(html);
|
|
const faq = [];
|
|
const seen = new Set();
|
|
const pattern =
|
|
/<(h[23])[^>]*>([\s\S]*?)<\/\1>\s*(?:<p[^>]*>([\s\S]*?)<\/p>|<div[^>]*>([\s\S]*?)<\/div>)/gi;
|
|
for (const match of source.matchAll(pattern)) {
|
|
const item = sanitizeFaqItem({
|
|
q: match[2],
|
|
a: match[3] ?? match[4],
|
|
});
|
|
if (!item) continue;
|
|
const key = item.q.toLowerCase();
|
|
if (seen.has(key)) continue;
|
|
seen.add(key);
|
|
faq.push(item);
|
|
if (faq.length >= maxItems) break;
|
|
}
|
|
return faq;
|
|
}
|
|
|
|
export function resolveGeoKeywords(html, authorMeta = null) {
|
|
const meta = authorMeta ?? parseMindspaceGeoMeta(html);
|
|
const fromAuthor = Array.isArray(meta.keywords)
|
|
? meta.keywords.map((item) => String(item ?? '').trim()).filter(Boolean)
|
|
: [];
|
|
if (fromAuthor.length > 0) return fromAuthor.slice(0, 12);
|
|
|
|
const keywords = [];
|
|
const seen = new Set();
|
|
const push = (value) => {
|
|
const token = String(value ?? '').trim();
|
|
if (!token || token.length < 2 || seen.has(token)) return;
|
|
seen.add(token);
|
|
keywords.push(token);
|
|
};
|
|
|
|
const cover = extractCoverSignals(String(html ?? ''));
|
|
push(cover.tag);
|
|
push(cover.subtitle);
|
|
|
|
const title = String(html ?? '').match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]?.trim() ?? '';
|
|
if (title) push(title);
|
|
|
|
for (const match of String(html ?? '').matchAll(/<h2[^>]*>([\s\S]*?)<\/h2>/gi)) {
|
|
push(stripHtml(match[1]));
|
|
if (keywords.length >= 12) break;
|
|
}
|
|
return keywords.slice(0, 12);
|
|
}
|
|
|
|
export function resolveGeoFaq(html, authorMeta = null) {
|
|
const meta = authorMeta ?? parseMindspaceGeoMeta(html);
|
|
const fromAuthor = Array.isArray(meta.faq)
|
|
? meta.faq.map((item) => sanitizeFaqItem(item)).filter(Boolean)
|
|
: [];
|
|
if (fromAuthor.length > 0) return fromAuthor.slice(0, 8);
|
|
return extractFaqFromHeadings(html);
|
|
}
|