aliChoice 跑通并开始批量:物流属性(保存两遍)+CKEditor 描述;配方与批量脚本入库

This commit is contained in:
admin9webs committed 2026-09-20 21:40:42 +08:00
1 parent 924ca86fdc
commit 0466698490
5 files changed
+363 -10

No files matched your search

+28 -9
View File
@@ -1,21 +1,21 @@
# aliChoice(速卖通全托管)配方 — 差最后一步,下一轮从「物流属性」接着做
# aliChoice(速卖通全托管)配方 — **已跑通**(批量进行中)
路由:`https://www.dianxiaomi.com/web/smtChoice/add` 列表:`/web/smtChoice/{draft=采集箱, offline=待发布}`
## 已经打通的(`alc_try1.mjs`,一条完整填到最后一步)
## 已经打通的(批量脚本 `alcbatch.mjs`,单条已验证保存成功)
1. **店铺**:`店铺名称` 下拉(实测 2 家:`XYZ Store` / `UniStyle Bag Store`),取第一家 → `shopId=4393191`
2. **备货类型**:下拉 `国内履约` / `海外备货` → 取第一个(→ `productType='1'`)
3. **类目**:`选择分类` 弹窗 → 用 `sh_build.selectCategory(c, cat_kw, cat_kw)`(与 SHEIN/Temu 同一套)
4. **标题** → `smtChoiceAddBasicStore.formState.subject`(⚠️ **不是 Joom 的 `name`**)
5. **主图** → `smtChoiceAddProductStore.formState.mainImage[]`(吃 `{url,imgUrl,name,width,height}`)
6. **PC端描述** → `smtChoiceAddStore.describeInfoData.detail`(DOM 里是个 `TEXTAREA`)
6. **PC端描述** → `smtChoiceAddStore.describeInfoData.detail` + **CKEditor setData**(见下)
7. **SKU 行** → `smtChoiceSkuDataStore.formState.skuDataList[]`
字段:`skuPrice / skuCode / scItemBarCode / packageWeight / packageLength / packageWidth /
packageHeight / logisticAttrListJson / logisticAttrList / imageList`
8. **必填类目属性**(品牌 / 高关注化学品 / 产地)→ `fillRequiredSelects()` 现已能自动填
(产地会输「中国」过滤后选 `中国大陆`)
## ⛔ 当前唯一卡点:「请选择物流属性」
## ✅ 「请选择物流属性」已解(保存两遍)
保存时钩子给 `MYJ.error: 校验失败`,DOM 里是一个**没有 label** 的报错块:
```html
@@ -25,15 +25,34 @@
</div>
```
- 它带一个 `icon_edit2 link` 的编辑入口 → 点开大概率是「物流属性」弹窗/抽屉
- SKU 行里有 `logisticAttrListJson` / `logisticAttrList` 两个字段空着,应该就是它要填的
- 下一步:点那个 `icon_edit2`,dump 弹窗结构(很可能要按类目选物流渠道/时效)
解法(**保存两遍**):
1. 先点一次保存 → 报错块出现
2. 点报错块里的 `i.icon_edit2` → 弹出「修改物流属性」modal(20 个复选框,按大类分组:
禁运 / 普货 / 纯电 / 强磁 / 特货(液体·粉末·膏体·其他) / 特敏 / 内电(8 种电池) / 外电 / 弱磁)
3. 勾 **普货** → 点「确定」→ 再点保存 → `您的产品已保存到「待发布」`
## 另一个未验证项:4 个规格组都是 0 选项
对应 SKU 行的 `logisticAttrListJson` / `logisticAttrList` 两个字段。
## ✅ PC端描述是 CKEditor,写 textarea 没用
DOM 里那个 `TEXTAREA` 是 `display:none` 的藏匿字段,真实控件是 **CKEditor**
(aria-label「所见即所得编辑器, cked…」)。两种赋值方式,实测都写上才稳:
```js
// 1) 给编辑器实例 setData(本例 window.CKEDITOR.instances 有 2 个实例)
Object.values(window.CKEDITOR.instances).forEach(x => x.setData(html));
// 2) 同时写 store
pinia._s.get('smtChoiceAddStore').$state.describeInfoData.detail = html;
```
## 另一个未验证项:4 个规格组都是 0 选项## 另一个未验证项:4 个规格组都是 0 选项
`smtChoiceSkuAttrStore.formState.skuAttrs` 有 4 组:
`Ships From(200007763) / Color(14) / Number of Pcs(200116262) / Bulk Buy(200483263)`,
但 `options` 都是 0(值在 `_options` 或要异步加载),DOM 里能看到值(卡其色/粉红/6Pc/Set…)。
**未验证变种值是否必须选**——先解物流属性,再看保存是否又冒出新的一层。
**实测:不选变种值也能保存成功**(保存链路里没有因为变种报错),所以批量里先不选。
## 单条顺序(批量脚本里的固定顺序)
`店铺 → 备货类型 → 类目 → 标题/主图/SKU行 → 必填属性(品牌/高关注化学品/产地) → PC端描述(CKEditor)
→ 保存① → 物流属性(普货) → 保存② → 待发布`
## 本轮顺手修好的共享 bug(`shein建品/sh_build.mjs` 的 `fillRequiredSelects`)
之前它在 aliChoice 上**一个必填属性都填不上**,三个原因,都已修:
+97
View File
@@ -0,0 +1,97 @@
import { newPage, closePage, wait, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { selectCategory, fillRequiredSelects } from '/home/admin9webs/shein建品/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 = process.argv[2] || '676236596821';
const it = JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json','utf8'))[pid];
const IMGS = (it.imgs||[]).slice(0,6);
const c = await newPage('https://www.dianxiaomi.com/web/smtChoice/add');
await wait(12000); await installHook(c);
// 店
const sp = JSON.parse(String(await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const s=f.querySelector('.ant-select-selector'); 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)]);})()`)));
await c.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:sp[0],y:sp[1],buttons:0}); await wait(300);
for(const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent',{type:t,x:sp[0],y:sp[1],button:'left',clickCount:1,buttons:1});
await wait(2000);
await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
const op=[...dd.querySelectorAll('.ant-select-item-option')][0]; 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 1;})()`);
await wait(6000);
// 备货类型
const bp = JSON.parse(String(await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/备货类型/.test((e.querySelector('label')||{}).innerText||''));
const s=f.querySelector('.ant-select-selector'); 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)]);})()`)));
await c.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:bp[0],y:bp[1],buttons:0}); await wait(300);
for(const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent',{type:t,x:bp[0],y:bp[1],button:'left',clickCount:1,buttons:1});
await wait(2000);
await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
const op=[...dd.querySelectorAll('.ant-select-item-option')][0]; 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 1;})()`);
await wait(3000);
console.log('类目:', S(String(await selectCategory(c, it.cat_kw || '发夹', it.cat_kw))).slice(0,120));
await wait(6000);
// 结构 dump
const shape = await c.evalJS(`(()=>{const p=${P};
const sd=p._s.get('smtChoiceSkuDataStore').$state.formState;
const sku=JSON.stringify(sd.skuDataList&&sd.skuDataList[0]||{}).slice(0,700);
const desc=p._s.get('smtChoiceAddStore').$state.describeInfoData;
const sa=p._s.get('smtChoiceSkuAttrStore').$state.formState.skuAttrs||[];
return JSON.stringify({skuRow:sku, descKeys:Object.keys(desc||{}).slice(0,10), desc:JSON.stringify(desc||{}).slice(0,300),
attrs:sa.map(a=>a.attributeId+':'+a.attributeName+' opts='+((a.options||a._options||[]).length))});})()`);
console.log('结构:', S(String(shape)).slice(0,1200));
// PC端描述 DOM
const dom = await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/PC端描述/.test((e.querySelector('label')||{}).innerText||''));
if(!f) return 'no-item';
return JSON.stringify({tags:[...f.querySelectorAll('input,textarea,[contenteditable],.ql-editor,.w-e-text')].map(x=>x.tagName+(x.className?'.'+String(x.className).slice(0,30):'')).slice(0,6),
txt:(f.innerText||'').replace(/\\s+/g,' ').trim().slice(0,60)});})()`);
console.log('PC端描述:', S(String(dom)).slice(0,400));
// 填 store
const fill = await c.evalJS(`(()=>{const p=${P}; const imgs=${JSON.stringify(IMGS)};
const mk=(u,i)=>({url:u,imgUrl:u,name:'img'+(i+1)+'.jpg',width:800,height:800});
const b=p._s.get('smtChoiceAddBasicStore').$state.formState;
b.subject=${JSON.stringify(it.title)};
const pr=p._s.get('smtChoiceAddProductStore').$state.formState;
pr.mainImage=imgs.map(mk);
const sd=p._s.get('smtChoiceSkuDataStore').$state.formState;
(sd.skuDataList||[]).forEach((r,i)=>{ for(const k of ['skuPrice','price','skuStock','stock','inventory','skuSupplierCode','supplierCode','skuCode','code']) { if(k in r) { if(/price/i.test(k)) r[k]=String(${JSON.stringify(String(it.price_cny||'19.90'))}); else if(/stock|inventory/i.test(k)) r[k]='999'; else r[k]='AC-'+${JSON.stringify(pid)}+'-'+(i+1); } } });
const desc=p._s.get('smtChoiceAddStore').$state.describeInfoData;
if(desc) desc.detail=(desc.detail||'')+${JSON.stringify((it.bullets||[]).join('<br/>'))};
(sd.skuDataList||[]).forEach(r=>{ r.skuPrice=String(${JSON.stringify(String(it.price_cny||'19.90'))}); r.skuCode='AC-'+${JSON.stringify(pid)};
r.packageWeight='100'; r.packageLength='20'; r.packageWidth='15'; r.packageHeight='3'; });
return JSON.stringify({subject:b.subject.slice(0,20), imgs:pr.mainImage.length, rows:(sd.skuDataList||[]).length, desc:(desc&&desc.detail||'').length});})()`);
console.log('填充:', S(String(fill)).slice(0,300));
await wait(3500);
console.log('必填属性:', S(String(await fillRequiredSelects(c))).slice(0,300));
await wait(2000);
// 先触发一次保存校验,让「请选择物流属性」的报错块出现
await c.evalJS(`window.scrollTo(0,0)`); await wait(600);
await c.evalJS(`(()=>{const xs=[...document.querySelectorAll('button')].filter(e=>e.offsetParent!==null);
const b=xs.find(e=>/^\\s*保存\\s*$/.test((e.innerText||'').trim()))||xs.find(e=>/保存/.test(e.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(12000);
// 找报错块里的编辑入口
const info = await c.evalJS(`(()=>{
const bad=[...document.querySelectorAll('.ant-form-item-has-error')];
const out=bad.map(b=>({txt:(b.innerText||'').replace(/\\s+/g,' ').trim().slice(0,40), icons:[...b.querySelectorAll('i,a,.link')].map(x=>String(x.className).slice(0,40))}));
const link=document.querySelector('.ant-form-item-has-error .icon_edit2')||document.querySelector('.icon_edit2.link');
let pos='[]';
if(link){ link.scrollIntoView({block:'center'}); const r=link.getBoundingClientRect(); pos=JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]); }
return JSON.stringify({out, pos});})()`);
console.log('报错块:', S(String(info)).slice(0,500));
const pos = JSON.parse(String(JSON.parse(String(info)).pos || '[]'));
if(pos.length){
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(4000);
}
const dlg = await c.evalJS(`(()=>{
const ds=[...document.querySelectorAll('.ant-modal,.ant-drawer')].filter(e=>e.offsetParent!==null)
.map(e=>({cls:String(e.className).slice(0,40), txt:(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,320),
checks:e.querySelectorAll('.ant-checkbox-wrapper,.ant-radio-wrapper').length, selects:e.querySelectorAll('.ant-select').length,
btns:[...e.querySelectorAll('button')].map(b=>(b.innerText||'').trim().slice(0,12)).filter(Boolean).slice(0,6)}));
return JSON.stringify({n:ds.length, ds:ds.slice(0,2)});})()`);
console.log('弹层:', S(String(dlg)).slice(0,1200));
try{ await c.shot('/tmp/alc_logi.jpg'); console.log('shot ok'); }catch(e){}
await closePage(c); process.exit(0);
+78
View File
@@ -0,0 +1,78 @@
import { newPage, closePage, wait, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { selectCategory, fillRequiredSelects } from '/home/admin9webs/shein建品/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 = process.argv[2] || '676236596821';
const it = JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json','utf8'))[pid];
const IMGS = (it.imgs||[]).slice(0,6);
const c = await newPage('https://www.dianxiaomi.com/web/smtChoice/add');
await wait(12000); await installHook(c);
// 店
const sp = JSON.parse(String(await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const s=f.querySelector('.ant-select-selector'); 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)]);})()`)));
await c.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:sp[0],y:sp[1],buttons:0}); await wait(300);
for(const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent',{type:t,x:sp[0],y:sp[1],button:'left',clickCount:1,buttons:1});
await wait(2000);
await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
const op=[...dd.querySelectorAll('.ant-select-item-option')][0]; 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 1;})()`);
await wait(6000);
// 备货类型
const bp = JSON.parse(String(await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/备货类型/.test((e.querySelector('label')||{}).innerText||''));
const s=f.querySelector('.ant-select-selector'); 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)]);})()`)));
await c.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:bp[0],y:bp[1],buttons:0}); await wait(300);
for(const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent',{type:t,x:bp[0],y:bp[1],button:'left',clickCount:1,buttons:1});
await wait(2000);
await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
const op=[...dd.querySelectorAll('.ant-select-item-option')][0]; 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 1;})()`);
await wait(3000);
console.log('类目:', S(String(await selectCategory(c, it.cat_kw || '发夹', it.cat_kw))).slice(0,120));
await wait(6000);
// 结构 dump
const shape = await c.evalJS(`(()=>{const p=${P};
const sd=p._s.get('smtChoiceSkuDataStore').$state.formState;
const sku=JSON.stringify(sd.skuDataList&&sd.skuDataList[0]||{}).slice(0,700);
const desc=p._s.get('smtChoiceAddStore').$state.describeInfoData;
const sa=p._s.get('smtChoiceSkuAttrStore').$state.formState.skuAttrs||[];
return JSON.stringify({skuRow:sku, descKeys:Object.keys(desc||{}).slice(0,10), desc:JSON.stringify(desc||{}).slice(0,300),
attrs:sa.map(a=>a.attributeId+':'+a.attributeName+' opts='+((a.options||a._options||[]).length))});})()`);
console.log('结构:', S(String(shape)).slice(0,1200));
// PC端描述 DOM
const dom = await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/PC端描述/.test((e.querySelector('label')||{}).innerText||''));
if(!f) return 'no-item';
return JSON.stringify({tags:[...f.querySelectorAll('input,textarea,[contenteditable],.ql-editor,.w-e-text')].map(x=>x.tagName+(x.className?'.'+String(x.className).slice(0,30):'')).slice(0,6),
txt:(f.innerText||'').replace(/\\s+/g,' ').trim().slice(0,60)});})()`);
console.log('PC端描述:', S(String(dom)).slice(0,400));
// 填 store
const fill = await c.evalJS(`(()=>{const p=${P}; const imgs=${JSON.stringify(IMGS)};
const mk=(u,i)=>({url:u,imgUrl:u,name:'img'+(i+1)+'.jpg',width:800,height:800});
const b=p._s.get('smtChoiceAddBasicStore').$state.formState;
b.subject=${JSON.stringify(it.title)};
const pr=p._s.get('smtChoiceAddProductStore').$state.formState;
pr.mainImage=imgs.map(mk);
const sd=p._s.get('smtChoiceSkuDataStore').$state.formState;
(sd.skuDataList||[]).forEach((r,i)=>{ for(const k of ['skuPrice','price','skuStock','stock','inventory','skuSupplierCode','supplierCode','skuCode','code']) { if(k in r) { if(/price/i.test(k)) r[k]=String(${JSON.stringify(String(it.price_cny||'19.90'))}); else if(/stock|inventory/i.test(k)) r[k]='999'; else r[k]='AC-'+${JSON.stringify(pid)}+'-'+(i+1); } } });
const desc=p._s.get('smtChoiceAddStore').$state.describeInfoData;
if(desc) desc.detail=(desc.detail||'')+${JSON.stringify((it.bullets||[]).join('<br/>'))};
(sd.skuDataList||[]).forEach(r=>{ r.skuPrice=String(${JSON.stringify(String(it.price_cny||'19.90'))}); r.skuCode='AC-'+${JSON.stringify(pid)};
r.packageWeight='100'; r.packageLength='20'; r.packageWidth='15'; r.packageHeight='3'; });
return JSON.stringify({subject:b.subject.slice(0,20), imgs:pr.mainImage.length, rows:(sd.skuDataList||[]).length, desc:(desc&&desc.detail||'').length});})()`);
console.log('填充:', S(String(fill)).slice(0,300));
await wait(3500);
console.log('必填属性:', S(String(await fillRequiredSelects(c))).slice(0,300));
await wait(2000);
// 只 dump PC端描述 控件属性
const d = await c.evalJS(`(()=>{
const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/PC端描述/.test((e.querySelector('label')||{}).innerText||''));
if(!f) return 'no-item';
const ta=f.querySelector('textarea');
const info=ta?{ro:ta.readOnly,dis:ta.disabled,cls:String(ta.className).slice(0,50),ph:ta.placeholder||'',rect:JSON.stringify(ta.getBoundingClientRect()),style:(ta.getAttribute('style')||'').slice(0,80)}:'no-ta';
const btns=[...f.querySelectorAll('button,a,span')].filter(x=>(x.innerText||'').trim()).map(x=>(x.innerText||'').trim().slice(0,14)).slice(0,8);
return JSON.stringify({info, btns, html:f.innerHTML.replace(/\\s+/g,' ').slice(0,600)});})()`);
console.log('PC端描述详情:', S(String(d)).slice(0,1200));
try{ await c.shot('/tmp/alc_desc.jpg'); console.log('shot ok'); }catch(e){}
await closePage(c); process.exit(0);
+159
View File
@@ -0,0 +1,159 @@
/**
* aliChoice(速卖通全托管)批量建品
*
* 用法: node alcbatch.mjs [--dry] [pid...]
*
* 顺序:店铺(下拉第一家) → 备货类型(国内履约) → 类目(弹窗) → 标题(subject)/主图/PC端描述
* → SKU 行 → 必填类目属性(品牌/高关注化学品/产地) → **物流属性(先保存一次触发报错,
* 点报错里的 icon_edit2 → 勾「普货」→ 确定)** → 再保存
*/
import { readFileSync, writeFileSync, appendFileSync } from 'fs';
import { newPage, closePage, wait, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { selectCategory, fillRequiredSelects } from '/home/admin9webs/shein建品/sh_build.mjs';
const DIR = '/home/admin9webs/alichoice建品';
const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`;
const INPUT = JSON.parse(readFileSync('/home/admin9webs/tiktok建品/tk_input.json', 'utf8'));
const DONE_F = `${DIR}/alc_done.json`;
const LOG_F = `${DIR}/alc_batch.log`;
const SKIP = new Set(['601964534137']); // 遥控器保护套:无干净图,全平台统一跳过
const argv = process.argv.slice(2);
const dry = argv.includes('--dry');
const only = argv.filter(a => !a.startsWith('--'));
let tasks = Object.keys(INPUT).filter(pid => (INPUT[pid].imgs || []).length > 0).map(pid => ({ pid }));
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'); };
/** PC端描述:真实控件是 CKEditor(隐藏 textarea + 所见即所得),给编辑器实例 setData 才有效 */
async function fillDescription(c, text) {
return c.evalJS(`(()=>{const p=${P};
const html=${JSON.stringify(text)};
let how='none';
try{ if(window.CKEDITOR&&window.CKEDITOR.instances){ const inst=Object.values(window.CKEDITOR.instances);
if(inst.length){ inst.forEach(x=>{ try{ x.setData(html); }catch(e){} }); how='ckeditor:'+inst.length; } } }catch(e){}
const d=p._s.get('smtChoiceAddStore').$state.describeInfoData;
if(d){ d.detail=html; if('mobileDetail' in d) d.mobileDetail=html; how+='|store'; }
if(how==='none'){ const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null)
.find(e=>/PC端描述/.test((e.querySelector('label')||{}).innerText||''));
const ta=f?f.querySelector('textarea'):null;
if(ta){ const setter=Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype,'value').set;
setter.call(ta, html); ta.dispatchEvent(new Event('input',{bubbles:true})); how='textarea'; } }
return how;})()`);
}
async function pickFirstOption(c, labelRe) {
const p = JSON.parse(String(await c.evalJS(`(()=>{const f=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null)
.find(e=>new RegExp(${JSON.stringify(labelRe)}).test((e.querySelector('label')||{}).innerText||''));
const s=f?f.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 (!p.length) return 'no-select';
await c.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: p[0], y: p[1], buttons: 0 }); await wait(300);
for (const t of ['mousePressed', 'mouseReleased'])
await c.send('Input.dispatchMouseEvent', { type: t, x: p[0], y: p[1], button: 'left', clickCount: 1, buttons: 1 });
await wait(2000);
return 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,16);})()`);
}
async function clickSave(c) {
await c.evalJS(`window.scrollTo(0,0)`); await wait(500);
await c.evalJS(`(()=>{const xs=[...document.querySelectorAll('button')].filter(e=>e.offsetParent!==null);
const b=xs.find(e=>/^\\s*保存\\s*$/.test((e.innerText||'').trim()))||xs.find(e=>/保存/.test(e.innerText||''));
if(b) for(const ev of ['mousedown','mouseup','click']) b.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
return !!b;})()`);
}
/** 物流属性:先保存一次逼出「请选择物流属性」,点它的编辑入口 → 勾「普货」→ 确定 */
async function fixLogistics(c) {
const pos = JSON.parse(String(await c.evalJS(`(()=>{
const bad=[...document.querySelectorAll('.ant-form-item-has-error')].find(e=>/物流属性/.test(e.innerText||''));
if(!bad) return '[]';
const link=bad.querySelector('.icon_edit2')||bad.querySelector('i.link');
if(!link) return '[]'; link.scrollIntoView({block:'center'});
const r=link.getBoundingClientRect(); return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`)));
if (!pos.length) return 'no-entry';
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(3500);
const tick = await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
if(!m) return 'no-modal';
const ws=[...m.querySelectorAll('.ant-checkbox-wrapper')];
const w=ws.find(x=>/普货/.test(x.innerText||''))||ws[0];
if(!w) return 'no-checkbox';
for(const ev of ['mousedown','mouseup','click']) w.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
const i=w.querySelector('input'); if(i&&!i.checked) i.click();
const ok=[...m.querySelectorAll('button')].find(x=>/^\\s*确定\\s*$/.test((x.innerText||'').trim()));
if(ok) for(const ev of ['mousedown','mouseup','click']) ok.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
return 'ticked:'+(w.innerText||'').trim().slice(0,10);})()`);
await wait(3000);
return tick;
}
log(`aliChoice 批量开始:任务 ${tasks.length},dry=${dry}`);
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} kw=${it.cat_kw || '?'} :: ${String(it.title).slice(0, 40)}`);
let c;
try {
c = await newPage('https://www.dianxiaomi.com/web/smtChoice/add');
await wait(12000);
await installHook(c);
log(` 店铺: ${await pickFirstOption(c, '店铺名称')}`);
await wait(6000);
log(` 备货类型: ${await pickFirstOption(c, '备货类型')}`);
await wait(3000);
const cat = await selectCategory(c, it.cat_kw || '发夹', it.cat_kw);
log(` 类目: ${String(cat).slice(0, 110)}`);
if (!String(cat).startsWith('path:')) throw new Error('选类目失败:' + cat);
await wait(6000);
const fill = await c.evalJS(`(()=>{const p=${P}; const imgs=${JSON.stringify((it.imgs || []).slice(0, 6))};
const mk=(u,i)=>({url:u,imgUrl:u,name:'img'+(i+1)+'.jpg',width:800,height:800});
const b=p._s.get('smtChoiceAddBasicStore').$state.formState; b.subject=${JSON.stringify(it.title)};
const pr=p._s.get('smtChoiceAddProductStore').$state.formState; pr.mainImage=imgs.map(mk);
const sd=p._s.get('smtChoiceSkuDataStore').$state.formState;
(sd.skuDataList||[]).forEach((r,i)=>{ r.skuPrice=String(${JSON.stringify(String(it.price_cny || '19.90'))});
r.skuCode='AC-'+${JSON.stringify(t.pid)}+'-'+(i+1); r.packageWeight='100';
r.packageLength='20'; r.packageWidth='15'; r.packageHeight='3'; });
const desc=p._s.get('smtChoiceAddStore').$state.describeInfoData;
if(desc&&!desc.detail) desc.detail=${JSON.stringify((it.bullets || []).join('<br/>'))};
return JSON.stringify({subject:b.subject.slice(0,16), imgs:pr.mainImage.length, rows:(sd.skuDataList||[]).length});})()`);
log(` 填充: ${fill}`);
await wait(3500);
const attrs = await fillRequiredSelects(c);
if (attrs !== 'none') log(` 必填属性: ${String(attrs).slice(0, 150)}`);
await wait(1200);
log(` PC端描述: ${await fillDescription(c, (it.bullets || []).join('<br/>') || it.title)}`);
await wait(1500);
if (dry) { delete done[t.pid]; log(' ✓ dry 构建完成(未保存)'); await closePage(c); continue; }
// 保存 → 若报「请选择物流属性」就补上 → 再保存
await clickSave(c);
await wait(11000);
let mods = String(await c.evalJS(`JSON.stringify([...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,110)))`));
if (!/已保存|保存成功|待发布/.test(mods)) {
const lg = await fixLogistics(c);
log(` 物流属性: ${lg}`);
await clickSave(c);
await wait(12000);
mods = String(await c.evalJS(`JSON.stringify([...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,110)))`));
}
const cap = await readHook(c);
const ok = /已保存|保存成功|待发布/.test(mods);
done[t.pid] = ok ? { ok: true } : { ok: false, mods: mods.slice(0, 160), hook: String(cap).slice(-240) };
log(ok ? ' ✓ 已保存到待发布' : ` ✗ 失败 ${mods.slice(0, 110)} | ${String(cap).slice(-170)}`);
} catch (e) {
done[t.pid] = { ok: false, msg: String(e).slice(0, 240) };
log(` ✗ 异常: ${String(e).slice(0, 220)}`);
} finally {
if (!dry) writeFileSync(DONE_F, JSON.stringify(done, null, 1));
if (c) await closePage(c);
await wait(2500);
}
}
log(`aliChoice 批量结束:成功 ${Object.values(done).filter(x => x.ok).length}/${tasks.length}`);