/** * Ozon 采集编排(平台文件):四路径合并(来自 extension-v2 生产逻辑)。 * ① SSR widget state(DOM data-state 属性,白名单)★主路径 * ② JSON-LD(schema.org/Product) * ③ 站内页 JSON API(entrypoint-api.bx) * ④ DOM data-widget 选择器兜底 + 详情图补充 */ import { autoScrollToBottom, waitForAny } from '../dom'; import { collectImages, type ImageMaterial } from '../image'; import { collectTexts, mergeTexts, type TextMaterial } from '../text'; import { toOriginalUrl, toThumbUrl } from '../url'; import { extractJsonLd } from '../jsonld'; import { fetchOzonPageData, type OzonPageData } from '../ozon-api'; import { extractOzonState, type OzonStateData } from '../ozon-state'; import { finalize, mergeImages, type ScanResult } from '../merge'; import type { SiteProfile } from '../../profiles/types'; interface StructuredBundle { title?: string; price?: string; brand?: string; description?: string; characteristics: Array<{ key: string; value: string }>; galleryImages: string[]; videos: string[]; videoCovers: string[]; skuVariants: Array<{ name: string; image?: string }>; } function mergeStructured( state: OzonStateData, jsonld: ReturnType, api: OzonPageData | null ): StructuredBundle { const bundle: StructuredBundle = { title: state.title || jsonld?.title || api?.title, price: state.price || jsonld?.price || api?.price, brand: jsonld?.brand, description: api?.description || jsonld?.description, characteristics: [...state.characteristics], galleryImages: [...state.galleryImages], videos: [...state.videos], videoCovers: [...state.videoCovers], skuVariants: [...state.skuVariants], }; for (const u of api?.images ?? []) { if (!bundle.galleryImages.includes(u)) bundle.galleryImages.push(u); } for (const u of api?.videos ?? []) { if (!bundle.videos.includes(u)) bundle.videos.push(u); } const seenChars = new Set(bundle.characteristics.map((c) => `${c.key}::${c.value}`)); for (const c of api?.characteristics ?? []) { const k = `${c.key}::${c.value}`; if (!seenChars.has(k)) { seenChars.add(k); bundle.characteristics.push(c); } } return bundle; } function buildFromBundle(profile: SiteProfile, bundle: StructuredBundle): { texts: TextMaterial[]; images: ImageMaterial[]; } { const texts: TextMaterial[] = []; const images: ImageMaterial[] = []; if (bundle.title) texts.push({ kind: 'title', content: bundle.title }); if (bundle.price) texts.push({ kind: 'price', content: bundle.price }); if (bundle.brand) texts.push({ kind: 'brand', content: bundle.brand }); if (bundle.characteristics.length) { texts.push({ kind: 'params', content: bundle.characteristics.map((p) => `${p.key}: ${p.value}`).join('\n'), pairs: bundle.characteristics, }); } if (bundle.description) texts.push({ kind: 'desc', content: bundle.description }); let idx = 0; bundle.galleryImages.forEach((u, i) => { const orig = toOriginalUrl(u, profile.originalUrlRules); images.push({ key: `main-${String(i + 1).padStart(3, '0')}`, groupKey: 'main', groupName: '主图', url: orig, thumbUrl: toThumbUrl(orig), index: idx++, type: 'img', }); }); bundle.skuVariants.forEach((s, i) => { if (!s.image) return; const orig = toOriginalUrl(s.image, profile.originalUrlRules); images.push({ key: `sku-${String(i + 1).padStart(3, '0')}`, groupKey: 'sku', groupName: 'SKU图片', variantName: s.name || undefined, url: orig, thumbUrl: toThumbUrl(orig), index: idx++, type: 'img', }); }); bundle.videos.forEach((u, i) => { images.push({ key: `video-${String(i + 1).padStart(3, '0')}`, groupKey: 'video', groupName: '视频', url: u, thumbUrl: bundle.videoCovers[i] ?? '', index: idx++, type: 'video', }); }); return { texts, images }; } export async function scanOzon(profile: SiteProfile, itemId: string | null): Promise { const state = extractOzonState(); let source: ScanResult['source'] = state.title || state.galleryImages.length ? 'state' : 'dom'; const jsonld = extractJsonLd(); let api: OzonPageData | null = null; if (itemId) { try { api = await fetchOzonPageData(itemId); } catch (err) { console.warn('[SuiteCollector] API 提取异常:', err); } } const bundle = mergeStructured(state, jsonld, api); const structured = buildFromBundle(profile, bundle); if ((structured.texts.some((t) => t.kind === 'title') || structured.images.length > 0) && source === 'dom') { source = 'mixed'; } // ── 路径④:DOM 采集(兜底 + 详情图补充)── // 先滚到底触发懒加载(详情图不滚不加载),有界滚动防无限推荐流 await autoScrollToBottom(); const anchor = await waitForAny(profile.readySelectors, profile.readyTimeoutMs ?? 8_000); if (!anchor) console.warn('[SuiteCollector] 等待页面就绪超时(继续尝试 DOM 采集)'); const domTexts = collectTexts(profile).materials; const domImages = collectImages(profile); const texts = mergeTexts(structured.texts, domTexts); const images = mergeImages(structured.images, domImages, profile); return finalize(profile, itemId, texts, images, state.breadcrumbs, source); }