// ==UserScript== // @name 学习通效率助手(蚌医版) // @namespace http://tampermonkey.net/ // @version 2.0 // @description 作业试卷导出Word(图文内嵌/强制A4/去黑点/彻底修复重叠) | 单题/整页搜题:题库海 + DeepSeek 网页版,两边答案逐题对比、按所选答案一键填充 | 一键复制题目给其他 AI | 课程资源下载(PDF/课件/视频, D键下载全部PDF) | 解除限制 // @author jasminen // @copyright 2026, jasminen // @license CC-BY-NC-ND-4.0 // @match *://*.chaoxing.com/* // @match *://chat.deepseek.com/* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_openInTab // @grant GM_setClipboard // @grant GM_addElement // @grant unsafeWindow // @connect search.tikuhai.com // @connect chaoxing.com // @connect cldisk.com // @connect * // @run-at document-start // ==/UserScript== /* * 学习通效率助手(蚌医版) * Copyright (c) 2026 jasminen * * 本脚本采用「知识共享 署名-非商业性使用-禁止演绎 4.0 国际」许可协议(CC BY-NC-ND 4.0)授权。 * 协议全文:https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode.zh-hans * * 简要说明(以协议全文为准): * - 允许:以任何媒介、任何形式复制和分发本脚本的原样版本,例如分享给同学。 * - 署名:分发时须注明作者 jasminen,附上本许可协议链接,并保留本版权声明。 * - 非商业性使用:不得用于商业目的,例如出售、收费代装、捆绑销售。 * - 禁止演绎:不得分发修改、改编或基于本脚本二次开发的版本。 * * 本脚本按「现状」提供,不附带任何形式的保证。 * 如需二次开发、商业使用等本协议以外的授权,请联系作者。 */ (function() { 'use strict'; // 脚本在 document-start 注入:DeepSeek 页面要赶在它自己的代码之前接管 fetch; // 学习通页面要等 DOM 就绪(相当于原来的 document-end)再运行 if (location.hostname !== 'chat.deepseek.com' && document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main, { once: true }); } else { main(); } function main() { const isTop = window.self === window.top; if (isTop && document.getElementById('cx-helper-panel')) return; const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; const nativeAlert = window.alert.bind(window); const MSG_KEY = 'cxHelper'; // ========== 通用工具 ========== function isTrustedOrigin(origin) { try { const host = new URL(origin).hostname; return host === 'chaoxing.com' || host.endsWith('.chaoxing.com'); } catch (e) { return false; } } function post(target, data, origin = '*') { try { target.postMessage({ [MSG_KEY]: true, ...data }, origin); } catch (e) {} } function escapeHtml(str) { return str.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } function toBase64(buf) { const bytes = new Uint8Array(buf); let binary = ''; for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000)); return btoa(binary); } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // D 键下载全部 PDF:正在输入或按着组合键时不触发 function isPdfHotkey(ev) { if ((ev.key !== 'd' && ev.key !== 'D') || ev.ctrlKey || ev.metaKey || ev.altKey || ev.repeat) return false; const target = ev.target; return !(target && (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName))) && document.designMode !== 'on'; } // ========== DeepSeek 网页版:在 chat.deepseek.com 标签页里代为提问 ========== // 学习通页面把提示词写进 GM 存储并打开 DeepSeek 标签页;这里认领任务,填入输入框发送, // 等回答生成完后把回答文字写回存储。两边都是同一个脚本,靠 GM_setValue / GM_addValueChangeListener 通信 const DS_URL = 'https://chat.deepseek.com/'; const DS_TASK_KEY = 'dsTask', DS_RESULT_KEY = 'dsResult'; const DS_TAG = 'cx-helper-'; const DS_ANSWER_SELECTOR = '.ds-markdown'; const DS_ANSWER_TIMEOUT = 5 * 60 * 1000; // 从回答里取出 JSON 数组(模型常会包一层 ```json 代码块或带几句说明) function parseJsonArray(text) { const start = text.indexOf('['), end = text.lastIndexOf(']'); if (start < 0 || end <= start) return null; try { const value = JSON.parse(text.slice(start, end + 1)); return Array.isArray(value) ? value : null; } catch (e) { return null; } } async function waitFor(check, timeout, interval = 500) { const end = Date.now() + timeout; for (;;) { const value = check(); if (value) return value; if (Date.now() > end) return null; await delay(interval); } } // 标签页 URL 带着任务 id;万一被站内路由去掉了,就认领 30 秒内新建、还没被认领的任务 function claimDeepSeekTask() { const task = GM_getValue(DS_TASK_KEY, null); if (!task || task.status !== 'pending' || !Array.isArray(task.prompts)) return null; const nav = performance.getEntriesByType('navigation')[0]; const urls = location.href + ' ' + (nav ? nav.name : ''); if (!urls.includes(DS_TAG + task.id) && Date.now() - task.created > 30000) return null; GM_setValue(DS_TASK_KEY, { ...task, status: 'claimed' }); return task; } function isTaskCancelled(id) { const task = GM_getValue(DS_TASK_KEY, null); return !task || task.id !== id || task.status === 'cancelled'; } function dsReport(id, data) { GM_setValue(DS_RESULT_KEY, { id, time: Date.now(), ...data }); } // DeepSeek 页面是 React,要用原生 setter 改值再触发 input,React 才会认 function setNativeValue(field, value) { Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set.call(field, value); field.dispatchEvent(new Event('input', { bubbles: true })); } function pressEnter(field) { field.focus(); const init = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true }; ['keydown', 'keypress', 'keyup'].forEach(type => field.dispatchEvent(new KeyboardEvent(type, init))); } // 回车没发出去时的后备:输入区里最后一个可用按钮就是发送按钮 function findSendButton(field) { for (let node = field.parentElement, depth = 0; node && depth < 6; node = node.parentElement, depth++) { const buttons = [...node.querySelectorAll('button, [role="button"]')] .filter(b => !b.disabled && b.getAttribute('aria-disabled') !== 'true'); if (buttons.length) return buttons[buttons.length - 1]; } return null; } // 优先取代码块里的内容,避免把代码块标题栏的「复制」「下载」等按钮文字带进来 function readAnswer(node) { const blocks = [...node.querySelectorAll('pre')].map(pre => pre.textContent); return blocks.length ? blocks.join('\n') : node.innerText; } // 从正在生成的回答里取出已经完整的题目对象,不用等整个数组写完就能先显示 function parseAnswerItems(text) { const items = []; (text.match(/\{[^{}]*\}/g) || []).forEach(chunk => { try { const item = JSON.parse(chunk); if (item && Number.isInteger(Number(item.no))) items.push(item); } catch (e) {} }); return items; } async function waitForInput() { const input = await waitFor(() => (/sign_in|login/.test(location.pathname) ? 'login' : document.querySelector('textarea#chat-input, textarea')), 60000); if (input === 'login') throw new Error('DeepSeek 未登录,请先在打开的 DeepSeek 页面登录,然后重试'); if (!input) throw new Error('没找到 DeepSeek 的输入框,页面可能改版了'); return input; } // 输入框下方「深度思考」「智能搜索」这类开关按钮 const DEEP_THINK_RE = /^(深度思考|DeepThink)/i; // 智能搜索是 DeepSeek 新版的叫法,旧版叫联网搜索,两个都认 const WEB_SEARCH_RE = /^(智能搜索|联网搜索|Smart Search|Search)$/i; const CLICKABLE_RE = /button|toggle|switch/i; function shortText(node) { const text = (node.innerText || '').trim(); return text.length < 20 ? text : ''; } // 先找文字匹配的最内层元素,再往上找到按钮本身(不一定是