diff --git a/NEXT-ROUND-PROMPT.md b/NEXT-ROUND-PROMPT.md
index 8d24ee3..9e75327 100644
--- a/NEXT-ROUND-PROMPT.md
+++ b/NEXT-ROUND-PROMPT.md
@@ -75,7 +75,7 @@ function wrap(o,n,l){const f=o[n];o[n]=function(){window.__cap.push(l+': '+[...a
| 平台 | add 路由 | 店铺控件 | 类目控件 | store 家族 |
|---|---|---|---|---|
| Joom | `/web/joomProduct/add` | **复选框组** 85 个(全部+84 店) | 可搜索 `.ant-select` 平铺列表 | `smtChoice*` + `joom*`(已跑通) |
- | aliChoice | `/web/smtChoice/add` | 下拉(实测 2 家:XYZ Store / UniStyle Bag Store) | 弹窗 + 自动识别分类 | `smtChoice*`,**字段名与 Joom 不同**:标题在 `smtChoiceAddBasicStore.subject`(Joom 是 `.name`);描述在 `smtChoiceAddStore.describeInfoData.detail`。**已填到最后一步**,卡在「**请选择物流属性**」(无 label 的报错块 + `icon_edit2` 编辑入口,对应 SKU 行的 `logisticAttrListJson`)——下一轮从这里接着做,细节见 `alichoice建品/RECIPE.md` |
+ | aliChoice | `/web/smtChoice/add` | 下拉(实测 2 家) | 弹窗 | `smtChoice*`:标题 `.subject`、描述要写 **CKEditor setData**、保存要**两遍**(第二遍前补「物流属性=普货」)——**已跑通**,`alcbatch.mjs` 批量中 |
| Lazada | `/web/lazada/add` | 下拉 | 弹窗 | 选完店铺才注册 `lazada*` store(懒加载) |
| fruugo | `/web/fruugoProduct/add` | 下拉 | 弹窗 | `fruugoAdd*`(Common/Basic/Dxm/Product/Variation/Image/Describe/Form) |
diff --git a/alichoice建品/RECIPE.md b/alichoice建品/RECIPE.md
index feaaf4e..b7d97a6 100644
--- a/alichoice建品/RECIPE.md
+++ b/alichoice建品/RECIPE.md
@@ -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 @@
```
-- 它带一个 `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 上**一个必填属性都填不上**,三个原因,都已修:
diff --git a/alichoice建品/alc_probe4.mjs b/alichoice建品/alc_probe4.mjs
new file mode 100644
index 0000000..d309bdd
--- /dev/null
+++ b/alichoice建品/alc_probe4.mjs
@@ -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('
'))};
+ (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);
diff --git a/alichoice建品/alc_probe5.mjs b/alichoice建品/alc_probe5.mjs
new file mode 100644
index 0000000..5b9799f
--- /dev/null
+++ b/alichoice建品/alc_probe5.mjs
@@ -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('
'))};
+ (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);
diff --git a/alichoice建品/alcbatch.mjs b/alichoice建品/alcbatch.mjs
new file mode 100644
index 0000000..3cc423e
--- /dev/null
+++ b/alichoice建品/alcbatch.mjs
@@ -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('
'))};
+ 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('
') || 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}`);