您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
V1.2: 新增“答案模式”选择器,包括专为简答/论述题优化的“拓展论述”模式,解决答案过短问题!集成更强大的UEditor粘贴解锁方案,解决部分作业无法粘贴的问题。感谢用户反馈!
// ==UserScript== // @name 超星学习通+自定义AI答题脚本 (SiliconFlow完美融合版) // @namespace http://tampermonkey.net/ // @version 1.2 // @description V1.2: 新增“答案模式”选择器,包括专为简答/论述题优化的“拓展论述”模式,解决答案过短问题!集成更强大的UEditor粘贴解锁方案,解决部分作业无法粘贴的问题。感谢用户反馈! // @author Weinan (Modified by Gemini) // @license GPL-3.0-or-later // @match *://*.chaoxing.com/exam-ans/mooc2/exam/preview* // @match *://*.chaoxing.com/mooc2/work/dowork* // @match *://*.chaoxing.com/mycourse/studentstudy* // @match *://*.chaoxing.com/mooc-ans/mooc2/work/dowork* // @icon https://i.miji.bid/2025/06/13/cdf1843af3a8dab9804bd8a53e11f092.png // @grant unsafeWindow // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @connect api.siliconflow.cn // @run-at document-end // ==/UserScript== (function() { 'use strict'; // --- CSS样式 (适配新控件) --- GM_addStyle(` #ai-helper-window { position: fixed; top: 20px; left: 20px; width: 520px; height: 550px; /* 增加高度 */ background-color: #ffffff; border: 1px solid #e2e8f0; border-radius: 8px; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); z-index: 99999; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; resize: both; min-width: 320px; min-height: 350px; display: flex; flex-direction: column; } #ai-helper-window .header { background-color: #2d3748; color: white; padding: 8px 12px; font-size: 14px; font-weight: 600; cursor: move; display: flex; justify-content: space-between; align-items: center; } #ai-helper-window .header #ai-helper-hide-btn { background: transparent; color: white; font-size: 24px; font-weight: bold; border: none; padding: 0 8px; cursor: pointer; line-height: 1; } #ai-helper-window .header #ai-helper-hide-btn:hover { color: #cbd5e1; } #ai-helper-window .content-wrapper { padding: 12px; display: flex; flex-direction: column; overflow: hidden; flex-grow: 1; } #ai-helper-window .question-section, #ai-helper-window .answer-section { border: 1px solid #e2e8f0; padding: 12px; border-radius: 4px; margin-bottom: 8px; overflow-y: auto; word-break: break-word; } #ai-helper-window .answer-section { flex: 1; } #ai-helper-window .controls-area { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; } #ai-helper-window .button-row, .api-key-row, .model-select-row, .settings-row { display: flex; gap: 8px; align-items: center; } #ai-helper-window button { padding: 6px 12px; border-radius: 4px; font-size: 12px; cursor: pointer; border: none; background: #e2e8f0; transition: background-color 0.2s; display: flex; align-items: center; justify-content: center; } #ai-helper-window button:hover:not(:disabled) { background: #cbd5e1; } #ai-helper-window button:disabled { cursor: not-allowed; background-color: #f1f5f9; color: #94a3b8; } #ai-helper-window button#ai-helper-refresh-btn { background: #3b82f6; color: white; flex-grow: 1; } #ai-helper-window button#ai-helper-refresh-btn:hover:not(:disabled) { background: #2563eb; } #ai-helper-window button#ai-helper-get-all-btn { background: #8b5cf6; color: white; flex-grow: 1; } #ai-helper-window button#ai-helper-get-all-btn:hover:not(:disabled) { background: #7c3aed; } #ai-helper-window button#ai-helper-copy-btn { background: #10b981; color: white; } #ai-helper-window button#ai-helper-copy-btn:hover { background: #059669; } #ai-helper-window .api-key-row input, #ai-helper-model-select, #ai-helper-answer-mode-select { flex: 1; padding: 6px 8px; border-radius: 4px; font-size: 12px; border: 1px solid #e2e8f0; } #ai-helper-window .api-key-row button { white-space: nowrap; } #ai-helper-window button svg { margin-right: 4px; } #ai-helper-window .features-section { display: flex; align-items: center; gap: 15px; padding: 8px 4px; font-size: 12px; } .features-section .feature-toggle { display: flex; align-items: center; gap: 4px; } .features-section .feature-toggle input { margin-right: 4px; } #ai-helper-window .status-info-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 4px 0px; border-top: 1px solid #e2e8f0; margin-top: 5px; font-size: 12px; color: #64748b; } #ai-helper-floating-button { position: fixed; top: 20px; left: -48px; width: 55px; height: 50px; background-color: #2d3748; color: white; border-radius: 0 25px 25px 0; text-align: right; padding-right: 12px; line-height: 50px; cursor: pointer; transition: all 0.3s ease; z-index: 99998; font-size: 14px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } #ai-helper-floating-button:hover { left: 0; } .label { color: #64748b; font-size: 12px; margin-bottom: 4px; } `); // --- HTML结构 (更新为“答案模式”选择器) --- const windowHTML = ` <div id="ai-helper-window"> <div class="header"> <span>AI答题 (Alt+Z 快速隐藏)</span> <button id="ai-helper-hide-btn" title="关闭">×</button> </div> <div class="content-wrapper"> <div class="question-section"> <div class="label">当前题目 (双击可编辑):</div> <div id="ai-helper-question">正在获取题目...</div> </div> <div class="answer-section"> <div class="label">AI答案:</div> <div id="ai-helper-answer"></div> </div> <div class="controls-area"> <div class="button-row"> <button id="ai-helper-prev-btn" title="上一题"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M15 18l-6-6 6-6" /></svg></button> <button id="ai-helper-next-btn" title="下一题"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M9 18l6-6-6-6" /></svg></button> <button id="ai-helper-refresh-btn"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 11-9-9 9 9 0 019 9z" /><path d="M9 12l2 2 4-4" /></svg>获取答案</button> <button id="ai-helper-get-all-btn"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M3 6h18M3 12h18M3 18h18"/></svg>一键获取全部</button> <button id="ai-helper-copy-btn"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" /><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" /></svg>复制答案</button> </div> <div class="settings-row"> <select id="ai-helper-model-select" title="选择AI模型"></select> <select id="ai-helper-answer-mode-select" title="选择答案生成模式"> <option value="simple">简洁答案 (选择/填空)</option> <option value="detailed">详细解析 (选择题带过程)</option> <option value="essay">拓展论述 (简答/论述)</option> </select> </div> <div class="api-key-row"> <input type="password" id="ai-helper-api-key-input" placeholder="请输入SiliconFlow API Key"> <button id="ai-helper-save-api-key-btn">保存API Key</button> </div> <div class="features-section"> <span>其他功能:</span> <label class="feature-toggle"><input type="checkbox" id="ai-helper-paste-toggle">允许粘贴</label> <label class="feature-toggle"><input type="checkbox" id="ai-helper-copy-toggle">光标处复制</label> <label class="feature-toggle"><input type="checkbox" id="ai-helper-watermark-toggle">移除水印</label> </div> <div class="status-info-row"> <div class="status" id="ai-helper-status-text">请先配置API Key</div> <div class="info">by: Weinan & Gemini</div> </div> </div> </div> </div> <div id="ai-helper-floating-button" style="display: none;">显示</div> `; document.body.insertAdjacentHTML('beforeend', windowHTML); // --- 脚本逻辑 --- const getElem = (id) => document.getElementById(id); // --- 常量与状态变量 --- const SILICONFLOW_API_ENDPOINT = "https://api.siliconflow.cn/v1/chat/completions"; const MODELS = [ { id: "deepseek-ai/DeepSeek-R1-0528-Qwen3-8B", name: "DeepSeek R1 (推荐)" }, { id: "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", name: "DeepSeek R1 Distill 7B" }, { id: "alibaba/Qwen2-7B-Instruct", name: "Qwen2 7B" }, { id: "01-ai/Yi-1.5-9B-Chat", name: "Yi 1.5 9B" }, { id: "meta-llama/Meta-Llama-3.1-8B-Instruct", name: "Llama 3.1 8B" }, { id: "google/gemma-2-9b-it", name: "Gemma 2 9B" }, { id: "Qwen/Qwen3-8B", name: "Qwen3 8B (New)" }, { id: "THUDM/glm-4-9b-chat", name: "GLM-4 9B Chat" }, { id: "THUDM/GLM-Z1-9B-0414", name: "GLM-Z1 9B" }, ]; let answerCache = {}; let currentIndex = 0; let timu = ["正在获取题目..."]; let questionsFetched = false; let isFetchingAll = false; // --- UI元素引用 --- const myWindow = getElem('ai-helper-window'); const floatingButton = getElem('ai-helper-floating-button'); const questionEl = getElem('ai-helper-question'); const answerEl = getElem('ai-helper-answer'); const apiKeyInput = getElem('ai-helper-api-key-input'); const statusText = getElem('ai-helper-status-text'); const modelSelect = getElem('ai-helper-model-select'); const answerModeSelect = getElem('ai-helper-answer-mode-select'); const refreshBtn = getElem('ai-helper-refresh-btn'); const getAllBtn = getElem('ai-helper-get-all-btn'); // --- 窗口拖动与显隐 --- let isDragging = false, initialX, initialY, xOffset = 0, yOffset = 0; myWindow.querySelector('.header').addEventListener('mousedown', (e) => { if (e.target.tagName !== 'BUTTON') { isDragging = true; initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; } }); document.addEventListener('mousemove', (e) => { if (isDragging) { e.preventDefault(); xOffset = e.clientX - initialX; yOffset = e.clientY - initialY; myWindow.style.transform = `translate(${xOffset}px, ${yOffset}px)`; } }); document.addEventListener('mouseup', () => { isDragging = false; }); const toggleWindow = () => { const isHidden = myWindow.style.display === 'none'; myWindow.style.display = isHidden ? 'flex' : 'none'; floatingButton.style.display = isHidden ? 'none' : 'block'; }; getElem('ai-helper-hide-btn').addEventListener('click', toggleWindow); floatingButton.addEventListener('click', toggleWindow); document.addEventListener('keydown', e => { if (e.altKey && e.key.toLowerCase() === 'z') toggleWindow(); }); // --- 核心API请求函数 (重构以支持不同答案模式) --- async function callSiliconFlowAPI(question, modelId) { return new Promise((resolve, reject) => { const apiKey = GM_getValue('siliconflow_api_key'); if (!apiKey) { return reject(new Error('错误:请先输入并保存API Key。')); } const answerMode = answerModeSelect.value; let systemPrompt = ""; let maxTokens = 1024; switch(answerMode) { case 'essay': systemPrompt = "你是一位专业的学术助手。针对用户提出的简答题或论述题,请提供一个全面、结构清晰、内容详实的回答。请确保回答逻辑连贯,覆盖问题的所有关键点,并以清晰的段落形式呈现。请根据问题的要求进行充分的展开论述,而不仅仅是简单的概括。"; maxTokens = 2048; break; case 'detailed': systemPrompt = "你是一个详尽的答题助手。请根据用户的问题,提供详细的、带有解析过程的答案。如果是选择题,请先给出正确选项,然后换行详细解释为什么选这个选项,以及为什么其他选项是错误的。"; maxTokens = 1024; break; case 'simple': default: systemPrompt = "你是一个答题助手,请根据用户提供的问题,直接、简洁地给出最可能的答案。如果是选择题,请直接给出选项字母(例如:A)。如果是填空题,请给出填空的内容。如果是判断题,请给出“对”或“错”。不要解释,不要有多余的文字。"; maxTokens = 256; break; } GM_xmlhttpRequest({ method: 'POST', url: SILICONFLOW_API_ENDPOINT, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ "model": modelId, "messages": [{ "role": "system", "content": systemPrompt }, { "role": "user", "content": question }], "max_tokens": maxTokens, }), onload: (response) => { if (response.status >= 200 && response.status < 300) { try { const result = JSON.parse(response.responseText); const answer = result.choices[0]?.message?.content?.trim(); if (answer) { resolve(answer); } else { reject(new Error("API返回内容为空。")); } } catch (e) { reject(new Error(`解析API响应失败: ${e.message}`)); } } else { try { const errorData = JSON.parse(response.responseText); reject(new Error(`(状态码: ${response.status}): ${errorData.error?.message || response.statusText}`)); } catch (e) { reject(new Error(`(状态码: ${response.status}): ${response.statusText}`)); } } }, onerror: (err) => reject(new Error('网络错误,无法连接到API服务器。')), ontimeout: () => reject(new Error('请求超时。')) }); }); } // --- 功能实现 --- async function fetchAnswerForCurrent() { if (!questionsFetched) { answerEl.textContent = "请等待题目加载完成。"; return; } const currentQuestion = timu[currentIndex]; answerEl.textContent = "AI思考中,请稍候..."; statusText.textContent = `正在请求 ${modelSelect.options[modelSelect.selectedIndex].text}...`; refreshBtn.disabled = true; try { const answer = await callSiliconFlowAPI(currentQuestion, modelSelect.value); answerCache[currentIndex] = answer; answerEl.textContent = answer; statusText.textContent = "答案获取成功!"; } catch (error) { answerEl.innerHTML = `<span style="color: red;">${error.message}</span>`; statusText.textContent = "获取失败"; } finally { refreshBtn.disabled = false; } } async function fetchAllAnswers() { if (isFetchingAll) return; isFetchingAll = true; getAllBtn.disabled = true; refreshBtn.disabled = true; const total = timu.length; for (let i = 0; i < total; i++) { statusText.textContent = `正在获取第 ${i + 1} / ${total} 题...`; if (answerCache[i]) { if (i === currentIndex) updateDisplay(currentIndex); continue; } try { const answer = await callSiliconFlowAPI(timu[i], modelSelect.value); answerCache[i] = answer; if (i === currentIndex) { answerEl.textContent = answer; } } catch (error) { answerCache[i] = `获取失败: ${error.message}`; if (i === currentIndex) { answerEl.innerHTML = `<span style="color: red;">${answerCache[i]}</span>`; } console.error(`获取第 ${i + 1} 题答案失败:`, error); } await new Promise(resolve => setTimeout(resolve, 200)); } isFetchingAll = false; getAllBtn.disabled = false; refreshBtn.disabled = false; statusText.textContent = `全部 ${total} 题答案已获取完毕!`; updateDisplay(currentIndex); } // --- 题目内容获取与显示 --- function updateDisplay(index) { questionEl.textContent = timu[index]; answerEl.textContent = answerCache[index] || '点击 "获取答案" 或 "一键获取全部"'; statusText.textContent = `当前: ${index + 1} / ${timu.length}`; } function navigateQuestion(direction) { const newIndex = currentIndex + direction; if (newIndex >= 0 && newIndex < timu.length) { currentIndex = newIndex; updateDisplay(currentIndex); } } questionEl.addEventListener("dblclick", () => { const editableDiv = document.createElement('div'); editableDiv.setAttribute('contenteditable', 'true'); editableDiv.textContent = questionEl.textContent; editableDiv.style.cssText = 'background: #fff; padding: 4px; border: 1px solid #3b82f6; border-radius: 4px; min-height: 50px;'; questionEl.replaceWith(editableDiv); editableDiv.focus(); const saveEdit = () => { timu[currentIndex] = editableDiv.textContent.trim(); questionEl.textContent = timu[currentIndex]; editableDiv.replaceWith(questionEl); }; editableDiv.addEventListener('blur', saveEdit); editableDiv.addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); saveEdit(); } }); }); // --- 页面题目抓取逻辑 --- function getQuestionsFromPage() { console.log("AI助手: 开始查找题目..."); let attempts = 0; const intervalId = setInterval(() => { if (questionsFetched || attempts++ > 10) { clearInterval(intervalId); if (!questionsFetched) console.log("AI助手: 查找超时,未找到题目。"); return; } let doc = document, elements = null; const selectors = ['.questionLi', '.TiMu', '.padBom50']; const findInDoc = (d) => { for (const s of selectors) { const els = d.querySelectorAll(s); if (els.length > 0) return els; } return null; }; try { elements = findInDoc(doc); const iframe = doc.getElementById('iframe'); if (!elements && iframe) { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; elements = findInDoc(iframeDoc); const secondIframe = iframeDoc.querySelector('iframe'); if (!elements && secondIframe) { elements = findInDoc(secondIframe.contentDocument || secondIframe.contentWindow.document); } } } catch (e) { /* ignore */ } if (elements && elements.length > 0) { const fetchedTexts = Array.from(elements).map(el => { let text = el.innerText.trim(); if (!text) return null; text = text.replace(/(\s*\r?\n\s*)+/g, ' ').replace(/\s+/g, ' '); text = text.replace(/^[A-Z]\s?[、.]\s?/, ''); // 移除题号 return text.trim(); }).filter(Boolean); if (fetchedTexts.length > 0) { console.log(`AI助手: 成功获取 ${fetchedTexts.length} 道题目。`); questionsFetched = true; timu = fetchedTexts; currentIndex = 0; answerCache = {}; updateDisplay(currentIndex); clearInterval(intervalId); } } }, 500); } // --- 初始化与事件绑定 --- function initialize() { // API Key const savedKey = GM_getValue('siliconflow_api_key'); if(savedKey) { apiKeyInput.value = savedKey; statusText.textContent = "准备就绪"; } getElem('ai-helper-save-api-key-btn').addEventListener('click', () => { const key = apiKeyInput.value.trim(); if (key) { GM_setValue('siliconflow_api_key', key); statusText.textContent = "API Key 已保存!"; apiKeyInput.type = 'password'; } }); apiKeyInput.addEventListener('click', () => { apiKeyInput.type = 'text'; }); // 按钮事件 getElem('ai-helper-prev-btn').addEventListener('click', () => navigateQuestion(-1)); getElem('ai-helper-next-btn').addEventListener('click', () => navigateQuestion(1)); refreshBtn.addEventListener('click', fetchAnswerForCurrent); getAllBtn.addEventListener('click', fetchAllAnswers); getElem('ai-helper-copy-btn').addEventListener('click', () => { navigator.clipboard.writeText(answerEl.innerText).then(() => { statusText.textContent = "答案已复制!"; }); }); // 模型选择器初始化 const savedModel = GM_getValue('selected_model', MODELS[0].id); MODELS.forEach(model => { const option = document.createElement('option'); option.value = model.id; option.textContent = model.name; modelSelect.appendChild(option); }); modelSelect.value = savedModel; modelSelect.addEventListener('change', (e) => { GM_setValue('selected_model', e.target.value); statusText.textContent = "模型已切换并保存。"; }); // 答案模式选择器初始化 const savedAnswerMode = GM_getValue('answer_mode', 'simple'); answerModeSelect.value = savedAnswerMode; answerModeSelect.addEventListener('change', (e) => { GM_setValue('answer_mode', e.target.value); statusText.textContent = "答案模式已切换。"; }); // 其他功能开关 const features = { paste: { el: getElem('ai-helper-paste-toggle'), def: true, cb: (v) => { if (v) { // 使用setTimeout确保UEditor已经完全加载 setTimeout(enableAdvancedPaste, 1500); } } }, copy: { el: getElem('ai-helper-copy-toggle'), def: true, cb: (v) => v ? enableCopyFeature() : disableCopyFeature() }, watermark: { el: getElem('ai-helper-watermark-toggle'), def: true, cb: (v) => v ? removeWatermarks() : restoreWatermarks() } }; for (const key in features) { const feature = features[key]; const savedValue = GM_getValue(`feature_${key}`, feature.def); feature.el.checked = savedValue; if (feature.cb) feature.cb(savedValue); feature.el.addEventListener('change', (e) => { const isChecked = e.target.checked; GM_setValue(`feature_${key}`, isChecked); if (feature.cb) feature.cb(isChecked); }); } // 启动题目抓取 getQuestionsFromPage(); } // --- 附加功能 (复制、水印、粘贴等) --- /** * 【新】使用用户提供的方法解锁UEditor的粘贴功能 * 这种方法更具体,直接操作编辑器实例,效果更好 */ function enableAdvancedPaste() { try { // 检查页面是否加载了jQuery和UEditor // `unsafeWindow`用于从沙箱中访问页面级别的变量 const $ = unsafeWindow.jQuery; const UE = unsafeWindow.UE; const editorPaste = unsafeWindow.editorPaste; if (typeof $ === 'function' && typeof UE !== 'undefined' && UE.instants) { console.log("AI助手: 检测到UEditor,执行高级粘贴解锁..."); // 移除选择限制 $("body").removeAttr("onselectstart"); $("html").css("user-select", "unset"); // 遍历所有UEditor实例并移除粘贴限制 Object.entries(UE.instants).forEach(item => { const editor = item[1]; // item[1]是编辑器实例 if (editor) { editor.options.disablePasteImage = false; // 允许粘贴图片 // 移除'beforepaste'事件的监听器,这里的`editorPaste`是学习通页面定义的一个函数 editor.removeListener('beforepaste', editorPaste); } }); console.log("AI助手: UEditor粘贴限制已成功移除。"); statusText.textContent = "粘贴功能已解锁。"; } else { console.log("AI助手: 未检测到UEditor,跳过高级粘贴解锁。"); } } catch (e) { console.error("AI助手: 解锁粘贴时发生错误。这可能是因为页面结构已更改或`editorPaste`函数不存在。错误信息:", e); statusText.textContent = "粘贴解锁失败(见控制台)。"; } } let copyFeatureEnabled = false; let mouseMoveListener = null, keydownListener = null; function enableCopyFeature() { if (copyFeatureEnabled) return; let mouseX = 0, mouseY = 0; mouseMoveListener = (e) => { mouseX = e.clientX; mouseY = e.clientY; }; keydownListener = (e) => { if (e.ctrlKey && e.key === 'c') { let el = document.elementFromPoint(mouseX, mouseY); if (el) navigator.clipboard.writeText(el.innerText); } }; document.addEventListener('mousemove', mouseMoveListener); document.addEventListener('keydown', keydownListener); copyFeatureEnabled = true; } function disableCopyFeature() { if (!copyFeatureEnabled) return; if (mouseMoveListener) document.removeEventListener('mousemove', mouseMoveListener); if (keydownListener) document.removeEventListener('keydown', keydownListener); copyFeatureEnabled = false; } function removeWatermarks() { document.querySelectorAll('.mask_div, .yd_j_water_mark').forEach(w => w.style.display = 'none'); } function restoreWatermarks() { document.querySelectorAll('.mask_div, .yd_j_water_mark').forEach(w => w.style.display = ''); } // --- 脚本启动 --- window.addEventListener('load', initialize); })();