feat(extension): 采集插件功能开发

This commit is contained in:
2026-09-06 22:32:39 +08:00
parent 87db74fb89
commit 4c0f3a4a79
35 changed files with 790 additions and 20 deletions
+32
View File
@@ -0,0 +1,32 @@
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<HTMLLinkElement>('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<HTMLMetaElement>('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<string, { sourceVariantKey: string; sourceSku?: string; displayName: string; selections: never[]; confidence: 'verified' | 'partial'; evidenceRefs: string[] }>();
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<HTMLMetaElement>('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<HTMLMetaElement>('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; }