Expand seller store functionality
This commit is contained in:
@@ -1,15 +1,151 @@
|
||||
import { collectCurrentProduct, supportedProductPlatform } from '../lib/source-parsers.js';
|
||||
import {
|
||||
collectCurrentProduct,
|
||||
supportedProductPlatform,
|
||||
} from '../lib/source-parsers.js';
|
||||
import type { OzonPageJsonFetcher } from '../lib/ozon-parser.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;
|
||||
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 browser.runtime.sendMessage({ type: 'atlas.session' }).then((value: { session?: unknown }) => value.session).catch(() => null);
|
||||
root.innerHTML = `<style>:host{all:initial}.box{width:156px;padding:12px;background:#fff;border-radius:12px;box-shadow:0 8px 28px #0003;font:14px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#1f2937}.head{display:flex;justify-content:space-between;align-items:center;font-weight:700;margin-bottom:10px}.dot{width:9px;height:9px;border-radius:50%;background:#ffd43b;border:0}.btn{display:block;width:100%;border:0;border-radius:18px;padding:9px 8px;margin:7px 0;background:#2878f0;color:#fff;font-weight:600;cursor:pointer}.btn.primary{background:#ff5353}.link{display:block;width:100%;border:0;background:transparent;color:#1668dc;cursor:pointer;padding:5px}.hint{font-size:12px;color:#6b7280;text-align:center;margin-top:6px}</style><div class="box"><div class="head"><span>Atlas ERP</span><button class="dot" aria-label="收起"></button></div>${session ? '<button class="btn primary" data-action="edit">编辑上架</button><button class="btn" data-action="collect">采集商品</button><button class="link" data-action="erp">进入 ERP</button>' : '<button class="btn primary" data-action="login">登录 Atlas ERP</button>'}<div class="hint" data-hint></div></div>`;
|
||||
root.querySelector('.dot')?.addEventListener('click', () => { mount?.remove(); mount = null; shadow = null; });
|
||||
root.querySelectorAll<HTMLButtonElement>('[data-action]').forEach((button) => button.addEventListener('click', () => void action(button.dataset.action ?? '', root)));
|
||||
};
|
||||
const action = async (kind: string, shadow: ShadowRoot) => { const hint = shadow.querySelector<HTMLElement>('[data-hint]')!; try { if (kind === 'login') { hint.textContent = '正在登录…'; await browser.runtime.sendMessage({ type: 'atlas.login' }); await render(); return; } if (kind === 'erp') { await browser.runtime.sendMessage({ type: 'atlas.open', path: '/' }); return; } hint.textContent = '正在采集…'; const payload = collectCurrentProduct(); const response = await browser.runtime.sendMessage({ type: 'atlas.collect', payload }); if (kind === 'edit') await browser.runtime.sendMessage({ type: 'atlas.open', path: response.result.data.editor_entry_path }); hint.textContent = kind === 'edit' ? '已打开采集数据测试页' : '已进入采集箱'; } catch (error) { hint.textContent = error instanceof Error ? error.message : '采集失败,请重试'; } };
|
||||
void render(); setInterval(() => { if (location.href !== observedUrl) { observedUrl = location.href; void render(); } }, 700);
|
||||
} });
|
||||
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;
|
||||
|
||||
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 browser.runtime
|
||||
.sendMessage({ type: 'atlas.session' })
|
||||
.then((value: { session?: unknown }) => value.session)
|
||||
.catch(() => null);
|
||||
root.innerHTML = `<style>:host{all:initial}.box{width:156px;padding:12px;background:#fff;border-radius:12px;box-shadow:0 8px 28px #0003;font:14px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#1f2937}.head{display:flex;justify-content:space-between;align-items:center;font-weight:700;margin-bottom:10px}.dot{width:9px;height:9px;border-radius:50%;background:#ffd43b;border:0}.btn{display:block;width:100%;border:0;border-radius:18px;padding:9px 8px;margin:7px 0;background:#2878f0;color:#fff;font-weight:600;cursor:pointer}.btn.primary{background:#ff5353}.link{display:block;width:100%;border:0;background:transparent;color:#1668dc;cursor:pointer;padding:5px}.hint{font-size:12px;color:#6b7280;text-align:center;margin-top:6px}</style><div class="box"><div class="head"><span>Atlas ERP</span><button class="dot" aria-label="收起"></button></div>${session ? '<button class="btn primary" data-action="edit">编辑上架</button><button class="btn" data-action="collect">采集商品</button><button class="link" data-action="erp">进入 ERP</button>' : '<button class="btn primary" data-action="login">登录 Atlas ERP</button>'}<div class="hint" data-hint></div></div>`;
|
||||
root.querySelector('.dot')?.addEventListener('click', () => {
|
||||
mount?.remove();
|
||||
mount = null;
|
||||
shadow = null;
|
||||
});
|
||||
root.querySelectorAll<HTMLButtonElement>('[data-action]').forEach(
|
||||
(button) =>
|
||||
button.addEventListener('click', () =>
|
||||
void action(button.dataset.action ?? '', root),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const action = async (kind: string, root: ShadowRoot) => {
|
||||
const hint = root.querySelector<HTMLElement>('[data-hint]')!;
|
||||
try {
|
||||
if (kind === 'login') {
|
||||
hint.textContent = '正在登录…';
|
||||
await browser.runtime.sendMessage({ type: 'atlas.login' });
|
||||
await render();
|
||||
return;
|
||||
}
|
||||
if (kind === 'erp') {
|
||||
await browser.runtime.sendMessage({ type: 'atlas.open', path: '/' });
|
||||
return;
|
||||
}
|
||||
hint.textContent = '正在采集多 SKU…';
|
||||
const payload = await collectCurrentProduct(
|
||||
document,
|
||||
location.href,
|
||||
supportedProductPlatform() === 'ozon'
|
||||
? requestOzonPageJson
|
||||
: undefined,
|
||||
);
|
||||
const response = await browser.runtime.sendMessage({
|
||||
type: 'atlas.collect',
|
||||
payload,
|
||||
});
|
||||
if (kind === 'edit') {
|
||||
await browser.runtime.sendMessage({
|
||||
type: 'atlas.open',
|
||||
path: response.result.data.editor_entry_path,
|
||||
});
|
||||
}
|
||||
hint.textContent =
|
||||
kind === 'edit' ? '已打开采集数据测试页' : '已进入采集箱';
|
||||
} catch (error) {
|
||||
hint.textContent =
|
||||
error instanceof Error ? error.message : '采集失败,请重试';
|
||||
}
|
||||
};
|
||||
|
||||
void render();
|
||||
setInterval(() => {
|
||||
if (location.href !== observedUrl) {
|
||||
observedUrl = location.href;
|
||||
void render();
|
||||
}
|
||||
}, 700);
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
|
||||
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')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Runs in Ozon's MAIN world so the request has exactly the same origin and
|
||||
* session context as the page. It accepts only Atlas' two fixed, numeric-SKU
|
||||
* routes and returns a small widget-state allowlist; it never exposes cookies,
|
||||
* request headers or arbitrary fetch capability to the isolated script.
|
||||
*/
|
||||
export default defineContentScript({
|
||||
matches: ['*://www.ozon.ru/*', '*://www.ozon.kz/*', '*://www.ozon.by/*'],
|
||||
world: 'MAIN',
|
||||
runAt: 'document_idle',
|
||||
main() {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.source !== window || !isRequest(event.data)) return;
|
||||
void respond(event.data);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
type Request = {
|
||||
source: 'atlas-collector';
|
||||
type: 'ozon-page-json.request';
|
||||
requestId: string;
|
||||
kind: 'modal-aspects' | 'product' | 'product-detail';
|
||||
sku: string;
|
||||
};
|
||||
|
||||
function isRequest(value: unknown): value is Request {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value))
|
||||
return false;
|
||||
const data = value as Record<string, unknown>;
|
||||
return (
|
||||
data.source === 'atlas-collector' &&
|
||||
data.type === 'ozon-page-json.request' &&
|
||||
typeof data.requestId === 'string' &&
|
||||
/^[a-zA-Z0-9-]{16,120}$/.test(data.requestId) &&
|
||||
(data.kind === 'modal-aspects' ||
|
||||
data.kind === 'product' ||
|
||||
data.kind === 'product-detail') &&
|
||||
typeof data.sku === 'string' &&
|
||||
/^\d{4,}$/.test(data.sku)
|
||||
);
|
||||
}
|
||||
|
||||
async function respond(request: Request) {
|
||||
try {
|
||||
const pagePath =
|
||||
request.kind === 'modal-aspects'
|
||||
? `/modal/aspectsNew?product_id=${request.sku}`
|
||||
: request.kind === 'product-detail'
|
||||
? `/product/${request.sku}/?layout_container=pdpPage2column&layout_page_index=2`
|
||||
: `/product/${request.sku}/`;
|
||||
const response = await fetch(
|
||||
`/api/entrypoint-api.bx/page/json/v2?url=${encodeURIComponent(pagePath)}`,
|
||||
{ credentials: 'same-origin' },
|
||||
);
|
||||
if (!response.ok)
|
||||
throw new Error(`Ozon 页面状态请求失败(HTTP ${response.status})`);
|
||||
const value = await response.json();
|
||||
window.postMessage(
|
||||
{
|
||||
source: 'atlas-collector',
|
||||
type: 'ozon-page-json.response',
|
||||
requestId: request.requestId,
|
||||
ok: true,
|
||||
data: allowlistedPageState(value),
|
||||
},
|
||||
location.origin,
|
||||
);
|
||||
} catch (error) {
|
||||
window.postMessage(
|
||||
{
|
||||
source: 'atlas-collector',
|
||||
type: 'ozon-page-json.response',
|
||||
requestId: request.requestId,
|
||||
ok: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Ozon 页面状态请求失败',
|
||||
},
|
||||
location.origin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function allowlistedPageState(value: unknown) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value))
|
||||
return { widgetStates: {} };
|
||||
const states = (value as { widgetStates?: unknown }).widgetStates;
|
||||
if (!states || typeof states !== 'object' || Array.isArray(states))
|
||||
return { widgetStates: {} };
|
||||
const widgetStates: Record<string, unknown> = {};
|
||||
for (const [name, state] of Object.entries(
|
||||
states as Record<string, unknown>,
|
||||
)) {
|
||||
if (
|
||||
/^web(Aspects|AspectsModal|ProductHeading|Price|Gallery|Description|RichContent|Features|ShortCharacteristics|DetailedCharacteristics|Characteristics)-/.test(
|
||||
name,
|
||||
)
|
||||
) {
|
||||
widgetStates[name] = state;
|
||||
}
|
||||
}
|
||||
return { widgetStates };
|
||||
}
|
||||
Reference in New Issue
Block a user