一键成品面板:输入“型号 商品名称”后自动拆分填写.
// ==UserScript==
// @name Yv3.3
// @namespace http://tampermonkey.net/
// @version 28.1.486486
// @description 一键成品面板:输入“型号 商品名称”后自动拆分填写.
// @author Assistant
// @match *://agentseller.temu.com/goods/create*
// @match *://agentseller.temu.com/goods/edit*
// @grant none
// @license y
// ==/UserScript==
(function () {
'use strict';
/* ─── 配置区 ────────────────────────────────────────────────────── */
const CONFIG = {
// 1. 商品属性
queue: [ // 商品属性总配置:[字段名, 默认值];留空会自动跳过
['主题', '无'],
['材料', '涤纶'],
['商品产地', '中国大陆'],
['图案', '伪装'],
['关闭', '拉链'],
['包含的组件', '斜挎包'],
['衬里说明', '涤纶'],
['油边', '否'],
['货源产地', '广州产区'],
['护理说明', '手洗或干洗'],
['颜色', '黑色'],
['点缀功能', '无'],
['特征', '可调节肩带'],
['图案样式', '其他印花'],
['印花类型', '定位印花'],
['风格', '休闲'],
['是否印花样板', '否'],
['品牌名', ''],
['系列线', ''],
['边油', ''],
['供电方式', ''],
['图案风格', ''],
['款式', ''],
['包袋大小', ''],
['适用场景', ''],
['适用季节', ''],
['适用性别', ''],
['是否可定制', ''],
['是否防水', ''],
['颜色分类', ''],
['容量', ''],
['硬度', ''],
['开口方式', ''],
['内部结构', ''],
['肩带样式', ''],
['箱包形状', ''],
['货号属性', '']
],
categorySelection: { // 新建商品第一页的商品分类配置
keyword: '女士斜挎包', // 分类关键词;例如 女士斜挎包 / 女包套装
recommendedTexts: [ // 常用推荐真实可点击文本;按顺序优先匹配,留空则跳过
'服装、鞋靴和珠宝饰品>...>女士斜挎包',
'服装、鞋靴和珠宝饰品>...>女包套装',
'服装、鞋靴和珠宝饰品>...>女士单肩包',
'服装、鞋靴和珠宝饰品>...>女士托特包'
],
nextButtonText: '下一步' // 分类页下一步按钮文案
},
province: '广东省', // 货源省份
sensitiveAttrValue: '否', // 敏感属性默认选项;留空则跳过
// 2. 规格
parentSpec1: { // 父规格1:既支持 specValue,也支持 specValues[]
typeValueId: 1001, // 规格类型ID
specValue: '黑色', // 单值模板
specValues: [] // 多值模板,例如 ['黄色', '白色']
},
parentSpec2: { // 父规格2:既支持 specValue,也支持 specValues[]
typeValueId: 45114199, // 规格类型ID
specValue: '', // 单值模板;为空时默认回退到输入框里的型号
specValues: [] // 多值模板,例如 ['套装A', '套装B', '套装C']
},
// 3. 尺码与体积重量
dimensions: { // 体积重量总配置:支持 default/byModel,也兼容旧 marker/value 数组
default: ['', '', '', ''], // 顺序:最长边 / 次长边 / 最短边 / 重量
byModel: {
// '套装A': ['32', '20', '3', '260'],
},
markers: ['最长边', '次长边', '最短边', 'g'] // 没有按型号行时的字段顺序
},
sizeChart: [ // 尺码表区间;留空项自动跳过
{ label: '长度', min: '29', max: '31' },
{ label: '宽度', min: '20', max: '22' },
{ label: '高度', min: '13', max: '15' }
],
// 4. SKU 与包装清单
sku: { // SKU 总配置:简单模板和多套装模板都放这里,不用的留空
declarePrice: '31', // 简单模板申报价格;留空则跳过
declarePriceByModel: {
// '套装A': '',
},
declarePriceCurrency: '', // 申报价格币种占位;当前脚本未用,留作总模板配置
skuTypeId: 1, // SKU 类型ID;留空则跳过
skuTypeLabel: '', // SKU 类型名称占位;当前脚本未用,留作总模板配置
skuClassLabel: '', // 多套装模板的 SKU分类,例如 混合套装;留空则跳过
singleItemCount: '', // 简单模板单品数量;留空则跳过
singleItemCountByModel: {
// '套装A': '',
},
packMode: '', // 包装模式,例如 不是独立包装;留空则跳过
currency: 'CNY', // 建议零售价币种;留空则跳过
suggestPrice: '199', // 建议零售价;留空则跳过
suggestPriceByModel: {
// '套装A': '',
},
packageItemCount: '', // 包装清单单项数量;留空则跳过
packageList: [], // 简单模板包装清单;单 SKU 或不分套装时可直接写 ['托特包', '帽子']
packageListByModel: {
// '套装A': [],
},
packageNameOptions: [], // 包装清单候选项占位;当前脚本未用,留作总模板配置
cargoNo: '', // 货号;为空时默认回退到输入型号
customsCode: '', // 海关编码占位;当前脚本未用,留作总模板配置
barcode: '', // 条码占位;当前脚本未用,留作总模板配置
remark: '' // SKU 备注占位;当前脚本未用,留作总模板配置
},
// 5. 图片上传规则
imageUpload: { // 图片上传总配置:单入口、多入口、颜色/型号都从这里配
carousel: {
mode: 'custom', // 轮播图排序模式:sequential=按原顺序;custom=按下面 customOrderTop10 自定义
customOrderTop10: [ // 自定义轮播图前10张顺序;写法示例:1st=第1张,6st=第6张,10st=第10张;留空跳过
'6', // 图1
'1', // 图2
'3', // 图3
'4', // 图4
'2', // 图5
'5', // 图6
'7', // 图7
'', // 图8
'', // 图9
'' // 图10
]
},
preview: {
singleEntryImage: { mode: 'fromEnd', index: 1 }, // 单个预览图入口时用哪张:fromEnd=倒数,fromStart=正数;index 从 1 开始
multiEntryFallbackByOrder: [ // 多个入口但没有命中规则时,按行顺序兜底
{ mode: 'fromEnd', index: 1 },
{ mode: 'fromEnd', index: 2 },
{ mode: 'fromEnd', index: 3 }
],
rules: [ // 多入口匹配规则:可按型号、颜色或任何行里能看到的文字匹配
{ matchTexts: ['套装A'], image: { mode: 'fromEnd', index: 1 } },
{ matchTexts: ['套装B'], image: { mode: 'fromEnd', index: 2 } },
{ matchTexts: ['套装C'], image: { mode: 'fromEnd', index: 3 } },
{ matchTexts: ['黑色'], image: { mode: 'fromEnd', index: 1 } },
{ matchTexts: ['白色'], image: { mode: 'fromEnd', index: 2 } },
{ matchTexts: ['红色'], image: { mode: 'fromEnd', index: 3 } },
{ matchTexts: [''], image: { mode: 'fromEnd', index: 1 } } // 留空规则自动跳过
]
}
}
};
const STATE = {
modelText: '', // 输入框里拆出来的型号
productNameText: '' // 输入框里拆出来的商品名称
};
const EDITABLE_CONFIG = { // 统一可改配置区
UI: { // 悬浮面板 UI 尺寸
panelLeft: 10, // 面板距左侧
panelTop: 10, // 面板距顶部
panelWidth: 88, // 面板宽度
panelPadding: '8px 8px 9px', // 面板内边距
dragbarWidth: 30, // 拖拽条宽度
dragbarHeight: 4, // 拖拽条高度
inputHeight: 25, // 输入框高度
inputPadding: '5px 8px', // 输入框内边距
buttonPadding: '8px 8px', // 按钮内边距
borderRadius: 15, // 面板圆角
controlRadius: 8 // 输入框/按钮圆角
},
FEATURES: { // 工作流总开关:true=开启,false=跳过
selectCategory: true, // 商品分类选择
randomWaitBeforeCategoryNext: true, // 分类选完后,点下一步前随机等待
uploadPrepare: true, // 预授权图片目录
fillAttributes: true, // 商品属性下拉
fillProvince: true, // 货源省份
fillParentSpecs: true, // 父规格
fillProductName: true, // 商品名称
waitSpecGeneration: true, // 等待规格生成
fillSizeChart: true, // 尺码表
fillDimensions: true, // 体积重量
fillSensitiveAttr: true, // 敏感属性
clickBatchFill: true, // 批量填写按钮
fillSkuBlock: true, // SKU 填写
complianceAgreement: true, // 勾选合规声明
extraSkuFill: true, // 附加 SKU 小脚本
imageUpload: true, // 图片上传脚本
randomWaitBeforeCreate: true, // 点创建前随机等待
clickCreate: true, // 点击创建
continueCreate: true // 点击继续新建商品
},
T: { // 主流程时间常量(毫秒)
MICRO: 80, // 极短等待
SHORT: 180, // 短等待
MED: 300, // 中等待
SPEC: 1800 // 规格生成等待
},
CATEGORY_TIMING: { // 商品分类页等待
afterCategoryClickMs: 80, // 点完推荐分类后的极短缓冲
nextPagePollMs: 120, // 点下一步后轮询新页面的步长
nextPageStableMs: 500 // 新页面出现后额外稳定多久再继续
},
RANDOM_WAIT_TIMING: { // 随机等待配置(毫秒)
categoryNextMinMs: 500, // 分类选完后,点下一步前随机等待最小值
categoryNextMaxMs: 10000, // 分类选完后,点下一步前随机等待最大值
createMinMs: 1000, // 点创建前随机等待最小值
createMaxMs: 30000 // 点创建前随机等待最大值
},
SIZE_CHART_TIMING: { // 尺码表等待
openDialogMs: 240, // 打开尺码表弹层后的等待
checkboxMs: 80, // 勾选基础选项后的等待
rangeCheckboxMs: 80, // 勾选范围区间后的等待
inputMs: 80, // 每个尺码输入框填写后的等待
closeDialogMs: 400, // 确认后等待弹层关闭
pollMs: 80 // 尺码表内部轮询步长
},
SKU_TIMING: { // 规格 / 体积重量 / SKU / 包装清单等待
specPollMs: 120, // 子规格新增输入框轮询步长
dimensionRowMs: 60, // 体积重量逐行填写后的等待
dimensionFallbackMs: 100, // 体积重量兜底填写后的等待
packageAddMs: 260, // 点包装清单“添加”后的等待
packageConfirmMs: 260, // 包装清单选择后点确认的等待
packagePollMs: 100, // 包装清单新增项轮询步长
packageCountMs: 45, // 包装清单数量输入后的等待
rowDeclareMs: 45, // SKU行申报价填写后的等待
rowSkuClassMs: 70, // SKU分类选择后的等待
rowCountMs: 45, // 单品数量填写后的等待
rowPackModeMs: 70, // 包装模式选择后的等待
rowSuggestMs: 70, // 建议零售价填写后的等待
rowCurrencyMs: 45, // 建议零售价币种选择后的等待
cargoMs: 100, // 货号填写后的等待
sensitiveAfterMs: 200 // 敏感属性选择完成后的等待
},
IMAGE_TIMING: { // 图片脚本等待参数(毫秒)
listStableMs: 1000, // “在列表中查看”稳定出现多久后再点
previewStableMs: 1200, // 预览图比轮播图额外更稳的缓冲时间
pollStepMs: 40, // 轮询步长
stableStepMs: 60, // 稳定性检测步长
confirmStableMs: 420, // “确认”按钮稳定出现多久后再点
confirmPostMs: 60, // 点完“确认”后额外收尾等待
genericTimeoutMs: 10000, // 通用等待超时
dialogTimeoutMs: 8000, // 素材中心弹层等待超时
shortPollMs: 120, // 慢轮询步长
fastPollMs: 40, // 快轮询步长
listReadyTimeoutMs: 5000, // 上传列表就绪超时
outerUploadInputTimeoutMs: 1000, // 外包装上传输入框超时
retryOpenMs: 240, // 素材中心首次没打开时的重试等待
cardToggleMs: 240, // 取消已选素材卡片后的等待
cardSelectMs: 180, // 重新选择素材卡片后的等待
uploadInputPollMs: 100 // 上传 input 轮询步长
},
SKU_FILL: { // 附加 SKU 自动填写脚本默认值
declaredPrice: '27', // 申报价格
retailPrice: '159', // 建议零售价
retailCurrency: 'CNY', // 建议零售价币种
skuClass: '单品', // SKU 分类
maxEdge: '31', // 最长边
midEdge: '18', // 次长边
minEdge: '1', // 最短边
weight: '120' // 重量
},
SKU_FILL_TIMING: { // 附加 SKU 小脚本等待
selectOpenMs: 250, // 下拉选择器展开后的等待
selectDoneMs: 100, // 选中下拉项后的等待
sensitiveSwitchMs: 350 // 小脚本里敏感属性开关切换后的等待
}
};
/* ─── 时间常量 ──────────────────────────────────────────────────── */
const T = { ...(EDITABLE_CONFIG.T || {}) };
const FEATURES = { ...(EDITABLE_CONFIG.FEATURES || {}) };
const CATEGORY_TIMING = { ...(EDITABLE_CONFIG.CATEGORY_TIMING || {}) };
const RANDOM_WAIT_TIMING = { ...(EDITABLE_CONFIG.RANDOM_WAIT_TIMING || {}) };
const SIZE_CHART_TIMING = { ...(EDITABLE_CONFIG.SIZE_CHART_TIMING || {}) };
const SKU_TIMING = { ...(EDITABLE_CONFIG.SKU_TIMING || {}) };
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const clean = (t) => (t || '').replace(/\s+/g, ' ').trim();
function isBlankValue(value) {
if (value == null) return true;
if (typeof value === 'string') return clean(value) === '';
if (Array.isArray(value)) return value.length === 0 || value.every(isBlankValue);
return false;
}
function isFeatureOn(key) {
return FEATURES[key] !== false;
}
function randomIntBetween(min, max) {
const safeMin = Math.max(0, Number(min) || 0);
const safeMax = Math.max(safeMin, Number(max) || safeMin);
return Math.floor(Math.random() * (safeMax - safeMin + 1)) + safeMin;
}
async function waitRandomBetween(min, max) {
const ms = randomIntBetween(min, max);
await wait(ms);
return ms;
}
function activeItems(list) {
return (Array.isArray(list) ? list : []).filter((item) => {
if (!item || typeof item !== 'object') return false;
return Object.values(item).some((v) => !isBlankValue(v));
});
}
function normalizeSpecValues(specConfig, fallbackValue = '') {
if (!specConfig || isBlankValue(specConfig.typeValueId)) return [];
const values = [];
if (Array.isArray(specConfig.specValues)) {
values.push(...specConfig.specValues.filter((v) => !isBlankValue(v)).map((v) => clean(v)));
}
if (values.length) return values;
if (!isBlankValue(specConfig.specValue)) return [clean(specConfig.specValue)];
if (!isBlankValue(fallbackValue)) return [clean(fallbackValue)];
return [];
}
function getParentSpecTypeIds() {
return [CONFIG.parentSpec1?.typeValueId, CONFIG.parentSpec2?.typeValueId]
.filter((v) => !isBlankValue(v))
.map((v) => String(v));
}
function getParentSpec2ModelNames() {
const values = normalizeSpecValues(CONFIG.parentSpec2, STATE.modelText);
return values.length ? values : (isBlankValue(STATE.modelText) ? [] : [clean(STATE.modelText)]);
}
function getDimensionConfig() {
return CONFIG.dimensions || {};
}
function getDimensionDefaultValues() {
const dimensions = getDimensionConfig();
if (Array.isArray(dimensions)) {
const ordered = ['最长边', '次长边', '最短边', 'g'].map((marker) =>
dimensions.find((item) => clean(item?.marker) === marker)?.value || ''
);
return ordered;
}
return Array.isArray(dimensions.default) ? dimensions.default : [];
}
function getDimensionValuesByModel(modelName = '') {
const dimensions = getDimensionConfig();
if (Array.isArray(dimensions)) {
return getDimensionDefaultValues();
}
const direct = dimensions.byModel?.[modelName];
if (Array.isArray(direct) && direct.some((v) => !isBlankValue(v))) return direct;
return getDimensionDefaultValues();
}
function hasAdvancedSkuConfig() {
const sku = CONFIG.sku || {};
return !isBlankValue(sku.skuClassLabel)
|| Object.keys(sku.declarePriceByModel || {}).length > 0
|| Object.keys(sku.singleItemCountByModel || {}).length > 0
|| Object.keys(sku.packageListByModel || {}).length > 0
|| Object.keys(sku.suggestPriceByModel || {}).length > 0
|| !isBlankValue(sku.packMode);
}
function getEditableConfig() {
return EDITABLE_CONFIG;
}
function getUiConfig() {
return EDITABLE_CONFIG.UI || {};
}
window.__TEMU_GET_UNIFIED_CONFIG__ = getEditableConfig;
window.__TEMU_GET_MAIN_SCRIPT_CONFIG__ = () => CONFIG;
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 isCategoryCreatePage() {
return location.pathname.includes('/goods/create/category');
}
async function selectConfiguredCategory() {
const keyword = clean(CONFIG.categorySelection?.keyword || '');
const nextText = clean(CONFIG.categorySelection?.nextButtonText || '下一步');
if (!isCategoryCreatePage() || isBlankValue(keyword)) return false;
const categoryEl = await waitForStableElement(() => {
return [...document.querySelectorAll('div, span, p, a, button, label')]
.filter((el) => {
const text = clean(el.innerText || el.textContent || '');
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
return text === keyword
&& style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
})
.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)[0]
|| null;
}, 15000, 120, 360);
if (!categoryEl) throw new Error(`未找到商品分类:${keyword}`);
const categoryTarget = categoryEl.closest('button, a, label') || categoryEl;
if (typeof categoryTarget.click === 'function') categoryTarget.click();
else click(categoryTarget);
await wait(T.MED);
const nextBtn = await waitForStableElement(() => {
return [...document.querySelectorAll('button, [role="button"], a, span, div')]
.filter((el) => {
const text = clean(el.innerText || el.textContent || '');
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
return text === nextText
&& style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
})
.map((el) => el.closest('button, a') || el)
.at(0) || null;
}, 15000, 120, 360);
if (!nextBtn) throw new Error(`未找到按钮:${nextText}`);
if (typeof nextBtn.click === 'function') nextBtn.click();
else click(nextBtn);
await wait(T.MED);
return true;
}
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;
if (!normalizeSpecValues(CONFIG.parentSpec2).length) {
CONFIG.parentSpec2.specValue = parsed.model;
}
if (isBlankValue(CONFIG.sku.cargoNo)) {
CONFIG.sku.cargoNo = parsed.model;
}
return parsed;
}
async function selectConfiguredCategory2() {
const keyword = clean(CONFIG.categorySelection?.keyword || '');
const nextText = clean(CONFIG.categorySelection?.nextButtonText || '下一步');
if (!isCategoryCreatePage() || isBlankValue(keyword)) return false;
const categoryEl = await waitForStableElement(() => {
const isVisible = (el) => {
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
};
const title = [...document.querySelectorAll('div, span, p, label')]
.find((el) => isVisible(el) && clean(el.innerText || el.textContent || '').startsWith('常用推荐'));
const scopes = [];
if (title?.parentElement) scopes.push(title.parentElement);
if (title?.parentElement?.parentElement) scopes.push(title.parentElement.parentElement);
scopes.push(document);
for (const scope of scopes) {
const hit = [...scope.querySelectorAll('div, span, p, a, button, label')]
.filter((el) => isVisible(el) && clean(el.innerText || el.textContent || '') === keyword)
.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)[0];
if (hit) return hit;
}
return null;
}, 15000, 120, 360);
if (!categoryEl) throw new Error(`未找到商品分类:${keyword}`);
const categoryTarget = categoryEl.closest('button, a, label') || categoryEl;
if (typeof categoryTarget.click === 'function') categoryTarget.click();
else click(categoryTarget);
await wait(T.MED);
const nextBtn = await waitForStableElement(() => {
return [...document.querySelectorAll('button, [role="button"], a, span, div')]
.filter((el) => {
const text = clean(el.innerText || el.textContent || '');
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
return text === nextText
&& style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
})
.map((el) => el.closest('button, a') || el)
.at(0) || null;
}, 15000, 120, 360);
if (!nextBtn) throw new Error(`未找到按钮:${nextText}`);
if (typeof nextBtn.click === 'function') nextBtn.click();
else click(nextBtn);
await waitForStableElement(() => !isCategoryCreatePage() ? document.body : null, 20000, 120, 360);
return true;
}
/* ════════════════════════════════════════════════════════════════
原点击/输入工具
════════════════════════════════════════════════════════════════ */
function isVisibleNodeForCategory(el) {
if (!el) return false;
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function hasPostCategoryReadyMarkers() {
const bodyText = clean(document.body?.innerText || '');
if (!bodyText) return false;
return [
'商品名称',
'商品属性',
'尺码表',
'商品素材图',
'货号',
'敏感属性'
].some((text) => bodyText.includes(text));
}
async function selectConfiguredCategory3() {
const keyword = clean(CONFIG.categorySelection?.keyword || '');
const nextText = clean(CONFIG.categorySelection?.nextButtonText || '下一步');
if (!isCategoryCreatePage() || isBlankValue(keyword)) return false;
const selectedBlock = await waitForStableElement(() => {
return [...document.querySelectorAll('div, span, p, label')]
.filter((el) => isVisibleNodeForCategory(el))
.find((el) => {
const text = clean(el.innerText || el.textContent || '');
return text.includes('已选分类') && text.includes(keyword);
}) || null;
}, 1200, 80, 180);
if (!selectedBlock) {
const categoryEl = await waitForStableElement(() => {
const title = [...document.querySelectorAll('div, span, p, label')]
.find((el) => isVisibleNodeForCategory(el) && clean(el.innerText || el.textContent || '').includes('常用推荐'));
const scopes = [];
if (title?.parentElement) scopes.push(title.parentElement);
if (title?.parentElement?.parentElement) scopes.push(title.parentElement.parentElement);
scopes.push(document);
for (const scope of scopes) {
const hits = [...scope.querySelectorAll('div, span, p, a, button, label')]
.filter((el) => {
if (!isVisibleNodeForCategory(el)) return false;
const text = clean(el.innerText || el.textContent || '');
if (!text || text.includes('已选分类')) return false;
return text === keyword || text.includes(keyword);
})
.sort((a, b) => {
const at = a.getBoundingClientRect();
const bt = b.getBoundingClientRect();
return (at.top - bt.top) || (at.width - bt.width);
});
if (hits.length) return hits[0];
}
return null;
}, 15000, 100, 260);
if (!categoryEl) throw new Error(`未找到商品分类:${keyword}`);
const categoryTarget = categoryEl.closest('button, a, label') || categoryEl;
if (typeof categoryTarget.click === 'function') categoryTarget.click();
else click(categoryTarget);
await waitForStableElement(() => {
return [...document.querySelectorAll('div, span, p, label')]
.filter((el) => isVisibleNodeForCategory(el))
.find((el) => {
const text = clean(el.innerText || el.textContent || '');
return text.includes('已选分类') && text.includes(keyword);
}) || null;
}, 10000, 100, 220);
}
const nextBtn = await waitForStableElement(() => {
return [...document.querySelectorAll('button, [role="button"], a, span, div')]
.filter((el) => {
if (!isVisibleNodeForCategory(el)) return false;
const text = clean(el.innerText || el.textContent || '');
return text === nextText || text.includes(nextText);
})
.map((el) => el.closest('button, a') || el)
.at(0) || null;
}, 15000, 100, 260);
if (!nextBtn) throw new Error(`未找到按钮:${nextText}`);
const beforeUrl = location.href;
if (typeof nextBtn.click === 'function') nextBtn.click();
else click(nextBtn);
const readyBody = await waitForStableElement(() => {
const urlChanged = location.href !== beforeUrl && !isCategoryCreatePage();
if (urlChanged && hasPostCategoryReadyMarkers()) return document.body;
if (!isCategoryCreatePage() && hasPostCategoryReadyMarkers()) return document.body;
return null;
}, 30000, 120, 500);
if (!readyBody) {
throw new Error('点击下一步后,未等到商品发布页面加载完成');
}
return true;
}
function isPublishFormPageReady2() {
const path = location.pathname || '';
if (path.includes('/goods/create/category')) return false;
const categoryPanel = document.querySelector('[class*="product-category-select_panelContainer__"]')
|| document.querySelector('[class*="product-category-select_productPublishContainer__"]');
if (categoryPanel && isVisibleNodeForCategory(categoryPanel)) return false;
const productNameControl = typeof findProductNameInput === 'function' ? findProductNameInput() : null;
if (productNameControl) return true;
if (path.includes('/goods/edit') || path.includes('/goods/add')) {
const bodyText = clean(document.body?.innerText || '');
return bodyText.includes('商品名称')
|| bodyText.includes('商品属性')
|| bodyText.includes('尺码表')
|| bodyText.includes('商品素材图')
|| bodyText.includes('货号');
}
const bodyText = clean(document.body?.innerText || '');
return bodyText.includes('商品名称')
|| bodyText.includes('商品属性')
|| bodyText.includes('尺码表')
|| bodyText.includes('商品素材图')
|| bodyText.includes('货号');
}
async function selectConfiguredCategory4() {
const keyword = clean(CONFIG.categorySelection?.keyword || '');
const nextText = clean(CONFIG.categorySelection?.nextButtonText || '下一步');
const preferredTexts = (Array.isArray(CONFIG.categorySelection?.recommendedTexts) ? CONFIG.categorySelection.recommendedTexts : [])
.map((text) => clean(text))
.filter(Boolean);
if (!isCategoryCreatePage() || (!keyword && !preferredTexts.length)) return false;
const publishRoot = document.querySelector('[class*="product-category-select_productPublishContainer__"]')
|| document.querySelector('[class*="product-category-select_productCreateContainer__"]')
|| document;
const panelRoot = publishRoot.querySelector('[class*="product-category-select_panelContainer__"]') || publishRoot;
const isSelected = () => {
const text = clean(panelRoot.innerText || '');
return text.includes('已选分类') && ((keyword && text.includes(keyword)) || preferredTexts.some((item) => text.includes(item)));
};
if (!isSelected()) {
const categoryEl = await waitForStableElement(() => {
const candidates = [...panelRoot.querySelectorAll('div, span, p, a, button, label')]
.filter((el) => isVisibleNodeForCategory(el))
.filter((el) => {
const text = clean(el.innerText || el.textContent || '');
if (!text || text.includes('常用推荐') || text.includes('已选分类') || text.includes('全部分类')) return false;
const rect = el.getBoundingClientRect();
if (rect.y < 340 || rect.y > 430 || rect.width > 280) return false;
return preferredTexts.includes(text)
|| preferredTexts.some((item) => text.includes(item))
|| (keyword && text.includes(keyword));
})
.sort((a, b) => {
const at = a.getBoundingClientRect();
const bt = b.getBoundingClientRect();
return (at.y - bt.y) || (at.x - bt.x) || (at.width - bt.width);
});
return candidates[0] || null;
}, 15000, 100, 260);
if (!categoryEl) throw new Error(`未找到商品分类:${keyword || preferredTexts[0] || ''}`);
const categoryTarget = categoryEl.closest('button, a, label') || categoryEl;
if (typeof categoryTarget.click === 'function') categoryTarget.click();
else click(categoryTarget);
await wait(CATEGORY_TIMING.afterCategoryClickMs ?? T.MICRO);
}
const nextBtn = await waitForStableElement(() => {
return [...publishRoot.querySelectorAll('button, [role="button"], a, span, div')]
.filter((el) => {
if (!isVisibleNodeForCategory(el)) return false;
const text = clean(el.innerText || el.textContent || '');
return text === nextText || text.includes(nextText);
})
.map((el) => el.closest('button, a') || el)
.at(-1) || null;
}, 15000, 100, 260);
if (!nextBtn) throw new Error(`未找到按钮:${nextText}`);
if (isFeatureOn('randomWaitBeforeCategoryNext')) {
await waitRandomBetween(
RANDOM_WAIT_TIMING.categoryNextMinMs,
RANDOM_WAIT_TIMING.categoryNextMaxMs
);
}
const beforeUrl = location.href;
if (typeof nextBtn.click === 'function') nextBtn.click();
else click(nextBtn);
const readyBody = await waitForStableElement(() => {
const urlChanged = location.href !== beforeUrl;
const categoryPanelStillVisible = document.querySelector('[class*="product-category-select_panelContainer__"]')
|| document.querySelector('[class*="product-category-select_productPublishContainer__"]');
if (categoryPanelStillVisible && isVisibleNodeForCategory(categoryPanelStillVisible)) return null;
if (urlChanged && isPublishFormPageReady2()) return document.body;
if (isPublishFormPageReady2()) return document.body;
return null;
}, 30000, CATEGORY_TIMING.nextPagePollMs ?? T.SHORT, CATEGORY_TIMING.nextPageStableMs ?? T.MED);
if (!readyBody) {
throw new Error('点击下一步后,未等到商品发布页面加载完成');
}
return true;
}
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 (isBlankValue(labelText) || isBlankValue(val)) return;
if (Array.isArray(val)) {
for (const one of val.filter((x) => !isBlankValue(x))) {
await selectDropdown(labelText, one);
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() {
if (isBlankValue(CONFIG.province)) return;
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 clickAddSpec2() {
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) {
for (const el of document.querySelectorAll('[class*="Form_itemLabelContent"]')) {
const t = clean(el.innerText);
if (t === labelText || t.startsWith(labelText)) {
return el.closest('[class*="Form_item_"]');
}
}
return 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);
const ids = getParentSpecTypeIds();
return ctrl?.options?.some((o) => ids.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 container = specTypeEl.closest(
'[class*="specRow"], [class*="SpecRow"], tr, [class*="row_"], [class*="Row_"]'
) || 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: findSpecBlockRoot(specTypeEl), valueEl };
}
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 spec1Values = normalizeSpecValues(CONFIG.parentSpec1);
const spec2Values = normalizeSpecValues(CONFIG.parentSpec2, STATE.modelText);
const hasParent1 = !isBlankValue(CONFIG.parentSpec1?.typeValueId) && spec1Values.length > 0;
const hasParent2 = !isBlankValue(CONFIG.parentSpec2?.typeValueId) && spec2Values.length > 0;
if (!hasParent1 && !hasParent2) return;
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(SKU_TIMING.specPollMs ?? T.SHORT);
}
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 (hasParent1 && !specInputs.length) {
console.warn('[Temu助手] 未找到父规格1');
return;
}
if (hasParent1) {
const parent1 = await fillOneParentSpec(specInputs[0], CONFIG.parentSpec1.typeValueId, spec1Values[0]);
const root1 = findSpecificSpecTableRoot('颜色') || parent1.blockRoot;
if (spec1Values.length > 1) {
await appendChildSpecValues(spec1Values, root1);
}
}
if (!hasParent2) return;
await clickAddSpec2();
specInputs = findParentSpecTypeInputs();
if (specInputs.length < 2) {
console.warn('[Temu助手] 未找到父规格2');
return;
}
const parent2 = await fillOneParentSpec(specInputs[1], CONFIG.parentSpec2.typeValueId, spec2Values[0]);
const root2 = findSpecificSpecTableRoot('型号') || parent2.blockRoot;
if (spec2Values.length > 1) {
await appendChildSpecValues(spec2Values, root2);
}
}
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() {
if (isBlankValue(STATE.productNameText)) return;
const input = findProductNameInput();
if (!input) {
console.warn('[Temu助手] 未找到商品名称输入框');
return;
}
input.focus();
setVal(input, STATE.productNameText);
await wait(T.SHORT);
}
async function fillDimensions() {
const dimensions = getDimensionConfig();
const modelValues = getParentSpec2ModelNames();
const volumeTable = document.querySelector('.product-sku_skuTableContainer__sX1e0 table.performance-table_performanceTable__dwfgW')
|| document.querySelector('table.performance-table_performanceTable__dwfgW');
if (!Array.isArray(dimensions) && volumeTable && modelValues.length) {
const rows = [...volumeTable.querySelectorAll('tbody tr')].filter((row) => row.querySelector('input[placeholder="请输入"]'));
for (const row of rows) {
const rowText = clean(row.innerText || '');
const modelName = modelValues.find((name) => rowText.includes(name)) || '';
const rowValues = getDimensionValuesByModel(modelName);
const inputs = [...row.querySelectorAll('input[placeholder="请输入"]')]
.filter((el) => !el.disabled && !el.readOnly)
.slice(0, 4);
for (let i = 0; i < Math.min(inputs.length, rowValues.length); i++) {
if (isBlankValue(rowValues[i])) continue;
inputs[i].focus();
setVal(inputs[i], rowValues[i]);
await wait(SKU_TIMING.dimensionRowMs ?? T.MICRO);
}
}
return;
}
const fallbackItems = Array.isArray(dimensions)
? activeItems(dimensions)
: (dimensions.markers || ['最长边', '次长边', '最短边', 'g']).map((marker, index) => ({
marker,
value: getDimensionDefaultValues()[index] || ''
}));
for (const { marker, value } of fallbackItems) {
if (isBlankValue(marker) || isBlankValue(value)) continue;
const target = [...document.querySelectorAll('input, textarea')]
.filter((el) => !el.disabled && !el.readOnly && clean(el.placeholder) === '请输入')
.find((el) => clean(el.closest('div')?.innerText || '').includes(marker));
if (!target) {
console.warn(`[Temu助手] 找不到尺寸输入: ${marker}`);
continue;
}
target.focus();
setVal(target, value);
await wait(SKU_TIMING.dimensionFallbackMs ?? T.SHORT);
}
}
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(SIZE_CHART_TIMING.openDialogMs ?? T.MED);
const start = Date.now();
while (!modalBody()) {
if (Date.now() - start > 5000) throw new Error('尺码表弹层未打开');
await wait(SIZE_CHART_TIMING.pollMs ?? T.SHORT);
}
}
function getBaseCheckBoxes() {
const modal = modalBody();
if (!modal) return [];
const targets = new Set(activeItems(CONFIG.sizeChart).map((item) => `${clean(item.label)}(cm)`));
return [...modal.querySelectorAll('label[data-testid="beast-core-checkbox"]')]
.filter((label) => targets.has(clean(label.innerText)));
}
async function waitUntilBaseMetricsReady() {
const expected = activeItems(CONFIG.sizeChart).length;
if (!expected) return [];
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(SIZE_CHART_TIMING.pollMs ?? T.SHORT);
}
}
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(SIZE_CHART_TIMING.checkboxMs ?? T.SHORT);
}
}
}
async function ensureRangeCheckboxes() {
const modal = modalBody();
if (!modal) throw new Error('未找到尺码表弹层');
const labels = activeItems(CONFIG.sizeChart).map((item) => `${clean(item.label)} 范围区间`);
if (!labels.length) return [];
const start = Date.now();
while (true) {
const text = clean(modal.innerText);
if (labels.every((label) => text.includes(label))) {
const rangeBoxes = [...modal.querySelectorAll('input[type="checkbox"]')].slice(-labels.length);
if (rangeBoxes.length === labels.length) return rangeBoxes;
}
if (Date.now() - start > 5000) throw new Error('范围区间复选框未就绪');
await wait(SIZE_CHART_TIMING.pollMs ?? T.SHORT);
}
}
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() {
const charts = activeItems(CONFIG.sizeChart).filter((item) => !isBlankValue(item.label));
if (!charts.length) return;
await openSizeChartDialog();
await clickBaseMetrics();
const rangeBoxes = await ensureRangeCheckboxes();
for (const box of rangeBoxes) {
if (!box.checked) {
click(box);
await wait(SIZE_CHART_TIMING.rangeCheckboxMs ?? T.SHORT);
}
}
const start = Date.now();
const expectedCount = charts.length * 2;
while (getRangeInputs().length < expectedCount) {
if (Date.now() - start > 5000) throw new Error('范围输入框未出现');
await wait(SIZE_CHART_TIMING.pollMs ?? T.SHORT);
}
const inputs = getRangeInputs();
const values = charts.flatMap((item) => [item.min, item.max]).filter((v) => !isBlankValue(v));
for (let i = 0; i < values.length; i++) {
setVal(inputs[i], values[i]);
await wait(SIZE_CHART_TIMING.inputMs ?? T.SHORT);
}
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(SIZE_CHART_TIMING.pollMs ?? T.SHORT);
}
await wait(SIZE_CHART_TIMING.closeDialogMs ?? T.MED);
}
async function selectSensitiveNo() {
const targetLabel = clean(CONFIG.sensitiveAttrValue || '否');
if (isBlankValue(targetLabel)) return;
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(T.MED);
const noLabel = [...document.querySelectorAll('label, span, div')]
.find((el) => clean(el.innerText) === targetLabel);
if (!noLabel) throw new Error('未找到“否”选项');
click(noLabel.closest('label') || noLabel);
await wait(T.MED);
}
async function clickBatchFill() {
const start = Date.now();
while (Date.now() - start < 5000) {
const btn = [...document.querySelectorAll('button, span, div')]
.find((el) => clean(el.innerText) === '批量填写');
if (btn) {
const target = btn.closest('button') || btn;
if (typeof target.click === 'function') {
target.click();
} else {
click(target);
}
await wait(T.MED);
return true;
}
await wait(T.SHORT);
}
throw new Error('未找到“批量填写”按钮');
}
async function clickComplianceAgreement() {
const label = [...document.querySelectorAll('label[data-testid="beast-core-checkbox"], label')]
.find((el) => clean(el.innerText || el.textContent || '').includes('我已阅读并同意')
&& clean(el.innerText || el.textContent || '').includes('商品合规声明'));
if (!label) {
console.warn('[Temu助手] 未找到商品合规声明勾选框');
return;
}
const checkbox = label.querySelector('input[type="checkbox"]');
if (checkbox?.checked) return;
if (typeof label.click === 'function') label.click();
else click(label);
await wait(T.SHORT);
}
async function clickCreateButton() {
const agreementLabel = [...document.querySelectorAll('label[data-testid="beast-core-checkbox"], label')]
.find((el) => clean(el.innerText || el.textContent || '').includes('我已阅读并同意')
&& clean(el.innerText || el.textContent || '').includes('商品合规声明'));
const scopedButtons = agreementLabel?.parentElement
? [...agreementLabel.parentElement.querySelectorAll('button, [role="button"], a, span, div')]
: [];
const btn = scopedButtons
.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;
})
.map((el) => el.closest('button') || el)
.at(0)
|| [...document.querySelectorAll('button, [role="button"], span, div')]
.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;
})
.map((el) => el.closest('button') || el)
.at(0)
|| null;
if (!btn) {
console.warn('[Temu助手] 未找到创建按钮');
return;
}
if (typeof btn.click === 'function') btn.click();
else click(btn);
await wait(T.MED);
}
async function clickContinueCreateButton() {
const start = Date.now();
while (Date.now() - start < 15000) {
const btn = [...document.querySelectorAll('button, [role="button"], a, span, div')]
.filter((el) => {
const text = clean(el.innerText || el.textContent || '');
if (text !== '继续新建商品' && 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;
})
.map((el) => el.closest('button, a') || el)
.at(0) || null;
if (btn) {
if (typeof btn.click === 'function') btn.click();
else click(btn);
await wait(T.MED);
return true;
}
await wait(SKU_TIMING.specPollMs ?? T.SHORT);
}
console.warn('[Temu助手] 未找到继续新建商品按钮');
return false;
}
async function clickComplianceAgreement() {
const textTarget = '我已阅读并同意《商品合规声明》';
const label = [...document.querySelectorAll('label, div, span, p')]
.find((el) => clean(el.innerText || el.textContent || '') === textTarget);
if (!label) {
console.warn('[Temu助手] 未找到商品合规声明区域');
return;
}
const wrapper = label.closest('label, div, span')?.parentElement || label.parentElement || label;
const checkbox = wrapper.querySelector('input[type="checkbox"]')
|| label.closest('label')?.querySelector('input[type="checkbox"]')
|| null;
if (checkbox) {
if (!checkbox.checked) {
click(checkbox);
await wait(T.SHORT);
}
return;
}
const clickable = [...(wrapper || document).querySelectorAll('label, span, div, i')]
.find((el) => {
const text = clean(el.innerText || el.textContent || '');
if (text && text !== textTarget) return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
})
|| label;
click(clickable);
await wait(T.SHORT);
}
async function waitForStableElement(fn, timeoutMs = 15000, stepMs = 120, stableMs = 360) {
const start = Date.now();
let stableStart = 0;
let lastKey = '';
let lastEl = null;
while (Date.now() - start < timeoutMs) {
const el = fn();
if (!el) {
stableStart = 0;
lastKey = '';
lastEl = null;
await wait(stepMs);
continue;
}
const rect = el.getBoundingClientRect();
const key = `${clean(el.innerText || el.textContent || '')}|${Math.round(rect.left)}|${Math.round(rect.top)}|${Math.round(rect.width)}|${Math.round(rect.height)}`;
if (key !== lastKey) {
lastKey = key;
lastEl = el;
stableStart = Date.now();
} else if (Date.now() - stableStart >= stableMs) {
return lastEl;
}
await wait(stepMs);
}
return null;
}
async function clickCreateButtonStable() {
const btn = await waitForStableElement(() => {
const agreementLabel = [...document.querySelectorAll('label[data-testid="beast-core-checkbox"], label')]
.find((el) => {
const text = clean(el.innerText || el.textContent || '');
return text.includes('我已阅读并同意') && text.includes('商品合规声明');
});
const scope = agreementLabel?.parentElement || agreementLabel?.closest('div, section, article') || document;
return [...scope.querySelectorAll('button, [role="button"], a, span, div')]
.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;
})
.map((el) => el.closest('button, a') || el)
.at(0) || null;
}, 15000, 120, 360);
if (!btn) {
console.warn('[Temu助手] 未找到创建按钮');
return false;
}
if (isFeatureOn('randomWaitBeforeCreate')) {
await waitRandomBetween(
RANDOM_WAIT_TIMING.createMinMs,
RANDOM_WAIT_TIMING.createMaxMs
);
}
if (typeof btn.click === 'function') btn.click();
else click(btn);
await wait(T.MED);
return true;
}
async function clickCreateButtonStable2() {
const btn = await waitForStableElement(() => {
return [...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) || (ar.left - br.left);
})
.at(0) || null;
}, 15000, 120, 360);
if (!btn) {
console.warn('[Temu助手] 未找到创建按钮');
return false;
}
if (typeof btn.click === 'function') btn.click();
else click(btn);
await wait(T.MED);
return true;
}
async function clickContinueCreateButtonStable() {
const successDialog = await waitForStableElement(() => {
return [...document.querySelectorAll('div, section, article')]
.find((el) => {
const text = clean(el.innerText || el.textContent || '');
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0
&& text.includes('创建成功')
&& text.includes('继续新建商品')
&& text.includes('查看商品列表');
}) || null;
}, 20000, 120, 420);
if (!successDialog) {
console.warn('[Temu助手] 未找到创建成功弹层');
return false;
}
const btn = await waitForStableElement(() => {
return [...successDialog.querySelectorAll('button, [role="button"], a, span, div')]
.filter((el) => {
const text = clean(el.innerText || el.textContent || '');
if (text !== '继续新建商品' && 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;
})
.map((el) => el.closest('button, a') || el)
.at(0) || null;
}, 10000, 120, 420);
if (!btn) {
console.warn('[Temu助手] 未找到继续新建商品按钮');
return false;
}
if (typeof btn.click === 'function') btn.click();
else click(btn);
await wait(T.MED);
if (isCategoryCreatePage()) {
await selectConfiguredCategory();
} else {
const start = Date.now();
while (Date.now() - start < 20000) {
if (isCategoryCreatePage()) {
await selectConfiguredCategory();
break;
}
await wait(CATEGORY_TIMING.nextPagePollMs ?? T.SHORT);
}
}
return true;
}
async function clickContinueCreateButtonStable2() {
const successDialog = await waitForStableElement(() => {
return [...document.querySelectorAll('div, section, article')]
.find((el) => {
const text = clean(el.innerText || el.textContent || '');
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0
&& text.includes('创建成功')
&& text.includes('继续新建商品')
&& text.includes('查看商品列表');
}) || null;
}, 20000, 120, 420);
if (!successDialog) {
console.warn('[Temu助手] 未找到创建成功弹层');
return false;
}
const btn = await waitForStableElement(() => {
return [...successDialog.querySelectorAll('button, [role="button"], a, span, div')]
.filter((el) => {
const text = clean(el.innerText || el.textContent || '');
if (text !== '继续新建商品' && 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;
})
.map((el) => el.closest('button, a') || el)
.at(0) || null;
}, 10000, 120, 420);
if (!btn) {
console.warn('[Temu助手] 未找到继续新建商品按钮');
return false;
}
if (typeof btn.click === 'function') btn.click();
else click(btn);
await wait(T.MED);
return true;
}
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('批量填写');
}) || 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) {
if (isBlankValue(label)) return false;
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) {
if (isBlankValue(label)) return false;
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) => {
const text = clean(el.innerText || '');
return text.includes('是独立包装') || text.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 getParentSpec2ModelNames().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(SKU_TIMING.packageAddMs ?? T.MED);
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(SKU_TIMING.packageConfirmMs ?? T.MED);
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(SKU_TIMING.packagePollMs ?? T.SHORT);
}
if (roots.length <= beforeCount) break;
}
return listPackageRootsInRow(row);
}
async function fillPackageInventoryInRow(row, modelName) {
const items = CONFIG.sku.packageListByModel?.[modelName]
|| (Array.isArray(CONFIG.sku.packageList) ? CONFIG.sku.packageList : []);
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]);
await clickConfirmIfVisible();
const countInput = root.querySelector('input[data-testid="beast-core-inputNumber-htmlInput"], input[placeholder="数量"], input[placeholder="请输入"]');
if (countInput && !countInput.disabled && !countInput.readOnly && !isBlankValue(CONFIG.sku.packageItemCount)) {
setVal(countInput, CONFIG.sku.packageItemCount);
await wait(SKU_TIMING.packageCountMs ?? T.MICRO);
}
}
}
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.declarePrice || '';
const singleItemCount = CONFIG.sku.singleItemCountByModel?.[modelName] || CONFIG.sku.singleItemCount || '';
const suggestPrice = CONFIG.sku.suggestPriceByModel?.[modelName] || CONFIG.sku.suggestPrice || '';
const declareRoot = row.querySelector('[id$=".supplierPrice"]');
const declareInput = declareRoot?.querySelector('input[placeholder="请输入"]');
if (declareInput && !declareInput.disabled && !declareInput.readOnly && !isBlankValue(declarePrice)) {
setVal(declareInput, declarePrice);
await wait(SKU_TIMING.rowDeclareMs ?? T.MICRO);
}
const skuClassRoot = row.querySelector('[id*=".productSkuMultiPack.skuClassification"]');
if (skuClassRoot && !isBlankValue(CONFIG.sku.skuClassLabel)) {
await pickSelectValueIn(skuClassRoot, CONFIG.sku.skuClassLabel);
await wait(SKU_TIMING.rowSkuClassMs ?? T.SHORT);
}
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 && !isBlankValue(singleItemCount)) {
setVal(countInput, singleItemCount);
await wait(SKU_TIMING.rowCountMs ?? T.MICRO);
}
const packModeRoot = findPackModeRootInRow(row);
if (packModeRoot && !isBlankValue(CONFIG.sku.packMode)) {
await pickPackModeValueIn(packModeRoot, CONFIG.sku.packMode)
|| await pickSelectValueIn(packModeRoot, CONFIG.sku.packMode);
await wait(SKU_TIMING.rowPackModeMs ?? T.SHORT);
}
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 && !isBlankValue(suggestPrice)) {
setVal(suggestInput, suggestPrice);
await wait(SKU_TIMING.rowSuggestMs ?? T.SHORT);
}
const selectWrappers = suggestRoot ? [...suggestRoot.querySelectorAll('[class*="ST_outerWrapper"], [data-testid="beast-core-select"]')] : [];
const currencyRoot = selectWrappers[selectWrappers.length - 1] || null;
if (currencyRoot && !isBlankValue(CONFIG.sku.currency)) {
await pickSelectValueIn(currencyRoot, CONFIG.sku.currency);
await wait(SKU_TIMING.rowCurrencyMs ?? T.MICRO);
}
}
return true;
}
async function fillSkuBlock() {
if (isBlankValue(CONFIG.sku?.declarePrice)
&& isBlankValue(CONFIG.sku?.skuTypeId)
&& isBlankValue(CONFIG.sku?.currency)
&& isBlankValue(CONFIG.sku?.suggestPrice)
&& isBlankValue(CONFIG.sku?.cargoNo)
&& !hasAdvancedSkuConfig()) {
return;
}
if (hasAdvancedSkuConfig()) {
const skuBatchFilled = await fillSkuBatchTable();
if (skuBatchFilled) {
const cargoEl = findInput('货号');
const cargoNo = isBlankValue(CONFIG.sku.cargoNo) ? STATE.modelText : CONFIG.sku.cargoNo;
if (cargoEl && !isBlankValue(cargoNo)) {
setVal(cargoEl, cargoNo);
await wait(SKU_TIMING.cargoMs ?? T.SHORT);
}
return;
}
}
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 && !isBlankValue(CONFIG.sku.declarePrice)) {
setVal(declareEl, CONFIG.sku.declarePrice);
await wait(SKU_TIMING.cargoMs ?? T.SHORT);
} else if (!isBlankValue(CONFIG.sku.declarePrice)) {
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 && !isBlankValue(CONFIG.sku.skuTypeId)) {
pickById(getCtrl(skuTypeEl), CONFIG.sku.skuTypeId);
await wait(T.SHORT);
} else if (!isBlankValue(CONFIG.sku.skuTypeId)) {
console.warn('[Temu助手] 未找到 SKU 分类选择器');
}
const currencyEl = rowInputs.find((el) => {
const ctrl = getCtrl(el);
return ctrl?.options?.length >= 50;
});
if (currencyEl && !isBlankValue(CONFIG.sku.currency)) {
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 if (!isBlankValue(CONFIG.sku.currency)) {
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 && !isBlankValue(CONFIG.sku.suggestPrice)) {
setVal(suggestEl, CONFIG.sku.suggestPrice);
await wait(SKU_TIMING.cargoMs ?? T.SHORT);
} else if (!isBlankValue(CONFIG.sku.suggestPrice)) {
console.warn('[Temu助手] 未找到建议零售价输入');
}
const cargoEl = findInput('货号');
const cargoNo = isBlankValue(CONFIG.sku.cargoNo) ? STATE.modelText : CONFIG.sku.cargoNo;
if (cargoEl && !isBlankValue(cargoNo)) {
setVal(cargoEl, cargoNo);
await wait(SKU_TIMING.cargoMs ?? T.SHORT);
} else if (!isBlankValue(cargoNo)) {
console.warn('[Temu助手] 未找到货号输入');
}
}
const STEPS = [
{
key: 'fillAttributes',
name: '属性下拉',
fn: async () => {
for (const [name, val] of (Array.isArray(CONFIG.queue) ? CONFIG.queue : [])) {
if (isBlankValue(name) || isBlankValue(val)) continue;
await selectDropdown(name, val);
}
}
},
{ key: 'fillProvince', name: '货源省份', fn: () => selectProvince() },
{ key: 'fillParentSpecs', name: '父规格', fn: () => fillParentSpecs() },
{ key: 'fillProductName', name: '商品名称', fn: () => fillProductName() },
{ key: 'waitSpecGeneration', name: '等待规格生成', fn: () => wait(T.SPEC) },
{ key: 'fillSizeChart', name: '尺码表', fn: () => fillSizeChart() },
{ key: 'fillDimensions', name: '尺寸重量', fn: () => fillDimensions() },
{ key: 'fillSensitiveAttr', name: '敏感属性', fn: async () => { await selectSensitiveNo(); await wait(SKU_TIMING.sensitiveAfterMs ?? T.SHORT); } },
{ key: 'clickBatchFill', name: '批量填写', fn: async () => { await clickBatchFill(); } },
{ key: 'fillSkuBlock', name: 'SKU 填写', fn: () => fillSkuBlock() }
];
function getActiveSteps() {
return STEPS.filter((step) => isFeatureOn(step.key));
}
async function run(btn) {
if (btn.dataset.running === 'true') return;
btn.dataset.running = 'true';
const label = btn.querySelector('.temu-label');
const orig = label.textContent;
try {
if (isFeatureOn('selectCategory') && isCategoryCreatePage()) {
label.textContent = '选择商品分类';
await selectConfiguredCategory4();
}
readPanelData();
if (isFeatureOn('uploadPrepare') && typeof window.__TEMU_UPLOAD_PREPARE__ === 'function') {
label.textContent = '预授权图片目录';
await window.__TEMU_UPLOAD_PREPARE__();
}
const activeSteps = getActiveSteps();
for (const [i, step] of activeSteps.entries()) {
label.textContent = `${i + 1}/${activeSteps.length} ${step.name}`;
await step.fn();
}
if (isFeatureOn('extraSkuFill') && typeof window.__TEMU_SKU_RUNFILL__ === 'function') {
label.textContent = `附加 1/2 SKU脚本`;
await window.__TEMU_SKU_RUNFILL__();
}
if (isFeatureOn('imageUpload') && typeof window.__TEMU_UPLOAD_RUN__ === 'function') {
label.textContent = `附加 2/2 图片脚本`;
await window.__TEMU_UPLOAD_RUN__(STATE.modelText);
}
if (isFeatureOn('complianceAgreement')) {
label.textContent = '勾选合规声明';
await clickComplianceAgreement();
}
if (isFeatureOn('clickCreate')) {
label.textContent = '点击创建';
await clickCreateButtonStable2();
}
if (isFeatureOn('continueCreate')) {
label.textContent = '继续新建商品';
await clickContinueCreateButtonStable2();
}
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 ui = getUiConfig();
const style = document.createElement('style');
style.textContent = `
#temu-v28-panel {
position: fixed;
left: ${ui.panelLeft}px;
top: ${ui.panelTop}px;
z-index: 100000;
width: ${ui.panelWidth}px;
background: rgba(255,255,255,.96);
border: 1px solid rgba(0,0,0,.12);
border-radius: ${ui.borderRadius}px;
box-shadow: 0 8px 22px rgba(0,0,0,.15);
padding: ${ui.panelPadding};
font: 12px/1.3 sans-serif;
color: #222;
user-select: none;
}
#temu-v28-dragbar {
width: ${ui.dragbarWidth}px;
height: ${ui.dragbarHeight}px;
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: ${ui.inputHeight}px;
border: 1px solid #d9d9d9;
border-radius: ${ui.controlRadius}px;
padding: ${ui.inputPadding};
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: ${ui.buttonPadding};
border: 0;
border-radius: ${ui.controlRadius}px;
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">= V =</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 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;
const TIMING = { ...((window.__TEMU_GET_UNIFIED_CONFIG__?.().SKU_FILL_TIMING) || {}) };
fastClick(trigger);
await wait(TIMING.selectOpenMs ?? 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(TIMING.selectDoneMs ?? 100);
return true;
}
return false;
}
async function runFill() {
try {
const CONFIG = {
...((window.__TEMU_GET_UNIFIED_CONFIG__?.().SKU_FILL) || {})
};
const swLabel = Array.from(document.querySelectorAll('span')).find(el => el.innerText?.includes('敏感属性') && !el.closest('[class*="batch"]'));
if (swLabel) {
const TIMING = {
...((window.__TEMU_GET_UNIFIED_CONFIG__?.().SKU_FILL_TIMING) || {})
};
const sw = swLabel.closest('.Form_item_container, .Form_item_5-120-1')?.querySelector('.beast-switch');
if (sw && !sw.classList.contains('beast-switch-checked')) {
fastClick(sw);
await wait(TIMING.sensitiveSwitchMs ?? 350);
}
const radioNo = Array.from(document.querySelectorAll('.beast-radio-group label')).find(r => r.innerText?.trim() === '否');
if (radioNo) fastClick(radioNo);
}
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 *://agentseller.temu.com/goods/edit*
// @match *://agentseller.temu.com/goods/add*
// @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 getImageTimingConfig() {
return {
...((window.__TEMU_GET_UNIFIED_CONFIG__?.().IMAGE_TIMING) || {})
};
}
function getPreviewUploadConfig() {
return window.__TEMU_GET_MAIN_SCRIPT_CONFIG__?.().imageUpload?.preview || {};
}
function getCarouselUploadConfig() {
return window.__TEMU_GET_MAIN_SCRIPT_CONFIG__?.().imageUpload?.carousel || {};
}
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();
return true;
}
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 findPreviewItem() {
const exact = document.querySelector('[id$=".previewImgsI18n.common"]');
if (exact) return exact;
const skuPreviewWrap = document.querySelector('div[class*="sku-preview-img_wrap"]');
if (skuPreviewWrap) return skuPreviewWrap.closest('[class*="Form_item_"], td, div') || skuPreviewWrap;
return [...document.querySelectorAll('div, section, form, article')]
.find((el) => {
const t = clean(el.innerText);
return t.includes('素材中心') && t.includes('AI 制图');
}) || null;
}
function findPreviewEntries() {
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 previewItem = row.querySelector('[id$=".previewImgsI18n.common"]');
if (!previewItem) return null;
const cells = [...row.querySelectorAll('td')].map((td) => clean(td.innerText)).filter(Boolean);
return {
item: previewItem,
rowText: clean(row.innerText || ''),
cells
};
}).filter(Boolean);
}
function resolveImageByPick(images, pick) {
if (!Array.isArray(images) || !images.length || !pick) return null;
const mode = clean(pick.mode || 'fromEnd');
const rawIndex = Number(pick.index);
const index = Number.isFinite(rawIndex) && rawIndex > 0 ? Math.floor(rawIndex) : 1;
if (mode === 'fromStart') {
return images[index - 1] || null;
}
return images[images.length - index] || null;
}
function resolvePreviewRuleForEntry(entry) {
const previewConfig = getPreviewUploadConfig();
const rules = Array.isArray(previewConfig.rules) ? previewConfig.rules : [];
const haystack = [clean(entry?.rowText || ''), ...(entry?.cells || []).map(clean)].filter(Boolean);
return rules.find((rule) => {
const matchTexts = (Array.isArray(rule?.matchTexts) ? rule.matchTexts : [rule?.matchText])
.filter((text) => !isBlankValue(text))
.map((text) => clean(text));
if (!matchTexts.length) return false;
return matchTexts.some((needle) => haystack.some((text) => text.includes(needle)));
}) || null;
}
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 imageTiming = getImageTimingConfig();
timeoutMs = timeoutMs || imageTiming.genericTimeoutMs;
stepMs = stepMs || imageTiming.shortPollMs;
const start = Date.now();
while (Date.now() - start < timeoutMs) {
const result = fn();
if (result) return result;
await wait(stepMs);
}
return null;
}
async function waitForStableResult(fn, timeoutMs = 10000, stepMs = 60, stableMs = 220) {
const imageTiming = getImageTimingConfig();
timeoutMs = timeoutMs || imageTiming.genericTimeoutMs;
stepMs = stepMs || imageTiming.stableStepMs;
stableMs = stableMs || imageTiming.listStableMs;
const start = Date.now();
let stableStart = 0;
let lastKey = '';
let lastResult = null;
while (Date.now() - start < timeoutMs) {
const result = fn();
if (!result) {
stableStart = 0;
lastKey = '';
lastResult = null;
await wait(stepMs);
continue;
}
const rect = typeof result.getBoundingClientRect === 'function' ? result.getBoundingClientRect() : null;
const key = rect
? `${clean(result.innerText || result.textContent || '')}|${Math.round(rect.left)}|${Math.round(rect.top)}|${Math.round(rect.width)}|${Math.round(rect.height)}`
: clean(result.innerText || result.textContent || '');
if (key !== lastKey) {
lastKey = key;
lastResult = result;
stableStart = Date.now();
} else if (Date.now() - stableStart >= stableMs) {
return lastResult;
}
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(getImageTimingConfig().retryOpenMs ?? 120);
click(trigger.closest('button') || trigger);
dialog = await waitFor(() => ensureMaterialCenterDialog().catch(() => null), 8000);
}
if (!dialog) throw new Error('商品素材图素材中心没有打开');
return dialog;
}
async function handleUploadResultDialog(stableMs = getImageTimingConfig().listStableMs) {
const imageTiming = getImageTimingConfig();
const listBtn = await waitForStableResult(() => {
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;
}, imageTiming.dialogTimeoutMs, imageTiming.pollStepMs, stableMs);
if (listBtn) {
click(listBtn.closest('button') || listBtn);
await waitFor(() => {
const dialog = findTopDialogWithText('上传列表') || findTopDialogWithText('素材中心');
if (!dialog) return null;
const cards = dialog.querySelectorAll('div[class*="cardContainer"]');
const confirm = [...dialog.querySelectorAll('button, [role="button"]')]
.find((el) => isVisible(el) && clean(el.innerText || el.textContent || '') === '确认');
return cards.length || confirm ? dialog : null;
}, imageTiming.listReadyTimeoutMs, imageTiming.pollStepMs);
}
}
async function waitForPreviewListViewReady(timeoutMs = 8000) {
const imageTiming = getImageTimingConfig();
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 || imageTiming.dialogTimeoutMs, imageTiming.fastPollMs);
}
async function clickConfirmInMaterialCenter(stableMs = getImageTimingConfig().confirmStableMs) {
const imageTiming = getImageTimingConfig();
const confirm = await waitForStableResult(() => {
return [...document.querySelectorAll('button, [role="button"]')]
.filter((el) => isVisible(el))
.map((el) => ({
el,
r: el.getBoundingClientRect(),
text: clean(el.innerText || el.textContent || ''),
disabled: el.disabled || el.getAttribute('aria-disabled') === 'true' || el.classList.contains('disabled')
}))
.filter(({ text, r, disabled }) => text === '确认' && r.width > 60 && r.height > 24 && !disabled)
.sort((a, b) => b.r.top - a.r.top)
.at(0)?.el
|| null;
}, imageTiming.dialogTimeoutMs, imageTiming.stableStepMs, stableMs);
if (confirm) {
const target = confirm.closest('button') || confirm;
if (typeof target.click === 'function') target.click();
else click(target);
await wait(imageTiming.confirmPostMs);
await waitFor(() => !findTopDialogWithText('素材中心') && !findTopDialogWithText('上传列表'), imageTiming.listReadyTimeoutMs, imageTiming.shortPollMs);
return true;
}
return false;
}
function baseName(file) {
return file.name.replace(/\.[^.]+$/, '');
}
function parseCarouselOrderToken(token) {
const text = clean(String(token || '')).toLowerCase();
if (!text) return null;
const match = text.match(/^(\d+)(?:st|nd|rd|th)?$/);
if (!match) return null;
const index = Number(match[1]);
if (!Number.isFinite(index) || index <= 0) return null;
return index - 1;
}
function isCarouselCustomMode() {
const carouselConfig = getCarouselUploadConfig();
return clean(carouselConfig.mode || 'sequential').toLowerCase() === 'custom';
}
function getCarouselCustomIndexes() {
const carouselConfig = getCarouselUploadConfig();
const customTokens = Array.isArray(carouselConfig.customOrderTop10) ? carouselConfig.customOrderTop10 : [];
const indexes = [];
const used = new Set();
for (const token of customTokens) {
const idx = parseCarouselOrderToken(token);
if (idx == null || used.has(idx)) continue;
used.add(idx);
indexes.push(idx);
}
return indexes;
}
function buildCarouselOrderFiles(files) {
if (!isCarouselCustomMode()) return files;
const customIndexes = getCarouselCustomIndexes();
const picked = [];
const used = new Set();
for (const idx of customIndexes) {
const file = files[idx];
if (!file) continue;
const key = `${file.name}|${file.size}|${file.lastModified}`;
if (used.has(key)) continue;
used.add(key);
picked.push(file);
}
return picked.length ? picked : files;
}
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);
}
function findMatchedMaterialCardsByIndex(cards) {
return getCarouselCustomIndexes()
.map((idx) => cards[idx] || null)
.filter(Boolean);
}
function findMatchedMaterialCards(cards, files) {
const targetNames = files.map(baseName);
return targetNames
.map((name) => cards.find((card) => card.name === name || card.text.includes(name)))
.filter(Boolean);
}
async function clearSelectedMaterialCards(cards) {
const checkedCards = cards.filter((item) => item.card.className.includes('checked'));
for (const item of checkedCards) {
click(item.card);
await wait(getImageTimingConfig().cardToggleMs ?? 120);
}
}
async function selectMaterialCards(cards) {
for (const item of cards) {
click(item.card);
await wait(getImageTimingConfig().cardSelectMs ?? 150);
}
}
async function reorderMaterialCards(files) {
const dialog = await waitFor(() => {
const root = findTopDialogWithText('上传列表') || findTopDialogWithText('素材中心');
if (!root) return null;
const cards = root.querySelectorAll('div[class*="cardContainer"]');
return cards.length ? root : null;
}, getImageTimingConfig().listReadyTimeoutMs, getImageTimingConfig().shortPollMs) || await ensureMaterialCenterDialog();
let cards = findMaterialCardsInDialog(dialog);
if (!cards.length) return;
const orderedFiles = buildCarouselOrderFiles(files);
const expectedCount = isCarouselCustomMode() ? orderedFiles.length : files.length;
const settledCards = await waitFor(() => {
const currentCards = findMaterialCardsInDialog(dialog);
if (!currentCards.length) return null;
if (expectedCount > 0 && currentCards.length < expectedCount) return null;
return currentCards;
}, getImageTimingConfig().listReadyTimeoutMs, getImageTimingConfig().shortPollMs);
cards = settledCards || cards;
if (!cards.length) return;
let matchedCards = findMatchedMaterialCards(cards, orderedFiles);
if (isCarouselCustomMode() && (!matchedCards.length || matchedCards.length < orderedFiles.length)) {
matchedCards = findMatchedMaterialCardsByIndex(cards);
}
if (!matchedCards.length) return;
await clearSelectedMaterialCards(cards);
await selectMaterialCards(matchedCards);
}
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(getImageTimingConfig().uploadInputPollMs ?? 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(getImageTimingConfig().uploadInputPollMs ?? 100);
}
return null;
}
async function uploadCarouselImages(images) {
await openMaterialCenterFromGoodsMaterial();
const fileInput = await waitForMaterialUploadInput(10000);
if (!fileInput) throw new Error('未找到素材中心里的本地上传输入框');
setFiles(fileInput, images);
await handleUploadResultDialog();
await reorderMaterialCards(images);
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(getImageTimingConfig().outerUploadInputTimeoutMs);
if (!fileInput) throw new Error('未找到外包装图片的本地上传输入框');
setFiles(fileInput, [lastImage]);
}
async function uploadPreviewLastImage(images) {
const previewConfig = getPreviewUploadConfig();
const previewEntries = findPreviewEntries();
if (!previewEntries.length) {
const lastImage = images[images.length - 1];
const item = findPreviewItem();
if (!item) throw new Error('未找到预览图区域');
const trigger = findUploadTrigger(item) || findClickableText(item, ['素材中心', '预览图']);
if (!trigger) throw new Error('未找到预览图素材中心入口');
click(trigger.closest('button') || trigger);
const dialog = await waitFor(() => ensureMaterialCenterDialog().catch(() => null), getImageTimingConfig().dialogTimeoutMs, getImageTimingConfig().shortPollMs);
if (!dialog) throw new Error('预览图素材中心没有打开');
const fileInput = await waitForMaterialUploadInput(getImageTimingConfig().genericTimeoutMs);
if (!fileInput) throw new Error('未找到预览图素材中心里的本地上传输入框');
setFiles(fileInput, [lastImage]);
await handleUploadResultDialog(getImageTimingConfig().previewStableMs);
await clickConfirmInMaterialCenter(getImageTimingConfig().previewStableMs);
return;
}
if (previewEntries.length === 1) {
const onlyImage = resolveImageByPick(images, previewConfig.singleEntryImage) || images[images.length - 1];
const onlyItem = previewEntries[0].item;
const trigger = findUploadTrigger(onlyItem) || findClickableText(onlyItem, ['素材中心', '预览图']);
if (!trigger) throw new Error('未找到预览图素材中心入口');
click(trigger.closest('button') || trigger);
const dialog = await waitFor(() => ensureMaterialCenterDialog().catch(() => null), getImageTimingConfig().dialogTimeoutMs, getImageTimingConfig().shortPollMs);
if (!dialog) throw new Error('预览图素材中心没有打开');
const fileInput = await waitForMaterialUploadInput(getImageTimingConfig().genericTimeoutMs);
if (!fileInput) throw new Error('未找到预览图素材中心里的本地上传输入框');
setFiles(fileInput, [onlyImage]);
await handleUploadResultDialog(getImageTimingConfig().previewStableMs);
await clickConfirmInMaterialCenter(getImageTimingConfig().previewStableMs);
return;
}
const fallbackByOrder = Array.isArray(previewConfig.multiEntryFallbackByOrder)
? previewConfig.multiEntryFallbackByOrder
: [];
for (let i = 0; i < previewEntries.length; i++) {
const entry = previewEntries[i];
const rule = resolvePreviewRuleForEntry(entry);
const fallbackPick = fallbackByOrder[i] || previewConfig.singleEntryImage;
const image = resolveImageByPick(images, rule?.image || fallbackPick);
if (!image) continue;
const trigger = findUploadTrigger(entry.item) || findClickableText(entry.item, ['素材中心', '预览图']);
if (!trigger) {
console.warn(`[Temu助手] 未找到预览图素材中心入口:${entry.rowText}`);
continue;
}
click(trigger.closest('button') || trigger);
const dialog = await waitFor(() => ensureMaterialCenterDialog().catch(() => null), getImageTimingConfig().dialogTimeoutMs, getImageTimingConfig().shortPollMs);
if (!dialog) throw new Error(`预览图素材中心没有打开:${entry.rowText}`);
const fileInput = await waitForMaterialUploadInput(getImageTimingConfig().genericTimeoutMs);
if (!fileInput) throw new Error(`未找到预览图素材中心里的本地上传输入框:${entry.rowText}`);
setFiles(fileInput, [image]);
await handleUploadResultDialog(getImageTimingConfig().previewStableMs);
await clickConfirmInMaterialCenter(getImageTimingConfig().previewStableMs);
await waitFor(() => !findTopDialogWithText('素材中心') && !findTopDialogWithText('上传列表'), getImageTimingConfig().listReadyTimeoutMs, getImageTimingConfig().shortPollMs);
}
}
async function uploadMaterialAndOuterPackaging(sku) {
const images = await getSkuImages(sku);
await uploadCarouselImages(images);
await Promise.all([
uploadOuterPackagingLastImage(images),
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;
})();