feat: 插件开发 ozon 端主体完成
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* 1688 提取器离线验证 —— 用 reference/1688.html 快照跑真实提取逻辑。
|
||||
*
|
||||
* 用法:node scripts/verify-1688.mjs [快照路径]
|
||||
* 依赖 esbuild 打包 TS 提取器(node_modules 里有)。
|
||||
*/
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const htmlPath = process.argv[2] ?? '/Users/joey/sites/seller-store/ozon-seller-kit/reference/1688.html';
|
||||
|
||||
// 1. 打包提取器(纯函数无 DOM 依赖);pnpm 布局下 esbuild bin 可能不在 .bin,动态查找
|
||||
function findEsbuild() {
|
||||
const candidates = [join(root, 'node_modules/.bin/esbuild')];
|
||||
try {
|
||||
const pnpmDir = join(root, 'node_modules/.pnpm');
|
||||
for (const d of readdirSync(pnpmDir)) {
|
||||
if (d.startsWith('esbuild@')) {
|
||||
candidates.push(join(pnpmDir, d, 'node_modules/esbuild/bin/esbuild'));
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
candidates.push('esbuild'); // 全局兜底
|
||||
for (const c of candidates) {
|
||||
try { execSync(`${c} --version`, { stdio: 'pipe' }); return c; } catch { /* try next */ }
|
||||
}
|
||||
throw new Error('找不到可用的 esbuild');
|
||||
}
|
||||
const outFile = '/tmp/1688-state.bundle.mjs';
|
||||
execSync(`${JSON.stringify(findEsbuild())} src/collector/1688-state.ts --bundle --format=esm --outfile=${outFile}`, { cwd: root });
|
||||
const { extract1688State } = await import(`file://${outFile}`);
|
||||
|
||||
// 2. 从快照提取 script#3 并在 window 垫片里求值(还原 window.context)
|
||||
const html = readFileSync(htmlPath, 'utf8');
|
||||
const scripts = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map(m => m[1]);
|
||||
const ctxScript = scripts.find(s => s.includes('window.context')) ?? scripts[3];
|
||||
const windowShim = {};
|
||||
new Function('window', 'document', 'location', ctxScript)(
|
||||
windowShim, { querySelector: () => null }, { hostname: 'detail.1688.com' },
|
||||
);
|
||||
const context = windowShim.context;
|
||||
if (!context) {
|
||||
console.error('✗ window.context 求值失败');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✓ window.context 就绪(keys: ${Object.keys(context).join(', ')})`);
|
||||
|
||||
// 3. 跑提取器并断言
|
||||
const st = extract1688State(context);
|
||||
const assert = (cond, msg) => { if (!cond) { console.error(`✗ ${msg}`); process.exit(1); } console.log(`✓ ${msg}`); };
|
||||
|
||||
assert(st !== null, '提取器返回非空');
|
||||
assert(!!st.title && st.title.length >= 5, `标题: ${st.title}`);
|
||||
assert(st.galleryImages.length >= 5, `主图 ${st.galleryImages.length} 张`);
|
||||
assert(st.videos.length >= 1 && /\.mp4/.test(st.videos[0].url), `视频: ${st.videos[0]?.url?.slice(0, 60) ?? '无'}…`);
|
||||
assert(st.skus.length >= 3, `SKU ${st.skus.length} 个(含规格名/图)`);
|
||||
assert(st.skus.every(s => /:/.test(s.name)), `SKU 名称带维度前缀: ${st.skus.slice(0, 3).map(s => s.name).join(' | ')}…`);
|
||||
assert(!!st.price && /\d/.test(st.price), `价格区间: ${st.price}`);
|
||||
assert(!!st.sales && /\d/.test(st.sales), `销量: ${st.sales}`);
|
||||
assert(!!st.shop && st.shop.length >= 2, `店铺: ${st.shop}`);
|
||||
const dimPair = st.params.find(p => p.key === '产品尺寸');
|
||||
assert(!!dimPair && /\d+×\d+×\d+/.test(dimPair.value), `产品尺寸: ${dimPair?.value}`);
|
||||
assert(st.params.some(p => p.key === '重量'), `重量: ${st.params.find(p => p.key === '重量')?.value}`);
|
||||
assert(!!st.detailUrl?.startsWith('https://'), `detailUrl: ${st.detailUrl?.slice(0, 70)}…`);
|
||||
const priced = st.skus.filter(s => s.price);
|
||||
assert(priced.length >= 1, `SKU 价格明细 ${priced.length} 条(如 ${priced[0]?.name} ${priced[0]?.price})`);
|
||||
|
||||
console.log('\n全部断言通过 ✅');
|
||||
Reference in New Issue
Block a user