From cdde45ed9a718d9b3c60253a6ea27cef3a4b99cb Mon Sep 17 00:00:00 2001 From: admin9webs Date: Sun, 20 Sep 2026 17:45:18 +0800 Subject: [PATCH] =?UTF-8?q?Temu=20=E5=85=A8=E6=89=98=E7=AE=A1=EF=BC=9A?= =?UTF-8?q?=E8=A7=A3=E3=80=8C=E9=A2=84=E8=A7=88=E5=9B=BE=E3=80=8D=E9=9D=99?= =?UTF-8?q?=E9=BB=98=E6=8B=A6=E6=88=AA=EF=BC=88=E9=80=90=E8=A1=8C=E7=82=B9?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=A0=BC=E2=86=92=E5=BC=95=E7=94=A8=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E8=BD=AE=E6=92=AD=E5=9B=BE=EF=BC=89=EF=BC=8C=E9=A6=96?= =?UTF-8?q?=E6=9D=A1=E5=B7=B2=E8=BF=9B=E5=BE=85=E5=8F=91=E5=B8=83=EF=BC=9B?= =?UTF-8?q?=E8=A1=A5=20tmpv.mjs=20=E9=85=8D=E6=96=B9=E4=B8=8E=20/draft=3D?= =?UTF-8?q?=E9=87=87=E9=9B=86=E7=AE=B1=20/offline=3D=E5=BE=85=E5=8F=91?= =?UTF-8?q?=E5=B8=83=20=E8=B7=AF=E7=94=B1=E4=BA=8B=E5=AE=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +++++-- temu建品/STATUS.md | 30 ++++++++++++++++++ temu建品/tm_find3.mjs | 24 ++++++++++++++ temu建品/tmpv.mjs | 73 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 temu建品/tm_find3.mjs create mode 100644 temu建品/tmpv.mjs diff --git a/README.md b/README.md index fca96d7..f7f9fd4 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ | Walmart | Snowmountain | **46** | 完成 | | Ozon | Yejet | **47** | 已建;改价 40/47 | | SHEIN | 4 家分品类 | 0 | 配方跑通第一条,40 条待批量 | -| Temu 全托管 | ①Alimingpro | 0 | 差「预览图」一步 | +| Temu 全托管 | ①Alimingpro | **1** | 配方跑通首条(预览图解了),待批量 | > `601964534137`(遥控器保护套)18 张图全带水印、无干净图,所有平台统一跳过。 @@ -57,17 +57,20 @@ function wrap(o, n, l) { | Walmart | `walmart建品/` | 保存体是 **zip 的 multipart**;Brand 必须点「无品牌」;图片必须 **hover** 开下拉走「网络图片」 | | 速卖通 | `速卖通建品/` | `check_box` 属性值**必须是数组**;`idStr ≠ id` | | SHEIN | `shein建品/` | 店铺**锁品类**;类目要**两个规格**;**加规格会重建 SKU 行** | -| Temu | `temu建品/` | 类目 1038 个从 store 直读;规格是 `.theme-value-add`(与 SHEIN 不同构) | +| Temu | `temu建品/` | 类目 1038 个从 store 直读;规格是 `.theme-value-add`(与 SHEIN 不同构);**预览图要逐行点单元格** → 引用产品轮播图 → 全部 → 选择 | ## 通用技术事实 - Chrome CDP 端口 **9223**,profile `/tmp/dxm7` +- **列表路由:`/draft` = 采集箱,`/offline` = 待发布**(找不到刚存的货,先确认进的是 `offline`) + - Temu 全托管 `/web/temu/choiceTemuList/*`;Temu 半托管 POP `/web/popTemu/pageList/*` + - Temu 全托管保存接口 `/api/pddkjProduct/add*.json`(拼多多跨境 = Temu 全托管) - 站点是 Vue3 + Pinia: `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia._s.get('').$state` - 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` @@ -79,6 +82,8 @@ function wrap(o, n, l) { 8. **类目决定是否强制资质文件**(速卖通某些类目要上传说明书) 9. **SHEIN 店铺锁品类** → 不能用「首字母第一家店」 10. `pkill -f ` 会自杀 → 用 `[.]` 技巧(`pgrep -f 'foo[.]sh'`) +11. **SKU 表里「一格一图」的弹窗是单格的** → 勾「全部」只作用当前行,N 行要走 N 遍 + (Temu 预览图就是这样:点 `.sku-image` 单元格 → 引用产品轮播图 → 全部 → 选择) ## EAN-13 台账(湖北易千亿彩 · 69803093 段) diff --git a/temu建品/STATUS.md b/temu建品/STATUS.md index fe381d6..8d11578 100644 --- a/temu建品/STATUS.md +++ b/temu建品/STATUS.md @@ -56,3 +56,33 @@ Temu 的规格机制和 SHEIN **不同构**——没有 `theme-select-box`, `预览图 | 颜色 | 尺码 | SKU货号 | EAN UPC ISBN | 申报价格(CNY) | 尺寸(cm) | 重量(g) | 净重(g) | 建议售价 | SKU分类` ⚠️ EAN 用公司台账下一段:pc 3447 起(Walmart 已用 3400~3446) + +--- + +## 更新3:预览图已解 → **首条已保存进待发布(2026-09-20)** + +### 解法(点单元格,不是点表头) +表头那个「批量」下拉是**通用图片工具**(批量传图/压缩/加水印…),跟预览图无关,别点它。 +真正的入口是**单元格本身**: + +1. 用**真实鼠标事件**点 `.sku-image` 单元格(`mouseMoved` → `mousePressed` → `mouseReleased`; + DOM click 打不开) +2. 弹出下拉 `.myj-image-drop`:`本地图片 / 空间图片 / 网络图片 / **引用产品轮播图** / 引用采集图片` +3. 点「**引用产品轮播图**」→ 弹窗「**引用产品图片**」(列出产品轮播图,每个带「选择图片」) +4. 勾「**全部**」→ 点「**选择**」 +5. 行数据出现 `image: {url, imgUrl, name, width, height}`,单元格变成真图 + +**关键坑:这个弹窗是「单格」的**——勾全部只作用**当前那一行**。N 行就要走 N 遍。 +(`全部` = 把那 5 张轮播图都塞给这一格,单元格只显示第一张。) + +现成脚本:`tmpv.mjs`(`node tmpv.mjs` 给所有 SKU 行挂图,`node tmpv.mjs 1 3` 只挂指定行) + +### 结果 +`/web/temu/choiceTemuList/offline`(**待发布**)搜标题命中: +`Hair Clips Set for Women Girls Fashion Hair Accessories 「①Alimingpro」 ... TM-TEST-1 TM-TEST-2 CNY 12.90` +保存成功弹「您的产品已保存到「待发布」,请在待发布页完成发布!」 + +### ⚠️ 路由纠正 +店小秘的列表路由是 **`/draft` = 采集箱、`/offline` = 待发布**(之前搜 `/draft` 找不到东西就是这个原因)。 +Temu 全托管列表 = `/web/temu/choiceTemuList/{draft|offline|online}`; +Temu 半托管 POP = `/web/popTemu/pageList/*`。保存接口是 `/api/pddkjProduct/add*.json`(拼多多跨境=Temu 全托管)。 diff --git a/temu建品/tm_find3.mjs b/temu建品/tm_find3.mjs new file mode 100644 index 0000000..64743f0 --- /dev/null +++ b/temu建品/tm_find3.mjs @@ -0,0 +1,24 @@ +import { newTarget, attach, SAN } from '/tmp/cdp3.mjs'; +const wait=ms=>new Promise(z=>setTimeout(z,ms)); +const path='/web/temu/choiceTemuList/offline'; +await newTarget('https://www.dianxiaomi.com'+path); +await wait(11000); +const c = await attach('/choiceTemuList/offline'); +if(!c){console.log('attach fail');process.exit(1);} +const set = await c.evalJS(`(()=>{ + const el=[...document.querySelectorAll('input.ant-input')].filter(e=>e.offsetParent!==null).find(e=>!e.placeholder||e.placeholder===' '); + if(!el) return 'no-input'; + const setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; + setter.call(el,'Hair Clips Set for Women'); el.dispatchEvent(new Event('input',{bubbles:true})); return 'set';})()`); +console.log('输入:', String(set)); +await wait(700); +await c.evalJS(`(()=>{const b=[...document.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(9000); +const o = await c.evalJS(`(()=>{ + const rows=[...document.querySelectorAll('.vxe-body--row')].map(r=>(r.innerText||'').replace(/\\s+/g,' ').trim()); + const body=(document.body.innerText||'').replace(/\\s+/g,' '); const m=body.match(/共\\s*\\d+\\s*条/); + return JSON.stringify({total:m?m[0]:'', n:rows.length, first:rows.slice(0,3).map(t=>t.slice(0,150))});})()`); +console.log('待发布搜索:', SAN(String(o)).slice(0,900)); +try{ await c.shot('/tmp/temu_offline.jpg'); console.log('shot ok'); }catch(e){ console.log('shot fail'); } +process.exit(0); diff --git a/temu建品/tmpv.mjs b/temu建品/tmpv.mjs new file mode 100644 index 0000000..efce591 --- /dev/null +++ b/temu建品/tmpv.mjs @@ -0,0 +1,73 @@ +/** + * Temu 全托管:给 SKU 表「预览图」列批量挂图(已跑通,保存成功) + * + * 用法: node tmpv.mjs # 给所有 SKU 行挂图 + * node tmpv.mjs 1 3 # 只给第 1、3 行(0 基)挂图 + * + * 交互链(每一行都要走一遍,弹窗是「单格」的,选一次只作用当前行): + * 点 .sku-image 单元格 + * → 下拉 .myj-image-drop 里点「引用产品轮播图」 + * → 弹窗「引用产品图片」勾「全部」→ 点「选择」 + * 结果:行数据里出现 image: {url, imgUrl},单元格 img.src 变成 alicdn 真图。 + * + * 注意:必须用真实鼠标事件点单元格(先 mouseMoved 再 press/release), + * DOM click 打不开这个下拉。 + */ +import { attach } from '/tmp/cdp3.mjs'; +const wait = ms => new Promise(z => setTimeout(z, ms)); +const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`; + +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 = []; + 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); + } + return done; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const c = await attach('/web/temu/add'); + if (!c) { console.log('没有 Temu 建品页'); process.exit(1); } + const idxs = process.argv.slice(2).map(Number).filter(n => !Number.isNaN(n)); + const done = await fillPreviewImages(c, idxs); + const chk = await c.evalJS(`(()=>{const p=${P}; + return JSON.stringify(p._s.get('temuAddSkuDataStore').$state.formState.skuDataList.map(r=>({id:r._id, img:r.image?(r.image.url||'').slice(-38):'NONE'})));})()`); + console.log('已挂图行:', JSON.stringify(done)); + console.log('核对:', String(chk).slice(0, 500)); + process.exit(0); +}