// ==UserScript==
// @name 多平台题目整理工具
// @namespace 题目整理--支持超星、智慧树、Mooc
// @version 2.8.1
// @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 = `
`;
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);
}
};
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: data.map(item => ({
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: item.type,
question: item.question,
options: item.options.split(_c.OPTION_SPLITTER),
answer: item.answer || item.hiddenAnswer
}))
});
}
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 = `
${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 `
| ${esc(item.courseName || '')} |
${sourceType} |
${esc(sourceName)} |
${esc(item.type)} |
${esc(item.question)} |
${esc(item.options).replace(/\|/g, ' | ')} |
${this.renderAnswer(item.answer, item.hiddenAnswer)} |
`;
}).join('')}
`;
}
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),
previewHtml: data => PreviewUI.createPreview(data).innerHTML,
decryptDocument: doc => FontDecryptor.decryptDocument(doc)
};
bootstrap();
})();