Files
image-suite-studio/extension/scripts/verify-1688.mjs
T
2026-08-16 17:32:43 +08:00

72 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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全部断言通过 ✅');