SHEIN 40/40 + Temu 全托管 46/46 批量进待发布;补 shbatch/tmbatch 配方、类目弹窗必须关、SHEIN 规格上限3、必填属性兜底、EAN 游标落盘
This commit is contained in:
1 parent
cdde45ed9a
commit
fd4cc6619b
16 files changed
+1260
-41
No files matched your search
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* SHEIN 批量建品(40 条,按 shein_route.json 分 4 家店)
|
||||
*
|
||||
* 用法:
|
||||
* node shbatch.mjs # 跑全部未完成的
|
||||
* node shbatch.mjs 661190598139 # 只跑指定商品
|
||||
* node shbatch.mjs --dry 661190598139 # 只构建不保存(验证用)
|
||||
*
|
||||
* 顺序(硬约束):选店铺 → 选类目 → 标题 → **加规格 → 选规格值** → 填 SKU 行 → 尺码表 → 图片 → 保存
|
||||
* 加规格会重建 SKU 行,所以 SKU 表必须在规格之后填。
|
||||
*/
|
||||
import { readFileSync, writeFileSync, appendFileSync, existsSync } from 'fs';
|
||||
import { newPage, closePage, wait, installHook, readHook } from './sh_lib.mjs';
|
||||
import { selectShop, selectCategory, fillTitle, addSpecs, pickSpecValues, fillSkuRows, fillSizeTable, fillImages, fillRequiredSelects } from './sh_build.mjs';
|
||||
import { P } from './sh_lib.mjs';
|
||||
|
||||
const DIR = '/home/admin9webs/shein建品';
|
||||
const INPUT = JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json', 'utf8'));
|
||||
const ROUTE = JSON.parse(readFileSync(`${DIR}/shein_route.json`, 'utf8'));
|
||||
const DONE_F = `${DIR}/sh_done.json`;
|
||||
const LOG_F = `${DIR}/sh_batch.log`;
|
||||
const TASKS = [];
|
||||
for (const [group, cfg] of Object.entries(ROUTE))
|
||||
for (const [pid, kw] of Object.entries(cfg.kw)) TASKS.push({ pid, kw, shop: cfg.shop, group });
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const dry = argv.includes('--dry');
|
||||
const only = argv.filter(a => !a.startsWith('--'));
|
||||
const list = only.length ? TASKS.filter(t => only.includes(t.pid)) : TASKS;
|
||||
let done = {};
|
||||
try { done = JSON.parse(readFileSync(DONE_F, 'utf8')); } catch {}
|
||||
const log = m => { const line = `[${new Date().toISOString().slice(11, 19)}] ${m}`; console.log(line); appendFileSync(LOG_F, line + '\n'); };
|
||||
|
||||
log(`SHEIN 批量开始:任务 ${list.length} 条(总 ${TASKS.length}),dry=${dry}`);
|
||||
|
||||
for (const t of list) {
|
||||
if (!dry && done[t.pid] && done[t.pid].ok) { log(`跳过 ${t.pid}(已建)`); continue; }
|
||||
const it = INPUT[t.pid];
|
||||
if (!it) { log(`✗ ${t.pid} 无输入数据`); done[t.pid] = { ok: false, msg: 'no-input' }; continue; }
|
||||
log(`── ${t.pid} [${t.group}] ${t.shop} / ${t.kw} :: ${String(it.title).slice(0, 40)}`);
|
||||
let c;
|
||||
try {
|
||||
c = await newPage('https://www.dianxiaomi.com/web/sheinProduct/add');
|
||||
await wait(11000);
|
||||
await installHook(c);
|
||||
const shopName = process.env.SHOP_OVERRIDE || t.shop; // ①Shein-武汉九个网-箱包 报「无可用的发品额度」,可临时换 ② 验证
|
||||
const shop = await selectShop(c, shopName);
|
||||
if (shop !== `picked:${shopName}`) throw new Error('选店铺失败:' + shop);
|
||||
const cat = await selectCategory(c, t.kw);
|
||||
if (!String(cat).startsWith('path:')) throw new Error('选类目失败:' + cat);
|
||||
await fillTitle(c, it.title);
|
||||
await wait(1500);
|
||||
const specs = await addSpecs(c);
|
||||
const vals = await pickSpecValues(c);
|
||||
log(` 规格: ${specs} / 值: ${vals}`);
|
||||
const st = await c.evalJS(`(()=>{const p=${P};
|
||||
const f=p._s.get('sheinAddBasicStore').$state.formState;
|
||||
const d=p._s.get('sheinSkuDataStore').$state.formState;
|
||||
const a=p._s.get('sheinSkuAttrStore').$state.formState;
|
||||
return JSON.stringify({cat:f.categoryId, rows:(d.skuDataList||[]).length,
|
||||
attrs:(a.skuAttrs||[]).map(x=>x.name+':'+((x.options||x._options||[]).length))});})()`);
|
||||
log(` 状态: ${st}`);
|
||||
if (/"rows":0/.test(String(st))) throw new Error('SKU 行为 0(规格值没选中)');
|
||||
await fillSkuRows(c, { prefix: 'SHEIN', price: String(it.price_cny || '12.90'), supply: String(it.cost || '8.50') });
|
||||
await wait(2500);
|
||||
const sz = await fillSizeTable(c);
|
||||
log(` 尺码表: ${sz}`);
|
||||
const im = await fillImages(c, (it.imgs || []).slice(0, 6));
|
||||
log(` 图片: ${im}`);
|
||||
const attrs = await fillRequiredSelects(c); // 类目相关的必填属性(如箱包的「特征」)
|
||||
if (attrs !== 'none') log(` 必填属性: ${attrs}`);
|
||||
if (dry) { log(` ✓ dry 构建完成(未保存)`); delete done[t.pid]; await closePage(c); continue; }
|
||||
|
||||
// 保存
|
||||
await c.evalJS(`window.scrollTo(0,0)`); await wait(600);
|
||||
await c.evalJS(`(()=>{const x=[...document.querySelectorAll('button')].filter(e=>e.offsetParent!==null).find(e=>/^\\s*保存\\s*$/.test((e.innerText||'').trim()));
|
||||
if(x) for(const ev of ['mousedown','mouseup','click']) x.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 1;})()`);
|
||||
await wait(13000);
|
||||
const cap = await readHook(c);
|
||||
const info = await c.evalJS(`(()=>{
|
||||
const mods=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,160));
|
||||
const ex=[...document.querySelectorAll('.ant-form-item-explain-error')].map(e=>(e.innerText||'').trim()).filter(Boolean);
|
||||
return JSON.stringify({href:location.href, mods, ex:[...new Set(ex)].slice(0,5)});})()`);
|
||||
const ok = /已保存|保存成功|待发布/.test(String(info));
|
||||
if (!ok) {
|
||||
// 有些情况会弹「确定」确认框
|
||||
await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
|
||||
if(m){const b=[...m.querySelectorAll('button')].find(x=>/确定|确认/.test(x.innerText||'')); if(b)b.click();} return 1;})()`);
|
||||
await wait(5000);
|
||||
}
|
||||
const info2 = await c.evalJS(`(()=>{const mods=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,160));
|
||||
return JSON.stringify({href:location.href, mods});})()`);
|
||||
const ok2 = ok || /已保存|保存成功|待发布/.test(String(info2));
|
||||
done[t.pid] = ok2 ? { ok: true, msg: 'saved' } : { ok: false, msg: String(cap).slice(-260), info: String(info).slice(0, 200) };
|
||||
log(ok2 ? ` ✓ 已保存到待发布` : ` ✗ 保存失败 | ${String(info).slice(0, 220)} | hook=${String(cap).slice(-220)}`);
|
||||
} catch (e) {
|
||||
done[t.pid] = { ok: false, msg: String(e).slice(0, 300) };
|
||||
log(` ✗ 异常: ${String(e).slice(0, 260)}`);
|
||||
} finally {
|
||||
if (!dry) writeFileSync(DONE_F, JSON.stringify(done, null, 1));
|
||||
if (c) await closePage(c);
|
||||
await wait(2500);
|
||||
}
|
||||
}
|
||||
const okN = Object.values(done).filter(x => x.ok).length;
|
||||
log(`SHEIN 批量结束:累计成功 ${okN} / ${TASKS.length}`);
|
||||
Reference in new issue
Block a user