import { newTarget, CDP, SAN } from '/tmp/cdp3.mjs'; import fs from 'fs'; /* ── TikTok (AutoMavenShop_MY) 建品配方 ── node tkbatch.mjs [catKeyword] [variantValue] 流程:选店铺 → 分类弹窗 → 标题/卖点/关键词/描述 → 变种属性 → SKU价+库存 → 重量/尺寸 → 仓库 → 图片(外链) → 来源URL → 保存 → 回读 */ const OID = process.argv[2]; const CAT_KW = process.argv[3] || ''; const VAR_VAL = process.argv[4] || 'Default'; if (!OID) { console.log('用法: node tkbatch.mjs [catKeyword] [variantValue]'); process.exit(1); } const IN = JSON.parse(fs.readFileSync('/tmp/tk_input.json', 'utf8'))[OID]; if (!IN) { console.log('无输入数据', OID); process.exit(1); } const wait = ms => new Promise(z => setTimeout(z, ms)); const log = (...a) => console.log(' ', ...a); const DIMS = (() => { const W = Math.max(100, Math.round(IN.weight * 1.3)); return { L: Math.max(60, Math.min(400, W)), Wi: Math.max(40, Math.min(300, Math.round(W * 0.6))), H: Math.max(10, Math.min(200, Math.round(W * 0.2))) }; })(); // 0. 清理编辑页 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' && /tiktok/i.test(x.url))) { await fetch(`http://127.0.0.1:9223/json/close/${t.id}`).catch(() => {}); k++; } if (k) { log(`清理 tiktok 页 ${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/tiktokProduct/add' }); await wait(20000); const cur = async () => String(await c.evalJS(`(()=>{const s=[...document.querySelectorAll('.ant-select')].filter(e=>e.offsetParent!==null)[0]; return s?((s.querySelector('.ant-select-selection-item')||{}).innerText||''):'';})()`)); // 1. 选店铺 AutoMavenShop_MY async function pickShop() { for (let attempt = 0; attempt < 3; attempt++) { const pos = JSON.parse(String(await c.evalJS(`(()=>{const s=[...document.querySelectorAll('.ant-select')].filter(e=>e.offsetParent!==null)[0]; 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 (!pos.length) return 'no-select'; for (const type of ['mousePressed', 'mouseReleased']) await c.send('Input.dispatchMouseEvent', { type, x: pos[0], y: pos[1], button: 'left', clickCount: 1 }); await wait(1800); await c.evalJS(`(()=>{const inp=document.querySelector('.ant-select-selection-search-input')||document.querySelector('.ant-select input'); if(!inp)return 0; inp.focus(); Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set.call(inp,'AutoMaven'); inp.dispatchEvent(new Event('input',{bubbles:true})); return 1;})()`); await wait(2200); const r = await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null); if(!dd.length)return'no-dd'; const d=dd[dd.length-1]; const hit=[...d.querySelectorAll('.ant-select-item-option')].find(o=>/AutoMavenShop_MY/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 'picked';})()`); await wait(6000); const s = await cur(); if (/AutoMavenShop_MY/i.test(s)) return 'ok:' + s; log(' 店铺重试', attempt + 1, r, JSON.stringify(s)); } return 'fail'; } // 2. 分类 async function pickCategory(kw) { await c.evalJS(`(()=>{document.querySelectorAll('.ant-select-dropdown').forEach(d=>{if(d.offsetParent!==null)d.style.display='none';}); document.body.dispatchEvent(new MouseEvent('mousedown',{bubbles:true})); document.body.click(); return 1;})()`); await wait(1000); const open = 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[0].dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 'ok';})()`); if (open === 'nf') return 'no-btn'; await wait(3500); const r = await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop(); if(!m)return'no-modal'; const inp=m.querySelector('input[placeholder="搜索分类名称"]'); if(!inp)return'no-input'; inp.focus(); Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set.call(inp,${JSON.stringify(kw)}); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); const b=[...m.querySelectorAll('button')].find(x=>/^\\s*搜索\\s*$/.test(x.innerText||'')); if(b) for(const ev of ['mousedown','mouseup','click']) b.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 'searched';})()`); if (r !== 'searched') return r; await wait(4000); const pick = await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop(); if(!m)return'no-modal'; const its=[...m.querySelectorAll('.search-result-item')]; if(!its.length)return'no-result'; const BAD=/二手|Barang Terpakai|Second|Used|Pre-?owned|Refurb/i; const texts=its.map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim()); let idx=texts.findIndex(t=>!BAD.test(t)); if(idx<0) idx=0; const hit=its[idx]; for(const ev of ['mousedown','mouseup','click']) hit.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 'picked['+idx+'/'+texts.length+']:'+texts[idx];})()`); await wait(4500); return pick; } // 2b. 多关键词回退 + 失败即中止 async function pickCategoryAny(kws) { const tried = []; for (const kw of kws) { const r = String(await pickCategory(kw)); tried.push(kw + '\u2192' + r.slice(0, 45)); if (!/no-result|no-input|no-modal|no-btn/.test(r)) return { r, tried }; await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop(); if(m){const x=m.querySelector('.ant-modal-close'); if(x&&x.offsetParent!==null)x.click();} return 1;})()`); await wait(1500); } return { r: 'ALL-FAILED', tried }; } // 3. 直填 store const setStore = (js) => c.evalJS(js); log('=== TikTok 建品 ===', OID, IN.title.slice(0, 50)); log('1) 店铺:', await pickShop()); const kw = CAT_KW || IN.cat_kw || IN.title.split(/[ ,]+/).slice(0, 2).join(' '); const catRes = await pickCategoryAny([kw].concat(IN.cat_fb || [])); log('2) 分类:', SAN(String(catRes.r)).slice(0, 110), '| 尝试:', catRes.tried.join(' / ')); if (/ALL-FAILED|no-result|no-input|no-btn/.test(String(catRes.r))) { log('\u274c 分类未选中,跳过保存'); await c.shot('/tmp/tk_fail_' + OID + '.jpeg').catch(() => {}); process.exit(2); } // 4. 文案 const desc = IN.bullets.map(b => '• ' + b).join('
') + '

Keywords: ' + (IN.keywords || ''); const wr = await setStore(`(()=>{const p=document.querySelector('#app').__vue_app__.config.globalProperties.$pinia; const f=p._s.get('tiktokGlobalAddProductStore').$state.formState; f.productName=${JSON.stringify(IN.title)}; f.keyProductFeatures=${JSON.stringify(IN.bullets.slice(0, 5))}; f.searchTerms=${JSON.stringify((IN.keywords || '').split(',').map(s => s.trim()).filter(Boolean).slice(0, 10))}; f.description=''+${JSON.stringify(desc)}; return 'ok';})()`); log('3) 文案:', wr, '| 标长', IN.title.length); // 5. 变种属性 const vr = await c.evalJS(`(()=>{ const h=[...document.querySelectorAll('*')].filter(e=>e.offsetParent!==null&&/^\\s*变种属性\\s*$/.test((e.innerText||'').trim())).pop(); if(!h)return'no-sec'; let box=h; for(let i=0;i<6&&box.parentElement;i++){ box=box.parentElement; if(box.querySelector('input[class*="!w-200"]'))break; } const inp=box.querySelector('input[class*="!w-200"]'); if(!inp)return'no-inp'; inp.focus(); Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set.call(inp,${JSON.stringify(VAR_VAL)}); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); inp.dispatchEvent(new KeyboardEvent('keydown',{key:'Enter',code:'Enter',keyCode:13,which:13,bubbles:true})); const btn=[...box.querySelectorAll('button,a,span')].find(e=>/^\\s*(添加|Add)\\s*$/.test(e.innerText||'')); if(btn) for(const ev of ['mousedown','mouseup','click']) btn.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 'ok';})()`); log('4) 变种属性:', vr, VAR_VAL); await wait(3500); // 6. SKU 价格/库存 const SKU = 'AM-' + OID; const sr = await c.evalJS(`(()=>{ const setV=(el,v)=>{ Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set.call(el,String(v)); el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); el.dispatchEvent(new Event('blur',{bubbles:true})); }; const tbl=[...document.querySelectorAll('table')].filter(t=>t.offsetParent!==null).find(t=>[...t.querySelectorAll('th')].some(h=>/本地展示价/.test(h.innerText||''))); if(!tbl)return'no-table'; const ths=[...tbl.querySelectorAll('th')].map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim()); const rows=[...tbl.querySelectorAll('tbody tr')].filter(x=>x.querySelectorAll('input').length); const done=[]; rows.forEach((tr,ri)=>{ [...tr.querySelectorAll('td')].forEach((td,ci)=>{ const name=ths[ci]||''; const ins=[...td.querySelectorAll('input')]; if(/^SKU/i.test(name)&&ins[0]&&!ins[0].value){ setV(ins[0],${JSON.stringify(SKU)}+(rows.length>1?('-'+ri):'')); done.push('sku'); } if(/本地展示价/.test(name)&&ins[0]&&!ins[0].value){ setV(ins[0],${JSON.stringify(String(IN.price_myr))}); done.push('price'); } if(/^库存/.test(name)&&ins[0]&&!ins[0].value){ setV(ins[0],'999'); done.push('stock'); } }); }); return 'rows'+rows.length+':'+[...new Set(done)].join(',');})()`); log('5) SKU:', sr, '| MYR', IN.price_myr); await wait(2000); // 7. 重量 + 尺寸 const setItem = (rx, sel, 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||'');}); const el=it?it.querySelector(${JSON.stringify(sel)}):null; if(!el)return'nf'; 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';})()`); log('6) 重量:', await setItem('重量', 'input:not([type=hidden])', IN.weight)); const dim = 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||'');}); if(!it)return'nf'; const ins=[...it.querySelectorAll('input')]; const setV=(el,v)=>{ Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set.call(el,String(v)); el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); el.dispatchEvent(new Event('blur',{bubbles:true})); }; const L=ins.find(i=>i.placeholder==='长')||ins[0], W2=ins.find(i=>i.placeholder==='宽')||ins[1], H=ins.find(i=>i.placeholder==='高')||ins[2]; if(L)setV(L,${DIMS.L}); if(W2)setV(W2,${DIMS.Wi}); if(H)setV(H,${DIMS.H}); return 'ok:'+ins.length;})()`); log('7) 尺寸:', dim, JSON.stringify(DIMS)); // 8. 图片 const ir = await c.evalJS(`(()=>{const p=document.querySelector('#app').__vue_app__.config.globalProperties.$pinia; const st=p._s.get('tiktokGlobalImgStore').$state; const urls=${JSON.stringify(IN.imgs)}; st.formState.mainImage=urls.map((u,i)=>({url:u,imgUrl:u,name:'img'+(i+1)+'.jpg',width:800,height:800,checked:true})); return 'imgs:'+st.formState.mainImage.length;})()`); log('8) 图片:', ir, '(共', IN.imgs.length, ')'); await wait(2500); // 9. 来源URL log('9) 来源URL:', await setItem('来源URL', 'input:not([type=hidden])', 'https://detail.1688.com/offer/' + OID + '.html')); await wait(1200); // 10. 保存 const before = await c.evalJS(`(()=>{const p=document.querySelector('#app').__vue_app__.config.globalProperties.$pinia; const b=p._s.get('tiktokGlobalAddBasicStore').$state; return JSON.stringify({shopId:b.formState.shopId, cat:b.formState.categoryId, site:b.site});})()`); log('10) 保存前状态:', before); 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('11) 保存点击:', sv); await wait(9000); const msg = await c.evalJS(`(()=>{const ms=[...document.querySelectorAll('.ant-message-notice,.ant-notification-notice,.ant-modal')].filter(e=>e.offsetParent!==null) .map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,160)).filter(Boolean); return JSON.stringify({url:location.href, msgs:ms.slice(0,4)});})()`); log('12) 结果:', SAN(String(msg)).slice(0, 400)); await c.shot('/tmp/tk_pilot.jpeg').catch(() => {}); process.exit(0);