SHEIN 40/40 + Temu 全托管 46/46 批量进待发布;补 shbatch/tmbatch 配方、类目弹窗必须关、SHEIN 规格上限3、必填属性兜底、EAN 游标落盘
This commit is contained in:
1 parent
cdde45ed9a
commit
fd4cc6619b
16 files changed
+1260
-41
No files matched your search
@@ -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 <pid>` 单条 · `--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=<rowid>`(DOM click 无效,一次没关标签就会攒一堆)。
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"next": 3552,
|
||||
"at": "2026-09-20T11:48:20.937972Z"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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}`);
|
||||
+47
-34
@@ -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); }
|
||||
|
||||
Reference in new issue
Block a user