import { newTarget, CDP, SAN } from '/tmp/cdp3.mjs';
import fs from 'fs';
/* 速卖通批量建品(全自动):分类走 UI 弹窗搜索,其余按已验证配方 */
const SHOP = 4024039;
const FREIGHT_NAME = process.argv[2] || 'MY beauty 30g+1美金';
const ONLY = process.argv[3];
const wait = ms => new Promise(z => setTimeout(z, ms));
const log = (...a) => console.log(' ', ...a);
const EN = JSON.parse(fs.readFileSync('/tmp/en_all.json', 'utf8'));
const CLEAN = JSON.parse(fs.readFileSync('/tmp/wm_clean.json', 'utf8'));
const ALL = JSON.parse(fs.readFileSync('/tmp/all48.json', 'utf8'));
const KW_FB = {};
const DONE_F = '/tmp/smt_batch_done.json';
const DONE = fs.existsSync(DONE_F) ? JSON.parse(fs.readFileSync(DONE_F, 'utf8')) : {};
// 分类关键词:从英文标题里取最能定位品类的词
function catKeywords(oid) {
const t = (EN[oid] || {}).title || '';
const CAND = [
[/whistle/i, ['whistle']],
[/hair tie|scrunchie|hair ring|ponytail|elastic/i, ['hair', 'hairband']],
[/hair clip|barrette|hairpin|clip/i, ['clip', 'hair']],
[/headband|hair band/i, ['headband', 'hair']],
[/keychain|key ring|key holder/i, ['keychain']],
[/sticker|decal/i, ['sticker']],
[/thermometer|hygrometer/i, ['thermometer']],
[/button/i, ['button']],
[/yarn|knitting/i, ['yarn']],
[/sock/i, ['socks']],
[/necklace|pendant/i, ['necklace']],
[/bracelet/i, ['bracelet']],
[/earring/i, ['earring']],
[/card holder|card case|badge/i, ['card']],
[/organizer|storage/i, ['organizer']],
[/box|case|holder|container/i, ['box']],
[/bag|pouch/i, ['bag']],
[/comb|brush/i, ['comb']],
[/mirror/i, ['mirror']],
[/tape dispenser/i, ['tape']],
[/hair dryer/i, ['hair dryer']],
];
for (const [rx, ks] of CAND) if (rx.test(t)) return ks;
return [(t.split(/\s+/)[0] || 'gift')];
}
const oids = ONLY ? [ONLY] : Object.keys(ALL).filter(o => !DONE[o] && (CLEAN[o] || []).length >= 1);
log(`待建商品: ${oids.length} | 已有记录: ${Object.keys(DONE).length}`);
for (const oid of oids) {
const en = EN[oid], imgs = CLEAN[oid] || [];
let kws = catKeywords(oid);
if (!en || !imgs.length) { log(`⚠ ${oid} 缺文案或图片,跳过`); continue; }
log('');
log(`═══ ${oid} | 图${imgs.length} 张 | 关键词候选 ${JSON.stringify(kws)} ═══`);
// 清掉旧标签
const tl = await (await fetch('http://127.0.0.1:9223/json/list')).json();
for (const t of tl.filter(x => x.type === 'page' && /\/web\/smt\/add/.test(x.url))) await fetch(`http://127.0.0.1:9223/json/close/${t.id}`).catch(() => {});
await wait(900);
const t = await newTarget('about:blank');
const c = await CDP.connect(t.webSocketDebuggerUrl);
await c.send('Page.enable'); await c.send('Runtime.enable'); await c.send('Network.enable');
let posted = false;
c.on(m => { if (m.method === 'Network.requestWillBeSent') { const r = m.params.request;
if (r.method === 'POST' && /\/api\/smtProduct\/add\.json/.test(r.url)) posted = true; } });
try {
await c.send('Page.navigate', { url: 'https://www.dianxiaomi.com/web/smt/add' });
await wait(32000);
await c.evalJS(`(function(){
window.__f=function(rx,val,sel){ sel=sel||'input';
var its=[].slice.call(document.querySelectorAll('.ant-form-item'));
for(var i=0;i
') + '
Keywords: ' + en.keywords + '
')}); ed.fire('change'); ed.fire('blur'); ed.updateElement(); return 1; }catch(e){ return 0; }})()`); await wait(2000); // ⑨ 图片(直接赋值,外部 URL 可用) const imgData = imgs.slice(0, 6).map((u, i) => ({ id: String(i + 1), url: u, name: 'img' + i + '.jpg', width: 800, height: 800, imgId: String(1000 + i), checked: false })); await c.evalJS(`(function(){var el=document.querySelector('#app');var p=el.__vue_app__.config.globalProperties.$pinia; p._s.get('smtAddProductStore').$state.formState.imageList=${JSON.stringify(imgData)}; return 1;})()`); await wait(3000); log(` ⑥ 描述+图片(${imgData.length}张) ok`); // ⑩ 保存(调试版:埋钩子) const hook = await c.evalJS(`(function(){ window.__cap=[]; try{ var M = window.MYJ && window.MYJ.message; if(M){ ['error','warning','info','success'].forEach(function(k){ var orig=M[k]; M[k]=function(){ try{ window.__cap.push(k+': '+Array.prototype.slice.call(arguments).map(function(a){return typeof a==='string'?a:JSON.stringify(a);}).join(' ').slice(0,200)); }catch(e){} return orig.apply(M, arguments); }; }); return 'hooked MYJ.message'; } return 'no MYJ'; }catch(e){ return 'ERR '+e.message; }})()`); log(' 钩子:', hook); const btns = await c.evalJS(`JSON.stringify([].slice.call(document.querySelectorAll('button')).filter(function(e){return e.offsetParent!==null;}).map(function(b){ var r=b.getBoundingClientRect(); return {t:(b.innerText||'').replace(/\\s+/g,' ').trim().slice(0,14), y:Math.round(r.y), dis:b.disabled, cls:String(b.className||'').slice(0,34)};}).filter(function(x){return x.t;}))`); log(' 按钮清单:', SAN(String(btns)).slice(0,420)); await c.evalJS(`(function(){var bs=[].slice.call(document.querySelectorAll('button')).filter(function(e){return e.offsetParent!==null&&/^\\s*保存\\s*$/.test((e.innerText||'').trim());}); if(bs.length) bs[0].click(); return 1;})()`); await wait(9000); const cap = await c.evalJS(`JSON.stringify(window.__cap||[])`); log(' ★ 被捕获的错误消息:', SAN(String(cap)).slice(0,500)); const errs = await c.evalJS(`JSON.stringify([].slice.call(document.querySelectorAll('.ant-form-item-explain-error')).map(function(e){ var t=(e.innerText||'').trim(); if(!t) return null; var p=e.closest? e.closest('.ant-form-item') : null; var lab=p? (p.querySelector('label')||{}).innerText||'' : ''; return lab.replace(/\\s+/g,' ').trim().slice(0,40)+' → '+t;}).filter(Boolean))`); log(' 行内校验错误(带字段):', SAN(String(errs)).slice(0,400)); const hasErrItems = await c.evalJS(`JSON.stringify([].slice.call(document.querySelectorAll('.ant-form-item-has-error')).map(function(x){ var l=x.querySelector('label'); return l?(l.innerText||'').replace(/\\s+/g,' ').trim().slice(0,44):'?';}))`); log(' 出错字段列表:', SAN(String(hasErrItems)).slice(0,400)); await wait(8000); const msg = String(await c.evalJS(`JSON.stringify([].slice.call(document.querySelectorAll('.ant-modal,.ant-message-notice')).filter(function(e){return e.offsetParent!==null;}).map(function(e){return (e.innerText||'').replace(/\\s+/g,' ').trim().slice(0,120);}))`)); const ok = /已保存到/.test(msg); log(` ${ok ? '✅' : '❌'} 保存: ${SAN(msg).slice(0, 110)} | POST=${posted}`); DONE[oid] = { ok, msg: SAN(msg).slice(0, 100), imgs: imgData.length, kws, time: new Date().toISOString() }; fs.writeFileSync(DONE_F, JSON.stringify(DONE, null, 1)); } catch (e) { log(` ❌ 异常: ${String(e).slice(0, 90)}`); DONE[oid] = { ok: false, err: String(e).slice(0, 90) }; fs.writeFileSync(DONE_F, JSON.stringify(DONE, null, 1)); } try { c.close(); } catch (e) {} await wait(1200); } log(''); log(`全部结束。成功 ${Object.values(DONE).filter(x => x.ok).length} / ${Object.keys(DONE).length}`);