Files
sehuatang/content.js

2767 lines
100 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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{',
/* === 收藏按钮 === */',
'.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);',
'}',
'}',\t '#magnet-favorites-view .magnet-favorite-remove{',
' background:rgba(239,68,68,0.15);color:var(--m-error);',
'}',
/* === 吜索记录下拉 === */
'.magnet-history-dropdown{'
' background:rgba(239,68,68,0.15);color:var(--m-error);',
'}',
/* === 吜索记录下拉 === */
'.magnet-history-dropdown{'
'.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();
}
}
var resultsView = document.getElementById('magnet-results-view');
var cacheView = document.getElementById('magnet-cache-view');
var resultsBtn = document.getElementById('magnet-view-results');
var cacheBtn = document.getElementById('magnet-view-cache');
if (!resultsView || !cacheView || !resultsBtn || !cacheBtn) {
return;
}
var showCache = viewName === 'cache';
resultsView.classList.toggle('is-active', !showCache);
cacheView.classList.toggle('is-active', showCache);
resultsBtn.classList.toggle('is-active', !showCache);
cacheBtn.classList.toggle('is-active', showCache);
}
function createFloatingPanel() {
var existing = document.getElementById('magnet-floating-panel');
if (existing) return existing;
ensurePanelStyles();
var ball = document.createElement('div');
ball.id = 'magnet-float-ball';
ball.innerHTML = '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>';
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 = '<label style="display:block;padding:10px 16px;cursor:pointer;color:var(--m-text-primary)"><input type="checkbox" id="debug-check" style="accent-color:var(--m-accent);margin-right:8px;"' + (DEBUG_MODE ? ' checked' : '') + '> 调试模式</label>';
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 = '<div class="magnet-panel-header"><div class="magnet-panel-brand"><div class="magnet-panel-title">MAGNET LINKS</div><div class="magnet-panel-subtitle">智能抓取 · 缓存加速 · 一键复制</div></div><div class="magnet-panel-head-actions"><button id="magnet-view-results" class="magnet-panel-switch is-active">结果</button><button id="magnet-view-favorites" class="magnet-panel-switch">收藏</button><button id="magnet-view-cache" class="magnet-panel-switch">缓存</button><button class="magnet-panel-close" title="关闭">×</button></div></div><div class="magnet-settings" id="magnet-settings"></div><div class="magnet-panel-content"><div id="magnet-results-view" class="magnet-view is-active"><div class="magnet-view-toolbar"><div><div class="magnet-view-title">搜索结果</div><div class="magnet-view-meta">关键词命中的磁力链接</div></div><div class="magnet-view-meta">共 <span id="magnet-count-num">0</span> 条</div></div><div class="magnet-list" id="magnet-list"></div></div><div id="magnet-favorites-view" class="magnet-view"><div class="magnet-view-toolbar"><div><div class="magnet-view-title">我的收藏</div><div class="magnet-view-meta">持久保存的磁力链接</div></div><div class="magnet-view-toolbar-actions"><button id="magnet-clear-favorites" class="magnet-panel-switch">清空收藏</button></div></div><div id="magnet-favorites-list"></div></div><div id="magnet-cache-view" class="magnet-view"><div class="magnet-view-toolbar"><div><div class="magnet-view-title">缓存总览</div><div class="magnet-view-meta">数据统计与快照管理</div></div><div class="magnet-view-toolbar-actions"><button id="magnet-refresh-cache" class="magnet-panel-switch is-active">刷新</button><button id="magnet-clear-cache-inline" class="magnet-panel-switch">清空</button></div></div><div id="magnet-cache-panel"></div></div></div><div class="magnet-panel-footer"><div id="magnet-status">设置参数后开始抓取</div><div class="magnet-progress-container"><div class="magnet-progress-bar" id="magnet-progress-bar" style="width:0%"></div></div><div class="magnet-progress-text"><span id="magnet-progress-label">等待开始</span><span id="magnet-progress-percent">0%</span></div><button id="magnet-copy-all">一键复制全部</button></div>';
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 = '<div class="magnet-empty-state"><div class="magnet-empty-state-icon">📭</div><div class="magnet-empty-state-text">暂无收藏\br>点击结果列表中的 ♡ 按钮添加收藏</div></div>';
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 = '<div class="magnet-cache-card"><div class="magnet-cache-card-label">缓存状态</div><div class="magnet-cache-card-value" style="font-size:14px">暂无缓存数据</div></div>';
return;
}
var summary = cacheOverview.summary;
var recentThreads = Array.isArray(cacheOverview.recentThreads) ? cacheOverview.recentThreads : [];
var recentCoverages = Array.isArray(cacheOverview.recentCoverages) ? cacheOverview.recentCoverages : [];
var html = '';
html += '<div class="magnet-cache-card" style="margin-bottom:12px;background:linear-gradient(135deg,#f7fbff,#eef6ff)"><div class="magnet-cache-card-label">说明</div><div class="magnet-cache-entry-meta">这里的帖子数是 <strong>唯一帖子数</strong>,同一板块重复搜索同一帖子不会重复累计;重复搜索只会新增/更新范围快照。</div></div>';
html += '<div class="magnet-cache-grid">';
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">当前板块唯一帖子</div><div class="magnet-cache-card-value">' + summary.forumThreadCount + '</div><div class="magnet-cache-entry-meta">范围快照 ' + summary.forumCoverageCount + ' 份 · 页缓存 ' + Number(summary.forumPageCoverageCount || 0) + ' 页</div></div>';
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">全部唯一帖子</div><div class="magnet-cache-card-value">' + summary.totalThreadCount + '</div><div class="magnet-cache-entry-meta">总快照 ' + summary.totalCoverageCount + ' 份 · 页缓存 ' + Number(summary.totalPageCoverageCount || 0) + ' 页</div></div>';
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">磁链缓存</div><div class="magnet-cache-card-value">' + summary.recentMagnetCount + '</div><div class="magnet-cache-entry-meta">最近命中 ' + summary.recentMagnetCachedThreads + ' 帖</div></div>';
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">存储占用</div><div class="magnet-cache-card-value" style="font-size:16px">' + formatBytesLabel(summary.storageUsage) + '</div><div class="magnet-cache-entry-meta">配额 ' + (summary.storageQuota ? formatBytesLabel(summary.storageQuota) : '未知') + '</div></div>';
html += '</div>';
html += '<div class="magnet-cache-section"><div class="magnet-cache-section-title">最近缓存帖子</div>';
if (recentThreads.length === 0) {
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">提示</div><div class="magnet-cache-entry-meta">当前板块还没有缓存帖子</div></div>';
} else {
recentThreads.forEach(function(item) {
var title = item.title || '未命名帖子';
html += '<div class="magnet-cache-entry">';
html += '<div class="magnet-cache-entry-title">' + escapeHtml(title) + '</div>';
html += '<div class="magnet-cache-entry-meta">磁链缓存:' + Number(item.magnetCount || 0) + ' 条 · 帖子缓存:' + formatTimeLabel(item.lastSeenAt) + ' · 磁链缓存:' + formatTimeLabel(item.lastMagnetSyncAt) + '</div>';
html += '</div>';
});
}
html += '</div>';
html += '<div class="magnet-cache-section"><div class="magnet-cache-section-title">最近范围快照</div>';
if (recentCoverages.length === 0) {
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">提示</div><div class="magnet-cache-entry-meta">暂无范围快照</div></div>';
} else {
recentCoverages.forEach(function(item) {
html += '<div class="magnet-cache-entry">';
html += '<div class="magnet-cache-entry-title">页码 ' + item.startPage + '-' + item.endPage + '</div>';
html += '<div class="magnet-cache-entry-meta">' + item.threadCount + ' 帖 · 策略 ' + item.strategy + ' · ' + formatTimeLabel(item.crawledAt) + '</div>';
html += '</div>';
});
}
html += '</div>';
panel.innerHTML = html;
}
async function refreshCacheOverview(options) {
options = options || {};
var cachePanel = document.getElementById('magnet-cache-panel');
if (cachePanel) {
cachePanel.innerHTML = '<div style="padding:8px;color:#8892a4;font-size:11px">正在读取缓存...</div>';
}
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 = '<input type="text" id="keyword-input" placeholder="关键词(逗号分隔多关键词)" style="width:100%;padding:11px 14px;border:1px solid rgba(0,212,170,0.3);border-radius:12px;font-size:13px;box-sizing:border-box;background:#0f1419;color:#f0f4f8;box-shadow:inset 0 1px 2px rgba(0,0,0,0.2)">';
// 添加历史记录下拉功能
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 = '<span>页码范围</span><input type="number" id="page-start" value="' + currentPage + '" min="1" style="width:72px;padding:9px 10px;border:1px solid rgba(0,212,170,0.3);border-radius:12px;text-align:center;font-size:13px;background:#0f1419;color:#f0f4f8"><span>到</span><input type="number" id="page-end" value="' + currentPage + '" min="1" style="width:72px;padding:9px 10px;border:1px solid rgba(0,212,170,0.3);border-radius:12px;text-align:center;font-size:13px;background:#0f1419;color:#f0f4f8"><span>页</span>';
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 = '<span>抓取速度</span><select id="speed-select" style="flex:1;padding:10px 12px;border:1px solid rgba(0,212,170,0.3);border-radius:12px;font-size:13px;background:#0f1419;color:#f0f4f8"><option value="slow">慢</option><option value="medium">中</option><option value="fast" selected>快</option><option value="ultrafast">超快</option></select>';
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;
}
});
})();