// ==UserScript==
// @name 智慧树刷课助手
// @namespace https://github.com/CXRunfree/Autovisor
// @version 1.4.3
// @description 智慧树普通课程与 AI 课程自动播放、完成后切换下一资源、助教随堂题支持 AI/免费题库/付费题库、自动静音、倍速播放、检测暂停续播、刷习惯分、习题 AI 答题
// @author 叶屿
// @license GPL3
// @antifeature payment 题库答题功能需要激活码(付费),视频播放等基础功能完全免费
// @match *://passport.zhihuishu.com/*
// @match *://studyh5.zhihuishu.com/*
// @match *://studyvideoh5.zhihuishu.com/*
// @match *://fusioncourseh5.zhihuishu.com/*
// @match *://hike.zhihuishu.com/*
// @match *://www.zhihuishu.com/*
// @match *://studyservice-api.zhihuishu.com/*
// @match *://examh5.zhihuishu.com/*
// @match *://onlineexamh5new.zhihuishu.com/*
// @match *://studentexambaseh5.zhihuishu.com/*
// @match *://studentexamcomh5.zhihuishu.com/studentReviewTestOrExam/*
// @match *://ai-smart-course-student-pro.zhihuishu.com/point/*
// @match *://ai-smart-course-student-pro.zhihuishu.com/examPreview/*
// @match *://ai-smart-course-student-pro.zhihuishu.com/singleCourse/knowledgeStudy/*
// @match *://ai.zhihuishu.com/AIstudent/*
// @match *://ai-smart-course-student-pro.zhihuishu.com/learnPage/*
// @match *://hike-teaching-center.polymas.com/stu-hike/agent-course-hike/ai-course-center*
// @match *://onlineweb.zhihuishu.com/*
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect qsy.iano.cn
// @connect lyck6.cn
// @connect *
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// v1.4.3 (2026-10-15):助教随堂题可独立选择 AI、免费题库或付费题库;
// 学习面板补齐验证码、付费激活及余额配置,不自动切换来源;
// 请求期间持续校验免费有效期,同题重绘复用查询结果,换题/切换来源/停止取消旧流程。
// v1.4.2 (2026-10-15):保留平台根节点和异步弹层,避免白屏及误触退出;
// AI 课程按实际播放结束切课,防旧视频状态串课;助教题接入 AI 并确认提交。
// 新增学习页 AI 配置入口,修复异步换题、SPA 切页和停止/重启生命周期。
/*
* ==========================================
* 免责声明 / Disclaimer
* ==========================================
*
* 本脚本仅供个人学习和技术研究使用,请勿用于任何违反
* 学校规章制度、考试纪律或法律法规的行为。
*
* 使用本脚本即表示您同意以下条款:
* 1. 本脚本不鼓励、不支持任何形式的考试作弊行为
* 2. 使用者应自行承担使用本脚本产生的一切后果
* 3. 作者不对因使用本脚本而导致的任何直接或间接
* 损失(包括但不限于学业处分、账号封禁等)承担责任
* 4. 本脚本不保证题库答案的准确性和完整性
* 5. 请遵守《中华人民共和国教育法》及相关法律法规
* 6. 请遵守所在学校的学术诚信规范
*
* 如您不同意以上条款,请立即停止使用并删除本脚本。
*
* This script is for personal learning and technical
* research purposes only. The author assumes no
* responsibility for any consequences.
* ==========================================
*/
// ===================== 配置区 =====================
const DEFAULT_CONFIG = {
playbackSpeed: 1.5, // 播放倍速 (0.5 ~ 1.8)
soundOff: true, // 是否静音
autoNext: true, // 自动切换下一集
autoAnswer: true, // 自动处理随堂弹题(AI 助教使用用户配置)
autoPlay: true, // 自动播放 & 暂停检测
habitScore: true, // 刷习惯分(模拟鼠标移动)
autoExam: false, // 考试自动答题(默认关闭,手动点击搜题)
examAutoFill: false, // 考试自动填入答案(默认关闭,需手动确认)
showPanel: true, // 显示悬浮面板
};
// 从存储读取或使用默认配置
function loadConfig() {
const saved = GM_getValue('autovisor_config', null);
if (saved) {
return Object.assign({}, DEFAULT_CONFIG, saved);
}
return Object.assign({}, DEFAULT_CONFIG);
}
function saveConfig(cfg) {
GM_setValue('autovisor_config', cfg);
}
let config = loadConfig();
// ===================== 日志系统 =====================
const LOG_PREFIX = '[智慧树助手]';
const log = {
info: (msg) => console.log(`%c${LOG_PREFIX} ${msg}`, 'color:#409EFF;font-weight:bold;'),
warn: (msg) => console.warn(`%c${LOG_PREFIX} ${msg}`, 'color:#E6A23C;font-weight:bold;'),
error: (msg) => console.error(`%c${LOG_PREFIX} ${msg}`, 'color:#F56C6C;font-weight:bold;'),
success: (msg) => console.log(`%c${LOG_PREFIX} ${msg}`, 'color:#67C23A;font-weight:bold;'),
};
// ===================== 工具函数 =====================
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function $(selector) {
return document.querySelector(selector);
}
function $$(selector) {
return document.querySelectorAll(selector);
}
function safeClick(selector) {
const el = typeof selector === 'string' ? $(selector) : selector;
if (el) {
el.click();
return true;
}
return false;
}
// 目录项有时是带有默认 href 的链接。直接调用 .click() 会在
// Vue/React 事件未接管时触发默认导航,智慧树会被带回课程首页。
// 仅拦截明确指向站点根页面的默认导航,保留目录组件自己的点击事件。
function isRootNavigationHref(href) {
if (!href) return false;
try {
const target = new URL(href, location.href);
if (target.origin !== location.origin) return false;
const path = (target.pathname || '/').replace(/\/+$/, '') || '/';
const hashPath = target.hash.replace(/^#/, '').split('?')[0].replace(/\/+$/, '') || '/';
return (path === '/' || path === '/index.html' || path === '/home') &&
(!target.hash || /^(?:\/|\/home|\/index|\/index.html)$/.test(hashPath));
} catch (e) {
return false;
}
}
function triggerLessonClick(item) {
if (!item || !item.isConnected) return false;
const clickTarget = isAICourseClass()
? (item.closest?.('a,button,[role="button"]') || item)
: item;
try {
clickTarget.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'smooth' });
} catch (_) {}
const anchor = isAICourseClass() ? clickTarget.closest?.('a[href]') :
(clickTarget.matches?.('a[href]') ? clickTarget : clickTarget.querySelector?.('a[href]'));
const href = anchor?.getAttribute('href') || anchor?.href || '';
// AI 资源卡里的首页/退出链接不能作为下一资源入口。取消默认事件
// 无法取消 Vue 的 router.push,必须在触发监听器之前拒绝这种链接。
if (isAICourseClass() && anchor && href && href !== '#') {
try {
const target = new URL(href, location.href);
if (!/^https?:$/.test(target.protocol) || target.origin !== location.origin ||
isRootNavigationHref(target.href) || /\/(?:login|logout|passport)(?:\/|$)/i.test(target.pathname)) return false;
} catch (_) { return false; }
}
if (anchor && isRootNavigationHref(href)) {
// dispatchEvent 可让框架监听器正常收到点击;捕获阶段只取消
// 根首页的浏览器默认动作,避免 fallback href 改写当前课程状态。
const guard = (event) => event.preventDefault();
anchor.addEventListener('click', guard, true);
try {
clickTarget.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
clickTarget.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
clickTarget.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
anchor.dispatchEvent(new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
}));
return true;
} finally {
anchor.removeEventListener('click', guard, true);
}
}
try {
clickTarget.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
clickTarget.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
clickTarget.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
clickTarget.dispatchEvent(new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
}));
return true;
} catch (e) {
try { item.click(); return true; } catch (_) { return false; }
}
}
function safeRemove(selector) {
const el = $(selector);
if (el) {
el.remove();
return true;
}
return false;
}
// 判断页面类型
function isFusionClass() {
return location.href.includes('fusioncourseh5.zhihuishu.com') ||
location.href.includes('fusioncourseh5') ||
!!document.querySelector('.chapter-item');
}
function isNewVersion() {
return isFusionClass();
}
function isHikeClass() {
return location.href.includes('hike.zhihuishu.com');
}
// 智慧树 AI 课程使用独立的 learnPage/singleCourse 路由,目录和资源卡
// 也不再复用普通共享课的 .child-info/.clearfix.video 结构。这里只做
// 页面和 DOM 适配,不直接请求课程接口,避免影响平台自身的数据流。
function isAICourseClass() {
const host = location.hostname || '';
const path = location.pathname || '';
return host === 'ai-smart-course-student-pro.zhihuishu.com' &&
(/\/learnPage\//.test(path) || /\/singleCourse\/knowledgeStudy\//.test(path)) ||
host === 'hike-teaching-center.polymas.com' && path.includes('/stu-hike/agent-course-hike/ai-course-center') ||
host === 'onlineweb.zhihuishu.com' && /(learn|study|course)/i.test(path);
}
function getAICourseResourceItems() {
const selectors = [
'.resources-list .basic-info-video-card-container',
'.resources-list [class*="basic-info"][class*="card-container"]',
'.basic-info-video-card-container',
'[class*="basic-info"][class*="card-container"]',
'.resources-list [data-resources-uid]',
'.resources-list [data-resource-uid]',
'.resource-list .resource-card',
'.resource-card[data-uid]',
];
const seen = new Set();
const items = [];
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(item => {
if (!seen.has(item)) {
seen.add(item);
items.push(item);
}
});
});
// 同一张卡片的外层和 data-resource-* 子节点可能同时命中。
// 保持页面顺序并只保留外层卡片,避免一次切换被当成两课。
return items.filter(item => !items.some(other => other !== item && other.contains(item)))
.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1);
}
function getAICoursePointItems() {
const selectors = [
'.section-item-collapse-info',
'.collapse-item-sub',
'[data-knowledge-id]',
'[data-point-id]',
];
const seen = new Set();
const items = [];
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(item => {
if (!seen.has(item)) {
seen.add(item);
items.push(item);
}
});
});
return items.filter(item => !items.some(other => other !== item && item.contains(other)))
.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1);
}
function getAICourseItems() {
const resources = getAICourseResourceItems();
if (resources.length) return resources;
return getAICoursePointItems();
}
function getAICourseItemProgress(item) {
if (!item) return 0;
const studyStatus = item.getAttribute('data-study-status');
const status = studyStatus || item.getAttribute('data-status') || item.getAttribute('data-learn-status');
if (studyStatus === '1' || /^(done|finished|completed|已学|已完成)$/i.test(String(status || '').trim())) return 100;
// schedule 可能是已观看秒数,通用 data-status=1 也可能仅代表可用状态。
const attrNames = ['data-progress', 'data-percent', 'aria-valuenow'];
for (const name of attrNames) {
const raw = item.getAttribute(name);
if (raw !== null && /^\s*\d+(?:\.\d+)?\s*%?\s*$/.test(raw)) return parseProgress(raw);
}
const progress = item.querySelector('[aria-valuenow], [data-progress], [class*="progress"]');
if (progress) {
const raw = progress.getAttribute('aria-valuenow') || progress.getAttribute('data-progress') || progress.textContent;
if (/^\s*\d+(?:\.\d+)?\s*%?\s*$/.test(raw)) return parseProgress(raw);
}
const statusNodes = [item, ...item.querySelectorAll('[class*="status"], [class*="progress"], .tag, .el-tag')];
if (statusNodes.some(node => /^(?:已学|已完成|学习完成|完成学习|100%)$/.test((node.textContent || '').trim()))) return 100;
return 0;
}
function isAICourseItemComplete(item) {
return getAICourseItemProgress(item) >= 100;
}
function getActiveAICourseItem() {
const items = getAICourseItems();
if (!items.length) return null;
const activeSelectors = [
'.active', '.current', '.selected', '.is-active', '.current-resource',
'[aria-current="true"]', '[aria-selected="true"]', '[data-active="true"]',
];
const marked = items.filter(item => activeSelectors.some(selector => item.matches(selector)) ||
activeSelectors.slice(4).some(selector => item.querySelector(selector)));
if (marked.length === 1) return marked[0];
// 多个目录都留有 active 时不能默认采用第一张旧卡片。
if (marked.length > 1) {
const visible = marked.filter(isElementVisible);
return visible.length === 1 ? visible[0] : null;
}
const video = Array.from($$('video')).find(isElementVisible);
if (video) {
let current = video.closest('.basic-info-video-card-container, [class*="resource"], .resources-list');
if (current && !items.includes(current)) {
current = items.find(item => item.contains(video) || item.contains(current));
}
if (current) return current;
}
return null;
}
function isStudyPage() {
return location.href.includes('studyh5.zhihuishu.com') ||
location.href.includes('studyvideoh5.zhihuishu.com') ||
location.href.includes('fusioncourseh5.zhihuishu.com') ||
location.href.includes('hike.zhihuishu.com') ||
isAICourseClass();
}
function isExamPage() {
const url = location.href;
const aiExamHost = /ai-smart-course-student-pro\.zhihuishu\.com|studentexamcomh5\.zhihuishu\.com|ai\.zhihuishu\.com/i.test(location.hostname);
const aiExamRoute = /\/studentReviewTestOrExam\/|\/examPreview\/|\/point\//i.test(location.pathname);
// 只匹配实际做题页面,不匹配作业/考试列表页
return url.includes('examh5.zhihuishu.com') ||
url.includes('onlineexamh5new.zhihuishu.com') ||
url.includes('studentexambaseh5.zhihuishu.com') ||
url.includes('/stuExamWeb.html') ||
url.includes('/webExamList/dohomework/') ||
url.includes('/webExamList/doexamination/') ||
(aiExamHost && aiExamRoute) ||
// 通用: 页面内存在题目容器(延迟检测)
(url.includes('zhihuishu.com') && hasStandaloneExamQuestion('.examPaper_subject, .questionBox, .ques-detail')) ||
(aiExamHost && hasStandaloneExamQuestion('.questionContent .el-radio-group, .questionContent .el-checkbox-group, .questionContent ul.radio-view, .questionContent ul.checkbox-view'));
}
function hasStandaloneExamQuestion(selector) {
return Array.from(document.querySelectorAll(selector)).some(node =>
isElementVisible(node) && !node.closest('.el-dialog, .el-message-box, [role="dialog"], .dialog-test, .ai-class-exercise-dialog') &&
!getStudyExerciseDialog()?.contains(node));
}
function isAiExamHost() {
const hostMatched = /ai-smart-course-student-pro\.zhihuishu\.com|studentexamcomh5\.zhihuishu\.com|ai\.zhihuishu\.com/i.test(location.hostname);
if (!hostMatched) return false;
if (/\/studentReviewTestOrExam\/|\/examPreview\/|\/point\//i.test(location.pathname)) return true;
// AI 课程把练习嵌在 singleCourse/knowledgeStudy 路由中,题目通常异步挂载。
// 只有检测到题目容器时才切到 AI 答题模式,避免普通学习页误显示答题面板。
return /\/singleCourse\/knowledgeStudy\//i.test(location.pathname) &&
hasStandaloneExamQuestion('.questionContent, .questionTitle, .exam-preview, [class*="question-content"]');
}
// ===================== 平台提示保护 =====================
// 平台根节点和弹层都由框架管理,不能根据后代文字删除/隐藏它们。
// 错误提示的确认/关闭回调也可能导航到首页,因此只暂停自动操作。
function getBlockingPlatformDialog() {
return Array.from(document.querySelectorAll('.el-dialog, .el-message-box, [role="dialog"], .ant-modal'))
.find(dialog => {
if (!isElementVisible(dialog) || isStudyExerciseDialog(dialog)) return false;
const text = dialogTextWithoutControls(dialog);
const fullText = (dialog.textContent || '').replace(/\s+/g, '');
return !text || /异常脚本|关闭插件|异常行为|登录|重新登录|网络(?:错误|异常)|加载失败|播放失败|操作失败|访问异常|安全验证|身份验证/.test(fullText);
}) || null;
}
function hasBlockingPlatformDialog() {
return !!getBlockingPlatformDialog();
}
let lastPlatformDialog = null;
function checkPlatformDialogs() {
const dialog = getBlockingPlatformDialog();
if (!dialog) {
lastPlatformDialog = null;
return false;
}
completionSince = 0;
if (dialog !== lastPlatformDialog) {
lastPlatformDialog = dialog;
log.warn('平台提示尚未处理,已暂停自动操作并保留原页面');
updatePanelStatus('⏸ 请先处理页面提示');
}
return true;
}
// ===================== 核心功能模块 =====================
// --- 获取视频元素 ---
function getVideo() {
return $('video');
}
const STALL_TIMEOUT_MS = 120000;
const SWITCH_COOLDOWN_MS = 8000;
let switchInProgress = false;
let nextSwitchAllowedAt = 0;
let verifyBlocking = false;
let progressHealth = {
lessonKey: '',
percent: -1,
currentTime: -1,
lastChangedAt: Date.now(),
recoveryCount: 0,
};
let completionSince = 0;
let completionLessonKey = '';
let endedPlayback = null;
function isElementVisible(element) {
if (!element) return false;
const style = getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== 'none' &&
style.visibility !== 'hidden' &&
Number.parseFloat(style.opacity || '1') > 0.05 &&
rect.width > 0 && rect.height > 0;
}
function isVerificationVisible() {
return isElementVisible($('.yidun_modal__title'));
}
function parseProgress(raw) {
const text = String(raw ?? '').replace(/\s+/g, '');
const match = text.match(/(\d+(?:\.\d+)?)%?/);
if (!match) return 0;
const value = Number(match[1]);
if (!Number.isFinite(value)) return 0;
return Math.max(0, Math.min(100, value));
}
function formatProgress(raw) {
const value = parseProgress(raw);
return `${Number.isInteger(value) ? value : value.toFixed(1)}%`;
}
function getLessonItems() {
if (isAICourseClass()) return getAICourseItems();
if (isFusionClass()) return Array.from($$('.chapter-item'));
if (isHikeClass()) return Array.from($$('.file-item'));
const wisdomItems = Array.from($$('.child-info.hasvideo'));
return wisdomItems.length ? wisdomItems : Array.from($$('.clearfix.video'));
}
function getActiveLesson() {
if (isAICourseClass()) return getActiveAICourseItem();
if (isFusionClass()) {
const activeChild = $('.chapter-content-second.current');
return activeChild ? activeChild.closest('.chapter-item') : null;
}
if (isHikeClass()) return $('.file-item.active');
if ($('.child-info.hasvideo')) return $('.child-info.hasvideo.current');
return $('.clearfix.video.current_play');
}
function getLessonKey(item = getActiveLesson()) {
if (!item) return '';
const explicit = item.getAttribute('data-id') || item.getAttribute('data-lesson-id') ||
item.getAttribute('data-resources-uid') || item.getAttribute('data-resource-uid') ||
item.getAttribute('data-knowledge-id') || item.getAttribute('data-point-id') ||
(isAICourseClass() ? item.getAttribute('data-uid') : '');
if (explicit) return explicit;
if (isAICourseClass()) {
const nestedId = item.querySelector('[data-resources-uid], [data-resource-uid], [data-knowledge-id], [data-point-id], [data-uid]');
if (nestedId) {
const key = ['data-resources-uid', 'data-resource-uid', 'data-knowledge-id', 'data-point-id', 'data-uid']
.map(name => nestedId.getAttribute(name)).find(Boolean);
if (key) return key;
}
// 百分比、已学时长会动态改变,不能并入课时身份导致播放器失去绑定。
const heading = item.querySelector('.title-text, [title], [class*="title"], [class*="name"]');
const title = heading?.getAttribute('title') || heading?.textContent;
if (title?.trim()) return title.replace(/\s+/g, ' ').trim().slice(0, 120);
}
const title = item.querySelector('[title]')?.getAttribute('title');
return (title || item.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 120);
}
function isLessonComplete(item) {
if (!item) return false;
if (isAICourseClass()) return isAICourseItemComplete(item);
if (item.querySelector('.finish-icon, .icon-finish, .time_icofinish, .child-check')) {
return true;
}
const progress = item.querySelector(
'.el-progress[aria-valuenow], .progress-num, .rate, [role="progressbar"][aria-valuenow]'
);
if (!progress) return false;
const value = progress.getAttribute('aria-valuenow') ?? progress.textContent;
return parseProgress(value) >= 100;
}
function resetProgressHealth() {
progressHealth = {
lessonKey: getLessonKey(),
percent: parseProgress(getCourseProgress()),
currentTime: Number(getVideo()?.currentTime) || 0,
lastChangedAt: Date.now(),
recoveryCount: 0,
};
}
function monitorProgressHealth(progress) {
if (verifyBlocking || isVerificationVisible()) return;
const video = getVideo();
if (!video || video.paused || video.ended || video.readyState < 2) {
progressHealth.lastChangedAt = Date.now();
return;
}
const lessonKey = getLessonKey();
const currentTime = Number(video.currentTime);
const moved = Number.isFinite(currentTime) &&
Number.isFinite(progressHealth.currentTime) &&
currentTime > progressHealth.currentTime + 0.25;
if (lessonKey !== progressHealth.lessonKey ||
progress !== progressHealth.percent || moved) {
progressHealth.lessonKey = lessonKey;
progressHealth.percent = progress;
progressHealth.currentTime = Number.isFinite(currentTime) ? currentTime : progressHealth.currentTime;
progressHealth.lastChangedAt = Date.now();
progressHealth.recoveryCount = 0;
return;
}
if (Date.now() - progressHealth.lastChangedAt < STALL_TIMEOUT_MS) return;
if (progressHealth.recoveryCount === 0) {
progressHealth.recoveryCount = 1;
progressHealth.lastChangedAt = Date.now();
log.warn('视频和平台进度连续 120 秒未推进,正在尝试恢复播放');
updatePanelStatus('⚠️ 进度停滞,正在恢复...');
video.play().catch(() => {});
return;
}
log.error('视频和平台进度连续 120 秒未推进,已停止自动任务');
updatePanelStatus('⏹ 进度停滞,请手动检查页面');
stopAllTasks();
}
// --- 页面优化:关闭弹窗、移除多余元素 ---
async function optimizePage(canContinue = () => true) {
await sleep(2000);
if (!canContinue()) return;
// 与定时清理共用规则,不能把随堂弹题的关闭图标当成普通提示。
closePopups();
if (!isNewVersion() && !isHikeClass() && !isAICourseClass()) {
// 自动夜间模式 (18:00 ~ 7:00)
const hour = new Date().getHours();
if (hour >= 18 || hour < 7) {
const darkBtn = $('.Patternbtn-div');
if (darkBtn) darkBtn.click();
}
// 移除无关元素
safeRemove('.exploreTip');
// AI 助教及消息容器可能承载题目,保留平台组件及其事件状态。
}
log.info('页面优化完成');
}
// --- 保存原始 pause,交给播放保活任务决定是否恢复 ---
function overridePause() {
const video = getVideo();
if (video && !video._pauseOverridden) {
video._autovisorOriginalPause = video.pause.bind(video);
video._pauseOverridden = true;
log.info('已保存视频 pause 方法,由播放保活任务负责恢复');
}
}
function pauseForVerification() {
const video = getVideo();
if (video && !video.paused && typeof video._autovisorOriginalPause === 'function') {
video._autovisorOriginalPause();
}
}
// --- 静音控制 ---
function setMute() {
if (!config.soundOff) return;
const video = getVideo();
if (video && video.volume !== 0) {
video.volume = 0;
const volumeBox = $('.volumeBox');
if (volumeBox) volumeBox.classList.add('volumeNone');
log.info('已静音');
}
}
// --- 倍速控制 ---
function setSpeed() {
const video = getVideo();
if (!video) return;
const speed = Math.max(0.5, Math.min(config.playbackSpeed, 1.8));
if (video.playbackRate !== speed) {
video.playbackRate = speed;
const speedLabel = $('.speedBox span');
if (speedLabel) speedLabel.innerText = `X ${speed}`;
log.info(`已设置倍速: ${speed}x`);
}
}
// --- 检测暂停并续播 ---
async function checkAndPlay() {
if (!config.autoPlay) return;
if (isVerificationVisible()) {
verifyBlocking = true;
pauseForVerification();
return;
}
verifyBlocking = false;
// 弹题仍待处理时平台会主动暂停;不要和弹题处理流程争抢播放。
if (exerciseInProgress || hasBlockingStudyDialog()) return;
const video = getVideo();
// 对 ended 的视频调用 play() 会从头重播,让下一集检测错过结束状态。
if (video && video.paused && !video.ended) {
try {
await video.play();
log.info('检测到视频暂停,已恢复播放');
} catch (e) {
// 自动播放可能被浏览器策略阻止
log.warn('自动播放被阻止,请手动点击播放');
}
}
}
// --- 随堂弹题 ---
let exerciseInProgress = false;
let exerciseGeneration = 0;
const aiExerciseAttempts = new WeakMap();
const aiExerciseTransitions = new WeakMap();
const studyExerciseAnswerCache = new WeakMap();
function getStudyAnswerMode() {
const mode = GM_getValue('av_study_answer_mode', 'ai');
return ['ai', 'free', 'paid'].includes(mode) ? mode : 'ai';
}
function invalidateStudyExerciseAnswer() {
exerciseGeneration++;
const dialog = getStudyExerciseDialog();
if (dialog) {
aiExerciseAttempts.delete(dialog);
studyExerciseAnswerCache.delete(dialog);
}
}
function setStudyAnswerMode(mode) {
if (!['ai', 'free', 'paid'].includes(mode)) return false;
if (mode !== getStudyAnswerMode()) {
GM_setValue('av_study_answer_mode', mode);
invalidateStudyExerciseAnswer();
}
return true;
}
function isAiStudyExerciseDialog(dialog) {
if (!dialog) return false;
const heading = dialog.querySelector('.el-dialog__title, .dialog-title, .modal-title, h1, h2, h3');
const title = `${heading?.textContent || ''} ${(dialog.textContent || '').slice(0, 180)}`.replace(/\s+/g, '');
return dialog.matches('.ai-class-exercise-dialog') || /AI助教.{0,30}(?:出题|随堂|练习)|AI随堂(?:练习|测试)/i.test(title) ||
(isAICourseClass() && !!dialog.querySelector('.questionContent .el-radio-group, .questionContent .el-checkbox-group'));
}
function getAiExerciseChoices(dialog) {
const selectors = [
'.topic-item', 'label.el-radio, label.el-checkbox', '[role="radio"], [role="checkbox"]',
'.questionContent ul.radio-view > li, .questionContent ul.checkbox-view > li',
'.option-item, .options-item', '.topic-list > li, .option-list > li, .options-list > li', '.option',
'input[type="radio"], input[type="checkbox"]',
];
for (const selector of selectors) {
const choices = Array.from(dialog.querySelectorAll(selector)).filter(isElementVisible)
.filter(node => !node.closest('.answer-analysis, .analysis, .answer-result'));
if (choices.length >= 2) return choices.filter(node => !choices.some(other => other !== node && other.contains(node)));
}
return [];
}
function readAiExerciseQuestion(dialog) {
const choices = getAiExerciseChoices(dialog);
if (choices.length < 2) return null;
const cleanOption = value => formatString(value).replace(/^\s*[A-H][..、::))\s]+\s*/i, '').trim();
const options = choices.map(choice => cleanOption(choice.textContent || choice.getAttribute('aria-label') ||
(choice.labels && Array.from(choice.labels).map(label => label.textContent).join(' ')) || choice.value || ''));
if (options.some(option => !option)) return null;
const candidates = Array.from(dialog.querySelectorAll('.topic-title, .question-title, .questionTitle .centent-pre, .questionTitle, .question-stem, .stem, .subject-title'));
const usableText = node => {
if (!isElementVisible(node) || choices.some(choice => choice === node || choice.contains(node) || node.contains(choice))) return '';
if (node.closest('.el-dialog__header, .el-dialog__footer, .answer, .analysis, .answer-analysis, .answer-result, button, [role="button"]')) return '';
const text = formatString(node.textContent || '').trim();
if (text.length < 4 || /AI助教|学习过程中有问题|随时提问|^\s*[((【\[]?(?:单选题?|多选题?|判断题?)[))】\]]?\s*$/i.test(text)) return '';
return text;
};
let question = candidates.map(usableText).find(Boolean) || '';
if (!question) {
const fallback = Array.from(dialog.querySelectorAll('p, div, span')).filter(node =>
!Array.from(node.children).some(child => /\S/.test(child.textContent || '')));
question = fallback.map(usableText).find(Boolean) || '';
}
if (!question) return null;
const text = (dialog.textContent || '').replace(/\s+/g, '');
const type = /多选|多项选择|不定项/.test(text) || choices.some(choice =>
choice.matches('.el-checkbox, [role="checkbox"], input[type="checkbox"]') || choice.querySelector('input[type="checkbox"]')) ? 1 :
/判断题|是非题/.test(text) ? 3 : 0;
const current = dialog.querySelector('.number.active, .number.current, .number.is-active, .number[aria-selected="true"]');
const contentKey = JSON.stringify([question, options, type]);
const key = JSON.stringify([current?.textContent || '', contentKey]);
return { question, options, choices, type, key, contentKey };
}
function matchAiExerciseAnswers(question, answers) {
const normalize = text => formatString(String(text || '')).replace(/\s+/g, '').toLowerCase();
const truth = text => /^(正确|对|是|√|true|t)$/i.test(normalize(text)) ? true :
/^(错误|错|否|×|false|f)$/i.test(normalize(text)) ? false : null;
const indices = new Set();
const normalizedAnswers = (Array.isArray(answers) ? answers : answers == null ? [] : [answers]).flatMap(answer => {
const value = String(answer ?? '').trim();
if (question.options.some(option => normalize(option) === normalize(value))) return [value];
if (question.type === 1 && /^(?:[A-H]{2,8}|[A-H](?:\s*[,,、;;|/\s]\s*[A-H])+)$/.test(value.toUpperCase())) {
return value.toUpperCase().match(/[A-H]/g) || [];
}
if (question.type === 1 && value.includes('||')) return value.split('||').map(part => part.trim());
return [value];
});
for (const answer of normalizedAnswers) {
const value = String(answer || '').trim();
let index = -1;
if (/^[A-H]$/i.test(value)) index = value.toUpperCase().charCodeAt(0) - 65;
else {
const matching = question.options.map((option, i) => normalize(option) === normalize(value) ||
(question.type === 3 && truth(value) !== null && truth(option) === truth(value)) ? i : -1).filter(i => i >= 0);
if (matching.length === 1) index = matching[0];
}
if (index < 0 || index >= question.choices.length) return null;
indices.add(index);
}
if (!indices.size || (question.type !== 1 && indices.size !== 1)) return null;
return indices;
}
async function answerAiStudyExercise(dialog, canContinue) {
if (hasExerciseAnswer(dialog)) return true;
const question = readAiExerciseQuestion(dialog);
if (!question) {
updatePanelStatus('⏳ 等待 AI 助教题目和选项加载');
return false;
}
const mode = getStudyAnswerMode();
const sourceName = { ai: 'AI', free: '免费题库', paid: '付费题库' }[mode];
const canAnswer = () => canContinue() && getStudyAnswerMode() === mode &&
(mode !== 'free' || isVerifyValid());
if (mode === 'free' && !isVerifyValid()) {
updatePanelStatus('⏸ 请在助教答题设置中验证免费题库验证码');
return false;
}
if (!canAnswer()) return false;
const previous = aiExerciseAttempts.get(dialog);
if (previous?.key === question.key && previous.mode === mode && Date.now() < previous.retryAt) return false;
const sameQuestionContent = () => canAnswer() && dialog.isConnected && isElementVisible(dialog) &&
readAiExerciseQuestion(dialog)?.key === question.key;
const sameQuestion = () => sameQuestionContent() &&
question.choices.every(choice => choice.isConnected && dialog.contains(choice));
const confirmedOriginal = () => {
if (!canAnswer() || !dialog.isConnected || !isElementVisible(dialog) || !hasExerciseAnswer(dialog)) return false;
const current = readAiExerciseQuestion(dialog);
return !current || current.key === question.key;
};
aiExerciseAttempts.set(dialog, { key: question.key, mode, retryAt: Date.now() + 20000 });
updatePanelStatus(`正在使用${sourceName}回答助教随堂题`);
const cached = studyExerciseAnswerCache.get(dialog);
const reusable = cached && cached.key === question.key && cached.mode === mode && cached.route === location.href;
const result = reusable ? cached.result : await searchAnswer(question.question, question.options, question.type, mode);
// 提交未确认时复用本题答案;付费查询即便响应异常也不自动重复扣费。
// 同题重绘只取消旧节点点击,仍保留结果;换题、停止或切换来源不缓存旧请求。
if (!reusable && sameQuestionContent() && (mode === 'paid' || result?.answers?.length)) {
studyExerciseAnswerCache.set(dialog, { key: question.key, mode, route: location.href, result });
}
if (!sameQuestion()) return confirmedOriginal();
// 用户在查询期间自行提交时,以平台结果为准,不再覆盖答案。
if (hasExerciseAnswer(dialog)) return true;
const targets = matchAiExerciseAnswers(question, result?.answers);
if (!targets) {
log.warn(`${sourceName}未取得可匹配的助教题答案,保留题窗`);
updatePanelStatus(mode === 'ai' ? '⚠️ AI 暂无可用答案,请检查配置或切换来源' :
`⚠️ ${sourceName}暂无可用答案,请切换来源或手动作答`);
return false;
}
// 多选先清除错误勾选,单选由平台原生互斥;每一步都核对仍是原题。
const indices = question.type === 1 ? question.choices.map((_choice, i) => i) : [...targets];
for (const index of indices) {
if (confirmedOriginal()) return true;
if (!sameQuestion()) return false;
if (hasExerciseAnswer(dialog)) return true;
const choice = question.choices[index];
if (isExerciseChoiceSelected(choice) === targets.has(index)) continue;
if (choice.disabled || choice.matches('.is-disabled, [aria-disabled="true"]') || choice.querySelector('input:disabled')) return false;
choice.click();
await sleep(150);
}
if (!sameQuestion()) return confirmedOriginal();
if (hasExerciseAnswer(dialog)) return true;
const selected = question.choices.map(isExerciseChoiceSelected);
if (!selected.every((checked, i) => checked === targets.has(i))) {
updatePanelStatus('⚠️ AI 助教选项未选中,请检查题目');
return false;
}
const submit = findExerciseButton(dialog, /^(提交(?:答案|作答)?|确定|确认(?:答案|作答)?|完成作答)$/);
if (submit) submit.click();
if (!await waitForExerciseAnswer(dialog, canAnswer, question.key)) {
updatePanelStatus('⚠️ AI 助教题尚未确认提交,已保留题窗');
return false;
}
return true;
}
function isStudyExerciseDialog(dialog) {
if (!dialog) return false;
return dialog.matches('.dialog-test, .ai-class-exercise-dialog') || isAiStudyExerciseDialog(dialog) ||
!!dialog.querySelector('.topic-item') ||
(!!dialog.querySelector('.number') && /弹题|出题|随堂|练习/.test(dialog.textContent || ''));
}
function getStudyExerciseDialog() {
return Array.from($$('.dialog-test, .el-dialog, .el-message-box, .ai-class-exercise-dialog, [role="dialog"]'))
.find(dialog => isElementVisible(dialog) && isStudyExerciseDialog(dialog)) || null;
}
function isUnansweredExerciseWarning(dialog) {
return /未[作做]答.*(?:不能|无法)关闭/.test((dialog?.textContent || '').replace(/\s+/g, ''));
}
function hasBlockingStudyDialog() {
return hasBlockingPlatformDialog() || !!getStudyExerciseDialog() || Array.from($$('.el-message-box, .el-dialog'))
.some(dialog => isElementVisible(dialog) && !isStudyExerciseDialog(dialog) && isUnansweredExerciseWarning(dialog));
}
function dismissExerciseWarning() {
// 只关闭明确的“未作答不能关闭”提示,保留下面的题目继续处理。
Array.from($$('.el-message-box, .el-dialog')).forEach(dialog => {
if (!isElementVisible(dialog) || isStudyExerciseDialog(dialog) || !isUnansweredExerciseWarning(dialog)) return;
const close = dialog.querySelector('.el-message-box__headerbtn, .el-dialog__headerbtn, .iconfont.iconguanbi');
if (close && isElementVisible(close)) close.click();
});
}
function isExerciseChoiceSelected(choice) {
return choice.matches('input:checked, .active, .selected, .checked, .is-checked, [aria-checked="true"]') ||
!!choice.querySelector('input:checked, .is-checked, [aria-checked="true"]');
}
function getExerciseQuestionKey(dialog) {
if (isAiStudyExerciseDialog(dialog)) return readAiExerciseQuestion(dialog)?.contentKey || '';
return Array.from(dialog.querySelectorAll('.topic-title, .topic-item')).filter(isElementVisible)
.map(node => (node.textContent || node.getAttribute('aria-label') || node.value || '').trim()).join('\n');
}
async function showExerciseQuestion(dialog, number, canContinue) {
// 题号可能先变蓝、题干稍后才返回。不能沿用上一题尚未消失的 .answer。
const aiQuestion = isAiStudyExerciseDialog(dialog);
const previousTransition = aiQuestion ? aiExerciseTransitions.get(dialog) : null;
const active = number.matches('.active, .current, .is-active, [aria-selected="true"], [aria-current="true"]');
if (active && (!previousTransition || previousTransition.number !== number)) return true;
const previousKey = getExerciseQuestionKey(dialog);
let transition = previousTransition?.number === number ? previousTransition : null;
if (aiQuestion && !transition) {
const before = readAiExerciseQuestion(dialog);
transition = { number, contentKey: before?.contentKey || previousKey, choices: before?.choices || [],
results: Array.from(dialog.querySelectorAll('.answer, .answer-result, .question-result, .result-content, .answer-analysis, .analysis, .correct-answer, [role="status"]'))
.filter(isElementVisible).map(node => ({ node, text: node.textContent || '' })) };
aiExerciseTransitions.set(dialog, transition);
}
if (!active) number.click();
for (let attempt = 0; attempt < 30; attempt++) {
if (!canContinue() || !dialog.isConnected || !isElementVisible(dialog)) return false;
if (aiQuestion) {
const current = readAiExerciseQuestion(dialog);
const bodyChanged = current && (current.contentKey !== transition.contentKey ||
(transition.choices.length > 0 && transition.choices.every(choice => !dialog.contains(choice))));
const staleResult = transition.results.some(({ node, text }) => dialog.contains(node) &&
isElementVisible(node) && (node.textContent || '') === text);
if (bodyChanged && !staleResult) {
aiExerciseTransitions.delete(dialog);
return true;
}
} else {
const key = getExerciseQuestionKey(dialog);
if (key && key !== previousKey) return true;
}
await sleep(100);
}
return false;
}
function hasExerciseAnswer(dialog) {
// .answer 是平台作答后的答案/解析区。仅看当前弹题内可见的结果,
// 不能用页面其他题目的 .answer 或单纯勾选状态当作提交成功。
const aiDialog = isAiStudyExerciseDialog(dialog);
const legacyResult = !aiDialog && Array.from(dialog.querySelectorAll('.answer')).some(answer =>
isElementVisible(answer) && !answer.querySelector('.topic-item') &&
!!((answer.textContent || '').trim() || answer.querySelector('img')));
if (legacyResult) return true;
if (!aiDialog) return false;
return Array.from(dialog.querySelectorAll('.answer, .answer-result, .question-result, .result-content, .answer-analysis, .analysis, .correct-answer, [role="status"]'))
.filter(result => !getAiExerciseChoices(result).length)
.some(result => isElementVisible(result) && /正确答案|参考答案|回答正确|回答错误|回答[对错]了|作答成功|提交成功|已作答|答案解析/.test(result.textContent || ''));
}
function findExerciseButton(dialog, textPattern) {
return Array.from(dialog.querySelectorAll('button, .btn, [role="button"], .el-button'))
.find(button => isElementVisible(button) && !button.disabled &&
!button.classList.contains('is-disabled') && button.getAttribute('aria-disabled') !== 'true' &&
textPattern.test((button.textContent || '').replace(/\s+/g, '')));
}
async function waitForExerciseAnswer(dialog, canContinue, questionKey = '') {
for (let attempt = 0; attempt < (questionKey ? 40 : 20); attempt++) {
if (!canContinue()) return false;
if (!isElementVisible(dialog) || !dialog.isConnected) return true;
const current = questionKey ? readAiExerciseQuestion(dialog) : null;
if (questionKey && current && current.key !== questionKey) return false;
if (hasExerciseAnswer(dialog)) return true;
await sleep(100);
}
return false;
}
async function handleFusionExercise(canContinue = () => true) {
if (!isFusionClass()) return false;
const dialog = $('.ai-class-exercise-dialog');
if (!isElementVisible(dialog) || !canContinue()) return false;
try {
const option = dialog.querySelector('.option');
if (option && !isExerciseChoiceSelected(option)) option.click();
await sleep(600);
if (!canContinue() || !isElementVisible(dialog)) return false;
const submit = dialog.querySelector('.el-dialog__footer button.el-button');
if (submit && !submit.disabled && !submit.classList.contains('is-disabled')) submit.click();
await sleep(1000);
if (!canContinue()) return false;
log.info('融合课 AI 随堂练习已尝试提交');
return Boolean(option || submit);
} catch (e) {
log.warn('融合课 AI 随堂练习处理失败: ' + e.message);
return false;
}
}
async function skipQuestions() {
if (!config.autoAnswer || exerciseInProgress) return;
if (isHikeClass()) return; // 翻转课不支持自动答题
if (isVerificationVisible()) {
verifyBlocking = true;
pauseForVerification();
return;
}
const dialog = getStudyExerciseDialog();
if (!dialog) return;
const generation = exerciseGeneration;
const route = location.href;
const canContinue = () => generation === exerciseGeneration && route === location.href && config.autoAnswer &&
!isVerificationVisible() && !hasBlockingPlatformDialog();
exerciseInProgress = true;
try {
dismissExerciseWarning();
if (isFusionClass() && !isAiStudyExerciseDialog(dialog)) {
await handleFusionExercise(canContinue);
return;
}
// 目录和题目都使用 el-scrollbar__view,必须从当前可见题窗内找题。
const numbers = Array.from(dialog.querySelectorAll('.number')).filter(isElementVisible);
const total = Math.max(1, numbers.length);
log.info(`检测到 ${total} 道弹题`);
for (let index = 0; index < total; index++) {
if (!canContinue() || !dialog.isConnected || !isElementVisible(dialog)) return;
const currentNumbers = Array.from(dialog.querySelectorAll('.number')).filter(isElementVisible);
if (numbers.length > 1) {
if (!currentNumbers[index]) return;
if (!await showExerciseQuestion(dialog, currentNumbers[index], canContinue)) {
if (canContinue()) updatePanelStatus('⏳ 弹题切换未确认,请检查题目');
return;
}
}
if (!canContinue() || !dialog.isConnected || !isElementVisible(dialog)) return;
if (hasExerciseAnswer(dialog)) continue;
if (isAiStudyExerciseDialog(dialog)) {
if (!await answerAiStudyExercise(dialog, canContinue)) return;
continue;
}
const choices = Array.from(dialog.querySelectorAll('.topic-item')).filter(isElementVisible);
if (!choices.length) {
updatePanelStatus('⏳ 等待弹题选项加载');
return;
}
// 随堂练习沿用选项触发作答的流程;单选只点一次,多选保留已有勾选。
// 点击后若平台已显示答案,不能继续点击下一选项把答案反选。
if (!choices.some(isExerciseChoiceSelected)) {
const multiple = /多选|多项选择/.test(dialog.textContent || '') ||
!!dialog.querySelector('input[type="checkbox"], .el-checkbox');
for (const choice of choices.slice(0, multiple ? 2 : 1)) {
if (!canContinue() || hasExerciseAnswer(dialog)) break;
if (choice.disabled || choice.getAttribute('aria-disabled') === 'true') continue;
choice.click();
await sleep(200);
}
}
if (!canContinue()) return;
// 部分版本选择后直接显示答案,其他版本必须点击提交/确定。
const submit = findExerciseButton(dialog, /^(提交(?:答案|作答)?|确定|确认(?:答案|作答)?|完成作答)$/);
if (!hasExerciseAnswer(dialog) && submit) {
submit.click();
}
if (!await waitForExerciseAnswer(dialog, canContinue)) {
if (canContinue()) {
log.warn(`第 ${index + 1} 道弹题尚未确认作答,保留题窗等待处理`);
updatePanelStatus('⚠️ 弹题未完成,请检查题目');
}
return;
}
}
if (!canContinue() || !isElementVisible(dialog) || !dialog.isConnected) return;
const close = findExerciseButton(dialog, /^(关闭|继续学习|继续播放)$/) ||
Array.from(dialog.querySelectorAll('.el-dialog__headerbtn, .iconfont.iconguanbi')).find(isElementVisible);
if (close) {
close.click();
await sleep(350);
if (!dialog.isConnected || !isElementVisible(dialog)) log.success('弹题已完成并关闭');
else updatePanelStatus('⏳ 等待弹题关闭');
}
} catch (e) {
log.warn('处理弹题时出错: ' + e.message);
} finally {
exerciseInProgress = false;
if (canContinue() && !hasBlockingStudyDialog()) await checkAndPlay();
}
}
function dialogTextWithoutControls(dialog) {
if (!dialog) return '';
const clone = dialog.cloneNode(true);
clone.querySelectorAll('button, .el-dialog__header, .el-message-box__header, .el-dialog__footer, .el-message-box__btns, .el-dialog__headerbtn, [aria-label="关闭"], [aria-label="Close"]').forEach(node => node.remove());
return (clone.textContent || '').replace(/\s+/g, ' ').trim();
}
// 空白弹层可能仍在加载课程或题目。保留节点和遮罩供框架继续渲染,
// 不点击确认、不发送 ESC,也不删除框架持有的 DOM。
function clearBlankDialogs() {
return checkPlatformDialogs();
}
// --- 关闭其他弹窗 ---
function closePopups() {
if (isVerificationVisible()) return;
dismissExerciseWarning();
clearBlankDialogs();
if (hasBlockingPlatformDialog()) return;
document.querySelectorAll('.el-dialog').forEach(dialog => {
if (!isElementVisible(dialog) || isStudyExerciseDialog(dialog) || isUnansweredExerciseWarning(dialog)) return;
// 仅关闭已识别的学习时长提示,其他弹窗交给平台自身处理。
if (!/学习时长|学习时间.*(?:提醒|提示)|休息提醒/.test(dialogTextWithoutControls(dialog))) return;
const close = dialog.querySelector('.iconfont.iconguanbi, .el-dialog__headerbtn');
if (close && isElementVisible(close)) close.click();
});
}
// --- 检测人机验证 ---
function checkVerify() {
const visible = isVerificationVisible();
if (visible) {
verifyBlocking = true;
pauseForVerification();
log.warn('⚠️ 检测到安全验证,请手动完成验证!');
updatePanelStatus('⚠️ 需要手动验证!');
return true;
}
if (verifyBlocking) {
verifyBlocking = false;
resetProgressHealth();
updatePanelStatus('▶️ 验证完成,恢复运行');
}
return false;
}
// --- 习惯分:模拟鼠标在视频区域移动 ---
function simulateMouseMove() {
if (!config.habitScore) return;
const videoArea = $('.videoArea') || getVideo();
if (!videoArea) return;
const rect = videoArea.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const x = rect.left + rect.width / 2 + (Math.random() - 0.5) * 20;
const y = rect.top + rect.height / 2 + (Math.random() - 0.5) * 20;
videoArea.dispatchEvent(new MouseEvent('mousemove', {
bubbles: true,
clientX: x,
clientY: y,
}));
}
// AI 目录先切活动项、播放器后换源时,旧视频不能归属到新课时。
// 保留源和时间绑定,只有换源、换播放器或时间回到起点后才接管新课。
let aiPlaybackBinding = null;
function getAICoursePlayback(active) {
const video = Array.from($$('video')).find(isElementVisible);
const lessonKey = getLessonKey(active);
if (!active || !lessonKey || !video) return null;
const source = video.currentSrc || video.src || '';
const time = Number(video.currentTime) || 0;
if (!aiPlaybackBinding || aiPlaybackBinding.video !== video || aiPlaybackBinding.source !== source) {
aiPlaybackBinding = { video, source, lessonKey, time, ready: true };
} else {
if (aiPlaybackBinding.lessonKey !== lessonKey) {
aiPlaybackBinding.lessonKey = lessonKey;
aiPlaybackBinding.ready = false;
}
if (!aiPlaybackBinding.ready && !video.ended && time + 0.25 < aiPlaybackBinding.time) {
aiPlaybackBinding.ready = true;
}
aiPlaybackBinding.time = time;
}
return aiPlaybackBinding.ready ? video : null;
}
function isAICourseVideoItem(item) {
return !!item && (item.matches('.basic-info-video-card-container') ||
/^(?:video|11|22)$/i.test(item.getAttribute('data-resource-type') || item.getAttribute('data-resources-data-type') || '') ||
!!item.querySelector('video'));
}
// --- 获取当前学习进度 ---
function getCourseProgress() {
const active = getActiveLesson();
if (!active) return '0%';
if (isAICourseClass()) {
const video = getAICoursePlayback(active);
if (video &&
Number.isFinite(video.duration) && video.duration > 0 && video.currentTime >= 0) {
if (video.ended) return '100%';
const percent = Math.min(99.9, (video.currentTime / video.duration) * 100);
return `${percent.toFixed(1)}%`;
}
if (isAICourseVideoItem(active) || Array.from($$('video')).some(isElementVisible)) return '0%';
return `${getAICourseItemProgress(active)}%`;
}
if (isLessonComplete(active)) return '100%';
const progress = active.querySelector(
'.el-progress[aria-valuenow], .progress-num, .rate, [role="progressbar"][aria-valuenow]'
);
if (!progress) return '0%';
return formatProgress(progress.getAttribute('aria-valuenow') ?? progress.textContent);
}
// --- 获取未完成的课程列表 ---
function getUnfinishedLessons() {
return getLessonItems().filter(item => !isLessonComplete(item));
}
function isCurrentLessonFinished() {
if (isAICourseClass()) {
const active = getActiveLesson();
if (!active) return false;
const video = getAICoursePlayback(active);
if (video) return video.ended === true;
// 视频尚未加载/仍是上一课的源时,不采用历史目录进度跳课。
if (isAICourseVideoItem(active) || Array.from($$('video')).some(isElementVisible)) return false;
return isAICourseItemComplete(active);
}
// 平台目录的百分比可能晚于播放器结束更新,ended 同样是完成证据。
const video = getVideo();
const lessonKey = getLessonKey();
let ended = false;
if (video?.ended && lessonKey) {
const source = video.currentSrc || video.src || '';
if (!endedPlayback || endedPlayback.video !== video || endedPlayback.source !== source) {
endedPlayback = { video, source, lessonKey };
}
// 目录先切换、播放器后换源时,旧视频的 ended 不能算作新课时完成。
ended = endedPlayback.lessonKey === lessonKey;
} else {
endedPlayback = null;
}
return ended || parseProgress(getCourseProgress()) >= 100;
}
async function checkLessonProgress() {
const progress = getCourseProgress();
updatePanelProgress(progress);
if (isVerificationVisible()) {
verifyBlocking = true;
pauseForVerification();
completionSince = 0;
return;
}
verifyBlocking = false;
if (exerciseInProgress || hasBlockingStudyDialog()) {
completionSince = 0;
return;
}
monitorProgressHealth(parseProgress(progress));
const currentKey = getLessonKey();
if (currentKey !== completionLessonKey) {
completionLessonKey = currentKey;
completionSince = 0;
}
if (isCurrentLessonFinished()) {
if (!completionSince) completionSince = Date.now();
if (Date.now() - completionSince >= 2000) await switchToNext();
} else {
completionSince = 0;
}
}
// --- 切换到下一集 ---
async function switchToNext() {
if (!config.autoNext || verifyBlocking || isVerificationVisible()) return;
if (exerciseInProgress || hasBlockingStudyDialog()) return;
if (switchInProgress || Date.now() < nextSwitchAllowedAt) return;
if (!isCurrentLessonFinished()) return;
const allItems = getLessonItems();
if (allItems.length === 0) {
log.warn('未识别到课程目录,暂不判断为全部完成');
updatePanelStatus('⚠️ 未识别到课程目录');
return;
}
const active = getActiveLesson();
if (!active) {
log.warn('未识别到当前活动课时,暂不切换');
updatePanelStatus('⚠️ 未识别到当前课时');
return;
}
const currentIndex = allItems.indexOf(active);
if (currentIndex < 0) return;
const ordered = [
...allItems.slice(currentIndex + 1),
...allItems.slice(0, currentIndex + 1),
];
// 折叠章节中的目录项虽然暂不可见,仍是未完成课时。目录组件的点击
// 监听器可以接收事件,不能因此把课程误判为全部完成并关闭定时任务。
const next = ordered.find(item => item !== active && item.isConnected &&
!isLessonComplete(item));
if (!next) {
log.success('🎉 所有课程已学习完毕!');
updatePanelStatus('🎉 全部完成!');
stopAllTasks();
return;
}
switchInProgress = true;
const generation = exerciseGeneration;
nextSwitchAllowedAt = Date.now() + SWITCH_COOLDOWN_MS;
const oldKey = getLessonKey(active);
try {
log.success('当前课程已完成,正在切换下一集...');
updatePanelStatus('🔄 正在切换下一集...');
const beforeUrl = location.href;
if (!triggerLessonClick(next)) {
throw new Error('目录项不可点击');
}
log.info('已发送下一集切换请求');
const deadline = Date.now() + 8000;
let changed = false;
while (Date.now() < deadline) {
if (generation !== exerciseGeneration) return;
// 路由已离开课程时停止任务;history.back 会再次触发旧定时器,
// 造成页面反复进出,不能用历史后退修补平台自身的跳转。
if (beforeUrl !== location.href && isRootNavigationHref(location.href)) {
log.warn('检测到切换请求将返回站点首页,已阻止继续执行');
stopAllTasks();
updatePanelStatus('⏹ 已离开课程,请手动返回学习页面');
return;
}
const current = getActiveLesson();
if (current && (getLessonKey(current) !== oldKey || (!oldKey && current !== active))) {
changed = true;
break;
}
await sleep(250);
}
if (generation !== exerciseGeneration) return;
if (changed && !isRootNavigationHref(location.href)) {
afterLessonSwitch();
} else {
log.warn('切换下一集后未确认活动课时变化');
updatePanelStatus('⚠️ 课时切换未确认');
}
} catch (e) {
log.warn('切换下一集失败: ' + e.message);
updatePanelStatus('⚠️ 课时切换失败');
} finally {
switchInProgress = false;
}
}
// 切换课程后重新初始化
function afterLessonSwitch() {
overridePause();
setMute();
setSpeed();
resetProgressHealth();
completionSince = 0;
completionLessonKey = getLessonKey();
const title = getLessonTitle();
log.info(`正在学习: ${title}`);
updatePanelStatus(`▶️ ${title}`);
}
// 获取当前课程名称
function getLessonTitle() {
if (isAICourseClass()) {
const active = getActiveLesson();
if (active) {
const title = active.querySelector('.title-text, [title], [class*="title"], [class*="name"]');
const text = title?.getAttribute('title') || title?.textContent || active.textContent;
if (text && text.trim()) return text.replace(/\s+/g, ' ').trim().slice(0, 120);
}
const heading = document.querySelector('.study-main h1, .knowledge-title, h1, h2');
return heading?.textContent?.trim() || '未知课程';
}
if (isFusionClass()) {
const item = getActiveLesson();
const name = item?.querySelector('.item-name');
if (name) return name.getAttribute('title') || name.textContent.trim() || '未知课程';
}
if (isHikeClass()) {
const span = $('.file-item.active span[title]');
return span ? span.getAttribute('title') : '未知课程';
} else {
const el = $('#lessonOrder');
return el ? el.getAttribute('title') : '未知课程';
}
}
// ===================== 定时任务管理 =====================
let timers = [];
let taskGeneration = 0;
function getRuntimePageMode() {
return isExamPage() ? 'exam' : isStudyPage() ? 'study' : 'idle';
}
function watchPageContext(generation) {
let route = location.href;
let mode = getRuntimePageMode();
// 独立于学习任务检测 SPA 地址和延迟习题组件;不改写平台 history。
// 仍纳入 timers,手动停止后不会因路由变化而自行复活。
const id = setInterval(() => {
if (generation !== taskGeneration) return;
const nextRoute = location.href;
const nextMode = getRuntimePageMode();
if (route === nextRoute && mode === nextMode) return;
route = nextRoute;
mode = nextMode;
main().catch(error => log.warn('页面切换初始化失败: ' + error.message));
}, 1000);
timers.push(id);
}
function syncRuntimePanels(mode) {
const studyPanel = $('#autovisor-panel');
const examPanel = $('#av-exam-panel');
if (studyPanel) studyPanel.style.display = mode === 'exam' ? 'none' : '';
if (examPanel) examPanel.style.display = mode === 'exam' ? '' : 'none';
const trial = $('#av-trial-modal');
if (trial) trial.hidden = true;
}
function addTimer(fn, interval) {
const generation = taskGeneration;
let running = false;
const tick = async () => {
if (running || generation !== taskGeneration || !isStudyPage() || isExamPage()) return;
if (hasBlockingPlatformDialog()) {
checkPlatformDialogs();
return;
}
running = true;
try {
await fn();
} catch (e) {
log.warn('定时任务执行失败: ' + e.message);
} finally {
running = false;
}
};
const id = setInterval(tick, interval);
timers.push(id);
return id;
}
function stopAllTasks() {
taskGeneration++;
invalidateStudyExerciseAnswer();
completionSince = 0;
timers.forEach(id => clearInterval(id));
timers = [];
log.info('所有任务已停止');
}
// ===================== 悬浮控制面板 =====================
let panelStatusEl = null;
let panelProgressEl = null;
function createPanel() {
if (!config.showPanel) return;
if ($('#autovisor-panel')) return;
const panel = document.createElement('div');
panel.id = 'autovisor-panel';
panel.innerHTML = `
播放控制
⏩ 倍速
${config.playbackSpeed}x
🔇 静音
自动化
▶️ 自动播放
⏭️ 自动下一集
💬 弹题作答
🏆 习惯分
`;
document.body.appendChild(panel);
createStudyAiSettings(panel);
panelStatusEl = $('#av-status');
panelProgressEl = $('#av-progress');
// 收起/展开
$('#av-collapse').addEventListener('click', () => {
panel.classList.toggle('collapsed');
const collapsed = panel.classList.contains('collapsed');
$('#av-collapse').textContent = collapsed ? '▶' : '▼';
$('#av-collapse').setAttribute('aria-expanded', String(!collapsed));
$('#av-collapse').setAttribute('aria-label', collapsed ? '展开面板' : '收起面板');
keepPanelInViewport(panel);
});
// 倍速控制
$('#av-speed-down').addEventListener('click', () => {
config.playbackSpeed = Math.max(0.5, +(config.playbackSpeed - 0.25).toFixed(2));
saveConfig(config);
setSpeed();
$('#av-speed-val').textContent = config.playbackSpeed + 'x';
});
$('#av-speed-up').addEventListener('click', () => {
config.playbackSpeed = Math.min(1.8, +(config.playbackSpeed + 0.25).toFixed(2));
saveConfig(config);
setSpeed();
$('#av-speed-val').textContent = config.playbackSpeed + 'x';
});
// 静音切换
$('#av-mute-btn').addEventListener('click', () => {
config.soundOff = !config.soundOff;
saveConfig(config);
const video = getVideo();
if (video) {
video.volume = config.soundOff ? 0 : 0.5;
const volumeBox = $('.volumeBox');
if (volumeBox) {
if (config.soundOff) volumeBox.classList.add('volumeNone');
else volumeBox.classList.remove('volumeNone');
}
}
const muteBtn = $('#av-mute-btn');
muteBtn.textContent = config.soundOff ? '已静音' : '有声音';
muteBtn.className = 'av-btn ' + (config.soundOff ? 'av-on' : 'av-off');
});
// 开关切换
function toggleBtn(key, btnId) {
$(btnId).addEventListener('click', () => {
config[key] = !config[key];
saveConfig(config);
const btn = $(btnId);
btn.textContent = config[key] ? '已开启' : '已关闭';
btn.className = 'av-btn ' + (config[key] ? 'av-on' : 'av-off');
});
}
toggleBtn('autoPlay', '#av-autoplay-btn');
toggleBtn('autoNext', '#av-autonext-btn');
toggleBtn('autoAnswer', '#av-autoanswer-btn');
toggleBtn('habitScore', '#av-habit-btn');
// 拖拽
makeDraggable(panel, $('#av-drag-handle'), { constrainToViewport: true });
const onResize = () => {
if (!panel.isConnected) {
window.removeEventListener('resize', onResize);
sizeObserver?.disconnect();
return;
}
keepPanelInViewport(panel);
};
const sizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(onResize) : null;
sizeObserver?.observe(panel);
window.addEventListener('resize', onResize);
}
function createStudyAiSettings(panel) {
if (!panel || panel.querySelector('#av-ai-settings')) return;
const body = panel.querySelector('#av-panel-body');
if (!body) return;
const settings = document.createElement('details');
settings.id = 'av-ai-settings';
settings.className = 'av-section av-ai-settings';
settings.innerHTML = `
助教答题设置
用于助教随堂题,独立于考试页的答题来源。更换来源或重新保存配置后可重试当前题目。
打开二维码原图
扫码按页面提示获取验证码,有效期以验证结果为准。到期后可重新验证。
`;
body.appendChild(settings);
const find = selector => settings.querySelector(selector);
const providerInput = find('#av-ai-provider');
const keyInput = find('#av-ai-key');
const urlInput = find('#av-ai-url');
const modelInput = find('#av-ai-model');
const message = find('#av-ai-msg');
const modeInput = find('#av-study-answer-mode');
const verifyInput = find('#av-study-verify-code');
const verifyButton = find('#av-study-verify-btn');
const verifyMessage = find('#av-study-verify-msg');
const paidInput = find('#av-study-paid-code');
const activateButton = find('#av-study-activate-btn');
const creditsButton = find('#av-study-credits-btn');
const paidMessage = find('#av-study-paid-msg');
let uiGeneration = 0;
let verifyPending = false;
let paidPending = false;
let verifyExpiryTimer = null;
const isCurrentView = (generation, mode) => settings.isConnected && generation === uiGeneration && modeInput.value === mode;
const showMessage = (element, text, error = false) => {
element.textContent = text;
element.style.color = error ? '#fca5a5' : '#86efac';
};
const refreshVerification = () => {
clearTimeout(verifyExpiryTimer);
if (!settings.isConnected || modeInput.value !== 'free') return;
const validUntil = Number(getVerifyState()?.validUntil || 0);
const valid = Boolean(isVerifyValid());
const status = find('#av-study-verify-status');
status.textContent = valid ? `已验证,有效期至 ${new Date(validUntil * 1000).toLocaleString()}` :
validUntil > 0 ? `验证码已于 ${new Date(validUntil * 1000).toLocaleString()} 到期,请重新验证。` : '尚未验证,请输入验证码。';
status.style.color = valid ? '#86efac' : '#cbd5e1';
verifyInput.disabled = verifyPending;
verifyButton.disabled = verifyPending;
if (valid) verifyExpiryTimer = setTimeout(refreshVerification, Math.min(Math.max(validUntil * 1000 - Date.now() + 50, 100), 2147483647));
};
const syncMode = () => {
['ai', 'free', 'paid'].forEach(mode => { find(`#av-study-area-${mode}`).hidden = modeInput.value !== mode; });
if (modeInput.value === 'free') {
const qr = find('#av-study-verify-qr');
if (!qr.getAttribute('src')) qr.src = 'https://qsy.iano.cn/yzm.png';
refreshVerification();
} else clearTimeout(verifyExpiryTimer);
if (modeInput.value === 'paid') find('#av-study-device-id').value = getDeviceId();
keepPanelInViewport(panel);
};
modeInput.value = getStudyAnswerMode();
modeInput.addEventListener('change', () => {
uiGeneration++;
setStudyAnswerMode(modeInput.value);
verifyMessage.textContent = '';
paidMessage.textContent = '';
syncMode();
});
settings.addEventListener('toggle', () => { if (settings.open) syncMode(); });
find('#av-study-verify-qr').addEventListener('error', event => { event.target.hidden = true; });
const configured = loadExamAiConfig();
Object.entries(AI_PROVIDERS).forEach(([value, info]) => {
const option = document.createElement('option');
option.value = value;
option.textContent = info.name;
providerInput.appendChild(option);
});
const customOption = document.createElement('option');
customOption.value = 'custom';
customOption.textContent = '自定义接口';
providerInput.appendChild(customOption);
providerInput.value = configured.provider;
keyInput.value = configured.apiKey;
urlInput.value = configured.customUrl;
modelInput.value = configured.customModel;
const fields = [keyInput, urlInput, modelInput];
const resetValidation = () => fields.forEach(field => field.removeAttribute('aria-invalid'));
const sync = () => {
const custom = providerInput.value === 'custom';
find('#av-ai-url-row').hidden = !custom;
find('#av-ai-model-row').hidden = !custom;
urlInput.required = custom;
modelInput.required = custom;
find('#av-ai-hint').textContent = custom ? '填写 OpenAI 兼容接口地址、模型名称和 API Key。' :
(AI_PROVIDERS[providerInput.value]?.desc || '');
resetValidation();
keepPanelInViewport(panel);
};
providerInput.addEventListener('change', sync);
settings.addEventListener('toggle', () => keepPanelInViewport(panel));
fields.forEach(field => field.addEventListener('input', () => field.removeAttribute('aria-invalid')));
sync();
syncMode();
message.textContent = configured.apiKey ? '已载入本脚本保存的 AI 配置。' : '尚未配置 API Key,请填写后保存。';
find('#av-ai-save').addEventListener('click', () => {
if (modeInput.value !== 'ai') return;
resetValidation();
const provider = providerInput.value;
const invalid = !keyInput.value.trim() ? { field: keyInput, text: '请填写 API Key。' } :
provider === 'custom' && !urlInput.value.trim() ? { field: urlInput, text: '请填写自定义接口地址。' } :
provider === 'custom' && !modelInput.value.trim() ? { field: modelInput, text: '请填写自定义模型名称。' } :
provider === 'custom' && !/^https?:\/\//i.test(urlInput.value.trim()) ? { field: urlInput, text: '接口地址必须以 http:// 或 https:// 开头。' } : null;
if (invalid) {
invalid.field.setAttribute('aria-invalid', 'true');
message.textContent = invalid.text;
message.style.color = '#fca5a5';
invalid.field.focus();
return;
}
const previous = loadExamAiConfig();
saveExamAiConfig({ provider, apiKey: keyInput.value, customUrl: urlInput.value,
customModel: modelInput.value, model: provider === previous.provider ? previous.model : '' });
// 新配置立即供本题重试使用,旧请求返回后不得继续选择或提交。
invalidateStudyExerciseAnswer();
message.textContent = 'AI 配置已保存,助教题将使用新配置。';
message.style.color = '#86efac';
});
verifyInput.addEventListener('input', () => verifyInput.removeAttribute('aria-invalid'));
paidInput.addEventListener('input', () => paidInput.removeAttribute('aria-invalid'));
verifyInput.addEventListener('keydown', event => { if (event.key === 'Enter') { event.preventDefault(); verifyButton.click(); } });
paidInput.addEventListener('keydown', event => { if (event.key === 'Enter') { event.preventDefault(); activateButton.click(); } });
verifyButton.addEventListener('click', async () => {
if (verifyPending || modeInput.value !== 'free') return;
const code = verifyInput.value.trim();
if (code.length !== 4) {
verifyInput.setAttribute('aria-invalid', 'true');
showMessage(verifyMessage, '请输入4位验证码。', true);
verifyInput.focus();
return;
}
const generation = uiGeneration;
verifyPending = true;
verifyInput.disabled = verifyButton.disabled = true;
verifyButton.textContent = '验证中…';
find('#av-study-area-free').setAttribute('aria-busy', 'true');
showMessage(verifyMessage, '正在验证,请稍候。');
try {
const result = await verifyCode(code);
if (!isCurrentView(generation, 'free')) return;
showMessage(verifyMessage, result?.msg || '验证失败,请重试。', !result?.ok);
if (result?.ok) {
verifyInput.value = '';
invalidateStudyExerciseAnswer();
} else verifyInput.setAttribute('aria-invalid', 'true');
} catch (_) {
if (isCurrentView(generation, 'free')) showMessage(verifyMessage, '验证请求失败,请重试。', true);
} finally {
verifyPending = false;
verifyInput.disabled = verifyButton.disabled = false;
verifyButton.textContent = '验证';
find('#av-study-area-free').removeAttribute('aria-busy');
refreshVerification();
}
});
const runPaidAction = async activate => {
if (paidPending || modeInput.value !== 'paid') return;
const code = paidInput.value.trim();
if (activate && !code) {
paidInput.setAttribute('aria-invalid', 'true');
showMessage(paidMessage, '请输入激活码。', true);
paidInput.focus();
return;
}
const generation = uiGeneration;
paidPending = true;
paidInput.disabled = activateButton.disabled = creditsButton.disabled = true;
find('#av-study-area-paid').setAttribute('aria-busy', 'true');
showMessage(paidMessage, activate ? '正在激活,请稍候。' : '正在刷新余额,请稍候。');
try {
if (activate) {
const result = await activateCode(code);
if (!isCurrentView(generation, 'paid')) return;
showMessage(paidMessage, result?.msg || (result?.ok ? '激活成功,正在刷新余额。' : '激活失败,请重试。'), !result?.ok);
if (!result?.ok) { paidInput.setAttribute('aria-invalid', 'true'); return; }
paidInput.value = '';
invalidateStudyExerciseAnswer();
}
const credits = await queryCredits();
if (!isCurrentView(generation, 'paid')) return;
find('#av-study-credits').textContent = Number.isFinite(Number(credits)) && Number(credits) >= 0 ? String(credits) : '--';
showMessage(paidMessage, activate ? '激活成功,余额已刷新。' : '余额已刷新。');
} catch (_) {
if (isCurrentView(generation, 'paid')) showMessage(paidMessage, '请求失败,请重试。', true);
} finally {
paidPending = false;
paidInput.disabled = activateButton.disabled = creditsButton.disabled = false;
find('#av-study-area-paid').removeAttribute('aria-busy');
}
};
activateButton.addEventListener('click', () => runPaidAction(true));
creditsButton.addEventListener('click', () => runPaidAction(false));
}
function keepPanelInViewport(el) {
const rect = el.getBoundingClientRect();
const width = document.documentElement.clientWidth || window.innerWidth;
const height = document.documentElement.clientHeight || window.innerHeight;
const left = Math.max(12, Math.min(rect.left, width - rect.width - 12));
const top = Math.max(12, Math.min(rect.top, height - rect.height - 12));
if (Math.abs(left - rect.left) > 0.5 || Math.abs(top - rect.top) > 0.5) {
el.style.left = left + 'px';
el.style.top = top + 'px';
el.style.right = 'auto';
}
}
function makeDraggable(el, handle, { constrainToViewport = false } = {}) {
let offsetX = 0, offsetY = 0, isDragging = false;
handle.addEventListener('mousedown', (e) => {
if (e.button !== 0 || e.target.closest('button, a, input, select, textarea')) return;
isDragging = true;
offsetX = e.clientX - el.getBoundingClientRect().left;
offsetY = e.clientY - el.getBoundingClientRect().top;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
el.style.left = (e.clientX - offsetX) + 'px';
el.style.top = (e.clientY - offsetY) + 'px';
el.style.right = 'auto';
if (constrainToViewport) keepPanelInViewport(el);
});
document.addEventListener('mouseup', () => { isDragging = false; });
}
function updatePanelStatus(text) {
if (panelStatusEl) panelStatusEl.textContent = text;
}
function updatePanelProgress(text) {
if (panelProgressEl) panelProgressEl.textContent = text;
}
// ===================== 考试答题模块 =====================
// 普通课程仍保留原有手动搜题入口;AI 课程使用下方 OpenAI 兼容接口
// 直接作答,不接入第三方题库,也不上传用户身份信息。
const AI_PROVIDERS = {
zhipu: {
name: '智谱 GLM-4-Flash', model: 'glm-4-flash',
url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
desc: '智谱 AI,中文题目适配较好'
},
doubao: {
name: '豆包 Doubao-lite', model: 'doubao-lite-32k',
url: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
desc: '火山引擎豆包模型(模型名可按控制台配置修改)'
},
deepseek: {
name: 'DeepSeek Flash(V4.1)', model: 'deepseek-flash',
url: 'https://api.deepseek.com/chat/completions', thinking: 'disabled',
desc: 'DeepSeek 官方 Flash 模型'
},
deepseek_v4: {
name: 'DeepSeek V4 Pro', model: 'deepseek-v4-pro',
url: 'https://api.deepseek.com/chat/completions', thinking: 'disabled',
desc: 'DeepSeek 官方 Pro 模型'
}
};
function readSharedAiConfig() {
let raw = null;
try { raw = GM_getValue('cx_answer_config', null); } catch (_) {}
if (typeof raw === 'string') {
try { raw = JSON.parse(raw); } catch (_) { raw = null; }
}
const shared = raw && typeof raw === 'object' ? raw : {};
// 雨课堂使用 localStorage 保存同一组 AI 配置;跨脚本复用时只读取
// 接口、模型和 Key,不读取任何题库/身份数据。
try {
const provider = localStorage.getItem('ykt_ai_provider');
const apiKey = localStorage.getItem('ykt_ai_api_key');
const customUrl = localStorage.getItem('ykt_ai_custom_url');
const customModel = localStorage.getItem('ykt_ai_custom_model');
if (provider || apiKey || customUrl || customModel) {
return Object.assign(shared, {
aiProvider: provider || shared.aiProvider || '',
aiApiKey: apiKey || shared.aiApiKey || '',
aiCustomUrl: customUrl || shared.aiCustomUrl || '',
aiCustomModel: customModel || shared.aiCustomModel || ''
});
}
} catch (_) {}
return shared;
}
function loadExamAiConfig() {
const shared = readSharedAiConfig();
let own = {};
try { own = GM_getValue('av_ai_config', {}) || {}; } catch (_) {}
if (typeof own === 'string') {
try { own = JSON.parse(own); } catch (_) { own = {}; }
}
const rawProvider = String(own.provider || shared.aiProvider || (isAiExamHost() ? 'deepseek' : 'zhipu'));
const provider = (AI_PROVIDERS[rawProvider] || rawProvider === 'custom') ? rawProvider : 'deepseek';
return {
provider,
apiKey: String(own.apiKey || shared.aiApiKey || '').trim(),
customUrl: String(own.customUrl || shared.aiCustomUrl || '').trim(),
customModel: String(own.customModel || shared.aiCustomModel || '').trim(),
model: String(own.model || '').trim()
};
}
function saveExamAiConfig(config) {
const clean = {
provider: String(config.provider || 'deepseek'),
apiKey: String(config.apiKey || '').trim(),
customUrl: String(config.customUrl || '').trim(),
customModel: String(config.customModel || '').trim(),
model: String(config.model || '').trim()
};
try { GM_setValue('av_ai_config', clean); } catch (_) {}
// 与学习通/雨课堂共用 API Key 和自定义接口配置,用户只需填写一次。
const shared = readSharedAiConfig();
shared.aiProvider = clean.provider;
shared.aiApiKey = clean.apiKey;
shared.aiCustomUrl = clean.customUrl;
shared.aiCustomModel = clean.customModel;
try { GM_setValue('cx_answer_config', JSON.stringify(shared)); } catch (_) {}
try {
localStorage.setItem('ykt_ai_provider', clean.provider);
localStorage.setItem('ykt_ai_api_key', clean.apiKey);
localStorage.setItem('ykt_ai_custom_url', clean.customUrl);
localStorage.setItem('ykt_ai_custom_model', clean.customModel);
} catch (_) {}
return clean;
}
function getAiEndpoint(ai) {
if (ai.provider !== 'custom') return AI_PROVIDERS[ai.provider]?.url || AI_PROVIDERS.deepseek.url;
let raw = String(ai.customUrl || '').trim();
if (!raw) return '';
// 允许用户直接填写完整端点,并兼容末尾斜杠或查询参数。
if (/\/chat\/completions\/?(?:\?|$)/i.test(raw)) return raw.replace(/\/$/, '');
raw = raw.replace(/\/+$/, '');
return /\/v1$/i.test(raw) ? `${raw}/chat/completions` : `${raw}/v1/chat/completions`;
}
function getAiModel(ai) {
if (ai.provider === 'custom') return String(ai.customModel || '').trim();
return String(ai.model || AI_PROVIDERS[ai.provider]?.model || '').trim();
}
function parseAiResponseText(json) {
const choice = json && Array.isArray(json.choices) ? json.choices[0] : null;
const finishReason = String(choice?.finish_reason || '').toLowerCase();
if (['length', 'content_filter', 'tool_calls', 'aborted'].includes(finishReason)) {
return '';
}
let content = choice && choice.message ? choice.message.content : (choice && choice.text);
if (Array.isArray(content)) content = content.map(x => typeof x === 'string' ? x : (x && x.text || '')).join('');
return String(content || '').trim();
}
function parseAiAnswers(content, q) {
let text = String(content || '').trim();
if (!text) return [];
const fenced = text.match(/```(?:json|text)?\s*([\s\S]*?)```/i);
if (fenced) text = fenced[1].trim();
try {
const obj = JSON.parse(text);
if (obj && typeof obj === 'object') text = String(obj.answer ?? obj.answers ?? obj.result ?? obj.output ?? '').trim();
} catch (_) {}
text = text.replace(/^\s*(?:正确答案|最终答案|答案|答复|答)\s*[::]\s*/i, '').trim();
if (!text) return [];
// 选择题优先解析 A、B、C 等标签,兼容 “A,C”/“A||C”/“选 A 和 C”。
if (q && (q.type === 0 || q.type === 1 || q.type === 3)) {
const labels = text.match(/\b[A-H](?:\s*(?:[,,、|/&和与及]\s*)[A-H]){0,7}\b/i);
if (labels && labels[0].replace(/[^A-H]/ig, '').length >= 1 &&
(/[答案选项选择]/i.test(content) || /^[A-H](?:\s*[,,、|/&和与及]\s*[A-H])*\s*$/i.test(text))) {
const letters = labels[0].match(/[A-H]/ig) || [];
const unique = [...new Set(letters.map(x => x.toUpperCase()))];
return q.type === 0 || q.type === 3 ? unique.slice(0, 1) : unique;
}
}
// 主观题/填空答案可能自然包含逗号,只有显式 || 才表示多个空。
if (q && q.type >= 2) {
return text.split(/\s*\|\|\s*/).map(x => x.trim()).filter(Boolean);
}
const pieces = text.split(/\s*(?:\|\||[,,、;;]|\s+和\s+|\s+与\s+)\s*/).map(x => x.trim()).filter(Boolean);
return pieces.length ? pieces : [text];
}
async function askAiAPI(question, options, type) {
const ai = loadExamAiConfig();
const endpoint = getAiEndpoint(ai);
const model = getAiModel(ai);
if (!endpoint || !ai.apiKey || !model) {
log.warn('[考试] AI 配置不完整,请在面板填写接口和 API Key');
return null;
}
const typeName = TYPE_NAMES[type] || '未知题型';
const optionLines = (options || []).map((o, i) => `${String.fromCharCode(65 + i)}. ${o}`).join('\n');
const prompt = [
'你是学习辅助答题助手。根据题干和选项给出最可能正确的答案。',
'只输出 JSON,不要解释:{"answer":"A"};多选用逗号分隔(如 A,C);填空/简答直接写答案文本。',
`题型:${typeName}`, `题干:${formatString(question)}`,
optionLines ? `选项:\n${optionLines}` : '该题没有显式选项。'
].join('\n');
const payload = {
model, temperature: 0.1, max_tokens: type >= 4 ? 800 : 256, stream: false,
messages: [{ role: 'system', content: '只返回可解析的 JSON,不要输出 Markdown 或解释。' }, { role: 'user', content: prompt }]
};
if (AI_PROVIDERS[ai.provider]?.thinking) payload.thinking = { type: AI_PROVIDERS[ai.provider].thinking };
const res = await gmRequest({
method: 'POST', url: endpoint,
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${ai.apiKey}` },
data: JSON.stringify(payload), timeout: 60000
});
if (!res || res.status < 200 || res.status >= 300) {
log.warn(`[考试] AI 请求失败${res ? ` (HTTP ${res.status})` : ''}`);
return null;
}
try {
const json = JSON.parse(res.responseText || '{}');
const content = parseAiResponseText(json);
const answers = parseAiAnswers(content, { type });
return answers.length ? { answers, remaining: -1, source: `AI·${AI_PROVIDERS[ai.provider]?.name || '自定义接口'}` } : null;
} catch (e) {
log.warn('[考试] AI 响应解析失败: ' + e.message);
return null;
}
}
// --- 设备ID ---
function getDeviceId() {
let id = GM_getValue('av_device_id', '');
if (!id) {
id = 'av_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
GM_setValue('av_device_id', id);
}
return id;
}
// --- 文本格式化 (借鉴万能脚本 formatString) ---
function formatString(src) {
src = String(src);
// HTML 实体解码
if (!src.includes('img') && !src.includes('iframe')) {
const tmp = document.createElement('div');
tmp.innerHTML = src;
src = tmp.textContent || tmp.innerText || '';
}
// 全角转半角
src = src.replace(/[\uff01-\uff5e]/g, s => String.fromCharCode(s.charCodeAt(0) - 65248));
return src.replace(/\s+/g, ' ').replace(/[""]/g, '"').replace(/['']/g, "'")
.replace(/。/g, '.').replace(/[,.?:!;]$/, '').trim();
}
// 判断题匹配 (借鉴万能脚本)
function isTrue(str) {
return Boolean(String(str).match(/(^|,)(正确|是|对|√|T|ri|true|A)(,|$)/));
}
function isFalse(str) {
return Boolean(String(str).match(/(^|,)(错误|否|错|×|F|不是|wr|false|B)(,|$)/));
}
function isPlainAnswer(answer) {
if (!answer || answer.length > 8 || !/[A-Z]/.test(answer)) return false;
let min = 0;
for (let i = 0; i < answer.length; i++) {
if (answer.charCodeAt(i) < min) return false;
min = answer.charCodeAt(i);
}
return true;
}
// 答案文本相似度 (简化版)
function answerSimilar(answer, options) {
return options.map(opt => {
const o = opt.replace(/\s/g, '');
const a = answer.replace(/\s/g, '');
if (o === a) return 100;
if (o.includes(a) || a.includes(o)) return 85;
let match = 0;
const shorter = a.length < o.length ? a : o;
const longer = a.length < o.length ? o : a;
for (let i = 0; i < shorter.length; i++) {
if (longer.includes(shorter[i])) match++;
}
return Math.round(match / longer.length * 100);
});
}
// --- 题型检测 (万能脚本 getQuestionType) ---
function getQuestionType(typeText) {
const t = String(typeText).trim();
if (/单选|单项选择|radio/i.test(t)) return 0;
if (/多选|多项选择|checkbox|不定项/i.test(t)) return 1;
if (/填空/i.test(t)) return 2;
if (/判断|是非|对错/i.test(t)) return 3;
if (/简答|问答|主观/i.test(t)) return 4;
if (/名词解释/i.test(t)) return 5;
if (/论述/i.test(t)) return 6;
if (/计算/i.test(t)) return 7;
if (/阅读理解|完型填空|听力/i.test(t)) return 8;
return 4; // 默认
}
const TYPE_NAMES = { 0: '单选', 1: '多选', 2: '填空', 3: '判断', 4: '简答', 5: '名词解释', 6: '论述', 7: '计算', 8: '其他' };
// --- 题目提取 (借鉴万能脚本的智慧树选择器) ---
function extractExamQuestions() {
const questions = [];
// 按优先级尝试不同页面版本的选择器
const layouts = [
{ // 智慧树 AI 课程习题/考试(questionContent)
root: '.questionContent',
question: '.questionTitle .centent-pre, .questionTitle, .centent-pre',
options: '.el-radio-group > label.el-radio .preStyle, .el-checkbox-group > label.el-checkbox .preStyle, ul.radio-view > li .preStyle, ul.checkbox-view > li .preStyle, .option-list li .preStyle',
$options: '.el-radio-group > label.el-radio, .el-checkbox-group > label.el-checkbox, ul.radio-view > li, ul.checkbox-view > li, .option-list li',
type: '.questionTitle, .letterSortNum',
checked: '.is-checked, .is-checked input:checked, input:checked',
},
{ // 智慧树作业/考试 (examPaper)
root: '.examPaper_subject',
question: '.subject_describe div, .smallStem_describe p',
options: '.subject_node .nodeLab .node_detail',
$options: '.subject_node .nodeLab .node_detail',
type: '.subject_type span:first-child',
checked: '.onChecked',
},
{ // 智慧树学分课作业
root: '.questionBox',
question: '.questionContent',
options: '.optionUl label .el-radio__label, .optionUl label .el-checkbox__label',
$options: '.optionUl label',
type: '.questionTit',
checked: '.is-checked',
},
{ // 智慧树学分课考试
root: '.ques-detail',
question: '.questionName .centent-pre',
options: '.radio-view li .preStyle, .checkbox-views label .preStyle',
$options: '.radio-view li, .checkbox-views label',
type: '.letterSortNum',
checked: '.is-checked',
},
{ // 通用回退
root: 'div[class*="subject"], div[class*="question"], div[class*="exam"]',
question: '.subject_describe, .question-title, .ques-title, .topic-title, .stem, h3',
options: '.subject_node, .option-item, .topic-item, label',
$options: '.subject_node, .option-item, .topic-item, label',
type: '.subject_type, .questionTit, .letterSortNum',
checked: '.onChecked, .is-checked',
}
];
let activeLayout = null;
let containers = [];
for (const layout of layouts) {
containers = $$(layout.root);
if (containers.length > 0) { activeLayout = layout; break; }
}
if (!activeLayout || containers.length === 0) {
log.warn('[考试] 未检测到题目容器');
return [];
}
log.info(`[考试] 使用选择器: ${activeLayout.root}, 检测到 ${containers.length} 题`);
// 跳过题型/分值标题的正则
const SKIP_HEADER = /^[\d\s\.\))、]*[【\[((]*[单多判填简论计][选断空答述算]|^\d+[\s\.、)\)]*\(?\d+分\)?$|^第?\d+题|^【.*题】/;
containers.forEach((container, index) => {
// 题目文本 — 跳过题型/分值标题,找到真正的问题
let questionText = '';
const qEls = container.querySelectorAll(activeLayout.question);
for (const qEl of qEls) {
const t = qEl.textContent.trim();
// 跳过:太短、题型标记如"1.【单选题】(2分)"、纯选项字母
if (t.length <= 5) continue;
if (SKIP_HEADER.test(t) && t.length < 30) continue;
if (/^[A-G][\..\s、]/.test(t)) continue;
questionText = formatString(t);
break;
}
// 备用:直接在container里找长文本段落
if (!questionText) {
const ps = container.querySelectorAll('p, span, div');
for (const p of ps) {
const t = p.textContent.trim();
if (t.length <= 8) continue;
if (SKIP_HEADER.test(t) && t.length < 30) continue;
if (/^[A-G][\..\s、]/.test(t)) continue;
questionText = formatString(t);
break;
}
}
// 清理残留的题号和分值
if (questionText) {
questionText = questionText
.replace(/^[\d\s\.\))、]+/, '')
.replace(/\(\s*\d+分\s*\)/g, '')
.replace(/^[【\[((]*[单多判填简论计][选断空答述算][题】\]))]*\s*/g, '')
.trim();
}
if (!questionText || questionText.length < 4) return;
// 题型
const typeEl = container.querySelector(activeLayout.type);
let typeCode = typeEl ? getQuestionType(typeEl.textContent) : (activeLayout.root === '.questionContent' ?
(container.querySelector('.el-checkbox-group, ul.checkbox-view') ? 1 : 0) : 4);
// 选项文本 & 可点击元素
const optTexts = [];
const $optEls = [];
const optTextEls = container.querySelectorAll(activeLayout.options);
const optClickEls = container.querySelectorAll(activeLayout.$options);
const useClickEls = optClickEls.length >= optTextEls.length ? optClickEls : optTextEls;
if (activeLayout.root === '.questionContent' && typeCode >= 4 && useClickEls.length) {
typeCode = container.querySelector('.el-checkbox-group, ul.checkbox-view') ? 1 : 0;
}
for (let i = 0; i < useClickEls.length; i++) {
const textSrc = optTextEls[i] || useClickEls[i];
const raw = formatString(textSrc.textContent).replace(/^[A-Za-z][.\s、.]+/, '').trim();
optTexts.push(raw);
$optEls.push(useClickEls[i]);
}
// 已选检测
const isChecked = (el) => {
if (!activeLayout.checked) return false;
return activeLayout.checked.split(',').some(sel => el.matches(sel.trim()) || el.querySelector(sel.trim()));
};
questions.push({
index, el: container, text: questionText,
type: typeCode, options: optTexts,
optionEls: $optEls, isChecked,
});
});
return questions;
}
// --- API调用 ---
function gmRequest(opts) {
return new Promise(resolve => {
const timer = setTimeout(() => resolve(null), opts.timeout || 12000);
try {
GM_xmlhttpRequest({
...opts,
onload: (res) => { clearTimeout(timer); resolve(res); },
onerror: () => { clearTimeout(timer); resolve(null); },
ontimeout: () => { clearTimeout(timer); resolve(null); },
});
} catch { clearTimeout(timer); resolve(null); }
});
}
// 通用答案数组解析
function parseAnswersFromResult(resultObj, options) {
if (!resultObj) return [];
if (resultObj.success === false) return [];
const raw = resultObj.answers || resultObj.answer || [];
let answers = [];
if (typeof raw === 'string' && raw.trim()) {
// 单个字符串答案
answers = [raw.trim()];
} else if (Array.isArray(raw) && raw.length > 0) {
if (Array.isArray(raw[0])) {
// 嵌套数组 [[答案1, 答案2]]
answers = raw[0].map(a => String(a));
} else if (typeof raw[0] === 'number') {
// 索引数组 [0, 2] => 对应选项
answers = raw.map(idx => options[idx] || String.fromCharCode(65 + idx));
} else {
answers = raw.map(a => String(a));
}
}
return answers.filter(a => a && a.trim());
}
// 你的中转API (qsy.iano.cn)
async function askPaidAPI(question, options, type) {
const deviceId = getDeviceId();
log.info(`[考试] 发送题目: "${question.slice(0, 40)}..." 选项:${options.length}个`);
const res = await gmRequest({
method: 'POST',
url: 'https://qsy.iano.cn/index.php?s=/api/question_bank/answer',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({
device_id: deviceId,
question: question,
options: options,
type: type,
location: '智慧树'
}),
timeout: 12000,
});
if (!res) { log.warn('[考试] 付费API无响应'); return null; }
if (res.status !== 200) { log.warn(`[考试] 付费API HTTP ${res.status}`); return null; }
try {
const json = JSON.parse(res.responseText);
log.info(`[考试] 付费API响应: code=${json.code}, msg=${json.msg || ''}`);
if (json.code === 1 && json.data) {
const result = json.data.result || json.data;
const remaining = json.data.remaining_credits || 0;
const answers = parseAnswersFromResult(result, options);
if (answers.length > 0) {
return { answers, remaining, source: '付费题库' };
}
log.info('[考试] 付费API返回结果但无有效答案');
}
if (json.code === 0) {
log.warn(`[考试] 付费API: ${json.msg || '查询失败'}`);
}
} catch (e) { log.warn('[考试] 付费API解析错误: ' + e.message); }
return null;
}
// 免费题库 (lyck6.cn, 与万能脚本同源)
async function askFreeAPI(question, options, type) {
const res = await gmRequest({
method: 'POST',
url: 'http://lyck6.cn/scriptService/api/autoFreeAnswer',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({
question: question,
options: options,
type: type,
location: location.href
}),
timeout: 15000,
});
if (!res) { log.warn('[考试] 免费API无响应'); return null; }
if (res.status !== 200) { log.warn(`[考试] 免费API HTTP ${res.status}`); return null; }
try {
const json = JSON.parse(res.responseText);
const result = json.result || json.data || json;
const answers = parseAnswersFromResult(result, options);
if (answers.length > 0) {
return { answers, remaining: -1, source: '免费题库' };
}
} catch (e) { log.warn('[考试] 免费API解析错误: ' + e.message); }
return null;
}
// --- 当前题库模式: 'free' 或 'paid' ---
let examMode = isAiExamHost() ? 'ai' : GM_getValue('av_exam_mode', 'free');
if (!['free', 'paid', 'ai'].includes(examMode)) examMode = isAiExamHost() ? 'ai' : 'free';
function setExamMode(mode) {
examMode = mode;
GM_setValue('av_exam_mode', mode);
}
// --- 免费验证码相关 ---
function getVerifyState() {
return GM_getValue('av_verify_state', { validUntil: 0 });
}
function setVerifyState(state) {
GM_setValue('av_verify_state', state);
}
function isVerifyValid() {
const state = getVerifyState();
return state.validUntil && state.validUntil > Date.now() / 1000;
}
async function verifyCode(code) {
const res = await gmRequest({
method: 'POST',
url: 'https://qsy.iano.cn/index.php?s=/api/code/verify',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: `code=${encodeURIComponent(code)}`,
timeout: 10000,
});
if (!res || res.status !== 200) return { ok: false, msg: '网络错误' };
try {
const json = JSON.parse(res.responseText);
if (json.code === 1 && json.data) {
const validUntil = json.data.valid_until || (Date.now() / 1000 + 86400);
const validStr = json.data.valid_until_str || new Date(validUntil * 1000).toLocaleString();
setVerifyState({ validUntil });
return { ok: true, msg: `验证成功,有效期至 ${validStr}` };
}
return { ok: false, msg: json.msg || '验证码无效或已过期' };
} catch { return { ok: false, msg: '解析错误' }; }
}
// 综合搜索:考试沿用考试模式,助教显式传入自己的来源,避免串用/意外扣费。
async function searchAnswer(question, options, type, mode = examMode) {
if (!['ai', 'free', 'paid'].includes(mode)) return null;
const cleanQ = formatString(question);
if (!cleanQ || cleanQ.length < 4) return null;
const cleanOpts = (options || []).map(o => formatString(o));
if (mode === 'ai') {
return askAiAPI(cleanQ, cleanOpts, type);
}
if (mode === 'paid') {
// 付费模式
log.info('[考试] 正在查询付费题库...');
const paidResult = await askPaidAPI(cleanQ, cleanOpts, type);
if (paidResult && paidResult.answers.length > 0) {
log.success(`[考试] 付费题库找到答案 (余${paidResult.remaining}积分)`);
updateCreditsDisplay(paidResult.remaining);
return paidResult;
}
} else {
// 免费模式
if (!isVerifyValid()) {
log.warn('[考试] 免费验证码未验证或已过期,请先验证');
return null;
}
log.info('[考试] 正在查询免费题库...');
const freeResult = await askFreeAPI(cleanQ, cleanOpts, type);
if (freeResult && freeResult.answers.length > 0) {
log.success('[考试] 免费题库找到答案');
return freeResult;
}
}
return null;
}
// --- 智能答案匹配 & 填入 (借鉴万能脚本) ---
function matchAndFill(q, result) {
if (!result || !result.answers || result.answers.length === 0) return false;
const answers = result.answers;
let filled = false;
if (q.type === 3) {
// 判断题
const ansJoined = answers.join(',');
for (let i = 0; i < q.optionEls.length; i++) {
const shouldClick =
(isTrue(ansJoined) && isFalse(q.options[i])) ? false :
(isTrue(ansJoined) && !isFalse(q.options[i])) ? true :
(isFalse(ansJoined) && isTrue(q.options[i])) ? false :
(isFalse(ansJoined) && !isTrue(q.options[i])) ? true : false;
// 更简单的逻辑: 如果答案为"对",点包含"对/正确/√"的选项
const optText = q.options[i] || '';
let match = false;
if (isTrue(ansJoined)) match = isTrue(optText) || /对|正确|√|是|true/i.test(optText);
else if (isFalse(ansJoined)) match = isFalse(optText) || /错|错误|×|否|false/i.test(optText);
if (match && !q.isChecked(q.optionEls[i])) {
q.optionEls[i].click();
filled = true;
}
}
} else if (q.type === 2 || q.type === 4 || q.type === 5 || q.type === 6 || q.type === 7) {
// 填空题/简答/论述等: 填入 textarea/input
const inputs = q.el.querySelectorAll('input[type="text"], textarea');
const editorNames = q.el.querySelectorAll('textarea[name]');
if (editorNames.length > 0) {
// 尝试 UEditor
editorNames.forEach((ta, i) => {
const val = answers[i] || answers[0] || '';
try {
if (typeof UE !== 'undefined' && UE.getEditor) {
UE.getEditor(ta.getAttribute('name')).setContent(val);
filled = true;
}
} catch { }
});
}
if (!filled && inputs.length > 0) {
inputs.forEach((inp, i) => {
const val = answers[i] || answers[0] || '';
inp.value = val.trim();
inp.dispatchEvent(new Event('input', { bubbles: true }));
inp.dispatchEvent(new Event('change', { bubbles: true }));
});
filled = inputs.length > 0;
}
} else {
// 单选(0)/多选(1)/其他选择题
const beautifulOptions = q.options.map(o => formatString(o).toLowerCase().replace(/\s/g, ''));
let targetIndices = new Set();
for (const ans of answers) {
// 1. 字母匹配 (A/B/C...)
if (ans.length === 1 && isPlainAnswer(ans.toUpperCase())) {
targetIndices.add(ans.toUpperCase().charCodeAt(0) - 65);
continue;
}
// 2. 精确文本匹配
const val = formatString(ans).toLowerCase().replace(/\s/g, '');
const exactIdx = beautifulOptions.indexOf(val);
if (exactIdx >= 0) {
targetIndices.add(exactIdx);
continue;
}
// 3. 模糊匹配
if (val.length >= 5) {
const ratings = answerSimilar(val, beautifulOptions);
const maxScore = Math.max(...ratings);
if (maxScore > 65) {
targetIndices.add(ratings.indexOf(maxScore));
}
}
}
for (const idx of targetIndices) {
if (idx >= 0 && idx < q.optionEls.length && !q.isChecked(q.optionEls[idx])) {
q.optionEls[idx].click();
filled = true;
}
}
}
return filled;
}
// --- 积分管理 ---
function updateCreditsDisplay(credits) {
document.querySelectorAll('#ae-credits, #av-study-credits').forEach(el => {
el.textContent = credits >= 0 ? credits : '--';
});
}
async function queryCredits() {
const res = await gmRequest({
method: 'GET',
url: `https://qsy.iano.cn/index.php?s=/api/question_bank/credits&device_id=${encodeURIComponent(getDeviceId())}`,
timeout: 8000,
});
if (!res || res.status !== 200) return 0;
try {
const json = JSON.parse(res.responseText);
if (json.code === 1 && json.data) return json.data.remaining_credits || 0;
} catch { }
return 0;
}
async function activateCode(code) {
const res = await gmRequest({
method: 'POST',
url: 'https://qsy.iano.cn/index.php?s=/api/question_bank/activate',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ code, device_id: getDeviceId() }),
timeout: 10000,
});
if (!res || res.status !== 200) return { ok: false, msg: '网络错误' };
try {
const json = JSON.parse(res.responseText);
return { ok: json.code === 1, msg: json.msg || '', data: json.data };
} catch { return { ok: false, msg: '解析错误' }; }
}
// --- 考试面板 UI ---
function createExamPanel() {
if ($('#av-exam-panel')) return;
const panel = document.createElement('div');
panel.id = 'av-exam-panel';
panel.innerHTML = `
📝 考试助手
▼
📱 扫码观看广告获取验证码
验证后免费使用24小时
剩余积分
--
点击查看测试激活页面
每次答题消耗1积分 · 联系微信: C919irt
⚠️ 仅供学习交流,请勿用于违规行为
使用者自行承担一切后果
`;
document.body.appendChild(panel);
// 领取付费题库测试弹窗(独立于面板,避免被面板滚动区域遮挡)
const trialModal = document.createElement('div');
trialModal.id = 'av-trial-modal';
trialModal.hidden = true;
trialModal.innerHTML = `
🎁 领取付费题库测试
按下方页面提示领取测试激活码,领取后回到付费题库区域输入激活码并点击“激活”。
`;
document.body.appendChild(trialModal);
let trialPreviousFocus = null;
const closeTrialModal = () => {
trialModal.hidden = true;
if (trialPreviousFocus && typeof trialPreviousFocus.focus === 'function') trialPreviousFocus.focus();
};
const openTrialModal = () => {
trialPreviousFocus = document.activeElement;
trialModal.hidden = false;
const closeButton = trialModal.querySelector('.ae-trial-close');
if (closeButton) closeButton.focus();
};
$('#ae-trial-btn').addEventListener('click', openTrialModal);
trialModal.querySelector('.ae-trial-close').addEventListener('click', closeTrialModal);
trialModal.querySelector('.ae-trial-done').addEventListener('click', closeTrialModal);
trialModal.addEventListener('click', (event) => {
if (event.target === trialModal) closeTrialModal();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !trialModal.hidden) closeTrialModal();
});
// 收起/展开
$('#ae-collapse').addEventListener('click', () => {
panel.classList.toggle('collapsed');
$('#ae-collapse').textContent = panel.classList.contains('collapsed') ? '▶' : '▼';
});
makeDraggable(panel, $('#ae-drag-handle'));
// --- 模式切换 ---
function switchModeUI(mode) {
const freeTab = $('#ae-mode-free');
const paidTab = $('#ae-mode-paid');
const aiTab = $('#ae-mode-ai');
const freeArea = $('#ae-area-free');
const paidArea = $('#ae-area-paid');
const aiArea = $('#ae-area-ai');
[freeTab, paidTab, aiTab].forEach(tab => {
if (tab) { tab.style.background = 'transparent'; tab.style.color = '#94a3b8'; }
});
[freeArea, paidArea, aiArea].forEach(area => { if (area) area.style.display = 'none'; });
if (mode === 'ai') {
aiTab.style.background = 'rgba(167,139,250,0.16)'; aiTab.style.color = '#c4b5fd';
aiArea.style.display = '';
setExamMode('ai');
} else if (mode === 'paid') {
freeTab.style.background = 'transparent'; freeTab.style.color = '#94a3b8';
paidTab.style.background = 'rgba(96,165,250,0.15)'; paidTab.style.color = '#60a5fa';
freeArea.style.display = 'none';
paidArea.style.display = '';
setExamMode('paid');
} else {
paidTab.style.background = 'transparent'; paidTab.style.color = '#94a3b8';
freeTab.style.background = 'rgba(52,211,153,0.15)'; freeTab.style.color = '#34d399';
paidArea.style.display = 'none';
freeArea.style.display = '';
setExamMode('free');
}
}
$('#ae-mode-free').addEventListener('click', () => switchModeUI('free'));
$('#ae-mode-paid').addEventListener('click', () => switchModeUI('paid'));
$('#ae-mode-ai').addEventListener('click', () => switchModeUI('ai'));
// 恢复上次模式
switchModeUI(examMode);
// --- AI 配置 ---
const aiCfg = loadExamAiConfig();
const aiProviderEl = $('#ae-ai-provider');
const aiKeyEl = $('#ae-ai-key');
const aiUrlEl = $('#ae-ai-url');
const aiModelEl = $('#ae-ai-model');
const aiCustomRow = $('#ae-ai-custom-row');
const aiModelRow = $('#ae-ai-model-row');
const aiHint = $('#ae-ai-hint');
const syncAiConfigUI = () => {
const provider = aiProviderEl.value;
const custom = provider === 'custom';
aiCustomRow.style.display = custom ? '' : 'none';
aiModelRow.style.display = custom ? '' : 'none';
const info = custom ? { desc: 'OpenAI 兼容接口,填写 Base URL、模型名称和 API Key。' } : AI_PROVIDERS[provider];
aiHint.textContent = info ? info.desc : '';
};
aiProviderEl.value = ['deepseek', 'deepseek_v4', 'doubao', 'zhipu', 'custom'].includes(aiCfg.provider) ? aiCfg.provider : 'deepseek';
aiKeyEl.value = aiCfg.apiKey;
aiUrlEl.value = aiCfg.customUrl;
aiModelEl.value = aiCfg.customModel;
aiProviderEl.addEventListener('change', syncAiConfigUI);
syncAiConfigUI();
$('#ae-ai-save').addEventListener('click', () => {
const provider = aiProviderEl.value;
if (!aiKeyEl.value.trim()) {
$('#ae-ai-msg').textContent = '请填写 API Key';
$('#ae-ai-msg').style.color = '#f87171';
return;
}
if (provider === 'custom' && (!aiUrlEl.value.trim() || !aiModelEl.value.trim())) {
$('#ae-ai-msg').textContent = '自定义接口需要填写地址和模型名称';
$('#ae-ai-msg').style.color = '#f87171';
return;
}
saveExamAiConfig({ provider, apiKey: aiKeyEl.value, customUrl: aiUrlEl.value, customModel: aiModelEl.value });
$('#ae-ai-msg').textContent = '✅ AI 配置已保存';
$('#ae-ai-msg').style.color = '#34d399';
});
// --- 免费验证码 ---
// 恢复验证状态
if (isVerifyValid()) {
const state = getVerifyState();
const expStr = new Date(state.validUntil * 1000).toLocaleString();
$('#ae-verify-msg').textContent = `✅ 已验证,有效期至 ${expStr}`;
$('#ae-verify-msg').style.color = '#34d399';
$('#ae-verify-code').disabled = true;
$('#ae-verify-btn').disabled = true;
}
$('#ae-verify-btn').addEventListener('click', async () => {
const code = ($('#ae-verify-code').value || '').trim();
if (!code || code.length !== 4) {
$('#ae-verify-msg').textContent = '请输入4位验证码';
$('#ae-verify-msg').style.color = '#f87171';
return;
}
$('#ae-verify-btn').disabled = true;
$('#ae-verify-btn').textContent = '验证中...';
const result = await verifyCode(code);
$('#ae-verify-msg').textContent = result.ok ? `✅ ${result.msg}` : `❌ ${result.msg}`;
$('#ae-verify-msg').style.color = result.ok ? '#34d399' : '#f87171';
if (result.ok) {
$('#ae-verify-code').disabled = true;
} else {
$('#ae-verify-btn').disabled = false;
}
$('#ae-verify-btn').textContent = '验证';
});
// 回车验证
$('#ae-verify-code').addEventListener('keypress', (e) => {
if (e.key === 'Enter') $('#ae-verify-btn').click();
});
// --- 付费激活码 ---
$('#ae-activate-btn').addEventListener('click', async () => {
const code = ($('#ae-code-input').value || '').trim();
if (!code) return;
$('#ae-activate-msg').textContent = '激活中...';
const result = await activateCode(code);
$('#ae-activate-msg').textContent = result.msg;
$('#ae-activate-msg').style.color = result.ok ? '#34d399' : '#f87171';
if (result.ok) {
const c = await queryCredits();
updateCreditsDisplay(c);
}
});
// --- 搜题/填入/停止 ---
$('#ae-search-all').addEventListener('click', () => searchAllQuestions());
$('#ae-fill-all').addEventListener('click', () => fillAllAnswers());
$('#ae-stop').addEventListener('click', () => {
isSearching = false;
log.info('[考试] 已停止搜题');
});
// 付费模式查询积分
if (examMode === 'paid') {
queryCredits().then(c => updateCreditsDisplay(c));
}
}
// --- 搜题与填入逻辑 ---
let examResults = new Map();
let isSearching = false;
async function searchAllQuestions() {
if (isSearching) { log.warn('[考试] 正在搜题中...'); return; }
isSearching = true;
const questions = extractExamQuestions();
const listEl = $('#ae-question-list');
if (!listEl) { isSearching = false; return; }
listEl.innerHTML = '';
examResults.clear();
$('#ae-total').textContent = questions.length;
$('#ae-found').textContent = '0';
$('#ae-notfound').textContent = '0';
log.info(`[考试] 共 ${questions.length} 道题目,开始搜题...`);
let found = 0, notFound = 0;
for (let i = 0; i < questions.length; i++) {
if (!isSearching) break;
const q = questions[i];
const itemDiv = document.createElement('div');
itemDiv.className = 'ae-q-item searching';
itemDiv.id = `ae-q-${i}`;
itemDiv.innerHTML = `
第${i + 1}题 [${TYPE_NAMES[q.type] || '未知'}]
${q.text}
☕ 搜索中...
`;
listEl.appendChild(itemDiv);
const result = await searchAnswer(q.text, q.options, q.type);
const answerDiv = itemDiv.querySelector('.ae-q-answer');
const sourceDiv = itemDiv.querySelector('.ae-q-source');
if (result && result.answers.length > 0) {
answerDiv.textContent = '✅ ' + result.answers.join(' | ');
sourceDiv.textContent = '来源: ' + result.source;
itemDiv.className = 'ae-q-item found';
found++;
examResults.set(i, { question: q, result });
} else {
answerDiv.textContent = '❌ 未找到答案';
answerDiv.className = 'ae-q-answer empty';
itemDiv.className = 'ae-q-item not-found';
notFound++;
examResults.set(i, { question: q, result: null });
}
$('#ae-found').textContent = found;
$('#ae-notfound').textContent = notFound;
await sleep(600);
}
// 绑定单题按钮
listEl.querySelectorAll('.ae-q-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
const action = e.target.dataset.action;
const idx = parseInt(e.target.dataset.idx);
if (action === 'retry') {
const q = questions[idx];
const itemDiv = $(`#ae-q-${idx}`);
if (!itemDiv) return;
itemDiv.className = 'ae-q-item searching';
itemDiv.querySelector('.ae-q-answer').textContent = '☕ 重新搜索中...';
const result = await searchAnswer(q.text, q.options, q.type);
const ad = itemDiv.querySelector('.ae-q-answer');
const sd = itemDiv.querySelector('.ae-q-source');
if (result && result.answers.length > 0) {
ad.textContent = '✅ ' + result.answers.join(' | ');
sd.textContent = '来源: ' + result.source;
itemDiv.className = 'ae-q-item found';
examResults.set(idx, { question: q, result });
} else {
ad.textContent = '❌ 未找到答案';
ad.className = 'ae-q-answer empty';
itemDiv.className = 'ae-q-item not-found';
}
} else if (action === 'fill') {
const data = examResults.get(idx);
if (data && data.result) {
const ok = matchAndFill(data.question, data.result);
log.info(`[考试] 第${idx + 1}题 填入${ok ? '成功' : '失败'}`);
}
}
});
});
isSearching = false;
log.success(`[考试] 搜题完成! 找到:${found} 未找到:${notFound}`);
if (config.examAutoFill && found > 0) {
await sleep(1000);
fillAllAnswers();
}
}
async function fillAllAnswers() {
let filled = 0, failed = 0;
for (const [idx, data] of examResults) {
if (data.result) {
const ok = matchAndFill(data.question, data.result);
if (ok) filled++; else failed++;
await sleep(400);
}
}
log.success(`[考试] 填入完成! 成功:${filled} 失败:${failed}`);
}
// --- 考试页入口 ---
async function examMain(canContinue = () => true) {
log.info('考试助手启动!');
await sleep(3000);
if (!canContinue()) return;
// 练习组件可能在首次路由进入后才挂载;到面板创建前再判断一次,
// 让 AI 课程的 singleCourse/knowledgeStudy 习题自动进入 AI 模式。
if (isAiExamHost()) {
examMode = 'ai';
GM_setValue('av_exam_mode', 'ai');
}
createExamPanel();
if (config.autoExam) {
await sleep(2000);
if (!canContinue()) return;
searchAllQuestions();
}
}
// 获取并展示公告
function fetchAnnouncement() {
try {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://qsy.iano.cn/index.php?s=/admin/unified_manage/scriptannouncementapi',
timeout: 5000,
onload: function(resp) {
try {
var res = JSON.parse(resp.responseText);
if (res.code === 1 && res.data && res.data.enabled) {
var area = $('#av-notice-area');
if (!area) return;
var colors = { info: '#60a5fa', warning: '#fbbf24', error: '#f87171', success: '#4ade80' };
var c = colors[res.data.type] || '#60a5fa';
var el = document.createElement('div');
el.className = 'av-notice';
var borders = { info:'rgba(96,165,250,0.3)', warning:'rgba(251,191,36,0.3)', error:'rgba(248,113,113,0.3)', success:'rgba(74,222,128,0.3)' };
el.style.borderColor = borders[res.data.type] || borders.info;
var header = document.createElement('div');
header.className = 'av-notice-title';
var title = document.createElement('span');
title.style.color = c;
title.innerHTML = '📢 ';
title.appendChild(document.createTextNode(String(res.data.title || '公告')));
var close = document.createElement('span');
close.className = 'av-notice-close';
close.innerHTML = '×';
header.appendChild(title);
header.appendChild(close);
el.appendChild(header);
// 公告图片可能由 images/image_url 字段返回,也可能直接写在正文(Markdown 或 img 标签)中。
// 旧实现只把 content 塞进 innerHTML,后台使用独立图片字段时图片会被完全丢弃。
var rawContent = String(res.data.content || '');
var noticeImages = [];
var addImage = function(url) {
url = String(url || '').trim();
if (!url) return;
if (/^data:image\//i.test(url) || /^https?:\/\//i.test(url)) {
noticeImages.push(url);
return;
}
if (/^\/\//.test(url)) {
noticeImages.push('https:' + url);
return;
}
try {
var parsedUrl = new URL(url, 'https://qsy.iano.cn/');
if (/^https?:$/.test(parsedUrl.protocol)) noticeImages.push(parsedUrl.href);
} catch (_) {}
};
var images = res.data.images;
if (Array.isArray(images)) {
images.forEach(addImage);
} else if (typeof images === 'string' && images.trim()) {
try {
var parsedImages = JSON.parse(images);
(Array.isArray(parsedImages) ? parsedImages : [images]).forEach(addImage);
} catch (_) {
images.split(/[\n,]+/).forEach(addImage);
}
}
if (res.data.image_url) addImage(res.data.image_url);
rawContent = rawContent.replace(/!\[[^\]]*\]\(\s*([^\s)>]+)>?(?:\s+["'][^)]*)?\)/g, function(_, url) {
addImage(url);
return '';
});
rawContent = rawContent.replace(/
]*\bsrc\s*=\s*(?:["']([^"']+)["']|([^\s>]+))[^>]*>/gi, function(_, quotedUrl, bareUrl) {
addImage(quotedUrl || bareUrl);
return '';
}).trim();
noticeImages = noticeImages.filter(function(url, idx, arr) {
return url && arr.indexOf(url) === idx;
}).slice(0, 6);
var body = document.createElement('div');
body.className = 'av-notice-body';
// 保留常见的换行/粗体等公告排版,同时移除脚本和事件属性。
var contentBox = document.createElement('div');
var contentSource = document.createElement('div');
contentSource.innerHTML = rawContent;
contentSource.querySelectorAll('script,style,iframe,object,embed,link,meta').forEach(function(node) {
node.remove();
});
contentSource.querySelectorAll('*').forEach(function(node) {
Array.from(node.attributes).forEach(function(attr) {
if (/^on/i.test(attr.name) || attr.name === 'style' || attr.name === 'src' || attr.name === 'href') {
node.removeAttribute(attr.name);
}
});
});
contentBox.innerHTML = contentSource.innerHTML;
if (contentBox.textContent.trim() || contentBox.querySelector('*')) body.appendChild(contentBox);
if (noticeImages.length) {
var imageBox = document.createElement('div');
imageBox.className = 'av-notice-images';
noticeImages.forEach(function(url) {
var link = document.createElement('a');
link.className = 'av-notice-image-link';
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.title = '点击查看原图';
link.setAttribute('aria-label', '查看公告原图');
var img = document.createElement('img');
img.src = url;
img.alt = '公告图片';
img.loading = 'lazy';
img.decoding = 'async';
img.referrerPolicy = 'no-referrer';
img.addEventListener('error', function() {
link.style.display = 'none';
var tip = document.createElement('div');
tip.className = 'av-notice-image-fallback';
tip.textContent = '公告图片加载失败';
imageBox.appendChild(tip);
}, { once: true });
link.appendChild(img);
imageBox.appendChild(link);
});
body.appendChild(imageBox);
var imageHint = document.createElement('div');
imageHint.className = 'av-notice-image-hint';
imageHint.textContent = '点击图片查看原图';
body.appendChild(imageHint);
}
el.appendChild(body);
area.appendChild(el);
close.onclick = function(){ el.style.display='none'; };
}
} catch(e) {}
}
});
} catch(e) {}
}
// ===================== 主流程 =====================
async function main() {
// 重启或停止后,旧的异步初始化不能再次创建面板/注册定时器。
stopAllTasks();
const generation = taskGeneration;
const startUrl = location.href;
const canContinue = () => generation === taskGeneration && location.href === startUrl;
watchPageContext(generation);
syncRuntimePanels(getRuntimePageMode());
// AI 课程的习题组件由 SPA 异步挂载。路由刚进入时可能只有空壳,
// 不能仅等待一次就落入学习面板,否则题目稍后出现时不会再创建答题面板。
if (isAICourseClass() && /\/(?:singleCourse\/knowledgeStudy|learnPage)\//i.test(location.pathname) &&
!document.querySelector('.questionContent, .questionTitle, .exam-preview, [class*="question-content"]')) {
const questionDeadline = Date.now() + 8000;
while (Date.now() < questionDeadline && !isExamPage() && !getVideo()) {
if (!canContinue()) return;
await sleep(400);
}
}
if (!canContinue()) return;
// 考试页走考试流程。AI 课程习题组件可能晚于首屏挂载,再补一次检测,
// 避免把没有 video 的习题页误当成普通学习页。
let examPage = isExamPage();
if (examPage) {
await examMain(canContinue);
return;
}
if (!isStudyPage()) {
log.info('当前页面非学习页,脚本待命中');
createPanel();
fetchAnnouncement();
updatePanelStatus('⏳ 等待进入学习/考试页面...');
return;
}
log.info('智慧树刷课助手启动!');
log.info(`页面类型: ${isAICourseClass() ? '智慧树 AI 课程' : isHikeClass() ? '翻转课' : isNewVersion() ? '智慧共享课(新版)' : '共享课'}`);
// 等待页面加载
await sleep(3000);
if (!canContinue()) return;
// 创建面板
createPanel();
// 获取公告
fetchAnnouncement();
// 页面优化
await optimizePage(canContinue);
if (!canContinue()) return;
// 等待视频元素
await waitForVideo(canContinue);
if (!canContinue()) return;
// 异步加载期间出现的独立习题页才进入习题流程,助教弹题保留学习流程。
if (isExamPage()) {
await examMain(canContinue);
return;
}
resetProgressHealth();
completionSince = 0;
// 初始化视频设置
if (!hasBlockingPlatformDialog()) {
overridePause();
setMute();
setSpeed();
}
const title = getLessonTitle();
log.info(`正在学习: ${title}`);
updatePanelStatus(`▶️ ${title}`);
// 启动定时任务
// 1. 检测暂停并续播 (每3秒)
addTimer(checkAndPlay, 3000);
// 2. 视频设置维持(倍速、静音) (每5秒)
addTimer(() => {
overridePause();
setMute();
setSpeed();
}, 5000);
// 3. 弹题检测 (每3秒)
addTimer(skipQuestions, 3000);
// 保留加载中/错误弹层,暂停自动操作,避免触发平台的退出回调。
addTimer(clearBlankDialogs, 1000);
// 4. 弹窗关闭 (每4秒)
addTimer(closePopups, 4000);
// 5. 人机验证检测 (每5秒)
addTimer(checkVerify, 5000);
// 6. 习惯分模拟鼠标 (每8秒)
addTimer(simulateMouseMove, 8000);
// 7. 进度监控 & 自动切换 (每3秒)
addTimer(checkLessonProgress, 3000);
log.success('所有任务模块已启动');
}
async function waitForVideo(canContinue = () => true) {
let retries = 0;
while (!getVideo() && retries < 30) {
if (!canContinue() || isExamPage()) return;
await sleep(1000);
retries++;
}
if (!canContinue()) return;
if (getVideo()) {
log.info('视频元素已就绪');
} else {
log.warn('未找到视频元素,部分功能可能不可用');
}
}
// ===================== 油猴菜单命令 =====================
GM_registerMenuCommand('⚡ 重新启动脚本', () => {
stopAllTasks();
const panel = $('#autovisor-panel');
if (panel) panel.remove();
main();
});
GM_registerMenuCommand('⏹ 停止脚本', () => {
stopAllTasks();
updatePanelStatus('⏹ 脚本已停止');
log.info('脚本已手动停止');
});
GM_registerMenuCommand('🔄 重置配置', () => {
config = Object.assign({}, DEFAULT_CONFIG);
saveConfig(config);
log.info('配置已重置为默认值');
location.reload();
});
// ===================== 启动 =====================
main();
})();