// ==UserScript==
// @name 超星学习通+AI自动答题脚本(支持考试,作业,章节测试,支持新版学习通,一键最小化,多种AI)
// @namespace http://tampermonkey.net/
// @version 2.1
// @description 超星学习通+ChatGPT自动答题脚本(支持考试和作业和章节测试,支持新版学习通)。直接获取到答案,可以复制答案到剪切板。
// @author Cwyu
// @license GPL3
// @supportURL 1441577495@qq.com
// @contributionURL https://ifdian.net/a/cwyuu
// @match *://mooc1.chaoxing.com/exam-ans/mooc2/exam/preview?*
// @match *://mooc1.chaoxing.com/mooc2/work/dowork?*
// @match *://mooc1.chaoxing.com/mycourse/studentstudy?*
// @match *://mooc1.chaoxing.com/mycourse/studentstudy*
// @match *://mooc1.chaoxing.com/mooc-ans/mooc2/work/dowork*
// @icon https://www.google.com/s2/favicons?sz=64&domain=chaoxing.com
// @grant unsafeWindow
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
GM_addStyle(`
#my-window {
position: fixed;
top: 10px;
left: 10px;
width: 500px;
height: 400px;
background-color: rgba(255, 255, 255, 0.95);
border: 1px solid #e2e8f0;
border-radius: 8px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
z-index: 9999;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
resize: both;
min-width: 300px;
min-height: 200px;
}
#my-window .header {
background-color: #1a202c;
color: white;
padding: 8px 12px;
font-size: 14px;
font-weight: 600;
border-radius: 8px 8px 0 0;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
}
#my-window .xxt-content {
padding: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
width: auto;
min-height: 0;
}
#my-window .question-section,
#my-window .answer-section {
background-color: #fff;
border: 1px solid #e2e8f0;
padding: 12px;
border-radius: 4px;
margin-bottom: 8px;
word-break: break-word;
overflow-wrap: break-word;
}
#my-window .answer-section {
flex: 1;
overflow-y: auto;
}
#my-window .controls {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 10px;
}
#my-window .settings-row {
display: flex;
align-items: center;
gap: 12px;
}
#my-window .button-row {
display: flex;
gap: 8px;
}
#my-window button {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
border: none;
background: #e2e8f0;
transition: background-color 0.2s;
}
#my-window button:hover {
background: #cbd5e1;
}
#my-window button#refresh-btn {
background: #3b82f6;
color: white;
}
#my-window button#refresh-btn:hover {
background: #2563eb;
}
#my-window button#copy-btn {
background: #10b981;
color: white;
}
#my-window button#copy-btn:hover {
background: #059669;
}
#my-window select {
padding: 4px;
border-radius: 4px;
font-size: 12px;
border: 1px solid #e2e8f0;
flex: 1;
max-width: 68%;
}
#my-window .mcheckbox {
min-width:65px
}
#my-window input[type="text"] {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
border: 1px solid #e2e8f0;
flex: 1;
}
#floating-button {
position: fixed;
top: 20px;
left: -45px;
width: 50px;
height: 50px;
background-color: #1a202c;
color: white;
border-radius: 25px;
text-align: center;
line-height: 50px;
cursor: pointer;
transition: all 0.3s ease;
z-index: 9999;
font-size: 12px;
}
#floating-button:hover {
left: 0;
}
#my-window .label {
color: #64748b;
font-size: 12px;
margin-bottom: 4px;
}
#my-window .subscription-row {
display: flex;
gap: 8px;
width: 100%;
}
#my-window .subscription-row input {
flex: 1;
}
#my-window .subscription-row button {
white-space: nowrap;
}
#my-window button svg {
display: inline-block;
vertical-align: middle;
margin-right: 4px;
}
#my-window #prev-btn svg,
#my-window #next-btn svg {
margin-right: 0;
}
#my-window .status-info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
border-top: 1px solid #e2e8f0;
font-size: 12px;
color: #64748b;
}
.features-section {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
}
.feature-toggles {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.feature-toggle {
display: flex;
align-items: center;
gap: 4px;
position: relative;
cursor: pointer;
}
.feature-toggle input[type="checkbox"] {
margin: 0;
}
.toggle-label {
font-size: 12px;
color: #1a202c;
}
.tooltip {
display: none;
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
background: #1a202c;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
margin-left: 8px;
z-index: 10000;
}
.feature-toggle:hover .tooltip {
display: block;
}
`);
const windowHTML = `
<div id="my-window">
<div class="header">
<span>AI答题(Alt+Z 快速隐藏)</span>
<button id="hide-btn">×</button>
</div>
<div class="xxt-content">
<div class="question-section">
<div class="label">当前题目:</div>
<div id="question"></div>
</div>
<div class="answer-section">
<div class="label">答案:</div>
<div id="answer"></div>
</div>
<div class="controls">
<div class="settings-row">
<label class="mcheckbox"><input type="checkbox" id="use-tiku" checked> 使用题库</label>
<label class="mcheckbox"><input type="checkbox" id="need-analysis"> 需要解析</label>
<select id="ai-model">
<option value="">加载中...</option>
</select>
</div>
<div class="button-row">
<button id="prev-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<button id="next-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 18l6-6-6-6" />
</svg>
</button>
<button id="refresh-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
class="mr-1">
<path d="M21 12a9 9 0 11-9-9 9 9 0 019 9z" />
<path d="M9 12l2 2 4-4" />
</svg>
获取答案
</button>
<button id="copy-btn">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
class="mr-1">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
</svg>
复制答案
</button>
</div>
<div class="subscription-row">
<input type="text" id="subscription-key" placeholder="请输入订阅链接">
<button id="save-btn">保存订阅</button>
</div>
<div class="features-section">
<div class="label">功能开关:</div>
<div class="feature-toggles">
<label class="feature-toggle">
<input type="checkbox" id="paste-toggle" checked>
<span class="toggle-label">允许粘贴</span>
<span class="tooltip">启用后可以在页面中自由粘贴内容</span>
</label>
<label class="feature-toggle">
<input type="checkbox" id="copy-toggle" checked>
<span class="toggle-label">光标处复制</span>
<span class="tooltip">启用后可以使用Ctrl+C复制光标位置处的文本</span>
</label>
<label class="feature-toggle">
<input type="checkbox" id="watermark-toggle" checked>
<span class="toggle-label">移除水印</span>
<span class="tooltip">移除页面上的水印遮罩</span>
</label>
</div>
</div>
<div class="status-info-row">
<div class="status" id="card_status">状态内容</div>
<div class="info">by: cwyu</div>
</div>
</div>
</div>
</div>
<div id="floating-button">显示</div>
`;
document.body.insertAdjacentHTML('beforeend', windowHTML);
const myWindow = document.getElementById('my-window');
const floatingButton = document.getElementById('floating-button');
const hideBtn = document.getElementById('hide-btn');
const header = document.querySelector('.header');
const answerEl = document.getElementById('answer');
const questionEl = document.getElementById('question');
const subscriptionKey = document.getElementById('subscription-key');
let answerCache = {};
let currentIndex = 0;
let timu = ["正在获取题目"];
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
header.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
document.addEventListener('keydown', function(e) {
if (e.altKey && e.key.toLowerCase() === 'z') {
toggleWindow();
}
});
hideBtn.addEventListener('click', toggleWindow);
floatingButton.addEventListener('click', toggleWindow);
document.getElementById('prev-btn').addEventListener('click', () => {
if (currentIndex > 0) {
currentIndex--;
updateDisplay(currentIndex);
}
});
document.getElementById('next-btn').addEventListener('click', () => {
if (currentIndex < timu.length - 1) {
currentIndex++;
updateDisplay(currentIndex);
}
});
document.getElementById('refresh-btn').addEventListener('click', fetchAnswer);
document.getElementById('copy-btn').addEventListener('click', () => {
const answerText = answerEl.textContent;
navigator.clipboard.writeText(answerText)
.catch(err => console.error('复制失败:', err));
});
document.getElementById('save-btn').addEventListener('click', saveSubscriptionKey);
questionEl.addEventListener("dblclick", editQuestion);
async function loadModels() {
try {
const response = await fetch("https://xxt.uycc.xyz/api/v1/models");
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
const selectElement = document.getElementById("ai-model");
selectElement.innerHTML = "";
data.models.forEach((model) => {
const option = document.createElement("option");
option.value = model.modelName;
option.textContent = model.title;
selectElement.appendChild(option);
});
} catch (error) {
console.error("Failed to load models:", error);
const selectElement = document.getElementById("ai-model");
selectElement.innerHTML = '<option value="">加载模型失败</option>';
}
}
loadModels();
const savedKey = GM_getValue("subscription_key");
if (savedKey) {
subscriptionKey.value = savedKey;
checkSerial(savedKey);
}
floatingButton.style.display = "none";
function initializeFeatures() {
const features = {
paste: GM_getValue('feature_paste', true),
copy: GM_getValue('feature_copy', true),
watermark: GM_getValue('feature_watermark', false)
};
document.getElementById('paste-toggle').checked = features.paste;
document.getElementById('copy-toggle').checked = features.copy;
document.getElementById('watermark-toggle').checked = features.watermark;
applyFeatures(features);
document.getElementById('paste-toggle').addEventListener('change', (e) => {
features.paste = e.target.checked;
GM_setValue('feature_paste', features.paste);
applyFeatures(features);
});
document.getElementById('copy-toggle').addEventListener('change', (e) => {
features.copy = e.target.checked;
GM_setValue('feature_copy', features.copy);
applyFeatures(features);
});
document.getElementById('watermark-toggle').addEventListener('change', (e) => {
features.watermark = e.target.checked;
GM_setValue('feature_watermark', features.watermark);
applyFeatures(features);
});
}
function applyFeatures(features) {
if (features.paste) {
enablePasteFeature();
}
if (features.copy) {
enableCopyFeature();
} else {
disableCopyFeature();
}
if (features.watermark) {
removeWatermarks();
} else {
restoreWatermarks();
}
}
function enablePasteFeature() {
let script = document.createElement('script');
script.textContent = `
function editorPaste() {
return true;
}
function _0x4b84e4() {
return;
}
`;
(document.head || document.documentElement).appendChild(script);
script.remove();
}
let copyFeatureEnabled = false;
let mouseMoveListener = null;
let keydownListener = null;
function enableCopyFeature() {
if (copyFeatureEnabled) return;
let mouseX = 0;
let mouseY = 0;
mouseMoveListener = function(e) {
mouseX = e.clientX;
mouseY = e.clientY;
};
keydownListener = function(e) {
if (e.ctrlKey && e.key === 'c') {
let element = document.elementFromPoint(mouseX, mouseY);
if (element) {
let text = element.innerText;
copyToClipboard(text);
}
}
};
document.addEventListener('mousemove', mouseMoveListener);
document.addEventListener('keydown', keydownListener);
document.body.onselectstart = function() { return true; };
copyFeatureEnabled = true;
}
function disableCopyFeature() {
if (!copyFeatureEnabled) return;
if (mouseMoveListener) {
document.removeEventListener('mousemove', mouseMoveListener);
}
if (keydownListener) {
document.removeEventListener('keydown', keydownListener);
}
document.body.onselectstart = null;
copyFeatureEnabled = false;
}
function copyToClipboard(text) {
let textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
textarea.remove();
}
function removeWatermarks() {
const watermarks = document.querySelectorAll('.mask_div');
watermarks.forEach(watermark => {
watermark.style.display = 'none';
});
}
function restoreWatermarks() {
const watermarks = document.querySelectorAll('.mask_div');
watermarks.forEach(watermark => {
watermark.style.display = '';
});
}
if (document.title === "学生学习页面") {
queryElements();
}
if (document.title == "作业作答") {
const divs = document.querySelectorAll('.padBom50.questionLi:not(script)');
const texts = [];
divs.forEach(div => {
const text = div.textContent.trim().replace(/\s+/g, ' ');
texts.push(text);
});
timu = texts.map((text) =>
text.replace(/var\s+wordNum\s*=.*$/gm, "").trim()
);
updateDisplay(0);
}
if (document.title == "整卷预览") {
initializeFeatures();
const elements = document.querySelectorAll('div.questionLi:not(script)');
const texts = [];
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const text = element.textContent.trim().replace(/\s+/g, ' ');
texts.push(text);
}
timu = texts.map((text) =>
text.replace(/window\.UEDITOR_CONFIG\.initialFrameWidth.*保存/g, "").trim()
);
updateDisplay(0);
}
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
const content = myWindow.querySelector('.xxt-content');
if (content) {
content.style.height = `${height - 40}px`;
}
}
});
resizeObserver.observe(myWindow);
async function fetchAnswer() {
const currentQuestion = timu[currentIndex];
answerEl.textContent = "获取答案中...";
const data = {
serial: subscriptionKey.value,
question: currentQuestion,
model: document.getElementById('ai-model').value,
use_tiku: document.getElementById('use-tiku').checked,
have_analyze: document.getElementById('need-analysis').checked
};
try {
const response = await fetch('https://xxt.uycc.xyz/api/v1/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const result = await response.json();
if (result.answer) {
answerCache[currentIndex] = result.answer;
answerEl.textContent = result.answer;
if (result.cost===0) {
document.getElementById('card_status').textContent =
`题库中包含此题,不花费token`;
}
if (result.cost && result.balances) {
document.getElementById('card_status').textContent =
`本次花费: ${result.cost} | 剩余余额: ${result.balances}`;
}
}
} catch (error) {
console.error('Search API error:', error);
try {
const errorResponse = await error.response?.json();
answerEl.textContent = errorResponse?.detail || "请求失败,请检查网络连接";
} catch (e) {
answerEl.textContent = "请求失败,请检查网络连接";
}
}
}
function updateDisplay(index) {
questionEl.textContent = timu[index];
answerEl.textContent = answerCache[index] || '';
}
async function queryElements() {
const iframe = document.getElementById('iframe');
if (!iframe) {
setTimeout(queryElements, 3000);
return;
}
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
const second = iframeDoc.querySelector('iframe');
if (!second) {
setTimeout(queryElements, 3000);
return;
}
const secondDoc = second.contentDocument || second.contentWindow.document;
const third = secondDoc.getElementById('frame_content');
if (!third) {
setTimeout(queryElements, 3000);
return;
}
const thirdDoc = third.contentDocument || third.contentWindow.document;
const elements = thirdDoc.querySelectorAll('div.TiMu:not(script)');
if (elements.length > 0) {
const texts = [];
elements.forEach(element => {
const text = element.textContent.trim().replace(/\s+/g, ' ');
texts.push(text);
});
const styles = thirdDoc.querySelectorAll('style[type="text/css"]');
let fontBase64 = null;
for (const style of styles) {
const content = style.textContent;
const match = content.match(/data:application\/font-ttf;charset=utf-8;base64,([^'")]+)/);
if (match && match[1]) {
fontBase64 = match[1];
break;
}
}
timu = texts.map((text) =>
text
.replace(/var\s+wordNum\s*=.*$/gm, "")
.replace(/点击上传x[^}]*}/gm, "")
.replace(/填写答案[^x]*x/gm, "")
.trim()
);
await decrypt(fontBase64);
} else {
setTimeout(queryElements, 3000);
}
}
async function decrypt(b64) {
const data = {
text: timu,
b64: b64
};
try {
const response = await fetch("https://xxt.uycc.xyz/api/v1/decrypt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const result = await response.json();
timu = result.text;
updateDisplay(0);
} catch (error) {
console.error('Decrypt API error:', error);
answerEl.textContent = "解密失败,请稍后重试";
}
}
function dragStart(e) {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
if (e.target === header) {
isDragging = true;
}
}
function drag(e) {
if (isDragging) {
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
myWindow.style.transform = `translate(${currentX}px, ${currentY}px)`;
}
}
function dragEnd() {
isDragging = false;
}
function toggleWindow() {
if (myWindow.style.display === 'none') {
myWindow.style.display = 'block';
floatingButton.style.display = 'none';
} else {
myWindow.style.display = 'none';
floatingButton.style.display = 'block';
}
}
async function saveSubscriptionKey() {
const key = subscriptionKey.value;
GM_setValue('subscription_key', key);
const data = { serial: key };
try {
const response = await fetch('https://xxt.uycc.xyz/api/v1/serial/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const result = await response.json();
document.getElementById('card_status').textContent = "可用的token余额:" + result.balances;
} catch (error) {
console.error('Serial check API error:', error);
document.getElementById('card_status').textContent = "卡密验证失败,请检查卡密是否正确";
}
}
async function checkSerial(serial) {
const data = { serial: serial };
try {
const response = await fetch('https://xxt.uycc.xyz/api/v1/serial/check', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const result = await response.json();
document.getElementById('card_status').textContent = "可用的token余额:" + result.balances;
} catch (error) {
console.error('Serial check API error:', error);
document.getElementById('card_status').textContent = "卡密验证失败,请检查卡密是否正确";
}
}
function editQuestion(e) {
e.preventDefault();
const editableDiv = document.createElement('div');
editableDiv.setAttribute('contenteditable', true);
editableDiv.textContent = questionEl.textContent;
editableDiv.style.background = '#fff';
editableDiv.style.padding = '4px';
editableDiv.style.border = '1px solid #3b82f6';
editableDiv.style.borderRadius = '4px';
editableDiv.style.minHeight = '50px';
questionEl.replaceWith(editableDiv);
editableDiv.focus();
function saveEdit() {
timu[currentIndex] = editableDiv.textContent;
questionEl.textContent = editableDiv.textContent;
editableDiv.replaceWith(questionEl);
}
editableDiv.addEventListener('blur', saveEdit);
editableDiv.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
saveEdit();
}
});
}
})();