Files
sehuatang/content.js
2026-03-10 23:12:18 +08:00

1801 lines
68 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function() {
if (window.magnetPluginInitialized) return;
window.magnetPluginInitialized = true;
var DEBUG_MODE = localStorage.getItem('magnetDebugMode') === 'true';
function log(msg) { if (DEBUG_MODE) console.log('[MagnetPlugin] ' + msg); }
function escapeHtml(text) {
return String(text || '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function ensurePanelStyles() {
if (document.getElementById('magnet-panel-style')) {
return;
}
var style = document.createElement('style');
style.id = 'magnet-panel-style';
style.textContent = [
'#magnet-float-ball{position:fixed;bottom:18px;right:18px;width:56px;height:56px;background:linear-gradient(135deg,#2e7d32,#43a047);border-radius:18px;box-shadow:0 16px 30px rgba(46,125,50,.35);z-index:2147483647;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#fff;font-size:26px;font-family:"Trebuchet MS","Segoe UI","Microsoft YaHei",sans-serif !important;transition:transform .18s ease,box-shadow .18s ease;}',
'#magnet-float-ball:hover{transform:translateY(-2px);box-shadow:0 18px 34px rgba(46,125,50,.42);}',
'#magnet-floating-panel{position:fixed;right:16px;bottom:16px;width:min(760px,calc(100vw - 20px));height:min(82vh,820px);background:linear-gradient(180deg,#f9fbff 0%,#f3f6fb 100%);border:1px solid rgba(133,151,178,.25);border-radius:18px;box-shadow:0 24px 54px rgba(15,23,42,.24);z-index:2147483647;font-family:"Trebuchet MS","Segoe UI","Microsoft YaHei",sans-serif;font-size:13px;color:#122033;display:none;flex-direction:column;overflow:hidden;backdrop-filter:blur(10px);}',
'#magnet-floating-panel .magnet-panel-header{display:flex;justify-content:space-between;align-items:flex-start;padding:18px 20px 14px;background:linear-gradient(135deg,#1f5f3e 0%,#2f7c52 58%,#4caf50 100%);color:#fff;gap:14px;}',
'#magnet-floating-panel .magnet-panel-brand{display:flex;flex-direction:column;gap:4px;min-width:0;}',
'#magnet-floating-panel .magnet-panel-title{font-size:20px;font-weight:700;letter-spacing:.3px;}',
'#magnet-floating-panel .magnet-panel-subtitle{font-size:12px;opacity:.88;line-height:1.45;}',
'#magnet-floating-panel .magnet-panel-head-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;}',
'#magnet-floating-panel .magnet-panel-switch{padding:8px 14px;border:none;border-radius:999px;background:rgba(255,255,255,.16);color:#eff8f0;cursor:pointer;font-size:12px;font-weight:700;transition:background .18s ease,transform .18s ease;}',
'#magnet-floating-panel .magnet-panel-switch:hover{background:rgba(255,255,255,.24);transform:translateY(-1px);}',
'#magnet-floating-panel .magnet-panel-switch.is-active{background:#fff;color:#1f5f3e;box-shadow:0 10px 20px rgba(14,30,37,.18);}',
'#magnet-floating-panel .magnet-panel-close{width:34px;height:34px;border:none;border-radius:12px;background:rgba(255,255,255,.16);color:#fff;cursor:pointer;font-size:18px;line-height:1;}',
'#magnet-floating-panel .magnet-panel-close:hover{background:rgba(255,255,255,.24);}',
'#magnet-settings{padding:16px 18px;background:rgba(255,255,255,.88);border-bottom:1px solid rgba(214,223,235,.95);display:flex;flex-direction:column;gap:10px;}',
'#magnet-floating-panel .magnet-control-row{display:flex;gap:10px;align-items:center;flex-wrap:wrap;}',
'#magnet-floating-panel .magnet-control-row > *{min-width:0;}',
'#magnet-floating-panel .magnet-panel-content{flex:1;min-height:0;padding:16px 18px;background:linear-gradient(180deg,rgba(255,255,255,.78),rgba(247,250,255,.95));}',
'#magnet-floating-panel .magnet-view{display:none;height:100%;flex-direction:column;gap:12px;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:10px;padding:10px 14px;background:#fff;border:1px solid #e4ebf5;border-radius:14px;box-shadow:0 10px 20px rgba(31,48,74,.06);}',
'#magnet-floating-panel .magnet-view-toolbar-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:flex-end;}',
'#magnet-floating-panel .magnet-view-title{font-size:14px;font-weight:700;color:#19324d;}',
'#magnet-floating-panel .magnet-view-meta{font-size:12px;color:#5d6b82;}',
'#magnet-list{display:flex;flex-direction:column;gap:10px;min-height:0;overflow-y:auto;padding-right:4px;}',
'#magnet-cache-panel{flex:1;min-height:0;overflow-y:auto;padding-right:4px;}',
'#magnet-floating-panel .magnet-panel-footer{padding:14px 18px 18px;background:rgba(255,255,255,.96);border-top:1px solid rgba(214,223,235,.95);display:flex;flex-direction:column;gap:10px;}',
'#magnet-status{padding:10px 12px;border-radius:12px;font-size:12px;line-height:1.5;}',
'#magnet-copy-all{width:100%;padding:12px 14px;background:linear-gradient(135deg,#1e88e5,#42a5f5);color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;box-shadow:0 10px 22px rgba(30,136,229,.25);}',
'#magnet-copy-all:hover{filter:brightness(1.03);}',
'.magnet-item{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;background:#fff;border:1px solid #e3eaf2;border-radius:14px;box-shadow:0 10px 18px rgba(31,48,74,.06);}',
'.magnet-title{flex:1;cursor:pointer;color:#20324d;min-width:0;font-size:12px;line-height:1.55;word-break:break-all;font-weight:600;}',
'.magnet-copy-btn{padding:8px 12px;background:linear-gradient(135deg,#43a047,#66bb6a);color:#fff;border:none;border-radius:10px;cursor:pointer;font-size:11px;font-weight:700;white-space:nowrap;flex-shrink:0;}',
'.magnet-copy-btn:hover{filter:brightness(1.03);}',
'.magnet-cache-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-bottom:12px;}',
'.magnet-cache-card{padding:12px;background:#fff;border:1px solid #e4ebf5;border-radius:14px;box-shadow:0 10px 18px rgba(31,48,74,.05);}',
'.magnet-cache-card-label{font-size:11px;color:#6b778c;margin-bottom:6px;}',
'.magnet-cache-card-value{font-size:18px;font-weight:700;color:#163049;}',
'.magnet-cache-section{margin-top:12px;}',
'.magnet-cache-section-title{font-size:12px;font-weight:700;color:#5c6b82;margin-bottom:8px;}',
'.magnet-cache-entry{padding:10px 12px;margin-top:8px;background:#fff;border:1px solid #e4ebf5;border-radius:12px;box-shadow:0 8px 14px rgba(31,48,74,.05);}',
'.magnet-cache-entry-title{font-size:12px;font-weight:700;color:#1b314b;line-height:1.5;word-break:break-all;}',
'.magnet-cache-entry-meta{font-size:11px;color:#6a768d;margin-top:4px;line-height:1.45;}',
'@media (max-width: 900px){#magnet-floating-panel{right:10px;bottom:10px;width:calc(100vw - 20px);height:min(84vh,760px);}#magnet-floating-panel .magnet-panel-header{padding:16px 16px 12px;}#magnet-settings,#magnet-floating-panel .magnet-panel-content,#magnet-floating-panel .magnet-panel-footer{padding-left:14px;padding-right:14px;}}'
].join('');
document.head.appendChild(style);
}
function setPanelView(viewName) {
var resultsView = document.getElementById('magnet-results-view');
var cacheView = document.getElementById('magnet-cache-view');
var resultsBtn = document.getElementById('magnet-view-results');
var cacheBtn = document.getElementById('magnet-view-cache');
if (!resultsView || !cacheView || !resultsBtn || !cacheBtn) {
return;
}
var showCache = viewName === 'cache';
resultsView.classList.toggle('is-active', !showCache);
cacheView.classList.toggle('is-active', showCache);
resultsBtn.classList.toggle('is-active', !showCache);
cacheBtn.classList.toggle('is-active', showCache);
}
function createFloatingPanel() {
var existing = document.getElementById('magnet-floating-panel');
if (existing) return existing;
ensurePanelStyles();
var ball = document.createElement('div');
ball.id = 'magnet-float-ball';
ball.innerHTML = '⚡';
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:80px;right:20px;background:#fff;border-radius:4px;box-shadow:0 2px 10px rgba(0,0,0,0.3);z-index:999999;padding:5px 0;font-size:12px';
menu.innerHTML = '<label style="display:block;padding:8px 15px;cursor:pointer"><input type="checkbox" id="debug-check"' + (DEBUG_MODE ? ' checked' : '') + '> 调试模式</label>';
document.body.appendChild(menu);
menu.querySelector('input').onchange = function() {
DEBUG_MODE = this.checked;
localStorage.setItem('magnetDebugMode', DEBUG_MODE);
};
setTimeout(function() {
document.addEventListener('click', function hideMenu() {
menu.remove();
document.removeEventListener('click', hideMenu);
});
}, 100);
};
document.body.appendChild(ball);
var panel = document.createElement('div');
panel.id = 'magnet-floating-panel';
panel.innerHTML = '<div class="magnet-panel-header"><div class="magnet-panel-brand"><div class="magnet-panel-title">磁力链接助手</div><div class="magnet-panel-subtitle">更大的面板、更清晰的缓存视图、智能增量搜索</div></div><div class="magnet-panel-head-actions"><button id="magnet-view-results" class="magnet-panel-switch is-active">结果</button><button id="magnet-view-cache" class="magnet-panel-switch">缓存</button><button class="magnet-panel-close" title="关闭">×</button></div></div><div class="magnet-settings" id="magnet-settings"></div><div class="magnet-panel-content"><div id="magnet-results-view" class="magnet-view is-active"><div class="magnet-view-toolbar"><div><div class="magnet-view-title">搜索结果</div><div class="magnet-view-meta">当前关键词命中的磁链会显示在这里</div></div><div class="magnet-view-meta">共 <span id="magnet-count-num">0</span> 条磁链</div></div><div class="magnet-list" id="magnet-list"></div></div><div id="magnet-cache-view" class="magnet-view"><div class="magnet-view-toolbar"><div><div class="magnet-view-title">缓存总览</div><div class="magnet-view-meta">查看当前板块与全部缓存的占用、快照和最近命中</div></div><div class="magnet-view-toolbar-actions"><button id="magnet-refresh-cache" class="magnet-panel-switch is-active" style="background:#e8f1ff;color:#2456a6">刷新缓存</button><button id="magnet-clear-cache-inline" class="magnet-panel-switch" style="background:#fbe9e7;color:#8d3b2f">清缓存</button></div></div><div id="magnet-cache-panel"></div></div></div><div class="magnet-panel-footer"><div id="magnet-status">输入页码范围获取磁力</div><button id="magnet-copy-all">一键复制全部</button></div>';
document.body.appendChild(panel);
setPanelView('results');
ball.onclick = function() {
panel.style.display = 'flex';
ball.style.display = 'none';
};
var resultsSwitch = panel.querySelector('#magnet-view-results');
if (resultsSwitch) {
resultsSwitch.onclick = function() {
setPanelView('results');
};
}
var cacheSwitch = panel.querySelector('#magnet-view-cache');
if (cacheSwitch) {
cacheSwitch.onclick = function() {
setPanelView('cache');
refreshCacheOverview({ showStatus: true });
};
}
var refreshCacheBtn = panel.querySelector('#magnet-refresh-cache');
if (refreshCacheBtn) {
refreshCacheBtn.onclick = function() {
refreshCacheOverview({ showStatus: true });
};
}
var clearCacheInlineBtn = panel.querySelector('#magnet-clear-cache-inline');
if (clearCacheInlineBtn) {
clearCacheInlineBtn.onclick = clearAllCacheWithConfirm;
}
var closeBtn = panel.querySelector('.magnet-panel-close');
if (closeBtn) closeBtn.onclick = function() {
panel.style.display = 'none';
ball.style.display = 'flex';
};
var copyAllBtn = panel.querySelector('#magnet-copy-all');
if (copyAllBtn) {
copyAllBtn.onclick = function() {
var links = allMagnetLinks.length > 0
? allMagnetLinks.slice()
: Array.from(document.querySelectorAll('.magnet-item .magnet-copy-btn'))
.map(function(btn) { return btn.getAttribute('data-magnet'); })
.filter(function(link) { return !!link; });
if (links.length === 0) {
alert('暂无可复制的磁力链接');
return;
}
var allLinks = links.join('\n');
navigator.clipboard.writeText(allLinks)
.then(function() {
alert('已复制 ' + links.length + ' 个磁力链接!');
})
.catch(function(err) {
var errorMsg = err && err.message ? err.message : '复制失败';
alert('复制失败:' + errorMsg);
});
};
}
return panel;
}
function isListPage() {
return document.querySelector('#threadlisttableid') !== null;
}
function getCurrentPage() {
var match = window.location.href.match(/[?&]page=(\d+)/);
if (match) return parseInt(match[1]);
match = window.location.href.match(/forum-\d+-(\d+)\.html/);
if (match) return parseInt(match[1]);
return 1;
}
function getForumOrigin(rawUrl) {
var sourceUrl = typeof rawUrl === 'string' && rawUrl ? rawUrl : window.location.href;
var match = sourceUrl.match(/^(https?:\/\/[^\/]+)\/?/i);
if (match) return match[1];
return window.location.origin || 'https://www.sehuatang.net';
}
function getForumIdFromUrl(rawUrl) {
var sourceUrl = typeof rawUrl === 'string' && rawUrl ? rawUrl : window.location.href;
var match = sourceUrl.match(/[?&]fid=(\d+)/i);
if (match) return match[1];
match = sourceUrl.match(/forum-(\d+)(?:-|\.html)/i);
if (match) return match[1];
return '2';
}
function getForumKey() {
return getForumOrigin(window.location.href) + '|fid:' + getForumIdFromUrl(window.location.href);
}
function getBaseUrl() {
return getForumOrigin(window.location.href) + '/forum-' + getForumIdFromUrl(window.location.href) + '-';
}
function normalizeThreadUrl(url) {
if (typeof url !== 'string' || !url) {
return '';
}
try {
var parsed = new URL(url, window.location.origin);
parsed.hash = '';
return parsed.href;
} catch (e) {
return url;
}
}
function getThreadKeyFromUrl(url) {
var normalizedUrl = normalizeThreadUrl(url);
if (!normalizedUrl) {
return '';
}
var match = normalizedUrl.match(/thread-(\d+)-/i) || normalizedUrl.match(/[?&]tid=(\d+)/i);
return match ? match[1] : normalizedUrl;
}
function normalizeCachedThreads(threads) {
var result = [];
var seen = Object.create(null);
(Array.isArray(threads) ? threads : []).forEach(function(thread) {
if (!thread || typeof thread !== 'object') return;
var normalizedUrl = normalizeThreadUrl(thread.url);
var normalizedTitle = typeof thread.title === 'string'
? thread.title.replace(/\s+/g, ' ').trim()
: '';
var threadKey = typeof thread.threadKey === 'string' && thread.threadKey
? thread.threadKey
: getThreadKeyFromUrl(normalizedUrl);
if (!normalizedUrl || !threadKey || seen[threadKey]) return;
seen[threadKey] = true;
result.push({
url: normalizedUrl,
title: normalizedTitle,
threadKey: threadKey
});
});
return result;
}
function normalizeMagnetList(magnets) {
var seen = Object.create(null);
var result = [];
(Array.isArray(magnets) ? magnets : []).forEach(function(magnet) {
if (typeof magnet !== 'string' || !magnet) return;
if (seen[magnet]) return;
seen[magnet] = true;
result.push(magnet);
});
return result;
}
function extractThreadsFromHtml(html) {
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
var threads = [];
var tbodies = doc.querySelectorAll('#threadlisttableid tbody[id^="normalthread_"]');
tbodies.forEach(function(tbody) {
var link = tbody.querySelector('th a[href*="thread-"]');
var title = tbody.querySelector('th a.xst') || tbody.querySelector('th .xst');
var titleText = title ? title.textContent : '';
var normalizedUrl = link && link.href ? normalizeThreadUrl(link.href) : '';
if (normalizedUrl) {
threads.push({
url: normalizedUrl,
title: titleText,
threadKey: getThreadKeyFromUrl(normalizedUrl)
});
}
});
return normalizeCachedThreads(threads);
}
function updateStatus(message, type) {
var status = document.getElementById('magnet-status');
if (status) {
status.textContent = message;
status.setAttribute('data-type', type || '');
status.style.background = '#e3f2fd';
status.style.color = '#1976D2';
if (type === 'loading') {
status.style.background = '#fff3e0';
status.style.color = '#f57c00';
} else if (type === 'error') {
status.style.background = '#ffebee';
status.style.color = '#c62828';
} else if (type === 'done') {
status.style.background = '#e8f5e9';
status.style.color = '#388e3c';
}
if (type && type !== 'loading') {
scheduleStatePersist();
}
}
}
function updateCount(count) {
var countEl = document.getElementById('magnet-count-num');
if (countEl) countEl.textContent = count;
}
function clearMagnetList(skipPersist) {
var list = document.getElementById('magnet-list');
if (list) list.innerHTML = '';
allMagnetLinks = [];
allMagnetRecords = [];
magnetRecordMap = Object.create(null);
updateCount(0);
if (!skipPersist) {
scheduleStatePersist();
}
}
function addMagnetItem(title, link, options) {
var list = document.getElementById('magnet-list');
if (!list) return;
var safeTitle = typeof title === 'string' ? title : String(title || '');
var safeLink = typeof link === 'string' ? link : String(link || '');
if (!safeLink) return;
if (magnetRecordMap[safeLink]) {
return;
}
magnetRecordMap[safeLink] = safeTitle || '恢复记录';
allMagnetRecords.push({
title: magnetRecordMap[safeLink],
link: safeLink
});
allMagnetLinks.push(safeLink);
var item = document.createElement('div');
item.className = 'magnet-item';
var titleEl = document.createElement('span');
titleEl.className = 'magnet-title';
titleEl.title = safeTitle;
titleEl.textContent = safeTitle;
var copyBtn = document.createElement('button');
copyBtn.className = 'magnet-copy-btn';
copyBtn.setAttribute('data-magnet', safeLink);
copyBtn.textContent = '复制';
titleEl.onclick = function() {
navigator.clipboard.writeText(safeLink)
.then(function() {
titleEl.textContent = '已复制: ' + safeTitle.substring(0, 20) + '...';
setTimeout(function() {
titleEl.textContent = safeTitle;
}, 1500);
})
.catch(function(err) {
var errorMsg = err && err.message ? err.message : '复制失败';
log('标题复制失败: ' + errorMsg);
updateStatus('复制失败,请检查剪贴板权限', 'error');
});
};
copyBtn.onclick = function() {
navigator.clipboard.writeText(safeLink)
.then(function() {
copyBtn.textContent = '已复制';
setTimeout(function() {
copyBtn.textContent = '复制';
}, 1000);
})
.catch(function(err) {
var errorMsg = err && err.message ? err.message : '复制失败';
log('按钮复制失败: ' + errorMsg);
updateStatus('复制失败,请检查剪贴板权限', 'error');
});
};
item.appendChild(titleEl);
item.appendChild(copyBtn);
list.appendChild(item);
setPanelView('results');
updateCount(list.children.length);
if (!options || !options.skipPersist) {
scheduleStatePersist();
}
}
function extractMagnets() {
var magnetPattern = /magnet:\?xt=urn:btih:[a-fA-F0-9]{32,40}/gi;
var links = new Set();
function walk(node) {
if (node.nodeType === Node.TEXT_NODE) {
var matches = node.textContent.match(magnetPattern);
if (matches) matches.forEach(function(m) { links.add(m); });
} else if (node.nodeType === Node.ELEMENT_NODE) {
var tag = node.tagName.toLowerCase();
if (tag !== 'script' && tag !== 'style' && tag !== 'noscript') {
node.childNodes.forEach(walk);
}
}
}
walk(document.body);
document.querySelectorAll('a[href^="magnet:"]').forEach(function(a) { links.add(a.href); });
return Array.from(links);
}
var speedMode = 'fast';
var speedConfig = {
slow: {
thread: [300, 600],
page: [500, 900],
concurrency: 1,
fetchTimeout: 25000,
messageTimeout: 30000
},
medium: {
thread: [80, 180],
page: [150, 300],
concurrency: 2,
fetchTimeout: 18000,
messageTimeout: 22000
},
fast: {
thread: [0, 60],
page: [60, 150],
concurrency: 4,
fetchTimeout: 14000,
messageTimeout: 18000
},
ultrafast: {
thread: [0, 20],
page: [20, 80],
concurrency: 6,
fetchTimeout: 10000,
messageTimeout: 14000
}
};
function getSpeedProfile() {
return speedConfig[speedMode] || speedConfig.fast;
}
function getSpeedDelay(type) {
var profile = getSpeedProfile();
var range = profile[type] || [0, 0];
return Math.random() * (range[1] - range[0]) + range[0];
}
function getThreadConcurrency() {
return Math.max(1, getSpeedProfile().concurrency || 1);
}
function getFetchTimeout() {
return getSpeedProfile().fetchTimeout || 15000;
}
function getMessageTimeout() {
return getSpeedProfile().messageTimeout || 20000;
}
function sleep(ms) { return new Promise(function(resolve) { setTimeout(resolve, ms); }); }
function sendRuntimeMessage(payload, timeoutMs) {
return new Promise(function(resolve, reject) {
if (!chrome.runtime || !chrome.runtime.id) {
reject(new Error('扩展已失效,请刷新页面'));
return;
}
var finished = false;
var safeTimeoutMs = Number(timeoutMs);
if (!Number.isFinite(safeTimeoutMs) || safeTimeoutMs <= 0) {
safeTimeoutMs = 15000;
}
var timer = setTimeout(function() {
if (finished) return;
finished = true;
reject(new Error('请求超时,请稍后重试'));
}, safeTimeoutMs);
try {
chrome.runtime.sendMessage(payload, function(response) {
if (finished) return;
finished = true;
clearTimeout(timer);
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
return;
}
resolve(response);
});
} catch (err) {
if (finished) return;
finished = true;
clearTimeout(timer);
reject(err);
}
});
}
function buildKeywordList(keyword) {
return String(keyword || '')
.split(',')
.map(function(item) { return item.trim(); })
.filter(function(item) { return !!item; });
}
function filterThreadsByKeywords(threadList, keywords) {
var normalizedThreads = normalizeCachedThreads(threadList);
if (!keywords || keywords.length === 0) {
return normalizedThreads;
}
return normalizedThreads.filter(function(thread) {
return keywords.some(function(keyword) {
return thread.title.indexOf(keyword) !== -1;
});
});
}
function mergeCoverageThreads(targetMap, threads) {
normalizeCachedThreads(threads).forEach(function(thread) {
if (!targetMap[thread.threadKey]) {
targetMap[thread.threadKey] = thread;
return;
}
if (!targetMap[thread.threadKey].title && thread.title) {
targetMap[thread.threadKey].title = thread.title;
}
if (!targetMap[thread.threadKey].url && thread.url) {
targetMap[thread.threadKey].url = thread.url;
}
});
}
function coverageMapToList(targetMap) {
return Object.keys(targetMap).map(function(threadKey) {
return targetMap[threadKey];
});
}
function getSmartFrontRefreshPages(startPage, endPage) {
if (startPage !== 1) {
return 0;
}
return Math.min(20, Math.max(0, endPage - startPage + 1));
}
async function getCachedCoveragePlan(forumKey, startPage, endPage, frontRefreshPages) {
try {
var response = await sendRuntimeMessage({
action: 'cacheGetCoveragePlan',
forumKey: forumKey,
startPage: startPage,
endPage: endPage,
frontRefreshPages: frontRefreshPages
}, 8000);
if (!response || !response.ok) {
log('读取缓存计划失败: ' + (response && response.error ? response.error : '空响应'));
return null;
}
return response;
} catch (e) {
var errorMsg = e && e.message ? e.message : String(e);
log('读取缓存计划异常: ' + errorMsg);
return null;
}
}
async function saveCoverageSnapshot(forumKey, startPage, endPage, threads, frontRefreshPages, strategy) {
var normalizedThreads = normalizeCachedThreads(threads);
if (!forumKey || normalizedThreads.length === 0) {
return;
}
try {
var response = await sendRuntimeMessage({
action: 'cacheSaveCoverage',
forumKey: forumKey,
startPage: startPage,
endPage: endPage,
frontRefreshPages: frontRefreshPages,
strategy: strategy,
crawledAt: Date.now(),
threads: normalizedThreads
}, 12000);
if (!response || !response.ok) {
log('保存范围缓存失败: ' + (response && response.error ? response.error : '空响应'));
}
} catch (e) {
var errorMsg = e && e.message ? e.message : String(e);
log('保存范围缓存异常: ' + errorMsg);
}
}
async function savePageCoverageSnapshot(forumKey, page, threads) {
var normalizedThreads = normalizeCachedThreads(threads);
try {
var response = await sendRuntimeMessage({
action: 'cacheSavePageCoverage',
forumKey: forumKey,
page: page,
crawledAt: Date.now(),
threads: normalizedThreads
}, 12000);
if (!response || !response.ok) {
log('保存页缓存失败: ' + (response && response.error ? response.error : '空响应'));
}
} catch (e) {
var errorMsg = e && e.message ? e.message : String(e);
log('保存页缓存异常: ' + errorMsg);
}
}
async function getCachedThreadMagnets(forumKey, threads) {
var normalizedThreads = normalizeCachedThreads(threads);
if (!forumKey || normalizedThreads.length === 0) {
return [];
}
try {
var response = await sendRuntimeMessage({
action: 'cacheGetThreadMagnets',
forumKey: forumKey,
threads: normalizedThreads
}, 12000);
if (!response || !response.ok || !Array.isArray(response.threads)) {
log('读取帖子磁链缓存失败: ' + (response && response.error ? response.error : '空响应'));
return [];
}
return response.threads.map(function(thread) {
return {
threadKey: typeof thread.threadKey === 'string' ? thread.threadKey : getThreadKeyFromUrl(thread.url),
url: normalizeThreadUrl(thread.url),
title: typeof thread.title === 'string' ? thread.title : '',
magnets: normalizeMagnetList(thread.magnets)
};
});
} catch (e) {
var errorMsg = e && e.message ? e.message : String(e);
log('读取帖子磁链缓存异常: ' + errorMsg);
return [];
}
}
async function saveThreadMagnetsToCache(forumKey, threads) {
var normalizedThreads = [];
(Array.isArray(threads) ? threads : []).forEach(function(thread) {
if (!thread || typeof thread !== 'object') {
return;
}
var normalizedUrl = normalizeThreadUrl(thread.url);
var threadKey = typeof thread.threadKey === 'string' && thread.threadKey
? thread.threadKey
: getThreadKeyFromUrl(normalizedUrl);
var magnets = normalizeMagnetList(thread.magnets);
if (!normalizedUrl || !threadKey || magnets.length === 0) {
return;
}
normalizedThreads.push({
threadKey: threadKey,
url: normalizedUrl,
title: typeof thread.title === 'string' ? thread.title : '',
magnets: magnets
});
});
if (!forumKey || normalizedThreads.length === 0) {
return;
}
try {
var response = await sendRuntimeMessage({
action: 'cacheSaveThreadMagnets',
forumKey: forumKey,
syncedAt: Date.now(),
threads: normalizedThreads
}, 12000);
if (!response || !response.ok) {
log('保存帖子磁链缓存失败: ' + (response && response.error ? response.error : '空响应'));
}
} catch (e) {
var errorMsg = e && e.message ? e.message : String(e);
log('保存帖子磁链缓存异常: ' + errorMsg);
}
}
function formatTimeLabel(timestamp) {
var value = Number(timestamp);
if (!value) {
return '无';
}
var date = new Date(value);
var month = String(date.getMonth() + 1).padStart(2, '0');
var day = String(date.getDate()).padStart(2, '0');
var hour = String(date.getHours()).padStart(2, '0');
var minute = String(date.getMinutes()).padStart(2, '0');
return month + '-' + day + ' ' + hour + ':' + minute;
}
function formatBytesLabel(bytes) {
var size = Number(bytes);
if (!Number.isFinite(size) || size <= 0) {
return '0 B';
}
if (size < 1024) {
return size + ' B';
}
if (size < 1024 * 1024) {
return (size / 1024).toFixed(1) + ' KB';
}
return (size / (1024 * 1024)).toFixed(2) + ' MB';
}
async function getCacheOverview(forumKey) {
try {
var response = await sendRuntimeMessage({
action: 'cacheGetOverview',
forumKey: forumKey,
limit: 12
}, 12000);
if (!response || !response.ok) {
log('读取缓存总览失败: ' + (response && response.error ? response.error : '空响应'));
return null;
}
return response;
} catch (e) {
var errorMsg = e && e.message ? e.message : String(e);
log('读取缓存总览异常: ' + errorMsg);
return null;
}
}
function renderCacheOverview(cacheOverview) {
var panel = document.getElementById('magnet-cache-panel');
if (!panel) {
return;
}
if (!cacheOverview || !cacheOverview.summary) {
panel.innerHTML = '<div class="magnet-cache-card"><div class="magnet-cache-card-label">缓存状态</div><div class="magnet-cache-card-value" style="font-size:14px">暂无缓存数据</div></div>';
return;
}
var summary = cacheOverview.summary;
var recentThreads = Array.isArray(cacheOverview.recentThreads) ? cacheOverview.recentThreads : [];
var recentCoverages = Array.isArray(cacheOverview.recentCoverages) ? cacheOverview.recentCoverages : [];
var html = '';
html += '<div class="magnet-cache-card" style="margin-bottom:12px;background:linear-gradient(135deg,#f7fbff,#eef6ff)"><div class="magnet-cache-card-label">说明</div><div class="magnet-cache-entry-meta">这里的帖子数是 <strong>唯一帖子数</strong>,同一板块重复搜索同一帖子不会重复累计;重复搜索只会新增/更新范围快照。</div></div>';
html += '<div class="magnet-cache-grid">';
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">当前板块唯一帖子</div><div class="magnet-cache-card-value">' + summary.forumThreadCount + '</div><div class="magnet-cache-entry-meta">范围快照 ' + summary.forumCoverageCount + ' 份 · 页缓存 ' + Number(summary.forumPageCoverageCount || 0) + ' 页</div></div>';
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">全部唯一帖子</div><div class="magnet-cache-card-value">' + summary.totalThreadCount + '</div><div class="magnet-cache-entry-meta">总快照 ' + summary.totalCoverageCount + ' 份 · 页缓存 ' + Number(summary.totalPageCoverageCount || 0) + ' 页</div></div>';
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">磁链缓存</div><div class="magnet-cache-card-value">' + summary.recentMagnetCount + '</div><div class="magnet-cache-entry-meta">最近命中 ' + summary.recentMagnetCachedThreads + ' 帖</div></div>';
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">存储占用</div><div class="magnet-cache-card-value" style="font-size:16px">' + formatBytesLabel(summary.storageUsage) + '</div><div class="magnet-cache-entry-meta">配额 ' + (summary.storageQuota ? formatBytesLabel(summary.storageQuota) : '未知') + '</div></div>';
html += '</div>';
html += '<div class="magnet-cache-section"><div class="magnet-cache-section-title">最近缓存帖子</div>';
if (recentThreads.length === 0) {
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">提示</div><div class="magnet-cache-entry-meta">当前板块还没有缓存帖子</div></div>';
} else {
recentThreads.forEach(function(item) {
var title = item.title || '未命名帖子';
html += '<div class="magnet-cache-entry">';
html += '<div class="magnet-cache-entry-title">' + escapeHtml(title) + '</div>';
html += '<div class="magnet-cache-entry-meta">磁链缓存:' + Number(item.magnetCount || 0) + ' 条 · 帖子缓存:' + formatTimeLabel(item.lastSeenAt) + ' · 磁链缓存:' + formatTimeLabel(item.lastMagnetSyncAt) + '</div>';
html += '</div>';
});
}
html += '</div>';
html += '<div class="magnet-cache-section"><div class="magnet-cache-section-title">最近范围快照</div>';
if (recentCoverages.length === 0) {
html += '<div class="magnet-cache-card"><div class="magnet-cache-card-label">提示</div><div class="magnet-cache-entry-meta">暂无范围快照</div></div>';
} else {
recentCoverages.forEach(function(item) {
html += '<div class="magnet-cache-entry">';
html += '<div class="magnet-cache-entry-title">页码 ' + item.startPage + '-' + item.endPage + '</div>';
html += '<div class="magnet-cache-entry-meta">' + item.threadCount + ' 帖 · 策略 ' + item.strategy + ' · ' + formatTimeLabel(item.crawledAt) + '</div>';
html += '</div>';
});
}
html += '</div>';
panel.innerHTML = html;
}
async function refreshCacheOverview(options) {
options = options || {};
var cachePanel = document.getElementById('magnet-cache-panel');
if (cachePanel) {
cachePanel.innerHTML = '<div style="padding:8px;color:#666;font-size:11px">正在读取缓存...</div>';
}
var overview = await getCacheOverview(getForumKey());
renderCacheOverview(overview);
if (options.showStatus) {
if (overview && overview.summary) {
updateStatus('已加载缓存:当前板块唯一帖子 ' + overview.summary.forumThreadCount + ' 帖,当前页缓存 ' + Number(overview.summary.forumPageCoverageCount || 0) + ' 页', 'done');
} else {
updateStatus('读取缓存失败,请稍后重试', 'error');
}
}
return overview;
}
async function clearAllCacheWithConfirm() {
if (!confirm('确认清空全部缓存吗?这不会删除当前页面已显示的结果。')) {
return;
}
if (!confirm('二次确认:真的要清空全部标题缓存、范围快照和磁链缓存吗?')) {
return;
}
updateStatus('正在清空缓存...', 'loading');
try {
var response = await sendRuntimeMessage({ action: 'cacheClearAll' }, 12000);
if (!response || !response.ok) {
updateStatus('清空缓存失败', 'error');
return;
}
await refreshCacheOverview();
updateStatus('已清空全部缓存', 'done');
} catch (e) {
var errorMsg = e && e.message ? e.message : String(e);
log('清空缓存异常: ' + errorMsg);
updateStatus('清空缓存失败:' + errorMsg, 'error');
}
}
function toggleCachePanel() {
var cacheView = document.getElementById('magnet-cache-view');
var resultsView = document.getElementById('magnet-results-view');
if (!cacheView || !resultsView) {
return;
}
var isVisible = cacheView.classList.contains('is-active');
setPanelView(isVisible ? 'results' : 'cache');
if (!isVisible) {
refreshCacheOverview({ showStatus: true });
}
}
async function applyCachedMagnetHits(pageLabel, filteredThreads, context) {
var cachedEntries = await getCachedThreadMagnets(context.forumKey, filteredThreads);
var cachedMap = Object.create(null);
var threadsToFetch = [];
var cachedThreadCount = 0;
var cachedMagnetCount = 0;
cachedEntries.forEach(function(entry) {
if (!entry || !entry.threadKey) return;
cachedMap[entry.threadKey] = entry;
});
normalizeCachedThreads(filteredThreads).forEach(function(thread) {
var threadKey = thread.threadKey || getThreadKeyFromUrl(thread.url);
var cachedEntry = threadKey ? cachedMap[threadKey] : null;
var cachedMagnets = cachedEntry ? normalizeMagnetList(cachedEntry.magnets) : [];
if (cachedMagnets.length === 0) {
if (!context.processedThreadKeys[threadKey]) {
threadsToFetch.push(thread);
}
return;
}
if (context.processedThreadKeys[threadKey]) {
return;
}
context.processedThreadKeys[threadKey] = true;
cachedThreadCount += 1;
context.totalFetched += 1;
cachedMagnets.forEach(function(magnet) {
if (context.allMagnets.has(magnet)) {
return;
}
context.allMagnets.add(magnet);
cachedMagnetCount += 1;
addMagnetItem(thread.title || cachedEntry.title || '缓存帖子', magnet);
});
});
if (cachedThreadCount > 0) {
updateStatus(
'正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,直接读取缓存磁链 ' + cachedThreadCount + ' 帖(' + cachedMagnetCount + ' 条)',
'loading'
);
}
return {
threadsToFetch: threadsToFetch,
cachedThreadCount: cachedThreadCount,
cachedMagnetCount: cachedMagnetCount,
pageLabel: pageLabel
};
}
var STATE_BACKUP_KEY = 'magnetPluginStateBackupV1';
var MAX_STATE_ITEMS = 2000;
var statePersistTimer = null;
var hasRestoredProgress = false;
var lastRestoredState = null;
var manualClearRequested = false;
var isFetching = false;
var stopFetching = false;
var allMagnetLinks = [];
var allMagnetRecords = [];
var magnetRecordMap = Object.create(null);
var progressRuntime = {
isRunning: false,
stoppedByUser: false,
startPage: 1,
endPage: 1,
resumeFromPage: 1,
keyword: '',
speedMode: 'fast'
};
function resetProgressRuntimeToIdle() {
var startInput = document.getElementById('page-start');
var endInput = document.getElementById('page-end');
var keywordInput = document.getElementById('keyword-input');
var currentStart = startInput ? parseInt(startInput.value, 10) || 1 : 1;
var currentEnd = endInput ? parseInt(endInput.value, 10) || currentStart : currentStart;
progressRuntime.isRunning = false;
progressRuntime.stoppedByUser = true;
progressRuntime.startPage = currentStart;
progressRuntime.endPage = Math.max(currentStart, currentEnd);
progressRuntime.resumeFromPage = currentStart;
progressRuntime.keyword = keywordInput ? keywordInput.value.trim() : '';
progressRuntime.speedMode = speedConfig[speedMode] ? speedMode : 'fast';
}
function clearSessionBackupState() {
try {
sessionStorage.removeItem(STATE_BACKUP_KEY);
} catch (e) {
log('清理会话备份失败: ' + e);
}
}
async function clearAllResults() {
manualClearRequested = true;
stopFetching = true;
resetProgressRuntimeToIdle();
clearMagnetList(true);
clearSessionBackupState();
try {
await sendRuntimeMessage({ action: 'clearProgressState' }, 6000);
} catch (e) {
var clearErrorMsg = e && e.message ? e.message : String(e);
log('远端清理失败: ' + clearErrorMsg);
}
if (!isFetching) {
manualClearRequested = false;
updateStatus('已手动清空结果', 'done');
scheduleStatePersist();
} else {
updateStatus('正在停止任务并清空结果...', 'loading');
}
}
function buildStateSnapshot() {
var keywordInput = document.getElementById('keyword-input');
var startInput = document.getElementById('page-start');
var endInput = document.getElementById('page-end');
var speedSelect = document.getElementById('speed-select');
var statusEl = document.getElementById('magnet-status');
var startPage = startInput ? parseInt(startInput.value, 10) || 1 : 1;
var endPage = endInput ? parseInt(endInput.value, 10) || startPage : startPage;
var resumeFromPage = Number(progressRuntime.resumeFromPage);
if (!Number.isFinite(resumeFromPage) || resumeFromPage < 1) {
resumeFromPage = startPage;
}
return {
links: allMagnetLinks.slice(-MAX_STATE_ITEMS),
items: allMagnetRecords.slice(-MAX_STATE_ITEMS),
keyword: keywordInput ? keywordInput.value.trim() : '',
speedMode: speedConfig[speedMode] ? speedMode : 'fast',
startPage: startPage,
endPage: endPage,
resumeFromPage: resumeFromPage,
isRunning: !!progressRuntime.isRunning,
stoppedByUser: !!progressRuntime.stoppedByUser,
statusText: statusEl ? statusEl.textContent || '' : '',
statusType: statusEl ? statusEl.getAttribute('data-type') || '' : '',
pageUrl: window.location.href,
updatedAt: Date.now()
};
}
function saveStateToSessionBackup(snapshot) {
try {
sessionStorage.setItem(STATE_BACKUP_KEY, JSON.stringify(snapshot));
} catch (e) {
log('保存会话备份失败: ' + e);
}
}
function readStateFromSessionBackup() {
try {
var raw = sessionStorage.getItem(STATE_BACKUP_KEY);
if (!raw) return null;
return JSON.parse(raw);
} catch (e) {
log('读取会话备份失败: ' + e);
return null;
}
}
function applyStateSnapshot(snapshot) {
if (!snapshot || typeof snapshot !== 'object') {
return false;
}
var speedSelect = document.getElementById('speed-select');
if (speedSelect && snapshot.speedMode && speedConfig[snapshot.speedMode]) {
speedSelect.value = snapshot.speedMode;
speedMode = snapshot.speedMode;
}
var keywordInput = document.getElementById('keyword-input');
if (keywordInput && typeof snapshot.keyword === 'string') {
keywordInput.value = snapshot.keyword;
}
var startInput = document.getElementById('page-start');
if (startInput && Number.isFinite(Number(snapshot.startPage))) {
startInput.value = Math.max(1, Number(snapshot.startPage));
}
var endInput = document.getElementById('page-end');
if (endInput && Number.isFinite(Number(snapshot.endPage))) {
endInput.value = Math.max(1, Number(snapshot.endPage));
}
progressRuntime.startPage = Number.isFinite(Number(snapshot.startPage))
? Math.max(1, Number(snapshot.startPage))
: 1;
progressRuntime.endPage = Number.isFinite(Number(snapshot.endPage))
? Math.max(progressRuntime.startPage, Number(snapshot.endPage))
: progressRuntime.startPage;
progressRuntime.resumeFromPage = Number.isFinite(Number(snapshot.resumeFromPage))
? Math.max(1, Number(snapshot.resumeFromPage))
: progressRuntime.startPage;
progressRuntime.isRunning = !!snapshot.isRunning;
progressRuntime.stoppedByUser = !!snapshot.stoppedByUser;
progressRuntime.keyword = typeof snapshot.keyword === 'string' ? snapshot.keyword : '';
progressRuntime.speedMode = speedConfig[snapshot.speedMode] ? snapshot.speedMode : 'fast';
var records = Array.isArray(snapshot.items) && snapshot.items.length > 0
? snapshot.items
: (Array.isArray(snapshot.links)
? snapshot.links.map(function(link) {
return {
title: '恢复记录',
link: link
};
})
: []);
clearMagnetList(true);
records.forEach(function(record) {
if (!record || typeof record !== 'object') return;
if (typeof record.link !== 'string' || !record.link) return;
var title = typeof record.title === 'string' ? record.title : '恢复记录';
addMagnetItem(title, record.link, { skipPersist: true });
});
if (allMagnetLinks.length > 0) {
var statusText = typeof snapshot.statusText === 'string' && snapshot.statusText
? '已恢复: ' + snapshot.statusText
: '已恢复上次抓取记录,共' + allMagnetLinks.length + '条';
updateStatus(statusText, snapshot.statusType || 'done');
return true;
}
return false;
}
function saveStateNow() {
var snapshot = buildStateSnapshot();
saveStateToSessionBackup(snapshot);
sendRuntimeMessage({
action: 'saveProgressState',
state: snapshot
}, 6000).catch(function(e) {
var errorMsg = e && e.message ? e.message : String(e);
log('远端状态保存失败: ' + errorMsg);
});
}
function scheduleStatePersist() {
if (statePersistTimer) {
clearTimeout(statePersistTimer);
}
statePersistTimer = setTimeout(function() {
statePersistTimer = null;
saveStateNow();
}, 500);
}
async function restoreProgressState() {
if (hasRestoredProgress) {
return {
restored: allMagnetLinks.length > 0,
state: lastRestoredState
};
}
hasRestoredProgress = true;
var sessionState = readStateFromSessionBackup();
var sessionStateTime = sessionState && Number.isFinite(Number(sessionState.updatedAt))
? Number(sessionState.updatedAt)
: 0;
var remoteState = null;
var remoteStateTime = 0;
try {
var response = await sendRuntimeMessage({ action: 'loadProgressState' }, 6000);
if (response && response.ok && response.state) {
remoteState = response.state;
remoteStateTime = Number.isFinite(Number(response.state.updatedAt))
? Number(response.state.updatedAt)
: 0;
}
} catch (e) {
var errorMsg = e && e.message ? e.message : String(e);
log('远端状态恢复失败: ' + errorMsg);
}
var chosenState = null;
if (sessionState) {
chosenState = sessionState;
}
if (remoteState && (!chosenState || remoteStateTime >= sessionStateTime)) {
chosenState = remoteState;
}
lastRestoredState = chosenState;
var restored = chosenState ? applyStateSnapshot(chosenState) : false;
return {
restored: restored,
state: chosenState
};
}
window.addEventListener('pagehide', function() {
if (statePersistTimer) {
clearTimeout(statePersistTimer);
statePersistTimer = null;
}
saveStateNow();
});
async function fetchThreadsInParallel(page, filteredThreads, allMagnets, processedThreadKeys, options) {
if (!filteredThreads || filteredThreads.length === 0) {
return 0;
}
options = options || {};
var concurrency = Math.min(getThreadConcurrency(), filteredThreads.length);
var cursor = 0;
var fetchedCount = 0;
var cachePayload = [];
async function worker() {
while (!stopFetching) {
var currentIndex = cursor;
cursor += 1;
if (currentIndex >= filteredThreads.length) {
return;
}
var threadItem = filteredThreads[currentIndex];
var threadUrl = threadItem.url;
var threadTitle = threadItem.title || '未命名帖子';
var threadKey = threadItem.threadKey || getThreadKeyFromUrl(threadUrl);
if (processedThreadKeys && threadKey) {
if (processedThreadKeys[threadKey]) {
continue;
}
processedThreadKeys[threadKey] = true;
}
updateStatus('页' + page + '/帖子' + (currentIndex + 1) + '/' + filteredThreads.length, 'loading');
try {
var threadResponse = await sendRuntimeMessage({
action: 'openAndFetch',
url: threadUrl,
timeoutMs: getFetchTimeout()
}, getMessageTimeout());
if (threadResponse && threadResponse.error) {
log('帖子请求失败: ' + threadUrl + ' - ' + threadResponse.error);
} else if (threadResponse && threadResponse.magnets) {
var threadMagnets = normalizeMagnetList(threadResponse.magnets);
if (threadMagnets.length > 0 && options.forumKey) {
cachePayload.push({
threadKey: threadKey,
url: threadUrl,
title: threadTitle,
magnets: threadMagnets
});
}
threadMagnets.forEach(function(m) {
if (!allMagnets.has(m)) {
allMagnets.add(m);
addMagnetItem(threadTitle, m);
}
});
}
} catch (e) {
var threadErrorMsg = e && e.message ? e.message : String(e);
log('获取帖子失败: ' + threadUrl + ' - ' + threadErrorMsg);
}
fetchedCount += 1;
var delay = getSpeedDelay('thread');
if (delay > 0) {
await sleep(delay);
}
}
}
var workers = [];
for (var workerIndex = 0; workerIndex < concurrency; workerIndex++) {
workers.push(worker());
}
await Promise.all(workers);
if (options.forumKey && cachePayload.length > 0) {
await saveThreadMagnetsToCache(options.forumKey, cachePayload);
}
return fetchedCount;
}
async function processCachedThreadBatch(pageLabel, threads, context, statusText) {
var normalizedThreads = normalizeCachedThreads(threads);
if (normalizedThreads.length === 0) {
return;
}
if (statusText) {
updateStatus(statusText, 'loading');
}
mergeCoverageThreads(context.coverageThreadMap, normalizedThreads);
var filteredThreads = filterThreadsByKeywords(normalizedThreads, context.keywords);
context.matchedThreads += filteredThreads.length;
if (filteredThreads.length === 0) {
updateStatus('正在搜索缓存:已检查缓存标题 ' + normalizedThreads.length + ' 帖,当前关键词未命中', 'loading');
return;
}
var cacheResult = await applyCachedMagnetHits(pageLabel, filteredThreads, context);
if (cacheResult.threadsToFetch.length === 0) {
updateStatus('正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,结果已全部由缓存秒出', 'loading');
return;
}
updateStatus(
'正在搜索缓存:关键词命中 ' + filteredThreads.length + ' 帖,缓存磁链命中 ' + cacheResult.cachedThreadCount + ' 帖,补抓 ' + cacheResult.threadsToFetch.length + ' 帖',
'loading'
);
context.totalFetched += await fetchThreadsInParallel(
pageLabel,
cacheResult.threadsToFetch,
context.allMagnets,
context.processedThreadKeys,
{ forumKey: context.forumKey }
);
}
async function fetchLivePageRange(startPage, endPage, context) {
if (startPage > endPage) {
return;
}
for (var page = startPage; page <= endPage; page++) {
if (stopFetching) break;
progressRuntime.resumeFromPage = page;
scheduleStatePersist();
if (!chrome.runtime || !chrome.runtime.id) {
updateStatus('扩展已失效,请刷新页面', 'error');
break;
}
updateStatus('第' + page + '/' + context.normalizedEnd + '页...', 'loading');
var 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;
var panel = createFloatingPanel();
var ball = document.getElementById('magnet-float-ball');
panel.style.display = 'flex';
if (ball) ball.style.display = 'none';
if (!preserveExisting) {
clearMagnetList(true);
}
try {
var normalizedStart = Math.max(1, parseInt(startPage, 10) || 1);
var normalizedEnd = Math.max(normalizedStart, parseInt(endPage, 10) || normalizedStart);
var baseUrl = getBaseUrl();
var keywordInput = document.getElementById('keyword-input');
var speedSelect = document.getElementById('speed-select');
var keyword = typeof options.keyword === 'string'
? options.keyword.trim()
: (keywordInput ? keywordInput.value.trim() : '');
var selectedSpeed = typeof options.speedMode === 'string'
? options.speedMode
: (speedSelect ? speedSelect.value : 'fast');
speedMode = speedConfig[selectedSpeed] ? selectedSpeed : 'fast';
if (speedSelect) {
speedSelect.value = speedMode;
}
if (keywordInput) {
keywordInput.value = keyword;
}
progressRuntime.isRunning = true;
progressRuntime.stoppedByUser = false;
progressRuntime.startPage = normalizedStart;
progressRuntime.endPage = normalizedEnd;
progressRuntime.resumeFromPage = normalizedStart;
progressRuntime.keyword = keyword;
progressRuntime.speedMode = speedMode;
if (!baseUrl) {
updateStatus('无法获取页面URL', 'error');
progressRuntime.isRunning = false;
scheduleStatePersist();
return;
}
log('开始获取第 ' + normalizedStart + ' 到 ' + normalizedEnd + ' 页, 关键词: ' + keyword + ', 速度:' + speedMode + ', 并发:' + getThreadConcurrency());
if (preserveExisting && allMagnetLinks.length > 0) {
updateStatus('检测到未完成任务,已恢复' + allMagnetLinks.length + '条,继续抓取...', 'loading');
} else {
updateStatus('开始获取...', 'loading');
}
scheduleStatePersist();
var forumKey = getForumKey();
var frontRefreshPages = getSmartFrontRefreshPages(normalizedStart, normalizedEnd);
var searchContext = {
forumKey: forumKey,
baseUrl: baseUrl,
normalizedEnd: normalizedEnd,
keywords: buildKeywordList(keyword),
coverageThreadMap: Object.create(null),
processedThreadKeys: Object.create(null),
allMagnets: new Set(allMagnetLinks),
matchedThreads: 0,
totalFetched: 0,
failedPages: 0
};
var cachePlan = await getCachedCoveragePlan(forumKey, normalizedStart, normalizedEnd, frontRefreshPages);
var cacheStrategy = 'full_live';
if (cachePlan && cachePlan.exactCoverage && Array.isArray(cachePlan.exactCoverage.threads) && cachePlan.exactCoverage.threads.length > 0) {
cacheStrategy = 'exact_cache';
await processCachedThreadBatch('缓存', cachePlan.exactCoverage.threads, searchContext, '命中当前板块缓存,直接搜索...');
progressRuntime.resumeFromPage = normalizedEnd + 1;
scheduleStatePersist();
} else {
var refreshedFrontEnd = normalizedStart - 1;
if (frontRefreshPages > 0) {
refreshedFrontEnd = normalizedStart + frontRefreshPages - 1;
updateStatus('智能增量:刷新前' + frontRefreshPages + '页...', 'loading');
await fetchLivePageRange(normalizedStart, refreshedFrontEnd, searchContext);
}
var shiftedReuseEnd = refreshedFrontEnd;
if (!stopFetching && cachePlan && cachePlan.shiftedCoverage && Array.isArray(cachePlan.shiftedCoverage.threads) && cachePlan.shiftedCoverage.threads.length > 0) {
cacheStrategy = 'smart_incremental';
shiftedReuseEnd = Math.max(shiftedReuseEnd, Math.min(normalizedEnd, Number(cachePlan.shiftedCoverage.reusedEndPage) || refreshedFrontEnd));
await processCachedThreadBatch(
String(cachePlan.shiftedCoverage.reusedStartPage) + '-' + String(shiftedReuseEnd),
cachePlan.shiftedCoverage.threads,
searchContext,
'智能增量:复用当前板块历史缓存 ' + cachePlan.shiftedCoverage.reusedStartPage + '-' + shiftedReuseEnd + ' 页'
);
progressRuntime.resumeFromPage = shiftedReuseEnd + 1;
scheduleStatePersist();
}
var liveTailStart = Math.max(normalizedStart, shiftedReuseEnd + 1);
if (!stopFetching && liveTailStart <= normalizedEnd) {
if (liveTailStart > normalizedStart) {
updateStatus('智能增量:补抓未覆盖页 ' + liveTailStart + '-' + normalizedEnd, 'loading');
}
await fetchLivePageRange(liveTailStart, normalizedEnd, searchContext);
}
if (!stopFetching) {
await saveCoverageSnapshot(
forumKey,
normalizedStart,
normalizedEnd,
coverageMapToList(searchContext.coverageThreadMap),
frontRefreshPages,
cacheStrategy
);
}
}
progressRuntime.isRunning = false;
if (manualClearRequested) {
manualClearRequested = false;
clearMagnetList(true);
clearSessionBackupState();
resetProgressRuntimeToIdle();
updateStatus('已手动清空结果', 'done');
scheduleStatePersist();
return;
}
var keywordMsg = keyword ? ' (关键词:' + keyword + ' 匹配:' + searchContext.matchedThreads + '帖)' : '';
var failedMsg = searchContext.failedPages > 0 ? ',失败页:' + searchContext.failedPages : '';
if (stopFetching) {
progressRuntime.stoppedByUser = true;
updateStatus('已停止 - 找到' + searchContext.allMagnets.size + '个磁力' + keywordMsg + ',已处理帖子:' + searchContext.totalFetched + failedMsg, 'error');
} else {
progressRuntime.stoppedByUser = false;
progressRuntime.resumeFromPage = normalizedEnd + 1;
updateStatus('完成! 共' + searchContext.allMagnets.size + '个磁力' + keywordMsg + ',已处理帖子:' + searchContext.totalFetched + failedMsg, 'done');
}
scheduleStatePersist();
} finally {
if (ball) ball.style.display = 'flex';
isFetching = false;
}
}
function fetchAllMagnets() {
var startPage = parseInt(document.getElementById('page-start').value, 10) || 1;
var endPage = parseInt(document.getElementById('page-end').value, 10) || 1;
if (endPage < startPage) {
var temp = startPage;
startPage = endPage;
endPage = temp;
}
fetchFromPage(startPage, endPage);
}
function stopFetch() {
stopFetching = true;
progressRuntime.isRunning = false;
progressRuntime.stoppedByUser = true;
scheduleStatePersist();
}
async function initializePluginUi() {
createFloatingPanel();
var panel = document.getElementById('magnet-floating-panel');
if (!panel) return;
var settingsArea = panel.querySelector('#magnet-settings');
if (!settingsArea) return;
if (isListPage() && !document.getElementById('keyword-input')) {
var currentPage = getCurrentPage();
var keywordDiv = document.createElement('div');
keywordDiv.className = 'magnet-control-row';
keywordDiv.innerHTML = '<input type="text" id="keyword-input" placeholder="关键词(逗号分隔多关键词)" style="width:100%;padding:11px 12px;border:1px solid #d8e2ef;border-radius:12px;font-size:13px;box-sizing:border-box;background:#fff;color:#17314a;box-shadow:inset 0 1px 2px rgba(15,23,42,.03)">';
var pageRange = document.createElement('div');
pageRange.className = 'magnet-control-row';
pageRange.style.cssText = 'font-size:12px;color:#4e5d73';
pageRange.innerHTML = '<span>页码范围</span><input type="number" id="page-start" value="' + currentPage + '" min="1" style="width:72px;padding:9px 10px;border:1px solid #d8e2ef;border-radius:12px;text-align:center;font-size:13px;background:#fff"><span>到</span><input type="number" id="page-end" value="' + currentPage + '" min="1" style="width:72px;padding:9px 10px;border:1px solid #d8e2ef;border-radius:12px;text-align:center;font-size:13px;background:#fff"><span>页</span>';
var btnContainer = document.createElement('div');
btnContainer.className = 'magnet-control-row';
var btn = document.createElement('button');
btn.textContent = '开始';
btn.style.cssText = 'flex:1;padding:11px 14px;background:linear-gradient(135deg,#fb8c00,#ffb300);color:#fff;border:none;border-radius:12px;cursor:pointer;font-size:13px;font-weight:700;box-shadow:0 10px 20px rgba(251,140,0,.24)';
btn.onclick = fetchAllMagnets;
var stopBtn = document.createElement('button');
stopBtn.textContent = '停止';
stopBtn.style.cssText = 'padding:11px 14px;background:linear-gradient(135deg,#e53935,#f4511e);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:#8d98a8;color:#fff;border:none;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:#4e5d73';
speedDiv.innerHTML = '<span>抓取速度</span><select id="speed-select" style="flex:1;padding:10px 12px;border:1px solid #d8e2ef;border-radius:12px;font-size:13px;background:#fff;color:#17314a"><option value="slow">慢</option><option value="medium">中</option><option value="fast" selected>快</option><option value="ultrafast">超快</option></select>';
settingsArea.appendChild(keywordDiv);
settingsArea.appendChild(pageRange);
settingsArea.appendChild(btnContainer);
settingsArea.appendChild(speedDiv);
}
if (!document.getElementById('magnet-status')) {
var footer = panel.querySelector('.magnet-panel-footer');
if (footer) {
var statusText = document.createElement('div');
statusText.id = 'magnet-status';
statusText.style.cssText = 'margin-bottom:8px;padding:6px;background:#e3f2fd;border-radius:4px;font-size:11px;color:#1976D2';
footer.insertBefore(statusText, footer.firstChild);
}
}
var restoreResult = await restoreProgressState();
var restored = !!(restoreResult && restoreResult.restored);
var restoredState = restoreResult ? restoreResult.state : null;
if (isListPage()) {
var resumePage = Number.isFinite(Number(restoredState && restoredState.resumeFromPage))
? Math.max(1, Number(restoredState.resumeFromPage))
: Math.max(1, Number(restoredState && restoredState.startPage) || 1);
var resumeEndPage = Number.isFinite(Number(restoredState && restoredState.endPage))
? Math.max(1, Number(restoredState.endPage))
: resumePage;
var statusType = typeof (restoredState && restoredState.statusType) === 'string'
? restoredState.statusType
: '';
var statusText = typeof (restoredState && restoredState.statusText) === 'string'
? restoredState.statusText
: '';
var legacyRunningHint = statusType === 'loading' || /正在|开始|第\d+\/\d+页/.test(statusText);
var shouldAutoResume = !!(
restoredState &&
(restoredState.isRunning || legacyRunningHint) &&
!restoredState.stoppedByUser &&
resumePage <= resumeEndPage
);
if (shouldAutoResume && !isFetching) {
updateStatus('检测到未完成任务,正在自动继续...', 'loading');
fetchFromPage(resumePage, resumeEndPage, {
preserveExisting: true,
keyword: typeof restoredState.keyword === 'string' ? restoredState.keyword : '',
speedMode: typeof restoredState.speedMode === 'string' ? restoredState.speedMode : 'fast'
});
return;
}
if (!restored) {
updateStatus('输入页码范围获取磁力', '');
}
return;
}
var pageTitle = document.title || '当前帖子';
var magnets = extractMagnets();
if (magnets.length > 0) {
magnets.forEach(function(m) { addMagnetItem(pageTitle, m); });
updateStatus('找到 ' + magnets.length + ' 个磁力', 'done');
} else if (!restored) {
updateStatus('未找到磁力链接', 'error');
}
}
var currentUrl = window.location.hostname;
if (currentUrl.includes('sehuatang')) {
initializePluginUi().catch(function(e) {
log('初始化失败: ' + e);
});
setInterval(function() {
var panelExists = !!document.getElementById('magnet-floating-panel');
var ballExists = !!document.getElementById('magnet-float-ball');
if (panelExists && ballExists) {
return;
}
hasRestoredProgress = false;
initializePluginUi().catch(function(e) {
log('自动恢复UI失败: ' + e);
});
}, 2500);
}
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
if (request.action === 'getMagnets') {
var magnets = extractMagnets();
sendResponse({
magnets: magnets,
found: magnets.length > 0,
count: magnets.length
});
return;
}
if (request.action === 'copyMagnet') {
var magnetsToCopy = extractMagnets();
if (magnetsToCopy.length === 0) {
sendResponse({ found: false, count: 0 });
return;
}
navigator.clipboard.writeText(magnetsToCopy.join('\n'))
.then(function() {
sendResponse({ found: true, count: magnetsToCopy.length });
})
.catch(function(err) {
var errorMsg = err && err.message ? err.message : '复制失败';
sendResponse({ found: false, count: magnetsToCopy.length, error: errorMsg });
});
return true;
}
});
})();