import { newTarget, CDP, SAN } from '/tmp/cdp3.mjs'; import fs from 'fs'; /* 速卖通批量建品(全自动):分类走 UI 弹窗搜索,其余按已验证配方 */ const SHOP = 4024039; const FREIGHT_NAME = process.argv[2] || 'MY beauty 30g+1美金'; const ONLY = process.argv[3]; const wait = ms => new Promise(z => setTimeout(z, ms)); const log = (...a) => console.log(' ', ...a); const EN = JSON.parse(fs.readFileSync('/tmp/en_all.json', 'utf8')); const CLEAN = JSON.parse(fs.readFileSync('/tmp/wm_clean.json', 'utf8')); const ALL = JSON.parse(fs.readFileSync('/tmp/all48.json', 'utf8')); const KW_FB = {}; const DONE_F = '/tmp/smt_batch_done.json'; const DONE = fs.existsSync(DONE_F) ? JSON.parse(fs.readFileSync(DONE_F, 'utf8')) : {}; // 分类关键词:从英文标题里取最能定位品类的词 function catKeywords(oid) { const t = (EN[oid] || {}).title || ''; const CAND = [ [/whistle/i, ['whistle']], [/hair tie|scrunchie|hair ring|ponytail|elastic/i, ['hair', 'hairband']], [/hair clip|barrette|hairpin|clip/i, ['clip', 'hair']], [/headband|hair band/i, ['headband', 'hair']], [/keychain|key ring|key holder/i, ['keychain']], [/sticker|decal/i, ['sticker']], [/thermometer|hygrometer/i, ['thermometer']], [/button/i, ['button']], [/yarn|knitting/i, ['yarn']], [/sock/i, ['socks']], [/necklace|pendant/i, ['necklace']], [/bracelet/i, ['bracelet']], [/earring/i, ['earring']], [/card holder|card case|badge/i, ['card']], [/organizer|storage/i, ['organizer']], [/box|case|holder|container/i, ['box']], [/bag|pouch/i, ['bag']], [/comb|brush/i, ['comb']], [/mirror/i, ['mirror']], [/tape dispenser/i, ['tape']], [/hair dryer/i, ['hair dryer']], ]; for (const [rx, ks] of CAND) if (rx.test(t)) return ks; return [(t.split(/\s+/)[0] || 'gift')]; } const oids = ONLY ? [ONLY] : Object.keys(ALL).filter(o => !DONE[o] && (CLEAN[o] || []).length >= 1); log(`待建商品: ${oids.length} | 已有记录: ${Object.keys(DONE).length}`); for (const oid of oids) { const en = EN[oid], imgs = CLEAN[oid] || []; let kws = catKeywords(oid); if (!en || !imgs.length) { log(`⚠ ${oid} 缺文案或图片,跳过`); continue; } log(''); log(`═══ ${oid} | 图${imgs.length} 张 | 关键词候选 ${JSON.stringify(kws)} ═══`); // 清掉旧标签 const tl = await (await fetch('http://127.0.0.1:9223/json/list')).json(); for (const t of tl.filter(x => x.type === 'page' && /\/web\/smt\/add/.test(x.url))) await fetch(`http://127.0.0.1:9223/json/close/${t.id}`).catch(() => {}); await wait(900); const t = await newTarget('about:blank'); const c = await CDP.connect(t.webSocketDebuggerUrl); await c.send('Page.enable'); await c.send('Runtime.enable'); await c.send('Network.enable'); let posted = false; c.on(m => { if (m.method === 'Network.requestWillBeSent') { const r = m.params.request; if (r.method === 'POST' && /\/api\/smtProduct\/add\.json/.test(r.url)) posted = true; } }); try { await c.send('Page.navigate', { url: 'https://www.dianxiaomi.com/web/smt/add' }); await wait(32000); await c.evalJS(`(function(){ window.__f=function(rx,val,sel){ sel=sel||'input'; var its=[].slice.call(document.querySelectorAll('.ant-form-item')); for(var i=0;isetTimeout(z,2500)); try{await b.updateShopIdFun(${SHOP});}catch(e){} await new Promise(z=>setTimeout(z,3000)); return 1;})()`, true); const tplN = await c.evalJS(`(function(){var el=document.querySelector('#app');var p=el.__vue_app__.config.globalProperties.$pinia; return (p._s.get('smtAddBasicStore').$state.dataState.freightTemplateList||[]).length;})()`); log(` ① 店铺 ok,运费模板 ${tplN} 个`); // ② 分类(UI 弹窗,多关键词重试 + 校验关闭) let catOk = false, catInfo = ''; if (process.env.SMT_KW) { kws = process.env.SMT_KW.split(',').filter(Boolean); log(' 关键词覆盖:', kws.join('/')); } else { kws = kws.concat(KW_FB[oid] || []); } for (const kw of kws) { await c.evalJS(`(function(){var ms=[].slice.call(document.querySelectorAll('.ant-modal')).filter(function(e){return e.offsetParent!==null;}); ms.forEach(function(m){var cb=m.querySelector('.ant-modal-close'); if(cb)cb.click();}); return 1;})()`); await wait(1200); await c.evalJS(`(function(){var bs=[].slice.call(document.querySelectorAll('button')).filter(function(e){return e.offsetParent!==null&&/选择分类/.test(e.innerText||'');}); if(bs.length){bs[0].scrollIntoView({block:'center'});['mousedown','mouseup','click'].forEach(function(ev){bs[0].dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));});} return 1;})()`); await wait(4200); const n0 = await c.evalJS(`(function(){return [].slice.call(document.querySelectorAll('.ant-modal')).filter(function(e){return e.offsetParent!==null;}).length;})()`); if (!n0) { catInfo = 'modal未开'; continue; } await c.evalJS(`(function(){var ms=[].slice.call(document.querySelectorAll('.ant-modal')).filter(function(e){return e.offsetParent!==null;}); if(!ms.length)return 0; var m=ms[ms.length-1]; var el=m.querySelector('input[name="searchCategory"]'); if(!el)return 0; Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set.call(el,${JSON.stringify(kw)}); ['input','change'].forEach(function(ev){el.dispatchEvent(new Event(ev,{bubbles:true}));}); var b=[].slice.call(m.querySelectorAll('button')).filter(function(x){return /搜索/.test(x.innerText||'');}); if(b.length)b[0].click(); return 1;})()`); await wait(5500); const picked = await c.evalJS(`(function(){var ms=[].slice.call(document.querySelectorAll('.ant-modal')).filter(function(e){return e.offsetParent!==null;}); if(!ms.length)return 'nomodal'; var m=ms[ms.length-1]; var its=[].slice.call(m.querySelectorAll('.search-result-item')); if(!its.length) return 'noresult'; var best=null,bs=-999; var TITLE=${JSON.stringify((EN[oid]||{}).title||'')}; var BAD=/慈善|Благотвор|二手|Second-?hand|Used|Refurb|Sexual|情趣|Dewasa/i; var KID=/Baby|Kids|Kanak|婴儿|儿童|Infant|Toddler/i; var AUTO=/Automobile|Automotive|汽车|Motosikal/i; var ISK=KID.test(TITLE), ISA=AUTO.test(TITLE); var KW=${JSON.stringify(kw)}.toLowerCase(); its.forEach(function(it){ var tx=(it.innerText||'').replace(/\\s+/g,' ').trim(); var s=0; if(BAD.test(tx)) { s-=200; } if(!ISK && KID.test(tx)) s-=60; if(!ISA && AUTO.test(tx)) s-=60; var segs=tx.split('/'); var lastSeg=(segs[segs.length-1]||'').replace(/\(.*?\)/g,'').trim().toLowerCase(); if(KW && lastSeg.indexOf(KW)>=0) s+=40; if(/小百货|配饰|饰品|发饰|家居|住宅|文具|玩具|运动|美容|健康/i.test(tx)) s+=12; s -= Math.min(tx.length/80,5); if(s>bs){bs=s;best=it;} }); if(!best) return 'noresult'; best.scrollIntoView({block:'center'}); ['mousedown','mouseup','click'].forEach(function(ev){best.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));}); return 'picked:'+(best.innerText||'').replace(/\\s+/g,' ').trim().slice(0,60);})()`); await wait(7000); const left = await c.evalJS(`(function(){return [].slice.call(document.querySelectorAll('.ant-modal')).filter(function(e){return e.offsetParent!==null && (e.innerText||'').indexOf('选择类目')>=0;}).length;})()`); const cid = await c.evalJS(`(function(){var el=document.querySelector('#app');var p=el.__vue_app__.config.globalProperties.$pinia; return String(p._s.get('smtAddBasicStore').$state.formState.categoryId||'');})()`); catInfo = kw + ' → ' + SAN(String(picked)).slice(0,60); if (!left && cid) { catOk = true; break; } catInfo += ' [modal未关/无cat]'; } log(` ② 分类${catOk ? '✅' : '❌'}: ${catInfo.slice(0, 100)}`); if (!catOk) { log(' 分类失败,跳过该商品'); DONE[oid] = { ok:false, msg:'分类失败:'+catInfo.slice(0,60) }; fs.writeFileSync(DONE_F, JSON.stringify(DONE,null,1)); c.close(); await wait(1000); continue; } // ③ 文本 + 尺寸 await c.evalJS(`(function(){ window.__f('产品标题',${JSON.stringify(en.title)},'input:not([type=hidden])'); window.__f('零售价格','1.99'); window.__f('货值','0.50'); window.__f('库存数量','100'); window.__f('重量(kg)','0.02'); window.__f('包装后重量','0.02'); return 1;})()`); await c.evalJS(`(function(){ var set=function(el,v){Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set.call(el,v); ['input','change','blur'].forEach(function(ev){el.dispatchEvent(new Event(ev,{bubbles:true}));});}; [['长','10'],['宽','5'],['高','3']].forEach(function(p){ var els=[].slice.call(document.querySelectorAll('input')).filter(function(e){return e.offsetParent!==null&&(e.placeholder||'')===p[0];}); if(els.length) set(els[els.length-1],p[1]); }); var its=[].slice.call(document.querySelectorAll('.ant-form-item')); for(var i=0;i=0){ var inp=ws[j].querySelector('input'); if(inp&&!inp.checked)inp.click(); return 1; } } } return 0;})()`); await wait(1800); // ⑥ 物流属性(直接赋值,不依赖 UI 点击) await c.evalJS(`(function(){var el=document.querySelector('#app');var p=el.__vue_app__.config.globalProperties.$pinia; p._s.get('smtAddProductStore').$state.formState.specialProductTypeList=['1983471271:2025103']; return 1;})()`); await wait(1500); log(' ⑤ 含税+物流属性 ok'); // ⑦ 属性(含 check_box 归一化) await c.evalJS(`(function(){ var el=document.querySelector('#app');var p=el.__vue_app__.config.globalProperties.$pinia; var at=p._s.get('smtAddAttrStore'); var a=p._s.get('smtAddStore').$state.attrsInfo.attrsList||[]; var want={'品牌':'201512802','产地':'9441741844','化学品':'23399591357'}; var d=at.$state.dynamicFormData||{}; for(var i=0;i=0) d[key]='history-'+want[k]; } } var ra=at.$state.renderAttrs||[]; for(var j=0;j' + en.bullets.map(b => '• ' + b).join('
') + '

Keywords: ' + en.keywords + '

')}); ed.fire('change'); ed.fire('blur'); ed.updateElement(); return 1; }catch(e){ return 0; }})()`); await wait(2000); // ⑨ 图片(直接赋值,外部 URL 可用) const imgData = imgs.slice(0, 6).map((u, i) => ({ id: String(i + 1), url: u, name: 'img' + i + '.jpg', width: 800, height: 800, imgId: String(1000 + i), checked: false })); await c.evalJS(`(function(){var el=document.querySelector('#app');var p=el.__vue_app__.config.globalProperties.$pinia; p._s.get('smtAddProductStore').$state.formState.imageList=${JSON.stringify(imgData)}; return 1;})()`); await wait(3000); log(` ⑥ 描述+图片(${imgData.length}张) ok`); // ⑩ 保存 await c.evalJS(`(function(){var bs=[].slice.call(document.querySelectorAll('button')).filter(function(e){return e.offsetParent!==null&&/^\\s*保存\\s*$/.test((e.innerText||'').trim());}); if(bs.length) bs[0].click(); return 1;})()`); await wait(17000); const msg = String(await c.evalJS(`JSON.stringify([].slice.call(document.querySelectorAll('.ant-modal,.ant-message-notice')).filter(function(e){return e.offsetParent!==null;}).map(function(e){return (e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,120);}))`)); const ok = /已保存到/.test(msg); log(` ${ok ? '✅' : '❌'} 保存: ${SAN(msg).slice(0, 110)} | POST=${posted}`); DONE[oid] = { ok, msg: SAN(msg).slice(0, 100), imgs: imgData.length, kws, time: new Date().toISOString() }; fs.writeFileSync(DONE_F, JSON.stringify(DONE, null, 1)); } catch (e) { log(` ❌ 异常: ${String(e).slice(0, 90)}`); DONE[oid] = { ok: false, err: String(e).slice(0, 90) }; fs.writeFileSync(DONE_F, JSON.stringify(DONE, null, 1)); } try { c.close(); } catch (e) {} await wait(1200); } log(''); log(`全部结束。成功 ${Object.values(DONE).filter(x => x.ok).length} / ${Object.keys(DONE).length}`);