Temu 全托管:解「预览图」静默拦截(逐行点单元格→引用产品轮播图),首条已进待发布;补 tmpv.mjs 配方与 /draft=采集箱 /offline=待发布 路由事实

This commit is contained in:
admin9webs committed 2026-09-20 17:45:18 +08:00
1 parent 719ad2b609
commit cdde45ed9a
4 files changed
+135 -3

No files matched your search

+8 -3
View File
@@ -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('<store>').$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 <pat>` 会自杀 → 用 `[.]` 技巧(`pgrep -f 'foo[.]sh'`)
11. **SKU 表里「一格一图」的弹窗是单格的** → 勾「全部」只作用当前行,N 行要走 N 遍
(Temu 预览图就是这样:点 `.sku-image` 单元格 → 引用产品轮播图 → 全部 → 选择)
## EAN-13 台账(湖北易千亿彩 · 69803093 段)
+30
View File
@@ -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 全托管)。
+24
View File
@@ -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);
+73
View File
@@ -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);
}