From fd4cc6619b26b5853359ee610058774822a5a2f1 Mon Sep 17 00:00:00 2001 From: admin9webs Date: Sun, 20 Sep 2026 19:48:59 +0800 Subject: [PATCH] =?UTF-8?q?SHEIN=2040/40=20+=20Temu=20=E5=85=A8=E6=89=98?= =?UTF-8?q?=E7=AE=A1=2046/46=20=E6=89=B9=E9=87=8F=E8=BF=9B=E5=BE=85?= =?UTF-8?q?=E5=8F=91=E5=B8=83=EF=BC=9B=E8=A1=A5=20shbatch/tmbatch=20?= =?UTF-8?q?=E9=85=8D=E6=96=B9=E3=80=81=E7=B1=BB=E7=9B=AE=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=E5=BF=85=E9=A1=BB=E5=85=B3=E3=80=81SHEIN=20=E8=A7=84=E6=A0=BC?= =?UTF-8?q?=E4=B8=8A=E9=99=903=E3=80=81=E5=BF=85=E5=A1=AB=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E5=85=9C=E5=BA=95=E3=80=81EAN=20=E6=B8=B8=E6=A0=87?= =?UTF-8?q?=E8=90=BD=E7=9B=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 24 +++- shein建品/RECIPE.md | 32 +++++ shein建品/sh_build.mjs | 259 +++++++++++++++++++++++++++++++++++++ shein建品/sh_diag.mjs | 38 ++++++ shein建品/sh_done.json | 162 +++++++++++++++++++++++ shein建品/sh_lib.mjs | 38 ++++++ shein建品/sh_verify.mjs | 13 ++ shein建品/shbatch.mjs | 106 +++++++++++++++ temu建品/STATUS.md | 30 +++++ temu建品/ean_cursor.json | 4 + temu建品/ean_temu_used.txt | 49 +++++++ temu建品/tm_count.mjs | 12 ++ temu建品/tm_done.json | 212 ++++++++++++++++++++++++++++++ temu建品/tm_fixean2.mjs | 40 ++++++ temu建品/tmbatch.mjs | 201 ++++++++++++++++++++++++++++ temu建品/tmpv.mjs | 81 +++++++----- 16 files changed, 1260 insertions(+), 41 deletions(-) create mode 100644 shein建品/sh_build.mjs create mode 100644 shein建品/sh_diag.mjs create mode 100644 shein建品/sh_done.json create mode 100644 shein建品/sh_lib.mjs create mode 100644 shein建品/sh_verify.mjs create mode 100644 shein建品/shbatch.mjs create mode 100644 temu建品/ean_cursor.json create mode 100644 temu建品/ean_temu_used.txt create mode 100644 temu建品/tm_count.mjs create mode 100644 temu建品/tm_done.json create mode 100644 temu建品/tm_fixean2.mjs create mode 100644 temu建品/tmbatch.mjs diff --git a/README.md b/README.md index f7f9fd4..3c4afd4 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ | 速卖通 | 4024039 | **47** | 完成(含清理 4 份重复) | | Walmart | Snowmountain | **46** | 完成 | | Ozon | Yejet | **47** | 已建;改价 40/47 | -| SHEIN | 4 家分品类 | 0 | 配方跑通第一条,40 条待批量 | -| Temu 全托管 | ①Alimingpro | **1** | 配方跑通首条(预览图解了),待批量 | +| SHEIN | 4 家分品类 | **40** | 完成(`shbatch.mjs` 自动批量;1 条因 ①箱包店发品额度用尽改挂 ②店) | +| Temu 全托管 | ①Alimingpro | **47** | 完成(`tmbatch.mjs` 批量 46 + 手工首条;预览图已解) | > `601964534137`(遥控器保护套)18 张图全带水印、无干净图,所有平台统一跳过。 @@ -56,8 +56,8 @@ function wrap(o, n, l) { | Shopee | `shopee建品/` | 用**单件模式**绕开变种;价格 **CNY**;列表字段是 `itemName` 不是 title | | Walmart | `walmart建品/` | 保存体是 **zip 的 multipart**;Brand 必须点「无品牌」;图片必须 **hover** 开下拉走「网络图片」 | | 速卖通 | `速卖通建品/` | `check_box` 属性值**必须是数组**;`idStr ≠ id` | -| SHEIN | `shein建品/` | 店铺**锁品类**;类目要**两个规格**;**加规格会重建 SKU 行** | -| Temu | `temu建品/` | 类目 1038 个从 store 直读;规格是 `.theme-value-add`(与 SHEIN 不同构);**预览图要逐行点单元格** → 引用产品轮播图 → 全部 → 选择 | +| SHEIN | `shein建品/` | 店铺**锁品类**;**一个产品最多 3 个规格**(加满就停);**加规格会重建 SKU 行**;类目动态必填属性(`.attr-label.required`)不填只报「产品信息中有错误」 | +| Temu | `temu建品/` | 类目 1038 个从 store 直读;规格是 `.theme-value-add`(**必须手动输值再点「添加」**,与 SHEIN 不同构);主图**必须满 5 张**;**预览图逐行点单元格** → 引用产品轮播图 → 全部 → 选择 | ## 通用技术事实 @@ -70,7 +70,7 @@ function wrap(o, n, l) { - CDP helper:`cdp3.mjs`(`newTarget`/`attach`/`CDP`/`SAN`),ESM,**不能用 require** - VNC:`http://192.168.9.122:6097/vnc.html` 或 `:6100`(免密) -## 十一条会重复踩的坑 +## 十五条会重复踩的坑 1. **保存静默拦截** → 挂钩子,别猜 2. **`idStr ≠ id`** → 大整数精度丢失,服务端操作一律用 `idStr` @@ -84,6 +84,13 @@ function wrap(o, n, l) { 10. `pkill -f ` 会自杀 → 用 `[.]` 技巧(`pgrep -f 'foo[.]sh'`) 11. **SKU 表里「一格一图」的弹窗是单格的** → 勾「全部」只作用当前行,N 行要走 N 遍 (Temu 预览图就是这样:点 `.sku-image` 单元格 → 引用产品轮播图 → 全部 → 选择) +12. **选了类目别忘了确认弹窗关了** → 弹窗还开着时后面的点击全被挡,最后只会看到 + 「请上传预览图」这种牛头不对马嘴的报错(Temu 有 13 条就是这么挂的) +13. **SHEIN 规格上限 3 个** → 主题列表里看着能全点,点第 4 个就报「规格最多选择3个」 +14. **类目动态必填属性**(`.attr-label.required`,如下拉、勾选组)不填不给具体提示, + 只报「产品信息中有错误,请检查」→ 兜底:空的下拉选第一项、勾选组勾第一个 +15. **EAN 游标必须落盘** → 计数器只存内存,换进程重跑就会从起点重发,必撞号 + (Temu 第一轮就撞了一次,靠编辑页改号救回来) ## EAN-13 台账(湖北易千亿彩 · 69803093 段) @@ -93,6 +100,9 @@ function wrap(o, n, l) { - 台账唯一来源:**S03 主机 `/root/ean_*.txt`**(KVM-2 够不到) - 已用 3190,下一号 pc 3400 → `6980309334003` - **本批已分配**:Walmart pc **3400~3446**(见 `walmart建品/ean_69803093_3400-3446.txt`); - Temu 拟用 pc **3447+** -- ⚠️ **必须在 S03 落盘**,否则下次续号会撞号 + Temu 全托管 pc **3447~3551**(46 条,逐条见 `temu建品/ean_temu_used.txt`; + 游标 `temu建品/ean_cursor.json` → 下一号 pc **3552**) +- ⚠️ **必须在 S03 落盘**,否则下次续号会撞号。Temu 这批目前只落在 KVM-2 + (`temu建品/ean_temu_used.txt`),**还没回 S03** —— 下一轮开工第一件事就把它并进 + `/root/ean_*.txt` - 说明书 §1 的例子 `pc 1000 → 6980309310004` **是错的**,正确是 `6980309310007` diff --git a/shein建品/RECIPE.md b/shein建品/RECIPE.md index c533e9d..ede4427 100644 --- a/shein建品/RECIPE.md +++ b/shein建品/RECIPE.md @@ -23,3 +23,35 @@ ## 待批量 - 40 条按品类分到 4 家店(美妆18 / 文具13 / 箱包8 / 内睡袜子1) - 需要把上述流程写成 `shbatch.mjs`,逐条跑 + +--- + +# 批量(已完成:40/40 进待发布,2026-09-20) + +`node shbatch.mjs`(全部)· `node shbatch.mjs `(单条)· `node shbatch.mjs --dry `(只构建不保存) + +- 数据源:`/home/admin9webs/tiktok建品/tk_input.json`(48 商品:title/imgs/cost/price_cny) +- 路由:`shein_route.json`(4 家店 × 每条的类目关键词),`sh_done.json` 记逐条结果,可断点续跑 +- 公共库:`sh_lib.mjs`(newPage/closePage/wait/installHook/readHook)· `sh_build.mjs`(各步骤函数) + +## 单条固定顺序(不能换) +`选店铺 → 选类目 → 标题 → 加规格(≤3) → 选规格值 → 填 SKU 行 → 尺码表 → 图片 → 填必填属性 → 保存` + +## 这次新踩的坑 +1. **选类目弹窗必须确认关掉**:`搜索` → 点 `.search-result-item`(它里面嵌 ``, + 所以别用 `children.length===0` 过滤)→ 若弹窗还在就点「选择/确定」或右上角 ×、按 Esc。 + 弹窗没关时后面的点击全被挡,最终报的却是别的错(Temu 那边表现为「请上传预览图」)。 +2. **SHEIN 一个产品最多 3 个规格**(颜色已占 1 个)→ `addSpecs` 里加满 3 个就停, + 否则报「规格最多选择3个,请移除后再添加」。 +3. **规格要去重**:按标签去重,否则会点出「款式/款式」两个同名规格。 +4. **类目动态必填属性**用 `.attr-label.required` 标记(不是 `.ant-form-item-required`): + - 下拉型空着 → 选第一项 + - 勾选组(如箱包的「特征 Features」,`.checkbox-group-with-search`)一个都没勾 → 勾第一个 + - 不填只会得到一句「产品信息中有错误,请检查」 +5. **店铺发品额度**:①Shein-武汉九个网-箱包 报「商家无可用的发品额度,暂时无法发布商品」, + 把那条改挂 ②Shein-易世通达-箱包(`SHOP_OVERRIDE='②Shein-易世通达-箱包'`)后成功。 + 该类目在 ② 店只有「不保温午餐包」,与路由表写的「保温午餐包」不完全一致。 + +## 结果 +40 条全部「已保存到待发布」(39 条在路由指定店铺 + 1 条改挂 ② 店)。 +店小秘 SHEIN 待发布列表核对:共 44 条(含此前手工首条等)。 diff --git a/shein建品/sh_build.mjs b/shein建品/sh_build.mjs new file mode 100644 index 0000000..a3d366b --- /dev/null +++ b/shein建品/sh_build.mjs @@ -0,0 +1,259 @@ +/** SHEIN 建品:单条产品的构建函数集(逐步验证中) */ +import { P, wait } from './sh_lib.mjs'; + +export async function mouseClick(c, x, y) { + await c.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y, buttons: 0 }); await wait(250); + for (const t of ['mousePressed', 'mouseReleased']) + await c.send('Input.dispatchMouseEvent', { type: t, x, y, button: 'left', clickCount: 1, buttons: 1 }); +} +async function centerOf(c, expr) { + return JSON.parse(String(await c.evalJS(`(()=>{const e=${expr}; if(!e) return '[]'; + e.scrollIntoView({block:'center'}); const r=e.getBoundingClientRect(); + return JSON.stringify([Math.round(r.x+r.width/2), Math.round(r.y+r.height/2)]);})()`))); +} + +/** 选店铺账号(下拉里按名字精确匹配) */ +export async function selectShop(c, shopName) { + const pos = await centerOf(c, `[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺账号/.test((e.querySelector('label')||{}).innerText||''))?.querySelector('.ant-select-selector')`); + if (!pos.length) return 'no-select'; + await mouseClick(c, pos[0], pos[1]); + await wait(2200); + const hit = await c.evalJS(`(()=>{ + const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop(); + if(!dd) return 'no-dd'; + const it=[...dd.querySelectorAll('.ant-select-item-option')].find(e=>(e.innerText||'').replace(/\\s+/g,'').includes(${JSON.stringify(shopName.replace(/\s+/g, ''))})); + if(!it) return 'no-match'; + const r=it.getBoundingClientRect(); + for(const ev of ['mousedown','mouseup','click']) it.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2})); + return 'picked:'+(it.innerText||'').trim().slice(0,24);})()`); + await wait(4500); + return hit; +} + +/** 选产品分类(弹窗搜索关键词 → 点第一个结果 → 确定) */ +export async function selectCategory(c, kw, preferName) { + const clicked = await c.evalJS(`(()=>{ + const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/产品分类/.test((e.querySelector('label')||{}).innerText||'')); + if(!it) return 'no-item'; + const b=[...it.querySelectorAll('*')].find(e=>(e.innerText||'').trim()==='选择分类'&&e.children.length===0); + if(!b) return 'no-btn'; + for(const ev of ['mousedown','mouseup','click']) b.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); + return 'opened';})()`); + if (clicked !== 'opened') return clicked; + await wait(3000); + const typed = 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="搜索分类名称"]')||m.querySelector('input'); + if(!inp) return 'no-input'; + const setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; + setter.call(inp, ${JSON.stringify(kw)}); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true})); + return 'typed';})()`); + if (typed !== 'typed') return typed; + await wait(800); + // 2) 点「搜索」→ 结果是一条红字完整类目路径,点它才算选中 + await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop(); + const b=[...m.querySelectorAll('button')].find(x=>/^\\s*搜索\\s*$/.test((x.innerText||'').trim())); + if(b) for(const ev of ['mousedown','mouseup','click']) b.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 1;})()`); + await wait(3200); + let picked = 'no-path'; + for (let attempt = 0; attempt < 5; attempt++) { + picked = await c.evalJS(`(()=>{ + const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop(); + if(!m) return 'no-modal2'; + // 搜索结果:.search-result-item(里面通常还嵌 最后一级,所以别按 children.length===0 过滤) + const items=[...m.querySelectorAll('.search-result-item')].filter(e=>e.offsetParent!==null); + const want=${JSON.stringify(preferName || '')}; + const seg=e=>{const t=(e.innerText||'').replace(/\s+/g,''); const parts=t.split('/'); return ((parts[parts.length-1]||'').split('(')[0]);}; + const it=(want && items.find(e=>seg(e)===want)) || (want && items.find(e=>(e.innerText||'').replace(/\s+/g,'').includes(want))) || items[0]; + if(!it) return 'no-path'; + const r=it.getBoundingClientRect(); + for(const ev of ['mousedown','mouseup','click']) it.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2})); + return 'path:'+(it.innerText||'').replace(/\\s+/g,' ').trim().slice(0,60);})()`); + if (String(picked).startsWith('path:')) break; + if (picked === 'no-modal2') break; + await wait(2000); + } + await wait(1800); + // 3) 弹窗必须关掉,否则后面的点击全被挡(曾出现「类目已选但弹窗还开着 → 预览图挂不上」) + let left = Number(await c.evalJS(`[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).length`)); + for (let k = 0; k < 3 && left; k++) { + await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop(); + if(!m) return 'closed'; + const b=[...m.querySelectorAll('button')].find(x=>/^\\s*(确定|选择)\\s*$/.test((x.innerText||'').trim())); + if(b) for(const ev of ['mousedown','mouseup','click']) b.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); + const c2=[...m.querySelectorAll('.ant-modal-close')][0]; if(c2) c2.click(); + return b?'ok':'still-open';})()`); + await wait(1500); + if (k >= 1) await c.send('Input.dispatchKeyEvent', {type:'keyDown', key:'Escape', code:'Escape', windowsVirtualKeyCode:27}).catch(()=>{}); + left = Number(await c.evalJS(`[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).length`)); + } + await wait(3000); + left = Number(await c.evalJS(`[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).length`)); + return picked + (left ? ' / MODAL-STILL-OPEN' : ''); +} + +export async function fillTitle(c, title) { + return c.evalJS(`(()=>{const p=${P}; + const f=p._s.get('sheinAddProductStore').$state.formState; f.productName=${JSON.stringify(title)}; + return 'ok';})()`); +} + +/** 逐个点开主题里还没启用的规格(颜色默认已启用,parent 带 disabled 类)。 + * 按标签去重:点开一个后同类主题可能还留在 DOM 里,重复点会多出「款式/款式」这种重复规格。 */ +export async function addSpecs(c, max = 4, maxTotal = 3) { + const added = [], seen = new Set(); + for (let i = 0; i < max; i++) { + // SHEIN 硬限制:一个产品最多 3 个规格(颜色已占 1 个)。加满就停,否则报「规格最多选择3个」。 + const boxes = Number(await c.evalJS(`[...document.querySelectorAll('.sku-info-box')].filter(e=>e.offsetParent!==null).length`)); + if (boxes >= maxTotal) break; + const r = await c.evalJS(`(()=>{ + const items=[...document.querySelectorAll('.theme-select-item')].filter(e=>e.offsetParent!==null&&!/disabled/.test(String(e.className))); + const out=items.map(it=>{const t=[...it.querySelectorAll('*')].find(x=>x.children.length===0&&(x.innerText||'').trim()); return t?((t.innerText||'').trim().slice(0,16)):'';}).filter(Boolean); + return JSON.stringify(out);})()`); + const cands = JSON.parse(String(r || '[]')).filter(x => !seen.has(x)); + if (!cands.length) break; + const label = cands[0]; + await c.evalJS(`(()=>{ + const items=[...document.querySelectorAll('.theme-select-item')].filter(e=>e.offsetParent!==null&&!/disabled/.test(String(e.className))); + const it=items.map(it=>{const t=[...it.querySelectorAll('*')].find(x=>x.children.length===0&&(x.innerText||'').trim()); return {it, t, label:t?(t.innerText||'').trim().slice(0,16):''};}) + .find(o=>o.label===${JSON.stringify(label)}); + if(!it) return 'nf'; + for(const ev of ['mousedown','mouseup','click']) it.t.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); + return 'ok';})()`); + seen.add(label); + added.push(label); + await wait(5000); + } + return added.join(' | '); +} + +/** 每个规格组各勾第一个值(加规格会重建 SKU 行,必须在选完值之后再填 SKU 表) */ +export async function pickSpecValues(c) { + const r = await c.evalJS(`(()=>{const boxes=[...document.querySelectorAll('.sku-info-box')].filter(e=>e.offsetParent!==null); + const done=[]; + boxes.forEach((b,i)=>{ const opts=[...b.querySelectorAll('.sku-option')].filter(e=>e.offsetParent!==null); + if(!opts.length){done.push(i+':no-opt');return;} + const inp=opts[0].querySelector('input[type=checkbox]'); + if(inp&&inp.checked){done.push(i+':already');return;} + if(inp&&!inp.checked) inp.click(); + const lab=opts[0].querySelector('label')||opts[0]; + for(const ev of ['mousedown','mouseup','click']) lab.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); + done.push(i+':'+(opts[0].innerText||'').trim().slice(0,16)); }); + return done.join(' | ');})()`); + await wait(8000); + return r; +} + +/** 填 SKU 行(在规格值选完之后调用) */ +export async function fillSkuRows(c, { prefix = 'SHEIN', price = '12.90', supply = '8.50', stock = '999' } = {}) { + return c.evalJS(`(()=>{const p=${P}; + const d=p._s.get('sheinSkuDataStore').$state.formState; + const rows=d.skuDataList||[]; let n=0; + rows.forEach((x,i)=>{ x.skcSupplierCode=${JSON.stringify(prefix)}+'-'+String(i+1).padStart(3,'0'); + x.skcRetailPrice=${JSON.stringify(price)}; x.supplyPrice=${JSON.stringify(supply)}; x.stockVariety=${JSON.stringify(stock)}; + x.weight='100'; x.length='200'; x.width='150'; x.height='30'; n++; }); + return 'rows:'+n;})()`); +} + +/** 尺码表弹窗:6 个「请输入」→ 确定 */ +export async function fillSizeTable(c) { + 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 0; const el=it.querySelector('.link')||it.querySelector('a,span'); if(!el)return 0; + el.scrollIntoView({block:'center'}); + for(const ev of ['mousedown','mouseup','click']) el.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 1;})()`); + await wait(5000); + const f = await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop(); + if(!m)return'no-modal'; const ins=[...m.querySelectorAll('input')].filter(i=>i.offsetParent!==null&&i.placeholder==='请输入'); + const vals=['10','18','3','2','18','5']; let n=0; + ins.forEach((el,i)=>{ if(i>=vals.length)return; el.focus(); + Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set.call(el,vals[i]); + el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); el.dispatchEvent(new Event('blur',{bubbles:true})); n++; }); + return 'filled:'+n;})()`); + await wait(1200); + await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop(); + if(m){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 1;})()`); + await wait(4000); + return f; +} + +/** 图片:sheinImgStore.mainImgData.multipleSpuImgs 吃外链数组 */ +export async function fillImages(c, urls) { + const r = await c.evalJS(`(()=>{const p=${P}; + const im=p._s.get('sheinImgStore').$state.formState; + const arr=${JSON.stringify(urls)}.map((u,i)=>({url:u,imgUrl:u,name:'img'+(i+1)+'.jpg',width:800,height:800})); + if(!Array.isArray(im.mainImgData.multipleSpuImgs)||im.mainImgData.multipleSpuImgs.length { + const out = []; + for (let round = 0; round < 6; round++) { + const r = await c.evalJS(`(()=>{ + const items=[...document.querySelectorAll('.ant-form-item')].filter(e=>{ + if(e.offsetParent===null) return false; + const req=e.querySelector('.attr-label.required')||e.querySelector('label.ant-form-item-required'); + if(!req) return false; + const name=(req.innerText||'').replace(/\s+/g,' ').trim(); + if(${JSON.stringify(skip)}.some(s=>name.includes(s))) return false; + const g=e.querySelector('.ant-checkbox-group'); if(!g) return false; + const boxes=[...g.querySelectorAll('.ant-checkbox-wrapper')].filter(x=>x.offsetParent!==null); + if(!boxes.length) return false; + const checked=[...g.querySelectorAll('input.ant-checkbox-input')].some(i=>i.checked); + return !checked;}); + const it=items[0]; if(!it) return '[]'; + const g=it.querySelector('.ant-checkbox-group'); + const box=[...g.querySelectorAll('.ant-checkbox-wrapper')].filter(x=>x.offsetParent!==null)[0]; + box.scrollIntoView({block:'center'}); + for(const ev of ['mousedown','mouseup','click']) box.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); + const lab=box.querySelector('span')||box; + return JSON.stringify({name:(it.querySelector('.attr-label.required').innerText||'').replace(/\s+/g,' ').trim().slice(0,24), val:(lab.innerText||'').trim().slice(0,18)});})()`); + const o = JSON.parse(String(r || '[]')); + if (!o.name) break; + out.push(o.name + '=' + o.val); + await wait(1500); + } + return out.join(' | '); + })(); + const filled = []; + for (let round = 0; round < 8; round++) { + const tgt = JSON.parse(String(await c.evalJS(`(()=>{ + const items=[...document.querySelectorAll('.ant-form-item')].filter(e=>{ + const l=e.querySelector('label'); if(!l||!/ant-form-item-required/.test(String(l.className))) return false; + if(e.offsetParent===null) return false; + const name=(l.innerText||'').replace(/\\s+/g,' ').trim(); + if(${JSON.stringify(skip)}.some(s=>name.includes(s))) return false; + const sel=e.querySelector('.ant-select'); if(!sel) return false; + const txt=(sel.innerText||'').replace(/\\s+/g,' ').trim(); + return !txt || /请选择/.test(txt);}); + const it=items[0]; if(!it) return '[]'; + const l=it.querySelector('label'); const sel=it.querySelector('.ant-select-selector')||it.querySelector('.ant-select'); + sel.scrollIntoView({block:'center'}); const r=sel.getBoundingClientRect(); + return JSON.stringify({name:(l.innerText||'').replace(/\\s+/g,' ').trim().slice(0,30), x:Math.round(r.x+r.width/2), y:Math.round(r.y+r.height/2)});})()`))); + if (!tgt.name) break; + await c.send('Input.dispatchMouseEvent', {type:'mouseMoved', x:tgt.x, y:tgt.y, buttons:0}); await wait(250); + for (const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent', {type:t, x:tgt.x, y:tgt.y, button:'left', clickCount:1, buttons:1}); + await wait(1800); + const pick = await 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,18);})()`); + filled.push(tgt.name + '=' + pick); + await wait(1800); + if (pick === 'no-dd' || pick === 'no-op') break; + } + return [filled.join(' | '), ticked].filter(x=>x&&x!=='none').join(' | ') || 'none'; +} diff --git a/shein建品/sh_diag.mjs b/shein建品/sh_diag.mjs new file mode 100644 index 0000000..99fd453 --- /dev/null +++ b/shein建品/sh_diag.mjs @@ -0,0 +1,38 @@ +import { newPage, closePage, wait, installHook, readHook } from './sh_lib.mjs'; +import { selectShop, selectCategory, fillTitle, addSpecs, pickSpecValues, fillSkuRows, fillSizeTable, fillImages } from './sh_build.mjs'; +import { SAN as S } from '/tmp/cdp3.mjs'; +import { readFileSync } from 'fs'; +const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`; +const pid='694637537727', kw='保温午餐包', shop=process.env.SHOP_OVERRIDE||'②Shein-易世通达-箱包'; +const it=JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json','utf8'))[pid]; +const c=await newPage('https://www.dianxiaomi.com/web/sheinProduct/add'); +await wait(11000); await installHook(c); +console.log('shop:', await selectShop(c, shop)); +console.log('cat:', await selectCategory(c, kw)); +await fillTitle(c, it.title); await wait(1500); +console.log('specs:', await addSpecs(c)); +console.log('vals:', await pickSpecValues(c)); +await fillSkuRows(c,{prefix:'SHEIN',price:String(it.price_cny),supply:String(it.cost)}); await wait(2500); +console.log('size:', await fillSizeTable(c)); +console.log('imgs:', await fillImages(c,(it.imgs||[]).slice(0,6))); +await wait(2000); +// 保存一次并抓全部证据 +await c.evalJS(`window.scrollTo(0,0)`); await wait(500); +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(12000); +const d = await c.evalJS(`(()=>{ + const errItems=[...document.querySelectorAll('.ant-form-item-has-error')].map(x=>{const l=x.querySelector('label'); return (l?(l.innerText||''):'?').replace(/\\s+/g,' ').trim().slice(0,40);}); + const ex=[...document.querySelectorAll('.ant-form-item-explain-error')].map(e=>(e.innerText||'').trim()); + const reds=[]; document.querySelectorAll('*').forEach(el=>{ if(el.offsetParent===null||el.children.length)return; + const t=(el.innerText||'').trim(); if(!t||t.length>50)return; const s=getComputedStyle(el).color||''; + const m=s.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)/); if(m&&+m[1]>150&&+m[2]<130&&+m[3]<130) reds.push(t); }); + const emptyReq=[...document.querySelectorAll('.ant-form-item')].filter(e=>{ + const l=e.querySelector('label'); if(!l||!/ant-form-item-required/.test(String(l.className))) return false; + const inp=e.querySelector('input,textarea'); if(!inp) return false; + return !inp.value && inp.offsetParent!==null;}).map(e=>(e.querySelector('label').innerText||'').replace(/\\s+/g,' ').trim().slice(0,34)); + return JSON.stringify({errItems:[...new Set(errItems)], ex:[...new Set(ex)].slice(0,8), reds:[...new Set(reds)].slice(0,12), emptyReq:[...new Set(emptyReq)].slice(0,12)});})()`); +console.log('诊断:', S(String(d)).slice(0,1600)); +console.log('hook:', String(await readHook(c)).slice(-300)); +try{ await c.shot('/tmp/shein_diag.jpg'); console.log('shot ok'); }catch(e){} +await closePage(c); process.exit(0); diff --git a/shein建品/sh_done.json b/shein建品/sh_done.json new file mode 100644 index 0000000..d03de10 --- /dev/null +++ b/shein建品/sh_done.json @@ -0,0 +1,162 @@ +{ + "661190598139": { + "ok": true, + "msg": "saved" + }, + "902440558099": { + "ok": true, + "msg": "saved" + }, + "823560564174": { + "ok": true, + "msg": "saved" + }, + "607330049419": { + "ok": true, + "msg": "saved" + }, + "854003999648": { + "ok": true, + "msg": "saved" + }, + "735147938123": { + "ok": true, + "msg": "saved" + }, + "610164857063": { + "ok": true, + "msg": "saved" + }, + "833753630925": { + "ok": true, + "msg": "saved" + }, + "676236596821": { + "ok": true, + "msg": "saved" + }, + "944470567338": { + "ok": true, + "msg": "saved" + }, + "787261097583": { + "ok": true, + "msg": "saved" + }, + "857781294719": { + "ok": true, + "msg": "saved" + }, + "863384503032": { + "ok": true, + "msg": "saved" + }, + "726442972676": { + "ok": true, + "msg": "saved" + }, + "893592264148": { + "ok": true, + "msg": "saved" + }, + "836512561982": { + "ok": true, + "msg": "saved" + }, + "714492799386": { + "ok": true, + "msg": "saved" + }, + "719391062058": { + "ok": true, + "msg": "saved" + }, + "953869908708": { + "ok": true, + "msg": "saved" + }, + "863524134410": { + "ok": true, + "msg": "saved" + }, + "623011262210": { + "ok": true, + "msg": "saved" + }, + "997450831819": { + "ok": true, + "msg": "saved" + }, + "639409307567": { + "ok": true, + "msg": "saved" + }, + "698782391248": { + "ok": true, + "msg": "saved" + }, + "591994902720": { + "ok": true, + "msg": "saved" + }, + "682195499823": { + "ok": true, + "msg": "saved" + }, + "657617635303": { + "ok": true, + "msg": "saved" + }, + "703822717997": { + "ok": true, + "msg": "saved" + }, + "621300323670": { + "ok": true, + "msg": "saved" + }, + "823838532886": { + "ok": true, + "msg": "saved" + }, + "1027332215847": { + "ok": true, + "msg": "saved" + }, + "641673262387": { + "ok": true, + "msg": "saved" + }, + "707826905566": { + "ok": true, + "msg": "saved" + }, + "694637537727": { + "ok": true, + "msg": "saved" + }, + "708420302844": { + "ok": true, + "msg": "saved" + }, + "750271307455": { + "ok": true, + "msg": "saved" + }, + "774494945453": { + "ok": true, + "msg": "saved" + }, + "775057912118": { + "ok": true, + "msg": "saved" + }, + "896469995553": { + "ok": true, + "msg": "saved" + }, + "627879474686": { + "ok": true, + "msg": "saved" + } +} \ No newline at end of file diff --git a/shein建品/sh_lib.mjs b/shein建品/sh_lib.mjs new file mode 100644 index 0000000..43dc6be --- /dev/null +++ b/shein建品/sh_lib.mjs @@ -0,0 +1,38 @@ +/** SHEIN 批量公共库:按 target 直连页面,避免多处 attach(URL) 抢到同一个标签 */ +import { CDP } from '/tmp/cdp3.mjs'; +export const wait = ms => new Promise(z => setTimeout(z, ms)); +export const BASE = process.env.CDP_BASE || 'http://127.0.0.1:9223'; + +/** 新开一个页面并返回它自己的 CDP 连接(不会被别的同 URL 标签抢走) */ +export async function newPage(url = 'about:blank') { + const r = await fetch(`${BASE}/json/new?${encodeURIComponent(url)}`, { method: 'PUT' }); + const t = await r.json(); + const c = await CDP.connect(t.webSocketDebuggerUrl); + await c.send('Page.enable'); await c.send('Runtime.enable'); + c.targetId = t.id; c.pageUrl = url; + return c; +} +export async function closePage(c) { try { await fetch(`${BASE}/json/close/${c.targetId}`); } catch {} try { c.close(); } catch {} } +export async function listPages() { return (await (await fetch(`${BASE}/json/list`)).json()).filter(t => t.type === 'page'); } +/** 关掉所有匹配 url 的旧标签(清理重复 tab,attach(URL) 才不会抢错) */ +export async function closePagesByUrl(part) { + const ps = (await listPages()).filter(t => t.url.includes(part)); + for (const t of ps) await fetch(`${BASE}/json/close/${t.id}`).catch(() => {}); + return ps.length; +} +export const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`; +export const stub = store => `(()=>{const p=${P};return p._s.get('${store}').$state;})()`; +export async function state(c, store) { return c.evalJS(`(()=>{const p=${P};const s=p._s.get('${store}').$state;return JSON.stringify(s.formState||s);})()`); } +/** 在指定页面上点「保存」并抓 12 秒内的被吞消息 */ +export async function installHook(c) { + await c.evalJS(`(()=>{ window.__cap=[]; + function wrap(o,n,l){ try{const f=o[n]; o[n]=function(){ try{window.__cap.push(l+': '+[...arguments].map(a=>typeof a==='string'?a:JSON.stringify(a)).join(' ').slice(0,170));}catch(e){} return f.apply(o,arguments);}; }catch(e){} } + try{['error','warning','info','success'].forEach(k=>wrap(MYJ.message,k,'MYJ.'+k));}catch(e){} + try{['error','warning','info','success'].forEach(k=>wrap(antd.message,k,'antd.'+k));}catch(e){} return 1;})()`); +} +export async function readHook(c) { return String(await c.evalJS(`JSON.stringify(window.__cap||[])`)); } +export async function clickByText(c, re, tag = 'button') { + return c.evalJS(`(()=>{const x=[...document.querySelectorAll('${tag}')].filter(e=>e.offsetParent!==null).find(e=>new RegExp(${JSON.stringify(re)}).test((e.innerText||'').trim())); + if(!x) return 'nf'; x.scrollIntoView({block:'center'}); + for(const ev of ['mousedown','mouseup','click']) x.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return (x.innerText||'').trim().slice(0,20);})()`); +} diff --git a/shein建品/sh_verify.mjs b/shein建品/sh_verify.mjs new file mode 100644 index 0000000..1b6a18b --- /dev/null +++ b/shein建品/sh_verify.mjs @@ -0,0 +1,13 @@ +import { newPage, closePage, wait } from './sh_lib.mjs'; +import { SAN as S } from '/tmp/cdp3.mjs'; +const c = await newPage('https://www.dianxiaomi.com/web/sheinProduct/offline'); +await wait(13000); +const o = await c.evalJS(`(()=>{ + const body=(document.body.innerText||'').replace(/\\s+/g,' '); + const m=body.match(/共\\s*\\d+\\s*条/); + const rows=[...document.querySelectorAll('.vxe-body--row')].slice(0,3).map(r=>(r.innerText||'').replace(/\\s+/g,' ').trim().slice(0,110)); + const shops=[...new Set([...document.querySelectorAll('.vxe-body--row')].map(r=>{const m=(r.innerText||'').match(/[①-⑳][^\\s]{2,20}/); return m?m[0]:''}).filter(Boolean))]; + return JSON.stringify({total:m?m[0]:'', rows, shops});})()`); +console.log(S(String(o)).slice(0,900)); +try{ await c.shot('/tmp/shein_offline.jpg'); console.log('shot ok'); }catch(e){ console.log('shot fail'); } +await closePage(c); process.exit(0); diff --git a/shein建品/shbatch.mjs b/shein建品/shbatch.mjs new file mode 100644 index 0000000..6b47ca7 --- /dev/null +++ b/shein建品/shbatch.mjs @@ -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}`); diff --git a/temu建品/STATUS.md b/temu建品/STATUS.md index 8d11578..8a215f6 100644 --- a/temu建品/STATUS.md +++ b/temu建品/STATUS.md @@ -86,3 +86,33 @@ Temu 的规格机制和 SHEIN **不同构**——没有 `theme-select-box`, 店小秘的列表路由是 **`/draft` = 采集箱、`/offline` = 待发布**(之前搜 `/draft` 找不到东西就是这个原因)。 Temu 全托管列表 = `/web/temu/choiceTemuList/{draft|offline|online}`; Temu 半托管 POP = `/web/popTemu/pageList/*`。保存接口是 `/api/pddkjProduct/add*.json`(拼多多跨境=Temu 全托管)。 + +--- + +## 更新4:批量完成(46/46 进待发布,2026-09-20) + +脚本:`tmbatch.mjs`(`node tmbatch.mjs` 全量 · `node tmbatch.mjs ` 单条 · `--dry` 只构建) +数据:`tk_input.json` × `temu_route.json`(pid → 中文类目名 + catId),`tm_done.json` 断点续跑。 + +### 单条顺序 +`选店铺(①Alimingpro) → 选类目(弹窗搜索,按**最后一级路径精确匹配**,别用 contains, +否则「口哨」会选到「狗口哨」) → 标题/主图/素材图/包装图/产地(region2Id) → 变种值 +(两个 .theme-value-add 各输值再点「添加」)→ 填 SKU 行(货号/EAN/价/尺寸/重量/净重) +→ 逐行挂预览图 → 保存` + +### 这一轮新踩的坑 +1. **主图必须满 5 张**,否则 `MYJ.error: 产品轮播图至少要上传5张!` → 图不够就重复补齐(`padImgs`) +2. **类目弹窗必须关掉**(同 SHEIN),否则预览图挂不上,最后只报「请上传预览图」—— + 第一轮 13 条就是这么挂的,补上「关弹窗 + 预览图重试」后**全部重跑成功** +3. **EAN 游标必须落盘**:`ean_cursor.json` + `ean_temu_used.txt`。 + 第一轮游标只在内存里,重跑换进程后从 3449 重发,和手工首条的口哨撞了号 + (`6980309334492`)→ 用编辑页 `tm_fixean2.mjs` 改成 `6980309335512`,台账已补齐、当前无重复 + +### 结果 +46/46 保存成功(+ 手工首条 = 47 条在待发布)。 +EAN:pc **3447~3551** 已用,游标 → **3552**。 +⚠️ 台账只在 KVM-2,**尚未回 S03 `/root/ean_*.txt`**。 + +### 编辑已有草稿 +待发布列表里「编辑」按钮**必须用真实鼠标事件**点,而且会**新开一个标签** +`/web/temu/edit?id=`(DOM click 无效,一次没关标签就会攒一堆)。 diff --git a/temu建品/ean_cursor.json b/temu建品/ean_cursor.json new file mode 100644 index 0000000..bf2ac7a --- /dev/null +++ b/temu建品/ean_cursor.json @@ -0,0 +1,4 @@ +{ + "next": 3552, + "at": "2026-09-20T11:48:20.937972Z" +} \ No newline at end of file diff --git a/temu建品/ean_temu_used.txt b/temu建品/ean_temu_used.txt new file mode 100644 index 0000000..d730a4b --- /dev/null +++ b/temu建品/ean_temu_used.txt @@ -0,0 +1,49 @@ +# Temu 全托管 EAN 台账(本机记录,**必须回 S03 /root/ean_*.txt 落盘**) +# 手工测试条(首条 Hair Clips,待发布):6980309334478 = TM-TEST-1, 6980309334485 = TM-TEST-2 → pc 3447, 3448 +548459586741 ["6980309334492"] 2026-09-20T10:08:17.075Z +764423265986 ["6980309334492"] 2026-09-20T10:10:33.003Z +735147938123 ["6980309334539"] 2026-09-20T10:13:00.353Z +627879474686 ["6980309334553"] 2026-09-20T10:14:17.101Z +610164857063 ["6980309334577"] 2026-09-20T10:15:33.832Z +676236596821 ["6980309334591"] 2026-09-20T10:16:45.577Z +708420302844 ["6980309334614"] 2026-09-20T10:17:57.242Z +657617635303 ["6980309334676"] 2026-09-20T10:21:40.176Z +641673262387 ["6980309334690"] 2026-09-20T10:22:51.838Z +953869908708 ["6980309334713"] 2026-09-20T10:24:03.501Z +703822717997 ["6980309334737"] 2026-09-20T10:25:15.175Z +863524134410 ["6980309334751"] 2026-09-20T10:26:26.915Z +853414558425 ["6980309334874"] 2026-09-20T10:33:51.478Z +997450831819 ["6980309334898"] 2026-09-20T10:35:03.229Z +639409307567 ["6980309334911"] 2026-09-20T10:36:14.937Z +698782391248 ["6980309334973"] 2026-09-20T10:39:57.711Z +707826905566 ["6980309334997"] 2026-09-20T10:41:14.383Z +682195499823 ["6980309335031"] 2026-09-20T10:43:37.831Z +621300323670 ["6980309335093"] 2026-09-20T10:47:20.685Z +896469995553 ["6980309335116"] 2026-09-20T10:48:37.489Z +944470567338 ["6980309335130"] 2026-09-20T10:49:49.266Z +787261097583 ["6980309335154"] 2026-09-20T10:51:00.930Z +833753630925 ["6980309335178"] 2026-09-20T10:52:12.648Z +719391062058 ["6980309335192"] 2026-09-20T10:53:24.372Z +857781294719 ["6980309335215"] 2026-09-20T10:54:36.124Z +863384503032 ["6980309335239"] 2026-09-20T10:55:47.868Z +774494945453 ["6980309335253"] 2026-09-20T10:56:59.572Z +750271307455 ["6980309335277"] 2026-09-20T10:58:11.203Z +726442972676 ["6980309335291"] 2026-09-20T10:59:22.923Z +775057912118 ["6980309335314"] 2026-09-20T11:00:34.635Z +893592264148 ["6980309335338"] 2026-09-20T11:01:46.284Z +607330049419 ["6980309335352"] 2026-09-20T11:02:58.127Z +854003999648 ["6980309335376"] 2026-09-20T11:04:09.964Z +685456757807 ["6980309335383"] 2026-09-20T11:10:56.878Z +866537106657 ["6980309335390"] 2026-09-20T11:12:06.702Z +547183997437 ["6980309335406"] 2026-09-20T11:13:16.531Z +694637537727 ["6980309335413"] 2026-09-20T11:14:26.293Z +623011262210 ["6980309335420"] 2026-09-20T11:15:36.009Z +836512561982 ["6980309335437"] 2026-09-20T11:16:45.737Z +714492799386 ["6980309335444"] 2026-09-20T11:17:55.515Z +591994902720 ["6980309335451"] 2026-09-20T11:19:05.239Z +902440558099 ["6980309335468"] 2026-09-20T11:20:15.013Z +823838532886 ["6980309335475"] 2026-09-20T11:21:24.754Z +1027332215847 ["6980309335482"] 2026-09-20T11:22:34.538Z +823560564174 ["6980309335499"] 2026-09-20T11:23:44.319Z +739147684759 ["6980309335505"] 2026-09-20T11:24:54.151Z +764423265986-FIX ["6980309335512"] 2026-09-20T11:48:20.937829Z diff --git a/temu建品/tm_count.mjs b/temu建品/tm_count.mjs new file mode 100644 index 0000000..f5af7d2 --- /dev/null +++ b/temu建品/tm_count.mjs @@ -0,0 +1,12 @@ +import { newPage, closePage, wait } from '/home/admin9webs/shein建品/sh_lib.mjs'; +import { SAN as S } from '/tmp/cdp3.mjs'; +const c = await newPage('https://www.dianxiaomi.com/web/temu/choiceTemuList/offline'); +await wait(13000); +const o = await c.evalJS(`(()=>{ + const body=(document.body.innerText||'').replace(/\\s+/g,' '); + const m=body.match(/共\\s*\\d+\\s*条/); + const rows=[...document.querySelectorAll('.vxe-body--row')].slice(0,2).map(r=>(r.innerText||'').replace(/\\s+/g,' ').trim().slice(0,120)); + return JSON.stringify({total:m?m[0]:'', rows});})()`); +console.log(S(String(o)).slice(0,700)); +try{ await c.shot('/tmp/temu_final.jpg'); console.log('shot ok'); }catch(e){} +await closePage(c); process.exit(0); diff --git a/temu建品/tm_done.json b/temu建品/tm_done.json new file mode 100644 index 0000000..a6a028b --- /dev/null +++ b/temu建品/tm_done.json @@ -0,0 +1,212 @@ +{ + "548459586741": { + "ok": true, + "ean": 3449 + }, + "764423265986": { + "ok": true, + "ean": 3449 + }, + "685456757807": { + "ok": true, + "ean": [ + "6980309335383" + ] + }, + "735147938123": { + "ok": true, + "ean": 3453 + }, + "627879474686": { + "ok": true, + "ean": 3455 + }, + "610164857063": { + "ok": true, + "ean": 3457 + }, + "676236596821": { + "ok": true, + "ean": 3459 + }, + "708420302844": { + "ok": true, + "ean": 3461 + }, + "866537106657": { + "ok": true, + "ean": [ + "6980309335390" + ] + }, + "547183997437": { + "ok": true, + "ean": [ + "6980309335406" + ] + }, + "657617635303": { + "ok": true, + "ean": 3467 + }, + "641673262387": { + "ok": true, + "ean": 3469 + }, + "953869908708": { + "ok": true, + "ean": 3471 + }, + "703822717997": { + "ok": true, + "ean": 3473 + }, + "863524134410": { + "ok": true, + "ean": 3475 + }, + "694637537727": { + "ok": true, + "ean": [ + "6980309335413" + ] + }, + "623011262210": { + "ok": true, + "ean": [ + "6980309335420" + ] + }, + "836512561982": { + "ok": true, + "ean": [ + "6980309335437" + ] + }, + "714492799386": { + "ok": true, + "ean": [ + "6980309335444" + ] + }, + "591994902720": { + "ok": true, + "ean": [ + "6980309335451" + ] + }, + "853414558425": { + "ok": true, + "ean": 3487 + }, + "997450831819": { + "ok": true, + "ean": 3489 + }, + "639409307567": { + "ok": true, + "ean": 3491 + }, + "902440558099": { + "ok": true, + "ean": [ + "6980309335468" + ] + }, + "823838532886": { + "ok": true, + "ean": [ + "6980309335475" + ] + }, + "698782391248": { + "ok": true, + "ean": 3497 + }, + "707826905566": { + "ok": true, + "ean": 3499 + }, + "1027332215847": { + "ok": true, + "ean": [ + "6980309335482" + ] + }, + "682195499823": { + "ok": true, + "ean": 3503 + }, + "823560564174": { + "ok": true, + "ean": [ + "6980309335499" + ] + }, + "739147684759": { + "ok": true, + "ean": [ + "6980309335505" + ] + }, + "621300323670": { + "ok": true, + "ean": 3509 + }, + "896469995553": { + "ok": true, + "ean": 3511 + }, + "944470567338": { + "ok": true, + "ean": 3513 + }, + "787261097583": { + "ok": true, + "ean": 3515 + }, + "833753630925": { + "ok": true, + "ean": 3517 + }, + "719391062058": { + "ok": true, + "ean": 3519 + }, + "857781294719": { + "ok": true, + "ean": 3521 + }, + "863384503032": { + "ok": true, + "ean": 3523 + }, + "774494945453": { + "ok": true, + "ean": 3525 + }, + "750271307455": { + "ok": true, + "ean": 3527 + }, + "726442972676": { + "ok": true, + "ean": 3529 + }, + "775057912118": { + "ok": true, + "ean": 3531 + }, + "893592264148": { + "ok": true, + "ean": 3533 + }, + "607330049419": { + "ok": true, + "ean": 3535 + }, + "854003999648": { + "ok": true, + "ean": 3537 + } +} \ No newline at end of file diff --git a/temu建品/tm_fixean2.mjs b/temu建品/tm_fixean2.mjs new file mode 100644 index 0000000..8adaf6f --- /dev/null +++ b/temu建品/tm_fixean2.mjs @@ -0,0 +1,40 @@ +import { wait, listPages, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs'; +import { CDP, SAN as S } from '/tmp/cdp3.mjs'; +import { readFileSync, writeFileSync, appendFileSync } from 'fs'; +const DIR='/home/admin9webs/temu建品'; +const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`; +const chk = pc => { const s='69803093'+String(pc).padStart(4,'0'); const d=[...s].map(Number); + return s + String((10 - d.reduce((a,x,i)=>a+x*(i%2===0?1:3),0)%10)%10); }; +const cur = JSON.parse(readFileSync(`${DIR}/ean_cursor.json`,'utf8')).next; +const NEWEAN = chk(cur); +const tabs = (await listPages()).filter(t=>t.url.includes('/web/temu/edit')); +console.log('编辑页标签数:', tabs.length, '→ 用最后一个;换号 pc', cur, '=', NEWEAN); +const t = tabs[tabs.length-1]; +const e = await CDP.connect(t.webSocketDebuggerUrl); +await e.send('Page.enable'); await e.send('Runtime.enable'); +await wait(9000); +await installHook(e); +console.log('标题:', String(await e.evalJS(`(document.querySelector('input[name="productName"], .ant-input')||{}).value||''`)).slice(0,60)); +const set = await e.evalJS(`(()=>{const p=${P}; + const rows=p._s.get('temuAddSkuDataStore').$state.formState.skuDataList||[]; + if(!rows.length) return 'no-rows'; + const old=rows.map(r=>r.code).join(','); rows[0].code=${JSON.stringify(NEWEAN)}; + return 'old='+old+' new='+rows[0].code;})()`); +console.log('改 EAN:', String(set)); +await wait(2500); +await e.evalJS(`window.scrollTo(0,0)`); await wait(600); +await e.evalJS(`(()=>{const x=[...document.querySelectorAll('button')].filter(z=>z.offsetParent!==null).find(z=>/^\s*保存\s*$/.test((z.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 info = await e.evalJS(`JSON.stringify([...document.querySelectorAll('.ant-modal')].filter(z=>z.offsetParent!==null).map(z=>(z.innerText||'').replace(/\s+/g,' ').trim().slice(0,130)))`); +console.log('保存:', S(String(info)).slice(0,240)); +console.log('hook:', String(await readHook(e)).slice(-120)); +if(/已保存|保存成功|待发布/.test(String(info))){ + writeFileSync(`${DIR}/ean_cursor.json`, JSON.stringify({next:cur+1, at:new Date().toISOString()},null,1)); + appendFileSync(`${DIR}/ean_temu_used.txt`, `764423265986-FIX\t["${NEWEAN}"]\t${new Date().toISOString()}\n`); + console.log('✓ 换号完成,cursor →', cur+1); +} +// 关掉多余的编辑页(含本次用的) +for (const x of (await listPages()).filter(p=>p.url.includes('/web/temu/edit'))) await fetch(`http://127.0.0.1:9223/json/close/${x.id}`).catch(()=>{}); +console.log('已清理编辑页标签'); +process.exit(0); diff --git a/temu建品/tmbatch.mjs b/temu建品/tmbatch.mjs new file mode 100644 index 0000000..61d9054 --- /dev/null +++ b/temu建品/tmbatch.mjs @@ -0,0 +1,201 @@ +/** + * Temu 全托管(①Alimingpro, shopId 4021529)批量建品 + * + * 用法: + * node tmbatch.mjs --dry 548459586741 # 只构建不保存(验证) + * node tmbatch.mjs 548459586741 # 建这一条并保存 + * node tmbatch.mjs # 跑全部未完成的 + * + * 报错是层层递进的(每修一层才露出下一层),全靠 MYJ.message 钩子看: + * 省份 → 变种 → 尺寸 → 净重<重量 → 预览图(已解:见 tmpv.mjs) + * 顺序不可换:先规格值(theme-value-add 手动输值+添加)→ 再填 SKU 行 → 再挂预览图。 + */ +import { readFileSync, writeFileSync, appendFileSync } from 'fs'; +import { newPage, closePage, wait, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs'; +import { selectCategory } from '/home/admin9webs/shein建品/sh_build.mjs'; +import { fillPreviewImages } from './tmpv.mjs'; + +const DIR = '/home/admin9webs/temu建品'; +const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`; +const INPUT = JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json', 'utf8')); +const ROUTE = JSON.parse(readFileSync(`${DIR}/temu_route.json`, 'utf8')); +const DONE_F = `${DIR}/tm_done.json`; +const LOG_F = `${DIR}/tm_batch.log`; +const SHOP = '①Alimingpro'; +const PROVINCE = '43000000000002'; // 北京市(region2Id) +// 已手工建过、别再建一条的(否则待发布里重复) +const SKIP = new Set(['661190598139']); + +const argv = process.argv.slice(2); +const dry = argv.includes('--dry'); +const only = argv.filter(a => !a.startsWith('--')); +let tasks = Object.entries(ROUTE).map(([pid, v]) => ({ pid, cat: v.cat, catId: v.catId })); +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'); }; + +async function selectShop(c, name) { + const pos = JSON.parse(String(await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||'')); + const s=it?it.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 (!pos.length) return 'no-select'; + 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(2200); + const r = await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop(); + if(!dd) return 'no-dd'; + const it=[...dd.querySelectorAll('.ant-select-item-option')].find(e=>(e.innerText||'').replace(/\\s+/g,'').includes(${JSON.stringify(name)})); + if(!it) return 'no-match'; const rr=it.getBoundingClientRect(); + for(const ev of ['mousedown','mouseup','click']) it.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:rr.x+rr.width/2,clientY:rr.y+rr.height/2})); + return 'picked:'+(it.innerText||'').trim();})()`); + await wait(5000); + return r; +} + +/** 变种:每个 .theme-value-add 块输一个值 → 点「添加」(直接改 store 无效) */ +async function addVariantValues(c, values = ['Black', 'One Size']) { + const out = []; + for (let i = 0; i < 2; i++) { + const info = JSON.parse(String(await c.evalJS(`(()=>{const blocks=[...document.querySelectorAll('.theme-value-add')].filter(e=>e.offsetParent!==null); + const b=blocks[${i}]; if(!b) return '[]'; + const inp=b.querySelector('input[class*="!w-200"]')||b.querySelector('input'); if(!inp) return '[0,0]'; + b.scrollIntoView({block:'center'}); const r=inp.getBoundingClientRect(); + inp.setAttribute('data-tmfill','1'); + return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`))); + if (!info.length) { out.push('no-block' + i); continue; } + for (const t of ['mousePressed', 'mouseReleased']) + await c.send('Input.dispatchMouseEvent', { type: t, x: info[0], y: info[1], button: 'left', clickCount: 1, buttons: 1 }); + await wait(300); + await c.send('Input.insertText', { text: values[i] || ('V' + i) }).catch(() => {}); + // 兜底:直接写 value + input 事件 + await c.evalJS(`(()=>{const inp=document.querySelector('input[data-tmfill="1"]'); if(!inp) return 0; + const setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; + if(!inp.value) { setter.call(inp, ${JSON.stringify(values[i] || '')}); inp.dispatchEvent(new Event('input',{bubbles:true})); } + return 1;})()`); + await wait(600); + const added = await c.evalJS(`(()=>{const b=[...document.querySelectorAll('.theme-value-add')].filter(e=>e.offsetParent!==null)[${i}]; + if(!b) return 'no-block'; + const btn=[...b.querySelectorAll('button')].find(x=>/添加/.test(x.innerText||'')); + if(!btn) return 'no-add-btn'; + for(const ev of ['mousedown','mouseup','click']) btn.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); + return 'added';})()`); + out.push(added); + await wait(2500); + } + return out.join(' | '); +} + +/** 填其余表单(图片/产地/包装/标题) */ +function padImgs(list, n = 5) { const a = (list || []).slice(0, 6); if (!a.length) return a; + let i = 0; while (a.length < n) { a.push(a[i % list.length]); i++; } return a; } + +async function fillBasics(c, it) { + const PADS = padImgs(it.imgs); + return c.evalJS(`(()=>{const p=${P}; + const f=p._s.get('temuAddProductStore').$state.formState; + const imgs=${JSON.stringify(PADS)}; + const mk=(u,i)=>({url:u,imgUrl:u,name:'img'+(i+1)+'.jpg',width:800,height:800}); + f.productName=${JSON.stringify(it.title)}; + f.mainImage=imgs.slice(0,5).map(mk); + f.materialImage=imgs.slice(0,3).map(mk); + f.productOrigin='CN'; + f.region2Id=${JSON.stringify(PROVINCE)}; + const q=p._s.get('temuAddOtherStore').$state; + if(q.packageFormState){ q.packageFormState.packageImages=imgs.slice(0,3).map(mk); q.packageFormState.packageShape=1; q.packageFormState.packageType=0; } + return JSON.stringify({main:f.mainImage.length, padded:imgs.length, mat:Array.isArray(f.materialImage)?f.materialImage.length:'obj', + pkg:q.packageFormState?q.packageFormState.packageImages.length:'-'});})()`); +} + +/** SKU 行:货号/EAN/价格/尺寸/重量/净重。返回 {rows, codes, next} —— 游标必须落盘,否则换进程重跑会撞号 */ +async function fillSkuRows(c, it, eanStart, pid) { + return JSON.parse(String(await c.evalJS(`(()=>{const p=${P}; + const d=p._s.get('temuAddSkuDataStore').$state.formState; + const rows=d.skuDataList||[]; let pc=${eanStart}; const codes=[]; + const price=String(${JSON.stringify(String(it.price_cny || '19.90'))}); + const cost=String(${JSON.stringify(String(it.cost || '10'))}); + const chk=v=>{const s=String(v||'');const d12=s.slice(0,12).split('').map(Number); + const sum=d12.reduce((a,x,i)=>a+x*(i%2===0?1:3),0); return s.slice(0,12)+String((10-sum%10)%10);}; + rows.forEach((x,i)=>{ x.extCode='TM-'+${JSON.stringify(pid)}+'-'+String(i+1); + x.code=chk('69803093'+String(pc).padStart(4,'0')); codes.push(x.code); + x.supplierPrice=cost; x.suggestedPrice=price; + x.length='20'; x.width='15'; x.height='3'; x.weight='100'; x.netWeight='90'; pc++; }); + return JSON.stringify({rows:rows.length, codes, next:pc});})()`))); +} + +log(`Temu 批量开始:任务 ${tasks.length},dry=${dry}`); +// EAN 游标必须持久化:换进程重跑时从 ean_cursor.json 接着走,否则会撞号 +const CURSOR_F = `${DIR}/ean_cursor.json`; +function loadCursor() { + try { const j = JSON.parse(readFileSync(CURSOR_F, 'utf8')); if (j.next) return j.next; } catch {} + let max = 3448; // 3447/3448 已被手工测试条(Hair Clips)占用 + try { + for (const line of readFileSync(`${DIR}/ean_temu_used.txt`, 'utf8').split('\n')) { + const m = line.match(/69803093(\d{4})\d/); if (m) max = Math.max(max, Number(m[1])); + } + } catch {} + return max + 1; +} +let eanCursor = loadCursor(); +const saveCursor = () => writeFileSync(CURSOR_F, JSON.stringify({ next: eanCursor, at: new Date().toISOString() }, null, 1)); +log(`EAN 游标起点:pc ${eanCursor}(台账文件 ${CURSOR_F})`); +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} ${t.cat}(${t.catId}) :: ${String(it.title).slice(0, 42)}`); + let c; + try { + c = await newPage('https://www.dianxiaomi.com/web/temu/add'); + await wait(12000); + await installHook(c); + log(` 店铺: ${await selectShop(c, SHOP)}`); + log(` 类目: ${await selectCategory(c, t.cat, t.cat)}`); + const catNow = await c.evalJS(`(()=>{const p=${P};const f=p._s.get('temuAddBasicStore').$state.formState;return String(f.categoryId||'');})()`); + if (catNow && String(t.catId) && catNow !== String(t.catId)) log(` ⚠ 类目 id 不一致:选到 ${catNow},路由表写 ${t.catId}`); + log(` 基础: ${await fillBasics(c, it)}`); + await wait(3500); + log(` 变种: ${await addVariantValues(c, ['Black', 'One Size'])}`); + const st = await c.evalJS(`(()=>{const p=${P}; + const d=p._s.get('temuAddSkuDataStore').$state.formState; + const a=p._s.get('temuAddSkuAttrStore').$state.formState; + return JSON.stringify({rows:(d.skuDataList||[]).length, attrs:(a.skuAttrs||[]).map(x=>x.parentSpecName+':'+((x.options||[]).length))});})()`); + log(` 状态: ${st}`); + if (/"rows":0/.test(String(st))) throw new Error('变种行没生成'); + const sku = await fillSkuRows(c, it, eanCursor, t.pid); + eanCursor = sku.next; saveCursor(); // 用掉几个号就前进几个,且立刻落盘(失败也不回退,宁浪费不撞号) + log(` SKU: rows=${sku.rows} EAN=${JSON.stringify(sku.codes)} → 下一号 pc ${eanCursor}`); + await wait(2500); + const pv = await fillPreviewImages(c); + const miss = JSON.parse(String(await c.evalJS(`(()=>{const p=${P}; + const rows=p._s.get('temuAddSkuDataStore').$state.formState.skuDataList||[]; + return JSON.stringify(rows.map((r,i)=>r.image?null:i).filter(x=>x!==null));})()`))); + if (miss.length) { log(` ⚠ 预览图缺 ${JSON.stringify(miss)},补挂`); pv.push(...await fillPreviewImages(c, miss)); } + log(` 预览图行: ${JSON.stringify(pv)}`); + if (dry) { delete done[t.pid]; log(' ✓ dry 构建完成(未保存)'); 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 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)); + return JSON.stringify({mods});})()`); + const cap = await readHook(c); + const ok = /已保存|保存成功|待发布/.test(String(info)); + if (ok) { + const codes = await c.evalJS(`(()=>{const p=${P};const d=p._s.get('temuAddSkuDataStore').$state.formState; + return JSON.stringify((d.skuDataList||[]).map(r=>r.code));})()`); + appendFileSync(`${DIR}/ean_temu_used.txt`, `${t.pid}\t${codes}\t${new Date().toISOString()}\n`); + log(` EAN: ${codes}(已记入 ean_temu_used.txt,需回 S03 落盘)`); + } + done[t.pid] = ok ? { ok: true, ean: sku.codes } : { ok: false, ean: sku.codes, msg: String(info).slice(0, 160), hook: String(cap).slice(-240) }; + log(ok ? ` ✓ 已保存到待发布` : ` ✗ 失败 ${String(info).slice(0, 200)} | ${String(cap).slice(-200)}`); + } catch (e) { + done[t.pid] = { ok: false, msg: String(e).slice(0, 260) }; + log(` ✗ 异常: ${String(e).slice(0, 240)}`); + } finally { + if (!dry) writeFileSync(DONE_F, JSON.stringify(done, null, 1)); + if (c) await closePage(c); + await wait(2500); + } +} +log(`Temu 批量结束:成功 ${Object.values(done).filter(x => x.ok).length}/${tasks.length}`); diff --git a/temu建品/tmpv.mjs b/temu建品/tmpv.mjs index efce591..85c4144 100644 --- a/temu建品/tmpv.mjs +++ b/temu建品/tmpv.mjs @@ -21,45 +21,58 @@ export async function fillPreviewImages(c, rowIdxs = null) { const n = Number(await c.evalJS(`(()=>{const p=${P}; return p._s.get('temuAddSkuDataStore').$state.formState.skuDataList.length;})()`)); const idxs = rowIdxs && rowIdxs.length ? rowIdxs : [...Array(n).keys()]; - const done = []; + const done = [], failed = []; for (const i of idxs) { - await c.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 }).catch(() => {}); - await wait(400); - const pos = JSON.parse(String(await c.evalJS(`(()=>{const cell=[...document.querySelectorAll('.sku-image')][${i}]; - if(!cell) return '[]'; cell.scrollIntoView({block:'center'}); - const r=cell.getBoundingClientRect(); return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`))); - if (!pos.length) { console.log(' 行' + i + ': 找不到单元格'); continue; } - await c.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: pos[0], y: pos[1], buttons: 0 }); - await wait(400); - 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(1800); - const li = await c.evalJS(`(()=>{ - const it=[...document.querySelectorAll('.myj-image-drop .ant-dropdown-menu-item')].filter(e=>e.offsetParent!==null).find(e=>/引用产品轮播图/.test(e.innerText||'')); - if(!it) return 'no-menu'; - const r=it.getBoundingClientRect(); - for(const ev of ['mousedown','mouseup','click']) it.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2})); - return 'ok';})()`); - if (li !== 'ok') { console.log(' 行' + i + ': 下拉没出(' + li + ')'); continue; } - await wait(2000); - const hasModal = async () => Number(await c.evalJS( - `[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null&&/引用产品图片/.test(e.innerText||'')).length`)); - if (!await hasModal()) await wait(1600); - if (!await hasModal()) { console.log(' 行' + i + ': 引用产品图片弹窗没开'); continue; } - await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null&&/引用产品图片/.test(e.innerText||'')).pop(); - const all=[...m.querySelectorAll('*')].find(e=>(e.innerText||'').trim()==='全部'&&e.children.length===0); - const t=all?(all.closest('label')||all.parentElement):null; - if(t){const cb=t.querySelector('.ant-checkbox')||t; for(const ev of ['mousedown','mouseup','click']) cb.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));} return 1;})()`); - await wait(1200); - await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null&&/引用产品图片/.test(e.innerText||'')).pop(); - const b=[...m.querySelectorAll('button')].find(x=>/^\\s*选择\\s*$/.test((x.innerText||'').trim())); - if(b) for(const ev of ['mousedown','mouseup','click']) b.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 1;})()`); - await wait(2500); - done.push(i); + let reason = 'unknown'; + for (let attempt = 0; attempt < 3; attempt++) { + reason = await fillOneRow(c, i); + if (reason === 'ok') break; + await c.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 }).catch(() => {}); + await wait(1500); + } + if (reason === 'ok') done.push(i); else failed.push(i + ':' + reason); } + if (failed.length) console.log(' 预览图未挂上:', failed.join(', ')); return done; } +async function fillOneRow(c, i) { + await c.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 }).catch(() => {}); + await wait(400); + const pos = JSON.parse(String(await c.evalJS(`(()=>{const cell=[...document.querySelectorAll('.sku-image')][${i}]; + if(!cell) return '[]'; cell.scrollIntoView({block:'center'}); + const r=cell.getBoundingClientRect(); return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`))); + if (!pos.length) return 'no-cell'; + await c.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: pos[0], y: pos[1], buttons: 0 }); + await wait(400); + 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(1800); + const li = await c.evalJS(`(()=>{ + const it=[...document.querySelectorAll('.myj-image-drop .ant-dropdown-menu-item')].filter(e=>e.offsetParent!==null).find(e=>/引用产品轮播图/.test(e.innerText||'')); + if(!it) return 'no-menu'; + const r=it.getBoundingClientRect(); + for(const ev of ['mousedown','mouseup','click']) it.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2})); + return 'ok';})()`); + if (li !== 'ok') return li; + await wait(2000); + const hasModal = async () => Number(await c.evalJS( + `[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null&&/引用产品图片/.test(e.innerText||'')).length`)); + if (!await hasModal()) await wait(1600); + if (!await hasModal()) return 'no-ref-modal'; + await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null&&/引用产品图片/.test(e.innerText||'')).pop(); + const all=[...m.querySelectorAll('*')].find(e=>(e.innerText||'').trim()==='全部'&&e.children.length===0); + const t=all?(all.closest('label')||all.parentElement):null; + if(t){const cb=t.querySelector('.ant-checkbox')||t; for(const ev of ['mousedown','mouseup','click']) cb.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));} return 1;})()`); + await wait(1200); + await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null&&/引用产品图片/.test(e.innerText||'')).pop(); + const b=[...m.querySelectorAll('button')].find(x=>/^\\s*选择\\s*$/.test((x.innerText||'').trim())); + if(b) for(const ev of ['mousedown','mouseup','click']) b.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 1;})()`); + await wait(2500); + const has = await c.evalJS(`(()=>{const p=${P};const r=p._s.get('temuAddSkuDataStore').$state.formState.skuDataList[${i}]; return r&&r.image?1:0;})()`); + return Number(has) ? 'ok' : 'no-image-set'; +} + if (import.meta.url === `file://${process.argv[1]}`) { const c = await attach('/web/temu/add'); if (!c) { console.log('没有 Temu 建品页'); process.exit(1); }