Files
memind/mindspace-geo-meta.mjs
john 357e26ab49
Memind CI / Test, build, and release guards (pull_request) Failing after 7m12s
Memind CI / Test, build, and release guards (push) Has been cancelled
feat(seo-geo): 落盘自动补齐与 143 全量回填支持
Finish/交付写盘前自动回填 mindspace-geo,扩展 storage publications 扫描与 geo meta 解析,并增加 143 生产回填脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 10:26:02 +08:00

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('&quot;', '"').replaceAll('&amp;', '&').replaceAll('&lt;', '<');
}
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) === '&quot;') {
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);
}