Joom 47 条进待发布:jmbatch 跑通(店铺复选框组/可搜索类目/smtChoice* store),新增 3 条坑(无星号必填、虚拟滚动下拉、哨兵串定位 store)与 4 平台侦察表

This commit is contained in:
admin9webs committed 2026-09-20 21:00:59 +08:00
1 parent c69952bd59
commit c275571317
17 files changed
+1156 -4

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
# Joom 建品配方(已跑通,批量进行中)
路由:`https://www.dianxiaomi.com/web/joomProduct/add` 列表:`/web/joomProduct/{draft=采集箱, offline=待发布}`
脚本:`jmbatch.mjs`(`node jmbatch.mjs` 全量 · `node jmbatch.mjs <pid>` 单条 · `--dry` 只构建)
断点:`jm_done.json` 逐条日志:`jm_batch.log` 实际落到的类目:`joom_route.json`
## ⭐ 关键发现:Joom 的表单数据不在 `joom*` store 里
商品主体数据落在 **`smtChoiceAddBasicStore`**(速卖通全托管的 store),
Joom 与 **aliChoice 共用同一套 `smtChoice*` store**。定位方法:在页面上手输一个
哨兵串(`PROBE-TITLE-XYZ`),再遍历所有 pinia store 的 `$state` 找它落在哪。
| 字段 | 落点 |
|---|---|
| 店铺 | `smtChoiceAddBasicStore.formState.shopIds[]` / `shopId`(**店铺是复选框组,不是下拉**) |
| 类目 | `smtChoiceAddBasicStore.formState.categoryId`(复合串 `1473…-16-2-118-2600…`) |
| 标题 / ParentSKU / 描述 / 标签 | `smtChoiceAddBasicStore.formState.{name, parentSku, description, tags[]}` |
| 价格 / 库存 / 运费 / MSRP / 报关金额 | `joomAddPriceStore.formState.{price, inventory, shipping, msrp, declaredValue}` |
| 主图 / 详情图 | `joomAddImageStore.formState.imageList[]` / `detailImageList[0].images[]`(吃 `{url,imgUrl,name,width,height}` 数组) |
| SKU / 颜色 / 尺码 | `joomSkuDataStore.formState.skuDataList[]` / `joomSkuAttrStore.formState.{colorValues,sizeValues}` |
| 自定义属性 | `smtChoiceAddAttrStore.formState.customAttrs[]` |
| 类目属性(`attrsInfo.attrsList` 10 条) | `joomAddStore.attrsInfo`(`joomAddStore` **没有 formState**) |
## 单条顺序(不能换)
1. **勾店铺**:`店铺名称` 是 `.d-checkbox-group-wrapper`,共 **85 个复选框** = `全部` + 84 家店。
勾「第一个非全部」= `Dummer-Pets`。⚠️ **别碰 `全部`**(那是铺满 84 家店)。
2. **选类目**:`产品分类` 是可搜索 `.ant-select`(不是 SHEIN/Temu 的弹窗)。点开 →
`Input.insertText(cat_kw)` → 在 `.ant-select-item-option` 里选**去掉括注后完全相等**的,
没有就退到第一个(Joom 类目比 1688 粗,实测「发圈」只能落到「发夹(Hairpins)」)。
3. 填 store(标题/ParentSKU/描述/标签 + 价格/库存/运费/报关金额 + 主图/详情图)
4. `fillRequiredSelects()`(复用 SHEIN 的兜底)→ **敏感货类型 = 普通货**
5. **详情图语言** → 目标项**标签就叫「详情图」**(不含"语言"二字):
下拉是虚拟滚动的,前 10 项没有「英语」→ 先 `Input.insertText('英')` 过滤,
再点含「英」的项(否则会落到「阿拉伯语」)
6. 保存 → `您的产品已保存到「待发布」`
## 钩子新抓出的两条「没标必填但必须填」
| 被吞的报错 | 真相 |
|---|---|
| `MYJ.error: 运费不能为空` | 运费**没有** `*`,但必须填(给 `0` 即可) |
| `MYJ.error: 请选择详情图语言且至少添加一张详情图` | 语言必选;图片 store 已赋值也要选语言 |
## 与其它平台的差异
- 店铺:Joom 是**复选框组**(多选=铺多店),SHEIN/Temu/Lazada 是下拉
- 类目:Joom 是**可搜索下拉平铺列表**,SHEIN/Temu 是「选择类目」弹窗(搜索→点结果→关弹窗)
- 数据落点:Joom 用 `smtChoice*`(与 aliChoice 同构),SHEIN/Temu 各有自己的 `shein*` / `temu*`
- **不消耗 EAN**(GTIN 非必填、`showGtin=false`)
+145
View File
@@ -0,0 +1,145 @@
{
"548459586741": {
"ok": true
},
"661190598139": {
"ok": true
},
"764423265986": {
"ok": true
},
"685456757807": {
"ok": true
},
"735147938123": {
"ok": true
},
"627879474686": {
"ok": true
},
"610164857063": {
"ok": true
},
"708420302844": {
"ok": true
},
"866537106657": {
"ok": true
},
"547183997437": {
"ok": true
},
"657617635303": {
"ok": true
},
"641673262387": {
"ok": true
},
"953869908708": {
"ok": true
},
"703822717997": {
"ok": true
},
"863524134410": {
"ok": true
},
"694637537727": {
"ok": true
},
"623011262210": {
"ok": true
},
"836512561982": {
"ok": true
},
"714492799386": {
"ok": true
},
"591994902720": {
"ok": true
},
"853414558425": {
"ok": true
},
"997450831819": {
"ok": true
},
"639409307567": {
"ok": true
},
"902440558099": {
"ok": true
},
"823838532886": {
"ok": true
},
"698782391248": {
"ok": true
},
"707826905566": {
"ok": true
},
"1027332215847": {
"ok": true
},
"682195499823": {
"ok": true
},
"823560564174": {
"ok": true
},
"739147684759": {
"ok": true
},
"621300323670": {
"ok": true
},
"601964534137": {
"ok": false,
"skipped": true,
"msg": "全平台统一跳过:18 张图全带水印、无干净图(imgs=0)"
},
"896469995553": {
"ok": true
},
"944470567338": {
"ok": true
},
"787261097583": {
"ok": true
},
"833753630925": {
"ok": true
},
"719391062058": {
"ok": true
},
"857781294719": {
"ok": true
},
"863384503032": {
"ok": true
},
"774494945453": {
"ok": true
},
"750271307455": {
"ok": true
},
"726442972676": {
"ok": true
},
"775057912118": {
"ok": true
},
"893592264148": {
"ok": true
},
"607330049419": {
"ok": true
},
"854003999648": {
"ok": true
}
}
+31
View File
@@ -0,0 +1,31 @@
import { newPage, closePage, wait } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { SAN as S } from '/tmp/cdp3.mjs';
const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`;
const c = await newPage('https://www.dianxiaomi.com/web/joomProduct/add');
await wait(12000);
// 1) 店铺选择控件长什么样
const shop = await c.evalJS(`(()=>{
const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
if(!it) return 'no-item';
const ctrls=[...it.querySelectorAll('.ant-select,.ant-checkbox-group,.ant-radio-group')].map(x=>x.tagName+'.'+String(x.className).slice(0,40));
const txt=(it.innerText||'').replace(/\\s+/g,' ').trim().slice(0,120);
return JSON.stringify({ctrls, txt, html:it.innerHTML.replace(/\\s+/g,' ').slice(0,500)});})()`);
console.log('店铺:', S(String(shop)).slice(0,900));
// 2) 点开它
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)]);})()`)));
console.log('坐标:', JSON.stringify(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(2500);
}
const opts = await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
if(!dd) return 'no-dd';
const items=[...dd.querySelectorAll('.ant-select-item-option')].map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim());
const multi=!!dd.querySelector('.ant-select-item-option-selected, .ant-checkbox');
return JSON.stringify({n:items.length, items:items.slice(0,12), multi});})()`);
console.log('店铺选项:', S(String(opts)).slice(0,800));
try{ await c.shot('/tmp/joom_shop.jpg'); console.log('shot ok'); }catch(e){}
await closePage(c); process.exit(0);
+96
View File
@@ -0,0 +1,96 @@
import { newPage, closePage, wait, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { 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/joomProduct/add');
await wait(12000);
await installHook(c);
// 店
console.log('店:', String(await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const w=[...it.querySelectorAll('.ant-checkbox-wrapper')].find(x=>{const t=(x.innerText||'').trim(); return t&&!/^全部/.test(t);});
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(); return (w.innerText||'').trim();})()`)));
await wait(6000);
// 类目
const pos = 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:pos[0],y:pos[1],buttons:0}); await wait(250);
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(1500);
await c.send('Input.insertText',{text: it.cat_kw || '发夹'}).catch(()=>{});
await wait(2500);
console.log('类目:', String(await 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')][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 (op.innerText||'').trim().slice(0,30);})()`)));
await wait(7000);
// 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.name=${JSON.stringify(it.title)}; b.parentSku='JM-'+${JSON.stringify(pid)}; b.description=(${JSON.stringify((it.bullets||[]).join('. '))}||b.name); b.tags=(${JSON.stringify(it.keywords||'')}.split(', ').slice(0,10));
const pr=p._s.get('joomAddPriceStore').$state.formState;
pr.price=String(${JSON.stringify(String(it.price_cny||'19.90'))}); pr.inventory='999'; pr.msrp=''; pr.shipping='0'; pr.declaredValue=String(${JSON.stringify(String(it.cost||'10'))});
const im=p._s.get('joomAddImageStore').$state.formState;
im.imageList=imgs.map(mk);
if(Array.isArray(im.detailImageList)&&im.detailImageList[0]) im.detailImageList[0].images=imgs.map(mk);
return JSON.stringify({name:b.name.slice(0,20), price:pr.price, imgs:im.imageList.length, cat:b.categoryId});})()`);
console.log('填充:', S(String(fill)).slice(0,400));
await wait(4000);
// 列出所有带 select 的表单项,找「详情图语言」
const pairs = await c.evalJS(`(()=>{ try {
const out = [];
for (const e of document.querySelectorAll('.ant-form-item')) {
if (e.offsetParent === null) continue;
const sel = e.querySelector('.ant-select');
if (!sel) continue;
const l = e.querySelector('label');
out.push(((l ? l.innerText : '') || '?').replace(/\\s+/g, ' ').trim().slice(0, 26) + ' => ' + (sel.innerText || '').replace(/\\s+/g, ' ').trim().slice(0, 24));
}
return JSON.stringify(out.slice(0, 20));
} catch (e) { return 'ERR ' + e.message; } })()`);
console.log('带下拉的表项:', S(String(pairs)).slice(0, 1200));
// 点「详情图语言」那个 select,选第一项
const lang = JSON.parse(String(await c.evalJS(`(()=>{ try {
let target = null;
for (const e of document.querySelectorAll('.ant-form-item')) {
if (e.offsetParent === null) continue;
const l = e.querySelector('label'); const t = l ? (l.innerText || '') : '';
if (t.indexOf('详情图') >= 0 && e.querySelector('.ant-select')) { target = e; break; }
}
if (!target) return '[]';
const s = target.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)]);
} catch (e) { return '[]'; } })()`)));
console.log('语言控件坐标:', JSON.stringify(lang));
if (lang.length) {
await c.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: lang[0], y: lang[1], buttons: 0 }); await wait(300);
for (const t of ['mousePressed', 'mouseReleased']) await c.send('Input.dispatchMouseEvent', { type: t, x: lang[0], y: lang[1], button: 'left', clickCount: 1, buttons: 1 });
await wait(2000);
console.log('选了:', String(await 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')][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 (op.innerText||'').trim().slice(0,20);})()`)));
await wait(3000);
}
// 保存
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(13000);
console.log('hook:', String(await readHook(c)).slice(-260));
const res = await c.evalJS(`JSON.stringify({mods:[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,120))})`);
console.log('结果:', S(String(res)).slice(0,400));
try { await c.shot('/tmp/joom_lang.jpg'); console.log('shot ok'); } catch (e) {}
await closePage(c); process.exit(0);
+54
View File
@@ -0,0 +1,54 @@
import { newPage, closePage, wait } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { SAN as S } from '/tmp/cdp3.mjs';
const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`;
const c = await newPage('https://www.dianxiaomi.com/web/joomProduct/add');
await wait(12000);
// 全部店铺标签
const labels = await c.evalJS(`(()=>{
const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const ls=[...it.querySelectorAll('.ant-checkbox-wrapper')].map(w=>(w.innerText||'').replace(/\\s+/g,' ').trim());
return JSON.stringify({n:ls.length, first:ls.slice(0,10), last:ls.slice(-3)});})()`);
console.log('店铺:', S(String(labels)).slice(0,600));
// 勾第一个真实店铺(跳过「全部」)
const tick = await c.evalJS(`(()=>{
const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const ws=[...it.querySelectorAll('.ant-checkbox-wrapper')];
const target=ws.find(w=>{const t=(w.innerText||'').trim(); return t && !/^全部/.test(t);});
if(!target) return 'nf';
for(const ev of ['mousedown','mouseup','click']) target.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
const inp=target.querySelector('input[type=checkbox]'); if(inp&&!inp.checked) inp.click();
return 'ticked:'+(target.innerText||'').trim();})()`);
console.log('勾选:', String(tick));
await wait(6000);
// 选类目
const cat = await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/产品分类|分类/.test((e.querySelector('label')||{}).innerText||''));
if(!it) return 'no-item';
const b=[...it.querySelectorAll('*')].find(e=>(e.innerText||'').trim()==='选择分类'&&e.children.length===0);
if(!b) return 'no-btn';
for(const ev of ['mousedown','mouseup','click']) b.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 'opened';})()`);
console.log('类目按钮:', String(cat));
await wait(3500);
const modal = await c.evalJS(`(()=>{
const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
if(!m) return 'no-modal';
const inp=[...m.querySelectorAll('input')].map(e=>e.placeholder||'').slice(0,4);
const txt=(m.innerText||'').replace(/\\s+/g,' ').trim().slice(0,220);
return JSON.stringify({inp, txt});})()`);
console.log('类目弹窗:', S(String(modal)).slice(0,700));
// 搜一个关键词
await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
const inp=m.querySelector('input[placeholder="搜索分类名称"]')||m.querySelector('input'); if(!inp) return 'no-input';
const setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;
setter.call(inp,'发圈'); inp.dispatchEvent(new Event('input',{bubbles:true})); return 1;})()`);
await wait(600);
await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).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(4000);
const res = await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
if(!m) return 'closed';
const items=[...m.querySelectorAll('.search-result-item')].map(e=>(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,70));
return JSON.stringify({n:items.length, items:items.slice(0,5)});})()`);
console.log('类目结果:', S(String(res)).slice(0,600));
try{ await c.shot('/tmp/joom_cat.jpg'); console.log('shot ok'); }catch(e){}
await closePage(c); process.exit(0);
+25
View File
@@ -0,0 +1,25 @@
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/joomProduct/add');
await wait(12000);
await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const w=[...it.querySelectorAll('.ant-checkbox-wrapper')].find(x=>{const t=(x.innerText||'').trim(); return t&&!/^全部/.test(t);});
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(); return 1;})()`);
await wait(6000);
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)]);})()`)));
console.log('分类select坐标:', JSON.stringify(pos));
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(3000);
const d = await c.evalJS(`(()=>{
const dd=[...document.querySelectorAll('.ant-select-dropdown,.ant-cascader-menus,.ant-cascader-dropdown')].filter(e=>e.offsetParent!==null);
const info=dd.map(e=>({cls:String(e.className).slice(0,60), txt:(e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,300)}));
const menus=[...document.querySelectorAll('.ant-cascader-menu')].length;
const inputs=[...document.querySelectorAll('.ant-select-dropdown input,.ant-cascader-menus input')].filter(e=>e.offsetParent!==null).map(e=>e.placeholder||'');
return JSON.stringify({n:info.length, info, menus, inputs});})()`);
console.log('下拉:', S(String(d)).slice(0,1200));
try{ await c.shot('/tmp/joom_cat3.jpg'); console.log('shot ok'); }catch(e){}
await closePage(c); process.exit(0);
+48
View File
@@ -0,0 +1,48 @@
import { newPage, closePage, wait } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { SAN as S } from '/tmp/cdp3.mjs';
const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`;
const c = await newPage('https://www.dianxiaomi.com/web/joomProduct/add');
await wait(12000);
// 勾店
await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const w=[...it.querySelectorAll('.ant-checkbox-wrapper')].find(x=>{const t=(x.innerText||'').trim(); return t&&!/^全部/.test(t);});
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(); return 1;})()`);
await wait(6000);
// 点分类 select 并输入关键词
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)]);})()`)));
await c.send('Input.dispatchMouseEvent',{type:'mouseMoved',x:pos[0],y:pos[1],buttons:0}); await wait(250);
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(1500);
await c.send('Input.insertText',{text:'发圈'}).catch(()=>{});
await wait(2500);
const pick = await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
if(!dd) return 'no-dd';
const ops=[...dd.querySelectorAll('.ant-select-item-option')].filter(e=>e.offsetParent!==null);
if(!ops.length) return 'no-op';
const t=ops[0]; const r=t.getBoundingClientRect();
for(const ev of ['mousedown','mouseup','click']) t.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2}));
return 'picked:'+(t.innerText||'').trim().slice(0,30)+' (共'+ops.length+'项)';})()`);
console.log('类目:', String(pick));
await wait(6000);
// dump store 状态
const st = await c.evalJS(`(()=>{const p=${P};
const ids=(p._s instanceof Map?[...p._s.keys()]:Object.keys(p._s)).filter(x=>/joom/i.test(x));
const out={};
for(const id of ids){ try{ const s=(p._s.get?p._s.get(id):p._s[id]).$state; const f=s.formState||{};
const keep={}; for(const k of Object.keys(f)){ const v=f[k]; if(v===null||v===undefined||v===''||(Array.isArray(v)&&!v.length)) continue;
keep[k]=Array.isArray(v)?('['+v.length+'] '+(v[0]?JSON.stringify(v[0]).slice(0,80):'')):(typeof v==='object'?JSON.stringify(v).slice(0,80):String(v).slice(0,50));
if(Object.keys(keep).length>12) break; }
const extra=Object.keys(s).filter(k=>k!=='formState').slice(0,4);
out[id]={...keep, _keys:extra}; }catch(e){ out[id]='ERR'; } }
return JSON.stringify(out);})()`);
console.log('stores:', S(String(st)).slice(0,1800));
// 可见表单项(带必填标记)
const items = await c.evalJS(`JSON.stringify([...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).slice(0,22).map(e=>{
const l=e.querySelector('label'); const req=l?/required/.test(String(l.className)):'';
return (req?'*':'')+((e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,46));}))`);
console.log('表单项:', S(String(items)).slice(0,1400));
try{ await c.shot('/tmp/joom_form.jpg'); console.log('shot ok'); }catch(e){}
await closePage(c); process.exit(0);
+43
View File
@@ -0,0 +1,43 @@
import { newPage, closePage, wait } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { SAN as S } from '/tmp/cdp3.mjs';
const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`;
const c = await newPage('https://www.dianxiaomi.com/web/joomProduct/add');
await wait(12000);
await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const w=[...it.querySelectorAll('.ant-checkbox-wrapper')].find(x=>{const t=(x.innerText||'').trim(); return t&&!/^全部/.test(t);});
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(); return 1;})()`);
await wait(6000);
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.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:pos[0],y:pos[1],buttons:0}); await wait(250);
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(1500);
await c.send('Input.insertText',{text:'发夹'}).catch(()=>{});
await wait(2500);
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(7000);
// 1) joomAddStore 全量结构
const st = await c.evalJS(`(()=>{const p=${P};
const s=p._s.get('joomAddStore').$state;
const keys=Object.keys(s);
const pi=s.productInfo||{};
const pick=(o,d=0)=>{ if(d>2||!o||typeof o!=='object') return o; const out={}; let n=0;
for(const k of Object.keys(o)){ const v=o[k]; out[k]=(v&&typeof v==='object')?(Array.isArray(v)?'['+v.length+']':pick(v,d+1)):String(v).slice(0,40); if(++n>14) break; } return out; };
return JSON.stringify({keys, productInfo:pick(pi), attrsInfo:pick(s.attrsInfo), sharedData:pick(s.sharedData)});})()`);
console.log('joomAddStore:', S(String(st)).slice(0,1500));
// 2) 价格/库存/SKU/图片 控件
const ctrls = await c.evalJS(`(()=>{const out={};
const bag=(re)=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>{const l=e.querySelector('label'); return l&&new RegExp(re).test(l.innerText||'');});
if(!it) return 'no-item';
return {txt:(it.innerText||'').replace(/\\s+/g,' ').trim().slice(0,60),
inputs:[...it.querySelectorAll('input')].filter(i=>i.offsetParent!==null).map(i=>({ph:i.placeholder||'', name:i.name||'', cls:String(i.className).slice(0,34)})).slice(0,5),
selects:[...it.querySelectorAll('.ant-select')].length,};};
out['价格']=bag('^\\*?价格'); out['库存']=bag('库存'); out['ParentSKU']=bag('ParentSKU'); out['产品图']=bag('产品图'); out['敏感货类型']=bag('敏感货类型');
const sku=p=>0;
return JSON.stringify(out);})()`);
console.log('控件:', S(String(ctrls)).slice(0,1600));
await closePage(c); process.exit(0);
+45
View File
@@ -0,0 +1,45 @@
import { newPage, closePage, wait } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { SAN as S } from '/tmp/cdp3.mjs';
const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`;
const c = await newPage('https://www.dianxiaomi.com/web/joomProduct/add');
await wait(12000);
await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const w=[...it.querySelectorAll('.ant-checkbox-wrapper')].find(x=>{const t=(x.innerText||'').trim(); return t&&!/^全部/.test(t);});
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(); return 1;})()`);
await wait(6000);
// 类目
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.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:pos[0],y:pos[1],buttons:0}); await wait(250);
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(1500);
await c.send('Input.insertText',{text:'发夹'}).catch(()=>{});
await wait(2500);
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 tpos = JSON.parse(String(await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/^\\s*\\*?产品标题/.test((e.innerText||'').trim()));
const inp=it?it.querySelector('input,textarea'):null; if(!inp) return '[]'; inp.scrollIntoView({block:'center'});
inp.setAttribute('data-probe','1'); const r=inp.getBoundingClientRect();
return JSON.stringify([Math.round(r.x+r.width/2),Math.round(r.y+r.height/2)]);})()`)));
console.log('标题框坐标:', JSON.stringify(tpos));
for(const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent',{type:t,x:tpos[0],y:tpos[1],button:'left',clickCount:1,buttons:1});
await wait(400);
await c.send('Input.insertText',{text:'PROBE-TITLE-XYZ'}).catch(()=>{});
await wait(2500);
// 全 store 搜索这个字符串
const found = await c.evalJS(`(()=>{const p=${P}; const needle='PROBE-TITLE-XYZ'; const hits=[];
const ids=(p._s instanceof Map?[...p._s.keys()]:Object.keys(p._s));
for(const id of ids){ try{ const s=(p._s.get?p._s.get(id):p._s[id]).$state;
const js=JSON.stringify(s); if(js&&js.includes(needle)) hits.push(id+':'+js.slice(Math.max(0,js.indexOf(needle)-70), js.indexOf(needle)+40).replace(/\\\\/g,'')); }catch(e){} }
const domVal=(document.querySelector('input[data-probe="1"]')||{}).value||'';
// joomAddStore 的非 $state 属性
let extra=[];
try{ const st=p._s.get('joomAddStore'); extra=Object.keys(st).filter(k=>k!=='$state'&&k!=='_s'&&!/^\\$/.test(k)).slice(0,18); }catch(e){}
return JSON.stringify({hits, domVal, joomAddStoreOwnKeys:extra});})()`);
console.log('命中:', S(String(found)).slice(0,1600));
await closePage(c); process.exit(0);
+32
View File
@@ -0,0 +1,32 @@
import { newPage, closePage, wait } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { SAN as S } from '/tmp/cdp3.mjs';
const P = `document.querySelector('#app').__vue_app__.config.globalProperties.$pinia`;
const c = await newPage('https://www.dianxiaomi.com/web/joomProduct/add');
await wait(12000);
await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const w=[...it.querySelectorAll('.ant-checkbox-wrapper')].find(x=>{const t=(x.innerText||'').trim(); return t&&!/^全部/.test(t);});
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(); return 1;})()`);
await wait(6000);
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.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:pos[0],y:pos[1],buttons:0}); await wait(250);
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(1500);
await c.send('Input.insertText',{text:'发夹'}).catch(()=>{});
await wait(2500);
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(7000);
const d = await c.evalJS(`(()=>{const p=${P};
const ids=(p._s instanceof Map?[...p._s.keys()]:Object.keys(p._s)).filter(x=>/smtChoice|joom|image|Image/i.test(x));
const out={};
const fmt=(v,d=0)=>{ if(v===null||v===undefined) return String(v); if(Array.isArray(v)) return '['+v.length+'] '+(v[0]?fmt(v[0],d+1):'');
if(typeof v==='object'){ if(d>1) return '{...}'; const o={}; let n=0; for(const k of Object.keys(v)){ o[k]=fmt(v[k],d+1); if(++n>16){o['…']='';break;} } return o; } return String(v).slice(0,46); };
for(const id of ids){ try{ const s=(p._s.get?p._s.get(id):p._s[id]).$state; const f=s.formState;
out[id]= f ? fmt(f) : '(no formState) keys='+Object.keys(s).slice(0,10).join(','); }catch(e){ out[id]='ERR'; } }
return JSON.stringify(out);})()`);
console.log('stores:', JSON.stringify(JSON.parse(String(d)), null, 1).slice(0,3000));
await closePage(c); process.exit(0);
+69
View File
@@ -0,0 +1,69 @@
import { newPage, closePage, wait, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { 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/joomProduct/add');
await wait(12000);
await installHook(c);
// 店
console.log('店:', String(await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const w=[...it.querySelectorAll('.ant-checkbox-wrapper')].find(x=>{const t=(x.innerText||'').trim(); return t&&!/^全部/.test(t);});
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(); return (w.innerText||'').trim();})()`)));
await wait(6000);
// 类目
const pos = 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:pos[0],y:pos[1],buttons:0}); await wait(250);
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(1500);
await c.send('Input.insertText',{text: it.cat_kw || '发夹'}).catch(()=>{});
await wait(2500);
console.log('类目:', String(await 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')][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 (op.innerText||'').trim().slice(0,30);})()`)));
await wait(7000);
// 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.name=${JSON.stringify(it.title)}; b.parentSku='JM-'+${JSON.stringify(pid)}; b.description=(${JSON.stringify((it.bullets||[]).join('. '))}||b.name); b.tags=(${JSON.stringify(it.keywords||'')}.split(', ').slice(0,10));
const pr=p._s.get('joomAddPriceStore').$state.formState;
pr.price=String(${JSON.stringify(String(it.price_cny||'19.90'))}); pr.inventory='999'; pr.msrp=''; pr.shipping='0'; pr.declaredValue=String(${JSON.stringify(String(it.cost||'10'))});
const im=p._s.get('joomAddImageStore').$state.formState;
im.imageList=imgs.map(mk);
if(Array.isArray(im.detailImageList)&&im.detailImageList[0]) im.detailImageList[0].images=imgs.map(mk);
return JSON.stringify({name:b.name.slice(0,20), price:pr.price, imgs:im.imageList.length, cat:b.categoryId});})()`);
console.log('填充:', S(String(fill)).slice(0,400));
await wait(4000);
const attrs = await fillRequiredSelects(c);
console.log('必填属性:', S(String(attrs)).slice(0,300));
await wait(2000);
// dump(简化版,避免页面里复杂表达式报错)
const d = await c.evalJS(`(()=>{ try {
const p = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia;
const im = p._s.get('joomAddImageStore').$state;
const labels = [];
const all = document.querySelectorAll('.ant-form-item');
for (const e of all) {
if (e.offsetParent === null) continue;
const l = e.querySelector('label');
const t = l ? (l.innerText || '') : '';
if (t.indexOf('图') >= 0 || t.indexOf('语言') >= 0) {
labels.push(t.replace(/\\s+/g, ' ').trim().slice(0, 30) + ' | selects=' + e.querySelectorAll('.ant-select').length + ' imgs=' + e.querySelectorAll('img').length);
}
}
return JSON.stringify({ keys: Object.keys(im), imageList: JSON.stringify(im.imageList).slice(0, 160),
detail: JSON.stringify(im.detailImageList).slice(0, 260), labels: labels.slice(0, 12) });
} catch (err) { return 'INNER-ERR ' + err.message; } })()`);
console.log('图片区:', S(String(d)).slice(0, 1400));
try { await c.shot('/tmp/joom_img.jpg'); console.log('shot ok'); } catch (e) {}
await closePage(c); process.exit(0);
+64
View File
@@ -0,0 +1,64 @@
import { newPage, closePage, wait, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { 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/joomProduct/add');
await wait(12000);
await installHook(c);
// 店
console.log('店:', String(await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
const w=[...it.querySelectorAll('.ant-checkbox-wrapper')].find(x=>{const t=(x.innerText||'').trim(); return t&&!/^全部/.test(t);});
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(); return (w.innerText||'').trim();})()`)));
await wait(6000);
// 类目
const pos = 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:pos[0],y:pos[1],buttons:0}); await wait(250);
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(1500);
await c.send('Input.insertText',{text: it.cat_kw || '发夹'}).catch(()=>{});
await wait(2500);
console.log('类目:', String(await 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')][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 (op.innerText||'').trim().slice(0,30);})()`)));
await wait(7000);
// 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.name=${JSON.stringify(it.title)}; b.parentSku='JM-'+${JSON.stringify(pid)}; b.description=(${JSON.stringify((it.bullets||[]).join('. '))}||b.name); b.tags=(${JSON.stringify(it.keywords||'')}.split(', ').slice(0,10));
const pr=p._s.get('joomAddPriceStore').$state.formState;
pr.price=String(${JSON.stringify(String(it.price_cny||'19.90'))}); pr.inventory='999'; pr.msrp=''; pr.shipping='0'; pr.declaredValue=String(${JSON.stringify(String(it.cost||'10'))});
const im=p._s.get('joomAddImageStore').$state.formState;
im.imageList=imgs.map(mk);
if(Array.isArray(im.detailImageList)&&im.detailImageList[0]) im.detailImageList[0].images=imgs.map(mk);
return JSON.stringify({name:b.name.slice(0,20), price:pr.price, imgs:im.imageList.length, cat:b.categoryId});})()`);
console.log('填充:', S(String(fill)).slice(0,400));
await wait(4000);
const dumpImgs = async (tag) => {
const r = await c.evalJS(`(()=>{ try {
const p = document.querySelector('#app').__vue_app__.config.globalProperties.$pinia;
const st = p._s.get('joomAddImageStore').$state;
const f = st.formState || {};
return JSON.stringify({ keys: Object.keys(f), imageList: Array.isArray(f.imageList) ? f.imageList.length : 'undef',
detail: Array.isArray(f.detailImageList) && f.detailImageList[0] && Array.isArray(f.detailImageList[0].images) ? f.detailImageList[0].images.length : 'undef',
domImgs: document.querySelectorAll('img').length });
} catch (e) { return 'ERR ' + e.message; } })()`);
console.log(tag, S(String(r)).slice(0, 300));
};
await wait(800); await dumpImgs('填完立刻:');
await wait(6000); await dumpImgs('6 秒后:');
const attrs = await fillRequiredSelects(c);
console.log('必填属性:', S(String(attrs)).slice(0, 200));
await wait(2500); await dumpImgs('填必填属性后:');
try { await c.shot('/tmp/joom_img2.jpg'); console.log('shot ok'); } catch (e) {}
await closePage(c); process.exit(0);
+13
View File
@@ -0,0 +1,13 @@
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/joomProduct/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,3).map(r=>(r.innerText||'').replace(/\s+/g,' ').trim().slice(0,90));
const n=document.querySelectorAll('.vxe-body--row').length;
return JSON.stringify({total:m?m[0]:'', n, rows});})()`);
console.log(S(String(o)).slice(0,700));
try{ await c.shot('/tmp/joom_offline.jpg'); console.log('shot ok'); }catch(e){}
await closePage(c); process.exit(0);
+179
View File
@@ -0,0 +1,179 @@
/**
* Joom 批量建品(84 家店取第一家;与 aliChoice 共用 smtChoice* store)
*
* 用法: node jmbatch.mjs [--dry] [pid...]
*
* 单条顺序:勾店铺(复选框) → 选类目(可搜索 ant-select) → 标题/ParentSKU/描述/标签
* → 价格/库存/运费/报关金额 → 图片(主图+详情图) → 敏感货类型 → 详情图语言 → 保存
*
* 被钩子抓出来的两条「没标必填但必须填」:运费 shipping、详情图语言(标签就叫「详情图」)
*/
import { readFileSync, writeFileSync, appendFileSync } from 'fs';
import { newPage, closePage, wait, installHook, readHook } from '/home/admin9webs/shein建品/sh_lib.mjs';
import { fillRequiredSelects } from '/home/admin9webs/shein建品/sh_build.mjs';
const DIR = '/home/admin9webs/joom建品';
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}/jm_done.json`;
const LOG_F = `${DIR}/jm_batch.log`;
const SKIP = new Set([
'676236596821', // 首条手工测试已进待发布(详情图语言选了阿拉伯语),不再重复建
'601964534137', // 遥控器保护套:18 张图全带水印、无干净图 → 所有平台统一跳过
]);
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 {}
let ROUTE = {}; try { ROUTE = JSON.parse(readFileSync(`${DIR}/joom_route.json`, 'utf8')); } catch {}
const log = m => { const l = `[${new Date().toISOString().slice(11, 19)}] ${m}`; console.log(l); appendFileSync(LOG_F, l + '\n'); };
/** 勾第一家真实店铺(Joom 店名没有圈号,取 DOM 第一个非「全部」,即 Dummer-Pets) */
async function tickShop(c, want = null) {
return c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null)
.find(e=>/店铺名称/.test((e.querySelector('label')||{}).innerText||''));
if(!it) return 'no-item';
const ws=[...it.querySelectorAll('.ant-checkbox-wrapper')];
const want=${JSON.stringify(want)};
const w=want ? ws.find(x=>(x.innerText||'').trim()===want) : ws.find(x=>{const t=(x.innerText||'').trim(); return t&&!/^全部/.test(t);});
if(!w) return 'no-shop';
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();
return 'ticked:'+(w.innerText||'').trim();})()`);
}
/** 类目:点开可搜索下拉 → 输关键词 → 选名字匹配的项(优先去掉括注后完全相等) */
async function pickCategory(c, kw) {
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(250);
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(1500);
await c.send('Input.insertText', { text: kw }).catch(() => {});
await wait(2600);
return c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
if(!dd) return 'no-dd';
const ops=[...dd.querySelectorAll('.ant-select-item-option')].filter(e=>e.offsetParent!==null);
if(!ops.length) return 'no-option';
const clean=s=>(s||'').replace(/\\s+/g,'').split('(')[0];
const op=ops.find(e=>clean(e.innerText)===clean(${JSON.stringify(kw)}))||ops[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 'picked:'+(op.innerText||'').trim().slice(0,26)+'/'+ops.length;})()`);
}
/** 详情图语言:标签就是「详情图」,优先英语 */
async function pickDetailLang(c) {
const pos = JSON.parse(String(await c.evalJS(`(()=>{
for(const e of document.querySelectorAll('.ant-form-item')){
if(e.offsetParent===null) continue;
const l=e.querySelector('label'); const t=l?(l.innerText||''):'';
if(t.indexOf('详情图')>=0&&e.querySelector('.ant-select')){
const s=e.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)]); } }
return '[]';})()`)));
if (!pos.length) return 'no-lang';
await c.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x: pos[0], y: pos[1], buttons: 0 }); await wait(250);
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(2000);
// 列表是虚拟滚动的,前 10 项里没有「英语」→ 先试着输「英」过滤,不行就滚到底
await c.send('Input.insertText', { text: '英' }).catch(() => {});
await wait(1500);
let hit = await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
if(!dd) return 'no-dd';
const ops=[...dd.querySelectorAll('.ant-select-item-option')].filter(e=>e.offsetParent!==null);
const names=ops.map(e=>(e.innerText||'').trim()).filter(Boolean);
const op=ops.find(e=>/英|English/i.test(e.innerText||''));
if(!op) return 'none|'+names.slice(0,6).join('/');
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();})()`);
if (String(hit).startsWith('none')) {
hit = await c.evalJS(`(()=>{const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
if(!dd) return 'no-dd';
const sc=dd.querySelector('.rc-virtual-list-holder')||dd; sc.scrollTop=sc.scrollHeight;
const ops=[...dd.querySelectorAll('.ant-select-item-option')].filter(e=>e.offsetParent!==null);
const op=ops.find(e=>/英|English/i.test(e.innerText||''));
if(!op) return 'still-none|'+ops.map(e=>(e.innerText||'').trim()).slice(-6).join('/');
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();})()`);
}
return hit;
}
log(`Joom 批量开始:任务 ${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/joomProduct/add');
await wait(12000);
await installHook(c);
const shop = await tickShop(c, process.env.JOOM_SHOP || null);
if (!String(shop).startsWith('ticked:')) throw new Error('选店铺失败:' + shop);
await wait(6000);
const cat = await pickCategory(c, it.cat_kw || '发夹');
log(` 店=${shop.slice(7)} 类目=${cat}`);
if (!String(cat).startsWith('picked:')) throw new Error('选类目失败:' + cat);
// 记录实际落到的类目名(Joom 类目比 1688 粗,kw 常常只能近似匹配,事后要人工抽查)
const catName = 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'):null; return s?(s.innerText||'').replace(/\\s+/g,' ').trim().slice(0,40):'';})()`));
ROUTE[t.pid] = { shop: shop.slice(7), kw: it.cat_kw || '', cat: catName };
writeFileSync(`${DIR}/joom_route.json`, JSON.stringify(ROUTE, null, 1));
await wait(7000);
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.name=${JSON.stringify(it.title)}; b.parentSku='JM-'+${JSON.stringify(t.pid)};
b.description=(${JSON.stringify((it.bullets || []).join('. '))}||b.name);
b.tags=String(${JSON.stringify(it.keywords || '')}).split(', ').slice(0,10);
const pr=p._s.get('joomAddPriceStore').$state.formState;
pr.price=String(${JSON.stringify(String(it.price_cny || '19.90'))}); pr.inventory='999';
pr.shipping='0'; pr.msrp=''; pr.declaredValue=String(${JSON.stringify(String(it.cost || '10'))});
const im=p._s.get('joomAddImageStore').$state.formState;
im.imageList=imgs.map(mk);
if(Array.isArray(im.detailImageList)&&im.detailImageList[0]) im.detailImageList[0].images=imgs.map(mk);
return JSON.stringify({name:b.name.slice(0,18), price:pr.price, imgs:im.imageList.length, cat:b.categoryId});})()`);
log(` 填充: ${fill}`);
await wait(3500);
const attrs = await fillRequiredSelects(c);
if (attrs !== 'none') log(` 必填属性: ${attrs}`);
await wait(1500);
log(` 详情图语言: ${await pickDetailLang(c)}`);
await wait(2500);
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 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(13000);
const 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(-220) };
log(ok ? ' ✓ 已保存到待发布' : ` ✗ 失败 ${String(cap).slice(-180)}`);
} 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(`Joom 批量结束:成功 ${Object.values(done).filter(x => x.ok).length}/${tasks.length}`);
+242
View File
@@ -0,0 +1,242 @@
{
"676236596821": {
"shop": "Dummer-Pets",
"kw": "发夹",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"548459586741": {
"shop": "Dummer-Pets",
"kw": "口哨",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"661190598139": {
"shop": "Dummer-Pets",
"kw": "发圈",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"764423265986": {
"shop": "Dummer-Pets",
"kw": "温度计",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"685456757807": {
"shop": "Dummer-Pets",
"kw": "毛线",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"735147938123": {
"shop": "Dummer-Pets",
"kw": "发箍",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"627879474686": {
"shop": "Dummer-Pets",
"kw": "袜子",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"610164857063": {
"shop": "Dummer-Pets",
"kw": "发带",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"708420302844": {
"shop": "Dummer-Pets",
"kw": "钥匙扣",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"866537106657": {
"shop": "Dummer-Pets",
"kw": "纽扣",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"547183997437": {
"shop": "Dummer-Pets",
"kw": "项链",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"657617635303": {
"shop": "Dummer-Pets",
"kw": "收纳盒",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"641673262387": {
"shop": "Dummer-Pets",
"kw": "首饰袋",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"953869908708": {
"shop": "Dummer-Pets",
"kw": "窗贴",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"703822717997": {
"shop": "Dummer-Pets",
"kw": "笔夹",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"863524134410": {
"shop": "Dummer-Pets",
"kw": "贴纸",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"694637537727": {
"shop": "Dummer-Pets",
"kw": "保温袋",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"623011262210": {
"shop": "Dummer-Pets",
"kw": "窗贴",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"836512561982": {
"shop": "Dummer-Pets",
"kw": "化妆包",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"714492799386": {
"shop": "Dummer-Pets",
"kw": "化妆包",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"591994902720": {
"shop": "Dummer-Pets",
"kw": "笔袋",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"853414558425": {
"shop": "Dummer-Pets",
"kw": "手机挂绳",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"997450831819": {
"shop": "Dummer-Pets",
"kw": "贴纸",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"639409307567": {
"shop": "Dummer-Pets",
"kw": "墙贴",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"902440558099": {
"shop": "Dummer-Pets",
"kw": "发圈",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"823838532886": {
"shop": "Dummer-Pets",
"kw": "卡套",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"698782391248": {
"shop": "Dummer-Pets",
"kw": "贴纸",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"707826905566": {
"shop": "Dummer-Pets",
"kw": "卡包",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"1027332215847": {
"shop": "Dummer-Pets",
"kw": "收纳盒",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"682195499823": {
"shop": "Dummer-Pets",
"kw": "笔袋",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"823560564174": {
"shop": "Dummer-Pets",
"kw": "发圈",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"739147684759": {
"shop": "Dummer-Pets",
"kw": "纽扣",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"621300323670": {
"shop": "Dummer-Pets",
"kw": "胶带座",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"601964534137": {
"shop": "Dummer-Pets",
"kw": "遥控器保护套",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"896469995553": {
"shop": "Dummer-Pets",
"kw": "钥匙扣",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"944470567338": {
"shop": "Dummer-Pets",
"kw": "发夹",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"787261097583": {
"shop": "Dummer-Pets",
"kw": "发夹",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"833753630925": {
"shop": "Dummer-Pets",
"kw": "发箍",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"719391062058": {
"shop": "Dummer-Pets",
"kw": "收纳盒",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"857781294719": {
"shop": "Dummer-Pets",
"kw": "发夹",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"863384503032": {
"shop": "Dummer-Pets",
"kw": "发夹",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"774494945453": {
"shop": "Dummer-Pets",
"kw": "钥匙扣",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"750271307455": {
"shop": "Dummer-Pets",
"kw": "钥匙扣",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"726442972676": {
"shop": "Dummer-Pets",
"kw": "发夹",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"775057912118": {
"shop": "Dummer-Pets",
"kw": "钥匙扣",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"893592264148": {
"shop": "Dummer-Pets",
"kw": "发夹",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"607330049419": {
"shop": "Dummer-Pets",
"kw": "发圈",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
},
"854003999648": {
"shop": "Dummer-Pets",
"kw": "发圈",
"cat": "发夹(Hairpins) 发夹(Hairpins)"
}
}