import { newTarget, CDP, SAN } from '/tmp/cdp3.mjs'; import fs from 'fs'; /* ── Walmart (Snowmountain, shopId 7661631) 建品配方 ── node wmbatch.mjs 选店铺 → 选类目(索引) → Product Name/SKU/UPC(EAN)/Brand → 价格+库存+运输重量 → 图片 → 描述+key feature → 保存 → 回读 */ const OID = process.argv[2]; if (!OID) { console.log('用法: node wmbatch.mjs '); process.exit(1); } const IN = JSON.parse(fs.readFileSync('/tmp/wm_input.json', 'utf8'))[OID]; if (!IN) { console.log('无输入数据', OID); process.exit(1); } const CATS = JSON.parse(fs.readFileSync('/tmp/wm_cats.json', 'utf8')); const CAT = CATS[IN.cat_index]; const SHOP = 'Snowmountain'; const SKU = 'WM-' + OID; const BRAND = process.env.WM_BRAND || 'Generic'; const wait = ms => new Promise(z => setTimeout(z, ms)); const log = (...a) => console.log(' ', ...a); const tl = await (await fetch('http://127.0.0.1:9223/json/list')).json(); let k = 0; for (const t of tl.filter(x => x.type === 'page' && /walmart/i.test(x.url))) { await fetch(`http://127.0.0.1:9223/json/close/${t.id}`).catch(() => {}); k++; } if (k) { log(`清理 walmart 页 ${k} 个`); await wait(1200); } 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('Page.navigate', { url: 'https://www.dianxiaomi.com/web/walmartProduct/add' }); await wait(24000); // 1. 店铺 const spos = JSON.parse(String(await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null) .find(x=>{const l=x.querySelector('label'); return l&&/店铺/.test(l.innerText||'');}); const s=it.querySelector('.ant-select-selector'); 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)]);})()`))); for (const type of ['mousePressed', 'mouseReleased']) await c.send('Input.dispatchMouseEvent', { type, x: spos[0], y: spos[1], button: 'left', clickCount: 1 }); await wait(2500); const pk = await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop(); if(!dd)return'no-dd'; const hit=[...dd.querySelectorAll('.ant-select-item-option')].find(o=>new RegExp('^\\\\s*${SHOP}\\\\s*$','i').test(o.innerText||'')); if(!hit)return'no-hit'; for(const ev of ['mousedown','mouseup','click']) hit.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 'ok';})()`); log('1) 店铺:', pk); await wait(9000); // 2. 类目 await c.evalJS(`document.body.dispatchEvent(new MouseEvent('mousedown',{bubbles:true}));document.body.click();`); await wait(1200); const cpos = JSON.parse(String(await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null) .find(x=>{const l=x.querySelector('label'); return l&&/产品分类/.test(l.innerText||'');}); const s=it.querySelector('.ant-select-selector'); 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)]);})()`))); for (const type of ['mousePressed', 'mouseReleased']) await c.send('Input.dispatchMouseEvent', { type, x: cpos[0], y: cpos[1], button: 'left', clickCount: 1 }); await wait(3000); let catRes = 'nf'; for (let i = 0; i < 70; i++) { const r = await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop(); if(!dd)return'no-dd'; const target=${JSON.stringify(CAT)}; const hit=[...dd.querySelectorAll('.ant-select-item-option')].find(o=>(o.innerText||'').trim()===target); if(!hit)return'scroll'; for(const ev of ['mousedown','mouseup','click']) hit.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 'picked';})()`); if (r === 'picked') { catRes = 'picked:' + CAT; break; } if (r === 'no-dd') { catRes = 'no-dd'; break; } await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop(); if(!dd)return 0; const rc=dd.querySelector('.rc-virtual-list-holder')||dd; rc.scrollTop=rc.scrollTop+340; return rc.scrollTop;})()`); await wait(330); } log('2) 类目:', catRes); if (catRes === 'nf' || catRes === 'no-dd') { log('❌ 类目未选中,中止'); process.exit(2); } await wait(16000); // 3. 全部字段直填 store const wr = await c.evalJS(`(()=>{const p=document.querySelector('#app').__vue_app__.config.globalProperties.$pinia; const g=n=>p._s.get(n).$state; const basic=g('walmartBasicStore').formState; basic.hasVariant=false; const pr=g('walmartProductStore').formState; pr.productName=${JSON.stringify(IN.title)}; pr.productIdType='UPC'; pr.productId=${JSON.stringify(IN.ean)}; pr.sku=${JSON.stringify(SKU)}; const at=g('walmartAttrStore').formState; at.brand=${JSON.stringify(BRAND)}; const ps=g('walmartPriceShippingStore').formState; ps.price=${JSON.stringify(String(IN.price_usd))}; ps.inventory='999'; ps.shippingWeight=${JSON.stringify(String(IN.weight_lbs))}; const ds=g('walmartDescStore').formState; ds.description=${JSON.stringify(IN.bullets.map(b => '• ' + b).join('
'))}; ds.keyFeatures=${JSON.stringify(IN.bullets.slice(0, 3).map(b => b.slice(0, 100)))}; const im=g('walmartImgStore').formState; const urls=${JSON.stringify(IN.imgs)}; im.mainImage=urls.map((u,i)=>({url:u,imgUrl:u,name:'img'+(i+1)+'.jpg',width:800,height:800})); return JSON.stringify({name:pr.productName.length, upc:pr.productId, price:ps.price, w:ps.shippingWeight, imgs:urls.length, kf:ds.keyFeatures.length});})()`); log('3) 字段:', wr); await wait(2500); // 3b. DOM 兜底:把关键文本字段再按真实事件写一遍(Brand/价格/库存/重量等 store 不绑 DOM) const setByLabel = (rx, val) => c.evalJS(`(()=>{ const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null) .find(x=>{const l=x.querySelector('label'); return l&&new RegExp(${JSON.stringify(rx)}).test(l.innerText||'');}); if(!it)return'nf'; const el=it.querySelector('textarea')||it.querySelector('input:not([type=hidden])'); if(!el)return'no-el'; const proto=el.tagName==='TEXTAREA'?window.HTMLTextAreaElement.prototype:window.HTMLInputElement.prototype; Object.getOwnPropertyDescriptor(proto,'value').set.call(el, ${JSON.stringify(String(val))}); el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); el.dispatchEvent(new Event('blur',{bubbles:true})); return 'ok';})()`); const fills = [ ['Product Name|产品名称', IN.title], ['^\\s*SKU', SKU], ['^\\s*Brand|品牌', BRAND], ['^\\s*价格', String(IN.price_usd)], ['^\\s*库存', '999'], ['运输重量', String(IN.weight_lbs)], ['产品描述', IN.bullets.map(b => '• ' + b).join('\n')], ['key feature', IN.bullets.slice(0, 3).join('\n')], ]; const fillRes = []; for (const [rx, v] of fills) fillRes.push(rx.split('|')[0] + '=' + (await setByLabel(rx, v))); log('3b) DOM兜底:', fillRes.join(' ')); await wait(2000); // 4. 保存 const sv = await c.evalJS(`(()=>{const b=[...document.querySelectorAll('button,a,span')].filter(e=>e.offsetParent!==null&&/^\\s*保存\\s*$/.test(e.innerText||'')); if(!b.length)return'nf'; for(const ev of ['mousedown','mouseup','click']) b[b.length-1].dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 'clicked';})()`); log('4) 保存:', sv); const seen = []; for (let i = 0; i < 18; i++) { await wait(700); const m = await c.evalJS(`(()=>{const ms=[...document.querySelectorAll('.ant-message-notice,.ant-notification-notice,.ant-modal-confirm')].map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim()).filter(Boolean); return JSON.stringify(ms.slice(0,3));})()`); JSON.parse(String(m)).forEach(x => seen.push(x)); } log('5) 提示:', SAN([...new Set(seen)].join(' || ')).slice(0, 420) || '(无)'); const er = await c.evalJS(`(()=>{const e=[...document.querySelectorAll('.ant-form-item-has-error')].map(x=>{const l=x.querySelector('label'); return (l?l.innerText.trim():'?');}); return JSON.stringify([...new Set(e)].slice(0,8));})()`); log('6) 仍报错的字段:', SAN(String(er)).slice(0, 200)); await c.shot('/tmp/wm_pilot.jpeg').catch(() => {}); process.exit(0);