Files
dianxiaomi-crossborder-play…/shein建品/sh_build.mjs
T

360 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** SHEIN 建品:单条产品的构建函数集(逐步验证中) */
import { P, wait } from './sh_lib.mjs';
export async function mouseClick(c, x, y) {
await c.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y, buttons: 0 }); await wait(250);
for (const t of ['mousePressed', 'mouseReleased'])
await c.send('Input.dispatchMouseEvent', { type: t, x, y, button: 'left', clickCount: 1, buttons: 1 });
}
async function centerOf(c, expr) {
return JSON.parse(String(await c.evalJS(`(()=>{const e=${expr}; if(!e) return '[]';
e.scrollIntoView({block:'center'}); const r=e.getBoundingClientRect();
return JSON.stringify([Math.round(r.x+r.width/2), Math.round(r.y+r.height/2)]);})()`)));
}
/** 选店铺账号(下拉里按名字精确匹配) */
export async function selectShop(c, shopName) {
const pos = await centerOf(c, `[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>/店铺账号/.test((e.querySelector('label')||{}).innerText||''))?.querySelector('.ant-select-selector')`);
if (!pos.length) return 'no-select';
await mouseClick(c, pos[0], pos[1]);
await wait(2200);
const hit = await c.evalJS(`(()=>{
const dd=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null).pop();
if(!dd) return 'no-dd';
const it=[...dd.querySelectorAll('.ant-select-item-option')].find(e=>(e.innerText||'').replace(/\\s+/g,'').includes(${JSON.stringify(shopName.replace(/\s+/g, ''))}));
if(!it) return 'no-match';
const r=it.getBoundingClientRect();
for(const ev of ['mousedown','mouseup','click']) it.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2}));
return 'picked:'+(it.innerText||'').trim().slice(0,24);})()`);
await wait(4500);
return hit;
}
/** 选产品分类(弹窗搜索关键词 → 点第一个结果 → 确定) */
export async function selectCategory(c, kw, preferName) {
const clicked = await c.evalJS(`(()=>{
// 类目项标签各平台不一:SHEIN/Temu/aliChoice 叫「产品分类」,Lazada 叫「分类」;排除「店小秘分类」
const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null).find(e=>{
const t=(e.querySelector('label')||{}).innerText||'';
return /分类/.test(t) && !/店小秘/.test(t) && !!/选择分类/.test(e.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';})()`);
if (clicked !== 'opened') return clicked;
await wait(3000);
const typed = await c.evalJS(`(()=>{
const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
if(!m) return 'no-modal';
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, ${JSON.stringify(kw)}); inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true}));
return 'typed';})()`);
if (typed !== 'typed') return typed;
await wait(800);
// 2) 点「搜索」→ 结果是一条红字完整类目路径,点它才算选中
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(3200);
let picked = 'no-path';
for (let attempt = 0; attempt < 5; attempt++) {
picked = await c.evalJS(`(()=>{
const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
if(!m) return 'no-modal2';
// 搜索结果:.search-result-item(里面通常还嵌 <span class="f-red">最后一级</span>,所以别按 children.length===0 过滤)
const items=[...m.querySelectorAll('.search-result-item')].filter(e=>e.offsetParent!==null);
const want=${JSON.stringify(preferName || '')};
const seg=e=>{const t=(e.innerText||'').replace(/\s+/g,''); const parts=t.split('/'); return ((parts[parts.length-1]||'').split('(')[0]);};
const it=(want && items.find(e=>seg(e)===want)) || (want && items.find(e=>(e.innerText||'').replace(/\s+/g,'').includes(want))) || items[0];
if(!it) return 'no-path';
const r=it.getBoundingClientRect();
for(const ev of ['mousedown','mouseup','click']) it.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window,clientX:r.x+r.width/2,clientY:r.y+r.height/2}));
return 'path:'+(it.innerText||'').replace(/\\s+/g,' ').trim().slice(0,60);})()`);
if (String(picked).startsWith('path:')) break;
if (picked === 'no-modal2') break;
await wait(2000);
}
await wait(1800);
// 3) 弹窗必须关掉,否则后面的点击全被挡(SHEIN/Temu 上是「预览图挂不上」,
// aliChoice 上更狠:弹窗不关 → 所有必填属性都填不上 → 保存报一串牛头不对马嘴的错)
let left = Number(await c.evalJS(`[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).length`));
for (let k = 0; k < 6 && left; k++) {
await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
if(!m) return 'closed';
const bar=[...m.querySelectorAll('button')];
const b=bar.find(x=>/^\\s*(确定|选择)\\s*$/.test((x.innerText||'').trim()))||bar.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}));
const c2=[...m.querySelectorAll('.ant-modal-close')][0]; if(c2) c2.click();
return b?'ok':'still-open';})()`);
await wait(1500);
await c.send('Input.dispatchKeyEvent', {type:'keyDown', key:'Escape', code:'Escape', windowsVirtualKeyCode:27}).catch(()=>{});
await wait(600);
if (k >= 3) await c.evalJS(`(()=>{const w=[...document.querySelectorAll('.ant-modal-wrap')].filter(e=>e.offsetParent!==null).pop(); if(w) w.click(); return 1;})()`);
left = Number(await c.evalJS(`[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).length`));
}
await wait(3000);
left = Number(await c.evalJS(`[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).length`));
return picked + (left ? ' / MODAL-STILL-OPEN' : '');
}
export async function fillTitle(c, title) {
return c.evalJS(`(()=>{const p=${P};
const f=p._s.get('sheinAddProductStore').$state.formState; f.productName=${JSON.stringify(title)};
return 'ok';})()`);
}
/** 逐个点开主题里还没启用的规格(颜色默认已启用,parent 带 disabled 类)。
* 按标签去重:点开一个后同类主题可能还留在 DOM 里,重复点会多出「款式/款式」这种重复规格。 */
export async function addSpecs(c, max = 4, maxTotal = 3) {
const added = [], seen = new Set();
for (let i = 0; i < max; i++) {
// SHEIN 硬限制:一个产品最多 3 个规格(颜色已占 1 个)。加满就停,否则报「规格最多选择3个」。
const boxes = Number(await c.evalJS(`[...document.querySelectorAll('.sku-info-box')].filter(e=>e.offsetParent!==null).length`));
if (boxes >= maxTotal) break;
const r = await c.evalJS(`(()=>{
const items=[...document.querySelectorAll('.theme-select-item')].filter(e=>e.offsetParent!==null&&!/disabled/.test(String(e.className)));
const out=items.map(it=>{const t=[...it.querySelectorAll('*')].find(x=>x.children.length===0&&(x.innerText||'').trim()); return t?((t.innerText||'').trim().slice(0,16)):'';}).filter(Boolean);
return JSON.stringify(out);})()`);
const cands = JSON.parse(String(r || '[]')).filter(x => !seen.has(x));
if (!cands.length) break;
const label = cands[0];
await c.evalJS(`(()=>{
const items=[...document.querySelectorAll('.theme-select-item')].filter(e=>e.offsetParent!==null&&!/disabled/.test(String(e.className)));
const it=items.map(it=>{const t=[...it.querySelectorAll('*')].find(x=>x.children.length===0&&(x.innerText||'').trim()); return {it, t, label:t?(t.innerText||'').trim().slice(0,16):''};})
.find(o=>o.label===${JSON.stringify(label)});
if(!it) return 'nf';
for(const ev of ['mousedown','mouseup','click']) it.t.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
return 'ok';})()`);
seen.add(label);
added.push(label);
await wait(5000);
}
return added.join(' | ');
}
/** 每个规格组各勾第一个值(加规格会重建 SKU 行,必须在选完值之后再填 SKU 表) */
export async function pickSpecValues(c) {
const r = await c.evalJS(`(()=>{const boxes=[...document.querySelectorAll('.sku-info-box')].filter(e=>e.offsetParent!==null);
const done=[];
boxes.forEach((b,i)=>{ const opts=[...b.querySelectorAll('.sku-option')].filter(e=>e.offsetParent!==null);
if(!opts.length){done.push(i+':no-opt');return;}
const inp=opts[0].querySelector('input[type=checkbox]');
if(inp&&inp.checked){done.push(i+':already');return;}
if(inp&&!inp.checked) inp.click();
const lab=opts[0].querySelector('label')||opts[0];
for(const ev of ['mousedown','mouseup','click']) lab.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
done.push(i+':'+(opts[0].innerText||'').trim().slice(0,16)); });
return done.join(' | ');})()`);
await wait(8000);
return r;
}
/** 填 SKU 行(在规格值选完之后调用) */
export async function fillSkuRows(c, { prefix = 'SHEIN', price = '12.90', supply = '8.50', stock = '999' } = {}) {
return c.evalJS(`(()=>{const p=${P};
const d=p._s.get('sheinSkuDataStore').$state.formState;
const rows=d.skuDataList||[]; let n=0;
rows.forEach((x,i)=>{ x.skcSupplierCode=${JSON.stringify(prefix)}+'-'+String(i+1).padStart(3,'0');
x.skcRetailPrice=${JSON.stringify(price)}; x.supplyPrice=${JSON.stringify(supply)}; x.stockVariety=${JSON.stringify(stock)};
x.weight='100'; x.length='200'; x.width='150'; x.height='30'; n++; });
return 'rows:'+n;})()`);
}
/** 尺码表弹窗:6 个「请输入」→ 确定 */
export async function fillSizeTable(c) {
await c.evalJS(`(()=>{const it=[...document.querySelectorAll('.ant-form-item')].filter(e=>e.offsetParent!==null)
.find(x=>{const l=x.querySelector('label'); return l&&/尺码表/.test(l.innerText||'');});
if(!it)return 0; const el=it.querySelector('.link')||it.querySelector('a,span'); if(!el)return 0;
el.scrollIntoView({block:'center'});
for(const ev of ['mousedown','mouseup','click']) el.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window})); return 1;})()`);
await wait(5000);
const f = await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
if(!m)return'no-modal'; const ins=[...m.querySelectorAll('input')].filter(i=>i.offsetParent!==null&&i.placeholder==='请输入');
const vals=['10','18','3','2','18','5']; let n=0;
ins.forEach((el,i)=>{ if(i>=vals.length)return; el.focus();
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set.call(el,vals[i]);
el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); el.dispatchEvent(new Event('blur',{bubbles:true})); n++; });
return 'filled:'+n;})()`);
await wait(1200);
await c.evalJS(`(()=>{const m=[...document.querySelectorAll('.ant-modal')].filter(e=>e.offsetParent!==null).pop();
if(m){const b=[...m.querySelectorAll('button')].find(x=>/^\\s*确定\\s*$/.test(x.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(4000);
return f;
}
/** 图片:sheinImgStore.mainImgData.multipleSpuImgs 吃外链数组 */
export async function fillImages(c, urls) {
const r = await c.evalJS(`(()=>{const p=${P};
const im=p._s.get('sheinImgStore').$state.formState;
const arr=${JSON.stringify(urls)}.map((u,i)=>({url:u,imgUrl:u,name:'img'+(i+1)+'.jpg',width:800,height:800}));
if(!Array.isArray(im.mainImgData.multipleSpuImgs)||im.mainImgData.multipleSpuImgs.length<arr.length) im.mainImgData.multipleSpuImgs=arr;
return 'imgs:'+(Array.isArray(im.mainImgData.multipleSpuImgs)?im.mainImgData.multipleSpuImgs.length:'?');})()`);
await wait(3000);
return r;
}
/** 兜底填「必填但没有值」的类目动态属性。
* · 下拉型(.ant-select 空着)→ 选第一个选项
* · 勾选型(.checkbox-group-with-search 里一个都没勾,如箱包的「特征 Features」)→ 勾第一个
* 这些属性用 .attr-label.required 标记(不是 .ant-form-item-required),不填只会得到
* 一句「产品信息中有错误,请检查」。
*/
export async function fillRequiredSelects(c, skip = ['店铺账号', '产品分类', '主规格', '产品标题', '店小秘分类', '尺码表']) {
const ticked = await (async () => {
const out = [];
for (let round = 0; round < 6; round++) {
const r = await c.evalJS(`(()=>{
const items=[...document.querySelectorAll('.ant-form-item')].filter(e=>{
if(e.offsetParent===null) return false;
const lab=e.querySelector('label');
const req=e.querySelector('.attr-label.required')||(lab&&/required/.test(String(lab.className))?lab:null)
||(lab&&(lab.innerText||'').trim().startsWith('*')?lab:null);
if(!req) return false;
const name=(req.innerText||'').replace(/\s+/g,' ').trim();
if(${JSON.stringify(skip)}.some(s=>name.includes(s))) return false;
const g=e.querySelector('.ant-checkbox-group'); if(!g) return false;
const boxes=[...g.querySelectorAll('.ant-checkbox-wrapper')].filter(x=>x.offsetParent!==null);
if(!boxes.length) return false;
const checked=[...g.querySelectorAll('input.ant-checkbox-input')].some(i=>i.checked);
return !checked;});
const it=items[0]; if(!it) return '[]';
const g=it.querySelector('.ant-checkbox-group');
const box=[...g.querySelectorAll('.ant-checkbox-wrapper')].filter(x=>x.offsetParent!==null)[0];
box.scrollIntoView({block:'center'});
for(const ev of ['mousedown','mouseup','click']) box.dispatchEvent(new MouseEvent(ev,{bubbles:true,cancelable:true,view:window}));
const lab=box.querySelector('span')||box;
return JSON.stringify({name:(it.querySelector('.attr-label.required').innerText||'').replace(/\s+/g,' ').trim().slice(0,24), val:(lab.innerText||'').trim().slice(0,18)});})()`);
const o = JSON.parse(String(r || '[]'));
if (!o.name) break;
out.push(o.name + '=' + o.val);
await wait(1500);
}
return out.join(' | ');
})();
const filled = [];
for (let round = 0; round < 8; round++) {
const tgt = JSON.parse(String(await c.evalJS(`(()=>{
const items=[...document.querySelectorAll('.ant-form-item')].filter(e=>{
const l=e.querySelector('label');
const reqOk = l && (/required/.test(String(l.className)) || (l.innerText||'').trim().startsWith('*') || e.querySelector('.attr-label.required'));
if(!reqOk) return false;
if(e.offsetParent===null) return false;
if(e.querySelector('.ant-form-item')) return false; // 容器项(如「产品属性」):只管叶子项
const name=(l.innerText||'').replace(/\\s+/g,' ').trim();
if(${JSON.stringify(skip)}.some(s=>name.includes(s))) return false;
const sel=e.querySelector('.ant-select'); if(!sel) return false;
const txt=(sel.innerText||'').replace(/\\s+/g,' ').trim();
return !txt || /请选择/.test(txt);});
const it=items[0]; if(!it) return '[]';
const l=it.querySelector('label'); const sel=it.querySelector('.ant-select-selector')||it.querySelector('.ant-select');
sel.scrollIntoView({block:'center'}); const r=sel.getBoundingClientRect();
return JSON.stringify({name:(l.innerText||'').replace(/\\s+/g,' ').trim().slice(0,30), x:Math.round(r.x+r.width/2), y:Math.round(r.y+r.height/2)});})()`)));
if (!tgt.name) break;
await c.send('Input.dispatchKeyEvent', {type:'keyDown', key:'Escape', code:'Escape', windowsVirtualKeyCode:27}).catch(()=>{});
await wait(400);
await c.send('Input.dispatchMouseEvent', {type:'mouseMoved', x:tgt.x, y:tgt.y, buttons:0}); await wait(250);
for (const t of ['mousePressed','mouseReleased']) await c.send('Input.dispatchMouseEvent', {type:t, x:tgt.x, y:tgt.y, button:'left', clickCount:1, buttons:1});
await wait(1800);
if (/产地|Origin/i.test(tgt.name)) { await c.send('Input.insertText', {text:'中国'}).catch(()=>{}); await wait(1500); }
const pick = await c.evalJS(`(()=>{const cy=${JSON.stringify(tgt.y)};
const cands=[...document.querySelectorAll('.ant-select-dropdown')].filter(e=>e.offsetParent!==null);
if(!cands.length) return 'no-dd';
const dd=cands.map(e=>({e,d:Math.abs(e.getBoundingClientRect().top-cy)})).sort((a,b)=>a.d-b.d)[0].e;
const ops=[...dd.querySelectorAll('.ant-select-item-option')].filter(e=>e.offsetParent!==null);
if(!ops.length) return 'no-op';
const want=${JSON.stringify(tgt.name)};
const clean=x=>(x.innerText||'').replace(/\s+/g,' ').trim();
const op=(/产地|Origin/i.test(want) ? ops.find(o=>/中国|China/i.test(clean(o))) : null)
|| ops.find(o=>!/^(无|none|NONE)/.test(clean(o))) || 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,18);})()`);
filled.push(tgt.name + '=' + pick);
await wait(1800);
if (pick === 'no-dd' || pick === 'no-op') break;
}
return [filled.join(' | '), ticked].filter(x=>x&&x!=='none').join(' | ') || 'none';
}
/** 兜底填「必填但空着」的**文本输入**型类目属性(aliChoice 的「材质(Material)」就是 input,不是 select)。
* values:候选值,按顺序给每个空文本项发一个(不够就重复最后一个)。 */
export async function fillRequiredTextInputs(c, values = ['Other'], skip = ['店铺账号', '产品分类', '主规格', '产品标题', '店小秘分类', '尺码表', 'ParentSKU']) {
// values 可以是数组(按顺序发),也可以是 (字段名)=>值的函数(推荐:不同字段给对的值)
const filled = [];
for (let round = 0; round < 6; round++) {
const tgt = JSON.parse(String(await c.evalJS(`(()=>{
const items=[...document.querySelectorAll('.ant-form-item')].filter(e=>{
if(e.offsetParent===null) return false;
const l=e.querySelector('label');
const reqOk = l && (/required/.test(String(l.className)) || (l.innerText||'').trim().startsWith('*') || e.querySelector('.attr-label.required'));
if(!reqOk) return false;
if(e.querySelector('.ant-form-item')) return false;
const name=(l.innerText||'').replace(/\\s+/g,' ').trim();
if(${JSON.stringify(skip)}.some(s=>name.includes(s))) return false;
if(e.querySelector('.ant-select,.ant-checkbox-group,.ant-radio-group,.ant-cascader')) return false;
const inp=e.querySelector('input.ant-input, textarea.ant-input');
if(!inp || inp.offsetParent===null) return false;
if(String(inp.value||'').trim()) return false;
return true;});
const it=items[0]; if(!it) return '[]';
const l=it.querySelector('label');
return JSON.stringify({name:(l.innerText||'').replace(/\\s+/g,' ').trim().slice(0,30)});})()`)));
if (!tgt.name) break;
const v = (typeof values === 'function')
? String(values(tgt.name) || 'Other')
: (values[Math.min(filled.length, values.length - 1)] || 'Other');
const ok = await c.evalJS(`(()=>{
const items=[...document.querySelectorAll('.ant-form-item')].filter(e=>{
if(e.offsetParent===null) return false;
const l=e.querySelector('label');
const reqOk = l && (/required/.test(String(l.className)) || (l.innerText||'').trim().startsWith('*') || e.querySelector('.attr-label.required'));
if(!reqOk) return false;
if(e.querySelector('.ant-form-item')) return false;
const name=(l.innerText||'').replace(/\\s+/g,' ').trim();
if(${JSON.stringify(skip)}.some(s=>name.includes(s))) return false;
if(e.querySelector('.ant-select,.ant-checkbox-group,.ant-radio-group,.ant-cascader')) return false;
const inp=e.querySelector('input.ant-input, textarea.ant-input');
return inp && inp.offsetParent!==null && !String(inp.value||'').trim();});
const it=items.find(e=>((e.querySelector('label')||{}).innerText||'').replace(/\\s+/g,' ').trim().slice(0,30)===${JSON.stringify(tgt.name)});
if(!it) return 'gone';
const inp=it.querySelector('input.ant-input, textarea.ant-input');
const proto = inp.tagName==='TEXTAREA' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
Object.getOwnPropertyDescriptor(proto,'value').set.call(inp, ${JSON.stringify(v)});
inp.dispatchEvent(new Event('input',{bubbles:true})); inp.dispatchEvent(new Event('change',{bubbles:true}));
inp.dispatchEvent(new Event('blur',{bubbles:true}));
return 'ok';})()`);
filled.push(tgt.name + '=' + v);
await wait(1200);
if (ok === 'gone') break;
}
return filled.join(' | ') || 'none';
}
/** 兜底填**原生 <select>**(aliChoice 的「SKU分类」= 单品单件/同款多件/混合套装,
* 以及「件/个 / 双」这类单位选择)。antd 选择器一个都匹配不到,只能按原生元素处理。 */
export async function fillNativeSelects(c, preferred = ['单品单件', '件/个']) {
return c.evalJS(`(()=>{const pref=${JSON.stringify(preferred)};
const done=[];
for (const sel of document.querySelectorAll('select')) {
if (sel.offsetParent === null) continue;
if (sel.multiple) continue;
const cur = sel.options[sel.selectedIndex];
const empty = !sel.value || (cur && cur.disabled) || sel.selectedIndex <= 0;
if (!empty) continue;
const opts = [...sel.options].filter(o => !o.disabled && o.value !== '');
if (!opts.length) continue;
let pick = null;
for (const p of pref) { pick = opts.find(o => (o.textContent || '').includes(p)); if (pick) break; }
if (!pick) pick = opts[0];
sel.value = pick.value;
sel.dispatchEvent(new Event('input', { bubbles: true }));
sel.dispatchEvent(new Event('change', { bubbles: true }));
const label = (sel.closest('.ant-form-item') || {}).innerText || '';
done.push((label.replace(/\\s+/g, ' ').trim().slice(0, 12) || 'select') + '=' + (pick.textContent || '').trim().slice(0, 12));
}
return done.join(' | ') || 'none';})()`);
}