- TikTok/Shopee/速卖通/Walmart 各 46-47 条已在待发布 - Ozon 47 条已建、改价 40/47 - SHEIN 配方跑通第一条;Temu 差「预览图」一步 - 核心产出:MYJ.message.error 钩子诊断法(解开了三个平台的保存静默拦截) - 含下一轮提示词 NEXT-ROUND-PROMPT.md
101 lines
6.0 KiB
JavaScript
101 lines
6.0 KiB
JavaScript
import { newTarget, CDP, SAN } from '/tmp/cdp3.mjs';
|
|
import fs from 'fs';
|
|
|
|
/* ── Ozon 改价(≤500g 档,售价 = 合计成本 ÷ (1 − 20% − 利润率))──
|
|
node ozprice_set.mjs <pid> <priceCNY> [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 <pid> <priceCNY> [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<tbls.length;ti++){
|
|
const ths=[...tbls[ti].querySelectorAll('th')].map(e=>(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);
|