// ==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);
}
});
})();