您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Fab Helper 优化版 - 减少API请求,提高性能,增强稳定性,修复限速刷新
// ==UserScript== // @name Fab Helper (优化版) // @name:zh-CN Fab Helper (优化版) // @name:en Fab Helper (Optimized) // @namespace https://www.fab.com/ // @version 3.2.5-20250723 // @description Fab Helper 优化版 - 减少API请求,提高性能,增强稳定性,修复限速刷新 // @description:zh-CN Fab Helper 优化版 - 减少API请求,提高性能,增强稳定性,修复限速刷新 // @description:en Fab Helper Optimized - Reduced API requests, improved performance, enhanced stability, fixed rate limit refresh // @author RunKing // @match https://www.fab.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=fab.com // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_openInTab // @connect fab.com // @connect www.fab.com // @run-at document-idle // ==/UserScript== (function () { 'use strict'; // --- 模块一: 配置与常量 (Config & Constants) --- const Config = { SCRIPT_NAME: 'Fab Helper (优化版)', DB_VERSION: 3, DB_NAME: 'fab_helper_db', MAX_WORKERS: 5, // Maximum number of concurrent worker tabs MAX_CONCURRENT_WORKERS: 7, // 最大并发工作标签页数量 - 提高到7个,增加并行处理能力 WORKER_TIMEOUT: 30000, // 工作标签页超时时间,30秒 UI_CONTAINER_ID: 'fab-helper-container', UI_LOG_ID: 'fab-helper-log', DB_KEYS: { DONE: 'fab_done_v8', FAILED: 'fab_failed_v8', TODO: 'fab_todo_v1', // 新增:用于永久存储待办列表 HIDE: 'fab_hide_v8', AUTO_ADD: 'fab_autoAdd_v8', // Key for the new setting REMEMBER_POS: 'fab_rememberPos_v8', LAST_CURSOR: 'fab_lastCursor_v8', // Store only the cursor string WORKER_DONE: 'fab_worker_done_v8', // This is the ONLY key workers use to report back. APP_STATUS: 'fab_app_status_v1', // For tracking 429 rate limiting STATUS_HISTORY: 'fab_status_history_v1', // For persisting the history log AUTO_RESUME: 'fab_auto_resume_v1', // For the new auto-recovery feature IS_EXECUTING: 'fab_is_executing_v1', // For saving the "一键开刷" state AUTO_REFRESH_EMPTY: 'fab_auto_refresh_empty_v1', // 新增:无商品可见时自动刷新 // All other keys are either session-based or for main-tab persistence. }, SELECTORS: { card: 'div.fabkit-Stack-root.nTa5u2sc, div.AssetCard-root', cardLink: 'a[href*="/listings/"]', addButton: 'button[aria-label*="Add to"], button[aria-label*="添加至"], button[aria-label*="cart"]', rootElement: '#root', successBanner: 'div[class*="Toast-root"]', freeStatus: '.csZFzinF', ownedStatus: '.cUUvxo_s' }, TEXTS: { en: { hide: 'Hide Done', show: 'Show Done', sync: 'Sync State', execute: 'Start Tasks', executing: 'Executing...', stopExecute: 'Stop', added: 'Done', failed: 'Failed', todo: 'To-Do', hidden: 'Hidden', clearLog: 'Clear Log', copyLog: 'Copy Log', copied: 'Copied!', log_init: 'Assistant is online!', log_db_loaded: 'Reading archive...', log_exec_no_tasks: 'To-Do list is empty.', log_verify_success: 'Verified and added to library!', log_verify_fail: "Couldn't add. Will retry later.", log_429_error: 'Request limit hit! Taking a 15s break...', goto_page_label: 'Page:', goto_page_btn: 'Go', tab_dashboard: 'Dashboard', tab_settings: 'Settings', tab_debug: 'Debug' }, zh: { hide: '隐藏已得', show: '显示已得', sync: '同步状态', execute: '一键开刷', executing: '执行中...', stopExecute: '停止', added: '已入库', failed: '失败', todo: '待办', hidden: '已隐藏', clearLog: '清空日志', copyLog: '复制日志', copied: '已复制!', log_init: '助手已上线!', log_db_loaded: '正在读取存档...', log_exec_no_tasks: '"待办"清单是空的。', log_verify_success: '搞定!已成功入库。', log_verify_fail: '哎呀,这个没加上。稍后会自动重试!', log_429_error: '请求太快被服务器限速了!休息15秒后自动重试...', goto_page_label: '页码:', goto_page_btn: '跳转', tab_dashboard: '仪表盘', tab_settings: '设定', tab_debug: '调试' } }, // Centralized keyword sets, based STRICTLY on the rules in FAB_HELPER_RULES.md OWNED_SUCCESS_CRITERIA: { // Check for an H2 tag with the specific success text. h2Text: ['已保存在我的库中', 'Saved in My Library'], // Check for buttons/links with these texts. buttonTexts: ['在我的库中查看', 'View in My Library'], // Check for the temporary success popup (snackbar). snackbarText: ['产品已添加至您的库中', 'Product added to your library'], }, ACQUISITION_TEXT_SET: new Set(['添加到我的库', 'Add to my library']), // Kept for backward compatibility with recon logic. SAVED_TEXT_SET: new Set(['已保存在我的库中', 'Saved in My Library', '在我的库中', 'In My Library']), FREE_TEXT_SET: new Set(['免费', 'Free', '起始价格 免费']), // 添加一个实例ID,用于防止多实例运行 INSTANCE_ID: 'fab_instance_id_' + Math.random().toString(36).substring(2, 15), }; // --- 模块二: 全局状态管理 (Global State) --- const State = { db: { todo: [], // 待办任务列表 done: [], // 已完成任务列表 failed: [], // 失败任务列表 }, hideSaved: false, // 是否隐藏已保存项目 autoAddOnScroll: false, // 是否在滚动时自动添加任务 rememberScrollPosition: false, // 是否记住滚动位置 autoResumeAfter429: false, // 是否在429后自动恢复 autoRefreshEmptyPage: true, // 新增:无商品可见时自动刷新(默认开启) debugMode: false, // 是否启用调试模式 isExecuting: false, // 是否正在执行任务 isRefreshScheduled: false, // 新增:标记是否已经安排了页面刷新 isWorkerTab: false, // 是否是工作标签页 isReconning: false, // 是否正在进行API扫描 lastReconUrl: '', // 最后一次API扫描的URL totalTasks: 0, // API扫描的总任务数 completedTasks: 0, // API扫描的已完成任务数 isDispatchingTasks: false, // 新增:标记是否正在派发任务 savedCursor: null, // Holds the loaded cursor for hijacking // --- NEW: State for 429 monitoring --- appStatus: 'NORMAL', // 'NORMAL' or 'RATE_LIMITED' rateLimitStartTime: null, normalStartTime: Date.now(), successfulSearchCount: 0, statusHistory: [], // Holds the history of NORMAL/RATE_LIMITED periods autoResumeAfter429: false, // The new setting for the feature // --- 限速恢复相关状态 --- consecutiveSuccessCount: 0, // 连续成功请求计数 requiredSuccessCount: 3, // 退出限速需要的连续成功请求数 lastLimitSource: '', // 最后一次限速的来源 isCheckingRateLimit: false, // 是否正在检查限速状态 // --- End New State --- showAdvanced: false, activeWorkers: 0, runningWorkers: {}, // NEW: To track active workers for the watchdog { workerId: { task, startTime } } lastKnownHref: null, // To detect SPA navigation hiddenThisPageCount: 0, executionTotalTasks: 0, // For execution progress executionCompletedTasks: 0, // For execution progress executionFailedTasks: 0, // For execution progress watchdogTimer: null, // UI-related state UI: { container: null, logPanel: null, tabs: {}, // For tab buttons tabContents: {}, // For tab content panels progressContainer: null, progressText: null, progressBarFill: null, progressBar: null, statusTodo: null, statusDone: null, statusFailed: null, statusHidden: null, execBtn: null, hideBtn: null, syncBtn: null, statusVisible: null, debugContent: null, settingsVisible: false, historyVisible: false, historyTab: 'all', statusBarContainer: null, statusItems: {}, savedPositionDisplay: null, // 新增:保存位置显示元素的引用 // 排序选择器已移除 }, valueChangeListeners: [], sessionCompleted: new Set(), // Phase15: URLs completed this session isLogCollapsed: localStorage.getItem('fab_helper_log_collapsed') === 'true' || false, // 日志面板折叠状态 hasRunDomPart: false, observerDebounceTimer: null, isObserverRunning: false, // New flag for the robust launcher lastKnownCardCount: 0, workerTaskId: null, // 新增:当前工作标签页的任务ID // 添加排序相关的状态 sortOptions: { 'relevance': { name: '相关度', value: '-relevance' }, 'rating': { name: '评分', value: '-rating' }, 'newest': { name: '最新', value: '-created_at' }, 'oldest': { name: '最旧', value: 'created_at' }, 'price_asc': { name: '价格 (从低到高)', value: 'price' }, 'price_desc': { name: '价格 (从高到低)', value: '-price' }, 'title_asc': { name: '标题 A-Z', value: 'title' }, 'title_desc': { name: '标题 Z-A', value: '-title' } }, currentSortOption: 'title_desc', // 默认排序方式 }; // --- 模块三: 日志与工具函数 (Logger & Utilities) --- const Utils = { logger: (type, ...args) => { // 支持debug级别日志 if (type === 'debug') { // 默认不在控制台显示debug级别日志,除非启用了调试模式 if (State.debugMode) { // 调试模式下在控制台输出日志,使用console.log而不是console.debug以确保可见性 console.log(`${Config.SCRIPT_NAME} [DEBUG]`, ...args); } // 无论是否调试模式,都记录到日志面板 if (State.UI.logPanel) { const logEntry = document.createElement('div'); logEntry.style.cssText = 'padding: 2px 4px; border-bottom: 1px solid #444; font-size: 11px; color: #888;'; const timestamp = new Date().toLocaleTimeString(); logEntry.innerHTML = `<span style="color: #888;">[${timestamp}]</span> <span style="color: #8a8;">[DEBUG]</span> ${args.join(' ')}`; State.UI.logPanel.prepend(logEntry); while (State.UI.logPanel.children.length > 100) { State.UI.logPanel.removeChild(State.UI.logPanel.lastChild); } } return; } // 在工作标签页中,只记录关键日志 if (State.isWorkerTab) { if (type === 'error' || args.some(arg => typeof arg === 'string' && arg.includes('Worker'))) { console[type](`${Config.SCRIPT_NAME} [Worker]`, ...args); } return; } console[type](`${Config.SCRIPT_NAME}`, ...args); // The actual logging to screen will be handled by the UI module // to keep modules decoupled. if (State.UI.logPanel) { const logEntry = document.createElement('div'); logEntry.style.cssText = 'padding: 2px 4px; border-bottom: 1px solid #444; font-size: 11px;'; const timestamp = new Date().toLocaleTimeString(); logEntry.innerHTML = `<span style="color: #888;">[${timestamp}]</span> ${args.join(' ')}`; State.UI.logPanel.prepend(logEntry); while (State.UI.logPanel.children.length > 100) { State.UI.logPanel.removeChild(State.UI.logPanel.lastChild); } } }, getText: (key, replacements = {}) => { let text = (Config.TEXTS[State.lang]?.[key]) || (Config.TEXTS['en']?.[key]) || ''; for (const placeholder in replacements) { text = text.replace(`%${placeholder}%`, replacements[placeholder]); } return text; }, detectLanguage: () => { State.lang = window.location.href.includes('/zh-cn/') ? 'zh' : 'en'; }, waitForElement: (selector, timeout = 5000) => { return new Promise((resolve, reject) => { const interval = setInterval(() => { const element = document.querySelector(selector); if (element) { clearInterval(interval); resolve(element); } }, 100); setTimeout(() => { clearInterval(interval); reject(new Error(`Timeout waiting for selector: ${selector}`)); }, timeout); }); }, waitForButtonEnabled: (button, timeout = 5000) => { return new Promise((resolve, reject) => { const interval = setInterval(() => { if (button && !button.disabled) { clearInterval(interval); resolve(); } }, 100); setTimeout(() => { clearInterval(interval); reject(new Error('Timeout waiting for button to be enabled.')); }, timeout); }); }, // This function is now for UI display purposes only. getDisplayPageFromUrl: (url) => { if (!url) return '1'; try { const urlParams = new URLSearchParams(new URL(url).search); const cursor = urlParams.get('cursor'); if (!cursor) return '1'; // Try to decode offset-based cursors for a nice page number display. if (cursor.startsWith('bz')) { const decoded = atob(cursor); const offsetMatch = decoded.match(/o=(\d+)/); if (offsetMatch && offsetMatch[1]) { const offset = parseInt(offsetMatch[1], 10); const pageSize = 24; const pageNum = Math.round((offset / pageSize) + 1); return pageNum.toString(); } } // For timestamp-based cursors, we can't calculate a page number. return 'Cursor Mode'; } catch (e) { return '...'; } }, getCookie: (name) => { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) return parts.pop().split(';').shift(); return null; }, // Simulates a more forceful click by dispatching mouse events, which can succeed // where a simple .click() is ignored by a framework's event handling. deepClick: (element) => { if (!element) return; // A small delay to ensure the browser's event loop is clear and any framework // event listeners on the element have had a chance to attach. setTimeout(() => { const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; Utils.logger('info', `Performing deep click on element: <${element.tagName.toLowerCase()} class="${element.className}">`); // Add pointerdown for modern frameworks const pointerDownEvent = new PointerEvent('pointerdown', { view: pageWindow, bubbles: true, cancelable: true }); const mouseDownEvent = new MouseEvent('mousedown', { view: pageWindow, bubbles: true, cancelable: true }); const mouseUpEvent = new MouseEvent('mouseup', { view: pageWindow, bubbles: true, cancelable: true }); element.dispatchEvent(pointerDownEvent); element.dispatchEvent(mouseDownEvent); element.dispatchEvent(mouseUpEvent); // Also trigger the standard click for maximum compatibility. element.click(); }, 50); // 50ms delay }, cleanup: () => { if (State.watchdogTimer) { clearInterval(State.watchdogTimer); State.watchdogTimer = null; } State.valueChangeListeners.forEach(id => { try { GM_removeValueChangeListener(id); } catch (e) { /* Ignore errors */ } }); State.valueChangeListeners = []; }, // 在Utils对象中添加一个新函数来解码cursor decodeCursor: (cursor) => { if (!cursor) return '无保存位置'; try { // Base64解码 const decoded = atob(cursor); // cursor通常格式为: o=1&p=Item+Name // 或者: p=Item+Name // 我们主要提取p参数的值,它通常包含项目名称 let match; if (decoded.includes('&p=')) { match = decoded.match(/&p=([^&]+)/); } else if (decoded.startsWith('p=')) { match = decoded.match(/p=([^&]+)/); } if (match && match[1]) { // 解码URI组件并替换+为空格 const itemName = decodeURIComponent(match[1].replace(/\+/g, ' ')); return `位置: "${itemName}"`; } return `位置: (已保存,但无法读取名称)`; } catch (e) { Utils.logger('error', `Cursor解码失败: ${e.message}`); return '位置: (格式无法解析)'; } }, }; // --- DOM Creation Helpers (moved outside for broader scope) --- // 移除createOwnedElement函数,不再手动添加"已保存在我的库中"标记 const createFreeElement = () => { const freeContainer = document.createElement('div'); freeContainer.className = 'fabkit-Stack-root fabkit-Stack--align_center fabkit-scale--gapX-spacing-2 fabkit-scale--gapY-spacing-2 csZFzinF'; const innerStack = document.createElement('div'); innerStack.className = 'fabkit-Stack-root fabkit-scale--gapX-spacing-1 fabkit-scale--gapY-spacing-1 J9vFXlBh'; const freeText = document.createElement('div'); freeText.className = 'fabkit-Typography-root fabkit-Typography--align-start fabkit-Typography--intent-primary fabkit-Text--sm fabkit-Text--regular'; freeText.textContent = '免费'; innerStack.appendChild(freeText); freeContainer.appendChild(innerStack); return freeContainer; }; // --- 新增: 数据缓存系统 --- const DataCache = { // 商品数据缓存 - 键为商品ID,值为商品数据 listings: new Map(), // 拥有状态缓存 - 键为商品ID,值为拥有状态对象 ownedStatus: new Map(), // 价格缓存 - 键为报价ID,值为价格信息对象 prices: new Map(), // 等待网页原生请求更新的UID列表 waitingList: new Set(), // 缓存时间戳 - 用于判断缓存是否过期 timestamps: { listings: new Map(), ownedStatus: new Map(), prices: new Map() }, // 缓存有效期(毫秒) TTL: 5 * 60 * 1000, // 5分钟 // 检查缓存是否有效 isValid: function(type, key) { const timestamp = this.timestamps[type].get(key); return timestamp && (Date.now() - timestamp < this.TTL); }, // 保存商品数据到缓存 saveListings: function(items) { if (!Array.isArray(items)) return; const now = Date.now(); items.forEach(item => { if (item && item.uid) { this.listings.set(item.uid, item); this.timestamps.listings.set(item.uid, now); } }); }, // 添加到等待列表 addToWaitingList: function(uids) { if (!uids || !Array.isArray(uids)) return; uids.forEach(uid => this.waitingList.add(uid)); Utils.logger('debug', `[Cache] 添加 ${uids.length} 个商品ID到等待列表,当前等待列表大小: ${this.waitingList.size}`); }, // 检查并从等待列表中移除 checkWaitingList: function() { if (this.waitingList.size === 0) return; // 检查等待列表中的UID是否已经有了拥有状态 let removedCount = 0; for (const uid of this.waitingList) { if (this.ownedStatus.has(uid)) { this.waitingList.delete(uid); removedCount++; } } if (removedCount > 0) { Utils.logger('info', `[Cache] 从等待列表中移除了 ${removedCount} 个已更新的商品ID,剩余: ${this.waitingList.size}`); } }, // 保存拥有状态到缓存 saveOwnedStatus: function(states) { if (!Array.isArray(states)) return; const now = Date.now(); states.forEach(state => { if (state && state.uid) { this.ownedStatus.set(state.uid, { acquired: !!state.acquired, lastUpdatedAt: state.lastUpdatedAt || new Date().toISOString(), uid: state.uid }); this.timestamps.ownedStatus.set(state.uid, now); // 如果在等待列表中,从等待列表移除 if (this.waitingList.has(state.uid)) { this.waitingList.delete(state.uid); } } }); // 如果有更新,检查等待列表 if (states.length > 0) { this.checkWaitingList(); } }, // 保存价格信息到缓存 savePrices: function(offers) { if (!Array.isArray(offers)) return; const now = Date.now(); offers.forEach(offer => { if (offer && offer.offerId) { this.prices.set(offer.offerId, { offerId: offer.offerId, price: offer.price || 0, currencyCode: offer.currencyCode || 'USD' }); this.timestamps.prices.set(offer.offerId, now); } }); }, // 获取商品数据,如果缓存有效则使用缓存 getListings: function(uids) { const result = []; const missing = []; uids.forEach(uid => { if (this.isValid('listings', uid)) { result.push(this.listings.get(uid)); } else { missing.push(uid); } }); return { result, missing }; }, // 获取拥有状态,如果缓存有效则使用缓存 getOwnedStatus: function(uids) { const result = []; const missing = []; uids.forEach(uid => { if (this.isValid('ownedStatus', uid)) { result.push(this.ownedStatus.get(uid)); } else { missing.push(uid); } }); return { result, missing }; }, // 获取价格信息,如果缓存有效则使用缓存 getPrices: function(offerIds) { const result = []; const missing = []; offerIds.forEach(offerId => { if (this.isValid('prices', offerId)) { result.push(this.prices.get(offerId)); } else { missing.push(offerId); } }); return { result, missing }; }, // 清理过期缓存 cleanupExpired: function() { try { const now = Date.now(); const cacheTypes = ['listings', 'ownedStatus', 'prices']; // 统一清理所有类型的缓存 for (const type of cacheTypes) { for (const [key, timestamp] of this.timestamps[type].entries()) { if (now - timestamp > this.TTL) { this[type].delete(key); this.timestamps[type].delete(key); } } } if (State.debugMode) { Utils.logger('debug', `[Cache] 清理完成,当前缓存大小: 商品=${this.listings.size}, 拥有状态=${this.ownedStatus.size}, 价格=${this.prices.size}`); } } catch (e) { Utils.logger('error', `缓存清理失败: ${e.message}`); } } }; // --- 模块四: 异步网络请求 (Promisified GM_xmlhttpRequest) --- const API = { gmFetch: (options) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ anonymous: false, // Default to false to ensure cookies are sent ...options, onload: (response) => resolve(response), onerror: (error) => reject(new Error(`GM_xmlhttpRequest error: ${error.statusText || 'Unknown Error'}`)), ontimeout: () => reject(new Error('Request timed out.')), onabort: () => reject(new Error('Request aborted.')) }); }); }, // 新增API响应数据提取函数 extractStateData: (rawData, source = '') => { // 记录原始数据格式 const dataType = Array.isArray(rawData) ? 'Array' : typeof rawData; if (State.debugMode) { Utils.logger('debug', `[${source}] API返回数据类型: ${dataType}`); } // 如果是数组,直接返回 if (Array.isArray(rawData)) { return rawData; } // 如果是对象,尝试提取可能的数组字段 if (rawData && typeof rawData === 'object') { // 记录对象的顶级键 const keys = Object.keys(rawData); if (State.debugMode) { Utils.logger('debug', `[${source}] API返回对象键: ${keys.join(', ')}`); } // 尝试常见的数组字段名 const possibleArrayFields = ['data', 'results', 'items', 'listings', 'states']; for (const field of possibleArrayFields) { if (rawData[field] && Array.isArray(rawData[field])) { Utils.logger('info', `[${source}] 在字段 "${field}" 中找到数组数据`); return rawData[field]; } } // 如果没有找到预定义字段,查找任何数组类型的字段 for (const key of keys) { if (Array.isArray(rawData[key])) { Utils.logger('info', `[${source}] 在字段 "${key}" 中找到数组数据`); return rawData[key]; } } // 如果对象中有uid和acquired字段,可能是单个项目 if (rawData.uid && 'acquired' in rawData) { Utils.logger('info', `[${source}] 返回的是单个项目数据,转换为数组`); return [rawData]; } } // 如果无法提取,记录详细信息并返回空数组 Utils.logger('warn', `[${source}] 无法从API响应中提取数组数据`); if (State.debugMode) { try { const preview = JSON.stringify(rawData).substring(0, 200); Utils.logger('debug', `[${source}] API响应预览: ${preview}...`); } catch (e) { Utils.logger('debug', `[${source}] 无法序列化API响应: ${e.message}`); } } return []; }, // 优化后的商品拥有状态检查函数 - 只使用缓存和网页原生请求的数据 checkItemsOwnership: async function(uids) { if (!uids || uids.length === 0) return []; try { // 从缓存中获取已知的拥有状态 const { result: cachedResults, missing: missingUids } = DataCache.getOwnedStatus(uids); // 如果有缺失的UID,记录但不主动请求 if (missingUids.length > 0) { Utils.logger('info', `有 ${missingUids.length} 个商品状态未知,等待网页原生请求更新`); // 将这些UID添加到等待列表,等待网页原生请求更新 DataCache.addToWaitingList(missingUids); } // 只返回缓存中已有的结果 return cachedResults; } catch (e) { Utils.logger('error', `检查拥有状态失败: ${e.message}`); return []; // 出错时返回空数组 } }, // 优化后的价格验证函数 checkItemsPrices: async function(offerIds) { if (!offerIds || offerIds.length === 0) return []; try { // 从缓存中获取已知的价格信息 const { result: cachedResults, missing: missingOfferIds } = DataCache.getPrices(offerIds); // 如果所有报价都有缓存,直接返回 if (missingOfferIds.length === 0) { if (State.debugMode) { Utils.logger('info', `使用缓存的价格数据,避免API请求`); } return cachedResults; } // 对缺失的报价ID发送API请求 if (State.debugMode) { Utils.logger('info', `对 ${missingOfferIds.length} 个缺失的报价ID发送API请求`); } const csrfToken = Utils.getCookie('fab_csrftoken'); if (!csrfToken) { throw new Error("CSRF token not found"); } const pricesUrl = new URL('https://www.fab.com/i/listings/prices-infos'); missingOfferIds.forEach(offerId => pricesUrl.searchParams.append('offer_ids', offerId)); const response = await this.gmFetch({ method: 'GET', url: pricesUrl.href, headers: { 'x-csrftoken': csrfToken, 'x-requested-with': 'XMLHttpRequest' } }); try { const pricesData = JSON.parse(response.responseText); // 提取并缓存价格信息 if (pricesData.offers && Array.isArray(pricesData.offers)) { DataCache.savePrices(pricesData.offers); // 合并缓存结果和API结果 return [...cachedResults, ...pricesData.offers]; } } catch (e) { Utils.logger('error', `[优化] 解析价格API响应失败: ${e.message}`); } // 出错时返回缓存结果 return cachedResults; } catch (e) { Utils.logger('error', `[优化] 获取价格信息失败: ${e.message}`); return []; // 出错时返回空数组 } } // ... Other API-related functions will go here ... }; // --- 模块五: 数据库交互 (Database Interaction) --- const Database = { load: async () => { // 从存储中加载待办列表,不再是session-only State.db.todo = await GM_getValue(Config.DB_KEYS.TODO, []); State.db.done = await GM_getValue(Config.DB_KEYS.DONE, []); State.db.failed = await GM_getValue(Config.DB_KEYS.FAILED, []); State.hideSaved = await GM_getValue(Config.DB_KEYS.HIDE, false); State.autoAddOnScroll = await GM_getValue(Config.DB_KEYS.AUTO_ADD, false); // Load the setting State.rememberScrollPosition = await GM_getValue(Config.DB_KEYS.REMEMBER_POS, false); State.autoResumeAfter429 = await GM_getValue(Config.DB_KEYS.AUTO_RESUME, false); State.autoRefreshEmptyPage = await GM_getValue(Config.DB_KEYS.AUTO_REFRESH_EMPTY, true); // 加载无商品自动刷新设置 State.debugMode = await GM_getValue('fab_helper_debug_mode', false); // 加载调试模式设置 State.currentSortOption = await GM_getValue('fab_helper_sort_option', 'title_desc'); // 加载排序设置 State.isExecuting = await GM_getValue(Config.DB_KEYS.IS_EXECUTING, false); // Load the execution state const persistedStatus = await GM_getValue(Config.DB_KEYS.APP_STATUS); if (persistedStatus && persistedStatus.status === 'RATE_LIMITED') { State.appStatus = 'RATE_LIMITED'; State.rateLimitStartTime = persistedStatus.startTime; // 添加空值检查,防止persistedStatus.startTime为null const previousDuration = persistedStatus && persistedStatus.startTime ? ((Date.now() - persistedStatus.startTime) / 1000).toFixed(2) : '0.00'; Utils.logger('warn', `Script starting in RATE_LIMITED state. 429 period has lasted at least ${previousDuration}s.`); } State.statusHistory = await GM_getValue(Config.DB_KEYS.STATUS_HISTORY, []); Utils.logger('info', Utils.getText('log_db_loaded'), `(Session) To-Do: ${State.db.todo.length}, Done: ${State.db.done.length}, Failed: ${State.db.failed.length}`); }, // 添加保存待办列表的方法 saveTodo: () => GM_setValue(Config.DB_KEYS.TODO, State.db.todo), saveDone: () => GM_setValue(Config.DB_KEYS.DONE, State.db.done), saveFailed: () => GM_setValue(Config.DB_KEYS.FAILED, State.db.failed), saveHidePref: () => GM_setValue(Config.DB_KEYS.HIDE, State.hideSaved), saveAutoAddPref: () => GM_setValue(Config.DB_KEYS.AUTO_ADD, State.autoAddOnScroll), // Save the setting saveRememberPosPref: () => GM_setValue(Config.DB_KEYS.REMEMBER_POS, State.rememberScrollPosition), saveAutoResumePref: () => GM_setValue(Config.DB_KEYS.AUTO_RESUME, State.autoResumeAfter429), saveAutoRefreshEmptyPref: () => GM_setValue(Config.DB_KEYS.AUTO_REFRESH_EMPTY, State.autoRefreshEmptyPage), // 保存无商品自动刷新设置 saveExecutingState: () => GM_setValue(Config.DB_KEYS.IS_EXECUTING, State.isExecuting), // Save the execution state resetAllData: async () => { if (window.confirm('您确定要清空所有本地存储的脚本数据(已完成、失败、待办列表)吗?此操作不可逆!')) { // 清除待办列表 await GM_deleteValue(Config.DB_KEYS.TODO); await GM_deleteValue(Config.DB_KEYS.DONE); await GM_deleteValue(Config.DB_KEYS.FAILED); State.db.todo = []; State.db.done = []; State.db.failed = []; Utils.logger('info', '所有脚本数据已重置。'); UI.removeAllOverlays(); UI.update(); } }, isDone: (url) => { if (!url) return false; return State.db.done.includes(url.split('?')[0]); }, isFailed: (url) => { if (!url) return false; const cleanUrl = url.split('?')[0]; return State.db.failed.some(task => task.url === cleanUrl); }, isTodo: (url) => { if (!url) return false; const cleanUrl = url.split('?')[0]; return State.db.todo.some(task => task.url === cleanUrl); }, markAsDone: async (task) => { if (!task || !task.uid) { Utils.logger('error', '标记任务完成失败,收到无效任务:', JSON.stringify(task)); return; } // 从待办列表中移除任务 const initialTodoCount = State.db.todo.length; State.db.todo = State.db.todo.filter(t => t.uid !== task.uid); // 如果待办列表发生了变化,保存到存储 if (State.db.todo.length !== initialTodoCount) { Database.saveTodo(); } if (State.db.todo.length === initialTodoCount && initialTodoCount > 0) { Utils.logger('warn', '任务未能从待办列表中移除,可能已被其他操作处理'); } let changed = false; // The 'done' list can still use URLs for simplicity, as it's for display/hiding. const cleanUrl = task.url.split('?')[0]; if (!Database.isDone(cleanUrl)) { State.db.done.push(cleanUrl); changed = true; } if (changed) { await Database.saveDone(); } }, markAsFailed: async (task) => { if (!task || !task.uid) { Utils.logger('error', '标记任务失败,收到无效任务:', JSON.stringify(task)); return; } // Remove from todo const initialTodoCount = State.db.todo.length; State.db.todo = State.db.todo.filter(t => t.uid !== task.uid); let changed = State.db.todo.length < initialTodoCount; // Add to failed, ensuring no duplicates by UID if (!State.db.failed.some(f => f.uid === task.uid)) { State.db.failed.push(task); // Store the whole task object for potential retry changed = true; } if (changed) { await Database.saveFailed(); } }, }; // --- 模块六: 网络请求过滤器 (Network Filter) --- const NetworkFilter = { init: () => { // 此模块的功能已完全被 MonkeyPatcher 取代,以确保在 document-start 时能立即生效。 Utils.logger('info', '网络过滤器(NetworkFilter)模块已弃用,功能由补丁程序(PagePatcher)处理。'); } }; // 集中处理限速状态的函数 const RateLimitManager = { // 添加防止重复日志的变量 _lastLogTime: 0, _lastLogType: null, _duplicateLogCount: 0, // 进入限速状态 enterRateLimitedState: async function(source = '未知来源') { // 如果已经处于限速状态,不需要重复处理 if (State.appStatus === 'RATE_LIMITED') { Utils.logger('info', `已处于限速状态,来源: ${State.lastLimitSource},忽略新的限速触发: ${source}`); return false; } // 重置连续成功计数 State.consecutiveSuccessCount = 0; State.lastLimitSource = source; // 记录正常运行期的统计信息 // 添加空值检查,防止normalStartTime为null const normalDuration = State.normalStartTime ? ((Date.now() - State.normalStartTime) / 1000).toFixed(2) : '0.00'; // 检查是否是短时间内重复的日志记录 const now = Date.now(); const isSameType = this._lastLogType === 'NORMAL'; const isWithinTimeThreshold = (now - this._lastLogTime) < 2000; // 2秒内的重复记录视为重复 if (isSameType && isWithinTimeThreshold) { // 增加重复计数,但不记录日志 this._duplicateLogCount++; Utils.logger('debug', `检测到重复的正常状态记录 (${this._duplicateLogCount}次),跳过日志记录`); } else { // 不是重复记录,正常记录日志 const logEntry = { type: 'NORMAL', duration: parseFloat(normalDuration), requests: State.successfulSearchCount, endTime: new Date().toISOString() }; // 保存到历史记录 State.statusHistory.push(logEntry); await GM_setValue(Config.DB_KEYS.STATUS_HISTORY, State.statusHistory); // 更新日志记录状态 this._lastLogTime = now; this._lastLogType = 'NORMAL'; // 如果有累积的重复记录,一并提示 if (this._duplicateLogCount > 0) { Utils.logger('error', `🚨 RATE LIMIT DETECTED from [${source}]! Normal operation lasted ${normalDuration}s with ${State.successfulSearchCount} successful search requests. (已合并 ${this._duplicateLogCount} 条重复记录)`); this._duplicateLogCount = 0; // 重置计数 } else { Utils.logger('error', `🚨 RATE LIMIT DETECTED from [${source}]! Normal operation lasted ${normalDuration}s with ${State.successfulSearchCount} successful search requests.`); } } // 切换到限速状态 State.appStatus = 'RATE_LIMITED'; State.rateLimitStartTime = Date.now(); // 保存状态到存储 await GM_setValue(Config.DB_KEYS.APP_STATUS, { status: 'RATE_LIMITED', startTime: State.rateLimitStartTime, source: source }); // 更新UI UI.updateDebugTab(); UI.update(); // 重新计算实际可见的商品数量,确保与DOM状态同步 const totalCards = document.querySelectorAll(Config.SELECTORS.card).length; const hiddenCards = document.querySelectorAll(`${Config.SELECTORS.card}[style*="display: none"]`).length; const actualVisibleCards = totalCards - hiddenCards; // 更新UI显示的可见商品数量,确保UI与实际DOM状态一致 const visibleCountElement = document.getElementById('fab-status-visible'); if (visibleCountElement) { visibleCountElement.textContent = actualVisibleCards.toString(); } // 更新全局状态 State.hiddenThisPageCount = hiddenCards; // 检查是否有待办任务、活动工作线程,或者可见的商品数量不为0 if (State.db.todo.length > 0 || State.activeWorkers > 0 || actualVisibleCards > 0) { if (actualVisibleCards > 0) { Utils.logger('info', `检测到页面上有 ${actualVisibleCards} 个可见商品,暂不自动刷新页面。`); Utils.logger('info', '当仍有可见商品时不触发自动刷新,以避免中断浏览。'); } else { Utils.logger('info', `检测到有 ${State.db.todo.length} 个待办任务和 ${State.activeWorkers} 个活动工作线程,暂不自动刷新页面。`); Utils.logger('info', '请手动完成或取消这些任务后再刷新页面。'); } // 显示明显提示 Utils.logger('warn', '⚠️ 处于限速状态,但不满足自动刷新条件,请在需要时手动刷新页面。'); } else { // 无任务情况下,开始随机刷新 // 缩短延迟时间为5-7秒,使恢复更快 const randomDelay = 5000 + Math.random() * 2000; if (State.autoResumeAfter429) { // 添加空值检查,防止randomDelay为null Utils.logger('info', '🔄 429自动恢复启动!将在 ' + (randomDelay ? (randomDelay/1000).toFixed(1) : '未知') + ' 秒后刷新页面尝试恢复...'); } else { // 添加空值检查,防止randomDelay为null Utils.logger('info', '🔄 检测到429错误,将在 ' + (randomDelay ? (randomDelay/1000).toFixed(1) : '未知') + ' 秒后自动刷新页面尝试恢复...'); } countdownRefresh(randomDelay, '429自动恢复'); } return true; }, // 记录成功请求 recordSuccessfulRequest: async function(source = '未知来源', hasResults = true) { // 无论在什么状态下,总是增加成功请求计数 - 修复统计问题 if (hasResults) { State.successfulSearchCount++; UI.updateDebugTab(); } // 只有在限速状态下才需要记录连续成功 if (State.appStatus !== 'RATE_LIMITED') { return; } // 如果请求没有返回有效结果,不计入连续成功 if (!hasResults) { Utils.logger('info', `请求成功但没有返回有效结果,不计入连续成功计数。来源: ${source}`); State.consecutiveSuccessCount = 0; return; } // 增加连续成功计数 State.consecutiveSuccessCount++; Utils.logger('info', `限速状态下成功请求 +1,当前连续成功: ${State.consecutiveSuccessCount}/${State.requiredSuccessCount},来源: ${source}`); // 如果达到所需的连续成功数,退出限速状态 if (State.consecutiveSuccessCount >= State.requiredSuccessCount) { await this.exitRateLimitedState(`连续${State.consecutiveSuccessCount}次成功请求 (${source})`); } }, // 退出限速状态 exitRateLimitedState: async function(source = '未知来源') { // 如果当前不是限速状态,不需要处理 if (State.appStatus !== 'RATE_LIMITED') { Utils.logger('info', `当前不是限速状态,忽略退出限速请求: ${source}`); return false; } // 记录限速期的统计信息 // 添加空值检查,防止rateLimitStartTime为null const rateLimitDuration = State.rateLimitStartTime ? ((Date.now() - State.rateLimitStartTime) / 1000).toFixed(2) : '0.00'; // 检查是否是短时间内重复的日志记录 const now = Date.now(); const isSameType = this._lastLogType === 'RATE_LIMITED'; const isWithinTimeThreshold = (now - this._lastLogTime) < 2000; // 2秒内的重复记录视为重复 if (isSameType && isWithinTimeThreshold) { // 增加重复计数,但不记录日志 this._duplicateLogCount++; Utils.logger('debug', `检测到重复的限速状态记录 (${this._duplicateLogCount}次),跳过日志记录`); } else { // 不是重复记录,正常记录日志 const logEntry = { type: 'RATE_LIMITED', duration: parseFloat(rateLimitDuration), endTime: new Date().toISOString(), source: source }; // 保存到历史记录 State.statusHistory.push(logEntry); await GM_setValue(Config.DB_KEYS.STATUS_HISTORY, State.statusHistory); // 更新日志记录状态 this._lastLogTime = now; this._lastLogType = 'RATE_LIMITED'; // 如果有累积的重复记录,一并提示 if (this._duplicateLogCount > 0) { Utils.logger('info', `✅ Rate limit appears to be lifted from [${source}]. The 429 period lasted ${rateLimitDuration}s. (已合并 ${this._duplicateLogCount} 条重复记录)`); this._duplicateLogCount = 0; // 重置计数 } else { Utils.logger('info', `✅ Rate limit appears to be lifted from [${source}]. The 429 period lasted ${rateLimitDuration}s.`); } } // 恢复到正常状态 State.appStatus = 'NORMAL'; State.rateLimitStartTime = null; State.normalStartTime = Date.now(); // 不重置请求计数,保留累计值,这样每个正常期的请求数会累加起来 // State.successfulSearchCount = 0; State.consecutiveSuccessCount = 0; // 删除存储的限速状态 await GM_deleteValue(Config.DB_KEYS.APP_STATUS); // 更新UI UI.updateDebugTab(); UI.update(); // 如果有待办任务,继续执行 if (State.db.todo.length > 0 && !State.isExecuting) { Utils.logger('info', `发现 ${State.db.todo.length} 个待办任务,自动恢复执行...`); State.isExecuting = true; Database.saveExecutingState(); TaskRunner.executeBatch(); } return true; }, // 检查限速状态 checkRateLimitStatus: async function() { // 如果已经在检查中,避免重复检查 if (State.isCheckingRateLimit) { Utils.logger('info', '已有限速状态检查正在进行,跳过本次检查'); return false; } State.isCheckingRateLimit = true; try { Utils.logger('info', '开始检查限速状态...'); // 首先检查页面内容是否包含限速信息 const pageText = document.body.innerText || ''; if (pageText.includes('Too many requests') || pageText.includes('rate limit') || pageText.match(/\{\s*"detail"\s*:\s*"Too many requests"\s*\}/i)) { Utils.logger('warn', '页面内容包含限速信息,确认仍处于限速状态'); await this.enterRateLimitedState('页面内容检测'); return false; } // 使用Performance API检查最近的网络请求,而不是主动发送API请求 Utils.logger('info', '使用Performance API检查最近的网络请求,不再主动发送API请求'); if (window.performance && window.performance.getEntriesByType) { const recentRequests = window.performance.getEntriesByType('resource') .filter(r => r.name.includes('/i/listings/search') || r.name.includes('/i/users/me/listings-states')) .filter(r => Date.now() - r.startTime < 10000); // 最近10秒内的请求 // 如果有最近的请求,检查它们的状态 if (recentRequests.length > 0) { // 检查是否有429状态码的请求 const has429 = recentRequests.some(r => r.responseStatus === 429); if (has429) { Utils.logger('info', `检测到最近10秒内有429状态码的请求,判断为限速状态`); await this.enterRateLimitedState('Performance API检测429'); return false; } // 检查是否有成功的请求 const hasSuccess = recentRequests.some(r => r.responseStatus >= 200 && r.responseStatus < 300); if (hasSuccess) { Utils.logger('info', `检测到最近10秒内有成功的API请求,判断为正常状态`); await this.recordSuccessfulRequest('Performance API检测成功', true); return true; } } } // 如果没有足够的信息判断,保持当前状态 Utils.logger('info', `没有足够的信息判断限速状态,保持当前状态`); return State.appStatus === 'NORMAL'; } catch (e) { Utils.logger('error', `限速状态检查失败: ${e.message}`); return false; } finally { State.isCheckingRateLimit = false; } } }; const PagePatcher = { _patchHasBeenApplied: false, _lastSeenCursor: null, // REMOVED: This state variable was the source of the bug. // _secondToLastSeenCursor: null, // --- NEW: State for request debouncing --- _debounceXhrTimer: null, _pendingXhr: null, async init() { // 初始化时,从存储中加载上次保存的cursor try { const savedCursor = await GM_getValue(Config.DB_KEYS.LAST_CURSOR); if (savedCursor) { State.savedCursor = savedCursor; this._lastSeenCursor = savedCursor; Utils.logger('info', `[Cursor] Initialized. Loaded saved cursor: ${savedCursor.substring(0, 30)}...`); } else { Utils.logger('info', `[Cursor] Initialized. No saved cursor found.`); } } catch (e) { Utils.logger('warn', '[Cursor] Failed to restore cursor state:', e); } // 应用拦截器 this.applyPatches(); Utils.logger('info', '[Cursor] Network interceptors applied.'); // 监听URL变化,检测排序方式变更 this.setupSortMonitor(); }, // 添加监听URL变化的方法,检测排序方式变更 setupSortMonitor() { // 初始检查当前URL中的排序参数 this.checkCurrentSortFromUrl(); // 使用MutationObserver监听URL变化 if (typeof MutationObserver !== 'undefined') { // 监听body变化,因为SPA应用可能不会触发popstate事件 const bodyObserver = new MutationObserver((mutations) => { // 如果URL发生变化,检查排序参数 if (window.location.href !== this._lastCheckedUrl) { this._lastCheckedUrl = window.location.href; this.checkCurrentSortFromUrl(); } }); bodyObserver.observe(document.body, { childList: true, subtree: true }); // 保存引用以便后续可以断开 this._bodyObserver = bodyObserver; } // 监听popstate事件(浏览器前进/后退按钮) window.addEventListener('popstate', () => { this.checkCurrentSortFromUrl(); }); // 监听hashchange事件 window.addEventListener('hashchange', () => { this.checkCurrentSortFromUrl(); }); // 保存当前URL作为初始状态 this._lastCheckedUrl = window.location.href; }, // 从URL中检查当前排序方式并更新设置 checkCurrentSortFromUrl() { try { const url = new URL(window.location.href); const sortParam = url.searchParams.get('sort_by'); if (!sortParam) return; // 如果URL中没有排序参数,不做任何更改 // 查找匹配的排序选项 let matchedOption = null; for (const [key, option] of Object.entries(State.sortOptions)) { if (option.value === sortParam) { matchedOption = key; break; } } // 如果找到匹配的排序选项,且与当前选项不同,则更新 if (matchedOption && matchedOption !== State.currentSortOption) { const previousSort = State.currentSortOption; State.currentSortOption = matchedOption; GM_setValue('fab_helper_sort_option', State.currentSortOption); // 排序选择器UI已移除,不需要更新 Utils.logger('info', `检测到URL排序参数变更,排序方式已从"${State.sortOptions[previousSort].name}"更改为"${State.sortOptions[State.currentSortOption].name}"`); // 清除已保存的浏览位置 State.savedCursor = null; GM_deleteValue(Config.DB_KEYS.LAST_CURSOR); if (State.UI.savedPositionDisplay) { State.UI.savedPositionDisplay.textContent = '无保存位置'; } Utils.logger('info', '由于排序方式变更,已清除保存的浏览位置'); } } catch (e) { Utils.logger('warn', `检查URL排序参数时出错: ${e.message}`); } }, async handleSearchResponse(request) { if (request.status === 429) { // 使用统一的限速管理器处理限速情况 await RateLimitManager.enterRateLimitedState('搜索响应429'); } else if (request.status >= 200 && request.status < 300) { try { // 检查响应是否包含有效数据 const responseText = request.responseText; if (responseText) { const data = JSON.parse(responseText); const hasResults = data && data.results && data.results.length > 0; // 记录成功请求,并传递是否有结果的信息 await RateLimitManager.recordSuccessfulRequest('搜索响应成功', hasResults); } } catch (e) { Utils.logger('warn', `搜索响应解析失败: ${e.message}`); } } }, isDebounceableSearch(url) { return typeof url === 'string' && url.includes('/i/listings/search') && !url.includes('aggregate_on=') && !url.includes('count=0'); }, shouldPatchUrl(url) { if (typeof url !== 'string') return false; if (this._patchHasBeenApplied) return false; if (!State.rememberScrollPosition || !State.savedCursor) return false; if (!url.includes('/i/listings/search')) return false; if (url.includes('aggregate_on=') || url.includes('count=0') || url.includes('in=wishlist')) return false; // 同时支持sort_by=title和sort_by=-title的URL Utils.logger('info', `[PagePatcher] -> ✅ MATCH! URL will be patched: ${url}`); return true; }, getPatchedUrl(originalUrl) { if (State.savedCursor) { const urlObj = new URL(originalUrl, window.location.origin); urlObj.searchParams.set('cursor', State.savedCursor); // 确保不改变原始URL中的sort_by参数,如果存在的话 // 这样可以支持sort_by=-title(降序)和sort_by=title(升序) const modifiedUrl = urlObj.pathname + urlObj.search; // NEW: Logging for injection Utils.logger('info', `[Cursor] Injecting cursor. Original: ${originalUrl}`); Utils.logger('info', `[Cursor] Patched URL: ${modifiedUrl}`); this._patchHasBeenApplied = true; // This should be set here return modifiedUrl; } return originalUrl; }, saveLatestCursorFromUrl(url) { // 改进实现,确保不会保存过早的浏览位置 try { if (typeof url !== 'string' || !url.includes('/i/listings/search') || !url.includes('cursor=')) return; const urlObj = new URL(url, window.location.origin); const newCursor = urlObj.searchParams.get('cursor'); // 如果是有效的cursor且与上次的不同 if (newCursor && newCursor !== this._lastSeenCursor) { // 解码cursor,检查是否是有效的浏览位置 let isValidPosition = true; let decodedCursor = ''; try { decodedCursor = atob(newCursor); // 1. 检查特定的过滤关键词列表 const filterKeywords = [ "Nude+Tennis+Racket", "Nordic+Beach+Boulder", "Nordic+Beach+Rock" ]; // 检查是否包含任何需要过滤的关键词 if (filterKeywords.some(keyword => decodedCursor.includes(keyword))) { Utils.logger('info', `[Cursor] 跳过已知位置的保存: ${decodedCursor}`); isValidPosition = false; } // 2. 检查是否是已经滚动过的前面位置(直接检测首字母) if (isValidPosition && this._lastSeenCursor) { try { // 从解码的cursor中提取物品名称 let newItemName = ''; let lastItemName = ''; // 提取当前cursor中的物品名 if (decodedCursor.includes("p=")) { const match = decodedCursor.match(/p=([^&]+)/); if (match && match[1]) { newItemName = decodeURIComponent(match[1].replace(/\+/g, ' ')); } } // 提取上次保存cursor中的物品名 const lastDecoded = atob(this._lastSeenCursor); if (lastDecoded.includes("p=")) { const match = lastDecoded.match(/p=([^&]+)/); if (match && match[1]) { lastItemName = decodeURIComponent(match[1].replace(/\+/g, ' ')); } } // 提取首字母或首个单词进行比较 if (newItemName && lastItemName) { // 获取首个单词或首字母 const getFirstWord = (text) => { // 优先获取前三个字母,如果不足三个则获取全部 return text.trim().substring(0, 3); }; const newFirstWord = getFirstWord(newItemName); const lastFirstWord = getFirstWord(lastItemName); // 检查URL中的排序参数 const sortParam = urlObj.searchParams.get('sort_by') || ''; const isReverseSort = sortParam.startsWith('-'); // 根据排序方向决定比较逻辑 // 如果是按标题排序: // - 升序排列(title):如果新位置的首字母在字母表中排在当前位置前面,说明是回退了 // - 降序排列(-title):如果新位置的首字母在字母表中排在当前位置后面,说明是回退了 if ((isReverseSort && sortParam.includes('title') && newFirstWord > lastFirstWord) || (!isReverseSort && sortParam.includes('title') && newFirstWord < lastFirstWord)) { Utils.logger('info', `[Cursor] 跳过回退位置: ${newItemName} (当前位置: ${lastItemName}), 排序: ${isReverseSort ? '降序' : '升序'}`); isValidPosition = false; } } } catch (compareError) { // 比较错误,继续正常流程 } } } catch (decodeError) { // 解码错误,继续正常流程 } // 只有是有效位置才保存 if (isValidPosition) { this._lastSeenCursor = newCursor; State.savedCursor = newCursor; // 立即更新状态 // 持久化保存cursor供下次页面加载使用 GM_setValue(Config.DB_KEYS.LAST_CURSOR, newCursor); // 日志记录保存操作 Utils.logger('info', `[Cursor] 保存新的恢复点: ${newCursor.substring(0, 30)}...`); // 更新UI中的位置显示 if (State.UI.savedPositionDisplay) { State.UI.savedPositionDisplay.textContent = Utils.decodeCursor(newCursor); } } } } catch (e) { Utils.logger('warn', `[Cursor] 保存cursor时出错:`, e); } }, applyPatches() { const self = this; const originalXhrOpen = XMLHttpRequest.prototype.open; const originalXhrSend = XMLHttpRequest.prototype.send; const DEBOUNCE_DELAY_MS = 350; // Centralize debounce delay const listenerAwareSend = function(...args) { const request = this; // 为所有请求添加监听器 const onLoad = () => { request.removeEventListener("load", onLoad); // 记录所有网络活动 if (typeof window.recordNetworkActivity === 'function') { window.recordNetworkActivity(); } // 只统计商品相关的请求,保持原有逻辑 if (request.status >= 200 && request.status < 300 && request._url && self.isDebounceableSearch(request._url)) { // 只记录商品卡片相关请求 window.recordNetworkRequest('XHR商品请求', true); } // 对所有请求检查429错误 if (request.status === 429 || request.status === '429' || request.status.toString() === '429') { Utils.logger('warn', `[XHR] 检测到429状态码: ${request.responseURL || request._url}`); // 调用handleRateLimit函数处理限速情况 RateLimitManager.enterRateLimitedState(request.responseURL || request._url || 'XHR响应429'); return; } // 检查其他可能的限速情况(返回空结果或错误信息) if (request.status >= 200 && request.status < 300) { try { const responseText = request.responseText; if (responseText) { // 先检查原始文本是否包含限速相关的关键词 if (responseText.includes("Too many requests") || responseText.includes("rate limit") || responseText.match(/\{\s*"detail"\s*:\s*"Too many requests"\s*\}/i)) { Utils.logger('warn', `[XHR限速检测] 检测到限速情况,原始响应: ${responseText}`); RateLimitManager.enterRateLimitedState('XHR响应内容限速'); return; } // 尝试解析JSON try { const data = JSON.parse(responseText); // 检查是否返回了空结果或错误信息 if (data.detail && (data.detail.includes("Too many requests") || data.detail.includes("rate limit"))) { Utils.logger('warn', `[隐性限速检测] 检测到限速错误信息: ${JSON.stringify(data)}`); RateLimitManager.enterRateLimitedState('XHR响应限速错误'); return; } // 检查是否返回了空结果 if (data.results && data.results.length === 0 && self.isDebounceableSearch(request._url)) { // 情况1: 到达列表末尾的正常情况(next为null但previous不为null) const isEndOfList = data.next === null && data.previous !== null && data.cursors && data.cursors.next === null && data.cursors.previous !== null; // 情况2: 完全空的结果集,但可能是正常的搜索结果为空 const isEmptySearch = data.next === null && data.previous === null && data.cursors && data.cursors.next === null && data.cursors.previous === null; // 获取当前URL的参数 const urlObj = new URL(request._url, window.location.origin); const params = urlObj.searchParams; // 检查是否有特殊的搜索参数(如果有特殊过滤条件,空结果可能是正常的) const hasSpecialFilters = params.has('query') || params.has('category') || params.has('subcategory') || params.has('tag'); if (isEndOfList) { Utils.logger('info', `[列表末尾] 检测到已到达列表末尾,这是正常情况,不触发限速: ${JSON.stringify(data).substring(0, 200)}...`); // 记录成功请求,虽然没有结果,但这是正常情况 RateLimitManager.recordSuccessfulRequest('XHR列表末尾', true); return; } else if (isEmptySearch && hasSpecialFilters) { Utils.logger('info', `[空搜索结果] 检测到搜索结果为空,但包含特殊过滤条件,这可能是正常情况: ${JSON.stringify(data).substring(0, 200)}...`); // 记录成功请求,虽然没有结果,但这可能是正常情况 RateLimitManager.recordSuccessfulRequest('XHR空搜索结果', true); return; } else if (isEmptySearch && State.appStatus === 'RATE_LIMITED') { // 如果已经处于限速状态,不要重复触发 Utils.logger('info', `[空搜索结果] 已处于限速状态,不重复触发: ${JSON.stringify(data).substring(0, 200)}...`); return; } else if (isEmptySearch && document.readyState !== 'complete') { // 如果页面尚未完全加载,可能是初始请求,不要立即触发限速 Utils.logger('info', `[空搜索结果] 页面尚未完全加载,可能是初始请求,不触发限速: ${JSON.stringify(data).substring(0, 200)}...`); return; } else if (isEmptySearch && Date.now() - (window.pageLoadTime || 0) < 5000) { // 如果页面刚刚加载不到5秒,可能是初始请求,不要立即触发限速 Utils.logger('info', `[空搜索结果] 页面刚刚加载,可能是初始请求,不触发限速: ${JSON.stringify(data).substring(0, 200)}...`); return; } else { Utils.logger('warn', `[隐性限速检测] 检测到可能的限速情况(空结果): ${JSON.stringify(data).substring(0, 200)}...`); RateLimitManager.enterRateLimitedState('XHR响应空结果'); return; } } // 如果是搜索请求且有结果,记录成功请求 if (self.isDebounceableSearch(request._url) && data.results && data.results.length > 0) { RateLimitManager.recordSuccessfulRequest('XHR搜索成功', true); } } catch (jsonError) { // JSON解析错误,忽略 } } } catch (e) { // 解析错误,忽略 } } // 处理搜索请求的特殊逻辑(429检测等) if (self.isDebounceableSearch(request._url)) { self.handleSearchResponse(request); } }; request.addEventListener("load", onLoad); return originalXhrSend.apply(request, args); }; XMLHttpRequest.prototype.open = function(method, url, ...args) { let modifiedUrl = url; // Priority 1: Handle the "remember position" patch, which should not be debounced. if (self.shouldPatchUrl(url)) { modifiedUrl = self.getPatchedUrl(url); this._isDebouncedSearch = false; // Explicitly mark it as NOT debounced } // Priority 2: Tag all other infinite scroll requests to be debounced. else if (self.isDebounceableSearch(url)) { self.saveLatestCursorFromUrl(url); // FIX: Ensure we save the cursor before debouncing. this._isDebouncedSearch = true; } // Priority 3: All other requests just save the cursor. else { self.saveLatestCursorFromUrl(url); } this._url = modifiedUrl; // We still call the original open, but the send will be intercepted. return originalXhrOpen.apply(this, [method, modifiedUrl, ...args]); }; XMLHttpRequest.prototype.send = function(...args) { // If this is not a request we need to debounce, send it immediately. if (!this._isDebouncedSearch) { // Still use the wrapper to catch responses for non-debounced search requests return listenerAwareSend.apply(this, args); } // NEW: Use [Debounce] tag for clarity Utils.logger('info', `[Debounce] 🚦 Intercepted scroll request. Applying ${DEBOUNCE_DELAY_MS}ms delay...`); // If there's a previously pending request, abort it. if (self._pendingXhr) { self._pendingXhr.abort(); Utils.logger('info', `[Debounce] 🗑️ Discarded previous pending request.`); } // Clear any existing timer. clearTimeout(self._debounceXhrTimer); // Store the current request as the latest one. self._pendingXhr = this; // Set a timer to send the latest request after a period of inactivity. self._debounceXhrTimer = setTimeout(() => { Utils.logger('info', `[Debounce] ▶️ Sending latest scroll request: ${this._url}`); listenerAwareSend.apply(self._pendingXhr, args); self._pendingXhr = null; // Clear after sending }, DEBOUNCE_DELAY_MS); }; const originalFetch = window.fetch; window.fetch = function(input, init) { let url = (typeof input === 'string') ? input : input.url; let modifiedInput = input; if (self.shouldPatchUrl(url)) { const modifiedUrl = self.getPatchedUrl(url); if (typeof input === 'string') { modifiedInput = modifiedUrl; } else { modifiedInput = new Request(modifiedUrl, input); } } else { self.saveLatestCursorFromUrl(url); } // 拦截响应以检测429错误 return originalFetch.apply(this, [modifiedInput, init]) .then(async response => { // 记录所有网络活动 if (typeof window.recordNetworkActivity === 'function') { window.recordNetworkActivity(); } // 只统计商品相关的请求 if (response.status >= 200 && response.status < 300 && typeof url === 'string' && self.isDebounceableSearch(url)) { window.recordNetworkRequest('Fetch商品请求', true); } // 检查429错误 if (response.status === 429 || response.status === '429' || response.status.toString() === '429') { // 克隆响应以避免"已消费"错误 const clonedResponse = response.clone(); Utils.logger('warn', `[Fetch] 检测到429状态码: ${response.url}`); // 使用RateLimitManager处理限速情况 RateLimitManager.enterRateLimitedState('Fetch响应429').catch(e => Utils.logger('error', `处理限速时出错: ${e.message}`) ); } // 检查其他可能的限速情况(返回空结果或错误信息) if (response.status >= 200 && response.status < 300) { try { // 克隆响应以避免"已消费"错误 const clonedResponse = response.clone(); // 先检查原始文本 const text = await clonedResponse.text(); if (text.includes("Too many requests") || text.includes("rate limit") || text.match(/\{\s*"detail"\s*:\s*"Too many requests"\s*\}/i)) { Utils.logger('warn', `[Fetch限速检测] 检测到限速情况,原始响应: ${text.substring(0, 100)}...`); RateLimitManager.enterRateLimitedState('Fetch响应内容限速').catch(e => Utils.logger('error', `处理限速时出错: ${e.message}`) ); return response; } // 尝试解析JSON - 增强版 try { const data = JSON.parse(text); // 检查明确的限速信息 if (data.detail && (data.detail.includes("Too many requests") || data.detail.includes("rate limit"))) { Utils.logger('warn', `[限速检测] 检测到API限速响应`); RateLimitManager.enterRateLimitedState('API限速响应').catch(e => Utils.logger('error', `处理限速时出错: ${e.message}`) ); return; } // 检查是否返回了空结果 const responseUrl = response.url || ''; if (data.results && data.results.length === 0 && responseUrl.includes('/i/listings/search')) { // 情况1: 到达列表末尾的正常情况(next为null但previous不为null) const isEndOfList = data.next === null && data.previous !== null && data.cursors && data.cursors.next === null && data.cursors.previous !== null; // 情况2: 完全空的结果集,但可能是正常的搜索结果为空 const isEmptySearch = data.next === null && data.previous === null && data.cursors && data.cursors.next === null && data.cursors.previous === null; // 获取当前URL的参数 const urlObj = new URL(responseUrl, window.location.origin); const params = urlObj.searchParams; // 检查是否有特殊的搜索参数(如果有特殊过滤条件,空结果可能是正常的) const hasSpecialFilters = params.has('query') || params.has('category') || params.has('subcategory') || params.has('tag'); if (isEndOfList) { Utils.logger('info', `[Fetch列表末尾] 检测到已到达列表末尾,这是正常情况,不触发限速: ${JSON.stringify(data).substring(0, 200)}...`); // 记录成功请求,虽然没有结果,但这是正常情况 RateLimitManager.recordSuccessfulRequest('Fetch列表末尾', true); } else if (isEmptySearch && hasSpecialFilters) { Utils.logger('info', `[Fetch空搜索结果] 检测到搜索结果为空,但包含特殊过滤条件,这可能是正常情况: ${JSON.stringify(data).substring(0, 200)}...`); // 记录成功请求,虽然没有结果,但这可能是正常情况 RateLimitManager.recordSuccessfulRequest('Fetch空搜索结果', true); } else if (isEmptySearch && State.appStatus === 'RATE_LIMITED') { // 如果已经处于限速状态,不要重复触发 Utils.logger('info', `[Fetch空搜索结果] 已处于限速状态,不重复触发: ${JSON.stringify(data).substring(0, 200)}...`); } else if (isEmptySearch && document.readyState !== 'complete') { // 如果页面尚未完全加载,可能是初始请求,不要立即触发限速 Utils.logger('info', `[Fetch空搜索结果] 页面尚未完全加载,可能是初始请求,不触发限速: ${JSON.stringify(data).substring(0, 200)}...`); } else if (isEmptySearch && Date.now() - (window.pageLoadTime || 0) < 5000) { // 如果页面刚刚加载不到5秒,可能是初始请求,不要立即触发限速 Utils.logger('info', `[Fetch空搜索结果] 页面刚刚加载,可能是初始请求,不触发限速: ${JSON.stringify(data).substring(0, 200)}...`); } else { Utils.logger('warn', `[Fetch隐性限速] 检测到可能的限速情况(空结果): ${JSON.stringify(data).substring(0, 200)}...`); RateLimitManager.enterRateLimitedState('Fetch响应空结果').catch(e => Utils.logger('error', `处理限速时出错: ${e.message}`) ); } } } catch (jsonError) { // JSON解析错误,忽略 Utils.logger('debug', `JSON解析错误: ${jsonError.message}`); // 添加更多调试信息,帮助诊断问题 if (responseText && responseText.length > 0) { Utils.logger('debug', `响应长度: ${responseText.length}, 前100个字符: ${responseText.substring(0, 100)}`); } } } catch (e) { // 解析错误,忽略 } } return response; }); }; } }; // --- 模块七: 任务运行器与事件处理 (Task Runner & Event Handlers) --- const TaskRunner = { isCardFinished: (card) => { const link = card.querySelector(Config.SELECTORS.cardLink); // If there's no link, we can't get a URL to check against the DB. // In this case, rely only on visual cues. const url = link ? link.href.split('?')[0] : null; // 如果没有链接,无法获取UID,则只能依赖视觉提示 if (!link) { // 检查是否有"已拥有"样式标记(绿色对勾图标) const icons = card.querySelectorAll('i.fabkit-Icon--intent-success, i.edsicon-check-circle-filled'); if (icons.length > 0) return true; // 检查是否有"已保存"文本 const text = card.textContent || ''; return text.includes("已保存在我的库中") || text.includes("已保存") || text.includes("Saved to My Library") || text.includes("In your library"); } // 从链接中提取UID const uidMatch = link.href.match(/listings\/([a-f0-9-]+)/); if (!uidMatch || !uidMatch[1]) { return false; } const uid = uidMatch[1]; // 优先使用缓存的API数据判断 if (DataCache.ownedStatus.has(uid)) { const status = DataCache.ownedStatus.get(uid); if (status && status.acquired) { return true; } } // 如果缓存中没有,则检查网页元素 if (card.querySelector(Config.SELECTORS.ownedStatus) !== null) { // 找到了,将状态保存到缓存 if (uid) { DataCache.saveOwnedStatus([{ uid: uid, acquired: true, lastUpdatedAt: new Date().toISOString() }]); } return true; } // 最后检查本地数据库 if (url) { if (Database.isDone(url)) return true; if (Database.isFailed(url)) return true; // A failed item is also considered "finished" for skipping/hiding purposes. if (State.sessionCompleted.has(url)) return true; } return false; }, // --- Toggles --- // This is the new main execution function, triggered by the "一键开刷" button. toggleExecution: () => { if (State.isExecuting) { // If it's running, stop it. State.isExecuting = false; // 保存执行状态 Database.saveExecutingState(); State.runningWorkers = {}; State.activeWorkers = 0; State.executionTotalTasks = 0; State.executionCompletedTasks = 0; State.executionFailedTasks = 0; Utils.logger('info', '执行已由用户手动停止。'); UI.update(); return; } // NEW: Divert logic if auto-add is on. The observer populates the list, // so the button should just act as a "start" signal. if (State.autoAddOnScroll) { Utils.logger('info', '"自动添加"已开启。将直接处理当前"待办"队列中的所有任务。'); // 先检查当前页面上的卡片状态,更新数据库 TaskRunner.checkVisibleCardsStatus().then(() => { // 然后开始执行任务 TaskRunner.startExecution(); // This will use the existing todo list }); return; } // --- BEHAVIOR CHANGE: From Accumulate to Overwrite Mode --- // As per user request for waterfall pages, clear the existing To-Do list before every scan. // This part now only runs when auto-add is OFF. State.db.todo = []; Utils.logger('info', '待办列表已清空。现在将扫描并仅添加当前可见的项目。'); Utils.logger('info', '正在扫描已加载完成的商品...'); const cards = document.querySelectorAll(Config.SELECTORS.card); const newlyAddedList = []; let alreadyInQueueCount = 0; let ownedCount = 0; let skippedCount = 0; const isCardSettled = (card) => { return card.querySelector(`${Config.SELECTORS.freeStatus}, ${Config.SELECTORS.ownedStatus}`) !== null; }; cards.forEach(card => { // 正确的修复:直接检查元素的 display 样式。如果它是 'none',就意味着它被隐藏了,应该跳过。 if (card.style.display === 'none') { return; } if (!isCardSettled(card)) { skippedCount++; return; // Skip unsettled cards } // UNIFIED LOGIC: Use the new single source of truth to check if the card is finished. if (TaskRunner.isCardFinished(card)) { ownedCount++; return; } const link = card.querySelector(Config.SELECTORS.cardLink); const url = link ? link.href.split('?')[0] : null; if (!url) return; // Should be caught by isCardFinished, but good for safety. // The only check unique to adding is whether it's already in the 'todo' queue. const isTodo = Database.isTodo(url); if (isTodo) { alreadyInQueueCount++; return; } const name = card.querySelector('a[aria-label*="创作的"]')?.textContent.trim() || card.querySelector('a[href*="/listings/"]')?.textContent.trim() || 'Untitled'; newlyAddedList.push({ name, url, type: 'detail', uid: url.split('/').pop() }); }); if (skippedCount > 0) { Utils.logger('info', `已跳过 ${skippedCount} 个状态未加载的商品。`); } if (newlyAddedList.length > 0) { State.db.todo.push(...newlyAddedList); Utils.logger('info', `已将 ${newlyAddedList.length} 个新商品加入待办队列。`); } const actionableCount = State.db.todo.length; if (actionableCount > 0) { if (newlyAddedList.length === 0 && alreadyInQueueCount > 0) { Utils.logger('info', `本页的 ${alreadyInQueueCount} 个可领取商品已全部在待办或失败队列中。`); } // 先检查当前页面上的卡片状态,更新数据库 TaskRunner.checkVisibleCardsStatus().then(() => { // 然后开始执行任务 TaskRunner.startExecution(); }); } else { Utils.logger('info', `本页没有可领取的新商品 (已拥有: ${ownedCount} 个, 已跳过: ${skippedCount} 个)。`); UI.update(); } }, // This function starts the execution loop without scanning. startExecution: () => { // Case 1: Execution is already running. We just need to update the total task count. if (State.isExecuting) { const newTotal = State.db.todo.length; if (newTotal > State.executionTotalTasks) { Utils.logger('info', `任务执行中,新任务已添加。总任务数更新为: ${newTotal}`); State.executionTotalTasks = newTotal; UI.update(); // Update the UI to reflect the new total. } else { Utils.logger('info', '执行器已在运行中,新任务已加入队列等待处理。'); } // IMPORTANT: Do not start a new execution loop. The current one will pick up the new tasks. return; } // Case 2: Starting a new execution from an idle state. if (State.db.todo.length === 0) { Utils.logger('info', Utils.getText('log_exec_no_tasks')); return; } Utils.logger('info', `队列中有 ${State.db.todo.length} 个任务,即将开始执行...`); State.isExecuting = true; // 保存执行状态 Database.saveExecutingState(); State.executionTotalTasks = State.db.todo.length; State.executionCompletedTasks = 0; State.executionFailedTasks = 0; // 立即更新UI,确保按钮状态与执行状态一致 UI.update(); TaskRunner.executeBatch(); }, // 执行按钮的点击处理函数 toggleExecution: () => { if (State.isExecuting) { TaskRunner.stop(); } else { // 检查待办清单是否为空,如果为空则先扫描页面 if (State.db.todo.length === 0) { Utils.logger('info', '待办清单为空,正在扫描当前页面...'); // 使用主扫描函数,这会清空待办并添加新发现的商品 const cards = document.querySelectorAll(Config.SELECTORS.card); const newlyAddedList = []; let alreadyInQueueCount = 0; let ownedCount = 0; let skippedCount = 0; const isCardSettled = (card) => { return card.querySelector(`${Config.SELECTORS.freeStatus}, ${Config.SELECTORS.ownedStatus}`) !== null; }; cards.forEach(card => { // 检查元素是否被隐藏 if (card.style.display === 'none') { return; } if (!isCardSettled(card)) { skippedCount++; return; // 跳过未加载完成的卡片 } // 使用统一逻辑检查卡片是否已处理 if (TaskRunner.isCardFinished(card)) { ownedCount++; return; } const link = card.querySelector(Config.SELECTORS.cardLink); const url = link ? link.href.split('?')[0] : null; if (!url) return; // 检查是否已在待办队列 const isTodo = Database.isTodo(url); if (isTodo) { alreadyInQueueCount++; return; } const name = card.querySelector('a[aria-label*="创作的"]')?.textContent.trim() || card.querySelector('a[href*="/listings/"]')?.textContent.trim() || 'Untitled'; newlyAddedList.push({ name, url, type: 'detail', uid: url.split('/').pop() }); }); if (skippedCount > 0) { Utils.logger('info', `已跳过 ${skippedCount} 个状态未加载的商品。`); } if (newlyAddedList.length > 0) { State.db.todo.push(...newlyAddedList); Utils.logger('info', `已将 ${newlyAddedList.length} 个新商品加入待办队列。`); // 保存待办列表到存储 Database.saveTodo(); } else { Utils.logger('info', `本页没有可领取的新商品 (已拥有: ${ownedCount} 个, 已跳过: ${skippedCount} 个)。`); } } // 然后开始执行 TaskRunner.startExecution(); } // 立即更新UI,确保按钮状态与执行状态一致 UI.update(); }, toggleHideSaved: async () => { State.hideSaved = !State.hideSaved; await Database.saveHidePref(); TaskRunner.runHideOrShow(); // 如果关闭了隐藏功能,确保更新可见商品计数 if (!State.hideSaved) { // 重新计算实际可见的商品数量 const actualVisibleCount = document.querySelectorAll(`${Config.SELECTORS.card}:not([style*="display: none"])`).length; Utils.logger('info', `👁️ 显示模式已切换,当前页面有 ${actualVisibleCount} 个可见商品`); } UI.update(); }, toggleAutoAdd: async () => { if (State.isTogglingSetting) return; State.isTogglingSetting = true; State.autoAddOnScroll = !State.autoAddOnScroll; await Database.saveAutoAddPref(); Utils.logger('info', `无限滚动自动添加任务已 ${State.autoAddOnScroll ? '开启' : '关闭'}.`); // No need to call UI.update() as the visual state is handled by the component itself. setTimeout(() => { State.isTogglingSetting = false; }, 200); }, toggleAutoResume: async () => { if (State.isTogglingSetting) return; State.isTogglingSetting = true; State.autoResumeAfter429 = !State.autoResumeAfter429; await Database.saveAutoResumePref(); Utils.logger('info', `429后自动恢复功能已 ${State.autoResumeAfter429 ? '开启' : '关闭'}.`); setTimeout(() => { State.isTogglingSetting = false; }, 200); }, toggleRememberPosition: async () => { if (State.isTogglingSetting) return; State.isTogglingSetting = true; State.rememberScrollPosition = !State.rememberScrollPosition; await Database.saveRememberPosPref(); Utils.logger('info', `记住瀑布流浏览位置功能已 ${State.rememberScrollPosition ? '开启' : '关闭'}.`); if (!State.rememberScrollPosition) { await GM_deleteValue(Config.DB_KEYS.LAST_CURSOR); // 重置PagePatcher中的状态 PagePatcher._patchHasBeenApplied = false; PagePatcher._lastSeenCursor = null; State.savedCursor = null; Utils.logger('info', '已清除已保存的浏览位置。'); // 更新UI中的位置显示 if (State.UI.savedPositionDisplay) { State.UI.savedPositionDisplay.textContent = Utils.decodeCursor(null); } } else if (State.UI.savedPositionDisplay) { // 如果开启功能,更新显示当前保存的位置 State.UI.savedPositionDisplay.textContent = Utils.decodeCursor(State.savedCursor); } setTimeout(() => { State.isTogglingSetting = false; }, 200); }, // 停止执行任务 stop: () => { if (!State.isExecuting) return; State.isExecuting = false; // 保存执行状态 Database.saveExecutingState(); // 保存待办列表 Database.saveTodo(); // 清理任务和工作线程 GM_deleteValue(Config.DB_KEYS.TASK); State.runningWorkers = {}; State.activeWorkers = 0; State.executionTotalTasks = 0; State.executionCompletedTasks = 0; State.executionFailedTasks = 0; Utils.logger('info', '执行已由用户手动停止。'); // 立即更新UI,确保按钮状态与执行状态一致 UI.update(); }, runRecoveryProbe: async () => { const randomDelay = Math.floor(Math.random() * (30000 - 15000 + 1) + 15000); // 15-30 seconds Utils.logger('info', `[Auto-Recovery] In recovery mode. Probing connection in ${(randomDelay / 1000).toFixed(1)} seconds...`); setTimeout(async () => { Utils.logger('info', `[Auto-Recovery] Probing connection...`); try { const csrfToken = Utils.getCookie('fab_csrftoken'); if (!csrfToken) throw new Error("CSRF token not found for probe."); // Use a lightweight, known-good endpoint for the probe const probeResponse = await API.gmFetch({ method: 'GET', url: 'https://www.fab.com/i/users/context', headers: { 'x-csrftoken': csrfToken, 'x-requested-with': 'XMLHttpRequest' } }); if (probeResponse.status === 429) { throw new Error("Probe failed with 429. Still rate-limited."); } else if (probeResponse.status >= 200 && probeResponse.status < 300) { // SUCCESS! // Manually create a fake request object to reuse the recovery logic in handleSearchResponse await PagePatcher.handleSearchResponse({ status: 200 }); Utils.logger('info', `[Auto-Recovery] ✅ Connection restored! Auto-resuming operations...`); TaskRunner.toggleExecution(); // Auto-start the process! } else { throw new Error(`Probe failed with unexpected status: ${probeResponse.status}`); } } catch (e) { Utils.logger('error', `[Auto-Recovery] ❌ ${e.message}. Scheduling next refresh...`); setTimeout(() => location.reload(), 2000); // Wait 2s before next refresh } }, randomDelay); }, resetReconProgress: async () => { if (State.isReconquening) { Utils.logger('warn', 'Cannot reset progress while recon is active.'); return; } await GM_deleteValue(Config.DB_KEYS.NEXT_URL); if (State.UI.reconProgressDisplay) { State.UI.reconProgressDisplay.textContent = 'Page: 1'; } Utils.logger('info', 'Recon progress has been reset. Next scan will start from the beginning.'); }, refreshVisibleStates: async () => { const API_ENDPOINT = 'https://www.fab.com/i/users/me/listings-states'; const API_CHUNK_SIZE = 24; // Server-side limit const isElementInViewport = (el) => { if (!el) return false; const rect = el.getBoundingClientRect(); return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0; }; try { const csrfToken = Utils.getCookie('fab_csrftoken'); if (!csrfToken) throw new Error('CSRF token not found. Are you logged in?'); // Step 1: Gather all unique UIDs to check // 只收集可见的未入库商品 const uidsFromVisibleCards = new Set([...document.querySelectorAll(Config.SELECTORS.card)] .filter(isElementInViewport) .filter(card => { // 过滤掉已经确认入库的商品 const link = card.querySelector(Config.SELECTORS.cardLink); if (!link) return false; const url = link.href.split('?')[0]; return !Database.isDone(url); }) .map(card => card.querySelector(Config.SELECTORS.cardLink)?.href.match(/listings\/([a-f0-9-]+)/)?.[1]) .filter(Boolean)); // 收集已经入库失败的商品 const uidsFromFailedList = new Set(State.db.failed.map(task => task.uid)); // 合并两类商品ID const allUidsToCheck = Array.from(new Set([...uidsFromVisibleCards, ...uidsFromFailedList])); if (allUidsToCheck.length === 0) { Utils.logger('info', '[Fab DOM Refresh] 没有未入库的可见商品或入库失败的商品需要检查。'); return; } Utils.logger('info', `[Fab DOM Refresh] 正在分批检查 ${uidsFromVisibleCards.size} 个未入库的可见商品和 ${uidsFromFailedList.size} 个入库失败的商品...`); // Step 2: Process UIDs in chunks const ownedUids = new Set(); for (let i = 0; i < allUidsToCheck.length; i += API_CHUNK_SIZE) { const chunk = allUidsToCheck.slice(i, i + API_CHUNK_SIZE); const apiUrl = new URL(API_ENDPOINT); chunk.forEach(uid => apiUrl.searchParams.append('listing_ids', uid)); Utils.logger('info', `[Fab DOM Refresh] 正在处理批次 ${Math.floor(i / API_CHUNK_SIZE) + 1}... (${chunk.length}个项目)`); const response = await fetch(apiUrl.href, { headers: { 'accept': 'application/json, text/plain, */*', 'x-csrftoken': csrfToken, 'x-requested-with': 'XMLHttpRequest' } }); if (!response.ok) { Utils.logger('warn', `批次处理失败,状态码: ${response.status}。将跳过此批次。`); continue; // Skip to next chunk } const rawData = await response.json(); // 使用API.extractStateData处理可能的不同格式的响应 const data = API.extractStateData(rawData, 'RefreshStates'); if (!data || !Array.isArray(data)) { Utils.logger('warn', `API返回的数据格式异常: ${JSON.stringify(rawData).substring(0, 200)}...`); continue; // Skip to next chunk if data format is unexpected } data.filter(item => item.acquired).forEach(item => ownedUids.add(item.uid)); // Add a small delay between chunks to be safe if (allUidsToCheck.length > i + API_CHUNK_SIZE) { await new Promise(r => setTimeout(r, 250)); } } Utils.logger('info', `[Fab DOM Refresh] API查询完成,共确认 ${ownedUids.size} 个已拥有的项目。`); // Step 3: Update database based on all results let dbUpdated = false; const langPath = State.lang === 'zh' ? '/zh-cn' : ''; if (ownedUids.size > 0) { const initialFailedCount = State.db.failed.length; State.db.failed = State.db.failed.filter(failedTask => !ownedUids.has(failedTask.uid)); if (State.db.failed.length < initialFailedCount) { dbUpdated = true; ownedUids.forEach(uid => { const url = `${window.location.origin}${langPath}/listings/${uid}`; if (!Database.isDone(url)) { State.db.done.push(url); } }); Utils.logger('info', `[Fab DB Sync] 从"失败"列表中清除了 ${initialFailedCount - State.db.failed.length} 个已手动完成的商品。`); } } // Step 4: Update UI for visible cards const uidToCardMap = new Map([...document.querySelectorAll(Config.SELECTORS.card)] .filter(isElementInViewport) .map(card => { const uid = card.querySelector(Config.SELECTORS.cardLink)?.href.match(/listings\/([a-f0-9-]+)/)?.[1]; return uid ? [uid, card] : null; }).filter(Boolean)); let updatedCount = 0; uidToCardMap.forEach((card, uid) => { const isOwned = ownedUids.has(uid); // 不再手动修改DOM元素,只更新计数 if (isOwned) { updatedCount++; } }); if (dbUpdated) { await Database.saveFailed(); await Database.saveDone(); } Utils.logger('info', `[Fab DOM Refresh] Complete. Updated ${updatedCount} visible card states.`); TaskRunner.runHideOrShow(); } catch (e) { Utils.logger('error', '[Fab DOM Refresh] An error occurred:', e); alert('API 刷新失败。请检查控制台中的错误信息,并确认您已登录。'); } }, retryFailedTasks: async () => { if (State.db.failed.length === 0) { Utils.logger('info', 'No failed tasks to retry.'); return; } const count = State.db.failed.length; Utils.logger('info', `Re-queuing ${count} failed tasks...`); State.db.todo.push(...State.db.failed); // Append failed tasks to the end of the todo list State.db.failed = []; // Clear the failed list await Database.saveFailed(); Utils.logger('info', `${count} tasks moved from Failed to To-Do list.`); UI.update(); // Force immediate UI update }, // --- Core Logic Functions --- reconWithApi: async (url = null) => { if (!State.isReconning) return; try { // 不再主动发送API请求,而是使用网页原生请求的数据 Utils.logger('info', `[优化] 不再主动发送API请求,而是使用网页原生请求的数据`); Utils.logger('info', `[优化] 当前等待列表中有 ${DataCache.waitingList.size} 个商品ID等待更新`); // 更新UI显示 if (State.UI.reconProgressDisplay) { State.UI.reconProgressDisplay.textContent = `使用网页原生请求,等待中: ${DataCache.waitingList.size}`; } // 结束扫描 State.isReconning = false; await GM_deleteValue(Config.DB_KEYS.NEXT_URL); Utils.logger('info', Utils.getText('log_recon_end')); UI.update(); return; // 以下代码不再执行 const searchData = null; // 保存商品数据到缓存 if (searchData.results && Array.isArray(searchData.results)) { DataCache.saveListings(searchData.results); } const initialResultsCount = searchData.results?.length || 0; State.totalTasks += initialResultsCount; // 检查是否有结果 if (!searchData.results || initialResultsCount === 0) { State.isReconning = false; await GM_deleteValue(Config.DB_KEYS.NEXT_URL); Utils.logger('info', Utils.getText('log_recon_end')); UI.update(); return; } // 过滤有效的商品数据 const validResults = searchData.results.filter(item => { const hasUid = typeof item.uid === 'string' && item.uid.length > 5; const hasTitle = typeof item.title === 'string' && item.title.length > 0; const hasOffer = item.startingPrice && typeof item.startingPrice.offerId === 'string' && item.startingPrice.offerId.length > 0; return hasUid && hasTitle && hasOffer; }); // 提取候选项 const candidates = validResults.map(item => ({ uid: item.uid, offerId: item.startingPrice?.offerId })).filter(item => { const itemUrl = `${window.location.origin}${langPath}/listings/${item.uid}`; const isFailed = State.db.failed.some(failedTask => failedTask.uid === item.uid); return !Database.isDone(itemUrl) && !Database.isTodo(itemUrl) && !isFailed; }); const initiallySkippedCount = initialResultsCount - candidates.length; State.completedTasks += initiallySkippedCount; // 如果没有候选项,继续下一页 if (candidates.length === 0) { const nextUrl = searchData.next; if (nextUrl && State.isReconning) { await GM_setValue(Config.DB_KEYS.NEXT_URL, nextUrl); await new Promise(r => setTimeout(r, 300)); TaskRunner.reconWithApi(nextUrl); } else { State.isReconning = false; await GM_deleteValue(Config.DB_KEYS.NEXT_URL); Utils.logger('info', Utils.getText('log_recon_end')); } UI.update(); return; } // 检查拥有状态 Utils.logger('info', `[优化] 正在检查 ${candidates.length} 个候选项的拥有状态...`); const uids = candidates.map(item => item.uid); const statesData = await API.checkItemsOwnership(uids); // 提取未拥有的商品 const ownedUids = new Set(statesData.filter(s => s && s.acquired).map(s => s.uid)); const notOwnedItems = candidates.filter(item => !ownedUids.has(item.uid)); // 更新已拥有计数 State.completedTasks += candidates.length - notOwnedItems.length; if (notOwnedItems.length === 0) { Utils.logger('info', "本批次中没有发现未拥有的商品。"); } else { // 验证价格 Utils.logger('info', `[优化] 正在验证 ${notOwnedItems.length} 个未拥有商品的价格...`); const offerIds = notOwnedItems.map(item => item.offerId).filter(Boolean); const pricesData = await API.checkItemsPrices(offerIds); // 创建价格映射 const priceMap = new Map(); pricesData.forEach(offer => { if (offer && offer.offerId) { priceMap.set(offer.offerId, offer); } }); // 添加免费商品到任务列表 const newTasks = []; notOwnedItems.forEach(item => { const priceInfo = priceMap.get(item.offerId); const originalItem = validResults.find(r => r.uid === item.uid); if (priceInfo && priceInfo.price === 0 && originalItem) { const task = { name: originalItem.title, url: `${window.location.origin}${langPath}/listings/${item.uid}`, type: 'detail', uid: item.uid }; newTasks.push(task); } }); if (newTasks.length > 0) { Utils.logger('info', Utils.getText('log_api_owned_done', { newCount: newTasks.length })); State.db.todo = State.db.todo.concat(newTasks); } else { Utils.logger('info', "找到未拥有的商品,但价格验证后没有真正免费的商品。"); } } // 处理分页 const nextUrl = searchData.next; if (nextUrl && State.isReconning) { await GM_setValue(Config.DB_KEYS.NEXT_URL, nextUrl); await new Promise(r => setTimeout(r, 500)); // 限速保护 TaskRunner.reconWithApi(nextUrl); } else { State.isReconning = false; await GM_deleteValue(Config.DB_KEYS.NEXT_URL); Utils.logger('info', Utils.getText('log_recon_end')); } } catch (error) { Utils.logger('error', `API扫描出错: ${error.message}`); if (error.message && error.message.includes('429')) { Utils.logger('warn', '检测到429错误,可能是请求过于频繁。将暂停扫描。'); State.isReconning = false; } UI.update(); } }, // This is the watchdog timer that patrols for stalled workers. runWatchdog: () => { if (State.watchdogTimer) clearInterval(State.watchdogTimer); // Clear any existing timer State.watchdogTimer = setInterval(async () => { // 如果当前实例不是活跃实例,不执行监控 if (!InstanceManager.isActive) return; if (!State.isExecuting || Object.keys(State.runningWorkers).length === 0) { clearInterval(State.watchdogTimer); State.watchdogTimer = null; return; } const now = Date.now(); const STALL_TIMEOUT = Config.WORKER_TIMEOUT; // 使用配置的超时时间 const stalledWorkers = []; // 先收集所有超时的工作标签页,避免在循环中修改对象 for (const workerId in State.runningWorkers) { const workerInfo = State.runningWorkers[workerId]; // 只处理由当前实例创建的工作标签页 if (workerInfo.instanceId !== Config.INSTANCE_ID) continue; if (now - workerInfo.startTime > STALL_TIMEOUT) { stalledWorkers.push({ workerId, task: workerInfo.task }); } } // 如果有超时的工作标签页,处理它们 if (stalledWorkers.length > 0) { Utils.logger('warn', `发现 ${stalledWorkers.length} 个超时的工作标签页,正在清理...`); // 逐个处理超时的工作标签页 for (const stalledWorker of stalledWorkers) { const { workerId, task } = stalledWorker; Utils.logger('error', `🚨 WATCHDOG: Worker [${workerId.substring(0,12)}] has stalled!`); // 1. Remove from To-Do State.db.todo = State.db.todo.filter(t => t.uid !== task.uid); await Database.saveTodo(); // 2. Add to Failed if (!State.db.failed.some(f => f.uid === task.uid)) { State.db.failed.push(task); await Database.saveFailed(); } State.executionFailedTasks++; // 3. Clean up worker delete State.runningWorkers[workerId]; State.activeWorkers--; // 删除任务数据 await GM_deleteValue(workerId); } Utils.logger('info', `已清理 ${stalledWorkers.length} 个超时的工作标签页。剩余活动工作标签页: ${State.activeWorkers}`); // 4. Update UI UI.update(); // 5. 延迟一段时间后继续派发任务 setTimeout(() => { if (State.isExecuting && State.activeWorkers < Config.MAX_CONCURRENT_WORKERS && State.db.todo.length > 0) { TaskRunner.executeBatch(); } }, 2000); } }, 5000); // Check every 5 seconds }, executeBatch: async () => { // 只有主页面才需要检查是否是活跃实例 if (!State.isWorkerTab && !InstanceManager.isActive) { Utils.logger('warn', '当前实例不是活跃实例,不执行任务。'); return; } if (!State.isExecuting) return; // 防止重复执行 if (State.isDispatchingTasks) { Utils.logger('info', '正在派发任务中,请稍候...'); return; } // 设置派发任务标志 State.isDispatchingTasks = true; try { // Stop condition for the entire execution process if (State.db.todo.length === 0 && State.activeWorkers === 0) { Utils.logger('info', '✅ 🎉 All tasks have been completed!'); State.isExecuting = false; // 保存执行状态 Database.saveExecutingState(); // 保存待办列表(虽然为空,但仍需保存以更新存储) Database.saveTodo(); if (State.watchdogTimer) { clearInterval(State.watchdogTimer); State.watchdogTimer = null; } // 关闭所有可能残留的工作标签页 TaskRunner.closeAllWorkerTabs(); UI.update(); State.isDispatchingTasks = false; return; } // 如果处于限速状态,记录日志但继续执行任务 if (State.appStatus === 'RATE_LIMITED') { Utils.logger('info', '当前处于限速状态,但仍将继续执行待办任务...'); } // 限制最大活动工作标签页数量 if (State.activeWorkers >= Config.MAX_CONCURRENT_WORKERS) { Utils.logger('info', `已达到最大并发工作标签页数量 (${Config.MAX_CONCURRENT_WORKERS}),等待现有任务完成...`); State.isDispatchingTasks = false; return; } // --- DISPATCHER FOR DETAIL TASKS --- // 创建一个当前正在执行的任务UID集合,用于防止重复派发 const inFlightUIDs = new Set(Object.values(State.runningWorkers).map(w => w.task.uid)); // 创建一个副本,避免在迭代过程中修改原数组 const todoList = [...State.db.todo]; let dispatchedCount = 0; // 创建一个集合,记录本次派发的任务UID const dispatchedUIDs = new Set(); for (const task of todoList) { if (State.activeWorkers >= Config.MAX_CONCURRENT_WORKERS) break; // 如果任务已经在执行中,跳过 if (inFlightUIDs.has(task.uid) || dispatchedUIDs.has(task.uid)) { Utils.logger('info', `任务 ${task.name} 已在执行中,跳过。`); continue; } // 如果任务已经在完成列表中,从待办列表移除并跳过 if (Database.isDone(task.url)) { Utils.logger('info', `任务 ${task.name} 已完成,从待办列表中移除。`); State.db.todo = State.db.todo.filter(t => t.uid !== task.uid); Database.saveTodo(); continue; } // 记录本次派发的任务 dispatchedUIDs.add(task.uid); State.activeWorkers++; dispatchedCount++; const workerId = `worker_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; State.runningWorkers[workerId] = { task, startTime: Date.now(), instanceId: Config.INSTANCE_ID // 记录创建此工作标签页的实例ID }; Utils.logger('info', `🚀 Dispatching Worker [${workerId.substring(0, 12)}...] for: ${task.name}`); await GM_setValue(workerId, { task, instanceId: Config.INSTANCE_ID // 在任务数据中也记录实例ID }); const workerUrl = new URL(task.url); workerUrl.searchParams.set('workerId', workerId); // 使用active:false确保标签页在后台打开,并使用insert:true确保标签页在当前标签页之后打开 GM_openInTab(workerUrl.href, { active: false, insert: true }); // 等待一小段时间再派发下一个任务,避免浏览器同时打开太多标签页 await new Promise(resolve => setTimeout(resolve, 500)); } if (dispatchedCount > 0) { Utils.logger('info', `本批次派发了 ${dispatchedCount} 个任务。`); } if (!State.watchdogTimer && State.activeWorkers > 0) { TaskRunner.runWatchdog(); } UI.update(); } finally { // 无论如何都要重置派发任务标志 State.isDispatchingTasks = false; } }, // 添加一个方法来关闭所有工作标签页 closeAllWorkerTabs: () => { // 目前没有直接的方法可以关闭由GM_openInTab打开的标签页 // 但我们可以清理相关的状态 const workerIds = Object.keys(State.runningWorkers); if (workerIds.length > 0) { Utils.logger('info', `正在清理 ${workerIds.length} 个工作标签页的状态...`); for (const workerId of workerIds) { GM_deleteValue(workerId); } State.runningWorkers = {}; State.activeWorkers = 0; Utils.logger('info', '已清理所有工作标签页的状态。'); } }, processDetailPage: async () => { const urlParams = new URLSearchParams(window.location.search); const workerId = urlParams.get('workerId'); // If there's no workerId, this is not a worker tab, so we do nothing. if (!workerId) return; // 标记当前标签页为工作标签页,避免执行主脚本逻辑 State.isWorkerTab = true; State.workerTaskId = workerId; // 记录工作标签页的启动时间 const startTime = Date.now(); let hasReported = false; let closeAttempted = false; // 设置一个定时器,确保工作标签页最终会关闭 const forceCloseTimer = setTimeout(() => { if (!closeAttempted) { console.log('强制关闭工作标签页'); try { window.close(); } catch (e) { console.error('关闭工作标签页失败:', e); } } }, 60000); // 60秒后强制关闭 try { // This is a safety check. If the main tab stops execution, it might delete the task. const payload = await GM_getValue(workerId); if (!payload || !payload.task) { Utils.logger('info', '任务数据已被清理,工作标签页将关闭。'); closeWorkerTab(); return; } // 检查创建此工作标签页的实例ID是否与当前活跃实例一致 const activeInstance = await GM_getValue('fab_active_instance', null); if (activeInstance && activeInstance.id !== payload.instanceId) { Utils.logger('warn', `此工作标签页由实例 [${payload.instanceId}] 创建,但当前活跃实例是 [${activeInstance.id}]。将关闭此标签页。`); await GM_deleteValue(workerId); // 清理任务数据 closeWorkerTab(); return; } const currentTask = payload.task; const logBuffer = [`[${workerId.substring(0, 12)}] Started: ${currentTask.name}`]; let success = false; try { // API-First Ownership Check... try { const csrfToken = Utils.getCookie('fab_csrftoken'); if (!csrfToken) throw new Error("CSRF token not found for API check."); const statesUrl = new URL('https://www.fab.com/i/users/me/listings-states'); statesUrl.searchParams.append('listing_ids', currentTask.uid); const response = await API.gmFetch({ method: 'GET', url: statesUrl.href, headers: { 'x-csrftoken': csrfToken, 'x-requested-with': 'XMLHttpRequest' } }); let statesData; try { statesData = JSON.parse(response.responseText); if (!Array.isArray(statesData)) { logBuffer.push('API返回的数据不是数组格式,这可能是API变更导致的'); // 尝试提取数组数据 statesData = API.extractStateData(statesData, 'SingleItemCheck'); } } catch (e) { logBuffer.push(`解析API响应失败: ${e.message}`); statesData = []; } const isOwned = Array.isArray(statesData) && statesData.some(s => s && s.uid === currentTask.uid && s.acquired); if (isOwned) { logBuffer.push(`API check confirms item is already owned.`); success = true; } else { logBuffer.push(`API check confirms item is not owned. Proceeding to UI interaction.`); } } catch (apiError) { logBuffer.push(`API ownership check failed: ${apiError.message}. Falling back to UI-based check.`); } if (!success) { try { const isItemOwned = () => { const criteria = Config.OWNED_SUCCESS_CRITERIA; const snackbar = document.querySelector('.fabkit-Snackbar-root, div[class*="Toast-root"]'); if (snackbar && criteria.snackbarText.some(text => snackbar.textContent.includes(text))) return { owned: true, reason: `Snackbar text "${snackbar.textContent}"` }; const successHeader = document.querySelector('h2'); if (successHeader && criteria.h2Text.some(text => successHeader.textContent.includes(text))) return { owned: true, reason: `H2 text "${successHeader.textContent}"` }; const allButtons = [...document.querySelectorAll('button, a.fabkit-Button-root')]; const ownedButton = allButtons.find(btn => criteria.buttonTexts.some(keyword => btn.textContent.includes(keyword))); if (ownedButton) return { owned: true, reason: `Button text "${ownedButton.textContent}"` }; return { owned: false }; }; const initialState = isItemOwned(); if (initialState.owned) { logBuffer.push(`Item already owned on page load (UI Fallback PASS: ${initialState.reason}).`); success = true; } else { // 检查是否需要选择许可证 const licenseButton = [...document.querySelectorAll('button')].find(btn => btn.textContent.includes('选择许可') || btn.textContent.includes('Select license') ); if (licenseButton) { logBuffer.push(`Multi-license item detected. Setting up observer for dropdown.`); try { await new Promise((resolve, reject) => { const observer = new MutationObserver((mutationsList, obs) => { for (const mutation of mutationsList) { if (mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; // 查找"免费"或"个人"选项 const freeTextElement = Array.from(node.querySelectorAll('span, div')).find(el => Array.from(el.childNodes).some(cn => cn.nodeType === 3 && (cn.textContent.trim() === '免费' || cn.textContent.trim() === 'Free' || cn.textContent.trim() === '个人' || cn.textContent.trim() === 'Personal') ) ); if (freeTextElement) { const clickableParent = freeTextElement.closest('[role="option"], button, label, input[type="radio"]'); if (clickableParent) { logBuffer.push(`Found free/personal license option, clicking it.`); Utils.deepClick(clickableParent); observer.disconnect(); resolve(); return; } } } } } }); observer.observe(document.body, { childList: true, subtree: true }); logBuffer.push(`Clicking license button to open dropdown.`); Utils.deepClick(licenseButton); // First click attempt // 有时第一次点击可能不成功,1.5秒后再试一次 setTimeout(() => { logBuffer.push(`Second attempt to click license button.`); Utils.deepClick(licenseButton); }, 1500); // 如果5秒内没有出现下拉菜单,则超时 setTimeout(() => { observer.disconnect(); reject(new Error('Timeout (5s): The free/personal option did not appear.')); }, 5000); }); // 许可选择后等待UI更新 logBuffer.push(`License selected, waiting for UI update.`); await new Promise(r => setTimeout(r, 1000)); // 重新检查是否已拥有 if (isItemOwned().owned) { logBuffer.push(`Item became owned after license selection.`); success = true; } } catch (licenseError) { logBuffer.push(`License selection failed: ${licenseError.message}`); } } // 如果许可选择后仍未成功,或者不需要选择许可,尝试点击添加按钮 if (!success) { const actionButton = [...document.querySelectorAll('button')].find(btn => btn.textContent.includes('添加到我的库') || btn.textContent.includes('Add to my library') ); if (actionButton) { logBuffer.push(`Found add button, clicking it.`); Utils.deepClick(actionButton); // 等待添加操作完成 try { await new Promise((resolve, reject) => { const timeout = 25000; // 25秒超时 const interval = setInterval(() => { const currentState = isItemOwned(); if (currentState.owned) { logBuffer.push(`Item became owned after clicking add button: ${currentState.reason}`); success = true; clearInterval(interval); resolve(); } }, 500); // 每500ms检查一次 setTimeout(() => { clearInterval(interval); reject(new Error(`Timeout waiting for page to enter an 'owned' state.`)); }, timeout); }); } catch (timeoutError) { logBuffer.push(`Timeout waiting for ownership: ${timeoutError.message}`); } } else { logBuffer.push(`Could not find an add button.`); } } } } catch (uiError) { logBuffer.push(`UI interaction failed: ${uiError.message}`); } } } catch (error) { logBuffer.push(`A critical error occurred: ${error.message}`); success = false; } finally { try { // 标记为已报告 hasReported = true; // 报告任务结果 await GM_setValue(Config.DB_KEYS.WORKER_DONE, { workerId: workerId, success: success, logs: logBuffer, task: currentTask, instanceId: payload.instanceId, executionTime: Date.now() - startTime }); } catch (error) { console.error('Error setting worker done value:', error); } try { await GM_deleteValue(workerId); // 清理任务数据 } catch (error) { console.error('Error deleting worker value:', error); } // 确保工作标签页在报告完成后关闭 closeWorkerTab(); } } catch (error) { Utils.logger('error', `Worker tab error: ${error.message}`); closeWorkerTab(); } // 关闭工作标签页的函数 function closeWorkerTab() { closeAttempted = true; clearTimeout(forceCloseTimer); // 如果尚未报告结果,尝试报告失败 if (!hasReported && workerId) { try { GM_setValue(Config.DB_KEYS.WORKER_DONE, { workerId: workerId, success: false, logs: ['Worker tab closed before completion'], task: payload?.task, instanceId: payload?.instanceId, executionTime: Date.now() - startTime }); } catch (e) { // 忽略错误 } } try { window.close(); } catch (error) { Utils.logger('error', `关闭工作标签页失败: ${error.message}`); // 如果关闭失败,尝试其他方法 try { window.location.href = 'about:blank'; } catch (e) { Utils.logger('error', `重定向失败: ${e.message}`); } } } }, // 删除这个未使用的函数 // This function is now fully obsolete. // advanceDetailTask: async () => {}, runHideOrShow: () => { // 无论是否在限速状态下,都应该执行隐藏功能 State.hiddenThisPageCount = 0; const cards = document.querySelectorAll(Config.SELECTORS.card); // 添加一个计数器,用于跟踪实际隐藏的卡片数量 let actuallyHidden = 0; // 首先检查是否有未加载完成的卡片 let hasUnsettledCards = false; const unsettledCards = []; // 检查卡片是否已加载完成的函数 const isCardSettled = (card) => { // 检查卡片是否有价格、免费标签或已拥有标签 return card.querySelector(`${Config.SELECTORS.freeStatus}, ${Config.SELECTORS.ownedStatus}`) !== null; }; // 检查是否有未加载完成的卡片 cards.forEach(card => { if (!isCardSettled(card)) { hasUnsettledCards = true; unsettledCards.push(card); } }); // 如果有未加载完成的卡片,延迟执行隐藏操作 if (hasUnsettledCards && unsettledCards.length > 0) { Utils.logger('info', `检测到 ${unsettledCards.length} 张卡片尚未加载完成,延迟隐藏操作...`); // 设置一个较长的延迟,等待卡片加载完成 setTimeout(() => { Utils.logger('info', `延迟后重新执行隐藏操作,确保卡片已加载完成`); TaskRunner.runHideOrShow(); }, 2000); // 延迟2秒 return; // 直接返回,等待下次执行 } // 首先收集所有需要隐藏的卡片 const cardsToHide = []; // 添加一个数据属性来标记已处理的卡片,避免重复处理 cards.forEach(card => { // 检查卡片是否已经被处理过 const isProcessed = card.getAttribute('data-fab-processed') === 'true'; // 如果卡片已经被处理且已经隐藏,则不需要再次处理 if (isProcessed && card.style.display === 'none') { State.hiddenThisPageCount++; return; } const isFinished = TaskRunner.isCardFinished(card); if (State.hideSaved && isFinished) { cardsToHide.push(card); State.hiddenThisPageCount++; // 标记卡片为已处理 card.setAttribute('data-fab-processed', 'true'); } else { // 如果不需要隐藏,也标记为已处理 card.setAttribute('data-fab-processed', 'true'); } }); // 如果有需要隐藏的卡片,使用更长的初始延迟和更慢的隐藏速度 if (cardsToHide.length > 0) { Utils.logger('info', `准备隐藏 ${cardsToHide.length} 张卡片,将使用更长的延迟...`); // 随机打乱卡片顺序,使隐藏更加随机 cardsToHide.sort(() => Math.random() - 0.5); // 分批次隐藏卡片,每批次最多10张(减少批次大小) const batchSize = 10; const batches = Math.ceil(cardsToHide.length / batchSize); // 设置一个初始延迟,确保页面有足够时间加载 const initialDelay = 1000; // 1秒的初始延迟 for (let i = 0; i < batches; i++) { const start = i * batchSize; const end = Math.min(start + batchSize, cardsToHide.length); const currentBatch = cardsToHide.slice(start, end); // 为每个批次设置一个更长的延迟,增加延迟时间 const batchDelay = initialDelay + i * 300 + Math.random() * 300; setTimeout(() => { currentBatch.forEach((card, index) => { // 为每张卡片设置一个更长的随机延迟 const cardDelay = index * 50 + Math.random() * 100; setTimeout(() => { card.style.display = 'none'; actuallyHidden++; // 当所有卡片都隐藏后,更新UI if (actuallyHidden === cardsToHide.length) { Utils.logger('info', `已完成所有 ${actuallyHidden} 张卡片的隐藏`); // 延迟更新UI,确保DOM已经完全更新 setTimeout(() => { UI.update(); // 隐藏完成后检查可见性并决定是否刷新 TaskRunner.checkVisibilityAndRefresh(); }, 300); } }, cardDelay); }); }, batchDelay); } } // 确保所有不应该隐藏的卡片都是可见的 if (State.hideSaved) { // 找出所有不应该隐藏的卡片 const visibleCards = Array.from(cards).filter(card => { // 不隐藏未完成的卡片 return !TaskRunner.isCardFinished(card); }); // 显示这些卡片(如果它们之前被隐藏了) visibleCards.forEach(card => { card.style.display = ''; }); // 只有在没有需要隐藏的卡片时才立即更新UI和检查可见性 if (cardsToHide.length === 0) { UI.update(); TaskRunner.checkVisibilityAndRefresh(); } } else { // 如果没有隐藏功能,正常显示所有卡片并更新UI cards.forEach(card => { card.style.display = ''; }); UI.update(); } }, // 新增:检查可见性并决定是否刷新的方法 checkVisibilityAndRefresh: () => { // 计算实际可见的商品数量 const cards = document.querySelectorAll(Config.SELECTORS.card); // 重新检查所有卡片,确保隐藏状态正确 let needsReprocessing = false; cards.forEach(card => { const isProcessed = card.getAttribute('data-fab-processed') === 'true'; if (!isProcessed) { needsReprocessing = true; } }); // 如果发现未处理的卡片,重新执行隐藏逻辑 if (needsReprocessing) { Utils.logger('info', '检测到未处理的卡片,重新执行隐藏逻辑'); setTimeout(() => { TaskRunner.runHideOrShow(); }, 100); return; } // 使用更准确的方式检查元素是否可见 const visibleCards = Array.from(cards).filter(card => { // 检查元素自身的display属性 if (card.style.display === 'none') return false; // 检查是否被CSS规则隐藏 const computedStyle = window.getComputedStyle(card); return computedStyle.display !== 'none' && computedStyle.visibility !== 'hidden'; }).length; // 更新真实的可见商品数量 Utils.logger('info', `👁️ 隐藏后实际可见商品数: ${visibleCards},隐藏商品数: ${State.hiddenThisPageCount}`); // 更新UI上显示的可见商品数 const visibleCountElement = document.getElementById('fab-status-visible'); if (visibleCountElement) { visibleCountElement.textContent = visibleCards.toString(); } if (visibleCards === 0) { // 无可见商品,根据状态决定是否刷新 if (State.appStatus === 'RATE_LIMITED' && State.autoRefreshEmptyPage) { // 如果已经安排了刷新,不要重复安排 if (State.isRefreshScheduled) { Utils.logger('info', `已有刷新计划正在进行中,不再安排新的刷新 (无商品可见)`); return; } Utils.logger('info', '🔄 所有商品都已隐藏且处于限速状态,将在2秒后刷新页面...'); // 标记已安排刷新 State.isRefreshScheduled = true; setTimeout(() => { // 再次检查实际可见的商品数量 const currentVisibleCards = Array.from(document.querySelectorAll(Config.SELECTORS.card)) .filter(card => card.style.display !== 'none').length; // 检查是否有待办任务或活动工作线程 if (State.db.todo.length > 0 || State.activeWorkers > 0) { Utils.logger('info', `⏹️ 刷新取消,检测到 ${State.db.todo.length} 个待办任务和 ${State.activeWorkers} 个活动工作线程`); State.isRefreshScheduled = false; // 重置刷新标记 return; } if (currentVisibleCards === 0 && State.appStatus === 'RATE_LIMITED' && State.autoRefreshEmptyPage) { Utils.logger('info', '🔄 执行刷新...'); // 使用更可靠的刷新方式 window.location.href = window.location.href; } else { Utils.logger('info', `⏹️ 刷新取消,检测到 ${currentVisibleCards} 个可见商品`); State.isRefreshScheduled = false; // 重置刷新标记 } }, 2000); } else if (State.appStatus === 'NORMAL' && State.hiddenThisPageCount > 0) { // 正常状态下也没有可见商品,可能是全部隐藏了 // 只记录日志,不提示刷新,也不执行刷新 Utils.logger('info', `👁️ 检测到页面上有 ${State.hiddenThisPageCount} 个隐藏商品,但没有可见商品`); } } }, // 添加一个方法来检查并确保待办任务被执行 ensureTasksAreExecuted: () => { // 如果没有待办任务,不需要执行 if (State.db.todo.length === 0) return; // 如果已经在执行中,不需要重新启动 if (State.isExecuting) { // 如果有待办任务但没有活动工作线程,可能是执行卡住了,尝试重新执行 if (State.activeWorkers === 0) { Utils.logger('info', '检测到有待办任务但没有活动工作线程,尝试重新执行...'); TaskRunner.executeBatch(); } return; } // 如果有待办任务但没有执行,自动开始执行 Utils.logger('info', `检测到有 ${State.db.todo.length} 个待办任务但未执行,自动开始执行...`); TaskRunner.startExecution(); }, // 添加一个方法来批量检查当前页面上所有可见卡片的状态 checkVisibleCardsStatus: async () => { try { // 获取所有可见卡片 const visibleCards = [...document.querySelectorAll(Config.SELECTORS.card)]; // 如果没有可见卡片,直接返回 if (visibleCards.length === 0) { Utils.logger('info', '[Fab DOM Refresh] 没有可见的卡片需要刷新'); return; } // 首先检查是否有未加载完成的卡片 let hasUnsettledCards = false; const unsettledCards = []; // 检查卡片是否已加载完成的函数 const isCardSettled = (card) => { // 检查卡片是否有价格、免费标签或已拥有标签 return card.querySelector(`${Config.SELECTORS.freeStatus}, ${Config.SELECTORS.ownedStatus}`) !== null; }; // 检查是否有未加载完成的卡片 visibleCards.forEach(card => { if (!isCardSettled(card)) { hasUnsettledCards = true; unsettledCards.push(card); } }); // 如果有未加载完成的卡片,等待一段时间后再检查 if (hasUnsettledCards && unsettledCards.length > 0) { Utils.logger('info', `[Fab DOM Refresh] 检测到 ${unsettledCards.length} 张卡片尚未加载完成,等待加载...`); // 等待一段时间后再次检查 await new Promise(resolve => setTimeout(resolve, 3000)); // 重新获取所有可见卡片 return TaskRunner.checkVisibleCardsStatus(); } // 提取卡片的UID和DOM元素 const allItems = []; let confirmedOwned = 0; visibleCards.forEach(card => { const link = card.querySelector(Config.SELECTORS.cardLink); const uidMatch = link?.href.match(/listings\/([a-f0-9-]+)/); if (uidMatch && uidMatch[1]) { const uid = uidMatch[1]; const url = link.href.split('?')[0]; // 移除查询参数 // 检查是否已经在已完成列表中 if (State.db.done.includes(url)) { // 已经知道是已拥有的,不需要再次检查 return; } allItems.push({ uid, url, element: card }); } }); // 如果没有需要检查的项目,直接返回 if (allItems.length === 0) { Utils.logger('info', '[Fab DOM Refresh] 没有需要检查的卡片'); return; } Utils.logger('info', `[Fab DOM Refresh] 正在检查 ${allItems.length} 个项目的状态...`); // 提取所有需要检查的商品ID const uids = allItems.map(item => item.uid); // 使用优化后的API函数检查拥有状态 const statesData = await API.checkItemsOwnership(uids); // 创建已拥有商品ID的集合,便于快速查找 const ownedUids = new Set( statesData .filter(state => state && state.acquired) .map(state => state.uid) ); // 处理结果 for (const item of allItems) { if (ownedUids.has(item.uid)) { // 如果不在已完成列表中,添加 if (!State.db.done.includes(item.url)) { State.db.done.push(item.url); confirmedOwned++; // 不再手动添加"已保存"标记,网页会自动更新 } // 从失败列表中移除 State.db.failed = State.db.failed.filter(f => f.uid !== item.uid); // 从待办列表中移除 State.db.todo = State.db.todo.filter(t => t.uid !== item.uid); } } // 保存更改 if (confirmedOwned > 0) { await Database.saveDone(); await Database.saveFailed(); Utils.logger('info', `[Fab DOM Refresh] API查询完成,共确认 ${confirmedOwned} 个已拥有的项目。`); // 不立即执行隐藏,而是在调用方决定何时执行 Utils.logger('info', `[Fab DOM Refresh] Complete. Updated ${confirmedOwned} visible card states.`); } else { Utils.logger('info', '[Fab DOM Refresh] API查询完成,没有发现新的已拥有项目。'); } } catch (error) { Utils.logger('error', `[Fab DOM Refresh] 检查项目状态时出错: ${error.message}`); // 如果是429错误,进入限速状态并退出 if (error.message && error.message.includes('429')) { RateLimitManager.enterRateLimitedState('[Fab DOM Refresh] 429错误'); } } }, scanAndAddTasks: async (cards) => { // This function should ONLY ever run if auto-add is enabled. if (!State.autoAddOnScroll) return; // 创建一个状态追踪对象 if (!window._apiWaitStatus) { window._apiWaitStatus = { isWaiting: false, pendingCards: [], lastApiActivity: 0, apiCheckInterval: null }; } // 如果已经有等待过程在进行,将当前卡片加入队列 if (window._apiWaitStatus.isWaiting) { window._apiWaitStatus.pendingCards = [...window._apiWaitStatus.pendingCards, ...cards]; Utils.logger('info', `[自动添加] 已有API等待过程在进行,将当前 ${cards.length} 张卡片加入等待队列。`); return; } // 标记开始等待API window._apiWaitStatus.isWaiting = true; window._apiWaitStatus.pendingCards = [...cards]; window._apiWaitStatus.lastApiActivity = Date.now(); Utils.logger('info', `[自动添加] 开始等待API响应,将在API活动停止后处理 ${cards.length} 张卡片...`); // 创建一个函数来检测API活动 const waitForApiCompletion = () => { return new Promise((resolve) => { // 清除之前的检查间隔 if (window._apiWaitStatus.apiCheckInterval) { clearInterval(window._apiWaitStatus.apiCheckInterval); } // 设置一个最大等待时间(10秒) const maxWaitTime = 10000; const startTime = Date.now(); // 监听网络请求 const originalFetch = window.fetch; window.fetch = function(...args) { // 只关注商品状态相关的API请求 const url = args[0]?.toString() || ''; if (url.includes('/listings-states') || url.includes('/listings/search')) { window._apiWaitStatus.lastApiActivity = Date.now(); Utils.logger('debug', `[API监控] 检测到API活动: ${url.substring(0, 50)}...`); } return originalFetch.apply(this, args); }; // 检查API活动的间隔 window._apiWaitStatus.apiCheckInterval = setInterval(() => { const now = Date.now(); const timeSinceLastActivity = now - window._apiWaitStatus.lastApiActivity; const totalWaitTime = now - startTime; // 如果超过最大等待时间,或者API活动停止超过2秒,则认为API已完成 if (totalWaitTime > maxWaitTime || timeSinceLastActivity > 2000) { clearInterval(window._apiWaitStatus.apiCheckInterval); // 恢复原始的fetch函数 window.fetch = originalFetch; if (totalWaitTime > maxWaitTime) { Utils.logger('warn', `[自动添加] API等待超时,已等待 ${totalWaitTime}ms,将继续处理卡片。`); } else { Utils.logger('info', `[自动添加] API活动已停止 ${timeSinceLastActivity}ms,继续处理卡片。`); } resolve(); } }, 200); // 每200ms检查一次 }); }; // 等待API完成 try { await waitForApiCompletion(); } catch (error) { Utils.logger('error', `[自动添加] 等待API时出错: ${error.message}`); } // 处理卡片 const cardsToProcess = [...window._apiWaitStatus.pendingCards]; window._apiWaitStatus.pendingCards = []; window._apiWaitStatus.isWaiting = false; Utils.logger('info', `[自动添加] API等待完成,开始处理 ${cardsToProcess.length} 张卡片...`); // 现在处理卡片 const newlyAddedList = []; let skippedAlreadyOwned = 0; let skippedInTodo = 0; cardsToProcess.forEach(card => { const link = card.querySelector(Config.SELECTORS.cardLink); const url = link ? link.href.split('?')[0] : null; if (!url) return; // 1. 检查是否已经入库或在待办列表中 // 更严格的检查,确保已入库的商品不会被添加到待办列表 // 检查URL是否在完成列表中 if (Database.isDone(url)) { skippedAlreadyOwned++; return; } // 检查URL是否在待办列表中 if (Database.isTodo(url)) { skippedInTodo++; return; } // 检查卡片是否有"已保存"标记 const text = card.textContent || ''; if (text.includes("已保存在我的库中") || text.includes("已保存") || text.includes("Saved to My Library") || text.includes("In your library")) { skippedAlreadyOwned++; return; } // 检查卡片是否有成功图标 const icons = card.querySelectorAll('i.fabkit-Icon--intent-success, i.edsicon-check-circle-filled'); if (icons.length > 0) { skippedAlreadyOwned++; return; } // 从链接中提取UID并检查缓存 const uidMatch = url.match(/listings\/([a-f0-9-]+)/); if (uidMatch && uidMatch[1]) { const uid = uidMatch[1]; // 检查缓存中是否标记为已拥有 if (DataCache.ownedStatus.has(uid)) { const status = DataCache.ownedStatus.get(uid); if (status && status.acquired) { skippedAlreadyOwned++; return; } } } // 2. Must be visibly "Free". This is the most critical filter. const isFree = card.querySelector(Config.SELECTORS.freeStatus) !== null; if (!isFree) { return; } // If it passes all checks, it's a valid new task. const name = card.querySelector('a[aria-label*="创作的"]')?.textContent.trim() || card.querySelector('a[href*="/listings/"]')?.textContent.trim() || 'Untitled'; newlyAddedList.push({ name, url, type: 'detail', uid: url.split('/').pop() }); }); if (newlyAddedList.length > 0 || skippedAlreadyOwned > 0 || skippedInTodo > 0) { if (newlyAddedList.length > 0) { State.db.todo.push(...newlyAddedList); Utils.logger('info', `[自动添加] 新增 ${newlyAddedList.length} 个任务到队列。`); // 保存待办列表到存储 Database.saveTodo(); } // 添加详细的过滤信息日志 if (skippedAlreadyOwned > 0 || skippedInTodo > 0) { Utils.logger('info', `[自动添加] 过滤掉 ${skippedAlreadyOwned} 个已入库商品和 ${skippedInTodo} 个已在待办列表中的商品。`); } // 如果已经在执行,只更新总数 if (State.isExecuting) { State.executionTotalTasks = State.db.todo.length; // 确保任务继续执行 TaskRunner.executeBatch(); } else if (State.autoAddOnScroll) { // 如果启用了自动添加但尚未开始执行,自动开始执行 TaskRunner.startExecution(); } UI.update(); } }, async handleRateLimit(url) { // 使用统一的限速管理器进入限速状态 await RateLimitManager.enterRateLimitedState(url || '网络请求'); }, reportTaskDone: async (task, success) => { try { // 报告任务完成 await GM_setValue(Config.DB_KEYS.WORKER_DONE, { workerId: `worker_task_${task.uid}`, success: success, logs: [`任务${success ? '成功' : '失败'}: ${task.name || task.uid}`], task: task, instanceId: Config.INSTANCE_ID, executionTime: 0 }); Utils.logger('info', `工作标签页报告任务${success ? '成功' : '失败'}: ${task.name || task.uid}`); } catch (error) { Utils.logger('error', `报告任务状态时出错: ${error.message}`); } }, toggleAutoRefreshEmpty: async () => { if (State.isTogglingSetting) return; State.isTogglingSetting = true; State.autoRefreshEmptyPage = !State.autoRefreshEmptyPage; await Database.saveAutoRefreshEmptyPref(); Utils.logger('info', `无商品可见时自动刷新功能已${State.autoRefreshEmptyPage ? '开启' : '关闭'}。`); setTimeout(() => { State.isTogglingSetting = false; }, 200); }, }; // --- 模块八: 用户界面 (User Interface) --- const UI = { create: () => { // New, more robust rule: A detail page is identified by the presence of a main "acquisition" button, // not by its URL, which can be inconsistent. const acquisitionButton = [...document.querySelectorAll('button')].find(btn => [...Config.ACQUISITION_TEXT_SET].some(keyword => btn.textContent.includes(keyword)) ); // The "Download" button is another strong signal. const downloadButton = [...document.querySelectorAll('a[href*="/download/"], button')].find(btn => btn.textContent.includes('下载') || btn.textContent.includes('Download') ); if (acquisitionButton || downloadButton) { const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('workerId')) return false; // Explicitly return false for worker Utils.logger('info', "On a detail page (detected by action buttons), skipping UI creation."); return false; // Explicitly return false to halt further execution } if (document.getElementById(Config.UI_CONTAINER_ID)) return true; // Already created // --- Style Injection --- const styles = ` :root { --bg-color: rgba(28, 28, 30, 0.9); --border-color: rgba(255, 255, 255, 0.15); --text-color-primary: #f5f5f7; --text-color-secondary: #a0a0a5; --radius-l: 12px; --radius-m: 8px; --radius-s: 6px; --blue: #007aff; --pink: #ff2d55; --green: #34c759; --orange: #ff9500; --gray: #8e8e93; --dark-gray: #3a3a3c; --blue-bg: rgba(0, 122, 255, 0.2); } #${Config.UI_CONTAINER_ID} { position: fixed; bottom: 20px; right: 20px; z-index: 9999; background: var(--bg-color); backdrop-filter: blur(15px) saturate(1.8); -webkit-backdrop-filter: blur(15px) saturate(1.8); border: 1px solid var(--border-color); border-radius: var(--radius-l); color: var(--text-color-primary); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; width: 300px; font-size: 14px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); } /* FINAL FIX: Apply a robust box model to all elements within the container */ #${Config.UI_CONTAINER_ID} *, #${Config.UI_CONTAINER_ID} *::before, #${Config.UI_CONTAINER_ID} *::after { box-sizing: border-box; } .fab-helper-tabs { display: flex; border-bottom: 1px solid var(--border-color); } .fab-helper-tabs button { flex: 1; padding: 10px 0; font-size: 14px; font-weight: 500; cursor: pointer; background: transparent; border: none; color: var(--text-color-secondary); transition: color 0.2s, border-bottom 0.2s; border-bottom: 2px solid transparent; /* --- FIX: Center align tab text --- */ display: flex; justify-content: center; align-items: center; } .fab-helper-tabs button.active { color: var(--text-color-primary); border-bottom: 2px solid var(--blue); } .fab-helper-tab-content { padding: 12px; } .fab-helper-status-bar { display: flex; flex-wrap: wrap; gap: 6px; /* REMOVED: No longer needed at the bottom of the log */ /* margin-bottom: 12px; */ } .fab-helper-status-item { background: var(--dark-gray); padding: 8px 6px; border-radius: var(--radius-m); font-size: 12px; color: var(--text-color-secondary); display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 2px; min-width: 0; flex-grow: 1; /* This formula is now correct thanks to box-sizing: border-box */ flex-basis: calc((100% - 12px) / 3); /* (100% width - 2*6px gap) / 3 columns */ } .fab-helper-status-label { display: flex; align-items: center; justify-content: center; gap: 4px; white-space: nowrap; /* REMOVED: No longer needed with a wrapping layout */ } .fab-helper-status-item span { display: block; font-size: 18px; font-weight: 600; color: #fff; margin-top: 0; } .fab-helper-execute-btn { width: 100%; border: none; border-radius: var(--radius-m); padding: 12px 14px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; color: #fff; background: var(--blue); margin-bottom: 12px; /* --- FIX: Center align button content --- */ display: flex; justify-content: center; align-items: center; gap: 8px; /* Add space between icon and text */ } .fab-helper-execute-btn.executing { background: var(--pink); } .fab-helper-actions { display: flex; gap: 8px; } .fab-helper-actions button { flex: 1; /* RESTORED: Distribute space equally */ min-width: 0; /* ADDED BACK: Crucial for flex shrinking */ display: flex; align-items: center; justify-content: center; gap: 5px; background: var(--dark-gray); border: none; border-radius: var(--radius-m); color: var(--text-color-primary); padding: 8px 6px; /* CRITICAL FIX: Reduced horizontal padding */ cursor: pointer; transition: background-color 0.2s; white-space: nowrap; font-size: 13.5px; font-weight: normal; } .fab-helper-actions button:hover { background: #4a4a4c; } .fab-log-container { padding: 0 12px 12px 12px; /* FIX: Swapped border and margin from top to bottom */ border-bottom: 1px solid var(--border-color); margin-bottom: 12px; } .fab-log-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; margin-top: 8px; } .fab-log-header span { font-size: 14px; font-weight: 500; color: var(--text-color-secondary); } .fab-log-controls button { background: transparent; border: none; color: var(--text-color-secondary); cursor: pointer; padding: 4px; font-size: 18px; line-height: 1; } #${Config.UI_LOG_ID} { background: rgba(10,10,10,0.85); color: #ddd; font-size: 11px; line-height: 1.4; padding: 8px; border-radius: var(--radius-m); max-height: 150px; overflow-y: auto; min-height: 50px; display: flex; flex-direction: column-reverse; box-shadow: inset 0 1px 4px rgba(0,0,0,0.2); scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.3) rgba(0,0,0,0.2); } /* 自定义滚动条样式 */ #${Config.UI_LOG_ID}::-webkit-scrollbar { width: 8px; height: 8px; } #${Config.UI_LOG_ID}::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 4px; } #${Config.UI_LOG_ID}::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.3); border-radius: 4px; } #${Config.UI_LOG_ID}::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.5); } /* 添加状态周期历史记录的滚动条样式 */ #${Config.UI_DEBUG_HISTORY_ID}, .fab-debug-history-container { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.3) rgba(0,0,0,0.2); } #${Config.UI_DEBUG_HISTORY_ID}::-webkit-scrollbar, .fab-debug-history-container::-webkit-scrollbar { width: 8px; height: 8px; } #${Config.UI_DEBUG_HISTORY_ID}::-webkit-scrollbar-track, .fab-debug-history-container::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); border-radius: 4px; } #${Config.UI_DEBUG_HISTORY_ID}::-webkit-scrollbar-thumb, .fab-debug-history-container::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.3); border-radius: 4px; } #${Config.UI_DEBUG_HISTORY_ID}::-webkit-scrollbar-thumb:hover, .fab-debug-history-container::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.5); } @keyframes fab-pulse { 0% { box-shadow: 0 0 0 0 rgba(0, 122, 255, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(0, 122, 255, 0); } 100% { box-shadow: 0 0 0 0 rgba(0, 122, 255, 0); } } .fab-helper-pulse { animation: fab-pulse 2s infinite; } .fab-setting-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--border-color); } .fab-setting-row:last-child { border-bottom: none; } .fab-setting-label { font-size: 14px; } .fab-toggle-switch { position: relative; display: inline-block; width: 44px; height: 24px; } .fab-toggle-switch input { opacity: 0; width: 0; height: 0; } .fab-toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--dark-gray); transition: .4s; border-radius: 24px; } .fab-toggle-slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .fab-toggle-slider { background-color: var(--blue); } input:checked + .fab-toggle-slider:before { transform: translateX(20px); } `; const styleSheet = document.createElement("style"); styleSheet.type = "text/css"; styleSheet.innerText = styles; document.head.appendChild(styleSheet); const container = document.createElement('div'); container.id = Config.UI_CONTAINER_ID; State.UI.container = container; // --- Header with Version --- const header = document.createElement('div'); header.style.cssText = 'padding: 8px 12px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center;'; const title = document.createElement('span'); title.textContent = 'Fab Helper'; title.style.fontWeight = '600'; const version = document.createElement('span'); version.textContent = `v${GM_info.script.version}`; version.style.cssText = 'font-size: 12px; color: var(--text-color-secondary); background: var(--dark-gray); padding: 2px 5px; border-radius: var(--radius-s);'; header.append(title, version); container.appendChild(header); // --- Tab Controls --- const tabContainer = document.createElement('div'); tabContainer.className = 'fab-helper-tabs'; const tabs = ['dashboard', 'settings', 'debug']; tabs.forEach(tabName => { const btn = document.createElement('button'); btn.textContent = Utils.getText(`tab_${tabName}`); btn.onclick = () => UI.switchTab(tabName); // 设置仪表盘标签为默认激活状态 if (tabName === 'dashboard') { btn.classList.add('active'); } tabContainer.appendChild(btn); State.UI.tabs[tabName] = btn; }); container.appendChild(tabContainer); // --- Dashboard Tab --- const dashboardContent = document.createElement('div'); dashboardContent.className = 'fab-helper-tab-content'; // 仪表盘标签页默认显示 dashboardContent.style.display = 'block'; State.UI.tabContents.dashboard = dashboardContent; const statusBar = document.createElement('div'); statusBar.className = 'fab-helper-status-bar'; const createStatusItem = (id, label, icon) => { const item = document.createElement('div'); item.className = 'fab-helper-status-item'; item.innerHTML = `<div class="fab-helper-status-label">${icon} ${label}</div><span id="${id}">0</span>`; return item; }; State.UI.statusVisible = createStatusItem('fab-status-visible', '可见', '👁️'); State.UI.statusTodo = createStatusItem('fab-status-todo', Utils.getText('todo'), '📥'); State.UI.statusDone = createStatusItem('fab-status-done', Utils.getText('added'), '✅'); State.UI.statusFailed = createStatusItem('fab-status-failed', Utils.getText('failed'), '❌'); State.UI.statusFailed.style.cursor = 'pointer'; State.UI.statusFailed.title = '点击打开所有失败的项目'; State.UI.statusFailed.onclick = () => { if (State.db.failed.length === 0) { Utils.logger('info', '失败列表为空,无需操作。'); return; } if (window.confirm(`您确定要在新标签页中打开 ${State.db.failed.length} 个失败的项目吗?`)) { Utils.logger('info', `正在打开 ${State.db.failed.length} 个失败项目...`); State.db.failed.forEach(task => { GM_openInTab(task.url, { active: false }); }); } }; State.UI.statusHidden = createStatusItem('fab-status-hidden', Utils.getText('hidden'), '🙈'); statusBar.append(State.UI.statusTodo, State.UI.statusDone, State.UI.statusFailed, State.UI.statusVisible, State.UI.statusHidden); State.UI.execBtn = document.createElement('button'); State.UI.execBtn.className = 'fab-helper-execute-btn'; State.UI.execBtn.onclick = TaskRunner.toggleExecution; // 根据State.isExecuting设置按钮初始状态 if (State.isExecuting) { State.UI.execBtn.innerHTML = `<span>${Utils.getText('executing')}</span>`; State.UI.execBtn.classList.add('executing'); } else { State.UI.execBtn.textContent = Utils.getText('execute'); State.UI.execBtn.classList.remove('executing'); } const actionButtons = document.createElement('div'); actionButtons.className = 'fab-helper-actions'; State.UI.syncBtn = document.createElement('button'); State.UI.syncBtn.textContent = '🔄 ' + Utils.getText('sync'); State.UI.syncBtn.onclick = TaskRunner.refreshVisibleStates; State.UI.hideBtn = document.createElement('button'); State.UI.hideBtn.onclick = TaskRunner.toggleHideSaved; actionButtons.append(State.UI.syncBtn, State.UI.hideBtn); // --- Log Panel (created before other elements to be appended first) --- const logContainer = document.createElement('div'); logContainer.className = 'fab-log-container'; const logHeader = document.createElement('div'); logHeader.className = 'fab-log-header'; const logTitle = document.createElement('span'); logTitle.textContent = '📝 操作日志'; const logControls = document.createElement('div'); logControls.className = 'fab-log-controls'; const copyLogBtn = document.createElement('button'); copyLogBtn.innerHTML = '📄'; copyLogBtn.title = Utils.getText('copyLog'); copyLogBtn.onclick = () => { navigator.clipboard.writeText(State.UI.logPanel.innerText).then(() => { const originalText = copyLogBtn.textContent; copyLogBtn.textContent = '✅'; setTimeout(() => { copyLogBtn.textContent = originalText; }, 1500); }).catch(err => Utils.logger('error', 'Failed to copy log:', err)); }; const clearLogBtn = document.createElement('button'); clearLogBtn.innerHTML = '🗑️'; clearLogBtn.title = Utils.getText('clearLog'); clearLogBtn.onclick = () => { State.UI.logPanel.innerHTML = ''; }; logControls.append(copyLogBtn, clearLogBtn); logHeader.append(logTitle, logControls); State.UI.logPanel = document.createElement('div'); State.UI.logPanel.id = Config.UI_LOG_ID; logContainer.append(logHeader, State.UI.logPanel); // 添加当前保存的浏览位置显示 const positionContainer = document.createElement('div'); positionContainer.className = 'fab-helper-position-container'; positionContainer.style.cssText = 'margin: 8px 0; padding: 6px 8px; background-color: rgba(0,0,0,0.05); border-radius: 4px; font-size: 13px;'; const positionIcon = document.createElement('span'); positionIcon.textContent = '📍 '; positionIcon.style.marginRight = '4px'; const positionInfo = document.createElement('span'); positionInfo.textContent = Utils.decodeCursor(State.savedCursor); // 保存引用以便后续更新 State.UI.savedPositionDisplay = positionInfo; positionContainer.appendChild(positionIcon); positionContainer.appendChild(positionInfo); // Reorder elements for the new layout: Log first, then position, status, then buttons dashboardContent.append(logContainer, positionContainer, statusBar, State.UI.execBtn, actionButtons); container.appendChild(dashboardContent); // --- Settings Tab --- const settingsContent = document.createElement('div'); settingsContent.className = 'fab-helper-tab-content'; const createSettingRow = (labelText, stateKey) => { const row = document.createElement('div'); row.className = 'fab-setting-row'; const label = document.createElement('span'); label.className = 'fab-setting-label'; label.textContent = labelText; const switchContainer = document.createElement('label'); switchContainer.className = 'fab-toggle-switch'; const input = document.createElement('input'); input.type = 'checkbox'; input.checked = State[stateKey]; input.onchange = (e) => { // Stop the event from doing anything weird, just in case. e.stopPropagation(); e.preventDefault(); if(stateKey === 'autoAddOnScroll') { TaskRunner.toggleAutoAdd(); } else if (stateKey === 'rememberScrollPosition') { TaskRunner.toggleRememberPosition(); } else if (stateKey === 'autoResumeAfter429') { TaskRunner.toggleAutoResume(); } else if (stateKey === 'autoRefreshEmptyPage') { TaskRunner.toggleAutoRefreshEmpty(); } // Manually sync the visual state of the checkbox since we prevented default action e.target.checked = State[stateKey]; }; const slider = document.createElement('span'); slider.className = 'fab-toggle-slider'; switchContainer.append(input, slider); row.append(label, switchContainer); // 所有设置行都使用相同的布局 row.appendChild(label); row.appendChild(switchContainer); return row; }; const autoAddSetting = createSettingRow('无限滚动时自动添加任务', 'autoAddOnScroll'); settingsContent.appendChild(autoAddSetting); const rememberPosSetting = createSettingRow('记住瀑布流浏览位置', 'rememberScrollPosition'); settingsContent.appendChild(rememberPosSetting); const autoResumeSetting = createSettingRow('429后自动恢复并继续', 'autoResumeAfter429'); settingsContent.appendChild(autoResumeSetting); const autoRefreshEmptySetting = createSettingRow('无商品可见时自动刷新', 'autoRefreshEmptyPage'); settingsContent.appendChild(autoRefreshEmptySetting); const resetButton = document.createElement('button'); resetButton.textContent = '🗑️ 清空所有存档'; resetButton.style.cssText = 'width: 100%; margin-top: 15px; background-color: var(--pink); color: white; padding: 10px; border-radius: var(--radius-m); border: none; cursor: pointer;'; resetButton.onclick = Database.resetAllData; settingsContent.appendChild(resetButton); // 添加调试模式切换按钮 - 使用自定义行而不是createSettingRow const debugModeRow = document.createElement('div'); debugModeRow.className = 'fab-setting-row'; debugModeRow.title = '启用详细日志记录,用于排查问题'; const debugLabel = document.createElement('span'); debugLabel.className = 'fab-setting-label'; debugLabel.textContent = '调试模式'; debugLabel.style.color = '#ff9800'; const switchContainer = document.createElement('label'); switchContainer.className = 'fab-toggle-switch'; const input = document.createElement('input'); input.type = 'checkbox'; input.checked = State.debugMode; input.onchange = (e) => { State.debugMode = e.target.checked; debugModeRow.classList.toggle('active', State.debugMode); Utils.logger('info', `调试模式已${State.debugMode ? '开启' : '关闭'}。${State.debugMode ? '将显示详细日志信息' : ''}`); GM_setValue('fab_helper_debug_mode', State.debugMode); }; const slider = document.createElement('span'); slider.className = 'fab-toggle-slider'; switchContainer.append(input, slider); debugModeRow.append(debugLabel, switchContainer); debugModeRow.classList.toggle('active', State.debugMode); settingsContent.appendChild(debugModeRow); // 排序选择已移除,改为自动从URL获取 State.UI.tabContents.settings = settingsContent; container.appendChild(settingsContent); // 确保设置标签页默认隐藏 settingsContent.style.display = 'none'; // --- 调试标签页 --- const debugContent = document.createElement('div'); debugContent.className = 'fab-helper-tab-content'; // 确保调试标签页默认隐藏 debugContent.style.display = 'none'; // 初始化调试内容容器 State.UI.debugContent = debugContent; const debugHeader = document.createElement('div'); debugHeader.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;'; const debugTitle = document.createElement('h4'); debugTitle.textContent = '状态周期历史记录'; debugTitle.style.margin = '0'; const debugControls = document.createElement('div'); debugControls.style.cssText = 'display: flex; gap: 8px;'; const copyHistoryBtn = document.createElement('button'); copyHistoryBtn.textContent = '复制'; copyHistoryBtn.title = '复制详细历史记录'; copyHistoryBtn.style.cssText = 'background: var(--dark-gray); border: 1px solid var(--border-color); color: var(--text-color-secondary); padding: 4px 8px; border-radius: var(--radius-m); cursor: pointer;'; copyHistoryBtn.onclick = () => { if (State.statusHistory.length === 0) { Utils.logger('info', '没有历史记录可供复制。'); return; } const formatEntry = (entry) => { const date = new Date(entry.endTime).toLocaleString(); if (entry.type === 'STARTUP') { return `🚀 脚本启动\n - 时间: ${date}\n - 信息: ${entry.message || ''}`; } else { const type = entry.type === 'NORMAL' ? '✅ 正常运行' : '🚨 限速时期'; // 添加空值检查,防止toFixed错误 let details = `持续: ${entry.duration !== undefined && entry.duration !== null ? entry.duration.toFixed(2) : '未知'}s`; if (entry.requests !== undefined) { details += `, 请求: ${entry.requests}次`; } return `${type}\n - 结束于: ${date}\n - ${details}`; } }; const fullLog = State.statusHistory.map(formatEntry).join('\n\n'); navigator.clipboard.writeText(fullLog).then(() => { const originalText = copyHistoryBtn.textContent; copyHistoryBtn.textContent = '已复制!'; setTimeout(() => { copyHistoryBtn.textContent = originalText; }, 2000); }).catch(err => Utils.logger('error', '复制失败:', err)); }; const clearHistoryBtn = document.createElement('button'); clearHistoryBtn.textContent = '清空'; clearHistoryBtn.title = '清空历史记录'; clearHistoryBtn.style.cssText = 'background: var(--dark-gray); border: 1px solid var(--border-color); color: var(--text-color-secondary); padding: 4px 8px; border-radius: var(--radius-m); cursor: pointer;'; clearHistoryBtn.onclick = async () => { if (window.confirm('您确定要清空所有状态历史记录吗?')) { State.statusHistory = []; await GM_deleteValue(Config.DB_KEYS.STATUS_HISTORY); // 添加一个新的"当前会话"记录 const currentSessionEntry = { type: 'STARTUP', duration: 0, endTime: new Date().toISOString(), message: '历史记录已清空,新会话开始' }; State.statusHistory.push(currentSessionEntry); await GM_setValue(Config.DB_KEYS.STATUS_HISTORY, State.statusHistory); UI.updateDebugTab(); Utils.logger('info', '状态历史记录已清空。'); } }; debugControls.append(copyHistoryBtn, clearHistoryBtn); debugHeader.append(debugTitle, debugControls); const historyListContainer = document.createElement('div'); historyListContainer.style.cssText = 'max-height: 250px; overflow-y: auto; background: rgba(10,10,10,0.85); color: #ddd; padding: 8px; border-radius: var(--radius-m);'; historyListContainer.className = 'fab-debug-history-container'; // 将historyListContainer保存为State.UI.historyContainer,而不是debugContent State.UI.historyContainer = historyListContainer; debugContent.append(debugHeader, historyListContainer); State.UI.tabContents.debug = debugContent; // 确保调试标签页默认隐藏 debugContent.style.display = 'none'; container.appendChild(debugContent); document.body.appendChild(container); // --- BUG FIX: Explicitly return true on successful creation --- return true; }, update: () => { if (!State.UI.container) return; // --- Update Status Numbers --- const todoCount = State.db.todo.length; const doneCount = State.db.done.length; const failedCount = State.db.failed.length; const visibleCount = document.querySelectorAll(Config.SELECTORS.card).length - State.hiddenThisPageCount; State.UI.statusTodo.querySelector('span').textContent = todoCount; State.UI.statusDone.querySelector('span').textContent = doneCount; State.UI.statusFailed.querySelector('span').textContent = failedCount; State.UI.statusHidden.querySelector('span').textContent = State.hiddenThisPageCount; State.UI.statusVisible.querySelector('span').textContent = visibleCount; // --- Update Button States --- // 确保按钮状态与State.isExecuting一致 if (State.isExecuting) { State.UI.execBtn.innerHTML = `<span>${Utils.getText('executing')}</span>`; State.UI.execBtn.classList.add('executing'); // 添加提示信息,显示当前执行状态 if (State.executionTotalTasks > 0) { const progress = State.executionCompletedTasks + State.executionFailedTasks; const percentage = Math.round((progress / State.executionTotalTasks) * 100); State.UI.execBtn.title = `执行中: ${progress}/${State.executionTotalTasks} (${percentage}%)`; } else { State.UI.execBtn.title = '执行中'; } } else { State.UI.execBtn.textContent = Utils.getText('execute'); State.UI.execBtn.classList.remove('executing'); State.UI.execBtn.title = '点击开始执行任务'; } State.UI.hideBtn.textContent = (State.hideSaved ? '🙈 ' : '👁️ ') + (State.hideSaved ? Utils.getText('show') : Utils.getText('hide')); }, removeAllOverlays: () => { document.querySelectorAll(Config.SELECTORS.card).forEach(card => { const overlay = card.querySelector('.fab-helper-overlay'); if (overlay) overlay.remove(); card.style.opacity = '1'; }); }, switchTab: (tabName) => { for (const name in State.UI.tabs) { State.UI.tabs[name].classList.toggle('active', name === tabName); State.UI.tabContents[name].style.display = name === tabName ? 'block' : 'none'; } }, updateDebugTab: () => { // 使用historyContainer而不是debugContent if (!State.UI.historyContainer) return; State.UI.historyContainer.innerHTML = ''; // Clear previous entries // 创建历史记录项 const createHistoryItem = (entry) => { const item = document.createElement('div'); item.style.cssText = 'padding: 8px; border-bottom: 1px solid var(--border-color);'; const header = document.createElement('div'); header.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-bottom: 4px;'; let icon, color, titleText; if (entry.type === 'STARTUP') { icon = '🚀'; color = 'var(--blue)'; titleText = '脚本启动'; } else if (entry.type === 'NORMAL') { icon = '✅'; color = 'var(--green)'; titleText = '正常运行期'; } else { // RATE_LIMITED icon = '🚨'; color = 'var(--orange)'; titleText = '限速期'; } header.innerHTML = `<span style="font-size: 18px;">${icon}</span> <strong style="color: ${color};">${titleText}</strong>`; const details = document.createElement('div'); details.style.cssText = 'font-size: 12px; color: var(--text-color-secondary); padding-left: 26px;'; let detailsHtml = ''; if (entry.type === 'STARTUP') { detailsHtml = `<div>时间: ${new Date(entry.endTime).toLocaleString()}</div>`; if (entry.message) { detailsHtml += `<div>信息: <strong>${entry.message}</strong></div>`; } } else { // 添加空值检查,防止toFixed错误 const duration = entry.duration !== undefined && entry.duration !== null ? entry.duration.toFixed(2) : '未知'; detailsHtml = `<div>持续时间: <strong>${duration}s</strong></div>`; if (entry.requests !== undefined) { detailsHtml += `<div>期间请求数: <strong>${entry.requests}</strong></div>`; } // 添加空值检查,防止日期错误 const endTime = entry.endTime ? new Date(entry.endTime).toLocaleString() : '未知时间'; detailsHtml += `<div>结束于: ${endTime}</div>`; } details.innerHTML = detailsHtml; item.append(header, details); return item; }; // 创建当前状态项(即使没有历史记录也会显示) const createCurrentStatusItem = () => { if(State.appStatus === 'NORMAL' || State.appStatus === 'RATE_LIMITED') { const item = document.createElement('div'); item.style.cssText = 'padding: 8px; border-bottom: 1px solid var(--border-color); background: var(--blue-bg);'; const header = document.createElement('div'); header.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-bottom: 4px;'; const icon = State.appStatus === 'NORMAL' ? '✅' : '🚨'; const color = State.appStatus === 'NORMAL' ? 'var(--green)' : 'var(--orange)'; const titleText = State.appStatus === 'NORMAL' ? '当前: 正常运行' : '当前: 限速中'; header.innerHTML = `<span style="font-size: 18px;">${icon}</span> <strong style="color: ${color};">${titleText}</strong>`; const details = document.createElement('div'); details.style.cssText = 'font-size: 12px; color: var(--text-color-secondary); padding-left: 26px;'; const startTime = State.appStatus === 'NORMAL' ? State.normalStartTime : State.rateLimitStartTime; // 添加空值检查,防止startTime为null或undefined const duration = startTime ? ((Date.now() - startTime) / 1000).toFixed(2) : '未知'; let detailsHtml = `<div>已持续: <strong>${duration}s</strong></div>`; if (State.appStatus === 'NORMAL') { detailsHtml += `<div>期间请求数: <strong>${State.successfulSearchCount}</strong></div>`; } // 添加空值检查,防止startTime为null const startTimeDisplay = startTime ? new Date(startTime).toLocaleString() : '未知时间'; detailsHtml += `<div>开始于: ${startTimeDisplay}</div>`; details.innerHTML = detailsHtml; item.append(header, details); State.UI.historyContainer.appendChild(item); } }; // 添加当前状态项(始终显示) createCurrentStatusItem(); // 如果没有历史记录,显示提示信息 if (State.statusHistory.length === 0) { const emptyMessage = document.createElement('div'); emptyMessage.style.cssText = 'color: #888; text-align: center; padding: 20px;'; emptyMessage.textContent = '没有可显示的历史记录。'; State.UI.historyContainer.appendChild(emptyMessage); return; } // 显示历史记录(如果有) const reversedHistory = [...State.statusHistory].reverse(); reversedHistory.forEach(entry => State.UI.historyContainer.appendChild(createHistoryItem(entry))); }, }; // --- 模块九: 主程序与初始化 (Main & Initialization) --- const InstanceManager = { isActive: false, lastPingTime: 0, pingInterval: null, // 初始化实例管理 init: async function() { try { // 检查当前页面是否是搜索页面 const isSearchPage = window.location.href.includes('/search') || window.location.pathname === '/' || window.location.pathname === '/zh-cn/' || window.location.pathname === '/en/'; // 如果是搜索页面,总是成为活跃实例 if (isSearchPage) { this.isActive = true; await this.registerAsActive(); Utils.logger('info', `当前是搜索页面,实例 [${Config.INSTANCE_ID}] 已激活。`); // 启动ping机制,每3秒更新一次活跃状态 this.pingInterval = setInterval(() => this.ping(), 3000); return true; } // 如果是工作标签页,检查是否有活跃实例 const activeInstance = await GM_getValue('fab_active_instance', null); const currentTime = Date.now(); if (activeInstance && (currentTime - activeInstance.lastPing < 10000)) { // 如果有活跃实例且在10秒内有ping,则当前实例不活跃 Utils.logger('info', `检测到活跃的脚本实例 [${activeInstance.id}],当前工作标签页将与之协作。`); this.isActive = false; return true; // 工作标签页也返回true,因为它需要执行自己的任务 } else { // 没有活跃实例或实例超时,当前实例成为活跃实例 this.isActive = true; await this.registerAsActive(); Utils.logger('info', `没有检测到活跃实例,当前实例 [${Config.INSTANCE_ID}] 已激活。`); // 启动ping机制,每3秒更新一次活跃状态 this.pingInterval = setInterval(() => this.ping(), 3000); return true; } } catch (error) { Utils.logger('error', `实例管理初始化失败: ${error.message}`); // 出错时默认为活跃,避免脚本不工作 this.isActive = true; return true; } }, // 注册为活跃实例 registerAsActive: async function() { await GM_setValue('fab_active_instance', { id: Config.INSTANCE_ID, lastPing: Date.now() }); }, // 定期更新活跃状态 ping: async function() { if (!this.isActive) return; this.lastPingTime = Date.now(); await this.registerAsActive(); }, // 检查是否可以接管 checkTakeover: async function() { if (this.isActive) return; try { const activeInstance = await GM_getValue('fab_active_instance', null); const currentTime = Date.now(); if (!activeInstance || (currentTime - activeInstance.lastPing > 10000)) { // 如果没有活跃实例或实例超时,接管 this.isActive = true; await this.registerAsActive(); Utils.logger('info', `之前的实例不再活跃,当前实例 [${Config.INSTANCE_ID}] 已接管。`); // 启动ping机制 this.pingInterval = setInterval(() => this.ping(), 3000); // 刷新页面以确保正确加载 location.reload(); } else { // 继续等待 setTimeout(() => this.checkTakeover(), 5000); } } catch (error) { Utils.logger('error', `接管检查失败: ${error.message}`); // 5秒后重试 setTimeout(() => this.checkTakeover(), 5000); } }, // 清理实例 cleanup: function() { if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } } }; async function main() { // 记录页面加载时间 window.pageLoadTime = Date.now(); Utils.logger('info', '脚本开始运行...'); Utils.detectLanguage(); // 检查是否是工作标签页 const urlParams = new URLSearchParams(window.location.search); const workerId = urlParams.get('workerId'); if (workerId) { // 如果是工作标签页,只执行工作标签页的逻辑,不执行主脚本逻辑 State.isWorkerTab = true; State.workerTaskId = workerId; // 初始化实例管理,但不检查返回值,工作标签页总是需要执行自己的任务 await InstanceManager.init(); Utils.logger('info', `工作标签页初始化完成,开始处理任务...`); await TaskRunner.processDetailPage(); return; } // 初始化实例管理 await InstanceManager.init(); // 主页面总是继续执行,不需要检查isActiveInstance await Database.load(); // 确保执行状态与存储状态一致 const storedExecutingState = await GM_getValue(Config.DB_KEYS.IS_EXECUTING, false); if (State.isExecuting !== storedExecutingState) { Utils.logger('info', `执行状态不一致,从存储中恢复:${storedExecutingState ? '执行中' : '已停止'}`); State.isExecuting = storedExecutingState; } // 从存储中恢复限速状态 const persistedStatus = await GM_getValue(Config.DB_KEYS.APP_STATUS); if (persistedStatus && persistedStatus.status === 'RATE_LIMITED') { State.appStatus = 'RATE_LIMITED'; State.rateLimitStartTime = persistedStatus.startTime; // 添加空值检查,防止persistedStatus.startTime为null const previousDuration = persistedStatus && persistedStatus.startTime ? ((Date.now() - persistedStatus.startTime) / 1000).toFixed(2) : '0.00'; Utils.logger('warn', `脚本启动时处于限速状态。限速已持续至少 ${previousDuration}s,来源: ${persistedStatus.source || '未知'}`); } // 初始化请求拦截器 setupRequestInterceptors(); await PagePatcher.init(); // 检查是否有临时保存的待办任务(从429恢复) const tempTasks = await GM_getValue('temp_todo_tasks', null); if (tempTasks && tempTasks.length > 0) { Utils.logger('info', `从429恢复:找到 ${tempTasks.length} 个临时保存的待办任务,正在恢复...`); State.db.todo = tempTasks; await GM_deleteValue('temp_todo_tasks'); // 清除临时存储 } // 添加工作标签页完成任务的监听器 State.valueChangeListeners.push(GM_addValueChangeListener(Config.DB_KEYS.WORKER_DONE, async (key, oldValue, newValue) => { if (!newValue) return; // 如果值被删除,忽略此事件 try { // 删除值,防止重复处理 await GM_deleteValue(Config.DB_KEYS.WORKER_DONE); const { workerId, success, task, logs, instanceId, executionTime } = newValue; // 检查是否由当前实例处理 if (instanceId !== Config.INSTANCE_ID) { Utils.logger('info', `收到来自其他实例 [${instanceId}] 的工作报告,当前实例 [${Config.INSTANCE_ID}] 将忽略。`); return; } if (!workerId || !task) { Utils.logger('error', '收到无效的工作报告。缺少workerId或task。'); return; } // 记录执行时间(如果有) if (executionTime) { // 添加空值检查,防止executionTime为null Utils.logger('info', `任务执行时间: ${executionTime ? (executionTime / 1000).toFixed(2) : '未知'}秒`); } // 移除此工作标签页的记录 if (State.runningWorkers[workerId]) { delete State.runningWorkers[workerId]; State.activeWorkers--; } // 记录工作标签页的日志 if (logs && logs.length) { logs.forEach(log => Utils.logger('info', log)); } // 处理任务结果 if (success) { Utils.logger('info', `✅ 任务完成: ${task.name}`); // 从待办列表中移除此任务 const initialTodoCount = State.db.todo.length; State.db.todo = State.db.todo.filter(t => t.uid !== task.uid); // 检查是否实际移除了任务 if (State.db.todo.length < initialTodoCount) { Utils.logger('info', `已从待办列表中移除任务 ${task.name}`); } else { Utils.logger('warn', `任务 ${task.name} 不在待办列表中,可能已被其他工作标签页处理。`); } // 保存待办列表 await Database.saveTodo(); // 如果尚未在完成列表中,则添加 if (!State.db.done.includes(task.url)) { State.db.done.push(task.url); await Database.saveDone(); } // 更新会话状态 State.sessionCompleted.add(task.url); // 更新执行统计 State.executionCompletedTasks++; } else { Utils.logger('warn', `❌ 任务失败: ${task.name}`); // 从待办列表中移除此任务 State.db.todo = State.db.todo.filter(t => t.uid !== task.uid); // 保存待办列表 await Database.saveTodo(); // 添加到失败列表(如果尚未存在) if (!State.db.failed.some(f => f.uid === task.uid)) { State.db.failed.push(task); await Database.saveFailed(); } // 更新执行统计 State.executionFailedTasks++; } // 更新UI UI.update(); // 如果还有待办任务,继续执行 if (State.isExecuting && State.activeWorkers < Config.MAX_CONCURRENT_WORKERS && State.db.todo.length > 0) { // 延迟一小段时间再派发新任务,避免同时打开太多标签页 setTimeout(() => TaskRunner.executeBatch(), 1000); } // 如果所有任务都已完成,停止执行 if (State.isExecuting && State.db.todo.length === 0 && State.activeWorkers === 0) { Utils.logger('info', '所有任务已完成。'); State.isExecuting = false; // 保存执行状态 Database.saveExecutingState(); // 保存待办列表(虽然为空,但仍需保存以更新存储) await Database.saveTodo(); // 如果处于限速状态且待办任务为0,触发页面刷新 if (State.appStatus === 'RATE_LIMITED') { Utils.logger('info', '所有任务已完成,且处于限速状态,将刷新页面尝试恢复...'); const randomDelay = 3000 + Math.random() * 5000; countdownRefresh(randomDelay, '任务完成后限速恢复'); } UI.update(); } // 更新隐藏状态 TaskRunner.runHideOrShow(); } catch (error) { Utils.logger('error', `处理工作报告时出错: ${error.message}`); } })); // 添加执行状态变化监听器,确保UI状态与存储状态一致 State.valueChangeListeners.push(GM_addValueChangeListener(Config.DB_KEYS.IS_EXECUTING, (key, oldValue, newValue) => { // 如果当前不是工作标签页,且存储状态与当前状态不一致,则更新当前状态 if (!State.isWorkerTab && State.isExecuting !== newValue) { Utils.logger('info', `检测到执行状态变化:${newValue ? '执行中' : '已停止'}`); State.isExecuting = newValue; UI.update(); } })); // --- ROBUST LAUNCHER --- // This interval is launched from the clean userscript context and is less likely to be interfered with. // It will persistently try to launch the DOM-dependent part of the script. // 使用一个全局变量来防止多次初始化 window._fabHelperLauncherActive = window._fabHelperLauncherActive || false; if (!window._fabHelperLauncherActive) { window._fabHelperLauncherActive = true; const launcherInterval = setInterval(() => { if (document.readyState === 'interactive' || document.readyState === 'complete') { if (!State.hasRunDomPart) { Utils.logger('info', '[Launcher] DOM is ready. Running main script logic...'); runDomDependentPart(); } if (State.hasRunDomPart) { clearInterval(launcherInterval); window._fabHelperLauncherActive = false; Utils.logger('info', '[Launcher] Main logic has been launched or skipped. Launcher is now idle.'); } } }, 500); // 增加间隔到500ms,减少频繁检查 } else { Utils.logger('info', '[Launcher] Another launcher is already active. Skipping initialization.'); } // 添加无活动超时刷新功能 let lastNetworkActivityTime = Date.now(); // 记录网络活动的函数 // 记录网络活动时间 window.recordNetworkActivity = function() { lastNetworkActivityTime = Date.now(); }; // 记录网络请求 window.recordNetworkRequest = function(source, isSuccess) { // 记录网络活动 window.recordNetworkActivity(); }; // 定期检查是否长时间无活动 setInterval(() => { // 只有在限速状态下才考虑无活动刷新 if (State.appStatus === 'RATE_LIMITED') { const inactiveTime = Date.now() - lastNetworkActivityTime; // 如果超过30秒没有网络活动,强制刷新 if (inactiveTime > 30000) { Utils.logger('warn', `⚠️ 检测到在限速状态下 ${Math.floor(inactiveTime/1000)} 秒无网络活动,即将强制刷新页面...`); // 使用延迟以便用户能看到日志 setTimeout(() => { window.location.reload(); }, 1500); } } }, 5000); // 每5秒检查一次 } async function runDomDependentPart() { if (State.hasRunDomPart) return; // 如果是工作标签页,不执行主脚本的DOM相关逻辑 if (State.isWorkerTab) { State.hasRunDomPart = true; // 标记为已运行,避免重复检查 return; } // The new, correct worker detection logic. const urlParams = new URLSearchParams(window.location.search); if (urlParams.has('workerId')) { // 这里不需要再调用processDetailPage,因为main函数中已经处理了 Utils.logger('info', `工作标签页DOM部分初始化,跳过UI创建`); State.hasRunDomPart = true; // Mark as run to stop the launcher return; } // --- NEW FLOW: Create the UI FIRST for immediate user feedback --- const uiCreated = UI.create(); if (!uiCreated) { Utils.logger('info', 'This is a detail or worker page. Halting main script execution.'); State.hasRunDomPart = true; // Mark as run to stop the launcher return; } // 初始化完成后,确保UI状态与执行状态一致 UI.update(); // 确保UI创建后立即更新调试标签页 UI.update(); UI.updateDebugTab(); UI.switchTab('dashboard'); // 设置初始标签页 State.hasRunDomPart = true; // Mark as run *after* successful UI creation // --- Dead on Arrival Check for initial 429 page load --- // 使enterRateLimitedState函数全局可访问,以便其他部分可以调用 window.enterRateLimitedState = function(source = '全局调用') { // 使用统一的限速管理器进入限速状态 RateLimitManager.enterRateLimitedState(source); }; // 添加全局函数用于记录所有网络请求 - 简化版 window.recordNetworkRequest = function(source = '网络请求', hasResults = true) { // 只记录成功请求,不再进行复杂的计数 if (hasResults) { RateLimitManager.recordSuccessfulRequest(source, hasResults); } }; // 添加页面内容检测功能,定期检查页面是否显示了限速错误信息 setInterval(() => { // 如果已经处于限速状态,不需要检查 if (State.appStatus === 'NORMAL') { // 检查页面内容是否包含限速错误信息 const pageText = document.body.innerText || ''; if (pageText.includes('Too many requests') || pageText.includes('rate limit') || pageText.match(/\{\s*"detail"\s*:\s*"Too many requests"\s*\}/i)) { Utils.logger('warn', '[页面内容检测] 检测到页面显示限速错误信息!'); RateLimitManager.enterRateLimitedState('页面内容检测'); } } }, 5000); // 每5秒检查一次 const checkIsErrorPage = (title, text) => { const isCloudflareTitle = title.includes('Cloudflare') || title.includes('Attention Required'); const is429Text = text.includes('429') || text.includes('Too Many Requests') || text.includes('Too many requests') || text.match(/\{\s*"detail"\s*:\s*"Too many requests"\s*\}/i); if (isCloudflareTitle || is429Text) { Utils.logger('warn', `[页面加载] 检测到429错误页面: ${document.location.href}`); window.enterRateLimitedState('页面内容429检测'); return true; } return false; }; // 如果检测到错误页面,不要立即返回,而是继续尝试恢复 const isErrorPage = checkIsErrorPage(document.title, document.body.innerText || ''); // 不要在这里return,让代码继续执行到自动恢复部分 // The auto-resume logic is preserved - always try to recover from 429 if (State.appStatus === 'RATE_LIMITED') { Utils.logger('info', '[Auto-Resume] 页面在限速状态下加载。正在进行恢复探测...'); // 使用统一的限速状态检查 const isRecovered = await RateLimitManager.checkRateLimitStatus(); if (isRecovered) { Utils.logger('info', '✅ 恢复探测成功!限速已解除,继续正常操作。'); // 如果有待办任务,继续执行 if (State.db.todo.length > 0 && !State.isExecuting) { Utils.logger('info', `发现 ${State.db.todo.length} 个待办任务,自动恢复执行...`); State.isExecuting = true; Database.saveExecutingState(); TaskRunner.executeBatch(); } } else { // 仍然处于限速状态,继续随机刷新 Utils.logger('warn', '恢复探测失败。仍处于限速状态,将继续随机刷新...'); // 如果有活动任务,等待它们完成 if (State.activeWorkers > 0) { Utils.logger('info', `仍有 ${State.activeWorkers} 个任务在执行中,等待它们完成后再刷新...`); } else if (State.db.todo.length > 0) { // 如果有待办任务但没有活动任务,尝试继续执行 Utils.logger('info', `有 ${State.db.todo.length} 个待办任务等待执行,将尝试继续执行...`); if (!State.isExecuting) { State.isExecuting = true; Database.saveExecutingState(); TaskRunner.executeBatch(); } } else { // 没有任务,直接刷新 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, '恢复探测失败'); } } } // --- Observer setup is now directly inside runDomDependentPart --- const containerSelectors = [ 'main', '#main', '.AssetGrid-root', '.fabkit-responsive-grid-container' ]; let targetNode = null; for (const selector of containerSelectors) { targetNode = document.querySelector(selector); if (targetNode) break; } if (!targetNode) targetNode = document.body; const observer = new MutationObserver((mutationsList) => { const hasNewContent = mutationsList.some(mutation => [...mutation.addedNodes].some(node => node.nodeType === 1 && (node.matches(Config.SELECTORS.card) || node.querySelector(Config.SELECTORS.card)) ) ); if (hasNewContent) { // 不再立即执行隐藏,而是等待一段时间,确保API请求完成 // 延迟进行处理 clearTimeout(State.observerDebounceTimer); State.observerDebounceTimer = setTimeout(() => { Utils.logger('info', '[Observer] 检测到新内容加载,等待API请求完成...'); // 首先等待一段较长的时间,确保API请求有足够时间完成 setTimeout(() => { Utils.logger('info', '[Observer] 开始处理新加载的内容...'); // 执行一次状态检查,尝试更新卡片状态 TaskRunner.checkVisibleCardsStatus().then(() => { // 状态检查后再次执行隐藏,确保新状态被应用 // 使用更长的延迟执行隐藏,确保DOM和API状态已完全更新 setTimeout(() => { if (State.hideSaved) { TaskRunner.runHideOrShow(); } }, 1000); // 只在非限速状态下执行自动添加任务功能 if (State.appStatus === 'NORMAL' || State.autoAddOnScroll) { // 异步调用scanAndAddTasks,但也增加延迟 setTimeout(() => { TaskRunner.scanAndAddTasks(document.querySelectorAll(Config.SELECTORS.card)) .catch(error => Utils.logger('error', `自动添加任务失败: ${error.message}`)); }, 500); } }).catch(() => { // 即使状态检查失败也执行隐藏,但延迟更长 setTimeout(() => { if (State.hideSaved) { TaskRunner.runHideOrShow(); } }, 1500); }); }, 2000); // 等待2秒,确保API请求完成 }, 500); // 增加防抖延迟 } }); observer.observe(targetNode, { childList: true, subtree: true }); Utils.logger('info', `✅ Core DOM observer is now active on <${targetNode.tagName.toLowerCase()}>.`); // 初始化时运行一次隐藏逻辑,确保页面加载时已有的内容能被正确处理 TaskRunner.runHideOrShow(); // 添加定期检查功能,确保所有卡片都被正确处理 setInterval(() => { // 如果没有开启隐藏功能,不需要检查 if (!State.hideSaved) return; // 检查是否有未处理的卡片 const cards = document.querySelectorAll(Config.SELECTORS.card); let unprocessedCount = 0; cards.forEach(card => { const isProcessed = card.getAttribute('data-fab-processed') === 'true'; if (!isProcessed) { unprocessedCount++; } else { // 检查已处理的卡片是否状态正确 const isFinished = TaskRunner.isCardFinished(card); const shouldBeHidden = isFinished && State.hideSaved; const isHidden = card.style.display === 'none'; // 如果状态不一致,重置处理标记 if (shouldBeHidden !== isHidden) { card.removeAttribute('data-fab-processed'); unprocessedCount++; } } }); // 如果有未处理的卡片,重新执行隐藏逻辑 if (unprocessedCount > 0) { Utils.logger('info', `检测到 ${unprocessedCount} 个未处理或状态不一致的卡片,重新执行隐藏逻辑`); TaskRunner.runHideOrShow(); } }, 3000); // 每3秒检查一次 // 添加定期检查功能,每10秒检查一次待办列表中的任务是否已经完成 setInterval(() => { // 如果待办列表为空,不需要检查 if (State.db.todo.length === 0) return; // 检查待办列表中的每个任务,看是否已经在"完成"列表中 const initialTodoCount = State.db.todo.length; State.db.todo = State.db.todo.filter(task => { const url = task.url.split('?')[0]; // 如果任务已经在"完成"列表中,则从待办列表中移除 return !State.db.done.includes(url); }); // 如果待办列表的数量发生了变化,更新UI if (State.db.todo.length < initialTodoCount) { Utils.logger('info', `[自动清理] 从待办列表中移除了 ${initialTodoCount - State.db.todo.length} 个已完成的任务。`); UI.update(); } }, 10000); // 添加定期检查功能,检测是否请求不出新商品(隐性限速) let lastCardCount = document.querySelectorAll(Config.SELECTORS.card).length; let noNewCardsCounter = 0; let lastScrollY = window.scrollY; setInterval(() => { // 如果已经处于限速状态,不需要检查 if (State.appStatus !== 'NORMAL') return; // 获取当前卡片数量 const currentCardCount = document.querySelectorAll(Config.SELECTORS.card).length; // 如果滚动了但卡片数量没有增加,可能是隐性限速 if (window.scrollY > lastScrollY + 100 && currentCardCount === lastCardCount) { noNewCardsCounter++; // 如果连续3次检查都没有新卡片,认为是隐性限速 if (noNewCardsCounter >= 3) { Utils.logger('warn', `[隐性限速检测] 检测到可能的限速情况:连续${noNewCardsCounter}次滚动后卡片数量未增加。`); try { // 使用RateLimitManager处理限速 RateLimitManager.enterRateLimitedState('隐性限速检测'); } catch (error) { Utils.logger('error', `处理限速出错: ${error.message}`); // 备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, '隐性限速检测'); } noNewCardsCounter = 0; } } else if (currentCardCount > lastCardCount) { // 有新卡片,重置计数器 noNewCardsCounter = 0; } // 更新上次卡片数量和滚动位置 lastCardCount = currentCardCount; lastScrollY = window.scrollY; }, 5000); // 每5秒检查一次 // 添加页面内容检测功能,定期检查页面是否显示了限速错误信息 setInterval(() => { // 如果已经处于限速状态,不需要检查 if (State.appStatus !== 'NORMAL') return; // 检查页面内容是否包含限速错误信息 const pageText = document.body.innerText || ''; const jsonPattern = /\{\s*"detail"\s*:\s*"Too many requests"\s*\}/i; if (pageText.match(jsonPattern) || pageText.includes('Too many requests') || pageText.includes('rate limit')) { Utils.logger('warn', '[页面内容检测] 检测到页面显示限速错误信息!'); try { // 直接使用全局函数,避免使用PagePatcher.handleRateLimit if (typeof window.enterRateLimitedState === 'function') { window.enterRateLimitedState(); } else { // 最后的备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, '页面内容检测'); } } catch (error) { Utils.logger('error', `处理限速出错: ${error.message}`); // 最后的备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, '错误恢复'); } } }, 3000); // 每3秒检查一次 // 添加HTTP状态码检测功能,定期检查当前页面的HTTP状态码 const checkHttpStatus = async () => { try { // 如果已经处于限速状态,不需要检查 if (State.appStatus !== 'NORMAL') return; // 使用window.performance API检查最近的页面请求 if (window.performance && window.performance.getEntriesByType) { const navigationEntries = window.performance.getEntriesByType('navigation'); if (navigationEntries && navigationEntries.length > 0) { const lastNavigation = navigationEntries[0]; if (lastNavigation.responseStatus === 429) { Utils.logger('warn', `[HTTP状态检测] 检测到导航请求状态码为429!`); if (typeof window.enterRateLimitedState === 'function') { window.enterRateLimitedState(); } else { const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, 'HTTP状态检测'); } return; } } } // 不再发送HEAD请求,只使用Performance API Utils.logger('info', `[HTTP状态检测] 使用Performance API检查,不再发送HEAD请求`); // 检查页面内容是否包含限速信息 const pageText = document.body.innerText || ''; if (pageText.includes('Too many requests') || pageText.includes('rate limit') || pageText.match(/\{\s*"detail"\s*:\s*"Too many requests"\s*\}/i)) { Utils.logger('warn', `[HTTP状态检测] 页面内容包含限速信息,判断为429状态`); try { // 直接使用全局函数,避免使用PagePatcher.handleRateLimit if (typeof window.enterRateLimitedState === 'function') { window.enterRateLimitedState(); } else { // 最后的备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, 'HTTP状态检测'); } } catch (error) { Utils.logger('error', `处理限速出错: ${error.message}`); // 最后的备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, '错误恢复'); } } } catch (error) { // 忽略错误 } }; // 每10秒检查一次HTTP状态码 setInterval(checkHttpStatus, 10000); // 添加状态监控,定期检查页面状态 const checkPageStatus = async () => { try { // 重新计算实际可见的商品数量,确保与DOM状态同步 const totalCards = document.querySelectorAll(Config.SELECTORS.card).length; // 使用更准确的方式检查元素是否可见 const visibleCards = Array.from(document.querySelectorAll(Config.SELECTORS.card)).filter(card => { // 检查元素自身的display属性 if (card.style.display === 'none') return false; // 检查是否被CSS规则隐藏 const computedStyle = window.getComputedStyle(card); return computedStyle.display !== 'none' && computedStyle.visibility !== 'hidden'; }); const actualVisibleCards = visibleCards.length; const hiddenCards = totalCards - actualVisibleCards; // 更新UI显示的可见商品数量,确保UI与实际DOM状态一致 const visibleCountElement = document.getElementById('fab-status-visible'); if (visibleCountElement) { visibleCountElement.textContent = actualVisibleCards.toString(); } // 更新全局状态 State.hiddenThisPageCount = hiddenCards; // 如果处于限速状态且没有可见商品,考虑刷新 // 只有在明确开启了自动刷新功能时才触发 if (State.appStatus === 'RATE_LIMITED' && actualVisibleCards === 0 && State.autoRefreshEmptyPage) { // 如果已经有倒计时在运行,不要干扰它 if (window._pendingZeroVisibleRefresh || currentCountdownInterval || currentRefreshTimeout) { return; } Utils.logger('info', `[状态监控] 检测到限速状态下没有可见商品且自动刷新已开启,准备刷新页面`); const randomDelay = 3000 + Math.random() * 2000; // 3-5秒的短延迟 countdownRefresh(randomDelay, '限速状态无可见商品'); return; } // 移除正常状态下因隐藏商品而自动刷新的逻辑 // 如果处于正常状态且所有商品都被隐藏,只记录日志,不触发刷新 if (State.appStatus === 'NORMAL' && actualVisibleCards === 0 && hiddenCards > 25) { Utils.logger('info', `[状态监控] 检测到正常状态下所有商品都被隐藏 (${hiddenCards}个)`); return; } // 使用window.performance API检查最近的API请求 if (window.performance && window.performance.getEntriesByType) { const recentRequests = window.performance.getEntriesByType('resource') .filter(r => r.name.includes('/i/listings/search') || r.name.includes('/i/users/me/listings-states')) .filter(r => Date.now() - r.startTime < 15000); // 最近15秒内的请求 // 检查是否有429状态码的请求 const has429 = recentRequests.some(r => r.responseStatus === 429); if (has429 && State.appStatus === 'NORMAL') { Utils.logger('warn', `[状态监控] 检测到最近15秒内有429状态码的请求,进入限速状态`); if (typeof window.enterRateLimitedState === 'function') { window.enterRateLimitedState('性能API检测429'); } return; } // 检查是否有成功的请求 const hasSuccess = recentRequests.some(r => r.responseStatus >= 200 && r.responseStatus < 300); if (hasSuccess && State.appStatus === 'RATE_LIMITED' && State.consecutiveSuccessCount >= 2) { Utils.logger('info', `[状态监控] 检测到最近15秒内有成功的API请求,尝试退出限速状态`); if (typeof RateLimitManager.exitRateLimitedState === 'function') { RateLimitManager.exitRateLimitedState('性能API检测成功'); } } } } catch (error) { Utils.logger('error', `页面状态检查出错: ${error.message}`); } }; // 每10秒检查一次页面状态 setInterval(checkPageStatus, 10000); // 添加定期检查功能,确保待办任务能被执行 setInterval(() => { // 如果没有待办任务,不需要检查 if (State.db.todo.length === 0) return; // 确保任务被执行 TaskRunner.ensureTasksAreExecuted(); }, 5000); // 每5秒检查一次 // 添加专门针对滚动加载API请求的拦截器 const originalXMLHttpRequestSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send = function(...args) { const xhr = this; // 添加额外的事件监听器,专门用于检测429错误 xhr.addEventListener('load', function() { // 只检查listings/search相关的请求 if (xhr._url && xhr._url.includes('/i/listings/search')) { // 检查状态码 if (xhr.status === 429 || xhr.status === '429' || xhr.status.toString() === '429') { Utils.logger('warn', `[滚动API监控] 检测到API请求状态码为429: ${xhr._url}`); try { // 直接使用全局函数,避免使用PagePatcher.handleRateLimit if (typeof window.enterRateLimitedState === 'function') { window.enterRateLimitedState(); } else { // 最后的备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, '滚动API监控'); } } catch (error) { Utils.logger('error', `处理限速出错: ${error.message}`); // 最后的备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, '错误恢复'); } return; } // 检查响应内容 try { const responseText = xhr.responseText; if (responseText && ( responseText.includes('Too many requests') || responseText.match(/\{\s*"detail"\s*:\s*"Too many requests"\s*\}/i) )) { Utils.logger('warn', `[滚动API监控] 检测到API响应内容包含限速信息: ${responseText}`); try { // 直接使用全局函数,避免使用PagePatcher.handleRateLimit if (typeof window.enterRateLimitedState === 'function') { window.enterRateLimitedState(); } else { // 最后的备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, '滚动API监控'); } } catch (error) { Utils.logger('error', `处理限速出错: ${error.message}`); // 最后的备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, '错误恢复'); } return; } } catch (e) { // 忽略错误 } } }); return originalXMLHttpRequestSend.apply(this, args); }; } main(); // 添加一个通用的倒计时刷新函数 // 使用一个全局变量来跟踪当前的倒计时,避免多个倒计时同时运行 let currentCountdownInterval = null; let currentRefreshTimeout = null; const countdownRefresh = (delay, reason = '备选方案') => { // 如果已经安排了刷新,不要重复安排 if (State.isRefreshScheduled) { Utils.logger('info', `已有刷新计划正在进行中,不再安排新的刷新 (${reason})`); return; } // 标记已安排刷新 State.isRefreshScheduled = true; // 如果已经有倒计时在运行,先清除它 if (currentCountdownInterval) { clearInterval(currentCountdownInterval); currentCountdownInterval = null; } if (currentRefreshTimeout) { clearTimeout(currentRefreshTimeout); currentRefreshTimeout = null; } // 添加空值检查,防止delay为null const seconds = delay ? (delay/1000).toFixed(1) : '未知'; // 添加明显的倒计时日志 Utils.logger('info', `🔄 ${reason}启动!将在 ${seconds} 秒后刷新页面尝试恢复...`); // 每秒更新倒计时日志 let remainingSeconds = Math.ceil(delay/1000); currentCountdownInterval = setInterval(() => { remainingSeconds--; if (remainingSeconds <= 0) { clearInterval(currentCountdownInterval); currentCountdownInterval = null; Utils.logger('info', `⏱️ 倒计时结束,正在刷新页面...`); } else { Utils.logger('info', `⏱️ 自动刷新倒计时: ${remainingSeconds} 秒...`); // 如果用户手动取消了刷新标记 if (!State.isRefreshScheduled) { Utils.logger('info', `⏹️ 检测到刷新已被取消,停止倒计时`); clearInterval(currentCountdownInterval); currentCountdownInterval = null; if (currentRefreshTimeout) { clearTimeout(currentRefreshTimeout); currentRefreshTimeout = null; } return; } // 每3秒重新检查一次条件 if (remainingSeconds % 3 === 0) { // 尝试使用优化后的API函数检查限速状态 checkRateLimitStatus().then(isNotLimited => { if (isNotLimited) { Utils.logger('info', `⏱️ 检测到API限速已解除,取消刷新...`); clearInterval(currentCountdownInterval); currentCountdownInterval = null; if (currentRefreshTimeout) { clearTimeout(currentRefreshTimeout); currentRefreshTimeout = null; } // 重置刷新标记 State.isRefreshScheduled = false; // 恢复正常状态 if (State.appStatus === 'RATE_LIMITED') { RateLimitManager.exitRateLimitedState(); } return; } // 如果是429限速状态,则检查可见商品是否为0 if (State.appStatus === 'RATE_LIMITED') { // 使用UI上显示的可见商品数量作为判断依据 const actualVisibleCount = parseInt(document.getElementById('fab-status-visible')?.textContent || '0'); // 只检查是否有待办任务或活动工作线程 if (State.db.todo.length > 0 || State.activeWorkers > 0) { clearInterval(currentCountdownInterval); clearTimeout(currentRefreshTimeout); currentCountdownInterval = null; currentRefreshTimeout = null; // 重置刷新标记 State.isRefreshScheduled = false; Utils.logger('info', `⏹️ 检测到有 ${State.db.todo.length} 个待办任务和 ${State.activeWorkers} 个活动工作线程,已取消自动刷新。`); Utils.logger('warn', '⚠️ 刷新条件已变化,自动刷新已取消。'); return; } // 如果没有实际可见的商品,继续刷新 if (actualVisibleCount === 0) { Utils.logger('info', `🔄 页面上没有可见商品且处于限速状态,将继续自动刷新。`); } else { Utils.logger('info', `⏹️ 虽然处于限速状态,但页面上有 ${actualVisibleCount} 个可见商品,暂不刷新。`); clearInterval(currentCountdownInterval); clearTimeout(currentRefreshTimeout); currentCountdownInterval = null; currentRefreshTimeout = null; return; } } else { // 正常状态下,如果有可见商品、待办任务或活动工作线程,则取消刷新 // 使用UI上显示的可见商品数量 const visibleCount = parseInt(document.getElementById('fab-status-visible')?.textContent || '0'); if (State.db.todo.length > 0 || State.activeWorkers > 0 || visibleCount > 0) { clearInterval(currentCountdownInterval); clearTimeout(currentRefreshTimeout); currentCountdownInterval = null; currentRefreshTimeout = null; // 重置刷新标记 State.isRefreshScheduled = false; if (visibleCount > 0) { Utils.logger('info', `⏹️ 检测到页面上有 ${visibleCount} 个可见商品,已取消自动刷新。`); } else { Utils.logger('info', `⏹️ 检测到有 ${State.db.todo.length} 个待办任务和 ${State.activeWorkers} 个活动工作线程,已取消自动刷新。`); } Utils.logger('warn', '⚠️ 刷新条件已变化,自动刷新已取消。'); return; } } }).catch(e => { if (State.debugMode) { Utils.logger('debug', `检查限速状态出错: ${e.message}`); } }); } } }, 1000); // 设置刷新定时器 currentRefreshTimeout = setTimeout(() => { // 最后一次检查条件,确保在刷新前条件仍然满足 // 使用UI上显示的可见商品数量 const visibleCount = parseInt(document.getElementById('fab-status-visible')?.textContent || '0'); // 如果是429限速状态,检查实际可见商品 if (State.appStatus === 'RATE_LIMITED') { // 使用UI上显示的可见商品数量 const actualVisibleCount = parseInt(document.getElementById('fab-status-visible')?.textContent || '0'); // 只检查是否有待办任务或活动工作线程 if (State.db.todo.length > 0 || State.activeWorkers > 0) { Utils.logger('info', `⏹️ 刷新前检测到有 ${State.db.todo.length} 个待办任务和 ${State.activeWorkers} 个活动工作线程,已取消自动刷新。`); Utils.logger('warn', '⚠️ 最后一刻检查:刷新条件不满足,自动刷新已取消。'); State.isRefreshScheduled = false; // 重置刷新标记 return; } // 如果没有实际可见的商品,执行刷新 if (actualVisibleCount === 0) { Utils.logger('info', `🔄 页面上没有可见商品且处于限速状态,将执行自动刷新。`); // 使用更可靠的刷新方式 window.location.href = window.location.href; } else { Utils.logger('info', `⏹️ 虽然处于限速状态,但页面上有 ${actualVisibleCount} 个可见商品,取消自动刷新。`); State.isRefreshScheduled = false; // 重置刷新标记 return; } } else { // 正常状态下的检查 if (State.db.todo.length > 0 || State.activeWorkers > 0 || visibleCount > 0) { if (visibleCount > 0) { Utils.logger('info', `⏹️ 刷新前检测到页面上有 ${visibleCount} 个可见商品,已取消自动刷新。`); } else { Utils.logger('info', `⏹️ 刷新前检测到有 ${State.db.todo.length} 个待办任务和 ${State.activeWorkers} 个活动工作线程,已取消自动刷新。`); } Utils.logger('warn', '⚠️ 最后一刻检查:刷新条件不满足,自动刷新已取消。'); State.isRefreshScheduled = false; // 重置刷新标记 } else { // 所有条件都满足,执行刷新 // 使用更可靠的刷新方式 window.location.href = window.location.href; } } }, delay); }; // 优化后的限速状态检查函数 - 完全依赖网站自身请求流量 async function checkRateLimitStatus() { try { // 重新计算实际可见的商品数量,确保与DOM状态同步 const totalCards = document.querySelectorAll(Config.SELECTORS.card).length; const hiddenCards = document.querySelectorAll(`${Config.SELECTORS.card}[style*="display: none"]`).length; const actualVisibleCards = totalCards - hiddenCards; // 更新UI显示的可见商品数量,确保UI与实际DOM状态一致 const visibleCountElement = document.getElementById('fab-status-visible'); if (visibleCountElement) { visibleCountElement.textContent = actualVisibleCards.toString(); } // 使用实际DOM状态更新全局状态 State.hiddenThisPageCount = hiddenCards; Utils.logger('info', `📊 状态检查 - 实际可见: ${actualVisibleCards}, 总卡片: ${totalCards}, 隐藏商品数: ${hiddenCards}`); // 如果处于限速状态且没有可见商品,直接返回false触发刷新 if (State.appStatus === 'RATE_LIMITED' && actualVisibleCards === 0) { Utils.logger('info', `🔄 处于限速状态且没有可见商品,建议刷新页面`); return false; } // 即使在正常状态下,如果所有商品都被隐藏且隐藏的商品数量超过25个,也建议刷新 if (actualVisibleCards === 0 && hiddenCards > 25) { Utils.logger('info', `🔄 检测到页面上有 ${hiddenCards} 个隐藏商品,但没有可见商品,建议刷新页面`); return false; } // 使用window.performance API检查最近的网络请求 if (window.performance && window.performance.getEntriesByType) { const recentRequests = window.performance.getEntriesByType('resource') .filter(r => r.name.includes('/i/listings/search') || r.name.includes('/i/users/me/listings-states')) .filter(r => Date.now() - r.startTime < 10000); // 最近10秒内的请求 // 如果有最近的请求,检查它们的状态 if (recentRequests.length > 0) { // 检查是否有429状态码的请求 const has429 = recentRequests.some(r => r.responseStatus === 429); if (has429) { Utils.logger('info', `📊 检测到最近10秒内有429状态码的请求,判断为限速状态`); return false; } // 检查是否有成功的请求 const hasSuccess = recentRequests.some(r => r.responseStatus >= 200 && r.responseStatus < 300); if (hasSuccess) { Utils.logger('info', `📊 检测到最近10秒内有成功的API请求,判断为正常状态`); return true; } } // 如果没有最近的请求或者没有明确的成功/失败状态,保持当前状态 return State.appStatus === 'NORMAL'; } // 如果无法使用Performance API,根据当前状态返回 // 在限速状态下返回false,表示需要刷新 // 在正常状态下返回true,表示不需要刷新 return State.appStatus === 'NORMAL'; } catch (error) { Utils.logger('error', `检查限速状态出错: ${error.message}`); // 出错时保守处理,认为仍然处于限速状态 return false; } } // 在页面卸载时清理实例 window.addEventListener('beforeunload', () => { InstanceManager.cleanup(); Utils.cleanup(); }); // 添加请求拦截器设置函数 function setupRequestInterceptors() { try { // 设置XHR拦截器 setupXHRInterceptor(); // 设置Fetch拦截器 setupFetchInterceptor(); // 设置定期清理过期缓存的定时器 setInterval(() => DataCache.cleanupExpired(), 60000); // 每分钟清理一次 Utils.logger('info', '请求拦截和缓存系统已初始化'); } catch (e) { Utils.logger('error', `初始化请求拦截器失败: ${e.message}`); } } // 设置XHR拦截器 function setupXHRInterceptor() { const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(...args) { this._url = args[1]; // 保存URL以便后续使用 return originalOpen.apply(this, args); }; XMLHttpRequest.prototype.send = function(...args) { const xhr = this; // 只拦截相关API请求 if (xhr._url && typeof xhr._url === 'string') { // 添加加载完成事件监听器 xhr.addEventListener('load', function() { if (xhr.readyState === 4 && xhr.status === 200) { try { const responseData = JSON.parse(xhr.responseText); // 处理商品列表搜索响应 if (xhr._url.includes('/i/listings/search') && responseData.results && Array.isArray(responseData.results)) { DataCache.saveListings(responseData.results); if (State.debugMode) { Utils.logger('debug', `[Cache] 已缓存 ${responseData.results.length} 个商品数据`); } } // 处理拥有状态响应 else if (xhr._url.includes('/i/users/me/listings-states')) { if (Array.isArray(responseData)) { DataCache.saveOwnedStatus(responseData); } else { const extractedData = API.extractStateData(responseData, 'XHRInterceptor'); if (Array.isArray(extractedData) && extractedData.length > 0) { DataCache.saveOwnedStatus(extractedData); } } } // 处理价格信息响应 else if (xhr._url.includes('/i/listings/prices-infos') && responseData.offers && Array.isArray(responseData.offers)) { DataCache.savePrices(responseData.offers); } } catch (e) { // 解析错误时只在调试模式下记录 if (State.debugMode) { Utils.logger('debug', `[Cache] 解析响应失败: ${e.message}`); } } } }); } return originalSend.apply(this, args); }; if (State.debugMode) { Utils.logger('debug', '[优化] XHR拦截器已设置'); } } // 设置Fetch拦截器 function setupFetchInterceptor() { const originalFetch = window.fetch; window.fetch = async function(...args) { const url = args[0]?.toString() || ''; // 只拦截相关API请求 if (url.includes('/i/listings/search') || url.includes('/i/users/me/listings-states') || url.includes('/i/listings/prices-infos')) { try { // 执行原始fetch请求 const response = await originalFetch.apply(this, args); // 如果请求成功,处理响应数据 if (response.ok) { // 克隆响应以避免消耗原始响应 const clonedResponse = response.clone(); // 异步处理响应数据 clonedResponse.json().then(data => { // 处理商品列表搜索响应 - 简化版 if (url.includes('/i/listings/search') && data.results && Array.isArray(data.results)) { DataCache.saveListings(data.results); } // 处理拥有状态响应 else if (url.includes('/i/users/me/listings-states')) { if (Array.isArray(data)) { Utils.logger('info', `[网页请求] 捕获到拥有状态API响应,包含 ${data.length} 个商品状态`); DataCache.saveOwnedStatus(data); } else { const extractedData = API.extractStateData(data, 'FetchInterceptor'); if (Array.isArray(extractedData) && extractedData.length > 0) { Utils.logger('info', `[网页请求] 捕获到拥有状态API响应,提取出 ${extractedData.length} 个商品状态`); DataCache.saveOwnedStatus(extractedData); } } } // 处理价格信息响应 else if (url.includes('/i/listings/prices-infos') && data.offers && Array.isArray(data.offers)) { DataCache.savePrices(data.offers); } }).catch((e) => { // 解析错误时只在调试模式下记录 if (State.debugMode) { Utils.logger('debug', `[Cache] Fetch: 解析响应失败: ${e.message}`); } }); } // 返回原始响应 return response; } catch (e) { // 请求错误,继续使用原始fetch Utils.logger('error', `[Cache] Fetch拦截器错误: ${e.message}`); return originalFetch.apply(this, args); } } // 非相关API请求,直接使用原始fetch return originalFetch.apply(this, args); }; if (State.debugMode) { Utils.logger('debug', '[优化] Fetch拦截器已设置'); } } // 添加一个函数,确保UI在刷新后能正确重新加载 function ensureUILoaded() { // 检查UI是否已加载 if (!document.getElementById(Config.UI_CONTAINER_ID)) { // 如果UI未加载,尝试重新初始化 Utils.logger('warn', '检测到UI未加载,尝试重新初始化...'); // 延迟执行,确保页面已完全加载 setTimeout(() => { try { // 重新执行初始化逻辑 runDomDependentPart(); } catch (error) { Utils.logger('error', `UI重新初始化失败: ${error.message}`); } }, 1000); } } // 添加页面加载完成后的检查 window.addEventListener('load', () => { // 延迟检查,确保所有脚本都有机会执行 setTimeout(ensureUILoaded, 2000); }); // 添加可见性变化检查,处理标签页切换回来的情况 document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { // 页面变为可见时检查UI setTimeout(ensureUILoaded, 500); } }); })();