/** * aliChoice 类目 dry 扫描(不保存) * 对每个商品:cat_kw 起,逐次回退 cat_fb —— 选中后检查类目是否**强制商品说明书**, * 取第一个不强制说明书的词;全都强制就标 needsManual。 * 结果写 alc_route.json,供 alcbatch.mjs 直接使用(避免重跑时再猜类目)。 */ import { readFileSync, writeFileSync, appendFileSync } from 'fs'; import { newPage, closePage, wait } from '/home/admin9webs/shein建品/sh_lib.mjs'; import { selectCategory } from '/home/admin9webs/shein建品/sh_build.mjs'; const DIR = '/home/admin9webs/alichoice建品'; const INPUT = JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json', 'utf8')); const OUT = `${DIR}/alc_route.json`; const LOG = `${DIR}/alc_dry.log`; const SKIP = new Set(['601964534137']); const log = m => { const l = `[${new Date().toISOString().slice(11, 19)}] ${m}`; console.log(l); appendFileSync(LOG, l + '\n'); }; let route = {}; try { route = JSON.parse(readFileSync(OUT, 'utf8')); } catch {} async function pickFirstOption(c, labelRe) { const p = JSON.parse(String(await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null) .find(e=>new RegExp(${JSON.stringify(labelRe)}).test((e.querySelector('label')||{}).innerText||'')); const s=f?f.querySelector('.ant-select-selector'):null; if(!s) return '[]'; s.scrollIntoView({block:'center'}); const r=s.getBoundingClientRect(); return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`))); if (!p.length) return 'no-sel'; await c.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: p[0], y: p[1], buttons: 0 }); await wait(250); for (const t of ['mousePressed', 'mouseReleased']) await c.send('Input.dispatchMouseEvent', { type: t, x: p[0], y: p[1], button: 'left', clickCount: 1, buttons: 1 }); await wait(1800); return c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop(); if(!dd) return 'no-dd'; const op=[...dd.querySelectorAll('.ant-select-item-option')].filter(e=>e.offsetParent!==null)[0]; if(!op) return 'no-op'; const r=op.getBoundingClientRect(); for(const ev of ['mousedown','mouseup','click']) op.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2})); return 'picked:'+(op.innerText||'').trim().slice(0,14);})()`); } /** 类目选中后,看表单里有没有「商品说明书」这类强制文件项 */ async function needsManual(c) { return c.evalJS(`(()=>{ for (const e of document.querySelectorAll('.ant-form-item')) { if (e.offsetParent === null) continue; const t = (e.innerText || ''); if (!/商品说明书|说明书/.test(t)) continue; const lab = e.querySelector('label'); const req = (lab && /required/.test(String(lab.className))) || e.querySelector('.attr-label.required') || /请上传文件/.test(t); if (req) return 1; } return 0;})()`); } const pids = Object.keys(INPUT).filter(p => (INPUT[p].imgs || []).length > 0).filter(p => !SKIP.has(p)); log(`aliChoice 类目 dry 扫描:${pids.length} 条`); for (const pid of pids) { if (route[pid] && route[pid].cat) { log(`跳过 ${pid}(已有 ${route[pid].cat})`); continue; } const it = INPUT[pid]; let c; try { c = await newPage('https://www.dianxiaomi.com/web/smtChoice/add'); await wait(11000); await pickFirstOption(c, '店铺名称'); await wait(5500); await pickFirstOption(c, '备货类型'); await wait(2500); const cands = [it.cat_kw, ...(it.cat_fb || [])].filter(Boolean).slice(0, 6); let chosen = null; for (const kw of cands) { const r = String(await selectCategory(c, kw, kw)); await wait(3500); if (!r.startsWith('path:')) { log(` ${pid} 「${kw}」无结果`); continue; } const nm = await needsManual(c); log(` ${pid} 「${kw}」 → ${r.slice(5, 60)} 说明书=${nm ? '强制' : '否'}`); if (!nm) { chosen = { cat: kw, path: r.slice(5, 90), needsManual: false }; break; } } route[pid] = chosen || { cat: null, needsManual: true, tried: cands }; writeFileSync(OUT, JSON.stringify(route, null, 1)); } catch (e) { log(` ${pid} 异常: ${String(e).slice(0, 120)}`); route[pid] = { cat: null, error: String(e).slice(0, 120) }; writeFileSync(OUT, JSON.stringify(route, null, 1)); } finally { if (c) await closePage(c); await wait(1500); } } const okN = Object.values(route).filter(v => v.cat).length; log(`dry 扫描结束:可用类目 ${okN} / ${pids.length},需人工 ${Object.values(route).filter(v => v.needsManual).length}`);