// ==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 = `
🧠 DeepSeek 答题 v2
就绪
不勾则仅显示答案
总:0 👍:0 👎:0
`; const style = document.createElement("style"); style.textContent = ` #ocs-v2-panel { position:fixed; top:60px; right:20px; width:380px; z-index:999999; background:#fff; border:1px solid #e5e7eb; border-radius:12px; box-shadow:0 8px 30px rgba(0,0,0,0.18); font:14px -apple-system,BlinkMacSystemFont,sans-serif; color:#222; } #ocs2-hdr { display:flex; justify-content:space-between; align-items:center; padding:10px 16px; background:linear-gradient(135deg,#2563EB,#7C3AED); color:#fff; border-radius:12px 12px 0 0; font-weight:600; cursor:move; } #ocs2-hide { background:rgba(255,255,255,0.2); border:none; color:#fff; width:26px; height:26px; border-radius:6px; cursor:pointer; } #ocs2-hide:hover { background:rgba(255,255,255,0.35); } #ocs2-body { padding:12px 16px; } #ocs2-body.mini { display:none; } #ocs2-st { padding:4px 10px; background:#F3F4F6; border-radius:6px; font-size:12px; color:#666; margin-bottom:10px; } .ocs2-row { display:flex; align-items:center; gap:8px; margin-bottom:10px; } .ocs2-row input[type="text"] { flex:1; padding:7px 10px; border:1px solid #d1d5db; border-radius:6px; font-size:13px; outline:none; } .ocs2-row input[type="text"]:focus { border-color:#2563EB; box-shadow:0 0 0 2px rgba(37,99,235,0.15); } #ocs2-save { padding:7px 14px; background:#2563EB; color:#fff; border:none; border-radius:6px; cursor:pointer; font-size:12px; white-space:nowrap; } #ocs2-save:hover { background:#1D4ED8; } .o2btn { padding:7px 16px; border:none; border-radius:6px; cursor:pointer; font-size:12px; font-weight:500; } .o2prim { background:#10B981; color:#fff; } .o2prim:hover { background:#059669; } .o2dng { background:#EF4444; color:#fff; } .o2dng:hover { background:#DC2626; } .o2sec { background:#6B7280; color:#fff; } .o2sec:hover { background:#4B5563; } .o2btn:disabled { opacity:0.5; cursor:default; } #ocs2-out { margin:8px 0; padding:10px; background:#F9FAFB; border-radius:8px; min-height:36px; max-height:200px; overflow-y:auto; font-size:12px; line-height:1.5; } #ocs2-q { color:#666; } #ocs2-a { color:#059669; font-weight:600; font-size:13px; } #ocs2-stats { font-size:11px; color:#9CA3AF; text-align:right; } .o2toast { position:fixed; top:20px; left:50%; transform:translateX(-50%); padding:10px 28px; border-radius:8px; color:#fff; font-size:14px; z-index:1000000; animation:o2fade 0.25s; box-shadow:0 4px 16px rgba(0,0,0,0.2); } .o2toast.ok { background:#10B981; } .o2toast.err { background:#EF4444; } .o2toast.warn { background:#F59E0B; } .o2toast.info { background:#3B82F6; } @keyframes o2fade { from{opacity:0;transform:translateX(-50%) translateY(-12px)} to{opacity:1;transform:translateX(-50%) translateY(0)} } `; document.head.appendChild(style); document.body.appendChild(panel); // 拖拽 let drag = false, sx, sy, sl, st; const hdr = document.getElementById("ocs2-hdr"); hdr.addEventListener("mousedown", e => { drag = true; sx = e.clientX; sy = e.clientY; sl = panel.offsetLeft; st = panel.offsetTop; }); document.addEventListener("mousemove", e => { if (!drag) return; panel.style.left = sl + e.clientX - sx + "px"; panel.style.top = st + e.clientY - sy + "px"; panel.style.right = "auto"; }); document.addEventListener("mouseup", () => { drag = false; }); document.getElementById("ocs2-hide").onclick = () => document.getElementById("ocs2-body").classList.toggle("mini"); document.getElementById("ocs2-save").onclick = () => { CFG.API_KEY = document.getElementById("ocs2-key").value.trim(); GM_setValue("ds_api_key", CFG.API_KEY); showToast("API Key 已保存", "ok"); }; document.getElementById("ocs2-go").onclick = startAutoLoop; document.getElementById("ocs2-stop").onclick = stopAuto; document.getElementById("ocs2-once").onclick = async () => { await processCurrentQuestion(); updateStats(); }; document.getElementById("ocs2-auto").onchange = e => { CFG.MODE = e.target.checked ? "auto" : "semi"; GM_setValue("ds_mode", CFG.MODE); }; } // ==================================================================== // 11. 辅助函数 // ==================================================================== function showToast(msg, type = "info") { const el = document.createElement("div"); el.className = `o2toast ${type}`; el.textContent = msg; document.body.appendChild(el); setTimeout(() => el.remove(), 3000); } function updateStatus(msg) { const el = document.getElementById("ocs2-st"); if (el) el.textContent = msg; } function showResult(q, a) { const qe = document.getElementById("ocs2-q"); const ae = document.getElementById("ocs2-a"); if (qe) qe.textContent = "📝 " + (q ? q.slice(0, 100) + (q.length > 100 ? "..." : "") : ""); if (ae) ae.textContent = "✅ " + (a || "等待中..."); } function updateStats() { const el = document.getElementById("ocs2-stats"); if (el) el.textContent = `总:${qCount} 👍:${okCount} 👎:${failCount}`; } function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } function typeLabel(t) { return { single: "单选题", multi: "多选题", judge: "判断题", fill: "填空题", unknown: "未知" }[t] || t; } // ==================================================================== // 12. 启动 // ==================================================================== function init() { setTimeout(() => { const hasExam = document.querySelector( ".question, [class*='exam'], [class*='question'], [class*='test'], " + ".ZyExamTit, .CyMainTit" ) || /exam|test|quiz|作业|考试|答题|测验|答题卡/.test(document.title); if (hasExam) { createPanel(); updateStatus("✅ 已就绪 - 检测到考试页面"); // 如果已有 API Key,自动搜第一题 if (CFG.API_KEY) { setTimeout(() => { const parsed = extractQuestion(); if (parsed.question) processCurrentQuestion(); }, 1500); } } else { console.log("%c🧠 OCS v2%c 未检测到考试页面", "color:#2563EB;font-weight:bold", "color:#666"); } }, 1000); } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init); else init(); // 全局暴露 unsafeWindow.OCSv2 = { start: startAutoLoop, stop: stopAuto, search: processCurrentQuestion, scanOptions, }; console.log("%c🧠 OCS DeepSeek v2%c 已加载(自动勾选 + CoT提示)", "color:#2563EB;font-weight:bold;font-size:14px", "color:#666"); })();