aliChoice:类目 dry 扫描(47/47 避开强制说明书类目)+ 批量读 alc_route.json;记录 SKU分类/材质/尺寸 两类新失败

This commit is contained in:
admin9webs committed 2026-09-20 22:46:02 +08:00
1 parent a5c32e4726
commit de9d988f4c
5 files changed
+351 -10

No files matched your search

+84
View File
@@ -0,0 +1,84 @@
/**
* 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}`);