Files
atlas-seller-kit/apps/extension/entrypoints/collector.content.ts
T

184 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<HTMLElement>('[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<T>(message: { type: string; payload?: unknown; path?: string }) {
return unwrapBackgroundResponse<T>(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<unknown>) => {
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<unknown>) => {
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<string, unknown>;
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<string, unknown>;
return message.source === 'atlas-collector' && message.type === 'source-page-state.response'
&& message.requestId === requestId && !!message.data && typeof message.data === 'object';
}