XSJv3.1

一键成品面板:输入“型号 商品名称”后自动拆分填写;只保留一个按钮。

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         XSJv3.1
// @namespace    http://tampermonkey.net/
// @version      28.0.1112.3
// @description  一键成品面板:输入“型号 商品名称”后自动拆分填写;只保留一个按钮。
// @author       Assistant
// @match        *://agentseller.temu.com/goods*
// @grant        none
// @license      y
// ==/UserScript==

(function () {
    'use strict';

    /* ─── 配置区 ────────────────────────────────────────────────────── */
    const CONFIG = {
        queue: [
            ['商品产地', '中国大陆'],
            ['关闭', '不关闭'],
            ['包含的组件', '无'],
            ['衬里说明', '无衬里'],
            ['油边', '否'],
            ['货源产地', '广州产区'],
            ['护理说明', '不可洗'],
            ['特征', '固定肩带'],
            ['图案样式', '其他印花'],
            ['印花类型', '定位印花'],
            ['风格', '休闲'],
            ['材料', '涤纶'],
        ],
        province: '广东省',

        parentSpec1: { typeValueId: 1001, specValues: ['黄色'] },
        parentSpec2: { typeValueId: 45114199, specValues: ['套装A', '套装B', '套装C', '套装D', '套装E'] },

        dimensions: {
            default: ['34', '30', '6', '335'],
            byModel: {
                '套装A': ['32', '20', '3', '260'],
                '套装B': ['32', '27', '6', '430'],
                '套装C': ['32', '20', '3', '327'],
                '套装D': ['32', '27', '6.5', '470'],
                '套装E': ['32', '27', '6.5', '490']
            }
        },
        sizeChart: [
            { label: '宽度', min: '30', max: '33' },
            { label: '长度', min: '40', max: '43' }
        ],
        sku: {
            skuTypeId: 1,
            skuClassLabel: '混合套装',
            declarePriceByModel: {
                '套装A': '30',
                '套装B': '40',
                '套装C': '38',
                '套装D': '43',
                '套装E': '45'
            },
            singleItemCountByModel: {
                '套装A': '2',
                '套装B': '2',
                '套装C': '3',
                '套装D': '3',
                '套装': '3'
            },
            packMode: '不是独立包装',
            currency: 'CNY',
            suggestPrice: '199',
            packageItemCount: '1',
            packageListByModel: {
                '套装A': ['托特包', '化妆包'],
                '套装B': ['托特包', '帽子'],
                '套装C': ['托特包', '化妆包', '帽子']
            },
            cargoNo: 'NYZX'
        }
    };

    const STATE = {
        modelText: '',
        productNameText: ''
    };

    /* ─── 时间常量 ──────────────────────────────────────────────────── */
    const T = {
        MICRO: 80,
        SHORT: 150,
        MED: 300,
        SPEC: 1800
    };

    const wait = (ms) => new Promise((r) => setTimeout(r, ms));
    const clean = (t) => (t || '').replace(/\s+/g, ' ').trim();

    function parseInputLine(raw) {
        const text = String(raw || '').replace(/\u00A0/g, ' ').trim();
        if (!text) {
            return null;
        }
        const m = text.match(/^(\S+)\s+(.+)$/);
        if (!m) {
            return null;
        }
        return {
            model: clean(m[1]),
            name: clean(m[2])
        };
    }

    function readPanelData() {
        const input = document.getElementById('temu-onekey-source');
        const parsed = parseInputLine(input?.value || '');
        if (!parsed) {
            throw new Error('请输入“型号 商品名称”,中间至少一个空格或 Tab');
        }
        STATE.modelText = parsed.model;
        STATE.productNameText = parsed.name;
        CONFIG.sku.cargoNo = parsed.model;
        return parsed;
    }

    /* ════════════════════════════════════════════════════════════════
       原点击/输入工具
    ════════════════════════════════════════════════════════════════ */

    async function superClick(el) {
        if (!el) return;
        el.scrollIntoView({ block: 'center' });
        await wait(0);
        const rect = el.getBoundingClientRect();
        const clientX = rect.left + rect.width / 2;
        const clientY = rect.top + rect.height / 2;
        [
            new PointerEvent('pointerdown', { bubbles: true, cancelable: true, pointerType: 'mouse', clientX, clientY }),
            new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window, clientX, clientY }),
            new PointerEvent('pointerup', { bubbles: true, cancelable: true, pointerType: 'mouse', clientX, clientY }),
            new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window, clientX, clientY }),
            new MouseEvent('click', { bubbles: true, cancelable: true, view: window, clientX, clientY })
        ].forEach((ev) => el.dispatchEvent(ev));
    }

    function fastClick(el) {
        if (!el) return;
        const opts = { bubbles: true, view: window };
        el.dispatchEvent(new MouseEvent('mousedown', opts));
        el.dispatchEvent(new MouseEvent('click', opts));
        el.dispatchEvent(new MouseEvent('mouseup', opts));
    }

    function setReactValue(el, val) {
        if (!el) return;
        const prev = el.value;
        el.value = val;
        const t = el._valueTracker;
        if (t) t.setValue(prev);
        el.dispatchEvent(new Event('input', { bubbles: true }));
        el.dispatchEvent(new Event('change', { bubbles: true }));
    }

    function setVal(el, val) {
        if (!el) return;
        const prev = el.value;
        el.value = val;
        const tracker = el._valueTracker;
        if (tracker) tracker.setValue(prev);
        el.dispatchEvent(new Event('input', { bubbles: true }));
        el.dispatchEvent(new Event('change', { bubbles: true }));
    }

    function click(el) {
        if (!el) return;
        el.scrollIntoView({ block: 'center' });
        const r = el.getBoundingClientRect();
        const cx = r.left + r.width / 2;
        const cy = r.top + r.height / 2;
        const s = { bubbles: true, cancelable: true, view: window, clientX: cx, clientY: cy };
        ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) =>
            el.dispatchEvent(type.startsWith('pointer')
                ? new PointerEvent(type, { ...s, pointerType: 'mouse' })
                : new MouseEvent(type, s))
        );
    }

    async function pickOptionExact(val) {
        await wait(0);
        const popups = Array.from(document.querySelectorAll(
            '.beast-select-dropdown:not([style*="display: none"]), .ST_popupWrapper_5-120-1, [class*="ST_popup"]'
        )).filter((el) => window.getComputedStyle(el).display !== 'none');
        const lastPopup = popups[popups.length - 1] || document;
        const opts = Array.from(lastPopup.querySelectorAll(
            '.ST_item_5-120-1, .beast-select-item-option-content, [role="option"], li'
        ));
        const hit = opts.find((o) => o.innerText.trim() === val)
            || opts.find((o) => o.innerText.trim().includes(val));
        if (hit) {
            fastClick(hit);
            return true;
        }
        return false;
    }

    function findItemV26(labelText) {
        for (const el of document.querySelectorAll('.Form_itemLabelContent_5-120-1')) {
            const txt = el.innerText.trim();
            if (txt === labelText || txt.startsWith(labelText)) {
                return el.closest('.Form_item_5-120-1');
            }
        }
        return null;
    }

    async function selectDropdown(labelText, val) {
        if (Array.isArray(val)) {
            for (const item of val) {
                await selectDropdown(labelText, item);
                await wait(T.SHORT);
            }
            return;
        }
        const item = findItemV26(labelText);
        if (!item) return;
        const trigger = item.querySelector('.ST_outerWrapper_5-120-1, .beast-select-selector, input');
        document.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 27, bubbles: true }));
        await wait(0);
        fastClick(trigger);
        const input = item.querySelector('input');
        if (input) {
            setReactValue(input, val);
            await wait(0);
        }
        await pickOptionExact(val);
    }

    async function selectProvince() {
        const provInp = document.querySelector('input[placeholder="请选择省份"]');
        if (!provInp) return;
        fastClick(provInp.closest('.ST_outerWrapper_5-120-1') || provInp);
        setReactValue(provInp, CONFIG.province);
        await pickOptionExact(CONFIG.province);
    }

    async function clickAddParentSpec2() {
        const target = Array.from(document.querySelectorAll('div, span, button'))
            .filter((el) => (el.innerText || '').includes('添加父规格 2') && el.offsetWidth > 0)
            .reverse()[0];
        if (target) {
            await superClick(target);
        } else {
            const fallback = Array.from(document.querySelectorAll('button, div, span'))
                .find((el) => el.innerText.trim() === '添加父规格' && el.offsetWidth > 0);
            if (fallback) await superClick(fallback);
        }
    }

    function getCtrl(el) {
        if (!el) return null;
        const key = Object.keys(el).find(
            (k) => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')
        );
        let node = key ? el[key] : null;
        let d = 0;
        while (node && d < 20) {
            const p = node.memoizedProps || {};
            if (Array.isArray(p.options) && typeof p.onChange === 'function') return p;
            node = node.return;
            d++;
        }
        return null;
    }

    function pickById(ctrl, valueId) {
        if (!ctrl) return false;
        const opt = ctrl.options.find((o) => String(o.value) === String(valueId));
        if (!opt) return false;
        ctrl.onChange(opt.value, opt);
        return true;
    }

    function pickByLabel(ctrl, label) {
        if (!ctrl) return false;
        const opt = ctrl.options.find((o) => clean(o.label) === label)
            || ctrl.options.find((o) => clean(o.label).includes(label));
        if (!opt) return false;
        ctrl.onChange(opt.value, opt);
        return true;
    }

    function findFormItem(labelText) {
        const target = clean(labelText).replace(/\s+/g, '');
        for (const el of document.querySelectorAll('[class*="Form_itemLabelContent"]')) {
            const t = clean(el.innerText);
            const nt = t.replace(/\s+/g, '');
            if (t === labelText || t.startsWith(labelText) || nt === target || nt.startsWith(target)) {
                return el.closest('[class*="Form_item_"]');
            }
        }
        return null;
    }

    function findSectionRootByText(sectionLabel) {
        const target = clean(sectionLabel).replace(/\s+/g, '');
        const sections = [...document.querySelectorAll('div, section, form, article')];
        const byText = sections.find((el) => {
            const text = clean(el.innerText || '');
            const nt = text.replace(/\s+/g, '');
            return (text.includes(sectionLabel) || nt.includes(target)) && text.includes('批量填写');
        }) || null;
        if (byText) return byText;

        const byInput = [...document.querySelectorAll('input, textarea')].find((el) => {
            const hint = clean(el.placeholder || '');
            const owner = clean(el.closest('div, section, form, article')?.innerText || '');
            const nh = hint.replace(/\s+/g, '');
            const no = owner.replace(/\s+/g, '');
            return (hint.includes(sectionLabel) || nh.includes(target) || owner.includes(sectionLabel) || no.includes(target));
        });
        return byInput ? byInput.closest('div, section, form, article') : null;
    }

    function findVolumeWeightSectionRoot() {
        return [...document.querySelectorAll('[data-testid="beast-core-form-item"], div, section, form, article')]
            .find((el) => {
                const text = clean(el.innerText || '').replace(/\s+/g, '');
                return text.includes('敏感属性与体积重量') && text.includes('体积(单位:cm)') && text.includes('重量');
            }) || null;
    }

    function findVolumeWeightBatchButton(root) {
        const scope = root || findVolumeWeightSectionRoot();
        if (!scope) return null;
        return scope.querySelector('.batch-input_batchContainer__GINEg button')
            || [...scope.querySelectorAll('button, span, div')]
                .find((el) => clean(el.innerText) === '批量填写')
            || null;
    }

    function findInput(labelText, { last = true } = {}) {
        const item = findFormItem(labelText);
        if (!item) return null;
        const inputs = [...item.querySelectorAll(
            'input:not([readonly]):not([type="radio"]):not([type="checkbox"]):not([type="file"]), textarea'
        )];
        return inputs.length ? (last ? inputs[inputs.length - 1] : inputs[0]) : null;
    }

    function findParentSpecTypeInputs() {
        return [...document.querySelectorAll('input')]
            .filter((el) => {
                if (!el.readOnly) return false;
                const ctrl = getCtrl(el);
                return ctrl?.options?.some((o) => ['1001', '45114199'].includes(String(o.value)));
            });
    }

    function findSpecBlockRoot(specTypeEl) {
        let root = specTypeEl.closest(
            '[class*="Form_item_"], [class*="specRow"], [class*="SpecRow"], tr, [class*="row_"], [class*="Row_"]'
        ) || specTypeEl.parentElement;

        let node = root;
        let depth = 0;
        while (node && depth < 8) {
            const text = clean(node.innerText || '');
            if (text.includes('继续添加子规格') || text.includes('添加子规格')) {
                root = node;
            }
            node = node.parentElement;
            depth++;
        }
        return root || specTypeEl.parentElement;
    }

    function listEditableSpecInputs(root) {
        return [...(root || document).querySelectorAll('input, textarea')]
            .filter((el) => !el.readOnly && !el.disabled)
            .filter((el) => !['file', 'radio', 'checkbox', 'hidden'].includes((el.type || '').toLowerCase()));
    }

    function findSpecificSpecTableRoot(labelText) {
        const normalize = (t) => clean(t).replace(/^\+\s*/, '');
        const tables = [...document.querySelectorAll('table.TB_tableWrapper_5-120-1')];
        return tables.find((table) => {
            const text = clean(table.innerText || '');
            const hasContinue = [...table.querySelectorAll('button, div, span, a')]
                .some((el) => normalize(el.innerText) === '继续添加子规格');
            return hasContinue && text.includes(`*${labelText}`);
        }) || null;
    }

    async function fillOneParentSpec(specTypeEl, typeValueId, specTextValue) {
        const ctrl = getCtrl(specTypeEl);
        if (!pickById(ctrl, typeValueId)) {
            pickByLabel(ctrl, String(typeValueId));
        }
        await wait(T.MED);

        const blockRoot = findSpecBlockRoot(specTypeEl);
        const container = specTypeEl.closest(
            '[class*="specRow"], [class*="SpecRow"], tr, [class*="row_"], [class*="Row_"]'
        ) || blockRoot || specTypeEl.parentElement?.parentElement;

        let valueEl = container
            ? container.querySelector('input:not([readonly]):not([disabled]):not([type="file"]):not([type="radio"]):not([type="checkbox"])')
            : null;

        if (!valueEl) {
            const all = [...document.querySelectorAll('input')];
            const selfIdx = all.indexOf(specTypeEl);
            valueEl = all.slice(selfIdx + 1, selfIdx + 6)
                .find((el) => !el.readOnly && !el.disabled && !['file', 'radio', 'checkbox'].includes(el.type));
        }

        if (valueEl) {
            valueEl.focus();
            setVal(valueEl, specTextValue);
            await wait(T.SHORT);
        } else {
            console.warn('[Temu助手] 找不到规格值输入框');
        }
        return { blockRoot, valueEl };
    }

    async function clickAddSpec2Fallback() {
        const target = Array.from(document.querySelectorAll('div, span, button'))
            .filter((el) => (el.innerText || '').includes('添加父规格 2') && el.offsetWidth > 0)
            .reverse()[0];
        if (target) {
            await superClick(target);
            return;
        }
        const fallback = Array.from(document.querySelectorAll('button, div, span'))
            .find((el) => el.innerText.trim() === '添加父规格' && el.offsetWidth > 0);
        if (fallback) {
            await superClick(fallback);
            return;
        }
        throw new Error('未找到“添加父规格 2”按钮');
    }

    async function clickContinueAddChildSpec(root = document) {
        const normalize = (t) => clean(t).replace(/^\+\s*/, '');
        const btn = [...root.querySelectorAll('button')]
            .find((el) => normalize(el.innerText) === '继续添加子规格')
            || [...root.querySelectorAll('div, span, a')]
                .find((el) => normalize(el.innerText) === '继续添加子规格');
        if (!btn) throw new Error('未找到“继续添加子规格”按钮');
        click(btn.closest('button') || btn);
        await wait(T.MED);
    }

    async function fillParentSpecs() {
        const appendChildSpecValues = async (values, root) => {
            let inputs = listEditableSpecInputs(root);
            for (let i = 1; i < values.length; i++) {
                const beforeCount = inputs.length;
                await clickContinueAddChildSpec(root);
                const start = Date.now();
                while (true) {
                    inputs = listEditableSpecInputs(root);
                    if (inputs.length > beforeCount) break;
                    if (Date.now() - start > 5000) throw new Error('新增子规格输入框超时');
                    await wait(120);
                }

                const input = inputs[beforeCount];
                if (!input) throw new Error(`未找到子规格输入框 ${i + 1}`);
                input.focus();
                setVal(input, values[i]);
                await wait(T.SHORT);
            }
        };

        let specInputs = findParentSpecTypeInputs();
        if (!specInputs.length) {
            console.warn('[Temu助手] 未找到父规格1');
            return;
        }
        const parentSpec1Result = await fillOneParentSpec(specInputs[0], CONFIG.parentSpec1.typeValueId, CONFIG.parentSpec1.specValues[0]);
        const parentSpec1Root = findSpecificSpecTableRoot('颜色') || parentSpec1Result.blockRoot;
        await appendChildSpecValues(CONFIG.parentSpec1.specValues, parentSpec1Root);

        await clickAddParentSpec2();

        specInputs = findParentSpecTypeInputs();
        if (specInputs.length < 2) {
            console.warn('[Temu助手] 未找到父规格2');
            return;
        }
        const parentSpec2Result = await fillOneParentSpec(specInputs[1], CONFIG.parentSpec2.typeValueId, CONFIG.parentSpec2.specValues[0]);
        const parentSpec2Root = findSpecificSpecTableRoot('型号') || parentSpec2Result.blockRoot;
        await appendChildSpecValues(CONFIG.parentSpec2.specValues, parentSpec2Root);
    }

    function isEditableTextControl(el) {
        if (!el) return false;
        if (el.disabled || el.readOnly) return false;
        if (el.matches('textarea')) return true;
        if (el.matches('input')) {
            const t = (el.type || '').toLowerCase();
            return !['file', 'radio', 'checkbox', 'hidden'].includes(t);
        }
        return false;
    }

    function findProductNameInput() {
        const exactLabelEls = [...document.querySelectorAll('div, span, label, p')]
            .filter((el) => clean(el.innerText) === '商品名称' && el.offsetParent !== null);

        for (const labelEl of exactLabelEls) {
            let cur = labelEl;
            for (let i = 0; i < 6 && cur; i++) {
                const scope = cur.parentElement || cur;
                const controls = [...scope.querySelectorAll('textarea, input')]
                    .filter(isEditableTextControl)
                    .filter((el) => {
                        const text = clean(el.closest('[class*="Form_item_"], div, section, form')?.innerText || '');
                        return !text.includes('英文名称');
                    });
                if (controls.length) return controls[0];
                cur = cur.parentElement;
            }

            let next = labelEl.nextElementSibling;
            let guard = 0;
            while (next && guard < 8) {
                const controls = [...next.querySelectorAll('textarea, input')]
                    .filter(isEditableTextControl)
                    .filter((el) => {
                        const text = clean(el.closest('[class*="Form_item_"], div, section, form')?.innerText || '');
                        return !text.includes('英文名称');
                    });
                if (controls.length) return controls[0];
                next = next.nextElementSibling;
                guard++;
            }
        }

        const candidates = [...document.querySelectorAll('textarea[placeholder="请输入"], input[placeholder="请输入"]')]
            .filter(isEditableTextControl)
            .filter((el) => {
                const areaText = clean(el.closest('[class*="Form_item_"], form, section, div')?.innerText || '');
                return areaText.includes('商品名称') && !areaText.includes('英文名称');
            });

        if (candidates.length) return candidates[0];

        return null;
    }

    async function fillProductName() {
        const input = findProductNameInput();
        if (!input) {
            console.warn('[Temu助手] 未找到商品名称输入框');
            return;
        }
        input.focus();
        setVal(input, STATE.productNameText);
        await wait(T.SHORT);
    }

    async function fillDimensions() {
        const volumeRoot = findVolumeWeightSectionRoot();
        const volumeTable = volumeRoot?.querySelector('table.performance-table_performanceTable__dwfgW');
        if (volumeTable) {
            const rows = [...volumeTable.querySelectorAll('tbody tr')];
            for (const row of rows) {
                const rowText = clean(row.innerText || '');
                const modelName = Object.keys(CONFIG.dimensions.byModel).find((name) => rowText.includes(name));
                const rowValues = CONFIG.dimensions.byModel[modelName] || CONFIG.dimensions.default;
                const inputs = [...row.querySelectorAll('input[placeholder="请输入"]')]
                    .filter((el) => !el.disabled && !el.readOnly);
                if (inputs.length < rowValues.length) {
                    console.warn('[Temu助手] 体积重量行输入框数量不足');
                    continue;
                }
                for (let i = 0; i < rowValues.length; i++) {
                    inputs[i].focus();
                    setVal(inputs[i], rowValues[i]);
                    await wait(60);
                }
            }
            return;
        }
    }

    function modalBody() {
        return document.querySelector('[data-testid="beast-core-modal-body"]');
    }

    async function openSizeChartDialog() {
        const trigger = [...document.querySelectorAll('button, span, div')]
            .find((el) => {
                const txt = clean(el.innerText);
                return txt === '添加尺码表' || txt === '编辑尺码表';
            });
        if (!trigger) throw new Error('未找到“添加尺码表”按钮');

        click(trigger.closest('button') || trigger);
        await wait(500);

        const start = Date.now();
        while (!modalBody()) {
            if (Date.now() - start > 5000) throw new Error('尺码表弹层未打开');
            await wait(100);
        }
    }

    function getBaseCheckBoxes() {
        const modal = modalBody();
        if (!modal) return [];
        const labels = new Set(CONFIG.sizeChart.map((item) => `${item.label}(cm)`));
        return [...modal.querySelectorAll('label[data-testid="beast-core-checkbox"]')]
            .filter((label) => labels.has(clean(label.innerText)));
    }

    async function waitUntilBaseMetricsReady() {
        const expected = CONFIG.sizeChart.length;
        const start = Date.now();
        while (true) {
            const boxes = getBaseCheckBoxes();
            if (boxes.length === expected) return boxes;
            if (Date.now() - start > 5000) throw new Error('尺码表基础选项未就绪');
            await wait(100);
        }
    }

    async function clickBaseMetrics() {
        const boxes = await waitUntilBaseMetricsReady();
        for (const label of boxes) {
            const checkbox = label.querySelector('input[type="checkbox"]');
            if (checkbox && !checkbox.checked) {
                click(checkbox);
                await wait(120);
            }
        }
    }

    async function ensureRangeCheckboxes() {
        const modal = modalBody();
        if (!modal) throw new Error('未找到尺码表弹层');

        const expectedLabels = CONFIG.sizeChart.map((item) => `${item.label} 范围区间`);
        const start = Date.now();
        while (true) {
            const text = clean(modal.innerText);
            if (expectedLabels.every((label) => text.includes(label))) {
                const rangeBoxes = [...modal.querySelectorAll('input[type="checkbox"]')].slice(-expectedLabels.length);
                if (rangeBoxes.length === expectedLabels.length) return rangeBoxes;
            }

            if (Date.now() - start > 5000) throw new Error('范围区间复选框未就绪');
            await wait(100);
        }
    }

    function getRangeInputs() {
        const modal = modalBody();
        if (!modal) return [];
        return [...modal.querySelectorAll('input[type="text"], textarea')]
            .filter((el) => !el.disabled && !el.readOnly)
            .filter((el) => clean(el.placeholder) === '请输入');
    }

    async function fillSizeChart() {
        await openSizeChartDialog();
        await clickBaseMetrics();

        const rangeBoxes = await ensureRangeCheckboxes();
        for (const box of rangeBoxes) {
            if (!box.checked) {
                click(box);
                await wait(150);
            }
        }

        const start = Date.now();
        const expectedInputCount = CONFIG.sizeChart.length * 2;
        while (getRangeInputs().length < expectedInputCount) {
            if (Date.now() - start > 5000) throw new Error('范围输入框未出现');
            await wait(100);
        }

        const inputs = getRangeInputs();
        const values = CONFIG.sizeChart.flatMap((item) => [item.min, item.max]);
        for (let i = 0; i < values.length; i++) {
            setVal(inputs[i], values[i]);
            await wait(100);
        }

        const confirm = [...document.querySelectorAll('button, span, div')]
            .find((el) => clean(el.innerText) === '确认');
        if (!confirm) throw new Error('未找到尺码表确认按钮');
        click(confirm.closest('button') || confirm);

        const closeStart = Date.now();
        while (modalBody()) {
            if (Date.now() - closeStart > 5000) break;
            await wait(100);
        }
        await wait(300);
    }

    async function selectSensitiveNo() {
        const input = [...document.querySelectorAll('input')].find((el) => clean(el.placeholder) === '敏感属性');
        const trigger = input
            ? input.closest('[class*="ST_outerWrapper"], [class*="IPT_inputWrapper"], [class*="IPT_outerWrapper"], div')
            : [...document.querySelectorAll('div, span, input')].find((el) => {
                const txt = clean(el.innerText || el.value || el.placeholder || '');
                return txt.includes('敏感属性') && !txt.includes('说明');
            });
        if (!trigger) throw new Error('未找到敏感属性触发器');

        click(trigger);
        await wait(300);

        const popup = [...document.querySelectorAll('div, section, form, article')]
            .filter((el) => {
                const rect = el.getBoundingClientRect();
                const style = window.getComputedStyle(el);
                const text = clean(el.innerText || '');
                return style.display !== 'none'
                    && style.visibility !== 'hidden'
                    && rect.width > 0
                    && rect.height > 0
                    && text.includes('是否敏感品')
                    && text.includes('敏感属性');
            })
            .sort((a, b) => {
                const ar = a.getBoundingClientRect();
                const br = b.getBoundingClientRect();
                return (br.width * br.height) - (ar.width * ar.height);
            })[0] || document;

        const magneticLabel = [...popup.querySelectorAll('label')]
            .find((el) => clean(el.innerText) === '否')
            || [...popup.querySelectorAll('span, div')]
                .find((el) => clean(el.innerText) === '否');
        if (!magneticLabel) throw new Error('未找到“否”选项');

        const checkbox = magneticLabel.closest('label')?.querySelector('input[type="checkbox"]')
            || magneticLabel.parentElement?.querySelector?.('input[type="checkbox"]')
            || magneticLabel.previousElementSibling?.querySelector?.('input[type="checkbox"]')
            || null;

        if (checkbox) {
            if (!checkbox.checked) click(checkbox);
        } else {
            click(magneticLabel.closest('label') || magneticLabel);
        }
        await wait(300);
    }

    async function clickBatchFillInSection(sectionLabel) {
        if (sectionLabel === '敏感属性与体积重量') {
            const root = findVolumeWeightSectionRoot();
            if (!root) throw new Error('未找到“敏感属性与体积重量”区域');
            const target = findVolumeWeightBatchButton(root);
            if (!target) throw new Error('未找到体积重量批量填写按钮');
            const btn = target.closest('button') || target;
            if (typeof btn.click === 'function') btn.click();
            else click(btn);
            await wait(160);
            return;
        }

        const item = findSectionRootByText(sectionLabel) || findFormItem(sectionLabel);
        if (!item) throw new Error(`未找到“${sectionLabel}”区域`);
        const btn = [...item.querySelectorAll('button, span, div')]
            .find((el) => clean(el.innerText) === '批量填写');
        if (!btn) throw new Error(`未找到“${sectionLabel}”区域里的“批量填写”按钮`);
        const target = btn.closest('button') || btn;
        if (typeof target.click === 'function') target.click();
        else click(target);
        await wait(160);
    }

    function findSkuBatchSectionRoot() {
        return document.querySelector('.product-sku_skuTableContainer__sX1e0')
            || [...document.querySelectorAll('div, section, form, article')]
            .find((el) => {
                const text = clean(el.innerText || '').replace(/\s+/g, '');
                return text.includes('SKU信息')
                    && text.includes('批量填写')
                    && text.includes('申报价格(CNY)')
                    && text.includes('SKU分类')
                    && text.includes('建议零售价');
            }) || null;
    }

    function findSkuBatchTable() {
        return document.querySelector('.product-sku_skuTableContainer__sX1e0 table.performance-table_performanceTable__dwfgW')
            || findSkuBatchSectionRoot()?.querySelector('table.performance-table_performanceTable__dwfgW')
            || null;
    }

    function findSelectInputIn(root) {
        return root?.querySelector('input[data-testid="beast-core-select-htmlInput"]') || null;
    }

    async function pickSelectValueIn(root, label) {
        const input = findSelectInputIn(root);
        const ctrl = getCtrl(input);
        if (ctrl && pickByLabel(ctrl, label)) {
            await wait(T.SHORT);
            return true;
        }

        const trigger = root?.querySelector('[class*="ST_outerWrapper"], [data-testid="beast-core-select"]') || root;
        if (!trigger) return false;
        click(trigger);
        await wait(T.SHORT);
        return await pickOptionExact(label);
    }

    async function pickPackModeValueIn(root, label) {
        const trigger = root?.querySelector('[class*="ST_outerWrapper"], [data-testid="beast-core-select"]') || root;
        if (!trigger) return false;
        click(trigger);
        await wait(T.SHORT);

        const popup = [...document.querySelectorAll('[role="listbox"], .ST_dropdownPanel_5-120-1, .beast-select-dropdown, .ST_popupWrapper_5-120-1, [class*="ST_popup"]')]
            .filter((el) => {
                const style = window.getComputedStyle(el);
                const rect = el.getBoundingClientRect();
                return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
            })
            .reverse()
            .find((el) => clean(el.innerText || '').includes('是独立包装') || clean(el.innerText || '').includes('不是独立包装'));
        if (!popup) return false;

        const option = [...popup.querySelectorAll('[role="option"], li, .beast-select-item-option-content, .ST_item_5-120-1')]
            .find((el) => clean(el.innerText) === label || clean(el.innerText).includes(label));
        if (!option) return false;
        click(option);
        await wait(T.SHORT);
        return true;
    }

    function findPackModeRootInRow(row) {
        const direct = row.querySelector('[id*=".productSkuMultiPack.packIncludeInfo"]');
        if (direct) return direct;

        const categoryItems = [...row.querySelectorAll('.sku-category_formItem__iqG7r [data-testid="beast-core-form-item"]')];
        if (categoryItems.length >= 3) return categoryItems[2];

        return [...row.querySelectorAll('[data-testid="beast-core-form-item"]')].find((el) => {
            const text = clean(el.innerText || '');
            return text.includes('请选择独立包装') || text.includes('不是独立包装') || text.includes('独立包装');
        }) || null;
    }

    function findModelNameInRow(row) {
        const rowText = clean(row?.innerText || '');
        return CONFIG.parentSpec2.specValues.find((name) => rowText.includes(name)) || '';
    }

    function listPackageRootsInRow(row) {
        const supplierRoot = row.querySelector('[id$=".supplierPrice"]');
        const baseId = (supplierRoot?.id || '').replace(/\.supplierPrice$/, '');
        const selector = baseId
            ? `[id^="${baseId}.packageInventoryList["]`
            : '[id*=".packageInventoryList["]';
        return [...document.querySelectorAll(selector)]
            .sort((a, b) => {
                const ai = Number((a.id.match(/\[(\d+)\]/) || [])[1] || 0);
                const bi = Number((b.id.match(/\[(\d+)\]/) || [])[1] || 0);
                return ai - bi;
            });
    }

    async function clickPackageAddInRow(row) {
        const roots = listPackageRootsInRow(row);
        const anchorRoot = roots[roots.length - 1] || row;
        const packageCell = anchorRoot.closest('td') || anchorRoot.parentElement || row;
        const btn = [...packageCell.querySelectorAll('button, a, span, div')]
            .find((el) => {
                const text = clean(el.innerText || el.textContent || '');
                return text === '+ 添加' || text.includes('+ 添加') || text.includes('添加');
            })
            || [...packageCell.querySelectorAll('a[class*="BTN_outerWrapperLink"], button[class*="BTN_textPrimary"], a[class*="BTN_textPrimary"]')].at(-1)
            || null;
        if (!btn) return false;

        const target = btn.closest('button, a') || btn;
        if (typeof target.click === 'function') target.click();
        else click(target);
        await wait(260);
        return true;
    }

    async function clickConfirmIfVisible() {
        const confirm = [...document.querySelectorAll('button, [role="button"]')]
            .filter((el) => {
                const text = clean(el.innerText || el.textContent || '');
                if (text !== '确认') return false;
                const rect = el.getBoundingClientRect();
                const style = window.getComputedStyle(el);
                return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
            })
            .sort((a, b) => {
                const ar = a.getBoundingClientRect();
                const br = b.getBoundingClientRect();
                return (br.top - ar.top) || (br.left - ar.left);
            })[0];
        if (!confirm) return false;
        const target = confirm.closest('button') || confirm;
        if (typeof target.click === 'function') target.click();
        click(target);
        await wait(260);
        return true;
    }

    async function ensurePackageRootsCount(row, expectedCount) {
        let roots = listPackageRootsInRow(row);
        const addTimes = Math.max(0, expectedCount - 1);
        for (let i = 0; i < addTimes; i++) {
            const beforeCount = roots.length;
            const ok = await clickPackageAddInRow(row);
            if (!ok) break;

            const start = Date.now();
            while (Date.now() - start < 3000) {
                roots = listPackageRootsInRow(row);
                if (roots.length > beforeCount) break;
                await wait(100);
            }
            if (roots.length <= beforeCount) break;
        }
        return listPackageRootsInRow(row);
    }

    async function fillPackageInventoryInRow(row, modelName) {
        const items = CONFIG.sku.packageListByModel[modelName] || [];
        if (!items.length) return;

        const roots = await ensurePackageRootsCount(row, items.length);
        for (let i = 0; i < items.length; i++) {
            const root = roots[i];
            if (!root) continue;

            const selectWrapper = root.querySelector('[class*="ST_outerWrapper"], [data-testid="beast-core-select"]') || root;
            const selectInput = root.querySelector('input[data-testid="beast-core-select-htmlInput"]');
            let ok = false;

            if (selectWrapper) {
                click(selectWrapper);
                await wait(T.SHORT);
            }
            if (selectInput) {
                setReactValue(selectInput, items[i]);
                await wait(T.SHORT);
                ok = await pickOptionExact(items[i]);
            }
            if (!ok) {
                ok = await pickSelectValueIn(root, items[i]);
            }
            if (!ok) {
                console.warn(`[Temu助手] 未能设置包装清单项=${items[i]}`);
            }
            await clickConfirmIfVisible();
            await wait(45);

            const countInput = root.querySelector('input[data-testid="beast-core-inputNumber-htmlInput"], input[placeholder="数量"], input[placeholder="请输入"]');
            if (countInput && !countInput.disabled && !countInput.readOnly) {
                setVal(countInput, CONFIG.sku.packageItemCount);
                await wait(45);
            }
        }
    }

    async function fillSkuBatchTable() {
        const table = findSkuBatchTable();
        if (!table) return false;

        const rows = [...table.querySelectorAll('tbody tr')].filter((row) => row.querySelector('[id$=".supplierPrice"]'));
        if (!rows.length) return false;

        for (const row of rows) {
            const modelName = findModelNameInRow(row);
            const declarePrice = CONFIG.sku.declarePriceByModel[modelName] || CONFIG.sku.declarePriceByModel['套装A'];
            const singleItemCount = CONFIG.sku.singleItemCountByModel[modelName] || CONFIG.sku.singleItemCountByModel['套装A'];

            const declareRoot = row.querySelector('[id$=".supplierPrice"]');
            const declareInput = declareRoot?.querySelector('input[placeholder="请输入"]');
            if (declareInput && !declareInput.disabled && !declareInput.readOnly) {
                setVal(declareInput, declarePrice);
                await wait(45);
            }

            const skuClassRoot = row.querySelector('[id*=".productSkuMultiPack.skuClassification"]');
            if (skuClassRoot) {
                const ok = await pickSelectValueIn(skuClassRoot, CONFIG.sku.skuClassLabel);
                if (!ok) console.warn(`[Temu助手] 未能设置 SKU分类=${CONFIG.sku.skuClassLabel}`);
                await wait(100);
            }

            const countRoot = row.querySelector('[id*=".productSkuMultiPack.numberOfInfo"]');
            const countInput = countRoot?.querySelector('input[data-testid="beast-core-inputNumber-htmlInput"], input[placeholder="请输入"]');
            if (countInput && !countInput.disabled && !countInput.readOnly) {
                setVal(countInput, singleItemCount);
                await wait(45);
            }

            const packModeRoot = findPackModeRootInRow(row);
            if (packModeRoot) {
                const ok = await pickPackModeValueIn(packModeRoot, CONFIG.sku.packMode)
                    || await pickSelectValueIn(packModeRoot, CONFIG.sku.packMode);
                if (!ok) console.warn(`[Temu助手] 未能设置 包装模式=${CONFIG.sku.packMode}`);
                await wait(70);
            }

            await fillPackageInventoryInRow(row, modelName);

            const suggestRoot = row.querySelector('[id$=".suggestSalesPrice"]');
            const suggestInput = suggestRoot?.querySelector('input[data-testid="beast-core-input-htmlInput"], input[data-testid="beast-core-inputNumber-htmlInput"], input[placeholder="请输入"]');
            if (suggestInput && !suggestInput.disabled && !suggestInput.readOnly) {
                setVal(suggestInput, CONFIG.sku.suggestPrice);
                await wait(70);
            }

            const selectWrappers = suggestRoot
                ? [...suggestRoot.querySelectorAll('[class*="ST_outerWrapper"], [data-testid="beast-core-select"]')]
                : [];
            const currencyRoot = selectWrappers[selectWrappers.length - 1] || null;
            if (currencyRoot) {
                const ok = await pickSelectValueIn(currencyRoot, CONFIG.sku.currency);
                if (!ok) console.warn(`[Temu助手] 未能设置建议零售价币种=${CONFIG.sku.currency}`);
                await wait(45);
            }
        }

        return true;
    }

    async function fillSkuBlock() {
        const skuBatchFilled = await fillSkuBatchTable();
        if (skuBatchFilled) {
            const cargoEl = findInput('货号');
            if (cargoEl) {
                setVal(cargoEl, CONFIG.sku.cargoNo);
                await wait(100);
            } else {
                console.warn('[Temu助手] 未找到货号输入');
            }
            return;
        }

        if (findSkuBatchSectionRoot()) {
            throw new Error('SKU 信息批量填写表已打开,但未匹配到可填写行');
        }

        const allInputs = [...document.querySelectorAll('input, textarea')];
        const headerEl = allInputs.find((el) => el.placeholder === '申报价格' && el.readOnly);

        let rowInputs = [];
        if (headerEl) {
            const headerRow = headerEl.closest('tr, [class*="tableRow"], [class*="TableRow"]');
            const table = headerEl.closest('table, [class*="Table"]');
            if (headerRow && table) {
                const dataRow = [...table.querySelectorAll('tr, [class*="tableRow"], [class*="TableRow"]')]
                    .find((r) => r !== headerRow && !headerRow.contains(r));
                if (dataRow) {
                    rowInputs = [...dataRow.querySelectorAll(
                        'input:not([type="file"]):not([type="radio"]):not([type="checkbox"]), textarea'
                    )];
                }
            }
        }

        if (!rowInputs.length && headerEl) {
            const idx = allInputs.indexOf(headerEl);
            rowInputs = allInputs.slice(idx + 1, idx + 20)
                .filter((el) => !['file', 'radio', 'checkbox'].includes(el.type));
        }

        if (!rowInputs.length) {
            console.warn('[Temu助手] 未找到 SKU 数据行输入框');
        }

        const declareEl = rowInputs.find(
            (el) => !el.readOnly && !el.disabled
                && ['text', ''].includes(el.type)
                && !getCtrl(el)?.options?.length
        );
        if (declareEl) {
            setVal(declareEl, CONFIG.sku.declarePrice);
            await wait(100);
        } else {
            console.warn('[Temu助手] 未找到申报价格输入');
        }

        const skuTypeEl = rowInputs.find((el) => {
            const ctrl = getCtrl(el);
            return ctrl?.options?.some((o) => o.value === 1 || o.value === '1');
        });
        if (skuTypeEl) {
            pickByLabel(getCtrl(skuTypeEl), CONFIG.sku.skuClassLabel) || pickById(getCtrl(skuTypeEl), CONFIG.sku.skuTypeId);
            await wait(T.SHORT);
        } else {
            console.warn('[Temu助手] 未找到 SKU 分类选择器');
        }

        const currencyEl = rowInputs.find((el) => {
            const ctrl = getCtrl(el);
            return ctrl?.options?.length >= 50;
        });
        if (currencyEl) {
            const ctrl = getCtrl(currencyEl);
            const cnOpt = ctrl.options.find((o) => String(o.value) === CONFIG.sku.currency);
            if (cnOpt) {
                ctrl.onChange(cnOpt.value, cnOpt);
                await wait(T.SHORT);
            }
        } else {
            console.warn('[Temu助手] 未找到货币选择器');
        }

        const priceInputs = rowInputs.filter(
            (el) => !el.readOnly && !el.disabled
                && ['text', ''].includes(el.type)
                && !getCtrl(el)?.options?.length
        );
        const suggestEl = priceInputs[1] || null;
        if (suggestEl) {
            setVal(suggestEl, CONFIG.sku.suggestPrice);
            await wait(100);
        } else {
            console.warn('[Temu助手] 未找到建议零售价输入');
        }

        const cargoEl = findInput('货号');
        if (cargoEl) {
            setVal(cargoEl, CONFIG.sku.cargoNo);
            await wait(100);
        } else {
            console.warn('[Temu助手] 未找到货号输入');
        }
    }

    const STEPS = [
        {
            name: '属性下拉',
            fn: async () => {
                for (const [name, val] of CONFIG.queue) await selectDropdown(name, val);
            }
        },
        { name: '货源省份', fn: () => selectProvince() },
        { name: '父规格', fn: () => fillParentSpecs() },
        { name: '商品名称', fn: () => fillProductName() },
        { name: '等待规格生成', fn: () => wait(T.SPEC) },
        { name: '尺码表', fn: () => fillSizeChart() },
        { name: '敏感批量', fn: async () => { await clickBatchFillInSection('敏感属性与体积重量'); } },
        { name: '尺寸重量', fn: () => fillDimensions() },
        { name: '敏感属性', fn: async () => { await selectSensitiveNo(); await wait(200); } },
        { name: 'SKU 批量', fn: async () => { await clickBatchFillInSection('SKU 信息'); } },
        { name: 'SKU 填写', fn: () => fillSkuBlock() }
    ];

    async function run(btn) {
        if (btn.dataset.running === 'true') return;

        readPanelData();

        btn.dataset.running = 'true';
        const label = btn.querySelector('.temu-label');
        const orig = label.textContent;

        try {
            if (typeof window.__TEMU_UPLOAD_PREPARE__ === 'function') {
                label.textContent = '预授权图片目录';
                await window.__TEMU_UPLOAD_PREPARE__();
            }

            for (const [i, step] of STEPS.entries()) {
                label.textContent = `${i + 1}/${STEPS.length} ${step.name}`;
                await step.fn();
            }

            if (typeof window.__TEMU_SKU_RUNFILL__ === 'function') {
                label.textContent = `附加 1/2 SKU脚本`;
                await window.__TEMU_SKU_RUNFILL__();
            }

            if (typeof window.__TEMU_UPLOAD_RUN__ === 'function') {
                label.textContent = `附加 2/2 图片脚本`;
                await window.__TEMU_UPLOAD_RUN__(STATE.modelText);
            }

            btn.style.setProperty('--bg', '#4caf50');
            label.textContent = '完成 ✓';
        } catch (e) {
            console.error('[Temu助手]', e);
            btn.style.setProperty('--bg', '#f44336');
            label.textContent = '出错 ✗';
            alert(e.message || '执行失败');
        }

        setTimeout(() => {
            btn.dataset.running = 'false';
            btn.style.setProperty('--bg', 'rgba(255,87,34,.9)');
            label.textContent = orig;
        }, 2200);
    }

    function injectUI() {
        if (document.getElementById('temu-v28-panel')) return;

        const style = document.createElement('style');
        style.textContent = `
            #temu-v28-panel {
                position: fixed;
                left: 15px;
                top: 15px;
                z-index: 100000;
                width: 286px;
                background: rgba(255,255,255,.96);
                border: 1px solid rgba(0,0,0,.12);
                border-radius: 12px;
                box-shadow: 0 8px 22px rgba(0,0,0,.15);
                padding: 8px 10px 10px;
                font: 12px/1.3 sans-serif;
                color: #222;
                user-select: none;
            }
            #temu-v28-dragbar {
                width: 44px;
                height: 4px;
                margin: 0 auto 8px;
                border-radius: 999px;
                background: rgba(0,0,0,.18);
                cursor: move;
            }
            #temu-onekey-source {
                width: 100%;
                box-sizing: border-box;
                height: 30px;
                border: 1px solid #d9d9d9;
                border-radius: 8px;
                padding: 5px 10px;
                outline: none;
                font: 12px/1.1 monospace;
                color: #222;
                background: #fff;
            }
            #temu-onekey-source:focus {
                border-color: #ff5722;
                box-shadow: 0 0 0 2px rgba(255,87,34,.12);
            }
            #temu-v28-btn {
                margin-top: 8px;
                width: 100%;
                padding: 8px 12px;
                border: 0;
                border-radius: 8px;
                background: rgba(255,87,34,.95);
                color: #fff;
                font: bold 13px/1 sans-serif;
                cursor: pointer;
                user-select: none;
                transition: opacity .2s, background .3s;
            }
            #temu-v28-btn:hover { opacity: .88; }
            #temu-v28-btn[data-running="true"] { opacity: .92; cursor: wait; }
        `;
        document.head.appendChild(style);

        const panel = document.createElement('div');
        panel.id = 'temu-v28-panel';
        panel.innerHTML = `
            <div id="temu-v28-dragbar"></div>
            <input id="temu-onekey-source" type="text" placeholder="型号 商品名称" value="${STATE.modelText} ${STATE.productNameText}" />
            <button id="temu-v28-btn" type="button" data-running="false"><span class="temu-label">一键成品</span></button>
        `;

        document.body.appendChild(panel);

        const dragbar = panel.querySelector('#temu-v28-dragbar');
        let dragging = false;
        let startX = 0;
        let startY = 0;
        let startLeft = 0;
        let startTop = 0;

        dragbar.addEventListener('pointerdown', (e) => {
            dragging = true;
            panel.style.right = 'auto';
            panel.style.bottom = 'auto';
            const rect = panel.getBoundingClientRect();
            startX = e.clientX;
            startY = e.clientY;
            startLeft = rect.left;
            startTop = rect.top;
            dragbar.setPointerCapture(e.pointerId);
            e.preventDefault();
        });

        dragbar.addEventListener('pointermove', (e) => {
            if (!dragging) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            panel.style.left = `${Math.max(0, startLeft + dx)}px`;
            panel.style.top = `${Math.max(0, startTop + dy)}px`;
        });

        const stopDrag = () => { dragging = false; };
        dragbar.addEventListener('pointerup', stopDrag);
        dragbar.addEventListener('pointercancel', stopDrag);
        window.addEventListener('pointerup', stopDrag);

        const btn = panel.querySelector('#temu-v28-btn');
        btn.addEventListener('click', () => run(btn));
    }

    injectUI();
    new MutationObserver(injectUI).observe(document.body, { childList: true });

    /* ════════════════════════════════════════════════════════════════
       第二脚本逻辑保留,仅不单独注入按钮
    ════════════════════════════════════════════════════════════════ */

    (function () {
        'use strict';

        const CONFIG = {
            declaredPrice: '27',
            retailPrice: '159',
            retailCurrency: 'CNY',
            skuClass: '单品',
            maxEdge: '31',
            midEdge: '18',
            minEdge: '1',
            weight: '120',
        };

        const wait = ms => new Promise(r => setTimeout(r, ms));

        function fastClick(el) {
            if (!el) return;
            ['mousedown', 'click', 'mouseup'].forEach(n =>
                el.dispatchEvent(new MouseEvent(n, { bubbles: true, view: window }))
            );
        }

        function setReactValue(el, val) {
            if (!el) return;
            const lastValue = el.value;
            el.value = val;
            const event = new Event('input', { bubbles: true });
            const tracker = el._valueTracker;
            if (tracker) tracker.setValue(lastValue);
            el.dispatchEvent(event);
            el.dispatchEvent(new Event('change', { bubbles: true }));
        }

        const byPh = ph => {
            const inputs = Array.from(document.querySelectorAll(`input[placeholder*="${ph}"]`));
            return inputs.find(inp => {
                const wrapper = inp.closest('.beast-input-inner-wrapper');
                const hasCurrencySymbol = wrapper?.innerText.includes('¥') ||
                    wrapper?.parentElement?.innerText.includes('¥');
                return hasCurrencySymbol && !inp.closest('[class*="batch"], [class*="Batch"]');
            }) || inputs.find(inp => !inp.closest('[class*="batch"]'));
        };

        function findInputNear(labelText) {
            const labels = Array.from(document.querySelectorAll('*')).filter(el =>
                el.childElementCount === 0 && el.innerText?.trim() === labelText && !el.closest('[class*="batch"]')
            );
            for (const el of labels) {
                let p = el.parentElement;
                for (let i = 0; i < 5; i++) {
                    if (!p) break;
                    const inp = p.querySelector('input:not([readonly])');
                    if (inp) return inp;
                    p = p.parentElement;
                }
            }
            return null;
        }

        async function selectOption(trigger, optionText) {
            if (!trigger) return false;
            fastClick(trigger);
            await wait(250);
            const opts = Array.from(document.querySelectorAll('.beast-select-item-option-content, [role="option"], li'));
            const hit = opts.find(o => o.innerText?.trim() === optionText) || opts.find(o => o.innerText?.includes(optionText));
            if (hit) {
                fastClick(hit);
                await wait(100);
                return true;
            }
            return false;
        }

        async function runFill() {
            try {
                const declaredInp = byPh('申报价格');
                if (declaredInp) setReactValue(declaredInp, CONFIG.declaredPrice);

                const retailInp = byPh('建议零售价') || byPh('零售价');
                if (retailInp) setReactValue(retailInp, CONFIG.retailPrice);

                const currTrigger = Array.from(document.querySelectorAll('[class*="ST_outerWrapper"], [class*="appendCell"]'))
                    .find(el => !el.closest('[class*="batch"], [class*="Batch"]') && ['USD', 'CNY', 'JPY'].includes(el.innerText?.trim()));
                if (currTrigger) await selectOption(currTrigger, CONFIG.retailCurrency);

                const skuInp = byPh('SKU分类');
                if (skuInp) await selectOption(skuInp.closest('[class*="ST_outerWrapper"]') || skuInp, CONFIG.skuClass);

                const dims = { '最长边': CONFIG.maxEdge, '次长边': CONFIG.midEdge, '最短边': CONFIG.minEdge };
                for (const [k, v] of Object.entries(dims)) {
                    const inp = byPh(k) || findInputNear(k);
                    if (inp) setReactValue(inp, v);
                }
                const wtInp = findInputNear('g')?.parentElement?.querySelector('input') || findInputNear('重量');
                if (wtInp) setReactValue(wtInp, CONFIG.weight);
            } catch (e) {
                console.error('[Temu SKU信息自动填写]', e);
                throw e;
            }
        }

        window.__TEMU_SKU_RUNFILL__ = runFill;
    })();

})();


// ==UserScript==
// @name         Temu 商品素材图上传 3.1x
// @namespace    http://tampermonkey.net/
// @version      0.4.1
// @description  上传商品素材图和外包装图片:输入货号,自动把该货号文件夹内全部图片上传到“商品素材图 -> 素材中心 -> 本地上传”,并把最后一张图上传到外包装图片。
// @author       Assistant
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp']);
    const ROOT_KEY = 'temu_material_root_dir_v1';
    const ROOT_HINT = 'C:\\Users\\1\\Desktop\\新建文件夹 (2)\\NYZX包\\11111';

    const wait = (ms) => new Promise((r) => setTimeout(r, ms));
    const clean = (t) => (t || '').replace(/\s+/g, ' ').trim();

    function isVisible(el) {
        if (!el) return false;
        const rect = el.getBoundingClientRect();
        const style = window.getComputedStyle(el);
        return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden';
    }

    function click(el) {
        if (!el) return;
        el.scrollIntoView({ block: 'center', inline: 'center' });
        const rect = el.getBoundingClientRect();
        const shared = {
            bubbles: true,
            cancelable: true,
            view: window,
            clientX: rect.left + rect.width / 2,
            clientY: rect.top + rect.height / 2,
        };
        ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => {
            el.dispatchEvent(type.startsWith('pointer')
                ? new PointerEvent(type, { ...shared, pointerType: 'mouse' })
                : new MouseEvent(type, shared));
        });
    }

    function setFiles(input, files) {
        const dt = new DataTransfer();
        for (const file of files) dt.items.add(file);
        input.files = dt.files;
        input.dispatchEvent(new Event('change', { bubbles: true }));
    }

    function compareFileNames(a, b) {
        return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' });
    }

    async function pickImagesFromDir(dirHandle) {
        const files = [];
        for await (const [name, handle] of dirHandle.entries()) {
            if (handle.kind !== 'file') continue;
            const dot = name.lastIndexOf('.');
            const ext = dot >= 0 ? name.slice(dot).toLowerCase() : '';
            if (!IMAGE_EXTS.has(ext)) continue;
            files.push(await handle.getFile());
        }
        files.sort(compareFileNames);
        return files;
    }

    async function openDb() {
        return await new Promise((resolve, reject) => {
            const req = indexedDB.open('temu-material-root-db', 1);
            req.onupgradeneeded = () => {
                req.result.createObjectStore('kv');
            };
            req.onsuccess = () => resolve(req.result);
            req.onerror = () => reject(req.error);
        });
    }

    async function saveRootHandle(handle) {
        try {
            const db = await openDb();
            await new Promise((resolve, reject) => {
                const tx = db.transaction('kv', 'readwrite');
                tx.objectStore('kv').put(handle, ROOT_KEY);
                tx.oncomplete = () => resolve();
                tx.onerror = () => reject(tx.error);
            });
            db.close();
        } catch (e) {
            console.warn('[Temu素材图] 保存根目录句柄失败', e);
        }
    }

    async function loadRootHandle() {
        try {
            const db = await openDb();
            const handle = await new Promise((resolve, reject) => {
                const tx = db.transaction('kv', 'readonly');
                const req = tx.objectStore('kv').get(ROOT_KEY);
                req.onsuccess = () => resolve(req.result || null);
                req.onerror = () => reject(req.error);
            });
            db.close();
            if (!handle) return null;
            if (handle.queryPermission && (await handle.queryPermission({ mode: 'read' })) !== 'granted') {
                return null;
            }
            return handle;
        } catch {
            return null;
        }
    }

    async function ensureRootHandle() {
        const stored = await loadRootHandle();
        if (stored) return stored;
        const handle = await window.showDirectoryPicker({ mode: 'read', startIn: 'documents' });
        await saveRootHandle(handle);
        return handle;
    }

    async function prepareRootHandle() {
        await ensureRootHandle();
    }

    async function getSkuImages(sku) {
        if (!sku) throw new Error('请输入货号');
        const rootHandle = await ensureRootHandle();
        const skuDir = await rootHandle.getDirectoryHandle(sku, { create: false });
        const images = await pickImagesFromDir(skuDir);
        if (!images.length) throw new Error(`货号文件夹里没有图片:${sku}`);
        return images;
    }

    function findFormItem(labelText) {
        return [...document.querySelectorAll('[class*="Form_itemLabelContent"]')]
            .find((el) => {
                const t = clean(el.innerText);
                return t === labelText || t.startsWith(labelText);
            })?.closest('[class*="Form_item_"]') || null;
    }

    function findMaterialItem() {
        const exact = findFormItem('商品素材图');
        if (exact) return exact;
        return [...document.querySelectorAll('div, section, form, article')]
            .find((el) => {
                const t = clean(el.innerText);
                return t.includes('商品素材图') && (t.includes('素材中心') || t.includes('上传列表') || t.includes('本地上传'));
            }) || null;
    }

    function findCarouselItem() {
        const exact = findFormItem('商品轮播图');
        if (exact) return exact;
        return [...document.querySelectorAll('div, section, form, article')]
            .find((el) => clean(el.innerText).includes('商品轮播图')) || null;
    }

    function findOuterPackagingItem() {
        const exact = findFormItem('商品包装信息');
        if (exact) return exact;
        return [...document.querySelectorAll('div, section, form, article')]
            .find((el) => {
                const t = clean(el.innerText);
                return t.includes('外包装图片') && t.includes('批量上传');
            }) || null;
    }

    function findPreviewItemsByModel() {
        const table = document.querySelector('.product-sku_skuTableContainer__sX1e0 table.performance-table_performanceTable__dwfgW')
            || document.querySelector('table.performance-table_performanceTable__dwfgW');
        if (!table) return [];
        const rows = [...table.querySelectorAll('tbody tr')];
        return rows.map((row) => {
            const modelCell = [...row.querySelectorAll('td')]
                .map((td) => clean(td.innerText))
                .find((text) => /^套装[A-Z]$/.test(text));
            const previewItem = row.querySelector('[id$=".previewImgsI18n.common"]');
            return modelCell && previewItem ? { model: modelCell, item: previewItem } : null;
        }).filter(Boolean);
    }

    function findUploadTrigger(root) {
        if (!root) return null;
        return root.querySelector('.upload-trigger_wrapProduct__caAk7, .upload-trigger_wrap__kMsdx')
            || [...root.querySelectorAll('button, div, span, label, a')].find((el) => {
                if (!isVisible(el)) return false;
                const t = clean(el.innerText);
                return t.includes('素材中心');
            })
            || null;
    }

    function findClickableText(root, texts) {
        if (!root) return null;
        const terms = Array.isArray(texts) ? texts : [texts];
        return [...root.querySelectorAll('button, div, span, label, a')]
            .find((el) => {
                if (!isVisible(el)) return false;
                const t = clean(el.innerText);
                return terms.some((term) => t === term || t.includes(term));
            }) || null;
    }

    function findTopDialogWithText(text) {
        const dialogs = [...document.querySelectorAll('div, section, form, article')].filter((el) => {
            const t = clean(el.innerText);
            return t.includes(text) && isVisible(el);
        });
        if (!dialogs.length) return null;
        return dialogs.sort((a, b) => {
            const ar = a.getBoundingClientRect();
            const br = b.getBoundingClientRect();
            return (br.width * br.height) - (ar.width * ar.height);
        })[0];
    }

    async function waitFor(fn, timeoutMs = 10000, stepMs = 120) {
        const start = Date.now();
        while (Date.now() - start < timeoutMs) {
            const result = fn();
            if (result) return result;
            await wait(stepMs);
        }
        return null;
    }

    async function ensureMaterialCenterDialog() {
        const dialog = findTopDialogWithText('素材中心') || findTopDialogWithText('上传列表') || findTopDialogWithText('本地上传');
        if (!dialog) throw new Error('未找到素材中心弹层');
        return dialog;
    }

    async function openMaterialCenterFromGoodsMaterial() {
        const item = findCarouselItem() || findMaterialItem();
        if (!item) throw new Error('未找到商品轮播图区域');

        const trigger = findUploadTrigger(item) || findClickableText(item, ['素材中心', '进入素材中心']);
        if (!trigger) throw new Error('未找到商品轮播图入口');

        click(trigger.closest('button') || trigger);
        let dialog = await waitFor(() => ensureMaterialCenterDialog().catch(() => null), 4000);
        if (!dialog) {
            click(item);
            await wait(120);
            click(trigger.closest('button') || trigger);
            dialog = await waitFor(() => ensureMaterialCenterDialog().catch(() => null), 8000);
        }
        if (!dialog) throw new Error('商品素材图素材中心没有打开');
        return dialog;
    }

    async function handleUploadResultDialog() {
        const listBtn = await waitFor(() => {
            const candidates = [...document.querySelectorAll('button, div, span, label, a')];
            return candidates.find((el) => isVisible(el) && clean(el.innerText) === '在列表中查看')
                || candidates.find((el) => clean(el.innerText) === '在列表中查看')
                || null;
        }, 8000);
        if (listBtn) {
            click(listBtn.closest('button') || listBtn);
            await wait(600);
        }
    }

    async function waitForPreviewListViewReady(timeoutMs = 8000) {
        return await waitFor(() => {
            const dialog = findTopDialogWithText('素材中心') || findTopDialogWithText('上传列表');
            if (!dialog) return null;
            const candidates = [...dialog.querySelectorAll('button, div, span, label, a')];
            return candidates.find((el) => isVisible(el) && clean(el.innerText) === '在列表中查看')
                || candidates.find((el) => clean(el.innerText) === '在列表中查看')
                || null;
        }, timeoutMs, 120);
    }

    async function clickConfirmInMaterialCenter() {
        const waitStart = Date.now();
        let confirm = null;
        while (!confirm && Date.now() - waitStart < 6000) {
            confirm = [...document.querySelectorAll('button, [role="button"]')]
            .filter((el) => isVisible(el))
            .map((el) => ({ el, r: el.getBoundingClientRect(), text: clean(el.innerText || el.textContent || '') }))
            .filter(({ text, r }) => text === '确认' && r.width > 60 && r.height > 24)
            .sort((a, b) => b.r.top - a.r.top)
            .at(0)?.el
            || [...document.querySelectorAll('button, [role="button"]')]
                .find((el) => {
                    if (!isVisible(el)) return false;
                    const t = clean(el.innerText || el.textContent || '');
                    return t === '确认';
                }) || null;
            if (!confirm) await wait(120);
        }

        if (confirm) {
            const target = confirm.closest('button') || confirm;
            if (typeof target.click === 'function') target.click();
            else click(target);
            await wait(400);
            await waitFor(() => !findTopDialogWithText('素材中心') && !findTopDialogWithText('上传列表'), 5000, 120);
            return true;
        }
        return false;
    }

    function baseName(file) {
        return file.name.replace(/\.[^.]+$/, '');
    }

    function findMaterialCardsInDialog(dialog) {
        return [...dialog.querySelectorAll('div[class*="cardContainer"]')]
            .filter(isVisible)
            .map((card) => {
                const text = clean(card.textContent);
                const nameEl = [...card.querySelectorAll('div,span,a')].find((el) => {
                    const t = clean(el.textContent);
                    return t && !t.includes('裁剪') && !t.includes('美化') && !t.includes('翻译') && /^[A-Za-z0-9_-]+$/.test(t);
                });
                return {
                    card,
                    text,
                    name: nameEl ? clean(nameEl.textContent) : ''
                };
            })
            .filter((item) => item.name);
    }

    async function reorderMaterialCards(files) {
        const dialog = findTopDialogWithText('上传列表') || await ensureMaterialCenterDialog();
        const cards = findMaterialCardsInDialog(dialog);
        if (!cards.length) return;

        const targetNames = files.map(baseName);
        const matchedCards = targetNames
            .map((name) => cards.find((card) => card.name === name || card.text.includes(name)))
            .filter(Boolean);

        if (!matchedCards.length) return;

        for (const item of matchedCards) {
            if (item.card.className.includes('checked')) {
                click(item.card);
                await wait(120);
            }
        }

        for (const item of matchedCards) {
            click(item.card);
            await wait(150);
        }
    }

    async function waitForMaterialUploadInput(timeoutMs = 10000) {
        const start = Date.now();
        while (Date.now() - start < timeoutMs) {
            const input = [...document.querySelectorAll('input[type="file"]')].find((el) => {
                const accept = (el.getAttribute('accept') || '').toLowerCase();
                const parentText = clean(el.parentElement?.textContent || el.closest('div,section,article,form')?.textContent || '');
                return parentText.includes('本地上传') && accept.includes('.png') && accept.includes('.jpg');
            });
            if (input) return input;
            await wait(100);
        }
        return null;
    }

    async function waitForOuterPackagingUploadInput(timeoutMs = 10000) {
        const start = Date.now();
        while (Date.now() - start < timeoutMs) {
            const input = [...document.querySelectorAll('input[type="file"]')].find((el) => {
                const accept = (el.getAttribute('accept') || '').toLowerCase();
                const parentText = clean(el.parentElement?.textContent || el.closest('div,section,article,form')?.textContent || '');
                return parentText.includes('批量上传') && accept.includes('.png') && accept.includes('.jpg');
            });
            if (input) return input;
            await wait(100);
        }
        return null;
    }

    async function uploadCarouselImages(images) {
        await openMaterialCenterFromGoodsMaterial();
        const fileInput = await waitForMaterialUploadInput(10000);
        if (!fileInput) throw new Error('未找到素材中心里的本地上传输入框');

        setFiles(fileInput, images);
        await wait(1400);
        await handleUploadResultDialog();
        await wait(850);
        await reorderMaterialCards(images);
        await wait(650);
        await clickConfirmInMaterialCenter();
    }

    async function uploadOuterPackagingLastImage(images) {
        const lastImage = images[images.length - 1];
        const item = findOuterPackagingItem();
        if (!item) throw new Error('未找到外包装图片区');

        const trigger = findClickableText(item, ['批量上传']);
        if (!trigger) throw new Error('未找到外包装图片上传按钮');

        click(trigger.closest('button') || trigger);
        const fileInput = await waitForOuterPackagingUploadInput(10000);
        if (!fileInput) throw new Error('未找到外包装图片的本地上传输入框');

        setFiles(fileInput, [lastImage]);
        await wait(200);
    }

    async function uploadPreviewLastImage(images) {
        const previewItems = findPreviewItemsByModel();
        if (!previewItems.length) throw new Error('未找到预览图区域');

        const imageByModel = {
            '套装A': images[images.length - 1] || null,
            '套装B': images[images.length - 2] || images[images.length - 1] || null,
            '套装C': images[images.length - 3] || images[images.length - 2] || images[images.length - 1] || null
        };

        for (const { model, item } of previewItems) {
            const image = imageByModel[model];
            if (!image) continue;

            const trigger = findUploadTrigger(item) || findClickableText(item, ['素材中心', '预览图']);
            if (!trigger) {
                console.warn(`[Temu助手] 未找到 ${model} 的预览图素材中心入口`);
                continue;
            }

            click(trigger.closest('button') || trigger);
            const dialog = await waitFor(() => ensureMaterialCenterDialog().catch(() => null), 8000);
            if (!dialog) throw new Error(`${model} 预览图素材中心没有打开`);

            await wait(550);

            const fileInput = await waitForMaterialUploadInput(10000);
            if (!fileInput) throw new Error(`未找到 ${model} 预览图素材中心里的本地上传输入框`);

            setFiles(fileInput, [image]);
            await wait(300);
            await waitForPreviewListViewReady(8000);
            await handleUploadResultDialog();
            await wait(300);
            await clickConfirmInMaterialCenter();
            await wait(180);
        }
    }

    async function uploadMaterialAndOuterPackaging(sku) {
        const images = await getSkuImages(sku);
        await uploadCarouselImages(images);
        await uploadOuterPackagingLastImage(images);
        await wait(250);
        await uploadPreviewLastImage(images);
    }

    function readSharedSku() {
        const source = document.getElementById('temu-onekey-source');
        const raw = clean(source?.value || '');
        if (!raw) return '';
        return clean(raw.split(/\s+/)[0] || '');
    }

    window.__TEMU_UPLOAD_RUN__ = async function (sku) {
        const finalSku = clean(sku || readSharedSku());
        if (!finalSku) throw new Error('未找到可用于上传图片的货号');
        await uploadMaterialAndOuterPackaging(finalSku);
    };

    window.__TEMU_UPLOAD_PREPARE__ = prepareRootHandle;
})();