/** * JSON-LD 提取器(schema.org/Product) * * Ozon 是 SSR 站点,商品页 HTML 里带 application/ld+json, * 是 DOM 之外最稳定的结构化来源(比哈希类名稳定一个数量级)。 * * 参考实现(毛子ERP)也解析 application/ld+json 取 description / offers.url。 */ export interface JsonLdProduct { title?: string; description?: string; brand?: string; sku?: string; price?: string; currency?: string; images: string[]; rating?: string; reviewCount?: string; } function asString(v: unknown): string | undefined { if (typeof v === 'string') return v; if (typeof v === 'number') return String(v); return undefined; } function findProduct(node: unknown): any | null { if (Array.isArray(node)) { for (const item of node) { const r = findProduct(item); if (r) return r; } return null; } if (!node || typeof node !== 'object') return null; const obj = node as Record; const type = obj['@type']; const types = Array.isArray(type) ? type : [type]; if (types.some((t) => t === 'Product')) return obj; // @graph 包裹 if (Array.isArray(obj['@graph'])) { for (const g of obj['@graph']) { const r = findProduct(g); if (r) return r; } } return null; } function collectImages(node: unknown, out: string[]): void { if (!node) return; if (typeof node === 'string') { if (/^(https?:)?\/\/.+/i.test(node) && !out.includes(node)) out.push(node); return; } if (Array.isArray(node)) { node.forEach((n) => collectImages(n, out)); return; } if (typeof node === 'object') { for (const v of Object.values(node as Record)) { collectImages(v, out); } } } export function extractJsonLd(): JsonLdProduct | null { try { const scripts = document.querySelectorAll('script[type="application/ld+json"]'); for (const script of Array.from(scripts)) { const text = script.textContent?.trim(); if (!text) continue; let data: unknown; try { data = JSON.parse(text); } catch { continue; } const product = findProduct(data); if (!product) continue; const offers = Array.isArray(product.offers) ? product.offers[0] : product.offers; const brandName = product.brand?.name ?? (typeof product.brand === 'string' ? product.brand : undefined); const images: string[] = []; if (product.image) collectImages(product.image, images); return { title: asString(product.name), description: asString(product.description), brand: asString(brandName), sku: asString(product.sku), price: asString(offers?.price), currency: asString(offers?.priceCurrency), images, rating: asString(product.aggregateRating?.ratingValue), reviewCount: asString(product.aggregateRating?.reviewCount), }; } } catch (err) { console.warn('[JSON-LD] 提取失败:', err); } return null; }