// ==UserScript== // @name OCS DeepSeek 自动答题助手 v2 // @namespace https://github.com/ocs-answer // @version 2.0.0 // @description 【改进版】自动识别网课考试题目,调用DeepSeek推理后自动勾选答案,支持单选/多选/判断 // @author OCS Helper // @license MIT // // ========== 匹配规则 ========== // @match https://*.chaoxing.com/* // @match https://*.xuexi.com/* // @match https://*.zhihuishu.com/* // @match https://*.icve.com.cn/* // @match https://*.ivew.cn/* // @match https://*.icourse163.org/* // @match https://*/* // // ========== 权限 ========== // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant unsafeWindow // // @run-at document-end // ==/UserScript== (function () { "use strict"; // ==================================================================== // 1. 配置 // ==================================================================== const CFG = { API_KEY: GM_getValue("ds_api_key", ""), API_URL: "https://api.deepseek.com/v1/chat/completions", MODEL: "deepseek-chat", // "auto"=自动勾选 "semi"=仅显示答案 MODE: GM_getValue("ds_mode", "auto"), ANSWER_GAP: 1200, // 每道题间隔ms }; // ==================================================================== // 2. 状态 // ==================================================================== let running = false; let stopFlag = false; let qCount = 0, okCount = 0, failCount = 0; // ==================================================================== // 3. ★★★ 核心改进:精准的选项查找 + 自动勾选 ★★★ // ==================================================================== // 扫描整个页面,找出所有 选项字母 ↔ input元素 的映射 function scanOptions() { const map = {}; // { "A": { input, labelText, el }, "B": {...} } const inputs = document.querySelectorAll("input[type='radio'], input[type='checkbox']"); if (inputs.length === 0) return map; inputs.forEach((inp, idx) => { // 找关联的 label let labelEl = null; let labelText = ""; // 方式1: label[for=id] if (inp.id) { labelEl = document.querySelector(`label[for="${CSS.escape(inp.id)}"]`); } // 方式2: 父级 label if (!labelEl) { labelEl = inp.closest("label"); } // 方式3: 父级容器内的文本(li、div.option 等) if (!labelEl) { const parent = inp.closest("li, .option, .option-item, .answer-item, [class*='option'], [class*='choice'], td, div"); if (parent) labelEl = parent; } // 方式4: 紧跟在 input 后面的文本节点或 span if (!labelEl) { const next = inp.nextElementSibling; if (next && /^(span|label|b|i|em|text)/i.test(next.tagName)) { labelEl = next; } } if (labelEl) { labelText = labelEl.textContent.replace(/\s+/g, " ").trim(); } else { // 纯靠周围文本 labelText = inp.parentElement?.textContent?.replace(/\s+/g, " ").trim() || `选项${idx}`; } // 从文本中提取选项字母 const letter = extractLetter(labelText, idx); if (letter) { map[letter] = { input: inp, labelText, el: labelEl }; } else { // 找不到字母就按索引存 map[String(idx)] = { input: inp, labelText, el: labelEl }; } }); return map; } // 从文本中提取 A B C D 字母 function extractLetter(text, fallbackIdx) { // 匹配 "A." "A、" "A." "A)" "A )" 开头 const m = text.match(/^\s*([A-Da-d])[.、.))\s]/); if (m) return m[1].toUpperCase(); // 匹配 "A" 单独成词 const m2 = text.match(/\b([A-Da-d])\b/); if (m2) return m2[1].toUpperCase(); return null; } // ★ 核心:勾选指定字母的选项 ★ function selectOption(letter, optMap) { const entry = optMap[letter.toUpperCase()]; if (!entry) return false; const { input } = entry; // 对 radio:直接设置 if (input.type === "radio") { // 先取消同name的其他选中项(模拟真实点击行为) const siblings = document.querySelectorAll(`input[name="${CSS.escape(input.name)}"]`); siblings.forEach(s => { if (s !== input) s.checked = false; }); } // 对 checkbox:如果已经选了就别重复点 if (input.type === "checkbox" && input.checked) return true; // 勾选 + 派发事件(很多平台靠事件监听更新UI) input.checked = true; input.dispatchEvent(new Event("click", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true })); input.dispatchEvent(new Event("input", { bubbles: true })); // 如果 label 元素存在且可点击,也点一下(某些框架需要) if (entry.el && entry.el !== input && entry.el.tagName !== "INPUT") { try { entry.el.click(); } catch (e) { /* ignore */ } } return true; } // ★ 多选:逐个勾选 ★ function selectMultiOptions(answerLetters, optMap) { for (const ch of answerLetters.toUpperCase()) { if (/[A-D]/.test(ch)) { selectOption(ch, optMap); // 两个勾选之间留一点间隔 } } return true; } // ★ 判断题专用:找"正确""错误"按钮 ★ function selectJudge(answer, optMap) { // 答案可能是"正确""错误""对""错""√""×" const isRight = /正确|对|√|T|TRUE/i.test(answer); const targetText = isRight ? /正确|对|√/i : /错误|错|×/i; // 在所有选项中找目标文本 for (const [letter, entry] of Object.entries(optMap)) { if (targetText.test(entry.labelText)) { return selectOption(letter, optMap); } } // 兜底:在页面上找包含文字的任意可点击元素 const allEls = document.querySelectorAll("span, label, div, button, a"); const kw = isRight ? ["正确", "对", "√"] : ["错误", "错", "×"]; for (const el of allEls) { const t = el.textContent.trim(); if (kw.some(k => t === k || t.startsWith(k) || t.endsWith(k))) { el.click(); return true; } } return false; } // ==================================================================== // 4. ★★★ 改进:用 XPath 精准找"下一题" ★★★ // ==================================================================== function clickNextOrSubmit() { // 优先找"下一题" 或 "下一题(Enter)" const texts = ["下一题", "下一", "下一题 (Enter)", "下一题(Enter)", "保存并下一题"]; for (const t of texts) { const xpath = `//*[contains(text(), '${t}') and not(contains(@style, 'display:none')) and not(contains(@class, 'hidden'))]`; const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); if (result.singleNodeValue) { const btn = result.singleNodeValue; btn.click(); return "next"; } } // 找"提交"按钮(最后一题) const submitTexts = ["提交", "交卷", "提交试卷", "提交答案", "完成"]; for (const t of submitTexts) { const xpath = `//*[contains(text(), '${t}') and not(contains(@style, 'display:none'))]`; const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); if (result.singleNodeValue) { result.singleNodeValue.click(); return "submit"; } } // 兜底:找任何带 next/continue 的按钮 const fallbacks = document.querySelectorAll( "[class*='next'], [class*='Next'], [class*='continue'], [class*='Continue'], " + "[class*='forward'], .btn-next, .next-btn" ); for (const fb of fallbacks) { if (fb.offsetParent !== null) { // 可见 fb.click(); return "next"; } } return "none"; } // ==================================================================== // 5. ★★★ 改进:精准的题目提取 ★★★ // ==================================================================== function extractQuestion() { let question = ""; const options = []; // 保留完整文本 "A. xxx" const OPTION_RE = /^\s*([A-Da-d])[.、.))\s]/; let qType = "unknown"; // ----- 找个容器 ----- // 先看有没有明显的 exam/question 容器 const containers = document.querySelectorAll( ".questionLi, .question, .exam-question, .ti, [class*='questionItem'], " + ".ZyExamTit, .CyMainTit, .topic-item, .testpaper-item, " + "li[class*='question'], div[class*='question'], " + ".exam-content, .test-content" ); let container = null; for (const c of containers) { const text = c.textContent.trim(); if (text.length > 10 && text.length < 3000) { container = c; break; } } if (!container) container = document.body; // 兜底 // 从容器中提取题目文字:取第一个有问号/冒号的段落 const paragraphs = container.querySelectorAll("p, div, span, h1, h2, h3, h4, h5"); for (const p of paragraphs) { const t = p.textContent.trim(); if (t.length > 5 && !OPTION_RE.test(t) && /[??::]/.test(t)) { question = t; break; } } // 如果还没找到,取容器文本第一段 if (!question) { const allText = container.textContent.trim(); // 按行切,取第一行 const lines = allText.split("\n").filter(l => l.trim()); for (const line of lines) { if (line.trim().length > 5 && !OPTION_RE.test(line.trim())) { question = line.trim(); break; } } } if (!question) { question = container.textContent.trim().slice(0, 300); } // ----- 提取选项(保留字母前缀)----- // 先尝试用 scanOptions 得到的 input + labelText const optMap = scanOptions(); if (Object.keys(optMap).length >= 2) { for (const [letter, entry] of Object.entries(optMap)) { // 如果 labelText 以字母开头就直接用,否则拼上字母 if (OPTION_RE.test(entry.labelText)) { options.push(entry.labelText); } else { options.push(`${letter}. ${entry.labelText}`); } } // 按字母排序 options.sort((a, b) => a.localeCompare(b)); } // 如果没有 input 方式找到,回退到纯文本扫描 if (options.length < 2) { options.length = 0; const allEls = container.querySelectorAll("li, .option-item, [class*='option'], [class*='choice'], label, .radio-item"); const seen = new Set(); allEls.forEach(el => { const t = el.textContent.trim(); if (t.length > 0 && t.length < 300 && OPTION_RE.test(t) && !seen.has(t)) { seen.add(t); options.push(t); } }); options.sort((a, b) => a.localeCompare(b)); } // ----- 题型检测 ----- const fullText = container.textContent; if (/多选题|(多选)|\(多选\)|多项选择/i.test(fullText)) qType = "multi"; else if (/单选题|(单选)|\(单选\)|单项选择/i.test(fullText)) qType = "single"; else if (/判断题/i.test(fullText)) qType = "judge"; else if (options.length >= 2 && !/填空/i.test(fullText)) { // 有选项就是选择类,多数是单选 qType = "single"; } else if (/填空|____|___|__/.test(fullText)) qType = "fill"; // 如果只有2个选项且含"正确""错误" if (options.length === 2 && /正确|错误|对|错/.test(options.join(""))) { qType = "judge"; } return { question, options, type: qType, optMap }; } // ==================================================================== // 6. ★★★ 改进:DeepSeek 提示词(加入 Chain-of-Thought)★★★ // ==================================================================== async function callDeepSeek(question, options, qType) { if (!CFG.API_KEY) { showToast("⚠️ 请先设置 DeepSeek API Key", "error"); return null; } // 构建带选项的完整题目文本 let questionWithOptions = `【题目】\n${question}\n\n`; if (options.length > 0) { questionWithOptions += `【选项】\n${options.join("\n")}\n\n`; } questionWithOptions += `【题型】${typeLabel(qType)}\n`; // ★ 用 Chain-of-Thought 提示让模型先推理再给答案 ★ const systemPrompt = `你是专业答题助手,擅长解答各类考试题目。 请按以下步骤思考(但最终只输出答案本身,不要输出思考过程): ## 步骤 1. 理解题目在问什么 2. 逐一分析每个选项是否正确(单选题只有一个正确,多选题可能有多个正确) 3. 根据分析选择正确答案 4. 输出最终答案 ## 输出格式(严格遵守) 单选题 → 只输出一个字母,如:A 多选题 → 输出所有字母,如:ABC 判断题 → 输出"正确"或"错误" 填空题 → 输出填空内容 ## 示例 示例1: 题目:以下哪个是Python的关键字? 选项: A. list B. def C. array D. string 答案:B 示例2: 题目:以下哪些是关系数据库? 选项: A. MySQL B. MongoDB C. Oracle D. Redis 答案:AC 示例3: 题目:地球是太阳系中最大的行星。 选项: A. 正确 B. 错误 答案:错误 ## 重要规则 - 必须基于题目和选项内容推理,不要凭空猜测 - 如果不确定,选择最合理的一项 - 不要输出解释、不要输出推理过程、不要输出多余文字 - 只输出最终答案`; const payload = { model: CFG.MODEL, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: questionWithOptions }, ], temperature: 0, // ★ 设为0 → 确定性输出,每次相同题目给出相同答案 max_tokens: 200, stream: false, }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: CFG.API_URL, headers: { "Content-Type": "application/json", Authorization: `Bearer ${CFG.API_KEY}`, }, data: JSON.stringify(payload), timeout: 30000, onload: resp => { try { const data = JSON.parse(resp.responseText); let content = data?.choices?.[0]?.message?.content || ""; content = content.replace(/\s+/g, "").trim(); // 如果答案带"答案:"前缀,去掉 content = content.replace(/^答案[::]/, ""); resolve(content); } catch (e) { reject(e); } }, onerror: reject, ontimeout: () => reject(new Error("超时")), }); }); } // ==================================================================== // 7. ★★★ 支持多重试 + 混合模式 ★★★ // ==================================================================== async function searchWithRetry(question, options, qType, maxRetries = 2) { for (let i = 0; i <= maxRetries; i++) { try { const answer = await callDeepSeek(question, options, qType); if (answer && answer.length >= 1) { // 简单校验:单选题答案应该是一个字母 if (qType === "single" && /^[A-D]$/i.test(answer)) return answer; // 多选题:字母组合 if (qType === "multi" && /^[A-D]+$/i.test(answer)) return answer.toUpperCase(); // 判断题 if (qType === "judge" && /正确|错误|对|错/i.test(answer)) return answer; // 其他:只要非空就接受 if (answer.length <= 50) return answer; } } catch (e) { console.warn(`第${i + 1}次调用失败:`, e); } // 重试前等一会儿 if (i < maxRetries) await sleep(1000); } return null; } // ==================================================================== // 8. 主流程 // ==================================================================== async function processCurrentQuestion() { updateStatus("正在提取题目..."); const parsed = extractQuestion(); if (!parsed.question) { updateStatus("❌ 未找到题目"); return null; } // 显示 showResult(parsed.question, "正在搜索..."); updateStatus(`搜索中 (${typeLabel(parsed.type)})...`); // 搜索答案(含重试) const answer = await searchWithRetry(parsed.question, parsed.options, parsed.type); if (!answer) { updateStatus("❌ 未找到答案"); showResult(parsed.question, "未搜到答案"); return null; } showResult(parsed.question, answer); updateStatus(`✅ 答案: ${answer}`); // ----- 自动勾选 ----- if (CFG.MODE === "auto") { let filled = false; const optMap = parsed.optMap || scanOptions(); if (parsed.type === "judge") { filled = selectJudge(answer, optMap); } else if (parsed.type === "multi") { filled = selectMultiOptions(answer, optMap); } else if (parsed.type === "single") { // 单选的答案可能是一个字母 if (/^[A-D]$/i.test(answer.trim())) { filled = selectOption(answer.trim(), optMap); } else { // 如果不是字母,尝试全文本匹配 filled = fillByText(answer, optMap); } } else { // 填空或其他类型 filled = fillByText(answer, optMap); } if (filled) { showToast(`✅ 已勾选: ${answer}`, "success"); return answer; } else { showToast("⚠️ 找到答案但自动勾选失败,请手动点选", "warning"); return answer; } } return answer; } // 按文本匹配(兜底方案) function fillByText(answer, optMap) { // 在 optMap 中搜索 labelText 包含 answer 的 for (const [letter, entry] of Object.entries(optMap)) { if (entry.labelText.includes(answer) || answer.includes(entry.labelText.trim())) { return selectOption(letter, optMap); } } // 最后尝试:全页面搜索 const allClickable = document.querySelectorAll("span, label, div, button, a, li"); for (const el of allClickable) { if (el.textContent.trim() === answer || el.textContent.trim().includes(answer)) { el.click(); return true; } } return false; } // ==================================================================== // 9. 循环答题 // ==================================================================== async function startAutoLoop() { if (running) return; if (!CFG.API_KEY) { showToast("请先设置 API Key", "error"); return; } running = true; stopFlag = false; qCount = 0; okCount = 0; failCount = 0; updateStatus("🟢 自动答题中..."); document.getElementById("ocs-start") && (document.getElementById("ocs-start").disabled = true); updateStats(); while (!stopFlag) { qCount++; const ans = await processCurrentQuestion(); if (ans) okCount++; else failCount++; updateStats(); await sleep(CFG.ANSWER_GAP); const action = clickNextOrSubmit(); if (action === "submit" || action === "none") { // 如果是提交,等确认弹窗 if (action === "submit") { showToast("检测到交卷按钮,点击后停止", "info"); await sleep(3000); } // 没有"下一题"按钮了 → 结束 break; } await sleep(800); } running = false; document.getElementById("ocs-start") && (document.getElementById("ocs-start").disabled = false); updateStatus("■ 已完成"); showToast(`答题完成!成功 ${okCount}/${qCount}`, okCount > 0 ? "success" : "error"); } function stopAuto() { stopFlag = true; } // ==================================================================== // 10. 面板UI // ==================================================================== function createPanel() { const panel = document.createElement("div"); panel.id = "ocs-v2-panel"; panel.innerHTML = `