// ==UserScript==
// @name 磨题帮爬题
// @namespace https://tampermonkey.net/
// @version 1.0
// @description 自动抓取磨题帮题库,支持单选、多选、判断、填空,提取题目导出TXT/Excel
// @match https://www.motibang.com/*
// @grant none
// @run-at document-end
// ==/UserScript==
(async function () {
'use strict';
// ========== 常量配置 ==========
const MAX_FAIL = 5; // 连续抓取失败上限,超过则自动停止
const PANEL_WIDTH = 280; // 控制面板宽度(像素)
const DELAY_ANSWER = 800; // 点击"看答案"后等待渲染的时间(毫秒)
const DELAY_NEXT = 1000; // 点击"下一页"后等待加载的时间(毫秒)
const DELAY_RETRY = 400; // 失败重试前的等待时间(毫秒)
const NEXT_RETRY = 5; // 查找"下一页"按钮的最大重试次数
const NEXT_INTERVAL = 300; // 查找"下一页"按钮的重试间隔(毫秒)
const ANIM_DURATION = 300; // UI 动画持续时间(毫秒)
const TIMEOUT = 12000; // 单题抓取的超时时间(毫秒)
const BTN_HEIGHT = '38px'; // 面板按钮统一高度
// ========== 通用工具函数 ==========
/** 异步延时函数,返回指定毫秒后 resolve 的 Promise */
const sleep = ms => new Promise(r => setTimeout(r, ms));
/** 清理文件名中的非法字符(Windows/Mac 兼容) */
const cleanFileName = str => (str || '题库').replace(/[\/\\:*?"<>|]/g, '_');
/**
* 判断 DOM 元素当前是否可以被点击
* 条件:存在、未禁用、可见、未被隐藏
*/
function isClickable(el) {
if (!el) return false;
if (el.disabled) return false;
if (el.offsetParent === null) return false;
const style = window.getComputedStyle(el);
if (style.visibility === 'hidden' || style.opacity === '0') return false;
if (style.display === 'none') return false;
return true;
}
/**
* 为异步操作添加超时保护
* 如果 promise 在 ms 毫秒内未完成,返回 fallback
*/
async function withTimeout(promise, ms, fallback = null) {
let timer;
const timeoutPromise = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('Timeout')), ms);
});
try {
const result = await Promise.race([promise, timeoutPromise]);
clearTimeout(timer);
return result;
} catch (e) {
clearTimeout(timer);
return fallback;
}
}
// ========== 全局运行状态 ==========
let isRunning = false; // 是否正在抓取中
let output = ''; // TXT 导出内容的累积字符串
let records = []; // Excel 导出的题目记录数组
let count = 0; // 已成功抓取的题目数量
let totalQuestions = 0; // 题库总题数(从页面解析)
let failCount = 0; // 连续抓取失败计数
let examTitle = ''; // 试卷标题(用于导出文件名)
let isDark = false; // 深色模式开关
let currentStatus = ''; // 当前状态栏文本
let isMinimized = false; // 面板是否已最小化
let exportExpanded = false; // 导出子面板是否已展开
const seenQuestions = new Set(); // 已抓取题目去重集合
let panelBodyHeight = 0; // 面板本体高度缓存(用于折叠动画)
// ========== 主题配色方案 ==========
/** 根据当前 isDark 状态返回主题配色对象 */
function getTheme() {
return {
panelBg: isDark ? 'rgba(30,30,30,0.96)' : '#ffffff',
panelShadow: isDark ? '0 4px 22px rgba(0,0,0,0.65)' : '0 4px 22px rgba(0,0,0,0.17)',
headerBg: isDark ? 'rgba(50,50,55,0.98)' : '#f0f2f5',
headerBorder: isDark ? '#555' : '#dee2e6',
textColor: isDark ? '#e8e8e8' : '#1a1a2e',
subTextColor: isDark ? '#aaa' : '#6c757d',
btnColor: isDark ? '#ddd' : '#495057',
};
}
/** 将当前主题应用到面板的所有子元素 */
function applyTheme() {
const t = getTheme();
panel.style.background = t.panelBg;
panel.style.boxShadow = t.panelShadow;
dragHandle.style.background = t.headerBg;
dragHandle.style.borderBottomColor = t.headerBorder;
panelTitleWrap.style.color = t.textColor;
darkBtn.style.color = t.btnColor;
minBtn.style.color = t.btnColor;
progressRow.style.color = t.textColor;
statusEl.style.color = t.subTextColor;
}
// ========== 控制面板 DOM 构建 ==========
/** 创建整个控制面板并插入页面 */
const panel = document.createElement('div');
panel.style.cssText = `position:fixed;top:80px;right:28px;z-index:2147483647;border-radius:14px;width:${PANEL_WIDTH}px;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;cursor:default;user-select:none;overflow:visible;touch-action:none;transition:box-shadow ${ANIM_DURATION}ms ease,background ${ANIM_DURATION}ms ease;`;
panel.innerHTML = `
`;
document.body.appendChild(panel);
// ========== DOM 元素引用缓存 ==========
const dragHandle = document.getElementById('dragHandle'); // 拖拽手柄
const panelTitleWrap = document.getElementById('panelTitleWrap'); // 标题栏文字区域
const statusText = document.getElementById('statusText'); // 最小化时显示的状态文字
const minBtn = document.getElementById('minBtn'); // 最小化按钮
const darkBtn = document.getElementById('darkBtn'); // 深色模式按钮
const panelBody = document.getElementById('panelBody'); // 面板内容区域
const progressRow = document.getElementById('progressRow'); // 进度显示行
const statusEl = document.getElementById('status'); // 状态文字元素
const startStopBtn = document.getElementById('startStopBtn'); // 开始/暂停按钮
const exportBtn = document.getElementById('exportBtn'); // 导出按钮
const exportSub = document.getElementById('exportSub'); // 导出子面板
const exportTxtBtn = document.getElementById('exportTxtBtn'); // TXT 导出按钮
const exportCsvBtn = document.getElementById('exportCsvBtn'); // Excel 导出按钮
// ========== 面板高度缓存 ==========
/** 在面板展开状态下缓存其真实高度,供折叠动画使用 */
function cachePanelHeight() {
if (!isMinimized) {
const pmh = panelBody.style.maxHeight;
const po = panelBody.style.overflow;
panelBody.style.maxHeight = 'none';
panelBody.style.overflow = 'visible';
panelBodyHeight = panelBody.scrollHeight;
panelBody.style.maxHeight = pmh;
panelBody.style.overflow = po;
}
}
// ========== 导出子面板展开/收起 ==========
/** 展开导出子面板,显示 TXT 和 Excel 两个按钮 */
function expandExport() {
if (exportExpanded) { collapseExport(); return; }
exportExpanded = true;
exportBtn.style.opacity = '0';
exportBtn.style.pointerEvents = 'none';
exportSub.style.transform = 'translateY(0) scale(1)';
exportSub.style.opacity = '1';
exportSub.style.pointerEvents = 'auto';
}
/** 收起导出子面板,只显示"导出题库"主按钮 */
function collapseExport() {
exportExpanded = false;
exportSub.style.transform = 'translateY(-10px) scale(0.88)';
exportSub.style.opacity = '0';
exportSub.style.pointerEvents = 'none';
exportBtn.style.opacity = '1';
exportBtn.style.pointerEvents = 'auto';
}
// 绑定导出按钮事件
exportBtn.onclick = (e) => { e.stopPropagation(); expandExport(); };
exportTxtBtn.onclick = (e) => { e.stopPropagation(); collapseExport(); saveTxt(); };
exportCsvBtn.onclick = (e) => { e.stopPropagation(); collapseExport(); saveExcel(); };
// 拖拽标题栏或点击标题栏时自动收起导出子面板
dragHandle.addEventListener('mousedown', collapseExport);
dragHandle.addEventListener('touchstart', collapseExport, { passive: false });
// ========== 面板拖拽功能 ==========
let isDragging = false, startX, startY, startLeft, startTop;
/** 获取鼠标或触摸事件的客户端坐标 */
function getPos(e) {
return e.touches?.length > 0
? { x: e.touches[0].clientX, y: e.touches[0].clientY }
: { x: e.clientX, y: e.clientY };
}
/** 拖拽开始:记录起始位置,暂停面板过渡动画 */
function dragStart(e) {
if (e.target.closest('button')) return;
e.preventDefault();
isDragging = true;
const p = getPos(e);
startX = p.x; startY = p.y;
const r = panel.getBoundingClientRect();
startLeft = r.left; startTop = r.top;
panel.style.transition = 'none';
}
/** 拖拽移动:实时更新面板位置,限制在视口范围内 */
function dragMove(e) {
if (!isDragging) return;
e.preventDefault();
const p = getPos(e);
let l = startLeft + p.x - startX;
let t = startTop + p.y - startY;
l = Math.max(0, Math.min(l, window.innerWidth - PANEL_WIDTH));
t = Math.max(0, Math.min(t, window.innerHeight - 60));
panel.style.left = l + 'px';
panel.style.top = t + 'px';
panel.style.right = 'auto';
}
/** 拖拽结束:恢复面板过渡动画 */
function dragEnd() {
if (isDragging) {
isDragging = false;
requestAnimationFrame(() => {
panel.style.transition = `box-shadow ${ANIM_DURATION}ms ease,background ${ANIM_DURATION}ms ease`;
});
}
}
// 绑定拖拽事件(同时支持鼠标和触摸)
dragHandle.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', dragMove);
document.addEventListener('mouseup', dragEnd);
dragHandle.addEventListener('touchstart', dragStart, { passive: false });
document.addEventListener('touchmove', dragMove, { passive: false });
document.addEventListener('touchend', dragEnd);
document.addEventListener('touchcancel', dragEnd);
// ========== 深色模式切换 ==========
darkBtn.onclick = (e) => {
e.stopPropagation();
collapseExport();
isDark = !isDark;
darkBtn.textContent = isDark ? '●' : '○';
applyTheme();
};
// ========== 面板最小化/展开 ==========
/** 切换面板最小化状态,使用 max-height 动画实现平滑折叠 */
async function toggleMinimize() {
collapseExport();
isMinimized = !isMinimized;
if (isMinimized) {
cachePanelHeight();
panelBody.style.maxHeight = panelBodyHeight + 'px';
panelBody.style.opacity = '1';
void panelBody.offsetHeight; // 强制回流
panelBody.style.maxHeight = '0px';
panelBody.style.opacity = '0';
panelBody.style.padding = '0 14px';
panelBody.style.pointerEvents = 'none';
dragHandle.style.borderRadius = '14px';
dragHandle.style.borderBottom = 'none';
minBtn.textContent = '+';
} else {
if (panelBodyHeight === 0) cachePanelHeight();
panelBody.style.maxHeight = '0px';
panelBody.style.opacity = '0';
panelBody.style.padding = '0 14px';
void panelBody.offsetHeight;
panelBody.style.maxHeight = panelBodyHeight + 'px';
panelBody.style.opacity = '1';
panelBody.style.padding = '10px 14px 14px';
panelBody.style.pointerEvents = 'auto';
dragHandle.style.borderRadius = '14px 14px 0 0';
dragHandle.style.borderBottom = '1px solid';
minBtn.textContent = '−';
}
updateTitle();
applyTheme();
}
minBtn.onclick = (e) => { e.stopPropagation(); toggleMinimize(); };
// ========== 界面状态更新 ==========
/** 更新标题栏:最小化时在标题栏右侧显示当前状态 */
function updateTitle() {
if (isMinimized && currentStatus) {
statusText.style.display = 'inline-block';
statusText.textContent = currentStatus;
} else {
statusText.style.display = 'none';
statusText.textContent = '';
}
}
/** 刷新进度显示(已抓取数 / 总题数) */
function setProgress() {
const el = document.getElementById('progress');
if (el) el.innerText = `${count}/${totalQuestions || '?'}`;
updateTitle();
}
/** 设置底部状态栏文本,同时更新标题栏(最小化时可见) */
function setStatus(s) {
currentStatus = s;
if (statusEl) statusEl.innerText = s;
updateTitle();
}
// ========== 导出功能 ==========
/** 导出为 TXT 文件(UTF-8 BOM 编码,兼容 Windows 记事本) */
function saveTxt() {
if (!output) { setStatus('⚠️ 暂无数据'); return; }
const b = new Blob(['\uFEFF' + output], { type: "text/plain;charset=utf-8" });
const u = URL.createObjectURL(b);
const a = document.createElement("a");
a.href = u;
a.download = `${cleanFileName(examTitle)}_${count}题.txt`;
a.click();
URL.revokeObjectURL(u);
setStatus('✅ TXT导出完成');
}
/** 题型代码转中文名称 */
function typeToText(t) {
const m = { single: '单选题', multiple: '多选题', judge: '判断题', fillblank: '填空题' };
return m[t] || '单选题';
}
/** CSV 字段值转义:处理逗号、引号、换行符 */
function escapeCSV(s) {
if (!s) return '';
const x = s.replace(/[\r\n]+/g, ' ').trim();
if (x.includes(',') || x.includes('"')) return `"${x.replace(/"/g, '""')}"`;
return x;
}
/** 导出为 Excel (CSV) 文件,每个选项单独一列 */
function saveExcel() {
if (records.length === 0) { setStatus('⚠️ 暂无数据'); return; }
// 收集所有题目中出现过的选项字母
const allKeys = new Set();
records.forEach(r => {
if (r.type === 'single' || r.type === 'multiple') {
Object.keys(r.options).forEach(k => allKeys.add(k));
}
});
const sortedKeys = Array.from(allKeys).sort();
// 构建表头
let csv = '\uFEFF题干,题型,' + sortedKeys.join(',') + ',答案\n';
// 逐行填充数据
records.forEach(r => {
const q = escapeCSV(r.question);
const t = typeToText(r.type);
const optionCols = sortedKeys.map(k => escapeCSV(r.options[k] || ''));
const a = escapeCSV(r.answer);
csv += `${q},${t},${optionCols.join(',')},${a}\n`;
});
const b = new Blob([csv], { type: "text/csv;charset=utf-8" });
const u = URL.createObjectURL(b);
const a = document.createElement("a");
a.href = u;
a.download = `${cleanFileName(examTitle)}_${count}题.csv`;
a.click();
URL.revokeObjectURL(u);
setStatus('✅ Excel导出完成');
}
// ========== 清空答题记录并跳回第 1 题 ==========
/**
* 执行完整流程:点击"更多"→"清空所有答题记录"→确认弹窗点"是"→点击答题卡第 1 题
* 确保每次抓取都从第 1 题开始,避免因历史答题记录导致抓取不完整
*/
async function resetAllRecords() {
try {
// 1. 点击"更多"按钮打开设置菜单
const moreBtn = Array.from(document.querySelectorAll('button')).find(
b => b.textContent.trim() === '更多' && isClickable(b)
);
if (!moreBtn) { setStatus('⚠️ 未找到更多按钮'); return false; }
moreBtn.click();
await sleep(600);
// 2. 在菜单中点击"清空所有答题记录"
const clearBtn = Array.from(document.querySelectorAll('.clear-btn')).find(
b => b.textContent.trim() === '清空所有答题记录' && b.offsetParent !== null
);
if (!clearBtn) { moreBtn.click(); setStatus('⚠️ 未找到清空按钮'); return false; }
clearBtn.click();
// 3. 轮询等待确认弹窗的"是"按钮出现(Element UI 弹窗渲染可能有延迟)
let confirmBtn = null;
const st = Date.now();
while (Date.now() - st < 3000) {
confirmBtn = Array.from(document.querySelectorAll('button')).find(b =>
b.textContent.trim() === '是' &&
b.classList.contains('el-button--primary') &&
b.offsetParent !== null
);
if (confirmBtn) break;
await sleep(150);
}
if (!confirmBtn) { setStatus('⚠️ 未找到确认按钮'); return false; }
confirmBtn.click();
setStatus('🔄 已清空记录');
await sleep(1500);
// 4. 点击右侧答题卡中的第 1 题
const firstQ = Array.from(document.querySelectorAll('.question-col.pointer')).find(
el => el.textContent.trim() === '1' && isClickable(el)
);
if (firstQ) {
firstQ.click();
setStatus('✅ 已跳回第1题');
await sleep(1000);
}
return true;
} catch (e) {
console.error('清空记录失败', e);
return false;
}
}
// ========== 页面元素定位与交互 ==========
/** 点击"看答案"按钮(精确匹配文字,排除"更多"按钮) */
function clickShowAnswer() {
const b = Array.from(document.querySelectorAll('button')).find(b =>
['看答案', '查看答案', '显示答案'].includes(b.textContent.trim()) && isClickable(b)
);
if (b) { b.click(); return true; }
return false;
}
/** 等待正确答案渲染完成(支持选择题的 .correct 标记、填空答案、判断文本) */
async function waitAnswerReady(to = 3000) {
const st = Date.now();
while (Date.now() - st < to) {
if (document.querySelector('.button.correct.choice-abc')) return true;
if (document.querySelector('.fillblank-answer')) return true;
if (/正确答案[::]\s*[对错正确错误是否√×TFYNtfyn]/.test(document.body.innerText)) return true;
await sleep(200);
}
return false;
}
/** 查找并点击"下一页"按钮(排除 disabled 状态) */
async function waitNextBtn() {
for (let i = 0; i < NEXT_RETRY; i++) {
const b = Array.from(document.querySelectorAll('button')).find(b =>
b.textContent.trim() === '下一页' && isClickable(b) && !b.classList.contains('is-disabled')
);
if (b) { b.click(); return true; }
await sleep(NEXT_INTERVAL);
}
return false;
}
// ========== 题目数据提取 ==========
/** 检测当前题目类型(填空 > 判断 > 多选 > 单选) */
function detectType() {
if (document.querySelector('.fillblank-answer')) return 'fillblank';
const t = document.body.innerText;
if (/正确答案[::]\s*[对错正确错误是否√×TFYNtfyn]/.test(t)) return 'judge';
const c = document.querySelectorAll('.button.correct.choice-abc');
if (c.length > 1) return 'multiple';
if (c.length === 1) return 'single';
if (/单选题/.test(t)) return 'single';
if (/多选题/.test(t)) return 'multiple';
return 'single';
}
/**
* 提取题干文本
* 优先从 .desc-container .content 内的 div 提取,
* 兼容答完题(有 .color-1)和未答题(无 .color-1)两种状态
*/
function getQuestion() {
const q = document.querySelector('.question-content');
if (!q) return '';
const d = q.querySelector('.desc-container .content div') ||
q.querySelector('.desc-container .content');
if (d) return d.textContent.trim();
const t = q.textContent || '';
const m = t.match(/\d+\/\d+\s*(.+?)(?:A|$)/);
if (m) return m[1].trim();
return t.split(/[A-F]\./)[0].replace(/^\d+\/\d+\s*/, '').trim();
}
/**
* 提取答案
* 选择题从 .button.correct.choice-abc 提取选项字母,
* 判断题映射常见文字到"对/错",
* 填空题从 .fillblank-answer 提取
*/
function getAnswer(type) {
if (type === 'fillblank') {
const f = document.querySelector('.fillblank-answer');
return f ? f.textContent.trim() : '';
}
const c = document.querySelectorAll('.button.correct.choice-abc');
if (c.length > 0) return Array.from(c).map(e => e.textContent.trim()).join('');
const t = document.body.innerText;
if (type === 'judge') {
const j = {
'对': '对', '错': '错', '正确': '对', '错误': '错',
'是': '对', '否': '错', '√': '对', '×': '错',
'T': '对', 'F': '错', 'Y': '对', 'N': '错',
't': '对', 'f': '错', 'y': '对', 'n': '错'
};
const m = t.match(/正确答案[::]\s*([对错正确错误是否√×TFYNtfyn]+)/);
if (m) return j[m[1].trim()] || m[1].trim();
}
const m = t.match(/正确答案[是::\s]*([A-F]+)/i);
return m ? m[1].toUpperCase() : '';
}
/**
* 提取选项键值对
* 从 .choice-option 容器中匹配选项字母按钮和选项文字
*/
function getOptions() {
const o = {};
document.querySelectorAll('.choice-option').forEach(el => {
const b = el.querySelector('.button.choice-abc');
const d = el.querySelector('.desc-container .content div') ||
el.querySelector('.desc-container .content');
if (b && d) o[b.textContent.trim()] = d.textContent.trim();
});
return o;
}
/** 提取试卷标题(从包含"更多"的容器文本中截取) */
function getExamTitle() {
const w = document.querySelector('[class*="question"]');
if (!w) return '';
const t = w.textContent || '';
const m = t.match(/^(.+?)更多/);
if (m) return m[1].trim();
return t.split('更多')[0].trim();
}
/** 从页面文本中解析题目总数(匹配 "当前/总数" 格式) */
function getTotal() {
const m = document.body.innerText.match(/(\d+)\/(\d+)/);
if (m) totalQuestions = parseInt(m[2]);
}
/** 等待题目区域加载完成(检查 .question-content 是否有内容) */
async function waitQuestionReady() {
for (let i = 0; i < 20; i++) {
const q = document.querySelector('.question-content');
if (q && q.textContent.trim()) return true;
await sleep(300);
}
return false;
}
// ========== 单题抓取流程 ==========
/**
* 抓取当前页面显示的一道题目
* 流程:点击看答案 → 等待渲染 → 检测题型 → 提取题干/答案/选项 → 去重
*/
async function grabOne() {
try {
clickShowAnswer();
if (!await waitAnswerReady()) { setStatus('⚠️ 答案未加载'); return null; }
const type = detectType();
const question = getQuestion();
const answer = getAnswer(type);
const options = getOptions();
if (!question) { setStatus('⚠️ 未识别题目'); return null; }
if (type !== 'fillblank' && !answer) { setStatus('⚠️ 无答案'); return null; }
if (seenQuestions.has(question)) { setStatus('⏭️ 跳过重复题'); return null; }
seenQuestions.add(question);
return { type, question, answer, options };
} catch (e) {
console.error('抓取异常', e);
setStatus('⚠️ 抓取异常');
return null;
}
}
/** 将开始/暂停按钮恢复为初始的"开始抓取"状态 */
function resetBtnOnly() {
isRunning = false;
startStopBtn.textContent = '开始抓取';
startStopBtn.style.background = '#2563eb';
startStopBtn.style.color = '#fff';
}
// ========== 主抓取循环 ==========
/**
* 开始/暂停按钮点击处理
* 首次点击:清空旧记录 → 跳回第 1 题 → 初始化 → 逐题抓取
* 暂停后再次点击:从当前题继续
*/
startStopBtn.onclick = async function () {
collapseExport();
// 暂停逻辑
if (isRunning) {
isRunning = false;
startStopBtn.textContent = '继续抓取';
startStopBtn.style.background = '#f59e0b';
startStopBtn.style.color = '#fff';
setStatus('已暂停');
return;
}
// 首次启动:清空旧记录并跳回第 1 题
if (count === 0) {
setStatus('🔄 清空答题记录...');
await resetAllRecords();
setStatus('⏳ 等待页面加载...');
if (!await waitQuestionReady()) { setStatus('❌ 页面未加载完成'); return; }
output = '';
records = [];
failCount = 0;
seenQuestions.clear();
examTitle = getExamTitle();
if (examTitle) output += examTitle + '\n\n';
getTotal();
} else {
failCount = 0;
}
// 开始抓取
isRunning = true;
startStopBtn.textContent = '暂停';
startStopBtn.style.background = '#dc2626';
startStopBtn.style.color = '#fff';
setStatus('🚀 抓取中');
setProgress();
// 主循环:逐题抓取
while (isRunning) {
let data = null;
try { data = await withTimeout(grabOne(), TIMEOUT); } catch (e) { data = null; }
// 抓取失败处理
if (!data || !data.answer) {
failCount++;
setStatus(`⚠️ 抓取失败 ${failCount}/${MAX_FAIL}`);
if (failCount >= MAX_FAIL) {
setStatus('❌ 连续抓取失败,自动停止');
if (output) saveTxt();
resetBtnOnly();
return;
}
await sleep(DELAY_RETRY);
if (!await waitNextBtn()) { setStatus('✅ 抓取完成'); resetBtnOnly(); return; }
await sleep(DELAY_NEXT);
continue;
}
// 抓取成功:记录数据并追加到导出内容
failCount = 0;
count++;
records.push(data);
if (data.type === 'fillblank') {
output += `${count}. ${data.question}\n答案:${data.answer}\n\n`;
} else if (data.type === 'judge') {
output += `${count}. ${data.question}\n答案:${data.answer}\n\n`;
} else {
const keys = Object.keys(data.options).sort();
output += `${count}. ${data.question}\n${keys.map(k => `${k}. ${data.options[k]}`).join('\n')}\n答案:${data.answer}\n\n`;
}
setProgress();
await sleep(DELAY_RETRY);
if (!await waitNextBtn()) { setStatus('✅ 抓取完成'); resetBtnOnly(); return; }
await sleep(DELAY_NEXT);
}
};
// ========== 初始化 ==========
setTimeout(() => cachePanelHeight(), 100);
applyTheme();
setStatus('就绪');
})();