184 lines
7.2 KiB
TypeScript
184 lines
7.2 KiB
TypeScript
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';
|
||
}
|