您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Fab Helper 优化版 - 减少API请求,提高性能,增强稳定性,修复限速刷新
// ==UserScript== // @name Fab Helper (优化版) // @name:zh-CN Fab Helper (优化版) // @name:en Fab Helper (Optimized) // @namespace https://www.fab.com/ // @version 3.3.0-20250831165136 // @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'; // --- 模块一: 配置与常量 --- 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, // 最大并发工作标签页数量 WORKER_TIMEOUT: 30000, // 工作标签页超时时间 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', // 自动添加设置键 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', // 状态历史记录持久化 AUTO_RESUME: 'fab_auto_resume_v1', // 自动恢复功能设置 IS_EXECUTING: 'fab_is_executing_v1', // 执行状态保存 AUTO_REFRESH_EMPTY: 'fab_auto_refresh_empty_v1', // 无商品可见时自动刷新 // 其他键值用于会话或主标签页持久化 }, 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: { // 基础UI 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', visible: 'Visible', clearLog: 'Clear Log', copyLog: 'Copy Log', copied: 'Copied!', tab_dashboard: 'Dashboard', tab_settings: 'Settings', tab_debug: 'Debug', // 应用标题和标签 app_title: 'Fab Helper', free_label: 'Free', operation_log: '📝 Operation Log', position_indicator: '📍 ', // 按钮文本 clear_all_data: '🗑️ Clear All Data', debug_mode: 'Debug Mode', page_diagnosis: 'Page Diagnosis', copy_btn: 'Copy', clear_btn: 'Clear', copied_success: 'Copied!', // 状态文本 status_history: 'Status Cycle History', script_startup: 'Script Startup', normal_period: 'Normal Operation', rate_limited_period: 'Rate Limited', current_normal: 'Current: Normal', current_rate_limited: 'Current: Rate Limited', no_history: 'No history records to display.', no_saved_position: 'No saved position', // 状态历史详细信息 time_label: 'Time', info_label: 'Info', ended_at: 'Ended at', duration_label: 'Duration', requests_label: 'Requests', requests_unit: 'times', unknown_duration: 'Unknown', // 日志消息 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...', log_recon_reset: 'Recon progress has been reset. Next scan will start from the beginning.', log_recon_active: 'Cannot reset progress while recon is active.', log_no_failed_tasks: 'No failed tasks to retry.', log_requeuing_tasks: 'Re-queuing {0} failed tasks...', log_detail_page: 'This is a detail or worker page. Halting main script execution.', log_copy_failed: 'Failed to copy log:', log_auto_add_enabled: '"Auto add" is enabled. Will process all tasks in the current "To-Do" queue.', log_auto_add_toggle: 'Infinite scroll auto add tasks {0}.', log_remember_pos_toggle: 'Remember waterfall browsing position {0}.', log_auto_resume_toggle: '429 auto resume function {0}.', log_auto_resume_start: '🔄 429 auto resume activated! Will refresh page in {0} seconds to attempt recovery...', log_auto_resume_detect: '🔄 Detected 429 error, will auto refresh page in {0} seconds to attempt recovery...', // 调试日志消息 debug_save_cursor: 'Saving new recovery point: {0}', debug_prepare_hide: 'Preparing to hide {0} cards, will use longer delay...', debug_unprocessed_cards: 'Detected {0} unprocessed or inconsistent cards, re-executing hide logic', debug_new_content_loading: 'Detected new content loading, waiting for API requests to complete...', debug_process_new_content: 'Starting to process newly loaded content...', debug_unprocessed_cards_simple: 'Detected unprocessed cards, re-executing hide logic', debug_hide_completed: 'Completed hiding all {0} cards', debug_visible_after_hide: '👁️ Actual visible items after hiding: {0}, hidden items: {1}', debug_filter_owned: 'Filtered out {0} owned items and {1} items already in todo list.', debug_api_wait_complete: 'API wait completed, starting to process {0} cards...', debug_api_stopped: 'API activity stopped for {0}ms, continuing to process cards.', debug_wait_api_response: 'Starting to wait for API response, will process {0} cards after API activity stops...', debug_api_wait_in_progress: 'API wait process already in progress, adding current {0} cards to wait queue.', debug_cached_items: 'Cached {0} item data', debug_no_cards_to_check: 'No cards need to be checked', // Fab DOM Refresh 相关 fab_dom_api_complete: 'API query completed, confirmed {0} owned items.', fab_dom_checking_status: 'Checking status of {0} items...', fab_dom_add_to_waitlist: 'Added {0} item IDs to wait list, current wait list size: {0}', fab_dom_unknown_status: '{0} items have unknown status, waiting for native web requests to update', // 状态监控 status_monitor_all_hidden: 'Detected all items hidden in normal state ({0} items)', // 空搜索结果 empty_search_initial: 'Page just loaded, might be initial request, not triggering rate limit', // 游标相关 cursor_patched_url: 'Patched URL', cursor_injecting: 'Injecting cursor. Original', page_patcher_match: '-> ✅ MATCH! URL will be patched', // 自动刷新相关 auto_refresh_countdown: '⏱️ Auto refresh countdown: {0} seconds...', rate_limit_success_request: 'Successful request during rate limit +1, current consecutive: {0}/{1}, source: {2}', rate_limit_no_visible_continue: '🔄 No visible items on page and in rate limit state, will continue auto refresh.', rate_limit_no_visible_suggest: '🔄 In rate limit state with no visible items, suggest refreshing page', status_check_summary: '📊 Status check - Actually visible: {0}, Total cards: {1}, Hidden items: {2}', refresh_plan_exists: 'Refresh plan already in progress, not scheduling new refresh (429 auto recovery)', page_content_rate_limit_detected: '[Page Content Detection] Detected page showing rate limit error message!', last_moment_check_cancelled: '⚠️ Last moment check: refresh conditions not met, auto refresh cancelled.', refresh_cancelled_visible_items: '⏹️ Detected {0} visible items on page before refresh, auto refresh cancelled.', // 限速检测来源 rate_limit_source_page_content: 'Page Content Detection', rate_limit_source_global_call: 'Global Call', // 日志标签 log_tag_auto_add: 'Auto Add', // 自动添加相关消息 auto_add_api_timeout: 'API wait timeout, waited {0}ms, will continue processing cards.', auto_add_api_error: 'Error while waiting for API: {0}', auto_add_new_tasks: 'Added {0} new tasks to queue.', // HTTP状态检测 http_status_check_performance_api: 'Using Performance API check, no longer sending HEAD requests', // 页面状态检测 page_status_hidden_no_visible: '👁️ Detected {0} hidden items on page, but no visible items', page_status_suggest_refresh: '🔄 Detected {0} hidden items on page, but no visible items, suggest refreshing page', // 限速状态相关 rate_limit_already_active: 'Already in rate limit state, source: {0}, ignoring new rate limit trigger: {1}', xhr_detected_429: '[XHR] Detected 429 status code: {0}', // 状态历史消息 history_cleared_new_session: 'History cleared, new session started', status_history_cleared: 'Status history cleared.', duplicate_normal_status_detected: 'Detected duplicate normal status record, source: {0}', execution_status_changed: 'Detected execution status change: {0}', status_executing: 'Executing', status_stopped: 'Stopped', // 状态历史UI文本 status_duration_label: 'Duration: ', status_requests_label: 'Requests: ', status_ended_at_label: 'Ended at: ', status_started_at_label: 'Started at: ', status_ongoing_label: 'Ongoing: ', status_unknown_time: 'Unknown time', status_unknown_duration: 'Unknown', // 启动时状态检测 startup_rate_limited: 'Script started in rate limited state. Rate limit has lasted at least {0}s, source: {1}', status_unknown_source: 'Unknown', // 请求成功来源 request_source_search_response: 'Search Response Success', request_source_xhr_search: 'XHR Search Success', request_source_xhr_item: 'XHR Item Request', consecutive_success_exit: 'Consecutive {0} successful requests ({1})', search_response_parse_failed: 'Search response parsing failed: {0}', // 缓存清理和Fab DOM相关 cache_cleanup_complete: '[Cache] Cleanup complete, current cache size: items={0}, owned status={1}, prices={2}', fab_dom_no_new_owned: '[Fab DOM Refresh] API query completed, no new owned items found.', // 状态报告UI标签 status_time_label: 'Time', status_info_label: 'Info', // 隐性限速检测和API监控 implicit_rate_limit_detection: '[Implicit Rate Limit Detection]', scroll_api_monitoring: '[Scroll API Monitoring]', task_execution_time: 'Task execution time: {0} seconds', detected_rate_limit_error: 'Detected rate limit error info: {0}', detected_possible_rate_limit_empty: 'Detected possible rate limit situation (empty result): {0}', detected_possible_rate_limit_scroll: 'Detected possible rate limit situation: no card count increase after {0} consecutive scrolls.', detected_api_429_status: 'Detected API request status code 429: {0}', detected_api_rate_limit_content: 'Detected API response content contains rate limit info: {0}', // 限速来源标识 source_implicit_rate_limit: 'Implicit Rate Limit Detection', source_scroll_api_monitoring: 'Scroll API Monitoring', // 设置项 setting_auto_refresh: 'Auto refresh when no items visible', setting_auto_add_scroll: 'Auto add tasks on infinite scroll', setting_remember_position: 'Remember waterfall browsing position', setting_auto_resume_429: 'Auto resume after 429 errors', setting_debug_tooltip: 'Enable detailed logging for troubleshooting', // 状态文本 status_enabled: 'enabled', status_disabled: 'disabled', // 确认对话框 confirm_clear_data: 'Are you sure you want to clear all locally stored script data (completed, failed, to-do lists)? This action cannot be undone!', confirm_open_failed: 'Are you sure you want to open {0} failed items in new tabs?', confirm_clear_history: 'Are you sure you want to clear all status history records?', // 错误提示 error_api_refresh: 'API refresh failed. Please check console for error details and confirm you are logged in.', // 工具提示 tooltip_open_failed: 'Click to open all failed items', tooltip_executing_progress: 'Executing: {0}/{1} ({2}%)', tooltip_executing: 'Executing', tooltip_start_tasks: 'Click to start executing tasks', // 其他 goto_page_label: 'Page:', goto_page_btn: 'Go', page_reset: 'Page: 1', untitled: 'Untitled', cursor_mode: 'Cursor Mode', using_native_requests: 'Using native web requests, waiting: {0}', worker_closed: 'Worker tab closed before completion' }, zh: { // 基础UI hide: '隐藏已得', show: '显示已得', sync: '同步状态', execute: '一键开刷', executing: '执行中...', stopExecute: '停止', added: '已入库', failed: '失败', todo: '待办', hidden: '已隐藏', visible: '可见', clearLog: '清空日志', copyLog: '复制日志', copied: '已复制!', tab_dashboard: '仪表盘', tab_settings: '设定', tab_debug: '调试', // 应用标题和标签 app_title: 'Fab Helper', free_label: '免费', operation_log: '📝 操作日志', position_indicator: '📍 ', // 按钮文本 clear_all_data: '🗑️ 清空所有存档', debug_mode: '调试模式', page_diagnosis: '页面诊断', copy_btn: '复制', clear_btn: '清空', copied_success: '已复制!', // 状态文本 status_history: '状态周期历史记录', script_startup: '脚本启动', normal_period: '正常运行期', rate_limited_period: '限速期', current_normal: '当前: 正常运行', current_rate_limited: '当前: 限速中', no_history: '没有可显示的历史记录。', no_saved_position: '无保存位置', // 状态历史详细信息 time_label: '时间', info_label: '信息', ended_at: '结束于', duration_label: '持续', requests_label: '请求', requests_unit: '次', unknown_duration: '未知', // 日志消息 log_init: '助手已上线!', log_db_loaded: '正在读取存档...', log_exec_no_tasks: '"待办"清单是空的。', log_verify_success: '搞定!已成功入库。', log_verify_fail: '哎呀,这个没加上。稍后会自动重试!', log_429_error: '请求太快被服务器限速了!休息15秒后自动重试...', log_recon_reset: '重置进度已完成。下次扫描将从头开始。', log_recon_active: '扫描正在进行中,无法重置进度。', log_no_failed_tasks: '没有失败的任务需要重试。', log_requeuing_tasks: '正在重新排队 {0} 个失败任务...', log_detail_page: '这是详情页或工作标签页。停止主脚本执行。', log_copy_failed: '复制日志失败:', log_auto_add_enabled: '"自动添加"已开启。将直接处理当前"待办"队列中的所有任务。', log_auto_add_toggle: '无限滚动自动添加任务已{0}。', log_remember_pos_toggle: '记住瀑布流浏览位置功能已{0}。', log_auto_resume_toggle: '429后自动恢复功能已{0}。', log_auto_resume_start: '🔄 429自动恢复启动!将在{0}秒后刷新页面尝试恢复...', log_auto_resume_detect: '🔄 检测到429错误,将在{0}秒后自动刷新页面尝试恢复...', // 调试日志消息 debug_save_cursor: '保存新的恢复点: {0}', debug_prepare_hide: '准备隐藏 {0} 张卡片,将使用更长的延迟...', debug_unprocessed_cards: '检测到 {0} 个未处理或状态不一致的卡片,重新执行隐藏逻辑', debug_new_content_loading: '检测到新内容加载,等待API请求完成...', debug_process_new_content: '开始处理新加载的内容...', debug_unprocessed_cards_simple: '检测到未处理的卡片,重新执行隐藏逻辑', debug_hide_completed: '已完成所有 {0} 张卡片的隐藏', debug_visible_after_hide: '👁️ 隐藏后实际可见商品数: {0},隐藏商品数: {1}', debug_filter_owned: '过滤掉 {0} 个已入库商品和 {1} 个已在待办列表中的商品。', debug_api_wait_complete: 'API等待完成,开始处理 {0} 张卡片...', debug_api_stopped: 'API活动已停止 {0}ms,继续处理卡片。', debug_wait_api_response: '开始等待API响应,将在API活动停止后处理 {0} 张卡片...', debug_api_wait_in_progress: '已有API等待过程在进行,将当前 {0} 张卡片加入等待队列。', debug_cached_items: '已缓存 {0} 个商品数据', debug_no_cards_to_check: '没有需要检查的卡片', // Fab DOM Refresh 相关 fab_dom_api_complete: 'API查询完成,共确认 {0} 个已拥有的项目。', fab_dom_checking_status: '正在检查 {0} 个项目的状态...', fab_dom_add_to_waitlist: '添加 {0} 个商品ID到等待列表,当前等待列表大小: {0}', fab_dom_unknown_status: '有 {0} 个商品状态未知,等待网页原生请求更新', // 状态监控 status_monitor_all_hidden: '检测到正常状态下所有商品都被隐藏 ({0}个)', // 空搜索结果 empty_search_initial: '页面刚刚加载,可能是初始请求,不触发限速', // 游标相关 cursor_patched_url: 'Patched URL', cursor_injecting: 'Injecting cursor. Original', page_patcher_match: '-> ✅ MATCH! URL will be patched', // 自动刷新相关 auto_refresh_countdown: '⏱️ 自动刷新倒计时: {0} 秒...', rate_limit_success_request: '限速状态下成功请求 +1,当前连续成功: {0}/{1},来源: {2}', rate_limit_no_visible_continue: '🔄 页面上没有可见商品且处于限速状态,将继续自动刷新。', rate_limit_no_visible_suggest: '🔄 处于限速状态且没有可见商品,建议刷新页面', status_check_summary: '📊 状态检查 - 实际可见: {0}, 总卡片: {1}, 隐藏商品数: {2}', refresh_plan_exists: '已有刷新计划正在进行中,不再安排新的刷新 (429自动恢复)', page_content_rate_limit_detected: '[页面内容检测] 检测到页面显示限速错误信息!', last_moment_check_cancelled: '⚠️ 最后一刻检查:刷新条件不满足,自动刷新已取消。', refresh_cancelled_visible_items: '⏹️ 刷新前检测到页面上有 {0} 个可见商品,已取消自动刷新。', // 限速检测来源 rate_limit_source_page_content: '页面内容检测', rate_limit_source_global_call: '全局调用', // 日志标签 log_tag_auto_add: '自动添加', // 自动添加相关消息 auto_add_api_timeout: 'API等待超时,已等待 {0}ms,将继续处理卡片。', auto_add_api_error: '等待API时出错: {0}', auto_add_new_tasks: '新增 {0} 个任务到队列。', // HTTP状态检测 http_status_check_performance_api: '使用Performance API检查,不再发送HEAD请求', // 页面状态检测 page_status_hidden_no_visible: '👁️ 检测到页面上有 {0} 个隐藏商品,但没有可见商品', page_status_suggest_refresh: '🔄 检测到页面上有 {0} 个隐藏商品,但没有可见商品,建议刷新页面', // 限速状态相关 rate_limit_already_active: '已处于限速状态,来源: {0},忽略新的限速触发: {1}', xhr_detected_429: '[XHR] 检测到429状态码: {0}', // 状态历史消息 history_cleared_new_session: '历史记录已清空,新会话开始', status_history_cleared: '状态历史记录已清空。', duplicate_normal_status_detected: '检测到重复的正常状态记录,来源: {0}', execution_status_changed: '检测到执行状态变化:{0}', status_executing: '执行中', status_stopped: '已停止', // 状态历史UI文本 status_duration_label: '持续时间: ', status_requests_label: '期间请求数: ', status_ended_at_label: '结束于: ', status_started_at_label: '开始于: ', status_ongoing_label: '已持续: ', status_unknown_time: '未知时间', status_unknown_duration: '未知', // 启动时状态检测 startup_rate_limited: '脚本启动时处于限速状态。限速已持续至少 {0}s,来源: {1}', status_unknown_source: '未知', // 请求成功来源 request_source_search_response: '搜索响应成功', request_source_xhr_search: 'XHR搜索成功', request_source_xhr_item: 'XHR商品请求', consecutive_success_exit: '连续{0}次成功请求 ({1})', search_response_parse_failed: '搜索响应解析失败: {0}', // 缓存清理和Fab DOM相关 cache_cleanup_complete: '[Cache] 清理完成,当前缓存大小: 商品={0}, 拥有状态={1}, 价格={2}', fab_dom_no_new_owned: '[Fab DOM Refresh] API查询完成,没有发现新的已拥有项目。', // 状态报告UI标签 status_time_label: '时间', status_info_label: '信息', // 隐性限速检测和API监控 implicit_rate_limit_detection: '[隐性限速检测]', scroll_api_monitoring: '[滚动API监控]', task_execution_time: '任务执行时间: {0}秒', detected_rate_limit_error: '检测到限速错误信息: {0}', detected_possible_rate_limit_empty: '检测到可能的限速情况(空结果): {0}', detected_possible_rate_limit_scroll: '检测到可能的限速情况:连续{0}次滚动后卡片数量未增加。', detected_api_429_status: '检测到API请求状态码为429: {0}', detected_api_rate_limit_content: '检测到API响应内容包含限速信息: {0}', // 限速来源标识 source_implicit_rate_limit: '隐性限速检测', source_scroll_api_monitoring: '滚动API监控', // 设置项 setting_auto_refresh: '无商品可见时自动刷新', setting_auto_add_scroll: '无限滚动时自动添加任务', setting_remember_position: '记住瀑布流浏览位置', setting_auto_resume_429: '429后自动恢复并继续', setting_debug_tooltip: '启用详细日志记录,用于排查问题', // 状态文本 status_enabled: '开启', status_disabled: '关闭', // 确认对话框 confirm_clear_data: '您确定要清空所有本地存储的脚本数据(已完成、失败、待办列表)吗?此操作不可逆!', confirm_open_failed: '您确定要在新标签页中打开 {0} 个失败的项目吗?', confirm_clear_history: '您确定要清空所有状态历史记录吗?', // 错误提示 error_api_refresh: 'API 刷新失败。请检查控制台中的错误信息,并确认您已登录。', // 工具提示 tooltip_open_failed: '点击打开所有失败的项目', tooltip_executing_progress: '执行中: {0}/{1} ({2}%)', tooltip_executing: '执行中', tooltip_start_tasks: '点击开始执行任务', // 其他 goto_page_label: '页码:', goto_page_btn: '跳转', page_reset: 'Page: 1', untitled: 'Untitled', cursor_mode: 'Cursor Mode', using_native_requests: '使用网页原生请求,等待中: {0}', worker_closed: '工作标签页在完成前关闭' } }, // 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), }; // --- 模块二: 全局状态管理 --- const State = { db: { todo: [], // 待办任务列表 done: [], // 已完成任务列表 failed: [], // 失败任务列表 }, hideSaved: false, // 是否隐藏已保存项目 autoAddOnScroll: false, // 是否在滚动时自动添加任务 rememberScrollPosition: false, // 是否记住滚动位置 autoResumeAfter429: false, // 是否在429后自动恢复 autoRefreshEmptyPage: true, // 新增:无商品可见时自动刷新(默认开启) debugMode: false, // 是否启用调试模式 lang: 'zh', // 当前语言,默认中文,会在detectLanguage中更新 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', // 默认排序方式 }; // --- 模块三: 页面状态诊断工具 --- const PageDiagnostics = { // 诊断商品详情页面状态 diagnoseDetailPage: () => { const report = { timestamp: new Date().toISOString(), url: window.location.href, pageTitle: document.title, buttons: [], licenseOptions: [], priceInfo: {}, ownedStatus: {}, dynamicContent: {} }; // 检测所有按钮 const buttons = document.querySelectorAll('button'); buttons.forEach((btn, index) => { const text = btn.textContent?.trim(); const isVisible = btn.offsetParent !== null; const isDisabled = btn.disabled; const classes = btn.className; if (text) { report.buttons.push({ index, text, isVisible, isDisabled, classes, hasClickHandler: btn.onclick !== null }); } }); // 检测许可选择相关元素 const licenseElements = document.querySelectorAll('[class*="license"], [class*="License"], [role="option"]'); licenseElements.forEach((elem, index) => { const text = elem.textContent?.trim(); const isVisible = elem.offsetParent !== null; if (text) { report.licenseOptions.push({ index, text, isVisible, tagName: elem.tagName, classes: elem.className, role: elem.getAttribute('role') }); } }); // 检测价格信息 const priceElements = document.querySelectorAll('[class*="price"], [class*="Price"]'); priceElements.forEach((elem, index) => { const text = elem.textContent?.trim(); if (text) { report.priceInfo[`price_${index}`] = { text, isVisible: elem.offsetParent !== null, classes: elem.className }; } }); // 检测拥有状态相关元素 const ownedElements = document.querySelectorAll('h2, [class*="owned"], [class*="library"]'); ownedElements.forEach((elem, index) => { const text = elem.textContent?.trim(); if (text && (text.includes('库') || text.includes('Library') || text.includes('拥有') || text.includes('Owned'))) { report.ownedStatus[`owned_${index}`] = { text, isVisible: elem.offsetParent !== null, tagName: elem.tagName, classes: elem.className }; } }); return report; }, // 输出诊断报告到日志 logDiagnosticReport: (report) => { console.log('=== 页面状态诊断报告 ==='); console.log(`页面: ${report.url}`); console.log(`标题: ${report.pageTitle}`); console.log(`--- 按钮信息 (${report.buttons.length}个) ---`); report.buttons.forEach(btn => { if (btn.isVisible) { console.log(`按钮: "${btn.text}" (可见: ${btn.isVisible}, 禁用: ${btn.isDisabled})`); } }); console.log(`--- 许可选项 (${report.licenseOptions.length}个) ---`); report.licenseOptions.forEach(opt => { if (opt.isVisible) { console.log(`许可: "${opt.text}" (可见: ${opt.isVisible}, 角色: ${opt.role})`); } }); console.log(`--- 价格信息 ---`); Object.entries(report.priceInfo).forEach(([, price]) => { if (price.isVisible) { console.log(`价格: "${price.text}"`); } }); console.log(`--- 拥有状态 ---`); Object.entries(report.ownedStatus).forEach(([, status]) => { if (status.isVisible) { console.log(`状态: "${status.text}"`); } }); console.log('=== 诊断报告结束 ==='); } }; // --- 模块四: 日志与工具函数 --- 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, ...args) => { let text = (Config.TEXTS[State.lang]?.[key]) || (Config.TEXTS['en']?.[key]) || key; // 支持两种格式的参数替换 if (args.length > 0) { // 如果第一个参数是对象,使用 %placeholder% 格式 if (typeof args[0] === 'object' && args[0] !== null) { const replacements = args[0]; for (const placeholder in replacements) { text = text.replace(`%${placeholder}%`, replacements[placeholder]); } } else { // 否则使用 {0}, {1}, {2} 格式 args.forEach((arg, index) => { text = text.replace(new RegExp(`\\{${index}\\}`, 'g'), arg); }); } } return text; }, detectLanguage: () => { const oldLang = State.lang; State.lang = window.location.href.includes('/zh-cn/') ? 'zh' : 'en'; Utils.logger('info', `语言检测: 地址=${window.location.href}, 检测到语言=${State.lang}${oldLang !== State.lang ? ` (从${oldLang}切换)` : ''}`); // 如果语言发生了变化且UI已经创建,更新UI if (oldLang !== State.lang && State.UI.container) { Utils.logger('info', `语言已切换到${State.lang},正在更新界面...`); UI.update(); } }, 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 = []; }, // 添加游标解码函数 decodeCursor: (cursor) => { if (!cursor) return '无保存位置'; try { // Base64解码 const decoded = atob(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', `游标解码失败: ${e.message}`); return '位置: (格式无法解析)'; } }, // 账号验证函数 checkAuthentication: () => { const csrfToken = Utils.getCookie('fab_csrftoken'); if (!csrfToken) { Utils.logger('error', '账号失效:未找到 CSRF token,请重新登录'); // 停止执行状态 if (State.isExecuting) { State.isExecuting = false; GM_setValue(Config.DB_KEYS.IS_EXECUTING, false); } // 更新UI显示 if (State.UI.startStopButton) { State.UI.startStopButton.textContent = Utils.getText('start_execution'); State.UI.startStopButton.disabled = true; } // 显示警告信息 alert('账号失效:请重新登录后再使用脚本'); return false; } return true; }, }; // --- DOM Creation Helpers (moved outside for broader scope) --- // 移除createOwnedElement函数,不再手动添加"已保存在我的库中"标记 // createFreeElement函数已移除,不再使用 // --- 新增: 数据缓存系统 --- 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] ${Utils.getText('fab_dom_add_to_waitlist', uids.length, 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', Utils.getText('cache_cleanup_complete', this.listings.size, this.ownedStatus.size, this.prices.size)); } } catch (e) { Utils.logger('error', `缓存清理失败: ${e.message}`); } } }; // --- 模块四: 异步网络请求 --- 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.')) }); }); }, // 接口响应数据提取函数 extractStateData: (rawData, source = '') => { // 记录原始数据格式 const dataType = Array.isArray(rawData) ? 'Array' : typeof rawData; if (State.debugMode) { Utils.logger('debug', `[${source}] 接口返回数据类型: ${dataType}`); } // 如果是数组,直接返回 if (Array.isArray(rawData)) { return rawData; } // 如果是对象,尝试提取可能的数组字段 if (rawData && typeof rawData === 'object') { // 记录对象的顶级键 const keys = Object.keys(rawData); if (State.debugMode) { Utils.logger('debug', `[${source}] 接口返回对象键: ${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('debug', Utils.getText('fab_dom_unknown_status', 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) { Utils.checkAuthentication(); 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 ... }; // --- 模块五: 数据库交互 --- const Database = { load: async () => { // 从存储中加载待办列表 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(Utils.getText('confirm_clear_data'))) { // 清除待办列表 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(); } }, }; // 集中处理限速状态的函数 const RateLimitManager = { // 添加防止重复日志的变量 _lastLogTime: 0, _lastLogType: null, _duplicateLogCount: 0, // 检查是否与最后一条记录重复 isDuplicateRecord: function(newEntry) { if (State.statusHistory.length === 0) return false; const lastEntry = State.statusHistory[State.statusHistory.length - 1]; // 检查类型是否相同 if (lastEntry.type !== newEntry.type) return false; // 检查时间是否过于接近(10秒内) const lastTime = new Date(lastEntry.endTime).getTime(); const newTime = new Date(newEntry.endTime).getTime(); const timeDiff = Math.abs(newTime - lastTime); if (timeDiff < 10000) { // 10秒内 // 如果是相同类型且时间很接近,检查持续时间是否相似 const durationDiff = Math.abs((lastEntry.duration || 0) - (newEntry.duration || 0)); if (durationDiff < 5) { // 持续时间差异小于5秒 return true; } } return false; }, // 添加记录到历史,带去重检查 addToHistory: async function(entry) { // 检查是否重复 if (this.isDuplicateRecord(entry)) { Utils.logger('debug', `检测到重复的状态记录,跳过: ${entry.type} - ${entry.endTime}`); return false; } // 添加到历史记录 State.statusHistory.push(entry); // 限制历史记录数量,保留最近50条 if (State.statusHistory.length > 50) { State.statusHistory = State.statusHistory.slice(-50); } // 保存到存储 await GM_setValue(Config.DB_KEYS.STATUS_HISTORY, State.statusHistory); return true; }, // 进入限速状态 enterRateLimitedState: async function(source = '未知来源') { // 如果已经处于限速状态,不需要重复处理 if (State.appStatus === 'RATE_LIMITED') { Utils.logger('info', Utils.getText('rate_limit_already_active', 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 logEntry = { type: 'NORMAL', duration: parseFloat(normalDuration), requests: State.successfulSearchCount, endTime: new Date().toISOString() }; // 使用新的去重方法添加到历史记录 const wasAdded = await this.addToHistory(logEntry); if (wasAdded) { Utils.logger('error', `🚨 RATE LIMIT DETECTED from [${source}]! Normal operation lasted ${normalDuration}s with ${State.successfulSearchCount} successful search requests.`); } else { Utils.logger('debug', Utils.getText('duplicate_normal_status_detected', source)); } // 切换到限速状态 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', Utils.getText('log_auto_resume_start', randomDelay ? (randomDelay/1000).toFixed(1) : '未知')); } else { // 添加空值检查,防止randomDelay为null Utils.logger('info', Utils.getText('log_auto_resume_detect', 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', Utils.getText('rate_limit_success_request', State.consecutiveSuccessCount, State.requiredSuccessCount, source)); // 如果达到所需的连续成功数,退出限速状态 if (State.consecutiveSuccessCount >= State.requiredSuccessCount) { await this.exitRateLimitedState(Utils.getText('consecutive_success_exit', 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 logEntry = { type: 'RATE_LIMITED', duration: parseFloat(rateLimitDuration), endTime: new Date().toISOString(), source: source }; // 使用新的去重方法添加到历史记录 const wasAdded = await this.addToHistory(logEntry); if (wasAdded) { Utils.logger('info', `✅ Rate limit appears to be lifted from [${source}]. The 429 period lasted ${rateLimitDuration}s.`); } else { Utils.logger('debug', `检测到重复的限速状态记录,来源: ${source}`); } // 恢复到正常状态 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('debug', '使用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(() => { // 如果URL发生变化,检查排序参数和语言 if (window.location.href !== this._lastCheckedUrl) { this._lastCheckedUrl = window.location.href; this.checkCurrentSortFromUrl(); // 重新检测语言 Utils.detectLanguage(); } }); bodyObserver.observe(document.body, { childList: true, subtree: true }); // 保存引用以便后续可以断开 this._bodyObserver = bodyObserver; } // 监听popstate事件(浏览器前进/后退按钮) window.addEventListener('popstate', () => { this.checkCurrentSortFromUrl(); Utils.detectLanguage(); }); // 监听hashchange事件 window.addEventListener('hashchange', () => { this.checkCurrentSortFromUrl(); Utils.detectLanguage(); }); // 保存当前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.getText('no_saved_position'); } 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(Utils.getText('request_source_search_response'), hasResults); } } catch (e) { Utils.logger('warn', Utils.getText('search_response_parse_failed', 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); // 日志记录保存操作 if (State.debugMode) { Utils.logger('debug', `[Cursor] ${Utils.getText('debug_save_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(Utils.getText('request_source_xhr_item'), true); } // 对所有请求检查429错误 if (request.status === 429 || request.status === '429' || request.status.toString() === '429') { Utils.logger('warn', Utils.getText('xhr_detected_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', `${Utils.getText('implicit_rate_limit_detection')} ${Utils.getText('detected_rate_limit_error', 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', `[空搜索结果] ${Utils.getText('empty_search_initial')}: ${JSON.stringify(data).substring(0, 200)}...`); return; } else { Utils.logger('warn', `${Utils.getText('implicit_rate_limit_detection')} ${Utils.getText('detected_possible_rate_limit_empty', JSON.stringify(data).substring(0, 200) + '...')}`); RateLimitManager.enterRateLimitedState('XHR响应空结果'); return; } } // 如果是搜索请求且有结果,记录成功请求 if (self.isDebounceableSearch(request._url) && data.results && data.results.length > 0) { RateLimitManager.recordSuccessfulRequest(Utils.getText('request_source_xhr_search'), 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 if (State.debugMode) { Utils.logger('debug', `[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(() => { if (State.debugMode) { Utils.logger('debug', `[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') { // 克隆响应以避免"已消费"错误 // 克隆响应以避免"已消费"错误(但这里不需要使用) 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 (!Utils.checkAuthentication()) { return; } 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', Utils.getText('log_auto_add_enabled')); // 先检查当前页面上的卡片状态,更新数据库 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() || Utils.getText('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('debug', 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 (!Utils.checkAuthentication()) { return; } 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() || Utils.getText('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', Utils.getText('log_auto_add_toggle', State.autoAddOnScroll ? Utils.getText('status_enabled') : Utils.getText('status_disabled'))); // 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', Utils.getText('log_auto_resume_toggle', State.autoResumeAfter429 ? Utils.getText('status_enabled') : Utils.getText('status_disabled'))); setTimeout(() => { State.isTogglingSetting = false; }, 200); }, toggleRememberPosition: async () => { if (State.isTogglingSetting) return; State.isTogglingSetting = true; State.rememberScrollPosition = !State.rememberScrollPosition; await Database.saveRememberPosPref(); Utils.logger('info', Utils.getText('log_remember_pos_toggle', State.rememberScrollPosition ? Utils.getText('status_enabled') : Utils.getText('status_disabled'))); 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) { Utils.checkAuthentication(); 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.isReconning) { Utils.logger('warn', Utils.getText('log_recon_active')); return; } await GM_deleteValue(Config.DB_KEYS.NEXT_URL); if (State.UI.reconProgressDisplay) { State.UI.reconProgressDisplay.textContent = Utils.getText('page_reset'); } Utils.logger('info', Utils.getText('log_recon_reset')); }, 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) { Utils.checkAuthentication(); 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] ${Utils.getText('fab_dom_api_complete', 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 3.5: Remove non-free items from the Failed list try { const failedTasksSnapshot = [...State.db.failed]; Utils.logger('info', `[Fab DB Sync] 开始检查失败列表中的 ${failedTasksSnapshot.length} 个商品的价格状态...`); if (failedTasksSnapshot.length > 0) { // Map failed UID -> offerId (from cached listings) const uidToOfferId = new Map(); let foundOfferIds = 0; const missingCacheUids = []; failedTasksSnapshot.forEach(task => { const listing = DataCache.listings.get(task.uid); const offerId = listing?.startingPrice?.offerId; if (offerId) { uidToOfferId.set(task.uid, offerId); foundOfferIds++; } else { missingCacheUids.push(task.uid); Utils.logger('debug', `[Fab DB Sync] 商品 ${task.uid} 没有找到缓存的商品信息或价格ID`); } }); Utils.logger('info', `[Fab DB Sync] 在 ${failedTasksSnapshot.length} 个失败商品中找到了 ${foundOfferIds} 个有价格ID的商品`); // 对于没有缓存数据的商品,尝试重新获取信息 if (missingCacheUids.length > 0) { Utils.logger('info', `[Fab DB Sync] 尝试为 ${missingCacheUids.length} 个缺失缓存的商品重新获取信息...`); try { const csrfToken = Utils.getCookie('fab_csrftoken'); if (csrfToken) { // 分批查询商品信息 const SEARCH_CHUNK_SIZE = 5; // 每次查询5个商品 for (let i = 0; i < missingCacheUids.length; i += SEARCH_CHUNK_SIZE) { const chunk = missingCacheUids.slice(i, i + SEARCH_CHUNK_SIZE); for (const uid of chunk) { try { const searchUrl = `https://www.fab.com/i/listings/search?q=${uid}`; const response = await fetch(searchUrl, { headers: { 'accept': 'application/json, text/plain, */*', 'x-csrftoken': csrfToken, 'x-requested-with': 'XMLHttpRequest' } }); if (response.ok) { const searchData = await response.json(); if (searchData.results && searchData.results.length > 0) { // 找到匹配的商品 const matchedItem = searchData.results.find(item => item.uid === uid); if (matchedItem && matchedItem.startingPrice?.offerId) { // 缓存商品信息 DataCache.saveListings([matchedItem]); // 添加到offerId映射 uidToOfferId.set(uid, matchedItem.startingPrice.offerId); foundOfferIds++; Utils.logger('debug', `[Fab DB Sync] 成功获取商品 ${uid} 的价格ID: ${matchedItem.startingPrice.offerId}`); } } } // 添加延迟避免请求过快 await new Promise(r => setTimeout(r, 200)); } catch (e) { Utils.logger('debug', `[Fab DB Sync] 获取商品 ${uid} 信息失败: ${e.message}`); } } } Utils.logger('info', `[Fab DB Sync] 重新获取完成,现在总共有 ${foundOfferIds} 个商品有价格ID`); } } catch (e) { Utils.logger('warn', `[Fab DB Sync] 重新获取商品信息时出错: ${e.message}`); } } const offerIds = Array.from(uidToOfferId.values()); if (offerIds.length > 0) { const CHUNK = 50; const nonFreeOfferIds = new Set(); Utils.logger('info', `[Fab DB Sync] 开始检查 ${offerIds.length} 个商品的价格...`); for (let i = 0; i < offerIds.length; i += CHUNK) { const chunk = offerIds.slice(i, i + CHUNK); Utils.logger('info', `[Fab DB Sync] 检查价格批次 ${Math.floor(i / CHUNK) + 1},包含 ${chunk.length} 个商品...`); const prices = await API.checkItemsPrices(chunk); Utils.logger('info', `[Fab DB Sync] 价格API返回了 ${prices.length} 个结果`); prices.forEach(offer => { if (offer && typeof offer.price === 'number' && offer.price > 0) { nonFreeOfferIds.add(offer.offerId); Utils.logger('debug', `[Fab DB Sync] 发现付费商品: ${offer.offerId}, 价格: ${offer.price}`); } else if (offer) { Utils.logger('debug', `[Fab DB Sync] 发现免费商品: ${offer.offerId}, 价格: ${offer.price}`); } }); // Gentle pacing to be safe if (offerIds.length > i + CHUNK) { await new Promise(r => setTimeout(r, 150)); } } Utils.logger('info', `[Fab DB Sync] 价格检查完成,发现 ${nonFreeOfferIds.size} 个付费商品`); if (nonFreeOfferIds.size > 0) { const before = State.db.failed.length; const removedItems = []; State.db.failed = State.db.failed.filter(task => { const offerId = uidToOfferId.get(task.uid); const shouldRemove = offerId && nonFreeOfferIds.has(offerId); if (shouldRemove) { removedItems.push(`${task.name || task.uid} (${offerId})`); } // Remove only when we are sure it's not free (price > 0) return !offerId || !nonFreeOfferIds.has(offerId); }); const removed = before - State.db.failed.length; if (removed > 0) { dbUpdated = true; Utils.logger('info', `[Fab DB Sync] 从"失败"列表中移除了 ${removed} 个非免费商品:`); removedItems.forEach(item => Utils.logger('info', ` - ${item}`)); } else { Utils.logger('info', `[Fab DB Sync] 没有找到需要移除的付费商品`); } } else { Utils.logger('info', `[Fab DB Sync] 没有发现付费商品,失败列表保持不变`); } } else { Utils.logger('info', `[Fab DB Sync] 失败列表中的商品都没有找到价格ID,跳过价格检查`); } } } catch (e) { Utils.logger('warn', `[Fab DB Sync] 检查失败项价格失败: ${e.message}`); } // 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('debug', `[Fab DOM Refresh] Complete. Updated ${updatedCount} visible card states.`); TaskRunner.runHideOrShow(); } catch (e) { Utils.logger('error', '[Fab DOM Refresh] An error occurred:', e); alert(Utils.getText('error_api_refresh')); } }, retryFailedTasks: async () => { if (State.db.failed.length === 0) { Utils.logger('info', Utils.getText('log_no_failed_tasks')); return; } const count = State.db.failed.length; Utils.logger('info', Utils.getText('log_requeuing_tasks', count)); 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 () => { 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 = Utils.getText('using_native_requests', 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; } 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 (!Utils.checkAuthentication()) { return; } // 只有主页面才需要检查是否是活跃实例 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).substring(2, 11)}`; 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 () => { // 检查账号状态 if (!Utils.checkAuthentication()) { return; } 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 { // 等待页面加载完成 await new Promise(resolve => setTimeout(resolve, 3000)); // 执行页面状态诊断 logBuffer.push(`=== 页面状态诊断开始 ===`); const diagnosticReport = PageDiagnostics.diagnoseDetailPage(); // 记录关键信息到日志缓冲区 logBuffer.push(`页面标题: ${diagnosticReport.pageTitle}`); logBuffer.push(`可见按钮数量: ${diagnosticReport.buttons.filter(btn => btn.isVisible).length}`); // 记录所有可见按钮 diagnosticReport.buttons.forEach(btn => { if (btn.isVisible) { logBuffer.push(`按钮: "${btn.text}" (禁用: ${btn.isDisabled})`); } }); // 记录价格信息 Object.entries(diagnosticReport.priceInfo).forEach(([, price]) => { if (price.isVisible) { logBuffer.push(`价格显示: "${price.text}"`); } }); // 记录许可选项 diagnosticReport.licenseOptions.forEach(opt => { if (opt.isVisible) { logBuffer.push(`许可选项: "${opt.text}"`); } }); logBuffer.push(`=== 页面状态诊断结束 ===`); // API-First Ownership Check... try { const csrfToken = Utils.getCookie('fab_csrftoken'); if (!csrfToken) { Utils.checkAuthentication(); 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) => { 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 => { if (cn.nodeType !== 3) return false; const text = cn.textContent.trim(); return [...Config.FREE_TEXT_SET].some(freeWord => text === freeWord) || text === '个人' || text === '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) { // 首先尝试找标准的添加按钮 let actionButton = [...document.querySelectorAll('button')].find(btn => [...Config.ACQUISITION_TEXT_SET].some(keyword => btn.textContent.includes(keyword)) ); // 如果没有标准添加按钮,检查是否是限时免费商品 if (!actionButton) { // 查找包含"免费/Free"和"-100%"的按钮(限时免费商品的许可按钮) actionButton = [...document.querySelectorAll('button')].find(btn => { const text = btn.textContent; const hasFreeText = [...Config.FREE_TEXT_SET].some(freeWord => text.includes(freeWord)); const hasDiscount = text.includes('-100%'); const hasPersonal = text.includes('个人') || text.includes('Personal'); return hasFreeText && hasDiscount && hasPersonal; }); if (actionButton) { logBuffer.push(`Found limited-time free license button: "${actionButton.textContent.trim()}"`); } } 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: [Utils.getText('worker_closed')], 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) { if (State.debugMode) { Utils.logger('debug', Utils.getText('debug_prepare_hide', 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) { if (State.debugMode) { Utils.logger('debug', Utils.getText('debug_hide_completed', 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) { if (State.debugMode) { Utils.logger('debug', Utils.getText('debug_unprocessed_cards_simple')); } 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; // 更新真实的可见商品数量 if (State.debugMode) { Utils.logger('debug', Utils.getText('debug_visible_after_hide', 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', Utils.getText('refresh_plan_exists').replace('(429自动恢复)', '(无商品可见)')); 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', Utils.getText('page_status_hidden_no_visible', 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('debug', `[Fab DOM Refresh] ${Utils.getText('debug_no_cards_to_check')}`); return; } Utils.logger('info', `[Fab DOM Refresh] ${Utils.getText('fab_dom_checking_status', 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] ${Utils.getText('fab_dom_api_complete', confirmedOwned)}`); // 不立即执行隐藏,而是在调用方决定何时执行 Utils.logger('info', `[Fab DOM Refresh] Complete. Updated ${confirmedOwned} visible card states.`); } else { Utils.logger('info', Utils.getText('fab_dom_no_new_owned')); } } 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', `[${Utils.getText('log_tag_auto_add')}] ${Utils.getText('debug_api_wait_in_progress', cards.length)}`); return; } // 标记开始等待API window._apiWaitStatus.isWaiting = true; window._apiWaitStatus.pendingCards = [...cards]; window._apiWaitStatus.lastApiActivity = Date.now(); if (State.debugMode) { Utils.logger('debug', `[${Utils.getText('log_tag_auto_add')}] ${Utils.getText('debug_wait_api_response', 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', `[${Utils.getText('log_tag_auto_add')}] ${Utils.getText('auto_add_api_timeout', totalWaitTime)}`); } else { Utils.logger('debug', `[${Utils.getText('log_tag_auto_add')}] ${Utils.getText('debug_api_stopped', timeSinceLastActivity)}`); } resolve(); } }, 200); // 每200ms检查一次 }); }; // 等待API完成 try { await waitForApiCompletion(); } catch (error) { Utils.logger('error', `[${Utils.getText('log_tag_auto_add')}] ${Utils.getText('auto_add_api_error', error.message)}`); } // 处理卡片 const cardsToProcess = [...window._apiWaitStatus.pendingCards]; window._apiWaitStatus.pendingCards = []; window._apiWaitStatus.isWaiting = false; if (State.debugMode) { Utils.logger('debug', `[${Utils.getText('log_tag_auto_add')}] ${Utils.getText('debug_api_wait_complete', 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*="创作的"], a[aria-label*="by "]')?.textContent.trim() || card.querySelector('a[href*="/listings/"]')?.textContent.trim() || Utils.getText('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', `[${Utils.getText('log_tag_auto_add')}] ${Utils.getText('auto_add_new_tasks', newlyAddedList.length)}`); // 保存待办列表到存储 Database.saveTodo(); } // 添加详细的过滤信息日志 if (skippedAlreadyOwned > 0 || skippedInTodo > 0) { Utils.logger('debug', `[${Utils.getText('log_tag_auto_add')}] ${Utils.getText('debug_filter_owned', 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 downloadTexts = ['下载', 'Download']; const downloadButton = [...document.querySelectorAll('a[href*="/download/"], button')].find(btn => downloadTexts.some(text => btn.textContent.includes(text)) ); 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"; // 不再需要设置type属性 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 = Utils.getText('app_title'); 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', Utils.getText('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 = Utils.getText('tooltip_open_failed'); State.UI.statusFailed.onclick = () => { if (State.db.failed.length === 0) { Utils.logger('info', '失败列表为空,无需操作。'); return; } if (window.confirm(Utils.getText('confirm_open_failed', 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 = Utils.getText('operation_log'); 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 = Utils.getText('position_indicator'); 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(Utils.getText('setting_auto_add_scroll'), 'autoAddOnScroll'); settingsContent.appendChild(autoAddSetting); const rememberPosSetting = createSettingRow(Utils.getText('setting_remember_position'), 'rememberScrollPosition'); settingsContent.appendChild(rememberPosSetting); const autoResumeSetting = createSettingRow(Utils.getText('setting_auto_resume_429'), 'autoResumeAfter429'); settingsContent.appendChild(autoResumeSetting); const autoRefreshEmptySetting = createSettingRow(Utils.getText('setting_auto_refresh'), 'autoRefreshEmptyPage'); settingsContent.appendChild(autoRefreshEmptySetting); const resetButton = document.createElement('button'); resetButton.textContent = Utils.getText('clear_all_data'); 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 = Utils.getText('setting_debug_tooltip'); const debugLabel = document.createElement('span'); debugLabel.className = 'fab-setting-label'; debugLabel.textContent = Utils.getText('debug_mode'); 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 = Utils.getText('status_history'); debugTitle.style.margin = '0'; const debugControls = document.createElement('div'); debugControls.style.cssText = 'display: flex; gap: 8px;'; const copyHistoryBtn = document.createElement('button'); copyHistoryBtn.textContent = Utils.getText('copy_btn'); 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 `🚀 ${Utils.getText('script_startup')}\n - ${Utils.getText('time_label')}: ${date}\n - ${Utils.getText('info_label')}: ${entry.message || ''}`; } else { const type = entry.type === 'NORMAL' ? `✅ ${Utils.getText('normal_period')}` : `🚨 ${Utils.getText('rate_limited_period')}`; // 添加空值检查,防止toFixed错误 let details = `${Utils.getText('duration_label')}: ${entry.duration !== undefined && entry.duration !== null ? entry.duration.toFixed(2) : Utils.getText('unknown_duration')}s`; if (entry.requests !== undefined) { details += `, ${Utils.getText('requests_label')}: ${entry.requests}${Utils.getText('requests_unit')}`; } return `${type}\n - ${Utils.getText('ended_at')}: ${date}\n - ${details}`; } }; const fullLog = State.statusHistory.map(formatEntry).join('\n\n'); navigator.clipboard.writeText(fullLog).then(() => { const originalText = copyHistoryBtn.textContent; copyHistoryBtn.textContent = Utils.getText('copied_success'); setTimeout(() => { copyHistoryBtn.textContent = originalText; }, 2000); }).catch(err => Utils.logger('error', Utils.getText('log_copy_failed'), err)); }; const clearHistoryBtn = document.createElement('button'); clearHistoryBtn.textContent = Utils.getText('clear_btn'); 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(Utils.getText('confirm_clear_history'))) { State.statusHistory = []; await GM_deleteValue(Config.DB_KEYS.STATUS_HISTORY); // 添加一个新的"当前会话"记录 const currentSessionEntry = { type: 'STARTUP', duration: 0, endTime: new Date().toISOString(), message: Utils.getText('history_cleared_new_session') }; await RateLimitManager.addToHistory(currentSessionEntry); UI.updateDebugTab(); Utils.logger('info', Utils.getText('status_history_cleared')); } }; // 添加页面诊断按钮 const diagnosisBtn = document.createElement('button'); diagnosisBtn.textContent = Utils.getText('page_diagnosis'); diagnosisBtn.className = 'fab-helper-btn'; diagnosisBtn.style.cssText = 'margin-left: 10px; background: #2196F3; color: white;'; diagnosisBtn.onclick = () => { try { const report = PageDiagnostics.diagnoseDetailPage(); PageDiagnostics.logDiagnosticReport(report); Utils.logger('info', '页面诊断完成,请查看控制台输出'); } catch (error) { Utils.logger('error', `页面诊断失败: ${error.message}`); } }; debugControls.append(copyHistoryBtn, clearHistoryBtn, diagnosisBtn); 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 Static Text Elements (for language switching) --- // 更新应用标题 const titleElement = State.UI.container.querySelector('span[style*="font-weight: 600"]'); if (titleElement) { titleElement.textContent = Utils.getText('app_title'); } // 更新标签页文本 const tabs = ['dashboard', 'settings', 'debug']; tabs.forEach((tabName) => { const tabButton = State.UI.tabs[tabName]; if (tabButton) { tabButton.textContent = Utils.getText(`tab_${tabName}`); } }); // 更新同步按钮文本 if (State.UI.syncBtn) { State.UI.syncBtn.textContent = '🔄 ' + Utils.getText('sync'); } // --- 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 = Utils.getText('tooltip_executing_progress', progress, State.executionTotalTasks, percentage); } else { State.UI.execBtn.title = Utils.getText('tooltip_executing'); } } else { State.UI.execBtn.textContent = Utils.getText('execute'); State.UI.execBtn.classList.remove('executing'); State.UI.execBtn.title = Utils.getText('tooltip_start_tasks'); } 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 = Utils.getText('script_startup'); } else if (entry.type === 'NORMAL') { icon = '✅'; color = 'var(--green)'; titleText = Utils.getText('normal_period'); } else { // RATE_LIMITED icon = '🚨'; color = 'var(--orange)'; titleText = Utils.getText('rate_limited_period'); } 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>${Utils.getText('status_time_label')}: ${new Date(entry.endTime).toLocaleString()}</div>`; if (entry.message) { detailsHtml += `<div>${Utils.getText('status_info_label')}: <strong>${entry.message}</strong></div>`; } } else { // 添加空值检查,防止toFixed错误 const duration = entry.duration !== undefined && entry.duration !== null ? entry.duration.toFixed(2) : Utils.getText('status_unknown_duration'); detailsHtml = `<div>${Utils.getText('status_duration_label')}<strong>${duration}s</strong></div>`; if (entry.requests !== undefined) { detailsHtml += `<div>${Utils.getText('status_requests_label')}<strong>${entry.requests}</strong></div>`; } // 添加空值检查,防止日期错误 const endTime = entry.endTime ? new Date(entry.endTime).toLocaleString() : Utils.getText('status_unknown_time'); detailsHtml += `<div>${Utils.getText('status_ended_at_label')}${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' ? Utils.getText('current_normal') : Utils.getText('current_rate_limited'); 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) : Utils.getText('status_unknown_duration'); let detailsHtml = `<div>${Utils.getText('status_ongoing_label')}<strong>${duration}s</strong></div>`; if (State.appStatus === 'NORMAL') { detailsHtml += `<div>${Utils.getText('status_requests_label')}<strong>${State.successfulSearchCount}</strong></div>`; } // 添加空值检查,防止startTime为null const startTimeDisplay = startTime ? new Date(startTime).toLocaleString() : Utils.getText('status_unknown_time'); detailsHtml += `<div>${Utils.getText('status_started_at_label')}${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 = Utils.getText('no_history'); 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(); // 检查账号状态 if (!Utils.checkAuthentication()) { Utils.logger('error', '账号未登录,脚本停止执行'); return; } // 检查是否是工作标签页 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', Utils.getText('startup_rate_limited', previousDuration, persistedStatus.source || Utils.getText('status_unknown_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', Utils.getText('task_execution_time', executionTime ? (executionTime / 1000).toFixed(2) : Utils.getText('status_unknown_duration'))); } // 移除此工作标签页的记录 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', Utils.getText('execution_status_changed', newValue ? Utils.getText('status_executing') : Utils.getText('status_stopped'))); 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('debug', '[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', Utils.getText('log_detail_page')); 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 = Utils.getText('rate_limit_source_global_call')) { // 使用统一的限速管理器进入限速状态 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', Utils.getText('page_content_rate_limit_detected')); RateLimitManager.enterRateLimitedState(Utils.getText('rate_limit_source_page_content')); } } }, 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(() => { if (State.debugMode) { Utils.logger('debug', `[Observer] ${Utils.getText('debug_new_content_loading')}`); } // 首先等待一段较长的时间,确保API请求有足够时间完成 setTimeout(() => { if (State.debugMode) { Utils.logger('debug', `[Observer] ${Utils.getText('debug_process_new_content')}`); } // 执行一次状态检查,尝试更新卡片状态 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('debug', `✅ 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) { if (State.debugMode) { Utils.logger('debug', Utils.getText('debug_unprocessed_cards', 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', `${Utils.getText('implicit_rate_limit_detection')} ${Utils.getText('detected_possible_rate_limit_scroll', noNewCardsCounter)}`); try { // 使用RateLimitManager处理限速 RateLimitManager.enterRateLimitedState(Utils.getText('source_implicit_rate_limit')); } catch (error) { Utils.logger('error', `处理限速出错: ${error.message}`); // 备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, Utils.getText('source_implicit_rate_limit')); } 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', Utils.getText('page_content_rate_limit_detected')); 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('debug', `[HTTP状态检测] ${Utils.getText('http_status_check_performance_api')}`); // 检查页面内容是否包含限速信息 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', `[状态监控] ${Utils.getText('status_monitor_all_hidden', 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', `${Utils.getText('scroll_api_monitoring')} ${Utils.getText('detected_api_429_status', xhr._url)}`); try { // 直接使用全局函数,避免使用PagePatcher.handleRateLimit if (typeof window.enterRateLimitedState === 'function') { window.enterRateLimitedState(); } else { // 最后的备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, Utils.getText('source_scroll_api_monitoring')); } } 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', `${Utils.getText('scroll_api_monitoring')} ${Utils.getText('detected_api_rate_limit_content', responseText)}`); try { // 直接使用全局函数,避免使用PagePatcher.handleRateLimit if (typeof window.enterRateLimitedState === 'function') { window.enterRateLimitedState(); } else { // 最后的备选方案:直接刷新页面 const randomDelay = 5000 + Math.random() * 10000; countdownRefresh(randomDelay, Utils.getText('source_scroll_api_monitoring')); } } 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', Utils.getText('refresh_plan_exists').replace('(429自动恢复)', `(${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', Utils.getText('auto_refresh_countdown', 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', Utils.getText('rate_limit_no_visible_continue')); } 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', Utils.getText('status_check_summary', actualVisibleCards, totalCards, hiddenCards)); // 如果处于限速状态且没有可见商品,直接返回false触发刷新 if (State.appStatus === 'RATE_LIMITED' && actualVisibleCards === 0) { Utils.logger('info', Utils.getText('rate_limit_no_visible_suggest')); return false; } // 即使在正常状态下,如果所有商品都被隐藏且隐藏的商品数量超过25个,也建议刷新 if (actualVisibleCards === 0 && hiddenCards > 25) { Utils.logger('info', Utils.getText('page_status_suggest_refresh', 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] ${Utils.getText('debug_cached_items', 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); } }); })();