(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-download-btn{',
' padding:8px 16px;',
' background:linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);',
' color:#fff;',
' 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-download-btn:hover{',
' transform:scale(1.05);',
' box-shadow:0 4px 15px rgba(37, 99, 235, 0.35);',
'}',
'.magnet-download-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:999px;overflow:hidden;',
'}',
'.magnet-progress-bar{',
' width:0%;height:100%;background:linear-gradient(90deg, var(--m-accent), #00f5c4);transition:width 0.25s ease;',
'}',
'.magnet-progress-text{',
' display:flex;justify-content:space-between;gap:12px;font-size:11px;color:var(--m-text-muted);',
'}',
/* === 结果项操作 === */
'.magnet-item-actions{',
' display:flex;gap:6px;flex-shrink:0;',
'}',
'.magnet-favorite-btn{',
' width:36px;height:36px;flex:0 0 36px;padding:0;display:inline-flex;align-items:center;justify-content:center;line-height:1;background:transparent;border:1px solid var(--m-border);border-radius:var(--m-radius-md);cursor:pointer;font-size:16px;color:var(--m-text-muted);transition:all 0.2s ease;',
'}',
'.magnet-favorite-btn:hover{',
' border-color:rgba(239,68,68,0.5);color:#ff7b8a;background:rgba(239,68,68,0.08);',
'}',
'.magnet-favorite-btn.is-favorite{',
' color:#ff4d6d;border-color:rgba(255,77,109,0.55);background:rgba(255,77,109,0.12);box-shadow:0 0 12px rgba(255,77,109,0.18);',
'}',
/* === 收藏视图 === */
'#magnet-favorites-list{',
' flex:1;min-height:0;overflow-y:auto;padding-right:6px;',
' scrollbar-width:thin;scrollbar-color:var(--m-accent) var(--m-bg-secondary);',
'}',
'#magnet-favorites-list::-webkit-scrollbar{width:6px;}',
'#magnet-favorites-list::-webkit-scrollbar-track{background:var(--m-bg-secondary);border-radius:3px;}',
'#magnet-favorites-list::-webkit-scrollbar-thumb{background:var(--m-accent);border-radius:3px;}',
'.magnet-favorite-item{',
' display:flex;align-items:center;gap:10px;padding:12px 14px;margin-bottom:8px;background:var(--m-bg-card);border:1px solid var(--m-border);border-radius:var(--m-radius-md);',
'}',
'.magnet-favorite-item:hover{',
' border-color:var(--m-border-accent);',
'}',
'.magnet-favorite-title{',
' flex:1;min-width:0;font-size:12px;color:var(--m-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;',
'}',
'.magnet-favorite-actions{',
' display:flex;gap:6px;flex-shrink:0;',
'}',
'.magnet-favorite-copy,.magnet-favorite-remove{',
' padding:6px 10px;border:none;border-radius:8px;cursor:pointer;font-size:11px;font-weight:700;',
'}',
'.magnet-favorite-copy{',
' background:linear-gradient(135deg, var(--m-accent), #00f5c4);color:var(--m-bg-deep);',
'}',
'.magnet-favorite-remove{',
' background:rgba(239,68,68,0.14);color:var(--m-error);',
'}',
/* === 历史记录下拉 === */
'.magnet-keyword-wrap{position:relative;}',
'.magnet-history-dropdown{',
' position:absolute;left:0;right:0;top:calc(100% + 6px);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:2147483647;overflow:hidden;',
' max-height:260px;display:flex;flex-direction:column;',
'}',
'.magnet-history-list{overflow-y:auto;max-height:214px;}',
'.magnet-history-item{',
' display:flex;align-items:center;gap:8px;padding:10px 12px;font-size:12px;color:var(--m-text-primary);cursor:pointer;transition:background 0.2s ease;',
'}',
'.magnet-history-item:hover{',
' background:var(--m-bg-secondary);',
'}',
'.magnet-history-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}',
'.magnet-history-delete{flex:0 0 auto;border:none;background:transparent;color:var(--m-text-muted);cursor:pointer;font-size:13px;line-height:1;padding:0 0 0 4px;font-weight:500;}',
'.magnet-history-delete:hover{color:var(--m-error);}',
'.magnet-history-clear{',
' padding:10px 12px;border-top:1px solid var(--m-border);font-size:11px;color:var(--m-error);cursor:pointer;text-align:center;position:sticky;bottom:0;background:var(--m-bg-card);',
'}',
/* === 云同步 === */
'.magnet-cloud-status{display:inline-flex;align-items:center;gap:6px;margin-left:8px;}',
'.magnet-cloud-dot{width:8px;height:8px;border-radius:50%;background:#ef4444;box-shadow:0 0 8px rgba(239,68,68,.35);flex:0 0 8px;}',
'.magnet-cloud-dot.is-ok{background:#10b981;box-shadow:0 0 8px rgba(16,185,129,.35);}',
'.magnet-cloud-label{font-size:11px;color:var(--m-text-secondary);}',
'.magnet-cloud-auth-card{padding:12px 14px;background:var(--m-bg-card);border:1px solid var(--m-border);border-radius:var(--m-radius-lg);display:flex;flex-direction:column;gap:10px;}',
'.magnet-cloud-auth-title{font-size:12px;font-weight:700;color:var(--m-text-primary);}',
'.magnet-cloud-auth-meta{font-size:11px;color:var(--m-text-secondary);line-height:1.6;}',
'.magnet-cloud-auth-row{display:flex;gap:8px;flex-wrap:wrap;}',
'.magnet-cloud-auth-row input[type="password"],.magnet-cloud-auth-row input[type="text"]{flex:1;min-width:0;}',
'.magnet-cloud-secondary-btn{padding:10px 14px;background:rgba(26,31,46,0.92);color:var(--m-text-secondary);border:1px solid var(--m-border);border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;}',
'.magnet-cloud-secondary-btn:hover{border-color:var(--m-border-accent);color:var(--m-text-primary);}',
'.magnet-cloud-danger-btn{padding:10px 14px;background:rgba(239,68,68,0.14);color:var(--m-error);border:1px solid rgba(239,68,68,0.25);border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;}',
'.magnet-cloud-danger-btn:hover{background:rgba(239,68,68,0.22);}',
'#magnet-settings input[type="password"]{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;}'
].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 cloudView = document.getElementById('magnet-cloud-view');
var resultsBtn = document.getElementById('magnet-view-results');
var cacheBtn = document.getElementById('magnet-view-cache');
var favoritesBtn = document.getElementById('magnet-view-favorites');
var cloudBtn = document.getElementById('magnet-view-cloud');
if (!resultsView || !cacheView || !favoritesView || !cloudView || !resultsBtn || !cacheBtn || !favoritesBtn || !cloudBtn) {
return;
}
resultsView.classList.toggle('is-active', viewName === 'results');
cacheView.classList.toggle('is-active', viewName === 'cache');
favoritesView.classList.toggle('is-active', viewName === 'favorites');
cloudView.classList.toggle('is-active', viewName === 'cloud');
resultsBtn.classList.toggle('is-active', viewName === 'results');
cacheBtn.classList.toggle('is-active', viewName === 'cache');
favoritesBtn.classList.toggle('is-active', viewName === 'favorites');
cloudBtn.classList.toggle('is-active', viewName === 'cloud');
if (viewName === 'favorites') {
renderFavoritesList();
} else if (viewName === 'cloud') {
renderCloudAuthSection();
}
}
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 = ' ';
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 = '
';
document.body.appendChild(panel);
var subtitle = panel.querySelector('.magnet-panel-subtitle');
if (subtitle) {
subtitle.innerHTML = '智能抓取 · 缓存加速 · 一键复制 云同步未登录 ';
}
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 favoritesSwitch = panel.querySelector('#magnet-view-favorites');
if (favoritesSwitch) {
favoritesSwitch.onclick = function() {
setPanelView('favorites');
};
}
var cloudSwitch = panel.querySelector('#magnet-view-cloud');
if (cloudSwitch) {
cloudSwitch.onclick = function() {
setPanelView('cloud');
};
}
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 clearFavoritesBtn = panel.querySelector('#magnet-clear-favorites');
if (clearFavoritesBtn) {
clearFavoritesBtn.onclick = function() {
if (!confirm('确认清空全部收藏吗?')) {
return;
}
saveFavorites([]);
renderFavoritesList();
};
}
var closeBtn = panel.querySelector('.magnet-panel-close');
if (closeBtn) closeBtn.onclick = function() {
panel.style.display = 'none';
ball.style.display = 'flex';
};
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 isThreadPage() {
return /\/thread-\d+-/i.test(window.location.href) || /[?&]tid=\d+/i.test(window.location.href);
}
function isListPage() {
if (isThreadPage()) {
return false;
}
if (document.querySelector('#threadlisttableid')) {
return true;
}
if (document.querySelector('tbody[id^="normalthread_"]') || document.querySelector('tbody[id^="stickthread_"]')) {
return true;
}
if (/\/forum-\d+(?:-\d+)?\.html/i.test(window.location.href) || /[?&]mod=forumdisplay/i.test(window.location.href) || /[?&]fid=\d+/i.test(window.location.href)) {
return true;
}
return false;
}
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 getCurrentPage() {
var currentUrl = window.location.href;
var match = currentUrl.match(/forum-\d+-(\d+)\.html/i) || currentUrl.match(/[?&]page=(\d+)/i);
return match ? Math.max(1, Number(match[1]) || 1) : 1;
}
function getLastPage() {
var pageLinks = document.querySelectorAll('a[href*="forum-"][href$=".html"], .pg a, .pgt a');
var maxPage = getCurrentPage();
pageLinks.forEach(function(link) {
var href = link && link.href ? link.href : '';
var text = link && link.textContent ? link.textContent.trim() : '';
var match = href.match(/forum-\d+-(\d+)\.html/i) || href.match(/[?&]page=(\d+)/i);
var pageNum = match ? Number(match[1]) : Number(text);
if (Number.isFinite(pageNum) && pageNum > maxPage) {
maxPage = pageNum;
}
});
return Math.max(1, maxPage);
}
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');
var percent = total > 0 ? Math.max(0, Math.min(100, Math.round(current / total * 100))) : 0;
if (progressBar) {
progressBar.style.width = percent + '%';
}
if (progressLabel) {
progressLabel.textContent = label || ('进度 ' + current + '/' + total);
}
if (progressPercent) {
progressPercent.textContent = percent + '%';
}
}
function resetProgress() {
updateProgress(0, 0, '等待开始');
}
function buildRangeProgressLabel(rangeStart, rangeEnd, threadIndex, threadTotal) {
return '页' + rangeStart + '-' + rangeEnd + '/帖子' + threadIndex + '/' + threadTotal;
}
function updateRangeThreadProgress(context, rangeStart, rangeEnd, threadIndex, threadTotal) {
var totalPages = Math.max(1, Number(context.totalPageCount) || (Number(context.normalizedEnd || 0) - Number(context.startPage || 0) + 1));
var safeRangeStart = Math.max(Number(context.startPage) || 1, Number(rangeStart) || Number(context.startPage) || 1);
var safeRangeEnd = Math.max(safeRangeStart, Number(rangeEnd) || safeRangeStart);
var completedBefore = Math.max(0, safeRangeStart - (Number(context.startPage) || 1));
var coveredPages = Math.max(1, safeRangeEnd - safeRangeStart + 1);
var fractional = threadTotal > 0 ? Math.max(0, Math.min(1, Number(threadIndex) / Number(threadTotal))) : 0;
var completedPages = completedBefore + coveredPages * fractional;
updateProgress(completedPages, totalPages, buildRangeProgressLabel(safeRangeStart, safeRangeEnd, threadIndex, threadTotal));
}
var FAVORITES_KEY = 'magnet-favorites';
var SEARCH_HISTORY_KEY = 'magnet-search-history';
var MAX_SEARCH_HISTORY = 20;
var PRIVATE_STORAGE_KEY = 'magnet-private-state-v1';
var privateStateLoaded = false;
var favoritesCache = [];
var searchHistoryCache = [];
var sessionBackupState = null;
function cloneSimpleArray(value) {
return JSON.parse(JSON.stringify(Array.isArray(value) ? value : []));
}
function loadPrivateStateFromExtensionStorage() {
return new Promise(function(resolve) {
var migratedFavorites = [];
var migratedHistory = [];
var state = null;
if (privateStateLoaded) {
resolve({ favorites: cloneSimpleArray(favoritesCache), searchHistory: cloneSimpleArray(searchHistoryCache) });
return;
}
chrome.storage.local.get(PRIVATE_STORAGE_KEY, function(result) {
state = result && result[PRIVATE_STORAGE_KEY] && typeof result[PRIVATE_STORAGE_KEY] === 'object' ? result[PRIVATE_STORAGE_KEY] : {};
favoritesCache = Array.isArray(state.favorites) ? state.favorites : [];
searchHistoryCache = Array.isArray(state.searchHistory) ? state.searchHistory : [];
if (favoritesCache.length === 0) {
try {
migratedFavorites = JSON.parse(localStorage.getItem(FAVORITES_KEY) || '[]');
favoritesCache = Array.isArray(migratedFavorites) ? migratedFavorites : [];
localStorage.removeItem(FAVORITES_KEY);
} catch (e) {
favoritesCache = [];
}
}
if (searchHistoryCache.length === 0) {
try {
migratedHistory = JSON.parse(localStorage.getItem(SEARCH_HISTORY_KEY) || '[]');
searchHistoryCache = Array.isArray(migratedHistory) ? migratedHistory : [];
localStorage.removeItem(SEARCH_HISTORY_KEY);
} catch (e) {
searchHistoryCache = [];
}
}
privateStateLoaded = true;
persistPrivateState();
resolve({ favorites: cloneSimpleArray(favoritesCache), searchHistory: cloneSimpleArray(searchHistoryCache) });
});
});
}
function persistPrivateState() {
if (!chrome.storage || !chrome.storage.local) {
return;
}
chrome.storage.local.set((function() {
var data = {};
data[PRIVATE_STORAGE_KEY] = {
favorites: favoritesCache,
searchHistory: searchHistoryCache
};
return data;
})());
}
function loadFavorites() {
return cloneSimpleArray(favoritesCache);
}
function saveFavorites(favorites, options) {
options = options || {};
favoritesCache = Array.isArray(favorites) ? favorites : [];
persistPrivateState();
if (!options.skipCloudSync) {
scheduleCloudVaultSync();
}
}
function isFavorited(link) {
return loadFavorites().some(function(item) {
return item && item.link === link;
});
}
function toggleFavorite(title, link, favoriteBtn) {
var favorites = loadFavorites();
var foundIndex = favorites.findIndex(function(item) {
return item.link === link;
});
if (foundIndex >= 0) {
favorites.splice(foundIndex, 1);
if (favoriteBtn) {
favoriteBtn.classList.remove('is-favorite');
favoriteBtn.textContent = '❤';
favoriteBtn.title = '收藏';
}
} else {
favorites.unshift({
title: title || '未命名帖子',
link: link,
addedAt: Date.now()
});
if (favoriteBtn) {
favoriteBtn.classList.add('is-favorite');
favoriteBtn.textContent = '❤';
favoriteBtn.title = '取消收藏';
}
}
saveFavorites(favorites);
var favoritesView = document.getElementById('magnet-favorites-view');
if (favoritesView && favoritesView.classList.contains('is-active')) {
renderFavoritesList();
}
}
function renderFavoritesList() {
var favoritesList = document.getElementById('magnet-favorites-list');
if (!favoritesList) return;
var favorites = loadFavorites();
favoritesList.innerHTML = '';
if (favorites.length === 0) {
favoritesList.innerHTML = '';
return;
}
favorites.forEach(function(favorite) {
var item = document.createElement('div');
item.className = 'magnet-favorite-item';
var title = document.createElement('div');
title.className = 'magnet-favorite-title';
title.textContent = favorite.title || '未命名帖子';
title.title = favorite.title || '未命名帖子';
var actions = document.createElement('div');
actions.className = 'magnet-favorite-actions';
var copyBtn = document.createElement('button');
copyBtn.className = 'magnet-favorite-copy';
copyBtn.textContent = '复制';
copyBtn.onclick = function() {
navigator.clipboard.writeText(favorite.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() {
saveFavorites(loadFavorites().filter(function(itemData) {
return itemData.link !== favorite.link;
}));
renderFavoritesList();
};
actions.appendChild(copyBtn);
actions.appendChild(removeBtn);
item.appendChild(title);
item.appendChild(actions);
favoritesList.appendChild(item);
});
}
function loadSearchHistory() {
return cloneSimpleArray(searchHistoryCache);
}
function removeSearchHistoryItem(keyword) {
var value = String(keyword || '').trim();
if (!value) {
return;
}
setSearchHistoryList(loadSearchHistory().filter(function(item) {
return item !== value;
}), { skipCloudSync: false });
}
function saveSearchHistory(keyword) {
var value = String(keyword || '').trim();
if (!value) return;
var history = loadSearchHistory().filter(function(item) {
return item !== value;
});
history.unshift(value);
setSearchHistoryList(history.slice(0, MAX_SEARCH_HISTORY));
}
function removeHistoryDropdown() {
var dropdown = document.getElementById('magnet-history-dropdown');
if (dropdown) {
dropdown.remove();
}
}
function showHistoryDropdown(input) {
if (!input) return;
removeHistoryDropdown();
var history = loadSearchHistory();
var list = null;
if (history.length === 0) return;
var wrap = input.parentNode;
if (!wrap) return;
wrap.classList.add('magnet-keyword-wrap');
var dropdown = document.createElement('div');
dropdown.id = 'magnet-history-dropdown';
dropdown.className = 'magnet-history-dropdown';
list = document.createElement('div');
list.className = 'magnet-history-list';
history.forEach(function(historyKeyword) {
var item = document.createElement('div');
item.className = 'magnet-history-item';
var text = document.createElement('div');
var removeBtn = document.createElement('button');
text.className = 'magnet-history-text';
text.textContent = historyKeyword;
removeBtn.className = 'magnet-history-delete';
removeBtn.type = 'button';
removeBtn.textContent = '×';
removeBtn.title = '删除这条历史记录';
item.onclick = function() {
input.value = historyKeyword;
removeHistoryDropdown();
};
removeBtn.onclick = function(event) {
event.preventDefault();
event.stopPropagation();
removeSearchHistoryItem(historyKeyword);
showHistoryDropdown(input);
};
item.appendChild(text);
item.appendChild(removeBtn);
list.appendChild(item);
});
dropdown.appendChild(list);
var clearItem = document.createElement('div');
clearItem.className = 'magnet-history-clear';
clearItem.textContent = '清空历史记录';
clearItem.onclick = function() {
setSearchHistoryList([], { skipCloudSync: false });
removeHistoryDropdown();
};
dropdown.appendChild(clearItem);
wrap.appendChild(dropdown);
setTimeout(function() {
document.addEventListener('click', function hideHistory(event) {
if (!dropdown.contains(event.target) && event.target !== input) {
removeHistoryDropdown();
document.removeEventListener('click', hideHistory);
}
});
}, 0);
}
function playDoneTone() {
try {
var AudioContextCtor = window.AudioContext || window.webkitAudioContext;
if (!AudioContextCtor) return;
var context = new AudioContextCtor();
var oscillator = context.createOscillator();
var gainNode = context.createGain();
oscillator.connect(gainNode);
gainNode.connect(context.destination);
oscillator.type = 'sine';
oscillator.frequency.value = 880;
gainNode.gain.setValueAtTime(0.0001, context.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.18, context.currentTime + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.0001, context.currentTime + 0.35);
oscillator.start(context.currentTime);
oscillator.stop(context.currentTime + 0.35);
} catch (e) {
log('提示音播放失败: ' + e);
}
}
function showDoneNotification(title, body) {
if (!('Notification' in window)) return;
if (Notification.permission === 'granted') {
new Notification(title, { body: body });
return;
}
if (Notification.permission !== 'denied') {
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
new Notification(title, { body: body });
}
});
}
}
var cloudSyncState = {
authenticated: false,
healthy: false,
color: 'red',
text: '云同步未登录',
email: '',
lastError: ''
};
var cloudVaultHydrated = false;
var cloudVaultSyncTimer = null;
function updateCloudSyncIndicator(status) {
var dot = document.getElementById('magnet-cloud-sync-dot');
var label = document.getElementById('magnet-cloud-sync-text');
cloudSyncState = Object.assign({}, cloudSyncState, status || {});
if (dot) {
dot.classList.toggle('is-ok', cloudSyncState.color === 'green');
}
if (label) {
label.textContent = cloudSyncState.text || (cloudSyncState.color === 'green' ? '云同步正常' : '云同步异常');
label.title = cloudSyncState.email ? (cloudSyncState.text + ' · ' + cloudSyncState.email) : (cloudSyncState.lastError || cloudSyncState.text || '云同步');
}
}
async function getCloudSyncStatus() {
try {
var response = await sendRuntimeMessage({ action: 'cloudGetSyncStatus' }, 12000);
if (response && response.ok) {
updateCloudSyncIndicator(response);
return response;
}
updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: response && response.error ? response.error : '状态获取失败' });
return null;
} catch (e) {
updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
return null;
}
}
function getUiSettingsSnapshot() {
return {
speedMode: speedConfig[speedMode] ? speedMode : 'fast'
};
}
function applyUiSettingsSnapshot(snapshot) {
if (!snapshot || typeof snapshot !== 'object') {
return;
}
if (snapshot.speedMode && speedConfig[snapshot.speedMode]) {
speedMode = snapshot.speedMode;
var speedSelect = document.getElementById('speed-select');
if (speedSelect) {
speedSelect.value = speedMode;
}
}
}
function setSearchHistoryList(historyList, options) {
var normalized = [];
var seen = Object.create(null);
options = options || {};
(Array.isArray(historyList) ? historyList : []).forEach(function(item) {
var value = String(item || '').trim();
if (!value || seen[value]) {
return;
}
seen[value] = true;
normalized.push(value);
});
searchHistoryCache = normalized.slice(0, MAX_SEARCH_HISTORY);
persistPrivateState();
if (!options.skipCloudSync) {
scheduleCloudVaultSync();
}
}
function openCloudSyncCenter() {
sendRuntimeMessage({ action: 'openCloudSyncPage' }, 8000).catch(function(e) {
updateStatus('打开云同步中心失败:' + (e && e.message ? e.message : String(e)), 'error');
});
}
function triggerMagnetDownload(link, downloadBtn) {
var anchor = null;
var cleanup = null;
var fallbackCopy = function(message, type) {
navigator.clipboard.writeText(link).then(function() {
updateStatus(message || '未检测到本地下载器,已复制磁力链接。请在扩展页下载 qBittorrent(推荐节点1/2)', type || 'done');
}).catch(function() {
updateStatus('未能调用本地下载器,请手动复制磁力链接', 'error');
});
};
try {
updateStatus('正在调用本地下载器;如未安装,可到扩展页下载 qBittorrent(推荐节点1/2)', 'loading');
anchor = document.createElement('a');
anchor.href = link;
anchor.style.display = 'none';
anchor.rel = 'noreferrer noopener';
document.body.appendChild(anchor);
anchor.click();
if (downloadBtn) {
downloadBtn.textContent = '已发送';
cleanup = function() {
downloadBtn.textContent = '下载';
};
setTimeout(cleanup, 1200);
}
setTimeout(function() {
updateStatus('已发送到本地下载器,如未弹出请先安装并关联 qBittorrent', 'done');
}, 300);
} catch (e) {
fallbackCopy('未检测到本地下载器,已复制磁力链接。请在扩展页下载 qBittorrent(推荐节点1/2)', 'done');
} finally {
if (anchor && anchor.parentNode) {
anchor.parentNode.removeChild(anchor);
}
}
}
function mergeFavoritesList(localItems, remoteItems) {
var merged = [];
var seen = Object.create(null);
(Array.isArray(remoteItems) ? remoteItems : []).concat(Array.isArray(localItems) ? localItems : []).forEach(function(item) {
var link = item && typeof item.link === 'string' ? item.link : '';
if (!link || seen[link]) {
return;
}
seen[link] = true;
merged.push({
title: typeof item.title === 'string' ? item.title : '未命名帖子',
link: link,
addedAt: Number(item.addedAt) || Date.now()
});
});
return merged;
}
function mergeHistoryList(localItems, remoteItems) {
var merged = [];
var seen = Object.create(null);
(Array.isArray(remoteItems) ? remoteItems : []).concat(Array.isArray(localItems) ? localItems : []).forEach(function(item) {
var value = String(item || '').trim();
if (!value || seen[value]) {
return;
}
seen[value] = true;
merged.push(value);
});
return merged.slice(0, MAX_SEARCH_HISTORY);
}
async function pushCloudVaultItems(items) {
try {
var response = await sendRuntimeMessage({ action: 'cloudPushVaultItems', items: items }, 20000);
if (response && response.status) {
updateCloudSyncIndicator(response.status);
}
return response;
} catch (e) {
updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
return null;
}
}
async function pullCloudVaultItems(itemTypes) {
try {
var response = await sendRuntimeMessage({ action: 'cloudPullVaultItems', itemTypes: itemTypes }, 20000);
if (response && response.ok) {
if (response.status) {
updateCloudSyncIndicator(response.status);
}
return response.items || [];
}
return [];
} catch (e) {
updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
return [];
}
}
function scheduleCloudVaultSync() {
if (cloudVaultSyncTimer) {
clearTimeout(cloudVaultSyncTimer);
}
cloudVaultSyncTimer = setTimeout(function() {
cloudVaultSyncTimer = null;
syncAllLocalVaultData();
}, 600);
}
async function syncAllLocalVaultData() {
if (!cloudSyncState.authenticated) {
return null;
}
return pushCloudVaultItems([
{ itemType: 'favorites', itemKey: 'default', data: loadFavorites() },
{ itemType: 'search_history', itemKey: 'default', data: loadSearchHistory() },
{ itemType: 'ui_settings', itemKey: 'default', data: getUiSettingsSnapshot() }
]);
}
function applyVaultItems(items, options) {
var remoteFavorites = [];
var remoteHistory = [];
var remoteSettings = null;
var remoteProgress = null;
options = options || {};
(Array.isArray(items) ? items : []).forEach(function(item) {
if (!item || typeof item !== 'object') {
return;
}
if (item.itemType === 'favorites' && Array.isArray(item.data)) {
remoteFavorites = item.data;
} else if (item.itemType === 'search_history' && Array.isArray(item.data)) {
remoteHistory = item.data;
} else if (item.itemType === 'ui_settings' && item.data && typeof item.data === 'object') {
remoteSettings = item.data;
} else if (item.itemType === 'progress_state' && item.data && typeof item.data === 'object') {
remoteProgress = item.data;
}
});
if (remoteFavorites.length > 0) {
saveFavorites(mergeFavoritesList(loadFavorites(), remoteFavorites), { skipCloudSync: true });
}
if (remoteHistory.length > 0) {
setSearchHistoryList(mergeHistoryList(loadSearchHistory(), remoteHistory), { skipCloudSync: true });
}
if (remoteSettings) {
applyUiSettingsSnapshot(remoteSettings);
}
if (options.restoreProgress && remoteProgress && allMagnetLinks.length === 0) {
applyStateSnapshot(remoteProgress);
}
cloudVaultHydrated = true;
}
async function refreshCloudStatusAndVault(options) {
var status = await getCloudSyncStatus();
if (!status) {
renderCloudAuthSection();
return null;
}
if (status.authenticated && !cloudVaultHydrated && !options?.skipVaultPull) {
applyVaultItems(await pullCloudVaultItems(['favorites', 'search_history', 'ui_settings', 'progress_state']), { restoreProgress: !!options?.restoreProgress });
}
renderCloudAuthSection();
return status;
}
async function logoutCloudAccount() {
try {
var response = await sendRuntimeMessage({ action: 'cloudLogout' }, 15000);
cloudVaultHydrated = false;
updateCloudSyncIndicator(response && response.status ? response.status : { color: 'red', text: '云同步未登录', authenticated: false, healthy: false, email: '' });
renderCloudAuthSection();
updateStatus('已退出云同步', 'done');
} catch (e) {
updateCloudSyncIndicator({ color: 'red', text: '云同步异常', lastError: e && e.message ? e.message : String(e) });
updateStatus('退出失败:' + (e && e.message ? e.message : String(e)), 'error');
}
}
function renderCloudAuthSection() {
var settingsArea = document.getElementById('magnet-cloud-page');
var existing = document.getElementById('magnet-cloud-auth-card');
var viewMeta = document.getElementById('magnet-cloud-view-meta');
var html = '';
var card = null;
var syncNowBtn = null;
var logoutBtn = null;
var openCenterBtn = null;
if (!settingsArea) {
return;
}
if (existing) {
existing.remove();
}
card = document.createElement('div');
card.id = 'magnet-cloud-auth-card';
card.className = 'magnet-cloud-auth-card';
if (viewMeta) {
viewMeta.textContent = cloudSyncState.email ? (cloudSyncState.text + ' · ' + cloudSyncState.email) : (cloudSyncState.text || '状态未知');
}
if (cloudSyncState.authenticated) {
html = '云同步已开启
账号:' + escapeHtml(cloudSyncState.email || '已登录') + ' ' + escapeHtml(cloudSyncState.text || '云同步正常') + '
立即同步 打开云同步中心 退出登录
';
} else {
html = '云同步账号
为了安全,登录/注册已移到扩展独立页面中进行。网页本身不会再承载账号密码输入框。
打开云同步中心
';
}
card.innerHTML = html;
settingsArea.innerHTML = '';
settingsArea.appendChild(card);
if (cloudSyncState.authenticated) {
syncNowBtn = card.querySelector('#magnet-cloud-sync-now');
logoutBtn = card.querySelector('#magnet-cloud-logout');
openCenterBtn = card.querySelector('#magnet-cloud-open-center');
if (syncNowBtn) {
syncNowBtn.onclick = function() {
updateStatus('正在同步保险柜...', 'loading');
syncAllLocalVaultData().then(function() {
updateStatus('保险柜同步完成', 'done');
}).catch(function(e) {
updateStatus('保险柜同步失败:' + (e && e.message ? e.message : String(e)), 'error');
});
};
}
if (logoutBtn) {
logoutBtn.onclick = logoutCloudAccount;
}
if (openCenterBtn) {
openCenterBtn.onclick = openCloudSyncCenter;
}
} else {
openCenterBtn = card.querySelector('#magnet-cloud-open-center');
if (openCenterBtn) {
openCenterBtn.onclick = openCloudSyncCenter;
}
}
}
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 actions = document.createElement('div');
actions.className = 'magnet-item-actions';
var favoriteBtn = document.createElement('button');
favoriteBtn.className = 'magnet-favorite-btn';
favoriteBtn.textContent = '❤';
favoriteBtn.title = '收藏';
if (isFavorited(safeLink)) {
favoriteBtn.classList.add('is-favorite');
favoriteBtn.textContent = '❤';
favoriteBtn.title = '取消收藏';
}
favoriteBtn.onclick = function() {
toggleFavorite(safeTitle, safeLink, favoriteBtn);
};
var copyBtn = document.createElement('button');
copyBtn.className = 'magnet-copy-btn';
copyBtn.setAttribute('data-magnet', safeLink);
copyBtn.textContent = '复制';
var downloadBtn = document.createElement('button');
downloadBtn.className = 'magnet-download-btn';
downloadBtn.setAttribute('data-magnet', safeLink);
downloadBtn.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');
});
};
downloadBtn.onclick = function() {
triggerMagnetDownload(safeLink, downloadBtn);
};
item.appendChild(titleEl);
actions.appendChild(favoriteBtn);
actions.appendChild(copyBtn);
actions.appendChild(downloadBtn);
item.appendChild(actions);
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 escapeHtml(value) {
return String(value || '').replace(/[&<>"']/g, function(character) {
return {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[character] || character;
});
}
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 probeForumUpdateDepth(forumKey, baseUrl, startPage, endPage) {
if (startPage !== 1) {
return { refreshPages: 0, probeThreads: null, reason: 'not_from_page1' };
}
var totalRange = endPage - startPage + 1;
var maxRefresh = Math.min(20, totalRange);
var probeUrl = baseUrl + '1.html';
try {
var response = await sendRuntimeMessage({
action: 'fetchHtml',
url: probeUrl,
timeoutMs: getFetchTimeout()
}, getMessageTimeout());
if (!response || response.error || !response.html) {
log('[探针] 获取第1页失败,回退到固定刷新');
return { refreshPages: maxRefresh, probeThreads: null, reason: 'probe_fetch_failed' };
}
var liveThreads = extractThreadsFromHtml(response.html);
var threadsPerPage = liveThreads.length || 25;
if (liveThreads.length === 0) {
log('[探针] 第1页未提取到帖子,回退到固定刷新');
return { refreshPages: maxRefresh, probeThreads: null, reason: 'no_threads_extracted' };
}
var cachedPage = await sendRuntimeMessage({
action: 'cacheGetPageThreadKeys',
forumKey: forumKey,
page: 1
}, 5000);
if (!cachedPage || !cachedPage.ok || !cachedPage.threadKeys || cachedPage.threadKeys.length === 0) {
log('[探针] 无第1页缓存记录,首次抓取');
return { refreshPages: maxRefresh, probeThreads: liveThreads, probeHtml: response.html, reason: 'no_cached_page1' };
}
var cachedKeySet = Object.create(null);
cachedPage.threadKeys.forEach(function(key) {
var pureKey = String(key || '').split('::').pop();
cachedKeySet[pureKey] = true;
});
var newCount = 0;
liveThreads.forEach(function(thread) {
var key = thread.threadKey || getThreadKeyFromUrl(thread.url);
if (key && !cachedKeySet[key]) {
newCount++;
}
});
if (newCount === 0) {
log('[探针] 第1页与缓存完全一致,无需刷新');
return { refreshPages: 0, probeThreads: liveThreads, probeHtml: response.html, reason: 'no_change', newCount: 0 };
}
var estimatedShift = Math.ceil(newCount / threadsPerPage) + 1;
var refreshPages = Math.min(estimatedShift, maxRefresh);
log('[探针] 检测到 ' + newCount + ' 个新帖 (每页' + threadsPerPage + '帖),预估影响 ' + estimatedShift + ' 页,将刷新前 ' + refreshPages + ' 页');
return {
refreshPages: refreshPages,
probeThreads: liveThreads,
probeHtml: response.html,
newCount: newCount,
threadsPerPage: threadsPerPage,
reason: 'detected_' + newCount + '_new'
};
} catch (e) {
var errMsg = e && e.message ? e.message : String(e);
log('[探针] 异常: ' + errMsg + ',回退到固定刷新');
return { refreshPages: maxRefresh, probeThreads: null, reason: 'probe_error' };
}
}
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 + '
范围快照 ' + summary.forumCoverageCount + ' 份 · 页缓存 ' + Number(summary.forumPageCoverageCount || 0) + ' 页
';
html += '
全部唯一帖子
' + summary.totalThreadCount + '
总快照 ' + summary.totalCoverageCount + ' 份 · 页缓存 ' + Number(summary.totalPageCoverageCount || 0) + ' 页
';
html += '
磁链缓存
' + summary.recentMagnetCount + '
最近命中 ' + summary.recentMagnetCachedThreads + ' 帖
';
html += '
存储占用
' + formatBytesLabel(summary.storageUsage) + '
配额 ' + (summary.storageQuota ? formatBytesLabel(summary.storageQuota) : '未知') + '
';
html += '
';
html += '最近缓存帖子
';
if (recentThreads.length === 0) {
html += '
';
} else {
recentThreads.forEach(function(item) {
var title = item.title || '未命名帖子';
html += '
';
html += '
' + escapeHtml(title) + '
';
html += '
磁链缓存:' + Number(item.magnetCount || 0) + ' 条 · 帖子缓存:' + formatTimeLabel(item.lastSeenAt) + ' · 磁链缓存:' + formatTimeLabel(item.lastMagnetSyncAt) + '
';
html += '
';
});
}
html += '
';
html += '最近范围快照
';
if (recentCoverages.length === 0) {
html += '
';
} else {
recentCoverages.forEach(function(item) {
html += '
';
html += '
页码 ' + item.startPage + '-' + item.endPage + '
';
html += '
' + item.threadCount + ' 帖 · 策略 ' + item.strategy + ' · ' + formatTimeLabel(item.crawledAt) + '
';
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() {
sessionBackupState = null;
}
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) {
sessionBackupState = snapshot;
}
function readStateFromSessionBackup() {
return sessionBackupState;
}
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;
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;
}
if (!options.suppressStatusProgress) {
updateStatus('页' + page + '/帖子' + (currentIndex + 1) + '/' + filteredThreads.length, 'loading');
}
if (options.onProgress) {
options.onProgress(currentIndex + 1, filteredThreads.length);
}
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) {
try {
await saveThreadMagnetsToCache(options.forumKey, [{
threadKey: threadKey,
url: threadUrl,
title: threadTitle,
magnets: threadMagnets
}]);
} catch (saveErr) {
log('实时保存线程缓存失败: ' + (saveErr && saveErr.message ? saveErr.message : String(saveErr)));
}
}
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);
return fetchedCount;
}
async function processCachedThreadBatch(pageLabel, threads, context, statusText) {
var normalizedThreads = normalizeCachedThreads(threads);
var rangeText = typeof pageLabel === 'string' ? pageLabel : String(pageLabel || '');
var rangeMatch = rangeText.match(/^(\d+)(?:-(\d+))?$/);
var rangeStart = rangeMatch ? Number(rangeMatch[1]) : Number(context.startPage || 1);
var rangeEnd = rangeMatch ? Math.max(rangeStart, Number(rangeMatch[2] || rangeMatch[1])) : rangeStart;
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,
suppressStatusProgress: true,
onProgress: function(threadIndex, threadTotal) {
updateRangeThreadProgress(context, rangeStart, rangeEnd, threadIndex, threadTotal);
}
}
);
}
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');
updateProgress(page - context.startPage + 1, context.normalizedEnd - context.startPage + 1, '第' + 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 panel = createFloatingPanel();
var ball = document.getElementById('magnet-float-ball');
var startTime = Date.now();
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 searchContext = {
forumKey: forumKey,
baseUrl: baseUrl,
startPage: normalizedStart,
normalizedEnd: normalizedEnd,
totalPageCount: normalizedEnd - normalizedStart + 1,
keywords: buildKeywordList(keyword),
coverageThreadMap: Object.create(null),
processedThreadKeys: Object.create(null),
allMagnets: new Set(allMagnetLinks),
matchedThreads: 0,
totalFetched: 0,
failedPages: 0
};
var frontRefreshPages = getSmartFrontRefreshPages(normalizedStart, normalizedEnd);
updateStatus('正在规划缓存:检查本地缓存 / 云端规划 / 复用块...', 'loading');
var cachePlan = await getCachedCoveragePlan(forumKey, normalizedStart, normalizedEnd, frontRefreshPages);
var cacheStrategy = 'full_live';
var probeResult = null;
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();
if (normalizedStart === 1) {
updateStatus('智能探测:检测论坛是否有更新...', 'loading');
probeResult = await probeForumUpdateDepth(forumKey, baseUrl, normalizedStart, normalizedEnd);
if (probeResult.refreshPages > 0) {
frontRefreshPages = probeResult.refreshPages;
log('[智能更新] 缓存命中,探针检测到' + (probeResult.newCount || 0) + '个新帖,刷新前' + frontRefreshPages + '页');
updateStatus('发现' + (probeResult.newCount || 0) + '个新帖,刷新前' + frontRefreshPages + '页...', 'loading');
if (probeResult.probeThreads) {
mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
var probeFiltered = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
searchContext.matchedThreads += probeFiltered.length;
var probeCacheResult = await applyCachedMagnetHits(1, probeFiltered, searchContext);
if (probeCacheResult.threadsToFetch.length > 0) {
searchContext.totalFetched += await fetchThreadsInParallel(
1, probeCacheResult.threadsToFetch, searchContext.allMagnets,
searchContext.processedThreadKeys, { forumKey: forumKey }
);
}
}
var exactRefreshEnd = Math.min(normalizedEnd, normalizedStart + frontRefreshPages - 1);
if (exactRefreshEnd >= 2) {
await fetchLivePageRange(2, exactRefreshEnd, searchContext);
}
if (!stopFetching) {
await saveCoverageSnapshot(
forumKey, normalizedStart, normalizedEnd,
coverageMapToList(searchContext.coverageThreadMap),
frontRefreshPages, 'exact_cache_refreshed'
);
}
} else {
log('[智能更新] 缓存命中,探针未检测到变化,补充处理探针数据以填补缓存缺口');
if (probeResult.probeThreads && probeResult.probeThreads.length > 0) {
mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
var gapFiltered = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
if (gapFiltered.length > 0) {
var gapCacheResult = await applyCachedMagnetHits(1, gapFiltered, searchContext);
if (gapCacheResult.threadsToFetch.length > 0) {
log('[智能更新] 探针填补缺口:发现' + gapCacheResult.threadsToFetch.length + '个缓存未覆盖的帖子,补抓中...');
searchContext.totalFetched += await fetchThreadsInParallel(
1, gapCacheResult.threadsToFetch, searchContext.allMagnets,
searchContext.processedThreadKeys, { forumKey: forumKey }
);
}
}
}
}
}
} else {
if (normalizedStart === 1) {
updateStatus('智能探测:检测论坛更新情况...', 'loading');
probeResult = await probeForumUpdateDepth(forumKey, baseUrl, normalizedStart, normalizedEnd);
frontRefreshPages = probeResult.refreshPages;
if (probeResult.reason === 'no_change') {
log('[智能更新] 探针未检测到变化,补充处理探针数据以填补缓存缺口');
if (probeResult.probeThreads && probeResult.probeThreads.length > 0) {
mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
var gapFiltered3 = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
if (gapFiltered3.length > 0) {
var gapCacheResult3 = await applyCachedMagnetHits(1, gapFiltered3, searchContext);
if (gapCacheResult3.threadsToFetch.length > 0) {
log('[智能更新] 探针填补缺口:发现' + gapCacheResult3.threadsToFetch.length + '个缓存未覆盖的帖子,补抓中...');
searchContext.totalFetched += await fetchThreadsInParallel(
1, gapCacheResult3.threadsToFetch, searchContext.allMagnets,
searchContext.processedThreadKeys, { forumKey: forumKey }
);
}
}
}
} else if (frontRefreshPages > 0) {
log('[智能更新] 探针检测到更新,需刷新前' + frontRefreshPages + '页');
}
}
var refreshedFrontEnd = normalizedStart - 1;
var shiftedReuseEnd = normalizedStart - 1;
var cachedBlocks = cachePlan && Array.isArray(cachePlan.cachedBlocks) ? cachePlan.cachedBlocks.slice() : [];
var liveCursor = normalizedStart;
if (frontRefreshPages > 0) {
refreshedFrontEnd = Math.min(normalizedEnd, normalizedStart + frontRefreshPages - 1);
if (probeResult && probeResult.probeThreads) {
mergeCoverageThreads(searchContext.coverageThreadMap, probeResult.probeThreads);
await savePageCoverageSnapshot(forumKey, 1, probeResult.probeThreads);
var probeFiltered2 = filterThreadsByKeywords(probeResult.probeThreads, searchContext.keywords);
searchContext.matchedThreads += probeFiltered2.length;
var probeCacheResult2 = await applyCachedMagnetHits(1, probeFiltered2, searchContext);
if (probeCacheResult2.threadsToFetch.length > 0) {
searchContext.totalFetched += await fetchThreadsInParallel(
1, probeCacheResult2.threadsToFetch, searchContext.allMagnets,
searchContext.processedThreadKeys, { forumKey: forumKey }
);
}
if (refreshedFrontEnd >= 2) {
updateStatus('智能增量:刷新第2-' + refreshedFrontEnd + '页 (发现' + (probeResult.newCount || 0) + '个新帖)...', 'loading');
await fetchLivePageRange(2, refreshedFrontEnd, searchContext);
}
} else {
updateStatus('智能增量:刷新前' + frontRefreshPages + '页...', 'loading');
await fetchLivePageRange(normalizedStart, refreshedFrontEnd, searchContext);
}
liveCursor = refreshedFrontEnd + 1;
}
cachedBlocks = cachedBlocks.filter(function(block) {
return block && Array.isArray(block.threads) && block.threads.length > 0 && Number(block.endPage || 0) >= liveCursor;
}).sort(function(a, b) {
return Number(a.startPage || 0) - Number(b.startPage || 0);
});
if (!stopFetching && cachedBlocks.length > 0) {
cacheStrategy = 'assembled_pages';
for (var blockIndex = 0; blockIndex < cachedBlocks.length; blockIndex++) {
var cachedBlock = cachedBlocks[blockIndex];
var blockStart = Math.max(liveCursor, Number(cachedBlock.startPage) || liveCursor);
var blockEnd = Math.min(normalizedEnd, Number(cachedBlock.endPage) || blockStart);
if (blockStart > blockEnd) {
continue;
}
if (liveCursor < blockStart) {
updateStatus('缓存复用:补抓缺口页 ' + liveCursor + '-' + (blockStart - 1), 'loading');
await fetchLivePageRange(liveCursor, blockStart - 1, searchContext);
}
await processCachedThreadBatch(
String(blockStart) + '-' + String(blockEnd),
cachedBlock.threads,
searchContext,
'缓存复用:直接使用页缓存 ' + blockStart + '-' + blockEnd + ' 页'
);
shiftedReuseEnd = Math.max(shiftedReuseEnd, blockEnd);
liveCursor = blockEnd + 1;
progressRuntime.resumeFromPage = liveCursor;
scheduleStatePersist();
}
} else 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();
liveCursor = shiftedReuseEnd + 1;
}
var liveTailStart = Math.max(normalizedStart, liveCursor, 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 : '';
updateProgress(normalizedEnd - normalizedStart + 1, normalizedEnd - normalizedStart + 1, stopFetching ? '已停止' : '已完成');
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');
if (keyword) {
saveSearchHistory(keyword);
}
playDoneTone();
showDoneNotification('磁力链接抓取完成', '共获取 ' + searchContext.allMagnets.size + ' 个磁力链接,用时 ' + Math.max(1, Math.round((Date.now() - startTime) / 1000)) + ' 秒');
}
scheduleStatePersist();
} finally {
if (ball) ball.style.display = 'flex';
isFetching = false;
}
}
function fetchAllMagnets() {
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission().catch(function() {});
}
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;
saveStateNow();
}
async function initializePluginUi() {
createFloatingPanel();
var panel = document.getElementById('magnet-floating-panel');
if (!panel) return;
var settingsArea = panel.querySelector('#magnet-settings');
if (!settingsArea) return;
await loadPrivateStateFromExtensionStorage();
renderCloudAuthSection();
if (isListPage() && !document.getElementById('keyword-input')) {
var currentPage = getCurrentPage();
var keywordDiv = document.createElement('div');
keywordDiv.className = 'magnet-control-row';
keywordDiv.innerHTML = ' ';
var keywordInputEl = keywordDiv.querySelector('#keyword-input');
if (keywordInputEl) {
keywordInputEl.addEventListener('focus', function() {
showHistoryDropdown(keywordInputEl);
});
keywordInputEl.addEventListener('input', function() {
removeHistoryDropdown();
});
}
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);
var pageStartEl = pageRange.querySelector('#page-start');
var pageEndEl = pageRange.querySelector('#page-end');
var pageRangeAllBtn = pageRange.querySelector('#page-range-all');
if (pageRangeAllBtn && pageStartEl && pageEndEl) {
pageRangeAllBtn.addEventListener('click', function() {
var lastPage = getLastPage();
pageStartEl.value = '1';
pageEndEl.value = String(lastPage);
updateStatus('已选择全页范围:1-' + lastPage, 'done');
});
}
var speedSelectEl = speedDiv.querySelector('#speed-select');
if (speedSelectEl) {
speedSelectEl.addEventListener('change', function() {
speedMode = speedConfig[speedSelectEl.value] ? speedSelectEl.value : 'fast';
scheduleCloudVaultSync();
});
}
}
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;
await refreshCloudStatusAndVault({ restoreProgress: !restored });
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;
}
});
})();