import type { CollectedProductPayload } from '@atlas/contracts'; export function isOzonProductPage(url = location.href) { return /^https:\/\/www\.ozon\.(ru|kz|by)\/product\//.test(url); } export function collectOzonProduct(documentRoot: Document = document, pageUrl = location.href): CollectedProductPayload { const canonical = documentRoot.querySelector('link[rel="canonical"]')?.href ?? pageUrl.split('?')[0]; const currentSku = skuFromUrl(canonical); if (!currentSku) throw new Error('未识别到 Ozon 商品 SKU'); const title = text(documentRoot.querySelector('h1')) ?? documentRoot.querySelector('meta[property="og:title"]')?.content?.trim(); const images = Array.from(documentRoot.images).map((image) => image.currentSrc || image.src).filter((url) => /^https:\/\//.test(url)).filter(unique).slice(0, 30); const evidence: CollectedProductPayload['evidence'] = [{ ref: 'page:url', kind: 'page', path: 'location.href', raw_value: canonical }]; const variants = new Map(); variants.set(currentSku, { sourceVariantKey: currentSku, sourceSku: currentSku, displayName: title ?? `Ozon SKU ${currentSku}`, selections: [], confidence: 'verified', evidenceRefs: ['page:url'] }); for (const script of Array.from(documentRoot.scripts)) { const source = script.textContent ?? ''; // Only inspect Ozon's variant-related state; never use generic page-wide // SKU hits because recommendation cards would create false variants. if (!/webProducts|webAspects|aspects/i.test(source)) continue; const matches = source.matchAll(/["']sku["']\s*[:=]\s*["']?(\d{4,})/g); let count = 0; for (const match of matches) { if (count++ >= 100) break; const sku = match[1]; if (!variants.has(sku)) variants.set(sku, { sourceVariantKey: sku, sourceSku: sku, displayName: `Ozon SKU ${sku}`, selections: [], confidence: 'partial', evidenceRefs: ['state:variant-sku'] }); } if (count) evidence.push({ ref: 'state:variant-sku', kind: 'state', path: 'webProducts/webAspects', raw_value: `发现 ${count} 个公开 SKU` }); } const amount = parsePrice(documentRoot); const currency = documentRoot.querySelector('meta[property="product:price:currency"]')?.content?.toUpperCase() ?? 'RUB'; return { source: { platform: 'ozon', itemKey: currentSku, canonicalUrl: canonical, collectedAt: new Date().toISOString(), parserVersion: 'ozon-public-1.0.0' }, common: { ...(title ? { title: { value: title, raw_value: title, confidence: 'verified' as const } } : {}), ...(amount ? { sourcePrice: { amount, currencyCode: currency, raw_value: amount } } : {}), attributes: [], categories: [] }, variantAxes: [], variants: [...variants.values()], assets: images.map((url, index) => ({ url, role: 'source_main' as const, relation: 'product_candidate' as const, sortOrder: index, evidenceRef: 'page:gallery' })), evidence, }; } function skuFromUrl(url: string) { return url.match(/-(\d+)(?:\/|$|\?)/)?.[1] ?? null; } function text(element: Element | null) { const value = element?.textContent?.replace(/\s+/g, ' ').trim(); return value || undefined; } function parsePrice(root: Document) { const meta = root.querySelector('meta[property="product:price:amount"]')?.content; if (meta && /^\d+(\.\d+)?$/.test(meta)) return meta; const candidate = Array.from(root.querySelectorAll('[data-widget*="webPrice"], [data-widget*="price"]')).map((node) => node.textContent ?? '').find((value) => /\d/.test(value)); const digits = candidate?.replace(/[^\d,\.]/g, '').replace(',', '.'); return digits && /^\d+(\.\d+)?$/.test(digits) ? digits : undefined; } function unique(value: string, index: number, array: string[]) { return array.indexOf(value) === index; }