Files
dianxiaomi-crossborder-play…/alichoice建品/alcbatch.mjs
T

160 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* aliChoice(速卖通全托管)批量建品
*
* 用法: node alcbatch.mjs [--dry] [pid...]
*
* 顺序:店铺(下拉第一家) → 备货类型(国内履约) → 类目(弹窗) → 标题(subject)/主图/PC端描述
* → SKU 行 → 必填类目属性(品牌/高关注化学品/产地) → **物流属性(先保存一次触发报错,
* 点报错里的 icon_edit2 → 勾「普货」→ 确定)** → 再保存
*/
import { readFileSync, writeFileSync, appendFileSync } from 'fs';
import { newPage, closePage, wait, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { selectCategory, fillRequiredSelects } from '/home/admin9webs/shein建品/sh_build.mjs';
const DIR = '/home/admin9webs/alichoice建品';
const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`;
const INPUT = JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json', 'utf8'));
const DONE_F = `${DIR}/alc_done.json`;
const LOG_F = `${DIR}/alc_batch.log`;
const SKIP = new Set(['601964534137']); // 遥控器保护套:无干净图,全平台统一跳过
const argv = process.argv.slice(2);
const dry = argv.includes('--dry');
const only = argv.filter(a => !a.startsWith('--'));
let tasks = Object.keys(INPUT).filter(pid => (INPUT[pid].imgs || []).length > 0).map(pid => ({ pid }));
if (only.length) tasks = tasks.filter(t => only.includes(t.pid)); else tasks = tasks.filter(t => !SKIP.has(t.pid));
let done = {}; try { done = JSON.parse(readFileSync(DONE_F, 'utf8')); } catch {}
const log = m => { const l = `[${new Date().toISOString().slice(11, 19)}] ${m}`; console.log(l); appendFileSync(LOG_F, l + '\n'); };
/** PC端描述:真实控件是 CKEditor(隐藏 textarea + 所见即所得),给编辑器实例 setData 才有效 */
async function fillDescription(c, text) {
return c.evalJS(`(()=>{const p=${P};
const html=${JSON.stringify(text)};
let how='none';
try{ if(window.CKEDITOR&&window.CKEDITOR.instances){ const inst=Object.values(window.CKEDITOR.instances);
if(inst.length){ inst.forEach(x=>{ try{ x.setData(html); }catch(e){} }); how='ckeditor:'+inst.length; } } }catch(e){}
const d=p._s.get('smtChoiceAddStore').$state.describeInfoData;
if(d){ d.detail=html; if('mobileDetail' in d) d.mobileDetail=html; how+='|store'; }
if(how==='none'){ const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null)
.find(e=>/PC端描述/.test((e.querySelector('label')||{}).innerText||''));
const ta=f?f.querySelector('textarea'):null;
if(ta){ const setter=Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value').set;
setter.call(ta, html); ta.dispatchEvent(new Event('input',{bubbles:true})); how='textarea'; } }
return how;})()`);
}
async function pickFirstOption(c, labelRe) {
const p = JSON.parse(String(await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null)
.find(e=>new RegExp(${JSON.stringify(labelRe)}).test((e.querySelector('label')||{}).innerText||''));
const s=f?f.querySelector('.ant-select-selector'):null; 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 (!p.length) return 'no-select';
await c.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: p[0], y: p[1], buttons: 0 }); await wait(300);
for (const t of ['mousePressed', 'mouseReleased'])
await c.send('Input.dispatchMouseEvent', { type: t, x: p[0], y: p[1], button: 'left', clickCount: 1, buttons: 1 });
await wait(2000);
return c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
if(!dd) return 'no-dd'; const op=[...dd.querySelectorAll('.ant-select-item-option')].filter(e=>e.offsetParent!==null)[0];
if(!op) return 'no-op'; const r=op.getBoundingClientRect();
for(const ev of ['mousedown','mouseup','click']) op.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2}));
return 'picked:'+(op.innerText||'').trim().slice(0,16);})()`);
}
async function clickSave(c) {
await c.evalJS(`window.scrollTo(0,0)`); await wait(500);
await c.evalJS(`(()=>{const xs=[...document.querySelectorAll('button')].filter(e=>e.offsetParent!==null);
const b=xs.find(e=>/^\\s*保存\\s*$/.test((e.innerText||'').trim()))||xs.find(e=>/保存/.test(e.innerText||''));
if(b) for(const ev of ['mousedown','mouseup','click']) b.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
return !!b;})()`);
}
/** 物流属性:先保存一次逼出「请选择物流属性」,点它的编辑入口 → 勾「普货」→ 确定 */
async function fixLogistics(c) {
const pos = JSON.parse(String(await c.evalJS(`(()=>{
const bad=[...document.querySelectorAll('.ant-form-item-has-error')].find(e=>/物流属性/.test(e.innerText||''));
if(!bad) return '[]';
const link=bad.querySelector('.icon_edit2')||bad.querySelector('i.link');
if(!link) return '[]'; link.scrollIntoView({block:'center'});
const r=link.getBoundingClientRect(); return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`)));
if (!pos.length) return 'no-entry';
await c.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: pos[0], y: pos[1], buttons: 0 }); await wait(300);
for (const t of ['mousePressed', 'mouseReleased'])
await c.send('Input.dispatchMouseEvent', { type: t, x: pos[0], y: pos[1], button: 'left', clickCount: 1, buttons: 1 });
await wait(3500);
const tick = await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
if(!m) return 'no-modal';
const ws=[...m.querySelectorAll('.ant-checkbox-wrapper')];
const w=ws.find(x=>/普货/.test(x.innerText||''))||ws[0];
if(!w) return 'no-checkbox';
for(const ev of ['mousedown','mouseup','click']) w.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
const i=w.querySelector('input'); if(i&&!i.checked) i.click();
const ok=[...m.querySelectorAll('button')].find(x=>/^\\s*确定\\s*$/.test((x.innerText||'').trim()));
if(ok) for(const ev of ['mousedown','mouseup','click']) ok.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
return 'ticked:'+(w.innerText||'').trim().slice(0,10);})()`);
await wait(3000);
return tick;
}
log(`aliChoice 批量开始:任务 ${tasks.length},dry=${dry}`);
for (const t of tasks) {
if (!dry && done[t.pid] && done[t.pid].ok) { log(`跳过 ${t.pid}(已建)`); continue; }
const it = INPUT[t.pid]; if (!it) { log(`✗ ${t.pid} 无输入`); continue; }
log(`── ${t.pid} kw=${it.cat_kw || '?'} :: ${String(it.title).slice(0, 40)}`);
let c;
try {
c = await newPage('https://www.dianxiaomi.com/web/smtChoice/add');
await wait(12000);
await installHook(c);
log(` 店铺: ${await pickFirstOption(c, '店铺名称')}`);
await wait(6000);
log(` 备货类型: ${await pickFirstOption(c, '备货类型')}`);
await wait(3000);
const cat = await selectCategory(c, it.cat_kw || '发夹', it.cat_kw);
log(` 类目: ${String(cat).slice(0, 110)}`);
if (!String(cat).startsWith('path:')) throw new Error('选类目失败:' + cat);
await wait(6000);
const fill = await c.evalJS(`(()=>{const p=${P}; const imgs=${JSON.stringify((it.imgs || []).slice(0, 6))};
const mk=(u,i)=>({url:u,imgUrl:u,name:'img'+(i+1)+'.jpg',width:800,height:800});
const b=p._s.get('smtChoiceAddBasicStore').$state.formState; b.subject=${JSON.stringify(it.title)};
const pr=p._s.get('smtChoiceAddProductStore').$state.formState; pr.mainImage=imgs.map(mk);
const sd=p._s.get('smtChoiceSkuDataStore').$state.formState;
(sd.skuDataList||[]).forEach((r,i)=>{ r.skuPrice=String(${JSON.stringify(String(it.price_cny || '19.90'))});
r.skuCode='AC-'+${JSON.stringify(t.pid)}+'-'+(i+1); r.packageWeight='100';
r.packageLength='20'; r.packageWidth='15'; r.packageHeight='3'; });
const desc=p._s.get('smtChoiceAddStore').$state.describeInfoData;
if(desc&&!desc.detail) desc.detail=${JSON.stringify((it.bullets || []).join('<br/>'))};
return JSON.stringify({subject:b.subject.slice(0,16), imgs:pr.mainImage.length, rows:(sd.skuDataList||[]).length});})()`);
log(` 填充: ${fill}`);
await wait(3500);
const attrs = await fillRequiredSelects(c);
if (attrs !== 'none') log(` 必填属性: ${String(attrs).slice(0, 150)}`);
await wait(1200);
log(` PC端描述: ${await fillDescription(c, (it.bullets || []).join('<br/>') || it.title)}`);
await wait(1500);
if (dry) { delete done[t.pid]; log(' ✓ dry 构建完成(未保存)'); await closePage(c); continue; }
// 保存 → 若报「请选择物流属性」就补上 → 再保存
await clickSave(c);
await wait(11000);
let mods = String(await c.evalJS(`JSON.stringify([...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,110)))`));
if (!/已保存|保存成功|待发布/.test(mods)) {
const lg = await fixLogistics(c);
log(` 物流属性: ${lg}`);
await clickSave(c);
await wait(12000);
mods = String(await c.evalJS(`JSON.stringify([...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,110)))`));
}
const cap = await readHook(c);
const ok = /已保存|保存成功|待发布/.test(mods);
done[t.pid] = ok ? { ok: true } : { ok: false, mods: mods.slice(0, 160), hook: String(cap).slice(-240) };
log(ok ? ' ✓ 已保存到待发布' : ` ✗ 失败 ${mods.slice(0, 110)} | ${String(cap).slice(-170)}`);
} catch (e) {
done[t.pid] = { ok: false, msg: String(e).slice(0, 240) };
log(` ✗ 异常: ${String(e).slice(0, 220)}`);
} finally {
if (!dry) writeFileSync(DONE_F, JSON.stringify(done, null, 1));
if (c) await closePage(c);
await wait(2500);
}
}
log(`aliChoice 批量结束:成功 ${Object.values(done).filter(x => x.ok).length}/${tasks.length}`);