From c69952bd592864f88b185f7dbfbc9e04de67536b Mon Sep 17 00:00:00 2001 From: admin9webs Date: Sun, 20 Sep 2026 19:50:04 +0800 Subject: [PATCH] =?UTF-8?q?NEXT-ROUND-PROMPT=20=E6=9B=B4=E6=96=B0=E5=88=B0?= =?UTF-8?q?=E4=B8=8B=E4=B8=80=E8=BD=AE=EF=BC=9AEAN=20=E5=9B=9E=20S03=20?= =?UTF-8?q?=E7=BD=AE=E9=A1=B6=E3=80=81=E5=89=A9=E4=BD=995=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0/=E5=A6=99=E6=89=8B=EF=BC=9B=E8=A1=A5=20SHEIN=E2=89=A0?= =?UTF-8?q?Temu=20=E8=A7=84=E6=A0=BC=E5=B7=AE=E5=BC=82=E8=A1=A8=E3=80=81cd?= =?UTF-8?q?p3.mjs=20=E5=A4=87=E4=BB=BD=E4=B8=8E=E4=B8=A2=E5=A4=B1=E9=87=8D?= =?UTF-8?q?=E5=BB=BA=E3=80=81=E9=92=A9=E5=AD=90=E6=96=B9=E6=B3=95=E8=AE=BA?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NEXT-ROUND-PROMPT.md | 72 ++++++++++++++++++++++++++------------------ README.md | 6 +++- cdp3.mjs | 37 +++++++++++++++++++++++ 静默拦截诊断/HOOK.md | 14 +++++++++ 4 files changed, 98 insertions(+), 31 deletions(-) create mode 100644 cdp3.mjs diff --git a/NEXT-ROUND-PROMPT.md b/NEXT-ROUND-PROMPT.md index 6c3356c..380c155 100644 --- a/NEXT-ROUND-PROMPT.md +++ b/NEXT-ROUND-PROMPT.md @@ -1,7 +1,7 @@ # 下一轮提示词(整份复制给新会话即可接着干) > ⚠️ **先读 `README.md` 和 `静默拦截诊断/HOOK.md`,再动手。** -> ⭐ **遇到「点了保存没反应」→ 第一件事是挂钩子,不要猜字段**(见下面第 2 节,最显眼那一段)。 +> ⭐ **遇到「点了保存没反应」→ 第一件事是挂钩子,不要猜字段**(见下面第 2 节,全篇最重要的一段)。 --- @@ -9,7 +9,7 @@ | 文件 | 读它干什么 | |---|---| -| **`README.md`**(仓库根) | 战果总表、各平台要点速查、通用技术事实、**十条会重复踩的坑**、EAN-13 台账 | +| **`README.md`**(仓库根) | 战果总表、各平台要点速查、通用技术事实、**十五条会重复踩的坑**、EAN-13 台账 | | **`静默拦截诊断/HOOK.md`** | 保存静默拦截的完整诊断步骤 + 已定位的各平台被吞报错 | 仓库(Gitea):`http://192.168.9.206:3000/admin9webs/dianxiaomi-crossborder-playbook` @@ -20,6 +20,10 @@ ## 2. ⭐⭐ 最显眼的一条:遇到「点了保存没反应」,第一件事是挂钩子,不要猜字段 ⭐⭐ +**为什么单独占一节**:这是整轮唯一一条**方法论级别**的收获。之前在 SHEIN 和 Temu 上 +反复猜「到底哪个字段没填」,几十轮全部无效;挂上钩子后**两个平台的真报错一次全暴露**。 +放在这个位置,是为了让新会话在**踩坑之前**就看见,而不是踩完才翻到。 + **症状**:点「保存」→ **不发 POST**、**无界面提示**、**无 `.ant-form-item-has-error`**。 **根因**:校验确实失败了,但报错被 **`MYJ.message.error` 吞掉**;而且报错是**层层递进**的 (修完 A 才显示 B),肉眼和 DOM 扫描都抓不到。 @@ -35,12 +39,12 @@ function wrap(o,n,l){const f=o[n];o[n]=function(){window.__cap.push(l+': '+[...a 然后 **点保存 → 等 ~9 秒 → 读 `window.__cap`**,被吞掉的真实报错就在里面。 -- 现成脚本:`静默拦截诊断/shhook.mjs`(SHEIN)、`静默拦截诊断/tmhook.mjs`(Temu)—— - 除了钩子还会把 `.ant-form-item-explain-error` 的行内错误一起打出来。 +- 现成脚本:`静默拦截诊断/shhook.mjs`(SHEIN)、`静默拦截诊断/tmhook.mjs`(Temu); + 批量脚本里用 `shein建品/sh_lib.mjs` 的 `installHook(c)` / `readHook(c)`。 - **「必填项扫描」不够**:扫 `.ant-form-item-required` 且为空,会漏掉**条件必填** - (Temu 的省份,只有「产地=中国」才出现)和**类目相关的文件字段** - (速卖通的商品说明书,有些类目强制、有些不强制)。 -- 上一轮在 SHEIN 和 Temu 上盲试了几十轮没进展,挂上钩子后**两个平台一次全暴露**。 + (Temu 的省份,只有「产地=中国」才出现)、**类目相关的文件字段**(速卖通的商品说明书) + 和 **`.attr-label.required` 这类类目动态属性**(SHEIN 的「特征 Features」,不填只报 + 「产品信息中有错误,请检查」)。 --- @@ -51,41 +55,49 @@ function wrap(o,n,l){const f=o[n];o[n]=function(){window.__cap.push(l+': '+[...a ## 4. 已完成 -TikTok 47 / Shopee 47 / 速卖通 47 / Walmart 46 条已在待发布;Ozon 47 条已建、改价 40/47。 -各平台的配方脚本、输入数据、逐条日志都在仓库对应目录里。 +- TikTok 47 / Shopee 47 / 速卖通 47 / Walmart 46 / Ozon 47(改价 40/47) +- **SHEIN 40/40**(`shein建品/shbatch.mjs`,按 `shein_route.json` 分 4 家店;1 条因 + ①Shein-武汉九个网-箱包 报「商家无可用的发品额度」改挂 ②Shein-易世通达-箱包) +- **Temu 全托管 47**(`temu建品/tmbatch.mjs` 批量 46/46 + 手工首条),逐条结果在 `tm_done.json` ## 5. 这轮要做的(按优先级) -1. **Temu 解「请上传预览图」** - Temu 的报错是层层递进的,前面 4 层我已经解掉了(省份 → 变种 → 尺寸 → 净重<重量), - 现在只剩 `MYJ.error: 请上传预览图`。SKU 表「预览图」列需要**每个变种行的真实图片**, - 不是文本。大概率是点单元格弹图片空间。参考 Walmart 的解法:**hover 触发的下拉要先 - `Input.dispatchMouseEvent` 发 `mouseMoved` 再 `click`**。 - Temu 的规格是 `.theme-value-add` 块(值输入 `input[class*="!w-200"]` + 添加按钮), - **必须手动输入值再点添加**,直接改 store 无效。 - 解出来后把 `temu建品/` 的配方补完。 +0. **EAN 台账回 S03(最优先,硬约束)** + Temu 这 46 个号目前只落在 KVM-2 的 `temu建品/ean_temu_used.txt` + (pc 3447~3551,游标 `ean_cursor.json` → **3552**)。KVM-2 够不到 S03, + **需要在 S03 上把这份台账并进 `/root/ean_*.txt`**,之后从 3552 续号。 -2. **SHEIN 批量** - `shein建品/RECIPE.md` 已经有跑通第一条的完整步骤。把它写成 `shbatch.mjs` 循环跑 40 条, - 按品类分到 4 家店(美妆 18 / 文具 13 / 箱包 8 / 内睡袜子 1),路由见 `shein_route.json`。 - **注意顺序**:先选规格 → 再填 SKU 行 → 再填尺码表(加规格会重建 SKU 行)。 +1. **继续铺剩余店小秘平台**:Joom(84 家)、Lazada(24 家)、aliChoice(64 家)、 + pddkj(15 家)、fruugo(3 家)。店铺用**字母序/圈号自然序第一家** + (除 SHEIN 那种锁品类的)。这 5 个平台的字段落点和第一家店规则都还没勘,属于新探路。 -3. **Temu 批量**(解完预览图之后) +2. **妙手各平台**(店小秘全部完成后) -4. **继续铺剩余店小秘平台**:Joom(84 家)、Lazada(24 家)、aliChoice(64 家)、 - pddkj(15 家)、fruugo(3 家)。店铺用**字母序/圈号自然序第一家**(除 SHEIN 那种锁品类的)。 - -5. **妙手各平台**(店小秘全部完成后) +3. **可选重构**:`shein建品/sh_lib.mjs` + `sh_build.mjs`(`newPage`/`selectCategory`/ + `fillRequiredSelects`/`installHook`)已是平台无关骨架,铺新平台先复用它, + 别再从头写 `attach(url)`——同 URL 有多个标签时 `attach` 会抢到错的那个。 ## 6. 硬约束 - 平台「首字母第一家店」规则(SHEIN 除外,它锁品类必须分店) - 图片一律**外链 alicdn**,且必须**无水印** -- **EAN 台账在 S03 的 `/root/ean_*.txt`**,KVM-2 够不到。本批已分配 pc 3400~3446(Walmart), - Temu 用 3447 起。**用了就要在 S03 落盘**,否则撞号。 +- **EAN 台账在 S03 的 `/root/ean_*.txt`**,KVM-2 够不到。Walmart 已用 pc 3400~3446, + Temu 已用 pc 3447~3551(游标 3552)。**用了就要在 S03 落盘**,否则撞号。 ## 7. 环境 - Chrome CDP **9223**,profile `/tmp/dxm7`;VNC `http://192.168.9.122:6097/vnc.html` -- CDP helper `/tmp/cdp3.mjs`(ESM,`newTarget`/`attach`/`CDP`/`SAN`,**不能用 require**) -- 批量跑动辄 1 小时,用 `nohup ... &` 后台跑 + 逐条日志,别前台等 +- CDP helper `/tmp/cdp3.mjs`(ESM:`newTarget`/`attach`/`CDP`/`SAN`,**不能用 require**) + ⚠️ **它是 /tmp 下的临时文件,重启机器可能就没了**——仓库根目录留了备份 `cdp3.mjs`, + 丢了就 `cp cdp3.mjs /tmp/cdp3.mjs` 再跑。 +- 批量动辄 1 小时:`setsid nohup node xxx.mjs > xxx.log 2>&1 < /dev/null &`; + 脚本自己往 `*_done.json` 写断点,失败直接带 pid 重跑(`node shbatch.mjs `)。 + +## 8. 机械差异,别互套(SHEIN ≠ Temu) + +| | SHEIN | Temu 全托管 | +|---|---|---| +| 规格控件 | `.theme-select-box` 点主题 → `.sku-info-box` 里勾值 | 两个 `.theme-value-add` 块,`input[class*="!w-200"]` **手动输值 + 点「添加」**(直接改 store 无效) | +| 规格数量 | **最多 3 个**(颜色已占 1 个) | 颜色 + 尺码各一个值 | +| 图片 | store 直接吃外链数组 | 主图**必须满 5 张**;预览图**逐行**点 `.sku-image` → 引用产品轮播图 → 全部 → 选择 | +| 类目选择 | 弹窗搜索 → 点 `.search-result-item`(里面嵌 ``)→ **弹窗必须确认关掉** | 同一个弹窗,但**按最后一级路径精确匹配**(否则「口哨」会选到「狗口哨」) | diff --git a/README.md b/README.md index 3c4afd4..96f5c9f 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,11 @@ function wrap(o, n, l) { - 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** +- CDP helper:`cdp3.mjs`(`newTarget`/`attach`/`closeAllTabs`/`CDP`/`SAN`),ESM,**不能用 require** + - 平时跑在 `/tmp/cdp3.mjs`,**是 /tmp 下的临时文件,重启机器会丢** → 仓库根目录有备份, + 丢了 `cp cdp3.mjs /tmp/cdp3.mjs` 即可 + - ⚠️ 同一个 URL 开了多个标签时,`attach(url)` 会抢到**最后一个**匹配的标签。 + 批量脚本请用 `shein建品/sh_lib.mjs` 的 `newPage(url)`:按 target 直连,不会抢错 - VNC:`http://192.168.9.122:6097/vnc.html` 或 `:6100`(免密) ## 十五条会重复踩的坑 diff --git a/cdp3.mjs b/cdp3.mjs new file mode 100644 index 0000000..032a2ce --- /dev/null +++ b/cdp3.mjs @@ -0,0 +1,37 @@ +const BASE = process.env.CDP_BASE || 'http://127.0.0.1:9223'; +export const SAN = s => (s||'').replace(/data:[a-z+/-]+;base64,[A-Za-z0-9+/=\s]{40,}/gi,'').replace(/[A-Za-z0-9+/]{100,}={0,2}/g,'').replace(/\s+/g,' ').trim(); +export async function newTarget(url='about:blank'){ + const r=await fetch(`${BASE}/json/new?${encodeURIComponent(url)}`,{method:'PUT'}); + return r.ok? r.json() : (await fetch(`${BASE}/json/new?${encodeURIComponent(url)}`)).json(); +} +export async function attach(match){ + const list=await (await fetch(`${BASE}/json/list`)).json(); + const ps=list.filter(t=>t.type==='page'&&(!match||t.url.includes(match))); + if(!ps.length) return null; + const c=await CDP.connect(ps[ps.length-1].webSocketDebuggerUrl); + await c.send('Page.enable'); await c.send('Runtime.enable'); return c; +} +export async function closeAllTabs(){ + const list=await (await fetch(`${BASE}/json/list`)).json(); + for(const t of list.filter(x=>x.type==='page')) await fetch(`${BASE}/json/close/${t.id}`).catch(()=>{}); +} +export class CDP{ + constructor(ws){this.ws=ws;this.id=0;this.pending=new Map();this.handlers=[];} + static async connect(u){const ws=new WebSocket(u);await new Promise((s,j)=>{ws.onopen=s;ws.onerror=j;}); + const c=new CDP(ws); + ws.onmessage=ev=>{const m=JSON.parse(ev.data); + if(m.id&&c.pending.has(m.id)){const{res,rej}=c.pending.get(m.id);c.pending.delete(m.id); + m.error?rej(new Error(JSON.stringify(m.error))):res(m.result);} + else if(m.method){for(const h of c.handlers)h(m);}}; + return c;} + send(method,params={}){const id=++this.id; + return new Promise((res,rej)=>{this.pending.set(id,{res,rej}); + this.ws.send(JSON.stringify({id,method,params})); + setTimeout(()=>{if(this.pending.has(id)){this.pending.delete(id);rej(new Error('timeout '+method));}},60000);});} + on(f){this.handlers.push(f);} + async evalJS(e,aw=true){const r=await this.send('Runtime.evaluate',{expression:e,returnByValue:true,awaitPromise:aw}); + if(r.exceptionDetails)throw new Error(JSON.stringify(r.exceptionDetails).slice(0,220));return r.result?.value;} + async shot(p){const r=await this.send('Page.captureScreenshot',{format:'jpeg',quality:80}); + const fs=await import('fs'); fs.writeFileSync(p,Buffer.from(r.data,'base64')); return p;} + close(){try{this.ws.close();}catch{}} +} diff --git a/静默拦截诊断/HOOK.md b/静默拦截诊断/HOOK.md index aba2b21..546a930 100644 --- a/静默拦截诊断/HOOK.md +++ b/静默拦截诊断/HOOK.md @@ -45,3 +45,17 @@ function wrap(obj,name,label){ `必填项扫描`(找 `.ant-form-item-required` 且为空)**不够**—— 条件必填字段(Temu 省份)和类目相关的文件字段(速卖通说明书)都不在这个扫描范围内。 以后遇到「点了没反应」,第一步就该挂这个钩子。 + +## 为什么这条要单独记住(整轮唯一的方法论级收获) +在 SHEIN 和 Temu 上,我们花了**几十轮**去猜「到底哪个字段没填」:扫 DOM、翻 store、 +逐个试填——**全部无效**。挂上钩子后,两个平台的真实报错**一次全暴露** +(SHEIN `有必选规格未选择`、Temu `产地…省份必填` → `产品变种不能为空` → … 层层递进)。 +所以它不是「一个调试技巧」,而是**顺序问题**:先让平台把话说出来,再动手填。 + +配套的三条(都是这一轮才踩出来的): +1. **类目弹窗没确认关掉就往下走** → 后面的点击全被挡住,最后报的却是别的错 + (Temu 第一轮 13 条都表现为「请上传预览图」) +2. **`.attr-label.required`** 这类类目动态属性(下拉、勾选组)不填,只给一句 + 「产品信息中有错误,请检查」——必填扫描同样扫不到 +3. **同一个弹窗可能是「单格」的**(Temu 预览图:勾「全部」只作用当前行), + N 行要走 N 遍