feat: 插件开发 ozon 端主体完成

This commit is contained in:
Joey
2026-08-16 17:32:43 +08:00
parent 1591d5e35a
commit b57933e983
32 changed files with 1960 additions and 512 deletions
+137
View File
@@ -0,0 +1,137 @@
/**
* 淘宝/天猫 SSR 状态提取 —— 通过 MAIN world 桥读取页面全局变量。
*
* 候选键(selectors-taobao.md §5.3 列出的待评估项):
* __ICE_APP_CONTEXT__ ICE 框架上下文(loaderData.home.data.resv1 已知结构)★主
* __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,
};
}