// ==UserScript== // @name 高校邦网课助手 // @namespace https://github.com/Wu557666/gaoxiaobang // @version 1.0.0 // @description 刷课+讨论+AI答题 // @author Wu557666 // @icon https://favicon.im/xmut.gaoxiaobang.com?size=128 // @match https://*.class.gaoxiaobang.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant unsafeWindow // @connect api.deepseek.com // @run-at document-idle // ==/UserScript== (function() { 'use strict'; const $ = window.$ || unsafeWindow.$; const wait = ms => new Promise(r => setTimeout(r, ms)); // ========== 存储键 ========== const STATE_KEY = 'gb_auto_step'; const PROCESSED_TOPICS_KEY = 'gb_processed_topics'; const STORAGE_API_KEY = 'deepseek_api_key'; const STORAGE_API_URL = 'deepseek_api_url'; const STORAGE_MODEL = 'deepseek_model'; const STORAGE_INTERVAL = 'deepseek_interval'; const STORAGE_TIMEOUT = 'deepseek_timeout'; const STORAGE_RETRY = 'deepseek_retry'; const DEFAULT_API_URL = 'https://api.deepseek.com/chat/completions'; const DEFAULT_MODEL = 'deepseek-chat'; const DEFAULT_TIMEOUT = 30000; const DEFAULT_RETRY = 2; const DEFAULT_INTERVAL = 300; // ========== 配置读写 ========== function getApiKey() { return GM_getValue(STORAGE_API_KEY, ''); } function setApiKey(key) { GM_setValue(STORAGE_API_KEY, key); } function getApiUrl() { return GM_getValue(STORAGE_API_URL, DEFAULT_API_URL); } function setApiUrl(url) { GM_setValue(STORAGE_API_URL, url); } function getModel() { return GM_getValue(STORAGE_MODEL, DEFAULT_MODEL); } function setModel(model) { GM_setValue(STORAGE_MODEL, model); } function getInterval() { return parseInt(GM_getValue(STORAGE_INTERVAL, DEFAULT_INTERVAL), 10); } function setIntervalMs(ms) { GM_setValue(STORAGE_INTERVAL, ms); } function getTimeout() { return parseInt(GM_getValue(STORAGE_TIMEOUT, DEFAULT_TIMEOUT), 10); } function setTimeoutMs(ms) { GM_setValue(STORAGE_TIMEOUT, ms); } function getRetry() { return parseInt(GM_getValue(STORAGE_RETRY, DEFAULT_RETRY), 10); } function setRetry(count) { GM_setValue(STORAGE_RETRY, count); } // ========== 页面检测 ========== const isChapterPage = !!(unsafeWindow.classinfo?.classId && (unsafeWindow.unitList || unsafeWindow.chapterList)); const isDiscussPage = () => !!document.querySelector('a[content_type="Topic"]'); const isExamPage = () => /\/exam\//.test(location.pathname) || /\/quiz\//.test(location.pathname); const hasQuestionData = () => { const q = window.questionList || (unsafeWindow && unsafeWindow.questionList); return q && q.length > 0; }; // ========== 屏蔽无关报错 ========== const originalConsoleError = console.error; console.error = function(...args) { const msg = args.join(' '); if (msg.includes('JSON.parse error') || msg.includes('MEDIA_ERR_SRC_NOT_SUPPORTED') || msg.includes('Failed to load resource')) { return; } originalConsoleError.apply(console, args); }; // ========== 刷视频&页面 ========== async function runProgressOnly() { console.log('%c📚 开始刷视频/页面进度', 'color:#1fb6ff;font-size:16px'); const urlPrefix = location.protocol + '//' + location.host; const classId = unsafeWindow.classinfo.classId; function extractAllChapters(source) { let result = []; if (!source) return result; if (Array.isArray(source)) { source.forEach(item => { if (item.contentType) result.push(item); if (item.itemList) result = result.concat(extractAllChapters(item.itemList)); if (item.chapterList) result = result.concat(extractAllChapters(item.chapterList)); }); } else if (typeof source === 'object') { if (source.contentType) result.push(source); if (source.itemList) result = result.concat(extractAllChapters(source.itemList)); if (source.chapterList) result = result.concat(extractAllChapters(source.chapterList)); } return result; } const dataSource = unsafeWindow.unitList || unsafeWindow.chapterList || unsafeWindow.courseData || unsafeWindow.chapters || unsafeWindow.data; if (!dataSource) { alert('❌ 未找到章节数据,请在课程章节页刷新后再试。'); throw new Error('No chapter data'); } const allChapters = extractAllChapters(dataSource); const videoChapters = allChapters.filter(c => c.contentType === 'Video'); const pageChapters = allChapters.filter(c => ['Page', 'UEditor', 'Html'].includes(c.contentType)); console.log(`📹 视频章节: ${videoChapters.length} 个 | 📄 页面章节: ${pageChapters.length} 个`); const videoPromises = videoChapters.map(chapter => new Promise(resolve => { $.ajax({ url: `${urlPrefix}/class/${classId}/chapter/${chapter.chapterId}/api`, type: 'GET', dataType: 'text', success: result => { let seconds = 0; try { const data = JSON.parse(result); seconds = (data.chapter?.video?.seconds) || 0; } catch (e) {} $.ajax({ url: `${urlPrefix}/log/video/${chapter.chapterId}/${classId}/api`, type: 'POST', dataType: 'text', data: { data: JSON.stringify([{ state: "listening", level: 2, ch: seconds, mh: 0 }]) }, success: () => console.log(`✅ 视频 ${chapter.chapterId} 上报成功`), error: xhr => console.error(`❌ 视频 ${chapter.chapterId} 上报失败 (${xhr.status})`) }).always(resolve); }, error: xhr => { console.error(`❌ 视频 ${chapter.chapterId} 获取失败 (${xhr.status})`); resolve(); } }); })); pageChapters.forEach(chapter => { $.ajax({ url: `${urlPrefix}/class/${classId}/chapter/${chapter.chapterId}/api?${Date.now()}`, type: 'GET', dataType: 'text', success: () => console.log(`🟢 页面 ${chapter.chapterId} 访问成功`), error: xhr => console.warn(`🔴 页面 ${chapter.chapterId} 访问失败 (${xhr.status})`) }); }); await Promise.all(videoPromises); console.log('%c✅ 视频和页面刷取完成', 'color:green;font-size:14px'); } // ========== 讨论区批量回复 ========== async function runDiscussOnly() { console.log('%c💬 开始批量回复讨论', 'color:#1fb6ff;font-size:16px'); function getProcessedTopicIds() { const stored = GM_getValue(PROCESSED_TOPICS_KEY, '[]'); try { return JSON.parse(stored); } catch(e) { return []; } } function markTopicAsProcessed(chapterId) { const ids = getProcessedTopicIds(); if (!ids.includes(chapterId)) { ids.push(chapterId); GM_setValue(PROCESSED_TOPICS_KEY, JSON.stringify(ids)); } } const Editor = { getUEditor() { const UE = unsafeWindow.UE || window.UE; if (UE?.instants) { const inst = Object.values(UE.instants).find(i => i && i.setContent); if (inst) return inst; } if (UE && typeof UE.getEditor === 'function') { const inst = UE.getEditor('ueditor'); if (inst && inst.setContent) return inst; } if (UE && UE.instants) { for (let k in UE.instants) { if (UE.instants[k] && UE.instants[k].setContent) return UE.instants[k]; } } return null; }, setContent(content) { const ue = this.getUEditor(); if (ue) { ue.setContent(content); try { ue.fireEvent('contentChange'); } catch(e) {} return true; } const iframe = document.querySelector('iframe[id^="ueditor_"]'); if (iframe) { try { const doc = iframe.contentDocument || iframe.contentWindow.document; const body = doc.querySelector('body[contenteditable="true"]'); if (body) { body.innerHTML = `

${content.replace(/\n/g, '

')}

`; body.dispatchEvent(new Event('input', { bubbles: true })); return true; } } catch(e) {} } return false; }, getLongestComment() { const texts = []; document.querySelectorAll('.reply-content p').forEach(p => { const t = p.innerText.trim(); if (t) texts.push(t); }); return texts.length ? texts.reduce((a, b) => a.length > b.length ? a : b) : ''; }, hasSubmitted() { const editBtn = document.querySelector('i.post-submit-edit'); return editBtn && editBtn.offsetParent !== null; }, findReplyBtn() { const candidates = document.querySelectorAll('i.post-submit, i, button, .btn'); for (let el of candidates) { const text = el.innerText.trim(); if ((text === '回复' || text.includes('回复')) && !text.includes('编辑')) { if (el.offsetParent !== null && window.getComputedStyle(el).display !== 'none') return el; } } return null; }, enableButton(btn) { if (!btn) return; btn.classList.remove('disabled', 'btn-disabled', 'ban-click'); btn.style.pointerEvents = 'auto'; btn.style.opacity = '1'; btn.disabled = false; } }; const topics = Array.from(document.querySelectorAll('a[content_type="Topic"]')).map(a => ({ chapterId: a.getAttribute('chapter_id'), title: a.querySelector('.title')?.innerText?.trim() || a.innerText.trim() })); if (!topics.length) { alert('未找到任何讨论专题'); return; } const processedIds = getProcessedTopicIds(); const remaining = topics.filter(t => !processedIds.includes(t.chapterId)); console.log(`🎯 总共 ${topics.length} 个专题,已完成 ${processedIds.length} 个,剩余 ${remaining.length} 个`); if (remaining.length === 0) { alert('所有专题已处理完毕!'); return; } for (let i = 0; i < remaining.length; i++) { const t = remaining[i]; console.log(`\n📌 [${i+1}/${remaining.length}] 处理专题: ${t.title}`); location.hash = location.hash.replace(/chapterId=\d+/, `chapterId=${t.chapterId}`); await wait(4000); if (Editor.hasSubmitted()) { console.warn(' ⏭️ 已提交过'); markTopicAsProcessed(t.chapterId); continue; } const comment = Editor.getLongestComment(); if (!comment) { console.warn(' ⚠️ 无评论可复制'); markTopicAsProcessed(t.chapterId); continue; } if (!Editor.setContent(comment)) { console.error(' ❌ 填充失败'); markTopicAsProcessed(t.chapterId); continue; } await wait(1000); const btn = Editor.findReplyBtn(); if (!btn) { console.warn(' ❌ 未找到回复按钮'); markTopicAsProcessed(t.chapterId); continue; } Editor.enableButton(btn); btn.click(); markTopicAsProcessed(t.chapterId); console.log(` ✅ 已回复`); await wait(2000); } console.log('%c🎉 讨论区批量回复完成!', 'color:green;font-size:16px'); alert('讨论区回复完成!'); } // ========== AI 答题 ========== function getQuestionsFromData() { return window.questionList || (unsafeWindow && unsafeWindow.questionList); } function selectOptionByAnswerId(answerId) { if (!answerId) return false; const icon = document.querySelector(`i[answer_id="${answerId}"]`); if (!icon) return false; if (icon.classList.contains('gxb-icon-radio-selected') || icon.classList.contains('gxb-icon-check-selected') || icon.classList.contains('selected')) return true; ['mousedown', 'mouseup', 'click'].forEach(type => { icon.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true })); }); return true; } function requestWithRetry(url, headers, data, timeoutMs, maxRetry) { return new Promise((resolve, reject) => { let attempts = 0; const doRequest = () => { GM_xmlhttpRequest({ method: 'POST', url, headers, data, timeout: timeoutMs, onload: function(resp) { if (resp.status < 200 || resp.status >= 300) { if (attempts < maxRetry) { attempts++; setTimeout(doRequest, 1500); } else reject(new Error(`HTTP ${resp.status}`)); return; } let data; try { data = JSON.parse(resp.responseText); } catch (e) { if (attempts < maxRetry) { attempts++; setTimeout(doRequest, 1500); } else reject(new Error('JSON parse error')); return; } if (!data.choices || !Array.isArray(data.choices) || data.choices.length === 0) { if (attempts < maxRetry) { attempts++; setTimeout(doRequest, 1500); } else reject(new Error('No choices')); return; } resolve(resp); }, onerror: err => { if (attempts < maxRetry) { attempts++; setTimeout(doRequest, 1500); } else reject(err); }, ontimeout: () => { if (attempts < maxRetry) { attempts++; setTimeout(doRequest, 1500); } else reject(new Error('Timeout')); } }); }; doRequest(); }); } async function answerAllQuestionsUI(statusElement) { const apiKey = getApiKey(); if (!apiKey) { alert('请先在 AI 设置中填写 API Key'); return; } const questions = getQuestionsFromData(); if (!questions || questions.length === 0) { alert('没有找到题目数据'); return; } const apiUrl = getApiUrl(), model = getModel(), timeout = getTimeout(), maxRetry = getRetry(), interval = getInterval(); const total = questions.length; let completed = 0; const updateStatus = () => { if (statusElement) statusElement.innerHTML = ` AI 答题中 ${completed}/${total}`; }; updateStatus(); const promises = questions.map(async (q, index) => { const questionText = q.name || q.questionName; const options = q.answerList || []; if (!questionText || options.length === 0) { completed++; updateStatus(); return; } const optionsText = options.map((opt, i) => `${String.fromCharCode(65 + i)}. ${opt.text || opt}`).join('\n'); const prompt = `请回答以下题目,只返回正确答案的字母(如 A, B, C 或 AB):\n题目:${questionText}\n选项:\n${optionsText}`; try { const resp = await requestWithRetry( apiUrl, { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, JSON.stringify({ model, messages: [ { role: 'system', content: '你是一个考试答题助手,只返回正确答案的字母,不要任何解释。' }, { role: 'user', content: prompt } ], temperature: 0.1 }), timeout, maxRetry ); const data = JSON.parse(resp.responseText); const answer = data.choices[0].message.content.trim(); const letters = answer.match(/[A-D]/gi); if (letters) { letters.forEach(letter => { const optIndex = letter.toUpperCase().charCodeAt(0) - 65; if (optIndex < options.length) { const opt = options[optIndex]; if (opt.answerId) selectOptionByAnswerId(opt.answerId); } }); } } catch (e) { console.error(`❌ 第 ${index+1} 题失败:`, e); } finally { completed++; updateStatus(); if (interval > 0) await wait(interval); } }); await Promise.all(promises); if (statusElement) statusElement.innerHTML = ' AI 答题完成'; } // ========== UI ========== function createPanel() { if (document.getElementById('gb-helper-panel')) return; if (!document.getElementById('gb-helper-style')) { const style = document.createElement('style'); style.id = 'gb-helper-style'; style.textContent = ` #gb-helper-panel { position: fixed; bottom: 20px; right: 20px; z-index: 99999; width: 300px; background: #ffffff; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; border: 1px solid #eee; } .gb-header { padding: 14px 16px; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; background: #fff; } .gb-header strong { font-size: 15px; font-weight: 600; color: #333; display: flex; align-items: center; gap: 6px; } .gb-header button { background: transparent; border: none; color: #999; font-size: 18px; cursor: pointer; padding: 0 4px; line-height: 1; } .gb-header button:hover { color: #555; } .gb-divider { height: 3px; background: linear-gradient(90deg, #667eea, #f093fb, #4facfe); margin: 0; } .gb-tabs { display: flex; background: #fafafa; border-bottom: 1px solid #eee; } .gb-tab { flex: 1; text-align: center; padding: 10px 4px; cursor: pointer; font-size: 12px; color: #666; transition: all 0.2s; border-bottom: 2px solid transparent; } .gb-tab.active { color: #333; font-weight: 600; background: #fff; border-bottom-color: #667eea; } .gb-tab-content { display: none; padding: 16px; } .gb-tab-content.active { display: block; } .gb-card { background: #fafafa; border-radius: 8px; padding: 14px; margin-bottom: 10px; cursor: pointer; transition: all 0.2s; border: 1px solid #f0f0f0; } .gb-card:hover:not(.disabled) { background: #f0f0f0; border-color: #ddd; } .gb-card.disabled { opacity: 0.5; cursor: not-allowed; background: #f5f5f5; } .gb-card-title { font-size: 14px; font-weight: 600; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; color: #222; } .gb-card-desc { font-size: 12px; color: #888; } .gb-status { text-align: center; font-size: 12px; color: #888; margin-top: 12px; display: flex; align-items: center; justify-content: center; gap: 6px; } .gb-input-group { margin-bottom: 14px; } .gb-input-group label { display: block; font-size: 12px; font-weight: 500; color: #444; margin-bottom: 4px; } .gb-input-group input { width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; background: #fff; box-sizing: border-box; transition: border-color 0.2s; } .gb-input-group input:focus { border-color: #667eea; outline: none; } .gb-btn { padding: 10px 12px; border: none; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; width: 100%; margin-bottom: 8px; text-align: center; transition: opacity 0.2s; } .gb-btn-primary { background: #667eea; color: white; } .gb-btn-danger { background: #ff6b6b; color: white; } .gb-btn:hover { opacity: 0.9; } .gb-msg { font-size: 12px; margin-top: 8px; text-align: center; } `; document.head.appendChild(style); } const panel = document.createElement('div'); panel.id = 'gb-helper-panel'; // 头部 const header = document.createElement('div'); header.className = 'gb-header'; header.innerHTML = '🚀 高校邦助手'; const minBtn = document.createElement('button'); minBtn.innerHTML = '─'; minBtn.onclick = () => { const body = panel.querySelector('.gb-panel-body'); body.style.display = body.style.display === 'none' ? '' : 'none'; minBtn.innerHTML = body.style.display === 'none' ? '□' : '─'; }; header.appendChild(minBtn); // 拖拽 let isDragging = false, offsetX, offsetY; header.addEventListener('mousedown', (e) => { if (e.target === minBtn) return; isDragging = true; offsetX = e.clientX - panel.offsetLeft; offsetY = e.clientY - panel.offsetTop; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; panel.style.left = `${e.clientX - offsetX}px`; panel.style.top = `${e.clientY - offsetY}px`; panel.style.right = 'auto'; panel.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { isDragging = false; }); // 彩色分隔线 const divider = document.createElement('div'); divider.className = 'gb-divider'; // 标签栏(三个标签) const tabs = document.createElement('div'); tabs.className = 'gb-tabs'; const tab1 = document.createElement('div'); tab1.className = 'gb-tab active'; tab1.textContent = '🛠️ 功能'; const tab2 = document.createElement('div'); tab2.className = 'gb-tab'; tab2.textContent = '⚙️ AI设置'; const tab3 = document.createElement('div'); tab3.className = 'gb-tab'; tab3.textContent = '🔄 重置'; tabs.appendChild(tab1); tabs.appendChild(tab2); tabs.appendChild(tab3); // 内容体 const body = document.createElement('div'); body.className = 'gb-panel-body'; // 功能页(同之前) const featuresTab = document.createElement('div'); featuresTab.className = 'gb-tab-content active'; featuresTab.id = 'gb-features'; const cardProgress = document.createElement('div'); cardProgress.className = 'gb-card'; cardProgress.innerHTML = '
📹 刷视频+页面
自动完成所有视频和图文进度
'; const cardDiscuss = document.createElement('div'); cardDiscuss.className = 'gb-card'; cardDiscuss.innerHTML = '
💬 讨论区回复
批量复制评论并回复所有话题
'; const cardExam = document.createElement('div'); cardExam.className = 'gb-card'; cardExam.innerHTML = '
🤖 AI 答题
调用 DeepSeek 自动作答考试/测验
'; const statusDiv = document.createElement('div'); statusDiv.className = 'gb-status'; statusDiv.innerHTML = ' 就绪'; featuresTab.appendChild(cardProgress); featuresTab.appendChild(cardDiscuss); featuresTab.appendChild(cardExam); featuresTab.appendChild(statusDiv); // AI设置页(仅API配置) const settingsTab = document.createElement('div'); settingsTab.className = 'gb-tab-content'; settingsTab.id = 'gb-settings'; const apiKey = getApiKey(), apiUrl = getApiUrl(), model = getModel(); const timeout = getTimeout(), retry = getRetry(), interval = getInterval(); settingsTab.innerHTML = `
从 platform.deepseek.com 获取
`; // 重置页(单独按钮) const resetTab = document.createElement('div'); resetTab.className = 'gb-tab-content'; resetTab.id = 'gb-reset'; resetTab.innerHTML = `

如果脚本卡住、状态异常,可以重置后刷新页面重新开始。

`; body.appendChild(featuresTab); body.appendChild(settingsTab); body.appendChild(resetTab); panel.appendChild(header); panel.appendChild(divider); panel.appendChild(tabs); panel.appendChild(body); document.body.appendChild(panel); // 标签切换逻辑 const allTabs = [tab1, tab2, tab3]; const allContents = [featuresTab, settingsTab, resetTab]; function switchTab(index) { allTabs.forEach((t, i) => t.classList.toggle('active', i === index)); allContents.forEach((c, i) => c.classList.toggle('active', i === index)); } tab1.addEventListener('click', () => switchTab(0)); tab2.addEventListener('click', () => switchTab(1)); tab3.addEventListener('click', () => switchTab(2)); // 按钮状态更新 function updateCardStates() { cardProgress.classList.toggle('disabled', !isChapterPage); cardDiscuss.classList.toggle('disabled', !isDiscussPage()); cardExam.classList.toggle('disabled', !(isExamPage() && hasQuestionData())); cardProgress.title = isChapterPage ? '' : '请在课程章节页使用'; cardDiscuss.title = isDiscussPage() ? '' : '请在讨论区页面使用'; cardExam.title = (isExamPage() && hasQuestionData()) ? '' : '请在考试/测验页使用'; } updateCardStates(); setInterval(updateCardStates, 2000); // 功能点击 cardProgress.addEventListener('click', async () => { if (!isChapterPage) return; statusDiv.innerHTML = ' 正在处理...'; try { await runProgressOnly(); statusDiv.innerHTML = ' 刷视频完成,微信:windows557'; } catch (e) { statusDiv.innerHTML = ' 操作失败'; } setTimeout(() => statusDiv.innerHTML = ' 就绪', 4000); }); cardDiscuss.addEventListener('click', async () => { if (!isDiscussPage()) return; statusDiv.innerHTML = ' 正在批量回复...'; await runDiscussOnly(); statusDiv.innerHTML = ' 讨论回复完成,微信:windows557'; setTimeout(() => statusDiv.innerHTML = ' 就绪', 3000); }); cardExam.addEventListener('click', async () => { if (!isExamPage() || !hasQuestionData()) return; if (!getApiKey()) { switchTab(1); document.getElementById('settings-msg').innerText = '请先填写 API Key'; return; } statusDiv.innerHTML = ' AI 正在答题...'; await answerAllQuestionsUI(statusDiv); }); // 保存AI设置 document.getElementById('save-ai-settings').addEventListener('click', () => { const newKey = document.getElementById('ai-api-key-input').value.trim(); if (!newKey) { document.getElementById('settings-msg').innerText = '❌ API Key 不能为空'; return; } setApiKey(newKey); setApiUrl(document.getElementById('ai-api-url-input').value.trim() || DEFAULT_API_URL); setModel(document.getElementById('ai-model-input').value.trim() || DEFAULT_MODEL); setTimeoutMs(document.getElementById('ai-timeout-input').value || DEFAULT_TIMEOUT); setRetry(document.getElementById('ai-retry-input').value || DEFAULT_RETRY); setIntervalMs(document.getElementById('ai-interval-input').value || DEFAULT_INTERVAL); document.getElementById('settings-msg').innerText = '✅ 配置已保存,部分功能需刷新页面生效'; setTimeout(() => location.reload(), 1500); }); // 重置状态 document.getElementById('reset-all-states').addEventListener('click', () => { GM_deleteValue(STATE_KEY); GM_deleteValue(PROCESSED_TOPICS_KEY); document.getElementById('reset-msg').innerText = '✅ 状态已清除,即将刷新...'; setTimeout(() => location.reload(), 800); }); } // 启动 window.addEventListener('load', () => setTimeout(createPanel, 800)); GM_registerMenuCommand('🤖 显示高校邦助手', () => { const panel = document.getElementById('gb-helper-panel'); if (panel) panel.style.display = 'block'; else createPanel(); }); })();