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}`);