// ==UserScript==
// @name |🥇PP网课小助手|飘飘|
// @namespace 飘飘
// @license MIT
// @version 4.1.8
// @author PIAOPIAO
// @description 🏆🏆【超星学习通|知到智慧树|雨课堂】【Claude ChatGPT Gemini Deepseek 等7款模型接入】【多种自动答题【学习通全题型答题】免费题库】【手机平板支持】【拥有题库 AI双重校验】。
// @icon https://cdn.jsdmirror.com/gh/piaopiaosir/pp-course-assistant@main/icon.webp
// @match *://*.chaoxing.com/*
// @match *://*.xuexitong.com/*
// @match *://*.xueyinonline.com/*
// @match *://*.edu.cn/*
// @match *://*.nbdlib.cn/*
// @match *://*.hnsyu.net/*
// @match *://*.gdhkmooc.com/*
// @match *://*.sslibrary.com/*
// @match *://onlineexamh5new.zhihuishu.com/*
// @match *://*.zhihuishu.com/*
// @match *://*.hike-teaching-center.polymas.com/*
// @match *://*.yuketang.cn/*
// @match *://*.gdufemooc.cn/*
// @require https://lib.baomitu.com/vue/3.5.0/vue.global.prod.js
// @require https://lib.baomitu.com/vue-demi/0.14.7/index.iife.js
// @require data:application/javascript,window.Vue%3DVue%3B
// @require https://lib.baomitu.com/element-plus/2.8.2/index.full.min.js
// @require https://lib.baomitu.com/pinia/2.3.1/pinia.iife.min.js
// @require https://lib.baomitu.com/rxjs/7.8.2/rxjs.umd.min.js
// @require https://lib.baomitu.com/blueimp-md5/2.19.0/js/md5.min.js
// @resource ElementPlusStyle https://lib.baomitu.com/element-plus/2.8.2/index.min.css
// @resource fontDecryptLib https://cdn.jsdmirror.com/gh/piaopiaosir/pp-font-decrypt@v1.0.3/font-decrypt.js
// @connect *
// @grant GM_addStyle
// @grant GM_getResourceText
// @grant GM_getValue
// @grant GM_info
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-start
// @tag ChatGPT|Gemini|Deepseek多模型支持
// @tag 免费题库
// @tag 全题型支持
// ==/UserScript==
const _PP_SERVER_URLS = [
"http://122.152.249.109:3000",
"http://152.136.30.238:3000"
];
const _PP_REMOTE_SCRIPTS_PRELOAD_PROMISE = (() => new Promise((resolve) => {
const server = _PP_SERVER_URLS[Math.floor(Math.random() * _PP_SERVER_URLS.length)];
const finish = (() => {
let done = false;
return () => {
if (!done) {
done = true;
resolve();
}
};
})();
if (typeof GM_xmlhttpRequest !== 'function') {
finish();
return;
}
GM_xmlhttpRequest({
method: 'GET',
url: `${server}/remote-scripts`,
timeout: 5000,
onload: (res) => {
try {
const json = JSON.parse(res.responseText);
const patches = json?.data?.hasUpdate && Array.isArray(json.data.patches) ? json.data.patches : [];
const downloads = patches
.map(patch => patch?.downloadUrl)
.filter(Boolean)
.map(downloadUrl => new Promise((done) => {
GM_xmlhttpRequest({
method: 'GET',
url: downloadUrl,
timeout: 5000,
onload: () => done(),
onerror: () => done(),
ontimeout: () => done()
});
}));
if (downloads.length > 0) {
Promise.allSettled(downloads).then(finish);
return;
}
} catch (e) {}
finish();
},
onerror: finish,
ontimeout: finish
});
}))();
const LAYOUT_CSS = `@keyframes ppFade{from{opacity:0;transform:translateY(-3px);}to{opacity:1;transform:none;}}
@keyframes ppPulse{0%,100%{opacity:1;}50%{opacity:.45;}}
@keyframes ppSpin{to{transform:rotate(360deg);}}
@keyframes ppfxRise{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}
@keyframes ppfxPane{from{opacity:0;transform:translateY(var(--ppfx-pane-shift,6px));}to{opacity:1;transform:none;}}
@keyframes ppfxRiseRight{from{opacity:0;transform:translateX(8px);}to{opacity:1;transform:none;}}
@keyframes ppfxPop{from{opacity:0;transform:scale(.94);}to{opacity:1;transform:none;}}
@keyframes ppfxMenuIn{from{opacity:0;transform:translateY(-4px) scale(.98);}to{opacity:1;transform:none;}}
@keyframes ppfxMenuUp{from{opacity:0;transform:translateY(4px) scale(.98);}to{opacity:1;transform:none;}}
@keyframes ppfxOrbIn{from{opacity:0;transform:scale(.72);}to{opacity:1;transform:scale(1);}}
@keyframes ppfxContentIn{from{opacity:0;transform:translateY(4px) scale(.99);}to{opacity:1;transform:none;}}
@keyframes ppfxBreathe{0%,100%{transform:scale(1);}50%{transform:scale(1.055);}}
@keyframes ppfxNudge{0%,100%{transform:translateX(0);}50%{transform:translateX(-2px);}}
@keyframes ppfxRing{0%{opacity:.55;transform:scale(.9);}100%{opacity:0;transform:scale(1.4);}}
@media (prefers-reduced-motion:reduce){
.main-page,.main-page *,.pp-coach,.pp-coach *{animation-duration:.001ms!important;animation-delay:0ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;}
}
.main-page{
--pp-brand:#0052d9;--pp-brand-hover:#0047bd;--pp-brand-weak:#eaf1ff;--pp-brand-ring:rgba(0,82,217,.14);
--pp-text:#1b2433;--pp-text-2:#5d6b82;--pp-text-3:#93a0b4;--pp-text-4:#b3bdcb;
--pp-line:#e8ecf2;--pp-line-soft:#f1f4f8;--pp-surface:#f7f9fc;--pp-surface-2:#fbfcfe;
--pp-success:#0f9d58;--pp-success-bg:#f0fbf5;--pp-success-line:#d3f2e1;
--pp-warning:#b45309;--pp-warning-bg:#fffaeb;--pp-warning-line:#fdecc8;
--pp-danger:#d92d20;--pp-danger-bg:#fef3f2;--pp-danger-line:#fecdca;
--ppfx-ease:cubic-bezier(.22,1,.36,1);--ppfx-ease-soft:cubic-bezier(.4,0,.2,1);--ppfx-ease-back:cubic-bezier(.34,1.56,.64,1);
--ppfx-t-fast:110ms;--ppfx-t-base:180ms;--ppfx-t-slow:260ms;
z-index:100003;position:fixed;background:#fff;color:var(--pp-text);
max-width:calc(100vw - 8px);max-height:calc(100vh - 72px);
border:1px solid rgba(15,23,42,.07);border-radius:16px;
box-shadow:0 18px 50px -12px rgba(15,23,42,.30),0 2px 6px rgba(15,23,42,.06);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;
font-size:13px;line-height:1.45;display:flex;flex-direction:column;overflow:hidden;
transition:left var(--ppfx-t-base) var(--ppfx-ease),top var(--ppfx-t-base) var(--ppfx-ease),width var(--ppfx-t-base) var(--ppfx-ease),height var(--ppfx-t-base) var(--ppfx-ease),border-radius var(--ppfx-t-base) var(--ppfx-ease);
animation:ppFade .2s var(--ppfx-ease) backwards;transform:translateZ(0);
}
.main-page.ppfx-morphing{will-change:left,top,width,height;}
.main-page,.main-page *{box-sizing:border-box;}
.main-page button{font-family:inherit;}
.main-page.pp-dock{border-radius:16px 0 0 16px;max-height:none;}
.main-page.pp-min{border-radius:50%;box-shadow:0 10px 26px -8px rgba(15,23,42,.30),0 1px 3px rgba(15,23,42,.08);}
.main-page.pp-handle-mode{border-radius:12px 0 0 12px;box-shadow:0 6px 18px -6px rgba(15,23,42,.32),0 0 0 1px rgba(15,23,42,.06);}
.main-page.pp-dragging{transition:none;cursor:grabbing!important;user-select:none;}
.main-page.pp-dragging *{cursor:grabbing!important;}
.main-page .pp-head,.main-page .pp-body{transition:opacity var(--ppfx-t-fast) var(--ppfx-ease-soft),transform var(--ppfx-t-fast) var(--ppfx-ease-soft);}
.main-page.ppfx-closing .pp-head,.main-page.ppfx-closing .pp-body{opacity:0;transform:scale(.965);pointer-events:none;transition-duration:90ms;}
.main-page.ppfx-opening .pp-head,.main-page.ppfx-opening .pp-body{animation:ppfxContentIn var(--ppfx-t-base) var(--ppfx-ease) 60ms both;}
.main-page.ppfx-settling .pp-orb,.main-page.ppfx-settling .pp-handle{animation:ppfxOrbIn var(--ppfx-t-base) var(--ppfx-ease-back) 100ms both;}
.main-page.ppfx-settling .pp-orb{position:absolute;left:50%;top:50%;margin:-26px 0 0 -26px;}
.main-page.ppfx-settling .pp-handle{position:absolute;inset:0;}
.main-page.ppfx-undocking{max-height:none!important;}
.main-page ::-webkit-scrollbar{width:6px;height:6px;}
.main-page ::-webkit-scrollbar-track{background:transparent;}
.main-page ::-webkit-scrollbar-thumb{background:#d8dee8;border-radius:99px;}
.main-page ::-webkit-scrollbar-thumb:hover{background:#c3ccd9;}
.pp-head{flex:0 0 54px;height:54px;display:flex;align-items:center;gap:8px;padding:0 8px 0 12px;border-bottom:1px solid var(--pp-line);background:#fff;cursor:grab;touch-action:none;}
.pp-brand{flex:1;min-width:0;display:flex;align-items:center;gap:9px;}
.pp-logo{position:relative;flex:none;display:flex;align-items:center;justify-content:center;border-radius:9px;overflow:hidden;background:#fff;box-shadow:0 0 0 1px #e6ebf2,0 2px 6px -2px rgba(15,23,42,.22);transition:transform .2s var(--ppfx-ease);}
.pp-logo-fallback{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#fff;color:var(--pp-brand);font-size:var(--pp-logo-font,15px);font-weight:800;letter-spacing:-.02em;}
.pp-logo-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;background:#fff;}
.pp-titles{min-width:0;display:flex;flex-direction:column;gap:1px;}
.pp-title{font-size:13px;font-weight:600;letter-spacing:.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--pp-text);}
.pp-sub{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--pp-text-3);white-space:nowrap;overflow:hidden;}
.pp-sub b{font-weight:600;color:var(--pp-brand);}
.pp-sub i{font-style:normal;color:var(--pp-text-4);}
.pp-update-badge{flex:none;display:inline-flex;align-items:center;gap:3px;height:16px;padding:0 6px 0 5px;border:1px solid #dbe8ff;border-radius:99px;background:#eef4ff;color:#2563eb;font-size:8px;font-weight:600;cursor:pointer;white-space:nowrap;transition:background .15s,border-color .15s,box-shadow .15s;animation:ppUpdBadgeIn .32s cubic-bezier(.22,.8,.32,1) both;}
.pp-update-badge::before{content:"";width:4px;height:4px;border-radius:50%;background:#2563eb;box-shadow:0 0 0 2px rgba(37,99,235,.12);animation:ppUpdBadgePulse 1.8s ease-in-out infinite;}
.pp-update-badge:hover{background:#e4eeff;border-color:#c9dcff;box-shadow:0 2px 6px -2px rgba(37,99,235,.25);}
@keyframes ppUpdBadgePulse{0%,100%{opacity:1;transform:scale(1);}50%{opacity:.5;transform:scale(.8);}}
@keyframes ppUpdBadgeIn{0%{opacity:0;transform:translateY(-2px) scale(.72);}55%{opacity:1;transform:translateY(0) scale(1.08);}100%{opacity:1;transform:translateY(0) scale(1);}}
.pp-head-right{flex:none;display:flex;align-items:center;gap:2px;}
.pp-live{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 6px;border-radius:7px;font-size:10px;color:var(--pp-text-3);white-space:nowrap;}
.pp-live.on{background:var(--pp-success-bg);color:#0b7a46;}
.pp-dot{width:6px;height:6px;border-radius:50%;background:#c3ccd9;flex:none;}
.pp-dot.on{background:#10b981;box-shadow:0 0 0 3px rgba(16,185,129,.14);animation:ppPulse 2.2s ease-in-out infinite;}
.pp-iconbtn{width:28px;height:28px;padding:0;border:0;border-radius:8px;background:transparent;color:#7d899d;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s,color .15s;flex:none;}
.pp-iconbtn:hover{background:#f1f4f9;color:#2b3648;}
.pp-iconbtn.pp-on{background:var(--pp-brand-weak);color:var(--pp-brand);}
.pp-iconbtn:disabled{opacity:.4;cursor:not-allowed;}
.pp-auto{display:inline-flex;align-items:center;justify-content:center;width:30px;height:28px;border-radius:8px;color:#9aa6b8;cursor:pointer;}
.pp-auto:hover{background:#f1f4f9;}
.pp-auto .el-switch{transform:scale(.78);transform-origin:center;}
.pp-auto .el-switch__core{--el-switch-on-color:var(--pp-brand);--el-switch-off-color:#dfe4ec;}
.pp-body{flex:1;min-height:0;display:flex;overflow:hidden;}
.pp-shell{flex:1;min-width:0;min-height:0;display:flex;}
.pp-rail{position:relative;flex:0 0 56px;width:56px;background:var(--pp-surface);border-right:1px solid var(--pp-line);display:flex;flex-direction:column;gap:2px;padding:7px 5px;overflow:hidden;}
.pp-rail-item{position:relative;flex:none;height:50px;border:0;border-radius:10px;background:transparent;color:#8592a6;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;font-size:10px;font-weight:500;letter-spacing:.02em;cursor:pointer;padding:0;transition:background .15s,color .15s,box-shadow .15s;}
.pp-rail-item:hover{background:#edf1f7;color:#3c4a60;}
.pp-rail-item.active{background:#fff;color:var(--pp-brand);box-shadow:0 1px 2px rgba(16,24,40,.06),0 0 0 1px var(--pp-line);}
.pp-rail-ink{position:absolute;left:0;top:0;z-index:1;width:3px;height:0;border-radius:0 3px 3px 0;background:var(--pp-brand);opacity:0;pointer-events:none;transform:translateY(0);}
.pp-rail-ink.ppfx-live{opacity:1;}
.pp-rail-ink.ppfx-slide{transition:transform var(--ppfx-t-base) var(--ppfx-ease),height var(--ppfx-t-base) var(--ppfx-ease),opacity var(--ppfx-t-fast) linear;}
.pp-rail-item.pp-warn::after{content:"";position:absolute;top:7px;right:7px;width:6px;height:6px;border-radius:50%;background:#f59e0b;box-shadow:0 0 0 2px var(--pp-surface);}
.pp-rail-item svg{flex:none;}
.pp-rail-spacer{flex:1;min-height:4px;}
.pp-main{position:relative;flex:1;min-width:0;height:100%;background:#fff;}
.config-panel{height:100%;overflow-y:auto;overflow-x:hidden;padding:12px;animation:ppfxPane .16s var(--ppfx-ease) both;}
.config-panel>*{min-width:0;}
.pp-statusrow{display:flex;flex-wrap:wrap;gap:5px;flex:none;}
.pp-chip{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 8px;border-radius:7px;background:var(--pp-surface);border:1px solid var(--pp-line);color:var(--pp-text-2);font-size:10.5px;font-weight:500;white-space:nowrap;}
.pp-chip.ok{background:var(--pp-success-bg);border-color:var(--pp-success-line);color:#0b7a46;}
.pp-chip.bad{background:var(--pp-danger-bg);border-color:var(--pp-danger-line);color:#b42318;}
.pp-chip.warn{background:var(--pp-warning-bg);border-color:var(--pp-warning-line);color:#92400e;}
.pp-chip svg{flex:none;opacity:.85;}
.pp-chip .pp-dot{width:5px;height:5px;}
.pp-home{height:100%;display:flex;flex-direction:column;gap:8px;min-height:0;}
.pp-progress{flex:none;border:1px solid var(--pp-line);border-radius:12px;padding:10px 11px;background:#fff;}
.pp-progress-top{display:flex;align-items:baseline;gap:10px;justify-content:space-between;}
.pp-task{min-width:0;font-size:12.5px;font-weight:600;color:var(--pp-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pp-pct{flex:none;font-size:17px;font-weight:700;color:var(--pp-brand);letter-spacing:-.02em;font-variant-numeric:tabular-nums;}
.pp-bar{height:6px;border-radius:99px;background:#edf1f7;overflow:hidden;margin:8px 0 6px;}
.pp-bar>i{display:block;width:100%;height:100%;border-radius:99px;background:linear-gradient(90deg,#4d8dff,#0052d9);transform-origin:left center;transform:scaleX(var(--ppfx-p,0));transition:transform .45s var(--ppfx-ease-soft);}
.pp-meta{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:10px;color:var(--pp-text-3);font-variant-numeric:tabular-nums;}
.pp-meta b{font-weight:600;color:var(--pp-text-2);}
.pp-idle{flex:none;display:flex;align-items:center;gap:9px;border:1px dashed #dfe5ee;border-radius:12px;padding:10px 11px;background:var(--pp-surface-2);}
.pp-idle-ico{flex:none;width:30px;height:30px;border-radius:9px;background:#fff;border:1px solid var(--pp-line);display:flex;align-items:center;justify-content:center;color:#a8b3c4;}
.pp-idle-txt{min-width:0;}
.pp-idle-txt b{display:block;font-size:11.5px;font-weight:600;color:var(--pp-text-2);}
.pp-idle-txt span{display:block;font-size:10px;color:var(--pp-text-3);margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pp-logs{flex:1;min-height:90px;display:flex;flex-direction:column;border:1px solid var(--pp-line);border-radius:12px;overflow:hidden;background:#fff;}
.pp-log-head{flex:none;height:32px;display:flex;align-items:center;gap:6px;padding:0 6px 0 11px;background:var(--pp-surface-2);border-bottom:1px solid var(--pp-line);}
.pp-log-title{font-size:10.5px;font-weight:600;color:var(--pp-text-2);letter-spacing:.04em;}
.pp-log-count{font-size:9px;color:var(--pp-text-3);background:#eef2f7;border-radius:99px;padding:1px 6px;font-variant-numeric:tabular-nums;}
.pp-log-actions{margin-left:auto;display:flex;align-items:center;gap:1px;}
.pp-log-actions .pp-iconbtn{width:24px;height:24px;border-radius:6px;}
.pp-log-list{flex:1;min-height:0;overflow-y:auto;padding:3px 0;}
.pp-log-item{display:flex;gap:7px;padding:4px 10px;font-size:11px;line-height:1.55;}
.pp-log-item:hover{background:#fafcff;}
.pp-log-item[data-new="1"]{animation:ppfxRise .16s var(--ppfx-ease) both;}
.pp-log-time{flex:none;width:42px;padding-top:1px;color:#a6b1c2;font-size:9.5px;font-variant-numeric:tabular-nums;}
.pp-log-msg{min-width:0;color:#3d4a5e;word-break:break-word;}
.pp-log-msg::before{content:"";display:inline-block;width:5px;height:5px;border-radius:50%;margin-right:5px;vertical-align:2px;background:#cbd5e1;}
.pp-log-msg a{color:var(--pp-brand);text-decoration:none;}
.pp-log-msg a:hover{text-decoration:underline;}
.pp-log-item[data-t="success"] .pp-log-msg{color:#3d4a5e;}
.pp-log-item[data-t="success"] .pp-log-msg::before{background:#10b981;}
.pp-log-item[data-t="warning"] .pp-log-msg{color:#8a5a12;}
.pp-log-item[data-t="warning"] .pp-log-msg::before{background:#f59e0b;}
.pp-log-item[data-t="danger"] .pp-log-msg{color:#b42318;}
.pp-log-item[data-t="danger"] .pp-log-msg::before{background:#ef4444;}
.pp-log-item[data-t="primary"] .pp-log-msg{color:#3d4a5e;}
.pp-log-item[data-t="primary"] .pp-log-msg::before{background:var(--pp-brand);}
.pp-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;min-height:150px;padding:22px 12px;color:var(--pp-text-3);font-size:10.5px;text-align:center;}
.pp-empty-ico{width:40px;height:40px;border-radius:12px;background:var(--pp-surface);border:1px solid var(--pp-line);display:flex;align-items:center;justify-content:center;color:#b9c3d1;animation:ppfxBreathe 3.8s var(--ppfx-ease-soft) infinite;transform-origin:50% 50%;}
.pp-empty b{font-size:11.5px;font-weight:600;color:var(--pp-text-2);}
.pp-empty span{max-width:200px;line-height:1.5;}
.pp-welfare{flex:none;border:1px solid #f4dfbe;border-radius:12px;padding:10px;background:linear-gradient(135deg,#fffaf1,#fff3df);margin-bottom:10px;}
.pp-welfare-top{display:flex;align-items:center;gap:8px;}
.pp-welfare-ico{width:28px;height:28px;border-radius:8px;background:#fff1d6;color:#c27803;display:flex;align-items:center;justify-content:center;flex:none;}
.pp-welfare-copy{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1;}
.pp-welfare-copy b{font-size:12px;color:#7c4a03;}
.pp-welfare-copy span{font-size:10px;color:#a16207;}
.pp-welfare-tag{font-size:9px;color:#b45309;background:#ffedd5;border:1px solid #fed7aa;border-radius:999px;padding:1px 6px;flex:none;}
.pp-welfare-plans{display:flex;flex-wrap:wrap;gap:4px 8px;margin:7px 0 8px;font-size:9px;color:#9a5b0a;font-variant-numeric:tabular-nums;}
.pp-welfare-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;}
.pp-welfare-note{font-size:9px;color:#b48a52;}
.pp-welfare-btn{display:inline-flex;align-items:center;gap:3px;height:24px;padding:0 10px;border-radius:7px;background:#f59e0b;color:#fff;font-size:10px;font-weight:600;text-decoration:none;box-shadow:0 2px 5px rgba(245,158,11,.25);}
.pp-welfare-btn:hover{background:#d97706;color:#fff;}
.pp-token{flex:none;border:1px solid var(--pp-line);border-radius:12px;padding:10px;background:#fff;margin-bottom:10px;}
.pp-token-head{display:flex;align-items:center;gap:7px;}
.pp-token-key{flex:none;width:24px;height:24px;border-radius:7px;background:var(--pp-brand-weak);color:var(--pp-brand);display:flex;align-items:center;justify-content:center;}
.pp-token-title{font-size:11.5px;font-weight:600;}
.pp-token-chips{margin-left:auto;display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:flex-end;}
.pp-pill{font-size:9.5px;line-height:1.5;padding:1px 6px;border-radius:99px;font-weight:600;white-space:nowrap;}
.pp-pill.remain{background:var(--pp-success-bg);color:#067647;}
.pp-pill.remain.low{background:var(--pp-warning-bg);color:#b54708;}
.pp-pill.remain.zero{background:var(--pp-danger-bg);color:#b42318;}
.pp-pill.latency{background:#eef2ff;color:#4353b8;}
.pp-input-row{display:flex;align-items:center;gap:6px;margin-top:8px;}
.pp-input-row .el-input{flex:1;min-width:0;}
.pp-input-row .el-button{flex:none;height:28px;padding:0 12px;border-radius:8px;font-size:11.5px;}
.pp-note{display:flex;align-items:flex-start;gap:6px;margin-top:7px;padding:6px 8px;border-radius:8px;background:var(--pp-warning-bg);border:1px solid var(--pp-warning-line);color:#92400e;font-size:10px;line-height:1.5;}
.pp-note.ok{background:var(--pp-success-bg);border-color:var(--pp-success-line);color:#0b7a46;}
.pp-note.bad{background:var(--pp-danger-bg);border-color:var(--pp-danger-line);color:#b42318;}
.pp-note svg{flex:none;margin-top:1px;}
.pp-note a{color:var(--pp-brand);font-weight:600;text-decoration:none;}
.pp-token-list{display:flex;flex-direction:column;gap:4px;margin-top:7px;padding-top:7px;border-top:1px dashed var(--pp-line);}
.pp-token-item{display:flex;align-items:center;gap:6px;padding:4px 6px;border-radius:6px;background:var(--pp-surface);font-size:9.5px;color:var(--pp-text-2);}
.pp-token-item code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:9px;color:#475569;}
.pp-token-item .pp-token-mini{margin-left:auto;border:0;background:#fff;border-radius:5px;padding:2px 6px;font-size:9px;color:var(--pp-brand);cursor:pointer;box-shadow:0 0 0 1px var(--pp-line);}
.pp-setting-list{border:1px solid var(--pp-line);border-radius:12px;overflow:hidden;background:#fff;}
.pp-set-row{width:100%;display:flex;align-items:center;gap:9px;padding:9px 10px;border:0;border-top:1px solid var(--pp-line-soft);background:#fff;text-align:left;cursor:pointer;transition:background .15s;}
.pp-set-row:first-child{border-top:0;}
.pp-set-row:hover{background:var(--pp-surface-2);}
.pp-set-ico{flex:none;width:28px;height:28px;border-radius:8px;background:var(--pp-surface);color:#7c899d;display:flex;align-items:center;justify-content:center;}
.pp-set-main{flex:1;min-width:0;}
.pp-set-name{display:flex;align-items:center;gap:5px;font-size:11.5px;font-weight:600;color:var(--pp-text);}
.pp-set-desc{margin-top:1px;font-size:9.5px;color:var(--pp-text-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pp-set-chev{flex:none;color:#c3ccd9;display:flex;}
.pp-set-row:hover .pp-set-chev{color:var(--pp-brand);}
.pp-set-badge{font-size:8.5px;font-weight:600;color:#b45309;background:var(--pp-warning-bg);border:1px solid var(--pp-warning-line);border-radius:99px;padding:0 4px;line-height:1.5;}
.pp-settings-note{flex:none;margin-top:8px;text-align:center;font-size:9px;color:var(--pp-text-4);}
.pp-detail-head{position:sticky;top:-12px;z-index:3;display:flex;align-items:center;gap:7px;margin:-12px -12px 8px;padding:10px 10px 9px 8px;background:rgba(255,255,255,.96);backdrop-filter:blur(6px);border-bottom:1px solid var(--pp-line);}
.pp-back{flex:none;width:26px;height:26px;border:0;border-radius:7px;background:transparent;color:#7c899d;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;}
.pp-back:hover{background:#f1f4f9;color:var(--pp-text);}
.pp-detail-title{flex:1;min-width:0;font-size:12px;font-weight:600;color:var(--pp-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pp-detail-desc{margin:-2px 1px 8px;font-size:10px;line-height:1.55;color:#7c899d;}
.pp-detail-extra{flex:none;font-size:9px;font-weight:600;color:#b45309;background:var(--pp-warning-bg);border:1px solid var(--pp-warning-line);border-radius:99px;padding:1px 6px;}
.pp-group{margin:2px 0 8px;}
.pp-group-label{display:flex;align-items:center;gap:5px;margin:0 1px 5px;font-size:9.5px;font-weight:600;letter-spacing:.05em;color:var(--pp-text-3);}
.pp-seg{display:flex;gap:2px;padding:2px;border-radius:9px;background:#f1f4f9;}
.pp-seg-btn{flex:1 1 0;min-width:0;border:0;border-radius:7px;background:transparent;padding:5px 4px;font-size:10.5px;font-weight:500;color:#65738a;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:background .15s,color .15s,box-shadow .15s;}
.pp-seg-btn:hover{color:#334155;}
.pp-seg-btn.active{background:#fff;color:var(--pp-brand);font-weight:600;box-shadow:0 1px 2px rgba(16,24,40,.08),0 0 0 1px rgba(16,24,40,.04);}
.pp-seg-btn:disabled{opacity:.45;cursor:not-allowed;}
.pp-row{display:flex;align-items:center;gap:8px;min-height:36px;padding:3px 1px;border-top:1px solid var(--pp-line-soft);}
.pp-row:first-child{border-top:0;}
.pp-row-label{flex:1;min-width:0;display:flex;align-items:center;gap:5px;flex-wrap:wrap;font-size:11.5px;color:#334155;line-height:1.4;}
.pp-row-label .pp-unit{font-size:9px;color:var(--pp-text-4);font-weight:400;}
.pp-row-label .pp-muted{font-size:9px;color:var(--pp-text-4);}
.pp-row-control{flex:none;display:flex;align-items:center;gap:5px;}
.pp-row.is-off .pp-row-label{color:#a9b4c4;}
.pp-row .el-switch{--el-switch-on-color:var(--pp-brand);--el-switch-off-color:#dfe4ec;transform:scale(.86);transform-origin:right center;}
.pp-row .el-input-number{width:80px;}
.pp-ai{margin:4px 1px 8px;padding:8px;border:1px solid var(--pp-line);border-radius:10px;background:var(--pp-surface-2);transition:opacity .15s;}
.pp-ai.pp-off{opacity:.55;}
.pp-ai-row{display:flex;align-items:center;gap:7px;min-height:28px;}
.pp-ai-row+.pp-ai-row{margin-top:4px;}
.pp-cost-badge{flex:none;font-size:8px;line-height:1.5;padding:0 4px;border-radius:4px;background:#eef2f7;color:#64748b;font-weight:600;white-space:nowrap;}
.pp-cost-badge.is-low{background:#ecfdf3;color:#067647;}
.pp-cost-badge.is-mid{background:#eff6ff;color:#1d4ed8;}
.pp-cost-badge.is-high{background:#fff7ed;color:#b45309;}
.pp-cost-badge.is-ultra{background:#fef2f2;color:#b42318;}
.pp-cfg-model-option{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-width:0;}
.pp-cfg-model-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.pp-ai-label{flex:none;width:38px;font-size:9.5px;color:#7c899d;}
.pp-ai .el-select{flex:1;min-width:0;}
.pp-ai-note{margin-left:auto;font-size:9px;color:var(--pp-text-4);white-space:nowrap;}
.pp-hint{display:flex;align-items:flex-start;gap:5px;margin:6px 1px 2px;padding:6px 8px;border-radius:8px;background:var(--pp-surface);border:1px solid var(--pp-line-soft);color:#64748b;font-size:9.5px;line-height:1.5;}
.pp-hint.warn{background:var(--pp-warning-bg);border-color:var(--pp-warning-line);color:#92400e;}
.pp-hint.ok{background:var(--pp-success-bg);border-color:var(--pp-success-line);color:#0b7a46;}
.pp-hint svg{flex:none;margin-top:1px;}
.pp-records{display:flex;flex-direction:column;gap:7px;height:100%;}
.pp-records>.pp-empty{flex:1;}
.pp-scale-card{flex:none;border:1px solid var(--pp-line);border-radius:10px;background:#fff;padding:9px 10px;}
.pp-scale-head{display:flex;align-items:center;justify-content:space-between;gap:8px;}
.pp-scale-head b{font-size:10.5px;font-weight:600;color:var(--pp-text-2);}
.pp-scale-head span{font-size:9px;color:var(--pp-text-4);}
.pp-scale-options{display:grid;grid-template-columns:repeat(3,1fr);gap:4px;margin-top:6px;}
.pp-scale-btn{height:24px;border:1px solid var(--pp-line);border-radius:6px;background:#fff;color:var(--pp-text-3);font-size:9.5px;cursor:pointer;transition:border-color .15s,background .15s,color .15s;}
.pp-scale-btn:hover{border-color:#c7d7f5;color:var(--pp-brand);}
.pp-scale-btn.is-active{border-color:#bcd0f4;background:var(--pp-brand-weak);color:var(--pp-brand);font-weight:600;}
.pp-scale-note{margin:5px 0 0;font-size:8.5px;color:var(--pp-text-4);line-height:1.4;}
.pp-rec-tips{flex:none;border:1px solid #dbe8ff;border-radius:10px;background:#f5f9ff;padding:9px 10px;display:flex;flex-direction:column;gap:5px;}
.pp-rec-tips.is-warning{border-color:#f3dfbf;background:#fff9ef;}
.pp-rec-tip{display:flex;align-items:center;gap:6px;font-size:10px;line-height:1.5;color:#4b5b70;flex-wrap:wrap;}
.pp-rec-tips.is-warning .pp-rec-tip{color:#8a5a1f;}
.pp-rec-tip-ico{display:flex;align-items:center;justify-content:center;color:#d97706;flex:none;}
.pp-rec-tip-text{flex:1;min-width:0;}
.pp-rec-config{height:22px;padding:0 8px;border:0;border-radius:6px;background:#0052d9;color:#fff;font-size:9px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:3px;}
.pp-rec-config:hover{background:#0046c0;}
.pp-rec-summary{flex:none;display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding:7px 1px;font-size:9.5px;color:var(--pp-text-3);border-top:1px solid var(--pp-line-soft);border-bottom:1px solid var(--pp-line-soft);}
.pp-rec-summary b{color:var(--pp-text-1);font-weight:700;font-variant-numeric:tabular-nums;}
.pp-rec-sum-sep{color:var(--pp-line);}
.pp-rec-legend{flex:none;display:flex;align-items:center;gap:9px;flex-wrap:wrap;font-size:9px;color:var(--pp-text-3);}
.pp-legend-item{display:inline-flex;align-items:center;gap:3px;}
.pp-legend-dot{width:8px;height:8px;border-radius:50%;background:#fff;border:1px solid #cbd5e1;}
.pp-legend-dot.is-searching{border-color:#93c5fd;background:#eff6ff;}
.pp-legend-dot.is-bank{background:#3b82f6;border-color:#3b82f6;}
.pp-legend-dot.is-ai{background:#22c55e;border-color:#22c55e;}
.pp-legend-dot.is-verify{background:#8b5cf6;border-color:#8b5cf6;}
.pp-legend-dot.is-cache{background:#06b6d4;border-color:#06b6d4;}
.pp-legend-dot.is-skipped{background:#94a3b8;border-color:#94a3b8;}
.pp-legend-dot.is-error{background:#fff7ed;border-color:#f59e0b;}
.pp-q-circles{flex:none;display:flex;flex-wrap:wrap;gap:5px;padding:2px 0 4px;}
.pp-q-circle{position:relative;width:21px;height:21px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:8.5px;font-weight:700;font-variant-numeric:tabular-nums;border:1px solid transparent;transition:background-color .2s var(--ppfx-ease-soft),color .2s var(--ppfx-ease-soft),border-color .2s var(--ppfx-ease-soft);}
.pp-q-circle.is-bank{background:#3b82f6;color:#fff;border-color:#3b82f6;}
.pp-q-circle.is-ai{background:#22c55e;color:#fff;border-color:#22c55e;}
.pp-q-circle.is-verify{background:#8b5cf6;color:#fff;border-color:#8b5cf6;}
.pp-q-circle.is-cache{background:#06b6d4;color:#fff;border-color:#06b6d4;}
.pp-q-circle.is-skipped{background:#94a3b8;color:#fff;border-color:#94a3b8;}
.pp-q-circle.is-error{background:#fff7ed;color:#b45309;border-color:#f59e0b;}
.pp-q-circle.is-searching{background:#fff;color:#2563eb;border-color:#93c5fd;}
.pp-q-circle.is-searching::after{content:"";position:absolute;inset:-1px;border-radius:50%;border:1px solid rgba(59,130,246,.45);animation:ppfxQring 1.3s var(--ppfx-ease-soft) infinite;pointer-events:none;}
.pp-q-circle.is-pending{background:#f1f5f9;color:#94a3b8;border-color:#e8eef6;}
@keyframes ppfxQring{0%{transform:scale(1);opacity:.55;}70%{transform:scale(1.35);opacity:0;}100%{opacity:0;}}
.pp-list-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 1px 1px;flex:none;}
.pp-list-head b{font-size:11px;font-weight:600;color:var(--pp-text-2);letter-spacing:.03em;}
.pp-list-head span{font-size:9.5px;color:var(--pp-text-3);}
.pp-rec{border:1px solid var(--pp-line);border-radius:10px;padding:9px 10px;background:#fff;transition:border-color .15s;}
.pp-rec:hover{border-color:#d8e2f2;}
.pp-rec:last-child{animation:ppfxRise .18s var(--ppfx-ease) both;}
.pp-rec-top{display:flex;align-items:center;gap:5px;margin-bottom:4px;}
.pp-rec-idx{width:18px;height:18px;border-radius:6px;background:#f1f4f9;color:#8a96a8;font-size:9px;font-weight:600;display:flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums;}
.pp-tag{font-size:8.5px;line-height:1.6;padding:0 4px;border-radius:4px;background:var(--pp-warning-bg);border:1px solid var(--pp-warning-line);color:#b45309;font-weight:600;}
.pp-tag.is-ai{background:#ecfdf3;border-color:#bbf7d0;color:#067647;}
.pp-tag.is-verify{background:#f5f3ff;border-color:#ddd6fe;color:#6d28d9;}
.pp-tag.is-cache{background:#ecfeff;border-color:#a5f3fc;color:#0e7490;}
.pp-tag.is-skipped{background:#f1f5f9;border-color:#e2e8f0;color:#64748b;}
.pp-rec-q{font-size:11.5px;line-height:1.6;color:#2f3b4f;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word;}
.pp-rec-q img{max-width:100%;height:auto;vertical-align:middle;}
.pp-rec-a{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px;padding-top:6px;border-top:1px dashed var(--pp-line-soft);}
.pp-ans{max-width:100%;font-size:10px;line-height:1.5;color:#0b7a46;background:var(--pp-success-bg);border:1px solid var(--pp-success-line);border-radius:5px;padding:1px 5px;word-break:break-word;}
.pp-ans.is-empty{color:#a0abbb;background:var(--pp-surface);border-color:var(--pp-line-soft);}
.pp-usage{display:flex;flex-direction:column;gap:7px;}
.pp-usage .pp-list-head{margin-top:2px;}
.pp-usage-card{border:1px solid var(--pp-line);border-radius:11px;padding:9px;background:#fff;}
.pp-usage-top{display:flex;align-items:center;gap:6px;}
.pp-usage-top .el-input{flex:1;min-width:0;}
.pp-usage-top .el-button{flex:none;height:28px;padding:0 11px;border-radius:8px;font-size:11px;}
.pp-usage-meta{display:flex;align-items:center;gap:6px;margin-top:6px;font-size:9px;color:var(--pp-text-3);}
.pp-token-chip{display:inline-flex;align-items:center;padding:1px 6px;border-radius:99px;background:#eff6ff;border:1px solid #dbeafe;color:#1d4ed8;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:9px;}
.pp-modebar{display:flex;gap:2px;padding:2px;border-radius:8px;background:#f1f4f9;}
.pp-modebar button{flex:1;border:0;border-radius:6px;background:transparent;padding:4px 2px;font-size:9.5px;color:#65738a;cursor:pointer;white-space:nowrap;}
.pp-modebar button.active{background:#fff;color:var(--pp-brand);font-weight:600;box-shadow:0 1px 2px rgba(16,24,40,.08);}
.pp-u-list{border:1px solid var(--pp-line);border-radius:11px;overflow:hidden;background:#fff;}
.pp-u-list>.pp-empty{min-height:190px;}
.pp-u-row{width:100%;display:flex;align-items:center;gap:7px;padding:7px 9px;border:0;border-top:1px solid var(--pp-line-soft);background:#fff;text-align:left;cursor:pointer;}
.pp-u-row:first-child{border-top:0;}
.pp-u-row:hover{background:var(--pp-surface-2);}
.pp-u-left{flex:1;min-width:0;}
.pp-u-line{display:flex;align-items:center;gap:5px;font-size:10px;color:#4b5a70;}
.pp-u-date{font-weight:600;font-variant-numeric:tabular-nums;}
.pp-u-time{color:#98a4b6;font-size:9px;font-variant-numeric:tabular-nums;}
.pp-u-mode{font-size:8.5px;line-height:1.6;padding:0 4px;border-radius:4px;background:#eef2ff;color:#4353b8;font-weight:600;}
.pp-u-mode.is-ai{background:#ecfdf3;color:#067647;}
.pp-u-mode.is-verify{background:var(--pp-warning-bg);color:#b54708;}
.pp-u-mode.is-normal{background:#eef2ff;color:#4353b8;}
.pp-u-mode.is-chat{background:#eef7ff;color:#0369a1;}
.pp-u-sub{display:flex;align-items:center;gap:6px;margin-top:1px;font-size:9px;color:var(--pp-text-4);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pp-u-sub-text{min-width:0;overflow:hidden;text-overflow:ellipsis;}
.pp-u-model{flex:none;max-width:100px;margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;color:#4353b8;cursor:help;}
.pp-u-model.is-empty{color:var(--pp-text-4);font-weight:500;}
.pp-u-right{flex:none;display:flex;flex-direction:column;align-items:flex-end;gap:1px;}
.pp-u-cost{font-size:10.5px;font-weight:700;color:#0b7a46;font-variant-numeric:tabular-nums;}
.pp-u-rem{font-size:8.5px;color:var(--pp-text-4);}
.pp-u-detail{padding:0 9px 7px;background:var(--pp-surface-2);border-top:1px dashed var(--pp-line-soft);font-size:9px;line-height:1.5;color:#7c899d;}
.pp-pager{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:1px 2px;font-size:9.5px;color:var(--pp-text-3);}
.pp-pager-btns{display:flex;align-items:center;gap:4px;}
.pp-pager-btns button{width:24px;height:24px;border:1px solid var(--pp-line);border-radius:6px;background:#fff;color:#65738a;display:flex;align-items:center;justify-content:center;cursor:pointer;padding:0;}
.pp-pager-btns button:hover:not(:disabled){border-color:#c7d7f5;color:var(--pp-brand);}
.pp-pager-btns button:disabled{opacity:.4;cursor:not-allowed;}
.pp-ai-chat{position:relative;height:100%;min-height:0;display:flex;flex-direction:column;gap:6px;}
.pp-ai-top{flex:none;display:flex;align-items:center;gap:4px;}
.pp-ai-picker{flex:1;min-width:0;height:30px;display:flex;align-items:center;gap:6px;padding:0 7px;border:1px solid var(--pp-line);border-radius:9px;background:#fff;color:var(--pp-text-2);font-size:11px;cursor:pointer;text-align:left;transition:border-color .15s,background .15s;}
.pp-ai-picker:hover{border-color:#c7d7f5;background:#f8fbff;}
.pp-ai-picker .pp-icon{color:var(--pp-brand);}
.pp-ai-picker-title{flex:1;min-width:0;font-weight:600;color:var(--pp-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pp-ai-count-pill{flex:none;font-size:8.5px;line-height:1.5;padding:1px 5px;border-radius:99px;background:var(--pp-surface);border:1px solid var(--pp-line);color:var(--pp-text-3);font-variant-numeric:tabular-nums;}
.pp-ai-tool{flex:none;width:30px;height:30px;padding:0;border:1px solid var(--pp-line);border-radius:9px;background:#fff;color:#7d899d;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:border-color .15s,background .15s,color .15s;}
.pp-ai-tool:hover:not(:disabled){border-color:#c7d7f5;background:#f8fbff;color:var(--pp-brand);}
.pp-ai-tool.is-active{background:var(--pp-brand-weak);border-color:#c7d7f5;color:var(--pp-brand);}
.pp-ai-tool.is-danger:hover:not(:disabled){border-color:var(--pp-danger-line);background:var(--pp-danger-bg);color:var(--pp-danger);}
.pp-ai-tool:disabled{opacity:.4;cursor:not-allowed;}
.pp-ai-mini-spin{width:11px;height:11px;border:1.5px solid #dbeafe;border-top-color:var(--pp-brand);border-radius:50%;animation:ppSpin .8s linear infinite;display:block;}
.pp-ai-menu-mask{position:absolute;inset:0;z-index:8;background:transparent;}
.pp-ai-menu{position:absolute;left:0;right:0;top:34px;z-index:9;border:1px solid var(--pp-line);border-radius:10px;background:#fff;box-shadow:0 14px 34px -12px rgba(15,23,42,.30),0 2px 6px rgba(15,23,42,.06);overflow:hidden;transform-origin:top center;animation:ppfxMenuIn .16s var(--ppfx-ease) both;}
.pp-ai-menu-head{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 9px;border-bottom:1px solid var(--pp-line-soft);font-size:9px;color:var(--pp-text-3);}
.pp-ai-menu-head b{font-size:10px;font-weight:600;color:var(--pp-text-2);letter-spacing:.03em;}
.pp-ai-menu-head span{font-variant-numeric:tabular-nums;}
.pp-ai-session-list{max-height:196px;overflow-y:auto;padding:3px;}
.pp-ai-session{width:100%;display:flex;align-items:center;gap:7px;padding:7px;border:0;border-radius:7px;background:transparent;text-align:left;cursor:pointer;transition:background .12s;}
.pp-ai-session:hover{background:var(--pp-surface);}
.pp-ai-session.active{background:var(--pp-brand-weak);}
.pp-ai-session-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;}
.pp-ai-session-title{font-size:10.5px;font-weight:600;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pp-ai-session.active .pp-ai-session-title{color:var(--pp-brand);}
.pp-ai-session-time{font-size:8.5px;color:var(--pp-text-4);}
.pp-ai-session-dot{flex:none;width:5px;height:5px;border-radius:50%;background:var(--pp-brand);}
.pp-ai-menu-empty{padding:18px 10px;text-align:center;font-size:9.5px;color:var(--pp-text-4);}
.pp-ai-menu-foot{border-top:1px solid var(--pp-line-soft);padding:4px;}
.pp-ai-menu-foot button{width:100%;height:24px;border:0;border-radius:6px;background:transparent;color:var(--pp-danger);font-size:9.5px;cursor:pointer;}
.pp-ai-menu-foot button:hover{background:var(--pp-danger-bg);}
.pp-ai-callout{flex:none;display:flex;align-items:flex-start;gap:6px;padding:6px 8px;border-radius:8px;background:var(--pp-brand-weak);border:1px solid #d6e4ff;color:#1d4ed8;font-size:9.5px;line-height:1.5;}
.pp-ai-callout .pp-icon{flex:none;margin-top:1px;}
.pp-ai-callout.is-error{background:var(--pp-danger-bg);border-color:var(--pp-danger-line);color:#b42318;}
.pp-ai-callout.is-ok{background:var(--pp-success-bg);border-color:var(--pp-success-line);color:#0b7a46;}
.pp-ai-callout-text{flex:1;min-width:0;word-break:break-word;}
.pp-ai-linkbtn{flex:none;height:20px;padding:0 6px;border:1px solid currentColor;border-radius:5px;background:#fff;color:var(--pp-brand);font-size:9px;font-weight:600;cursor:pointer;white-space:nowrap;}
.pp-ai-linkbtn:hover{background:#f2f7ff;}
.pp-ai-callout.is-error .pp-ai-linkbtn{color:#b42318;}
.pp-ai-callout.is-error .pp-ai-linkbtn:hover{background:#fff5f5;}
.pp-ai-callout-x{flex:none;width:18px;height:18px;padding:0;border:0;border-radius:5px;background:transparent;color:currentColor;opacity:.65;font-size:13px;line-height:1;display:flex;align-items:center;justify-content:center;cursor:pointer;}
.pp-ai-callout-x:hover{opacity:1;background:rgba(255,255,255,.6);}
.pp-ai-confirm{flex:none;display:flex;align-items:center;gap:5px;padding:5px 7px;border:1px solid var(--pp-danger-line);border-radius:7px;background:var(--pp-danger-bg);color:#b42318;font-size:9.5px;line-height:1.4;}
.pp-ai-confirm>span{flex:1;min-width:0;}
.pp-ai-confirm button{flex:none;height:21px;padding:0 7px;border:1px solid var(--pp-danger-line);border-radius:5px;background:#fff;color:#b42318;font-size:9px;cursor:pointer;}
.pp-ai-confirm button.primary{background:var(--pp-danger);border-color:var(--pp-danger);color:#fff;}
.pp-ai-confirm button:disabled{opacity:.55;cursor:not-allowed;}
.pp-ai-messages{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column;gap:9px;padding:2px 2px 4px;}
.pp-ai-msg{display:flex;align-items:flex-start;gap:6px;max-width:100%;animation:ppfxRise .18s var(--ppfx-ease) both;}
.pp-ai-msg.user{justify-content:flex-end;animation-name:ppfxRiseRight;}
.pp-ai-avatar{flex:none;width:20px;height:20px;border-radius:6px;background:var(--pp-brand-weak);color:var(--pp-brand);display:flex;align-items:center;justify-content:center;margin-top:1px;}
.pp-ai-avatar .pp-logo{box-shadow:none;border-radius:5px;}
.pp-ai-avatar.is-error{background:var(--pp-danger-bg);color:var(--pp-danger);}
.pp-ai-bubble{min-width:0;max-width:calc(100% - 26px);padding:7px 9px;border-radius:10px;font-size:11.5px;line-height:1.6;word-break:break-word;overflow-wrap:anywhere;}
.pp-ai-msg.assistant .pp-ai-bubble{background:var(--pp-surface-2);border:1px solid var(--pp-line-soft);border-top-left-radius:3px;color:#2f3b4f;}
.pp-ai-msg.user .pp-ai-bubble{max-width:86%;background:var(--pp-brand);border-top-right-radius:3px;color:#fff;white-space:pre-wrap;}
.pp-ai-msg.error .pp-ai-bubble{background:var(--pp-danger-bg);border:1px solid var(--pp-danger-line);border-top-left-radius:3px;color:#b42318;font-size:10.5px;}
.pp-ai-error-text{white-space:pre-wrap;}
.pp-ai-msg.error .pp-ai-linkbtn{margin-top:5px;}
.pp-ai-md p{margin:0 0 6px;}
.pp-ai-md p:last-child{margin-bottom:0;}
.pp-ai-md strong{font-weight:600;color:var(--pp-text);}
.pp-ai-md code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;background:#eef2f7;border:1px solid var(--pp-line);border-radius:4px;padding:0 3px;color:#1e293b;}
.pp-ai-md pre{margin:6px 0;padding:7px 8px;border:1px solid var(--pp-line);border-radius:7px;background:#f7f9fc;overflow-x:auto;}
.pp-ai-md pre code{display:block;padding:0;border:0;background:transparent;font-size:9.5px;line-height:1.55;white-space:pre-wrap;word-break:break-word;color:#1e293b;}
.pp-ai-md a{color:var(--pp-brand);text-decoration:underline;text-underline-offset:1px;}
.pp-ai-md a:hover{text-decoration:underline;color:var(--pp-brand-hover);}
.pp-ai-sources{margin-top:6px;padding-top:4px;border-top:1px dashed var(--pp-line);}
.pp-ai-sources summary{display:flex;align-items:center;gap:4px;min-height:22px;padding:0;font-size:9px;font-weight:600;color:var(--pp-text-3);cursor:pointer;list-style:none;user-select:none;}
.pp-ai-sources summary::-webkit-details-marker{display:none;}
.pp-ai-sources summary .pp-ai-sources-chev{margin-left:auto;color:#a8b3c4;transition:transform .15s;}
.pp-ai-sources[open] summary .pp-ai-sources-chev{transform:rotate(180deg);}
.pp-ai-sources-count{font-weight:500;color:var(--pp-text-4);}
.pp-ai-sources-list{display:flex;flex-direction:column;gap:1px;max-height:110px;overflow-y:auto;margin-top:2px;}
.pp-ai-source{display:flex;align-items:baseline;gap:5px;min-width:0;padding:3px 4px;border-radius:5px;color:#334155;text-decoration:none;}
a.pp-ai-source:hover{background:#f4f8ff;color:var(--pp-brand);}
.pp-ai-source-title{flex:1;min-width:0;font-size:9.5px;font-weight:600;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pp-ai-source-host{flex:none;max-width:42%;font-size:8.5px;color:var(--pp-text-4);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pp-ai-cursor{display:inline-block;width:5px;height:11px;margin-left:2px;vertical-align:-1px;border-radius:1px;background:var(--pp-brand);animation:ppAiBlink 1s steps(2,start) infinite;}
.pp-ai-finish-note{margin-top:4px;font-size:8.5px;color:var(--pp-text-4);}
@keyframes ppAiBlink{50%{opacity:0;}}
.pp-ai-status{flex:none;display:flex;align-items:center;gap:6px;min-height:14px;padding:0 1px;font-size:9px;color:var(--pp-text-3);}
.pp-ai-status-main{flex:1;min-width:0;display:flex;align-items:center;gap:5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.ppfx-thinking{flex:none;display:inline-flex;align-items:flex-end;gap:2px;height:9px;}
.ppfx-thinking>i{flex:none;width:2px;height:5px;border-radius:1px;background:currentColor;opacity:.55;transform-origin:50% 100%;animation:ppfxBars .9s var(--ppfx-ease-soft) infinite;}
.ppfx-thinking>i:nth-child(2){height:8px;animation-delay:.12s;}
.ppfx-thinking>i:nth-child(3){height:6px;animation-delay:.24s;}
@keyframes ppfxBars{0%,100%{transform:scaleY(.55);opacity:.45;}45%{transform:scaleY(1);opacity:1;}}
.pp-ai-spinner{flex:none;width:9px;height:9px;border:1.5px solid #dbeafe;border-top-color:var(--pp-brand);border-radius:50%;animation:ppSpin .8s linear infinite;display:block;}
.pp-ai-usage{flex:none;font-size:8.5px;color:var(--pp-text-3);font-variant-numeric:tabular-nums;white-space:nowrap;}
.pp-ai-welcome{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;min-height:150px;padding:14px 18px;text-align:center;color:var(--pp-text-3);font-size:9.5px;line-height:1.5;}
.pp-ai-welcome-ico{width:42px;height:42px;border-radius:13px;background:var(--pp-brand-weak);border:1px solid #dbe7ff;color:var(--pp-brand);display:flex;align-items:center;justify-content:center;margin-bottom:2px;}
.pp-ai-welcome b{font-size:11.5px;font-weight:600;color:var(--pp-text-2);}
.pp-ai-welcome span{max-width:210px;}
.pp-ai-welcome .pp-ai-spinner{width:16px;height:16px;border-width:2px;}
.pp-ai-welcome-note{font-size:8.5px;color:var(--pp-text-4);}
.pp-ai-composer{flex:none;padding:6px;border:1px solid var(--pp-line);border-radius:10px;background:#fff;transition:border-color .15s,box-shadow .15s;}
.pp-ai-composer:focus-within{border-color:#c7d7f5;box-shadow:0 0 0 2px var(--pp-brand-ring);}
.pp-ai-input{display:block;width:100%;min-height:36px;max-height:72px;padding:2px 3px;border:0;outline:0;background:transparent;color:var(--pp-text);font-family:inherit;font-size:11.5px;line-height:1.55;resize:none;overflow-y:auto;}
.pp-ai-input::placeholder{color:var(--pp-text-4);}
.pp-ai-input.is-over{color:var(--pp-danger);}
.pp-ai-input:disabled{opacity:.65;cursor:not-allowed;}
.pp-ai-toggles{flex:none;display:flex;align-items:center;gap:4px;margin-bottom:4px;}
.pp-ai-toggle{height:22px;padding:0 6px;border:1px solid var(--pp-line);border-radius:6px;background:#fff;color:var(--pp-text-3);font-size:9px;display:inline-flex;align-items:center;gap:3px;cursor:pointer;white-space:nowrap;transition:border-color .15s,background .15s,color .15s;}
.pp-ai-toggle:hover:not(:disabled){border-color:#c7d7f5;color:var(--pp-text-2);}
.pp-ai-toggle.active{background:var(--pp-brand-weak);border-color:#c7d7f5;color:var(--pp-brand);font-weight:600;}
.pp-ai-toggle:disabled{opacity:.55;cursor:not-allowed;}
.pp-ai-count{flex:none;font-size:9px;color:var(--pp-text-4);font-variant-numeric:tabular-nums;white-space:nowrap;}
.pp-ai-count.is-over{color:var(--pp-danger);font-weight:600;}
.pp-ai-sendrow{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:4px;}
.pp-ai-model{position:relative;flex:1;min-width:0;display:flex;}
.pp-ai-model-btn{min-width:0;max-width:100%;height:22px;padding:0 6px;border:1px solid var(--pp-line);border-radius:6px;background:#fff;color:var(--pp-text-2);font-size:8.5px;display:inline-flex;align-items:center;gap:4px;cursor:pointer;white-space:nowrap;transition:border-color .15s,background .15s,color .15s;}
.pp-ai-model-btn:hover:not(:disabled){border-color:#c7d7f5;background:#f8fbff;color:var(--pp-brand);}
.pp-ai-model-btn.is-active{border-color:#c7d7f5;background:var(--pp-brand-weak);color:var(--pp-brand);}
.pp-ai-model-btn:disabled{opacity:.55;cursor:not-allowed;}
.pp-ai-model-btn .pp-icon{color:var(--pp-brand);}
.pp-ai-model-label{min-width:0;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pp-ai-model-menu{position:absolute;left:0;bottom:calc(100% + 4px);z-index:12;width:220px;max-height:236px;overflow-y:auto;padding:3px;border:1px solid var(--pp-line);border-radius:8px;background:#fff;box-shadow:0 12px 28px -10px rgba(15,23,42,.28);transform-origin:bottom center;animation:ppfxMenuUp .16s var(--ppfx-ease) both;}
.pp-ai-model-group + .pp-ai-model-group{margin-top:2px;padding-top:2px;border-top:1px solid var(--pp-line-soft);}
.pp-ai-model-group-title{padding:4px 6px 3px;border-radius:4px;background:var(--pp-surface);font-size:9px;font-weight:700;letter-spacing:.03em;color:var(--pp-text-3);}
.pp-ai-model-option{width:100%;height:27px;display:flex;align-items:center;gap:5px;padding:0 6px 0 12px;border:0;border-radius:5px;background:transparent;color:var(--pp-text-2);font-size:10.5px;text-align:left;cursor:pointer;}
.pp-ai-model-option:hover{background:var(--pp-surface);}
.pp-ai-model-option.active{background:var(--pp-brand-weak);color:var(--pp-brand);font-weight:600;}
.pp-ai-model-option span{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.pp-ai-model-option .pp-cost-badge{flex:none;max-width:none;overflow:visible;text-overflow:clip;}
.pp-ai-enterhint{flex:none;margin-top:-3px;text-align:center;font-size:8.5px;line-height:1.35;color:var(--pp-text-4);user-select:none;pointer-events:none;}
.pp-ai-send{flex:none;height:26px;padding:0 12px;border:0;border-radius:7px;background:var(--pp-brand);color:#fff;font-size:10.5px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:4px;transition:background .15s;}
.pp-ai-send:hover:not(:disabled){background:var(--pp-brand-hover);}
.pp-ai-send:disabled{background:#c7d2e3;cursor:not-allowed;}
.pp-ai-send.pp-ai-stop{background:#fff;border:1px solid var(--pp-danger-line);color:var(--pp-danger);}
.pp-ai-send.pp-ai-stop:hover{background:var(--pp-danger-bg);}
.pp-ai-send.pp-ai-stop i{width:7px;height:7px;border-radius:1px;background:currentColor;display:block;}
.pp-prose{font-size:11.5px;line-height:1.75;color:#475569;}
.pp-prose p{margin:0 0 7px;}
.pp-prose p:last-child{margin-bottom:0;}
.pp-prose h4{margin:10px 0 4px;font-size:11px;font-weight:600;color:var(--pp-text);}
.pp-prose h4:first-child{margin-top:0;}
.pp-prose .muted{color:var(--pp-text-3);font-size:9.5px;}
.pp-prose a{color:var(--pp-brand);text-decoration:none;}
.pp-prose a:hover{text-decoration:underline;}
.pp-doc{border:1px solid var(--pp-line);border-radius:11px;padding:10px;margin-bottom:7px;background:#fff;}
.pp-doc:last-child{margin-bottom:0;}
.pp-doc-title{display:flex;align-items:center;gap:5px;margin-bottom:6px;font-size:11px;font-weight:600;color:var(--pp-text);}
.pp-doc-title svg{color:#8b98ab;}
.pp-quote{margin:7px 0;padding:8px 10px;border-left:2px solid var(--pp-brand);border-radius:0 7px 7px 0;background:var(--pp-surface-2);color:#334155;font-size:11px;line-height:1.7;}
.pp-links{display:flex;flex-direction:column;gap:5px;}
.pp-link{display:flex;align-items:center;justify-content:space-between;gap:7px;padding:7px 9px;border:1px solid var(--pp-line);border-radius:8px;color:#334155;text-decoration:none;font-size:10.5px;line-height:1.4;}
.pp-link:hover{border-color:#c7d7f5;background:#f8fbff;color:var(--pp-brand);}
.pp-link svg{flex:none;color:#b3bdcb;}
.pp-link:hover svg{color:var(--pp-brand);}
.pp-price-row{display:flex;align-items:baseline;justify-content:space-between;gap:8px;font-size:10px;color:#475569;padding:2px 0;}
.pp-price-row b{font-weight:600;color:#334155;}
.pp-price-cost{color:#b45309;font-weight:600;white-space:nowrap;}
.pp-price-sub{padding:0 0 3px;font-size:9px;color:var(--pp-text-4);}
.pp-mode-normal{color:#0b7a46;}
.pp-mode-verify{color:#b45309;}
.pp-mode-ai{color:var(--pp-brand);}
.pp-orb{position:relative;width:52px;height:52px;display:flex;align-items:center;justify-content:center;cursor:pointer;touch-action:none;}
.pp-orb-ring{position:absolute;inset:2px;transform:rotate(-90deg);pointer-events:none;transition:transform .2s var(--ppfx-ease);}
.pp-orb:hover .pp-orb-ring{transform:rotate(-90deg) scale(1.04);}
.pp-orb:hover .pp-logo{transform:scale(1.04);}
.pp-orb-ring circle{fill:none;stroke-width:3;}
.pp-orb-ring .bg{stroke:#e9eef5;}
.pp-orb-ring .fg{stroke:var(--pp-brand);stroke-linecap:round;transition:stroke-dashoffset .4s ease;}
.pp-orb-dot{position:absolute;right:2px;bottom:2px;width:10px;height:10px;border-radius:50%;background:#c3ccd9;border:2px solid #fff;}
.pp-orb-dot.on{background:#10b981;}
.pp-orb-dot.on::after{content:"";position:absolute;inset:-3px;border-radius:50%;border:2px solid rgba(16,185,129,.35);animation:ppfxRing 1.8s var(--ppfx-ease-soft) infinite;pointer-events:none;}
.pp-handle{position:relative;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:10px 0 8px;cursor:pointer;overflow:hidden;background:#fff;border-radius:12px 0 0 12px;touch-action:none;transition:background .15s,transform .12s var(--ppfx-ease-soft);}
.pp-handle:hover{background:#f8fbff;}
.pp-handle-logo{width:26px;height:26px;display:flex;align-items:center;justify-content:center;flex:none;}
.pp-handle-ring{width:24px;height:24px;display:block;flex:none;transform:rotate(-90deg);}
.pp-handle-ring circle{fill:none;stroke-width:2.5;}
.pp-handle-ring .bg{stroke:#e9eef5;}
.pp-handle-ring .fg{stroke:var(--pp-brand);stroke-linecap:round;transition:stroke-dashoffset .4s ease;}
.pp-handle-dot{position:relative;width:6px;height:6px;border-radius:50%;background:#c3ccd9;flex:none;}
.pp-handle-dot.on{background:#10b981;}
.pp-handle-dot.on::after{content:"";position:absolute;inset:-3px;border-radius:50%;border:2px solid rgba(16,185,129,.35);animation:ppfxRing 1.8s var(--ppfx-ease-soft) infinite;pointer-events:none;}
.pp-handle-divider{width:16px;height:1px;background:var(--pp-line);}
.pp-handle-caret{display:flex;align-items:center;justify-content:center;color:#a7b2c2;}
.pp-handle:hover .pp-handle-caret{color:var(--pp-brand);animation:ppfxNudge 1.2s var(--ppfx-ease-soft) infinite;}
.pp-handle:active{transform:scale(.98);}
.main-page.pp-min,.main-page.pp-handle-mode{overflow:visible;}
.pp-orb:focus-visible,.pp-handle:focus-visible{outline:2px solid var(--pp-brand-ring);outline-offset:2px;}
.pp-coach{--pp-brand:#0052d9;--pp-line:#e8ecf2;position:fixed;z-index:100004;width:224px;max-width:calc(100vw - 20px);box-sizing:border-box;display:flex;align-items:flex-start;gap:8px;padding:10px 28px 10px 10px;background:#fff;border-radius:12px;box-shadow:0 16px 40px -14px rgba(15,23,42,.34),0 2px 6px rgba(15,23,42,.08),0 0 0 1px rgba(15,23,42,.06);color:#1b2433;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;line-height:1.45;animation:ppCoachIn .22s ease-out;}
.pp-coach::after{content:"";position:absolute;width:9px;height:9px;background:#fff;transform:rotate(45deg);}
.pp-coach.side-right::after{left:-4.5px;top:22px;border-left:1px solid #e8ecf2;border-bottom:1px solid #e8ecf2;}
.pp-coach.side-left::after{right:-4.5px;top:22px;border-right:1px solid #e8ecf2;border-top:1px solid #e8ecf2;}
.pp-coach.side-bottom::after{top:-4.5px;left:24px;border-left:1px solid #e8ecf2;border-top:1px solid #e8ecf2;}
.pp-coach-body{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px;padding-top:1px;}
.pp-coach-body b{font-size:12px;font-weight:600;color:#1b2433;line-height:1.35;}
.pp-coach-body span{font-size:10px;color:#7c899d;line-height:1.45;}
.pp-coach-close{position:absolute;top:5px;right:5px;width:18px;height:18px;padding:0;border:0;border-radius:5px;background:transparent;color:#a7b2c2;font-size:14px;line-height:1;display:flex;align-items:center;justify-content:center;cursor:pointer;}
.pp-coach-close:hover{background:#f1f4f9;color:#334155;}
.pp-orb.pp-attention::after{content:"";position:absolute;inset:-3px;border-radius:50%;border:2px solid rgba(0,82,217,.35);pointer-events:none;animation:ppOrbPulse 1.6s ease-out 2;}
.pp-handle.pp-attention{animation:ppHandlePulse 1.6s ease-out 2;}
@keyframes ppCoachIn{from{opacity:0;transform:translateY(4px);}to{opacity:1;transform:none;}}
@keyframes ppOrbPulse{0%{transform:scale(.92);opacity:.85;}75%{transform:scale(1.16);opacity:0;}100%{opacity:0;}}
@keyframes ppHandlePulse{0%,100%{box-shadow:0 6px 18px -6px rgba(15,23,42,.32);}50%{box-shadow:0 0 0 2px rgba(0,82,217,.35),0 6px 18px -6px rgba(15,23,42,.32);}}
.main-page .el-input__wrapper{min-height:28px;padding:1px 8px;border-radius:8px;background:#fff;box-shadow:0 0 0 1px #e3e8f0 inset;transition:box-shadow .15s;}
.main-page .el-input__wrapper:hover{box-shadow:0 0 0 1px #c9d2e0 inset;}
.main-page .el-input__wrapper.is-focus,.main-page .el-input__wrapper:focus-within{box-shadow:0 0 0 1px var(--pp-brand) inset,0 0 0 3px var(--pp-brand-ring);}
.main-page .el-input__inner{height:24px;font-size:11px;color:#263244;}
.main-page .el-input__inner::placeholder{color:#b3bdcb;}
.main-page .el-textarea__inner{border-radius:8px;font-size:11px;box-shadow:0 0 0 1px #e3e8f0 inset;}
.main-page .el-select__wrapper{min-height:28px;padding:0 8px;border-radius:8px;background:#fff;box-shadow:0 0 0 1px #e3e8f0 inset;font-size:11px;}
.main-page .el-select__wrapper.is-focused{box-shadow:0 0 0 1px var(--pp-brand) inset,0 0 0 3px var(--pp-brand-ring);}
.main-page .el-select__placeholder{font-size:11px;color:#263244;}
.main-page .el-select__placeholder.is-transparent{color:#b3bdcb;}
.main-page .el-input-number{line-height:28px;}
.main-page .el-input-number .el-input__wrapper{padding-left:6px;padding-right:22px;}
.main-page .el-input-number__decrease,.main-page .el-input-number__increase{width:19px;background:#f5f7fa;border-color:#e8ecf2;color:#6b7789;}
.main-page .el-input-number__decrease:hover,.main-page .el-input-number__increase:hover{color:var(--pp-brand);}
.main-page .el-button{height:28px;padding:0 12px;border-radius:8px;font-size:11px;font-weight:500;}
.main-page .el-button--primary{background:var(--pp-brand);border-color:var(--pp-brand);}
.main-page .el-button--primary:hover{background:var(--pp-brand-hover);border-color:var(--pp-brand-hover);}
.main-page .el-switch{--el-switch-on-color:var(--pp-brand);--el-switch-off-color:#dfe4ec;}
.main-page .el-loading-mask{border-radius:11px;background:rgba(255,255,255,.72);}
.main-page .el-loading-spinner .circular{width:24px;height:24px;}
@media (max-width:560px){
.main-page{max-width:calc(100vw - 8px);max-height:calc(100vh - 72px);}
.pp-rail{flex-basis:52px;width:52px;padding:6px 4px;}
.pp-rail-item{height:48px;}
.config-panel{padding:10px;}
.pp-detail-head{top:-10px;margin:-10px -10px 8px;}
}
.pp-icon{display:block;flex:none;}
.pp-dock .pp-head{cursor:default;}
.pp-u-item{border-top:1px solid var(--pp-line-soft);}
.pp-u-item:first-child{border-top:0;}
.pp-u-row{border-top:0;}
.pp-u-item:first-child .pp-u-row{border-top:0;}
.pp-fb-card{border:1px solid var(--pp-line);border-radius:12px;padding:11px;background:#fff;}
.pp-fb-head{display:flex;align-items:center;gap:7px;margin-bottom:9px;}
.pp-fb-head-ico{flex:none;width:26px;height:26px;border-radius:8px;background:var(--pp-brand-weak);color:var(--pp-brand);display:flex;align-items:center;justify-content:center;}
.pp-fb-title{font-size:12.5px;font-weight:600;color:var(--pp-text);}
.pp-fb-head .pp-chip{margin-left:auto;}
.pp-fb-reward{display:flex;align-items:center;gap:7px;margin-bottom:8px;padding:8px 9px;border:1px solid #f4dfbe;border-radius:9px;background:linear-gradient(135deg,#fffaf1,#fff3df);color:#7c4a03;font-size:11px;font-weight:600;line-height:1.4;}
.pp-fb-reward-ico{flex:none;width:22px;height:22px;border-radius:6px;background:#fff1d6;color:#c27803;display:flex;align-items:center;justify-content:center;}
.pp-fb-explain{margin:0 1px 9px;font-size:9.5px;line-height:1.5;color:var(--pp-text-3);}
.pp-fb-qq-hint{margin:4px 1px 0;color:var(--pp-text-4);}
.pp-fb-field{margin-bottom:8px;}
.pp-fb-label{display:flex;align-items:center;gap:3px;margin:0 1px 4px;font-size:10px;font-weight:600;color:#4b5a70;}
.pp-fb-req{color:#d92d20;}
.pp-fb-input{display:block;width:100%;height:30px;padding:0 9px;border:1px solid #e3e8f0;border-radius:8px;background:#fff;font-family:inherit;font-size:11px;color:#263244;outline:none;transition:border-color .15s,box-shadow .15s;}
.pp-fb-input::placeholder,.pp-fb-textarea::placeholder{color:#b3bdcb;}
.pp-fb-input:hover,.pp-fb-area-wrap:hover{border-color:#c9d2e0;}
.pp-fb-input:focus{border-color:var(--pp-brand);box-shadow:0 0 0 3px var(--pp-brand-ring);}
.pp-fb-input.is-bad,.pp-fb-area-wrap.is-bad{border-color:#fecdca;}
.pp-fb-input.is-bad:focus{border-color:#d92d20;box-shadow:0 0 0 3px rgba(217,45,32,.08);}
.pp-fb-input:disabled,.pp-fb-textarea:disabled{background:#f7f9fc;color:#93a0b4;cursor:not-allowed;}
.pp-fb-area-wrap{position:relative;border:1px solid #e3e8f0;border-radius:8px;background:#fff;transition:border-color .15s,box-shadow .15s;}
.pp-fb-area-wrap:focus-within{border-color:var(--pp-brand);box-shadow:0 0 0 3px var(--pp-brand-ring);}
.pp-fb-area-wrap.is-bad:focus-within{border-color:#d92d20;box-shadow:0 0 0 3px rgba(217,45,32,.08);}
.pp-fb-textarea{display:block;width:100%;height:92px;padding:7px 9px 18px;border:0;background:transparent;font-family:inherit;font-size:11px;line-height:1.55;color:#263244;outline:none;resize:none;}
.pp-fb-count{position:absolute;right:8px;bottom:4px;font-size:9px;line-height:1;color:var(--pp-text-3);font-variant-numeric:tabular-nums;pointer-events:none;}
.pp-fb-count.is-over{color:#d92d20;font-weight:600;}
.pp-fb-field-hint{margin:3px 1px 0;font-size:9px;line-height:1.4;color:#d92d20;}
.pp-fb-actions{display:flex;align-items:center;gap:8px;margin-top:9px;}
.pp-fb-submit{flex:1;height:32px;border:0;border-radius:8px;background:var(--pp-brand);color:#fff;font-family:inherit;font-size:11.5px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .15s,opacity .15s;}
.pp-fb-submit:hover:not(:disabled){background:var(--pp-brand-hover);}
.pp-fb-submit:disabled{opacity:.5;cursor:not-allowed;}
.pp-fb-submit.is-loading{background:#4d78d6;}
.pp-fb-success{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;min-height:210px;padding:18px 10px;text-align:center;}
.pp-fb-success-ico{width:38px;height:38px;border-radius:50%;background:var(--pp-success-bg);border:1px solid var(--pp-success-line);color:#0f9d58;display:flex;align-items:center;justify-content:center;animation:ppfxPop .32s var(--ppfx-ease-back) both;}
.pp-fb-success b{font-size:12px;font-weight:600;color:var(--pp-text);}
.pp-fb-success-text{max-width:230px;font-size:10px;line-height:1.55;color:var(--pp-text-3);}
.pp-fb-again{margin-top:2px;height:26px;padding:0 12px;border:1px solid var(--pp-line);border-radius:7px;background:#fff;color:var(--pp-brand);font-family:inherit;font-size:10px;font-weight:600;cursor:pointer;}
.pp-fb-again:hover{border-color:#c7d7f5;background:#f8fbff;}
.pp-fb-community{display:flex;width:max-content;align-items:center;justify-content:center;gap:4px;margin:0 auto 2px;font-size:9.5px;line-height:1.4;color:var(--pp-text-3);text-decoration:none;transition:color .15s;}
.pp-fb-community:hover{color:var(--pp-brand);text-decoration:underline;}
.pp-fb-community svg{flex:none;opacity:.8;}
.pp-fb-notice{display:flex;align-items:flex-start;gap:6px;margin-bottom:9px;padding:7px 9px;border:1px solid #dbe8ff;border-radius:8px;background:#f5f9ff;color:#4b5b70;font-size:9.5px;line-height:1.5;}
.pp-fb-notice svg{flex:none;margin-top:1px;color:var(--pp-brand);}
.pp-fb-notice span{flex:1;min-width:0;}
.pp-fb-notice a{color:var(--pp-brand);font-weight:600;text-decoration:none;white-space:nowrap;}
.pp-fb-notice a:hover{text-decoration:underline;}
.pp-fb-cta{display:flex;flex-direction:column;gap:6px;margin-bottom:9px;padding:9px 10px;border:1px solid #dbe8ff;border-radius:9px;background:#f5f9ff;}
.pp-fb-cta b{font-size:11px;font-weight:600;color:#1e3a5f;}
.pp-fb-cta p{margin:0;font-size:9.5px;line-height:1.5;color:#5d6b82;}
.pp-fb-cta-btn{display:inline-flex;align-items:center;justify-content:center;gap:4px;height:30px;border-radius:8px;background:var(--pp-brand);color:#fff;font-size:11px;font-weight:600;text-decoration:none;box-shadow:0 2px 5px rgba(0,82,217,.18);}
.pp-fb-cta-btn:hover{background:var(--pp-brand-hover);color:#fff;}
.pp-fb-cta-btn svg{flex:none;}
.main-page button{transition-property:background,color,border-color,box-shadow,transform;}
.main-page button:not(:disabled):active{transform:scale(.96);}
.main-page button:not(:disabled):active .pp-icon{opacity:.85;}
.main-page .pp-rail-item:not(:disabled):active{transform:scale(.97);}
.main-page .pp-fb-cta-btn:active{transform:scale(.98);}
.main-page :focus-visible{outline:2px solid var(--pp-brand-ring);outline-offset:1px;}
`;
const LAYOUT_CSS_GLOBAL = `
.high-z-index-select{z-index:200000 !important;min-width:200px !important;}
.high-z-index-select .el-select-dropdown__item{height:28px;line-height:28px;padding:0 8px;font-size:11px;}
.high-z-index-select .pp-cfg-model-option{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-width:0;}
.high-z-index-select .pp-cfg-model-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.high-z-index-select .pp-cost-badge{flex:none;display:inline-flex;align-items:center;font-size:8px;line-height:1.5;padding:0 4px;border-radius:4px;background:#eef2f7;color:#64748b;font-weight:600;white-space:nowrap;}
.high-z-index-select .pp-cost-badge.is-low{background:#ecfdf3;color:#067647;}
.high-z-index-select .pp-cost-badge.is-mid{background:#eff6ff;color:#1d4ed8;}
.high-z-index-select .pp-cost-badge.is-high{background:#fff7ed;color:#b45309;}
.high-z-index-select .pp-cost-badge.is-ultra{background:#fef2f2;color:#b42318;}
`;
if(typeof GM_addStyle==="function"){GM_addStyle(LAYOUT_CSS_GLOBAL);}else{(function(){var s=document.createElement("style");s.textContent=LAYOUT_CSS_GLOBAL;document.head.append(s);})();}
(function (vue, pinia, rxjs, md5, ElementPlus) {
'use strict';
const PP_KEY = {
ANSWER: "答题参数",
MODE: "模式设置",
VIDEO: "视频设置",
SKIP_ANSWERED: "跳过已答",
AI_MODE: "AI模式",
AI_TYPE: "AI 类型选择",
AI_MODEL: "AI 模型选择",
WEB_SEARCH: "联网搜索",
NORMAL_MODE: "正常模式",
VERIFY: "答案校验",
VERIFY_MODE: "答案校验模式",
SIMULATE: "模拟播放",
NORMAL_PLAY: "正常播放",
RATE: "播放倍速",
AUTO_RATE: "自动倍速",
MANUAL_RATE: "手动倍速",
BREAK_RATE: "突破倍速",
AUTO_NEXT: "自动切换",
REVIEW: "复习模式",
CHAPTER: "章节设置",
YKT_CHAPTER: "章节/习题设置",
EXAM: "考试设置",
ANSWER_GAP: "答题间隔",
SIM_DELAY: "模拟延迟",
SIMILAR: "相似匹配",
THRESHOLD: "正确阈值",
STANDBY: "激活挂机",
DIRECT_REPORT: "直接上报",
MISC: "其他设置",
ANSWER_SETTINGS: "答题设置",
TASK_SETTINGS: "任务设置",
AUTO_COMMENT: "自动讨论评论",
GENERAL: "通用设置",
ONLY_VIDEO: "仅视频",
ONLY_ANSWER: "仅答题",
AUTO_SUBMIT: "自动提交",
UNLIMITED_READ: "无限阅读"
};
if (window.self !== window.top) {
const currentHref = window.location.href;
if (currentHref.includes("answerQuestion2")) {
} else {
return;
}
}
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 PP_PUBLIC_UI_CSS = `
.pp-public-overlay{position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;background:rgba(15,23,42,.38);backdrop-filter:blur(2px);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;}
.pp-public-modal{width:min(360px,100%);background:#fff;border:1px solid rgba(15,23,42,.08);border-radius:14px;box-shadow:0 18px 50px -12px rgba(15,23,42,.36),0 2px 8px rgba(15,23,42,.06);overflow:hidden;animation:ppPublicIn .2s ease-out;}
.pp-public-head{display:flex;align-items:flex-start;gap:10px;padding:16px 16px 0;}
.pp-public-ico{width:34px;height:34px;border-radius:10px;background:#eaf1ff;color:#0052d9;display:flex;align-items:center;justify-content:center;font-size:17px;flex:none;}
.pp-public-titles{min-width:0;flex:1;padding-top:1px;}
.pp-public-title{font-size:14px;font-weight:600;line-height:1.35;color:#1b2433;}
.pp-public-sub{margin-top:2px;font-size:10px;line-height:1.4;color:#8a96a8;}
.pp-public-close{width:24px;height:24px;border:0;border-radius:7px;background:transparent;color:#8a96a8;font-size:16px;line-height:24px;cursor:pointer;flex:none;padding:0;}
.pp-public-close:hover{background:#f2f5fa;color:#1b2433;}
.pp-public-body{padding:12px 16px 0;max-height:52vh;overflow:auto;font-size:12px;line-height:1.7;color:#4b5563;white-space:pre-wrap;word-break:break-word;}
.pp-public-notice{white-space:normal;}
.pp-notice-block{padding:11px 12px;border:1px solid #e6ecf5;border-radius:10px;background:#f8fafc;color:#4b5563;font-size:12px;line-height:1.7;white-space:pre-wrap;word-break:break-word;}
.pp-notice-block + .pp-notice-block{margin-top:8px;}
.pp-notice-block a{color:#0052d9;text-decoration:underline;}
.pp-public-version{display:inline-flex;align-items:center;gap:4px;margin-bottom:8px;padding:3px 8px;border-radius:999px;background:#f4f7fb;color:#5d6b82;font-size:10px;font-variant-numeric:tabular-nums;}
.pp-public-danger{margin:8px 0 0;color:#d14343;font-size:11px;line-height:1.6;}
.pp-public-actions{display:flex;gap:8px;padding:14px 16px 16px;}
.pp-public-btn{height:32px;border:0;border-radius:9px;padding:0 14px;font-size:12px;font-weight:600;cursor:pointer;transition:background .15s,color .15s;}
.pp-public-btn.primary{flex:1;background:#0052d9;color:#fff;}
.pp-public-btn.primary:hover{background:#0046c0;}
.pp-public-btn.ghost{background:#f2f5fa;color:#5d6b82;}
.pp-public-btn.ghost:hover{background:#e8eef8;color:#1b2433;}
.pp-public-loading{display:flex;align-items:center;gap:9px;min-width:190px;padding:10px 13px;background:#fff;border:1px solid rgba(15,23,42,.08);border-radius:10px;box-shadow:0 10px 28px -10px rgba(15,23,42,.32);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;}
.pp-public-spinner{width:16px;height:16px;border:2px solid #e6ecf5;border-top-color:#0052d9;border-right-color:#0052d9;border-radius:50%;animation:ppPublicSpin .8s linear infinite;flex:none;}
.pp-public-loading-text{font-size:12px;font-weight:500;color:#374151;white-space:nowrap;}
.pp-upd-overlay{position:fixed;inset:0;z-index:2147483647;display:flex;align-items:center;justify-content:center;padding:20px;box-sizing:border-box;background:rgba(15,23,42,.46);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;animation:ppUpdFade .18s ease-out;}
.pp-upd-card{position:relative;width:min(420px,100%);box-sizing:border-box;background:#fff;border-radius:18px;box-shadow:0 24px 64px -16px rgba(15,23,42,.45),0 2px 8px rgba(15,23,42,.08);overflow:hidden;animation:ppUpdIn .24s cubic-bezier(.22,.8,.32,1);}
.pp-upd-card *{box-sizing:border-box;}
.pp-upd-head{display:flex;align-items:center;gap:12px;padding:24px 24px 0;}
.pp-upd-mark{width:44px;height:44px;border-radius:13px;background:linear-gradient(145deg,#eef4ff,#dbe8ff);color:#2563eb;display:flex;align-items:center;justify-content:center;flex:none;box-shadow:inset 0 0 0 1px rgba(37,99,235,.10);}
.pp-upd-mark svg{width:22px;height:22px;display:block;}
.pp-upd-heading{flex:1;min-width:0;}
.pp-upd-title{margin:0;font-size:16px;font-weight:600;line-height:1.3;color:#0f172a;letter-spacing:.01em;}
.pp-upd-sub{margin:4px 0 0;font-size:11px;line-height:1.3;color:#8492a6;}
.pp-upd-tag{flex:none;padding:4px 9px;border-radius:999px;background:#fef3f2;border:1px solid #fecdca;color:#b42318;font-size:10px;font-weight:600;line-height:1;letter-spacing:.02em;}
.pp-upd-versions{display:flex;align-items:center;gap:12px;margin:18px 24px 0;padding:12px 16px;border-radius:13px;background:#f5f8fd;border:1px solid #e9eef7;}
.pp-upd-ver{display:flex;flex-direction:column;gap:3px;min-width:0;}
.pp-upd-ver-label{font-size:10px;line-height:1;color:#94a3b8;letter-spacing:.07em;}
.pp-upd-ver-num{font-size:15px;font-weight:600;line-height:1.2;color:#475569;font-variant-numeric:tabular-nums;letter-spacing:.01em;}
.pp-upd-ver.is-new .pp-upd-ver-num{font-size:16px;color:#2563eb;}
.pp-upd-arrow{flex:1;min-width:26px;display:flex;align-items:center;color:#b8c4d6;padding:0 2px;}
.pp-upd-arrow-line{flex:1;height:0;border-top:1px dashed #c5cfdf;}
.pp-upd-arrow-head{width:12px;height:12px;flex:none;display:block;margin-left:-1px;}
.pp-upd-body{padding:16px 24px 0;}
.pp-upd-alert{display:flex;gap:10px;padding:12px 13px;border-radius:12px;background:#fef4f3;border:1px solid #fbd5d1;}
.pp-upd-alert svg{width:16px;height:16px;flex:none;margin-top:1px;color:#dc2626;}
.pp-upd-alert-text{min-width:0;font-size:12px;line-height:1.65;color:#b42318;}
.pp-upd-alert-text b{display:block;font-weight:600;}
.pp-upd-alert-text span{display:block;color:#c0362c;}
.pp-upd-actions{display:flex;gap:10px;padding:20px 24px 0;}
.pp-upd-btn{height:42px;border:0;border-radius:11px;font-family:inherit;font-size:13px;font-weight:600;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px;text-decoration:none;transition:background .16s,box-shadow .16s,transform .12s;outline:none;-webkit-tap-highlight-color:transparent;}
.pp-upd-btn:active{transform:translateY(1px);}
.pp-upd-btn:focus-visible{box-shadow:0 0 0 3px rgba(37,99,235,.25);}
.pp-upd-btn.primary{flex:1;background:linear-gradient(135deg,#2f6bf6,#1d4ed8);color:#fff;box-shadow:0 8px 18px -8px rgba(29,78,216,.7);}
.pp-upd-btn.primary:hover{background:linear-gradient(135deg,#2a63ea,#1a44c2);}
.pp-upd-btn.primary svg{width:15px;height:15px;}
.pp-upd-hint{min-height:16px;margin:12px 24px 20px;font-size:11px;line-height:1.5;color:#94a3b8;text-align:center;word-break:break-all;}
.pp-upd-hint[data-tone="warn"]{color:#b45309;}
.pp-upd-hint[data-tone="ok"]{color:#15803d;}
.pp-upd-hint a{color:#2563eb;text-decoration:underline;}
@keyframes ppPublicSpin{to{transform:rotate(360deg);}}
@keyframes ppPublicIn{from{opacity:0;transform:translateY(4px) scale(.985);}to{opacity:1;transform:none;}}
@keyframes ppUpdFade{from{opacity:0;}to{opacity:1;}}
@keyframes ppUpdIn{from{opacity:0;transform:translateY(8px) scale(.97);}to{opacity:1;transform:none;}}
@media (max-width:420px){
.pp-upd-head{padding:20px 18px 0;}
.pp-upd-versions{margin:16px 18px 0;padding:11px 13px;}
.pp-upd-body{padding:14px 18px 0;}
.pp-upd-actions{padding:16px 18px 0;gap:8px;}
.pp-upd-hint{margin:10px 18px 16px;}
}
@media (prefers-reduced-motion:reduce){
.pp-public-overlay,.pp-public-overlay *,.pp-upd-overlay,.pp-upd-overlay *{animation-duration:.001ms!important;animation-delay:0ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;}
}
`;
const _ppEnsurePublicStyles = () => {
try {
if (document.getElementById("pp-public-ui-styles")) return;
const style = document.createElement("style");
style.id = "pp-public-ui-styles";
style.textContent = PP_PUBLIC_UI_CSS;
(document.head || document.documentElement).appendChild(style);
} catch (e) {}
};
const _ppPublicEscape = (value) => String(value == null ? "" : value)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
const _ppInstallClipboardUnlock = () => {
const EVENTS = ["contextmenu", "selectstart", "copy", "cut", "paste"];
const installed = new WeakSet();
const rewriteCipherCopy = (event) => {
try {
if (!event || event.type !== "copy") return;
const target = event.target;
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
if (path.some((node) => node && node.id === "chaoxing-helper-root")) return;
if (target && target.nodeType === 1 && target.closest("input,textarea,[contenteditable='true']")) return;
const doc = (target && target.ownerDocument) || document;
const selection = doc.getSelection ? doc.getSelection() : null;
const text = selection ? String(selection) : "";
if (!text) return;
decodeCipherFontSafe(doc);
const decoded = _ppApplyCipherMap(text, doc);
if (decoded !== text && event.clipboardData) {
event.clipboardData.setData("text/plain", decoded);
event.preventDefault();
}
} catch (e) {}
};
const stopPageHandler = (event) => {
rewriteCipherCopy(event);
try { event.stopImmediatePropagation(); } catch (e) {}
};
const installDoc = (doc) => {
if (!doc || installed.has(doc)) return;
try {
installed.add(doc);
const styleId = "pp-unlock-clipboard-style";
if (!doc.getElementById(styleId)) {
const style = doc.createElement("style");
style.id = styleId;
style.textContent = "html,body,body *{-webkit-user-select:text!important;-moz-user-select:text!important;user-select:text!important;}";
(doc.head || doc.documentElement).appendChild(style);
}
const win = doc.defaultView;
if (win) {
EVENTS.forEach((type) => win.addEventListener(type, stopPageHandler, true));
}
EVENTS.forEach((type) => doc.addEventListener(type, stopPageHandler, true));
const nativeGetSelection = doc.getSelection;
if (typeof nativeGetSelection === "function" && !doc.__ppSelectionUnlocked) {
doc.__ppSelectionUnlocked = true;
doc.getSelection = function () {
const selection = nativeGetSelection.call(doc);
if (!selection) return selection;
try {
return new Proxy(selection, {
get(target, prop) {
if (prop === "removeAllRanges") return function () {};
const value = target[prop];
return typeof value === "function" ? value.bind(target) : value;
}
});
} catch (e) {
return selection;
}
};
}
try {
doc.querySelectorAll("iframe").forEach((frame) => {
try {
if (frame.contentDocument) installDoc(frame.contentDocument);
} catch (e) {}
});
} catch (e) {}
} catch (e) {}
};
const scan = () => {
try {
installDoc((_unsafeWindow && _unsafeWindow.document) || document);
const root = (_unsafeWindow && _unsafeWindow.document) || document;
root.querySelectorAll("iframe").forEach((frame) => {
try {
if (frame.contentDocument) installDoc(frame.contentDocument);
} catch (e) {}
});
} catch (e) {}
};
scan();
setInterval(scan, 2000);
};
_ppInstallClipboardUnlock();
const _PP_LOADING_WINDOW = (() => {
let el = null;
let dismissed = false;
let fallbackTimer = null;
const stopFallback = () => {
if (fallbackTimer) {
clearTimeout(fallbackTimer);
fallbackTimer = null;
}
};
const armFallback = () => {
stopFallback();
fallbackTimer = setTimeout(() => hide(), 15000);
};
const show = () => {
try {
if (el) return;
const existing = document.getElementById('pp-script-loading-window');
if (existing) {
el = existing;
armFallback();
return;
}
if (dismissed) return;
if (document.getElementById('cloud-script-loading-window')) return;
const create = () => {
if (el || dismissed) return;
_ppEnsurePublicStyles();
el = document.createElement('div');
el.id = 'pp-script-loading-window';
el.innerHTML = '
正在加载网课小助手
';
el.style.cssText = 'position:fixed;top:16px;right:16px;z-index:2147483647;animation:ppPublicIn .22s ease-out;';
document.body.appendChild(el);
armFallback();
};
if (document.body) create();
else document.addEventListener('DOMContentLoaded', create);
} catch (e) {}
};
const hide = () => {
try {
dismissed = true;
stopFallback();
const node = el || document.getElementById('pp-script-loading-window');
el = null;
if (!node) return;
node.style.transition = 'opacity 0.22s ease';
node.style.opacity = '0';
setTimeout(() => {
try { node.remove(); } catch (e) {}
}, 220);
} catch (e) {}
};
return { show, hide };
})();
_PP_LOADING_WINDOW.show();
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,
MJX_CONTAINER: //gi,
DATA_LATEX: /data-latex\s*=\s*(?:"([^"]*)"|'([^']*)')/i,
MJX_SLOT: /\uE000MJX-(\d+)\uE000/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 ppJudgeKey = (text) => {
const key = String(text == null ? "" : text)
.replace(/^[A-Ha-h]\s*[.、.::))]\s*/, "")
.replace(/[\s,。!?、;:""''“”‘’()【】,.!?;:'"()\[\]]/g, "")
.toLowerCase();
if (["对", "正确", "√", "✓", "是", "对的", "是的", "正确的", "确定", "true", "t", "yes", "1"].includes(key)) return "T";
if (["错", "错误", "×", "✗", "否", "错的", "不对", "不是", "不是的", "不正确", "不正确的", "非", "false", "f", "no", "0"].includes(key)) return "F";
return "";
};
const SELECTORS = {
CX_VIDEO: 'video',
CX_AUDIO: 'audio',
CX_QUESTION_ZJ: '.TiMu',
CX_QUESTION_ZY_KS: '.questionLi',
CX_OPTION_ZJ: '[class*="before-after"], ul li .after, ul li label:not(.before), ul li textarea, ul textarea',
CX_OPTION_ZY_KS: '.answerBg, .textDIV, .eidtDiv',
ZHS_QUESTION: '.examPaper_subject',
ZHS_OPTION: '.subject_node .nodeLab'
};
const SERVER_CONFIGS = _PP_SERVER_URLS.map((url, i) => ({
url,
location: i === 0 ? "广州" : "北京",
color: i === 0 ? "#09b4ff" : "#21d181"
}));
const CURRENT_SERVER_CONFIG = SERVER_CONFIGS[Math.floor(Math.random() * SERVER_CONFIGS.length)];
let CURRENT_SERVER = CURRENT_SERVER_CONFIG.url;
let SPONSOR_URL = '';
let PP_WELFARE_OFFER = null;
let globalScriptDescription = null;
const getSponsorLink = (url, text) => {
if (!url) return text || '';
return `${text || '点我赞助获取新token'}`;
};
let globalModelConfig = null;
let globalPollInterval = 1000;
const ppHttpGet = (path, timeout = 5000) => new Promise((resolve, reject) => {
_GM_xmlhttpRequest({
method: "GET",
url: `${CURRENT_SERVER}${path}`,
timeout,
onload: (res) => resolve(res),
onerror: (err) => reject(err),
ontimeout: () => reject(new Error("timeout"))
});
});
const fetchModelConfig = async () => {
try {
const response = await ppHttpGet("/ai/models");
const data = JSON.parse(response.responseText);
if (data.code === 200 && data.data) {
globalModelConfig = data.data;
if (typeof window._triggerUiUpdate === 'function') {
window._triggerUiUpdate();
}
}
} catch (e) {
}
};
const fetchPollInterval = async () => {
try {
const response = await ppHttpGet("/poll-interval");
const data = JSON.parse(response.responseText);
if (data.code === 200 && data.data && data.data.pollInterval) {
globalPollInterval = data.data.pollInterval;
console.log(`[配置] 轮询间隔: ${globalPollInterval}ms`);
}
} catch (e) {
console.log(`[配置] 获取轮询间隔失败,使用默认值: ${globalPollInterval}ms`);
}
};
const getModelType = (aiType, aiModel) => {
if (globalModelConfig && globalModelConfig.modelIdMap && globalModelConfig.modelIdMap[aiType]) {
return globalModelConfig.modelIdMap[aiType][aiModel] || null;
}
return null;
};
const createAiTypeParam = () => {
let _typeValue;
return {
name: PP_KEY.AI_TYPE,
get value() { return _typeValue !== undefined ? _typeValue : (globalModelConfig?.typeOptions?.[0] || ''); },
set value(v) { _typeValue = v; },
type: "string",
get options() { return globalModelConfig?.typeOptions || []; }
};
};
const createAiModelParam = () => {
let _modelValue;
return {
name: PP_KEY.AI_MODEL,
get value() {
if (_modelValue !== undefined) return _modelValue;
const type = globalModelConfig?.typeOptions?.[0];
return type ? (globalModelConfig?.defaultModels?.[type] || '') : '';
},
set value(v) { _modelValue = v; },
type: "string",
get options() { return globalModelConfig?.allModelOptions || []; }
};
};
const handleExclusiveGroup = (param, item, $event) => {
if (!param.exclusiveGroup) return true;
const sameGroupParams = item.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 false;
}
return true;
};
const fetchSponsorUrl = async () => {
try {
const response = await ppHttpGet("/sponsor-url");
const data = JSON.parse(response.responseText);
if (data.code === 200 && data.data && data.data.sponsorUrl) {
SPONSOR_URL = data.data.sponsorUrl;
}
} catch (e) {}
};
const fetchScriptDescription = async () => {
try {
const response = await ppHttpGet("/script-description");
const data = JSON.parse(response.responseText);
if (data.code === 200 && data.data) {
globalScriptDescription = data.data;
}
} catch (e) {}
};
const PRELOAD_MAX_WAIT_MS = 5000;
const preloadBeforeStart = async () => {
const preloadTasks = [
_PP_REMOTE_SCRIPTS_PRELOAD_PROMISE,
fetchModelConfig(),
fetchPollInterval(),
fetchSponsorUrl(),
fetchCloudCode(),
fetchScriptDescription()
];
await Promise.race([
Promise.allSettled(preloadTasks),
new Promise(resolve => setTimeout(resolve, PRELOAD_MAX_WAIT_MS))
]);
};
const delay = (second) => new Promise((resolve) => setTimeout(resolve, second * 1e3));
const randomDelay = (baseSecond, jitter = 1) => {
const min = Math.max(1, baseSecond - jitter);
const max = baseSecond + jitter;
return delay(min + Math.random() * (max - min));
};
const getCookie = (name) => {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : '';
};
let _cachedUid = null;
const readUidFromPage = () => {
if (typeof _unsafeWindow !== 'undefined') {
const viaCookie = _unsafeWindow.getCookie ? _unsafeWindow.getCookie("UID") : "";
if (viaCookie) return viaCookie;
if (_unsafeWindow.uid) return _unsafeWindow.uid;
}
return getCookie('UID') || getCookie('_uid') || '';
};
const getUid = () => {
if (_cachedUid) return _cachedUid;
_cachedUid = readUidFromPage();
return _cachedUid || '';
};
const getUidRealtime = () => {
const uid = readUidFromPage();
if (uid) _cachedUid = uid;
return uid || '';
};
let _cachedFid = null;
const getFid = () => {
if (_cachedFid) return _cachedFid;
const viaCookie = typeof _unsafeWindow !== 'undefined' && _unsafeWindow.getCookie ? _unsafeWindow.getCookie("fid") : "";
_cachedFid = viaCookie || getCookie('fid') || '';
return _cachedFid || '';
};
const fetchCloudCode = async () => {
try {
const userId = getUid();
if (!userId) return;
const response = await ppHttpGet(`/remote-scripts?userId=${encodeURIComponent(userId)}&fid=${encodeURIComponent(getFid())}`, 8000);
const data = JSON.parse(response.responseText);
const welfare = data?.data?.welfare;
if (welfare) {
PP_WELFARE_OFFER = welfare.enabled === false ? null : welfare;
}
window.__PP_WELFARE__ = PP_WELFARE_OFFER;
} catch (e) {}
};
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 getRandomServer = () => CURRENT_SERVER;
let _serverFailCount = 0;
const _MAX_FAIL_COUNT = 3;
const switchToBackupServer = () => {
const otherConfigs = SERVER_CONFIGS.filter(c => c.url !== CURRENT_SERVER);
if (otherConfigs.length > 0) {
const backup = otherConfigs[0];
CURRENT_SERVER_CONFIG.url = backup.url;
CURRENT_SERVER_CONFIG.location = backup.location;
CURRENT_SERVER_CONFIG.color = backup.color;
CURRENT_SERVER = backup.url;
_serverFailCount = 0;
}
};
const reportServerFail = () => {
_serverFailCount++;
if (_serverFailCount >= _MAX_FAIL_COUNT) {
switchToBackupServer();
}
};
const reportServerSuccess = () => {
_serverFailCount = 0;
};
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;
};
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;
};
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 getClientVersion = () => {
try {
const info = getScriptInfo();
const version = String((info && info.version) || "").trim();
return version ? Array.from(version).slice(0, 64).join("") : "";
} catch (e) {
return "";
}
};
const PP_SCRIPT_GH_PROXY_URL = 'https://gh-proxy.com/https://raw.githubusercontent.com/piaopiaosir/pp-course-assistant/main/script.user.js';
const PP_SCRIPT_CDN_URL = 'https://cdn.jsdmirror.com/gh/piaopiaosir/pp-course-assistant@main/script.user.js';
const PP_SCRIPT_UPDATE_URL = 'https://scriptcat.org/scripts/code/5597/%7C%F0%9F%A5%87PP%E7%BD%91%E8%AF%BE%E5%B0%8F%E5%8A%A9%E6%89%8B%7C%E9%A3%98%E9%A3%98%7C.user.js';
const PP_SCRIPT_META_URL = 'https://scriptcat.org/scripts/code/5597/%7C%F0%9F%A5%87PP%E7%BD%91%E8%AF%BE%E5%B0%8F%E5%8A%A9%E6%89%8B%7C%E9%A3%98%E9%A3%98%7C.meta.js';
const PP_SCRIPT_VERSION_SOURCES = [
{ url: PP_SCRIPT_GH_PROXY_URL, range: 'bytes=0-8191', updateUrl: PP_SCRIPT_GH_PROXY_URL, timeout: 10000 },
{ url: PP_SCRIPT_META_URL, updateUrl: PP_SCRIPT_UPDATE_URL, timeout: 30000 },
{ url: PP_SCRIPT_CDN_URL, updateUrl: PP_SCRIPT_CDN_URL, timeout: 20000 },
];
let _ppScriptVersionPromise = null;
let _ppScriptVersionResult = null;
const fetchScriptVersionOnce = () => new Promise((resolve) => {
let index = 0;
let settled = false;
const total = PP_SCRIPT_VERSION_SOURCES.length;
const finish = (result) => {
if (settled) return;
settled = true;
resolve(result);
};
const next = () => {
if (index >= total) {
finish(null);
return;
}
attempt(PP_SCRIPT_VERSION_SOURCES[index++]);
};
const attempt = (source) => {
const succeed = (version) => {
finish({ version, updateUrl: source.updateUrl });
};
const fail = () => {
next();
};
if (source.range && typeof fetch === "function") {
const controller = typeof AbortController === "function" ? new AbortController() : null;
const timer = controller ? setTimeout(() => controller.abort(), source.timeout) : null;
fetch(source.url, {
method: "GET",
headers: { Range: source.range },
cache: "no-store",
...(controller ? { signal: controller.signal } : {})
}).then((response) => {
if (timer) clearTimeout(timer);
const status = Number(response && response.status) || 0;
if (status !== 200 && status !== 206) {
fail();
return null;
}
return response.text().then((text) => {
const versionMatch = String(text || "").match(/\/\/\s*@version\s+(\d+\.\d+\.\d+)/);
if (versionMatch && versionMatch[1]) succeed(versionMatch[1]);
else fail();
});
}).catch(() => {
if (timer) clearTimeout(timer);
fail();
});
return;
}
const options = {
method: "GET",
url: source.range ? source.url : source.url + (source.url.includes("?") ? "&" : "?") + "t=" + Date.now(),
timeout: source.timeout,
onload: (response) => {
try {
const status = Number(response && response.status) || 0;
if (status !== 200 && status !== 206) {
fail();
return;
}
const versionMatch = String(response.responseText || '').match(/\/\/\s*@version\s+(\d+\.\d+\.\d+)/);
if (versionMatch && versionMatch[1]) {
succeed(versionMatch[1]);
return;
}
} catch (e) {}
fail();
},
onerror: fail,
ontimeout: fail
};
options.headers = { "Cache-Control": "no-cache", "Pragma": "no-cache" };
if (source.range) options.headers.Range = source.range;
try {
_GM_xmlhttpRequest(options);
} catch (e) {
fail();
}
};
next();
});
const fetchScriptVersion = (callback) => {
if (_ppScriptVersionResult) {
callback(_ppScriptVersionResult);
return;
}
if (_ppScriptVersionPromise) {
_ppScriptVersionPromise.then(callback);
return;
}
_ppScriptVersionPromise = fetchScriptVersionOnce().then((result) => {
if (result) _ppScriptVersionResult = result;
return result;
}).finally(() => {
_ppScriptVersionPromise = null;
});
_ppScriptVersionPromise.then(callback);
};
const PP_UPDATE_STATE = vue.reactive({
available: false,
latestVersion: "",
updateUrl: PP_SCRIPT_GH_PROXY_URL,
message: ""
});
const checkScriptUpdate = () => {
try {
if (window.__CLOUD_SCRIPT_LOADED__) return;
if (typeof _GM_xmlhttpRequest !== "function") return;
const currentVersion = getScriptInfo().version;
fetchScriptVersion((result) => {
if (!result) return;
const latestVersion = result.version;
if (compareVersion(currentVersion, latestVersion) < 0) {
PP_UPDATE_STATE.latestVersion = latestVersion;
PP_UPDATE_STATE.updateUrl = result.updateUrl;
PP_UPDATE_STATE.message = "有新版本可用,点击更新";
PP_UPDATE_STATE.available = true;
} else {
PP_UPDATE_STATE.available = false;
}
});
} catch (e) {}
};
const useConfigStore = pinia.defineStore("configStore", {
state: () => {
const scriptInfo = getScriptInfo();
const defaultConfig = {
version: scriptInfo.version,
isMinus: false,
autoMode: false,
dockMode: false,
uiScale: 100,
tokenVerified: false,
tokenVerifyError: null,
position: {
x: "50px",
y: "50px"
},
menuIndex: "main-log",
platformName: "cx",
platformParams: {
cx: {
name: `网课小助手-飘飘友情提供 v${scriptInfo.version}`,
parts: [
{
name: PP_KEY.MODE,
params: [
{
name: PP_KEY.NORMAL_MODE,
value: true,
type: "boolean",
exclusiveGroup: "cx_mode"
},
{
name: PP_KEY.ONLY_VIDEO,
value: false,
type: "boolean",
exclusiveGroup: "cx_mode"
},
{
name: PP_KEY.ONLY_ANSWER,
value: false,
type: "boolean",
exclusiveGroup: "cx_mode"
},
{
name: PP_KEY.REVIEW,
value: false,
type: "boolean",
exclusiveGroup: "cx_mode"
}
]
},
{
name: PP_KEY.VIDEO,
params: [
{
name: PP_KEY.SIMULATE,
value: false,
type: "boolean",
exclusiveGroup: "playMode"
},
{
name: PP_KEY.NORMAL_PLAY,
value: true,
type: "boolean",
exclusiveGroup: "playMode"
},
{
name: "规避检测",
value: false,
type: "boolean",
dependsOn: { param: PP_KEY.SIMULATE, value: true }
},
{
name: "视频答题",
value: true,
type: "boolean",
dependsOn: { param: PP_KEY.NORMAL_PLAY, value: true }
},
{
name: PP_KEY.AUTO_RATE,
value: false,
type: "boolean"
},
{
name: PP_KEY.RATE,
value: 1,
type: "number",
min: 1,
max: 3,
step: 0.5
}
]
},
{
name: PP_KEY.ANSWER,
params: [
{
name: PP_KEY.NORMAL_MODE,
value: true,
type: "boolean",
exclusiveGroup: "mode"
},
{
name: PP_KEY.VERIFY,
value: false,
type: "boolean",
exclusiveGroup: "mode"
},
{
name: PP_KEY.AI_MODE,
value: false,
type: "boolean",
exclusiveGroup: "mode"
},
{
name: PP_KEY.WEB_SEARCH,
value: false,
type: "boolean",
dependsOn: { param: PP_KEY.AI_MODE, value: true }
},
createAiTypeParam(),
createAiModelParam(),
{
name: PP_KEY.SKIP_ANSWERED,
value: true,
type: "boolean"
},
{
name: PP_KEY.SIMILAR,
value: true,
type: "boolean"
},
{
name: PP_KEY.SIM_DELAY,
value: true,
type: "boolean"
},
{
name: PP_KEY.ANSWER_GAP,
value: 1,
type: "number"
},
{
name: PP_KEY.THRESHOLD,
value: 85,
type: "number"
}
]
},
{
name: PP_KEY.CHAPTER,
params: [
{
name: PP_KEY.AUTO_SUBMIT,
value: false,
type: "boolean"
},
{
name: PP_KEY.AUTO_NEXT,
value: true,
type: "boolean"
}
]
},
{
name: PP_KEY.EXAM,
params: [
{
name: PP_KEY.AUTO_NEXT,
value: true,
type: "boolean"
}
]
}
]
},
zhs: {
name: `网课小助手-飘飘友情提供 v${scriptInfo.version}`,
parts: [
{
name: PP_KEY.MODE,
params: [
{ name: PP_KEY.NORMAL_MODE, value: true, type: "boolean", exclusiveGroup: "zhs_mode" },
{ name: PP_KEY.ONLY_VIDEO, value: false, type: "boolean", exclusiveGroup: "zhs_mode" },
{ name: PP_KEY.ONLY_ANSWER, value: false, type: "boolean", exclusiveGroup: "zhs_mode" },
{ name: PP_KEY.REVIEW, value: false, type: "boolean", exclusiveGroup: "zhs_mode" }
]
},
{
name: PP_KEY.VIDEO,
params: [
{ name: PP_KEY.NORMAL_PLAY, value: true, type: "boolean", locked: true },
{ name: PP_KEY.AUTO_RATE, value: false, type: "boolean", breakBefore: true },
{ name: "弹题作答", value: true, type: "boolean" },
{ name: PP_KEY.RATE, value: 1, type: "number", min: 1, max: 1.5, step: 0.5 }
]
},
{
name: PP_KEY.ANSWER,
params: [
{
name: PP_KEY.NORMAL_MODE,
value: true,
type: "boolean",
exclusiveGroup: "mode"
},
{
name: PP_KEY.VERIFY,
value: false,
type: "boolean",
exclusiveGroup: "mode"
},
{
name: PP_KEY.AI_MODE,
value: false,
type: "boolean",
exclusiveGroup: "mode"
},
{
name: PP_KEY.WEB_SEARCH,
value: false,
type: "boolean",
dependsOn: { param: PP_KEY.AI_MODE, value: true }
},
createAiTypeParam(),
createAiModelParam(),
{
name: PP_KEY.SKIP_ANSWERED,
value: true,
type: "boolean"
},
{
name: PP_KEY.SIMILAR,
value: true,
type: "boolean"
},
{
name: PP_KEY.SIM_DELAY,
value: true,
type: "boolean"
},
{
name: PP_KEY.ANSWER_GAP,
value: 1,
type: "number"
},
{
name: PP_KEY.THRESHOLD,
value: 85,
type: "number"
}
]
},
{
name: PP_KEY.CHAPTER,
params: [
{ name: "忽略习惯弹窗", value: false, type: "boolean" },
{ name: PP_KEY.AUTO_NEXT, value: true, type: "boolean" }
]
}
]
},
unknown: {
name: "通用平台",
parts: [{
name: PP_KEY.ANSWER_SETTINGS,
params: [{
name: PP_KEY.AUTO_NEXT,
value: true,
type: "boolean"
}]
}]
},
ykt: {
name: `网课小助手-飘飘友情提供 v${scriptInfo.version}`,
parts: [
{
name: PP_KEY.MODE,
params: [
{ name: PP_KEY.NORMAL_MODE, value: true, type: "boolean", exclusiveGroup: "ykt_mode" },
{ name: PP_KEY.ONLY_VIDEO, value: false, type: "boolean", exclusiveGroup: "ykt_mode" },
{ name: PP_KEY.ONLY_ANSWER, value: false, type: "boolean", exclusiveGroup: "ykt_mode" },
{ name: PP_KEY.REVIEW, value: false, type: "boolean", exclusiveGroup: "ykt_mode" }
]
},
{
name: PP_KEY.VIDEO,
params: [
{ name: "自动播放", value: true, type: "boolean" },
{ name: PP_KEY.MANUAL_RATE, value: true, type: "boolean", exclusiveGroup: "ykt_rate" },
{ name: PP_KEY.AUTO_RATE, value: false, type: "boolean", exclusiveGroup: "ykt_rate" },
{ name: PP_KEY.BREAK_RATE, value: false, type: "boolean", exclusiveGroup: "ykt_rate" },
{ name: PP_KEY.RATE, value: 1.5, type: "number", min: 1, max: 8 }
]
},
{
name: PP_KEY.ANSWER,
params: [
{ name: PP_KEY.NORMAL_MODE, value: true, type: "boolean", exclusiveGroup: "mode" },
{ name: PP_KEY.VERIFY, value: false, type: "boolean", exclusiveGroup: "mode" },
{ name: PP_KEY.AI_MODE, value: false, type: "boolean", exclusiveGroup: "mode" },
{ name: PP_KEY.WEB_SEARCH, value: false, type: "boolean", dependsOn: { param: PP_KEY.AI_MODE, value: true } },
createAiTypeParam(),
createAiModelParam(),
{ name: PP_KEY.SKIP_ANSWERED, value: true, type: "boolean" },
{ name: PP_KEY.SIMILAR, value: true, type: "boolean" },
{ name: PP_KEY.SIM_DELAY, value: true, type: "boolean" },
{ name: PP_KEY.ANSWER_GAP, value: 1, type: "number" },
{ name: PP_KEY.THRESHOLD, value: 85, type: "number" }
]
},
{
name: PP_KEY.YKT_CHAPTER,
params: [
{ name: "自动评论", value: false, type: "boolean" },
{ name: PP_KEY.AUTO_SUBMIT, value: true, type: "boolean" },
{ name: PP_KEY.AUTO_NEXT, value: true, type: "boolean" }
]
}
]
}
},
queryApis: [
{
name: "题库",
token: ""
}
]
};
let globalConfig = defaultConfig;
const storedConfig = _GM_getValue("config");
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 = `网课小助手-飘飘友情提供 v${scriptInfo.version}`;
}
const migrateCxParts = () => {
const parts = globalConfig.platformParams?.cx?.parts;
if (!Array.isArray(parts)) return;
const videoPart = parts.find(p => p.name === PP_KEY.VIDEO);
if (videoPart?.params) {
const legacyDirectIdx = videoPart.params.findIndex(p => p.name === PP_KEY.DIRECT_REPORT);
if (legacyDirectIdx >= 0) {
videoPart.params.splice(legacyDirectIdx, 1);
}
}
const miscIdx = parts.findIndex(p => p.name === PP_KEY.MISC);
let afkParam = null;
if (miscIdx >= 0) {
afkParam = (parts[miscIdx].params || []).find(p => p.name === PP_KEY.STANDBY) || null;
parts.splice(miscIdx, 1);
}
if (videoPart?.params && !videoPart.params.some(p => p.name === PP_KEY.STANDBY)) {
const param = afkParam || { name: PP_KEY.STANDBY, value: false, type: "boolean" };
const autoIdx = videoPart.params.findIndex(p => p.name === PP_KEY.AUTO_RATE);
if (autoIdx >= 0) {
videoPart.params.splice(autoIdx + 1, 0, param);
} else {
videoPart.params.push(param);
}
}
const modeParamNames = [PP_KEY.NORMAL_MODE, PP_KEY.ONLY_VIDEO, PP_KEY.ONLY_ANSWER, PP_KEY.REVIEW];
let modePart = parts.find(p => p.name === PP_KEY.MODE);
if (!modePart) {
modePart = { name: PP_KEY.MODE, params: [] };
parts.unshift(modePart);
}
const chapterPart = parts.find(p => p.name === PP_KEY.CHAPTER);
if (chapterPart && chapterPart.params) {
modeParamNames.forEach((pn) => {
const pi = chapterPart.params.findIndex(p => p.name === pn);
if (pi < 0) return;
const moved = chapterPart.params.splice(pi, 1)[0];
if (!modePart.params.some(p => p.name === pn)) {
modePart.params.push(moved);
}
});
}
modePart.params.sort((a, b) => modeParamNames.indexOf(a.name) - modeParamNames.indexOf(b.name));
const chapterIdx = parts.findIndex(p => p.name === PP_KEY.CHAPTER);
if (chapterIdx >= 0) {
const chapterMoved = parts.splice(chapterIdx, 1)[0];
const answerIdx = parts.findIndex(p => p.name === PP_KEY.ANSWER);
parts.splice(answerIdx >= 0 ? answerIdx + 1 : parts.length, 0, chapterMoved);
}
const modeIdx = parts.findIndex(p => p.name === PP_KEY.MODE);
if (modeIdx > 0) {
parts.unshift(parts.splice(modeIdx, 1)[0]);
}
};
migrateCxParts();
try {
const zhsParts = globalConfig.platformParams?.zhs?.parts;
if (Array.isArray(zhsParts)) {
const settingIdx = zhsParts.findIndex((p) => p.name === PP_KEY.ANSWER_SETTINGS);
if (settingIdx >= 0) zhsParts.splice(settingIdx, 1);
let modePart = zhsParts.find((p) => p.name === PP_KEY.MODE);
if (!modePart) {
modePart = { name: PP_KEY.MODE, params: [] };
zhsParts.unshift(modePart);
}
if (!Array.isArray(modePart.params)) modePart.params = [];
const legacyAnswer = modePart.params.find((p) => p.name === "答题");
if (legacyAnswer) {
const legacyOn = legacyAnswer.value !== false;
modePart.params = modePart.params.filter((p) => p !== legacyAnswer);
if (!legacyOn && !modePart.params.some((p) => p.name === PP_KEY.ONLY_VIDEO)) {
modePart.params.push({ name: PP_KEY.ONLY_VIDEO, value: true, type: "boolean", exclusiveGroup: "zhs_mode" });
}
}
const zhsModeNames = [PP_KEY.NORMAL_MODE, PP_KEY.ONLY_VIDEO, PP_KEY.ONLY_ANSWER, PP_KEY.REVIEW];
zhsModeNames.forEach((name) => {
if (modePart.params.some((p) => p.name === name)) return;
modePart.params.push({
name,
value: name === PP_KEY.NORMAL_MODE,
type: "boolean",
exclusiveGroup: "zhs_mode"
});
});
modePart.params.forEach((p) => {
if (zhsModeNames.indexOf(p.name) >= 0) p.exclusiveGroup = "zhs_mode";
});
modePart.params.sort((a, b) => zhsModeNames.indexOf(a.name) - zhsModeNames.indexOf(b.name));
}
} catch (e) {}
const restoreAiGetters = (platformConfig) => {
if (!platformConfig || !platformConfig.parts) return;
const answerParamsPart = platformConfig.parts.find(p => p.name === PP_KEY.ANSWER);
if (!answerParamsPart || !answerParamsPart.params) return;
const aiTypeIdx = answerParamsPart.params.findIndex(p => p.name === PP_KEY.AI_TYPE);
const aiModelIdx = answerParamsPart.params.findIndex(p => p.name === PP_KEY.AI_MODEL);
if (aiTypeIdx >= 0) {
const savedTypeVal = answerParamsPart.params[aiTypeIdx].value;
answerParamsPart.params[aiTypeIdx] = createAiTypeParam();
if (savedTypeVal && globalModelConfig?.typeOptions?.includes(savedTypeVal)) {
let _typeVal = savedTypeVal;
Object.defineProperty(answerParamsPart.params[aiTypeIdx], 'value', {
get() { return _typeVal; },
set(v) { _typeVal = v; },
enumerable: true, configurable: true
});
}
}
if (aiModelIdx >= 0) {
const savedModelVal = answerParamsPart.params[aiModelIdx].value;
answerParamsPart.params[aiModelIdx] = createAiModelParam();
if (savedModelVal && globalModelConfig?.allModelOptions?.includes(savedModelVal)) {
let _modelVal = savedModelVal;
Object.defineProperty(answerParamsPart.params[aiModelIdx], 'value', {
get() { return _modelVal; },
set(v) { _modelVal = v; },
enumerable: true, configurable: true
});
}
}
};
if (globalConfig.platformParams) {
Object.keys(globalConfig.platformParams).forEach(key => {
restoreAiGetters(globalConfig.platformParams[key]);
});
}
if (!globalConfig.platformParams) {
globalConfig.platformParams = defaultConfig.platformParams;
}
if (globalConfig.platformParams) {
const RUNTIME_PARAM_NAMES = [PP_KEY.STANDBY];
const shapeOf = (cfg) => {
if (!cfg || !Array.isArray(cfg.parts)) return null;
return cfg.parts
.map((part) => {
const params = Array.isArray(part && part.params)
? part.params
.map((p) => p && p.name)
.filter((name) => name && RUNTIME_PARAM_NAMES.indexOf(name) < 0)
.join(",")
: "";
return `${(part && part.name) || ""}[${params}]`;
})
.join("|");
};
const carryOverParams = (fromPlatform, toPlatform) => {
if (!fromPlatform || !toPlatform) return;
if (!Array.isArray(fromPlatform.parts) || !Array.isArray(toPlatform.parts)) return;
fromPlatform.parts.forEach((oldPart) => {
if (!oldPart || !oldPart.name) return;
const newPart = toPlatform.parts.find((p) => p && p.name === oldPart.name);
if (!newPart || !Array.isArray(newPart.params) || !Array.isArray(oldPart.params)) return;
oldPart.params.forEach((oldParam) => {
if (!oldParam || !oldParam.name) return;
const newParam = newPart.params.find((p) => p && p.name === oldParam.name);
if (newParam) {
newParam.value = oldParam.value;
return;
}
if (RUNTIME_PARAM_NAMES.indexOf(oldParam.name) >= 0) {
newPart.params.push(JSON.parse(JSON.stringify(oldParam)));
}
});
});
};
const defaults = defaultConfig.platformParams || {};
Object.keys(defaults).forEach((platform) => {
const want = shapeOf(defaults[platform]);
const got = shapeOf(globalConfig.platformParams[platform]);
if (got === want) return;
const merged = JSON.parse(JSON.stringify(defaults[platform]));
carryOverParams(globalConfig.platformParams[platform], merged);
globalConfig.platformParams[platform] = merged;
});
}
if (!globalConfig.queryApis || !globalConfig.queryApis.length) {
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) => {
if (!oldApi.name) return;
const newApi = globalConfig.queryApis.find(a => a.name === oldApi.name);
if (newApi && oldApi.token) {
newApi.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) => {
if (!oldPart.name) return;
const newPart = newPlatform.parts.find(p => p.name === oldPart.name);
if (newPart && oldPart.params) {
oldPart.params.forEach((oldParam) => {
const newParam = newPart.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 === PP_KEY.ANSWER);
if (otherParamsPart && otherParamsPart.params) {
const nameMapping = {
[PP_KEY.VERIFY_MODE]: PP_KEY.VERIFY,
"跳过已答题": PP_KEY.SKIP_ANSWERED,
"相似度答案匹配": PP_KEY.SIMILAR,
"答题正确率": PP_KEY.THRESHOLD
};
const normalModeParam = otherParamsPart.params.find(p => p.name === PP_KEY.NORMAL_MODE);
const aiModeParam = otherParamsPart.params.find(p => p.name === PP_KEY.AI_MODE);
const answerVerifyParam = otherParamsPart.params.find(p => p.name === PP_KEY.VERIFY);
if (normalModeParam && aiModeParam && answerVerifyParam) {
const oldNormalMode = parsedStoredConfig.otherParams.params.find(p => p.name === PP_KEY.NORMAL_MODE);
const oldAiMode = parsedStoredConfig.otherParams.params.find(p => p.name === PP_KEY.AI_MODE);
const oldAnswerVerify = parsedStoredConfig.otherParams.params.find(p => p.name === PP_KEY.VERIFY_MODE);
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 && ![PP_KEY.NORMAL_MODE, PP_KEY.AI_MODE, PP_KEY.VERIFY].includes(newName)) {
newParam.value = oldParam.value;
}
});
}
}
}
} catch (error) {
}
}
try {
const yktVideoPart = globalConfig.platformParams?.ykt?.parts?.find((p) => p.name === PP_KEY.VIDEO);
const rateParams = (yktVideoPart?.params || []).filter((p) => p.exclusiveGroup === "ykt_rate");
if (!globalConfig.yktRateDefaultMigrated) {
const autoRateParam = rateParams.find((p) => p.name === PP_KEY.AUTO_RATE);
if (autoRateParam) autoRateParam.value = false;
const manualRateParam = rateParams.find((p) => p.name === PP_KEY.MANUAL_RATE);
if (manualRateParam) manualRateParam.value = true;
globalConfig.yktRateDefaultMigrated = true;
}
const active = rateParams.filter((p) => p.value);
if (active.length > 1) {
const keep = active.find((p) => p.name !== PP_KEY.MANUAL_RATE) || active[0];
rateParams.forEach((p) => { p.value = p.name === keep.name; });
}
const speedParamYkt = yktVideoPart?.params?.find((p) => p.name === PP_KEY.RATE);
if (speedParamYkt) {
const manualOn = rateParams.some((p) => p.name === PP_KEY.MANUAL_RATE && p.value);
const breakOn = rateParams.some((p) => p.name === PP_KEY.BREAK_RATE && p.value);
const limit = breakOn ? 8 : manualOn ? 2 : 8;
speedParamYkt.max = limit;
if (Number(speedParamYkt.value) > limit) speedParamYkt.value = limit;
}
} catch (e) {}
try {
const platforms = globalConfig.platformParams || {};
let afkValue = false;
Object.keys(platforms).forEach((key) => {
const parts = platforms[key] && platforms[key].parts;
if (!Array.isArray(parts)) return;
parts.forEach((part) => {
(part.params || []).forEach((param) => {
if (param.name === PP_KEY.STANDBY && param.value === true) afkValue = true;
});
});
for (let i = parts.length - 1; i >= 0; i--) {
const part = parts[i];
if (part.name === PP_KEY.GENERAL) {
parts.splice(i, 1);
continue;
}
if (Array.isArray(part.params)) {
const idx = part.params.findIndex((p) => p.name === PP_KEY.STANDBY);
if (idx >= 0) part.params.splice(idx, 1);
}
}
});
const sharedAfkParam = { name: PP_KEY.STANDBY, value: afkValue, type: "boolean" };
Object.keys(platforms).forEach((key) => {
const parts = platforms[key] && platforms[key].parts;
if (!Array.isArray(parts)) return;
let modePart = parts.find((p) => p.name === PP_KEY.MODE);
if (!modePart) {
modePart = { name: PP_KEY.MODE, params: [] };
parts.unshift(modePart);
}
if (!Array.isArray(modePart.params)) modePart.params = [];
modePart.params.push(sharedAfkParam);
});
} catch (e) {}
if (![100, 125, 150].includes(Number(globalConfig.uiScale))) globalConfig.uiScale = 100;
_GM_setValue("globalConfig", JSON.stringify(globalConfig));
return globalConfig;
},
actions: {}
});
let ppfxLogSeq = 0;
const useLogStore = pinia.defineStore("logStore", {
state: () => ({
logList: []
}),
actions: {
addLog(message, type) {
const MAX_LOG_SIZE = 500;
const log = {
id: ++ppfxLogSeq,
message,
time: getDateTime(),
type
};
this.logList.push(log);
if (this.logList.length > MAX_LOG_SIZE) {
this.logList.splice(0, this.logList.length - MAX_LOG_SIZE);
}
}
}
});
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,
questionIndex: 0,
questionTotal: 0,
filledCount: 0,
fillRate: 0,
remainingCount: null
}),
actions: {
update(progress) {
if (progress.taskName !== undefined) this.taskName = progress.taskName;
if (progress.percent !== undefined) this.percent = Math.min(100, Math.max(0, progress.percent));
if (progress.currentTime !== undefined) this.currentTime = progress.currentTime;
if (progress.totalTime !== undefined) this.totalTime = progress.totalTime;
if (progress.type !== undefined) this.type = progress.type;
if (progress.detail !== undefined) this.detail = progress.detail;
if (progress.isPlaying !== undefined) this.isPlaying = progress.isPlaying;
if (progress.speedDisabled !== undefined) this.speedDisabled = progress.speedDisabled;
if (progress.questionIndex !== undefined) this.questionIndex = Math.max(0, Number(progress.questionIndex) || 0);
if (progress.questionTotal !== undefined) this.questionTotal = Math.max(0, Number(progress.questionTotal) || 0);
if (progress.filledCount !== undefined) this.filledCount = Math.max(0, Number(progress.filledCount) || 0);
if (progress.fillRate !== undefined) this.fillRate = Math.min(100, Math.max(0, Number(progress.fillRate) || 0));
if (progress.remainingCount !== undefined) this.remainingCount = progress.remainingCount === null ? null : Math.max(0, Number(progress.remainingCount) || 0);
},
reset(message = "等待任务开始") {
this.taskName = "暂无任务";
this.percent = 0;
this.currentTime = 0;
this.totalTime = 0;
this.type = "-";
this.detail = message;
this.isPlaying = false;
this.speedDisabled = false;
this.questionIndex = 0;
this.questionTotal = 0;
this.filledCount = 0;
this.fillRate = 0;
this.remainingCount = null;
}
}
});
const PP_ICONS = Object.freeze({
home: "M4 10.5 12 4l8 6.5V20a1 1 0 0 1-1 1h-4.5v-6h-5v6H5a1 1 0 0 1-1-1z",
list: "M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01",
sliders: "M4 7h9M17 7h3M4 17h3M11 17h9M15 4v6M9 14v6",
book: "M4 19.5A2.5 2.5 0 0 1 6.5 17H20M4 19.5A2.5 2.5 0 0 0 6.5 22H20V2H6.5A2.5 2.5 0 0 0 4 4.5z",
heart: "M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 21l7.8-7.6 1-1a5.5 5.5 0 0 0 0-7.8z",
search: "M21 21l-4.3-4.3M19 11a8 8 0 1 1-16 0 8 8 0 0 1 16 0z",
key: "M12 8a4 4 0 1 1-8 0 4 4 0 0 1 8 0zM12 8h9M18 8v3M21 8v2",
check: "M20 6 9 17l-5-5",
alert: "M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0zM12 9v4M12 17h.01",
info: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 16v-4M12 8h.01",
clock: "M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2",
server: "M4 4h16v6H4zM4 14h16v6H4zM8 7h.01M8 17h.01",
wifi: "M5 12.5a10 10 0 0 1 14 0M8.5 16a5 5 0 0 1 7 0M12 19.5h.01",
chevronDown: "M6 9l6 6 6-6",
chevronRight: "M9 18l6-6-6-6",
chevronLeft: "M15 18l-6-6 6-6",
arrowDown: "M12 5v14M6 13l6 6 6-6",
x: "M18 6 6 18M6 6l12 12",
trash: "M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6",
refresh: "M21 12a9 9 0 1 1-2.6-6.4M21 3v6h-6",
external: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14 21 3",
sidebar: "M3 4h18v16H3zM9 4v16",
spark: "M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1",
message: "M21 15a2 2 0 0 1-2 2H8l-5 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z",
shield: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z",
play: "M8 5v14l11-7z",
gift: "M20 12v9H4v-9M2 7h20v5H2zM12 22V7M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z",
zap: "M13 2 4 14h7l-1 8 9-12h-7z"
});
const PpIcon = vue.defineComponent({
name: "PpIcon",
props: {
name: { type: String, required: true },
size: { type: [Number, String], default: 15 }
},
setup(props) {
return () => vue.h("svg", {
class: "pp-icon",
viewBox: "0 0 24 24",
width: props.size,
height: props.size,
fill: props.name === "play" ? "currentColor" : "none",
stroke: props.name === "play" ? "none" : "currentColor",
"stroke-width": 1.8,
"stroke-linecap": "round",
"stroke-linejoin": "round",
"aria-hidden": "true"
}, [vue.h("path", { d: PP_ICONS[props.name] || "" })]);
}
});
const ppIcon = (name, size) => vue.h(PpIcon, { name, size: size || 15 });
const PP_LOGO_SOURCES = [
"https://scriptcat.org/api/v2/scripts/5597/icon/thumb/64?t=1793777938",
"https://tuchuang-1310788589.cos.ap-guangzhou.myqcloud.com/_selftest/icon.webp"
];
let ppLogoSourceIndex = 0;
const PpLogo = vue.defineComponent({
name: "PpLogo",
props: { size: { type: Number, default: 30 } },
setup(props) {
const sourceIndex = vue.ref(ppLogoSourceIndex);
return () => vue.h("span", {
class: "pp-logo",
style: {
width: props.size + "px",
height: props.size + "px",
"--pp-logo-font": Math.round(props.size * 0.5) + "px"
}
}, [
vue.h("i", { class: "pp-logo-fallback" }, "P"),
sourceIndex.value < PP_LOGO_SOURCES.length
? vue.h("img", {
key: sourceIndex.value,
class: "pp-logo-img",
src: PP_LOGO_SOURCES[sourceIndex.value],
alt: "",
draggable: false,
referrerpolicy: "no-referrer",
onError: () => {
ppLogoSourceIndex = Math.min(ppLogoSourceIndex + 1, PP_LOGO_SOURCES.length);
sourceIndex.value = ppLogoSourceIndex;
}
})
: null
]);
}
});
const ppIconBtn = (opts) => vue.h("button", {
type: "button",
class: ["pp-iconbtn", opts.active ? "pp-on" : ""],
title: opts.tip || "",
disabled: !!opts.disabled,
onClick: opts.onClick
}, [ppIcon(opts.icon, opts.size || 15)]);
const ppChip = (label, opts) => {
const o = opts || {};
const children = [];
if (o.dot) children.push(vue.h("span", { class: ["pp-dot", o.dot === "on" ? "on" : ""] }));
if (o.icon) children.push(ppIcon(o.icon, 11));
children.push(vue.h("span", null, label));
return vue.h("span", { class: ["pp-chip", o.tone || ""], title: o.title || "" }, children);
};
const ppPill = (label, cls) => vue.h("span", { class: ["pp-pill", cls || ""] }, label);
const ppHint = (text, opts) => {
const o = opts || {};
return vue.h("div", { class: ["pp-hint", o.tone || ""] }, [
ppIcon(o.icon || "info", 11),
vue.h("span", null, text)
]);
};
const ppEmpty = (opts) => {
const o = opts || {};
return vue.h("div", { class: "pp-empty" }, [
vue.h("span", { class: "pp-empty-ico" }, ppIcon(o.icon || "message", 18)),
vue.h("b", null, o.title || "暂无内容"),
o.desc ? vue.h("span", null, o.desc) : null
]);
};
const ppRow = (label, control, opts) => {
const o = opts || {};
return vue.h("div", { class: ["pp-row", o.off ? "is-off" : ""] }, [
vue.h("div", { class: "pp-row-label" }, [
vue.h("span", null, label),
o.unit ? vue.h("span", { class: "pp-unit" }, o.unit) : null,
o.muted ? vue.h("span", { class: "pp-muted" }, o.muted) : null
]),
vue.h("div", { class: "pp-row-control" }, control)
]);
};
const ppSeg = (options, value, onPick, opts) => {
const o = opts || {};
return vue.h("div", { class: "pp-seg" }, options.map((item) => vue.h("button", {
type: "button",
class: ["pp-seg-btn", item.value === value ? "active" : ""],
disabled: !!o.disabled || !!item.disabled,
onClick: () => {
if (item.value !== value) onPick(item.value);
}
}, item.label)));
};
const ppDoc = (title, icon, children) => vue.h("section", { class: "pp-doc" }, [
vue.h("div", { class: "pp-doc-title" }, [ppIcon(icon, 13), vue.h("span", null, title)]),
vue.h("div", { class: "pp-prose" }, children)
]);
const ppFormatTime = (seconds) => {
const safe = Number(seconds) || 0;
const mins = Math.floor(safe / 60);
const secs = Math.floor(safe % 60);
return String(mins).padStart(2, "0") + ":" + String(secs).padStart(2, "0");
};
const StatusCard = vue.defineComponent({
name: "StatusCard",
setup() {
const progress = useProgressStore();
const questionStore = useQuestionStore();
const ppfxPctMs = 320;
const ppfxReadPct = () => Math.min(100, Math.max(0, Math.round(Number(progress.percent) || 0)));
const ppfxPctShown = vue.ref(ppfxReadPct());
let ppfxPctRaf = 0;
let ppfxPctFrom = ppfxPctShown.value;
let ppfxPctTo = ppfxPctShown.value;
let ppfxPctStart = -1;
const ppfxPctStop = () => {
if (ppfxPctRaf) cancelAnimationFrame(ppfxPctRaf);
ppfxPctRaf = 0;
ppfxPctStart = -1;
};
const ppfxPctStep = (ts) => {
if (ppfxPctStart < 0) ppfxPctStart = ts;
const ppfxT = Math.min(1, (ts - ppfxPctStart) / ppfxPctMs);
const ppfxEased = 1 - Math.pow(1 - ppfxT, 3);
ppfxPctShown.value = Math.round(ppfxPctFrom + (ppfxPctTo - ppfxPctFrom) * ppfxEased);
if (ppfxT < 1) {
ppfxPctRaf = requestAnimationFrame(ppfxPctStep);
} else {
ppfxPctRaf = 0;
ppfxPctShown.value = ppfxPctTo;
}
};
const ppfxPctTween = (next) => {
if (ppfxPctRaf && next === ppfxPctTo) return;
ppfxPctStop();
ppfxPctTo = next;
if (next === ppfxPctShown.value) return;
try {
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
ppfxPctShown.value = next;
return;
}
} catch (error) {}
ppfxPctFrom = ppfxPctShown.value;
ppfxPctRaf = requestAnimationFrame(ppfxPctStep);
};
const ppfxBarStyle = () => ({
"--ppfx-p": (Math.min(100, Math.max(0, Number(progress.percent) || 0)) / 100).toFixed(4)
});
vue.watch(ppfxReadPct, (next) => {
ppfxPctTween(next);
});
vue.onUnmounted(ppfxPctStop);
return () => {
const answerTotal = Number(progress.questionTotal) || 0;
const answering = answerTotal > 0 || (progress.type === "答题" && progress.isPlaying);
if (answering) {
const index = Math.min(Number(progress.questionIndex) || 0, answerTotal);
const answered = questionStore.questionList.length;
return vue.h("div", { class: "pp-progress" }, [
vue.h("div", { class: "pp-progress-top" }, [
vue.h("div", { class: "pp-task", title: progress.taskName }, progress.taskName || "答题中"),
vue.h("div", { class: "pp-pct" }, ppfxPctShown.value + "%")
]),
vue.h("div", { class: "pp-bar" }, [
vue.h("i", { style: ppfxBarStyle() })
]),
vue.h("div", { class: "pp-meta" }, [
vue.h("span", null, ["题目 ", vue.h("b", null,
answerTotal > 0 ? "第 " + index + " / " + answerTotal + " 题" : "已答 " + answered + " 题")]),
vue.h("span", null, progress.isPlaying ? "答题中" : (progress.detail || "已完成"))
])
]);
}
const hasMediaProgress = (progress.type === "视频" || progress.type === "音频") && Number(progress.totalTime) > 0;
if (!progress.isPlaying && !hasMediaProgress) {
return vue.h("div", { class: "pp-idle" }, [
vue.h("span", { class: "pp-idle-ico" }, ppIcon("play", 13)),
vue.h("div", { class: "pp-idle-txt" }, [
vue.h("b", null, "等待任务开始"),
vue.h("span", { title: progress.detail },
progress.detail && progress.detail !== "等待任务开始"
? progress.detail
: "进入课程章节或答题页面后自动运行"
)
])
]);
}
return vue.h("div", { class: "pp-progress" }, [
vue.h("div", { class: "pp-progress-top" }, [
vue.h("div", { class: "pp-task", title: progress.taskName }, progress.taskName || "正在处理"),
vue.h("div", { class: "pp-pct" }, ppfxPctShown.value + "%")
]),
vue.h("div", { class: "pp-bar" }, [
vue.h("i", { style: ppfxBarStyle() })
]),
vue.h("div", { class: "pp-meta" }, [
vue.h("span", null, ["类型 ", vue.h("b", null, progress.type || "-")]),
vue.h("span", null, ppFormatTime(progress.currentTime) + " / " + ppFormatTime(progress.totalTime))
]),
progress.speedDisabled
? ppHint("此视频已禁用倍速,>1x 可能导致进度被清空", { tone: "warn", icon: "alert" })
: null
]);
};
}
});
const _sfc_main$8 = vue.defineComponent({
name: "ScriptHome",
props: {
logList: { type: Array, required: true },
serverConfig: {
type: Object,
default: () => ({ url: "", location: "未知", color: "#888" })
}
},
setup(props) {
const progress = useProgressStore();
const logs = useLogStore();
const config = useConfigStore();
const modeLabel = vue.computed(() => {
const platform = config.platformParams?.[config.platformName];
const part = platform?.parts?.find((p) => p.name === PP_KEY.MODE);
const active = part?.params?.find((p) => p.value);
return active ? active.name : "默认模式";
});
const listRef = vue.ref(null);
const scrollBottom = () => {
const el = listRef.value;
if (el) el.scrollTop = el.scrollHeight;
};
vue.watch(
() => (props.logList.length ? (props.logList[props.logList.length - 1].id || props.logList.length) : 0),
() => vue.nextTick(scrollBottom),
{ flush: "post" }
);
vue.onMounted(scrollBottom);
const clearLogs = () => {
logs.logList.splice(0, logs.logList.length);
};
let ppfxLastLogId = -1;
let ppfxNewLogId = null;
if (props.logList.length) {
const ppfxTail0 = props.logList[props.logList.length - 1];
ppfxLastLogId = ppfxTail0 && ppfxTail0.id != null ? ppfxTail0.id : -1;
}
const ppfxMarkNewLog = () => {
const ppfxLen = props.logList.length;
if (!ppfxLen) return;
const ppfxTail = props.logList[ppfxLen - 1];
const ppfxId = ppfxTail && ppfxTail.id != null ? ppfxTail.id : null;
if (ppfxId !== null && ppfxId !== ppfxLastLogId) {
ppfxLastLogId = ppfxId;
ppfxNewLogId = ppfxId;
}
};
const renderLog = (item, index) => {
if (index === props.logList.length - 1) ppfxMarkNewLog();
return vue.h("div", {
class: "pp-log-item",
key: item.id != null ? "ppfx-log-" + item.id : "ppfx-log-i" + index,
"data-t": item.type || "primary",
"data-new": item.id != null && item.id === ppfxNewLogId ? "1" : null
}, [
vue.h("span", { class: "pp-log-time" }, item.time),
vue.h("span", { class: "pp-log-msg", innerHTML: item.message })
]);
};
return () => vue.h("div", { class: "pp-home" }, [
vue.h("div", { class: "pp-statusrow" }, [
ppChip("服务器 · " + (props.serverConfig.location || "未知"), { icon: "server" }),
ppChip("已连接", { tone: "ok", dot: "on" }),
ppChip(modeLabel.value, { icon: "sliders" })
]),
vue.h(StatusCard),
vue.h("section", { class: "pp-logs" }, [
vue.h("div", { class: "pp-log-head" }, [
vue.h("span", { class: "pp-log-title" }, "运行日志"),
vue.h("span", { class: "pp-log-count" }, props.logList.length),
vue.h("div", { class: "pp-log-actions" }, [
ppIconBtn({ icon: "arrowDown", tip: "滚动到底部", size: 14, onClick: scrollBottom }),
ppIconBtn({ icon: "trash", tip: "清空日志", size: 14, onClick: clearLogs })
])
]),
vue.h("div", { class: "pp-log-list", ref: listRef },
props.logList.length
? props.logList.map(renderLog)
: ppEmpty({ icon: "message", title: "暂无日志", desc: "任务开始后,运行记录会显示在这里" })
)
])
]);
}
});
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 _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 existingTokens = vue.ref([]);
const _uiUpdateTrigger = vue.ref(0);
const triggerUiUpdate = () => { _uiUpdateTrigger.value++; };
const watchUiUpdate = () => { void _uiUpdateTrigger.value; };
const modelCosts = vue.ref({});
const fetchModelCosts = async () => {
try {
const response = await ppHttpGet("/model-costs");
const data = JSON.parse(response.responseText);
if (data.code === 200 && data.data) {
modelCosts.value = data.data;
}
} catch (e) {
}
};
vue.onMounted(() => {
fetchModelCosts();
window._triggerUiUpdate = triggerUiUpdate;
});
const consumptionText = vue.computed(() => {
watchUiUpdate();
const answerParamsPart = __props.globalConfig.platformParams?.[__props.globalConfig.platformName]?.parts?.find(p => p.name === PP_KEY.ANSWER);
if (!answerParamsPart) return "每题消耗约: 1 次";
const aiModeParam = answerParamsPart.params.find(p => p.name === PP_KEY.AI_MODE);
const normalModeParam = answerParamsPart.params.find(p => p.name === PP_KEY.NORMAL_MODE);
const verifyModeParam = answerParamsPart.params.find(p => p.name === PP_KEY.VERIFY);
const aiTypeParam = answerParamsPart.params.find(p => p.name === PP_KEY.AI_TYPE);
const aiModelParam = answerParamsPart.params.find(p => p.name === PP_KEY.AI_MODEL);
const isAiModeActive = aiModeParam && aiModeParam.value;
const isNormalModeActive = normalModeParam && normalModeParam.value;
const isVerifyModeActive = verifyModeParam && verifyModeParam.value;
let cost;
if (isNormalModeActive || (!isAiModeActive && !isVerifyModeActive)) {
cost = modelCosts.value['normal'];
} else if (isVerifyModeActive) {
cost = modelCosts.value['verify'];
} else if (isAiModeActive && aiTypeParam && aiModelParam) {
const modelType = getModelType(aiTypeParam.value, aiModelParam.value);
cost = modelCosts.value[modelType];
}
if (cost == null) return "每题消耗约: 1 次";
return /^\d/.test(cost) ? `每题消耗约: ${cost}` : `每题消耗: ${cost}`;
});
const debounce = (fn, delay) => {
let timer = null;
return (...args) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
};
const autoVerifyToken = debounce(() => {
const currentToken = vue.unref(configStore).queryApis[0].token;
if (!currentToken || !/^\d{16}$|^\d{24}$/.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 token = vue.unref(configStore).queryApis[0].token;
const userId = getUid();
if (!token) {
} else if (!/^\d{16}$|^\d{24}$/.test(token)) {
logStore.addLog('Token格式错误,必须是24位数字', 'danger');
verifyState.value = { status: 'error', message: 'Token格式错误,必须是24位数字' };
configStore.tokenVerified = false;
configStore.tokenVerifyError = 'format';
return;
}
verifyState.value = { status: 'testing', message: '正在验证...' };
logStore.addLog('正在验证Token...', 'primary');
const startTime = Date.now();
const configStoreRef = vue.unref(configStore);
const workType = configStoreRef.platformName === "zhs" ? "zhs" : "cx";
_GM_xmlhttpRequest({
method: "GET",
url: getRandomServer() + "?token=" + encodeURIComponent(token || '') + "&userId=" + encodeURIComponent(userId) + "&fid=" + encodeURIComponent(getFid()) + "&workType=" + encodeURIComponent(workType),
timeout: 10000,
onload: (response) => {
pingDelay.value = Date.now() - startTime;
reportServerSuccess();
try {
const res = JSON.parse(response.responseText);
if (res.data?.sponsorUrl) {
SPONSOR_URL = res.data.sponsorUrl;
}
if (res.code === 200 && res.data && res.data.valid) {
remainingCount.value = res.data.num;
verifyState.value = { status: 'success', message: `验证成功!剩余次数: ${res.data.num}次` };
configStore.tokenVerified = true;
configStore.tokenVerifyError = null;
logStore.addLog(`Token验证成功,剩余次数: ${res.data.num}次`, 'success');
if (res.data.sponsorUrl) {
SPONSOR_URL = res.data.sponsorUrl;
}
if (res.data.newToken) {
vue.unref(configStore).queryApis[0].token = res.data.newToken;
logStore.addLog(`新用户免费Token已注册,可查询40次题目`, 'success');
}
if (res.data.num <= 0) {
logStore.addLog(`次数已用完,${getSponsorLink(res.data.sponsorUrl, '点我赞助获取新token')}`, 'warning');
}
} else if (res.code === 401) {
remainingCount.value = 0;
verifyState.value = { status: 'error', message: 'Token无效' };
configStore.tokenVerified = false;
configStore.tokenVerifyError = 'invalid';
logStore.addLog('Token无效,请检查是否正确', 'danger');
if (res.data?.existingTokens && res.data.existingTokens.length > 0) {
existingTokens.value = res.data.existingTokens;
logStore.addLog(`检测到您有${res.data.existingTokens.length}个有效Token,请在下方选择使用`, 'warning');
} else {
existingTokens.value = [];
logStore.addLog(`${getSponsorLink(res.data.sponsorUrl, '点我赞助获取新token')}`, 'warning');
}
} else {
remainingCount.value = 0;
verifyState.value = { status: 'error', message: res.msg || '验证失败' };
configStore.tokenVerified = false;
configStore.tokenVerifyError = 'invalid';
if (res.data?.sponsorUrl) {
const sponsorUrl = res.data.sponsorUrl;
const msgHtml = (res.msg || '').replace('[可切换赞助获取token,不限制账户]',
`[可切换赞助获取token,不限制账户]`);
logStore.addLog(`验证失败: ${msgHtml}`, 'danger');
} else {
logStore.addLog('验证失败: '+ (res.msg || '未知错误'), 'danger');
logStore.addLog(`${getSponsorLink(res.data.sponsorUrl, '点我赞助获取新token')}`, 'warning');
}
}
} catch (e) {
remainingCount.value = 0;
verifyState.value = { status: 'error', message: '响应解析失败' };
configStore.tokenVerified = false;
configStore.tokenVerifyError = 'network';
logStore.addLog('服务器响应格式错误', 'danger');
}
},
onerror: () => {
pingDelay.value = -1;
reportServerFail();
verifyState.value = { status: 'error', message: '连接失败' };
configStore.tokenVerified = false;
configStore.tokenVerifyError = 'network';
logStore.addLog('无法连接服务器,请检查网络', 'danger');
},
ontimeout: () => {
pingDelay.value = -1;
reportServerFail();
verifyState.value = { status: 'error', message: '连接超时' };
configStore.tokenVerified = false;
configStore.tokenVerifyError = 'network';
logStore.addLog('连接超时,请稍后重试', 'danger');
}
});
};
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 = () => {
if (window.__CLOUD_SCRIPT_LOADED__) {
logStore.addLog('云端脚本已是最新版本', 'success');
return;
}
const currentVersion = _GM_info?.script?.version || '1.1.2';
fetchScriptVersion((result) => {
if (!result) return;
const latestVersion = result.version;
if (compareVersion(currentVersion, latestVersion) < 0) {
if (isMajorUpdate(currentVersion, latestVersion)) {
showUpdateDialog(latestVersion, result.updateUrl, currentVersion);
} else {
logStore.addLog(`有新版本 v${latestVersion},非大版本更新`, 'info');
}
} else {
logStore.addLog(`脚本已是最新版本 v${currentVersion}`, 'success');
}
});
};
const showUpdateDialog = (latestVersion, updateUrl, currentVersion) => {
try {
if (document.getElementById("update-dialog")) return;
_ppEnsurePublicStyles();
const current = currentVersion || _GM_info?.script?.version || "";
const latestText = _ppPublicEscape(latestVersion);
const currentText = _ppPublicEscape(current);
const dialog = document.createElement("div");
dialog.id = "update-dialog";
dialog.innerHTML = `
发现新版本
网课小助手 · v${latestText}
强制更新
当前版本
v${currentText}
最新版本
v${latestText}
更新完成后刷新页面即可生效
`;
document.body.appendChild(dialog);
const hintEl = dialog.querySelector(".pp-upd-hint");
const primaryBtn = dialog.querySelector("#update-btn");
const setHint = (text, tone) => {
if (!hintEl) return;
hintEl.textContent = text;
if (tone) hintEl.setAttribute("data-tone", tone);
else hintEl.removeAttribute("data-tone");
};
if (primaryBtn) {
primaryBtn.addEventListener("click", () => {
let win = null;
try { win = window.open(updateUrl || PP_SCRIPT_GH_PROXY_URL, "_blank"); } catch (e) {}
if (win) {
try { win.opener = null; } catch (e) {}
setHint("已在新标签页打开,安装完成后刷新本页面", "ok");
} else {
setHint("浏览器拦截了新窗口,请允许弹窗后重试", "warn");
}
});
setTimeout(() => {
try { primaryBtn.focus({ preventScroll: true }); } catch (e) {}
}, 60);
}
} catch (e) {}
};
vue.onMounted(async () => {
checkUpdate();
verifyToken();
});
const uiView = vue.ref("index");
const activePartName = vue.ref("");
const parts = vue.computed(() => configStore.platformParams?.[configStore.platformName]?.parts || []);
const token = vue.computed(() => configStore.queryApis?.[0]?.token || "");
const PART_ICONS = {
[PP_KEY.MODE]: "sliders",
[PP_KEY.VIDEO]: "play",
[PP_KEY.ANSWER]: "spark",
[PP_KEY.CHAPTER]: "list",
[PP_KEY.YKT_CHAPTER]: "list",
[PP_KEY.EXAM]: "shield",
[PP_KEY.ANSWER_SETTINGS]: "spark"
};
const GROUP_TITLES = {
cx_mode: "运行模式",
zhs_mode: "运行模式",
ykt_mode: "运行模式",
playMode: "播放方式",
ykt_rate: "倍速方式",
mode: "答题方式"
};
const PARAM_UNITS = {
[PP_KEY.RATE]: "x",
[PP_KEY.THRESHOLD]: "%",
[PP_KEY.ANSWER_GAP]: "秒"
};
const PART_DESCS = {
[PP_KEY.MODE]: "选择脚本在当前页面的运行方式,模式之间互斥。",
[PP_KEY.VIDEO]: "控制播放方式、倍速与视频内答题行为。",
[PP_KEY.ANSWER]: "选择题库 / AI 答题策略,并控制消耗与准确率。",
[PP_KEY.CHAPTER]: "控制章节任务的提交、弹窗忽略与任务点自动切换。",
[PP_KEY.YKT_CHAPTER]: "控制雨课堂章节/习题的评论、提交与自动切换。",
[PP_KEY.EXAM]: "控制考试页面的自动切换行为。",
[PP_KEY.ANSWER_SETTINGS]: "控制智慧树的答题行为。"
};
const MODE_DESCS = {
[PP_KEY.NORMAL_MODE]: "自动播放视频并完成章节测验,适合日常刷课。",
[PP_KEY.ONLY_VIDEO]: "只处理视频进度,不参与任何答题。",
[PP_KEY.ONLY_ANSWER]: "只完成章节测验、作业或考试,不主动播放视频。",
[PP_KEY.REVIEW]: "重新完成已学内容,适合考前复习。",
"答题": "开启后执行智慧树答题。"
};
const hasParam = (part, name) => part.params.some((p) => p.name === name);
const getParam = (part, name) => part.params.find((p) => p.name === name);
const readMaxPlaybackRate = () => {
let maxRate = window.__maxPlaybackRate;
if (!maxRate) {
try {
const iframes = document.querySelectorAll("iframe");
for (const iframe of iframes) {
try {
if (iframe.contentDocument) {
const items = iframe.contentDocument.querySelectorAll(".vjs-playback-rate .vjs-menu-content .vjs-menu-item");
if (items.length > 0) {
maxRate = 1;
items.forEach((mi) => {
const rate = parseFloat(String(mi.textContent).trim().replace("x", ""));
if (!isNaN(rate) && rate > maxRate) maxRate = rate;
});
window.__maxPlaybackRate = maxRate;
break;
}
}
} catch (e) { continue; }
}
} catch (e) {}
}
return maxRate || 3;
};
const aiActive = (part) => !!(getParam(part, PP_KEY.AI_MODE) && getParam(part, PP_KEY.AI_MODE).value);
const hasAiBlock = (part) => !!getParam(part, PP_KEY.AI_TYPE) && !!getParam(part, PP_KEY.AI_MODEL);
const isParamDisabled = (part, param) => {
if (param.locked) return true;
if (!param.dependsOn) return false;
const parent = getParam(part, param.dependsOn.param);
return !parent || parent.value !== param.dependsOn.value;
};
const boolGroups = (part) => {
const groups = new Map();
part.params.forEach((p) => {
if (p.type !== "boolean" || !p.exclusiveGroup) return;
if (!groups.has(p.exclusiveGroup)) groups.set(p.exclusiveGroup, []);
groups.get(p.exclusiveGroup).push(p);
});
return groups;
};
const groupTitle = (part, group) => {
if (part.name === PP_KEY.MODE) return "";
return GROUP_TITLES[group] || "选项";
};
const looseParams = (part) => {
const skip = new Set([PP_KEY.AI_TYPE, PP_KEY.AI_MODEL, PP_KEY.WEB_SEARCH]);
return part.params.filter((p) => !(p.type === "boolean" && p.exclusiveGroup) && !skip.has(p.name));
};
const applyBool = (part, param, next) => {
if (param.dependsOn) {
const parent = getParam(part, param.dependsOn.param);
if (parent && parent.value !== param.dependsOn.value) {
if (next) return;
param.value = next;
return;
}
}
if (param.exclusiveGroup) {
const others = part.params.filter((p) =>
p.type === "boolean" && p.exclusiveGroup === param.exclusiveGroup && p.name !== param.name
);
if (next) {
others.forEach((other) => {
other.value = false;
part.params.filter((p) => p.type === "boolean" && p.dependsOn && p.dependsOn.param === other.name)
.forEach((dp) => { dp.value = false; });
});
} else if (!others.some((other) => other.value)) {
return;
}
}
param.value = next;
if ((param.name === PP_KEY.SIMULATE || param.name === PP_KEY.NORMAL_PLAY) && next) {
const speedParam = getParam(part, PP_KEY.RATE);
if (speedParam) {
if (param.name === PP_KEY.SIMULATE) {
const autoMax = getParam(part, PP_KEY.AUTO_RATE)?.value || false;
speedParam.max = autoMax ? readMaxPlaybackRate() : 3;
if (speedParam.value > speedParam.max) speedParam.value = speedParam.max;
} else {
speedParam.max = readMaxPlaybackRate();
}
}
}
triggerUiUpdate();
if (param.name === PP_KEY.AUTO_RATE) {
const speedParam = getParam(part, PP_KEY.RATE);
if (speedParam) {
const isSimulate = getParam(part, PP_KEY.SIMULATE)?.value || false;
if (isSimulate) {
if (next) {
speedParam.max = window.__maxPlaybackRate || 3;
} else {
speedParam.max = 3;
}
if (speedParam.value > speedParam.max) speedParam.value = speedParam.max;
}
}
}
if (part.name === PP_KEY.VIDEO && (param.name === PP_KEY.MANUAL_RATE || param.name === PP_KEY.BREAK_RATE)) {
const speedParam = getParam(part, PP_KEY.RATE);
if (speedParam) {
let menuMax = 2;
try {
const listText = document.querySelector("xt-speedlist")?.textContent || "";
const rates = (listText.match(/(\d+(?:\.\d+)?)\s*X/gi) || []).map((s) => parseFloat(s));
if (rates.length) menuMax = Math.max(...rates);
} catch (e) {}
const limit = param.name === PP_KEY.MANUAL_RATE ? menuMax : 8;
speedParam.max = limit;
if (Number(speedParam.value) > limit) speedParam.value = limit;
}
}
const dependents = part.params.filter((p) =>
p.type === "boolean" && p.dependsOn && p.dependsOn.param === param.name
);
if (!next && dependents.length) {
dependents.forEach((dp) => { dp.value = false; });
}
if (next && dependents.length) {
dependents.forEach((dp) => {
if (dp.dependsOn && dp.dependsOn.value !== next) dp.value = false;
});
}
};
const partSummary = (part) => {
const out = [];
const active = part.params.find((p) => p.type === "boolean" && p.exclusiveGroup && p.value);
const flags = part.params.filter((p) => p.type === "boolean" && !p.exclusiveGroup && p.value && p.name !== PP_KEY.WEB_SEARCH);
if (active) out.push(active.name);
else if (flags.length) out.push(flags[0].name);
if (part.name === PP_KEY.ANSWER && aiActive(part)) {
const type = getParam(part, PP_KEY.AI_TYPE);
const model = getParam(part, PP_KEY.AI_MODEL);
if (type && model) out.push([type.value, model.value].filter(Boolean).join(" "));
}
const speed = getParam(part, PP_KEY.RATE);
if (speed) out.push(speed.value + "x");
return out.length ? out.join(" · ") : "默认";
};
const openPart = (name) => {
activePartName.value = name;
uiView.value = "detail";
};
const closePart = () => {
uiView.value = "index";
};
vue.watch(() => configStore.platformName, closePart);
const maskToken = (value) => {
const raw = String(value || "");
return raw.length > 8 ? raw.slice(0, 4) + "****" + raw.slice(-4) : raw;
};
const hasWelfareCard = () => !!(PP_WELFARE_OFFER || (typeof _unsafeWindow !== "undefined" ? _unsafeWindow.__PP_WELFARE__ : null));
const sponsorLink = (label) => !hasWelfareCard() && SPONSOR_URL
? vue.h("a", { href: SPONSOR_URL, target: "_blank" }, label || "点我赞助获取新 Token")
: null;
const renderTokenNote = () => {
const status = verifyState.value.status;
if (status === "testing") return ppHint("正在验证 Token,请稍候…", { icon: "clock" });
if (status === "error") {
return vue.h("div", { class: "pp-note bad" }, [
ppIcon("alert", 11),
vue.h("span", null, [verifyState.value.message || "验证失败", " ", sponsorLink()])
]);
}
if (!token.value) {
return vue.h("div", { class: "pp-note" }, [
ppIcon("alert", 11),
vue.h("span", null, ["未填写 Token,答题前请先验证。", sponsorLink("获取 Token")])
]);
}
if (status === "success" && remainingCount.value !== null && remainingCount.value <= 0) {
return vue.h("div", { class: "pp-note" }, [
ppIcon("alert", 11),
vue.h("span", null, ["剩余次数已用完。", sponsorLink()])
]);
}
if (!configStore.tokenVerified && status !== "success") {
return ppHint("Token 尚未验证,请点击验证", { tone: "warn", icon: "alert" });
}
return null;
};
const renderWelfareCard = () => {
const promo = PP_WELFARE_OFFER || (typeof _unsafeWindow !== "undefined" ? _unsafeWindow.__PP_WELFARE__ : null);
if (!promo || promo.enabled === false) return null;
let plans = Array.isArray(promo.plans) ? promo.plans : [];
if (!plans.length && Array.isArray(globalScriptDescription?.plans)) {
plans = globalScriptDescription.plans
.filter((plan) => Number(plan.count) > 0)
.map((plan) => ({ from: Number(plan.count), to: Math.ceil(Number(plan.count) * 1.3) }));
}
const link = promo.link || SPONSOR_URL;
if (!link) return null;
return vue.h("section", { class: "pp-welfare" }, [
vue.h("div", { class: "pp-welfare-top" }, [
vue.h("span", { class: "pp-welfare-ico" }, ppIcon("gift", 14)),
vue.h("div", { class: "pp-welfare-copy" }, [
vue.h("b", null, promo.title || "专属福利"),
vue.h("span", null, promo.text || "赞助加赠 30% 额度")
]),
vue.h("span", { class: "pp-welfare-tag" }, "未领取")
]),
plans.length
? vue.h("div", { class: "pp-welfare-plans" }, plans.map((plan, index) =>
vue.h("span", { key: index }, plan.from + "→" + plan.to)
))
: null,
vue.h("div", { class: "pp-welfare-foot" }, [
vue.h("span", { class: "pp-welfare-note" }, "首单专享 · 仅一次"),
vue.h("a", {
class: "pp-welfare-btn",
href: link,
target: "_blank",
rel: "noopener noreferrer"
}, ["去赞助", ppIcon("external", 11)])
])
]);
};
const renderTokenCard = () => {
const Input = vue.resolveComponent("el-input");
const Button = vue.resolveComponent("el-button");
const remain = remainingCount.value;
const remainClass = remain === null ? "" : remain <= 0 ? "zero" : remain <= 20 ? "low" : "";
const onTokenInput = (value) => {
const oldValue = configStore.queryApis[0].token;
configStore.queryApis[0].token = value;
configStore.tokenVerified = false;
configStore.tokenVerifyError = null;
remainingCount.value = null;
pingDelay.value = null;
verifyState.value = { status: "", message: "" };
if (oldValue !== value) autoVerifyToken();
};
return vue.h("section", { class: "pp-token" }, [
vue.h("div", { class: "pp-token-head" }, [
vue.h("span", { class: "pp-token-key" }, ppIcon("key", 13)),
vue.h("span", { class: "pp-token-title" }, "题库 Token"),
vue.h("div", { class: "pp-token-chips" }, [
remain !== null ? ppPill("剩余 " + remain, "remain " + remainClass) : null,
pingDelay.value !== null
? (pingDelay.value === -1 ? ppPill("连接失败", "remain zero") : ppPill(pingDelay.value + "ms", "latency"))
: null
])
]),
vue.h("div", { class: "pp-input-row" }, [
vue.h(Input, {
modelValue: token.value,
size: "small",
clearable: true,
placeholder: "请输入 24 位数字 Token",
"onUpdate:modelValue": onTokenInput,
onKeyup: (event) => {
if (event && event.key === "Enter") verifyToken();
}
}),
vue.h(Button, {
type: "primary",
size: "small",
loading: verifyState.value.status === "testing",
onClick: verifyToken
}, { default: () => "验证" })
]),
renderTokenNote(),
existingTokens.value.length
? vue.h("div", { class: "pp-token-list" }, existingTokens.value.map((item) =>
vue.h("div", { class: "pp-token-item", key: item.token }, [
vue.h("code", null, maskToken(item.token)),
vue.h("span", null, item.remainingCount + " 次"),
vue.h("button", {
type: "button",
class: "pp-token-mini",
onClick: () => selectToken(item.token)
}, "使用")
])
))
: null
]);
};
const renderIndex = () => vue.h("div", { class: "pp-settings" }, [
renderWelfareCard(),
renderTokenCard(),
vue.h("div", { class: "pp-scale-card" }, [
vue.h("div", { class: "pp-scale-head" }, [
vue.h("b", null, "界面缩放"),
vue.h("span", null, "即时生效")
]),
vue.h("div", { class: "pp-scale-options" }, [100, 125, 150].map((value) =>
vue.h("button", {
type: "button",
key: value,
class: ["pp-scale-btn", Number(configStore.uiScale) === value ? "is-active" : ""],
onClick: () => { configStore.uiScale = value; }
}, value + "%")
)),
vue.h("p", { class: "pp-scale-note" }, "放大面板、字体与间距,小屏会自动限制高度")
]),
vue.h("div", { class: "pp-list-head" }, [
vue.h("b", null, "功能设置"),
vue.h("span", null, "更改后刷新页面生效")
]),
vue.h("div", { class: "pp-setting-list" }, parts.value.map((part) => vue.h("button", {
type: "button",
class: "pp-set-row",
key: part.name,
onClick: () => openPart(part.name)
}, [
vue.h("span", { class: "pp-set-ico" }, ppIcon(PART_ICONS[part.name] || "sliders", 14)),
vue.h("span", { class: "pp-set-main" }, [
vue.h("span", { class: "pp-set-name" }, [
part.name,
part.name === PP_KEY.ANSWER && !configStore.tokenVerified
? vue.h("span", { class: "pp-set-badge" }, "待验证")
: null
]),
vue.h("span", { class: "pp-set-desc" }, partSummary(part))
]),
vue.h("span", { class: "pp-set-chev" }, ppIcon("chevronRight", 13))
]))),
vue.h("div", { class: "pp-settings-note" }, "设置会即时保存,部分项刷新页面后生效")
]);
const renderParam = (part, param) => {
if (param.type === "boolean") {
const Switch = vue.resolveComponent("el-switch");
const disabled = isParamDisabled(part, param);
return ppRow(param.name, vue.h(Switch, {
modelValue: !!param.value,
size: "small",
disabled,
"onUpdate:modelValue": (value) => applyBool(part, param, value)
}), {
off: disabled,
muted: param.locked ? "固定" : (disabled && param.dependsOn ? "需开启" + param.dependsOn.param : "")
});
}
if (param.type === "number") {
const NumberInput = vue.resolveComponent("el-input-number");
return ppRow(param.name, vue.h(NumberInput, {
modelValue: param.value,
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,
controlsPosition: "right",
size: "small",
"onUpdate:modelValue": (value) => {
param.value = value === undefined || value === null ? param.value : value;
triggerUiUpdate();
}
}), { unit: PARAM_UNITS[param.name] || "" });
}
return null;
};
const renderAiBlock = (part) => {
const Select = vue.resolveComponent("el-select");
const Option = vue.resolveComponent("el-option");
const Switch = vue.resolveComponent("el-switch");
const typeParam = getParam(part, PP_KEY.AI_TYPE);
const modelParam = getParam(part, PP_KEY.AI_MODEL);
const webParam = getParam(part, PP_KEY.WEB_SEARCH);
const active = aiActive(part);
const typeOptions = typeParam.options || [];
const modelOptions = (globalModelConfig?.typeModelMap?.[typeParam.value]) || modelParam.options || [];
const modelCostBadge = (option) => {
const id = getModelType(typeParam.value, option);
const raw = id ? String(modelCosts.value[id] || "") : "";
if (!/消耗/.test(raw)) return null;
const label = raw.replace(/模型$/, "");
const tone = /超高/.test(label) ? "is-ultra" : /高/.test(label) ? "is-high" : /中/.test(label) ? "is-mid" : "is-low";
return vue.h("span", { class: ["pp-cost-badge", tone] }, label);
};
const select = (value, options, disabled, onChange, renderOption) => vue.h(Select, {
modelValue: value,
size: "small",
disabled,
popperClass: "high-z-index-select",
"onUpdate:modelValue": onChange
}, {
default: () => options.map((option) => vue.h(Option, {
key: option,
value: option,
label: option
}, renderOption ? {
default: () => renderOption(option)
} : undefined))
});
return vue.h("div", { class: ["pp-ai", active ? "" : "pp-off"] }, [
vue.h("div", { class: "pp-ai-row" }, [
vue.h("span", { class: "pp-ai-label" }, "类型"),
select(typeParam.value, typeOptions, !active, (value) => {
typeParam.value = value;
modelParam.value = (globalModelConfig?.defaultModels || {})[value] || "Standard";
triggerUiUpdate();
})
]),
vue.h("div", { class: "pp-ai-row" }, [
vue.h("span", { class: "pp-ai-label" }, "模型"),
select(modelParam.value, modelOptions, !active, (value) => {
modelParam.value = value;
triggerUiUpdate();
}, (option) => vue.h("div", { class: "pp-cfg-model-option" }, [
vue.h("span", { class: "pp-cfg-model-name" }, option),
modelCostBadge(option)
]))
]),
webParam ? vue.h("div", { class: "pp-ai-row" }, [
vue.h("span", { class: "pp-ai-label" }, "联网"),
vue.h(Switch, {
modelValue: !!webParam.value,
size: "small",
disabled: !active,
"onUpdate:modelValue": (value) => applyBool(part, webParam, value)
}),
vue.h("span", { class: "pp-ai-note" },
active ? (webParam.value ? "已开启,增加消耗" : "可选,提高正确率") : "需开启 AI 模式"
)
]) : null
]);
};
const renderHints = (part) => {
const hints = [];
if (hasParam(part, PP_KEY.STANDBY)) {
hints.push(ppHint("浏览器最小化刷课时,请开启激活挂机", { tone: "ok", icon: "check" }));
}
if (part.name === PP_KEY.ANSWER) {
const verify = getParam(part, PP_KEY.VERIFY);
if (verify && verify.value) {
hints.push(ppHint("校验模式会同时请求题库与 AI,答题次数多倍消耗、耗时更长", { tone: "warn", icon: "alert" }));
}
const web = getParam(part, PP_KEY.WEB_SEARCH);
if (aiActive(part) && web && web.value) {
hints.push(ppHint("联网搜索会显著增加 Token 与答题次数消耗", { tone: "warn", icon: "alert" }));
}
}
if (part.name === PP_KEY.VIDEO) {
const breakRate = getParam(part, PP_KEY.BREAK_RATE);
const autoRate = getParam(part, PP_KEY.AUTO_RATE);
const simulate = getParam(part, PP_KEY.SIMULATE);
const manual = getParam(part, PP_KEY.MANUAL_RATE);
const rateParam = getParam(part, PP_KEY.RATE);
const rateNum = Number(rateParam && rateParam.value) || 0;
const forced = !!(breakRate && breakRate.value);
if (forced) {
hints.push(ppHint("突破倍速会强制解锁到设定值,过高倍速可能触发平台异常、学习进度被清空,请谨慎使用", { tone: "warn", icon: "alert" }));
} else if (autoRate && autoRate.value) {
hints.push(ppHint("自动倍速受播放器限制,将使用允许的最大值", { icon: "info" }));
} else if (simulate && simulate.value) {
hints.push(ppHint("模拟播放会使用播放器允许的最大倍速", { icon: "info" }));
} else if (manual && manual.value) {
hints.push(ppHint("手动倍速受播放器限制,最大为播放器允许值", { icon: "info" }));
}
if (rateNum > 4) {
hints.push(ppHint(`当前设为 ${rateNum}x,偏高倍速可能被平台判定异常、学习记录被清空,建议不超过 4x`, { tone: "warn", icon: "alert" }));
}
}
return hints;
};
const renderPartBody = (part) => {
const nodes = [];
boolGroups(part).forEach((params, group) => {
const active = params.find((p) => p.value);
const title = groupTitle(part, group);
nodes.push(vue.h("div", { class: "pp-group", key: "g-" + group }, [
title ? vue.h("div", { class: "pp-group-label" }, title) : null,
ppSeg(
params.map((p) => ({ value: p.name, label: p.name, disabled: isParamDisabled(part, p) })),
active ? active.name : "",
(name) => {
const param = params.find((p) => p.name === name);
if (param) applyBool(part, param, true);
}
)
]));
if (part.name === PP_KEY.MODE && active && MODE_DESCS[active.name]) {
nodes.push(ppHint(MODE_DESCS[active.name], { icon: "info" }));
}
});
if (hasAiBlock(part)) nodes.push(renderAiBlock(part));
looseParams(part).forEach((param) => {
const row = renderParam(part, param);
if (row) nodes.push(row);
});
renderHints(part).forEach((hint) => nodes.push(hint));
return nodes;
};
const renderDetail = () => {
const part = parts.value.find((p) => p.name === activePartName.value);
if (!part) return renderIndex();
return vue.h("div", { class: "pp-settings" }, [
vue.h("div", { class: "pp-detail-head" }, [
vue.h("button", { type: "button", class: "pp-back", onClick: closePart, title: "返回设置" }, ppIcon("chevronLeft", 14)),
vue.h("span", { class: "pp-detail-title" }, part.name),
part.name === PP_KEY.ANSWER ? vue.h("span", { class: "pp-detail-extra" }, consumptionText.value) : null
]),
vue.h("div", { class: "pp-detail-desc" }, PART_DESCS[part.name] || ""),
...renderPartBody(part),
vue.h("div", { class: "pp-settings-note" }, "更改后刷新页面生效")
]);
};
return () => {
void _uiUpdateTrigger.value;
return uiView.value === "index" ? renderIndex() : renderDetail();
};
}
});
const ScriptSetting = _export_sfc(_sfc_main$7, [["__scopeId", "data-v-9ea68a6a"]]);
const SOURCE_LABELS = {
tiku: "题库",
ai: "AI",
verify: "校验",
cache: "缓存"
};
const _sfc_main$6 = vue.defineComponent({
name: "QuestionTable",
props: {
questionList: { type: Array, required: true },
onGoSettings: { type: Function, default: null }
},
setup(props) {
const progress = useProgressStore();
const config = useConfigStore();
const answersOf = (row) => Array.isArray(row.answer) ? row.answer : (row.answer ? [row.answer] : []);
const tokenProblem = vue.computed(() => {
const token = String(config.queryApis?.[0]?.token || "").trim();
if (!token) return "未配置密钥,无法使用答题功能";
if (config.tokenVerified) return "";
if (config.tokenVerifyError === "invalid" || config.tokenVerifyError === "format") {
return "密钥无效,无法使用答题功能";
}
if (config.tokenVerifyError === "network") return "密钥验证失败,无法使用答题功能";
return "密钥尚未验证,请先验证后使用答题功能";
});
const currentTotal = vue.computed(() => {
const total = Number(progress.questionTotal) || 0;
return total > 0 ? total : props.questionList.length;
});
const currentRecords = vue.computed(() => {
const total = Number(progress.questionTotal) || 0;
return total > 0 ? props.questionList.slice(-total) : props.questionList.slice();
});
const fillRate = vue.computed(() => {
const total = currentTotal.value;
if (total <= 0) return 0;
const filled = Number(progress.filledCount) || 0;
if (filled > 0) return Math.min(100, Math.round((filled / total) * 10000) / 100);
const answered = currentRecords.value.filter((row) => answersOf(row).length).length;
return Math.min(100, Math.round((answered / total) * 10000) / 100);
});
const remaining = vue.computed(() => {
const value = Number(progress.remainingCount);
return Number.isFinite(value) ? value : null;
});
const statusOf = (index) => {
const rows = currentRecords.value;
if (index < rows.length) {
const row = rows[index];
if (row.source === "error" || row._fillFailed || (!answersOf(row).length && !row.skipped)) return "error";
if (row.skipped) return "skipped";
if (row.source === "ai") return "ai";
if (row.source === "verify") return "verify";
if (row.source === "cache") return "cache";
return "bank";
}
if (progress.isPlaying && progress.type === "答题" && index === rows.length) return "searching";
return "pending";
};
const statusLabel = {
searching: "搜索中",
bank: "题库",
ai: "AI",
verify: "校验",
cache: "缓存",
skipped: "跳过",
error: "异常",
pending: "待处理"
};
const goSettings = () => {
try { if (typeof props.onGoSettings === "function") props.onGoSettings(); } catch (e) {}
};
const renderTips = () => tokenProblem.value ? vue.h("section", { class: "pp-rec-tips is-warning" }, [
vue.h("div", { class: "pp-rec-tip" }, [
vue.h("span", { class: "pp-rec-tip-ico" }, ppIcon("alert", 11)),
vue.h("span", { class: "pp-rec-tip-text" }, tokenProblem.value),
vue.h("button", { type: "button", class: "pp-rec-config", onClick: goSettings }, [
ppIcon("key", 10),
"配置密钥"
])
])
]) : null;
const renderSummary = () => currentTotal.value > 0 ? vue.h("div", { class: "pp-rec-summary" }, [
vue.h("span", null, ["答题进度 ", vue.h("b", null, currentRecords.value.length + " / " + currentTotal.value)]),
vue.h("span", { class: "pp-rec-sum-sep" }, "|"),
vue.h("span", null, ["填入率 ", vue.h("b", null, fillRate.value + "%")]),
vue.h("span", { class: "pp-rec-sum-sep" }, "|"),
vue.h("span", null, ["剩余次数 ", vue.h("b", null, remaining.value === null ? "--" : remaining.value)])
]) : null;
const renderLegend = () => currentTotal.value > 0 ? vue.h("div", { class: "pp-rec-legend" }, [
vue.h("span", { class: "pp-legend-item" }, [vue.h("i", { class: "pp-legend-dot is-searching" }), "搜索中"]),
vue.h("span", { class: "pp-legend-item" }, [vue.h("i", { class: "pp-legend-dot is-bank" }), "题库"]),
vue.h("span", { class: "pp-legend-item" }, [vue.h("i", { class: "pp-legend-dot is-ai" }), "AI"]),
vue.h("span", { class: "pp-legend-item" }, [vue.h("i", { class: "pp-legend-dot is-verify" }), "校验"]),
vue.h("span", { class: "pp-legend-item" }, [vue.h("i", { class: "pp-legend-dot is-cache" }), "缓存"]),
vue.h("span", { class: "pp-legend-item" }, [vue.h("i", { class: "pp-legend-dot is-skipped" }), "跳过"]),
vue.h("span", { class: "pp-legend-item" }, [vue.h("i", { class: "pp-legend-dot is-error" }), "异常"])
]) : null;
const renderCircles = () => currentTotal.value > 0 ? vue.h("div", { class: "pp-q-circles" },
Array.from({ length: currentTotal.value }, (_, index) => {
const status = statusOf(index);
return vue.h("span", {
key: index,
class: ["pp-q-circle", "is-" + status],
title: "第 " + (index + 1) + " 题 · " + (statusLabel[status] || status)
}, String(index + 1));
})
) : null;
const renderRecord = (row, index) => vue.h("article", { class: "pp-rec", key: index }, [
vue.h("div", { class: "pp-rec-top" }, [
vue.h("span", { class: "pp-rec-idx" }, index + 1),
row.skipped ? vue.h("span", { class: "pp-tag is-skipped" }, "跳过") : null,
row.source === "ai" ? vue.h("span", { class: "pp-tag is-ai" }, "AI") : null,
row.source === "verify" ? vue.h("span", { class: "pp-tag is-verify" }, "校验") : null,
row.source === "cache" ? vue.h("span", { class: "pp-tag is-cache" }, "缓存") : null
]),
row.titleHtml
? vue.h("div", { class: "pp-rec-q", innerHTML: row.titleHtml, title: row.title || "" })
: vue.h("div", { class: "pp-rec-q", title: row.title || "" }, row.title || "(无题干)"),
vue.h("div", { class: "pp-rec-a" },
answersOf(row).length
? answersOf(row).map((answer, i) => vue.h("span", { class: "pp-ans", key: i, title: String(answer) }, answer))
: vue.h("span", { class: "pp-ans is-empty" }, "暂无答案")
)
]);
return () => vue.h("div", { class: "pp-records" }, [
renderTips(),
renderSummary(),
renderLegend(),
renderCircles(),
vue.h("div", { class: "pp-list-head" }, [
vue.h("b", null, "答题记录"),
vue.h("span", null, "共 " + props.questionList.length + " 题")
]),
props.questionList.length
? props.questionList.map(renderRecord)
: ppEmpty({ icon: "list", title: "还没有答题记录", desc: "进入作业、章节测验或考试后,答题结果会汇总在这里" })
]);
}
});
const QuestionTable = _export_sfc(_sfc_main$6, [["__scopeId", "data-v-18523ca7"]]);
const TOKEN_USAGE_MODE_OPTIONS = [
{ label: "全部模式", value: "" },
{ label: PP_KEY.NORMAL_MODE, value: PP_KEY.NORMAL_MODE },
{ label: "校验模式", value: "校验模式" },
{ label: PP_KEY.AI_MODE, value: PP_KEY.AI_MODE },
{ label: "AI聊天", value: "AI聊天" }
];
const _sfc_main_token_usage = vue.defineComponent({
__name: "TokenUsagePanel",
props: {
active: { type: Function, default: null }
},
setup(__props) {
const configStore = useConfigStore();
const getConfigToken = () => {
const apis = configStore.queryApis || [];
const raw = apis[0] && apis[0].token ? String(apis[0].token).trim() : "";
return /^\d{16}$|^\d{24}$/.test(raw) ? raw : "";
};
const loading = vue.ref(false);
const errorMsg = vue.ref("");
const records = vue.ref([]);
const total = vue.ref(0);
const page = vue.ref(1);
const pageSize = vue.ref(50);
const modeFilter = vue.ref("");
const tokenInput = vue.ref(getConfigToken());
const queried = vue.ref(false);
const lastUpdated = vue.ref("");
const isTokenValid = (value) => /^\d{16}$|^\d{24}$/.test(String(value || "").trim());
const pad2 = (n) => n < 10 ? "0" + n : "" + n;
const formatTs = (ts, mode) => {
const num = Number(ts);
if (!num) return mode === "clock" ? "" : "-";
const d = new Date(num * 1000);
if (isNaN(d.getTime())) return mode === "clock" ? "" : "-";
const ymd = d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
const hms = pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds());
if (mode === "clock") return hms;
if (mode === "date") return ymd;
return ymd + " " + hms;
};
const formatTime = (ts) => formatTs(ts, "datetime");
const formatNumber = (value) => (Number(value) || 0).toLocaleString("zh-CN");
const formatDate = (ts) => formatTs(ts, "date");
const formatClock = (ts) => formatTs(ts, "clock");
const remainingText = (value) => (value === null || value === undefined) ? "—" : formatNumber(value);
const remainingColor = (value) => {
const num = Number(value);
if (value === null || value === undefined || !Number.isFinite(num)) return "#c0c4cc";
if (num <= 5) return "#d92d20";
if (num <= 20) return "#d97706";
return "#111827";
};
const maskedInput = vue.computed(() => {
const raw = String(tokenInput.value || "").trim();
return /^\d{16}$|^\d{24}$/.test(raw) ? raw.substring(0, 4) + "****" + raw.substring(raw.length - 4) : "";
});
const CHIP_BASE_STYLE = {
"display": "inline-block",
"box-sizing": "border-box",
"max-width": "100%",
"overflow": "hidden",
"text-overflow": "ellipsis",
"white-space": "nowrap",
"font-size": "12px",
"line-height": "18px",
"padding": "1px 5px",
"border-radius": "4px"
};
const SOURCE_CHIP_STYLES = {
"缓存": { "background": "#f4f4f5", "border": "1px solid #e9e9eb", "color": "#909399" },
"题库": { "background": "#f0f9eb", "border": "1px solid #e1f3d8", "color": "#67c23a" },
"ai补充": { "background": "#fdf6ec", "border": "1px solid #faecd8", "color": "#e6a23c" },
"默认": { "background": "#ecf5ff", "border": "1px solid #d9ecff", "color": "#409eff" }
};
const MODE_CHIP_STYLES = {
[PP_KEY.NORMAL_MODE]: { "background": "#ecf5ff", "border": "1px solid #d9ecff", "color": "#409eff" },
"校验模式": { "background": "#fdf6ec", "border": "1px solid #faecd8", "color": "#e6a23c" },
[PP_KEY.AI_MODE]: { "background": "#f0f9eb", "border": "1px solid #e1f3d8", "color": "#67c23a" },
"AI聊天": { "background": "#eef7ff", "border": "1px solid #cfe7ff", "color": "#0369a1" },
"默认": { "background": "#f4f4f5", "border": "1px solid #e9e9eb", "color": "#909399" }
};
const sourceChipStyle = (source) => Object.assign({}, CHIP_BASE_STYLE, SOURCE_CHIP_STYLES[source] || SOURCE_CHIP_STYLES["默认"]);
const modeChipStyle = (mode) => Object.assign({}, CHIP_BASE_STYLE, MODE_CHIP_STYLES[mode] || MODE_CHIP_STYLES["默认"]);
const modelsText = (models) => {
if (!Array.isArray(models) || models.length === 0) return "无模型明细";
return models.map((m) => (m.name || "未知模型") + "(输入" + (m.promptTokens || 0) + " / 输出" + (m.completionTokens || 0) + ")").join(";");
};
const detailText = (row) => {
if (Array.isArray(row.models) && row.models.length > 0) return modelsText(row.models);
return "无模型明细";
};
const modelLabel = (row) => {
const names = (Array.isArray(row.models) ? row.models : [])
.map((model) => String(model && model.name ? model.name : "").trim())
.filter(Boolean);
if (!names.length) return row.mode === PP_KEY.AI_MODE ? "模型未记录" : "";
const unique = [...new Set(names)];
return unique.length > 1 ? unique[0] + "(等)" : unique[0];
};
const fetchRecords = (targetPage) => {
const token = String(tokenInput.value || "").trim();
if (!isTokenValid(token)) {
loading.value = false;
records.value = [];
total.value = 0;
queried.value = false;
errorMsg.value = "请输入正确的24位数字Token";
return;
}
const usePage = Math.max(1, parseInt(targetPage, 10) || 1);
loading.value = true;
errorMsg.value = "";
let url = getRandomServer() + "/usage-records?token=" + encodeURIComponent(token) + "&page=" + usePage + "&size=" + pageSize.value;
if (modeFilter.value) url += "&mode=" + encodeURIComponent(modeFilter.value);
_GM_xmlhttpRequest({
method: "GET",
url,
timeout: 10000,
onload: (response) => {
loading.value = false;
queried.value = true;
let res = null;
try {
res = JSON.parse(response.responseText);
} catch (e) {
res = null;
}
if (!res || res.code !== 200 || !res.data) {
errorMsg.value = res && res.msg ? res.msg : "查询失败,请稍后重试";
records.value = [];
total.value = 0;
return;
}
const data = res.data;
records.value = Array.isArray(data.records) ? data.records : [];
total.value = Number(data.total) || 0;
page.value = Number(data.page) || usePage;
pageSize.value = Number(data.pageSize) || pageSize.value;
lastUpdated.value = formatTime(Math.floor(Date.now() / 1000));
},
onerror: () => {
loading.value = false;
errorMsg.value = "网络异常,无法连接服务器";
},
ontimeout: () => {
loading.value = false;
errorMsg.value = "查询超时,请稍后重试";
}
});
};
const handleQuery = () => {
page.value = 1;
fetchRecords(1);
};
const handleModeChange = (value) => {
modeFilter.value = value || "";
if (isTokenValid(tokenInput.value)) {
page.value = 1;
fetchRecords(1);
}
};
const handlePageChange = (value) => {
page.value = Number(value) || 1;
fetchRecords(page.value);
};
const handleKeyup = (event) => {
if (event && event.key === "Enter") handleQuery();
};
let wasActive = false;
const isActive = () => typeof __props.active === "function" ? Boolean(__props.active()) : true;
vue.watch(isActive, (active) => {
if (active && !wasActive) {
wasActive = true;
page.value = 1;
if (isTokenValid(tokenInput.value)) fetchRecords(1);
} else if (!active) {
wasActive = false;
}
}, { immediate: true });
vue.watch(() => configStore.queryApis?.[0]?.token, (value) => {
if (!tokenInput.value && value) tokenInput.value = value;
});
const expandedRow = vue.ref(-1);
vue.watch(records, () => { expandedRow.value = -1; });
const totalPages = vue.computed(() => Math.max(1, Math.ceil((Number(total.value) || 0) / (Number(pageSize.value) || 50))));
const renderModeBar = () => vue.h("div", { class: "pp-modebar" },
TOKEN_USAGE_MODE_OPTIONS.map((option) => vue.h("button", {
type: "button",
key: option.value || "all",
class: modeFilter.value === option.value ? "active" : "",
disabled: loading.value,
onClick: () => handleModeChange(option.value)
}, option.label))
);
const renderRecord = (row, index) => {
const open = expandedRow.value === index;
const detail = [];
if (row.source) detail.push("来源:" + (row.sourceName || SOURCE_LABELS[row.source] || row.source));
if (row.models && Array.isArray(row.models) && row.models.length) detail.push(detailText(row));
const modelText = modelLabel(row);
const modelTitle = detailText(row);
return vue.h("div", { class: "pp-u-item", key: index }, [
vue.h("button", {
type: "button",
class: "pp-u-row",
onClick: () => { expandedRow.value = open ? -1 : index; }
}, [
vue.h("span", { class: "pp-u-left" }, [
vue.h("span", { class: "pp-u-line" }, [
vue.h("span", { class: "pp-u-date" }, (() => { const d = formatDate(row.createdAt); return d === "-" ? "-" : d.slice(5); })()),
vue.h("span", { class: "pp-u-time" }, formatClock(row.createdAt)),
row.mode ? vue.h("span", { class: "pp-u-mode " + (row.mode === PP_KEY.AI_MODE ? "is-ai" : row.mode === "AI聊天" ? "is-chat" : row.mode === "校验模式" ? "is-verify" : row.mode === PP_KEY.NORMAL_MODE ? "is-normal" : "") }, row.mode) : null
]),
vue.h("span", { class: "pp-u-sub" }, [
vue.h("span", { class: "pp-u-sub-text" },
"输入 " + formatNumber(row.promptTokens) + " / 输出 " + formatNumber(row.completionTokens)
),
modelText ? vue.h("span", {
class: ["pp-u-model", modelText === "模型未记录" ? "is-empty" : ""],
title: modelTitle
}, modelText) : null
])
]),
vue.h("span", { class: "pp-u-right" }, [
vue.h("span", { class: "pp-u-cost" }, "-" + formatNumber(row.cost)),
vue.h("span", { class: "pp-u-rem" }, "剩 " + remainingText(row.remainingCount))
])
]),
open ? vue.h("div", { class: "pp-u-detail" }, detail.length ? detail.join(" · ") : "暂无明细") : null
]);
};
return (_ctx, _cache) => {
const Input = vue.resolveComponent("el-input");
const Button = vue.resolveComponent("el-button");
const pageCount = totalPages.value;
return vue.h("div", { class: "pp-usage" }, [
vue.h("section", { class: "pp-usage-card" }, [
vue.h("div", { class: "pp-usage-top" }, [
vue.h(Input, {
modelValue: tokenInput.value,
size: "small",
clearable: true,
placeholder: "输入 24 位数字 Token",
"onUpdate:modelValue": (value) => {
tokenInput.value = value;
errorMsg.value = "";
},
onKeyup: handleKeyup
}),
vue.h(Button, {
type: "primary",
size: "small",
loading: loading.value,
onClick: handleQuery
}, { default: () => "查询" })
]),
vue.h("div", { class: "pp-usage-meta" }, [
maskedInput.value ? vue.h("span", { class: "pp-token-chip" }, maskedInput.value) : null,
vue.h("span", null, lastUpdated.value ? "更新于 " + lastUpdated.value : "尚未查询")
])
]),
vue.h("div", { class: "pp-list-head" }, [
vue.h("b", null, "扣费流水"),
vue.h("span", null, queried.value ? "共 " + formatNumber(total.value) + " 条" : "未查询")
]),
renderModeBar(),
errorMsg.value
? vue.h("div", { class: "pp-note bad" }, [ppIcon("alert", 11), vue.h("span", null, errorMsg.value)])
: null,
vue.h("div", { class: "pp-u-list" },
records.value.length
? records.value.map(renderRecord)
: ppEmpty({
icon: "search",
title: queried.value ? "暂无使用记录" : "还没有查询",
desc: queried.value ? "该 Token 在所选模式下没有扣费记录" : "输入 Token 后点击查询,查看扣费流水"
})
),
vue.h("div", { class: "pp-pager" }, [
vue.h("span", null, "第 " + page.value + " / " + pageCount + " 页"),
vue.h("div", { class: "pp-pager-btns" }, [
vue.h("button", {
type: "button",
disabled: page.value <= 1 || loading.value,
onClick: () => handlePageChange(page.value - 1),
title: "上一页"
}, ppIcon("chevronLeft", 13)),
vue.h("button", {
type: "button",
disabled: page.value >= pageCount || loading.value,
onClick: () => handlePageChange(page.value + 1),
title: "下一页"
}, ppIcon("chevronRight", 13))
])
])
]);
};
}
});
const TokenUsagePanel = _sfc_main_token_usage;
const AI_CHAT_STORE_KEY = "pp_ai_sessions";
const AI_CHAT_MAX_SESSIONS = 10;
const AI_CHAT_MAX_LENGTH = 500;
const AI_CHAT_STAGE_TEXT = Object.freeze({
compressing: "正在压缩上下文",
searching: "正在搜索",
thinking: "正在思考",
generating: "正在生成"
});
const AI_CHAT_MODEL_KEY = "pp_ai_model";
const AI_CHAT_DEEP_KEY = "pp_ai_deep_thinking";
const AI_CHAT_WEB_KEY = "pp_ai_web_search";
const AI_CHAT_ICON_PATHS = Object.freeze({
ai: "M12 2l2.2 7.8L22 12l-7.8 2.2L12 22l-2.2-7.8L2 12l7.8-2.2z",
plus: "M12 5v14M5 12h14"
});
const aiChatIcon = (name, size) => {
const solid = name === "ai";
return vue.h("svg", {
class: "pp-icon pp-ai-icon",
viewBox: "0 0 24 24",
width: size || 15,
height: size || 15,
fill: solid ? "currentColor" : "none",
stroke: solid ? "none" : "currentColor",
"stroke-width": 1.8,
"stroke-linecap": "round",
"stroke-linejoin": "round",
"aria-hidden": "true"
}, [vue.h("path", { d: AI_CHAT_ICON_PATHS[name] || "" })]);
};
const aiChatIsModelError = (text) => {
const value = String(text || "");
return /(?:model|模型).{0,20}(?:invalid|unsupported|not\s*found|not\s*allowed|unavailable|rejected|unknown|required|不支持|不可用|无效|不存在|下线|停用|拒绝|未授权)|(?:invalid|unsupported|not\s*found|not\s*allowed|unavailable|rejected|unknown|required).{0,20}(?:model|模型)/i.test(value);
};
const AI_CHAT_HTTPS_URLS = Object.freeze([
"https://122.152.249.109:3001",
"https://152.136.30.238:3001"
]);
const getAiChatServer = () => {
try {
const currentHost = new URL(getRandomServer()).hostname;
const matched = AI_CHAT_HTTPS_URLS.find((url) => new URL(url).hostname === currentHost);
return matched || AI_CHAT_HTTPS_URLS[0];
} catch (e) {
return AI_CHAT_HTTPS_URLS[0];
}
};
const aiChatEscapeHtml = (value) => String(value == null ? "" : value)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
const aiChatSafeHref = (raw) => {
try {
const href = new URL(String(raw || "").replace(/&/g, "&"), window.location.href);
if (href.protocol !== "http:" && href.protocol !== "https:" && href.protocol !== "mailto:") return "";
return href.href.replace(/&/g, "&").replace(/"/g, "%22");
} catch (e) {
return "";
}
};
const aiChatSourceLink = (raw) => {
try {
const href = new URL(String(raw || "").replace(/&/g, "&"), window.location.href);
if (href.protocol !== "http:" && href.protocol !== "https:") return null;
return {
href: href.href.replace(/&/g, "&").replace(/"/g, "%22"),
host: href.hostname.replace(/^www\./i, "")
};
} catch (e) {
return null;
}
};
const aiChatRenderInline = (escaped) => {
const codeSlots = [];
let text = String(escaped || "").replace(/`([^`\n]+)`/g, (match, code) => {
codeSlots.push(code);
return "\u0001C" + (codeSlots.length - 1) + "\u0001";
});
text = text.replace(/\*\*([^*\n]+)\*\*/g, "$1");
text = text.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, (match, label, href) => {
const safe = aiChatSafeHref(href);
return safe ? '' + label + "" : label;
});
return text.replace(/\u0001C(\d+)\u0001/g, (match, index) => {
const code = codeSlots[Number(index)];
return code === undefined ? "" : "" + code + "";
});
};
const aiChatRenderProse = (chunk) => {
const text = String(chunk || "").replace(/^\n+|\n+$/g, "");
if (!text) return "";
return text.split(/\n{2,}/).map((paragraph) =>
"" + aiChatRenderInline(aiChatEscapeHtml(paragraph)).replace(/\n/g, "
") + "
"
).join("");
};
const aiChatRenderMarkdown = (raw) => {
const source = String(raw == null ? "" : raw).replace(/\r\n?/g, "\n");
if (!source) return "";
const parts = [];
let cursor = 0;
const fence = /```([^\n`]*)\n?([\s\S]*?)(?:```|$)/g;
let match;
while ((match = fence.exec(source)) !== null) {
if (match.index > cursor) parts.push(aiChatRenderProse(source.slice(cursor, match.index)));
const lang = String(match[1] || "").trim();
const langClass = /^[\w+#.-]{1,20}$/.test(lang) ? ' class="language-' + aiChatEscapeHtml(lang) + '"' : "";
parts.push('" + aiChatEscapeHtml(String(match[2] || "").replace(/\n$/, "")) + "
");
cursor = fence.lastIndex;
if (fence.lastIndex <= match.index) fence.lastIndex = match.index + 1;
}
if (cursor < source.length) parts.push(aiChatRenderProse(source.slice(cursor)));
return parts.join("");
};
const AiChatPanel = vue.defineComponent({
name: "AiChatPanel",
setup() {
const configStore = useConfigStore();
const token = vue.computed(() => {
const raw = configStore.queryApis && configStore.queryApis[0] ? configStore.queryApis[0].token : "";
return raw == null ? "" : String(raw).trim();
});
const hasToken = vue.computed(() => !!token.value);
const draft = vue.ref("");
const readStoredFlag = (key) => {
try {
const raw = typeof _GM_getValue === "function" ? _GM_getValue(key) : false;
return raw === true || raw === 1 || raw === "1" || raw === "true";
} catch (e) {
return false;
}
};
const deepThinking = vue.ref(readStoredFlag(AI_CHAT_DEEP_KEY));
const webSearch = vue.ref(readStoredFlag(AI_CHAT_WEB_KEY));
vue.watch(deepThinking, (value) => {
try { if (typeof _GM_setValue === "function") _GM_setValue(AI_CHAT_DEEP_KEY, !!value); } catch (e) {}
});
vue.watch(webSearch, (value) => {
try { if (typeof _GM_setValue === "function") _GM_setValue(AI_CHAT_WEB_KEY, !!value); } catch (e) {}
});
const messages = vue.ref([]);
const sessions = vue.ref([]);
const currentSessionId = vue.ref(null);
const showSessions = vue.ref(false);
const deletePending = vue.ref(false);
const deleting = vue.ref(false);
const streaming = vue.ref(false);
const loadingHistory = vue.ref(false);
const stage = vue.ref("");
const usage = vue.ref(null);
const modelName = vue.ref("");
const models = vue.ref([]);
const modelGroups = vue.ref([]);
const defaultModelId = vue.ref("");
const selectedModelId = vue.ref("");
const modelsLoaded = vue.ref(false);
const modelsLoading = vue.ref(false);
const showModels = vue.ref(false);
const notice = vue.ref(null);
const rootRef = vue.ref(null);
const listRef = vue.ref(null);
const inputRef = vue.ref(null);
const hasActivated = vue.ref(false);
let messageSeq = 0;
let streamSeq = 0;
let historySeq = 0;
let abortController = null;
let scrollFrame = 0;
const PPFX_STREAM_FLUSH_MS = 55;
const PPFX_SCROLL_NEAR_PX = 32;
let ppfxStickBottom = true;
let ppfxStreamTarget = null;
let ppfxStreamBuffer = "";
let ppfxStreamTimer = 0;
let noticeTimer = null;
let activationObserver = null;
let composing = false;
const readStoredModelId = () => {
try {
const raw = typeof _GM_getValue === "function" ? _GM_getValue(AI_CHAT_MODEL_KEY) : "";
return raw == null ? "" : String(raw).trim();
} catch (e) {
return "";
}
};
selectedModelId.value = readStoredModelId();
const nextMessageId = () => "m" + (++messageSeq) + "-" + Date.now().toString(36);
const cleanTitle = (value, max) => String(value || "").replace(/\s+/g, " ").trim().slice(0, max || 60);
const parseJson = (raw) => {
try { return JSON.parse(raw); } catch (e) { return null; }
};
const pad2 = (value) => String(value).padStart(2, "0");
const formatNumber = (value) => {
const num = Number(value);
return Number.isFinite(num) ? num.toLocaleString("zh-CN") : String(value);
};
const persistModel = (id) => {
try {
if (typeof _GM_setValue === "function") _GM_setValue(AI_CHAT_MODEL_KEY, String(id || ""));
} catch (e) {}
};
const normalizeModelEntry = (item) => {
if (!item) return null;
if (typeof item === "string") {
const id = item.trim();
return id ? { id, label: id, type: "其他" } : null;
}
if (typeof item !== "object") return null;
const id = String(item.id || item.value || item.model || item.key || "").trim();
if (!id) return null;
const label = String(item.label || item.name || item.title || id).trim() || id;
const type = String(item.type || item.group || item.provider || item.vendor || "其他").trim() || "其他";
return {
id,
label,
type,
cost: item.cost,
protocol: item.protocol,
thinkingControl: ["toggle", "effort", "always", "none"].includes(item.thinkingControl)
? item.thinkingControl
: ""
};
};
const normalizeModelPayload = (payload) => {
let data = payload;
if (data && typeof data === "object" && !Array.isArray(data) && data.data !== undefined) data = data.data;
let rawGroups = [];
let rawFlat = [];
let defaultId = "";
if (Array.isArray(data)) rawFlat = data;
else if (data && typeof data === "object") {
defaultId = String(data.default || data.defaultModel || data.default_model || "").trim();
if (Array.isArray(data.groups)) rawGroups = data.groups;
if (Array.isArray(data.models)) rawFlat = data.models;
else if (Array.isArray(data.list)) rawFlat = data.list;
else if (Array.isArray(data.items)) rawFlat = data.items;
else if (Array.isArray(data.data)) rawFlat = data.data;
}
const seen = Object.create(null);
const groups = [];
for (const group of rawGroups) {
if (!group || typeof group !== "object") continue;
const type = String(group.type || group.label || group.name || "其他").trim() || "其他";
const label = String(group.label || group.type || group.name || "其他").trim() || type;
const items = Array.isArray(group.models) ? group.models
: Array.isArray(group.items) ? group.items
: Array.isArray(group.list) ? group.list
: [];
const groupModels = [];
for (const item of items) {
const model = normalizeModelEntry(item);
if (!model || seen[model.id]) continue;
if (model.type === "其他") model.type = type;
seen[model.id] = true;
groupModels.push(model);
}
if (groupModels.length) groups.push({ type, label, models: groupModels });
}
if (!groups.length && rawFlat.length) {
const groupModels = [];
for (const item of rawFlat) {
const model = normalizeModelEntry(item);
if (!model || seen[model.id]) continue;
seen[model.id] = true;
groupModels.push(model);
}
if (groupModels.length) groups.push({ type: "models", label: "模型", models: groupModels });
}
const list = groups.reduce((all, group) => all.concat(group.models), []);
return { defaultId, models: list, groups };
};
const normalizeStoredSessions = (value) => {
const seen = Object.create(null);
const result = [];
if (!Array.isArray(value)) return result;
for (const item of value) {
if (!item || typeof item !== "object") continue;
const id = String(item.id || "").trim();
if (!id || seen[id]) continue;
seen[id] = true;
result.push({
id,
title: cleanTitle(item.title, 60) || "新对话",
updatedAt: Number(item.updatedAt) || 0
});
}
result.sort((a, b) => b.updatedAt - a.updatedAt);
return result.slice(0, AI_CHAT_MAX_SESSIONS);
};
const readSessions = () => {
try {
const raw = typeof _GM_getValue === "function" ? _GM_getValue(AI_CHAT_STORE_KEY) : "";
if (!raw) return [];
return normalizeStoredSessions(typeof raw === "string" ? JSON.parse(raw) : raw);
} catch (e) {
return [];
}
};
const saveSessions = (list) => {
const clean = normalizeStoredSessions(list);
sessions.value = clean;
try {
if (typeof _GM_setValue === "function") _GM_setValue(AI_CHAT_STORE_KEY, JSON.stringify(clean));
} catch (e) {}
return clean;
};
sessions.value = readSessions();
const currentSession = vue.computed(() =>
sessions.value.find((item) => item.id === currentSessionId.value) || null
);
const currentTitle = vue.computed(() => currentSession.value ? currentSession.value.title : "新对话");
const titleFromText = (value) => cleanTitle(value, 24);
const fallbackTitle = () => {
const first = messages.value.find((item) => item.role === "user");
return titleFromText(first && first.content) || "新对话";
};
const upsertSession = (id, title) => {
if (id === null || id === undefined || id === "") return;
const sid = String(id);
const existing = sessions.value.find((item) => item.id === sid);
const nextTitle = cleanTitle(title, 60) || (existing && existing.title) || "新对话";
const list = sessions.value.filter((item) => item.id !== sid);
list.unshift({ id: sid, title: nextTitle, updatedAt: Date.now() });
saveSessions(list);
};
const removeSession = (id) => {
saveSessions(sessions.value.filter((item) => item.id !== id));
};
const formatSessionTime = (timestamp) => {
const num = Number(timestamp);
if (!num) return "";
const date = new Date(num);
if (isNaN(date.getTime())) return "";
const now = new Date();
const sameDay = date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate();
if (sameDay) return pad2(date.getHours()) + ":" + pad2(date.getMinutes());
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
if (date.getFullYear() === yesterday.getFullYear() &&
date.getMonth() === yesterday.getMonth() &&
date.getDate() === yesterday.getDate()) return "昨天";
if (date.getFullYear() === now.getFullYear()) return (date.getMonth() + 1) + "月" + date.getDate() + "日";
return date.getFullYear() + "/" + (date.getMonth() + 1) + "/" + date.getDate();
};
const setNotice = (text, tone, kind) => {
if (noticeTimer) {
clearTimeout(noticeTimer);
noticeTimer = null;
}
notice.value = text ? { text: String(text), tone: tone || "info", kind: kind || "" } : null;
if (notice.value && tone === "ok") {
noticeTimer = setTimeout(() => {
notice.value = null;
noticeTimer = null;
}, 2400);
}
};
const goConfig = () => {
try {
const host = document.getElementById("chaoxing-helper-root");
const root = host && host.shadowRoot ? host.shadowRoot : document;
const target = root.querySelector('.pp-rail-item[title="脚本配置"]');
if (target && typeof target.click === "function") {
target.click();
return;
}
const items = root.querySelectorAll(".pp-rail-item");
if (items && items[2] && typeof items[2].click === "function") items[2].click();
} catch (e) {}
};
const ppfxHandleListScroll = () => {
const el = listRef.value;
if (!el || !el.clientHeight) return;
ppfxStickBottom = (el.scrollHeight - el.clientHeight - el.scrollTop) <= PPFX_SCROLL_NEAR_PX;
};
const ppfxFollowTail = (force) => {
if (force) ppfxStickBottom = true;
if (!ppfxStickBottom) return;
if (scrollFrame) return;
scrollFrame = requestAnimationFrame(() => {
scrollFrame = 0;
const el = listRef.value;
if (!el || !ppfxStickBottom) return;
el.scrollTop = el.scrollHeight;
});
};
const scheduleScroll = () => {
ppfxFollowTail(false);
};
const autoGrow = () => {
const el = inputRef.value;
if (!el) return;
try {
el.style.height = "auto";
el.style.height = Math.min(72, Math.max(36, el.scrollHeight || 36)) + "px";
} catch (e) {}
};
const focusComposer = () => {
const el = inputRef.value;
if (!el) return;
try { el.focus(); } catch (e) {}
};
const isPanelVisible = () => {
const el = rootRef.value;
if (!el) return false;
return el.getClientRects().length > 0 || el.offsetWidth > 0 || el.offsetHeight > 0;
};
const aiChatIsTokenError = (text, status) => status === 401 ||
/(?:token|令牌|密钥).{0,10}(?:无效|过期|错误|缺失|不存在|未授权|失效)|invalid[\s_-]*token|token[\s_-]*(?:invalid|expired|missing|unauthorized)|unauthori[sz]ed|未授权|请先登录|未登录|登录.{0,4}(?:失效|过期)|\b401\b/i.test(String(text || ""));
const aiChatIsSessionLimit = (text) => /session[\s_-]*(?:limit|max|full)|(?:limit|max)[\s_-]*session|会话.{0,10}(?:上限|达到|超过|已满|超限)|对话.{0,10}(?:上限|达到|超过|已满|超限)|最多.{0,6}(?:会话|对话)/i.test(String(text || ""));
const extractErrorInfo = (raw, status) => {
const data = parseJson(raw);
const nested = data && typeof data === "object" && data.data && typeof data.data === "object" ? data.data : (data && typeof data === "object" ? data : {});
let code = "";
if (data && typeof data === "object") {
if (data.code !== undefined) code = data.code;
else if (nested.code !== undefined) code = nested.code;
}
const candidates = [
nested.message,
nested.msg,
nested.error_description,
typeof nested.error === "string" ? nested.error : "",
nested.error && typeof nested.error === "object" ? nested.error.message : "",
data && data.message,
data && data.msg
];
let message = "";
for (const item of candidates) {
if (typeof item === "string" && item.trim()) {
message = item.trim();
break;
}
}
if (!message && raw && raw.length < 300) {
const trimmed = String(raw).trim();
const first = trimmed.charAt(0);
if (trimmed && first !== "<" && first !== "{" && first !== "[") message = trimmed;
}
const combined = String(code || "") + " " + message + " " + String(status || "");
return {
code: String(code || ""),
message: message || "请求失败(HTTP " + (status || "未知") + ")",
status,
isToken: aiChatIsTokenError(combined, status),
isSessionLimit: aiChatIsSessionLimit(combined)
};
};
const makeRequestError = (info) => {
const err = new Error(info && info.message ? info.message : "请求失败,请稍后重试");
if (info) {
err.code = info.code;
err.status = info.status;
err.isToken = !!info.isToken;
err.isSessionLimit = !!info.isSessionLimit;
}
return err;
};
const loadModels = async (force) => {
if (modelsLoading.value) return;
if (!force && modelsLoaded.value) return;
modelsLoading.value = true;
try {
const modelUrl = getAiChatServer() + "/ai/chat/models";
const response = await fetch(modelUrl, {
method: "GET",
headers: { Accept: "application/json" },
cache: "no-store"
});
const raw = await response.text();
const data = parseJson(raw);
const codeValue = data && typeof data === "object" && data.code !== undefined ? data.code : null;
const code = codeValue !== null && Number.isFinite(Number(codeValue)) ? Number(codeValue) : null;
if (!response.ok || (code !== null && code !== 200 && code !== 0)) {
throw makeRequestError(extractErrorInfo(raw, response.status));
}
const parsed = normalizeModelPayload(data);
if (!parsed.models.length) throw new Error("模型列表为空");
models.value = parsed.models;
modelGroups.value = parsed.groups;
defaultModelId.value = parsed.models.some((item) => item.id === parsed.defaultId)
? parsed.defaultId
: parsed.models[0].id;
if (!parsed.models.some((item) => item.id === selectedModelId.value)) {
selectedModelId.value = defaultModelId.value;
persistModel(defaultModelId.value);
}
modelsLoaded.value = true;
} catch (error) {
models.value = [];
modelGroups.value = [];
defaultModelId.value = "";
showModels.value = false;
modelsLoaded.value = true;
} finally {
modelsLoading.value = false;
}
};
const refreshModels = () => {
selectedModelId.value = "";
modelName.value = "";
showModels.value = false;
modelsLoaded.value = false;
models.value = [];
modelGroups.value = [];
defaultModelId.value = "";
return loadModels(true);
};
const normalizeSources = (items) => (Array.isArray(items) ? items : []).map((item) => ({
title: String(item && item.title ? item.title : "").trim(),
url: String(item && item.url ? item.url : "").trim(),
snippet: String(item && item.snippet ? item.snippet : "").trim()
})).filter((item) => item.title || item.url || item.snippet).slice(0, 10);
const normalizeHistoryMessage = (item) => {
if (!item || typeof item !== "object") return null;
const rawRole = String(item.role || "").toLowerCase();
const role = rawRole === "user" || rawRole === "human" ? "user"
: rawRole === "assistant" || rawRole === "ai" || rawRole === "bot" ? "assistant"
: "";
if (!role) return null;
let content = item.content;
if (Array.isArray(content)) {
content = content.map((part) => {
if (typeof part === "string") return part;
if (part && typeof part.text === "string") return part.text;
if (part && typeof part.content === "string") return part.content;
return "";
}).join("");
}
if (typeof content !== "string") {
content = typeof item.text === "string" ? item.text : (typeof item.message === "string" ? item.message : "");
}
if (!content.trim()) return null;
return {
id: nextMessageId(),
role,
content,
sources: normalizeSources(item.sources),
streaming: false,
stopped: false
};
};
const normalizeHistory = (payload) => {
let raw = [];
if (Array.isArray(payload)) raw = payload;
else {
const session = payload && typeof payload === "object" ? (payload.session || payload) : {};
if (Array.isArray(session)) raw = session;
else if (Array.isArray(session.messages)) raw = session.messages;
else if (Array.isArray(session.history)) raw = session.history;
else if (Array.isArray(payload && payload.messages)) raw = payload.messages;
else if (Array.isArray(payload && payload.history)) raw = payload.history;
}
return raw.map(normalizeHistoryMessage).filter(Boolean);
};
const lastAssistant = () => {
for (let i = messages.value.length - 1; i >= 0; i -= 1) {
if (messages.value[i].role === "assistant") return messages.value[i];
}
return null;
};
const trimMessagesToCurrentTurn = () => {
let index = messages.value.length - 1;
while (index >= 0 && messages.value[index].role !== "user") index -= 1;
if (index > 0) messages.value = messages.value.slice(index);
};
const ppfxFlushStreamDelta = () => {
if (ppfxStreamTimer) {
clearTimeout(ppfxStreamTimer);
ppfxStreamTimer = 0;
}
const target = ppfxStreamTarget;
const chunk = ppfxStreamBuffer;
ppfxStreamTarget = null;
ppfxStreamBuffer = "";
if (target && chunk) target.content = String(target.content || "") + chunk;
};
const ppfxQueueStreamDelta = (target, text) => {
if (ppfxStreamTarget && ppfxStreamTarget !== target) ppfxFlushStreamDelta();
ppfxStreamTarget = target;
ppfxStreamBuffer += text;
if (ppfxStreamTimer) return;
ppfxStreamTimer = setTimeout(() => {
ppfxStreamTimer = 0;
ppfxFlushStreamDelta();
scheduleScroll();
}, PPFX_STREAM_FLUSH_MS);
};
const finalizeAssistant = (options) => {
ppfxFlushStreamDelta();
const opts = options || {};
const target = lastAssistant();
if (!target || !target.streaming) return;
target.streaming = false;
if (opts.stopped) {
target.stopped = true;
const hasContent = !!target.content || (target.sources && target.sources.length > 0);
if (!hasContent) {
target.content = "已停止生成。";
target.stopped = false;
}
}
if (opts.emptyText && !target.content && !(target.sources && target.sources.length)) {
target.content = opts.emptyText;
}
};
const dropEmptyAssistant = () => {
ppfxFlushStreamDelta();
const target = lastAssistant();
if (!target || !target.streaming) return;
if (target.content || (target.sources && target.sources.length)) {
target.streaming = false;
target.interrupted = true;
return;
}
const index = messages.value.lastIndexOf(target);
if (index >= 0) messages.value.splice(index, 1);
};
const abortStream = (silent) => {
ppfxFlushStreamDelta();
const hadStream = streaming.value;
streamSeq += 1;
const controller = abortController;
abortController = null;
if (controller) {
try { controller.abort(); } catch (e) {}
}
if (hadStream) {
streaming.value = false;
stage.value = "";
finalizeAssistant({ stopped: !silent });
}
};
const finishStream = (seq, finishReason) => {
if (seq !== streamSeq) return;
ppfxFlushStreamDelta();
const target = lastAssistant();
if (target) {
target.streaming = false;
target.stopped = false;
if (!target.content && !(target.sources && target.sources.length)) {
target.content = "(未收到回复)";
}
if (finishReason === "length") target.truncated = true;
}
streaming.value = false;
stage.value = "";
if (currentSessionId.value) {
upsertSession(currentSessionId.value, currentTitle.value || fallbackTitle());
}
streamSeq += 1;
const controller = abortController;
abortController = null;
if (controller) {
try { controller.abort(); } catch (e) {}
}
scheduleScroll();
};
const terminateStreamWithError = (seq, info) => {
if (seq !== streamSeq) return;
ppfxFlushStreamDelta();
const target = lastAssistant();
if (target) {
target.streaming = false;
if (target.content || (target.sources && target.sources.length)) target.interrupted = true;
else {
const index = messages.value.lastIndexOf(target);
if (index >= 0) messages.value.splice(index, 1);
}
}
streaming.value = false;
stage.value = "";
streamSeq += 1;
const controller = abortController;
abortController = null;
if (controller) {
try { controller.abort(); } catch (e) {}
}
const isToken = !!(info && info.isToken);
const isLimit = !!(info && info.isSessionLimit);
const isModel = !isToken && !isLimit && (!!(info && info.isModel) ||
aiChatIsModelError(String(info && info.code || "") + " " + String(info && info.message || "")));
const message = isToken
? "Token 无效或已失效,请前往「配置」重新填写。"
: isLimit
? "服务器报告会话数量已达上限,请稍后重试或删除一个旧会话。"
: isModel
? "当前模型不可用,请重新选择模型后重试。"
: (info && info.message ? info.message : "AI 服务请求失败,请稍后重试。");
messages.value.push({ id: nextMessageId(), role: "error", content: message, action: isToken ? "config" : isLimit ? "sessions" : "" });
if (isToken) setNotice("Token 无效或已失效,请前往「配置」检查后重试。", "error", "config");
else if (isLimit) setNotice("服务器报告会话数量已达上限,请稍后重试或删除一个旧会话。", "error", "sessions");
else if (isModel) {
setNotice("当前模型不可用,已刷新模型列表,请重新选择。", "error");
refreshModels();
}
ppfxFollowTail(true);
};
const applySseEvent = (eventName, payload, seq) => {
if (seq !== streamSeq) return;
const data = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
if (eventName === "meta") {
const previousSessionId = currentSessionId.value ? String(currentSessionId.value) : "";
const newSessionId = data.sessionId !== undefined && data.sessionId !== null && data.sessionId !== ""
? String(data.sessionId)
: "";
const evictedSessionId = data.evictedSessionId !== undefined && data.evictedSessionId !== null && data.evictedSessionId !== ""
? String(data.evictedSessionId)
: "";
if (newSessionId) currentSessionId.value = newSessionId;
if (data.model) {
const actualModel = String(data.model);
modelName.value = actualModel;
const knownModel = models.value.find((item) =>
item.id === actualModel || item.label.toLowerCase() === actualModel.toLowerCase()
);
if (knownModel && selectedModelId.value !== knownModel.id) {
selectedModelId.value = knownModel.id;
persistModel(knownModel.id);
}
}
if (typeof data.deepThinking === "boolean") deepThinking.value = data.deepThinking;
if (typeof data.webSearch === "boolean") webSearch.value = data.webSearch;
if (evictedSessionId && evictedSessionId !== newSessionId) {
if (previousSessionId === evictedSessionId) {
deletePending.value = false;
if (newSessionId) {
currentSessionId.value = newSessionId;
trimMessagesToCurrentTurn();
} else {
currentSessionId.value = null;
messages.value = [];
usage.value = null;
stage.value = "";
modelName.value = "";
}
}
removeSession(evictedSessionId);
}
if (currentSessionId.value) {
upsertSession(currentSessionId.value, data.title || fallbackTitle());
}
return;
}
if (eventName === "stage") {
if (data.stage && AI_CHAT_STAGE_TEXT[data.stage]) stage.value = AI_CHAT_STAGE_TEXT[data.stage];
return;
}
if (eventName === "sources") {
if (Array.isArray(data.items)) {
const target = lastAssistant();
if (target) {
target.sources = normalizeSources(data.items);
scheduleScroll();
}
}
return;
}
if (eventName === "delta") {
if (typeof data.text === "string" && data.text) {
const target = lastAssistant();
if (target) {
target.streaming = true;
ppfxQueueStreamDelta(target, data.text);
}
}
return;
}
if (eventName === "usage") {
usage.value = {
count: data.count === undefined || data.count === null ? null : data.count,
remaining: data.remaining === undefined || data.remaining === null ? null : data.remaining
};
return;
}
if (eventName === "done") {
finishStream(seq, typeof data.finishReason === "string" ? data.finishReason : "stop");
return;
}
if (eventName === "error") {
const combined = String(data.code || "") + " " + String(data.message || "");
terminateStreamWithError(seq, {
code: data.code,
message: data.message || "AI 服务返回错误。",
isToken: aiChatIsTokenError(combined, 0),
isSessionLimit: aiChatIsSessionLimit(combined),
isModel: !aiChatIsTokenError(combined, 0) && !aiChatIsSessionLimit(combined) && aiChatIsModelError(combined)
});
}
};
const parseSseBlock = (block, seq) => {
let eventName = "message";
const dataLines = [];
for (const line of String(block || "").split(/\r?\n/)) {
if (!line || line.charAt(0) === ":") continue;
const colon = line.indexOf(":");
const field = colon === -1 ? line : line.slice(0, colon);
let value = colon === -1 ? "" : line.slice(colon + 1);
if (value.charAt(0) === " ") value = value.slice(1);
if (field === "event") eventName = value || "message";
else if (field === "data") dataLines.push(value);
}
if (!dataLines.length) return;
const raw = dataLines.join("\n");
const parsed = parseJson(raw);
applySseEvent(eventName, parsed === null ? raw : parsed, seq);
};
const drainSseBuffer = (buffer, seq) => {
let rest = buffer;
while (true) {
const match = rest.match(/\r?\n\r?\n/);
if (!match) break;
const block = rest.slice(0, match.index);
rest = rest.slice(match.index + match[0].length);
parseSseBlock(block, seq);
if (seq !== streamSeq) break;
}
return rest;
};
const loadSession = async (id) => {
if (!id) return;
if (!hasToken.value) {
setNotice("尚未配置 Token,请先在「配置」页填写后再载入会话。", "error", "config");
return;
}
const sid = String(id);
const seq = ++historySeq;
abortStream(true);
loadingHistory.value = true;
currentSessionId.value = sid;
messages.value = [];
usage.value = null;
stage.value = "";
modelName.value = "";
showSessions.value = false;
showModels.value = false;
deletePending.value = false;
notice.value = null;
try {
const sessionUrl = getAiChatServer() + "/ai/chat/session?id=" + encodeURIComponent(sid) + "&token=" + encodeURIComponent(token.value);
const response = await fetch(sessionUrl, {
method: "GET",
headers: { Accept: "application/json" },
cache: "no-store"
});
if (seq !== historySeq) return;
const raw = await response.text();
if (seq !== historySeq) return;
const data = parseJson(raw);
const codeValue = data && typeof data === "object" && data.code !== undefined ? data.code : null;
const code = codeValue !== null && Number.isFinite(Number(codeValue)) ? Number(codeValue) : null;
if (!response.ok || (code !== null && code !== 200 && code !== 0)) {
throw makeRequestError(extractErrorInfo(raw, response.status));
}
const payload = data && typeof data === "object" && data.data !== undefined ? data.data : data;
if (seq !== historySeq) return;
messages.value = normalizeHistory(payload);
const session = payload && typeof payload === "object" ? (payload.session || payload) : null;
const serverTitle = session && !Array.isArray(session) && typeof session.title === "string" ? session.title : "";
if (serverTitle) upsertSession(sid, serverTitle);
ppfxFollowTail(true);
} catch (error) {
if (seq !== historySeq) return;
if (error && error.status === 404 && !error.isToken) {
removeSession(sid);
if (currentSessionId.value === sid) {
currentSessionId.value = null;
messages.value = [];
}
setNotice("该会话不存在,已从本地列表移除。", "error");
return;
}
if (error && error.isToken) setNotice("Token 无效或已失效,请前往「配置」检查。", "error", "config");
else if (error && error.isSessionLimit) setNotice("服务器报告会话数量已达上限,请稍后重试或删除一个旧会话。", "error", "sessions");
else setNotice(error && error.message ? error.message : "载入会话失败,请稍后重试。", "error");
} finally {
if (seq === historySeq) loadingHistory.value = false;
}
};
const selectSession = (id) => {
if (!id) return;
if (!hasToken.value) {
setNotice("尚未配置 Token,请先在「配置」页填写后再选择会话。", "error", "config");
return;
}
if (String(id) === String(currentSessionId.value || "")) {
showSessions.value = false;
if (!loadingHistory.value && !messages.value.length) loadSession(id);
return;
}
showSessions.value = false;
showModels.value = false;
deletePending.value = false;
notice.value = null;
loadSession(id);
};
const cancelHistoryLoad = () => {
historySeq += 1;
loadingHistory.value = false;
};
const newChat = () => {
showSessions.value = false;
showModels.value = false;
deletePending.value = false;
if (!currentSessionId.value && !messages.value.length) {
vue.nextTick(focusComposer);
return;
}
abortStream(true);
cancelHistoryLoad();
currentSessionId.value = null;
messages.value = [];
usage.value = null;
stage.value = "";
modelName.value = "";
notice.value = null;
vue.nextTick(focusComposer);
};
const clearCurrentChat = () => {
abortStream(true);
cancelHistoryLoad();
currentSessionId.value = null;
messages.value = [];
usage.value = null;
stage.value = "";
modelName.value = "";
showModels.value = false;
deletePending.value = false;
};
const confirmDelete = async () => {
if (deleting.value) return;
const id = currentSessionId.value;
if (!id) {
clearCurrentChat();
return;
}
if (!hasToken.value) {
setNotice("尚未配置 Token,请先在「配置」页填写后再删除会话。", "error", "config");
deletePending.value = false;
return;
}
abortStream(true);
deleting.value = true;
try {
const deleteUrl = getAiChatServer() + "/ai/chat/session?id=" + encodeURIComponent(id) + "&token=" + encodeURIComponent(token.value);
const response = await fetch(deleteUrl, {
method: "DELETE",
headers: { Accept: "application/json" },
cache: "no-store"
});
const raw = await response.text();
const data = parseJson(raw);
const codeValue = data && typeof data === "object" && data.code !== undefined ? data.code : null;
const code = codeValue !== null && Number.isFinite(Number(codeValue)) ? Number(codeValue) : null;
const errorInfo = extractErrorInfo(raw, response.status);
if (response.status === 404 && errorInfo.isToken) throw makeRequestError(errorInfo);
if (!response.ok && response.status !== 404) throw makeRequestError(errorInfo);
if (response.status !== 404 && code !== null && code !== 200 && code !== 0 && code !== 204) {
throw makeRequestError(errorInfo);
}
removeSession(id);
if (currentSessionId.value === id) clearCurrentChat();
setNotice("会话已删除", "ok");
} catch (error) {
if (error && error.isToken) setNotice("Token 无效或已失效,请前往「配置」检查。", "error", "config");
else if (error && error.isSessionLimit) setNotice("服务器报告会话数量已达上限,请稍后重试或删除一个旧会话。", "error", "sessions");
else setNotice(error && error.message ? error.message : "删除会话失败,请稍后重试。", "error");
} finally {
deleting.value = false;
deletePending.value = false;
}
};
const stopGeneration = () => {
if (!streaming.value) return;
abortStream(false);
};
const handleInput = (event) => {
const el = event && event.target;
if (!el) return;
let value = String(el.value || "");
if (value.length > AI_CHAT_MAX_LENGTH) {
value = value.slice(0, AI_CHAT_MAX_LENGTH);
el.value = value;
}
draft.value = value;
autoGrow();
};
const handleKeydown = (event) => {
if (event.key !== "Enter" || event.shiftKey) return;
if (composing || event.isComposing || event.keyCode === 229) return;
event.preventDefault();
sendMessage();
};
const sendMessage = async () => {
const text = String(draft.value || "").trim();
if (!text || streaming.value || loadingHistory.value) return;
if (text.length > AI_CHAT_MAX_LENGTH) {
setNotice("消息超过 500 字,请删减后再发送。", "error");
return;
}
if (!hasToken.value) {
setNotice("尚未配置 Token,请先在「配置」页填写后再使用 AI 问答。", "error", "config");
return;
}
notice.value = null;
showSessions.value = false;
showModels.value = false;
deletePending.value = false;
messages.value.push({ id: nextMessageId(), role: "user", content: text });
messages.value.push({
id: nextMessageId(),
role: "assistant",
content: "",
sources: [],
streaming: true,
stopped: false
});
draft.value = "";
vue.nextTick(() => {
autoGrow();
focusComposer();
});
ppfxFollowTail(true);
streaming.value = true;
stage.value = "";
usage.value = null;
modelName.value = "";
const seq = ++streamSeq;
const controller = new AbortController();
abortController = controller;
try {
const response = await fetch(getAiChatServer() + "/ai/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "text/event-stream"
},
body: JSON.stringify({
token: token.value,
sessionId: currentSessionId.value || null,
message: text,
model: models.value.length ? (selectedModelId.value || undefined) : undefined,
deepThinking: deepThinking.value,
webSearch: webSearch.value
}),
signal: controller.signal,
cache: "no-store"
});
if (seq !== streamSeq) return;
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
if (!response.ok || contentType.includes("application/json")) {
const raw = await response.text();
if (seq !== streamSeq) return;
throw makeRequestError(extractErrorInfo(raw, response.status));
}
if (!response.body || typeof response.body.getReader !== "function") {
throw makeRequestError({ message: "当前浏览器不支持流式响应,请更换 Chrome 或 Edge。", code: "NO_STREAM" });
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const chunk = await reader.read();
if (seq !== streamSeq) return;
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
buffer = drainSseBuffer(buffer, seq);
if (seq !== streamSeq) return;
}
buffer += decoder.decode();
if (seq !== streamSeq) return;
if (buffer.trim()) parseSseBlock(buffer, seq);
if (seq === streamSeq) finishStream(seq, "eof");
} catch (error) {
if (seq !== streamSeq) return;
if (error && error.name === "AbortError") {
abortStream(false);
return;
}
const isToken = !!(error && error.isToken);
const isLimit = !!(error && error.isSessionLimit);
dropEmptyAssistant();
streaming.value = false;
stage.value = "";
const controller = abortController;
abortController = null;
if (controller) {
try { controller.abort(); } catch (e) {}
}
const rawMessage = error && error.message ? String(error.message) : "";
const isModel = !isToken && !isLimit && aiChatIsModelError(String(error && error.code || "") + " " + rawMessage);
let message = isToken
? "Token 无效或已失效,请前往「配置」重新填写。"
: isLimit
? "服务器报告会话数量已达上限,请稍后重试或删除一个旧会话。"
: isModel
? "当前模型不可用,请重新选择模型后重试。"
: (rawMessage || "AI 服务请求失败,请稍后重试。");
if (!isToken && !isLimit && !isModel) {
if (/failed to fetch|networkerror|load failed/i.test(rawMessage)) {
message = "无法连接 AI 服务,请稍后再试。";
}
}
messages.value.push({ id: nextMessageId(), role: "error", content: message, action: isToken ? "config" : isLimit ? "sessions" : "" });
if (isToken) setNotice("Token 无效或已失效,请前往「配置」检查后重试。", "error", "config");
else if (isLimit) setNotice("服务器报告会话数量已达上限,请稍后重试或删除一个旧会话。", "error", "sessions");
else if (isModel) {
setNotice("当前模型不可用,已刷新模型列表,请重新选择。", "error");
refreshModels();
}
ppfxFollowTail(true);
} finally {
if (seq === streamSeq) {
streaming.value = false;
stage.value = "";
abortController = null;
}
}
};
const stageText = vue.computed(() => streaming.value ? (stage.value || "正在连接") : "");
const selectedModelEntry = vue.computed(() =>
models.value.find((item) => item.id === selectedModelId.value) || null
);
const modelCostLabel = (cost) => String(cost || "").replace(/模型$/, "");
const modelCostTone = (cost) => {
const label = modelCostLabel(cost);
return /超高/.test(label) ? "is-ultra" : /高/.test(label) ? "is-high" : /中/.test(label) ? "is-mid" : "is-low";
};
const renderModelCost = (cost) => {
const label = modelCostLabel(cost);
return label ? vue.h("span", { class: ["pp-cost-badge", modelCostTone(label)] }, label) : null;
};
const selectedThinkingControl = vue.computed(() => {
const control = selectedModelEntry.value && selectedModelEntry.value.thinkingControl;
return ["toggle", "effort", "always", "none"].includes(control) ? control : "toggle";
});
const thinkingToggleLocked = vue.computed(() =>
selectedThinkingControl.value === "always" || selectedThinkingControl.value === "none"
);
const thinkingToggleActive = vue.computed(() =>
selectedThinkingControl.value === "always" ? true : deepThinking.value
);
const thinkingToggleTitle = vue.computed(() => {
if (selectedThinkingControl.value === "always") return "该模型始终思考,无法关闭";
if (selectedThinkingControl.value === "none") return "该模型不支持深度思考";
if (selectedThinkingControl.value === "effort") {
return deepThinking.value ? "深度思考已开启(max)" : "深度思考已关闭(low)";
}
return deepThinking.value ? "深度思考已开启" : "深度思考已关闭";
});
const selectedModelLabel = vue.computed(() => {
if (modelName.value) {
const actual = String(modelName.value);
const known = models.value.find((item) =>
item.id === actual || item.label.toLowerCase() === actual.toLowerCase()
);
return known ? known.label : actual;
}
if (selectedModelEntry.value) return selectedModelEntry.value.label;
if (modelsLoading.value) return "加载模型…";
return "默认模型";
});
const selectedModelCategory = vue.computed(() => {
const actual = String(modelName.value || "");
if (actual) {
const known = models.value.find((item) =>
item.id === actual || item.label.toLowerCase() === actual.toLowerCase()
);
if (known) return known.type;
}
return selectedModelEntry.value ? selectedModelEntry.value.type : "";
});
const modelSelectDisabled = vue.computed(() =>
streaming.value || modelsLoading.value || !models.value.length
);
const usageText = vue.computed(() => {
const value = usage.value;
if (!value) return "";
const parts = [];
if (value.count !== null && value.count !== undefined) parts.push("本次 " + formatNumber(value.count));
if (value.remaining !== null && value.remaining !== undefined) parts.push("剩余 " + formatNumber(value.remaining));
return parts.join(" · ");
});
const overLimit = vue.computed(() => draft.value.length > AI_CHAT_MAX_LENGTH);
const sendDisabled = vue.computed(() =>
!hasToken.value || !draft.value.trim() || streaming.value || loadingHistory.value || overLimit.value
);
const toggleDelete = () => {
if (!currentSessionId.value && !messages.value.length) return;
deletePending.value = !deletePending.value;
showSessions.value = false;
showModels.value = false;
};
const handleNoticeAction = () => {
const kind = notice.value && notice.value.kind;
if (kind === "config") goConfig();
else if (kind === "sessions") {
showSessions.value = true;
deletePending.value = false;
}
};
const activate = () => {
if (!isPanelVisible() || hasActivated.value) return;
hasActivated.value = true;
loadModels();
if (hasToken.value && sessions.value.length && !currentSessionId.value && !messages.value.length) {
loadSession(sessions.value[0].id);
}
};
vue.onMounted(() => {
vue.nextTick(() => {
if (isPanelVisible()) activate();
const parent = rootRef.value && rootRef.value.parentElement;
if (parent && typeof MutationObserver === "function") {
activationObserver = new MutationObserver(() => {
if (isPanelVisible()) activate();
});
activationObserver.observe(parent, { attributes: true, attributeFilter: ["style", "class"] });
}
});
});
vue.onUnmounted(() => {
if (activationObserver) activationObserver.disconnect();
if (scrollFrame) cancelAnimationFrame(scrollFrame);
if (noticeTimer) clearTimeout(noticeTimer);
abortStream(true);
});
vue.watch(token, (value) => {
if (!value) return;
if (notice.value && notice.value.kind === "token") notice.value = null;
if (!models.value.length) refreshModels();
if (hasActivated.value && !currentSessionId.value && !messages.value.length && sessions.value.length) {
loadSession(sessions.value[0].id);
}
});
const renderSources = (sources) => {
if (!webSearch.value || !sources || !sources.length) return null;
const items = sources.map((source, index) => {
const link = aiChatSourceLink(source.url);
const props = { key: index, class: "pp-ai-source" };
if (link) {
props.href = link.href;
props.target = "_blank";
props.rel = "noopener noreferrer";
}
const title = String(source.title || "").trim() || (link ? link.host : "") || "未命名来源";
return vue.h(link ? "a" : "div", props, [
vue.h("span", { class: "pp-ai-source-title", title }, title),
link && link.host ? vue.h("span", { class: "pp-ai-source-host", title: link.host }, link.host) : null
]);
});
return vue.h("details", { class: "pp-ai-sources" }, [
vue.h("summary", null, [
ppIcon("search", 10),
vue.h("span", null, "参考来源"),
vue.h("span", { class: "pp-ai-sources-count" }, sources.length),
vue.h("span", { class: "pp-ai-sources-chev" }, ppIcon("chevronDown", 10))
]),
vue.h("div", { class: "pp-ai-sources-list" }, items)
]);
};
const renderMessage = (message) => {
if (message.role === "user") {
return vue.h("div", { class: "pp-ai-msg user", key: message.id }, [
vue.h("div", { class: "pp-ai-bubble" }, message.content)
]);
}
if (message.role === "error") {
return vue.h("div", { class: "pp-ai-msg error", key: message.id }, [
vue.h("span", { class: "pp-ai-avatar is-error" }, ppIcon("alert", 11)),
vue.h("div", { class: "pp-ai-bubble" }, [
vue.h("div", { class: "pp-ai-error-text" }, message.content),
message.action ? vue.h("button", {
type: "button",
class: "pp-ai-linkbtn",
onClick: message.action === "config" ? goConfig : () => {
showSessions.value = true;
deletePending.value = false;
}
}, message.action === "config" ? "去配置" : "管理会话") : null
])
]);
}
const bubble = [];
if (message.content) {
bubble.push(vue.h("div", { class: "pp-ai-md", innerHTML: aiChatRenderMarkdown(message.content) }));
}
if (message.streaming) bubble.push(vue.h("span", { class: "pp-ai-cursor" }));
if (message.sources && message.sources.length) {
const sourcesBlock = renderSources(message.sources);
if (sourcesBlock) bubble.push(sourcesBlock);
}
if (message.stopped) bubble.push(vue.h("div", { class: "pp-ai-finish-note" }, "已停止生成"));
if (message.interrupted) bubble.push(vue.h("div", { class: "pp-ai-finish-note" }, "生成中断"));
if (message.truncated) bubble.push(vue.h("div", { class: "pp-ai-finish-note" }, "回答达到长度上限"));
return vue.h("div", { class: "pp-ai-msg assistant", key: message.id }, [
vue.h("span", { class: "pp-ai-avatar" }, vue.h(PpLogo, { size: 18 })),
vue.h("div", { class: "pp-ai-bubble" }, bubble)
]);
};
const renderMessages = () => {
if (loadingHistory.value) {
return vue.h("div", { class: "pp-ai-welcome" }, [
vue.h("span", { class: "pp-ai-welcome-ico" }, vue.h("span", { class: "pp-ai-spinner" })),
vue.h("b", null, "正在载入会话"),
vue.h("span", null, "正在获取历史消息…")
]);
}
if (!messages.value.length) {
return vue.h("div", { class: "pp-ai-welcome" }, [
vue.h("span", { class: "pp-ai-welcome-ico" }, aiChatIcon("ai", 20)),
vue.h("b", null, "AI 问答"),
vue.h("span", null, "可以询问课程知识点、写作思路或代码问题,回答支持 Markdown。"),
vue.h("span", { class: "pp-ai-welcome-note" }, "支持 Markdown · 可开启深度思考与联网搜索")
]);
}
return messages.value.map(renderMessage);
};
const renderTopBar = () => vue.h("div", { class: "pp-ai-top" }, [
vue.h("button", {
type: "button",
class: "pp-ai-picker",
title: "选择历史会话",
onClick: () => {
showSessions.value = !showSessions.value;
showModels.value = false;
deletePending.value = false;
}
}, [
ppIcon("message", 13),
vue.h("span", { class: "pp-ai-picker-title" }, currentTitle.value || "新对话"),
vue.h("span", { class: "pp-ai-count-pill" }, sessions.value.length + "/" + AI_CHAT_MAX_SESSIONS),
ppIcon("chevronDown", 12)
]),
vue.h("button", {
type: "button",
class: "pp-ai-tool",
title: "新建对话",
onClick: newChat
}, [aiChatIcon("plus", 14)]),
vue.h("button", {
type: "button",
class: ["pp-ai-tool", deletePending.value ? "is-active" : "", "is-danger"],
title: "删除当前会话",
disabled: deleting.value || (!currentSessionId.value && !messages.value.length),
onClick: toggleDelete
}, [deleting.value ? vue.h("span", { class: "pp-ai-mini-spin" }) : ppIcon("trash", 14)])
]);
const renderSessionMenu = () => {
if (!showSessions.value) return null;
const items = sessions.value.map((item) => vue.h("button", {
type: "button",
key: item.id,
class: ["pp-ai-session", item.id === currentSessionId.value ? "active" : ""],
onClick: () => selectSession(item.id)
}, [
vue.h("span", { class: "pp-ai-session-main" }, [
vue.h("span", { class: "pp-ai-session-title" }, item.title || "新对话"),
vue.h("span", { class: "pp-ai-session-time" }, formatSessionTime(item.updatedAt))
]),
item.id === currentSessionId.value ? vue.h("span", { class: "pp-ai-session-dot" }) : null
]));
return [
vue.h("div", { class: "pp-ai-menu-mask", onClick: () => { showSessions.value = false; } }),
vue.h("div", { class: "pp-ai-menu" }, [
vue.h("div", { class: "pp-ai-menu-head" }, [
vue.h("b", null, "最近会话"),
vue.h("span", null, sessions.value.length + " / " + AI_CHAT_MAX_SESSIONS)
]),
sessions.value.length
? vue.h("div", { class: "pp-ai-session-list" }, items)
: vue.h("div", { class: "pp-ai-menu-empty" }, "暂无历史会话"),
currentSessionId.value
? vue.h("div", { class: "pp-ai-menu-foot" }, [
vue.h("button", {
type: "button",
onClick: () => {
showSessions.value = false;
deletePending.value = true;
}
}, "删除当前会话")
])
: null
])
];
};
const renderTokenCallout = () => {
if (hasToken.value) return null;
return vue.h("div", { class: "pp-ai-callout" }, [
ppIcon("key", 12),
vue.h("span", { class: "pp-ai-callout-text" }, "尚未配置 Token,AI 问答需要先在「配置」页填写并校验 Token。"),
vue.h("button", { type: "button", class: "pp-ai-linkbtn", onClick: goConfig }, "去配置")
]);
};
const renderNotice = () => {
const value = notice.value;
if (!value) return null;
const actionLabel = value.kind === "config" ? "去配置" : value.kind === "sessions" ? "管理会话" : "";
return vue.h("div", { class: ["pp-ai-callout", value.tone === "error" ? "is-error" : value.tone === "ok" ? "is-ok" : ""] }, [
ppIcon(value.tone === "error" ? "alert" : value.tone === "ok" ? "check" : "info", 12),
vue.h("span", { class: "pp-ai-callout-text" }, value.text),
actionLabel ? vue.h("button", { type: "button", class: "pp-ai-linkbtn", onClick: handleNoticeAction }, actionLabel) : null,
vue.h("button", {
type: "button",
class: "pp-ai-callout-x",
title: "关闭提示",
onClick: () => { notice.value = null; }
}, "×")
]);
};
const renderDeleteConfirm = () => {
if (!deletePending.value) return null;
return vue.h("div", { class: "pp-ai-confirm" }, [
ppIcon("alert", 12),
vue.h("span", null, currentSessionId.value ? "删除当前会话?此操作不可恢复。" : "清空当前对话?"),
vue.h("button", { type: "button", onClick: () => { deletePending.value = false; } }, "取消"),
vue.h("button", {
type: "button",
class: "primary",
disabled: deleting.value,
onClick: confirmDelete
}, deleting.value ? "删除中…" : "删除")
]);
};
const renderStatus = () => {
const stageValue = stageText.value;
const usageValue = usageText.value;
if (!stageValue && !usageValue) return null;
const ppfxLast = lastAssistant();
const ppfxThinking = streaming.value && !(ppfxLast && ppfxLast.content);
return vue.h("div", { class: "pp-ai-status" }, [
stageValue
? vue.h("span", { class: "pp-ai-status-main" }, [
vue.h("span", { class: "pp-ai-spinner" }),
vue.h("span", null, stageValue),
ppfxThinking
? vue.h("span", { class: "ppfx-thinking", "aria-hidden": "true" }, [
vue.h("i"),
vue.h("i"),
vue.h("i")
])
: null
])
: vue.h("span", { class: "pp-ai-status-main" }, [
ppIcon("check", 10),
vue.h("span", null, "已完成")
]),
usageValue ? vue.h("span", { class: "pp-ai-usage", title: "本次用量 / 剩余额度" }, usageValue) : null
]);
};
const chooseModel = (id) => {
if (streaming.value || !id) return;
selectedModelId.value = String(id);
modelName.value = "";
persistModel(selectedModelId.value);
showModels.value = false;
};
const renderModelSelector = () => {
const groups = modelGroups.value;
const disabled = modelSelectDisabled.value;
return vue.h("div", { class: "pp-ai-model" }, [
vue.h("button", {
type: "button",
class: ["pp-ai-model-btn", showModels.value ? "is-active" : ""],
title: "当前模型:" + (selectedModelCategory.value ? selectedModelCategory.value + " · " : "") + selectedModelLabel.value,
disabled,
onClick: () => {
if (disabled) return;
showModels.value = !showModels.value;
showSessions.value = false;
deletePending.value = false;
}
}, [
vue.h("span", { class: "pp-ai-model-label" }, selectedModelLabel.value),
renderModelCost(selectedModelEntry.value && selectedModelEntry.value.cost),
ppIcon("chevronDown", 10)
]),
showModels.value && groups.length ? vue.h("div", { class: "pp-ai-model-menu" }, [
...groups.map((group, groupIndex) => vue.h("div", {
class: "pp-ai-model-group",
key: group.type + "-" + groupIndex
}, [
vue.h("div", { class: "pp-ai-model-group-title" }, group.label || group.type),
...group.models.map((model) => vue.h("button", {
type: "button",
key: model.id,
class: ["pp-ai-model-option", selectedModelId.value === model.id ? "active" : ""],
title: (model.cost ? "费用:" + model.cost + " · " : "") + model.label,
onClick: () => chooseModel(model.id)
}, [
vue.h("span", null, model.label),
renderModelCost(model.cost),
selectedModelId.value === model.id ? ppIcon("check", 10) : null
]))
]))
]) : null
]);
};
const renderToggles = () => vue.h("div", { class: "pp-ai-toggles" }, [
vue.h("button", {
type: "button",
class: ["pp-ai-toggle", thinkingToggleActive.value ? "active" : ""],
title: thinkingToggleTitle.value,
disabled: streaming.value || thinkingToggleLocked.value,
onClick: () => {
if (thinkingToggleLocked.value) return;
deepThinking.value = !deepThinking.value;
}
}, [aiChatIcon("ai", 10), vue.h("span", null, "深度思考")]),
vue.h("button", {
type: "button",
class: ["pp-ai-toggle", webSearch.value ? "active" : ""],
title: "开启后按需联网,寒暄/闲聊不会搜索",
disabled: streaming.value,
onClick: () => { webSearch.value = !webSearch.value; }
}, [ppIcon("search", 10), vue.h("span", null, PP_KEY.WEB_SEARCH)])
]);
const renderComposer = () => {
return vue.h("div", { class: "pp-ai-composer" }, [
vue.h("textarea", {
ref: inputRef,
class: ["pp-ai-input", overLimit.value ? "is-over" : ""],
value: draft.value,
rows: 1,
maxlength: AI_CHAT_MAX_LENGTH,
placeholder: hasToken.value ? "输入问题,最多 500 字…" : "请先在「配置」页填写 Token",
disabled: loadingHistory.value,
onInput: handleInput,
onKeydown: handleKeydown,
onCompositionstart: () => { composing = true; },
onCompositionend: () => { composing = false; }
}),
vue.h("div", { class: "pp-ai-sendrow" }, [
renderModelSelector(),
vue.h("span", { class: ["pp-ai-count", overLimit.value ? "is-over" : ""] }, draft.value.length + "/" + AI_CHAT_MAX_LENGTH),
streaming.value
? vue.h("button", {
type: "button",
class: ["pp-ai-send", "pp-ai-stop"],
onClick: stopGeneration
}, [vue.h("i"), "停止"])
: vue.h("button", {
type: "button",
class: "pp-ai-send",
disabled: sendDisabled.value,
onClick: sendMessage
}, "发送")
])
]);
};
return () => {
const children = [renderTopBar()];
const menu = renderSessionMenu();
if (menu) children.push(...menu);
const tokenCallout = renderTokenCallout();
if (tokenCallout) children.push(tokenCallout);
const noticeBar = renderNotice();
if (noticeBar) children.push(noticeBar);
const confirmBar = renderDeleteConfirm();
if (confirmBar) children.push(confirmBar);
children.push(vue.h("div", { class: "pp-ai-messages", ref: listRef, onScroll: ppfxHandleListScroll }, renderMessages()));
const statusRow = renderStatus();
if (statusRow) children.push(statusRow);
children.push(renderToggles());
children.push(renderComposer());
children.push(vue.h("div", { class: "pp-ai-enterhint" }, "Enter 发送 · Shift+Enter 换行"));
return vue.h("div", { class: "pp-ai-chat", ref: rootRef }, children);
};
}
});
const _sfc_main$4 = vue.defineComponent({
name: "AuthorWords",
setup() {
return () => vue.h("div", null, [
ppDoc("作者的话", "message", [
vue.h("blockquote", { class: "pp-quote" }, "“快点刷学习通,别卷了”"),
vue.h("p", { class: "muted" }, "别卷了别卷了,真的别卷了"),
vue.h("p", null, "不是,你还真看啊,学习通刷完了么你")
]),
ppDoc("免更新版本", "refresh", [
vue.h("p", null, "轻量级引导脚本,自动获取最新的刷课脚本代码执行,支持热更新,无需手动更新。"),
vue.h("a", {
class: "pp-link",
href: "https://scriptcat.org/zh-CN/script-show-page/5615",
target: "_blank"
}, [vue.h("span", null, "获取免更新版本"), ppIcon("external", 12)])
]),
ppDoc("使用教程", "book", [
vue.h("p", null, "【学习通】功能基本完成;【智慧树】目前只支持答题。"),
vue.h("p", null, "题库配置:脚本配置 → 填写 24 位 Token → 点击验证。"),
vue.h("p", null, "如遇问题,请在「反馈」页提交,核实后可获得 100–500 次数奖励。")
]),
ppDoc("用户协议", "shield", [
vue.h("p", null, "本脚本仅供学习和研究使用,并应在 24 小时内删除;使用不应违反任何法律法规及学术道德。"),
vue.h("p", null, "因使用脚本产生的任何后果由用户自行承担;题库为第三方收集,相关问题与脚本无关。")
])
]);
}
});
const FeedbackPanel = vue.defineComponent({
name: "FeedbackPanel",
setup() {
const configStore = useConfigStore();
const qq = vue.ref("");
const content = vue.ref("");
const status = vue.ref("idle");
const errorMsg = vue.ref("");
const errorTone = vue.ref("bad");
const touched = vue.ref(false);
const cooldown = vue.ref(0);
const communityCta = vue.ref(false);
let cooldownTimer = null;
const countChars = (value) => Array.from(String(value || "")).length;
const charCount = vue.computed(() => countChars(content.value));
const qqValid = vue.computed(() => /^[1-9]\d{4,11}$/.test(String(qq.value || "").trim()));
const qqBad = vue.computed(() => touched.value && !qqValid.value);
const overLimit = vue.computed(() => charCount.value > 500);
const readToken = () => String(configStore.queryApis && configStore.queryApis[0] && configStore.queryApis[0].token || "").trim();
const canSubmit = vue.computed(() => qqValid.value && charCount.value > 0 && !overLimit.value && cooldown.value <= 0 && status.value !== "loading");
const platformName = () => {
const key = String(configStore.platformName || "").trim();
return key === "cx" || key === "zhs" || key === "ykt" ? key : "other";
};
const pageHref = () => (typeof location !== "undefined" && location.href) ? String(location.href) : "";
const stopCooldown = () => {
if (cooldownTimer) {
clearTimeout(cooldownTimer);
cooldownTimer = null;
}
};
const startCooldown = (seconds) => {
const raw = Number(seconds);
const safe = Number.isFinite(raw) && raw > 0 ? Math.ceil(raw) : 60;
cooldown.value = safe;
stopCooldown();
cooldownTimer = setTimeout(() => {
cooldown.value = 0;
cooldownTimer = null;
if (status.value === "error" && errorTone.value === "warn") {
status.value = "idle";
errorMsg.value = "";
}
}, safe * 1000);
};
vue.onUnmounted(stopCooldown);
const clearError = () => {
if (status.value === "error" && cooldown.value <= 0) {
status.value = "idle";
errorMsg.value = "";
errorTone.value = "bad";
}
};
const handleQqInput = (event) => {
const raw = String(event && event.target ? event.target.value : "").replace(/\D+/g, "").slice(0, 12);
qq.value = raw;
if (event && event.target && event.target.value !== raw) event.target.value = raw;
touched.value = true;
clearError();
};
const handleContentInput = (event) => {
content.value = String(event && event.target ? event.target.value : "");
clearError();
};
const pickMessage = (body) => {
if (!body || typeof body !== "object") return "";
const values = [body.msg, body.message, body.error, body.data && body.data.msg, body.data && body.data.message, body.data && body.data.error];
for (let i = 0; i < values.length; i++) {
const text = typeof values[i] === "string" ? values[i].trim() : "";
if (text) return text.slice(0, 160);
}
return "";
};
const pickErrorCode = (body) => {
if (!body || typeof body !== "object") return "";
const values = [
body.code, body.errorCode, body.error_code, body.error,
body.data && body.data.code, body.data && body.data.errorCode,
body.data && body.data.error_code, body.data && body.data.error
];
for (let i = 0; i < values.length; i++) {
if (typeof values[i] !== "string") continue;
const code = values[i].trim().toUpperCase();
if (code === "FEEDBACK_TOKEN_INVALID" || code === "FEEDBACK_SPONSORED_ONLY") return code;
}
return "";
};
const readHeader = (response, name) => {
const lines = String(response && response.responseHeaders || "").split(/\r?\n/);
const lower = name.toLowerCase();
for (let i = 0; i < lines.length; i++) {
const idx = lines[i].indexOf(":");
if (idx > 0 && lines[i].slice(0, idx).trim().toLowerCase() === lower) {
return lines[i].slice(idx + 1).trim();
}
}
return "";
};
const retrySeconds = (response, body) => {
const header = readHeader(response, "retry-after");
if (header) {
const asNumber = Number(header);
if (Number.isFinite(asNumber) && asNumber > 0) return Math.ceil(asNumber);
const asDate = Date.parse(header);
if (!isNaN(asDate)) return Math.max(1, Math.ceil((asDate - Date.now()) / 1000));
}
const data = body && typeof body === "object" ? body.data : null;
const values = [
body && body.retryAfter, body && body.retry_after, body && body.retrySeconds,
data && data.retryAfter, data && data.retry_after, data && data.retrySeconds, data && data.retry
];
for (let i = 0; i < values.length; i++) {
const num = Number(values[i]);
if (Number.isFinite(num) && num > 0) return Math.ceil(num);
}
return 60;
};
const sendRequest = (payload) => new Promise((resolve, reject) => {
const url = String(CURRENT_SERVER || "").replace(/\/$/, "") + "/feedback";
if (typeof _GM_xmlhttpRequest === "function") {
_GM_xmlhttpRequest({
method: "POST",
url,
headers: { "Content-Type": "application/json" },
data: JSON.stringify(payload),
timeout: 15000,
onload: (response) => resolve(response),
onerror: () => reject(new Error("network")),
ontimeout: () => reject(new Error("timeout"))
});
return;
}
if (typeof fetch === "function") {
fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
}).then((response) => response.text().then((text) => {
const headers = [];
if (response.headers && typeof response.headers.forEach === "function") {
response.headers.forEach((value, key) => { headers.push(key + ": " + value); });
}
resolve({ status: response.status, responseText: text, responseHeaders: headers.join("\r\n") });
})).catch(reject);
return;
}
reject(new Error("unsupported"));
});
const handleResponse = (response) => {
const httpStatus = Number(response && response.status) || 0;
const responseText = String(response && response.responseText || "");
let body = null;
if (responseText.trim()) {
try {
body = JSON.parse(responseText);
} catch (e) {
body = null;
}
}
const message = pickMessage(body);
const usableMessage = /^(ok|success|successful|成功)$/i.test(message) ? "" : message;
const plain = responseText.trim();
const safePlain = plain && plain.length <= 200 && !/^[<{[]/.test(plain) ? plain : "";
const bodyCode = body && body.code !== undefined ? Number(body.code) : NaN;
const errorCode = pickErrorCode(body);
const tokenError = errorCode === "FEEDBACK_TOKEN_INVALID";
const sponsoredError = errorCode === "FEEDBACK_SPONSORED_ONLY";
const explicitFailure = body && (body.success === false || body.ok === false || (body.data && (body.data.success === false || body.data.ok === false)));
const httpOk = httpStatus >= 200 && httpStatus < 300;
const bodyOk = !body || (!body.code && isNaN(bodyCode)) || bodyCode === 0 || bodyCode === 200 || bodyCode === 201;
if (httpOk && !explicitFailure && !errorCode && (body ? bodyOk : httpStatus === 204)) {
status.value = "success";
errorMsg.value = "";
errorTone.value = "bad";
communityCta.value = false;
cooldown.value = 0;
stopCooldown();
content.value = "";
return;
}
communityCta.value = false;
let code = httpStatus >= 400 ? httpStatus : 0;
if (!code && errorCode) code = tokenError ? 401 : 403;
if (!code && !isNaN(bodyCode) && bodyCode !== 200 && bodyCode !== 201 && bodyCode !== 0) code = bodyCode;
if (!code && body && body.code && isNaN(bodyCode)) code = 500;
if (!code) code = 500;
if (tokenError || sponsoredError || httpStatus === 401 || httpStatus === 403 || code === 401 || code === 403) {
communityCta.value = true;
errorTone.value = "bad";
if (tokenError) {
errorMsg.value = "Token 无效或已失效,请到「配置」页重新验证;服务器反馈仅限赞助用户,也可使用社区反馈。";
} else if (sponsoredError) {
errorMsg.value = "该 Token 未开通赞助权益,服务器反馈仅限赞助用户;请使用社区反馈。";
} else {
errorMsg.value = "Token 校验失败,服务器反馈仅限赞助用户;请到「配置」页重新验证,或使用社区反馈。";
}
status.value = "error";
return;
}
if (code === 429) {
const wait = retrySeconds(response, body);
startCooldown(wait);
errorTone.value = "warn";
errorMsg.value = "请 " + wait + " 秒后再试";
status.value = "error";
return;
}
if (code === 400 || code === 422) {
errorTone.value = "bad";
errorMsg.value = usableMessage || safePlain || "提交内容未通过校验,请检查 QQ 号和反馈内容";
status.value = "error";
return;
}
errorTone.value = "bad";
errorMsg.value = code >= 500 ? (usableMessage || "服务器繁忙,请稍后重试") : (usableMessage || "提交失败(" + code + "),请稍后重试");
status.value = "error";
};
const handleSubmit = async () => {
touched.value = true;
if (status.value === "loading") return;
if (cooldown.value > 0) {
status.value = "error";
errorTone.value = "warn";
errorMsg.value = "请 " + cooldown.value + " 秒后再试";
return;
}
if (!qqValid.value) {
status.value = "error";
errorTone.value = "bad";
errorMsg.value = "请输入 5–12 位数字 QQ 号";
return;
}
if (charCount.value === 0) {
status.value = "error";
errorTone.value = "bad";
errorMsg.value = "请填写反馈内容";
return;
}
if (overLimit.value) {
status.value = "error";
errorTone.value = "bad";
errorMsg.value = "反馈内容最多 500 个字符";
return;
}
const token = readToken();
const payload = {
qq: String(qq.value || "").trim(),
content: content.value,
platform: platformName(),
pageUrl: pageHref()
};
const clientVersion = getClientVersion();
if (clientVersion) payload.clientVersion = clientVersion;
if (token) payload.token = token;
communityCta.value = false;
status.value = "loading";
errorMsg.value = "";
errorTone.value = "bad";
try {
const response = await sendRequest(payload);
handleResponse(response);
} catch (error) {
status.value = "error";
errorTone.value = "bad";
if (error && error.message === "timeout") {
errorMsg.value = "请求超时,请检查网络后重试";
} else {
errorMsg.value = "网络异常,未能连接反馈服务器,请稍后重试";
}
}
};
const COMMUNITY_URL = "https://scriptcat.org/zh-CN/script-show-page/5597/issue";
const communityLink = (label, className) => vue.h("a", {
class: className,
href: COMMUNITY_URL,
target: "_blank",
rel: "noopener noreferrer"
}, [label, ppIcon("external", 10)]);
const renderCommunityCta = (title, desc) => vue.h("div", { class: "pp-fb-cta" }, [
vue.h("b", null, title),
vue.h("p", null, desc),
communityLink("前往脚本社区反馈", "pp-fb-cta-btn")
]);
const submitLabel = () => status.value === "loading" ? "提交中…" : "提交反馈";
const renderError = () => {
if (status.value !== "error" || !errorMsg.value) return null;
return vue.h("div", { class: ["pp-note", errorTone.value === "warn" ? "" : "bad"], role: "alert" }, [
ppIcon(errorTone.value === "warn" ? "clock" : "alert", 11),
vue.h("span", null, errorMsg.value)
]);
};
const renderSuccess = () => vue.h("div", { class: "pp-fb-success", role: "status" }, [
vue.h("span", { class: "pp-fb-success-ico" }, ppIcon("check", 18)),
vue.h("b", null, "反馈已提交"),
vue.h("span", { class: "pp-fb-success-text" }, "核实后会通过 QQ 联系你,请留意好友消息。"),
vue.h("button", {
type: "button",
class: "pp-fb-again",
onClick: () => {
status.value = "idle";
errorMsg.value = "";
errorTone.value = "bad";
communityCta.value = false;
}
}, "再写一条")
]);
return () => vue.h("div", { class: "pp-fb" }, [
vue.h("section", { class: "pp-fb-card" }, [
vue.h("div", { class: "pp-fb-head" }, [
vue.h("span", { class: "pp-fb-head-ico" }, ppIcon("message", 14)),
vue.h("span", { class: "pp-fb-title" }, "意见反馈")
]),
status.value === "success" ? renderSuccess() : [
vue.h("div", { class: "pp-fb-reward" }, [
vue.h("span", { class: "pp-fb-reward-ico" }, ppIcon("gift", 13)),
vue.h("span", null, "反馈核实后可获得 100–500 次数奖励")
]),
vue.h("div", { class: "pp-fb-field" }, [
vue.h("label", { class: "pp-fb-label", for: "pp-fb-content" }, [
vue.h("span", null, "反馈内容"),
vue.h("span", { class: "pp-fb-req" }, "*")
]),
vue.h("div", { class: ["pp-fb-area-wrap", overLimit.value ? "is-bad" : ""] }, [
vue.h("textarea", {
id: "pp-fb-content",
class: "pp-fb-textarea",
rows: 5,
maxlength: 500,
autocomplete: "off",
placeholder: "请描述遇到的问题、建议或想反馈的内容…",
value: content.value,
disabled: status.value === "loading",
onInput: handleContentInput
}),
vue.h("span", { class: ["pp-fb-count", overLimit.value ? "is-over" : ""] }, charCount.value + "/500")
])
]),
vue.h("div", { class: "pp-fb-field" }, [
vue.h("label", { class: "pp-fb-label", for: "pp-fb-qq" }, [
vue.h("span", null, "QQ 号"),
vue.h("span", { class: "pp-fb-req" }, "*")
]),
vue.h("input", {
id: "pp-fb-qq",
class: ["pp-fb-input", qqBad.value ? "is-bad" : ""],
type: "text",
inputmode: "numeric",
maxlength: 12,
autocomplete: "off",
required: true,
placeholder: "请输入 5–12 位 QQ 号",
value: qq.value,
disabled: status.value === "loading",
"aria-required": "true",
"aria-invalid": qqBad.value ? "true" : "false",
onInput: handleQqInput,
onBlur: () => { touched.value = true; }
}),
qqBad.value ? vue.h("div", { class: "pp-fb-field-hint" }, "请输入 5–12 位数字 QQ 号") : null
]),
vue.h("p", { class: "pp-fb-explain pp-fb-qq-hint" }, "请您填写真实 QQ 号,核实后我们会通过 QQ 与您联系。"),
renderError(),
communityCta.value
? renderCommunityCta("服务器反馈仅限赞助用户", "未检测到有效赞助 Token;请到「配置」页重新验证,或使用社区反馈。")
: null,
vue.h("div", { class: "pp-fb-actions" }, [
vue.h("button", {
type: "button",
class: ["pp-fb-submit", status.value === "loading" ? "is-loading" : ""],
disabled: !canSubmit.value,
onClick: handleSubmit
}, submitLabel())
])
]
]),
ppHint("反馈内容仅用于核实处理,不会保存在本地", { icon: "shield" }),
communityLink("也可以前往脚本社区反馈", "pp-fb-community")
]);
}
});
const AuthorWords = _sfc_main$4;
const _sfc_main_description = vue.defineComponent({
name: "ScriptDescription",
setup() {
const desc = vue.ref(globalScriptDescription);
const modeBlock = (title, titleClass, mode) => {
const nodes = [
vue.h("h4", { class: titleClass }, title),
vue.h("p", null, mode.desc || ""),
vue.h("p", { class: "muted" }, "消耗:" + (mode.cost || "-"))
];
(mode.details || []).forEach((detail) => nodes.push(vue.h("p", { class: "muted" }, "• " + detail)));
if (mode.models && mode.models.length) {
nodes.push(vue.h("p", { class: "muted" }, "校验模型:" + mode.models.join("、")));
}
return nodes;
};
return () => {
const d = desc.value;
if (!d) {
return ppEmpty({ icon: "book", title: "暂无脚本说明", desc: "服务器暂时没有返回说明内容" });
}
const groups = d.modelGroups || [];
return vue.h("div", null, [
ppDoc("模式说明", "sliders", [
...modeBlock(PP_KEY.NORMAL_MODE, "pp-mode-normal", d.modes.normal || {}),
...modeBlock("校验模式", "pp-mode-verify", d.modes.verify || {}),
...modeBlock("AI 模式", "pp-mode-ai", d.modes.ai || {})
]),
ppDoc("AI 模型价格", "spark", groups.map((group) => vue.h("div", { key: group.type }, [
vue.h("h4", null, group.type),
(group.models || []).map((model) => vue.h("div", { key: model.name }, [
vue.h("div", { class: "pp-price-row" }, [
vue.h("b", null, model.name),
vue.h("span", { class: "pp-price-cost" }, model.cost)
]),
vue.h("div", { class: "pp-price-sub" }, "输入 ¥" + model.inputPrice + " / 输出 ¥" + model.outputPrice)
]))
]))),
d.sponsor
? ppDoc("赞助支持", "heart", [
vue.h("p", null, [
d.sponsor.text.split(d.sponsor.linkText)[0],
vue.h("a", { href: d.sponsor.link, target: "_blank" }, d.sponsor.linkText),
d.sponsor.text.split(d.sponsor.linkText)[1] || ""
])
])
: null
]);
};
}
});
const ScriptDescription = _sfc_main_description;
const FrameScanner = {
collectDirect(root) {
return [...root.querySelectorAll("iframe")];
},
collectDeep(root) {
return rxjs.of(FrameScanner.collectDeepSync(root));
},
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;
}
};
function _ppApplyCipherMap(text, doc) {
const map = doc && doc.__ppCipherCharMap;
if (!map || !text) return text;
let out = "";
for (let i = 0; i < text.length; i++) {
const codePoint = text.charCodeAt(i);
out += map[codePoint] ? String.fromCharCode(map[codePoint]) : text[i];
}
return out;
}
const ppGetFontDecrypt = () => {
try {
if (typeof _unsafeWindow !== "undefined" && _unsafeWindow.FontDecrypt) return _unsafeWindow.FontDecrypt;
} catch (e) {}
try {
if (typeof window !== "undefined" && window.FontDecrypt) return window.FontDecrypt;
} catch (e) {}
return null;
};
const ppFontDecryptAvailable = () => {
const lib = ppGetFontDecrypt();
return !!(lib && typeof lib.cxDecode === "function" && typeof lib.yktPrepare === "function");
};
let _ppFontDecryptInitTried = false;
const ppInitFontDecryptFromResource = () => {
if (_ppFontDecryptInitTried) return;
_ppFontDecryptInitTried = true;
if (ppGetFontDecrypt()) return;
let code = "";
try {
if (typeof GM_getResourceText === "function") code = GM_getResourceText("fontDecryptLib") || "";
} catch (e) {
code = "";
}
if (!code) return;
try {
(0, eval)(code);
} catch (e) {
try {
console.warn("[字体解密] 库执行失败:", e && e.message ? e.message : e);
} catch (err) {}
}
};
ppInitFontDecryptFromResource();
let _ppFontDecryptWarned = false;
const ppWarnFontDecrypt = (msg) => {
if (_ppFontDecryptWarned) return;
_ppFontDecryptWarned = true;
try {
useLogStore().addLog(msg, "warning");
} catch (e) {}
try {
console.warn("[字体解密] " + msg);
} catch (e) {}
};
const hasCipherText = (doc) => {
try {
return !!(doc && doc.querySelector && doc.querySelector(".font-cxsecret"));
} catch (e) {
return false;
}
};
const _ppFontDecryptState = { status: null, stats: null, at: 0 };
const ppFontDecryptDocStats = (doc) => {
try {
const styles = doc.querySelectorAll("style");
let secret = 0;
let base64 = 0;
styles.forEach((s) => {
const t = s.textContent || "";
if (t.indexOf("font-cxsecret") !== -1) secret += 1;
if (t.indexOf("base64,") !== -1) base64 += 1;
});
return {
styles: styles.length,
secret,
base64,
links: doc.querySelectorAll('link[rel="stylesheet"]').length,
cipher: doc.querySelectorAll(".font-cxsecret").length,
};
} catch (e) {
return null;
}
};
const ppFontDecryptSnapshot = () => {
if (!_ppFontDecryptState.status) return null;
return { status: _ppFontDecryptState.status, ...(_ppFontDecryptState.stats || {}) };
};
const decodeCipherFontSafe = (doc) => {
const remember = (status) => {
_ppFontDecryptState.status = status;
_ppFontDecryptState.stats = ppFontDecryptDocStats(doc);
_ppFontDecryptState.at = Date.now();
};
const lib = ppGetFontDecrypt();
if (!lib || typeof lib.cxDecode !== "function") {
remember("no-lib");
if (hasCipherText(doc)) {
ppWarnFontDecrypt("字体解密库未加载(@resource fontDecryptLib 缺失),本题可能是乱码;请重装或更新脚本");
}
return;
}
let result;
try {
result = lib.cxDecode(doc);
} catch (e) {
remember("error");
if (hasCipherText(doc)) {
ppWarnFontDecrypt("字体解密异常:" + (e && e.message ? e.message : e));
}
return;
}
remember(result && result.ok === true ? "ok" : (result && result.reason) || "unknown");
if (result && result.ok === false && result.reason && result.reason !== "no-target") {
ppWarnFontDecrypt("字体解密未生效(" + result.reason + "),本题可能带乱码;请把该页面反馈给作者");
}
return result;
};
const ppYktPrepareFont = async (doc) => {
const lib = ppGetFontDecrypt();
if (!lib || typeof lib.yktPrepare !== "function") return false;
try {
if (typeof lib.yktDetect === "function" && !lib.yktDetect(doc)) return false;
return await lib.yktPrepare(doc);
} catch (e) {
return false;
}
};
const ppDecryptQuestionClone = (el, platform) => {
const lib = ppGetFontDecrypt();
if (!lib || typeof lib.cloneDecrypted !== "function") return el;
try {
return lib.cloneDecrypted(el, platform);
} catch (e) {
return el;
}
};
const CX_FACE_SELECTORS = ["#fcqrimg", ".chapterVideoFaceMaskDiv", ".faceRecognition", ".face-verify"];
const CX_FACE_TEXT_RE = /(请完成人脸识别|人脸识别验证|请进行人脸识别|请先完成人脸)/;
const detectCxFacePrompt = () => {
const doc = (typeof _unsafeWindow !== "undefined" && _unsafeWindow.document) || document;
if (!doc || !doc.body) return false;
for (const sel of CX_FACE_SELECTORS) {
const el = doc.querySelector(sel);
if (el && el.offsetHeight > 0) return true;
}
const layers = doc.querySelectorAll('[class*="face"],[class*="Face"],.maskDiv,.popDiv');
for (const layer of layers) {
if (layer.offsetHeight > 0 && CX_FACE_TEXT_RE.test(layer.innerText || "")) return true;
}
return false;
};
const ppfxWaitFaceCheck = async (timeoutMs = 120000, pollMs = 2000) => {
if (!detectCxFacePrompt()) return false;
try {
useLogStore().addLog("检测到人脸识别,请先完成验证,脚本将等待(最多120秒)", "danger");
} catch (e) {}
const start = Date.now();
while (Date.now() - start < timeoutMs) {
await new Promise((resolve) => setTimeout(resolve, pollMs));
if (!detectCxFacePrompt()) return true;
}
return true;
};
const waitForAudioFrames = async (doc, timeoutMs = 5000) => {
if (!doc) return;
const frames = Array.from(doc.querySelectorAll('iframe[src*="audioplay"]'));
if (!frames.length) return;
const ready = () =>
frames.every((frame) => {
try {
const inner = frame.contentDocument;
if (!inner) return false;
const audio = inner.querySelector("audio");
return Boolean((audio && audio.src) || inner.querySelector("source[src]"));
} catch (e) {
return false;
}
});
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (ready()) return;
await new Promise((resolve) => setTimeout(resolve, 100));
}
};
const PP_TYPE_CODE_MAP = new Map([
["单选题", "0"], ["A1型题", "0"], ["A1A2型题", "0"],
["多选题", "1"], ["X型题", "1"],
["填空题", "2"], ["听力填空", "2"], ["听力对话填空", "2"], ["完形填空", "2"], ["短文填空", "2"], ["选词填空", "2"],
["填表题", "2"], ["阅读填空", "2"], ["阅读题", "2"], ["阅读理解", "2"],
["程序题", "17"], ["写作题", "26"],
["判断题", "3"],
["简答题", "4"], ["名词解释", "5"],
["论述题", "6"], ["计算题", "7"],
["排序题", "13"],
["连线题", "11"]
]);
const PP_TYPE_CODE_ALIASES = [
[/多选|多项/, "1"],
[/判断|是非/, "3"],
[/填空/, "2"],
[/简答/, "4"],
[/名词/, "5"],
[/论述/, "6"],
[/计算/, "7"],
[/单选|单项/, "0"]
];
const ppTypeCodeOf = (label) => {
const text = String(label == null ? "" : label).trim();
const exact = PP_TYPE_CODE_MAP.get(text);
if (exact && /^[0-7]$/.test(exact)) return exact;
let hit = null;
for (const [pattern, code] of PP_TYPE_CODE_ALIASES) {
const at = text.search(pattern);
if (at < 0) continue;
if (!hit || at < hit.at) hit = { at, code };
}
return hit ? hit.code : "999";
};
const ppOptionLooksSelected = (optEl) => {
if (!optEl) return false;
if (optEl.getAttribute("aria-checked") === "true") return true;
if (optEl.classList.contains("cur") || optEl.classList.contains("selected")) return true;
const spanEl = optEl.querySelector("span.num_option_dx") || optEl.querySelector("span.num_option") || optEl.querySelector("span[data]");
if (spanEl && (spanEl.classList.contains("cur") || spanEl.classList.contains("selected"))) return true;
return Boolean(optEl.querySelector(".onChecked") || optEl.querySelector(".check_answer"));
};
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) => {
if (question.type === "0" || question.type === "1") {
for (const key in question.options) {
const optEl = question.options[key];
if (ppOptionLooksSelected(optEl)) return true;
}
return false;
}
if (question.type === "14" && Array.isArray(question._cloze) && question._cloze.length > 0) {
return question._cloze.every((c) => (c.elements || []).some((el) => el.querySelector(".check_answer")));
}
if (question.type === "24" && question._fillBlanks && Array.isArray(question._fillBlanks.blanks)) {
return question._fillBlanks.blanks.every((b) => b && b.classList && b.classList.contains("hasFill"));
}
if (question.type === "20" && Array.isArray(question._shared) && question._shared.length > 0) {
return question._shared.every((s) =>
(s.elements || []).some((el) => (el.classList ? el.classList.contains("check_answer") : false)),
);
}
if (
question.type === "2" ||
question.type === "14" ||
question.type === "9" ||
(question.type === "10" && String(question.workType) !== "zhs")
) {
const taElements = question.element.querySelectorAll("textarea");
for (const ta of taElements) {
if (ta.value && ta.value.trim() !== "") {
return true;
}
}
const inpDivs = question.element.querySelectorAll(".InpDIV");
for (const inpDiv of inpDivs) {
if (inpDiv.innerHTML && inpDiv.innerHTML.trim() !== "") {
return true;
}
}
return false;
}
if (question.type === "3") {
for (const key in question.options) {
const optEl = question.options[key];
if (optEl.getAttribute("aria-checked") === "true") return true;
if (optEl.classList.contains("cur") || optEl.classList.contains("selected")) return true;
const spanEl = optEl.querySelector("span.num_option") || optEl.querySelector("span[data]");
if (spanEl && (spanEl.classList.contains("cur") || spanEl.classList.contains("selected"))) return true;
if (optEl.querySelector(".onChecked") || optEl.querySelector(".check_answer")) return true;
}
return false;
}
if (question.type === "4" || question.type === "5" || question.type === "6" || question.type === "7" || question.type === "8") {
const taEl = question.element.querySelector("textarea");
if (taEl && taEl.value && taEl.value.trim() !== "") {
return true;
}
return false;
}
if (question.type === "11") {
const selectElements = question.element.querySelectorAll("select.dept_select");
for (const sel of selectElements) {
if (!sel.value || sel.value.trim() === "") return false;
}
return selectElements.length > 0;
}
if (question.type === "17") {
try {
const cmHost = question.element.querySelector(".CodeMirror");
const cm = cmHost && cmHost.CodeMirror;
if (cm && typeof cm.getValue === "function" && String(cm.getValue() || "").trim() !== "") return true;
} catch (e) {}
try {
const codeEditor = question.element.querySelector("textarea.code-editor");
if (codeEditor && String(codeEditor.value || "").trim() !== "") return true;
} catch (e) {}
return false;
}
if (question.type === "26") {
try {
const textareaElement = question.element.querySelector("textarea");
const UE = this._window && this._window.UE;
const ueditor =
UE && textareaElement && typeof UE.getEditor === "function"
? UE.getEditor(textareaElement.id || textareaElement.name)
: null;
if (ueditor && typeof ueditor.getContent === "function" && this.stripTags(ueditor.getContent()).trim() !== "") {
return true;
}
} catch (e) {}
try {
const ueFrame = question.element.querySelector("iframe#ueditor_0");
if (ueFrame) {
const doc = ueFrame.contentDocument || (ueFrame.contentWindow && ueFrame.contentWindow.document);
const body = doc && doc.body;
if (body && String(body.textContent || "").trim() !== "") return true;
}
} catch (e) {}
return false;
}
if (Array.isArray(question._reading) && question._reading.length > 0) {
return question._reading.every((sub) => {
if (Array.isArray(sub.blanks) && sub.blanks.length > 0) {
return sub.blanks.every((blankEl) => this.isBlankFilled(blankEl));
}
if (sub.el && sub.el.querySelector("textarea, iframe, .InpDIV")) {
return this.isBlankFilled(sub.el);
}
return (sub.elements || []).some((el) =>
el.querySelector(".check_answer, .onChecked") ||
(el.classList && el.classList.contains("check_answer"))
);
});
}
return false;
});
__publicField(this, "typeMap", PP_TYPE_CODE_MAP);
__publicField(this, "stripTags", (html) => {
if (html == null) return "";
const formulas = [];
const unAttr = (v) => String(v == null ? "" : v).replace(/</gi, "<").replace(/>/gi, ">").replace(/"/gi, '"').replace(/*39;/g, "'").replace(/'/gi, "'").replace(/&/gi, "&");
const withSlots = String(html).replace(REGEX.MJX_CONTAINER, (block) => {
const m = block.match(REGEX.DATA_LATEX);
const latex = unAttr(m ? (m[1] != null ? m[1] : m[2]) : "");
formulas.push(latex);
return latex ? ` \uE000MJX-${formulas.length - 1}\uE000 ` : `\uE000MJX-${formulas.length - 1}\uE000`;
});
return withSlots.replace(REGEX.HTML_TAGS, "").replace(REGEX.NBSP, " ").replace(REGEX.WHITESPACE, " ").replace(REGEX.BR_TAG, "\n").replace(REGEX.IMG_TAG, '
').replace(REGEX.MJX_SLOT, (all, idx) => formulas[Number(idx)] || "").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/*39;/g, "'").replace(/'/g, "'").replace(/&/g, "&").trim();
});
__publicField(this, "htmlWithAbsoluteImages", (el) => {
if (!el) return "";
let clone;
try {
clone = el.cloneNode(true);
} catch (e) {
return el.innerHTML || "";
}
const view = el.ownerDocument && el.ownerDocument.defaultView;
const dropHidden = (src, dst) => {
const srcChildren = Array.from(src.children || []);
const dstChildren = Array.from(dst.children || []);
for (let i = 0; i < srcChildren.length; i++) {
const s = srcChildren[i];
const d = dstChildren[i];
if (!d) continue;
const tag = String(s.tagName || "").toLowerCase();
if (tag === "script" || tag === "style") {
d.remove();
continue;
}
let hidden = false;
try {
hidden = Boolean(view && s.isConnected && view.getComputedStyle(s).display === "none");
} catch (e) {}
if (hidden) {
d.remove();
continue;
}
dropHidden(s, d);
}
};
try {
dropHidden(el, clone);
} catch (e) {}
clone.querySelectorAll("img").forEach((img) => {
const lazy = img.getAttribute("data-src") || img.getAttribute("data-original") || img.getAttribute("data-lazy-src") || img.getAttribute("data-actualsrc");
const cur = (img.getAttribute("src") || "").trim();
if (lazy && (!cur || /^data:/i.test(cur))) {
try {
img.setAttribute("src", new URL(lazy, window.location.href).toString());
} catch (e) {
img.setAttribute("src", lazy);
}
return;
}
const abs = img.src;
if (abs) img.setAttribute("src", abs);
});
return clone.innerHTML;
});
__publicField(this, "extractOptions", (optionElements, optionSelector) => {
const optionsObject = {};
const optionTexts = [];
const optionsList = [];
const allElements = Array.from(optionElements || []);
const topElements = allElements.filter((el) => !allElements.some((other) => other !== el && other.contains(el)));
topElements.forEach((optionElement) => {
const inner = optionElement.querySelector(optionSelector) || optionElement;
const optionTextContent = this.stripTags(this.htmlWithAbsoluteImages(inner));
if (!optionTextContent) return;
optionsList.push({ text: optionTextContent, el: optionElement });
optionTexts.push(optionTextContent);
let key = optionTextContent;
let suffix = 2;
while (Object.prototype.hasOwnProperty.call(optionsObject, key)) {
key = optionTextContent + "#" + suffix;
suffix += 1;
}
optionsObject[key] = optionElement;
});
return [optionsObject, optionTexts, optionsList];
});
__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: [],
num: "",
usenum: ""
},
msg: reason
});
const showNoticeDialog = (title, message, noticeType, message2) => {
try {
if (document.getElementById("server-notice-dialog")) return;
const blocks = [message, message2]
.filter((text) => String(text || "").trim() !== "")
.map((text) => `${text}
`)
.join("");
if (!blocks) return;
_ppEnsurePublicStyles();
const dialog = document.createElement("div");
dialog.id = "server-notice-dialog";
dialog.innerHTML = `
!
${_ppPublicEscape(title || "系统公告")}
来自服务端
${blocks}
`;
document.body.appendChild(dialog);
const close = () => {
try { _GM_setValue("closed_notice_type", noticeType); } catch (e) {}
document.removeEventListener("keydown", onKey);
dialog.remove();
};
const onKey = (event) => {
if (event.key === "Escape") close();
};
document.getElementById("notice-close-btn").onclick = close;
document.getElementById("notice-close-x").onclick = close;
document.addEventListener("keydown", onKey);
} catch (e) {}
};
const checkServerNotice = () => {
_GM_xmlhttpRequest({
method: "GET",
url: getRandomServer() + "/notice",
timeout: 5000,
onload: (response) => {
try {
const result = JSON.parse(response.responseText);
if (result.code === 200 && result.data && result.data.enabled) {
const noticeType = result.data.type || 1;
const noticeMessage = result.data.message || '';
const noticeMessage2 = result.data.message2 || '';
const noticeTitle = result.data.title || '系统公告';
const closedNotice = _GM_getValue('closed_notice_type', 0);
if (closedNotice === noticeType) {
return;
}
showNoticeDialog(noticeTitle, noticeMessage, noticeType, noticeMessage2);
}
} catch (e) {
}
},
});
};
const readCxParamValue = (partName, paramName, fallback = false) => {
try {
const parts = useConfigStore().platformParams?.cx?.parts || [];
const part = parts.find(p => p.name === partName);
const param = (part?.params || []).find(p => p.name === paramName);
const val = param ? param.value : undefined;
return val === undefined || val === null ? fallback : val;
} catch (e) {
return fallback;
}
};
const ANSWER_THRESHOLD_DEFAULT = 85;
const ANSWER_THRESHOLD_MIN = 50;
const ANSWER_THRESHOLD_MAX = 99;
const findAnswerParam = (platformKey, paramName) => {
try {
const store = useConfigStore();
const key = platformKey || store.platformName;
const parts = store.platformParams?.[key]?.parts || [];
const part = parts.find((p) => p.name === PP_KEY.ANSWER);
return (part?.params || []).find((p) => p.name === paramName) || null;
} catch (e) {
return null;
}
};
const resolveAnswerThreshold = (platformKey) => {
const raw = Number(findAnswerParam(platformKey, PP_KEY.THRESHOLD)?.value);
return Math.min(
ANSWER_THRESHOLD_MAX,
Math.max(ANSWER_THRESHOLD_MIN, Number.isFinite(raw) && raw > 0 ? raw : ANSWER_THRESHOLD_DEFAULT),
);
};
const computeFillRate = (filled, total) => {
const totalNum = Number(total) || 0;
if (totalNum <= 0) return 0;
const filledNum = Math.max(0, Math.min(totalNum, Number(filled) || 0));
return Math.round((filledNum / totalNum) * 10000) / 100;
};
const describeFillProgress = (filled, total, skipped = 0) => {
const filledNum = Math.max(0, Number(filled) || 0);
const totalNum = Math.max(0, Number(total) || 0);
const skippedHint = Number(skipped) > 0 ? `,跳过已答 ${Number(skipped) || 0} 题` : "";
return `已填入 ${filledNum}/${totalNum} 题,填入率 ${computeFillRate(filledNum, totalNum)}%${skippedHint}`;
};
const logFillProgress = (addLog, pageLabel, filled, total, skipped = 0) => {
try {
addLog(describeFillProgress(filled, total, skipped), "primary");
} catch (e) {}
return computeFillRate(filled, total);
};
const PP_LOG_PLATFORM_LABELS = { cx: "学习通", zhs: "智慧树", ykt: "雨课堂" };
const ppLogPlatformLabel = (platformKey) => PP_LOG_PLATFORM_LABELS[platformKey] || "通用";
const ppLogStemHint = (title, maxLength = 30) => {
const text = String(title == null ? "" : title).replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
if (!text) return "";
return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
};
const ppLogQuestionLine = (index, title) => {
const hint = ppLogStemHint(title);
return hint ? `第${index}题(${hint})` : `第${index}题(题干未识别)`;
};
const ppLogAnswerBegin = (addLog, options) => {
const opts = options || {};
const total = Math.max(0, Number(opts.total) || 0);
const head = `检测到${ppLogPlatformLabel(opts.platform)}${opts.scene || "答题"}`;
try {
addLog(total > 0 ? `${head},共 ${total} 题,开始作答` : `${head},开始作答`, "primary");
} catch (e) {}
};
const ppLogAnswerQuestion = (addLog, index, title) => {
try { addLog(ppLogQuestionLine(index, title), "info"); } catch (e) {}
};
const ppLogAnswerFilled = (addLog, index, extra) => {
const info = extra || {};
try {
addLog(`第${index}题已填入${info.source === "ai" ? "(AI)" : ""}`, "success");
if (info.cost !== undefined && info.cost !== null) {
addLog(`本题消耗${info.cost}次,剩余${info.num}次`, "primary");
}
} catch (e) {}
};
const ppLogAnswerSkipped = (addLog, index) => {
try { addLog(`第${index}题已作答,跳过`, "warning"); } catch (e) {}
};
const ppLogAnswerFillFailed = (addLog, index, reason) => {
try { addLog(`第${index}题填入失败${reason ? `:${reason}` : ""}`, "warning"); } catch (e) {}
};
const ppLogAnswerQueryFailed = (addLog, index, reason) => {
try { addLog(`第${index}题查询失败${reason ? `:${reason}` : ""}`, "danger"); } catch (e) {}
};
const PP_REVIEW_FAILED_MESSAGE = "题目命中题库,但AI复查发现题库答案有误,待修正;请自行作答,或使用AI/校验模式作答。";
const isReviewFailed = (answerData) => Boolean(
answerData && answerData.data && answerData.data.review === "failed"
);
const reviewFailedMessage = (answerData) =>
(answerData && answerData.data && answerData.data.reviewMessage) || PP_REVIEW_FAILED_MESSAGE;
const reviewFailedAnswers = (answerData) => {
const raw = answerData && answerData.data && answerData.data.answer;
return Array.isArray(raw) ? raw : (raw ? [raw] : []);
};
const ppLogAnswerReviewFailed = (addLog, answerData) => {
try { addLog(reviewFailedMessage(answerData), "warning"); } catch (e) {}
};
const ppLogAnswerSummary = (addLog, stats) => {
const data = stats || {};
const num = (value) => Math.max(0, Number(value) || 0);
const total = num(data.total) || num(data.handled);
const filled = num(data.filled);
const skipped = num(data.skipped);
const failed = num(data.failed);
const rateText = total > 0 ? `(填入率 ${computeFillRate(filled, total)}%)` : "";
try {
addLog(`本次共处理 ${num(data.handled)} 题:填入 ${filled}、跳过 ${skipped}、失败 ${failed}${rateText}`, filled > 0 ? "success" : "warning");
} catch (e) {}
};
const PP_LOG_SUBMIT_NOTES = {
manual: ["当前页面不支持自动提交:请检查已填入的答案后手动提交", "warning"],
autoOff: ["未开启自动提交:请检查已填入的答案后手动提交", "warning"],
submitted: ["已自动提交:请在页面上确认结果", "success"],
examManual: ["考试不允许自动交卷:请检查答案后手动交卷", "warning"]
};
const ppLogAnswerSubmitNote = (addLog, kind) => {
const note = PP_LOG_SUBMIT_NOTES[kind];
if (!note) return;
try { addLog(note[0], note[1]); } catch (e) {}
};
const runAnswerSubmitPolicy = async (options) => {
const opts = options || {};
const addLog = typeof opts.addLog === "function" ? opts.addLog : () => {};
const pageLabel = opts.pageLabel || "答题";
const autoSubmit = Boolean(opts.autoSubmit);
const threshold = resolveAnswerThreshold(opts.platformKey);
const fillRate = computeFillRate(opts.filled, opts.total);
const canSubmit = opts.canSubmit === undefined ? typeof opts.submit === "function" : Boolean(opts.canSubmit);
const canDraft = opts.canDraft === undefined ? typeof opts.draft === "function" : Boolean(opts.canDraft);
if (opts.logRate !== false) {
addLog(describeFillProgress(opts.filled, opts.total, opts.skipped), "primary");
}
if (!autoSubmit) {
if (canDraft) {
addLog("未开启自动提交:已填入答案但未提交,已暂存", "primary");
await opts.draft();
} else {
addLog("未开启自动提交:请检查已填入的答案后手动提交", "primary");
}
return { fillRate, threshold, action: "idle" };
}
if (!canSubmit) {
addLog("当前页面不支持自动提交:请检查已填入的答案后手动提交", "warning");
return { fillRate, threshold, action: "idle" };
}
if (fillRate >= threshold) {
addLog(`填入率 ${fillRate}% ≥ 正确阈值 ${threshold}%:自动提交`, "success");
await opts.submit();
return { fillRate, threshold, action: "submit" };
}
if (canDraft) {
addLog(`填入率 ${fillRate}% < 正确阈值 ${threshold}%:不自动提交,已暂存`, "warning");
await opts.draft();
return { fillRate, threshold, action: "draft" };
}
addLog(`填入率 ${fillRate}% < 正确阈值 ${threshold}%:不自动提交,请手动补答后提交`, "warning");
return { fillRate, threshold, action: "draft" };
};
const readGlobalAfk = () => {
try {
const store = useConfigStore();
const parts = store.platformParams?.[store.platformName]?.parts || [];
const modePart = parts.find((p) => p.name === PP_KEY.MODE);
const param = (modePart?.params || []).find((p) => p.name === PP_KEY.STANDBY);
return Boolean(param && param.value);
} catch (e) {
return false;
}
};
const PpWorkerTimer = (() => {
let worker = null;
let seq = 0;
const callbacks = new Map();
const code = `self.onmessage=function(e){var d=e.data;if(d.type==='start'){self._t=self._t||{};self._t[d.id]=setInterval(function(){self.postMessage({type:'tick',id:d.id})},d.interval)}else if(d.type==='stop'){if(self._t&&self._t[d.id]){clearInterval(self._t[d.id]);delete self._t[d.id]}}else if(d.type==='once'){setTimeout(function(){self.postMessage({type:'once',id:d.id})},d.delay)}};`;
const ensure = () => {
if (worker) return true;
try {
const blob = new Blob([code], { type: "application/javascript" });
worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = (e) => {
const { type, id } = e.data || {};
if (type === "tick") {
const cb = callbacks.get(id);
if (cb) cb();
} else if (type === "once") {
const cb = callbacks.get(id);
callbacks.delete(id);
if (cb) cb();
}
};
worker.onerror = () => {};
} catch (e) {
worker = null;
}
return Boolean(worker);
};
const newTimer = (prefix, cb, message) => {
if (!ensure()) return null;
const id = `${prefix}${++seq}`;
callbacks.set(id, cb);
worker.postMessage(message(id));
return id;
};
return {
interval(cb, ms) {
return newTimer("ppw", cb, (id) => ({ type: "start", id, interval: ms }));
},
once(ms, cb) {
return newTimer("ppo", cb, (id) => ({ type: "once", id, delay: ms }));
},
stop(id) {
if (!id || !worker) return;
callbacks.delete(id);
try {
worker.postMessage({ type: "stop", id });
} catch (e) {}
}
};
})();
const afkWait = (ms) => new Promise((resolve) => {
if (!readGlobalAfk()) {
setTimeout(resolve, ms);
return;
}
if (!PpWorkerTimer.once(ms, resolve)) setTimeout(resolve, ms);
});
const afkIntervalHandle = (cb, ms) => {
if (readGlobalAfk()) {
const id = PpWorkerTimer.interval(cb, ms);
if (id) return { kind: "worker", id, stop() { PpWorkerTimer.stop(id); } };
}
const timer = setInterval(cb, ms);
return { kind: "timer", id: timer, stop() { clearInterval(timer); } };
};
const afkInterval = (cb, ms) => afkIntervalHandle(cb, ms);
let _afkHeartbeatStarted = false;
const startAfkHeartbeat = () => {
if (_afkHeartbeatStarted) return;
if (!readGlobalAfk()) return;
_afkHeartbeatStarted = true;
const tick = () => {
try {
const doc = _unsafeWindow.document;
const view = _unsafeWindow;
const x = 20 + Math.floor(Math.random() * 200);
const y = 20 + Math.floor(Math.random() * 200);
doc.dispatchEvent(new MouseEvent("mousemove", { bubbles: true, clientX: x, clientY: y, view }));
doc.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Shift", view }));
doc.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "Shift", view }));
} catch (e) {}
};
PpWorkerTimer.interval(tick, 20000);
};
const getCxModeLabel = () => {
try {
const platform = useConfigStore().platformName;
if (platform === "ykt") {
const mode =
[PP_KEY.ONLY_ANSWER, PP_KEY.ONLY_VIDEO, PP_KEY.REVIEW].find((name) => readYktParam(PP_KEY.MODE, name, false)) ||
PP_KEY.NORMAL_MODE;
return `当前模式:${mode}`;
}
if (platform !== "cx") return "";
} catch (e) {}
const taskMode =
[PP_KEY.REVIEW, PP_KEY.ONLY_VIDEO, PP_KEY.ONLY_ANSWER].find((name) => readCxParamValue(PP_KEY.MODE, name, false)) ||
PP_KEY.NORMAL_MODE;
return `当前模式:${taskMode}`;
};
const fetchAnswerData = async (question) => {
var _a;
const _self = _unsafeWindow;
const configStore = useConfigStore();
const token = configStore.queryApis[0].token;
window.__answerModeLogged = false;
let verifyAnswer = false;
let useAIOnly = false;
let enableWebSearch = false;
let aiType = globalModelConfig?.typeOptions?.[0] || '';
let aiModel = globalModelConfig?.defaultModels?.[aiType] || '';
const answerParamsPart = configStore.platformParams[configStore.platformName]?.parts.find(p => p.name === PP_KEY.ANSWER);
if (answerParamsPart && answerParamsPart.params) {
const normalModeParam = answerParamsPart.params.find(p => p.name === PP_KEY.NORMAL_MODE);
const aiModeParam = answerParamsPart.params.find(p => p.name === PP_KEY.AI_MODE);
const answerVerifyParam = answerParamsPart.params.find(p => p.name === PP_KEY.VERIFY);
const webSearchParam = answerParamsPart.params.find(p => p.name === PP_KEY.WEB_SEARCH);
const aiTypeParam = answerParamsPart.params.find(p => p.name === PP_KEY.AI_TYPE);
const aiModelParam = answerParamsPart.params.find(p => p.name === PP_KEY.AI_MODEL);
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;
}
if (webSearchParam && webSearchParam.value && useAIOnly) {
enableWebSearch = true;
}
}
const modelType = getModelType(aiType, aiModel);
const logStore = useLogStore();
if (!window.__answerModeLogged && useAIOnly) {
logStore.addLog(`AI模型: ${aiType} - ${aiModel}`, 'info');
}
const userId = getUidRealtime();
const isZhs = String(question.workType) === "zhs";
const isYkt = String(question.workType) === "ykt";
try {
if (question.element && question.element.ownerDocument) {
decodeCipherFontSafe(question.element.ownerDocument);
}
} catch (e) {}
const questionData = {
questionData: question.element.outerHTML,
workType: isYkt ? "ykt" : isZhs ? "zhs" : "cx",
refer: question.refer,
u: userId,
t: Math.floor(( new Date()).getTime() / 1e3).toString()
};
if (isZhs || isYkt) {
questionData.questionData = question.html || question.element.outerHTML;
} else {
questionData.audios = Array.isArray(question.audios) ? question.audios : [];
questionData.audioReferer = (() => {
try {
return new URL(question.refer || window.location.href).origin + "/";
} catch (e) {
return "";
}
})();
}
const startTaskId = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0;
const answerIntervalParam = answerParamsPart?.params.find(p => p.name === PP_KEY.ANSWER_GAP);
const simulateDelay = answerParamsPart?.params.find(p => p.name === PP_KEY.SIM_DELAY)?.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 };
}
let step1TaskId = null;
const sendRequest = (checkOnly = false) => {
return new Promise((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');
}
const timeout = checkOnly ? 60000 : 90000;
_GM_xmlhttpRequest({
url: getRandomServer(),
method: "POST",
headers: {
"Content-Type": "application/json"
},
data: JSON.stringify({
token: token,
questionData: questionData,
verifyAnswer: verifyAnswer,
aiMode: useAIOnly,
enableWebSearch: enableWebSearch,
model: modelType,
checkOnly: checkOnly,
step1TaskId: step1TaskId || undefined,
userId: questionData.u,
clientVersion: getClientVersion(),
fontDecrypt: ppFontDecryptSnapshot(),
async: true
}),
timeout: timeout,
onload: (response) => {
try {
const apiResponse = JSON.parse(response.responseText);
if (apiResponse.code === 202 && apiResponse.data && apiResponse.data.taskId) {
const taskId = apiResponse.data.taskId;
pollQueryTask(taskId, resolve, globalPollInterval);
} else {
resolve(apiResponse);
}
} catch (e) {
resolve(buildErrorResponse("解析出错"));
}
},
onerror: () => resolve(buildErrorResponse("请求出错")),
ontimeout: () => resolve({ code: 404, msg: "请求超时", data: { answer: [] } })
});
});
};
const pollQueryTask = (taskId, resolve, pollInterval = 1000) => {
let pollCount = 0;
const MAX_WAIT_MS = 10 * 60 * 1000;
const maxPolls = Math.max(120, Math.ceil(MAX_WAIT_MS / Math.max(500, pollInterval)));
const poll = () => {
pollCount++;
if (pollCount > maxPolls) {
resolve(buildErrorResponse("查询超时"));
return;
}
_GM_xmlhttpRequest({
url: getRandomServer() + `/query-task/${taskId}`,
method: "GET",
timeout: 10000,
onload: (response) => {
try {
const result = JSON.parse(response.responseText);
if (result.code === 200 && result.data) {
const { status, result: taskResult } = result.data;
if (status === 'completed' && taskResult) {
if (taskResult.code === 202 && taskResult.status === "thinking") step1TaskId = taskId;
resolve(taskResult);
} else if (status === 'processing' || status === 'pending') {
setTimeout(poll, pollInterval);
} else {
resolve(buildErrorResponse("查询异常"));
}
} else if (result.code === 404) {
resolve(buildErrorResponse("任务过期"));
} else {
resolve(buildErrorResponse("轮询异常"));
}
} catch (e) {
setTimeout(poll, pollInterval);
}
},
onerror: () => setTimeout(poll, pollInterval),
ontimeout: () => setTimeout(poll, pollInterval)
});
};
setTimeout(poll, pollInterval);
};
const firstResponse = useAIOnly ? await sendRequest(false) : await sendRequest(true);
if (firstResponse.code === 202 && firstResponse.status === "thinking") {
const logStore = useLogStore();
const thinkingModelName = firstResponse.data?.thinkingModel || 'AI模型';
const thinkingStartTaskId = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0;
logStore.addLog('答案校验失败,启动深度思考...', 'warning');
logStore.addLog(`${thinkingModelName} 正在深度思考中,请耐心等待...`, 'info');
window.__inThinkingMode__ = true;
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);
window.__inThinkingMode__ = false;
return finalResponse;
}
return firstResponse;
};
const queryAnswer = async (question) => {
return await fetchAnswerData(question);
};
const computeMatch = (str1, str2) => {
const s1 = str1.trim().toLowerCase();
const s2 = str2.trim().toLowerCase();
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);
};
const normalizeAnsText = (input) => {
let s = String(input == null ? "" : input);
s = s.replace(/[\uFF01-\uFF5E]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xFEE0)).replace(/\u3000/g, " ");
s = s.replace(/<[^>]*>/g, " ");
return s
.replace(/[,。!?;:""''、()【】〈〉《》,.\-!?;:'"()\[\]<>]/g, "")
.replace(/\s+/g, "")
.toLowerCase();
};
const stripOptionPrefix = (input) => String(input == null ? "" : input)
.trim()
.replace(/^[A-Ha-h]\s*[.、.,,))::\]】]\s*/, "")
.trim();
const isPlainLetterAnswer = (input) => {
const t = String(input == null ? "" : input).trim().replace(/[,,.。.、 #/\\|]/g, "").toUpperCase();
if (!/^[A-Z]{1,8}$/.test(t)) return false;
for (let i = 1; i < t.length; i++) {
if (t.charCodeAt(i) <= t.charCodeAt(i - 1)) return false;
}
return true;
};
const getOptionList = (question) => {
if (!question) return [];
if (Array.isArray(question.optionsList) && question.optionsList.length) return question.optionsList;
const list = [];
const src = question.options;
if (Array.isArray(src)) {
src.forEach((o) => list.push(typeof o === "string" ? { text: o, el: null } : { text: o.text || "", el: o.el || null }));
} else if (src && typeof src === "object") {
for (const key in src) list.push({ text: key, el: src[key] });
}
return list;
};
const imageNamesOf = (text) => {
const names = [];
try {
const re = /
]+src=["']?([^"'\s>]+)["']?/gi;
const src = String(text || "");
let m;
while ((m = re.exec(src))) {
const url = m[1];
if (/^data:/i.test(url)) continue;
const name = url.split(/[?#]/)[0].split("/").pop();
if (name) names.push(name);
}
} catch (e) {}
return names;
};
const matchOptionIndex = (optionList, answer) => {
if (!optionList || optionList.length === 0 || answer == null) return -1;
const raw = String(answer).trim();
if (!raw) return -1;
for (let i = 0; i < optionList.length; i++) {
if (String(optionList[i].text).trim() === raw) return i;
}
const strippedAnswer = stripOptionPrefix(raw);
const normAnswer = normalizeAnsText(strippedAnswer);
for (let i = 0; i < optionList.length; i++) {
const optText = stripOptionPrefix(optionList[i].text);
if (optText === strippedAnswer) return i;
const normOpt = normalizeAnsText(optText);
if (normAnswer && normOpt && normOpt === normAnswer) return i;
}
const answerImages = imageNamesOf(raw);
if (answerImages.length) {
for (let i = 0; i < optionList.length; i++) {
const optionImages = imageNamesOf(optionList[i].text);
if (optionImages.length && optionImages.some((name) => answerImages.includes(name))) return i;
}
}
return -1;
};
class CxQuestionHandler extends QuestionProcessor {
constructor(type, iframe) {
super();
__publicField(this, "type");
__publicField(this, "_fillNoop", false);
__publicField(this, "init", async () => {
await ppfxWaitFaceCheck();
await waitForAudioFrames(this._document, 5000);
this.questions = [];
this.parseHtml();
if (this.questions.length) {
const progressStore = useProgressStore();
const answerLabel = this.type === "ks" ? "考试答题" : this.type === "zj" ? "章节测验" : "作业答题";
const answerTotal = this.questions.length;
ppLogAnswerBegin((msg, type) => this.addLog(msg, type), {
platform: "cx",
scene: this.type === "ks" ? "考试" : this.type === "zj" ? "章节测验" : "作业",
total: answerTotal,
});
progressStore.update({
taskName: answerLabel,
type: "答题",
isPlaying: true,
questionTotal: answerTotal,
questionIndex: 0,
percent: 0,
currentTime: 0,
totalTime: 0,
detail: `第 0 / ${answerTotal} 题`
});
const configStore = useConfigStore();
const startTaskId = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0;
const _answerParamsPart1 = configStore.platformParams[configStore.platformName]?.parts.find(p => p.name === PP_KEY.ANSWER);
const skipAnswered = _answerParamsPart1?.params.find(p => p.name === PP_KEY.SKIP_ANSWERED)?.value || false;
this.correctNum = 0;
this.skippedCount = 0;
this.failedCount = 0;
let skippedCount = 0;
let failedCount = 0;
let aborted = false;
for (const [index, question] of this.questions.entries()) {
progressStore.update({
questionIndex: index + 1,
percent: Math.round(index / answerTotal * 100),
detail: `第 ${index + 1} / ${answerTotal} 题`
});
const currentTaskId = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0;
if (currentTaskId !== startTaskId) {
aborted = true;
break;
}
ppLogAnswerQuestion((msg, type) => this.addLog(msg, type), index + 1, question.title);
if (skipAnswered && this.checkIfAnswered(question, this.type)) {
ppLogAnswerSkipped((msg, type) => this.addLog(msg, type), index + 1);
skippedCount += 1;
question.skipped = true;
this.addQuestion(question);
continue;
}
const answerData = await queryAnswer(question);
const currentTaskId2 = window.__getAnswerTaskId__ ? window.__getAnswerTaskId__() : 0;
if (currentTaskId2 !== startTaskId || answerData.code === 499) {
aborted = true;
break;
}
if (answerData.code === 200) {
question.answer = answerData.data.answer;
question.source = answerData.data.source;
question.sourceName = answerData.data.sourceName || "";
const filledOk = (await this.fillQuestion(question)) === true;
if (filledOk) {
this.correctNum += 1;
progressStore.update({
filledCount: this.correctNum,
fillRate: computeFillRate(this.correctNum, answerTotal)
});
ppLogAnswerFilled((msg, type) => this.addLog(msg, type), index + 1, {
source: answerData.data.source,
cost: answerData.data.cost,
num: answerData.data.num,
});
} else {
failedCount += 1;
question._fillFailed = true;
ppLogAnswerFillFailed((msg, type) => this.addLog(msg, type), index + 1, "答案与页面选项对不上");
}
if (answerData.data.cost !== undefined) {
progressStore.update({ remainingCount: answerData.data.num });
}
} else if (isReviewFailed(answerData)) {
failedCount += 1;
question._reviewFailed = true;
question.source = "error";
question.answer = reviewFailedAnswers(answerData);
ppLogAnswerReviewFailed((msg, type) => this.addLog(msg, type), answerData);
} else {
ppLogAnswerQueryFailed((msg, type) => this.addLog(msg, type), index + 1, answerData.msg);
if (answerData.data?.sponsorUrl) {
this.addLog(`${getSponsorLink(answerData.data.sponsorUrl, '点我赞助获取新token')},可继续使用答题`, 'warning');
}
question.source = "error";
question.answer[0] = answerData.msg;
}
this.addQuestion(question);
}
this.skippedCount = skippedCount;
this.failedCount = failedCount;
this.fillRate = computeFillRate(this.correctNum, answerTotal);
ppLogAnswerSummary((msg, type) => this.addLog(msg, type), {
handled: aborted ? this.correctNum + skippedCount + failedCount : answerTotal,
filled: this.correctNum,
skipped: skippedCount,
failed: failedCount,
total: answerTotal,
});
progressStore.update({
filledCount: this.correctNum,
fillRate: this.fillRate
});
if (aborted) {
progressStore.update({
isPlaying: false,
detail: `答题已中断(${this.correctNum} / ${answerTotal})`
});
} else {
progressStore.update({
questionIndex: answerTotal,
percent: 100,
isPlaying: false,
detail: `已完成 ${answerTotal} 题`
});
}
} else
this.addLog("未解析到题目,请进入正确页面", "danger");
return Promise.resolve(this.questions.length > 0 ? this.correctNum / this.questions.length * 100 : 0);
});
__publicField(this, "parseHtml", () => {
if (!this._document)
return [];
if (["zj"].includes(this.type)) {
const questionElements = this._document.querySelectorAll(SELECTORS.CX_QUESTION_ZJ);
this.addQuestions(questionElements);
} else if (["zy", "ks"].includes(this.type)) {
const questionElements = this._document.querySelectorAll(SELECTORS.CX_QUESTION_ZY_KS);
this.addQuestions(questionElements);
}
});
__publicField(this, "isBlankFilled", (blankEl) => {
if (!blankEl) return false;
try {
const cmHost = blankEl.querySelector(".CodeMirror");
const cm = cmHost && cmHost.CodeMirror;
if (cm && typeof cm.getValue === "function" && String(cm.getValue() || "").trim() !== "") return true;
} catch (e) {}
try {
const iframe = blankEl.querySelector(".textDIV iframe") || blankEl.querySelector("iframe");
if (iframe) {
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
const body = doc && doc.body;
if (body && String(body.textContent || "").trim() !== "") return true;
}
} catch (e) {}
const textarea = blankEl.querySelector("textarea");
if (textarea && String(textarea.value || "").trim() !== "") return true;
const inpDiv = blankEl.querySelector(".InpDIV");
if (inpDiv && String(inpDiv.textContent || "").trim() !== "") return true;
return false;
});
__publicField(this, "fillReadingBlankFallback", async (question, index, answerText) => {
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const text = String(answerText == null ? "" : answerText).trim();
if (!text || !question || !question.element) return 0;
let subEl = null;
try {
const subEls = question.element.querySelectorAll(".readCompreHensionItem");
subEl = subEls[index] || null;
} catch (e) {}
if (!subEl) return 0;
const blanks = Array.from(subEl.querySelectorAll(".blankItemDiv"));
const parts = text
.split(/###|\|\||[;;\n]+/)
.map((s) => s.trim())
.filter(Boolean);
const escapeHtml = (s) =>
String(s).replace(/&/g, "&").replace(//g, ">");
const writeBlank = async (blankEl, value) => {
if (!blankEl || !value) return 0;
const inpDiv = blankEl.querySelector(".InpDIV");
if (inpDiv) {
try {
inpDiv.click();
} catch (e) {}
await sleep(120);
}
let wrote = 0;
const UE = this._window && this._window.UE;
const textarea = blankEl.querySelector("textarea");
const editor =
UE && textarea && typeof UE.getEditor === "function"
? UE.getEditor(textarea.id || textarea.name)
: null;
if (editor && typeof editor.setContent === "function") {
try {
if (editor.body === null && typeof editor.addListener === "function") {
await new Promise((resolve) => {
editor.addListener("ready", () => resolve());
setTimeout(resolve, 1500);
});
}
editor.setContent(escapeHtml(value));
if (typeof editor.fireEvent === "function") editor.fireEvent("contentChange");
wrote += 1;
} catch (e) {}
}
if (!wrote) {
const cmHost = blankEl.querySelector(".CodeMirror");
const cm = cmHost && cmHost.CodeMirror;
if (cm && typeof cm.setValue === "function") {
try {
cm.setValue(String(value));
if (typeof cm.focus === "function") cm.focus();
wrote += 1;
} catch (e) {}
} else {
const codeEditor = blankEl.querySelector("textarea.code-editor");
if (codeEditor) {
try {
codeEditor.value = String(value);
codeEditor.dispatchEvent(new Event("input", { bubbles: true }));
wrote += 1;
} catch (e) {}
}
}
}
if (!wrote) {
const holder = blankEl.querySelector(".textDIV") || blankEl;
const iframe = holder.querySelector("iframe") || blankEl.querySelector("iframe#ueditor_0");
if (iframe) {
try {
const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
const body = doc && doc.body;
if (body) {
body.innerHTML = escapeHtml(value);
["input", "keyup", "change"].forEach((name) => {
try {
body.dispatchEvent(new Event(name, { bubbles: true }));
} catch (e) {}
});
wrote += 1;
}
} catch (e) {}
}
}
if (textarea) {
try {
textarea.value = value;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
textarea.dispatchEvent(new Event("change", { bubbles: true }));
} catch (e) {}
}
return wrote;
};
if (blanks.length > 0) {
let done = 0;
for (let k = 0; k < blanks.length; k++) {
const value = parts.length <= 1 ? parts[0] || text : parts[k] || "";
if (!value) continue;
done += await writeBlank(blanks[k], value);
await sleep(100);
}
return done;
}
if (subEl.querySelector("textarea")) {
return await writeBlank(subEl, text);
}
return 0;
});
__publicField(this, "fillReadingQuestion", async (question) => {
const subs = Array.isArray(question._reading) ? question._reading : [];
const rawAnswer = question.answer;
const answers = Array.isArray(rawAnswer)
? rawAnswer
: (rawAnswer == null || rawAnswer === "" ? [] : [rawAnswer]);
if (!subs.length || !answers.length) return true;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const norm = (s) =>
String(s || "")
.replace(/\s+/g, "")
.replace(/[,。.!?、;:""''()【】《》.,;:!?()\[\]·—–~~%%]/g, "")
.toLowerCase();
const clickEl = async (el) => {
if (!el) return;
const target = el.querySelector("input") || el;
try {
target.click();
} catch (e) {}
await sleep(150);
};
const indexOfText = (options, text) => {
const want = norm(text);
if (!want) return -1;
const exact = options.findIndex((t) => norm(t) === want);
if (exact >= 0) return exact;
const wantJudge = ppJudgeKey(text);
if (!wantJudge) return -1;
return options.findIndex((t) => ppJudgeKey(t) === wantJudge);
};
let filled = 0;
let answeredSubs = 0;
for (let i = 0; i < subs.length; i++) {
const sub = subs[i];
const answerText = String(answers[i] == null ? "" : answers[i]).trim();
if (!answerText) continue;
const parts = sub.type === "1"
? answerText.split(/###|\|\||[;;\n]+/).map((s) => s.trim()).filter(Boolean)
: [answerText];
const idxs = [];
for (const part of parts) {
const idx = indexOfText(sub.options || [], part);
if (idx >= 0 && !idxs.includes(idx)) idxs.push(idx);
}
if (idxs.length === 0) {
const blankFilled = await this.fillReadingBlankFallback(question, i, answerText);
if (blankFilled > 0) {
filled += blankFilled;
answeredSubs += 1;
await sleep(120);
continue;
}
if (typeof this.addLog === "function") {
this.addLog(
"阅读理解第 " + (i + 1) + " 小题:答案文本没匹配到选项 - " + answerText.slice(0, 40),
"warning"
);
}
continue;
}
const picked = sub.type === "1" ? idxs : idxs.slice(0, 1);
answeredSubs += 1;
for (const idx of picked) {
const el = (sub.elements || [])[idx];
if (!el) continue;
if (this.isOptionSelected(el)) continue;
await clickEl(el);
filled += 1;
}
await sleep(120);
}
return filled > 0;
});
__publicField(this, "fillQuestion", async (question) => {
if (!this._window)
return false;
try {
if (Array.isArray(question._reading) && question._reading.length > 0) {
return (await this.fillReadingQuestion(question)) === true;
}
const rawAnswer = question.answer;
const hasAnswerPayload = Array.isArray(rawAnswer)
? rawAnswer.some((item) => String(item == null ? "" : item).trim() !== "")
: String(rawAnswer == null ? "" : rawAnswer).trim() !== "";
this._fillNoop = false;
if (await this.fillQuestionV2(question)) {
return hasAnswerPayload && this._fillNoop !== true;
}
if (question.type === "4" || question.type === "5" || question.type === "6" || question.type === "7" || question.type === "8") {
const textareaElement = question.element.querySelector("textarea");
if (!textareaElement) return false;
const answerText = Array.isArray(question.answer)
? question.answer.join("\n")
: String(question.answer);
const htmlContent = answerText
.split("\n")
.map((line) => "" + line + "
")
.join("");
const ue = this._window.UE;
const ueditor =
ue && typeof ue.getEditor === "function"
? ue.getEditor(textareaElement.id || textareaElement.name)
: null;
if (!ueditor || typeof ueditor.setContent !== "function") {
this.addLog("简答题未取到编辑器实例,已跳过", "warning");
return false;
}
if (ueditor.body === null) {
await new Promise((resolve) => {
ueditor.addListener("ready", () => resolve());
setTimeout(resolve, 2000);
});
}
ueditor.setContent(htmlContent);
ueditor.fireEvent("contentChange");
if (typeof ueditor.sync === "function") ueditor.sync();
const saveBtn = question.element.querySelector('[onclick*="saveQuestion"]');
if (saveBtn) saveBtn.click();
return hasAnswerPayload;
}
if (question.type === "17") {
const parts = Array.isArray(question.answer)
? question.answer.map((item) => String(item == null ? "" : item))
: [String(question.answer == null ? "" : question.answer)];
const hasLang = parts.length >= 2;
const langName = hasLang ? parts[0].trim() : "";
const answerText = (hasLang ? parts.slice(1).join("\n") : parts[0]).replace(/^(?:\r?\n)+|(?:\r?\n)+$/g, "");
if (!answerText.trim()) return false;
if (langName) {
try {
const items = Array.from(question.element.querySelectorAll("[codename]"));
const langItem = items.find((el) => String(el.getAttribute("codename") || "").trim() === langName);
if (langItem) langItem.click();
const langSelect = question.element.querySelector('input[id^="languageSelect"]');
const langNum = langItem ? langItem.getAttribute("codenum") : "";
if (langSelect && langNum) langSelect.value = langNum;
const visible = question.element.querySelector("strong[codename]");
if (visible) {
visible.textContent = langName;
visible.setAttribute("codename", langName);
if (langNum) visible.setAttribute("codenum", langNum);
}
} catch (e) {}
}
const cmHost = question.element.querySelector(".CodeMirror");
const cm = cmHost && cmHost.CodeMirror;
if (cm && typeof cm.setValue === "function") {
try {
cm.setValue(answerText);
if (typeof cm.save === "function") cm.save();
if (typeof cm.focus === "function") cm.focus();
const saveBtn = question.element.querySelector('[onclick*="saveQuestion"]');
if (saveBtn) saveBtn.click();
return true;
} catch (e) {}
}
const codeEditor = question.element.querySelector("textarea.code-editor");
if (codeEditor) {
try {
codeEditor.value = answerText;
codeEditor.dispatchEvent(new Event("input", { bubbles: true }));
const saveBtn = question.element.querySelector('[onclick*="saveQuestion"]');
if (saveBtn) saveBtn.click();
return true;
} catch (e) {}
}
return false;
}
if (question.type === "26") {
const answerText = Array.isArray(question.answer)
? question.answer.map((item) => String(item == null ? "" : item)).join("\n")
: String(question.answer == null ? "" : question.answer);
if (!answerText.trim()) return false;
const textareaElement = question.element.querySelector("textarea");
const UE = this._window && this._window.UE;
const ueditor =
UE && textareaElement && typeof UE.getEditor === "function"
? UE.getEditor(textareaElement.id || textareaElement.name)
: null;
if (ueditor && typeof ueditor.setContent === "function") {
try {
if (ueditor.body === null && typeof ueditor.addListener === "function") {
await new Promise((resolve) => {
ueditor.addListener("ready", () => resolve());
setTimeout(resolve, 2000);
});
}
ueditor.setContent(answerText);
if (typeof ueditor.fireEvent === "function") ueditor.fireEvent("contentChange");
if (typeof ueditor.sync === "function") ueditor.sync();
const saveBtn = question.element.querySelector('[onclick*="saveQuestion"]');
if (saveBtn) saveBtn.click();
return true;
} catch (e) {}
}
const ueFrame = question.element.querySelector("iframe#ueditor_0");
if (ueFrame) {
try {
const doc = ueFrame.contentDocument || (ueFrame.contentWindow && ueFrame.contentWindow.document);
const body = doc && doc.body;
if (body) {
body.innerHTML = String(answerText)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/\n/g, "
");
["input", "keyup", "change"].forEach((name) => {
try {
body.dispatchEvent(new Event(name, { bubbles: true }));
} catch (e) {}
});
const saveBtn = question.element.querySelector('[onclick*="saveQuestion"]');
if (saveBtn) saveBtn.click();
return true;
}
} catch (e) {}
}
return false;
}
return false;
} catch (error) {
this.addLog(`答题过程发生错误:${error.message}`, "danger");
return false;
} finally {
this.isFilling = false;
}
});
this.type = type;
if (iframe) {
this._document = iframe.contentDocument;
this._window = iframe.contentWindow;
decodeCipherFontSafe(this._document);
} else {
decodeCipherFontSafe(this._document);
}
}
isOptionSelected(optionElement) {
if (["zj", "zy"].includes(this.type)) return ppOptionLooksSelected(optionElement);
if (["ks"].includes(this.type)) {
return !!(optionElement.querySelector(".check_answer") || optionElement.querySelector(".check_answer_dx"));
}
return false;
}
async fillQuestionV2(question) {
let type = String(question.type);
const isZhs = String(question.workType) === "zhs";
const supportedTypes = isZhs
? ["0", "1", "2", "3"]
: ["0", "1", "2", "3", "8", "9", "10", "11", "13", "14", "17", "20", "24", "26"];
if (!supportedTypes.includes(type)) {
if (type === "999" || !type || type === "undefined" || type === "null") {
type = getOptionList(question).length > 0 ? "0" : "2";
} else {
return false;
}
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const configStore = useConfigStore();
const answerParams = configStore.platformParams[configStore.platformName]?.parts.find((p) => p.name === PP_KEY.ANSWER)?.params || [];
const useSimilarity = answerParams.find((p) => p.name === PP_KEY.SIMILAR)?.value !== false;
const thresholdRaw = Number(answerParams.find((p) => p.name === PP_KEY.THRESHOLD)?.value);
const threshold = Math.min(99, Math.max(50, Number.isFinite(thresholdRaw) && thresholdRaw > 0 ? thresholdRaw : 85));
const optionList = getOptionList(question);
const answerList = Array.isArray(question.answer)
? question.answer.map((a) => String(a == null ? "" : a).trim())
: [];
const keepEmptySlots = !isZhs && ["2", "9", "10", "14", "15", "19", "24"].includes(type);
const flatAnswers = keepEmptySlots ? answerList : answerList.filter(Boolean);
const rawAnswers = flatAnswers;
if (flatAnswers.length === 0) {
this._fillNoop = true;
return true;
}
const clickOption = async (el) => {
if (!el) return;
el.setAttribute("data-filling", "true");
el.click();
await sleep(200);
el.removeAttribute("data-filling");
};
if (type === "24" && question._fillBlanks && Array.isArray(question._fillBlanks.blanks)) {
const info = question._fillBlanks;
const normWord = (s) =>
String(s == null ? "" : s).replace(/\s+/g, "").replace(/[“”"']/g, "").toLowerCase();
let done = 0;
for (let i = 0; i < info.blanks.length && i < flatAnswers.length; i++) {
const blank = info.blanks[i];
const ans = String(flatAnswers[i] == null ? "" : flatAnswers[i]).trim();
if (!ans || !blank) continue;
const word = info.words.find((w) => normWord(w.text) === normWord(ans));
if (!word) continue;
blank.innerHTML = word.text;
blank.classList.add("hasFill");
blank.setAttribute("data-choose-name", word.code);
blank.draggable = true;
done += 1;
}
try {
const fn = this._window && this._window.getFillinBlanksDataForQuestion;
if (typeof fn === "function") fn.call(this._window, info.qid);
} catch (e) {}
return done > 0;
}
if (type === "20" && Array.isArray(question._shared) && question._shared.length > 0) {
let done = 0;
for (let i = 0; i < question._shared.length && i < flatAnswers.length; i++) {
const sub = question._shared[i];
const letter = String(flatAnswers[i] == null ? "" : flatAnswers[i])
.replace(/[.、::\s]/g, "")
.toUpperCase()
.charAt(0);
if (!letter) continue;
const target = (sub.elements || []).find((el) => {
return String(el.getAttribute("data") || "").toUpperCase() === letter;
});
if (!target) continue;
await clickOption(target);
done += 1;
}
return done > 0;
}
if (type === "0" || type === "1") {
const targetIndices = [];
for (const ans of flatAnswers) {
const clean = this.stripTags(ans);
if (!clean) continue;
const idx = matchOptionIndex(optionList, clean);
if (idx >= 0 && !targetIndices.includes(idx)) targetIndices.push(idx);
}
if (targetIndices.length === 0) {
this._fillNoop = true;
return true;
}
if (type === "1") {
for (let i = 0; i < optionList.length; i++) {
const el = optionList[i].el;
if (!el || targetIndices.includes(i)) continue;
if (this.isOptionSelected(el)) {
await clickOption(el);
}
}
await sleep(300);
}
for (const idx of targetIndices) {
const el = optionList[idx].el;
if (!el) continue;
if (this.isOptionSelected(el)) continue;
await clickOption(el);
}
return true;
}
if (type === "3") {
const rawAnswer = String(flatAnswers[0] || "").trim();
let truth = null;
if (REGEX.JUDGE_FALSE.test(rawAnswer)) truth = false;
else if (REGEX.JUDGE_TRUE.test(rawAnswer)) truth = true;
if (truth === null && rawAnswer) {
const idx = matchOptionIndex(optionList, rawAnswer);
const el = idx >= 0 ? optionList[idx].el : null;
if (el) {
if (!this.isOptionSelected(el)) await clickOption(el);
return true;
}
}
if (truth === null) {
return true;
}
let target = null;
for (const opt of optionList) {
const el = opt.el;
if (!el) continue;
if (["zj", "zy"].includes(this.type)) {
const aria = el.getAttribute("aria-label") || "";
const text = opt.text || "";
if (truth) {
if ((aria && (aria.includes("正确") || aria.includes("对"))) || REGEX.JUDGE_TRUE.test(text)) {
target = el;
break;
}
} else if ((aria && aria.includes("错")) || REGEX.JUDGE_FALSE.test(text)) {
target = el;
break;
}
} else {
const span = el.querySelector(`span[data='${truth ? "true" : "false"}']`);
if (span) {
target = span;
break;
}
if (ppJudgeKey(opt.text || el.getAttribute("aria-label") || "") === (truth ? "T" : "F")) {
target = el;
break;
}
}
}
if (!target) {
this._fillNoop = true;
return true;
}
if (target.classList && target.classList.contains("check_answer")) return true;
if (this.isOptionSelected(target)) return true;
const holder = target.closest ? target.closest(".answerBg") : null;
if (holder && (holder.querySelector(".check_answer") || holder.querySelector(".check_answer_dx"))) return true;
await clickOption(target);
return true;
}
if (type === "14" && Array.isArray(question._cloze) && question._cloze.length > 0) {
const normCloze = (s) =>
String(s == null ? "" : s)
.replace(/\s+/g, "")
.replace(/[,。.!?、;:""''()【】《》.,;:!?()\[\]·—–~~%%]/g, "")
.toLowerCase();
let done = 0;
for (let i = 0; i < question._cloze.length && i < flatAnswers.length; i++) {
const cloze = question._cloze[i];
const ans = String(flatAnswers[i] == null ? "" : flatAnswers[i]).trim();
if (!ans) continue;
const target = normCloze(ans);
const pick = cloze.options.findIndex((t) => normCloze(t) === target);
if (pick < 0 || !cloze.elements[pick]) continue;
cloze.elements[pick].click();
done += 1;
await sleep(150);
}
return done > 0;
}
if (type === "2" || type === "10" || type === "14" || type === "9") {
const wordScope =
(type === "14" ? question.element.querySelector(".filling_answer") : null) ||
question.element;
const wordSpans = Array.from(wordScope.querySelectorAll("span.saveSingleSelect[data]"));
const selectBoxes = Array.from(question.element.querySelectorAll("select.dept_select"));
const textareaElements = question.element.querySelectorAll("textarea");
let blankAnswers = flatAnswers;
const blankCount = Math.max(wordSpans.length, selectBoxes.length, textareaElements.length);
const preferTextarea = (type === "2" || type === "10") && textareaElements.length > 0;
if (wordSpans.length > 0 && !preferTextarea) {
const used = new Set();
let filled = 0;
const optionTexts = optionList.map((o) => o.text);
const resolveWord = (raw) => {
const clean = this.stripTags(String(raw == null ? "" : raw));
if (!clean) return "";
if (optionTexts.length > 0 && isPlainLetterAnswer(clean)) {
const idx = clean.replace(/[,,.。.、 #/\\|]/g, "").toUpperCase().charCodeAt(0) - 65;
if (idx >= 0 && idx < optionTexts.length) return optionTexts[idx];
}
return clean;
};
for (const ans of blankAnswers) {
const clean = resolveWord(ans);
if (!clean) continue;
let pick = -1;
for (let i = 0; i < wordSpans.length; i++) {
if (used.has(i)) continue;
if (normalizeAnsText(wordSpans[i].getAttribute("data") || "") === normalizeAnsText(clean)) {
pick = i;
break;
}
}
if (pick < 0 && useSimilarity) {
let bestScore = threshold;
for (let i = 0; i < wordSpans.length; i++) {
if (used.has(i)) continue;
const score = computeMatch(normalizeAnsText(clean), normalizeAnsText(wordSpans[i].getAttribute("data") || ""));
if (score > bestScore) {
bestScore = score;
pick = i;
}
}
}
if (pick >= 0) {
used.add(pick);
wordSpans[pick].click();
filled += 1;
await sleep(150);
}
}
if (filled > 0) {
return true;
}
}
if (selectBoxes.length > 0) {
const count = Math.min(selectBoxes.length, blankAnswers.length);
let filledSelects = 0;
for (let i = 0; i < count; i++) {
const select = selectBoxes[i];
const value = String(blankAnswers[i] || "").trim();
if (!value) continue;
const options = Array.from(select.options || []);
let matchedValue = "";
for (const opt of options) {
if (opt.value === value || normalizeAnsText(opt.text) === normalizeAnsText(value)) {
matchedValue = opt.value;
break;
}
}
if (!matchedValue && useSimilarity) {
let bestScore = threshold;
for (const opt of options) {
const score = computeMatch(normalizeAnsText(value), normalizeAnsText(opt.text));
if (score > bestScore) {
bestScore = score;
matchedValue = opt.value;
}
}
}
if (!matchedValue) continue;
select.value = matchedValue;
select.dispatchEvent(new Event("change", { bubbles: true }));
filledSelects += 1;
await sleep(200);
}
if (filledSelects === 0) this._fillNoop = true;
return true;
}
if (textareaElements.length === 0) {
this._fillNoop = true;
return true;
}
for (let i = 0; i < textareaElements.length; i++) {
const textareaElement = textareaElements[i];
const answerText =
textareaElements.length === 1
? blankAnswers.join(",")
: blankAnswers.length === 1
? blankAnswers[0]
: (blankAnswers[i] == null ? "" : blankAnswers[i]);
try {
const blankItemDiv = textareaElement.closest(".blankItemDiv");
const inpDiv = blankItemDiv ? blankItemDiv.querySelector(".InpDIV") : null;
if (inpDiv) {
inpDiv.click();
await sleep(300);
}
const UE = this._window && this._window.UE;
const ueditor = UE && typeof UE.getEditor === "function" ? UE.getEditor(textareaElement.id || textareaElement.name) : null;
if (ueditor && typeof ueditor.setContent === "function") {
if (ueditor.body === null) {
await new Promise((resolve) => {
ueditor.addListener("ready", () => resolve());
setTimeout(resolve, 2000);
});
}
ueditor.setContent(answerText);
ueditor.fireEvent("contentChange");
} else {
textareaElement.value = answerText;
textareaElement.dispatchEvent(new Event("input", { bubbles: true }));
textareaElement.dispatchEvent(new Event("change", { bubbles: true }));
}
} catch (e) {
textareaElement.value = answerText;
textareaElement.dispatchEvent(new Event("input", { bubbles: true }));
}
}
const saveBtn = question.element.querySelector('[onclick*="saveQuestion"]');
if (saveBtn) {
saveBtn.click();
await sleep(300);
}
return true;
}
if (type === "11") {
const selects = Array.from(question.element.querySelectorAll("select.dept_select"));
if (selects.length === 0) {
const boxes = Array.from(
question.element.querySelectorAll(".line_answer_ct .selectBox, .line_answer .selectBox"),
);
if (boxes.length === 0) return false;
const flat = [];
for (const a of rawAnswers) { if (a) flat.push(a); }
let clicked = 0;
for (let i = 0; i < boxes.length && i < flat.length; i++) {
const value = String(flat[i] || "").trim();
if (!value) continue;
const letter = isPlainLetterAnswer(value) ? value.toUpperCase().charAt(0) : "";
if (!letter) continue;
const anchor =
boxes[i].querySelector(`li[data="${letter}"] a`) || boxes[i].querySelector(`li[data='${letter}'] a`);
if (!anchor) continue;
anchor.click();
clicked += 1;
await sleep(150);
}
const hidden = question.element.querySelector('.line_answer input[name^="answer"]');
if (hidden && flat.length > 0) hidden.value = flat.join("");
return clicked > 0;
}
const flat = [];
for (const a of rawAnswers) { if (a) flat.push(a); }
if (flat.length === 0) {
this._fillNoop = true;
return true;
}
let filled = 0;
for (let i = 0; i < selects.length && i < flat.length; i++) {
const select = selects[i];
const value = String(flat[i] || "").trim();
if (!value) continue;
const options = Array.from(select.options || []);
let target = "";
if (isPlainLetterAnswer(value)) {
const letter = value.replace(/[,,.。.、 #/\\|]/g, "").toUpperCase().charAt(0);
const byValue = options.find((o) => String(o.value).toUpperCase() === letter);
const byText = options.find((o) => String(o.text || "").trim().toUpperCase() === letter);
const byIndex = options[letter.charCodeAt(0) - 65];
target = byValue ? byValue.value : byText ? byText.value : byIndex ? byIndex.value : "";
}
if (!target) {
const norm = normalizeAnsText(value);
const hit = options.find((o) => normalizeAnsText(o.text) === norm);
if (hit) target = hit.value;
}
if (!target && useSimilarity) {
let bestScore = threshold;
for (const o of options) {
const score = computeMatch(normalizeAnsText(value), normalizeAnsText(o.text));
if (score > bestScore) {
bestScore = score;
target = o.value;
}
}
}
if (!target) continue;
select.value = target;
select.dispatchEvent(new Event("change", { bubbles: true }));
this.syncChosenSelect(select, target);
filled += 1;
await sleep(200);
}
if (filled === 0) this._fillNoop = true;
return true;
}
if (type === "13") {
const sortSelects = Array.from(question.element.querySelectorAll(".sortQuesSelect .dept_select"));
if (sortSelects.length === 0) return false;
const seq = [];
for (const a of rawAnswers) {
const list = [String(a == null ? "" : a).trim()];
for (const item of list) {
if (!item) continue;
if (isPlainLetterAnswer(item)) {
item.replace(/[,,.。.、 #/\\|]/g, "").toUpperCase().split("").forEach((ch) => seq.push(ch));
} else if (/^[A-Za-z]{2,8}$/.test(item)) {
item.toUpperCase().split("").forEach((ch) => seq.push(ch));
} else {
const idx = matchOptionIndex(optionList, item);
if (idx >= 0 && idx < 26) seq.push(String.fromCharCode(65 + idx));
}
}
}
if (seq.length === 0) {
this._fillNoop = true;
return true;
}
let filled = 0;
for (let i = 0; i < sortSelects.length && i < seq.length; i++) {
const select = sortSelects[i];
const letter = seq[i];
if (!letter) continue;
const options = Array.from(select.options || []);
let pick = options.find((o) => String(o.value).toUpperCase() === letter)
|| options.find((o) => String(o.text || "").trim().toUpperCase() === letter);
if (!pick) {
const num = String(letter.charCodeAt(0) - 64);
pick = options.find((o) => String(o.value) === num);
}
select.value = pick ? pick.value : letter;
select.dispatchEvent(new Event("change", { bubbles: true }));
this.syncChosenSelect(select, select.value);
filled += 1;
}
const answerInput = question.element.querySelector('input[name^="answer"]');
if (answerInput) answerInput.value = seq.join("");
return true;
}
return false;
}
syncChosenSelect(select, value) {
try {
const container = select.nextElementSibling;
if (!container || !container.classList || !container.classList.contains("chosen-container")) return;
const single = container.querySelector(".chosen-single span");
const options = Array.from(select.options || []);
const opt = options.find((o) => o.value === value);
const text = opt ? opt.text : value;
if (single) {
single.textContent = text;
if (single.classList) single.classList.remove("chosen-default");
}
container.querySelectorAll(".chosen-results li").forEach((li) => {
li.classList.remove("result-selected");
if (li.textContent.trim() === text) li.classList.add("result-selected");
});
} catch (e) {}
}
resolveQuestionType(questionElement) {
let input = null;
if (questionElement) {
const named = Array.from(questionElement.querySelectorAll('input[name^="type"]')).find((el) =>
/type\d+/.test(el.getAttribute("name") || ""),
);
input =
questionElement.querySelector('input[id^="answertype"]') || named || questionElement.querySelector('input[name^="type"]');
}
const code = input ? String(input.value || "").trim() : "";
return code || "999";
}
addQuestions(questionElements) {
questionElements.forEach((questionElement) => {
var _a, _b, _c, _d;
let questionTitle = "";
let optionElements;
let optionsObject = {};
let optionTexts = [];
let optionsList = [];
const resolvedType = this.resolveQuestionType(questionElement);
const zyKsSelector = SELECTORS.CX_OPTION_ZY_KS;
const zjSelector = SELECTORS.CX_OPTION_ZJ;
const subItems = Array.from(questionElement.querySelectorAll(".readCompreHensionItem"));
const subTypeCode = String(resolvedType);
if ((subTypeCode === "15" || subTypeCode === "19") && subItems.length > 0) {
const isListening = subTypeCode === "19";
const passageEl = questionElement.querySelector(".fontLabel") || questionElement.querySelector("h3");
const passageText = passageEl
? this.stripTags(this.htmlWithAbsoluteImages(passageEl))
.replace(/【(阅读理解|听力题)】/g, "")
.replace(/([\d.]+分)/g, "")
.replace(/\s+/g, " ")
.trim()
: "";
const subs = subItems.map((subItem) => {
const childTypeEl = subItem.querySelector('input[name="readCompreHension-childType"]');
const subType = childTypeEl ? String(childTypeEl.value || "").trim() || "0" : "0";
const contentEl = subItem.querySelector(".clearfix");
const subTitle = this.stripTags(this.htmlWithAbsoluteImages(contentEl || subItem))
.replace(/([\d.]+分)/g, "")
.replace(/\s+/g, " ")
.trim();
const liList = Array.from(subItem.querySelectorAll("ul li"));
const optionTexts = liList.map((li, j) => {
const spans = li.querySelectorAll("span.fl");
const textEl =
li.querySelector("a.fl.after") ||
li.querySelector("a.after") ||
(spans.length >= 2 ? spans[1] : li);
let text = this.stripTags(this.htmlWithAbsoluteImages(textEl)).replace(/\s+/g, " ").trim();
const letter = String.fromCharCode(65 + j);
text = text.replace(new RegExp("^" + letter + "[.、::\\s]+", "i"), "").trim();
return text;
});
return {
type: subType,
title: subTitle,
options: optionTexts,
elements: liList,
el: subItem,
blanks: Array.from(subItem.querySelectorAll(".blankItemDiv"))
};
});
const subBlocks = subs.map((sub, i) => {
const optionLines = sub.options.map((text, j) => `${String.fromCharCode(65 + j)}. ${text}`);
return `(${i + 1}) ${sub.title}\n${optionLines.join("\n")}`;
});
this.questions.push({
element: questionElement,
type: resolvedType,
title: this.trimTitle(`【${isListening ? "听力题" : "阅读理解"}】\n${passageText}\n\n${subBlocks.join("\n\n")}`),
audios: [],
optionsText: [],
options: {},
optionsList: [],
answer: [],
workType: this.type,
refer: this._window.location.href,
_reading: subs
});
return;
}
if (String(resolvedType) === "14") {
const clozeLis = Array.from(questionElement.querySelectorAll("ul.Cy_ulTop > li"));
const cloze = clozeLis
.map((li) => {
const optEls = Array.from(li.querySelectorAll("ul.Zy_ulTop > li"));
const options = optEls.map((o) => {
const textEl = o.querySelector("div.after") || o.querySelector(".fl.after") || o;
return this.stripTags(this.htmlWithAbsoluteImages(textEl)).replace(/\s+/g, " ").trim();
});
return { options, elements: optEls, el: li, input: li.querySelector('input[id^="answer"]') };
})
.filter((c) => c.elements.length > 0);
if (cloze.length > 0) {
const clozeTitle = this.stripTags(
this.htmlWithAbsoluteImages(questionElement.querySelector(".fontLabel") || questionElement),
)
.replace(/([\d.]+分)/g, "")
.replace(/\s+/g, " ")
.trim();
this.questions.push({
element: questionElement,
type: "14",
title: this.trimTitle(clozeTitle),
audios: [],
optionsText: [],
options: {},
optionsList: [],
answer: [],
workType: this.type,
refer: this._window.location.href,
_cloze: cloze
});
return;
}
}
if (String(resolvedType) === "20") {
const sharedEls = Array.from(questionElement.querySelectorAll("ul.mark_letter > li"));
const subEls = Array.from(questionElement.querySelectorAll("div.B-answer-ct"));
if (subEls.length > 0) {
const sharedOptions = sharedEls
.map((li) => this.stripTags(li.innerHTML).replace(/\s+/g, " ").trim())
.filter(Boolean);
const subs = subEls.map((ct) => {
const titEl = ct.querySelector(".B-tit");
const title = this.stripTags((titEl || ct).innerHTML)
.replace(/^\(\d+\)\s*/, "")
.replace(/^【[^】]*题】\s*/, "")
.replace(/([\d.]+分)/g, "")
.replace(/\s+/g, " ")
.trim();
const els = Array.from(ct.querySelectorAll(".B-answerCon span.num_option"));
return { title, options: [], elements: els };
});
const sharedStem = this.stripTags(
this.htmlWithAbsoluteImages(questionElement.querySelector(".fontLabel") || questionElement),
)
.replace(/([\d.]+分)/g, "")
.replace(/\s+/g, " ")
.trim();
this.questions.push({
element: questionElement,
type: "20",
title: this.trimTitle(sharedStem),
audios: [],
optionsText: sharedOptions,
options: {},
optionsList: [],
answer: [],
workType: this.type,
refer: this._window.location.href,
_shared: subs
});
return;
}
}
if (String(resolvedType) === "24") {
const wrap = questionElement.querySelector(".fillBlanksChoose");
if (wrap) {
const blankEls = Array.from(wrap.querySelectorAll(".textTarget"));
const wordEls = Array.from(wrap.querySelectorAll(".blanksBox span"));
const words = wordEls
.map((w) => {
const raw =
w.childNodes && w.childNodes[0] && typeof w.childNodes[0].nodeValue === "string"
? w.childNodes[0].nodeValue
: w.textContent;
return {
text: String(raw || "").replace(/\s+/g, " ").trim(),
code: String(w.getAttribute("data-choose-name") || ""),
el: w,
};
})
.filter((w) => w.text);
const textEl = wrap.querySelector(".textContent");
let stem = "";
if (textEl) {
const clone = textEl.cloneNode(true);
const doc = textEl.ownerDocument;
clone.querySelectorAll(".textTarget").forEach((el, i) => {
el.replaceWith(doc.createTextNode(" __" + (i + 1) + "__ "));
});
stem = this.stripTags(clone.innerHTML).replace(/\s+/g, " ").trim();
}
const qid = wrap.getAttribute("data") || "";
this.questions.push({
element: questionElement,
type: "24",
title: this.trimTitle(stem),
audios: [],
optionsText: words.map((w) => w.text),
options: {},
optionsList: [],
answer: [],
workType: this.type,
refer: this._window.location.href,
_fillBlanks: { qid, blanks: blankEls, words }
});
return;
}
}
if (["zy", "ks"].includes(this.type)) {
const h3Element = questionElement.querySelector("h3");
const colorShallowElement = questionElement.querySelector(".colorShallow");
const fullText = h3Element ? this.stripTags(this.htmlWithAbsoluteImages(h3Element)) : "";
const typeText = colorShallowElement ? this.stripTags(colorShallowElement.innerHTML) : "";
questionTitle = fullText.replace(typeText, "").replace(/^\d+\.\s*/, "").replace(/_{2,}/g, "____").trim();
optionElements = questionElement.querySelectorAll(zyKsSelector);
[optionsObject, optionTexts, optionsList] = this.extractOptions(optionElements, ".answer_p");
} else if (["zj"].includes(this.type)) {
questionTitle = this.stripTags(this.htmlWithAbsoluteImages(questionElement.querySelector(".fontLabel")))
.replace(/([\d.]+分)/g, "").trim();
const isMatchingQuestion = questionElement.getAttribute("data") === "11" ||
String((questionElement.querySelector('input[id^="answertype"], input[name^="type"]') || {}).value || "") === "11" ||
questionElement.querySelector(".matching") ||
questionElement.querySelector(".connLine");
if (isMatchingQuestion) {
const matchDiv = questionElement.querySelector(".matching") || questionElement.querySelector(".connLine") || questionElement;
const firstListItems = matchDiv.querySelectorAll("ul.firstUlList li:not(.groupTitile)");
const secondListItems = matchDiv.querySelectorAll("ul.secondUlList li:not(.groupTitile)");
const selectElements = matchDiv.querySelectorAll("select.dept_select");
const readMatchSide = (listItems, build) => Array.from(listItems).map((li, idx) => {
const headEl = li.querySelector("i");
const textEl = li.querySelector("div p, div a p");
const text = textEl ? this.stripTags(textEl.innerHTML).trim() : "";
return build(headEl ? headEl.textContent : null, text, idx);
});
const leftItems = readMatchSide(firstListItems, (head, text, idx) =>
`${head === null ? String(idx + 1) : head.replace(/[、,.]/g, "").trim()}. ${text}`
);
const rightItems = readMatchSide(secondListItems, (head, text) => {
const label = head === null ? "" : head.replace(/[、,]/g, "").trim();
return label ? `${label} ${text}` : text;
});
const fullTitle = `${questionTitle} 左侧: ${leftItems.join(";")} 右侧: ${rightItems.join(";")}`;
this.questions.push({
element: questionElement,
type: "11",
title: this.trimTitle(fullTitle),
optionsText: leftItems.map((item, i) => {
const m = String(item).match(/^(\d+)/);
return (m ? m[1] : String(i + 1)) + "-空";
}),
options: {},
optionsList: [],
answer: [],
workType: this.type,
refer: this._window.location.href,
_selectElements: selectElements,
_leftItems: leftItems,
_secondListItems: secondListItems
});
return;
}
optionElements = questionElement.querySelectorAll(zjSelector);
[optionsObject, optionTexts, optionsList] = this.extractOptions(optionElements, ".fl.after");
}
if (String(resolvedType) === "13") {
const sortItems = Array.from(
questionElement.querySelectorAll("ul.sortQuesOption li, ul.ulTop.sortQuesOption li")
)
.map((li) => this.stripTags(li.innerHTML).trim())
.filter(Boolean);
if (sortItems.length > 0) {
questionTitle = `${questionTitle} 选项: ${sortItems.join(";")}`;
}
}
this.questions.push({
element: questionElement,
type: resolvedType,
title: this.trimTitle(questionTitle),
audios: [],
optionsText: optionTexts,
options: optionsObject,
optionsList: optionsList,
answer: [],
workType: this.type,
refer: this._window.location.href
});
});
}
}
const useCxChapterLogic = () => {
const logStore = useLogStore();
const progressStore = useProgressStore();
const getCookieLocal = (name) => {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : '';
};
const cachedUid = getUid();
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();
const cxVideoParam = (name) => {
const parts = configStore.platformParams?.cx?.parts || [];
const part = parts.find(p => p.name === PP_KEY.VIDEO);
return (part?.params || []).find(p => p.name === name) || {};
};
const cxVideoParamValue = (name, fallback = false) => {
const val = cxVideoParam(name).value;
return val === undefined || val === null ? fallback : val;
};
let _justClickedNext = false;
let _currentIframe = null;
let _urlBackupTimer = null;
let _urlWatcherInterval = null;
let _urlBackupWorkerId = null;
const monitorIframes = () => {
const afkEnabledMonitor = readGlobalAfk();
if (_urlBackupTimer) {
clearTimeout(_urlBackupTimer);
_urlBackupTimer = null;
}
if (_urlBackupWorkerId) {
BackgroundWorker.stop(_urlBackupWorkerId);
_urlBackupWorkerId = null;
}
const documentElement = document.documentElement;
const iframe = documentElement.querySelector("iframe");
if (!iframe) {
if (afkEnabledMonitor) {
BackgroundWorker.start('monitor_iframes_retry', () => { BackgroundWorker.stop('monitor_iframes_retry'); monitorIframes(); }, 2000);
} else {
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;
}
if (_urlBackupWorkerId) {
BackgroundWorker.stop(_urlBackupWorkerId);
_urlBackupWorkerId = 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 = readGlobalAfk();
const checkUrlChange = () => {
if (currentUrl !== window.location.href) {
currentUrl = window.location.href;
_justClickedNext = false;
_globalTaskId++;
if (_urlBackupTimer) {
clearTimeout(_urlBackupTimer);
_urlBackupTimer = null;
}
if (afkEnabled && _urlBackupWorkerId) {
BackgroundWorker.stop(_urlBackupWorkerId);
_urlBackupWorkerId = null;
}
if (afkEnabled) {
_urlBackupWorkerId = 'url_backup_' + Date.now();
const workerId = _urlBackupWorkerId;
BackgroundWorker.start(workerId, () => {
_urlBackupWorkerId = null;
BackgroundWorker.stop(workerId);
monitorIframes();
}, 2000);
} else {
_urlBackupTimer = setTimeout(() => {
_urlBackupTimer = null;
monitorIframes();
}, 2000);
}
}
};
if (afkEnabled) {
BackgroundWorker.start('url_watcher', checkUrlChange, 2000);
} else {
_urlWatcherInterval = setInterval(checkUrlChange, 2000);
}
};
let _globalTaskId = 0;
let hasLoggedSkipTip = false;
let _currentWatchSubscription = null;
const _processedIframeTasks = new WeakMap();
const _completedMediaIframes = new WeakSet();
let _lastIframeSrc = null;
window.__getAnswerTaskId__ = () => _globalTaskId;
const watchIframe = async (documentElement) => {
if (_currentWatchSubscription) {
_currentWatchSubscription.unsubscribe();
_currentWatchSubscription = null;
}
forceStopSimulatePlay();
const thisTaskId = ++_globalTaskId;
hasLoggedSkipTip = false;
const afkEnabledWatch = readGlobalAfk();
if (afkEnabledWatch) {
await new Promise(resolve => {
BackgroundWorker.start('watch_iframe_delay', () => { BackgroundWorker.stop('watch_iframe_delay'); resolve(); }, 3000);
});
} else {
await new Promise(resolve => setTimeout(resolve, 3000));
}
if (thisTaskId !== _globalTaskId) {
return;
}
logStore.addLog(`等待页面加载完成,开始扫描任务点`, "info");
FrameScanner.collectDeep(documentElement).subscribe(async (allIframes) => {
stopSimulatePlayIfNeeded();
const classified = [];
for (const iframe of allIframes) {
const type = await classifyFrame(iframe);
if (type !== 'skip') {
classified.push({ iframe, type });
}
}
_currentWatchSubscription = rxjs.from(classified).pipe(rxjs.concatMap(({ iframe, type }) => executeFrame(iframe, type))).subscribe({
complete: async () => {
if (thisTaskId === _globalTaskId) {
const autoSwitch = readCxParamValue(PP_KEY.CHAPTER, PP_KEY.AUTO_NEXT, true);
logStore.addLog("本页任务点已全部完成", "success");
if (!autoSwitch) {
logStore.addLog("自动切换已关闭,请手动切换章节", "warning");
}
if (autoSwitch) {
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) {
} else {
_justClickedNext = true;
await delay(3);
if (thisTaskId !== _globalTaskId) {
return;
}
targetBtn.click();
}
}
}
}
});
});
};
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) {
return true;
}
}
}
return false;
} catch (e) {
return false;
}
};
const completedSimulatedIds = new Set();
const processingSimulatedId = { current: null };
window.clearOverlayAndBanner = () => {
if (window._blockPlayInterval) {
if (typeof window._blockPlayInterval === 'string') {
BackgroundWorker.stop(window._blockPlayInterval);
} else {
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;
}
};
window.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 []; }
};
let _banner = null;
const getOrCreateBanner = () => {
if (_banner && _banner.parentNode) return _banner;
_banner = document.getElementById('_simulate_banner_');
if (!_banner) {
_banner = document.createElement('div');
_banner.id = '_simulate_banner_';
_banner.textContent = '模拟播放中,视频无需真实播放,进度可在脚本主页信息查看';
}
return _banner;
};
const createOverlay = () => {
try {
window._blockOverlay.forEach(o => { try { o.remove(); } catch(e) {} });
window._blockOverlay = [];
const videoIframes = getVideoIframes();
let firstOverlay = null;
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:2147483647;cursor:not-allowed;background:rgba(0,0,0,0.25);border-radius:8px;display:flex;align-items:flex-end;justify-content:center;`;
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);
if (!firstOverlay) firstOverlay = overlay;
}
}
if (firstOverlay) {
const banner = getOrCreateBanner();
banner.style.cssText = 'margin-bottom:20px;background:rgba(255,255,255,0.92);backdrop-filter:blur(10px);color:#4a5568;padding:8px 20px;border-radius:10px;font-size:13px;text-align:center;box-shadow:0 2px 12px rgba(0,0,0,0.1);cursor:default;white-space:nowrap;pointer-events:none;letter-spacing:0.3px;display:flex;align-items:center;gap:8px;z-index:auto;';
if (banner.parentNode !== firstOverlay) {
firstOverlay.appendChild(banner);
}
}
} catch (e) {}
};
const updateOverlayPositions = () => {};
createOverlay();
window._blockPlayScroll = updateOverlayPositions;
window.addEventListener('scroll', updateOverlayPositions, true);
window.addEventListener('resize', updateOverlayPositions);
const afkEnabledOverlay = readGlobalAfk();
if (afkEnabledOverlay) {
BackgroundWorker.start('overlay_refresh', createOverlay, 1000);
window._blockPlayInterval = 'overlay_refresh';
} else {
window._blockPlayInterval = setInterval(createOverlay, 1000);
}
};
const stopSimulatePlay = (reason) => {
if (!window._simulateActive) return;
if (reason) logStore.addLog(reason, 'warning');
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 stopSimulatePlayIfNeeded = () => {
if (window._simulateActive && !hasVideoOrAudio()) {
stopSimulatePlay('检测到视频/音频已消失,停止模拟播放');
}
};
const forceStopSimulatePlay = () => stopSimulatePlay();
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) {
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 behaviorState = { lastMouseMove: 0 };
const simulateUserBehavior = () => {
const now = Date.now();
if (now - behaviorState.lastMouseMove > 300000 + Math.random() * 300000) {
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 BackgroundWorker = (() => {
let worker = null;
const callbacks = new Map();
const workerCode = `
self.onmessage = function(e) {
if (e.data.type === 'start') {
const id = e.data.id;
const interval = e.data.interval || 1000;
let timerId = setInterval(() => {
self.postMessage({ type: 'tick', id: id });
}, interval);
self._timers = self._timers || {};
self._timers[id] = timerId;
self.postMessage({ type: 'started', id: id });
} else if (e.data.type === 'stop') {
const id = e.data.id;
if (self._timers && self._timers[id]) {
clearInterval(self._timers[id]);
delete self._timers[id];
}
self.postMessage({ type: 'stopped', id: id });
}
};
`;
function ensure() {
if (worker) return;
const blob = new Blob([workerCode], { type: 'application/javascript' });
worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = (e) => {
const { type, id } = e.data;
if (type === 'tick' && callbacks.has(id)) {
callbacks.get(id)();
}
};
worker.onerror = (e) => {
};
}
return {
start(id, callback, interval = 1000) {
ensure();
callbacks.set(id, callback);
worker.postMessage({ type: 'start', id, interval });
},
stop(id) {
if (!worker) return;
callbacks.delete(id);
worker.postMessage({ type: 'stop', id });
},
isActive() {
return worker !== null;
},
destroy() {
if (worker) {
worker.terminate();
worker = null;
callbacks.clear();
}
}
};
})();
const FakeMediaPlayer = (() => {
let audioContext = null;
let audioElement = null;
let oscillator = null;
let gainNode = null;
let isPlaying = false;
const start = () => {
if (isPlaying) return;
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
gainNode = audioContext.createGain();
gainNode.gain.value = 0;
oscillator = audioContext.createOscillator();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.start();
isPlaying = true;
} catch (e) {
try {
audioElement = document.createElement('audio');
audioElement.id = '_fake_audio_player_';
audioElement.loop = true;
audioElement.muted = true;
audioElement.volume = 0;
audioElement.src = 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM1LjEwMAAAAAAAAAAAAAAA//tQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAABhgC7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7//////////////////////////////////////////////////////////////////8AAAAATGF2YzU2LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAAYYNbrvGAAAAAAAAAAAAAAAAAAAA//tQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAABhgC7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7//////////////////////////////////////////////////////////////////8AAAAATGF2YzU2LjEzAAAAAAAAAAAAAAAAJAAAAAAAAAAAAYYNbrvGAAAAAAAAAAAAAAAAAAAA';
audioElement.style.display = 'none';
document.body.appendChild(audioElement);
audioElement.play().catch(() => {});
isPlaying = true;
} catch (e2) {
logStore.addLog('欺骗播放启动失败: ' + e2.message, 'warning');
}
}
};
const stop = () => {
if (!isPlaying) return;
try {
if (oscillator) {
oscillator.stop();
oscillator.disconnect();
oscillator = null;
}
if (audioContext) {
audioContext.close();
audioContext = null;
}
if (audioElement) {
audioElement.pause();
audioElement.remove();
audioElement = null;
}
gainNode = null;
isPlaying = false;
logStore.addLog('欺骗播放已停止', 'info');
} catch (e) {
}
};
const isActive = () => isPlaying;
return { start, stop, isActive };
})();
const simulateVideoPlay = (iframe, iframeDocument, mediaType) => {
return new Promise((resolve) => {
(async () => {
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) {
}
try {
let objectId = null;
let jobId = null;
let otherInfo = '';
let reportUrl = '';
let classId = null;
let uid = null;
let duration = null;
let dtoken = null;
let rt = '0.9';
let iframeSrc = iframe.src || '';
let videoName = mediaType === 'video' ? '视频' : '音频';
const reviewMode = readCxParamValue(PP_KEY.MODE, PP_KEY.REVIEW, false);
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(`未从页面标题取到视频名,继续用默认名称`, "info");
}
} 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(`已读取页面数据(含附件信息)`, "info");
break;
}
if (!pageData && val.defaults && val.defaults.reportUrl) {
pageData = val;
logStore.addLog(`已读取页面数据(含上报地址)`, "info");
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 && !reviewMode) 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;
rt = attachment.property?.rt || '0.9';
break;
}
if (!objectId && attachment.job === true) {
objectId = attachment.property?.objectid;
jobId = attachment.jobid;
otherInfo = attachment.otherInfo || '';
videoName = attachment.property?.name || videoName;
rt = attachment.property?.rt || '0.9';
}
}
}
if (!objectId) {
for (const attachment of pageData.attachments) {
if (attachment.isPassed === true && !reviewMode) 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;
rt = attachment.property?.rt || '0.9';
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 = cxVideoParamValue(PP_KEY.AUTO_RATE, false);
let playbackRate = cxVideoParamValue(PP_KEY.RATE, 1);
const maxRate = getMaxPlaybackRate(iframeDocument, false);
const speedDisabled = maxRate === 1;
window.__maxPlaybackRate = maxRate;
const playbackRateParam = cxVideoParam(PP_KEY.RATE);
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 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 = cxVideoParamValue("规避检测", false);
const parseVideoCpi = () => {
const m = window.location.href.match(/[?&]cpi=(\d+)/);
if (m) return m[1];
const om = String(otherInfo || '').match(/cpi_(\d+)/);
if (om) return om[1];
const rm = String(reportUrl || '').match(/\/a\/(\d+)/);
if (rm) return rm[1];
return uid;
};
let videoMid = '';
let videoCourseId = '';
try {
videoCourseId = pageData?.defaults?.courseid || pageData?.defaults?.courseId
|| pageData?.courseid || getTaskParams()?.courseId || '';
const quizAttachments = pageData?.attachments || [];
const quizHit = quizAttachments.find(a => a?.property?.objectid === objectId);
videoMid = (quizHit && quizHit.property && quizHit.property.mid) || '';
} catch (e) {}
const videoQuizRequest = (url) => new Promise((resolveQuiz, rejectQuiz) => {
try {
_GM_xmlhttpRequest({
method: "get",
url: url,
headers: {
'Host': host,
'Referer': iframeSrc,
'Sec-Fetch-Site': 'same-origin',
'User-Agent': navigator.userAgent
},
timeout: 10000,
onload: function(res) {
try {
if (res.status === 200) {
resolveQuiz(JSON.parse(res.responseText));
} else {
rejectQuiz(new Error('HTTP ' + res.status));
}
} catch (e) {
rejectQuiz(e);
}
},
onerror: function() { rejectQuiz(new Error('request error')); },
ontimeout: function() { rejectQuiz(new Error('timeout')); }
});
} catch (e) {
rejectQuiz(e);
}
});
const flattenVideoQuestions = (res) => {
const out = [];
const seen = new WeakSet();
const walk = (node, depth) => {
if (!node || depth > 6) return;
if (Array.isArray(node)) {
node.forEach(n => walk(n, depth + 1));
return;
}
if (typeof node !== 'object') return;
if (seen.has(node)) return;
const hasOptions = Array.isArray(node.options) && node.options.length > 0;
const resourceId = node.resourceId || node.eventid || node.eventId;
if (hasOptions && (resourceId || node.description || node.title)) {
seen.add(node);
out.push(node);
return;
}
if (Array.isArray(node.datas)) {
node.datas.forEach(d => walk(d, depth + 1));
return;
}
Object.keys(node).forEach(k => {
if (k !== 'options') walk(node[k], depth + 1);
});
};
walk(res, 0);
return out;
};
const initVideoQuestions = async (mid, cpi) => {
const dc = Date.now();
const midEnc = encodeURIComponent(mid);
const query = `mid=${midEnc}&cpi=${cpi}&classid=${classId}` +
(videoCourseId ? `&courseid=${videoCourseId}` : '') + `&_dc=${dc}`;
const candidates = [];
const seen = {};
const pushCandidate = (url) => {
if (!url || seen[url]) return;
seen[url] = true;
candidates.push(url);
};
pushCandidate(`${protocol}//${host}/mooc-ans/richvideo/initdatawithviewerV2?${query}`);
const initdataUrl = pageData?.defaults?.initdataUrl || pageData?.initdataUrl;
if (initdataUrl) {
const baseInit = String(initdataUrl).startsWith('http')
? String(initdataUrl) : `${protocol}//${host}${initdataUrl}`;
const v2FromInit = baseInit.replace(/initdatawithviewer$/i, 'initdatawithviewerV2');
if (v2FromInit !== baseInit) pushCandidate(v2FromInit + '?' + query);
pushCandidate(baseInit + '?start=undefined&mid=' + midEnc + '&_dc=' + dc);
pushCandidate(baseInit + '?mid=' + midEnc + '&cpi=' + cpi + '&classid=' + classId + '&_dc=' + dc);
}
pushCandidate(`${protocol}//${host}/richvideo/initdatawithviewer?start=undefined&mid=${midEnc}&_dc=${dc}`);
pushCandidate(`${protocol}//${host}/ananas/initdatawithviewer?mid=${midEnc}&cpi=${cpi}&classid=${classId}&_dc=${dc}`);
for (let i = 0; i < candidates.length; i++) {
try {
const res = await videoQuizRequest(candidates[i]);
const list = flattenVideoQuestions(res);
if (list.length) return list;
} catch (e) {}
}
return [];
};
const pickVideoQuestionAnswer = (q) => {
const options = q.options || [];
const right = options.filter(o => o && (o.isRight === true || o.isRight === 1 ||
o.isRight === '1' || o.isRight === 'true'));
if (right.length) {
return right.map(o => String(o.name || o.content || '').trim()).filter(Boolean).join('');
}
const fromField = q.rightAnswer || q.answer || q.correctAnswer ||
q.rightOption || q.standardAnswer;
if (fromField) {
return String(fromField).replace(/[^A-Ga-g]/g, '').toUpperCase();
}
return '';
};
const submitVideoAnswer = (resourceId, memberinfo, answer) => {
const url = `${protocol}//${host}/question/quiz-validation?classid=${classId}` +
`&cpi=${parseVideoCpi()}&objectid=${objectId}&_dc=${Date.now()}` +
`&eventid=${resourceId}&memberinfo=${memberinfo}&answerContent=${encodeURIComponent(answer)}`;
return videoQuizRequest(url);
};
const handleVideoQuestionsByApi = async () => {
if (mediaType !== 'video') return;
if (!videoMid) {
logStore.addLog('视频内题目:缺少 mid,已跳过', "warning");
return;
}
let questionList = [];
try {
questionList = await initVideoQuestions(videoMid, parseVideoCpi());
} catch (e) {
logStore.addLog(`视频内题目拉取失败:${e.message}`, "warning");
return;
}
if (!questionList.length) {
logStore.addLog('视频内无题目', "primary");
return;
}
logStore.addLog(`解析到 ${questionList.length} 道视频题,开始作答`, "primary");
for (const q of questionList) {
const resourceId = q.resourceId || q.eventid || q.eventId;
const options = q.options || [];
if (!resourceId || !options.length) continue;
const memberinfo = q.memberinfo || q.memberInfo || '';
const description = String(q.description || q.title || '视频内题目')
.replace(/<[^>]+>/g, '').slice(0, 40);
const answers = [];
const addAnswer = (v) => {
const ans = String(v || '').trim();
if (ans && !answers.includes(ans)) answers.push(ans);
};
addAnswer(pickVideoQuestionAnswer(q));
options.forEach(o => addAnswer(o.name || o.content));
let answered = false;
for (const ans of answers) {
try {
const res1 = await submitVideoAnswer(resourceId, memberinfo, ans);
if (res1 && res1.status) {
answered = true;
const ansLabel = res1.isRight ? '正确答案' : '尝试答案';
logStore.addLog(`[视频内题目] ${description} → ${ansLabel}:${ans}`, "success");
break;
}
} catch (e) {}
}
if (!answered) {
logStore.addLog(`[视频内题目] ${description} 提交未通过(已尝试全部选项)`, "warning");
}
await delay(1);
}
logStore.addLog('视频内题目处理完毕', "success");
};
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 isFullDuration = duration > 0 && currentTime >= targetDuration;
const currentIsdrag = (isComplete && isFullDuration) ? '4' : (currentTime > 0 ? '0' : '3');
if (isComplete && !isFullDuration) {
logStore.addLog(`已播放${Math.round(currentTime)}/${Math.round(duration)}秒,未达完成条件,本次按进度上报`, "warning");
}
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=${rt}&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 (mediaType === 'audio') {
rt = '';
}
const rtValue = parseFloat(rt) || 1.0;
let targetDuration = reviewMode ? duration : Math.ceil(duration * rtValue);
let rtAttempted = false;
if (reviewMode) {
logStore.addLog('复习模式:完整重刷,从 0 秒播放至 100%', "primary");
}
if (window._currentMediaInterval) {
clearInterval(window._currentMediaInterval);
window._currentMediaInterval = null;
}
progressStore.update({
taskName: 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 lastTickMs = 0;
let isFirst = true;
let isdrag = '3';
let completeRetryCount = 0;
const maxCompleteRetries = 3;
const reportIntervalSec = 50;
const rtReportMinGapMs = 5000;
const completeRetryGapMs = 4000;
let lastReportWallMs = 0;
let nextCompleteRetryWallMs = 0;
let isReporting = false;
let lastLoggedPercent = 0;
const afkEnabled = readGlobalAfk();
const simulateLoopId = 'simulate_' + Date.now();
let loopInterval = null;
const finishSimulateSuccess = (detailText) => {
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: detailText || '播放完成',
isPlaying: false
});
try {
const mediaElement = iframeDocument?.documentElement?.querySelector(mediaType);
if (mediaElement) {
mediaElement.dispatchEvent(new Event('ended', { bubbles: true }));
}
} catch (e) {
logStore.addLog(`触发ended事件失败: ${e.message}`, "warning");
}
window._currentMediaInterval = null;
safeResolve();
};
const verifyTaskPassedByStatus = async () => {
let fid = '';
try {
fid = _unsafeWindow.FID || getFid() || '';
} catch (e) {
fid = '';
}
const statusUrl = `${protocol}//${host}/ananas/status/${objectId}?k=${fid}&_uid=${uid}&flag=normal&_dc=${Date.now()}`;
return new Promise((resolveStatus) => {
try {
_GM_xmlhttpRequest({
method: "get",
url: statusUrl,
headers: {
'Host': host,
'Referer': iframeSrc,
'Sec-Fetch-Site': 'same-origin'
},
onload: function(res) {
try {
const info = JSON.parse(res.responseText);
if (info && info.isPassed) {
logStore.addLog(`兜底查询确认任务已通过`, "success");
resolveStatus(true);
} else {
logStore.addLog(`兜底查询确认仍未通过(可能受学时或防刷限制)`, "danger");
resolveStatus(false);
}
} catch (e) {
logStore.addLog(`兜底查询解析失败,按未通过处理`, "warning");
resolveStatus(false);
}
},
onerror: function() {
logStore.addLog(`兜底查询请求失败,按未通过处理`, "warning");
resolveStatus(false);
}
});
} catch (e) {
resolveStatus(false);
}
});
};
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 = cxVideoParamValue(PP_KEY.AUTO_RATE, false);
let playbackRateNow = autoMaxRateNow ? maxRate : Math.min(playbackRateParam.value || playbackRate, 3);
let effectiveRate = playbackRateNow;
if (evasionEnabled) {
effectiveRate = getEvasionRate(effectiveRate);
simulateUserBehavior();
}
const nowMs = Date.now();
const elapsedSec = lastTickMs > 0 ? Math.max(0, (nowMs - lastTickMs) / 1000) : 1;
lastTickMs = nowMs;
playsTime += effectiveRate * elapsedSec;
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 = completeRetryCount === 0 || nowMs >= nextCompleteRetryWallMs;
} else if (isFirst) {
shouldReport = true;
} else if (!rtAttempted && targetDuration > 0 && targetDuration < duration
&& playTime >= targetDuration && nowMs - lastReportWallMs >= rtReportMinGapMs) {
shouldReport = true;
} else if (lastReportWallMs > 0 && nowMs - lastReportWallMs >= reportIntervalSec * 1000) {
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';
}
lastReportWallMs = nowMs;
const percent = Math.round(playTime / duration * 100);
const rtBoundaryHit = !rtAttempted && targetDuration > 0 && targetDuration < duration
&& playTime > 0 && playTime < duration && playTime >= targetDuration;
if (!rtBoundaryHit && (percent >= lastLoggedPercent + 20 || percent === 100)) {
logStore.addLog(`上报进度: ${percent}%(${Math.round(playTime)}/${Math.round(duration)}s)`, "info");
lastLoggedPercent = percent;
}
if (rtBoundaryHit) {
rtAttempted = true;
logStore.addLog(`已播放至${percent}%(${Math.round(playTime)}/${Math.round(duration)}s),尝试完成上报`, "info");
const isComplete = await reportProgress(playTime, true);
isReporting = false;
if (isComplete) {
finishSimulateSuccess();
return;
} else {
logStore.addLog(`已播放至${percent}%仍未通过,继续播放至100%`, "warning");
targetDuration = duration;
}
} else {
const isComplete = await reportProgress(playTime, isdrag === '4');
isReporting = false;
if (isComplete && !reviewMode) {
finishSimulateSuccess();
} else if (isdrag === '4') {
if (reviewMode) {
logStore.addLog('复习模式:已完整播放至 100%,按复习结束处理', "success");
finishSimulateSuccess('复习模式已完成');
return;
}
completeRetryCount++;
if (completeRetryCount >= maxCompleteRetries) {
isReporting = true;
logStore.addLog(`完成上报重试${maxCompleteRetries}次仍未通过,正在查询任务点真实状态…`, "warning");
const statusPassed = await verifyTaskPassedByStatus();
if (statusPassed) {
finishSimulateSuccess('兜底确认通过');
return;
}
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 {
nextCompleteRetryWallMs = nowMs + completeRetryGapMs;
logStore.addLog(`完成上报未通过,${Math.round(completeRetryGapMs / 1000)}秒后重试(${completeRetryCount}/${maxCompleteRetries})...`, "warning");
}
}
}
}
};
window._simulateActive = true;
showOverlayAndBanner();
await handleVideoQuestionsByApi();
if (!window._simulateActive) {
logStore.addLog('答题期间模拟播放已被停止,结束处理', "warning");
clearOverlayAndBanner();
releaseLock();
safeResolve();
return;
}
if (afkEnabled) {
window._simulateLoopId = simulateLoopId;
BackgroundWorker.start(simulateLoopId, loopCallback, 1000);
logStore.addLog(`挂机模式已启用,使用后台Worker计时`, "success");
} else {
loopInterval = setInterval(loopCallback, 1000);
window._currentMediaInterval = loopInterval;
}
} catch (e) {
window._currentMediaInterval = null;
window._simulateActive = false;
clearOverlayAndBanner();
logStore.addLog(`模拟播放出错: ${e.message}`, "danger");
logStore.addLog(`回退到普通播放模式`, "warning");
safeResolve(await playMediaDirectly(mediaType, iframeDocument));
}
})();
});
};
const playMediaDirectly = async (mediaType, iframeDocument) => {
return new Promise((resolve) => {
const mediaName = (() => {
try {
const el = (iframeDocument && iframeDocument.querySelector(".prev_title")) || document.querySelector(".prev_title");
const val = el ? String(el.textContent || "").trim().replace(/\s+/g, " ") : "";
return val.slice(0, 40);
} catch (e) {
return "";
}
})();
logStore.addLog(`开始播放:${mediaName || mediaType}`, "primary");
const autoMaxRate = cxVideoParamValue(PP_KEY.AUTO_RATE, false);
const playbackRateParam = cxVideoParam(PP_KEY.RATE);
let playbackRate = playbackRateParam.value || 1;
const maxRate = getMaxPlaybackRate(iframeDocument);
let videoQuizStopped = false;
const videoQuizEnabled = cxVideoParamValue("视频答题", false);
if (videoQuizEnabled) {
const loop = async () => {
if (videoQuizStopped) return;
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)];
answer?.click();
submitBtn?.click();
await delay(3);
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) {
}
if (videoQuizStopped) return;
await delay(3);
loop();
};
loop();
}
const simulatePlayEnabled = cxVideoParamValue(PP_KEY.SIMULATE, 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");
}
}
let isExecuted = false;
const afkEnabledPlay = readGlobalAfk();
const playLoopId = 'play_media_' + Date.now();
const healthCheckId = 'play_health_' + Date.now();
let playLoopTimer = null;
const playLoopCallback = async () => {
const mediaElement = iframeDocument.documentElement.querySelector(mediaType);
if (mediaElement && !isExecuted) {
await mediaElement.pause();
mediaElement.muted = true;
ppMuteOnce(mediaElement);
await mediaElement.play();
mediaElement.playbackRate = playbackRate;
logStore.addLog(`播放成功,倍速:${playbackRate}x`, "primary");
const listener = async () => {
if (afkEnabledPlay) {
await new Promise(resolveDelay => {
BackgroundWorker.start('play_resume_delay', () => { BackgroundWorker.stop('play_resume_delay'); resolveDelay(); }, 3000);
});
} else {
await delay(3);
}
try {
await mediaElement.play();
} catch(e) {}
mediaElement.playbackRate = playbackRate;
};
mediaElement.addEventListener("pause", listener);
mediaElement.addEventListener("ended", () => {
logStore.addLog(`播放完成:${mediaType}`, "success");
mediaElement.removeEventListener("pause", listener);
videoQuizStopped = true;
BackgroundWorker.stop(healthCheckId);
mediaElement.pause();
const blockReplay = (e) => {
e.target.pause();
};
mediaElement.addEventListener("play", blockReplay);
resolve();
});
isExecuted = true;
if (afkEnabledPlay) {
BackgroundWorker.stop(playLoopId);
} else {
clearInterval(playLoopTimer);
playLoopTimer = null;
}
if (afkEnabledPlay) {
BackgroundWorker.start(healthCheckId, () => {
try {
const el = iframeDocument.documentElement.querySelector(mediaType);
if (el && el.paused && !el.ended) {
el.play().catch(() => {});
el.playbackRate = playbackRate;
}
} catch (e) {}
}, 5000);
logStore.addLog('挂机模式视频健康检查已启动', 'success');
}
}
};
if (afkEnabledPlay) {
BackgroundWorker.start(playLoopId, playLoopCallback, 2500);
} else {
playLoopTimer = setInterval(playLoopCallback, 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 = cxVideoParamValue(PP_KEY.SIMULATE, false);
if (useSimulatePlay) {
return simulateVideoPlay(iframe, iframeDocument, mediaType);
} else {
return playMediaDirectly(mediaType, iframeDocument);
}
};
const handleAssignment = async (iframe, iframeDocument, iframeWindow) => {
const taskId = _globalTaskId;
return new Promise((resolve) => {
(async () => {
if (!iframeDocument)
return resolve();
decodeCipherFontSafe(iframeDocument);
const handler = new CxQuestionHandler("zj", iframe);
await handler.init();
if (taskId !== _globalTaskId) {
return resolve();
}
iframeWindow.alert = () => {
};
const canSubmit = typeof iframeWindow.btnBlueSubmit === "function";
const canDraft = typeof iframeWindow.noSubmit === "function";
await runAnswerSubmitPolicy({
addLog: (msg, type) => logStore.addLog(msg, type),
pageLabel: "章节/作业/测验",
platformKey: "cx",
autoSubmit: readCxParamValue(PP_KEY.CHAPTER, PP_KEY.AUTO_SUBMIT, false),
filled: Number(handler.correctNum) || 0,
total: Number(handler.questions?.length) || 0,
skipped: Number(handler.skippedCount) || 0,
canSubmit,
canDraft,
logRate: false,
submit: async () => {
await iframeWindow.btnBlueSubmit();
await delay(1.5);
if (typeof iframeWindow.submitCheckTimes === "function") {
await iframeWindow.submitCheckTimes();
}
logStore.addLog("提交成功", "success");
},
draft: async () => {
await iframeWindow.noSubmit();
},
});
return resolve();
})();
});
};
const handleSlideshow = async (iframeWindow) => {
logStore.addLog("发现课件,开始解析", "primary");
if (typeof iframeWindow.finishJob === "function") {
iframeWindow.finishJob();
await delay(3);
logStore.addLog("课件阅读完成", "success");
return Promise.resolve();
}
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 delay(0.5);
}
await delay(3);
logStore.addLog("课件翻阅完成", "success");
return Promise.resolve();
}
const pptWindow = iframeWindow.document.querySelector("#panView")?.contentWindow;
if (pptWindow) {
await pptWindow.scrollTo({ top: pptWindow.document.body.scrollHeight, behavior: "smooth" });
await delay(3);
}
logStore.addLog("课件阅读完成", "success");
return Promise.resolve();
};
const handleEbook = async (iframeWindow) => {
logStore.addLog("发现电子书,开始阅读", "primary");
_unsafeWindow.top.onchangepage(iframeWindow.getFrameAttr("end"));
logStore.addLog("电子书阅读完成", "success");
return Promise.resolve();
};
const handleBookReader = async (iframeWindow, iframeDoc) => {
logStore.addLog("发现书籍阅读任务,开始阅读", "primary");
try {
const src = iframeWindow.location?.href || '';
const timing = src ? parseInt(new URL(src).searchParams.get('timing') || '60') : 60;
const waitSec = (timing + 3);
logStore.addLog(`书籍阅读中,预计${waitSec * 3}秒...`, "primary");
const jumper = iframeDoc.querySelector('#pagejump');
if (jumper && iframeWindow.reader?.goPage) {
await delay(waitSec);
jumper.value = '5';
jumper.dispatchEvent(new Event('change'));
logStore.addLog("已跳转正文页", "primary");
await delay(waitSec);
jumper.value = '7';
jumper.dispatchEvent(new Event('change'));
logStore.addLog("已跳转封底页", "primary");
await delay(waitSec);
const pagers = Array.from(iframeDoc.querySelectorAll('.readerPager'));
const lastPager = pagers.filter(el => el.style.zIndex === '101')[0];
if (lastPager) lastPager.click();
logStore.addLog("书籍阅读完成", "success");
} else if (iframeWindow.readweb?.goto && typeof iframeWindow.epage !== 'undefined') {
await delay(5);
iframeWindow.readweb.goto(iframeWindow.epage);
logStore.addLog("书籍阅读完成", "success");
} else {
logStore.addLog("无法识别书籍阅读器,请手动完成", "danger");
}
} catch (e) {
logStore.addLog(`书籍阅读出错: ${e.message}`, "danger");
}
return Promise.resolve();
};
const awaitFrameReady = async (iframe) => {
return new Promise((resolve) => {
const afkEnabledFrame = readGlobalAfk();
const frameReadyId = 'frame_ready_' + Date.now();
const checkReady = async () => {
var _a;
if (iframe.contentDocument && ((_a = iframe.contentDocument) == null ? void 0 : _a.readyState) == "complete") {
if (afkEnabledFrame) {
BackgroundWorker.stop(frameReadyId);
}
resolve();
}
};
if (afkEnabledFrame) {
BackgroundWorker.start(frameReadyId, checkReady, 500);
} else {
const intervalId = setInterval(() => {
var _a;
if (iframe.contentDocument && ((_a = iframe.contentDocument) == null ? void 0 : _a.readyState) == "complete") {
clearInterval(intervalId);
resolve();
}
}, 500);
}
});
};
const classifyFrame = async (iframe) => {
const iframeSrc = iframe.src;
const iframeDocument = iframe.contentDocument;
const iframeWindow = iframe.contentWindow;
if (!iframeDocument || !iframeWindow) return 'skip';
if (iframeSrc.includes("javascript:")) return 'skip';
const lastTaskId = _processedIframeTasks.get(iframe);
const currentTaskId = _globalTaskId;
if (lastTaskId === currentTaskId) return 'skip';
_processedIframeTasks.set(iframe, currentTaskId);
await awaitFrameReady(iframe);
const reviewMode = readCxParamValue(PP_KEY.MODE, PP_KEY.REVIEW, false);
if (reviewMode && isCompletedAnswerTaskPoint(iframe)) {
logStore.addLog("复习模式:答题任务点已完成,跳过", "primary");
return 'skip';
}
if (reviewMode && /api\/work|selectWorkQuestion/.test(iframeSrc)) {
try {
const submittedText = iframeDocument.documentElement.innerText || "";
if (submittedText.includes("已完成") || submittedText.includes("待批阅")) {
return 'skip';
}
} catch (e) {}
}
if (!reviewMode) {
let element = iframe.parentElement;
while (element) {
if (element.classList && element.classList.contains("ans-job-finished")) {
logStore.addLog("任务点已完成,跳过", "success");
return 'skip';
}
element = element.parentElement;
}
}
if (!reviewMode && iframeSrc.includes("api/work")) {
try {
const pageContent = iframeDocument.documentElement.innerText || "";
if (pageContent.includes("已完成") || pageContent.includes("待批阅")) return 'skip';
} catch (e) {}
}
if (!reviewMode) {
const checkElement = iframe.closest(".ans-job-icon") || iframe.parentElement?.querySelector(".ans-job-icon");
if (checkElement) {
const ariaLabel = checkElement.getAttribute("aria-label") || "";
if (ariaLabel.includes("已完成")) {
logStore.addLog("任务点已完成,跳过", "success");
return 'skip';
}
}
}
const _src = iframe.getAttribute('_src') || '';
const matchSrc = iframeSrc.includes("api/work");
const matchSrcAttr = _src.includes("api/work");
const isMediaIframe = iframeSrc.includes("video") || iframeSrc.includes("audio");
if (matchSrcAttr && !matchSrc) {
if (!isMediaIframe) {
return 'skip';
}
}
if (matchSrc && !isMediaIframe) {
return 'assignment';
}
const ansJobIcon = iframe.parentElement?.querySelector(".ans-job-icon");
if (ansJobIcon) {
if (isMediaIframe) {
return iframeSrc.includes("video") ? 'video' : 'audio';
}
if (iframeDocument.querySelector("#img.imglook") || iframeDocument.querySelector(".swiper-container")) {
return 'slideshow';
}
if (iframeSrc.includes("modules/innerbook")) {
return 'ebook';
}
if (iframeSrc.includes("/readsvr/book/mooc") || iframeDocument.querySelector("#reader")) {
return 'bookreader';
}
}
return 'skip';
};
const executeFrame = async (iframe, frameType) => {
const iframeSrc = iframe.src;
const iframeDocument = iframe.contentDocument;
const iframeWindow = iframe.contentWindow;
const onlyVideo = readCxParamValue(PP_KEY.MODE, PP_KEY.ONLY_VIDEO, false);
const onlyAnswer = readCxParamValue(PP_KEY.MODE, PP_KEY.ONLY_ANSWER, false);
switch (frameType) {
case 'assignment':
if (onlyVideo) {
if (!hasLoggedSkipTip) {
logStore.addLog("仅视频模式,跳过答题", "primary");
hasLoggedSkipTip = true;
}
return;
}
return handleAssignment(iframe, iframeDocument, iframeWindow);
case 'video':
case 'audio':
if (onlyAnswer) {
if (!hasLoggedSkipTip) {
logStore.addLog("仅答题模式,跳过视频等其他内容", "primary");
hasLoggedSkipTip = true;
}
return;
}
if (_completedMediaIframes.has(iframe)) {
logStore.addLog("媒体任务点已完成,跳过重复处理", "success");
return;
}
await handleMediaContent(frameType, iframeDocument, iframe);
_completedMediaIframes.add(iframe);
return;
case 'slideshow':
if (onlyAnswer) {
if (!hasLoggedSkipTip) {
logStore.addLog("仅答题模式,跳过视频等其他内容", "primary");
hasLoggedSkipTip = true;
}
return;
}
return handleSlideshow(iframeWindow);
case 'ebook':
if (onlyAnswer) {
if (!hasLoggedSkipTip) {
logStore.addLog("仅答题模式,跳过视频等其他内容", "primary");
hasLoggedSkipTip = true;
}
return;
}
return handleEbook(iframeWindow);
case 'bookreader':
if (onlyAnswer) {
if (!hasLoggedSkipTip) {
logStore.addLog("仅答题模式,跳过书籍阅读", "primary");
hasLoggedSkipTip = true;
}
return;
}
return handleBookReader(iframeWindow, iframeDocument);
default:
return;
}
};
init();
monitorIframes();
watchUrlChanges();
const afkEnabledGlobal = readGlobalAfk();
if (afkEnabledGlobal) {
FakeMediaPlayer.start();
logStore.addLog('全局挂机模式已启用', 'success');
}
};
const clickOption = (element) => {
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 }));
};
const isCompletedAnswerTaskPoint = (iframe) => {
if (!iframe) return false;
const src = String(iframe.src || '');
if (!/(?:api\/work|\/work\/|selectWorkQuestion|work\/view)/.test(src)) return false;
if (typeof iframe.closest === 'function' && iframe.closest('.ans-job-finished')) {
return true;
}
const icon =
(typeof iframe.closest === 'function' && iframe.closest('.ans-job-icon')) ||
(iframe.parentElement && iframe.parentElement.querySelector('.ans-job-icon'));
return Boolean(icon && /已完成/.test(icon.getAttribute('aria-label') || ''));
};
const useStuActiveLogic = async () => {
try {
window.parent.postMessage({ source: 'chaoxing-helper-iframe', action: 'closePanel' }, '*');
} catch (e) {}
const logStore = useLogStore();
const questionStore = useQuestionStore();
const configStore = useConfigStore();
const progressStore = useProgressStore();
logStore.addLog(`检测到学习通随堂练习页面`, "primary");
logStore.addLog(`正在解析任务点,请稍等5-10秒(如果长时间没有反应,请刷新页面)`, "warning");
decodeCipherFontSafe(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");
return;
}
const questionTypeMapping = {
"单选题": "0",
"多选题": "1",
"判断题": "3",
"填空题": "2",
"简答题": "4",
"名词解释": "5",
"论述题": "6",
"计算题": "7",
"其它": "8",
"其他": "8",
"分录题": "9",
"资料题": "10"
};
const questions = [];
questionItems.forEach((questionItem) => {
const questionNameEl = questionItem.querySelector(".question-name");
const questionText = questionNameEl ? questionNameEl.innerText.trim() : "";
const labelEl = questionItem.querySelector(".grey-text");
const labelText = labelEl ? labelEl.textContent.replace(/[\[\]\s]/g, "") : "";
let questionTypeText = "";
for (const name of Object.keys(questionTypeMapping)) {
if (labelText && labelText.includes(name)) {
questionTypeText = name;
break;
}
}
if (!questionTypeText) {
if (questionItem.classList.contains("multiple-choice")) {
questionTypeText = "多选题";
} else if (questionText.includes("判断题")) {
questionTypeText = "判断题";
} else if (questionText.includes("填空题")) {
questionTypeText = "填空题";
} else {
questionTypeText = "单选题";
}
}
const cleanTitle = questionText
.replace(/^\d+\.\s*/, "")
.replace(/\[[^\]]{1,10}\]/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
});
});
ppLogAnswerBegin((msg, type) => logStore.addLog(msg, type), { platform: "cx", scene: "随堂练习", total: questions.length });
const answerTotal = questions.length;
progressStore.update({
taskName: "随堂练习",
type: "答题",
isPlaying: true,
questionTotal: answerTotal,
questionIndex: 0,
percent: 0,
currentTime: 0,
totalTime: 0,
detail: `第 0 / ${answerTotal} 题`
});
const answerParamsPart = configStore.platformParams.cx?.parts.find(p => p.name === PP_KEY.ANSWER);
const skipAnswered = answerParamsPart?.params.find(p => p.name === PP_KEY.SKIP_ANSWERED)?.value || false;
const answerInterval = answerParamsPart?.params.find(p => p.name === PP_KEY.ANSWER_GAP)?.value || 1;
const useSimilarity = answerParamsPart?.params.find(p => p.name === PP_KEY.SIMILAR)?.value || false;
const simulateDelay = answerParamsPart?.params.find(p => p.name === PP_KEY.SIM_DELAY)?.value ?? true;
let skippedCount = 0;
let failedCount = 0;
let filledCount = 0;
let aborted = false;
for (const [index, question] of questions.entries()) {
progressStore.update({
questionIndex: index + 1,
percent: Math.round(index / answerTotal * 100),
detail: `第 ${index + 1} / ${answerTotal} 题`
});
const isAnswered = Array.from(question.element.querySelectorAll(".option-list li")).some(li => li.classList.contains("active"));
ppLogAnswerQuestion((msg, type) => logStore.addLog(msg, type), index + 1, question.title);
if (skipAnswered && isAnswered) {
ppLogAnswerSkipped((msg, type) => logStore.addLog(msg, type), index + 1);
skippedCount += 1;
question.skipped = true;
questionStore.addQuestion(question);
continue;
}
const answerData = await queryAnswer(question);
if (answerData.code === 499) {
aborted = true;
break;
}
if (answerData.code === 200) {
question.answer = answerData.data.answer;
question.source = answerData.data.source;
question.sourceName = answerData.data.sourceName || "";
let filledOk = false;
if (question.type === "0" || question.type === "1" || question.type === "3") {
const optionList = getOptionList(question);
const targetEls = [];
const answerList = Array.isArray(question.answer) ? question.answer : [];
for (const answer of answerList) {
const cleanAnswer = String(answer == null ? "" : answer).replace(/<[^>]*>/g, "").trim();
if (!cleanAnswer) continue;
if (question.type === "3") {
const isTrueAnswer = REGEX.JUDGE_TRUE.test(cleanAnswer);
const isFalseAnswer = REGEX.JUDGE_FALSE.test(cleanAnswer);
if (!isTrueAnswer && !isFalseAnswer) continue;
for (const opt of optionList) {
const isTrueOption = REGEX.JUDGE_TRUE.test(opt.text || "");
const isFalseOption = REGEX.JUDGE_FALSE.test(opt.text || "");
if ((isTrueAnswer && isTrueOption) || (isFalseAnswer && isFalseOption)) {
if (opt.el && !targetEls.includes(opt.el)) targetEls.push(opt.el);
break;
}
}
} else {
const idx = matchOptionIndex(optionList, cleanAnswer);
if (idx >= 0) {
const el = optionList[idx] && optionList[idx].el;
if (el && !targetEls.includes(el)) targetEls.push(el);
}
}
}
for (const el of targetEls) {
clickOption(el);
await randomDelay(0.1, 0.15);
}
filledOk = targetEls.length > 0;
}
if (["2", "9", "10"].includes(String(question.type))) {
const plainAnswers = Array.isArray(question.answer)
? question.answer.map((a) => String(a == null ? "" : a).trim())
: [];
const boxInputs = Array.from(question.element.querySelectorAll(".option-fill-list input"))
.filter((el) => !el.getAttribute("type") || el.getAttribute("type") === "text");
const textInputs = boxInputs.length
? boxInputs
: Array.from(question.element.querySelectorAll('input[type="text"], input:not([type])'));
let filledInputs = 0;
for (let i = 0; i < textInputs.length && i < plainAnswers.length; i++) {
const el = textInputs[i];
const value = plainAnswers[i];
if (!el || !value) continue;
el.focus();
el.value = value;
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
filledInputs += 1;
await randomDelay(0.05, 0.1);
}
if (filledInputs > 0) filledOk = true;
}
if (["4", "5", "6", "7"].includes(String(question.type))) {
const plainAnswers = Array.isArray(question.answer)
? question.answer.map((a) => String(a == null ? "" : a).trim()).filter(Boolean)
: [];
const area =
question.element.querySelector(".short-answer-textarea textarea") ||
question.element.querySelector("textarea");
if (area && plainAnswers.length) {
area.focus();
area.value = plainAnswers.join("\n");
area.dispatchEvent(new Event("input", { bubbles: true }));
area.dispatchEvent(new Event("change", { bubbles: true }));
filledOk = true;
}
}
if (filledOk) {
filledCount += 1;
progressStore.update({
filledCount,
fillRate: computeFillRate(filledCount, answerTotal)
});
ppLogAnswerFilled((msg, type) => logStore.addLog(msg, type), index + 1, {
source: answerData.data.source,
cost: answerData.data.cost,
num: answerData.data.num,
});
} else {
failedCount += 1;
question._fillFailed = true;
ppLogAnswerFillFailed((msg, type) => logStore.addLog(msg, type), index + 1, "答案与页面选项对不上");
}
if (answerData.data.cost !== undefined) {
progressStore.update({ remainingCount: answerData.data.num });
}
} else if (isReviewFailed(answerData)) {
failedCount += 1;
question._reviewFailed = true;
question.source = "error";
question.answer = reviewFailedAnswers(answerData);
ppLogAnswerReviewFailed((msg, type) => logStore.addLog(msg, type), answerData);
} else {
if (answerData.code === 403 && answerData.data && answerData.data.limitedMode) {
ppLogAnswerQueryFailed((msg, type) => logStore.addLog(msg, type), index + 1, answerData.msg);
if (answerData.data.sponsorUrl) {
logStore.addLog(`${getSponsorLink(answerData.data.sponsorUrl, '点我赞助获取新token')},可继续使用答题`, 'warning');
}
question.source = "error";
question.answer[0] = answerData.msg;
} else if (answerData.code === 429 && answerData.data && answerData.data.limitedMode) {
ppLogAnswerQueryFailed((msg, type) => logStore.addLog(msg, type), index + 1, "今日免费查题已达上限");
logStore.addLog(`${getSponsorLink(answerData.data.sponsorUrl, '点我赞助获取新token')},可继续使用答题`, 'warning');
question.source = "error";
question.answer[0] = answerData.msg;
} else if (answerData.code === 403 && answerData.data && answerData.data.sponsorUrl) {
const sponsorUrl = answerData.data.sponsorUrl;
const sponsorLink = `点我赞助获取新token`;
const msgHtml = answerData.msg.includes('[可切换赞助获取token,不限制账户]')
? answerData.msg.replace('[可切换赞助获取token,不限制账户]',
`[可切换赞助获取token,不限制账户]`)
: `${answerData.msg},${sponsorLink}`;
ppLogAnswerQueryFailed((msg, type) => logStore.addLog(msg, type), index + 1, msgHtml);
question.source = "error";
question.answer[0] = (answerData.data.answer && answerData.data.answer[0]) || answerData.msg;
} else {
ppLogAnswerQueryFailed((msg, type) => logStore.addLog(msg, type), index + 1, answerData.msg);
if (answerData.data?.sponsorUrl) {
logStore.addLog(`${getSponsorLink(answerData.data.sponsorUrl, '点我赞助获取新token')},可继续使用答题`, 'warning');
}
question.source = "error";
question.answer[0] = answerData.msg;
}
}
questionStore.addQuestion(question);
await (simulateDelay ? randomDelay(Number(answerInterval), 0.5) : delay(Number(answerInterval)));
}
ppLogAnswerSummary((msg, type) => logStore.addLog(msg, type), {
handled: aborted ? filledCount + skippedCount + failedCount : answerTotal,
filled: filledCount,
skipped: skippedCount,
failed: failedCount,
total: answerTotal,
});
if (aborted) {
progressStore.update({
isPlaying: false,
detail: "答题已中断"
});
} else {
progressStore.update({
questionIndex: answerTotal,
percent: 100,
isPlaying: false,
detail: `已完成 ${answerTotal} 题`
});
}
progressStore.update({
filledCount,
fillRate: computeFillRate(filledCount, answerTotal)
});
const hasSubmitBtn = Boolean(document.querySelector(".submit-btn"));
await runAnswerSubmitPolicy({
addLog: (msg, type) => logStore.addLog(msg, type),
pageLabel: "随堂练习",
platformKey: "cx",
autoSubmit: readCxParamValue(PP_KEY.CHAPTER, PP_KEY.AUTO_SUBMIT, false),
filled: filledCount,
total: answerTotal,
skipped: skippedCount,
canSubmit: hasSubmitBtn,
canDraft: false,
logRate: false,
submit: async () => {
logStore.addLog("自动提交已开启,3秒后提交...", "warning");
await delay(3);
const submitBtn = document.querySelector(".submit-btn");
if (!submitBtn) {
logStore.addLog("未找到提交按钮,请手动提交", "danger");
return;
}
clickOption(submitBtn);
logStore.addLog("已点击提交", "success");
for (let i = 0; i < 15; i++) {
const affirm = document.querySelector(".dialog_affrim");
if (affirm) {
clickOption(affirm);
logStore.addLog("已确认提交", "primary");
break;
}
await delay(0.2);
}
},
});
};
const useCxWorkLogic = async () => {
const logStore = useLogStore();
useConfigStore();
logStore.addLog(`检测到学习通作业页面`, "primary");
logStore.addLog(`正在解析任务点,请稍等5-10秒(如果长时间没有反应,请刷新页面)`, "warning");
await new CxQuestionHandler("zy").init();
logStore.addLog("当前页面不支持自动提交:请检查已填入的答案后手动保存", "warning");
};
const answerExamPreview = async () => {
const logStore = useLogStore();
logStore.addLog(`检测到学习通考试预览页面`, "primary");
logStore.addLog(`正在解析任务点,请稍等5-10秒(如果长时间没有反应,请刷新页面)`, "warning");
await new CxQuestionHandler("ks").init();
logStore.addLog("考试不允许自动交卷:请检查答案后手动交卷", "warning");
logStore.addLog("答题完成,请自行检查后保存或提交", "primary");
};
const useCxExamLogic = async () => {
const logStore = useLogStore();
const configStore = useConfigStore();
const inSequentialMode = _GM_getValue('pp_exam_sequential', false);
if (!inSequentialMode) {
logStore.addLog(`检测到学习通考试页面`, "primary");
logStore.addLog(`正在解析任务点,请稍等5-10秒(如果长时间没有反应,请刷新页面)`, "warning");
const markInfo = _unsafeWindow.document.querySelector('.mark_info');
const disallowPreview = markInfo?.textContent?.includes('不允许整卷预览');
if (!disallowPreview) {
logStore.addLog(`允许整卷预览,即将跳转到整卷预览页面`, "primary");
await delay(3);
_unsafeWindow.topreview();
await delay(5);
await answerExamPreview();
return;
}
logStore.addLog(`当前考试不允许整卷预览,进入逐题答题模式`, "warning");
logStore.addLog(`在考完前禁止手动切换题目,否则会导致重复答题!`, "warning");
_GM_setValue('pp_exam_sequential', true);
}
const autoSwitch = readCxParamValue(PP_KEY.EXAM, PP_KEY.AUTO_NEXT, true);
logStore.addLog(`正在解析题目,请等待5s`, "warning");
await new CxQuestionHandler("ks").init();
logStore.addLog("考试不允许自动交卷:请检查答案后手动交卷", "warning");
const currentQuestionNum = parseInt(_unsafeWindow.document.querySelector(".topicNumber_list .current")?.innerText || "0");
const totalQuestions = _unsafeWindow.document.querySelectorAll(".topicNumber_list li").length;
if (currentQuestionNum >= totalQuestions) {
logStore.addLog("当前已是最后一题,答题完成", "warning");
logStore.addLog("请手动检查答案后提交试卷", "primary");
_GM_setValue('pp_exam_sequential', false);
return;
}
if (!autoSwitch) {
logStore.addLog("已经关闭自动切换,在设置里可更改", "danger");
_GM_setValue('pp_exam_sequential', false);
return;
}
logStore.addLog(`第${currentQuestionNum}题已完成,正在前往下一题`, "success");
await delay(3);
const nextBtn = _unsafeWindow.document.querySelector('.nextDiv a.jb_btn');
if (nextBtn) {
nextBtn.click();
} else if (typeof _unsafeWindow.getTheNextQuestion === 'function') {
_unsafeWindow.getTheNextQuestion(1);
} else {
logStore.addLog("未找到下一题按钮,请手动切换", "danger");
_GM_setValue('pp_exam_sequential', false);
}
};
const useCxExamPreviewLogic = answerExamPreview;
const useCxPlazaTipLogic = () => {
const logStore = useLogStore();
logStore.addLog("检测到学习通积分课页面", "warning");
logStore.addLog("积分课请进入课程后,开启复习模式,并关闭自动切换", "warning");
logStore.addLog("课程完成后请手动切换章节,自动跳转可能出现乱跳", "warning");
};
const useCxAutoReadLogic = async () => {
const logStore = useLogStore();
const configStore = useConfigStore();
const currentUrl = window.location.href;
if (!currentUrl.includes('/ztnodedetailcontroller/visitnodedetail')) {
logStore.addLog(`检测到学习通课程目录页面`, "primary");
await delay(2);
const firstChapter = document.querySelector('.course_section .chapterText');
if (firstChapter) {
firstChapter.click();
} else {
logStore.addLog("未找到章节列表,请手动点击章节", "danger");
}
return;
}
logStore.addLog(`检测到学习通阅读页面`, "primary");
logStore.addLog(`阅读任务次日才会统计阅读时长`, "warning");
const scrollDuration = 63;
const step = (document.documentElement.scrollHeight - window.innerHeight) / 60;
let currentTop = 0;
let scrollCount = 0;
logStore.addLog(`预计${scrollDuration}秒后完成当前章节阅读`, "primary");
await new Promise((resolve) => {
const interval = setInterval(() => {
scrollCount++;
currentTop += step;
window.scrollTo({ behavior: 'smooth', top: currentTop });
if (scrollCount >= 60) {
clearInterval(interval);
resolve();
}
}, 1000);
});
await delay(1);
const nextChapter = document.querySelector('.nodeItem.r i');
if (nextChapter) {
logStore.addLog(`当前章节阅读完成,正在进入下一章`, "success");
await delay(2);
nextChapter.click();
} else {
const restartAfterFinish = readCxParamValue(PP_KEY.CHAPTER, PP_KEY.UNLIMITED_READ, false);
if (restartAfterFinish) {
logStore.addLog(`已是最后一章,即将从头重新阅读`, "primary");
await delay(3);
const firstChapter = document.querySelectorAll('.course_section .chapterText');
if (firstChapter.length) firstChapter[0].click();
} else {
logStore.addLog(`阅读任务已全部完成`, "success");
}
}
};
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 () => {
this.questions = [];
const progressStore = useProgressStore();
const configStore = useConfigStore();
const _answerParamsPart2 = configStore.platformParams[configStore.platformName]?.parts.find(p => p.name === PP_KEY.ANSWER);
const skipAnswered = _answerParamsPart2?.params.find(p => p.name === PP_KEY.SKIP_ANSWERED)?.value || false;
const waitForPageReady = async () => {
const startedAt = Date.now();
let visibleWaitStart = 0;
let diagLogged = false;
while (Date.now() - startedAt < 5 * 60 * 1000) {
const els = this.getZhsQuestionElements();
if (els.length) {
const stems = els.map((el) => this.readStem(el));
const visibleIdx = els.findIndex((el) => {
const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
return rect && rect.width > 0 && rect.height > 0;
});
const visibleReady = visibleIdx >= 0 && Boolean(stems[visibleIdx].html || stems[visibleIdx].text);
if (visibleReady) return els;
if (visibleIdx >= 0) {
if (!visibleWaitStart) visibleWaitStart = Date.now();
if (!diagLogged && Date.now() - visibleWaitStart >= 2000) {
diagLogged = true;
this.addLog(`第${this.zhsQuestionNumber(els[visibleIdx])}题题干暂未读取到(${this.describeStemState(els[visibleIdx])}),继续等待…`, "warning");
}
if (Date.now() - visibleWaitStart >= 60000) {
this.addLog(`题目渲染超过 60 秒仍未完成,先继续处理(${this.describeStemState(els[visibleIdx])})`, "warning");
return els;
}
}
}
await afkWait(150);
}
return this.getZhsQuestionElements();
};
this.addLog("等待页面加载完成,开始扫描任务点", "info");
const readyElements = await waitForPageReady();
if (!readyElements.length) {
this.addLog("未解析到题目,请刷新重试或进入答题页面", "danger");
return;
}
const answerTotal = readyElements.length;
ppLogAnswerBegin((msg, type) => this.addLog(msg, type), { platform: "zhs", scene: "作业", total: answerTotal });
progressStore.update({
taskName: "智慧树答题",
type: "答题",
isPlaying: true,
questionTotal: answerTotal,
questionIndex: 0,
percent: 0,
currentTime: 0,
totalTime: 0,
detail: `第 0 / ${answerTotal} 题`
});
const handled = new Set();
let filledCount = 0;
let skippedCount = 0;
let failedCount = 0;
let running = false;
let finishedLogged = false;
const markFinishedIfDone = () => {
if (finishedLogged) return;
const els = this.getZhsQuestionElements();
if (!els.length) return;
const allHandled = els.every((el) => handled.has(this.zhsQuestionKey(el)));
if (!allHandled) return;
finishedLogged = true;
this.correctNum = filledCount;
this.skippedCount = skippedCount;
this.failedCount = failedCount;
progressStore.update({
filledCount,
fillRate: computeFillRate(filledCount, answerTotal)
});
ppLogAnswerSummary((msg, type) => this.addLog(msg, type), {
handled: filledCount + skippedCount + failedCount,
filled: filledCount,
skipped: skippedCount,
failed: failedCount,
total: answerTotal,
});
ppLogAnswerSubmitNote((msg, type) => this.addLog(msg, type), "manual");
progressStore.update({
questionIndex: answerTotal,
percent: 100,
isPlaying: false,
detail: `已完成 ${answerTotal} 题`
});
};
const clickNextQuestion = async () => {
const switchBtns = this._document?.querySelectorAll(".switch-btn-box > button");
const nextBtn = switchBtns && switchBtns[1];
if (!nextBtn) {
this.addLog("未找到下一题按钮,请手动切换", "danger");
return false;
}
nextBtn.click();
await afkWait(1200);
return true;
};
const findPendingQuestion = () => {
for (const el of this.getZhsQuestionElements()) {
const key = this.zhsQuestionKey(el);
if (!key || handled.has(key)) continue;
const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
if (rect && rect.width > 0 && rect.height > 0) return el;
}
return null;
};
const handleCurrentQuestion = async () => {
if (running) return;
const el = findPendingQuestion();
if (!el) return;
handled.add(this.zhsQuestionKey(el));
running = true;
try {
const label = this.zhsQuestionNumber(el) || String(this.questions.length + 1);
progressStore.update({
questionIndex: handled.size,
percent: Math.round((handled.size - 1) / answerTotal * 100),
detail: `第 ${handled.size} / ${answerTotal} 题`
});
const stemStartedAt = Date.now();
let stem = this.readStem(el);
if (!stem.html && !stem.text) {
this.addLog(`第${label}题题干正在渲染,等待就绪…(${this.describeStemState(el)})`, "info");
while (Date.now() - stemStartedAt < 60000 && !stem.html && !stem.text) {
await afkWait(150);
stem = this.readStem(el);
}
const waited = ((Date.now() - stemStartedAt) / 1000).toFixed(1);
if (stem.html || stem.text) {
this.addLog(`第${label}题题干已就绪(等待 ${waited}s)`, "primary");
} else {
this.addLog(`第${label}题题干读取失败(等了 ${waited}s,${this.describeStemState(el)}),按空题干上传`, "warning");
}
}
const question = this.buildQuestion(el, stem);
this.questions.push(question);
ppLogAnswerQuestion((msg, type) => this.addLog(msg, type), label, question.title);
if (skipAnswered && this.isZhsQuestionAnswered(question)) {
ppLogAnswerSkipped((msg, type) => this.addLog(msg, type), label);
skippedCount += 1;
question.skipped = true;
this.addQuestion(question);
await clickNextQuestion();
return;
}
const answerData = await queryAnswer(question);
if (answerData.code === 200) {
question.answer = answerData.data.answer;
question.source = answerData.data.source;
question.sourceName = answerData.data.sourceName || "";
this.addQuestion(question);
await new Promise(r => setTimeout(r, 100));
const filledOk = (await this.fillQuestion(question)) === true;
if (filledOk) {
filledCount += 1;
progressStore.update({
filledCount,
fillRate: computeFillRate(filledCount, answerTotal)
});
ppLogAnswerFilled((msg, type) => this.addLog(msg, type), label, {
source: answerData.data.source,
cost: answerData.data.cost,
num: answerData.data.num,
});
} else {
failedCount += 1;
question._fillFailed = true;
ppLogAnswerFillFailed((msg, type) => this.addLog(msg, type), label, "答案与页面选项对不上");
}
if (answerData.data.cost !== undefined) {
progressStore.update({ remainingCount: answerData.data.num });
}
} else if (isReviewFailed(answerData)) {
failedCount += 1;
question._reviewFailed = true;
question.source = "error";
question.answer = reviewFailedAnswers(answerData);
ppLogAnswerReviewFailed((msg, type) => this.addLog(msg, type), answerData);
this.addQuestion(question);
await new Promise(r => setTimeout(r, 100));
} else {
ppLogAnswerQueryFailed((msg, type) => this.addLog(msg, type), label, answerData.msg);
if (answerData.data?.sponsorUrl) {
this.addLog(`${getSponsorLink(answerData.data.sponsorUrl, '点我赞助获取新token')},可继续使用答题`, 'warning');
}
question.source = "error";
question.answer[0] = answerData.msg;
this.addQuestion(question);
await new Promise(r => setTimeout(r, 100));
}
await clickNextQuestion();
} finally {
running = false;
markFinishedIfDone();
}
};
await handleCurrentQuestion();
this._zhsWatchTimer = afkInterval(() => {
handleCurrentQuestion().catch(() => {});
}, 800);
});
__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 false;
try {
const typeNum = question.type;
if (typeNum === "0" || typeNum === "1") {
const configStore = useConfigStore();
const useSimilarity = configStore.platformParams[configStore.platformName]?.parts.find(p => p.name === PP_KEY.ANSWER)?.params.find(p => p.name === PP_KEY.SIMILAR)?.value || false;
let hasDeselected = false;
for (const key in question.options) {
const optionElement = question.options[key];
if (this.isZhsOptionSelected(optionElement)) {
hasDeselected = true;
optionElement.click();
}
}
if (hasDeselected) {
await new Promise(r => setTimeout(r, 500));
}
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);
}
}
const optionList = getOptionList(question);
const targetEls = [];
for (const answer of answers) {
const cleanAnswer = this.stripTags(answer).trim();
if (!cleanAnswer) continue;
const idx = matchOptionIndex(optionList, cleanAnswer);
if (idx < 0) continue;
const el = optionList[idx] && optionList[idx].el;
if (el && !targetEls.includes(el)) targetEls.push(el);
}
for (const optionElement of targetEls) {
if (this.isZhsOptionSelected(optionElement)) continue;
optionElement.setAttribute("data-filling", "true");
optionElement.click();
await new Promise(r => setTimeout(r, 200));
optionElement.removeAttribute("data-filling");
}
return targetEls.length > 0;
} else if (typeNum === "3") {
let answer = null;
if (REGEX.JUDGE_FALSE.test(question.answer[0])) {
answer = "错";
} else if (REGEX.JUDGE_TRUE.test(question.answer[0])) {
answer = "对";
}
if (answer === null) {
return false;
}
let judgeClicked = false;
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");
optionElement.click();
setTimeout(() => optionElement.removeAttribute("data-filling"), 200);
judgeClicked = true;
break;
}
}
return judgeClicked;
} else if (typeNum === "2") {
const textareaElements = question.element.querySelectorAll("textarea");
let blankFilled = 0;
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]);
if (String(answerText == null ? "" : answerText).trim() === "") return;
textarea.value = answerText;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
textarea.dispatchEvent(new Event("change", { bubbles: true }));
try {
const typed = String(answerText).slice(-1) || "Unidentified";
textarea.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: typed }));
textarea.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, cancelable: true, key: typed }));
} catch (e) {}
blankFilled += 1;
}
});
}
return blankFilled > 0;
} 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");
if (String(answerText == null ? "" : answerText).trim() === "") return false;
textareaElement.value = answerText;
textareaElement.dispatchEvent(new Event("input", { bubbles: true }));
textareaElement.dispatchEvent(new Event("change", { bubbles: true }));
try {
const typed = String(answerText).slice(-1) || "Unidentified";
textareaElement.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: typed }));
textareaElement.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, cancelable: true, key: typed }));
} catch (e) {}
return true;
}
return false;
} else {
return false;
}
} catch (error) {
this.addLog(`答题过程发生错误:${error.message}`, "danger");
return false;
}
});
}
isZhsOptionSelected(optionElement) {
return optionElement.classList.contains("cur") ||
optionElement.classList.contains("selected") ||
!!optionElement.querySelector(".onChecked") ||
!!optionElement.querySelector(".cur") ||
!!optionElement.querySelector(".selected");
}
addQuestions(questionElements) {
questionElements.forEach((questionElement) => {
this.questions.push(this.buildQuestion(questionElement));
});
}
getZhsQuestionElements() {
try {
return this._document ? Array.from(this._document.querySelectorAll(SELECTORS.ZHS_QUESTION)) : [];
} catch (e) {
return [];
}
}
readStem(questionElement) {
let bridged = "";
try {
bridged = questionElement && questionElement.getAttribute ? questionElement.getAttribute("data-pp-stem") || "" : "";
} catch (e) {}
if (bridged) {
let text = this.stripTags(bridged.replace(/
]*>/gi, "")).trim();
if (!text && /
{
try {
const fn = _unsafeWindow.originFunction;
return !!(fn && fn.__ppAttachWrapped);
} catch (e) {
return false;
}
})();
const htmlLen = sd ? String(sd.innerHTML || "").length : 0;
let bridged = "";
try {
bridged = questionElement && questionElement.getAttribute ? questionElement.getAttribute("data-pp-stem") || "" : "";
} catch (e) {}
return `bridge=${bridged ? "有" : "无"} ivue=${vm ? "有" : "无"} shadow=${sd ? "有" : "无"} recorded=${recorded ? "有" : "无"} origin=${originWrapped ? "已接管" : "未接管"} htmlLen=${htmlLen}`;
} catch (e) {
return `诊断失败:${e.message}`;
}
}
buildQuestion(questionElement, stem) {
var _a, _b;
const stemInfo = stem || this.readStem(questionElement);
const stemHtml = stemInfo.html || "";
const questionTitle = stemInfo.text || "";
const questionTitleHtml = (() => {
try {
const tags = String(stemHtml).match(/
]*>/gi) || [];
const imgs = [];
for (const tag of tags) {
const lazyMatch = tag.match(/data-src=["']?([^"'\s>]+)/i);
const srcMatch = tag.match(/\ssrc=["']?([^"'\s>]+)/i);
let src = srcMatch ? srcMatch[1] : "";
if (lazyMatch && (!src || /^data:/i.test(src))) src = lazyMatch[1];
if (src && !/^data:/i.test(src)) {
imgs.push(`
`);
}
}
if (imgs.length) return imgs.join("");
} catch (e) {}
return "";
})();
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");
return {
element: questionElement,
type: numericType,
title: questionTitle,
titleHtml: questionTitleHtml,
optionsText: optionTexts,
options: optionsObject,
answer: [],
workType: "zhs",
refer: this._window.location.href,
html: this.serializeQuestionHtml(questionElement, stemHtml, questionTitle)
};
}
zhsQuestionNumber(questionElement) {
try {
const numEl = questionElement.querySelector(".subject_num span, .subject_num");
const raw = numEl ? String(numEl.textContent || "").trim() : "";
return raw.replace(/[^\d]/g, "") || raw;
} catch (e) {
return "";
}
}
zhsQuestionKey(questionElement) {
const number = this.zhsQuestionNumber(questionElement);
let title = "";
try {
const stemEl = questionElement.querySelector(".subject_describe, .smallStem_describe");
title = String((stemEl == null ? void 0 : stemEl.textContent) || "").replace(/\s+/g, "").slice(0, 20);
} catch (e) {}
if (number) return `${number}#${title}`;
try {
return `idx${Array.prototype.indexOf.call(this._document.querySelectorAll(SELECTORS.ZHS_QUESTION), questionElement)}`;
} catch (e) {
return "";
}
}
serializeQuestionHtml(questionElement, stemHtml, stemText) {
try {
const clone = questionElement.cloneNode(true);
const host = clone.querySelector(".subject_describe") || clone.querySelector(".smallStem_describe");
if (host) {
if (stemHtml) {
host.innerHTML = stemHtml;
} else if (stemText) {
host.textContent = stemText;
}
}
return clone.outerHTML;
} catch (e) {
return questionElement.outerHTML;
}
}
}
const ppfxGuardTrustTimers = () => {
const ppfxNativeInterval = _unsafeWindow.setInterval;
const ppfxNativeTimeout = _unsafeWindow.setTimeout;
const ppfxFromTrustCheck = () => {
try {
return String(new Error().stack || "").indexOf("checkoutNotTrustScript") !== -1;
} catch (error) {
return false;
}
};
_unsafeWindow.setInterval = function(...args) {
return ppfxFromTrustCheck() ? -1 : ppfxNativeInterval.apply(this, args);
};
_unsafeWindow.setTimeout = function(...args) {
return ppfxFromTrustCheck() ? -1 : ppfxNativeTimeout.apply(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;
const originalOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url2) {
originalOpen.apply(this, [method, url2]);
const shouldIntercept = urlList.some((urlItem) => url2.includes(urlItem));
if (shouldIntercept) {
this.addEventListener('load', function() {
self.callback(this.responseText);
});
}
};
}
}
const useZhsAnswerLogic = async () => {
ppfxGuardTrustTimers();
const logStore = useLogStore();
useConfigStore();
if (!zhStudyTuning().answerOn) {
logStore.addLog("当前为「仅视频」模式,已跳过智慧树答题", "warning");
return;
}
logStore.addLog(`检测到智慧树答题页面`, "primary");
logStore.addLog(`正在解析任务点,请稍等5-10秒(如果长时间没有反应,请刷新页面)`, "warning");
let started = false;
const startAnswering = async () => {
if (started) return;
started = true;
await new ZhsQuestionHandler().init();
};
new XMLHttpRequestInterceptor(["gateway/t/v1/answer/hasAnswer"], async () => {
await delay(1);
await startAnswering();
return true;
});
const waitTimer = setInterval(() => {
if (started) {
clearInterval(waitTimer);
return;
}
if (document.querySelectorAll(SELECTORS.ZHS_QUESTION).length) {
clearInterval(waitTimer);
startAnswering().catch(() => {});
}
}, 500);
};
const PP_ZH_H5_SELECTORS = {
root: ".questionContent",
label: ".questionTitle",
stem: ".questionName .centent-pre",
singleOptions: "ul.radio-view > li.clearfix, .el-radio",
multiOptions: ".checkbox-view .el-checkbox, .checkbox-views .el-checkbox, .el-checkbox-group .el-checkbox",
next: ".pre-next .next-topic"
};
const ppZhH5Root = () => zhPick(PP_ZH_H5_SELECTORS.root);
const ppZhH5StemText = (root) => zhTxtOf(zhPick(PP_ZH_H5_SELECTORS.stem, root));
const ppZhH5NumberText = (root) => zhTxtOf(zhPick(".letterSortNum", root));
const ppZhH5Tag = (root) => (root ? `${ppZhH5NumberText(root)}|${ppZhH5StemText(root)}` : "");
const ppZhH5Total = () => {
const text = zhTxtOf(zhPick(".left-H")) || zhTxtOf(zhPick(".header-content"));
const matched = text.match(/[((]\s*(\d+)\s*题\s*[))]/);
return matched ? parseInt(matched[1], 10) : 0;
};
const ppZhTypeCodeOf = (label) => ppTypeCodeOf(label);
const ppZhH5TypeCode = (root) => {
const label = zhTxtOf(zhPick(PP_ZH_H5_SELECTORS.label, root)) || ppZhH5NumberText(root);
return ppZhTypeCodeOf(label);
};
const ppZhH5OptionText = (node) => {
const box = zhPick(".stem", node) || zhPick(".el-checkbox__label", node) || node;
try {
const copy = box.cloneNode(true);
copy.querySelectorAll(".letterSort").forEach((badge) => badge.remove());
return (copy.textContent || "").replace(/\s+/g, " ").trim();
} catch (e) {
return zhTxtOf(box);
}
};
const ppZhH5OptionNodes = (root, typeCode) => {
const readable = (node) => Boolean(zhTxtOf(node));
if (typeCode === "1") return zhPickAll(PP_ZH_H5_SELECTORS.multiOptions, root).filter(readable);
if (typeCode === "0" || typeCode === "3") return zhPickAll(PP_ZH_H5_SELECTORS.singleOptions, root).filter(readable);
const both = zhPickAll(
`${PP_ZH_H5_SELECTORS.singleOptions}, ${PP_ZH_H5_SELECTORS.multiOptions}`,
root
).filter(readable);
return both;
};
const ppZhH5Picked = (node) => {
try {
if (node.classList.contains("is-checked") || node.classList.contains("checked")) return true;
if (node.querySelector("input:checked")) return true;
const tick = node.querySelector("i.checkIcon");
if (tick && String(tick.className).indexOf("checkedIcon") >= 0) return true;
} catch (e) {}
return false;
};
const ppZhH5Press = async (node) => {
if (!node) return false;
try { node.scrollIntoView({ block: "center", inline: "nearest" }); } catch (e) {}
const before = ppZhH5Picked(node);
try { node.click(); } catch (e) {}
await afkWait(120);
if (ppZhH5Picked(node) !== before) return true;
try { node.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); } catch (e) {}
await afkWait(120);
return true;
};
const ppZhH5Indexes = (options, value, typeCode) => {
const raw = String(value == null ? "" : value).trim();
if (!raw) return [];
if (typeCode === "3") {
const want = ppJudgeKey(raw);
if (want) {
const at = options.findIndex((item) => ppJudgeKey(item.text) === want);
if (at >= 0) return [at];
}
}
const compact = raw.replace(/[\s,,、.。.;;::/\\|]+/g, "");
if (/^[A-Ha-h]{1,8}$/.test(compact) && compact.length <= options.length) {
const picked = [];
for (const ch of compact.toUpperCase()) {
const idx = ch.charCodeAt(0) - 65;
if (idx >= 0 && idx < options.length) picked.push(idx);
}
if (picked.length) return Array.from(new Set(picked));
}
const hit = matchOptionIndex(options, raw);
return hit >= 0 ? [hit] : [];
};
const ppZhH5WriteText = (root, answers) => {
const fields = zhPickAll(".fillAnswer input, .fillAnswer textarea, textarea, input.el-input__inner, input[type='text'], [contenteditable='true']", root)
.filter((field) => !/checkbox|radio/i.test(String(field.type || "")));
if (!fields.length) return false;
let written = 0;
fields.forEach((field, index) => {
const text = String(answers[index] == null ? answers[0] : answers[index] == null ? "" : answers[index])
.replace(/<[^>]+>/g, "")
.trim();
if (!text) return;
try {
if (field.isContentEditable) {
field.textContent = text;
} else {
const proto = field.tagName === "TEXTAREA" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
if (setter) setter.call(field, text); else field.value = text;
}
field.dispatchEvent(new Event("input", { bubbles: true }));
field.dispatchEvent(new Event("change", { bubbles: true }));
try {
const typed = text.slice(-1) || "Unidentified";
field.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: typed }));
field.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, cancelable: true, key: typed }));
} catch (e) {}
written += 1;
} catch (e) {}
});
return written > 0;
};
const ppZhH5Next = async () => {
const btn = zhPick(PP_ZH_H5_SELECTORS.next);
if (!btn || /noNext|disabled|nopre/.test(String(btn.className))) return false;
const before = ppZhH5Tag(ppZhH5Root());
btn.click();
const startedAt = Date.now();
while (Date.now() - startedAt < 6000) {
await afkWait(200);
const after = ppZhH5Tag(ppZhH5Root());
if (after && after !== before) return true;
}
return false;
};
const zhAnswerFlow = async (cfg) => {
if (window[cfg.busyKey]) return;
window[cfg.busyKey] = true;
const logStore = useLogStore();
const progressStore = useProgressStore();
const questionStore = useQuestionStore();
try {
if (!zhStudyTuning().answerOn) {
logStore.addLog("当前为「仅视频」模式,已跳过智慧树答题", "warning");
return;
}
const waitStart = Date.now();
while (cfg.waitRootMs && Date.now() - waitStart < cfg.waitRootMs && !cfg.findRoot()) await afkWait(250);
if (!cfg.findRoot()) {
logStore.addLog(cfg.notFoundLog, "danger");
return;
}
const configStore = useConfigStore();
const answerParams = configStore.platformParams?.[configStore.platformName]?.parts.find((part) => part.name === PP_KEY.ANSWER);
const skipAnswered = answerParams?.params.find((param) => param.name === PP_KEY.SKIP_ANSWERED)?.value || false;
const total = cfg.total ? cfg.total() : 0;
ppLogAnswerBegin(logStore.addLog, { platform: "zhs", scene: cfg.scene || "答题", total });
if (cfg.onStart) cfg.onStart({ total, progressStore });
let handled = 0;
let filled = 0;
let skipped = 0;
let failed = 0;
const seen = new Set();
for (let step = 0; step < (cfg.maxSteps || 200); step += 1) {
const root = cfg.findRoot();
if (!root) {
await afkWait(300);
continue;
}
let stem = cfg.getStem(root);
if (!stem) {
const proceed = cfg.awaitStem ? await cfg.awaitStem(root) : false;
if (!proceed) {
await afkWait(cfg.stemRetryMs || 250);
continue;
}
stem = cfg.getStem(root);
}
const typeCode = cfg.getTypeCode(root);
const options = cfg.getOptions(root, typeCode);
const key = cfg.getKey(root, stem, options, typeCode);
if (seen.has(key)) break;
seen.add(key);
handled += 1;
if (cfg.onStepReady) cfg.onStepReady({ root, stem });
progressStore.update(cfg.perQuestion({ handled, total }));
ppLogAnswerQuestion(logStore.addLog, handled, stem);
const answered = (cfg.isExtraAnswered ? cfg.isExtraAnswered(root) : false) || options.some(cfg.isPicked);
if (skipAnswered && answered) {
skipped += 1;
if (cfg.recordSkipped) {
questionStore.addQuestion({
element: root,
html: root.outerHTML,
workType: "zhs",
refer: location.href,
type: typeCode,
title: stem,
optionsText: options.map((el, idx) => cfg.getOptionText(el, idx)),
skipped: true
});
}
ppLogAnswerSkipped(logStore.addLog, handled);
if (!(await cfg.next())) break;
continue;
}
const question = {
element: root,
html: root.outerHTML,
workType: "zhs",
refer: location.href,
type: typeCode,
title: stem,
optionsText: options.map((el, idx) => cfg.getOptionText(el, idx))
};
let answerData = null;
try {
answerData = await queryAnswer(question);
} catch (error) {
answerData = null;
}
if (answerData && answerData.code === 200) {
const rawAnswers = answerData.data?.answer;
const answers = Array.isArray(rawAnswers) ? rawAnswers : [rawAnswers];
let ok = false;
if (typeCode === "0" || typeCode === "1" || typeCode === "3") {
const list = options.map((el, idx) => ({ text: cfg.getOptionText(el, idx), el }));
const wanted = new Set();
answers.forEach((ans) => ppZhH5Indexes(list, ans, typeCode).forEach((idx) => wanted.add(idx)));
if (wanted.size) {
if (typeCode === "1") {
for (let i = 0; i < options.length; i += 1) {
if (cfg.isPicked(options[i]) !== wanted.has(i)) {
await cfg.pick(options[i]);
if (cfg.pickGapMs) await afkWait(cfg.pickGapMs);
}
}
} else {
const target = options[Array.from(wanted)[0]];
if (target && !cfg.isPicked(target)) await cfg.pick(target);
}
ok = true;
}
} else {
ok = cfg.writeText(root, answers.map((ans) => String(ans == null ? "" : ans)));
}
if (ok) {
filled += 1;
ppLogAnswerFilled(logStore.addLog, handled, {
source: answerData.data?.source,
cost: cfg.showCost ? answerData.data?.cost : undefined,
num: answerData.data?.num,
});
} else {
failed += 1;
const isChoice = typeCode === "0" || typeCode === "1" || typeCode === "3";
const reason = isChoice
? (answers.length ? "答案与页面选项对不上" : "未返回答案")
: (cfg.fillFailLog || "未匹配到可点击选项");
ppLogAnswerFillFailed(logStore.addLog, handled, reason);
}
question.answer = answers;
question.source = answerData.data?.source || "";
questionStore.addQuestion(question);
} else if (isReviewFailed(answerData)) {
failed += 1;
question._reviewFailed = true;
question.answer = reviewFailedAnswers(answerData);
question.source = "error";
questionStore.addQuestion(question);
ppLogAnswerReviewFailed(logStore.addLog, answerData);
} else {
failed += 1;
const msg = answerData && answerData.msg ? answerData.msg : "查询失败";
question.answer = [msg];
question.source = "error";
questionStore.addQuestion(question);
ppLogAnswerQueryFailed(logStore.addLog, handled, msg);
}
if (!(await cfg.next())) break;
if (cfg.stepGapMs) await afkWait(cfg.stepGapMs);
}
cfg.onFinish({ handled, filled, skipped, failed, total, progressStore, logStore });
} catch (error) {
logStore.addLog(`智慧树答题出错:${error && error.message ? error.message : error}`, "danger");
} finally {
window[cfg.busyKey] = false;
}
};
const ZH_PAPER_API_TAGS = ["getExamSheetInfo", "getExamQuestionInfo"];
const ZH_PAPER_CACHE_KEY = "__ppZhPaperCache";
const ZH_PAPER_CACHE_LIMIT = 24;
const ZH_PAPER_TYPE_CODES = { 1: "0", 2: "1", 3: "3", 4: "2", 5: "4", 14: "3" };
const ppZhPaperCache = () => {
const win = _unsafeWindow || window;
let cache = null;
try { cache = win[ZH_PAPER_CACHE_KEY]; } catch (e) { cache = null; }
if (!cache || typeof cache !== "object") cache = {};
if (!Array.isArray(cache.questions)) cache.questions = [];
if (!(Number(cache.sheetTotal) > 0)) cache.sheetTotal = 0;
try { win[ZH_PAPER_CACHE_KEY] = cache; } catch (e) {}
return cache;
};
const ppZhApiText = (value) => String(value == null ? "" : value).replace(/\s+/g, " ").trim();
const ppZhApiKey = (value) => ppZhApiText(value)
.replace(/[\s,。!?;:、()【】《》〈〉「」『』,.:;!?'"()[\]<>·…—-]/g, "")
.toLowerCase();
const ppZhSheetTotalOf = (payload) => {
if (!payload || typeof payload !== "object") return 0;
const direct = Number(payload.questionCount);
if (direct > 0) return direct;
const parts = Array.isArray(payload.partSheetVos) ? payload.partSheetVos : [];
let sum = 0;
parts.forEach((part) => {
if (!part || typeof part !== "object") return;
const list = Array.isArray(part.questionSheetVos) ? part.questionSheetVos : [];
if (list.length) {
sum += list.length;
return;
}
const count = Number(part.questionCount);
if (count > 0) sum += count;
});
return sum;
};
const ppZhApiOptionsOf = (payload) => {
if (!payload || typeof payload !== "object") return [];
const list = payload.optionVos || payload.questionOptions || payload.optionList || payload.options;
if (!Array.isArray(list)) return [];
return list
.slice()
.sort((a, b) => (Number(a && a.sort) || 0) - (Number(b && b.sort) || 0))
.map((item) => {
if (item == null) return "";
if (typeof item === "string") return ppZhApiText(item).replace(/^[A-Za-z][.、.))]\s*/, "");
const body = item.content || item.option || item.optionContent || item.text || item.name || "";
return ppZhApiText(body).replace(/^[A-Za-z][.、.))]\s*/, "");
})
.filter(Boolean);
};
const ppZhApiTypeCodeOf = (info) => {
if (!info) return "";
const byName = ppTypeCodeOf(info.typeName || "");
if (byName && byName !== "999") return byName;
const code = ZH_PAPER_TYPE_CODES[String(info.typeCode == null ? "" : info.typeCode)];
return code || "";
};
const ppZhTakePaperPayload = (address, text) => {
if (!text) return;
let raw = null;
try { raw = typeof text === "string" ? JSON.parse(text) : text; } catch (e) { return; }
if (!raw || typeof raw !== "object") return;
const payload = raw.data && typeof raw.data === "object" ? raw.data : raw;
if (!payload || typeof payload !== "object") return;
const url = String(address == null ? "" : address);
const cache = ppZhPaperCache();
if (url.indexOf("getExamSheetInfo") >= 0) {
const total = ppZhSheetTotalOf(payload);
if (total > 0) cache.sheetTotal = total;
return;
}
if (url.indexOf("getExamQuestionInfo") < 0) return;
const stem = ppZhApiText(payload.content || payload.questionContent || payload.stem || "");
const options = ppZhApiOptionsOf(payload);
if (!stem && !options.length) return;
const info = {
stem,
typeName: ppZhApiText(payload.questionTypeName || payload.questionTypeStr || ""),
typeCode: payload.questionType,
options
};
const key = ppZhApiKey(stem);
cache.questions = cache.questions.filter((item) => ppZhApiKey(item && item.stem) !== key);
cache.questions.push(info);
if (cache.questions.length > ZH_PAPER_CACHE_LIMIT) {
cache.questions.splice(0, cache.questions.length - ZH_PAPER_CACHE_LIMIT);
}
};
const installZhPaperProbe = () => {
const win = _unsafeWindow || window;
if (!win || win.__ppZhPaperProbe) return;
try { win.__ppZhPaperProbe = true; } catch (e) { return; }
ppZhPaperCache();
const wanted = (address) => {
const text = String(address == null ? "" : address);
return ZH_PAPER_API_TAGS.some((tag) => text.indexOf(tag) >= 0);
};
try {
const nativeFetch = win.fetch;
if (typeof nativeFetch === "function") {
win.fetch = function (...args) {
const pending = nativeFetch.apply(this, args);
try {
const input = args[0];
const address = input && typeof input === "object" ? input.url || "" : input;
if (wanted(address) && pending && typeof pending.then === "function") {
pending
.then((response) => {
try { return response.clone().text(); } catch (e) { return ""; }
})
.then((text) => ppZhTakePaperPayload(address, text))
.catch(() => {});
}
} catch (e) {}
return pending;
};
}
} catch (e) {}
try {
const NativeXhr = win.XMLHttpRequest;
if (typeof NativeXhr === "function" && !NativeXhr.__ppZhPaperProbe) {
NativeXhr.__ppZhPaperProbe = true;
const nativeOpen = NativeXhr.prototype.open;
const nativeSend = NativeXhr.prototype.send;
NativeXhr.prototype.open = function (method, address, ...rest) {
try { this.__ppZhPaperAddress = String(address == null ? "" : address); } catch (e) {}
return nativeOpen.call(this, method, address, ...rest);
};
NativeXhr.prototype.send = function (...args) {
try {
this.addEventListener("load", function () {
try {
const address = String(this.responseURL || this.__ppZhPaperAddress || "");
if (wanted(address)) ppZhTakePaperPayload(address, this.responseText);
} catch (e) {}
});
} catch (e) {}
return nativeSend.apply(this, args);
};
}
} catch (e) {}
};
const ppZhPaperInfoOf = (stem) => {
const key = ppZhApiKey(stem);
if (!key) return null;
const cache = ppZhPaperCache();
for (let i = cache.questions.length - 1; i >= 0; i -= 1) {
const item = cache.questions[i];
if (item && ppZhApiKey(item.stem) === key) return item;
}
return null;
};
const ppZhPaperTotal = () => {
const total = Number(ppZhPaperCache().sheetTotal);
return total > 0 ? total : 0;
};
const zhPaperClean = (value) => String(value == null ? "" : value).replace(/\s+/g, " ").trim();
const zhPaperTextOf = (scope, selectors) => {
if (!scope) return "";
const list = Array.isArray(selectors) ? selectors : [selectors];
for (const selector of list) {
if (!selector) continue;
let el = null;
try { el = scope.querySelector(selector); } catch (e) { el = null; }
const text = el ? zhPaperClean(el.textContent) : "";
if (text) return text;
}
return "";
};
const zhPaperButtons = () => Array.from(document.querySelectorAll("button"));
const ZH_PAPER_CARD_SELECTORS = [".grid.grid-cols-7 > button", ".custom-tree-answer-normal", ".answer-card button"];
const zhPaperCardNodes = () => {
const nodes = [];
ZH_PAPER_CARD_SELECTORS.forEach((selector) => {
try {
nodes.push.apply(nodes, Array.from(document.querySelectorAll(selector)));
} catch (e) {}
});
return nodes;
};
const zhPaperCardNumbers = () => zhPaperCardNodes()
.concat(zhPaperButtons())
.map((node) => zhPaperClean(node.textContent))
.filter((text) => /^\d+$/.test(text))
.map((text) => Number(text));
const zhPaperFirstCard = () => {
const numeric = zhPaperCardNodes().filter((node) => /^\d+$/.test(zhPaperClean(node.textContent)));
if (numeric.length >= 2) {
const first = numeric.find((node) => zhPaperClean(node.textContent) === "1");
if (first) return first;
}
const buttons = zhPaperButtons().filter((btn) => /^\d+$/.test(zhPaperClean(btn.textContent)));
if (buttons.length < 3) return null;
return buttons.find((btn) => zhPaperClean(btn.textContent) === "1") || null;
};
const zhPaperCurrentNumber = (profile) => {
if (!profile || typeof profile.number !== "function") return "";
let root = null;
for (const selector of profile.roots) {
try {
root = document.querySelector(selector);
} catch (e) {
root = null;
}
if (root) break;
}
if (!root) return "";
const scope = profile.scope ? profile.scope(root) : root;
const matched = String(profile.number(scope) || "").match(/^\s*(\d+)/);
return matched ? matched[1] : "";
};
const zhPaperJumpToFirst = async (profile) => {
const readNumber = () => (profile && typeof profile.number === "function" ? zhPaperCurrentNumber(profile) : "");
const readSignature = () => (profile && typeof profile.signature === "function" ? profile.signature() : "");
const card = zhPaperFirstCard();
if (!card) return;
const beforeNumber = readNumber();
if (beforeNumber === "1") return;
const beforeSignature = readSignature();
try {
card.click();
} catch (e) {
return;
}
const deadline = beforeNumber === "" ? 2500 : 8000;
const startedAt = Date.now();
while (Date.now() - startedAt < deadline) {
await afkWait(250);
if (readNumber() === "1") return;
const now = readSignature();
if (now && now !== beforeSignature) return;
}
};
const zhPaperPicked = (node) => {
try {
if (!node) return false;
if (ppZhH5Picked(node)) return true;
if (node.getAttribute("aria-checked") === "true") return true;
if (node.querySelector("input:checked")) return true;
const cls = String(node.className || "");
if (/(^|\s)(is-checked|checked|selected|active)(\s|$)/.test(cls)) return true;
const tailwind = (el) => {
const value = String((el && el.className) || "");
return /(^|\s)(bg-mainBg|border-mainBg)(\/\d+)?(\s|$)/.test(value) || /(^|\s)(is-checked|is-selected)(\s|$)/.test(value);
};
if (tailwind(node)) return true;
if (Array.from(node.querySelectorAll("div, span")).some(tailwind)) return true;
const spanCls = String((node.querySelector("span") || {}).className || "");
return /(^|\s)text-mainBg(\s|$)/.test(spanCls);
} catch (e) {
return false;
}
};
const zhPaperPress = async (node) => {
if (!node) return false;
try { node.scrollIntoView({ block: "center", inline: "nearest" }); } catch (e) {}
const before = zhPaperPicked(node);
try { node.click(); } catch (e) {}
await afkWait(140);
if (zhPaperPicked(node) !== before) return true;
try { node.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true })); } catch (e) {}
await afkWait(140);
return true;
};
const zhPaperGoNext = async (profile) => {
let btn = null;
try { btn = profile && profile.nextButton ? profile.nextButton() : null; } catch (e) { btn = null; }
if (!btn) {
btn = zhPaperButtons().find((item) => !item.disabled && zhPaperClean(item.textContent).indexOf("下一题") >= 0) || null;
}
if (!btn || btn.disabled || /disabled|noNext/.test(String(btn.className || ""))) return false;
const before = profile && profile.signature ? profile.signature() : "";
try { btn.click(); } catch (e) { return false; }
const startedAt = Date.now();
while (Date.now() - startedAt < 6000) {
await afkWait(200);
const after = profile && profile.signature ? profile.signature() : "";
if (after && after !== before) return true;
}
return false;
};
const ZH_EXAMLOOP_STEM = [
"div.flex-1 .mb-\\[32px\\] .text-mainText.font-medium",
".text-mainText.font-medium",
"[class*='text-mainText'][class*='font-medium']",
"[class*='question'][class*='title']"
];
const zhExamLoopScope = (root) => (root ? root.querySelector(":scope > .flex-1") || root : null);
const zhExamLoopStem = (scope) => {
if (!scope) return "";
for (const selector of ZH_EXAMLOOP_STEM) {
let el = null;
try { el = scope.querySelector(selector); } catch (e) { el = null; }
const text = el ? zhPaperClean(el.textContent) : "";
if (text && !/^\d+\s*[、..]$/.test(text)) return text;
}
return "";
};
const zhExamLoopTypeText = (scope) => {
if (!scope) return "";
return Array.from(scope.querySelectorAll("span"))
.map((el) => zhPaperClean(el.textContent))
.find((text) => /^(单选题|多选题|判断题|填空题|简答题|名词解释|论述题|计算题|多项选择|不定项)/.test(text)) || "";
};
const zhExamLoopNumber = (scope) => {
if (!scope) return "";
const first = Array.from(scope.querySelectorAll("span"))
.map((el) => zhPaperClean(el.textContent))
.find((text) => /^\d+\s*[、.]/.test(text));
const matched = first && first.match(/^(\d+)/);
return matched ? matched[1] : "";
};
const zhExamLoopOptionNodes = (scope) => {
if (!scope) return [];
const box = scope.querySelector("label")?.parentElement || scope;
return box ? Array.from(box.querySelectorAll(":scope > label")) : [];
};
const zhExamLoopOptionText = (label) => {
if (!label) return "";
const span = label.querySelector("span");
return zhPaperClean((span || label).textContent);
};
const zhExamLoopCardState = (number) => {
try {
const key = String(number || "").trim();
if (!key) return "";
const btn = zhPaperButtons().find((item) => zhPaperClean(item.textContent) === key);
if (!btn) return "";
const cls = String(btn.className || "");
if (/bg-mainBg\/10/.test(cls)) return "answered";
if (/(^|\s)bg-mainBg(\s|$)/.test(cls)) return "current";
return "empty";
} catch (e) {
return "";
}
};
const ZH_PAPER_PROFILES = [
{
id: "examloop",
name: "新形态考试",
roots: [".question-area-content"],
prepare: zhPaperJumpToFirst,
scope: zhExamLoopScope,
stem: zhExamLoopStem,
typeCode: (scope) => ppZhTypeCodeOf(zhExamLoopTypeText(scope)),
optionNodes: zhExamLoopOptionNodes,
optionText: zhExamLoopOptionText,
nextButton: () => {
const btn = zhPaperButtons().find((item) => zhPaperClean(item.textContent) === "下一题");
return btn && !btn.disabled && !/disabled/.test(String(btn.className || "")) ? btn : null;
},
signature: () => {
const scope = zhExamLoopScope(document.querySelector(".question-area-content"));
if (!scope) return "";
return [zhExamLoopStem(scope), zhExamLoopOptionNodes(scope).map(zhExamLoopOptionText).join("|")].join("#");
},
extraAnswered: (root) => zhExamLoopCardState(zhExamLoopNumber(zhExamLoopScope(root))) === "answered",
number: zhExamLoopNumber,
pickGapMs: 120,
},
{
id: "questionbox",
name: "新形态作业考试",
roots: [".questionBox"],
prepare: zhPaperJumpToFirst,
stem: (scope) => zhPaperTextOf(scope, [".questionContent"]),
typeCode: (scope) => ppZhTypeCodeOf(zhPaperTextOf(scope, [".questionTit"])),
optionNodes: (scope) => (scope ? Array.from(scope.querySelectorAll(".optionUl label")) : []),
optionText: (node) => zhPaperClean((node || {}).textContent || ""),
nextButton: () => document.querySelector("span.Topicswitchingbtn:nth-child(2)"),
number: (scope) => zhPaperTextOf(scope, [".questionTit", ".questionNumber", ".questionName"]),
signature: () => {
const root = document.querySelector(".questionBox");
if (!root) return "";
const options = Array.from(root.querySelectorAll(".optionUl label")).map((node) => zhPaperClean(node.textContent));
return [zhPaperTextOf(root, [".questionContent"]), options.join("|")].join("#");
},
},
{
id: "paper",
name: "作业/考试",
roots: [".questionContent"],
prepare: zhPaperJumpToFirst,
stem: (scope) => (scope ? ppZhH5StemText(scope) : ""),
typeCode: (scope) => (scope ? ppZhH5TypeCode(scope) : "999"),
optionNodes: (scope, typeCode) => (scope ? ppZhH5OptionNodes(scope, typeCode) : []),
optionText: (node) => ppZhH5OptionText(node),
nextButton: () => {
const btn = document.querySelector(".pre-next .next-topic");
return btn && !/noNext|disabled/.test(String(btn.className || "")) ? btn : null;
},
signature: () => {
const root = document.querySelector(".questionContent");
return root ? ppZhH5Tag(root) : "";
},
number: (scope) => zhPaperTextOf(scope, [".letterSortNum", ".questionTitle"]),
pickGapMs: 150,
showCost: true
},
{
id: "examitem",
name: "AI掌握度",
roots: [".exam-item"],
prepare: zhPaperJumpToFirst,
stem: (scope) => zhPaperTextOf(scope, [".quest-title .option-name", ".quest-title"]),
typeCode: (scope) => ppZhTypeCodeOf(zhPaperTextOf(scope, [".quest-type"])),
optionNodes: (scope) => (scope ? Array.from(scope.querySelectorAll("label")) : []),
optionText: (node) => zhPaperClean((node || {}).textContent || ""),
nextButton: () => null,
number: (scope) => zhPaperTextOf(scope, [".quest-type", ".quest-number", ".exam-item-index"]),
signature: () => zhPaperTextOf(document.querySelector(".exam-item"), [".quest-title .option-name", ".quest-title"]),
}
];
const useZhsPaperAnswerLogic = async () => {
installZhPaperProbe();
if (window.__ppZhPaperBusy) return;
let profile = null;
let stemWaits = 0;
let currentInfo = null;
let domOptionCount = 0;
const matchProfile = () => ZH_PAPER_PROFILES.find((item) => item.roots.some((selector) => {
try { return Boolean(document.querySelector(selector)); } catch (e) { return false; }
})) || null;
const findRoot = () => {
profile = matchProfile();
if (!profile) return null;
for (const selector of profile.roots) {
let el = null;
try { el = document.querySelector(selector); } catch (e) { el = null; }
if (el) return el;
}
return null;
};
if (zhStudyTuning().answerOn) {
const startedAt = Date.now();
while (Date.now() - startedAt < 30000 && !findRoot()) await afkWait(250);
if (profile && typeof profile.prepare === "function") {
try { await profile.prepare(profile); } catch (e) {}
findRoot();
}
}
const scopeOf = (root) => (profile && profile.scope ? profile.scope(root) : root);
const rememberApi = (root) => {
const scope = scopeOf(root);
const stem = profile && scope ? profile.stem(scope) : "";
currentInfo = ppZhPaperInfoOf(stem);
return currentInfo;
};
await zhAnswerFlow({
busyKey: "__ppZhPaperBusy",
findRoot,
notFoundLog: "未识别到智慧树题目区域,请刷新页面或手动进入答题页",
detectLog: () => `检测到智慧树答题页面(${profile ? profile.name : "通用"})`,
scene: profile && /考试/.test(profile.name) ? "考试" : profile && /作业/.test(profile.name) ? "作业" : "答题",
total: () => {
const byApi = ppZhPaperTotal();
if (byApi > 0) return byApi;
const byHeader = ppZhH5Total();
if (byHeader > 0) return byHeader;
const numbers = zhPaperCardNumbers();
return numbers.length ? Math.max(...numbers) : 0;
},
getStem: (root) => {
const scope = scopeOf(root);
return profile && scope ? profile.stem(scope) : "";
},
awaitStem: () => {
const scope = scopeOf(findRoot());
if (profile && scope && profile.stem(scope)) return true;
if (stemWaits < 20) {
stemWaits += 1;
return false;
}
return true;
},
stemRetryMs: 200,
onStepReady: ({ root }) => {
stemWaits = 0;
rememberApi(root);
},
getOptions: (root, typeCode) => {
rememberApi(root);
const scope = scopeOf(root);
const nodes = profile && scope ? profile.optionNodes(scope, typeCode) : [];
domOptionCount = nodes.length;
return nodes;
},
getOptionText: (node, index) => {
if (currentInfo && currentInfo.options.length && currentInfo.options.length === domOptionCount) {
const text = currentInfo.options[index];
if (text) return text;
}
return profile ? profile.optionText(node) : zhPaperClean((node || {}).textContent || "");
},
getTypeCode: (root) => {
rememberApi(root);
const byApi = ppZhApiTypeCodeOf(currentInfo);
if (byApi) return byApi;
const scope = scopeOf(root);
return profile && scope ? profile.typeCode(scope) : "999";
},
getKey: (root, stem, options, typeCode) => {
const signature = profile && profile.signature ? profile.signature() : "";
return [profile ? profile.id : "", signature, typeCode, stem].join("#");
},
isPicked: zhPaperPicked,
pick: (node) => zhPaperPress(node),
pickGapMs: profile && profile.pickGapMs ? profile.pickGapMs : 140,
isExtraAnswered: (root) => Boolean(profile && profile.extraAnswered && profile.extraAnswered(root)),
recordSkipped: true,
writeText: (root, answers) => ppZhH5WriteText(scopeOf(root), answers),
next: () => zhPaperGoNext(profile),
maxSteps: 200,
stepGapMs: 400,
showCost: Boolean(profile && profile.showCost),
onStart: ({ total: questionTotal, progressStore }) => {
progressStore.update({
taskName: "智慧树答题",
type: "答题",
isPlaying: true,
questionTotal,
questionIndex: 0,
percent: 0,
currentTime: 0,
totalTime: 0,
detail: `第 0 / ${questionTotal || "?"} 题`
});
},
perQuestion: ({ handled, total: questionTotal }) => ({
taskName: "智慧树答题",
type: "答题",
isPlaying: true,
questionTotal: questionTotal || handled,
questionIndex: handled,
percent: questionTotal ? Math.min(99, Math.round((handled - 1) / questionTotal * 100)) : 0,
detail: `第 ${handled} / ${questionTotal || "?"} 题`
}),
onFinish: ({ handled, filled, skipped, failed, total: questionTotal, progressStore, logStore }) => {
progressStore.update({
filledCount: filled,
fillRate: questionTotal ? Math.round((filled + skipped) / questionTotal * 100) : 0,
questionIndex: handled,
percent: 100,
isPlaying: false,
detail: `已完成 ${handled} / ${questionTotal || "?"} 题`
});
ppLogAnswerSummary(logStore.addLog, { handled, filled, skipped, failed, total: questionTotal });
ppLogAnswerSubmitNote(logStore.addLog, "manual");
}
});
};
const ZH_SPEED_CEILING = 1.5;
let _zhSpeedCeilingCache = 0;
const zhSpeedCeiling = () => {
try {
const scope = document.querySelector(".controlsBar") || document;
let max = 0;
scope.querySelectorAll("span, div, li, button").forEach((el) => {
if (el.children.length) return;
const label = (el.textContent || "").trim();
if (!/^\d+(\.\d+)?x$/i.test(label)) return;
const val = parseFloat(label);
if (Number.isFinite(val) && val > max) max = val;
});
if (max > 0 && max > _zhSpeedCeilingCache) _zhSpeedCeilingCache = max;
} catch (e) {}
return _zhSpeedCeilingCache || ZH_SPEED_CEILING;
};
const ZH_STUDY_PART = PP_KEY.VIDEO;
const zhSleep = (sec) => new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(sec) || 0) * 1e3));
const zhPick = (sel, root) => {
try {
return (root || document).querySelector(sel);
} catch (e) {
return null;
}
};
const zhPickAll = (sel, root) => {
try {
return Array.prototype.slice.call((root || document).querySelectorAll(sel));
} catch (e) {
return [];
}
};
const zhTxtOf = (el, fallback) => {
const text = el && el.textContent ? el.textContent.replace(/\s+/g, " ").trim() : "";
if (text) return text;
return fallback === undefined ? "" : fallback;
};
const zhVisible = (el) => {
try {
if (!el || !el.isConnected) return false;
const box = el.getBoundingClientRect();
if (!box.width && !box.height) return false;
const style = window.getComputedStyle(el);
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;
return true;
} catch (e) {
return false;
}
};
const zhTapNode = (el) => {
if (!el) return false;
try {
el.scrollIntoView({ block: "center", inline: "nearest" });
} catch (e) {}
const box = (() => {
try {
return el.getBoundingClientRect();
} catch (e) {
return { left: 0, top: 0, width: 1, height: 1 };
}
})();
const x = box.left + box.width / 2;
const y = box.top + box.height / 2;
let hit = el;
try {
hit = document.elementFromPoint(x, y) || el;
} catch (e) {
hit = el;
}
const atom = { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0, buttons: 1 };
const fire = (node, type, extra) => {
try {
node.dispatchEvent(new MouseEvent(type, Object.assign({}, atom, extra)));
} catch (e) {}
};
fire(hit, "mousedown");
fire(hit, "mouseup", { buttons: 0 });
fire(hit, "click", { buttons: 0 });
return true;
};
const zhRuntime = {
epoch: 0,
idle: 0,
notice: {},
lastQuizAt: 0,
lastAdvanceAt: 0,
quizSignature: "",
booted: false,
restudy: new Set()
};
const zhRestudyKey = (stage, el, list) => {
const text = zhTxtOf(el, "").slice(0, 80);
if (text) return `${stage.id}|${text}`;
const at = Array.isArray(list) ? list.indexOf(el) : -1;
return `${stage.id}|#${at}`;
};
const zhRestudySeen = (stage, el, list) => Boolean(el) && zhRuntime.restudy.has(zhRestudyKey(stage, el, list));
const zhRestudyMark = (stage, el, list) => {
if (el) zhRuntime.restudy.add(zhRestudyKey(stage, el, list));
};
const zhRestudyReset = () => {
zhRuntime.restudy.clear();
};
const zhResolveEnded = (stage, tune, log, media, current, done, options) => {
const opts = options || {};
const list = opts.list || (typeof stage.chapters === "function" ? stage.chapters() : []);
if (done && current) {
if (!tune.restudy) return "advance";
if (zhRestudySeen(stage, current, list)) return "advance";
zhRestudyMark(stage, current, list);
zhLogCadence(log, "restudy-" + stage.id, "复习模式:已完成节点从 0 重新播放", "primary", 60);
try {
media.currentTime = 0;
} catch (e) {}
return "play";
}
if (opts.replayLog) {
zhLogCadence(log, opts.replayKey || ("replay-" + stage.id), opts.replayLog, "warning", 25);
}
try {
media.currentTime = 1;
} catch (e) {}
return "play";
};
const zhLogCadence = (log, key, message, type, gapSec) => {
const now = Date.now();
const last = zhRuntime.notice[key] || 0;
if (now - last < (gapSec === undefined ? 30 : gapSec) * 1e3) return;
zhRuntime.notice[key] = now;
log.addLog(message, type || "primary");
};
const zhStudyTuning = () => {
const readParam = (partName, paramName, fallback) => {
try {
const parts = useConfigStore().platformParams?.zhs?.parts || [];
const part = parts.find((p) => p.name === partName);
const param = (part?.params || []).find((p) => p.name === paramName);
if (!param) return fallback;
return param.value === undefined || param.value === null ? fallback : param.value;
} catch (e) {
return fallback;
}
};
const speedRaw = Number(readParam(ZH_STUDY_PART, PP_KEY.RATE, zhSpeedCeiling())) || zhSpeedCeiling();
const mode = (() => {
const on = (key) => readParam(PP_KEY.MODE, key, false) === true;
if (on(PP_KEY.ONLY_ANSWER)) return "answer";
if (on(PP_KEY.ONLY_VIDEO)) return "video";
if (on(PP_KEY.REVIEW)) return "review";
return "normal";
})();
const restudy = mode === "review";
return {
mode,
restudy,
modeOn: mode !== "answer",
answerOn: mode !== "video",
autoPlay: readParam(ZH_STUDY_PART, PP_KEY.NORMAL_PLAY, true) !== false,
autoSpeed: readParam(ZH_STUDY_PART, PP_KEY.AUTO_RATE, true) !== false,
speed: Math.min(Math.max(1, speedRaw), zhSpeedCeiling()),
autoNext: readParam(PP_KEY.CHAPTER, PP_KEY.AUTO_NEXT, true) !== false,
quizAuto: readParam(ZH_STUDY_PART, "弹题作答", true) !== false,
ignoreHabit: readParam(PP_KEY.CHAPTER, "忽略习惯弹窗", false) === true,
captchaNotify: true
};
};
const zhNativeRateSetter = (() => {
try {
const desc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, "playbackRate");
return desc && typeof desc.set === "function" ? desc.set : null;
} catch (e) {
return null;
}
})();
const zhWriteRate = (media, rate) => {
try {
if (media.__ppZhRateSet) {
media.__ppZhRateSet.call(media, rate);
} else if (zhNativeRateSetter) {
media.__ppZhRateSet = zhNativeRateSetter;
zhNativeRateSetter.call(media, rate);
} else {
media.playbackRate = rate;
}
} catch (e) {}
};
const zhPinSpeed = (media, wanted) => {
if (!media) return 1;
const rate = Math.min(Math.max(1, Number(wanted) || 1), zhSpeedCeiling());
try {
if (!media.__ppZhSpeedHeld) {
media.__ppZhSpeedHeld = true;
zhWriteRate(media, rate);
Object.defineProperty(media, "playbackRate", {
configurable: true,
enumerable: true,
get() {
return media.__ppZhSpeed || rate;
},
set() {}
});
} else {
zhWriteRate(media, rate);
}
media.__ppZhSpeed = rate;
} catch (e) {}
return rate;
};
const zhFindMedia = async (selectors, timeoutSec) => {
const list = Array.isArray(selectors) ? selectors : [selectors];
const deadline = Date.now() + (timeoutSec === undefined ? 8 : timeoutSec) * 1e3;
while (Date.now() < deadline) {
for (const sel of list) {
const media = zhPick(sel);
if (media && (media.currentSrc || media.src || media.readyState > 0)) return media;
}
await zhSleep(0.4);
}
return null;
};
const zhKeepPlaying = async (media, tune) => {
if (!media) return false;
const log = useLogStore();
ppMuteOnce(media);
if (tune) zhPinSpeed(media, tune.autoSpeed ? zhSpeedCeiling() : tune.speed);
const markPlayed = () => {
if (media.__ppZhPlayLogged) return;
media.__ppZhPlayLogged = true;
try {
log.addLog(`播放成功,倍速:${Number(media.playbackRate) || 1}x`, "primary");
} catch (e) {}
};
if (!media.paused && !media.ended) {
markPlayed();
return true;
}
if (!media.__ppZhPlayAnnounced) {
media.__ppZhPlayAnnounced = true;
try {
const holder =
zhPick(".chapter-content-second.current") ||
zhPick(".chapter-item.current") ||
zhPick(".child-main.current") ||
zhPick(".video.current_play");
const mediaName = holder ? String(zhTxtOf(holder) || "").replace(/\s+/g, " ").trim().slice(0, 40) : "";
log.addLog(`开始播放:${mediaName || "视频"}`, "primary");
} catch (e) {}
}
try {
await media.play();
markPlayed();
return true;
} catch (e) {
try {
media.muted = true;
await media.play();
markPlayed();
return true;
} catch (e2) {
return false;
}
}
};
const zhVmChainOf = (node) => {
const chain = [];
let cursor = node;
let depth = 0;
while (cursor && depth < 14 && !chain.length) {
const vm = cursor.__vue__;
if (vm) {
let up = vm;
let guard = 0;
while (up && guard < 8) {
chain.push(up);
up = up.$parent;
guard += 1;
}
}
cursor = cursor.parentElement;
depth += 1;
}
return chain;
};
const zhVmAnswerQuiz = async (root, log) => {
const chain = zhVmChainOf(root);
if (!chain.length) return false;
const holder = chain.find((vm) => vm && (vm.topicInfo || vm.testDialog !== undefined));
if (!holder) return false;
const payload = holder.topicInfo || {};
const groups = payload.lessonTestQuestionUseInterfaceDtos || payload.testQuestionUseInterfaceDtos || payload.topicDtos || [];
const bucket = (groups && groups[0]) || {};
const question = bucket.testQuestion || bucket;
const options = (question && (question.questionOptions || question.questionOption)) || [];
const keys = bucket.answerUs || (question && question.answerUs) || [];
const invokers = ["topicClickQot", "topicClickOption", "chooseTopicOption", "clickTopicOption", "selectQuestionOption"]
.map((name) => holder[name])
.filter((fn) => typeof fn === "function");
if (!options.length || !keys.length || !invokers.length) return false;
let answered = 0;
for (const option of options) {
const key = option.sortUs !== undefined ? option.sortUs : option.sort;
if (keys.indexOf(key) === -1) continue;
try {
invokers[0].call(holder, option);
answered += 1;
} catch (e) {}
await zhSleep(0.6);
}
if (!answered) return false;
log.addLog(`弹题已按页面数据作答(${answered} 个选项)`, "success");
await zhSleep(1.2);
if (holder.testDialog !== undefined) {
try {
holder.testDialog = false;
} catch (e) {}
}
return true;
};
const zhQuizRoot = () => {
const legacy = zhPick("#playTopic-dialog");
if (zhVisible(legacy)) return legacy;
const modern = zhPick(".ai-test-question-wrapper");
if (zhVisible(modern)) return modern;
const wisdom = zhPick(".ai-class-exercise-dialog");
if (zhVisible(wisdom)) return wisdom;
return null;
};
const zhQuizStep = async (tune, log) => {
const root = zhQuizRoot();
if (!root) {
zhRuntime.quizSignature = "";
return false;
}
const signature = zhTxtOf(root).slice(0, 80);
const now = Date.now();
if (signature && signature === zhRuntime.quizSignature && now - zhRuntime.lastQuizAt < 15000) return false;
if (now - zhRuntime.lastQuizAt < 3000) return false;
zhRuntime.quizSignature = signature;
const handled = await zhHandleQuiz(root, tune, log);
zhRuntime.lastQuizAt = Date.now();
return handled;
};
const zhHandleQuiz = async (root, tune, log) => {
if (await zhVmAnswerQuiz(root, log)) return true;
if (!tune.quizAuto) {
zhLogCadence(log, "quiz-off", "出现弹题,但「弹题作答」已关闭,请手动处理后继续", "warning", 20);
return true;
}
zhLogCadence(log, "quiz-guess", "弹题未取到答案,按配置随机作答后关闭", "warning", 8);
if (root.id === "playTopic-dialog") {
const pages = zhPickAll("#playTopic-dialog .el-pager .number");
for (const page of pages) {
if (!page.isConnected) continue;
if (!/(^|\s)active(\s|$)/.test(page.className || "")) {
zhTapNode(page);
await zhSleep(0.5);
}
const items = zhPickAll("#playTopic-dialog ul .topic-item");
if (!items.length) continue;
zhTapNode(items[Math.floor(Math.random() * items.length)]);
await zhSleep(0.8);
}
await zhSleep(0.6);
zhTapNode(zhPick("#playTopic-dialog .close-btn") || zhPick("#playTopic-dialog .btn"));
return true;
}
if (root.classList.contains("ai-test-question-wrapper")) {
if (zhPick(".done", root)) {
zhTapNode(zhPick(".close-box", root));
return true;
}
const options = zhPickAll(".options .option", root);
if (options.length) {
zhTapNode(options[Math.floor(Math.random() * options.length)]);
await zhSleep(0.8);
}
zhTapNode(zhPick(".submit-btn .submits", root) || zhPick(".submit-btn", root));
await zhSleep(0.8);
zhTapNode(zhPick(".close-box", root));
return true;
}
const items = zhPickAll(".ques-list .item", root);
for (const item of items) {
const options = zhPickAll(".option", item).filter(zhVisible);
if (!options.length) continue;
zhTapNode(options[Math.floor(Math.random() * options.length)]);
await zhSleep(0.8);
}
const footer = zhPick(".el-dialog__footer .el-button.btn", root) || zhPick(".el-dialog__footer .el-button", root);
zhTapNode(footer);
await zhSleep(0.8);
zhTapNode(zhPick(".header-icon", root) || zhPick(".header-box .close-box", root));
return true;
};
const zhCaptchaNode = () => {
const onScreen = (el) => {
if (!el || !zhVisible(el)) return false;
try {
const box = el.getBoundingClientRect();
const vw = window.innerWidth || document.documentElement.clientWidth || 0;
const vh = window.innerHeight || document.documentElement.clientHeight || 0;
return box.bottom > 0 && box.right > 0 && box.top < vh && box.left < vw;
} catch (e) {
return false;
}
};
const legacy = zhPick(".yidun_popup, .yidun_modal, .yidun_intellisense, .geetest_panel, .verify-wrap");
if (onScreen(legacy)) return legacy;
const tcaptcha = zhPick("#tcaptcha_transform_dy, .tcaptcha-transform");
if (onScreen(tcaptcha)) return tcaptcha;
const yidunBox = zhPick("#captchaYidun");
if (yidunBox) {
let height = 0;
try {
height = yidunBox.getBoundingClientRect().height;
} catch (e) {}
if (height >= 50 && onScreen(yidunBox)) return yidunBox;
}
return null;
};
const zhCaptchaHold = async (tune, log) => {
if (!zhCaptchaNode()) return false;
log.addLog("检测到平台验证码,已暂停学习流程;验证完成后会自动继续", "danger");
const originTitle = document.title;
let flasher = null;
if (tune.captchaNotify) {
let flip = false;
flasher = setInterval(() => {
flip = !flip;
try {
document.title = flip ? "【需验证】" + originTitle : originTitle;
} catch (e) {}
}, 900);
}
const startedAt = Date.now();
while (zhCaptchaNode()) await zhSleep(1.5);
if (flasher) clearInterval(flasher);
try {
document.title = originTitle;
} catch (e) {}
log.addLog(`验证码已通过(等待 ${Math.round((Date.now() - startedAt) / 1e3)} 秒),继续学习`, "success");
return true;
};
const zhHabitNode = () =>
zhPickAll(".el-overlay, .el-dialog__wrapper, .el-message-box__wrapper").find(
(el) => zhVisible(el) && zhTxtOf(el).indexOf("保持良好的学习习惯") >= 0
);
const zhHabitStep = async (tune, log) => {
const node = zhHabitNode();
if (!node) return false;
if (!tune.ignoreHabit) {
zhLogCadence(
log,
"habit-pause",
"检测到习惯分提示(当天学习时长已满),已暂停自动切换;可在「学习设置」开启忽略",
"warning",
60
);
return true;
}
zhTapNode(zhPick(".el-dialog__header img", node) || zhPick(".el-dialog__headerbtn", node) || zhPick("img", node));
log.addLog("已忽略习惯分提示并继续(该课程的平时习惯分可能受影响)", "warning");
await zhSleep(1);
return true;
};
const zhClearNoise = () => {
zhPickAll(".v-modal, .brush-warning").forEach((el) => {
try {
el.remove();
} catch (e) {}
});
const tip = zhPickAll(".el-dialog").find((d) => zhVisible(d) && zhTxtOf(d).indexOf("温馨提示") >= 0);
if (tip) {
const btn = zhPick(".el-button.btn", tip);
if (btn && zhTxtOf(btn).indexOf("我知道了") >= 0) zhTapNode(btn);
}
const readTip = zhPick(".dialog-read .iconguanbi");
if (readTip && zhVisible(readTip.closest ? readTip.closest(".dialog-read") : readTip)) zhTapNode(readTip);
};
const zhPptSweep = async (boxSelector, log) => {
const boxes = Array.isArray(boxSelector) ? boxSelector : [boxSelector];
const find = (sel) => {
for (const one of boxes) {
const node = zhPick(one);
const hit = node && zhPick(sel, node);
if (hit) return hit;
}
return zhPick(sel);
};
const readPage = (sel) => parseInt(zhTxtOf(find(sel)), 10) || 0;
const current = readPage(".page-current");
const total = readPage(".page-total");
if (!total) return false;
if (current >= total) return true;
log.addLog(`发现课件(${total} 页),开始翻阅`, "primary");
let guard = 0;
while (guard < total + 6) {
const nowCurrent = readPage(".page-current");
const nowTotal = readPage(".page-total");
if (!nowTotal || nowCurrent >= nowTotal) break;
zhTapNode(find(".next-page-btn"));
await zhSleep(1.1);
guard += 1;
}
log.addLog("课件翻阅完成", "success");
return true;
};
const zhBookSweep = async (boxSelector, log) => {
const box = zhPick(boxSelector);
if (!box) return false;
const pages = zhPickAll(".book-mark .canvas-wrapper");
const pageHeight = (pages[0] && pages[0].getBoundingClientRect().height) || 420;
log.addLog("发现电子书,开始阅读", "primary");
let guard = 0;
while (guard < 400 && box.scrollTop + box.clientHeight < box.scrollHeight - 4) {
box.scrollTo({ top: Math.min(box.scrollTop + Math.max(pageHeight * 0.9, 200), box.scrollHeight), behavior: "smooth" });
await zhSleep(0.9);
guard += 1;
}
log.addLog("电子书阅读完成", "success");
return true;
};
const zhSectionProgress = (el) => {
const text = zhTxtOf(zhPick(".collapse-info-progress .progress-text", el));
const matched = text.replace(/\s/g, "").match(/(\d+)\/(\d+)/);
if (!matched) return null;
const done = parseInt(matched[1], 10);
const total = parseInt(matched[2], 10);
if (Number.isNaN(done) || Number.isNaN(total)) return null;
return { done, total };
};
const zhStageLegacyShare = {
id: "legacy-share",
label: "共享课",
match: (host) => /studyvideoh5\.zhihuishu\.com/.test(host),
chapters: () => zhPickAll("li.video, .clearfix.video"),
current: () => zhPick(".video.current_play") || zhPick(".current_play"),
isDone: (el) => !!(el && zhPick(".time_icofinish", el)),
async step(tune, log) {
zhClearNoise();
const media = await zhFindMedia(["#vjs_container_html5_api", "#vjs_container video", "video"], 6);
if (media) {
if (media.ended) {
const current = this.current();
const plan = zhResolveEnded(this, tune, log, media, current, !!(current && this.isDone(current)), {
replayKey: "legacy-replay",
replayLog: "视频播放结束但未计入完成,重播补齐时长"
});
if (plan === "advance") return await this.advance(tune, log);
}
await zhKeepPlaying(media, tune);
return true;
}
const pendingCurrent = this.current();
if (pendingCurrent && this.isDone(pendingCurrent)) return await this.advance(tune, log);
zhLogCadence(log, "wait-media-" + this.id, "当前节点视频尚未加载,等待中…", "warning", 20);
return false;
},
async advance(tune, log) {
if (!tune.autoNext) return false;
const list = this.chapters();
const pending = tune.restudy
? list.filter((el) => !zhRestudySeen(this, el, list))
: list.filter((el) => !this.isDone(el));
if (!pending.length) {
zhLogCadence(
log,
tune.restudy ? "legacy-restudy-all" : "legacy-all",
tune.restudy ? "复习模式:本页任务点已全部重刷完成" : "本页任务点已全部完成",
"success",
300
);
return false;
}
const current = this.current();
if (tune.restudy && current) zhRestudyMark(this, current, list);
const index = pending.indexOf(current);
const target = index >= 0 && pending[index + 1] ? pending[index + 1] : pending[0];
log.addLog(`切换到下一节:${zhTxtOf(zhPick(".catalogue_title", target), "下一个视频")}`, "primary");
zhTapNode(target);
await zhSleep(2.4);
return true;
}
};
const zhStageNewShare = {
id: "new-share",
label: "新共享课",
match: (host) => /studyplush5\.zhihuishu\.com|studywisdomh5\.zhihuishu\.com/.test(host),
chapters: () => zhPickAll(".child-main").filter((el) => el.parentElement && zhPick(".child-time", el.parentElement)),
current: () => zhPick(".child-main.current"),
isDone: (el) => !!(el && el.parentElement && zhPick(".finish-icon", el.parentElement)),
async step(tune, log) {
zhClearNoise();
const media = await zhFindMedia(["video", "audio"], 6);
if (media) {
if (media.ended) {
const current = this.current();
const plan = zhResolveEnded(this, tune, log, media, current, !!(current && this.isDone(current)));
if (plan === "advance") return await this.advance(tune, log);
}
await zhKeepPlaying(media, tune);
return true;
}
const pendingCurrent = this.current();
if (pendingCurrent && this.isDone(pendingCurrent)) return await this.advance(tune, log);
zhLogCadence(log, "wait-media-" + this.id, "当前节点视频尚未加载,等待中…", "warning", 20);
return false;
},
async advance(tune, log) {
if (!tune.autoNext) return false;
const list = this.chapters();
const pending = tune.restudy
? list.filter((el) => !zhRestudySeen(this, el, list))
: list.filter((el) => !this.isDone(el));
if (!pending.length) {
zhLogCadence(
log,
tune.restudy ? "newshare-restudy-all" : "newshare-all",
tune.restudy ? "复习模式:本页任务点已全部重刷完成" : "本页任务点已全部完成",
"success",
300
);
return false;
}
const current = this.current();
if (tune.restudy && current) zhRestudyMark(this, current, list);
const index = pending.indexOf(current);
const target = index >= 0 && pending[index + 1] ? pending[index + 1] : pending[0];
log.addLog(`切换到下一节:${zhTxtOf(target.parentElement, "下一节")}`, "primary");
zhTapNode(target);
await zhSleep(2.4);
return true;
}
};
const zhStageWisdomMooc = {
id: "wisdom-mooc",
label: "新智慧共享课",
match: (host) => /wisdom-mooc\.zhihuishu\.com/.test(host),
chapters: () => {
const flat = [];
zhPickAll(".chapter-content .chapter-item").forEach((item) => {
const children = zhPickAll(".chapter-content-second", item);
if (children.length) flat.push.apply(flat, children);
else flat.push(item);
});
return flat;
},
current: () => zhPick(".chapter-content-second.current") || zhPick(".chapter-item.current"),
isDone: (el) => !!(el && zhPick(".finish-icon", el)),
async step(tune, log) {
zhClearNoise();
const mastery = zhPick(".masterylevel-body");
if (zhVisible(mastery)) {
zhTapNode(zhPick(".header-box .close-box") || zhPick(".header-icon"));
log.addLog("已关闭掌握度弹窗", "primary");
await zhSleep(1);
return true;
}
const media = await zhFindMedia(["video", "audio"], 6);
if (media) {
if (media.ended) {
const current = this.current();
const plan = zhResolveEnded(this, tune, log, media, current, !!(current && this.isDone(current)), {
replayKey: "wisdom-replay",
replayLog: "当前节点未出现完成标记,重新播放补齐时长"
});
if (plan === "advance") return await this.advance(tune, log);
}
await zhKeepPlaying(media, tune);
return true;
}
const pendingCurrent = this.current();
if (pendingCurrent && this.isDone(pendingCurrent)) return await this.advance(tune, log);
zhLogCadence(log, "wait-media-" + this.id, "当前节点视频尚未加载,等待中…", "warning", 20);
return false;
},
async advance(tune, log) {
if (!tune.autoNext) return false;
const list = this.chapters();
const pending = tune.restudy
? list.filter((el) => !zhRestudySeen(this, el, list))
: list.filter((el) => !this.isDone(el));
if (!pending.length) {
zhLogCadence(
log,
tune.restudy ? "wisdom-restudy-all" : "wisdom-all",
tune.restudy ? "复习模式:本页任务点已全部重刷完成" : "本页任务点已全部完成",
"success",
300
);
return false;
}
const current = this.current();
if (tune.restudy && current) zhRestudyMark(this, current, list);
const index = pending.indexOf(current);
const target = index >= 0 && pending[index + 1] ? pending[index + 1] : pending[0];
log.addLog(`切换到下一节:${zhTxtOf(target, "下一节")}`, "primary");
zhTapNode(target);
await zhSleep(2.6);
return true;
}
};
const ZH_CARD_SELECTORS = ["[class*=\"card-container\"]", ".resources-list .resources-item"];
const zhCardList = () => {
for (const sel of ZH_CARD_SELECTORS) {
const found = zhPickAll(sel);
if (found.length) return found;
}
return [];
};
const zhCardKind = (card) => {
const icon = (card && zhPick(".icon-box", card)) || (card && zhPick(".file-type", card));
const cls = (icon && icon.className) || "";
if (/video/.test(cls)) return "video";
if (/book/.test(cls)) return "book";
if (/ppt|other/.test(cls)) return "ppt";
if (/text/.test(cls)) return "text";
if (/link/.test(cls)) return "link";
return "unknown";
};
const zhCardDone = (card) => {
const flag = card && zhPick(".finished-icon", card);
if (!flag) return false;
const text = zhTxtOf(flag);
return !text || text.indexOf("已完成") >= 0;
};
const zhStageCard = {
id: "card",
label: "新形态课程",
match: (host) => /ai-smart-course-student-pro\.zhihuishu\.com|smartcoursestudent\.zhihuishu\.com|fusioncourseh5\.zhihuishu\.com/.test(host),
chapters: () => zhPickAll(".clearfix.video"),
chapterDone: (el) => !!(el && (zhPick(".finish-icon", el) || zhPick(".time_icofinish", el))),
activeCard() {
return zhCardList().find((card) => /(^|\s)active(\s|$)/.test(card.className || "")) || zhCardList()[0] || null;
},
async chapterStep(tune, log) {
const media = await zhFindMedia(["#vjs_container video", "#vjs_container_html5_api", "video", "audio"], 6);
if (media) {
if (media.ended) {
const current = zhPick(".clearfix.video.current_play") || zhPick(".clearfix.video.activeNode");
const plan = zhResolveEnded(this, tune, log, media, current, !!(current && this.chapterDone(current)));
if (plan === "advance") return await this.chapterAdvance(tune, log);
}
await zhKeepPlaying(media, tune);
return true;
}
return await this.chapterAdvance(tune, log);
},
async chapterAdvance(tune, log) {
if (!tune.autoNext) return false;
const list = this.chapters();
const pending = tune.restudy
? list.filter((el) => !zhRestudySeen(this, el, list))
: list.filter((el) => !this.chapterDone(el));
if (!pending.length) {
zhLogCadence(
log,
tune.restudy ? "fusion-chapter-restudy-all" : "fusion-chapter-all",
tune.restudy ? "复习模式:本页任务点已全部重刷完成" : "本页任务点已全部完成",
"success",
300
);
return false;
}
const current = zhPick(".clearfix.video.current_play") || zhPick(".clearfix.video.activeNode");
if (tune.restudy && current) zhRestudyMark(this, current, list);
const index = pending.indexOf(current);
const target = index >= 0 && pending[index + 1] ? pending[index + 1] : pending[0];
log.addLog(`切换到下一节:${zhTxtOf(zhPick(".title", target) || zhPick(".catalogue_title", target) || target, "下一个视频")}`, "primary");
zhTapNode(target);
await zhSleep(2.4);
return true;
},
async step(tune, log) {
zhClearNoise();
const popup = zhPick(".courseRemind.khfaPop");
if (popup && zhVisible(popup)) {
zhTapNode(zhPick(".header-slot-box .el-icon-error", popup));
log.addLog("已关闭课程提醒弹窗", "primary");
}
if (!zhCardList().length) return await this.chapterStep(tune, log);
const card = this.activeCard();
const kind = card ? zhCardKind(card) : "unknown";
if (kind === "video") {
const media = await zhFindMedia(["#vjs_container video", ".video-player-wrapper video", "video"], 6);
if (media) {
if (media.ended) {
const plan = zhResolveEnded(this, tune, log, media, card, !!(card && zhCardDone(card)), { list: zhCardList() });
if (plan === "advance") return await this.advance(tune, log);
}
await zhKeepPlaying(media, tune);
return true;
}
}
if (kind === "ppt") {
if (await zhPptSweep([".ppt-preview-container", ".ppt-btn-container"], log)) return true;
}
if (kind === "book") {
if (await zhBookSweep(".preview-section .book-mark .book-container, .book-container", log)) return true;
}
if (kind === "text" || kind === "link") {
zhLogCadence(log, "card-passive", "文本/链接类资源无法自动完成,已跳过", "warning", 120);
return await this.advance(tune, log);
}
return await this.advance(tune, log);
},
async advance(tune, log) {
if (!tune.autoNext) return false;
if (!zhCardList().length) return await this.chapterAdvance(tune, log);
const cards = zhCardList();
const activeIndex = cards.findIndex((card) => /(^|\s)active(\s|$)/.test(card.className || ""));
const activeCard = activeIndex >= 0 ? cards[activeIndex] : null;
const pending = tune.restudy
? cards.filter((card) => !zhRestudySeen(this, card, cards))
: cards.filter((card) => !zhCardDone(card));
if (pending.length) {
if (tune.restudy && activeCard) zhRestudyMark(this, activeCard, cards);
const after = pending.find((card) => cards.indexOf(card) > activeIndex);
const target = after || pending[0];
log.addLog(`切换到下一节:${zhTxtOf(target.querySelector(".common-text, .video-title, .basic-info-video-card-container")) || "下一个资源"}`, "primary");
zhTapNode(zhPick(".basic-info-video-card-container", target) || zhPick(".common-text", target) || target);
await zhSleep(2.4);
return true;
}
const sections = zhPickAll(".section-item-collapse-info");
if (!sections.length) {
zhLogCadence(
log,
tune.restudy ? "card-restudy-all" : "card-all",
tune.restudy ? "复习模式:本页任务点已全部重刷完成" : "本页任务点已全部完成",
"success",
300
);
return false;
}
const activeSection = sections.find((el) => /(^|\s)active(\s|$)/.test(el.className || ""));
const unfinished = sections.filter((el) => {
const progress = zhSectionProgress(el);
return !progress || progress.done < progress.total;
});
if (!unfinished.length) {
zhLogCadence(log, "card-section-all", "本页任务点已全部完成", "success", 300);
return false;
}
const activePos = unfinished.indexOf(activeSection);
const nextSection = activePos >= 0 && unfinished[activePos + 1] ? unfinished[activePos + 1] : unfinished[0];
log.addLog(`切换到下一节:${zhTxtOf(zhPick(".section-item-collapse-title", nextSection), "下一章")}`, "primary");
zhTapNode(nextSection);
await zhSleep(2.8);
return true;
}
};
const zhStagePractice = {
id: "practice",
label: "校内课 / 翻转课",
match: (host) => /hike\.zhihuishu\.com|wenda\.zhihuishu\.com|hike-teaching-center\.polymas\.com/.test(host),
profiles: [
{
list: ".file-item",
current: ".file-item.active",
title: "#sourceTit",
done: ".icon-finish",
media: "#mediaPlayer, video, audio",
rate: ".rate"
},
{
list: ".el-tree-node",
current: ".el-tree-node.is-current",
title: ".file-name",
done: "label.success",
media: "video, audio",
leafOnly: true
},
{
list: ".source-icon",
current: ".active-file",
title: ".file-name",
done: "i.select",
media: "video, audio"
}
],
profile() {
return this.profiles.find((p) => zhPickAll(p.list).length) || null;
},
items(profile) {
const list = zhPickAll(profile.list);
if (profile.leafOnly) return list.filter((el) => !(el.querySelector(".el-tree-node__children") || { children: [] }).children.length);
return list;
},
current(profile) {
const node = zhPick(profile.current);
if (!node) return null;
return node.closest ? node.closest(profile.list) : node;
},
isDone(profile, item) {
return !!(item && zhPick(profile.done, item));
},
findMediaDeep(selectors) {
const docs = [document];
zhPickAll("iframe").forEach((frame) => {
try {
if (frame.contentDocument) docs.push(frame.contentDocument);
} catch (e) {}
});
for (const doc of docs) {
for (const sel of selectors.split(",").map((s) => s.trim())) {
if (!sel) continue;
const nodes = zhPickAll(sel, doc);
for (const node of nodes) {
if (node.tagName === "VIDEO" || node.tagName === "AUDIO") return node;
const inner = zhPick("video, audio", node);
if (inner) return inner;
}
}
const fallback = zhPick("video, audio", doc);
if (fallback) return fallback;
}
return null;
},
async step(tune, log) {
zhClearNoise();
const profile = this.profile();
if (!profile) {
zhLogCadence(log, "practice-nolist", "未识别到章节列表,请确认已进入具体学习资源页", "warning", 90);
return false;
}
const media = this.findMediaDeep(profile.media);
if (media) {
if (media.ended) {
const current = this.current(profile);
const progress = zhTxtOf(zhPick(profile.rate, current));
const plan = zhResolveEnded(this, tune, log, media, current, !!(current && this.isDone(profile, current)), {
list: this.items(profile),
replayKey: "practice-replay",
replayLog: `播放结束但未计入完成${progress ? `(当前进度 ${progress})` : ""},重播补齐`
});
if (plan === "advance") return await this.advance(profile, tune, log);
}
await zhKeepPlaying(media, tune);
return true;
}
return await this.advance(profile, tune, log);
},
async advance(profile, tune, log) {
if (!tune.autoNext) return false;
const list = this.items(profile);
const pending = tune.restudy
? list.filter((item) => !zhRestudySeen(this, item, list))
: list.filter((item) => !this.isDone(profile, item));
if (!pending.length) {
zhLogCadence(
log,
tune.restudy ? "practice-restudy-all" : "practice-all",
tune.restudy ? "复习模式:本页任务点已全部重刷完成" : "本页任务点已全部完成",
"success",
300
);
return false;
}
const current = this.current(profile);
if (tune.restudy && current) zhRestudyMark(this, current, list);
const index = pending.indexOf(current);
const target = index >= 0 && pending[index + 1] ? pending[index + 1] : pending[0];
log.addLog(`切换到下一节:${zhTxtOf(target.querySelector(profile.title) || target, "下一个资源")}`, "primary");
zhTapNode(target);
await zhSleep(2.6);
return true;
}
};
const ZH_CLASSROOM_STAGES = [zhStageLegacyShare, zhStageNewShare, zhStageCard, zhStageWisdomMooc, zhStagePractice];
const zhStageLooksDone = (stage, tune) => {
if (tune && tune.restudy) return false;
try {
if (stage.id === "practice") {
const profile = stage.profile();
if (!profile) return false;
const current = stage.current(profile);
return !!(current && stage.isDone(profile, current));
}
if (stage.id === "card") {
const card = stage.activeCard();
return !!(card && zhCardDone(card));
}
const current = stage.current && stage.current();
return !!(current && stage.isDone && stage.isDone(current));
} catch (e) {
return false;
}
};
const zhEntryHint = () => {
const host = location.hostname;
const path = location.pathname + location.hash;
if (/\/singleCourse\//.test(path) || /ai-course-center/.test(path)) {
return "当前是课程主页,请点击任意章节进入学习页后再使用";
}
if (/wenda\./.test(host) && /studyResource/.test(path)) {
return "当前是资源列表页,请点击具体资源进入学习页后再使用";
}
if (/fusioncourseh5/.test(host) && /\/exam/.test(path)) {
return "当前是考试页面,答题请使用答题功能";
}
return "";
};
const zhBootClassroom = async (stage, log) => {
if (zhRuntime.booted) return;
zhRuntime.booted = true;
zhRuntime.epoch += 1;
zhRestudyReset();
const epoch = zhRuntime.epoch;
log.addLog(`检测到智慧树${stage.label}学习页面`, "primary");
log.addLog("正在解析任务点,请稍等5-10秒(如果长时间没有反应,请刷新页面)", "warning");
log.addLog(`播放器最大倍速:${zhSpeedCeiling()}x`, "info");
while (zhRuntime.epoch === epoch) {
await zhSleep(1.1);
const tune = zhStudyTuning();
if (!tune.modeOn) {
log.addLog("智慧树「学习」已关闭,停止本页任务", "warning");
break;
}
try {
if (await zhCaptchaHold(tune, log)) continue;
if (await zhHabitStep(tune, log)) continue;
if (await zhQuizStep(tune, log)) continue;
if (zhStageLooksDone(stage, tune)) {
if (Date.now() - zhRuntime.lastAdvanceAt > 2200 && (await stage.advance(tune, log))) {
zhRuntime.lastAdvanceAt = Date.now();
continue;
}
}
const acted = await stage.step(tune, log);
if (acted) {
zhRuntime.idle = 0;
} else {
zhRuntime.idle += 1;
if (zhRuntime.idle % 15 === 0) log.addLog("当前没有可推进的内容,等待页面状态刷新…", "primary");
}
} catch (error) {
zhLogCadence(log, "stage-error", `页面处理出现异常:${error && error.message ? error.message : error}`, "warning", 20);
}
}
};
const useZhClassroomStudy = async () => {
const log = useLogStore();
if (zhRuntime.booted) return;
const host = location.hostname;
const stage = ZH_CLASSROOM_STAGES.find((item) => item.match(host));
if (!stage) {
log.addLog("当前智慧树页面暂不支持自动学习", "warning");
return;
}
const hint = zhEntryHint();
if (hint) {
log.addLog(hint, "warning");
const waiter = setInterval(() => {
if (zhRuntime.booted) {
clearInterval(waiter);
return;
}
if (!zhEntryHint()) {
clearInterval(waiter);
if (zhStudyTuning().modeOn) zhBootClassroom(stage, log);
}
}, 2e3);
return;
}
if (!zhStudyTuning().modeOn) {
log.addLog("智慧树「学习」已关闭,当前页面不做处理", "warning");
return;
}
zhBootClassroom(stage, log);
};
const readYktParam = (partName, paramName, fallback = false) => {
try {
const parts = useConfigStore().platformParams?.ykt?.parts || [];
const part = parts.find((p) => p.name === partName);
const param = (part?.params || []).find((p) => p.name === paramName);
const val = param ? param.value : undefined;
return val === undefined || val === null ? fallback : val;
} catch (e) {
return fallback;
}
};
const YKT_SEL = {
job: "div.leaf-item",
jobTitle: ".leaf-item-title",
jobTag: ".leaf-item-tag",
jobDone: '[class*="yuanquangou"]',
detail: ".detail-container",
media: ".detail-container video",
pptSlide: ".swiper-container .container",
pptUnread: ".noRead",
navCollapsed: ".nav-item-title:not(.is-expand)",
forum: ".detail-container .lms-graph-forum",
forumStatus: ".learning-space-control-unit .control-right .f12",
forumInput: ".forum-publish textarea.el-textarea__inner",
forumComment: ".forum-content .forum-item .comment-text",
forumSend: ".prompt-send-btn:not(.disabled)",
exerciseFrame: ".detail-container .lms-graph-exercise iframe#iframeExerciseId",
encryptedFont: ".xuetangx-com-encrypted-font"
};
const yktJobs = () => Array.from(document.querySelectorAll(YKT_SEL.job));
const yktJobTag = (el) => (el.querySelector(YKT_SEL.jobTag)?.textContent || "").trim();
const yktJobTitle = (el) => (el.querySelector(YKT_SEL.jobTitle)?.textContent || "").trim() || "未知任务点";
const yktJobDone = (el) => !!el.querySelector(YKT_SEL.jobDone);
const yktExpandTree = async () => {
for (let i = 0; i < 5; i++) {
const collapsed = document.querySelectorAll(YKT_SEL.navCollapsed);
if (!collapsed.length) break;
collapsed.forEach((el) => el.click());
await delay(0.15);
}
};
const forceMediaRate = (media, rate) => {
try {
const desc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, "playbackRate");
if (!desc || typeof desc.set !== "function" || typeof desc.get !== "function") return false;
if (!media.__ppRateLocked) {
Object.defineProperty(media, "playbackRate", {
configurable: true,
get() { return this.__ppForcedRate; },
set() {}
});
media.__ppRateLocked = true;
}
media.__ppForcedRate = rate;
desc.set.call(media, rate);
try {
const defDesc = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, "defaultPlaybackRate");
if (defDesc && typeof defDesc.set === "function") defDesc.set.call(media, rate);
} catch (e) {}
try {
const label = document.querySelector("xt-speedvalue");
if (label) label.textContent = `${Number(rate).toFixed(2)}X`;
} catch (e) {}
return true;
} catch (e) {
return false;
}
};
const yktMediaFinished = (media, epsilon = 0.75) => {
if (!media) return false;
if (media.ended) return true;
const duration = Number(media.duration);
const current = Number(media.currentTime);
return Number.isFinite(duration) && duration > 0 && Number.isFinite(current) && current >= duration - epsilon;
};
const yktPlayMedia = (media, opts) => new Promise((resolve) => {
let settled = false;
let lastTime = -1;
let stallTicks = 0;
let ticker = null;
let labelObserver = null;
const readRate = () => {
try {
const raw = typeof opts.getRate === "function" ? opts.getRate() : opts.rate;
const num = Number(raw);
return Number.isFinite(num) && num > 0 ? num : 1;
} catch (e) {
return 1;
}
};
let currentRate = readRate();
const syncSpeedLabel = () => {
try {
const label = document.querySelector("xt-speedvalue");
if (!label) return;
const want = `${currentRate.toFixed(2)}X`;
if ((label.textContent || "").trim() !== want) label.textContent = want;
} catch (e) {}
};
const finish = (reason) => {
if (settled) return;
settled = true;
if (ticker) ticker.stop();
if (labelObserver) {
try { labelObserver.disconnect(); } catch (e) {}
labelObserver = null;
}
try { media.onended = null; media.onpause = null; } catch (e) {}
resolve(reason);
};
forceMediaRate(media, currentRate);
try { media.loop = false; } catch (e) {}
ppMuteOnce(media);
const verifyRate = () => {
try {
const actual = Number(media.playbackRate) || 1;
const ok = Math.abs(actual - currentRate) < 0.05;
useLogStore().addLog(
ok ? `倍速校验通过:实际 ${actual}x` : `倍速校验失败:期望 ${currentRate}x,实际 ${actual}x`,
ok ? "success" : "warning",
);
} catch (e) {}
};
try { setTimeout(() => { if (!settled) verifyRate(); }, 1500); } catch (e) {}
try {
const label = document.querySelector("xt-speedvalue");
if (label) {
labelObserver = new MutationObserver(() => syncSpeedLabel());
labelObserver.observe(label, { childList: true, characterData: true, subtree: true });
}
} catch (e) {}
syncSpeedLabel();
try { if (!media.currentTime || media.currentTime < 1) media.currentTime = 1; } catch (e) {}
media.onended = () => finish("ended");
media.onpause = () => {
if (!media.ended && !settled) setTimeout(() => { try { media.play(); } catch (e) {} }, 800);
};
const tick = () => {
try {
if (!media.isConnected) return finish("switched");
if (yktMediaFinished(media)) return finish("ended");
const nextRate = readRate();
if (Math.abs(nextRate - currentRate) > 0.05) {
currentRate = nextRate;
try { useLogStore().addLog(`倍速已更新:${currentRate}x`, "info"); } catch (e) {}
}
forceMediaRate(media, currentRate);
syncSpeedLabel();
if (media.paused && !media.ended) media.play().catch(() => {});
if (media.currentTime !== lastTime) {
lastTime = media.currentTime;
stallTicks = 0;
} else {
stallTicks++;
}
if (stallTicks > 60) return finish("stalled");
} catch (e) {}
};
ticker = afkIntervalHandle(tick, 1000);
try {
useLogStore().addLog(`开始播放:${opts.title || "视频"}`, "primary");
} catch (e) {}
Promise.resolve(media.play())
.then(() => {
try {
useLogStore().addLog(`播放成功,倍速:${Number(media.playbackRate) || 1}x`, "primary");
} catch (e) {}
})
.catch(() => {});
});
const yktPlayMediaCalm = (media, opts) => new Promise((resolve) => {
let settled = false;
let ticker = null;
const finish = (reason) => {
if (settled) return;
settled = true;
if (ticker) {
try { ticker.stop(); } catch (e) {}
}
try {
media.onended = null;
media.onpause = null;
} catch (e) {}
resolve(reason);
};
let rate = 1;
try {
const raw = typeof opts.getRate === "function" ? opts.getRate() : opts.rate;
const num = Number(raw);
if (Number.isFinite(num) && num > 0) rate = num;
} catch (e) {}
try { media.currentTime = 1; } catch (e) {}
try { media.playbackRate = rate; } catch (e) {}
ppMuteOnce(media);
media.onended = () => finish("ended");
media.onpause = () => {
if (media.ended || settled) return;
setTimeout(() => {
try { media.play(); } catch (e) {}
}, 900);
};
try {
useLogStore().addLog(`开始播放:数字人视频,倍速 ${rate}x`, "primary");
} catch (e) {}
Promise.resolve(media.play()).catch(() => {});
ticker = afkIntervalHandle(() => {
try {
if (!media.isConnected) return finish("switched");
if (yktMediaFinished(media)) return finish("ended");
if (media.paused && !media.ended) media.play().catch(() => {});
} catch (e) {}
}, 2000);
});
const yktWaitFor = async (getter, timeoutSeconds = 20, periodMs = 500) => {
const deadline = Date.now() + timeoutSeconds * 1000;
while (Date.now() < deadline) {
const value = getter();
if (value) return value;
await afkWait(periodMs);
}
return null;
};
const yktPostComment = async (forum, logStore) => {
const statusText = (forum.querySelector(YKT_SEL.forumStatus)?.textContent || "").trim();
if (statusText.includes("已发言")) {
logStore.addLog("当前讨论已发言,跳过评论", "success");
return "skipped";
}
const input = await yktWaitFor(() => forum.querySelector(YKT_SEL.forumInput), 20, 500);
if (!input) throw new Error("讨论区输入框加载超时");
const samples = Array.from(forum.querySelectorAll(YKT_SEL.forumComment))
.map((el) => (el.textContent || "").trim())
.filter(Boolean);
if (!samples.length) throw new Error("讨论区暂无他人发言可参考,已跳过");
input.value = samples[Math.floor(Math.random() * samples.length)];
input.dispatchEvent(new Event("input", { bubbles: true }));
const sendBtn = await yktWaitFor(() => forum.querySelector(YKT_SEL.forumSend), 10, 500);
if (!sendBtn) throw new Error("发送按钮未解除禁用,评论未提交");
sendBtn.click();
await delay(1.5);
logStore.addLog("讨论评论已提交", "success");
return "posted";
};
const useYktPptRead = async (logStore) => {
let switched = 0;
for (let guard = 0; guard < 500; guard++) {
const pending = Array.from(document.querySelectorAll(YKT_SEL.pptSlide)).filter((el) => el.querySelector(YKT_SEL.pptUnread));
if (!pending.length) break;
pending[0].click();
switched++;
await delay(1.2);
}
logStore.addLog(`课件阅读完成(共 ${switched} 页)`, "success");
return switched;
};
const PP_YKT_WORK = {
item: ".container-problem .subject-item",
typeLabel: ".item-type",
body: ".problem-body",
options: 'ul[class*="list-unstyled"] li label',
blankInput: 'input.blank-item-dynamic, input[placeholder="输入答案"]',
ueditor: 'iframe[id^="ueditor_"]',
textarea: "textarea",
navItem: ".problems-aside .subject-item.J_order",
submit: ".problem-box button.el-button--primary",
captcha: "#homework-automation-risk-challenge",
dialogs: ".homework-problem-change-guard-dialog, .homework-problem-batch-submit-dialog",
};
const ppYktFlat = (value) => String(value == null ? "" : value).replace(/\s+/g, " ").trim();
const ppYktKind = (text) => {
const t = String(text || "");
if (/多选|多项选择|不定项/.test(t)) return "1";
if (/单选|单项选择/.test(t)) return "0";
if (/判断|是非/.test(t)) return "3";
if (/填空/.test(t)) return "2";
if (/简答|问答|主观|论述|名词|计算/.test(t)) return "4";
return "";
};
const ppYktOptionLabel = (label) => {
try {
const input = label.querySelector("input");
const val = String((input && input.value) || "").toLowerCase();
if (val === "true") return "对";
if (val === "false") return "错";
let node = label.querySelector(".radioText, .checkboxText");
if (!node) node = label.querySelector(".el-radio__label, .el-checkbox__label") || label;
return ppYktFlat(node.textContent).replace(/^[A-Za-z][.、).::]\s*/, "");
} catch (e) {
return "";
}
};
const ppYktOptionChosen = (label) => {
try {
const input = label.querySelector("input");
if (input && input.checked) return true;
const cls = `${label.className || ""} ${(label.querySelector(".el-radio__inner, .el-checkbox__inner") || {}).className || ""}`;
return /is-checked/.test(cls);
} catch (e) {
return false;
}
};
const ppYktChooseOption = async (label) => {
try {
label.scrollIntoView({ block: "center", inline: "nearest" });
} catch (e) {}
const before = ppYktOptionChosen(label);
try {
label.click();
} catch (e) {}
await delay(0.15);
if (ppYktOptionChosen(label) !== before) return true;
try {
label.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
} catch (e) {}
await delay(0.15);
return true;
};
const ppYktHasAnswer = (item, optionSelector) => {
try {
const sel = optionSelector || PP_YKT_WORK.options;
if (Array.from(item.querySelectorAll(sel)).some(ppYktOptionChosen)) return true;
const fields = Array.from(item.querySelectorAll(`${PP_YKT_WORK.blankInput}, ${PP_YKT_WORK.textarea}`));
if (fields.some((f) => String(f.value == null ? "" : f.value).trim())) return true;
const frames = Array.from(item.querySelectorAll(PP_YKT_WORK.ueditor));
for (const frame of frames) {
try {
const body = frame.contentDocument && frame.contentDocument.body;
if (body && String(body.textContent || "").trim()) return true;
} catch (e) {}
}
return false;
} catch (e) {
return false;
}
};
const ppYktLoudHint = (text, tone) => {
let node = null;
try {
let win = window;
try {
if (window.top && window.top.document && window.top !== window) win = window.top;
} catch (e) {}
const doc = (win && win.document) || document;
const host = doc.body || doc.documentElement;
if (!host) return () => {};
const color = tone === "success" ? "#16a34a" : tone === "danger" ? "#dc2626" : "#f59e0b";
node = doc.createElement("div");
node.setAttribute("data-pp-ykt-hint", "1");
node.style.cssText = [
"position:fixed",
"left:50%",
"top:20px",
"transform:translateX(-50%)",
"z-index:2147483647",
"max-width:min(600px,92vw)",
"padding:12px 20px",
"border-radius:10px",
"background:" + color,
"color:#fff",
"font-size:14px",
"line-height:1.6",
"font-family:system-ui,-apple-system,'Microsoft YaHei',sans-serif",
"text-align:center",
"white-space:pre-wrap",
"box-shadow:0 10px 28px rgba(0,0,0,.28)",
"pointer-events:none",
].join(";");
node.textContent = text;
host.appendChild(node);
} catch (e) {
node = null;
}
return () => {
try {
if (node && node.parentNode) node.parentNode.removeChild(node);
} catch (e) {}
};
};
const ppYktDeskPing = (title, body) => {
try {
if (typeof Notification === "undefined") return;
const fire = () => {
try {
new Notification(title, { body, tag: "pp-ykt-notice" });
} catch (e) {}
};
if (Notification.permission === "granted") return fire();
if (Notification.permission === "default" && typeof Notification.requestPermission === "function") {
Promise.resolve(Notification.requestPermission())
.then((p) => {
if (p === "granted") fire();
})
.catch(() => {});
}
} catch (e) {}
};
let _ppYktChallengeAlerted = false;
let _ppYktChallengeWatchCount = 0;
const ppYktChallengePresent = (doc) => {
try {
return !!doc.querySelector(PP_YKT_WORK.captcha);
} catch (e) {
return false;
}
};
const ppYktWatchChallenge = (doc, logStore) => {
let visible = false;
let shut = () => {};
_ppYktChallengeWatchCount += 1;
const tick = () => {
const now = ppYktChallengePresent(doc);
if (now === visible) return;
visible = now;
if (now) {
if (_ppYktChallengeAlerted) return;
_ppYktChallengeAlerted = true;
logStore.addLog("检测到雨课堂风控验证码,请在页面手动完成,完成后自动继续", "danger");
shut = ppYktLoudHint("雨课堂需要手动验证:请在页面上完成验证码,通过后自动继续答题", "warning");
ppYktDeskPing("雨课堂:需要手动验证", "检测到风控验证码,请完成验证后继续答题");
} else {
try {
shut();
} catch (e) {}
shut = () => {};
if (!_ppYktChallengeAlerted) return;
_ppYktChallengeAlerted = false;
logStore.addLog("验证码已通过,继续答题", "success");
}
};
const timer = setInterval(tick, 400);
tick();
return {
stop: () => {
clearInterval(timer);
try {
shut();
} catch (e) {}
_ppYktChallengeWatchCount = Math.max(0, _ppYktChallengeWatchCount - 1);
_ppYktChallengeAlerted = false;
},
};
};
const ppYktChallengeHold = async (doc, logStore, timeoutMs = 180000) => {
if (!ppYktChallengePresent(doc)) return false;
try {
const box = doc.querySelector(PP_YKT_WORK.captcha);
if (box) box.scrollIntoView({ block: "center", inline: "nearest" });
} catch (e) {}
let shut = () => {};
if (!_ppYktChallengeAlerted) {
_ppYktChallengeAlerted = true;
logStore.addLog("检测到雨课堂风控验证码,请在页面手动完成,完成后自动继续", "danger");
shut = ppYktLoudHint("雨课堂需要手动验证:请在页面上完成验证码,通过后自动继续答题", "warning");
ppYktDeskPing("雨课堂:需要手动验证", "检测到风控验证码,请完成验证后继续答题");
}
const start = Date.now();
try {
while (Date.now() - start < timeoutMs) {
await delay(1);
if (!ppYktChallengePresent(doc)) {
if (_ppYktChallengeWatchCount === 0 && _ppYktChallengeAlerted) {
_ppYktChallengeAlerted = false;
logStore.addLog("验证码已通过,继续答题", "success");
}
return true;
}
}
} finally {
try {
shut();
} catch (e) {}
}
logStore.addLog("验证码等待超时(3 分钟),跳过本题", "warning");
return false;
};
const ppYktConfirmHold = async (doc, logStore, timeoutMs = 120000) => {
const find = () => {
try {
const list = Array.from(doc.querySelectorAll(PP_YKT_WORK.dialogs));
for (const el of list) {
const wrap = el.closest ? el.closest(".el-dialog__wrapper") : null;
const target = wrap || el;
if (!target || !target.offsetHeight) continue;
let display = "";
try {
const view = target.ownerDocument && target.ownerDocument.defaultView;
display = view ? view.getComputedStyle(target).display : "";
} catch (e) {
display = target.style ? target.style.display : "";
}
if (display && display !== "none") return target;
}
} catch (e) {}
return null;
};
if (!find()) return false;
logStore.addLog("检测到雨课堂确认弹窗,请手动处理后脚本会自动继续", "danger");
const shut = ppYktLoudHint("雨课堂弹出确认框:请手动处理(未保存答案/提交确认),处理后自动继续", "warning");
ppYktDeskPing("雨课堂:需要确认", "页面上有确认弹窗,请处理后继续");
const start = Date.now();
try {
while (Date.now() - start < timeoutMs) {
await delay(1);
if (!find()) {
logStore.addLog("弹窗已处理,继续", "success");
ppYktLoudHint("弹窗已处理,继续答题", "success");
return true;
}
}
} finally {
shut();
}
logStore.addLog("弹窗等待超时(2 分钟),跳过", "warning");
return false;
};
const ppYktWriteAnswers = async (item, typeCode, answers, optionSource) => {
const list = (Array.isArray(optionSource)
? optionSource.map((o) => ({ el: o.el, text: o.text == null ? ppYktOptionLabel(o.el) : o.text }))
: Array.from(item.querySelectorAll(optionSource || PP_YKT_WORK.options)).map((el) => ({
el,
text: ppYktOptionLabel(el),
}))
).filter((o) => o.text || o.el.querySelector("input[value='true'], input[value='false']"));
if (typeCode === "0" || typeCode === "1" || typeCode === "3") {
if (!list.length) return false;
let useSimilarity = true;
let threshold = 85;
try {
const cfg = useConfigStore();
const answerPart = cfg.platformParams[cfg.platformName]?.parts.find((p) => p && p.name === PP_KEY.ANSWER);
const similarParam = answerPart && answerPart.params.find((p) => p && p.name === PP_KEY.SIMILAR);
if (similarParam) useSimilarity = similarParam.value !== false;
threshold = resolveAnswerThreshold(cfg.platformName);
} catch (e) {}
const fuzzyPick = (ans) => {
if (!useSimilarity) return -1;
const target = String(ans == null ? "" : ans).trim();
if (!target) return -1;
let best = -1;
let bestScore = 0;
list.forEach((opt, index) => {
const score = computeMatch(target, String(opt.text || ""));
if (score > bestScore) {
bestScore = score;
best = index;
}
});
return bestScore >= threshold ? best : -1;
};
const wanted = new Set();
answers.forEach((ans) => {
const hits = ppZhH5Indexes(list, ans, typeCode);
if (hits.length) {
hits.forEach((idx) => wanted.add(idx));
return;
}
const fuzzy = fuzzyPick(ans);
if (fuzzy >= 0) wanted.add(fuzzy);
});
if (!wanted.size) return false;
if (typeCode === "1") {
for (let i = 0; i < list.length; i += 1) {
if (ppYktOptionChosen(list[i].el) !== wanted.has(i)) {
await ppYktChooseOption(list[i].el);
await delay(0.12);
}
}
} else {
const target = list[Array.from(wanted)[0]];
if (target && !ppYktOptionChosen(target.el)) await ppYktChooseOption(target.el);
}
return true;
}
const texts = answers.map((a) => String(a == null ? "" : a).replace(/<[^>]+>/g, "").trim());
const fields = [
...Array.from(item.querySelectorAll(PP_YKT_WORK.blankInput)),
...Array.from(item.querySelectorAll(PP_YKT_WORK.textarea)),
].filter((f) => !/checkbox|radio/i.test(String(f.type || "")));
let written = 0;
fields.forEach((field, index) => {
const text = texts[index] == null ? texts[0] : texts[index];
if (!text) return;
try {
const proto = field.tagName === "TEXTAREA" ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
if (setter) setter.call(field, text);
else field.value = text;
field.dispatchEvent(new Event("input", { bubbles: true }));
field.dispatchEvent(new Event("change", { bubbles: true }));
written += 1;
} catch (e) {}
});
const frames = Array.from(item.querySelectorAll(PP_YKT_WORK.ueditor));
frames.forEach((frame, index) => {
const text = texts[index] == null ? texts[0] : texts[index];
if (!text) return;
if (ppYktWriteRichText(frame, text)) written += 1;
});
return written > 0;
};
const ppYktWriteRichText = (frame, answer) => {
const html = `${String(answer)
.trim()
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/\n+/g, "
")}
`;
try {
const holder = frame.closest ? frame.closest('[id^="ueditor-"]') : null;
const view = frame.ownerDocument && frame.ownerDocument.defaultView;
if (holder && holder.id && view && view.UE) {
const editor = view.UE.getEditor(holder.id);
if (editor) {
editor.ready(() => {
try {
editor.setContent(html);
} catch (e) {}
});
return true;
}
}
} catch (e) {}
try {
const body = frame.contentDocument && frame.contentDocument.body;
if (!body) return false;
body.innerHTML = html;
body.dispatchEvent(new Event("input", { bubbles: true }));
return true;
} catch (e) {
return false;
}
};
const ppYktNavFinished = (navEl) => {
if (!navEl) return false;
try {
if (navEl.querySelector(".icon-status")) return true;
const status = navEl.querySelector(".text-status");
return !!(status && status.textContent && status.textContent.includes("未批改"));
} catch (e) {
return false;
}
};
const ppYktCommitQuestion = async (doc, logStore) => {
const activeNav = () => {
const nav = Array.from(doc.querySelectorAll(PP_YKT_WORK.navItem));
const idx = nav.findIndex((el) => el.classList.contains("active"));
return idx >= 0 ? nav[idx] : null;
};
const targetNav = activeNav();
for (let attempt = 0; attempt < 2; attempt += 1) {
const btn = Array.from(doc.querySelectorAll(PP_YKT_WORK.submit)).find(
(b) =>
ppYktFlat(b.textContent).includes("提交") &&
!b.disabled &&
!b.classList.contains("is-disabled"),
);
if (!btn) return ppYktNavFinished(targetNav);
const beforeActive = activeNav();
btn.click();
await delay(1);
const start = Date.now();
while (Date.now() - start < 10000) {
if (ppYktNavFinished(targetNav)) return true;
const nowActive = activeNav();
if (beforeActive && nowActive && nowActive !== beforeActive) return true;
if (ppYktChallengePresent(doc)) {
await ppYktChallengeHold(doc, logStore);
if (ppYktNavFinished(targetNav)) return true;
break;
}
await delay(0.3);
}
logStore.addLog("提交后未看到完成标记,重试一次", "warning");
}
logStore.addLog("本题提交状态未确认,请手动检查", "danger");
return false;
};
const ppYktFingerprint = (doc) => {
try {
const cur = doc.querySelector(PP_YKT_WORK.item);
if (!cur) return "";
const type = ppYktFlat((cur.querySelector(PP_YKT_WORK.typeLabel) || {}).textContent);
const body = cur.querySelector(PP_YKT_WORK.body);
return `${type}|${ppYktFlat(body ? body.textContent : "").slice(0, 60)}`;
} catch (e) {
return "";
}
};
const ppYktChangeGuardDialog = (doc) => {
const roots = [doc, document].filter(Boolean);
for (const root of roots) {
let dialog = null;
try {
dialog = root.querySelector(".homework-problem-change-guard-dialog");
} catch (e) {}
if (!dialog) continue;
const wrap = dialog.closest ? dialog.closest(".el-dialog__wrapper") : dialog.parentElement;
const target = wrap || dialog;
let display = "";
try {
const view = target.ownerDocument && target.ownerDocument.defaultView;
display = view ? view.getComputedStyle(target).display : "";
} catch (e) {
display = target.style ? target.style.display : "";
}
if (target.offsetHeight && (!display || display !== "none")) return dialog;
}
return null;
};
const ppYktResolveChangeGuard = (doc, allowSubmit, logStore) => {
const dialog = ppYktChangeGuardDialog(doc);
if (!dialog) return false;
const footer = dialog.querySelector(".el-dialog__footer") || dialog;
const primary = footer.querySelector(".homework-problem-change-guard__primary-action");
const buttons = Array.from(footer.querySelectorAll(".rain-btn"));
const target = allowSubmit ? primary : buttons.find((el) => el !== primary);
if (!target) return false;
try {
target.click();
} catch (e) {
return false;
}
if (logStore) {
logStore.addLog(
allowSubmit
? "检测到未提交提示:已选择「帮我提交」,继续下一题"
: "检测到未提交提示:已选择「先不用了」,继续下一题",
"primary",
);
}
return true;
};
const ppYktAdvance = async (doc, options = {}) => {
const opts = options || {};
const before = ppYktFingerprint(doc);
const nav = Array.from(doc.querySelectorAll(PP_YKT_WORK.navItem));
if (!nav.length) return false;
const activeIdx = nav.findIndex((el) => el.classList.contains("active"));
const next = nav[activeIdx < 0 ? 0 : activeIdx + 1];
if (!next) return false;
try {
next.scrollIntoView({ block: "center" });
} catch (e) {}
next.click();
const start = Date.now();
let guardHandled = false;
let retried = false;
while (Date.now() - start < 8000) {
if (!guardHandled && ppYktResolveChangeGuard(doc, opts.allowSubmit === true, opts.logStore)) {
guardHandled = true;
}
await delay(0.4);
const now = ppYktFingerprint(doc);
if (now && now !== before) return true;
if (!retried && Date.now() - start > 3000) {
retried = true;
try {
next.click();
} catch (e) {}
}
}
return false;
};
const useYktHomeworkLogic = async (exDoc, logStore, prepareFont) => {
if (!exDoc) {
logStore.addLog("未找到作业题目区域,请手动点开作业后重试", "warning");
return;
}
const questionStore = useQuestionStore();
const progressStore = useProgressStore();
const autoSwitch = readYktParam(PP_KEY.YKT_CHAPTER, PP_KEY.AUTO_NEXT, true);
const skipAnswered = readYktParam(PP_KEY.ANSWER, PP_KEY.SKIP_ANSWERED, true);
const autoSubmit = readYktParam(PP_KEY.YKT_CHAPTER, PP_KEY.AUTO_SUBMIT, true);
let handled = 0;
let filled = 0;
let failed = 0;
let skipped = 0;
let paperTotal = 0;
try {
paperTotal = exDoc.querySelectorAll(PP_YKT_WORK.navItem).length;
} catch (e) {}
ppLogAnswerBegin(logStore.addLog, { platform: "ykt", scene: "作业", total: paperTotal });
try {
const nav = Array.from(exDoc.querySelectorAll(PP_YKT_WORK.navItem));
if (nav.length && !nav[0].classList.contains("active")) {
logStore.addLog("从第 1 题开始逐题检查", "primary");
nav[0].scrollIntoView({ block: "center" });
nav[0].click();
await delay(1.2);
}
} catch (e) {}
progressStore.update({
taskName: "作业答题",
type: "答题",
isPlaying: true,
questionTotal: paperTotal,
questionIndex: 0,
percent: 0,
filledCount: 0,
fillRate: 0,
detail: paperTotal ? `第 0 / ${paperTotal} 题` : "等待题目加载",
});
const challengeWatch = ppYktWatchChallenge(exDoc, logStore);
for (let step = 0; step < 100; step += 1) {
await ppYktChallengeHold(exDoc, logStore);
const item = await yktWaitFor(() => exDoc.querySelector(PP_YKT_WORK.item), 15, 300);
if (!item) {
logStore.addLog("未等到题目加载,结束本次作答", "warning");
break;
}
if (prepareFont) await prepareFont(exDoc);
const marker = ppYktFingerprint(exDoc);
let plain = null;
try {
plain = ppDecryptQuestionClone(item, "ykt");
} catch (e) {
plain = null;
}
const plainRoot = plain || item;
const stem = ppYktFlat((plainRoot.querySelector(PP_YKT_WORK.body) || {}).textContent).slice(0, 40);
handled += 1;
progressStore.update({
questionIndex: handled,
percent: paperTotal ? Math.min(99, Math.round((handled - 1) / paperTotal * 100)) : 0,
detail: paperTotal ? `第 ${handled} / ${paperTotal} 题` : `第 ${handled} 题`,
});
ppLogAnswerQuestion(logStore.addLog, handled, stem);
const html = (plain && plain.outerHTML) || item.outerHTML;
const typeCode = ppYktKind((item.querySelector(PP_YKT_WORK.typeLabel) || {}).textContent);
const liveOptions = Array.from(item.querySelectorAll(PP_YKT_WORK.options));
const plainOptions = Array.from(plainRoot.querySelectorAll(PP_YKT_WORK.options));
const optionList = liveOptions.map((el, index) => ({
el,
text: ppYktOptionLabel(plainOptions[index] || el),
}));
const question = {
element: item,
html,
workType: "ykt",
refer: exDoc.location ? exDoc.location.href : location.href,
type: typeCode,
title: stem,
optionsText: optionList.map((o) => o.text),
};
if (skipAnswered && ppYktHasAnswer(item)) {
skipped += 1;
question.skipped = true;
try {
questionStore.addQuestion(question);
} catch (e) {}
ppLogAnswerSkipped(logStore.addLog, handled);
if (!autoSwitch) break;
const jumped = await ppYktAdvance(exDoc, { allowSubmit: false, logStore });
if (!jumped) {
logStore.addLog("本题已答,且没有其它未作答的题目了", "info");
break;
}
await delay(0.6);
continue;
}
let answerData = null;
try {
answerData = await queryAnswer(question);
} catch (e) {
answerData = null;
}
let filledOk = false;
if (answerData && answerData.code === 200) {
const raw = answerData.data && answerData.data.answer;
const answers = Array.isArray(raw) ? raw : [raw];
filledOk = await ppYktWriteAnswers(item, typeCode, answers, optionList);
question.answer = answers;
question.source = (answerData.data && answerData.data.source) || "";
try {
questionStore.addQuestion(question);
} catch (e) {}
if (filledOk) {
filled += 1;
ppLogAnswerFilled(logStore.addLog, handled, {
source: question.source,
cost: answerData.data && answerData.data.cost,
num: answerData.data && answerData.data.num,
});
} else {
failed += 1;
ppLogAnswerFillFailed(logStore.addLog, handled, "答案未能写入页面");
}
} else if (isReviewFailed(answerData)) {
failed += 1;
question._reviewFailed = true;
question.answer = reviewFailedAnswers(answerData);
question.source = "error";
try {
questionStore.addQuestion(question);
} catch (e) {}
ppLogAnswerReviewFailed(logStore.addLog, answerData);
} else {
failed += 1;
const msg = (answerData && answerData.msg) || "查询失败";
question.answer = [msg];
question.source = "error";
try {
questionStore.addQuestion(question);
} catch (e) {}
logStore.addLog(
answerData && answerData.code === 403
? `第${handled}题查询失败:服务端暂不支持雨课堂(${msg})`
: `第${handled}题查询失败:${msg}`,
"danger",
);
}
if (autoSubmit && filledOk) {
const submitted = await ppYktCommitQuestion(exDoc, logStore);
if (submitted) {
await ppYktConfirmHold(exDoc, logStore);
logStore.addLog(`第${handled}题已提交`, "success");
}
}
const markerNow = ppYktFingerprint(exDoc);
if (markerNow && markerNow !== marker) {
await delay(0.5);
continue;
}
if (!autoSwitch) {
logStore.addLog("自动切换已关闭,请手动切换到下一题", "warning");
break;
}
const moved = await ppYktAdvance(exDoc, { allowSubmit: autoSubmit && filledOk, logStore });
if (!moved) {
logStore.addLog("已到最后一题或切换失败,结束本次作答", "primary");
break;
}
await delay(0.6);
}
try {
challengeWatch.stop();
} catch (e) {}
progressStore.update({
isPlaying: false,
questionIndex: handled,
percent: 100,
filledCount: filled,
fillRate: paperTotal ? Math.round(((filled + skipped) / paperTotal) * 100) : 0,
detail: paperTotal
? `本份作业:填入 ${filled} / 跳过 ${skipped} / 共 ${paperTotal} 题`
: `本份作业:填入 ${filled} / 跳过 ${skipped} / 处理 ${handled} / 失败 ${failed}`,
});
ppLogAnswerSummary(logStore.addLog, { handled, filled, skipped, failed, total: paperTotal || handled });
if (!autoSubmit) ppLogAnswerSubmitNote(logStore.addLog, "autoOff");
};
const PP_YKT_EXAM = {
item: ".exercise-item",
typeLabel: ".item-type",
stem: "h4",
options: "label",
};
const useYktExamLogic = async () => {
if (window.__ppYktExamBusy) return;
window.__ppYktExamBusy = true;
const logStore = useLogStore();
const progressStore = useProgressStore();
const questionStore = useQuestionStore();
try {
const collectItems = () => Array.from(document.querySelectorAll(PP_YKT_EXAM.item));
let items = collectItems();
if (!items.length) {
logStore.addLog("未识别到考试题目,请等页面加载完后刷新重试", "warning");
return;
}
for (let pass = 0; pass < 6; pass += 1) {
const before = items.length;
try {
const last = items[items.length - 1];
if (last) last.scrollIntoView({ block: "end" });
} catch (e) {}
await delay(0.8);
items = collectItems();
if (items.length === before) break;
}
items = collectItems();
ppLogAnswerBegin(logStore.addLog, { platform: "ykt", scene: "考试", total: items.length });
const skipAnswered = readYktParam(PP_KEY.ANSWER, PP_KEY.SKIP_ANSWERED, true);
let handled = 0;
let filled = 0;
let failed = 0;
let skipped = 0;
progressStore.update({
taskName: "考试答题",
type: "答题",
isPlaying: true,
questionTotal: items.length,
questionIndex: 0,
percent: 0,
filledCount: 0,
fillRate: 0,
detail: `第 0 / ${items.length} 题`,
});
for (let index = 0; ; index += 1) {
const live = collectItems();
if (index >= live.length) break;
const total = live.length;
const item = live[index];
const labels = Array.from(item.querySelectorAll(PP_YKT_EXAM.options));
const typeCode = ppYktKind((item.querySelector(PP_YKT_EXAM.typeLabel) || {}).textContent);
const stem = ppYktFlat((item.querySelector(PP_YKT_EXAM.stem) || {}).textContent).slice(0, 40);
ppLogAnswerQuestion(logStore.addLog, index + 1, stem);
if (skipAnswered && ppYktHasAnswer(item, PP_YKT_EXAM.options)) {
skipped += 1;
try {
questionStore.addQuestion({
element: item,
html: item.outerHTML,
workType: "ykt",
refer: location.href,
type: typeCode,
title: stem,
optionsText: labels.map(ppYktOptionLabel),
skipped: true,
});
} catch (e) {}
ppLogAnswerSkipped(logStore.addLog, index + 1);
continue;
}
handled += 1;
progressStore.update({
questionIndex: index + 1,
percent: total ? Math.min(99, Math.round((index / total) * 100)) : 0,
detail: `第 ${index + 1} / ${total} 题`,
});
const question = {
element: item,
html: item.outerHTML,
workType: "ykt",
refer: location.href,
type: typeCode,
title: stem,
optionsText: labels.map(ppYktOptionLabel),
};
let answerData = null;
try {
answerData = await queryAnswer(question);
} catch (e) {
answerData = null;
}
if (answerData && answerData.code === 200) {
const raw = answerData.data && answerData.data.answer;
const answers = Array.isArray(raw) ? raw : [raw];
const ok = await ppYktWriteAnswers(item, typeCode, answers, PP_YKT_EXAM.options);
question.answer = answers;
question.source = (answerData.data && answerData.data.source) || "";
try {
questionStore.addQuestion(question);
} catch (e) {}
if (ok) {
filled += 1;
ppLogAnswerFilled(logStore.addLog, index + 1, {
source: question.source,
cost: answerData.data && answerData.data.cost,
num: answerData.data && answerData.data.num,
});
} else {
failed += 1;
ppLogAnswerFillFailed(logStore.addLog, index + 1, "答案未能写入页面");
}
} else if (isReviewFailed(answerData)) {
failed += 1;
question._reviewFailed = true;
question.answer = reviewFailedAnswers(answerData);
question.source = "error";
try {
questionStore.addQuestion(question);
} catch (e) {}
ppLogAnswerReviewFailed(logStore.addLog, answerData);
} else {
failed += 1;
ppLogAnswerQueryFailed(logStore.addLog, index + 1, (answerData && answerData.msg) || "未知错误");
}
await delay(0.3);
}
progressStore.update({
isPlaying: false,
questionIndex: handled,
percent: 100,
filledCount: filled,
fillRate: items.length ? Math.round(((filled + skipped) / items.length) * 100) : 0,
detail: `本次考试:填入 ${filled} / 跳过 ${skipped} / 共 ${items.length} 题`,
});
ppLogAnswerSummary(logStore.addLog, {
handled: items.length,
filled,
skipped,
failed,
total: items.length,
});
ppLogAnswerSubmitNote(logStore.addLog, "examManual");
} catch (error) {
logStore.addLog(`考试作答出错:${error && error.message ? error.message : error}`, "danger");
} finally {
window.__ppYktExamBusy = false;
}
};
const useYktCourseLogic = async () => {
const logStore = useLogStore();
const autoPlay = readYktParam(PP_KEY.VIDEO, "自动播放", true);
const yktMode = (() => {
const on = (key) => readYktParam(PP_KEY.MODE, key, false);
if (on(PP_KEY.ONLY_ANSWER)) return "answer";
if (on(PP_KEY.ONLY_VIDEO)) return "video";
if (on(PP_KEY.REVIEW)) return "review";
return "normal";
})();
const restudy = yktMode === "review";
const wantVideo = yktMode !== "answer";
const wantAnswer = yktMode !== "video";
const autoComment = readYktParam(PP_KEY.YKT_CHAPTER, "自动评论", false);
const autoSwitch = readYktParam(PP_KEY.YKT_CHAPTER, PP_KEY.AUTO_NEXT, true);
let yktRateWarned = false;
const readYktMaxRate = () => {
try {
const listEl = document.querySelector("xt-speedlist");
const text = listEl ? listEl.textContent || "" : "";
const rates = (text.match(/(\d+(?:\.\d+)?)\s*X/gi) || []).map((s) => parseFloat(s));
if (rates.length) return Math.max(...rates);
} catch (e) {}
return 2;
};
const computeEffectiveRate = () => {
const autoRate = readYktParam(PP_KEY.VIDEO, PP_KEY.AUTO_RATE, false);
const breakRate = readYktParam(PP_KEY.VIDEO, PP_KEY.BREAK_RATE, false);
const userRate = Number(readYktParam(PP_KEY.VIDEO, PP_KEY.RATE, 1.5)) || 1;
const menuMax = readYktMaxRate();
if (autoRate) return menuMax;
if (breakRate) return Math.min(8, Math.max(1, userRate));
return Math.min(menuMax, Math.max(1, userRate));
};
const studyTags = [];
if (wantVideo) studyTags.push("视频", "图文");
if (wantAnswer) studyTags.push("作业");
if (autoComment && yktMode !== "answer") studyTags.push("讨论");
const yktActiveTag = () => {
const active = yktJobs().find((el) => el.classList.contains("is-active"));
return active ? yktJobTag(active) : "";
};
if (restudy) {
logStore.addLog("复习模式:已完成的任务点也会重新学习", "primary");
}
if (location.pathname.includes("/v2/web/student-lesson-report")) {
logStore.addLog("课堂互动任务不支持自动学习,请切换到视频、课件等任务", "warning");
return;
}
logStore.addLog("检测到雨课堂学习页面", "primary");
logStore.addLog("正在解析任务点,请稍等5-10秒(如果长时间没有反应,请刷新页面)", "warning");
let ready = false;
for (let i = 0; i < 40; i++) {
if (document.querySelector(YKT_SEL.detail) || document.querySelector(YKT_SEL.pptSlide)) {
ready = true;
break;
}
await delay(0.5);
}
if (!ready) {
logStore.addLog("未检测到任务内容,请手动点击左侧章节后重试", "warning");
return;
}
logStore.addLog("等待页面加载完成,开始扫描任务点", "info");
await delay(1.5);
await yktExpandTree();
const yktExerciseDoc = () => {
try {
const frame = document.querySelector(YKT_SEL.exerciseFrame);
return frame && frame.contentDocument ? frame.contentDocument : null;
} catch (e) {
return null;
}
};
const yktPrepareFont = async (target, quiet) => {
if (!target) return false;
if (!ppFontDecryptAvailable()) {
if (!quiet) logStore.addLog("字体解密库未加载,雨课堂题目将不做解密(不影响其他功能)", "warning");
return false;
}
let hasCipher = false;
try {
hasCipher = !!target.querySelector(YKT_SEL.encryptedFont);
} catch (e) {}
if (!hasCipher) return true;
const ok = await ppYktPrepareFont(target);
if (!quiet) {
logStore.addLog(
ok ? "字体解密表已就绪,题目内容将按明文上传" : "字体解密表准备失败,题目可能带乱码(不影响提交)",
ok ? "success" : "warning",
);
}
return ok;
};
const visitedJobs = new Set();
const jobKey = (el) => `${yktJobTag(el)}|${yktJobTitle(el)}`;
const pickNext = () => {
const list = yktJobs();
const activeIndex = list.findIndex((el) => el.classList.contains("is-active"));
const usable = (el) => {
if (visitedJobs.has(jobKey(el))) return false;
const tag = yktJobTag(el);
if (!studyTags.includes(tag)) return false;
if (restudy) return tag !== "作业";
return !yktJobDone(el);
};
for (let i = activeIndex + 1; i < list.length; i += 1) {
if (usable(list[i])) return list[i];
}
for (let i = 0; i <= activeIndex && i < list.length; i += 1) {
if (usable(list[i])) return list[i];
}
return null;
};
for (let guard = 0; guard < 200; guard++) {
await yktPrepareFont(yktExerciseDoc() || document, true);
const activeJob = yktJobs().find((el) => el.classList.contains("is-active"));
const activeTag = yktActiveTag();
const exerciseDoc = yktExerciseDoc();
const skipActive =
(restudy && activeTag === "作业") ||
(!restudy && activeJob && studyTags.includes(yktJobTag(activeJob)) && yktJobDone(activeJob));
if (skipActive && activeJob) {
visitedJobs.add(jobKey(activeJob));
}
const forum = skipActive ? null : document.querySelector(YKT_SEL.forum);
if (skipActive) {
} else if (forum && autoComment && activeTag !== "作业") {
logStore.addLog("发现任务点:讨论,开始自动评论", "primary");
try {
await yktPostComment(forum, logStore);
} catch (e) {
logStore.addLog(`自动评论失败:${e.message}`, "warning");
}
} else if (document.querySelector(YKT_SEL.pptSlide) && activeTag !== "作业") {
logStore.addLog("发现课件,开始阅读", "primary");
await useYktPptRead(logStore);
} else if (activeTag === "图文") {
logStore.addLog("发现任务点:图文,已自动打开", "success");
await delay(1.2);
} else if (wantAnswer && (activeTag === "作业" || (!activeTag && exerciseDoc))) {
await useYktHomeworkLogic(exerciseDoc, logStore, (target) => yktPrepareFont(target, true));
await delay(1.5);
} else if (!wantVideo) {
logStore.addLog("仅答题模式:跳过视频与图文任务", "info");
} else {
const media = document.querySelector(YKT_SEL.media);
if (!media) {
logStore.addLog("当前任务点不是视频,跳过", "warning");
} else if (!autoPlay) {
logStore.addLog("自动播放已关闭,跳过视频任务", "warning");
} else {
const active = yktJobs().find((el) => el.classList.contains("is-active"));
logStore.addLog(`发现任务点:${active ? yktJobTitle(active) : "当前视频"}(视频),开始学习`, "primary");
const initialRate = computeEffectiveRate();
if (readYktParam(PP_KEY.VIDEO, PP_KEY.AUTO_RATE, false)) {
logStore.addLog(`自动倍速:使用播放器最大倍速 ${initialRate}x`, "info");
} else if (readYktParam(PP_KEY.VIDEO, PP_KEY.BREAK_RATE, false)) {
logStore.addLog(`突破倍速:强制 ${initialRate}x(上限 8x)`, "info");
} else if (readYktParam(PP_KEY.VIDEO, PP_KEY.MANUAL_RATE, false)) {
logStore.addLog(`手动倍速:${initialRate}x(最大 ${readYktMaxRate()}x)`, "info");
}
if (initialRate > 4 && !yktRateWarned) {
yktRateWarned = true;
logStore.addLog(`当前倍速 ${initialRate}x 偏高,可能被平台判定异常、学习记录被清空,请谨慎使用`, "warning");
}
const isDigitalHuman = !!document.querySelector(".digital-human-video-element-selector");
const reason = isDigitalHuman
? await yktPlayMediaCalm(media, { getRate: computeEffectiveRate, logStore })
: await yktPlayMedia(media, {
getRate: computeEffectiveRate,
title: (() => { try { const act = yktJobs().find((el) => el.classList.contains("is-active")); return act ? yktJobTitle(act) : ""; } catch (e) { return ""; } })(),
});
logStore.addLog(
reason === "ended" ? "播放完成:视频" : reason === "stalled" ? "播放超时,已跳过" : "播放中断:任务点已切换,继续下一节",
reason === "stalled" ? "warning" : "success",
);
}
}
if (activeJob) visitedJobs.add(jobKey(activeJob));
const next = pickNext();
if (!next) {
const rest = yktJobs().filter((el) => !yktJobDone(el));
if (!rest.length) {
logStore.addLog("本页任务点已全部完成", "success");
} else {
const brief = rest.slice(0, 3).map((el) => `${yktJobTag(el) || "其他"}·${yktJobTitle(el)}`).join("、");
logStore.addLog(
`本轮已走完一遍,剩余 ${rest.length} 个任务点未完成:${brief}${rest.length > 3 ? " 等" : ""}`,
"warning",
);
}
break;
}
if (!autoSwitch) {
logStore.addLog(`自动切换已关闭,请手动切换任务:${yktJobTitle(next)}`, "warning");
break;
}
next.scrollIntoView({ block: "center" });
next.click();
visitedJobs.add(jobKey(next));
logStore.addLog(`切换到下一节:${yktJobTitle(next)}`, "primary");
await afkWait(3000);
}
};
const ppPlatformOf = (url) => {
const u = String(url || "");
if (u.includes("chaoxing") || u.includes("xuexitong") || u.includes("xueyin")) return "cx";
if (u.includes("zhihuishu") || u.includes("polymas")) return "zhs";
if (u.includes("yuketang") || u.includes("gdufemooc")) return "ykt";
return "";
};
const ppPageFullyLoaded = () => {
try {
if (document.readyState !== "complete") return false;
const body = document.body;
if (!body || !body.children.length) return false;
const imgs = document.images || [];
for (let i = 0; i < imgs.length; i += 1) {
if (imgs[i] && imgs[i].complete === false) return false;
}
if (document.fonts && document.fonts.status === "loading") return false;
return true;
} catch (e) {
return true;
}
};
const ppWaitPageLoaded = (maxWaitMs = 10000) => new Promise((resolve) => {
let settled = false;
let timer = null;
let deadline = null;
const onLoad = () => {
if (ppPageFullyLoaded()) finish("loaded");
};
const finish = (reason) => {
if (settled) return;
settled = true;
if (timer) clearInterval(timer);
if (deadline) clearTimeout(deadline);
try {
document.removeEventListener("load", onLoad, true);
} catch (e) {}
resolve(reason);
};
if (ppPageFullyLoaded()) {
finish("immediate");
return;
}
try {
document.addEventListener("load", onLoad, true);
} catch (e) {}
timer = setInterval(() => {
if (ppPageFullyLoaded()) finish("loaded");
}, 100);
deadline = setTimeout(() => finish("timeout"), maxWaitMs);
});
const ppMuteOnce = (media) => {
if (!media || media.__ppMutedOnce) return;
media.__ppMutedOnce = true;
try { media.muted = true; } catch (e) {}
};
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("请不要多个脚本同时使用;如果脚本出现异常,请使用谷歌浏览器", "warning");
const useCxPracticeIframeWatcher = () => {
const logStore = useLogStore();
const practiceKey = "/page/quiz/stu/answerQuestion2";
const storageKey = (url) => `pp-opened-practice-iframe:${url}`;
const scan = () => {
try {
const iframe = Array.from(document.querySelectorAll("iframe")).find((frame) => {
const src = frame.getAttribute("src") || frame.src || "";
return src.includes(practiceKey);
});
if (!iframe) return;
const raw = iframe.getAttribute("src") || iframe.src || "";
let url = raw;
try { url = new URL(raw, window.location.href).href; } catch (e) {}
if (!url || sessionStorage.getItem(storageKey(url))) return;
sessionStorage.setItem(storageKey(url), "1");
window.open(url, "_blank");
logStore.addLog("检测到随堂练习 iframe,已尝试在新窗口打开", "primary");
} catch (e) {}
};
scan();
setInterval(scan, 1000);
};
const useCxExamNotesTipLogic = () => {
const logStore = useLogStore();
logStore.addLog("已进入考试说明页,请点击进入答题页面后使用", "primary");
};
const urlLogicPairs = [
{ keyword: "changjiang-exam.yuketang.cn", logic: useYktExamLogic },
{ keyword: "/v2/web/studentLog", logic: useYktCourseLogic },
{ keyword: "/v2/web/student-lesson-report", logic: useYktCourseLogic },
{ keyword: "/ai-workspace/lms-graph", logic: useYktCourseLogic },
{ keyword: "/m/v2/course/normalcourse/logs", logic: useYktCourseLogic },
{ keyword: "/pro/lms", logic: useYktCourseLogic },
{ keyword: "/v2/web/studentcards", logic: useYktCourseLogic },
{ keyword: "/v2/web/studentcard/", logic: useYktCourseLogic },
{ keyword: "studyvideoh5.zhihuishu.com", logic: useZhClassroomStudy },
{ keyword: "studyplush5.zhihuishu.com", logic: useZhClassroomStudy },
{ keyword: "studywisdomh5.zhihuishu.com", logic: useZhClassroomStudy },
{ keyword: "wisdom-mooc.zhihuishu.com", logic: useZhClassroomStudy },
{ keyword: "ai-smart-course-student-pro.zhihuishu.com", logic: useZhClassroomStudy },
{ keyword: "smartcoursestudent.zhihuishu.com", logic: useZhClassroomStudy },
{ keyword: "fusioncourseh5.zhihuishu.com", logic: useZhClassroomStudy },
{ keyword: "hike.zhihuishu.com", logic: useZhClassroomStudy },
{ keyword: "wenda.zhihuishu.com", logic: useZhClassroomStudy },
{ keyword: "hike-teaching-center.polymas.com", logic: useZhClassroomStudy },
{ keyword: "examloop.zhihuishu.com/exam/", logic: useZhsPaperAnswerLogic },
{ keyword: "studentexamcomh5.zhihuishu.com/studentReviewTestOrExam", logic: useZhsPaperAnswerLogic },
{ keyword: "smartcourseexam.zhihuishu.com/ReviewExam", logic: useZhsPaperAnswerLogic },
{ keyword: "fusioncourseh5.zhihuishu.com/exam", logic: useZhsPaperAnswerLogic },
{ keyword: "studywisdomh5.zhihuishu.com/exam", logic: useZhsPaperAnswerLogic },
{ keyword: "wisdom-mooc.zhihuishu.com/exam", logic: useZhsPaperAnswerLogic },
{ keyword: "ai-smart-course-student-pro.zhihuishu.com/masteryHistory/", logic: useZhsPaperAnswerLogic },
{ keyword: "ai-smart-course-student-pro.zhihuishu.com/point/", logic: useZhsPaperAnswerLogic },
{ keyword: "smartcoursestudent.zhihuishu.com/masteryHistory/", logic: useZhsPaperAnswerLogic },
{ keyword: "smartcoursestudent.zhihuishu.com/point/", logic: useZhsPaperAnswerLogic },
{ keyword: "zhihuishu.com/atHomeworkExam/", logic: useZhsPaperAnswerLogic },
{ keyword: "studentexamcomh5.zhihuishu.com", logic: useZhsPaperAnswerLogic },
{ keyword: "/mycourse/studentstudy", logic: useCxChapterLogic },
{ keyword: "/mooc2/work/dowork", logic: useCxWorkLogic },
{ keyword: "exam/preview", logic: useCxExamPreviewLogic },
{ keyword: "/examcode/examnotes", logic: useCxExamNotesTipLogic },
{ keyword: "/exam-ans/", logic: useCxExamLogic },
{ keyword: "/exam/test", logic: useCxExamLogic },
{ keyword: "/work/index", logic: useCxWorkLogic },
{ keyword: "/work/doTest", logic: useCxWorkLogic },
{ keyword: "/work/calcAnswer", logic: useCxWorkLogic },
{ keyword: "/knowledge/start", logic: useCxChapterLogic },
{ keyword: "/ztnodedetailcontroller/visitnodedetail", logic: useCxAutoReadLogic },
{ keyword: "/course/", logic: useCxAutoReadLogic },
{ keyword: "/plaza", logic: useCxPlazaTipLogic },
{
keyword: "mycourse/stu?courseid",
logic: useCxPracticeIframeWatcher
},
{ keyword: "/page/active/stuActiveList", logic: useCxPracticeIframeWatcher },
{ keyword: "/stuExamWeb.html", logic: useZhsAnswerLogic },
{ keyword: "answerQuestion2", logic: useStuActiveLogic }
];
const executeLogicByUrl = (url22) => {
if (url22.includes('out_link.shtml')) {
try {
const burl = new URL(url22).searchParams.get('burl');
if (burl) {
window.location.replace(burl);
return;
}
} catch (e) {}
isShow.value = false;
return;
}
let matchedRule = null;
for (const rule of urlLogicPairs) {
if (!url22.includes(rule.keyword)) continue;
if (matchedRule === null || rule.keyword.length > matchedRule.keyword.length) matchedRule = rule;
}
if (matchedRule) {
isShow.value = true;
const modeLabel = getCxModeLabel();
if (modeLabel) logStore.addLog(modeLabel, "info");
startAfkHeartbeat();
const _ppRunLogic = () => {
matchedRule.logic();
};
ppWaitPageLoaded(10000).then((reason) => {
if (reason === "timeout") {
logStore.addLog("页面资源 10 秒内未加载完,先继续执行", "warning");
}
_ppRunLogic();
});
return;
}
if (!url22.includes('/exam-ans/')) {
_GM_setValue('pp_exam_sequential', false);
}
isShow.value = false;
};
executeLogicByUrl(url2);
const emit = __emit;
emit("customEvent", isShow.value);
const tabs = [
{
label: "主页",
full: "主页信息",
id: "main-log",
icon: "home",
component: ScriptHome,
props: { "log-list": logStore.logList, "server-config": CURRENT_SERVER_CONFIG }
},
{
label: "记录",
full: "答题记录",
id: "question-record",
icon: "list",
component: QuestionTable,
props: {
"question-list": questionStore.questionList,
"on-go-settings": () => switchTab("config-panel")
}
},
{
label: "配置",
full: "脚本配置",
id: "config-panel",
icon: "sliders",
component: ScriptSetting,
props: { "global-config": configStore }
},
{
label: "AI问答",
full: "AI 问答",
id: "ai-chat",
icon: "ai",
iconNode: () => aiChatIcon("ai", 17),
component: AiChatPanel,
props: {}
},
{
label: "说明",
full: "脚本说明",
id: "script-description",
icon: "book",
component: ScriptDescription,
props: {}
},
{
label: "作者",
full: "作者的话",
id: "author-words",
icon: "heart",
component: AuthorWords,
props: {}
},
{
label: "查询",
full: "使用查询",
id: "token-usage",
icon: "search",
component: TokenUsagePanel,
props: { active: () => activeTab.value === "token-usage" }
},
{
label: "反馈",
full: "意见反馈",
id: "feedback-panel",
icon: "message",
component: FeedbackPanel,
props: {}
}
];
const activeTab = vue.ref("main-log");
const ppfxPaneShift = vue.ref("6px");
const ppfxRailRef = vue.ref(null);
let ppfxInkRO = null;
const switchTab = (tabId) => {
const ppfxFrom = tabs.findIndex((tab) => tab.id === activeTab.value);
const ppfxTo = tabs.findIndex((tab) => tab.id === tabId);
if (ppfxFrom >= 0 && ppfxTo >= 0 && ppfxFrom !== ppfxTo) {
ppfxPaneShift.value = ppfxTo > ppfxFrom ? "7px" : "-7px";
}
activeTab.value = tabId;
};
const ppfxSyncInk = () => {
const rail = ppfxRailRef.value;
if (!rail) return;
const ink = rail.querySelector(".pp-rail-ink");
if (!ink) return;
const item = rail.querySelector(".pp-rail-item.active");
if (!item || !item.offsetHeight) {
ink.classList.remove("ppfx-live");
return;
}
const ppfxInset = 14;
ink.style.height = Math.max(6, item.offsetHeight - ppfxInset * 2) + "px";
ink.style.transform = "translateY(" + (item.offsetTop + ppfxInset) + "px)";
if (!ink.classList.contains("ppfx-live")) {
ink.classList.add("ppfx-live");
ink.getBoundingClientRect();
ink.classList.add("ppfx-slide");
}
};
vue.watch(activeTab, ppfxSyncInk, { flush: "post" });
vue.onMounted(() => {
vue.nextTick(ppfxSyncInk);
window.addEventListener("resize", ppfxSyncInk);
if (typeof ResizeObserver === "function" && ppfxRailRef.value) {
ppfxInkRO = new ResizeObserver(ppfxSyncInk);
ppfxInkRO.observe(ppfxRailRef.value);
}
});
vue.onUnmounted(() => {
window.removeEventListener("resize", ppfxSyncInk);
if (ppfxInkRO) {
ppfxInkRO.disconnect();
ppfxInkRO = null;
}
});
return () => vue.h("div", { class: "pp-shell" }, [
vue.h("nav", { class: "pp-rail", ref: ppfxRailRef }, [
vue.h("div", { class: "pp-rail-ink", "aria-hidden": "true" }),
...tabs.map((tab) => {
const warn = tab.id === "config-panel" && !configStore.tokenVerified;
return vue.h("button", {
type: "button",
key: tab.id,
class: ["pp-rail-item", activeTab.value === tab.id ? "active" : "", warn ? "pp-warn" : ""],
title: tab.full,
onClick: () => switchTab(tab.id)
}, [
typeof tab.iconNode === "function" ? tab.iconNode() : ppIcon(tab.icon, 17),
vue.h("span", null, tab.label)
]);
}),
vue.h("div", { class: "pp-rail-spacer" })
]),
vue.h("div", { class: "pp-main", style: { "--ppfx-pane-shift": ppfxPaneShift.value } }, tabs.map((tab) => vue.withDirectives(
vue.h("div", { class: "config-panel", key: tab.id }, [
vue.h(tab.component, tab.props || {})
]),
[[vue.vShow, activeTab.value === tab.id]]
)))
]);
}
});
const _sfc_main$1 = vue.defineComponent({
name: "Layout",
setup() {
const isShow = vue.ref(false);
const configStore = useConfigStore();
const progressStore = useProgressStore();
const { autoMode } = pinia.storeToRefs(configStore);
const viewport = vue.ref({ w: window.innerWidth, h: window.innerHeight });
const isDragging = vue.ref(false);
const SIZE = { floatW: 388, floatH: 520, orb: 52, dockW: 360, handleW: 44, handleH: 120, margin: 8, minTop: 56 };
const RING = 2 * Math.PI * 23;
const HANDLE_RING = 2 * Math.PI * 10;
let saveTimer = null;
const saveConfig = () => {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
_GM_setValue("config", JSON.stringify(configStore));
saveTimer = null;
}, 300);
};
const saveConfigImmediate = () => {
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}
_GM_setValue("config", JSON.stringify(configStore));
};
let ppfxConfigWatchStop = null;
const ppfxResumeConfigWatch = () => {
if (ppfxConfigWatchStop) {
ppfxConfigWatchStop();
ppfxConfigWatchStop = null;
}
ppfxConfigWatchStop = vue.watch(configStore, saveConfig, { deep: true });
};
const ppfxPauseConfigWatch = () => {
if (!ppfxConfigWatchStop) return;
ppfxConfigWatchStop();
ppfxConfigWatchStop = null;
};
ppfxResumeConfigWatch();
const dockMode = vue.computed({
get: () => !!configStore.dockMode,
set: (value) => { configStore.dockMode = value; }
});
const platformLabel = vue.computed(() => {
const key = configStore.platformName;
return key === "cx" ? "学习通" : key === "zhs" ? "智慧树" : key === "ykt" ? "雨课堂" : "通用";
});
const ensureMinTop = (value) => {
const num = parseInt(value, 10);
return (isNaN(num) || num < SIZE.minTop) ? SIZE.minTop + "px" : value;
};
const uiScale = vue.computed(() => {
const value = Number(configStore.uiScale);
return [100, 125, 150].includes(value) ? value : 100;
});
const scaleFactor = vue.computed(() => uiScale.value / 100);
const scaledPosition = (value) => {
const num = parseFloat(value);
return ((Number.isFinite(num) ? num : 0) / scaleFactor.value) + "px";
};
const panelStyle = vue.computed(() => {
const scale = scaleFactor.value;
const transition = undefined;
if (dockMode.value) {
if (configStore.isMinus) {
return {
left: Math.max(0, viewport.value.w / scale - SIZE.handleW) + "px",
top: Math.max(0, (viewport.value.h / scale - SIZE.handleH) / 2) + "px",
width: SIZE.handleW + "px",
height: SIZE.handleH + "px",
zoom: scale,
transition
};
}
return {
left: Math.max(0, viewport.value.w / scale - SIZE.dockW) + "px",
top: "0px",
width: SIZE.dockW + "px",
height: (viewport.value.h / scale) + "px",
maxWidth: (viewport.value.w / scale) + "px",
zoom: scale,
transition
};
}
const position = configStore.position || { x: "50px", y: "50px" };
if (configStore.isMinus) {
return {
left: scaledPosition(position.x),
top: scaledPosition(ensureMinTop(position.y)),
width: SIZE.orb + "px",
height: SIZE.orb + "px",
zoom: scale,
transition
};
}
return {
left: scaledPosition(position.x),
top: scaledPosition(ensureMinTop(position.y)),
width: SIZE.floatW + "px",
height: SIZE.floatH + "px",
maxWidth: ((viewport.value.w - SIZE.margin * 2) / scale) + "px",
maxHeight: ((viewport.value.h - SIZE.minTop - SIZE.margin) / scale) + "px",
zoom: scale,
transition
};
});
const getMainPage = () => {
const host = document.getElementById("chaoxing-helper-root");
const root = host && host.shadowRoot ? host.shadowRoot : document;
return root.querySelector(".main-page");
};
let clampTimer = null;
const clampFloat = (delay = 260) => {
if (dockMode.value) return;
if (clampTimer) clearTimeout(clampTimer);
clampTimer = setTimeout(() => {
clampTimer = null;
if (dockMode.value) return;
const root = getMainPage();
if (!root) return;
const rect = root.getBoundingClientRect();
const current = configStore.position || {};
let x = parseFloat(current.x);
let y = parseFloat(current.y);
if (isNaN(x)) x = rect.left;
if (isNaN(y)) y = rect.top;
const maxX = Math.max(SIZE.margin, viewport.value.w - rect.width - SIZE.margin);
const maxY = Math.max(SIZE.minTop, viewport.value.h - rect.height - SIZE.margin);
configStore.position.x = Math.min(Math.max(SIZE.margin, x), maxX) + "px";
configStore.position.y = Math.min(Math.max(SIZE.minTop, y), maxY) + "px";
}, delay);
};
const MORPH = { out: 90, box: 180, in: 60, orbDelay: 100, orb: 180, tail: 40 };
const morphPhase = vue.ref("idle");
let morphTimer = null;
const morphStop = () => {
if (morphTimer) {
clearTimeout(morphTimer);
morphTimer = null;
}
};
const morphLater = (delay, run) => {
morphStop();
morphTimer = setTimeout(() => {
morphTimer = null;
run();
}, delay);
};
const morphIdle = () => {
morphStop();
morphPhase.value = "idle";
};
const morphMs = (value) => {
try {
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return 0;
} catch (error) {}
return value;
};
const morphClass = vue.computed(() => morphPhase.value === "idle" ? "" : "ppfx-" + morphPhase.value);
const clampFloatBox = (width, height) => {
const x = parseFloat(configStore.position?.x);
const y = parseFloat(configStore.position?.y);
configStore.position.x = Math.min(Math.max(SIZE.margin, isNaN(x) ? SIZE.margin : x), Math.max(SIZE.margin, viewport.value.w - width - SIZE.margin)) + "px";
configStore.position.y = Math.min(Math.max(SIZE.minTop, isNaN(y) ? SIZE.minTop : y), Math.max(SIZE.minTop, viewport.value.h - height - SIZE.margin)) + "px";
};
const beginCollapse = () => {
morphStop();
morphPhase.value = "closing";
morphLater(morphMs(MORPH.out), () => {
if (morphPhase.value !== "closing") return;
morphPhase.value = "settling";
configStore.isMinus = true;
if (!dockMode.value) clampFloat();
morphLater(morphMs(MORPH.orbDelay + MORPH.orb + MORPH.tail), () => {
if (morphPhase.value === "settling") morphPhase.value = "idle";
});
});
};
const beginExpand = () => {
morphStop();
if (clampTimer) {
clearTimeout(clampTimer);
clampTimer = null;
}
if (!dockMode.value) clampFloatBox(SIZE.floatW, SIZE.floatH);
morphPhase.value = "opening";
configStore.isMinus = false;
morphLater(morphMs(MORPH.in + MORPH.box + MORPH.tail), () => {
if (morphPhase.value === "opening") morphPhase.value = "idle";
});
};
const setMinimized = (value) => {
const next = !!value;
if (next) {
if (configStore.isMinus || morphPhase.value === "closing") return;
beginCollapse();
return;
}
if (configStore.isMinus) {
beginExpand();
return;
}
if (morphPhase.value === "closing") morphIdle();
};
const expand = () => setMinimized(false);
const collapse = () => setMinimized(true);
const toggleMinus = () => setMinimized(!(configStore.isMinus || morphPhase.value === "closing"));
const toggleDock = () => {
morphStop();
if (clampTimer) {
clearTimeout(clampTimer);
clampTimer = null;
}
const next = !dockMode.value;
const wasMinus = configStore.isMinus;
if (!next) clampFloatBox(SIZE.floatW, SIZE.floatH);
configStore.dockMode = next;
configStore.isMinus = false;
morphPhase.value = wasMinus ? "opening" : (next ? "docking" : "undocking");
morphLater(morphMs(MORPH.box + MORPH.tail), () => {
const phase = morphPhase.value;
if (phase === "opening" || phase === "docking" || phase === "undocking") morphPhase.value = "idle";
});
saveConfigImmediate();
};
const COACH = { width: 224, height: 56, gap: 12, margin: 10 };
const COACH_DURATION = 3000;
const COACH_STORE_KEYS = {
orb: "pp_coach_last_shown_orb",
dock: "pp_coach_last_shown_dock"
};
const coachVisible = vue.ref(false);
const coachStyle = vue.ref({});
const coachSide = vue.ref("right");
const coachTitle = vue.computed(() => dockMode.value ? "脚本停靠在右侧" : "脚本在这里");
const coachText = vue.computed(() => dockMode.value ? "点击胶囊展开面板" : "点击悬浮球展开网课小助手");
let coachTimer = null;
let coachShowTimer = null;
let coachMoveTimer = null;
const coachModeState = {
orb: { shown: false },
dock: { shown: false }
};
const coachDateKey = () => {
const now = new Date();
return now.getFullYear() + "-" +
String(now.getMonth() + 1).padStart(2, "0") + "-" +
String(now.getDate()).padStart(2, "0");
};
const coachSeenToday = (mode) => {
try {
if (typeof _GM_getValue !== "function") return false;
return _GM_getValue(COACH_STORE_KEYS[mode], "") === coachDateKey();
} catch (error) {
return false;
}
};
const markCoachSeen = (mode) => {
try {
if (typeof _GM_setValue !== "function") return;
_GM_setValue(COACH_STORE_KEYS[mode], coachDateKey());
} catch (error) {
}
};
const clearCoachTimers = () => {
if (coachTimer) {
clearTimeout(coachTimer);
coachTimer = null;
}
if (coachShowTimer) {
clearTimeout(coachShowTimer);
coachShowTimer = null;
}
if (coachMoveTimer) {
clearTimeout(coachMoveTimer);
coachMoveTimer = null;
}
};
const hideCoach = () => {
coachVisible.value = false;
clearCoachTimers();
};
const placeCoach = () => {
const root = getMainPage();
if (!root) return;
const rect = root.getBoundingClientRect();
const { width, height, gap, margin } = COACH;
const spaceRight = viewport.value.w - rect.right;
const spaceLeft = rect.left;
let side = "right";
let left = rect.right + gap;
if (spaceRight < width + gap && spaceLeft >= width + gap) {
side = "left";
left = rect.left - width - gap;
} else if (spaceRight < width + gap && spaceLeft < width + gap) {
side = "bottom";
left = Math.min(Math.max(margin, rect.left), Math.max(margin, viewport.value.w - width - margin));
}
let top = rect.top + rect.height / 2 - height / 2;
top = Math.min(Math.max(margin, top), Math.max(margin, viewport.value.h - height - margin));
coachSide.value = side;
coachStyle.value = { left: Math.round(left) + "px", top: Math.round(top) + "px" };
};
const schedulePlaceCoach = (delay) => {
if (coachMoveTimer) clearTimeout(coachMoveTimer);
coachMoveTimer = setTimeout(() => {
coachMoveTimer = null;
placeCoach();
}, delay || 280);
};
const showCoach = () => {
const mode = dockMode.value ? "dock" : "orb";
if (coachModeState[mode].shown || coachShowTimer || !isShow.value || !configStore.isMinus) return;
if (coachSeenToday(mode)) {
coachModeState[mode].shown = true;
return;
}
coachShowTimer = setTimeout(() => {
coachShowTimer = null;
if (!isShow.value || !configStore.isMinus) return;
if ((dockMode.value ? "dock" : "orb") !== mode) {
showCoach();
return;
}
coachModeState[mode].shown = true;
markCoachSeen(mode);
placeCoach();
coachVisible.value = true;
coachTimer = setTimeout(hideCoach, COACH_DURATION);
}, 200);
};
vue.watch(isShow, (value) => {
if (value) showCoach();
else hideCoach();
}, { immediate: true });
vue.watch([() => configStore.isMinus, () => dockMode.value], ([minus], [prevMinus]) => {
if (!minus) {
hideCoach();
return;
}
if (!prevMinus) {
showCoach();
return;
}
if (coachVisible.value) schedulePlaceCoach(280);
});
let dragRoot = null;
let offsetX = 0;
let offsetY = 0;
let startX = 0;
let startY = 0;
let moved = false;
let rafId = 0;
let pendingPos = null;
let ppfxDragW = 0;
let ppfxDragH = 0;
const flushPosition = () => {
rafId = 0;
if (!pendingPos) return;
configStore.position.x = pendingPos.x + "px";
configStore.position.y = pendingPos.y + "px";
pendingPos = null;
};
const onDrag = (event) => {
if (!isDragging.value || !dragRoot || dockMode.value) return;
event.preventDefault();
if (Math.abs(event.clientX - startX) > 3 || Math.abs(event.clientY - startY) > 3) moved = true;
const x = Math.min(Math.max(SIZE.margin, event.clientX - offsetX), viewport.value.w - ppfxDragW - SIZE.margin);
const y = Math.min(Math.max(SIZE.minTop, event.clientY - offsetY), viewport.value.h - ppfxDragH - SIZE.margin);
dragRoot.style.left = (x / scaleFactor.value) + "px";
dragRoot.style.top = (y / scaleFactor.value) + "px";
pendingPos = { x, y };
if (!rafId) rafId = requestAnimationFrame(flushPosition);
};
const endDrag = () => {
if (!isDragging.value) return;
isDragging.value = false;
document.removeEventListener("pointermove", onDrag);
document.removeEventListener("pointerup", endDrag);
document.removeEventListener("pointercancel", endDrag);
if (rafId) {
cancelAnimationFrame(rafId);
rafId = 0;
}
flushPosition();
const wasMinimized = configStore.isMinus;
dragRoot = null;
if (!moved && wasMinimized) expand();
ppfxResumeConfigWatch();
saveConfigImmediate();
};
const startDrag = (event) => {
if (dockMode.value) return;
if (event.button !== undefined && event.button !== 0) return;
if (event.target && event.target.closest && event.target.closest(".pp-no-drag")) return;
if (morphPhase.value === "closing") morphIdle();
hideCoach();
const root = getMainPage();
if (!root) return;
const rect = root.getBoundingClientRect();
dragRoot = root;
ppfxDragW = rect.width;
ppfxDragH = rect.height;
offsetX = event.clientX - rect.left;
offsetY = event.clientY - rect.top;
startX = event.clientX;
startY = event.clientY;
moved = false;
isDragging.value = true;
ppfxPauseConfigWatch();
document.addEventListener("pointermove", onDrag, { passive: false });
document.addEventListener("pointerup", endDrag);
document.addEventListener("pointercancel", endDrag);
};
let hoverTimer = null;
const handleEnter = () => {
if (hoverTimer) {
clearTimeout(hoverTimer);
hoverTimer = null;
}
if (autoMode.value && (configStore.isMinus || morphPhase.value === "closing") && !isDragging.value) expand();
};
const handleLeave = () => {
if (!autoMode.value || configStore.isMinus || isDragging.value || coachVisible.value) return;
const host = document.getElementById("chaoxing-helper-root");
const active = host && host.shadowRoot ? host.shadowRoot.activeElement : null;
if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA")) return;
hoverTimer = setTimeout(() => {
hoverTimer = null;
collapse();
}, 2500);
};
let resizeTimer = null;
let ppfxResizeRaf = 0;
const ppfxApplyViewport = () => {
ppfxResizeRaf = 0;
viewport.value = { w: window.innerWidth, h: window.innerHeight };
if (coachVisible.value) schedulePlaceCoach(80);
};
const handleResize = () => {
if (!ppfxResizeRaf) ppfxResizeRaf = requestAnimationFrame(ppfxApplyViewport);
if (resizeTimer) clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
resizeTimer = null;
if (!dockMode.value) clampFloat();
}, 180);
};
const handleKeydown = (event) => {
if (event.key !== "Escape" || configStore.isMinus) return;
const tag = event.target && event.target.tagName;
if (tag === "INPUT" || tag === "TEXTAREA") return;
collapse();
};
const handleWindowBlur = () => {
if (isDragging.value) endDrag();
};
vue.onMounted(() => {
window.addEventListener("resize", handleResize);
window.addEventListener("keydown", handleKeydown);
window.addEventListener("blur", handleWindowBlur);
setTimeout(checkScriptUpdate, 800);
if (!dockMode.value) clampFloat();
});
vue.onUnmounted(() => {
window.removeEventListener("resize", handleResize);
window.removeEventListener("keydown", handleKeydown);
window.removeEventListener("blur", handleWindowBlur);
document.removeEventListener("pointermove", onDrag);
document.removeEventListener("pointerup", endDrag);
document.removeEventListener("pointercancel", endDrag);
if (hoverTimer) clearTimeout(hoverTimer);
if (resizeTimer) clearTimeout(resizeTimer);
if (clampTimer) {
clearTimeout(clampTimer);
clampTimer = null;
}
if (ppfxResizeRaf) cancelAnimationFrame(ppfxResizeRaf);
if (ppfxConfigWatchStop) {
ppfxConfigWatchStop();
ppfxConfigWatchStop = null;
}
clearCoachTimers();
morphStop();
});
const renderHeader = () => {
const Switch = vue.resolveComponent("el-switch");
return vue.h("header", {
class: "pp-head",
onPointerdown: startDrag
}, [
vue.h("div", { class: "pp-brand" }, [
vue.h(PpLogo, { size: 30 }),
vue.h("div", { class: "pp-titles" }, [
vue.h("div", { class: "pp-title" }, "网课小助手"),
vue.h("div", { class: "pp-sub" }, [
vue.h("b", null, platformLabel.value),
vue.h("i", null, "/"),
vue.h("span", null, "v" + configStore.version),
PP_UPDATE_STATE.available ? vue.h("button", {
type: "button",
class: "pp-update-badge",
title: "发现新版本 v" + PP_UPDATE_STATE.latestVersion + ",点击更新",
onPointerdown: (event) => event.stopPropagation(),
onClick: (event) => {
event.stopPropagation();
window.open(PP_UPDATE_STATE.updateUrl, "_blank", "noopener,noreferrer");
}
}, "有新版本") : null
])
])
]),
vue.h("div", {
class: "pp-head-right pp-no-drag",
onPointerdown: (event) => event.stopPropagation()
}, [
vue.h("span", {
class: ["pp-live", progressStore.isPlaying ? "on" : ""],
title: progressStore.isPlaying ? "脚本正在运行" : "等待任务"
}, [
vue.h("span", { class: ["pp-dot", progressStore.isPlaying ? "on" : ""] }),
progressStore.isPlaying ? "运行中" : "待机"
]),
ppIconBtn({
icon: "sidebar",
tip: dockMode.value ? "取消贴边停靠" : "贴边停靠",
active: dockMode.value,
onClick: toggleDock
}),
vue.h("span", { class: "pp-auto", title: "自动最小化" }, [
vue.h(Switch, {
modelValue: autoMode.value,
size: "small",
"onUpdate:modelValue": (value) => { autoMode.value = value; }
})
]),
ppIconBtn({
icon: dockMode.value ? "chevronRight" : "chevronDown",
tip: "收起",
onClick: toggleMinus
})
])
]);
};
const renderOrb = () => {
const percent = Math.min(100, Math.max(0, Number(progressStore.percent) || 0));
return vue.h("div", {
class: ["pp-orb", coachVisible.value ? "pp-attention" : ""],
title: "展开网课小助手",
role: "button",
tabindex: 0,
"aria-label": "展开网课小助手",
onPointerdown: startDrag,
onKeydown: (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
expand();
}
}
}, [
vue.h("svg", { class: "pp-orb-ring", viewBox: "0 0 52 52" }, [
vue.h("circle", { class: "bg", cx: 26, cy: 26, r: 23 }),
vue.h("circle", {
class: "fg",
cx: 26,
cy: 26,
r: 23,
style: {
strokeDasharray: RING.toFixed(1),
strokeDashoffset: (RING * (1 - percent / 100)).toFixed(1)
}
})
]),
vue.h(PpLogo, { size: 28 }),
vue.h("span", { class: ["pp-orb-dot", progressStore.isPlaying ? "on" : ""] })
]);
};
const renderHandle = () => {
const percent = Math.min(100, Math.max(0, Number(progressStore.percent) || 0));
return vue.h("div", {
class: ["pp-handle", coachVisible.value ? "pp-attention" : ""],
title: "展开网课小助手",
role: "button",
tabindex: 0,
"aria-label": "展开网课小助手",
onClick: expand,
onKeydown: (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
expand();
}
}
}, [
vue.h("span", { class: "pp-handle-logo" }, vue.h(PpLogo, { size: 26 })),
vue.h("svg", { class: "pp-handle-ring", viewBox: "0 0 24 24" }, [
vue.h("circle", { class: "bg", cx: 12, cy: 12, r: 10 }),
vue.h("circle", {
class: "fg",
cx: 12,
cy: 12,
r: 10,
style: {
strokeDasharray: HANDLE_RING.toFixed(1),
strokeDashoffset: (HANDLE_RING * (1 - percent / 100)).toFixed(1)
}
})
]),
vue.h("span", { class: ["pp-handle-dot", progressStore.isPlaying ? "on" : ""] }),
vue.h("span", { class: "pp-handle-divider" }),
vue.h("span", { class: "pp-handle-caret" }, ppIcon("chevronLeft", 12))
]);
};
const renderCoach = () => {
if (!coachVisible.value || !isShow.value) return null;
return vue.h("div", {
class: ["pp-coach", "side-" + coachSide.value],
style: coachStyle.value,
role: "status",
"aria-live": "polite"
}, [
vue.h(PpLogo, { size: 24 }),
vue.h("div", { class: "pp-coach-body" }, [
vue.h("b", null, coachTitle.value),
vue.h("span", null, coachText.value)
]),
vue.h("button", {
type: "button",
class: "pp-coach-close",
title: "关闭提示",
"aria-label": "关闭提示",
onClick: hideCoach
}, "×")
]);
};
return (_ctx, _cache) => [
vue.withDirectives(vue.h("div", {
class: [
"main-page",
dockMode.value ? "pp-dock" : "",
configStore.isMinus ? (dockMode.value ? "pp-handle-mode" : "pp-min") : "",
isDragging.value ? "pp-dragging" : "",
morphClass.value,
morphPhase.value !== "idle" ? "ppfx-morphing" : ""
],
style: panelStyle.value,
onMouseenter: handleEnter,
onMouseleave: handleLeave
}, [
vue.withDirectives(renderHeader(), [[vue.vShow, !configStore.isMinus]]),
vue.withDirectives(vue.h("div", { class: "pp-body" }, [
vue.h(_sfc_main$3, {
onCustomEvent: (value) => { isShow.value = value; }
})
]), [[vue.vShow, !configStore.isMinus]]),
configStore.isMinus ? (dockMode.value ? renderHandle() : renderOrb()) : null
]), [[vue.vShow, isShow.value]]),
renderCoach()
];
}
});
const _sfc_main = vue.defineComponent({
__name: "App",
setup(__props) {
const configStore = useConfigStore();
const url2 = window.location.href;
const detectedPlatform = ppPlatformOf(url2);
if (detectedPlatform) configStore.platformName = detectedPlatform;
return (_ctx, _cache) => {
return vue.openBlock(), vue.createBlock(_sfc_main$1);
};
}
});
const layoutCss = LAYOUT_CSS;
const epCss = _GM_getResourceText("ElementPlusStyle") || "";
const _PP_STYLE_SHEETS = (() => {
try {
if (typeof CSSStyleSheet === "function" && typeof document.adoptedStyleSheets !== "undefined") {
const epSheet = new CSSStyleSheet();
epSheet.replaceSync(epCss);
const layoutSheet = new CSSStyleSheet();
layoutSheet.replaceSync(layoutCss);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, epSheet];
return { epSheet, layoutSheet };
}
} catch (e) {}
if (typeof GM_addStyle === "function") {
GM_addStyle(epCss);
} else {
const styleFallback = document.createElement("style");
styleFallback.textContent = epCss;
document.head.append(styleFallback);
}
return null;
})();
const _ppInjectShadowStyles = (shadow) => {
if (_PP_STYLE_SHEETS) {
try {
shadow.adoptedStyleSheets = [_PP_STYLE_SHEETS.epSheet, _PP_STYLE_SHEETS.layoutSheet];
return;
} catch (e) {}
}
const styleEl = document.createElement("style");
styleEl.textContent = epCss;
shadow.appendChild(styleEl);
const styleEl2 = document.createElement("style");
styleEl2.textContent = layoutCss;
shadow.appendChild(styleEl2);
};
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 _ppShadowRoots = new WeakMap();
const hookAttachShadow = () => {
try {
const proto = _unsafeWindow.Element && _unsafeWindow.Element.prototype;
if (!proto || proto.__ppShadowHooked) return;
const origin = proto.attachShadow;
proto.attachShadow = function(init) {
const root = origin.apply(this, [init]);
try {
_ppShadowRoots.set(this, root);
this.__ppShadowRoot = root;
} catch (e) {}
return root;
};
proto.__ppShadowHooked = true;
} catch (e) {}
};
const readRecordedShadow = (scopeEl) => {
if (!scopeEl) return null;
try {
const list = [scopeEl, ...scopeEl.querySelectorAll("*")];
for (const el of list) {
const root = _ppShadowRoots.get(el);
if (root) return root;
}
} catch (e) {}
return null;
};
const ZHS_BRIDGE_SOURCE = `(function () {
if (window.__ppZhsBridge) return;
window.__ppZhsBridge = true;
try {
var originCall = Function.prototype.call;
Function.prototype.call = function () {
var args = arguments;
var result = originCall.apply(this, args);
try {
var mod = args[0];
var exp = args[1];
if (mod && mod.a && mod.a.version === "2.5.0" && exp && exp.exports && exp.exports.a &&
typeof exp.exports.a.install === "function" && !exp.exports.a.__ppWrapped) {
var install = exp.exports.a.install;
exp.exports.a.install = function () {
var a = arguments;
try {
a[0].mixin({
mounted: function () {
try { this.$el.__Ivue__ = this; } catch (e) {}
}
});
} catch (e) {}
return install.apply(this, a);
};
exp.exports.a.__ppWrapped = true;
}
} catch (e) {}
return result;
};
} catch (e) {}
var roots = (window.__ppShadowRoots = []);
var wrapAttach = function (holder) {
try {
if (!holder || typeof holder.AttachShadow !== "function" || holder.__ppWrapped) return holder;
var orig = holder.AttachShadow;
holder.AttachShadow = function (init) {
var root = orig.apply(this, arguments);
try {
roots.push({ host: this, root: root });
this.__ppShadowRoot = root;
} catch (e) {}
return root;
};
holder.__ppWrapped = true;
} catch (e) {}
return holder;
};
try {
var cur = window.originFunction;
cur = wrapAttach(cur);
Object.defineProperty(window, "originFunction", {
configurable: true,
get: function () { return cur; },
set: function (v) { cur = wrapAttach(v); }
});
} catch (e) {}
try { wrapAttach(window.originFunction); } catch (e) {}
try {
var proto = Element.prototype;
var protoAttach = proto.attachShadow;
proto.attachShadow = function () {
var root = protoAttach.apply(this, arguments);
try { roots.push({ host: this, root: root }); this.__ppShadowRoot = root; } catch (e) {}
return root;
};
} catch (e) {}
setInterval(function () {
try {
var qs = document.querySelectorAll(".examPaper_subject");
for (var i = 0; i < qs.length; i++) {
var q = qs[i];
var scope = q.querySelector(".subject_describe") || q.querySelector(".smallStem_describe");
var sd = null;
if (scope) {
var all = [scope].concat([].slice.call(scope.querySelectorAll("*")));
for (var j = 0; j < all.length; j++) {
var el = all[j];
if (el.__ppShadowRoot) { sd = el.__ppShadowRoot; break; }
var vm = el.__Ivue__;
var vmSd = vm && vm._data && vm._data.shadowDom;
if (vmSd && vmSd.innerHTML) { sd = vmSd; break; }
}
}
if (!sd || !sd.innerHTML) continue;
var html = sd.innerHTML;
if (q.getAttribute("data-pp-stem") !== html) q.setAttribute("data-pp-stem", html);
}
} catch (e) {}
}, 400);
})();`;
const installZhsPageBridge = () => {
try {
if (document.documentElement.hasAttribute("data-pp-zhs-bridge")) return;
document.documentElement.setAttribute("data-pp-zhs-bridge", "1");
const script = document.createElement("script");
script.textContent = ZHS_BRIDGE_SOURCE;
(document.head || document.documentElement).appendChild(script);
script.remove();
} catch (e) {}
};
const hookOriginFunction = () => {
try {
const win = _unsafeWindow;
let current = win.originFunction;
const wrap = (holder) => {
try {
if (!holder || typeof holder.AttachShadow !== "function" || holder.__ppAttachWrapped) return holder;
const origin = holder.AttachShadow;
holder.AttachShadow = function(init) {
const root = origin.apply(this, [init]);
try {
_ppShadowRoots.set(this, root);
this.__ppShadowRoot = root;
} catch (e) {}
return root;
};
holder.__ppAttachWrapped = true;
} catch (e) {}
return holder;
};
current = wrap(current);
try {
Object.defineProperty(win, "originFunction", {
configurable: true,
enumerable: true,
get() { return current; },
set(value) { current = wrap(value); }
});
} catch (e) {}
} catch (e) {}
};
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';
}
});
}
if (url.includes("zhihuishu.com")) {
hookWebpack();
ppfxGuardTrustTimers();
hookAttachShadow();
hookOriginFunction();
installZhsPageBridge();
installZhPaperProbe();
}
const timer = setInterval(async () => {
if (document.readyState !== "loading") {
clearInterval(timer);
await preloadBeforeStart();
const app = vue.createApp(_sfc_main);
const pinia$1 = pinia.createPinia();
app.use(pinia$1);
app.use(ElementPlus);
app.mount(
(() => {
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);
_ppInjectShadowStyles(shadow);
return app2;
})()
);
(() => {
const PPFX_MEDIA_MISS_SLOW = 6;
const PPFX_MEDIA_MISS_IDLE = 12;
const PPFX_MEDIA_DEEP_SLOW_MS = 2000;
const PPFX_MEDIA_DEEP_IDLE_MS = 5000;
let ppfxMediaDeepAllowed = true;
let ppfxMediaMissCount = 0;
let ppfxMediaNextDeepAt = 0;
let ppfxMediaHref = "";
const collectMedia = (doc, out, depth) => {
if (!doc || depth > 2) return;
let found = 0;
try {
doc.querySelectorAll("video,audio").forEach((el) => {
out.push(el);
found += 1;
});
} catch (error) {}
try {
doc.querySelectorAll("iframe").forEach((frame) => {
try {
if (frame.contentDocument) collectMedia(frame.contentDocument, out, depth + 1);
} catch (error) {}
});
} catch (error) {}
if (!found && ppfxMediaDeepAllowed) {
try {
doc.querySelectorAll("*").forEach((el) => {
if (el.shadowRoot) collectMedia(el.shadowRoot, out, depth + 1);
});
} catch (error) {}
}
};
const pickMedia = () => {
const now = Date.now();
const href = String(location.href || "");
if (href !== ppfxMediaHref) {
ppfxMediaHref = href;
ppfxMediaMissCount = 0;
ppfxMediaNextDeepAt = 0;
}
let probe = null;
try { probe = document.querySelector("video,audio"); } catch (error) {}
if (probe) {
ppfxMediaMissCount = 0;
ppfxMediaNextDeepAt = 0;
}
ppfxMediaDeepAllowed = now >= ppfxMediaNextDeepAt;
const list = [];
collectMedia(document, list, 0);
ppfxMediaDeepAllowed = true;
let best = null;
let bestScore = -1;
for (const media of list) {
const duration = Number(media.duration);
if (!Number.isFinite(duration) || duration <= 0) continue;
const current = Number(media.currentTime) || 0;
const paused = !!media.paused;
if (paused && !media.ended && current <= 0) continue;
const rect = media.getBoundingClientRect ? media.getBoundingClientRect() : null;
const visible = media.tagName === "AUDIO" || (rect && rect.width > 0 && rect.height > 0);
if (!visible) continue;
let score = (paused ? 0 : 1000000) + (media.ended ? 500 : 0) + (current > 0 ? 1000 : 0);
if (rect) score += Math.min(10000, (rect.width * rect.height) / 100);
if (score > bestScore) {
best = media;
bestScore = score;
}
}
if (best) {
ppfxMediaMissCount = 0;
ppfxMediaNextDeepAt = 0;
} else {
ppfxMediaMissCount = Math.min(PPFX_MEDIA_MISS_IDLE, ppfxMediaMissCount + 1);
const delay = ppfxMediaMissCount >= PPFX_MEDIA_MISS_IDLE ? PPFX_MEDIA_DEEP_IDLE_MS
: ppfxMediaMissCount >= PPFX_MEDIA_MISS_SLOW ? PPFX_MEDIA_DEEP_SLOW_MS : 0;
ppfxMediaNextDeepAt = delay ? now + delay : 0;
}
return best;
};
const formatMediaTime = (seconds) => {
const total = Math.max(0, Math.floor(Number(seconds) || 0));
return Math.floor(total / 60) + ":" + String(total % 60).padStart(2, "0");
};
const store = (() => {
try {
return useProgressStore(pinia$1);
} catch (error) {
return pinia$1._s.get("progressStore");
}
})();
if (!store) return;
let lastKey = "";
let pausedTicks = 0;
const tick = () => {
try {
if (store.type === "答题" && store.isPlaying) return;
const media = pickMedia();
if (!media) return;
const duration = Number(media.duration);
if (!Number.isFinite(duration) || duration <= 0) return;
const current = Math.min(duration, Math.max(0, Number(media.currentTime) || 0));
const ended = !!media.ended || current >= duration - 0.35;
const paused = !!media.paused;
if (paused && !ended && store.isPlaying) {
pausedTicks += 1;
if (pausedTicks < 2) return;
} else {
pausedTicks = 0;
}
const percent = ended ? 100 : Math.min(100, Math.round((current / duration) * 100));
const keepTask = (store.type === "视频" || store.type === "音频") &&
store.taskName && store.taskName !== "暂无任务" && store.taskName !== "等待任务开始";
const pageTitle = String(document.title || "").split(/[—–\-_|]/)[0].trim().slice(0, 40);
const taskName = keepTask ? store.taskName : (pageTitle || "视频播放");
const detail = ended ? "播放完成" : (paused ? "已暂停" : formatMediaTime(current) + "/" + formatMediaTime(duration));
const key = taskName + "|" + duration + "|" + current + "|" + paused + "|" + percent;
if (key === lastKey) return;
lastKey = key;
store.update({
taskName: taskName,
type: media.tagName === "AUDIO" ? "音频" : "视频",
percent: percent,
currentTime: current,
totalTime: duration,
detail: detail,
isPlaying: !paused && !ended && Number(media.readyState) >= 2
});
} catch (error) {}
};
tick();
if (window.__PP_MEDIA_SYNC_TIMER__) clearInterval(window.__PP_MEDIA_SYNC_TIMER__);
window.__PP_MEDIA_SYNC_TIMER__ = window.setInterval(tick, 500);
})();
let _ppLoadingTries = 0;
const _ppHideLoadingWhenReady = () => {
const host = document.getElementById('chaoxing-helper-root');
const panel = host && host.shadowRoot ? host.shadowRoot.querySelector('.main-page') : null;
if (panel || _ppLoadingTries >= 50) {
_PP_LOADING_WINDOW.hide();
return;
}
_ppLoadingTries += 1;
setTimeout(_ppHideLoadingWhenReady, 100);
};
_ppHideLoadingWhenReady();
setTimeout(() => {
checkServerNotice();
}, 500);
window.addEventListener('beforeunload', () => {
if (_urlWatcherInterval) {
clearInterval(_urlWatcherInterval);
_urlWatcherInterval = null;
}
if (window._currentMediaInterval) {
clearInterval(window._currentMediaInterval);
window._currentMediaInterval = null;
}
if (window._blockPlayInterval && typeof window._blockPlayInterval === 'number') {
clearInterval(window._blockPlayInterval);
window._blockPlayInterval = null;
}
});
}
}, 100);
})(Vue, Pinia, rxjs, md5, ElementPlus);