// ==UserScript== // @name 多平台题目整理工具 // @namespace 题目整理--支持超星、智慧树、Mooc // @version 2.8.2 // @description 一键读取题目到本地。 // @author Jason7187 // @match *://*.chaoxing.com/mooc-ans/mooc2/work/view* // @match *://*.chaoxing.com/mooc-ans/mooc2/work/* // @match *://*.chaoxing.com/mooc-ans/work/* // @match *://*.chaoxing.com/exam-ans/exam/* // @match *://*.chaoxing.com/mycourse/studentstudy* // @match *://*.chaoxing.com/mooc-ans/knowledge/cards* // @match *://*.chaoxing.com/ananas/* // @match *://*.chaoxing.com/mooc-ans/api/* // @match *://*.icourse163.org/* // @match *://*.icve.com.cn/* // @match *://*.zhihuishu.com/* // @grant GM_registerMenuCommand // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_getResourceText // @connect tk.awk618.cn // @connect localhost // @source https://github.com/Jason7187/chaoxing // @icon https://free.picui.cn/free/2025/10/11/68e9b26f92cc7.jpg // @require https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.17.4/xlsx.full.min.js // @require https://scriptcat.org/lib/668/2.0/TyprMd5.js // @resource Table https://www.forestpolice.org/ttf/2.0/table.json // ==/UserScript== (function () { 'use strict'; const IDS = { MARKER: 'cx-script-marker', TOOLBAR: 'cx-toolbar', TOOLBAR_STYLE: 'cx-toolbar-style', PREVIEW: 'cx-preview', PREVIEW_STYLE: 'cx-preview-style' }; const MSG = { PING: 'CX_EXPORT_PING', PONG: 'CX_EXPORT_PONG', CLAIM: 'CX_EXPORT_HOST_CLAIM', HELLO: 'CX_EXPORT_ASK_UP', HOST: 'CX_EXPORT_HOST_HERE' }; const Q_SELECTOR = '.aiArea, .questionLi, .TiMu, #ZyBottom, h3.newTestType'; const CONTAINER_CANDIDATES = ['.questionLi', '.aiArea', '.TiMu']; const OPTION_SELECTOR = '.mark_letter li, .stem_answer > div, ul.Zy_ulTop > li, ul.qtDetail > li'; const RAW = (() => { try { if (typeof unsafeWindow !== 'undefined' && unsafeWindow && unsafeWindow.document) return unsafeWindow; } catch (e) {} return window; })(); let currentData = []; let suppressed = false; let claimed = false; function notify(text) { try { if (typeof GM_notification === 'function') { GM_notification({ title: '工具', text, timeout: 4000 }); } } catch (e) { console.warn('[多平台题目整理]', text); } } function guard(label, fn) { try { return fn(); } catch (err) { console.warn('[多平台题目整理] ' + label + ' 出错:', err); } } function rawFrames(win) { const list = []; let frames; try { frames = win.frames; } catch (e) { return list; } if (!frames) return list; let len = 0; try { len = frames.length; } catch (e) { return list; } for (let i = 0; i < len; i++) { try { list.push(frames[i]); } catch (e) {} } return list; } function childWindows(win) { const out = []; const add = list => list.forEach(w => { if (out.indexOf(w) === -1) out.push(w); }); if (win === RAW || win === window) { add(rawFrames(RAW)); if (RAW !== window) add(rawFrames(window)); } else { add(rawFrames(win)); } return out; } const util = { text(doc, sel) { try { const el = doc.querySelector(sel); if (!el) return ''; return (el.textContent || '').replace(/\u00A0/g, ' ').replace(/\s+/g, ' ').trim(); } catch (e) { return ''; } }, value(doc, sel) { try { const el = doc.querySelector(sel); return el && 'value' in el ? String(el.value || '').trim() : ''; } catch (e) { return ''; } }, urlParam(url, key) { const m = new RegExp('[?&]' + key + '=([^&#]*)', 'i').exec(url || ''); if (!m) return ''; try { return decodeURIComponent(m[1].replace(/\+/g, ' ')); } catch (e) { return m[1]; } }, scriptVar(doc, key) { try { const re = new RegExp('["\']' + key + '["\']\\s*:\\s*["\']([^"\']{1,200})["\']'); const scripts = doc.querySelectorAll('script:not([src])'); for (const s of scripts) { const m = re.exec(s.textContent || ''); if (!m) continue; let v = m[1]; try { v = decodeURIComponent(v.replace(/\+/g, ' ')); } catch (e) {} return v.replace(/\s+/g, ' ').trim(); } } catch (e) {} return ''; }, escapeHtml(str) { return String(str == null ? '' : str) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } }; function showCustomDialog(title, defaultValue, onConfirm) { const oldModal = document.getElementById('cx-custom-modal'); if (oldModal) oldModal.remove(); const safeTitle = util.escapeHtml(title); const safeDefault = util.escapeHtml(defaultValue); const placeholder = '例如:https://你的域名/u'; const safePlaceholder = util.escapeHtml(placeholder); const modalHtml = `
${safeTitle}
`; document.body.insertAdjacentHTML('beforeend', modalHtml); const modal = document.getElementById('cx-custom-modal'); const input = document.getElementById('cx-modal-input'); input.focus(); input.select(); document.getElementById('cx-modal-cancel').onclick = () => modal.remove(); document.getElementById('cx-modal-confirm').onclick = () => { const val = input.value.trim(); if (val) onConfirm(val); modal.remove(); }; modal.addEventListener('keydown', (e) => { if (e.key === 'Enter') document.getElementById('cx-modal-confirm').click(); if (e.key === 'Escape') modal.remove(); }); } const Frame = { topDoc() { try { return RAW.top.document; } catch (e) { return null; } }, topHasScript() { const doc = this.topDoc(); try { return !!(doc && doc.getElementById(IDS.MARKER)); } catch (e) { return false; } }, localContainers() { return Frame.containerCount(document); }, containerCount(doc) { for (const selector of CONTAINER_CANDIDATES) { try { const count = doc.querySelectorAll(selector).length; if (count) return count; } catch (e) {} } return 0; }, hasQuestionsDeep(win) { try { if (win.document.querySelector(Q_SELECTOR)) return true; } catch (e) {} for (const w of childWindows(win)) { try { if (!w.document) continue; } catch (e) { continue; } if (this.hasQuestionsDeep(w)) return true; } return false; }, docs(start) { const list = []; const seen = new Set(); const walk = win => { let doc; try { doc = win.document; } catch (e) { return; } if (!doc || seen.has(doc)) return; seen.add(doc); list.push(doc); childWindows(win).forEach(w => { try { if (w.document) walk(w); } catch (e) {} }); }; walk(start || RAW); return list; }, localInfo() { let url = ''; try { url = RAW.location ? RAW.location.href : ''; } catch (e) {} return { url, containers: this.localContainers() }; }, markPresence() { try { if (document.getElementById(IDS.MARKER)) return; const el = document.createElement('div'); el.id = IDS.MARKER; el.style.display = 'none'; (document.head || document.documentElement).appendChild(el); } catch (e) {} } }; const Bus = { _pending: {}, token() { return 'cx' + Date.now().toString(36) + Math.random().toString(36).slice(2, 8); }, post(win, payload) { try { if (win) win.postMessage(payload, '*'); } catch (e) {} }, broadcastDown(payload) { childWindows(RAW).forEach(w => this.post(w, payload)); }, listen() { const handler = ev => { const data = ev.data; if (!data || typeof data !== 'object') return; if (data.type === MSG.PING) this.onPing(data, ev.source); else if (data.type === MSG.PONG && this._pending[data.token]) this._pending[data.token](data); else if (data.type === MSG.CLAIM) this.onClaim(); else if (data.type === MSG.HELLO) this.onHello(data, ev.source); else if (data.type === MSG.HOST) this.onHost(data); }; const targets = [RAW]; if (RAW !== window) targets.push(window); targets.forEach(t => { try { t.addEventListener('message', handler, false); } catch (e) {} }); }, parentWindow() { try { const parent = RAW.parent; if (parent && parent !== RAW) return parent; } catch (e) {} return null; }, askUp(token) { const parent = this.parentWindow(); if (parent) this.post(parent, { type: MSG.HELLO, token }); }, onHello(data, source) { if (!source || (data.hop || 0) > 8) return; if (claimed) { this.post(source, { type: MSG.HOST, token: data.token }); return; } const parent = this.parentWindow(); if (!parent) return; this._parentAsk[data.token] = source; this.post(parent, { type: MSG.HELLO, token: data.token, hop: (data.hop || 0) + 1 }); }, onHost(data) { suppressed = true; this.removeToolbar(); const asker = this._parentAsk[data.token]; if (asker) { delete this._parentAsk[data.token]; this.post(asker, { type: MSG.HOST, token: data.token }); } }, onClaim() { suppressed = true; this.removeToolbar(); childWindows(RAW).forEach(w => this.post(w, { type: MSG.CLAIM })); }, removeToolbar() { const el = document.getElementById(IDS.TOOLBAR); if (el) el.remove(); }, async onPing(data, source) { if (!source || (data.hop || 0) > 8) return; const token = data.token; const info = Frame.localInfo(); let items = []; try { await FontDecryptor.decryptAll(); items = PlatformParser.parseAll(); } catch (e) { items = []; } if (!childWindows(RAW).length) { this.post(source, { type: MSG.PONG, token, items, infos: [info] }); return; } const childItems = []; const childInfos = []; let finished = false; let quietTimer = null; let capTimer = null; const finish = () => { if (finished) return; finished = true; clearTimeout(quietTimer); clearTimeout(capTimer); delete this._pending[token]; this.post(source, { type: MSG.PONG, token, items: items.concat(childItems), infos: [info].concat(childInfos) }); }; this._pending[token] = res => { if (Array.isArray(res.items)) childItems.push(...res.items); if (Array.isArray(res.infos)) childInfos.push(...res.infos); clearTimeout(quietTimer); quietTimer = setTimeout(finish, _c.QUIET_WAIT); }; childWindows(RAW).forEach(w => this.post(w, { type: MSG.PING, token, hop: (data.hop || 0) + 1 })); quietTimer = setTimeout(finish, _c.QUIET_WAIT); capTimer = setTimeout(finish, _c.PONG_WAIT); } }; Bus._parentAsk = {}; const Collector = { async collect(timeoutMs) { await FontDecryptor.decryptAll(); let direct = []; try { direct = PlatformParser.parseAll(); } catch (e) { direct = []; } const extra = []; const infos = []; const token = Bus.token(); return new Promise(resolve => { let finished = false; let quietTimer = null; let capTimer = null; const finish = () => { if (finished) return; finished = true; clearTimeout(quietTimer); clearTimeout(capTimer); delete Bus._pending[token]; resolve({ items: PlatformParser.dedupe(direct.concat(extra)), infos }); }; Bus._pending[token] = res => { if (Array.isArray(res.items)) extra.push(...res.items); if (Array.isArray(res.infos)) infos.push(...res.infos); clearTimeout(quietTimer); quietTimer = setTimeout(finish, _c.QUIET_WAIT); }; if (!childWindows(RAW).length) { finish(); return; } Bus.broadcastDown({ type: MSG.PING, token, hop: 0 }); quietTimer = setTimeout(finish, _c.QUIET_WAIT); capTimer = setTimeout(finish, timeoutMs || _c.COLLECT_TIMEOUT); }); } }; function extractCustomVariables(html) { const marker = /customVariablesJson\s*=\s*/i.exec(html); if (!marker) return null; const source = html.slice(marker.index + marker[0].length); let jsonText = ''; if (source.startsWith('{')) { let depth = 0; let quote = ''; let escaped = false; for (let i = 0; i < source.length; i += 1) { const ch = source[i]; if (quote) { if (escaped) escaped = false; else if (ch === '\\') escaped = true; else if (ch === quote) quote = ''; continue; } if (ch === '"' || ch === "'" || ch === '`') { quote = ch; continue; } if (ch === '{') depth += 1; if (ch === '}') { depth -= 1; if (depth === 0) { jsonText = source.slice(0, i + 1); break; } } } if (jsonText) { try { return JSON.parse(jsonText); } catch (e) {} } } else { const quote = source[0]; if (quote === '"' || quote === "'" || quote === '`') { let escaped = false; for (let i = 1; i < source.length; i += 1) { const ch = source[i]; if (escaped) { escaped = false; continue; } if (ch === '\\') { escaped = true; continue; } if (ch === quote) { jsonText = source.slice(1, i); try { const outer = JSON.parse(source.slice(0, i + 1)); return typeof outer === 'string' ? JSON.parse(outer) : outer; } catch (e) {} try { return JSON.parse(jsonText); } catch (e) {} break; } } } } const vars = jsonText || source.slice(0, 2000); const courseMatch = vars.match(/["']?courseName["']?\s*:\s*["']([^"']+)["']/i); const chapterMatch = vars.match(/["']?chapterName["']?\s*:\s*["']([^"']+)["']/i); if (!courseMatch && !chapterMatch) return null; return { courseName: courseMatch ? courseMatch[1] : '', chapterName: chapterMatch ? chapterMatch[1] : '' }; } const Meta = { resolve(docs) { const list = docs && docs.length ? docs : [document]; const meta = { title: '', courseName: '', courseId: '', chapterName: '', chapterId: '', workName: '', workId: '', examName: '', examId: '' }; list.forEach(doc => { try { const html = doc.documentElement ? doc.documentElement.innerHTML : ''; const url = (doc.location && doc.location.href) || ''; if (!meta.courseId) { meta.courseId = (url.match(/[?&]courseId=(\d+)/i) || [])[1] || ''; } if (!meta.courseName) { const courseMatch = html.match(/["']courseName["']\s*:\s*["']([^"']+)["']/i); if (courseMatch) meta.courseName = courseMatch[1].trim(); } if (url.includes('/studentstudy')) { if (!meta.chapterId) { meta.chapterId = (url.match(/[?&]chapterId=(\d+)/i) || [])[1] || ''; } if (!meta.chapterName) { const chapterMatch = html.match(/["']chapterName["']\s*:\s*["']([^"']+)["']/i); if (chapterMatch) meta.chapterName = chapterMatch[1].trim(); } if (!meta.chapterName && meta.chapterId) { const activeNode = doc.querySelector( `#cur${meta.chapterId}, .posCatalog_active` ); if (activeNode) { const nameEl = activeNode.querySelector( '.posCatalog_name, .posCatalog_title' ) || activeNode; meta.chapterName = String( nameEl.getAttribute('title') || nameEl.textContent ).trim(); } } } else if (url.includes('/work/view')) { if (!meta.workId) { meta.workId = (url.match(/[?&](?:workId|workid)=(\d+)/i) || [])[1] || ''; } if (!meta.workName) { const titleEl = doc.querySelector('.mark_title'); if (titleEl) meta.workName = titleEl.textContent.trim(); } } else if (url.includes('/exam-ans/')) { if (!meta.examId) { meta.examId = (url.match(/[?&](?:examId|examid)=(\d+)/i) || [])[1] || ''; } if (!meta.examName) { const titleEl = doc.querySelector('.mark_title'); if (titleEl) meta.examName = titleEl.textContent.trim(); } } } catch (e) {} }); meta.title = meta.workName || meta.examName || meta.chapterName || meta.courseName || ''; return meta; } }; class CXParser { static parseAll() { const docs = Frame.docs(); const meta = Meta.resolve(docs); this._meta = meta; let items = []; docs.forEach(doc => { items = items.concat(this.parseDoc(doc)); }); items.forEach(item => { item.courseName = meta.courseName || ''; item.courseId = meta.courseId || ''; item.chapterName = meta.chapterName || ''; item.chapterId = meta.chapterId || ''; item.workName = meta.workName || ''; item.workId = meta.workId || ''; item.examName = meta.examName || ''; item.examId = meta.examId || ''; item.courseRealName = meta.courseName || ''; }); return this.dedupe(items); } static gc_i() { if (!this._meta) this._meta = Meta.resolve(Frame.docs()); return { exportName: this._meta.courseName || '', courseName: this._meta.courseName || '', taskName: this._meta.title || '', courseId: this._meta.courseId || '', chapterName: this._meta.chapterName || '', chapterId: this._meta.chapterId || '', workName: this._meta.workName || '', workId: this._meta.workId || '', examName: this._meta.examName || '', examId: this._meta.examId || '', realCourseName: this._meta.courseName || '' }; } static parseDoc(doc) { return this.questionRoots(doc) .flatMap(el => this.parseContainer(el)) .filter(item => item && item.answer); } static questionRoots(doc) { let all = []; let selector = ''; for (const candidate of CONTAINER_CANDIDATES) { try { const found = doc.querySelectorAll(candidate); if (found.length) { all = Array.from(found); selector = candidate; break; } } catch (e) {} } if (!all.length) return []; const roots = all.filter(el => !(el.parentElement && el.parentElement.closest && el.parentElement.closest(selector))); const stems = this.countStems(doc); if (stems > 1 && roots.length < 2) return this.rootsFromStems(doc); return roots; } static countStems(doc) { try { return doc.querySelectorAll('.qtContent').length; } catch (e) { return 0; } } static rootsFromStems(doc) { const roots = []; let stems = []; try { stems = Array.from(doc.querySelectorAll('.qtContent')); } catch (e) { return roots; } stems.forEach(stem => { let node = stem; while (node.parentElement) { const parent = node.parentElement; let count = 0; try { count = parent.querySelectorAll('.qtContent').length; } catch (e) { break; } if (count !== 1) break; node = parent; } if (roots.indexOf(node) === -1) roots.push(node); }); return roots; } static stripOptionLabel(text) { return String(text == null ? '' : text) .replace(/^\s*[A-Za-z]\s*[..。、,,::))]\s*/, '') .replace(/^\s*[A-Za-z][\s\u3000]+/, ''); } static parseContainer(container) { const type = this.parseType(container); const stems = Array.from(container.querySelectorAll('.B_stem .order_answer')); const rightAnswer = container.querySelector('.mark_answer .rightAnswerContent'); if (stems.length && rightAnswer) { const optionEls = Array.from(container.querySelectorAll('.mark_letter li')); const options = optionEls.map((el, index) => { const label = String.fromCharCode(65 + index); return { label, text: this.stripOptionLabel(this.extractTextWithImgs(el)) }; }); const answerMap = new Map(Array.from(this.extractTextWithImgs(rightAnswer).matchAll(/[((]\s*(\d+)\s*[))]\s*([A-Z])/gi), match => [Number(match[1]), match[2].toUpperCase()])); return stems.map((stem, index) => { const text = this.extractTextWithImgs(stem); const numberMatch = text.match(/^\s*[((]\s*(\d+)\s*[))]/); const number = numberMatch ? Number(numberMatch[1]) : index + 1; const letter = answerMap.get(number); const option = options.find(item => item.label === letter); const question = text.replace(/^\s*[((]\s*\d+\s*[))]\s*/, '').trim(); return question && option ? { type: 'B1', question, options: options.map(item => item.label + '.' + item.text).join(_c.OPTION_SPLITTER), answer: option.label, score: this.parseScore(container) / stems.length } : null; }).filter(Boolean); } return { type, question: this.parseQuestion(container), options: this.parseOptions(container), answer: this.parseAnswer(container, type), score: this.parseScore(container) }; } static dedupe(items) { const map = new Map(); items.forEach(item => { const key = [item.type, item.question, item.options].join('\u0001'); const prev = map.get(key); if (!prev) map.set(key, item); else if (!prev.answer && item.answer) map.set(key, item); }); return Array.from(map.values()); } static parseType(container) { const candidates = []; const shallow = container.querySelector('.colorShallow'); if (shallow) candidates.push(shallow.textContent); const tag = container.querySelector('.qtContent .newZy_TItle') || container.querySelector('.newZy_TItle'); if (tag) candidates.push(tag.textContent); const section = this.findSectionTitle(container); if (section) candidates.push(section); for (const raw of candidates) { const type = this.normalizeType(raw); if (type) return type; } return ''; } static findSectionTitle(container) { const doc = container.ownerDocument; const body = doc.body || doc.documentElement; let el = container; while (el && el !== body && el !== doc.documentElement) { let prev = el.previousElementSibling; while (prev) { if (prev.matches && prev.matches('h3.newTestType')) return prev.textContent; const inner = prev.querySelector && prev.querySelector('h3.newTestType'); if (inner) return inner.textContent; prev = prev.previousElementSibling; } el = el.parentElement; } return ''; } static normalizeType(raw) { const src = String(raw == null ? '' : raw).replace(/\u00A0/g, ' ').trim(); if (!src) return ''; const clean = src .replace(/[([(【{<]/g, '').replace(/[\]))】}>]/g, '') .replace(/([.·。:,,]?\s*)(共|满分)?\s*\d+\.?\d*\s*分/g, '') .replace(/[.·。:,,]\s*分/g, '').replace(/\s*分/g, '').trim(); if (/^(?:A\d+|B\d+)+$/.test(clean)) return clean; const compact = clean.replace(/[\s\u3000.,。·:,、]/g, ''); const map = [ ['单选', '单选题'], ['单项选择', '单选题'], ['选择题A型', '单选题'], ['多选', '多选题'], ['多项选择', '多选题'], ['共用选项', '共用选项题'], ['共用备选答案', '共用选项题'], ['共用题干', '共用选项题'], ['配伍', '共用选项题'], ['判断', '判断题'], ['填空', '填空题'], ['简答', '简答题'], ['名词解释', '名词解释'], ['分析', '分析题'], ['问答', '问答题'], ['综合', '综合题'] ]; for (const [keyword, name] of map) { if (compact.includes(keyword)) return name; } if (/^[一二三四五六七八九十百\d.、,,::]+$/.test(compact)) return ''; return compact; } static parseQuestion(container) { const content = container.querySelector('.qtContent') || container.querySelector('.Zy_TItle .fontLabel') || container.querySelector('.mark_name') || container.querySelector('.Zy_TItle'); if (!content) return ''; return this.extractTextWithImgs(content) .replace(/^\s*【[^】]{1,12}】\s*/, '') .replace(/^\s*[\[((]\s*(?:单选题|多选题|判断题|填空题|简答题|名词解释|分析题|问答题|综合题)\s*[\]))]\s*/, '') .trim(); } static parseOptions(container) { const optionElements = container.querySelectorAll(OPTION_SELECTOR); return Array.from(optionElements).map((optionDiv, idx) => { const letter = String.fromCharCode(65 + idx); const text = this.stripOptionLabel(this.extractTextWithImgs(optionDiv)); return `${letter}.${text}`; }).join(' | '); } static isCorrectByUI(container) { if (container.querySelector('.marking_dui')) return true; if (container.querySelector('[aria-label*="正确"]')) return true; return false; } static isWrongByUI(container) { if (container.querySelector('.marking_cuo')) return true; if (container.querySelector('[aria-label*="错误"]')) return true; return false; } static isHalfCorrect(container) { if (container.querySelector('.marking_bandui')) return true; if (container.querySelector('[aria-label*="半对"]')) return true; return false; } static parseScore(container) { const el = container.querySelector('.totalScore i, .mark_score i, .newAnswerScore .scoreNum, .scoreNum'); return el ? parseFloat(el.textContent.trim()) : null; } static parseAnswer(container, type) { if (type === '填空题') { const rightAnswerElements = container.querySelectorAll( '.rightAnswerContent, .correctAnswerBx .correctAnswer .answerCon, .correctAnswer .answerCon' ); if (rightAnswerElements.length) { const parts = Array.from(rightAnswerElements) .map(el => this.cleanAnswerText(el.textContent)).filter(Boolean); if (parts.length) return parts.join(_c.ANSWER_SPLITTER); } const score = this.parseScore(container) || 0; if (score > 0 && !this.isHalfCorrect(container)) return this.extractStudentAnswer(container); return ''; } let answer = this.extractRightAnswer(container); if (!answer && this.isMultipleChoice(container)) answer = this.extractMultipleChoiceAnswer(container); if (!answer) { const score = this.parseScore(container) || 0; if (score > 0 && !this.isHalfCorrect(container)) answer = this.extractStudentAnswer(container); } if (!answer && this.isCorrectByUI(container) && !this.isWrongByUI(container) && !this.isHalfCorrect(container)) { answer = this.extractStudentAnswer(container); } if (!answer) return ''; answer = this.normalizeJudgmentSymbol(answer); return this.mapAnswerToOptionLetters(answer, container); } static extractMultipleChoiceAnswer(container) { const el = container.querySelector('.rightAnswerContent, .correctAnswer .answerCon'); return el ? el.textContent.trim() : ''; } static isOptionLetter(answer) { return /^[A-Za-z]+$/.test(answer); } static normalizeJudgmentSymbol(text) { if (/^[√✓]$/.test(text)) return '正确'; if (/^[×✗]$/.test(text)) return '错误'; return text; } static buildOptionsMap(container) { const optionElements = container.querySelectorAll(OPTION_SELECTOR); return Array.from(optionElements).reduce((map, el, idx) => { const letter = String.fromCharCode(65 + idx); let text = this.stripOptionLabel(this.extractTextWithImgs(el)).trim(); if (text.startsWith('。')) text = text.substring(1).trim(); map[letter] = text; return map; }, {}); } static mapAnswerToOptionLetters(answer, container) { const source = this.cleanAnswerText(answer); const optionEntries = Object.entries(this.buildOptionsMap(container)); if (!optionEntries.length) return source; const normalize = value => String(value || '').replace(/[\s\p{P}\p{S}]/gu, '').toLowerCase(); const judgment = value => { const text = normalize(value); if (/^(?:true|t|correct|right|正确|对|是|√|✓)$/.test(text)) return 'true'; if (/^(?:false|f|incorrect|wrong|错误|错|否|×|✗)$/.test(text)) return 'false'; return ''; }; return source.split(_c.ANSWER_SPLITTER).map(part => { const raw = part.trim(); if (!raw) return ''; const compact = raw.replace(/[\s,,、;;|/]+/g, ''); if (/^[A-Za-z]+$/.test(compact)) return compact.toUpperCase(); const key = normalize(raw); const meaning = judgment(raw); const matched = optionEntries.find(([, text]) => { return normalize(text) === key || Boolean(meaning && judgment(text) === meaning); }); return matched ? matched[0] : raw; }).filter(Boolean).join(_c.ANSWER_SPLITTER); } static cleanAnswerText(answer) { if (!answer) return ''; return String(answer) .replace(/\u00A0/g, ' ') .split(_c.ANSWER_SPLITTER) .map(part => part.replace(/^\s*(正确答案|参考答案|标准答案|我的答案|学生答案|作答)\s*[::]\s*/, '').replace(/\s+/g, ' ').trim()) .filter(Boolean) .join(_c.ANSWER_SPLITTER); } static extractRightAnswer(container) { const selectors = [ '.correctAnswerBx .correctAnswer .answerCon', '.correctAnswer .answerCon', '.rightAnswerContent', '.colorGreen dd', '.mark_answer_key .colorGreen dd', '.mark_fill.colorGreen dd', '.colorGreen', '.correctAnswer', '.answerKey' ]; for (const sel of selectors) { const els = container.querySelectorAll(sel); if (!els.length) continue; const parts = Array.from(els).map(el => this.cleanAnswerText(el.textContent)).filter(Boolean); if (parts.length) return parts.join(_c.ANSWER_SPLITTER); } return ''; } static extractStudentAnswer(container) { const selectors = ['.myAnswerBx .myAnswer .answerCon', '.stuAnswerContent', '.myAnswer .answerCon']; for (const sel of selectors) { const els = container.querySelectorAll(sel); if (!els.length) continue; const parts = Array.from(els).map(el => this.cleanAnswerText(el.textContent)).filter(Boolean); if (parts.length) return parts.join(_c.ANSWER_SPLITTER); } return ''; } static isMultipleChoice(container) { const type = this.parseType(container); return type.includes('多选') || /^A3|A4$/.test(type); } static extractTextWithImgs(element) { const cloned = element.cloneNode(true); cloned.querySelectorAll('img').forEach(img => { const url = img.getAttribute('src') || img.getAttribute('data-original') || ''; img.replaceWith(document.createTextNode(url)); }); return cloned.textContent.replace(/\u00a0/g, ' ').replace(/\r?\n/g, ' ').replace(/\s+/g, ' ').trim(); } } const FontDecryptor = { _tablePromise: null, _mappingCache: new Map(), async decryptAll(docs) { const list = docs && docs.length ? docs : Frame.docs(); for (const doc of list) { try { await this.decryptDocument(doc); } catch (err) { console.warn('[超星题目导出] 字体解密失败:', err); } } }, async decryptDocument(doc) { if (!doc || !doc.querySelectorAll) return; const targets = Array.from(doc.querySelectorAll('.font-cxsecret')); if (!targets.length) return; const styles = doc.head ? Array.from(doc.head.querySelectorAll('style')) : []; const fontStyle = styles.find(style => style.textContent && style.textContent.includes('font-cxsecret') ); const match = fontStyle && fontStyle.textContent.match(/base64,([\w\W]+?)'/); if (!match) return; const mapping = await this.mappingFor(match[1]); if (!mapping || !Object.keys(mapping).length) return; targets.forEach(target => { const view = doc.defaultView || window; const walker = doc.createTreeWalker( target, view.NodeFilter ? view.NodeFilter.SHOW_TEXT : 4 ); const nodes = []; while (walker.nextNode()) nodes.push(walker.currentNode); nodes.forEach(node => { const text = node.nodeValue || ''; node.nodeValue = text.replace(/[\u4e00-\u9fff]/g, char => { const code = char.charCodeAt(0); return mapping[code] ? String.fromCharCode(mapping[code]) : char; }); }); target.classList.remove('font-cxsecret'); }); }, async mappingFor(base64) { if (this._mappingCache.has(base64)) return this._mappingCache.get(base64); const mapping = await this.buildMapping(base64); if (mapping && Object.keys(mapping).length) this._mappingCache.set(base64, mapping); return mapping; }, async buildMapping(base64) { const typr = typeof Typr !== 'undefined' ? Typr : RAW.Typr; const md5Fn = typeof md5 !== 'undefined' ? md5 : RAW.md5; if (!typr || !typr.parse || !typr.U || typeof md5Fn !== 'function') { console.warn('[超星题目导出] 字体解密依赖未加载,跳过加密字体。'); return null; } const table = await this.loadTable(); if (!table || !Object.keys(table).length) return null; const data = atob(base64); const bytes = new Uint8Array(data.length); for (let i = 0; i < data.length; i += 1) bytes[i] = data.charCodeAt(i); const parsed = typr.parse(bytes); const font = Array.isArray(parsed) ? parsed[0] : parsed; if (!font) return null; const mapping = {}; for (let code = 19968; code < 40870; code += 1) { const glyph = typr.U.codeToGlyph(font, code); if (!glyph) continue; const path = typr.U.glyphToPath(font, glyph); const hash = String(md5Fn(JSON.stringify(path))).slice(24); const decoded = Number(table[hash]); if (Number.isFinite(decoded) && decoded > 0) mapping[code] = decoded; } return mapping; }, loadTable() { if (this._tablePromise) return this._tablePromise; this._tablePromise = new Promise(resolve => { try { if (typeof GM_getResourceText === 'function') { const raw = GM_getResourceText('Table'); if (raw) { resolve(JSON.parse(raw)); return; } } } catch (err) { console.warn('[超星题目导出] 字体字库资源读取失败:', err); } console.warn('[超星题目导出] 字体字库资源不可用,跳过字体解密。'); resolve({}); }); return this._tablePromise; } }; const EXTERNAL_TYPE_MAP = { single_choice: '单选题', multiple_choice: '多选题', true_false: '判断题', fill_blank: '填空题', short_answer: '简答题', essay: '论述题', calculation: '计算题', definition: '名词解释', reading: '阅读理解', listening: '听力题', cloze: '完形填空' }; const PLATFORM_ADAPTERS = [ { id: 'icourse163', name: '中国大学MOOC', matches: url => /icourse163\.org$/i.test(url.hostname) || url.hostname.includes('icourse163.org'), selectors: ['.u-questionItem'] }, { id: 'icve', name: '智慧职教', matches: url => url.hostname.includes('icve.com.cn'), selectors: ['.subjectDet'] }, { id: 'zhihuishu', name: '智慧树', matches: url => url.hostname.includes('zhihuishu.com'), selectors: [ '.question-item', '.questionItem', '.subject-item', '.subjectItem', '.quiz-item', '.exam-question', '.question-box' ] }, { id: 'chaoxing', name: '超星学习通', matches: url => url.hostname.includes('chaoxing.com'), selectors: CONTAINER_CANDIDATES } ]; const PlatformParser = { detectPlatform(url) { let parsed; try { parsed = new URL(String(url || location.href), location.href); } catch (err) { parsed = { hostname: '', href: String(url || '') }; } return PLATFORM_ADAPTERS.find(adapter => { try { return adapter.matches(parsed); } catch (err) { return false; } }) || { id: 'unknown', name: '未知平台', selectors: CONTAINER_CANDIDATES }; }, parseAll() { const href = RAW.location && RAW.location.href; const adapter = this.detectPlatform(href); if (adapter.id === 'chaoxing') return CXParser.parseAll(); return this.parseDocuments(Frame.docs(), href); }, parseDocuments(docs, sourceUrl) { const adapter = this.detectPlatform(sourceUrl); if (adapter.id === 'chaoxing') { let items = []; docs.forEach(doc => { items = items.concat(CXParser.parseDoc(doc)); }); return this.dedupe(items); } const meta = this.resolveMeta(Meta.resolve(docs), docs, sourceUrl); CXParser._meta = meta; let items = []; docs.forEach(doc => { items = items.concat(this.parsePlatformDocument(doc, sourceUrl)); }); items.forEach(item => { item.courseName = meta.courseName || ''; item.courseId = meta.courseId || ''; item.chapterName = meta.chapterName || ''; item.chapterId = meta.chapterId || ''; item.workName = meta.workName || ''; item.workId = meta.workId || ''; item.examName = meta.examName || ''; item.examId = meta.examId || ''; item.courseRealName = meta.courseName || ''; }); return this.dedupe(items); }, parsePlatformDocument(doc, sourceUrl) { const adapter = this.detectPlatform(sourceUrl || (doc.location && doc.location.href)); if (adapter.id === 'chaoxing') return CXParser.parseDoc(doc); const roots = this.findRoots(doc, adapter.selectors); const items = []; roots.forEach(root => { let parsed; try { if (adapter.id === 'icourse163') parsed = this.parseMoocQuestion(root); else if (adapter.id === 'icve') parsed = this.parseIcveQuestion(root); else parsed = this.parseGenericQuestion(root); } catch (err) { console.warn('[超星题目导出] 平台题目解析失败:', err); } this.expandExternal(parsed).forEach(item => { if (item && item.question && item.answer) items.push(item); }); }); return this.dedupe(items); }, normalizeExternal(item) { if (!item) return null; let type = EXTERNAL_TYPE_MAP[item.type] || String(item.type || ''); if (!type) type = this.detectChineseType(item.title || '') || '未知题型'; const options = this.formatOptions(item.options); const answer = this.cleanAnswer(item.answer); return { type, question: this.cleanQuestion(item.title || item.question || ''), options, answer: answer === '未找到答案' ? '' : answer }; }, expandExternal(parsed) { if (!parsed) return []; if (Array.isArray(parsed.children) && parsed.children.length) { return parsed.children.map(child => this.normalizeExternal({ ...child, title: [parsed.title, child.title].filter(Boolean).join(' ') })); } return [this.normalizeExternal(parsed)]; }, findRoots(doc, selectors) { for (const selector of selectors) { try { const list = Array.from(doc.querySelectorAll(selector)); if (list.length) { return list.filter(el => !(el.parentElement && el.parentElement.closest(selector)) ); } } catch (err) {} } return []; }, text(element) { if (!element) return ''; const clone = element.cloneNode(true); clone.querySelectorAll('img').forEach(img => { const src = img.getAttribute('data-original') || img.getAttribute('src') || ''; if (src) img.replaceWith((element.ownerDocument || document).createTextNode(src)); }); return String(clone.innerText || clone.textContent || '') .replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim(); }, cleanQuestion(text) { return String(text || '') .replace(/[\u200B-\u200F\uFEFF]/g, '') .replace(/^\s*\d+\s*[.、]\s*/, '') .replace(/\s*[((【]\s*(?:单选题|多选题|判断题|填空题|简答题|论述题|名词解释|计算题|听力题|阅读理解|完形填空)[^))】]*[))】]\s*/g, '') .replace(/\s+/g, ' ').trim(); }, cleanAnswer(answer) { return String(answer == null ? '' : answer) .replace(/^\s*(?:正确答案|参考答案|标准答案|我的答案|学生答案|作答)\s*[::]\s*/i, '') .replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim(); }, stripOptionLabel(text) { return String(text == null ? '' : text) .replace(/^\s*[A-Za-z]\s*[..。、,,::))]\s*/, '') .replace(/^\s*\[?[A-Za-z]\]?\s+/, '') .trim(); }, formatOptions(options) { let list = options; if (typeof list === 'string') list = list.split(/\s*\|\s*/); if (!Array.isArray(list)) return ''; return list.map((option, index) => { const letter = String.fromCharCode(65 + index); return `${letter}.${this.stripOptionLabel(option)}`; }).filter(option => !/^[A-Z]\.$/.test(option)).join(' | '); }, detectChineseType(text) { const source = String(text || ''); const pairs = [ ['多选', '多选题'], ['多项选择', '多选题'], ['单选', '单选题'], ['单项选择', '单选题'], ['判断', '判断题'], ['填空', '填空题'], ['简答', '简答题'], ['论述', '论述题'], ['计算', '计算题'], ['名词解释', '名词解释'], ['阅读', '阅读理解'], ['听力', '听力题'], ['完形', '完形填空'] ]; const found = pairs.find(([keyword]) => source.includes(keyword)); if (found) return found[1]; const raw = source.match(/\b(?:A\d+|B\d+|X)\b/); return raw ? raw[0] : ''; }, mapAnswerLetters(answer, options) { const raw = String(answer || '').trim(); if (!Array.isArray(options) || !options.length) return raw; const compact = raw.replace(/[\s,,、;;]+/g, ''); if (/^[A-Za-z]+$/.test(compact)) return compact.toUpperCase(); const normalize = value => String(value || '').replace(/[\s\p{P}\p{S}]/gu, '').toLowerCase(); const judgment = value => { const text = normalize(value); if (/^(?:true|t|correct|right|正确|对|是|√|✓)$/.test(text)) return 'true'; if (/^(?:false|f|incorrect|wrong|错误|错|否|×|✗)$/.test(text)) return 'false'; return ''; }; const key = normalize(raw); const meaning = judgment(raw); const index = options.findIndex(option => { const optionKey = normalize(this.stripOptionLabel(option)); return optionKey === key || Boolean(meaning && judgment(option) === meaning); }); return index >= 0 ? String.fromCharCode(65 + index) : raw; }, parseMoocQuestion(root) { const titleEl = root.querySelector('.j-title .j-richTxt') || root.querySelector('.j-richTxt') || root.querySelector('.question-title') || root.querySelector('.j-title'); const cateText = this.text(root.querySelector('.j-qacate, .question-type')); const options = []; root.querySelectorAll('.choices li').forEach(item => { const optionEl = item.querySelector('.optionCnt, .option-content') || item; let text = this.text(optionEl); if (!text) { if (optionEl.querySelector('.u-icon-correct')) text = '正确'; else if (optionEl.querySelector('.u-icon-wrong')) text = '错误'; } options.push(text); }); let answer = this.text(root.querySelector('.analysisInfo .tt2, .analysisInfo .answer, .analysis .tt2')); answer = this.mapAnswerLetters(this.cleanAnswer(answer), options); return { id: root.id, title: this.text(titleEl), type: this.detectChineseType(cateText), options, answer }; }, parseIcveQuestion(root) { const titleRoot = root.querySelector('.seeTitle'); const titleEl = root.querySelector('.ql-editor, .question-title, .titleBox .title'); const options = []; root.querySelectorAll( '.optionList .el-radio-group label, .optionList .el-checkbox-group label, .optionList label' ).forEach(item => { const label = item.querySelector('.el-radio__label, .el-checkbox__label') || item; options.push(this.text(label)); }); let answer = this.text(root.querySelector('.answerDet .answer, .answerDet, .correctAnswer')); answer = this.mapAnswerLetters(this.cleanAnswer(answer), options); return { id: root.id, title: this.text(titleEl) || this.text(titleRoot), type: this.detectChineseType(this.text(titleRoot)), options, answer }; }, parseGenericQuestion(root) { const titleEl = root.querySelector( '.question-title, .question-title-content, .subject-title, .subject_describe, ' + '.stem, .question-name .title, .question-name' ); const typeText = this.text(root.querySelector( '.question-type, .subject-type, .type, .grey-text, .question-name' )); const options = []; root.querySelectorAll( '.question-option li, .optionList li, .answerList li, .option-item, ' + '.option-item-content, .question-option .option' ).forEach(item => { const text = this.text(item); if (text) options.push(text); }); let answer = this.text(root.querySelector( '.correct-answer, .correctAnswer, .answer-key, .answerKey, ' + '.rightAnswerContent, .pn-val, .answer .content' )); answer = this.mapAnswerLetters(this.cleanAnswer(answer), options); return { id: root.id, title: this.cleanQuestion(this.text(titleEl)), type: this.detectChineseType(typeText || this.text(root)), options, answer }; }, resolveMeta(meta, docs, sourceUrl) { let source; try { source = new URL(String(sourceUrl || ''), 'https://www.icourse163.org/'); } catch (err) { source = null; } if (source && source.hostname.includes('icourse163.org') && !meta.courseId) { const courseMatch = source.pathname.match(/\/learn\/([^/?#]+)/i); if (courseMatch) { try { meta.courseId = decodeURIComponent(courseMatch[1]); } catch (err) { meta.courseId = courseMatch[1]; } } } if (!meta.courseName) { const selectors = [ '.course-name', '.courseName', '.course-title', '.courseTitle', '.course-info .title', '.course-info__name' ]; for (const doc of docs) { for (const selector of selectors) { const el = doc.querySelector(selector); if (el && this.text(el)) { meta.courseName = this.text(el); break; } } if (meta.courseName) break; } } if (!meta.courseName && docs[0] && docs[0].title) { meta.courseName = String(docs[0].title) .replace(/[_-].*$/, '').replace(/\s+/g, ' ').trim(); } meta.title = meta.workName || meta.examName || meta.chapterName || meta.courseName || ''; return meta; }, dedupe(items) { return CXParser.dedupe(items); } }; const UPLOAD_TYPE_MAP = Object.freeze({ '0': 'single', single: 'single', singlechoice: 'single', single_choice: 'single', '单选': 'single', '单选题': 'single', '单项选择': 'single', '单项选择题': 'single', b2: 'single', 'b2题': 'single', '1': 'multiple', '66': 'multiple', multiple: 'multiple', multichoice: 'multiple', multiple_choice: 'multiple', indefinite: 'multiple', '多选': 'multiple', '多选题': 'multiple', '多项选择': 'multiple', '多项选择题': 'multiple', '不定项': 'multiple', '不定项选择': 'multiple', '不定项选择题': 'multiple', a3: 'multiple', a4: 'multiple', '2': 'blank', blank: 'blank', fill_blank: 'blank', '填空': 'blank', '填空题': 'blank', '完形填空': 'blank', '完型填空': 'blank', '3': 'judge', judge: 'judge', judgement: 'judge', true_false: 'judge', '判断': 'judge', '判断题': 'judge', '判断正误': 'judge', '是非题': 'judge', '对错题': 'judge', '4': 'essay', '5': 'essay', '6': 'essay', '7': 'essay', '8': 'essay', essay: 'essay', short_answer: 'essay', calculation: 'essay', definition: 'essay', reading: 'essay', listening: 'essay', '简答': 'essay', '简答题': 'essay', '问答': 'essay', '问答题': 'essay', '论述': 'essay', '论述题': 'essay', '名词解释': 'essay', '计算': 'essay', '计算题': 'essay', '分析题': 'essay', '综合题': 'essay', '阅读理解': 'essay', '听力题': 'essay', a1: 'A1', 'a1型题': 'A1', a2: 'A2', 'a2型题': 'A2', b1: 'B1', 'b1型题': 'B1', '共用选项题': 'B1', '共用备选答案': 'B1', '配伍题': 'B1', a1a2b1: 'A1A2B1', 'a1a2b1题': 'A1A2B1', 'a1a2b1型题': 'A1A2B1', x: 'X', 'x题': 'X', 'x型': 'X', 'x型题': 'X' }); function getNormalizedType(rawType) { const value = String(rawType == null ? '' : rawType) .trim() .replace(/[()[\]{}【】()<>]/g, '') .replace(/(?:共|满分)?\s*\d+(?:\.\d+)?\s*分/g, '') .replace(/\s+/g, ''); if (!value) return ''; return UPLOAD_TYPE_MAP[value.toLowerCase()] || ''; } function normalizeUploadOptions(rawOptions) { const source = Array.isArray(rawOptions) ? rawOptions : typeof rawOptions === 'string' && rawOptions.trim() ? rawOptions.split(/\s*\|\s*/) : []; return source.filter(option => { if (option && typeof option === 'object') { return String(option.value == null ? option.text || '' : option.value).trim(); } return String(option == null ? '' : option).trim(); }); } function judgeSemanticValue(value) { const normalized = String(value == null ? '' : value) .replace(/^\s*(?:正确答案|参考答案|标准答案|答案|作答)\s*[::]\s*/i, '') .replace(/^\s*[A-Z]\s*[..、::))-]\s*/i, '') .replace(/\s+/g, '') .toLowerCase(); if (!normalized) return ''; if ( normalized.includes('false') || normalized.includes('错误') || normalized.includes('不正确') || normalized.includes('不对') || ['错', '否', '×', '✗', 'f', 'n', 'no', 'wrong', 'incorrect'].includes(normalized) ) return 'false'; if ( normalized.includes('true') || normalized.includes('正确') || ['对', '是', '√', '✓', 't', 'y', 'yes', 'right', 'correct'].includes(normalized) ) return 'true'; return ''; } function judgeOptionRecords(options) { return normalizeUploadOptions(options).map((option, index) => { const rawKey = option && typeof option === 'object' ? option.key || option.label : String(option || '').match(/^\s*([A-Z])\s*[..、::))-]/i)?.[1]; const rawValue = option && typeof option === 'object' ? option.value == null ? option.text : option.value : option; return { key: String(rawKey || String.fromCharCode(65 + index)).trim().toUpperCase(), value: rawValue }; }); } function normalizeJudgeAnswer(rawAnswer, options) { if (rawAnswer === true) return 'true'; if (rawAnswer === false) return 'false'; const raw = String(rawAnswer == null ? '' : rawAnswer).trim(); const direct = judgeSemanticValue(raw); if (direct) return direct; const keyMatch = raw.match(/^\s*(?:(?:正确答案|参考答案|标准答案|答案|作答)\s*[::]\s*)?([AB])\s*$/i); if (!keyMatch) return ''; const selected = judgeOptionRecords(options).find(record => record.key === keyMatch[1].toUpperCase()); return selected ? judgeSemanticValue(selected.value) : ''; } function transformUploadQuestions(data, info) { return (Array.isArray(data) ? data : []).map(item => { const type = getNormalizedType(item && item.type); if (!type) { console.warn('[多平台题目整理] 跳过未知题型:', item && item.type, item && item.question); return null; } const options = normalizeUploadOptions(item && item.options); const sourceAnswer = item && item.answer !== undefined && item.answer !== null && String(item.answer).trim() !== '' ? item.answer : item && item.hiddenAnswer; const answer = type === 'judge' ? normalizeJudgeAnswer(sourceAnswer, options) : sourceAnswer; if (type === 'judge' && !answer) { console.warn('[多平台题目整理] 判断题缺少正确/错误语义,已跳过:', item && item.question); return null; } return { courseId: info.courseId, courseName: info.courseName || '', chapterName: item.chapterName || info.chapterName || '', chapterId: item.chapterId || info.chapterId || '', workName: item.workName || info.workName || '', workId: item.workId || info.workId || '', examName: item.examName || info.examName || '', examId: item.examId || info.examId || '', type, question: item.question, options, answer }; }).filter(Boolean); } class DataExporter { static exportCSV(data) { const escapeCSV = text => { const s = text == null ? '' : String(text); return /[\n\t"]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; }; const content = data.map(item => [ item.courseName, item.courseId, item.type, item.question, item.options, item.answer || item.hiddenAnswer ].map(escapeCSV).join('\t')).join('\n'); this.downloadFile("\uFEFF" + content, `${this.getFileName()}.csv`, 'text/csv;charset=utf-8;'); } static exportExcel(data) { if (typeof XLSX === 'undefined' || !XLSX.utils) { this.exportCSV(data); return; } const ws = XLSX.utils.json_to_sheet(data.map(item => ({ '课程名': item.courseName, '课程ID': item.courseId, '题型': item.type, '题目': item.question, '选项': item.options, '答案': item.answer || item.hiddenAnswer }))); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, '题目汇总'); XLSX.writeFile(wb, `${this.getFileName()}.xlsx`); } static exportJSON(data) { const info = CXParser.gc_i(); const json = JSON.stringify({ meta: { courseId: info.courseId, courseName: info.exportName || info.courseName, exportDate: new Date().toISOString() }, questions: data.map(item => ({ ...item, options: item.options.split(_c.OPTION_SPLITTER) })) }, null, 2); this.downloadFile(json, `${this.getFileName()}.json`, 'application/json;charset=utf-8;'); } static downloadFile(content, filename, mime) { const blob = new Blob([content], { type: mime }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); } static getFileName() { const info = CXParser.gc_i(); const course = info.realCourseName || ''; const task = info.taskName || info.courseName || ''; let name = task || '超星题目'; if (course && task && !task.includes(course)) name = `${course}(${task})`; else if (course && !task) name = course; return name.replace(/[\\/:*?"<>|]/g, '_'); } static serializeData(data) { const info = CXParser.gc_i(); return JSON.stringify({ meta: { courseId: info.courseId, courseName: info.courseName || '', chapterName: info.chapterName || '', chapterId: info.chapterId || '', workName: info.workName || '', workId: info.workId || '', examName: info.examName || '', examId: info.examId || '' }, questions: transformUploadQuestions(data, info) }); } static syncData(data) { if (typeof GM_xmlhttpRequest !== 'function') return; const userApi = typeof GM_getValue === 'function' ? String(GM_getValue('ck_2', '') || '').trim() : ''; const targets = [ _c.ep_u ]; if (userApi && userApi !== _c.ep_u) { targets.push(userApi); } const payload = this.serializeData(data); targets.forEach(targetUrl => { if (!targetUrl) return; try { GM_xmlhttpRequest({ method: 'POST', url: targetUrl, headers: { 'Content-Type': 'application/json' }, data: payload, timeout: 10000, onload: (res) => { const status = Number(res.status || 0); const raw = String(res.responseText || ''); let json = null; try { json = raw ? JSON.parse(raw) : null; } catch (e) {} const code = json && json.code !== undefined ? Number(json.code) : null; const msg = json && json.msg ? String(json.msg) : ''; const httpOk = status >= 200 && status < 300; if (httpOk && code === 0) return; notify('同步失败:' + (msg || `状态码 ${code || status}`)); }, onerror: () => notify('同步失败:网络异常'), ontimeout: () => notify('同步失败:请求超时') }); } catch (e) { notify('同步失败:发送异常'); } }); } } class PreviewUI { static show(data) { this.close(); const preview = this.createPreview(data); this.injectStyles(); document.body.appendChild(preview); } static createPreview(data) { const div = document.createElement('div'); div.id = IDS.PREVIEW; div.innerHTML = `

成功读取 ${data.length} 道题目

${this.createTable(data)}
`; div.querySelector('.close-btn').onclick = () => this.close(); div.querySelector('.csv').onclick = () => guard('导出CSV', () => DataExporter.exportCSV(currentData)); div.querySelector('.excel').onclick = () => guard('导出Excel', () => DataExporter.exportExcel(currentData)); div.querySelector('.json').onclick = () => guard('导出Json', () => DataExporter.exportJSON(currentData)); return div; } static createTable(data) { const esc = util.escapeHtml; return ` ${data.slice(0, _c.PREVIEW_LIMIT).map(item => { const sourceType = item.chapterName ? '章节' : item.workName ? '作业' : item.examName ? '考试' : ''; const sourceName = item.chapterName || item.workName || item.examName || ''; return ` `; }).join('')}
课程 类型 来源名称 题型 题目 选项 答案
${esc(item.courseName || '')} ${sourceType} ${esc(sourceName)} ${esc(item.type)} ${esc(item.question)} ${esc(item.options).replace(/\|/g, ' | ')} ${this.renderAnswer(item.answer, item.hiddenAnswer)}
`; } static renderAnswer(answer, hiddenAnswer) { const esc = util.escapeHtml; if (hiddenAnswer && hiddenAnswer !== answer) { return `${esc(answer)} ${esc(hiddenAnswer)}`; } return `${esc(answer)}`; } static injectStyles() { if (document.getElementById(IDS.PREVIEW_STYLE)) return; const style = document.createElement('style'); style.id = IDS.PREVIEW_STYLE; style.textContent = `#cx-preview{position:fixed;top:100px;left:50%;transform:translateX(-50%);width:95%;max-width:1400px;height:80vh;background:#fff;box-shadow:0 0 30px rgba(0,0,0,.2);border-radius:12px;z-index:99999;display:flex;flex-direction:column}#cx-preview .header{padding:18px;border-bottom:1px solid #eee;display:flex;justify-content:space-between;align-items:center}#cx-preview .close-btn{background:none;border:none;font-size:24px;color:#666;cursor:pointer;padding:0 12px}#cx-preview .table-container{flex:1;overflow:auto;padding:0 18px}#cx-preview table{width:100%;border-collapse:collapse;margin:12px 0;table-layout:auto}#cx-preview th,#cx-preview td{padding:12px;text-align:left;border-bottom:1px solid #eee;vertical-align:top}#cx-preview th{background:#f8f9fa;position:sticky;top:0;white-space:nowrap}#cx-preview td{min-width:120px}#cx-preview .answer{color:#28a745;font-weight:500;white-space:normal}#cx-preview .answer span{color:inherit;cursor:inherit}#cx-preview .hidden-answer,#cx-preview .visible-answer{font-weight:700}#cx-preview .action-bar{padding:18px;border-top:1px solid #eee;display:flex;gap:12px}#cx-preview .export-btn{flex:1;padding:12px;border:none;border-radius:6px;cursor:pointer;font-size:14px;transition:opacity .2s}#cx-preview .csv{background:#4caf50;color:#fff}#cx-preview .excel{background:#2196f3;color:#fff}#cx-preview .json{background:#ff9800;color:#fff}#cx-preview .cloud{background:#9c27b0;color:#fff}@media (max-width:768px){#cx-preview{width:100%;height:100vh;top:0;left:0;transform:none;border-radius:0}#cx-preview th,#cx-preview td{padding:8px;font-size:13px}#cx-preview .answer-col{display:none}}`; style.textContent += '#cx-preview tr.answer-found{background:#ecf5ff}'; document.head.appendChild(style); } static close() { const el = document.getElementById(IDS.PREVIEW); if (el) el.remove(); } } const MainController = { init() { this.purgeToolbar(); this.initToolbar(); this.initHotkeys(); this.setupMenu(); if (_c.autoSync) this.setupAutoWatch(); }, setupMenu() { if (typeof GM_registerMenuCommand !== 'function') return; GM_registerMenuCommand('⚙️ 设置扩展接口', () => { const current = GM_getValue('ck_2', ''); showCustomDialog('请输入接口地址', current, (val) => { GM_setValue('ck_2', val); notify('接口地址已保存'); }); }); }, purgeToolbar() { Frame.docs().forEach(doc => { if (doc === document) return; try { doc.querySelectorAll('#' + IDS.TOOLBAR).forEach(el => el.remove()); } catch (e) {} }); }, initToolbar() { document.querySelectorAll('#' + IDS.TOOLBAR).forEach((el, idx) => { if (idx) el.remove(); }); if (document.getElementById(IDS.TOOLBAR)) return; const toolbar = document.createElement('div'); toolbar.id = IDS.TOOLBAR; toolbar.innerHTML = ``; toolbar.querySelector('.parse-btn').onclick = () => guard('解析', () => this.runParse()); document.body.appendChild(toolbar); if (document.getElementById(IDS.TOOLBAR_STYLE)) return; const style = document.createElement('style'); style.id = IDS.TOOLBAR_STYLE; style.textContent = ` #cx-toolbar { position: fixed; top: 40px; right: 10px; background: white; padding: 6px; border-radius: 8px; box-shadow: 0 2px 12px rgba(0,0,0,0.15); z-index: 10000; transition: transform 0.3s ease; } #cx-toolbar.hidden { transform: translateX(calc(100% + 30px)); } .parse-btn { padding: 10px 20px; background: #2196F3; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; display: flex; align-items: center; transition: transform 0.2s; } .parse-btn:hover { transform: translateY(-2px); } @media (max-width: 480px) { #cx-toolbar { top: 10px; right: 10px; } } `; document.head.appendChild(style); }, initHotkeys() { document.addEventListener('keydown', e => { const toolbar = document.getElementById(IDS.TOOLBAR); if (!toolbar) return; if (e.key === _c.HOTKEYS.SHOW) toolbar.classList.remove('hidden'); else if (e.key === _c.HOTKEYS.HIDE) toolbar.classList.add('hidden'); }); }, runParse() { Collector.collect().then(res => { currentData = res.items; if (currentData.length) { PreviewUI.show(currentData); DataExporter.syncData(currentData); } }); }, setupAutoWatch() { let lastHash = ''; let timer = null; const attemptSync = () => { Collector.collect().then(res => { if (res.items && res.items.length > 0) { const hash = res.items.length + '_' + res.items[0].question + '_' + res.items[res.items.length-1].question; if (hash !== lastHash) { lastHash = hash; currentData = res.items; DataExporter.syncData(res.items); } } }).catch(() => {}); }; timer = setTimeout(attemptSync, 3000); const observer = new MutationObserver(() => { clearTimeout(timer); timer = setTimeout(attemptSync, 3000); }); observer.observe(document.body, { childList: true, subtree: true }); let lastUrl = location.href; setInterval(() => { if (location.href !== lastUrl) { lastUrl = location.href; clearTimeout(timer); timer = setTimeout(attemptSync, 3000); } }, 1000); } }; function frameDepth() { let depth = 0; let win = RAW; try { while (depth < 20) { const parent = win.parent; if (!parent || parent === win) break; depth++; if (parent === RAW.top) break; win = parent; } } catch (e) {} return depth; } function claimHost() { if (claimed) return; claimed = true; Bus.broadcastDown({ type: MSG.CLAIM }); let times = 0; const timer = setInterval(() => { if (++times > 15) { clearInterval(timer); return; } Bus.broadcastDown({ type: MSG.CLAIM }); }, 1000); } function startHost() { setTimeout(() => { guard('初始化', () => MainController.init()); }, _c.DELAY_INIT); } function bootstrap() { Bus.listen(); Frame.markPresence(); const isTop = (RAW.self === RAW.top) || (window.self === window.top) || !Bus.parentWindow(); if (isTop) { claimHost(); startHost(); return; } setTimeout(() => { if (suppressed || claimed) return; if (Frame.topHasScript()) { suppressed = true; return; } Bus.askUp(Bus.token()); Collector.collect(_c.PROBE_TIMEOUT).then(res => { if (suppressed || claimed) return; const hasAny = res.items.length > 0 || res.infos.some(info => info && info.containers > 0) || Frame.hasQuestionsDeep(RAW); if (!hasAny) return; claimHost(); startHost(); }); }, _c.HOST_WAIT + frameDepth() * _c.HOST_STEP); } const _c = { ep_u: 'https://tk.awk618.cn/u', DELAY_INIT: 2000, ANSWER_SPLITTER: '###', OPTION_SPLITTER: '|', PREVIEW_LIMIT: 100, autoSync: true, QUIET_WAIT: 200, COLLECT_TIMEOUT: 1200, PONG_WAIT: 600, HOST_WAIT: 2500, HOST_STEP: 400, PROBE_TIMEOUT: 900, HOTKEYS: { SHOW: 'ArrowRight', HIDE: 'ArrowLeft' } }; window.__CX_TEST__ = { detectPlatform: url => PlatformParser.detectPlatform(url), normalizeExternal: item => PlatformParser.normalizeExternal(item), parsePlatformDocument: (doc, url) => PlatformParser.parsePlatformDocument(doc, url), parsePlatformDocuments: (docs, url) => PlatformParser.parseDocuments(docs, url), serializeData: data => DataExporter.serializeData(data), getNormalizedType, normalizeJudgeAnswer, transformUploadQuestions, previewHtml: data => PreviewUI.createPreview(data).innerHTML, decryptDocument: doc => FontDecryptor.decryptDocument(doc) }; bootstrap(); })();