// ==UserScript==
// @name U助手-自用本地版
// @namespace http://tampermonkey.net/
// @version 1.0
// @description 解决U校园的刷题烦恼(使用硅基流动API)
// @author Gemini
// @match *://ucontent.unipus.cn/*
// @grant GM_xmlhttpRequest
// @connect api.siliconflow.cn
// @connect *
// ==/UserScript==
// --- (1) 用户需要修改的配置 ---
const SILICONFLOW_API_KEY = "sk-vmkgibbrlnmyfgofxilejrulypmephmidvcyfwgijabxqyny"; // <--- 在这里填入你的硅基流动API Key
const SILICONFLOW_API_ENDPOINT = "https://api.siliconflow.cn/v1/chat/completions"; // <--- 确认这是正确的API端点
const SILICONFLOW_MODEL_NAME = "deepseek-ai/DeepSeek-V3.2-Exp"; // <--- 在这里填入你想使用的模型名称
// --- 结束配置 ---
let loadedQuestionBank = null;
let currentTopicUsedAnswers = new Set();
let lastActiveTopicName = '';
// 移除所有付费和积分相关的功能,默认返回true以通过校验
async function checkAndDeductPoints() {
console.log("付费功能已移除,积分检查已跳过。");
return true;
}
let useKimiAI = localStorage.getItem('useKimiAI') === 'true'; // 变量名保留,避免用户设置丢失
const SYSTEM_PROMPT = `你是一个专业的英语教学助手,擅长分析英语题目。请注意:
1. 阅读整个页面的所有题目和选项
2. 给出每道题的答案,格式为:1.A, 2.B 这样的形式
3. 只使用实际存在的选项字母
4. 理解题目类型,特别是"not collocate"类型表示选择不正确的搭配
5. 确保答案数量与题目数量一致
6. 如果是填空题,直接给出单词或短语答案
7. 如果遇到不确定的答案,说明原因并给出最可能的选项`;
async function askAI(question, retryCount = 3, retryDelay = 1000) {
if (!useKimiAI) {
alert('请先开启AI功能');
return null;
}
if (SILICONFLOW_API_KEY === "YOUR_API_KEY_HERE") {
alert('错误:请先在脚本代码中填入您的硅基流动API Key!');
return null;
}
for (let attempt = 1; attempt <= retryCount; attempt++) {
try {
console.log(`正在调用硅基流动 API... (第${attempt}次尝试)`);
const messages = [
{
role: "system",
content: SYSTEM_PROMPT
},
{
role: "user",
content: question
}
];
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: SILICONFLOW_API_ENDPOINT,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${SILICONFLOW_API_KEY}`
},
data: JSON.stringify({
model: SILICONFLOW_MODEL_NAME,
messages: messages,
temperature: 0.6
}),
timeout: 30000,
onload: function(response) {
console.log('硅基流动 API响应状态:', response.status);
console.log('硅基流动 API响应体:', response.responseText);
resolve(response);
},
onerror: function(error) {
console.error('硅基流动 API请求错误:', error);
reject(error);
},
ontimeout: function() {
console.error('硅基流动 API请求超时');
reject(new Error('请求超时'));
}
});
});
if (response.status >= 200 && response.status < 300 && response.responseText) {
try {
const data = JSON.parse(response.responseText);
console.log('硅基流动 API响应:', data);
if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) {
return data.choices[0].message.content;
} else {
throw new Error('API响应格式不正确');
}
} catch (error) {
console.error('解析API响应失败:', error);
if (attempt === retryCount) throw error;
}
} else {
let errorMessage = `API请求失败 (HTTP ${response.status})`;
try {
if (response.responseText) {
const errorData = JSON.parse(response.responseText);
errorMessage += `: ${JSON.stringify(errorData)}`;
}
} catch (e) {
// Ignore parsing error
}
console.error(errorMessage);
if (attempt === retryCount) {
console.log('所有重试都失败,使用本地分析替代API响应');
return await analyzeQuestionLocally(question);
}
}
if (attempt < retryCount) {
const delay = retryDelay * Math.pow(2, attempt - 1);
console.log(`等待 ${delay}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
} catch (error) {
console.error(`第${attempt}次尝试失败:`, error);
if (attempt === retryCount) {
console.log('所有重试都失败,使用本地分析替代API响应');
return await analyzeQuestionLocally(question);
}
const delay = retryDelay * Math.pow(2, attempt - 1);
console.log(`等待 ${delay}ms 后重试...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
return null;
}
async function analyzeQuestionLocally(question) {
console.log('开始本地分析题目');
const questionLines = question.split('\n');
const questionText = questionLines.find(line =>
line.includes('Identify') ||
line.includes('Choose') ||
line.includes('Select') ||
line.includes('which')
) || questionLines[0];
console.log('题目文本:', questionText);
const options = [];
let optionLetters = '';
questionLines.forEach(line => {
const optionMatch = line.match(/^([A-D])[\.:\)\s]\s*(.+)$/);
if (optionMatch) {
const [, letter, text] = optionMatch;
options.push({ letter, text });
console.log(`选项 ${letter}: ${text}`);
}
});
let simulatedAnswer = '';
if (questionText.includes('not collocate') ||
questionText.includes('do not') ||
questionText.includes('incorrect') ||
questionText.includes('wrong') ||
questionText.includes('false')) {
console.log('识别为"查找不正确"类型题目');
simulatedAnswer = 'B';
optionLetters = 'B';
} else if (questionText.includes('collocate') ||
questionText.includes('match') ||
questionText.includes('pair') ||
questionText.includes('correct') ||
questionText.includes('true')) {
console.log('识别为"查找正确"类型题目');
simulatedAnswer = 'A, C, D';
optionLetters = 'ACD';
} else {
console.log('无法确定题目类型,返回所有选项');
optionLetters = options.map(opt => opt.letter).join(', ');
simulatedAnswer = optionLetters;
}
console.log('本地分析生成的答案:', simulatedAnswer);
return simulatedAnswer;
}
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function createCollapsibleSection(title, storageKey) {
const section = document.createElement('div');
section.className = 'u-helper-section u-collapsible';
const header = document.createElement('div');
header.className = 'u-helper-section-header';
const titleEl = document.createElement('div');
titleEl.className = 'u-helper-section-title';
titleEl.textContent = title;
const icon = document.createElement('div');
icon.className = 'u-collapse-icon';
icon.innerHTML = '';
const content = document.createElement('div');
content.className = 'u-helper-section-content';
header.appendChild(titleEl);
header.appendChild(icon);
section.appendChild(header);
section.appendChild(content);
const isCollapsed = localStorage.getItem(storageKey) === 'true';
if (isCollapsed) {
section.classList.add('u-collapsed');
}
header.addEventListener('click', () => {
const collapsed = section.classList.toggle('u-collapsed');
localStorage.setItem(storageKey, collapsed);
});
return { section, content };
}
function createFloatingButton() {
const styles = `
:root {
--u-bg-color: rgba(255, 255, 255, 0.75);
--u-blur-bg-color: rgba(255, 255, 255, 0.5);
--u-border-color: rgba(0, 0, 0, 0.08);
--u-text-color-primary: #1a202c;
--u-text-color-secondary: #4a5568;
--u-text-color-tertiary: #718096;
--u-accent-color: #4299e1;
--u-accent-color-dark: #3182ce;
--u-success-color: #38a169;
--u-danger-color: #e53e3e;
--u-warning-color: #dd6b20;
--u-font-family: 'Segoe UI', 'Roboto', 'Helvetica Neue', Arial, sans-serif;
--u-border-radius-lg: 16px;
--u-border-radius-md: 12px;
--u-border-radius-sm: 8px;
--u-shadow-lg: 0 10px 25px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--u-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.u-helper-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
display: flex;
flex-direction: column;
font-family: var(--u-font-family);
background: var(--u-bg-color);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border-radius: var(--u-border-radius-lg);
border: 1px solid rgba(255, 255, 255, 0.6);
box-shadow: var(--u-shadow-lg);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
width: 320px;
opacity: 0;
transform: translateY(-20px) scale(0.95);
color: var(--u-text-color-primary);
}
.u-helper-container.u-visible {
opacity: 1;
transform: translateY(0) scale(1);
}
.u-helper-title-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: var(--u-blur-bg-color);
font-weight: 600;
cursor: move;
user-select: none;
font-size: 15px;
border-bottom: 1px solid var(--u-border-color);
flex-shrink: 0;
}
.u-helper-title {
display: flex;
align-items: center;
gap: 10px;
}
.u-helper-control-btn {
background: transparent;
border: none;
color: var(--u-text-color-secondary);
font-size: 20px;
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
height: 28px;
width: 28px;
border-radius: var(--u-border-radius-sm);
transition: all 0.2s ease-in-out;
}
.u-helper-control-btn:hover {
background-color: rgba(0, 0, 0, 0.08);
transform: scale(1.1);
}
.u-helper-content {
display: flex;
flex-direction: column;
gap: 16px;
padding: 16px;
width: 100%;
box-sizing: border-box;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow-y: auto;
max-height: calc(100vh - 150px);
}
.u-helper-section {
display: flex;
flex-direction: column;
padding: 12px;
background: rgba(255, 255, 255, 0.6);
border-radius: var(--u-border-radius-md);
}
.u-helper-section-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
}
.u-helper-section.u-collapsible > .u-helper-section-header {
cursor: pointer;
margin: -12px;
padding: 12px;
}
.u-helper-section-content {
padding-top: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.u-collapse-icon {
transition: transform 0.2s ease-in-out;
color: var(--u-text-color-tertiary);
}
.u-collapsible.u-collapsed .u-collapse-icon {
transform: rotate(-90deg);
}
.u-collapsible.u-collapsed .u-helper-section-content {
display: none;
}
.u-helper-section-title {
font-size: 13px;
color: var(--u-text-color-primary);
font-weight: 600;
}
.u-helper-input-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.u-helper-label {
font-size: 13px;
color: var(--u-text-color-secondary);
}
.u-helper-input {
width: 75px;
padding: 6px 8px;
border-radius: var(--u-border-radius-sm);
border: 1px solid var(--u-border-color);
text-align: center;
background-color: #fff;
font-size: 13px;
transition: all 0.2s;
box-shadow: var(--u-shadow-md);
}
.u-helper-input:focus {
outline: none;
border-color: var(--u-accent-color);
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
}
.u-helper-file-upload-btn {
padding: 10px 16px;
border: 1px solid transparent;
border-radius: var(--u-border-radius-md);
font-size: 14px;
font-weight: 500;
color: var(--u-accent-color-dark);
background: rgba(66, 153, 225, 0.1);
cursor: pointer;
text-align: center;
transition: all 0.3s ease;
width: 100%;
box-sizing: border-box;
}
.u-helper-file-upload-btn:hover {
background: rgba(66, 153, 225, 0.2);
border-color: rgba(66, 153, 225, 0.3);
transform: translateY(-2px);
box-shadow: var(--u-shadow-md);
}
.u-helper-select-group {
display: flex;
align-items: center;
gap: 8px;
}
.u-helper-select {
flex-grow: 1;
padding: 8px 12px;
border: 1px solid var(--u-border-color);
border-radius: var(--u-border-radius-md);
font-size: 13px;
background-color: #fff;
cursor: pointer;
width: 100%;
min-width: 0;
box-shadow: var(--u-shadow-md);
transition: all 0.2s;
}
.u-helper-select:focus {
outline: none;
border-color: var(--u-accent-color);
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
}
.u-helper-delete-btn {
background: rgba(0, 0, 0, 0.05);
color: var(--u-text-color-tertiary);
border: none;
border-radius: 50%;
width: 28px;
height: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
display: none;
}
.u-helper-delete-btn:hover {
background-color: var(--u-danger-color);
color: white;
transform: scale(1.1) rotate(90deg);
}
.u-helper-btn {
padding: 10px 16px;
border: none;
border-radius: var(--u-border-radius-md);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease-in-out;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
box-sizing: border-box;
box-shadow: var(--u-shadow-md);
}
.u-helper-btn:hover {
transform: translateY(-2px);
box-shadow: var(--u-shadow-lg);
}
.u-helper-btn:active {
transform: translateY(0) scale(0.98);
box-shadow: var(--u-shadow-md);
}
.u-helper-btn-primary {
background: var(--u-accent-color);
color: white;
}
.u-helper-btn-primary:hover {
background: var(--u-accent-color-dark);
}
.u-helper-btn-success {
background: var(--u-success-color);
color: white;
}
.u-helper-btn-warning {
background: var(--u-warning-color);
color: white;
}
.u-helper-btn-danger {
background: var(--u-danger-color);
color: white;
}
.u-helper-btn-secondary {
background: #fff;
color: var(--u-text-color-secondary);
border: 1px solid var(--u-border-color);
}
.u-helper-btn-secondary:hover {
background: #f7fafc;
}
.u-helper-info-display {
padding: 12px;
background: var(--u-blur-bg-color);
border-radius: var(--u-border-radius-md);
font-size: 13px;
color: var(--u-text-color-primary);
width: 100%;
max-height: 250px;
overflow-y: auto;
overflow-x: hidden;
overflow-wrap: break-word;
white-space: pre-wrap;
line-height: 1.4;
display: none;
border: 1px solid rgba(255, 255, 255, 0.8);
box-sizing: border-box;
opacity: 0;
transform: translateY(-10px) scale(0.98);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.u-helper-info-display.u-visible {
display: block;
opacity: 1;
transform: translateY(0) scale(1);
}
.u-helper-info-display::-webkit-scrollbar { width: 6px; }
.u-helper-info-display::-webkit-scrollbar-track { background: transparent; }
.u-helper-info-display::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 3px; }
.u-helper-info-display::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.3); }
.u-helper-footer {
width: 100%;
padding: 8px 0;
margin-top: 8px;
border-top: 1px solid var(--u-border-color);
font-size: 12px;
color: var(--u-text-color-tertiary);
text-align: center;
font-weight: 500;
cursor: default;
user-select: none;
}
.u-ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.4);
transform: scale(0);
animation: u-ripple-anim 0.6s linear;
pointer-events: none;
}
@keyframes u-ripple-anim {
to {
transform: scale(4);
opacity: 0;
}
}
`;
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = styles;
document.head.appendChild(styleSheet);
const container = document.createElement('div');
container.className = 'u-helper-container';
setTimeout(() => {
container.classList.add('u-visible');
}, 100);
const titleBar = document.createElement('div');
titleBar.className = 'u-helper-title-bar';
titleBar.innerHTML = `
`;
const buttonContainer = document.createElement('div');
const minimizeButton = document.createElement('button');
minimizeButton.innerHTML = '⚊';
minimizeButton.className = 'u-helper-control-btn';
const contentContainer = document.createElement('div');
contentContainer.className = 'u-helper-content';
const { section: aiConfigSection, content: aiConfigContent } = createCollapsibleSection('AI配置', 'u-collapse-ai');
const aiToggleContainer = document.createElement('div');
aiToggleContainer.className = 'u-helper-setting-item';
const aiToggleLabel = document.createElement('label');
aiToggleLabel.className = 'u-helper-checkbox-label';
aiToggleLabel.style.display = 'flex';
aiToggleLabel.style.alignItems = 'center';
aiToggleLabel.style.gap = '8px';
const aiToggle = document.createElement('input');
aiToggle.type = 'checkbox';
aiToggle.checked = useKimiAI;
aiToggle.onchange = (e) => {
useKimiAI = e.target.checked;
localStorage.setItem('useKimiAI', useKimiAI.toString());
};
aiToggleLabel.appendChild(aiToggle);
aiToggleLabel.appendChild(document.createTextNode('AI答题 (硅基流动)'));
aiToggleContainer.appendChild(aiToggleLabel);
const aiDescription = document.createElement('div');
aiDescription.className = 'u-helper-setting-item';
aiDescription.style.marginTop = '8px';
aiDescription.style.padding = '8px';
aiDescription.style.backgroundColor = 'rgba(0,0,0,0.03)';
aiDescription.style.borderRadius = '4px';
aiDescription.style.fontSize = '12px';
aiDescription.innerHTML = `AI模型: ${SILICONFLOW_MODEL_NAME}`;
aiConfigContent.appendChild(aiToggleContainer);
aiConfigContent.appendChild(aiDescription);
contentContainer.appendChild(aiConfigSection);
const { section: fileManagementSection, content: fileManagementContent } = createCollapsibleSection('题库管理', 'u-collapse-file');
const fileInputButton = document.createElement('label');
fileInputButton.htmlFor = 'question-bank-uploader';
fileInputButton.textContent = '上传新题库 (.json)';
fileInputButton.className = 'u-helper-file-upload-btn';
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.id = 'question-bank-uploader';
fileInput.accept = '.json';
fileInput.style.display = 'none';
const bankSelectorGroup = document.createElement('div');
bankSelectorGroup.className = 'u-helper-select-group';
const bankSelector = document.createElement('select');
bankSelector.className = 'u-helper-select';
const clearSelectedBankButton = document.createElement('button');
clearSelectedBankButton.innerHTML = '';
clearSelectedBankButton.title = '删除选中的题库';
clearSelectedBankButton.className = 'u-helper-delete-btn';
bankSelectorGroup.appendChild(bankSelector);
bankSelectorGroup.appendChild(clearSelectedBankButton);
fileManagementContent.appendChild(fileInputButton);
fileManagementContent.appendChild(fileInput);
fileManagementContent.appendChild(bankSelectorGroup);
const { section: delaySettingsContainer, content: delaySettingsContent } = createCollapsibleSection('自动化延迟 (毫秒)', 'u-collapse-delay');
const answerDelayContainer = document.createElement('div');
answerDelayContainer.className = 'u-helper-input-row';
const answerDelayLabel = document.createElement('label');
answerDelayLabel.textContent = '答案填入延迟';
answerDelayLabel.className = 'u-helper-label';
const answerDelayInput = document.createElement('input');
answerDelayInput.type = 'number';
answerDelayInput.min = '100';
answerDelayInput.max = '5000';
answerDelayInput.value = localStorage.getItem('u-answer-delay') || '800';
answerDelayInput.className = 'u-helper-input';
answerDelayInput.addEventListener('change', () => {
localStorage.setItem('u-answer-delay', answerDelayInput.value);
});
const pageDelayContainer = document.createElement('div');
pageDelayContainer.className = 'u-helper-input-row';
const pageDelayLabel = document.createElement('label');
pageDelayLabel.textContent = '页面切换延迟';
pageDelayLabel.className = 'u-helper-label';
const pageDelayInput = document.createElement('input');
pageDelayInput.type = 'number';
pageDelayInput.min = '1000';
pageDelayInput.max = '10000';
pageDelayInput.value = localStorage.getItem('u-page-delay') || '3500';
pageDelayInput.className = 'u-helper-input';
pageDelayInput.addEventListener('change', () => {
localStorage.setItem('u-page-delay', pageDelayInput.value);
});
answerDelayContainer.appendChild(answerDelayLabel);
answerDelayContainer.appendChild(answerDelayInput);
pageDelayContainer.appendChild(pageDelayLabel);
pageDelayContainer.appendChild(pageDelayInput);
delaySettingsContent.appendChild(answerDelayContainer);
delaySettingsContent.appendChild(pageDelayContainer);
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
let xOffset = 0;
let yOffset = 0;
function dragStart(e) {
if (e.type === "mousedown") {
initialX = e.clientX - xOffset;
initialY = e.clientY - yOffset;
}
isDragging = true;
}
function dragEnd(e) {
initialX = currentX;
initialY = currentY;
isDragging = false;
}
function drag(e) {
if (isDragging) {
e.preventDefault();
if (e.clientX) {
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
xOffset = currentX;
yOffset = currentY;
container.style.transform = `translate(${currentX}px, ${currentY}px)`;
}
}
}
titleBar.addEventListener('mousedown', dragStart);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
let isMinimized = false;
minimizeButton.addEventListener('click', () => {
isMinimized = !isMinimized;
contentContainer.style.display = isMinimized ? 'none' : 'flex';
minimizeButton.innerHTML = isMinimized ? '+' : '⚊';
container.style.width = isMinimized ? 'auto' : '320px';
});
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const fileContent = e.target.result;
JSON.parse(fileContent);
const collection = getBankCollection();
collection[file.name] = fileContent;
saveBankCollection(collection);
localStorage.setItem('u-question-bank-selected', file.name);
console.log(`题库 "${file.name}" 已成功添加并保存。`);
updateBankSelectorUI();
fileInput.value = '';
} catch (error) {
console.error('加载题库文件失败:', error);
alert('加载失败,请检查文件是否为有效的JSON格式。');
}
};
reader.readAsText(file, 'UTF-8');
}
});
bankSelector.addEventListener('change', () => {
const selectedFile = bankSelector.value;
if (selectedFile) {
const collection = getBankCollection();
const fileContent = collection[selectedFile];
if(fileContent) {
try {
loadedQuestionBank = JSON.parse(fileContent);
localStorage.setItem('u-question-bank-selected', selectedFile);
console.log(`已切换到题库: ${selectedFile}`);
} catch(e) {
console.error(`解析缓存的题库 ${selectedFile} 失败:`, e);
loadedQuestionBank = null;
}
}
}
});
clearSelectedBankButton.addEventListener('click', () => {
const selectedFile = bankSelector.value;
if (selectedFile && confirm(`确定要删除题库 "${selectedFile}" 吗?此操作不可撤销。`)) {
const collection = getBankCollection();
delete collection[selectedFile];
saveBankCollection(collection);
const lastSelected = localStorage.getItem('u-question-bank-selected');
if (lastSelected === selectedFile) {
localStorage.removeItem('u-question-bank-selected');
loadedQuestionBank = null;
}
console.log(`题库 "${selectedFile}" 已被删除。`);
updateBankSelectorUI();
}
});
function getBankCollection() {
const stored = localStorage.getItem('u-question-bank-collection');
return stored ? JSON.parse(stored) : {};
}
function saveBankCollection(collection) {
localStorage.setItem('u-question-bank-collection', JSON.stringify(collection));
}
function updateBankSelectorUI() {
const collection = getBankCollection();
const lastSelected = localStorage.getItem('u-question-bank-selected');
const bankNames = Object.keys(collection);
bankSelector.innerHTML = '';
loadedQuestionBank = null;
if (bankNames.length === 0) {
const option = document.createElement('option');
option.textContent = '暂无题库';
option.disabled = true;
bankSelector.appendChild(option);
clearSelectedBankButton.style.display = 'none';
} else {
bankNames.forEach(name => {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
bankSelector.appendChild(option);
});
const selectedFile = bankNames.includes(lastSelected) ? lastSelected : bankNames[0];
bankSelector.value = selectedFile;
try {
loadedQuestionBank = JSON.parse(collection[selectedFile]);
localStorage.setItem('u-question-bank-selected', selectedFile);
console.log(`已自动加载题库: ${selectedFile}`);
} catch(e) {
console.error(`解析缓存的题库 ${selectedFile} 失败:`, e);
}
clearSelectedBankButton.style.display = 'flex';
}
}
updateBankSelectorUI();
const infoDisplay = document.createElement('div');
infoDisplay.className = 'u-helper-info-display';
const toggleInfoButton = document.createElement('button');
toggleInfoButton.className = 'u-helper-btn u-helper-btn-secondary';
toggleInfoButton.innerHTML = '显示信息';
let isInfoVisible = false;
toggleInfoButton.addEventListener('click', () => {
isInfoVisible = !isInfoVisible;
infoDisplay.classList.toggle('u-visible', isInfoVisible);
toggleInfoButton.innerHTML = isInfoVisible ?
'收起信息' :
'显示信息';
});
const infoButton = document.createElement('button');
infoButton.className = 'u-helper-btn u-helper-btn-primary';
infoButton.innerHTML = '获取章节信息';
const autoSelectButton = document.createElement('button');
autoSelectButton.className = 'u-helper-btn u-helper-btn-primary';
autoSelectButton.innerHTML = '自动选择答案';
const autoRunButton = document.createElement('button');
autoRunButton.id = 'auto-run-btn';
autoRunButton.className = 'u-helper-btn u-helper-btn-success';
autoRunButton.innerHTML = '开始挂机';
autoSelectButton.addEventListener('click', () => {
autoSelectAnswers();
});
autoRunButton.addEventListener('click', () => {
toggleAutoRun();
});
const addRippleEffect = (button) => {
button.addEventListener('mousedown', (e) => {
const rect = button.getBoundingClientRect();
const ripple = document.createElement('span');
ripple.className = 'u-ripple';
ripple.style.left = `${e.clientX - rect.left}px`;
ripple.style.top = `${e.clientY - rect.top}px`;
button.appendChild(ripple);
setTimeout(() => ripple.remove(), 600);
});
};
[infoButton, autoSelectButton, autoRunButton, toggleInfoButton].forEach(addRippleEffect);
const footerBar = document.createElement('div');
footerBar.className = 'u-helper-footer';
footerBar.innerHTML = 'By 恶搞之家';
buttonContainer.appendChild(minimizeButton);
titleBar.appendChild(buttonContainer);
contentContainer.appendChild(aiConfigSection);
contentContainer.appendChild(fileManagementSection);
contentContainer.appendChild(delaySettingsContainer);
const { section: videoSettingsSection, content: videoSettingsContent } = createCollapsibleSection('视频设置', 'u-collapse-video');
const videoSpeedContainer = document.createElement('div');
videoSpeedContainer.className = 'u-helper-input-row';
const videoSpeedLabel = document.createElement('label');
videoSpeedLabel.className = 'u-helper-label';
videoSpeedLabel.textContent = '播放速度';
const videoSpeedSelector = document.createElement('select');
videoSpeedSelector.className = 'u-helper-select';
videoSpeedSelector.style.width = '120px';
['1.0', '1.25', '1.5', '2.0', '2.5', '3.0'].forEach(speed => {
const option = document.createElement('option');
option.value = speed;
option.textContent = `${speed}x`;
videoSpeedSelector.appendChild(option);
});
videoSpeedSelector.value = localStorage.getItem('u-video-speed') || '2.0';
videoSpeedSelector.addEventListener('change', () => {
const newSpeed = parseFloat(videoSpeedSelector.value);
localStorage.setItem('u-video-speed', newSpeed);
document.querySelectorAll('video[data-handled-by-script="true"]').forEach(v => {
v.playbackRate = newSpeed;
console.log(`[视频助手] 已将视频速度调整为 ${newSpeed}x`);
});
});
videoSpeedContainer.appendChild(videoSpeedLabel);
videoSpeedContainer.appendChild(videoSpeedSelector);
videoSettingsContent.appendChild(videoSpeedContainer);
contentContainer.appendChild(videoSettingsSection);
const buttonGrid = document.createElement('div');
buttonGrid.style.cssText = 'display: grid; grid-template-columns: 1fr 1fr; gap: 10px;';
buttonGrid.appendChild(infoButton);
buttonGrid.appendChild(toggleInfoButton);
contentContainer.appendChild(buttonGrid);
contentContainer.appendChild(autoSelectButton);
contentContainer.appendChild(autoRunButton);
contentContainer.appendChild(infoDisplay);
contentContainer.appendChild(footerBar);
container.appendChild(titleBar);
container.appendChild(contentContainer);
document.body.appendChild(container);
}
function processSpecialNumbering(answers) {
const result = [];
for (let i = 0; i < answers.length; i++) {
let answer = answers[i];
let lines = answer.split(/\n/).map(line => line.trim()).filter(Boolean);
let processedLines = [];
for (let j = 0; j < lines.length; j++) {
let currentLine = lines[j];
let nextLine = j + 1 < lines.length ? lines[j + 1] : '';
if (/^\d+$/.test(currentLine) && nextLine.match(/^[00][\.\、\)]/)) {
const prefix = currentLine;
const match = nextLine.match(/^[00]([\.\、\)].*)/);
if (match) {
processedLines.push(`${prefix}${match[1]}`);
j++;
} else {
processedLines.push(currentLine);
}
} else {
processedLines.push(currentLine);
}
}
result.push(processedLines.join('\n'));
}
return result;
}
function searchAnswersInJson(jsonContent, chapterContent, topicContent) {
try {
function formatJsonAnswer(answerObj) {
try {
let lines = [];
if (Array.isArray(answerObj)) {
lines = answerObj.map((item, index) => {
const trimmedItem = item.toString().trim();
return /^\d+[\.\))]/.test(trimmedItem) ? trimmedItem : `${index + 1}) ${trimmedItem}`;
});
} else if (typeof answerObj === 'object' && answerObj !== null) {
const allKeys = Object.keys(answerObj).sort((a, b) => parseInt(a) - parseInt(b));
allKeys.forEach(key => {
let value = answerObj[key];
if (typeof value === 'object' && value !== null) value = Object.values(value).join(' ');
const formattedKey = key.trim().replace(/\.$/, ')');
lines.push(`${formattedKey} ${value}`);
});
} else {
return [];
}
if (lines.some(line => line.includes('|||'))) {
const fillInAnswers = [], textAnswers = [];
lines.forEach(line => {
const parts = line.split('|||');
const prefix = (line.match(/^(\d+[\.\))]\s*)/) || [''])[0];
if (parts.length > 1) {
fillInAnswers.push(prefix + parts[0].replace(/^(\d+[\.\))]\s*)/, '').trim());
textAnswers.push(prefix + parts[1].trim());
} else {
fillInAnswers.push(line);
}
});
const groups = [];
if (fillInAnswers.length > 0) groups.push(fillInAnswers.join('\n'));
if (textAnswers.length > 0) groups.push(textAnswers.join('\n'));
return groups;
}
const numberCounts = lines.reduce((acc, line) => {
const match = line.match(/^(\d+)/);
if (match) acc[match[1]] = (acc[match[1]] || 0) + 1;
return acc;
}, {});
const hasDuplicates = Object.values(numberCounts).some(count => count > 1);
if (hasDuplicates) {
console.log("检测到重复题号,启用特殊分组逻辑。");
const group1 = [];
const group2 = [];
const spares = [];
const seenNumbers = new Set();
lines.forEach(line => {
const numMatch = line.match(/^(\d+)/);
if (numMatch) {
const num = numMatch[1];
if (numberCounts[num] > 1) {
if (seenNumbers.has(num)) {
group1.push(line);
} else {
group2.push(line);
seenNumbers.add(num);
}
} else {
spares.push(line);
}
}
});
const finalGroup1 = [...group1, ...spares].join('\n');
const finalGroup2 = [...group2, ...spares].join('\n');
console.log("分组完成,生成两组包含多余项的答案。");
return [finalGroup2, finalGroup1];
}
return [lines.join('\n')];
} catch (error) {
console.error('格式化答案时出错:', error);
return [];
}
}
console.log('--- 开始在JSON题库中搜索答案 ---');
const breadcrumbs = document.querySelectorAll('.ant-breadcrumb-link span');
const navigationPath = Array.from(breadcrumbs).map(span => span.textContent.trim());
const unitName = navigationPath.length > 1 ? navigationPath[1] : '';
const sectionName = navigationPath.length > 2 ? navigationPath[2] : '';
const activeTopicElement = document.querySelector('.pc-header-tab-activity');
const activeTopicName = activeTopicElement ? activeTopicElement.textContent.trim() : '';
const subTopicElement = document.querySelector('.pc-task.pc-header-task-activity');
const subTopicName = subTopicElement ? subTopicElement.textContent.trim() : '';
console.log(`导航路径: ${navigationPath.join(' > ')}`);
console.log(`搜索范围: 单元='${unitName}', 区域='${sectionName}'`);
console.log(`搜索关键字: 主题='${activeTopicName}', 子主题='${subTopicName}'`);
let targetUnit = null;
let targetUnitKey = null;
if (unitName) {
const normalizedUnitName = unitName.toLowerCase().replace(/['\s-]/g, '');
for (const [key, value] of Object.entries(jsonContent)) {
const normalizedKey = key.toLowerCase().replace(/['\s-]/g, '');
if (normalizedKey.includes(normalizedUnitName)) {
targetUnit = value;
targetUnitKey = key;
console.log(`成功匹配到单元: "${key}"`);
break;
}
}
}
if (!targetUnit) {
console.log('在题库中未能匹配到当前单元。');
return [];
}
let searchScope = targetUnit;
let scopeName = targetUnitKey;
console.log(`搜索范围已设置为整个单元: "${scopeName}",以提高定位准确性。`);
const searchKeywords = [...new Set([activeTopicName, subTopicName])].filter(Boolean).map(s => s.toLowerCase());
const foundAnswers = [];
function searchRecursive(obj, path = []) {
if (!obj || typeof obj !== 'object') return;
const isAnswerObject = Array.isArray(obj) || Object.keys(obj).some(k => /^\d+[\.\))]?$/.test(k));
if (isAnswerObject) {
const currentPathStr = path.join(' ').toLowerCase();
const allKeywordsMatch = searchKeywords.every(keyword => currentPathStr.includes(keyword));
if (allKeywordsMatch) {
const formattedAnswers = formatJsonAnswer(obj);
if (formattedAnswers.length > 0) {
foundAnswers.push(...formattedAnswers);
console.log(`在路径 [${path.join(' -> ')}] 下找到 ${formattedAnswers.length} 组匹配答案`);
}
}
return;
}
for (const [key, value] of Object.entries(obj)) {
searchRecursive(value, [...path, key]);
}
}
console.log(`开始在 "${scopeName}" 中搜索, 关键字: ${searchKeywords.join(', ')}`);
searchRecursive(searchScope, []);
console.log(`--- 搜索完成, 找到 ${foundAnswers.length} 组答案 ---`);
const uniqueAnswers = [...new Set(foundAnswers)];
if (uniqueAnswers.length > 0) {
console.log('找到的唯一答案:', uniqueAnswers);
}
return uniqueAnswers;
} catch (error) {
console.error('JSON题库搜索错误:', error);
return [];
}
}
function formatAnswer(answer) {
if (!answer) return '';
const lines = answer.split(/\n/).map(line => line.trim()).filter(Boolean);
return '\n' + lines.join('\n') + '\n';
}
function deduplicateAnswers(answers) {
const uniqueAnswers = new Set();
const result = [];
for (const answer of answers) {
const lowerAnswer = answer.toLowerCase();
if (!uniqueAnswers.has(lowerAnswer)) {
uniqueAnswers.add(lowerAnswer);
result.push(answer);
}
}
return result;
}
function getAnswerType(answerGroup) {
if (!answerGroup || typeof answerGroup !== 'string') return 'unknown';
const lines = answerGroup.split('\n').map(l => l.trim()).filter(Boolean);
if (lines.length === 0) return 'unknown';
const allLinesAreChoiceFormat = lines.every(line => {
const cleanedAnswer = line.replace(/^\d+[\.\、\)]+\s*/, '').trim();
return /^[A-ZА-Я](?:\s*,\s*[A-ZА-Я])*$/.test(cleanedAnswer);
});
if (allLinesAreChoiceFormat) {
return 'choice';
}
return 'fill-in';
}
function getAnswerDelay() {
const baseDelay = parseInt(localStorage.getItem('u-answer-delay') || '800');
const randomFactor = 0.8 + Math.random() * 0.4;
return Math.floor(baseDelay * randomFactor);
}
function getPageDelay() {
return parseInt(localStorage.getItem('u-page-delay') || '3500');
}
function simulateHumanBehavior(element) {
if (!element) return;
const mouseEvents = [
new MouseEvent('mouseover', { bubbles: true }),
new MouseEvent('mouseenter', { bubbles: true }),
];
const focusEvents = [
new Event('focus', { bubbles: true }),
new Event('focusin', { bubbles: true }),
];
setTimeout(() => {
mouseEvents.forEach(event => element.dispatchEvent(event));
}, Math.random() * 200);
setTimeout(() => {
focusEvents.forEach(event => element.dispatchEvent(event));
element.focus();
}, Math.random() * 200 + 100);
}
async function handleSpecialFillInQuestions() {
try {
console.log('检查特殊填空题结构...');
const scoopContainers = document.querySelectorAll('.fe-scoop');
if (!scoopContainers || scoopContainers.length === 0) {
console.log('未找到特殊填空题结构');
return false;
}
console.log(`找到 ${scoopContainers.length} 个特殊填空题`);
const directions = document.querySelector(".layout-direction-container, .abs-direction");
const directionsText = directions ? directions.textContent.trim() : '';
const availableWords = [];
const optionPlaceholders = document.querySelectorAll('.option-placeholder');
optionPlaceholders.forEach(placeholder => {
const word = placeholder.textContent.trim();
if (word) {
availableWords.push(word);
console.log('找到可用填空词:', word);
}
});
if (availableWords.length === 0) {
const wordsList = document.querySelectorAll('.words-color');
wordsList.forEach(word => {
const wordText = word.textContent.trim();
if (wordText) {
availableWords.push(wordText);
console.log('从words-color找到可用填空词:', wordText);
}
});
}
const questions = [];
scoopContainers.forEach((container, index) => {
const numberElement = container.querySelector('.question-number');
const number = numberElement ? numberElement.textContent.trim() : (index + 1).toString();
const paragraphElement = container.closest('p');
const contextText = paragraphElement ? paragraphElement.textContent.trim() : '';
questions.push({
number: number,
context: contextText,
type: 'special-fill-in',
container: container
});
});
if (questions.length === 0) {
console.log('未能提取填空题信息');
return false;
}
let prompt = `请帮我完成以下填空题。\n指示:${directionsText}\n\n`;
if (availableWords.length > 0) {
prompt += "可用词汇(答案必须从以下词汇中选择):\n";
availableWords.forEach(word => {
prompt += `- ${word}\n`;
});
prompt += "\n";
}
questions.forEach(q => {
prompt += `${q.number}. ${q.context}\n`;
});
prompt += "\n请按照以下格式回答每个题目:\n";
prompt += "1. [填空答案]\n2. [填空答案] ...\n";
if (availableWords.length > 0) {
prompt += "注意:答案必须从上面列出的可用词汇中选择。\n";
}
prompt += "注意:只需提供填空的单词或短语,无需解释。\n";
console.log('生成的AI提示:', prompt);
console.log('正在请求AI回答...');
const aiAnswer = await askAI(prompt);
if (!aiAnswer) {
console.log('未能获取AI答案');
return false;
}
const answers = aiAnswer.split('\n').filter(line => /^\d+\./.test(line));
console.log('AI答案:', answers);
for (let i = 0; i < questions.length; i++) {
const question = questions[i];
const answerLine = answers[i];
if (!answerLine) continue;
const answer = answerLine.replace(/^\d+\.\s*/, '').trim();
console.log(`准备填写题目 ${question.number} 的答案:`, answer);
const inputSelectors = [
'.scoop-input-wrapper input',
'input',
'.comp-abs-input input',
'.input-user-answer input'
];
let input = null;
for (const selector of inputSelectors) {
input = question.container.querySelector(selector);
if (input) {
console.log(`找到填空输入框,使用选择器: ${selector}`);
break;
}
}
if (input) {
await simulateHumanBehavior(input);
input.value = answer + '\n';
console.log(`已添加换行符: ${answer}\\n`);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new Event('blur', { bubbles: true }));
} else {
console.log('未找到填空题输入框');
}
await new Promise(resolve => setTimeout(resolve, getAnswerDelay()));
}
return true;
} catch (error) {
console.error('处理特殊填空题时发生错误:', error);
return false;
}
}
async function parseConsoleQuestions() {
try {
await new Promise(resolve => setTimeout(resolve, 1000));
const directions = document.querySelector(".layout-direction-container, .abs-direction");
const directionsText = directions ? directions.textContent.trim() : '';
console.log('题目指示:', directionsText);
console.log('尝试从控制台输出获取题目信息');
const questions = [];
const questionContainers = [
'.question-common-abs-question-container',
'.question-wrap',
'.question-basic',
'.layoutBody-container.has-reply',
'.question-material-banked-cloze.question-abs-question'
];
let mainContainer = null;
for (const selector of questionContainers) {
const container = document.querySelector(selector);
if (container) {
mainContainer = container;
console.log(`找到题目容器: ${selector}`);
break;
}
}
if (!mainContainer) {
console.log('未找到题目容器,使用默认查询');
mainContainer = document;
}
const availableWords = [];
const optionPlaceholders = mainContainer.querySelectorAll('.option-placeholder');
optionPlaceholders.forEach(placeholder => {
const word = placeholder.textContent.trim();
if (word) {
availableWords.push(word);
console.log('找到可用填空词:', word);
}
});
const questionSelectors = [
'.question-common-abs-reply',
'.question-inputbox',
'.question',
'[class*="question"]',
'[class*="stem"]'
];
let questionElements = [];
for (const selector of questionSelectors) {
const elements = mainContainer.querySelectorAll(selector);
if (elements && elements.length > 0) {
questionElements = Array.from(elements);
console.log(`使用选择器 ${selector} 找到题目数量: ${elements.length}`);
break;
}
}
questionElements.forEach((element, index) => {
let questionText = '';
if (element.querySelector('.fe-scoop, .scoop-input-wrapper')) {
const paragraphs = element.querySelectorAll('p');
if (paragraphs.length > 0) {
questionText = Array.from(paragraphs)
.map(p => p.textContent.trim())
.join(' ');
console.log(`从段落中提取填空题文本: ${questionText.substring(0, 50)}${questionText.length > 50 ? '...' : ''}`);
}
}
if (!questionText) {
const contentSelectors = [
'.question-inputbox-header',
'.component-htmlview',
'p',
'.title',
'[class*="content"]',
'strong + span',
'.words-color'
];
for (const selector of contentSelectors) {
const contentElements = element.querySelectorAll(selector);
if (contentElements && contentElements.length > 0) {
questionText = Array.from(contentElements)
.map(el => el.textContent.trim())
.join(' ');
console.log(`使用选择器 ${selector} 找到题目文本`);
break;
}
}
}
if (!questionText) {
questionText = element.textContent.trim();
console.log('使用元素自身文本作为题目内容');
}
questionText = questionText
.replace(/\s+/g, ' ')
.replace(/^\d+\.\s*/, '')
.trim();
let number = (index + 1).toString();
const numberElement = element.querySelector('strong, [class*="number"], [class*="index"]');
if (numberElement) {
const numberMatch = numberElement.textContent.match(/\d+/);
if (numberMatch) {
number = numberMatch[0];
}
}
let type = 'unknown';
if (element.querySelector('.fe-scoop, .scoop-input-wrapper, .comp-abs-input, .input-user-answer') ||
element.classList.contains('fill-blank-reply') ||
element.querySelector('span.question-number')) {
type = 'fill-in';
console.log(`题目 ${number} 是填空题 (通过特殊类识别)`);
} else if (element.querySelector('textarea')) {
type = 'text';
console.log(`题目 ${number} 是文本题`);
} else if (element.querySelector('input[type="text"]')) {
type = 'fill-in';
console.log(`题目 ${number} 是填空题`);
} else if (element.querySelector('input[type="radio"]')) {
type = 'single-choice';
console.log(`题目 ${number} 是单选题`);
} else if (element.querySelector('input[type="checkbox"]')) {
type = 'multiple-choice';
console.log(`题目 ${number} 是多选题`);
} else if (element.querySelector('.option-wrap, .option.isNotReview, .caption')) {
const options = element.querySelectorAll('.option-wrap, .option.isNotReview');
if (options.length > 0) {
const titleElement = element.querySelector('.ques-title, .component-htmlview.ques-title');
const titleText = titleElement ? titleElement.textContent : '';
if (titleText.includes('多选') ||
titleText.includes('所有') ||
titleText.includes('多个') ||
titleText.includes('multiple')) {
type = 'multiple-choice';
console.log(`题目 ${number} 是多选题 (通过选项和标题识别)`);
} else {
type = 'single-choice';
console.log(`题目 ${number} 是单选题 (通过选项识别)`);
}
}
} else {
const parentElement = element.parentElement;
if (parentElement && (
parentElement.querySelector('.fe-scoop') ||
parentElement.querySelector('.scoop-input-wrapper') ||
parentElement.querySelector('.comp-abs-input')
)) {
type = 'fill-in';
console.log(`题目 ${number} 是填空题 (通过父元素识别)`);
} else if (parentElement && parentElement.querySelector('.option-wrap, .option.isNotReview, .caption')) {
type = 'single-choice';
console.log(`题目 ${number} 是选择题 (通过父元素识别)`);
} else {
type = 'text';
console.log(`题目 ${number} 类型未知,默认为文本题`);
}
}
questions.push({
number: number,
text: questionText,
type: type,
element: element
});
});
let prompt = `请帮我完成以下题目。\n指示:${directionsText}\n\n`;
questions.forEach(q => {
let typeDesc = '';
switch (q.type) {
case 'single-choice':
typeDesc = '【单选题】';
break;
case 'multiple-choice':
typeDesc = '【多选题】';
break;
case 'fill-in':
typeDesc = '【填空题】';
break;
case 'text':
typeDesc = '【文本题】';
break;
default:
typeDesc = '';
}
prompt += `${q.number}. ${typeDesc}${q.text}\n`;
});
prompt += "\n请按照以下格式回答每个题目:\n";
prompt += "1. [答案]\n2. [答案] ...\n\n";
prompt += "注意事项:\n";
prompt += "- 只需提供答案,无需解释\n";
prompt += "- 单选题请直接回答选项内容,例如 'energy' 或 'future'\n";
prompt += "- 多选题请直接回答选项内容,用逗号分隔,例如 'energy, future'\n";
prompt += "- 填空题直接提供单词或短语\n";
prompt += "- 文本题提供完整句子或段落\n";
console.log('生成的AI提示:', prompt);
return {
prompt: prompt,
questions: questions
};
} catch (error) {
console.error('解析题目时发生错误:', error);
return null;
}
}
async function autoSelectAnswers() {
try {
console.log('开始自动选择答案');
console.log('AI状态:', { useKimiAI });
if (useKimiAI) {
const specialFillInResult = await handleSpecialFillInQuestions();
if (specialFillInResult) {
console.log('已成功处理特殊填空题');
return true;
}
}
if (useKimiAI) {
console.log('使用AI模式答题');
const questionInfo = await parseConsoleQuestions();
if (!questionInfo) {
console.log('无法获取题目信息');
return false;
}
console.log('正在请求AI回答...');
const aiAnswer = await askAI(questionInfo.prompt);
if (!aiAnswer) {
console.log('未能获取AI答案');
return false;
}
const answers = aiAnswer.split('\n').filter(line => /^\d+\./.test(line));
console.log('AI答案:', answers);
for (let i = 0; i < questionInfo.questions.length; i++) {
const question = questionInfo.questions[i];
const answerLine = answers[i];
if (!answerLine) continue;
const answer = answerLine.replace(/^\d+\.\s*/, '').trim();
console.log(`准备填写题目 ${question.number} 的答案:`, answer);
const questionElement = question.element;
if (!questionElement) {
console.log(`题目 ${question.number} 没有找到对应的DOM元素`);
continue;
}
if (question.type === 'fill-in') {
const inputSelectors = [
'input[type="text"]',
'.comp-abs-input input',
'.input-user-answer input',
'.scoop-input-wrapper input',
'.fe-scoop input',
'input'
];
let input = null;
for (const selector of inputSelectors) {
input = questionElement.querySelector(selector);
if (input) {
console.log(`找到填空输入框,使用选择器: ${selector}`);
break;
}
}
if (input) {
console.log(`填写填空题答案: ${answer}`);
await simulateHumanBehavior(input);
input.value = answer + '\n';
console.log(`已添加换行符: ${answer}\\n`);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new Event('blur', { bubbles: true }));
const submitButton = questionElement.querySelector('button[type="submit"], [class*="submit"], [class*="confirm"]');
if (submitButton) {
console.log('找到提交按钮,点击提交');
await new Promise(resolve => setTimeout(resolve, 300));
submitButton.click();
}
} else {
console.log('未找到填空题输入框');
}
} else if (question.type === 'text') {
const textareaSelectors = [
'textarea.question-inputbox-input',
'textarea',
'.question-inputbox-input-container textarea',
'.question-inputbox-body textarea'
];
let textarea = null;
for (const selector of textareaSelectors) {
textarea = questionElement.querySelector(selector);
if (textarea) break;
}
if (textarea) {
console.log(`找到文本框,填写答案: ${answer.substring(0, 20)}${answer.length > 20 ? '...' : ''}`);
await simulateHumanBehavior(textarea);
const typeText = async (text, element) => {
const delay = () => Math.floor(Math.random() * 30) + 10;
element.value = '';
element.dispatchEvent(new Event('input', { bubbles: true }));
let currentText = '';
for (let i = 0; i < text.length; i++) {
await new Promise(resolve => setTimeout(resolve, delay()));
currentText += text[i];
element.value = currentText;
element.dispatchEvent(new Event('input', { bubbles: true }));
}
currentText += '\n';
element.value = currentText;
console.log(`已添加换行符: ${text}\\n`);
element.dispatchEvent(new Event('input', { bubbles: true }));
element.dispatchEvent(new Event('change', { bubbles: true }));
};
await typeText(answer, textarea);
} else {
console.log('未找到文本框元素');
}
} else if (question.type === 'single-choice') {
console.log('处理单选题,答案:', answer);
const cleanAnswer = answer.trim().toLowerCase();
console.log('清理后的答案:', cleanAnswer);
let targetOption = null;
let targetLetter = null;
let foundByText = false;
const options = questionElement.querySelectorAll('.option.isNotReview');
const optionsArray = Array.from(options);
for (let i = 0; i < optionsArray.length; i++) {
const option = optionsArray[i];
const content = option.querySelector('.component-htmlview.content');
const caption = option.querySelector('.caption');
if (content && caption) {
const contentText = content.textContent.trim().toLowerCase();
const letter = caption.textContent.trim();
console.log(`选项 ${letter}: "${contentText}"`);
if (contentText === cleanAnswer || contentText.includes(cleanAnswer) || cleanAnswer.includes(contentText)) {
targetOption = option;
targetLetter = letter;
foundByText = true;
console.log(`找到匹配的选项 ${letter}: "${contentText}"`);
break;
}
}
}
if (!targetOption && cleanAnswer.length === 1 && /^[A-Z]$/i.test(cleanAnswer)) {
const letter = cleanAnswer.toUpperCase();
for (const option of optionsArray) {
const caption = option.querySelector('.caption');
if (caption && caption.textContent.trim() === letter) {
targetOption = option;
targetLetter = letter;
console.log(`通过选项字母找到选项 ${letter}`);
break;
}
}
}
if (targetOption && targetLetter) {
console.log(`准备点击选项 ${targetLetter} (${foundByText ? '通过文本匹配' : '通过字母匹配'})`);
await simulateHumanBehavior(targetOption);
targetOption.click();
console.log(`已点击选项 ${targetLetter}`);
const submitButton = questionElement.querySelector('button[type="submit"], [class*="submit"], [class*="confirm"]');
if (submitButton) {
console.log('找到提交按钮,点击提交');
await new Promise(resolve => setTimeout(resolve, 300));
submitButton.click();
}
} else {
console.log(`未找到匹配的选项: ${cleanAnswer}`);
}
if (targetOption) {
console.log(`选择选项: ${foundByText ? '通过文本内容匹配' : '通过选项字母'}`);
await simulateHumanBehavior(targetOption);
if (targetOption.tagName === 'INPUT' && targetOption.type === 'radio') {
targetOption.checked = true;
targetOption.dispatchEvent(new Event('change', { bubbles: true }));
}
targetOption.click();
console.log(`已点击选项`);
const submitButton = questionElement.querySelector('button[type="submit"], [class*="submit"], [class*="confirm"]');
if (submitButton) {
console.log('找到提交按钮,点击提交');
await new Promise(resolve => setTimeout(resolve, 300));
submitButton.click();
}
} else {
console.log(`未找到匹配的选项: ${answer}`);
}
} else if (question.type === 'multiple-choice') {
console.log('处理多选题,答案:', answer);
const options = questionElement.querySelectorAll('.option.isNotReview');
const optionsArray = Array.from(options);
const foundOptions = [];
const letterMatches = answer.match(/[A-Z]/gi);
if (letterMatches && letterMatches.length > 0) {
console.log('检测到选项字母答案:', letterMatches);
for (const option of optionsArray) {
const caption = option.querySelector('.caption');
if (caption) {
const letter = caption.textContent.trim();
if (letterMatches.includes(letter.toUpperCase())) {
foundOptions.push({
option: option,
letter: letter,
text: option.querySelector('.component-htmlview.content')?.textContent.trim() || ''
});
console.log(`找到字母匹配的选项 ${letter}`);
}
}
}
}
if (foundOptions.length === 0) {
const answerParts = answer.split(/[,,、;;\s]+/).filter(part => part.trim().length > 0);
console.log('答案拆分为多个部分:', answerParts);
for (let i = 0; i < optionsArray.length; i++) {
const option = optionsArray[i];
const content = option.querySelector('.component-htmlview.content');
const caption = option.querySelector('.caption');
if (content && caption) {
const contentText = content.textContent.trim().toLowerCase();
const letter = caption.textContent.trim();
console.log(`选项 ${letter}: "${contentText}"`);
for (const part of answerParts) {
const cleanPart = part.trim().toLowerCase();
if (contentText === cleanPart || contentText.includes(cleanPart) || cleanPart.includes(contentText)) {
foundOptions.push({
option: option,
letter: letter,
text: contentText
});
console.log(`找到文本匹配的选项 ${letter}: "${contentText}"`);
break;
}
}
}
}
}
if (foundOptions.length > 0) {
console.log(`找到 ${foundOptions.length} 个匹配的选项`);
for (const option of optionsArray) {
if (option.classList.contains('selected')) {
option.click();
await new Promise(resolve => setTimeout(resolve, 100));
}
}
for (const found of foundOptions) {
console.log(`准备点击选项 ${found.letter}: "${found.text}"`);
await simulateHumanBehavior(found.option);
found.option.click();
console.log(`已点击选项 ${found.letter}`);
await new Promise(resolve => setTimeout(resolve, Math.random() * 300 + 100));
}
const submitButton = questionElement.querySelector('button[type="submit"], [class*="submit"], [class*="confirm"]');
if (submitButton) {
console.log('找到提交按钮,点击提交');
await new Promise(resolve => setTimeout(resolve, 300));
submitButton.click();
}
} else {
console.log('未找到匹配的选项:', answer);
}
}
await new Promise(resolve => setTimeout(resolve, getAnswerDelay()));
}
return true;
}
const contentInfo = await getContentInfo();
if (!contentInfo || !contentInfo.answers || contentInfo.answers.length === 0) {
console.log('%c未找到匹配的答案,可能为主观题,将自动跳过。', 'color: #f44336; font-weight: bold;');
return false;
}
if (contentInfo.activeTopicName !== lastActiveTopicName) {
currentTopicUsedAnswers.clear();
lastActiveTopicName = contentInfo.activeTopicName;
console.log('%c检测到主题切换,已重置答案使用记录', 'color: #2196F3;');
}
const textareas = document.querySelectorAll('textarea.question-inputbox-input');
if (textareas && textareas.length > 0) {
console.log('%c检测到文本框题,开始自动填写答案', 'color: #4CAF50; font-weight: bold;');
let selectedAnswer = null;
for (const answer of contentInfo.answers) {
if (getAnswerType(answer) === 'fill-in' && !currentTopicUsedAnswers.has(answer)) {
selectedAnswer = answer;
currentTopicUsedAnswers.add(answer);
console.log('%c使用新答案组 (文本题型)', 'color: #2196F3;', answer);
break;
}
}
if (!selectedAnswer) {
for (const answer of contentInfo.answers) {
if (getAnswerType(answer) === 'fill-in') {
selectedAnswer = answer;
console.log('%c所有文本题答案组都已使用,将重用第一组匹配的答案', 'color: #FFA500;');
break;
}
}
}
if (!selectedAnswer) {
console.log('%c未找到适用于当前文本题的答案组。', 'color: #f44336;');
return false;
}
const answerMatches = selectedAnswer.match(/\d+[\.\、\) ][\s\S]*?(?=\d+[\.\、\) ]|$)/g);
if (!answerMatches) {
console.log('%c无法解析答案格式', 'color: #f44336;');
return false;
}
for (let index = 0; index < textareas.length; index++) {
const textarea = textareas[index];
try {
if (answerMatches[index]) {
const rawAnswer = answerMatches[index]
.replace(/^\d+[\.\、\) ]\s*/, '')
.trim();
const parts = rawAnswer.split('|||');
let answer = '';
let answerType = '';
if (parts.length > 1) {
answer = parts[0].trim();
answerType = '填空题';
} else {
answer = parts[0].trim();
answerType = '文本题';
}
await new Promise(resolve => {
setTimeout(() => {
simulateHumanBehavior(textarea);
let currentText = '';
const answerText = answer + '\n';
let charIndex = 0;
const typeNextChar = () => {
if (charIndex < answerText.length) {
currentText += answerText.charAt(charIndex);
textarea.value = currentText;
textarea.dispatchEvent(new Event('input', { bubbles: true }));
const typingDelay = 30 + Math.random() * 80;
charIndex++;
setTimeout(typeNextChar, typingDelay);
} else {
textarea.dispatchEvent(new Event('change', { bubbles: true }));
textarea.dispatchEvent(new Event('blur', { bubbles: true }));
resolve();
}
};
typeNextChar();
}, getAnswerDelay() + index * 500);
});
console.log(`%c第 ${index + 1} 题 (${answerType}) 已填写答案: ${answer}`, 'color: #2196F3;');
} else {
console.log(`%c第 ${index + 1} 题未找到对应答案`, 'color: #f44336;');
}
} catch (error) {
console.error(`填写第 ${index + 1} 题时发生错误:`, error);
}
}
return true;
}
const fillInBlanks = document.querySelectorAll('.fe-scoop');
if (fillInBlanks && fillInBlanks.length > 0) {
console.log('%c检测到填空题,开始自动填写答案', 'color: #4CAF50; font-weight: bold;');
let selectedAnswer = null;
for (const answer of contentInfo.answers) {
if (getAnswerType(answer) === 'fill-in' && !currentTopicUsedAnswers.has(answer)) {
selectedAnswer = answer;
currentTopicUsedAnswers.add(answer);
console.log('%c使用新答案组 (填空题型)', 'color: #2196F3;', answer);
break;
}
}
if (!selectedAnswer) {
for (const answer of contentInfo.answers) {
if (getAnswerType(answer) === 'fill-in') {
selectedAnswer = answer;
console.log('%c所有填空题答案组都已使用,将重用第一组匹配的答案', 'color: #FFA500;');
break;
}
}
}
if (!selectedAnswer) {
console.log('%c未找到适用于当前填空题的答案组。', 'color: #f44336;');
return false;
}
const answerLines = selectedAnswer.split('\n').map(line => line.trim()).filter(Boolean);
if (!answerLines || answerLines.length === 0) {
console.log('%c无法解析答案格式', 'color: #f44336;');
return false;
}
const fillInAnswers = answerLines.map(line => {
return line.replace(/^\d+[\.\、\) ]\s*/, '').trim();
}).filter(answer => {
return !/^[A-Z]$/.test(answer);
});
if (fillInAnswers.length < fillInBlanks.length) {
console.log(`%c警告:找到的填空答案数量 (${fillInAnswers.length}) 少于页面空格数量 (${fillInBlanks.length})。`, 'color: #FFA500;');
}
for (let index = 0; index < fillInBlanks.length; index++) {
const blank = fillInBlanks[index];
try {
const inputContainer = blank.querySelector('.comp-abs-input');
const input = inputContainer ? inputContainer.querySelector('input') : null;
if (input && fillInAnswers[index]) {
const rawAnswer = fillInAnswers[index];
const answer = rawAnswer.split('|||')[0].trim();
await new Promise(resolve => {
setTimeout(() => {
simulateHumanBehavior(input);
let currentText = '';
const answerText = answer + '\n';
let charIndex = 0;
const typeNextChar = () => {
if (charIndex < answerText.length) {
currentText += answerText.charAt(charIndex);
input.value = currentText;
input.dispatchEvent(new Event('input', { bubbles: true }));
const typingDelay = 30 + Math.random() * 80;
charIndex++;
setTimeout(typeNextChar, typingDelay);
} else {
input.dispatchEvent(new Event('change', { bubbles: true }));
input.dispatchEvent(new Event('blur', { bubbles: true }));
resolve();
}
};
typeNextChar();
}, getAnswerDelay() + index * 500);
});
console.log(`%c第 ${index + 1} 题已填写答案: ${answer}`, 'color: #2196F3;');
} else {
console.log(`%c第 ${index + 1} 题未找到输入框或有效答案`, 'color: #f44336;');
}
} catch (error) {
console.error(`填写第 ${index + 1} 题时发生错误:`, error);
}
}
return true;
}
const matchingWrapper = document.querySelector('#sortableListWrapper');
if (matchingWrapper) {
console.log('%c检测到匹配/排序题,开始自动填写答案', 'color: #4CAF50; font-weight: bold;');
const iframe = document.querySelector('iframe#pc-sequence-iframe');
const doc = iframe ? (iframe.contentDocument || iframe.contentWindow.document) : document;
const inputs = doc.querySelectorAll('input[type="text"], input.answer-item-input');
if (!inputs || inputs.length === 0) {
console.log('%c在此类匹配题中未找到输入框。', 'color: #f44336;');
return false;
}
let selectedAnswer = null;
for (const answer of contentInfo.answers) {
if (!currentTopicUsedAnswers.has(answer)) {
selectedAnswer = answer;
currentTopicUsedAnswers.add(answer);
console.log('%c使用新答案组', 'color: #2196F3;', answer);
break;
}
}
if (!selectedAnswer) {
console.log('%c所有答案组都已使用,将重用第一组答案', 'color: #FFA500;');
selectedAnswer = contentInfo.answers[0];
}
const answerMatches = selectedAnswer.match(/\d+[\.\、\)]\s*[A-Z]/g);
if (!answerMatches) {
console.log('%c无法解析匹配题答案格式 (e.g., "1) D 2) C ...")', 'color: #f44336;');
return false;
}
const answers = answerMatches.map(ans => ans.replace(/\d+[\.\、\)]\s*/, '').trim());
for (let index = 0; index < inputs.length; index++) {
const input = inputs[index];
if (answers[index]) {
await new Promise(resolve => {
setTimeout(() => {
simulateHumanBehavior(input);
input.value = answers[index];
input.dispatchEvent(new Event('input', { bubbles: true }));
setTimeout(() => {
input.dispatchEvent(new Event('blur', { bubbles: true }));
resolve();
}, 100 + Math.random() * 200);
}, getAnswerDelay() + index * 400);
});
console.log(`%c匹配题 ${index + 1} 已填写答案: ${answers[index]}`, 'color: #2196F3;');
}
}
return true;
}
const choiceContainer = document.querySelector('.question-common-abs-choice');
const optionDivs = document.querySelectorAll('div.option');
if (!choiceContainer && (!optionDivs || optionDivs.length === 0)) {
console.log('%c当前页面既不是选择题也不是填空题也不是文本框题', 'color: #f44336; font-weight: bold;');
return false;
}
const questions = choiceContainer
? document.querySelectorAll('.question-common-abs-reply')
: document.querySelectorAll('.question-common-abs-banked-cloze');
if (!questions || questions.length === 0) {
console.log('%c未找到题目', 'color: #f44336; font-weight: bold;');
return false;
}
console.log('%c开始自动选择答案', 'color: #4CAF50; font-weight: bold;');
let selectedAnswer = null;
for (const answer of contentInfo.answers) {
if (getAnswerType(answer) === 'choice' && !currentTopicUsedAnswers.has(answer)) {
selectedAnswer = answer;
currentTopicUsedAnswers.add(answer);
console.log('%c使用新答案组 (选择题型)', 'color: #2196F3;', answer);
break;
}
}
if (!selectedAnswer) {
for (const answer of contentInfo.answers) {
if (getAnswerType(answer) === 'choice') {
selectedAnswer = answer;
console.log('%c所有选择题答案组都已使用,将重用第一组匹配的答案', 'color: #FFA500;');
break;
}
}
}
if (!selectedAnswer) {
console.log('%c未找到适用于当前选择题的答案组。', 'color: #f44336;');
return false;
}
for (let questionIndex = 0; questionIndex < questions.length; questionIndex++) {
const question = questions[questionIndex];
await new Promise(resolve => setTimeout(resolve,
getAnswerDelay() * (1 + questionIndex * 0.5) + Math.random() * 300));
const options = choiceContainer
? question.querySelectorAll('.option.isNotReview')
: question.querySelectorAll('div.option');
if (!options || options.length === 0) {
console.log(`%c第 ${questionIndex + 1} 题未找到选项`, 'color: #f44336;');
continue;
}
const answerPattern = /(\d+)[\.\、\)]\s*([A-K](?:\s*,\s*[A-K])*)/g;
const answers = [];
let match;
answerPattern.lastIndex = 0;
while ((match = answerPattern.exec(selectedAnswer)) !== null) {
const questionNum = parseInt(match[1]);
const answerChoices = match[2].split(/\s*,\s*/);
answers[questionNum - 1] = answerChoices;
}
if (answers.length > 0 && questionIndex < answers.length) {
const answerChoices = answers[questionIndex];
if (answerChoices && answerChoices.length > 0) {
console.log(`%c第 ${questionIndex + 1} 题检测到答案: ${answerChoices.join(', ')}`, 'color: #2196F3;');
for (let letterIndex = 0; letterIndex < answerChoices.length; letterIndex++) {
const letter = answerChoices[letterIndex];
let targetOption = null;
for (let i = 0; i < options.length; i++) {
const caption = options[i].querySelector('.caption');
if (caption && caption.textContent.trim() === letter) {
targetOption = options[i];
break;
}
}
if (targetOption) {
await new Promise(resolve => {
setTimeout(() => {
const isSelected = targetOption.classList.contains('selected');
if (!isSelected) {
targetOption.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
setTimeout(() => {
targetOption.click();
console.log(`%c第 ${questionIndex + 1} 题已选择选项 ${letter}`, 'color: #2196F3;');
setTimeout(resolve, 100 + Math.random() * 200);
}, 100 + Math.random() * 300);
} else {
console.log(`%c第 ${questionIndex + 1} 题选项 ${letter} 已经被选中`, 'color: #FFA500;');
resolve();
}
}, getAnswerDelay() + letterIndex * 200);
});
} else {
console.log(`%c第 ${questionIndex + 1} 题未找到选项 ${letter}`, 'color: #f44336;');
}
}
} else {
console.log(`%c第 ${questionIndex + 1} 题未找到答案`, 'color: #f44336;');
}
} else {
console.log(`%c第 ${questionIndex + 1} 题未找到答案`, 'color: #f44336;');
}
}
return true;
} catch (error) {
console.error('自动选择答案时发生错误:', error);
return false;
}
}
async function getContentInfo() {
try {
const breadcrumbs = document.querySelectorAll('.ant-breadcrumb-link span');
const navigationPath = Array.from(breadcrumbs).map(span => span.textContent.trim());
const chapterContent = navigationPath.length >= 2 ? navigationPath[1] : '未找到章节内容';
const topicElement = document.querySelector(".pc-header-tasks-container .pc-task.pc-header-task-activity");
const topicElementSecond = document.querySelector(".pc-header-tasks-container .pc-task.pc-header-task-activity.pc-task-last");
const topicFirstPart = topicElement ? topicElement.textContent.trim() : '';
const topicSecondPart = topicElementSecond ? topicElementSecond.textContent.trim() : '';
const topicContent = [topicFirstPart, topicSecondPart].filter(Boolean).join(' : ');
const finalTopicContent = topicContent || '未找到主题内容';
const tabContainer = document.querySelector('.pc-tab-container');
const allTopics = tabContainer ? tabContainer.querySelectorAll('.ant-col') : [];
const totalTopics = allTopics.length;
const allTopicNames = [];
allTopics.forEach((topic, index) => {
const topicDiv = topic.querySelector('.pc-tab-view-container');
if (topicDiv) {
const isActive = topic.classList.contains('pc-header-tab-activity');
allTopicNames.push({
index: index + 1,
name: topicDiv.textContent.trim(),
isActive: isActive
});
}
});
let activeTopicName = '';
let nextTopicName = '';
let foundActive = false;
for (let i = 0; i < allTopicNames.length; i++) {
if (foundActive) {
nextTopicName = allTopicNames[i].name;
break;
}
if (allTopicNames[i].isActive) {
activeTopicName = allTopicNames[i].name;
foundActive = true;
}
}
console.log('%c当前页面信息', 'color: #2196F3; font-weight: bold; font-size: 14px;');
console.log('导航路径:', navigationPath);
console.log('章节内容:', chapterContent);
console.log('主题内容:', finalTopicContent);
console.log('主题总数:', totalTopics);
console.log('当前选中的主题:', activeTopicName || '未找到选中的主题');
console.log('下一个主题:', nextTopicName || '没有下一个主题');
console.log('\n%c所有主题列表', 'color: #2196F3; font-weight: bold; font-size: 14px;');
allTopicNames.forEach(topic => {
console.log(`${topic.index}. ${topic.name} ${topic.isActive ? '(当前选中)' : ''}`);
});
let answers = [];
try {
console.log('\n%c正在从题库搜索答案...', 'color: #4CAF50; font-weight: bold; font-size: 14px;');
if (!loadedQuestionBank) {
console.log('%c请先上传题库文件。', 'color: #f44336; font-weight: bold;');
return {
chapter: chapterContent,
topic: finalTopicContent,
totalTopics: totalTopics,
activeTopicName: activeTopicName,
nextTopicName: nextTopicName,
allTopics: allTopicNames,
answers: []
};
}
const questionBankData = { type: 'json', content: loadedQuestionBank };
answers = await searchAnswers(questionBankData, chapterContent, finalTopicContent, nextTopicName);
console.log('\n%c匹配到的答案', 'color: #4CAF50; font-weight: bold; font-size: 14px;');
if (answers.length === 0) {
console.log('%c未找到匹配的答案', 'color: #f44336;');
} else {
answers.forEach((answer, index) => {
console.log(`\n%c答案 ${index + 1}:`, 'color: #4CAF50; font-weight: bold;');
console.log(formatAnswer(answer));
});
}
} catch (error) {
console.error('获取或搜索题库时发生错误:', error);
}
return {
chapter: chapterContent,
topic: finalTopicContent,
totalTopics: totalTopics,
activeTopicName: activeTopicName,
nextTopicName: nextTopicName,
allTopics: allTopicNames,
answers: answers
};
} catch (error) {
console.error('获取内容时发生错误:', error);
return null;
}
}
async function searchAnswers(questionBankData, chapterContent, topicContent, nextTopicName) {
try {
chapterContent = chapterContent.trim();
topicContent = topicContent.trim();
if (!questionBankData || questionBankData.type !== 'json' || !questionBankData.content) {
console.log('题库数据无效或不是JSON格式');
return [];
}
console.log('使用JSON格式题库进行搜索');
return searchAnswersInJson(questionBankData.content, chapterContent, topicContent);
} catch (error) {
console.error('搜索答案时发生错误:', error);
return [];
}
}
let isAutoRunning = false;
let autoRunTimeoutId = null;
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
function updateAutoRunButtonUI() {
const btn = document.getElementById('auto-run-btn');
if (!btn) return;
if (isAutoRunning) {
btn.innerHTML = '停止挂机';
btn.className = 'u-helper-btn u-helper-btn-danger';
} else {
btn.innerHTML = '开始挂机';
btn.className = 'u-helper-btn u-helper-btn-success';
}
}
function findFooterButtonByText(text) {
const footerButtons = document.querySelectorAll('#pc-foot a, #pc-foot button');
for (const btn of footerButtons) {
if (btn.textContent.replace(/\s/g, '').includes(text)) {
return btn;
}
}
return null;
}
async function handleVocabularyCards() {
console.log('[挂机] 检测到词汇卡片学习页面,开始自动点击下一个...');
let cardCount = 0;
while (cardCount < 300) {
const nextButton = document.querySelector("#main-content > div > div > div > div.layoutBody-container > div > div > div.vocContainer > div.vocActions > div.action.next");
if (!nextButton || nextButton.classList.contains('disabled')) {
console.log(`[挂机] 词汇卡片处理完成,共处理 ${cardCount} 个卡片。准备跳转到下一任务。`);
return;
}
await sleep(500 + Math.random() * 1000);
nextButton.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
await sleep(100 + Math.random() * 200);
console.log(`[挂机] 点击第 ${cardCount + 1} 个词汇卡片的"下一个"按钮`);
nextButton.click();
cardCount++;
await sleep(800 + Math.random() * 500);
}
console.log(`[挂机] 已达到最大处理数量限制 (${cardCount})`);
}
async function waitForVideosToEnd() {
const videos = Array.from(document.querySelectorAll('video')).filter(v => !v.ended && v.duration > 0);
if (videos.length === 0) {
console.log('[挂机] 页面上没有需要等待的视频,继续执行。');
return;
}
console.log(`[挂机] 检测到 ${videos.length} 个视频,等待播放完毕...`);
const videoPromises = videos.map(video => {
return new Promise(resolve => {
if (video.ended) {
resolve();
return;
}
const onEnded = () => {
console.log('[挂机] 一个视频已播放完毕。');
video.removeEventListener('ended', onEnded);
resolve();
};
video.addEventListener('ended', onEnded);
const observer = new MutationObserver(() => {
if (!document.body.contains(video)) {
console.log('[挂机] 一个视频已从页面移除,视为播放结束。');
observer.disconnect();
video.removeEventListener('ended', onEnded);
resolve();
}
});
observer.observe(document.body, { childList: true, subtree: true });
});
});
await Promise.all(videoPromises);
console.log('[挂机] 所有视频均已播放完毕。');
}
async function runNextStep() {
if (!isAutoRunning) return;
try {
const vocContainer = document.querySelector("#main-content > div > div > div > div.layoutBody-container > div > div > div.vocContainer");
const discussionTextarea = document.querySelector("#bottom > div > div > div.discussion-cloud-bottom > div.discussion-cloud-bottom-textArea-container > div.ant-input-textarea.ant-input-textarea-show-count.discussion-cloud-bottom-textArea > textarea");
if (vocContainer) {
await handleVocabularyCards();
} else if (discussionTextarea) {
await handleDiscussionPage();
}
else {
console.log('[挂机] 尝试自动选择答案...');
const foundAnswers = await autoSelectAnswers();
if (foundAnswers) {
console.log('[挂机] 已尝试填充答案。');
await sleep(getAnswerDelay() * 2);
const submitButton = findFooterButtonByText('提交');
if (submitButton) {
console.log('[挂机] 题目作答完毕,点击 "提交"');
await sleep(800 + Math.random() * 1000);
submitButton.click();
await sleep(2500);
}
} else {
console.log('[挂机] 未找到答案,可能为主观题,将直接尝试导航。');
await sleep(500);
}
}
await waitForVideosToEnd();
console.log('[挂机] 内容处理/提交完毕,查找下一步操作进行导航...');
const nextQuestionButton = findFooterButtonByText('下一题');
if (nextQuestionButton) {
console.log('[挂机] 操作: 点击 "下一题"');
await sleep(500 + Math.random() * 800);
nextQuestionButton.click();
autoRunTimeoutId = setTimeout(runNextStep, getPageDelay());
return;
}
const navigatedToSubTopic = await navigateToNextSubTopic();
if (navigatedToSubTopic) {
autoRunTimeoutId = setTimeout(runNextStep, getPageDelay());
return;
}
const navigatedByToc = await navigateToNextTocItem();
if (navigatedByToc) {
autoRunTimeoutId = setTimeout(runNextStep, getPageDelay());
return;
}
console.log('[挂机] 结束: 未找到任何可执行的导航操作。');
isAutoRunning = false;
updateAutoRunButtonUI();
} catch (error) {
console.error('[挂机] 执行步骤时发生错误:', error);
isAutoRunning = false;
updateAutoRunButtonUI();
}
}
async function navigateToNextSubTopic() {
console.log('[挂机] 尝试导航到下一个子主题 (横向Tab)...');
const tabSystems = [
{
name: "子任务Tabs",
containerSelector: '.pc-header-tasks-container',
tabSelector: '.pc-task',
activeClass: 'pc-header-task-activity'
},
{
name: "主任务Tabs",
containerSelector: '.pc-tab-container',
tabSelector: '.ant-col',
activeClass: 'pc-header-tab-activity'
}
];
for (const system of tabSystems) {
const container = document.querySelector(system.containerSelector);
if (!container) {
continue;
}
const tabs = Array.from(container.querySelectorAll(system.tabSelector));
if (tabs.length <= 1) {
continue;
}
const activeIndex = tabs.findIndex(tab => tab.classList.contains(system.activeClass));
if (activeIndex === -1) {
continue;
}
if (activeIndex + 1 < tabs.length) {
const nextTab = tabs[activeIndex + 1];
const clickable = nextTab.querySelector('.pc-tab-view-container') || nextTab;
const nextTabName = (clickable.textContent || nextTab.title || '未知').trim();
console.log(`[挂机] 导航到下一个子主题: "${nextTabName}"`);
await sleep(600 + Math.random() * 800);
clickable.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
await sleep(200 + Math.random() * 300);
clickable.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
await sleep(50 + Math.random() * 50);
clickable.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
clickable.click();
return true;
} else {
console.log(`[挂机] 在 ${system.name} 中已是最后一个Tab。`);
}
}
console.log('[挂机] 未找到可切换的下一个子主题。');
return false;
}
async function navigateToNextTocItem() {
console.log('[挂机] 未找到操作按钮,尝试导航到目录中的下一项...');
const tocContainer = document.querySelector('.pc-slier-menu-container.show, .pc-slider-menu-container.show');
if (!tocContainer) {
console.log('[挂机] 结束: 无法找到目录容器。');
return false;
}
const tocItems = tocContainer.querySelectorAll('div[data-role="micro"]');
if (tocItems.length === 0) {
console.log('[挂机] 结束: 无法找到目录中的具体条目 (micro items)。');
return false;
}
let activeIndex = -1;
for (let i = 0; i < tocItems.length; i++) {
if (tocItems[i].classList.contains('pc-menu-activity')) {
activeIndex = i;
break;
}
}
if (activeIndex === -1) {
console.log('[挂机] 结束: 无法在目录中定位到当前活动项。');
return false;
}
if (activeIndex + 1 < tocItems.length) {
const nextItem = tocItems[activeIndex + 1];
const nextItemName = nextItem.querySelector('.pc-menu-node-name')?.textContent.trim() || '未知项';
console.log(`[挂机] 导航到下一项: "${nextItemName}"`);
await sleep(700 + Math.random() * 1000);
nextItem.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
await sleep(300 + Math.random() * 400);
nextItem.click();
return true;
} else {
console.log('[挂机] 结束: 已是目录中的最后一项。');
return false;
}
}
function toggleAutoRun() {
isAutoRunning = !isAutoRunning;
updateAutoRunButtonUI();
if (isAutoRunning) {
console.log('自动挂机已启动...');
runNextStep();
} else {
if (autoRunTimeoutId) {
clearTimeout(autoRunTimeoutId);
autoRunTimeoutId = null;
}
console.log('自动挂机已手动停止。');
}
}
function handleVideo(video) {
if (video.dataset.handledByScript) return;
video.dataset.handledByScript = 'true';
console.log('[视频助手] 发现视频,开始处理:', video);
const setPlaybackRate = () => {
const targetSpeed = parseFloat(localStorage.getItem('u-video-speed') || '2.0');
if (video.playbackRate !== targetSpeed) {
video.playbackRate = targetSpeed;
console.log(`[视频助手] 视频倍速已设置为 ${targetSpeed}x`);
}
};
const attemptPlay = () => {
video.muted = true;
const playPromise = video.play();
if (playPromise !== undefined) {
playPromise.then(() => {
console.log('[视频助手] 视频已自动播放。');
setPlaybackRate();
}).catch(error => {
console.warn('[视频助手] 自动播放失败,可能是浏览器策略限制。等待用户交互后再次尝试。');
const playOnInteraction = () => {
video.play();
setPlaybackRate();
document.body.removeEventListener('click', playOnInteraction, true);
};
document.body.addEventListener('click', playOnInteraction, { once: true, capture: true });
});
}
};
if (video.readyState >= 3) {
attemptPlay();
} else {
video.addEventListener('canplay', attemptPlay, { once: true });
}
video.addEventListener('ratechange', () => {
setTimeout(setPlaybackRate, 100);
});
video.addEventListener('playing', setPlaybackRate);
video.addEventListener('pause', () => {
setTimeout(() => {
const popup = document.querySelector('.question-video-popup');
if (popup && popup.offsetParent !== null) {
console.log('[视频助手] 视频暂停,检测到弹窗问题,开始处理...');
handleVideoPopupQuestions(popup);
}
}, 200);
});
}
function setupVideoHandler() {
document.querySelectorAll('video').forEach(handleVideo);
const observer = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName === 'VIDEO') {
handleVideo(node);
} else if (node.querySelectorAll) {
node.querySelectorAll('video').forEach(handleVideo);
}
}
});
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
console.log('已启动视频自动播放和倍速调整功能。');
}
async function handleVideoPopupQuestions(popupElement) {
if (popupElement.dataset.handledByRandomSelect) return;
popupElement.dataset.handledByRandomSelect = 'true';
console.log('[视频弹题助手] 检测到视频中的弹窗问题,准备随机选择...');
await sleep(500);
const options = popupElement.querySelectorAll('.option.isNotReview');
if (options.length > 0) {
await sleep(1000 + Math.random() * 1500);
const randomIndex = Math.floor(Math.random() * options.length);
const randomOption = options[randomIndex];
const optionCaption = randomOption.querySelector('.caption')?.textContent.trim() || `选项 ${randomIndex + 1}`;
console.log(`[视频弹题助手] 随机选择选项: ${optionCaption}`);
randomOption.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
await sleep(150 + Math.random() * 200);
randomOption.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
await sleep(50 + Math.random() * 50);
randomOption.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
randomOption.click();
console.log('[视频弹题助手] 已成功点击选项。');
await sleep(500 + Math.random() * 300);
const confirmButton = popupElement.querySelector('button.submit-btn');
if (confirmButton && !confirmButton.disabled) {
console.log('[视频弹题助手] 找到"确定"按钮,正在点击...');
confirmButton.click();
console.log('[视频弹题助手] 已点击"确定"按钮。');
} else {
console.log('[视频弹题助手] 未找到或"确定"按钮不可用。');
}
} else {
console.log('[视频弹题助手] 未在弹窗中找到可选选项。');
}
}
function setupVideoPopupObserver() {
const observer = new MutationObserver(() => {
const popup = document.querySelector('.question-video-popup');
if (popup && popup.offsetParent === null) {
if (popup.dataset.handledByRandomSelect) {
console.log('[视频弹题助手] 弹窗已隐藏,重置处理标记以便下次使用。');
delete popup.dataset.handledByRandomSelect;
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class', 'hidden']
});
console.log('已启动视频内弹窗问题状态监视器。');
}
async function handleDiscussionPage() {
const textarea = document.querySelector("#bottom > div > div > div.discussion-cloud-bottom > div.discussion-cloud-bottom-textArea-container > div.ant-input-textarea.ant-input-textarea-show-count.discussion-cloud-bottom-textArea > textarea");
const submitButton = document.querySelector("#bottom > div > div > div.discussion-cloud-bottom > div.discussion-cloud-bottom-btns > div > div.btns-submit.student-btns-submit > button");
if (submitButton && !submitButton.disabled) {
console.log('[挂机] 检测到讨论区,准备自动发送 "Hello"。');
await sleep(1000 + Math.random() * 500);
textarea.dispatchEvent(new Event('focus', { bubbles: true }));
textarea.value = "Hello";
textarea.dispatchEvent(new Event('input', { bubbles: true }));
await sleep(200);
textarea.dispatchEvent(new Event('blur', { bubbles: true }));
console.log('[挂机] 已输入 "Hello"');
await sleep(500 + Math.random() * 500);
if (!submitButton.disabled) {
submitButton.click();
console.log('[挂机] 已点击发布按钮。');
await sleep(2500);
}
} else {
console.log('[挂机] 讨论区发布按钮不可用或未找到,跳过。');
}
}
function setupPopupHandler() {
setInterval(() => {
document.querySelectorAll('.ant-modal-wrap:not([style*="display: none"])').forEach(modal => {
const confirmButton = modal.querySelector('.ant-btn-primary');
if (confirmButton && confirmButton.offsetParent !== null) {
const buttonText = confirmButton.textContent || confirmButton.innerText;
if (buttonText.includes('确')) {
console.log(`[自动确认] 发现弹窗,点击 "${buttonText.trim()}"`);
confirmButton.click();
}
}
});
}, 1000);
console.log('已启动全局弹窗自动确认处理器。');
}
window.addEventListener('load', () => {
createFloatingButton();
setupPopupHandler();
setupVideoHandler();
setupVideoPopupObserver();
});