// ==UserScript== // @name GGn Video Games by Genre - Search Helper (One-Click Auto Edition) // @namespace feng9148scripts // @version 1.7 // @author feng9148 (Modified) // @description GGn Forum Game "Video Games by Genre" helper with One-Click background fetch & auto-fill. // @match https://gazellegames.net/forums.php* // @match https://gazellegames.net/torrents.php* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; // ========================================================================= // SEARCH RESULTS PAGE — active when ?vgbg=1 is present // ========================================================================= if (location.pathname === '/torrents.php') { if (!new URLSearchParams(location.search).has('vgbg')) return; const style = document.createElement('style'); style.textContent = ` .vgbg-copy { margin-left: 6px; padding: 0 6px; height: 16px; line-height: 16px; font-size: 11px; font-family: Arial, sans-serif; background: #2b4e66; color: #fff; border: none; border-radius: 3px; cursor: pointer; vertical-align: middle; } .vgbg-copy.done { background: #2b6640; } tr.group.vgbg-hidden, tr.group_torrent.vgbg-hidden { display: none; } `; document.head.appendChild(style); function process() { for (const row of document.querySelectorAll('tr.group:not([data-vgbg])')) { row.dataset.vgbg = '1'; const ratingSpan = row.querySelector('span#grouprating'); if (ratingSpan && ratingSpan.textContent.includes('[18+]')) { row.classList.add('vgbg-hidden'); const idMatch = row.className.match(/\bgroup_(\d+)\b/); if (idMatch) { for (const child of document.querySelectorAll(`tr.group_torrent.groupid_${idMatch[1]}`)) { child.classList.add('vgbg-hidden'); } } continue; } const idMatch = row.className.match(/\bgroup_(\d+)\b/); if (!idMatch) continue; const nameLink = row.querySelector('span#groupname a'); if (!nameLink) continue; const groupName = nameLink.textContent.trim(); const yearEl = row.querySelector('#groupyear'); const groupYear = yearEl ? yearEl.textContent.trim() : ''; const bbcode = `[url=https://gazellegames.net/torrents.php?id=${idMatch[1]}]${groupName} ${groupYear}[/url]`; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'vgbg-copy'; btn.textContent = 'click to copy'; btn.title = bbcode; btn.addEventListener('click', e => { e.preventDefault(); function done() { btn.textContent = '✓ copied!'; btn.classList.add('done'); setTimeout(() => { btn.textContent = 'click to copy'; btn.classList.remove('done'); }, 1500); } if (navigator.clipboard) { navigator.clipboard.writeText(bbcode).then(done).catch(fallback); } else { fallback(); } function fallback() { const ta = document.createElement('textarea'); ta.value = bbcode; ta.style.cssText = 'position:fixed;opacity:0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); ta.remove(); done(); } }); nameLink.after(btn); } } process(); new MutationObserver(process).observe(document.body, { childList: true, subtree: true }); return; } // ========================================================================= // FORUM THREAD PAGE // ========================================================================= const THREAD_ID = '14895'; const threadInput = document.querySelector('input[name="thread"]'); if (!threadInput || threadInput.value !== THREAD_ID) return; //////// Genre → GGn tag(s) mapping (具体子分类优先,宽泛大类垫底) const GENRE_TAGS = [ // --- 复合/具体子分类 (优先匹配) --- [/\bfps\b|first.?person.?shoot/i, ['shooter', 'first.person']], [/\bshoot.?em.?up\b|\bshmup\b/i, ['shoot.em.up']], [/\brail.?shoot/i, ['rail.shooter']], [/\btwin.?stick/i, ['twin.stick']], [/\bbullet.?hell\b/i, ['bullet.hell']], [/\bbeat.?em.?up\b/i, ['beat.em.up']], [/\bhack.?and.?slash\b/i, ['hack.and.slash']], [/\breal.?time.?strat|\brts\b/i, ['real.time.strategy']], [/\bturn.?based.?strat|\btbs\b/i, ['turn.based.strategy']], [/\bgrand.?strat/i, ['grand.strategy']], [/\b(vehicle|flight|space).?sim/i, ['vehicle.simulation', 'flight.simulation', 'space.simulation']], [/\blife.?sim/i, ['life.simulation']], [/\bdating.?sim/i, ['dating.simulation']], [/\bbusiness.?sim/i, ['business.simulation']], [/\bconstruction.?sim/i, ['construction.simulation']], [/\bwalking.?sim/i, ['walking.simulation']], [/\bvisual.?novel\b/i, ['visual.novel']], [/\btext.?adventure\b/i, ['text.adventure']], [/\bgraphic.?adventure\b/i, ['graphic.adventure']], [/\bpoint.?and.?click\b/i, ['adventure', 'point.and.click']], [/\binteractive.?fiction\b/i, ['interactive.fiction']], [/\binteractive.?movie\b/i, ['interactive.movie']], [/\bfull.?motion.?video\b|\bfmv\b/i, ['full.motion.video']], [/\btower.?de[fe][en][cn][sc]e\b/i, ['tower.defense']], [/\bbattle.?royale\b/i, ['battle.royale']], [/\bcard.?battler\b/i, ['card.battler']], [/\bcard.?game\b/i, ['card.game']], [/\bdeck.?build/i, ['deck.building']], [/\bboard.?game\b/i, ['board.game']], [/\bauto.?battler\b/i, ['auto.battler']], [/\bdungeon.?crawl/i, ['dungeon.crawler']], [/\bsouls.?like\b/i, ['souls.like']], [/\bgod.?game\b/i, ['god.game']], [/\bcity.?build/i, ['city.building']], [/\bhidden.?object\b/i, ['hidden.object']], [/\bmatch.?3\b/i, ['match.3']], [/\bcreature.?collect/i, ['creature.collector']], // --- 独立/基础大类 (置后兜底,防止提前截胡复合词) --- [/\brpg\b|role.?playing/i, ['role.playing']], [/\bmetroidvania\b/i, ['metroidvania']], [/\broguelite\b/i, ['roguelite']], [/\broguelike\b/i, ['roguelike']], [/\bplatform(?:er)?\b/i, ['platformer']], [/\bpuzzle\b/i, ['puzzle']], [/\brhythm\b/i, ['rhythm']], [/\bfight(?:ing)?\b/i, ['fighting']], [/\bsurvival\b/i, ['survival']], [/\bstealth\b/i, ['stealth']], [/\bracing\b/i, ['racing']], [/\bhorror\b/i, ['horror']], [/\bsport[s]?\b/i, ['sports']], [/\bidle\b|\bclicker\b/i, ['idle']], [/\bmanagement\b/i, ['management']], [/\bparty\b/i, ['party']], [/\btrivia\b/i, ['trivia']], [/\barcade\b/i, ['arcade']], [/\btactics\b/i, ['tactics']], [/\b4[xX]\b/i, ['4x']], [/\bwargame\b/i, ['wargame']], [/\btycoon\b/i, ['tycoon']], [/\bpinball\b/i, ['pinball']], [/\bshooter\b/i, ['shooter']], [/\badventure\b/i, ['adventure']], [/\bstrategy\b/i, ['strategy']], [/\bsimulat/i, ['simulation']], [/\baction\b/i, ['action']], ]; function getTagsForGenre(genre) { const cleaned = genre.trim().replace(/\./g, ' '); for (const [re, tags] of GENRE_TAGS) { if (re.test(cleaned)) return tags; } return [cleaned.toLowerCase().replace(/\s+/g, '.')]; } //////// Genre bank const GENRE_BANK = [ 'Action', 'Adventure', 'Beat em Up', 'Board Game', 'Bullet Hell', 'Card Game', 'Fighting', 'Horror', 'Idle', 'Management', 'Metroidvania', 'Party', 'Platformer', 'Puzzle', 'Racing', 'Rhythm', 'Roguelike', 'Roguelite', 'RPG', 'Shoot em Up', 'Shooter', 'Simulation', 'Sports', 'Stealth', 'Strategy', 'Survival', 'Tower Defense', 'Turn-Based Strategy', 'Real-Time Strategy', 'Visual Novel', 'Hack and Slash', ]; ///////// Parse last post function getLastPost() { const posts = Array.from(document.querySelectorAll('[id^="post"]')) .filter(p => !p.classList.contains('sticky_post')); if (!posts.length) return null; const last = posts[posts.length - 1]; const body = last.querySelector('td.body'); return { el: last, text: body ? body.innerText.trim() : '' }; } function parsePrompt(text) { const lines = text.split('\n') .map(l => l.trim()) .filter(l => l && !/^last edited by\b/i.test(l)); if (!lines.length) return null; let promptLine = null; for (let i = lines.length - 1; i >= 0; i--) { if (/\d{4}/.test(lines[i])) { promptLine = lines[i]; break; } } if (!promptLine) return null; const rangeM = promptLine.match(/(\d{4})\s*[-–]\s*(\d{4})/); const openM = promptLine.match(/(\d{4})\s*[-–]\s*\)/); const singleM = promptLine.match(/\(?\s*(\d{4})\s*\)?/); let yearStart, yearEnd; if (rangeM) { yearStart = +rangeM[1]; yearEnd = +rangeM[2]; } else if (openM) { yearStart = +openM[1]; yearEnd = null; } else if (singleM) { yearStart = yearEnd = +singleM[1]; } else { return null; } const genre = promptLine.replace(/[,(\s]\s*\d{4}\s*[-–]?\s*\d{0,4}\s*\)?.*$/, '').trim(); if (!genre) return null; return { genre, yearStart, yearEnd }; } ////////// Genre/year generator function getRecentGenres(n) { return Array.from(document.querySelectorAll('[id^="post"]')) .filter(p => !p.classList.contains('sticky_post')) .slice(-(n || 15)) .map(p => { const body = p.querySelector('td.body'); if (!body) return null; const lines = body.innerText.trim().split('\n') .map(l => l.trim()) .filter(l => l && !/^last edited by\b/i.test(l)); for (let i = lines.length - 1; i >= 0; i--) { if (/\d{4}/.test(lines[i])) { return lines[i].replace(/[,(\s]\s*\d{4}.*$/, '').trim().toLowerCase() || null; } } return null; }) .filter(Boolean); } function pickGenre(recentGenres) { const recentSet = new Set(recentGenres.map(g => g.toLowerCase())); const available = GENRE_BANK.filter(g => !recentSet.has(g.toLowerCase())); const pool = available.length ? available : GENRE_BANK; return pool[Math.floor(Math.random() * pool.length)]; } function pickYearRange(genre) { const now = new Date().getFullYear(); const lengths = [4, 4, 5, 5, 6, 6, 7, 7, 8]; const len = lengths[Math.floor(Math.random() * lengths.length)]; let minStart = 1990; if (genre && genre.toLowerCase() === 'tower defense') { minStart = 2007; } const maxStart = now - len + 1; const safeMinStart = minStart > maxStart ? maxStart : minStart; const start = safeMinStart + Math.floor(Math.random() * (maxStart - safeMinStart + 1)); return { start, end: start + len - 1 }; } function formatCombo(genre, start, end) { return `${genre} ${start}-${end}`; } /////// Build search URL function buildSearchUrl(genre, yearStart, yearEnd) { const tags = getTagsForGenre(genre); const yearParam = yearStart ? (yearEnd && yearEnd !== yearStart ? `${yearStart}-${yearEnd}` : String(yearStart)) : null; const p = new URLSearchParams({ action: 'advanced', taglist: tags.join(','), tags_type: '0', order_by: 's3', order_way: 'desc', 'filter_cat[1]': '1', vgbg: '1', }); if (yearParam) p.set('year', yearParam); return 'https://gazellegames.net/torrents.php?' + p.toString(); } /////// Background Fetch, Timeout, Cache & Rate Limit (核心后台控制) const FETCH_TIMEOUT_MS = 10000; // 10秒超时 const COOLDOWN_MS = 3000; // 网络请求后 3秒冷却时间 const searchCache = new Map(); // URL -> { status: 'ok'|'empty'|'error', games: [], message?: string } let activeController = null; let lastNetworkTime = 0; // 页面关闭或跳转时取消未完成的请求,避免多标签页堆积挂起请求 window.addEventListener('beforeunload', () => { if (activeController) { activeController.abort(); activeController = null; } }); function getRecentGameIds(n = 15) { const ids = new Set(); const posts = Array.from(document.querySelectorAll('[id^="post"]')) .filter(p => !p.classList.contains('sticky_post')) .slice(-n); for (const post of posts) { const body = post.querySelector('td.body'); if (!body) continue; for (const a of body.querySelectorAll('a[href*="torrents.php"]')) { const match = a.href.match(/[?&]id=(\d+)/); if (match) ids.add(match[1]); } const textMatches = body.innerText.matchAll(/torrents\.php\?[^ \n\r\t"'\]]*?id=(\d+)/g); for (const match of textMatches) { ids.add(match[1]); } } return ids; } async function fetchGameByGenre(genre, yearStart, yearEnd) { const url = buildSearchUrl(genre, yearStart, yearEnd); // 1. 检查同页缓存:避免对同一搜索 URL 重复发起网络请求(也避免重复请求失败的页面) if (searchCache.has(url)) { const cached = searchCache.get(url); if (cached.status === 'error') { throw new Error(`上次请求已失败 (${cached.message})。为避免重复请求失败页面,请修改条件或点击 Search Games 手动查看。`); } if (cached.status === 'empty' || !cached.games.length) { return { bbcode: null, fromCache: true }; } const pick = cached.games[Math.floor(Math.random() * cached.games.length)]; return { bbcode: pick, fromCache: true }; } // 2. 检查请求频率限制 const now = Date.now(); const elapsed = now - lastNetworkTime; if (elapsed < COOLDOWN_MS) { const waitSec = Math.ceil((COOLDOWN_MS - elapsed) / 1000); throw new Error(`请求过于频繁,请等待 ${waitSec} 秒后再试。`); } // 3. 设置 AbortController 超时控制 if (activeController) { activeController.abort(); } const controller = new AbortController(); activeController = controller; const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); lastNetworkTime = Date.now(); try { const resp = await fetch(url, { credentials: 'same-origin', signal: controller.signal, }); if (!resp.ok) { searchCache.set(url, { status: 'error', games: [], message: `HTTP ${resp.status}` }); throw new Error(`HTTP ${resp.status}`); } const html = await resp.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const recentGameIds = getRecentGameIds(15); const validGames = []; for (const row of doc.querySelectorAll('tr.group')) { // 过滤 [18+] 游戏 const ratingSpan = row.querySelector('span#grouprating'); if (ratingSpan && ratingSpan.textContent.includes('[18+]')) { continue; } // 提取 Torrent Group ID、游戏名、年份 const displayEl = row.querySelector('#displayname') || row; const linkElement = displayEl.querySelector('#groupname a'); if (!linkElement) continue; const groupName = linkElement.textContent.trim(); const yearEl = displayEl.querySelector('#groupyear'); const groupYear = yearEl ? yearEl.textContent.trim() : ''; let torrentId = null; const href = linkElement.getAttribute('href') || ''; if (href.includes('?')) { torrentId = new URLSearchParams(href.split('?')[1]).get('id'); } if (!torrentId) { const idMatch = row.className.match(/\bgroup_(\d+)\b/); if (idMatch) torrentId = idMatch[1]; } // 排除最近 15 楼已经回答过的游戏 ID if (!torrentId || recentGameIds.has(String(torrentId))) { continue; } if (groupName) { const formatted = `[url=https://gazellegames.net/torrents.php?id=${torrentId}]${groupName} ${groupYear}[/url]`; validGames.push(formatted); } } if (!validGames.length) { searchCache.set(url, { status: 'empty', games: [] }); return { bbcode: null, fromCache: false }; } // 写入缓存并返回随机一项 searchCache.set(url, { status: 'ok', games: validGames }); const pick = validGames[Math.floor(Math.random() * validGames.length)]; return { bbcode: pick, fromCache: false }; } catch (err) { if (err.name === 'AbortError') { searchCache.set(url, { status: 'error', games: [], message: 'Timeout' }); throw new Error(`网络请求超时 (${FETCH_TIMEOUT_MS / 1000}秒),已终止请求。`); } throw err; } finally { clearTimeout(timeoutId); if (activeController === controller) { activeController = null; } } } /////// Inject panel function inject() { const afterNode = document.getElementById('quickreplytext'); if (!afterNode) return; const textarea = document.getElementById('quickpost'); const uiStyle = document.createElement('style'); uiStyle.textContent = ` .vgbg-panel { background: #111e29; border: 1px solid #1c3245; border-radius: 6px; padding: 10px 14px; margin: 8px 0; display: flex; flex-direction: column; align-items: center; gap: 8px; font-family: Arial, sans-serif; } .vgbg-title { font-size: 11px; color: #8da6b8; font-weight: bold; letter-spacing: 0.5px; } .vgbg-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; width: 420px; } .vgbg-input { height: 26px; padding: 0 8px; background: #182a3a; border: 1px solid #294863; border-radius: 4px; color: #fff; font-size: 12px; box-sizing: border-box; } .vgbg-input:focus { border-color: #4384b5; outline: none; } .vgbg-btn { height: 26px; padding: 0 12px; background: #2b4e66; border: 1px solid #36617f; border-radius: 4px; color: #fff; font-size: 11px; font-weight: bold; cursor: pointer; white-space: nowrap; transition: background 0.15s ease, border-color 0.15s ease; } .vgbg-btn:hover { background: #345f7c; border-color: #4a80a6; } .vgbg-btn:disabled { opacity: 0.6; cursor: not-allowed; } .vgbg-btn-generate { width: 100%; background: #1f394c; } .vgbg-btn-generate:hover { background: #2b4e66; } .vgbg-btn-oneclick { width: 100%; height: 30px; background: #235c3b; border-color: #2e7a4e; font-size: 12px; } .vgbg-btn-oneclick:hover { background: #2b7048; border-color: #3b9c64; } .vgbg-dash { color: #708b9e; font-weight: bold; } `; document.head.appendChild(uiStyle); const panel = document.createElement('div'); panel.className = 'vgbg-panel'; const title = document.createElement('div'); title.className = 'vgbg-title'; title.textContent = 'FIND VIDEO GAMES BY GENRE'; // Row 1: 快速识别行 const pasteRow = document.createElement('div'); pasteRow.className = 'vgbg-row'; const pasteInput = document.createElement('input'); pasteInput.type = 'text'; pasteInput.className = 'vgbg-input'; pasteInput.placeholder = 'Paste "Genre 2010-2015" here...'; pasteInput.style.flex = '1'; const pasteBtn = document.createElement('input'); pasteBtn.type = 'button'; pasteBtn.className = 'vgbg-btn'; pasteBtn.value = 'Clipboard Auto-Fill'; pasteRow.append(pasteInput, pasteBtn); // Row 2: 搜索字段与操作行 const searchRow = document.createElement('div'); searchRow.className = 'vgbg-row'; const genreInput = document.createElement('input'); genreInput.type = 'text'; genreInput.className = 'vgbg-input'; genreInput.placeholder = 'Genre'; genreInput.style.flex = '1'; const makeYearInput = (placeholder) => { const el = document.createElement('input'); el.type = 'number'; el.className = 'vgbg-input'; el.placeholder = placeholder; el.style.width = '62px'; el.style.textAlign = 'center'; el.min = '1975'; el.max = String(new Date().getFullYear()); return el; }; const yearFrom = makeYearInput('From'); const yearTo = makeYearInput('To'); const dash = document.createElement('span'); dash.className = 'vgbg-dash'; dash.textContent = '–'; const searchBtn = document.createElement('input'); searchBtn.type = 'button'; searchBtn.className = 'vgbg-btn'; searchBtn.value = 'Search Games'; searchBtn.style.width = '110px'; searchRow.append(genreInput, yearFrom, dash, yearTo, searchBtn); // Row 3: 原本的单独生成下一题按钮 const genRow = document.createElement('div'); genRow.className = 'vgbg-row'; const genBtn = document.createElement('input'); genBtn.type = 'button'; genBtn.className = 'vgbg-btn vgbg-btn-generate'; genBtn.value = 'Generate Genre / Year Combo Only'; genRow.appendChild(genBtn); // Row 4: 新增的一键全自动按钮 const oneClickRow = document.createElement('div'); oneClickRow.className = 'vgbg-row'; const DEFAULT_BTN_TEXT = '⚡ One-Click Auto Fill (一键生成)'; const oneClickBtn = document.createElement('input'); oneClickBtn.type = 'button'; oneClickBtn.className = 'vgbg-btn vgbg-btn-oneclick'; oneClickBtn.value = DEFAULT_BTN_TEXT; oneClickRow.appendChild(oneClickBtn); // 解析与自动填充核心逻辑 function applyParsedText(text) { if (!text) return false; const parsed = parsePrompt(text); if (parsed) { genreInput.value = parsed.genre; yearFrom.value = parsed.yearStart; yearTo.value = parsed.yearEnd != null ? parsed.yearEnd : ''; return true; } return false; } const lp = getLastPost(); if (lp && lp.text) { applyParsedText(lp.text); } const handlePasteEvent = e => { const clipboardData = e.clipboardData || window.clipboardData; if (!clipboardData) return; const pastedText = clipboardData.getData('text'); if (applyParsedText(pastedText)) { e.preventDefault(); } }; genreInput.addEventListener('paste', handlePasteEvent); pasteInput.addEventListener('paste', handlePasteEvent); pasteBtn.addEventListener('click', async () => { if (navigator.clipboard && navigator.clipboard.readText) { try { const text = await navigator.clipboard.readText(); if (applyParsedText(text)) { pasteInput.value = text; } else { alert('Could not recognize Genre/Year pattern in clipboard text.'); } } catch (err) { alert('Failed to read clipboard. Please paste manually into the text box.'); } } else { alert('Clipboard API not supported. Please paste manually.'); } }); searchBtn.addEventListener('click', () => { const genre = genreInput.value.trim(); if (!genre) { genreInput.focus(); return; } const ys = parseInt(yearFrom.value) || null; const ye = parseInt(yearTo.value) || null; window.open(buildSearchUrl(genre, ys, ye), '_blank', 'noopener'); }); genBtn.addEventListener('click', () => { if (!textarea) return; const selectedGenre = pickGenre(getRecentGenres(15)); const years = pickYearRange(selectedGenre); const combo = formatCombo(selectedGenre, years.start, years.end); textarea.value = (textarea.value ? textarea.value.trimEnd() + '\n\n' : '') + combo; textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); }); // 冷却状态锁与倒计时辅助函数 let isCoolingDown = false; function startButtonCooldown(btn, durationMs, statusPrefix) { isCoolingDown = true; btn.disabled = true; let remaining = Math.ceil(durationMs / 1000); btn.value = `${statusPrefix} (${remaining}s 冷却中...)`; const timer = setInterval(() => { remaining -= 1; if (remaining > 0) { btn.value = `${statusPrefix} (${remaining}s 冷却中...)`; } else { clearInterval(timer); isCoolingDown = false; btn.disabled = false; btn.value = DEFAULT_BTN_TEXT; } }, 1000); } // 一键全自动事件处理 oneClickBtn.addEventListener('click', async () => { if (!textarea || isCoolingDown) return; const genre = genreInput.value.trim(); if (!genre) { genreInput.focus(); return; } const ys = parseInt(yearFrom.value) || null; const ye = parseInt(yearTo.value) || null; oneClickBtn.disabled = true; oneClickBtn.value = '⏳ Searching & generating in background...'; let triggeredNetwork = false; try { // 1 & 2. 后台请求搜索页(或读取同页缓存)并提取 ByGenre 格式链接 const result = await fetchGameByGenre(genre, ys, ye); triggeredNetwork = !result.fromCache; if (!result.bbcode) { alert(`未找到符合条件的游戏: ${genre} (${ys || ''}-${ye || ''}),请手动修改条件或点击 Search Games 查看。`); if (triggeredNetwork) { startButtonCooldown(oneClickBtn, COOLDOWN_MS, '⚠ 无结果'); } else { oneClickBtn.disabled = false; oneClickBtn.value = DEFAULT_BTN_TEXT; } return; } // 3. 生成新的 Genre / Year Combo 并填入回复框 const selectedGenre = pickGenre(getRecentGenres(15)); const years = pickYearRange(selectedGenre); const combo = formatCombo(selectedGenre, years.start, years.end); textarea.value = `${result.bbcode}\n\n${combo}`; textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); if (triggeredNetwork) { // 首次网络请求后进入 3 秒冷却,防止频繁请求服务器 startButtonCooldown(oneClickBtn, COOLDOWN_MS, '✓ Done!'); } else { // 命中本地缓存重抽时,不消耗网络请求,仅短暂防抖 400ms oneClickBtn.value = '✓ Re-rolled from cache! (缓存秒抽)'; setTimeout(() => { if (!isCoolingDown) { oneClickBtn.disabled = false; oneClickBtn.value = DEFAULT_BTN_TEXT; } }, 400); } } catch (err) { console.error(err); alert('操作提示: ' + err.message); startButtonCooldown(oneClickBtn, COOLDOWN_MS, '✗ 请求中止'); } }); panel.append(title, pasteRow, searchRow, genRow, oneClickRow); afterNode.after(panel); } inject(); })();