Fab Helper Optimized - Auto-claim free items, auto-hide owned items, background multi-tab processing, smart rate-limit handling
// ==UserScript==
// @name Fab Helper
// @name:zh-CN Fab Helper
// @name:en Fab Helper
// @namespace https://www.fab.com/
// @version 3.5.7-20260709-1133
// @description Fab Helper 优化版 - 自动领取免费商品,已拥有自动隐藏,后台多标签处理,智能限速处理
// @description:zh-CN Fab Helper 优化版 - 自动领取免费商品,已拥有自动隐藏,后台多标签处理,智能限速处理
// @description:en Fab Helper Optimized - Auto-claim free items, auto-hide owned items, background multi-tab processing, smart rate-limit handling
// @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-start
// ==/UserScript==
(() => {
var __defProp = Object.defineProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
// src/i18n/en.js
var en = {
// 基础UI
hide: "Hide Done",
show: "Show Done",
sync: "Sync State",
execute: "Start Tasks",
executing: "Executing...",
stopExecute: "Stop",
retry_failed: "Retry Failed",
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",
toast_all_tasks_completed: "\u{1F389} All tasks have been completed!",
toast_reached_bottom: "\u{1F3C1} Reached the bottom of the list, all items scanned!",
close: "Close",
// 应用标题和标签
app_title: "Fab Helper",
free_label: "Free",
operation_log: "\u{1F4DD} Operation Log",
position_indicator: "\u{1F4CD} ",
// 按钮文本
clear_all_data: "\u{1F5D1}\uFE0F 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_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: "\u{1F504} 429 auto resume activated! Will refresh page in {0} seconds to attempt recovery...",
log_auto_resume_detect: "\u{1F504} Detected 429 error, will auto refresh page in {0} seconds to attempt recovery...",
log_refresh_error: "Error during state synchronization:",
log_unsettled_cards: "{0} cards are still loading. Will check again shortly.",
// 调试日志消息
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: "\u{1F441}\uFE0F 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: "-> \u2705 MATCH! URL will be patched",
// 自动刷新相关
auto_refresh_countdown: "\u23F1\uFE0F 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: "\u{1F504} No visible items on page and in rate limit state, will continue auto refresh.",
rate_limit_no_visible_suggest: "\u{1F504} In rate limit state with no visible items, suggest refreshing page",
status_check_summary: "\u{1F4CA} 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: "\u26A0\uFE0F Last moment check: refresh conditions not met, auto refresh cancelled.",
refresh_cancelled_visible_items: "\u23F9\uFE0F Detected {0} visible items on page before refresh, auto refresh cancelled.",
log_all_hidden_rate_limited: "Rate limited with no visible items; scheduling one recovery refresh.",
log_refresh_cancelled_tasks: "Detected {0} to-do tasks and {1} active workers; auto refresh cancelled.",
log_refresh_cancelled_visible: "Detected {0} visible items before refresh; auto refresh cancelled.",
log_refreshing: "No visible items and still rate limited; refreshing page.",
rate_limit_no_visible_reason: "rate limited with no visible items",
// 限速检测来源
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.",
auto_scroll_attempt: "[Auto Scroll] Queue empty, attempting to scroll down to load more... (Attempt {0}/{1})",
auto_scroll_success: "[Auto Scroll] Successfully loaded and identified {0} new tasks, continuing...",
auto_scroll_cards_loaded: "[Auto Scroll] Detected {0} new cards, continuing hide/scan and scroll...",
auto_scroll_reached_bottom: "[Auto Scroll] Reached page bottom, stopping scroll.",
auto_scroll_no_new_items: "[Auto Scroll] No new eligible items found after {0} consecutive scrolls, stopping auto scroll.",
auto_scroll_waiting: "[Auto Scroll] No new eligible items found, waiting for next scroll attempt...",
// HTTP状态检测
http_status_check_performance_api: "Using Performance API check, no longer sending HEAD requests",
// 页面状态检测
page_status_hidden_no_visible: "\u{1F441}\uFE0F Detected {0} hidden items on page, but no visible items",
page_status_suggest_refresh: "\u{1F504} 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_hide_discounted: "Hide discounted paid items",
setting_hide_paid: "Hide all paid items",
setting_block_large_resources: "Disable images and media",
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",
worker_captcha: "CAPTCHA / human verification detected (manual action needed)",
log_keepalive_on: "Background keep-alive started (Worker heartbeat + freeze guard)",
log_keepalive_failed: "Failed to start keep-alive: {0}",
log_auto_retry: "\u21BB Auto-retry ({0}/{1}): {2}",
keepalive_label: "KeepAlive",
keepalive_dead: "\u26A0\uFE0Foff (CSP?)",
// 脚本启动和初始化
log_script_starting: "Script starting...",
log_network_filter_deprecated: "NetworkFilter module deprecated, functionality handled by PagePatcher.",
// 限速状态检查
log_rate_limit_check_active: "Rate limit check already in progress, skipping",
log_rate_limit_check_start: "Starting rate limit status check...",
log_page_content_rate_limit: "Page content contains rate limit info, confirming still rate limited",
log_use_performance_api: "Using Performance API to check recent requests, no longer sending active requests",
log_detected_429_in_10s: "Detected 429 status in recent 10s, judging as rate limited",
log_detected_success_in_10s: "Detected successful request in recent 10s, judging as normal",
log_insufficient_info_status: "Insufficient info to judge rate limit status, maintaining current state",
log_rate_limit_check_failed: "Rate limit status check failed: {0}",
// 游标和位置
log_cursor_initialized_with: "[Cursor] Initialized. Loaded saved cursor: {0}...",
log_cursor_initialized_empty: "[Cursor] Initialized. No saved cursor found.",
log_cursor_restore_failed: "[Cursor] Failed to restore cursor state:",
log_cursor_interceptors_applied: "[Cursor] Network interceptors applied.",
log_cursor_skip_known_position: "[Cursor] Skipping known position save: {0}",
log_cursor_skip_backtrack: "[Cursor] Skipping backtrack position: {0} (current: {1}), sort: {2}",
log_cursor_save_error: "[Cursor] Error saving cursor:",
log_url_sort_changed: 'Detected URL sort parameter change from "{0}" to "{1}"',
log_sort_changed_position_cleared: "Cleared saved position due to sort method change",
log_sort_check_error: "Error checking URL sort parameter: {0}",
log_position_cleared: "Cleared saved browsing position.",
clear_position_tooltip: "Reset position and refresh",
confirm_reset_position: "Are you sure you want to clear the saved browsing position and refresh the page?",
no_position_to_reset: "No saved browsing position.",
log_sort_ascending: "Ascending",
log_sort_descending: "Descending",
// XHR/Fetch 限速检测
log_xhr_rate_limit_detect: "[XHR Rate Limit] Detected rate limit, response: {0}",
log_list_end_normal: "[List End] Reached end of list, normal situation: {0}...",
log_empty_search_with_filters: "[Empty Search] Empty result but has filters, may be normal: {0}...",
log_empty_search_already_limited: "[Empty Search] Already rate limited, not triggering again: {0}...",
log_empty_search_page_loading: "[Empty Search] Page still loading, may be initial request: {0}...",
log_debounce_intercept: "[Debounce] \u{1F6A6} Intercepted scroll request. Applying {0}ms delay...",
log_debounce_discard: "[Debounce] \u{1F5D1}\uFE0F Discarded previous pending request.",
log_debounce_sending: "[Debounce] \u25B6\uFE0F Sending latest scroll request: {0}",
log_fetch_detected_429: "[Fetch] Detected 429 status code: {0}",
log_fetch_rate_limit_detect: "[Fetch Rate Limit] Detected rate limit, response: {0}...",
log_fetch_list_end: "[Fetch List End] Reached end, normal: {0}...",
log_fetch_empty_with_filters: "[Fetch Empty] Empty with filters, may be normal: {0}...",
log_fetch_empty_already_limited: "[Fetch Empty] Already limited, not triggering: {0}...",
log_fetch_empty_page_loading: "[Fetch Empty] Page loading, may be initial: {0}...",
log_fetch_implicit_rate_limit: "[Fetch Implicit] Possible rate limit (empty): {0}...",
log_json_parse_error: "JSON parse error: {0}",
log_response_length: "Response length: {0}, first 100 chars: {1}",
log_handling_rate_limit_error: "Error handling rate limit: {0}",
// 执行控制
log_execution_stopped_manually: "Execution manually stopped by user.",
log_todo_cleared_scan: "To-do list cleared. Will scan and add only visible items.",
log_scanning_loaded_items: "Scanning loaded items...",
log_executor_running_queued: "Executor running, new tasks queued for processing.",
log_todo_empty_scanning: "To-do list empty, scanning current page...",
log_request_no_results_not_counted: "Request successful but no valid results, not counting. Source: {0}",
log_not_rate_limited_ignore_exit: "Not rate limited, ignoring exit request: {0}",
log_found_todo_auto_resume: "Found {0} to-do tasks, auto-resuming execution...",
log_dispatching_wait: "Dispatching tasks, please wait...",
log_rate_limited_continue_todo: "In rate limited state, but will continue executing to-do tasks...",
log_detected_todo_no_workers: "Detected to-do tasks but no active workers, attempting retry...",
// 数据库和同步
log_db_sync_cleared_failed: "[Fab DB Sync] Cleared {0} manually completed items from failed list.",
log_no_unowned_in_batch: "No unowned items found in this batch.",
log_no_truly_free_after_verify: "Found unowned items, but none truly free after price verification.",
log_429_scan_paused: "Detected 429 error, requesting too frequently. Will pause scanning.",
// 工作线程
log_worker_tabs_cleared: "Cleared all worker tab states.",
log_worker_task_cleared_closing: "Task data cleared, worker tab will close.",
log_worker_instance_cooperate: "Detected active instance [{0}], worker tab will cooperate.",
log_other_instance_report_ignore: "Received report from other instance [{0}], current [{1}] will ignore.",
// 失败和重试
log_failed_list_empty: "Failed list empty, no action needed.",
log_cleared_from_failed: "Cleared {0} actually-owned items from the failed list.",
log_failed_check_start: "Checking status of failed items (verifying local records and server status)...",
log_failed_check_local: "[Local Check] Found {0} items already marked done locally, removing from failed list...",
log_failed_check_network: "[Server Check] Querying server for acquisition status of remaining {0} items...",
log_failed_check_network_confirmed: "[Server Check] Confirmed {0} items actually acquired, updating database...",
log_failed_check_network_none: "[Server Check] No other acquired items detected.",
log_failed_check_complete: "\u2728 Check complete! Removed {0} successfully acquired items from the failed list.",
log_failed_check_all_done: "\u{1F389} All failed items are verified as acquired! Failed list is now empty.",
confirm_open_failed_remaining: "Status check complete. {0} items are still not in library.\nDo you want to open these {0} failed items in new tabs for manual processing?",
alert_failed_all_done: "All failed items are verified as acquired. No manual action needed!",
// Wake/visibility recovery
log_wake_recovery: "\u{1F514} Tab re-activated (lock screen / tab switch recovery). Checking execution state...",
log_wake_cleanup_stale: "\u{1F9F9} Cleaned up {0} stale workers frozen by lock screen. Restarting execution.",
log_wake_restarting: "\u25B6\uFE0F Re-activating execution ({0} pending tasks).",
// 调试模式
log_debug_mode_toggled: "Debug mode {0}. {1}",
log_debug_mode_detail_info: "Will display detailed log information",
log_no_history_to_copy: "No history to copy.",
// 启动和恢复
log_execution_state_inconsistent: "Execution state inconsistent, restoring from storage: {0}",
log_invalid_worker_report: "Received invalid worker report. Missing workerId or task.",
log_all_tasks_completed: "All tasks completed.",
log_all_tasks_completed_rate_limited: "All tasks completed and rate limited, will refresh to recover...",
log_recovery_probe_failed: "Recovery probe failed. Still rate limited, will continue refresh...",
// 实例管理
log_not_active_instance: "Current instance not active, not executing tasks.",
log_no_active_instance_activating: "No active instance detected, instance [{0}] activated.",
log_inactive_instance_taking_over: "Previous instance [{0}] inactive, taking over.",
log_is_search_page_activated: "This is search page, instance [{0}] activated.",
// 可见性和刷新
log_no_visible_items_todo_workers: "Rate limited with {0} to-do and {1} workers, not auto-refreshing.",
log_visible_items_detected_skipping: "\u23F9\uFE0F Detected {0} visible items, not refreshing to avoid interruption.",
log_please_complete_tasks_first: "Please complete or cancel these tasks before refreshing.",
log_display_mode_switched: "\u{1F441}\uFE0F Display mode switched, current page has {0} visible items",
position_label: "Location",
log_entering_rate_limit_from: "\u{1F6A8} Entering RATE LIMIT state from [{0}]! Normal period lasted {1}s with {2} requests.",
log_entering_rate_limit_from_v2: "\u{1F6A8} RATE LIMIT DETECTED from [{0}]! Normal operation lasted {1}s with {2} successful search requests.",
rate_limit_recovery_success: "\u2705 Rate limit appears to be lifted from [{0}]. The 429 period lasted {1}s.",
fab_dom_refresh_complete: "[Fab DOM Refresh] Complete. Updated {0} visible card states.",
auto_refresh_disabled_rate_limit: "\u26A0\uFE0F In rate limit state, auto refresh is disabled. Please manually refresh the page if needed.",
// 页面诊断
log_diagnosis_complete: "Page diagnosis complete, check console output",
log_diagnosis_failed: "Page diagnosis failed: {0}",
// Auto resume
log_auto_resume_page_loading: "[Auto-Resume] Page loaded in rate limited state. Running recovery probe...",
log_recovery_probe_success: "\u2705 Recovery probe succeeded! Rate limit lifted, continuing normal operations.",
log_tasks_still_running: "Still have {0} tasks running, waiting for them to finish before refresh...",
log_todo_tasks_waiting: "{0} to-do tasks waiting to execute, will try to continue execution...",
countdown_refresh_source: "Recovery probe failed",
failed_list_empty: "Failed list is empty, no action needed.",
opening_failed_items: "Opening {0} failed items...",
// 自动滚动挂起恢复
log_auto_scroll_stuck_refresh: "\u26A0\uFE0F Auto-scroll failed to find new items consecutively while executing tasks, triggering auto page refresh to recover...",
log_auto_scroll_stuck_96_refresh: "\u26A0\uFE0F Detected special scenario ({0} items hidden and no visible items, pagination stuck), saving temporary cursor, refreshing page to recover...",
log_auto_scroll_stuck_recovery_failed: "Already tried to recover using the current cursor but still no new items. Assuming end of list has been reached, stopping refresh.",
// 账号验证
auth_error: "Session expired: CSRF token not found, please log in again",
auth_error_alert: "Session expired: Please log in again before using the script",
auth_session_invalid: "Login session is invalid. Please log in again before starting a run.",
auth_worker_aborted: "Worker tab detected no active login; task processing aborted.",
auth_scan_blocked: "Not logged in (or session expired). Scan skipped to avoid treating every paid item as free."
};
// src/i18n/zh.js
var zh = {
// 基础UI
hide: "\u9690\u85CF\u5DF2\u5F97",
show: "\u663E\u793A\u5DF2\u5F97",
sync: "\u540C\u6B65\u72B6\u6001",
execute: "\u4E00\u952E\u5F00\u5237",
executing: "\u6267\u884C\u4E2D...",
stopExecute: "\u505C\u6B62",
retry_failed: "\u91CD\u8BD5\u5931\u8D25",
hideDiscounted: "\u9690\u85CF\u6253\u6298",
showDiscounted: "\u663E\u793A\u6253\u6298",
hidePaid: "\u9690\u85CF\u4ED8\u8D39",
showPaid: "\u663E\u793A\u4ED8\u8D39",
added: "\u5DF2\u5165\u5E93",
failed: "\u5931\u8D25",
todo: "\u5F85\u529E",
hidden: "\u5DF2\u9690\u85CF",
visible: "\u53EF\u89C1",
clearLog: "\u6E05\u7A7A\u65E5\u5FD7",
copyLog: "\u590D\u5236\u65E5\u5FD7",
copied: "\u5DF2\u590D\u5236!",
tab_dashboard: "\u4EEA\u8868\u76D8",
tab_settings: "\u8BBE\u5B9A",
tab_debug: "\u8C03\u8BD5",
toast_all_tasks_completed: "\u{1F389} \u6240\u6709\u4EFB\u52A1\u5DF2\u5904\u7406\u5B8C\u6210\uFF01",
toast_reached_bottom: "\u{1F3C1} \u5DF2\u5230\u8FBE\u5546\u54C1\u5217\u8868\u5E95\u90E8\uFF0C\u5168\u90E8\u5546\u54C1\u626B\u63CF\u5B8C\u6BD5\uFF01",
close: "\u5173\u95ED",
// 应用标题和标签
app_title: "Fab Helper",
free_label: "\u514D\u8D39",
operation_log: "\u{1F4DD} \u64CD\u4F5C\u65E5\u5FD7",
position_indicator: "\u{1F4CD} ",
// 按钮文本
clear_all_data: "\u{1F5D1}\uFE0F \u6E05\u7A7A\u6240\u6709\u5B58\u6863",
debug_mode: "\u8C03\u8BD5\u6A21\u5F0F",
page_diagnosis: "\u9875\u9762\u8BCA\u65AD",
copy_btn: "\u590D\u5236",
clear_btn: "\u6E05\u7A7A",
copied_success: "\u5DF2\u590D\u5236!",
// 状态文本
status_history: "\u72B6\u6001\u5468\u671F\u5386\u53F2\u8BB0\u5F55",
script_startup: "\u811A\u672C\u542F\u52A8",
normal_period: "\u6B63\u5E38\u8FD0\u884C\u671F",
rate_limited_period: "\u9650\u901F\u671F",
current_normal: "\u5F53\u524D: \u6B63\u5E38\u8FD0\u884C",
current_rate_limited: "\u5F53\u524D: \u9650\u901F\u4E2D",
no_history: "\u6CA1\u6709\u53EF\u663E\u793A\u7684\u5386\u53F2\u8BB0\u5F55\u3002",
no_saved_position: "\u65E0\u4FDD\u5B58\u4F4D\u7F6E",
// 状态历史详细信息
time_label: "\u65F6\u95F4",
info_label: "\u4FE1\u606F",
ended_at: "\u7ED3\u675F\u4E8E",
duration_label: "\u6301\u7EED",
requests_label: "\u8BF7\u6C42",
requests_unit: "\u6B21",
unknown_duration: "\u672A\u77E5",
// 日志消息
log_init: "\u52A9\u624B\u5DF2\u4E0A\u7EBF\uFF01",
log_db_loaded: "\u6B63\u5728\u8BFB\u53D6\u5B58\u6863...",
log_exec_no_tasks: '"\u5F85\u529E"\u6E05\u5355\u662F\u7A7A\u7684\u3002',
log_verify_success: "\u641E\u5B9A\uFF01\u5DF2\u6210\u529F\u5165\u5E93\u3002",
log_verify_fail: "\u54CE\u5440\uFF0C\u8FD9\u4E2A\u6CA1\u52A0\u4E0A\u3002\u7A0D\u540E\u4F1A\u81EA\u52A8\u91CD\u8BD5\uFF01",
log_429_error: "\u8BF7\u6C42\u592A\u5FEB\u88AB\u670D\u52A1\u5668\u9650\u901F\u4E86\uFF01\u4F11\u606F15\u79D2\u540E\u81EA\u52A8\u91CD\u8BD5...",
log_no_failed_tasks: "\u6CA1\u6709\u5931\u8D25\u7684\u4EFB\u52A1\u9700\u8981\u91CD\u8BD5\u3002",
log_requeuing_tasks: "\u6B63\u5728\u91CD\u65B0\u6392\u961F {0} \u4E2A\u5931\u8D25\u4EFB\u52A1...",
log_detail_page: "\u8FD9\u662F\u8BE6\u60C5\u9875\u6216\u5DE5\u4F5C\u6807\u7B7E\u9875\u3002\u505C\u6B62\u4E3B\u811A\u672C\u6267\u884C\u3002",
log_copy_failed: "\u590D\u5236\u65E5\u5FD7\u5931\u8D25:",
log_auto_add_enabled: '"\u81EA\u52A8\u6DFB\u52A0"\u5DF2\u5F00\u542F\u3002\u5C06\u76F4\u63A5\u5904\u7406\u5F53\u524D"\u5F85\u529E"\u961F\u5217\u4E2D\u7684\u6240\u6709\u4EFB\u52A1\u3002',
log_auto_add_toggle: "\u65E0\u9650\u6EDA\u52A8\u81EA\u52A8\u6DFB\u52A0\u4EFB\u52A1\u5DF2{0}\u3002",
log_remember_pos_toggle: "\u8BB0\u4F4F\u7011\u5E03\u6D41\u6D4F\u89C8\u4F4D\u7F6E\u529F\u80FD\u5DF2{0}\u3002",
log_auto_resume_toggle: "429\u540E\u81EA\u52A8\u6062\u590D\u529F\u80FD\u5DF2{0}\u3002",
log_auto_resume_start: "\u{1F504} 429\u81EA\u52A8\u6062\u590D\u542F\u52A8\uFF01\u5C06\u5728{0}\u79D2\u540E\u5237\u65B0\u9875\u9762\u5C1D\u8BD5\u6062\u590D...",
log_auto_resume_detect: "\u{1F504} \u68C0\u6D4B\u5230429\u9519\u8BEF\uFF0C\u5C06\u5728{0}\u79D2\u540E\u81EA\u52A8\u5237\u65B0\u9875\u9762\u5C1D\u8BD5\u6062\u590D...",
log_refresh_error: "\u72B6\u6001\u540C\u6B65\u8FC7\u7A0B\u4E2D\u51FA\u9519:",
log_unsettled_cards: "\u8FD8\u6709 {0} \u5F20\u5361\u7247\u6B63\u5728\u52A0\u8F7D\uFF0C\u7A0D\u540E\u518D\u68C0\u67E5\u3002",
// 调试日志消息
debug_save_cursor: "\u4FDD\u5B58\u65B0\u7684\u6062\u590D\u70B9: {0}",
debug_prepare_hide: "\u51C6\u5907\u9690\u85CF {0} \u5F20\u5361\u7247\uFF0C\u5C06\u4F7F\u7528\u66F4\u957F\u7684\u5EF6\u8FDF...",
debug_unprocessed_cards: "\u68C0\u6D4B\u5230 {0} \u4E2A\u672A\u5904\u7406\u6216\u72B6\u6001\u4E0D\u4E00\u81F4\u7684\u5361\u7247\uFF0C\u91CD\u65B0\u6267\u884C\u9690\u85CF\u903B\u8F91",
debug_new_content_loading: "\u68C0\u6D4B\u5230\u65B0\u5185\u5BB9\u52A0\u8F7D\uFF0C\u7B49\u5F85API\u8BF7\u6C42\u5B8C\u6210...",
debug_process_new_content: "\u5F00\u59CB\u5904\u7406\u65B0\u52A0\u8F7D\u7684\u5185\u5BB9...",
debug_unprocessed_cards_simple: "\u68C0\u6D4B\u5230\u672A\u5904\u7406\u7684\u5361\u7247\uFF0C\u91CD\u65B0\u6267\u884C\u9690\u85CF\u903B\u8F91",
debug_hide_completed: "\u5DF2\u5B8C\u6210\u6240\u6709 {0} \u5F20\u5361\u7247\u7684\u9690\u85CF",
debug_visible_after_hide: "\u{1F441}\uFE0F \u9690\u85CF\u540E\u5B9E\u9645\u53EF\u89C1\u5546\u54C1\u6570: {0}\uFF0C\u9690\u85CF\u5546\u54C1\u6570: {1}",
debug_filter_owned: "\u8FC7\u6EE4\u6389 {0} \u4E2A\u5DF2\u5165\u5E93\u5546\u54C1\u548C {1} \u4E2A\u5DF2\u5728\u5F85\u529E\u5217\u8868\u4E2D\u7684\u5546\u54C1\u3002",
debug_api_wait_complete: "API\u7B49\u5F85\u5B8C\u6210\uFF0C\u5F00\u59CB\u5904\u7406 {0} \u5F20\u5361\u7247...",
debug_api_stopped: "API\u6D3B\u52A8\u5DF2\u505C\u6B62 {0}ms\uFF0C\u7EE7\u7EED\u5904\u7406\u5361\u7247\u3002",
debug_wait_api_response: "\u5F00\u59CB\u7B49\u5F85API\u54CD\u5E94\uFF0C\u5C06\u5728API\u6D3B\u52A8\u505C\u6B62\u540E\u5904\u7406 {0} \u5F20\u5361\u7247...",
debug_api_wait_in_progress: "\u5DF2\u6709API\u7B49\u5F85\u8FC7\u7A0B\u5728\u8FDB\u884C\uFF0C\u5C06\u5F53\u524D {0} \u5F20\u5361\u7247\u52A0\u5165\u7B49\u5F85\u961F\u5217\u3002",
debug_cached_items: "\u5DF2\u7F13\u5B58 {0} \u4E2A\u5546\u54C1\u6570\u636E",
debug_no_cards_to_check: "\u6CA1\u6709\u9700\u8981\u68C0\u67E5\u7684\u5361\u7247",
// Fab DOM Refresh 相关
fab_dom_api_complete: "API\u67E5\u8BE2\u5B8C\u6210\uFF0C\u5171\u786E\u8BA4 {0} \u4E2A\u5DF2\u62E5\u6709\u7684\u9879\u76EE\u3002",
fab_dom_checking_status: "\u6B63\u5728\u68C0\u67E5 {0} \u4E2A\u9879\u76EE\u7684\u72B6\u6001...",
fab_dom_add_to_waitlist: "\u6DFB\u52A0 {0} \u4E2A\u5546\u54C1ID\u5230\u7B49\u5F85\u5217\u8868\uFF0C\u5F53\u524D\u7B49\u5F85\u5217\u8868\u5927\u5C0F: {0}",
fab_dom_unknown_status: "\u6709 {0} \u4E2A\u5546\u54C1\u72B6\u6001\u672A\u77E5\uFF0C\u7B49\u5F85\u7F51\u9875\u539F\u751F\u8BF7\u6C42\u66F4\u65B0",
// 状态监控
status_monitor_all_hidden: "\u68C0\u6D4B\u5230\u6B63\u5E38\u72B6\u6001\u4E0B\u6240\u6709\u5546\u54C1\u90FD\u88AB\u9690\u85CF ({0}\u4E2A)",
// 空搜索结果
empty_search_initial: "\u9875\u9762\u521A\u521A\u52A0\u8F7D\uFF0C\u53EF\u80FD\u662F\u521D\u59CB\u8BF7\u6C42\uFF0C\u4E0D\u89E6\u53D1\u9650\u901F",
// 游标相关
cursor_patched_url: "Patched URL",
cursor_injecting: "Injecting cursor. Original",
page_patcher_match: "-> \u2705 MATCH! URL will be patched",
// 自动刷新相关
auto_refresh_countdown: "\u23F1\uFE0F \u81EA\u52A8\u5237\u65B0\u5012\u8BA1\u65F6: {0} \u79D2...",
rate_limit_success_request: "\u9650\u901F\u72B6\u6001\u4E0B\u6210\u529F\u8BF7\u6C42 +1\uFF0C\u5F53\u524D\u8FDE\u7EED\u6210\u529F: {0}/{1}\uFF0C\u6765\u6E90: {2}",
rate_limit_no_visible_continue: "\u{1F504} \u9875\u9762\u4E0A\u6CA1\u6709\u53EF\u89C1\u5546\u54C1\u4E14\u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u5C06\u7EE7\u7EED\u81EA\u52A8\u5237\u65B0\u3002",
rate_limit_no_visible_suggest: "\u{1F504} \u5904\u4E8E\u9650\u901F\u72B6\u6001\u4E14\u6CA1\u6709\u53EF\u89C1\u5546\u54C1\uFF0C\u5EFA\u8BAE\u5237\u65B0\u9875\u9762",
status_check_summary: "\u{1F4CA} \u72B6\u6001\u68C0\u67E5 - \u5B9E\u9645\u53EF\u89C1: {0}, \u603B\u5361\u7247: {1}, \u9690\u85CF\u5546\u54C1\u6570: {2}",
refresh_plan_exists: "\u5DF2\u6709\u5237\u65B0\u8BA1\u5212\u6B63\u5728\u8FDB\u884C\u4E2D\uFF0C\u4E0D\u518D\u5B89\u6392\u65B0\u7684\u5237\u65B0 (429\u81EA\u52A8\u6062\u590D)",
page_content_rate_limit_detected: "[\u9875\u9762\u5185\u5BB9\u68C0\u6D4B] \u68C0\u6D4B\u5230\u9875\u9762\u663E\u793A\u9650\u901F\u9519\u8BEF\u4FE1\u606F\uFF01",
last_moment_check_cancelled: "\u26A0\uFE0F \u6700\u540E\u4E00\u523B\u68C0\u67E5\uFF1A\u5237\u65B0\u6761\u4EF6\u4E0D\u6EE1\u8DB3\uFF0C\u81EA\u52A8\u5237\u65B0\u5DF2\u53D6\u6D88\u3002",
refresh_cancelled_visible_items: "\u23F9\uFE0F \u5237\u65B0\u524D\u68C0\u6D4B\u5230\u9875\u9762\u4E0A\u6709 {0} \u4E2A\u53EF\u89C1\u5546\u54C1\uFF0C\u5DF2\u53D6\u6D88\u81EA\u52A8\u5237\u65B0\u3002",
log_all_hidden_rate_limited: "\u9650\u901F\u72B6\u6001\u4E0B\u5F53\u524D\u6CA1\u6709\u53EF\u89C1\u5546\u54C1\uFF0C\u51C6\u5907\u5B89\u6392\u4E00\u6B21\u6062\u590D\u5237\u65B0\u3002",
log_refresh_cancelled_tasks: "\u68C0\u6D4B\u5230\u6709 {0} \u4E2A\u5F85\u529E\u4EFB\u52A1\u548C {1} \u4E2A\u6D3B\u52A8\u5DE5\u4F5C\u7EBF\u7A0B\uFF0C\u5DF2\u53D6\u6D88\u81EA\u52A8\u5237\u65B0\u3002",
log_refresh_cancelled_visible: "\u5237\u65B0\u524D\u68C0\u6D4B\u5230\u9875\u9762\u4E0A\u6709 {0} \u4E2A\u53EF\u89C1\u5546\u54C1\uFF0C\u5DF2\u53D6\u6D88\u81EA\u52A8\u5237\u65B0\u3002",
log_refreshing: "\u9875\u9762\u4E0A\u6CA1\u6709\u53EF\u89C1\u5546\u54C1\u4E14\u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u6B63\u5728\u5237\u65B0\u9875\u9762\u3002",
rate_limit_no_visible_reason: "\u9650\u901F\u72B6\u6001\u65E0\u53EF\u89C1\u5546\u54C1",
// 限速检测来源
rate_limit_source_page_content: "\u9875\u9762\u5185\u5BB9\u68C0\u6D4B",
rate_limit_source_global_call: "\u5168\u5C40\u8C03\u7528",
// 日志标签
log_tag_auto_add: "\u81EA\u52A8\u6DFB\u52A0",
// 自动添加相关消息
auto_add_api_timeout: "API\u7B49\u5F85\u8D85\u65F6\uFF0C\u5DF2\u7B49\u5F85 {0}ms\uFF0C\u5C06\u7EE7\u7EED\u5904\u7406\u5361\u7247\u3002",
auto_add_api_error: "\u7B49\u5F85API\u65F6\u51FA\u9519: {0}",
auto_add_new_tasks: "\u65B0\u589E {0} \u4E2A\u4EFB\u52A1\u5230\u961F\u5217\u3002",
auto_scroll_attempt: "[\u81EA\u52A8\u6EDA\u52A8] \u961F\u5217\u5DF2\u7A7A\uFF0C\u5C1D\u8BD5\u5411\u4E0B\u6EDA\u52A8\u52A0\u8F7D\u66F4\u591A\u5546\u54C1... (\u5C1D\u8BD5 {0}/{1})",
auto_scroll_success: "[\u81EA\u52A8\u6EDA\u52A8] \u6210\u529F\u52A0\u8F7D\u5E76\u8BC6\u522B\u5230 {0} \u4E2A\u65B0\u4EFB\u52A1\uFF0C\u7EE7\u7EED\u6267\u884C...",
auto_scroll_cards_loaded: "[\u81EA\u52A8\u6EDA\u52A8] \u68C0\u6D4B\u5230 {0} \u5F20\u65B0\u5361\u7247\uFF0C\u7EE7\u7EED\u9690\u85CF/\u626B\u63CF\u5E76\u6EDA\u52A8...",
auto_scroll_reached_bottom: "[\u81EA\u52A8\u6EDA\u52A8] \u5DF2\u5230\u8FBE\u9875\u9762\u5E95\u90E8\uFF0C\u505C\u6B62\u6EDA\u52A8\u3002",
auto_scroll_no_new_items: "[\u81EA\u52A8\u6EDA\u52A8] \u8FDE\u7EED {0} \u6B21\u6EDA\u52A8\u5747\u672A\u53D1\u73B0\u7B26\u5408\u6761\u4EF6\u7684\u65B0\u5546\u54C1\uFF0C\u505C\u6B62\u81EA\u52A8\u6EDA\u52A8\u3002",
auto_scroll_waiting: "[\u81EA\u52A8\u6EDA\u52A8] \u672A\u53D1\u73B0\u7B26\u5408\u6761\u4EF6\u7684\u65B0\u5546\u54C1\uFF0C\u7B49\u5F85\u4E0B\u4E00\u6B21\u6EDA\u52A8\u5C1D\u8BD5...",
// HTTP状态检测
http_status_check_performance_api: "\u4F7F\u7528Performance API\u68C0\u67E5\uFF0C\u4E0D\u518D\u53D1\u9001HEAD\u8BF7\u6C42",
// 页面状态检测
page_status_hidden_no_visible: "\u{1F441}\uFE0F \u68C0\u6D4B\u5230\u9875\u9762\u4E0A\u6709 {0} \u4E2A\u9690\u85CF\u5546\u54C1\uFF0C\u4F46\u6CA1\u6709\u53EF\u89C1\u5546\u54C1",
page_status_suggest_refresh: "\u{1F504} \u68C0\u6D4B\u5230\u9875\u9762\u4E0A\u6709 {0} \u4E2A\u9690\u85CF\u5546\u54C1\uFF0C\u4F46\u6CA1\u6709\u53EF\u89C1\u5546\u54C1\uFF0C\u5EFA\u8BAE\u5237\u65B0\u9875\u9762",
// 限速状态相关
rate_limit_already_active: "\u5DF2\u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u6765\u6E90: {0}\uFF0C\u5FFD\u7565\u65B0\u7684\u9650\u901F\u89E6\u53D1: {1}",
xhr_detected_429: "[XHR] \u68C0\u6D4B\u5230429\u72B6\u6001\u7801: {0}",
// 状态历史消息
history_cleared_new_session: "\u5386\u53F2\u8BB0\u5F55\u5DF2\u6E05\u7A7A\uFF0C\u65B0\u4F1A\u8BDD\u5F00\u59CB",
status_history_cleared: "\u72B6\u6001\u5386\u53F2\u8BB0\u5F55\u5DF2\u6E05\u7A7A\u3002",
duplicate_normal_status_detected: "\u68C0\u6D4B\u5230\u91CD\u590D\u7684\u6B63\u5E38\u72B6\u6001\u8BB0\u5F55\uFF0C\u6765\u6E90: {0}",
execution_status_changed: "\u68C0\u6D4B\u5230\u6267\u884C\u72B6\u6001\u53D8\u5316\uFF1A{0}",
status_executing: "\u6267\u884C\u4E2D",
status_stopped: "\u5DF2\u505C\u6B62",
// 状态历史UI文本
status_duration_label: "\u6301\u7EED\u65F6\u95F4: ",
status_requests_label: "\u671F\u95F4\u8BF7\u6C42\u6570: ",
status_ended_at_label: "\u7ED3\u675F\u4E8E: ",
status_started_at_label: "\u5F00\u59CB\u4E8E: ",
status_ongoing_label: "\u5DF2\u6301\u7EED: ",
status_unknown_time: "\u672A\u77E5\u65F6\u95F4",
status_unknown_duration: "\u672A\u77E5",
// 启动时状态检测
startup_rate_limited: "\u811A\u672C\u542F\u52A8\u65F6\u5904\u4E8E\u9650\u901F\u72B6\u6001\u3002\u9650\u901F\u5DF2\u6301\u7EED\u81F3\u5C11 {0}s\uFF0C\u6765\u6E90: {1}",
status_unknown_source: "\u672A\u77E5",
// 请求成功来源
request_source_search_response: "\u641C\u7D22\u54CD\u5E94\u6210\u529F",
request_source_xhr_search: "XHR\u641C\u7D22\u6210\u529F",
request_source_xhr_item: "XHR\u5546\u54C1\u8BF7\u6C42",
consecutive_success_exit: "\u8FDE\u7EED{0}\u6B21\u6210\u529F\u8BF7\u6C42 ({1})",
search_response_parse_failed: "\u641C\u7D22\u54CD\u5E94\u89E3\u6790\u5931\u8D25: {0}",
// 缓存清理和Fab DOM相关
cache_cleanup_complete: "[Cache] \u6E05\u7406\u5B8C\u6210\uFF0C\u5F53\u524D\u7F13\u5B58\u5927\u5C0F: \u5546\u54C1={0}, \u62E5\u6709\u72B6\u6001={1}, \u4EF7\u683C={2}",
fab_dom_no_new_owned: "[Fab DOM Refresh] API\u67E5\u8BE2\u5B8C\u6210\uFF0C\u6CA1\u6709\u53D1\u73B0\u65B0\u7684\u5DF2\u62E5\u6709\u9879\u76EE\u3002",
// 状态报告UI标签
status_time_label: "\u65F6\u95F4",
status_info_label: "\u4FE1\u606F",
// 隐性限速检测和API监控
implicit_rate_limit_detection: "[\u9690\u6027\u9650\u901F\u68C0\u6D4B]",
scroll_api_monitoring: "[\u6EDA\u52A8API\u76D1\u63A7]",
task_execution_time: "\u4EFB\u52A1\u6267\u884C\u65F6\u95F4: {0}\u79D2",
detected_rate_limit_error: "\u68C0\u6D4B\u5230\u9650\u901F\u9519\u8BEF\u4FE1\u606F: {0}",
detected_possible_rate_limit_empty: "\u68C0\u6D4B\u5230\u53EF\u80FD\u7684\u9650\u901F\u60C5\u51B5(\u7A7A\u7ED3\u679C): {0}",
detected_possible_rate_limit_scroll: "\u68C0\u6D4B\u5230\u53EF\u80FD\u7684\u9650\u901F\u60C5\u51B5\uFF1A\u8FDE\u7EED{0}\u6B21\u6EDA\u52A8\u540E\u5361\u7247\u6570\u91CF\u672A\u589E\u52A0\u3002",
detected_api_429_status: "\u68C0\u6D4B\u5230API\u8BF7\u6C42\u72B6\u6001\u7801\u4E3A429: {0}",
detected_api_rate_limit_content: "\u68C0\u6D4B\u5230API\u54CD\u5E94\u5185\u5BB9\u5305\u542B\u9650\u901F\u4FE1\u606F: {0}",
// 限速来源标识
source_implicit_rate_limit: "\u9690\u6027\u9650\u901F\u68C0\u6D4B",
source_scroll_api_monitoring: "\u6EDA\u52A8API\u76D1\u63A7",
// 设置项
setting_auto_refresh: "\u65E0\u5546\u54C1\u53EF\u89C1\u65F6\u81EA\u52A8\u5237\u65B0",
setting_auto_add_scroll: "\u65E0\u9650\u6EDA\u52A8\u65F6\u81EA\u52A8\u6DFB\u52A0\u4EFB\u52A1",
setting_remember_position: "\u8BB0\u4F4F\u7011\u5E03\u6D41\u6D4F\u89C8\u4F4D\u7F6E",
setting_auto_resume_429: "429\u540E\u81EA\u52A8\u6062\u590D\u5E76\u7EE7\u7EED",
setting_hide_discounted: "\u9690\u85CF\u6253\u6298\u7684\u4ED8\u8D39\u5546\u54C1",
setting_hide_paid: "\u9690\u85CF\u6240\u6709\u4ED8\u8D39\u5546\u54C1",
setting_block_large_resources: "\u7981\u7528\u56FE\u7247/\u5A92\u4F53\u7B49\u5927\u8D44\u6E90",
setting_debug_tooltip: "\u542F\u7528\u8BE6\u7EC6\u65E5\u5FD7\u8BB0\u5F55\uFF0C\u7528\u4E8E\u6392\u67E5\u95EE\u9898",
// 状态文本
status_enabled: "\u5F00\u542F",
status_disabled: "\u5173\u95ED",
// 确认对话框
confirm_clear_data: "\u60A8\u786E\u5B9A\u8981\u6E05\u7A7A\u6240\u6709\u672C\u5730\u5B58\u50A8\u7684\u811A\u672C\u6570\u636E\uFF08\u5DF2\u5B8C\u6210\u3001\u5931\u8D25\u3001\u5F85\u529E\u5217\u8868\uFF09\u5417\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u9006\uFF01",
confirm_open_failed: "\u60A8\u786E\u5B9A\u8981\u5728\u65B0\u6807\u7B7E\u9875\u4E2D\u6253\u5F00 {0} \u4E2A\u5931\u8D25\u7684\u9879\u76EE\u5417\uFF1F",
confirm_clear_history: "\u60A8\u786E\u5B9A\u8981\u6E05\u7A7A\u6240\u6709\u72B6\u6001\u5386\u53F2\u8BB0\u5F55\u5417\uFF1F",
// 错误提示
error_api_refresh: "API \u5237\u65B0\u5931\u8D25\u3002\u8BF7\u68C0\u67E5\u63A7\u5236\u53F0\u4E2D\u7684\u9519\u8BEF\u4FE1\u606F\uFF0C\u5E76\u786E\u8BA4\u60A8\u5DF2\u767B\u5F55\u3002",
// 工具提示
tooltip_open_failed: "\u70B9\u51FB\u6253\u5F00\u6240\u6709\u5931\u8D25\u7684\u9879\u76EE",
tooltip_executing_progress: "\u6267\u884C\u4E2D: {0}/{1} ({2}%)",
tooltip_executing: "\u6267\u884C\u4E2D",
tooltip_start_tasks: "\u70B9\u51FB\u5F00\u59CB\u6267\u884C\u4EFB\u52A1",
// 其他
goto_page_label: "\u9875\u7801:",
goto_page_btn: "\u8DF3\u8F6C",
page_reset: "Page: 1",
untitled: "Untitled",
cursor_mode: "Cursor Mode",
using_native_requests: "\u4F7F\u7528\u7F51\u9875\u539F\u751F\u8BF7\u6C42\uFF0C\u7B49\u5F85\u4E2D: {0}",
worker_closed: "\u5DE5\u4F5C\u6807\u7B7E\u9875\u5728\u5B8C\u6210\u524D\u5173\u95ED",
worker_captcha: "\u68C0\u6D4B\u5230\u4EBA\u673A\u9A8C\u8BC1 / \u9700\u8981\u4EBA\u5DE5\u5904\u7406",
log_keepalive_on: "\u540E\u53F0\u4FDD\u6D3B\u5DF2\u542F\u52A8\uFF08Worker \u5FC3\u8DF3 + \u9632\u51BB\u7ED3\uFF09",
log_keepalive_failed: "\u540E\u53F0\u4FDD\u6D3B\u542F\u52A8\u5931\u8D25: {0}",
log_auto_retry: "\u21BB \u81EA\u52A8\u91CD\u8BD5 ({0}/{1}): {2}",
keepalive_label: "\u4FDD\u6D3B",
keepalive_dead: "\u26A0\uFE0F\u672A\u542F\u52A8(\u7591\u88ABCSP\u62E6)",
// 脚本启动和初始化
log_script_starting: "\u811A\u672C\u5F00\u59CB\u8FD0\u884C...",
log_network_filter_deprecated: "\u7F51\u7EDC\u8FC7\u6EE4\u5668(NetworkFilter)\u6A21\u5757\u5DF2\u5F03\u7528\uFF0C\u529F\u80FD\u7531\u8865\u4E01\u7A0B\u5E8F(PagePatcher)\u5904\u7406\u3002",
// 限速状态检查
log_rate_limit_check_active: "\u5DF2\u6709\u9650\u901F\u72B6\u6001\u68C0\u67E5\u6B63\u5728\u8FDB\u884C\uFF0C\u8DF3\u8FC7\u672C\u6B21\u68C0\u67E5",
log_rate_limit_check_start: "\u5F00\u59CB\u68C0\u67E5\u9650\u901F\u72B6\u6001...",
log_page_content_rate_limit: "\u9875\u9762\u5185\u5BB9\u5305\u542B\u9650\u901F\u4FE1\u606F\uFF0C\u786E\u8BA4\u4ECD\u5904\u4E8E\u9650\u901F\u72B6\u6001",
log_use_performance_api: "\u4F7F\u7528Performance API\u68C0\u67E5\u6700\u8FD1\u7684\u7F51\u7EDC\u8BF7\u6C42\uFF0C\u4E0D\u518D\u4E3B\u52A8\u53D1\u9001API\u8BF7\u6C42",
log_detected_429_in_10s: "\u68C0\u6D4B\u5230\u6700\u8FD110\u79D2\u5185\u6709429\u72B6\u6001\u7801\u7684\u8BF7\u6C42\uFF0C\u5224\u65AD\u4E3A\u9650\u901F\u72B6\u6001",
log_detected_success_in_10s: "\u68C0\u6D4B\u5230\u6700\u8FD110\u79D2\u5185\u6709\u6210\u529F\u7684API\u8BF7\u6C42\uFF0C\u5224\u65AD\u4E3A\u6B63\u5E38\u72B6\u6001",
log_insufficient_info_status: "\u6CA1\u6709\u8DB3\u591F\u7684\u4FE1\u606F\u5224\u65AD\u9650\u901F\u72B6\u6001\uFF0C\u4FDD\u6301\u5F53\u524D\u72B6\u6001",
log_rate_limit_check_failed: "\u9650\u901F\u72B6\u6001\u68C0\u67E5\u5931\u8D25: {0}",
// 游标和位置
log_cursor_initialized_with: "[Cursor] \u521D\u59CB\u5316\u5B8C\u6210\u3002\u52A0\u8F7D\u5DF2\u4FDD\u5B58\u7684cursor: {0}...",
log_cursor_initialized_empty: "[Cursor] \u521D\u59CB\u5316\u5B8C\u6210\u3002\u672A\u627E\u5230\u5DF2\u4FDD\u5B58\u7684cursor\u3002",
log_cursor_restore_failed: "[Cursor] \u6062\u590Dcursor\u72B6\u6001\u5931\u8D25:",
log_cursor_interceptors_applied: "[Cursor] \u7F51\u7EDC\u62E6\u622A\u5668\u5DF2\u5E94\u7528\u3002",
log_cursor_skip_known_position: "[Cursor] \u8DF3\u8FC7\u5DF2\u77E5\u4F4D\u7F6E\u7684\u4FDD\u5B58: {0}",
log_cursor_skip_backtrack: "[Cursor] \u8DF3\u8FC7\u56DE\u9000\u4F4D\u7F6E: {0} (\u5F53\u524D\u4F4D\u7F6E: {1}), \u6392\u5E8F: {2}",
log_cursor_save_error: "[Cursor] \u4FDD\u5B58cursor\u65F6\u51FA\u9519:",
log_url_sort_changed: '\u68C0\u6D4B\u5230URL\u6392\u5E8F\u53C2\u6570\u53D8\u66F4\uFF0C\u6392\u5E8F\u65B9\u5F0F\u5DF2\u4ECE"{0}"\u66F4\u6539\u4E3A"{1}"',
log_sort_changed_position_cleared: "\u7531\u4E8E\u6392\u5E8F\u65B9\u5F0F\u53D8\u66F4\uFF0C\u5DF2\u6E05\u9664\u4FDD\u5B58\u7684\u6D4F\u89C8\u4F4D\u7F6E",
log_sort_check_error: "\u68C0\u67E5URL\u6392\u5E8F\u53C2\u6570\u65F6\u51FA\u9519: {0}",
log_position_cleared: "\u5DF2\u6E05\u9664\u5DF2\u4FDD\u5B58\u7684\u6D4F\u89C8\u4F4D\u7F6E\u3002",
clear_position_tooltip: "\u91CD\u7F6E\u6D4F\u89C8\u4F4D\u7F6E\u5E76\u5237\u65B0",
confirm_reset_position: "\u786E\u5B9A\u8981\u6E05\u9664\u5DF2\u4FDD\u5B58\u7684\u6D4F\u89C8\u4F4D\u7F6E\u5E76\u5237\u65B0\u9875\u9762\u5417\uFF1F",
no_position_to_reset: "\u5F53\u524D\u6CA1\u6709\u4FDD\u5B58\u7684\u6D4F\u89C8\u4F4D\u7F6E\u3002",
log_sort_ascending: "\u5347\u5E8F",
log_sort_descending: "\u964D\u5E8F",
// XHR/Fetch 限速检测
log_xhr_rate_limit_detect: "[XHR\u9650\u901F\u68C0\u6D4B] \u68C0\u6D4B\u5230\u9650\u901F\u60C5\u51B5\uFF0C\u539F\u59CB\u54CD\u5E94: {0}",
log_list_end_normal: "[\u5217\u8868\u672B\u5C3E] \u68C0\u6D4B\u5230\u5DF2\u5230\u8FBE\u5217\u8868\u672B\u5C3E\uFF0C\u8FD9\u662F\u6B63\u5E38\u60C5\u51B5\uFF0C\u4E0D\u89E6\u53D1\u9650\u901F: {0}...",
log_empty_search_with_filters: "[\u7A7A\u641C\u7D22\u7ED3\u679C] \u68C0\u6D4B\u5230\u641C\u7D22\u7ED3\u679C\u4E3A\u7A7A\uFF0C\u4F46\u5305\u542B\u7279\u6B8A\u8FC7\u6EE4\u6761\u4EF6\uFF0C\u8FD9\u53EF\u80FD\u662F\u6B63\u5E38\u60C5\u51B5: {0}...",
log_empty_search_already_limited: "[\u7A7A\u641C\u7D22\u7ED3\u679C] \u5DF2\u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u4E0D\u91CD\u590D\u89E6\u53D1: {0}...",
log_empty_search_page_loading: "[\u7A7A\u641C\u7D22\u7ED3\u679C] \u9875\u9762\u5C1A\u672A\u5B8C\u5168\u52A0\u8F7D\uFF0C\u53EF\u80FD\u662F\u521D\u59CB\u8BF7\u6C42\uFF0C\u4E0D\u89E6\u53D1\u9650\u901F: {0}...",
log_debounce_intercept: "[Debounce] \u{1F6A6} \u62E6\u622A\u6EDA\u52A8\u8BF7\u6C42\u3002\u5E94\u7528{0}ms\u5EF6\u8FDF...",
log_debounce_discard: "[Debounce] \u{1F5D1}\uFE0F \u4E22\u5F03\u4E4B\u524D\u7684\u6302\u8D77\u8BF7\u6C42\u3002",
log_debounce_sending: "[Debounce] \u25B6\uFE0F \u53D1\u9001\u6700\u65B0\u6EDA\u52A8\u8BF7\u6C42: {0}",
log_fetch_detected_429: "[Fetch] \u68C0\u6D4B\u5230429\u72B6\u6001\u7801: {0}",
log_fetch_rate_limit_detect: "[Fetch\u9650\u901F\u68C0\u6D4B] \u68C0\u6D4B\u5230\u9650\u901F\u60C5\u51B5\uFF0C\u539F\u59CB\u54CD\u5E94: {0}...",
log_fetch_list_end: "[Fetch\u5217\u8868\u672B\u5C3E] \u68C0\u6D4B\u5230\u5DF2\u5230\u8FBE\u5217\u8868\u672B\u5C3E\uFF0C\u8FD9\u662F\u6B63\u5E38\u60C5\u51B5\uFF0C\u4E0D\u89E6\u53D1\u9650\u901F: {0}...",
log_fetch_empty_with_filters: "[Fetch\u7A7A\u641C\u7D22\u7ED3\u679C] \u68C0\u6D4B\u5230\u641C\u7D22\u7ED3\u679C\u4E3A\u7A7A\uFF0C\u4F46\u5305\u542B\u7279\u6B8A\u8FC7\u6EE4\u6761\u4EF6\uFF0C\u8FD9\u53EF\u80FD\u662F\u6B63\u5E38\u60C5\u51B5: {0}...",
log_fetch_empty_already_limited: "[Fetch\u7A7A\u641C\u7D22\u7ED3\u679C] \u5DF2\u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u4E0D\u91CD\u590D\u89E6\u53D1: {0}...",
log_fetch_empty_page_loading: "[Fetch\u7A7A\u641C\u7D22\u7ED3\u679C] \u9875\u9762\u5C1A\u672A\u5B8C\u5168\u52A0\u8F7D\uFF0C\u53EF\u80FD\u662F\u521D\u59CB\u8BF7\u6C42\uFF0C\u4E0D\u89E6\u53D1\u9650\u901F: {0}...",
log_fetch_implicit_rate_limit: "[Fetch\u9690\u6027\u9650\u901F] \u68C0\u6D4B\u5230\u53EF\u80FD\u7684\u9650\u901F\u60C5\u51B5(\u7A7A\u7ED3\u679C): {0}...",
log_json_parse_error: "JSON\u89E3\u6790\u9519\u8BEF: {0}",
log_response_length: "\u54CD\u5E94\u957F\u5EA6: {0}, \u524D100\u4E2A\u5B57\u7B26: {1}",
log_handling_rate_limit_error: "\u5904\u7406\u9650\u901F\u65F6\u51FA\u9519: {0}",
// 执行控制
log_execution_stopped_manually: "\u6267\u884C\u5DF2\u7531\u7528\u6237\u624B\u52A8\u505C\u6B62\u3002",
log_todo_cleared_scan: "\u5F85\u529E\u5217\u8868\u5DF2\u6E05\u7A7A\u3002\u73B0\u5728\u5C06\u626B\u63CF\u5E76\u4EC5\u6DFB\u52A0\u5F53\u524D\u53EF\u89C1\u7684\u9879\u76EE\u3002",
log_scanning_loaded_items: "\u6B63\u5728\u626B\u63CF\u5DF2\u52A0\u8F7D\u5B8C\u6210\u7684\u5546\u54C1...",
log_executor_running_queued: "\u6267\u884C\u5668\u5DF2\u5728\u8FD0\u884C\u4E2D\uFF0C\u65B0\u4EFB\u52A1\u5DF2\u52A0\u5165\u961F\u5217\u7B49\u5F85\u5904\u7406\u3002",
log_todo_empty_scanning: "\u5F85\u529E\u6E05\u5355\u4E3A\u7A7A\uFF0C\u6B63\u5728\u626B\u63CF\u5F53\u524D\u9875\u9762...",
log_request_no_results_not_counted: "\u8BF7\u6C42\u6210\u529F\u4F46\u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7ED3\u679C\uFF0C\u4E0D\u8BA1\u5165\u8FDE\u7EED\u6210\u529F\u8BA1\u6570\u3002\u6765\u6E90: {0}",
log_not_rate_limited_ignore_exit: "\u5F53\u524D\u4E0D\u662F\u9650\u901F\u72B6\u6001\uFF0C\u5FFD\u7565\u9000\u51FA\u9650\u901F\u8BF7\u6C42: {0}",
log_found_todo_auto_resume: "\u53D1\u73B0 {0} \u4E2A\u5F85\u529E\u4EFB\u52A1\uFF0C\u81EA\u52A8\u6062\u590D\u6267\u884C...",
log_dispatching_wait: "\u6B63\u5728\u6D3E\u53D1\u4EFB\u52A1\u4E2D\uFF0C\u8BF7\u7A0D\u5019...",
log_rate_limited_continue_todo: "\u5F53\u524D\u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u4F46\u4ECD\u5C06\u7EE7\u7EED\u6267\u884C\u5F85\u529E\u4EFB\u52A1...",
log_detected_todo_no_workers: "\u68C0\u6D4B\u5230\u6709\u5F85\u529E\u4EFB\u52A1\u4F46\u6CA1\u6709\u6D3B\u52A8\u5DE5\u4F5C\u7EBF\u7A0B\uFF0C\u5C1D\u8BD5\u91CD\u65B0\u6267\u884C...",
// 数据库和同步
log_db_sync_cleared_failed: '[Fab DB Sync] \u4ECE"\u5931\u8D25"\u5217\u8868\u4E2D\u6E05\u9664\u4E86 {0} \u4E2A\u5DF2\u624B\u52A8\u5B8C\u6210\u7684\u5546\u54C1\u3002',
log_no_unowned_in_batch: "\u672C\u6279\u6B21\u4E2D\u6CA1\u6709\u53D1\u73B0\u672A\u62E5\u6709\u7684\u5546\u54C1\u3002",
log_no_truly_free_after_verify: "\u627E\u5230\u672A\u62E5\u6709\u7684\u5546\u54C1\uFF0C\u4F46\u4EF7\u683C\u9A8C\u8BC1\u540E\u6CA1\u6709\u771F\u6B63\u514D\u8D39\u7684\u5546\u54C1\u3002",
log_429_scan_paused: "\u68C0\u6D4B\u5230429\u9519\u8BEF\uFF0C\u53EF\u80FD\u662F\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\u3002\u5C06\u6682\u505C\u626B\u63CF\u3002",
// 工作线程
log_worker_tabs_cleared: "\u5DF2\u6E05\u7406\u6240\u6709\u5DE5\u4F5C\u6807\u7B7E\u9875\u7684\u72B6\u6001\u3002",
log_worker_task_cleared_closing: "\u4EFB\u52A1\u6570\u636E\u5DF2\u88AB\u6E05\u7406\uFF0C\u5DE5\u4F5C\u6807\u7B7E\u9875\u5C06\u5173\u95ED\u3002",
log_worker_instance_cooperate: "\u68C0\u6D4B\u5230\u6D3B\u8DC3\u7684\u811A\u672C\u5B9E\u4F8B [{0}]\uFF0C\u5F53\u524D\u5DE5\u4F5C\u6807\u7B7E\u9875\u5C06\u4E0E\u4E4B\u534F\u4F5C\u3002",
log_other_instance_report_ignore: "\u6536\u5230\u6765\u81EA\u5176\u4ED6\u5B9E\u4F8B [{0}] \u7684\u5DE5\u4F5C\u62A5\u544A\uFF0C\u5F53\u524D\u5B9E\u4F8B [{1}] \u5C06\u5FFD\u7565\u3002",
// 失败和重试
log_failed_list_empty: "\u5931\u8D25\u5217\u8868\u4E3A\u7A7A\uFF0C\u65E0\u9700\u64CD\u4F5C\u3002",
log_cleared_from_failed: "\u5DF2\u4ECE\u5931\u8D25\u5217\u8868\u4E2D\u6E05\u9664 {0} \u4E2A\u5B9E\u9645\u5DF2\u5165\u5E93\u7684\u5546\u54C1\u3002",
log_failed_check_start: "\u6B63\u5728\u5BF9\u5931\u8D25\u9879\u76EE\u8FDB\u884C\u72B6\u6001\u68C0\u6D4B\uFF08\u6BD4\u5BF9\u672C\u5730\u8BB0\u5F55\u4E0E\u8054\u7F51\u72B6\u6001\uFF09...",
log_failed_check_local: "[\u672C\u5730\u68C0\u6D4B] \u53D1\u73B0 {0} \u4E2A\u9879\u76EE\u5728\u672C\u5730\u5DF2\u6807\u8BB0\u5B8C\u6210\uFF0C\u6B63\u5728\u4ECE\u5931\u8D25\u5217\u8868\u4E2D\u79FB\u9664...",
log_failed_check_network: "[\u8054\u7F51\u68C0\u6D4B] \u6B63\u5728\u5411\u670D\u52A1\u5668\u67E5\u8BE2\u5269\u4F59 {0} \u4E2A\u9879\u76EE\u7684\u5165\u5E93\u72B6\u6001...",
log_failed_check_network_confirmed: "[\u8054\u7F51\u68C0\u6D4B] \u786E\u8BA4\u6709 {0} \u4E2A\u9879\u76EE\u5B9E\u9645\u5DF2\u88AB\u52A0\u5E93\uFF0C\u6B63\u5728\u66F4\u65B0\u6570\u636E\u5E93...",
log_failed_check_network_none: "[\u8054\u7F51\u68C0\u6D4B] \u672A\u68C0\u6D4B\u5230\u5176\u4ED6\u5DF2\u52A0\u5E93\u9879\u76EE\u3002",
log_failed_check_complete: "\u2728 \u68C0\u6D4B\u5B8C\u6210\uFF01\u5171\u5254\u9664\u4E86 {0} \u4E2A\u5DF2\u6210\u529F\u5165\u5E93\u7684\u9879\u76EE\u3002",
log_failed_check_all_done: "\u{1F389} \u6240\u6709\u5931\u8D25\u9879\u76EE\u68C0\u6D4B\u5747\u5DF2\u786E\u8BA4\u5165\u5E93\uFF0C\u5931\u8D25\u5217\u8868\u5DF2\u6E05\u7A7A\uFF01",
confirm_open_failed_remaining: "\u72B6\u6001\u6838\u5BF9\u5B8C\u6210\uFF0C\u4ECD\u6709 {0} \u4E2A\u5546\u54C1\u672A\u5165\u5E93\u3002\n\u662F\u5426\u5728\u65B0\u6807\u7B7E\u9875\u4E2D\u6253\u5F00\u8FD9 {0} \u4E2A\u5931\u8D25\u5546\u54C1\u4EE5\u4F9B\u624B\u52A8\u5904\u7406\uFF1F",
alert_failed_all_done: "\u6240\u6709\u5931\u8D25\u9879\u76EE\u68C0\u6D4B\u5747\u5DF2\u786E\u8BA4\u5165\u5E93\uFF0C\u65E0\u9700\u624B\u52A8\u64CD\u4F5C\uFF01",
// 锁屏/后台恢复
log_wake_recovery: "\u{1F514} \u68C0\u6D4B\u5230\u6807\u7B7E\u9875\u91CD\u65B0\u6FC0\u6D3B\uFF08\u9501\u5C4F/\u5207\u6362\u540E\u6062\u590D\uFF09\uFF0C\u6B63\u5728\u68C0\u67E5\u6267\u884C\u72B6\u6001...",
log_wake_cleanup_stale: "\u{1F9F9} \u6E05\u7406 {0} \u4E2A\u56E0\u9501\u5C4F\u51BB\u7ED3\u8D85\u65F6\u7684\u5DE5\u4F5C\u7EBF\u7A0B\uFF0C\u91CD\u65B0\u542F\u52A8\u6267\u884C\u3002",
log_wake_restarting: "\u25B6\uFE0F \u91CD\u65B0\u6FC0\u6D3B\u6267\u884C\uFF08\u5171 {0} \u4E2A\u5F85\u529E\u4EFB\u52A1\uFF09\u3002",
// 调试模式
log_debug_mode_toggled: "\u8C03\u8BD5\u6A21\u5F0F\u5DF2{0}\u3002{1}",
log_debug_mode_detail_info: "\u5C06\u663E\u793A\u8BE6\u7EC6\u65E5\u5FD7\u4FE1\u606F",
log_no_history_to_copy: "\u6CA1\u6709\u5386\u53F2\u8BB0\u5F55\u53EF\u4F9B\u590D\u5236\u3002",
// 启动和恢复
log_execution_state_inconsistent: "\u6267\u884C\u72B6\u6001\u4E0D\u4E00\u81F4\uFF0C\u4ECE\u5B58\u50A8\u4E2D\u6062\u590D\uFF1A{0}",
log_invalid_worker_report: "\u6536\u5230\u65E0\u6548\u7684\u5DE5\u4F5C\u62A5\u544A\u3002\u7F3A\u5C11workerId\u6216task\u3002",
log_all_tasks_completed: "\u6240\u6709\u4EFB\u52A1\u5DF2\u5B8C\u6210\u3002",
log_all_tasks_completed_rate_limited: "\u6240\u6709\u4EFB\u52A1\u5DF2\u5B8C\u6210\uFF0C\u4E14\u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u5C06\u5237\u65B0\u9875\u9762\u5C1D\u8BD5\u6062\u590D...",
log_recovery_probe_failed: "\u6062\u590D\u63A2\u6D4B\u5931\u8D25\u3002\u4ECD\u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u5C06\u7EE7\u7EED\u968F\u673A\u5237\u65B0...",
// 实例管理
log_not_active_instance: "\u5F53\u524D\u5B9E\u4F8B\u4E0D\u662F\u6D3B\u8DC3\u5B9E\u4F8B\uFF0C\u4E0D\u6267\u884C\u4EFB\u52A1\u3002",
log_no_active_instance_activating: "\u6CA1\u6709\u68C0\u6D4B\u5230\u6D3B\u8DC3\u5B9E\u4F8B\uFF0C\u5F53\u524D\u5B9E\u4F8B [{0}] \u5DF2\u6FC0\u6D3B\u3002",
log_inactive_instance_taking_over: "\u524D\u4E00\u4E2A\u5B9E\u4F8B [{0}] \u4E0D\u6D3B\u8DC3\uFF0C\u5F53\u524D\u5B9E\u4F8B\u63A5\u7BA1\u3002",
log_is_search_page_activated: "\u5F53\u524D\u662F\u641C\u7D22\u9875\u9762\uFF0C\u5B9E\u4F8B [{0}] \u5DF2\u6FC0\u6D3B\u3002",
// 可见性和刷新
log_no_visible_items_todo_workers: "\u867D\u7136\u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u4F46\u68C0\u6D4B\u5230\u6709 {0} \u4E2A\u5F85\u529E\u4EFB\u52A1\u548C {1} \u4E2A\u6D3B\u52A8\u5DE5\u4F5C\u7EBF\u7A0B\uFF0C\u6682\u4E0D\u81EA\u52A8\u5237\u65B0\u9875\u9762\u3002",
log_visible_items_detected_skipping: "\u23F9\uFE0F \u68C0\u6D4B\u5230\u9875\u9762\u4E0A\u6709 {0} \u4E2A\u53EF\u89C1\u5546\u54C1\uFF0C\u4E0D\u89E6\u53D1\u81EA\u52A8\u5237\u65B0\u4EE5\u907F\u514D\u4E2D\u65AD\u6D4F\u89C8\u3002",
log_please_complete_tasks_first: "\u8BF7\u624B\u52A8\u5B8C\u6210\u6216\u53D6\u6D88\u8FD9\u4E9B\u4EFB\u52A1\u540E\u518D\u5237\u65B0\u9875\u9762\u3002",
log_display_mode_switched: "\u{1F441}\uFE0F \u663E\u793A\u6A21\u5F0F\u5DF2\u5207\u6362\uFF0C\u5F53\u524D\u9875\u9762\u6709 {0} \u4E2A\u53EF\u89C1\u5546\u54C1",
position_label: "\u4F4D\u7F6E",
log_entering_rate_limit_from: "\u{1F6A8} \u6765\u81EA [{0}] \u7684\u9650\u901F\u89E6\u53D1\uFF01\u6B63\u5E38\u8FD0\u884C\u671F\u6301\u7EED\u4E86 {1} \u79D2\uFF0C\u671F\u95F4\u6709 {2} \u6B21\u6210\u529F\u7684\u641C\u7D22\u8BF7\u6C42\u3002",
log_entering_rate_limit_from_v2: "\u{1F6A8} \u4ECE [{0}] \u68C0\u6D4B\u5230\u9650\u901F\uFF01\u6B63\u5E38\u8FD0\u884C\u6301\u7EED\u4E86 {1} \u79D2\uFF0C\u5305\u542B {2} \u6B21\u6210\u529F\u641C\u7D22\u8BF7\u6C42\u3002",
rate_limit_recovery_success: "\u2705 \u9650\u901F\u4F3C\u4E4E\u5DF2\u4ECE [{0}] \u89E3\u9664\u3002429 \u72B6\u6001\u6301\u7EED\u4E86 {1} \u79D2\u3002",
fab_dom_refresh_complete: "[Fab DOM Refresh] \u5B8C\u6210\u3002\u66F4\u65B0\u4E86 {0} \u4E2A\u53EF\u89C1\u5361\u7247\u7684\u72B6\u6001\u3002",
auto_refresh_disabled_rate_limit: "\u26A0\uFE0F \u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u81EA\u52A8\u5237\u65B0\u529F\u80FD\u5DF2\u5173\u95ED\uFF0C\u8BF7\u5728\u9700\u8981\u65F6\u624B\u52A8\u5237\u65B0\u9875\u9762\u3002",
// 页面诊断
log_diagnosis_complete: "\u9875\u9762\u8BCA\u65AD\u5B8C\u6210\uFF0C\u8BF7\u67E5\u770B\u63A7\u5236\u53F0\u8F93\u51FA",
log_diagnosis_failed: "\u9875\u9762\u8BCA\u65AD\u5931\u8D25: {0}",
// Auto resume
log_auto_resume_page_loading: "[Auto-Resume] \u9875\u9762\u5728\u9650\u901F\u72B6\u6001\u4E0B\u52A0\u8F7D\u3002\u6B63\u5728\u8FDB\u884C\u6062\u590D\u63A2\u6D4B...",
log_recovery_probe_success: "\u2705 \u6062\u590D\u63A2\u6D4B\u6210\u529F\uFF01\u9650\u901F\u5DF2\u89E3\u9664\uFF0C\u7EE7\u7EED\u6B63\u5E38\u64CD\u4F5C\u3002",
log_tasks_still_running: "\u4ECD\u6709 {0} \u4E2A\u4EFB\u52A1\u5728\u6267\u884C\u4E2D\uFF0C\u7B49\u5F85\u5B83\u4EEC\u5B8C\u6210\u540E\u518D\u5237\u65B0...",
log_todo_tasks_waiting: "\u6709 {0} \u4E2A\u5F85\u529E\u4EFB\u52A1\u7B49\u5F85\u6267\u884C\uFF0C\u5C06\u5C1D\u8BD5\u7EE7\u7EED\u6267\u884C...",
countdown_refresh_source: "\u6062\u590D\u63A2\u6D4B\u5931\u8D25",
failed_list_empty: "\u5931\u8D25\u5217\u8868\u4E3A\u7A7A\uFF0C\u65E0\u9700\u64CD\u4F5C\u3002",
opening_failed_items: "\u6B63\u5728\u6253\u5F00 {0} \u4E2A\u5931\u8D25\u9879\u76EE...",
// 自动滚动挂起恢复
log_auto_scroll_stuck_refresh: "\u26A0\uFE0F \u81EA\u52A8\u6EDA\u52A8\u8FDE\u7EED\u672A\u53D1\u73B0\u65B0\u5546\u54C1\uFF0C\u4E14\u6B63\u5728\u6267\u884C\u4EFB\u52A1\u4E2D\uFF0C\u89E6\u53D1\u81EA\u52A8\u9875\u9762\u5237\u65B0\u4EE5\u5C1D\u8BD5\u6062\u590D...",
log_auto_scroll_stuck_96_refresh: "\u26A0\uFE0F \u76D1\u6D4B\u5230\u7279\u6B8A\u60C5\u666F\uFF08\u5DF2\u9690\u85CF {0} \u4E2A\u5546\u54C1\u4E14\u65E0\u53EF\u89C1\u5546\u54C1\uFF0C\u7FFB\u9875\u6302\u8D77\uFF09\uFF0C\u4FDD\u5B58\u4E34\u65F6\u6E38\u6807\uFF0C\u6B63\u5728\u5237\u65B0\u9875\u9762\u4EE5\u5C1D\u8BD5\u6062\u590D...",
log_auto_scroll_stuck_recovery_failed: "\u5DF2\u5C1D\u8BD5\u8FC7\u4F7F\u7528\u5F53\u524D\u6E38\u6807\u6062\u590D\u4F46\u4F9D\u7136\u65E0\u65B0\u5546\u54C1\uFF0C\u5224\u5B9A\u4E3A\u5DF2\u5230\u8FBE\u5217\u8868\u672B\u5C3E\uFF0C\u505C\u6B62\u5237\u65B0\u3002",
// 账号验证
auth_error: "\u8D26\u53F7\u5931\u6548\uFF1A\u672A\u627E\u5230 CSRF token\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55",
auth_error_alert: "\u8D26\u53F7\u5931\u6548\uFF1A\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u4F7F\u7528\u811A\u672C",
auth_session_invalid: "\u68C0\u6D4B\u5230\u767B\u5F55\u4F1A\u8BDD\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u542F\u52A8\u4EFB\u52A1\u3002",
auth_worker_aborted: "\u5DE5\u4F5C\u6807\u7B7E\u9875\u68C0\u6D4B\u5230\u672A\u767B\u5F55\uFF0C\u5DF2\u4E2D\u6B62\u4EFB\u52A1\u5904\u7406\u3002",
auth_scan_blocked: "\u672A\u767B\u5F55\u6216\u4F1A\u8BDD\u5DF2\u5931\u6548\uFF0C\u672C\u6B21\u626B\u63CF\u88AB\u8DF3\u8FC7\uFF08\u907F\u514D\u628A\u6240\u6709\u5546\u54C1\u8BEF\u5224\u4E3A\u514D\u8D39\u7A7A\u8DD1\uFF09\u3002"
};
// src/config.js
var Config = {
SCRIPT_NAME: "Fab Helper (\u4F18\u5316\u7248)",
DB_VERSION: 3,
DB_NAME: "fab_helper_db",
MAX_CONCURRENT_WORKERS: 7,
// 最大并发工作标签页数量
WORKER_TIMEOUT: 9e4,
// 工作标签页超时时间(watchdog 判定卡死)。领取一般 <1min,留足余量避免误杀慢任务
KEEPALIVE_TICK_MS: 2e3,
// 后台保活心跳间隔(Web Worker postMessage 频率)
ENABLE_FREEZE_GUARD: true,
// 是否启用 WebRTC 防整页冻结(锁屏/最小化场景需要)
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",
// 无商品可见时自动刷新
HIDE_DISCOUNTED: "fab_hideDiscounted_v8",
// 隐藏打折的付费商品
HIDE_PAID: "fab_hidePaid_v8",
// 隐藏所有付费商品
BLOCK_RESOURCES: "fab_block_resources_v1"
// 禁用大资源设置键
// 其他键值用于会话或主标签页持久化
},
SELECTORS: {
card: "div.fabkit-Stack-root.nTa5u2sc, div.AssetCard-root",
cardLink: 'a[href*="/listings/"]',
addButton: 'button[aria-label*="Add to"], button[aria-label*="\u6DFB\u52A0\u81F3"], button[aria-label*="cart"]',
rootElement: "#root",
successBanner: 'div[class*="Toast-root"]',
freeStatus: ".csZFzinF",
ownedStatus: ".cUUvxo_s"
},
TEXTS: {
en,
zh
},
// 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: ["\u5DF2\u4FDD\u5B58\u5728\u6211\u7684\u5E93\u4E2D", "Saved in My Library"],
// Check for buttons/links with these texts.
buttonTexts: ["\u5728\u6211\u7684\u5E93\u4E2D\u67E5\u770B", "View in My Library"],
// Check for the temporary success popup (snackbar).
snackbarText: ["\u4EA7\u54C1\u5DF2\u6DFB\u52A0\u81F3\u60A8\u7684\u5E93\u4E2D", "Product added to your library"]
},
ACQUISITION_TEXT_SET: /* @__PURE__ */ new Set([
"\u6DFB\u52A0\u5230\u6211\u7684\u5E93",
"Add to my library",
"\u52A0\u5165\u8D2D\u7269\u8F66",
"Add to cart",
"\u7ED3\u8D26",
"Checkout",
"\u7ACB\u5373\u83B7\u53D6",
"Get it",
"\u514D\u8D39\u83B7\u53D6",
"Get for free",
"\u5B8C\u6210\u8BA2\u5355",
"Complete order",
"\u7ACB\u5373\u8D2D\u4E70",
"Buy now",
"\u83B7\u53D6\u8D44\u6E90",
"Get asset",
"Place order",
"\u786E\u8BA4\u8BA2\u5355",
"\u4E0B\u5355"
]),
// Kept for backward compatibility with recon logic.
SAVED_TEXT_SET: /* @__PURE__ */ new Set(["\u5DF2\u4FDD\u5B58\u5728\u6211\u7684\u5E93\u4E2D", "Saved in My Library", "\u5728\u6211\u7684\u5E93\u4E2D", "In My Library"]),
FREE_TEXT_SET: /* @__PURE__ */ new Set(["\u514D\u8D39", "Free", "Free*", "0.00", "\u8D77\u59CB\u4EF7\u683C \u514D\u8D39", "Starting at Free"]),
EXTERNAL_CTA_TEXT_SET: /* @__PURE__ */ new Set([
"\u5728\u5916\u90E8\u7F51\u7AD9\u67E5\u770B",
"View on external website"
]),
// 添加一个实例ID,用于防止多实例运行
INSTANCE_ID: "fab_instance_id_" + Math.random().toString(36).substring(2, 15),
STATUS_CHECK_INTERVAL: 3e3
// Status check interval in ms (throttled to reduce log spam)
};
// src/state.js
var State = {
db: {
todo: [],
// 待办任务列表
done: [],
// 已完成任务列表
failed: []
// 失败任务列表
},
hideSaved: false,
// 是否隐藏已保存项目
autoAddOnScroll: false,
// 是否在滚动时自动添加任务
isAutoScrolling: false,
// 是否正在自动滚动中
autoScrollAttempts: 0,
// 连续自动滚动未发现新任务的次数
rememberScrollPosition: false,
// 是否记住滚动位置
autoResumeAfter429: false,
// 是否在429后自动恢复
autoRefreshEmptyPage: true,
// 新增:无商品可见时自动刷新(默认开启)
hideDiscountedPaid: false,
// 是否隐藏打折的付费商品
hidePaid: false,
// 是否隐藏所有付费商品
blockLargeResources: true,
// 新增:是否在工作标签页中禁用图片/媒体等大资源以节省流量和并发
debugMode: false,
// 是否启用调试模式
lang: "zh",
// 当前语言,默认中文,会在detectLanguage中更新
isExecuting: false,
// 是否正在执行任务
isRefreshScheduled: false,
// 新增:标记是否已经安排了页面刷新
isWorkerTab: false,
// 是否是工作标签页
totalTasks: 0,
// API扫描的总任务数
completedTasks: 0,
// API扫描的已完成任务数
isDispatchingTasks: false,
// 新增:标记是否正在派发任务
isScanningTasks: false,
// 新增:标记是否正在扫描任务,防止重复扫描
processedCardUids: /* @__PURE__ */ new Set(),
// 新增:已处理过的卡片UID,防止重复添加
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
// --- 限速恢复相关状态 ---
consecutiveSuccessCount: 0,
// 连续成功请求计数
requiredSuccessCount: 3,
// 退出限速需要的连续成功请求数
lastLimitSource: "",
// 最后一次限速的来源
isCheckingRateLimit: false,
// 是否正在检查限速状态
// --- 登录态 ---
isAuthenticated: false,
// 当前是否检测到有效的登录态(fab_csrftoken cookie)
// --- 恢复模式 ---
isRecoveryMode: 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,
cardCountCache: {
total: 0,
hidden: 0,
visible: 0,
dirty: true,
documentRef: null,
href: ""
},
lastHideModeKey: "",
executionTotalTasks: 0,
// For execution progress
executionCompletedTasks: 0,
// For execution progress
executionFailedTasks: 0,
// For execution progress
watchdogTimer: null,
// UI-related state
uiExpanded: true,
logs: [],
valueChangeListeners: [],
// For remembering scroll position
knownCursors: /* @__PURE__ */ new Set(),
lastSortMethod: null,
// Session-level tracking (not persisted)
sessionCompleted: /* @__PURE__ */ new Set(),
sessionFailed: /* @__PURE__ */ new Set(),
// 工作线程标签页任务ID
workerTaskId: null,
// 是否显示状态历史表格
showStatusHistory: false,
// Launcher flag
hasRunDomPart: false,
// Observer debounce timer
observerDebounceTimer: null,
// Pending retry for hide/show while card status labels are still loading
hideRetryTimer: null,
// Pending retry for auto-add while card status labels are still loading
autoAddRetryTimer: null,
// UI element references - populated by UI.create()
UI: {
container: null,
tabs: {},
tabContents: {},
statusVisible: null,
statusTodo: null,
statusDone: null,
statusFailed: null,
statusHidden: null,
execBtn: null,
syncBtn: null,
hideBtn: null,
logPanel: null,
savedPositionDisplay: null,
debugContent: null,
historyContainer: null
}
};
// src/modules/utils.js
var UI = null;
var setUIReference = /* @__PURE__ */ __name((uiModule) => {
UI = uiModule;
}, "setUIReference");
var Utils = {
logger: /* @__PURE__ */ __name((type, ...args) => {
if (State.isWorkerTab) {
if (type === "error" || args.some((arg) => typeof arg === "string" && arg.includes("Worker"))) {
console[type](`${Config.SCRIPT_NAME} [Worker]`, ...args);
}
return;
}
if (type === "debug") {
if (!State.debugMode) {
return;
}
console.log(`${Config.SCRIPT_NAME} [DEBUG]`, ...args);
} else {
console[type](`${Config.SCRIPT_NAME}`, ...args);
}
if (State.UI && State.UI.logPanel) {
const messageText = args.join(" ");
const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
const debugPrefix = type === "debug" ? '<span style="color: #34c759;">[DEBUG]</span> ' : "";
const firstChild = State.UI.logPanel.firstChild;
if (firstChild && firstChild.dataset.rawText === messageText) {
const count = parseInt(firstChild.dataset.count || "1") + 1;
firstChild.dataset.count = count.toString();
const timeSpan = firstChild.querySelector(".log-timestamp");
if (timeSpan) timeSpan.textContent = `[${timestamp}]`;
const contentSpan = firstChild.querySelector(".log-content");
if (contentSpan) {
contentSpan.innerHTML = `${debugPrefix}${messageText} <span style="color: #007aff; font-weight: bold; margin-left: 4px;">(x${count})</span>`;
}
} else {
const logEntry = document.createElement("div");
logEntry.style.cssText = "padding: 2px 4px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); font-size: 11px;";
logEntry.dataset.rawText = messageText;
logEntry.dataset.count = "1";
if (type === "error") {
logEntry.style.color = "#ff3b30";
logEntry.style.fontWeight = "500";
} else if (type === "warn") {
logEntry.style.color = "#ff9500";
logEntry.style.fontWeight = "500";
} else if (type === "debug") {
logEntry.style.color = "#8e8e93";
} else {
logEntry.style.color = "#f5f5f7";
}
logEntry.innerHTML = `<span class="log-timestamp" style="color: rgba(255, 255, 255, 0.4);">[${timestamp}]</span> <span class="log-content">${debugPrefix}${messageText}</span>`;
State.UI.logPanel.prepend(logEntry);
}
while (State.UI.logPanel.children.length > 100) {
State.UI.logPanel.removeChild(State.UI.logPanel.lastChild);
}
}
}, "logger"),
getText: /* @__PURE__ */ __name((key, ...args) => {
let text = Config.TEXTS[State.lang]?.[key] || Config.TEXTS["en"]?.[key] || key;
if (args.length > 0) {
if (typeof args[0] === "object" && args[0] !== null) {
const replacements = args[0];
for (const placeholder in replacements) {
text = text.replace(`%${placeholder}%`, replacements[placeholder]);
}
} else {
args.forEach((arg, index) => {
text = text.replace(new RegExp(`\\{${index}\\}`, "g"), arg);
});
}
}
return text;
}, "getText"),
detectLanguage: /* @__PURE__ */ __name(() => {
const oldLang = State.lang;
State.lang = window.location.href.includes("/zh-cn/") ? "zh" : navigator.language.toLowerCase().startsWith("zh") ? "zh" : "en";
Utils.logger("debug", `\u8BED\u8A00\u68C0\u6D4B: \u5730\u5740=${window.location.href}, \u68C0\u6D4B\u5230\u8BED\u8A00=${State.lang}${oldLang !== State.lang ? ` (\u4ECE${oldLang}\u5207\u6362)` : ""}`);
if (oldLang !== State.lang && State.UI && State.UI.container && UI) {
Utils.logger("info", `\u8BED\u8A00\u5DF2\u5207\u6362\u5230${State.lang}\uFF0C\u6B63\u5728\u66F4\u65B0\u754C\u9762...`);
UI.update();
}
}, "detectLanguage"),
waitForElement: /* @__PURE__ */ __name((selector, timeout = 5e3) => {
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);
});
}, "waitForElement"),
waitForButtonEnabled: /* @__PURE__ */ __name((button, timeout = 5e3) => {
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);
});
}, "waitForButtonEnabled"),
// This function is now for UI display purposes only.
getDisplayPageFromUrl: /* @__PURE__ */ __name((url) => {
if (!url) return "1";
try {
const urlParams = new URLSearchParams(new URL(url).search);
const cursor = urlParams.get("cursor");
if (!cursor) return "1";
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();
}
}
return "Cursor Mode";
} catch (e) {
return "...";
}
}, "getDisplayPageFromUrl"),
getCookie: /* @__PURE__ */ __name((name) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(";").shift();
return null;
}, "getCookie"),
// 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: /* @__PURE__ */ __name((element) => {
if (!element) return;
try {
element.focus();
} catch (e) {
}
setTimeout(() => {
const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
Utils.logger("info", `Performing deep click on element: <${element.tagName.toLowerCase()} class="${element.className}">`);
const eventOptions = { view: pageWindow, bubbles: true, cancelable: true, composed: true };
element.dispatchEvent(new PointerEvent("pointerdown", eventOptions));
element.dispatchEvent(new MouseEvent("mousedown", eventOptions));
element.dispatchEvent(new PointerEvent("pointerup", eventOptions));
element.dispatchEvent(new MouseEvent("mouseup", eventOptions));
element.click();
}, 50);
}, "deepClick"),
cleanup: /* @__PURE__ */ __name(() => {
if (State.watchdogTimer) {
clearInterval(State.watchdogTimer);
State.watchdogTimer = null;
}
State.valueChangeListeners.forEach((id) => {
try {
GM_removeValueChangeListener(id);
} catch (e) {
}
});
State.valueChangeListeners = [];
}, "cleanup"),
// ... (existing helper methods) ...
// 添加游标解码函数
decodeCursor: /* @__PURE__ */ __name((cursor) => {
if (!cursor) return Utils.getText("no_saved_position");
try {
const decoded = atob(cursor);
let match;
if (decoded.includes("&p=")) {
match = decoded.match(/&p=([^&]+)/);
} else if (decoded.startsWith("p=")) {
match = decoded.match(/p=([^&]+)/);
}
if (match && match[1]) {
let itemName = decodeURIComponent(match[1].replace(/\+/g, " "));
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(itemName)) {
try {
const date = new Date(itemName);
const dateStr = date.toLocaleDateString();
const timeStr = date.toLocaleTimeString([], { hour12: false });
itemName = `${dateStr} ${timeStr}`;
} catch (e) {
}
}
return `${Utils.getText("position_label")}: "${itemName}"`;
}
return `${Utils.getText("position_label")}: (Unknown)`;
} catch (e) {
Utils.logger("error", `Cursor decode failed: ${e.message}`);
return `${Utils.getText("position_label")}: (Invalid)`;
}
}, "decodeCursor"),
// Helper to extract just the item name from cursor
getCursorItemName: /* @__PURE__ */ __name((cursor) => {
if (!cursor) return null;
try {
const decoded = atob(cursor);
let match;
if (decoded.includes("&p=")) {
match = decoded.match(/&p=([^&]+)/);
} else if (decoded.startsWith("p=")) {
match = decoded.match(/p=([^&]+)/);
}
if (match && match[1]) {
return decodeURIComponent(match[1].replace(/\+/g, " "));
}
} catch (e) {
}
return null;
}, "getCursorItemName"),
// 账号验证函数 - silent模式用于初始化时的检查,不弹出警告
checkAuthentication: /* @__PURE__ */ __name((silent = false) => {
const csrfToken = Utils.getCookie("fab_csrftoken");
if (!csrfToken) {
if (!silent) {
Utils.logger("error", Utils.getText("auth_error"));
if (State.isExecuting) {
State.isExecuting = false;
GM_setValue(Config.DB_KEYS.IS_EXECUTING, false);
}
if (State.UI && State.UI.execBtn) {
State.UI.execBtn.textContent = Utils.getText("execute");
State.UI.execBtn.disabled = true;
}
alert(Utils.getText("auth_error_alert"));
}
State.isAuthenticated = false;
return false;
}
State.isAuthenticated = true;
return true;
}, "checkAuthentication"),
// 从当前页面的 SSR 数据里同步读取登录态。fab.com 在每个页面都内嵌了:
// 1. window._epicAccountId — 已登录是真实 UUID,未登录是空字符串
// 2. <script id="js-json-data-prefetched-data"> 里 "/i/users/me".isAnonymous
// 3. 同一块 JSON 里的 result UUID — 全零(00000000-...)也代表匿名
// 三者任一明确指向匿名 → 返回 false;任一明确指向已登录 → 返回 true;
// 都拿不到 → 返回 null,留给调用方决定要不要走 API 兜底。
detectLoginFromPage: /* @__PURE__ */ __name(() => {
const ZERO_UUID = "00000000-0000-0000-0000-000000000000";
try {
if (typeof window !== "undefined" && Object.prototype.hasOwnProperty.call(window, "_epicAccountId")) {
const id = window._epicAccountId;
if (typeof id === "string") {
if (id === "" || id === ZERO_UUID) return false;
if (/^[0-9a-f-]{32,36}$/i.test(id)) return true;
}
}
} catch (e) {
}
try {
const tag = document && document.getElementById && document.getElementById("js-json-data-prefetched-data");
if (tag && tag.textContent) {
const data = JSON.parse(tag.textContent);
const userInfo = data && data["/i/users/me"];
if (userInfo) {
if (userInfo.isAnonymous === true) return false;
if (userInfo.isAnonymous === false) return true;
if (typeof userInfo.result === "string") {
if (userInfo.result === ZERO_UUID) return false;
if (/^[0-9a-f-]{32,36}$/i.test(userInfo.result)) return true;
}
}
}
} catch (e) {
}
return null;
}, "detectLoginFromPage"),
// 校验登录态。优先用页面 SSR 数据(同步、零网络、零 Cloudflare 风险),
// 拿不到再退回 /i/users/me API 探测。结果缓存到 State.isAuthenticated。
verifyServerSession: /* @__PURE__ */ __name(() => {
const csrfToken = Utils.getCookie("fab_csrftoken");
if (!csrfToken) {
State.isAuthenticated = false;
return Promise.resolve(false);
}
const fromPage = Utils.detectLoginFromPage();
if (fromPage === true) {
State.isAuthenticated = true;
return Promise.resolve(true);
}
if (fromPage === false) {
State.isAuthenticated = false;
return Promise.resolve(false);
}
return new Promise((resolve) => {
try {
GM_xmlhttpRequest({
method: "GET",
url: "https://www.fab.com/i/users/me",
headers: { "x-csrftoken": csrfToken, "x-requested-with": "XMLHttpRequest" },
anonymous: false,
timeout: 5e3,
onload: /* @__PURE__ */ __name((response) => {
if (response.status === 401 || response.status === 403) {
State.isAuthenticated = false;
resolve(false);
return;
}
if (response.status >= 200 && response.status < 300) {
try {
const data = JSON.parse(response.responseText);
if (data && (data.id || data.uid || data.email || data.username || data.isAnonymous === false)) {
State.isAuthenticated = true;
resolve(true);
return;
}
} catch (e) {
}
State.isAuthenticated = false;
resolve(false);
return;
}
resolve(State.isAuthenticated);
}, "onload"),
onerror: /* @__PURE__ */ __name(() => resolve(State.isAuthenticated), "onerror"),
ontimeout: /* @__PURE__ */ __name(() => resolve(State.isAuthenticated), "ontimeout")
});
} catch (e) {
resolve(State.isAuthenticated);
}
});
}, "verifyServerSession"),
// 强校验:cookie + 服务端 session 一起验。返回 boolean。
// notify=true 时对用户提示并停止当前执行。
ensureAuthenticated: /* @__PURE__ */ __name(async (notify = true) => {
if (!Utils.checkAuthentication(true)) {
if (notify) Utils.notifyAuthFailure();
return false;
}
const sessionOk = await Utils.verifyServerSession();
if (!sessionOk) {
if (notify) Utils.notifyAuthFailure();
return false;
}
return true;
}, "ensureAuthenticated"),
notifyAuthFailure: /* @__PURE__ */ __name(() => {
Utils.logger("error", Utils.getText("auth_error"));
if (State.isExecuting) {
State.isExecuting = false;
try {
GM_setValue(Config.DB_KEYS.IS_EXECUTING, false);
} catch (e) {
}
}
if (State.UI && State.UI.execBtn) {
State.UI.execBtn.textContent = Utils.getText("execute");
State.UI.execBtn.disabled = true;
}
try {
alert(Utils.getText("auth_error_alert"));
} catch (e) {
}
}, "notifyAuthFailure"),
// 将所有空白字符(包括换行、多个空格)统一替换为单个空格
normalizeWhitespace: /* @__PURE__ */ __name((text) => {
if (!text) return "";
return text.replace(/\s+/g, " ").trim();
}, "normalizeWhitespace"),
// Broadened to find more clickable elements including inputs and divs/spans that look like buttons.
// Also traverses into same-origin iframes.
findAllButtonsWithShadow: /* @__PURE__ */ __name((root = document) => {
const interactables = [];
const visitedIframes = /* @__PURE__ */ new WeakSet();
const traverse = /* @__PURE__ */ __name((node) => {
if (!node) return;
if (node.nodeType === 1) {
if (node.shadowRoot) {
traverse(node.shadowRoot);
}
if (node.tagName === "IFRAME" && !visitedIframes.has(node)) {
visitedIframes.add(node);
try {
const iframeDoc = node.contentDocument || node.contentWindow?.document;
if (iframeDoc) {
Utils.logger("debug", `Traversing iframe: ${node.src || "(inline)"}`);
traverse(iframeDoc.body || iframeDoc);
}
} catch (e) {
Utils.logger("debug", `Cannot access cross-origin iframe: ${node.src}`);
}
}
const tagName = node.tagName;
const role = node.getAttribute && node.getAttribute("role");
const type = node.getAttribute && node.getAttribute("type");
const className = node.className && typeof node.className === "string" ? node.className : "";
if (tagName === "BUTTON") {
interactables.push(node);
} else if (tagName === "A" && (role === "button" || className.includes("btn") || className.includes("button"))) {
interactables.push(node);
} else if (tagName === "INPUT" && (type === "submit" || type === "button" || type === "reset")) {
interactables.push(node);
} else if (role === "button" || className.includes("payment-order-confirm__btn") || className.includes("place-order")) {
interactables.push(node);
}
}
let child = node.firstChild;
while (child) {
traverse(child);
child = child.nextSibling;
}
}, "traverse");
traverse(root);
if (root === document) {
try {
const allIframes = document.querySelectorAll("iframe");
allIframes.forEach((iframe) => {
if (!visitedIframes.has(iframe)) {
visitedIframes.add(iframe);
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
if (iframeDoc) {
Utils.logger("debug", `Top-level iframe search: ${iframe.src || "(inline)"}`);
traverse(iframeDoc.body || iframeDoc);
}
} catch (e) {
}
}
});
} catch (e) {
}
}
return interactables;
}, "findAllButtonsWithShadow")
};
// src/modules/page-diagnostics.js
var PageDiagnostics = {
// 诊断商品详情页面状态
diagnoseDetailPage: /* @__PURE__ */ __name(() => {
const report = {
timestamp: (/* @__PURE__ */ 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("\u5E93") || text.includes("Library") || text.includes("\u62E5\u6709") || text.includes("Owned"))) {
report.ownedStatus[`owned_${index}`] = {
text,
isVisible: elem.offsetParent !== null,
tagName: elem.tagName,
classes: elem.className
};
}
});
return report;
}, "diagnoseDetailPage"),
// 输出诊断报告到日志
logDiagnosticReport: /* @__PURE__ */ __name((report) => {
console.group("=== Fab Helper \u9875\u9762\u72B6\u6001\u8BCA\u65AD\u62A5\u544A ===");
console.log(`\u9875\u9762: ${report.url}`);
console.log(`\u6807\u9898: ${report.pageTitle}`);
const visibleButtons = report.buttons.filter((b) => b.isVisible);
if (visibleButtons.length > 0) {
console.log(`--- \u53EF\u89C1\u6309\u94AE (${visibleButtons.length}) ---`);
visibleButtons.forEach((btn) => {
console.log(` [${btn.index}] "${btn.text}" (\u7981\u7528: ${btn.isDisabled}, \u7C7B: ${btn.classes.split(" ").slice(0, 2).join(" ")}...)`);
});
}
const visibleLicenses = report.licenseOptions.filter((l) => l.isVisible);
if (visibleLicenses.length > 0) {
console.log(`--- \u53EF\u89C1\u8BB8\u53EF\u9009\u9879 (${visibleLicenses.length}) ---`);
visibleLicenses.forEach((opt) => {
console.log(` [${opt.index}] "${opt.text}" (\u6807\u7B7E: ${opt.tagName}, \u89D2\u8272: ${opt.role || "\u65E0"})`);
});
}
const visiblePrices = Object.values(report.priceInfo).filter((p) => p.isVisible);
if (visiblePrices.length > 0) {
console.log(`--- \u4EF7\u683C\u4FE1\u606F (${visiblePrices.length}) ---`);
visiblePrices.forEach((p) => console.log(` \u4EF7\u683C: "${p.text}"`));
}
const visibleOwned = Object.values(report.ownedStatus).filter((s) => s.isVisible);
if (visibleOwned.length > 0) {
console.log(`--- \u62E5\u6709\u72B6\u6001 (${visibleOwned.length}) ---`);
visibleOwned.forEach((s) => console.log(` \u72B6\u6001: "${s.text}"`));
}
console.groupEnd();
}, "logDiagnosticReport")
};
// src/modules/data-cache.js
var DataCache = {
// 商品数据缓存 - 键为商品ID,值为商品数据
listings: /* @__PURE__ */ new Map(),
// 拥有状态缓存 - 键为商品ID,值为拥有状态对象
ownedStatus: /* @__PURE__ */ new Map(),
// 价格缓存 - 键为报价ID,值为价格信息对象
prices: /* @__PURE__ */ new Map(),
// Maps Fab offer ids back to listing uids so done listings can skip price requests.
offerListingUids: /* @__PURE__ */ new Map(),
// 等待网页原生请求更新的UID列表
waitingList: /* @__PURE__ */ new Set(),
// 缓存时间戳 - 用于判断缓存是否过期
timestamps: {
listings: /* @__PURE__ */ new Map(),
ownedStatus: /* @__PURE__ */ new Map(),
prices: /* @__PURE__ */ new Map()
},
// 缓存有效期(毫秒)
TTL: 5 * 60 * 1e3,
// 5分钟
// 检查缓存是否有效
isValid: /* @__PURE__ */ __name(function(type, key) {
const timestamp = this.timestamps[type].get(key);
return timestamp && Date.now() - timestamp < this.TTL;
}, "isValid"),
// 保存商品数据到缓存
saveListings: /* @__PURE__ */ __name(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);
this.extractOfferIds(item).forEach((offerId) => {
this.offerListingUids.set(offerId, item.uid);
});
}
});
}, "saveListings"),
extractOfferIds: /* @__PURE__ */ __name(function(item) {
const offerIds = /* @__PURE__ */ new Set();
const add = /* @__PURE__ */ __name((value) => {
if (typeof value === "string" && value) offerIds.add(value);
}, "add");
add(item.offerId);
add(item.offer_id);
add(item.defaultOfferId);
add(item.default_offer_id);
add(item.offer?.offerId);
add(item.offer?.offer_id);
add(item.defaultOffer?.offerId);
add(item.defaultOffer?.offer_id);
add(item.startingPrice?.offerId);
add(item.startingPrice?.offer_id);
add(item.starting_price?.offerId);
add(item.starting_price?.offer_id);
add(item.price?.offerId);
add(item.price?.offer_id);
add(item.priceInfo?.offerId);
add(item.priceInfo?.offer_id);
if (Array.isArray(item.offers)) {
item.offers.forEach((offer) => {
add(offer?.offerId);
add(offer?.offer_id);
});
}
if (Array.isArray(item.licenses)) {
item.licenses.forEach((license) => {
add(license?.offerId);
add(license?.offer_id);
add(license?.offer?.offerId);
add(license?.offer?.offer_id);
});
}
return Array.from(offerIds);
}, "extractOfferIds"),
getListingUidForOffer: /* @__PURE__ */ __name(function(offerId) {
return this.offerListingUids.get(offerId) || "";
}, "getListingUidForOffer"),
// 添加到等待列表
addToWaitingList: /* @__PURE__ */ __name(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)}`);
}, "addToWaitingList"),
// 检查并从等待列表中移除
checkWaitingList: /* @__PURE__ */ __name(function() {
if (this.waitingList.size === 0) return;
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] \u4ECE\u7B49\u5F85\u5217\u8868\u4E2D\u79FB\u9664\u4E86 ${removedCount} \u4E2A\u5DF2\u66F4\u65B0\u7684\u5546\u54C1ID\uFF0C\u5269\u4F59: ${this.waitingList.size}`);
}
}, "checkWaitingList"),
// 保存拥有状态到缓存
saveOwnedStatus: /* @__PURE__ */ __name(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 || (/* @__PURE__ */ 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();
}
}, "saveOwnedStatus"),
// 保存价格信息到缓存
savePrices: /* @__PURE__ */ __name(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);
}
});
}, "savePrices"),
// 获取商品数据,如果缓存有效则使用缓存
getListings: /* @__PURE__ */ __name(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 };
}, "getListings"),
// 获取拥有状态,如果缓存有效则使用缓存
getOwnedStatus: /* @__PURE__ */ __name(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 };
}, "getOwnedStatus"),
// 获取价格信息,如果缓存有效则使用缓存
getPrices: /* @__PURE__ */ __name(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 };
}, "getPrices"),
// 清理过期缓存
cleanupExpired: /* @__PURE__ */ __name(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", `\u7F13\u5B58\u6E05\u7406\u5931\u8D25: ${e.message}`);
}
}, "cleanupExpired")
};
// src/modules/api.js
var API = {
gmFetch: /* @__PURE__ */ __name((options) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
anonymous: false,
// Default to false to ensure cookies are sent
...options,
onload: /* @__PURE__ */ __name((response) => resolve(response), "onload"),
onerror: /* @__PURE__ */ __name((error) => reject(new Error(`GM_xmlhttpRequest error: ${error.statusText || "Unknown Error"}`)), "onerror"),
ontimeout: /* @__PURE__ */ __name(() => reject(new Error("Request timed out.")), "ontimeout"),
onabort: /* @__PURE__ */ __name(() => reject(new Error("Request aborted.")), "onabort")
});
});
}, "gmFetch"),
// 接口响应数据提取函数
extractStateData: /* @__PURE__ */ __name((rawData, source = "") => {
const dataType = Array.isArray(rawData) ? "Array" : typeof rawData;
if (State.debugMode) {
Utils.logger("debug", `[${source}] \u63A5\u53E3\u8FD4\u56DE\u6570\u636E\u7C7B\u578B: ${dataType}`);
}
if (Array.isArray(rawData)) {
return rawData;
}
if (rawData && typeof rawData === "object") {
const keys = Object.keys(rawData);
if (State.debugMode) {
Utils.logger("debug", `[${source}] \u63A5\u53E3\u8FD4\u56DE\u5BF9\u8C61\u952E: ${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}] \u5728\u5B57\u6BB5 "${field}" \u4E2D\u627E\u5230\u6570\u7EC4\u6570\u636E`);
return rawData[field];
}
}
for (const key of keys) {
if (Array.isArray(rawData[key])) {
Utils.logger("info", `[${source}] \u5728\u5B57\u6BB5 "${key}" \u4E2D\u627E\u5230\u6570\u7EC4\u6570\u636E`);
return rawData[key];
}
}
if (rawData.uid && "acquired" in rawData) {
Utils.logger("info", `[${source}] \u8FD4\u56DE\u7684\u662F\u5355\u4E2A\u9879\u76EE\u6570\u636E\uFF0C\u8F6C\u6362\u4E3A\u6570\u7EC4`);
return [rawData];
}
}
Utils.logger("warn", `[${source}] \u65E0\u6CD5\u4ECEAPI\u54CD\u5E94\u4E2D\u63D0\u53D6\u6570\u7EC4\u6570\u636E`);
if (State.debugMode) {
try {
const preview = JSON.stringify(rawData).substring(0, 200);
Utils.logger("debug", `[${source}] API\u54CD\u5E94\u9884\u89C8: ${preview}...`);
} catch (e) {
Utils.logger("debug", `[${source}] \u65E0\u6CD5\u5E8F\u5217\u5316API\u54CD\u5E94: ${e.message}`);
}
}
return [];
}, "extractStateData"),
// 优化后的商品拥有状态检查函数 - 只使用缓存和网页原生请求的数据
checkItemsOwnership: /* @__PURE__ */ __name(async function(uids) {
if (!uids || uids.length === 0) return [];
try {
const { result: cachedResults, missing: missingUids } = DataCache.getOwnedStatus(uids);
if (missingUids.length > 0) {
Utils.logger("debug", Utils.getText("fab_dom_unknown_status", missingUids.length));
DataCache.addToWaitingList(missingUids);
}
return cachedResults;
} catch (e) {
Utils.logger("error", `\u68C0\u67E5\u62E5\u6709\u72B6\u6001\u5931\u8D25: ${e.message}`);
return [];
}
}, "checkItemsOwnership"),
// 优化后的价格验证函数
checkItemsPrices: /* @__PURE__ */ __name(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", `\u4F7F\u7528\u7F13\u5B58\u7684\u4EF7\u683C\u6570\u636E\uFF0C\u907F\u514DAPI\u8BF7\u6C42`);
}
return cachedResults;
}
if (State.debugMode) {
Utils.logger("info", `\u5BF9 ${missingOfferIds.length} \u4E2A\u7F3A\u5931\u7684\u62A5\u4EF7ID\u53D1\u9001API\u8BF7\u6C42`);
}
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);
return [...cachedResults, ...pricesData.offers];
}
} catch (e) {
Utils.logger("error", `[\u4F18\u5316] \u89E3\u6790\u4EF7\u683CAPI\u54CD\u5E94\u5931\u8D25: ${e.message}`);
}
return cachedResults;
} catch (e) {
Utils.logger("error", `[\u4F18\u5316] \u83B7\u53D6\u4EF7\u683C\u4FE1\u606F\u5931\u8D25: ${e.message}`);
return [];
}
}, "checkItemsPrices")
};
// src/modules/database.js
var UI2 = null;
var setUIReference2 = /* @__PURE__ */ __name((uiModule) => {
UI2 = uiModule;
}, "setUIReference");
var Database = {
normalizeListingUrl: /* @__PURE__ */ __name((url) => {
if (!url) return "";
const cleanUrl = String(url).split("?")[0].toLowerCase();
const uidMatch = cleanUrl.match(/\/listings\/([^/?#]+)/i);
if (uidMatch && uidMatch[1]) {
return `https://www.fab.com/listings/${uidMatch[1].toLowerCase()}`;
}
return cleanUrl;
}, "normalizeListingUrl"),
getListingUid: /* @__PURE__ */ __name((url) => {
if (!url) return "";
const uidMatch = String(url).split("?")[0].match(/\/listings\/([^/?#]+)/i);
return uidMatch && uidMatch[1] ? uidMatch[1].toLowerCase() : "";
}, "getListingUid"),
seedDoneOwnedStatusCache: /* @__PURE__ */ __name(() => {
const states = State.db.done.map((url) => Database.getListingUid(url)).filter(Boolean).map((uid) => ({
uid,
acquired: true,
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
}));
DataCache.saveOwnedStatus(states);
return states.length;
}, "seedDoneOwnedStatusCache"),
normalizeDoneList: /* @__PURE__ */ __name(() => {
const normalized = [];
const seen = /* @__PURE__ */ new Set();
State.db.done.forEach((url) => {
const cleanUrl = Database.normalizeListingUrl(url);
if (!cleanUrl || seen.has(cleanUrl)) return;
seen.add(cleanUrl);
normalized.push(cleanUrl);
});
const changed = normalized.length !== State.db.done.length || normalized.some((url, index) => url !== State.db.done[index]);
State.db.done = normalized;
return changed;
}, "normalizeDoneList"),
addDoneUrl: /* @__PURE__ */ __name((url) => {
const normalizedChanged = Database.normalizeDoneList();
const cleanUrl = Database.normalizeListingUrl(url);
if (!cleanUrl) return normalizedChanged;
if (Database.isDone(cleanUrl)) return normalizedChanged;
State.db.done.push(cleanUrl);
Database.seedDoneOwnedStatusCache();
return true;
}, "addDoneUrl"),
load: /* @__PURE__ */ __name(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);
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.hideDiscountedPaid = await GM_getValue(Config.DB_KEYS.HIDE_DISCOUNTED, false);
State.hidePaid = await GM_getValue(Config.DB_KEYS.HIDE_PAID, false);
State.blockLargeResources = await GM_getValue(Config.DB_KEYS.BLOCK_RESOURCES, 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);
State.statusHistory = await GM_getValue(Config.DB_KEYS.STATUS_HISTORY, []);
if (Database.normalizeDoneList()) {
await Database.saveDone();
}
Database.seedDoneOwnedStatusCache();
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}`);
}, "load"),
// 添加保存待办列表的方法
saveTodo: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.TODO, State.db.todo), "saveTodo"),
saveDone: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.DONE, State.db.done), "saveDone"),
saveFailed: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.FAILED, State.db.failed), "saveFailed"),
saveHidePref: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.HIDE, State.hideSaved), "saveHidePref"),
saveAutoAddPref: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.AUTO_ADD, State.autoAddOnScroll), "saveAutoAddPref"),
// Save the setting
saveRememberPosPref: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.REMEMBER_POS, State.rememberScrollPosition), "saveRememberPosPref"),
saveAutoResumePref: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.AUTO_RESUME, State.autoResumeAfter429), "saveAutoResumePref"),
saveAutoRefreshEmptyPref: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.AUTO_REFRESH_EMPTY, State.autoRefreshEmptyPage), "saveAutoRefreshEmptyPref"),
// 保存无商品自动刷新设置
saveHideDiscountedPref: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.HIDE_DISCOUNTED, State.hideDiscountedPaid), "saveHideDiscountedPref"),
// 保存隐藏打折付费设置
saveHidePaidPref: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.HIDE_PAID, State.hidePaid), "saveHidePaidPref"),
// 保存隐藏所有付费设置
saveBlockResourcesPref: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.BLOCK_RESOURCES, State.blockLargeResources), "saveBlockResourcesPref"),
// 保存禁用大资源设置
saveExecutingState: /* @__PURE__ */ __name(() => GM_setValue(Config.DB_KEYS.IS_EXECUTING, State.isExecuting), "saveExecutingState"),
// Save the execution state
resetAllData: /* @__PURE__ */ __name(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);
await GM_deleteValue(Config.DB_KEYS.LAST_CURSOR);
await GM_deleteValue(Config.DB_KEYS.HIDE_DISCOUNTED);
await GM_deleteValue(Config.DB_KEYS.HIDE_PAID);
await GM_deleteValue(Config.DB_KEYS.BLOCK_RESOURCES);
State.db.todo = [];
State.db.done = [];
State.db.failed = [];
State.savedCursor = null;
State.blockLargeResources = true;
Utils.logger("info", "\u6240\u6709\u811A\u672C\u6570\u636E\uFF08\u5305\u62EC\u6EDA\u52A8\u8BB0\u5FC6\u4E0E\u5927\u8D44\u6E90\u7981\u7528\u8BBE\u7F6E\uFF09\u5DF2\u91CD\u7F6E\u3002");
if (UI2) {
UI2.removeAllOverlays();
UI2.update();
}
}
}, "resetAllData"),
isDone: /* @__PURE__ */ __name((url) => {
if (!url) return false;
const cleanUrl = Database.normalizeListingUrl(url);
return State.db.done.some((doneUrl) => Database.normalizeListingUrl(doneUrl) === cleanUrl);
}, "isDone"),
isFailed: /* @__PURE__ */ __name((url) => {
if (!url) return false;
const cleanUrl = url.split("?")[0];
return State.db.failed.some((task) => task.url === cleanUrl);
}, "isFailed"),
isTodo: /* @__PURE__ */ __name((url) => {
if (!url) return false;
const cleanUrl = url.split("?")[0];
return State.db.todo.some((task) => task.url === cleanUrl);
}, "isTodo"),
markAsDone: /* @__PURE__ */ __name(async (task) => {
if (!task || !task.uid) {
Utils.logger("error", "\u6807\u8BB0\u4EFB\u52A1\u5B8C\u6210\u5931\u8D25\uFF0C\u6536\u5230\u65E0\u6548\u4EFB\u52A1:", 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", "\u4EFB\u52A1\u672A\u80FD\u4ECE\u5F85\u529E\u5217\u8868\u4E2D\u79FB\u9664\uFF0C\u53EF\u80FD\u5DF2\u88AB\u5176\u4ED6\u64CD\u4F5C\u5904\u7406");
}
let changed = false;
const initialFailedCount = State.db.failed.length;
State.db.failed = State.db.failed.filter((failedTask) => failedTask.uid !== task.uid);
if (State.db.failed.length !== initialFailedCount) {
changed = true;
await Database.saveFailed();
}
if (Database.addDoneUrl(task.url)) {
changed = true;
}
if (changed) {
await Database.saveDone();
}
}, "markAsDone"),
// 根据失败原因判定自动重试上限:
// - 人机验证/适配类:重试无意义且会招更多风控 → 0 次,直接进失败列表
// - 环境类(超时/被远程判死/唤醒清理):值得自动重试 → 3 次
// - 其他(可能只是页面没加载好):给 1 次机会
getMaxRetry: /* @__PURE__ */ __name((reason) => {
if (!reason) return 1;
if (/人机验证|captcha|需要人工|verification/i.test(reason)) return 0;
if (/超时|Watchdog|完成前关闭|closed before|Timeout|唤醒/i.test(reason)) return 3;
return 1;
}, "getMaxRetry"),
// 返回 { retried: bool }。retried=true 表示已按归因自动放回待办重试,未计入最终失败;
// 调用方据此决定是否累加 executionFailedTasks。
markAsFailed: /* @__PURE__ */ __name(async (task, failureInfo = {}) => {
if (!task || !task.uid) {
Utils.logger("error", "\u6807\u8BB0\u4EFB\u52A1\u5931\u8D25\uFF0C\u6536\u5230\u65E0\u6548\u4EFB\u52A1:", JSON.stringify(task));
return { retried: false };
}
const reason = failureInfo.reason || "\u672A\u77E5\u539F\u56E0";
const nextRetry = (task.retryCount || 0) + 1;
const maxRetry = Database.getMaxRetry(reason);
State.db.todo = State.db.todo.filter((t) => t.uid !== task.uid);
if (nextRetry <= maxRetry) {
State.db.todo.push({ ...task, retryCount: nextRetry });
Utils.logger("info", Utils.getText("log_auto_retry", nextRetry, maxRetry, task.name) + ` [${reason}]`);
await Database.saveTodo();
if (UI2) UI2.update();
return { retried: true };
}
const failedTask = {
...task,
failedAt: (/* @__PURE__ */ new Date()).toISOString(),
failureReason: reason,
errorDetails: failureInfo.details || null,
workerLogs: failureInfo.logs || [],
retryCount: nextRetry
};
Utils.logger("warn", `\u{1F4CB} \u4EFB\u52A1\u5931\u8D25\u8BE6\u60C5:`);
Utils.logger("warn", ` - \u4EFB\u52A1\u540D\u79F0: ${task.name}`);
Utils.logger("warn", ` - \u4EFB\u52A1UID: ${task.uid}`);
Utils.logger("warn", ` - \u5931\u8D25\u539F\u56E0: ${failedTask.failureReason}`);
Utils.logger("warn", ` - \u91CD\u8BD5\u6B21\u6570: ${failedTask.retryCount}`);
if (failedTask.errorDetails) {
Utils.logger("warn", ` - \u9519\u8BEF\u8BE6\u60C5: ${JSON.stringify(failedTask.errorDetails)}`);
}
if (failedTask.workerLogs && failedTask.workerLogs.length > 0) {
Utils.logger("warn", ` - \u5DE5\u4F5C\u7EBF\u7A0B\u65E5\u5FD7 (${failedTask.workerLogs.length} \u6761):`);
failedTask.workerLogs.slice(-5).forEach((log, i) => {
Utils.logger("warn", ` ${i + 1}. ${log}`);
});
}
const existingIndex = State.db.failed.findIndex((f) => f.uid === task.uid);
if (existingIndex >= 0) {
State.db.failed[existingIndex] = failedTask;
Utils.logger("debug", `\u66F4\u65B0\u4E86\u5DF2\u5B58\u5728\u7684\u5931\u8D25\u8BB0\u5F55: ${task.name}`);
} else {
State.db.failed.push(failedTask);
}
await Database.saveTodo();
await Database.saveFailed();
return { retried: false };
}, "markAsFailed")
};
// src/modules/rate-limit-manager.js
var UI3 = null;
var TaskRunner = null;
var countdownRefresh = null;
var setDependencies = /* @__PURE__ */ __name((deps) => {
UI3 = deps.UI;
TaskRunner = deps.TaskRunner;
countdownRefresh = deps.countdownRefresh;
}, "setDependencies");
var RateLimitManager = {
// 添加防止重复日志的变量
_lastLogTime: 0,
_lastLogType: null,
_duplicateLogCount: 0,
// 活跃心跳跟踪
_lastActiveTime: 0,
// 本实例上次写入活跃时间戳的时刻
_trackingInterval: null,
// 心跳 setInterval 句柄
// 检查是否与最后一条记录重复
isDuplicateRecord: /* @__PURE__ */ __name(function(newEntry) {
if (State.statusHistory.length === 0) return false;
const lastEntry = State.statusHistory[State.statusHistory.length - 1];
if (lastEntry.type !== newEntry.type) return false;
const lastTime = new Date(lastEntry.endTime).getTime();
const newTime = new Date(newEntry.endTime).getTime();
const timeDiff = Math.abs(newTime - lastTime);
if (timeDiff < 1e4) {
const durationDiff = Math.abs((lastEntry.duration || 0) - (newEntry.duration || 0));
if (durationDiff < 5) {
return true;
}
}
return false;
}, "isDuplicateRecord"),
// 添加记录到历史,带去重检查
addToHistory: /* @__PURE__ */ __name(async function(entry) {
if (this.isDuplicateRecord(entry)) {
Utils.logger("debug", `\u68C0\u6D4B\u5230\u91CD\u590D\u7684\u72B6\u6001\u8BB0\u5F55\uFF0C\u8DF3\u8FC7: ${entry.type} - ${entry.endTime}`);
return false;
}
State.statusHistory.push(entry);
if (State.statusHistory.length > 50) {
State.statusHistory = State.statusHistory.slice(-50);
}
await GM_setValue(Config.DB_KEYS.STATUS_HISTORY, State.statusHistory);
return true;
}, "addToHistory"),
// 进入限速状态
enterRateLimitedState: /* @__PURE__ */ __name(async function(source = "\u672A\u77E5\u6765\u6E90") {
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;
const normalDuration = State.normalStartTime ? ((Date.now() - State.normalStartTime) / 1e3).toFixed(2) : "0.00";
const logEntry = {
type: "NORMAL",
duration: parseFloat(normalDuration),
requests: State.successfulSearchCount,
endTime: (/* @__PURE__ */ new Date()).toISOString()
};
const wasAdded = await this.addToHistory(logEntry);
if (wasAdded) {
Utils.logger("error", Utils.getText("log_entering_rate_limit_from_v2", source, normalDuration, State.successfulSearchCount));
} 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
});
if (UI3) {
UI3.updateDebugTab();
UI3.update();
}
const { visible: actualVisibleCards } = TaskRunner?.getCardCounts ? TaskRunner.getCardCounts() : { visible: document.querySelectorAll(Config.SELECTORS.card).length };
const visibleCountElement = document.getElementById("fab-status-visible");
if (visibleCountElement) {
visibleCountElement.textContent = actualVisibleCards.toString();
}
if (State.db.todo.length > 0 || State.activeWorkers > 0 || actualVisibleCards > 0) {
if (actualVisibleCards > 0) {
Utils.logger("info", `\u68C0\u6D4B\u5230\u9875\u9762\u4E0A\u6709 ${actualVisibleCards} \u4E2A\u53EF\u89C1\u5546\u54C1\uFF0C\u6682\u4E0D\u81EA\u52A8\u5237\u65B0\u9875\u9762\u3002`);
Utils.logger("info", "\u5F53\u4ECD\u6709\u53EF\u89C1\u5546\u54C1\u65F6\u4E0D\u89E6\u53D1\u81EA\u52A8\u5237\u65B0\uFF0C\u4EE5\u907F\u514D\u4E2D\u65AD\u6D4F\u89C8\u3002");
} else {
Utils.logger("info", `\u68C0\u6D4B\u5230\u6709 ${State.db.todo.length} \u4E2A\u5F85\u529E\u4EFB\u52A1\u548C ${State.activeWorkers} \u4E2A\u6D3B\u52A8\u5DE5\u4F5C\u7EBF\u7A0B\uFF0C\u6682\u4E0D\u81EA\u52A8\u5237\u65B0\u9875\u9762\u3002`);
Utils.logger("info", "\u8BF7\u624B\u52A8\u5B8C\u6210\u6216\u53D6\u6D88\u8FD9\u4E9B\u4EFB\u52A1\u540E\u518D\u5237\u65B0\u9875\u9762\u3002");
}
Utils.logger("warn", "\u26A0\uFE0F \u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u4F46\u4E0D\u6EE1\u8DB3\u81EA\u52A8\u5237\u65B0\u6761\u4EF6\uFF0C\u8BF7\u5728\u9700\u8981\u65F6\u624B\u52A8\u5237\u65B0\u9875\u9762\u3002");
} else if (State.autoRefreshEmptyPage) {
const randomDelay = 45e3 + Math.random() * 15e3;
if (State.autoResumeAfter429) {
Utils.logger("info", Utils.getText("log_auto_resume_start", randomDelay ? (randomDelay / 1e3).toFixed(1) : "\u672A\u77E5"));
} else {
Utils.logger("info", Utils.getText("log_auto_resume_detect", randomDelay ? (randomDelay / 1e3).toFixed(1) : "\u672A\u77E5"));
}
if (countdownRefresh) {
countdownRefresh(randomDelay, "429\u81EA\u52A8\u6062\u590D");
}
} else {
Utils.logger("info", Utils.getText("auto_refresh_disabled_rate_limit"));
}
return true;
}, "enterRateLimitedState"),
// 记录成功请求
recordSuccessfulRequest: /* @__PURE__ */ __name(async function(source = "\u672A\u77E5\u6765\u6E90", hasResults = true) {
if (hasResults) {
State.successfulSearchCount++;
if (UI3) UI3.updateDebugTab();
}
if (State.appStatus !== "RATE_LIMITED") {
return;
}
if (State.rateLimitStartTime) {
const timeSinceRateLimit = Date.now() - State.rateLimitStartTime;
if (timeSinceRateLimit < 4e4) {
Utils.logger("debug", `\u5904\u4E8E\u9650\u901F\u5F3A\u5236\u51B7\u9759\u671F (${(timeSinceRateLimit / 1e3).toFixed(1)}s < 40s)\uFF0C\u5FFD\u7565\u6B64\u6B21\u6210\u529F\u8BF7\u6C42\u3002`);
return;
}
}
if (!hasResults) {
Utils.logger("debug", `\u8BF7\u6C42\u6210\u529F\u4F46\u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7ED3\u679C\uFF0C\u4E0D\u8BA1\u5165\u8FDE\u7EED\u6210\u529F\u8BA1\u6570\u3002\u6765\u6E90: ${source}`);
State.consecutiveSuccessCount = 0;
return;
}
State.consecutiveSuccessCount++;
const isMilestone = State.consecutiveSuccessCount % 3 === 0 || State.consecutiveSuccessCount >= State.requiredSuccessCount;
const logMsg = Utils.getText("rate_limit_success_request", State.consecutiveSuccessCount, State.requiredSuccessCount, source);
Utils.logger(isMilestone ? "info" : "debug", logMsg);
if (State.consecutiveSuccessCount >= State.requiredSuccessCount) {
await this.exitRateLimitedState(Utils.getText("consecutive_success_exit", State.consecutiveSuccessCount, source));
}
}, "recordSuccessfulRequest"),
// 退出限速状态
exitRateLimitedState: /* @__PURE__ */ __name(async function(source = "\u672A\u77E5\u6765\u6E90") {
if (State.appStatus !== "RATE_LIMITED") {
Utils.logger("info", `\u5F53\u524D\u4E0D\u662F\u9650\u901F\u72B6\u6001\uFF0C\u5FFD\u7565\u9000\u51FA\u9650\u901F\u8BF7\u6C42: ${source}`);
return false;
}
const rateLimitDuration = State.rateLimitStartTime ? ((Date.now() - State.rateLimitStartTime) / 1e3).toFixed(2) : "0.00";
const logEntry = {
type: "RATE_LIMITED",
duration: parseFloat(rateLimitDuration),
endTime: (/* @__PURE__ */ new Date()).toISOString(),
source
};
const wasAdded = await this.addToHistory(logEntry);
if (wasAdded) {
Utils.logger("info", Utils.getText("rate_limit_recovery_success", source, rateLimitDuration));
} else {
Utils.logger("debug", `\u68C0\u6D4B\u5230\u91CD\u590D\u7684\u9650\u901F\u72B6\u6001\u8BB0\u5F55\uFF0C\u6765\u6E90: ${source}`);
}
State.appStatus = "NORMAL";
State.rateLimitStartTime = null;
State.normalStartTime = Date.now();
State.consecutiveSuccessCount = 0;
await GM_deleteValue(Config.DB_KEYS.APP_STATUS);
if (UI3) {
UI3.updateDebugTab();
UI3.update();
}
if (State.db.todo.length > 0 && !State.isExecuting && TaskRunner) {
Utils.logger("info", `\u53D1\u73B0 ${State.db.todo.length} \u4E2A\u5F85\u529E\u4EFB\u52A1\uFF0C\u81EA\u52A8\u6062\u590D\u6267\u884C...`);
State.isExecuting = true;
Database.saveExecutingState();
TaskRunner.executeBatch();
}
return true;
}, "exitRateLimitedState"),
// 检查限速状态
checkRateLimitStatus: /* @__PURE__ */ __name(async function() {
if (State.isCheckingRateLimit) {
Utils.logger("info", "\u5DF2\u6709\u9650\u901F\u72B6\u6001\u68C0\u67E5\u6B63\u5728\u8FDB\u884C\uFF0C\u8DF3\u8FC7\u672C\u6B21\u68C0\u67E5");
return false;
}
State.isCheckingRateLimit = true;
try {
Utils.logger("debug", Utils.getText("log_rate_limit_check_start"));
let pageText = "";
const uiContainer = document.getElementById(Config.UI_CONTAINER_ID);
if (uiContainer) {
const originalDisplay = uiContainer.style.display;
uiContainer.style.display = "none";
pageText = document.body.innerText || "";
uiContainer.style.display = originalDisplay;
} else {
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", "\u9875\u9762\u5185\u5BB9\u5305\u542B\u9650\u901F\u4FE1\u606F\uFF0C\u786E\u8BA4\u4ECD\u5904\u4E8E\u9650\u901F\u72B6\u6001");
await this.enterRateLimitedState("\u9875\u9762\u5185\u5BB9\u68C0\u6D4B");
return false;
}
Utils.logger("debug", "\u4F7F\u7528Performance API\u68C0\u67E5\u6700\u8FD1\u7684\u7F51\u7EDC\u8BF7\u6C42\uFF0C\u4E0D\u518D\u4E3B\u52A8\u53D1\u9001API\u8BF7\u6C42");
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 < 1e4);
if (recentRequests.length > 0) {
const has429 = recentRequests.some((r) => r.responseStatus === 429);
if (has429) {
Utils.logger("info", `\u68C0\u6D4B\u5230\u6700\u8FD110\u79D2\u5185\u6709429\u72B6\u6001\u7801\u7684\u8BF7\u6C42\uFF0C\u5224\u65AD\u4E3A\u9650\u901F\u72B6\u6001`);
await this.enterRateLimitedState("Performance API\u68C0\u6D4B429");
return false;
}
const hasSuccess = recentRequests.some((r) => r.responseStatus >= 200 && r.responseStatus < 300);
if (hasSuccess) {
Utils.logger("info", `\u68C0\u6D4B\u5230\u6700\u8FD110\u79D2\u5185\u6709\u6210\u529F\u7684API\u8BF7\u6C42\uFF0C\u5224\u65AD\u4E3A\u6B63\u5E38\u72B6\u6001`);
await this.recordSuccessfulRequest("Performance API\u68C0\u6D4B\u6210\u529F", true);
return true;
}
}
}
if (State.appStatus === "RATE_LIMITED") {
const cooldownPassed = State.rateLimitStartTime ? Date.now() - State.rateLimitStartTime > 4e4 : true;
if (cooldownPassed) {
Utils.logger("info", "\u9650\u901F\u51B7\u9759\u671F\u5DF2\u8FC7\uFF0C\u4E3B\u52A8\u53D1\u9001\u8F7B\u91CF\u63A2\u6D4B\u8BF7\u6C42\u4EE5\u786E\u8BA4\u72B6\u6001...");
try {
const csrfToken = Utils.getCookie("fab_csrftoken");
if (csrfToken) {
const response = await API.gmFetch({
method: "GET",
url: "https://www.fab.com/i/users/context",
headers: { "x-csrftoken": csrfToken, "x-requested-with": "XMLHttpRequest" }
});
if (response.status === 200) {
Utils.logger("info", "\u63A2\u6D4B\u8BF7\u6C42\u6210\u529F\uFF0CAPI\u9650\u901F\u5DF2\u89E3\u9664");
await this.exitRateLimitedState("\u63A2\u6D4B\u8BF7\u6C42\u6210\u529F");
return true;
} else if (response.status === 429) {
Utils.logger("warn", "\u63A2\u6D4B\u8BF7\u6C42\u8FD4\u56DE429\uFF0C\u786E\u8BA4\u4ECD\u5904\u4E8E\u9650\u901F\u72B6\u6001");
State.rateLimitStartTime = Date.now();
await GM_setValue(Config.DB_KEYS.APP_STATUS, {
status: "RATE_LIMITED",
startTime: State.rateLimitStartTime,
source: "\u63A2\u6D4B\u8BF7\u6C42429"
});
return false;
}
}
} catch (probeError) {
Utils.logger("error", `\u63A2\u6D4B\u8BF7\u6C42\u53D1\u9001\u5931\u8D25: ${probeError.message}`);
}
}
}
Utils.logger("debug", Utils.getText("log_insufficient_info_status"));
return State.appStatus === "NORMAL";
} catch (e) {
Utils.logger("error", `\u9650\u901F\u72B6\u6001\u68C0\u67E5\u5931\u8D25: ${e.message}`);
return false;
} finally {
State.isCheckingRateLimit = false;
}
}, "checkRateLimitStatus"),
/**
* 检测不活动区间(浏览器关闭 / 标签页冻结),并修正限速期时长。
* @param {boolean} isStartup - 是否为脚本启动时的首次检查
*/
checkInactivity: /* @__PURE__ */ __name(async function(isStartup = false) {
const GM_KEY = "fab_last_active_time";
const now = Date.now();
const lastActive = await GM_getValue(GM_KEY, 0);
if (lastActive > 0) {
const gap = now - lastActive;
if (gap > 15e3 && State.appStatus === "RATE_LIMITED" && State.rateLimitStartTime) {
if (gap > 12e4) {
const activeDuration = lastActive > State.rateLimitStartTime ? ((lastActive - State.rateLimitStartTime) / 1e3).toFixed(2) : "0.00";
Utils.logger(
"warn",
`[\u4F11\u7720\u68C0\u6D4B] \u68C0\u6D4B\u5230 ${(gap / 1e3).toFixed(0)}s \u4E0D\u6D3B\u52A8\u533A\u95F4\uFF08\u6D4F\u89C8\u5668\u5173\u95ED\u6216\u6807\u7B7E\u51BB\u7ED3\uFF09\uFF0C429 \u9650\u901F\u5DF2\u81EA\u7136\u6062\u590D\uFF0C\u6709\u6548\u9650\u901F\u65F6\u957F\u7EA6 ${activeDuration}s\uFF0C\u81EA\u52A8\u89E3\u9664\u9650\u901F\u72B6\u6001\u3002`
);
const logEntry = {
type: "RATE_LIMITED",
duration: parseFloat(activeDuration),
endTime: new Date(lastActive).toISOString(),
source: "\u4F11\u7720/\u5173\u95ED\u540E\u81EA\u52A8\u6062\u590D"
};
await this.addToHistory(logEntry);
State.appStatus = "NORMAL";
State.rateLimitStartTime = null;
State.normalStartTime = now;
State.consecutiveSuccessCount = 0;
await GM_deleteValue(Config.DB_KEYS.APP_STATUS);
if (UI3) {
UI3.updateDebugTab();
UI3.update();
}
} else {
const oldStart = State.rateLimitStartTime;
State.rateLimitStartTime += gap;
Utils.logger(
"debug",
`[\u4F11\u7720\u68C0\u6D4B] \u68C0\u6D4B\u5230 ${(gap / 1e3).toFixed(0)}s \u4E0D\u6D3B\u52A8\u533A\u95F4\uFF0C\u9650\u901F\u5F00\u59CB\u65F6\u95F4\u7531 ${new Date(oldStart).toLocaleTimeString()} \u987A\u79FB\u81F3 ${new Date(State.rateLimitStartTime).toLocaleTimeString()}\u3002`
);
const persistedStatus = await GM_getValue(Config.DB_KEYS.APP_STATUS);
if (persistedStatus) {
persistedStatus.startTime = State.rateLimitStartTime;
await GM_setValue(Config.DB_KEYS.APP_STATUS, persistedStatus);
}
}
} else if (gap > 15e3 && State.appStatus === "NORMAL" && State.normalStartTime) {
State.normalStartTime += gap;
Utils.logger(
"debug",
`[\u4F11\u7720\u68C0\u6D4B] \u6B63\u5E38\u72B6\u6001\u4E0B\u68C0\u6D4B\u5230 ${(gap / 1e3).toFixed(0)}s \u4E0D\u6D3B\u52A8\u533A\u95F4\uFF0C\u6B63\u5E38\u8FD0\u884C\u8BA1\u65F6\u5DF2\u987A\u79FB\u3002`
);
}
}
this._lastActiveTime = now;
await GM_setValue(GM_KEY, now);
}, "checkInactivity"),
/** 启动周期性活跃心跳(每 2 秒更新一次时间戳,用于检测不活动区间)。幂等。 */
startActivityTracking: /* @__PURE__ */ __name(function() {
if (this._trackingInterval) return;
this._trackingInterval = setInterval(async () => {
const now = Date.now();
this._lastActiveTime = now;
try {
await GM_setValue("fab_last_active_time", now);
} catch (e) {
}
}, 2e3);
Utils.logger("debug", "[\u6D3B\u8DC3\u5FC3\u8DF3] \u5DF2\u542F\u52A8\uFF0C\u6BCF 2s \u66F4\u65B0\u6D3B\u8DC3\u65F6\u95F4\u6233\u3002");
}, "startActivityTracking"),
/** 脚本启动时调用:加载持久化限速状态、执行启动时不活动检测、启动活跃心跳。 */
initStatus: /* @__PURE__ */ __name(async function() {
const persistedStatus = await GM_getValue(Config.DB_KEYS.APP_STATUS);
if (persistedStatus && persistedStatus.status === "RATE_LIMITED") {
State.appStatus = "RATE_LIMITED";
State.rateLimitStartTime = persistedStatus.startTime;
const rawDuration = persistedStatus.startTime ? ((Date.now() - persistedStatus.startTime) / 1e3).toFixed(2) : "0.00";
Utils.logger("warn", Utils.getText("startup_rate_limited", rawDuration, persistedStatus.source || Utils.getText("status_unknown_source")));
}
await this.checkInactivity(true);
this.startActivityTracking();
}, "initStatus")
};
// src/modules/page-patcher.js
var PagePatcher = {
_patchHasBeenApplied: false,
_lastSeenCursor: null,
_lastCheckedUrl: null,
_bodyObserver: null,
// State for request debouncing
_debounceXhrTimer: null,
_pendingXhr: null,
_lastDebounceXhrFire: 0,
async init() {
try {
const savedCursor = await GM_getValue(Config.DB_KEYS.LAST_CURSOR);
if (savedCursor) {
State.savedCursor = savedCursor;
this._lastSeenCursor = savedCursor;
Utils.logger("debug", `[Cursor] Initialized. Loaded saved cursor: ${savedCursor.substring(0, 30)}...`);
} else {
Utils.logger("debug", `[Cursor] Initialized. No saved cursor found.`);
}
if (typeof sessionStorage !== "undefined") {
const recoveryCursor = sessionStorage.getItem("fab_helper_recovery_cursor");
if (recoveryCursor) {
State.savedCursor = recoveryCursor;
this._lastSeenCursor = recoveryCursor;
State.isRecoveryMode = true;
Utils.logger("info", `[Recovery] \u68C0\u6D4B\u5230\u4E34\u65F6\u6062\u590D\u6E38\u6807\uFF0C\u5C06\u5728\u521D\u6B21\u52A0\u8F7D\u65F6\u5F3A\u5236\u6062\u590D\u4F4D\u7F6E`);
}
}
} catch (e) {
Utils.logger("warn", "[Cursor] Failed to restore cursor state:", e);
}
this.applyPatches();
Utils.logger("debug", "[Cursor] Network interceptors applied.");
this.setupSortMonitor();
},
// 添加监听URL变化的方法,检测排序方式和筛选条件变更
setupSortMonitor() {
this.checkUrlChanges();
if (typeof MutationObserver !== "undefined") {
const bodyObserver = new MutationObserver(() => {
if (window.location.href !== this._lastCheckedUrl) {
this._lastCheckedUrl = window.location.href;
this.checkUrlChanges();
Utils.detectLanguage();
}
});
bodyObserver.observe(document.body, {
childList: true,
subtree: true
});
this._bodyObserver = bodyObserver;
}
window.addEventListener("popstate", () => {
this.checkUrlChanges();
Utils.detectLanguage();
});
window.addEventListener("hashchange", () => {
this.checkUrlChanges();
Utils.detectLanguage();
});
this._lastCheckedUrl = window.location.href;
},
// 检查URL变化(排序、搜索词、分类等)并重置状态
checkUrlChanges() {
try {
const url = new URL(window.location.href);
const isSearchOrList = url.pathname.includes("/search") || url.pathname === "/" || url.pathname === "/zh-cn/" || url.pathname === "/en/" || url.pathname.includes("/sellers/");
if (!isSearchOrList) {
return;
}
const currentSort = url.searchParams.get("sort_by");
const currentQuery = url.searchParams.get("query") || url.searchParams.get("q");
const currentCategory = url.searchParams.get("category");
if (currentSort) {
let matchedOption = null;
if (State.sortOptions) {
for (const [key, option] of Object.entries(State.sortOptions)) {
if (option.value === currentSort) {
matchedOption = key;
break;
}
}
}
if (matchedOption && matchedOption !== State.currentSortOption) {
const previousSort = State.currentSortOption;
State.currentSortOption = matchedOption;
GM_setValue("fab_helper_sort_option", State.currentSortOption);
Utils.logger("debug", Utils.getText(
"log_url_sort_changed",
State.sortOptions?.[previousSort]?.name || previousSort,
State.sortOptions?.[State.currentSortOption]?.name || State.currentSortOption
));
this.clearSavedPosition("Sort change");
}
}
if (this._lastParams) {
if (this._lastParams.query !== currentQuery || this._lastParams.category !== currentCategory) {
this.clearSavedPosition("Search/Filter change");
}
}
this._lastParams = {
query: currentQuery,
category: currentCategory,
sort: currentSort
};
} catch (e) {
Utils.logger("warn", Utils.getText("log_sort_check_error", e.message));
}
},
// 辅助方法:清除位置
async clearSavedPosition(reason) {
if (State.savedCursor) {
State.savedCursor = null;
await GM_deleteValue(Config.DB_KEYS.LAST_CURSOR);
if (State.UI && State.UI.savedPositionDisplay) {
State.UI.savedPositionDisplay.textContent = Utils.getText("no_saved_position");
}
Utils.logger("info", `${Utils.getText("log_sort_changed_position_cleared")} (${reason})`);
}
this._patchHasBeenApplied = false;
this._lastSeenCursor = null;
},
async handleSearchResponse(request) {
if (request.status === 429) {
await RateLimitManager.enterRateLimitedState("\u641C\u7D22\u54CD\u5E94429");
} 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) {
if (typeof url !== "string" || !url.includes("/i/listings/search")) return false;
if (url.includes("aggregate_on=") || url.includes("count=0")) return false;
try {
const urlObj = new URL(url, window.location.origin);
if (urlObj.searchParams.has("cursor")) return false;
} catch (e) {
if (url.includes("cursor=")) return false;
}
return true;
},
shouldPatchUrl(url) {
if (typeof url !== "string") return false;
if (this._patchHasBeenApplied) 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;
if (State.isRecoveryMode && State.savedCursor) {
return true;
}
if (!State.rememberScrollPosition || !State.savedCursor) return false;
Utils.logger("debug", Utils.getText("page_patcher_match") + ` URL: ${url}`);
return true;
},
getPatchedUrl(originalUrl) {
if (State.savedCursor) {
const urlObj = new URL(originalUrl, window.location.origin);
urlObj.searchParams.set("cursor", State.savedCursor);
const modifiedUrl = urlObj.pathname + urlObj.search;
Utils.logger("debug", `[Cursor] ${Utils.getText("cursor_injecting")}: ${originalUrl}`);
Utils.logger("debug", `[Cursor] ${Utils.getText("cursor_patched_url")}: ${modifiedUrl}`);
this._patchHasBeenApplied = true;
if (State.isRecoveryMode) {
State.isRecoveryMode = false;
try {
sessionStorage.removeItem("fab_helper_recovery_cursor");
Utils.logger("debug", `[Recovery] \u4E34\u65F6\u6062\u590D\u6E38\u6807\u5DF2\u6D88\u8D39\u5E76\u6E05\u9664`);
} catch (e) {
}
}
if (State.UI && State.UI.savedPositionDisplay) {
const container = State.UI.savedPositionDisplay.parentElement;
if (container) {
const originalBg = container.style.backgroundColor;
const originalColor = container.style.color;
container.style.transition = "background-color 0.5s, color 0.5s";
container.style.backgroundColor = "rgba(52, 199, 89, 0.2)";
container.style.color = "var(--green)";
setTimeout(() => {
container.style.backgroundColor = originalBg;
container.style.color = originalColor;
}, 5e3);
}
}
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");
if (newCursor && newCursor !== this._lastSeenCursor) {
this._lastSeenCursor = newCursor;
State.savedCursor = newCursor;
GM_setValue(Config.DB_KEYS.LAST_CURSOR, newCursor);
if (State.debugMode) {
Utils.logger("debug", Utils.getText("debug_save_cursor", newCursor.substring(0, 30) + "..."));
}
if (State.UI && State.UI.savedPositionDisplay) {
State.UI.savedPositionDisplay.textContent = Utils.decodeCursor(newCursor);
}
}
} catch (e) {
Utils.logger("warn", Utils.getText("log_cursor_save_error"), e);
}
},
applyPatches() {
const self = this;
const originalXhrOpen = XMLHttpRequest.prototype.open;
const originalXhrSend = XMLHttpRequest.prototype.send;
const DEBOUNCE_DELAY_MS = 350;
const LEADING_EDGE_QUIET_MS = 1500;
const listenerAwareSend = /* @__PURE__ */ __name(function(...args) {
const request = this;
const onLoad = /* @__PURE__ */ __name(() => {
request.removeEventListener("load", onLoad);
if (typeof window.recordNetworkActivity === "function") {
window.recordNetworkActivity();
}
if (request.status >= 200 && request.status < 300 && request._url && self.isDebounceableSearch(request._url)) {
if (typeof window.recordNetworkRequest === "function") {
window.recordNetworkRequest(Utils.getText("request_source_xhr_item"), true);
}
}
if (request.status === 429 || request.status === "429" || request.status.toString() === "429") {
Utils.logger("warn", Utils.getText("xhr_detected_429", request.responseURL || request._url));
RateLimitManager.enterRateLimitedState(request.responseURL || request._url || "XHR\u54CD\u5E94429");
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", Utils.getText("log_xhr_rate_limit_detect", responseText));
RateLimitManager.enterRateLimitedState("XHR\u54CD\u5E94\u5185\u5BB9\u9650\u901F");
return;
}
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("detected_rate_limit_error", JSON.stringify(data)));
RateLimitManager.enterRateLimitedState("XHR\u54CD\u5E94\u9650\u901F\u9519\u8BEF");
return;
}
if (data.results && data.results.length === 0 && self.isDebounceableSearch(request._url)) {
const isEndOfList = data.next === null && data.previous !== null && data.cursors && data.cursors.next === null && data.cursors.previous !== null;
const isEmptySearch = data.next === null && data.previous === null && data.cursors && data.cursors.next === null && data.cursors.previous === null;
const urlObj = new URL(request._url, window.location.origin);
const params = urlObj.searchParams;
const nonFilteringParams = /* @__PURE__ */ new Set(["cursor", "count", "limit", "offset", "workerId"]);
const hasSpecialFilters = Array.from(params.keys()).some((key) => !nonFilteringParams.has(key));
if (isEndOfList) {
Utils.logger("info", Utils.getText("log_list_end_normal", JSON.stringify(data).substring(0, 200)));
RateLimitManager.recordSuccessfulRequest("XHR\u5217\u8868\u672B\u5C3E", true);
return;
} else if (isEmptySearch && hasSpecialFilters) {
Utils.logger("info", Utils.getText("log_empty_search_with_filters", JSON.stringify(data).substring(0, 200)));
RateLimitManager.recordSuccessfulRequest("XHR\u7A7A\u641C\u7D22\u7ED3\u679C", true);
return;
} else if (isEmptySearch && State.appStatus === "RATE_LIMITED") {
Utils.logger("info", Utils.getText("log_empty_search_already_limited", JSON.stringify(data).substring(0, 200)));
return;
} else if (isEmptySearch && document.readyState !== "complete") {
Utils.logger("info", Utils.getText("log_empty_search_page_loading", JSON.stringify(data).substring(0, 200)));
return;
} else if (isEmptySearch && Date.now() - (window.pageLoadTime || 0) < 5e3) {
Utils.logger("info", Utils.getText("empty_search_initial"));
return;
} else {
Utils.logger("warn", Utils.getText("detected_possible_rate_limit_empty", JSON.stringify(data).substring(0, 200)));
RateLimitManager.enterRateLimitedState("XHR\u54CD\u5E94\u7A7A\u7ED3\u679C");
return;
}
}
if (self.isDebounceableSearch(request._url) && data.results && data.results.length > 0) {
RateLimitManager.recordSuccessfulRequest(Utils.getText("request_source_xhr_search"), true);
}
} catch (jsonError) {
}
}
} catch (e) {
}
}
if (self.isDebounceableSearch(request._url)) {
self.handleSearchResponse(request);
}
}, "onLoad");
request.addEventListener("load", onLoad);
return originalXhrSend.apply(request, args);
}, "listenerAwareSend");
XMLHttpRequest.prototype.open = function(method, url, ...args) {
let modifiedUrl = url;
if (self.shouldPatchUrl(url)) {
modifiedUrl = self.getPatchedUrl(url);
this._isDebouncedSearch = false;
} else if (self.isDebounceableSearch(url)) {
self.saveLatestCursorFromUrl(url);
this._isDebouncedSearch = true;
} else {
self.saveLatestCursorFromUrl(url);
this._isDebouncedSearch = false;
}
this._url = modifiedUrl;
return originalXhrOpen.apply(this, [method, modifiedUrl, ...args]);
};
XMLHttpRequest.prototype.send = function(...args) {
if (!this._isDebouncedSearch) {
return listenerAwareSend.apply(this, args);
}
if (State.debugMode) {
Utils.logger("debug", Utils.getText("log_debounce_intercept", DEBOUNCE_DELAY_MS));
}
if (self._pendingXhr) {
self._pendingXhr.abort();
Utils.logger("info", Utils.getText("log_debounce_discard"));
}
clearTimeout(self._debounceXhrTimer);
self._pendingXhr = this;
const now = Date.now();
const sendArgs = args;
const sendNow = /* @__PURE__ */ __name(() => {
if (State.debugMode) {
Utils.logger("debug", Utils.getText("log_debounce_sending", self._pendingXhr && self._pendingXhr._url));
}
self._lastDebounceXhrFire = Date.now();
const xhr = self._pendingXhr;
self._pendingXhr = null;
listenerAwareSend.apply(xhr, sendArgs);
}, "sendNow");
if (now - self._lastDebounceXhrFire >= LEADING_EDGE_QUIET_MS) {
sendNow();
} else {
self._debounceXhrTimer = setTimeout(sendNow, 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);
}
return originalFetch.apply(this, [modifiedInput, init]).then((response) => {
if (typeof window.recordNetworkActivity === "function") {
window.recordNetworkActivity();
}
if (response.status >= 200 && response.status < 300 && typeof url === "string" && self.isDebounceableSearch(url)) {
if (typeof window.recordNetworkRequest === "function") {
window.recordNetworkRequest("Fetch\u5546\u54C1\u8BF7\u6C42", true);
}
}
if (response.status === 429 || response.status === "429" || response.status.toString() === "429") {
response.clone();
Utils.logger("warn", Utils.getText("log_fetch_detected_429", response.url));
RateLimitManager.enterRateLimitedState("Fetch\u54CD\u5E94429").catch(
(e) => Utils.logger("error", Utils.getText("log_handling_rate_limit_error", e.message))
);
}
if (response.status >= 200 && response.status < 300) {
response.clone().text().then((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", Utils.getText("log_fetch_rate_limit_detect", text.substring(0, 100)));
RateLimitManager.enterRateLimitedState("Fetch\u54CD\u5E94\u5185\u5BB9\u9650\u901F").catch(
(e) => Utils.logger("error", Utils.getText("log_handling_rate_limit_error", e.message))
);
return response;
}
try {
const data = JSON.parse(text);
if (data.detail && (data.detail.includes("Too many requests") || data.detail.includes("rate limit"))) {
Utils.logger("warn", Utils.getText("detected_rate_limit_error", "API\u9650\u901F\u54CD\u5E94"));
RateLimitManager.enterRateLimitedState("API\u9650\u901F\u54CD\u5E94").catch(
(e) => Utils.logger("error", Utils.getText("log_handling_rate_limit_error", e.message))
);
return response;
}
const responseUrl = response.url || "";
if (data.results && data.results.length === 0 && responseUrl.includes("/i/listings/search")) {
const isEndOfList = data.next === null && data.previous !== null && data.cursors && data.cursors.next === null && data.cursors.previous !== null;
const isEmptySearch = data.next === null && data.previous === null && data.cursors && data.cursors.next === null && data.cursors.previous === null;
const urlObj = new URL(responseUrl, window.location.origin);
const params = urlObj.searchParams;
const nonFilteringParams = /* @__PURE__ */ new Set(["cursor", "count", "limit", "offset", "workerId"]);
const hasSpecialFilters = Array.from(params.keys()).some((key) => !nonFilteringParams.has(key));
if (isEndOfList) {
Utils.logger("info", Utils.getText("log_fetch_list_end", JSON.stringify(data).substring(0, 200)));
RateLimitManager.recordSuccessfulRequest("Fetch\u5217\u8868\u672B\u5C3E", true);
} else if (isEmptySearch && hasSpecialFilters) {
Utils.logger("info", Utils.getText("log_fetch_empty_with_filters", JSON.stringify(data).substring(0, 200)));
RateLimitManager.recordSuccessfulRequest("Fetch\u7A7A\u641C\u7D22\u7ED3\u679C", true);
} else if (isEmptySearch && State.appStatus === "RATE_LIMITED") {
Utils.logger("info", Utils.getText("log_fetch_empty_already_limited", JSON.stringify(data).substring(0, 200)));
} else if (isEmptySearch && document.readyState !== "complete") {
Utils.logger("info", Utils.getText("log_fetch_empty_page_loading", JSON.stringify(data).substring(0, 200)));
} else if (isEmptySearch && Date.now() - (window.pageLoadTime || 0) < 5e3) {
Utils.logger("info", Utils.getText("empty_search_initial"));
} else {
Utils.logger("warn", Utils.getText("log_fetch_implicit_rate_limit", JSON.stringify(data).substring(0, 200)));
RateLimitManager.enterRateLimitedState("Fetch\u54CD\u5E94\u7A7A\u7ED3\u679C").catch(
(e) => Utils.logger("error", Utils.getText("log_handling_rate_limit_error", e.message))
);
}
}
} catch (jsonError) {
Utils.logger("debug", Utils.getText("log_json_parse_error", jsonError.message));
}
}).catch(() => {
});
}
return response;
});
};
}
};
// src/modules/instance-manager.js
var InstanceManager = {
isActive: false,
lastPingTime: 0,
pingInterval: null,
// 初始化实例管理
init: /* @__PURE__ */ __name(async function() {
try {
if (new URLSearchParams(window.location.search).has("workerId")) {
this.isActive = false;
return true;
}
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", Utils.getText("log_instance_activated", Config.INSTANCE_ID));
this.pingInterval = setInterval(() => this.ping(), 3e3);
return true;
}
const activeInstance = await GM_getValue("fab_active_instance", null);
const currentTime = Date.now();
if (activeInstance && currentTime - activeInstance.lastPing < 1e4) {
Utils.logger("info", Utils.getText("log_instance_collaborating", activeInstance.id));
this.isActive = false;
return true;
} else {
this.isActive = true;
await this.registerAsActive();
Utils.logger("info", Utils.getText("log_instance_no_active", Config.INSTANCE_ID));
this.pingInterval = setInterval(() => this.ping(), 3e3);
return true;
}
} catch (error) {
Utils.logger("error", Utils.getText("log_instance_init_failed", error.message));
this.isActive = true;
return true;
}
}, "init"),
// 注册为活跃实例
registerAsActive: /* @__PURE__ */ __name(async function() {
await GM_setValue("fab_active_instance", {
id: Config.INSTANCE_ID,
lastPing: Date.now()
});
}, "registerAsActive"),
// 定期更新活跃状态
ping: /* @__PURE__ */ __name(async function() {
if (!this.isActive) return;
this.lastPingTime = Date.now();
await this.registerAsActive();
}, "ping"),
// 检查是否可以接管
checkTakeover: /* @__PURE__ */ __name(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 > 1e4) {
this.isActive = true;
await this.registerAsActive();
Utils.logger("info", Utils.getText("log_instance_takeover", Config.INSTANCE_ID));
this.pingInterval = setInterval(() => this.ping(), 3e3);
location.reload();
} else {
setTimeout(() => this.checkTakeover(), 5e3);
}
} catch (error) {
Utils.logger("error", Utils.getText("log_instance_takeover_failed", error.message));
setTimeout(() => this.checkTakeover(), 5e3);
}
}, "checkTakeover"),
// 清理实例
cleanup: /* @__PURE__ */ __name(function() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
}, "cleanup"),
// 手动强制激活(用于用户手动开始任务时)
activate: /* @__PURE__ */ __name(async function() {
if (this.isActive) return;
this.isActive = true;
await this.registerAsActive();
Utils.logger("info", Utils.getText("log_instance_activated", Config.INSTANCE_ID));
if (!this.pingInterval) {
this.pingInterval = setInterval(() => this.ping(), 3e3);
}
}, "activate")
};
// src/modules/keepalive.js
var KeepAlive = {
_worker: null,
_workerUrl: null,
_tickCb: null,
_running: false,
_lastTickAt: null,
// WebRTC freeze-guard
_pc1: null,
_pc2: null,
_dc: null,
/** 注入心跳要执行的调度回调(由 index.js 提供,内含 ping/派发/watchdog) */
setTick(cb) {
this._tickCb = cb;
},
/** 启动后台保活(心跳 + 可选的防冻结)。幂等。 */
start() {
if (this._running) return;
this._running = true;
this._startHeartbeat();
if (Config.ENABLE_FREEZE_GUARD) {
this._startFreezeGuard();
}
},
/** 停止后台保活,释放 Worker 与 RTC 资源。幂等。 */
stop() {
if (!this._running) return;
this._running = false;
this._stopHeartbeat();
this._stopFreezeGuard();
},
_startHeartbeat() {
if (this._worker) return;
try {
const intervalMs = Config.KEEPALIVE_TICK_MS || 2e3;
const workerSrc = "let n=0;setInterval(function(){postMessage(++n);}," + intervalMs + ");";
const blob = new Blob([workerSrc], { type: "application/javascript" });
this._workerUrl = URL.createObjectURL(blob);
this._worker = new Worker(this._workerUrl);
this._worker.onmessage = () => {
this._lastTickAt = Date.now();
try {
const r = this._tickCb && this._tickCb();
if (r && typeof r.catch === "function") {
r.catch((e) => console.error("[Fab Helper] keepalive tick error:", e));
}
} catch (e) {
console.error("[Fab Helper] keepalive tick error:", e);
}
};
Utils.logger("debug", Utils.getText("log_keepalive_on"));
} catch (e) {
Utils.logger("warn", Utils.getText("log_keepalive_failed", e.message));
}
},
_stopHeartbeat() {
if (this._worker) {
try {
this._worker.terminate();
} catch (e) {
}
this._worker = null;
}
if (this._workerUrl) {
try {
URL.revokeObjectURL(this._workerUrl);
} catch (e) {
}
this._workerUrl = null;
}
},
async _startFreezeGuard() {
if (this._pc1 || typeof RTCPeerConnection === "undefined") return;
try {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
this._pc1 = pc1;
this._pc2 = pc2;
pc1.onicecandidate = (e) => {
if (e.candidate) pc2.addIceCandidate(e.candidate).catch(() => {
});
};
pc2.onicecandidate = (e) => {
if (e.candidate) pc1.addIceCandidate(e.candidate).catch(() => {
});
};
this._dc = pc1.createDataChannel("fab-keepalive");
const offer = await pc1.createOffer();
await pc1.setLocalDescription(offer);
await pc2.setRemoteDescription(offer);
const answer = await pc2.createAnswer();
await pc2.setLocalDescription(answer);
await pc1.setRemoteDescription(answer);
} catch (e) {
Utils.logger("warn", Utils.getText("log_keepalive_failed", e.message));
this._stopFreezeGuard();
}
},
_stopFreezeGuard() {
try {
if (this._dc) this._dc.close();
} catch (e) {
}
try {
if (this._pc1) this._pc1.close();
} catch (e) {
}
try {
if (this._pc2) this._pc2.close();
} catch (e) {
}
this._dc = null;
this._pc1 = null;
this._pc2 = null;
},
/** 由心跳驱动,偶尔通过 data channel 发一下保持活跃 */
poke() {
try {
if (this._dc && this._dc.readyState === "open") this._dc.send("p");
} catch (e) {
}
},
/** 返回保活运行状态,供 UI 展示、用于实测验证(尤其判断 Worker 是否被 CSP 拦) */
getStatus() {
return {
running: this._running,
heartbeatAlive: !!this._worker,
lastTickAgoMs: this._lastTickAt ? Date.now() - this._lastTickAt : null,
freezeGuard: this._dc ? this._dc.readyState : this._pc1 ? "connecting" : "off"
};
}
};
// src/modules/task-runner.js
var UI4 = null;
var countdownRefresh2 = null;
function setUIReference3(uiModule) {
UI4 = uiModule;
}
__name(setUIReference3, "setUIReference");
function setDependencies2(deps = {}) {
countdownRefresh2 = deps.countdownRefresh || countdownRefresh2;
}
__name(setDependencies2, "setDependencies");
var TaskRunner2 = {
findFreeLicenseOption: /* @__PURE__ */ __name((root) => {
if (!root || typeof root.querySelectorAll !== "function") {
return null;
}
const candidates = Array.from(root.querySelectorAll("span, div")).map((el) => {
const text = Utils.normalizeWhitespace(el.textContent || "");
const clickTarget = el.closest('[role="option"], button, label, input[type="radio"]');
return { text, clickTarget };
}).filter((candidate) => candidate.text && candidate.clickTarget);
const hasExplicitFreeSignal = /* @__PURE__ */ __name((text) => {
const cleanText = text.replace(/royalty\s*-?\s*free/gi, "");
return [...Config.FREE_TEXT_SET].some((freeWord) => cleanText.includes(freeWord));
}, "hasExplicitFreeSignal");
const explicitFree = candidates.find((candidate) => hasExplicitFreeSignal(candidate.text));
if (explicitFree) {
return explicitFree.clickTarget;
}
const personalFree = candidates.find((candidate) => {
const isPersonal = candidate.text.includes("\u4E2A\u4EBA") || candidate.text.includes("Personal");
return isPersonal && hasExplicitFreeSignal(candidate.text);
});
return personalFree ? personalFree.clickTarget : null;
}, "findFreeLicenseOption"),
getExternalProductState: /* @__PURE__ */ __name((root = document) => {
if (!root || typeof root.querySelectorAll !== "function") {
return { handled: false };
}
const currentHref = typeof window !== "undefined" && window.location?.href ? window.location.href : "https://www.fab.com/";
const currentHostname = typeof window !== "undefined" && window.location?.hostname ? window.location.hostname : "www.fab.com";
const links = [...root.querySelectorAll("a[href]")];
const externalLink = links.find((link) => {
const text2 = Utils.normalizeWhitespace(link.textContent || "");
if (!text2 || ![...Config.EXTERNAL_CTA_TEXT_SET].some((label) => text2.includes(label))) {
return false;
}
const rect = typeof link.getBoundingClientRect === "function" ? link.getBoundingClientRect() : { width: 1, height: 1 };
if (rect.width === 0 || rect.height === 0) {
return false;
}
try {
const href = link.href || link.getAttribute?.("href");
if (!href) return false;
const targetUrl = new URL(href, currentHref);
return targetUrl.hostname !== currentHostname;
} catch (error) {
return false;
}
});
if (!externalLink) {
return { handled: false };
}
const text = Utils.normalizeWhitespace(externalLink.textContent || "");
return {
handled: true,
reason: `External CTA "${text}"`,
href: externalLink.href || externalLink.getAttribute?.("href") || ""
};
}, "getExternalProductState"),
hasSavedLibraryText: /* @__PURE__ */ __name((card) => {
const cardText = Utils.normalizeWhitespace(card.textContent || "");
return [...Config.SAVED_TEXT_SET].some((savedText) => cardText.includes(savedText));
}, "hasSavedLibraryText"),
hasPositivePriceText: /* @__PURE__ */ __name((text) => {
const priceMatches = Utils.normalizeWhitespace(text || "").match(/\$\s*(\d+(?:\.\d{2})?)/g);
if (!priceMatches) return false;
return priceMatches.some((priceStr) => {
const numValue = parseFloat(priceStr.replace(/[^0-9.]/g, ""));
return numValue > 0;
});
}, "hasPositivePriceText"),
isCardSettled: /* @__PURE__ */ __name((card) => {
const link = card.querySelector(Config.SELECTORS.cardLink);
const url = link ? link.href.split("?")[0] : null;
return card.querySelector(`${Config.SELECTORS.freeStatus}, ${Config.SELECTORS.ownedStatus}`) !== null || TaskRunner2.hasSavedLibraryText(card) || TaskRunner2.isFreeCard(card) || TaskRunner2.hasPositivePriceText(card.textContent || "") || url && (Database.isDone(url) || Database.isFailed(url) || State.sessionCompleted.has(Database.normalizeListingUrl(url)));
}, "isCardSettled"),
// Check if a card is finished (owned, done, or failed)
isCardFinished: /* @__PURE__ */ __name((card) => {
const link = card.querySelector(Config.SELECTORS.cardLink);
const url = link ? link.href.split("?")[0] : null;
const hasSavedText = TaskRunner2.hasSavedLibraryText(card);
if (!link) {
const icons = card.querySelectorAll("i.fabkit-Icon--intent-success, i.edsicon-check-circle-filled");
if (icons.length > 0) return true;
return hasSavedText;
}
const uidMatch = link.href.match(/listings\/([a-f0-9-]+)/);
if (!uidMatch || !uidMatch[1]) return false;
const uid = uidMatch[1];
if (DataCache.ownedStatus.has(uid)) {
const status = DataCache.ownedStatus.get(uid);
if (status && status.acquired) return true;
}
if (card.querySelector(Config.SELECTORS.ownedStatus) !== null || hasSavedText) {
if (uid) {
DataCache.saveOwnedStatus([{
uid,
acquired: true,
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
}]);
}
return true;
}
if (url) {
if (Database.isDone(url)) return true;
if (Database.isFailed(url)) return true;
if (State.sessionCompleted.has(Database.normalizeListingUrl(url))) return true;
}
return false;
}, "isCardFinished"),
// Check if a card represents a free item
isFreeCard: /* @__PURE__ */ __name((card) => {
const rawText = card.textContent || "";
const cardText = Utils.normalizeWhitespace(rawText);
const cleanText = cardText.replace(/royalty\s*-?\s*free/gi, "");
const hasFreeKeyword = [...Config.FREE_TEXT_SET].some((freeWord) => cleanText.includes(freeWord));
const has100PercentDiscount = /-\s*100\s*%\s*(?:OFF|折扣)?/i.test(cleanText);
const hasPositivePrice = TaskRunner2.hasPositivePriceText(cardText);
if (hasPositivePrice) {
if (!has100PercentDiscount && !hasFreeKeyword) {
return false;
}
}
return hasFreeKeyword || has100PercentDiscount;
}, "isFreeCard"),
// Check if a card is a discounted paid item
isDiscountedPaidCard: /* @__PURE__ */ __name((card) => {
if (TaskRunner2.isFreeCard(card)) return false;
const rawText = card.textContent || "";
const cardText = Utils.normalizeWhitespace(rawText);
const hasDiscountTag = /-\d+%/.test(cardText) || cardText.includes("% off") || cardText.includes("% Off");
if (!hasDiscountTag) return false;
return TaskRunner2.hasPositivePriceText(cardText);
}, "isDiscountedPaidCard"),
// Toggle execution state
toggleExecution: /* @__PURE__ */ __name(async () => {
if (!Utils.checkAuthentication()) return;
if (State.isExecuting) {
State.isExecuting = false;
Database.saveExecutingState();
State.runningWorkers = {};
State.activeWorkers = 0;
State.executionTotalTasks = 0;
State.executionCompletedTasks = 0;
State.executionFailedTasks = 0;
Utils.logger("info", Utils.getText("log_execution_stopped"));
if (UI4) UI4.update();
return;
}
const sessionOk = await Utils.verifyServerSession();
if (!sessionOk) {
Utils.notifyAuthFailure();
return;
}
if (State.autoAddOnScroll) {
Utils.logger("info", Utils.getText("log_auto_add_enabled"));
Utils.logger("debug", "\u542F\u52A8\u4EFB\u52A1\u524D\u6B63\u5728\u786E\u8BA4\u5F53\u524D\u9875\u9762\u5546\u54C1\u8BC6\u522B\u72B6\u6001...");
TaskRunner2.checkVisibleCardsStatus().then(() => {
Utils.logger("debug", "\u6B63\u5728\u626B\u63CF\u5F53\u524D\u9875\u9762\u7B26\u5408\u6761\u4EF6\u7684\u5546\u54C1...");
TaskRunner2.scanAndAddTasks(document.querySelectorAll(TaskRunner2.getVisibleCardSelector())).then(() => {
TaskRunner2.startExecution();
});
});
return;
}
State.db.todo = [];
Utils.logger("info", Utils.getText("log_todo_cleared"));
Utils.logger("debug", Utils.getText("log_scanning_items"));
const cards = document.querySelectorAll(TaskRunner2.getVisibleCardSelector());
const newlyAddedList = [];
let alreadyInQueueCount = 0;
let ownedCount = 0;
let skippedCount = 0;
cards.forEach((card) => {
if (TaskRunner2.isCardHidden(card)) return;
if (!TaskRunner2.isCardSettled(card)) {
skippedCount++;
return;
}
if (TaskRunner2.isCardFinished(card)) {
ownedCount++;
return;
}
const link = card.querySelector(Config.SELECTORS.cardLink);
const url = link ? link.href.split("?")[0] : null;
if (!url) return;
if (Database.isTodo(url)) {
alreadyInQueueCount++;
return;
}
if (!TaskRunner2.isFreeCard(card)) return;
const name = card.querySelector('a[aria-label*="\u521B\u4F5C\u7684"]')?.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("debug", Utils.getText("log_skipped_unsettled", skippedCount));
}
if (newlyAddedList.length > 0) {
State.db.todo.push(...newlyAddedList);
Utils.logger("info", Utils.getText("log_added_to_queue", newlyAddedList.length));
}
const actionableCount = State.db.todo.length;
if (actionableCount > 0) {
if (newlyAddedList.length === 0 && alreadyInQueueCount > 0) {
Utils.logger("info", Utils.getText("log_all_in_queue", alreadyInQueueCount));
}
TaskRunner2.checkVisibleCardsStatus().then(() => {
TaskRunner2.startExecution();
});
} else {
Utils.logger("info", Utils.getText("log_no_new_items", ownedCount, skippedCount));
if (UI4) UI4.update();
}
}, "toggleExecution"),
// Start execution without scanning
startExecution: /* @__PURE__ */ __name(() => {
if (State.isExecuting) {
const newTotal = State.db.todo.length;
if (newTotal > State.executionTotalTasks) {
Utils.logger("info", Utils.getText("log_new_tasks_added", newTotal));
State.executionTotalTasks = newTotal;
if (UI4) UI4.update();
} else {
Utils.logger("info", Utils.getText("log_executor_running"));
}
return;
}
if (State.db.todo.length === 0) {
Utils.logger("debug", Utils.getText("log_exec_no_tasks"));
return;
}
Utils.logger("info", Utils.getText("log_starting_execution", State.db.todo.length));
if (typeof InstanceManager !== "undefined" && InstanceManager.activate) {
InstanceManager.activate();
}
KeepAlive.start();
State.isExecuting = true;
Database.saveExecutingState();
State.executionTotalTasks = State.db.todo.length;
State.executionCompletedTasks = 0;
State.executionFailedTasks = 0;
State.autoScrollAttempts = 0;
if (UI4) UI4.update();
TaskRunner2.executeBatch();
}, "startExecution"),
// Toggle hide saved items
toggleHideSaved: /* @__PURE__ */ __name(async () => {
State.hideSaved = !State.hideSaved;
await Database.saveHidePref();
TaskRunner2.runHideOrShow();
if (!State.hideSaved) {
const { visible: actualVisibleCount } = TaskRunner2.getCardCounts(true);
Utils.logger("info", Utils.getText("log_display_mode_switched", actualVisibleCount));
}
if (UI4) UI4.update();
}, "toggleHideSaved"),
toggleAutoAdd: /* @__PURE__ */ __name(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")));
setTimeout(() => {
State.isTogglingSetting = false;
}, 200);
}, "toggleAutoAdd"),
toggleAutoResume: /* @__PURE__ */ __name(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);
}, "toggleAutoResume"),
toggleRememberPosition: /* @__PURE__ */ __name(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._patchHasBeenApplied = false;
PagePatcher._lastSeenCursor = null;
State.savedCursor = null;
Utils.logger("info", Utils.getText("log_position_cleared"));
if (State.UI && State.UI.savedPositionDisplay) {
State.UI.savedPositionDisplay.textContent = Utils.decodeCursor(null);
}
} else if (State.UI && State.UI.savedPositionDisplay) {
State.UI.savedPositionDisplay.textContent = Utils.decodeCursor(State.savedCursor);
}
setTimeout(() => {
State.isTogglingSetting = false;
}, 200);
}, "toggleRememberPosition"),
toggleAutoRefreshEmpty: /* @__PURE__ */ __name(async () => {
if (State.isTogglingSetting) return;
State.isTogglingSetting = true;
State.autoRefreshEmptyPage = !State.autoRefreshEmptyPage;
await Database.saveAutoRefreshEmptyPref();
Utils.logger("info", Utils.getText("log_auto_refresh_toggle", State.autoRefreshEmptyPage ? Utils.getText("status_enabled") : Utils.getText("status_disabled")));
setTimeout(() => {
State.isTogglingSetting = false;
}, 200);
}, "toggleAutoRefreshEmpty"),
toggleHideDiscountedPaid: /* @__PURE__ */ __name(async () => {
State.hideDiscountedPaid = !State.hideDiscountedPaid;
await Database.saveHideDiscountedPref();
TaskRunner2.runHideOrShow();
if (State.hideDiscountedPaid) {
Utils.logger("info", "\u5DF2\u5F00\u542F\u9690\u85CF\u6253\u6298\u4ED8\u8D39\u5546\u54C1");
} else {
Utils.logger("info", "\u5DF2\u5173\u95ED\u9690\u85CF\u6253\u6298\u4ED8\u8D39\u5546\u54C1");
}
if (UI4) UI4.update();
}, "toggleHideDiscountedPaid"),
toggleHidePaid: /* @__PURE__ */ __name(async () => {
State.hidePaid = !State.hidePaid;
await Database.saveHidePaidPref();
TaskRunner2.runHideOrShow();
if (State.hidePaid) {
Utils.logger("info", "\u5DF2\u5F00\u542F\u9690\u85CF\u4ED8\u8D39\u5546\u54C1");
} else {
Utils.logger("info", "\u5DF2\u5173\u95ED\u9690\u85CF\u4ED8\u8D39\u5546\u54C1");
}
if (UI4) UI4.update();
}, "toggleHidePaid"),
toggleBlockResources: /* @__PURE__ */ __name(async () => {
State.blockLargeResources = !State.blockLargeResources;
await Database.saveBlockResourcesPref();
if (State.blockLargeResources) {
Utils.logger("info", "\u5DF2\u5F00\u542F\u5DE5\u4F5C\u6807\u7B7E\u9875\u5927\u8D44\u6E90\u8FC7\u6EE4");
} else {
Utils.logger("info", "\u5DF2\u5173\u95ED\u5DE5\u4F5C\u6807\u7B7E\u9875\u5927\u8D44\u6E90\u8FC7\u6EE4");
}
if (UI4) UI4.update();
}, "toggleBlockResources"),
stop: /* @__PURE__ */ __name(() => {
if (!State.isExecuting) return;
State.isExecuting = false;
Database.saveExecutingState();
Database.saveTodo();
State.runningWorkers = {};
State.activeWorkers = 0;
State.executionTotalTasks = 0;
State.executionCompletedTasks = 0;
State.executionFailedTasks = 0;
Utils.logger("info", Utils.getText("log_execution_stopped"));
if (UI4) UI4.update();
}, "stop"),
runRecoveryProbe: /* @__PURE__ */ __name(async () => {
const randomDelay = Math.floor(Math.random() * (3e4 - 15e3 + 1) + 15e3);
Utils.logger("info", Utils.getText("log_recovery_probe", (randomDelay / 1e3).toFixed(1)));
setTimeout(async () => {
Utils.logger("info", Utils.getText("log_probing_connection"));
try {
const csrfToken = Utils.getCookie("fab_csrftoken");
if (!csrfToken) {
Utils.checkAuthentication();
throw new Error("CSRF token not found for 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) {
await PagePatcher.handleSearchResponse({ status: 200 });
Utils.logger("info", Utils.getText("log_connection_restored"));
TaskRunner2.toggleExecution();
} else {
throw new Error(`Probe failed with unexpected status: ${probeResponse.status}`);
}
} catch (e) {
Utils.logger("error", Utils.getText("log_recovery_failed", e.message));
setTimeout(() => location.reload(), 2e3);
}
}, randomDelay);
}, "runRecoveryProbe"),
refreshVisibleStates: /* @__PURE__ */ __name(async () => {
const API_ENDPOINT = "https://www.fab.com/i/users/me/listings-states";
const API_CHUNK_SIZE = 24;
const isElementInViewport = /* @__PURE__ */ __name((el) => {
if (!el) return false;
const rect = el.getBoundingClientRect();
return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
}, "isElementInViewport");
try {
const csrfToken = Utils.getCookie("fab_csrftoken");
if (!csrfToken) {
Utils.checkAuthentication();
throw new Error("CSRF token not found. Are you logged in?");
}
const uidsFromVisibleCards = new Set([...document.querySelectorAll(TaskRunner2.getVisibleCardSelector())].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) && !Database.isTodo(url) && TaskRunner2.isFreeCard(card);
}).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));
const allUidsToCheck = Array.from(/* @__PURE__ */ new Set([...uidsFromVisibleCards, ...uidsFromFailedList]));
if (allUidsToCheck.length === 0) {
Utils.logger("info", Utils.getText("log_no_items_to_check"));
return;
}
Utils.logger("debug", Utils.getText("log_checking_items", uidsFromVisibleCards.size, uidsFromFailedList.size));
const ownedUids = /* @__PURE__ */ 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("debug", Utils.getText("log_processing_batch", 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", Utils.getText("log_batch_failed", response.status));
continue;
}
const rawData = await response.json();
const data = API.extractStateData(rawData, "RefreshStates");
if (!data || !Array.isArray(data)) {
Utils.logger("warn", Utils.getText("log_unexpected_data_format"));
continue;
}
data.filter((item) => item.acquired).forEach((item) => ownedUids.add(item.uid));
if (allUidsToCheck.length > i + API_CHUNK_SIZE) {
await new Promise((r) => setTimeout(r, 250));
}
}
Utils.logger("debug", Utils.getText("fab_dom_api_complete", ownedUids.size));
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}`;
Database.addDoneUrl(url);
});
Utils.logger("info", Utils.getText("log_cleared_from_failed", initialFailedCount - State.db.failed.length));
}
}
if (dbUpdated) {
await Database.saveFailed();
await Database.saveDone();
}
TaskRunner2.runHideOrShow();
} catch (e) {
Utils.logger("error", Utils.getText("log_refresh_error"), e);
}
}, "refreshVisibleStates"),
retryFailedTasks: /* @__PURE__ */ __name(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);
State.db.failed = [];
await Database.saveFailed();
Utils.logger("info", Utils.getText("log_tasks_moved", count));
if (UI4) UI4.update();
}, "retryFailedTasks"),
// 检查并清理超时(卡死)的 worker。被 watchdog 定时器与后台心跳(KeepAlive)共同调用。
// 抽成独立方法,是为了让主线程定时器被后台节流时,心跳也能驱动这套清理。
// 返回被清理的 worker 数量。
checkStalledWorkers: /* @__PURE__ */ __name(async () => {
if (!State.isExecuting) return 0;
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) continue;
if (workerInfo.instanceId !== Config.INSTANCE_ID) continue;
if (now - workerInfo.startTime > STALL_TIMEOUT) {
stalledWorkers.push({ workerId, task: workerInfo.task });
}
}
if (stalledWorkers.length === 0) return 0;
Utils.logger("warn", Utils.getText("log_stalled_workers", stalledWorkers.length));
for (const stalledWorker of stalledWorkers) {
const { workerId, task } = stalledWorker;
const workerInfo = State.runningWorkers[workerId];
const stallDuration = workerInfo ? ((Date.now() - workerInfo.startTime) / 1e3).toFixed(2) : "\u672A\u77E5";
Utils.logger("error", Utils.getText("log_watchdog_stalled", workerId.substring(0, 12)));
const _failRes = await Database.markAsFailed(task, {
reason: "\u5DE5\u4F5C\u7EBF\u7A0B\u8D85\u65F6 (Watchdog)",
logs: [`Worker ${workerId.substring(0, 12)} \u8D85\u65F6`, `\u8D85\u65F6\u65F6\u957F: ${stallDuration}s`],
details: {
workerId,
stallDuration: `${stallDuration}s`,
timeout: `${Config.WORKER_TIMEOUT / 1e3}s`
}
});
if (!_failRes || !_failRes.retried) State.executionFailedTasks++;
delete State.runningWorkers[workerId];
State.activeWorkers = Math.max(0, State.activeWorkers - 1);
await GM_deleteValue(workerId);
}
Utils.logger("info", Utils.getText("log_cleaned_workers", stalledWorkers.length, State.activeWorkers));
if (UI4) UI4.update();
return stalledWorkers.length;
}, "checkStalledWorkers"),
runWatchdog: /* @__PURE__ */ __name(() => {
if (State.watchdogTimer) clearInterval(State.watchdogTimer);
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 cleaned = await TaskRunner2.checkStalledWorkers();
if (cleaned > 0) {
setTimeout(() => {
if (State.isExecuting && State.activeWorkers < Config.MAX_CONCURRENT_WORKERS && State.db.todo.length > 0) {
TaskRunner2.executeBatch();
}
}, 2e3);
}
}, 5e3);
}, "runWatchdog"),
executeBatch: /* @__PURE__ */ __name(async () => {
if (!Utils.checkAuthentication()) return;
if (!State.isWorkerTab && !InstanceManager.isActive) {
Utils.logger("warn", Utils.getText("log_not_active_instance"));
return;
}
if (!State.isExecuting) return;
if (State.isDispatchingTasks) {
Utils.logger("debug", "Task dispatching already in progress, skipping executeBatch.");
return;
}
State.isDispatchingTasks = true;
try {
if (State.db.todo.length === 0 && State.activeWorkers === 0) {
if (State.autoAddOnScroll) {
State.isDispatchingTasks = false;
TaskRunner2.attemptAutoScroll();
return;
}
await TaskRunner2.stopExecutionAndSettle();
State.isDispatchingTasks = false;
return;
}
if (State.appStatus === "RATE_LIMITED") {
Utils.logger("info", Utils.getText("log_rate_limited_continue"));
}
if (State.activeWorkers >= Config.MAX_CONCURRENT_WORKERS) {
Utils.logger("info", Utils.getText("log_max_workers_reached", Config.MAX_CONCURRENT_WORKERS));
State.isDispatchingTasks = false;
return;
}
const inFlightUIDs = new Set(Object.values(State.runningWorkers).map((w) => w.task.uid));
const todoList = [...State.db.todo];
let dispatchedCount = 0;
const dispatchedUIDs = /* @__PURE__ */ new Set();
const slotsAvailable = Config.MAX_CONCURRENT_WORKERS - State.activeWorkers;
const tasksToDispatch = [];
for (const task of todoList) {
if (tasksToDispatch.length >= slotsAvailable) break;
if (inFlightUIDs.has(task.uid) || dispatchedUIDs.has(task.uid)) {
Utils.logger("debug", Utils.getText("log_task_already_running", task.name));
continue;
}
if (Database.isDone(task.url)) {
Utils.logger("debug", Utils.getText("log_task_already_done", task.name));
State.db.todo = State.db.todo.filter((t) => t.uid !== task.uid);
Database.saveTodo();
continue;
}
tasksToDispatch.push(task);
}
for (const task of tasksToDispatch) {
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
};
Utils.logger("debug", Utils.getText("log_dispatching_worker", workerId.substring(0, 12), task.name));
const workerUrl = new URL(task.url);
workerUrl.searchParams.set("workerId", workerId);
await GM_setValue(workerId, {
task,
instanceId: Config.INSTANCE_ID
});
GM_openInTab(workerUrl.href, { active: false, insert: true });
}
if (dispatchedCount > 0) {
Utils.logger("debug", Utils.getText("log_batch_dispatched", dispatchedCount));
}
if (!State.watchdogTimer && State.activeWorkers > 0) {
TaskRunner2.runWatchdog();
}
if (UI4) UI4.update();
} finally {
State.isDispatchingTasks = false;
}
}, "executeBatch"),
closeAllWorkerTabs: /* @__PURE__ */ __name(() => {
const workerIds = Object.keys(State.runningWorkers);
if (workerIds.length > 0) {
Utils.logger("debug", Utils.getText("log_cleaning_workers_state", workerIds.length));
for (const workerId of workerIds) {
GM_deleteValue(workerId);
}
State.runningWorkers = {};
State.activeWorkers = 0;
Utils.logger("info", Utils.getText("log_workers_cleaned"));
}
}, "closeAllWorkerTabs"),
processDetailPage: /* @__PURE__ */ __name(async () => {
if (!Utils.checkAuthentication()) return;
const urlParams = new URLSearchParams(window.location.search);
const workerId = urlParams.get("workerId");
if (!workerId) return;
State.isWorkerTab = true;
State.workerTaskId = workerId;
const startTime = Date.now();
let hasReported = false;
let closeAttempted = false;
let payload = null;
const forceCloseTimer = setTimeout(() => {
if (!closeAttempted) {
console.log("\u5DE5\u4F5C\u6807\u7B7E\u9875\u8D85\u65F6\uFF0C\u4E3B\u52A8\u4E0A\u62A5\u5E76\u5173\u95ED");
closeWorkerTab();
}
}, Math.max(1e4, Config.WORKER_TIMEOUT - 5e3));
function closeWorkerTab() {
closeAttempted = true;
clearTimeout(forceCloseTimer);
if (!hasReported && workerId) {
try {
GM_setValue(Config.DB_KEYS.WORKER_DONE, {
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", Utils.getText("log_close_worker_failed", error.message));
try {
window.location.href = "about:blank";
} catch (e) {
}
}
}
__name(closeWorkerTab, "closeWorkerTab");
try {
payload = await GM_getValue(workerId);
if (!payload || !payload.task) {
Utils.logger("info", Utils.getText("log_task_data_cleaned"));
closeWorkerTab();
return;
}
const activeInstance = await GM_getValue("fab_active_instance", null);
if (activeInstance && activeInstance.id !== payload.instanceId) {
Utils.logger("warn", Utils.getText("log_instance_mismatch", 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 {
const waitForPageReady = /* @__PURE__ */ __name(async () => {
const maxWait = 15e3;
const startTime2 = Date.now();
let lastState = "";
while (Date.now() - startTime2 < maxWait) {
const currentState = document.readyState;
const hasMainContent = document.querySelector('main, .product-detail, [class*="listing"], [class*="detail"]');
const hasButtons = document.querySelectorAll("button").length > 0;
const hasTitle = document.querySelector("h1, .fabkit-Heading--xl");
if (currentState !== lastState) {
logBuffer.push(`\u9875\u9762\u72B6\u6001: ${currentState}`);
lastState = currentState;
}
const isReadyState = currentState === "interactive" || currentState === "complete";
if (isReadyState && hasMainContent && (hasButtons || hasTitle)) {
logBuffer.push(`\u9875\u9762\u5C31\u7EEA\u68C0\u6D4B\u901A\u8FC7: readyState=${currentState}, hasContent=true`);
return true;
}
await new Promise((r) => setTimeout(r, 100));
}
logBuffer.push(`\u9875\u9762\u5C31\u7EEA\u68C0\u6D4B\u8D85\u65F6 (${maxWait}ms)\uFF0C\u7EE7\u7EED\u5C1D\u8BD5\u64CD\u4F5C`);
return false;
}, "waitForPageReady");
const pageReady = await waitForPageReady();
if (!pageReady) {
logBuffer.push(`\u26A0\uFE0F \u8B66\u544A: \u9875\u9762\u53EF\u80FD\u672A\u5B8C\u5168\u52A0\u8F7D\uFF0C\u8FD9\u53EF\u80FD\u5BFC\u81F4\u64CD\u4F5C\u5931\u8D25`);
}
const is404Page = (() => {
const bodyText = document.body ? document.body.textContent : "";
const title = document.title || "";
const h1 = document.querySelector("h1");
const h1Text = h1 ? h1.textContent : "";
const NOT_FOUND_PHRASES = [
"Sorry, we couldn't find that page",
"\u62B1\u6B49\uFF0C\u627E\u4E0D\u5230\u8BE5\u9875\u9762",
"\u627E\u4E0D\u5230\u8BE5\u9875\u9762",
"Page not found",
"404"
];
return NOT_FOUND_PHRASES.some(
(phrase) => bodyText.includes(phrase) || title.includes(phrase) || h1Text.includes(phrase)
);
})();
if (is404Page) {
logBuffer.push(`\u26A0\uFE0F \u68C0\u6D4B\u5230 404 \u9875\u9762\uFF08\u5546\u54C1\u5DF2\u4E0B\u67B6\u6216\u4E0D\u5B58\u5728\uFF09\uFF0C\u6807\u8BB0\u4E3A\u5DF2\u8DF3\u8FC7\u5E76\u5173\u95ED\u3002`);
success = true;
hasReported = true;
GM_setValue(Config.DB_KEYS.WORKER_DONE, {
workerId,
success: true,
logs: [...logBuffer, "\u5546\u54C1\u4E0D\u5B58\u5728 (404)\uFF0C\u5DF2\u81EA\u52A8\u8DF3\u8FC7"],
task: currentTask,
instanceId: payload.instanceId,
executionTime: Date.now() - startTime
});
closeWorkerTab();
return;
}
await (/* @__PURE__ */ __name(function waitForKeyElement(maxWait = 2e3) {
const matchKey = /* @__PURE__ */ __name(() => {
const buttons = document.querySelectorAll("button");
for (const btn of buttons) {
const t = Utils.normalizeWhitespace(btn.textContent || "");
if (!t) continue;
if (Config.ACQUISITION_TEXT_SET.has(t)) return true;
if (Config.SAVED_TEXT_SET.has(t)) return true;
if (Config.EXTERNAL_CTA_TEXT_SET.has(t)) return true;
}
const bodyText = document.body && document.body.textContent;
if (bodyText) {
for (const phrase of Config.SAVED_TEXT_SET) {
if (bodyText.includes(phrase)) return true;
}
}
return false;
}, "matchKey");
if (matchKey()) return Promise.resolve();
return new Promise((resolve) => {
let done = false;
const finish = /* @__PURE__ */ __name(() => {
if (done) return;
done = true;
try {
observer.disconnect();
} catch (e) {
}
clearTimeout(timer);
resolve();
}, "finish");
const observer = new MutationObserver(() => {
if (matchKey()) finish();
});
observer.observe(document.body, { childList: true, subtree: true });
const timer = setTimeout(finish, maxWait);
});
}, "waitForKeyElement"))();
const adultContentWarning = document.querySelector(".fabkit-Heading--xl");
if (adultContentWarning && (adultContentWarning.textContent.includes("\u6210\u4EBA\u5185\u5BB9") || adultContentWarning.textContent.includes("Adult Content") || adultContentWarning.textContent.includes("Mature Content"))) {
logBuffer.push(`\u68C0\u6D4B\u5230\u6210\u4EBA\u5185\u5BB9\u8B66\u544A\u5BF9\u8BDD\u6846\uFF0C\u81EA\u52A8\u70B9\u51FB"\u7EE7\u7EED"\u6309\u94AE...`);
const continueButton = [...document.querySelectorAll("button.fabkit-Button--primary")].find(
(btn) => btn.textContent.includes("\u7EE7\u7EED") || btn.textContent.includes("Continue")
);
if (continueButton) {
Utils.deepClick(continueButton);
logBuffer.push(`\u5DF2\u70B9\u51FB"\u7EE7\u7EED"\u6309\u94AE\uFF0C\u7B49\u5F85\u9875\u9762\u52A0\u8F7D...`);
await new Promise((resolve) => setTimeout(resolve, 2e3));
}
}
logBuffer.push(`=== \u9875\u9762\u72B6\u6001\u8BCA\u65AD\u5F00\u59CB ===`);
const diagnosticReport = PageDiagnostics.diagnoseDetailPage();
logBuffer.push(`\u9875\u9762\u6807\u9898: ${diagnosticReport.pageTitle}`);
logBuffer.push(`\u53EF\u89C1\u6309\u94AE\u6570\u91CF: ${diagnosticReport.buttons.filter((btn) => btn.isVisible).length}`);
logBuffer.push(`=== \u9875\u9762\u72B6\u6001\u8BCA\u65AD\u7ED3\u675F ===`);
try {
const csrfToken = Utils.getCookie("fab_csrftoken");
if (!csrfToken) throw new Error("CSRF token not found for API check.");
const statesUrl = new URL("https://www.fab.com/i/users/me/listings-states");
statesUrl.searchParams.append("listing_ids", currentTask.uid);
const response = await API.gmFetch({
method: "GET",
url: statesUrl.href,
headers: { "x-csrftoken": csrfToken, "x-requested-with": "XMLHttpRequest" }
});
let statesData;
try {
statesData = JSON.parse(response.responseText);
if (!Array.isArray(statesData)) {
statesData = API.extractStateData(statesData, "SingleItemCheck");
}
} catch (e) {
logBuffer.push(`\u89E3\u6790API\u54CD\u5E94\u5931\u8D25: ${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) {
const isItemOwned = /* @__PURE__ */ __name(() => {
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 };
}, "isItemOwned");
const initialState = isItemOwned();
if (initialState.owned) {
logBuffer.push(`Item already owned on page load (UI Fallback PASS: ${initialState.reason}).`);
success = true;
} else {
const externalState = TaskRunner2.getExternalProductState(document);
if (externalState.handled) {
logBuffer.push(`Detected non-purchasable external listing (${externalState.reason}). Marking task as handled.`);
success = true;
}
}
if (!success) {
const allVisibleButtons = [...document.querySelectorAll("button")].filter((btn) => {
const rect = btn.getBoundingClientRect();
const text = btn.textContent.trim();
return rect.width > 0 && rect.height > 0 && text.length > 0;
});
const criticalKeywords = [...Config.ACQUISITION_TEXT_SET, ...Config.FREE_TEXT_SET, "\u8BB8\u53EF", "License", "Select", "\u9009\u62E9", "Add", "\u6DFB\u52A0", "Library", "\u5E93"];
const criticalButtons = allVisibleButtons.filter((btn) => {
const text = btn.textContent;
return criticalKeywords.some((key) => text.includes(key));
});
logBuffer.push(`=== \u6309\u94AE\u68C0\u6D4B: \u53EF\u89C1=${allVisibleButtons.length}, \u5173\u952E=${criticalButtons.length} ===`);
if (criticalButtons.length > 0) {
criticalButtons.slice(0, 5).forEach((btn, i) => {
logBuffer.push(` \u5173\u952E\u6309\u94AE${i + 1}: "${btn.textContent.trim().substring(0, 40)}"`);
});
} else if (allVisibleButtons.length > 0) {
allVisibleButtons.slice(0, 3).forEach((btn, i) => {
logBuffer.push(` \u6309\u94AE${i + 1}: "${btn.textContent.trim().substring(0, 40)}"`);
});
}
const licenseButton = allVisibleButtons.find((btn) => {
const text = Utils.normalizeWhitespace(btn.textContent);
return text.includes("\u9009\u62E9\u8BB8\u53EF") || text.includes("Select license") || btn.getAttribute("aria-haspopup") === "true" && TaskRunner2.isFreeCard(btn);
});
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 clickableParent = TaskRunner2.findFreeLicenseOption(node);
if (clickableParent) {
logBuffer.push(`Found explicit free 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);
setTimeout(() => {
logBuffer.push(`Second attempt to click license button.`);
Utils.deepClick(licenseButton);
}, 1500);
setTimeout(() => {
observer.disconnect();
reject(new Error("Timeout (5s): The free/personal option did not appear."));
}, 5e3);
});
logBuffer.push(`License selected, waiting for UI update.`);
await new Promise((r) => setTimeout(r, 2e3));
if (isItemOwned().owned) {
logBuffer.push(`Item became owned after license selection.`);
success = true;
}
} catch (licenseError) {
logBuffer.push(`License selection failed: ${licenseError.message}`);
}
}
if (!success) {
const freshButtons = [...document.querySelectorAll("button")].filter((btn) => {
const rect = btn.getBoundingClientRect();
const text = btn.textContent.trim();
return rect.width > 0 && rect.height > 0 && text.length > 0;
});
const freshCritical = freshButtons.filter((btn) => {
const text = btn.textContent;
return criticalKeywords.some((key) => text.includes(key));
});
logBuffer.push(`=== \u91CD\u65B0\u68C0\u6D4B: \u53EF\u89C1=${freshButtons.length}, \u5173\u952E=${freshCritical.length} ===`);
if (freshCritical.length > 0) {
freshCritical.slice(0, 3).forEach((btn, i) => {
logBuffer.push(` \u5173\u952E\u6309\u94AE${i + 1}: "${btn.textContent.trim().substring(0, 40)}"`);
});
}
let actionButton = freshButtons.find((btn) => {
const text = Utils.normalizeWhitespace(btn.textContent).toLowerCase();
const isPopup = btn.getAttribute("aria-haspopup") === "true";
const matchesKeyword = [...Config.ACQUISITION_TEXT_SET].some(
(keyword) => text.includes(keyword.toLowerCase())
);
return !isPopup && matchesKeyword;
});
if (!actionButton) {
actionButton = freshButtons.find((btn) => {
const text = Utils.normalizeWhitespace(btn.textContent).toLowerCase();
return [...Config.ACQUISITION_TEXT_SET].some(
(keyword) => text.includes(keyword.toLowerCase())
);
});
}
if (!actionButton) {
actionButton = freshButtons.find((btn) => {
const text = Utils.normalizeWhitespace(btn.textContent);
const isPopup = btn.getAttribute("aria-haspopup") === "true";
const hasFreeText = [...Config.FREE_TEXT_SET].some((freeWord) => text.includes(freeWord));
const hasDiscount = /-\s*100\s*%\s*(?:OFF|折扣)?/i.test(text);
const hasPersonal = text.includes("\u4E2A\u4EBA") || text.includes("Personal");
return hasFreeText && hasDiscount && hasPersonal;
});
if (actionButton) {
logBuffer.push(`Found limited-time free license button: "${actionButton.textContent.trim().substring(0, 50)}"`);
}
}
if (!actionButton) {
actionButton = freshButtons.find((btn) => {
const text = btn.textContent.toLowerCase();
return text.includes("add") && text.includes("library") || text.includes("\u6DFB\u52A0") && text.includes("\u5E93");
});
if (actionButton) {
logBuffer.push(`\u901A\u8FC7\u5907\u7528\u65B9\u6848\u627E\u5230\u6309\u94AE: "${actionButton.textContent.trim().substring(0, 50)}"`);
}
}
if (actionButton) {
logBuffer.push(`Found add button [${actionButton.textContent.trim().substring(0, 30)}], clicking it.`);
Utils.deepClick(actionButton);
try {
await new Promise((resolve, reject) => {
const timeout = 6e4;
const startTime2 = Date.now();
const interval = setInterval(() => {
const currentState = isItemOwned();
if (currentState.owned) {
logBuffer.push(`Successfully owned (UI Match: ${currentState.reason})`);
success = true;
clearInterval(interval);
resolve();
return;
}
const allButtonsWithShadow = Utils.findAllButtonsWithShadow();
let checkoutBtn = allButtonsWithShadow.find(
(btn) => btn.classList.contains("payment-order-confirm__btn")
);
if (!checkoutBtn) {
checkoutBtn = allButtonsWithShadow.find((btn) => {
const rect = btn.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return false;
const text = Utils.normalizeWhitespace(btn.textContent).toLowerCase();
if (text.includes("buy now") || text.includes("\u7ACB\u5373\u8D2D\u4E70")) return false;
const isCheckoutContext = btn.ownerDocument !== document || window.location.pathname.includes("/payment/");
if (isCheckoutContext) {
if (text.includes("add to library") || text.includes("\u6DFB\u52A0\u5230\u5E93") || text.includes("add to account") || text.includes("\u6DFB\u52A0\u5230\u8D26\u6237")) {
return true;
}
}
return text.includes("place order") || text.includes("\u4E0B\u5355") || text.includes("checkout") || text.includes("\u7ED3\u8D26") || text.includes("complete order") || text.includes("\u5B8C\u6210\u8BA2\u5355") || text.includes("confirm");
});
}
if (checkoutBtn && !checkoutBtn.disabled) {
const lastClickTime = parseInt(checkoutBtn.dataset.lastClickTime || "0");
const now = Date.now();
if (now - lastClickTime > 2e3) {
logBuffer.push(`Found checkout/place order button [${checkoutBtn.textContent.trim()}], clicking it.`);
checkoutBtn.dataset.lastClickTime = now.toString();
Utils.deepClick(checkoutBtn);
}
}
if (Date.now() - startTime2 > timeout) {
clearInterval(interval);
reject(new Error(`Timeout waiting for page to enter an 'owned' state. (UI might be stuck)`));
}
}, 500);
});
} catch (timeoutError) {
logBuffer.push(`Timeout waiting for ownership: ${timeoutError.message}`);
}
} else {
logBuffer.push(`Could not find an add button.`);
}
}
}
}
} catch (error) {
logBuffer.push(`A critical error occurred: ${error.message}`);
success = false;
} finally {
if (!success) {
try {
const vIframe = document.querySelector('iframe[src*="hcaptcha"], iframe[src*="recaptcha"], iframe[src*="captcha"], iframe[src*="turnstile"], iframe[src*="challenges.cloudflare.com"], iframe[src*="arkoselabs"]');
const bodyText = document.body && document.body.innerText || "";
const vPhrases = ["\u4EBA\u673A\u9A8C\u8BC1", "\u786E\u8BA4\u60A8\u662F\u771F\u4EBA", "\u8BF7\u5B8C\u6210\u5B89\u5168\u9A8C\u8BC1", "\u6211\u4E0D\u662F\u673A\u5668\u4EBA", "Verify you are human", "I'm not a robot", "Checking your browser", "complete the security check", "unusual traffic"];
if (vIframe || vPhrases.some((p) => bodyText.includes(p))) {
logBuffer.push(Utils.getText("worker_captcha"));
}
} catch (e) {
}
}
try {
hasReported = true;
await GM_setValue(Config.DB_KEYS.WORKER_DONE, {
workerId,
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();
}
}, "processDetailPage"),
// 节流标记:100ms 内多次调用只执行一次,消除 Observer/timer 风暴
selectorWithSuffix: /* @__PURE__ */ __name((selector, suffix) => {
return selector.split(",").map((part) => `${part.trim()}${suffix}`).join(", ");
}, "selectorWithSuffix"),
getVisibleCardSelector: /* @__PURE__ */ __name(() => {
return TaskRunner2.selectorWithSuffix(Config.SELECTORS.card, ':not([data-fab-hidden="true"])');
}, "getVisibleCardSelector"),
isHideModeActive: /* @__PURE__ */ __name(() => {
return State.hideSaved || State.hideDiscountedPaid || State.hidePaid;
}, "isHideModeActive"),
getHideModeKey: /* @__PURE__ */ __name(() => {
return [
State.hideSaved ? "saved" : "",
State.hideDiscountedPaid ? "discounted" : "",
State.hidePaid ? "paid" : ""
].join("|");
}, "getHideModeKey"),
isCardHidden: /* @__PURE__ */ __name((card) => {
return card?.getAttribute?.("data-fab-hidden") === "true" || card?.style?.display === "none";
}, "isCardHidden"),
invalidateCardCountCache: /* @__PURE__ */ __name(() => {
State.cardCountCache.dirty = true;
}, "invalidateCardCountCache"),
refreshCardCountCache: /* @__PURE__ */ __name((cardsArg = null) => {
if (typeof document === "undefined") {
State.cardCountCache.total = 0;
State.cardCountCache.hidden = 0;
State.cardCountCache.visible = 0;
State.cardCountCache.dirty = false;
State.cardCountCache.documentRef = null;
State.cardCountCache.href = "";
State.hiddenThisPageCount = 0;
return { total: 0, hidden: 0, visible: 0 };
}
const cards = cardsArg ? Array.from(cardsArg) : Array.from(document.querySelectorAll(Config.SELECTORS.card));
const hidden = cards.reduce((count, card) => count + (TaskRunner2.isCardHidden(card) ? 1 : 0), 0);
const total = cards.length;
const visible = total - hidden;
State.cardCountCache.total = total;
State.cardCountCache.hidden = hidden;
State.cardCountCache.visible = visible;
State.cardCountCache.dirty = false;
State.cardCountCache.documentRef = document;
State.cardCountCache.href = typeof window !== "undefined" ? window.location?.href || "" : "";
State.hiddenThisPageCount = hidden;
return { total, hidden, visible };
}, "refreshCardCountCache"),
getCardCounts: /* @__PURE__ */ __name((forceRefresh = false) => {
const cache = State.cardCountCache;
const href = typeof window !== "undefined" ? window.location?.href || "" : "";
const documentChanged = typeof document !== "undefined" && cache.documentRef !== document;
const hrefChanged = cache.href !== href;
if (forceRefresh || cache.dirty || documentChanged || hrefChanged) {
return TaskRunner2.refreshCardCountCache();
}
return {
total: cache.total,
hidden: cache.hidden,
visible: cache.visible
};
}, "getCardCounts"),
adjustCardCountCacheHidden: /* @__PURE__ */ __name((delta) => {
const cache = State.cardCountCache;
if (cache.dirty) return;
cache.hidden = Math.max(0, Math.min(cache.total, cache.hidden + delta));
cache.visible = Math.max(0, cache.total - cache.hidden);
State.hiddenThisPageCount = cache.hidden;
}, "adjustCardCountCacheHidden"),
setCardHidden: /* @__PURE__ */ __name((card, hidden) => {
if (!card) return;
const wasHidden = TaskRunner2.isCardHidden(card);
if (hidden) {
if (card.style) card.style.display = "none";
card.setAttribute?.("data-fab-hidden", "true");
} else {
if (card.style) card.style.display = "";
card.removeAttribute?.("data-fab-hidden");
}
const isHidden = TaskRunner2.isCardHidden(card);
if (wasHidden !== isHidden) {
TaskRunner2.adjustCardCountCacheHidden(isHidden ? 1 : -1);
}
}, "setCardHidden"),
resetHiddenCardState: /* @__PURE__ */ __name((cardsArg) => {
Array.from(cardsArg || []).forEach((card) => {
card.removeAttribute?.("data-fab-processed");
TaskRunner2.setCardHidden(card, false);
});
}, "resetHiddenCardState"),
shouldHideCard: /* @__PURE__ */ __name((card) => {
const isFinished = State.hideSaved && TaskRunner2.isCardFinished(card);
const isDiscountedPaid = State.hideDiscountedPaid && TaskRunner2.isDiscountedPaidCard(card);
const isPaidAndHidden = State.hidePaid && !TaskRunner2.isFreeCard(card);
return isFinished || isDiscountedPaid || isPaidAndHidden;
}, "shouldHideCard"),
_runHideOrShowTimer: null,
scheduleHideOrShow: /* @__PURE__ */ __name(() => {
if (TaskRunner2._runHideOrShowTimer) return;
TaskRunner2._runHideOrShowTimer = setTimeout(() => {
TaskRunner2._runHideOrShowTimer = null;
TaskRunner2.runHideOrShow();
}, 100);
}, "scheduleHideOrShow"),
runHideOrShow: /* @__PURE__ */ __name(() => {
if (TaskRunner2._runHideOrShowTimer) {
clearTimeout(TaskRunner2._runHideOrShowTimer);
TaskRunner2._runHideOrShowTimer = null;
}
if (!TaskRunner2.isHideModeActive()) {
const allCards = document.querySelectorAll(Config.SELECTORS.card);
TaskRunner2.refreshCardCountCache(allCards);
TaskRunner2.resetHiddenCardState(allCards);
State.lastHideModeKey = TaskRunner2.getHideModeKey();
if (UI4) UI4.update();
return;
}
const hideModeKey = TaskRunner2.getHideModeKey();
if (State.lastHideModeKey !== hideModeKey) {
State.lastHideModeKey = hideModeKey;
TaskRunner2.invalidateCardCountCache();
const allCards = document.querySelectorAll(Config.SELECTORS.card);
TaskRunner2.refreshCardCountCache(allCards);
TaskRunner2.resetHiddenCardState(allCards);
} else {
TaskRunner2.getCardCounts();
}
const cards = document.querySelectorAll(TaskRunner2.getVisibleCardSelector());
let actuallyHidden = 0;
let hasUnsettledCards = false;
const unsettledCards = [];
cards.forEach((card) => {
if (card.getAttribute("data-fab-processed") === "true" && TaskRunner2.isCardHidden(card)) {
TaskRunner2.setCardHidden(card, true);
return;
}
if (!TaskRunner2.isCardSettled(card)) {
hasUnsettledCards = true;
unsettledCards.push(card);
}
});
if (hasUnsettledCards && unsettledCards.length > 0) {
if (State.hideRetryTimer) return;
Utils.logger("debug", Utils.getText("log_unsettled_cards", unsettledCards.length));
State.hideRetryTimer = setTimeout(() => {
State.hideRetryTimer = null;
TaskRunner2.runHideOrShow();
}, 2e3);
} else if (State.hideRetryTimer) {
clearTimeout(State.hideRetryTimer);
State.hideRetryTimer = null;
}
const cardsToHide = [];
cards.forEach((card) => {
if (!TaskRunner2.isCardSettled(card)) {
return;
}
if (TaskRunner2.isCardHidden(card)) {
TaskRunner2.setCardHidden(card, true);
return;
}
card.setAttribute("data-fab-processed", "true");
if (TaskRunner2.shouldHideCard(card)) {
cardsToHide.push(card);
}
});
if (cardsToHide.length > 0) {
if (State.debugMode) {
Utils.logger("debug", Utils.getText("debug_prepare_hide", cardsToHide.length));
}
cardsToHide.sort(() => Math.random() - 0.5);
if (cardsToHide.length > 30) {
const FRAME_BATCH = 20;
let offset = 0;
const hideNextFrame = /* @__PURE__ */ __name(() => {
const end = Math.min(offset + FRAME_BATCH, cardsToHide.length);
for (let i = offset; i < end; i++) {
TaskRunner2.setCardHidden(cardsToHide[i], true);
actuallyHidden++;
}
offset = end;
if (offset < cardsToHide.length) {
requestAnimationFrame(hideNextFrame);
} else {
if (State.debugMode) {
Utils.logger("debug", Utils.getText("debug_hide_completed", actuallyHidden));
}
setTimeout(() => {
if (UI4) UI4.update();
TaskRunner2.checkVisibilityAndRefresh();
}, 300);
}
}, "hideNextFrame");
requestAnimationFrame(hideNextFrame);
} else {
const batchSize = 10;
const batches = Math.ceil(cardsToHide.length / batchSize);
const initialDelay = 200;
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;
const isLastBatch = i === batches - 1;
setTimeout(() => {
let batchHidden = 0;
currentBatch.forEach((card, index) => {
const cardDelay = index * 50 + Math.random() * 100;
setTimeout(() => {
TaskRunner2.setCardHidden(card, true);
actuallyHidden++;
batchHidden++;
if (batchHidden === currentBatch.length) {
if (UI4) UI4.update();
if (isLastBatch) {
if (State.debugMode) {
Utils.logger("debug", Utils.getText("debug_hide_completed", actuallyHidden));
}
setTimeout(() => {
TaskRunner2.checkVisibilityAndRefresh();
}, 300);
}
}
}, cardDelay);
});
}, batchDelay);
}
}
}
if (cardsToHide.length === 0) {
if (UI4) UI4.update();
TaskRunner2.checkVisibilityAndRefresh();
}
return;
}, "runHideOrShow"),
checkVisibilityAndRefresh: /* @__PURE__ */ __name(() => {
const cards = document.querySelectorAll(TaskRunner2.getVisibleCardSelector());
let needsReprocessing = false;
cards.forEach((card) => {
const isProcessed = card.getAttribute("data-fab-processed") === "true";
if (!isProcessed && TaskRunner2.isCardSettled(card)) needsReprocessing = true;
});
if (needsReprocessing) {
if (State.debugMode) {
Utils.logger("debug", Utils.getText("debug_unprocessed_cards_simple"));
}
setTimeout(() => TaskRunner2.runHideOrShow(), 100);
return;
}
const { visible: visibleCards } = TaskRunner2.getCardCounts();
if (State.debugMode) {
Utils.logger("debug", Utils.getText("debug_visible_after_hide", visibleCards, State.hiddenThisPageCount));
}
if (UI4) UI4.update();
if (visibleCards === 0) {
if (State.appStatus === "RATE_LIMITED" && State.autoRefreshEmptyPage) {
if (State.isRefreshScheduled) {
Utils.logger("debug", Utils.getText("refresh_plan_exists"));
return;
}
if (State.db.todo.length > 0 || State.activeWorkers > 0) {
Utils.logger("debug", Utils.getText("log_refresh_cancelled_tasks", State.db.todo.length, State.activeWorkers));
return;
}
Utils.logger("info", Utils.getText("log_all_hidden_rate_limited"));
const randomDelay = 3e3 + Math.random() * 2e3;
if (countdownRefresh2) {
countdownRefresh2(randomDelay, Utils.getText("rate_limit_no_visible_reason"));
}
} else if (State.appStatus === "NORMAL" && State.hiddenThisPageCount > 0) {
Utils.logger("debug", Utils.getText("page_status_hidden_no_visible", State.hiddenThisPageCount));
if (State.autoAddOnScroll) {
TaskRunner2.attemptAutoScroll();
}
}
}
}, "checkVisibilityAndRefresh"),
ensureTasksAreExecuted: /* @__PURE__ */ __name(() => {
if (State.db.todo.length === 0) return;
if (State.isExecuting) {
if (State.activeWorkers === 0) {
Utils.logger("info", Utils.getText("log_ensure_tasks"));
TaskRunner2.executeBatch();
}
return;
}
Utils.logger("info", Utils.getText("log_auto_start_execution", State.db.todo.length));
TaskRunner2.startExecution();
}, "ensureTasksAreExecuted"),
checkVisibleCardsStatus: /* @__PURE__ */ __name(async () => {
if (State.isCheckingStatus) {
return;
}
State.isCheckingStatus = true;
try {
const visibleCards = [...document.querySelectorAll(TaskRunner2.getVisibleCardSelector())];
if (visibleCards.length === 0) {
return;
}
let hasUnsettledCards = false;
const unsettledCards = [];
visibleCards.forEach((card) => {
if (!TaskRunner2.isCardSettled(card)) {
hasUnsettledCards = true;
unsettledCards.push(card);
}
});
if (hasUnsettledCards && unsettledCards.length > 0) {
}
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 (Database.isDone(url)) return;
allItems.push({ uid, url, element: card });
}
});
if (allItems.length === 0) {
return;
}
Utils.logger("debug", Utils.getText("fab_dom_checking_status", allItems.length));
const uids = allItems.map((item) => item.uid);
const statesData = await API.checkItemsOwnership(uids);
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 (Database.addDoneUrl(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("debug", Utils.getText("fab_dom_api_complete", confirmedOwned));
Utils.logger("debug", Utils.getText("fab_dom_refresh_complete", confirmedOwned));
if (UI4) UI4.update();
if (State.hideSaved || State.hideDiscountedPaid || State.hidePaid) {
TaskRunner2.runHideOrShow();
}
} else {
Utils.logger("debug", Utils.getText("fab_dom_no_new_owned"));
}
} catch (error) {
Utils.logger("error", Utils.getText("log_check_status_error", error.message));
if (error.message && error.message.includes("429")) {
RateLimitManager.enterRateLimitedState("[Fab DOM Refresh] 429\u9519\u8BEF");
}
} finally {
State.isCheckingStatus = false;
}
}, "checkVisibleCardsStatus"),
scanAndAddTasks: /* @__PURE__ */ __name(async (cards) => {
if (!State.autoAddOnScroll) return;
if (!State.isAuthenticated) {
if (State.debugMode) {
Utils.logger("debug", Utils.getText("auth_scan_blocked"));
}
return;
}
if (State.isScanningTasks) {
Utils.logger("debug", `\u5DF2\u6709\u626B\u63CF\u4EFB\u52A1\u8FDB\u884C\u4E2D\uFF0C\u8DF3\u8FC7\u672C\u6B21\u8C03\u7528 (${cards.length} \u5F20\u5361\u7247)`);
return;
}
State.isScanningTasks = true;
try {
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("debug_api_wait_in_progress", cards.length));
return;
}
window._apiWaitStatus.isWaiting = true;
window._apiWaitStatus.pendingCards = [...cards];
window._apiWaitStatus.lastApiActivity = Date.now();
if (State.debugMode) {
Utils.logger("debug", Utils.getText("debug_wait_api_response", cards.length));
}
const waitForApiCompletion = /* @__PURE__ */ __name(() => {
return new Promise((resolve) => {
if (window._apiWaitStatus.apiCheckInterval) {
clearInterval(window._apiWaitStatus.apiCheckInterval);
window._apiWaitStatus.apiCheckInterval = null;
}
const maxWaitTime = 1e4;
const startTime = Date.now();
window._apiWaitStatus.apiCheckInterval = setInterval(() => {
const now = Date.now();
const timeSinceLastActivity = now - window._apiWaitStatus.lastApiActivity;
const totalWaitTime = now - startTime;
if (totalWaitTime > maxWaitTime || timeSinceLastActivity > 2e3) {
clearInterval(window._apiWaitStatus.apiCheckInterval);
window._apiWaitStatus.apiCheckInterval = null;
resolve();
}
}, 200);
});
}, "waitForApiCompletion");
try {
await waitForApiCompletion();
} catch (error) {
Utils.logger("error", 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("debug_api_wait_complete", cardsToProcess.length));
}
const newlyAddedList = [];
let skippedAlreadyOwned = 0;
let skippedInTodo = 0;
let skippedUnsettled = 0;
cardsToProcess.forEach((card) => {
const link = card.querySelector(Config.SELECTORS.cardLink);
const url = link ? link.href.split("?")[0] : null;
if (!url) return;
if (!TaskRunner2.isCardSettled(card)) {
skippedUnsettled++;
return;
}
if (Database.isDone(url)) {
skippedAlreadyOwned++;
return;
}
if (Database.isTodo(url)) {
skippedInTodo++;
return;
}
const text = card.textContent || "";
if (text.includes("\u5DF2\u4FDD\u5B58\u5728\u6211\u7684\u5E93\u4E2D") || text.includes("\u5DF2\u4FDD\u5B58") || 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;
}
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;
}
}
}
if (!TaskRunner2.isFreeCard(card)) return;
const name = card.querySelector('a[aria-label*="\u521B\u4F5C\u7684"], 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 (skippedUnsettled > 0 && !State.autoAddRetryTimer) {
State.autoAddRetryTimer = setTimeout(() => {
State.autoAddRetryTimer = null;
if (State.autoAddOnScroll) {
TaskRunner2.scanAndAddTasks(document.querySelectorAll(TaskRunner2.getVisibleCardSelector())).catch((error) => Utils.logger("error", `\u81EA\u52A8\u6DFB\u52A0\u91CD\u8BD5\u5931\u8D25: ${error.message}`));
}
}, 2e3);
} else if (skippedUnsettled === 0 && State.autoAddRetryTimer) {
clearTimeout(State.autoAddRetryTimer);
State.autoAddRetryTimer = null;
}
if (newlyAddedList.length > 0 || skippedAlreadyOwned > 0 || skippedInTodo > 0) {
if (newlyAddedList.length > 0) {
const existingUids = new Set(State.db.todo.map((t) => t.uid));
const existingUrls = new Set(State.db.todo.map((t) => t.url.split("?")[0]));
const uniqueNewTasks = newlyAddedList.filter((task) => {
const cleanUrl = task.url.split("?")[0];
const isDuplicate = existingUids.has(task.uid) || existingUrls.has(cleanUrl);
if (isDuplicate) {
Utils.logger("debug", `\u8DF3\u8FC7\u91CD\u590D\u4EFB\u52A1: ${task.name} (uid: ${task.uid})`);
}
return !isDuplicate;
});
if (uniqueNewTasks.length > 0) {
State.db.todo.push(...uniqueNewTasks);
Utils.logger("info", Utils.getText("auto_add_new_tasks", uniqueNewTasks.length));
if (uniqueNewTasks.length < newlyAddedList.length) {
Utils.logger("debug", `\u8FC7\u6EE4\u4E86 ${newlyAddedList.length - uniqueNewTasks.length} \u4E2A\u91CD\u590D\u4EFB\u52A1`);
}
Database.saveTodo();
State.autoScrollAttempts = 0;
} else {
Utils.logger("debug", `\u6240\u6709 ${newlyAddedList.length} \u4E2A\u4EFB\u52A1\u90FD\u662F\u91CD\u590D\u7684\uFF0C\u5DF2\u8DF3\u8FC7`);
}
}
if (skippedAlreadyOwned > 0 || skippedInTodo > 0) {
Utils.logger("debug", Utils.getText("debug_filter_owned", skippedAlreadyOwned, skippedInTodo));
}
if (State.isExecuting) {
State.executionTotalTasks = State.db.todo.length;
TaskRunner2.executeBatch();
} else if (State.autoAddOnScroll) {
TaskRunner2.startExecution();
}
if (UI4) UI4.update();
}
} finally {
State.isScanningTasks = false;
}
}, "scanAndAddTasks"),
handleRateLimit: /* @__PURE__ */ __name(async (url) => {
await RateLimitManager.enterRateLimitedState(url || "\u7F51\u7EDC\u8BF7\u6C42");
}, "handleRateLimit"),
reportTaskDone: /* @__PURE__ */ __name(async (task, success) => {
try {
await GM_setValue(Config.DB_KEYS.WORKER_DONE, {
workerId: `worker_task_${task.uid}`,
success,
logs: [Utils.getText("task_report", success ? Utils.getText("task_success") : Utils.getText("task_failed"), task.name || task.uid)],
task,
instanceId: Config.INSTANCE_ID,
executionTime: 0
});
Utils.logger("info", Utils.getText("task_report", success ? Utils.getText("task_success") : Utils.getText("task_failed"), task.name || task.uid));
} catch (error) {
Utils.logger("error", Utils.getText("log_report_error", error.message));
}
}, "reportTaskDone"),
onQueueCompleted: null,
stopExecutionAndSettle: /* @__PURE__ */ __name(async () => {
if (State.watchdogTimer) {
clearInterval(State.watchdogTimer);
State.watchdogTimer = null;
}
KeepAlive.stop();
TaskRunner2.closeAllWorkerTabs();
if (typeof TaskRunner2.onQueueCompleted === "function") {
await TaskRunner2.onQueueCompleted();
} else {
Utils.logger("info", Utils.getText("log_all_tasks_completed"));
State.isExecuting = false;
Database.saveExecutingState();
Database.saveTodo();
if (UI4) UI4.update();
}
}, "stopExecutionAndSettle"),
attemptAutoScroll: /* @__PURE__ */ __name(async () => {
if (State.isAutoScrolling) return;
State.isAutoScrolling = true;
if (typeof State.autoScrollAttempts === "undefined") {
State.autoScrollAttempts = 0;
}
const maxScrollAttempts = 3;
Utils.logger("info", Utils.getText("auto_scroll_attempt", State.autoScrollAttempts + 1, maxScrollAttempts));
const getCurrentCardTotal = /* @__PURE__ */ __name(() => {
try {
return TaskRunner2.getCardCounts().total;
} catch (_error) {
return 0;
}
}, "getCurrentCardTotal");
const previousCardTotal = getCurrentCardTotal();
const previousScrollY = typeof window !== "undefined" ? window.scrollY : 0;
const tempRestoredCards = [];
if (typeof document !== "undefined" && typeof document.querySelectorAll === "function") {
document.querySelectorAll('[data-fab-hidden="true"]').forEach((card) => {
if (card.style && card.style.display === "none") {
card.style.display = "";
card.style.visibility = "hidden";
tempRestoredCards.push(card);
}
});
}
const previousScrollHeight = typeof document !== "undefined" && document.documentElement ? document.documentElement.scrollHeight : 0;
if (typeof window !== "undefined" && typeof window.scrollTo === "function") {
window.scrollTo(0, previousScrollHeight);
if (typeof window.dispatchEvent === "function") {
window.dispatchEvent(new Event("scroll"));
}
}
if (tempRestoredCards.length > 0) {
setTimeout(() => {
tempRestoredCards.forEach((card) => {
card.style.visibility = "";
card.style.display = "none";
});
}, 200);
}
setTimeout(async () => {
State.isAutoScrolling = false;
const currentScrollHeight = typeof document !== "undefined" && document.documentElement ? document.documentElement.scrollHeight : 0;
const currentScrollY = typeof window !== "undefined" ? window.scrollY : 0;
const newTodoCount = State.db.todo.length;
const currentCardTotal = getCurrentCardTotal();
if (newTodoCount > 0) {
Utils.logger("info", Utils.getText("auto_scroll_success", newTodoCount));
if (!State.isExecuting) {
TaskRunner2.startExecution();
}
return;
}
const newCardCount = currentCardTotal - previousCardTotal;
if (newCardCount > 0) {
State.autoScrollAttempts = 0;
Utils.logger("debug", Utils.getText("auto_scroll_cards_loaded", newCardCount));
TaskRunner2.runHideOrShow();
TaskRunner2.attemptAutoScroll();
return;
}
const reachedBottom = typeof window !== "undefined" && window.innerHeight + currentScrollY >= currentScrollHeight - 50 || currentScrollHeight === previousScrollHeight && currentScrollY === previousScrollY && previousScrollY > 0;
State.autoScrollAttempts++;
if (State.autoScrollAttempts >= maxScrollAttempts) {
if (reachedBottom) {
Utils.logger("info", Utils.getText("auto_scroll_reached_bottom"));
} else {
Utils.logger("info", Utils.getText("auto_scroll_no_new_items", maxScrollAttempts));
}
if (UI4 && typeof UI4.showToast === "function") {
UI4.showToast(Utils.getText("toast_reached_bottom"), true);
}
const canQuery = typeof document !== "undefined" && typeof document.querySelectorAll === "function";
const counts = canQuery ? TaskRunner2.getCardCounts() : { total: 0, hidden: 0, visible: 0 };
if (counts.visible === 0 && counts.hidden >= 20) {
const recoveryCursor = State.savedCursor;
if (recoveryCursor) {
try {
const lastRecoveryCursor = sessionStorage.getItem("fab_helper_last_recovery_cursor");
if (recoveryCursor !== lastRecoveryCursor) {
sessionStorage.setItem("fab_helper_recovery_cursor", recoveryCursor);
sessionStorage.setItem("fab_helper_last_recovery_cursor", recoveryCursor);
Utils.logger("warn", Utils.getText("log_auto_scroll_stuck_96_refresh", counts.hidden));
setTimeout(() => {
if (typeof window !== "undefined" && window.location) {
window.location.reload();
}
}, 1500);
return;
} else {
Utils.logger("info", Utils.getText("log_auto_scroll_stuck_recovery_failed"));
}
} catch (e) {
}
}
}
if (State.isExecuting && State.rememberScrollPosition && State.savedCursor) {
Utils.logger("warn", Utils.getText("log_auto_scroll_stuck_refresh"));
setTimeout(() => {
if (typeof window !== "undefined" && window.location) {
window.location.reload();
}
}, 1500);
return;
}
if (State.isExecuting) {
await TaskRunner2.stopExecutionAndSettle();
} else {
State.autoScrollAttempts = 0;
State.isAutoScrolling = false;
}
return;
}
Utils.logger("debug", Utils.getText("auto_scroll_waiting"));
TaskRunner2.attemptAutoScroll();
}, 3e3);
}, "attemptAutoScroll")
};
// src/modules/ui.js
var TaskRunner3 = null;
function setTaskRunnerReference(taskRunnerModule) {
TaskRunner3 = taskRunnerModule;
}
__name(setTaskRunnerReference, "setTaskRunnerReference");
var UI5 = {
init: /* @__PURE__ */ __name(() => {
return UI5.create();
}, "init"),
create: /* @__PURE__ */ __name(() => {
const acquisitionButton = [...document.querySelectorAll("button")].find(
(btn) => [...Config.ACQUISITION_TEXT_SET].some((keyword) => btn.textContent.includes(keyword))
);
const downloadTexts = ["\u4E0B\u8F7D", "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;
Utils.logger("info", "On a detail page (detected by action buttons), skipping UI creation.");
return false;
}
if (document.getElementById(Config.UI_CONTAINER_ID)) return true;
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);
}
#${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;
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;
}
.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;
flex-basis: calc((100% - 12px) / 3);
}
.fab-helper-status-label {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
white-space: nowrap;
}
.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;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.fab-helper-execute-btn.executing {
background: var(--pink);
}
.fab-helper-actions {
display: flex;
gap: 8px;
}
.fab-helper-actions button {
flex: 1;
min-width: 0;
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;
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;
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;
}
.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);
}
.fab-debug-history-container {
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.3) rgba(0,0,0,0.2);
}
.fab-debug-history-container::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.fab-debug-history-container::-webkit-scrollbar-track {
background: rgba(0,0,0,0.2);
border-radius: 4px;
}
.fab-debug-history-container::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.3);
border-radius: 4px;
}
.fab-helper-toast-container {
position: fixed;
bottom: 20px;
left: 20px;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
}
.fab-helper-toast {
pointer-events: auto;
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-m);
padding: 12px 16px;
color: var(--text-color-primary);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-width: 250px;
max-width: 350px;
animation: fab-helper-slide-in 0.3s ease-out;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.fab-helper-toast-content {
flex: 1;
font-size: 13px;
line-height: 1.4;
}
.fab-helper-toast-close {
cursor: pointer;
background: transparent;
border: none;
color: var(--text-color-secondary);
font-size: 18px;
font-weight: bold;
padding: 0 4px;
line-height: 1;
transition: color 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.fab-helper-toast-close:hover {
color: var(--pink);
}
@keyframes fab-helper-slide-in {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
`;
const styleSheet = document.createElement("style");
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
const container = document.createElement("div");
container.id = Config.UI_CONTAINER_ID;
State.UI.container = container;
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);
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 = () => UI5.switchTab(tabName);
if (tabName === "dashboard") btn.classList.add("active");
tabContainer.appendChild(btn);
State.UI.tabs[tabName] = btn;
});
container.appendChild(tabContainer);
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 = /* @__PURE__ */ __name((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;
}, "createStatusItem");
State.UI.statusVisible = createStatusItem("fab-status-visible", Utils.getText("visible"), "\u{1F441}\uFE0F");
State.UI.statusTodo = createStatusItem("fab-status-todo", Utils.getText("todo"), "\u{1F4E5}");
State.UI.statusDone = createStatusItem("fab-status-done", Utils.getText("added"), "\u2705");
State.UI.statusFailed = createStatusItem("fab-status-failed", Utils.getText("failed"), "\u274C");
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", Utils.getText("failed_list_empty"));
return;
}
if (window.confirm(Utils.getText("confirm_open_failed", State.db.failed.length))) {
Utils.logger("info", Utils.getText("opening_failed_items", 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"), "\u{1F648}");
statusBar.append(State.UI.statusTodo, State.UI.statusDone, State.UI.statusFailed, State.UI.statusVisible, State.UI.statusHidden);
State.UI.statusKeepAlive = document.createElement("div");
State.UI.statusKeepAlive.className = "fab-helper-keepalive-status";
State.UI.statusKeepAlive.style.cssText = "font-size:11px;opacity:0.8;margin:4px 2px 0;display:none;";
State.UI.statusKeepAlive.title = "\u540E\u53F0\u4FDD\u6D3B\uFF1A\u2665\u5FC3\u8DF3(Worker,\u4E0D\u88AB\u8282\u6D41) + \u2744\uFE0F\u9632\u51BB\u7ED3(WebRTC)\u3002\u540E\u53F0/\u9501\u5C4F\u65F6\u5E94\u6301\u7EED\u5237\u65B0\uFF1B\u82E5\u663E\u793A\u672A\u542F\u52A8\u8BF4\u660E Worker \u88AB\u9875\u9762 CSP \u62E6\u622A";
State.UI.execBtn = document.createElement("button");
State.UI.execBtn.className = "fab-helper-execute-btn";
State.UI.execBtn.onclick = () => TaskRunner3 && TaskRunner3.toggleExecution();
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 = "\u{1F504} " + Utils.getText("sync");
State.UI.syncBtn.onclick = () => TaskRunner3 && TaskRunner3.refreshVisibleStates();
State.UI.hideBtn = document.createElement("button");
State.UI.hideBtn.onclick = () => TaskRunner3 && TaskRunner3.toggleHideSaved();
State.UI.retryFailedBtn = document.createElement("button");
State.UI.retryFailedBtn.textContent = "\u{1F501} " + Utils.getText("retry_failed");
State.UI.retryFailedBtn.onclick = () => TaskRunner3 && TaskRunner3.retryFailedTasks();
actionButtons.append(State.UI.syncBtn, State.UI.hideBtn, State.UI.retryFailedBtn);
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 = "\u{1F4C4}";
copyLogBtn.title = Utils.getText("copyLog");
copyLogBtn.onclick = () => {
navigator.clipboard.writeText(State.UI.logPanel.innerText).then(() => {
const originalText = copyLogBtn.textContent;
copyLogBtn.textContent = "\u2705";
setTimeout(() => {
copyLogBtn.textContent = originalText;
}, 1500);
}).catch((err) => Utils.logger("error", "Failed to copy log:", err));
};
const clearLogBtn = document.createElement("button");
clearLogBtn.innerHTML = "\u{1F5D1}\uFE0F";
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; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: flex; align-items: center;";
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);
positionInfo.style.cssText = "flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;";
State.UI.savedPositionDisplay = positionInfo;
positionContainer.appendChild(positionIcon);
positionContainer.appendChild(positionInfo);
const clearPositionBtn = document.createElement("button");
clearPositionBtn.textContent = "\u{1F504}";
clearPositionBtn.title = Utils.getText("clear_position_tooltip");
clearPositionBtn.style.cssText = "background: none; border: none; cursor: pointer; margin-left: auto; font-size: 14px; padding: 0 4px; opacity: 0.7; transition: opacity 0.2s;";
clearPositionBtn.onmouseover = () => {
clearPositionBtn.style.opacity = "1";
};
clearPositionBtn.onmouseout = () => {
clearPositionBtn.style.opacity = "0.7";
};
clearPositionBtn.onclick = async () => {
if (State.savedCursor) {
if (confirm(Utils.getText("confirm_reset_position"))) {
State.savedCursor = null;
await GM_deleteValue(Config.DB_KEYS.LAST_CURSOR);
State.UI.savedPositionDisplay.textContent = Utils.getText("no_saved_position");
window.location.reload();
}
} else {
Utils.logger("info", Utils.getText("no_position_to_reset"));
}
};
positionContainer.appendChild(clearPositionBtn);
dashboardContent.append(logContainer, positionContainer, statusBar, State.UI.statusKeepAlive, State.UI.execBtn, actionButtons);
container.appendChild(dashboardContent);
const settingsContent = document.createElement("div");
settingsContent.className = "fab-helper-tab-content";
settingsContent.style.display = "none";
const createSettingRow = /* @__PURE__ */ __name((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) => {
e.stopPropagation();
e.preventDefault();
if (!TaskRunner3) return;
if (stateKey === "autoAddOnScroll") {
TaskRunner3.toggleAutoAdd();
} else if (stateKey === "rememberScrollPosition") {
TaskRunner3.toggleRememberPosition();
} else if (stateKey === "autoResumeAfter429") {
TaskRunner3.toggleAutoResume();
} else if (stateKey === "autoRefreshEmptyPage") {
TaskRunner3.toggleAutoRefreshEmpty();
} else if (stateKey === "hideDiscountedPaid") {
TaskRunner3.toggleHideDiscountedPaid();
} else if (stateKey === "hidePaid") {
TaskRunner3.toggleHidePaid();
} else if (stateKey === "blockLargeResources") {
TaskRunner3.toggleBlockResources();
}
e.target.checked = State[stateKey];
};
const slider = document.createElement("span");
slider.className = "fab-toggle-slider";
switchContainer.append(input, slider);
row.append(label, switchContainer);
return row;
}, "createSettingRow");
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 hideDiscountedPaidSetting = createSettingRow(Utils.getText("setting_hide_discounted"), "hideDiscountedPaid");
settingsContent.appendChild(hideDiscountedPaidSetting);
const hidePaidSetting = createSettingRow(Utils.getText("setting_hide_paid"), "hidePaid");
settingsContent.appendChild(hidePaidSetting);
const blockResourcesSetting = createSettingRow(Utils.getText("setting_block_large_resources"), "blockLargeResources");
settingsContent.appendChild(blockResourcesSetting);
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);
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 debugSwitchContainer = document.createElement("label");
debugSwitchContainer.className = "fab-toggle-switch";
const debugInput = document.createElement("input");
debugInput.type = "checkbox";
debugInput.checked = State.debugMode;
debugInput.onchange = (e) => {
State.debugMode = e.target.checked;
debugModeRow.classList.toggle("active", State.debugMode);
Utils.logger("info", Utils.getText("log_debug_mode_toggle", State.debugMode ? Utils.getText("status_enabled") : Utils.getText("status_disabled")));
GM_setValue("fab_helper_debug_mode", State.debugMode);
};
const debugSlider = document.createElement("span");
debugSlider.className = "fab-toggle-slider";
debugSwitchContainer.append(debugInput, debugSlider);
debugModeRow.append(debugLabel, debugSwitchContainer);
debugModeRow.classList.toggle("active", State.debugMode);
settingsContent.appendChild(debugModeRow);
State.UI.tabContents.settings = settingsContent;
container.appendChild(settingsContent);
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; flex-wrap: wrap; justify-content: space-between; align-items: center; margin-bottom: 10px; gap: 8px;";
const debugTitle = document.createElement("h4");
debugTitle.textContent = Utils.getText("status_history");
debugTitle.style.cssText = "margin: 0; font-size: 14px; white-space: nowrap;";
const debugControls = document.createElement("div");
debugControls.style.cssText = "display: flex; gap: 6px; flex-wrap: wrap;";
const copyHistoryBtn = document.createElement("button");
copyHistoryBtn.textContent = Utils.getText("copy_btn");
copyHistoryBtn.title = "\u590D\u5236\u8BE6\u7EC6\u5386\u53F2\u8BB0\u5F55";
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", Utils.getText("no_history_to_copy"));
return;
}
const formatEntry = /* @__PURE__ */ __name((entry) => {
const date = new Date(entry.endTime).toLocaleString();
if (entry.type === "STARTUP") {
return `\u{1F680} ${Utils.getText("script_startup")}
- ${Utils.getText("time_label")}: ${date}
- ${Utils.getText("info_label")}: ${entry.message || ""}`;
} else {
const type = entry.type === "NORMAL" ? `\u2705 ${Utils.getText("normal_period")}` : `\u{1F6A8} ${Utils.getText("rate_limited_period")}`;
let details = `${Utils.getText("duration_label")}: ${entry.duration !== void 0 && entry.duration !== null ? entry.duration.toFixed(2) : Utils.getText("unknown_duration")}s`;
if (entry.requests !== void 0) {
details += `, ${Utils.getText("requests_label")}: ${entry.requests}${Utils.getText("requests_unit")}`;
}
return `${type}
- ${Utils.getText("ended_at")}: ${date}
- ${details}`;
}
}, "formatEntry");
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;
}, 2e3);
}).catch((err) => Utils.logger("error", Utils.getText("log_copy_failed"), err));
};
const clearHistoryBtn = document.createElement("button");
clearHistoryBtn.textContent = Utils.getText("clear_btn");
clearHistoryBtn.title = "\u6E05\u7A7A\u5386\u53F2\u8BB0\u5F55";
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: (/* @__PURE__ */ new Date()).toISOString(),
message: Utils.getText("history_cleared_new_session")
};
await RateLimitManager.addToHistory(currentSessionEntry);
UI5.updateDebugTab();
Utils.logger("info", Utils.getText("status_history_cleared"));
}
};
const diagnosisBtn = document.createElement("button");
diagnosisBtn.textContent = Utils.getText("page_diagnosis");
diagnosisBtn.style.cssText = "background: #2196F3; border: 1px solid #1976D2; color: white; padding: 4px 8px; border-radius: var(--radius-m); cursor: pointer; white-space: nowrap;";
diagnosisBtn.onclick = () => {
try {
const report = PageDiagnostics.diagnoseDetailPage();
PageDiagnostics.logDiagnosticReport(report);
Utils.logger("info", Utils.getText("page_diagnosis_complete"));
} catch (error) {
Utils.logger("error", Utils.getText("page_diagnosis_failed", 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";
State.UI.historyContainer = historyListContainer;
debugContent.append(debugHeader, historyListContainer);
State.UI.tabContents.debug = debugContent;
container.appendChild(debugContent);
document.body.appendChild(container);
return true;
}, "create"),
update: /* @__PURE__ */ __name(() => {
if (!State.UI.container) return;
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 = "\u{1F504} " + Utils.getText("sync");
}
const todoCount = State.db.todo.length;
const doneCount = State.db.done.length;
const failedCount = State.db.failed.length;
const cardCounts = TaskRunner3?.getCardCounts ? TaskRunner3.getCardCounts() : {
total: document.querySelectorAll(Config.SELECTORS.card).length,
hidden: State.hiddenThisPageCount,
visible: Math.max(0, document.querySelectorAll(Config.SELECTORS.card).length - State.hiddenThisPageCount)
};
const hiddenCount = cardCounts.hidden;
const visibleCount = cardCounts.visible;
if (State.UI.statusTodo) State.UI.statusTodo.querySelector("span").textContent = todoCount;
if (State.UI.statusDone) State.UI.statusDone.querySelector("span").textContent = doneCount;
if (State.UI.statusFailed) State.UI.statusFailed.querySelector("span").textContent = failedCount;
if (State.UI.statusHidden) State.UI.statusHidden.querySelector("span").textContent = hiddenCount;
if (State.UI.statusVisible) State.UI.statusVisible.querySelector("span").textContent = visibleCount;
const statusLabelUpdates = [
{ element: State.UI.statusVisible, icon: "\u{1F441}\uFE0F", key: "visible" },
{ element: State.UI.statusTodo, icon: "\u{1F4E5}", key: "todo" },
{ element: State.UI.statusDone, icon: "\u2705", key: "added" },
{ element: State.UI.statusFailed, icon: "\u274C", key: "failed" },
{ element: State.UI.statusHidden, icon: "\u{1F648}", key: "hidden" }
];
statusLabelUpdates.forEach(({ element, icon, key }) => {
const labelDiv = element?.querySelector(".fab-helper-status-label");
if (labelDiv) {
labelDiv.textContent = `${icon} ${Utils.getText(key)}`;
}
});
if (State.UI.execBtn) {
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");
}
}
if (State.UI.statusKeepAlive) {
if (!State.isExecuting) {
State.UI.statusKeepAlive.style.display = "none";
} else {
State.UI.statusKeepAlive.style.display = "";
const ka = KeepAlive.getStatus();
if (!ka.heartbeatAlive) {
State.UI.statusKeepAlive.textContent = `\u{1F6E1}\uFE0F ${Utils.getText("keepalive_label")}: ${Utils.getText("keepalive_dead")}`;
} else {
const ago = ka.lastTickAgoMs == null ? "?" : Math.round(ka.lastTickAgoMs / 1e3);
const fg = ka.freezeGuard === "open" ? "\u2744\uFE0Fon" : ka.freezeGuard === "off" ? "\u2744\uFE0Foff" : "\u2744\uFE0F\u2026";
State.UI.statusKeepAlive.textContent = `\u{1F6E1}\uFE0F ${Utils.getText("keepalive_label")}: \u2665${ago}s \xB7 ${fg}`;
}
}
}
if (State.UI.hideBtn) {
State.UI.hideBtn.textContent = (State.hideSaved ? "\u{1F648} " : "\u{1F441}\uFE0F ") + (State.hideSaved ? Utils.getText("show") : Utils.getText("hide"));
}
if (State.UI.retryFailedBtn) {
State.UI.retryFailedBtn.disabled = State.db.failed.length === 0;
State.UI.retryFailedBtn.title = State.db.failed.length > 0 ? Utils.getText("retry_failed") + ` (${State.db.failed.length})` : Utils.getText("no_failed_tasks");
}
}, "update"),
removeAllOverlays: /* @__PURE__ */ __name(() => {
document.querySelectorAll(Config.SELECTORS.card).forEach((card) => {
const overlay = card.querySelector(".fab-helper-overlay");
if (overlay) overlay.remove();
card.style.opacity = "1";
});
}, "removeAllOverlays"),
showToast: /* @__PURE__ */ __name((message, isPersistent = false) => {
let container = document.getElementById("fab-helper-toast-container");
if (!container) {
container = document.createElement("div");
container.id = "fab-helper-toast-container";
container.className = "fab-helper-toast-container";
document.body.appendChild(container);
}
const existingToasts = container.querySelectorAll(".fab-helper-toast");
for (const existing of existingToasts) {
if (existing.querySelector(".fab-helper-toast-content")?.textContent === message) {
return;
}
}
const toast = document.createElement("div");
toast.className = "fab-helper-toast";
const content = document.createElement("div");
content.className = "fab-helper-toast-content";
content.textContent = message;
toast.appendChild(content);
const closeBtn = document.createElement("button");
closeBtn.className = "fab-helper-toast-close";
closeBtn.innerHTML = "×";
closeBtn.title = Utils.getText("close") || "Close";
closeBtn.onclick = () => {
toast.style.animation = "none";
toast.style.transition = "opacity 0.2s, transform 0.2s";
toast.style.opacity = "0";
toast.style.transform = "translateY(10px)";
setTimeout(() => toast.remove(), 200);
};
toast.appendChild(closeBtn);
container.appendChild(toast);
if (!isPersistent) {
setTimeout(() => {
if (toast.parentNode) {
closeBtn.click();
}
}, 5e3);
}
}, "showToast"),
switchTab: /* @__PURE__ */ __name((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";
}
if (tabName === "debug") {
UI5.updateDebugTab();
}
}, "switchTab"),
updateDebugTab: /* @__PURE__ */ __name(() => {
if (!State.UI.historyContainer) return;
State.UI.historyContainer.innerHTML = "";
const createHistoryItem = /* @__PURE__ */ __name((entry) => {
const item = document.createElement("div");
item.style.cssText = "padding: 8px; margin-bottom: 8px; background: rgba(50,50,55,0.5); border-radius: 6px; border-left: 3px solid;";
if (entry.type === "STARTUP") {
item.style.borderLeftColor = "#2196F3";
item.innerHTML = `
<div style="font-weight: 500; color: #fff;">\u{1F680} ${Utils.getText("script_startup")}</div>
<div style="font-size: 12px; color: var(--text-color-secondary); padding-left: 22px;">
<div>${Utils.getText("time_label")}: ${new Date(entry.endTime).toLocaleString()}</div>
${entry.message ? `<div>${Utils.getText("info_label")}: ${entry.message}</div>` : ""}
</div>
`;
} else {
const isNormal = entry.type === "NORMAL";
item.style.borderLeftColor = isNormal ? "var(--green)" : "var(--orange)";
const icon = isNormal ? "\u2705" : "\u{1F6A8}";
const title = isNormal ? Utils.getText("normal_period") : Utils.getText("rate_limited_period");
const durationText = entry.duration !== void 0 && entry.duration !== null ? entry.duration.toFixed(2) : Utils.getText("unknown_duration");
let detailsHtml = `<div>${Utils.getText("duration_label")}: <strong>${durationText}s</strong></div>`;
if (entry.requests !== void 0) {
detailsHtml += `<div>${Utils.getText("requests_label")}: <strong>${entry.requests}</strong>${Utils.getText("requests_unit")}</div>`;
}
detailsHtml += `<div>${Utils.getText("ended_at")}: ${new Date(entry.endTime).toLocaleString()}</div>`;
item.innerHTML = `
<div style="font-weight: 500; color: #fff;"><span style="font-size: 18px;">${icon}</span> ${title}</div>
<div style="font-size: 12px; color: var(--text-color-secondary); padding-left: 26px;">${detailsHtml}</div>
`;
}
return item;
}, "createHistoryItem");
const createCurrentStatusItem = /* @__PURE__ */ __name(() => {
const item = document.createElement("div");
item.style.cssText = "padding: 12px; margin-bottom: 10px; background: rgba(0,122,255,0.15); border-radius: 8px; border: 1px solid rgba(0,122,255,0.3);";
const header = document.createElement("div");
header.style.cssText = "display: flex; align-items: center; gap: 8px;";
const icon = State.appStatus === "NORMAL" ? "\u2705" : "\u{1F6A8}";
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;
const duration = startTime ? ((Date.now() - startTime) / 1e3).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>`;
}
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");
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)));
}, "updateDebugTab")
};
// src/index.js
(function() {
try {
const blockEnabled = typeof GM_getValue !== "undefined" ? GM_getValue("fab_block_resources_v1", true) : true;
if (blockEnabled) {
const meta = document.createElement("meta");
meta.httpEquiv = "Content-Security-Policy";
meta.content = "img-src 'none'; media-src 'none'; font-src 'none'; frame-src 'self' https://*.hcaptcha.com https://*.recaptcha.net https://*.google.com https://challenges.cloudflare.com; child-src 'self' https://*.hcaptcha.com https://*.recaptcha.net https://*.google.com https://challenges.cloudflare.com;";
const appendCspToHead = /* @__PURE__ */ __name(() => {
if (!document.head || meta.isConnected) return false;
document.head.appendChild(meta);
return true;
}, "appendCspToHead");
if (!appendCspToHead()) {
const onHeadReady = /* @__PURE__ */ __name(() => {
if (appendCspToHead()) {
document.removeEventListener("DOMContentLoaded", onHeadReady);
}
}, "onHeadReady");
document.addEventListener("DOMContentLoaded", onHeadReady, { once: true });
if (document.documentElement && typeof MutationObserver !== "undefined") {
const headObserver = new MutationObserver(() => {
if (appendCspToHead()) headObserver.disconnect();
});
headObserver.observe(document.documentElement, { childList: true });
}
}
const style = document.createElement("style");
style.textContent = `
img, source, picture, video, iframe:not([src*="/payment/web/purchase"]):not([src*="hcaptcha"]):not([src*="recaptcha"]):not([src*="captcha"]):not([src*="turnstile"]):not([src*="challenges.cloudflare.com"]):not([src*="arkoselabs"]), [style*="background-image"] {
display: none !important;
background-image: none !important;
}
`;
if (document.documentElement) {
document.documentElement.appendChild(style);
} else {
document.appendChild(style);
}
const blockImage = /* @__PURE__ */ __name((el) => {
if (el.tagName === "IMG") {
if (el.src && !el.src.startsWith("data:")) {
el.setAttribute("data-original-src", el.src);
el.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
}
if (el.srcset) {
el.setAttribute("data-original-srcset", el.srcset);
el.srcset = "";
}
} else if (el.tagName === "SOURCE") {
if (el.srcset) {
el.setAttribute("data-original-srcset", el.srcset);
el.srcset = "";
}
}
}, "blockImage");
const traverseAndBlock = /* @__PURE__ */ __name((node) => {
if (node.nodeType !== 1) return;
if (node.tagName === "IMG" || node.tagName === "SOURCE") {
blockImage(node);
}
const descendants = node.querySelectorAll("img, source");
descendants.forEach(blockImage);
}, "traverseAndBlock");
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
traverseAndBlock(node);
}
}
});
observer.observe(document.documentElement || document, {
childList: true,
subtree: true
});
console.log("[Fab Helper] Injected CSP, CSS, and MutationObserver to block images/media/iframes/fonts.");
}
} catch (e) {
console.error("[Fab Helper] Failed to inject CSP:", e);
}
})();
var currentCountdownInterval = null;
var currentRefreshTimeout = null;
function countdownRefresh3(delay, reason = "\u5907\u9009\u65B9\u6848") {
if (State.isRefreshScheduled) {
Utils.logger("debug", Utils.getText("refresh_plan_exists").replace("(429\u81EA\u52A8\u6062\u590D)", `(${reason})`));
return;
}
State.isRefreshScheduled = true;
if (currentCountdownInterval) {
clearInterval(currentCountdownInterval);
currentCountdownInterval = null;
}
if (currentRefreshTimeout) {
clearTimeout(currentRefreshTimeout);
currentRefreshTimeout = null;
}
const seconds = delay ? (delay / 1e3).toFixed(1) : "\u672A\u77E5";
Utils.logger("debug", `\u{1F504} ${reason}\u542F\u52A8\uFF01\u5C06\u5728 ${seconds} \u79D2\u540E\u5237\u65B0\u9875\u9762\u5C1D\u8BD5\u6062\u590D...`);
let remainingSeconds = Math.ceil(delay / 1e3);
currentCountdownInterval = setInterval(() => {
remainingSeconds--;
if (remainingSeconds <= 0) {
clearInterval(currentCountdownInterval);
currentCountdownInterval = null;
Utils.logger("debug", `\u23F1\uFE0F \u5012\u8BA1\u65F6\u7ED3\u675F\uFF0C\u6B63\u5728\u5237\u65B0\u9875\u9762...`);
} else {
Utils.logger("debug", Utils.getText("auto_refresh_countdown", remainingSeconds));
if (!State.isRefreshScheduled) {
Utils.logger("debug", `\u23F9\uFE0F \u68C0\u6D4B\u5230\u5237\u65B0\u5DF2\u88AB\u53D6\u6D88\uFF0C\u505C\u6B62\u5012\u8BA1\u65F6`);
clearInterval(currentCountdownInterval);
currentCountdownInterval = null;
if (currentRefreshTimeout) {
clearTimeout(currentRefreshTimeout);
currentRefreshTimeout = null;
}
return;
}
if (remainingSeconds % 3 === 0) {
checkRateLimitStatus().then((isNotLimited) => {
if (isNotLimited) {
Utils.logger("debug", `\u23F1\uFE0F \u68C0\u6D4B\u5230API\u9650\u901F\u5DF2\u89E3\u9664\uFF0C\u53D6\u6D88\u5237\u65B0...`);
clearInterval(currentCountdownInterval);
currentCountdownInterval = null;
if (currentRefreshTimeout) {
clearTimeout(currentRefreshTimeout);
currentRefreshTimeout = null;
}
State.isRefreshScheduled = false;
if (State.appStatus === "RATE_LIMITED") {
RateLimitManager.exitRateLimitedState();
}
return;
}
if (State.appStatus === "RATE_LIMITED") {
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", `\u23F9\uFE0F \u68C0\u6D4B\u5230\u6709 ${State.db.todo.length} \u4E2A\u5F85\u529E\u4EFB\u52A1\u548C ${State.activeWorkers} \u4E2A\u6D3B\u52A8\u5DE5\u4F5C\u7EBF\u7A0B\uFF0C\u5DF2\u53D6\u6D88\u81EA\u52A8\u5237\u65B0\u3002`);
return;
}
} else {
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;
Utils.logger("warn", "\u26A0\uFE0F \u5237\u65B0\u6761\u4EF6\u5DF2\u53D8\u5316\uFF0C\u81EA\u52A8\u5237\u65B0\u5DF2\u53D6\u6D88\u3002");
return;
}
}
}).catch(() => {
});
}
}
}, 1e3);
currentRefreshTimeout = setTimeout(() => {
const visibleCount = parseInt(document.getElementById("fab-status-visible")?.textContent || "0");
if (State.appStatus === "RATE_LIMITED") {
if (State.db.todo.length > 0 || State.activeWorkers > 0) {
Utils.logger("warn", "\u26A0\uFE0F \u6700\u540E\u4E00\u523B\u68C0\u67E5\uFF1A\u5237\u65B0\u6761\u4EF6\u4E0D\u6EE1\u8DB3\uFF0C\u81EA\u52A8\u5237\u65B0\u5DF2\u53D6\u6D88\u3002");
State.isRefreshScheduled = false;
return;
}
if (visibleCount === 0) {
Utils.logger("info", `\u{1F504} \u9875\u9762\u4E0A\u6CA1\u6709\u53EF\u89C1\u5546\u54C1\u4E14\u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u5C06\u6267\u884C\u81EA\u52A8\u5237\u65B0\u3002`);
window.location.href = window.location.href;
} else {
State.isRefreshScheduled = false;
return;
}
} else {
if (State.db.todo.length > 0 || State.activeWorkers > 0 || visibleCount > 0) {
Utils.logger("warn", "\u26A0\uFE0F \u6700\u540E\u4E00\u523B\u68C0\u67E5\uFF1A\u5237\u65B0\u6761\u4EF6\u4E0D\u6EE1\u8DB3\uFF0C\u81EA\u52A8\u5237\u65B0\u5DF2\u53D6\u6D88\u3002");
State.isRefreshScheduled = false;
} else {
window.location.href = window.location.href;
}
}
}, delay);
}
__name(countdownRefresh3, "countdownRefresh");
async function checkRateLimitStatus() {
try {
const { hidden: hiddenCards, visible: actualVisibleCards } = TaskRunner2.getCardCounts();
const visibleCountElement = document.getElementById("fab-status-visible");
if (visibleCountElement) {
visibleCountElement.textContent = actualVisibleCards.toString();
}
if (State.appStatus === "RATE_LIMITED" && actualVisibleCards === 0) {
return false;
}
if (actualVisibleCards === 0 && hiddenCards > 25) {
return false;
}
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 < 1e4);
if (recentRequests.length > 0) {
const has429 = recentRequests.some((r) => r.responseStatus === 429);
if (has429) return false;
const hasSuccess = recentRequests.some((r) => r.responseStatus >= 200 && r.responseStatus < 300);
if (hasSuccess) return true;
}
return State.appStatus === "NORMAL";
}
return State.appStatus === "NORMAL";
} catch (error) {
Utils.logger("error", `\u68C0\u67E5\u9650\u901F\u72B6\u6001\u51FA\u9519: ${error.message}`);
return false;
}
}
__name(checkRateLimitStatus, "checkRateLimitStatus");
setUIReference(UI5);
setUIReference2(UI5);
setUIReference3(UI5);
setDependencies2({
countdownRefresh: countdownRefresh3
});
setTaskRunnerReference(TaskRunner2);
setDependencies({
UI: UI5,
TaskRunner: TaskRunner2,
countdownRefresh: countdownRefresh3
});
var _ownedStatusUpdateTimer = null;
function triggerOwnedStatusUpdate() {
if (State.isWorkerTab) return;
clearTimeout(_ownedStatusUpdateTimer);
_ownedStatusUpdateTimer = setTimeout(() => {
if (State.hideSaved) {
try {
TaskRunner2.scheduleHideOrShow();
} catch (e) {
}
}
try {
TaskRunner2.checkVisibleCardsStatus().catch(() => {
});
} catch (e) {
}
}, 50);
}
__name(triggerOwnedStatusUpdate, "triggerOwnedStatusUpdate");
function parseListingsStatesUrl(urlString) {
try {
const urlObj = new URL(urlString, window.location.origin);
const uids = [];
let paramName = "listing_ids";
if (urlObj.searchParams.has("listing_ids")) {
uids.push(...urlObj.searchParams.getAll("listing_ids"));
paramName = "listing_ids";
} else if (urlObj.searchParams.has("listing_uids")) {
uids.push(...urlObj.searchParams.getAll("listing_uids"));
paramName = "listing_uids";
}
return { uids, paramName, urlObj };
} catch (e) {
return { uids: [], paramName: "listing_ids", urlObj: null };
}
}
__name(parseListingsStatesUrl, "parseListingsStatesUrl");
function createOwnedStateResponses(uids) {
return uids.map((uid) => ({
uid,
acquired: true,
lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
}));
}
__name(createOwnedStateResponses, "createOwnedStateResponses");
function defineMockXhrProperty(xhr, name, value) {
try {
Object.defineProperty(xhr, name, {
get: /* @__PURE__ */ __name(() => value, "get"),
configurable: true
});
} catch (e) {
}
}
__name(defineMockXhrProperty, "defineMockXhrProperty");
function dispatchMockXhrEvent(xhr, type) {
try {
xhr.dispatchEvent(new Event(type));
} catch (e) {
}
}
__name(dispatchMockXhrEvent, "dispatchMockXhrEvent");
function respondWithMockListingsStates(xhr, uids) {
const mockResponses = createOwnedStateResponses(uids);
const responseText = JSON.stringify(mockResponses);
DataCache.saveOwnedStatus(mockResponses);
triggerOwnedStatusUpdate();
defineMockXhrProperty(xhr, "readyState", 4);
defineMockXhrProperty(xhr, "status", 200);
defineMockXhrProperty(xhr, "statusText", "OK");
defineMockXhrProperty(xhr, "responseURL", xhr._url || "");
defineMockXhrProperty(xhr, "responseText", responseText);
defineMockXhrProperty(xhr, "response", xhr.responseType === "json" ? mockResponses : responseText);
setTimeout(() => {
dispatchMockXhrEvent(xhr, "readystatechange");
dispatchMockXhrEvent(xhr, "load");
dispatchMockXhrEvent(xhr, "loadend");
}, 0);
}
__name(respondWithMockListingsStates, "respondWithMockListingsStates");
function parsePricesInfoUrl(urlString) {
try {
const urlObj = new URL(urlString, window.location.origin);
return {
offerIds: urlObj.searchParams.getAll("offer_ids"),
urlObj
};
} catch (e) {
return { offerIds: [], urlObj: null };
}
}
__name(parsePricesInfoUrl, "parsePricesInfoUrl");
function splitDoneOfferIds(offerIds) {
const doneOfferIds = [];
const activeOfferIds = [];
offerIds.forEach((offerId) => {
const listingUid = DataCache.getListingUidForOffer(offerId);
if (listingUid && Database.isDone(`https://www.fab.com/listings/${listingUid}`)) {
doneOfferIds.push(offerId);
} else {
activeOfferIds.push(offerId);
}
});
return { doneOfferIds, activeOfferIds };
}
__name(splitDoneOfferIds, "splitDoneOfferIds");
function createEmptyPricesResponse() {
return { offers: [] };
}
__name(createEmptyPricesResponse, "createEmptyPricesResponse");
function createPricesResponse(offerIds) {
const { result: cachedOffers } = DataCache.getPrices(offerIds);
return { offers: cachedOffers };
}
__name(createPricesResponse, "createPricesResponse");
function respondWithMockPricesInfo(xhr, offerIds = []) {
const responseData = offerIds.length > 0 ? createPricesResponse(offerIds) : createEmptyPricesResponse();
const responseText = JSON.stringify(responseData);
defineMockXhrProperty(xhr, "readyState", 4);
defineMockXhrProperty(xhr, "status", 200);
defineMockXhrProperty(xhr, "statusText", "OK");
defineMockXhrProperty(xhr, "responseURL", xhr._url || "");
defineMockXhrProperty(xhr, "responseText", responseText);
defineMockXhrProperty(xhr, "response", xhr.responseType === "json" ? responseData : responseText);
setTimeout(() => {
dispatchMockXhrEvent(xhr, "readystatechange");
dispatchMockXhrEvent(xhr, "load");
dispatchMockXhrEvent(xhr, "loadend");
}, 0);
}
__name(respondWithMockPricesInfo, "respondWithMockPricesInfo");
function setupXHRInterceptor() {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url, ...args) {
if (url && typeof url === "string" && url.includes("/i/users/me/listings-states")) {
const { uids, paramName, urlObj } = parseListingsStatesUrl(url);
if (uids.length > 0) {
const doneUids = [];
const activeUids = [];
uids.forEach((uid) => {
const itemUrl = `https://www.fab.com/listings/${uid}`;
if (Database.isDone(itemUrl)) {
doneUids.push(uid);
} else {
activeUids.push(uid);
}
});
if (doneUids.length > 0) {
this._doneUids = doneUids;
this._allUids = uids;
this._paramName = paramName;
if (activeUids.length > 0) {
urlObj.searchParams.delete(paramName);
activeUids.forEach((uid) => urlObj.searchParams.append(paramName, uid));
url = urlObj.toString();
Utils.logger("debug", `[XHR Intercept] \u8FC7\u6EE4\u5DF2\u5165\u5E93\u5546\u54C1: \u539F\u8BF7\u6C42 ${uids.length} \u4E2A, \u8FC7\u6EE4 ${doneUids.length} \u4E2A, \u5B9E\u9645\u8BF7\u6C42 ${activeUids.length} \u4E2A`);
} else {
this._allDoneLocalResponse = true;
Utils.logger("debug", `[XHR Intercept] \u6240\u6709 ${uids.length} \u4E2A\u5546\u54C1\u5747\u5DF2\u672C\u5730\u5165\u5E93\uFF0C\u8F6C\u6362\u4E3A\u5355 ID \u5360\u4F4D\u8BF7\u6C42`);
}
}
}
} else if (url && typeof url === "string" && url.includes("/i/listings/prices-infos")) {
const { offerIds, urlObj } = parsePricesInfoUrl(url);
if (offerIds.length > 0) {
const { doneOfferIds, activeOfferIds } = splitDoneOfferIds(offerIds);
if (doneOfferIds.length > 0) {
this._donePriceOfferIds = doneOfferIds;
if (activeOfferIds.length > 0) {
urlObj.searchParams.delete("offer_ids");
activeOfferIds.forEach((offerId) => urlObj.searchParams.append("offer_ids", offerId));
url = urlObj.toString();
Utils.logger("debug", `[XHR Intercept] Filtered done listing price requests: original=${offerIds.length}, skipped=${doneOfferIds.length}, requested=${activeOfferIds.length}`);
} else {
this._allDonePricesLocalResponse = true;
Utils.logger("debug", `[XHR Intercept] Skipped ${offerIds.length} price requests for local done listings`);
}
}
}
}
this._url = url;
return originalOpen.apply(this, [method, url, ...args]);
};
XMLHttpRequest.prototype.send = function(...args) {
const xhr = this;
if (xhr._allDoneLocalResponse && xhr._allUids && xhr._allUids.length > 0) {
respondWithMockListingsStates(xhr, xhr._allUids);
return;
}
if (xhr._allDonePricesLocalResponse && xhr._donePriceOfferIds && xhr._donePriceOfferIds.length > 0) {
respondWithMockPricesInfo(xhr, xhr._donePriceOfferIds);
return;
}
if (xhr._url && typeof xhr._url === "string") {
xhr.addEventListener("readystatechange", function() {
if (xhr.readyState === 4 && xhr.status === 200 && xhr._doneUids && xhr._doneUids.length > 0) {
try {
const originalText = xhr.responseText;
const rawData = JSON.parse(originalText);
const serverResults = Array.isArray(rawData) ? rawData : API.extractStateData(rawData, "XHRInterceptor") || [];
const mockDoneResponses = createOwnedStateResponses(xhr._doneUids);
const mergedResults = [...serverResults, ...mockDoneResponses];
DataCache.saveOwnedStatus(mergedResults);
triggerOwnedStatusUpdate();
const mergedText = JSON.stringify(mergedResults);
Object.defineProperty(xhr, "responseText", { get: /* @__PURE__ */ __name(() => mergedText, "get"), configurable: true });
Object.defineProperty(xhr, "response", {
get: /* @__PURE__ */ __name(() => {
if (xhr.responseType === "json") {
return mergedResults;
}
return mergedText;
}, "get"),
configurable: true
});
} catch (e) {
Utils.logger("error", `XHR \u72B6\u6001\u62E6\u622A\u5408\u5E76\u5931\u8D25: ${e.message}`);
}
}
});
xhr.addEventListener("load", function() {
if (xhr.readyState === 4 && xhr.status === 200 && !xhr._doneUids) {
try {
const responseData = JSON.parse(xhr.responseText);
if (xhr._url.includes("/i/listings/search") && responseData.results && Array.isArray(responseData.results)) {
DataCache.saveListings(responseData.results);
} else if (xhr._url.includes("/i/users/me/listings-states")) {
if (Array.isArray(responseData)) {
DataCache.saveOwnedStatus(responseData);
triggerOwnedStatusUpdate();
} else {
const extractedData = API.extractStateData(responseData, "XHRInterceptor");
if (Array.isArray(extractedData) && extractedData.length > 0) {
DataCache.saveOwnedStatus(extractedData);
triggerOwnedStatusUpdate();
}
}
} else if (xhr._url.includes("/i/listings/prices-infos") && responseData.offers && Array.isArray(responseData.offers)) {
DataCache.savePrices(responseData.offers);
}
} catch (e) {
}
}
if (xhr._url && xhr._url.includes("/i/listings/search")) {
if (xhr.status === 429) {
Utils.logger("warn", Utils.getText("detected_api_429_status", xhr._url));
if (typeof window.enterRateLimitedState === "function") {
window.enterRateLimitedState();
}
}
}
});
}
return originalSend.apply(this, args);
};
}
__name(setupXHRInterceptor, "setupXHRInterceptor");
function setupFetchInterceptor() {
const originalFetch = window.fetch;
window.fetch = async function(...args) {
let url = args[0]?.toString() || "";
if (url.includes("/i/users/me/listings-states")) {
const { uids, paramName, urlObj } = parseListingsStatesUrl(url);
if (uids.length > 0) {
const doneUids = [];
const activeUids = [];
uids.forEach((uid) => {
const itemUrl = `https://www.fab.com/listings/${uid}`;
if (Database.isDone(itemUrl)) {
doneUids.push(uid);
} else {
activeUids.push(uid);
}
});
if (doneUids.length > 0) {
const mockDoneResponses = createOwnedStateResponses(doneUids);
if (activeUids.length === 0) {
Utils.logger("info", `[Fetch Intercept] \u6240\u6709 ${uids.length} \u4E2A\u5546\u54C1\u5747\u5DF2\u672C\u5730\u5165\u5E93\uFF0C\u5B8C\u5168\u7ED5\u8FC7\u7F51\u7EDC\u8BF7\u6C42`);
DataCache.saveOwnedStatus(mockDoneResponses);
triggerOwnedStatusUpdate();
return new Response(JSON.stringify(mockDoneResponses), {
status: 200,
statusText: "OK",
headers: { "Content-Type": "application/json" }
});
}
urlObj.searchParams.delete(paramName);
activeUids.forEach((uid) => urlObj.searchParams.append(paramName, uid));
url = urlObj.toString();
args[0] = url;
Utils.logger("debug", `[Fetch Intercept] \u8FC7\u6EE4\u5DF2\u5165\u5E93\u5546\u54C1: \u539F\u8BF7\u6C42 ${uids.length} \u4E2A, \u8FC7\u6EE4 ${doneUids.length} \u4E2A, \u5B9E\u9645\u8BF7\u6C42 ${activeUids.length} \u4E2A`);
if (window._apiWaitStatus) {
window._apiWaitStatus.lastApiActivity = Date.now();
}
try {
const response = await originalFetch.apply(this, args);
if (response.ok) {
const clonedResponse = response.clone();
const rawData = await clonedResponse.json();
const serverResults = Array.isArray(rawData) ? rawData : API.extractStateData(rawData, "FetchInterceptor") || [];
const mergedResults = [...serverResults, ...mockDoneResponses];
DataCache.saveOwnedStatus(mergedResults);
triggerOwnedStatusUpdate();
return new Response(JSON.stringify(mergedResults), {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}
return response;
} catch (e) {
Utils.logger("error", `\u62E6\u622A fetch listings-states \u5931\u8D25\uFF0C\u4F7F\u7528\u539F\u59CB fetch \u56DE\u9000: ${e.message}`);
return originalFetch.apply(this, args);
}
}
}
}
if (url.includes("/i/listings/prices-infos")) {
const { offerIds, urlObj } = parsePricesInfoUrl(url);
if (offerIds.length > 0) {
const { doneOfferIds, activeOfferIds } = splitDoneOfferIds(offerIds);
if (doneOfferIds.length > 0) {
if (activeOfferIds.length === 0) {
Utils.logger("debug", `[Fetch Intercept] Skipped ${offerIds.length} price requests for local done listings`);
return new Response(JSON.stringify(createPricesResponse(doneOfferIds)), {
status: 200,
statusText: "OK",
headers: { "Content-Type": "application/json" }
});
}
urlObj.searchParams.delete("offer_ids");
activeOfferIds.forEach((offerId) => urlObj.searchParams.append("offer_ids", offerId));
url = urlObj.toString();
args[0] = url;
Utils.logger("debug", `[Fetch Intercept] Filtered done listing price requests: original=${offerIds.length}, skipped=${doneOfferIds.length}, requested=${activeOfferIds.length}`);
}
}
}
if (url.includes("/i/listings/search") || url.includes("/i/users/me/listings-states") || url.includes("/i/listings/prices-infos")) {
if (window._apiWaitStatus) {
window._apiWaitStatus.lastApiActivity = Date.now();
}
try {
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)) {
DataCache.saveOwnedStatus(data);
triggerOwnedStatusUpdate();
} else {
const extractedData = API.extractStateData(data, "FetchInterceptor");
if (Array.isArray(extractedData) && extractedData.length > 0) {
DataCache.saveOwnedStatus(extractedData);
triggerOwnedStatusUpdate();
}
}
} else if (url.includes("/i/listings/prices-infos") && data.offers && Array.isArray(data.offers)) {
DataCache.savePrices(data.offers);
}
}).catch(() => {
});
}
return response;
} catch (e) {
return originalFetch.apply(this, args);
}
}
return originalFetch.apply(this, args);
};
}
__name(setupFetchInterceptor, "setupFetchInterceptor");
function setupRequestInterceptors() {
try {
setupXHRInterceptor();
setupFetchInterceptor();
setInterval(() => DataCache.cleanupExpired(), 6e4);
Utils.logger("debug", "\u8BF7\u6C42\u62E6\u622A\u548C\u7F13\u5B58\u7CFB\u7EDF\u5DF2\u521D\u59CB\u5316");
} catch (e) {
Utils.logger("error", `\u521D\u59CB\u5316\u8BF7\u6C42\u62E6\u622A\u5668\u5931\u8D25: ${e.message}`);
}
}
__name(setupRequestInterceptors, "setupRequestInterceptors");
async function runDomDependentPart() {
if (State.hasRunDomPart) return;
if (State.isWorkerTab) {
State.hasRunDomPart = true;
return;
}
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has("workerId")) {
Utils.logger("debug", `\u5DE5\u4F5C\u6807\u7B7E\u9875DOM\u90E8\u5206\u521D\u59CB\u5316\uFF0C\u8DF3\u8FC7UI\u521B\u5EFA`);
State.hasRunDomPart = true;
return;
}
const uiCreated = UI5.create();
if (!uiCreated) {
Utils.logger("info", Utils.getText("log_detail_page"));
State.hasRunDomPart = true;
return;
}
UI5.update();
UI5.updateDebugTab();
UI5.switchTab("dashboard");
State.hasRunDomPart = true;
window.enterRateLimitedState = function(source = Utils.getText("rate_limit_source_global_call")) {
RateLimitManager.enterRateLimitedState(source);
};
window.recordNetworkRequest = function(source = "\u7F51\u7EDC\u8BF7\u6C42", hasResults = true) {
if (hasResults) {
RateLimitManager.recordSuccessfulRequest(source, hasResults);
}
};
setInterval(() => {
if (State.appStatus === "NORMAL") {
let pageText = "";
const uiContainer = document.getElementById(Config.UI_CONTAINER_ID);
if (uiContainer) {
const originalDisplay = uiContainer.style.display;
uiContainer.style.display = "none";
pageText = document.body.innerText || "";
uiContainer.style.display = originalDisplay;
} else {
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"));
}
}
}, 5e3);
const checkIsErrorPage = /* @__PURE__ */ __name((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");
if (isCloudflareTitle || is429Text) {
Utils.logger("warn", `[\u9875\u9762\u52A0\u8F7D] \u68C0\u6D4B\u5230429\u9519\u8BEF\u9875\u9762`);
window.enterRateLimitedState("\u9875\u9762\u5185\u5BB9429\u68C0\u6D4B");
return true;
}
return false;
}, "checkIsErrorPage");
checkIsErrorPage(document.title, document.body.innerText || "");
if (State.appStatus === "RATE_LIMITED") {
Utils.logger("debug", Utils.getText("log_auto_resume_page_loading"));
const isRecovered = await RateLimitManager.checkRateLimitStatus();
if (isRecovered) {
Utils.logger("info", Utils.getText("log_recovery_probe_success"));
if (State.appStatus === "RATE_LIMITED") {
await RateLimitManager.exitRateLimitedState("\u9875\u9762\u52A0\u8F7D\u63A2\u6D4B\u6210\u529F");
}
if (State.db.todo.length > 0 && !State.isExecuting) {
Utils.logger("info", Utils.getText("log_found_todo_auto_resume", State.db.todo.length));
State.isExecuting = true;
Database.saveExecutingState();
TaskRunner2.executeBatch();
}
} else {
Utils.logger("warn", Utils.getText("log_recovery_probe_failed"));
if (State.activeWorkers === 0 && State.db.todo.length === 0) {
const randomDelay = 5e3 + Math.random() * 1e4;
countdownRefresh3(randomDelay, Utils.getText("countdown_refresh_source"));
}
}
}
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) {
TaskRunner2.invalidateCardCountCache();
clearTimeout(State.observerDebounceTimer);
State.observerDebounceTimer = setTimeout(() => {
if (State.debugMode) {
Utils.logger("debug", `[Observer] ${Utils.getText("debug_new_content_loading")}`);
}
const shouldProcessNewCards = State.hideSaved || State.hideDiscountedPaid || State.hidePaid || State.autoAddOnScroll || State.isExecuting || State.db.todo.length > 0;
if (!shouldProcessNewCards) return;
TaskRunner2.checkVisibleCardsStatus().then(() => {
if (State.hideSaved || State.hideDiscountedPaid || State.hidePaid) {
TaskRunner2.scheduleHideOrShow();
}
if (State.autoAddOnScroll) {
TaskRunner2.scanAndAddTasks(document.querySelectorAll(TaskRunner2.getVisibleCardSelector())).catch((error) => Utils.logger("error", `\u81EA\u52A8\u6DFB\u52A0\u4EFB\u52A1\u5931\u8D25: ${error.message}`));
}
}).catch(() => {
if (State.hideSaved || State.hideDiscountedPaid || State.hidePaid) {
TaskRunner2.scheduleHideOrShow();
}
});
}, 300);
}
});
observer.observe(targetNode, { childList: true, subtree: true });
Utils.logger("debug", `\u2705 Core DOM observer is now active on <${targetNode.tagName.toLowerCase()}>.`);
TaskRunner2.runHideOrShow();
if (State.autoAddOnScroll) {
setTimeout(() => {
Utils.logger("debug", "\u9875\u9762\u52A0\u8F7D\u5B8C\u6210\uFF0C\u6B63\u5728\u6267\u884C\u521D\u59CB\u5546\u54C1\u626B\u63CF...");
TaskRunner2.scanAndAddTasks(document.querySelectorAll(TaskRunner2.getVisibleCardSelector())).catch((error) => Utils.logger("error", `\u521D\u59CB\u626B\u63CF\u4EFB\u52A1\u5931\u8D25: ${error.message}`));
}, 3e3);
}
setInterval(() => {
if (!State.hideSaved && !State.hideDiscountedPaid && !State.hidePaid) return;
const cards = document.querySelectorAll(TaskRunner2.getVisibleCardSelector());
let unprocessedCount = 0;
cards.forEach((card) => {
if (card.getAttribute("data-fab-processed") === "true" && TaskRunner2.isCardHidden(card)) return;
const isProcessed = card.getAttribute("data-fab-processed") === "true";
if (!isProcessed) {
unprocessedCount++;
} else {
const shouldBeHidden = TaskRunner2.shouldHideCard(card);
const isHidden = TaskRunner2.isCardHidden(card);
if (shouldBeHidden !== isHidden) {
card.removeAttribute("data-fab-processed");
unprocessedCount++;
}
}
});
if (unprocessedCount > 0) {
if (State.debugMode) {
Utils.logger("debug", Utils.getText("debug_unprocessed_cards", unprocessedCount));
}
TaskRunner2.scheduleHideOrShow();
}
}, 1e4);
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 !Database.isDone(url);
});
if (State.db.todo.length < initialTodoCount) {
Utils.logger("info", `[\u81EA\u52A8\u6E05\u7406] \u4ECE\u5F85\u529E\u5217\u8868\u4E2D\u79FB\u9664\u4E86 ${initialTodoCount - State.db.todo.length} \u4E2A\u5DF2\u5B8C\u6210\u7684\u4EFB\u52A1\u3002`);
UI5.update();
}
}, 1e4);
let lastCardCount = TaskRunner2.getCardCounts().total;
let noNewCardsCounter = 0;
let lastScrollY = window.scrollY;
setInterval(() => {
if (State.appStatus !== "NORMAL") return;
const currentCardCount = TaskRunner2.getCardCounts().total;
if (window.scrollY > lastScrollY + 100 && currentCardCount === lastCardCount) {
noNewCardsCounter++;
if (noNewCardsCounter >= 3) {
Utils.logger("warn", `${Utils.getText("implicit_rate_limit_detection")}`);
RateLimitManager.enterRateLimitedState(Utils.getText("source_implicit_rate_limit"));
noNewCardsCounter = 0;
}
} else if (currentCardCount > lastCardCount) {
noNewCardsCounter = 0;
}
lastCardCount = currentCardCount;
lastScrollY = window.scrollY;
}, 5e3);
setInterval(async () => {
try {
if (UI5) UI5.update();
const { visible: actualVisibleCards } = TaskRunner2.getCardCounts();
if (State.appStatus === "RATE_LIMITED" && actualVisibleCards === 0 && State.autoRefreshEmptyPage) {
if (!State.isRefreshScheduled && !currentCountdownInterval && !currentRefreshTimeout) {
Utils.logger("debug", Utils.getText("log_all_hidden_rate_limited"));
const randomDelay = 3e3 + Math.random() * 2e3;
countdownRefresh3(randomDelay, Utils.getText("rate_limit_no_visible_reason"));
}
}
} catch (error) {
Utils.logger("error", `\u9875\u9762\u72B6\u6001\u68C0\u67E5\u51FA\u9519: ${error.message}`);
}
}, 1e4);
setInterval(() => {
if (State.db.todo.length === 0) return;
TaskRunner2.ensureTasksAreExecuted();
}, 5e3);
setInterval(async () => {
try {
if (State.appStatus !== "NORMAL") return;
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\u72B6\u6001\u68C0\u6D4B] \u68C0\u6D4B\u5230\u5BFC\u822A\u8BF7\u6C42\u72B6\u6001\u7801\u4E3A429\uFF01`);
if (typeof window.enterRateLimitedState === "function") {
window.enterRateLimitedState();
}
}
}
}
} catch (error) {
}
}, 1e4);
}
__name(runDomDependentPart, "runDomDependentPart");
function ensureUILoaded() {
if (!document.getElementById(Config.UI_CONTAINER_ID)) {
Utils.logger("warn", "\u68C0\u6D4B\u5230UI\u672A\u52A0\u8F7D\uFF0C\u5C1D\u8BD5\u91CD\u65B0\u521D\u59CB\u5316...");
setTimeout(() => {
try {
runDomDependentPart();
} catch (error) {
Utils.logger("error", `UI\u91CD\u65B0\u521D\u59CB\u5316\u5931\u8D25: ${error.message}`);
}
}, 1e3);
}
}
__name(ensureUILoaded, "ensureUILoaded");
async function main() {
window.pageLoadTime = Date.now();
TaskRunner2.onQueueCompleted = async () => {
Utils.logger("info", "\u6240\u6709\u4EFB\u52A1\u5DF2\u5B8C\u6210\u3002");
State.isExecuting = false;
Database.saveExecutingState();
await Database.saveTodo();
if (UI5 && typeof UI5.showToast === "function") {
UI5.showToast(Utils.getText("toast_all_tasks_completed"), true);
}
if (State.appStatus === "RATE_LIMITED") {
Utils.logger("info", "\u6240\u6709\u4EFB\u52A1\u5DF2\u5B8C\u6210\uFF0C\u4E14\u5904\u4E8E\u9650\u901F\u72B6\u6001\uFF0C\u5C06\u5237\u65B0\u9875\u9762\u5C1D\u8BD5\u6062\u590D...");
const randomDelay = 3e3 + Math.random() * 5e3;
countdownRefresh3(randomDelay, "\u4EFB\u52A1\u5B8C\u6210\u540E\u9650\u901F\u6062\u590D");
}
UI5.update();
};
Utils.logger("info", Utils.getText("log_script_starting"));
Utils.detectLanguage();
const hasCookie = Utils.checkAuthentication(true);
if (!hasCookie) {
Utils.logger("warn", "\u8D26\u53F7\u672A\u767B\u5F55\uFF0C\u90E8\u5206\u529F\u80FD\u53EF\u80FD\u53D7\u9650");
State.isAuthenticated = false;
} else {
State.isAuthenticated = true;
}
const urlParams = new URLSearchParams(window.location.search);
const workerId = urlParams.get("workerId");
if (workerId) {
State.isWorkerTab = true;
State.workerTaskId = workerId;
if (!hasCookie || !await Utils.verifyServerSession()) {
Utils.logger("error", Utils.getText("auth_worker_aborted"));
return;
}
await InstanceManager.init();
Utils.logger("info", `\u5DE5\u4F5C\u6807\u7B7E\u9875\u521D\u59CB\u5316\u5B8C\u6210\uFF0C\u5F00\u59CB\u5904\u7406\u4EFB\u52A1...`);
await TaskRunner2.processDetailPage();
return;
}
await InstanceManager.init();
await Database.load();
KeepAlive.setTick(async () => {
if (State.isWorkerTab || !State.isExecuting) {
KeepAlive.stop();
return;
}
InstanceManager.ping();
KeepAlive.poke();
await TaskRunner2.checkStalledWorkers();
if (State.isExecuting && State.activeWorkers < Config.MAX_CONCURRENT_WORKERS && State.db.todo.length > 0) {
TaskRunner2.executeBatch();
}
});
if (hasCookie) {
Utils.verifyServerSession().then((ok) => {
if (!ok) {
Utils.logger("warn", Utils.getText("auth_session_invalid"));
State.isAuthenticated = false;
}
});
}
const storedExecutingState = await GM_getValue(Config.DB_KEYS.IS_EXECUTING, false);
if (State.isExecuting !== storedExecutingState) {
Utils.logger("info", Utils.getText("log_execution_state_inconsistent", storedExecutingState ? "\u6267\u884C\u4E2D" : "\u5DF2\u505C\u6B62"));
State.isExecuting = storedExecutingState;
}
await RateLimitManager.initStatus();
setupRequestInterceptors();
await PagePatcher.init();
const tempTasks = await GM_getValue("temp_todo_tasks", null);
if (tempTasks && tempTasks.length > 0) {
Utils.logger("info", `\u4ECE429\u6062\u590D\uFF1A\u627E\u5230 ${tempTasks.length} \u4E2A\u4E34\u65F6\u4FDD\u5B58\u7684\u5F85\u529E\u4EFB\u52A1\uFF0C\u6B63\u5728\u6062\u590D...`);
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: workerId2, success, task, logs, instanceId, executionTime } = newValue;
if (instanceId !== Config.INSTANCE_ID) {
Utils.logger("info", `\u6536\u5230\u6765\u81EA\u5176\u4ED6\u5B9E\u4F8B [${instanceId}] \u7684\u5DE5\u4F5C\u62A5\u544A\uFF0C\u5F53\u524D\u5B9E\u4F8B [${Config.INSTANCE_ID}] \u5C06\u5FFD\u7565\u3002`);
return;
}
if (!workerId2 || !task) {
Utils.logger("error", "\u6536\u5230\u65E0\u6548\u7684\u5DE5\u4F5C\u62A5\u544A\u3002\u7F3A\u5C11workerId\u6216task\u3002");
return;
}
if (State.runningWorkers[workerId2]) {
delete State.runningWorkers[workerId2];
State.activeWorkers--;
}
if (logs && logs.length) {
logs.forEach((log) => {
Utils.logger("debug", log);
});
}
const isZh = State.lang === "zh";
const timeSuffix = executionTime ? ` (${Utils.getText("task_execution_time", (executionTime / 1e3).toFixed(2))})` : "";
if (success) {
const successMsg = isZh ? `\u2705 \u4EFB\u52A1\u5B8C\u6210: ${task.name}` : `\u2705 Task completed: ${task.name}`;
Utils.logger("info", successMsg + timeSuffix);
await Database.markAsDone(task);
State.sessionCompleted.add(Database.normalizeListingUrl(task.url));
State.executionCompletedTasks++;
} else {
const errorLog = logs && logs.length ? logs.find((log) => log.includes(Utils.getText("worker_captcha"))) || logs.find((log) => log.includes("Error") || log.includes("Timeout") || log.includes("failed") || log.includes("Critical")) || logs[logs.length - 1] : isZh ? "\u5DE5\u4F5C\u6807\u7B7E\u9875\u62A5\u544A\u5931\u8D25" : "Worker tab reported failure";
const cleanError = errorLog ? errorLog.replace(/^\[[a-f0-9-]+\]\s*/i, "") : isZh ? "\u672A\u77E5\u539F\u56E0" : "Unknown reason";
const failMsg = isZh ? `\u274C \u4EFB\u52A1\u5931\u8D25: ${task.name} (${cleanError})` : `\u274C Task failed: ${task.name} (${cleanError})`;
Utils.logger("warn", failMsg + timeSuffix);
const _failRes = await Database.markAsFailed(task, {
reason: cleanError,
logs: logs || [],
details: {
executionTime: executionTime ? `${(executionTime / 1e3).toFixed(2)}s` : "\u672A\u77E5",
workerId: workerId2,
instanceId
}
});
if (!_failRes || !_failRes.retried) State.executionFailedTasks++;
}
UI5.update();
if (State.isExecuting && State.activeWorkers < Config.MAX_CONCURRENT_WORKERS && State.db.todo.length > 0) {
setTimeout(() => TaskRunner2.executeBatch(), 200);
}
if (State.isExecuting && State.db.todo.length === 0 && State.activeWorkers === 0) {
if (State.autoAddOnScroll) {
TaskRunner2.attemptAutoScroll();
} else {
await TaskRunner2.stopExecutionAndSettle();
}
}
TaskRunner2.runHideOrShow();
} catch (error) {
Utils.logger("error", `\u5904\u7406\u5DE5\u4F5C\u62A5\u544A\u65F6\u51FA\u9519: ${error.message}`);
}
}));
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;
UI5.update();
}
}));
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...");
(async () => {
try {
await runDomDependentPart();
} catch (e) {
Utils.logger("error", `[Launcher] Error in runDomDependentPart: ${e.message}`);
console.error("[Fab Helper] runDomDependentPart error:", e);
State.hasRunDomPart = true;
}
})();
}
if (State.hasRunDomPart) {
clearInterval(launcherInterval);
window._fabHelperLauncherActive = false;
Utils.logger("debug", "[Launcher] Main logic has been launched or skipped. Launcher is now idle.");
}
}
}, 500);
}
let lastNetworkActivityTime = Date.now();
window.recordNetworkActivity = function() {
lastNetworkActivityTime = Date.now();
};
if (typeof document !== "undefined") {
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
lastNetworkActivityTime = Date.now();
Utils.logger("debug", "[Visibility] \u9875\u9762\u91CD\u65B0\u53EF\u89C1\uFF0C\u91CD\u7F6E\u65E0\u7F51\u7EDC\u6D3B\u52A8\u8BA1\u65F6\u3002");
}
});
}
setInterval(() => {
if (State.appStatus === "NORMAL" && State.isExecuting && (State.db.todo.length > 0 || State.activeWorkers > 0)) {
const inactiveTime = Date.now() - lastNetworkActivityTime;
if (inactiveTime > 3e4) {
Utils.logger("warn", `\u26A0\uFE0F \u68C0\u6D4B\u5230\u5728\u4EFB\u52A1\u6267\u884C\u72B6\u6001\u4E0B\u5DF2\u6301\u7EED ${Math.floor(inactiveTime / 1e3)} \u79D2\u65E0\u7F51\u7EDC\u6D3B\u52A8\uFF0C\u5373\u5C06\u5F3A\u5236\u5237\u65B0\u9875\u9762\u4EE5\u6062\u590D\u4EFB\u52A1...`);
setTimeout(() => {
window.location.reload();
}, 1500);
}
}
}, 5e3);
Utils.logger("info", Utils.getText("log_init"));
}
__name(main, "main");
window.addEventListener("beforeunload", () => {
InstanceManager.cleanup();
Utils.cleanup();
});
window.addEventListener("load", () => {
setTimeout(ensureUILoaded, 2e3);
});
async function handleWakeRecovery() {
if (State.isWorkerTab) return;
if (!State.isExecuting && State.db.todo.length === 0) return;
Utils.logger("info", Utils.getText("log_wake_recovery"));
await RateLimitManager.checkInactivity(false);
const cleaned = await TaskRunner2.checkStalledWorkers();
if (cleaned > 0) {
Utils.logger("warn", Utils.getText("log_wake_cleanup_stale", cleaned));
}
if (State.db.todo.length > 0) {
if (!State.isExecuting) {
Utils.logger("info", Utils.getText("log_wake_restarting", State.db.todo.length));
TaskRunner2.startExecution();
} else if (State.activeWorkers < Config.MAX_CONCURRENT_WORKERS) {
Utils.logger("info", Utils.getText("log_wake_restarting", State.db.todo.length));
TaskRunner2.executeBatch();
}
}
}
__name(handleWakeRecovery, "handleWakeRecovery");
var wakeRecoveryTimer = null;
var triggerWakeRecovery = /* @__PURE__ */ __name(() => {
clearTimeout(wakeRecoveryTimer);
wakeRecoveryTimer = setTimeout(handleWakeRecovery, 1e3);
}, "triggerWakeRecovery");
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
setTimeout(ensureUILoaded, 500);
triggerWakeRecovery();
}
});
window.addEventListener("focus", () => {
triggerWakeRecovery();
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", main);
} else {
main();
}
})();