feat: 插件开发 ozon 端主体完成
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 淘宝/天猫 SSR 状态提取 —— 通过 MAIN world 桥读取页面全局变量。
|
||||
*
|
||||
* 候选键(selectors-taobao.md §5.3 列出的待评估项):
|
||||
* __ICE_APP_CONTEXT__ ICE 框架上下文(loaderData.home.data.res,v1 已知结构)★主
|
||||
* __general_skupanel_cache_data 疑似完整 SKU 面板缓存(结构未知,容错深搜)
|
||||
* __ICE_DATA_LOADER__ ICE 框架数据层(容错深搜)
|
||||
*
|
||||
* 输出对齐 ssr.ts 的 SSRData,供 buildFromSSR 消费。
|
||||
*/
|
||||
import type { SSRData } from './ssr';
|
||||
|
||||
/** 从 __ICE_APP_CONTEXT__ 结构映射(原 v1 extractSSRData 的对象版) */
|
||||
function fromIceContext(ctx: unknown): SSRData | null {
|
||||
const res = (ctx as any)?.loaderData?.home?.data?.res;
|
||||
if (!res?.item?.title || !res?.item?.itemId) return null;
|
||||
const industryParams = res.plusViewVO?.industryParamVO;
|
||||
const extensionParams = res.componentsVO?.extensionInfoVO?.infos?.find(
|
||||
(i: any) => i.type === 'BASE_PROPS'
|
||||
);
|
||||
return {
|
||||
item: {
|
||||
title: res.item.title,
|
||||
itemId: res.item.itemId,
|
||||
images: res.item.images || [],
|
||||
videos: res.item.videos,
|
||||
},
|
||||
skuBase: res.skuBase,
|
||||
params: {
|
||||
basicParamList: industryParams?.basicParamList || extensionParams?.items || [],
|
||||
enhanceParamList: industryParams?.enhanceParamList || [],
|
||||
},
|
||||
price: res.componentsVO?.priceVO?.price || res.componentsVO?.priceVO?.extraPrice,
|
||||
};
|
||||
}
|
||||
|
||||
/** 容错:未知结构里深搜「SKU props 数组」(元素含 name + values/props 嵰 name/imageUrl) */
|
||||
function skuPropsFromUnknown(root: unknown): SSRData['skuBase'] | null {
|
||||
const candidates: any[] = [];
|
||||
const collect = (node: unknown, depth = 0) => {
|
||||
if (!node || typeof node !== 'object' || depth > 6 || candidates.length) return;
|
||||
if (Array.isArray(node)) {
|
||||
if (
|
||||
node.length >= 1 && node.every((x: any) => x && typeof x === 'object' &&
|
||||
typeof (x.prop ?? x.name) === 'string' && Array.isArray(x.values ?? x.props))
|
||||
) { candidates.push(node.map((x: any) => ({
|
||||
pid: String(x.pid ?? ''),
|
||||
name: x.prop ?? x.name,
|
||||
values: (x.values ?? x.props).map((v: any) => ({
|
||||
vid: String(v.vid ?? ''),
|
||||
name: v.name ?? v.valueName ?? '',
|
||||
image: v.image ?? v.imageUrl,
|
||||
})),
|
||||
}))); return;
|
||||
}
|
||||
node.forEach(n => collect(n, depth + 1));
|
||||
return;
|
||||
}
|
||||
for (const v of Object.values(node as Record<string, unknown>)) collect(v, depth + 1);
|
||||
};
|
||||
collect(root);
|
||||
return candidates.length ? { props: candidates[0] } : null;
|
||||
}
|
||||
|
||||
/** 容错:深搜参数数组(元素含 propertyName/valueName) */
|
||||
function paramsFromUnknown(root: unknown): Array<{ propertyName: string; valueName: string }> {
|
||||
const out: Array<{ propertyName: string; valueName: string }> = [];
|
||||
const seen = new Set<string>();
|
||||
const walk = (node: unknown, depth = 0) => {
|
||||
if (!node || typeof node !== 'object' || depth > 6 || out.length > 60) return;
|
||||
if (Array.isArray(node)) {
|
||||
if (node.length >= 2 && node.every((x: any) => x && typeof x === 'object' &&
|
||||
typeof x.propertyName === 'string' && typeof x.valueName === 'string')) {
|
||||
for (const p of node) {
|
||||
const k = `${p.propertyName}=${p.valueName}`;
|
||||
if (!seen.has(k)) { seen.add(k); out.push({ propertyName: p.propertyName, valueName: p.valueName }); }
|
||||
}
|
||||
}
|
||||
node.forEach(n => walk(n, depth + 1));
|
||||
return;
|
||||
}
|
||||
for (const v of Object.values(node as Record<string, unknown>)) walk(v, depth + 1);
|
||||
};
|
||||
walk(root);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function taobaoStateFromBridge(keys: Record<string, any>): SSRData | null {
|
||||
// 主路径:ICE 上下文
|
||||
const ice = fromIceContext(keys['__ICE_APP_CONTEXT__']);
|
||||
if (ice) {
|
||||
// 主路径缺 SKU 时用面板缓存补
|
||||
if (!ice.skuBase?.props?.length) {
|
||||
const fromCache = skuPropsFromUnknown(keys['__general_skupanel_cache_data'] ?? keys['__ICE_DATA_LOADER__']);
|
||||
if (fromCache?.props?.length) ice.skuBase = fromCache;
|
||||
}
|
||||
return ice;
|
||||
}
|
||||
|
||||
// 降级:只有面板缓存/数据层 —— 尽力拼一个最小 SSRData(标题给空,DOM 会补)
|
||||
const cacheRoot = keys['__general_skupanel_cache_data'] ?? keys['__ICE_DATA_LOADER__'];
|
||||
if (cacheRoot) {
|
||||
const skuBase = skuPropsFromUnknown(cacheRoot);
|
||||
const params = paramsFromUnknown(cacheRoot);
|
||||
if (skuBase?.props?.length || params.length) {
|
||||
return {
|
||||
item: { title: '', itemId: '', images: [], videos: [] },
|
||||
skuBase: skuBase ?? undefined,
|
||||
params: { basicParamList: params, enhanceParamList: [] },
|
||||
price: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** mtop pcdetail.data.get 响应 → SSRData(竞品验证的接口,结构同 res) */
|
||||
export function taobaoStateFromMtop(data: unknown): SSRData | null {
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
const d = data as any;
|
||||
const item = d.item ?? d.itemDO;
|
||||
if (!item) return null;
|
||||
const images: string[] = (item.images ?? []).filter((u: unknown) => typeof u === 'string');
|
||||
const videos = (item.videos ?? []).map((v: any) => ({ url: v?.url ?? v?.videoUrl, videoThumbnailURL: v?.videoThumbnailURL ?? v?.coverUrl })).filter((v: any) => v.url);
|
||||
const params = paramsFromUnknown(d.params ?? d.propsList ?? d);
|
||||
return {
|
||||
item: {
|
||||
title: item.title ?? '',
|
||||
itemId: String(item.itemId ?? item.itemNumId ?? ''),
|
||||
images,
|
||||
videos,
|
||||
},
|
||||
skuBase: d.skuBase,
|
||||
params: { basicParamList: params, enhanceParamList: [] },
|
||||
price: d.skuCore?.price ?? undefined,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user