import { newTarget, CDP, SAN } from '/tmp/cdp3.mjs'; import fs from 'fs'; /* ── Ozon 改价(≤500g 档,售价 = 合计成本 ÷ (1 − 20% − 利润率))── node ozprice_set.mjs [dry] dry = 只填不保存 */ const PID = process.argv[2]; const PRICE = process.argv[3]; const DRY = process.argv[4] === 'dry'; if (!PID || !PRICE) { console.log('用法: node ozprice_set.mjs [dry]'); process.exit(1); } 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' && x.url.includes('ozonProduct/edit'))) { await fetch(`http://127.0.0.1:9223/json/close/${t.id}`).catch(() => {}); k++; } if (k) { log(`清理残留编辑页 ${k} 个`); await wait(1100); } 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/ozonProduct/edit?id=${PID}` }); await wait(32000); for (let i = 0; i < 8; i++) { const x = await c.evalJS(`(()=>{const ms=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null); if(!ms.length)return'n'; ms.forEach(m=>{const cb=m.querySelector('.ant-modal-close'); if(cb&&cb.offsetParent!==null)cb.click();}); return 'c';})()`); if (x === 'n') break; await wait(1000); } await wait(2000); // 1. 找 SKU 表 + 价格列 const map = await c.evalJS(`(()=>{ const tbls=[...document.querySelectorAll('table')].filter(t=>t.offsetParent!==null); for(let ti=0;ti(e.innerText||'').replace(/\\s+/g,' ').trim()); if(!ths.some(h=>/价格|售价|₽|RUB/i.test(h))) continue; const rows=[...tbls[ti].querySelectorAll('tbody tr')].filter(x=>x.querySelectorAll('input').length); if(!rows.length) continue; return JSON.stringify({ti, ths, rowCount: rows.length, vals: rows.slice(0,3).map(tr=>[...tr.querySelectorAll('input')].map(i=>(i.value||'').slice(0,12)))}); } return 'no-table';})()`); log('SKU 表:', SAN(String(map)).slice(0, 500)); if (map === 'no-table') { log('❌ 找不到价格表'); process.exit(1); } const M = JSON.parse(String(map)); const IDX_PRICE = M.ths.findIndex(h => /价格|售价|₽|RUB/i.test(h) && !/划线|原价|市场价|最低/i.test(h)); const IDX_OLD = M.ths.findIndex(h => /划线|原价|市场价/i.test(h)); const OLD_PRICE = (Math.ceil(Number(PRICE) * 1.2 * 100) / 100).toFixed(2); log('价格列:', IDX_PRICE, JSON.stringify(M.ths[IDX_PRICE]), '| 原价列:', IDX_OLD, IDX_OLD >= 0 ? JSON.stringify(M.ths[IDX_OLD]) : '(无)', '→ 原价设为', OLD_PRICE); // 2. 填价 const r = 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 tbls=[...document.querySelectorAll('table')].filter(t=>t.offsetParent!==null); const tbl=tbls[${M.ti}]; if(!tbl)return'no-table'; const rows=[...tbl.querySelectorAll('tbody tr')].filter(x=>x.querySelectorAll('input').length); let n=0; rows.forEach(tr=>{ const tds=[...tr.querySelectorAll('td')]; const td=tds[${IDX_PRICE}]; if(!td)return; const inp=td.querySelector('input'); if(!inp)return; setV(inp,${JSON.stringify(PRICE)}); n++; const od=tds[${IDX_OLD}]; if(od){ const oi=od.querySelector('input'); if(oi) setV(oi, ${JSON.stringify(OLD_PRICE)}); } }); return 'set:'+n+'/'+rows.length;})()`); log('填价:', r); await wait(2500); // 3. 回读 const back = await c.evalJS(`(()=>{const tbls=[...document.querySelectorAll('table')].filter(t=>t.offsetParent!==null); const tbl=tbls[${M.ti}]; const rows=[...tbl.querySelectorAll('tbody tr')].filter(x=>x.querySelectorAll('input').length); return JSON.stringify(rows.slice(0,5).map(tr=>[...tr.querySelectorAll('td')].map(td=>{const i=td.querySelector('input'); return i?i.value:'';})));})()`); log('回读:', SAN(String(back)).slice(0, 300)); if (DRY) { log('dry-run,不保存'); process.exit(0); } // 4. 保存(优先「保存并移入待发布」,与 fixone3 一致) await c.evalJS(`window.scrollTo(0,0)`); await wait(700); const reqs = []; c.on(m => { if (m.method === 'Network.requestWillBeSent') { const u = m.params.request.url; if (/ozonProduct\/add\.json/.test(u)) reqs.push(m.params.request.method); } }); await c.send('Network.enable').catch(()=>{}); const sv = await c.evalJS(`(()=>{const xs=[...document.querySelectorAll('button')].filter(e=>e.offsetParent!==null); let x=xs.find(e=>/保存并移入待发布/.test((e.innerText||'').trim())); let tag='保存并移入待发布'; if(!x){ x=xs.find(e=>/^保\\s*存$/.test((e.innerText||'').trim())); tag='保存'; } if(!x) return 'nf'; for(const ev of ['mousedown','mouseup','click']) x.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return tag;})()`); log('保存按钮:', sv); await wait(13000); await c.evalJS(`(()=>{const ms=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null); for(const m of ms){const b=[...m.querySelectorAll('button')].find(x=>/确定|确认/.test(x.innerText||'')); if(b)b.click();} return 1;})()`); await wait(5000); log('保存请求:', JSON.stringify(reqs)); // 5. API 回读(权威) const back2 = await c.evalJS(`(async()=>{const rr=await fetch('/api/ozonProduct/add.json?id=${PID}&_t='+Date.now(),{headers:{'X-Requested-With':'XMLHttpRequest','Cache-Control':'no-cache'}}); const tx=await rr.text(); if(!tx.startsWith('{'))return'(err)'; const p=(JSON.parse(tx).data||{}).product||{}; const vl=p.variantList||[]; return JSON.stringify({n:vl.length, prices:vl.slice(0,3).map(v=>v.price), sales:vl.slice(0,3).map(v=>v.salePrice)});})()`, true); log('★ API 回读:', SAN(String(back2)).slice(0, 260)); process.exit(0);