feat(kb): 新增一件代发 FAQ 知识库
This commit is contained in:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user