'use strict'; const fs = require('fs'); const path = require('path'); const vm = require('node:vm'); function stripHtml(value) { return String(value || '') .replace(//gi, '\n') .replace(/<[^>]+>/g, ' ') .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/</g, '<') .replace(/>/g, '>') .replace(/"/g, '"') .replace(/\s+/g, ' ') .trim(); } function extractFaqFromLdJson(html, max = 50) { const re = /]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi; let m; while ((m = re.exec(String(html)))) { try { const data = JSON.parse(m[1].trim()); const candidates = Array.isArray(data) ? data : [data]; for (const item of candidates) { const main = item && (item.mainEntity || (item['@type'] === 'FAQPage' && item.mainEntity)); if (!Array.isArray(main)) continue; const out = main .filter((q) => q && q.name && q.acceptedAnswer && q.acceptedAnswer.text) .map((q) => ({ q: stripHtml(q.name), a: stripHtml(q.acceptedAnswer.text) })) .filter((q) => q.q && q.a) .slice(0, max); if (out.length) return out; } } catch { // 非 JSON-LD 块忽略 } } return []; } // Nuxt SSR 页面(如 dropshipping.yehwang.com)没有 FAQPage JSON-LD, // FAQ 数据内嵌在 window.__NUXT__ 的 payload 里(data[].info.content = [{title, content(HTML)}])。 // 在隔离 sandbox 里求值这段自执行脚本,只取回返回对象,不接触宿主全局。 function extractNuxtPayload(html) { const m = String(html || '').match(/__NUXT__=([\s\S]*?)<\/script>/); if (!m) return null; try { return vm.runInNewContext(m[1].replace(/;\s*$/, ''), Object.create(null), { timeout: 2000 }); } catch { return null; } } function extractFaqFromNuxt(html, max = 50) { const payload = extractNuxtPayload(html); if (!payload || !payload.data) return []; const out = []; const seen = new Set(); const walk = (node) => { if (out.length >= max) return; if (Array.isArray(node)) { for (const item of node) walk(item); return; } if (!node || typeof node !== 'object') return; if (typeof node.title === 'string' && typeof node.content === 'string') { const q = stripHtml(node.title); const a = stripHtml(node.content); if (q && a && !seen.has(q)) { seen.add(q); out.push({ q, a }); } } for (const value of Object.values(node)) { if (value && typeof value === 'object') walk(value); } }; walk(payload.data); return out; } function loadFaqConfig(file) { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return { enabled: false, pages: [] }; } } function createFaqFetcher(config = {}) { const pages = config.pages || []; const timeoutMs = config.timeoutMs || 15000; const max = config.maxFaqsPerPage || 50; async function fetchPage(page) { const res = await fetch(page.url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36', Accept: 'text/html,application/xhtml+xml' }, signal: AbortSignal.timeout(timeoutMs), }); if (!res.ok) throw new Error(`fetch ${page.url} failed: HTTP ${res.status}`); const html = await res.text(); return page.extractor === 'nuxt' ? extractFaqFromNuxt(html, max) : extractFaqFromLdJson(html, max); } async function fetchAll() { const out = []; for (const page of pages) { try { const questions = await fetchPage(page); out.push({ ...page, questions }); } catch (err) { out.push({ ...page, questions: [], error: err.message }); } } return out; } return { fetchPage, fetchAll, pages }; } module.exports = { createFaqFetcher, loadFaqConfig, extractFaqFromLdJson, extractFaqFromNuxt, extractNuxtPayload, stripHtml };