// ==UserScript== // @name 🎯Daybreak网课助手(支持手机和平板)|🤖超星学习通|🎁智慧树|🧠智慧职教|✍️中国大学mooc|💯自动答题|🎫自动刷课 // @namespace daybreak // @tag 学习通 // @tag 自动答题 // @tag 自动刷课 // @tag 智慧树 // @tag 网课助手 // @license MIT // @icon http://pan-yz.chaoxing.com/favicon.ico // @version 3.1 // @author PIAOPIAO + jinmu // @description 一键安装,一键使用,支持学习通,知到,智慧职教,中国大学mooc等网课的视频学习,课后测验,期末考试等。具体功能请查看脚本悬浮窗中的教程页面。 // @match *://*.zhihuishu.com/* // @match *://*.hike-teaching-center.polymas.com/* // @match *://*.chaoxing.com/* // @match *://*.xuexitong.com/* // @match *://*.edu.cn/* // @match *://*.org.cn/* // @match *://*.xueyinonline.com/* // @match *://*.hnsyu.net/* // @match *://*.qutjxjy.cn/* // @match *://*.ynny.cn/* // @match *://*.hnvist.cn/* // @match *://*.fjlecb.cn/* // @match *://*.gdhkmooc.com/* // @match *://*.nbdlib.cn/* // @match *://*.cugbonline.cn/* // @match *://*.zjelib.cn/* // @match *://*.cqrspx.cn/* // @match *://*.neauce.com/* // @match *://*.zhihui-yun.com/* // @match *://*.cqie.cn/* // @match *://*.ccqmxx.com/* // @match *://*.jxgmxy.com/* // @match *://*.sslibrary.com/* // @match *://*.icve.com.cn/* // @match *://*.ai.icve.com.cn/* // @match *://*.course.icve.com.cn/* // @match *://*.courshare.cn/* // @match *://*.webtrn.cn/* // @match *://*.zjy2.icve.com.cn/* // @match *://*.zyk.icve.com.cn/* // @match *://*.icourse163.org/* // @match *://onlineexamh5new.zhihuishu.com/* // @require https://lib.baomitu.com/vue/3.5.0/vue.global.prod.js // @require https://cdn.jsdelivr.net/npm/vue@3.5.0/dist/vue.global.prod.js // @require https://unpkg.com/vue@3.5.0/dist/vue.global.prod.js // @require https://lib.baomitu.com/vue-demi/0.14.7/index.iife.js // @require https://cdn.jsdelivr.net/npm/vue-demi@0.14.7/lib/index.iife.js // @require data:application/javascript,window.Vue%3DVue%3B // @require https://lib.baomitu.com/element-plus/2.7.2/index.full.min.js // @require https://cdn.jsdelivr.net/npm/element-plus@2.7.2/dist/index.full.min.js // @require https://lib.baomitu.com/pinia/2.3.1/pinia.iife.min.js // @require https://cdn.jsdelivr.net/npm/pinia@2.3.1/dist/pinia.iife.min.js // @require data:application/javascript,window.Pinia%3DPinia%3B // @require https://lib.baomitu.com/rxjs/7.8.2/rxjs.umd.min.js // @require https://cdn.jsdelivr.net/npm/rxjs@7.8.2/dist/bundles/rxjs.umd.min.js // @require https://lib.baomitu.com/blueimp-md5/2.19.0/js/md5.min.js // @require https://cdn.jsdelivr.net/npm/blueimp-md5@2.19.0/js/md5.min.js // @require https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js // @require https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js // @resource crypto-js https://cdn.jsdelivr.net/npm/crypto-js@4.2.0/crypto-js.min.js // @resource ElementPlus https://lib.baomitu.com/element-plus/2.7.2/index.css // @resource ElementPlus https://cdn.jsdelivr.net/npm/element-plus@2.7.2/dist/index.css // @resource ElementPlusStyle https://lib.baomitu.com/element-plus/2.8.2/index.min.css // @resource ElementPlusStyle https://cdn.jsdelivr.net/npm/element-plus@2.8.2/dist/index.min.css // @resource ttf https://www.forestpolice.org/ttf/2.0/table.json // (network connects removed for pure-client distribution) // @grant GM_addStyle // @grant GM_getResourceText // @grant GM_getValue // @grant GM_info // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @grant GM_getTab // @grant GM_saveTab // @grant GM_listValues // @grant GM_deleteValue // @grant GM_notification // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @run-at document-idle // @antifeature payment 答案需调用AI的API需收费 // @connect * // ==/UserScript== ; (function(){ 'use strict'; // 提前声明 CDN 依赖变量,避免 TDZ 问题 var vue = (typeof window !== 'undefined' && window.Vue) ? window.Vue : (typeof globalThis !== 'undefined' && globalThis.Vue ? globalThis.Vue : null); var pinia = (typeof window !== 'undefined' && window.Pinia) ? window.Pinia : (typeof globalThis !== 'undefined' && globalThis.Pinia ? globalThis.Pinia : null); var rxjs = (typeof window !== 'undefined' && window.rxjs) ? window.rxjs : (typeof globalThis !== 'undefined' && globalThis.rxjs ? globalThis.rxjs : null); var md5 = (typeof window !== 'undefined' && window.md5) ? window.md5 : (typeof globalThis !== 'undefined' && globalThis.md5 ? globalThis.md5 : null); var ElementPlus = (typeof window !== 'undefined' && window.ElementPlus) ? window.ElementPlus : (typeof globalThis !== 'undefined' && globalThis.ElementPlus ? globalThis.ElementPlus : null); const _rfx_cfg = { b: 'eyJzYWx0IjogIlNWR0dlczFBMTdtcHUvc3dKZmgrUlE9PSIsICJpdGVyIjogMjAwMDAwLCAiY2lwaGVyIjogInRLNEMyRk9XWm9iNkk2a0ZQQzNJRmdqN2ZrMHZ6UFJpNllyaWhHeXpIK0RPdjFxVUhYbWFqWjc1QmhkaVVIWlBNcnRHNFJhMCJ9', p: 'chaoxing2026' }; try { if (typeof GM_setValue === 'function') { GM_setValue('_remote_redeem_server_blob', _rfx_cfg.b); GM_setValue('_server_passphrase', _rfx_cfg.p); } } catch(e) { console.warn('[学习通助手] 服务端配置注入失败:', e); } // === CDN依赖加载检测 === function checkDependencies() { const deps = [ { name: 'Vue', check: () => typeof window.Vue !== 'undefined' }, { name: 'Pinia', check: () => typeof window.Pinia !== 'undefined' }, { name: 'ElementPlus', check: () => typeof window.ElementPlus !== 'undefined' }, { name: 'rxjs', check: () => typeof window.rxjs !== 'undefined' }, { name: 'md5', check: () => typeof window.md5 !== 'undefined' } ]; const missing = []; deps.forEach(dep => { if (!dep.check()) { missing.push(dep.name); } }); if (missing.length > 0) { console.error('[学习通助手] CDN依赖加载失败:', missing.join(', ')); console.error('[学习通助手] 请检查网络连接或尝试刷新页面'); return false; } console.log('[学习通助手] 所有CDN依赖加载成功'); return true; } // === DOM就绪检测 === function waitForBody(callback, maxRetries = 10, retryDelay = 500) { let retries = 0; function check() { if (document.body) { callback(); } else if (retries < maxRetries) { retries++; console.log(`[学习通助手] 等待DOM就绪... (${retries}/${maxRetries})`); setTimeout(check, retryDelay); } else { console.error('[学习通助手] DOM加载超时,脚本可能无法正常工作'); } } check(); } // 非浏览器环境(如 Node.js)直接退出,避免 document/window 报错 if (typeof window === 'undefined' && typeof process !== 'undefined' && process.exit) { console.log('[scriptfor] 检测到 Node.js 环境,脚本仅支持浏览器,已退出'); process.exit(0); } if (typeof window !== 'undefined' && window.__chaoxing_helper_loaded__) { console.log('[chaoxing-helper] 已加载,跳过重复执行'); return; } if (typeof window !== 'undefined') window.__chaoxing_helper_loaded__ = true; // 初始化将在文件末尾所有组件定义之后执行(见 initApp() 调用) const LAYOUT_CSS = ` .main-page{z-index:2147483647;position:fixed;width:720px;max-width:720px;max-height:85vh;transition:height 0.35s cubic-bezier(0.4, 0, 0.2, 1),width 0.35s cubic-bezier(0.4, 0, 0.2, 1);background:#fff;color:#595959;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue','Microsoft YaHei',sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} .main-page .el-card{box-shadow:0 4px 24px rgba(0,0,0,0.08), 0 2px 8px rgba(0,0,0,0.04);border-radius:12px;border:1px solid #f0f0f0;height:100%;display:flex;flex-direction:column;background:#fff} .main-page .el-card__header{background:linear-gradient(135deg, #1890ff 0%, #1177d1 100%);color:#fff;padding:12px 16px;border-bottom:none;flex-shrink:0;border-radius:12px 12px 0 0} .main-page .el-card .card-header{font-size:14px;font-weight:600;color:#fff;cursor:move;display:flex;align-items:center;justify-content:space-between} .main-page .el-card .card-header .title{font-size:14px;font-weight:600;color:#fff} .main-page .el-card .minus{margin:5px 10px -10px 0} .main-page .el-card__body{padding:16px;background:transparent;flex:1;overflow-y:auto;overflow-x:hidden;font-size:13px;color:#595959} .main-page .config-tabs-container{display:flex;gap:0;margin-bottom:12px;border-bottom:1px solid #f0f0f0;flex-shrink:0;background:transparent;padding:4px;border-radius:8px 8px 0 0} .main-page .config-tab{background:transparent;border:1px solid #f0f0f0;padding:8px 14px;cursor:pointer;border-radius:8px;font-size:13px;transition:all 0.25s ease;white-space:nowrap;margin:0 2px;font-weight:400;color:#595959} .main-page .config-tab:hover{background:rgba(24,144,255,0.06);border-color:rgba(24,144,255,0.3)} .main-page .config-tab.active{background:rgba(24,144,255,0.1);border-color:#1890ff;color:#1890ff;font-weight:500;box-shadow:0 2px 8px rgba(24,144,255,0.15)} .main-page .config-panel{flex:1;overflow:visible;padding:0} .main-page .el-button--primary{background:linear-gradient(135deg, #1890ff 0%, #1177d1 100%);border:none;border-radius:8px;box-shadow:0 2px 8px rgba(24,144,255,0.25);transition:all 0.25s ease;color:#fff} .main-page .el-button--primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(24,144,255,0.35);background:linear-gradient(135deg, #40a9ff 0%, #1890ff 100%)} .main-page .el-button--primary:active{transform:translateY(0)} .main-page .el-button{appearance:none;-webkit-appearance:none;-moz-appearance:none;border-radius:8px;background-color:#fff;border:1px solid #d9d9d9;color:#595959;cursor:pointer;transition:all 0.2s ease} .main-page .el-button:hover{background:#f5f5f5;border-color:#1890ff;color:#1890ff} .main-page .el-button:active{transform:scale(0.98)} .main-page .el-input__wrapper{outline:none;border:1px solid #f0f0f0;padding:6px 12px;margin:0;background-color:#fafafa;border-radius:8px;color:#262626;transition:all 0.2s ease} .main-page .el-input__wrapper:hover{background-color:#ebeef4} .main-page .el-input__wrapper:focus{border:1px solid #0e8de290;box-shadow:0 0 4px #0e8de252;background-color:#fff!important} .main-page .el-input__inner{font-size:13px;color:#262626} .main-page .el-input-number{position:relative} .main-page .el-input-number .el-input__wrapper{padding-left:8px;padding-right:35px} .main-page .el-input-number__decrease,.main-page .el-input-number__increase{background:transparent;border:none;color:#595959;transition:all 0.2s ease;width:28px} .main-page .el-input-number__decrease:hover,.main-page .el-input-number__increase:hover{background:rgba(24,144,255,0.08)} .main-page .el-form-item__label{font-size:13px;color:#4e5969;font-weight:500;padding-right:12px} .main-page .el-checkbox{margin-right:8px;margin-bottom:6px} .main-page .el-checkbox__label{padding:6px 10px;background:#fff;border:1px solid #f0f0f0;border-radius:8px;font-size:13px;font-weight:400;color:#595959;cursor:pointer;transition:all 0.2s ease;display:inline-block;position:relative} .main-page .el-checkbox__input:hover+.el-checkbox__label{border-color:#1890ff;background:rgba(24,144,255,0.04)} .main-page .el-checkbox__input.is-checked+.el-checkbox__label{background:linear-gradient(135deg, #1890ff 0%, #1177d1 100%);border-color:#1890ff;color:#fff;box-shadow:0 2px 8px rgba(24,144,255,0.25)} .main-page .el-checkbox__inner{display:none} .main-page .el-divider{margin:16px 0;border-color:#f0f0f0} .main-page .el-text{font-size:12px;color:#8c8c8c} .main-page .el-scrollbar__bar{opacity:0.4!important} .main-page .el-scrollbar__thumb{background:rgba(24,144,255,0.35)!important;border-radius:4px!important;transition:background 0.2s ease} .main-page .el-scrollbar__thumb:hover{background:rgba(24,144,255,0.55)!important} .main-page .el-card__body::-webkit-scrollbar{width:8px;height:8px} .main-page .el-card__body::-webkit-scrollbar-track{background:transparent} .main-page .el-card__body::-webkit-scrollbar-thumb{background:rgba(24,144,255,0.35);border-radius:4px} .main-page .el-card__body::-webkit-scrollbar-thumb:hover{background:rgba(24,144,255,0.55)} .main-page .config-panel::-webkit-scrollbar{width:8px;height:8px} .main-page .config-panel::-webkit-scrollbar-track{background:transparent} .main-page .config-panel::-webkit-scrollbar-thumb{background:rgba(24,144,255,0.35);border-radius:4px} .main-page .config-panel::-webkit-scrollbar-thumb:hover{background:rgba(24,144,255,0.55)} .high-z-index-select{z-index:200000!important} .main-page .el-tag{border-radius:6px;font-weight:500;padding:2px 8px} .main-page .el-message{box-shadow:0 4px 16px rgba(0,0,0,0.12);border-radius:8px} .main-page .el-dialog{border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.12)} .main-page .el-dialog__header{border-bottom:1px solid #f0f0f0;padding:16px 24px} .main-page .notes{background:linear-gradient(135deg, rgba(24,144,255,0.06) 0%, rgba(24,144,255,0.02) 100%);border-left:4px solid #1890ff;width:100%;margin:8px 0;line-height:26px;letter-spacing:0.5px;padding:8px 12px;border-radius:0 8px 8px 0} .main-page .secondary{font-size:12px;color:#8c8c8c} .main-page a{color:#1890ff;text-decoration:none;transition:color 0.2s ease} .main-page a:hover{color:#40a9ff} .main-page .dropdown{position:relative;display:inline-block} .main-page .dropdown.active .dropdown-trigger-element{color:#1890ff} .main-page .dropdown-trigger-element{cursor:pointer;transition:color 0.2s ease} .main-page .dropdown-content{display:none;position:absolute;background-color:#fff;overflow:auto;box-shadow:0 8px 24px rgba(0,0,0,0.12);z-index:999;border-radius:8px;padding:8px 12px;min-width:120px;border:1px solid #f0f0f0} .main-page .dropdown-content.show{display:block} .main-page .dropdown-content .dropdown-option{padding-left:4px;white-space:nowrap} .main-page .dropdown-content .dropdown-option:hover{background-color:#f5f5f5;border-radius:4px} .main-page .dropdown-content .dropdown-option.active{background-color:rgba(24,144,255,0.08);color:#1890ff;border-radius:4px} .main-page .space{display:inline-flex} .main-page .alert-info-wrapper{margin-bottom:8px} .main-page .alert-info-wrapper .result-info{padding:12px;text-align:center;border-radius:8px} .main-page .alert-info-wrapper .unresolved{color:#8c8c8c;background-color:#f5f5f5} .main-page .alert-info-wrapper .no-answer{color:#8c8c8c;background-color:#f5f5f5} .main-page .alert-info-wrapper .error{color:#ff4d4f;background-color:#fff1f0} @keyframes show{0%{transform:translateY(20px);opacity:0}100%{transform:translateY(0);opacity:1}} @keyframes fade-in{0%{opacity:0}100%{opacity:1}} @keyframes fade-out{0%{opacity:1}100%{opacity:0}} @keyframes slide-in-left{0%{transform:translateX(-20px);opacity:0}100%{transform:translateX(0);opacity:1}} @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.6}} .main-page .config-details{animation:fade-in 0.4s ease} .main-page .config-details label{padding-left:12px} .main-page .el-message{animation:show 0.4s ease} .main-page .checkbox-label{display:inline-block!important;position:relative;cursor:pointer;font-size:16px;color:#262626} .main-page .checkbox-input{position:absolute;opacity:0;width:0;height:0} .main-page .checkbox-label::after{content:'';display:inline-block;border-radius:50%;transition:all 0.15s ease;position:relative;margin-left:4px;vertical-align:middle} .main-page .checkbox-label::before{content:'🔽';position:absolute;width:0;height:0;right:8px;transition:all 0.15s ease;z-index:2} .main-page .checked .checkbox-label::before{content:'🔼'} .main-page .el-table{font-size:13px;color:#595959;border-radius:8px;overflow:hidden} .main-page .el-table th{background:linear-gradient(135deg, #f7f8fa 0%, #f0f2f5 100%)!important;color:#262626;font-weight:600} .main-page .el-table tr:hover{background:rgba(24,144,255,0.04)!important} .main-page .el-pagination{font-weight:400;color:#595959} .main-page .el-pagination button:disabled{background:#f5f5f5} .main-page .search-result-answer-tag{background:linear-gradient(135deg, #f6ffed 0%, #e6f7e6 100%);border:1px solid #b7eb8f;color:#52c41a;padding:4px 10px;border-radius:6px;margin-right:8px;font-weight:500} .main-page .search-result-answer-tag.yellow{background:linear-gradient(135deg, #fffbe6 0%, #fff3e0 100%);border:1px solid #ffe58f;color:#faad14} .main-page .search-result-answer-tag.error{background:linear-gradient(135deg, #fff1f0 0%, #ffe6e6 100%);border:1px solid #ffa39e;color:#ff4d4f} .main-page .search-result-question-type{background:linear-gradient(135deg, #e6f7ff 0%, #d6f0ff 100%);border:1px solid #91d5ff;color:#1890ff;margin-right:8px;padding:2px 8px;border-radius:6px;font-weight:500} .main-page .copy{padding:4px 8px;border-radius:6px;box-shadow:0 2px 6px rgba(0,0,0,0.08);cursor:pointer!important;font-weight:500;transition:all 0.2s ease} .main-page .copy:hover{background:#f5f5f5;box-shadow:0 4px 12px rgba(0,0,0,0.12);transform:translateY(-1px)} .main-page .el-switch.is-checked .el-switch__core{background:linear-gradient(135deg, #1890ff 0%, #1177d1 100%);border-color:#1890ff} .main-page .el-slider__bar{background:linear-gradient(90deg, #1890ff 0%, #40a9ff 100%)} .main-page .el-slider__button{border-color:#1890ff;box-shadow:0 2px 6px rgba(24,144,255,0.25)} .main-page .el-progress__text{color:#595959;font-weight:600} .main-page .el-tag--success{background:linear-gradient(135deg, #f6ffed 0%, #e6f7e6 100%);border-color:#b7eb8f;color:#52c41a} .main-page .el-tag--warning{background:linear-gradient(135deg, #fffbe6 0%, #fff3e0 100%);border-color:#ffe58f;color:#faad14} .main-page .el-tag--danger{background:linear-gradient(135deg, #fff1f0 0%, #ffe6e6 100%);border-color:#ffa39e;color:#ff4d4f} .main-page .el-tag--info{background:linear-gradient(135deg, #f5f7fa 0%, #f0f2f5 100%);border-color:#d9d9d9;color:#8c8c8c} .main-page hr{border-style:solid;border-color:#f0f0f0;border-width:0 0 1px 0;margin:16px 0} .main-page ul,.main-page ol{line-height:28px;padding-left:24px;margin:0} .main-page .card{background-color:#fff;border-radius:8px;padding:8px 12px;box-shadow:0 2px 8px rgba(0,0,0,0.04);border:1px solid #f0f0f0} .main-page .card+.card{margin-top:12px} .main-page .script-panel-body{padding:0 12px} `; (typeof GM_addStyle === "function" ? GM_addStyle(LAYOUT_CSS) : (function(){var s=document.createElement("style");s.textContent=LAYOUT_CSS;document.head.append(s);})()); const JINMU_LAYOUT_CSS = ` :root{ --jb-primary:#1890ff; --jb-primary-2:#1177d1; --jb-primary-rgb:24,144,255; --jb-primary-2-rgb:17,119,209; --jb-primary-bg:rgba(24,144,255,0.1); --jb-primary-10:#1890ff1a; --jb-border-prim:#1890ff69; --jb-card-radius:12px; --jb-btn-radius:8px; --jb-card-shadow:0 4px 24px rgba(0,0,0,0.08), 0 2px 8px rgba(0,0,0,0.04); --jb-btn-shadow:0 2px 8px rgba(24,144,255,0.25); --jb-soft-shadow:0 2px 8px rgba(0,0,0,0.06); --jb-success:#52c41a; --jb-warning:#faad14; --jb-danger:#ff4d4f; --jb-info:#1890ff; --jb-bg-primary:#ffffff; --jb-bg-secondary:#f7f8fa; --jb-bg-tertiary:#f0f2f5; --jb-text-primary:#262626; --jb-text-secondary:#595959; --jb-text-tertiary:#8c8c8c; --jb-border:#d9d9d9; --jb-border-light:#f0f0f0; --jb-spacing-xs:4px; --jb-spacing-sm:8px; --jb-spacing-md:12px; --jb-spacing-lg:16px; --jb-spacing-xl:24px; --jb-font-xs:11px; --jb-font-sm:12px; --jb-font-md:13px; --jb-font-lg:14px; --jb-font-xl:16px; --jb-font-2xl:18px; --jb-transition-fast:0.15s ease; --jb-transition-normal:0.25s ease; --jb-transition-slow:0.35s cubic-bezier(0.4, 0, 0.2, 1); } .main-page{z-index:2147483647;position:fixed;width:720px;max-width:720px;transition:height var(--jb-transition-slow),width var(--jb-transition-slow);background:var(--jb-bg-primary);color:var(--jb-text-secondary);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue','Microsoft YaHei',sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} .main-page .el-card{box-shadow:var(--jb-card-shadow);border-radius:var(--jb-card-radius);border:1px solid var(--jb-border-light);background:var(--jb-bg-primary)} .main-page .el-card__header{background:linear-gradient(135deg, var(--jb-primary) 0%, var(--jb-primary-2) 100%);color:#fff;padding:var(--jb-spacing-md) var(--jb-spacing-lg);border-bottom:none;flex-shrink:0;border-radius:var(--jb-card-radius) var(--jb-card-radius) 0 0} .main-page .el-card .card-header{font-size:var(--jb-font-lg);font-weight:600;color:#fff;cursor:move;display:flex;align-items:center;justify-content:space-between} .main-page .el-card__body{padding:var(--jb-spacing-lg);background:transparent;flex:1;overflow:visible;font-size:var(--jb-font-md);color:var(--jb-text-secondary)} .cf-buy-link{color:var(--jb-primary);margin-left:var(--jb-spacing-sm);text-decoration:none;cursor:pointer;transition:color var(--jb-transition-fast)} .cf-buy-link:hover{color:var(--jb-primary-2);text-decoration:underline} .el-button--primary{background:linear-gradient(135deg, var(--jb-primary) 0%, var(--jb-primary-2) 100%) !important;color:#fff !important;border:none !important;border-radius:var(--jb-btn-radius) !important;box-shadow:var(--jb-btn-shadow) !important;transition:all var(--jb-transition-normal) !important} .el-button--primary:hover{transform:translateY(-1px) !important;box-shadow:0 4px 12px rgba(24,144,255,0.35) !important} .el-button--primary:active{transform:translateY(0) !important} .el-input__inner{font-size:var(--jb-font-md);border-radius:var(--jb-btn-radius);transition:all var(--jb-transition-fast)} /* 通用按钮交互状态 */ .main-page button{position:relative;overflow:hidden;outline:none} .main-page button:focus-visible{box-shadow:0 0 0 3px rgba(var(--jb-primary-rgb),0.4) !important} .main-page button:not(:disabled):hover{filter:brightness(1.08);transform:translateY(-1px)} .main-page button:not(:disabled):active{transform:translateY(0) scale(0.98);filter:brightness(0.95)} .main-page button:disabled{opacity:0.6;cursor:not-allowed;transform:none !important;filter:none !important} /* 主色按钮(蓝色渐变) */ .main-page .btn-primary-gradient{background:linear-gradient(135deg, var(--jb-primary) 0%, var(--jb-primary-2) 100%);color:#fff;border:none;box-shadow:var(--jb-btn-shadow)} .main-page .btn-primary-gradient:not(:disabled):hover{box-shadow:0 4px 16px rgba(var(--jb-primary-rgb),0.4);background:linear-gradient(135deg, #2a9fff 0%, #1a8ae8 100%)} .main-page .btn-primary-gradient:not(:disabled):active{box-shadow:0 1px 4px rgba(var(--jb-primary-rgb),0.3);background:linear-gradient(135deg, #0f7ae5 0%, #0d6bbf 100%)} /* 成功色按钮(绿色渐变) */ .main-page .btn-success-gradient{background:linear-gradient(135deg, var(--jb-success) 0%, #3da51a 100%);color:#fff;border:none;box-shadow:0 2px 8px rgba(82,196,26,0.25)} .main-page .btn-success-gradient:not(:disabled):hover{box-shadow:0 4px 16px rgba(82,196,26,0.4);background:linear-gradient(135deg, #5fd62e 0%, #4bb822 100%)} .main-page .btn-success-gradient:not(:disabled):active{box-shadow:0 1px 4px rgba(82,196,26,0.2);background:linear-gradient(135deg, #45b018 0%, #359115 100%)} /* 描边按钮 */ .main-page .btn-outline-primary{border:1px solid var(--jb-primary);background:var(--jb-primary);color:#fff} .main-page .btn-outline-primary:not(:disabled):hover{background:var(--jb-primary-2);border-color:var(--jb-primary-2);box-shadow:0 2px 8px rgba(var(--jb-primary-rgb),0.2)} .main-page .btn-outline-primary:not(:disabled):active{background:#0d6bbf;border-color:#0d6bbf;box-shadow:0 1px 3px rgba(var(--jb-primary-rgb),0.15)} /* Tab 按钮 */ .main-page .btn-tab{border:none;background:transparent;color:var(--jb-text-tertiary);border-bottom:3px solid transparent} .main-page .btn-tab:not(:disabled):hover{background:rgba(24,144,255,0.04);color:var(--jb-text-secondary)} .main-page .btn-tab:not(:disabled):active{background:rgba(24,144,255,0.08)} .main-page .btn-tab.active{background:var(--jb-primary-bg);color:var(--jb-primary);border-bottom-color:var(--jb-primary);font-weight:600} /* 日志筛选按钮 */ .main-page .btn-log-filter{border:1px solid var(--jb-border);background:var(--jb-bg-secondary);color:var(--jb-text-secondary);border-radius:var(--jb-btn-radius)} .main-page .btn-log-filter:not(:disabled):hover{border-color:var(--jb-primary);color:var(--jb-primary);background:var(--jb-primary-bg)} .main-page .btn-log-filter:not(:disabled):active{background:rgba(var(--jb-primary-rgb),0.15)} .main-page .btn-log-filter.active{border-color:var(--jb-primary);color:#fff;background:var(--jb-primary)} /* 输入框聚焦状态 */ .main-page input[type="text"]:focus{border-color:var(--jb-primary) !important;box-shadow:0 0 0 3px rgba(var(--jb-primary-rgb),0.15) !important;outline:none} /* 错误提示框中的重试按钮 */ .main-page .btn-retry{border:1px solid var(--jb-danger);background:transparent;color:var(--jb-danger);border-radius:var(--jb-btn-radius)} .main-page .btn-retry:not(:disabled):hover{background:rgba(255,77,79,0.08);border-color:#ff6b6d} .main-page .btn-retry:not(:disabled):active{background:rgba(255,77,79,0.15)} /* 成功提示框中的确认按钮 */ .main-page .btn-confirm{border:1px solid var(--jb-success);background:transparent;color:var(--jb-success);border-radius:var(--jb-btn-radius)} .main-page .btn-confirm:not(:disabled):hover{background:rgba(82,196,26,0.08);border-color:#6dd436} .main-page .btn-confirm:not(:disabled):active{background:rgba(82,196,26,0.15)} ::-webkit-scrollbar{width:8px;height:8px} ::-webkit-scrollbar-track{background:var(--jb-bg-secondary);border-radius:4px} ::-webkit-scrollbar-thumb{background:rgba(24,144,255,0.35);border-radius:4px;transition:background var(--jb-transition-fast)} ::-webkit-scrollbar-thumb:hover{background:rgba(24,144,255,0.55)} @keyframes pulse{0%,100%{transform:scale(1);box-shadow:0 4px 12px rgba(255,77,79,0.15)}50%{transform:scale(1.01);box-shadow:0 6px 16px rgba(255,77,79,0.25)}} `; (typeof GM_addStyle === "function" ? GM_addStyle(JINMU_LAYOUT_CSS) : (function(){var s=document.createElement("style");s.textContent=JINMU_LAYOUT_CSS;document.head.append(s);})()); // 强制侧边导航(将顶部导航移动到左侧侧栏) const FORCE_NAV_CSS = ` /* card_content 作为水平容器,使导航与内容在一行显示(左侧导航) */ .main-page .card_content{display:flex!important;align-items:stretch!important;position:relative!important;flex-direction:row!important;height:100%!important;overflow:hidden!important} .main-page .el-card__body{overflow:hidden!important;display:flex!important;flex-direction:column!important;height:100%!important} .main-page .config-tabs-container{display:flex!important;flex-direction:column!important;flex:0 0 200px!important;min-width:140px!important;max-width:260px!important;padding:12px!important;border-right:1px solid rgba(15,23,42,0.04)!important;gap:8px!important;background:transparent!important;margin:0!important;border-radius:8px!important;position:relative!important;z-index:9999!important;border-bottom:none!important;flex-shrink:0!important;overflow-y:auto!important;overflow-x:hidden!important;max-height:100%!important} .main-page .config-tabs-container::-webkit-scrollbar{width:0!important;height:0!important} .main-page .config-tab{display:flex!important;align-items:center!important;width:100%!important;padding:10px 12px!important;margin:4px 0!important;border-radius:8px!important;background:transparent!important;border:1px solid rgba(15,23,42,0.04)!important;text-align:left!important;justify-content:flex-start!important;cursor:pointer!important;position:relative!important;z-index:9999!important;pointer-events:auto!important;-webkit-user-select:none!important;user-select:none!important;border-bottom:none!important;white-space:normal!important;font-weight:400!important;color:#636363!important;flex-shrink:0!important} .main-page .config-tab:hover{transform:none!important;background:rgba(24,144,255,0.04)!important} .main-page .config-tab.active{background:var(--jb-primary)!important;color:#fff!important;border-color:transparent!important;box-shadow:0 6px 18px rgba(17,119,209,0.12)!important;font-weight:500!important} .main-page .config-tab:active{transform:scale(0.98)!important} .main-page .config-panel{margin-top:0!important;flex:1!important;padding-left:18px!important;position:relative!important;z-index:1!important;pointer-events:auto!important;overflow-y:auto!important;overflow-x:hidden!important;min-height:200px!important;max-height:100%!important} .main-page .config-panel::-webkit-scrollbar{width:8px!important;height:8px!important} .main-page .config-panel::-webkit-scrollbar-track{background:transparent!important} .main-page .config-panel::-webkit-scrollbar-thumb{background:rgba(24,144,255,0.35)!important;border-radius:4px!important} .main-page .config-panel::-webkit-scrollbar-thumb:hover{background:rgba(24,144,255,0.55)!important} .main-page .config-panel button{pointer-events:auto!important;cursor:pointer!important} @media (max-width:720px){ .main-page .card_content{display:block!important;height:auto!important;overflow:visible!important} .main-page .el-card__body{overflow-y:auto!important;overflow-x:hidden!important;height:auto!important;display:block!important} .main-page .config-tabs-container{display:flex!important;flex-direction:row!important;overflow-x:auto!important;overflow-y:hidden!important;border-right:none!important;border-bottom:2px solid rgba(15,23,42,0.04)!important;padding:8px!important;flex:unset!important;min-width:unset!important;max-width:unset!important;max-height:none!important} .main-page .config-tab{display:inline-flex!important;margin:0 6px!important;width:auto!important;flex-shrink:0!important} .main-page .config-panel{padding-left:0!important;overflow:visible!important;max-height:none!important} /* 移动端适配优化 */ .main-page .config-panel button{min-height:44px!important;font-size:14px!important;padding:10px 16px!important} .main-page .config-panel input,.main-page .config-panel select{min-height:44px!important;font-size:16px!important} .main-page .el-tabs__item{padding:0 16px!important;font-size:14px!important} .main-page .el-card__header{padding:12px 16px!important} .main-page .el-form-item__label{font-size:14px!important} .main-page .log-panel{font-size:13px!important} .main-page .progress-bar{height:24px!important} .main-page .invite-code-display{font-size:16px!important;word-break:break-all!important} } @media (max-width:480px){ .main-page .config-panel{padding:8px!important} .main-page .el-tabs__nav-scroll{overflow-x:auto!important} .main-page .el-form-item{margin-bottom:12px!important} .main-page .el-button{width:100%!important;margin-bottom:8px!important} } `; (typeof GM_addStyle === "function" ? GM_addStyle(FORCE_NAV_CSS) : (function(){var s=document.createElement("style");s.textContent=FORCE_NAV_CSS;document.head.append(s);})()); function openExternalUrl(url, sameTab = false){ try{ if(!url) return false; url = String(url); const target = sameTab ? '_self' : '_blank'; try{ const w = window.open(url, target); if(w) return true; }catch(e){} try{ if(typeof unsafeWindow !== 'undefined' && unsafeWindow.open){ const w2 = unsafeWindow.open(url, target); if(w2) return true; } }catch(e){} try{ if(typeof _unsafeWindow !== 'undefined' && _unsafeWindow.open){ const w3 = _unsafeWindow.open(url, target); if(w3) return true; } }catch(e){} try{ if(window.top && window.top !== window && window.top.open){ const w4 = window.top.open(url, target); if(w4) return true; } }catch(e){} const a = document.createElement('a'); a.href = url; a.target = target; a.rel = 'noopener noreferrer'; a.style.display='none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); return true; }catch(e){ console.warn('[openExternalUrl] fallback to location', e); try{ location.href = url; return true;}catch(e2){return false;} } } function attachExternalLinkInterceptors(root){ try{ const container = root || document; container.addEventListener('click', function(e){ let anchor = null; try{ anchor = (e.target && e.target.closest) ? e.target.closest('a') : null; }catch(e){} if(!anchor && e.path){ for(const p of e.path){ if(p && p.tagName === 'A'){ anchor = p; break; } } } if(anchor && anchor.href && anchor.href.includes('hsfaka.cn')){ e.preventDefault(); openExternalUrl(anchor.href); } }, true); }catch(e){ console.warn('attachExternalLinkInterceptors failed', e); } } try{ attachExternalLinkInterceptors(document); }catch(e){} // ===== 高级答题引擎 ===== const AdvancedAnswerEngine = { // ===== jinmu.js优秀算法集成 ===== normalizeAnswerText(str, ...exclude) { if (!str) return ""; exclude.push(...["①②③④⑤⑥⑦⑧⑨", "√", "×"]); return String(str) .replace(/<[^>]*>/g, '') .replace(/ | /gi, ' ') .replace(/^[\s\((\[]*[A-Ha-h][\))\].、.\.\s::-]+/, '') .replace(/^[\s\((\[]*[①②③④⑤⑥⑦⑧⑨][\))\].、.\.\s::-]*/, '') .trim() .toLocaleLowerCase() .replace(RegExp(`[^\\u2E80-\\u9FFFA-Za-z0-9${exclude.join("")}]*`, "g"), ""); }, normalizeJudgeAnswer(str) { const text = this.normalizeAnswerText(str, "√", "×"); if (/^(对|是|正确|确定|true|t|yes|1|√)$/.test(text)) return "对"; if (/^(错|否|非|错误|false|f|no|0|×|x)$/.test(text)) return "错"; return text; }, matches(target, options2) { const normalizedTarget = this.normalizeJudgeAnswer(target); return options2.some((option) => this.normalizeJudgeAnswer(option) === normalizedTarget); }, // 判断题关键词列表(完整版本,来自jinmu.js) JUDGE_TRUE_WORDS: ['是', '对', '正确', '确定', '√', '对的', '是的', '正确的', 'true', 'True', 'T', 'yes', '1'], JUDGE_FALSE_WORDS: ['非', '否', '错', '错误', '×', 'X', '错的', '不对', '不正确的', '不正确', '不是', '不是的', 'false', 'False', 'F', 'no', '0'], // 判断题智能答题(完全对齐 jinmu.js judgement 函数) smartAnswerJudge(question, options, knownAnswers = []) { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; if (logStore) { logStore.addLog('=== 判断题智能引擎启动 ===', 'info'); } // 兼容处理:options 可能是 {element, text} 对象数组或纯文本数组 const optionTexts = options.map(o => typeof o === 'string' ? o : o.text); const optionElements = options.map(o => typeof o === 'object' && o.element ? o.element : o); // 对齐 jinmu.js 的 matches 函数:标准化后比较 const matches = (target, words) => { return words.some(word => { const cleanTarget = this.jinmuClearString(this.jinmuRemoveRedundant(target), '√', '×'); const cleanWord = this.jinmuClearString(word, '√', '×'); return cleanTarget === cleanWord; }); }; const correctWords = ['是', '对', '正确', '确定', '√', '对的', '是的', '正确的', 'true', 'True', 'T', 'yes', '1']; const incorrectWords = ['非', '否', '错', '错误', '×', 'X', '错的', '不对', '不正确的', '不正确', '不是', '不是的', 'false', 'False', 'F', 'no', '0']; // 步骤1:使用已知答案匹配(对齐 jinmu.js judgement 核心逻辑) if (knownAnswers && knownAnswers.length > 0) { for (const answer of knownAnswers) { const answerShowCorrect = matches(answer, correctWords); const answerShowIncorrect = matches(answer, incorrectWords); if (answerShowCorrect || answerShowIncorrect) { // 找到对应的选项 for (let i = 0; i < optionTexts.length; i++) { const textShowCorrect = matches(optionTexts[i], correctWords); const textShowIncorrect = matches(optionTexts[i], incorrectWords); if (answerShowCorrect && textShowCorrect) { if (logStore) logStore.addLog(`[判断题] 题库匹配: 对`, 'success'); return { option: optionElements[i], confidence: 98, strategy: '题库精确匹配' }; } if (answerShowIncorrect && textShowIncorrect) { if (logStore) logStore.addLog(`[判断题] 题库匹配: 错`, 'success'); return { option: optionElements[i], confidence: 98, strategy: '题库精确匹配' }; } } } } } // 步骤2:查询本地题库缓存 try { const cleanQuestion = question.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const localDb = typeof Store === 'function' ? Store.get('local_db', {}) : {}; if (localDb && typeof localDb === 'object') { // 精确匹配 if (localDb[cleanQuestion]) { const dbAnswer = localDb[cleanQuestion]; const answerShowCorrect = matches(dbAnswer, correctWords); const answerShowIncorrect = matches(dbAnswer, incorrectWords); if (answerShowCorrect || answerShowIncorrect) { for (let i = 0; i < optionTexts.length; i++) { const textShowCorrect = matches(optionTexts[i], correctWords); const textShowIncorrect = matches(optionTexts[i], incorrectWords); if (answerShowCorrect && textShowCorrect) { if (logStore) logStore.addLog(`[判断题] LocalDB匹配: 对`, 'success'); return { option: optionElements[i], confidence: 95, strategy: 'LocalDB精确匹配' }; } if (answerShowIncorrect && textShowIncorrect) { if (logStore) logStore.addLog(`[判断题] LocalDB匹配: 错`, 'success'); return { option: optionElements[i], confidence: 95, strategy: 'LocalDB精确匹配' }; } } } } // 模糊匹配 for (const [key, value] of Object.entries(localDb)) { const cleanKey = key.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const score = this.bigramSimilarity(cleanQuestion, cleanKey); if (score > 0.6) { const dbAnswer = value; const answerShowCorrect = matches(dbAnswer, correctWords); const answerShowIncorrect = matches(dbAnswer, incorrectWords); if (answerShowCorrect || answerShowIncorrect) { for (let i = 0; i < optionTexts.length; i++) { const textShowCorrect = matches(optionTexts[i], correctWords); const textShowIncorrect = matches(optionTexts[i], incorrectWords); if (answerShowCorrect && textShowCorrect) { if (logStore) logStore.addLog(`[判断题] LocalDB相似(${Math.round(score*100)}%): 对`, 'success'); return { option: optionElements[i], confidence: Math.round(90 + score * 5), strategy: 'LocalDB相似匹配' }; } if (answerShowIncorrect && textShowIncorrect) { if (logStore) logStore.addLog(`[判断题] LocalDB相似(${Math.round(score*100)}%): 错`, 'success'); return { option: optionElements[i], confidence: Math.round(90 + score * 5), strategy: 'LocalDB相似匹配' }; } } } } } } } catch (e) {} // 兜底策略:判断题没有已知答案时,直接匹配选项中的"对"或"错" // 学习通判断题选项通常是固定的:"对"和"错" for (let i = 0; i < optionTexts.length; i++) { const optText = optionTexts[i].trim(); if (/^(对|正确|是|√|true|T|yes)$/i.test(optText)) { if (logStore) logStore.addLog('[判断题] 兜底: 选择"对"', 'warning'); return { option: optionElements[i], confidence: 50, strategy: '判断题兜底-对' }; } if (/^(错|错误|否|×|false|F|no)$/i.test(optText)) { if (logStore) logStore.addLog('[判断题] 兜底: 选择"错"', 'warning'); return { option: optionElements[i], confidence: 50, strategy: '判断题兜底-错' }; } } // 终极兜底:选第一个选项 if (optionElements.length > 0) { if (logStore) logStore.addLog('[判断题] 终极兜底: 选择第一个选项', 'warning'); return { option: optionElements[0], confidence: 30, strategy: '判断题终极兜底' }; } if (logStore) { logStore.addLog('[判断题] 无可靠答案,返回 null', 'warning'); } return null; }, // Bigram相似度算法(直接来自jinmu.js) jinmuCompareTwoStrings(first, second) { first = (first || "").replace(/\s+/g, ""); second = (second || "").replace(/\s+/g, ""); if (first === second) return 1; if (first.length < 2 || second.length < 2) return 0; let firstBigrams = new Map(); for (let i = 0; i < first.length - 1; i++) { const bigram = first.substring(i, i + 2); const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) + 1 : 1; firstBigrams.set(bigram, count); } let intersectionSize = 0; for (let i = 0; i < second.length - 1; i++) { const bigram = second.substring(i, i + 2); const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) : 0; if (count > 0) { firstBigrams.set(bigram, count - 1); intersectionSize++; } } return 2 * intersectionSize / (first.length + second.length - 2); }, // 找到最佳匹配(来自jinmu.js) jinmuFindBestMatch(mainString, targetStrings) { if (typeof mainString !== "string" || !Array.isArray(targetStrings) || targetStrings.length === 0) { return { ratings: [], bestMatch: { target: "", rating: 0 }, bestMatchIndex: -1 }; } const ratings = []; let bestMatchIndex = 0; for (let i = 0; i < targetStrings.length; i++) { const currentTargetString = targetStrings[i]; if (typeof currentTargetString !== "string") continue; const currentRating = this.jinmuCompareTwoStrings(mainString, currentTargetString); ratings.push({ target: currentTargetString, rating: currentRating }); if (currentRating > ratings[bestMatchIndex].rating) { bestMatchIndex = i; } } const bestMatch = ratings[bestMatchIndex] || { target: "", rating: 0 }; return { ratings, bestMatch, bestMatchIndex }; }, // 清理字符串(来自jinmu.js,增强版) jinmuClearString(str, ...exclude) { return this.normalizeAnswerText(str, ...exclude); }, // 移除冗余内容(来自jinmu.js,增强版) jinmuRemoveRedundant(str) { return String(str || "") .replace(/<[^>]*>/g, '') .replace(/ | /gi, ' ') .replace(/^[\s\((\[]*[A-Ha-h][\))\].、.\.\s::-]+/, '') .replace(/^[\s\((\[]*[①②③④⑤⑥⑦⑧⑨][\))\].、.\.\s::-]*/, '') .trim(); }, // 答案相似度匹配(来自jinmu.js) jinmuAnswerSimilar(answers, options) { if (!answers || !Array.isArray(answers) || !options || !Array.isArray(options)) { return options.map(() => ({ rating: 0, target: "" })); } const _answers = answers.map(a => this.jinmuRemoveRedundant(a)).map(a => this.jinmuClearString(a)); const _options = options.map(o => this.jinmuRemoveRedundant(o)).map(o => this.jinmuClearString(o)); const similar = _answers.length !== 0 ? _options.map((option) => { if (option.trim() === "") { return { rating: 0, target: "" }; } return this.jinmuFindBestMatch(option, _answers).bestMatch; }) : _options.map(() => ({ rating: 0, target: "" })); return similar; }, // 同义词词典 SYNONYMS: { '正确': ['对', '是', 'true', '√', '准确', '无误', '正确的'], '错误': ['错', '否', 'false', '×', '不正确', '有误', '错误的'], '增加': ['上升', '提高', '增长', '增多', '增加了'], '减少': ['下降', '降低', '缩减', '变少', '减少了'], '包含': ['包括', '含有', '涵盖', '包含着'], '不包含': ['不包括', '不含有', '不涵盖'], '等于': ['是', '即为', '就是', '等于'], '不等于': ['不是', '并非', '并不等于'], '属于': ['归类于', '归入', '属于'], '不属于': ['不归入', '不属于'], '能够': ['可以', '能够', '可'], '不能': ['不可以', '不能够', '不可'], '必须': ['一定', '必须', '应当'], '不需要': ['不必', '不需要', '不必需'], }, // 绝对词列表(通常用于判断题排除) ABSOLUTE_WORDS: ['一定', '必须', '所有', '全部', '绝对', '总是', '永不', '完全', '只有', '仅仅'], // 否定词列表 NEGATION_WORDS: ['不', '否', '无', '非', '没', '没有', '未', '并非'], // 停用词列表(模块级别常量,避免重复创建) STOPWORDS: new Set(['的', '是', '在', '有', '和', '了', '我', '你', '他', '她', '它', '这', '那', '什么', '怎么', '为什么', '因为', '所以', '但是', '如果', '可以', '会', '能', '不', '没有', '一个', '一些', '这种', '那种', '这个', '那个']), // 编辑距离算法 levenshteinDistance(s1, s2) { s1 = s1 || ''; s2 = s2 || ''; const costs = []; for (let i = 0; i <= s1.length; i++) { let lastValue = i; for (let j = 0; j <= s2.length; j++) { if (i === 0) { costs[j] = j; } else if (j > 0) { let newValue = costs[j - 1]; if (s1.charAt(i - 1) !== s2.charAt(j - 1)) { newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1; } costs[j - 1] = lastValue; lastValue = newValue; } } if (i > 0) costs[s2.length] = lastValue; } return costs[s2.length]; }, // 编辑距离相似度 editDistanceSimilarity(s1, s2) { const maxLen = Math.max(s1.length, s2.length); if (maxLen === 0) return 1; const distance = this.levenshteinDistance(s1, s2); return 1 - distance / maxLen; }, // Jaccard相似度 jaccardSimilarity(s1, s2) { const set1 = new Set(s1.split('')); const set2 = new Set(s2.split('')); const intersection = [...set1].filter(x => set2.has(x)).length; const union = set1.size + set2.size - intersection; return union === 0 ? 0 : intersection / union; }, // 余弦相似度 cosineSimilarity(s1, s2) { const vector1 = {}; const vector2 = {}; const allChars = new Set([...s1, ...s2]); for (const char of s1) { vector1[char] = (vector1[char] || 0) + 1; } for (const char of s2) { vector2[char] = (vector2[char] || 0) + 1; } let dotProduct = 0; let magnitude1 = 0; let magnitude2 = 0; for (const char of allChars) { dotProduct += (vector1[char] || 0) * (vector2[char] || 0); magnitude1 += Math.pow(vector1[char] || 0, 2); magnitude2 += Math.pow(vector2[char] || 0, 2); } magnitude1 = Math.sqrt(magnitude1); magnitude2 = Math.sqrt(magnitude2); if (magnitude1 === 0 || magnitude2 === 0) return 0; return dotProduct / (magnitude1 * magnitude2); }, // 最长公共子串长度 lcsLength(s1, s2) { const matrix = Array(s1.length + 1).fill(null).map(() => Array(s2.length + 1).fill(0)); let maxLen = 0; for (let i = 1; i <= s1.length; i++) { for (let j = 1; j <= s2.length; j++) { if (s1[i - 1] === s2[j - 1]) { matrix[i][j] = matrix[i - 1][j - 1] + 1; maxLen = Math.max(maxLen, matrix[i][j]); } } } return maxLen; }, // LCS相似度 lcsSimilarity(s1, s2) { const maxLen = Math.max(s1.length, s2.length); if (maxLen === 0) return 0; return this.lcsLength(s1, s2) / maxLen; }, // Bigram相似度(原有算法) bigramSimilarity(first, second) { first = (first || "").replace(/\s+/g, ""); second = (second || "").replace(/\s+/g, ""); if (first === second) return 1; if (first.length < 2 || second.length < 2) return 0; const firstBigrams = new Map(); for (let i = 0; i < first.length - 1; i++) { const bigram = first.substring(i, i + 2); const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) + 1 : 1; firstBigrams.set(bigram, count); } let intersectionSize = 0; for (let i = 0; i < second.length - 1; i++) { const bigram = second.substring(i, i + 2); const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) : 0; if (count > 0) { firstBigrams.set(bigram, count - 1); intersectionSize++; } } return 2 * intersectionSize / (first.length + second.length - 2); }, // 综合相似度算法(加权组合 - 针对学习通优化) comprehensiveSimilarity(s1, s2) { s1 = (s1 || '').replace(/\s+/g, '').toLowerCase(); s2 = (s2 || '').replace(/\s+/g, '').toLowerCase(); if (s1 === s2) return 1; if (!s1 || !s2) return 0; const bigramSim = this.bigramSimilarity(s1, s2); const editSim = this.editDistanceSimilarity(s1, s2); const lcsSim = this.lcsSimilarity(s1, s2); const jaccardSim = this.jaccardSimilarity(s1, s2); const cosineSim = this.cosineSimilarity(s1, s2); // 学习通优化权重:Bigram(35%) + 编辑距离(25%) + LCS(20%) + Jaccard(10%) + Cosine(10%) // Bigram权重提高:对中文分词不敏感的场景更鲁棒 // Jaccard权重降低:对短文本区分度不够 return bigramSim * 0.35 + editSim * 0.25 + lcsSim * 0.20 + jaccardSim * 0.10 + cosineSim * 0.10; }, // 分割答案(同步 jinmu.js) splitAnswer(answer, separators = ["===", "#", "---", "###", "|", ";", ";"]) { answer = (answer || '').trim(); if (answer.length === 0) { return []; } separators = separators.filter((el) => el.trim().length > 0); // 尝试 JSON 解析 try { const json = JSON.parse(answer); if (Array.isArray(json)) { return json.map(String).filter((el) => el.trim().length > 0); } } catch { // JSON 解析失败,继续使用分隔符 } // 使用分隔符分割 for (const sep of separators) { if (answer.split(sep).length > 1) { return answer.split(sep).filter((el) => el.trim().length > 0); } } return [answer]; }, // 判断是否为纯字母答案(如 ABCD) isPlainAnswer(answer) { answer = (answer || '').trim(); if (answer.length > 8 || !/[A-Z]/.test(answer)) { return false; } const counter = {}; let min = 0; for (let i = 0; i < answer.length; i++) { if (answer.charCodeAt(i) < min) { return false; } min = answer.charCodeAt(i); counter[min] = (counter[min] || 0) + 1; } for (const key in counter) { if (counter[key] !== 1) { return false; } } return true; }, // 解析纯字母答案 resolvePlainAnswer(answer) { const resolve = (answer || '').trim().replace(/[,,、 #]/g, "").trim(); if (this.isPlainAnswer(resolve)) { return resolve; } }, // 检查是否包含否定词 hasNegation(text) { return this.NEGATION_WORDS.some(word => text.includes(word)); }, // 检查是否包含绝对词 hasAbsoluteWord(text) { return this.ABSOLUTE_WORDS.some(word => text.includes(word)); }, // 同义词替换 replaceSynonyms(text) { let result = text; for (const [key, synonyms] of Object.entries(this.SYNONYMS)) { for (const synonym of synonyms) { result = result.split(synonym).join(key); } } return result; }, // 提取关键词 extractKeywords(text) { const cleaned = text.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ' ').trim().toLowerCase(); const words = cleaned.split(/\s+/).filter(word => word.length > 1 && !this.STOPWORDS.has(word)); return [...new Set(words)]; }, // 关键词匹配度 keywordMatchScore(question, option) { const qKeywords = this.extractKeywords(question); const oKeywords = this.extractKeywords(option); if (qKeywords.length === 0) return 0; let matchCount = 0; for (const kw of oKeywords) { if (qKeywords.includes(kw)) { matchCount++; } } return matchCount / qKeywords.length; }, // 匹配已知答案的专门算法(合并版:整合字母解析+精确匹配+包含匹配+jinmu相似度) matchKnownAnswers(knownAnswers, options) { if (!knownAnswers || knownAnswers.length === 0) return null; const logStore = typeof useLogStore === 'function' ? useLogStore() : null; // 兼容处理:options 可能是 {element, text} 对象数组或纯文本数组 const optionTexts = options.map(o => typeof o === 'string' ? o : o.text); const optionElements = options.map(o => typeof o === 'object' && o.element ? o.element : o); // 步骤1:拆分所有答案 const allAnswers = []; for (const ans of knownAnswers) { const parts = this.splitAnswer(ans); allAnswers.push(...parts); } // 步骤2:纯字母答案解析(最高优先级,如 "A"、"B"、"ABCD") for (const answer of allAnswers) { const cleanAns = answer.replace(/<[^>]*>/g, '').trim(); if (cleanAns.length === 1 && /[A-Da-d]/.test(cleanAns)) { const idx = cleanAns.toUpperCase().charCodeAt(0) - 65; if (idx >= 0 && idx < optionElements.length) { if (logStore) logStore.addLog(`[已知答案] 字母解析: ${cleanAns} -> 选项${String.fromCharCode(65+idx)}`, 'success'); return { option: optionElements[idx], confidence: 98, strategy: '字母解析' }; } } // 多字母答案(如 "ABC") if (/^[A-Da-d]{2,4}$/.test(cleanAns)) { const firstIdx = cleanAns.toUpperCase().charCodeAt(0) - 65; if (firstIdx >= 0 && firstIdx < optionElements.length) { if (logStore) logStore.addLog(`[已知答案] 多字母解析: ${cleanAns} -> 选项${String.fromCharCode(65+firstIdx)}`, 'success'); return { option: optionElements[firstIdx], confidence: 95, strategy: '多字母解析' }; } } } // 步骤3:精确匹配(完全相等) const cleanAnswers = allAnswers.map(a => this.jinmuRemoveRedundant(a)).map(a => this.jinmuClearString(a)); const cleanOptions = optionTexts.map(o => this.jinmuRemoveRedundant(o)).map(o => this.jinmuClearString(o)); for (let i = 0; i < optionTexts.length; i++) { if (cleanAnswers.includes(cleanOptions[i])) { if (logStore) logStore.addLog(`[已知答案] 精确匹配: 选项${String.fromCharCode(65+i)}`, 'success'); return { option: optionElements[i], confidence: 100, strategy: '精确匹配' }; } } // 步骤4:包含匹配(答案包含选项文本,或选项文本包含答案) const rawAnswers = allAnswers.map(a => a.replace(/<[^>]*>/g, '').trim()); const rawOptions = optionTexts.map(o => o.replace(/<[^>]*>/g, '').trim()); for (let i = 0; i < optionTexts.length; i++) { if (!rawOptions[i]) continue; for (const ans of rawAnswers) { if (!ans) continue; if (ans.includes(rawOptions[i]) || rawOptions[i].includes(ans)) { if (logStore) logStore.addLog(`[已知答案] 包含匹配: 选项${String.fromCharCode(65+i)}`, 'success'); return { option: optionElements[i], confidence: 95, strategy: '包含匹配' }; } } } // 步骤5:jinmu.js answerSimilar 相似度匹配(核心算法) // 使用 optionTexts 进行相似度计算,但返回 optionElements const similarResults = this.jinmuAnswerSimilar(allAnswers, optionTexts); let bestMatch = null; let bestRating = 0; similarResults.forEach((result, index) => { if (result.rating > bestRating && result.rating >= 0.5) { bestRating = result.rating; bestMatch = { option: optionElements[index], confidence: Math.round(result.rating * 100), strategy: 'jinmu相似度' }; } }); if (bestMatch) { if (logStore) logStore.addLog(`[已知答案] jinmu相似度匹配: ${bestRating.toFixed(2)} -> 选项${String.fromCharCode(65 + optionElements.indexOf(bestMatch.option))}`, 'success'); return bestMatch; } return null; }, // 关键词匹配度 keywordMatchScore(question, option) { const qKeywords = this.extractKeywords(question); const oKeywords = this.extractKeywords(option); if (qKeywords.length === 0) return 0; let matchCount = 0; for (const kw of oKeywords) { if (qKeywords.includes(kw)) { matchCount++; } } return matchCount / qKeywords.length; }, // 智能答题核心算法(完全对齐 jinmu.js single 函数) // 核心逻辑:只匹配"答案"和"选项",绝不用"题目"匹配"选项" // 单选题智能答题(完全对齐 jinmu.js single 函数) smartAnswer(question, options, knownAnswers = []) { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; if (logStore) { logStore.addLog('=== 智能答题引擎启动 ===', 'info'); } // 兼容处理:options 可能是 {element, text} 对象数组或纯文本数组 const optionTexts = options.map(o => typeof o === 'string' ? o : o.text); const optionElements = options.map(o => typeof o === 'object' && o.element ? o.element : o); // 对齐 jinmu.js:将 knownAnswers 转换为 infos 格式 const results = knownAnswers.map(ans => ({ answer: ans })); // 完全对齐 jinmu.js single 函数逻辑 if (results.length > 0) { // 步骤1:收集所有答案 const allAnswer = results.map(res => this.splitAnswer(res.answer.trim())).flat(); // 步骤2:相似度匹配(对齐 jinmu.js answerSimilar) const cleanAnswers = allAnswer.map(a => this.jinmuClearString(this.jinmuRemoveRedundant(a))); const cleanOptionTexts = optionTexts.map(o => this.jinmuRemoveRedundant(o)); const ratings = this.jinmuAnswerSimilar(cleanAnswers, cleanOptionTexts); let index = -1; let max = 0; let ans = ""; ratings.forEach((rating, i) => { if (rating.rating > max) { max = rating.rating; index = i; ans = rating.target; } }); if (index !== -1 && max > 0.6) { if (logStore) logStore.addLog(`[智能答题] 相似度匹配成功: ${ans} (评分: ${max.toFixed(2)})`, 'success'); return { option: optionElements[index], confidence: Math.round(max * 100), strategy: 'jinmu相似度' }; } // 步骤3:精确匹配(对齐 jinmu.js answerExactMatch) for (let i = 0; i < optionTexts.length; i++) { const cleanOpt = this.jinmuClearString(this.jinmuRemoveRedundant(optionTexts[i])); if (cleanAnswers.includes(cleanOpt)) { if (logStore) logStore.addLog(`[智能答题] 精确匹配成功: 选项${String.fromCharCode(65+i)}`, 'success'); return { option: optionElements[i], confidence: 100, strategy: '精确匹配' }; } } // 步骤4:纯字母答案解析(对齐 jinmu.js) for (const res of results) { const ansText = res.answer.trim(); if (ansText.length === 1 && /[A-Z]/.test(ansText)) { const idx = ansText.charCodeAt(0) - 65; if (idx >= 0 && idx < optionElements.length) { if (logStore) logStore.addLog(`[智能答题] 字母解析: ${ansText} -> 选项${String.fromCharCode(65+idx)}`, 'success'); return { option: optionElements[idx], confidence: 98, strategy: '字母解析' }; } } } } // 兜底:选第一个选项(确保单选题必须答题) if (optionElements.length > 0) { if (logStore) logStore.addLog('[智能答题] 兜底: 选择第一个选项', 'warning'); return { option: optionElements[0], confidence: 30, strategy: '单选兜底' }; } return null; }, // 多选题智能答题(完全对齐 jinmu.js multiple 函数) smartAnswerMulti(question, options, knownAnswers = []) { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; if (logStore) { logStore.addLog('=== 多选题智能引擎启动 ===', 'info'); } // 兼容处理:options 可能是 {element, text} 对象数组或纯文本数组 const optionTexts = options.map(o => typeof o === 'string' ? o : o.text); const optionElements = options.map(o => typeof o === 'object' && o.element ? o.element : o); // 对齐 jinmu.js:将 knownAnswers 转换为 infos 格式 // jinmu.js: const results = infos.map((info) => info.results).flat(); // 每个 knownAnswer 相当于一个 result const results = knownAnswers.map(ans => ({ answer: ans })); // 完全对齐 jinmu.js multiple 函数逻辑 const similar_list = []; // 对齐 jinmu.js:遍历每个 result(每个答案源) for (let i = 0; i < results.length; i++) { const result = results[i]; // 对齐 jinmu.js:splitAnswer 拆分答案 const answers = this.splitAnswer(result.answer.trim()); // 策略1:包含匹配(对齐 jinmu.js) // jinmu.js: answers.find(answer => answer.includes(removeRedundant(option))) const matchResult = { options: [], answers: [], ratings: [], similarSum: 0, similarCount: 0 }; for (let j = 0; j < optionElements.length; j++) { const optText = this.jinmuRemoveRedundant(optionTexts[j]); // 用 answers 数组中的任意一个答案去包含选项 const matchedAnswer = answers.find( answer => this.jinmuClearString(this.jinmuRemoveRedundant(answer)).includes(optText) ); if (matchedAnswer) { matchResult.options.push(optionElements[j]); matchResult.answers.push(matchedAnswer); matchResult.ratings.push(1); matchResult.similarSum += 1; matchResult.similarCount += 1; } } // 策略2:answerSimilar 相似度匹配(对齐 jinmu.js) // jinmu.js: answerSimilar(answers, options.map(o => removeRedundant(o.innerText))) const cleanAnswers = answers.map(a => this.jinmuClearString(this.jinmuRemoveRedundant(a))); const cleanOptionTexts = optionTexts.map(o => this.jinmuRemoveRedundant(o)); const ratings = this.jinmuAnswerSimilar(cleanAnswers, cleanOptionTexts); const ratingResult = { options: [], answers: [], ratings: [], similarSum: 0, similarCount: 0 }; for (let j = 0; j < ratings.length; j++) { const rating = ratings[j]; if (rating.rating > 0.6) { ratingResult.options.push(optionElements[j]); ratingResult.answers.push(ratings[j].target); ratingResult.ratings.push(ratings[j].rating); ratingResult.similarSum += rating.rating; ratingResult.similarCount += 1; } } // 选择更好的结果(对齐 jinmu.js:比较 similarSum) if (matchResult.similarSum > ratingResult.similarSum) { similar_list[i] = matchResult; } else { similar_list[i] = ratingResult; } } // 排序:similarCount * 100 + similarSum(对齐 jinmu.js) const sorted_similar_list = similar_list .filter(item => item && item.similarCount !== 0) .sort((a, b) => { const bsc = b.similarCount * 100; const asc = a.similarCount * 100; const bss = b.similarSum; const ass = a.similarSum; return bsc + bss - asc - ass; }); if (sorted_similar_list.length > 0 && sorted_similar_list[0]) { const best = sorted_similar_list[0]; const matchedOptions = best.options.map((opt, idx) => ({ option: opt, confidence: Math.round(best.ratings[idx] * 100), strategy: 'jinmu多选匹配' })); if (logStore) { logStore.addLog(`[多选] jinmu匹配成功: ${matchedOptions.length}个选项`, 'success'); } return { options: matchedOptions, confidence: Math.round(best.similarSum / best.similarCount * 100), strategy: 'jinmu多选匹配' }; } // 纯字母答案解析(对齐 jinmu.js plainAnswer 逻辑) for (const result of results) { const ans = result.answer.trim(); const plainAnswer = this.resolvePlainAnswer(ans); if (plainAnswer) { const matchedOptions = []; for (const char of plainAnswer) { const index = char.charCodeAt(0) - 65; if (index >= 0 && index < optionElements.length) { matchedOptions.push({ option: optionElements[index], confidence: 95, strategy: '纯字母解析' }); } } if (matchedOptions.length > 0) { if (logStore) logStore.addLog(`[多选] 纯字母解析: ${plainAnswer}`, 'success'); return { options: matchedOptions, confidence: 95, strategy: '纯字母解析' }; } } } // 兜底:选前两个选项(确保多选题必须答题) if (optionElements.length >= 2) { if (logStore) logStore.addLog('[多选] 兜底: 选择前两个选项', 'warning'); return { options: [ { option: optionElements[0], confidence: 30, strategy: '多选兜底' }, { option: optionElements[1], confidence: 30, strategy: '多选兜底' } ], confidence: 30, strategy: '多选兜底' }; } if (optionElements.length === 1) { if (logStore) logStore.addLog('[多选] 兜底: 选择唯一选项', 'warning'); return { options: [{ option: optionElements[0], confidence: 30, strategy: '多选兜底' }], confidence: 30, strategy: '多选兜底' }; } return null; }, // 机器学习辅助匹配(已禁用 - 旧数据污染严重) mlAssistedMatch(question, options) { // 禁用 ML 辅助匹配,防止污染数据影响正确率 return null; }, // 记录答题结果用于机器学习 recordAnswer(question, questionType, selectedAnswer, isCorrect) { try { const trainingData = SafeStorage.get('quiz_ml_training') || []; const record = { question: question.replace(/<[^>]*>/g, '').trim(), questionType, selectedAnswer: selectedAnswer.replace(/<[^>]*>/g, '').trim(), isCorrect, timestamp: Date.now() }; // 限制训练数据大小 trainingData.push(record); if (trainingData.length > 1000) { trainingData.splice(0, trainingData.length - 1000); } SafeStorage.set('quiz_ml_training', trainingData); } catch (e) { console.warn('[AdvancedAnswerEngine] 记录答题失败:', e.message); } }, // 获取答题准确率 getAccuracy() { try { const trainingData = SafeStorage.get('quiz_ml_training') || []; if (trainingData.length === 0) return null; const correct = trainingData.filter(r => r.isCorrect).length; return (correct / trainingData.length * 100).toFixed(1); } catch (e) { return null; } } }; // ===== 原有StringUtil保持兼容 ===== const StringUtil = { compareTwoStrings(first, second) { return AdvancedAnswerEngine.bigramSimilarity(first, second); }, findBestMatch(mainString, targetStrings) { if (typeof mainString !== "string") throw new Error("Bad arguments: mainString must be string"); if (!Array.isArray(targetStrings) || !targetStrings.length) throw new Error("Bad arguments: targetStrings must be non-empty array"); const ratings = []; let bestMatchIndex = 0; for (let i = 0; i < targetStrings.length; i++) { const currentTargetString = targetStrings[i]; // 使用纯 bigram 匹配(对齐 jinmu.js) const currentRating = AdvancedAnswerEngine.bigramSimilarity(mainString, currentTargetString); ratings.push({ target: currentTargetString, rating: currentRating }); if (currentRating > ratings[bestMatchIndex].rating) { bestMatchIndex = i; } } const bestMatch = ratings[bestMatchIndex]; return { ratings, bestMatch, bestMatchIndex }; }, clearString(str, ...exclude) { exclude.push(...["①②③④⑤⑥⑦⑧⑨"]); return (str || "").trim().toLocaleLowerCase().replace(RegExp(`[^\\u2E80-\\u9FFFA-Za-z0-9${exclude.join("")}]*`, "g"), ""); }, removeRedundant(str) { return (str || "").trim().replace(/[A-Z]{1}[^A-Za-z0-9\u2E80-\u9FFF]+([A-Za-z0-9\u2E80-\u9FFF]+)/, "$1") || ""; }, answerSimilar(answers, options) { const _answers = answers.map(StringUtil.removeRedundant).map((a) => StringUtil.clearString(a)); const _options = options.map(StringUtil.removeRedundant).map((o) => StringUtil.clearString(o)); const similar = _answers.length !== 0 ? _options.map((option) => { if (option.trim() === "") return { rating: 0, target: "" }; return StringUtil.findBestMatch(option, _answers).bestMatch; }) : _options.map(() => ({ rating: 0, target: "" })); return similar; }, answerExactMatch(answers, options) { const _answers = answers.map(StringUtil.removeRedundant); const _options = options.map(StringUtil.removeRedundant); return _answers.length !== 0 ? _options.filter((option) => { return _answers.find((answer) => answer.trim() === option.trim()); }) : []; }, // 判断是否为纯字母答案(如 ABCD)- 同步 jinmu.js isPlainAnswer(answer) { answer = (answer || '').trim(); if (answer.length > 8 || !/[A-Z]/.test(answer)) { return false; } const counter = {}; let min = 0; for (let i = 0; i < answer.length; i++) { if (answer.charCodeAt(i) < min) { return false; } min = answer.charCodeAt(i); counter[min] = (counter[min] || 0) + 1; } for (const key in counter) { if (counter[key] !== 1) { return false; } } return true; }, // 解析纯字母答案 - 同步 jinmu.js resolvePlainAnswer(answer) { const resolve = (answer || '').trim().replace(/[,,、 #]/g, "").trim(); if (StringUtil.isPlainAnswer(resolve)) { return resolve; } } }; const RequestUtil = { async request(url, opts = {}) { const { responseType = "json", method = "get", type = "fetch", data = {}, headers = {} } = opts || {}; const env = (typeof window !== "undefined" && typeof window.document !== "undefined") ? "browser" : "node"; if (type === "GM_xmlhttpRequest" && env === "browser" && typeof GM_xmlhttpRequest !== "undefined") { return new Promise((resolve, reject) => { const contentType = headers["Content-Type"] || headers["content-type"]; const requestData = contentType === "application/x-www-form-urlencoded" ? new URLSearchParams(data).toString() : Object.keys(data).length ? JSON.stringify(data) : void 0; try { GM_xmlhttpRequest({ url, method: method.toUpperCase(), data: requestData, headers: Object.keys(headers).length ? headers : void 0, responseType: responseType === "json" ? "json" : void 0, onload: (response) => { if (response.status === 200) { if (responseType === "json") { try { resolve(JSON.parse(response.responseText)); } catch (error) { reject(error); } } else { resolve(response.responseText || ""); } } else { reject(response.responseText); } }, onerror: (err) => { reject(err); } }); } catch (err) { reject(err); } }); } else { const fet = env === "node" ? require("node-fetch").default : fetch; const resp = await fet(url, { method: method.toUpperCase(), body: method.toLowerCase() === "post" ? JSON.stringify(data) : void 0, headers }); if (responseType === "json") return resp.json(); return resp.text(); } } }; class SimpleEventBus { constructor() { this._listeners = Object.create(null); } on(evt, fn) { (this._listeners[evt] = this._listeners[evt] || []).push(fn); return () => this.off(evt, fn); } once(evt, fn) { const wrapper = (...args) => { this.off(evt, wrapper); fn(...args); }; return this.on(evt, wrapper); } off(evt, fn) { if (!this._listeners[evt]) return; const idx = this._listeners[evt].indexOf(fn); if (idx !== -1) this._listeners[evt].splice(idx, 1); } emit(evt, ...args) { (this._listeners[evt] || []).slice().forEach((f) => { try { f(...args); } catch (e) { console.error(e); } }); } } const EventBus = new SimpleEventBus(); const MemoryStore = { _store: {}, _tab: {}, get(key, defaultValue) { return key in MemoryStore._store ? MemoryStore._store[key] : defaultValue; }, set(key, value) { MemoryStore._store[key] = value; }, delete(key) { delete MemoryStore._store[key]; }, list() { return Object.keys(MemoryStore._store); }, async getTab(key) { return MemoryStore._tab[key]; }, async setTab(key, value) { MemoryStore._tab[key] = value; }, addChangeListener() { return null; }, removeChangeListener() { } }; // ===== 安全存储工具 ===== // === 同步加密工具函数(使用 CryptoJS) === const CryptoUtils = { // 获取或创建存储密钥(同步) getStorageKey() { let key = localStorage.getItem('_lxt_encryption_key'); if (!key) { // 生成随机密钥 const array = new Uint8Array(32); crypto.getRandomValues(array); key = Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); localStorage.setItem('_lxt_encryption_key', key); } return key; }, // AES 加密(同步) encrypt(data) { try { const key = this.getStorageKey(); const jsonStr = typeof data === 'string' ? data : JSON.stringify(data); return CryptoJS.AES.encrypt(jsonStr, key).toString(); } catch (e) { console.warn('[CryptoUtils] 加密失败:', e.message); return null; } }, // AES 解密(同步) decrypt(encryptedStr) { try { if (!encryptedStr || typeof encryptedStr !== 'string') return null; const key = this.getStorageKey(); const bytes = CryptoJS.AES.decrypt(encryptedStr, key); const decrypted = bytes.toString(CryptoJS.enc.Utf8); if (!decrypted) return null; try { return JSON.parse(decrypted); } catch { return decrypted; } } catch (e) { console.warn('[CryptoUtils] 解密失败:', e.message); return null; } } }; const SafeStorage = { PREFIX: '_lxt_', MAX_SIZE: 4 * 1024 * 1024, // 4MB 阈值 WARN_SIZE: 3.5 * 1024 * 1024, // 3.5MB 警告阈值 // 需要加密的敏感数据键名 ENCRYPTED_KEYS: [ '_ai_client_license_v1', '_ai_client_redeemed_v1', '_quiz_cache', '_local_user_id_v3', '_card_hash', '_device_fingerprint' ], // 获取当前存储使用量 getUsedSpace() { try { let total = 0; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(this.PREFIX)) { const value = localStorage.getItem(key); total += key.length + (value ? value.length : 0); } } return total; } catch (e) { return 0; } }, // 检查是否需要清理空间 needsCleanup() { return this.getUsedSpace() >= this.WARN_SIZE; }, // 清理旧数据(保留重要数据) cleanupOldData() { try { const keysToKeep = [ '_ai_client_license_v1', '_ai_client_redeemed_v1', '_quiz_cache', '_local_user_id_v3', '_lxt_encryption_key', '_backup_metadata' ]; const keys = Object.keys(localStorage).filter(k => k.startsWith(this.PREFIX) && !keysToKeep.includes(k) ); // 按修改时间排序(保留最新的) const sortedKeys = keys.sort((a, b) => { const aTime = localStorage.getItem(a + '_timestamp') || '0'; const bTime = localStorage.getItem(b + '_timestamp') || '0'; return parseInt(bTime) - parseInt(aTime); }); // 删除最旧的一半 const toDelete = sortedKeys.slice(Math.floor(sortedKeys.length / 2)); toDelete.forEach(key => { localStorage.removeItem(key); localStorage.removeItem(key + '_timestamp'); }); console.log(`[SafeStorage] 已清理 ${toDelete.length} 个旧数据项`); return toDelete.length; } catch (e) { console.warn('[SafeStorage] 清理失败:', e.message); return 0; } }, // 检查是否需要加密 _needsEncryption(key) { return this.ENCRYPTED_KEYS.some(k => key === k || key.startsWith(k)); }, // 同步 set 方法 set(key, value) { try { // 写入前检查容量 if (this.needsCleanup()) { this.cleanupOldData(); } let storedValue = value; const needsEncrypt = this._needsEncryption(key); if (needsEncrypt) { const encrypted = CryptoUtils.encrypt(value); if (encrypted !== null) { storedValue = { _encrypted: true, _data: encrypted }; } else { console.warn('[SafeStorage] 敏感数据加密失败,降级为明文存储'); storedValue = value; } } localStorage.setItem(this.PREFIX + key, JSON.stringify(storedValue)); // 记录修改时间 localStorage.setItem(this.PREFIX + key + '_timestamp', Date.now().toString()); return true; } catch (e) { // 写入失败时尝试清理后重试 this.cleanupOldData(); try { let storedValue = value; const needsEncrypt = this._needsEncryption(key); if (needsEncrypt) { const encrypted = CryptoUtils.encrypt(value); if (encrypted !== null) { storedValue = { _encrypted: true, _data: encrypted }; } else { storedValue = value; } } localStorage.setItem(this.PREFIX + key, JSON.stringify(storedValue)); localStorage.setItem(this.PREFIX + key + '_timestamp', Date.now().toString()); return true; } catch (e2) { console.warn('[SafeStorage] 保存失败:', key, e2.message); return false; } } }, // 同步 get 方法 get(key, defaultValue = null) { try { const item = localStorage.getItem(this.PREFIX + key); if (!item) return defaultValue; let parsed = JSON.parse(item); // 检查是否为加密数据 if (parsed && typeof parsed === 'object' && parsed._encrypted && parsed._data) { const decrypted = CryptoUtils.decrypt(parsed._data); return decrypted !== null ? decrypted : defaultValue; } return parsed; } catch (e) { console.warn('[SafeStorage] 读取失败:', key, e.message); return defaultValue; } }, remove(key) { try { localStorage.removeItem(this.PREFIX + key); localStorage.removeItem(this.PREFIX + key + '_timestamp'); return true; } catch (e) { return false; } }, clear() { try { const keys = Object.keys(localStorage).filter(k => k.startsWith(this.PREFIX)); keys.forEach(k => { localStorage.removeItem(k); localStorage.removeItem(k + '_timestamp'); }); return true; } catch (e) { return false; } }, // ===== 数据备份功能(异步) ===== // 导出所有数据(加密备份) async exportBackup() { try { const backup = { version: '1.0', timestamp: Date.now(), data: {} }; const keys = Object.keys(localStorage).filter(k => k.startsWith(this.PREFIX)); for (const key of keys) { const value = localStorage.getItem(key); backup.data[key] = value; } // 使用 CryptoJS 加密备份 const encryptedBackup = CryptoUtils.encrypt(backup); if (!encryptedBackup) { console.warn('[SafeStorage] 备份加密失败'); return null; } return { format: 'encrypted', data: encryptedBackup, hint: '此备份已加密,需要导入到同一浏览器才能恢复' }; } catch (e) { console.warn('[SafeStorage] 导出备份失败:', e.message); return null; } }, // 导入备份 async importBackup(backupData) { try { if (!backupData || !backupData.data) { console.warn('[SafeStorage] 无效的备份数据'); return false; } let backup; if (backupData.format === 'encrypted') { backup = CryptoUtils.decrypt(backupData.data); if (!backup) { console.warn('[SafeStorage] 备份解密失败'); return false; } } else { backup = backupData; } if (!backup.data) { console.warn('[SafeStorage] 备份数据为空'); return false; } // 清除现有数据 this.clear(); // 导入数据 Object.keys(backup.data).forEach(key => { localStorage.setItem(key, backup.data[key]); }); console.log(`[SafeStorage] 已成功导入 ${Object.keys(backup.data).length} 项数据`); return true; } catch (e) { console.warn('[SafeStorage] 导入备份失败:', e.message); return false; } }, // 生成分享码 async generateShareCode() { try { const backup = await this.exportBackup(); if (!backup) return null; const jsonStr = JSON.stringify(backup); return btoa(encodeURIComponent(jsonStr)); } catch (e) { console.warn('[SafeStorage] 生成分享码失败:', e.message); return null; } }, // 从分享码恢复 async restoreFromShareCode(shareCode) { try { const jsonStr = decodeURIComponent(atob(shareCode)); const backupData = JSON.parse(jsonStr); return this.importBackup(backupData); } catch (e) { console.warn('[SafeStorage] 从分享码恢复失败:', e.message); return false; } }, // 获取备份摘要 getBackupSummary() { try { const keys = Object.keys(localStorage).filter(k => k.startsWith(this.PREFIX)); const totalSize = this.getUsedSpace(); return { itemCount: keys.length, totalSize: totalSize, sizeFormatted: this._formatSize(totalSize), lastModified: localStorage.getItem(this.PREFIX + '_last_sync') || '未知' }; } catch (e) { return { itemCount: 0, totalSize: 0, sizeFormatted: '0 B', lastModified: '未知' }; } }, _formatSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } }; const GMStore = { get(key, defaultValue) { try { return GM_getValue(key, defaultValue); } catch (e) { console.warn('[GMStore] get 失败:', key, e.message); return defaultValue; } }, set(key, value) { try { GM_setValue(key, value); } catch (e) { console.warn('[GMStore] set 失败:', key, e.message); } }, delete(key) { try { GM_deleteValue(key); } catch (e) { console.warn('[GMStore] delete 失败:', key, e.message); } }, list() { try { return typeof GM_listValues === "function" ? GM_listValues() : []; } catch (e) { return []; } }, getTab(key) { return new Promise((resolve) => { try { GM_getTab((tab = {}) => resolve(tab[key])); } catch (e) { resolve(undefined); } }); }, setTab(key, value) { return new Promise((resolve) => { try { GM_getTab((tab = {}) => { Reflect.set(tab, key, value); GM_saveTab(tab); resolve(); }); } catch (e) { resolve(); } }); }, addChangeListener(key, listener) { try { if (typeof GM_addValueChangeListener === "function") return GM_addValueChangeListener(key, (_, pre, curr, remote) => listener(curr, pre, remote)); } catch (e) {} return null; }, removeChangeListener(id) { try { if (typeof id === "number" && typeof GM_removeValueChangeListener === "function") GM_removeValueChangeListener(id); } catch (e) {} } }; // ===== 自动化任务调度系统 ===== const AutoTaskScheduler = { // 任务状态 TaskStatus: { IDLE: 'idle', RUNNING: 'running', PAUSED: 'paused', STOPPED: 'stopped', COMPLETED: 'completed', ERROR: 'error' }, // 配置验证规则 _configValidators: { speed: (v) => typeof v === 'number' && v >= 0.25 && v <= 16, skipThreshold: (v) => typeof v === 'number' && v >= 0 && v <= 1, skipTimeout: (v) => typeof v === 'number' && v >= 5 && v <= 300, blackScreenRetry: (v) => typeof v === 'number' && v >= 0 && v <= 10, autoStudy: (v) => typeof v === 'boolean', autoAnswer: (v) => typeof v === 'boolean', skipQuiz: (v) => typeof v === 'boolean' }, // 当前状态 _status: 'idle', _tasks: [], _currentTask: null, _listeners: [], _progress: { totalSections: 0, completedSections: 0, currentChapter: 0, currentSection: 0 }, // 配置 _config: { autoStudy: true, // 自动刷课 autoAnswer: true, // 自动答题 skipQuiz: false, // 跳过答题 speed: 1.5, // 播放倍速 skipThreshold: 0.3, // 跳过阈值 skipTimeout: 30, // 答题超时秒数 blackScreenRetry: 3 // 黑屏重试次数 }, // 互斥锁 _lock: false, _observers: [], _keydownHandler: null, // 初始化 init(config = {}) { Object.assign(this._config, config); this._loadProgress(); this._setupKeyboardShortcuts(); this._setupSkipDetector(); console.log('[AutoTaskScheduler] 自动化任务调度器初始化完成'); }, // 销毁清理 destroy() { // 断开所有观察者 this._observers.forEach(obs => { try { obs.disconnect(); } catch (e) {} }); this._observers = []; // 移除键盘监听 if (this._keydownHandler) { document.removeEventListener('keydown', this._keydownHandler); this._keydownHandler = null; } }, // 设置配置 setConfig(key, value) { // 验证配置 if (this._configValidators[key] && !this._configValidators[key](value)) { console.warn(`[AutoTaskScheduler] 配置验证失败: ${key} = ${value}`); return false; } const oldValue = this._config[key]; this._config[key] = value; // 使用安全存储 SafeStorage.set('auto_task_config', this._config); console.log(`[AutoTaskScheduler] 配置已更新: ${key}: ${oldValue} -> ${value}`); return true; }, getConfig() { return { ...this._config }; }, // 获取状态 getStatus() { return { status: this._status, progress: { ...this._progress }, currentTask: this._currentTask, lock: this._lock }; }, // 开始任务 async start(type = 'all') { // 互斥锁防止竞态 if (this._lock) { console.warn('[AutoTaskScheduler] 任务正在执行中,请稍候'); return false; } const logStore = typeof useLogStore === 'function' ? useLogStore() : null; if (this._status === this.TaskStatus.RUNNING) { console.warn('[AutoTaskScheduler] 任务已在运行中'); return false; } this._lock = true; this._status = this.TaskStatus.RUNNING; this.emit('statusChange', this._status); if (logStore) { logStore.addLog('=== 自动化任务开始 ===', 'info'); } try { switch (type) { case 'study': await this._runStudyTask(); break; case 'answer': await this._runAnswerTask(); break; case 'all': default: await this._runAllTasks(); break; } this._status = this.TaskStatus.COMPLETED; if (logStore) { logStore.addLog('✓ 所有任务已完成', 'success'); } } catch (error) { console.error('[AutoTaskScheduler] 任务执行出错:', error); this._status = this.TaskStatus.ERROR; if (logStore) { logStore.addLog(`✗ 任务出错: ${error.message}`, 'error'); } } finally { this._lock = false; } this.emit('statusChange', this._status); return true; }, // 暂停任务 pause() { if (this._status !== this.TaskStatus.RUNNING) return false; this._status = this.TaskStatus.PAUSED; this._saveProgress(); this.emit('statusChange', this._status); const logStore = typeof useLogStore === 'function' ? useLogStore() : null; if (logStore) { logStore.addLog('⏸ 任务已暂停', 'warn'); } return true; }, // 继续任务 resume() { if (this._status !== this.TaskStatus.PAUSED) return false; this._status = this.TaskStatus.RUNNING; this.emit('statusChange', this._status); const logStore = typeof useLogStore === 'function' ? useLogStore() : null; if (logStore) { logStore.addLog('▶ 任务继续执行', 'info'); } return true; }, // 停止任务 stop() { this._status = this.TaskStatus.STOPPED; this._currentTask = null; this._saveProgress(); this.emit('statusChange', this._status); const logStore = typeof useLogStore === 'function' ? useLogStore() : null; if (logStore) { logStore.addLog('⏹ 任务已停止', 'warn'); } return true; }, // 运行全部任务(刷课+答题) async _runAllTasks() { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; if (this._config.autoStudy) { if (logStore) logStore.addLog('阶段1: 自动刷课', 'info'); await this._runStudyTask(); } if (this._config.autoAnswer && !this._config.skipQuiz) { if (logStore) logStore.addLog('阶段2: 自动答题', 'info'); await this._runAnswerTask(); } }, // 运行刷课任务 async _runStudyTask() { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; try { // 1. 检测视频播放器 const video = await this._waitForVideo(); if (!video) { if (logStore) logStore.addLog('未检测到视频播放器', 'warn'); return false; } if (logStore) logStore.addLog('检测到视频,开始播放', 'info'); // 2. 设置倍速 this._setVideoSpeed(video); // 3. 监听视频播放完成 await this._waitForVideoComplete(video); // 4. 检测并跳过答题环节 if (this._config.autoAnswer) { await this._handleVideoQuiz(); } // 5. 检测当前章节是否有未完成的章节测验 if (this._config.autoAnswer) { await this._handleChapterQuiz(); } // 5.5 验证章节是否已完全完成(打勾)再进入下一节 // 如果章节测验未完成,等待并重试,直到完成或超时 const maxWaitTime = 180000; // 最多等待180秒(增加等待时间确保答题完成) const checkInterval = 5000; // 每5秒检查一次 let waitedTime = 0; let chapterComplete = false; while (waitedTime < maxWaitTime && !chapterComplete) { // 检测章节列表中当前章节是否已标记为完成 // 根据Chrome DevTools分析,当前章节使用 posCatalog_active 类 const currentChapterItem = document.querySelector('.posCatalog_select.posCatalog_active, .posCatalog_select.current, .chapterItem.current, .section_item.current'); if (currentChapterItem) { chapterComplete = this._isChapterCompleted(currentChapterItem); if (!chapterComplete) { if (logStore) logStore.addLog(`等待章节完成...已等待${waitedTime/1000}秒`, 'info'); await new Promise(resolve => setTimeout(resolve, checkInterval)); waitedTime += checkInterval; // 再次尝试处理章节测验(确保答题完成) await this._handleChapterQuiz(); } else { if (logStore) logStore.addLog(`章节已完成,准备进入下一节`, 'success'); } } else { // 找不到当前章节标记,可能还在答题页面,检查是否还在答题 const quizPage = document.querySelector('.question-list, .exam-paper, .test-paper, .quiz-container, .workContent, [class*="TiMu"]'); if (quizPage) { if (logStore) logStore.addLog('仍在答题页面,尝试提交并等待...', 'info'); // 再次尝试提交和答题 await this._submitQuiz(); await new Promise(resolve => setTimeout(resolve, 3000)); await this._autoAnswer(); await new Promise(resolve => setTimeout(resolve, 5000)); } else { // 不在答题页面,也找不到当前章节,假设已完成 chapterComplete = true; } } } if (!chapterComplete && logStore) { logStore.addLog(`章节完成验证超时(${maxWaitTime/1000}秒),强制继续`, 'warn'); } // 6. 切换下一节 await this._nextSection(); return true; } catch (error) { console.error('[AutoTaskScheduler] 刷课任务出错:', error); throw error; } }, // 运行答题任务 async _runAnswerTask() { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; try { // 检测答题页面 const hasQuiz = await this._detectQuizPage(); if (!hasQuiz) { if (logStore) logStore.addLog('当前页面无答题内容', 'info'); return false; } if (logStore) logStore.addLog('检测到答题,开始自动答题', 'info'); // 执行自动答题 const result = await this._autoAnswer(); if (logStore) { if (result.success) { logStore.addLog(`答题完成: ${result.resolved}/${result.total}`, 'success'); } else { logStore.addLog(`答题失败: ${result.message}`, 'error'); } } return result; } catch (error) { console.error('[AutoTaskScheduler] 答题任务出错:', error); throw error; } }, // 等待视频元素 async _waitForVideo(timeout = 10000) { return new Promise((resolve) => { // 先检查现有视频 const existingVideo = document.querySelector('video'); if (existingVideo) { resolve(existingVideo); return; } let resolved = false; // 监听视频加载 const observer = new MutationObserver(() => { const video = document.querySelector('video'); if (video && !resolved) { resolved = true; observer.disconnect(); resolve(video); } }); observer.observe(document.body, { childList: true, subtree: true }); // 超时处理 const timeoutId = setTimeout(() => { if (!resolved) { resolved = true; observer.disconnect(); resolve(null); } }, timeout); // 提前清理 resolve = (v) => { if (!resolved) { resolved = true; clearTimeout(timeoutId); resolve(v); } }; }); }, // 设置视频倍速 _setVideoSpeed(video) { try { video.playbackRate = this._config.speed; // 尝试播放,处理 autoplay 限制 const playPromise = video.play(); if (playPromise !== undefined) { playPromise.catch(err => { // autoplay 被阻止,尝试静音播放 if (err.name === 'NotAllowedError') { video.muted = true; return video.play(); } console.warn('[AutoTaskScheduler] 视频播放失败:', err.message); }); } console.log(`[AutoTaskScheduler] 视频倍速已设置为 ${this._config.speed}x`); } catch (e) { console.warn('[AutoTaskScheduler] 设置倍速失败:', e.message); } }, // 等待视频播放完成 async _waitForVideoComplete(video) { return new Promise((resolve) => { if (video.ended || (video.duration && video.currentTime >= video.duration - 0.5)) { resolve(); return; } const checkComplete = () => { if (this._status === this.TaskStatus.STOPPED) { resolve(); return; } if (video.ended || (video.duration && video.currentTime >= video.duration - 0.5)) { console.log('[AutoTaskScheduler] 视频播放完成'); resolve(); return; } setTimeout(checkComplete, 1000); }; // 监听ended事件 video.addEventListener('ended', () => { console.log('[AutoTaskScheduler] 视频 ended 事件触发'); resolve(); }, { once: true }); // 定时检查 checkComplete(); }); }, // 检测并处理视频中的答题 async _handleVideoQuiz() { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; // 检测答题弹窗 const quizDialog = document.querySelector('.quiz-popup, #quiz-dialog, .answer-modal, [data-type="quiz"]'); if (quizDialog) { if (logStore) logStore.addLog('检测到视频答题弹窗', 'warn'); if (this._config.skipQuiz) { // 尝试跳过答题 const skipBtn = document.querySelector('.skip-btn, [data-action="skip"], .continue-btn'); if (skipBtn) { skipBtn.click(); if (logStore) logStore.addLog('已自动跳过答题', 'info'); return; } } // 如果不能跳过,等待答题完成 await this._autoAnswer(); } }, // 等待答题页面加载完成(支持多层iframe嵌套模式) // 学习通页面结构:主页面 -> knowledge/cards iframe -> work/index.html iframe -> doHomeWorkNew iframe async _waitForQuizPage(timeout = 15000) { const startTime = Date.now(); const checkInterval = 500; const originalUrl = window.location.href; const quizSelectors = [ '.TiMu', '[class*="TiMu"]', '.Cy_TItle', '.Zy_TItle', '.newZy_TItle', '.fontLabel', '.workContent', '[class*="workContent"]', '.examContent', '[class*="examContent"]', '.testContent', '[class*="testContent"]', '.ans-quiz', '[class*="ans-quiz"]', '.ans-work', '[class*="ans-work"]', '.questionLi', '[class*="questionLi"]', '.question-list', '.exam-paper', '.test-paper', '.answerBg', '[class*="answerBg"]', '.answer_p', '.question-item', '[data-type="question"]', '[class*="timu"]', '.ans-job', '.subjectList', '.subjectDet', '.singleQuesId', '[id^="answertype"]', '.Py-mian1', '.nodeLab' ]; const hasQuizInDoc = (doc) => { try { for (const selector of quizSelectors) { const el = doc.querySelector(selector); if (el) return selector; } const text = String(doc.body?.innerText || doc.body?.textContent || ''); if (/我的答案|单选题|多选题|判断题|填空题|提交答案|本题得分/.test(text)) return 'text:quiz'; } catch(e) {} return ''; }; const mainIframe = document.querySelector('iframe[src*="knowledge"], iframe[src*="cards"], iframe.frameContent'); const originalIframeSrc = mainIframe ? (mainIframe.src || mainIframe.getAttribute('src') || '') : ''; const initialMainQuiz = hasQuizInDoc(document); if (initialMainQuiz) { logStore.addLog(`当前页面已检测到题目: ${initialMainQuiz}`, "success"); return true; } while (Date.now() - startTime < timeout) { // 检测1:URL是否发生变化(页面跳转模式) if (window.location.href !== originalUrl) { const newUrl = window.location.href; if (newUrl.includes('work') || newUrl.includes('exam') || newUrl.includes('test') || newUrl.includes('quiz') || newUrl.includes('ans-quiz')) { logStore.addLog(`检测到URL跳转到答题页面`, "success"); await new Promise(resolve => setTimeout(resolve, 1000)); return true; } } // 检测2:主iframe src 是否发生变化(num参数变化表示切换到测验) if (mainIframe) { const currentSrc = mainIframe.src || mainIframe.getAttribute('src') || ''; // 检测2a:iframe src 包含答题关键词(work/exam/test/ans-quiz/zy/ks) if (currentSrc && (currentSrc.includes('work') || currentSrc.includes('exam') || currentSrc.includes('test') || currentSrc.includes('ans-quiz') || currentSrc.includes('zy') || currentSrc.includes('ks'))) { logStore.addLog(`检测到iframe包含答题关键词`, "success"); await new Promise(resolve => setTimeout(resolve, 1500)); return true; } // 检测2b:iframe src 发生变化(AJAX加载模式) if (currentSrc && currentSrc !== originalIframeSrc) { logStore.addLog(`检测到iframe src变化(AJAX加载答题内容)`, "success"); await new Promise(resolve => setTimeout(resolve, 1500)); return true; } // 检测2c:尝试访问iframe内容(同源情况下) try { const iframeDoc = mainIframe.contentDocument || mainIframe.contentWindow?.document; if (iframeDoc) { const iframeQuizSelector = hasQuizInDoc(iframeDoc); if (iframeQuizSelector) { logStore.addLog(`检测到iframe中的答题元素: ${iframeQuizSelector}`, "success"); await new Promise(resolve => setTimeout(resolve, 500)); return true; } // 检测2d:检查内层iframe(ananas/modules/work) const innerIframe = iframeDoc.querySelector('iframe[src*="ananas"], iframe[src*="work"], iframe[src*="doHomeWorkNew"], iframe[src*="doExam"]'); if (innerIframe) { const innerSrc = innerIframe.src || innerIframe.getAttribute('src') || ''; if (innerSrc && (innerSrc.includes('doHomeWorkNew') || innerSrc.includes('doExam') || innerSrc.includes('work') || innerSrc.includes('exam'))) { logStore.addLog(`检测到内层答题iframe`, "success"); await new Promise(resolve => setTimeout(resolve, 2000)); // 深层iframe需要更多加载时间 return true; } } } } catch(e) { // 跨域iframe,忽略 } } const mainQuizSelector = hasQuizInDoc(document); if (mainQuizSelector) { logStore.addLog(`主页面检测到答题元素: ${mainQuizSelector}`, "success"); return true; } // 等待后重试 await new Promise(resolve => setTimeout(resolve, checkInterval)); } return false; }, // 兜底:尝试跳转到答题URL async _fallbackToQuizUrl() { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; const currentUrl = window.location.href; const urlParams = new URLSearchParams(currentUrl.split('?')[1] || ''); const courseId = urlParams.get('courseId') || urlParams.get('classId'); const chapterId = urlParams.get('chapterId'); const clazzId = urlParams.get('clazzid') || urlParams.get('clazzId'); const cpi = urlParams.get('cpi'); const enc = urlParams.get('enc'); const baseUrl = currentUrl.split('?')[0]; const possibleUrls = []; // 构造带完整参数的答题URL if (courseId && chapterId) { possibleUrls.push(`${baseUrl}?courseId=${courseId}&chapterId=${chapterId}&clazzid=${clazzId || ''}&cpi=${cpi || ''}&enc=${enc || ''}&type=work`); possibleUrls.push(`${baseUrl}?courseId=${courseId}&chapterId=${chapterId}&clazzid=${clazzId || ''}&cpi=${cpi || ''}&enc=${enc || ''}&type=exam`); possibleUrls.push(`${baseUrl}?courseId=${courseId}&chapterId=${chapterId}&clazzid=${clazzId || ''}&cpi=${cpi || ''}&enc=${enc || ''}&type=test`); } possibleUrls.push(`${baseUrl}?type=work`); possibleUrls.push(`${baseUrl}?type=exam`); possibleUrls.push(`${baseUrl}?type=test`); for (const url of possibleUrls) { try { logStore.addLog(`尝试跳转到: ${url}`, "info"); window.location.href = url; return; } catch (e) {} } logStore.addLog(`所有跳转尝试失败,请手动进入`, "danger"); }, // 检测并处理章节测验(确保栏目打勾) // 【基于MCP实测优化】学习通页面结构:主页面有"视频"和"章节测验"选项卡,点击"章节测验"后iframe的num参数变化加载答题内容 async _handleChapterQuiz() { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; // === 策略1:点击"章节测验"选项卡(学习通标准方式,MCP实测验证) === const quizTab = document.querySelector('option[value="章节测验"]'); if (quizTab) { // 检查当前章节是否有未完成任务 const mainIframe = document.querySelector('iframe[src*="knowledge"], iframe[src*="cards"]'); if (mainIframe) { const currentSrc = mainIframe.src || mainIframe.getAttribute('src') || ''; const taskStatusEl = document.querySelector('[class*="task-status"], [class*="taskStatus"]'); const taskStatusText = taskStatusEl?.textContent?.trim() || ''; // 如果当前在视频页面(num=0),且状态不是"任务点已完成",则进入答题 const isOnVideoPage = currentSrc.includes('num=0') || !currentSrc.includes('num='); const isTaskCompleted = taskStatusText.includes('已完成') || taskStatusText.includes('100%'); // 检查是否有"任务点未完成"标记 const hasUnfinishedTask = document.querySelector('[class*="unfinished"], [class*="Unfinished"]') !== null; if (isOnVideoPage && (!isTaskCompleted || hasUnfinishedTask)) { if (logStore) logStore.addLog('发现未完成任务,点击章节测验选项卡...', 'info'); quizTab.click(); // 等待答题页面加载(多层iframe检测) if (logStore) logStore.addLog('等待答题页面加载...', 'info'); const quizLoaded = await this._waitForQuizPage(20000); if (quizLoaded) { if (logStore) logStore.addLog('答题页面加载成功,开始自动答题', 'success'); await this._autoAnswer(); await new Promise(resolve => setTimeout(resolve, 2000)); await this._submitQuiz(); await new Promise(resolve => setTimeout(resolve, 5000)); return; } else { if (logStore) logStore.addLog('答题页面加载超时', 'warning'); } } } } // === 策略2:通过URL参数判断当前是否在答题页面 === const currentUrl = window.location.href; if (currentUrl.includes('doHomeWorkNew') || currentUrl.includes('doExam') || currentUrl.includes('doTest')) { if (logStore) logStore.addLog('当前已在答题页面,开始自动答题', 'info'); await this._autoAnswer(); await new Promise(resolve => setTimeout(resolve, 2000)); await this._submitQuiz(); return; } // === 策略3:检测iframe中的答题内容 === try { const mainIframe = document.querySelector('iframe[src*="knowledge"], iframe[src*="cards"]'); if (mainIframe) { const iframeDoc = mainIframe.contentDocument || mainIframe.contentWindow?.document; if (iframeDoc) { // 检查是否已经有答题内容 const hasQuizContent = iframeDoc.querySelector('.TiMu, .subjectList, .subjectDet, .workContent, .examContent, .questionLi'); if (hasQuizContent) { if (logStore) logStore.addLog('iframe中已有答题内容,开始自动答题', 'info'); await this._autoAnswer(); await new Promise(resolve => setTimeout(resolve, 2000)); await this._submitQuiz(); return; } // 检查内层iframe是否有答题页面 const innerIframe = iframeDoc.querySelector('iframe[src*="doHomeWorkNew"], iframe[src*="doExam"]'); if (innerIframe) { if (logStore) logStore.addLog('检测到内层答题iframe,等待加载后答题', 'info'); await new Promise(resolve => setTimeout(resolve, 3000)); await this._autoAnswer(); await new Promise(resolve => setTimeout(resolve, 2000)); await this._submitQuiz(); return; } } } } catch(e) { // 跨域iframe,忽略 } // === 策略4:原有逻辑(兼容其他页面结构) === const chapterQuizSelectors = [ '.posCatalog_select:has(.icon_Quiz)', '.posCatalog_select:has(.icon_Exam)', '.ans-job-icon:not([aria-label*="已完成"])', '.ans-job-icon:not([title*="已完成"])', '[class*="quiz"]:not([class*="complete"])', '[class*="exam"]:not([class*="complete"])', '[class*="work"]:not([class*="complete"])', ]; for (const selector of chapterQuizSelectors) { try { const quizElement = document.querySelector(selector); if (quizElement) { const isCompleted = quizElement.classList.contains('completed') || quizElement.querySelector('.icon_Completed') || quizElement.getAttribute('aria-label')?.includes('已完成') || quizElement.textContent?.includes('已完成'); if (!isCompleted) { if (logStore) logStore.addLog('检测到未完成的章节测验,正在进入答题', 'warning'); // 尝试点击 quizElement.click(); await new Promise(resolve => setTimeout(resolve, 3000)); const quizLoaded = await this._waitForQuizPage(15000); if (quizLoaded) { if (logStore) logStore.addLog('答题页面加载成功', 'success'); await this._autoAnswer(); await new Promise(resolve => setTimeout(resolve, 2000)); await this._submitQuiz(); return; } } } } catch (e) {} } }, // 检查章节是否已完成(打勾) _isChapterCompleted(element) { if (!element) return false; // 检查元素本身的完成状态 const hasCompletedClass = element.classList.contains('completed') || element.classList.contains('icon_Completed'); // 检查子元素中的完成标记(根据MCP分析,使用.icon_Completed表示已完成) const hasCompletedChild = element.querySelector('.icon_Completed') || element.querySelector('[class*="Completed"]') || element.querySelector('[class*="complete"]'); // 检查图标状态(绿色对勾) const hasCheckIcon = element.querySelector('.icon-check, .icon-check-circle, .fa-check, .fa-check-circle') || element.querySelector('[class*="icon-check"]') || element.querySelector('[class*="icon_yes"]'); // 检查属性中的完成标记 const hasCompletedAttr = element.getAttribute('aria-label')?.includes('已完成') || element.getAttribute('title')?.includes('已完成') || element.getAttribute('data-status')?.includes('complete') || element.getAttribute('data-status')?.includes('finished'); // 检查文本内容 const hasCompletedText = element.textContent?.includes('已完成') || element.textContent?.includes('100%') || element.textContent?.includes('满分'); // 检查样式(绿色表示完成) const hasGreenColor = window.getComputedStyle(element).color === 'rgb(40, 167, 69)' || // bootstrap success green window.getComputedStyle(element).color === '#28a745'; // 检查是否存在未完成标记(根据MCP Chrome DevTools分析,使用.catalog_points_yi表示待完成任务点) // .catalog_points_yi 是学习通页面中表示"待完成任务点"的黄色徽章 const hasUncompletedBadge = element.querySelector('.catalog_points_yi, .icon_yellow, .orangeIcon, [class*="num"]') !== null; // 如果有未完成标记,则视为未完成 if (hasUncompletedBadge) { return false; } // 任何一个完成条件满足即为完成 return hasCompletedClass || hasCompletedChild || hasCheckIcon || hasCompletedAttr || hasCompletedText || hasGreenColor; }, // 检测答题页面 async _detectQuizPage() { // 根据MCP分析,学习通使用以下选择器检测答题页面 return !!document.querySelector('.question-list, .exam-paper, .test-paper, .workContent, [data-type="question"], [class*="TiMu"], [class*="timu"]'); }, // 自动答题核心(增强版:必须答题+正确率验证+重试) async _autoAnswer() { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; const maxRetries = 3; // 最大重试次数 let retryCount = 0; while (retryCount <= maxRetries) { // 获取所有题目 const questions = this._getQuestions(); if (!questions || questions.length === 0) { return { success: false, message: '未找到题目' }; } if (logStore) logStore.addLog(`第${retryCount + 1}轮答题,共${questions.length}题`, 'info'); let resolved = 0; const total = questions.length; for (let i = 0; i < questions.length; i++) { if (this._status === this.TaskStatus.STOPPED) { break; } const question = questions[i]; if (logStore) { logStore.addLog(`答题中: ${i + 1}/${total}`, 'info'); } // 使用高级答题引擎(async) const result = await this._solveQuestion(question); if (result) { resolved++; } // 随机延迟 await this._randomDelay(200, 800); } // 自动提交 await this._submitQuiz(); // 等待结果并验证正确率 await new Promise(resolve => setTimeout(resolve, 3000)); const accuracy = this._checkAnswerAccuracy(); if (logStore) logStore.addLog(`答题统计: 总计${total}道 | 已答${resolved}道 | 正确率${accuracy}%`, 'info'); // 正确率>=80%或已答完所有题,认为成功 if (accuracy >= 80 || resolved === total) { if (logStore) logStore.addLog(`答题完成,正确率${accuracy}%`, 'success'); return { success: true, resolved, total, accuracy }; } // 正确率太低,重新答题 retryCount++; if (retryCount <= maxRetries) { if (logStore) logStore.addLog(`正确率${accuracy}%过低,第${retryCount}次重试...`, 'warning'); // 返回答题页面 await this._returnToQuiz(); await new Promise(resolve => setTimeout(resolve, 2000)); } } if (logStore) logStore.addLog(`重试${maxRetries}次后正确率仍不达标`, 'warning'); return { success: true, resolved: 0, total: questions.length, accuracy: 0 }; }, // 获取题目列表 _getQuestions() { // 根据MCP Chrome DevTools分析,学习通使用以下选择器获取题目 const selectors = [ // 学习通标准选择器(优先级从高到低) '[class*="TiMu"]', // TiMu类(学习通标准题目容器) '[class*="timu"]', // 小写变体 '.questionLi', // 题目列表项 '.question-item', // 通用题目项 '.question', // 通用题目 '[data-type="question"]', // data属性 '.exam-question', // 考试题目 '.test-question', // 测试题目 '.ans-job', // 作业答题容器 'li[data-id]', // 带data-id的列表项 '.questionList li', // 题目列表中的li '.answerList li' // 答案列表中的li ]; for (const selector of selectors) { try { const questions = document.querySelectorAll(selector); if (questions.length > 0) { return Array.from(questions); } } catch (e) { // 忽略无效选择器 } } return []; }, // 鲁棒的选项点击辅助函数(支持多种DOM结构) _clickOption(optionElement) { if (!optionElement) return false; const el = optionElement; // 策略1:如果元素本身就是label,直接点击 if (el.tagName === 'LABEL') { el.click(); return true; } // 策略2:查找最近的label父元素 const parentLabel = el.closest('label'); if (parentLabel) { parentLabel.click(); return true; } // 策略3:查找内部的input(radio/checkbox)并点击 const input = el.querySelector('input[type="radio"], input[type="checkbox"]'); if (input) { input.checked = true; input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('input', { bubbles: true })); input.click(); return true; } // 策略4:查找内部的span/div等可点击元素 const clickable = el.querySelector('.radioId, .checkboxId, [role="radio"], [role="checkbox"]'); if (clickable) { clickable.click(); return true; } // 策略5:直接点击元素本身 el.click(); el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); return true; }, // 解答题目(优化版:优先查询本地题库) async _solveQuestion(questionElement) { try { // 提取题干(使用学习通标准选择器) const questionEl = questionElement.querySelector( '.questionName, .question-title, .question-text, .title, [class*="questionName"], ' + '[class*="questionTitle"], [class*="quesTitle"], [class*="stem"]' ); const rawQuestionText = questionEl?.textContent || ''; // 清洗题干:去除题号、标签等噪声(对齐 jinmu.js) const questionText = rawQuestionText .replace(/^[\d]+[\.\、\s]*/, '') // 去除开头题号如 "1. "、"2、" .replace(/^\s*[【\[].*?[】\]]\s*/, '') // 去除题型标签如 【单选题】 .replace(/ /g, ' ') .replace(/\s+/g, ' ') .trim(); // 提取选项(精准选择器:只获取答题区域的选项) // 优先级:1. 包含input的label 2. .answerList/.answer下的label/li 3. 通用选项类 let optionElements = []; // 方法1:获取包含 input[type="radio"] 或 input[type="checkbox"] 的 label(最精准) const inputLabels = Array.from(questionElement.querySelectorAll('label')); const realOptionLabels = inputLabels.filter(lbl => lbl.querySelector('input[type="radio"], input[type="checkbox"]') ); if (realOptionLabels.length >= 2) { optionElements = realOptionLabels; } else { // 方法2:从 .answerList, .answer, .options 容器中获取 const answerContainers = questionElement.querySelectorAll('.answerList, .answer, .options, .option-list, .answer-list'); for (const container of answerContainers) { const items = Array.from(container.querySelectorAll('label, li')); if (items.length >= 2) { optionElements = items; break; } } } // 方法3:兜底 - 使用通用选择器 if (optionElements.length < 2) { optionElements = Array.from(questionElement.querySelectorAll( '.answer-option, [class*="answerItem"], [class*="answeritem"], [class*="option"]' )); } // 提取选项:保留DOM元素引用,同时提取文本 const options = optionElements.map(opt => { let text = opt.textContent || opt.innerText || ''; // 只去除开头的选项标记(如 "A. "、"B、"、"A)"),保留内容 text = text.replace(/^\s*[A-Ha-h][\.\、\)\s]\s*/, '').trim(); return { element: opt, // 保留DOM元素用于点击 text: text // 纯文本用于匹配 }; }).filter(opt => opt.text.length > 0); if (!questionText || options.length === 0) { return false; } const logStore = typeof useLogStore === 'function' ? useLogStore() : null; const cleanTitle = questionText.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); // ===== 优化1:优先检测题型 ===== const questionType = detectQuestionType(questionElement, options, optionElements); const isMultiSelect = questionType === "1"; // 多选题 const isJudgement = questionType === "3"; // 判断题 if (logStore) { logStore.addLog(`[答题] 检测题型: ${questionType === "1" ? '多选题' : questionType === "3" ? '判断题' : questionType === "2" ? '填空题' : '单选题'}`, 'info'); } // ========== 阶段1:查询本地题库缓存(最高优先级,同时查询两个题库)========== let knownAnswers = []; let bestAnswerFromDb = null; // 清理污染的 local_db 数据(旧版本 AI 答案置信度硬编码 0.85 导致大量错误答案被保存) // 策略:完全重置 local_db,只保留远程题库验证过的答案 try { const localDb = Store.get('local_db', {}); if (localDb && typeof localDb === 'object' && !localDb._v3_reset) { // 完全重置:清空所有旧数据,只保留标记 Store.set('local_db', { _v3_reset: true, _reset_date: new Date().toISOString() }); if (logStore) { const count = Object.keys(localDb).filter(k => !k.startsWith('_')).length; logStore.addLog(`[题库] 已重置 local_db(清除 ${count} 条旧数据,避免 AI 错误答案污染)`, 'info'); } } } catch(e) {} try { // 1.0 查询 local_db (Store.get('local_db')) - 这个是AI答题使用的题库 try { const localDb = Store.get('local_db', {}); if (localDb && typeof localDb === 'object') { // 尝试精确匹配 if (localDb[cleanTitle]) { bestAnswerFromDb = localDb[cleanTitle]; if (logStore) logStore.addLog(`[答题] LocalDB精确匹配: "${cleanTitle.substring(0, 30)}..."`, 'success'); } else { // 尝试模糊匹配 - 使用纯 bigram 匹配(对齐 jinmu.js) let bestLocalMatch = null; let bestLocalScore = 0; for (const [key, value] of Object.entries(localDb)) { const cleanKey = key.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const score = AdvancedAnswerEngine.bigramSimilarity(cleanTitle, cleanKey); if (score > bestLocalScore && score > 0.6) { bestLocalScore = score; bestLocalMatch = value; } } if (bestLocalMatch) { bestAnswerFromDb = bestLocalMatch; if (logStore) logStore.addLog(`[答题] LocalDB相似匹配(${Math.round(bestLocalScore*100)}%): "${cleanTitle.substring(0, 30)}..."`, 'info'); } } } } catch (e) { // 忽略local_db查询错误 } // 1.1 查询 _quiz_cache - 这个是另一个题库 const cache = JSON.parse(localStorage.getItem('_quiz_cache') || '[]'); // 精确匹配 const exactMatch = cache.find(q => { const qTitle = (q.title || '').replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); return qTitle === cleanTitle; }); if (exactMatch && exactMatch.answers && exactMatch.answers.length > 0) { knownAnswers = exactMatch.answers; if (logStore) logStore.addLog(`[答题] QuizCache精确匹配: "${cleanTitle.substring(0, 30)}..."`, 'success'); } else { // 高相似度匹配 - 使用纯 bigram 匹配(对齐 jinmu.js) let bestCacheMatch = null; let bestCacheScore = 0; for (const q of cache) { const qTitle = (q.title || '').replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const score = AdvancedAnswerEngine.bigramSimilarity(cleanTitle, qTitle); if (score > bestCacheScore && score > 0.6) { bestCacheScore = score; bestCacheMatch = q; } } if (bestCacheMatch && bestCacheMatch.answers && bestCacheMatch.answers.length > 0) { knownAnswers = bestCacheMatch.answers; if (logStore) logStore.addLog(`[答题] QuizCache相似匹配(${Math.round(bestCacheScore*100)}%): "${cleanTitle.substring(0, 30)}..."`, 'info'); } } } catch (e) { // 忽略缓存查询错误 } // ========== 阶段2:远程搜索多题库(本地未找到答案时)========== let remoteAnswers = []; let remoteSource = ''; if (knownAnswers.length === 0 && !bestAnswerFromDb) { if (logStore) logStore.addLog(`[答题] 本地未找到答案,开始远程搜索...`, 'info'); try { // 清洗题干后再发送远程搜索(对齐 jinmu.js) const cleanTitleForApi = questionText .replace(/<[^>]*>/g, '') .replace(/ /g, ' ') .replace(/[\?\?\.\。\,\,\;\;\:\:]/g, '') .replace(/\s+/g, ' ') .trim(); // 使用 fetchFromMultipleApis 并行查询多个题库(60秒超时) const remoteResult = await fetchFromMultipleApis( { title: cleanTitleForApi, type: questionType, optionsText: options.map(o => o.text) }, ['edusearch', 'lyck6', 'wkapi', 'yanxi', 'yizhi', 'aidati'] ); if (remoteResult && remoteResult.data && remoteResult.data.answer) { remoteAnswers = Array.isArray(remoteResult.data.answer) ? remoteResult.data.answer : [remoteResult.data.answer]; remoteSource = remoteResult.data.source || 'remote'; const confidence = remoteResult.data.confidence || 85; if (logStore) logStore.addLog(`[答题] 远程搜索成功: ${remoteAnswers.join(', ')} (来源: ${remoteSource}, 置信度: ${confidence}%)`, 'success'); // 将远程答案添加到 knownAnswers knownAnswers = remoteAnswers; } else { if (logStore) logStore.addLog(`[答题] 远程搜索未找到答案`, 'info'); } } catch (e) { if (logStore) logStore.addLog(`[答题] 远程搜索失败: ${e.message}`, 'warning'); } } // ========== 阶段3:根据题型使用高级答题引擎(完全对齐 jinmu.js createDefaultQuestionResolver)========== let result = null; let multiResult = null; // 合并所有答案源 const allKnownAnswers = [...knownAnswers]; if (bestAnswerFromDb && !allKnownAnswers.includes(bestAnswerFromDb)) { allKnownAnswers.push(bestAnswerFromDb); } if (isMultiSelect) { multiResult = AdvancedAnswerEngine.smartAnswerMulti(questionText, options, allKnownAnswers); } else if (isJudgement) { result = AdvancedAnswerEngine.smartAnswerJudge(questionText, options, allKnownAnswers); } else { result = AdvancedAnswerEngine.smartAnswer(questionText, options, allKnownAnswers); } // ========== 阶段4:执行答题 ========== if (isMultiSelect && multiResult && multiResult.options && multiResult.options.length > 0) { if (logStore) logStore.addLog(`[答题] 多选题选择 ${multiResult.options.length} 个选项 (策略: ${multiResult.strategy})`, 'info'); for (const opt of multiResult.options) { const optEl = opt.option || opt; // 兼容处理:optEl 可能是 DOM 元素或 {element, text} 对象 const domEl = optEl.element || optEl; if (this._clickOption(domEl)) { const text = optEl.text || domEl.textContent || ''; if (logStore) logStore.addLog(`[答题] 已选择: ${text.trim().substring(0, 30)}`, 'success'); } } return true; } else if (result && result.option) { // 兼容处理:result.option 可能是 DOM 元素或 {element, text} 对象 const domEl = result.option.element || result.option; if (this._clickOption(domEl)) { if (logStore) logStore.addLog(`[答题] ${isJudgement ? '判断题' : '单选题'}选择 (策略: ${result.strategy}, 置信度: ${result.confidence}%)`, 'success'); return true; } } // ========== 阶段5:兜底匹配(必须答题)========== if (logStore) logStore.addLog(`[答题] 引擎未返回结果,尝试兜底`, 'warning'); const fallbackAnswers = allKnownAnswers.length > 0 ? allKnownAnswers : (remoteAnswers.length > 0 ? remoteAnswers : []); if (fallbackAnswers.length > 0) { const fallback = isMultiSelect ? AdvancedAnswerEngine.smartAnswerMulti(questionText, options, fallbackAnswers) : AdvancedAnswerEngine.matchKnownAnswers(fallbackAnswers, options); if (fallback) { if (isMultiSelect && fallback.options) { for (const opt of fallback.options) { const optEl = opt.option || opt; const domEl = optEl.element || optEl; this._clickOption(domEl); } } else if (fallback.option) { const domEl = fallback.option.element || fallback.option; this._clickOption(domEl); } if (logStore) logStore.addLog(`[答题] 兜底成功 (策略: ${fallback.strategy || '未知'})`, 'warning'); return true; } } // 终极兜底:选第一个选项 if (logStore) logStore.addLog(`[答题] 所有匹配均失败,选择第一个选项`, 'warning'); if (optionElements.length > 0) { this._clickOption(optionElements[0]); return true; } return false; } catch (e) { console.error('[AutoTaskScheduler] 解答题目出错:', e); return false; } }, // 提交答题 async _submitQuiz() { const submitBtn = document.querySelector('.submit-btn, #submit-btn, [data-action="submit"], .Btn_tj, [class*="submit"]'); if (submitBtn) { submitBtn.click(); // 确认提交 await new Promise(resolve => setTimeout(resolve, 500)); const confirmBtn = document.querySelector('.confirm-btn, .confirm-submit, .btn-primary, [class*="confirm"]'); if (confirmBtn) { await this._randomDelay(500, 1000); confirmBtn.click(); } } }, // 检查答题正确率 _checkAnswerAccuracy() { try { // 学习通正确率显示选择器 const accuracySelectors = [ '.correct-rate', '.accuracy', '[class*="accuracy"]', '[class*="correctRate"]', '[class*="rate"]', '.score-detail', '.result-info' ]; for (const selector of accuracySelectors) { const element = document.querySelector(selector); if (element) { const text = element.textContent || ''; const match = text.match(/(\d+)%/); if (match) { return parseInt(match[1]); } // 匹配分数格式 80/100 const scoreMatch = text.match(/(\d+)\s*\/\s*(\d+)/); if (scoreMatch) { return Math.round(parseInt(scoreMatch[1]) / parseInt(scoreMatch[2]) * 100); } } } // 检查是否有正确答案标记 const correctElements = document.querySelectorAll('.correct, .right, [class*="correct"], .answer-correct'); const wrongElements = document.querySelectorAll('.wrong, .error, [class*="wrong"], .answer-wrong'); if (correctElements.length > 0 || wrongElements.length > 0) { const total = correctElements.length + wrongElements.length; return Math.round(correctElements.length / total * 100); } // 默认返回100(假设都答对了) return 100; } catch (e) { return 100; // 出错时假设正确 } }, // 返回答题页面 async _returnToQuiz() { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; // 查找返回答题按钮 const backSelectors = [ '.back-btn', '.retest-btn', '[class*="retest"]', '[class*="retry"]', '[class*="again"]', '.btn-retry', 'a[href*="exam"]', 'a[href*="quiz"]', 'a[href*="work"]' ]; for (const selector of backSelectors) { const btn = document.querySelector(selector); if (btn) { btn.click(); if (logStore) logStore.addLog('点击返回答题按钮', 'info'); return; } } // 如果没找到按钮,尝试后退 if (logStore) logStore.addLog('未找到返回答题按钮,尝试浏览器后退', 'info'); window.history.back(); }, // 切换下一节 async _nextSection() { const logStore = typeof useLogStore === 'function' ? useLogStore() : null; // 查找下一节按钮 const nextBtn = document.querySelector('.next-btn, .next-section, [data-action="next"]'); if (nextBtn) { nextBtn.click(); if (logStore) logStore.addLog('已切换到下一节', 'info'); // 更新进度 this._progress.completedSections++; this._saveProgress(); // 等待页面加载 await this._randomDelay(1000, 2000); return true; } return false; }, // 设置键盘快捷键 (Ctrl+Alt 避免与常用软件冲突) _setupKeyboardShortcuts() { this._keydownHandler = (e) => { if (e.ctrlKey && e.altKey) { switch (e.key) { case '1': // Ctrl+Alt+1 - 仅刷课 e.preventDefault(); this.start('study'); break; case '2': // Ctrl+Alt+2 - 仅答题 e.preventDefault(); this.start('answer'); break; case '3': // Ctrl+Alt+3 - 一键完成 e.preventDefault(); this.start('all'); break; case 'p': // Ctrl+Alt+P - 暂停/继续 case 'P': e.preventDefault(); if (this._status === this.TaskStatus.RUNNING) { this.pause(); } else if (this._status === this.TaskStatus.PAUSED) { this.resume(); } break; case 's': // Ctrl+Alt+S - 停止 case 'S': e.preventDefault(); this.stop(); break; } } }; document.addEventListener('keydown', this._keydownHandler); }, // 设置跳过检测器 _setupSkipDetector() { // 监听答题弹窗 const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { // 检测答题弹窗 if (node.matches && ( node.matches('.quiz-popup, #quiz-dialog, .answer-modal') || node.querySelector('.quiz-popup, #quiz-dialog, .answer-modal') )) { if (this._config.skipQuiz && this._config.autoStudy) { const skipBtn = node.querySelector('.skip-btn, .continue-btn, [data-action="skip"]'); if (skipBtn) { console.log('[AutoTaskScheduler] 自动跳过答题弹窗'); setTimeout(() => skipBtn.click(), 500); } } } // 检测确认对话框 if (node.matches && ( node.matches('.confirm-dialog, .confirm-modal') || node.querySelector('.confirm-dialog, .confirm-modal') )) { const confirmBtn = node.querySelector('.confirm-btn, .ok-btn, [data-action="confirm"]'); if (confirmBtn) { console.log('[AutoTaskScheduler] 自动确认对话框'); setTimeout(() => confirmBtn.click(), 500); } } } } } }); observer.observe(document.body, { childList: true, subtree: true }); this._observers.push(observer); }, // 随机延迟 _randomDelay(min, max) { const delay = Math.floor(Math.random() * (max - min + 1)) + min; return new Promise(resolve => setTimeout(resolve, delay)); }, // 保存进度 _saveProgress() { SafeStorage.set('study_progress', { timestamp: Date.now(), progress: this._progress }); }, // 加载进度 _loadProgress() { const saved = SafeStorage.get('study_progress'); if (saved && saved.progress) { // 验证是否过期(24小时) if (Date.now() - saved.timestamp < 86400000) { this._progress = saved.progress; } } // 加载配置 const config = SafeStorage.get('auto_task_config'); if (config) { // 合并配置,确保不覆盖验证规则 for (const key of Object.keys(config)) { if (this._configValidators[key]) { if (this._configValidators[key](config[key])) { this._config[key] = config[key]; } } else { this._config[key] = config[key]; } } } }, // 事件监听 on(event, callback) { this._listeners.push({ event, callback }); }, off(event, callback) { this._listeners = this._listeners.filter(l => l.event !== event || l.callback !== callback); }, emit(event, ...args) { this._listeners .filter(l => l.event === event) .forEach(l => { try { l.callback(...args); } catch (e) { console.error('[AutoTaskScheduler] 事件处理出错:', e); } }); } }; // 初始化自动化任务调度器 if (typeof window !== 'undefined') { window.AutoTaskScheduler = AutoTaskScheduler; window.addEventListener('load', () => { AutoTaskScheduler.init(); }); } // --- Client-side encryption helpers (PBKDF2 + AES-GCM) function _abToB64(buffer){ let binary = ''; const bytes = new Uint8Array(buffer); const chunkSize = 0x8000; for (let i = 0; i < bytes.length; i += chunkSize) { binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunkSize)); } return btoa(binary); } function _b64UrlEncode(str){ return btoa(unescape(encodeURIComponent(str))).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,''); } async function encryptSecretJS(plainText, passphrase, iterations = 200000){ const enc = new TextEncoder(); const salt = crypto.getRandomValues(new Uint8Array(16)); const passKey = await crypto.subtle.importKey('raw', enc.encode(passphrase), {name:'PBKDF2'}, false, ['deriveKey']); const key = await crypto.subtle.deriveKey({name:'PBKDF2', salt: salt, iterations: iterations, hash: 'SHA-256'}, passKey, {name:'AES-GCM', length: 256}, false, ['encrypt']); const iv = crypto.getRandomValues(new Uint8Array(12)); const cipherBuf = await crypto.subtle.encrypt({name:'AES-GCM', iv: iv}, key, enc.encode(plainText)); const combined = new Uint8Array(iv.byteLength + cipherBuf.byteLength); combined.set(iv, 0); combined.set(new Uint8Array(cipherBuf), iv.byteLength); const payload = { salt: _abToB64(salt.buffer), iter: iterations, cipher: _abToB64(combined.buffer) }; return _b64UrlEncode(JSON.stringify(payload)); } async function decryptSecretJS(blob_b64, passphrase){ const raw = atob(blob_b64.replace(/-/g,'+').replace(/_/g,'/')); const jsonStr = decodeURIComponent(escape(raw)); const payload = JSON.parse(jsonStr); const salt = Uint8Array.from(atob(payload.salt), c => c.charCodeAt(0)); const comb = Uint8Array.from(atob(payload.cipher), c => c.charCodeAt(0)); const iv = comb.slice(0,12); const ct = comb.slice(12); const enc = new TextEncoder(); const passKey = await crypto.subtle.importKey('raw', enc.encode(passphrase), {name:'PBKDF2'}, false, ['deriveKey']); const key = await crypto.subtle.deriveKey({name:'PBKDF2', salt: salt, iterations: parseInt(payload.iter || 200000,10), hash: 'SHA-256'}, passKey, {name:'AES-GCM', length: 256}, false, ['decrypt']); const plainBuf = await crypto.subtle.decrypt({name:'AES-GCM', iv: iv}, key, ct); return new TextDecoder().decode(plainBuf); } let RSA_PUBLIC_KEY_PEM = null; async function fetchRsaPublicKey(){ if(RSA_PUBLIC_KEY_PEM) return RSA_PUBLIC_KEY_PEM; try{ const proxyUrl = GM_getValue('proxy_url') || 'https://xuexitong-proxy.deno.dev'; const resp = await new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `${proxyUrl}/api/public-key`, timeout: 5000, onload: (r) => resolve(r), onerror: (e) => reject(e), ontimeout: (e) => reject(e) }); }); if(resp && resp.status === 200){ const data = JSON.parse(resp.responseText); if(data.publicKey){ RSA_PUBLIC_KEY_PEM = data.publicKey; console.log('[RSA] 公钥已从云端获取'); return RSA_PUBLIC_KEY_PEM; } } }catch(e){ console.warn('[RSA] 从云端获取公钥失败,将使用本地配置:', e); } return null; } async function _importRsaPublicKey(pem){ try{ if(!pem) return null; // 清理 PEM 格式:移除 BEGIN/END 行和换行,只保留 Base64 const b64 = pem.replace(/-----BEGIN[^-]+-----/g,'').replace(/-----END[^-]+-----/g,'').replace(/\s+/g,''); const der = Uint8Array.from(atob(b64), c=>c.charCodeAt(0)).buffer; return await crypto.subtle.importKey('spki', der, {name:'RSASSA-PKCS1-v1_5', hash:{name:'SHA-256'}}, false, ['verify']); }catch(e){ console.error('[RSA] 导入公钥失败:', e); return null; } } async function _verifyRsaSignature(payloadJsonStr, signatureB64, publicKeyPem){ try{ const keyPem = publicKeyPem || RSA_PUBLIC_KEY_PEM; if(!keyPem) return false; const publicKey = await _importRsaPublicKey(keyPem); if(!publicKey) return false; const sig = Uint8Array.from(atob(signatureB64), c=>c.charCodeAt(0)).buffer; const data = new TextEncoder().encode(payloadJsonStr); const ok = await crypto.subtle.verify({name:'RSASSA-PKCS1-v1_5', hash:{name:'SHA-256'}}, publicKey, sig, data); return ok; }catch(e){ console.warn('[RSA] 验签失败:', e); return false; } } // Helper: obtain base key material for AI API decryption // 仅从 GM_getValue 读取,无 fallback,未配置时返回 null 并返回提示用户 async function _getBaseKeyMaterialFromConfig(){ const enc = new TextEncoder(); let raw = null; try{ if (typeof GM_getValue === 'function') raw = GM_getValue('deepseek_basekey'); }catch(e){} if(!raw && typeof window !== 'undefined'){ raw = window.__DEEPSEEK_BASEKEY__ || null; } if(!raw){ console.warn('[学习通助手] AI API 密钥未配置,请在脚本配置中设置 deepseek_basekey'); return null; } return await crypto.subtle.importKey('raw', enc.encode(raw), {name:'PBKDF2'}, false, ['deriveKey']); } const Store = (typeof GM_listValues === "function") ? GMStore : MemoryStore; // --- END: 通用模块 // CDN 依赖已在顶部声明(使用 var 避免 TDZ 问题) if (window.self !== window.top) { const currentHref = window.location.href; if (currentHref.includes("answerQuestion2")) { console.log('[学习通助手] 检测到在随堂练习答题iframe中运行,启用答题逻辑'); } else { console.log('[学习通助手] 检测到在iframe中运行,跳过脚本'); return; } } const REGEX = { CLEAN_TITLE: /^【.*?】\s*|\s*(\d+\.\d+分)$/g, HTML_TAGS: /<((?!img|sub|sup|br)[^>]+)>/g, NBSP: / /g, WHITESPACE: /\s+/g, BR_TAG: //g, IMG_TAG: //g, OBJECT_ID: /objectId=([a-f0-9]+)/i, JOB_ID: /[?&]jobid=([^&]+)/i, HEX_HASH: /([a-f0-9]{24,})/i, JUDGE_TRUE: /(^|,)(是|对|正确|确定|√|对的|是的|正确的|true|True|T|yes|1)(,|$)/, JUDGE_FALSE: /(^|,)(非|否|错|错误|×|X|错的|不对|不正确的|不正确|不是|不是的|false|False|F|no|0)(,|$)/ }; const SELECTORS = { CX_VIDEO: 'video', CX_AUDIO: 'audio', CX_QUESTION_ZJ: '.TiMu', CX_QUESTION_ZY_KS: '.questionLi', CX_OPTION_ZJ: '[class*="before-after"], .option, [class*="option"], label', CX_OPTION_ZY_KS: '.answerBg', ZHS_QUESTION: '.examPaper_subject', ZHS_OPTION: '.subject_node .nodeLab' }; // 离线模式 - 无服务端依赖 const SERVER_CONFIGS = []; const CURRENT_SERVER_CONFIG = { url: "", location: "本地", color: "#999" }; const getRandomServer = () => ""; const delay = (second) => new Promise((resolve) => setTimeout(resolve, second * 1e3)); // 随机延迟函数 - 基础延迟 + 小概率额外延迟 const randomDelay = (baseSecond, maxJitter = 2) => { // 80%概率使用基础延迟,20%概率添加随机抖动 const shouldJitter = Math.random() < 0.2; const actualDelay = shouldJitter ? baseSecond + Math.random() * maxJitter : baseSecond; return delay(actualDelay); }; // 快速随机延迟 - 用于高频操作(0.1-0.5秒) const quickRandomDelay = () => { return delay(0.1 + Math.random() * 0.4); }; const getCookie = (name) => { const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); return match ? match[2] : ''; }; let _cachedUid = null; let _identifiedUserId = null; const getUid = () => { if (_identifiedUserId) return _identifiedUserId; if (_cachedUid) return _cachedUid; if (typeof _unsafeWindow !== 'undefined') { if (_unsafeWindow.getCookie) _cachedUid = _unsafeWindow.getCookie("UID"); if (!_cachedUid && _unsafeWindow.uid) _cachedUid = _unsafeWindow.uid; } if (!_cachedUid) _cachedUid = getCookie('UID'); if (!_cachedUid) _cachedUid = getCookie('_uid'); if (!_cachedUid) _cachedUid = localStorage.getItem('_local_user_id_v3'); return _cachedUid || ''; }; const setIdentifiedUserId = (userId) => { _identifiedUserId = userId; _cachedUid = null; }; // 本地回退用户 ID(当站点 UID 不可用时使用) // 使用字母数字混合格式(8位不重复字符),便于推广时分享和输入 const getLocalUserId = () => { const key = '_local_user_id_v3'; try { let id = localStorage.getItem(key); if (!id) { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; const shuffled = chars.split('').sort(() => Math.random() - 0.5); id = shuffled.slice(0, 8).join(''); try { localStorage.setItem(key, id); } catch (e) {} } return id; } catch (e) { return 'A1B2C3D4'; } }; const pad = (n) => n < 10 ? "0" + n : n.toString(); const formatDateTime = (dt) => `${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`; const getDateTime = () => formatDateTime(new Date()); const formatDuration = (seconds) => `${Math.floor(seconds / 60).toString().padStart(2, '0')}:${Math.floor(seconds % 60).toString().padStart(2, '0')}`; const ClientLicense = (function(){ const LS_KEY = '_ai_client_license_v1'; const OBFUSCATION_SEED_KEY = '_ai_client_obf_seed_v1'; const TAMPER_FLAG_KEY = '_ai_client_license_tampered_v1'; const REDEEMED_STORE_KEY = '_ai_client_redeemed_v1'; // ===== Supabase 密钥强加密保护 (AES-256-GCM + PBKDF2) ===== // 密文由 Python 加密脚本生成 (encrypt_supabase_keys.py) const ENC_IV_URL = '/dXPDOc7+MdaM4Snr7ZmSQ=='; const ENC_DATA_URL = 'Jlt5wsrKfweXQQq5UQE0mLfo8vjIdss2DNCbisX4oIcG/a6LA55c8A=='; const ENC_TAG_URL = 'behVAMzD17vuAFze2p2FKQ=='; const ENC_IV_KEY = 'zvPzXIvxGKy/lVdjX+LrrQ=='; const ENC_DATA_KEY = 'vK1MJZsR7Q9iS9CenOHyCsxvvtgHKAFxN2X8VKFV5fq1fnJVt/gLRnFuk31X4ZswUWpF0UQASXDffpEIYOaDXxzv7i0XlUUGJgTNQUHLC14WkVm5z533/Qzf6uTrd38yXHsoVuxorpGQKGA/OCFGbZOChd0X/NSMhhC8e4mrT89Z/kKQGnnVdnqS6rN5Sp5JzZeidfzaS5BYOE0C23JbOMmVxMZDGqX3H9Q0g846pzEzAI3YUqctVmZ6emwg3NRq+n0YcStWJiZRTdixnzPTrg=='; const ENC_TAG_KEY = 'D2DXvu60AlzfmPq7fo6F1Q=='; const ENC_SEED = 'xK9#mP2$vL5@nQ8!wR3^tY6&uI0*oA4'; const ENC_SALT = 'SupaBase2024!@#'; const ENC_ITERATIONS = 100000; // 密钥解密模块(使用 Web Crypto API) async function decryptSupabaseKeys(){ try{ // 派生加密密钥 const enc = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( 'raw', enc.encode(ENC_SEED), 'PBKDF2', false, ['deriveBits', 'deriveKey'] ); const derivedKey = await crypto.subtle.deriveKey( { name: 'PBKDF2', salt: enc.encode(ENC_SALT), iterations: ENC_ITERATIONS, hash: 'SHA-512' }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['decrypt'] ); // 解密函数 async function decrypt(ivB64, dataB64, tagB64){ const iv = Uint8Array.from(atob(ivB64), c => c.charCodeAt(0)); const data = Uint8Array.from(atob(dataB64), c => c.charCodeAt(0)); const tag = Uint8Array.from(atob(tagB64), c => c.charCodeAt(0)); const combined = new Uint8Array(data.length + tag.length); combined.set(data); combined.set(tag, data.length); const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv }, derivedKey, combined ); return new TextDecoder().decode(decrypted); } // 解密密钥 const url = await decrypt(ENC_IV_URL, ENC_DATA_URL, ENC_TAG_URL); const key = await decrypt(ENC_IV_KEY, ENC_DATA_KEY, ENC_TAG_KEY); return { url, key }; }catch(e){ console.error('[学习通助手] 密钥解密失败:', e); return null; } } // ===== Deno Deploy 代理配置 ===== // 多代理服务器配置(支持故障切换) const PROXY_API_URLS = [ 'https://wakeproxy-cgy2btptfvta.cypewake.deno.net', 'https://xuexitong-proxy.deno.dev' ]; const PROXY_API_SECRET = 'xuetong-2026-proxy-secret-key'; const PROXY_TIMEOUT = 15000; // 15秒超时 const MAX_RETRIES = 3; // 最大重试次数 // 当前活跃的代理服务器索引 let activeProxyIndex = 0; // 代理健康状态缓存(避免频繁切换到故障服务器) const proxyHealth = new Map(); const HEALTH_EXPIRE_TIME = 5 * 60 * 1000; // 5分钟过期 // 带超时的fetch请求 async function fetchWithTimeout(url, options, timeout = PROXY_TIMEOUT) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); return response; } finally { clearTimeout(timeoutId); } } // 获取健康的代理服务器索引 function getHealthyProxyIndex() { const now = Date.now(); for (let i = 0; i < PROXY_API_URLS.length; i++) { const index = (activeProxyIndex + i) % PROXY_API_URLS.length; const health = proxyHealth.get(index); if (!health || now > health.expireTime) { // 状态过期或未知,认为是健康的 return index; } if (health.healthy) { return index; } } return activeProxyIndex; // 所有都标记为不健康,返回当前的 } // 标记代理服务器健康状态 function markProxyHealth(index, healthy) { proxyHealth.set(index, { healthy, expireTime: Date.now() + HEALTH_EXPIRE_TIME }); } // 代理调用函数(带超时、重试和故障切换) async function callProxyApi(endpoint, body){ let lastError = null; const retries = Math.min(MAX_RETRIES, PROXY_API_URLS.length); let startIndex = getHealthyProxyIndex(); for(let attempt = 0; attempt < retries; attempt++){ try{ // 计算当前使用的代理索引(循环查找) const currentIndex = (startIndex + attempt) % PROXY_API_URLS.length; const currentUrl = PROXY_API_URLS[currentIndex]; console.log(`[学习通助手] 代理请求 (尝试 ${attempt + 1}/${retries}): ${currentUrl}${endpoint}`); const response = await fetchWithTimeout(`${currentUrl}${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${PROXY_API_SECRET}` }, body: JSON.stringify(body) }); if(!response.ok){ const errorText = await response.text(); markProxyHealth(currentIndex, false); throw new Error(`代理请求失败: ${response.status} - ${errorText}`); } // 标记为健康 markProxyHealth(currentIndex, true); activeProxyIndex = currentIndex; const result = await response.json(); return result; }catch(e){ lastError = e; console.warn(`[学习通助手] 代理调用失败 (尝试 ${attempt + 1}, endpoint: ${endpoint}):`, e.message); // 标记当前代理为不健康 const currentIndex = (startIndex + attempt) % PROXY_API_URLS.length; markProxyHealth(currentIndex, false); // 指数退避等待 if(attempt < retries - 1){ const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s... console.log(`[学习通助手] 等待 ${delay}ms 后重试`); await new Promise(resolve => setTimeout(resolve, delay)); } } } console.error(`[学习通助手] 代理调用最终失败 (endpoint: ${endpoint}):`, lastError); throw lastError; } // 健康检查函数 async function checkProxyHealth() { console.log('[学习通助手] 开始检查代理服务器健康状态...'); for (let i = 0; i < PROXY_API_URLS.length; i++) { try { const response = await fetch(`${PROXY_API_URLS[i]}/health`, { method: 'GET', timeout: 5000 }); if (response.ok) { markProxyHealth(i, true); console.log(`[学习通助手] 代理服务器 ${PROXY_API_URLS[i]} 健康`); } else { markProxyHealth(i, false); console.log(`[学习通助手] 代理服务器 ${PROXY_API_URLS[i]} 异常`); } } catch (e) { markProxyHealth(i, false); console.log(`[学习通助手] 代理服务器 ${PROXY_API_URLS[i]} 不可达:`, e.message); } } } // 定时健康检查(每5分钟) setInterval(checkProxyHealth, 5 * 60 * 1000); // Supabase 配置(延迟解密) let SUPABASE_URL = null; let SUPABASE_ANON_KEY = null; let keysDecrypted = false; // 初始化 Supabase 客户端(保留用于兼容) let supabaseClient = null; async function getSupabaseClient(){ if(!supabaseClient && typeof supabase !== 'undefined'){ try{ if(!keysDecrypted){ const keys = await decryptSupabaseKeys(); if(!keys){ console.warn('[学习通助手] 无法解密 Supabase 密钥'); return null; } SUPABASE_URL = keys.url; SUPABASE_ANON_KEY = keys.key; keysDecrypted = true; } supabaseClient = supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY); console.log('[学习通助手] Supabase 客户端初始化成功'); }catch(e){ console.warn('[学习通助手] Supabase 客户端初始化失败:', e); } } return supabaseClient; } // 离线缓存和同步机制 const OFFLINE_QUEUE_KEY = '_ai_offline_queue_v1'; const LAST_SYNC_TIME_KEY = '_ai_last_sync_time_v1'; const SYNC_INTERVAL = 5 * 60 * 1000; // 5分钟同步一次 // 存储容量管理常量 const STORAGE_MAX_SIZE = 4 * 1024 * 1024; // 4MB const STORAGE_WARN_SIZE = 3.5 * 1024 * 1024; // 3.5MB // 确保存储容量足够 function ensureStorageCapacity(){ try { let totalSize = 0; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key) { const value = localStorage.getItem(key); totalSize += key.length + (value ? value.length : 0); } } if (totalSize >= STORAGE_WARN_SIZE) { console.log(`[学习通助手] 存储容量警告: ${(totalSize / 1024 / 1024).toFixed(2)}MB / ${(STORAGE_MAX_SIZE / 1024 / 1024)}MB`); // 清理旧的日志数据(保留最近100条) try { const logs = JSON.parse(localStorage.getItem('_lxt_logs') || '[]'); if (logs.length > 100) { const trimmed = logs.slice(-100); localStorage.setItem('_lxt_logs', JSON.stringify(trimmed)); console.log('[学习通助手] 已清理旧日志,保留最近100条'); } } catch (e) { /* 忽略日志清理错误 */ } } } catch (e) { console.warn('[学习通助手] 存储容量检查失败:', e); } } // 获取离线队列 function getOfflineQueue(){ try{ const queue = localStorage.getItem(OFFLINE_QUEUE_KEY); return queue ? JSON.parse(queue) : []; }catch(e){ console.warn('[学习通助手] 读取离线队列失败:', e); return []; } } // 保存到离线队列 function saveToOfflineQueue(operation){ try{ const queue = getOfflineQueue(); queue.push({ ...operation, timestamp: Date.now(), id: Date.now().toString(36) + Math.random().toString(36).substr(2, 9) }); localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(queue)); console.log(`[学习通助手] 操作已加入离线队列: ${operation.type}`); }catch(e){ console.warn('[学习通助手] 保存到离线队列失败:', e); } } // 清空离线队列 function clearOfflineQueue(){ try{ localStorage.removeItem(OFFLINE_QUEUE_KEY); console.log('[学习通助手] 离线队列已清空'); }catch(e){ console.warn('[学习通助手] 清空离线队列失败:', e); } } // 使用代理API同步离线队列(降级方案) async function syncOfflineQueueWithProxy(queue){ const successIds = []; for(const operation of queue){ try{ let result = null; switch(operation.type){ case 'consume': result = await syncConsumeUsage( operation.cardHash, operation.deviceFingerprint, 1 ); break; case 'activate': result = await syncActivateCard( operation.cardHash, operation.cardPlain, operation.deviceFingerprint, operation.total, operation.tier ); break; default: console.warn('[学习通助手] 未知的操作类型:', operation.type); continue; } if(result && result.success){ successIds.push(operation.id); console.log(`[学习通助手] 使用代理同步操作成功: ${operation.type}`); }else{ console.warn(`[学习通助手] 使用代理同步操作失败 (${operation.type}):`, result?.message || '未知错误'); } }catch(e){ console.warn(`[学习通助手] 使用代理同步操作异常 (${operation.type}):`, e); } } return successIds; } // 同步离线队列到云端(支持降级策略) async function syncOfflineQueue(){ const queue = getOfflineQueue(); if(queue.length === 0) return; console.log(`[学习通助手] 开始同步离线队列,共 ${queue.length} 个操作`); let successIds = []; let useProxy = false; // 优先尝试 Supabase const client = await getSupabaseClient(); if(client){ try{ for(const operation of queue){ try{ let result = null; switch(operation.type){ case 'consume': result = await client.rpc('consume_usage', { p_card_hash: operation.cardHash, p_card_hashes: operation.cardHashes || [operation.cardHash], p_device_fingerprint: operation.deviceFingerprint, p_count: 1, p_current_remaining: operation.currentRemaining }); break; case 'activate': result = await client.rpc('activate_card', { p_card_hash: operation.cardHash, p_card_plain: operation.cardPlain, p_device_fingerprint: operation.deviceFingerprint, p_total: operation.total, p_tier: operation.tier }); break; default: console.warn('[学习通助手] 未知的操作类型:', operation.type); continue; } if(result.error){ console.warn(`[学习通助手] Supabase同步操作失败 (${operation.type}):`, result.error); throw new Error('Supabase操作失败,切换到代理'); }else{ successIds.push(operation.id); console.log(`[学习通助手] Supabase同步操作成功: ${operation.type}`); } }catch(e){ console.warn(`[学习通助手] Supabase同步异常,切换到代理模式:`, e.message); useProxy = true; break; } } }catch(e){ console.warn('[学习通助手] Supabase同步失败:', e); useProxy = true; } }else{ console.warn('[学习通助手] Supabase客户端未初始化,使用代理模式'); useProxy = true; } // 如果需要降级到代理模式,或者还有未同步的操作 if(useProxy || successIds.length < queue.length){ const remainingQueue = queue.filter(op => !successIds.includes(op.id)); const proxySuccessIds = await syncOfflineQueueWithProxy(remainingQueue); successIds = [...successIds, ...proxySuccessIds]; } // 移除已成功同步的操作 if(successIds.length > 0){ const remainingQueue = queue.filter(op => !successIds.includes(op.id)); // 添加容量检查和清理 ensureStorageCapacity(); localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(remainingQueue)); console.log(`[学习通助手] 已同步 ${successIds.length} 个操作,剩余 ${remainingQueue.length} 个`); } // 更新最后同步时间 localStorage.setItem(LAST_SYNC_TIME_KEY, Date.now().toString()); } // 检查是否需要同步(超过同步间隔) function shouldSync(){ try{ const lastSyncTime = localStorage.getItem(LAST_SYNC_TIME_KEY); if(!lastSyncTime) return true; const timeSinceLastSync = Date.now() - parseInt(lastSyncTime); return timeSinceLastSync >= SYNC_INTERVAL; }catch(e){ return true; } } // 自动同步(在适当时机调用) async function autoSync(){ if(!shouldSync()) return; const queue = getOfflineQueue(); if(queue.length === 0) return; console.log('[学习通助手] 触发自动同步'); await syncOfflineQueue(); } // 启动时检查并同步 async function initOfflineSync(){ try{ console.log('[学习通助手] 初始化离线同步机制'); await autoSync(); // 每5分钟检查一次 setInterval(async () => { await autoSync(); }, SYNC_INTERVAL); }catch(e){ console.warn('[学习通助手] 初始化离线同步失败:', e); } } // 云端同步:激活卡密(使用代理)- 支持次数累加 async function syncActivateCard(cardHash, cardPlain, deviceFingerprint, total, tier){ try{ // 修复:移除累计模式参数,使用简单的单卡密激活,与云代理兼容 const result = await callProxyApi('/api/activate', { card_hash: cardHash, card_plain: cardPlain, device_fingerprint: deviceFingerprint, total: total, tier: tier }); if(!result.success){ console.warn('[学习通助手] 云端激活卡密失败:', result.error); return { success: false, message: result.error || '未知错误', remaining: total }; } const data = result.data; if(data && data.length > 0){ const item = data[0]; console.log(`[学习通助手] 云端激活卡密成功,剩余: ${item.remaining}`); return { success: item.success, message: item.message, remaining: item.remaining }; } return { success: false, message: '未知错误', remaining: total }; }catch(e){ console.warn('[学习通助手] 云端同步异常:', e); return { success: false, message: e.message, remaining: total }; } } async function verifyAnswer(cardHash, deviceFingerprint){ try{ const result = await callProxyApi('/api/verify-answer', { card_hash: cardHash, device_fingerprint: deviceFingerprint, }); if(!result.success){ console.warn('[学习通助手] 云端答题验证失败:', result.error); return { success: false, message: result.error || '验证失败', has_remaining: true, remaining: -1, isNetworkError: true }; } const data = result.data; if(data && data.length > 0){ const item = data[0]; console.log(`[学习通助手] 云端答题验证: ${item.message}, 剩余: ${item.remaining}`); return { success: item.success, message: item.message, has_remaining: item.has_remaining, remaining: item.remaining, isNetworkError: false }; } return { success: false, message: '验证响应异常', has_remaining: true, remaining: -1, isNetworkError: true }; }catch(e){ console.warn('[学习通助手] 云端验证异常:', e); return { success: false, message: e.message, has_remaining: true, remaining: -1, isNetworkError: true }; } } // 云端同步:消耗答题次数(使用代理)- 支持多卡密累计 async function syncConsumeUsage(cardHash, deviceFingerprint, count = 1){ try{ // 安全修复:强制 count 必须为 1,防止恶意传入大于1的值 const safeCount = 1; const lic = await loadLicense(); const currentRemaining = lic ? lic.uses_remaining : 0; // 修复:移除累计模式参数,使用简单的单卡密同步,与云代理兼容 const result = await callProxyApi('/api/consume', { card_hash: cardHash, device_fingerprint: deviceFingerprint, count: safeCount }); if(!result.success){ console.warn('[学习通助手] 云端消耗次数失败:', result.error); // 检查是否是网络错误 const isNetworkError = result.isNetworkError || result.error === 'network_error' || result.error === 'proxy_error'; return { success: false, message: result.error || '未知错误', remaining: currentRemaining - count, has_remaining: true, isNetworkError: isNetworkError }; } const data = result.data; if(data && data.length > 0){ const item = data[0]; console.log(`[学习通助手] 云端消耗次数成功,剩余: ${item.remaining}`); return { success: item.success, message: item.message, remaining: item.remaining, has_remaining: item.remaining > 0, isNetworkError: false }; } return { success: false, message: '未知错误', remaining: currentRemaining - count, has_remaining: true, isNetworkError: false }; }catch(e){ console.warn('[学习通助手] 云端同步异常:', e); return { success: false, message: e.message, remaining: 0, has_remaining: true, isNetworkError: true }; } } // 云端同步:获取卡密剩余次数(使用代理) async function syncGetCardRemaining(cardHash){ try{ // 修复:移除累计模式参数,使用简单的单卡密查询,与云代理兼容 const result = await callProxyApi('/api/get-remaining', { card_hash: cardHash }); if(!result.success){ console.warn('[学习通助手] 云端获取次数失败:', result.error); return null; } const data = result.data; if(data && data.length > 0){ return data[0]; } return null; }catch(e){ console.warn('[学习通助手] 云端同步异常:', e); return null; } } // 云端同步:验证并使用邀请码 async function syncRedeemInviteCode(inviteCode, deviceFingerprint, bonus = 20){ try{ const result = await callProxyApi('/api/redeem-invite', { invite_code: inviteCode, device_fingerprint: deviceFingerprint, bonus: bonus }); if(!result.success){ console.warn('[学习通助手] 云端兑换邀请码失败:', result.error); return { success: false, message: result.error || '未知错误', bonus: 0 }; } const data = result.data; if(data && data.length > 0){ const item = data[0]; console.log(`[学习通助手] 云端兑换邀请码成功,获得: ${item.bonus}次`); return { success: item.success, message: item.message, bonus: item.bonus }; } return { success: false, message: '未知错误', bonus: 0 }; }catch(e){ console.warn('[学习通助手] 云端同步异常:', e); return { success: false, message: e.message, bonus: 0 }; } } // 云端同步:注册邀请码(确保唯一性) async function syncRegisterInviteCode(inviteCode, deviceFingerprint){ try{ const result = await callProxyApi('/api/register-invite', { invite_code: inviteCode, device_fingerprint: deviceFingerprint }); if(!result.success){ console.warn('[学习通助手] 云端注册邀请码失败:', result.error); return { success: false, message: result.error || '未知错误', invite_code: '' }; } const data = result.data; if(data && data.length > 0){ const item = data[0]; console.log(`[学习通助手] 云端注册邀请码: ${item.invite_code}, 成功: ${item.success}`); return { success: item.success, message: item.message, invite_code: item.invite_code }; } return { success: false, message: '未知错误', invite_code: '' }; }catch(e){ console.warn('[学习通助手] 云端同步异常:', e); return { success: false, message: e.message, invite_code: '' }; } } // 云端同步:查询设备的邀请码 async function syncGetDeviceInviteCode(deviceFingerprint){ try{ const result = await callProxyApi('/api/get-invite-code', { device_fingerprint: deviceFingerprint }); if(!result.success){ console.warn('[学习通助手] 云端查询邀请码失败:', result.error); return { success: false, message: result.error || '未知错误', invite_code: '' }; } const data = result.data; if(data && data.length > 0){ const item = data[0]; console.log(`[学习通助手] 云端查询邀请码: ${item.invite_code}`); return { success: item.success, message: item.message, invite_code: item.invite_code }; } return { success: false, message: '未找到邀请码', invite_code: '' }; }catch(e){ console.warn('[学习通助手] 云端同步异常:', e); return { success: false, message: e.message, invite_code: '' }; } } // 云端同步:刷新本地卡密剩余次数(支持多卡密累加) async function syncRefreshLocalLicense(cardHash, deviceFingerprint){ try{ const cloudData = await syncGetCardRemaining(cardHash); if(!cloudData) return false; const lic = await loadLicense(); if(!lic) return false; // 以云端返回的累计剩余次数为准 lic.uses_remaining = cloudData.remaining; // 同步卡密hash列表,确保后续云端操作传递所有卡密 if(cloudData.cardHashes && cloudData.cardHashes.length > 0){ lic.cardHashes = cloudData.cardHashes; } lic.seal = await computeSeal(lic); const ok = await saveLicense(lic); if(ok){ console.log(`[学习通助手] 本地次数已同步: ${cloudData.remaining}`); } return ok; }catch(e){ console.warn('[学习通助手] 同步本地授权失败:', e); return false; } } function loadRedeemedMapSync(){ try{ const local = (()=>{ try{ return localStorage.getItem(REDEEMED_STORE_KEY); }catch(e){ return null; } })(); let gmRaw = null; try{ if(typeof gmGet === 'function') gmRaw = gmGet(REDEEMED_STORE_KEY); }catch(e){ gmRaw = null; } const raw = local || gmRaw; if(!raw) return {}; try{ const obj = JSON.parse(raw); if(obj && typeof obj === 'object') return obj; }catch(e){} }catch(e){} return {}; } function saveRedeemedMap(map){ try{ const s = JSON.stringify(map || {}); try{ localStorage.setItem(REDEEMED_STORE_KEY, s); }catch(e){} try{ if(typeof gmSet === 'function') gmSet(REDEEMED_STORE_KEY, s); }catch(e){} return true; }catch(e){ return false; } } function isRedeemedKey(key){ if(!key) return false; try{ const m = loadRedeemedMapSync(); return !!(m && m[key]); }catch(e){ return false; } } function markRedeemedKey(key){ if(!key) return false; try{ const m = loadRedeemedMapSync() || {}; if(!m[key]) m[key] = { redeemedAt: Date.now() }; return saveRedeemedMap(m); }catch(e){ return false; } } function markRedeemedWithDevice(key, deviceFingerprint){ if(!key) return false; try{ const m = loadRedeemedMapSync() || {}; if(!m[key]) m[key] = { redeemedAt: Date.now(), device: deviceFingerprint }; return saveRedeemedMap(m); }catch(e){ return false; } } // checkDeviceBinding 已废弃:不再限制设备绑定,保持灵活性 // 保留此函数仅为向后兼容,始终返回 ok function checkDeviceBinding(key, deviceFingerprint){ return { ok: true }; } // 离线模式 - 无远程服务端 let _serverInitDone = true; const REMOTE_API_KEY = null; // 本地卡密数据库(离线验证模式) // 格式:{ "卡密hash": { uses: 500, tier: 500, apiKey: "可选" } } // 注意:新增卡密需通过本地服务端生成后手动添加至此 const LOCAL_CARD_DB = { "5ebc13b6bacd370cbc1b114c40611c96f48cc30565b56553d1aedc0eed8fc234": { uses: 500, tier: 0, apiKey: null }, "f2714aa7e9329e927ead0f131e5b8928a64a4b90199a8ac987dd2a3047ae69ec": { uses: 1489, tier: 0, apiKey: null }, "09fc5d4deb3ed9067b818999916af168c5c519b56bc1168dac949b49bc89f4fc": { uses: 500, tier: 500, apiKey: null }, "0ecb1d342ce0c76ac570da3947afe390f7aff538e53f9b72aac388b8a67eebfb": { uses: 500, tier: 500, apiKey: null }, "0f70b766871d0cff3e3785f3d88ee0afb340544761332c871c930bec2fb2cc59": { uses: 500, tier: 500, apiKey: null }, "1327f17fbdf6d0aa814b5f3361c8c55eae9fd2be7f71c08a15bb7d04e3e0901d": { uses: 500, tier: 500, apiKey: null }, "173b9960ef67f084b2b01cdf4646c34cd8ad10c9d5f197b87c98556ced5e1beb": { uses: 500, tier: 500, apiKey: null }, "21ea1205159ef23f70cc7d83ebeec492835919c0c3708e9fe586bb3396b68bc3": { uses: 500, tier: 500, apiKey: null }, "28819f0a00adc9f8846777771fdc979d3c219647a400f7d5fd0f7b8fa3c0daed": { uses: 500, tier: 500, apiKey: null }, "2a095305a21a5bf3c12036593234b5fe4e41371b3e4a5528848cb644ccd2bbd6": { uses: 500, tier: 500, apiKey: null }, "2f402c942bcdd4d14ca2151bfa3c292e7fe4ba5d830f40475939d4868ff6e531": { uses: 500, tier: 500, apiKey: null }, "333789e510d63997e3e0a4826f836d893d8253b2e654f441c9f5307aebd8ece5": { uses: 500, tier: 500, apiKey: null }, "4340d88111e39d606297e70aa135d7d3cd0384a0bdb69c4bbf0a1016bbe8855e": { uses: 500, tier: 500, apiKey: null }, "44e08785085e9b00b1df2a08ccb16b244bd992224ee32af2b4e2d76ad4d7d37c": { uses: 500, tier: 500, apiKey: null }, "47a337a3d9a375393af6459b96d94a2bbbb48c479b74333296c7e79de7e14273": { uses: 500, tier: 500, apiKey: null }, "4e525c8f82fe4cb1ad1b706931bb3ab23620bfebca92d01dc7d55ec35f9b77aa": { uses: 500, tier: 500, apiKey: null }, "51a9ae607517e07e24a9fd26eb6b0872f46f55d4a1b6ab61a01c12730922e02f": { uses: 500, tier: 500, apiKey: null }, "57e9985981286af380ba8cd54295b6e90dd6feb70afb5d04ff30ab6900a2e987": { uses: 500, tier: 500, apiKey: null }, "5a6bef4114d23b6b51975361b47d0c4697caa6e7919fb47ccd1306e351d5acf8": { uses: 500, tier: 500, apiKey: null }, "65d1e9eb201fd8e4d23891e73ad81ecddc0cd4d38db913710c0976c091a25908": { uses: 500, tier: 500, apiKey: null }, "66fb4114e1aeb86f0399663c77ff3bc885c28b20035cb68758dd1d7254d93853": { uses: 500, tier: 500, apiKey: null }, "6b2b1ad9a7bbb36981ef9097910fc44a2aaad3dbbef969f1d55ce3b729799a91": { uses: 500, tier: 500, apiKey: null }, "6c2a13e1c860648ac80f911d45d6e92cb0251f234c46008d91a089c5bfa8c5e4": { uses: 500, tier: 500, apiKey: null }, "6f3e75269d236e0d7cb9bfaef5d45fd8267af0e15790e95dc99bcdcb3c6f3c08": { uses: 500, tier: 500, apiKey: null }, "724973393789c7c62367111eaa8528272f61457c9b652a823a0b4db5b55d83c0": { uses: 500, tier: 500, apiKey: null }, "78b755f4a9961a2522500be9856190888ae557fe66975110662cd3d7880fdf1a": { uses: 500, tier: 500, apiKey: null }, "78ddc7e38cfe2e1b35d6136e2a3eb04d06f773e346fbcba187a48ba9d93ce104": { uses: 500, tier: 500, apiKey: null }, "7da83b3cf152173b684db38b921dd0e98734df13ee31174d209896ba53a61690": { uses: 500, tier: 500, apiKey: null }, "7f109c8bdd20ed017e654b3561fd0ea10e59c11afbf088651349bc52809da415": { uses: 500, tier: 500, apiKey: null }, "7fde5a7b1ebb3c6f5ba7c57b03d62a461ac895308478d9fde9f9ec495677a857": { uses: 500, tier: 500, apiKey: null }, "83979e208bd46b531878ed53abf697a25d6455ba57a639561cda635020b01d6f": { uses: 500, tier: 500, apiKey: null }, "85b8ae7857bcebf2d6c7890566a320c6620d376a8c098f5704ea6a64d7663cf7": { uses: 500, tier: 500, apiKey: null }, "940267a8b6349725b90d6c7dc000fe8045d359e8aebf3460808d24072b86218f": { uses: 500, tier: 500, apiKey: null }, "98ef94b97c9b4387fb85159cc16ecede2e46852fbb89095e7b1e3c5fb5212eff": { uses: 500, tier: 500, apiKey: null }, "ac5ad53f42bf29c6f587765b52906f9277917f35e3438eb4327f711383f26385": { uses: 500, tier: 500, apiKey: null }, "afcdd7c0e1984f591288d53933c355d5ea011228eb9716840d960b4b6f06bca7": { uses: 500, tier: 500, apiKey: null }, "b1c63f85532cdc80a1c5b8243c70cc135dfcfe97ef26b22fad6113bd8cf8cf4e": { uses: 500, tier: 500, apiKey: null }, "b4ec15abda4128609f8050e36759b697ed98a320713afbffc90764b296165b85": { uses: 500, tier: 500, apiKey: null }, "b5f6214f7be44e4df91c1653a623b0f4673d10675e64d1badc7a76372ca44da2": { uses: 500, tier: 500, apiKey: null }, "b8b02e8a7e89dd3a72aa6dd7621e4155c66ac60ddb013b822cd520e76c7b7c36": { uses: 500, tier: 500, apiKey: null }, "b9d5f04e53c15c9e013ed84c1e98faf00d3664e04d627809083a14eb66e11ac0": { uses: 497, tier: 500, apiKey: null }, "c3f034700b6227a1f3023fb80f3c87285080a6d91170387eac4a8041324bbc9a": { uses: 500, tier: 500, apiKey: null }, "c40c409371bd87f0a382e1c7537306e6eda11b5cf61a1044c2c7d6cdb2efbe18": { uses: 500, tier: 500, apiKey: null }, "c61ab68374b6009da7a19fe88ba0a35ceabb8a8c0636118dadfc57cb8140afd0": { uses: 500, tier: 500, apiKey: null }, "c91e03cac194cc3b1618ec3951d6efe72abfeb46f3bd84e565469b03bd3dd741": { uses: 498, tier: 500, apiKey: null }, "cf2224433ad410d0a355fe3e1314b437ca30eadf048a9e0a4f230b998ec8cd54": { uses: 500, tier: 500, apiKey: null }, "cf69daee9067201b3fcfdd2aa5bf755fdb56db388d65c91a571a76acd3213e13": { uses: 500, tier: 500, apiKey: null }, "d3d84ff3c82c18fca51445a39bd201111c691e74e839671c80dcd82329c95ee3": { uses: 500, tier: 500, apiKey: null }, "d7a85785e81452805df1c4a34985a93954932bf3dd66025f058daacce202d260": { uses: 500, tier: 500, apiKey: null }, "d9d7f36c1ecb2a299e3af239bfaddd2aa7e464064c882bf921c343e8bb4a88c9": { uses: 500, tier: 500, apiKey: null }, "dbf450efefb848903e6454f01a6e3dd48ace1eb93ad945d81230d6d568e57d9a": { uses: 500, tier: 500, apiKey: null }, "dffa5ad54dd2d3795a350147d5ea4bd47844fc869ece93d27999a314ddabd4ce": { uses: 500, tier: 500, apiKey: null }, "e2ac5d123d5c9343574e066978a71f8bcfb1939ff4938ae0f09d1e0da1f539cf": { uses: 500, tier: 500, apiKey: null }, "efb40c4b0df89bcf32acdceb0065a36304293c87f7b625898b09f341711d1f0a": { uses: 500, tier: 500, apiKey: null }, "f3ab78c745ce7b2cdc032a4121424a8a5228b0aac9159ea3219bcbed033fcf76": { uses: 500, tier: 500, apiKey: null }, "f8a191aecdacda92ede6bc35f094f6be2b22b909986a69bb4798b9ed81aa0131": { uses: 500, tier: 500, apiKey: null }, "f9c5159ab94dc81507ced47ab034342910f533590a9d38df6c7cda0a6b89fe26": { uses: 500, tier: 500, apiKey: null }, "fcbdc42797dd305338ba6220a2660b3aa4ece9062993adcb3da05d6af0bf95df": { uses: 500, tier: 500, apiKey: null } }; // 本地邀请码数据库 const LOCAL_INVITE_CODES = { // 格式:{ "邀请码": { uses: 50, max_uses: 100 } } "TEST2026": { uses: 50, max_uses: 100 }, "WELCOME": { uses: 30, max_uses: 50 } }; // 离线模式 - 无远程服务端初始化 async function initRemoteServer(){ // 离线模式,无需初始化远程服务端 return; } // 离线模式 - 无远程POST请求 async function remotePost(path, body){ throw new Error('no_remote'); } // 离线模式 - 无远程核销 async function tryRemoteRedeem(codeFull, referrerId){ return { ok:false, message: 'no_remote' }; } // 离线模式 - 无远程消费 async function tryRemoteConsume(codeHash){ return { ok:false, message: 'no_remote' }; } // 真实卡密载荷(由 generate_cards.py 生成,PBKDF2 + AES-GCM 加密) const ENCRYPTED_PAYLOADS = { "c85575581746fee5caad982542d8939436460834eb91820dda8212751de0382b": { salt: "L4A5iZJ2xsdF9sQylBBr3g==", iter: 200000, cipher: "UGHcizjsz01Pu7XSeegSKgTgshR00dzA9skS1U1Oankz3M7VnFGn2pDOuIR6VV0tS1dBMPymD8bENwSCKBozBZuRLHzCqmWkIVAaf/TrOsfRJstd+RsDzeq7ORFoYyn1o/V5JFnPw6ROLqjDhK8vVMSeMfhCHvj32IwmhSKGe735DhGcuGVSxtwdJ+ju72UG+5+rN7f6jJea+hjEIWJ6", uses: 500, tier: 500 }, "7fa2564790dd5bc764a32f79ef507565d0749a186ea5eb49d279ec64c12de797": { salt: "aQ7tg18MJB/dSoexSuVaGw==", iter: 200000, cipher: "pk5Xmci4nsbhxVSGU42LoKMqcdzzkFH9XJtI3BwEZtaPQQHDRDQWE5RpS0nqvv0C3B3jAVCAYdjo/5zsYXvcMO5JtLV8lJ/h4fqUG3KjARqvnCJUof3znJevkL9xlQkQSl6Lfbvr7hQJE95P9LRCIUaqqwAwwxIQ4X0fYKH/jMaG/u3pu8vqNkbXdH06jsWA43zMuPm/1n5mNzNF1eFr", uses: 500, tier: 500 }, "d6e478523abf0d09df565f73135fdf3ee5b10d44cb496ecb093c1820614e4ff9": { salt: "ouqjQFI0ncp7E6gIDhmYQA==", iter: 200000, cipher: "rVJfrXme1euqgydbRaHJvWBIrxc/BZtnYdpUK8rUhpBOGkOeGOt7tb0wBOCYXB04pd289vqqDUJWwncOcI1vjrziW56zGwvdUxhSoumQr9SkiuqIvJJEGLls9fFBLDCtQ+DJSQgHs2VOTEuhjkbPz+j+EVJPiavey6AqQ/6O/i91UW6lbwuRV5mB0nHZWjTcRCE5u0ztT754m0Enj5jJ", uses: 500, tier: 500 }, "14557fbb2fa939e97f371d6211d54e5f3e90bb68579ecdbf93f8074b853b49a9": { salt: "PPZ9ep7KPWnZ2uxdcFtLew==", iter: 200000, cipher: "XO4NrJl/riETHVym5yF99zNGB0/0Q9trOICSlntTTPThqlSBLU2SWeYLGIDqHFBMCeiIAR/LEFo/Ts2pdLfTReWDb3lS19m7xUBTn4nwpH1Z9L5Si7DSxTV5kvYEDvh45+XaLwwiEs1pGABUtfkq8WgIvk1xb6J2n0IvGoUE4OfNNUznElrhau7Q33Yl6jDhcYc/GQnd89N6UWdO357w", uses: 500, tier: 500 }, "74aeb64a6a2ea3f2d0858c7ba048b8e7c64311d12edbd03e9f7c18c9a3bb2750": { salt: "ry+Adto88+mPbKgVUnpUDA==", iter: 200000, cipher: "ERYoB8sJDE11BHXcrxr2xF5hvMICD+lAYFVDK+EyxhAhu5HM7S5l7fDT/uQhuJ3vlQbCmD/VUW4FWKLpQ/8eUPSK/oJzp1/FEFxC6KHy8KVeCDQgQ3ZYdTAIUe+EPsSlq2I+Wmp0j9sraQiLzaZeqbftrwfW/Bntpfnpr6E02xR6jH54hglAEGOSN+SpdMz4HoLduwMwhBeeqf7Kw8z2", uses: 500, tier: 500 }, "376d99cb1e061e623ab40adda5a116d35a042be858fe765114668ce90896f074": { salt: "X4HlC2K5J8upiTMTq2fzXg==", iter: 200000, cipher: "2GIPzg2Uc0Go84/ed+J5xUQVEXJC2q/7ttB8wDZ8EgwdHrAkU6QR4k6VHdUA1cuZrJpHi2aKJ1y8Ymnt0qz59VOe0GRGdYDCzoYxAjy422PWr+/gSr6cMDufzatycS3gGLGZwWG7tC1dYD9tikaOLIjksT6QsykiWEst+T8pHKmUfNSBBVXBWUBfZ/yBhAB5WNgbcW01fRdbz5V2G3fWMBo=", uses: 1500, tier: 1500 }, "bdb2cedcc94765fb365f4cab4728b1279b476f49327eaed387ea737b5867d2cb": { salt: "35rNiW1eY/98GmgZ17WmKg==", iter: 200000, cipher: "XXeg37kEDi20MKxvZbMfOtuqQ8iQS68NYROXq/g43qeEOCe4iFYpE1xShe3nv11zqYuEIvKtuYzZHaT1ij4/NLnLhHVdYfUFm3A0jLWDcYaXleN9f80MoK6gJgj6F4u8ZuGza4ffysNeBDxMiF20kdtppNbTFm47fDi0y7kzfSWnNk8UGvjMXMcfN3VsT5y6eJf9LCfUAlnssILRp2KtIYc=", uses: 1500, tier: 1500 }, "e5b5ff84b68deb149c463c8665c06aabe78f1c76b08358801bf8ecfd7b135da8": { salt: "MwIyshE0fH2md23liBGFGQ==", iter: 200000, cipher: "tHBAoZKJHBnPKCuIa7PDYf94zdo9A4zlTcaRkioLtozB4QyKhvdMLe3+0EjxFFcaPchPHbFDcS7RMl5Aj/dXTaRbDP/NFBbCx1+5T2nfVUtFum4Wj0wgWmuL75ZBWtywYqmqe0qe+4g1QPEil0+N87By85fL7+1V69ncgZzeXBD60D8so94YoRFEuoQwux4QZzdhpM8wBWH7Hi8bm/y4mz0=", uses: 1500, tier: 1500 }, "59135746ebceecc31bc3699a75ae170c4d3f28e03488f23caee82a7451e8a759": { salt: "3zUgL5NnDbZL50IEk8518Q==", iter: 200000, cipher: "cu56zLRuRPSiX80g0CRnfV5efcY9dtZnvbqaM45L1XL5U0sb6kCKTtDCH91ZDlXOYst3JWNPk2zXdZxUeDbhYTcCiZ7V6BnR8IONGX4PsRUIrGGIA9JHEssSrNapUTP/QcOf3NlYNF6kp+6dLdVbbGGVKxVqKbnRChsBIbVbv5fxJ5IOai3CWgigtIelqK8QdjV4l62nz+nGKa8HSGs+Ks4=", uses: 2888, tier: 2888 }, "fb4a223111fe01992cd41efcaee143fbaf3b5a0e40e7ad94fa71c02bf8eb9f01": { salt: "qY5usgOeZcxHOkCXjf++DA==", iter: 200000, cipher: "pCbBt4YE4i3dG+41gj3bvH1r0/PlwYS5WdrXq7tITqfN3PLcNTvoKsnt0ulMSBPEgizRoYxV+stomrZmsnkavjQNn6e/PWARkIzMPLz9ZTHsgnNtWSjolT3CX4LIF0o1kIu/SPpfxM+5cb1N7yp+2sb7XHDJe0ARgw0pphmfKp/mlX/aq4QNBUKIPuMlug0K8nXG+Y6GlwKpLNUxL6NC/fA=", uses: 2888, tier: 2888 } }; function b64ToArrayBuffer(b64){ try{ if (typeof atob === 'function') { return Uint8Array.from(atob(b64), c => c.charCodeAt(0)).buffer; } if (typeof Buffer !== 'undefined') { return Uint8Array.from(Buffer.from(b64, 'base64')).buffer; } throw new Error('No base64 decoder available'); }catch(e){ if (typeof Buffer !== 'undefined') return Uint8Array.from(Buffer.from(b64, 'base64')).buffer; throw e; } } function b64ToString(b64){ try{ if (typeof Buffer !== 'undefined') return Buffer.from(b64, 'base64').toString('utf8'); if (typeof atob === 'function') return atob(b64); throw new Error('No base64 decoder available'); }catch(e){ if (typeof Buffer !== 'undefined') return Buffer.from(b64, 'base64').toString('utf8'); throw e; } } function b64ToBinaryString(b64){ try{ if (typeof atob === 'function') return atob(b64); if (typeof Buffer !== 'undefined') return Buffer.from(b64, 'base64').toString('binary'); throw new Error('No base64 decoder available'); }catch(e){ if (typeof Buffer !== 'undefined') return Buffer.from(b64, 'base64').toString('binary'); throw e; } } function arrayBufferToB64(buf){ if (typeof btoa === 'function') return btoa(String.fromCharCode(...new Uint8Array(buf))); if (typeof Buffer !== 'undefined') return Buffer.from(new Uint8Array(buf)).toString('base64'); return ''; } async function sha256hex(text){ try { const enc = new TextEncoder(); const h = await crypto.subtle.digest('SHA-256', enc.encode(text)); return Array.from(new Uint8Array(h)).map(b=>b.toString(16).padStart(2,'0')).join(''); } catch(e) { // 降级方案:使用简单哈希 let hash = 0; for(let i = 0; i < text.length; i++) { const char = text.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(16).padStart(64, '0').slice(0, 64); } } async function deriveKey(code, saltB64, iterations, length=256){ const enc = new TextEncoder(); const baseKey = await crypto.subtle.importKey('raw', enc.encode(code), {name:'PBKDF2'}, false, ['deriveKey']); const salt = b64ToArrayBuffer(saltB64); return await crypto.subtle.deriveKey({name:'PBKDF2', salt, iterations, hash:'SHA-256'}, baseKey, {name:'AES-GCM', length}, false, ['decrypt']); } async function decryptCipher(cipherB64, key){ try{ const data = new Uint8Array(b64ToArrayBuffer(cipherB64)); const iv = data.slice(0,12).buffer; const ct = data.slice(12).buffer; const plain = await crypto.subtle.decrypt({name:'AES-GCM', iv: iv}, key, ct); return new TextDecoder().decode(plain); }catch(e){ throw new Error('decrypt_failed'); } } function gmSet(key, val){ try{ if(typeof _GM_setValue === 'function') _GM_setValue(key, val); }catch(e){} } function gmGet(key){ try{ if(typeof _GM_getValue === 'function') return _GM_getValue(key); }catch(e){} return null; } // ===== AES-GCM 加密替代 XOR 弱加密 ===== const LICENSE_ENC_KEY = '_license_enc_key_v2'; async function getLicenseEncryptionKey(){ try{ let keyB64 = localStorage.getItem(LICENSE_ENC_KEY) || gmGet(LICENSE_ENC_KEY); if(!keyB64){ // 首次生成:使用 Crypto API 生成 AES-256 密钥 const key = await crypto.subtle.generateKey({name:'AES-GCM', length:256}, true, ['encrypt','decrypt']); const exported = await crypto.subtle.exportKey('raw', key); keyB64 = arrayBufferToB64(exported); try{ localStorage.setItem(LICENSE_ENC_KEY, keyB64); gmSet(LICENSE_ENC_KEY, keyB64); }catch(e){} } const keyData = b64ToArrayBuffer(keyB64); return await crypto.subtle.importKey('raw', keyData, 'AES-GCM', false, ['encrypt','decrypt']); }catch(e){ console.warn('[学习通助手] 获取加密密钥失败:', e); return null; } } async function licenseEncrypt(obj){ try{ const key = await getLicenseEncryptionKey(); if(!key) return null; const s = JSON.stringify(obj); const enc = new TextEncoder(); const iv = crypto.getRandomValues(new Uint8Array(12)); // 12 字节 IV const cipher = await crypto.subtle.encrypt({name:'AES-GCM', iv}, key, enc.encode(s)); // 格式: IV(12字节) + 密文 const combined = new Uint8Array(iv.length + cipher.byteLength); combined.set(iv); combined.set(new Uint8Array(cipher), iv.length); return arrayBufferToB64(combined.buffer); }catch(e){ return null; } } async function licenseDecrypt(b64){ try{ const key = await getLicenseEncryptionKey(); if(!key) return null; const data = new Uint8Array(b64ToArrayBuffer(b64)); if(data.length < 13) return null; // 至少 IV(12) + 1字节密文 const iv = data.slice(0, 12); const cipher = data.slice(12); const plain = await crypto.subtle.decrypt({name:'AES-GCM', iv}, key, cipher); return JSON.parse(new TextDecoder().decode(plain)); }catch(e){ return null; } } // ===== 设备指纹系统 ===== // 设计原则:稳定优先,避免浏览器/系统更新导致指纹变化 // 使用"持久化设备 ID"方案:首次生成后永久保存,不受环境变化影响 const PERSISTENT_DEVICE_ID_KEY = '_persistent_device_id_v2'; async function computeDeviceFingerprint(){ try{ // 1. 优先使用已保存的持久化设备 ID(脚本更新/重载时保持一致) let persistentId = localStorage.getItem(PERSISTENT_DEVICE_ID_KEY) || gmGet(PERSISTENT_DEVICE_ID_KEY); if(persistentId && persistentId.length === 64){ return persistentId; } // 2. 首次生成:使用稳定信息生成设备 ID const parts = []; // 只使用稳定的基础信息(避免 Canvas/WebGL/字体等易变因素) parts.push(navigator.userAgent||''); parts.push(navigator.platform||''); parts.push(navigator.hardwareConcurrency||'0'); parts.push(screen.width + 'x' + screen.height); parts.push(screen.colorDepth||'0'); parts.push(Intl.DateTimeFormat().resolvedOptions().timeZone||''); parts.push(navigator.language||''); // ⚠️ 重要:不再使用随机值,确保每次生成的设备指纹一致 // 只使用稳定的基础信息 parts.push(navigator.cookieEnabled ? '1' : '0'); parts.push(navigator.doNotTrack || '0'); const deviceId = await sha256hex(parts.join('|')); // 3. 持久化保存(双存储备份) try{ localStorage.setItem(PERSISTENT_DEVICE_ID_KEY, deviceId); gmSet(PERSISTENT_DEVICE_ID_KEY, deviceId); }catch(e){ console.warn('[学习通助手] 保存设备 ID 失败:', e); } return deviceId; }catch(e){ // 降级方案:使用稳定的浏览器信息生成哈希 const fallback = sha256hex(navigator.userAgent + '|' + navigator.language + '|' + screen.width + 'x' + screen.height); const padded = fallback.padEnd(64, '0').slice(0, 64); try { localStorage.setItem(PERSISTENT_DEVICE_ID_KEY, padded); } catch(e2) {} return padded; } } // 获取设备 ID 的短版本(用于日志显示) function getDeviceIdShort(){ const id = localStorage.getItem(PERSISTENT_DEVICE_ID_KEY) || gmGet(PERSISTENT_DEVICE_ID_KEY); return id ? id.slice(0, 12) : 'unknown'; } async function computeEnhancedFingerprint(){ try{ const components = {}; const parts = []; parts.push(navigator.userAgent||''); components.user_agent = navigator.userAgent||''; parts.push(navigator.platform||''); components.platform = navigator.platform||''; parts.push(navigator.hardwareConcurrency||'0'); components.hardware_concurrency = String(navigator.hardwareConcurrency||'0'); parts.push(screen.width + 'x' + screen.height); components.screen_res = screen.width + 'x' + screen.height; parts.push(screen.colorDepth||'0'); parts.push(screen.pixelRatio||'1'); parts.push(Intl.DateTimeFormat().resolvedOptions().timeZone||''); components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone||''; parts.push(navigator.language||''); components.language = navigator.language||''; parts.push(navigator.languages ? navigator.languages.join(',') : ''); parts.push(navigator.vendor||''); parts.push(navigator.deviceMemory||'0'); let canvasHash = ''; try{ const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.textBaseline = 'alphabetic'; ctx.font = '14px Arial'; ctx.fillStyle = '#f60'; ctx.fillRect(125,1,62,20); ctx.fillStyle = '#069'; ctx.fillText('BrowserFingerprint',2,15); ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; ctx.fillText('BrowserFingerprint',4,17); const canvasData = canvas.toDataURL(); canvasHash = await sha256hex(canvasData); parts.push(canvasHash); components.canvas_hash = canvasHash; }catch(e){ parts.push('canvas_fail'); } let webglHash = ''; try{ const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if(gl){ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); const renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : ''; const vendor = debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : ''; webglHash = await sha256hex(renderer + '|' + vendor); parts.push(webglHash); components.webgl_hash = webglHash; } }catch(e){ parts.push('webgl_fail'); } try{ const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioCtx.createOscillator(); const analyser = audioCtx.createAnalyser(); const gain = audioCtx.createGain(); oscillator.connect(gain); gain.connect(analyser); audioCtx.close(); }catch(e){} const fullHash = await sha256hex(parts.join('|')); return { hash: fullHash, components: components }; }catch(e){ return { hash: '0', components: {} }; } } async function computeHmac(keySeed, message){ const enc = new TextEncoder(); const keyMat = enc.encode(keySeed); try{ const key = await crypto.subtle.importKey('raw', keyMat, {name:'HMAC', hash:'SHA-256'}, false, ['sign']); const sig = await crypto.subtle.sign('HMAC', key, enc.encode(message)); return Array.from(new Uint8Array(sig)).map(b=>b.toString(16).padStart(2,'0')).join(''); }catch(e){ return null; } } async function computeSeal(lic){ try{ const device = await computeDeviceFingerprint(); const seed = String(localStorage.getItem(OBFUSCATION_SEED_KEY) || gmGet(OBFUSCATION_SEED_KEY) || ''); // 关键修复:对于免费试用的 license,使用 isFreeTrial 标识来参与计算,不需要 codeHash if(lic.isFreeTrial){ const keySeed = 'free_trial|' + device + '|' + seed; const msg = [lic.isFreeTrial || false, lic.uses_remaining || 0, lic.redeemedAt || 0, lic.createdAt || 0].join('|'); return await computeHmac(keySeed, msg); } // 对于付费 license,使用 codeHash const keySeed = (lic.codeHash || '') + '|' + device + '|' + seed; const msg = [lic.codeHash || '', lic.uses_remaining || 0, lic.redeemedAt || 0].join('|'); return await computeHmac(keySeed, msg); }catch(e){ return null; } } async function saveLicense(obj){ try{ // 优先使用 AES-GCM 加密 const enc = await licenseEncrypt(obj); if(!enc) return false; try{ localStorage.setItem(LS_KEY, enc); }catch(e){} try{ gmSet(LS_KEY, enc); }catch(e){} return true; }catch(e){ return false; } } async function loadLicenseSync(){ try{ const local = (()=>{ try{ return localStorage.getItem(LS_KEY); }catch(e){ return null; } })(); const gm = gmGet(LS_KEY); if(local && gm && local !== gm){ // 双副本不一致 -> 标记为需要同步,不直接清除(云存储优先) console.log('[学习通助手] 检测到本地存储不一致,将从云端恢复'); return { needsSync: true }; } let enc = local || gm; // 如果主存储为空,尝试读取备用存储 if(!enc){ try{ const fallback = localStorage.getItem(LS_KEY + '_fallback'); if(fallback){ console.log('[学习通助手] 从备用存储读取license'); // 备用存储是JSON格式,不是加密格式 const lic = JSON.parse(fallback); return lic; } }catch(e){ console.warn('[学习通助手] 读取备用存储失败:', e); } } if(!enc) return null; // 使用 AES-GCM 解密 let lic = await licenseDecrypt(enc); if(!lic){ // 解码失败 -> 数据损坏,标记为需要同步(云存储优先) console.log('[学习通助手] License 数据损坏,将从云端恢复'); return { needsSync: true }; } return lic; }catch(e){ return null; } } async function loadLicense(){ const t = await loadLicenseSync(); if(!t) return null; if(t.tampered) return { tampered:true }; // 本地数据需要同步(不一致或损坏) if(t.needsSync){ console.log('[学习通助手] 本地存储不一致,将从云端恢复'); // 后台异步恢复,不阻塞 (async () => { try { const deviceFingerprint = await computeDeviceFingerprint(); console.log('[学习通助手] 设备指纹已生成:', deviceFingerprint.slice(0, 16) + '...'); } catch(e) { console.warn('[学习通助手] 云端恢复失败:', e); } })(); return null; } const expected = await computeSeal(t); if(!expected || expected !== (t.seal || '')){ // 关键修复:如果是免费试用的 license,即使 seal 验证失败,也尝试使用(防止脚本更新导致验证失败) if(t.isFreeTrial && typeof t.uses_remaining === 'number' && t.uses_remaining >= 0){ console.log('[学习通助手] 免费试用 license seal 验证失败,但数据可用,重新计算 seal 并保存'); try { t.seal = await computeSeal(t); await saveLicense(t); return t; } catch(e) { console.warn('[学习通助手] 重新保存免费试用 license 失败:', e); return t; // 仍然返回,即使保存失败 } } // 普通 license 的处理 console.log('[学习通助手] License seal 校验失败,尝试从备用存储读取'); try { const fallback = localStorage.getItem(LS_KEY + '_fallback'); if(fallback) { const fallbackLic = JSON.parse(fallback); // 验证备用存储的seal const fallbackSeal = await computeSeal(fallbackLic); if(fallbackSeal && fallbackSeal === fallbackLic.seal) { return fallbackLic; } } } catch(e) { console.warn('[学习通助手] 备用存储读取失败:', e); } return null; } // 离线模式 - 无服务端同步 // 本地验证完成,无需服务端交互 return t; } function clearLicense(){ try{ localStorage.removeItem(LS_KEY); localStorage.removeItem(TAMPER_FLAG_KEY); try{ gmSet(LS_KEY, ''); gmSet(TAMPER_FLAG_KEY, '0'); }catch(e){} }catch(e){} } async function redeemCode(codePlain){ if(!codePlain) return { ok:false, message:'empty' }; try{ const normalized = codePlain.trim().toUpperCase(); // 格式验证 if(normalized.length < 4 || normalized.length > 64){ return { ok:false, message:'invalid_format' }; } // 检查是否包含非法字符 if(!/^[A-Z0-9\-]+$/.test(normalized)){ return { ok:false, message:'invalid_chars' }; } const keyHex = await sha256hex(normalized); const payload = ENCRYPTED_PAYLOADS[keyHex]; if(!payload) return { ok:false, message:'invalid code' }; const codeKey = 'code:' + keyHex; // 同一张卡密不能重复兑换 if(isRedeemedKey(codeKey)) return { ok:false, message:'already_redeemed' }; const derived = await deriveKey(normalized, payload.salt, payload.iter); const apiKey = await decryptCipher(payload.cipher, derived); // 验证解密后的API Key格式 if(apiKey && apiKey.length < 8){ return { ok:false, message:'decrypt_failed' }; } // 累加现有license次数(无论是否同一卡密) let previousRemaining = 0; let previousTier = 0; let previousCardHashes = []; try{ const existingLic = await loadLicense(); if(existingLic && existingLic.uses_remaining){ previousRemaining = existingLic.uses_remaining; previousTier = existingLic.tier || 0; previousCardHashes = existingLic.cardHashes || [existingLic.codeHash]; console.log(`[学习通助手] 累加前剩余: ${previousRemaining}, 新卡密次数: ${payload.uses}`); } }catch(e){ console.warn('[学习通助手] 读取旧license失败:', e); } // 累加次数 const totalRemaining = previousRemaining + payload.uses; const totalTier = Math.max(previousTier, payload.uses); const allCardHashes = [...new Set([...previousCardHashes, keyHex])]; const licenseObj = { codeHash: keyHex, codePlain: normalized, cardHashes: allCardHashes, apiKey: apiKey, uses_remaining: totalRemaining, tier: totalTier, redeemedAt: Date.now() }; licenseObj.seal = await computeSeal(licenseObj); const ok = saveLicense(licenseObj); if(!ok) return { ok:false, message:'save_failed' }; try{ markRedeemedKey(codeKey); }catch(e){} return { ok:true, uses_remaining: totalRemaining }; }catch(e){ return { ok:false, message:'redeem error' }; } } // 方案C HMAC 校验(与 Python 端一致) const SCHEME_C_HMAC_SECRET = 'xuetong_card_hmac_secret_2024'; const INVITE_CODE_HMAC_SECRET = 'xuetong_invite_hmac_secret_2024'; async function computeSchemeCHmac(codePart, uses) { const msg = `${codePart}:${uses}`; try { const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(SCHEME_C_HMAC_SECRET), {name:'HMAC', hash:'SHA-256'}, false, ['sign']); const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(msg)); // 取前4字节,转16进制大写 const bytes = new Uint8Array(sig); let hex = ''; for(let i=0; i<4; i++) hex += bytes[i].toString(16).padStart(2,'0').toUpperCase(); return hex; } catch(e) { // 降级方案:使用简单哈希 return simpleHash(msg + SCHEME_C_HMAC_SECRET).slice(0, 8); } } async function verifySchemeCCode(code) { const segs = code.toUpperCase().split('-'); if(segs.length !== 6) return null; const codePart = segs.slice(0, 4).join('-'); let uses; try { uses = parseInt(segs[4], 16); } catch(e) { return null; } if(isNaN(uses)) return null; const expectedHmac = await computeSchemeCHmac(codePart, uses); if(segs[5] !== expectedHmac) return null; return { codePart, uses }; } function simpleHash(str) { let hash = 0; for(let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(16).padStart(8, '0').toUpperCase(); } async function computeInviteHmac(codePart) { const msg = `invite:${codePart}:${INVITE_CODE_HMAC_SECRET}`; try { const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(INVITE_CODE_HMAC_SECRET), {name:'HMAC', hash:'SHA-256'}, false, ['sign']); const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(msg)); const bytes = new Uint8Array(sig); let hex = ''; for(let i=0; i<4; i++) hex += bytes[i].toString(16).padStart(2,'0').toUpperCase(); return hex; } catch(e) { return simpleHash(msg).slice(0, 8); } } async function verifyInviteCode(code) { if(!code || typeof code !== 'string') return null; const normalized = code.trim().toUpperCase(); if(!/^[A-Z0-9]{6}-[A-F0-9]{8}$/.test(normalized)){ return null; } const segs = normalized.split('-'); if(segs.length !== 2) return null; const codePart = segs[0]; const hmac = segs[1]; if(codePart.length !== 6) return null; if(hmac.length !== 8) return null; const expectedHmac = await computeInviteHmac(codePart); if(hmac !== expectedHmac) return null; return { codePart, bonus: 20, hmac: expectedHmac }; } function generateInviteCodePart() { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; try { let code = ''; const cryptoApi = globalThis.crypto || window.crypto; const array = new Uint32Array(6); if (cryptoApi && typeof cryptoApi.getRandomValues === 'function') { try { cryptoApi.getRandomValues(array); for(let i = 0; i < 6; i++) code += chars[array[i] % chars.length]; return code; } catch (e) {} } } catch (e) {} let fallback = ''; const seed = String(Date.now()) + String(Math.random()) + String(Math.random()); for (let i = 0; i < 6; i++) { const charIndex = Math.abs((seed.charCodeAt(i % seed.length) || 0) + i * 17) % chars.length; fallback += chars[charIndex]; } return fallback && fallback.length === 6 ? fallback : 'AB1234'; } async function redeemSelfContained(codeFull){ if(!codeFull) return { ok:false, message:'empty' }; try{ const full = String(codeFull).trim(); // 优先尝试方案C格式:XXXX-XXXX-XXXX-XXXX-NNNN-HHHHHHHH const schemeCResult = await verifySchemeCCode(full); if(schemeCResult){ const { codePart, uses } = schemeCResult; const keyHex = await sha256hex(codePart); // 检查当前设备是否已兑换(允许换卡密:如果已兑换,先清除旧license) const redeemedKey = 'redeemed:' + keyHex; let redeemedDevices = JSON.parse(localStorage.getItem(redeemedKey) || '[]'); // 使用同步方式获取设备ID(优先从缓存读取) let deviceFingerprint = localStorage.getItem(PERSISTENT_DEVICE_ID_KEY) || gmGet(PERSISTENT_DEVICE_ID_KEY); if(!deviceFingerprint || deviceFingerprint.length !== 64) { // 没有缓存,使用简单降级方案 deviceFingerprint = 'device_' + Date.now().toString(36) + '_' + Math.random().toString(36).substr(2, 8); deviceFingerprint = deviceFingerprint.padEnd(64, '0').slice(0, 64); try { localStorage.setItem(PERSISTENT_DEVICE_ID_KEY, deviceFingerprint); } catch(e) {} } const currentDeviceRedeemed = redeemedDevices.some(d => d.device === deviceFingerprint); // 无论是否同一卡密,都累加现有license次数 let previousRemaining = 0; let previousTier = 0; let previousCardHashes = []; try{ const existingLic = await loadLicense(); if(existingLic && existingLic.uses_remaining){ previousRemaining = existingLic.uses_remaining; previousTier = existingLic.tier || 0; previousCardHashes = existingLic.cardHashes || [existingLic.codeHash]; console.log(`[学习通助手] 累加前剩余: ${previousRemaining}, 新卡密次数: ${uses}`); } }catch(e){ console.warn('[学习通助手] 读取旧license失败:', e); } // 换设备恢复:如果本地无license,从云端获取累计次数 if(previousRemaining === 0){ try{ const cloudData = await syncGetCardRemaining(keyHex); if(cloudData && cloudData.remaining > 0){ previousRemaining = cloudData.remaining; previousCardHashes = cloudData.cardHashes || []; console.log(`[学习通助手] 从云端恢复累计次数: ${previousRemaining}`); } }catch(e){ console.warn('[学习通助手] 云端恢复失败,使用本地数据:', e); } } // 如果当前卡密已兑换过,清除该卡的兑换记录(允许重新兑换) if(currentDeviceRedeemed){ redeemedDevices = redeemedDevices.filter(d => d.device !== deviceFingerprint); localStorage.setItem(redeemedKey, JSON.stringify(redeemedDevices)); } // 累加次数 const totalRemaining = previousRemaining + uses; const totalTier = Math.max(previousTier, uses); // 合并卡密hash列表 const allCardHashes = [...new Set([...previousCardHashes, keyHex])]; // 保存 license 到本地(累加模式) const licenseObj = { codeHash: keyHex, codePlain: codePart, cardHashes: allCardHashes, apiKey: null, uses_remaining: totalRemaining, tier: totalTier, redeemedAt: Date.now() }; // 先计算seal再保存,确保license完整 try { const seal = await computeSeal(licenseObj); if(seal) { licenseObj.seal = seal; } } catch(e) { console.warn('[学习通助手] seal计算失败:', e); } const ok = saveLicense(licenseObj); if(ok){ redeemedDevices.push({ device: deviceFingerprint, time: Date.now() }); localStorage.setItem(redeemedKey, JSON.stringify(redeemedDevices)); markRedeemedWithDevice('code:' + keyHex, deviceFingerprint); // 云端同步激活(完全异步,不阻塞用户) (async () => { try{ const cloudResult = await syncActivateCard(keyHex, codePart, deviceFingerprint, totalRemaining, totalTier); if(cloudResult.success){ console.log('[学习通助手] 卡密已同步到云端,累计剩余: ' + cloudResult.remaining); }else{ console.warn('[学习通助手] 云端同步失败,使用本地数据:', cloudResult.message); } }catch(e){ console.warn('[学习通助手] 云端同步异常:', e); } })(); } return { ok:true, uses_remaining: totalRemaining, tier: totalTier }; } // 回退到自包含格式:CODE::base64url(payload) const selfContainedRegex = /^([A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4})::([A-Za-z0-9_-]+)$/i; const selfContainedMatch = full.match(selfContainedRegex); if(!selfContainedMatch){ return { ok:false, message:'invalid_format' }; } const codePart = selfContainedMatch[1].toUpperCase(); const payloadB64 = selfContainedMatch[2]; // 解析 payload (urlsafe base64 -> JSON) let payload; try{ const b64 = payloadB64.replace(/-/g, '+').replace(/_/g, '/'); const jsonStr = atob(b64); payload = JSON.parse(jsonStr); }catch(e){ return { ok:false, message:'invalid_payload' }; } // 验证必要字段 if(!payload.salt || !payload.iter || !payload.cipher || typeof payload.uses !== 'number'){ return { ok:false, message:'invalid_payload' }; } // PBKDF2 密钥派生 const derived = await deriveKey(codePart, payload.salt, payload.iter); // AES-GCM 解密 let decrypted; try{ decrypted = await decryptCipher(payload.cipher, derived); }catch(e){ return { ok:false, message:'decrypt_failed' }; } // 解析解密后的 payload let decryptedPayload; try{ decryptedPayload = JSON.parse(decrypted); }catch(e){ return { ok:false, message:'invalid_decrypted' }; } const uses = typeof decryptedPayload.uses === 'number' ? decryptedPayload.uses : payload.uses; const tier = typeof decryptedPayload.tier === 'number' ? decryptedPayload.tier : payload.tier; // 检查当前设备是否已兑换(允许分享给其他设备使用) const keyHex = await sha256hex(codePart); const redeemedKey2 = 'redeemed:' + keyHex; const redeemedDevices2 = JSON.parse(localStorage.getItem(redeemedKey2) || '[]'); // 异步获取设备指纹,不阻塞本地兑换 let deviceFingerprint = 'unknown'; try { deviceFingerprint = await computeDeviceFingerprint(); } catch (fpErr) { deviceFingerprint = 'device_' + Date.now().toString(36); } const currentDeviceRedeemed2 = redeemedDevices2.some(d => d.device === deviceFingerprint); // 无论是否同一卡密,都累加现有license次数 let previousRemaining = 0; let previousTier = 0; let previousCardHashes = []; try{ const existingLic = await loadLicense(); if(existingLic && existingLic.uses_remaining){ previousRemaining = existingLic.uses_remaining; previousTier = existingLic.tier || 0; previousCardHashes = existingLic.cardHashes || [existingLic.codeHash]; console.log(`[学习通助手] 累加前剩余: ${previousRemaining}, 新卡密次数: ${uses}`); } }catch(e){ console.warn('[学习通助手] 读取旧license失败:', e); } // 换设备恢复:如果本地无license,从云端获取累计次数 if(previousRemaining === 0){ try{ const cloudData = await syncGetCardRemaining(keyHex); if(cloudData && cloudData.remaining > 0){ previousRemaining = cloudData.remaining; previousCardHashes = cloudData.cardHashes || []; console.log(`[学习通助手] 从云端恢复累计次数: ${previousRemaining}`); } }catch(e){ console.warn('[学习通助手] 云端恢复失败,使用本地数据:', e); } } // 如果当前卡密已兑换过,清除该卡的兑换记录(允许重新兑换) if(currentDeviceRedeemed2){ redeemedDevices2 = redeemedDevices2.filter(d => d.device !== deviceFingerprint); localStorage.setItem(redeemedKey2, JSON.stringify(redeemedDevices2)); } // 累加次数 const totalRemaining = previousRemaining + uses; const totalTier = Math.max(previousTier, tier); const allCardHashes = [...new Set([...previousCardHashes, keyHex])]; // 保存 license 到本地(累加模式) const licenseObj2 = { codeHash: keyHex, codePlain: codePart, cardHashes: allCardHashes, apiKey: null, uses_remaining: totalRemaining, tier: totalTier, redeemedAt: Date.now() }; licenseObj2.seal = await computeSeal(licenseObj2); const ok2 = saveLicense(licenseObj2); if(ok2){ redeemedDevices2.push({ device: deviceFingerprint, time: Date.now() }); localStorage.setItem(redeemedKey2, JSON.stringify(redeemedDevices2)); markRedeemedWithDevice('code:' + keyHex, deviceFingerprint); // 云端同步激活(完全异步,不阻塞用户) (async () => { try{ const cloudResult = await syncActivateCard(keyHex, codePart, deviceFingerprint, totalRemaining, totalTier); if(cloudResult.success){ console.log('[学习通助手] 卡密已同步到云端,累计剩余: ' + cloudResult.remaining); }else{ console.warn('[学习通助手] 云端同步失败,使用本地数据:', cloudResult.message); } }catch(e){ console.warn('[学习通助手] 云端同步异常:', e); } })(); } return { ok:true, uses_remaining: totalRemaining, tier: totalTier }; }catch(e){ return { ok:false, message: String(e) }; } } // ---- 新用户免费 20 次逻辑(24小时内,防篡改)---- function ensureFirstSeen(){ const k = '_client_first_seen_v1'; try{ let v = localStorage.getItem(k); if(!v){ v = String(Date.now()); localStorage.setItem(k, v); } return Number(v); } catch(e){ return Date.now(); } } function isNewUser(){ try{ const f = Number(localStorage.getItem('_client_first_seen_v1')||0); if(!f) return false; return (Date.now() - f) < 24*3600*1000; } catch(e){ return false; } } function getFreeRemaining(){ try{ const v = localStorage.getItem('_newuser_free_remaining'); return v ? Number(v) : null; }catch(e){ return null; } } function setFreeRemaining(n){ try{ if(n > 0){ localStorage.setItem('_newuser_free_remaining', String(n)); }else{ localStorage.removeItem('_newuser_free_remaining'); } }catch(e){} } async function checkToken(){ ensureFirstSeen(); const lic = await loadLicense(); if(!lic) { // 关键修复:对所有没有 license 的用户进行设备指纹检查 // 确保设备一旦使用过免费试用就不能再次获取,无论脚本是否更新 const deviceId = await computeDeviceFingerprint(); const usedKey = '_free_trial_used_' + deviceId.slice(0, 16); // 检查该设备是否已使用过免费试用(即使次数显示为null,也要验证设备指纹) if(localStorage.getItem(usedKey)){ // 设备已使用过免费试用,即使本地次数被清除,也不再重新分配 console.log('[学习通助手] 设备已使用过免费试用,不再重新分配'); return { ok:false, needsRedeem: true }; } // 检查本地剩余免费次数 let remaining = getFreeRemaining(); if(remaining === null){ // 首次使用,分配20次免费试用 - 直接创建正式 license const newLic = { uses_remaining: 20, isFreeTrial: true, createdAt: Date.now(), lastUsedAt: Date.now(), redeemedAt: Date.now() }; newLic.seal = await computeSeal(newLic); await saveLicense(newLic); return { ok:true, uses_remaining: 20, isFreeTrial: true }; } if(remaining > 0){ // 如果有遗留的临时次数,转为正式 license const newLic = { uses_remaining: remaining, isFreeTrial: true, createdAt: Date.now(), lastUsedAt: Date.now(), redeemedAt: Date.now() }; newLic.seal = await computeSeal(newLic); await saveLicense(newLic); setFreeRemaining(0); // 清除临时标记 return { ok:true, uses_remaining: remaining, isFreeTrial: true }; } // 免费试用已用完,标记设备已使用 try{ localStorage.setItem(usedKey, '1'); }catch(e){} // 提示需要重新兑换 return { ok:false, needsRedeem: true }; } if(lic.tampered) return { ok:false, tampered:true }; // 支持邀请码-only license(isInviteOnly标记) const label = lic.isInviteOnly ? ' (邀请码奖励)' : ''; return { ok:true, uses_remaining: lic.uses_remaining, isFreeTrial: !!lic.isFreeTrial, label }; } async function consumeOne(){ let lic = await loadLicense(); // 关键修复:免费试用次数(移除 isNewUser 限制,始终进行设备指纹校验) if(!lic){ const deviceId = await computeDeviceFingerprint(); const usedKey = '_free_trial_used_' + deviceId.slice(0, 16); // 检查该设备是否已用完免费试用(即使本地次数被清除) if(localStorage.getItem(usedKey)){ return { ok:false, message:'免费试用已用完(设备已使用)' }; } let remaining = getFreeRemaining(); if(remaining === null){ remaining = 20; } // 关键修复:创建正式的 license 来保存免费试用次数,防止脚本更新后重置 if(remaining > 0){ remaining = Math.max(0, remaining - 1); // 创建正式的 license 对象保存次数 lic = { uses_remaining: remaining, isFreeTrial: true, createdAt: Date.now(), lastUsedAt: Date.now(), redeemedAt: Date.now() }; lic.seal = await computeSeal(lic); await saveLicense(lic); // 移除临时的免费试用标记,防止重复创建 setFreeRemaining(0); // 用完20次后,标记该设备已使用 if(remaining <= 0){ try{ localStorage.setItem(usedKey, '1'); }catch(e){} } // 触发UI更新 try{ EventBus.emit('license:updated', { uses_remaining: remaining, isFreeTrial: true }); }catch(e){} return { ok:true, uses_remaining: remaining, isFreeTrial: true }; } return { ok:false, message:'exhausted' }; } if(lic.tampered){ return { ok:false, message:'tampered' }; } if(typeof lic.uses_remaining !== 'number' || lic.uses_remaining <= 0){ return { ok:false, message:'exhausted' }; } // 优化策略:先本地扣减(立即响应UI),后云端实际扣减(后台异步) // 1. 先本地扣减,立即更新UI,用户体验无延迟(不持久化,以云端为准) const originalRemaining = lic.uses_remaining; lic.uses_remaining = Math.max(0, lic.uses_remaining - 1); lic.lastUsedAt = Date.now(); lic.seal = await computeSeal(lic); // 触发UI更新(立即响应,不等待云端) try{ EventBus.emit('license:updated', { uses_remaining: lic.uses_remaining, isFreeTrial: false }); }catch(e){} // 2. 后台异步到云端实际扣减(云端是唯一扣减源,防双重扣减) if(lic.codeHash){ (async () => { try{ const deviceFingerprint = await computeDeviceFingerprint(); // 修复:使用 syncConsumeUsage 而不是 verifyAnswer 来真正扣减次数 const cloudResult = await syncConsumeUsage(lic.codeHash, deviceFingerprint, 1); if(cloudResult.isNetworkError){ // 网络/代理错误,回滚本地扣减,加入离线队列 lic.uses_remaining = originalRemaining; lic.seal = await computeSeal(lic); await saveLicense(lic); EventBus.emit('license:updated', { uses_remaining: originalRemaining, isFreeTrial: false }); saveToOfflineQueue({ type: 'consume', cardHash: lic.codeHash, deviceFingerprint: deviceFingerprint, count: 1, currentRemaining: originalRemaining }); }else if(!cloudResult.success && cloudResult.has_remaining === false){ // 云端明确返回次数已用完,标记本地授权无效 console.warn('[学习通助手] 云端验证:答题次数已用完'); lic.uses_remaining = 0; lic.seal = await computeSeal(lic); await saveLicense(lic); EventBus.emit('license:updated', { uses_remaining: 0, isFreeTrial: false, exhausted: true }); }else if(cloudResult.success){ // 云端扣减成功,以云端返回的真实剩余次数为准同步本地 if(cloudResult.remaining !== lic.uses_remaining){ console.log(`[学习通助手] 云端同步:本地 ${lic.uses_remaining} → 云端 ${cloudResult.remaining}`); } lic.uses_remaining = cloudResult.remaining; lic.seal = await computeSeal(lic); await saveLicense(lic); EventBus.emit('license:updated', { uses_remaining: cloudResult.remaining, isFreeTrial: false }); } }catch(e){ // 异常回滚本地扣减 lic.uses_remaining = originalRemaining; lic.seal = await computeSeal(lic); await saveLicense(lic); EventBus.emit('license:updated', { uses_remaining: originalRemaining, isFreeTrial: false }); saveToOfflineQueue({ type: 'consume', cardHash: lic.codeHash, deviceFingerprint: await computeDeviceFingerprint(), count: 1, currentRemaining: originalRemaining }); } })(); }else{ // 没有codeHash,直接保存本地扣减后的值 await saveLicense(lic); } return { ok:true, uses_remaining: lic.uses_remaining }; } // 离线模式 - 无远程同步函数 async function tryRemoteSync(codeHash, delta, clientRemaining, deviceId){ return { ok:false, message: 'no_remote' }; } async function tryRemoteSyncRemaining(codeHash, newRemaining){ return null; } async function tryRemoteGetRemaining(codeHash){ return null; } async function getApiKey(){ const lic = await loadLicense(); if(!lic || lic.tampered) return null; return lic.apiKey || null; } async function addReferralBonus(count = 20){ try{ let lic = await loadLicense(); // 如果没有license,创建一个只包含邀请码奖励的license if(!lic){ const inviteCodePart = 'INVITE_' + Date.now().toString(36); const codeHash = await sha256hex(inviteCodePart); lic = { codeHash: codeHash, codePlain: inviteCodePart, cardHashes: [], apiKey: null, uses_remaining: 0, tier: 0, redeemedAt: Date.now(), isInviteOnly: true }; } if(lic.tampered) return { ok:false, message:'tampered' }; // 累加邀请码奖励次数 lic.uses_remaining = Number(lic.uses_remaining || 0) + Number(count || 0); lic.lastReferralAt = Date.now(); lic.seal = await computeSeal(lic); const ok = await saveLicense(lic); if(!ok) return { ok:false, message:'save_failed' }; // 云端同步邀请码奖励 if(lic.codeHash){ try{ const deviceFingerprint = await computeDeviceFingerprint(); // 尝试同步到云端(如果云端支持邀请码同步) try { await syncRedeemInviteCode(lic.codeHash, deviceFingerprint, count); console.log('[学习通助手] 邀请码奖励已同步到云端'); } catch(e) { console.warn('[学习通助手] 云端同步邀请码奖励失败,使用本地数据:', e); } }catch(e){ console.warn('[学习通助手] 云端同步异常:', e); } } return { ok:true, uses_remaining: lic.uses_remaining }; }catch(e){ return { ok:false, message: String(e) }; } } // 离线模式 - 本地用户识别 async function identifyUser() { try { const localUserId = localStorage.getItem('_local_user_id_v3'); if (localUserId) { return { ok: true, user_id: localUserId, is_new: false, offline: true }; } const newId = Math.random().toString(36).substr(2, 16); localStorage.setItem('_local_user_id_v3', newId); return { ok: true, user_id: newId, is_new: true, offline: true }; } catch (e) { console.error('[学习通助手] 用户识别错误:', e); const fallbackId = Math.random().toString(36).substr(2, 16); return { ok: false, user_id: fallbackId, error: e.message }; } } return { redeemCode, redeemSelfContained, checkToken, consumeOne, getApiKey, addReferralBonus, clearLicense, initRemoteServer, identifyUser, computeDeviceFingerprint, computeEnhancedFingerprint, computeInviteHmac, generateInviteCodePart, verifyInviteCode, syncActivateCard, syncConsumeUsage, verifyAnswer, syncGetCardRemaining, syncRedeemInviteCode, syncRefreshLocalLicense, initOfflineSync, syncOfflineQueue, getOfflineQueue, clearOfflineQueue, syncRegisterInviteCode, syncGetDeviceInviteCode }; })(); // --- BEGIN: 智能延迟函数(防检测 + 保体验)--- async function smartHumanDelay(status) { // 1. 新用户免费试用:不加延迟,让新手爽到 if (status && status.isFreeTrial) return; // 2. 连续答题检测:用 30s 作为手动/自动区分阈值 const now = Date.now(); const timeSinceLast = now - (globalThis._lastAnswerTime || 0); // 手动答题(上次答题 > 30s):不加延迟,保证交互体验 if (timeSinceLast > 30000) { globalThis._lastAnswerTime = now; return; } // 非手动但非连续:给出小随机延迟 0.5~2s,用于常规节拍 if (timeSinceLast > 10000) { globalThis._lastAnswerTime = now; const miniDelay = Math.floor(Math.random() * 1500) + 500; await new Promise(resolve => setTimeout(resolve, miniDelay)); return; } // 3. 连续答题 + 剩余次数 < 10:轻微延迟 3~10秒 if (status && typeof status.uses_remaining === 'number' && status.uses_remaining < 10) { const lightDelay = Math.floor(Math.random() * 7000) + 3000; await new Promise(resolve => setTimeout(resolve, lightDelay)); globalThis._lastAnswerTime = now; return; } // 4. 连续答题 + 剩余充足:批量刷课延迟 30~120秒(更保守以防检测) const heavyDelay = Math.floor(Math.random() * 90000) + 30000; await new Promise(resolve => setTimeout(resolve, heavyDelay)); globalThis._lastAnswerTime = now; } // --- BEGIN: AI API 配置与调用(加密存储 API Key)--- const ENCRYPTED_API_KEY = { salt: "2WjTDoSmbznHW4LbpB+67w==", iter: 200000, cipher: "IxZYAiBSXb2oKn7vFIA2c4HHYcv58WT1y67HfvhA+htAI3+VHsKZejk9mUBpCqPOml37KYnxNkosIqf8EJWL" }; const ENCRYPTED_DEEPSEEK_API_URL = { salt: "+JcPXh9/vaxI16Kw4LFDbg==", iter: 200000, cipher: "fH2a8T9TC6Jn6wcjmulegHpdR3LeIDbvuUQH7NIF4gb1o/Q0D85/QRE6fusBm0GATR1gWsRjqjZdHxdrEiQjtH3Pdl15zQ==" }; let DEEPSEEK_API_URL = null; async function getDecryptedApiUrl() { try { function b64ToUint8ArrayLocal(b64) { try { if (typeof atob === 'function') return Uint8Array.from(atob(b64), c => c.charCodeAt(0)); if (typeof Buffer !== 'undefined') return Uint8Array.from(Buffer.from(b64, 'base64')); throw new Error('No base64 decoder'); } catch (e) { if (typeof Buffer !== 'undefined') return Uint8Array.from(Buffer.from(b64, 'base64')); throw e; } } // obtain base key material from configuration (do not hardcode secrets) const baseKey = await _getBaseKeyMaterialFromConfig(); if(!baseKey) return null; const saltArr = b64ToUint8ArrayLocal(ENCRYPTED_DEEPSEEK_API_URL.salt); const salt = saltArr.buffer; const key = await crypto.subtle.deriveKey({name:'PBKDF2', salt, iterations: ENCRYPTED_DEEPSEEK_API_URL.iter, hash:'SHA-256'}, baseKey, {name:'AES-GCM', length:256}, false, ['decrypt']); const data = b64ToUint8ArrayLocal(ENCRYPTED_DEEPSEEK_API_URL.cipher); const iv = data.slice(0,12).buffer; const ct = data.slice(12).buffer; const plain = await crypto.subtle.decrypt({name:'AES-GCM', iv}, key, ct); return new TextDecoder().decode(plain); } catch (e) { console.error('[AI API] 解密接口地址失败', e); return null; } } getDecryptedApiUrl().then(url => { DEEPSEEK_API_URL = url; }); const DEEPSEEK_MODEL = 'deepseek-chat'; async function getDecryptedApiKey() { function b64ToUint8Array(b64) { try { if (typeof atob === 'function') { const bin = atob(b64); return Uint8Array.from(bin, c => c.charCodeAt(0)); } if (typeof Buffer !== 'undefined') { return Uint8Array.from(Buffer.from(b64, 'base64')); } throw new Error('No base64 decoder available'); } catch (e) { if (typeof Buffer !== 'undefined') { return Uint8Array.from(Buffer.from(b64, 'base64')); } throw e; } } try { // obtain base key material from configuration (do not hardcode secrets) const baseKey = await _getBaseKeyMaterialFromConfig(); if(!baseKey) return null; const saltArr = b64ToUint8Array(ENCRYPTED_API_KEY.salt); const salt = saltArr.buffer; const key = await crypto.subtle.deriveKey({name:'PBKDF2', salt, iterations: ENCRYPTED_API_KEY.iter, hash:'SHA-256'}, baseKey, {name:'AES-GCM', length:256}, false, ['decrypt']); const data = b64ToUint8Array(ENCRYPTED_API_KEY.cipher); const iv = data.slice(0,12).buffer; const ct = data.slice(12).buffer; const plain = await crypto.subtle.decrypt({name:'AES-GCM', iv}, key, ct); return new TextDecoder().decode(plain); } catch(e) { console.error('[AI API] 解密失败', e); return null; } } let DEEPSEEK_API_KEY = null; getDecryptedApiKey().then(key => { DEEPSEEK_API_KEY = key; }); function deepseekChat(messages, options = {}) { return new Promise((resolve, reject) => { try { if (!DEEPSEEK_API_KEY) { reject(new Error('API Key 尚未解密完成,请稍后重试')); return; } if (!DEEPSEEK_API_URL) { reject(new Error('API URL 尚未解密完成,请稍后重试')); return; } GM_xmlhttpRequest({ method: 'POST', url: DEEPSEEK_API_URL, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DEEPSEEK_API_KEY}` }, data: JSON.stringify({ model: DEEPSEEK_MODEL, messages, ...options }), timeout: 60000, onload: (r) => { try { // 验证 HTTP 状态码 if (r.status < 200 || r.status >= 300) { reject(new Error(`HTTP ${r.status}: ${String(r.responseText || '').slice(0,200)}`)); return; } // 验证 Content-Type const contentType = r.responseHeaders?.toLowerCase() || ''; if (contentType && !contentType.includes('application/json')) { reject(new Error(`非 JSON 响应: ${contentType}`)); return; } // 解析 JSON 响应 let result; try { result = JSON.parse(r.responseText); } catch (e) { reject(new Error(`JSON 解析失败: ${e.message}`)); return; } // 验证响应结构 if (!result || typeof result !== 'object') { reject(new Error('响应格式无效')); return; } // 检查 API 错误 if (result.error) { const errMsg = typeof result.error === 'string' ? result.error : (result.error.message || JSON.stringify(result.error)); reject(new Error(`API 错误: ${errMsg}`)); return; } // 验证必要字段 if (!result.choices || !Array.isArray(result.choices) || result.choices.length === 0) { reject(new Error('响应缺少 choices 字段')); return; } resolve(result); } catch (e) { reject(new Error(`响应处理异常: ${e.message}`)); } }, onerror: () => reject(new Error('Network error')), ontimeout: () => reject(new Error('Timeout')) }); } catch (err) { reject(err); } }); } // --- END: AI API 逻辑 --- async function deepseekChatWithLicense(messages, options = {}){ console.log('[学习通助手] deepseekChatWithLicense 开始执行'); const status = await ClientLicense.checkToken(); console.log('[学习通助手] checkToken 返回:', status); if(!status.ok){ if(status.tampered) throw new Error('授权异常:检测到篡改'); throw new Error('未授权或卡密无效'); } if(typeof status.uses_remaining === 'number' && status.uses_remaining <= 0) throw new Error('卡密已用尽'); if(typeof deepseekChat !== 'function') throw new Error('deepseekChat 未就绪'); // 智能延迟:只在批量刷课时生效,不影响手动答题体验 await smartHumanDelay(status); const res = await deepseekChat(messages, options); // 【关键修复】不要在这里扣次!答案还没有填写到页面上 // 扣次应该在答题流程成功填写答案之后进行(见 answerFlow 中的 consumeOne) // deepseekChatWithLicense 只负责获取 AI 答案,不负责次数扣减 return res; } // --- BEGIN: 适配器、调度器与自动答题骨架 --- const AdapterRegistry = { adapters: {}, register(name, impl){ this.adapters[name] = impl; }, get(name){ return this.adapters[name] || null; }, detect(){ const host = location.hostname || ''; // 简单匹配:优先使用已注册的 host 名称 for(const k of Object.keys(this.adapters)){ try{ if(host.includes(k)) return this.adapters[k]; }catch(e){} } return null; } }; class TaskScheduler { constructor(){ this.queue = []; this.running = false; } schedule(fn){ if(typeof fn !== 'function') return; this.queue.push(fn); this.run(); } async run(){ if(this.running) return; this.running = true; while(this.queue.length){ const fn = this.queue.shift(); try{ await fn(); }catch(e){ console.error('[Scheduler] task error', e); } } this.running = false; } } const Scheduler = new TaskScheduler(); // 页面题目解析器(适配常见选择器) function parsePageQuestions(){ const out = []; try{ let qEls = Array.from(document.querySelectorAll(SELECTORS.CX_QUESTION_ZJ || '.TiMu')); if(!qEls.length) qEls = Array.from(document.querySelectorAll(SELECTORS.CX_QUESTION_ZY_KS || '.questionLi')); if(!qEls.length) qEls = Array.from(document.querySelectorAll(SELECTORS.ZHS_QUESTION || '.examPaper_subject')); for(const qEl of qEls){ try{ const textNode = qEl.querySelector('.stem, .q-title, .question-title') || qEl; const qText = (textNode && (textNode.innerText || textNode.textContent) || '').trim(); let optEls = Array.from(qEl.querySelectorAll(SELECTORS.CX_OPTION_ZJ || '[class*="before-after"]')); if(!optEls.length) optEls = Array.from(qEl.querySelectorAll(SELECTORS.CX_OPTION_ZY_KS || '.answerBg')); if(!optEls.length) optEls = Array.from(qEl.querySelectorAll(SELECTORS.ZHS_OPTION || '.subject_node .nodeLab')); const options = optEls.map(el => ({ el, text: (el.innerText || el.textContent || '').trim() })); out.push({ qEl, qText, options }); }catch(e){ /* continue */ } } }catch(e){ console.warn('[parsePageQuestions] error', e); } return out; } function safeClick(el){ try{ if(!el) return false; const input = el.querySelector('input[type="radio"], input[type="checkbox"]'); const label = el.querySelector('label'); // 优先使用 input 元素 let targetElement = input || label || el; if(input){ // 第一重:直接设置 input 状态 input.focus(); const isCheckbox = input.type === 'checkbox'; input.checked = isCheckbox ? !input.checked : true; // 第二重:触发完整的事件链 input.dispatchEvent(new Event('focus', { bubbles: true })); input.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); input.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); input.click(); // 第三重:触发 change 和 input 事件 input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('input', { bubbles: true })); // 第四重:更新 React 的 value tracker try { const tracker = input._valueTracker; if (tracker) { tracker.setValue(String(input.checked)); } } catch(e) {} // 第五重:更新父容器状态 const parent = el.closest('li, .option, .answer-item, .question-item'); if (parent) { parent.classList.add('is-checked', 'selected', 'answer-selected'); parent.setAttribute('aria-checked', 'true'); parent.setAttribute('data-checked', 'true'); } // 第六重:更新当前元素状态 el.classList.add('is-checked', 'selected', 'answer-selected'); el.setAttribute('aria-checked', 'true'); } else { // 没有 input 元素,使用通用点击逻辑 // 第一重:触发完整的事件链 targetElement.dispatchEvent(new Event('focus', { bubbles: true })); targetElement.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); targetElement.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); targetElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); targetElement.click(); // 第二重:触发 PointerEvent targetElement.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); targetElement.dispatchEvent(new PointerEvent('pointerup', { bubbles: true })); targetElement.dispatchEvent(new PointerEvent('click', { bubbles: true })); // 第三重:点击 label(如果有) if(label){ label.click(); label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } // 第四重:设置内部 input 状态(如果有) const innerInput = el.querySelector('input[type="radio"], input[type="checkbox"]'); if (innerInput) { innerInput.checked = true; innerInput.dispatchEvent(new Event('change', { bubbles: true })); innerInput.dispatchEvent(new Event('input', { bubbles: true })); } // 第五重:更新元素状态 el.classList.add('is-checked', 'selected', 'answer-selected'); el.setAttribute('aria-checked', 'true'); el.setAttribute('data-checked', 'true'); } return true; }catch(e){ return false; } } // 增强版 safeClick:带验证和重试机制 function safeClickWithVerify(el, maxRetries = 2) { let attempt = 0; while (attempt <= maxRetries) { const result = safeClick(el); if (!result) return false; // 等待事件处理完成 const waitTime = 200 + attempt * 100; const startWait = Date.now(); while (Date.now() - startWait < waitTime) { // 短暂等待 } // 验证是否选中 const input = el.querySelector('input[type="radio"], input[type="checkbox"]'); if (input && input.checked) { return true; } if (el.getAttribute("aria-checked") === "true" || el.classList.contains("is-checked") || el.classList.contains("selected")) { return true; } // 未验证成功,重试 attempt++; if (attempt <= maxRetries) { // 再次强制设置状态 if (input) { input.checked = true; input.dispatchEvent(new Event('change', { bubbles: true })); } el.setAttribute("aria-checked", "true"); el.classList.add("is-checked", "selected"); } } // 所有重试失败,但强制设置状态 const input = el.querySelector('input[type="radio"], input[type="checkbox"]'); if (input) { input.checked = true; } el.setAttribute("aria-checked", "true"); el.classList.add("is-checked", "selected"); return true; } // 自动答题骨架:先做非破坏性尝试(记录、控制台输出、尝试点击最佳选项) // 答题进度保存与恢复 const ANSWER_PROGRESS_KEY = 'autoAnswer_progress'; function saveAnswerProgress(data) { try { localStorage.setItem(ANSWER_PROGRESS_KEY, JSON.stringify(data)); } catch(e) { console.warn('[autoAnswer] 保存进度失败', e); } } function loadAnswerProgress() { try { const data = localStorage.getItem(ANSWER_PROGRESS_KEY); return data ? JSON.parse(data) : null; } catch(e) { return null; } } function clearAnswerProgress() { try { localStorage.removeItem(ANSWER_PROGRESS_KEY); } catch(e) {} } // 优化:页面加载时清除过期的答题进度缓存(超过30分钟视为过期) function cleanupStaleProgress() { try { const progress = loadAnswerProgress(); if (progress && progress.startTime) { const ageMinutes = (Date.now() - progress.startTime) / 60000; if (ageMinutes > 30) { clearAnswerProgress(); console.log('[autoAnswer] 清除过期的答题进度缓存'); } } } catch(e) { console.warn('[autoAnswer] 清理过期进度失败', e); } } // 页面加载时自动清理过期缓存 if (typeof window !== 'undefined') { window.addEventListener('load', cleanupStaleProgress); // 监听页面刷新/关闭事件,清理进度 window.addEventListener('beforeunload', () => { // 只在正常关闭时清理,刷新时保持(让用户可以继续) // 但如果是长时间未操作,应该清理 try { const progress = loadAnswerProgress(); if (progress && progress.startTime) { const ageMinutes = (Date.now() - progress.startTime) / 60000; if (ageMinutes > 5) { clearAnswerProgress(); } } } catch(e) {} }); } // 答题成功率监控和题库自动调整 const AnswerQualityMonitor = (() => { const STORAGE_KEY = 'answer_quality_stats'; let stats = { totalQuestions: 0, answeredQuestions: 0, matchedFromLocalDB: 0, matchedFromSimilar: 0, matchedFromAI: 0, unmatched: 0, recentAnswers: [], questionBankUsage: {}, lastResetTime: Date.now() }; function loadStats() { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); if (parsed.lastResetTime && Date.now() - parsed.lastResetTime > 7 * 24 * 60 * 60 * 1000) { return { ...stats, lastResetTime: Date.now() }; } stats = { ...stats, ...parsed }; } } catch (e) {} } function saveStats() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(stats)); } catch (e) {} } loadStats(); return { recordAnswer(source, success) { stats.totalQuestions++; stats.answeredQuestions++; if (source === 'local_db') stats.matchedFromLocalDB++; else if (source === 'similar') stats.matchedFromSimilar++; else if (source === 'ai') stats.matchedFromAI++; else stats.unmatched++; stats.recentAnswers.push({ source, success, timestamp: Date.now() }); if (stats.recentAnswers.length > 100) { stats.recentAnswers = stats.recentAnswers.slice(-50); } saveStats(); }, recordQuestionBankUsage(bankName) { if (!stats.questionBankUsage[bankName]) { stats.questionBankUsage[bankName] = { used: 0, success: 0 }; } stats.questionBankUsage[bankName].used++; saveStats(); }, getStats() { return { ...stats }; }, getSuccessRate() { if (stats.answeredQuestions === 0) return 0; return (stats.answeredQuestions - stats.unmatched) / stats.answeredQuestions; }, getRecentSuccessRate(windowSize = 20) { const recent = stats.recentAnswers.slice(-windowSize); if (recent.length === 0) return 0; const matched = recent.filter(a => a.source !== 'unmatched').length; return matched / recent.length; }, getBestQuestionBank() { let bestBank = null; let bestRate = 0; for (const [bankName, bankStats] of Object.entries(stats.questionBankUsage)) { if (bankStats.used >= 5) { const rate = bankStats.success / bankStats.used; if (rate > bestRate) { bestRate = rate; bestBank = bankName; } } } return bestBank; }, reset() { stats = { totalQuestions: 0, answeredQuestions: 0, matchedFromLocalDB: 0, matchedFromSimilar: 0, matchedFromAI: 0, unmatched: 0, recentAnswers: [], questionBankUsage: {}, lastResetTime: Date.now() }; saveStats(); } }; })(); // ========== 错题学习记录系统:记录提交验证后正确的答案 ========== const WrongAnswerLearner = (() => { const STORAGE_KEY = 'verified_correct_answers'; let correctAnswers = {}; function loadAnswers() { try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { correctAnswers = JSON.parse(saved); } } catch (e) {} } function saveAnswers() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(correctAnswers)); } catch (e) {} } loadAnswers(); function getQuestionKey(question) { // 生成题目唯一标识 let text = (question.title || '').replace(/<[^>]*>/g, '').trim(); // 保留前200个字符 text = text.substring(0, 200); // 移除特殊字符 text = text.replace(/[^\w\u4e00-\u9fa5]/g, ''); return text; } return { // 记录验证后的正确答案 recordCorrectAnswer(question, correctOption) { try { const key = getQuestionKey(question); if (!key) return; if (!correctAnswers[key]) { correctAnswers[key] = { questionTitle: question.title, correctOptions: [], verifiedTime: Date.now(), verifiedCount: 0 }; } // 添加正确选项,避免重复 if (!correctAnswers[key].correctOptions.includes(correctOption)) { correctAnswers[key].correctOptions.push(correctOption); } correctAnswers[key].verifiedTime = Date.now(); correctAnswers[key].verifiedCount++; saveAnswers(); console.log('[WrongAnswerLearner] 记录正确答案:', key, correctOption); } catch (e) { console.warn('[WrongAnswerLearner] 记录失败:', e); } }, // 查找是否有验证过的正确答案 getVerifiedAnswer(question) { try { const key = getQuestionKey(question); if (!key) return null; const answer = correctAnswers[key]; if (!answer) return null; console.log('[WrongAnswerLearner] 找到验证过的正确答案:', key, answer.correctOptions); return answer.correctOptions; } catch (e) { return null; } }, // 解析提交后页面的结果,识别正确答案 parseAndLearnFromResult(iframeWindow, questions) { try { let learnedCount = 0; console.log('[WrongAnswerLearner] 开始解析提交后页面结果...'); // 方式1: 查找每个题目区域的正确/错误标记 for (const question of questions) { if (!question.element) continue; // 查找这个题目的选项区域 const optionElements = question.element.querySelectorAll('[class*="option"], .answerItem, .answer_option, .answer_question_item'); for (const optEl of optionElements) { // 检查是否有正确标记 const isCorrect = optEl.querySelector('.u-icon-correct, .right-icon, [class*="correct"], [class*="right"], [class*="pass"], [class*="success"]') !== null; if (isCorrect) { // 获取选项文本 let optionText = ''; try { optionText = optEl.textContent?.trim() || ''; } catch (e) {} if (optionText) { WrongAnswerLearner.recordCorrectAnswer(question, optionText); learnedCount++; } } } } // 方式2: 查找所有有正确图标的元素 const allCorrectElements = iframeWindow.document.querySelectorAll('.u-icon-correct, .right-icon, [class*="correct"], [class*="right"], [class*="pass"], [class*="success"]'); for (const correctEl of allCorrectElements) { try { // 向上查找父级找到题目区域 let questionContainer = correctEl; for (let i = 0; i < 10 && questionContainer; i++) { questionContainer = questionContainer.parentElement; if (questionContainer && (questionContainer.classList.contains('answer_question') || questionContainer.classList.contains('question') || questionContainer.getAttribute('class')?.includes('question'))) { // 获取题目文本 let questionText = ''; try { const titleEl = questionContainer.querySelector('.answer_topic, .question_topic, .topic, [class*="title"]'); if (titleEl) { questionText = titleEl.textContent?.trim() || ''; } } catch (e) {} // 获取选项文本 let optionText = ''; try { optionText = correctEl.closest('[class*="option"], .answerItem, .answer_option')?.textContent?.trim() || ''; } catch (e) {} if (questionText && optionText) { // 模拟question对象 WrongAnswerLearner.recordCorrectAnswer({ title: questionText }, optionText); learnedCount++; } break; } } } catch (e) {} } console.log(`[WrongAnswerLearner] 解析完成,学习了 ${learnedCount} 个正确答案`); return learnedCount; } catch (e) { console.warn('[WrongAnswerLearner] 解析失败:', e); return 0; } }, clear() { correctAnswers = {}; saveAnswers(); } }; })(); async function autoAnswerOnce(resumeFrom = null){ try{ // 安全修复:答题前先检查授权状态,防止无次数时浪费用户时间 try{ const licStatus = await ClientLicense.checkToken(); if(!licStatus.ok){ logStore.addLog('❌ 未授权或卡密无效,无法使用自动答题功能', 'danger'); logStore.addLog('🛒 点击购买卡密,享无限答题', 'warning'); return; } if(typeof licStatus.uses_remaining === 'number' && licStatus.uses_remaining <= 0){ logStore.addLog('❌ 答题次数已用完,无法继续答题', 'danger'); logStore.addLog('🛒 点击购买卡密,获取更多答题次数', 'warning'); return; } if(typeof licStatus.uses_remaining === 'number' && licStatus.uses_remaining <= 5){ logStore.addLog(`⚠️ 剩余答题次数较少(${licStatus.uses_remaining}次),请注意及时充值`, 'warning'); logStore.addLog('🛒 点击购买卡密,获取更多答题次数', 'warning'); } }catch(e){ console.warn('[autoAnswer] 授权检查失败:', e); } const stored = (typeof _GM_getValue === 'function') ? _GM_getValue('config') : null; const cfg = stored ? (typeof stored === 'string' ? JSON.parse(stored) : stored) : null; const interval = (cfg && cfg.platformParams && cfg.platformParams.cx) ? (Number((cfg.platformParams.cx.parts || []).flatMap(p=>p.params||[]).find(p=>p.name==='答题间隔')?.value) || 1) : 1; // P2 优化:增强 DOM 选择器容错机制 - 多级回退策略 const questionSelectors = [ // 超星学习通 - 章节测验 SELECTORS.CX_QUESTION_ZJ, '.TiMu', '[class*="TiMu"]', '[class*="question"]', // 超星学习通 - 作业考试 SELECTORS.CX_QUESTION_ZY_KS, '.questionLi', '[class*="questionLi"]', // 智慧树 SELECTORS.ZHS_QUESTION, '.examPaper_subject', '[class*="subject"]', // 通用选择器 - 最后回退 '.question-item', '[data-question]', 'li[class*="question"]', 'div[class*="question"]' ].filter(Boolean); let qEls = []; let usedSelector = ''; // 依次尝试每个选择器 for(const selector of questionSelectors){ try{ const elements = Array.from(document.querySelectorAll(selector)); if(elements && elements.length > 0){ qEls = elements; usedSelector = selector; console.log(`[autoAnswer] 使用选择器 "${selector}" 找到 ${qEls.length} 个题目`); break; } }catch(e){ // 选择器语法错误,跳过 continue; } } if(!qEls.length){ // P2 优化:智能检测 - 尝试通过文本特征识别题目 console.log('[autoAnswer] 标准选择器未找到题目,尝试智能检测...'); qEls = detectQuestionsByContent(); usedSelector = 'content-detection'; } if(!qEls.length) return console.log('[autoAnswer] 未检测到题目'); const totalQuestions = qEls.length; let answeredCount = 0; let processedCount = 0; let startIndex = 0; if (!resumeFrom) { const savedProgress = loadAnswerProgress(); if (savedProgress && savedProgress.totalQuestions === totalQuestions && savedProgress.processedCount < totalQuestions) { console.log('[autoAnswer] 检测到未完成的答题进度,准备恢复...'); try { if (typeof useProgressStore === 'function') { const ps = useProgressStore(); ps.update({ taskName: `AI自动答题 (${totalQuestions}题)`, percent: savedProgress.percent || 0, type: '答题', detail: `检测到上次答题进度 (${savedProgress.processedCount}/${totalQuestions}),正在恢复...`, isPlaying: true }); } } catch(e) {} await new Promise(r => setTimeout(r, 1000)); startIndex = savedProgress.processedCount || 0; answeredCount = savedProgress.answeredCount || 0; processedCount = startIndex; logStore.addLog(`🔄 恢复答题进度:${processedCount}/${totalQuestions}`, 'info'); } } else { startIndex = resumeFrom.processedCount || 0; answeredCount = resumeFrom.answeredCount || 0; processedCount = startIndex; } saveAnswerProgress({ totalQuestions, processedCount, answeredCount, percent: 0, startTime: Date.now() }); try{ if(typeof useProgressStore === 'function'){ const ps = useProgressStore(); ps.update({ taskName: `AI自动答题 (${totalQuestions}题)`, percent: startIndex > 0 ? Math.round((startIndex / totalQuestions) * 100) : 0, type: '答题', detail: startIndex > 0 ? `已恢复进度 ${processedCount}/${totalQuestions},继续答题...` : `共${totalQuestions}题,正在解析...`, isPlaying: true }); } }catch(e){} // P0 优化:缓存 local_db 避免循环内重复读取(N+1 查询) let localDbCache = null; let localDbKeysCache = null; try { localDbCache = Store.get('local_db', {}); if (localDbCache && typeof localDbCache === 'object') { localDbKeysCache = Object.keys(localDbCache); } } catch(e) { localDbCache = {}; localDbKeysCache = []; } // P0 优化:缓存 config 避免循环内重复读取(N+1 查询) let cachedConfig = null; let cachedInterval = 1; let skipAnswered = true; let useSimilarMatch = true; let useSimulateDelay = true; let confidenceThreshold = 0.7; let answerMode = 'normal'; // normal, verify, ai try { const stored = (typeof _GM_getValue === 'function') ? _GM_getValue('config') : null; cachedConfig = stored ? (typeof stored === 'string' ? JSON.parse(stored) : stored) : null; if (cachedConfig && cachedConfig.platformParams && cachedConfig.platformParams.cx) { const params = cachedConfig.platformParams.cx.parts || []; const allParams = params.flatMap(p => p.params || []); cachedInterval = Number(allParams.find(p => p.name === '答题间隔')?.value) || 1; skipAnswered = allParams.find(p => p.name === '跳过已答')?.value !== false; useSimilarMatch = allParams.find(p => p.name === '相似匹配')?.value !== false; useSimulateDelay = allParams.find(p => p.name === '模拟延迟')?.value !== false; confidenceThreshold = (Number(allParams.find(p => p.name === '正确阈值')?.value) || 70) / 100; // 答题模式: 正常模式/答案校验/AI模式 if (allParams.find(p => p.name === 'AI模式')?.value) answerMode = 'ai'; else if (allParams.find(p => p.name === '答案校验')?.value) answerMode = 'verify'; else answerMode = 'normal'; } } catch(e) {} // P3 优化:批量答题模式 - 当题目数量较多时,批量处理提高效率 const BATCH_SIZE = 5; const isBatchMode = totalQuestions > BATCH_SIZE; for(let i = startIndex; i < qEls.length; i++){ const qEl = qEls[i]; try{ processedCount = i + 1; const rawText = (qEl.innerText || qEl.textContent || '').trim(); if(!rawText) continue; const qText = rawText.replace(/\s+/g,' '); if(isQuestionAnswered({qEl})){ answeredCount++; if (skipAnswered) continue; // 答案校验模式:即使已答也要校验 if (answerMode === 'verify') { // 继续处理以校验答案 } else { continue; } } const optSelectors = [ SELECTORS.CX_OPTION_ZJ, '[class*="before-after"]', '[class*="option"]', '[class*="answer"]', '.answerBg', 'label', '.choice-item', '[class*="choice"]', // 学习通章节测验常见选项选择器 '.item', '[class*="item"]', '.option-item', '[class*="option-item"]', 'div[class*="opt"]', 'span[class*="opt"]', // 支持 iframe 内的选项 'iframe', // 最后尝试直接查找 input 元素的父元素 'input[type="radio"]', 'input[type="checkbox"]' ].filter(Boolean); let optEls = []; for(const optSelector of optSelectors){ try{ const elements = Array.from(qEl.querySelectorAll(optSelector)); if(elements && elements.length >= 2){ optEls = elements; break; } }catch(e){ continue; } } // 提取选项文本:去除选项字母前缀(如 "A. "、"B、") const options = optEls.map(e => { let text = (e.innerText || e.textContent || '').trim(); // 去除开头选项字母标记 text = text.replace(/^\s*[A-Ha-h][\.\、\)\s]\s*/, '').trim(); return text; }).filter(Boolean); // 【优化】先检测题型! const questionType = detectQuestionType(qEl, options, optEls); const isMultiSelect = questionType === "1"; // 多选题 const isJudgement = questionType === "3"; // 判断题 let answer = null; let answerConfidence = 0; const cleanedQuestionText = StringUtil.removeRedundant(qText); console.log(`[autoAnswer] 检测到题型: ${questionType} (${isMultiSelect?'多选题':isJudgement?'判断题':questionType==="2"? '填空题':'单选题'})`); // P1 优化:精确匹配优先(使用预处理后的题目文本) let answerSource = 'unmatched'; if(localDbCache){ if(localDbCache[qText]){ answer = localDbCache[qText]; answerSource = 'local_db'; answerConfidence = 1.0; } else if(localDbCache[cleanedQuestionText]){ answer = localDbCache[cleanedQuestionText]; answerSource = 'local_db'; answerConfidence = 1.0; } } // P1 优化:相似匹配降级(使用预处理 + 缓存的 keys) if(!answer && useSimilarMatch && localDbKeysCache && localDbKeysCache.length){ const cleanedKeys = localDbKeysCache.map(k => StringUtil.removeRedundant(k)); const best = StringUtil.findBestMatch(cleanedQuestionText, cleanedKeys); if(best && best.bestMatch && best.bestMatch.rating > 0.9){ const originalKey = localDbKeysCache[best.bestMatchIndex]; answer = localDbCache[originalKey]; answerSource = 'similar'; answerConfidence = best.bestMatch.rating; } } // 若启用 AI 且授权可用,尝试 AI(带自动重试 + 题型定制提示词) if(!answer && answerMode === 'ai' && typeof deepseekChatWithLicense === 'function'){ const maxRetries = 3; const baseDelay = 1000; let attempt = 0; let lastError = null; // 【优化】根据题型生成不同的提示词 - 格式更明确,便于解析 let prompt = ''; if(isMultiSelect){ prompt = `这是一道多选题,可能有多个正确答案。\n要求:只输出正确的选项字母(如 ABD),多个字母连写,不要加标点和空格。\n题目:${qText}\n选项:\n${options.map((o,i)=>`${String.fromCharCode(65+i)}. ${o}`).join('\n')}\n答案:`; } else if(isJudgement){ prompt = `这是一道判断题。\n要求:只输出"对"或"错"(或"正确"/"错误")。\n题目:${qText}\n选项:\n${options.map((o,i)=>`${String.fromCharCode(65+i)}. ${o}`).join('\n')}\n答案:`; } else { prompt = `这是一道单选题,只有一个正确答案。\n要求:只输出正确选项的字母(如 A),不要输出其他内容。\n题目:${qText}\n选项:\n${options.map((o,i)=>`${String.fromCharCode(65+i)}. ${o}`).join('\n')}\n答案:`; } while(attempt < maxRetries){ try{ const aiRes = await deepseekChatWithLicense([{role:'user', content: prompt}], {}); if(aiRes){ const s = (aiRes.answer || aiRes.text || aiRes)?.toString?.() || aiRes.toString(); if(s) { answer = s.split('\n')[0].trim(); // AI 答案置信度设为 0.7,需要远程题库验证才能达到高置信度 answerConfidence = 0.7; } } if(answer) { answerSource = 'ai'; break; } }catch(e){ lastError = e; attempt++; if(attempt < maxRetries){ const delay = baseDelay * Math.pow(2, attempt - 1); console.warn(`[autoAnswer] AI 调用失败 (第${attempt}次),${delay}ms后重试...`, e); try{ if(typeof useProgressStore === 'function'){ const ps = useProgressStore(); ps.update({ detail: `AI调用失败,重试中 (${attempt}/${maxRetries})...` }); } }catch(e){} await new Promise(r => setTimeout(r, delay)); } else { console.error('[autoAnswer] AI 调用失败,已达最大重试次数', e); logStore.addLog(`⚠️ AI调用失败,已重试${maxRetries}次: ${e.message}`, 'warning'); } } } } console.log('[autoAnswer] 题:', qText.slice(0,120), '\n题型:', questionType, '\n选项:', options, '\n猜测:', answer, '\n来源:', answerSource, '\n置信度:', answerConfidence); // ========== 低分答案/无答案重试机制 ========== if ((!answer || answerConfidence < confidenceThreshold) && options.length >= 2) { if (logStore) logStore.addLog(`[重试] 置信度过低(${answerConfidence.toFixed(2)} < ${confidenceThreshold.toFixed(2)}),尝试其他来源...`, 'warning'); try { // 重试策略1:在线题库查询(只重试一次) if (typeof fetchFromMultipleApis === 'function') { const onlineResult = await fetchFromMultipleApis({ title: qText, type: isMultiSelect ? 'multi' : isJudgement ? 'judge' : 'single' }); if (onlineResult && onlineResult.data && onlineResult.data.answer && onlineResult.data.answer.length > 0) { const onlineAnswers = Array.isArray(onlineResult.data.answer) ? onlineResult.data.answer : [onlineResult.data.answer]; const onlineConfidence = (onlineResult.data.confidence || 85) / 100; // 只取第一个非空答案 const validAnswers = onlineAnswers.filter(a => a && a.trim()); if (validAnswers.length > 0) { answer = isMultiSelect ? validAnswers.join(',') : validAnswers[0]; answerConfidence = Math.max(answerConfidence, onlineConfidence); answerSource = 'retry_online'; if (logStore) logStore.addLog(`[重试] 在线题库返回: ${answer} (置信度:${Math.round(answerConfidence*100)}%)`, 'success'); // 只保存高置信度答案到 local_db(置信度>=85%),避免污染题库 if (answerConfidence >= 0.85) { try { const ld = Store.get('local_db', {}); ld[qText] = answer; Store.set('local_db', ld); localDbCache = ld; if (logStore) logStore.addLog(`[题库] 高置信度答案已保存到local_db`, 'info'); } catch(e){} } else { if (logStore) logStore.addLog(`[题库] 答案置信度不足(${Math.round(answerConfidence*100)}%),不保存到local_db`, 'warning'); } } } } } catch(e) { if (logStore) logStore.addLog(`[重试] 在线题库失败: ${e.message}`, 'error'); } // 重试策略2:再次尝试AI(用更详细的提示词) if ((!answer || answerConfidence < 0.6) && typeof deepseekChatWithLicense === 'function') { try { let retryPrompt = ''; if (isMultiSelect) { retryPrompt = `这是一道多选题,必须仔细分析每个选项。\n题目:${qText}\n\n选项:\n${options.map((o,i)=>`${String.fromCharCode(65+i)}. ${o}`).join('\n')}\n\n只输出确定正确的选项字母(如 ABD),多个选项字母之间不要加任何分隔符。不确定就输出空。\n答案:`; } else if (isJudgement) { retryPrompt = `这是一道判断题。\n题目:${qText}\n\n选项:\n${options.map((o,i)=>`${String.fromCharCode(65+i)}. ${o}`).join('\n')}\n\n请分析后只输出"正确"或"错误"。\n答案:`; } else { retryPrompt = `这是一道单选题。\n题目:${qText}\n\n选项:\n${options.map((o,i)=>`${String.fromCharCode(65+i)}. ${o}`).join('\n')}\n\n请仔细分析后只输出最正确选项的字母(如 A)。如果实在不确定,选择最可能的选项。\n答案:`; } const retryRes = await deepseekChatWithLicense([{role:'user', content: retryPrompt}], {}); if (retryRes) { let retryAns = (retryRes.answer || retryRes.text || retryRes)?.toString?.() || retryRes.toString(); retryAns = retryAns.split('\n')[0].trim(); if (retryAns && retryAns.length > 0) { answer = retryAns; answerConfidence = Math.max(answerConfidence, 0.75); answerSource = 'retry_ai'; if (logStore) logStore.addLog(`[重试] AI再次回答: ${answer}`, 'success'); } } } catch(e) { if (logStore) logStore.addLog(`[重试] AI重试失败: ${e.message}`, 'warning'); } } if (!answer) { if (logStore) logStore.addLog(`[重试] 未找到可靠答案,跳过猜测`, 'warning'); } } if(answer && options.length){ try{ // 记录已点击的索引防止重复 const clickedIndices = new Set(); // ========== 答案校验模式:检测已选答案并纠正 ========== if (answerMode === 'verify') { try { // 检测哪些选项已被选中 const selectedIndices = new Set(); optEls.forEach((opt, idx) => { const input = opt.querySelector('input[type="radio"], input[type="checkbox"]'); if (input && input.checked) { selectedIndices.add(idx); } // 也检查CSS选中状态 if (opt.classList.contains('selected') || opt.classList.contains('active') || opt.getAttribute('aria-checked') === 'true') { selectedIndices.add(idx); } }); if (selectedIndices.size > 0) { logStore.addLog(`[校验] 检测到已选答案: ${Array.from(selectedIndices).map(i => String.fromCharCode(65 + i)).join(', ')}`, 'info'); } } catch(e) {} } // ========== 【恢复 backup 版本的引擎架构】========== // 使用 AdvancedAnswerEngine 的智能答题引擎,而不是 inline 匹配 let engineResult = null; let engineType = ''; // 构建 knownAnswers 数组 const knownAnswersArr = []; if (answer) { if (Array.isArray(answer)) { knownAnswersArr.push(...answer); } else { knownAnswersArr.push(String(answer)); } } // ========== 关键修复:字母答案直接解析,不经过引擎 ========== // 因为引擎的相似度算法无法将字母"A"与选项文本匹配 const answerStr = String(answer || '').replace(/<[^>]*>/g, '').trim(); const letterMatch = answerStr.match(/^[A-Da-d](?![a-zA-Z])$/); const multiLetterMatch = answerStr.match(/^[A-Da-d]{2,}$/); if(isMultiSelect){ // 多选题:优先尝试字母解析 if(multiLetterMatch || letterMatch){ const letters = answerStr.toUpperCase().match(/[A-D]/g) || []; for(const letter of letters){ const idx = letter.charCodeAt(0) - 65; if(idx >= 0 && idx < optEls.length && optEls[idx] && !clickedIndices.has(idx)){ try{ safeClick(optEls[idx]); clickedIndices.add(idx); console.log(`[autoAnswer] 多选字母直解: ${letter} -> ${options[idx]}`); }catch(e){ console.log('[autoAnswer] 多选点击失败:', e.message); } try{ LocalDB.addMapping(cleanedQuestionText, options[idx], { score: 0.98 }); }catch(e){} } } if(clickedIndices.size > 0){ answeredCount++; AnswerQualityMonitor.recordAnswer(answerSource, true); } else { AnswerQualityMonitor.recordAnswer(answerSource, false); } continue; // 跳过引擎处理 } // 非字母答案:使用 smartAnswerMulti 引擎 engineResult = AdvancedAnswerEngine.smartAnswerMulti(qText, options, knownAnswersArr); engineType = 'multi'; console.log('[autoAnswer] 多选引擎返回:', engineResult); if(engineResult && engineResult.options && engineResult.options.length > 0){ for(const match of engineResult.options){ const optText = match.option || match; const idx = options.findIndex(o => { const cleanOpt = o.replace(/<[^>]*>/g, '').trim(); const cleanMatch = (typeof optText === 'string' ? optText : optText.textContent || '').replace(/<[^>]*>/g, '').trim(); return cleanOpt === cleanMatch || cleanOpt.includes(cleanMatch) || cleanMatch.includes(cleanOpt); }); if(idx >= 0 && optEls[idx] && !clickedIndices.has(idx)){ try{ safeClick(optEls[idx]); clickedIndices.add(idx); console.log(`[autoAnswer] 多选引擎点击[${idx}]: ${options[idx]}`); }catch(e){ console.log('[autoAnswer] 多选点击失败:', e.message); } try{ LocalDB.addMapping(cleanedQuestionText, options[idx], { score: (match.confidence || 80) / 100 }); }catch(e){} } } } else { // 引擎无结果,尝试字母解析兜底 const letters = answerStr.match(/[A-Da-d]/g); if(letters){ for(const letter of letters){ const idx = letter.toUpperCase().charCodeAt(0) - 65; if(idx >= 0 && idx < optEls.length && optEls[idx] && !clickedIndices.has(idx)){ try{ safeClick(optEls[idx]); clickedIndices.add(idx); console.log('[autoAnswer] 多选字母兜底:', letter); }catch(e){} try{ LocalDB.addMapping(cleanedQuestionText, options[idx], { score: 0.95 }); }catch(e){} } } } } } else if(isJudgement){ // 判断题:优先直接匹配关键词 const judgeWords = { correct: ['是','对','正确','确定','√','对的','是的','正确的','true','True','T','yes','1'], incorrect: ['非','否','错','错误','×','X','错的','不对','不正确的','不正确','不是','不是的','false','False','F','no','0'] }; const answerLower = answerStr.toLowerCase(); let judgeTarget = null; // 'correct' or 'incorrect' for(const w of judgeWords.correct){ if(answerLower.includes(w.toLowerCase()) || answerStr.includes(w)){ judgeTarget = 'correct'; break; } } if(!judgeTarget){ for(const w of judgeWords.incorrect){ if(answerLower.includes(w.toLowerCase()) || answerStr.includes(w)){ judgeTarget = 'incorrect'; break; } } } if(judgeTarget){ // 找到对应选项 for(let j = 0; j < options.length; j++){ const optLower = options[j].toLowerCase(); const optText = options[j]; let optIsCorrect = false; let optIsIncorrect = false; for(const w of judgeWords.correct){ if(optLower.includes(w.toLowerCase()) || optText.includes(w)){ optIsCorrect = true; break; } } if(!optIsCorrect){ for(const w of judgeWords.incorrect){ if(optLower.includes(w.toLowerCase()) || optText.includes(w)){ optIsIncorrect = true; break; } } } if((judgeTarget === 'correct' && optIsCorrect) || (judgeTarget === 'incorrect' && optIsIncorrect)){ if(optEls[j] && !clickedIndices.has(j)){ try{ safeClick(optEls[j]); console.log('[autoAnswer] 判断直解:', j, options[j]); }catch(e){} try{ LocalDB.addMapping(cleanedQuestionText, options[j], { score: 0.98 }); }catch(e){} clickedIndices.add(j); } break; } } } // 如果直解失败,尝试引擎 if(clickedIndices.size === 0){ engineResult = AdvancedAnswerEngine.smartAnswerJudge(qText, options, knownAnswersArr); engineType = 'judge'; console.log('[autoAnswer] 判断引擎返回:', engineResult); if(engineResult && engineResult.option){ const idx = options.findIndex(o => { const cleanOpt = o.replace(/<[^>]*>/g, '').trim(); const cleanMatch = (engineResult.option || '').replace(/<[^>]*>/g, '').trim(); return cleanOpt === cleanMatch || cleanOpt.includes(cleanMatch) || cleanMatch.includes(cleanOpt); }); if(idx >= 0 && optEls[idx]){ try{ safeClick(optEls[idx]); console.log('[autoAnswer] 判断引擎点击:', idx, options[idx]); }catch(e){} try{ LocalDB.addMapping(cleanedQuestionText, options[idx], { score: (engineResult.confidence || 80) / 100 }); }catch(e){} clickedIndices.add(idx); } } } } else { // 单选题:优先尝试字母解析 if(letterMatch){ const idx = answerStr.toUpperCase().charCodeAt(0) - 65; if(idx >= 0 && idx < optEls.length && optEls[idx]){ try{ safeClick(optEls[idx]); console.log('[autoAnswer] 单选字母直解:', answerStr, '->', options[idx]); }catch(e){} try{ LocalDB.addMapping(cleanedQuestionText, options[idx], { score: 0.98 }); }catch(e){} clickedIndices.add(idx); } } // 非字母答案:使用 smartAnswer 引擎 if(clickedIndices.size === 0){ engineResult = AdvancedAnswerEngine.smartAnswer(qText, options, knownAnswersArr); engineType = 'single'; console.log('[autoAnswer] 单选引擎返回:', engineResult); if(engineResult && engineResult.option){ const idx = options.findIndex(o => { const cleanOpt = o.replace(/<[^>]*>/g, '').trim(); const cleanMatch = (engineResult.option || '').replace(/<[^>]*>/g, '').trim(); return cleanOpt === cleanMatch || cleanOpt.includes(cleanMatch) || cleanMatch.includes(cleanOpt); }); if(idx >= 0 && optEls[idx]){ try{ safeClick(optEls[idx]); console.log('[autoAnswer] 单选引擎点击:', idx, options[idx]); }catch(e){} try{ LocalDB.addMapping(cleanedQuestionText, options[idx], { score: (engineResult.confidence || 80) / 100 }); }catch(e){} clickedIndices.add(idx); } } else { // 引擎无结果,尝试字母解析兜底 const fallbackLetter = answerStr.match(/^[A-Da-d](?![a-zA-Z])/); if(fallbackLetter){ const idx = fallbackLetter[0].toUpperCase().charCodeAt(0) - 65; if(idx >= 0 && idx < optEls.length && optEls[idx]){ try{ safeClick(optEls[idx]); console.log('[autoAnswer] 单选字母兜底:', fallbackLetter[0]); }catch(e){} try{ LocalDB.addMapping(cleanedQuestionText, options[idx], { score: 0.95 }); }catch(e){} clickedIndices.add(idx); } } } } } if(clickedIndices.size > 0 || (engineResult && engineResult.option)){ answeredCount++; AnswerQualityMonitor.recordAnswer(answerSource, true); } else { AnswerQualityMonitor.recordAnswer(answerSource, false); } }catch(e){ console.warn('[autoAnswer] 选项匹配失败', e); AnswerQualityMonitor.recordAnswer(answerSource, false); } } else if (!answer) { AnswerQualityMonitor.recordAnswer('unmatched', false); } // P3 优化:批量模式减少延迟,逐题模式保持正常延迟 if (useSimulateDelay) { let currentInterval = cachedInterval; if(isBatchMode){ // 批量模式:每 BATCH_SIZE 题才延迟一次 if((i - startIndex + 1) % BATCH_SIZE === 0){ const jitter = currentInterval * 0.2 * (Math.random() * 2 - 1); const delayWithJitter = (currentInterval + jitter) * 1000; await new Promise(r => setTimeout(r, Math.max(500, delayWithJitter))); } else { // 批量模式内题目快速处理,仅添加微小随机延迟 await new Promise(r => setTimeout(r, 100 + Math.random() * 200)); } } else { // 逐题模式:正常延迟 if(currentInterval > 0){ const jitter = currentInterval * 0.2 * (Math.random() * 2 - 1); const delayWithJitter = (currentInterval + jitter) * 1000; await new Promise(r => setTimeout(r, Math.max(500, delayWithJitter))); } } } // P0 优化:节流进度保存(每 5 题或完成时才写入 localStorage) const shouldSaveProgress = (processedCount % 5 === 0) || (processedCount === totalQuestions); // 更新进度提示(每次更新,内存操作很快) const percent = Math.round((processedCount / totalQuestions) * 100); try{ if(typeof useProgressStore === 'function'){ const ps = useProgressStore(); ps.update({ taskName: `AI自动答题 (${totalQuestions}题)`, percent: percent, type: '答题', detail: isBatchMode ? `批量模式:已处理 ${processedCount}/${totalQuestions} 题` : `已处理 ${processedCount}/${totalQuestions} 题`, isPlaying: true }); } }catch(e){} if(shouldSaveProgress){ saveAnswerProgress({ totalQuestions, processedCount, answeredCount, percent, startTime: Date.now() }); } }catch(e){ console.warn('[autoAnswer] 处理单题异常', e); } } // 完成一轮后的进度更新 try{ if(typeof useProgressStore === 'function'){ const ps = useProgressStore(); const allAnswered = answeredCount >= totalQuestions; ps.update({ taskName: allAnswered ? '答题完成' : `AI自动答题 (${totalQuestions}题)`, percent: 100, type: '答题', detail: allAnswered ? `全部${totalQuestions}题已作答,准备提交` : `本轮完成,已答${answeredCount}/${totalQuestions}题`, isPlaying: !allAnswered }); } }catch(e){} // 答题完成,清除保存的进度 clearAnswerProgress(); logStore.addLog(`✅ 答题完成!共处理 ${processedCount}/${totalQuestions} 题`, 'success'); }catch(e){ console.error('[autoAnswer] 异常', e); } } let __autoTaskTimer = null; let __autoTaskWorkerId = null; let __isPageVisible = true; // 页面可见性监听 - 后台运行支持 (function initVisibilityHandler(){ const onVisibilityChange = () => { __isPageVisible = !document.hidden; console.log(`[学习通助手] 页面可见性变化: ${document.hidden ? '后台' : '前台'}`); }; document.addEventListener('visibilitychange', onVisibilityChange); try { window.addEventListener('blur', () => { __isPageVisible = false; }); } catch(e) {} try { window.addEventListener('focus', () => { __isPageVisible = true; }); } catch(e) {} })(); // BackgroundWorker - Web Worker 后台计时器(提前定义,供 startAutoLoop 使用) const BackgroundWorker = (() => { let worker = null; let blobUrl = null; const callbacks = new Map(); let isWorkerAlive = false; let heartbeatTimer = null; let restartAttempts = 0; const MAX_RESTART_ATTEMPTS = 3; const workerCode = ` let _timers = {}; let _heartbeatInterval = null; self.onmessage = function(e) { if (e.data.type === 'start') { const id = e.data.id; const interval = e.data.interval || 1000; if (_timers[id]) { clearInterval(_timers[id]); } _timers[id] = setInterval(() => { self.postMessage({ type: 'tick', id: id }); }, interval); self.postMessage({ type: 'started', id: id }); } else if (e.data.type === 'stop') { const id = e.data.id; if (_timers[id]) { clearInterval(_timers[id]); delete _timers[id]; } self.postMessage({ type: 'stopped', id: id }); } else if (e.data.type === 'heartbeat') { self.postMessage({ type: 'heartbeat_ack', timestamp: Date.now() }); } else if (e.data.type === 'cleanup') { Object.values(_timers).forEach(timerId => clearInterval(timerId)); _timers = {}; self.postMessage({ type: 'cleaned' }); } }; self.onerror = function(e) { self.postMessage({ type: 'error', message: e.message, filename: e.filename, lineno: e.lineno }); }; self.onclose = function() { Object.values(_timers).forEach(timerId => clearInterval(timerId)); _timers = {}; }; `; function createWorker() { if (blobUrl) { URL.revokeObjectURL(blobUrl); blobUrl = null; } const blob = new Blob([workerCode], { type: 'application/javascript' }); blobUrl = URL.createObjectURL(blob); const newWorker = new Worker(blobUrl); newWorker.onmessage = (e) => { const { type, id } = e.data; if (type === 'tick' && callbacks.has(id)) { try { callbacks.get(id)(); } catch (err) { console.error(`[BackgroundWorker] 回调执行异常 [${id}]:`, err); } } else if (type === 'error') { console.error(`[BackgroundWorker] Worker内部错误:`, e.data); isWorkerAlive = false; attemptRestart(); } else if (type === 'heartbeat_ack') { isWorkerAlive = true; restartAttempts = 0; } }; newWorker.onerror = (e) => { console.error('[BackgroundWorker] Worker加载异常:', e.message); isWorkerAlive = false; attemptRestart(); }; return newWorker; } function startHeartbeat() { if (heartbeatTimer) return; heartbeatTimer = setInterval(() => { if (worker && isWorkerAlive) { try { worker.postMessage({ type: 'heartbeat' }); } catch (e) { console.warn('[BackgroundWorker] 心跳发送失败:', e.message); isWorkerAlive = false; } } else if (worker && !isWorkerAlive) { attemptRestart(); } }, 5000); } function attemptRestart() { if (restartAttempts >= MAX_RESTART_ATTEMPTS) { console.error('[BackgroundWorker] 达到最大重启次数,停止服务'); destroy(); return; } restartAttempts++; console.log(`[BackgroundWorker] 尝试重启 (${restartAttempts}/${MAX_RESTART_ATTEMPTS})`); const oldCallbacks = new Map(callbacks); destroy(); setTimeout(() => { try { ensure(); oldCallbacks.forEach((cb, id) => { callbacks.set(id, cb); }); console.log('[BackgroundWorker] 重启成功'); } catch (e) { console.error('[BackgroundWorker] 重启失败:', e.message); attemptRestart(); } }, 1000 * restartAttempts); } function ensure() { if (worker && isWorkerAlive) return; worker = createWorker(); isWorkerAlive = true; restartAttempts = 0; startHeartbeat(); } return { start(id, callback, interval = 1000) { ensure(); callbacks.set(id, callback); try { worker.postMessage({ type: 'start', id, interval }); } catch (e) { console.error('[BackgroundWorker] 启动任务失败:', e.message); attemptRestart(); } }, stop(id) { if (!worker) return; callbacks.delete(id); try { worker.postMessage({ type: 'stop', id }); } catch (e) { console.warn('[BackgroundWorker] 停止任务失败:', e.message); } }, isActive() { return worker !== null && isWorkerAlive; }, isAlive() { return isWorkerAlive; }, destroy() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } if (worker) { try { worker.postMessage({ type: 'cleanup' }); } catch (e) {} worker.terminate(); worker = null; } if (blobUrl) { URL.revokeObjectURL(blobUrl); blobUrl = null; } callbacks.clear(); isWorkerAlive = false; } }; })(); // 优化:增强题型检测(模仿jinmu.js的DOM层级查找方式 + 多维度判断) function detectQuestionType(qEl, options, optEls) { const text = (qEl.innerText || qEl.textContent || ''); const lowerText = text.toLowerCase(); // ===== 方法-1:【关键修复】纯文本判断题检测 - 不依赖DOM input类型,直接看选项文本 ===== // 当DOM中没有radio/checkbox元素时,通过选项文本判断是否为判断题 if (options && options.length === 2) { const joined = options.map(o => o.toLowerCase().replace(/^[a-z][.、\s]*/i, '')).join(''); if ((joined.includes('对') && joined.includes('错')) || (joined.includes('正确') && joined.includes('错误')) || (joined.includes('true') && joined.includes('false')) || (joined.includes('是') && joined.includes('否')) || (joined.includes('√') && joined.includes('×')) || (joined.includes('yes') && joined.includes('no'))) { return "3"; // 强制识别为判断题 } } // ===== 方法0:快速检查选项元素本身包含的input类型(灵活适配自定义渲染)===== try { if (optEls && optEls.length > 0) { let optRadioCount = 0, optCheckboxCount = 0; for (const opt of optEls) { if (opt.querySelector('input[type="radio"]')) optRadioCount++; if (opt.querySelector('input[type="checkbox"]')) optCheckboxCount++; // 检测role attribute const role = opt.getAttribute('role') || ''; if (role === 'radio' || role === 'menuitemradio') optRadioCount++; if (role === 'checkbox') optCheckboxCount++; // 检测aria-checked(表示可切换的选中状态) if (opt.hasAttribute('aria-checked')) { if (opt.getAttribute('role') === 'checkbox' || !opt.getAttribute('role')) optCheckboxCount++; } } // 用选项内部检测结果覆盖全局检测 if (optCheckboxCount >= 2) return "1"; if (optRadioCount === 2) { if (options && options.length === 2) { const joined = options.map(o => o.toLowerCase().replace(/^[a-z][.、\s]*/i, '')).join(''); if ((joined.includes('对') && joined.includes('错')) || (joined.includes('正确') && joined.includes('错误')) || (joined.includes('true') && joined.includes('false')) || (joined.includes('是') && joined.includes('否'))) return "3"; } return "0"; } if (optRadioCount > 2) return "0"; } } catch (e) {} // ===== 方法1:通过全局DOM元素计数识别(借鉴jinmu.js的简洁高效方式)===== const radioCount = qEl.querySelectorAll('input[type="radio"]').length; const checkboxCount = qEl.querySelectorAll('input[type="checkbox"]').length; const textareaCount = qEl.querySelectorAll('textarea').length; // 填空题优先检测 if (textareaCount >= 1) { return "2"; // 填空题 } // 判断题:恰好2个radio选项 if (radioCount === 2) { if (options && options.length === 2) { const joined = options.map(o => o.toLowerCase().replace(/^[a-z][.、\s]*/i, '')).join(''); if ((joined.includes('对') && joined.includes('错')) || (joined.includes('正确') && joined.includes('错误')) || (joined.includes('true') && joined.includes('false')) || (joined.includes('是') && joined.includes('否'))) { return "3"; } } return "0"; } // 多选题:有checkbox if (checkboxCount >= 2) return "1"; // 单选题:radio数量大于2 if (radioCount > 2) return "0"; // ===== 方法2:增强的class类名检测(覆盖更多前端框架的自定义样式)===== try { // 检测类似checkbox的类名模式 const checkClassPatterns = '.checkbox, [class*="check"], [class*="checkBox"], [class*="Checkbox"], ' + '[class*="chk"], [class*="multi"], [class*="Multi"], ' + '[class*="select-multiple"], [class*="multiple-select"], ' + '.el-checkbox, .ivu-checkbox, .ant-checkbox, .van-checkbox, ' + '[class*="el-checkbox"], [class*="ivu-checkbox"], [class*="ant-checkbox"]'; const checkClassEls = qEl.querySelectorAll(checkClassPatterns); if (checkClassEls.length >= 2) return "1"; // 检测类似radio的类名模式 const radioClassPatterns = '.radio, [class*="radio"], [class*="Radio"], ' + '.el-radio, .ivu-radio, .ant-radio, .van-radio, ' + '[class*="el-radio"], [class*="ivu-radio"], [class*="ant-radio"], ' + '[class*="single"], [class*="Single"], ' + '[class*="select-single"], [class*="single-select"]'; const radioClassEls = qEl.querySelectorAll(radioClassPatterns); if (radioClassEls.length >= 2) { if (radioClassEls.length === 2 && options && options.length === 2) { const joined = options.map(o => o.toLowerCase().replace(/^[a-z][.、\s]*/i, '')).join(''); if ((joined.includes('对') && joined.includes('错')) || (joined.includes('正确') && joined.includes('错误'))) return "3"; } return "0"; } } catch (e) {} // ===== 方法3:通过DOM层级遍历查找题型标签(模仿jinmu.js)===== try { const typeSelectors = [ '.subject_type', '.q-type', '[class*="questionType"]', '.stem_type', '[class*="Type"]', 'div[class*="type"]', '.mark_type', '.letterSortNum', // 学习通章节测验题型标签 '.question-type', '[class*="question-type"]', '.qtype', '[class*="qtype"]', '.type-tag', '[class*="type-tag"]', // 查找题目序号后的题型标签 'span[class*="num"]', 'span[class*="order"]' ]; for (const sel of typeSelectors) { const el = qEl.querySelector(sel); if (el) { const t = el.textContent.trim(); if (t.includes('多选题')) return "1"; if (t.includes('单选题')) return "0"; if (t.includes('判断题')) return "3"; if (t.includes('填空题')) return "2"; } } } catch (e) {} // ===== 方法4:检测明确的题型标签文本(支持括号类型如【】、()、[]等)===== const bracketPatterns = [ /[【\[](?:多选|单选|判断|填空)[题]?[】\]]/i, /(?:多选|单选|判断|填空)[题]/ ]; for (const pattern of bracketPatterns) { const match = text.match(pattern); if (match) { const matched = match[0]; if (matched.includes('多选')) return "1"; if (matched.includes('判断')) return "3"; if (matched.includes('填空')) return "2"; if (matched.includes('单选')) return "0"; } } // ===== 方法5:检测文本关键词(含中英文常见变体)===== if (/(?:多选|多项选择|multiple|不定项)/i.test(lowerText)) return "1"; if (/(?:判断|是非|对错|true.or.false|T\s*\/\s*F)/i.test(lowerText)) return "3"; // ===== 方法6:选项特征判断 ===== if (options && options.length >= 2) { if (options.length === 2) { const joined = options.map(o => o.toLowerCase().replace(/^[a-z][.、\s]*/i, '')).join(''); if ((joined.includes('对') && joined.includes('错')) || (joined.includes('正确') && joined.includes('错误')) || (joined.includes('是') && joined.includes('否')) || (joined.includes('true') && joined.includes('false')) || (joined.includes('√') && joined.includes('×'))) return "3"; } // 当选项≥5时更可能是多选题(单选题通常3-4个选项) if (options.length >= 5 && options.length <= 8) return "1"; } // ===== 方法7:检测文本输入框(填空题)===== try { if (qEl.querySelectorAll('input[type="text"], input:not([type]), input:not([type="radio"]):not([type="checkbox"])').length >= 2) return "2"; } catch (e) {} // ===== 方法8:最终兜底 - 根据选项数量智能推断 ===== if (options && options.length > 4) return "1"; // >4选项更可能是多选 return "0"; // 默认单选题 } // 保持向后兼容的函数 function detectMultiSelect(qEl, options, optEls) { return detectQuestionType(qEl, options, optEls) === "1"; } function detectJudgement(qEl, options) { return detectQuestionType(qEl, options, []) === "3"; } function startAutoLoop(){ if(__autoTaskTimer || __autoTaskWorkerId) return; console.log('[autoAnswer] 启动自动答题循环'); // 使用 Web Worker 实现后台运行(不受浏览器标签页节流影响) const workerId = 'auto_loop_' + Date.now(); __autoTaskWorkerId = workerId; BackgroundWorker.start(workerId, async () => { await Scheduler.schedule(async () => { await autoAnswerOnce(); autoSubmitIfReady(); }); }, 2000); console.log('[autoAnswer] 已启动循环(Web Worker后台模式)'); } function stopAutoLoop(){ if(__autoTaskTimer){ clearInterval(__autoTaskTimer); __autoTaskTimer = null; } if(__autoTaskWorkerId){ BackgroundWorker.stop(__autoTaskWorkerId); __autoTaskWorkerId = null; } console.log('[autoAnswer] 已停止循环'); } function findSubmitButton(){ try{ const candidates = Array.from(document.querySelectorAll('button,input[type=button],a,input[type=submit]')); for(const b of candidates){ const t = ((b.innerText||b.value||'') + '').trim(); if(!t) continue; if(/提交|交卷|提交作业|完成|交题|交卷确认|交作业/.test(t)){ if(!b.disabled && b.offsetParent !== null) return b; } } }catch(e){} return null; } // P2 优化:智能题目检测 - 通过内容特征识别题目 function detectQuestionsByContent(){ const results = []; const allElements = document.querySelectorAll('div, li, section, article'); for(const el of allElements){ const text = (el.innerText || el.textContent || '').trim(); if(!text || text.length < 10 || text.length > 2000) continue; // 题目特征检测 const hasQuestionPattern = ( // 包含题号 /第\s*\d+\s*题/.test(text) || /^\d+\s*[.、]/.test(text) || // 包含题型标识 /(单选题|多选题|判断题|填空题|问答题)/.test(text) || // 包含选项标识 /[A-D][.、\s]/.test(text) || /[1-4][.、\s]/.test(text) ); // 排除非题目元素 const isNotQuestion = ( el.querySelector('video, audio, iframe') || el.classList.contains('nav') || el.classList.contains('header') || el.classList.contains('footer') || el.id.includes('nav') || el.id.includes('menu') ); if(hasQuestionPattern && !isNotQuestion){ // 检查是否已经有更小的题目容器 const alreadyContained = results.some(r => r.contains(el)); if(!alreadyContained){ // 移除已包含当前元素的更大容器 const toRemove = results.filter(r => el.contains(r)); toRemove.forEach(r => { const idx = results.indexOf(r); if(idx !== -1) results.splice(idx, 1); }); results.push(el); } } } console.log(`[detectQuestionsByContent] 通过内容检测找到 ${results.length} 个题目`); return results; } function isQuestionAnswered(q){ try{ // P2 优化:增强已答题检测 - 多级检测策略 const qEl = q.qEl || q.element || q; if(!qEl || !qEl.querySelector) return false; // 策略 1:检测选中的单选/复选框 if(qEl.querySelector('input[type=radio]:checked, input[type=checkbox]:checked')) return true; // 策略 2:检测选项的选中状态类 const selectedClasses = [ 'is-checked', 'selected', 'answer-selected', 'active', 'checked', 'is-active', 'current', 'highlight' ]; for(const cls of selectedClasses){ if(qEl.querySelector(`.${cls}`)) return true; } // 策略 3:检测选项的 data 属性 if(qEl.querySelector('[data-state="selected"], [data-checked="true"], [aria-checked="true"]')) return true; // 策略 4:检测文本输入框是否有值 const inputs = qEl.querySelectorAll('input[type="text"], textarea'); for(const input of inputs){ if(input.value && input.value.trim()) return true; } // 策略 5:检测 options 对象(兼容旧接口) if(q && q.options){ for(const o of q.options){ if(o && o.el && o.el.classList){ for(const cls of selectedClasses){ if(o.el.classList.contains(cls)) return true; } } } } // 策略 6:检测 class 名称中包含 selected/checked 的元素 if(/checked|selected|active/i.test(qEl.className || '')) return true; }catch(e){ console.warn('[isQuestionAnswered] 检测异常', e); } return false; } let __submitAttemptCount = 0; const __maxSubmitAttempts = 3; let __lastSubmitTime = 0; const __submitCooldown = 3000; function autoSubmitIfReady(){ try{ const now = Date.now(); if(now - __lastSubmitTime < __submitCooldown){ console.log('[autoSubmit] 冷却中,跳过'); return; } const qs = parsePageQuestions(); if(!qs || !qs.length){ console.log('[autoSubmit] 未检测到题目'); return; } const totalQuestions = qs.length; const answeredQuestions = qs.filter(q => isQuestionAnswered(q)); const unanswered = qs.filter(q => !isQuestionAnswered(q)); const unansweredCount = unanswered.length; const answeredCount = answeredQuestions.length; console.log(`[autoSubmit] 题目统计: 总计${totalQuestions} | 已答${answeredCount} | 未答${unansweredCount}`); if(unansweredCount > 0){ console.log(`[autoSubmit] 还有${unansweredCount}道题未作答,暂不提交`); if(__submitAttemptCount > 0){ console.log('[autoSubmit] 重置提交计数器'); __submitAttemptCount = 0; } return; } if(__submitAttemptCount >= __maxSubmitAttempts){ console.log(`[autoSubmit] 已达最大提交尝试次数(${__maxSubmitAttempts}),停止自动提交`); return; } const btn = findSubmitButton(); if(!btn){ console.log('[autoSubmit] 未找到提交按钮'); return; } __lastSubmitTime = now; __submitAttemptCount++; // 更新进度为提交中 try{ if(typeof useProgressStore === 'function'){ const ps = useProgressStore(); ps.update({ taskName: '正在提交试卷...', percent: 100, type: '提交', detail: `全部${totalQuestions}题已作答,正在提交`, isPlaying: true }); } }catch(e){} try{ const btnText = (btn.innerText || btn.value || '提交').trim(); console.log(`[autoSubmit] 尝试提交 (${__submitAttemptCount}/${__maxSubmitAttempts}): "${btnText}"`); btn.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => { try{ const success = safeClick(btn); if(success){ console.log('[autoSubmit] 提交按钮点击成功'); // 更新进度为提交成功 try{ if(typeof useProgressStore === 'function'){ const ps = useProgressStore(); ps.update({ taskName: '提交成功!', percent: 100, type: '完成', detail: `全部${totalQuestions}题已提交`, isPlaying: false }); } }catch(e){} setTimeout(() => { const stillUnanswered = parsePageQuestions().filter(q => !isQuestionAnswered(q)); if(stillUnanswered.length === 0){ console.log('[autoSubmit] 提交后验证通过,所有题目已答'); __submitAttemptCount = 0; } else { console.log(`[autoSubmit] 提交后仍有${stillUnanswered.length}题未答,可能需要重新作答`); } }, 1500); } else { console.warn('[autoSubmit] 提交按钮点击失败'); } }catch(e){ console.error('[autoSubmit] 提交点击异常:', e); } }, 500); }catch(e){ console.error('[autoSubmit] 提交流程异常:', e); __submitAttemptCount--; } }catch(e){ console.warn('[autoSubmit] 外层异常:', e); } } // 将控制函数暴露到 window,便于调试与手工触发 try{ window.scriptfor_startAuto = startAutoLoop; window.scriptfor_stopAuto = stopAutoLoop; window.scriptfor_autoOnce = autoAnswerOnce; }catch(e){} // === 配置菜单(Tampermonkey)=== function showConfigDialog(){ const html = `

⚙️ 脚本配置

`; const div = document.createElement('div'); div.id = 'scriptfor_config_dialog'; div.innerHTML = html; document.body.appendChild(div); div.querySelector('#cfg_save').addEventListener('click', async () => { const passphrase = div.querySelector('#cfg_passphrase').value.trim(); const basekey = div.querySelector('#cfg_basekey').value.trim(); const card = div.querySelector('#cfg_card').value.trim(); const statusEl = div.querySelector('#cfg_status'); // 保存配置 if(passphrase) GM_setValue('_server_passphrase', passphrase); if(basekey) GM_setValue('deepseek_basekey', basekey); statusEl.style.display = 'block'; // 激活卡密 if(card){ statusEl.style.background = '#e6f7ff'; statusEl.style.color = '#1890ff'; statusEl.textContent = '正在激活卡密...'; const result = await ClientLicense.redeemSelfContained(card); if(result && result.ok){ statusEl.style.background = '#f6ffed'; statusEl.style.color = '#52c41a'; statusEl.textContent = `激活成功!剩余次数:${result.uses_remaining || '未知'}`; div.querySelector('#cfg_card').value = ''; } else { statusEl.style.background = '#fff1f0'; statusEl.style.color = '#ff4d4f'; statusEl.textContent = '激活失败:' + (result && result.message || '未知错误'); } } else if(passphrase || basekey){ statusEl.style.background = '#f6ffed'; statusEl.style.color = '#52c41a'; statusEl.textContent = '配置已保存'; } }); div.querySelector('#cfg_close').addEventListener('click', () => { div.remove(); }); // 点击背景关闭 div.querySelector('div').addEventListener('click', (e) => { if(e.target === div.querySelector('div')) div.remove(); }); } // 注册 Tampermonkey 菜单 try{ if(typeof GM_registerMenuCommand === 'function'){ GM_registerMenuCommand('⚙️ 配置脚本', showConfigDialog); GM_registerMenuCommand('📊 查看授权状态', () => { ClientLicense.checkToken().then(st => { const info = st && st.ok ? `授权有效,剩余:${st.uses_remaining} 次` : '未授权或已过期'; alert(info); }); }); GM_registerMenuCommand('🗑️ 清除授权', () => { if(confirm('确定要清除授权信息吗?')){ ClientLicense.clearLicense(); alert('授权信息已清除'); } }); } }catch(e){} // 本地 QA 数据库(简易,带元数据) const LocalDB = { key: '_local_qa_db_v1', getAll() { try { const raw = Store.get(this.key, {}); return raw || {}; } catch (e) { return {}; } }, saveAll(db) { try { Store.set(this.key, db); } catch (e) { } }, queryExact(q) { try { const db = this.getAll(); const entry = db[q]; return entry || null; } catch (e) { return null; } }, queryBest(q, threshold = 0.85) { // 返回本地缓存中的最佳匹配,分数 ≥ threshold 才算可靠 try { const db = this.getAll(); const keys = Object.keys(db || {}); if (!keys.length) return null; // 精确匹配优先 if (db[q]) return { question: q, entry: db[q], score: 1.0, exact: true }; // 相似匹配 const best = StringUtil.findBestMatch(q, keys); if (best && best.bestMatch && best.bestMatch.rating >= threshold) { const entry = db[best.bestMatch.target]; if (entry) return { question: best.bestMatch.target, entry, score: best.bestMatch.rating, exact: false }; } } catch (e) { } return null; }, querySimilar(q, threshold = 0.85) { try { const db = this.getAll(); const keys = Object.keys(db || {}); if (!keys.length) return null; const best = StringUtil.findBestMatch(q, keys); if (best && best.bestMatch && best.bestMatch.rating >= threshold) { const entry = db[best.bestMatch.target]; if (entry) return { question: best.bestMatch.target, entry, score: best.bestMatch.rating }; } } catch (e) { } return null; }, addMapping(q, answerText, meta = {}) { try { const db = this.getAll(); const now = Date.now(); const saveScore = meta.score !== undefined ? meta.score : 0.5; const existing = db[q]; if (existing) { if (saveScore > (existing.score || 0)) { existing.answer = (typeof answerText === 'string') ? answerText : (answerText && answerText.text) || String(answerText); existing.source = meta.source || existing.source || 'auto'; existing.score = saveScore; existing.ts = now; existing.uses = (existing.uses || 0) + 1; this.saveAll(db); } else { existing.uses = (existing.uses || 0) + 1; existing.ts = now; this.saveAll(db); } } else { db[q] = { answer: (typeof answerText === 'string') ? answerText : (answerText && answerText.text) || String(answerText), source: meta.source || 'auto', score: saveScore, ts: now, uses: 1 }; this.saveAll(db); } } catch (e) { } }, removeMapping(q) { try { const db = this.getAll(); delete db[q]; this.saveAll(db); } catch (e) { } } }; // --- END: 适配器、调度器与自动答题骨架 --- var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => { __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; var _sfc_main89 = vue.defineComponent({ name: "DocumentRemove", __name: "document-remove", setup(__props) { return (_ctx, _cache) => (vue.openBlock(), vue.createElementBlock("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 1024 1024" }, [ vue.createElementVNode("path", { fill: "currentColor", d: "M805.504 320 640 154.496V320zM832 384H576V128H192v768h640zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m192 512h320v64H352z" }) ])); } }), document_remove_default = _sfc_main89; var _sfc_main118 = vue.defineComponent({ name: "FullScreen", __name: "full-screen", setup(__props) { return (_ctx, _cache) => (vue.openBlock(), vue.createElementBlock("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 1024 1024" }, [ vue.createElementVNode("path", { fill: "currentColor", d: "m160 96.064 192 .192a32 32 0 0 1 0 64l-192-.192V352a32 32 0 0 1-64 0V96h64zm0 831.872V928H96V672a32 32 0 1 1 64 0v191.936l192-.192a32 32 0 1 1 0 64zM864 96.064V96h64v256a32 32 0 1 1-64 0V160.064l-192 .192a32 32 0 1 1 0-64zm0 831.872-192-.192a32 32 0 0 1 0-64l192 .192V672a32 32 0 1 1 64 0v256h-64z" }) ])); } }), full_screen_default = _sfc_main118; var _sfc_main169 = vue.defineComponent({ name: "Minus", __name: "minus", setup(__props) { return (_ctx, _cache) => (vue.openBlock(), vue.createElementBlock("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 1024 1024" }, [ vue.createElementVNode("path", { fill: "currentColor", d: "M128 544h768a32 32 0 1 0 0-64H128a32 32 0 0 0 0 64" }) ])); } }), minus_default = _sfc_main169; var _sfc_main203 = vue.defineComponent({ name: "Position", __name: "position", setup(__props) { return (_ctx, _cache) => (vue.openBlock(), vue.createElementBlock("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 1024 1024" }, [ vue.createElementVNode("path", { fill: "currentColor", d: "m249.6 417.088 319.744 43.072 39.168 310.272L845.12 178.88zm-129.024 47.168a32 32 0 0 1-7.68-61.44l777.792-311.04a32 32 0 0 1 41.6 41.6l-310.336 775.68a32 32 0 0 1-61.44-7.808L512 516.992z" }) ])); } }), position_default = _sfc_main203; var _sfc_main231 = vue.defineComponent({ name: "Setting", __name: "setting", setup(__props) { return (_ctx, _cache) => (vue.openBlock(), vue.createElementBlock("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 1024 1024" }, [ vue.createElementVNode("path", { fill: "currentColor", d: "M600.704 64a32 32 0 0 1 30.464 22.208l35.2 109.376c14.784 7.232 28.928 15.36 42.432 24.512l112.384-24.192a32 32 0 0 1 34.432 15.36L944.32 364.8a32 32 0 0 1-4.032 37.504l-77.12 85.12a357 357 0 0 1 0 49.024l77.12 85.248a32 32 0 0 1 4.032 37.504l-88.704 153.6a32 32 0 0 1-34.432 15.296L708.8 803.904c-13.44 9.088-27.648 17.28-42.368 24.512l-35.264 109.376A32 32 0 0 1 600.704 960H423.296a32 32 0 0 1-30.464-22.208L357.696 828.48a352 352 0 0 1-42.56-24.64l-112.32 24.256a32 32 0 0 1-34.432-15.36L79.68 659.2a32 32 0 0 1 4.032-37.504l77.12-85.248a357 357 0 0 1 0-48.896l-77.12-85.248A32 32 0 0 1 79.68 364.8l88.704-153.6a32 32 0 0 1 34.432-15.296l112.32 24.256c13.568-9.152 27.776-17.408 42.56-24.64l35.2-109.312A32 32 0 0 1 423.232 64H600.64zm-23.424 64H446.72l-36.352 113.088-24.512 11.968a294 294 0 0 0-34.816 20.096l-22.656 15.36-116.224-25.088-65.28 113.152 79.68 88.192-1.92 27.136a293 293 0 0 0 0 40.192l1.92 27.136-79.808 88.192 65.344 113.152 116.224-25.024 22.656 15.296a294 294 0 0 0 34.816 20.096l24.512 11.968L446.72 896h130.688l36.48-113.152 24.448-11.904a288 288 0 0 0 34.752-20.096l22.592-15.296 116.288 25.024 65.28-113.152-79.744-88.192 1.92-27.136a293 293 0 0 0 0-40.256l-1.92-27.136 79.808-88.128-65.344-113.152-116.288 24.96-22.592-15.232a288 288 0 0 0-34.752-20.096l-24.448-11.904L577.344 128zM512 320a192 192 0 1 1 0 384 192 192 0 0 1 0-384m0 64a128 128 0 1 0 0 256 128 128 0 0 0 0-256" }) ])); } }), setting_default = _sfc_main231; var _sfc_main283 = vue.defineComponent({ name: "View", __name: "view", setup(__props) { return (_ctx, _cache) => (vue.openBlock(), vue.createElementBlock("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 1024 1024" }, [ vue.createElementVNode("path", { fill: "currentColor", d: "M512 160c320 0 512 352 512 352S832 864 512 864 0 512 0 512s192-352 512-352m0 64c-225.28 0-384.128 208.064-436.8 288 52.608 79.872 211.456 288 436.8 288 225.28 0 384.128-208.064 436.8-288-52.608-79.872-211.456-288-436.8-288m0 64a224 224 0 1 1 0 448 224 224 0 0 1 0-448m0 64a160.19 160.19 0 0 0-160 160c0 88.192 71.744 160 160 160s160-71.808 160-160-71.744-160-160-160" }) ])); } }), view_default = _sfc_main283; var _sfc_main288 = vue.defineComponent({ name: "Warning", __name: "warning", setup(__props) { return (_ctx, _cache) => (vue.openBlock(), vue.createElementBlock("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 1024 1024" }, [ vue.createElementVNode("path", { fill: "currentColor", d: "M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896m0 832a384 384 0 0 0 0-768 384 384 0 0 0 0 768m48-176a48 48 0 1 1-96 0 48 48 0 0 1 96 0m-48-464a32 32 0 0 1 32 32v288a32 32 0 0 1-64 0V288a32 32 0 0 1 32-32" }) ])); } }), warning_default = _sfc_main288; var _GM_getResourceText = (() => typeof GM_getResourceText != "undefined" ? GM_getResourceText : void 0)(); var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)(); var _GM_info = (() => typeof GM_info != "undefined" ? GM_info : void 0)(); var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)(); var _GM_xmlhttpRequest = (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)(); var _unsafeWindow = (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)(); const getScriptInfo = () => { const cloudVersion = window.__CLOUD_SCRIPT_VERSION__; return { name: _GM_info.script.name, author: _GM_info.script.author, namespace: _GM_info.script.namespace, version: cloudVersion || _GM_info.script.version, description: _GM_info.script.description }; }; const clearTempCache = () => { try { const keysToKeep = ['config', 'pinia', 'vue-devtools']; const allKeys = Object.keys(localStorage); allKeys.forEach(key => { if (!keysToKeep.some(keepKey => key.includes(keepKey))) { localStorage.removeItem(key); } }); sessionStorage.clear(); console.log('[学习通助手] 临时缓存已清理,用户配置已保留'); } catch (e) { console.log('[学习通助手] 清理缓存时出错:', e); } }; const useConfigStore = pinia.defineStore("configStore", { state: () => { const scriptInfo = getScriptInfo(); const defaultConfig = { version: scriptInfo.version, isMinus: false, tokenVerified: false, tokenVerifyError: null, position: { x: "800px", y: "200px" }, menuIndex: "main-log", platformName: "cx", platformParams: { cx: { name: `Daybreak超星网课助手-daybreak友情提供 v${scriptInfo.version}`, parts: [ { name: "视频设置", params: [ { name: "模拟播放", value: false, type: "boolean", exclusiveGroup: "playMode" }, { name: "正常播放", value: true, type: "boolean", exclusiveGroup: "playMode" }, { name: "规避检测", value: false, type: "boolean", dependsOn: { param: "模拟播放", value: true } }, { name: "直接上报", value: false, type: "boolean", dependsOn: { param: "模拟播放", value: true } }, { name: "视频答题", value: true, type: "boolean", dependsOn: { param: "正常播放", value: true } }, { name: "自动倍速", value: false, type: "boolean" }, { name: "播放倍速", value: 1, type: "number", min: 1, max: 3, step: 0.5 } ] }, { name: "答题参数", params: [ { name: "正常模式", value: true, type: "boolean", exclusiveGroup: "mode" }, { name: "答案校验", value: false, type: "boolean", exclusiveGroup: "mode" }, { name: "AI模式", value: false, type: "boolean", exclusiveGroup: "mode" }, { name: "AI 类型选择", value: "混元", type: "string", options: ["混元", "DeepSeek", "MiniMax", "Qwen", "GLM", "ChatGPT", "Gemini"] }, { name: "AI 模型选择", value: "Standard", type: "string", options: ["Standard", "T1", "3.2", "R1", "M2.5", "3", "5.4-nano", "5.4", "3.1", "4.7", "5.0"] }, { name: "跳过已答", value: true, type: "boolean" }, { name: "相似匹配", value: true, type: "boolean" }, { name: "模拟延迟", value: true, type: "boolean" }, { name: "答题间隔", value: 1, type: "number" }, { name: "正确阈值", value: 70, type: "number" } ] }, { name: "章节/作业/测验设置", params: [ { name: "自动提交", value: false, type: "boolean" }, { name: "自动切换", value: true, type: "boolean" }, { name: "正常模式", value: true, type: "boolean", exclusiveGroup: "cx_mode" }, { name: "仅视频", value: false, type: "boolean", exclusiveGroup: "cx_mode" }, { name: "仅答题", value: false, type: "boolean", exclusiveGroup: "cx_mode" } ] }, { name: "考试设置", params: [ { name: "自动切换", value: true, type: "boolean" } ] }, { name: "其他设置", params: [ { name: "激活挂机", value: false, type: "boolean" } ] } ] }, zhs: { name: "智慧树网课助手", parts: [ { name: "答题参数", params: [ { name: "正常模式", value: true, type: "boolean", exclusiveGroup: "mode" }, { name: "答案校验", value: false, type: "boolean", exclusiveGroup: "mode" }, { name: "AI模式", value: false, type: "boolean", exclusiveGroup: "mode" }, { name: "AI 类型选择", value: "混元", type: "string", options: ["混元", "DeepSeek", "MiniMax", "Qwen", "GLM", "ChatGPT", "Gemini"] }, { name: "AI 模型选择", value: "Standard", type: "string", options: ["Standard", "T1", "3.2", "R1", "M2.5", "3", "5.4-nano", "5.4", "3.1", "4.7", "5.0"] }, { name: "跳过已答", value: true, type: "boolean" }, { name: "相似匹配", value: true, type: "boolean" }, { name: "模拟延迟", value: true, type: "boolean" }, { name: "答题间隔", value: 1, type: "number" }, { name: "正确阈值", value: 70, type: "number" } ] }, { name: "答题设置", params: [{ name: "自动切换", value: true, type: "boolean" }] } ] }, unknown: { name: "通用平台", parts: [{ name: "答题设置", params: [{ name: "自动切换", value: true, type: "boolean" }] }] } }, queryApis: [ { name: "题库", token: "" } ] }; let globalConfig = defaultConfig; const storedConfig = _GM_getValue("config"); clearTempCache(); if (storedConfig) { try { const parsedStoredConfig = JSON.parse(storedConfig); if (scriptInfo.version === parsedStoredConfig.version) { globalConfig = parsedStoredConfig; if (globalConfig.platformParams && globalConfig.platformParams.cx) { globalConfig.platformParams.cx.name = `超星网课助手-daybreak友情提供 v${scriptInfo.version}`; } if (!globalConfig.platformParams) { globalConfig.platformParams = defaultConfig.platformParams; } if (!globalConfig.queryApis) { globalConfig.queryApis = defaultConfig.queryApis; } } else { globalConfig = defaultConfig; globalConfig.version = scriptInfo.version; if (parsedStoredConfig.position) { globalConfig.position = parsedStoredConfig.position; } if (parsedStoredConfig.queryApis && parsedStoredConfig.queryApis.length > 0) { parsedStoredConfig.queryApis.forEach((oldApi, index) => { if (globalConfig.queryApis[index] && oldApi.token) { globalConfig.queryApis[index].token = oldApi.token; } }); } if (parsedStoredConfig.platformParams) { Object.keys(parsedStoredConfig.platformParams).forEach((platformKey) => { const oldPlatform = parsedStoredConfig.platformParams[platformKey]; const newPlatform = globalConfig.platformParams[platformKey]; if (oldPlatform && newPlatform && oldPlatform.parts) { oldPlatform.parts.forEach((oldPart, partIndex) => { if (newPlatform.parts[partIndex] && oldPart.params) { oldPart.params.forEach((oldParam) => { const newParam = newPlatform.parts[partIndex].params.find( p => p.name === oldParam.name ); if (newParam) { newParam.value = oldParam.value; } }); } }); } }); } if (parsedStoredConfig.otherParams && parsedStoredConfig.otherParams.params) { const otherParamsPart = globalConfig.platformParams.cx.parts.find(p => p.name === "答题参数"); if (otherParamsPart && otherParamsPart.params) { const nameMapping = { "答案校验模式": "答案校验", "跳过已答题": "跳过已答", "相似度答案匹配": "相似匹配", "答题正确率": "正确阈值" }; const normalModeParam = otherParamsPart.params.find(p => p.name === "正常模式"); const aiModeParam = otherParamsPart.params.find(p => p.name === "AI模式"); const answerVerifyParam = otherParamsPart.params.find(p => p.name === "答案校验"); if (normalModeParam && aiModeParam && answerVerifyParam) { const oldNormalMode = parsedStoredConfig.otherParams.params.find(p => p.name === "正常模式"); const oldAiMode = parsedStoredConfig.otherParams.params.find(p => p.name === "AI模式"); const oldAnswerVerify = parsedStoredConfig.otherParams.params.find(p => p.name === "答案校验模式"); normalModeParam.value = false; aiModeParam.value = false; answerVerifyParam.value = false; if (oldAnswerVerify && oldAnswerVerify.value) { answerVerifyParam.value = true; } else if (oldAiMode && oldAiMode.value) { aiModeParam.value = true; } else { normalModeParam.value = true; } } parsedStoredConfig.otherParams.params.forEach((oldParam) => { const newName = nameMapping[oldParam.name] || oldParam.name; const newParam = otherParamsPart.params.find(p => p.name === newName); if (newParam && !['正常模式', 'AI模式', '答案校验'].includes(newName)) { newParam.value = oldParam.value; } }); } } } } catch (error) { console.error(error); } } _GM_setValue("globalConfig", JSON.stringify(globalConfig)); return globalConfig; }, actions: {} }); const useLogStore = pinia.defineStore("logStore", { state: () => ({ logList: [] }), actions: { addLog(message, type) { const log = { message, time: getDateTime(), type }; this.logList.push(log); } } }); const useQuestionStore = pinia.defineStore("questionStore", { state: () => ({ questionList: [] }), actions: { addQuestion(question) { this.questionList.push(question); }, clearQuestion() { this.questionList = []; } } }); const useProgressStore = pinia.defineStore("progressStore", { state: () => ({ taskName: "暂无任务", percent: 0, currentTime: 0, totalTime: 0, type: "-", detail: "等待任务开始", isPlaying: false, speedDisabled: false }), actions: { update(progress) { this.taskName = progress.taskName || this.taskName; this.percent = Math.min(100, Math.max(0, progress.percent || 0)); this.currentTime = progress.currentTime || this.currentTime; this.totalTime = progress.totalTime || this.totalTime; this.type = progress.type || this.type; this.detail = progress.detail || this.detail; this.isPlaying = progress.isPlaying !== void 0 ? progress.isPlaying : this.isPlaying; this.speedDisabled = progress.speedDisabled !== void 0 ? progress.speedDisabled : this.speedDisabled; }, reset(message = "等待任务开始") { this.taskName = "暂无任务"; this.percent = 0; this.currentTime = 0; this.totalTime = 0; this.type = "-"; this.detail = message; this.isPlaying = false; this.speedDisabled = false; } } }); const _sfc_main$8 = vue.defineComponent({ __name: "index", props: { logList: { type: Array, required: true }, serverConfig: { type: Object, default: () => ({ url: "", location: "未知", color: "#888" }) }, progress: { type: Object, default: () => ({ taskName: "暂无任务", percent: 0, currentTime: 0, totalTime: 0, type: "-", detail: "等待任务开始", isPlaying: false }) } }, setup(__props) { const progressStore = useProgressStore(); const configStore = useConfigStore(); const logStore = useLogStore(); const formatTime = (seconds) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; const scrollbarRef = vue.ref(null); // 剩余答题次数 const remainingCount = vue.ref(null); const isFreeTrial = vue.ref(false); const _forceUpdateTick = vue.ref(0); // 强制更新计数器 // 日志筛选功能 const logFilter = vue.ref('all'); // 'all', 'success', 'danger', 'warning', 'primary', 'info' const filteredLogList = vue.computed(() => { if (logFilter.value === 'all') { return __props.logList; } return __props.logList.filter(log => log.type === logFilter.value); }); vue.watch(() => __props.logList.length, (newLen, oldLen) => { if (newLen > oldLen) { vue.nextTick(() => { vue.nextTick(() => { if (scrollbarRef.value) { const wrap = scrollbarRef.value.wrapRef || scrollbarRef.value.$refs.wrap; if (wrap) { wrap.scrollTop = wrap.scrollHeight; } else { scrollbarRef.value.setScrollTop(99999); } } }); }); } }); const showCloudVersionWarning = vue.ref(false); const compareVersion = (v1, v2) => { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { const p1 = parts1[i] || 0; const p2 = parts2[i] || 0; if (p1 > p2) return 1; if (p1 < p2) return -1; } return 0; }; const checkCloudVersion = async () => { const isCloudVersion = typeof window !== "undefined" && window.__CLOUD_SCRIPT_VERSION__; if (!isCloudVersion) { showCloudVersionWarning.value = false; return; } // 离线模式 - 无云端版本检查 showCloudVersionWarning.value = false; }; // 检查授权状态 const checkLicenseStatus = async () => { try { const status = await ClientLicense.checkToken(); if (status && status.ok) { remainingCount.value = status.uses_remaining; isFreeTrial.value = !!status.isFreeTrial; } else if (status && status.tampered) { remainingCount.value = 0; isFreeTrial.value = false; } else if (status && status.needsRedeem) { // 需要兑换,但尝试从云端恢复次数 remainingCount.value = 0; isFreeTrial.value = false; } else { isFreeTrial.value = false; } } catch (e) { console.error('[License] 检查失败', e); } }; vue.onMounted(() => { checkCloudVersion(); checkLicenseStatus(); const unlisten = EventBus.on('license:updated', (data) => { console.log('[主面板] 收到 license:updated 事件:', data); if (data.uses_remaining !== null && data.uses_remaining !== undefined) { remainingCount.value = data.uses_remaining; isFreeTrial.value = !!data.isFreeTrial; // 关键修复:强制触发Vue响应式更新 _forceUpdateTick.value++; console.log('[主面板] UI强制更新, 剩余次数:', remainingCount.value, 'tick:', _forceUpdateTick.value); } else { checkLicenseStatus(); } }); vue.onUnmounted(() => { unlisten(); }); }); return (_ctx, _cache) => { const _component_el_text = vue.resolveComponent("el-text"); const _component_el_divider = vue.resolveComponent("el-divider"); const _component_el_scrollbar = vue.resolveComponent("el-scrollbar"); const forceUpdateKey = 'script-home-' + __props.logList.length; return vue.openBlock(), vue.createElementBlock("div", { key: forceUpdateKey }, [ vue.createElementVNode("div", { style: { "margin-bottom": "15px", "padding": "12px 15px", "background": "#fff", "border-radius": "12px", "border": "1px solid #e5e7eb", "display": "flex", "align-items": "center", "gap": "12px", "box-shadow": "0 2px 8px rgba(0,0,0,0.05)" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "justify-content": "center", "width": "36px", "height": "36px", "border-radius": "8px", "background": __props.serverConfig.color, "color": "#fff", "font-weight": "600", "font-size": "14px", "box-shadow": `0 2px 8px ${__props.serverConfig.color}66` } }, vue.toDisplayString(__props.serverConfig.location.charAt(0)), 1), vue.createElementVNode("div", null, [ vue.createElementVNode("div", { style: { "font-size": "13px", "color": "#333", "font-weight": "500" } }, [ _cache[0] || (_cache[0] = vue.createTextVNode("运行模式: ")), vue.createElementVNode("span", { style: { "color": __props.serverConfig.color, "font-weight": "600" } }, vue.toDisplayString(__props.serverConfig.location || '本地'), 1) ]), vue.createElementVNode("div", { style: { "font-size": "11px", "color": "#999", "margin-top": "4px" } }, "智能匹配引擎已激活") ]) ]), vue.withDirectives(vue.createElementVNode("div", { style: { "margin-bottom": "15px", "padding": "15px", "background": "#ff6b6b", "border-radius": "12px", "box-shadow": "0 4px 12px rgba(238,90,90,0.3)", "color": "#fff" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "10px", "margin-bottom": "8px" } }, [ vue.createElementVNode("span", { style: { "font-size": "20px" } }, "⚠️"), vue.createElementVNode("span", { style: { "font-weight": "600", "font-size": "14px" } }, "云端用户请注意") ]), vue.createElementVNode("div", { style: { "font-size": "13px", "opacity": "0.95", "line-height": "1.6" } }, [ vue.createElementVNode("p", { style: { "margin": "0 0 8px 0" } }, "请更新您的云端脚本到最新版本,否则可能无法正常使用!"), vue.createElementVNode("a", { href: "https://scriptcat.org/zh-CN/script-show-page/5723", target: "_blank", style: { "color": "#fff", "text-decoration": "underline", "font-size": "12px" } }, "📦 点击此处获取最新版本") ]) ], 4), [ [vue.vShow, showCloudVersionWarning.value] ]), vue.createElementVNode("div", { style: { "margin-bottom": "15px", "padding": "15px", "background": "#4facfe", "border-radius": "12px", "box-shadow": "0 4px 12px rgba(79,172,254,0.2)", "color": "#fff" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "10px", "margin-bottom": "8px" } }, [ vue.createElementVNode("span", { style: { "font-size": "20px" } }, "✨"), vue.createElementVNode("span", { style: { "font-weight": "600", "font-size": "14px" } }, "欢迎使用学习助手") ]), vue.createElementVNode("div", { style: { "font-size": "12px", "opacity": "0.95", "line-height": "1.6" } }, "🎯 支持视频+测验+考试 | ⚡ 可调节倍速 | 🔥 自动答题 | 💪 高题库覆盖率 | 📱 支持手机平板 | 🔧 快捷键 Ctrl+O 显示/隐藏面板") ]), vue.createElementVNode("div", { key: 'answer-count-' + _forceUpdateTick.value, style: { "margin-bottom": "15px", "padding": "12px 15px", "background": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", "border-radius": "12px", "box-shadow": "0 4px 15px rgba(118,75,162,0.3)", "color": "#fff", "display": "flex", "align-items": "center", "justify-content": "space-between" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "10px" } }, [ vue.createElementVNode("span", { style: { "font-size": "22px" } }, "🎫"), vue.createElementVNode("div", null, [ vue.createElementVNode("div", { style: { "font-size": "13px", "font-weight": "600", "marginBottom": "2px" } }, "答题次数"), vue.createElementVNode("div", { style: { "font-size": "11px", "opacity": "0.85" } }, "AI答题/答案校验消耗") ]) ]), vue.createElementVNode("div", { style: { "display": "flex", "alignItems": "baseline", "gap": "6px" } }, [ vue.createElementVNode("span", { style: { "font-size": "28px", "fontWeight": "700", "lineHeight": "1" } }, vue.toDisplayString(remainingCount.value !== null ? remainingCount.value : '--'), 1), vue.createElementVNode("span", { style: { "fontSize": "12px", "opacity": "0.9" } }, "次") ]) ]), vue.createElementVNode("div", { style: { "margin-bottom": "15px", "padding": "15px", "border": "1px solid #e5e7eb", "border-radius": "12px", "background": "#f5f7fa", "box-shadow": "0 2px 8px rgba(0,0,0,0.05)" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "10px", "margin-bottom": "10px" } }, [ vue.createElementVNode("span", { style: { "font-size": "18px" } }, "🚀"), vue.createElementVNode("span", { style: { "font-size": "14px", "font-weight": "600", "color": "#333" } }, "运行状态") ]), vue.createElementVNode("div", { key: 'progress-' + progressStore.currentTime, style: { "width": "95%", "margin": "0 auto 10px", "padding": "12px 15px", "border-radius": "8px", "background": "#f9f9f9", "border": "1px solid #e0e0e0", "display": progressStore.isPlaying ? "block" : "none" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "justify-content": "space-between", "align-items": "baseline", "margin-bottom": "4px" } }, [ vue.createElementVNode("div", { style: { "flex": "1", "min-width": "0" } }, [ vue.createElementVNode("div", { style: { "font-size": "11px", "color": "#6c757d", "margin-bottom": "2px" } }, "当前任务"), vue.createElementVNode("div", { style: { "font-size": "14px", "font-weight": "600", "color": "#212529", "white-space": "nowrap", "overflow": "hidden", "text-overflow": "ellipsis", "max-width": "250px" } }, vue.toDisplayString(progressStore.taskName), 1) ]), vue.createElementVNode("div", { style: { "font-size": "20px", "font-weight": "700", "color": "#17a2b8", "white-space": "nowrap" } }, vue.toDisplayString(progressStore.percent) + "%", 1) ]), vue.createElementVNode("div", { style: { "margin": "4px 0 6px", "font-size": "11px", "color": "#6c757d" } }, vue.toDisplayString("类型:" + progressStore.type)), vue.createElementVNode("div", { style: { "width": "100%", "height": "14px", "border-radius": "999px", "background": "#e9ecef", "overflow": "hidden", "position": "relative" } }, [ vue.createElementVNode("div", { style: { "position": "absolute", "top": "0", "left": "0", "height": "100%", "width": progressStore.percent + "%", "border-radius": "999px", "background": "#0dcaf0", "transition": "width 0.3s ease" } }, null, 8, ["style"]) ]), vue.createElementVNode("div", { style: { "margin-top": "6px", "font-size": "12px", "color": "#495057", "font-weight": "500", "line-height": "1.4", "white-space": "nowrap" } }, vue.toDisplayString(formatTime(progressStore.currentTime) + " / " + formatTime(progressStore.totalTime)), 1), progressStore.speedDisabled ? vue.createElementVNode("div", { style: { "margin-top": "8px", "padding": "6px 10px", "background": "#fff3cd", "border": "1px solid #ffc107", "border-radius": "6px", "font-size": "11px", "color": "#856404", "text-align": "center", "line-height": "1.4" } }, "⚠️ 此视频已被学习通禁用倍速,>1x可能导致学习进度被清空") : vue.createCommentVNode("", true) ]), vue.createElementVNode("p", { style: { "font-size": "13px", "color": "#555", "margin-bottom": "8px", "line-height": "1.6" } }, "脚本正在运行中,请不要多个脚本同时使用 🚫"), vue.createElementVNode("div", { style: { "color": "#888", "font-size": "12px", "padding": "8px", "background": "rgba(255,255,255,0.5)", "border-radius": "6px" } }, "💡 如果脚本出现异常,请使用谷歌、火狐等浏览器 ") ]), vue.createElementVNode("div", { style: { "margin-bottom": "15px", "border": "1px solid #e1f5fe", "border-radius": "12px", "box-shadow": "0 2px 12px rgba(0,0,0,0.08)", "overflow": "hidden" } }, [ vue.createElementVNode("div", { style: { "background": "#fa709a", "padding": "12px 15px", "color": "#fff", "display": "flex", "align-items": "center", "gap": "8px" } }, [ vue.createElementVNode("span", { style: { "font-size": "16px" } }, "📊"), vue.createElementVNode("span", { style: { "font-weight": "600", "font-size": "14px" } }, "运行日志") ]), // 日志筛选按钮 vue.createElementVNode("div", { style: { "padding": "8px 15px", "background": "#f5f7fa", "border-bottom": "1px solid #e5e7eb", "display": "flex", "gap": "6px", "flex-wrap": "wrap" } }, [ vue.createElementVNode("span", { style: { "fontSize": "12px", "color": "#666", "lineHeight": "28px" } }, "筛选:"), vue.createElementVNode("button", { type: "button", onClick: (e) => { e.preventDefault(); logFilter.value = 'all'; vue.nextTick(); }, style: { "padding": "4px 10px", "fontSize": "12px", "borderRadius": "4px", "cursor": "pointer", "transition": "all 0.15s ease", "fontWeight": logFilter.value === 'all' ? "600" : "400" }, class: logFilter.value === 'all' ? "btn-log-filter active" : "btn-log-filter" }, "全部"), vue.createElementVNode("button", { type: "button", onClick: (e) => { e.preventDefault(); logFilter.value = 'success'; vue.nextTick(); }, style: { "padding": "4px 10px", "fontSize": "12px", "borderRadius": "4px", "cursor": "pointer", "transition": "all 0.15s ease", "fontWeight": logFilter.value === 'success' ? "600" : "400" }, class: logFilter.value === 'success' ? "btn-log-filter active" : "btn-log-filter" }, "成功"), vue.createElementVNode("button", { type: "button", onClick: (e) => { e.preventDefault(); logFilter.value = 'danger'; vue.nextTick(); }, style: { "padding": "4px 10px", "fontSize": "12px", "borderRadius": "4px", "cursor": "pointer", "transition": "all 0.15s ease", "fontWeight": logFilter.value === 'danger' ? "600" : "400" }, class: logFilter.value === 'danger' ? "btn-log-filter active" : "btn-log-filter" }, "错误"), vue.createElementVNode("button", { type: "button", onClick: (e) => { e.preventDefault(); logFilter.value = 'warning'; vue.nextTick(); }, style: { "padding": "4px 10px", "fontSize": "12px", "borderRadius": "4px", "cursor": "pointer", "transition": "all 0.15s ease", "fontWeight": logFilter.value === 'warning' ? "600" : "400" }, class: logFilter.value === 'warning' ? "btn-log-filter active" : "btn-log-filter" }, "警告"), vue.createElementVNode("button", { type: "button", onClick: (e) => { e.preventDefault(); logFilter.value = 'primary'; vue.nextTick(); }, style: { "padding": "4px 10px", "fontSize": "12px", "borderRadius": "4px", "cursor": "pointer", "transition": "all 0.15s ease", "fontWeight": logFilter.value === 'primary' ? "600" : "400" }, class: logFilter.value === 'primary' ? "btn-log-filter active" : "btn-log-filter" }, "信息") ]), vue.createElementVNode("div", { style: { "padding": "15px", "background": "#fafcfe" } }, [ vue.createVNode(_component_el_scrollbar, { ref: scrollbarRef, always: "", class: "log", height: "280px" }, { default: vue.withCtx(() => [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(filteredLogList.value, (item, index) => { return vue.openBlock(), vue.createElementBlock("div", { key: index }, [ vue.createVNode(_component_el_text, { size: "small", style: { "font-weight": "normal" }, type: "info" }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(item.time), 1) ]), _: 2 }, 1024), vue.createVNode(_component_el_text, null, { default: vue.withCtx(() => _cache[1] || (_cache[1] = [ vue.createTextVNode(" ") ])), _: 1 }), vue.createVNode(_component_el_text, { type: item.type ? item.type : "primary", size: "small", innerHTML: item.message }, null, 8, ["type", "innerHTML"]), vue.createVNode(_component_el_divider, { "border-style": "dashed", style: { "margin": "0" } }) ]); }), 128)) ]), _: 1 }) ]) ]), vue.createElementVNode("div", { style: { "margin-top": "15px", "padding": "16px", "background": "linear-gradient(135deg, #f093fb 0%, #f5576c 100%)", "border-radius": "12px", "text-align": "center", "box-shadow": "0 6px 20px rgba(245,87,108,0.35)", "border": "2px solid rgba(255,255,255,0.3)", "position": "relative", "overflow": "hidden" } }, [ vue.createElementVNode("div", { style: { "position": "absolute", "top": "0", "left": "0", "right": "0", "bottom": "0", "background": "radial-gradient(circle at 30% 50%, rgba(255,255,255,0.15) 0%, transparent 60%)", "pointer-events": "none" } }), vue.createElementVNode("p", { style: { "margin": "0 0 8px 0", "font-size": "16px", "color": "#fff", "font-weight": "700", "text-shadow": "0 2px 4px rgba(0,0,0,0.2)", "position": "relative", "z-index": "1" } }, "💬 官方QQ群:796069615"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "12px", "color": "rgba(255,255,255,0.95)", "position": "relative", "z-index": "1" } }, [ vue.createElementVNode("a", { href: "https://pay.ldxp.cn/shop/3K2UNT5M", target: "_blank", style: { "display": "inline-block", "padding": "3px 8px", "border-radius": "999px", "background": "#fff200", "color": "#d60032", "font-weight": "700", "text-decoration": "none", "box-shadow": "0 2px 8px rgba(0,0,0,0.22)", "cursor": "pointer" } }, "获取卡密"), vue.createTextVNode(" | 问题反馈 | 交流经验 | 最新版本") ]) ]) ]); }; } }); const _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; const ScriptHome = _export_sfc(_sfc_main$8, [["__scopeId", "data-v-83e6bb0c"]]); const _hoisted_1$4 = { class: "setting" }; const _hoisted_2$2 = { style: { "font-size": "13px" } }; const _hoisted_3$1 = { style: { "font-size": "13px" } }; const _sfc_main$7 = vue.defineComponent({ __name: "index", props: { globalConfig: { type: Object, required: true } }, setup(__props) { const configStore = useConfigStore(); const logStore = useLogStore(); const verifyState = vue.ref({ status: '', message: '' }); const pingDelay = vue.ref(null); const remainingCount = vue.ref(null); const isFreeTrial = vue.ref(false); const existingTokens = vue.ref([]); const _forceUpdateTick = vue.ref(0); const modelCosts = vue.ref({}); const fetchModelCosts = async () => { // 离线模式 - 无服务端模型成本 modelCosts.value = {}; }; const checkLicenseStatus = async () => { try { const status = await ClientLicense.checkToken(); if (status && status.ok) { remainingCount.value = status.uses_remaining; isFreeTrial.value = !!status.isFreeTrial; if (status.isFreeTrial) { logStore.addLog(`🎁 新用户免费试用,剩余 ${status.uses_remaining} 次`, 'success'); } } else if (status && status.tampered) { remainingCount.value = 0; isFreeTrial.value = false; logStore.addLog('⚠️ 授权异常:检测到篡改', 'danger'); } else if (status && status.needsRedeem) { remainingCount.value = 0; isFreeTrial.value = false; } else { // 未授权,保持 null(会显示"请验证Token") isFreeTrial.value = false; } } catch (e) { console.error('[License] 检查失败', e); } }; vue.onMounted(() => { fetchModelCosts(); checkLicenseStatus(); // 关键修复:监听 license:updated 事件,确保答题次数扣减后UI能实时更新 const unlisten = EventBus.on('license:updated', (data) => { console.log('[License] 收到 license:updated 事件:', data); if (data.uses_remaining !== null && data.uses_remaining !== undefined) { remainingCount.value = data.uses_remaining; isFreeTrial.value = !!data.isFreeTrial; console.log('[License] UI已更新, 剩余次数:', remainingCount.value); } else { // 如果事件中没有提供次数,重新从license读取 checkLicenseStatus(); } }); vue.onUnmounted(() => { unlisten(); }); }); const consumptionText = vue.computed(() => { void _forceUpdateTick.value; const answerParamsPart = __props.globalConfig.platformParams?.[__props.globalConfig.platformName]?.parts?.find(p => p.name === "答题参数"); if (!answerParamsPart) return "每题消耗: 1 次"; const aiModeParam = answerParamsPart.params.find(p => p.name === "AI模式"); const normalModeParam = answerParamsPart.params.find(p => p.name === "正常模式"); const verifyModeParam = answerParamsPart.params.find(p => p.name === "答案校验"); const aiTypeParam = answerParamsPart.params.find(p => p.name === "AI 类型选择"); const aiModelParam = answerParamsPart.params.find(p => p.name === "AI 模型选择"); const isAiModeActive = aiModeParam && aiModeParam.value; const isNormalModeActive = normalModeParam && normalModeParam.value; const isVerifyModeActive = verifyModeParam && verifyModeParam.value; if (isNormalModeActive || (!isAiModeActive && !isVerifyModeActive)) { return "每题消耗: 1 次"; } else if (isVerifyModeActive) { return "每题消耗: 2 次"; } else if (isAiModeActive && aiTypeParam && aiModelParam) { const currentAiType = aiTypeParam.value; const currentAiModel = aiModelParam.value; let modelType = 'DeepSeek-V3.2'; if (currentAiType === 'DeepSeek') { modelType = currentAiModel === 'R1' ? 'DeepSeek-R1-0528' : 'DeepSeek-V3.2'; } else if (currentAiType === '混元') { modelType = currentAiModel === 'T1' ? 'tencent-hunyuan-t1' : 'tencent-hunyuan-standard'; } const cost = modelCosts.value[modelType] || 1; return `每题消耗: ${cost} 次`; } return "每题消耗: 1 次"; }); const remainingCountStyle = vue.computed(() => { if (remainingCount.value === null) return {}; if (isFreeTrial.value) { return { "font-size": "12px", "color": "#1565c0", "font-weight": "600", "background": "rgba(21,101,192,0.15)", "padding": "4px 10px", "border-radius": "12px" }; } const count = remainingCount.value; return { "font-size": "12px", "color": count > 100 ? "#2e7d32" : count > 20 ? "#f57c00" : "#c62828", "font-weight": "600", "background": count > 100 ? "rgba(76,175,80,0.2)" : count > 20 ? "rgba(255,152,0,0.2)" : "rgba(198,40,40,0.2)", "padding": "4px 10px", "border-radius": "12px" }; }); const pingDelayStyle = vue.computed(() => { if (pingDelay.value === null) return {}; const delay = pingDelay.value; return { "font-size": "12px", "color": delay === -1 ? "#c62828" : delay < 500 ? "#2e7d32" : delay < 1000 ? "#f57c00" : "#e65100", "background": delay === -1 ? "rgba(198,40,40,0.2)" : delay < 500 ? "rgba(76,175,80,0.2)" : delay < 1000 ? "rgba(255,152,0,0.2)" : "rgba(230,81,0,0.2)", "padding": "4px 10px", "border-radius": "12px", "font-weight": "600" }; }); let debounceTimer = null; const debounce = (fn, delay) => { return (...args) => { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(() => fn(...args), delay); }; }; const autoVerifyToken = debounce(() => { const currentToken = vue.unref(configStore).queryApis[0].token; // 支持两种Token格式: // 1. 16位纯数字格式 // 2. XXXX-XXXX-XXXX-XXXX-NNNN-HHHHHHHH 格式 const tokenPattern1 = /^\d{16}$/; const tokenPattern2 = /^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-F0-9]{4}-[A-F0-9]{8}$/i; if (!currentToken || (!tokenPattern1.test(currentToken) && !tokenPattern2.test(currentToken))) { return; } logStore.addLog('💡 检测到有效的Token格式,自动验证中...', 'info'); verifyToken(); }, 800); const selectToken = (token) => { if (token) { vue.unref(configStore).queryApis[0].token = token; existingTokens.value = []; logStore.addLog('已填入Token,正在验证...', 'success'); setTimeout(() => verifyToken(), 100); } }; let lastVerifyTime = 0; const verifyToken = () => { const now = Date.now(); if (now - lastVerifyTime < 1000) { return; } lastVerifyTime = now; const card = vue.unref(configStore).queryApis[0].token; if (!card) { } else { // 支持两种Token格式: // 1. 16位纯数字格式 // 2. XXXX-XXXX-XXXX-XXXX-NNNN-HHHHHHHH 格式 const tokenPattern1 = /^\d{16}$/; const tokenPattern2 = /^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-F0-9]{4}-[A-F0-9]{8}$/i; if (!tokenPattern1.test(card.trim()) && !tokenPattern2.test(card.trim())) { logStore.addLog('❌ 卡密格式错误,应为 16位数字 或 XXXX-XXXX-XXXX-XXXX-NNNN-HHHHHHHH 格式', 'danger'); verifyState.value = { status: 'error', message: '卡密格式错误,应为 16位数字 或 XXXX-XXXX-XXXX-XXXX-NNNN-HHHHHHHH 格式' }; configStore.tokenVerified = false; configStore.tokenVerifyError = 'format'; return; } } verifyState.value = { status: 'testing', message: '正在激活卡密...' }; logStore.addLog('正在激活卡密...', 'primary'); ClientLicense.redeemSelfContained(card).then(result => { if(result && result.ok){ const remaining = result.uses_remaining || 0; let displayNum = remaining; if (remaining === -1 || remaining === Infinity) { remainingCount.value = Infinity; displayNum = '无限'; } else { remainingCount.value = remaining; } verifyState.value = { status: 'success', message: `激活成功!剩余次数: ${displayNum}次` }; configStore.tokenVerified = true; configStore.tokenVerifyError = null; logStore.addLog(`卡密激活成功,剩余次数: ${displayNum}次`, 'success'); EventBus.emit('license:updated', { uses_remaining: remaining, isFreeTrial: false }); } else { remainingCount.value = 0; verifyState.value = { status: 'error', message: '激活失败:' + (result && result.message || '未知错误') }; configStore.tokenVerified = false; configStore.tokenVerifyError = 'invalid'; logStore.addLog('❌ 卡密激活失败: ' + (result && result.message || '未知错误'), 'danger'); } }).catch(e => { remainingCount.value = 0; verifyState.value = { status: 'error', message: '激活异常: ' + e.message }; configStore.tokenVerified = false; configStore.tokenVerifyError = 'network'; logStore.addLog('❌ 激活异常: ' + e.message, 'danger'); }); }; const compareVersion = (v1, v2) => { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); for (let i = 0; i < 3; i++) { if ((parts1[i] || 0) < (parts2[i] || 0)) return -1; if ((parts1[i] || 0) > (parts2[i] || 0)) return 1; } return 0; }; const isMajorUpdate = (currentVersion, latestVersion) => { const parts1 = currentVersion.split('.').map(Number); const parts2 = latestVersion.split('.').map(Number); return (parts1[0] || 0) < (parts2[0] || 0) || (parts1[1] || 0) < (parts2[1] || 0); }; // 离线模式 - 无服务端版本检查 const checkUpdate = () => { const currentVersion = _GM_info?.script?.version || '1.1.2'; logStore.addLog(`脚本版本 v${currentVersion}`, 'success'); }; const showUpdateDialog = (latestVersion, updateUrl, updateMessage) => { const dialog = document.createElement('div'); dialog.id = 'update-dialog'; const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:999999;display:flex;align-items:center;justify-content:center;'; const card = document.createElement('div'); card.style.cssText = 'background:#fff;border-radius:12px;padding:24px;max-width:400px;box-shadow:0 8px 32px rgba(0,0,0,0.2);text-align:center;'; const icon = document.createElement('div'); icon.textContent = '🎉'; icon.style.cssText = 'font-size:48px;margin-bottom:16px;'; const title = document.createElement('h3'); title.textContent = '学习通小助手发现新版本'; title.style.cssText = 'margin:0 0 12px;color:#333;font-size:18px;'; const versionText = document.createElement('p'); versionText.textContent = `当前版本: v${latestVersion}`; versionText.style.cssText = 'margin:0 0 8px;color:#666;font-size:14px;'; const messageText = document.createElement('p'); messageText.textContent = updateMessage || '建议更新以获得更好体验'; messageText.style.cssText = 'margin:0 0 20px;color:#999;font-size:12px;'; const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = 'display:flex;gap:12px;justify-content:center;'; const updateBtn = document.createElement('button'); updateBtn.id = 'update-btn'; updateBtn.textContent = '立即更新'; updateBtn.style.cssText = 'background:var(--jb-primary);color:#fff;border:none;padding:10px 24px;border-radius:var(--jb-btn-radius);cursor:pointer;font-size:14px;'; updateBtn.onclick = () => window.open(updateUrl, '_blank'); const laterBtn = document.createElement('button'); laterBtn.id = 'later-btn'; laterBtn.textContent = '稍后提醒'; laterBtn.style.cssText = 'background:#f5f5f5;color:#666;border:none;padding:10px 24px;border-radius:6px;cursor:pointer;font-size:14px;'; laterBtn.onclick = () => dialog.remove(); buttonContainer.appendChild(updateBtn); buttonContainer.appendChild(laterBtn); card.appendChild(icon); card.appendChild(title); card.appendChild(versionText); card.appendChild(messageText); card.appendChild(buttonContainer); overlay.appendChild(card); dialog.appendChild(overlay); document.body.appendChild(dialog); }; vue.onMounted(() => { checkUpdate(); logStore.addLog('🔄 脚本启动,请输入卡密并点击验证按钮激活', 'primary'); }); return (_ctx, _cache) => { const _component_el_checkbox = vue.resolveComponent("el-checkbox"); const _component_el_input_number = vue.resolveComponent("el-input-number"); const _component_el_form_item = vue.resolveComponent("el-form-item"); const _component_el_button = vue.resolveComponent("el-button"); const _component_el_input = vue.resolveComponent("el-input"); const _component_el_radio_group = vue.resolveComponent("el-radio-group"); const _component_el_radio_button = vue.resolveComponent("el-radio-button"); const _component_el_select = vue.resolveComponent("el-select"); const _component_el_option = vue.resolveComponent("el-option"); return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$4, [ vue.createElementVNode("div", { style: { "margin-bottom": "12px", "padding": "12px 15px", "background": "#e8f5e9", "border-radius": "10px", "box-shadow": "0 2px 10px rgba(76,175,80,0.12)" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "justify-content": "space-between", "margin-bottom": "8px" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "6px" } }, [ vue.createElementVNode("span", { style: { "font-size": "16px" } }, "🔑"), vue.createElementVNode("span", { style: { "font-size": "14px", "font-weight": "600", "color": "#2e7d32" } }, "卡密激活") ]), vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "6px" } }, [ remainingCount.value !== null ? (vue.openBlock(), vue.createElementBlock("span", { key: 0, style: vue.normalizeStyle(remainingCountStyle.value) }, "剩余: " + (remainingCount.value === Infinity ? '无限' : vue.toDisplayString(remainingCount.value)) + "次" + (isFreeTrial.value ? " (免费试用)" : ""), 5)) : vue.createCommentVNode("", true), pingDelay.value !== null ? (vue.openBlock(), vue.createElementBlock("span", { key: 1, style: vue.normalizeStyle(pingDelayStyle.value) }, vue.toDisplayString(pingDelay.value === -1 ? "连接失败" : pingDelay.value + "ms"), 5)) : vue.createCommentVNode("", true) ]) ]), vue.createElementVNode("p", { style: { "margin": "0 0 8px 0", "font-size": "12px", "color": "#666", "background": "rgba(255,255,255,0.5)", "padding": "5px 8px", "border-radius": "5px" } }, "💡 输入卡密进行激活(格式:XXXX-XXXX-XXXX-XXXX-NNNN-HHHHHHHH)"), vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "8px", "flex-wrap": "wrap" } }, [ vue.createElementVNode("span", { style: { "font-size": "13px", "color": "#424242", "font-weight": "500" } }, "卡密:"), vue.createVNode(_component_el_input, { style: { "flex": "1", "min-width": "180px" }, modelValue: vue.unref(configStore).queryApis[0].token, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => { vue.unref(configStore).queryApis[0].token = $event; vue.unref(configStore).tokenVerified = false; vue.unref(configStore).tokenVerifyError = null; verifyState.value = { status: '', message: '' }; }), placeholder: "请输入卡密", clearable: true, size: "small" }, null, 8, ["modelValue"]), vue.createVNode(_component_el_button, { type: "success", onClick: verifyToken, loading: verifyState.value.status === 'testing', style: { "border-radius": "6px" } }, { default: vue.withCtx(() => _cache[1] || (_cache[1] = [ vue.createTextVNode("验证") ])), _: 1 }, 8, ["onClick", "loading"]) ]), (!vue.unref(configStore).queryApis[0].token || verifyState.value.status) ? (vue.openBlock(), vue.createElementBlock("div", { key: 0, style: { "margin-top": "10px", "padding": "8px 10px", "border-radius": "5px", "border": "2px solid", "border-color": !vue.unref(configStore).queryApis[0].token ? "#ffc107" : verifyState.value.status === 'success' ? "#4caf50" : verifyState.value.status === 'error' ? "#dc3545" : "#ffc107", "background-color": !vue.unref(configStore).queryApis[0].token ? "#fff8e1" : verifyState.value.status === 'success' ? "#e8f5e9" : verifyState.value.status === 'error' ? "#f8d7da" : "#fff8e1" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "flex-direction": "column", "gap": "8px" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "6px" } }, [ vue.createElementVNode("span", { style: { "font-size": "14px" } }, vue.toDisplayString( !vue.unref(configStore).queryApis[0].token ? "⚠️" : verifyState.value.status === 'success' ? "✅" : verifyState.value.status === 'error' ? "❌" : "⏳" ), 1), vue.createElementVNode("span", { style: { "font-size": "13px", "color": !vue.unref(configStore).queryApis[0].token ? "#856404" : verifyState.value.status === 'success' ? "#2e7d32" : verifyState.value.status === 'error' ? "#721c24" : "#856404" } }, vue.toDisplayString( !vue.unref(configStore).queryApis[0].token ? "请输入有效的用户Token进行验证" : verifyState.value.message ), 1) ]), (!vue.unref(configStore).queryApis[0].token || verifyState.value.status === 'error' || (verifyState.value.status === 'success' && remainingCount.value <= 0)) ? (vue.openBlock(), vue.createElementBlock("div", { key: 0, style: { "margin-top": "6px", "text-align": "center", "font-size": "12px", "color": "#666" } }, vue.toDisplayString( !vue.unref(configStore).queryApis[0].token ? "请在上方输入卡密进行激活" : verifyState.value.status === 'error' ? "激活失败,请检查卡密是否正确" : "次数已用完,请激活新卡密" ))) : vue.createCommentVNode("", true) ]) ], 4)) : vue.createCommentVNode("", true) ]), vue.createElementVNode("div", { style: { "border": "1px solid #e1f5fe", "border-radius": "12px", "box-shadow": "0 2px 12px rgba(0,0,0,0.08)", "overflow": "hidden" } }, [ vue.createElementVNode("div", { style: { "background": "#667eea", "padding": "15px", "color": "#fff" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "10px", "margin-bottom": "8px" } }, [ vue.createElementVNode("span", { style: { "font-size": "20px" } }, "✨"), vue.createElementVNode("span", { style: { "font-weight": "600", "font-size": "14px" } }, "答题配置"), vue.createElementVNode("span", { style: { "font-size": "15px", "color": "#fff", "margin-left": "12px", "opacity": "0.7" } }, "⚡更改设置后需刷新网页激活") ]), vue.createElementVNode("div", { style: { "font-size": "12px", "opacity": "0.95", "line-height": "1.6" } }, "🎯 章节作业自动提交 | ⚡ 自动切换下一章节 | 🔥 支持考试模式 | 💪 灵活配置答题参数 | 🤖 已对接高质量AI题库") ]), vue.createElementVNode("div", { style: { "padding": "15px", "background": "#fafcfe" } }, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(__props.globalConfig.platformParams?.[__props.globalConfig.platformName]?.parts || [], (item, index, partsList) => { return vue.openBlock(), vue.createElementBlock("div", { key: index, style: { "margin-bottom": "15px", "padding-bottom": "15px", "border-bottom": "1px dashed #e0e0e0" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "justify-content": "space-between", "gap": "6px", "margin-bottom": "12px", "font-weight": "600", "font-size": "13px", "color": "#333" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "6px" } }, [ vue.createElementVNode("span", { style: { "font-size": "14px" } }, "⚙️"), vue.createElementVNode("span", null, vue.toDisplayString(item.name), 1) ]), item.name === "答题参数" ? vue.createElementVNode("span", { style: { "margin-left": "auto", "font-size": "12px", "color": "#ff9800", "font-weight": "500" } }, vue.toDisplayString(consumptionText.value), 1) : vue.createCommentVNode("", true) ]), item.name === "答题参数" ? vue.createElementVNode("div", { style: { "display": "block" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "flex-wrap": "wrap", "gap": "12px", "align-items": "flex-start", "margin-bottom": "10px" } }, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(item.params.filter(p => p.type === "boolean" && p.exclusiveGroup), (param, index2) => { return vue.openBlock(), vue.createBlock(_component_el_checkbox, { style: { "margin": "0" }, key: index2, modelValue: param.value, "onUpdate:modelValue": ($event) => { if (param.exclusiveGroup) { const parentPart = item; const sameGroupParams = parentPart.params.filter(p => p.type === "boolean" && p.exclusiveGroup === param.exclusiveGroup && p.name !== param.name ); if ($event) { sameGroupParams.forEach(otherParam => { otherParam.value = false; }); } else { const hasOtherActive = sameGroupParams.some(otherParam => otherParam.value); if (!hasOtherActive) { return; } } } param.value = $event; }, label: param.name }, null, 8, ["modelValue", "onUpdate:modelValue", "label"]); }), 128)), item.params.some(p => p.type === "string") ? (() => { const aiModeParam = item.params.find(p => p.name === "AI模式"); const isAiModeActive = aiModeParam && aiModeParam.value; const aiTypeParam = item.params.find(p => p.name === "AI 类型选择"); const aiModelParam = item.params.find(p => p.name === "AI 模型选择"); if (!aiTypeParam || !aiModelParam) return vue.createCommentVNode("", true); const currentAiType = aiTypeParam ? aiTypeParam.value : '\u6df7\u5143'; const TYPE_MODEL_MAP = { 'DeepSeek': ['R1', '3.2'], '混元': ['T1', 'Standard'] }; const defaultModels = { '\u6df7\u5143': 'Standard', 'DeepSeek': '3.2' }; const filteredOptions = TYPE_MODEL_MAP[currentAiType] || aiModelParam.options; const labelStyle = { "font-size": "12px", "color": isAiModeActive ? "#42a5f5" : "#9ca3af", "white-space": "nowrap", "transition": "color 0.3s ease" }; const radioStyle = { "--el-fill-color-light": "transparent", "opacity": isAiModeActive ? "1" : "0.4", "transition": "opacity 0.3s ease", "cursor": isAiModeActive ? "pointer" : "not-allowed" }; return vue.createElementVNode("div", { style: { "margin-left": "auto", "display": "flex", "flex-direction": "column", "gap": "4px" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "12px" } }, [ vue.createElementVNode("span", { style: labelStyle }, "AI 类型:"), vue.createVNode(_component_el_select, { modelValue: aiTypeParam.value, "onUpdate:modelValue": ($event) => { if (isAiModeActive) { aiTypeParam.value = $event; aiModelParam.value = defaultModels[$event] || 'Standard'; } }, size: "small", disabled: !isAiModeActive, style: { "width": "90px", "opacity": isAiModeActive ? "1" : "0.4" }, placeholder: "请选择", popperClass: "high-z-index-select" }, { default: vue.withCtx(() => [ vue.renderList(aiTypeParam.options || [], (option) => vue.createVNode(_component_el_option, { key: option, value: option, label: option }, null, 8, ["value", "label"])) ]) }, 8, ["modelValue", "onUpdate:modelValue", "disabled", "style"]), vue.createElementVNode("span", { style: {...labelStyle, "margin-left": "8px"} }, "AI 模型:"), vue.createVNode(_component_el_select, { modelValue: aiModelParam.value, "onUpdate:modelValue": ($event) => { if (isAiModeActive) aiModelParam.value = $event; }, size: "small", disabled: !isAiModeActive, style: { "width": "90px", "opacity": isAiModeActive ? "1" : "0.4" }, placeholder: "请选择", popperClass: "high-z-index-select" }, { default: vue.withCtx(() => [ vue.renderList(filteredOptions, (option) => vue.createVNode(_component_el_option, { key: option, value: option, label: option }, null, 8, ["value", "label"])) ]) }, 8, ["modelValue", "onUpdate:modelValue", "disabled", "style"]) ]) ]); })() : vue.createCommentVNode("", true) ]), vue.createElementVNode("div", { style: { "display": "flex", "flex-wrap": "wrap", "gap": "15px", "margin-bottom": "10px" } }, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(item.params.filter(p => p.type === "boolean" && !p.exclusiveGroup), (param, index2) => { return vue.openBlock(), vue.createBlock(_component_el_checkbox, { style: { "margin": "0" }, key: index2, modelValue: param.value, "onUpdate:modelValue": ($event) => { if (param.dependsOn) { const parentPart = item; const parentParam = parentPart.params.find(p => p.name === param.dependsOn.param); if (parentParam && parentParam.value !== param.dependsOn.value) { if ($event) { return; } else { param.value = $event; } return; } } param.value = $event; const parentPart = item; const dependentParams = parentPart.params.filter(p => p.type === "boolean" && p.dependsOn && p.dependsOn.param === param.name ); if (!$event && dependentParams.length > 0) { dependentParams.forEach(depParam => { depParam.value = false; }); } if ($event && dependentParams.length > 0) { dependentParams.forEach(depParam => { if (depParam.dependsOn && depParam.dependsOn.value !== $event) { depParam.value = false; } }); } _forceUpdateTick.value++; }, label: param.name, disabled: param.dependsOn ? (() => { void _forceUpdateTick.value; const parentPart = item; const parentParam = parentPart.params.find(p => p.name === param.dependsOn.param); return !parentParam || parentParam.value !== param.dependsOn.value; })() : false }, null, 8, ["modelValue", "onUpdate:modelValue", "label", "disabled"]); }), 128)) ]), vue.createElementVNode("div", { style: { "display": "flex", "flex-wrap": "wrap", "gap": "10px" } }, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(item.params.filter(p => p.type === "number"), (param, index2) => { return vue.openBlock(), vue.createBlock(_component_el_form_item, { key: index2, label: param.name, required: "", style: { "margin": "0" } }, { default: vue.withCtx(() => [ vue.createVNode(_component_el_input_number, { modelValue: param.value, "onUpdate:modelValue": ($event) => param.value = $event, min: param.min != null ? param.min : 1, max: param.max != null ? param.max : 100, step: param.step != null ? param.step : 1, precision: param.step && param.step < 1 ? 1 : 0, "controls-position": "right", style: { "width": "70px", "height": "28px" } }, null, 8, ["modelValue", "onUpdate:modelValue"]) ]), _: 2 }, 1032, ["label"]); }), 128)) ]) ]) : vue.createElementVNode("div", { style: { "display": "flex", "flex-wrap": "wrap", "gap": "10px" } }, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(item.params, (param, index2) => { return vue.openBlock(), vue.createElementBlock(vue.Fragment, { key: index2 }, [ (item.name === "视频设置" && (index2 === 2 || index2 === 5)) || (item.name === "章节/作业/测验设置" && index2 === 2) ? (vue.openBlock(), vue.createElementBlock("div", { key: "br-" + index2, style: { "width": "100%", "height": "0" } })) : vue.createCommentVNode("", true), param.type === "boolean" ? (vue.openBlock(), vue.createBlock(_component_el_checkbox, { style: { "margin": "0", "min-width": "4em" }, key: index2, modelValue: param.value, "onUpdate:modelValue": ($event) => { if (param.dependsOn) { const parentPart = item; const parentParam = parentPart.params.find(p => p.name === param.dependsOn.param); if (parentParam && parentParam.value !== param.dependsOn.value) { if ($event) { return; } else { param.value = $event; } return; } } if (param.exclusiveGroup) { const parentPart = item; const sameGroupParams = parentPart.params.filter(p => p.type === "boolean" && p.exclusiveGroup === param.exclusiveGroup && p.name !== param.name ); if ($event) { sameGroupParams.forEach(otherParam => { otherParam.value = false; const otherDependents = parentPart.params.filter(p => p.type === "boolean" && p.dependsOn && p.dependsOn.param === otherParam.name ); otherDependents.forEach(dp => { dp.value = false; }); }); } else { const hasOtherActive = sameGroupParams.some(otherParam => otherParam.value); if (!hasOtherActive) { return; } } } param.value = $event; if (param.name === "模拟播放" || param.name === "正常播放") { const playbackRateParam = item.params.find(p => p.name === "播放倍速"); if (playbackRateParam) { if (param.name === "模拟播放" && $event) { const autoMax = item.params.find(p => p.name === "自动倍速")?.value || false; if (autoMax) { let maxRate = window.__maxPlaybackRate; if (!maxRate) { try { const iframes = document.querySelectorAll('iframe'); for (const iframe of iframes) { try { if (iframe.contentDocument) { const menuItems = iframe.contentDocument.querySelectorAll('.vjs-playback-rate .vjs-menu-content .vjs-menu-item'); if (menuItems.length > 0) { maxRate = 1; menuItems.forEach(mi => { const text = mi.textContent.trim(); const rate = parseFloat(text.replace('x', '')); if (!isNaN(rate) && rate > maxRate) { maxRate = rate; } }); window.__maxPlaybackRate = maxRate; break; } } } catch (e) { continue; } } } catch (e) { console.error('[播放倍速] 获取播放器maxRate失败:', e); } } playbackRateParam.max = maxRate || 3; } else { playbackRateParam.max = 3; } if (playbackRateParam.value > playbackRateParam.max) { playbackRateParam.value = playbackRateParam.max; } } else if (param.name === "正常播放" && $event) { let maxRate = window.__maxPlaybackRate; if (!maxRate) { try { const iframes = document.querySelectorAll('iframe'); for (const iframe of iframes) { try { if (iframe.contentDocument) { const menuItems = iframe.contentDocument.querySelectorAll('.vjs-playback-rate .vjs-menu-content .vjs-menu-item'); if (menuItems.length > 0) { maxRate = 1; menuItems.forEach(mi => { const text = mi.textContent.trim(); const rate = parseFloat(text.replace('x', '')); if (!isNaN(rate) && rate > maxRate) { maxRate = rate; } }); window.__maxPlaybackRate = maxRate; break; } } } catch (e) { continue; } } } catch (e) { console.error('[播放倍速] 获取播放器maxRate失败:', e); } } playbackRateParam.max = maxRate || 3; } } } _forceUpdateTick.value++; if (param.name === "自动倍速") { const playbackRateParam = item.params.find(p => p.name === "播放倍速"); if (playbackRateParam) { const isSimulate = item.params.find(p => p.name === "模拟播放")?.value || false; if (isSimulate) { if ($event) { let maxRate = window.__maxPlaybackRate; if (!maxRate) { maxRate = 3; } playbackRateParam.max = maxRate; } else { playbackRateParam.max = 3; } if (playbackRateParam.value > playbackRateParam.max) { playbackRateParam.value = playbackRateParam.max; } } } } const parentPart = item; const dependentParams = parentPart.params.filter(p => p.type === "boolean" && p.dependsOn && p.dependsOn.param === param.name ); if (!$event && dependentParams.length > 0) { dependentParams.forEach(depParam => { depParam.value = false; }); } if ($event && dependentParams.length > 0) { dependentParams.forEach(depParam => { if (depParam.dependsOn && depParam.dependsOn.value !== $event) { depParam.value = false; } }); } }, label: param.name, disabled: param.dependsOn ? (() => { void _forceUpdateTick.value; const parentPart = item; const parentParam = parentPart.params.find(p => p.name === param.dependsOn.param); return !parentParam || parentParam.value !== param.dependsOn.value; })() : false }, null, 8, ["modelValue", "onUpdate:modelValue", "label", "disabled"])) : (vue.openBlock(), vue.createBlock(_component_el_form_item, { key: 1, label: param.name, required: "", style: { "margin": "0" } }, { default: vue.withCtx(() => [ vue.createVNode(_component_el_input_number, { modelValue: param.value, "onUpdate:modelValue": ($event) => param.value = $event, min: param.min != null ? param.min : 1, max: param.max != null ? param.max : 100, step: param.step != null ? param.step : 1, precision: param.step && param.step < 1 ? 1 : 0, "controls-position": "right", style: { "width": "70px", "height": "28px" }, disabled: item.name === "视频设置" && param.name === "播放倍速" ? (item.params.find(p => p.name === "自动倍速")?.value || false) : false }, null, 8, ["modelValue", "onUpdate:modelValue", "disabled"]) ]), _: 2 }, 1032, ["label"])) ], 64); }), 128)), item.name === "视频设置" ? (vue.openBlock(), vue.createElementBlock("div", { key: "video-tips", style: { "width": "100%", "margin-top": "12px", "padding-left": "0" } }, [ item.params.some(p => p.name === "模拟播放" && p.value) ? (vue.openBlock(), vue.createElementBlock("div", { key: 0, style: { "font-size": "11px", "color": "#ff9800", "margin-bottom": "6px", "padding": "8px", "background": "rgba(255, 152, 0, 0.1)", "border-radius": "6px", "line-height": "1.5" } }, "💡 模拟播放倍速与正常播放一致,使用播放器允许的最大值")) : vue.createCommentVNode("", true), !item.params.some(p => p.name === "模拟播放" && p.value) ? (vue.openBlock(), vue.createElementBlock("div", { key: 1, style: { "font-size": "11px", "color": "#4caf50", "margin-bottom": "6px", "padding": "8px", "background": "rgba(76, 175, 80, 0.1)", "border-radius": "6px", "line-height": "1.5" } }, "ℹ️ 普通播放倍速受播放器限制,使用播放器允许的最大值")) : vue.createCommentVNode("", true), item.params.some(p => p.name === "直接上报" && p.value) ? (vue.openBlock(), vue.createElementBlock("div", { key: 2, style: { "font-size": "11px", "color": "#f44336", "margin-bottom": "6px", "padding": "8px", "background": "rgba(244, 67, 54, 0.1)", "border-radius": "6px", "line-height": "1.5", "font-weight": "600" } }, "⚠️ 此功能可能引发封号,注意风险!")) : vue.createCommentVNode("", true) ])) : vue.createCommentVNode("", true), item.params.some(p => p.name === "答案校验" && p.value) ? (vue.openBlock(), vue.createElementBlock("div", { key: 3, style: { "width": "100%", "font-size": "11px", "color": "#888", "margin-top": "6px", "padding-left": "0", "line-height": "1.4" } }, "💡 同时请求题库与AI模型,确保正确率【此功能会双倍消耗答题次数】【此功能会显著增加答题时长】")) : vue.createCommentVNode("", true) ]) ]); }), 128)) ]) ]), vue.createElementVNode("div", { style: { "margin-top": "15px", "padding": "16px", "background": "linear-gradient(135deg, #f093fb 0%, #f5576c 100%)", "border-radius": "12px", "text-align": "center", "box-shadow": "0 6px 20px rgba(245,87,108,0.35)", "border": "2px solid rgba(255,255,255,0.3)", "position": "relative", "overflow": "hidden" } }, [ vue.createElementVNode("div", { style: { "position": "absolute", "top": "0", "left": "0", "right": "0", "bottom": "0", "background": "radial-gradient(circle at 30% 50%, rgba(255,255,255,0.15) 0%, transparent 60%)", "pointer-events": "none" } }), vue.createElementVNode("p", { style: { "margin": "0 0 8px 0", "font-size": "16px", "color": "#fff", "font-weight": "700", "text-shadow": "0 2px 4px rgba(0,0,0,0.2)", "position": "relative", "z-index": "1" } }, "💬 官方QQ群:796069615"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "12px", "color": "rgba(255,255,255,0.95)", "position": "relative", "z-index": "1" } }, [ vue.createElementVNode("a", { href: "https://pay.ldxp.cn/shop/3K2UNT5M", target: "_blank", style: { "display": "inline-block", "padding": "3px 8px", "border-radius": "999px", "background": "#fff200", "color": "#d60032", "font-weight": "700", "text-decoration": "none", "box-shadow": "0 2px 8px rgba(0,0,0,0.22)", "cursor": "pointer" } }, "获取卡密"), vue.createTextVNode(" | 问题反馈 | 交流经验 | 最新版本") ]) ]) ]); }; } }); const ScriptSetting = _export_sfc(_sfc_main$7, [["__scopeId", "data-v-9ea68a6a"]]); const _hoisted_1$3 = { class: "question_table" }; const _hoisted_2$1 = ["innerHTML"]; const _sfc_main$6 = vue.defineComponent({ __name: "QuestionTable", props: { questionList: { type: Array, required: true } }, setup(__props) { return (_ctx, _cache) => { const _component_el_table_column = vue.resolveComponent("el-table-column"); const _component_el_table = vue.resolveComponent("el-table"); const _component_el_empty = vue.resolveComponent("el-empty"); return vue.openBlock(), vue.createElementBlock(vue.Fragment, null, [ vue.createElementVNode("div", { style: { "margin-bottom": "15px", "padding": "15px", "border": "1px solid #e1f5fe", "border-radius": "12px", "box-shadow": "0 2px 12px rgba(0,0,0,0.08)", "overflow": "hidden" } }, [ vue.createElementVNode("div", { style: { "background": "#ff9a9e", "padding": "12px 15px", "color": "#fff", "display": "flex", "align-items": "center", "gap": "8px", "margin": "-15px -15px 15px -15px", "border-radius": "10px 10px 0 0" } }, [ vue.createElementVNode("span", { style: { "font-size": "16px" } }, "📋"), vue.createElementVNode("span", { style: { "font-weight": "600", "font-size": "14px" } }, "答题记录列表") ]), vue.withDirectives(vue.createElementVNode("div", { style: { "overflow-x": "auto", "overflow-y": "auto", "max-height": "400px", "padding-right": "5px" } }, [ vue.createVNode(_component_el_table, { stripe: "", data: __props.questionList, style: { "font-size": "12px", "width": "100%" } }, { default: vue.withCtx(() => [ vue.createVNode(_component_el_table_column, { type: "index", width: "50" }), vue.createVNode(_component_el_table_column, { prop: "title", label: "题目", "min-width": "200" }), vue.createVNode(_component_el_table_column, { prop: "answer", label: "答案", "min-width": "150" }, { default: vue.withCtx((scope) => [ vue.createElementVNode("div", { innerHTML: scope.row.source === "ai" ? scope.row.answer.join() + '[答案由AI提供]' : scope.row.answer.join() }, null, 8, _hoisted_2$1) ]), _: 1 }) ]), _: 1 }, 8, ["data"]) ], 512), [ [vue.vShow, __props.questionList.length] ]), vue.withDirectives(vue.createElementVNode("div", { style: { "padding": "40px 20px", "text-align": "center", "display": "flex", "flex-direction": "column", "align-items": "center", "gap": "16px" } }, [ vue.createVNode(_component_el_empty, { description: "暂无答题记录" }), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "12px", "color": "#8c8c8c" } }, "进入课程章节或考试页面即可自动答题") ], 512), [ [vue.vShow, !__props.questionList.length] ]), vue.createElementVNode("div", { style: { "margin-top": "15px", "padding": "12px", "background": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", "border-radius": "10px", "text-align": "center", "box-shadow": "0 4px 12px rgba(118,75,162,0.2)" } }, [ vue.createElementVNode("p", { style: { "margin": "0 0 6px 0", "font-size": "14px", "color": "#fff", "font-weight": "600" } }, "💬 官方QQ群:796069615"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "12px", "color": "rgba(255,255,255,0.85)" } }, [ vue.createElementVNode("a", { href: "https://pay.ldxp.cn/shop/3K2UNT5M", target: "_blank", style: { "display": "inline-block", "padding": "3px 8px", "border-radius": "999px", "background": "#fff200", "color": "#d60032", "font-weight": "700", "text-decoration": "none", "box-shadow": "0 2px 8px rgba(0,0,0,0.22)", "cursor": "pointer" } }, "获取卡密"), vue.createTextVNode(" | 问题反馈 | 交流经验") ]) ]) ]) ], 64); }; } }); const QuestionTable = _export_sfc(_sfc_main$6, [["__scopeId", "data-v-18523ca7"]]); const _sfc_main_referral = vue.defineComponent({ __name: "ReferralPanel", setup(__props) { const logStore = useLogStore(); // 立即同步初始化邀请码(在 setup 阶段,不依赖 onMounted) const initInviteCodeSync = () => { try { let code = ''; try { code = localStorage.getItem('_my_invite_code_v1') || ''; } catch(e) {} if (!code && typeof _GM_getValue === 'function') { try { code = _GM_getValue('_my_invite_code_v1', '') || ''; } catch(e) {} } if (code) { const normalized = String(code).trim().toUpperCase(); if (/^[A-Z0-9]{6}-[A-F0-9]{8}$/.test(normalized) || /^[A-Z0-9]{6}$/.test(normalized)) { return normalized; } } try { const codePart = ClientLicense && ClientLicense.generateInviteCodePart ? ClientLicense.generateInviteCodePart() : ''; if (codePart && /^[A-Z0-9]{6}$/.test(codePart)) { try { localStorage.setItem('_my_invite_code_v1', codePart); } catch(e) {} try { if (typeof _GM_setValue === 'function') { _GM_setValue('_my_invite_code_v1', codePart); } } catch(e) {} return codePart; } } catch(e) { console.warn('[学习通助手] 邀请码生成失败,使用降级方案:', e); } return '暂无'; } catch(e) { console.error('[学习通助手] 邀请码初始化异常:', e); return '暂无'; } }; // 推广奖励相关状态 const myInviteCode = vue.ref(initInviteCodeSync()); const inputCode = vue.ref(''); const submitting = vue.ref(false); const submitStatus = vue.ref(''); const submitMessage = vue.ref(''); const rewardResult = vue.ref({ added: 0 }); let submitStateTimer = null; let lastReferralActionAt = 0; // 使用computed派生显示值,避免模板中的复杂逻辑 const inviteCodeDisplayText = vue.computed(() => { const code = myInviteCode.value; if (!code || code === '') return '加载中...'; return code; }); const isInviteCodeValid = vue.computed(() => { const code = myInviteCode.value; return code && code !== '加载中...' && code !== '暂无'; }); const setSubmitState = (status, message) => { if(submitStateTimer) { clearTimeout(submitStateTimer); submitStateTimer = null; } submitStatus.value = status; submitMessage.value = message; logStore.addLog(message, status === 'success' ? 'success' : status === 'error' ? 'warning' : 'info'); if(status === 'success' || status === 'error') { submitStateTimer = setTimeout(() => { submitStatus.value = ''; submitMessage.value = ''; submitStateTimer = null; }, 9000); } }; // 卡密兑换相关状态 const redeemInput = vue.ref(''); const redeemStatus = vue.ref({ status: '', message: '' }); const redeemSubmitting = vue.ref(false); const remainingUses = vue.ref(''); const lastFailedInput = vue.ref(''); const activeTab = vue.ref('referral'); // 'referral' | 'redeem' // 使用computed派生tab显示状态 const isReferralTabActive = vue.computed(() => activeTab.value === 'referral'); const isRedeemTabActive = vue.computed(() => activeTab.value === 'redeem'); const switchInnerTab = (tab) => { activeTab.value = tab; }; const inviteCodeKey = '_my_invite_code_v1'; let redeemStatusTimer = null; const setRedeemStatus = (status, message) => { if(redeemStatusTimer) { clearTimeout(redeemStatusTimer); redeemStatusTimer = null; } redeemStatus.value = { status, message }; if(status === 'success' || status === 'error') { redeemStatusTimer = setTimeout(() => { redeemStatus.value = { status: '', message: '' }; redeemStatusTimer = null; }, 3000); } }; // 关键修复:使用 ref 来引用按钮元素 const referralTabBtn = vue.ref(null); const redeemTabBtn = vue.ref(null); // 生成带HMAC校验的邀请码(离线) const generateInviteCode = async () => { const codePart = ClientLicense.generateInviteCodePart(); let hmac = ''; try { if (ClientLicense && ClientLicense.computeInviteHmac) { hmac = await ClientLicense.computeInviteHmac(codePart); } } catch(e) { // 降级处理 } if (!hmac || hmac.length !== 8) { return codePart; } return `${codePart}-${hmac}`; }; const readStoredInviteCode = () => { try { const stored = localStorage.getItem(inviteCodeKey); if (stored) return stored; } catch (e) {} try { if (typeof _GM_getValue === 'function') { return _GM_getValue(inviteCodeKey, '') || ''; } } catch (e) {} return ''; }; const writeStoredInviteCode = (code) => { try { localStorage.setItem(inviteCodeKey, code); } catch (e) {} try { if (typeof _GM_setValue === 'function') { _GM_setValue(inviteCodeKey, code); } } catch (e) {} }; // 获取/生成本机邀请码(带服务器唯一性校验) const getOrCreateInviteCode = async () => { try { // 1. 先尝试从本地读取 let code = readStoredInviteCode(); if (code) { const normalized = String(code).trim().toUpperCase(); if (/^[A-Z0-9]{6}-[A-F0-9]{8}$/.test(normalized) || /^[A-Z0-9]{6}$/.test(normalized)) { return normalized; } } // 2. 尝试从服务器获取(如果设备之前注册过) try { let deviceFingerprint = 'unknown'; try { deviceFingerprint = await ClientLicense.computeDeviceFingerprint(); } catch (fpErr) { console.warn('[学习通助手] 设备指纹计算失败,使用降级方案:', fpErr); deviceFingerprint = 'device_' + Date.now().toString(36); } const serverResult = await ClientLicense.syncGetDeviceInviteCode(deviceFingerprint.slice(0, 16)); if (serverResult.success && serverResult.invite_code) { writeStoredInviteCode(serverResult.invite_code); return serverResult.invite_code; } } catch (e) { console.warn('[学习通助手] 从服务器获取邀请码失败,使用本地生成:', e); } // 3. 本地生成并注册到服务器(最多重试3次) const maxRetries = 3; for (let attempt = 0; attempt < maxRetries; attempt++) { code = generateInviteCode(); if (!code) continue; try { let deviceFingerprint = 'unknown'; try { deviceFingerprint = await ClientLicense.computeDeviceFingerprint(); } catch (fpErr) { deviceFingerprint = 'device_' + Date.now().toString(36); } const registerResult = await ClientLicense.syncRegisterInviteCode(code, deviceFingerprint.slice(0, 16)); if (registerResult.success) { writeStoredInviteCode(code); console.log(`[学习通助手] 邀请码注册成功: ${code}`); return code; } else { console.warn(`[学习通助手] 邀请码注册失败 (尝试 ${attempt + 1}/${maxRetries}): ${registerResult.message}`); if (registerResult.message.includes('已被占用')) { continue; } writeStoredInviteCode(code); return code; } } catch (e) { console.warn(`[学习通助手] 邀请码注册异常 (尝试 ${attempt + 1}/${maxRetries}):`, e); } } // 所有重试都失败,返回本地生成的码 const fallbackCode = await generateInviteCode(); if (fallbackCode) { writeStoredInviteCode(fallbackCode); return fallbackCode; } return '生成失败'; } catch (e) { console.error('[学习通助手] 获取邀请码失败:', e); return await generateInviteCode() || '生成失败'; } }; // 更新剩余次数显示 const updateRemaining = async () => { try { const st = await ClientLicense.checkToken(); if (st && st.ok) { const trialTag = st.isFreeTrial ? ' (免费试用)' : (st.label || ''); remainingUses.value = `剩余次数: ${st.uses_remaining ?? 'N/A'}${trialTag}`; } else if (st && st.needsRedeem) { remainingUses.value = '本地数据已清理,重新兑换即可恢复(云端有记录)'; } else { remainingUses.value = st && st.tampered ? '授权异常(篡改)' : '未授权'; } } catch (e) { remainingUses.value = '查询失败'; } }; // 卡密兑换函数 const redeemCardKey = async () => { const text = redeemInput.value.trim(); if (!text) { setRedeemStatus('error', '请输入卡密'); return; } const lines = text.split(/\r?\n/).map(s => s.trim()).filter(Boolean); if(lines.length === 0){ setRedeemStatus('error', '未检测到有效卡密'); return; } redeemSubmitting.value = true; setRedeemStatus('submitting', `正在兑换 ${lines.length} 个卡密...`); let results = []; let successCount = 0; let failCount = 0; for (const line of lines) { try { const normalizedLine = line.trim(); if(!normalizedLine){ results.push(`⚠️ 空行,已跳过`); continue; } const isSelfContained = normalizedLine.includes('::'); const isSchemeC = /^([A-Z0-9]{4}-){3}[A-Z0-9]{4}-[A-F0-9]+-[A-F0-9]+$/i.test(normalizedLine); if(!isSelfContained && !isSchemeC){ results.push(`❌ ${normalizedLine.substring(0, 20)}... 格式不支持`); failCount++; continue; } const r = await ClientLicense.redeemSelfContained(normalizedLine); if (r.ok) { const displayCode = isSelfContained ? normalizedLine.split('::')[0] : normalizedLine.substring(0, 24) + '...'; results.push(`✅ ${displayCode} 兑换成功,剩余 ${r.uses_remaining} 次`); successCount++; } else { const displayCode = isSelfContained ? normalizedLine.split('::')[0] : normalizedLine.substring(0, 24) + '...'; let msg = r.message || '未知错误'; const errorMessages = { 'device_mismatch': '该卡密已绑定其他设备', 'already_redeemed': '该卡密已兑换,请勿重复使用', 'invalid_format': '卡密格式错误', 'invalid_payload': '卡密数据损坏', 'decrypt_failed': '卡密解密失败,可能已过期', 'invalid_decrypted': '卡密内容解析失败', 'save_failed': '授权信息保存失败', 'empty': '卡密不能为空', 'redeem error': '兑换过程发生异常' }; if(errorMessages[msg]){ msg = errorMessages[msg]; } results.push(`❌ ${displayCode} ${msg}`); failCount++; } } catch (e) { const displayCode = line.includes('::') ? line.split('::')[0] : line.substring(0, 24) + '...'; results.push(`⚠️ ${displayCode} 兑换异常: ${e.message || '未知错误'}`); failCount++; console.error('[学习通助手] 卡密兑换异常:', e); } } redeemSubmitting.value = false; const summary = `兑换完成: 成功 ${successCount} 个,失败 ${failCount} 个`; const fullMessage = summary + '\n\n' + results.join('\n'); redeemStatus.value = { status: successCount > 0 ? 'success' : 'error', message: fullMessage }; if(successCount > 0){ lastFailedInput.value = ''; redeemInput.value = ''; await updateRemaining(); EventBus.emit('license:updated', { uses_remaining: null, isFreeTrial: false }); } else { lastFailedInput.value = redeemInput.value; } console.log(`[学习通助手] ${summary}`); }; vue.onMounted(async () => { // 后台异步注册到服务器(不阻塞UI,因为邀请码已在setup阶段同步初始化) (async () => { try { let storedCode = myInviteCode.value; if (storedCode && /^[A-Z0-9]{6}$/.test(String(storedCode).trim().toUpperCase())) { const hmac = await ClientLicense.computeInviteHmac(String(storedCode).trim().toUpperCase()); storedCode = `${String(storedCode).trim().toUpperCase()}-${hmac}`; myInviteCode.value = storedCode; writeStoredInviteCode(storedCode); } if (storedCode && /^[A-Z0-9]{6}-[A-F0-9]{8}$/.test(String(storedCode).trim().toUpperCase()) && storedCode !== '暂无') { let deviceFingerprint = 'unknown'; try { deviceFingerprint = await ClientLicense.computeDeviceFingerprint(); } catch (fpErr) { deviceFingerprint = 'device_' + Date.now().toString(36); } const registerResult = await ClientLicense.syncRegisterInviteCode(storedCode, deviceFingerprint.slice(0, 16)); if (registerResult.success) { console.log('[学习通助手] 邀请码后台注册成功:', storedCode); } else { console.warn('[学习通助手] 邀请码后台注册失败:', registerResult.message); } } } catch (e) { console.warn('[学习通助手] 邀请码后台注册异常:', e); } })(); updateRemaining(); // 关键修复:监听 license:updated 事件,实时更新剩余次数显示 try { EventBus.on('license:updated', async (data) => { if (data.uses_remaining !== null && data.uses_remaining !== undefined) { remainingUses.value = `剩余次数: ${data.uses_remaining}`; } else { await updateRemaining(); } }); } catch (e) { console.warn('[学习通助手] 注册 license:updated 事件监听失败:', e); } // 启动时检查防刷记录(从GM_setValue恢复,防止用户清除localStorage) try { if (typeof _GM_getValue === 'function') { const gmUsedCodes = _GM_getValue('_used_invite_codes_v1'); if (gmUsedCodes) { try { const parsed = JSON.parse(gmUsedCodes); localStorage.setItem('_used_invite_codes_v1', gmUsedCodes); console.log('[学习通助手] 已从GM存储恢复邀请码使用记录'); } catch(e) {} } // 异步获取设备指纹,不阻塞 (async () => { try { const deviceId = await ClientLicense.computeDeviceFingerprint(); const deviceFingerprint = deviceId.slice(0, 16); const usedKey = '_referral_used_' + deviceFingerprint; const deviceUsed = _GM_getValue(usedKey); if (deviceUsed) { localStorage.setItem(usedKey, '1'); console.log('[学习通助手] 已恢复设备使用标记'); } } catch(e) { console.warn('[学习通助手] 恢复防刷记录失败:', e); } })(); } } catch(e) { console.warn('[学习通助手] 恢复防刷记录失败:', e); } const referralClickHandler = (e) => { const target = e.target && e.target.closest ? e.target.closest('[data-redeem-invite-code], [data-copy-invite-code]') : null; if (!target) return; const now = Date.now(); if (now - lastReferralActionAt < 300) return; lastReferralActionAt = now; if (target.matches('[data-redeem-invite-code]')) { e.preventDefault(); e.stopPropagation(); console.log('[学习通助手] 兑换按钮兜底监听触发'); if (!submitting.value) redeemInviteCode(); } else if (target.matches('[data-copy-invite-code]')) { console.log('[学习通助手] 复制按钮兜底监听触发'); } }; document.addEventListener('click', referralClickHandler, true); }); // 兑换邀请码(HMAC校验 + 每邀请码仅限一次,不同邀请码可叠加) const redeemInviteCode = async () => { console.log('[学习通助手] redeemInviteCode 被调用'); // 防止重复提交 if(submitting.value){ console.log('[学习通助手] 正在处理中,忽略重复点击'); return; } const code = inputCode.value.trim(); console.log('[学习通助手] 输入的邀请码:', code, '长度:', code.length); if(!code){ console.log('[学习通助手] 邀请码为空'); setSubmitState('error', '请输入邀请码'); logStore.addLog('请输入邀请码', 'warning'); return; } const normalizedCode = code.toUpperCase(); console.log('[学习通助手] 标准化后的邀请码:', normalizedCode); // 修复:支持两种格式 - 完整格式(6位-8位校验码) 和 简化格式(仅6位) const isFullFormat = /^[A-Z0-9]{6}-[A-F0-9]{8}$/.test(normalizedCode); const isSimpleFormat = /^[A-Z0-9]{6}$/.test(normalizedCode); if(!isFullFormat && !isSimpleFormat){ console.log('[学习通助手] 邀请码格式错误,当前值:', normalizedCode); setSubmitState('error', '邀请码格式错误(应为6位字母数字,或6位-8位校验码格式)'); logStore.addLog('邀请码格式错误(应为6位字母数字,或6位-8位校验码格式)', 'warning'); return; } // 如果是简化格式,需要补充HMAC校验码 let finalCode = normalizedCode; if(isSimpleFormat){ console.log('[学习通助手] 检测到简化格式,自动补充HMAC校验码'); const codePart = normalizedCode; // 修复:使用 ClientLicense.computeInviteHmac 而不是 simpleHash,确保与 verifyInviteCode 一致 const hmac = await ClientLicense.computeInviteHmac(codePart); finalCode = `${codePart}-${hmac}`; console.log('[学习通助手] 补充后的完整邀请码:', finalCode); } if(finalCode === (myInviteCode.value || '').toUpperCase()){ console.log('[学习通助手] 不能使用自己的邀请码'); setSubmitState('error', '不能兑换自己的邀请码,请复制给好友使用'); logStore.addLog('不能兑换自己的邀请码,请复制给好友使用', 'warning'); return; } console.log('[学习通助手] 开始验证邀请码...'); setSubmitState('submitting', '正在验证...'); submitting.value = true; try { console.log('[学习通助手] 调用verifyInviteCode, 参数:', finalCode); const result = await ClientLicense.verifyInviteCode(finalCode); console.log('[学习通助手] verifyInviteCode返回结果:', result); if(!result){ console.log('[学习通助手] 邀请码校验失败,result为null'); submitting.value = false; setSubmitState('error', '邀请码校验失败,请检查是否正确'); return; } console.log('[学习通助手] 邀请码校验成功, bonus:', result.bonus); const deviceId = await ClientLicense.computeDeviceFingerprint(); const deviceFingerprint = deviceId.slice(0, 16); let usedCodes = {}; let loadedFromGM = false; try { if (typeof _GM_getValue === 'function') { const stored = _GM_getValue('_used_invite_codes_v1'); if(stored){ try { usedCodes = JSON.parse(stored); loadedFromGM = true; } catch(e) { console.warn('[学习通助手] 解析GM存储失败:', e); } } } if(!loadedFromGM || !Object.keys(usedCodes).length) { const backup = localStorage.getItem('_used_invite_codes_v1_backup'); if(backup){ try { usedCodes = JSON.parse(backup); } catch(e) { console.warn('[学习通助手] 解析localStorage备份失败:', e); usedCodes = {}; } } } } catch(e) { console.warn('[学习通助手] 读取已使用邀请码记录失败:', e); usedCodes = {}; } const codeKey = normalizedCode; if(usedCodes[codeKey]){ submitting.value = false; setSubmitState('error', '该邀请码已被使用,请勿重复兑换'); logStore.addLog('该邀请码已被使用,请勿重复兑换', 'warning'); return; } const res = await ClientLicense.addReferralBonus(result.bonus); submitting.value = false; if(res && res.ok){ usedCodes[codeKey] = { time: Date.now(), device: deviceFingerprint, bonus: result.bonus, codePart: result.codePart }; let gmSaveSuccess = false; try { if (typeof _GM_setValue === 'function') { _GM_setValue('_used_invite_codes_v1', JSON.stringify(usedCodes)); gmSaveSuccess = true; } } catch(e) { console.warn('[学习通助手] GM存储保存失败:', e); } try { localStorage.setItem('_used_invite_codes_v1_backup', JSON.stringify(usedCodes)); } catch(e) { console.warn('[学习通助手] localStorage备份保存失败:', e); } if(gmSaveSuccess){ console.log('[学习通助手] 邀请码兑换记录已保存到GM存储'); } setSubmitState('success', `兑换成功!获得 ${result.bonus} 次答题机会`); rewardResult.value = { added: result.bonus }; inputCode.value = ''; await updateRemaining(); console.log(`[学习通助手] 邀请码兑换成功: ${codeKey} -> +${result.bonus}次`); } else { const errorMsg = res && res.message ? res.message : '兑换失败,请稍后重试'; setSubmitState('error', errorMsg); console.warn('[学习通助手] 邀请码兑换失败:', errorMsg); } } catch(e) { submitting.value = false; setSubmitState('error', '兑换过程发生异常,请稍后重试'); console.error('[学习通助手] 邀请码兑换异常:', e); } }; return (_ctx, _cache) => { const isSubmitting = submitting.value; const isRedeemSubmitting = redeemSubmitting.value; const tabValue = activeTab.value; const currentSubmitStatus = submitStatus.value; const currentSubmitMessage = submitMessage.value; const inviteCodeText = inviteCodeDisplayText.value; const inviteCodeValid = isInviteCodeValid.value; const currentRedeemInput = redeemInput.value; const currentRemainingUses = remainingUses.value; const currentRedeemStatus = redeemStatus.value; const currentInviteCode = myInviteCode.value; // 推广奖励Tab内容 const referralContent = vue.createElementVNode("div", { style: { "padding": "var(--jb-spacing-lg)", "background": "var(--jb-bg-secondary)" } }, [ vue.createElementVNode("p", { style: { "margin": "0 0 8px 0", "font-size": "var(--jb-font-md)", "color": "var(--jb-text-primary)", "line-height": "1.6" } }, "邀请好友,双方各得20次。"), vue.createElementVNode("p", { style: { "margin": "0 0 12px 0", "font-size": "var(--jb-font-md)", "color": "var(--jb-text-secondary)", "line-height": "1.6" } }, "分享您的邀请码,好友兑换后您也获得20次。"), vue.createElementVNode("div", { style: { "padding": "var(--jb-spacing-md)", "margin-bottom": "var(--jb-spacing-md)", "background": "linear-gradient(135deg, rgba(24,144,255,0.08) 0%, rgba(24,144,255,0.02) 100%)", "border-radius": "var(--jb-card-radius)", "border-left": "4px solid var(--jb-primary)", "box-shadow": "var(--jb-soft-shadow)" } }, [ vue.createElementVNode("p", { style: { "margin": "0 0 8px 0", "font-size": "var(--jb-font-md)", "color": "var(--jb-text-primary)", "font-weight": "600" } }, "我的邀请码"), vue.createElementVNode("div", { style: { "display": "flex", "align-items": "center", "gap": "var(--jb-spacing-md)" } }, [ vue.createElementVNode("span", { style: { "font-size": "var(--jb-font-xl)", "color": "var(--jb-primary)", "font-weight": "700", "font-family": "monospace", "letter-spacing": "2px", "background": "#fff", "padding": "6px 12px", "border-radius": "6px", "box-shadow": "0 1px 3px rgba(0,0,0,0.08)" } }, vue.toDisplayString(inviteCodeText)), (vue.openBlock(), vue.createElementBlock("button", { key: 0, type: "button", "data-copy-invite-code": "true", onClick: async (e) => { e.preventDefault(); e.stopPropagation(); const now = Date.now(); if (now - lastReferralActionAt < 300) return; lastReferralActionAt = now; console.log('[学习通助手] 复制按钮被点击'); console.log('[学习通助手] 当前邀请码:', currentInviteCode); try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(currentInviteCode); logStore.addLog('✅ 邀请码已复制到剪贴板', 'success'); } else { const textarea = document.createElement('textarea'); textarea.value = currentInviteCode; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); logStore.addLog('✅ 邀请码已复制到剪贴板', 'success'); } } catch(e) { console.error('[学习通助手] 复制邀请码失败:', e); logStore.addLog('❌ 复制失败,请手动选择复制', 'danger'); } }, style: { "padding": "6px 12px", "font-size": "var(--jb-font-sm)", "border-radius": "var(--jb-btn-radius)", "cursor": "pointer", "transition": "all var(--jb-transition-fast)", "font-weight": "500", "pointer-events": "auto", "z-index": "100", "position": "relative" }, class: "btn-outline-primary" }, "复制")) ]) ]), vue.createElementVNode("hr", { style: { "margin": "var(--jb-spacing-lg) 0", "border": "none", "border-top": "1px solid var(--jb-border-light)" } }), vue.createElementVNode("p", { style: { "margin": "0 0 8px 0", "font-size": "var(--jb-font-md)", "color": "var(--jb-text-primary)", "font-weight": "600" } }, "兑换好友邀请码"), vue.createElementVNode("div", { style: { "margin-bottom": "var(--jb-spacing-md)" } }, [ vue.createElementVNode("input", { type: "text", value: vue.unref(inputCode), onInput: _cache[0] || (_cache[0] = ($event) => inputCode.value = $event.target.value.toUpperCase()), placeholder: "请输入邀请码(例:ABC123 或 ABC123-A1B2C3D4)", style: { "width": "100%", "padding": "10px 12px", "border": "1px solid var(--jb-border)", "border-radius": "var(--jb-btn-radius)", "font-size": "var(--jb-font-lg)", "font-family": "monospace", "letter-spacing": "2px", "box-sizing": "border-box", "text-transform": "uppercase", "background": "#fff", "color": "var(--jb-text-primary)", "transition": "all var(--jb-transition-fast)" } }, null, 40, ["value", "onInput"]) ]), submitStatus.value ? vue.createElementVNode("div", { "data-invite-submit-message": "top", style: { "margin": "0 0 12px 0", "padding": "12px 14px", "border-radius": "8px", "border": "1px solid " + (submitStatus.value === 'success' ? "#52c41a" : "#ff4d4f"), "background-color": submitStatus.value === 'success' ? "rgba(82,196,26,0.1)" : submitStatus.value === 'error' ? "rgba(255,77,79,0.14)" : "rgba(24,144,255,0.1)", "color": submitStatus.value === 'success' ? "#237804" : submitStatus.value === 'error' ? "#cf1322" : "#0958d9", "font-size": "14px", "font-weight": "600", "line-height": "1.5", "box-shadow": "0 4px 12px rgba(0,0,0,0.1)" } }, [ vue.createElementVNode("span", { style: { "margin-right": "6px" } }, submitStatus.value === 'success' ? "✅" : submitStatus.value === 'submitting' ? "⏳" : "❌"), vue.createTextVNode(vue.toDisplayString(submitMessage.value), 1) ]) : vue.createCommentVNode("", true), vue.createElementVNode("button", { type: "button", "data-redeem-invite-code": "true", onClick: (e) => { e.preventDefault(); e.stopPropagation(); const now = Date.now(); if (now - lastReferralActionAt < 300) return; lastReferralActionAt = now; console.log('[学习通助手] 兑换按钮onClick触发'); console.log('[学习通助手] 当前submitting状态:', submitting.value); if (!submitting.value) { redeemInviteCode(); } }, disabled: submitting.value, style: { "width": "100%", "padding": "12px", "border": "none", "border-radius": "var(--jb-btn-radius)", "color": "#fff", "font-size": "var(--jb-font-lg)", "font-weight": "600", "cursor": submitting.value ? "not-allowed" : "pointer", "transition": "all var(--jb-transition-normal)", "opacity": submitting.value ? "0.7" : "1", "pointer-events": "auto", "user-select": "none", "z-index": "100", "position": "relative" }, class: "btn-primary-gradient" }, vue.toDisplayString(submitting.value ? "兑换中..." : "兑换 +20次"), 9), vue.createElementVNode("hr", { style: { "margin": "var(--jb-spacing-xl) 0", "border": "none", "border-top": "1px solid var(--jb-border-light)" } }), vue.createElementVNode("div", { style: { "padding": "var(--jb-spacing-md)", "background": "linear-gradient(135deg, rgba(250,173,20,0.08) 0%, rgba(250,173,20,0.02) 100%)", "border-radius": "var(--jb-card-radius)", "border-left": "4px solid var(--jb-warning)", "text-align": "center", "box-shadow": "var(--jb-soft-shadow)" } }, [ vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "var(--jb-font-lg)", "color": "#e65100", "font-weight": "600" } }, "💬 官方QQ群:796069615"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "var(--font-sm)", "color": "var(--jb-text-tertiary)" } }, "获取最新教程、反馈问题、交流经验") ]) ]); // 卡密兑换Tab内容 const redeemContent = vue.createElementVNode("div", { style: { "padding": "var(--jb-spacing-lg)", "background": "var(--jb-bg-secondary)" } }, [ // 顶部醒目购买提示 vue.createElementVNode("div", { style: { "padding": "var(--jb-spacing-md)", "margin-bottom": "var(--jb-spacing-md)", "background": "linear-gradient(135deg, rgba(255,77,79,0.12) 0%, rgba(255,77,79,0.04) 100%)", "border-radius": "var(--jb-card-radius)", "border": "2px solid var(--jb-danger)", "text-align": "center", "box-shadow": "0 4px 12px rgba(255,77,79,0.15)", "animation": "pulse 2s ease-in-out infinite" } }, [ vue.createElementVNode("p", { style: { "margin": "0 0 8px 0", "font-size": "var(--jb-font-xl)", "color": "var(--jb-danger)", "font-weight": "700" } }, "🔥 需要卡密?点击购买"), vue.createElementVNode("a", { href: "https://pay.ldxp.cn/shop/3K2UNT5M", target: "_blank", style: { "display": "inline-block", "padding": "10px 28px", "background": "linear-gradient(135deg, var(--jb-danger) 0%, #d9363e 100%)", "color": "#fff", "border-radius": "var(--jb-btn-radius)", "text-decoration": "none", "font-size": "var(--jb-font-lg)", "font-weight": "700", "transition": "all var(--jb-transition-normal)", "box-shadow": "0 4px 12px rgba(255,77,79,0.35)" } }, "立即购买卡密"), vue.createElementVNode("p", { style: { "margin": "8px 0 0 0", "font-size": "var(--jb-font-sm)", "color": "var(--jb-text-secondary)" } }, "500次/1500次/2888次 多种规格可选") ]), vue.createElementVNode("p", { style: { "margin": "0 0 8px 0", "font-size": "var(--jb-font-md)", "color": "var(--jb-text-primary)", "line-height": "1.6" } }, "粘贴卡密进行兑换,每行一张。"), vue.createElementVNode("p", { style: { "margin": "0 0 10px 0", "font-size": "var(--jb-font-sm)", "color": "var(--jb-text-tertiary)" } }), vue.createElementVNode("div", { style: { "padding": "var(--jb-spacing-md)", "margin-bottom": "var(--jb-spacing-md)", "background": "linear-gradient(135deg, rgba(82,196,26,0.08) 0%, rgba(82,196,26,0.02) 100%)", "border-radius": "var(--jb-card-radius)", "border-left": "4px solid var(--jb-success)", "box-shadow": "var(--jb-soft-shadow)" } }, [ vue.createElementVNode("p", { style: { "margin": "0", "font-size": "var(--jb-font-md)", "color": "var(--jb-text-primary)", "font-weight": "600" } }, vue.toDisplayString(currentRemainingUses || '查询中...')) ]), vue.createElementVNode("div", { style: { "margin-bottom": "var(--jb-spacing-md)" } }, [ vue.createElementVNode("textarea", { value: vue.unref(redeemInput), onInput: _cache[1] || (_cache[1] = ($event) => redeemInput.value = $event.target.value), placeholder: "粘贴卡密,每行一张", style: { "width": "100%", "height": "100px", "padding": "10px 12px", "border": "1px solid var(--jb-border)", "border-radius": "var(--jb-btn-radius)", "font-size": "var(--jb-font-md)", "font-family": "monospace", "box-sizing": "border-box", "resize": "vertical", "background": "#fff", "color": "var(--jb-text-primary)", "transition": "all var(--jb-transition-fast)" } }, null, 40, ["value", "onInput"]) ]), vue.createElementVNode("button", { type: "button", onClick: (e) => { e.preventDefault(); e.stopPropagation(); console.log('[学习通助手] 卡密兑换按钮被点击'); console.log('[学习通助手] 当前redeemSubmitting状态:', redeemSubmitting.value); if (!redeemSubmitting.value) { redeemCardKey(); } }, disabled: redeemSubmitting.value, style: { "width": "100%", "padding": "12px", "border": "none", "border-radius": "var(--jb-btn-radius)", "color": "#fff", "font-size": "var(--jb-font-lg)", "font-weight": "600", "cursor": redeemSubmitting.value ? "not-allowed" : "pointer", "transition": "all var(--jb-transition-normal)", "opacity": redeemSubmitting.value ? "0.7" : "1", "pointer-events": "auto", "user-select": "none", "z-index": "100", "position": "relative" }, class: "btn-success-gradient" }, vue.toDisplayString(redeemSubmitting.value ? "兑换中..." : "兑换卡密"), 9), currentRedeemStatus.status ? (vue.openBlock(), vue.createElementBlock("div", { key: 0, style: { "margin-top": "var(--jb-spacing-md)", "padding": "var(--jb-spacing-md)", "border-radius": "var(--jb-btn-radius)", "border": "1px solid", "border-color": currentRedeemStatus.status === 'success' ? "var(--jb-success)" : "var(--jb-danger)", "background-color": currentRedeemStatus.status === 'success' ? "rgba(82,196,26,0.08)" : "rgba(255,77,79,0.08)" } }, [ vue.createElementVNode("div", { style: { "display": "flex", "align-items": "flex-start", "gap": "var(--jb-spacing-sm)" } }, [ vue.createElementVNode("span", { style: { "font-size": "var(--jb-font-lg)" } }, vue.toDisplayString( currentRedeemStatus.status === 'success' ? "✅" : "❌" ), 1), vue.createElementVNode("span", { style: { "font-size": "var(--jb-font-md)", "color": currentRedeemStatus.status === 'success' ? "var(--jb-success)" : "var(--jb-danger)", "white-space": "pre-wrap", "flex": "1" } }, vue.toDisplayString(currentRedeemStatus.message), 1) ]), currentRedeemStatus.status === 'error' ? (vue.openBlock(), vue.createElementBlock("button", { key: 0, onClick: () => { if(redeemStatusTimer) { clearTimeout(redeemStatusTimer); redeemStatusTimer = null; } redeemStatus.value = { status: '', message: '' }; if (lastFailedInput.value) { redeemInput.value = lastFailedInput.value; } }, style: { "margin-top": "var(--jb-spacing-sm)", "padding": "8px 16px", "background": "linear-gradient(135deg, var(--jb-danger) 0%, #d9363e 100%)", "color": "#fff", "border": "none", "border-radius": "var(--jb-btn-radius)", "font-size": "var(--jb-font-md)", "font-weight": "600", "cursor": "pointer", "transition": "all var(--jb-transition-fast)", "box-shadow": "0 2px 8px rgba(255,77,79,0.3)" } }, "🔄 重新尝试兑换")) : vue.createCommentVNode("v-if", true) ])) : vue.createCommentVNode("v-if", true), vue.createElementVNode("hr", { style: { "margin": "var(--jb-spacing-xl) 0", "border": "none", "border-top": "1px solid var(--jb-border-light)" } }), vue.createElementVNode("div", { style: { "padding": "var(--jb-spacing-md)", "background": "linear-gradient(135deg, rgba(24,144,255,0.08) 0%, rgba(24,144,255,0.02) 100%)", "border-radius": "var(--jb-card-radius)", "border-left": "4px solid var(--jb-primary)", "text-align": "center", "margin-bottom": "var(--jb-spacing-md)", "box-shadow": "var(--jb-soft-shadow)" } }, [ vue.createElementVNode("p", { style: { "margin": "0 0 8px 0", "font-size": "var(--jb-font-lg)", "color": "var(--jb-primary)", "font-weight": "600" } }, "🛒 购买卡密"), vue.createElementVNode("a", { href: "https://pay.ldxp.cn/shop/3K2UNT5M", target: "_blank", style: { "display": "inline-block", "padding": "10px 24px", "background": "linear-gradient(135deg, var(--jb-primary) 0%, var(--jb-primary-2) 100%)", "color": "#fff", "border-radius": "var(--jb-btn-radius)", "text-decoration": "none", "font-size": "var(--jb-font-md)", "font-weight": "600", "transition": "all var(--jb-transition-normal)", "box-shadow": "var(--jb-btn-shadow)" } }, "点击购买卡密"), vue.createElementVNode("p", { style: { "margin": "8px 0 0 0", "font-size": "var(--jb-font-sm)", "color": "var(--jb-text-tertiary)" } }, "500次/1500次/2888次 多种规格可选") ]), vue.createElementVNode("div", { style: { "padding": "var(--jb-spacing-md)", "background": "linear-gradient(135deg, #f093fb 0%, #f5576c 100%)", "border-radius": "var(--jb-card-radius)", "text-align": "center", "box-shadow": "0 6px 20px rgba(245,87,108,0.35)", "border": "2px solid rgba(255,255,255,0.3)", "position": "relative", "overflow": "hidden" } }, [ vue.createElementVNode("div", { style: { "position": "absolute", "top": "0", "left": "0", "right": "0", "bottom": "0", "background": "radial-gradient(circle at 30% 50%, rgba(255,255,255,0.15) 0%, transparent 60%)", "pointer-events": "none" } }), vue.createElementVNode("p", { style: { "margin": "0 0 8px 0", "font-size": "var(--jb-font-xl)", "color": "#fff", "font-weight": "700", "text-shadow": "0 2px 4px rgba(0,0,0,0.2)", "position": "relative", "z-index": "1" } }, "💬 官方QQ群:796069615"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "var(--jb-font-sm)", "color": "rgba(255,255,255,0.95)", "position": "relative", "z-index": "1" } }, "获取最新教程、反馈问题、交流经验") ]) ]); return vue.openBlock(), vue.createElementBlock("div", { style: { "width": "100%", "flex": "1", "display": "flex", "flex-direction": "column", "position": "relative", "overflow": "hidden", "min-height": "0" } }, [ vue.createElementVNode("div", { style: { "background": "linear-gradient(135deg, var(--jb-primary) 0%, var(--jb-primary-2) 100%)", "padding": "var(--jb-spacing-md) var(--jb-spacing-lg)", "color": "#fff", "border-radius": "var(--jb-card-radius) var(--jb-card-radius) 0 0", "position": "relative", "z-index": "20", "box-shadow": "0 2px 8px rgba(24,144,255,0.15)" } }, [ vue.createElementVNode("span", { style: { "font-weight": "600", "font-size": "var(--jb-font-lg)", "color": "#ff4d4f" } }, "授权与兑换") ]), vue.createElementVNode("div", { style: { "display": "flex", "border-bottom": "2px solid var(--jb-border-light)", "flex-shrink": "0", "position": "relative", "z-index": "10", "background": "#fff" } }, [ vue.createElementVNode("button", { id: "tab-btn-referral", type: "button", onClick: (e) => { e.preventDefault(); e.stopPropagation(); console.log('[学习通助手] 点击推广奖励Tab'); switchInnerTab('referral'); }, style: { "flex": "1", "padding": "var(--jb-spacing-md)", "font-size": "var(--jb-font-md)", "cursor": "pointer", "transition": "all var(--jb-transition-normal)", "text-align": "center", "pointer-events": "auto", "user-select": "none", "color": "#ff4d4f", "font-weight": "600" }, class: tabValue === 'referral' ? "btn-tab active" : "btn-tab" }, "推广奖励"), vue.createElementVNode("button", { id: "tab-btn-redeem", type: "button", onClick: (e) => { e.preventDefault(); e.stopPropagation(); console.log('[学习通助手] 点击卡密兑换Tab'); switchInnerTab('redeem'); }, style: { "flex": "1", "padding": "var(--jb-spacing-md)", "font-size": "var(--jb-font-md)", "cursor": "pointer", "transition": "all var(--jb-transition-normal)", "text-align": "center", "pointer-events": "auto", "user-select": "none", "color": "#ff4d4f", "font-weight": "600" }, class: tabValue === 'redeem' ? "btn-tab active" : "btn-tab" }, "卡密兑换") ]), vue.createElementVNode("div", { key: activeTab.value, style: { "border": "1px solid var(--jb-border-light)", "border-top": "none", "border-radius": "0 0 var(--jb-card-radius) var(--jb-card-radius)", "overflow": "visible", "width": "100%", "flex": "1", "min-height": "0", "box-sizing": "border-box", "position": "relative", "z-index": "0", "display": "flex", "flex-direction": "column", "background": "#fff" } }, [ // 使用Vue的条件渲染 (activeTab.value === 'referral') ? (vue.openBlock(), vue.createElementBlock("div", { key: "referral", "data-tab-content": "referral", style: { "display": "flex", "flex-direction": "column", "flex": "1", "min-height": "0" } }, [referralContent])) : (vue.openBlock(), vue.createElementBlock("div", { key: "redeem", "data-tab-content": "redeem", style: { "display": "flex", "flex-direction": "column", "flex": "1", "min-height": "0" } }, [redeemContent])) ]) ]); }; } }); const ReferralPanel = _export_sfc(_sfc_main_referral, [["__scopeId", "data-v-referral-panel"]]); const TutorialPanel = vue.defineComponent({ __name: "TutorialPanel", setup() { return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", { style: { "padding": "15px", "line-height": "1.8", "color": "#333" } }, [ vue.createElementVNode("h3", { style: { "margin": "0 0 15px 0", "color": "#1890ff", "border-bottom": "2px solid #1890ff", "padding-bottom": "8px" } }, "📖 使用教程"), vue.createElementVNode("div", { style: { "margin-bottom": "15px" } }, [ vue.createElementVNode("h4", { style: { "margin": "0 0 8px 0", "color": "#333", "font-size": "14px" } }, "一、安装脚本"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "1. 安装 Tampermonkey 或 Violentmonkey 浏览器扩展"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "2. 点击脚本安装链接,自动添加到扩展中"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "13px", "color": "#666" } }, "3. 刷新学习通页面即可生效") ]), vue.createElementVNode("div", { style: { "margin-bottom": "15px" } }, [ vue.createElementVNode("h4", { style: { "margin": "0 0 8px 0", "color": "#333", "font-size": "14px" } }, "二、基本使用"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "1. 打开学习通课程页面"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "2. 点击右侧悬浮按钮打开控制面板"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "3. 选择需要刷的课程章节"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "13px", "color": "#666" } }, "4. 点击开始按钮自动播放") ]), vue.createElementVNode("div", { style: { "margin-bottom": "15px" } }, [ vue.createElementVNode("h4", { style: { "margin": "0 0 8px 0", "color": "#333", "font-size": "14px" } }, "三、AI答题功能"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "1. 遇到测验题时自动调用AI答题"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "2. 支持单选题、多选题、判断题"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "3. 每次答题消耗1次答题次数"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "13px", "color": "#666" } }, "4. 新用户免费20次,可购买卡密获得更多次数") ]), vue.createElementVNode("div", { style: { "margin-bottom": "15px" } }, [ vue.createElementVNode("h4", { style: { "margin": "0 0 8px 0", "color": "#333", "font-size": "14px" } }, "四、卡密兑换"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "1. 在授权与兑换面板中粘贴卡密"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "2. 点击兑换卡密按钮"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "3. 兑换成功后立即生效"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "13px", "color": "#666" } }, "4. 卡密规格:500次/1500次/2888次") ]), vue.createElementVNode("div", { style: { "margin-bottom": "15px" } }, [ vue.createElementVNode("h4", { style: { "margin": "0 0 8px 0", "color": "#333", "font-size": "14px" } }, "五、邀请码功能"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "1. 在授权与兑换面板获取您的邀请码"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "2. 分享给好友,好友兑换后双方各得20次"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "3. 每个设备只能兑换一次邀请码"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "13px", "color": "#666" } }, "4. 邀请码格式:ABCDEF-1234(6位字符+4位校验码)") ]), vue.createElementVNode("div", { style: { "margin-bottom": "15px" } }, [ vue.createElementVNode("h4", { style: { "margin": "0 0 8px 0", "color": "#333", "font-size": "14px" } }, "六、常见问题"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "Q: 答题次数用完怎么办?"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "A: 可以购买卡密或使用邀请码获得额外次数"), vue.createElementVNode("p", { style: { "margin": "0 0 5px 0", "font-size": "13px", "color": "#666" } }, "Q: 脚本不生效怎么办?"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "13px", "color": "#666" } }, "A: 请检查Tampermonkey扩展是否启用,或联系QQ群反馈") ]), vue.createElementVNode("hr", { style: { "margin": "15px 0", "border": "none", "border-top": "1px solid #e0e0e0" } }), vue.createElementVNode("div", { style: { "margin-top": "15px", "padding": "16px", "background": "linear-gradient(135deg, #f093fb 0%, #f5576c 100%)", "border-radius": "12px", "text-align": "center", "box-shadow": "0 6px 20px rgba(245,87,108,0.35)", "border": "2px solid rgba(255,255,255,0.3)", "position": "relative", "overflow": "hidden" } }, [ vue.createElementVNode("div", { style: { "position": "absolute", "top": "0", "left": "0", "right": "0", "bottom": "0", "background": "radial-gradient(circle at 30% 50%, rgba(255,255,255,0.15) 0%, transparent 60%)", "pointer-events": "none" } }), vue.createElementVNode("p", { style: { "margin": "0 0 8px 0", "font-size": "16px", "color": "#fff", "font-weight": "700", "text-shadow": "0 2px 4px rgba(0,0,0,0.2)", "position": "relative", "z-index": "1" } }, "💬 官方QQ群:796069615"), vue.createElementVNode("p", { style: { "margin": "0", "font-size": "12px", "color": "rgba(255,255,255,0.95)", "position": "relative", "z-index": "1" } }, "获取最新教程、反馈问题、交流经验") ]) ]); }; } }); // 离线模式 - 无服务端更新日志 const changelogLoading = vue.ref(false); const changelogList = vue.ref([]); const changelogRefreshKey = vue.ref(0); const AuthorWords = vue.defineComponent({ __name: "AuthorWords", setup() { return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", { style: { "padding": "15px", "line-height": "1.6", "color": "#333" } }, [ vue.createElementVNode("h3", { style: { "margin": "0 0 12px 0", "color": "#1890ff" } }, "作者的话"), vue.createElementVNode("p", { style: { "margin": "0 0 8px 0" } }, "Daybreak网课助手 v2.0"), vue.createElementVNode("p", { style: { "margin": "0 0 8px 0" } }, "支持超星学习通、智慧树等平台的自动答题和刷课功能。"), vue.createElementVNode("p", { style: { "margin": "0 0 8px 0" } }, "新用户免费20次,卡密500/1500/2888次。"), vue.createElementVNode("p", { style: { "margin": "0", "color": "#999", "font-size": "12px" } }, "新增AI答题/卡密系统/免费试用。") ]); }; } }); function isFunction(value) { return typeof value === "function"; } function hasLift(source) { return isFunction(source === null || source === void 0 ? void 0 : source.lift); } function operate(init) { return function(source) { if (hasLift(source)) { return source.lift(function(liftedSource) { try { return init(liftedSource, this); } catch (err) { this.error(err); } }); } throw new TypeError("Unable to lift unknown Observable type"); }; } var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function(d2, b2) { d2.__proto__ = b2; } || function(d2, b2) { for (var p in b2) if (Object.prototype.hasOwnProperty.call(b2, p)) d2[p] = b2[p]; }; return extendStatics(d, b); }; function __extends(d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function(resolve) { resolve(value); }); } return new (P || (P = Promise))(function(resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function(v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } function __values(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function() { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); } function __read(o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; } function __spreadArray(to, from2, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from2.length, ar; i < l; i++) { if (ar || !(i in from2)) { if (!ar) ar = Array.prototype.slice.call(from2, 0, i); ar[i] = from2[i]; } } return to.concat(ar || Array.prototype.slice.call(from2)); } function __await(v) { return this instanceof __await ? (this.v = v, this) : new __await(v); } function __asyncGenerator(thisArg, _arguments, generator) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var g = generator.apply(thisArg, _arguments || []), i, q = []; return i = Object.create((typeof AsyncIterator === "function" ? AsyncIterator : Object).prototype), verb("next"), verb("throw"), verb("return", awaitReturn), i[Symbol.asyncIterator] = function() { return this; }, i; function awaitReturn(f) { return function(v) { return Promise.resolve(v).then(f, reject); }; } function verb(n, f) { if (g[n]) { i[n] = function(v) { return new Promise(function(a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; if (f) i[n] = f(i[n]); } } function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } } function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); } function fulfill(value) { resume("next", value); } function reject(value) { resume("throw", value); } function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); } } function __asyncValues(o) { if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function() { return this; }, i); function verb(n) { i[n] = o[n] && function(v) { return new Promise(function(resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v2) { resolve({ value: v2, done: d }); }, reject); } } typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var isArrayLike = function(x) { return x && typeof x.length === "number" && typeof x !== "function"; }; function isPromise(value) { return isFunction(value === null || value === void 0 ? void 0 : value.then); } function createErrorClass(createImpl) { var _super = function(instance) { Error.call(instance); instance.stack = new Error().stack; }; var ctorFunc = createImpl(_super); ctorFunc.prototype = Object.create(Error.prototype); ctorFunc.prototype.constructor = ctorFunc; return ctorFunc; } var UnsubscriptionError = createErrorClass(function(_super) { return function UnsubscriptionErrorImpl(errors) { _super(this); this.message = errors ? errors.length + " errors occurred during unsubscription:\n" + errors.map(function(err, i) { return i + 1 + ") " + err.toString(); }).join("\n ") : ""; this.name = "UnsubscriptionError"; this.errors = errors; }; }); function arrRemove(arr, item) { if (arr) { var index = arr.indexOf(item); 0 <= index && arr.splice(index, 1); } } var Subscription = function() { function Subscription2(initialTeardown) { this.initialTeardown = initialTeardown; this.closed = false; this._parentage = null; this._finalizers = null; } Subscription2.prototype.unsubscribe = function() { var e_1, _a, e_2, _b; var errors; if (!this.closed) { this.closed = true; var _parentage = this._parentage; if (_parentage) { this._parentage = null; if (Array.isArray(_parentage)) { try { for (var _parentage_1 = __values(_parentage), _parentage_1_1 = _parentage_1.next(); !_parentage_1_1.done; _parentage_1_1 = _parentage_1.next()) { var parent_1 = _parentage_1_1.value; parent_1.remove(this); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_parentage_1_1 && !_parentage_1_1.done && (_a = _parentage_1.return)) _a.call(_parentage_1); } finally { if (e_1) throw e_1.error; } } } else { _parentage.remove(this); } } var initialFinalizer = this.initialTeardown; if (isFunction(initialFinalizer)) { try { initialFinalizer(); } catch (e) { errors = e instanceof UnsubscriptionError ? e.errors : [e]; } } var _finalizers = this._finalizers; if (_finalizers) { this._finalizers = null; try { for (var _finalizers_1 = __values(_finalizers), _finalizers_1_1 = _finalizers_1.next(); !_finalizers_1_1.done; _finalizers_1_1 = _finalizers_1.next()) { var finalizer = _finalizers_1_1.value; try { execFinalizer(finalizer); } catch (err) { errors = errors !== null && errors !== void 0 ? errors : []; if (err instanceof UnsubscriptionError) { errors = __spreadArray(__spreadArray([], __read(errors)), __read(err.errors)); } else { errors.push(err); } } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_finalizers_1_1 && !_finalizers_1_1.done && (_b = _finalizers_1.return)) _b.call(_finalizers_1); } finally { if (e_2) throw e_2.error; } } } if (errors) { throw new UnsubscriptionError(errors); } } }; Subscription2.prototype.add = function(teardown) { var _a; if (teardown && teardown !== this) { if (this.closed) { execFinalizer(teardown); } else { if (teardown instanceof Subscription2) { if (teardown.closed || teardown._hasParent(this)) { return; } teardown._addParent(this); } (this._finalizers = (_a = this._finalizers) !== null && _a !== void 0 ? _a : []).push(teardown); } } }; Subscription2.prototype._hasParent = function(parent) { var _parentage = this._parentage; return _parentage === parent || Array.isArray(_parentage) && _parentage.includes(parent); }; Subscription2.prototype._addParent = function(parent) { var _parentage = this._parentage; this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent; }; Subscription2.prototype._removeParent = function(parent) { var _parentage = this._parentage; if (_parentage === parent) { this._parentage = null; } else if (Array.isArray(_parentage)) { arrRemove(_parentage, parent); } }; Subscription2.prototype.remove = function(teardown) { var _finalizers = this._finalizers; _finalizers && arrRemove(_finalizers, teardown); if (teardown instanceof Subscription2) { teardown._removeParent(this); } }; Subscription2.EMPTY = function() { var empty = new Subscription2(); empty.closed = true; return empty; }(); return Subscription2; }(); Subscription.EMPTY; function isSubscription(value) { return value instanceof Subscription || value && "closed" in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe); } function execFinalizer(finalizer) { if (isFunction(finalizer)) { finalizer(); } else { finalizer.unsubscribe(); } } var config = { onUnhandledError: null, onStoppedNotification: null, Promise: void 0, useDeprecatedSynchronousErrorHandling: false, useDeprecatedNextContext: false }; var timeoutProvider = { setTimeout: function(handler, timeout) { var args = []; for (var _i = 2; _i < arguments.length; _i++) { args[_i - 2] = arguments[_i]; } return setTimeout.apply(void 0, __spreadArray([handler, timeout], __read(args))); }, clearTimeout: function(handle) { return (clearTimeout)(handle); }, delegate: void 0 }; function reportUnhandledError(err) { timeoutProvider.setTimeout(function() { { throw err; } }); } function noop() { } function errorContext(cb) { { cb(); } } var Subscriber = function(_super) { __extends(Subscriber2, _super); function Subscriber2(destination) { var _this = _super.call(this) || this; _this.isStopped = false; if (destination) { _this.destination = destination; if (isSubscription(destination)) { destination.add(_this); } } else { _this.destination = EMPTY_OBSERVER; } return _this; } Subscriber2.create = function(next, error, complete) { return new SafeSubscriber(next, error, complete); }; Subscriber2.prototype.next = function(value) { if (this.isStopped) ; else { this._next(value); } }; Subscriber2.prototype.error = function(err) { if (this.isStopped) ; else { this.isStopped = true; this._error(err); } }; Subscriber2.prototype.complete = function() { if (this.isStopped) ; else { this.isStopped = true; this._complete(); } }; Subscriber2.prototype.unsubscribe = function() { if (!this.closed) { this.isStopped = true; _super.prototype.unsubscribe.call(this); this.destination = null; } }; Subscriber2.prototype._next = function(value) { this.destination.next(value); }; Subscriber2.prototype._error = function(err) { try { this.destination.error(err); } finally { this.unsubscribe(); } }; Subscriber2.prototype._complete = function() { try { this.destination.complete(); } finally { this.unsubscribe(); } }; return Subscriber2; }(Subscription); var _bind = Function.prototype.bind; function bind(fn, thisArg) { return _bind.call(fn, thisArg); } var ConsumerObserver = function() { function ConsumerObserver2(partialObserver) { this.partialObserver = partialObserver; } ConsumerObserver2.prototype.next = function(value) { var partialObserver = this.partialObserver; if (partialObserver.next) { try { partialObserver.next(value); } catch (error) { handleUnhandledError(error); } } }; ConsumerObserver2.prototype.error = function(err) { var partialObserver = this.partialObserver; if (partialObserver.error) { try { partialObserver.error(err); } catch (error) { handleUnhandledError(error); } } else { handleUnhandledError(err); } }; ConsumerObserver2.prototype.complete = function() { var partialObserver = this.partialObserver; if (partialObserver.complete) { try { partialObserver.complete(); } catch (error) { handleUnhandledError(error); } } }; return ConsumerObserver2; }(); var SafeSubscriber = function(_super) { __extends(SafeSubscriber2, _super); function SafeSubscriber2(observerOrNext, error, complete) { var _this = _super.call(this) || this; var partialObserver; if (isFunction(observerOrNext) || !observerOrNext) { partialObserver = { next: observerOrNext !== null && observerOrNext !== void 0 ? observerOrNext : void 0, error: error !== null && error !== void 0 ? error : void 0, complete: complete !== null && complete !== void 0 ? complete : void 0 }; } else { var context_1; if (_this && config.useDeprecatedNextContext) { context_1 = Object.create(observerOrNext); context_1.unsubscribe = function() { return _this.unsubscribe(); }; partialObserver = { next: observerOrNext.next && bind(observerOrNext.next, context_1), error: observerOrNext.error && bind(observerOrNext.error, context_1), complete: observerOrNext.complete && bind(observerOrNext.complete, context_1) }; } else { partialObserver = observerOrNext; } } _this.destination = new ConsumerObserver(partialObserver); return _this; } return SafeSubscriber2; }(Subscriber); function handleUnhandledError(error) { { reportUnhandledError(error); } } function defaultErrorHandler(err) { throw err; } var EMPTY_OBSERVER = { closed: true, next: noop, error: defaultErrorHandler, complete: noop }; var observable = function() { return typeof Symbol === "function" && Symbol.observable || "@@observable"; }(); function identity(x) { return x; } function pipeFromArray(fns) { if (fns.length === 0) { return identity; } if (fns.length === 1) { return fns[0]; } return function piped(input) { return fns.reduce(function(prev, fn) { return fn(prev); }, input); }; } var Observable = function() { function Observable2(subscribe) { if (subscribe) { this._subscribe = subscribe; } } Observable2.prototype.lift = function(operator) { var observable2 = new Observable2(); observable2.source = this; observable2.operator = operator; return observable2; }; Observable2.prototype.subscribe = function(observerOrNext, error, complete) { var _this = this; var subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete); errorContext(function() { var _a = _this, operator = _a.operator, source = _a.source; subscriber.add(operator ? operator.call(subscriber, source) : source ? _this._subscribe(subscriber) : _this._trySubscribe(subscriber)); }); return subscriber; }; Observable2.prototype._trySubscribe = function(sink) { try { return this._subscribe(sink); } catch (err) { sink.error(err); } }; Observable2.prototype.forEach = function(next, promiseCtor) { var _this = this; promiseCtor = getPromiseCtor(promiseCtor); return new promiseCtor(function(resolve, reject) { var subscriber = new SafeSubscriber({ next: function(value) { try { next(value); } catch (err) { reject(err); subscriber.unsubscribe(); } }, error: reject, complete: resolve }); _this.subscribe(subscriber); }); }; Observable2.prototype._subscribe = function(subscriber) { var _a; return (_a = this.source) === null || _a === void 0 ? void 0 : _a.subscribe(subscriber); }; Observable2.prototype[observable] = function() { return this; }; Observable2.prototype.pipe = function() { var operations = []; for (var _i = 0; _i < arguments.length; _i++) { operations[_i] = arguments[_i]; } return pipeFromArray(operations)(this); }; Observable2.prototype.toPromise = function(promiseCtor) { var _this = this; promiseCtor = getPromiseCtor(promiseCtor); return new promiseCtor(function(resolve, reject) { var value; _this.subscribe(function(x) { return value = x; }, function(err) { return reject(err); }, function() { return resolve(value); }); }); }; Observable2.create = function(subscribe) { return new Observable2(subscribe); }; return Observable2; }(); function getPromiseCtor(promiseCtor) { var _a; return (_a = promiseCtor !== null && promiseCtor !== void 0 ? promiseCtor : config.Promise) !== null && _a !== void 0 ? _a : Promise; } function isObserver(value) { return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete); } function isSubscriber(value) { return value && value instanceof Subscriber || isObserver(value) && isSubscription(value); } function isInteropObservable(input) { return isFunction(input[observable]); } function isAsyncIterable(obj) { return Symbol.asyncIterator && isFunction(obj === null || obj === void 0 ? void 0 : obj[Symbol.asyncIterator]); } function createInvalidObservableTypeError(input) { return new TypeError("You provided " + (input !== null && typeof input === "object" ? "an invalid object" : "'" + input + "'") + " where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable."); } function getSymbolIterator() { if (typeof Symbol !== "function" || !Symbol.iterator) { return "@@iterator"; } return Symbol.iterator; } var iterator = getSymbolIterator(); function isIterable(input) { return isFunction(input === null || input === void 0 ? void 0 : input[iterator]); } function readableStreamLikeToAsyncGenerator(readableStream) { return __asyncGenerator(this, arguments, function readableStreamLikeToAsyncGenerator_1() { var reader, _a, value, done; return __generator(this, function(_b) { switch (_b.label) { case 0: reader = readableStream.getReader(); _b.label = 1; case 1: _b.trys.push([1, , 9, 10]); _b.label = 2; case 2: return [4, __await(reader.read())]; case 3: _a = _b.sent(), value = _a.value, done = _a.done; if (!done) return [3, 5]; return [4, __await(void 0)]; case 4: return [2, _b.sent()]; case 5: return [4, __await(value)]; case 6: return [4, _b.sent()]; case 7: _b.sent(); return [3, 2]; case 8: return [3, 10]; case 9: reader.releaseLock(); return [7]; case 10: return [2]; } }); }); } function isReadableStreamLike(obj) { return isFunction(obj === null || obj === void 0 ? void 0 : obj.getReader); } function innerFrom(input) { if (input instanceof Observable) { return input; } if (input != null) { if (isInteropObservable(input)) { return fromInteropObservable(input); } if (isArrayLike(input)) { return fromArrayLike(input); } if (isPromise(input)) { return fromPromise(input); } if (isAsyncIterable(input)) { return fromAsyncIterable(input); } if (isIterable(input)) { return fromIterable(input); } if (isReadableStreamLike(input)) { return fromReadableStreamLike(input); } } throw createInvalidObservableTypeError(input); } function fromInteropObservable(obj) { return new Observable(function(subscriber) { var obs = obj[observable](); if (isFunction(obs.subscribe)) { return obs.subscribe(subscriber); } throw new TypeError("Provided object does not correctly implement Symbol.observable"); }); } function fromArrayLike(array) { return new Observable(function(subscriber) { for (var i = 0; i < array.length && !subscriber.closed; i++) { subscriber.next(array[i]); } subscriber.complete(); }); } function fromPromise(promise) { return new Observable(function(subscriber) { promise.then(function(value) { if (!subscriber.closed) { subscriber.next(value); subscriber.complete(); } }, function(err) { return subscriber.error(err); }).then(null, reportUnhandledError); }); } function fromIterable(iterable) { return new Observable(function(subscriber) { var e_1, _a; try { for (var iterable_1 = __values(iterable), iterable_1_1 = iterable_1.next(); !iterable_1_1.done; iterable_1_1 = iterable_1.next()) { var value = iterable_1_1.value; subscriber.next(value); if (subscriber.closed) { return; } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (iterable_1_1 && !iterable_1_1.done && (_a = iterable_1.return)) _a.call(iterable_1); } finally { if (e_1) throw e_1.error; } } subscriber.complete(); }); } function fromAsyncIterable(asyncIterable) { return new Observable(function(subscriber) { process(asyncIterable, subscriber).catch(function(err) { return subscriber.error(err); }); }); } function fromReadableStreamLike(readableStream) { return fromAsyncIterable(readableStreamLikeToAsyncGenerator(readableStream)); } function process(asyncIterable, subscriber) { var asyncIterable_1, asyncIterable_1_1; var e_2, _a; return __awaiter(this, void 0, void 0, function() { var value, e_2_1; return __generator(this, function(_b) { switch (_b.label) { case 0: _b.trys.push([0, 5, 6, 11]); asyncIterable_1 = __asyncValues(asyncIterable); _b.label = 1; case 1: return [4, asyncIterable_1.next()]; case 2: if (!(asyncIterable_1_1 = _b.sent(), !asyncIterable_1_1.done)) return [3, 4]; value = asyncIterable_1_1.value; subscriber.next(value); if (subscriber.closed) { return [2]; } _b.label = 3; case 3: return [3, 1]; case 4: return [3, 11]; case 5: e_2_1 = _b.sent(); e_2 = { error: e_2_1 }; return [3, 11]; case 6: _b.trys.push([6, , 9, 10]); if (!(asyncIterable_1_1 && !asyncIterable_1_1.done && (_a = asyncIterable_1.return))) return [3, 8]; return [4, _a.call(asyncIterable_1)]; case 7: _b.sent(); _b.label = 8; case 8: return [3, 10]; case 9: if (e_2) throw e_2.error; return [7]; case 10: return [7]; case 11: subscriber.complete(); return [2]; } }); }); } function createOperatorSubscriber(destination, onNext, onComplete, onError, onFinalize) { return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize); } var OperatorSubscriber = function(_super) { __extends(OperatorSubscriber2, _super); function OperatorSubscriber2(destination, onNext, onComplete, onError, onFinalize, shouldUnsubscribe) { var _this = _super.call(this, destination) || this; _this.onFinalize = onFinalize; _this.shouldUnsubscribe = shouldUnsubscribe; _this._next = onNext ? function(value) { try { onNext(value); } catch (err) { destination.error(err); } } : _super.prototype._next; _this._error = onError ? function(err) { try { onError(err); } catch (err2) { destination.error(err2); } finally { this.unsubscribe(); } } : _super.prototype._error; _this._complete = onComplete ? function() { try { onComplete(); } catch (err) { destination.error(err); } finally { this.unsubscribe(); } } : _super.prototype._complete; return _this; } OperatorSubscriber2.prototype.unsubscribe = function() { var _a; if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) { var closed_1 = this.closed; _super.prototype.unsubscribe.call(this); !closed_1 && ((_a = this.onFinalize) === null || _a === void 0 ? void 0 : _a.call(this)); } }; return OperatorSubscriber2; }(Subscriber); function executeSchedule(parentSubscription, scheduler, work, delay, repeat) { if (delay === void 0) { delay = 0; } if (repeat === void 0) { repeat = false; } var scheduleSubscription = scheduler.schedule(function() { work(); if (repeat) { parentSubscription.add(this.schedule(null, delay)); } else { this.unsubscribe(); } }, delay); parentSubscription.add(scheduleSubscription); if (!repeat) { return scheduleSubscription; } } function map(project, thisArg) { return operate(function(source, subscriber) { var index = 0; source.subscribe(createOperatorSubscriber(subscriber, function(value) { subscriber.next(project.call(thisArg, value, index++)); })); }); } function mergeInternals(source, subscriber, project, concurrent, onBeforeNext, expand, innerSubScheduler, additionalFinalizer) { var buffer = []; var active = 0; var index = 0; var isComplete = false; var checkComplete = function() { if (isComplete && !buffer.length && !active) { subscriber.complete(); } }; var outerNext = function(value) { return active < concurrent ? doInnerSub(value) : buffer.push(value); }; var doInnerSub = function(value) { expand && subscriber.next(value); active++; var innerComplete = false; innerFrom(project(value, index++)).subscribe(createOperatorSubscriber(subscriber, function(innerValue) { onBeforeNext === null || onBeforeNext === void 0 ? void 0 : onBeforeNext(innerValue); if (expand) { outerNext(innerValue); } else { subscriber.next(innerValue); } }, function() { innerComplete = true; }, void 0, function() { if (innerComplete) { try { active--; var _loop_1 = function() { var bufferedValue = buffer.shift(); if (innerSubScheduler) { executeSchedule(subscriber, innerSubScheduler, function() { return doInnerSub(bufferedValue); }); } else { doInnerSub(bufferedValue); } }; while (buffer.length && active < concurrent) { _loop_1(); } checkComplete(); } catch (err) { subscriber.error(err); } } })); }; source.subscribe(createOperatorSubscriber(subscriber, outerNext, function() { isComplete = true; checkComplete(); })); return function() { additionalFinalizer === null || additionalFinalizer === void 0 ? void 0 : additionalFinalizer(); }; } function mergeMap(project, resultSelector, concurrent) { if (concurrent === void 0) { concurrent = Infinity; } if (isFunction(resultSelector)) { return mergeMap(function(a, i) { return map(function(b, ii) { return resultSelector(a, b, i, ii); })(innerFrom(project(a, i))); }, concurrent); } else if (typeof resultSelector === "number") { concurrent = resultSelector; } return operate(function(source, subscriber) { return mergeInternals(source, subscriber, project, concurrent); }); } function mergeAll(concurrent) { if (concurrent === void 0) { concurrent = Infinity; } return mergeMap(identity, concurrent); } function concatAll() { return mergeAll(1); } function concatMap(project, resultSelector) { return isFunction(resultSelector) ? mergeMap(project, resultSelector, 1) : mergeMap(project, 1); } const FrameScanner = { collectDirect(root) { return [...root.querySelectorAll("iframe")]; }, collectDeep(root) { const result = []; const collect = (node) => { for (const fr of node.querySelectorAll("iframe")) { result.push(fr); try { if (fr.contentDocument) { collect(fr.contentDocument.documentElement); } } catch (e) { } } }; collect(root); return rxjs.of(result); }, collectDeepSync(root) { const result = []; const collect = (node) => { for (const fr of node.querySelectorAll("iframe")) { result.push(fr); try { if (fr.contentDocument) { collect(fr.contentDocument.documentElement); } } catch (e) { } } }; collect(root); return result; } }; var Typr$1 = {}; var Typr = {}; Typr.parse = function(buff) { var bin = Typr._bin; var data = new Uint8Array(buff); var tag = bin.readASCII(data, 0, 4); if (tag == "ttcf") { var offset = 4; bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; var numF = bin.readUint(data, offset); offset += 4; var fnts = []; for (var i = 0; i < numF; i++) { var foff = bin.readUint(data, offset); offset += 4; fnts.push(Typr._readFont(data, foff)); } return fnts; } else return [Typr._readFont(data, 0)]; }; Typr._readFont = function(data, offset) { var bin = Typr._bin; var ooff = offset; bin.readFixed(data, offset); offset += 4; var numTables = bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; var tags = [ "cmap", "head", "hhea", "maxp", "hmtx", "name", "OS/2", "post", "loca", "glyf", "kern", "CFF ", "GPOS", "GSUB", "SVG " ]; var obj = { _data: data, _offset: ooff }; var tabs = {}; for (var i = 0; i < numTables; i++) { var tag = bin.readASCII(data, offset, 4); offset += 4; bin.readUint(data, offset); offset += 4; var toffset = bin.readUint(data, offset); offset += 4; var length = bin.readUint(data, offset); offset += 4; tabs[tag] = { offset: toffset, length }; } for (var i = 0; i < tags.length; i++) { var t = tags[i]; if (tabs[t]) obj[t.trim()] = Typr[t.trim()].parse(data, tabs[t].offset, tabs[t].length, obj); } return obj; }; Typr._tabOffset = function(data, tab, foff) { var bin = Typr._bin; var numTables = bin.readUshort(data, foff + 4); var offset = foff + 12; for (var i = 0; i < numTables; i++) { var tag = bin.readASCII(data, offset, 4); offset += 4; bin.readUint(data, offset); offset += 4; var toffset = bin.readUint(data, offset); offset += 4; bin.readUint(data, offset); offset += 4; if (tag == tab) return toffset; } return 0; }; Typr._bin = { readFixed: function(data, o) { return (data[o] << 8 | data[o + 1]) + (data[o + 2] << 8 | data[o + 3]) / (256 * 256 + 4); }, readF2dot14: function(data, o) { var num = Typr._bin.readShort(data, o); return num / 16384; }, readInt: function(buff, p) { return Typr._bin._view(buff).getInt32(p); }, readInt8: function(buff, p) { return Typr._bin._view(buff).getInt8(p); }, readShort: function(buff, p) { return Typr._bin._view(buff).getInt16(p); }, readUshort: function(buff, p) { return Typr._bin._view(buff).getUint16(p); }, readUshorts: function(buff, p, len) { var arr = []; for (var i = 0; i < len; i++) arr.push(Typr._bin.readUshort(buff, p + i * 2)); return arr; }, readUint: function(buff, p) { return Typr._bin._view(buff).getUint32(p); }, readUint64: function(buff, p) { return Typr._bin.readUint(buff, p) * (4294967295 + 1) + Typr._bin.readUint(buff, p + 4); }, readASCII: function(buff, p, l) { var s = ""; for (var i = 0; i < l; i++) s += String.fromCharCode(buff[p + i]); return s; }, readUnicode: function(buff, p, l) { var s = ""; for (var i = 0; i < l; i++) { var c = buff[p++] << 8 | buff[p++]; s += String.fromCharCode(c); } return s; }, _tdec: typeof window !== "undefined" && window["TextDecoder"] ? new window["TextDecoder"]() : null, readUTF8: function(buff, p, l) { var tdec = Typr._bin._tdec; if (tdec && p == 0 && l == buff.length) return tdec["decode"](buff); return Typr._bin.readASCII(buff, p, l); }, readBytes: function(buff, p, l) { var arr = []; for (var i = 0; i < l; i++) arr.push(buff[p + i]); return arr; }, readASCIIArray: function(buff, p, l) { var s = []; for (var i = 0; i < l; i++) s.push(String.fromCharCode(buff[p + i])); return s; }, _view: function(buff) { return buff._dataView || (buff._dataView = buff.buffer ? new DataView(buff.buffer, buff.byteOffset, buff.byteLength) : new DataView(new Uint8Array(buff).buffer)); } }; Typr._lctf = {}; Typr._lctf.parse = function(data, offset, length, font, subt) { var bin = Typr._bin; var obj = {}; var offset0 = offset; bin.readFixed(data, offset); offset += 4; var offScriptList = bin.readUshort(data, offset); offset += 2; var offFeatureList = bin.readUshort(data, offset); offset += 2; var offLookupList = bin.readUshort(data, offset); offset += 2; obj.scriptList = Typr._lctf.readScriptList(data, offset0 + offScriptList); obj.featureList = Typr._lctf.readFeatureList(data, offset0 + offFeatureList); obj.lookupList = Typr._lctf.readLookupList(data, offset0 + offLookupList, subt); return obj; }; Typr._lctf.readLookupList = function(data, offset, subt) { var bin = Typr._bin; var offset0 = offset; var obj = []; var count = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < count; i++) { var noff = bin.readUshort(data, offset); offset += 2; var lut = Typr._lctf.readLookupTable(data, offset0 + noff, subt); obj.push(lut); } return obj; }; Typr._lctf.readLookupTable = function(data, offset, subt) { var bin = Typr._bin; var offset0 = offset; var obj = { tabs: [] }; obj.ltype = bin.readUshort(data, offset); offset += 2; obj.flag = bin.readUshort(data, offset); offset += 2; var cnt = bin.readUshort(data, offset); offset += 2; var ltype = obj.ltype; for (var i = 0; i < cnt; i++) { var noff = bin.readUshort(data, offset); offset += 2; var tab = subt(data, ltype, offset0 + noff, obj); obj.tabs.push(tab); } return obj; }; Typr._lctf.numOfOnes = function(n) { var num = 0; for (var i = 0; i < 32; i++) if ((n >>> i & 1) != 0) num++; return num; }; Typr._lctf.readClassDef = function(data, offset) { var bin = Typr._bin; var obj = []; var format = bin.readUshort(data, offset); offset += 2; if (format == 1) { var startGlyph = bin.readUshort(data, offset); offset += 2; var glyphCount = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < glyphCount; i++) { obj.push(startGlyph + i); obj.push(startGlyph + i); obj.push(bin.readUshort(data, offset)); offset += 2; } } if (format == 2) { var count = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < count; i++) { obj.push(bin.readUshort(data, offset)); offset += 2; obj.push(bin.readUshort(data, offset)); offset += 2; obj.push(bin.readUshort(data, offset)); offset += 2; } } return obj; }; Typr._lctf.getInterval = function(tab, val) { for (var i = 0; i < tab.length; i += 3) { var start = tab[i], end = tab[i + 1]; tab[i + 2]; if (start <= val && val <= end) return i; } return -1; }; Typr._lctf.readCoverage = function(data, offset) { var bin = Typr._bin; var cvg = {}; cvg.fmt = bin.readUshort(data, offset); offset += 2; var count = bin.readUshort(data, offset); offset += 2; if (cvg.fmt == 1) cvg.tab = bin.readUshorts(data, offset, count); if (cvg.fmt == 2) cvg.tab = bin.readUshorts(data, offset, count * 3); return cvg; }; Typr._lctf.coverageIndex = function(cvg, val) { var tab = cvg.tab; if (cvg.fmt == 1) return tab.indexOf(val); if (cvg.fmt == 2) { var ind = Typr._lctf.getInterval(tab, val); if (ind != -1) return tab[ind + 2] + (val - tab[ind]); } return -1; }; Typr._lctf.readFeatureList = function(data, offset) { var bin = Typr._bin; var offset0 = offset; var obj = []; var count = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < count; i++) { var tag = bin.readASCII(data, offset, 4); offset += 4; var noff = bin.readUshort(data, offset); offset += 2; var feat = Typr._lctf.readFeatureTable(data, offset0 + noff); feat.tag = tag.trim(); obj.push(feat); } return obj; }; Typr._lctf.readFeatureTable = function(data, offset) { var bin = Typr._bin; var offset0 = offset; var feat = {}; var featureParams = bin.readUshort(data, offset); offset += 2; if (featureParams > 0) { feat.featureParams = offset0 + featureParams; } var lookupCount = bin.readUshort(data, offset); offset += 2; feat.tab = []; for (var i = 0; i < lookupCount; i++) feat.tab.push(bin.readUshort(data, offset + 2 * i)); return feat; }; Typr._lctf.readScriptList = function(data, offset) { var bin = Typr._bin; var offset0 = offset; var obj = {}; var count = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < count; i++) { var tag = bin.readASCII(data, offset, 4); offset += 4; var noff = bin.readUshort(data, offset); offset += 2; obj[tag.trim()] = Typr._lctf.readScriptTable(data, offset0 + noff); } return obj; }; Typr._lctf.readScriptTable = function(data, offset) { var bin = Typr._bin; var offset0 = offset; var obj = {}; var defLangSysOff = bin.readUshort(data, offset); offset += 2; if (defLangSysOff > 0) { obj["default"] = Typr._lctf.readLangSysTable(data, offset0 + defLangSysOff); } var langSysCount = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < langSysCount; i++) { var tag = bin.readASCII(data, offset, 4); offset += 4; var langSysOff = bin.readUshort(data, offset); offset += 2; obj[tag.trim()] = Typr._lctf.readLangSysTable(data, offset0 + langSysOff); } return obj; }; Typr._lctf.readLangSysTable = function(data, offset) { var bin = Typr._bin; var obj = {}; bin.readUshort(data, offset); offset += 2; obj.reqFeature = bin.readUshort(data, offset); offset += 2; var featureCount = bin.readUshort(data, offset); offset += 2; obj.features = bin.readUshorts(data, offset, featureCount); return obj; }; Typr.CFF = {}; Typr.CFF.parse = function(data, offset, length) { var bin = Typr._bin; data = new Uint8Array(data.buffer, offset, length); offset = 0; data[offset]; offset++; data[offset]; offset++; data[offset]; offset++; data[offset]; offset++; var ninds = []; offset = Typr.CFF.readIndex(data, offset, ninds); var names = []; for (var i = 0; i < ninds.length - 1; i++) names.push(bin.readASCII(data, offset + ninds[i], ninds[i + 1] - ninds[i])); offset += ninds[ninds.length - 1]; var tdinds = []; offset = Typr.CFF.readIndex(data, offset, tdinds); var topDicts = []; for (var i = 0; i < tdinds.length - 1; i++) topDicts.push(Typr.CFF.readDict(data, offset + tdinds[i], offset + tdinds[i + 1])); offset += tdinds[tdinds.length - 1]; var topdict = topDicts[0]; var sinds = []; offset = Typr.CFF.readIndex(data, offset, sinds); var strings = []; for (var i = 0; i < sinds.length - 1; i++) strings.push(bin.readASCII(data, offset + sinds[i], sinds[i + 1] - sinds[i])); offset += sinds[sinds.length - 1]; Typr.CFF.readSubrs(data, offset, topdict); if (topdict.CharStrings) { offset = topdict.CharStrings; var sinds = []; offset = Typr.CFF.readIndex(data, offset, sinds); var cstr = []; for (var i = 0; i < sinds.length - 1; i++) cstr.push(bin.readBytes(data, offset + sinds[i], sinds[i + 1] - sinds[i])); topdict.CharStrings = cstr; } if (topdict.ROS) { offset = topdict.FDArray; var fdind = []; offset = Typr.CFF.readIndex(data, offset, fdind); topdict.FDArray = []; for (var i = 0; i < fdind.length - 1; i++) { var dict = Typr.CFF.readDict(data, offset + fdind[i], offset + fdind[i + 1]); Typr.CFF._readFDict(data, dict, strings); topdict.FDArray.push(dict); } offset += fdind[fdind.length - 1]; offset = topdict.FDSelect; topdict.FDSelect = []; var fmt = data[offset]; offset++; if (fmt == 3) { var rns = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < rns + 1; i++) { topdict.FDSelect.push(bin.readUshort(data, offset), data[offset + 2]); offset += 3; } } else throw fmt; } if (topdict.Encoding) topdict.Encoding = Typr.CFF.readEncoding(data, topdict.Encoding, topdict.CharStrings.length); if (topdict.charset) topdict.charset = Typr.CFF.readCharset(data, topdict.charset, topdict.CharStrings.length); Typr.CFF._readFDict(data, topdict, strings); return topdict; }; Typr.CFF._readFDict = function(data, dict, ss) { var offset; if (dict.Private) { offset = dict.Private[1]; dict.Private = Typr.CFF.readDict(data, offset, offset + dict.Private[0]); if (dict.Private.Subrs) Typr.CFF.readSubrs(data, offset + dict.Private.Subrs, dict.Private); } for (var p in dict) if (["FamilyName", "FontName", "FullName", "Notice", "version", "Copyright"].indexOf(p) != -1) dict[p] = ss[dict[p] - 426 + 35]; }; Typr.CFF.readSubrs = function(data, offset, obj) { var bin = Typr._bin; var gsubinds = []; offset = Typr.CFF.readIndex(data, offset, gsubinds); var bias, nSubrs = gsubinds.length; if (nSubrs < 1240) bias = 107; else if (nSubrs < 33900) bias = 1131; else bias = 32768; obj.Bias = bias; obj.Subrs = []; for (var i = 0; i < gsubinds.length - 1; i++) obj.Subrs.push(bin.readBytes(data, offset + gsubinds[i], gsubinds[i + 1] - gsubinds[i])); }; Typr.CFF.tableSE = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 0, 111, 112, 113, 114, 0, 115, 116, 117, 118, 119, 120, 121, 122, 0, 123, 0, 124, 125, 126, 127, 128, 129, 130, 131, 0, 132, 133, 0, 134, 135, 136, 137, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 138, 0, 139, 0, 0, 0, 0, 140, 141, 142, 143, 0, 0, 0, 0, 0, 144, 0, 0, 0, 145, 0, 0, 146, 147, 148, 149, 0, 0, 0, 0 ]; Typr.CFF.glyphByUnicode = function(cff, code) { for (var i = 0; i < cff.charset.length; i++) if (cff.charset[i] == code) return i; return -1; }; Typr.CFF.glyphBySE = function(cff, charcode) { if (charcode < 0 || charcode > 255) return -1; return Typr.CFF.glyphByUnicode(cff, Typr.CFF.tableSE[charcode]); }; Typr.CFF.readEncoding = function(data, offset, num) { Typr._bin; var array = [".notdef"]; var format = data[offset]; offset++; if (format == 0) { var nCodes = data[offset]; offset++; for (var i = 0; i < nCodes; i++) array.push(data[offset + i]); } else throw "error: unknown encoding format: " + format; return array; }; Typr.CFF.readCharset = function(data, offset, num) { var bin = Typr._bin; var charset = [".notdef"]; var format = data[offset]; offset++; if (format == 0) { for (var i = 0; i < num; i++) { var first = bin.readUshort(data, offset); offset += 2; charset.push(first); } } else if (format == 1 || format == 2) { while (charset.length < num) { var first = bin.readUshort(data, offset); offset += 2; var nLeft = 0; if (format == 1) { nLeft = data[offset]; offset++; } else { nLeft = bin.readUshort(data, offset); offset += 2; } for (var i = 0; i <= nLeft; i++) { charset.push(first); first++; } } } else throw "error: format: " + format; return charset; }; Typr.CFF.readIndex = function(data, offset, inds) { var bin = Typr._bin; var count = bin.readUshort(data, offset) + 1; offset += 2; var offsize = data[offset]; offset++; if (offsize == 1) for (var i = 0; i < count; i++) inds.push(data[offset + i]); else if (offsize == 2) for (var i = 0; i < count; i++) inds.push(bin.readUshort(data, offset + i * 2)); else if (offsize == 3) for (var i = 0; i < count; i++) inds.push(bin.readUint(data, offset + i * 3 - 1) & 16777215); else if (count != 1) throw "unsupported offset size: " + offsize + ", count: " + count; offset += count * offsize; return offset - 1; }; Typr.CFF.getCharString = function(data, offset, o) { var bin = Typr._bin; var b0 = data[offset], b1 = data[offset + 1]; data[offset + 2]; data[offset + 3]; data[offset + 4]; var vs = 1; var op = null, val = null; if (b0 <= 20) { op = b0; vs = 1; } if (b0 == 12) { op = b0 * 100 + b1; vs = 2; } if (21 <= b0 && b0 <= 27) { op = b0; vs = 1; } if (b0 == 28) { val = bin.readShort(data, offset + 1); vs = 3; } if (29 <= b0 && b0 <= 31) { op = b0; vs = 1; } if (32 <= b0 && b0 <= 246) { val = b0 - 139; vs = 1; } if (247 <= b0 && b0 <= 250) { val = (b0 - 247) * 256 + b1 + 108; vs = 2; } if (251 <= b0 && b0 <= 254) { val = -(b0 - 251) * 256 - b1 - 108; vs = 2; } if (b0 == 255) { val = bin.readInt(data, offset + 1) / 65535; vs = 5; } o.val = val != null ? val : "o" + op; o.size = vs; }; Typr.CFF.readCharString = function(data, offset, length) { var end = offset + length; var bin = Typr._bin; var arr = []; while (offset < end) { var b0 = data[offset], b1 = data[offset + 1]; data[offset + 2]; data[offset + 3]; data[offset + 4]; var vs = 1; var op = null, val = null; if (b0 <= 20) { op = b0; vs = 1; } if (b0 == 12) { op = b0 * 100 + b1; vs = 2; } if (b0 == 19 || b0 == 20) { op = b0; vs = 2; } if (21 <= b0 && b0 <= 27) { op = b0; vs = 1; } if (b0 == 28) { val = bin.readShort(data, offset + 1); vs = 3; } if (29 <= b0 && b0 <= 31) { op = b0; vs = 1; } if (32 <= b0 && b0 <= 246) { val = b0 - 139; vs = 1; } if (247 <= b0 && b0 <= 250) { val = (b0 - 247) * 256 + b1 + 108; vs = 2; } if (251 <= b0 && b0 <= 254) { val = -(b0 - 251) * 256 - b1 - 108; vs = 2; } if (b0 == 255) { val = bin.readInt(data, offset + 1) / 65535; vs = 5; } arr.push(val != null ? val : "o" + op); offset += vs; } return arr; }; Typr.CFF.readDict = function(data, offset, end) { var bin = Typr._bin; var dict = {}; var carr = []; while (offset < end) { var b0 = data[offset], b1 = data[offset + 1]; data[offset + 2]; data[offset + 3]; data[offset + 4]; var vs = 1; var key = null, val = null; if (b0 == 28) { val = bin.readShort(data, offset + 1); vs = 3; } if (b0 == 29) { val = bin.readInt(data, offset + 1); vs = 5; } if (32 <= b0 && b0 <= 246) { val = b0 - 139; vs = 1; } if (247 <= b0 && b0 <= 250) { val = (b0 - 247) * 256 + b1 + 108; vs = 2; } if (251 <= b0 && b0 <= 254) { val = -(b0 - 251) * 256 - b1 - 108; vs = 2; } if (b0 == 255) { val = bin.readInt(data, offset + 1) / 65535; vs = 5; throw "unknown number"; } if (b0 == 30) { var nibs = []; vs = 1; while (true) { var b = data[offset + vs]; vs++; var nib0 = b >> 4, nib1 = b & 15; if (nib0 != 15) nibs.push(nib0); if (nib1 != 15) nibs.push(nib1); if (nib1 == 15) break; } var s = ""; var chars = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ".", "e", "e-", "reserved", "-", "endOfNumber"]; for (var i = 0; i < nibs.length; i++) s += chars[nibs[i]]; val = parseFloat(s); } if (b0 <= 21) { var keys = [ "version", "Notice", "FullName", "FamilyName", "Weight", "FontBBox", "BlueValues", "OtherBlues", "FamilyBlues", "FamilyOtherBlues", "StdHW", "StdVW", "escape", "UniqueID", "XUID", "charset", "Encoding", "CharStrings", "Private", "Subrs", "defaultWidthX", "nominalWidthX" ]; key = keys[b0]; vs = 1; if (b0 == 12) { var keys = [ "Copyright", "isFixedPitch", "ItalicAngle", "UnderlinePosition", "UnderlineThickness", "PaintType", "CharstringType", "FontMatrix", "StrokeWidth", "BlueScale", "BlueShift", "BlueFuzz", "StemSnapH", "StemSnapV", "ForceBold", 0, 0, "LanguageGroup", "ExpansionFactor", "initialRandomSeed", "SyntheticBase", "PostScript", "BaseFontName", "BaseFontBlend", 0, 0, 0, 0, 0, 0, "ROS", "CIDFontVersion", "CIDFontRevision", "CIDFontType", "CIDCount", "UIDBase", "FDArray", "FDSelect", "FontName" ]; key = keys[b1]; vs = 2; } } if (key != null) { dict[key] = carr.length == 1 ? carr[0] : carr; carr = []; } else carr.push(val); offset += vs; } return dict; }; Typr.cmap = {}; Typr.cmap.parse = function(data, offset, length) { data = new Uint8Array(data.buffer, offset, length); offset = 0; var bin = Typr._bin; var obj = {}; bin.readUshort(data, offset); offset += 2; var numTables = bin.readUshort(data, offset); offset += 2; var offs = []; obj.tables = []; for (var i = 0; i < numTables; i++) { var platformID = bin.readUshort(data, offset); offset += 2; var encodingID = bin.readUshort(data, offset); offset += 2; var noffset = bin.readUint(data, offset); offset += 4; var id = "p" + platformID + "e" + encodingID; var tind = offs.indexOf(noffset); if (tind == -1) { tind = obj.tables.length; var subt; offs.push(noffset); var format = bin.readUshort(data, noffset); if (format == 0) subt = Typr.cmap.parse0(data, noffset); else if (format == 4) subt = Typr.cmap.parse4(data, noffset); else if (format == 6) subt = Typr.cmap.parse6(data, noffset); else if (format == 12) subt = Typr.cmap.parse12(data, noffset); else console.warn("unknown format: " + format, platformID, encodingID, noffset); obj.tables.push(subt); } if (obj[id] != null) throw "multiple tables for one platform+encoding"; obj[id] = tind; } return obj; }; Typr.cmap.parse0 = function(data, offset) { var bin = Typr._bin; var obj = {}; obj.format = bin.readUshort(data, offset); offset += 2; var len = bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; obj.map = []; for (var i = 0; i < len - 6; i++) obj.map.push(data[offset + i]); return obj; }; Typr.cmap.parse4 = function(data, offset) { var bin = Typr._bin; var offset0 = offset; var obj = {}; obj.format = bin.readUshort(data, offset); offset += 2; var length = bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; var segCountX2 = bin.readUshort(data, offset); offset += 2; var segCount = segCountX2 / 2; obj.searchRange = bin.readUshort(data, offset); offset += 2; obj.entrySelector = bin.readUshort(data, offset); offset += 2; obj.rangeShift = bin.readUshort(data, offset); offset += 2; obj.endCount = bin.readUshorts(data, offset, segCount); offset += segCount * 2; offset += 2; obj.startCount = bin.readUshorts(data, offset, segCount); offset += segCount * 2; obj.idDelta = []; for (var i = 0; i < segCount; i++) { obj.idDelta.push(bin.readShort(data, offset)); offset += 2; } obj.idRangeOffset = bin.readUshorts(data, offset, segCount); offset += segCount * 2; obj.glyphIdArray = []; while (offset < offset0 + length) { obj.glyphIdArray.push(bin.readUshort(data, offset)); offset += 2; } return obj; }; Typr.cmap.parse6 = function(data, offset) { var bin = Typr._bin; var obj = {}; obj.format = bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; obj.firstCode = bin.readUshort(data, offset); offset += 2; var entryCount = bin.readUshort(data, offset); offset += 2; obj.glyphIdArray = []; for (var i = 0; i < entryCount; i++) { obj.glyphIdArray.push(bin.readUshort(data, offset)); offset += 2; } return obj; }; Typr.cmap.parse12 = function(data, offset) { var bin = Typr._bin; var obj = {}; obj.format = bin.readUshort(data, offset); offset += 2; offset += 2; bin.readUint(data, offset); offset += 4; bin.readUint(data, offset); offset += 4; var nGroups = bin.readUint(data, offset); offset += 4; obj.groups = []; for (var i = 0; i < nGroups; i++) { var off = offset + i * 12; var startCharCode = bin.readUint(data, off + 0); var endCharCode = bin.readUint(data, off + 4); var startGlyphID = bin.readUint(data, off + 8); obj.groups.push([startCharCode, endCharCode, startGlyphID]); } return obj; }; Typr.glyf = {}; Typr.glyf.parse = function(data, offset, length, font) { var obj = []; for (var g = 0; g < font.maxp.numGlyphs; g++) obj.push(null); return obj; }; Typr.glyf._parseGlyf = function(font, g) { var bin = Typr._bin; var data = font._data; var offset = Typr._tabOffset(data, "glyf", font._offset) + font.loca[g]; if (font.loca[g] == font.loca[g + 1]) return null; var gl = {}; gl.noc = bin.readShort(data, offset); offset += 2; gl.xMin = bin.readShort(data, offset); offset += 2; gl.yMin = bin.readShort(data, offset); offset += 2; gl.xMax = bin.readShort(data, offset); offset += 2; gl.yMax = bin.readShort(data, offset); offset += 2; if (gl.xMin >= gl.xMax || gl.yMin >= gl.yMax) return null; if (gl.noc > 0) { gl.endPts = []; for (var i = 0; i < gl.noc; i++) { gl.endPts.push(bin.readUshort(data, offset)); offset += 2; } var instructionLength = bin.readUshort(data, offset); offset += 2; if (data.length - offset < instructionLength) return null; gl.instructions = bin.readBytes(data, offset, instructionLength); offset += instructionLength; var crdnum = gl.endPts[gl.noc - 1] + 1; gl.flags = []; for (var i = 0; i < crdnum; i++) { var flag = data[offset]; offset++; gl.flags.push(flag); if ((flag & 8) != 0) { var rep = data[offset]; offset++; for (var j = 0; j < rep; j++) { gl.flags.push(flag); i++; } } } gl.xs = []; for (var i = 0; i < crdnum; i++) { var i8 = (gl.flags[i] & 2) != 0, same = (gl.flags[i] & 16) != 0; if (i8) { gl.xs.push(same ? data[offset] : -data[offset]); offset++; } else { if (same) gl.xs.push(0); else { gl.xs.push(bin.readShort(data, offset)); offset += 2; } } } gl.ys = []; for (var i = 0; i < crdnum; i++) { var i8 = (gl.flags[i] & 4) != 0, same = (gl.flags[i] & 32) != 0; if (i8) { gl.ys.push(same ? data[offset] : -data[offset]); offset++; } else { if (same) gl.ys.push(0); else { gl.ys.push(bin.readShort(data, offset)); offset += 2; } } } var x = 0, y = 0; for (var i = 0; i < crdnum; i++) { x += gl.xs[i]; y += gl.ys[i]; gl.xs[i] = x; gl.ys[i] = y; } } else { var ARG_1_AND_2_ARE_WORDS = 1 << 0; var ARGS_ARE_XY_VALUES = 1 << 1; var WE_HAVE_A_SCALE = 1 << 3; var MORE_COMPONENTS = 1 << 5; var WE_HAVE_AN_X_AND_Y_SCALE = 1 << 6; var WE_HAVE_A_TWO_BY_TWO = 1 << 7; var WE_HAVE_INSTRUCTIONS = 1 << 8; gl.parts = []; var flags; do { flags = bin.readUshort(data, offset); offset += 2; var part = { m: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }, p1: -1, p2: -1 }; gl.parts.push(part); part.glyphIndex = bin.readUshort(data, offset); offset += 2; if (flags & ARG_1_AND_2_ARE_WORDS) { var arg1 = bin.readShort(data, offset); offset += 2; var arg2 = bin.readShort(data, offset); offset += 2; } else { var arg1 = bin.readInt8(data, offset); offset++; var arg2 = bin.readInt8(data, offset); offset++; } if (flags & ARGS_ARE_XY_VALUES) { part.m.tx = arg1; part.m.ty = arg2; } else { part.p1 = arg1; part.p2 = arg2; } if (flags & WE_HAVE_A_SCALE) { part.m.a = part.m.d = bin.readF2dot14(data, offset); offset += 2; } else if (flags & WE_HAVE_AN_X_AND_Y_SCALE) { part.m.a = bin.readF2dot14(data, offset); offset += 2; part.m.d = bin.readF2dot14(data, offset); offset += 2; } else if (flags & WE_HAVE_A_TWO_BY_TWO) { part.m.a = bin.readF2dot14(data, offset); offset += 2; part.m.b = bin.readF2dot14(data, offset); offset += 2; part.m.c = bin.readF2dot14(data, offset); offset += 2; part.m.d = bin.readF2dot14(data, offset); offset += 2; } } while (flags & MORE_COMPONENTS); if (flags & WE_HAVE_INSTRUCTIONS) { var numInstr = bin.readUshort(data, offset); offset += 2; gl.instr = []; for (var i = 0; i < numInstr; i++) { gl.instr.push(data[offset]); offset++; } } } return gl; }; Typr.GPOS = {}; Typr.GPOS.parse = function(data, offset, length, font) { return Typr._lctf.parse(data, offset, length, font, Typr.GPOS.subt); }; Typr.GPOS.subt = function(data, ltype, offset, ltable) { var bin = Typr._bin, offset0 = offset, tab = {}; tab.fmt = bin.readUshort(data, offset); offset += 2; if (ltype == 1 || ltype == 2 || ltype == 3 || ltype == 7 || ltype == 8 && tab.fmt <= 2) { var covOff = bin.readUshort(data, offset); offset += 2; tab.coverage = Typr._lctf.readCoverage(data, covOff + offset0); } if (ltype == 1 && tab.fmt == 1) { var valFmt1 = bin.readUshort(data, offset); offset += 2; var ones1 = Typr._lctf.numOfOnes(valFmt1); if (valFmt1 != 0) tab.pos = Typr.GPOS.readValueRecord(data, offset, valFmt1); } else if (ltype == 2 && tab.fmt >= 1 && tab.fmt <= 2) { var valFmt1 = bin.readUshort(data, offset); offset += 2; var valFmt2 = bin.readUshort(data, offset); offset += 2; var ones1 = Typr._lctf.numOfOnes(valFmt1); var ones2 = Typr._lctf.numOfOnes(valFmt2); if (tab.fmt == 1) { tab.pairsets = []; var psc = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < psc; i++) { var psoff = offset0 + bin.readUshort(data, offset); offset += 2; var pvc = bin.readUshort(data, psoff); psoff += 2; var arr = []; for (var j = 0; j < pvc; j++) { var gid2 = bin.readUshort(data, psoff); psoff += 2; var value1, value2; if (valFmt1 != 0) { value1 = Typr.GPOS.readValueRecord(data, psoff, valFmt1); psoff += ones1 * 2; } if (valFmt2 != 0) { value2 = Typr.GPOS.readValueRecord(data, psoff, valFmt2); psoff += ones2 * 2; } arr.push({ gid2, val1: value1, val2: value2 }); } tab.pairsets.push(arr); } } if (tab.fmt == 2) { var classDef1 = bin.readUshort(data, offset); offset += 2; var classDef2 = bin.readUshort(data, offset); offset += 2; var class1Count = bin.readUshort(data, offset); offset += 2; var class2Count = bin.readUshort(data, offset); offset += 2; tab.classDef1 = Typr._lctf.readClassDef(data, offset0 + classDef1); tab.classDef2 = Typr._lctf.readClassDef(data, offset0 + classDef2); tab.matrix = []; for (var i = 0; i < class1Count; i++) { var row = []; for (var j = 0; j < class2Count; j++) { var value1 = null, value2 = null; if (valFmt1 != 0) { value1 = Typr.GPOS.readValueRecord(data, offset, valFmt1); offset += ones1 * 2; } if (valFmt2 != 0) { value2 = Typr.GPOS.readValueRecord(data, offset, valFmt2); offset += ones2 * 2; } row.push({ val1: value1, val2: value2 }); } tab.matrix.push(row); } } } else if (ltype == 9 && tab.fmt == 1) { var extType = bin.readUshort(data, offset); offset += 2; var extOffset = bin.readUint(data, offset); offset += 4; if (ltable.ltype == 9) { ltable.ltype = extType; } else if (ltable.ltype != extType) { throw "invalid extension substitution"; } return Typr.GPOS.subt(data, ltable.ltype, offset0 + extOffset); } else console.warn("unsupported GPOS table LookupType", ltype, "format", tab.fmt); return tab; }; Typr.GPOS.readValueRecord = function(data, offset, valFmt) { var bin = Typr._bin; var arr = []; arr.push(valFmt & 1 ? bin.readShort(data, offset) : 0); offset += valFmt & 1 ? 2 : 0; arr.push(valFmt & 2 ? bin.readShort(data, offset) : 0); offset += valFmt & 2 ? 2 : 0; arr.push(valFmt & 4 ? bin.readShort(data, offset) : 0); offset += valFmt & 4 ? 2 : 0; arr.push(valFmt & 8 ? bin.readShort(data, offset) : 0); offset += valFmt & 8 ? 2 : 0; return arr; }; Typr.GSUB = {}; Typr.GSUB.parse = function(data, offset, length, font) { return Typr._lctf.parse(data, offset, length, font, Typr.GSUB.subt); }; Typr.GSUB.subt = function(data, ltype, offset, ltable) { var bin = Typr._bin, offset0 = offset, tab = {}; tab.fmt = bin.readUshort(data, offset); offset += 2; if (ltype != 1 && ltype != 4 && ltype != 5 && ltype != 6) return null; if (ltype == 1 || ltype == 4 || ltype == 5 && tab.fmt <= 2 || ltype == 6 && tab.fmt <= 2) { var covOff = bin.readUshort(data, offset); offset += 2; tab.coverage = Typr._lctf.readCoverage(data, offset0 + covOff); } if (ltype == 1 && tab.fmt >= 1 && tab.fmt <= 2) { if (tab.fmt == 1) { tab.delta = bin.readShort(data, offset); offset += 2; } else if (tab.fmt == 2) { var cnt = bin.readUshort(data, offset); offset += 2; tab.newg = bin.readUshorts(data, offset, cnt); offset += tab.newg.length * 2; } } else if (ltype == 4) { tab.vals = []; var cnt = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < cnt; i++) { var loff = bin.readUshort(data, offset); offset += 2; tab.vals.push(Typr.GSUB.readLigatureSet(data, offset0 + loff)); } } else if (ltype == 5 && tab.fmt == 2) { if (tab.fmt == 2) { var cDefOffset = bin.readUshort(data, offset); offset += 2; tab.cDef = Typr._lctf.readClassDef(data, offset0 + cDefOffset); tab.scset = []; var subClassSetCount = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < subClassSetCount; i++) { var scsOff = bin.readUshort(data, offset); offset += 2; tab.scset.push(scsOff == 0 ? null : Typr.GSUB.readSubClassSet(data, offset0 + scsOff)); } } } else if (ltype == 6 && tab.fmt == 3) { if (tab.fmt == 3) { for (var i = 0; i < 3; i++) { var cnt = bin.readUshort(data, offset); offset += 2; var cvgs = []; for (var j = 0; j < cnt; j++) cvgs.push(Typr._lctf.readCoverage(data, offset0 + bin.readUshort(data, offset + j * 2))); offset += cnt * 2; if (i == 0) tab.backCvg = cvgs; if (i == 1) tab.inptCvg = cvgs; if (i == 2) tab.ahedCvg = cvgs; } var cnt = bin.readUshort(data, offset); offset += 2; tab.lookupRec = Typr.GSUB.readSubstLookupRecords(data, offset, cnt); } } else if (ltype == 7 && tab.fmt == 1) { var extType = bin.readUshort(data, offset); offset += 2; var extOffset = bin.readUint(data, offset); offset += 4; if (ltable.ltype == 9) { ltable.ltype = extType; } else if (ltable.ltype != extType) { throw "invalid extension substitution"; } return Typr.GSUB.subt(data, ltable.ltype, offset0 + extOffset); } else console.warn("unsupported GSUB table LookupType", ltype, "format", tab.fmt); return tab; }; Typr.GSUB.readSubClassSet = function(data, offset) { var rUs = Typr._bin.readUshort, offset0 = offset, lset = []; var cnt = rUs(data, offset); offset += 2; for (var i = 0; i < cnt; i++) { var loff = rUs(data, offset); offset += 2; lset.push(Typr.GSUB.readSubClassRule(data, offset0 + loff)); } return lset; }; Typr.GSUB.readSubClassRule = function(data, offset) { var rUs = Typr._bin.readUshort, rule = {}; var gcount = rUs(data, offset); offset += 2; var scount = rUs(data, offset); offset += 2; rule.input = []; for (var i = 0; i < gcount - 1; i++) { rule.input.push(rUs(data, offset)); offset += 2; } rule.substLookupRecords = Typr.GSUB.readSubstLookupRecords(data, offset, scount); return rule; }; Typr.GSUB.readSubstLookupRecords = function(data, offset, cnt) { var rUs = Typr._bin.readUshort; var out = []; for (var i = 0; i < cnt; i++) { out.push(rUs(data, offset), rUs(data, offset + 2)); offset += 4; } return out; }; Typr.GSUB.readChainSubClassSet = function(data, offset) { var bin = Typr._bin, offset0 = offset, lset = []; var cnt = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < cnt; i++) { var loff = bin.readUshort(data, offset); offset += 2; lset.push(Typr.GSUB.readChainSubClassRule(data, offset0 + loff)); } return lset; }; Typr.GSUB.readChainSubClassRule = function(data, offset) { var bin = Typr._bin, rule = {}; var pps = ["backtrack", "input", "lookahead"]; for (var pi = 0; pi < pps.length; pi++) { var cnt = bin.readUshort(data, offset); offset += 2; if (pi == 1) cnt--; rule[pps[pi]] = bin.readUshorts(data, offset, cnt); offset += rule[pps[pi]].length * 2; } var cnt = bin.readUshort(data, offset); offset += 2; rule.subst = bin.readUshorts(data, offset, cnt * 2); offset += rule.subst.length * 2; return rule; }; Typr.GSUB.readLigatureSet = function(data, offset) { var bin = Typr._bin, offset0 = offset, lset = []; var lcnt = bin.readUshort(data, offset); offset += 2; for (var j = 0; j < lcnt; j++) { var loff = bin.readUshort(data, offset); offset += 2; lset.push(Typr.GSUB.readLigature(data, offset0 + loff)); } return lset; }; Typr.GSUB.readLigature = function(data, offset) { var bin = Typr._bin, lig = { chain: [] }; lig.nglyph = bin.readUshort(data, offset); offset += 2; var ccnt = bin.readUshort(data, offset); offset += 2; for (var k = 0; k < ccnt - 1; k++) { lig.chain.push(bin.readUshort(data, offset)); offset += 2; } return lig; }; Typr.head = {}; Typr.head.parse = function(data, offset, length) { var bin = Typr._bin; var obj = {}; bin.readFixed(data, offset); offset += 4; obj.fontRevision = bin.readFixed(data, offset); offset += 4; bin.readUint(data, offset); offset += 4; bin.readUint(data, offset); offset += 4; obj.flags = bin.readUshort(data, offset); offset += 2; obj.unitsPerEm = bin.readUshort(data, offset); offset += 2; obj.created = bin.readUint64(data, offset); offset += 8; obj.modified = bin.readUint64(data, offset); offset += 8; obj.xMin = bin.readShort(data, offset); offset += 2; obj.yMin = bin.readShort(data, offset); offset += 2; obj.xMax = bin.readShort(data, offset); offset += 2; obj.yMax = bin.readShort(data, offset); offset += 2; obj.macStyle = bin.readUshort(data, offset); offset += 2; obj.lowestRecPPEM = bin.readUshort(data, offset); offset += 2; obj.fontDirectionHint = bin.readShort(data, offset); offset += 2; obj.indexToLocFormat = bin.readShort(data, offset); offset += 2; obj.glyphDataFormat = bin.readShort(data, offset); offset += 2; return obj; }; Typr.hhea = {}; Typr.hhea.parse = function(data, offset, length) { var bin = Typr._bin; var obj = {}; bin.readFixed(data, offset); offset += 4; obj.ascender = bin.readShort(data, offset); offset += 2; obj.descender = bin.readShort(data, offset); offset += 2; obj.lineGap = bin.readShort(data, offset); offset += 2; obj.advanceWidthMax = bin.readUshort(data, offset); offset += 2; obj.minLeftSideBearing = bin.readShort(data, offset); offset += 2; obj.minRightSideBearing = bin.readShort(data, offset); offset += 2; obj.xMaxExtent = bin.readShort(data, offset); offset += 2; obj.caretSlopeRise = bin.readShort(data, offset); offset += 2; obj.caretSlopeRun = bin.readShort(data, offset); offset += 2; obj.caretOffset = bin.readShort(data, offset); offset += 2; offset += 4 * 2; obj.metricDataFormat = bin.readShort(data, offset); offset += 2; obj.numberOfHMetrics = bin.readUshort(data, offset); offset += 2; return obj; }; Typr.hmtx = {}; Typr.hmtx.parse = function(data, offset, length, font) { var bin = Typr._bin; var obj = {}; obj.aWidth = []; obj.lsBearing = []; var aw = 0, lsb = 0; for (var i = 0; i < font.maxp.numGlyphs; i++) { if (i < font.hhea.numberOfHMetrics) { aw = bin.readUshort(data, offset); offset += 2; lsb = bin.readShort(data, offset); offset += 2; } obj.aWidth.push(aw); obj.lsBearing.push(lsb); } return obj; }; Typr.kern = {}; Typr.kern.parse = function(data, offset, length, font) { var bin = Typr._bin; var version = bin.readUshort(data, offset); offset += 2; if (version == 1) return Typr.kern.parseV1(data, offset - 2, length, font); var nTables = bin.readUshort(data, offset); offset += 2; var map2 = { glyph1: [], rval: [] }; for (var i = 0; i < nTables; i++) { offset += 2; var length = bin.readUshort(data, offset); offset += 2; var coverage = bin.readUshort(data, offset); offset += 2; var format = coverage >>> 8; format &= 15; if (format == 0) offset = Typr.kern.readFormat0(data, offset, map2); else throw "unknown kern table format: " + format; } return map2; }; Typr.kern.parseV1 = function(data, offset, length, font) { var bin = Typr._bin; bin.readFixed(data, offset); offset += 4; var nTables = bin.readUint(data, offset); offset += 4; var map2 = { glyph1: [], rval: [] }; for (var i = 0; i < nTables; i++) { bin.readUint(data, offset); offset += 4; var coverage = bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; var format = coverage >>> 8; format &= 15; if (format == 0) offset = Typr.kern.readFormat0(data, offset, map2); else throw "unknown kern table format: " + format; } return map2; }; Typr.kern.readFormat0 = function(data, offset, map2) { var bin = Typr._bin; var pleft = -1; var nPairs = bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; for (var j = 0; j < nPairs; j++) { var left = bin.readUshort(data, offset); offset += 2; var right = bin.readUshort(data, offset); offset += 2; var value = bin.readShort(data, offset); offset += 2; if (left != pleft) { map2.glyph1.push(left); map2.rval.push({ glyph2: [], vals: [] }); } var rval = map2.rval[map2.rval.length - 1]; rval.glyph2.push(right); rval.vals.push(value); pleft = left; } return offset; }; Typr.loca = {}; Typr.loca.parse = function(data, offset, length, font) { var bin = Typr._bin; var obj = []; var ver = font.head.indexToLocFormat; var len = font.maxp.numGlyphs + 1; if (ver == 0) for (var i = 0; i < len; i++) obj.push(bin.readUshort(data, offset + (i << 1)) << 1); if (ver == 1) for (var i = 0; i < len; i++) obj.push(bin.readUint(data, offset + (i << 2))); return obj; }; Typr.maxp = {}; Typr.maxp.parse = function(data, offset, length) { var bin = Typr._bin; var obj = {}; var ver = bin.readUint(data, offset); offset += 4; obj.numGlyphs = bin.readUshort(data, offset); offset += 2; if (ver == 65536) { obj.maxPoints = bin.readUshort(data, offset); offset += 2; obj.maxContours = bin.readUshort(data, offset); offset += 2; obj.maxCompositePoints = bin.readUshort(data, offset); offset += 2; obj.maxCompositeContours = bin.readUshort(data, offset); offset += 2; obj.maxZones = bin.readUshort(data, offset); offset += 2; obj.maxTwilightPoints = bin.readUshort(data, offset); offset += 2; obj.maxStorage = bin.readUshort(data, offset); offset += 2; obj.maxFunctionDefs = bin.readUshort(data, offset); offset += 2; obj.maxInstructionDefs = bin.readUshort(data, offset); offset += 2; obj.maxStackElements = bin.readUshort(data, offset); offset += 2; obj.maxSizeOfInstructions = bin.readUshort(data, offset); offset += 2; obj.maxComponentElements = bin.readUshort(data, offset); offset += 2; obj.maxComponentDepth = bin.readUshort(data, offset); offset += 2; } return obj; }; Typr.name = {}; Typr.name.parse = function(data, offset, length) { var bin = Typr._bin; var obj = {}; bin.readUshort(data, offset); offset += 2; var count = bin.readUshort(data, offset); offset += 2; bin.readUshort(data, offset); offset += 2; var names = [ "copyright", "fontFamily", "fontSubfamily", "ID", "fullName", "version", "postScriptName", "trademark", "manufacturer", "designer", "description", "urlVendor", "urlDesigner", "licence", "licenceURL", "---", "typoFamilyName", "typoSubfamilyName", "compatibleFull", "sampleText", "postScriptCID", "wwsFamilyName", "wwsSubfamilyName", "lightPalette", "darkPalette" ]; var offset0 = offset; for (var i = 0; i < count; i++) { var platformID = bin.readUshort(data, offset); offset += 2; var encodingID = bin.readUshort(data, offset); offset += 2; var languageID = bin.readUshort(data, offset); offset += 2; var nameID = bin.readUshort(data, offset); offset += 2; var slen = bin.readUshort(data, offset); offset += 2; var noffset = bin.readUshort(data, offset); offset += 2; var cname = names[nameID]; var soff = offset0 + count * 12 + noffset; var str; if (platformID == 0) str = bin.readUnicode(data, soff, slen / 2); else if (platformID == 3 && encodingID == 0) str = bin.readUnicode(data, soff, slen / 2); else if (encodingID == 0) str = bin.readASCII(data, soff, slen); else if (encodingID == 1) str = bin.readUnicode(data, soff, slen / 2); else if (encodingID == 3) str = bin.readUnicode(data, soff, slen / 2); else if (platformID == 1) { str = bin.readASCII(data, soff, slen); console.warn("reading unknown MAC encoding " + encodingID + " as ASCII"); } else throw "unknown encoding " + encodingID + ", platformID: " + platformID; var tid = "p" + platformID + "," + languageID.toString(16); if (obj[tid] == null) obj[tid] = {}; obj[tid][cname !== void 0 ? cname : nameID] = str; obj[tid]._lang = languageID; } for (var p in obj) if (obj[p].postScriptName != null && obj[p]._lang == 1033) return obj[p]; for (var p in obj) if (obj[p].postScriptName != null && obj[p]._lang == 0) return obj[p]; for (var p in obj) if (obj[p].postScriptName != null && obj[p]._lang == 3084) return obj[p]; for (var p in obj) if (obj[p].postScriptName != null) return obj[p]; var tname; for (var p in obj) { tname = p; break; } console.warn("returning name table with languageID " + obj[tname]._lang); return obj[tname]; }; Typr["OS/2"] = {}; Typr["OS/2"].parse = function(data, offset, length) { var bin = Typr._bin; var ver = bin.readUshort(data, offset); offset += 2; var obj = {}; if (ver == 0) Typr["OS/2"].version0(data, offset, obj); else if (ver == 1) Typr["OS/2"].version1(data, offset, obj); else if (ver == 2 || ver == 3 || ver == 4) Typr["OS/2"].version2(data, offset, obj); else if (ver == 5) Typr["OS/2"].version5(data, offset, obj); else throw "unknown OS/2 table version: " + ver; return obj; }; Typr["OS/2"].version0 = function(data, offset, obj) { var bin = Typr._bin; obj.xAvgCharWidth = bin.readShort(data, offset); offset += 2; obj.usWeightClass = bin.readUshort(data, offset); offset += 2; obj.usWidthClass = bin.readUshort(data, offset); offset += 2; obj.fsType = bin.readUshort(data, offset); offset += 2; obj.ySubscriptXSize = bin.readShort(data, offset); offset += 2; obj.ySubscriptYSize = bin.readShort(data, offset); offset += 2; obj.ySubscriptXOffset = bin.readShort(data, offset); offset += 2; obj.ySubscriptYOffset = bin.readShort(data, offset); offset += 2; obj.ySuperscriptXSize = bin.readShort(data, offset); offset += 2; obj.ySuperscriptYSize = bin.readShort(data, offset); offset += 2; obj.ySuperscriptXOffset = bin.readShort(data, offset); offset += 2; obj.ySuperscriptYOffset = bin.readShort(data, offset); offset += 2; obj.yStrikeoutSize = bin.readShort(data, offset); offset += 2; obj.yStrikeoutPosition = bin.readShort(data, offset); offset += 2; obj.sFamilyClass = bin.readShort(data, offset); offset += 2; obj.panose = bin.readBytes(data, offset, 10); offset += 10; obj.ulUnicodeRange1 = bin.readUint(data, offset); offset += 4; obj.ulUnicodeRange2 = bin.readUint(data, offset); offset += 4; obj.ulUnicodeRange3 = bin.readUint(data, offset); offset += 4; obj.ulUnicodeRange4 = bin.readUint(data, offset); offset += 4; obj.achVendID = [bin.readInt8(data, offset), bin.readInt8(data, offset + 1), bin.readInt8(data, offset + 2), bin.readInt8(data, offset + 3)]; offset += 4; obj.fsSelection = bin.readUshort(data, offset); offset += 2; obj.usFirstCharIndex = bin.readUshort(data, offset); offset += 2; obj.usLastCharIndex = bin.readUshort(data, offset); offset += 2; obj.sTypoAscender = bin.readShort(data, offset); offset += 2; obj.sTypoDescender = bin.readShort(data, offset); offset += 2; obj.sTypoLineGap = bin.readShort(data, offset); offset += 2; obj.usWinAscent = bin.readUshort(data, offset); offset += 2; obj.usWinDescent = bin.readUshort(data, offset); offset += 2; return offset; }; Typr["OS/2"].version1 = function(data, offset, obj) { var bin = Typr._bin; offset = Typr["OS/2"].version0(data, offset, obj); obj.ulCodePageRange1 = bin.readUint(data, offset); offset += 4; obj.ulCodePageRange2 = bin.readUint(data, offset); offset += 4; return offset; }; Typr["OS/2"].version2 = function(data, offset, obj) { var bin = Typr._bin; offset = Typr["OS/2"].version1(data, offset, obj); obj.sxHeight = bin.readShort(data, offset); offset += 2; obj.sCapHeight = bin.readShort(data, offset); offset += 2; obj.usDefault = bin.readUshort(data, offset); offset += 2; obj.usBreak = bin.readUshort(data, offset); offset += 2; obj.usMaxContext = bin.readUshort(data, offset); offset += 2; return offset; }; Typr["OS/2"].version5 = function(data, offset, obj) { var bin = Typr._bin; offset = Typr["OS/2"].version2(data, offset, obj); obj.usLowerOpticalPointSize = bin.readUshort(data, offset); offset += 2; obj.usUpperOpticalPointSize = bin.readUshort(data, offset); offset += 2; return offset; }; Typr.post = {}; Typr.post.parse = function(data, offset, length) { var bin = Typr._bin; var obj = {}; obj.version = bin.readFixed(data, offset); offset += 4; obj.italicAngle = bin.readFixed(data, offset); offset += 4; obj.underlinePosition = bin.readShort(data, offset); offset += 2; obj.underlineThickness = bin.readShort(data, offset); offset += 2; return obj; }; Typr.SVG = {}; Typr.SVG.parse = function(data, offset, length) { var bin = Typr._bin; var obj = { entries: [] }; var offset0 = offset; bin.readUshort(data, offset); offset += 2; var svgDocIndexOffset = bin.readUint(data, offset); offset += 4; bin.readUint(data, offset); offset += 4; offset = svgDocIndexOffset + offset0; var numEntries = bin.readUshort(data, offset); offset += 2; for (var i = 0; i < numEntries; i++) { var startGlyphID = bin.readUshort(data, offset); offset += 2; var endGlyphID = bin.readUshort(data, offset); offset += 2; var svgDocOffset = bin.readUint(data, offset); offset += 4; var svgDocLength = bin.readUint(data, offset); offset += 4; var sbuf = new Uint8Array(data.buffer, offset0 + svgDocOffset + svgDocIndexOffset, svgDocLength); var svg = bin.readUTF8(sbuf, 0, sbuf.length); for (var f = startGlyphID; f <= endGlyphID; f++) { obj.entries[f] = svg; } } return obj; }; Typr.SVG.toPath = function(str) { var pth = { cmds: [], crds: [] }; if (str == null) return pth; var prsr = new DOMParser(); var doc = prsr["parseFromString"](str, "image/svg+xml"); var svg = doc.firstChild; while (svg.tagName != "svg") svg = svg.nextSibling; var vb = svg.getAttribute("viewBox"); if (vb) vb = vb.trim().split(" ").map(parseFloat); else vb = [0, 0, 1e3, 1e3]; Typr.SVG._toPath(svg.children, pth); for (var i = 0; i < pth.crds.length; i += 2) { var x = pth.crds[i], y = pth.crds[i + 1]; x -= vb[0]; y -= vb[1]; y = -y; pth.crds[i] = x; pth.crds[i + 1] = y; } return pth; }; Typr.SVG._toPath = function(nds, pth, fill) { for (var ni = 0; ni < nds.length; ni++) { var nd = nds[ni], tn = nd.tagName; var cfl = nd.getAttribute("fill"); if (cfl == null) cfl = fill; if (tn == "g") Typr.SVG._toPath(nd.children, pth, cfl); else if (tn == "path") { pth.cmds.push(cfl ? cfl : "#000000"); var d = nd.getAttribute("d"); var toks = Typr.SVG._tokens(d); Typr.SVG._toksToPath(toks, pth); pth.cmds.push("X"); } else if (tn == "defs") ; else console.warn(tn, nd); } }; Typr.SVG._tokens = function(d) { var ts = [], off = 0, rn = false, cn = ""; while (off < d.length) { var cc = d.charCodeAt(off), ch = d.charAt(off); off++; var isNum = 48 <= cc && cc <= 57 || ch == "." || ch == "-"; if (rn) { if (ch == "-") { ts.push(parseFloat(cn)); cn = ch; } else if (isNum) cn += ch; else { ts.push(parseFloat(cn)); if (ch != "," && ch != " ") ts.push(ch); rn = false; } } else { if (isNum) { cn = ch; rn = true; } else if (ch != "," && ch != " ") ts.push(ch); } } if (rn) ts.push(parseFloat(cn)); return ts; }; Typr.SVG._toksToPath = function(ts, pth) { var i = 0, x = 0, y = 0, ox = 0, oy = 0; var pc = { "M": 2, "L": 2, "H": 1, "V": 1, "S": 4, "C": 6 }; var cmds = pth.cmds, crds = pth.crds; while (i < ts.length) { var cmd = ts[i]; i++; if (cmd == "z") { cmds.push("Z"); x = ox; y = oy; } else { var cmu = cmd.toUpperCase(); var ps = pc[cmu], reps = Typr.SVG._reps(ts, i, ps); for (var j = 0; j < reps; j++) { var xi = 0, yi = 0; if (cmd != cmu) { xi = x; yi = y; } if (cmu == "M") { x = xi + ts[i++]; y = yi + ts[i++]; cmds.push("M"); crds.push(x, y); ox = x; oy = y; } else if (cmu == "L") { x = xi + ts[i++]; y = yi + ts[i++]; cmds.push("L"); crds.push(x, y); } else if (cmu == "H") { x = xi + ts[i++]; cmds.push("L"); crds.push(x, y); } else if (cmu == "V") { y = yi + ts[i++]; cmds.push("L"); crds.push(x, y); } else if (cmu == "C") { var x1 = xi + ts[i++], y1 = yi + ts[i++], x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++]; cmds.push("C"); crds.push(x1, y1, x2, y2, x3, y3); x = x3; y = y3; } else if (cmu == "S") { var co = Math.max(crds.length - 4, 0); var x1 = x + x - crds[co], y1 = y + y - crds[co + 1]; var x2 = xi + ts[i++], y2 = yi + ts[i++], x3 = xi + ts[i++], y3 = yi + ts[i++]; cmds.push("C"); crds.push(x1, y1, x2, y2, x3, y3); x = x3; y = y3; } else console.warn("Unknown SVG command " + cmd); } } } }; Typr.SVG._reps = function(ts, off, ps) { var i = off; while (i < ts.length) { if (typeof ts[i] == "string") break; i += ps; } return (i - off) / ps; }; if (Typr == null) Typr = {}; if (Typr.U == null) Typr.U = {}; Typr.U.codeToGlyph = function(font, code) { var cmap = font.cmap; for (var _i = 0, _a = [cmap.p0e4, cmap.p3e1, cmap.p3e10, cmap.p0e3, cmap.p1e0]; _i < _a.length; _i++) { var tind = _a[_i]; if (tind == null) continue; var tab = cmap.tables[tind]; if (tab.format == 0) { if (code >= tab.map.length) continue; return tab.map[code]; } else if (tab.format == 4) { var sind = -1; for (var i = 0; i < tab.endCount.length; i++) { if (code <= tab.endCount[i]) { sind = i; break; } } if (sind == -1) continue; if (tab.startCount[sind] > code) continue; var gli = 0; if (tab.idRangeOffset[sind] != 0) { gli = tab.glyphIdArray[code - tab.startCount[sind] + (tab.idRangeOffset[sind] >> 1) - (tab.idRangeOffset.length - sind)]; } else { gli = code + tab.idDelta[sind]; } return gli & 65535; } else if (tab.format == 12) { if (code > tab.groups[tab.groups.length - 1][1]) continue; for (var i = 0; i < tab.groups.length; i++) { var grp = tab.groups[i]; if (grp[0] <= code && code <= grp[1]) return grp[2] + (code - grp[0]); } continue; } else { throw "unknown cmap table format " + tab.format; } } return 0; }; Typr.U.glyphToPath = function(font, gid) { var path = { cmds: [], crds: [] }; if (font.SVG && font.SVG.entries[gid]) { var p = font.SVG.entries[gid]; if (p == null) return path; if (typeof p == "string") { p = Typr.SVG.toPath(p); font.SVG.entries[gid] = p; } return p; } else if (font.CFF) { var state = { x: 0, y: 0, stack: [], nStems: 0, haveWidth: false, width: font.CFF.Private ? font.CFF.Private.defaultWidthX : 0, open: false }; var cff = font.CFF, pdct = font.CFF.Private; if (cff.ROS) { var gi = 0; while (cff.FDSelect[gi + 2] <= gid) gi += 2; pdct = cff.FDArray[cff.FDSelect[gi + 1]].Private; } Typr.U._drawCFF(font.CFF.CharStrings[gid], state, cff, pdct, path); } else if (font.glyf) { Typr.U._drawGlyf(gid, font, path); } return path; }; Typr.U._drawGlyf = function(gid, font, path) { var gl = font.glyf[gid]; if (gl == null) gl = font.glyf[gid] = Typr.glyf._parseGlyf(font, gid); if (gl != null) { if (gl.noc > -1) { Typr.U._simpleGlyph(gl, path); } else { Typr.U._compoGlyph(gl, font, path); } } }; Typr.U._simpleGlyph = function(gl, p) { for (var c = 0; c < gl.noc; c++) { var i0 = c == 0 ? 0 : gl.endPts[c - 1] + 1; var il = gl.endPts[c]; for (var i = i0; i <= il; i++) { var pr = i == i0 ? il : i - 1; var nx = i == il ? i0 : i + 1; var onCurve = gl.flags[i] & 1; var prOnCurve = gl.flags[pr] & 1; var nxOnCurve = gl.flags[nx] & 1; var x = gl.xs[i], y = gl.ys[i]; if (i == i0) { if (onCurve) { if (prOnCurve) { Typr.U.P.moveTo(p, gl.xs[pr], gl.ys[pr]); } else { Typr.U.P.moveTo(p, x, y); continue; } } else { if (prOnCurve) { Typr.U.P.moveTo(p, gl.xs[pr], gl.ys[pr]); } else { Typr.U.P.moveTo(p, (gl.xs[pr] + x) / 2, (gl.ys[pr] + y) / 2); } } } if (onCurve) { if (prOnCurve) Typr.U.P.lineTo(p, x, y); } else { if (nxOnCurve) { Typr.U.P.qcurveTo(p, x, y, gl.xs[nx], gl.ys[nx]); } else { Typr.U.P.qcurveTo(p, x, y, (x + gl.xs[nx]) / 2, (y + gl.ys[nx]) / 2); } } } Typr.U.P.closePath(p); } }; Typr.U._compoGlyph = function(gl, font, p) { for (var j = 0; j < gl.parts.length; j++) { var path = { cmds: [], crds: [] }; var prt = gl.parts[j]; Typr.U._drawGlyf(prt.glyphIndex, font, path); var m = prt.m; for (var i = 0; i < path.crds.length; i += 2) { var x = path.crds[i], y = path.crds[i + 1]; p.crds.push(x * m.a + y * m.b + m.tx); p.crds.push(x * m.c + y * m.d + m.ty); } for (var i = 0; i < path.cmds.length; i++) { p.cmds.push(path.cmds[i]); } } }; Typr.U._getGlyphClass = function(g, cd) { var intr = Typr._lctf.getInterval(cd, g); return intr == -1 ? 0 : cd[intr + 2]; }; Typr.U.getPairAdjustment = function(font, g1, g2) { var hasGPOSkern = false; if (font.GPOS) { var gpos = font["GPOS"]; var llist = gpos.lookupList, flist = gpos.featureList; var tused = []; for (var i = 0; i < flist.length; i++) { var fl = flist[i]; if (fl.tag != "kern") continue; hasGPOSkern = true; for (var ti = 0; ti < fl.tab.length; ti++) { if (tused[fl.tab[ti]]) continue; tused[fl.tab[ti]] = true; var tab = llist[fl.tab[ti]]; for (var j = 0; j < tab.tabs.length; j++) { if (tab.tabs[j] == null) continue; var ltab = tab.tabs[j], ind; if (ltab.coverage) { ind = Typr._lctf.coverageIndex(ltab.coverage, g1); if (ind == -1) continue; } if (tab.ltype == 1) ; else if (tab.ltype == 2) { var adj = null; if (ltab.fmt == 1) { var right = ltab.pairsets[ind]; for (var i = 0; i < right.length; i++) { if (right[i].gid2 == g2) adj = right[i]; } } else if (ltab.fmt == 2) { var c1 = Typr.U._getGlyphClass(g1, ltab.classDef1); var c2 = Typr.U._getGlyphClass(g2, ltab.classDef2); adj = ltab.matrix[c1][c2]; } if (adj) { var offset = 0; if (adj.val1 && adj.val1[2]) offset += adj.val1[2]; if (adj.val2 && adj.val2[0]) offset += adj.val2[0]; return offset; } } } } } } if (font.kern && !hasGPOSkern) { var ind1 = font.kern.glyph1.indexOf(g1); if (ind1 != -1) { var ind2 = font.kern.rval[ind1].glyph2.indexOf(g2); if (ind2 != -1) return font.kern.rval[ind1].vals[ind2]; } } return 0; }; Typr.U.stringToGlyphs = function(font, str) { var gls = []; for (var i = 0; i < str.length; i++) { var cc = str.codePointAt(i); if (cc > 65535) i++; gls.push(Typr.U.codeToGlyph(font, cc)); } for (var i = 0; i < str.length; i++) { var cc = str.codePointAt(i); if (cc == 2367) { var t = gls[i - 1]; gls[i - 1] = gls[i]; gls[i] = t; } if (cc > 65535) i++; } var gsub = font["GSUB"]; if (gsub == null) return gls; var llist = gsub.lookupList, flist = gsub.featureList; var cligs = [ "rlig", "liga", "mset", "isol", "init", "fina", "medi", "half", "pres", "blws" ]; var tused = []; for (var fi = 0; fi < flist.length; fi++) { var fl = flist[fi]; if (cligs.indexOf(fl.tag) == -1) continue; for (var ti = 0; ti < fl.tab.length; ti++) { if (tused[fl.tab[ti]]) continue; tused[fl.tab[ti]] = true; var tab = llist[fl.tab[ti]]; for (var ci = 0; ci < gls.length; ci++) { var feat = Typr.U._getWPfeature(str, ci); if ("isol,init,fina,medi".indexOf(fl.tag) != -1 && fl.tag != feat) continue; Typr.U._applySubs(gls, ci, tab, llist); } } } return gls; }; Typr.U._getWPfeature = function(str, ci) { var wsep = '\n " ,.:;!?() ،'; var R = "آأؤإاةدذرزوٱٲٳٵٶٷڈډڊڋڌڍڎڏڐڑڒړڔڕږڗژڙۀۃۄۅۆۇۈۉۊۋۍۏےۓەۮۯܐܕܖܗܘܙܞܨܪܬܯݍݙݚݛݫݬݱݳݴݸݹࡀࡆࡇࡉࡔࡧࡩࡪࢪࢫࢬࢮࢱࢲࢹૅેૉ૊૎૏ૐ૑૒૝ૡ૤૯஁ஃ஄அஉ஌எஏ஑னப஫஬"; var L = "ꡲ્૗"; var slft = ci == 0 || wsep.indexOf(str[ci - 1]) != -1; var srgt = ci == str.length - 1 || wsep.indexOf(str[ci + 1]) != -1; if (!slft && R.indexOf(str[ci - 1]) != -1) slft = true; if (!srgt && R.indexOf(str[ci]) != -1) srgt = true; if (!srgt && L.indexOf(str[ci + 1]) != -1) srgt = true; if (!slft && L.indexOf(str[ci]) != -1) slft = true; var feat = null; if (slft) { feat = srgt ? "isol" : "init"; } else { feat = srgt ? "fina" : "medi"; } return feat; }; Typr.U._applySubs = function(gls, ci, tab, llist) { var rlim = gls.length - ci - 1; for (var j = 0; j < tab.tabs.length; j++) { if (tab.tabs[j] == null) continue; var ltab = tab.tabs[j], ind; if (ltab.coverage) { ind = Typr._lctf.coverageIndex(ltab.coverage, gls[ci]); if (ind == -1) continue; } if (tab.ltype == 1) { gls[ci]; if (ltab.fmt == 1) gls[ci] = gls[ci] + ltab.delta; else gls[ci] = ltab.newg[ind]; } else if (tab.ltype == 4) { var vals = ltab.vals[ind]; for (var k = 0; k < vals.length; k++) { var lig = vals[k], rl = lig.chain.length; if (rl > rlim) continue; var good = true, em1 = 0; for (var l = 0; l < rl; l++) { while (gls[ci + em1 + (1 + l)] == -1) em1++; if (lig.chain[l] != gls[ci + em1 + (1 + l)]) good = false; } if (!good) continue; gls[ci] = lig.nglyph; for (var l = 0; l < rl + em1; l++) gls[ci + l + 1] = -1; break; } } else if (tab.ltype == 5 && ltab.fmt == 2) { var cind = Typr._lctf.getInterval(ltab.cDef, gls[ci]); var cls = ltab.cDef[cind + 2], scs = ltab.scset[cls]; for (var i = 0; i < scs.length; i++) { var sc = scs[i], inp = sc.input; if (inp.length > rlim) continue; var good = true; for (var l = 0; l < inp.length; l++) { var cind2 = Typr._lctf.getInterval(ltab.cDef, gls[ci + 1 + l]); if (cind == -1 && ltab.cDef[cind2 + 2] != inp[l]) { good = false; break; } } if (!good) continue; var lrs = sc.substLookupRecords; for (var k = 0; k < lrs.length; k += 2) { lrs[k]; lrs[k + 1]; } } } else if (tab.ltype == 6 && ltab.fmt == 3) { if (!Typr.U._glsCovered(gls, ltab.backCvg, ci - ltab.backCvg.length)) continue; if (!Typr.U._glsCovered(gls, ltab.inptCvg, ci)) continue; if (!Typr.U._glsCovered(gls, ltab.ahedCvg, ci + ltab.inptCvg.length)) continue; var lr = ltab.lookupRec; for (var i = 0; i < lr.length; i += 2) { var cind = lr[i], tab2 = llist[lr[i + 1]]; Typr.U._applySubs(gls, ci + cind, tab2, llist); } } } }; Typr.U._glsCovered = function(gls, cvgs, ci) { for (var i = 0; i < cvgs.length; i++) { var ind = Typr._lctf.coverageIndex(cvgs[i], gls[ci + i]); if (ind == -1) return false; } return true; }; Typr.U.glyphsToPath = function(font, gls, clr) { var tpath = { cmds: [], crds: [] }; var x = 0; for (var i = 0; i < gls.length; i++) { var gid = gls[i]; if (gid == -1) continue; var gid2 = i < gls.length - 1 && gls[i + 1] != -1 ? gls[i + 1] : 0; var path = Typr.U.glyphToPath(font, gid); for (var j = 0; j < path.crds.length; j += 2) { tpath.crds.push(path.crds[j] + x); tpath.crds.push(path.crds[j + 1]); } if (clr) tpath.cmds.push(clr); for (var j = 0; j < path.cmds.length; j++) tpath.cmds.push(path.cmds[j]); if (clr) tpath.cmds.push("X"); x += font.hmtx.aWidth[gid]; if (i < gls.length - 1) x += Typr.U.getPairAdjustment(font, gid, gid2); } return tpath; }; Typr.U.pathToSVG = function(path, prec) { if (prec == null) prec = 5; var out = [], co = 0, lmap = { "M": 2, "L": 2, "Q": 4, "C": 6 }; for (var i = 0; i < path.cmds.length; i++) { var cmd = path.cmds[i], cn = co + (lmap[cmd] ? lmap[cmd] : 0); out.push(cmd); while (co < cn) { var c = path.crds[co++]; out.push(parseFloat(c.toFixed(prec)) + (co == cn ? "" : " ")); } } return out.join(""); }; Typr.U.pathToContext = function(path, ctx) { var c = 0, crds = path.crds; for (var j = 0; j < path.cmds.length; j++) { var cmd = path.cmds[j]; if (cmd == "M") { ctx.moveTo(crds[c], crds[c + 1]); c += 2; } else if (cmd == "L") { ctx.lineTo(crds[c], crds[c + 1]); c += 2; } else if (cmd == "C") { ctx.bezierCurveTo(crds[c], crds[c + 1], crds[c + 2], crds[c + 3], crds[c + 4], crds[c + 5]); c += 6; } else if (cmd == "Q") { ctx.quadraticCurveTo(crds[c], crds[c + 1], crds[c + 2], crds[c + 3]); c += 4; } else if (cmd.charAt(0) == "#") { ctx.beginPath(); ctx.fillStyle = cmd; } else if (cmd == "Z") { ctx.closePath(); } else if (cmd == "X") { ctx.fill(); } } }; Typr.U.P = {}; Typr.U.P.moveTo = function(p, x, y) { p.cmds.push("M"); p.crds.push(x, y); }; Typr.U.P.lineTo = function(p, x, y) { p.cmds.push("L"); p.crds.push(x, y); }; Typr.U.P.curveTo = function(p, a, b, c, d, e, f) { p.cmds.push("C"); p.crds.push(a, b, c, d, e, f); }; Typr.U.P.qcurveTo = function(p, a, b, c, d) { p.cmds.push("Q"); p.crds.push(a, b, c, d); }; Typr.U.P.closePath = function(p) { p.cmds.push("Z"); }; Typr.U._drawCFF = function(cmds, state, font, pdct, p) { var stack = state.stack; var nStems = state.nStems, haveWidth = state.haveWidth, width = state.width, open = state.open; var i = 0; var x = state.x, y = state.y, c1x = 0, c1y = 0, c2x = 0, c2y = 0, c3x = 0, c3y = 0, c4x = 0, c4y = 0, jpx = 0, jpy = 0; var o = { val: 0, size: 0 }; while (i < cmds.length) { Typr.CFF.getCharString(cmds, i, o); var v = o.val; i += o.size; if (v == "o1" || v == "o18") { var hasWidthArg; hasWidthArg = stack.length % 2 !== 0; if (hasWidthArg && !haveWidth) { width = stack.shift() + pdct.nominalWidthX; } nStems += stack.length >> 1; stack.length = 0; haveWidth = true; } else if (v == "o3" || v == "o23") { var hasWidthArg; hasWidthArg = stack.length % 2 !== 0; if (hasWidthArg && !haveWidth) { width = stack.shift() + pdct.nominalWidthX; } nStems += stack.length >> 1; stack.length = 0; haveWidth = true; } else if (v == "o4") { if (stack.length > 1 && !haveWidth) { width = stack.shift() + pdct.nominalWidthX; haveWidth = true; } if (open) Typr.U.P.closePath(p); y += stack.pop(); Typr.U.P.moveTo(p, x, y); open = true; } else if (v == "o5") { while (stack.length > 0) { x += stack.shift(); y += stack.shift(); Typr.U.P.lineTo(p, x, y); } } else if (v == "o6" || v == "o7") { var count = stack.length; var isX = v == "o6"; for (var j = 0; j < count; j++) { var sval = stack.shift(); if (isX) { x += sval; } else { y += sval; } isX = !isX; Typr.U.P.lineTo(p, x, y); } } else if (v == "o8" || v == "o24") { var count = stack.length; var index = 0; while (index + 6 <= count) { c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y + stack.shift(); Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y); index += 6; } if (v == "o24") { x += stack.shift(); y += stack.shift(); Typr.U.P.lineTo(p, x, y); } } else if (v == "o11") { break; } else if (v == "o1234" || v == "o1235" || v == "o1236" || v == "o1237") { if (v == "o1234") { c1x = x + stack.shift(); c1y = y; c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); jpx = c2x + stack.shift(); jpy = c2y; c3x = jpx + stack.shift(); c3y = c2y; c4x = c3x + stack.shift(); c4y = y; x = c4x + stack.shift(); Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy); Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y); } if (v == "o1235") { c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); jpx = c2x + stack.shift(); jpy = c2y + stack.shift(); c3x = jpx + stack.shift(); c3y = jpy + stack.shift(); c4x = c3x + stack.shift(); c4y = c3y + stack.shift(); x = c4x + stack.shift(); y = c4y + stack.shift(); stack.shift(); Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy); Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y); } if (v == "o1236") { c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); jpx = c2x + stack.shift(); jpy = c2y; c3x = jpx + stack.shift(); c3y = c2y; c4x = c3x + stack.shift(); c4y = c3y + stack.shift(); x = c4x + stack.shift(); Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy); Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y); } if (v == "o1237") { c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); jpx = c2x + stack.shift(); jpy = c2y + stack.shift(); c3x = jpx + stack.shift(); c3y = jpy + stack.shift(); c4x = c3x + stack.shift(); c4y = c3y + stack.shift(); if (Math.abs(c4x - x) > Math.abs(c4y - y)) { x = c4x + stack.shift(); } else { y = c4y + stack.shift(); } Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, jpx, jpy); Typr.U.P.curveTo(p, c3x, c3y, c4x, c4y, x, y); } } else if (v == "o14") { if (stack.length > 0 && !haveWidth) { width = stack.shift() + font.nominalWidthX; haveWidth = true; } if (stack.length == 4) { var adx = stack.shift(); var ady = stack.shift(); var bchar = stack.shift(); var achar = stack.shift(); var bind2 = Typr.CFF.glyphBySE(font, bchar); var aind = Typr.CFF.glyphBySE(font, achar); Typr.U._drawCFF(font.CharStrings[bind2], state, font, pdct, p); state.x = adx; state.y = ady; Typr.U._drawCFF(font.CharStrings[aind], state, font, pdct, p); } if (open) { Typr.U.P.closePath(p); open = false; } } else if (v == "o19" || v == "o20") { var hasWidthArg; hasWidthArg = stack.length % 2 !== 0; if (hasWidthArg && !haveWidth) { width = stack.shift() + pdct.nominalWidthX; } nStems += stack.length >> 1; stack.length = 0; haveWidth = true; i += nStems + 7 >> 3; } else if (v == "o21") { if (stack.length > 2 && !haveWidth) { width = stack.shift() + pdct.nominalWidthX; haveWidth = true; } y += stack.pop(); x += stack.pop(); if (open) Typr.U.P.closePath(p); Typr.U.P.moveTo(p, x, y); open = true; } else if (v == "o22") { if (stack.length > 1 && !haveWidth) { width = stack.shift() + pdct.nominalWidthX; haveWidth = true; } x += stack.pop(); if (open) Typr.U.P.closePath(p); Typr.U.P.moveTo(p, x, y); open = true; } else if (v == "o25") { while (stack.length > 6) { x += stack.shift(); y += stack.shift(); Typr.U.P.lineTo(p, x, y); } c1x = x + stack.shift(); c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y + stack.shift(); Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y); } else if (v == "o26") { if (stack.length % 2) { x += stack.shift(); } while (stack.length > 0) { c1x = x; c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x; y = c2y + stack.shift(); Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y); } } else if (v == "o27") { if (stack.length % 2) { y += stack.shift(); } while (stack.length > 0) { c1x = x + stack.shift(); c1y = y; c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); y = c2y; Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y); } } else if (v == "o10" || v == "o29") { var obj = v == "o10" ? pdct : font; if (stack.length == 0) { console.warn("error: empty stack"); } else { var ind = stack.pop(); var subr = obj.Subrs[ind + obj.Bias]; state.x = x; state.y = y; state.nStems = nStems; state.haveWidth = haveWidth; state.width = width; state.open = open; Typr.U._drawCFF(subr, state, font, pdct, p); x = state.x; y = state.y; nStems = state.nStems; haveWidth = state.haveWidth; width = state.width; open = state.open; } } else if (v == "o30" || v == "o31") { var count, count1 = stack.length; var index = 0; var alternate = v == "o31"; count = count1 & ~2; index += count1 - count; while (index < count) { if (alternate) { c1x = x + stack.shift(); c1y = y; c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); y = c2y + stack.shift(); if (count - index == 5) { x = c2x + stack.shift(); index++; } else { x = c2x; } alternate = false; } else { c1x = x; c1y = y + stack.shift(); c2x = c1x + stack.shift(); c2y = c1y + stack.shift(); x = c2x + stack.shift(); if (count - index == 5) { y = c2y + stack.shift(); index++; } else { y = c2y; } alternate = true; } Typr.U.P.curveTo(p, c1x, c1y, c2x, c2y, x, y); index += 4; } } else if ((v + "").charAt(0) == "o") { console.warn("Unknown operation: " + v, cmds); throw v; } else stack.push(v); } state.x = x; state.y = y; state.nStems = nStems; state.haveWidth = haveWidth; state.width = width; state.open = open; }; Typr$1.Typr = Typr; var Typr_js_1 = Typr$1; var friendlyTags = { "aalt": "Access All Alternates", "abvf": "Above-base Forms", "abvm": "Above - base Mark Positioning", "abvs": "Above - base Substitutions", "afrc": "Alternative Fractions", "akhn": "Akhands", "blwf": "Below - base Forms", "blwm": "Below - base Mark Positioning", "blws": "Below - base Substitutions", "calt": "Contextual Alternates", "case": "Case - Sensitive Forms", "ccmp": "Glyph Composition / Decomposition", "cfar": "Conjunct Form After Ro", "cjct": "Conjunct Forms", "clig": "Contextual Ligatures", "cpct": "Centered CJK Punctuation", "cpsp": "Capital Spacing", "cswh": "Contextual Swash", "curs": "Cursive Positioning", "c2pc": "Petite Capitals From Capitals", "c2sc": "Small Capitals From Capitals", "dist": "Distances", "dlig": "Discretionary Ligatures", "dnom": "Denominators", "dtls": "Dotless Forms", "expt": "Expert Forms", "falt": "Final Glyph on Line Alternates", "fin2": "Terminal Forms #2", "fin3": "Terminal Forms #3", "fina": "Terminal Forms", "flac": "Flattened accent forms", "frac": "Fractions", "fwid": "Full Widths", "half": "Half Forms", "haln": "Halant Forms", "halt": "Alternate Half Widths", "hist": "Historical Forms", "hkna": "Horizontal Kana Alternates", "hlig": "Historical Ligatures", "hngl": "Hangul", "hojo": "Hojo Kanji Forms(JIS X 0212 - 1990 Kanji Forms)", "hwid": "Half Widths", "init": "Initial Forms", "isol": "Isolated Forms", "ital": "Italics", "jalt": "Justification Alternates", "jp78": "JIS78 Forms", "jp83": "JIS83 Forms", "jp90": "JIS90 Forms", "jp04": "JIS2004 Forms", "kern": "Kerning", "lfbd": "Left Bounds", "liga": "Standard Ligatures", "ljmo": "Leading Jamo Forms", "lnum": "Lining Figures", "locl": "Localized Forms", "ltra": "Left - to - right alternates", "ltrm": "Left - to - right mirrored forms", "mark": "Mark Positioning", "med2": "Medial Forms #2", "medi": "Medial Forms", "mgrk": "Mathematical Greek", "mkmk": "Mark to Mark Positioning", "mset": "Mark Positioning via Substitution", "nalt": "Alternate Annotation Forms", "nlck": "NLC Kanji Forms", "nukt": "Nukta Forms", "numr": "Numerators", "onum": "Oldstyle Figures", "opbd": "Optical Bounds", "ordn": "Ordinals", "ornm": "Ornaments", "palt": "Proportional Alternate Widths", "pcap": "Petite Capitals", "pkna": "Proportional Kana", "pnum": "Proportional Figures", "pref": "Pre - Base Forms", "pres": "Pre - base Substitutions", "pstf": "Post - base Forms", "psts": "Post - base Substitutions", "pwid": "Proportional Widths", "qwid": "Quarter Widths", "rand": "Randomize", "rclt": "Required Contextual Alternates", "rkrf": "Rakar Forms", "rlig": "Required Ligatures", "rphf": "Reph Forms", "rtbd": "Right Bounds", "rtla": "Right - to - left alternates", "rtlm": "Right - to - left mirrored forms", "ruby": "Ruby Notation Forms", "rvrn": "Required Variation Alternates", "salt": "Stylistic Alternates", "sinf": "Scientific Inferiors", "size": "Optical size", "smcp": "Small Capitals", "smpl": "Simplified Forms", "ssty": "Math script style alternates", "stch": "Stretching Glyph Decomposition", "subs": "Subscript", "sups": "Superscript", "swsh": "Swash", "titl": "Titling", "tjmo": "Trailing Jamo Forms", "tnam": "Traditional Name Forms", "tnum": "Tabular Figures", "trad": "Traditional Forms", "twid": "Third Widths", "unic": "Unicase", "valt": "Alternate Vertical Metrics", "vatu": "Vattu Variants", "vert": "Vertical Writing", "vhal": "Alternate Vertical Half Metrics", "vjmo": "Vowel Jamo Forms", "vkna": "Vertical Kana Alternates", "vkrn": "Vertical Kerning", "vpal": "Proportional Alternate Vertical Metrics", "vrt2": "Vertical Alternates and Rotation", "vrtr": "Vertical Alternates for Rotation", "zero": "Slashed Zero" }; var Font = ( function() { function Font2(data) { var obj = Typr_js_1.Typr.parse(data); if (!obj.length || typeof obj[0] !== "object" || typeof obj[0].hasOwnProperty !== "function") { throw "unable to parse font"; } for (var n in obj[0]) { this[n] = obj[0][n]; } this.enabledGSUB = {}; } Font2.prototype.getFamilyName = function() { return this.name && (this.name.typoFamilyName || this.name.fontFamily) || ""; }; Font2.prototype.getSubFamilyName = function() { return this.name && (this.name.typoSubfamilyName || this.name.fontSubfamily) || ""; }; Font2.prototype.glyphToPath = function(gid) { return Typr_js_1.Typr.U.glyphToPath(this, gid); }; Font2.prototype.getPairAdjustment = function(gid1, gid2) { return Typr_js_1.Typr.U.getPairAdjustment(this, gid1, gid2); }; Font2.prototype.stringToGlyphs = function(str) { return Typr_js_1.Typr.U.stringToGlyphs(this, str); }; Font2.prototype.glyphsToPath = function(gls) { return Typr_js_1.Typr.U.glyphsToPath(this, gls); }; Font2.prototype.pathToSVG = function(path, prec) { return Typr_js_1.Typr.U.pathToSVG(path, prec); }; Font2.prototype.pathToContext = function(path, ctx) { return Typr_js_1.Typr.U.pathToContext(path, ctx); }; Font2.prototype.lookupFriendlyName = function(table, feature) { if (this[table] !== void 0) { var tbl = this[table]; var feat = tbl.featureList[feature]; return this.featureFriendlyName(feat); } return ""; }; Font2.prototype.featureFriendlyName = function(feature) { if (friendlyTags[feature.tag]) { return friendlyTags[feature.tag]; } if (feature.tag.match(/ss[0-2][0-9]/)) { var name_1 = "Stylistic Set " + Number(feature.tag.substr(2, 2)).toString(); if (feature.featureParams) { var version = Typr_js_1.Typr._bin.readUshort(this._data, feature.featureParams); if (version === 0) { var nameID = Typr_js_1.Typr._bin.readUshort(this._data, feature.featureParams + 2); if (this.name && this.name[nameID] !== void 0) { return name_1 + " - " + this.name[nameID]; } } } return name_1; } if (feature.tag.match(/cv[0-9][0-9]/)) { return "Character Variant " + Number(feature.tag.substr(2, 2)).toString(); } return ""; }; Font2.prototype.enableGSUB = function(featureNumber) { if (this.GSUB) { var feature = this.GSUB.featureList[featureNumber]; if (feature) { for (var i = 0; i < feature.tab.length; ++i) { this.enabledGSUB[feature.tab[i]] = (this.enabledGSUB[feature.tab[i]] || 0) + 1; } } } }; Font2.prototype.disableGSUB = function(featureNumber) { if (this.GSUB) { var feature = this.GSUB.featureList[featureNumber]; if (feature) { for (var i = 0; i < feature.tab.length; ++i) { if (this.enabledGSUB[feature.tab[i]] > 1) { --this.enabledGSUB[feature.tab[i]]; } else { delete this.enabledGSUB[feature.tab[i]]; } } } } }; Font2.prototype.codeToGlyph = function(code) { var g = Typr_js_1.Typr.U.codeToGlyph(this, code); if (this.GSUB) { var gls = [g]; for (var n in this.enabledGSUB) { var l = this.GSUB.lookupList[n]; Typr_js_1.Typr.U._applySubs(gls, 0, l, this.GSUB.lookupList); } if (gls.length === 1) return gls[0]; } return g; }; return Font2; }() ); var Font_1 = Font; function decodeCipherFont(doc) { function b64ToBinaryString(b64){ try{ if (typeof atob === 'function') return atob(b64); if (typeof Buffer !== 'undefined') return Buffer.from(b64, 'base64').toString('binary'); throw new Error('No base64 decoder available'); }catch(e){ if (typeof Buffer !== 'undefined') return Buffer.from(b64, 'base64').toString('binary'); throw e; } } const styleNodes = doc.querySelectorAll("style"); let cipherStyle = null; for (let i = 0; i < styleNodes.length; i++) { if (styleNodes[i].textContent?.includes("font-cxsecret")) { cipherStyle = styleNodes[i]; break; } } if (!cipherStyle) return; const b64Match = cipherStyle.textContent?.match(/base64,([\w\W]+?)'/); if (!b64Match) return; const raw = b64ToBinaryString(b64Match[1]); const buf = new ArrayBuffer(raw.length); const bytes = new Uint8Array(buf); for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i); const fontObj = new Font_1(buf); const ttfTable = JSON.parse(_GM_getResourceText("ttf")); const charMap = {}; for (let codePoint = 19968; codePoint <= 40870; codePoint++) { const glyphId = fontObj.codeToGlyph(codePoint); if (!glyphId) continue; const pathData = fontObj.glyphToPath(glyphId); const signature = md5(JSON.stringify(pathData)).slice(24); charMap[codePoint] = ttfTable[signature]; } const encryptedEls = doc.querySelectorAll(".font-cxsecret"); for (let i = 0; i < encryptedEls.length; i++) { const el = encryptedEls[i]; let text = el.innerHTML; for (const cp in charMap) { const decoded = String.fromCharCode(charMap[cp]); const re = new RegExp(String.fromCharCode(+cp), "g"); text = text.replace(re, decoded); } el.innerHTML = text; el.classList.remove("font-cxsecret"); } } class QuestionProcessor { constructor() { __publicField(this, "_document", document); __publicField(this, "_window", _unsafeWindow); __publicField(this, "addLog"); __publicField(this, "addQuestion"); __publicField(this, "questions", []); __publicField(this, "correctNum", 0); __publicField(this, "isFilling", false); __publicField(this, "parseHtml", () => { throw new Error("Abstract method: parseHtml must be implemented by subclass"); }); __publicField(this, "fillQuestion", (question) => { throw new Error("Abstract method: fillQuestion must be implemented by subclass"); }); __publicField(this, "checkIfAnswered", (question) => { // 首先检查 question.answer 数组是否有值 if (question.answer && question.answer.length > 0) { return true; } // 然后检查 DOM 元素是否已选中 if (question.type === "0" || question.type === "1") { for (const key in question.options) { const optEl = question.options[key]; if (optEl.getAttribute("aria-checked") === "true") { return true; } } return false; } if (question.type === "2") { const taElements = question.element.querySelectorAll("textarea"); for (const ta of taElements) { if (ta.value && ta.value.trim() !== "") { return true; } } return false; } if (question.type === "3") { for (const key in question.options) { if (question.options[key].getAttribute("aria-checked") === "true") { return true; } } return false; } if (question.type === "4" || question.type === "5" || question.type === "6" || question.type === "7") { const taEl = question.element.querySelector("textarea"); if (taEl && taEl.value && taEl.value.trim() !== "") { return true; } return false; } return false; }); __publicField(this, "typeMap", new Map([ ["单选题", "0"], ["A1型题", "0"], ["A1A2型题", "0"], ["单选", "0"], ["single", "0"], ["多选题", "1"], ["X型题", "1"], ["多选", "1"], ["multiple", "1"], ["复选", "1"], ["填空题", "2"], ["填空", "2"], ["fill", "2"], ["判断题", "3"], ["判断", "3"], ["truefalse", "3"], ["是非题", "3"], ["简答题", "4"], ["问答", "4"], ["简答", "4"], ["名词解释", "5"], ["名词", "5"], ["论述题", "6"], ["论述", "6"], ["问答", "6"], ["计算题", "7"], ["计算", "7"], ["排序题", "13"], ["排序", "13"], ["order", "13"], ["配伍题", "14"], ["配伍", "14"], ["matching", "14"], ["案例分析", "15"], ["案例", "15"], ["case", "15"], ["综合题", "16"], ["综合", "16"] ])); __publicField(this, "stripTags", (html) => { if (html == null) return ""; return html.replace(REGEX.HTML_TAGS, "").replace(REGEX.NBSP, " ").replace(REGEX.WHITESPACE, " ").replace(REGEX.BR_TAG, "\n").replace(REGEX.IMG_TAG, '').trim(); }); __publicField(this, "trimTitle", (str) => { return str.replace(REGEX.CLEAN_TITLE, ""); }); const logStore = useLogStore(); const questionStore = useQuestionStore(); this.addLog = logStore.addLog; this.addQuestion = questionStore.addQuestion; } } const buildErrorResponse = (reason) => ({ code: 50001, data: { answer: [], source: 'error', num: "", usenum: "" }, msg: reason }); const clearFillingFlag = (optionElement) => { setTimeout(() => optionElement.removeAttribute("data-filling"), 200); }; const showNoticeDialog = (title, message, noticeType) => { const dialog = document.createElement('div'); dialog.id = 'server-notice-dialog'; dialog.innerHTML = `
📢
${title}
${message}
💬 加入QQ群了解更多信息:
`; document.body.appendChild(dialog); document.getElementById('notice-qq-btn').onclick = () => { window.open('https://qm.qq.com/cgi-bin/qm/qr?k=WJSXpwNrbj-asm0JnQcz7IpQuBqFt1Gd&jump_from=webapi&authKey=tKVLGlIzyLPty8Rgrg6SmWF/jrGDNe6+3q57i3W28di3FtWbj92rixag/hVULc4T', '_blank'); }; document.getElementById('notice-close-btn').onclick = () => { _GM_setValue('closed_notice_type', noticeType); dialog.remove(); }; }; // 离线模式 - 无服务端通知 const checkServerNotice = () => { console.log('[学习通助手] 离线模式,跳过通知检查'); }; // 离线模式 - 无服务端用户ID上报 const reportUserId = async () => { console.log('[学习通助手] 离线模式,跳过用户ID上报'); }; // 离线模式 - 无需服务端Token验证 const verifyTokenForQuery = (token) => { return new Promise((resolve) => { // 离线模式,直接返回验证通过 resolve({ success: true, num: 999999 }); }); }; // ========== 离线本地答题匹配逻辑 ========== // 参考 jinmu.js 的 answerSimilar 和 answerExactMatch 实现 // ========== 题干清洗函数(对齐 jinmu.js)========== // 处理题目中的图片,将图片 src 转为隐藏文本 const optimizationElementWithImage = (root, clone_node = false) => { if (!root) return root; const clone = clone_node ? root.cloneNode(true) : root; try { for (const img of Array.from(clone.querySelectorAll("img"))) { const srcSpan = document.createElement("span"); srcSpan.innerText = img.src || img.dataset.src || ''; srcSpan.style.fontSize = "0px"; srcSpan.style.position = "absolute"; srcSpan.style.left = "-9999px"; img.after(srcSpan); } } catch(e) {} return clone; }; // 规范化空白字符 const nowrap = (str, replace_str = " ") => { if (!str) return ''; return str.replace(/\s+/g, replace_str); }; // 移除所有空白字符 const nospace = (str) => { if (!str) return ''; return str.replace(/\s+/g, ""); }; // 移除冗余词汇 const removeRedundantWords = (str, words) => { if (!str || !words || words.length === 0) return str || ''; for (const word of words.map(w => w.trim())) { if (word) { str = str.replace(new RegExp(word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), ""); } } return str; }; // 题干转换主函数(对齐 jinmu.js 的 workOrExamQuestionTitleTransform) const transformQuestionTitle = (titleElement, redundantWords = []) => { if (!titleElement) return ''; try { // 1. 克隆元素并处理图片 const optimized = optimizationElementWithImage(titleElement, true); // 2. 提取文本 let text = optimized.innerText || optimized.textContent || ''; // 3. 规范化空白 text = nowrap(text, " "); // 4. 移除空白 text = nospace(text); // 5. 移除冗余词汇 text = removeRedundantWords(text.trim(), redundantWords); return text.trim(); } catch(e) { return ''; } }; // 清除字符串中的特殊字符,保留中文、英文、数字 const clearString = (str, ...exclude) => { if (!str) return ''; exclude.push(...["①②③④⑤⑥⑦⑧⑨"]); return str.trim().toLowerCase().replace(/[^\u2E80-\u9FFFA-Za-z0-9①②③④⑤⑥⑦⑧⑨]*/g, ''); }; // 移除冗余前缀(如 A. B. C. D. 等) const removeRedundant = (str) => { if (!str) return ''; return str.trim().replace(/^[A-Z]{1}[\.\、\s]+/, '').replace(/^[①②③④⑤⑥⑦⑧⑨]+[\.\、\s]*/, ''); }; // 去除HTML标签 const stripHtmlTags = (str) => { if (!str) return ''; return str.replace(/<[^>]*>/g, '').trim(); }; // 答案去重 - 避免重复添加相同或高度相似的答案 const deduplicateAnswers = (answers, threshold = 0.85) => { if (!answers || answers.length <= 1) return answers; const unique = []; for (const answer of answers) { const cleanAnswer = clearString(removeRedundant(answer)); const isDuplicate = unique.some(existing => { const cleanExisting = clearString(removeRedundant(existing)); const sim = compareTwoStrings(cleanAnswer, cleanExisting); return sim >= threshold; }); if (!isDuplicate) { unique.push(answer); } } return unique; }; // 字符串相似度比较(基于 bigram 匹配) const compareTwoStrings = (first, second) => { first = first.replace(/\s+/g, ''); second = second.replace(/\s+/g, ''); if (first === second) return 1; if (first.length < 2 || second.length < 2) return 0; const firstBigrams = new Map(); for (let i = 0; i < first.length - 1; i++) { const bigram = first.substring(i, i + 2); const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) + 1 : 1; firstBigrams.set(bigram, count); } let intersectionSize = 0; for (let i = 0; i < second.length - 1; i++) { const bigram = second.substring(i, i + 2); const count = firstBigrams.has(bigram) ? firstBigrams.get(bigram) : 0; if (count > 0) { firstBigrams.set(bigram, count - 1); intersectionSize++; } } return (2 * intersectionSize) / (first.length + second.length - 2); }; // 编辑距离计算(Levenshtein distance) const levenshteinDistance = (str1, str2) => { const m = str1.length; const n = str2.length; const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0)); for (let i = 0; i <= m; i++) dp[i][0] = i; for (let j = 0; j <= n; j++) dp[0][j] = j; for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (str1[i - 1] === str2[j - 1]) { dp[i][j] = dp[i - 1][j - 1]; } else { dp[i][j] = Math.min( dp[i - 1][j - 1] + 1, dp[i][j - 1] + 1, dp[i - 1][j] + 1 ); } } } return dp[m][n]; }; // Jaccard 相似度(基于字符集合) const jaccardSimilarity = (str1, str2) => { if (!str1 || !str2 || str1.length === 0 || str2.length === 0) return 0; const set1 = new Set(str1.split('')); const set2 = new Set(str2.split('')); const intersection = new Set([...set1].filter(x => set2.has(x))); const union = new Set([...set1, ...set2]); return intersection.size / union.size; }; // 余弦相似度(基于字符频率) const cosineSimilarity = (str1, str2) => { if (!str1 || !str2 || str1.length === 0 || str2.length === 0) return 0; const getCharFreq = (str) => { const freq = {}; for (const char of str) { freq[char] = (freq[char] || 0) + 1; } return freq; }; const freq1 = getCharFreq(str1); const freq2 = getCharFreq(str2); const allChars = new Set([...Object.keys(freq1), ...Object.keys(freq2)]); let dotProduct = 0; let norm1 = 0; let norm2 = 0; for (const char of allChars) { const v1 = freq1[char] || 0; const v2 = freq2[char] || 0; dotProduct += v1 * v2; norm1 += v1 * v1; norm2 += v2 * v2; } if (norm1 === 0 || norm2 === 0) return 0; return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); }; // 综合相似度(bigram + 编辑距离 + LCS + Jaccard + 余弦) const comprehensiveSimilarity = (first, second) => { first = first.replace(/\s+/g, ''); second = second.replace(/\s+/g, ''); if (first === second) return 1; if (first.length < 2 || second.length < 2) return 0; // 1. Bigram 相似度 const bigramSim = compareTwoStrings(first, second); // 2. 编辑距离相似度 const editDist = levenshteinDistance(first, second); const maxLen = Math.max(first.length, second.length); const editSim = 1 - editDist / maxLen; // 3. 最长公共子串 let longestCommonSubstr = 0; for (let i = 0; i < first.length; i++) { for (let j = 0; j < second.length; j++) { let len = 0; while (i + len < first.length && j + len < second.length && first[i + len] === second[j + len]) { len++; } longestCommonSubstr = Math.max(longestCommonSubstr, len); } } const lcsSim = longestCommonSubstr / Math.max(first.length, second.length); // 4. Jaccard 相似度 const jaccardSim = jaccardSimilarity(first, second); // 5. 余弦相似度 const cosineSim = cosineSimilarity(first, second); // 加权综合:bigram 30% + 编辑距离 25% + LCS 20% + Jaccard 15% + 余弦 10% return bigramSim * 0.30 + editSim * 0.25 + lcsSim * 0.20 + jaccardSim * 0.15 + cosineSim * 0.10; }; // 查找最佳匹配 const findBestMatch = (mainString, targetStrings) => { const ratings = []; let bestMatchIndex = 0; for (let i = 0; i < targetStrings.length; i++) { const currentTargetString = targetStrings[i]; // 使用纯 bigram 匹配(对齐 jinmu.js) const currentRating = compareTwoStrings(mainString, currentTargetString); ratings.push({ target: currentTargetString, rating: currentRating }); if (currentRating > ratings[bestMatchIndex].rating) { bestMatchIndex = i; } } return { ratings, bestMatch: ratings[bestMatchIndex], bestMatchIndex }; }; // 精确匹配答案 - 返回原始选项文本(与 question.options 的键一致) const answerExactMatch = (answers, options) => { const _answers = answers.map(removeRedundant); const _options = options.map(removeRedundant); const result = _answers.length !== 0 ? options.filter((option, index) => { return _answers.find((answer) => answer.trim() === _options[index].trim()); }) : []; return result; }; // 相似度匹配答案 const answerSimilar = (answers, options, threshold = 0.6) => { const _answers = answers.map(removeRedundant).map(a => clearString(a)); const _options = options.map(removeRedundant).map(o => clearString(o)); const similar = _answers.length !== 0 ? _options.map((option, index) => { if (option.trim() === '') { return { rating: 0, target: '', originalIndex: index }; } return { ...findBestMatch(option, _answers).bestMatch, originalIndex: index }; }) : _options.map((_, index) => ({ rating: 0, target: '', originalIndex: index })); // 过滤出相似度高于阈值的选项,返回原始选项文本 const matchedOptions = []; similar.forEach((sim) => { if (sim.rating >= threshold) { matchedOptions.push(options[sim.originalIndex]); } }); return matchedOptions; }; // 离线本地答题匹配主函数(对齐 jinmu.js 核心逻辑) const performLocalAnswerMatch = (question) => { const logStore = useLogStore(); if (!question || !question.title) { return buildErrorResponse('题目数据无效'); } const questionType = question.type || '0'; // ========== 题干清洗(对齐 jinmu.js)========== // 使用新的 transformQuestionTitle 函数处理题干 let cleanTitle = ''; try { // 如果 question.title 是 DOM 元素,使用 transformQuestionTitle if (question.titleElement) { cleanTitle = transformQuestionTitle(question.titleElement, ['题目:', '问题:', '题干:']); } else { // 如果是字符串,使用传统清洗方式 cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); } } catch(e) { cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); } // ========== 选项清洗(对齐 jinmu.js)========== const cleanOptions = (options) => { return options.map(o => { try { // 如果选项是 DOM 元素,使用 optimizationElementWithImage if (o && o.nodeType) { const optimized = optimizationElementWithImage(o, true); return (optimized.innerText || optimized.textContent || '').trim(); } // 如果是字符串,使用传统清洗 return removeRedundant(String(o)).trim(); } catch(e) { return removeRedundant(String(o)).trim(); } }); }; // ========== 判断题处理 - 参考 jinmu.js 完整实现 ========== if (questionType === '3') { // jinmu.js 的判断题关键词列表(完整版本) const correctWords = ['是', '对', '正确', '确定', '√', '对的', '是的', '正确的', 'true', 'True', 'T', 'yes', '1']; const incorrectWords = ['非', '否', '错', '错误', '×', 'X', '错的', '不对', '不正确的', '不正确', '不是', '不是的', 'false', 'False', 'F', 'no', '0']; // 精确匹配函数(完全复制 jinmu.js) const matches = function(target, options2) { return options2.some( (option) => clearString(removeRedundant(option), "√", "×") === clearString(removeRedundant(target), "√", "×") ); }; // ========== 阶段1:优先查询本地题库(参考 jinmu.js)========== const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); let dbResult = null; try { dbResult = LocalDB.query(cleanTitle); } catch (e) {} if (dbResult && dbResult.length > 0) { const dbAnswers = dbResult.map(r => r.answer).filter(a => a && a.trim()); if (dbAnswers.length > 0) { // 使用 jinmu.js 的精确匹配逻辑 const answerShowCorrect = dbAnswers.find(answer => matches(answer, correctWords)); const answerShowIncorrect = dbAnswers.find(answer => matches(answer, incorrectWords)); if (answerShowCorrect || answerShowIncorrect) { const answer = answerShowCorrect ? '对' : '错'; logStore.addLog(`判断题: ${answer} (置信度: 98% - 题库匹配)`, 'success'); return { code: 200, msg: '本地题库匹配成功', data: { answer: [answer], source: 'local-db', num: -1, confidence: 98 } }; } } } // ========== 阶段2:检查选项文本是否匹配正确/错误关键词(参考 jinmu.js)========== const options = question.optionsText || []; if (options.length >= 2) { // 检查选项中是否有明确的"对"/"错"标识 const hasTrueOption = options.some(opt => matches(opt, correctWords)); const hasFalseOption = options.some(opt => matches(opt, incorrectWords)); if (hasTrueOption && hasFalseOption) { // 选项包含对和错,这是正常的判断题,需要根据题目推断答案 // 使用增强版关键词分析(参考 jinmu.js 的逻辑,但更保守) const title = question.title.toLowerCase(); // 检测否定词(扩展列表) const negativeWords = ['不', '非', '否', '无', '错误', 'false', 'wrong', '不存在', '不可能', '不一定', '不会', '不能', '不可', '无需', '不必', '无须', '并非', '绝非', '毫不', '未曾', '尚未', '没有', '缺乏', '未']; const hasNegative = negativeWords.some(word => title.includes(word)); // 检测双重否定 const doubleNegativePatterns = ['不.*不', '非.*不', '无.*不', '没有.*不', '未.*不']; const hasDoubleNegative = doubleNegativePatterns.some(pattern => new RegExp(pattern).test(title)); // 检测绝对化词语 const absoluteWords = ['所有', '全部', '任何', '绝对', '总是', '永远', '只有', '仅仅', '唯一', '完全', '任何情况下', '必定', '必然']; const hasAbsolute = absoluteWords.some(word => title.includes(word)); let answer = '对'; let confidence = 50; if (hasDoubleNegative) { // 双重否定表肯定 answer = '对'; confidence = 80; } else if (hasAbsolute) { // 绝对化词语通常为"错" answer = '错'; confidence = 75; } else if (hasNegative) { // 否定词通常为"错" answer = '错'; confidence = 70; } else { // 默认倾向于"错"(更保守的策略) answer = '错'; confidence = 55; } logStore.addLog(`判断题: ${answer} (置信度: ${confidence}% - 关键词分析)`, 'info'); return { code: 200, msg: '本地匹配成功', data: { answer: [answer], source: 'local-keyword', num: -1, confidence: confidence } }; } } logStore.addLog(`判断题: 未找到可靠答案,跳过本地猜测`, 'warning'); return buildErrorResponse('判断题未找到可靠答案'); } // 单选题和多选题处理 if (questionType === '0' || questionType === '1') { const options = question.optionsText || []; const optionElements = question.options || {}; if (options.length === 0) { return buildErrorResponse('无法获取选项'); } const questionTypeName = questionType === '0' ? '单选题' : '多选题'; logStore.addLog(`${questionTypeName}: 共 ${options.length} 个选项`, 'info'); // 使用新的 cleanOptions 函数清洗选项(对齐 jinmu.js) const cleanedOptions = cleanOptions(options); let matchedAnswers = []; let matchMethod = ''; let confidence = 50; // ========== 阶段1:优先查询本地题库(参考 jinmu.js)========== let dbResult = null; try { dbResult = LocalDB.query(cleanTitle); } catch (e) {} if (dbResult && dbResult.length > 0) { const dbAnswers = dbResult.map(r => r.answer).filter(a => a && a.trim()); if (dbAnswers.length > 0) { // 参考 jinmu.js:先尝试字母答案(如 A、B、C、D) for (const answer of dbAnswers) { const trimmedAnswer = answer.trim().toUpperCase(); // 字母答案匹配(如 "A"、"AB"、"ABCD") if (/^[A-Z]+$/.test(trimmedAnswer)) { for (const char of trimmedAnswer) { const index = char.charCodeAt(0) - 65; if (index >= 0 && index < options.length) { if (!matchedAnswers.includes(options[index])) { matchedAnswers.push(options[index]); } } } if (matchedAnswers.length > 0) { matchMethod = '字母匹配'; confidence = 95; logStore.addLog(`[题库] 字母匹配成功: ${trimmedAnswer}`, 'success'); break; } } } // 如果没有字母答案,使用相似度匹配(参考 jinmu.js 的 answerSimilar) if (matchedAnswers.length === 0) { // 使用清洗后的选项进行匹配 const optionStrings = cleanedOptions; const allAnswers = dbAnswers.map(a => splitAnswer(a)).flat(); // 精确匹配 const exactMatches = answerExactMatch(allAnswers, optionStrings); if (exactMatches.length > 0) { matchedAnswers = exactMatches.map(m => options[optionStrings.indexOf(m)]); matchMethod = '精确匹配'; confidence = 98; logStore.addLog(`[题库] 精确匹配成功: ${matchedAnswers.length} 个`, 'success'); } else { // 相似度匹配(参考 jinmu.js) const ratings = StringUtil.answerSimilar(allAnswers, optionStrings); const matchedIndices = []; for (let j = 0; j < ratings.length; j++) { if (ratings[j].rating > 0.6) { matchedIndices.push(j); } } if (matchedIndices.length > 0) { matchedAnswers = matchedIndices.map(idx => options[idx]); matchMethod = '相似度匹配'; confidence = 85; logStore.addLog(`[题库] 相似度匹配成功: ${matchedAnswers.length} 个`, 'success'); } } } } } // ========== 阶段2:本地策略匹配(兜底)========== if (matchedAnswers.length === 0) { // 策略1:选项文本完整出现在题目中(精确包含) for (let i = 0; i < options.length; i++) { const cleanOption = cleanedOptions[i]; if (cleanOption.length > 2 && cleanTitle.includes(cleanOption)) { matchedAnswers.push(options[i]); } } if (matchedAnswers.length > 0) { matchMethod = '精确包含'; confidence = 80; logStore.addLog(`策略1-精确包含: 找到 ${matchedAnswers.length} 个匹配`, 'success'); } // 策略2:关键词匹配 if (matchedAnswers.length === 0 || questionType === '1') { const keywords = cleanTitle.replace(/[的是否正确错误下列说法哪项哪个关于以下什么]/g, '').trim(); if (keywords.length > 2) { for (let i = 0; i < options.length; i++) { const cleanOption = cleanedOptions[i]; if (cleanOption.length > 2 && keywords.includes(cleanOption) && !matchedAnswers.includes(options[i])) { matchedAnswers.push(options[i]); } } } if (matchedAnswers.length > 0) { matchMethod = matchMethod ? `${matchMethod}+关键词匹配` : '关键词匹配'; confidence = 70; logStore.addLog(`策略2-关键词匹配: 找到 ${matchedAnswers.length} 个匹配`, 'success'); } } // 策略3:互斥选项分析 if ((matchedAnswers.length === 0 || questionType === '1') && options.length >= 2) { const cleanedForAntonym = cleanedOptions.map(o => clearString(o)); const antonymPairs = [ ['增加', '减少'], ['上升', '下降'], ['提高', '降低'], ['正确', '错误'], ['对', '错'], ['是', '否'], ['大于', '小于'], ['正相关', '负相关'], ['促进', '抑制'], ['扩大', '缩小'], ['加速', '减速'], ['增强', '减弱'] ]; for (const [a, b] of antonymPairs) { const hasA = cleanedForAntonym.some(o => o.includes(a)); const hasB = cleanedForAntonym.some(o => o.includes(b)); if (hasA && hasB) { const cleanTitleLower = cleanTitle.toLowerCase(); if (cleanTitleLower.includes(a.toLowerCase())) { const idx = cleanedForAntonym.findIndex(o => o.includes(a)); if (idx >= 0 && !matchedAnswers.includes(options[idx])) { matchedAnswers.push(options[idx]); } } else if (cleanTitleLower.includes(b.toLowerCase())) { const idx = cleanedForAntonym.findIndex(o => o.includes(b)); if (idx >= 0 && !matchedAnswers.includes(options[idx])) { matchedAnswers.push(options[idx]); } } break; } } if (matchedAnswers.length > 0) { matchMethod = matchMethod ? `${matchMethod}+互斥分析` : '互斥分析'; confidence = 65; logStore.addLog(`策略3-互斥分析: 找到 ${matchedAnswers.length} 个匹配`, 'success'); } } } if (matchedAnswers.length === 0) { logStore.addLog(`${questionTypeName}: 未找到可靠本地答案,跳过猜测`, 'warning'); return buildErrorResponse(`${questionTypeName}未找到可靠答案`); } // 多选题:限制选择数量(最少2个,最多选项总数) if (questionType === '1') { matchedAnswers = [...new Set(matchedAnswers)]; // 去重 if (matchedAnswers.length === 1 && options.length >= 2) { // 如果只有一个匹配,尝试补充一个语义相关的选项 const cleanedOptions = options.map(o => clearString(removeRedundant(o))); const matchedClean = clearString(removeRedundant(matchedAnswers[0])); let secondBest = null; let maxSim = 0; for (const option of options) { if (matchedAnswers.includes(option)) continue; const optClean = clearString(removeRedundant(option)); const commonChars = [...matchedClean].filter(c => optClean.includes(c)).length; const sim = commonChars / Math.max(matchedClean.length, optClean.length); if (sim > maxSim && sim > 0.2) { maxSim = sim; secondBest = option; } } if (secondBest) { matchedAnswers.push(secondBest); logStore.addLog(`多选题补充选项: ${secondBest}`, 'info'); } } } // 单选题:只选择一个 if (questionType === '0' && matchedAnswers.length > 1) { matchedAnswers = [matchedAnswers[0]]; } logStore.addLog(`${questionTypeName}匹配结果: ${matchedAnswers.length} 个选项 (置信度: ${confidence}%)`, 'info'); return { code: 200, msg: '本地匹配成功', data: { answer: matchedAnswers, source: matchMethod ? `local-${matchMethod}` : 'local', num: -1, confidence: confidence } }; } // 填空题处理 - 同步的多空填空逻辑 if (questionType === '2') { const title = question.title.replace(/<[^>]*>/g, '').trim(); // 检测题目中是否有括号提示答案 const bracketMatch = title.match(/[((]([^))]+)[))]/); if (bracketMatch && bracketMatch[1]) { const hint = bracketMatch[1].trim(); if (hint.length > 0 && hint.length < 50) { logStore.addLog(`填空题: 从题目中提取答案`, 'success'); return { code: 200, msg: '本地匹配成功', data: { answer: [hint], source: 'local-fill', num: -1 } }; } } // 从云端或本地获取答案后进行分割处理(支持多空填空) if (storedAnswers && storedAnswers.length > 0) { let answers = storedAnswers; // 如果只有一个答案,尝试用分隔符分割 if (answers.length === 1) { answers = AdvancedAnswerEngine.splitAnswer(answers[0]); } // 如果分割后的答案数量匹配填空数量 const fillCount = (title.match(/____+|空格+|______+/g) || []).length || 1; if (answers.length === fillCount || fillCount === 1) { logStore.addLog(`填空题: 分割出 ${answers.length} 个答案`, 'success'); return { code: 200, msg: '本地匹配成功', data: { answer: answers, source: 'local-fill-split', num: -1 } }; } else if (answers.length > 0) { // 答案数量不匹配时,取第一个答案或合并所有答案 logStore.addLog(`填空题: 合并 ${answers.length} 个答案`, 'success'); return { code: 200, msg: '本地匹配成功', data: { answer: fillCount === 1 ? answers : [answers[0]], source: 'local-fill-merge', num: -1 } }; } } logStore.addLog('该题型不支持自动答题', 'warning'); return buildErrorResponse('该题型需要手动答题'); } logStore.addLog('该题型不支持自动答题', 'warning'); return buildErrorResponse('该题型需要手动答题'); }; const CLOUD_QUIZ_API = '/api/query-quiz'; // 多题库API配置 - 聚合多个题库源(扩展版) const QUIZ_APIS = { // 言溪题库 (jinmu.js 默认题库) yanxi: { name: '言溪题库', url: 'https://tk.enncy.cn/api/v1/search', method: 'GET', params: (question) => ({ question: question.title }), parse: (data) => { if (data && data.code === 200 && data.data && data.data.answer) { return { answers: Array.isArray(data.data.answer) ? data.data.answer : [data.data.answer], confidence: 90 }; } return null; } }, // 教育搜题 (apibyte.cn) edusearch: { name: '教育搜题', url: 'https://apione.apibyte.cn/edusearch', method: 'GET', params: (question) => ({ platform: '超星学习通', question: question.title, type: question.type || 0 }), parse: (data) => { if (data && data.code === 200 && data.data && data.data.results) { const results = data.data.results; if (results.length > 0) { const first = results[0]; const answers = first.answer || (first.data && first.data.answer ? [first.data.answer] : []); if (answers.length > 0) { return { answers, confidence: 85, source: first.source }; } } } return null; } }, // 免费题库API (lyck6.cn) lyck6: { name: 'LYCK题库', url: 'http://cx.lyck6.cn/api/api.php', method: 'GET', params: (question) => ({ question: question.title }), parse: (data) => { if (data && data.code === 1 && data.answer) { return { answers: [data.answer], confidence: 80 }; } return null; } }, // wkapi 免费题库 (github.com/gzsj/wkapi) wkapi: { name: 'WKAPI题库', url: 'https://wkapi.gzsj.top/api/search', method: 'GET', params: (question) => ({ question: question.title }), parse: (data) => { if (data && data.success && data.data && data.data.answer) { return { answers: Array.isArray(data.data.answer) ? data.data.answer : [data.data.answer], confidence: 85 }; } return null; } }, // 一之题库 yizhi: { name: '一之题库', url: 'https://api.1zti.com/api/v1/search', method: 'GET', params: (question) => ({ question: question.title }), parse: (data) => { if (data && data.code === 200 && data.data) { const answers = data.data.answer || data.data.answers; if (answers) { return { answers: Array.isArray(answers) ? answers : [answers], confidence: 85 }; } } return null; } }, // 爱答题库 aidati: { name: '爱答题库', url: 'https://api.aidati.com/api/search', method: 'GET', params: (question) => ({ q: question.title }), parse: (data) => { if (data && data.success && data.answer) { return { answers: Array.isArray(data.answer) ? data.answer : [data.answer], confidence: 80 }; } return null; } }, // 新增:学习通题库 xuetang: { name: '学习通题库', url: 'https://api.xuetangx.com/api/search', method: 'GET', params: (question) => ({ query: question.title, type: 'quiz' }), parse: (data) => { if (data && data.code === 0 && data.result) { const answers = data.result.answers || []; if (answers.length > 0) { return { answers, confidence: 88 }; } } return null; } }, // 新增:智慧树题库 zhihuishu: { name: '智慧树题库', url: 'https://api.zhihuishu.com/api/quiz/search', method: 'GET', params: (question) => ({ q: question.title, limit: 1 }), parse: (data) => { if (data && data.success && data.data) { const answer = data.data.answer || ''; if (answer) { return { answers: [answer], confidence: 82 }; } } return null; } }, // 新增:MOOC题库 mooc: { name: 'MOOC题库', url: 'https://www.icourse163.org/api/search/quiz', method: 'GET', params: (question) => ({ keyword: question.title, pageSize: 1 }), parse: (data) => { if (data && data.code === 200 && data.result) { const items = data.result.list || []; if (items.length > 0 && items[0].answer) { return { answers: [items[0].answer], confidence: 85 }; } } return null; } }, // 新增:职教云题库 zhijiaoyun: { name: '职教云题库', url: 'https://api.zhijiaoyun.com/api/v1/quiz/search', method: 'GET', params: (question) => ({ question: question.title }), parse: (data) => { if (data && data.status === 'success' && data.data) { const answers = data.data.answers || []; if (answers.length > 0) { return { answers, confidence: 80 }; } } return null; } }, // 新增:学堂在线题库 xuetangonline: { name: '学堂在线题库', url: 'https://www.xuetangx.com/api/quiz/search', method: 'GET', params: (question) => ({ query: question.title }), parse: (data) => { if (data && data.success && data.data) { const answer = data.data.answer || ''; if (answer) { return { answers: [answer], confidence: 83 }; } } return null; } } }; // 查询云端题库 const fetchFromCloudQuiz = async (question) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } logStore.addLog('正在查询云端题库...', 'info'); try { const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const result = await callProxyApi(CLOUD_QUIZ_API, { title: cleanTitle, type: question.type || '0', options: question.optionsText || [], timestamp: Date.now() }); if (!result.success) { logStore.addLog(`云端题库查询失败: ${result.error}`, 'warning'); return null; } const data = result.data; if (!data || !data.answers || data.answers.length === 0) { logStore.addLog('云端题库未找到答案', 'info'); return null; } logStore.addLog(`云端题库匹配成功,答案: ${data.answers.join(', ')}`, 'success'); return { code: 200, msg: '云端题库匹配成功', data: { answer: data.answers, source: 'cloud-quiz', num: -1, confidence: data.confidence || 85 } }; } catch (e) { logStore.addLog(`云端题库查询异常: ${e.message}`, 'warning'); return null; } }; // 聚合查询多个题库API(扩展版:包含更多题库源) const fetchFromMultipleApis = async (question, apis = ['edusearch', 'lyck6', 'wkapi', 'yizhi', 'aidati', 'xuetang', 'zhihuishu', 'mooc', 'zhijiaoyun', 'xuetangonline', 'yanxi']) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); logStore.addLog(`正在聚合查询 ${apis.length} 个题库...`, 'info'); const results = []; const promises = []; // 题库优先级配置(数字越大优先级越高) const API_PRIORITY = { 'yanxi': 100, // 言溪题库 - jinmu.js 默认题库,最高优先级 'edusearch': 95, // 教育搜题 'wkapi': 90, // WKAPI题库 'xuetang': 88, // 学习通题库 'lyck6': 85, // LYCK题库 'yizhi': 82, // 一之题库 'mooc': 80, // MOOC题库 'xuetangonline': 78, // 学堂在线题库 'zhihuishu': 75, // 智慧树题库 'zhijiaoyun': 72, // 职教云题库 'aidati': 70 // 爱答题库 }; for (const apiKey of apis) { const apiConfig = QUIZ_APIS[apiKey]; if (!apiConfig) continue; const promise = (async () => { try { const params = apiConfig.params({ title: cleanTitle, type: question.type }); const url = new URL(apiConfig.url); if (apiConfig.method === 'GET') { Object.keys(params).forEach(key => url.searchParams.set(key, params[key])); } // 使用 GM_xmlhttpRequest 绕过 CORS(对齐 jinmu.js) return new Promise((resolve) => { const timeoutId = setTimeout(() => { logStore.addLog(`${apiConfig.name} 查询超时`, 'warning'); resolve(null); }, 60000); if (typeof GM_xmlhttpRequest !== 'undefined') { GM_xmlhttpRequest({ method: apiConfig.method.toUpperCase(), url: url.toString(), headers: { 'Content-Type': 'application/json' }, data: apiConfig.method === 'POST' ? JSON.stringify(params) : undefined, responseType: 'json', timeout: 60000, onload: (response) => { clearTimeout(timeoutId); try { if (response.status === 200) { const data = typeof response.responseText === 'string' ? JSON.parse(response.responseText) : response.response || response.responseText; const parsed = apiConfig.parse(data); if (parsed) { logStore.addLog(`${apiConfig.name} 匹配成功: ${parsed.answers.join(', ')}`, 'success'); resolve({ ...parsed, api: apiKey, apiName: apiConfig.name, priority: API_PRIORITY[apiKey] || 50, score: (parsed.confidence || 80) * 10 + (API_PRIORITY[apiKey] || 50) }); } else { resolve(null); } } else { resolve(null); } } catch (e) { resolve(null); } }, onerror: (err) => { clearTimeout(timeoutId); logStore.addLog(`${apiConfig.name} 查询失败: ${err.statusText || 'network error'}`, 'warning'); resolve(null); }, ontimeout: () => { logStore.addLog(`${apiConfig.name} 查询超时`, 'warning'); resolve(null); } }); } else { // 降级到 fetch(仅在不支持 GM_xmlhttpRequest 时) fetch(url.toString(), { method: apiConfig.method, headers: { 'Content-Type': 'application/json' }, body: apiConfig.method === 'POST' ? JSON.stringify(params) : undefined }) .then(r => r.json()) .then(data => { const parsed = apiConfig.parse(data); if (parsed) { resolve({ ...parsed, api: apiKey, apiName: apiConfig.name, priority: API_PRIORITY[apiKey] || 50, score: (parsed.confidence || 80) * 10 + (API_PRIORITY[apiKey] || 50) }); } else { resolve(null); } }) .catch(() => { logStore.addLog(`${apiConfig.name} 查询失败`, 'warning'); resolve(null); }); } }); } catch (e) { logStore.addLog(`${apiConfig.name} 查询异常: ${e.message}`, 'warning'); return null; } })(); promises.push(promise); } // 并行查询所有题库(Promise.allSettled 保证不阻塞) const settled = await Promise.allSettled(promises); for (const result of settled) { if (result.status === 'fulfilled' && result.value) { results.push(result.value); } } if (results.length === 0) { logStore.addLog('所有题库均未找到答案', 'info'); return null; } // 按综合得分排序(置信度 * 10 + 优先级) results.sort((a, b) => (b.score || 850) - (a.score || 850)); const best = results[0]; logStore.addLog(`聚合查询成功,最佳答案来自 ${best.apiName}: ${best.answers.join(', ')} (综合得分: ${best.score})`, 'success'); return { code: 200, msg: `聚合题库匹配成功 (${results.length}/${apis.length} 个题库命中)`, data: { answer: best.answers, source: `multi-api:${best.api}`, num: -1, confidence: best.confidence, allResults: results } }; }; // 同步题库到本地缓存 const syncQuizCache = async () => { const logStore = useLogStore(); try { const result = await callProxyApi('/api/sync-quiz', { last_sync_time: localStorage.getItem('_quiz_last_sync') || 0 }); if (!result.success) { logStore.addLog(`题库同步失败: ${result.error}`, 'warning'); return; } const newQuizzes = result.data || []; if (newQuizzes.length > 0) { // 合并到本地缓存 const existing = JSON.parse(localStorage.getItem('_quiz_cache') || '[]'); const merged = [...existing, ...newQuizzes]; // 去重 const unique = merged.filter((quiz, index, self) => index === self.findIndex(q => q.title === quiz.title) ); localStorage.setItem('_quiz_cache', JSON.stringify(unique)); localStorage.setItem('_quiz_last_sync', Date.now().toString()); logStore.addLog(`题库同步成功,新增 ${newQuizzes.length} 条题目`, 'success'); } } catch (e) { logStore.addLog(`题库同步异常: ${e.message}`, 'warning'); } }; // 从本地缓存查询 const queryLocalQuizCache = (question) => { const logStore = useLogStore(); try { const cache = JSON.parse(localStorage.getItem('_quiz_cache') || '[]'); if (cache.length === 0) return null; const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); // 精确匹配 const exactMatch = cache.find(q => q.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim() === cleanTitle ); if (exactMatch) { logStore.addLog('本地缓存精确匹配成功', 'success'); return { code: 200, msg: '本地缓存匹配成功', data: { answer: exactMatch.answers, source: 'local-cache', num: -1, confidence: 98 } }; } // 相似度匹配(作为备选) const similarMatch = cache.find(q => { const qTitle = q.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const sim = compareTwoStrings(cleanTitle, qTitle); return sim > 0.8; }); if (similarMatch) { logStore.addLog('本地缓存相似度匹配成功', 'success'); return { code: 200, msg: '本地缓存相似度匹配成功', data: { answer: similarMatch.answers, source: 'local-cache-similar', num: -1, confidence: 75 } }; } } catch (e) { logStore.addLog(`查询本地缓存异常: ${e.message}`, 'warning'); } return null; }; // ========== 增强版答题算法(多级匹配策略)========== // 答案匹配增强器 - 整合多种匹配策略 const AnswerEnhancer = { // 同义词词典(扩展版) SYNONYMS: { '正确': ['对', '是', 'true', 'True', 'T', '√', '正确的', '准确', '无误', '确切'], '错误': ['错', '否', 'false', 'False', 'F', '×', 'X', '错误的', '不正确', '有误'], '是': ['对', '正确', 'true', 'T', '确实', '的确'], '否': ['错', '错误', 'false', 'F', '不是', '并非'], 'A': ['甲', '1', '一', '第一个', '首选'], 'B': ['乙', '2', '二', '第二个', '次选'], 'C': ['丙', '3', '三', '第三个'], 'D': ['丁', '4', '四', '第四个'], 'E': ['戊', '5', '五', '第五个'], '增加': ['上升', '提高', '增长', '增多', '上升趋势'], '减少': ['下降', '降低', '减少', '缩减', '下降趋势'], '促进': ['推动', '加速', '助力', '带动'], '抑制': ['阻碍', '减缓', '限制', '制约'], '主要': ['核心', '关键', '重要', '主导'], '次要': ['辅助', '次要', '从属', '附带'], '必然': ['一定', '必定', '肯定', '必定'], '偶然': ['随机', '偶尔', '碰巧', '意外'], }, // 匹配阈值配置 THRESHOLDS: { exact: 1.0, // 精确匹配 high: 0.85, // 高置信度 medium: 0.7, // 中等置信度 low: 0.55, // 低置信度(需额外验证) fallback: 0.4 // 兜底匹配 }, // 主匹配函数 async matchAnswer(questionText, options, storedAnswers, ctx = {}) { const optionStrings = options.map(o => { const text = o.innerText || o.textContent || o; return this.cleanText(text); }); const cleanedAnswers = storedAnswers.map(a => this.cleanText(a)); const cleanQuestion = this.cleanText(questionText); // 策略1:精确匹配(最高优先级) const exactResult = this.exactMatch(cleanedAnswers, optionStrings, options); if (exactResult.success) { return { ...exactResult, method: 'exact', confidence: 100 }; } // 策略2:纯字母答案匹配(如 "A", "ABCD") const letterResult = this.letterMatch(storedAnswers, options); if (letterResult.success) { return { ...letterResult, method: 'letter', confidence: 95 }; } // 策略3:高置信度模糊匹配 const highResult = this.similarMatch(cleanedAnswers, optionStrings, options, this.THRESHOLDS.high); if (highResult.success) { return { ...highResult, method: 'high-confidence', confidence: 90 }; } // 策略4:判断题特殊处理 if (options.length === 2) { const judgeResult = this.judgementMatch(cleanedAnswers, options); if (judgeResult.success) { return { ...judgeResult, method: 'judgement', confidence: judgeResult.confidence }; } } if (options.length > 2) { const multipleResult = this.multipleMatch(storedAnswers, options); if (multipleResult.success) { return { success: true, indices: multipleResult.indices, answers: multipleResult.answers, method: 'multiple-' + multipleResult.method, confidence: multipleResult.confidence }; } } // 策略5:中等置信度 + 语义验证 const medResult = this.similarMatch(cleanedAnswers, optionStrings, options, this.THRESHOLDS.medium); if (medResult.success) { const semanticValid = this.validateSemantic(cleanQuestion, cleanedAnswers, optionStrings[medResult.index]); if (semanticValid) { return { ...medResult, method: 'medium-confidence', confidence: 75 }; } } // 策略6:互斥选项分析 const mutexResult = this.mutexAnalysis(cleanQuestion, options); if (mutexResult.success) { return { ...mutexResult, method: 'mutex-analysis', confidence: 70 }; } // 策略7:绝对词排除 const absResult = this.absoluteWordExclusion(options); if (absResult.success) { return { ...absResult, method: 'absolute-exclusion', confidence: 65 }; } // 策略8:最长选项优先 const longestResult = this.longestOptionMatch(options); if (longestResult.success) { return { ...longestResult, method: 'longest-option', confidence: 55 }; } // 策略9:关键词匹配 const keywordResult = this.keywordMatch(cleanQuestion, options); if (keywordResult.success) { return { ...keywordResult, method: 'keyword', confidence: 60 }; } // 策略10:低置信度兜底匹配 const lowResult = this.similarMatch(cleanedAnswers, optionStrings, options, this.THRESHOLDS.fallback); if (lowResult.success) { return { ...lowResult, method: 'fallback', confidence: 45 }; } return { success: false }; }, // 清理文本(修复:不过度清理,保留空格和标点用于相似度计算) cleanText(text) { if (!text) return ''; let cleaned = text.trim().toLowerCase(); // 同义词替换 for (const [target, sources] of Object.entries(this.SYNONYMS)) { sources.forEach(source => { cleaned = cleaned.replace(new RegExp(source, 'g'), target); }); } // 仅去除HTML标签和多余空白,保留文字内容 cleaned = cleaned.replace(/<[^>]*>/g, '').replace(/ /g, ' ').replace(/\s+/g, ' ').trim(); return cleaned; }, // 精确匹配 exactMatch(answers, options, originalOptions) { for (let i = 0; i < options.length; i++) { if (answers.includes(options[i])) { return { success: true, index: i, option: originalOptions[i] }; } } return { success: false }; }, // 字母答案匹配 letterMatch(answers, options) { for (const answer of answers) { const clean = answer.trim().toUpperCase(); // 单字母答案 if (clean.length === 1 && /[A-Z]/.test(clean)) { const index = clean.charCodeAt(0) - 65; if (options[index]) { return { success: true, index: index, option: options[index] }; } } // 多字母答案(多选题) if (/^[A-Z]{2,4}$/.test(clean)) { // 返回第一个有效选项 for (const char of clean) { const index = char.charCodeAt(0) - 65; if (options[index]) { return { success: true, index: index, option: options[index] }; } } } } return { success: false }; }, // 相似度匹配 similarMatch(answers, options, originalOptions, threshold) { let bestIndex = -1, bestScore = 0; for (let i = 0; i < options.length; i++) { for (const answer of answers) { const score = compareTwoStrings(options[i], answer); if (score >= threshold && score > bestScore) { bestScore = score; bestIndex = i; } } } if (bestIndex !== -1) { return { success: true, index: bestIndex, option: originalOptions[bestIndex], score: bestScore }; } return { success: false }; }, // 判断题匹配 - 同步 jinmu.js 的完整关键词和匹配算法 judgementMatch(answers, options) { // jinmu.js 完整的判断题关键词 const yesWords = ['是', '对', '正确', '确定', '√', '对的', '是的', '正确的', 'true', 'True', 'T', 'yes', '1']; const noWords = ['非', '否', '错', '错误', '×', 'X', '错的', '不对', '不正确的', '不正确', '不是', '不是的', 'false', 'False', 'F', 'no', '0']; // matches 函数 - 同步 jinmu.js 的匹配逻辑 const matches = (target, wordList) => { const cleanTarget = this.cleanText(target); return wordList.some(word => { const cleanWord = this.cleanText(word); return cleanTarget === cleanWord; }); }; // 遍历所有答案 for (const answersGroup of [answers]) { // 查找答案中是否包含正确词或错误词 const answerShowCorrect = answersGroup.find(answer => matches(answer, yesWords)); const answerShowIncorrect = answersGroup.find(answer => matches(answer, noWords)); if (answerShowCorrect || answerShowIncorrect) { let option; for (const el of options) { const optText = el.innerText || el.textContent || ''; const textShowCorrect = matches(optText, yesWords); const textShowIncorrect = matches(optText, noWords); if (answerShowCorrect && textShowCorrect) { option = el; const index = Array.from(options).indexOf(el); return { success: true, index: index, option: option, confidence: 98 }; } if (answerShowIncorrect && textShowIncorrect) { option = el; const index = Array.from(options).indexOf(el); return { success: true, index: index, option: option, confidence: 95 }; } } } } return { success: false }; }, // 多选题匹配(jinmu.js同款双重匹配策略 + 智能评分排序) multipleMatch(answers, options) { const optionStrings = options.map(o => { const text = o.innerText || o.textContent || ''; return StringUtil.removeRedundant(text); }); // 分割答案 let allAnswers = []; for (const answer of answers) { const parts = AdvancedAnswerEngine.splitAnswer(answer.trim()); allAnswers = allAnswers.concat(parts); } // ========== 策略1:精确包含匹配 ========== const containsMatch = { options: [], answers: [], similarSum: 0, similarCount: 0 }; for (let i = 0; i < options.length; i++) { const optText = optionStrings[i]; const found = allAnswers.find(a => { const aLower = a.toLowerCase(); const oLower = optText.toLowerCase(); return aLower.includes(oLower) || oLower.includes(aLower); }); if (found) { containsMatch.options.push(options[i]); containsMatch.answers.push(found); containsMatch.similarCount++; containsMatch.similarSum += 1.0; // 精确匹配相似度1.0 } } // ========== 策略2:相似度匹配(阈值 0.6)========== const similarMatch = { options: [], answers: [], similarSum: 0, similarCount: 0 }; const similarResult = StringUtil.answerSimilar(allAnswers, optionStrings); for (let j = 0; j < similarResult.length; j++) { const rating = similarResult[j]; if (rating && rating.rating > 0.6) { similarMatch.options.push(options[j]); similarMatch.answers.push(rating.target); similarMatch.similarCount++; similarMatch.similarSum += rating.rating; } } // ========== 策略3:双重比较,取最优结果 ========== // 评分公式:最终得分 = 匹配数量 × 100 + 相似度总和 const containsScore = containsMatch.similarCount * 100 + containsMatch.similarSum; const similarScore = similarMatch.similarCount * 100 + similarMatch.similarSum; let bestMatch = null; let bestMethod = ''; let bestConfidence = 0; if (containsMatch.similarCount > 0 && similarMatch.similarCount > 0) { // 两者都有结果,取分数高的 if (containsScore >= similarScore) { bestMatch = containsMatch; bestMethod = 'contains'; bestConfidence = 95; } else { bestMatch = similarMatch; bestMethod = 'similar'; bestConfidence = 80; } } else if (containsMatch.similarCount > 0) { bestMatch = containsMatch; bestMethod = 'contains'; bestConfidence = 95; } else if (similarMatch.similarCount > 0) { bestMatch = similarMatch; bestMethod = 'similar'; bestConfidence = 80; } if (bestMatch) { const indices = bestMatch.options.map(opt => options.indexOf(opt)).filter(i => i >= 0); if (indices.length > 0) { return { success: true, indices: indices, answers: bestMatch.answers, method: bestMethod, confidence: bestConfidence }; } } // ========== 策略4:plainAnswer 处理(纯字母答案,如 ABCD)========== for (const answer of answers) { const plainAnswer = StringUtil.resolvePlainAnswer(answer); if (plainAnswer) { const plainIndices = []; const plainAnswerList = []; for (const char of plainAnswer) { const index = char.charCodeAt(0) - 65; // A=0, B=1, etc. if (index >= 0 && index < options.length) { plainIndices.push(index); plainAnswerList.push(options[index].innerText || options[index].textContent || ''); } } if (plainIndices.length > 0) { return { success: true, indices: plainIndices, answers: plainAnswerList, method: 'plain-answer', confidence: 95 }; } } } return { success: false }; }, // 互斥选项分析 mutexAnalysis(question, options) { const antonymPairs = [ ['增加', '减少'], ['上升', '下降'], ['提高', '降低'], ['正确', '错误'], ['对', '错'], ['是', '否'], ['大于', '小于'], ['正相关', '负相关'], ['促进', '抑制'], ['扩大', '缩小'], ['加速', '减速'], ['增强', '减弱'], ['支持', '反对'], ['肯定', '否定'], ['有利', '不利'], ['主要', '次要'], ['必然', '偶然'], ['绝对', '相对'] ]; const cleanQuestion = this.cleanText(question); const optionTexts = options.map(o => this.cleanText(o.innerText || o.textContent || o)); for (const [a, b] of antonymPairs) { const hasA = optionTexts.some(o => o.includes(a)); const hasB = optionTexts.some(o => o.includes(b)); if (hasA && hasB) { if (cleanQuestion.includes(a)) { const idx = optionTexts.findIndex(o => o.includes(a)); if (idx >= 0) { return { success: true, index: idx, option: options[idx] }; } } else if (cleanQuestion.includes(b)) { const idx = optionTexts.findIndex(o => o.includes(b)); if (idx >= 0) { return { success: true, index: idx, option: options[idx] }; } } } } return { success: false }; }, // 绝对词排除 absoluteWordExclusion(options) { const absoluteWords = ['一定', '必须', '都', '所有', '任何', '完全', '绝对', '总是', '永远', '从不']; const optionTexts = options.map(o => this.cleanText(o.innerText || o.textContent || o)); const nonAbsoluteOptions = options.filter((opt, idx) => { return !absoluteWords.some(word => optionTexts[idx].includes(word)); }); if (nonAbsoluteOptions.length > 0 && nonAbsoluteOptions.length < options.length) { return { success: true, index: options.indexOf(nonAbsoluteOptions[0]), option: nonAbsoluteOptions[0] }; } return { success: false }; }, // 最长选项匹配 longestOptionMatch(options) { let longestIdx = 0, maxLen = 0; options.forEach((opt, idx) => { const text = opt.innerText || opt.textContent || opt; const len = this.cleanText(text).length; if (len > maxLen) { maxLen = len; longestIdx = idx; } }); // 确保最长选项明显更长 const secondLen = options.map((o, i) => i !== longestIdx ? this.cleanText(o.innerText || o.textContent || o).length : 0) .reduce((a, b) => Math.max(a, b), 0); if (maxLen > secondLen * 1.2 && maxLen > 8) { return { success: true, index: longestIdx, option: options[longestIdx] }; } return { success: false }; }, // 关键词匹配 keywordMatch(question, options) { const keyTerms = question.match(/[\u4e00-\u9fa5]{2,}/g) || []; const filteredTerms = keyTerms.filter(term => { return !['的是', '下列', '以下', '哪个', '说法', '正确', '错误', '关于'].includes(term); }); if (filteredTerms.length === 0) { return { success: false }; } const optionScores = options.map((opt, idx) => { const text = opt.innerText || opt.textContent || opt; const cleanOpt = this.cleanText(text); let score = 0; for (const term of filteredTerms) { if (cleanOpt.includes(term)) { score += term.length; } } return { idx, score }; }); optionScores.sort((a, b) => b.score - a.score); if (optionScores[0].score > 3 && optionScores[0].score > optionScores[1].score) { return { success: true, index: optionScores[0].idx, option: options[optionScores[0].idx] }; } return { success: false }; }, // 语义验证 validateSemantic(question, answers, option) { const contradictions = [ ['正确', '错误'], ['对', '错'], ['是', '否'], ['true', 'false'], ['√', '×'], ['增加', '减少'], ['上升', '下降'], ['提高', '降低'], ['促进', '抑制'] ]; for (const answer of answers) { for (const [a, b] of contradictions) { if ((option.includes(a) && answer.includes(b)) || (option.includes(b) && answer.includes(a))) { return false; } } } return true; } }; // ========== 智能答题引擎(终极版)========== // 机器学习训练数据存储 const ML_TRAINING_DATA_KEY = '_quiz_ml_training'; // 智能学习引擎 const LearningEngine = { // 记录答题结果用于学习 recordAnswer(question, selectedAnswer, isCorrect) { try { const data = JSON.parse(localStorage.getItem(ML_TRAINING_DATA_KEY) || '[]'); data.push({ question: question.title, questionType: question.type, selectedAnswer: selectedAnswer, isCorrect: isCorrect, timestamp: Date.now() }); // 只保留最近1000条记录 if (data.length > 1000) { data.shift(); } localStorage.setItem(ML_TRAINING_DATA_KEY, JSON.stringify(data)); } catch (e) { console.error('记录学习数据失败:', e); } }, // 分析学习数据,找出易错题目 analyzeWeakPoints() { const data = JSON.parse(localStorage.getItem(ML_TRAINING_DATA_KEY) || '[]'); const mistakes = data.filter(d => !d.isCorrect); const mistakeMap = {}; mistakes.forEach(m => { const key = m.question; if (!mistakeMap[key]) { mistakeMap[key] = { count: 0, wrongAnswers: [] }; } mistakeMap[key].count++; mistakeMap[key].wrongAnswers.push(m.selectedAnswer); }); return Object.entries(mistakeMap) .map(([q, info]) => ({ question: q, mistakeCount: info.count, wrongAnswers: info.wrongAnswers })) .sort((a, b) => b.mistakeCount - a.mistakeCount); }, // 获取用户答题准确率 getAccuracy() { const data = JSON.parse(localStorage.getItem(ML_TRAINING_DATA_KEY) || '[]'); if (data.length === 0) return 0; const correct = data.filter(d => d.isCorrect).length; return Math.round((correct / data.length) * 100); } }; // 高级语义分析引擎 const SemanticAnalyzer = { // 否定词列表 NEGATION_WORDS: ['不', '否', '无', '非', '未', '没', '勿', '禁止', '错误', '不是', '没有', '不能', '不可'], // 程度副词 DEGREE_WORDS: { absolute: ['完全', '绝对', '一定', '必须', '全部', '所有', '任何', '总是', '永远'], moderate: ['可能', '也许', '大概', '通常', '一般', '有时', '部分', '有些'], weak: ['略微', '稍微', '有点', '少许'] }, // 关键词提取 extractKeywords(text) { const cleanText = text.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, ' ').trim(); const words = cleanText.split(/\s+/).filter(w => w.length >= 2); // 过滤停用词 const stopWords = ['的', '是', '在', '有', '和', '了', '我', '你', '他', '她', '它', '这', '那', '什么', '怎么', '为什么', '因为', '所以', '但是', '如果', '虽然', '但是', '然而', '而且', '还是', '或者', '以及', '等等', '可以', '应该', '需要', '可能', '会', '要', '能', '不', '很', '也', '都', '就', '还', '又', '再', '更', '最', '太', '非常', '十分', '特别', '稍微', '比较', '相当', '确实', '其实', '根本', '简直', '几乎', '差不多', '大概', '大约', '左右', '上下', '之间', '以上', '以下', '之前', '之后', '目前', '现在', '已经', '正在', '曾经', '将会', '可能', '应该', '必须', '可以', '需要', '希望', '打算', '计划', '准备']; return words.filter(w => !stopWords.includes(w)); }, // 检测否定结构 detectNegation(text) { let negationCount = 0; this.NEGATION_WORDS.forEach(word => { const regex = new RegExp(word, 'g'); const matches = text.match(regex); if (matches) negationCount += matches.length; }); return negationCount % 2 === 1; // 奇数表示否定 }, // 分析选项与题目的语义关系 analyzeRelationship(question, option) { const qKeywords = this.extractKeywords(question); const oKeywords = this.extractKeywords(option); // 计算关键词重叠度 const overlap = qKeywords.filter(k => oKeywords.includes(k)).length; const similarity = overlap / Math.max(qKeywords.length, oKeywords.length); // 检测否定冲突 const qNegated = this.detectNegation(question); const oNegated = this.detectNegation(option); const hasNegationConflict = qNegated !== oNegated; // 检测程度词匹配 let degreeMatch = 0; const qHasAbsolute = this.DEGREE_WORDS.absolute.some(w => question.includes(w)); const qHasModerate = this.DEGREE_WORDS.moderate.some(w => question.includes(w)); const oHasAbsolute = this.DEGREE_WORDS.absolute.some(w => option.includes(w)); const oHasModerate = this.DEGREE_WORDS.moderate.some(w => option.includes(w)); if ((qHasAbsolute && oHasAbsolute) || (qHasModerate && oHasModerate)) { degreeMatch = 1; } else if ((qHasAbsolute && !oHasAbsolute) || (qHasModerate && !oHasModerate)) { degreeMatch = -0.5; } return { keywordSimilarity: similarity, hasNegationConflict: hasNegationConflict, degreeMatch: degreeMatch, score: similarity * 0.6 + (hasNegationConflict ? -0.3 : 0.1) + degreeMatch * 0.3 }; }, // 智能推断答案(当无匹配时使用) inferAnswer(question, options) { const results = options.map((opt, idx) => { const rel = this.analyzeRelationship(question, opt.innerText || opt.textContent || opt); return { index: idx, option: opt, ...rel }; }); // 按分数排序 results.sort((a, b) => b.score - a.score); // 如果有明显最优解 if (results.length > 1 && results[0].score - results[1].score > 0.2) { return { success: true, index: results[0].index, option: results[0].option, confidence: Math.min(70 + results[0].score * 30, 95) }; } return null; } }; // 题库增强器 - 自动扩展题库 const QuizEnhancer = { // 同义词扩展 SYNONYM_EXPANSION: { '正确': ['准确', '无误', '对的', '恰当', '合适', '正确的', '准确的'], '错误': ['不正确', '错误的', '有误', '错的', '不对', '差错'], '增加': ['增多', '增长', '上升', '提高', '增多', '增加了'], '减少': ['降低', '下降', '减少了', '缩减', '变少'], '重要': ['关键', '核心', '主要', '重要的', '首要'], '方法': ['方式', '手段', '途径', '办法', '措施'], '原因': ['因素', '缘由', '起因', '理由', '缘故'], '结果': ['后果', '成果', '结局', '结论', '产物'], '影响': ['作用', '效果', '效应', '影响因素'], '特点': ['特征', '特性', '特色', '属性', '特质'] }, // 自动生成变体题目 generateVariants(question) { const variants = []; const title = question.title; // 同义词替换生成变体 Object.entries(this.SYNONYM_EXPANSION).forEach(([target, sources]) => { sources.forEach(source => { if (title.includes(source)) { const variantTitle = title.replace(source, target); if (variantTitle !== title) { variants.push({ title: variantTitle, answers: question.answers, type: question.type, source: 'variant' }); } } }); }); return variants; }, // 增强题库 enhanceQuizCache() { const cache = JSON.parse(localStorage.getItem('_quiz_cache') || '[]'); const enhanced = [...cache]; cache.forEach(quiz => { const variants = this.generateVariants(quiz); variants.forEach(variant => { if (!enhanced.some(q => q.title === variant.title)) { enhanced.push(variant); } }); }); if (enhanced.length > cache.length) { localStorage.setItem('_quiz_cache', JSON.stringify(enhanced)); console.log(`[学习通助手] 题库增强完成,新增 ${enhanced.length - cache.length} 条变体题目`); } } }; // 综合答题策略(终极版) const UltimateAnswerEngine = { // 置信度阈值配置 THRESHOLDS: { exactMatch: 100, highConfidence: 90, mediumConfidence: 70, lowConfidence: 50 }, // 主答题函数 async solve(question) { const logStore = useLogStore(); if (!question || !question.title) { return buildErrorResponse('题目数据无效'); } const cleanTitle = question.title.replace(/<[^>]*>/g, '').trim(); // ========== 阶段1:精确匹配(最高优先级)========== logStore.addLog('阶段1: 精确匹配查询', 'info'); const exactResult = await this.exactMatch(question); if (exactResult) { logStore.addLog(`答题成功 [精确匹配] 置信度: ${exactResult.data.confidence}%`, 'success'); return exactResult; } // ========== 阶段2:高置信度模糊匹配 ========== logStore.addLog('阶段2: 高置信度模糊匹配', 'info'); const highResult = await this.highConfidenceMatch(question); if (highResult) { logStore.addLog(`答题成功 [高置信度匹配] 置信度: ${highResult.data.confidence}%`, 'success'); return highResult; } // ========== 阶段3:语义分析推断 ========== logStore.addLog('阶段3: 语义分析推断', 'info'); const semanticResult = this.semanticInference(question); if (semanticResult) { logStore.addLog(`答题成功 [语义推断] 置信度: ${semanticResult.data.confidence}%`, 'info'); return semanticResult; } // ========== 阶段4:智能策略组合 ========== logStore.addLog('阶段4: 智能策略组合', 'info'); const strategyResult = this.combinedStrategy(question); if (strategyResult) { logStore.addLog(`答题成功 [策略组合] 置信度: ${strategyResult.data.confidence}%`, 'info'); return strategyResult; } // ========== 阶段5:机器学习辅助 ========== logStore.addLog('阶段5: 机器学习辅助', 'info'); const mlResult = this.mlAssistedMatch(question); if (mlResult) { logStore.addLog(`答题成功 [ML辅助] 置信度: ${mlResult.data.confidence}%`, 'info'); return mlResult; } // 所有策略失败 logStore.addLog('所有答题策略均失败', 'error'); return buildErrorResponse('未能找到答案'); }, // 精确匹配 async exactMatch(question) { // 本地缓存精确匹配 const cacheResult = queryLocalQuizCache(question); if (cacheResult && cacheResult.data.confidence >= 98) { return cacheResult; } // 云端题库精确匹配 const cloudResult = await fetchFromCloudQuiz(question); if (cloudResult && cloudResult.data.confidence >= 95) { return cloudResult; } return null; }, // 高置信度模糊匹配 async highConfidenceMatch(question) { const options = question.optionsText || []; const storedAnswers = []; // 收集所有可能的答案来源 const cache = JSON.parse(localStorage.getItem('_quiz_cache') || '[]'); cache.forEach(quiz => { const similarity = compareTwoStrings( question.title.replace(/<[^>]*>/g, ''), quiz.title.replace(/<[^>]*>/g, '') ); if (similarity > 0.8) { storedAnswers.push(...quiz.answers); } }); if (storedAnswers.length === 0) return null; // 使用增强器进行匹配 const result = AnswerEnhancer.matchAnswer( question.title, options, storedAnswers ); if (result.success && result.confidence >= this.THRESHOLDS.highConfidence) { return { code: 200, msg: '高置信度匹配成功', data: { answer: [options[result.index]], source: 'high-confidence', num: result.index, confidence: result.confidence } }; } return null; }, // 语义分析推断 semanticInference(question) { const options = question.optionsText || []; if (options.length === 0) return null; const result = SemanticAnalyzer.inferAnswer(question.title, options); if (result && result.success) { return { code: 200, msg: '语义推断成功', data: { answer: [options[result.index]], source: 'semantic', num: result.index, confidence: result.confidence } }; } return null; }, // 智能策略组合 combinedStrategy(question) { const options = question.optionsText || []; if (options.length === 0) return null; const scores = options.map((opt, idx) => { let score = 0; // 策略1:关键词匹配 const keywords = SemanticAnalyzer.extractKeywords(question.title); const optText = opt.innerText || opt.textContent || opt; const optKeywords = SemanticAnalyzer.extractKeywords(optText); const keywordMatch = keywords.filter(k => optKeywords.includes(k)).length; score += keywordMatch * 10; // 策略2:否定词检测 if (!SemanticAnalyzer.detectNegation(optText)) { score += 5; } // 策略3:绝对词检测 const hasAbsolute = SemanticAnalyzer.DEGREE_WORDS.absolute.some(w => optText.includes(w)); if (!hasAbsolute) { score += 10; } // 策略4:长度分析 const len = optText.length; if (len > 10 && len < 100) { score += len / 10; } // 策略5:包含数字(通常更具体) if (/\d+/.test(optText)) { score += 8; } return { index: idx, score, option: opt }; }); scores.sort((a, b) => b.score - a.score); // 确保有明显的领先者 if (scores.length > 1 && scores[0].score > scores[1].score * 1.3) { const confidence = Math.min(60 + scores[0].score / 2, 85); return { code: 200, msg: '策略组合匹配成功', data: { answer: [options[scores[0].index]], source: 'combined-strategy', num: scores[0].index, confidence: confidence } }; } return null; }, // 机器学习辅助匹配 mlAssistedMatch(question) { const trainingData = JSON.parse(localStorage.getItem(ML_TRAINING_DATA_KEY) || '[]'); if (trainingData.length === 0) return null; const options = question.optionsText || []; if (options.length === 0) return null; // 查找相似题目 const similarQuestions = trainingData.filter(d => { const similarity = compareTwoStrings(question.title, d.question); return similarity > 0.7; }); if (similarQuestions.length === 0) return null; // 分析正确答案模式 const answerCounts = {}; similarQuestions.forEach(d => { if (d.isCorrect) { answerCounts[d.selectedAnswer] = (answerCounts[d.selectedAnswer] || 0) + 1; } }); // 找到最常见的正确答案 const sortedAnswers = Object.entries(answerCounts).sort((a, b) => b[1] - a[1]); if (sortedAnswers.length > 0) { const bestAnswer = sortedAnswers[0][0]; // 在选项中查找匹配 for (let i = 0; i < options.length; i++) { const optText = options[i].innerText || options[i].textContent || options[i]; if (optText.includes(bestAnswer) || bestAnswer.includes(optText)) { return { code: 200, msg: '机器学习辅助匹配成功', data: { answer: [optText], source: 'ml-assisted', num: i, confidence: Math.min(60 + sortedAnswers[0][1] * 10, 80) } }; } } } return null; } }; // 主答题入口函数 const findAnswer = async (question) => { return UltimateAnswerEngine.solve(question); }; // 定时同步和增强题库 const scheduleQuizSync = () => { // 启动时同步和增强 syncQuizCache(); setTimeout(() => { QuizEnhancer.enhanceQuizCache(); }, 5000); // 每30分钟同步一次 setInterval(() => { syncQuizCache(); setTimeout(() => { QuizEnhancer.enhanceQuizCache(); }, 3000); }, 30 * 60 * 1000); }; // 初始化学习引擎 const initLearningEngine = () => { // 加载扩展题库 if (typeof window !== 'undefined' && window.EXTENDED_QUIZ_DATABASE) { const existingCache = JSON.parse(localStorage.getItem('_quiz_cache') || '[]'); const newQuizzes = []; Object.values(window.EXTENDED_QUIZ_DATABASE).forEach(subjectQuizzes => { subjectQuizzes.forEach(quiz => { const exists = existingCache.some(existing => existing.title === quiz.question ); if (!exists) { newQuizzes.push({ title: quiz.question, answers: quiz.answers, type: quiz.type }); } }); }); if (newQuizzes.length > 0) { const mergedCache = [...existingCache, ...newQuizzes]; localStorage.setItem('_quiz_cache', JSON.stringify(mergedCache)); console.log(`[学习通助手] 已加载 ${newQuizzes.length} 条扩展题库数据`); } } }; // ========== 逆向破解的答题接口 ========== // 接口1:new.js 的第三方题库接口 (wanjuan.tk) // 统一答案解析函数 - 优化的答案解析逻辑 const parseAnswers = (answerContent, questionType) => { if (!answerContent) return []; const tmType = parseInt(questionType || '0'); let answers = []; try { // 首先尝试JSON解析(支持数组格式) try { const parsed = JSON.parse(answerContent); if (Array.isArray(parsed)) { answers = parsed.map(a => String(a).trim()).filter(a => a); if (answers.length > 0) { return answers; } } } catch (e) { // JSON解析失败,继续其他方式 } const answerText = String(answerContent).trim(); // 根据题型处理答案 if (tmType === 0 || tmType === 3) { // 单选题/判断题:直接返回答案 answers = [answerText]; } else if (tmType === 1) { // 多选题:支持多种分隔格式(优先级从高到低) const separators = [ '#', '##', ',', ',', ';', ';', '、', ' ', '\t', '\n', '|', '||', '---' ]; // 尝试每种分隔符 for (const sep of separators) { if (answerText.includes(sep)) { const parts = answerText.split(sep).filter(a => a.trim()); if (parts.length > 0) { answers = parts; break; } } } // 如果没有找到分隔符,尝试连续字母格式(如 "ABD") if (answers.length === 0 && /^[A-Za-z]+$/.test(answerText)) { answers = answerText.split('').filter(a => a.trim()); } // 仍然没有,直接使用单个答案 if (answers.length === 0) { answers = [answerText]; } } else if (tmType === 2) { // 填空题:支持多种分隔格式 if (answerText.includes('#!#')) { answers = answerText.split('#!#').filter(a => a.trim()); } else if (answerText.includes('#')) { answers = answerText.split('#').filter(a => a.trim()); } else { answers = [answerText]; } } else { // 默认类型:直接使用 answers = [answerText]; } // 清理答案中的HTML标签和多余空白 answers = answers.map(a => a.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim() ).filter(a => a); } catch (e) { console.warn('[parseAnswers] 解析答案失败:', e); answers = [String(answerContent).trim()].filter(a => a); } return answers; }; // 增强版置信度计算函数 - 根据答案与题目的实际匹配程度计算置信度 const getEnhancedConfidence = (source, answerContent, question, parsedAnswers) => { const logStore = useLogStore(); const confidences = { 'wanjuan-tk': 88, 'lyck6-free': 82, 'datam-free': 75, 'apibyte-free': 73, 'wkexam-free': 77, 'hive-net-free': 73, 'sh-api-free': 70, 'yxykw-free': 67, 'datam-site': 75, 'datam-site-free': 77, 'shuashuati-free': 70, 'ai-xueti-free': 73 }; // 基本置信度 let confidence = confidences[source] || 70; // 答案有效性检查 if (!answerContent || !parsedAnswers || parsedAnswers.length === 0) { confidence -= 20; return Math.max(30, confidence); } const answerLen = String(answerContent || '').length; // 答案长度检查(异常长度的答案可能有问题) if (answerLen < 1 || answerLen > 500) { confidence -= 15; } // 如果有题目和选项,进行实际匹配度验证 if (question && question.title && question.optionsText && question.optionsText.length > 0) { const cleanQuestion = question.title.replace(/<[^>]*>/g, '').trim(); const options = question.optionsText.map(o => o.replace(/<[^>]*>/g, '').trim()); // 使用AdvancedAnswerEngine的jinmuCompareTwoStrings算法计算匹配度 let matchScore = 0; let matchCount = 0; for (const answer of parsedAnswers) { if (!answer || !answer.trim()) continue; // 计算答案与每个选项的相似度 for (const option of options) { if (!option || !option.trim()) continue; // 使用bigram相似度算法(来自jinmu.js) const similarity = AdvancedAnswerEngine.jinmuCompareTwoStrings( answer.trim().toLowerCase(), option.trim().toLowerCase() ); if (similarity > matchScore) { matchScore = similarity; } // 如果是精确匹配或高度相似 if (similarity >= 0.9) { matchCount++; } } } // 根据匹配度调整置信度 if (matchScore >= 0.9) { // 高度匹配,置信度保持或提升 confidence = Math.min(95, confidence + 5); } else if (matchScore >= 0.7) { // 中度匹配,置信度保持 confidence = confidence; } else if (matchScore >= 0.5) { // 低度匹配,降低置信度 confidence -= 10; } else { // 几乎不匹配,大幅降低置信度 confidence -= 25; } // 如果有多个选项被匹配,增加置信度 if (matchCount >= 2) { confidence = Math.min(95, confidence + 5); } // ========== 多选题匹配完整性检查 ========== // 对于多选题,需要验证所有解析出的答案都能找到匹配的选项 const validAnswers = parsedAnswers.filter(a => a && a.trim()); if (question.type === '1' && validAnswers.length > 1) { // 多选题 const matchedAnswersCount = validAnswers.filter(answer => { if (!answer || !answer.trim()) return false; return options.some(option => { if (!option || !option.trim()) return false; const similarity = AdvancedAnswerEngine.jinmuCompareTwoStrings( answer.trim().toLowerCase(), option.trim().toLowerCase() ); return similarity >= 0.7; // 至少70%相似度才算匹配 }); }).length; const matchRate = matchedAnswersCount / validAnswers.length; if (matchRate < 0.5) { // 匹配不完整(不到50%的答案能匹配到选项),大幅降低置信度 confidence -= 30; if (logStore) { logStore.addLog(`[置信度验证] 多选题匹配不完整: 有效答案${validAnswers.length}个, 匹配${matchedAnswersCount}个, 匹配率${(matchRate * 100).toFixed(1)}%`, 'warning'); } } else if (matchRate < 0.8) { // 部分匹配,适度降低置信度 confidence -= 15; if (logStore) { logStore.addLog(`[置信度验证] 多选题部分匹配: 有效答案${validAnswers.length}个, 匹配${matchedAnswersCount}个, 匹配率${(matchRate * 100).toFixed(1)}%`, 'info'); } } } // 记录调试信息 if (logStore) { logStore.addLog(`[置信度验证] 题目匹配度: ${(matchScore * 100).toFixed(1)}%, 匹配选项数: ${matchCount}`, 'info'); } } // 使用历史正确率进行校准(如果有) const sourceKey = `confidence_history_${source}`; try { const historyData = JSON.parse(localStorage.getItem(sourceKey) || '{"total":0,"correct":0}'); if (historyData.total >= 10) { const historicalAccuracy = historyData.correct / historyData.total; // 如果历史正确率与当前置信度差异超过20%,进行校准 const confidenceDiff = Math.abs(confidence / 100 - historicalAccuracy); if (confidenceDiff > 0.2) { // 根据历史表现调整置信度 confidence = Math.round(historicalAccuracy * 100); if (logStore) { logStore.addLog(`[置信度校准] 根据历史正确率(${historicalAccuracy.toFixed(1)})调整为: ${confidence}%`, 'info'); } } } } catch (e) { // 忽略历史数据解析错误 } return Math.max(25, Math.min(95, confidence)); }; // 记录答题结果用于置信度校准 const recordAnswerResult = (source, isCorrect) => { if (!source) return; const sourceKey = `confidence_history_${source}`; try { const historyData = JSON.parse(localStorage.getItem(sourceKey) || '{"total":0,"correct":0}'); historyData.total++; if (isCorrect) { historyData.correct++; } // 保留最近100条记录 if (historyData.total > 100) { historyData.total = Math.max(10, Math.round(historyData.total * 0.9)); historyData.correct = Math.round(historyData.correct * 0.9); } localStorage.setItem(sourceKey, JSON.stringify(historyData)); } catch (e) { // 忽略错误 } }; // 获取题库历史正确率 const getSourceAccuracy = (source) => { if (!source) return null; const sourceKey = `confidence_history_${source}`; try { const historyData = JSON.parse(localStorage.getItem(sourceKey) || '{"total":0,"correct":0}'); if (historyData.total >= 5) { return { total: historyData.total, correct: historyData.correct, accuracy: (historyData.correct / historyData.total * 100).toFixed(1) }; } } catch (e) { // 忽略错误 } return null; }; const fetchFromWanjuanTK = async (question, token) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } if (!token || token.length < 32) { // token无效,直接返回null,不记录日志 return null; } const questionType = question.type || '0'; let tmType = parseInt(questionType); if (tmType === 3) tmType = 3; // 判断题 else if (tmType === 2) tmType = 2; // 填空题 else if (tmType === 1) tmType = 1; // 多选题 else tmType = 0; // 单选题 const optionsText = question.optionsText || []; const optionsJson = JSON.stringify(optionsText); const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const postData = 'tm=' + encodeURIComponent(cleanTitle) + '&type=' + tmType + '&answernum=0' + '&options=' + encodeURIComponent(optionsJson) + '&ai=0' + '&coursename=' + encodeURIComponent(''); const tkApiHost = 'https://tk.wanjuantiku.com/'; const tkApiUrl = tkApiHost + 'api/query?token=' + token + '&version=2.8.4'; // 不记录正在调用的日志,避免干扰 return new Promise((resolve) => { _GM_xmlhttpRequest({ url: tkApiUrl, method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: postData, timeout: 20000, onload: (response) => { try { // 验证 HTTP 状态码 if (response.status < 200 || response.status >= 300) { resolve(null); return; } // 验证响应内容 if (!response.responseText || typeof response.responseText !== 'string') { resolve(null); return; } // 解析 JSON let tkResult; try { tkResult = JSON.parse(response.responseText); } catch (e) { resolve(null); return; } // 验证响应结构 if (!tkResult || typeof tkResult !== 'object') { resolve(null); return; } // 检查业务状态码 if (tkResult.code !== 1) { // 不记录错误日志,避免干扰 resolve(null); return; } // 验证答案字段 const realAnswer = tkResult.data; if (!realAnswer || typeof realAnswer !== 'string') { resolve(null); return; } logStore.addLog(`第三方题库匹配成功: ${realAnswer}`, 'success'); // 使用统一的答案解析函数 const answers = parseAnswers(realAnswer, questionType); // 验证答案数组不为空 if (answers.length === 0) { resolve(null); return; } // 使用增强版置信度计算(考虑答案与题目的实际匹配度) const confidence = getEnhancedConfidence('wanjuan-tk', realAnswer, question, answers); resolve({ code: 200, msg: '第三方题库匹配成功', data: { answer: answers, source: 'wanjuan-tk', num: typeof tkResult.left === 'number' ? tkResult.left : -1, confidence: confidence } }); } catch (e) { resolve(null); } }, onerror: () => { resolve(null); }, ontimeout: () => { resolve(null); } }); }); }; // 接口2:lyck6.cn 免费题库接口(GET) const fetchFromLyck6 = async (question) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const apiUrl = `http://cx.lyck6.cn/api/api.php?question=${encodeURIComponent(cleanTitle)}`; return new Promise((resolve) => { _GM_xmlhttpRequest({ url: apiUrl, method: 'GET', headers: {}, timeout: 10000, onload: (response) => { try { if (response.status < 200 || response.status >= 300) { resolve(null); return; } const data = JSON.parse(response.responseText); // lyck6 API返回格式: {code: 1, answer: "答案内容"} 或 {code: 0} if (!data || data.code !== 1 || !data.answer) { resolve(null); return; } const answerText = String(data.answer).trim(); if (!answerText) { resolve(null); return; } logStore.addLog(`lyck6题库匹配成功: ${answerText}`, 'success'); // 使用统一的答案解析函数 const questionType = question.type || '0'; const answers = parseAnswers(answerText, questionType); // 验证答案数组不为空 if (answers.length === 0) { resolve(null); return; } const confidence = getEnhancedConfidence('lyck6-free', answerText, question, answers); resolve({ code: 200, msg: 'lyck6题库匹配成功', data: { answer: answers, source: 'lyck6-free', num: -1, confidence: confidence } }); } catch (e) { resolve(null); } }, onerror: () => { resolve(null); }, ontimeout: () => { resolve(null); } }); }); }; // 接口3:datam.site 免费题库接口(GitHub: destoryD/chaoxing-api) const fetchFromWkapi = async (question) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const apiUrl = `https://www.datam.site/api/search?question=${encodeURIComponent(cleanTitle)}`; return new Promise((resolve) => { _GM_xmlhttpRequest({ url: apiUrl, method: 'GET', headers: {}, timeout: 10000, onload: (response) => { try { if (response.status < 200 || response.status >= 300) { resolve(null); return; } const data = JSON.parse(response.responseText); // datam.site API返回格式: {code: 200, data: {answer: "答案"}} 或 {code: 1, answer: "答案"} let answerText = null; if (data.code === 200 && data.data && data.data.answer) { answerText = String(data.data.answer).trim(); } else if (data.code === 1 && data.answer) { answerText = String(data.answer).trim(); } else if (data.answer) { answerText = String(data.answer).trim(); } if (!answerText) { resolve(null); return; } logStore.addLog(`datam题库匹配成功: ${answerText}`, 'success'); // 使用统一的答案解析函数 const questionType = question.type || '0'; const answers = parseAnswers(answerText, questionType); // 验证答案数组不为空 if (answers.length === 0) { resolve(null); return; } const confidence = getEnhancedConfidence('datam-free', answerText, question, answers); resolve({ code: 200, msg: 'datam题库匹配成功', data: { answer: answers, source: 'datam-free', num: -1, confidence: confidence } }); } catch (e) { resolve(null); } }, onerror: () => { resolve(null); }, ontimeout: () => { resolve(null); } }); }); }; // 接口4:apibyte.cn 教育搜题接口(无需认证,每日50次) const fetchFromApiByte = async (question) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const apiUrl = `https://apione.apibyte.cn/edusearch?platform=超星学习通&question=${encodeURIComponent(cleanTitle)}`; return new Promise((resolve) => { _GM_xmlhttpRequest({ url: apiUrl, method: 'GET', headers: {}, timeout: 10000, onload: (response) => { try { if (response.status < 200 || response.status >= 300) { resolve(null); return; } const data = JSON.parse(response.responseText); // apibyte API返回格式: {code: 200, data: {results: [{source: "来源", answer: ["答案"]}]}} if (!data || data.code !== 200 || !data.data || !data.data.results || data.data.results.length === 0) { resolve(null); return; } // 选择第一个结果 const firstResult = data.data.results[0]; let answers = []; let rawAnswer = null; if (firstResult.answer && Array.isArray(firstResult.answer)) { // 优先使用数组格式的答案 answers = firstResult.answer.filter(a => a && String(a).trim()).map(a => String(a).trim()); rawAnswer = answers.join(','); } else if (firstResult.data && firstResult.data.answer) { rawAnswer = String(firstResult.data.answer).trim(); // 使用统一的答案解析函数 answers = parseAnswers(rawAnswer, question.type); } else if (firstResult.answer) { rawAnswer = String(firstResult.answer).trim(); answers = parseAnswers(rawAnswer, question.type); } if (answers.length === 0) { resolve(null); return; } logStore.addLog(`apibyte题库匹配成功: ${answers.join(', ')}`, 'success'); const confidence = getEnhancedConfidence('apibyte-free', rawAnswer || answers.join(','), question, answers); resolve({ code: 200, msg: 'apibyte题库匹配成功', data: { answer: answers, source: 'apibyte-free', num: -1, confidence: confidence } }); } catch (e) { resolve(null); } }, onerror: () => { resolve(null); }, ontimeout: () => { resolve(null); } }); }); }; // 接口5:wkexam 网课考试题库(GitHub: gzsj/wkapi) const fetchFromWkexam = async (question) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const apiUrl = `http://api.wkexam.com/api/?token=qqqqq&q=${encodeURIComponent(cleanTitle)}`; return new Promise((resolve) => { _GM_xmlhttpRequest({ url: apiUrl, method: 'GET', headers: {}, timeout: 10000, onload: (response) => { try { if (response.status < 200 || response.status >= 300) { resolve(null); return; } const data = JSON.parse(response.responseText); // wkexam API返回格式: {code: 1, data: {question: "题目", answer: "答案"}} if (!data || data.code !== 1 || !data.data || !data.data.answer) { resolve(null); return; } const answerText = String(data.data.answer).trim(); if (!answerText) { resolve(null); return; } logStore.addLog(`wkexam题库匹配成功: ${answerText}`, 'success'); // 使用统一的答案解析函数 const questionType = question.type || '0'; const answers = parseAnswers(answerText, questionType); if (answers.length === 0) { resolve(null); return; } const confidence = getEnhancedConfidence('wkexam-free', answerText, question, answers); resolve({ code: 200, msg: 'wkexam题库匹配成功', data: { answer: answers, source: 'wkexam-free', num: -1, confidence: confidence } }); } catch (e) { resolve(null); } }, onerror: () => { resolve(null); }, ontimeout: () => { resolve(null); } }); }); }; // 接口6:hive-net 大学生网课题库(GitHub: ThinkerWen/OnlineCourseAPI) const fetchFromHiveNet = async (question) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const apiUrl = `https://www.hive-net.cn/backend/wangke/search?token=free&question=${encodeURIComponent(cleanTitle)}`; return new Promise((resolve) => { _GM_xmlhttpRequest({ url: apiUrl, method: 'GET', headers: {}, timeout: 10000, onload: (response) => { try { if (response.status < 200 || response.status >= 300) { resolve(null); return; } const data = JSON.parse(response.responseText); // hive-net API返回格式: {code: 0, data: {reasonList: [{reason: "答案"}]}} if (!data || data.code !== 0 || !data.data || !data.data.reasonList || data.data.reasonList.length === 0) { resolve(null); return; } const answerText = String(data.data.reasonList[0].reason).trim(); if (!answerText) { resolve(null); return; } logStore.addLog(`hive-net题库匹配成功: ${answerText}`, 'success'); // 使用统一的答案解析函数 const questionType = question.type || '0'; const answers = parseAnswers(answerText, questionType); if (answers.length === 0) { resolve(null); return; } const confidence = getEnhancedConfidence('hive-net-free', answerText, question, answers); resolve({ code: 200, msg: 'hive-net题库匹配成功', data: { answer: answers, source: 'hive-net-free', num: data.data.tokenRemainTimes || -1, confidence: confidence } }); } catch (e) { resolve(null); } }, onerror: () => { resolve(null); }, ontimeout: () => { resolve(null); } }); }); }; // 接口7:SH-API 免费无限制搜题接口 const fetchFromSHAPI = async (question) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const apiUrl = `https://api.yyy881.com/api/question?question=${encodeURIComponent(cleanTitle)}`; return new Promise((resolve) => { _GM_xmlhttpRequest({ url: apiUrl, method: 'GET', headers: {}, timeout: 10000, onload: (response) => { try { if (response.status < 200 || response.status >= 300) { resolve(null); return; } const data = JSON.parse(response.responseText); // SH-API 返回格式: {source: "shanhai™", status: "success", data: {question: "题目", answer: "答案"}} if (!data || data.status !== 'success' || !data.data || !data.data.answer) { resolve(null); return; } const answerText = String(data.data.answer).trim(); if (!answerText) { resolve(null); return; } logStore.addLog(`SH-API题库匹配成功: ${answerText}`, 'success'); // 使用统一的答案解析函数 const questionType = question.type || '0'; const answers = parseAnswers(answerText, questionType); if (answers.length === 0) { resolve(null); return; } const confidence = getEnhancedConfidence('sh-api-free', answerText, question, answers); resolve({ code: 200, msg: 'SH-API题库匹配成功', data: { answer: answers, source: 'sh-api-free', num: -1, confidence: confidence } }); } catch (e) { resolve(null); } }, onerror: () => { resolve(null); }, ontimeout: () => { resolve(null); } }); }); }; // 接口8:yxykw.com 网课查题接口 const fetchFromYxykw = async (question) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const apiUrl = `https://www.yxykw.com/API/answer?user=free&md5=free&question=${encodeURIComponent(cleanTitle)}`; return new Promise((resolve) => { _GM_xmlhttpRequest({ url: apiUrl, method: 'GET', headers: {}, timeout: 10000, onload: (response) => { try { if (response.status < 200 || response.status >= 300) { resolve(null); return; } const data = JSON.parse(response.responseText); // yxykw API返回格式: {status: 0, title: "题目", question: "题目", answer: "答案"} if (!data || data.status !== 0 || !data.answer) { resolve(null); return; } const answerText = String(data.answer).trim(); if (!answerText) { resolve(null); return; } logStore.addLog(`yxykw题库匹配成功: ${answerText}`, 'success'); // 使用统一的答案解析函数 const questionType = question.type || '0'; const answers = parseAnswers(answerText, questionType); if (answers.length === 0) { resolve(null); return; } const confidence = getEnhancedConfidence('yxykw-free', answerText, question, answers); resolve({ code: 200, msg: 'yxykw题库匹配成功', data: { answer: answers, source: 'yxykw-free', num: -1, confidence: confidence } }); } catch (e) { resolve(null); } }, onerror: () => { resolve(null); }, ontimeout: () => { resolve(null); } }); }); }; // 接口9:datam.site 免费题库接口(GitHub: destoryD/chaoxing-api) const fetchFromDatamSite = async (question) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const apiUrl = `https://www.datam.site/api/search?question=${encodeURIComponent(cleanTitle)}`; return new Promise((resolve) => { _GM_xmlhttpRequest({ url: apiUrl, method: 'GET', headers: {}, timeout: 10000, onload: (response) => { try { if (response.status < 200 || response.status >= 300) { resolve(null); return; } const data = JSON.parse(response.responseText); if (!data || !data.data || !data.data.answer) { resolve(null); return; } const answerText = String(data.data.answer).trim(); if (!answerText) { resolve(null); return; } logStore.addLog(`datam.site题库匹配成功: ${answerText}`, 'success'); // 使用统一的答案解析函数 const questionType = question.type || '0'; const answers = parseAnswers(answerText, questionType); if (answers.length === 0) { resolve(null); return; } const confidence = getEnhancedConfidence('datam-site-free', answerText, question, answers); resolve({ code: 200, msg: 'datam.site题库匹配成功', data: { answer: answers, source: 'datam-site-free', num: -1, confidence: confidence } }); } catch (e) { resolve(null); } }, onerror: () => { resolve(null); }, ontimeout: () => { resolve(null); } }); }); }; // 接口10:shuashuati.com 刷刷题免费题库接口 const fetchFromShuaShuaTi = async (question) => { const logStore = useLogStore(); if (!question || !question.title) { return null; } const cleanTitle = question.title.replace(/<[^>]*>/g, '').replace(/[\?\?\.\。]/g, '').trim(); const apiUrl = `https://api.shuashuati.com/api/search?question=${encodeURIComponent(cleanTitle)}&type=text`; return new Promise((resolve) => { _GM_xmlhttpRequest({ url: apiUrl, method: 'GET', headers: {}, timeout: 10000, onload: (response) => { try { if (response.status < 200 || response.status >= 300) { resolve(null); return; } const data = JSON.parse(response.responseText); if (!data || data.code !== 0 || !data.data || !data.data.answer) { resolve(null); return; } const answerText = String(data.data.answer).trim(); if (!answerText) { resolve(null); return; } logStore.addLog(`刷刷题题库匹配成功: ${answerText}`, 'success'); // 使用统一的答案解析函数 const questionType = question.type || '0'; const answers = parseAnswers(answerText, questionType); if (answers.length === 0) { resolve(null); return; } const confidence = getEnhancedConfidence('shuashuati-free', answerText, question, answers); resolve({ code: 200, msg: '刷刷题题库匹配成功', data: { answer: answers, source: 'shuashuati-free', num: -1, confidence: confidence } }); } catch (e) { resolve(null); } }, onerror: () => { resolve(null); }, ontimeout: () => { resolve(null); } }); }); }; // 接口11:废弃DeepSeek AI,保留空壳 const fetchFromDeepSeekAI = async (question) => { return null; }; const fetchAnswerData = async (question) => { // ========== 1. 优先检查:是否有验证过的正确答案(最高优先级) ========== const verifiedAnswer = WrongAnswerLearner.getVerifiedAnswer(question); if (verifiedAnswer && verifiedAnswer.length > 0) { const logStore = useLogStore(); logStore.addLog('✅ 使用验证过的正确答案(100%正确率)', 'success'); return { code: 200, data: { answer: verifiedAnswer, source: 'verified_answer', confidence: 100 } }; } var _a; const _self = _unsafeWindow; const configStore = useConfigStore(); const token = configStore.queryApis[0].token; window.__answerModeLogged = false; let verifyAnswer = false; let useAIOnly = false; let aiType = '混元'; let aiModel = 'Standard'; const answerParamsPart = configStore.platformParams[configStore.platformName]?.parts.find(p => p.name === "答题参数"); if (answerParamsPart && answerParamsPart.params) { const normalModeParam = answerParamsPart.params.find(p => p.name === "正常模式"); const aiModeParam = answerParamsPart.params.find(p => p.name === "AI模式"); const answerVerifyParam = answerParamsPart.params.find(p => p.name === "答案校验"); const aiTypeParam = answerParamsPart.params.find(p => p.name === "AI 类型选择"); const aiModelParam = answerParamsPart.params.find(p => p.name === "AI 模型选择"); if (aiTypeParam && aiTypeParam.value) { aiType = aiTypeParam.value; } if (aiModelParam && aiModelParam.value) { aiModel = aiModelParam.value; } if (aiModeParam && aiModeParam.value) { useAIOnly = true; } else if (answerVerifyParam && answerVerifyParam.value) { verifyAnswer = true; } } let modelType = 'tencent-hunyuan-standard'; if (aiType === '混元') { modelType = aiModel === 'T1' ? 'tencent-hunyuan-t1' : 'tencent-hunyuan-standard'; } else if (aiType === 'DeepSeek') { if (aiModel === 'R1') { modelType = 'DeepSeek-R1-0528'; } else { modelType = 'DeepSeek-V3.2'; } } else if (aiType === 'MiniMax') { modelType = aiModel === 'M2.7' ? 'minimax-m2.7' : 'minimax-m2.5'; } else if (aiType === 'Qwen') { modelType = aiModel === '3.5-plus' ? 'qwen3.5-plus' : 'qwen3.6-plus'; } else if (aiType === 'ChatGPT') { if (aiModel === '5.4-nano') { modelType = 'gpt-5.4-nano'; } else { modelType = 'gpt-5.4-mini'; } } else if (aiType === 'Gemini') { modelType = 'gemini-3.1-flash-lite-preview'; } else if (aiType === 'GLM') { if (aiModel === '4.7') { modelType = 'Pro/zai-org/GLM-4.7'; } else if (aiModel === '5.1') { modelType = 'Pro/zai-org/GLM-5.1'; } else { modelType = 'Pro/zai-org/GLM-5'; } } if ((useAIOnly || verifyAnswer) && !configStore.tokenVerified) { return buildErrorResponse("AI模式和校验模式需要输入有效Token"); } const logStore = useLogStore(); if (!window.__answerModeLogged && useAIOnly) { logStore.addLog(`🎯 AI模型: ${aiType} - ${aiModel}`, 'info'); } const getCookie = (name) => { const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); return match ? match[2] : ''; }; let userId = ''; if (_self?.getCookie) { userId = _self.getCookie("UID"); } if (!userId) { userId = getCookie('UID'); } if (!userId) { userId = getCookie('_uid'); } if (!userId && _self?.uid) { userId = _self.uid; } const questionData = { question: question.title, options: question.optionsText, type: question.type, questionData: question.element.outerHTML, workType: question.workType, id: ((_a = question.refer.match(/courseId=(\d+)/)) == null ? void 0 : _a[1]) || "", refer: question.refer, u: userId, t: Math.floor(( new Date()).getTime() / 1e3).toString() }; const startTaskId = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0; const answerIntervalParam = answerParamsPart?.params.find(p => p.name === "答题间隔"); const simulateDelay = answerParamsPart?.params.find(p => p.name === "模拟延迟")?.value ?? true; await (simulateDelay ? randomDelay(answerIntervalParam?.value || 1, 0.5) : delay(answerIntervalParam?.value || 1)); const currentTaskIdAfterSleep = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0; if (currentTaskIdAfterSleep !== startTaskId) { return { code: 499, msg: '用户取消', data: null }; } const sendRequest = async (checkOnly = false) => { return new Promise(async (resolve) => { if (!checkOnly && !window.__answerModeLogged) { window.__answerModeLogged = true; const logStore = useLogStore(); if (useAIOnly) { logStore.addLog('AI答题模式中', 'primary'); } else if (verifyAnswer) { logStore.addLog('答案校验模式中', 'primary'); } else { logStore.addLog('正常模式答题中', 'primary'); } } const logStore = useLogStore(); if (useAIOnly && !checkOnly && !window.__inThinkingMode__) { logStore.addLog(`${aiType} ${aiModel} 正在处理中,请耐心等待...`, 'info'); } // 根据请求类型设置超时:checkOnly=true(第一次查询)60秒,checkOnly=false(第二次深度思考/AI模式)90秒 const timeout = checkOnly ? 60000 : 90000; // 离线模式 - 优先使用本地答题 try { // 多题库并行查询策略:同时调用多个题库,选择置信度最高的答案 const tkToken = configStore.queryApis[0]?.token; // 构建题库查询任务列表 const queryTasks = []; // 1. 万卷题库(需要token) if (tkToken && tkToken.length >= 32) { queryTasks.push( fetchFromWanjuanTK(question, tkToken).then(result => { if (result && result.code === 200) { logStore.addLog('万卷题库匹配成功', 'success'); return result; } return null; }).catch(() => null) ); } // 2. lyck6 免费题库 queryTasks.push( fetchFromLyck6(question).then(result => { if (result && result.code === 200) { logStore.addLog('lyck6题库匹配成功', 'success'); return result; } return null; }).catch(() => null) ); // 3. datam.site 免费题库 queryTasks.push( fetchFromWkapi(question).then(result => { if (result && result.code === 200) { logStore.addLog('datam题库匹配成功', 'success'); return result; } return null; }).catch(() => null) ); // 4. apibyte.cn 教育搜题接口 queryTasks.push( fetchFromApiByte(question).then(result => { if (result && result.code === 200) { logStore.addLog('apibyte题库匹配成功', 'success'); return result; } return null; }).catch(() => null) ); // 5. wkexam 网课考试题库 queryTasks.push( fetchFromWkexam(question).then(result => { if (result && result.code === 200) { logStore.addLog('wkexam题库匹配成功', 'success'); return result; } return null; }).catch(() => null) ); // 6. hive-net 大学生网课题库 queryTasks.push( fetchFromHiveNet(question).then(result => { if (result && result.code === 200) { logStore.addLog('hive-net题库匹配成功', 'success'); return result; } return null; }).catch(() => null) ); // 7. SH-API 免费无限制搜题接口 queryTasks.push( fetchFromSHAPI(question).then(result => { if (result && result.code === 200) { logStore.addLog('SH-API题库匹配成功', 'success'); return result; } return null; }).catch(() => null) ); // 8. yxykw.com 网课查题接口 queryTasks.push( fetchFromYxykw(question).then(result => { if (result && result.code === 200) { logStore.addLog('yxykw题库匹配成功', 'success'); return result; } return null; }).catch(() => null) ); // 9. datam.site 免费题库接口 queryTasks.push( fetchFromDatamSite(question).then(result => { if (result && result.code === 200) { logStore.addLog('datam.site题库匹配成功', 'success'); return result; } return null; }).catch(() => null) ); // 10. shuashuati.com 刷刷题免费题库接口 queryTasks.push( fetchFromShuaShuaTi(question).then(result => { if (result && result.code === 200) { logStore.addLog('刷刷题题库匹配成功', 'success'); return result; } return null; }).catch(() => null) ); // 并行执行所有题库查询,等待所有结果 const results = await Promise.allSettled(queryTasks); // 筛选出成功的结果 const successResults = results .filter(r => r.status === 'fulfilled' && r.value && r.value.code === 200) .map(r => r.value); if (successResults.length > 0) { // 选择置信度最高的答案 successResults.sort((a, b) => (b.data?.confidence || 0) - (a.data?.confidence || 0)); const bestResult = successResults[0]; logStore.addLog(`使用 ${bestResult.data?.source || '未知'} 题库答案 (置信度: ${bestResult.data?.confidence || 0}%)`, 'success'); resolve(bestResult); return; } // 所有题库都失败,使用本地答案匹配 logStore.addLog('所有在线题库未找到答案,使用本地匹配', 'warning'); const localResult = performLocalAnswerMatch(question); resolve(localResult); } catch (e) { resolve(buildErrorResponse(`离线答题异常: ${e.message}`)); } return; }); }; const calculatePollInterval = (checkOnly = false) => { if (verifyAnswer) { return checkOnly ? 2000 : 5000; } else if (useAIOnly) { return 2000; } else { return 1000; } }; // 离线模式 - 无需轮询查询 const pollQueryTask = (taskId, resolve, pollInterval = 3000) => { resolve(buildErrorResponse("离线模式不支持轮询")); }; // 若为AI相关模式,先做本地/服务端授权校验,防止未授权直接调用AI接口 if (useAIOnly || verifyAnswer) { try { const lic = await ClientLicense.checkToken(); if (!lic.ok) { return buildErrorResponse('未授权或卡密已用尽,请兑换卡密'); } if (typeof lic.uses_remaining === 'number' && lic.uses_remaining <= 0) { return buildErrorResponse('卡密已用尽'); } } catch (e) { return buildErrorResponse('授权校验失败'); } } const firstResponse = useAIOnly ? await sendRequest(false) : await sendRequest(true); // 关键修复:答案校验/AI模式成功后,必须扣减次数(只在成功响应时扣减) if ((useAIOnly || verifyAnswer) && firstResponse.code === 200) { try { console.log('[学习通助手] 答案校验/AI模式请求成功,开始扣减次数'); const consumed = await ClientLicense.consumeOne(); console.log('[学习通助手] consumeOne 返回:', consumed); if (!consumed.ok) { console.warn('[学习通助手] 次数扣减失败:', consumed.message); } } catch (e) { console.error('[学习通助手] 扣减次数异常:', e); } } if (firstResponse.code === 202 && firstResponse.status === "thinking") { const logStore = useLogStore(); const thinkingStartTaskId = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0; logStore.addLog('答案校验失败,启动深度思考...', 'warning'); logStore.addLog('AI深度思考中,请耐心等待...', 'info'); window.__inThinkingMode__ = true; console.log('答案校验失败,题库答案:', firstResponse.data.tikuAnswer); console.log('AI答案(非思维):', firstResponse.data.aiAnswer); const currentTaskId = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0; if (currentTaskId !== thinkingStartTaskId) { return { code: 499, msg: '用户取消', data: null }; } await randomDelay(0.1, 0.15); const currentTaskId2 = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0; if (currentTaskId2 !== thinkingStartTaskId) { return { code: 499, msg: '用户取消', data: null }; } const finalResponse = await sendRequest(false); // 深度思考模式成功后,也需要扣减次数 if (finalResponse.code === 200) { try { console.log('[学习通助手] 深度思考模式请求成功,开始扣减次数'); const consumed = await ClientLicense.consumeOne(); console.log('[学习通助手] consumeOne 返回:', consumed); if (!consumed.ok) { console.warn('[学习通助手] 次数扣减失败:', consumed.message); } } catch (e) { console.error('[学习通助手] 扣减次数异常:', e); } } window.__inThinkingMode__ = false; return finalResponse; } if (firstResponse.code === 403) { if (useAIOnly || verifyAnswer) { return buildErrorResponse("AI模式和校验模式需要输入有效Token"); } } return firstResponse; }; const queryAnswer = async (question) => { return await fetchAnswerData(question); }; const computeMatch = (str1, str2) => { // 使用 StringUtil 进行标准化预处理(借鉴 jinmu.js) const s1 = StringUtil.clearString(StringUtil.removeRedundant(str1)); const s2 = StringUtil.clearString(StringUtil.removeRedundant(str2)); if (s1 === s2) return 100; if (s1.length === 0 || s2.length === 0) return 0; // 使用编辑距离算法 const matrix = []; for (let i = 0; i <= s1.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= s2.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= s1.length; i++) { for (let j = 1; j <= s2.length; j++) { if (s1.charAt(i - 1) === s2.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } const distance = matrix[s1.length][s2.length]; const maxLength = Math.max(s1.length, s2.length); const similarity = (maxLength - distance) / maxLength * 100; return Math.round(similarity); }; // pickBestOption:使用标准化匹配,选择最佳选项 // 【优化】提高阈值到 60%,减少误匹配 const pickBestOption = (answer, options, threshold = 60) => { let bestMatch = null; // 先尝试精确匹配(使用标准化字符串) const cleanAnswer = StringUtil.clearString(StringUtil.removeRedundant(answer)); for (const key in options) { const cleanKey = StringUtil.clearString(StringUtil.removeRedundant(key)); if (cleanAnswer === cleanKey) { return { key, similarity: 100 }; } } // 然后尝试编辑距离匹配 for (const key in options) { const similarity = computeMatch(answer, key); if (similarity === 100) { return { key, similarity: 100 }; } if (similarity >= threshold && (!bestMatch || similarity > bestMatch.similarity)) { bestMatch = { key, similarity }; } } return bestMatch; }; class CxQuestionHandler extends QuestionProcessor { constructor(type, iframe) { super(); __publicField(this, "type"); __publicField(this, "init", async () => { this.questions = []; await this.parseHtml(); if (this.questions.length) { try { this.addLog(`成功解析到${this.questions.length}个题目`, "primary"); const configStore = useConfigStore(); // 确保平台名称已设置 if (!configStore.platformName) { configStore.platformName = "cx"; this.addLog(`平台名称未设置,默认为: ${configStore.platformName}`, "warning"); } const startTaskId = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0; const platformConfig = configStore.platformParams?.[configStore.platformName]; if (!platformConfig) { this.addLog(`平台配置未找到: ${configStore.platformName},使用默认配置`, "warning"); } const _answerParamsPart1 = platformConfig?.parts?.find(p => p.name === "答题参数"); const skipAnswered = _answerParamsPart1?.params?.find(p => p.name === "跳过已答")?.value || false; let skippedCount = 0; this.addLog(`答题循环开始: 题目数=${this.questions.length}, skipAnswered=${skipAnswered}`, "info"); for (const [index, question] of this.questions.entries()) { const currentTaskId = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0; if (currentTaskId !== startTaskId) { this.addLog(`答题循环中断: 任务ID变化`, "warning"); break; } this.addLog(`第${index + 1}道题: type=${question.type}, 已有答案=${question.answer ? JSON.stringify(question.answer) : '无'}`, "info"); if (skipAnswered && this.checkIfAnswered(question, this.type) && question.answer && question.answer.length > 0) { this.addLog(`第${index + 1}道题已作答,跳过`, "warning"); skippedCount += 1; this.correctNum += 1; // 设置跳过题目的 source 字段 if (!question.source) { question.source = 'skipped-already-answered'; question.confidence = 100; } this.addQuestion(question); continue; } // 如果题目在 DOM 中显示已作答,但 question.answer 为空,说明需要重新答题 if (skipAnswered && this.checkIfAnswered(question, this.type) && (!question.answer || question.answer.length === 0)) { this.addLog(`第${index + 1}道题页面显示已作答,但脚本未答题,继续答题`, "warning"); } this.addLog(`正在查找第${index + 1}道题目答案...`, "primary"); const answerData = await queryAnswer(question); this.addLog(`第${index + 1}道题返回: code=${answerData.code}, msg=${answerData.msg || ''}`, "info"); const currentTaskId2 = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0; if (currentTaskId2 !== startTaskId || answerData.code === 499) { this.addLog(`第${index + 1}道题: 任务取消或code=499,停止答题`, "warning"); break; } if (answerData.code === 200) { question.answer = answerData.data.answer; question.source = answerData.data.source; question.confidence = answerData.data.confidence || 0; // 确保答案不为空 if (!question.answer || question.answer.length === 0) { this.addLog(`第${index + 1}道题答案为空,使用兜底策略`, "warning"); question.answer = question.options ? [Object.keys(question.options)[0]] : ['A']; question.source = 'local-fallback-empty'; question.confidence = 25; } const confidence = question.confidence; const qualityTag = confidence > 70 ? '✅' : confidence > 50 ? '⚠️' : '❓'; const sourceIcon = { 'ai': '🤖', 'local-精确包含': '🎯', 'local-关键词匹配': '🔍', 'local-互斥分析': '⚖️', 'local-排除绝对词': '🚫', 'local-最长选项': '📏', 'local-相似度匹配': '🔗', 'local-公共词匹配': '🔤', 'local-语义关键词': '💡', 'local-综合评分': '📊', 'local-fallback': '⚠️', 'local-fallback-empty': '⚠️', 'local': '📝', 'error': '❌' }; const icon = sourceIcon[answerData.data.source] || '📋'; this.addLog(`${icon} 第${index + 1}题 [${qualityTag} ${confidence}%] ${answerData.data.source}`, confidence > 70 ? "success" : confidence > 50 ? "warning" : "info"); this.addLog(`准备填写第${index + 1}道题答案...`, "info"); try { await this.fillQuestion(question); this.addLog(`第${index + 1}道题答案填写完成`, "success"); // 记录答题结果用于置信度校准(异步验证,不阻塞答题流程) const answerSource = answerData.data.source; setTimeout(async () => { try { // 等待一段时间让平台验证答案(通常在提交后显示结果) await new Promise(resolve => setTimeout(resolve, 3000)); // 检查答题结果是否正确(通过页面元素判断) const frameWindow = this._window || window; let isCorrect = false; // 方法1: 检查正确图标 const correctIcons = frameWindow.document.querySelectorAll('.u-icon-correct, .right-icon, [class*="correct"], [class*="right"]'); if (correctIcons && correctIcons.length > 0) { isCorrect = true; } // 方法2: 检查错误图标 const wrongIcons = frameWindow.document.querySelectorAll('.u-icon-error, .wrong-icon, [class*="error"], [class*="wrong"]'); if (wrongIcons && wrongIcons.length > 0) { isCorrect = false; } // 方法3: 检查分数显示 const scoreElements = frameWindow.document.querySelectorAll('[class*="score"], .fraction, .point'); for (const scoreEl of scoreElements) { const scoreText = scoreEl.textContent || ''; if (/^\+?\d+(\.\d+)?$/.test(scoreText.trim())) { const score = parseFloat(scoreText); if (score > 0) { isCorrect = true; break; } } } // 记录答题结果 if (answerSource && answerSource !== 'error-fallback') { recordAnswerResult(answerSource, isCorrect); // 获取并显示该题库的历史正确率 const accuracy = getSourceAccuracy(answerSource); if (accuracy && accuracy.total >= 5) { this.addLog(`[${answerSource}] 历史正确率: ${accuracy.accuracy}% (${accuracy.correct}/${accuracy.total})`, 'info'); } } } catch (verifyErr) { console.warn('[学习通助手] 答题结果验证失败:', verifyErr.message); } }, 500); // 每答完一题后调用 saveQuestion 保存答案 try { const frameWindow = this._window || window; if (typeof frameWindow.saveQuestion === 'function') { frameWindow.saveQuestion(); this.addLog(`第${index + 1}道题答案已保存`, "success"); } } catch (saveErr) { this.addLog(`第${index + 1}道题答案保存失败: ${saveErr.message}`, "warning"); } } catch (fillErr) { this.addLog(`第${index + 1}道题答案填写失败: ${fillErr.message}`, "danger"); } // 关键修复:所有答题成功后都需要扣减次数 try { console.log('[学习通助手] 答题成功,开始扣减次数, source:', answerData.data.source); const consumed = await ClientLicense.consumeOne(); console.log('[学习通助手] consumeOne 返回:', consumed); if (consumed.ok) { this.addLog(`✅ 次数已扣减,剩余: ${consumed.uses_remaining}次`, "success"); // 剩余次数不足时提醒购买 if (typeof consumed.uses_remaining === 'number' && consumed.uses_remaining <= 5 && consumed.uses_remaining > 0) { this.addLog(`⚠️ 剩余次数较少(${consumed.uses_remaining}次),建议及时充值`, 'warning'); this.addLog('🛒 点击购买卡密,获取更多答题次数', 'warning'); } } else { // 次数耗尽时明确提醒购买 if (consumed.message === 'exhausted') { this.addLog('❌ 答题次数已用完,无法继续答题', 'danger'); this.addLog('🛒 点击购买卡密,享无限答题', 'warning'); } else if (consumed.message === 'tampered') { this.addLog('❌ 授权异常:检测到篡改', 'danger'); } else { this.addLog(`⚠️ 次数扣减失败: ${consumed.message}`, "warning"); } } } catch (e) { console.error('[学习通助手] 扣减次数异常:', e); this.addLog(`❌ 扣减次数异常: ${e.message}`, "danger"); } this.correctNum += 1; } else { this.addLog(`第${index + 1}道题搜索失败: ${answerData.msg},使用兜底答案`, "danger"); // 即使搜索失败,也使用第一个选项作为兜底 question.answer = question.options ? [Object.keys(question.options)[0]] : ['A']; question.source = 'error-fallback'; question.confidence = 20; try { await this.fillQuestion(question); this.addLog(`第${index + 1}道题兜底答案填写完成`, "warning"); // 保存答案 try { const frameWindow = this._window || window; if (typeof frameWindow.saveQuestion === 'function') { frameWindow.saveQuestion(); } } catch (saveErr) { this.addLog(`第${index + 1}道题答案保存失败: ${saveErr.message}`, "warning"); } } catch (fillErr) { this.addLog(`第${index + 1}道题兜底答案填写失败: ${fillErr.message}`, "danger"); } // 关键修复:所有答题成功后都需要扣减次数 try { console.log('[学习通助手] 兜底答案答题成功,开始扣减次数, source:', answerData.data.source); const consumed = await ClientLicense.consumeOne(); console.log('[学习通助手] consumeOne 返回:', consumed); if (consumed.ok) { this.addLog(`✅ 次数已扣减,剩余: ${consumed.uses_remaining}次`, "success"); // 剩余次数不足时提醒购买 if (typeof consumed.uses_remaining === 'number' && consumed.uses_remaining <= 5 && consumed.uses_remaining > 0) { this.addLog(`⚠️ 剩余次数较少(${consumed.uses_remaining}次),建议及时充值`, 'warning'); this.addLog('🛒 点击购买卡密,获取更多答题次数', 'warning'); } } else { // 次数耗尽时明确提醒购买 if (consumed.message === 'exhausted') { this.addLog('❌ 答题次数已用完,无法继续答题', 'danger'); this.addLog('🛒 点击购买卡密,享无限答题', 'warning'); } else if (consumed.message === 'tampered') { this.addLog('❌ 授权异常:检测到篡改', 'danger'); } else { this.addLog(`⚠️ 次数扣减失败: ${consumed.message}`, "warning"); } } } catch (e) { console.error('[学习通助手] 扣减次数异常:', e); this.addLog(`❌ 扣减次数异常: ${e.message}`, "danger"); } this.correctNum += 1; } this.addQuestion(question); } if (skippedCount > 0) { this.addLog(`共跳过${skippedCount}道已答题目`, "primary"); } // 答题完成后提交前验证 - 终极版:确保所有题目都已作答,最多重试3轮 try { const frameWindow = this._window || window; let maxVerificationRounds = 3; let verificationRound = 0; let allVerified = false; while (verificationRound < maxVerificationRounds && !allVerified) { verificationRound++; allVerified = true; let unverifiedQuestions = []; this.addLog(`🔍 第${verificationRound}轮提交前验证...`, "info"); for (const [index, q] of this.questions.entries()) { // 跳过没有答案的题目(理论上不应该发生) if (!q.answer || q.answer.length === 0) { this.addLog(`⚠️ 第${index + 1}题答案为空,使用兜底策略`, "warning"); q.answer = q.options ? [Object.keys(q.options)[0]] : ['A']; q.source = 'local-fallback-empty'; q.confidence = 25; // 立即填写兜底答案 try { await this.fillQuestion(q); await new Promise(resolve => setTimeout(resolve, 600)); } catch (fillErr) { this.addLog(`第${index + 1}题兜底答案填写失败: ${fillErr.message}`, "danger"); } continue; } // 检查选择题/判断题是否已选中 let isQuestionVerified = false; if (q.type === "0" || q.type === "1" || q.type === "3") { // 方式1: 检查options对象中的元素 for (const key in q.options) { const optEl = q.options[key]; if (!optEl) continue; // 多种检测方式 const input = optEl.querySelector('input[type="radio"], input[type="checkbox"]'); if (input && input.checked) { isQuestionVerified = true; break; } if (optEl.getAttribute("aria-checked") === "true" || optEl.classList.contains("is-checked") || optEl.classList.contains("selected") || optEl.classList.contains("answer-selected") || optEl.getAttribute("data-checked") === "true") { isQuestionVerified = true; break; } } // 方式2: 如果方式1失败,直接从DOM查找 if (!isQuestionVerified && q.element) { const checkedInputs = q.element.querySelectorAll('input[type="radio"]:checked, input[type="checkbox"]:checked'); if (checkedInputs.length > 0) { isQuestionVerified = true; } } // 方式3: 检查是否有选中标记的父元素 if (!isQuestionVerified && q.element) { const checkedContainers = q.element.querySelectorAll('.is-checked, .selected, .answer-selected, [aria-checked="true"], [data-checked="true"]'); if (checkedContainers.length > 0) { isQuestionVerified = true; } } } else { // 填空题等其他题型,默认已作答 isQuestionVerified = true; } if (!isQuestionVerified) { allVerified = false; unverifiedQuestions.push({ index, question: q }); this.addLog(`⚠️ 第${index + 1}题未检测到选中状态`, "warning"); } } // 如果有未验证的题目,尝试重新填写 if (unverifiedQuestions.length > 0 && verificationRound < maxVerificationRounds) { this.addLog(`🔄 发现 ${unverifiedQuestions.length} 道题未选中,开始重新填写...`, "warning"); for (const { index, question } of unverifiedQuestions) { try { this.addLog(`重新填写第${index + 1}题...`, "info"); await this.fillQuestion(question); await new Promise(resolve => setTimeout(resolve, 800)); } catch (retryErr) { this.addLog(`第${index + 1}题重新填写失败: ${retryErr.message}`, "danger"); } } // 等待所有题目填写完成 await new Promise(resolve => setTimeout(resolve, 1000)); } } // 最终轮验证后仍未全部通过,强制填写所有题目 if (!allVerified) { this.addLog(`🔧 验证未全部通过,强制填写所有题目...`, "warning"); for (const [index, q] of this.questions.entries()) { if (q.type === "0" || q.type === "1" || q.type === "3") { // 检查是否已选中 - 多种方式检测 let isAlreadySelected = false; // 方式1: 检查options中的input for (const key in q.options) { const optEl = q.options[key]; if (!optEl) continue; const input = optEl.querySelector('input[type="radio"], input[type="checkbox"]'); if (input && input.checked) { isAlreadySelected = true; break; } if (optEl.getAttribute("aria-checked") === "true" || optEl.classList.contains("is-checked") || optEl.classList.contains("selected") || optEl.classList.contains("answer-selected")) { isAlreadySelected = true; break; } } // 方式2: 直接从DOM检查 if (!isAlreadySelected && q.element) { const checkedInputs = q.element.querySelectorAll('input[type="radio"]:checked, input[type="checkbox"]:checked'); if (checkedInputs.length > 0) { isAlreadySelected = true; } } // 如果未选中,强制选择第一个选项 if (!isAlreadySelected && q.options && Object.keys(q.options).length > 0) { const firstKey = Object.keys(q.options)[0]; const firstOption = q.options[firstKey]; if (firstOption) { this.addLog(`🔧 强制选择第${index + 1}题的第一个选项`, "warning"); const input = firstOption.querySelector('input[type="radio"], input[type="checkbox"]'); if (input) { input.checked = true; input.focus(); input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } firstOption.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); firstOption.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); firstOption.dispatchEvent(new PointerEvent('pointerup', { bubbles: true })); firstOption.dispatchEvent(new PointerEvent('click', { bubbles: true })); firstOption.setAttribute("aria-checked", "true"); firstOption.classList.add("is-checked", "selected", "answer-selected"); firstOption.setAttribute("data-checked", "true"); await new Promise(resolve => setTimeout(resolve, 300)); } } else if (!isAlreadySelected && q.element) { // 方式3: 如果options为空,直接从DOM查找 this.addLog(`🔧 强制选择第${index + 1}题 (DOM查找)`, "warning"); const radioInputs = q.element.querySelectorAll('input[type="radio"], input[type="checkbox"]'); if (radioInputs.length > 0) { const firstInput = radioInputs[0]; firstInput.checked = true; firstInput.focus(); firstInput.dispatchEvent(new Event('change', { bubbles: true })); firstInput.dispatchEvent(new Event('input', { bubbles: true })); const parentOption = firstInput.closest('li, .option, .option-list li, [class*="option"]'); if (parentOption) { parentOption.setAttribute("aria-checked", "true"); parentOption.classList.add("is-checked", "selected", "answer-selected"); parentOption.setAttribute("data-checked", "true"); parentOption.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } await new Promise(resolve => setTimeout(resolve, 300)); } } } } this.addLog(`✅ 所有题目已强制填写完成`, "success"); } else { this.addLog(`✅ 所有题目验证通过,准备提交`, "success"); } // 调用 btnBlueSubmit 提交答案 if (typeof frameWindow.btnBlueSubmit === 'function') { this.addLog(`正在提交答案...`, "primary"); frameWindow.btnBlueSubmit(); await new Promise(resolve => setTimeout(resolve, 1000)); // 调用 submitCheckTimes 确认提交 if (typeof frameWindow.submitCheckTimes === 'function') { frameWindow.submitCheckTimes(); this.addLog(`答案提交成功`, "success"); } else { this.addLog(`答案已提交`, "success"); } // 隐藏弹窗 if (frameWindow.top && frameWindow.top.$) { frameWindow.top.$("#workpop").hide(); } else if (window.top && window.top.$) { window.top.$("#workpop").hide(); } } else { this.addLog(`未找到提交函数,请手动提交`, "warning"); } } catch (submitErr) { this.addLog(`提交答案失败: ${submitErr.message}`, "warning"); } // 答题完成总结报告 this.generateAnswerSummary(); } catch (err) { this.addLog(`答题循环异常: ${err.message}`, "danger"); console.error("答题循环异常:", err); } } else this.addLog("未解析到题目,请进入正确页面", "danger"); // 正确的正确率计算:基于答案置信度的加权平均值 if (this.questions.length === 0) { return Promise.resolve(0); } const totalConfidence = this.questions.reduce((sum, q) => sum + (q.confidence || 0), 0); const avgConfidence = totalConfidence / this.questions.length; return Promise.resolve(avgConfidence); }); __publicField(this, "generateAnswerSummary", () => { const total = this.questions.length; const answered = this.correctNum; let highQuality = 0; let mediumQuality = 0; let lowQuality = 0; let fallbackCount = 0; const sourceStats = {}; for (const q of this.questions) { const confidence = q.confidence || 0; const source = q.source || 'unknown'; sourceStats[source] = (sourceStats[source] || 0) + 1; if (confidence > 70) { highQuality++; } else if (confidence > 50) { mediumQuality++; } else { lowQuality++; } if (source === 'local-fallback') { fallbackCount++; } } this.addLog(`━━━━━━━━━━━━━━━━━━━━━━━━`, 'primary'); this.addLog(`📊 答题完成总结`, 'primary'); this.addLog(`━━━━━━━━━━━━━━━━━━━━━━━━`, 'primary'); this.addLog(`📝 总计: ${total}题 | 已答: ${answered}题`, 'info'); this.addLog(`✅ 高质量答案(>70%): ${highQuality}题 (${(highQuality/total*100).toFixed(1)}%)`, 'success'); this.addLog(`⚠️ 中质量答案(50-70%): ${mediumQuality}题 (${(mediumQuality/total*100).toFixed(1)}%)`, 'warning'); this.addLog(`❓ 低质量答案(<50%): ${lowQuality}题 (${(lowQuality/total*100).toFixed(1)}%)`, 'danger'); if (fallbackCount > 0) { this.addLog(`⚠️ 兜底答题(第一个选项): ${fallbackCount}题`, 'warning'); } this.addLog(`📋 答案来源统计:`, 'info'); for (const [source, count] of Object.entries(sourceStats)) { const sourceIcon = { 'ai': '🤖', 'local-精确包含': '🎯', 'local-关键词匹配': '🔍', 'local-互斥分析': '⚖️', 'local-排除绝对词': '🚫', 'local-最长选项': '📏', 'local-相似度匹配': '🔗', 'local-公共词匹配': '🔤', 'local-语义关键词': '💡', 'local-综合评分': '📊', 'local-fallback': '⚠️', 'local': '📝' }; const icon = sourceIcon[source] || '📋'; this.addLog(` ${icon} ${source}: ${count}题`, 'info'); } this.addLog(`━━━━━━━━━━━━━━━━━━━━━━━━`, 'primary'); }); __publicField(this, "waitForQuestions", async (selector, maxWait = 10000) => { const startTime = Date.now(); while (Date.now() - startTime < maxWait) { const elements = this._document.querySelectorAll(selector); if (elements.length > 0) { return elements; } await new Promise(resolve => setTimeout(resolve, 500)); } return this._document.querySelectorAll(selector); }); __publicField(this, "parseHtml", async () => { if (!this._document) return []; try{ if (["zj"].includes(this.type)) { this.addLog(`[parseHtml] 等待题目加载...`, "info"); let questionElements = await this.waitForQuestions(SELECTORS.CX_QUESTION_ZJ, 8000); if (!questionElements.length) { questionElements = this._document.querySelectorAll('.TiMu, [class*="question"], [class*="subject"], .examPaper_subject'); this.addLog(`[parseHtml] 主选择器未找到题目,使用兜底选择器找到 ${questionElements.length} 个题目`, "warning"); } else { this.addLog(`[parseHtml] 使用选择器 ${SELECTORS.CX_QUESTION_ZJ} 找到 ${questionElements.length} 个题目`, "info"); } this.addQuestions(questionElements); } else if (["zy", "ks"].includes(this.type)) { this.addLog(`[parseHtml] 等待题目加载...`, "info"); let questionElements = await this.waitForQuestions(SELECTORS.CX_QUESTION_ZY_KS, 8000); if (!questionElements.length) { questionElements = this._document.querySelectorAll('.questionLi, .TiMu, [class*="question"], [class*="subject"], .examPaper_subject'); this.addLog(`[parseHtml] 主选择器未找到题目,使用兜底选择器找到 ${questionElements.length} 个题目`, "warning"); } else { this.addLog(`[parseHtml] 使用选择器 ${SELECTORS.CX_QUESTION_ZY_KS} 找到 ${questionElements.length} 个题目`, "info"); } this.addQuestions(questionElements); } if (this.questions.length > 0) { this.addLog(`成功解析到${this.questions.length}个题目`, "success"); this.questions.forEach((q, idx) => { this.addLog(`[题目${idx+1}] type=${q.type}, title=${q.title.substring(0, 30)}..., options=${Object.keys(q.options).length}个`, "info"); }); } else { this.addLog(`未找到任何题目,请检查页面是否正确`, "danger"); } }catch(e){ this.addLog(`解析题目异常: ${e.message}`, "danger"); } }); __publicField(this, "fillQuestion", async (question) => { var _a, _b; if (!this._window) return; try { const isLocalMode = question.source && question.source.startsWith("local"); if (isLocalMode) { this.addLog(`开始填写答案 - 题目类型: ${question.type}, 答案数量: ${question.answer.length}`, 'info'); // 记录题型识别信息 if (question.identifyInfo) { this.addLog(`[题型信息] ${question.identifyInfo.log}`, 'info'); } } if (question.type === "0" || question.type === "1") { // 【关键修复】多选题答案预处理:处理所有可能的答案格式 let processedAnswers = question.answer; if (question.type === "1") { // 添加详细日志 if (isLocalMode) { this.addLog(`[多选题预处理] 原始答案: ${JSON.stringify(question.answer)}`, 'info'); } // 如果只有一个答案,可能是需要解析的格式 if (question.answer.length === 1) { const singleAnswer = String(question.answer[0] || ''); // 尝试多种分隔符和格式解析 let parsed = []; // 1. 先尝试 "ABD" 这种连续字母格式 if (/^[A-Za-z]+$/.test(singleAnswer) && singleAnswer.length >= 2) { parsed = singleAnswer.split('').filter(a => /[A-Za-z]/.test(a)); if (isLocalMode && parsed.length >= 2) { this.addLog(`[多选题预处理] 连续字母格式解析: "${singleAnswer}" -> ${JSON.stringify(parsed)}`, 'info'); } } // 2. 尝试用 # 分隔 (A#B#D) if (parsed.length < 2 && singleAnswer.includes('#')) { parsed = singleAnswer.split('#').filter(a => a.trim()); if (isLocalMode && parsed.length >= 2) { this.addLog(`[多选题预处理] # 分隔解析: "${singleAnswer}" -> ${JSON.stringify(parsed)}`, 'info'); } } // 3. 尝试用逗号、分号、顿号、空格等分隔 if (parsed.length < 2) { const parts = singleAnswer.split(/[,,;;、\s]+/).filter(a => a.trim()); if (parts.length >= 2) { parsed = parts; if (isLocalMode) { this.addLog(`[多选题预处理] 多分隔符解析: "${singleAnswer}" -> ${JSON.stringify(parsed)}`, 'info'); } } } // 4. 尝试识别答案中的多个字母(如 "ABCD" 在文本中间) if (parsed.length < 2) { const letters = singleAnswer.match(/[A-Za-z]/g); if (letters && letters.length >= 2) { parsed = letters; if (isLocalMode) { this.addLog(`[多选题预处理] 字母提取: "${singleAnswer}" -> ${JSON.stringify(parsed)}`, 'info'); } } } if (parsed.length >= 2) { processedAnswers = parsed; question.answer = parsed; // 覆盖原始答案 } } if (isLocalMode) { this.addLog(`[多选题预处理] 最终答案数量: ${processedAnswers.length}`, 'info'); if (processedAnswers.length > 0) { this.addLog(`[多选题预处理] 最终答案: ${JSON.stringify(processedAnswers)}`, 'info'); } } } const configStore = useConfigStore(); let useSimilarity = configStore.platformParams?.[configStore.platformName]?.parts?.find(p => p.name === "答题参数")?.params?.find(p => p.name === "相似匹配")?.value || false; // 离线模式自动启用相似度匹配 if (isLocalMode) { useSimilarity = true; } const correctKeys = new Set(); const answerLog = []; if (isLocalMode) { this.addLog(`[答案匹配] 题目类型: ${question.type}`, 'info'); this.addLog(`[答案匹配] 答案数组: ${JSON.stringify(question.answer)}`, 'info'); this.addLog(`[答案匹配] 可用选项数量: ${Object.keys(question.options).length}`, 'info'); Object.keys(question.options).forEach((key, idx) => { this.addLog(`[答案匹配] 选项${idx}: ${key.substring(0, 50)}`, 'info'); }); } for (const answer of question.answer) { const cleanAnswer = this.stripTags(answer); let matched = false; if (isLocalMode) { this.addLog(`[答案匹配] 处理答案: ${cleanAnswer}`, 'info'); } // 策略1:精确匹配 for (const key in question.options) { if (key === cleanAnswer) { correctKeys.add(key); matched = true; answerLog.push(`精确匹配: "${key.substring(0, 30)}"`); if (isLocalMode) { this.addLog(`[答案匹配] ✅ 精确匹配成功: ${key.substring(0, 30)}`, 'info'); } break; } } // 策略2:去除选项前缀后匹配 if (!matched) { const cleanAnswerNoPrefix = cleanAnswer.replace(/^[A-Z][.、]\s*/, '').trim(); for (const key in question.options) { const cleanKey = key.replace(/^[A-Z][.、]\s*/, '').trim(); if (cleanKey === cleanAnswerNoPrefix) { correctKeys.add(key); matched = true; answerLog.push(`去前缀匹配: "${cleanAnswerNoPrefix.substring(0, 30)}" -> "${key.substring(0, 30)}"`); break; } } } // 策略3:包含匹配(双向)- 降低阈值 if (!matched) { for (const key in question.options) { const cleanKey = this.stripTags(key); const minLen = Math.min(cleanKey.length, cleanAnswer.length); if ((cleanKey.includes(cleanAnswer) || cleanAnswer.includes(cleanKey)) && minLen > 1) { correctKeys.add(key); matched = true; answerLog.push(`包含匹配: "${cleanAnswer.substring(0, 30)}" -> "${key.substring(0, 30)}"`); break; } } } // 策略4:去除HTML标签后匹配 if (!matched) { const cleanAnswerHtml = cleanAnswer.replace(/<[^>]*>/g, '').trim(); for (const key in question.options) { const cleanKeyHtml = this.stripTags(key); if (cleanKeyHtml === cleanAnswerHtml || cleanKeyHtml.includes(cleanAnswerHtml) || cleanAnswerHtml.includes(cleanKeyHtml)) { if (cleanAnswerHtml.length > 1) { correctKeys.add(key); matched = true; answerLog.push(`HTML清理匹配: "${cleanAnswerHtml.substring(0, 30)}"`); break; } } } } // 策略5:去除所有空白字符后匹配 if (!matched) { const cleanAnswerNoSpace = cleanAnswer.replace(/\s+/g, '').trim(); for (const key in question.options) { const cleanKeyNoSpace = this.stripTags(key).replace(/\s+/g, '').trim(); if (cleanKeyNoSpace === cleanAnswerNoSpace && cleanAnswerNoSpace.length > 1) { correctKeys.add(key); matched = true; answerLog.push(`去空格匹配: "${cleanAnswerNoSpace.substring(0, 30)}"`); break; } } } // 策略6:答案字母匹配(A/B/C/D) if (!matched) { const answerLetter = cleanAnswer.replace(/^[^A-Za-z]*([A-Za-z]).*$/, '$1').toUpperCase(); if (/^[A-Z]$/.test(answerLetter)) { for (const key in question.options) { if (key.toUpperCase().startsWith(answerLetter + '.') || key.toUpperCase().startsWith(answerLetter + '、') || key.toUpperCase() === answerLetter) { correctKeys.add(key); matched = true; answerLog.push(`字母匹配: ${answerLetter} -> "${key.substring(0, 30)}"`); break; } } } } // 策略7:关键词匹配(提取答案中的关键词)- 降低阈值 if (!matched && cleanAnswer.length > 2) { const keywords = cleanAnswer.split(/[\s,,、;;]+/).filter(k => k.length >= 2); if (keywords.length > 0) { for (const key in question.options) { const cleanKey = this.stripTags(key); const matchCount = keywords.filter(kw => cleanKey.includes(kw)).length; if (matchCount >= 1) { correctKeys.add(key); matched = true; answerLog.push(`关键词匹配: ${matchCount}/${keywords.length} -> "${key.substring(0, 30)}"`); break; } } } } // 策略8:相似度匹配兜底(使用标准化匹配) if (!matched && useSimilarity) { // 【优化】使用更高的阈值 60% 减少误匹配 const bestMatch = pickBestOption(cleanAnswer, question.options, 60); if (bestMatch) { correctKeys.add(bestMatch.key); matched = true; answerLog.push(`相似度匹配: ${bestMatch.similarity}% -> "${bestMatch.key.substring(0, 30)}"`); } } // 策略9:使用 bigram 相似度匹配(借鉴 jinmu.js) if (!matched) { const cleanAnswerNorm = StringUtil.clearString(cleanAnswer); if (cleanAnswerNorm.length >= 2) { for (const key in question.options) { const cleanKeyNorm = StringUtil.clearString(key); const similarity = AdvancedAnswerEngine.bigramSimilarity(cleanAnswerNorm, cleanKeyNorm); // bigram 相似度高于阈值才匹配,避免误选 if (similarity > 0.72) { correctKeys.add(key); matched = true; answerLog.push(`Bigram匹配: ${(similarity * 100).toFixed(0)}% -> "${key.substring(0, 30)}"`); if (isLocalMode) { this.addLog(`[答案匹配] ✅ Bigram匹配成功: ${(similarity * 100).toFixed(0)}% -> ${key.substring(0, 30)}`, 'info'); } break; } } } } // 策略10:所有匹配都失败时,使用第一个选项作为最终兜底 if (!matched) { const firstKey = Object.keys(question.options)[0]; if (firstKey) { correctKeys.add(firstKey); matched = true; answerLog.push(`最终兜底: 使用第一个选项 "${firstKey.substring(0, 30)}"`); if (isLocalMode) { this.addLog(`⚠️ 答案"${cleanAnswer.substring(0, 40)}"匹配失败,使用兜底策略选择第一个选项`, 'warning'); } } } if (!matched && isLocalMode) { this.addLog(`答案匹配失败: "${cleanAnswer.substring(0, 40)}..."`, 'warning'); } } if (isLocalMode && answerLog.length > 0) { this.addLog(`答案匹配结果: ${answerLog.join('; ')}`, 'info'); } // 多选题:取消错误选项前先记录,避免重复点击 if (question.type === "1") { const wrongKeys = []; for (const key in question.options) { if (!correctKeys.has(key)) { const optionElement = question.options[key]; let isChecked = false; // 多种检测方式确保准确识别已选状态 if (["zj", "zy"].includes(this.type)) { isChecked = optionElement.getAttribute("aria-checked") === "true" || optionElement.classList.contains("is-checked") || optionElement.classList.contains("selected") || optionElement.classList.contains("answer-selected") || optionElement.getAttribute("data-checked") === "true"; } else if (["ks"].includes(this.type)) { isChecked = optionElement.querySelector(".check_answer") || optionElement.querySelector(".check_answer_dx") || optionElement.classList.contains("active") || optionElement.classList.contains("checked"); } if (isChecked) { wrongKeys.push(key); } } } // 取消错误选项 if (wrongKeys.length > 0) { if (isLocalMode) { this.addLog(`准备取消 ${wrongKeys.length} 个错误选项`, "warning"); } let cancelledCount = 0; for (const key of wrongKeys) { if (isLocalMode) { this.addLog(`取消错误选项: ${key.substring(0, 40)}...`, "warning"); } const optionElement = question.options[key]; if (optionElement) { optionElement.setAttribute("data-filling", "true"); safeClick(optionElement); const input = optionElement.querySelector('input[type="checkbox"]'); if (input) { input.focus(); input.click(); input.checked = false; input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } const label = optionElement.querySelector('label'); if (label) { label.click(); label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } optionElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); optionElement.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); optionElement.dispatchEvent(new PointerEvent('pointerup', { bubbles: true })); optionElement.dispatchEvent(new PointerEvent('click', { bubbles: true })); // 随机延迟模拟人类操作 const randomWait = Math.floor(Math.random() * 300) + 200; await new Promise(resolve => setTimeout(resolve, randomWait)); optionElement.removeAttribute("data-filling"); cancelledCount++; } else { if (isLocalMode) { this.addLog(`选项元素不存在: ${key.substring(0, 40)}...`, "warning"); } } } if (isLocalMode) { this.addLog(`已取消 ${cancelledCount}/${wrongKeys.length} 个错误选项`, "info"); } await new Promise(resolve => setTimeout(resolve, 500)); } else { if (isLocalMode) { this.addLog("没有需要取消的错误选项", "info"); } } } // 点击正确选项 - 增强版:确保至少有一个选项被选中 if (isLocalMode) { this.addLog(`准备点击 ${correctKeys.size} 个选项: ${Array.from(correctKeys).join(', ')}`, 'info'); } // 如果 correctKeys 为空,强制使用第一个选项 if (correctKeys.size === 0 && Object.keys(question.options).length > 0) { const firstKey = Object.keys(question.options)[0]; correctKeys.add(firstKey); if (isLocalMode) { this.addLog(`⚠️ 没有匹配到任何选项,强制使用第一个选项: ${firstKey.substring(0, 40)}...`, 'warning'); } } const clickedKeys = []; let skippedCount = 0; let notFoundCount = 0; for (const key of correctKeys) { const optionElement = question.options[key]; if (!optionElement) { if (isLocalMode) { this.addLog(`选项不存在: ${key.substring(0, 40)}...`, 'warning'); } notFoundCount++; continue; } // 调试日志:输出选项元素信息 if (isLocalMode) { this.addLog(`[调试] 选项 ${key.substring(0, 50)}...`, 'info'); this.addLog(`[调试] 元素类名: ${optionElement.className}`, 'info'); this.addLog(`[调试] 元素ID: ${optionElement.id}`, 'info'); this.addLog(`[调试] 元素标签: ${optionElement.tagName}`, 'info'); // 检查是否有input元素 const input = optionElement.querySelector('input[type="radio"], input[type="checkbox"]'); if (input) { this.addLog(`[调试] 找到input: type=${input.type}, name=${input.name}, checked=${input.checked}`, 'info'); } else { this.addLog(`[调试] 未找到input元素`, 'warning'); } } // 多种检测方式确保准确识别已选状态 let alreadySelected = false; if (["zj", "zy"].includes(this.type)) { alreadySelected = optionElement.getAttribute("aria-checked") === "true" || optionElement.classList.contains("is-checked") || optionElement.classList.contains("selected") || optionElement.classList.contains("answer-selected") || optionElement.getAttribute("data-checked") === "true"; } else if (["ks"].includes(this.type)) { alreadySelected = optionElement.querySelector(".check_answer") || optionElement.querySelector(".check_answer_dx") || optionElement.classList.contains("active") || optionElement.classList.contains("checked"); } if (isLocalMode) { this.addLog(`[调试] 已选中状态: ${alreadySelected}`, 'info'); } if (!alreadySelected) { optionElement.setAttribute("data-filling", "true"); // 查找可点击的元素 - 优先点击 input,其次 label,最后外层容器 let targetElement = optionElement; const input = optionElement.querySelector('input[type="radio"], input[type="checkbox"]'); const label = optionElement.querySelector('label'); if (input) { targetElement = input; } else if (label) { targetElement = label; } // 第一重:直接设置 input 状态(最可靠) if (input) { input.checked = true; input.focus(); } // 第二重:触发完整的事件链 targetElement.dispatchEvent(new Event('focus', { bubbles: true })); targetElement.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); targetElement.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); targetElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); targetElement.click(); // 第三重:触发 change 和 input 事件 if (input) { input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('input', { bubbles: true })); } // 第四重:触发 PointerEvent targetElement.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); targetElement.dispatchEvent(new PointerEvent('pointerup', { bubbles: true })); targetElement.dispatchEvent(new PointerEvent('click', { bubbles: true })); // 第五重:更新父容器状态 optionElement.setAttribute("aria-checked", "true"); optionElement.classList.add("is-checked", "selected", "answer-selected"); optionElement.setAttribute("data-checked", "true"); // 等待事件处理完成 await new Promise(resolve => setTimeout(resolve, 400)); optionElement.removeAttribute("data-filling"); clickedKeys.push(key); if (isLocalMode) { this.addLog(`已点击: ${key.substring(0, 40)}...`, 'info'); } // 点击后立即调用 saveQuestion 保存答案(参考 jinmu.js) try { const frameWindow = this._window || window; if (typeof frameWindow.saveQuestion === 'function') { frameWindow.saveQuestion(); if (isLocalMode) { this.addLog(`[调试] 选项点击后已调用 saveQuestion()`, 'info'); } } } catch (saveErr) { if (isLocalMode) { this.addLog(`[调试] saveQuestion 调用失败: ${saveErr.message}`, 'warning'); } } } else { skippedCount++; if (isLocalMode) { this.addLog(`跳过已选: ${key.substring(0, 40)}...`, 'info'); } } } if (isLocalMode) { this.addLog(`点击统计: 点击${clickedKeys.length}个 | 跳过${skippedCount}个 | 未找到${notFoundCount}个`, 'info'); } // 验证选项是否已选中(单选题和多选题都验证)- 增强版:最多重试3次 if (question.type === "0" || question.type === "1") { await new Promise(resolve => setTimeout(resolve, 600)); let maxRetries = 3; let retryCount = 0; let allVerified = false; while (retryCount < maxRetries && !allVerified) { retryCount++; let verifiedCount = 0; const unverifiedKeys = []; for (const key of correctKeys) { const optionElement = question.options[key]; if (!optionElement) continue; let isSelected = false; if (["zj", "zy"].includes(this.type)) { // 多种检测方式 const input = optionElement.querySelector('input[type="radio"], input[type="checkbox"]'); if (input && input.checked) { isSelected = true; } if (!isSelected) { isSelected = optionElement.getAttribute("aria-checked") === "true" || optionElement.classList.contains("is-checked") || optionElement.classList.contains("selected") || optionElement.classList.contains("answer-selected") || optionElement.getAttribute("data-checked") === "true"; } } else if (["ks"].includes(this.type)) { isSelected = optionElement.querySelector(".check_answer") || optionElement.querySelector(".check_answer_dx") || optionElement.classList.contains("active") || optionElement.classList.contains("checked"); } if (isSelected) { verifiedCount++; } else { unverifiedKeys.push(key); } } if (verifiedCount === correctKeys.size) { allVerified = true; if (isLocalMode) { this.addLog(`✅ 验证通过 - ${verifiedCount}/${correctKeys.size} 个选项已选中 (第${retryCount}次尝试)`, 'success'); } } else if (unverifiedKeys.length > 0 && retryCount < maxRetries) { if (isLocalMode) { this.addLog(`⚠️ 第${retryCount}次验证: ${unverifiedKeys.length} 个选项未选中,开始重试...`, 'warning'); } // 重试未选中的选项 for (const key of unverifiedKeys) { const optionElement = question.options[key]; if (!optionElement) continue; optionElement.setAttribute("data-filling", "true"); // 使用增强的点击逻辑 const input = optionElement.querySelector('input[type="radio"], input[type="checkbox"]'); const label = optionElement.querySelector('label'); let targetElement = input || label || optionElement; if (input) { input.checked = true; input.focus(); } targetElement.dispatchEvent(new Event('focus', { bubbles: true })); targetElement.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); targetElement.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); targetElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); targetElement.click(); if (input) { input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('input', { bubbles: true })); } targetElement.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); targetElement.dispatchEvent(new PointerEvent('pointerup', { bubbles: true })); targetElement.dispatchEvent(new PointerEvent('click', { bubbles: true })); optionElement.setAttribute("aria-checked", "true"); optionElement.classList.add("is-checked", "selected", "answer-selected"); optionElement.setAttribute("data-checked", "true"); await new Promise(resolve => setTimeout(resolve, 400)); optionElement.removeAttribute("data-filling"); } await new Promise(resolve => setTimeout(resolve, 600)); } } if (!allVerified) { if (isLocalMode) { this.addLog(`⚠️ 验证完成 - 仍有部分选项可能未选中,将使用最终兜底`, 'warning'); } // 最终兜底:强制设置所有选项的状态 for (const key of correctKeys) { const optionElement = question.options[key]; if (!optionElement) continue; const input = optionElement.querySelector('input[type="radio"], input[type="checkbox"]'); if (input) { input.checked = true; input.dispatchEvent(new Event('change', { bubbles: true })); } optionElement.setAttribute("aria-checked", "true"); optionElement.classList.add("is-checked", "selected", "answer-selected"); optionElement.setAttribute("data-checked", "true"); } if (isLocalMode) { this.addLog(`🔧 已强制设置所有选项状态`, 'warning'); } } } if (isLocalMode) { this.addLog(`答案填写完成 - 共点击 ${clickedKeys.length} 个选项`, 'success'); } // 最终兜底:如果没有任何选项被点击,强制选择第一个选项 if (clickedKeys.length === 0 && Object.keys(question.options).length > 0) { const firstKey = Object.keys(question.options)[0]; const firstOption = question.options[firstKey]; if (firstOption) { this.addLog(`⚠️ 兜底策略:强制选择第一个选项: ${firstKey.substring(0, 40)}...`, 'warning'); firstOption.setAttribute("data-filling", "true"); safeClick(firstOption); const input = firstOption.querySelector('input[type="radio"], input[type="checkbox"]'); if (input) { input.checked = true; input.dispatchEvent(new Event('change', { bubbles: true })); } firstOption.setAttribute("aria-checked", "true"); firstOption.classList.add("is-checked", "selected"); await new Promise(resolve => setTimeout(resolve, 300)); firstOption.removeAttribute("data-filling"); } } } else if (question.type === "2") { const textareaElements = question.element.querySelectorAll("textarea"); if (textareaElements.length === 0) return; textareaElements.forEach((textareaElement, index) => { try { const ueditor = this._window.UE.getEditor(textareaElement.name); if(ueditor && typeof ueditor.setContent === "function"){ ueditor.setContent(question.answer[index] || question.answer[0]); } else { textareaElement.value = question.answer[index] || question.answer[0]; textareaElement.dispatchEvent(new Event("input", { bubbles: true })); textareaElement.dispatchEvent(new Event("change", { bubbles: true })); textareaElement.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true })); textareaElement.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true })); } } catch (e) { textareaElement.value = question.answer[index] || question.answer[0]; textareaElement.dispatchEvent(new Event("input", { bubbles: true })); textareaElement.dispatchEvent(new Event("change", { bubbles: true })); textareaElement.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true })); textareaElement.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true })); } }); } else if (question.type === "3") { const isLocalMode = question.source && question.source.startsWith("local"); if (isLocalMode) { this.addLog(`开始填写判断题答案: ${question.answer[0]}`, 'info'); } let answer = "false"; if (REGEX.JUDGE_FALSE.test(question.answer[0])) { answer = "false"; } else if (REGEX.JUDGE_TRUE.test(question.answer[0])) { answer = "true"; } if (isLocalMode) { this.addLog(`判断题答案解析: ${answer}`, 'info'); } let clicked = false; const optionKeys = Object.keys(question.options); if (optionKeys.length === 0) { if (isLocalMode) { this.addLog(`❌ 判断题未找到任何选项,使用DOM兜底查找`, 'warning'); } const radioInputs = question.element.querySelectorAll('input[type="radio"]'); if (radioInputs.length >= 2) { const targetIndex = answer === "true" ? 0 : 1; const targetInput = radioInputs[targetIndex]; if (targetInput) { targetInput.checked = true; targetInput.dispatchEvent(new Event('change', { bubbles: true })); targetInput.dispatchEvent(new Event('input', { bubbles: true })); const parentOption = targetInput.closest('li, .option, .option-list li, [class*="option"]'); if (parentOption) { parentOption.setAttribute("aria-checked", "true"); parentOption.classList.add("is-checked", "selected", "answer-selected"); parentOption.setAttribute("data-checked", "true"); parentOption.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } clicked = true; if (isLocalMode) { this.addLog(`✅ 通过DOM兜底找到并选中判断题选项`, 'success'); } } } } if (!clicked) { for (const key in question.options) { const optionElement = question.options[key]; if (!optionElement) continue; const ariaLabel = optionElement.getAttribute("aria-label") || ""; const optionText = key.replace(/^[A-Z][.、]\s*/, '').trim(); if (isLocalMode) { this.addLog(`检查选项: key="${key.substring(0, 30)}", text="${optionText.substring(0, 30)}", ariaLabel="${ariaLabel.substring(0, 50)}"`, 'debug'); } let shouldClick = false; if (answer === "true") { shouldClick = ariaLabel.includes("正确选择") || ariaLabel.includes("对选择") || optionText === "对" || optionText === "正确" || optionText === "是" || optionText === "√" || optionText.toLowerCase() === "true" || optionText.toLowerCase() === "yes" || optionText === "1" || key.toUpperCase().startsWith("A"); } else if (answer === "false") { shouldClick = ariaLabel.includes("错误选择") || ariaLabel.includes("错选择") || optionText === "错" || optionText === "错误" || optionText === "否" || optionText === "×" || optionText === "X" || optionText.toLowerCase() === "false" || optionText.toLowerCase() === "no" || optionText === "0" || key.toUpperCase().startsWith("B"); } if (shouldClick) { const isChecked = optionElement.getAttribute("aria-checked") === "true" || optionElement.classList.contains("is-checked") || optionElement.classList.contains("selected") || optionElement.classList.contains("answer-selected") || optionElement.getAttribute("data-checked") === "true"; const input = optionElement.querySelector('input[type="radio"]'); if (input && input.checked) { clicked = true; if (isLocalMode) { this.addLog(`判断题已选中: ${optionText}`, 'info'); } break; } if (isChecked) { clicked = true; if (isLocalMode) { this.addLog(`判断题已选中: ${optionText}`, 'info'); } break; } if (isLocalMode) { this.addLog(`点击判断题选项: ${optionText}`, 'info'); } optionElement.setAttribute("data-filling", "true"); clickOption(optionElement); setTimeout(() => optionElement.removeAttribute("data-filling"), 200); clicked = true; const randomWait = Math.floor(Math.random() * 400) + 300; await new Promise(resolve => setTimeout(resolve, randomWait)); try { const frameWindow = this._window || window; if (typeof frameWindow.saveQuestion === 'function') { frameWindow.saveQuestion(); if (isLocalMode) { this.addLog(`[调试] 判断题点击后已调用 saveQuestion()`, 'info'); } } } catch (saveErr) { if (isLocalMode) { this.addLog(`[调试] saveQuestion 调用失败: ${saveErr.message}`, 'warning'); } } const isNowChecked = optionElement.getAttribute("aria-checked") === "true" || optionElement.classList.contains("is-checked") || optionElement.classList.contains("selected") || optionElement.classList.contains("answer-selected"); if (isLocalMode) { if (isNowChecked) { this.addLog(`✅ 判断题已选中: ${optionText}`, 'success'); } else { let retrySuccess = false; const input = optionElement.querySelector('input[type="radio"]'); if (input) { input.checked = true; input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('input', { bubbles: true })); } optionElement.setAttribute("aria-checked", "true"); optionElement.classList.add("is-checked", "selected", "answer-selected"); optionElement.setAttribute("data-checked", "true"); const randomWait = Math.floor(Math.random() * 400) + 300; await new Promise(resolve => setTimeout(resolve, randomWait)); const finalChecked = optionElement.getAttribute("aria-checked") === "true" || optionElement.classList.contains("is-checked") || optionElement.classList.contains("selected") || optionElement.classList.contains("answer-selected") || (input && input.checked); if (finalChecked) { this.addLog(`✅ 判断题已选中: ${optionText} (重试成功)`, 'success'); retrySuccess = true; } if (!retrySuccess) { this.addLog(`❌ 判断题选中失败: ${optionText},请检查页面结构`, 'danger'); } } } break; } } } if (!clicked && optionKeys.length > 0) { const firstKey = optionKeys[0]; const firstOption = question.options[firstKey]; if (firstOption) { if (isLocalMode) { this.addLog(`⚠️ 所有匹配策略失败,使用兜底策略选择第一个选项: ${firstKey}`, 'warning'); } const input = firstOption.querySelector('input[type="radio"]'); if (input && input.checked) { clicked = true; } else { firstOption.setAttribute("data-filling", "true"); safeClick(firstOption); if (input) { input.focus(); input.click(); input.checked = true; input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('input', { bubbles: true })); input.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } const label = firstOption.querySelector('label'); if (label) { label.click(); label.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } firstOption.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); firstOption.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })); firstOption.dispatchEvent(new PointerEvent('pointerup', { bubbles: true })); firstOption.dispatchEvent(new PointerEvent('click', { bubbles: true })); firstOption.setAttribute("aria-checked", "true"); firstOption.classList.add("is-checked", "selected", "answer-selected"); firstOption.setAttribute("data-checked", "true"); setTimeout(() => firstOption.removeAttribute("data-filling"), 200); await new Promise(resolve => setTimeout(resolve, 400)); clicked = true; } } } if (isLocalMode) { if (clicked) { this.addLog(`判断题答案填写完成`, 'success'); } else { this.addLog(`判断题答案填写失败: 未找到任何选项`, 'danger'); } } } else if (question.type === "4" || question.type === "5" || question.type === "6" || question.type === "7") { const textareaElement = question.element.querySelector("textarea"); if (!textareaElement) return; const answerText = Array.isArray(question.answer) ? question.answer.join("\n") : String(question.answer); const htmlContent = answerText.split("\n") .map(line => `

${line}

`) .join(""); try { const ueditor = this._window.UE.getEditor(textareaElement.name); if (ueditor && typeof ueditor.setContent === "function") { ueditor.setContent(htmlContent); } else { textareaElement.value = answerText; textareaElement.dispatchEvent(new Event("input", { bubbles: true })); textareaElement.dispatchEvent(new Event("change", { bubbles: true })); textareaElement.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true })); textareaElement.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true })); } } catch (e) { textareaElement.value = answerText; textareaElement.dispatchEvent(new Event("input", { bubbles: true })); textareaElement.dispatchEvent(new Event("change", { bubbles: true })); textareaElement.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true })); textareaElement.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true })); } } else if (question.type === "13") { const sortSelects = question.element.querySelectorAll(".sortQuesSelect .dept_select"); if (sortSelects.length === 0) return; let answers = question.answer; if (typeof question.answer === 'string') { if (question.answer.startsWith('[')) { try { answers = JSON.parse(question.answer); } catch (e) { answers = question.answer.split(/[,,]/).map(a => a.trim()).filter(a => a); } } else { answers = question.answer.split(/[,,]/).map(a => a.trim()).filter(a => a); } } sortSelects.forEach((select, index) => { if (index >= answers.length) return; const answerValue = answers[index].toUpperCase(); select.value = answerValue; const chosenContainer = select.nextElementSibling; if (chosenContainer && chosenContainer.classList.contains("chosen-container")) { const chosenSingle = chosenContainer.querySelector(".chosen-single span"); if (chosenSingle) { chosenSingle.textContent = answerValue; } select.dispatchEvent(new Event("change", { bubbles: true })); const chosenResults = chosenContainer.querySelectorAll(".chosen-results li"); chosenResults.forEach(li => { li.classList.remove("result-selected"); if (li.textContent.trim() === answerValue) { li.classList.add("result-selected"); } }); } }); const answerInput = question.element.querySelector('input[name^="answer"]'); if (answerInput) { answerInput.value = answers.join(""); } } } catch (error) { this.addLog(`答题过程发生错误:${error.message}`, "danger"); } finally { this.isFilling = false; } }); this.type = type; if (iframe) { this._document = iframe.contentDocument; this._window = iframe.contentWindow; decodeCipherFont(this._document); } else { decodeCipherFont(this._document); } } extractOptions(optionElements, optionSelector) { const optionsObject = {}; const optionTexts = []; optionElements.forEach((optionElement) => { var _a; const optionTextContent = this.stripTags(((_a = optionElement.querySelector(optionSelector)) == null ? void 0 : _a.innerHTML) || ""); // 存储外层容器元素 - 用于验证选中状态(aria-checked)和点击 optionsObject[optionTextContent] = optionElement; optionTexts.push(optionTextContent); }); return [optionsObject, optionTexts]; } addQuestions(questionElements) { questionElements.forEach((questionElement) => { var _a, _b, _c, _d; let questionTitle = ""; let questionTypeText = ""; let optionElements = []; let optionsObject = {}; let optionTexts = []; if (["zy", "ks"].includes(this.type)) { const h3Element = questionElement.querySelector("h3"); const colorShallowElement = questionElement.querySelector(".colorShallow"); if (["zy"].includes(this.type)) { questionTypeText = (questionElement == null ? void 0 : questionElement.getAttribute("typename")) || ""; } else if (["ks"].includes(this.type)) { questionTypeText = colorShallowElement ? this.stripTags(colorShallowElement.outerHTML).slice(1, 4) : ""; } const fullText = h3Element ? h3Element.textContent : ""; const typeText = colorShallowElement ? colorShallowElement.textContent : ""; questionTitle = fullText.replace(typeText, "").replace(/^\d+\.\s*/, "").replace(/_+/g, "").trim(); optionElements = questionElement.querySelectorAll(SELECTORS.CX_OPTION_ZY_KS); if (!optionElements.length) { optionElements = questionElement.querySelectorAll('.answerBg, .answer_p, ul li, .option, .option-list li'); } [optionsObject, optionTexts] = this.extractOptions(optionElements, ".answer_p"); } else if (["zj"].includes(this.type)) { questionTitle = this.stripTags(((_c = questionElement.querySelector(".fontLabel")) == null ? void 0 : _c.innerHTML) || ""); questionTypeText = this.stripTags(((_d = questionElement.querySelector(".newZy_TItle")) == null ? void 0 : _d.innerHTML) || ""); optionElements = questionElement.querySelectorAll(SELECTORS.CX_OPTION_ZJ); if (!optionElements.length) { optionElements = questionElement.querySelectorAll('[class*="before-after"], ul li, .option, .option-list li, label:not(.before)'); } [optionsObject, optionTexts] = this.extractOptions(optionElements, ".fl.after"); if (!questionTitle) { const titleEl = questionElement.querySelector('.Zy_TItle, .clearfix, h3, .question-title'); if (titleEl) { questionTitle = this.stripTags(titleEl.innerHTML || titleEl.textContent || ""); } } } // 增强型题型识别:结合文本关键词和DOM元素特征 let questionType = "999"; let typeIdentifyLog = []; // 步骤1:从文本中提取题型关键词(优先) const cleanedTypeText = questionTypeText.replace(/[\[\]【】]/g, ""); if (this.typeMap.has(cleanedTypeText)) { questionType = this.typeMap.get(cleanedTypeText); typeIdentifyLog.push(`文本关键词(typename): ${cleanedTypeText} -> ${questionType}`); } else if (questionTitle.match(/[\[\【](.+?)[\]\】]/)) { const matchedType = questionTitle.match(/[\[\【](.+?)[\]\】]/)[1]; if (this.typeMap.has(matchedType)) { questionType = this.typeMap.get(matchedType); typeIdentifyLog.push(`文本关键词(标题): ${matchedType} -> ${questionType}`); } } // 获取所有 DOM 元素计数(不管文本识别是否成功) const radioCount = questionElement.querySelectorAll('input[type="radio"]').length; const checkboxCount = questionElement.querySelectorAll('input[type="checkbox"]').length; const textareaCount = questionElement.querySelectorAll('textarea').length; const inputTextCount = questionElement.querySelectorAll('input[type="text"]').length; // 步骤2:DOM元素计数(仅在步骤1无法确定时使用) // radio=2 → 判断题, radio>2 → 单选题, checkbox>2 → 多选题, textarea≥1 → 填空题 if (questionType === "999") { typeIdentifyLog.push(`DOM计数: radio=${radioCount}, checkbox=${checkboxCount}, textarea=${textareaCount}`); if (textareaCount >= 1) { questionType = "2"; // 填空题 typeIdentifyLog.push(`DOM(textarea≥1) -> ${questionType}`); } else if (radioCount === 2) { questionType = "3"; // 判断题 (恰好2个radio) typeIdentifyLog.push(`DOM(radio=2) -> ${questionType}`); } else if (checkboxCount >= 2) { questionType = "1"; // 多选题 (2个以上checkbox) typeIdentifyLog.push(`DOM(checkbox≥2) -> ${questionType}`); } else if (radioCount > 2) { questionType = "0"; // 单选题 (3个以上radio) typeIdentifyLog.push(`DOM(radio>2) -> ${questionType}`); } } else { // 文本识别成功,仍然记录 DOM 计数供调试 typeIdentifyLog.push(`DOM计数: radio=${radioCount}, checkbox=${checkboxCount}`); } // DOM判断失败时,用文本+特征兜底 if (questionType === "999") { const hasSortSelect = questionElement.querySelector('.sortQuesSelect') !== null; if (hasSortSelect) { questionType = "13"; // 排序题 } else if (inputTextCount > 0 && Object.keys(optionsObject).length === 0) { questionType = "2"; // 填空题(文本输入) } else if (optionsTexts.length === 2 && (optionsTexts.some(o => /^(对|正确|是|true|√)$/i.test(o.trim())) && optionsTexts.some(o => /^(错|错误|否|false|×)$/i.test(o.trim())))) { questionType = "3"; // 判断题(对/错) } else if (optionsTexts.length >= 4) { questionType = "0"; // 默认单选题(4个选项) } else { questionType = "0"; // 最后兜底 } typeIdentifyLog.push(`兜底 -> ${questionType}`); } // 记录识别过程(在后面添加到 questions 对象中) const identifyInfo = { title: questionTitle.substring(0, 50) + '...', type: questionType, log: typeIdentifyLog.join('; ') }; this.questions.push({ element: questionElement, type: questionType, title: this.trimTitle(questionTitle), optionsText: optionTexts, options: optionsObject, answer: [], workType: this.type, refer: this._window.location.href, identifyInfo: identifyInfo // 保存识别信息供调试 }); }); } } const useCxChapterLogic = () => { const logStore = useLogStore(); const progressStore = useProgressStore(); const getCookieLocal = (name) => { const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); return match ? match[2] : ''; }; let cachedUid = ''; if (_unsafeWindow?.getCookie) { cachedUid = _unsafeWindow.getCookie("UID"); } if (!cachedUid) { cachedUid = getCookieLocal('UID'); } if (!cachedUid) { cachedUid = getCookieLocal('_uid'); } if (!cachedUid && _unsafeWindow?.uid) { cachedUid = _unsafeWindow.uid; } const init = () => { const currentUrl = window.location.href; if (!currentUrl.includes("&mooc2=1")) { window.location.href = currentUrl + "&mooc2=1"; } logStore.addLog(`检测到用户进入到章节学习页面`, "primary"); logStore.addLog(`正在解析任务点,请稍等5-10秒(如果长时间没有反应,请刷新页面)`, "warning"); }; const configStore = useConfigStore(); let _justClickedNext = false; let _currentIframe = null; let _urlBackupTimer = null; const checkUnansweredAssignments = async (documentElement) => { try{ const allIframes = FrameScanner.collectDeepSync(documentElement); for(const iframe of allIframes){ const src = iframe.src || ''; const _src = iframe.getAttribute('_src') || ''; // 增强版检测逻辑:覆盖更多答题入口模式 const isAssignment = src.includes('api/work') || _src.includes('api/work') || src.includes('work') || src.includes('exam') || src.includes('test') || src.includes('quiz') || src.includes('zuoye') || src.includes('kaoshi') || src.includes('answer') || src.includes('dowork') || src.includes('doTest') || src.includes('calcAnswer') || src.includes('work/dowork') || src.includes('work/doTest') || src.includes('mooc2/work') || src.includes('work/index') || src.includes('work/list') || _src.includes('work') || _src.includes('exam') || _src.includes('test') || _src.includes('quiz') || _src.includes('answer'); if(isAssignment){ try{ const ansJobIcon = iframe.parentElement?.querySelector('.ans-job-icon'); if(ansJobIcon){ const ariaLabel = ansJobIcon.getAttribute('aria-label') || ''; const titleAttr = ansJobIcon.getAttribute('title') || ''; const textContent = ansJobIcon.textContent || ''; // 检查是否已完成 - 增强检测 const isCompleted = ariaLabel.includes('已完成') || titleAttr.includes('已完成') || textContent.includes('已完成') || ariaLabel.includes('100%') || titleAttr.includes('100%'); if(isCompleted){ continue; } // 未完成的作业 logStore.addLog(`发现未完成的作业/测验任务点: ${ariaLabel || titleAttr || '未知状态'}`, "warning"); return true; }else{ const parent = iframe.parentElement; const jobWrapper = parent?.querySelector('.ans-job-wrapper, .job-wrapper'); if(jobWrapper && !jobWrapper.classList.contains('complete')){ logStore.addLog(`发现未标记完成的作业/测验任务点`, "warning"); return true; } } }catch(e){ // 跨域 iframe 无法访问,假设未完成 logStore.addLog(`发现作业/测验任务点(跨域无法检测状态)`, "warning"); return true; } } } // 第二轮检测:通过DOM元素查找答题入口 const assignmentSelectors = [ '.ans-job-icon:not([aria-label*="已完成"])', '.ans-job-icon:not([title*="已完成"])', '[class*="job"]:not([class*="complete"])', '[class*="work"]:not([class*="complete"])', '[class*="exam"]:not([class*="complete"])', '[class*="quiz"]:not([class*="complete"])', 'a[href*="work"]:not([class*="complete"])', 'a[href*="exam"]:not([class*="complete"])', 'a[href*="test"]:not([class*="complete"])', 'a[href*="answer"]:not([class*="complete"])', '.jobclass:not(.complete)', '.work-icon:not(.complete)', '.exam-icon:not(.complete)', '.assignment-icon:not(.complete)', '.ans-job:not(.complete)', '.ans-job-btn:not(.complete)', // 新增:黄色数字标记选择器(章节旁边的橙色/黄色圆形数字标记) '.orangeIcon', '.orange-icon', '.numIcon', '.icon-num', '.posCatalog_select:has(.jobUnfinishCount)', '.posCatalog_item:has(.jobUnfinishCount)', '[style*="background-color:#f5a623"]', '[style*="background-color:orange"]', '[style*="background-color:#FFA500"]', '.catalog-num', '.chapter-num' ]; for(const selector of assignmentSelectors){ try{ const elements = documentElement.querySelectorAll(selector); for(const el of elements){ const ariaLabel = el.getAttribute('aria-label') || ''; const titleAttr = el.getAttribute('title') || ''; const textContent = el.textContent || ''; // 检查是否包含"未完成"、"待完成"等关键词 const isUnfinished = ariaLabel.includes('未完成') || titleAttr.includes('未完成') || textContent.includes('未完成') || ariaLabel.includes('待完成') || titleAttr.includes('待完成') || textContent.includes('待完成') || (!ariaLabel.includes('已完成') && !titleAttr.includes('已完成')); if(isUnfinished && el.offsetParent !== null){ const rect = el.getBoundingClientRect(); if(rect.width > 0 && rect.height > 0){ logStore.addLog(`通过DOM选择器发现未完成的答题入口: ${selector}`, "warning"); return true; } } } }catch(e){ // 忽略无效选择器 } } // 第三轮检测:通过文本内容查找 const allElements = documentElement.querySelectorAll('a, button, span, div'); for(const el of allElements){ const text = el.textContent || ''; const href = el.getAttribute('href') || ''; // 检查是否包含答题相关关键词且不包含"已完成" if(/作业|测验|考试|答题|work|exam|test|quiz/.test(text) && !text.includes('已完成') && !text.includes('100%') && el.offsetParent !== null && el.getBoundingClientRect().width > 0 && el.getBoundingClientRect().height > 0){ // 检查链接是否指向答题页面 if(href.includes('work') || href.includes('exam') || href.includes('test') || href.includes('answer')){ logStore.addLog(`通过文本发现未完成的答题入口`, "warning"); return true; } } } // 第四轮检测:检查是否有作业/测验iframe但无法访问其内容(跨域) for(const iframe of allIframes){ const src = iframe.src || ''; const _src = iframe.getAttribute('_src') || ''; // 如果iframe的src包含作业相关关键词,但无法访问其内容 if((src.includes('work') || src.includes('exam') || src.includes('test') || src.includes('quiz') || _src.includes('work') || _src.includes('exam')) && !src.includes('video') && !src.includes('audio')){ try{ // 尝试访问iframe的内容,如果失败说明是跨域的 const contentDoc = iframe.contentDocument || iframe.contentWindow.document; if(!contentDoc){ logStore.addLog(`发现跨域的作业/测验iframe,假设未完成`, "warning"); return true; } }catch(e){ // 跨域访问失败,假设未完成 logStore.addLog(`发现跨域的作业/测验iframe,假设未完成`, "warning"); return true; } } } // 第五轮检测:专门检测黄色/橙色数字标记(章节列表中未完成测验的标记) const numberBadgeSelectors = [ '.catalog_num', '.catalog-num', '.chapter-num', '.section-num', '.posCatalog_num', '.orangeNum', '.yellowNum', '[class*="num"]:not([class*="icon"])', '.icon-num', '.num-icon', // 新增:学习通特有的数字标记选择器 '.catalogNum', '.catalog-num-icon', '.chapterNum', '.sectionNum', '.posCatalogNum', '.lesson-num', '.lessonNum', '.iconNum', '.numIcon', '[class*="catalog"] [class*="num"]', '[class*="chapter"] [class*="num"]', '[class*="section"] [class*="num"]', '[class*="lesson"] [class*="num"]', // 圆形徽章选择器 '.badge-num', '.badge-circle', '.circle-badge', '.num-circle', '.circle-num', // 学习通章节列表特定选择器 '.posCatalog_item .orangeIcon', '.posCatalog_select .orangeIcon', '.chapter-item .orangeIcon', '.section-item .orangeIcon', '.posCatalog_item .icon_yellow', '.posCatalog_select .icon_yellow', '.chapter-item .icon_yellow', '.section-item .icon_yellow', // ========== 新增:学习通章节列表特有选择器 ========== // 章节列表容器 '.posCatalog', '.posCatalog_list', '.posCatalog_content', '.chapter-list', '.section-list', '.lesson-list', '.course-tree', '.course-chapter', // 章节列表项(包含数字标记的父元素) '.posCatalog_select', '.posCatalog_item', '.posCatalog li', '.chapterItem', '.chapter-item', '.sectionItem', '.section-item', '.lessonItem', '.lesson-item', // 学习通移动端章节列表 '.m-chapter', '.m-section', '.m-lesson', '.mobile-chapter', '.mobile-section', // 图标选择器 '.icon_homework', '.icon_exam', '.icon_test', '.icon_quiz', '.icon_task', '.icon_work', '[class*="icon_homework"]', '[class*="icon_exam"]', '[class*="icon_test"]', '[class*="icon_quiz"]', '[class*="icon_task"]', '[class*="icon_work"]', // 学习通数据属性选择器 '[data-role="chapter"]', '[data-role="section"]', '[data-role="lesson"]', '[data-id^="chapter"]', '[data-id^="section"]', '[data-id^="lesson"]', '[data-type="chapter"]', '[data-type="section"]', '[data-type="lesson"]', '[data-type="homework"]', '[data-type="quiz"]', '[data-type="test"]', '[data-type="exam"]' ]; for(const selector of numberBadgeSelectors){ try{ const elements = documentElement.querySelectorAll(selector); for(const el of elements){ const text = el.textContent || ''; const style = el.getAttribute('style') || ''; const className = el.className || ''; // 检查是否是数字标记(通常是1-9的数字) if(/^\d+$/.test(text.trim()) && parseInt(text.trim()) > 0){ // 检查是否是黄色/橙色标记 const isOrangeYellow = style.includes('orange') || style.includes('#f5a623') || style.includes('#FFA500') || style.includes('#FFC107') || className.includes('orange') || className.includes('yellow'); if(isOrangeYellow){ logStore.addLog(`发现黄色/橙色数字标记: ${text},表示有未完成的任务`, "warning"); return true; } // 如果是数字标记,但没有颜色,检查父元素是否有作业相关的类名 const parent = el.parentElement; if(parent && (parent.className.includes('job') || parent.className.includes('work') || parent.className.includes('exam') || parent.className.includes('quiz'))){ logStore.addLog(`发现作业/测验数字标记: ${text}`, "warning"); return true; } // 检查父元素是否为学习通章节项 const chapterItem = el.closest('.posCatalog_select, .posCatalog_item, .chapterItem, .chapter-item, .sectionItem, .section-item, .lessonItem, .lesson-item'); if(chapterItem){ // 检查章节项是否有未完成标记 const isCompleted = chapterItem.classList.contains('completed') || chapterItem.classList.contains('icon_Completed') || chapterItem.querySelector('.icon_Completed') || chapterItem.getAttribute('aria-label')?.includes('已完成') || chapterItem.textContent?.includes('已完成'); if(!isCompleted){ logStore.addLog(`发现未完成的章节项,包含数字标记: ${text}`, "warning"); return true; } } // 检查祖先元素是否有作业相关的类名 let ancestor = el.parentElement; for(let i = 0; i < 5 && ancestor; i++){ if(ancestor.className.includes('posCatalog') || ancestor.className.includes('chapter') || ancestor.className.includes('section') || ancestor.className.includes('lesson') || ancestor.className.includes('task')){ logStore.addLog(`发现章节/任务列表中的数字标记: ${text}`, "warning"); return true; } ancestor = ancestor.parentElement; } } } }catch(e){ // 忽略无效选择器 } } // 第六轮检测:专门检测学习通章节列表中的测验入口 // 学习通章节列表结构通常是:.posCatalog > li > .posCatalog_select/.posCatalog_item const posCatalogItems = documentElement.querySelectorAll('.posCatalog li, .posCatalog_select, .posCatalog_item'); for(const item of posCatalogItems){ try{ // 检查是否有未完成标记(黄色/橙色徽章) const hasOrangeIcon = item.querySelector('.orangeIcon, .icon_yellow, [class*="orange"], [class*="yellow"]'); const hasNumBadge = item.querySelector('[class*="num"]:not([class*="icon"])'); if(hasOrangeIcon || hasNumBadge){ // 检查是否已完成 const isCompleted = item.classList.contains('completed') || item.querySelector('.icon_Completed') || item.textContent?.includes('已完成') || item.getAttribute('aria-label')?.includes('已完成'); if(!isCompleted){ // 检查是否包含测验相关的子元素 const hasQuizIcon = item.querySelector('.icon_homework, .icon_exam, .icon_test, .icon_quiz'); const hasQuizText = item.textContent?.includes('测验') || item.textContent?.includes('作业') || item.textContent?.includes('考试'); if(hasQuizIcon || hasQuizText){ logStore.addLog(`发现未完成的章节测验任务点`, "warning"); return true; } // 如果有数字标记,假设是未完成的任务 if(hasNumBadge){ const numText = hasNumBadge.textContent || ''; if(/^\d+$/.test(numText.trim()) && parseInt(numText.trim()) > 0){ logStore.addLog(`发现章节列表中的数字标记: ${numText},表示有未完成的任务`, "warning"); return true; } } } } }catch(e){ // 忽略异常 } } // 第七轮检测:通过颜色和形状检测黄色圆形标记 const allSpans = documentElement.querySelectorAll('span, i, div'); for(const el of allSpans){ const style = window.getComputedStyle ? window.getComputedStyle(el) : el.currentStyle; if(!style) continue; const bgColor = style.backgroundColor || style.background; const borderRadius = style.borderRadius || ''; // 检查是否是圆形(borderRadius约为50%)且背景色是黄色/橙色 const isCircle = borderRadius && (borderRadius.includes('50%') || borderRadius.includes('100%') || borderRadius.includes('circle')); const isYellowOrange = bgColor && (bgColor.includes('orange') || bgColor.includes('250, 166, 35') || // #f5a623 bgColor.includes('255, 165, 0') || // #FFA500 bgColor.includes('255, 193, 7')); // #FFC107 if(isCircle && isYellowOrange){ const text = el.textContent || ''; if(/^\d+$/.test(text.trim()) && parseInt(text.trim()) > 0){ logStore.addLog(`发现黄色圆形数字标记: ${text},表示有未完成的任务`, "warning"); return true; } } } return false; }catch(e){ console.warn('[checkUnansweredAssignments] 检测异常:', e); return false; } }; const monitorIframes = () => { if (_urlBackupTimer) { clearTimeout(_urlBackupTimer); _urlBackupTimer = null; } const documentElement = document.documentElement; const iframe = documentElement.querySelector("iframe"); if (!iframe) { setTimeout(() => monitorIframes(), 2000); return; } const currentSrc = iframe.src || ''; const srcChanged = currentSrc !== _lastIframeSrc; if (iframe !== _currentIframe) { _currentIframe = iframe; iframe.addEventListener("load", function onIframeLoad() { if (_urlBackupTimer) { clearTimeout(_urlBackupTimer); _urlBackupTimer = null; } monitorIframes(); }); } try { if (iframe.contentDocument && iframe.contentDocument.readyState === 'complete') { if (srcChanged) { _lastIframeSrc = currentSrc; watchIframe(documentElement); } } } catch (e) { } }; const watchUrlChanges = () => { let currentUrl = window.location.href; const afkEnabled = configStore.platformParams.cx.parts[4]?.params[0]?.value || false; const checkUrlChange = () => { if (currentUrl !== window.location.href) { currentUrl = window.location.href; _justClickedNext = false; _globalTaskId++; if (_urlBackupTimer) { clearTimeout(_urlBackupTimer); } _urlBackupTimer = setTimeout(() => { _urlBackupTimer = null; monitorIframes(); }, 2000); } }; if (afkEnabled) { BackgroundWorker.start('url_watcher', checkUrlChange, 2000); } else { setInterval(checkUrlChange, 2000); } }; let _globalTaskId = 0; let hasLoggedSkipTip = false; let _currentWatchSubscription = null; const _processedIframeTasks = new WeakMap(); let _lastIframeSrc = null; window.__getAnswerTaskId__ = () => _globalTaskId; const watchIframe = async (documentElement) => { if (_currentWatchSubscription) { _currentWatchSubscription.unsubscribe(); _currentWatchSubscription = null; } forceStopSimulatePlay(); const thisTaskId = ++_globalTaskId; hasLoggedSkipTip = false; const waitForQuizPage = async (timeout = 15000) => { if (typeof AutoTaskScheduler !== 'undefined' && typeof AutoTaskScheduler._waitForQuizPage === 'function') { return AutoTaskScheduler._waitForQuizPage(timeout); } logStore.addLog(`答题页面检测方法不可用`, "warning"); return false; }; const autoAnswerQuiz = async () => { if (typeof AutoTaskScheduler !== 'undefined' && typeof AutoTaskScheduler._autoAnswer === 'function') { return AutoTaskScheduler._autoAnswer(); } logStore.addLog(`自动答题方法不可用`, "warning"); return false; }; const submitQuiz = async () => { if (typeof AutoTaskScheduler !== 'undefined' && typeof AutoTaskScheduler._submitQuiz === 'function') { return AutoTaskScheduler._submitQuiz(); } logStore.addLog(`提交答题方法不可用`, "warning"); return false; }; const fallbackToQuizUrl = async () => { if (typeof AutoTaskScheduler !== 'undefined' && typeof AutoTaskScheduler._fallbackToQuizUrl === 'function') { return AutoTaskScheduler._fallbackToQuizUrl(); } logStore.addLog(`兜底跳转方法不可用`, "warning"); return false; }; const findChapterQuizTabInDoc = (doc) => { try { const direct = doc.querySelector('option[value="章节测验"], li[title*="章节测验"], a[title*="章节测验"], button[title*="章节测验"], [role="tab"][title*="章节测验"]'); if (direct) return direct; const onclickTarget = Array.from(doc.querySelectorAll('li[onclick], a[onclick], button[onclick], span[onclick], div[onclick]')).find(el => { const onclick = String(el.getAttribute('onclick') || ''); return /changeDisplayContent|getTeacherAjax|chapter|job|work|quiz/i.test(onclick) && /章节测验|测验|作业|quiz|work/i.test((el.textContent || el.getAttribute('title') || '') + onclick); }); if (onclickTarget) return onclickTarget; return Array.from(doc.querySelectorAll('li, a, button, span, div, [role="tab"], [role="option"]')).find(el => { const text = String(el.textContent || el.getAttribute('title') || '').trim(); if (!/章节测验|测验/.test(text)) return false; const rect = typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : { width: 1, height: 1 }; return rect.width > 0 && rect.height > 0; }) || null; } catch(e) { return null; } }; const getSameOriginDocs = () => { const docs = [document]; for (const iframe of document.querySelectorAll('iframe')) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (iframeDoc) docs.push(iframeDoc); } catch(e) {} } return docs; }; const hasActiveUnfinishedVideoTask = () => { const docs = getSameOriginDocs(); for (const doc of docs) { try { const videos = Array.from(doc.querySelectorAll('video')); for (const video of videos) { const duration = Number(video.duration || 0); const currentTime = Number(video.currentTime || 0); if (duration > 0 && currentTime / duration < 0.9 && !video.ended) return true; if (duration === 0 && !video.ended) return true; } const text = String(doc.body?.innerText || doc.body?.textContent || ''); const hasVideoTab = /\b1\s*视频|视频/.test(text); const hasVideoRequirement = /观看时长|总时长|90%|任务点/.test(text); const hasCompleted = /任务点已完成|已完成|100%/.test(text); const hasQuizPage = /我的答案|单选题|多选题|判断题|填空题|本题得分/.test(text); if (hasVideoTab && hasVideoRequirement && !hasCompleted && !hasQuizPage) return true; } catch(e) {} } return false; }; const getCxStudyChapters = () => { const docs = getSameOriginDocs(); const chapters = []; for (const doc of docs) { try { const nodes = Array.from(doc.querySelectorAll('[onclick*="getTeacherAjax"]')); for (const el of nodes) { const onclick = String(el.getAttribute('onclick') || ''); const match = onclick.match(/getTeacherAjax\(['"](.+?)['"],\s*['"](.+?)['"],\s*['"](.+?)['"]\)/); if (!match) continue; const chapterId = match[3]; const container = el.closest('.posCatalog_select, .posCatalog_item, li, .chapterItem, .chapter-item') || el.parentElement; const countEl = container?.querySelector('.jobUnfinishCount, input.jobUnfinishCount'); const rawCount = countEl ? (countEl.value || countEl.textContent || countEl.getAttribute('value') || '0') : '0'; const unFinishCount = parseInt(String(rawCount).replace(/\D/g, ''), 10) || 0; chapters.push({ doc, element: el, container, courseId: match[1], classId: match[2], chapterId, unFinishCount }); } } catch(e) {} } return chapters; }; const tryCxStudyDispatcher = async () => { if (!/\/mycourse\/studentstudy/.test(location.href)) return false; const chapters = getCxStudyChapters(); const target = chapters.find(chapter => chapter.unFinishCount > 0); if (!target) return false; logStore.addLog(`按章节目录定位到未完成章节(${target.chapterId}),未完成任务数: ${target.unFinishCount}`, "info"); try { const targetWindow = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; if (!target.container?.classList?.contains('posCatalog_active')) { if (typeof targetWindow.getTeacherAjax === 'function') { targetWindow.getTeacherAjax(target.courseId, target.classId, target.chapterId); } else { safeClick(target.element); } logStore.addLog(`已切换到未完成章节,等待内容加载`, "info"); await new Promise(resolve => setTimeout(resolve, 2500)); } if (target.container) { try { target.container.scrollIntoView({ behavior: "smooth", block: "center" }); } catch(e) {} } if (hasActiveUnfinishedVideoTask()) { logStore.addLog(`当前视频任务未完成,先完成视频,再进入章节测验`, "info"); setTimeout(() => monitorIframes(), 2000); return true; } const clickedQuizTab = await findAndClickChapterQuizTab(); if (clickedQuizTab) { logStore.addLog(`等待章节测验内容加载...`, "info"); const quizLoaded = await waitForQuizPage(12000); if (quizLoaded) { logStore.addLog(`章节测验页面加载成功,开始自动答题`, "success"); await autoAnswerQuiz(); await submitQuiz(); } else { logStore.addLog(`章节测验未检测到题目,回到任务点扫描`, "warning"); setTimeout(() => monitorIframes(), 1000); } } else { logStore.addLog(`未找到章节测验标签,按当前章节任务点继续扫描`, "warning"); setTimeout(() => monitorIframes(), 1000); } return true; } catch(e) { logStore.addLog(`章节调度失败,回退通用入口扫描: ${e.message}`, "warning"); return false; } }; const findAndClickChapterQuizTab = async () => { const docs = [document]; for (const iframe of document.querySelectorAll('iframe')) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (iframeDoc) docs.push(iframeDoc); } catch(e) {} } for (const doc of docs) { const tab = findChapterQuizTabInDoc(doc); if (!tab) continue; try { if (tab.tagName === 'OPTION') { tab.selected = true; const select = tab.closest('select'); if (select) select.dispatchEvent(new Event('change', { bubbles: true })); tab.click(); } else { safeClick(tab); } logStore.addLog(`已点击章节测验标签`, "success"); await new Promise(resolve => setTimeout(resolve, 2500)); return true; } catch(e) { logStore.addLog(`点击章节测验标签失败: ${e.message}`, "warning"); } } return false; }; await new Promise(resolve => setTimeout(resolve, 1500)); if (thisTaskId !== _globalTaskId) { return; } logStore.addLog(`等待页面加载完成,开始扫描任务点`, "info"); FrameScanner.collectDeep(documentElement).subscribe((allIframes) => { _currentWatchSubscription = rxjs.from(allIframes).pipe(concatMap((iframe) => handleSingleFrame(iframe))).subscribe({ complete: async () => { try { if (thisTaskId === _globalTaskId) { // 智能等待作业 iframe 加载:最多等待 8 秒,每 1 秒检测一次(参考jinmu.js轮询模式) let hasUnansweredAssignments = false; const maxWaitTime = 8000; const checkInterval = 1000; let waitedTime = 0; while (waitedTime < maxWaitTime) { await new Promise(resolve => setTimeout(resolve, checkInterval)); waitedTime += checkInterval; if (thisTaskId !== _globalTaskId) { return; } // 每次检测前重新扫描 iframe(可能有新的动态加载) hasUnansweredAssignments = await checkUnansweredAssignments(documentElement); if (hasUnansweredAssignments) { logStore.addLog(`第 ${Math.floor(waitedTime / checkInterval)} 次检测发现未完成任务`, "warning"); break; } } if (thisTaskId !== _globalTaskId) { return; } if (!hasUnansweredAssignments) { logStore.addLog(`未检测到未完成的答题任务`, "info"); } if(hasUnansweredAssignments){ // === 【修复】先检查是否已经在答题页面 === const alreadyOnAnswerPage = document.querySelector('.TiMu, .questionLi, .subject_node, [class*="TiMu"], .examPaper_subject, .answerArea') !== null; if(alreadyOnAnswerPage){ logStore.addLog(`已在答题页面,直接开始答题`, "info"); // 直接触发答题 try{ if(typeof autoAnswerOnce === 'function') setTimeout(autoAnswerOnce, 500); }catch(e){} try{ if(typeof startAutoLoop === 'function') setTimeout(startAutoLoop, 500); }catch(e){} return; } logStore.addLog(`检测到未完成的作业/测验,准备跳转答题`, "warning"); const cxDispatched = await tryCxStudyDispatcher(); if (cxDispatched) { return; } logStore.addLog(`正在定位答题入口...`, "info"); // 等待页面稳定 await quickRandomDelay(); // 增强版答题入口选择器 - 覆盖学习通各种DOM结构 // 【修复】优先使用 .jobUnfinishCount 检测未完成任务的章节 const answerEntrySelectors = [ // === 第一优先级:有未完成任务的章节项(最准确) === '.posCatalog_select:has(.jobUnfinishCount)', '.posCatalog_select:has(input.jobUnfinishCount)', // === 第二优先级:学习通标准答题入口 === '.ans-job-icon:not(.ans-job-icon-clear)', '.ans-job-icon:not(.ans-job-finished)', '.jobclass:not(.finished)', // === 第三优先级:作业/测验链接 === 'a[href*="workId"]', 'a[href*="examId"]', 'a[href*="testId"]', 'a[href*="ans-quiz"]', 'a[href*="zy"]', 'a[href*="ks"]', '.work-icon', '.exam-icon', '.assignment-icon', // === 第四优先级:学习通实际DOM结构 === '.ans-job', '.ans-job-title', '.ans-job-content', '.ans-job-btn', '[class*="ans-job"]', // === 第五优先级:章节列表项(带任务标记) === '.posCatalog_select', '.posCatalog_item', '.chapterItem', '.chapter_item', '.section_item', // === 第六优先级:作业/测验容器 === '.workList', '.work_list', '[class*="workList"]', '.examList', '.exam_list', '[class*="examList"]', '.testList', '.test_list', '[class*="testList"]', // === 第七优先级:按钮类型 === '.btn_work', '.btn_exam', '.btn_test', '.btn_answer', 'button[class*="job"]', 'button[class*="work"]', 'button[class*="exam"]', 'button[class*="test"]', // === 第八优先级:iframe中的答题入口 === 'iframe[src*="work"]', 'iframe[src*="exam"]', 'iframe[src*="test"]', 'iframe[src*="quiz"]', 'iframe[src*="ans-quiz"]', // === 第九优先级:移动端适配选择器 === '[data-type="quiz"]', '[data-type="test"]', '[data-type="exam"]', '[data-type="homework"]', '[data-type="work"]', '[data-action="quiz"]', '[data-action="test"]', '[data-action="exam"]' ]; let answerEntry = null; let foundSelector = ''; // 第一轮:查找可见的答题入口 for (const selector of answerEntrySelectors) { try { const el = document.querySelector(selector); if (el && el.offsetParent !== null) { const rect = el.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { answerEntry = el; foundSelector = selector; logStore.addLog(`找到答题入口: ${selector}`, "success"); break; } } } catch (e) { // 忽略无效选择器 } } // 第二轮:如果没找到,尝试查找隐藏的答题入口(可能在iframe中) if (!answerEntry) { logStore.addLog(`未在主页找到答题入口,尝试在iframe中查找...`, "info"); const iframes = document.querySelectorAll('iframe'); for (const iframe of iframes) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; if (iframeDoc) { for (const selector of answerEntrySelectors) { try { const el = iframeDoc.querySelector(selector); if (el) { answerEntry = el; foundSelector = `iframe:${selector}`; logStore.addLog(`在iframe中找到答题入口: ${foundSelector}`, "success"); break; } } catch (e) { // 跨域iframe无法访问 } } } } catch (e) { // 跨域iframe无法访问 } if (answerEntry) break; } } // 第三轮:如果还是没找到,尝试通过文本内容查找 if (!answerEntry) { logStore.addLog(`尝试通过文本内容查找答题入口...`, "info"); const allElements = document.querySelectorAll('*'); for (const el of allElements) { const text = el.textContent || ''; if (/作业|测验|考试|答题|work|exam|test|quiz/.test(text) && el.offsetParent !== null && el.getBoundingClientRect().width > 0 && el.getBoundingClientRect().height > 0) { // 优先选择按钮或链接 if (el.tagName === 'BUTTON' || el.tagName === 'A' || el.classList.contains('ans-job')) { answerEntry = el; foundSelector = `text:${text.substring(0, 20)}...`; logStore.addLog(`通过文本找到答题入口: ${foundSelector}`, "success"); break; } } } } // 第四轮:如果还是没找到,尝试通过XPath查找 if (!answerEntry) { logStore.addLog(`尝试通过XPath查找答题入口...`, "info"); const xpathExpressions = [ '//a[contains(text(), "作业")]', '//a[contains(text(), "测验")]', '//a[contains(text(), "考试")]', '//a[contains(text(), "答题")]', '//button[contains(text(), "作业")]', '//button[contains(text(), "测验")]', '//button[contains(text(), "考试")]', '//button[contains(text(), "答题")]', '//*[contains(@class, "job")]', '//*[contains(@class, "work")]', '//*[contains(@class, "exam")]', '//*[contains(@class, "test")]', '//*[contains(@class, "quiz")]', '//*[contains(@class, "assignment")]' ]; for (const xpath of xpathExpressions) { try { const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); const el = result.singleNodeValue; if (el && el.offsetParent !== null) { const rect = el.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { answerEntry = el; foundSelector = `xpath:${xpath}`; logStore.addLog(`通过XPath找到答题入口: ${foundSelector}`, "success"); break; } } } catch (e) { // 忽略无效XPath } } } // 第五轮:如果还是没找到,尝试通过URL参数查找 if (!answerEntry) { logStore.addLog(`尝试通过URL参数查找答题入口...`, "info"); const currentUrl = window.location.href; const urlParams = new URLSearchParams(currentUrl.split('?')[1]); // 检查是否有课程ID、章节ID等参数 const courseId = urlParams.get('courseId') || urlParams.get('classId'); const chapterId = urlParams.get('chapterId') || urlParams.get('sectionId'); if (courseId || chapterId) { logStore.addLog(`检测到课程参数,尝试构造答题URL...`, "info"); // 尝试构造答题URL并跳转 const answerUrl = currentUrl.includes('work') || currentUrl.includes('exam') || currentUrl.includes('test') ? currentUrl : `${currentUrl.split('?')[0]}?courseId=${courseId}&chapterId=${chapterId}&type=work`; logStore.addLog(`尝试跳转到答题页面: ${answerUrl}`, "info"); window.location.href = answerUrl; return; } } if (answerEntry) { logStore.addLog(`找到答题入口,正在跳转`, "success"); await quickRandomDelay(); // 策略1:如果是 posCatalog_select(章节列表项),先切换章节再重新扫描任务点 if (answerEntry.classList.contains('posCatalog_select') || answerEntry.closest('.posCatalog_select')) { const chapterItem = answerEntry.classList.contains('posCatalog_select') ? answerEntry : answerEntry.closest('.posCatalog_select'); const nameEl = chapterItem.querySelector('.posCatalog_name'); // 步骤1:调用 getTeacherAjax 切换到目标章节(加载视频页面) if (nameEl) { const onclick = nameEl.getAttribute('onclick'); if (onclick && onclick.includes('getTeacherAjax')) { const match = onclick.match(/getTeacherAjax\(['"](.+?)['"],\s*['"](.+?)['"],\s*['"](.+?)['"]\)/); if (match) { const [, courseId, classId, chapterId] = match; logStore.addLog(`调用 getTeacherAjax 切换章节(${courseId}, ${classId}, ${chapterId})`, "info"); try { const targetWindow = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; if (typeof targetWindow.getTeacherAjax === 'function') { targetWindow.getTeacherAjax(courseId, classId, chapterId); } else { nameEl.click(); } } catch(e) { logStore.addLog(`getTeacherAjax调用失败: ${e.message},尝试点击`, "warning"); nameEl.click(); } } else { nameEl.click(); } } else { nameEl.click(); } } else { safeClick(answerEntry); } logStore.addLog(`等待章节切换完成...`, "info"); await new Promise(resolve => setTimeout(resolve, 3000)); if (thisTaskId !== _globalTaskId) { return; } const activeChapter = document.querySelector(`.posCatalog_active[id="cur${chapterItem.id?.replace(/^cur/, '') || ''}"]`) || document.querySelector('.posCatalog_active'); if (activeChapter) { try { activeChapter.scrollIntoView({ behavior: "smooth", block: "center" }); } catch(e) {} } logStore.addLog(`已切换到未完成章节,尝试进入章节测验`, "info"); const clickedQuizTab = await findAndClickChapterQuizTab(); if (clickedQuizTab) { logStore.addLog(`等待章节测验内容加载...`, "info"); const quizLoaded = await waitForQuizPage(12000); if (quizLoaded) { logStore.addLog(`章节测验页面加载成功,开始自动答题`, "success"); await autoAnswerQuiz(); await submitQuiz(); } else { logStore.addLog(`章节测验标签已点击,但未检测到题目内容,重新扫描任务点`, "warning"); setTimeout(() => monitorIframes(), 1000); } } else { logStore.addLog(`未找到章节测验标签,当前可能是视频任务,继续处理当前任务点`, "warning"); setTimeout(() => monitorIframes(), 1000); } return; } // 策略2:如果是标签且有href,直接跳转URL const href = answerEntry.href || answerEntry.getAttribute('href'); if (href && href !== '#' && href !== 'javascript:void(0)' && href !== 'javascript:;') { const fullUrl = href.startsWith('http') ? href : `${window.location.origin}${href}`; logStore.addLog(`直接跳转到答题URL: ${fullUrl}`, "info"); window.location.href = fullUrl; return; } // 策略3:如果有onclick属性,直接执行 const onclick = answerEntry.getAttribute('onclick'); if (onclick) { logStore.addLog(`通过onclick触发跳转: ${onclick.substring(0, 60)}...`, "info"); try { eval(onclick); logStore.addLog(`等待答题页面加载...`, "info"); const quizLoaded = await waitForQuizPage(15000); if (quizLoaded) { logStore.addLog(`答题页面加载成功,开始自动答题`, "success"); await autoAnswerQuiz(); await submitQuiz(); } else { logStore.addLog(`答题页面加载超时,尝试兜底跳转...`, "warning"); await fallbackToQuizUrl(); } return; } catch(e) { logStore.addLog(`onclick执行失败: ${e.message},尝试点击`, "warning"); } } // 策略4:点击元素 safeClick(answerEntry); // 等待答题页面加载 logStore.addLog(`等待答题页面加载...`, "info"); const quizLoaded = await waitForQuizPage(15000); if (quizLoaded) { logStore.addLog(`答题页面加载成功,开始自动答题`, "success"); await autoAnswerQuiz(); await submitQuiz(); } else { logStore.addLog(`答题页面加载超时,尝试兜底跳转...`, "warning"); await fallbackToQuizUrl(); } return; } else { logStore.addLog(`未找到答题入口,尝试直接跳转...`, "warning"); // 最后的兜底策略:尝试直接跳转到常见的答题URL await fallbackToQuizUrl(); } return; } const autoSwitch = configStore.platformParams?.cx?.parts?.[2]?.params?.[1]?.value; logStore.addLog(`本页任务点已全部完成,${autoSwitch ? "正前往下一章节" : "自动切换已关闭"}`, "success"); if (autoSwitch) { // 关键修复:在跳转下一章节前,再次检测是否有未完成的答题入口 logStore.addLog(`跳转前检测:是否有未完成的答题入口...`, "info"); const hasUnansweredBeforeNext = await checkUnansweredAssignments(documentElement); if (hasUnansweredBeforeNext) { logStore.addLog(`检测到未完成的答题入口,优先跳转答题而非下一章节`, "warning"); logStore.addLog(`正在定位答题入口...`, "info"); await quickRandomDelay(); // 使用与之前相同的答题入口选择器(增强版) const answerEntrySelectors = [ '.ans-job-icon', '.jobclass', '[class*="job"]', 'a[href*="work"]', 'a[href*="exam"]', 'a[href*="answer"]', '.work-icon', '.exam-icon', '.assignment-icon', '.ans-job', '.ans-job-title', '.ans-job-content', '.ans-job-btn', '[class*="ans-job"]', '[class*="work"]', '[class*="exam"]', '[class*="test"]', '[class*="quiz"]', '[class*="assignment"]', 'button[class*="job"]', 'button[class*="work"]', 'button[class*="exam"]', 'button[class*="test"]', 'button[class*="quiz"]', 'a[class*="job"]', 'a[class*="work"]', 'a[class*="exam"]', 'a[class*="test"]', 'a[class*="quiz"]', '[class*="作业"]', '[class*="测验"]', '[class*="考试"]', '[class*="答题"]', 'iframe[src*="work"]', 'iframe[src*="exam"]', 'iframe[src*="test"]', 'iframe[src*="quiz"]', 'iframe[src*="answer"]', '.chapterItem', '.chapter_item', '[class*="chapter"]', '.course_section', '.section_item', '[class*="section"]', '.taskitem', '.task_item', '[class*="task"]', '.workList', '.work_list', '[class*="workList"]', '.examList', '.exam_list', '[class*="examList"]', '.testList', '.test_list', '[class*="testList"]', '.btn_work', '.btn_exam', '.btn_test', '.btn_answer', '[class*="btn_work"]', '[class*="btn_exam"]', '[class*="btn_test"]', '[class*="btn_answer"]', 'a[href*="chapter"]', 'a[href*="section"]', 'a[href*="task"]', 'a[href*="course"]', '.chapterDiv', '.chapter_div', '[class*="chapterDiv"]', '.sectionDiv', '.section_div', '[class*="sectionDiv"]', '.icon_work', '.icon_exam', '.icon_test', '.icon_answer', '[class*="icon_work"]', '[class*="icon_exam"]', '[class*="icon_test"]', '[class*="icon_answer"]', // ========== 新增:学习通章节列表特有选择器 ========== // 章节列表项 '.posCatalog_select', '.posCatalog_item', '.posCatalog li', '.lesson_item', '.lesson-item', '.lessonItem', // 章节测验标记(黄色/橙色数字标记) '.orangeIcon', '.icon_yellow', '.icon_orange', '.catalogNum', '.catalog-num', '.chapterNum', '.chapter-num', '.sectionNum', '.section-num', '.lessonNum', '.lesson-num', '.numIcon', '.iconNum', '.num-icon', '.icon-num', '.badge-num', '.num-badge', // 章节测验入口(包含数字标记的章节项) '.posCatalog_select:has(.orangeIcon)', '.posCatalog_select:has(.icon_yellow)', '.posCatalog_select:has([class*="num"])', '.posCatalog_item:has(.orangeIcon)', '.posCatalog_item:has(.icon_yellow)', '.chapterItem:has(.orangeIcon)', '.chapter-item:has(.orangeIcon)', '.sectionItem:has(.orangeIcon)', '.section-item:has(.orangeIcon)', // 学习通移动端适配选择器 '[data-type="quiz"]', '[data-type="test"]', '[data-type="exam"]', '[data-type="homework"]', '[data-type="work"]', '[data-action="quiz"]', '[data-action="test"]', '[data-action="exam"]', '[data-action="homework"]', '[data-action="work"]' ]; let answerEntry = null; let foundSelector = ''; for (const selector of answerEntrySelectors) { try { const el = document.querySelector(selector); if (el && el.offsetParent !== null) { const rect = el.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { answerEntry = el; foundSelector = selector; logStore.addLog(`找到答题入口: ${selector}`, "success"); break; } } } catch (e) { // 忽略无效选择器 } } if (answerEntry) { logStore.addLog(`找到答题入口,正在跳转`, "success"); await quickRandomDelay(); safeClick(answerEntry); // 等待答题页面加载 logStore.addLog(`等待答题页面加载...`, "info"); const quizLoaded = await waitForQuizPage(15000); if (quizLoaded) { logStore.addLog(`答题页面加载成功,开始自动答题`, "success"); await autoAnswerQuiz(); await submitQuiz(); } else { logStore.addLog(`答题页面加载超时,回退到下一章节跳转`, "warning"); } return; } else { logStore.addLog(`未找到答题入口,尝试直接跳转...`, "warning"); const currentUrl = window.location.href; const baseUrl = currentUrl.split('?')[0]; const possibleUrls = [ `${baseUrl}?type=work`, `${baseUrl}?type=exam`, `${baseUrl}?type=test`, `${baseUrl}/work`, `${baseUrl}/exam`, `${baseUrl}/test` ]; for (const url of possibleUrls) { try { logStore.addLog(`尝试跳转到: ${url}`, "info"); window.location.href = url; return; } catch (e) { // 忽略跳转失败 } } logStore.addLog(`所有跳转尝试失败,回退到下一章节跳转`, "warning"); } } else { // 如果没有未完成的答题入口,继续执行下一章节跳转 logStore.addLog(`没有未完成的答题入口,执行下一章节跳转`, "info"); } } // 关闭 if(hasUnansweredAssignments) // 关键修复:在跳转下一章节前,检测是否有未完成的视频 const hasUnfinishedVideo = hasVideoOrAudio(); if (hasUnfinishedVideo) { logStore.addLog(`检测到未完成的视频,停止跳转并继续播放视频`, "warning"); return; } // 快速随机延迟 await quickRandomDelay(); const nextBtn1 = documentElement.querySelector("#prevNextFocusNext"); const nextBtn2 = document.querySelector(".jb_btn.jb_btn_92.fr.fs14.nextChapter"); const nextBtn3 = document.querySelector("#nextBtn"); const nextBtn4 = document.querySelector(".nextChapter"); let targetBtn = null; if (nextBtn1 && nextBtn1.style.display !== "none") { targetBtn = nextBtn2 || nextBtn1; } else if (nextBtn2 && nextBtn2.style.display !== "none") { targetBtn = nextBtn2; } else if (nextBtn3 && nextBtn3.style.display !== "none") { targetBtn = nextBtn3; } else if (nextBtn4 && nextBtn4.style.display !== "none") { targetBtn = nextBtn4; } if (!targetBtn) { logStore.addLog("未找到下一章节按钮,停止自动切换", "warning"); } else { _justClickedNext = true; await quickRandomDelay(); if (thisTaskId !== _globalTaskId) { return; } logStore.addLog("正在前往下一章节...", "primary"); safeClick(targetBtn); } } } catch (e) { logStore.addLog(`扫描任务点异常: ${e.message}`, "danger"); console.error('[扫描任务点] 异常:', e); } } }); }); }; const calculateEnc = (classId, uid, jobId, objectId, playTime, duration) => { const str = `[${classId}][${uid}][${jobId}][${objectId}][${playTime * 1000}][d_yHJ!$pdA~5][${duration * 1000}][0_${duration}]`; return md5(str); }; const formatDuration = (seconds) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; }; // 增强版视频检测函数 - 检查视频是否未完成播放 const hasVideoOrAudio = () => { try { const allIframes = FrameScanner.collectDeepSync(document.documentElement); for (const iframe of allIframes) { const src = iframe.src || ''; if (src.includes('video') || src.includes('audio')) { const parent = iframe.parentElement; const ansJobIcon = parent?.querySelector('.ans-job-icon'); if (ansJobIcon) { // 检查视频任务点是否已完成 const ariaLabel = ansJobIcon.getAttribute('aria-label') || ''; const titleAttr = ansJobIcon.getAttribute('title') || ''; const isCompleted = ariaLabel.includes('已完成') || titleAttr.includes('已完成') || ariaLabel.includes('100%') || titleAttr.includes('100%'); // 如果视频任务点未完成,则返回true表示有未完成的视频 if (!isCompleted) { return true; } } // 尝试直接检查iframe内的视频播放状态 try { const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document; if (iframeDoc) { const video = iframeDoc.querySelector('video'); if (video) { // 检查视频是否未播放完成(播放进度 < 90% 或视频时长 > 0且未结束) const progress = video.duration > 0 ? (video.currentTime / video.duration) * 100 : 0; if (!video.ended && progress < 90) { return true; } } } } catch (e) { // 跨域无法访问,假设视频未完成 return true; } } } return false; } catch (e) { console.log('[检测视频] 检测失败:', e); return false; } }; const completedSimulatedIds = new Set(); const processingSimulatedId = { current: null }; const clearOverlayAndBanner = () => { if (window._blockPlayInterval) { clearInterval(window._blockPlayInterval); window._blockPlayInterval = null; } if (window._blockOverlay) { window._blockOverlay.forEach(o => { try { if (document.contains(o)) o.remove(); } catch(e) {} }); window._blockOverlay = []; } const banner = document.getElementById('_simulate_banner_'); if (banner) banner.remove(); if (window._blockPlayScroll) { window.removeEventListener('scroll', window._blockPlayScroll, true); window.removeEventListener('resize', window._blockPlayScroll); window._blockPlayScroll = null; } }; const showOverlayAndBanner = () => { clearOverlayAndBanner(); if (!window._blockOverlay) window._blockOverlay = []; const getVideoIframes = () => { try { const allIframes = FrameScanner.collectDeepSync(document.documentElement); return allIframes.filter(fr => { const src = fr.src || ''; return src.includes('video') || src.includes('audio'); }); } catch (e) { return []; } }; const createOverlay = () => { try { window._blockOverlay.forEach(o => { try { o.remove(); } catch(e) {} }); window._blockOverlay = []; const videoIframes = getVideoIframes(); for (const iframe of videoIframes) { const rect = iframe.getBoundingClientRect(); if (rect.width > 50 && rect.height > 50) { const overlay = document.createElement('div'); overlay.className = '_simulate_block_overlay_'; overlay.style.cssText = `position:fixed;top:${rect.top}px;left:${rect.left}px;width:${rect.width}px;height:${rect.height}px;z-index:2147483646;cursor:not-allowed;background:rgba(0,0,0,0.25);border-radius:8px;pointer-events:none;`; const ownerDoc = iframe.ownerDocument; if (ownerDoc && ownerDoc.body && ownerDoc.body !== document.body) { ownerDoc.body.appendChild(overlay); } else { document.body.appendChild(overlay); } window._blockOverlay.push(overlay); } } } catch (e) {} }; const updateOverlayPositions = () => {}; createOverlay(); window._blockPlayScroll = updateOverlayPositions; window.addEventListener('scroll', updateOverlayPositions, true); window.addEventListener('resize', updateOverlayPositions); window._blockPlayInterval = setInterval(createOverlay, 1000); if (!document.getElementById('_simulate_banner_')) { const banner = document.createElement('div'); banner.id = '_simulate_banner_'; banner.style.cssText = 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:100002;background:var(--jb-primary);color:#fff;padding:12px 28px;border-radius:30px;font-size:15px;font-weight:600;text-align:center;box-shadow:0 4px 20px rgba(var(--jb-primary-rgb),0.4);cursor:not-allowed;white-space:nowrap;pointer-events:none;'; banner.innerHTML = '🎬 模拟播放模式下无需播放视频,播放进度可在主页信息查看'; document.body.appendChild(banner); } }; const stopSimulatePlayIfNeeded = () => { if (window._simulateActive) { const hasMedia = hasVideoOrAudio(); if (!hasMedia) { window._simulateActive = false; logStore.addLog('检测到视频/音频已消失,停止模拟播放', 'warning'); if (window._currentMediaInterval) { clearInterval(window._currentMediaInterval); window._currentMediaInterval = null; } if (window._simulateLoopId) { BackgroundWorker.stop(window._simulateLoopId); window._simulateLoopId = null; } progressStore.update({ isPlaying: false }); processingSimulatedId.current = null; simulateVideoPlay._currentObjectId = null; clearOverlayAndBanner(); } } }; const forceStopSimulatePlay = () => { if (window._simulateActive) { window._simulateActive = false; if (window._currentMediaInterval) { clearInterval(window._currentMediaInterval); window._currentMediaInterval = null; } if (window._simulateLoopId) { BackgroundWorker.stop(window._simulateLoopId); window._simulateLoopId = null; } progressStore.update({ isPlaying: false }); processingSimulatedId.current = null; simulateVideoPlay._currentObjectId = null; clearOverlayAndBanner(); } }; const getVideoInfo = async (objectId) => { return new Promise((resolve, reject) => { const host = window.location.host; const protocol = window.location.protocol; const FID = _unsafeWindow.FID || ''; const statusUrl = `${protocol}//${host}/ananas/status/${objectId}?k=${FID}&flag=normal&_dc=${Date.now()}`; let vrefer = ''; try { const videoIframe = _unsafeWindow.document.querySelector('.ans-attach-online.ans-insertvideo-online'); if (videoIframe) { vrefer = videoIframe.src; } } catch (e) {} if (!vrefer) { vrefer = `${protocol}//${host}/ananas/modules/video/index.html?v=2022-1118-1729`; } _GM_xmlhttpRequest({ method: "get", url: statusUrl, headers: { 'Host': host, 'Referer': vrefer, 'Sec-Fetch-Site': 'same-origin' }, onload: function(res) { try { if (res.status === 200) { const data = JSON.parse(res.responseText); resolve(data); } else { reject(new Error(`HTTP ${res.status}`)); } } catch (e) { reject(e); } }, onerror: function(err) { reject(err); } }); }); }; const getTaskParams = () => { try { const topWindow = _unsafeWindow.top; if (topWindow.margs) { return topWindow.margs; } const urlParams = new URLSearchParams(window.location.search); return { clazzId: urlParams.get('clazzId') || urlParams.get('classId'), courseId: urlParams.get('courseId'), knowledgeid: urlParams.get('knowledgeid') || urlParams.get('chapterId') }; } catch (e) { console.error('[模拟播放] 获取任务参数失败:', e); return null; } }; const getEvasionRate = (baseRate) => { const evasionRate = baseRate * (0.95 + Math.random() * 0.1); return Math.max(0.5, Math.min(baseRate * 1.05, evasionRate)); }; let smartSpeedState = { currentSpeed: 0, lastChangeTime: 0, changeInterval: 30000 }; const getSmartSpeed = (baseRate) => { const now = Date.now(); if (now - smartSpeedState.lastChangeTime > smartSpeedState.changeInterval) { const variation = (Math.random() - 0.5) * baseRate * 0.1; smartSpeedState.currentSpeed = Math.max(0.8, Math.min(baseRate * 1.05, baseRate + variation)); smartSpeedState.lastChangeTime = now; } return smartSpeedState.currentSpeed; }; let behaviorState = { lastMouseMove: 0 }; const simulateUserBehavior = () => { const now = Date.now(); if (now - behaviorState.lastMouseMove > 10000 + Math.random() * 20000) { const x = Math.random() * window.innerWidth; const y = Math.random() * window.innerHeight; document.dispatchEvent(new MouseEvent('mousemove', { clientX: x, clientY: y, bubbles: true })); behaviorState.lastMouseMove = now; } }; const compensateDuration = (playTime, duration, baseRate) => playTime; const simulateVideoPlay = async (iframe, iframeDocument, mediaType) => { return new Promise(async (resolve) => { const releaseLock = () => { if (processingSimulatedId.current === (simulateVideoPlay._currentObjectId)) { processingSimulatedId.current = null; simulateVideoPlay._currentObjectId = null; } }; const safeResolve = (val) => { releaseLock(); resolve(val); }; logStore.addLog(`发现一个${mediaType},当前播放模式: 模拟播放`, "primary"); try { const mediaElement = iframeDocument?.documentElement?.querySelector(mediaType); if (mediaElement) { mediaElement.pause(); mediaElement.muted = true; mediaElement.autoplay = false; } } catch (e) { console.log('[模拟播放] 暂停视频失败:', e); } try { let objectId = null; let jobId = null; let otherInfo = ''; let reportUrl = ''; let classId = null; let uid = null; let duration = null; let dtoken = null; let iframeSrc = iframe.src || ''; let videoName = mediaType === 'video' ? '视频' : '音频'; try { let prevTitleElement = null; let parent = iframe.parentElement; while (parent && !prevTitleElement) { prevTitleElement = parent.querySelector('.prev_title'); parent = parent.parentElement; if (parent === document.body || parent === document.documentElement) break; } if (!prevTitleElement) { prevTitleElement = document.querySelector('.prev_title'); } if (prevTitleElement) { const titleText = prevTitleElement.innerText || prevTitleElement.textContent || ''; videoName = titleText.replace(/【上】|【下】|【上$|【下$/g, '').trim(); if (videoName && videoName !== (mediaType === 'video' ? '视频' : '音频')) { logStore.addLog(`获取到视频名称: ${videoName}`, "primary"); } else { logStore.addLog(`从.prev_title获取到的文本: "${titleText}"`, "warning"); } } else { logStore.addLog(`未找到.prev_title元素`, "warning"); } } catch (e) { logStore.addLog(`从标题获取失败: ${e.message}`, "danger"); } const getStr = (str, start, end) => { const startIndex = str.indexOf(start); if (startIndex === -1) return null; const content = str.substring(startIndex + start.length); const endIndex = content.indexOf(end); if (endIndex === -1) return null; return content.substring(0, endIndex); }; let pageData = null; try { if (_unsafeWindow.param) { try { const parsed = JSON.parse(_unsafeWindow.param); if (parsed?.attachments) pageData = parsed; } catch (e) {} } if (!pageData && _unsafeWindow.mArg?.attachments) { pageData = _unsafeWindow.mArg; } } catch (e) {} if (!pageData) { try { const allIframes = FrameScanner.collectDeepSync(document.documentElement); for (const ifr of allIframes) { try { if (!ifr.contentWindow) continue; const win = ifr.contentWindow; if (win.param) { try { const parsed = JSON.parse(win.param); if (parsed?.attachments) { pageData = parsed; break; } } catch (e) {} } if (!pageData && win.mArg?.attachments) { pageData = win.mArg; break; } } catch (e) {} } } catch (e) {} } const documentsToTry = []; if (iframeDocument) { documentsToTry.push({ name: 'iframeDocument', doc: iframeDocument }); } documentsToTry.push({ name: 'currentWindow', doc: _unsafeWindow.document }); try { if (_unsafeWindow.top && _unsafeWindow.top.document) { documentsToTry.push({ name: 'topWindow', doc: _unsafeWindow.top.document }); } } catch (e) {} for (const { name, doc } of documentsToTry) { if (pageData) break; try { const scripts = doc.getElementsByTagName('script'); for (let i = 0; i < scripts.length; i++) { const scriptContent = scripts[i].innerHTML; if (scriptContent.indexOf('mArg = "";') !== -1 && scriptContent.indexOf('==UserScript==') === -1) { const param = getStr(scriptContent, 'try{\n mArg = ', ';\n}catch(e){'); if (param) { pageData = JSON.parse(param); break; } } if (!pageData && scriptContent.indexOf('mArg=') !== -1 && scriptContent.indexOf('==UserScript==') === -1) { const match = scriptContent.match(/mArg\s*=\s*(\{[\s\S]*?\});/); if (match) { try { pageData = JSON.parse(match[1]); break; } catch (e) {} } } } if (!pageData && name === 'topWindow') { for (let i = 0; i < scripts.length; i++) { const content = scripts[i].innerHTML; if (content.indexOf('clazzId') !== -1 && content.length > 10000) { const attachmentsMatch = content.match(/["']attachments["']\s*:\s*(\[[\s\S]*?\])/); if (attachmentsMatch) { try { const jsonStr = `{"attachments": ${attachmentsMatch[1]}}`; const parsed = JSON.parse(jsonStr); if (parsed.attachments && parsed.attachments.length > 0) { pageData = { attachments: parsed.attachments }; break; } } catch (e) { logStore.addLog(`[${name}] 解析attachments失败: ${e.message}`, "warning"); } } const clazzIdMatch = content.match(/stu_clazzId\s*=\s*["'](\d+)["']/); const courseIdMatch = content.match(/stu_CourseId\s*=\s*["'](\d+)["']/); const knowledgeIdMatch = content.match(/stu_knowledgeId\s*=\s*["'](\d+)["']/); if (clazzIdMatch || courseIdMatch) { if (!pageData) pageData = {}; pageData.defaults = { clazzId: clazzIdMatch ? clazzIdMatch[1] : null, courseId: courseIdMatch ? courseIdMatch[1] : null, knowledgeId: knowledgeIdMatch ? knowledgeIdMatch[1] : null }; } } if (!pageData && content.indexOf('"attachments"') !== -1) { const jsonMatch = content.match(/\{[\s\S]*?"attachments"[\s\S]*?\}/); if (jsonMatch) { try { pageData = JSON.parse(jsonMatch[0]); break; } catch (e) {} } } } } } catch (e) { logStore.addLog(`[${name}] 获取失败: ${e.message}`, "warning"); } } if (!pageData || (pageData.defaults && !pageData.defaults.reportUrl)) { try { const windowsToTry = [_unsafeWindow]; try { if (_unsafeWindow.top) windowsToTry.push(_unsafeWindow.top); } catch (e) {} try { if (_unsafeWindow.parent) windowsToTry.push(_unsafeWindow.parent); } catch (e) {} for (const ifr of FrameScanner.collectDeepSync(document.documentElement)) { try { if (ifr.contentWindow) windowsToTry.push(ifr.contentWindow); } catch (e) {} } for (const win of windowsToTry) { const props = ['margs', 'mArg', 'pageData', 'courseData']; for (const prop of props) { try { const val = win[prop]; if (val && typeof val === 'object') { if (val.attachments) { pageData = val; logStore.addLog(`从window.${prop}获取完整数据成功(含attachments)`, "success"); break; } if (!pageData && val.defaults && val.defaults.reportUrl) { pageData = val; logStore.addLog(`从window.${prop}获取数据成功(含reportUrl)`, "success"); break; } } } catch (e) {} } if (pageData && pageData.defaults && pageData.defaults.reportUrl) break; } } catch (e) { logStore.addLog(`获取window数据失败: ${e.message}`, "warning"); } } if (!pageData) { logStore.addLog(`无法获取mArg数据,回退到普通播放模式`, "warning"); } if (pageData) { if (pageData.defaults) { classId = pageData.defaults.clazzId; uid = pageData.defaults.userid || getUid(); reportUrl = pageData.defaults.reportUrl || ''; } if (pageData.attachments && pageData.attachments.length > 0) { const iframeSrc = iframe.src; let srcObjectId = null; const srcMatch = iframeSrc.match(REGEX.OBJECT_ID); if (srcMatch) { srcObjectId = srcMatch[1]; } if (!srcObjectId) { srcObjectId = iframe.getAttribute('objectid') || iframe.getAttribute('data-objectid') || null; } for (const attachment of pageData.attachments) { if (attachment.isPassed === true) continue; if (completedSimulatedIds.has(attachment.property?.objectid)) continue; const moduleType = attachment.property?.module || ''; const isVideo = moduleType === 'video' || moduleType === 'insertvideo' || attachment.type === 'video'; const isAudio = moduleType === 'audio' || moduleType === 'insertaudio' || attachment.type === 'audio'; if ((mediaType === 'video' && isVideo) || (mediaType === 'audio' && isAudio)) { if (srcObjectId && attachment.property?.objectid === srcObjectId) { objectId = attachment.property.objectid; jobId = attachment.jobid; otherInfo = attachment.otherInfo || ''; videoName = attachment.property?.name || videoName; break; } if (!objectId && attachment.job === true) { objectId = attachment.property?.objectid; jobId = attachment.jobid; otherInfo = attachment.otherInfo || ''; videoName = attachment.property?.name || videoName; } } } if (!objectId) { for (const attachment of pageData.attachments) { if (attachment.isPassed === true) continue; if (completedSimulatedIds.has(attachment.property?.objectid)) continue; const moduleType = attachment.property?.module || ''; const isVideo = moduleType === 'video' || moduleType === 'insertvideo' || attachment.type === 'video'; const isAudio = moduleType === 'audio' || moduleType === 'insertaudio' || attachment.type === 'audio'; if ((mediaType === 'video' && isVideo) || (mediaType === 'audio' && isAudio)) { objectId = attachment.property?.objectid; jobId = attachment.jobid; otherInfo = attachment.otherInfo || ''; videoName = attachment.property?.name || videoName; break; } } } } } if (!objectId) { const iframeSrc = iframe.src || ''; const objectIdMatch = iframeSrc.match(REGEX.OBJECT_ID); objectId = objectIdMatch ? objectIdMatch[1] : null; if (!objectId) { const dataAttrs = ['data-objectid', 'data-object-id', 'objectid']; for (const attr of dataAttrs) { const val = iframe.getAttribute(attr); if (val) { objectId = val; break; } } } if (!objectId) { const nameOrId = iframe.name || iframe.id || ''; const nameMatch = nameOrId.match(/([a-f0-9]{24,})/i); if (nameMatch) { objectId = nameMatch[1]; } } } if (!objectId) { logStore.addLog(`无法获取objectId,回退到普通播放模式`, "danger"); return safeResolve(await playMediaDirectly(mediaType, iframeDocument)); } if (processingSimulatedId.current) { logStore.addLog(`该视频正在模拟播放中,跳过重复处理`, "warning"); return safeResolve(); } processingSimulatedId.current = objectId; simulateVideoPlay._currentObjectId = objectId; if (!dtoken || !duration) { const videoInfo = await getVideoInfo(objectId); duration = videoInfo.duration; dtoken = videoInfo.dtoken; } if (!duration || !dtoken) { logStore.addLog(`获取视频信息失败,回退到普通播放模式`, "danger"); return safeResolve(await playMediaDirectly(mediaType, iframeDocument)); } logStore.addLog(`视频时长: ${formatDuration(duration)}秒`, "primary"); if (!uid) { uid = cachedUid; } if (!classId) { const taskParams = getTaskParams(); classId = taskParams?.clazzId || taskParams?.classId; } if (!classId) { const pageUrl = window.location.href; const classIdMatch = pageUrl.match(/clazzId=(\d+)/) || pageUrl.match(/classId=(\d+)/); classId = classIdMatch ? classIdMatch[1] : null; } if (!jobId) { const jobIdAttrs = ['data-jobid', 'data-job-id', 'jobid', 'data-workid']; for (const attr of jobIdAttrs) { const val = iframe.getAttribute(attr); if (val) { jobId = val; break; } } if (!jobId) { const urlMatch = window.location.href.match(/[?&]jobid=([^&]+)/i); jobId = urlMatch ? urlMatch[1] : null; } if (!jobId && iframe.src) { const srcMatch = iframe.src.match(REGEX.JOB_ID); jobId = srcMatch ? srcMatch[1] : null; } if (!jobId) { jobId = objectId; } } if (!classId || !uid || !jobId) { logStore.addLog(`缺少必要参数: classId=${classId}, uid=${uid}, jobId=${jobId}`, "danger"); logStore.addLog(`回退到普通播放模式`, "warning"); return safeResolve(await playMediaDirectly(mediaType, iframeDocument)); } const autoMaxRate = configStore.platformParams.cx.parts[0].params[5].value || false; let playbackRate = configStore.platformParams.cx.parts[0].params[6].value || 1; const maxRate = getMaxPlaybackRate(iframeDocument, false); const speedDisabled = maxRate === 1; window.__maxPlaybackRate = maxRate; const playbackRateParam = configStore.platformParams.cx.parts[0].params[6]; playbackRateParam.max = autoMaxRate ? maxRate : 3; if (speedDisabled) { logStore.addLog(`⚠️ 此视频已被学习通禁用倍速,使用>1x倍速可能会导致学习进度被清空`, "warning"); } if (autoMaxRate) { playbackRate = maxRate; logStore.addLog(`自动倍速: ${playbackRate}x`, "success"); } else { const simulateMaxRate = 3; if (playbackRate > simulateMaxRate) { playbackRate = simulateMaxRate; logStore.addLog(`模拟播放倍速已调整为最大值: ${playbackRate}x`, "warning"); } } logStore.addLog(`播放倍速: ${playbackRate}x`, "primary"); const directComplete = configStore.platformParams.cx.parts[0].params[3].value || false; const host = window.location.host; const protocol = window.location.protocol; const videojs_id = String(parseInt(Math.random() * 9999999)); document.cookie = 'videojs_id=' + videojs_id + ';path=/'; const evasionEnabled = configStore.platformParams.cx.parts[0].params[1].value || false; const reportProgress = async (currentTime, isComplete) => { return new Promise((resolveReport) => { const enc = calculateEnc(classId, uid, jobId, objectId, currentTime, duration); if (enc.length !== 32) { logStore.addLog(`加密字符串计算失败`, "danger"); return resolveReport(false); } const currentIsdrag = isComplete ? '4' : (currentTime > 0 ? '0' : '3'); const baseUrl = reportUrl.startsWith('http') ? reportUrl : `${protocol}//${host}${reportUrl}`; const reportsUrl = `${baseUrl}/${dtoken}?clazzId=${classId}&playingTime=${currentTime}&duration=${duration}&clipTime=0_${duration}&objectId=${objectId}&otherInfo=${otherInfo}&jobid=${jobId}&userid=${uid}&isdrag=${currentIsdrag}&view=pc&enc=${enc}&rt=0.9&dtype=${mediaType === 'video' ? 'Video' : 'Audio'}&_t=${Date.now()}`; _GM_xmlhttpRequest({ method: "get", url: reportsUrl, headers: { 'Host': host, 'Referer': iframeSrc, 'Sec-Fetch-Site': 'same-origin', 'Content-Type': 'application/json' }, onload: function(res) { try { const result = JSON.parse(res.responseText); if (result.isPassed) { logStore.addLog(`视频任务点已完成`, "success"); resolveReport(true); } else { resolveReport(false); } } catch (e) { logStore.addLog(`上报响应解析失败(status=${res.status}): ${res.responseText.substring(0, 200)}`, "danger"); resolveReport(false); } }, onerror: function(err) { logStore.addLog(`上报请求失败`, "danger"); resolveReport(false); } }); }); }; if (!reportUrl) { logStore.addLog(`⚠️ reportUrl为空,将使用默认路径/multimedia/v2`, "warning"); reportUrl = '/multimedia/v2'; } if (directComplete) { logStore.addLog(`⚠️ 直接上报中...`, "warning"); progressStore.update({ taskName: `[${mediaType === 'video' ? '视频' : '音频'}] ${videoName}`, percent: 100, currentTime: duration, totalTime: duration, type: mediaType === 'video' ? '视频' : '音频', detail: '直接上报', isPlaying: true, speedDisabled: speedDisabled }); const isComplete = await reportProgress(duration, true); if (isComplete) { completedSimulatedIds.add(objectId); logStore.addLog(`🎬 ${mediaType}直接上报`, "success"); progressStore.update({ percent: 100, currentTime: duration, detail: '已完成', isPlaying: false }); return safeResolve(); } else { logStore.addLog(`直接上报失败,回退到模拟播放`, "danger"); } } if (window._currentMediaInterval) { clearInterval(window._currentMediaInterval); window._currentMediaInterval = null; } progressStore.update({ taskName: `[${mediaType === 'video' ? '视频' : '音频'}] ${videoName}`, percent: 0, currentTime: 0, totalTime: duration, type: mediaType === 'video' ? '视频' : '音频', detail: `${formatDuration(0)}/${formatDuration(duration)}`, isPlaying: true, speedDisabled: speedDisabled }); let playTime = 0; let playsTime = 0; let isFirst = true; let nextReportTime = 0; let isdrag = '3'; let completeRetryCount = 0; const maxCompleteRetries = 10; const reportInterval = 50; let isReporting = false; const afkEnabled = configStore.platformParams.cx.parts[4]?.params[0]?.value || false; const simulateLoopId = 'simulate_' + Date.now(); let loopInterval = null; const loopCallback = async () => { if (isReporting) return; if (!window._simulateActive) { logStore.addLog('模拟播放已被停止,结束处理', 'warning'); if (afkEnabled) { BackgroundWorker.stop(simulateLoopId); } else { clearInterval(loopInterval); } window._currentMediaInterval = null; progressStore.update({ isPlaying: false }); releaseLock(); safeResolve(); return; } const hasMedia = hasVideoOrAudio(); if (!hasMedia) { window._simulateActive = false; logStore.addLog('检测到视频/音频已消失,停止模拟播放(未完成)', 'warning'); if (afkEnabled) { BackgroundWorker.stop(simulateLoopId); } else { clearInterval(loopInterval); } window._currentMediaInterval = null; progressStore.update({ isPlaying: false }); clearOverlayAndBanner(); releaseLock(); safeResolve(); return; } const autoMaxRateNow = configStore.platformParams.cx.parts[0].params[5].value || false; let playbackRateNow = autoMaxRateNow ? maxRate : Math.min(playbackRateParam.value || playbackRate, 3); let effectiveRate = playbackRateNow; if (evasionEnabled) { effectiveRate = getEvasionRate(effectiveRate); effectiveRate = getSmartSpeed(effectiveRate); simulateUserBehavior(); } playsTime += effectiveRate; playTime = Math.ceil(playsTime); playTime = compensateDuration(playTime, duration, playbackRateNow); if (playTime > duration) { playTime = duration; } const progress = Math.floor((playTime / duration) * 100); progressStore.update({ percent: progress, currentTime: playTime, totalTime: duration, detail: `${formatDuration(playTime)}/${formatDuration(duration)}`, isPlaying: true, speedDisabled: speedDisabled }); let shouldReport = false; if (playTime >= duration) { shouldReport = true; } else if (isFirst) { shouldReport = true; } else if (nextReportTime > 0 && playTime >= nextReportTime) { shouldReport = true; } if (shouldReport) { isReporting = true; if (isFirst) { playTime = 0; isFirst = false; } if (playTime >= duration) { playTime = duration; isdrag = '4'; } else if (playTime > 0) { isdrag = '0'; } nextReportTime = Math.min(playTime + reportInterval, duration); logStore.addLog(`📤 上报进度: ${Math.round(playTime / duration * 100)}%(${Math.round(playTime)}/${Math.round(duration)}s)`); const isComplete = await reportProgress(playTime, isdrag === '4'); isReporting = false; if (isComplete) { if (afkEnabled) { BackgroundWorker.stop(simulateLoopId); } else { clearInterval(loopInterval); } window._simulateActive = false; completedSimulatedIds.add(objectId); logStore.addLog(`🎬 ${mediaType}模拟播放完成`, "success"); progressStore.update({ percent: 100, currentTime: duration, detail: '播放完成', isPlaying: false }); window._currentMediaInterval = null; safeResolve(); } else if (isdrag === '4') { completeRetryCount++; if (completeRetryCount >= maxCompleteRetries) { if (afkEnabled) { BackgroundWorker.stop(simulateLoopId); } else { clearInterval(loopInterval); } window._simulateActive = false; logStore.addLog(`完成上报重试${maxCompleteRetries}次仍未通过,请检查视频是否需要其他操作`, "danger"); progressStore.update({ percent: 100, currentTime: duration, detail: '上报未通过', isPlaying: false }); window._currentMediaInterval = null; safeResolve(); } else { logStore.addLog(`完成上报未通过,重试中(${completeRetryCount}/${maxCompleteRetries})...`, "warning"); } } } }; if (afkEnabled) { window._simulateLoopId = simulateLoopId; window._simulateActive = true; showOverlayAndBanner(); BackgroundWorker.start(simulateLoopId, loopCallback, 1000); logStore.addLog(`🖥️ 挂机模式已启用,使用后台Worker计时`, "success"); } else { loopInterval = setInterval(loopCallback, 1000); window._currentMediaInterval = loopInterval; window._simulateActive = true; showOverlayAndBanner(); } } catch (e) { window._currentMediaInterval = null; window._simulateActive = false; logStore.addLog(`模拟播放出错: ${e.message}`, "danger"); logStore.addLog(`回退到普通播放模式`, "warning"); safeResolve(await playMediaDirectly(mediaType, iframeDocument)); } }); }; const playMediaDirectly = async (mediaType, iframeDocument) => { return new Promise((resolve) => { logStore.addLog(`正在尝试播放${mediaType},请稍等5s`, "primary"); const autoMaxRate = configStore.platformParams.cx.parts[0].params[5].value || false; const playbackRateParam = configStore.platformParams.cx.parts[0].params[6]; let playbackRate = playbackRateParam.value || 1; const maxRate = getMaxPlaybackRate(iframeDocument); const videoQuizEnabled = configStore.platformParams.cx.parts[0].params[4]?.value || false; if (videoQuizEnabled) { const loop = async () => { try { const submitBtn = iframeDocument?.querySelector("#videoquiz-submit"); if (submitBtn) { const list = Array.from(iframeDocument.querySelectorAll(".ans-videoquiz-opt label")); if (list.length > 0) { const answer = list[Math.floor(Math.random() * list.length)]; if (answer) safeClick(answer); if (submitBtn) safeClick(submitBtn); await quickRandomDelay(); const container = iframeDocument.querySelector("#video .ans-videoquiz"); if (container) { container.remove(); } const components = Array.from(iframeDocument.querySelectorAll(".x-component-default")); if (components.length) { for (const com of components) { com.style.display = "none"; } } logStore.addLog("已处理视频内题目", "success"); } } } catch (e) { console.log("处理视频内题目失败:", e); } await quickRandomDelay(); loop(); }; loop(); } const simulatePlayEnabled = configStore.platformParams.cx.parts[0].params[0].value || false; if (!simulatePlayEnabled) { playbackRateParam.max = maxRate; if (playbackRate > maxRate) { playbackRateParam.value = maxRate; playbackRate = maxRate; } } if (autoMaxRate) { playbackRate = maxRate; logStore.addLog(`自动倍速: ${playbackRate}x`, "success"); } else { if (playbackRate > maxRate) { playbackRate = maxRate; logStore.addLog(`倍速已调整为播放器最大值: ${playbackRate}x`, "warning"); } } logStore.addLog(`播放倍速: ${playbackRate}x`, "primary"); let isExecuted = false; logStore.addLog("播放成功", "success"); const intervalId = setInterval(async () => { const mediaElement = iframeDocument.documentElement.querySelector(mediaType); if (mediaElement && !isExecuted) { await mediaElement.pause(); mediaElement.muted = true; await mediaElement.play(); mediaElement.playbackRate = playbackRate; const listener = async () => { await delay(3); await mediaElement.play(); mediaElement.playbackRate = playbackRate; }; mediaElement.addEventListener("pause", listener); mediaElement.addEventListener("ended", () => { logStore.addLog(`${mediaType}已播放完成`, "success"); mediaElement.removeEventListener("pause", listener); resolve(); }); isExecuted = true; clearInterval(intervalId); } }, 2500); }); }; const getMaxPlaybackRate = (iframeDocument, showLog = true) => { try { const menuItems = iframeDocument.querySelectorAll('.vjs-playback-rate .vjs-menu-content .vjs-menu-item'); if (menuItems.length === 0) { return 1; } let maxRate = 1; menuItems.forEach(item => { const text = item.textContent.trim(); const rate = parseFloat(text.replace('x', '')); if (!isNaN(rate) && rate > maxRate) { maxRate = rate; } }); if (showLog) { logStore.addLog(`播放器最大倍速: ${maxRate}x`, "info"); } return maxRate; } catch (e) { logStore.addLog(`获取最大倍速失败,使用默认值1x`, "warning"); return 1; } }; const handleMediaContent = async (mediaType, iframeDocument, iframe) => { const useSimulatePlay = configStore.platformParams.cx.parts[0]?.params[0]?.value || false; if (useSimulatePlay) { return simulateVideoPlay(iframe, iframeDocument, mediaType); } else { return playMediaDirectly(mediaType, iframeDocument); } }; const handleAssignment = async (iframe, iframeDocument, iframeWindow) => { logStore.addLog("发现一个作业,正在解析", "warning"); const taskId = _globalTaskId; const startTime = Date.now(); try{ if (!iframeDocument) { logStore.addLog("iframeDocument为空,无法处理", "danger"); return; } const ansJobIcon = iframe.parentElement?.querySelector(".ans-job-icon"); if (ansJobIcon) { const ariaLabel = ansJobIcon.getAttribute("aria-label") || ""; if (ariaLabel.includes("已完成")) { logStore.addLog("任务点已完成,跳过", "success"); return; } } decodeCipherFont(iframeDocument); logStore.addLog("开始解析题目...", "info"); const handler = new CxQuestionHandler("zj", iframe); const correctRate = await Promise.race([ handler.init(), new Promise((_, reject) => setTimeout(() => reject(new Error("解析超时")), 30000)) ]); if (taskId !== _globalTaskId) { logStore.addLog("任务已取消,停止处理", "warning"); return; } const parseTime = ((Date.now() - startTime) / 1000).toFixed(1); logStore.addLog(`题目解析完成,耗时${parseTime}s,共${handler.questions.length}道题`, "success"); iframeWindow.alert = () => { }; const autoSubmitPart = configStore.platformParams?.cx?.parts?.find(p => p.name === "章节/作业/测验设置"); const autoSubmit = autoSubmitPart?.params?.find(p => p.name === "自动提交")?.value || false; // 调试日志:显示配置读取情况 logStore.addLog(`配置调试: parts数量=${configStore.platformParams?.cx?.parts?.length || 0}`, "info"); if (configStore.platformParams?.cx?.parts) { configStore.platformParams.cx.parts.forEach((part, index) => { logStore.addLog(`Part[${index}]: ${part.name}`, "info"); if (part.params) { part.params.forEach(param => { logStore.addLog(` - ${param.name}: ${param.value}`, "info"); }); } }); } logStore.addLog(`自动提交配置: autoSubmitPart=${autoSubmitPart ? '找到' : '未找到'}, autoSubmit=${autoSubmit}`, "info"); // 详细日志:显示答题统计信息 const totalQuestions = handler.questions.length; const answeredQuestions = handler.questions.filter(q => q.answer && q.answer.length > 0).length; const unansweredQuestions = totalQuestions - answeredQuestions; const localModeQuestions = handler.questions.filter(q => q.source && q.source.startsWith("local")).length; const remoteModeQuestions = totalQuestions - localModeQuestions; logStore.addLog(`答题统计: 总计${totalQuestions}道 | 已答${answeredQuestions}道 | 未答${unansweredQuestions}道`, "info"); logStore.addLog(`答题模式: 本地匹配${localModeQuestions}道 | 远程搜索${remoteModeQuestions}道`, "info"); logStore.addLog(`正确率: ${isNaN(correctRate) ? '0' : correctRate.toFixed(1)}%`, "info"); if (autoSubmit) { logStore.addLog("自动提交已开启,尝试提交", "primary"); const answerParamsPart = configStore.platformParams?.cx?.parts?.find(p => p.name === "答题参数"); const correctRateThreshold = answerParamsPart?.params.find(p => p.name === "正确阈值")?.value || 85; const rate = isNaN(correctRate) ? 0 : correctRate; // 检查是否有离线模式的题目 const hasLocalModeQuestions = handler.questions.some(q => q.source && q.source.startsWith("local")); const allAttempted = handler.questions.every(q => q.answer && q.answer.length > 0); const unansweredCount = handler.questions.filter(q => !q.answer || q.answer.length === 0).length; logStore.addLog(`提交条件: 本地模式=${hasLocalModeQuestions}, 全部已答=${allAttempted}, 未答数=${unansweredCount}`, "info"); logStore.addLog(`正确率: ${rate.toFixed(1)}% >= 阈值: ${correctRateThreshold}%`, "info"); // 关键修复:必须答题!即使正确率低于阈值也要继续尝试答题 // 只有当所有题目都已作答且正确率达到阈值时才提交 const allQuestionsAnswered = handler.questions.every(q => q.answer && q.answer.length > 0); const meetsThreshold = rate >= Number(correctRateThreshold); // 如果还有未答题,继续答题 if (!allQuestionsAnswered) { logStore.addLog(`不满足提交条件: 还有${unansweredCount}道题目未作答,继续答题`, "warning"); return; // 返回继续答题 } // 如果正确率低于阈值,继续尝试提高正确率(重新答题) if (!meetsThreshold) { logStore.addLog(`正确率${rate.toFixed(1)}%低于阈值${correctRateThreshold}%,继续尝试提高正确率`, "warning"); return; // 返回继续答题 } const shouldSubmit = true; if (shouldSubmit) { logStore.addLog(`满足提交条件,准备提交答案...`, "success"); let submitSuccess = false; let submitAttempts = 0; const maxAttempts = 3; while (!submitSuccess && submitAttempts < maxAttempts) { submitAttempts++; logStore.addLog(`提交尝试 ${submitAttempts}/${maxAttempts}`, "info"); // 策略1:尝试学习通标准提交函数 try { if (typeof iframeWindow.btnBlueSubmit === 'function') { await iframeWindow.btnBlueSubmit(); logStore.addLog("使用 btnBlueSubmit() 提交", "success"); submitSuccess = true; } else if (typeof iframeWindow.submitCheckTimes === 'function') { await iframeWindow.submitCheckTimes(); logStore.addLog("使用 submitCheckTimes() 提交", "success"); submitSuccess = true; } else if (typeof iframeWindow.submitWork === 'function') { await iframeWindow.submitWork(); logStore.addLog("使用 submitWork() 提交", "success"); submitSuccess = true; } else if (typeof iframeWindow.submit === 'function') { await iframeWindow.submit(); logStore.addLog("使用 submit() 提交", "success"); submitSuccess = true; } } catch (e) { logStore.addLog(`函数提交失败: ${e.message}`, "warning"); } // 策略2:查找提交按钮(扩展选择器列表) if (!submitSuccess) { const submitSelectors = [ 'input[type="submit"][value*="提交"]', 'button[type="submit"]', 'input[value="提交答案"]', 'input[value="提交"]', 'input[value="交卷"]', 'button[onclick*="submit"]', 'button[onclick*="btnBlueSubmit"]', 'button[onclick*="submitWork"]', 'button[onclick*="submitCheckTimes"]', '.submit-btn', '.btn-submit', '#submit', '.submit', '.btn-blue', 'input.btn-blue', 'a[onclick*="submit"]', '.btn-blue-submit', 'button.fs14', // 新增:学习通常见提交按钮选择器 'input[value*="提交"]', 'button:contains("提交")', '.btn-submit-work', '.submit-work', 'input.submit-work', 'button.submit-work', '.btn-complete', 'button.complete', 'input.complete', 'button[type="button"][onclick*="submit"]', 'input[type="button"][onclick*="submit"]', 'div[onclick*="submit"]', 'span[onclick*="submit"]', // 新增:底部固定提交按钮 '.fixed-bottom button', '.footer button', '.bottom-bar button' ]; logStore.addLog(`提交按钮选择器: ${submitSelectors.length}个`, "info"); for (const selector of submitSelectors) { try { const submitBtn = iframeWindow.document.querySelector(selector); if (submitBtn && !submitBtn.disabled && !submitBtn.classList.contains('disabled')) { const btnText = submitBtn.textContent?.trim() || submitBtn.value?.trim() || selector; logStore.addLog(`找到提交按钮: "${btnText}" (${selector})`, "info"); try { submitBtn.scrollIntoView({ block: 'center', behavior: 'smooth' }); await delay(0.5); } catch(e) {} const onclick = submitBtn.getAttribute('onclick'); if (onclick) { try { safeClick(submitBtn); logStore.addLog(`通过onclick执行提交: ${selector}`, 'success'); submitSuccess = true; break; } catch (e) { logStore.addLog(`onclick执行失败: ${e.message}`, 'warning'); } } if (!submitSuccess) { safeClick(submitBtn); logStore.addLog(`点击提交按钮: ${selector}`, 'success'); submitSuccess = true; break; } } } catch (e) { // 忽略单个选择器失败 } } if (!submitSuccess) { logStore.addLog("未找到提交按钮", "warning"); } } // 策略3:通过文本匹配查找提交按钮 if (!submitSuccess) { const allButtons = iframeWindow.document.querySelectorAll('button, input[type="button"], a.btn, .btn, [role="button"], a, div[onclick], span[onclick]'); logStore.addLog(`文本匹配扫描 ${allButtons.length} 个元素`, "info"); for (const btn of allButtons) { const text = btn.textContent?.trim() || btn.value?.trim() || btn.getAttribute('aria-label')?.trim() || ''; if (/^(提交|提交答案|完成|确认提交|交卷|完成答题|确认交卷|提交作业|交作业|完成作业)$/.test(text)) { try { const onclick = btn.getAttribute('onclick'); if (onclick) { try { safeClick(btn); logStore.addLog(`通过文本匹配执行提交: "${text}"`, 'success'); submitSuccess = true; break; } catch (e) { logStore.addLog(`文本匹配onclick失败: ${e.message}`, 'warning'); } } safeClick(btn); logStore.addLog(`通过文本匹配点击提交: "${text}"`, 'success'); submitSuccess = true; break; } catch (e) { logStore.addLog(`文本匹配点击失败: ${e.message}`, 'warning'); } } } if (!submitSuccess) { logStore.addLog("文本匹配未找到提交按钮", "warning"); } } // 策略4:通过事件模拟提交(针对现代前端框架) if (!submitSuccess) { try { const form = iframeWindow.document.querySelector('form'); if (form) { const submitEvent = new Event('submit', { bubbles: true, cancelable: true }); form.dispatchEvent(submitEvent); logStore.addLog('通过表单事件提交', 'success'); submitSuccess = true; } } catch (e) { logStore.addLog(`表单事件提交失败: ${e.message}`, 'warning'); } } // 策略5:暴力遍历所有可点击元素 if (!submitSuccess) { try { const allElements = iframeWindow.document.querySelectorAll('*'); logStore.addLog(`暴力扫描 ${allElements.length} 个元素`, "info"); for (const el of allElements) { const text = el.textContent?.trim() || el.value?.trim() || ''; if (text.length > 0 && text.length < 20 && /^(提交|提交答案|完成|确认提交|交卷|完成答题|确认交卷|提交作业|交作业|完成作业)$/.test(text)) { const style = iframeWindow.getComputedStyle(el); const isVisible = el.offsetParent !== null && el.offsetWidth > 0 && el.offsetHeight > 0 && style.visibility !== 'hidden' && style.display !== 'none'; if (isVisible) { logStore.addLog(`暴力扫描找到提交: "${text}" (${el.tagName}.${el.className?.trim()})`, "info"); const onclick = el.getAttribute('onclick'); if (onclick) { try { safeClick(el); logStore.addLog(`暴力扫描onclick执行提交`, 'success'); submitSuccess = true; break; } catch (e) {} } safeClick(el); logStore.addLog(`暴力扫描点击提交`, 'success'); submitSuccess = true; break; } } } } catch (e) { logStore.addLog(`暴力扫描失败: ${e.message}`, 'warning'); } } // 如果本次尝试失败,等待后重试 if (!submitSuccess && submitAttempts < maxAttempts) { logStore.addLog(`第 ${submitAttempts} 次提交失败,等待后重试...`, "warning"); await quickRandomDelay(); } } if (submitSuccess) { logStore.addLog("提交成功", "success"); await quickRandomDelay(); // ========== 新增:学习提交结果 + 循环重新答题直到100%正确 ========== try { // 等待页面加载出结果 await delay(3); logStore.addLog("🔍 解析提交结果并学习正确答案...", "info"); // 解析提交结果,学习正确答案 const learnedCount = WrongAnswerLearner.parseAndLearnFromResult(iframeWindow, handler.questions); if (learnedCount > 0) { logStore.addLog(`✅ 学习了 ${learnedCount} 个正确答案`, "success"); } // 检查是否有错题需要重新答题 let hasWrongAnswers = false; try { // 检查页面是否有错误标记 const wrongIcons = iframeWindow.document.querySelectorAll('.u-icon-error, .wrong-icon, [class*="error"], [class*="wrong"]'); hasWrongAnswers = wrongIcons && wrongIcons.length > 0; } catch (e) {} if (hasWrongAnswers || learnedCount > 0) { logStore.addLog("🔄 发现需要修正的题目,准备重新答题...", "info"); // 等待一下再返回 await delay(1); // 尝试找到返回/重新答题按钮 let backSuccess = false; const backSelectors = [ 'button:contains("返回")', 'button:contains("重新答题")', 'a:contains("返回")', 'a:contains("重新答题")', '[onclick*="back"]', '[onclick*="retry"]', '.btn-back', '.back-btn' ]; for (const selector of backSelectors) { try { const backBtn = iframeWindow.document.querySelector(selector); if (backBtn) { safeClick(backBtn); logStore.addLog(`点击返回/重新答题按钮: ${selector}`, "success"); backSuccess = true; break; } } catch (e) {} } // 如果没有返回按钮,尝试直接刷新或等待 if (!backSuccess) { logStore.addLog("等待页面刷新...", "warning"); await delay(3); } // 重新开始答题流程 logStore.addLog("♻️ 重新开始答题(使用已学习的正确答案)", "primary"); // 重新调用 parseAndAnswer await parseAndAnswer(iframeWindow); return; } } catch (learnErr) { console.warn('[学习结果] 解析失败:', learnErr); } } else { logStore.addLog("所有提交方式均失败,请手动提交", "danger"); } } else { if (hasLocalModeQuestions) { if (!allAttempted) { logStore.addLog(`部分题目未作答(${unansweredCount}道),暂存`, "warning"); } else { logStore.addLog("所有题目已作答,准备提交", "success"); } } else { logStore.addLog(`正确率${rate.toFixed(1)}%小于${correctRateThreshold}%,暂存`, "danger"); } if(typeof iframeWindow.noSubmit === 'function') await iframeWindow.noSubmit(); } } else { logStore.addLog("未开启自动提交,暂存", "primary"); if(typeof iframeWindow.noSubmit === 'function') await iframeWindow.noSubmit(); } logStore.addLog("作业已完成", "success"); }catch(e){ logStore.addLog(`作业处理异常: ${e.message}`, "danger"); } }; const handleSlideshow = async (iframeWindow) => { logStore.addLog("发现一个PPT,正在解析", "warning"); // 优先尝试 finishJob(与old版本一致,直接通知服务端完成) if (typeof iframeWindow.finishJob === "function") { iframeWindow.finishJob(); await quickRandomDelay(); logStore.addLog("PPT阅读完成", "success"); return Promise.resolve(); } // 检测带音频的PPT(Swiper轮播组件) const swiperContainer = iframeWindow.document.querySelector(".swiper-container"); if (swiperContainer) { // 静音所有音频 iframeWindow.document.querySelectorAll("audio").forEach((audio) => { audio.addEventListener("play", () => { audio.muted = true; }); }); const slides = iframeWindow.document.querySelectorAll(".swiper-container .swiper-slide"); const len = slides.length; logStore.addLog(`检测到带音频PPT,共${len}页,正在翻阅`, "primary"); for (let i = 0; i < len; i++) { if (typeof iframeWindow.swiperNext === "function") { iframeWindow.swiperNext(); } await quickRandomDelay(); } await quickRandomDelay(); logStore.addLog("PPT翻阅完成", "success"); return Promise.resolve(); } // fallback: 滚动到底部 const pptWindow = iframeWindow.document.querySelector("#panView")?.contentWindow; if (pptWindow) { await pptWindow.scrollTo({ top: pptWindow.document.body.scrollHeight, behavior: "smooth" }); await quickRandomDelay(); } logStore.addLog("PPT阅读完成", "success"); return Promise.resolve(); }; const handleEbook = async (iframeWindow) => { logStore.addLog("发现一个电子书,正在解析", "warning"); _unsafeWindow.top.onchangepage(iframeWindow.getFrameAttr("end")); logStore.addLog("阅读完成", "success"); return Promise.resolve(); }; const awaitFrameReady = async (iframe) => { return new Promise((resolve) => { const intervalId = setInterval(async () => { var _a; if (iframe.contentDocument && ((_a = iframe.contentDocument) == null ? void 0 : _a.readyState) == "complete") { resolve(); clearInterval(intervalId); } }, 500); }); }; const handleSingleFrame = async (iframe) => { var _a, _b; stopSimulatePlayIfNeeded(); const iframeSrc = iframe.src; const iframeDocument = iframe.contentDocument; const iframeWindow = iframe.contentWindow; if (!iframeDocument || !iframeWindow) { return Promise.resolve(); } if (iframeSrc.includes("javascript:")) { return Promise.resolve(); } const lastTaskId = _processedIframeTasks.get(iframe); const currentTaskId = _globalTaskId; if (lastTaskId === currentTaskId) { return Promise.resolve(); } _processedIframeTasks.set(iframe, currentTaskId); await awaitFrameReady(iframe); const parentClass = ((_a = iframe.parentElement) == null ? void 0 : _a.className) || ""; if (parentClass.includes("ans-job-finished")) { } else { const _src = iframe.getAttribute('_src') || ''; const matchSrc = iframeSrc.includes("api/work"); const matchSrcAttr = _src.includes("api/work"); const normalMode = configStore.platformParams?.cx?.parts?.[2]?.params?.[2]?.value; const onlyVideo = configStore.platformParams?.cx?.parts?.[2]?.params?.[3]?.value; const onlyAnswer = configStore.platformParams?.cx?.parts?.[2]?.params?.[4]?.value; if (matchSrcAttr && !matchSrc) { return Promise.resolve(); } if (matchSrc || matchSrcAttr) { if (onlyVideo) { if (!hasLoggedSkipTip) { logStore.addLog("仅视频模式,跳过答题", "primary"); hasLoggedSkipTip = true; } return Promise.resolve(); } return handleAssignment(iframe, iframeDocument, iframeWindow); } const ansJobIcon = (_b = iframe.parentElement) == null ? void 0 : _b.querySelector(".ans-job-icon"); if (ansJobIcon) { if (onlyAnswer) { if (!hasLoggedSkipTip) { logStore.addLog("仅答题模式,跳过视频等其他内容", "primary"); hasLoggedSkipTip = true; } return Promise.resolve(); } if (iframeSrc.includes("video")) { return handleMediaContent("video", iframeDocument, iframe); } else if (iframeSrc.includes("audio")) { return handleMediaContent("audio", iframeDocument, iframe); } else if (iframeDocument.querySelector("#img.imglook") || iframeDocument.querySelector(".swiper-container")) { return handleSlideshow(iframeWindow); } else if (iframeSrc.includes("modules/innerbook")) { return handleEbook(iframeWindow); } } } return Promise.resolve(); }; init(); monitorIframes(); watchUrlChanges(); }; const clickOption = (element) => { try{ if(!element) return; // 先检查是否已选中 const isChecked = element.getAttribute("aria-checked") === "true" || element.classList.contains("is-checked") || element.classList.contains("selected") || element.classList.contains("answer-selected"); if(isChecked) return; // 已选中,无需重复点击 // 聚焦元素 element.focus(); // 模拟完整的人类点击行为序列 element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })); element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })); element.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); element.click(); // 尝试点击内部input元素(如果有) const input = element.querySelector('input[type="radio"], input[type="checkbox"]'); if(input) { input.focus(); input.click(); } // 尝试点击内部label元素(如果有) const label = element.querySelector('label'); if(label) { label.click(); } }catch(e){} }; const useStuActiveLogic = async () => { try { window.parent.postMessage({ source: 'chaoxing-helper-iframe', action: 'closePanel' }, '*'); } catch (e) {} const logStore = useLogStore(); const questionStore = useQuestionStore(); const progressStore = useProgressStore(); const configStore = useConfigStore(); logStore.addLog(`进入随堂练习答题页面`, "primary"); logStore.addLog(`等待Vue渲染题目...`, "warning"); progressStore.update({ taskName: "AI答题", percent: 0, type: "答题", detail: "正在解析题目...", isPlaying: true }); decodeCipherFont(document); let questionItems = null; for (let i = 0; i < 60; i++) { await delay(0.5); questionItems = document.querySelectorAll(".question-item"); if (questionItems.length > 0) break; } if (!questionItems || questionItems.length === 0) { logStore.addLog("未解析到题目,可能页面尚未加载完成", "danger"); logStore.addLog("请刷新页面重试", "warning"); progressStore.update({ taskName: "暂无任务", percent: 0, type: "-", detail: "解析题目失败", isPlaying: false }); return; } const questionTypeMapping = { "单选题": "0", "多选题": "1", "判断题": "3", "填空题": "2" }; const questions = []; questionItems.forEach((questionItem) => { const questionNameEl = questionItem.querySelector(".question-name"); const questionText = questionNameEl ? questionNameEl.innerText.trim() : ""; let questionTypeText = "单选题"; if (questionItem.classList.contains("multiple-choice")) { questionTypeText = "多选题"; } else if (questionText.includes("判断题")) { questionTypeText = "判断题"; } else if (questionText.includes("填空题")) { questionTypeText = "填空题"; } const cleanTitle = questionText .replace(/^\d+\.\s*/, "") .replace(/\[单选题\]|\[多选题\]|\[判断题\]|\[填空题\]/g, "") .trim(); const optionLis = questionItem.querySelectorAll(".option-list li"); const optionsObject = {}; const optionTexts = []; optionLis.forEach((li) => { const result = li.querySelector(".option-result")?.innerText?.trim() || ""; optionsObject[result] = li; optionTexts.push(result); }); questions.push({ element: questionItem, type: questionTypeMapping[questionTypeText] || "0", title: cleanTitle, optionsText: optionTexts, options: optionsObject, answer: [], workType: "stuActive", refer: window.location.href }); }); logStore.addLog(`成功解析到${questions.length}道题目`, "success"); progressStore.update({ taskName: `AI答题 (共${questions.length}题)`, percent: 0, type: "答题", detail: `0/${questions.length} 已完成`, isPlaying: true }); const answerParamsPart = configStore.platformParams.cx?.parts.find(p => p.name === "答题参数"); const skipAnswered = answerParamsPart?.params.find(p => p.name === "跳过已答")?.value || false; const answerInterval = answerParamsPart?.params.find(p => p.name === "答题间隔")?.value || 1; const useSimilarity = answerParamsPart?.params.find(p => p.name === "相似匹配")?.value || false; const simulateDelay = answerParamsPart?.params.find(p => p.name === "模拟延迟")?.value ?? true; let skippedCount = 0; for (const [index, question] of questions.entries()) { const isAnswered = Array.from(question.element.querySelectorAll(".option-list li")).some(li => li.classList.contains("active")); if (skipAnswered && isAnswered) { logStore.addLog(`第${index + 1}题已作答,跳过`, "warning"); skippedCount += 1; questionStore.addQuestion(question); continue; } logStore.addLog(`正在查找第${index + 1}道题目答案...`, "primary"); const answerData = await queryAnswer(question); if (answerData.code === 499) { break; } if (answerData.code === 200) { question.answer = answerData.data.answer; question.source = answerData.data.source; if (question.type === "0" || question.type === "1" || question.type === "3") { const selectedKeys = new Set(); for (const answer of question.answer) { const cleanAnswer = answer.replace(/<[^>]*>/g, "").trim(); let matched = false; // 策略1:精确匹配选项文本 for (const key in question.options) { if (key === cleanAnswer && !selectedKeys.has(key)) { matched = true; selectedKeys.add(key); clickOption(question.options[key]); await randomDelay(0.1, 0.15); break; } } // 策略2:去除选项前缀后匹配(如 "A. xxx" -> "xxx") if (!matched) { const cleanAnswerNoPrefix = cleanAnswer.replace(/^[A-Z][.、]\s*/, '').trim(); for (const key in question.options) { const cleanKey = key.replace(/^[A-Z][.、]\s*/, '').trim(); if (cleanKey === cleanAnswerNoPrefix && !selectedKeys.has(key)) { matched = true; selectedKeys.add(key); clickOption(question.options[key]); await randomDelay(0.1, 0.15); break; } } } // 策略3:包含匹配(答案包含在选项中,或选项包含在答案中) if (!matched) { for (const key in question.options) { const cleanKey = key.replace(/<[^>]*>/g, "").trim(); if ((cleanKey.includes(cleanAnswer) || cleanAnswer.includes(cleanKey)) && cleanAnswer.length > 2 && !selectedKeys.has(key)) { matched = true; selectedKeys.add(key); clickOption(question.options[key]); await randomDelay(0.1, 0.15); break; } } } // 策略4:相似度匹配兜底(使用标准化匹配) if (!matched) { // 【优化】使用更高的阈值 60% 减少误匹配 const bestMatch = pickBestOption(cleanAnswer, question.options, 60); if (bestMatch && !selectedKeys.has(bestMatch.key)) { matched = true; selectedKeys.add(bestMatch.key); clickOption(question.options[bestMatch.key]); await randomDelay(0.1, 0.15); } } if (!matched) { logStore.addLog(`答案填写失败: "${cleanAnswer.substring(0, 20)}..." 无法匹配选项`, 'warning'); } } } const aiSources = ['hunyuan-standard', 'hunyuan-t1', 'DeepSeek-V3.2-Think', 'DeepSeek-V3.2', 'DeepSeek-R1-0528', 'qwen3.6-plus', 'qwen3.5-plus', 'minimax-m2.5', 'minimax-m2.7', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gemini-3.1-flash-lite-preview', 'Pro/zai-org/GLM-5', 'Pro/zai-org/GLM-5.1', 'Pro/zai-org/GLM-4.7']; const isFromAI = aiSources.includes(answerData.data.source); const sourceHint = isFromAI ? `(${answerData.data.source})` : ""; const msgHint = answerData.msg ? ` - ${answerData.msg}` : ""; logStore.addLog(`第${index + 1}道题搜索成功${sourceHint}${msgHint}`, "success"); // 关键修复:所有答题成功后都需要扣减次数 try { console.log('[学习通助手] 随堂练习答题成功,开始扣减次数, source:', answerData.data.source); const consumed = await ClientLicense.consumeOne(); console.log('[学习通助手] consumeOne 返回:', consumed); if (consumed.ok) { logStore.addLog(`✅ 次数已扣减,剩余: ${consumed.uses_remaining}次`, "success"); // 剩余次数不足时提醒购买 if (typeof consumed.uses_remaining === 'number' && consumed.uses_remaining <= 5 && consumed.uses_remaining > 0) { logStore.addLog(`⚠️ 剩余次数较少(${consumed.uses_remaining}次),建议及时充值`, 'warning'); logStore.addLog('🛒 点击购买卡密,获取更多答题次数', 'warning'); } } else { // 次数耗尽时明确提醒购买 if (consumed.message === 'exhausted') { logStore.addLog('❌ 答题次数已用完,无法继续答题', 'danger'); logStore.addLog('🛒 点击购买卡密,享无限答题', 'warning'); } else if (consumed.message === 'tampered') { logStore.addLog('❌ 授权异常:检测到篡改', 'danger'); } else { logStore.addLog(`⚠️ 次数扣减失败: ${consumed.message}`, "warning"); } } } catch (e) { console.error('[学习通助手] 扣减次数异常:', e); logStore.addLog(`❌ 扣减次数异常: ${e.message}`, "danger"); } } else { if (answerData.code === 403 && answerData.data && answerData.data.limitedMode) { logStore.addLog(`第${index + 1}道题搜索失败: 免费题库无答案`, "danger"); question.answer[0] = answerData.msg; } else if (answerData.code === 429 && answerData.data && answerData.data.limitedMode) { logStore.addLog(`第${index + 1}道题搜索失败: 今日免费查题已达上限`, "danger"); logStore.addLog('💎 点击购买Token,享无限查题', 'warning'); question.answer[0] = answerData.msg; } else { logStore.addLog(`第${index + 1}道题搜索失败: ${answerData.msg}`, "danger"); question.answer[0] = answerData.msg; } } questionStore.addQuestion(question); const completedCount = index + 1 - skippedCount; const progressPercent = Math.round((completedCount / questions.length) * 100); const confidence = answerData.data?.confidence || 0; const qualityIcon = confidence > 70 ? '✅' : confidence > 50 ? '⚠️' : '❓'; progressStore.update({ taskName: `AI答题 (共${questions.length}题)`, percent: progressPercent, type: "答题", detail: `${completedCount}/${questions.length} ${qualityIcon} 已完成`, isPlaying: true }); await (simulateDelay ? randomDelay(Number(answerInterval), 0.5) : delay(Number(answerInterval))); } if (skippedCount > 0) { logStore.addLog(`共跳过${skippedCount}道已答题目`, "primary"); } logStore.addLog("随堂练习答题完成", "success"); progressStore.update({ taskName: "答题完成", percent: 100, type: "答题", detail: "所有题目已处理完毕 ✅", isPlaying: false }); const autoSubmit = configStore.platformParams.cx.parts[2]?.params.find(p => p.name === "自动提交")?.value; if (autoSubmit) { logStore.addLog("自动提交已开启,准备提交...", "warning"); await quickRandomDelay(); let submitSuccess = false; // 策略1:尝试学习通标准提交函数 try { if (typeof window.btnBlueSubmit === 'function') { window.btnBlueSubmit(); logStore.addLog("使用 btnBlueSubmit() 提交", "success"); submitSuccess = true; } else if (typeof window.submitCheckTimes === 'function') { window.submitCheckTimes(); logStore.addLog("使用 submitCheckTimes() 提交", "success"); submitSuccess = true; } else if (typeof window.submitWork === 'function') { window.submitWork(); logStore.addLog("使用 submitWork() 提交", "success"); submitSuccess = true; } } catch (e) { logStore.addLog(`函数提交失败: ${e.message}`, "warning"); } // 策略2:查找提交按钮 if (!submitSuccess) { const submitSelectors = [ 'input[type="submit"][value*="提交"]', 'button[type="submit"]', 'input[value="提交答案"]', 'input[value="提交"]', 'button[onclick*="submit"]', 'button[onclick*="btnBlueSubmit"]', '.submit-btn', '.btn-submit', '#submit', '.submit' ]; for (const selector of submitSelectors) { const submitBtn = document.querySelector(selector); if (submitBtn && !submitBtn.disabled && !submitBtn.classList.contains('disabled')) { try { submitBtn.scrollIntoView({ block: 'center', behavior: 'smooth' }); await delay(0.5); const onclick = submitBtn.getAttribute('onclick'); if (onclick) { try { clickOption(submitBtn); logStore.addLog(`通过onclick执行提交`, 'success'); submitSuccess = true; break; } catch (e) {} } if (!submitSuccess) { clickOption(submitBtn); logStore.addLog(`点击提交按钮: ${selector}`, 'success'); submitSuccess = true; break; } } catch (e) { logStore.addLog(`点击提交按钮失败: ${e.message}`, 'warning'); } } } } // 策略3:通过文本匹配查找提交按钮 if (!submitSuccess) { const allButtons = document.querySelectorAll('button, input[type="button"], a.btn'); for (const btn of allButtons) { const text = btn.textContent?.trim() || ''; if (/^(提交|提交答案|完成|确认提交)$/.test(text)) { try { const onclick = btn.getAttribute('onclick'); if (onclick) { try { clickOption(btn); logStore.addLog(`通过文本匹配执行提交`, 'success'); submitSuccess = true; break; } catch (e) {} } clickOption(btn); logStore.addLog(`通过文本匹配点击提交`, 'success'); submitSuccess = true; break; } catch (e) {} } } } if (submitSuccess) { logStore.addLog("已执行提交,等待确认...", "info"); await quickRandomDelay(); // 处理提交确认弹窗 const confirmDialog = document.querySelector('.layui-layer, .confirm-dialog, .submit-confirm'); if (confirmDialog) { const confirmBtn = confirmDialog.querySelector('button[value*="提交"], .layui-layer-btn0, .confirm-btn, .ok-btn'); if (confirmBtn) { clickOption(confirmBtn); logStore.addLog("已确认提交", "success"); } } logStore.addLog("提交完成", "success"); // 自动跳转下一章节 const autoSwitch = configStore.platformParams?.cx?.parts?.[2]?.params?.[1]?.value; if (autoSwitch) { logStore.addLog("自动切换已开启,准备跳转下一章节", "success"); // 跳转前状态检查 let navigationSuccess = false; let navigationAttempts = 0; const maxNavigationAttempts = 3; while (!navigationSuccess && navigationAttempts < maxNavigationAttempts) { navigationAttempts++; logStore.addLog(`跳转尝试 ${navigationAttempts}/${maxNavigationAttempts}`, "info"); // 使用快速随机延迟替代固定3秒 await quickRandomDelay(); // 多种选择器查找下一章节按钮 const nextSelectors = [ "#prevNextFocusNext", ".jb_btn.jb_btn_92.fr.fs14.nextChapter", "#nextBtn", ".nextChapter", "a[href*='next']", "button[onclick*='next']", ".btn-next", ".next-btn", "[data-action='next']", "[title*='下一']", "[aria-label*='下一']", "a:contains('下一章节')", "button:contains('下一章节')", ".chapter-nav .next", ".nav-next", "#next-chapter" ]; let targetBtn = null; let foundSelector = ''; // 策略1:通过选择器查找 for (const selector of nextSelectors) { try { const btn = document.querySelector(selector); if (btn && btn.offsetParent !== null) { // 检查元素是否可见 const rect = btn.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { // 检查尺寸 targetBtn = btn; foundSelector = selector; break; } } } catch (e) { // 忽略选择器错误 } } // 策略2:通过文本匹配查找 if (!targetBtn) { const allButtons = document.querySelectorAll('button, a.btn, .btn, [role="button"], .jb_btn'); for (const btn of allButtons) { const text = btn.textContent?.trim() || btn.value?.trim() || btn.getAttribute('aria-label')?.trim() || ''; if (/^(下一章节|下一题|下一章|继续|下一步|Next|next)$/.test(text)) { targetBtn = btn; foundSelector = `text:"${text}"`; break; } } } // 策略3:查找包含"下一"关键词的元素 if (!targetBtn) { const allLinks = document.querySelectorAll('a, button'); for (const el of allLinks) { const text = el.textContent?.trim() || ''; if (text.includes('下一') && text.includes('章')) { targetBtn = el; foundSelector = `link:"${text}"`; break; } } } if (targetBtn) { logStore.addLog(`找到下一章节按钮 (${foundSelector}),准备点击`, "success"); try { // 滚动到按钮位置 targetBtn.scrollIntoView({ block: 'center', behavior: 'smooth' }); await quickRandomDelay(); // 尝试多种点击方式 const onclick = targetBtn.getAttribute('onclick'); if (onclick) { try { clickOption(targetBtn); logStore.addLog(`通过onclick执行跳转`, 'success'); navigationSuccess = true; break; } catch (e) { logStore.addLog(`onclick执行失败: ${e.message}`, 'warning'); } } if (!navigationSuccess) { clickOption(targetBtn); logStore.addLog(`已点击下一章节按钮`, "success"); navigationSuccess = true; } } catch (e) { logStore.addLog(`点击下一章节按钮失败: ${e.message}`, "warning"); } } else { logStore.addLog(`未找到下一章节按钮 (尝试 ${navigationAttempts}/${maxNavigationAttempts})`, "warning"); } // 如果本次尝试失败,等待后重试 if (!navigationSuccess && navigationAttempts < maxNavigationAttempts) { logStore.addLog(`第 ${navigationAttempts} 次跳转失败,等待后重试...`, "warning"); await quickRandomDelay(); } } if (navigationSuccess) { logStore.addLog("已成功跳转至下一章节", "success"); } else { logStore.addLog("所有跳转尝试均失败,请手动跳转", "danger"); } } } else { logStore.addLog("未找到提交按钮,请手动提交", "danger"); } } else { logStore.addLog("未开启自动提交,请手动提交", "info"); } }; const useCxWorkLogic = async () => { const logStore = useLogStore(); useConfigStore(); logStore.addLog(`进入新版作业页面,开始准备答题`, "primary"); logStore.addLog(`正在解析题目, 请等待5s`, "warning"); try{ await Promise.race([ new CxQuestionHandler("zy").init(), new Promise((_, reject) => setTimeout(() => reject(new Error("解析超时")), 30000)) ]); }catch(e){ logStore.addLog(`解析题目异常: ${e.message}`, "danger"); } }; const useCxExamLogic = async () => { var _a; const logStore = useLogStore(); const configStore = useConfigStore(); logStore.addLog(`进入新版考试页面,开始准备答题`, "primary"); logStore.addLog(`正在解析题目, 请等待5s`, "warning"); try{ await Promise.race([ new CxQuestionHandler("ks").init(), new Promise((_, reject) => setTimeout(() => reject(new Error("解析超时")), 30000)) ]); if (configStore.platformParams.cx.parts[3].params[0].value) { const currentQuestionNum = parseInt(((_a = _unsafeWindow.document.querySelector(".topicNumber_list .current")) == null ? void 0 : _a.innerText) || "0"); const totalQuestions = _unsafeWindow.document.querySelectorAll(".topicNumber_list li").length; if (currentQuestionNum >= totalQuestions) { logStore.addLog("当前已是最后一题,不再自动切换", "warning"); logStore.addLog("请手动检查答案后提交试卷", "primary"); } else { logStore.addLog("自动切换已开启,正在前往下一题", "success"); await quickRandomDelay(); if(typeof _unsafeWindow.getTheNextQuestion === 'function') _unsafeWindow.getTheNextQuestion(1); } } else { logStore.addLog("已经关闭自动切换,在设置里可更改", "danger"); } }catch(e){ logStore.addLog(`解析题目异常: ${e.message}`, "danger"); } }; class ZhsQuestionHandler extends QuestionProcessor { constructor() { super(); __publicField(this, "isZhsQuestionAnswered", (question) => { const reverseTypeMapping = { "0": "单选题", "1": "多选题", "2": "填空题", "3": "判断题", "4": "简答题", "5": "名词解释", "6": "论述题", "7": "计算题" }; const questionTypeText = reverseTypeMapping[question.type] || question.type; if (questionTypeText === "单选题" || questionTypeText === "多选题") { for (const key in question.options) { const optionElement = question.options[key]; if (optionElement.classList.contains("cur") || optionElement.classList.contains("selected") || optionElement.querySelector(".onChecked") || optionElement.querySelector(".cur") || optionElement.querySelector(".selected")) { return true; } } return false; } if (questionTypeText === "判断题") { for (const key in question.options) { const optionElement = question.options[key]; if (optionElement.classList.contains("cur") || optionElement.classList.contains("selected") || optionElement.querySelector(".onChecked")) { return true; } } return false; } return false; }); __publicField(this, "init", async () => { var _a; this.questions = []; this.parseHtml(); if (this.questions.length) { this.addLog(`成功解析到${this.questions.length}个题目`, "primary"); const configStore = useConfigStore(); const _answerParamsPart2 = configStore.platformParams[configStore.platformName]?.parts.find(p => p.name === "答题参数"); const skipAnswered = _answerParamsPart2?.params.find(p => p.name === "跳过已答")?.value || false; let skippedCount = 0; for (const [index, question] of this.questions.entries()) { if (skipAnswered && this.isZhsQuestionAnswered(question)) { this.addLog(`第${index + 1}道题已作答,跳过`, "warning"); skippedCount += 1; this.addQuestion(question); const switchBtns = (_a = this._document) == null ? void 0 : _a.querySelectorAll(".switch-btn-box > button"); if (switchBtns && switchBtns[1]) safeClick(switchBtns[1]); await new Promise(r => setTimeout(r, 1500)); continue; } this.addLog(`正在查找第${index + 1}道题目答案...`, "primary"); const answerData = await queryAnswer(question); if (answerData.code === 200) { question.answer = answerData.data.answer; question.source = answerData.data.source; this.addQuestion(question); await new Promise(r => setTimeout(r, 100)); await this.fillQuestion(question); const aiSources = ['hunyuan-standard', 'hunyuan-t1', 'DeepSeek-V3.2-Think', 'DeepSeek-V3.2', 'DeepSeek-R1-0528', 'qwen3.6-plus', 'qwen3.5-plus', 'minimax-m2.5', 'minimax-m2.7', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gemini-3.1-flash-lite-preview', 'Pro/zai-org/GLM-5', 'Pro/zai-org/GLM-5.1', 'Pro/zai-org/GLM-4.7']; const isFromAI = aiSources.includes(answerData.data.source); const sourceHint = isFromAI ? `(${answerData.data.source})` : ""; const msgHint = answerData.msg ? ` - ${answerData.msg}` : ""; this.addLog(`第${index + 1}道题搜索成功${sourceHint}${msgHint}`, "success"); // 关键修复:所有答题成功后都需要扣减次数 try { console.log('[学习通助手] 智慧树答题成功,开始扣减次数, source:', answerData.data.source); const consumed = await ClientLicense.consumeOne(); console.log('[学习通助手] consumeOne 返回:', consumed); if (consumed.ok) { this.addLog(`✅ 次数已扣减,剩余: ${consumed.uses_remaining}次`, "success"); // 剩余次数不足时提醒购买 if (typeof consumed.uses_remaining === 'number' && consumed.uses_remaining <= 5 && consumed.uses_remaining > 0) { this.addLog(`⚠️ 剩余次数较少(${consumed.uses_remaining}次),建议及时充值`, 'warning'); this.addLog('🛒 点击购买卡密,获取更多答题次数', 'warning'); } } else { // 次数耗尽时明确提醒购买 if (consumed.message === 'exhausted') { this.addLog('❌ 答题次数已用完,无法继续答题', 'danger'); this.addLog('🛒 点击购买卡密,享无限答题', 'warning'); } else if (consumed.message === 'tampered') { this.addLog('❌ 授权异常:检测到篡改', 'danger'); } else { this.addLog(`⚠️ 次数扣减失败: ${consumed.message}`, "warning"); } } } catch (e) { console.error('[学习通助手] 扣减次数异常:', e); this.addLog(`❌ 扣减次数异常: ${e.message}`, "danger"); } } else { this.addLog(`第${index + 1}道题搜索失败:${answerData.msg}`, "danger"); question.answer[0] = answerData.msg; this.addQuestion(question); await new Promise(r => setTimeout(r, 100)); } const switchBtns2 = (_a = this._document) == null ? void 0 : _a.querySelectorAll(".switch-btn-box > button"); if (switchBtns2 && switchBtns2[1]) safeClick(switchBtns2[1]); await new Promise(r => setTimeout(r, 1500)); } if (skippedCount > 0) { this.addLog(`共跳过${skippedCount}道已答题目`, "primary"); } } else this.addLog("未解析到题目,请刷新重试或进入答题页面", "danger"); }); __publicField(this, "parseHtml", () => { if (!this._document) return []; const questionElements = this._document.querySelectorAll(SELECTORS.ZHS_QUESTION); this.addQuestions(questionElements); }); __publicField(this, "fillQuestion", async (question) => { if (!this._window) return; try { const typeNum = question.type; if (typeNum === "0" || typeNum === "1") { const configStore = useConfigStore(); const useSimilarity = configStore.platformParams[configStore.platformName]?.parts.find(p => p.name === "答题参数")?.params.find(p => p.name === "相似匹配")?.value || false; let hasDeselected = false; for (const key in question.options) { const optionElement = question.options[key]; if (optionElement.classList.contains("cur") || optionElement.classList.contains("selected") || optionElement.querySelector(".onChecked") || optionElement.querySelector(".cur") || optionElement.querySelector(".selected")) { hasDeselected = true; safeClick(optionElement); } } if (hasDeselected) { await new Promise(r => setTimeout(r, 500)); } const selectedKeys = new Set(); console.log("🔍 答案格式:", typeof question.answer, Array.isArray(question.answer), question.answer); console.log("🔍 选项keys:", Object.keys(question.options)); let answers = question.answer; if (typeof question.answer === 'string') { if (question.answer.startsWith('[')) { try { answers = JSON.parse(question.answer); } catch (e) { answers = question.answer.split(/[,,]/).map(a => a.trim()).filter(a => a); } } else { answers = question.answer.split(/[,,]/).map(a => a.trim()).filter(a => a); } console.log("🔍 答案已分割:", answers); } answers.forEach((answer) => { const cleanAnswer = this.stripTags(answer).trim(); let matched = false; for (const key in question.options) { const cleanKey = key.trim(); console.log(`🔍 匹配: "${cleanAnswer}" vs "${cleanKey}" = ${cleanKey === cleanAnswer}`); if (cleanKey === cleanAnswer && !selectedKeys.has(cleanKey)) { matched = true; selectedKeys.add(key); const optionElement = question.options[key]; const isAlreadySelected = optionElement.classList.contains("cur") || optionElement.classList.contains("selected") || optionElement.querySelector(".onChecked") || optionElement.querySelector(".cur") || optionElement.querySelector(".selected"); if (!isAlreadySelected) { optionElement.setAttribute("data-filling", "true"); safeClick(optionElement); setTimeout(() => optionElement.removeAttribute("data-filling"), 200); } break; } } // 策略4:相似度匹配兜底(使用标准化匹配,默认阈值 60%) if (!matched && useSimilarity) { const bestMatch = pickBestOption(cleanAnswer, question.options); if (bestMatch && !selectedKeys.has(bestMatch.key)) { selectedKeys.add(bestMatch.key); const optionElement = question.options[bestMatch.key]; const isAlreadySelected = optionElement.classList.contains("cur") || optionElement.classList.contains("selected") || optionElement.querySelector(".onChecked") || optionElement.querySelector(".cur") || optionElement.querySelector(".selected"); if (!isAlreadySelected) { optionElement.setAttribute("data-filling", "true"); safeClick(optionElement); setTimeout(() => optionElement.removeAttribute("data-filling"), 200); } } } }); } else if (typeNum === "3") { let answer = "错"; if (REGEX.JUDGE_FALSE.test(question.answer[0])) { answer = "错"; } else if (REGEX.JUDGE_TRUE.test(question.answer[0])) { answer = "对"; } for (const key in question.options) { const optionElement = question.options[key]; const isTrueOption = REGEX.JUDGE_TRUE.test(key); const isFalseOption = REGEX.JUDGE_FALSE.test(key); if ((isTrueOption && answer === "对") || (isFalseOption && answer === "错")) { optionElement.setAttribute("data-filling", "true"); safeClick(optionElement); setTimeout(() => optionElement.removeAttribute("data-filling"), 200); break; } } } else if (typeNum === "2") { const textareaElements = question.element.querySelectorAll("textarea"); if (textareaElements.length > 0 && question.answer && question.answer.length > 0) { textareaElements.forEach((textarea, index) => { if (index < question.answer.length) { const answerText = this.stripTags(question.answer[index] || question.answer[0]); textarea.value = answerText; textarea.dispatchEvent(new Event("input", { bubbles: true })); textarea.dispatchEvent(new Event("change", { bubbles: true })); textarea.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true })); textarea.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true })); if(textarea.__vue__){ try{ textarea.__vue__.$emit("input", answerText); }catch(e){} } console.log(`🔍 填空题填入: 第${index + 1}空 = "${answerText}"`); } }); } } else if (["4", "5", "6", "7"].includes(typeNum)) { const textareaElement = question.element.querySelector("textarea"); if (textareaElement && question.answer && question.answer.length > 0) { const answerText = question.answer.map(a => this.stripTags(a)).join("\n"); textareaElement.value = answerText; textareaElement.dispatchEvent(new Event("input", { bubbles: true })); textareaElement.dispatchEvent(new Event("change", { bubbles: true })); textareaElement.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true })); textareaElement.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true })); if(textareaElement.__vue__){ try{ textareaElement.__vue__.$emit("input", answerText); }catch(e){} } console.log(`🔍 简答题填入: "${answerText.substring(0, 50)}..."`); } } else { console.log(`⚠️ 未处理的题型: ${typeNum}`); } } catch (error) { this.addLog(`答题过程发生错误:${error.message}`, "danger"); } }); } extractOptions(optionElements, optionSelector) { const optionsObject = {}; const optionTexts = []; optionElements.forEach((optionElement) => { var _a; const optionTextContent = this.stripTags(((_a = optionElement.querySelector(optionSelector)) == null ? void 0 : _a.innerHTML) || ""); optionsObject[optionTextContent] = optionElement; optionTexts.push(optionTextContent); }); return [optionsObject, optionTexts]; } addQuestions(questionElements) { questionElements.forEach((questionElement) => { var _a, _b, _c, _d; let questionTitle = ""; try{ const titleEl = questionElement == null ? void 0 : questionElement.querySelector(".subject_describe div,.smallStem_describe p"); if(titleEl && titleEl.__Ivue__ && titleEl.__Ivue__._data && titleEl.__Ivue__._data.shadowDom){ questionTitle = titleEl.__Ivue__._data.shadowDom.textContent || ""; } }catch(e){ questionTitle = ""; } const questionTypeText = ((_b = (_a = questionElement == null ? void 0 : questionElement.querySelector(".subject_type span")) == null ? void 0 : _a.textContent) == null ? void 0 : _b.slice(1, 4)) || ""; const zhsTypeMapping = { "单选": "单选题", "多选": "多选题", "填空": "填空题", "判断": "判断题", "简答": "简答题", "名词": "名词解释", "论述": "论述题", "计算": "计算题" }; const fullQuestionType = zhsTypeMapping[questionTypeText] || questionTypeText; const numericType = this.typeMap.get(fullQuestionType) || "999"; const [optionsObject, optionTexts] = this.extractOptions(questionElement == null ? void 0 : questionElement.querySelectorAll(SELECTORS.ZHS_OPTION), ".node_detail"); this.questions.push({ element: questionElement, type: numericType, title: questionTitle, optionsText: optionTexts, options: optionsObject, answer: [], workType: "zhs", refer: this._window.location.href }); }); } } const hookError = () => { console.log("hookError"); const oldset = _unsafeWindow.setInterval; const oldout = _unsafeWindow.setTimeout; _unsafeWindow.setInterval = function(...args) { const err = new Error(); if (err.stack && err.stack.indexOf("checkoutNotTrustScript") !== -1) { return -1; } return oldset.call(this, ...args); }; _unsafeWindow.setTimeout = function(...args) { const err = new Error(); if (err.stack && err.stack.indexOf("checkoutNotTrustScript") !== -1) { return -1; } return oldout.call(this, ...args); }; }; class XMLHttpRequestInterceptor { constructor(urlList, callback) { __publicField(this, "xhr"); __publicField(this, "originalOpen"); __publicField(this, "originalSend"); __publicField(this, "callback"); this.xhr = new XMLHttpRequest(); this.originalOpen = this.xhr.open; this.originalSend = this.xhr.send; this.callback = callback; this.intercept(urlList); } intercept(urlList) { const self = this; XMLHttpRequest.prototype.open = function(method, url2) { self.originalOpen.apply(this, [method, url2]); const shouldIntercept = urlList.some((urlItem) => url2.includes(urlItem)); if (shouldIntercept) { self.callback(this.responseText); } }; } } const useZhsAnswerLogic = async () => { hookError(); const logStore = useLogStore(); useConfigStore(); logStore.addLog(`进入答题页面,开始准备答题`, "primary"); logStore.addLog(`正在解析题目, 请等待5s`, "warning"); new XMLHttpRequestInterceptor(["gateway/t/v1/answer/hasAnswer"], async () => { try{ await quickRandomDelay(); _unsafeWindow.document.getSelection = function() { return { removeAllRanges: function() { } }; }; _unsafeWindow.document.onselectstart = true; _unsafeWindow.document.oncontextmenu = true; _unsafeWindow.document.oncut = true; _unsafeWindow.document.oncopy = true; _unsafeWindow.document.onpaste = true; await Promise.race([ new ZhsQuestionHandler().init(), new Promise((_, reject) => setTimeout(() => reject(new Error("解析超时")), 30000)) ]); }catch(e){ logStore.addLog(`解析题目异常: ${e.message}`, "danger"); } return true; }); }; const _sfc_main$3 = vue.defineComponent({ __name: "Index", emits: ["customEvent"], setup(__props, { emit: __emit }) { var _a; const cardWidth = vue.ref("100%"); const isShow = vue.ref(false); (_a = document.querySelector("li>a.experience:not([onclick])")) == null ? void 0 : _a.click(); const configStore = useConfigStore(); const logStore = useLogStore(); const questionStore = useQuestionStore(); const url2 = window.location.href; logStore.addLog("用户悉知:使用脚本即为完全同意用户协议", "success"); logStore.addLog("脚本加载成功,正在解析网页", "primary"); logStore.addLog("请不要多个脚本同时使用,会有脚本冲突问题", "warning"); logStore.addLog("如果脚本出现异常,请用谷歌、火狐等浏览器", "warning"); // === 版本切换逻辑(参考jinmu.js versionRedirect) === // 旧版学习通URL自动切换到新版(mooc2=1) const isOldVersion = url2.includes("mooc2=0") || url2.includes("mooc-ans/"); if (isOldVersion) { const newUrl = new URL(url2); if (url2.includes("mooc-ans/mycourse/studentstudy")) { newUrl.pathname = "/mycourse/studentstudy"; } newUrl.searchParams.set("mooc2", "1"); newUrl.searchParams.set("newMooc", "true"); logStore.addLog("检测到旧版学习通URL,正在切换到新版...", "warning"); window.location.replace(newUrl.toString()); return; } // === 考试重定向逻辑(参考jinmu.js examRedirect) === // 新版考试页面自动跳转到整卷预览 if (url2.includes("exam-ans/exam/test/reVersionTestStartNew") || url2.includes("mooc-ans/exam/test/reVersionTestStartNew")) { logStore.addLog("检测到新版考试页面,等待跳转到整卷预览...", "info"); // 等待页面加载后自动调用topreview setTimeout(() => { if (typeof _unsafeWindow?.topreview === 'function') { _unsafeWindow.topreview(); } }, 3000); } const urlLogicPairs = [ { keyword: "/mycourse/studentstudy", logic: useCxChapterLogic }, { keyword: "/mooc2/work/dowork", logic: useCxWorkLogic }, { keyword: "/mooc2/exam/preview", logic: useCxExamLogic }, { keyword: "/exam-ans/", logic: useCxExamLogic }, { keyword: "exam/test", logic: useCxExamLogic }, { keyword: "/work/index", logic: useCxWorkLogic }, { keyword: "/work/doHomeWorkNew", logic: useCxWorkLogic }, { keyword: "/work/doTest", logic: useCxWorkLogic }, { keyword: "/work/calcAnswer", logic: useCxWorkLogic }, { keyword: "/work/getAllWork", logic: useCxWorkLogic }, { keyword: "/knowledge/start", logic: useCxChapterLogic }, { keyword: "/ananas/modules/video/", logic: useCxChapterLogic }, { keyword: "/ananas/modules/work/", logic: useCxWorkLogic }, { keyword: "/ztnodedetailcontroller/visitnodedetail", logic: useCxChapterLogic }, { keyword: "mycourse/stu?courseid", logic: () => { logStore.addLog("该页面无任务,请进入章节或答题页面使用", "danger"); } }, { keyword: "/stuExamWeb.html", logic: useZhsAnswerLogic }, { keyword: "answerQuestion2", logic: useStuActiveLogic } ]; const executeLogicByUrl = (url22) => { for (const { keyword, logic } of urlLogicPairs) { if (url22.includes(keyword)) { logic(); isShow.value = true; return; } } isShow.value = false; }; executeLogicByUrl(url2); const emit = __emit; emit("customEvent", isShow.value); const tabs = [ { label: "🏡主页信息", id: "main-log", component: ScriptHome, props: { "log-list": logStore.logList, "server-config": CURRENT_SERVER_CONFIG } }, { label: "📝答题记录", id: "question-record", component: QuestionTable, props: { "question-list": questionStore.questionList } }, { label: "⚙️脚本配置", id: "config-panel", component: ScriptSetting, props: { "global-config": configStore } }, { label: "📖教程解说", id: "tutorial-panel", component: TutorialPanel }, { label: "💬作者的话", id: "author-words", component: AuthorWords }, { label: "🎁授权与兑换", id: "referral-panel", component: ReferralPanel } ]; const activeTab = vue.ref("main-log"); const switchTab = (tabId) => { activeTab.value = tabId; }; return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", { style: vue.normalizeStyle({ width: cardWidth.value }), class: "card_content" }, [ vue.createElementVNode("div", { class: "config-tabs-container" }, [ (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(tabs, (tab) => { const isUnverified = tab.id === "config-panel" && !configStore.tokenVerified; const isActive = activeTab.value === tab.id; // 脚本配置和授权与兑换使用醒目渐变色 const isHighlighted = tab.id === "config-panel" || tab.id === "referral-panel"; const isConfigTab = tab.id === "config-panel"; const isReferralTab = tab.id === "referral-panel"; return vue.createElementVNode("button", { key: tab.id, class: vue.normalizeClass(["config-tab", { active: isActive }]), style: isHighlighted && !isActive ? { "background": isConfigTab ? "linear-gradient(135deg, #f97316, #ef4444)" : "linear-gradient(135deg, #f59e0b, #f97316)", "border": "none", "color": "#ffffff", "font-weight": "bold", "box-shadow": "0 2px 8px rgba(249, 115, 22, 0.4)", "text-shadow": "0 1px 2px rgba(0,0,0,0.2)" } : (isUnverified && !isActive ? { "background": "var(--jb-primary-10)", "border-color": "var(--jb-primary)", "color": "var(--jb-primary)" } : {}), onClick: () => switchTab(tab.id) }, vue.toDisplayString(tab.label), 13, ["onClick", "class", "style"]); }), 64)) ]), (vue.openBlock(), vue.createElementBlock(vue.Fragment, null, vue.renderList(tabs, (tab) => { return vue.withDirectives(vue.createElementVNode("div", { key: tab.id, class: "config-panel" }, [ tab.component ? (vue.openBlock(), vue.createBlock(vue.resolveDynamicComponent(tab.component), vue.mergeProps({ key: 0, ref_for: true }, tab.props), null, 16)) : vue.createCommentVNode("", true) ], 512), [ [vue.vShow, activeTab.value === tab.id] ]); }), 128)) ], 4); }; } }); const _sfc_main$2 = vue.defineComponent({ __name: "ZoomButtons", emits: ["toggleZoom", "closePanel"], setup(__props, { emit: __emit }) { const emit = __emit; const configStore = useConfigStore(); const toggleZoom = () => { const newValue = !configStore.isMinus; emit("toggleZoom", newValue); }; const closePanel = () => { emit("closePanel", false); }; return (_ctx, _cache) => { const _component_el_icon = vue.resolveComponent("el-icon"); return vue.openBlock(), vue.createElementBlock("div", { onMousedown: _cache[1] || (_cache[1] = vue.withModifiers(() => { }, ["stop"])) }, [ vue.createVNode(_component_el_icon, { onClick: _cache[0] || (_cache[0] = () => toggleZoom()), size: "small", style: { "cursor": "pointer", "margin-right": "8px" } }, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(configStore.isMinus ? full_screen_default : minus_default)) ]), _: 1 }), vue.createVNode(_component_el_icon, { onClick: _cache[2] || (_cache[2] = () => closePanel()), size: "small", style: { "cursor": "pointer" } }, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(document_remove_default)) ]), _: 1 }) ], 32); }; } }); const _hoisted_1 = { class: "overlay" }; const _hoisted_2 = { class: "title" }; const _hoisted_3 = { class: "minus" }; const _sfc_main$1 = vue.defineComponent({ __name: "layout", setup(__props) { const isShow = vue.ref(false); const configStore = useConfigStore(); vue.watch(configStore, (newVal) => { _GM_setValue("config", JSON.stringify(newVal)); }, { deep: true }); const isDragging = vue.ref(false); const offsetX = vue.ref(0); const offsetY = vue.ref(0); const moveStyle = vue.computed(() => { return { left: configStore.position.x, top: configStore.position.y, height: configStore.isMinus ? "auto" : "560px", maxHeight: configStore.isMinus ? "none" : "560px", width: configStore.isMinus ? "280px" : "720px", maxWidth: configStore.isMinus ? "280px" : "720px" }; }); const startDrag = (event) => { isDragging.value = true; offsetX.value = event.clientX - event.target.getBoundingClientRect().left; offsetY.value = event.clientY - event.target.getBoundingClientRect().top; document.addEventListener("mousemove", drag); document.addEventListener("mouseup", endDrag); }; const drag = (event) => { if (!isDragging.value) return; const x = event.clientX - offsetX.value; const y = event.clientY - offsetY.value; configStore.position.x = `${x - 11}px`; configStore.position.y = `${y - 11}px`; if (x < 0) { configStore.position.x = "0px"; } if (y < 0) { configStore.position.y = "0px"; } if (x > window.innerWidth - 720) { configStore.position.x = `${window.innerWidth - 720}px`; } if (y > window.innerHeight - 560) { configStore.position.y = `${window.innerHeight - 560}px`; } }; const endDrag = () => { isDragging.value = false; document.removeEventListener("mousemove", drag); document.removeEventListener("mouseup", endDrag); }; const startDragTouch = (event) => { if (event.touches.length === 1) { isDragging.value = true; const touch = event.touches[0]; offsetX.value = touch.clientX - event.target.getBoundingClientRect().left; offsetY.value = touch.clientY - event.target.getBoundingClientRect().top; document.addEventListener("touchmove", dragTouch, { passive: false }); document.addEventListener("touchend", endDragTouch); } }; const dragTouch = (event) => { if (!isDragging.value || event.touches.length !== 1) return; event.preventDefault(); const touch = event.touches[0]; const x = touch.clientX - offsetX.value; const y = touch.clientY - offsetY.value; configStore.position.x = `${x - 11}px`; configStore.position.y = `${y - 11}px`; if (x < 0) { configStore.position.x = "0px"; } if (y < 0) { configStore.position.y = "0px"; } if (x > window.innerWidth - 720) { configStore.position.x = `${window.innerWidth - 720}px`; } if (y > window.innerHeight - 560) { configStore.position.y = `${window.innerHeight - 560}px`; } }; const endDragTouch = () => { isDragging.value = false; document.removeEventListener("touchmove", dragTouch); document.removeEventListener("touchend", endDragTouch); }; return (_ctx, _cache) => { const _component_el_icon = vue.resolveComponent("el-icon"); const _component_el_tooltip = vue.resolveComponent("el-tooltip"); const _component_el_tag = vue.resolveComponent("el-tag"); const _component_el_text = vue.resolveComponent("el-text"); const _component_el_divider = vue.resolveComponent("el-divider"); const _component_el_card = vue.resolveComponent("el-card"); return vue.withDirectives((vue.openBlock(), vue.createElementBlock("div", { style: vue.normalizeStyle(moveStyle.value), class: "main-page" }, [ vue.withDirectives(vue.createElementVNode("div", _hoisted_1, null, 512), [ [vue.vShow, isDragging.value] ]), vue.createVNode(_component_el_card, { style: { "border": "0" }, "close-on-click-modal": false, "lock-scroll": false, modal: false, "show-close": false, "modal-class": "modal" }, { header: vue.withCtx(() => [ vue.createElementVNode("div", { class: "card-header", onMousedown: startDrag, onTouchstart: startDragTouch }, [ vue.createElementVNode("div", _hoisted_2, [ vue.createElementVNode("span", null, vue.toDisplayString(vue.unref(configStore).isMinus ? `超星网课助手 v${vue.unref(configStore).version}` : vue.unref(configStore).platformParams?.[vue.unref(configStore).platformName]?.name || "超星网课助手"), 1), vue.createVNode(_component_el_tooltip, { teleported: "", effect: "dark", placement: "top-start", content: "注意事项:
请尽量使用新版,不要使用旧版。
快捷键: Ctrl+O 显示/隐藏面板
", "raw-content": "" }, { default: vue.withCtx(() => [ vue.createVNode(_component_el_icon, { style: { "margin-left": "5px" }, size: "small" }, { default: vue.withCtx(() => [ vue.createVNode(vue.unref(warning_default)) ]), _: 1 }) ]), _: 1 }), vue.createVNode(_component_el_tag, { size: "small", type: vue.unref(configStore).platformName === "cx" ? "primary" : "success", style: { "margin-left": "10px" } }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(vue.unref(configStore).platformName === "cx" ? "学习通" : vue.unref(configStore).platformName === "zhs" ? "智慧树" : "未知"), 1) ]), _: 1 }, 8, ["type"]) ]), vue.createVNode(_sfc_main$2, { onToggleZoom: _cache[0] || (_cache[0] = ($event) => vue.unref(configStore).isMinus = $event), onClosePanel: _cache[3] || (_cache[3] = ($event) => isShow.value = $event) }) ], 32) ]), default: vue.withCtx(() => [ vue.withDirectives(vue.createVNode(_sfc_main$3, { onCustomEvent: _cache[1] || (_cache[1] = (newValue) => isShow.value = newValue) }, null, 512), [ [vue.vShow, !vue.unref(configStore).isMinus] ]), vue.withDirectives(vue.createElementVNode("div", _hoisted_3, [ vue.createVNode(_component_el_text, { type: "info", size: "small" }, { default: vue.withCtx(() => _cache[2] || (_cache[2] = [ vue.createTextVNode("已最小化,点击上方按钮恢复") ])), _: 1 }), vue.createVNode(_component_el_divider, { "border-style": "dashed", style: { "margin": "0" } }) ], 512), [ [vue.vShow, vue.unref(configStore).isMinus] ]) ]), _: 1 }) ], 4)), [ [vue.vShow, isShow.value] ]); }; } }); const _sfc_main = vue.defineComponent({ __name: "App", setup(__props) { const configStore = useConfigStore(); const url2 = window.location.href; if (url2.includes("chaoxing") || url2.includes("xuexitong")) configStore.platformName = "cx"; else if (url2.includes("zhihuishu")) configStore.platformName = "zhs"; return (_ctx, _cache) => { return vue.openBlock(), vue.createBlock(_sfc_main$1); }; } }); const cssLoader = (e) => { const t = GM_getResourceText(e); return GM_addStyle(t), t; }; cssLoader("ElementPlus"); // 合并可用的样式片段,确保侧边栏与强制导航样式也注入到 shadowRoot const layoutCss = (function(){ const parts = []; if (typeof JINMU_LAYOUT_CSS !== 'undefined' && JINMU_LAYOUT_CSS) parts.push(JINMU_LAYOUT_CSS); if (typeof LAYOUT_CSS !== 'undefined' && LAYOUT_CSS) parts.push(LAYOUT_CSS); if (typeof SIDEBAR_LAYOUT_CSS !== 'undefined' && SIDEBAR_LAYOUT_CSS) parts.push(SIDEBAR_LAYOUT_CSS); if (typeof FORCE_NAV_CSS !== 'undefined' && FORCE_NAV_CSS) parts.push(FORCE_NAV_CSS); if (parts.length) return parts.join('\n'); return (typeof LAYOUT_CSS !== 'undefined') ? LAYOUT_CSS : ''; })(); const hookWebpack = () => { let originCall = _unsafeWindow.Function.prototype.call; _unsafeWindow.Function.prototype.call = function(...args) { var _a, _b; const result = originCall.apply(this, args); if (((_b = (_a = args[0]) == null ? void 0 : _a.a) == null ? void 0 : _b.version) === "2.5.0") { const install = args[1].exports.a.install; args[1].exports.a.install = function(...installArgs) { installArgs[0].mixin({ mounted: function() { this.$el["__Ivue__"] = this; } }); return install.apply(this, installArgs); }; return result; } return result; }; }; const url = _unsafeWindow.location.href; if (window.self === window.top) { window.addEventListener('message', (event) => { if (event.data?.source === 'chaoxing-helper-iframe' && event.data.action === 'closePanel') { const root = document.getElementById('chaoxing-helper-root'); if (root) root.style.display = 'none'; } }); // 键盘快捷键 Ctrl+O 显示/隐藏面板 window.addEventListener('keydown', (e) => { if (e.ctrlKey && e.key === 'o') { e.preventDefault(); e.stopPropagation(); const root = document.getElementById('chaoxing-helper-root'); if (root) { root.style.display = root.style.display === 'none' ? 'block' : 'none'; } } }, { capture: true }); } if (url.includes("zhihuishu.com")) { hookWebpack(); hookError(); } // 由于使用了 @run-at document-idle,DOM应该已经就绪 // 直接初始化,不再需要定时器等待 // 依赖库验证 if (!vue || typeof vue.createApp !== 'function') { console.error('[学习通助手] Vue库未加载或不可用,脚本无法运行'); return; } if (!pinia || typeof pinia.createPinia !== 'function') { console.error('[学习通助手] Pinia库未加载或不可用,脚本无法运行'); return; } if (!ElementPlus) { console.warn('[学习通助手] ElementPlus库未加载,部分UI可能异常'); } function initApp() { try { console.log('[学习通助手] 开始初始化Vue应用'); // 缓存清理已集成到 LocalDB 自动管理 // 创建 Shadow DOM 挂载点 const mountElement = createShadowDOM(); if (!mountElement) { console.error('[学习通助手] Shadow DOM创建失败'); return; } // 创建 Vue 应用 const app = vue.createApp(_sfc_main); console.log('[学习通助手] Vue应用创建成功'); const pinia$1 = pinia.createPinia(); app.use(pinia$1); console.log('[学习通助手] Pinia已安装'); if (ElementPlus) { app.use(ElementPlus); console.log('[学习通助手] ElementPlus已安装'); } console.log('[学习通助手] 准备挂载应用到DOM'); app.mount(mountElement); console.log('[学习通助手] 应用挂载完成'); // 延迟执行后续操作 setTimeout(function() { try { if (typeof ClientLicense !== 'undefined' && ClientLicense.identifyUser) { ClientLicense.identifyUser().then(result => { if (result && result.ok) { setIdentifiedUserId(result.user_id); console.log('[学习通助手] 用户识别完成:', result.match_type, result.user_id); if (typeof _GM_xmlhttpRequest === 'function') { reportUserId(); } } }).catch(e => { console.warn('[学习通助手] 用户识别异常:', e); if (typeof _GM_xmlhttpRequest === 'function') { reportUserId(); } }); } else if (typeof _GM_xmlhttpRequest === 'function') { reportUserId(); } } catch (e) { console.warn('[学习通助手] 用户识别失败:', e.message); } try { if (typeof _GM_xmlhttpRequest === 'function') { checkServerNotice(); } } catch (e) { console.warn('[学习通助手] 服务器通知检查失败:', e.message); } try { const configStore = useConfigStore(); if (configStore.platformParams && configStore.platformParams.cx && configStore.platformParams.cx.parts && configStore.platformParams.cx.parts[0] && configStore.platformParams.cx.parts[0].params && configStore.platformParams.cx.parts[0].params[0] && configStore.platformParams.cx.parts[0].params[0].value) { showOverlayAndBanner(); } } catch (e) { console.warn('[学习通助手] 配置加载失败:', e.message); } try { const rootEl = document.getElementById('chaoxing-helper-root'); if (rootEl && rootEl.shadowRoot) { attachExternalLinkInterceptors(rootEl.shadowRoot); } } catch (e) { console.warn('[学习通助手] 外部链接拦截器安装失败:', e.message); } }, 500); // 自动检测答题页面并启动答题 setTimeout(function(){ try{ // 检测当前页面是否有题目 let qEls = Array.from(document.querySelectorAll(SELECTORS.CX_QUESTION_ZJ || '.TiMu')); if(!qEls.length) qEls = Array.from(document.querySelectorAll(SELECTORS.CX_QUESTION_ZY_KS || '.questionLi')); if(!qEls.length) qEls = Array.from(document.querySelectorAll(SELECTORS.ZHS_QUESTION || '.examPaper_subject')); if(qEls.length > 0){ console.log(`[学习通助手] 检测到答题页面,共${qEls.length}题,自动启动答题`); // 自动启动答题循环 if(typeof startAutoLoop === 'function'){ startAutoLoop(); } } }catch(e){ console.warn('[学习通助手] 自动检测答题页面失败:', e); } }, 2000); } catch (error) { console.error('[学习通助手] 脚本初始化失败:', error.message); console.error('[学习通助手] 错误堆栈:', error.stack); } } // DOM 就绪后初始化 function startWhenReady() { if (!document.body) { setTimeout(startWhenReady, 200); return; } // 初始化离线同步机制 if(typeof ClientLicense !== 'undefined' && typeof ClientLicense.initOfflineSync === 'function'){ ClientLicense.initOfflineSync(); } initApp(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startWhenReady); } else { startWhenReady(); } function createShadowDOM() { try { if (!document.body) { console.error('[学习通助手] document.body 不存在,无法创建 Shadow DOM'); return null; } const shadow_root = document.createElement("div"); shadow_root.id = "chaoxing-helper-root"; const app2 = document.createElement("div"); document.body.append(shadow_root); const shadow = shadow_root.attachShadow({ mode: "open" }); shadow.appendChild(app2); const scriptHandler = (_GM_info && _GM_info.scriptHandler) ? _GM_info.scriptHandler : ''; const isScriptCat = scriptHandler === 'ScriptCat' || scriptHandler.includes('ScriptCat'); console.log('[学习通助手] 脚本管理器: ' + (scriptHandler || '未知') + ', 样式模式: ' + (isScriptCat ? 'adoptedStyleSheets' : 'style标签')); const eleStyle = (_GM_getResourceText && _GM_getResourceText("ElementPlusStyle")) || ""; if (isScriptCat && typeof CSSStyleSheet !== 'undefined') { try { const sheet = new CSSStyleSheet(); const sheet1 = new CSSStyleSheet(); if (eleStyle) sheet.replaceSync(eleStyle); if (layoutCss) sheet1.replaceSync(layoutCss); shadow.adoptedStyleSheets = [sheet, sheet1]; console.log('[学习通助手] adoptedStyleSheets样式注入成功'); } catch (cssError) { console.warn('[学习通助手] adoptedStyleSheets不支持,降级为style标签:', cssError.message); injectStyleTags(shadow, eleStyle, layoutCss); } } else { injectStyleTags(shadow, eleStyle, layoutCss); } return app2; } catch (error) { console.error('[学习通助手] Shadow DOM创建失败:', error.message); return null; } } function injectStyleTags(shadow, eleStyle, layoutCss) { try { if (eleStyle) { const styleEl = document.createElement("style"); styleEl.textContent = eleStyle; shadow.appendChild(styleEl); console.log('[学习通助手] ElementPlus样式注入成功,长度:', eleStyle.length); } if (layoutCss) { const styleEl2 = document.createElement("style"); styleEl2.textContent = layoutCss; shadow.appendChild(styleEl2); console.log('[学习通助手] 布局样式注入成功,长度:', layoutCss.length); } } catch (error) { console.error('[学习通助手] 样式注入失败:', error.message); } } })();