(function() { if (window.magnetPluginInitialized) return; window.magnetPluginInitialized = true; var DEBUG_MODE = localStorage.getItem('magnetDebugMode') === 'true'; function log(msg) { if (DEBUG_MODE) console.log('[MagnetPlugin] ' + msg); } function escapeHtml(text) { return String(text || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function ensurePanelStyles() { if (document.getElementById('magnet-panel-style')) { return; } var style = document.createElement('style'); style.id = 'magnet-panel-style'; style.textContent = [ /* === 字体引入 === */ '@import url("https://fonts.googleapis.com/css2?family=Rajdhani:wght@500;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap");', /* === CSS 变量 === */ ':root {', ' --m-bg-deep: #0a0e14;', ' --m-bg-primary: #0f1419;', ' --m-bg-secondary: #1a1f2e;', ' --m-bg-card: rgba(26, 31, 46, 0.92);', ' --m-bg-card-hover: rgba(35, 41, 58, 0.95);', ' --m-accent: #00d4aa;', ' --m-accent-glow: rgba(0, 212, 170, 0.4);', ' --m-accent-secondary: #a78bfa;', ' --m-text-primary: #f0f4f8;', ' --m-text-secondary: #8892a4;', ' --m-text-muted: #5c6578;', ' --m-border: rgba(255, 255, 255, 0.06);', ' --m-border-accent: rgba(0, 212, 170, 0.3);', ' --m-success: #10b981;', ' --m-error: #ef4444;', ' --m-warning: #f59e0b;', ' --m-font-display: "Rajdhani", "Microsoft YaHei", sans-serif;', ' --m-font-body: "Noto Sans SC", "Microsoft YaHei", sans-serif;', ' --m-radius-sm: 8px;', ' --m-radius-md: 12px;', ' --m-radius-lg: 16px;', ' --m-radius-xl: 20px;', ' --m-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);', ' --m-shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4);', ' --m-shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.5);', ' --m-shadow-glow: 0 0 20px var(--m-accent-glow), 0 0 40px rgba(0, 212, 170, 0.15);', '}', /* === 悬浮球 === */ '#magnet-float-ball{', ' position:fixed;bottom:24px;right:24px;', ' width:60px;height:60px;', ' background:linear-gradient(135deg, #0f1419 0%, #1a1f2e 100%);', ' border:2px solid var(--m-accent);', ' border-radius:50%;', ' box-shadow:var(--m-shadow-glow), inset 0 0 20px rgba(0, 212, 170, 0.1);', ' z-index:2147483647;', ' cursor:pointer;', ' display:flex;align-items:center;justify-content:center;', ' color:var(--m-accent);', ' font-size:24px;', ' font-family:var(--m-font-display);', ' font-weight:700;', ' transition:all 0.3s cubic-bezier(0.4, 0, 0.2, 1);', ' animation:magnet-pulse 2.5s ease-in-out infinite;', '}', '#magnet-float-ball:hover{', ' transform:scale(1.08);', ' box-shadow:0 0 30px var(--m-accent-glow), 0 0 60px rgba(0, 212, 170, 0.2), inset 0 0 30px rgba(0, 212, 170, 0.15);', '}', '#magnet-float-ball:active{transform:scale(0.95);}', /* === 脉冲动画 === */ '@keyframes magnet-pulse{', ' 0%, 100%{box-shadow:var(--m-shadow-glow), inset 0 0 20px rgba(0, 212, 170, 0.1);}', ' 50%{box-shadow:0 0 30px var(--m-accent-glow), 0 0 50px rgba(0, 212, 170, 0.2), inset 0 0 25px rgba(0, 212, 170, 0.15);}', '}', /* === 主面板 === */ '#magnet-floating-panel{', ' position:fixed;right:20px;bottom:20px;', ' width:min(800px, calc(100vw - 40px));', ' height:min(85vh, 860px);', ' background:var(--m-bg-primary);', ' border:1px solid var(--m-border-accent);', ' border-radius:var(--m-radius-xl);', ' box-shadow:var(--m-shadow-lg), 0 0 80px rgba(0, 212, 170, 0.08);', ' z-index:2147483647;', ' font-family:var(--m-font-body);', ' font-size:13px;', ' color:var(--m-text-primary);', ' display:none;flex-direction:column;', ' overflow:hidden;', ' backdrop-filter:blur(20px);', ' animation:magnet-panel-in 0.4s cubic-bezier(0.4, 0, 0.2, 1);', '}', '@keyframes magnet-panel-in{', ' from{opacity:0;transform:translateY(20px) scale(0.98);}', ' to{opacity:1;transform:translateY(0) scale(1);}', '}', /* === 面板头部 === */ '#magnet-floating-panel .magnet-panel-header{', ' display:flex;justify-content:space-between;align-items:flex-start;', ' padding:20px 24px 16px;', ' background:linear-gradient(180deg, rgba(0, 212, 170, 0.08) 0%, transparent 100%);', ' border-bottom:1px solid var(--m-border);', ' gap:16px;', '}', '#magnet-floating-panel .magnet-panel-brand{', ' display:flex;flex-direction:column;gap:6px;min-width:0;', '}', '#magnet-floating-panel .magnet-panel-title{', ' font-family:var(--m-font-display);', ' font-size:24px;font-weight:700;', ' letter-spacing:1px;', ' background:linear-gradient(135deg, var(--m-accent) 0%, #00f5c4 50%, var(--m-accent-secondary) 100%);', ' -webkit-background-clip:text;', ' -webkit-text-fill-color:transparent;', ' background-clip:text;', '}', '#magnet-floating-panel .magnet-panel-subtitle{', ' font-size:12px;color:var(--m-text-secondary);line-height:1.5;', '}', '#magnet-floating-panel .magnet-panel-head-actions{', ' display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end;', '}', /* === 切换按钮 === */ '#magnet-floating-panel .magnet-panel-switch{', ' padding:10px 18px;', ' border:1px solid var(--m-border);', ' border-radius:var(--m-radius-lg);', ' background:var(--m-bg-card);', ' color:var(--m-text-secondary);', ' cursor:pointer;', ' font-family:var(--m-font-display);', ' font-size:13px;font-weight:600;', ' letter-spacing:0.5px;', ' transition:all 0.25s cubic-bezier(0.4, 0, 0.2, 1);', '}', '#magnet-floating-panel .magnet-panel-switch:hover{', ' background:var(--m-bg-card-hover);', ' border-color:var(--m-accent);', ' color:var(--m-text-primary);', '}', '#magnet-floating-panel .magnet-panel-switch.is-active{', ' background:linear-gradient(135deg, rgba(0, 212, 170, 0.2) 0%, rgba(0, 212, 170, 0.1) 100%);', ' border-color:var(--m-accent);', ' color:var(--m-accent);', ' box-shadow:0 0 15px rgba(0, 212, 170, 0.2);', '}', /* === 关闭按钮 === */ '#magnet-floating-panel .magnet-panel-close{', ' width:36px;height:36px;', ' border:1px solid var(--m-border);', ' border-radius:var(--m-radius-md);', ' background:var(--m-bg-card);', ' color:var(--m-text-secondary);', ' cursor:pointer;', ' font-size:20px;line-height:1;', ' transition:all 0.2s ease;', '}', '#magnet-floating-panel .magnet-panel-close:hover{', ' background:rgba(239, 68, 68, 0.15);', ' border-color:var(--m-error);', ' color:var(--m-error);', '}', /* === 设置区域 === */ '#magnet-settings{', ' padding:16px 20px;', ' background:var(--m-bg-secondary);', ' border-bottom:1px solid var(--m-border);', ' display:flex;flex-direction:column;gap:12px;', '}', '#magnet-floating-panel .magnet-control-row{', ' display:flex;gap:12px;align-items:center;flex-wrap:wrap;', '}', '#magnet-floating-panel .magnet-control-row > *{min-width:0;}', /* === 输入框样式 === */ '#magnet-settings input[type="text"],', '#magnet-settings input[type="number"],', '#magnet-settings select{', ' padding:10px 14px;', ' background:var(--m-bg-primary);', ' border:1px solid var(--m-border);', ' border-radius:var(--m-radius-md);', ' color:var(--m-text-primary);', ' font-family:var(--m-font-body);', ' font-size:13px;', ' transition:all 0.2s ease;', '}', '#magnet-settings input:focus,', '#magnet-settings select:focus{', ' outline:none;', ' border-color:var(--m-accent);', ' box-shadow:0 0 0 3px rgba(0, 212, 170, 0.15);', '}', '#magnet-settings input::placeholder{color:var(--m-text-muted);}', /* === 主按钮 === */ '#magnet-settings button:not(.magnet-panel-switch){', ' padding:10px 20px;', ' background:linear-gradient(135deg, var(--m-accent) 0%, #00f5c4 100%);', ' border:none;', ' border-radius:var(--m-radius-md);', ' color:var(--m-bg-deep);', ' font-family:var(--m-font-display);', ' font-size:14px;font-weight:700;', ' letter-spacing:0.5px;', ' cursor:pointer;', ' transition:all 0.25s cubic-bezier(0.4, 0, 0.2, 1);', '}', '#magnet-settings button:not(.magnet-panel-switch):hover{', ' transform:translateY(-2px);', ' box-shadow:0 8px 25px rgba(0, 212, 170, 0.4);', '}', '#magnet-settings button:not(.magnet-panel-switch):active{', ' transform:translateY(0);', '}', /* === 内容区域 === */ '#magnet-floating-panel .magnet-panel-content{', ' flex:1;min-height:0;', ' padding:16px 20px;', ' background:var(--m-bg-primary);', ' overflow:hidden;', '}', '#magnet-floating-panel .magnet-view{', ' display:none;height:100%;flex-direction:column;gap:14px;min-height:0;', '}', '#magnet-floating-panel .magnet-view.is-active{display:flex;}', /* === 视图工具栏 === */ '#magnet-floating-panel .magnet-view-toolbar{', ' display:flex;justify-content:space-between;align-items:center;', ' gap:12px;padding:14px 18px;', ' background:var(--m-bg-card);', ' border:1px solid var(--m-border);', ' border-radius:var(--m-radius-lg);', '}', '#magnet-floating-panel .magnet-view-toolbar-actions{', ' display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end;', '}', '#magnet-floating-panel .magnet-view-title{', ' font-family:var(--m-font-display);', ' font-size:16px;font-weight:700;', ' color:var(--m-text-primary);', ' letter-spacing:0.5px;', '}', '#magnet-floating-panel .magnet-view-meta{', ' font-size:12px;color:var(--m-text-secondary);', '}', /* === 磁力列表 === */ '#magnet-list{', ' display:flex;flex-direction:column;gap:10px;', ' min-height:0;overflow-y:auto;padding-right:6px;', ' scrollbar-width:thin;scrollbar-color:var(--m-accent) var(--m-bg-secondary);', '}', '#magnet-list::-webkit-scrollbar{width:6px;}', '#magnet-list::-webkit-scrollbar-track{background:var(--m-bg-secondary);border-radius:3px;}', '#magnet-list::-webkit-scrollbar-thumb{background:var(--m-accent);border-radius:3px;}', /* === 列表项 === */ '.magnet-item{', ' display:flex;align-items:flex-start;gap:14px;', ' padding:14px 16px;', ' background:var(--m-bg-card);', ' border:1px solid var(--m-border);', ' border-radius:var(--m-radius-lg);', ' transition:all 0.25s cubic-bezier(0.4, 0, 0.2, 1);', ' animation:magnet-item-in 0.3s ease forwards;', ' opacity:0;', '}', '@keyframes magnet-item-in{', ' from{opacity:0;transform:translateX(-10px);}', ' to{opacity:1;transform:translateX(0);}', '}', '.magnet-item:hover{', ' background:var(--m-bg-card-hover);', ' border-color:var(--m-border-accent);', ' transform:translateX(4px);', '}', '.magnet-title{', ' flex:1;cursor:pointer;', ' color:var(--m-text-primary);', ' min-width:0;font-size:13px;line-height:1.6;', ' word-break:break-all;font-weight:500;', ' transition:color 0.2s ease;', '}', '.magnet-title:hover{color:var(--m-accent);}', /* === 复制按钮 === */ '.magnet-copy-btn{', ' padding:8px 16px;', ' background:linear-gradient(135deg, var(--m-accent) 0%, #00f5c4 100%);', ' color:var(--m-bg-deep);', ' border:none;border-radius:var(--m-radius-md);', ' cursor:pointer;', ' font-family:var(--m-font-display);', ' font-size:12px;font-weight:700;', ' white-space:nowrap;flex-shrink:0;', ' transition:all 0.2s ease;', '}', '.magnet-copy-btn:hover{', ' transform:scale(1.05);', ' box-shadow:0 4px 15px rgba(0, 212, 170, 0.4);', '}', '.magnet-copy-btn:active{transform:scale(0.98);}', /* === 缓存面板 === */ '#magnet-cache-panel{', ' flex:1;min-height:0;overflow-y:auto;padding-right:6px;', ' scrollbar-width:thin;scrollbar-color:var(--m-accent) var(--m-bg-secondary);', '}', '#magnet-cache-panel::-webkit-scrollbar{width:6px;}', '#magnet-cache-panel::-webkit-scrollbar-track{background:var(--m-bg-secondary);border-radius:3px;}', '#magnet-cache-panel::-webkit-scrollbar-thumb{background:var(--m-accent);border-radius:3px;}', /* === 缓存网格 === */ '.magnet-cache-grid{', ' display:grid;', ' grid-template-columns:repeat(auto-fit, minmax(160px, 1fr));', ' gap:12px;margin-bottom:16px;', '}', '.magnet-cache-card{', ' padding:16px;', ' background:var(--m-bg-card);', ' border:1px solid var(--m-border);', ' border-radius:var(--m-radius-lg);', ' transition:all 0.2s ease;', '}', '.magnet-cache-card:hover{', ' border-color:var(--m-border-accent);', '}', '.magnet-cache-card-label{', ' font-size:11px;color:var(--m-text-muted);margin-bottom:8px;', ' text-transform:uppercase;letter-spacing:0.5px;', '}', '.magnet-cache-card-value{', ' font-family:var(--m-font-display);', ' font-size:22px;font-weight:700;', ' color:var(--m-accent);', '}', /* === 缓存区块 === */ '.magnet-cache-section{margin-top:16px;}', '.magnet-cache-section-title{', ' font-family:var(--m-font-display);', ' font-size:13px;font-weight:700;', ' color:var(--m-text-secondary);', ' margin-bottom:10px;letter-spacing:0.5px;', '}', '.magnet-cache-entry{', ' padding:12px 14px;margin-top:8px;', ' background:var(--m-bg-card);', ' border:1px solid var(--m-border);', ' border-radius:var(--m-radius-md);', ' transition:all 0.2s ease;', '}', '.magnet-cache-entry:hover{', ' border-color:var(--m-border-accent);', '}', '.magnet-cache-entry-title{', ' font-size:13px;font-weight:600;', ' color:var(--m-text-primary);', ' line-height:1.5;word-break:break-all;', '}', '.magnet-cache-entry-meta{', ' font-size:11px;color:var(--m-text-muted);', ' margin-top:6px;line-height:1.5;', '}', /* === 底部 === */ '#magnet-floating-panel .magnet-panel-footer{', ' padding:16px 20px 20px;', ' background:var(--m-bg-secondary);', ' border-top:1px solid var(--m-border);', ' display:flex;flex-direction:column;gap:12px;', '}', /* === 状态栏 === */ '#magnet-status{', ' padding:12px 16px;border-radius:var(--m-radius-md);', ' font-size:12px;line-height:1.6;', ' background:var(--m-bg-card);', ' border:1px solid var(--m-border);', ' color:var(--m-text-secondary);', '}', '#magnet-status[data-type="loading"]{', ' border-color:var(--m-warning);', ' color:var(--m-warning);', ' background:rgba(245, 158, 11, 0.1);', '}', '#magnet-status[data-type="error"]{', ' border-color:var(--m-error);', ' color:var(--m-error);', ' background:rgba(239, 68, 68, 0.1);', '}', '#magnet-status[data-type="done"]{', ' border-color:var(--m-success);', ' color:var(--m-success);', ' background:rgba(16, 185, 129, 0.1);', '}', /* === 一键复制按钮 === */ '#magnet-copy-all{', ' width:100%;padding:14px 20px;', ' background:linear-gradient(135deg, var(--m-accent) 0%, #00f5c4 100%);', ' color:var(--m-bg-deep);', ' border:none;border-radius:var(--m-radius-lg);', ' cursor:pointer;', ' font-family:var(--m-font-display);', ' font-size:15px;font-weight:700;', ' letter-spacing:0.5px;', ' transition:all 0.25s cubic-bezier(0.4, 0, 0.2, 1);', '}', '#magnet-copy-all:hover{', ' transform:translateY(-2px);', ' box-shadow:0 10px 30px rgba(0, 212, 170, 0.4);', '}', '#magnet-copy-all:active{transform:translateY(0);}', /* === 调试菜单 === */ '#magnet-debug-menu{', ' background:var(--m-bg-card) !important;', ' border:1px solid var(--m-border-accent) !important;', ' border-radius:var(--m-radius-md) !important;', ' box-shadow:var(--m-shadow-lg) !important;', '}', '#magnet-debug-menu label{', ' color:var(--m-text-primary) !important;', '}', '#magnet-debug-menu input[type="checkbox"]{', ' accent-color:var(--m-accent);', '}', /* === 响应式 === */ '@media (max-width: 900px){', ' #magnet-floating-panel{', ' right:10px;bottom:10px;', ' width:calc(100vw - 20px);', ' height:min(88vh, 800px);', ' }', ' #magnet-floating-panel .magnet-panel-header{padding:16px 18px 14px;}', ' #magnet-settings,', ' #magnet-floating-panel .magnet-panel-content,', ' #magnet-floating-panel .magnet-panel-footer{padding-left:16px;padding-right:16px;}', '}', /* === 空状态 === */ '.magnet-empty-state{', ' display:flex;flex-direction:column;align-items:center;justify-content:center;', ' padding:40px 20px;text-align:center;', ' color:var(--m-text-muted);', '}', '.magnet-empty-state-icon{', ' font-size:48px;margin-bottom:16px;opacity:0.5;', '}', '.magnet-empty-state-text{', ' font-size:14px;line-height:1.6;', '}', '/* === 进度条 === */', '.magnet-progress-container{', ' width:100%;height:6px;background:var(--m-bg-secondary);border-radius:3px;overflow:hidden;margin-top:8px;', '}', '.magnet-progress-bar{', ' height:100%;background:linear-gradient(90deg, var(--m-accent), #00f5c4);border-radius:3px;transition:width 0.3s ease;', '}', '.magnet-progress-text{', ' display:flex;justify-content:space-between;font-size:11px;color:var(--m-text-muted);margin-top:4px;', '}', '/* === 收藏按钮 === */', '.magnet-favorite-btn{', ' padding:6px 10px;background:transparent;border:1px solid var(--m-border);border-radius:8px;cursor:pointer;font-size:11px;color:var(--m-text-muted);transition:all 0.2s ease;', '}', '.magnet-favorite-btn:hover{', ' border-color:var(--m-accent-secondary);color:var(--m-accent-secondary);', '}', '.magnet-favorite-btn.is-favorite{', ' background:rgba(167,139,250,0.15);border-color:var(--m-accent-secondary);color:var(--m-accent-secondary);', '}', '/* === 收藏视图 === */', '#magnet-favorites-view .magnet-favorite-item{', ' display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--m-bg-card);border:1px solid var(--m-border);border-radius:var(--m-radius-md);margin-bottom:8px;', '}', '#magnet-favorites-view .magnet-favorite-item:hover{', ' border-color:var(--m-border-accent);', '}', '#magnet-favorites-view .magnet-favorite-title{', ' flex:1;font-size:12px;color:var(--m-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;', '}', '#magnet-favorites-view .magnet-favorite-actions{', ' display:flex;gap:6px;', '}', '#magnet-favorites-view .magnet-favorite-copy,', '#magnet-favorites-view .magnet-favorite-remove{', ' padding:5px 8px;border:none;border-radius:6px;cursor:pointer;font-size:10px;font-weight:600;transition:all 0.2s ease;', '}', '#magnet-favorites-view .magnet-favorite-copy{', ' background:linear-gradient(135deg, var(--m-accent), #00f5c4);color:var(--m-bg-deep);', '}', '#magnet-favorites-view .magnet-favorite-remove{', ' background:rgba(239,68,68,0.15);color:var(--m-error);', '}', '/* === 吜索记录下拉 === */', '.magnet-history-dropdown{', ' position:absolute;top:100%;left:0;right:0;background:var(--m-bg-card);border:1px solid var(--m-border);border-radius:var(--m-radius-md);box-shadow:var(--m-shadow-lg);z-index:100;max-height:200px;overflow-y:auto;', '}', '.magnet-history-item{', ' padding:10px 14px;cursor:pointer;font-size:12px;color:var(--m-text-primary);transition:background 0.2s ease;', '}', '.magnet-history-item:hover{', ' background:var(--m-bg-secondary);', '}', '.magnet-history-item:first-child{', ' border-radius:var(--m-radius-md) var(--m-radius-md) 0 0;', '}', '.magnet-history-item:last-child{', ' border-radius:0 0 var(--m-radius-md) var(--m-radius-md);', '}', '.magnet-history-clear{', ' padding:10px 14px;border-top:1px solid var(--m-border);font-size:11px;color:var(--m-error);cursor:pointer;text-align:center;', '}' ].join(''); document.head.appendChild(style); } function setPanelView(viewName) { var resultsView = document.getElementById('magnet-results-view'); var cacheView = document.getElementById('magnet-cache-view'); var favoritesView = document.getElementById('magnet-favorites-view'); var resultsBtn = document.getElementById('magnet-view-results'); var cacheBtn = document.getElementById('magnet-view-cache'); var favoritesBtn = document.getElementById('magnet-view-favorites'); if (!resultsView || !cacheView || !favoritesView || !resultsBtn || !cacheBtn || !favoritesBtn) { return; } resultsView.classList.toggle('is-active', viewName === 'results'); cacheView.classList.toggle('is-active', viewName === 'cache'); favoritesView.classList.toggle('is-active', viewName === 'favorites'); resultsBtn.classList.toggle('is-active', viewName === 'results'); cacheBtn.classList.toggle('is-active', viewName === 'cache'); favoritesBtn.classList.toggle('is-active', viewName === 'favorites'); if (viewName === 'favorites') { renderFavoritesList(); } } function createFloatingPanel() { var existing = document.getElementById('magnet-floating-panel'); var ball = document.createElement('div'); ball.id = 'magnet-float-ball'; ball.innerHTML = ''; ball.oncontextmenu = function(e) { e.preventDefault(); var menu = document.getElementById('magnet-debug-menu'); if (menu) menu.remove(); menu = document.createElement('div'); menu.id = 'magnet-debug-menu'; menu.style.cssText = 'position:fixed;bottom:90px;right:28px;background:var(--m-bg-card);border:1px solid var(--m-border-accent);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,0.5);z-index:999999;padding:8px 0;font-size:12px;font-family:var(--m-font-body);'; menu.innerHTML = ''; document.body.appendChild(menu); menu.querySelector('input').onchange = function() { DEBUG_MODE = this.checked; localStorage.setItem('magnetDebugMode', DEBUG_MODE); }; setTimeout(function() { document.addEventListener('click', function hideMenu() { menu.remove(); document.removeEventListener('click', hideMenu); }); }, 100); }; document.body.appendChild(ball); var panel = document.createElement('div'); panel.id = 'magnet-floating-panel'; panel.innerHTML = '
MAGNET LINKS
智能抓取 · 缓存加速 · 一键复制
搜索结果
关键词命中的磁力链接
0
我的收藏
持久保存的磁力链接
缓存总览
数据统计与快照管理
'; document.body.appendChild(panel); setPanelView('results'); ball.onclick = function() { panel.style.display = 'flex'; ball.style.display = 'none'; }; var resultsSwitch = panel.querySelector('#magnet-view-results'); if (resultsSwitch) { resultsSwitch.onclick = function() { setPanelView('results'); }; } var cacheSwitch = panel.querySelector('#magnet-view-cache'); if (cacheSwitch) { cacheSwitch.onclick = function() { setPanelView('cache'); refreshCacheOverview({ showStatus: true }); }; } var refreshCacheBtn = panel.querySelector('#magnet-refresh-cache'); if (refreshCacheBtn) { refreshCacheBtn.onclick = function() { refreshCacheOverview({ showStatus: true }); }; } var clearCacheInlineBtn = panel.querySelector('#magnet-clear-cache-inline'); if (clearCacheInlineBtn) { clearCacheInlineBtn.onclick = clearAllCacheWithConfirm; } var closeBtn = panel.querySelector('.magnet-panel-close'); if (closeBtn) closeBtn.onclick = function() { panel.style.display = 'none'; ball.style.display = 'flex'; }; var favoritesSwitch = panel.querySelector('#magnet-view-favorites'); if (favoritesSwitch) { favoritesSwitch.onclick = function() { setPanelView('favorites'); }; } var clearFavoritesBtn = panel.querySelector('#magnet-clear-favorites'); if (clearFavoritesBtn) { clearFavoritesBtn.onclick = function() { if (confirm('确定要清空所有收藏吗')) { saveFavorites([]); renderFavoritesList(); } }; } var copyAllBtn = panel.querySelector('#magnet-copy-all'); if (copyAllBtn) { copyAllBtn.onclick = function() { var links = allMagnetLinks.length > 0 ? allMagnetLinks.slice() : Array.from(document.querySelectorAll('.magnet-item .magnet-copy-btn')) .map(function(btn) { return btn.getAttribute('data-magnet'); }) .filter(function(link) { return !!link; }); if (links.length === 0) { alert('暂无可复制的磁力链接'); return; } var allLinks = links.join('\n'); navigator.clipboard.writeText(allLinks) .then(function() { alert('已复制 ' + links.length + ' 个磁力链接!'); }) .catch(function(err) { var errorMsg = err && err.message ? err.message : '复制失败'; alert('复制失败: ' + errorMsg); }); }; } return panel; var copyAllBtn = panel.querySelector('#magnet-copy-all'); if (copyAllBtn) { copyAllBtn.onclick = function() { var links = allMagnetLinks.length > 0 ? allMagnetLinks.slice() : Array.from(document.querySelectorAll('.magnet-item .magnet-copy-btn')) .map(function(btn) { return btn.getAttribute('data-magnet'); }) .filter(function(link) { return !!link; }); if (links.length === 0) { alert('暂无可复制的磁力链接'); return; } var allLinks = links.join('\n'); navigator.clipboard.writeText(allLinks) .then(function() { alert('已复制 ' + links.length + ' 个磁力链接!'); }) .catch(function(err) { var errorMsg = err && err.message ? err.message : '复制失败'; alert('复制失败:' + errorMsg); }); }; } return panel; } function isListPage() { return document.querySelector('#threadlisttableid') !== null; } function getCurrentPage() { var match = window.location.href.match(/[?&]page=(\d+)/); if (match) return parseInt(match[1]); match = window.location.href.match(/forum-\d+-(\d+)\.html/); if (match) return parseInt(match[1]); return 1; } function getForumOrigin(rawUrl) { var sourceUrl = typeof rawUrl === 'string' && rawUrl ? rawUrl : window.location.href; var match = sourceUrl.match(/^(https?:\/\/[^\/]+)\/?/i); if (match) return match[1]; return window.location.origin || 'https://www.sehuatang.net'; } function getForumIdFromUrl(rawUrl) { var sourceUrl = typeof rawUrl === 'string' && rawUrl ? rawUrl : window.location.href; var match = sourceUrl.match(/[?&]fid=(\d+)/i); if (match) return match[1]; match = sourceUrl.match(/forum-(\d+)(?:-|\.html)/i); if (match) return match[1]; return '2'; } function getForumKey() { return getForumOrigin(window.location.href) + '|fid:' + getForumIdFromUrl(window.location.href); } function getBaseUrl() { return getForumOrigin(window.location.href) + '/forum-' + getForumIdFromUrl(window.location.href) + '-'; } function normalizeThreadUrl(url) { if (typeof url !== 'string' || !url) { return ''; } try { var parsed = new URL(url, window.location.origin); parsed.hash = ''; return parsed.href; } catch (e) { return url; } } function getThreadKeyFromUrl(url) { var normalizedUrl = normalizeThreadUrl(url); if (!normalizedUrl) { return ''; } var match = normalizedUrl.match(/thread-(\d+)-/i) || normalizedUrl.match(/[?&]tid=(\d+)/i); return match ? match[1] : normalizedUrl; } function normalizeCachedThreads(threads) { var result = []; var seen = Object.create(null); (Array.isArray(threads) ? threads : []).forEach(function(thread) { if (!thread || typeof thread !== 'object') return; var normalizedUrl = normalizeThreadUrl(thread.url); var normalizedTitle = typeof thread.title === 'string' ? thread.title.replace(/\s+/g, ' ').trim() : ''; var threadKey = typeof thread.threadKey === 'string' && thread.threadKey ? thread.threadKey : getThreadKeyFromUrl(normalizedUrl); if (!normalizedUrl || !threadKey || seen[threadKey]) return; seen[threadKey] = true; result.push({ url: normalizedUrl, title: normalizedTitle, threadKey: threadKey }); }); return result; } function normalizeMagnetList(magnets) { var seen = Object.create(null); var result = []; (Array.isArray(magnets) ? magnets : []).forEach(function(magnet) { if (typeof magnet !== 'string' || !magnet) return; if (seen[magnet]) return; seen[magnet] = true; result.push(magnet); }); return result; } function extractThreadsFromHtml(html) { var parser = new DOMParser(); var doc = parser.parseFromString(html, 'text/html'); var threads = []; var tbodies = doc.querySelectorAll('#threadlisttableid tbody[id^="normalthread_"]'); tbodies.forEach(function(tbody) { var link = tbody.querySelector('th a[href*="thread-"]'); var title = tbody.querySelector('th a.xst') || tbody.querySelector('th .xst'); var titleText = title ? title.textContent : ''; var normalizedUrl = link && link.href ? normalizeThreadUrl(link.href) : ''; if (normalizedUrl) { threads.push({ url: normalizedUrl, title: titleText, threadKey: getThreadKeyFromUrl(normalizedUrl) }); } }); return normalizeCachedThreads(threads); } function updateStatus(message, type) { var status = document.getElementById('magnet-status'); if (status) { status.textContent = message; status.setAttribute('data-type', type || ''); // 默认样式 status.style.background = 'rgba(26,31,46,0.92)'; status.style.color = '#8892a4'; status.style.border = '1px solid rgba(255,255,255,0.06)'; if (type === 'loading') { status.style.background = 'rgba(245,158,11,0.1)'; status.style.color = '#f59e0b'; status.style.borderColor = 'rgba(245,158,11,0.3)'; } else if (type === 'error') { status.style.background = 'rgba(239,68,68,0.1)'; status.style.color = '#ef4444'; status.style.borderColor = 'rgba(239,68,68,0.3)'; } else if (type === 'done') { status.style.background = 'rgba(16,185,129,0.1)'; status.style.color = '#10b981'; status.style.borderColor = 'rgba(16,185,129,0.3)'; } if (type && type !== 'loading') { scheduleStatePersist(); } } } function updateCount(count) { var countEl = document.getElementById('magnet-count-num'); if (countEl) countEl.textContent = count; } // === 进度条功能 === function updateProgress(current, total, label) { var progressBar = document.getElementById('magnet-progress-bar'); var progressLabel = document.getElementById('magnet-progress-label'); var progressPercent = document.getElementById('magnet-progress-percent'); if (progressBar) { var percent = total > 0 ? Math.round((current / total) * 100) : 0; progressBar.style.width = percent + '%'; } if (progressLabel) { progressLabel.textContent = label || ('进度: ' + current + '/' + total); } if (progressPercent) { var percent = total > 0 ? Math.round((current / total) * 100) : 0; progressPercent.textContent = percent + '%'; } } function resetProgress() { updateProgress(0, 0, '等待开始'); } // === 收藏夹功能 === var FAVORITES_KEY = 'magnet-favorites'; var favoritesCache = null; function loadFavorites() { if (favoritesCache !== null) { return favoritesCache; } try { var stored = localStorage.getItem(FAVORITES_KEY); favoritesCache = stored ? JSON.parse(stored) : []; } catch (e) { favoritesCache = []; } return favoritesCache; } function saveFavorites(favorites) { try { localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites)); favoritesCache = favorites; } catch (e) { log('保存收藏失败: ' + e); } } function isFavorited(link) { var favorites = loadFavorites(); return favorites.some(function(f) { return f.link === link; }); } function toggleFavorite(title, link, btn) { var favorites = loadFavorites(); var existingIndex = favorites.findIndex(function(f) { return f.link === link; }); if (existingIndex >= 0) { favorites.splice(existingIndex, 1); btn.classList.remove('is-favorite'); btn.innerHTML = '♡'; btn.title = '收藏'; } else { favorites.push({ title: title, link: link, addedAt: Date.now() }); btn.classList.add('is-favorite'); btn.innerHTML = '♥'; btn.title = '取消收藏'; } saveFavorites(favorites); } function renderFavoritesList() { var list = document.getElementById('magnet-favorites-list'); if (!list) return; var favorites = loadFavorites(); list.innerHTML = ''; if (favorites.length === 0) { list.innerHTML = '
📭
暂无收藏\br>点击结果列表中的 ♡ 按钮添加收藏
'; return; } favorites.forEach(function(fav) { var item = document.createElement('div'); item.className = 'magnet-favorite-item'; var titleEl = document.createElement('div'); titleEl.className = 'magnet-favorite-title'; titleEl.textContent = fav.title; titleEl.title = fav.title; var actionsEl = document.createElement('div'); actionsEl.className = 'magnet-favorite-actions'; var copyBtn = document.createElement('button'); copyBtn.className = 'magnet-favorite-copy'; copyBtn.textContent = '复制'; copyBtn.onclick = function() { navigator.clipboard.writeText(fav.link) .then(function() { copyBtn.textContent = '已复制'; setTimeout(function() { copyBtn.textContent = '复制'; }, 1000); }); }; var removeBtn = document.createElement('button'); removeBtn.className = 'magnet-favorite-remove'; removeBtn.textContent = '删除'; removeBtn.onclick = function() { var favorites = loadFavorites(); var idx = favorites.findIndex(function(f) { return f.link === fav.link; }); if (idx >= 0) { favorites.splice(idx, 1); saveFavorites(favorites); renderFavoritesList(); } }; actionsEl.appendChild(copyBtn); actionsEl.appendChild(removeBtn); item.appendChild(titleEl); item.appendChild(actionsEl); list.appendChild(item); }); } // === 历史记录功能 === var HISTORY_KEY = 'magnet-search-history'; var MAX_HISTORY = 20; function loadSearchHistory() { try { var stored = localStorage.getItem(HISTORY_KEY); return stored ? JSON.parse(stored) : []; } catch (e) { return []; } } function saveSearchHistory(keyword) { if (!keyword || typeof keyword !== 'string' || !keyword.trim()) { return; } keyword = keyword.trim(); var history = loadSearchHistory(); // 移除已存在的相同关键词 var idx = history.indexOf(keyword); if (idx >= 0) { history.splice(idx, 1); } // 添加到开头 history.unshift(keyword); // 限制数量 if (history.length > MAX_HISTORY) { history = history.slice(0, MAX_HISTORY); } try { localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); } catch (e) { log('保存历史记录失败: ' + e); } } function showHistoryDropdown(input) { var history = loadSearchHistory(); if (history.length === 0) { return; } // 移除已存在的下拉框 var existing = document.querySelector('.magnet-history-dropdown'); if (existing) existing.remove(); var dropdown = document.createElement('div'); dropdown.className = 'magnet-history-dropdown'; history.forEach(function(kw) { var item = document.createElement('div'); item.className = 'magnet-history-item'; item.textContent = kw; item.onclick = function() { input.value = kw; dropdown.remove(); input.focus(); }; dropdown.appendChild(item); }); var clearItem = document.createElement('div'); clearItem.className = 'magnet-history-clear'; clearItem.textContent = '清空历史'; clearItem.onclick = function() { localStorage.removeItem(HISTORY_KEY); dropdown.remove(); }; dropdown.appendChild(clearItem); input.parentNode.style.position = 'relative'; input.parentNode.appendChild(dropdown); // 点击外部关闭 setTimeout(function() { document.addEventListener('click', function closeDropdown(e) { if (!dropdown.contains(e.target)) { dropdown.remove(); document.removeEventListener('click', closeDropdown); } }); }, 100); } // === 通知功能 === function playNotificationSound() { try { var audioContext = new (window.AudioContext || window.webkitAudioContext)(); var oscillator = audioContext.createOscillator(); var gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.value = 800; oscillator.type = 'sine'; gainNode.gain.setValue(0.3); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.3); } catch (e) { log('播放提示音失败: ' + e); } } function showBrowserNotification(title, body) { if (!('Notification' in window)) { return; } if (Notification.permission === 'granted') { new Notification(title, { body: body, icon: chrome.runtime ? chrome.runtime.getURL('icon.png') : undefined }); } else if (Notification.permission !== 'denied') { Notification.requestPermission().then(function(permission) { if (permission === 'granted') { new Notification(title, { body: body, icon: chrome.runtime ? chrome.runtime.getURL('icon.png') : undefined }); } }); } } function notifyComplete(count, duration) { playNotificationSound(); var durationText = ''; if (duration && duration > 0) { var seconds = Math.floor(duration / 1000); if (seconds >= 60) { durationText = ',耗时 ' + Math.floor(seconds / 60) + ' 分 ' + (seconds % 60) + ' 秒'; } else { durationText = ',耗时 ' + seconds + ' 秒'; } } showBrowserNotification('磁力链接抓取完成', '共获取 ' + count + ' 个磁力链接' + durationText); } var countEl = document.getElementById('magnet-count-num'); if (countEl) countEl.textContent = count; } function clearMagnetList(skipPersist) { var list = document.getElementById('magnet-list'); if (list) list.innerHTML = ''; allMagnetLinks = []; allMagnetRecords = []; magnetRecordMap = Object.create(null); updateCount(0); if (!skipPersist) { scheduleStatePersist(); } } function addMagnetItem(title, link, options) { var list = document.getElementById('magnet-list'); if (!list) return; var safeTitle = typeof title === 'string' ? title : String(title || ''); var safeLink = typeof link === 'string' ? link : String(link || ''); if (!safeLink) return; if (magnetRecordMap[safeLink]) { return; } magnetRecordMap[safeLink] = safeTitle || '恢复记录'; allMagnetRecords.push({ title: magnetRecordMap[safeLink], link: safeLink }); allMagnetLinks.push(safeLink); var item = document.createElement('div'); item.className = 'magnet-item'; var titleEl = document.createElement('span'); titleEl.className = 'magnet-title'; titleEl.title = safeTitle; titleEl.textContent = safeTitle; var btnContainer = document.createElement('div'); btnContainer.style.cssText = 'display:flex;gap:6px;flex-shrink:0;'; var copyBtn = document.createElement('button'); copyBtn.className = 'magnet-copy-btn'; copyBtn.setAttribute('data-magnet', safeLink); copyBtn.textContent = '复制'; // 收藏按钮 var favoriteBtn = document.createElement('button'); favoriteBtn.className = 'magnet-favorite-btn'; favoriteBtn.innerHTML = '♡'; favoriteBtn.title = '收藏'; favoriteBtn.onclick = function() { toggleFavorite(safeTitle, safeLink, favoriteBtn); }; // 检查是否已收藏 isFavorite(safeLink).then(function(isFav) { if (isFav) { favoriteBtn.classList.add('is-favorite'); favoriteBtn.title = '取消收藏'; } }); titleEl.onclick = function() { navigator.clipboard.writeText(safeLink) .then(function() { titleEl.textContent = '已复制: ' + safeTitle.substring(0, 20) + '...'; setTimeout(function() { titleEl.textContent = safeTitle; }, 1500); }) .catch(function(err) { var errorMsg = err && err.message ? err.message : '复制失败'; log('标题复制失败: ' + errorMsg); updateStatus('复制失败,请检查剪贴板权限', 'error'); }); }; copyBtn.onclick = function() { navigator.clipboard.writeText(safeLink) .then(function() { copyBtn.textContent = '已复制'; setTimeout(function() { copyBtn.textContent = '复制'; }, 1000); }) .catch(function(err) { var errorMsg = err && err.message ? err.message : '复制失败'; log('按钮复制失败: ' + errorMsg); updateStatus('复制失败,请检查剪贴板权限', 'error'); }); }; item.appendChild(titleEl); btnContainer.appendChild(favoriteBtn); btnContainer.appendChild(copyBtn); item.appendChild(btnContainer); list.appendChild(item); setPanelView('results'); updateCount(list.children.length); if (!options || !options.skipPersist) { scheduleStatePersist(); } } var list = document.getElementById('magnet-list'); if (!list) return; var safeTitle = typeof title === 'string' ? title : String(title || ''); var safeLink = typeof link === 'string' ? link : String(link || ''); if (!safeLink) return; if (magnetRecordMap[safeLink]) { return; } magnetRecordMap[safeLink] = safeTitle || '恢复记录'; allMagnetRecords.push({ title: magnetRecordMap[safeLink], link: safeLink }); allMagnetLinks.push(safeLink); var item = document.createElement('div'); item.className = 'magnet-item'; var titleEl = document.createElement('span'); titleEl.className = 'magnet-title'; titleEl.title = safeTitle; titleEl.textContent = safeTitle; var copyBtn = document.createElement('button'); copyBtn.className = 'magnet-copy-btn'; copyBtn.setAttribute('data-magnet', safeLink); copyBtn.textContent = '复制'; titleEl.onclick = function() { navigator.clipboard.writeText(safeLink) .then(function() { titleEl.textContent = '已复制: ' + safeTitle.substring(0, 20) + '...'; setTimeout(function() { titleEl.textContent = safeTitle; }, 1500); }) .catch(function(err) { var errorMsg = err && err.message ? err.message : '复制失败'; log('标题复制失败: ' + errorMsg); updateStatus('复制失败,请检查剪贴板权限', 'error'); }); }; copyBtn.onclick = function() { navigator.clipboard.writeText(safeLink) .then(function() { copyBtn.textContent = '已复制'; setTimeout(function() { copyBtn.textContent = '复制'; }, 1000); }) .catch(function(err) { var errorMsg = err && err.message ? err.message : '复制失败'; log('按钮复制失败: ' + errorMsg); updateStatus('复制失败,请检查剪贴板权限', 'error'); }); }; item.appendChild(titleEl); item.appendChild(copyBtn); list.appendChild(item); setPanelView('results'); updateCount(list.children.length); if (!options || !options.skipPersist) { scheduleStatePersist(); } } function extractMagnets() { var magnetPattern = /magnet:\?xt=urn:btih:[a-fA-F0-9]{32,40}/gi; var links = new Set(); function walk(node) { if (node.nodeType === Node.TEXT_NODE) { var matches = node.textContent.match(magnetPattern); if (matches) matches.forEach(function(m) { links.add(m); }); } else if (node.nodeType === Node.ELEMENT_NODE) { var tag = node.tagName.toLowerCase(); if (tag !== 'script' && tag !== 'style' && tag !== 'noscript') { node.childNodes.forEach(walk); } } } walk(document.body); document.querySelectorAll('a[href^="magnet:"]').forEach(function(a) { links.add(a.href); }); return Array.from(links); } var speedMode = 'fast'; var speedConfig = { slow: { thread: [300, 600], page: [500, 900], concurrency: 1, fetchTimeout: 25000, messageTimeout: 30000 }, medium: { thread: [80, 180], page: [150, 300], concurrency: 2, fetchTimeout: 18000, messageTimeout: 22000 }, fast: { thread: [0, 60], page: [60, 150], concurrency: 4, fetchTimeout: 14000, messageTimeout: 18000 }, ultrafast: { thread: [0, 20], page: [20, 80], concurrency: 6, fetchTimeout: 10000, messageTimeout: 14000 } }; function getSpeedProfile() { return speedConfig[speedMode] || speedConfig.fast; } function getSpeedDelay(type) { var profile = getSpeedProfile(); var range = profile[type] || [0, 0]; return Math.random() * (range[1] - range[0]) + range[0]; } function getThreadConcurrency() { return Math.max(1, getSpeedProfile().concurrency || 1); } function getFetchTimeout() { return getSpeedProfile().fetchTimeout || 15000; } function getMessageTimeout() { return getSpeedProfile().messageTimeout || 20000; } function sleep(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); } function sendRuntimeMessage(payload, timeoutMs) { return new Promise(function(resolve, reject) { if (!chrome.runtime || !chrome.runtime.id) { reject(new Error('扩展已失效,请刷新页面')); return; } var finished = false; var safeTimeoutMs = Number(timeoutMs); if (!Number.isFinite(safeTimeoutMs) || safeTimeoutMs <= 0) { safeTimeoutMs = 15000; } var timer = setTimeout(function() { if (finished) return; finished = true; reject(new Error('请求超时,请稍后重试')); }, safeTimeoutMs); try { chrome.runtime.sendMessage(payload, function(response) { if (finished) return; finished = true; clearTimeout(timer); if (chrome.runtime.lastError) { reject(new Error(chrome.runtime.lastError.message)); return; } resolve(response); }); } catch (err) { if (finished) return; finished = true; clearTimeout(timer); reject(err); } }); } function buildKeywordList(keyword) { return String(keyword || '') .split(',') .map(function(item) { return item.trim(); }) .filter(function(item) { return !!item; }); } function filterThreadsByKeywords(threadList, keywords) { var normalizedThreads = normalizeCachedThreads(threadList); if (!keywords || keywords.length === 0) { return normalizedThreads; } return normalizedThreads.filter(function(thread) { return keywords.some(function(keyword) { return thread.title.indexOf(keyword) !== -1; }); }); } function mergeCoverageThreads(targetMap, threads) { normalizeCachedThreads(threads).forEach(function(thread) { if (!targetMap[thread.threadKey]) { targetMap[thread.threadKey] = thread; return; } if (!targetMap[thread.threadKey].title && thread.title) { targetMap[thread.threadKey].title = thread.title; } if (!targetMap[thread.threadKey].url && thread.url) { targetMap[thread.threadKey].url = thread.url; } }); } function coverageMapToList(targetMap) { return Object.keys(targetMap).map(function(threadKey) { return targetMap[threadKey]; }); } function getSmartFrontRefreshPages(startPage, endPage) { if (startPage !== 1) { return 0; } return Math.min(20, Math.max(0, endPage - startPage + 1)); } async function getCachedCoveragePlan(forumKey, startPage, endPage, frontRefreshPages) { try { var response = await sendRuntimeMessage({ action: 'cacheGetCoveragePlan', forumKey: forumKey, startPage: startPage, endPage: endPage, frontRefreshPages: frontRefreshPages }, 8000); if (!response || !response.ok) { log('读取缓存计划失败: ' + (response && response.error ? response.error : '空响应')); return null; } return response; } catch (e) { var errorMsg = e && e.message ? e.message : String(e); log('读取缓存计划异常: ' + errorMsg); return null; } } async function saveCoverageSnapshot(forumKey, startPage, endPage, threads, frontRefreshPages, strategy) { var normalizedThreads = normalizeCachedThreads(threads); if (!forumKey || normalizedThreads.length === 0) { return; } try { var response = await sendRuntimeMessage({ action: 'cacheSaveCoverage', forumKey: forumKey, startPage: startPage, endPage: endPage, frontRefreshPages: frontRefreshPages, strategy: strategy, crawledAt: Date.now(), threads: normalizedThreads }, 12000); if (!response || !response.ok) { log('保存范围缓存失败: ' + (response && response.error ? response.error : '空响应')); } } catch (e) { var errorMsg = e && e.message ? e.message : String(e); log('保存范围缓存异常: ' + errorMsg); } } async function savePageCoverageSnapshot(forumKey, page, threads) { var normalizedThreads = normalizeCachedThreads(threads); try { var response = await sendRuntimeMessage({ action: 'cacheSavePageCoverage', forumKey: forumKey, page: page, crawledAt: Date.now(), threads: normalizedThreads }, 12000); if (!response || !response.ok) { log('保存页缓存失败: ' + (response && response.error ? response.error : '空响应')); } } catch (e) { var errorMsg = e && e.message ? e.message : String(e); log('保存页缓存异常: ' + errorMsg); } } async function getCachedThreadMagnets(forumKey, threads) { var normalizedThreads = normalizeCachedThreads(threads); if (!forumKey || normalizedThreads.length === 0) { return []; } try { var response = await sendRuntimeMessage({ action: 'cacheGetThreadMagnets', forumKey: forumKey, threads: normalizedThreads }, 12000); if (!response || !response.ok || !Array.isArray(response.threads)) { log('读取帖子磁链缓存失败: ' + (response && response.error ? response.error : '空响应')); return []; } return response.threads.map(function(thread) { return { threadKey: typeof thread.threadKey === 'string' ? thread.threadKey : getThreadKeyFromUrl(thread.url), url: normalizeThreadUrl(thread.url), title: typeof thread.title === 'string' ? thread.title : '', magnets: normalizeMagnetList(thread.magnets) }; }); } catch (e) { var errorMsg = e && e.message ? e.message : String(e); log('读取帖子磁链缓存异常: ' + errorMsg); return []; } } async function saveThreadMagnetsToCache(forumKey, threads) { var normalizedThreads = []; (Array.isArray(threads) ? threads : []).forEach(function(thread) { if (!thread || typeof thread !== 'object') { return; } var normalizedUrl = normalizeThreadUrl(thread.url); var threadKey = typeof thread.threadKey === 'string' && thread.threadKey ? thread.threadKey : getThreadKeyFromUrl(normalizedUrl); var magnets = normalizeMagnetList(thread.magnets); if (!normalizedUrl || !threadKey || magnets.length === 0) { return; } normalizedThreads.push({ threadKey: threadKey, url: normalizedUrl, title: typeof thread.title === 'string' ? thread.title : '', magnets: magnets }); }); if (!forumKey || normalizedThreads.length === 0) { return; } try { var response = await sendRuntimeMessage({ action: 'cacheSaveThreadMagnets', forumKey: forumKey, syncedAt: Date.now(), threads: normalizedThreads }, 12000); if (!response || !response.ok) { log('保存帖子磁链缓存失败: ' + (response && response.error ? response.error : '空响应')); } } catch (e) { var errorMsg = e && e.message ? e.message : String(e); log('保存帖子磁链缓存异常: ' + errorMsg); } } function formatTimeLabel(timestamp) { var value = Number(timestamp); if (!value) { return '无'; } var date = new Date(value); var month = String(date.getMonth() + 1).padStart(2, '0'); var day = String(date.getDate()).padStart(2, '0'); var hour = String(date.getHours()).padStart(2, '0'); var minute = String(date.getMinutes()).padStart(2, '0'); return month + '-' + day + ' ' + hour + ':' + minute; } function formatBytesLabel(bytes) { var size = Number(bytes); if (!Number.isFinite(size) || size <= 0) { return '0 B'; } if (size < 1024) { return size + ' B'; } if (size < 1024 * 1024) { return (size / 1024).toFixed(1) + ' KB'; } return (size / (1024 * 1024)).toFixed(2) + ' MB'; } async function getCacheOverview(forumKey) { try { var response = await sendRuntimeMessage({ action: 'cacheGetOverview', forumKey: forumKey, limit: 12 }, 12000); if (!response || !response.ok) { log('读取缓存总览失败: ' + (response && response.error ? response.error : '空响应')); return null; } return response; } catch (e) { var errorMsg = e && e.message ? e.message : String(e); log('读取缓存总览异常: ' + errorMsg); return null; } } function renderCacheOverview(cacheOverview) { var panel = document.getElementById('magnet-cache-panel'); if (!panel) { return; } if (!cacheOverview || !cacheOverview.summary) { panel.innerHTML = '
缓存状态
暂无缓存数据
'; return; } var summary = cacheOverview.summary; var recentThreads = Array.isArray(cacheOverview.recentThreads) ? cacheOverview.recentThreads : []; var recentCoverages = Array.isArray(cacheOverview.recentCoverages) ? cacheOverview.recentCoverages : []; var html = ''; html += '
说明
这里的帖子数是 唯一帖子数,同一板块重复搜索同一帖子不会重复累计;重复搜索只会新增/更新范围快照。
'; html += '
'; html += '
当前板块唯一帖子
' + summary.forumThreadCount + '
'; html += '
全部唯一帖子
' + summary.totalThreadCount + '
'; html += '
磁链缓存
' + summary.recentMagnetCount + '
'; html += '
存储占用
' + formatBytesLabel(summary.storageUsage) + '
'; html += '
'; html += '
最近缓存帖子
'; if (recentThreads.length === 0) { html += '
提示
'; } else { recentThreads.forEach(function(item) { var title = item.title || '未命名帖子'; html += '
'; html += '
' + escapeHtml(title) + '
'; html += ''; html += '
'; }); } html += '
'; html += '
最近范围快照
'; if (recentCoverages.length === 0) { html += '
提示
'; } else { recentCoverages.forEach(function(item) { html += '
'; html += '
页码 ' + item.startPage + '-' + item.endPage + '
'; html += ''; html += '
'; }); } html += '
'; panel.innerHTML = html; } async function refreshCacheOverview(options) { options = options || {}; var cachePanel = document.getElementById('magnet-cache-panel'); if (cachePanel) { cachePanel.innerHTML = '
正在读取缓存...
'; } var overview = await getCacheOverview(getForumKey()); renderCacheOverview(overview); if (options.showStatus) { if (overview && overview.summary) { updateStatus('已加载缓存:当前板块唯一帖子 ' + overview.summary.forumThreadCount + ' 帖,当前页缓存 ' + Number(overview.summary.forumPageCoverageCount || 0) + ' 页', 'done'); } else { updateStatus('读取缓存失败,请稍后重试', 'error'); } } return overview; } async function clearAllCacheWithConfirm() { if (!confirm('确认清空全部缓存吗?这不会删除当前页面已显示的结果。')) { return; } if (!confirm('二次确认:真的要清空全部标题缓存、范围快照和磁链缓存吗?')) { return; } updateStatus('正在清空缓存...', 'loading'); try { var response = await sendRuntimeMessage({ action: 'cacheClearAll' }, 12000); if (!response || !response.ok) { updateStatus('清空缓存失败', 'error'); return; } await refreshCacheOverview(); updateStatus('已清空全部缓存', 'done'); } catch (e) { var errorMsg = e && e.message ? e.message : String(e); log('清空缓存异常: ' + errorMsg); updateStatus('清空缓存失败:' + errorMsg, 'error'); } } function toggleCachePanel() { var cacheView = document.getElementById('magnet-cache-view'); var resultsView = document.getElementById('magnet-results-view'); if (!cacheView || !resultsView) { return; } var isVisible = cacheView.classList.contains('is-active'); setPanelView(isVisible ? 'results' : 'cache'); if (!isVisible) { refreshCacheOverview({ showStatus: true }); } } async function applyCachedMagnetHits(pageLabel, filteredThreads, context) { var cachedEntries = await getCachedThreadMagnets(context.forumKey, filteredThreads); var cachedMap = Object.create(null); var threadsToFetch = []; var cachedThreadCount = 0; var cachedMagnetCount = 0; cachedEntries.forEach(function(entry) { if (!entry || !entry.threadKey) return; cachedMap[entry.threadKey] = entry; }); normalizeCachedThreads(filteredThreads).forEach(function(thread) { var threadKey = thread.threadKey || getThreadKeyFromUrl(thread.url); var cachedEntry = threadKey ? cachedMap[threadKey] : null; var cachedMagnets = cachedEntry ? normalizeMagnetList(cachedEntry.magnets) : []; if (cachedMagnets.length === 0) { if (!context.processedThreadKeys[threadKey]) { threadsToFetch.push(thread); } return; } if (context.processedThreadKeys[threadKey]) { return; } context.processedThreadKeys[threadKey] = true; cachedThreadCount += 1; context.totalFetched += 1; cachedMagnets.forEach(function(magnet) { if (context.allMagnets.has(magnet)) { return; } context.allMagnets.add(magnet); cachedMagnetCount += 1; addMagnetItem(thread.title || cachedEntry.title || '缓存帖子', magnet); }); }); if (cachedThreadCount > 0) { updateStatus( '正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,直接读取缓存磁链 ' + cachedThreadCount + ' 帖(' + cachedMagnetCount + ' 条)', 'loading' ); } return { threadsToFetch: threadsToFetch, cachedThreadCount: cachedThreadCount, cachedMagnetCount: cachedMagnetCount, pageLabel: pageLabel }; } var STATE_BACKUP_KEY = 'magnetPluginStateBackupV1'; var MAX_STATE_ITEMS = 2000; var statePersistTimer = null; var hasRestoredProgress = false; var lastRestoredState = null; var manualClearRequested = false; var isFetching = false; var stopFetching = false; var allMagnetLinks = []; var allMagnetRecords = []; var magnetRecordMap = Object.create(null); var progressRuntime = { isRunning: false, stoppedByUser: false, startPage: 1, endPage: 1, resumeFromPage: 1, keyword: '', speedMode: 'fast' }; function resetProgressRuntimeToIdle() { var startInput = document.getElementById('page-start'); var endInput = document.getElementById('page-end'); var keywordInput = document.getElementById('keyword-input'); var currentStart = startInput ? parseInt(startInput.value, 10) || 1 : 1; var currentEnd = endInput ? parseInt(endInput.value, 10) || currentStart : currentStart; progressRuntime.isRunning = false; progressRuntime.stoppedByUser = true; progressRuntime.startPage = currentStart; progressRuntime.endPage = Math.max(currentStart, currentEnd); progressRuntime.resumeFromPage = currentStart; progressRuntime.keyword = keywordInput ? keywordInput.value.trim() : ''; progressRuntime.speedMode = speedConfig[speedMode] ? speedMode : 'fast'; } function clearSessionBackupState() { try { sessionStorage.removeItem(STATE_BACKUP_KEY); } catch (e) { log('清理会话备份失败: ' + e); } } async function clearAllResults() { manualClearRequested = true; stopFetching = true; resetProgressRuntimeToIdle(); clearMagnetList(true); clearSessionBackupState(); try { await sendRuntimeMessage({ action: 'clearProgressState' }, 6000); } catch (e) { var clearErrorMsg = e && e.message ? e.message : String(e); log('远端清理失败: ' + clearErrorMsg); } if (!isFetching) { manualClearRequested = false; updateStatus('已手动清空结果', 'done'); scheduleStatePersist(); } else { updateStatus('正在停止任务并清空结果...', 'loading'); } } function buildStateSnapshot() { var keywordInput = document.getElementById('keyword-input'); var startInput = document.getElementById('page-start'); var endInput = document.getElementById('page-end'); var speedSelect = document.getElementById('speed-select'); var statusEl = document.getElementById('magnet-status'); var startPage = startInput ? parseInt(startInput.value, 10) || 1 : 1; var endPage = endInput ? parseInt(endInput.value, 10) || startPage : startPage; var resumeFromPage = Number(progressRuntime.resumeFromPage); if (!Number.isFinite(resumeFromPage) || resumeFromPage < 1) { resumeFromPage = startPage; } return { links: allMagnetLinks.slice(-MAX_STATE_ITEMS), items: allMagnetRecords.slice(-MAX_STATE_ITEMS), keyword: keywordInput ? keywordInput.value.trim() : '', speedMode: speedConfig[speedMode] ? speedMode : 'fast', startPage: startPage, endPage: endPage, resumeFromPage: resumeFromPage, isRunning: !!progressRuntime.isRunning, stoppedByUser: !!progressRuntime.stoppedByUser, statusText: statusEl ? statusEl.textContent || '' : '', statusType: statusEl ? statusEl.getAttribute('data-type') || '' : '', pageUrl: window.location.href, updatedAt: Date.now() }; } function saveStateToSessionBackup(snapshot) { try { sessionStorage.setItem(STATE_BACKUP_KEY, JSON.stringify(snapshot)); } catch (e) { log('保存会话备份失败: ' + e); } } function readStateFromSessionBackup() { try { var raw = sessionStorage.getItem(STATE_BACKUP_KEY); if (!raw) return null; return JSON.parse(raw); } catch (e) { log('读取会话备份失败: ' + e); return null; } } function applyStateSnapshot(snapshot) { if (!snapshot || typeof snapshot !== 'object') { return false; } var speedSelect = document.getElementById('speed-select'); if (speedSelect && snapshot.speedMode && speedConfig[snapshot.speedMode]) { speedSelect.value = snapshot.speedMode; speedMode = snapshot.speedMode; } var keywordInput = document.getElementById('keyword-input'); if (keywordInput && typeof snapshot.keyword === 'string') { keywordInput.value = snapshot.keyword; } var startInput = document.getElementById('page-start'); if (startInput && Number.isFinite(Number(snapshot.startPage))) { startInput.value = Math.max(1, Number(snapshot.startPage)); } var endInput = document.getElementById('page-end'); if (endInput && Number.isFinite(Number(snapshot.endPage))) { endInput.value = Math.max(1, Number(snapshot.endPage)); } progressRuntime.startPage = Number.isFinite(Number(snapshot.startPage)) ? Math.max(1, Number(snapshot.startPage)) : 1; progressRuntime.endPage = Number.isFinite(Number(snapshot.endPage)) ? Math.max(progressRuntime.startPage, Number(snapshot.endPage)) : progressRuntime.startPage; progressRuntime.resumeFromPage = Number.isFinite(Number(snapshot.resumeFromPage)) ? Math.max(1, Number(snapshot.resumeFromPage)) : progressRuntime.startPage; progressRuntime.isRunning = !!snapshot.isRunning; progressRuntime.stoppedByUser = !!snapshot.stoppedByUser; progressRuntime.keyword = typeof snapshot.keyword === 'string' ? snapshot.keyword : ''; progressRuntime.speedMode = speedConfig[snapshot.speedMode] ? snapshot.speedMode : 'fast'; var records = Array.isArray(snapshot.items) && snapshot.items.length > 0 ? snapshot.items : (Array.isArray(snapshot.links) ? snapshot.links.map(function(link) { return { title: '恢复记录', link: link }; }) : []); clearMagnetList(true); records.forEach(function(record) { if (!record || typeof record !== 'object') return; if (typeof record.link !== 'string' || !record.link) return; var title = typeof record.title === 'string' ? record.title : '恢复记录'; addMagnetItem(title, record.link, { skipPersist: true }); }); if (allMagnetLinks.length > 0) { var statusText = typeof snapshot.statusText === 'string' && snapshot.statusText ? '已恢复: ' + snapshot.statusText : '已恢复上次抓取记录,共' + allMagnetLinks.length + '条'; updateStatus(statusText, snapshot.statusType || 'done'); return true; } return false; } function saveStateNow() { var snapshot = buildStateSnapshot(); saveStateToSessionBackup(snapshot); sendRuntimeMessage({ action: 'saveProgressState', state: snapshot }, 6000).catch(function(e) { var errorMsg = e && e.message ? e.message : String(e); log('远端状态保存失败: ' + errorMsg); }); } function scheduleStatePersist() { if (statePersistTimer) { clearTimeout(statePersistTimer); } statePersistTimer = setTimeout(function() { statePersistTimer = null; saveStateNow(); }, 500); } async function restoreProgressState() { if (hasRestoredProgress) { return { restored: allMagnetLinks.length > 0, state: lastRestoredState }; } hasRestoredProgress = true; var sessionState = readStateFromSessionBackup(); var sessionStateTime = sessionState && Number.isFinite(Number(sessionState.updatedAt)) ? Number(sessionState.updatedAt) : 0; var remoteState = null; var remoteStateTime = 0; try { var response = await sendRuntimeMessage({ action: 'loadProgressState' }, 6000); if (response && response.ok && response.state) { remoteState = response.state; remoteStateTime = Number.isFinite(Number(response.state.updatedAt)) ? Number(response.state.updatedAt) : 0; } } catch (e) { var errorMsg = e && e.message ? e.message : String(e); log('远端状态恢复失败: ' + errorMsg); } var chosenState = null; if (sessionState) { chosenState = sessionState; } if (remoteState && (!chosenState || remoteStateTime >= sessionStateTime)) { chosenState = remoteState; } lastRestoredState = chosenState; var restored = chosenState ? applyStateSnapshot(chosenState) : false; return { restored: restored, state: chosenState }; } window.addEventListener('pagehide', function() { if (statePersistTimer) { clearTimeout(statePersistTimer); statePersistTimer = null; } saveStateNow(); }); async function fetchThreadsInParallel(page, filteredThreads, allMagnets, processedThreadKeys, options) { if (!filteredThreads || filteredThreads.length === 0) { return 0; } options = options || {}; var concurrency = Math.min(getThreadConcurrency(), filteredThreads.length); var cursor = 0; var fetchedCount = 0; var cachePayload = []; async function worker() { while (!stopFetching) { var currentIndex = cursor; cursor += 1; if (currentIndex >= filteredThreads.length) { return; } var threadItem = filteredThreads[currentIndex]; var threadUrl = threadItem.url; var threadTitle = threadItem.title || '未命名帖子'; var threadKey = threadItem.threadKey || getThreadKeyFromUrl(threadUrl); if (processedThreadKeys && threadKey) { if (processedThreadKeys[threadKey]) { continue; } processedThreadKeys[threadKey] = true; } updateStatus('页' + page + '/帖子' + (currentIndex + 1) + '/' + filteredThreads.length, 'loading'); try { var threadResponse = await sendRuntimeMessage({ action: 'openAndFetch', url: threadUrl, timeoutMs: getFetchTimeout() }, getMessageTimeout()); if (threadResponse && threadResponse.error) { log('帖子请求失败: ' + threadUrl + ' - ' + threadResponse.error); } else if (threadResponse && threadResponse.magnets) { var threadMagnets = normalizeMagnetList(threadResponse.magnets); if (threadMagnets.length > 0 && options.forumKey) { cachePayload.push({ threadKey: threadKey, url: threadUrl, title: threadTitle, magnets: threadMagnets }); } threadMagnets.forEach(function(m) { if (!allMagnets.has(m)) { allMagnets.add(m); addMagnetItem(threadTitle, m); } }); } } catch (e) { var threadErrorMsg = e && e.message ? e.message : String(e); log('获取帖子失败: ' + threadUrl + ' - ' + threadErrorMsg); } fetchedCount += 1; var delay = getSpeedDelay('thread'); if (delay > 0) { await sleep(delay); } } } var workers = []; for (var workerIndex = 0; workerIndex < concurrency; workerIndex++) { workers.push(worker()); } await Promise.all(workers); if (options.forumKey && cachePayload.length > 0) { await saveThreadMagnetsToCache(options.forumKey, cachePayload); } return fetchedCount; } async function processCachedThreadBatch(pageLabel, threads, context, statusText) { var normalizedThreads = normalizeCachedThreads(threads); if (normalizedThreads.length === 0) { return; } if (statusText) { updateStatus(statusText, 'loading'); } mergeCoverageThreads(context.coverageThreadMap, normalizedThreads); var filteredThreads = filterThreadsByKeywords(normalizedThreads, context.keywords); context.matchedThreads += filteredThreads.length; if (filteredThreads.length === 0) { updateStatus('正在搜索缓存:已检查缓存标题 ' + normalizedThreads.length + ' 帖,当前关键词未命中', 'loading'); return; } var cacheResult = await applyCachedMagnetHits(pageLabel, filteredThreads, context); if (cacheResult.threadsToFetch.length === 0) { updateStatus('正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,结果已全部由缓存秒出', 'loading'); return; } updateStatus( '正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,缓存磁链命中 ' + cacheResult.cachedThreadCount + ' 帖,补抓 ' + cacheResult.threadsToFetch.length + ' 帖', 'loading' ); context.totalFetched += await fetchThreadsInParallel( pageLabel, cacheResult.threadsToFetch, context.allMagnets, context.processedThreadKeys, { forumKey: context.forumKey } ); } async function fetchLivePageRange(startPage, endPage, context) { if (startPage > endPage) { return; } for (var page = startPage; page <= endPage; page++) { if (stopFetching) break; progressRuntime.resumeFromPage = page; scheduleStatePersist(); if (!chrome.runtime || !chrome.runtime.id) { updateStatus('扩展已失效,请刷新页面', 'error'); break; } updateStatus('第' + page + '/' + context.normalizedEnd + '页...', 'loading'); // 更新进度条 var totalPages = context.normalizedEnd - (context.startPage || startPage) + 1; var currentPage = page - (context.startPage || startPage) + 1; updateProgress(currentPage, totalPages, '第' + page + '/' + context.normalizedEnd + '页'); var pageUrl = context.baseUrl + page + '.html'; try { var response = await sendRuntimeMessage({ action: 'fetchHtml', url: pageUrl, timeoutMs: getFetchTimeout() }, getMessageTimeout()); if (!response || response.error) { context.failedPages += 1; progressRuntime.resumeFromPage = page + 1; scheduleStatePersist(); log('获取第 ' + page + ' 页失败: ' + (response && response.error ? response.error : '空响应')); continue; } if (!response.html) { progressRuntime.resumeFromPage = page + 1; scheduleStatePersist(); continue; } var threadList = extractThreadsFromHtml(response.html); mergeCoverageThreads(context.coverageThreadMap, threadList); await savePageCoverageSnapshot(context.forumKey, page, threadList); var filteredThreads = filterThreadsByKeywords(threadList, context.keywords); context.matchedThreads += filteredThreads.length; var cacheResult = await applyCachedMagnetHits(page, filteredThreads, context); if (cacheResult.threadsToFetch.length > 0) { if (cacheResult.cachedThreadCount > 0) { updateStatus( '第' + page + '/' + context.normalizedEnd + '页:缓存磁链命中 ' + cacheResult.cachedThreadCount + ' 帖,补抓 ' + cacheResult.threadsToFetch.length + ' 帖', 'loading' ); } context.totalFetched += await fetchThreadsInParallel( page, cacheResult.threadsToFetch, context.allMagnets, context.processedThreadKeys, { forumKey: context.forumKey } ); } progressRuntime.resumeFromPage = page + 1; scheduleStatePersist(); } catch (e) { context.failedPages += 1; progressRuntime.resumeFromPage = page + 1; scheduleStatePersist(); var pageErrorMsg = e && e.message ? e.message : String(e); log('获取第 ' + page + ' 页异常: ' + pageErrorMsg); } if (stopFetching) break; var pageDelay = getSpeedDelay('page'); if (pageDelay > 0) { await sleep(pageDelay); } } } async function fetchFromPage(startPage, endPage, options) { if (isFetching) return; options = options || {}; var preserveExisting = !!options.preserveExisting; var earlyStart = Math.max(1, parseInt(startPage, 10) || 1); var earlyEnd = Math.max(earlyStart, parseInt(endPage, 10) || earlyStart); isFetching = true; stopFetching = false; progressRuntime.isRunning = true; progressRuntime.stoppedByUser = false; progressRuntime.startPage = earlyStart; progressRuntime.endPage = earlyEnd; progressRuntime.resumeFromPage = earlyStart; // 重置进度条 resetProgress(); // 记录开始时间 var startTime = Date.now(); stopFetching = false; progressRuntime.isRunning = true; progressRuntime.stoppedByUser = false; progressRuntime.startPage = earlyStart; progressRuntime.endPage = earlyEnd; progressRuntime.resumeFromPage = earlyStart; var panel = createFloatingPanel(); var ball = document.getElementById('magnet-float-ball'); panel.style.display = 'flex'; if (ball) ball.style.display = 'none'; if (!preserveExisting) { clearMagnetList(true); } try { var normalizedStart = Math.max(1, parseInt(startPage, 10) || 1); var normalizedEnd = Math.max(normalizedStart, parseInt(endPage, 10) || normalizedStart); var baseUrl = getBaseUrl(); var keywordInput = document.getElementById('keyword-input'); var speedSelect = document.getElementById('speed-select'); var keyword = typeof options.keyword === 'string' ? options.keyword.trim() : (keywordInput ? keywordInput.value.trim() : ''); var selectedSpeed = typeof options.speedMode === 'string' ? options.speedMode : (speedSelect ? speedSelect.value : 'fast'); speedMode = speedConfig[selectedSpeed] ? selectedSpeed : 'fast'; if (speedSelect) { speedSelect.value = speedMode; } if (keywordInput) { keywordInput.value = keyword; } progressRuntime.isRunning = true; progressRuntime.stoppedByUser = false; progressRuntime.startPage = normalizedStart; progressRuntime.endPage = normalizedEnd; progressRuntime.resumeFromPage = normalizedStart; progressRuntime.keyword = keyword; progressRuntime.speedMode = speedMode; if (!baseUrl) { updateStatus('无法获取页面URL', 'error'); progressRuntime.isRunning = false; scheduleStatePersist(); return; } log('开始获取第 ' + normalizedStart + ' 到 ' + normalizedEnd + ' 页, 关键词: ' + keyword + ', 速度:' + speedMode + ', 并发:' + getThreadConcurrency()); if (preserveExisting && allMagnetLinks.length > 0) { updateStatus('检测到未完成任务,已恢复' + allMagnetLinks.length + '条,继续抓取...', 'loading'); } else { updateStatus('开始获取...', 'loading'); } scheduleStatePersist(); var forumKey = getForumKey(); var frontRefreshPages = getSmartFrontRefreshPages(normalizedStart, normalizedEnd); var searchContext = { forumKey: forumKey, baseUrl: baseUrl, normalizedEnd: normalizedEnd, keywords: buildKeywordList(keyword), coverageThreadMap: Object.create(null), processedThreadKeys: Object.create(null), allMagnets: new Set(allMagnetLinks), matchedThreads: 0, totalFetched: 0, failedPages: 0 }; var cachePlan = await getCachedCoveragePlan(forumKey, normalizedStart, normalizedEnd, frontRefreshPages); var cacheStrategy = 'full_live'; if (cachePlan && cachePlan.exactCoverage && Array.isArray(cachePlan.exactCoverage.threads) && cachePlan.exactCoverage.threads.length > 0) { cacheStrategy = 'exact_cache'; await processCachedThreadBatch('缓存', cachePlan.exactCoverage.threads, searchContext, '命中当前板块缓存,直接搜索...'); progressRuntime.resumeFromPage = normalizedEnd + 1; scheduleStatePersist(); } else { var refreshedFrontEnd = normalizedStart - 1; if (frontRefreshPages > 0) { refreshedFrontEnd = normalizedStart + frontRefreshPages - 1; updateStatus('智能增量:刷新前' + frontRefreshPages + '页...', 'loading'); await fetchLivePageRange(normalizedStart, refreshedFrontEnd, searchContext); } var shiftedReuseEnd = refreshedFrontEnd; if (!stopFetching && cachePlan && cachePlan.shiftedCoverage && Array.isArray(cachePlan.shiftedCoverage.threads) && cachePlan.shiftedCoverage.threads.length > 0) { cacheStrategy = 'smart_incremental'; shiftedReuseEnd = Math.max(shiftedReuseEnd, Math.min(normalizedEnd, Number(cachePlan.shiftedCoverage.reusedEndPage) || refreshedFrontEnd)); await processCachedThreadBatch( String(cachePlan.shiftedCoverage.reusedStartPage) + '-' + String(shiftedReuseEnd), cachePlan.shiftedCoverage.threads, searchContext, '智能增量:复用当前板块历史缓存 ' + cachePlan.shiftedCoverage.reusedStartPage + '-' + shiftedReuseEnd + ' 页' ); progressRuntime.resumeFromPage = shiftedReuseEnd + 1; scheduleStatePersist(); } var liveTailStart = Math.max(normalizedStart, shiftedReuseEnd + 1); if (!stopFetching && liveTailStart <= normalizedEnd) { if (liveTailStart > normalizedStart) { updateStatus('智能增量:补抓未覆盖页 ' + liveTailStart + '-' + normalizedEnd, 'loading'); } await fetchLivePageRange(liveTailStart, normalizedEnd, searchContext); } if (!stopFetching) { await saveCoverageSnapshot( forumKey, normalizedStart, normalizedEnd, coverageMapToList(searchContext.coverageThreadMap), frontRefreshPages, cacheStrategy ); } } progressRuntime.isRunning = false; if (manualClearRequested) { manualClearRequested = false; clearMagnetList(true); clearSessionBackupState(); resetProgressRuntimeToIdle(); updateStatus('已手动清空结果', 'done'); scheduleStatePersist(); return; } var keywordMsg = keyword ? ' (关键词:' + keyword + ' 匹配:' + searchContext.matchedThreads + '帖)' : ''; var failedMsg = searchContext.failedPages > 0 ? ',失败页:' + searchContext.failedPages : ''; // 更新进度条为100% updateProgress(normalizedEnd - normalizedStart + 1, normalizedEnd - normalizedStart + 1, '已完成'); if (stopFetching) { progressRuntime.stoppedByUser = true; updateStatus('已停止 - 找到' + searchContext.allMagnets.size + '个磁力' + keywordMsg + ',已处理帖子:' + searchContext.totalFetched + failedMsg, 'error'); } else { progressRuntime.stoppedByUser = false; progressRuntime.resumeFromPage = normalizedEnd + 1; updateStatus('完成! 共' + searchContext.allMagnets.size + '个磁力' + keywordMsg + ',已处理帖子:' + searchContext.totalFetched + failedMsg, 'done'); // 发送完成通知 var duration = Date.now() - startTime; notifyComplete(searchContext.allMagnets.size, duration); // 保存搜索历史 if (keyword) { saveSearchHistory(keyword); } } var failedMsg = searchContext.failedPages > 0 ? ',失败页:' + searchContext.failedPages : ''; if (stopFetching) { progressRuntime.stoppedByUser = true; updateStatus('已停止 - 找到' + searchContext.allMagnets.size + '个磁力' + keywordMsg + ',已处理帖子:' + searchContext.totalFetched + failedMsg, 'error'); } else { progressRuntime.stoppedByUser = false; progressRuntime.resumeFromPage = normalizedEnd + 1; updateStatus('完成! 共' + searchContext.allMagnets.size + '个磁力' + keywordMsg + ',已处理帖子:' + searchContext.totalFetched + failedMsg, 'done'); } scheduleStatePersist(); } finally { if (ball) ball.style.display = 'flex'; isFetching = false; } } function fetchAllMagnets() { var startPage = parseInt(document.getElementById('page-start').value, 10) || 1; var endPage = parseInt(document.getElementById('page-end').value, 10) || 1; if (endPage < startPage) { var temp = startPage; startPage = endPage; endPage = temp; } fetchFromPage(startPage, endPage); } function stopFetch() { stopFetching = true; progressRuntime.isRunning = false; progressRuntime.stoppedByUser = true; scheduleStatePersist(); } async function initializePluginUi() { createFloatingPanel(); var panel = document.getElementById('magnet-floating-panel'); if (!panel) return; var settingsArea = panel.querySelector('#magnet-settings'); if (!settingsArea) return; if (isListPage() && !document.getElementById('keyword-input')) { var currentPage = getCurrentPage(); var keywordDiv = document.createElement('div'); keywordDiv.className = 'magnet-control-row'; keywordDiv.innerHTML = ''; // 添加历史记录下拉功能 var keywordInput = keywordDiv.querySelector('#keyword-input'); if (keywordInput) { keywordInput.addEventListener('focus', function() { showHistoryDropdown(keywordInput); }); } var pageRange = document.createElement('div'); pageRange.className = 'magnet-control-row'; pageRange.style.cssText = 'font-size:12px;color:#8892a4;display:flex;align-items:center;gap:8px'; pageRange.innerHTML = '页码范围'; var btnContainer = document.createElement('div'); btnContainer.className = 'magnet-control-row'; var btn = document.createElement('button'); btn.textContent = '开始'; btn.style.cssText = 'flex:1;padding:11px 14px;background:linear-gradient(135deg,#00d4aa,#00f5c4);color:#0a0e14;border:none;border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;box-shadow:0 10px 20px rgba(0,212,170,.3)'; btn.onclick = fetchAllMagnets; var stopBtn = document.createElement('button'); stopBtn.textContent = '停止'; stopBtn.style.cssText = 'padding:11px 14px;background:linear-gradient(135deg,#ef4444,#f87171);color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:13px;font-weight:700'; stopBtn.onclick = stopFetch; var clearBtn = document.createElement('button'); clearBtn.textContent = '清结果'; clearBtn.style.cssText = 'padding:11px 14px;background:rgba(26,31,46,0.92);color:#8892a4;border:1px solid rgba(255,255,255,0.06);border-radius:12px;cursor:pointer;font-size:13px;font-weight:700'; clearBtn.onclick = clearAllResults; btnContainer.appendChild(btn); btnContainer.appendChild(stopBtn); btnContainer.appendChild(clearBtn); var speedDiv = document.createElement('div'); speedDiv.className = 'magnet-control-row'; speedDiv.style.cssText = 'font-size:12px;color:#8892a4;display:flex;align-items:center;gap:8px'; speedDiv.innerHTML = '抓取速度'; settingsArea.appendChild(keywordDiv); settingsArea.appendChild(pageRange); settingsArea.appendChild(btnContainer); settingsArea.appendChild(speedDiv); } if (!document.getElementById('magnet-status')) { var footer = panel.querySelector('.magnet-panel-footer'); if (footer) { var statusText = document.createElement('div'); statusText.id = 'magnet-status'; statusText.style.cssText = 'margin-bottom:8px;padding:10px 12px;background:rgba(26,31,46,0.92);border:1px solid rgba(0,212,170,0.3);border-radius:12px;font-size:12px;color:#8892a4'; footer.insertBefore(statusText, footer.firstChild); } } var restoreResult = await restoreProgressState(); var restored = !!(restoreResult && restoreResult.restored); var restoredState = restoreResult ? restoreResult.state : null; if (isListPage()) { var resumePage = Number.isFinite(Number(restoredState && restoredState.resumeFromPage)) ? Math.max(1, Number(restoredState.resumeFromPage)) : Math.max(1, Number(restoredState && restoredState.startPage) || 1); var resumeEndPage = Number.isFinite(Number(restoredState && restoredState.endPage)) ? Math.max(1, Number(restoredState.endPage)) : resumePage; var statusType = typeof (restoredState && restoredState.statusType) === 'string' ? restoredState.statusType : ''; var statusText = typeof (restoredState && restoredState.statusText) === 'string' ? restoredState.statusText : ''; var legacyRunningHint = statusType === 'loading' || /正在|开始|第\d+\/\d+页/.test(statusText); var shouldAutoResume = !!( restoredState && (restoredState.isRunning || legacyRunningHint) && !restoredState.stoppedByUser && resumePage <= resumeEndPage ); if (shouldAutoResume && !isFetching) { updateStatus('检测到未完成任务,正在自动继续...', 'loading'); fetchFromPage(resumePage, resumeEndPage, { preserveExisting: true, keyword: typeof restoredState.keyword === 'string' ? restoredState.keyword : '', speedMode: typeof restoredState.speedMode === 'string' ? restoredState.speedMode : 'fast' }); return; } if (!restored) { updateStatus('输入页码范围获取磁力', ''); } return; } var pageTitle = document.title || '当前帖子'; var magnets = extractMagnets(); if (magnets.length > 0) { magnets.forEach(function(m) { addMagnetItem(pageTitle, m); }); updateStatus('找到 ' + magnets.length + ' 个磁力', 'done'); } else if (!restored) { updateStatus('未找到磁力链接', 'error'); } } var currentUrl = window.location.hostname; if (currentUrl.includes('sehuatang')) { initializePluginUi().catch(function(e) { log('初始化失败: ' + e); }); setInterval(function() { var panelExists = !!document.getElementById('magnet-floating-panel'); var ballExists = !!document.getElementById('magnet-float-ball'); if (panelExists && ballExists) { return; } hasRestoredProgress = false; initializePluginUi().catch(function(e) { log('自动恢复UI失败: ' + e); }); }, 2500); } chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { if (request.action === 'getMagnets') { var magnets = extractMagnets(); sendResponse({ magnets: magnets, found: magnets.length > 0, count: magnets.length }); return; } if (request.action === 'copyMagnet') { var magnetsToCopy = extractMagnets(); if (magnetsToCopy.length === 0) { sendResponse({ found: false, count: 0 }); return; } navigator.clipboard.writeText(magnetsToCopy.join('\n')) .then(function() { sendResponse({ found: true, count: magnetsToCopy.length }); }) .catch(function(err) { var errorMsg = err && err.message ? err.message : '复制失败'; sendResponse({ found: false, count: magnetsToCopy.length, error: errorMsg }); }); return true; } }); })();