// ==UserScript== // @name Temu补报活动自动化 // @namespace https://scriptcat.org/ // @version 3.0.0 // @description 自动处理Temu卖家平台商品急需补报活动,支持关键词过滤和价格判断,全页自动补报 // @author QoderWork // @match https://agentseller.temu.com/activity/marketing-activity* // @grant none // @run-at document-idle // @inject-into page // ==/UserScript== // // v3.0.0 逻辑重写: // - 逐页处理:每页填价→判断→有"是"则立即点击"确认报名"→跳回第1页重新开始 // - 当页全部低于阈值 → 按钮变"暂不报名" → 不点击,直接翻页 // - 点击"确认报名"后自动跳回第1页,开始新一轮 // - 所有页均无需报名时自动结束 // - 价格从c7 input.value读取(活动申报价格) // - 停止按钮立即中断脚本 / 控制面板可拖动 (function () { 'use strict'; const CONFIG = { keyword: '短袖', minPrice: 45, maxRounds: 50, delay: { short: 500, medium: 1500, long: 3000, pageLoad: 2500, dialogOpen: 3000 } }; // ==================== 全局状态 ==================== let stopped = false; // 停止标志,stop按钮设为true const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); const log = (msg) => console.log(`[补报脚本] ${msg}`); const warn = (msg) => console.warn(`[补报脚本] ${msg}`); const err = (msg) => console.error(`[补报脚本] ${msg}`); const checkStop = () => { if (stopped) throw new Error('用户手动停止'); }; // ==================== DOM工具 ==================== const getCleanText = (el) => { if (!el) return ''; const clone = el.cloneNode(true); clone.querySelectorAll('style, script').forEach(s => s.remove()); return clone.textContent.trim(); }; const findByText = (tag, text, container = document) => { const elements = container.querySelectorAll(tag); return Array.from(elements).find(el => getCleanText(el).includes(text)); }; const findClickableElement = (text, container = document) => { const root = container || document; const allEls = root.querySelectorAll('div, span, a'); const matched = Array.from(allEls).filter(el => getCleanText(el).includes(text)); if (matched.length === 0) return null; matched.sort((a, b) => getCleanText(a).length - getCleanText(b).length); for (const el of matched) { if (getCleanText(el).length === 0) continue; const isParent = matched.some(o => o !== el && getCleanText(o).length < getCleanText(el).length && el.contains(o)); if (!isParent) return el; } return matched[0]; }; const extractPrice = (text) => { if (!text) return 0; const m = text.match(/¥\s*([0-9,.]+)/); if (m) { const p = parseFloat(m[1].replace(/,/g, '')); if (!isNaN(p)) return p; } let numStr = ''; for (let i = 0; i < text.length; i++) { const c = text.charCodeAt(i); if (c >= 48 && c <= 57) numStr += text[i]; else if (c === 46 && numStr.length > 0 && !numStr.includes('.')) numStr += '.'; } return parseFloat(numStr) || 0; }; // ==================== Drawer相关 ==================== const findDialog = () => { const drawer = document.querySelector('[class*="Drawer_outerWrapper"][class*="Drawer_visible"]'); if (drawer) { if (getCleanText(drawer).includes('补报')) return drawer; } const searchTerms = ['补报提醒', '条补报提醒']; for (const term of searchTerms) { const titleEl = findByText('div, span', term); if (titleEl) { let parent = titleEl; for (let i = 0; i < 15; i++) { parent = parent.parentElement; if (!parent) break; if (window.getComputedStyle(parent).position === 'fixed') return parent; } } } return null; }; const isDialogOpen = () => { const drawer = document.querySelector('[class*="Drawer_outerWrapper"][class*="Drawer_visible"]'); return drawer ? getCleanText(drawer).includes('补报') : false; }; const getDialogRows = (dialog) => { if (!dialog) return []; const tbody = dialog.querySelector('tbody'); if (tbody) return Array.from(tbody.querySelectorAll('tr')); return Array.from(dialog.querySelectorAll('tr')).filter(tr => tr.querySelector('td')); }; // ==================== 分页 ==================== const getPageInfo = (dialog) => { if (!dialog) return { current: 1, total: 1, totalItems: 0, perPage: 10 }; const text = getCleanText(dialog); const totalMatch = text.match(/共\s*([0-9]+)\s*条/) || text.match(/([0-9]+)\s*条补报/); const perPageMatch = text.match(/每页\s*([0-9]+)\s*条/); const totalItems = totalMatch ? parseInt(totalMatch[1]) : 0; const perPage = perPageMatch ? parseInt(perPageMatch[1]) : 10; let currentPage = 1; const activePage = dialog.querySelector('[class*="PGT_pagerItemActi"]'); if (activePage) { const n = parseInt(activePage.textContent.trim()); if (n > 0) currentPage = n; } let totalPages = totalItems > 0 ? Math.ceil(totalItems / perPage) : 1; dialog.querySelectorAll('[class*="PGT_pagerItem"]').forEach(btn => { const n = parseInt(btn.textContent.trim()); if (n > totalPages && n < 10000) totalPages = n; }); return { current: currentPage, total: totalPages, totalItems, perPage }; }; const clickNextPage = (dialog) => { if (!dialog) return false; const nextBtn = dialog.querySelector('[class*="PGT_next"]'); if (nextBtn) { if (nextBtn.classList.toString().includes('disabled')) { log('已是最后一页'); return false; } log('点击下一页'); nextBtn.click(); return true; } log('未找到下一页按钮'); return false; }; // ==================== 行处理 ==================== // 从行的c7获取活动申报价格(input.value) const getActivityPrice = (row) => { const cells = row.querySelectorAll('td'); if (cells.length < 8) return 0; const c7 = cells[7]; const input = c7.querySelector('input[placeholder="请输入"]'); if (input && input.value) { const p = parseFloat(input.value); if (!isNaN(p)) return p; } // 回退:从c7文本提取 return extractPrice(getCleanText(c7)); }; const processRow = async (row) => { const cells = row.querySelectorAll('td'); if (cells.length < 8) return null; const nameText = getCleanText(cells[0]); // 从c7 input.value读取活动申报价格 const activityPrice = getActivityPrice(row); const hasKeyword = nameText.includes(CONFIG.keyword); const priceOk = activityPrice >= CONFIG.minPrice; const shouldRegister = hasKeyword && priceOk; log(`"${nameText.substring(0, 25)}" | 活动价: ¥${activityPrice} | 关键词:${hasKeyword} 价格:${priceOk} → ${shouldRegister ? '报名' : '不报名'}`); // 点击"是"或"否" const radioGroups = row.querySelectorAll('[class*="RDG_outerWrapper"]'); if (radioGroups.length > 0) { const labels = radioGroups[0].querySelectorAll('label'); for (const label of labels) { const t = label.textContent.trim(); if ((shouldRegister && t === '是') || (!shouldRegister && t === '否')) { if (!label.className.includes('active')) { label.click(); await sleep(CONFIG.delay.short); } break; } } } return { name: nameText, activityPrice, shouldRegister }; }; // ==================== 按钮操作 ==================== const clickFillRefPrice = async (dialog) => { if (!dialog) return false; // 检查c7 input是否已有值 const c7Inputs = dialog.querySelectorAll('td:nth-child(8) input[placeholder="请输入"], [class*="activityPrice"] input'); let hasValues = false; for (const input of c7Inputs) { if (input.value && input.value.trim() !== '') { hasValues = true; break; } } // 也检查所有placeholder="请输入"的input if (!hasValues) { const allInputs = dialog.querySelectorAll('input[placeholder="请输入"]'); for (const input of allInputs) { if (input.value && input.value.trim() !== '') { hasValues = true; break; } } } if (hasValues) { log('价格已填入,跳过"一键填入参考价"'); return false; } // 查找并点击 — 优先找标签 const fillLink = dialog.querySelector('a'); if (fillLink && getCleanText(fillLink).includes('一键填入参考价')) { log('点击"一键填入参考价" (a标签)'); fillLink.click(); await sleep(CONFIG.delay.long); return true; } const fillBtn = findByText('span, button, a', '一键填入参考价', dialog); if (fillBtn) { log('点击"一键填入参考价"'); fillBtn.click(); await sleep(CONFIG.delay.long); return true; } warn('未找到"一键填入参考价"'); return false; }; const clickConfirmBtn = async (dialog) => { if (!dialog) return false; // 新逻辑下,只有当页有"是"时才调用此函数,按钮应为"确认报名" let btn = findByText('button', '确认报名', dialog); if (!btn) { // 安全回退:如果按钮是"暂不报名"说明逻辑有误,不点击 const skipBtn = findByText('button', '暂不报名', dialog); if (skipBtn) { warn('按钮为"暂不报名",不应点击'); return false; } warn('未找到确认按钮'); return false; } const isDisabled = (btn.className || '').includes('disabled') || (btn.className || '').includes('BTN_disabled'); if (isDisabled) { warn('按钮被禁用,尝试勾选全选'); const cb = dialog.querySelector('#dxm_checkbox') || dialog.querySelector('input[type="checkbox"]'); if (cb && !cb.checked) { cb.click(); await sleep(CONFIG.delay.medium); } } log('点击"确认报名"'); btn.click(); return true; }; const handleSecondConfirm = async () => { await sleep(CONFIG.delay.medium); const allButtons = document.querySelectorAll('button'); for (const btn of allButtons) { if (btn.textContent.trim() !== '确认') continue; const parent = btn.parentElement; if (!parent) continue; const siblings = parent.parentElement?.querySelectorAll('button') || []; if (Array.from(siblings).some(b => b.textContent.trim() === '取消')) { log('点击二次确认"确认"'); btn.click(); await sleep(CONFIG.delay.long); return true; } } // 回退:找fixed容器中的"确认" for (const btn of allButtons) { if (btn.textContent.trim() !== '确认') continue; let p = btn.parentElement; for (let i = 0; i < 10 && p; i++) { const s = window.getComputedStyle(p); if (s.position === 'fixed' || p.classList?.toString().includes('modal') || p.classList?.toString().includes('Modal')) { log('点击二次确认"确认"(回退)'); btn.click(); await sleep(CONFIG.delay.long); return true; } p = p.parentElement; } } warn('未找到二次确认按钮'); return false; }; // ==================== 主流程 ==================== const main = async () => { stopped = false; log('=== Temu补报活动自动化启动 ==='); log(`配置: 关键词="${CONFIG.keyword}", 最低价格=¥${CONFIG.minPrice}`); try { // 步骤1: 点击"去补报" log('步骤1: 查找"去补报"...'); const targetEl = findClickableElement('去补报'); if (!targetEl) throw new Error('未找到"去补报"元素'); targetEl.click(); log('已点击,等待Drawer...'); await sleep(CONFIG.delay.dialogOpen); checkStop(); // 步骤2: 等待Drawer let dialog = findDialog(); if (!dialog) { await sleep(CONFIG.delay.long); dialog = findDialog(); } if (!dialog) throw new Error('Drawer未出现'); log('Drawer已打开'); let round = 0, grandRegistered = 0; while (round < CONFIG.maxRounds) { checkStop(); round++; log(`\n===== 第${round}轮 =====`); dialog = findDialog(); if (!dialog) { await sleep(CONFIG.delay.medium); const link = findClickableElement('去补报'); if (!link) { log('任务完成!'); break; } link.click(); await sleep(CONFIG.delay.dialogOpen); dialog = findDialog(); if (!dialog) { await sleep(CONFIG.delay.long); dialog = findDialog(); } if (!dialog) { warn('无法打开Drawer'); break; } } const pageInfo = getPageInfo(dialog); log(`分页: ${pageInfo.current}/${pageInfo.total}页, 共${pageInfo.totalItems}条`); if (pageInfo.totalItems === 0) break; let roundRegistered = 0; let submitted = false; // 本轮是否点击过"确认报名" for (let page = 1; page <= pageInfo.total; page++) { checkStop(); log(`\n--- 第${page}/${pageInfo.total}页 ---`); // 每页先填入参考价 await clickFillRefPrice(dialog); await sleep(CONFIG.delay.medium); const rows = getDialogRows(dialog); if (rows.length === 0) { warn('无数据行'); break; } // 扫描当页所有行 let pageHasYes = false; const rowData = []; for (const row of rows) { const cells = row.querySelectorAll('td'); if (cells.length < 8) continue; const name = getCleanText(cells[0]); const price = getActivityPrice(row); const hasKeyword = name.includes(CONFIG.keyword); const priceOk = price >= CONFIG.minPrice; if (hasKeyword && priceOk) pageHasYes = true; rowData.push({ row, name, price, hasKeyword, priceOk, shouldRegister: hasKeyword && priceOk }); } if (!pageHasYes) { // 当页全部低于阈值 → 按钮会变成"暂不报名" → 不点击,直接翻页 log(`当页${rows.length}行全部不满足条件,不操作,翻页`); if (page < pageInfo.total) { if (!clickNextPage(dialog)) break; await sleep(CONFIG.delay.pageLoad); } continue; } // 有满足条件的 → 逐行选择是/否 for (const data of rowData) { checkStop(); const radioGroups = data.row.querySelectorAll('[class*="RDG_outerWrapper"]'); if (radioGroups.length > 0) { const labels = radioGroups[0].querySelectorAll('label'); for (const label of labels) { const t = label.textContent.trim(); if ((data.shouldRegister && t === '是') || (!data.shouldRegister && t === '否')) { if (!label.className.includes('active')) { label.click(); await sleep(CONFIG.delay.short); } break; } } } log(`"${data.name.substring(0, 20)}" | ¥${data.price} | ${data.shouldRegister ? '报名' : '不报名'}`); } const registered = rowData.filter(d => d.shouldRegister).length; roundRegistered += registered; log(`第${page}页: ${registered}行报名,点击"确认报名"`); // 当页有"是" → 按钮是"确认报名" → 点击提交 checkStop(); if (!await clickConfirmBtn(dialog)) { warn('确认报名失败'); break; } // 处理二次确认弹窗 await handleSecondConfirm(); await sleep(CONFIG.delay.long); submitted = true; grandRegistered += registered; log(`已提交${registered}条,等待页面重置...`); // 点击确认报名后会自动跳回第1页,跳出页循环开始新一轮 break; } // 如果整轮遍历所有页都没有提交(全部页都是"暂不报名"),说明全部完成 if (!submitted) { log('所有页均无需报名,任务完成'); break; } log(`第${round}轮完成: 报名${roundRegistered}条`); // 等待dialog重置或关闭 await sleep(CONFIG.delay.medium); if (!isDialogOpen()) { const remaining = findClickableElement('去补报'); if (!remaining) { log('全部完成!'); break; } const m = getCleanText(remaining).match(/([0-9]+)/); if (m && parseInt(m[1]) === 0) { log('全部完成!'); break; } } } log(`\n=== 完成 === 共报名${grandRegistered}条`); } catch (e) { if (stopped) { log('脚本已停止'); } else { err(`错误: ${e.message}`); } } }; // ==================== 控制面板(可拖动) ==================== const createControlPanel = () => { if (document.getElementById('temu-auto-reg-panel')) return; const panel = document.createElement('div'); panel.id = 'temu-auto-reg-panel'; panel.innerHTML = `
Temu 补报自动化 v3.0 (可拖动)
就绪
`; document.body.appendChild(panel); const body = document.getElementById('temu-panel-body'); const handle = document.getElementById('temu-panel-drag-handle'); const keywordInput = document.getElementById('temu-keyword-input'); const priceInput = document.getElementById('temu-price-input'); const startBtn = document.getElementById('temu-auto-reg-btn'); const stopBtn = document.getElementById('temu-auto-reg-stop'); const statusEl = document.getElementById('temu-auto-reg-status'); let running = false; // ---- 拖动功能 ---- let dragging = false, dragOffX = 0, dragOffY = 0; handle.addEventListener('mousedown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return; dragging = true; const rect = body.getBoundingClientRect(); dragOffX = e.clientX - rect.left; dragOffY = e.clientY - rect.top; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!dragging) return; const x = e.clientX - dragOffX; const y = e.clientY - dragOffY; body.style.left = x + 'px'; body.style.top = y + 'px'; body.style.right = 'auto'; }); document.addEventListener('mouseup', () => { dragging = false; }); // ---- 输入事件 ---- keywordInput.addEventListener('input', () => { CONFIG.keyword = keywordInput.value.trim(); }); priceInput.addEventListener('input', () => { const v = parseFloat(priceInput.value); if (!isNaN(v) && v >= 0) CONFIG.minPrice = v; }); // ---- 开始 ---- startBtn.addEventListener('click', async () => { if (running) return; running = true; stopped = false; startBtn.style.display = 'none'; stopBtn.style.display = 'block'; keywordInput.disabled = true; priceInput.disabled = true; statusEl.textContent = '运行中...'; statusEl.style.color = '#1890ff'; try { await main(); if (!stopped) { statusEl.textContent = '完成'; statusEl.style.color = '#52c41a'; } } catch (e) { statusEl.textContent = stopped ? '已停止' : ('出错: ' + e.message); statusEl.style.color = '#ff4d4f'; } finally { running = false; startBtn.style.display = 'block'; stopBtn.style.display = 'none'; keywordInput.disabled = false; priceInput.disabled = false; } }); // ---- 停止(立即中断) ---- stopBtn.addEventListener('click', () => { stopped = true; statusEl.textContent = '正在停止...'; statusEl.style.color = '#ff4d4f'; }); log('控制面板已创建(可拖动标题栏移动)'); }; if (document.readyState === 'complete') { createControlPanel(); } else { window.addEventListener('load', createControlPanel); } })();