import { collectCurrentProduct, supportedProductPlatform, } from '../lib/source-parsers.js'; import type { OzonPageJsonFetcher } from '../lib/ozon-parser.js'; import type { MarketplacePageStateFetcher } from '../lib/source-parsers.js'; import { renderCollectorPanel } from '../lib/collector-panel.js'; import { unwrapBackgroundResponse } from '../lib/request-errors.js'; export default defineContentScript({ matches: [ '*://www.ozon.ru/*', '*://www.ozon.kz/*', '*://www.ozon.by/*', '*://detail.1688.com/*', '*://item.taobao.com/*', '*://detail.tmall.com/*', ], runAt: 'document_idle', main() { let mount: HTMLDivElement | null = null; let shadow: ShadowRoot | null = null; let observedUrl = location.href; let minimized = false; const render = async () => { if (!supportedProductPlatform()) { mount?.remove(); mount = null; shadow = null; return; } if (!mount) { mount = document.createElement('div'); mount.style.cssText = 'position:fixed;right:22px;bottom:80px;z-index:2147483647'; document.documentElement.append(mount); shadow = mount.attachShadow({ mode: 'open' }); } const root = shadow!; const session = await sendBackgroundMessage<{ session?: unknown }>({ type: 'atlas.session' }) .then((value: { session?: unknown }) => value.session) .catch(() => null); renderCollectorPanel( root, Boolean(session), minimized, (next) => { minimized = next; }, (kind) => void action(kind, root), ); }; const action = async (kind: string, root: ShadowRoot) => { const hint = root.querySelector('[data-hint]')!; try { if (kind === 'login') { hint.textContent = '正在登录…'; await sendBackgroundMessage({ type: 'atlas.login' }); await render(); return; } if (kind === 'erp') { await sendBackgroundMessage({ type: 'atlas.open', path: '/' }); return; } hint.textContent = '正在采集多 SKU…'; const payload = await collectCurrentProduct( document, location.href, supportedProductPlatform() === 'ozon' ? requestOzonPageJson : undefined, supportedProductPlatform() && supportedProductPlatform() !== 'ozon' ? requestMarketplacePageState : undefined, ); hint.textContent = `已解析 ${payload.variants.length || 1} 个 SKU,正在入库…`; const response = await sendBackgroundMessage<{ result: { data: { editor_entry_path: string } } }>({ type: 'atlas.collect', payload, }); if (kind === 'edit') { await sendBackgroundMessage({ type: 'atlas.open', path: response.result.data.editor_entry_path, }); } hint.textContent = `${kind === 'edit' ? '已打开商品编辑页' : '已进入采集箱'}(${payload.variants.length || 1} 个 SKU)`; } catch (error) { hint.textContent = error instanceof Error ? error.message : '采集失败,请重试'; } }; void render(); setInterval(() => { if (location.href !== observedUrl) { observedUrl = location.href; void render(); } }, 700); }, }); async function sendBackgroundMessage(message: { type: string; payload?: unknown; path?: string }) { return unwrapBackgroundResponse(await browser.runtime.sendMessage(message)); } const requestOzonPageJson: OzonPageJsonFetcher = ({ kind, sku }) => new Promise((resolve, reject) => { const requestId = crypto.randomUUID(); const timeout = window.setTimeout(() => { window.removeEventListener('message', receive); reject(new Error('Ozon 页面状态读取超时')); }, 15_000); const receive = (event: MessageEvent) => { if (event.source !== window || !isBridgeResponse(event.data, requestId)) return; window.clearTimeout(timeout); window.removeEventListener('message', receive); if (event.data.ok) resolve(event.data.data); else reject(new Error(event.data.error)); }; window.addEventListener('message', receive); window.postMessage( { source: 'atlas-collector', type: 'ozon-page-json.request', requestId, kind, sku, }, location.origin, ); }); const requestMarketplacePageState: MarketplacePageStateFetcher = (platform) => new Promise((resolve) => { const requestId = crypto.randomUUID(); const timeout = window.setTimeout(() => { window.removeEventListener('message', receive); resolve({}); }, 3_000); const receive = (event: MessageEvent) => { if (event.source !== window || !isMarketplaceStateResponse(event.data, requestId)) return; window.clearTimeout(timeout); window.removeEventListener('message', receive); resolve(event.data.data); }; window.addEventListener('message', receive); window.postMessage({ source: 'atlas-collector', type: 'source-page-state.request', requestId, platform }, location.origin); }); function isBridgeResponse( value: unknown, requestId: string, ): value is { source: 'atlas-collector'; type: 'ozon-page-json.response'; requestId: string; ok: boolean; data?: unknown; error: string; } { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; const message = value as Record; return ( message.source === 'atlas-collector' && message.type === 'ozon-page-json.response' && message.requestId === requestId && typeof message.ok === 'boolean' && (message.ok || typeof message.error === 'string') ); } function isMarketplaceStateResponse(value: unknown, requestId: string): value is { source: 'atlas-collector'; type: 'source-page-state.response'; requestId: string; data: unknown } { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; const message = value as Record; return message.source === 'atlas-collector' && message.type === 'source-page-state.response' && message.requestId === requestId && !!message.data && typeof message.data === 'object'; }