feat(kb): 新增一件代发 FAQ 知识库

This commit is contained in:
wangliang
2026-10-23 15:11:01 +08:00
parent 8ae8d2b468
commit dfc3b497f5
8 changed files with 300 additions and 15 deletions
+49 -6
View File
@@ -2,6 +2,7 @@
const fs = require('fs');
const path = require('path');
const vm = require('node:vm');
function stripHtml(value) {
return String(value || '')
@@ -40,6 +41,47 @@ function extractFaqFromLdJson(html, max = 50) {
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'));
@@ -53,20 +95,21 @@ function createFaqFetcher(config = {}) {
const timeoutMs = config.timeoutMs || 15000;
const max = config.maxFaqsPerPage || 50;
async function fetchPage(url) {
const res = await fetch(url, {
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 ${url} failed: HTTP ${res.status}`);
return extractFaqFromLdJson(await res.text(), max);
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.url);
const questions = await fetchPage(page);
out.push({ ...page, questions });
} catch (err) {
out.push({ ...page, questions: [], error: err.message });
@@ -78,4 +121,4 @@ function createFaqFetcher(config = {}) {
return { fetchPage, fetchAll, pages };
}
module.exports = { createFaqFetcher, loadFaqConfig, extractFaqFromLdJson, stripHtml };
module.exports = { createFaqFetcher, loadFaqConfig, extractFaqFromLdJson, extractFaqFromNuxt, extractNuxtPayload, stripHtml };