/forum/17

全能悬浮球

W WillZhong RichMan 60 views
// =====================================================
// PK 自动答题脚本 v5 — 多模式悬浮球控制面板
// 模式: PK拦截 | DOM匹配 | 自动打字 | 一键全关卡
// =====================================================
(function() {
    'use strict';

    // ====== 配置 ======
    const CONFIG = {
        mode: 'pk',       // 'pk' | 'dom' | 'typer'
        debug: true,
        hotkeys: { start: 'F6', stop: 'F5', answerCurrent: 'F7', cheatAll: 'F8' },
        pk: { answerDelay: 50, autoNext: true },
        dom: { autoClick: true, highlightColor: '#10b981', clickDelay: 500 },
        typer: { keyDelay: 45, centerSelector: '#word-当前', inputMode: 'keyboard', debounceDelay: 10 },
        cheat: { delayMs: 20000, totalStages: 60, studentName: '钟蔚然' }
    };

    // 持久化
    function loadConfig() {
        try { const s = localStorage.getItem('__pkAutoConfig'); if (s) deepMerge(CONFIG, JSON.parse(s)); } catch(e) {}
    }
    function saveConfig() {
        try { localStorage.setItem('__pkAutoConfig', JSON.stringify(CONFIG)); } catch(e) {}
    }
    function deepMerge(target, source) {
        for (const k in source) {
            if (source[k] && typeof source[k] === 'object' && !Array.isArray(source[k])) {
                if (!target[k]) target[k] = {};
                deepMerge(target[k], source[k]);
            } else { target[k] = source[k]; }
        }
    }
    loadConfig();

    // ====== 全局状态 ======
    let isRunning = false;
    let cheatRunning = false;
    let cheatStopRequested = false;
    let cheatProgress = { current: 0, total: 0, success: 0, fail: 0 };
    let listeningBtn = null;
    let panelOpen = false;

    // PK 模式状态
    let pkTimer = null;
    let pkAnsweredSet = new Set();
    let pkIsFinished = false;
    let pkPendingAnswer = null;
    let pkFetchHooked = false;

    // DOM 模式状态
    let domObserver = null;
    let domLastQuestionText = null;
    let domIsProcessing = false;

    // 打字模式状态
    let typerRunning = false;
    let typerLastWord = '';
    let typerObserver = null;
    let typerDebounceTimer = null;
    let typerToken = 0;
    let typerTyping = false;

    // ====== Toast ======
    function showToast(msg, type) {
        const c = document.getElementById('__pk-toast-container');
        if (!c) return;
        const icons = { success: '✓', error: '✕', warn: '!', info: 'i' };
        const colors = { success: '#00E676', error: '#FF5252', warn: '#FFC107', info: '#64B5F6' };
        const t = document.createElement('div');
        t.className = '__pk-toast';
        t.style.borderLeft = '3px solid ' + (colors[type] || colors.info);
        t.innerHTML = '<span class="__pk-ti" style="color:' + (colors[type] || colors.info) + '">' + (icons[type] || 'i') + '</span><span>' + msg + '</span>';
        c.appendChild(t);
        requestAnimationFrame(() => t.classList.add('__pk-toast-show'));
        setTimeout(() => { t.classList.remove('__pk-toast-show'); t.classList.add('__pk-toast-hide'); setTimeout(() => { if (t.parentNode) t.parentNode.removeChild(t); }, 350); }, 2200);
    }

    // ====== PK 模式逻辑 ======
    function pkDoAnswer(answerIdx) {
        if (typeof pkAnswer !== 'function') return false;
        if (typeof pkState !== 'undefined' && pkState.finished) return false;
        if (typeof pkState !== 'undefined' && pkState.answerLock) return false;
        const qIdx = typeof pkLastQuestionIdx !== 'undefined' ? pkLastQuestionIdx : undefined;
        if (qIdx !== undefined && pkAnsweredSet.has(qIdx)) return false;
        if (answerIdx === undefined || answerIdx === null) return false;
        if (CONFIG.debug) console.log('[PK] ✅ 答题 -> ' + 'ABCD'[answerIdx]);
        pkAnswer(answerIdx);
        if (qIdx !== undefined) pkAnsweredSet.add(qIdx);
        return true;
    }

    function pkAnswerCurrent() {
        if (pkPendingAnswer !== null) {
            const r = pkDoAnswer(pkPendingAnswer);
            showToast(r ? '已答题: ' + 'ABCD'[pkPendingAnswer] : '答题失败', r ? 'success' : 'warn');
        } else {
            showToast('暂无答案数据', 'warn');
        }
    }

    function pkInstallFetchHook() {
        if (pkFetchHooked) return true;
        const origFetch = window.fetch;
        if (!origFetch) return false;
        pkFetchHooked = true;
        window.fetch = function(input, init) {
            return origFetch.call(window, input, init).then(function(response) {
                const url = typeof input === 'string' ? input : (input.url || '');
                if (!url.includes('/api/pk/room-status')) return response;
                const cloned = response.clone();
                cloned.json().then(function(data) {
                    if (data && data.question && typeof data.question.answer === 'number') {
                        const answerIdx = data.question.answer;
                        const phase = data.phase || '';
                        pkPendingAnswer = answerIdx;
                        if (CONFIG.debug) console.log('[PK] 🔍 答案 [' + 'ABCD'[answerIdx] + '] phase=' + phase);
                        if (phase === 'answer' && isRunning && CONFIG.mode === 'pk') {
                            setTimeout(function() { pkDoAnswer(answerIdx); }, CONFIG.pk.answerDelay);
                        }
                    }
                }).catch(function() {});
                return response;
            });
        };
        return true;
    }

    function pkMainLoop() {
        if (!isRunning || CONFIG.mode !== 'pk') return;
        if (typeof pkState !== 'undefined' && pkState) {
            if (pkState.finished) {
                if (!pkIsFinished) { pkAnsweredSet.clear(); pkIsFinished = true; pkPendingAnswer = null; }
            } else {
                if (pkIsFinished) { pkIsFinished = false; }
                if (CONFIG.pk.autoNext) {
                    const nb = document.getElementById('pk-next-btn');
                    if (nb && nb.style.display !== 'none' && !nb.disabled) {
                        const txt = nb.innerText.trim();
                        if (!txt.includes('等待') && !txt.includes('⏳')) nb.click();
                    }
                }
            }
        }
        pkTimer = setTimeout(pkMainLoop, 300);
    }

    function pkStart() { pkStop(true); pkIsFinished = false; pkInstallFetchHook(); pkTimer = setTimeout(pkMainLoop, 500); }
    function pkStop(silent) { if (pkTimer) { clearTimeout(pkTimer); pkTimer = null; } if (!silent) { pkAnsweredSet.clear(); } }

    // ====== DOM 模式逻辑 ======
    function domCleanText(text) {
        if (!text) return '';
        return text.replace(/^[A-Z]\.\s*/i, '').trim();
    }

    function domGetQuestionData() {
        try {
            if (typeof battleCtx === 'undefined' || !battleCtx.currentQuestions) return null;
            const idx = battleCtx.currentQuestionIndex;
            const q = battleCtx.currentQuestions[idx];
            if (!q || !q.text || !q.options) return null;
            let correctText = null;
            if (typeof q.answer === 'number') { correctText = q.options[q.answer]; }
            else if (typeof q.answer === 'string') {
                if (q.options.includes(q.answer)) { correctText = q.answer; }
                else { const c = q.answer.toUpperCase().charCodeAt(0); if (c >= 65 && c <= 90 && q.options[c - 65]) correctText = q.options[c - 65]; }
            }
            if (!correctText) return null;
            return { id: q.text, options: q.options, correctText: correctText, cleanCorrectText: domCleanText(correctText) };
        } catch(e) { return null; }
    }

    function domHandleAnswer(forceClick) {
        if (domIsProcessing && !forceClick) return;
        const data = domGetQuestionData();
        if (!data) return;
        if (data.id === domLastQuestionText && !forceClick) return;
        domIsProcessing = true;
        domLastQuestionText = data.id;
        if (CONFIG.debug) console.log('[DOM] 新题目: ' + data.id.substring(0, 25) + '...');
        let buttons = Array.from(document.querySelectorAll('#options-container .btn-opt'));
        if (buttons.length === 0) {
            const oc = document.getElementById('pk-options');
            if (oc) buttons = Array.from(oc.querySelectorAll('button, .option-item'));
        }
        if (buttons.length === 0) { domIsProcessing = false; return; }
        let targetBtn = null;
        for (const btn of buttons) {
            const txt = btn.innerText || btn.textContent;
            const clean = domCleanText(txt);
            if (clean === data.cleanCorrectText) { targetBtn = btn; break; }
            if (txt.includes(data.correctText) && Math.abs(txt.length - data.correctText.length) < 10) { targetBtn = btn; break; }
        }
        if (!targetBtn) {
            try {
                const q = battleCtx.currentQuestions[battleCtx.currentQuestionIndex];
                if (typeof q.answer === 'number' && buttons[q.answer]) { targetBtn = buttons[q.answer]; }
            } catch(e) {}
        }
        if (targetBtn) {
            if (CONFIG.debug) console.log('[DOM] ✅ 匹配: ' + targetBtn.innerText.trim());
            const hl = CONFIG.dom.highlightColor;
            targetBtn.style.setProperty('border', '2px solid ' + hl, 'important');
            targetBtn.style.setProperty('background-color', hl + '1a', 'important');
            targetBtn.style.setProperty('box-shadow', '0 0 10px ' + hl, 'important');
            if (CONFIG.dom.autoClick || forceClick) {
                setTimeout(() => {
                    targetBtn.dispatchEvent(new MouseEvent('click', { view: window, bubbles: true, cancelable: true }));
                    setTimeout(() => { domIsProcessing = false; }, 1000);
                }, CONFIG.dom.clickDelay);
            } else { domIsProcessing = false; }
        } else { domIsProcessing = false; }
    }

    function domStart() {
        domStop();
        domLastQuestionText = null; domIsProcessing = false;
        const targets = [];
        const qc = document.getElementById('pk-question-text');
        const oc = document.getElementById('options-container') || document.getElementById('pk-options');
        if (qc) targets.push(qc); if (oc) targets.push(oc);
        if (targets.length === 0) { showToast('未找到监控DOM', 'warn'); return; }
        domObserver = new MutationObserver(() => { if (!domIsProcessing) setTimeout(() => domHandleAnswer(false), 300); });
        targets.forEach(t => domObserver.observe(t, { childList: true, subtree: true, characterData: true, attributes: true }));
        setTimeout(() => domHandleAnswer(false), 500);
    }

    function domStop() {
        if (domObserver) { domObserver.disconnect(); domObserver = null; }
        domIsProcessing = false;
    }

    // ====== 打字模式逻辑 ======
    function typerSleep(ms) { return new Promise(r => setTimeout(r, ms)); }

    function typerCleanWord(text) {
        if (!text) return '';
        return String(text).replace(/\s+/g, ' ').trim().split(' ')[0];
    }

    function typerGetCenterWord() {
        const pref = document.querySelector(CONFIG.typer.centerSelector);
        if (pref && pref.textContent.trim()) return typerCleanWord(pref.textContent);
        const el = document.elementFromPoint(Math.floor(window.innerWidth / 2), Math.floor(window.innerHeight / 2));
        if (!el) return '';
        return typerCleanWord(el.textContent);
    }

    function typerSendChar(ch) {
        if (CONFIG.typer.inputMode === 'input') {
            const a = document.activeElement;
            if (!a) return;
            if (a.tagName === 'INPUT' || a.tagName === 'TEXTAREA') {
                const s = a.selectionStart, e = a.selectionEnd;
                if (typeof s !== 'number') { a.value += ch; } else { a.setRangeText(ch, s, e, 'end'); }
                a.dispatchEvent(new InputEvent('input', { data: ch, inputType: 'insertText', bubbles: true }));
            } else if (a.isContentEditable) {
                document.execCommand('insertText', false, ch);
                a.dispatchEvent(new InputEvent('input', { data: ch, inputType: 'insertText', bubbles: true }));
            }
        } else {
            const u = ch.toUpperCase(), kc = u.charCodeAt(0);
            const opts = { key: ch, code: /^[a-zA-Z]$/.test(ch) ? 'Key' + u : '', charCode: ch.charCodeAt(0), keyCode: kc, which: kc, bubbles: true, cancelable: true };
            document.dispatchEvent(new KeyboardEvent('keydown', opts));
            document.dispatchEvent(new KeyboardEvent('keypress', opts));
            document.dispatchEvent(new KeyboardEvent('keyup', opts));
            window.dispatchEvent(new KeyboardEvent('keydown', opts));
            window.dispatchEvent(new KeyboardEvent('keypress', opts));
            window.dispatchEvent(new KeyboardEvent('keyup', opts));
        }
    }

    async function typerTypeWord(word, token) {
        typerTyping = true;
        for (let i = 0; i < word.length; i++) {
            if (!typerRunning || token !== typerToken) break;
            typerSendChar(word[i]);
            await typerSleep(CONFIG.typer.keyDelay);
        }
        if (token === typerToken) typerTyping = false;
    }

    function typerCheckAndType() {
        if (!typerRunning) return;
        const word = typerGetCenterWord();
        if (!word || word === typerLastWord) return;
        typerLastWord = word;
        typerToken++;
        if (CONFIG.debug) console.log('[Typer] word: ' + word);
        typerTypeWord(word, typerToken);
    }

    function typerScheduleCheck() {
        if (!typerRunning) return;
        clearTimeout(typerDebounceTimer);
        typerDebounceTimer = setTimeout(typerCheckAndType, CONFIG.typer.debounceDelay);
    }

    function typerStart() {
        typerStop();
        typerRunning = true; typerLastWord = ''; typerToken++; typerTyping = false;
        typerObserver = new MutationObserver(() => typerScheduleCheck());
        typerObserver.observe(document.body || document.documentElement, { childList: true, subtree: true, characterData: true });
        typerCheckAndType();
    }

    function typerStop() {
        typerRunning = false; typerToken++; typerTyping = false;
        clearTimeout(typerDebounceTimer);
        if (typerObserver) { typerObserver.disconnect(); typerObserver = null; }
    }

    // ====== 模式管理器 ======
    function startMode(silent) {
        stopMode(true);
        isRunning = true;
        switch (CONFIG.mode) {
            case 'pk': pkStart(); break;
            case 'dom': domStart(); break;
            case 'typer': typerStart(); break;
        }
        updateUIState();
        if (!silent) showToast('已启动: ' + modeLabel(CONFIG.mode), 'success');
    }

    function stopMode(silent) {
        switch (CONFIG.mode) {
            case 'pk': pkStop(silent); break;
            case 'dom': domStop(); break;
            case 'typer': typerStop(); break;
        }
        isRunning = false;
        updateUIState();
        if (!silent) showToast('已停止', 'warn');
    }

    function answerCurrent() {
        switch (CONFIG.mode) {
            case 'pk': pkAnswerCurrent(); break;
            case 'dom': domHandleAnswer(true); showToast('强制答题执行', 'info'); break;
            case 'typer': typerCheckAndType(); showToast('强制输入执行', 'info'); break;
        }
    }

    function switchMode(mode) {
        if (mode === CONFIG.mode) return;
        if (isRunning) stopMode(true);
        CONFIG.mode = mode;
        saveConfig();
        updateModeUI();
        showToast('切换至: ' + modeLabel(mode), 'info');
    }

    function modeLabel(m) {
        return { pk: 'PK拦截', dom: 'DOM匹配', typer: '自动打字' }[m] || m;
    }

    // ====== 全关卡 ======
    function cheatGenerateSign(action, payloadStr, timestamp) {
        const salt = 'CyberGame_Secret_2026_V1';
        let raw = action + payloadStr + timestamp + salt;
        let hash = 0;
        for (let i = 0; i < raw.length; i++) { let c = raw.charCodeAt(i); hash = ((hash << 5) - hash) + c; hash = hash & hash; }
        return hash.toString(16);
    }

    async function cheatRun() {
        if (cheatRunning) { showToast('全关卡已在运行中', 'warn'); return; }
        cheatRunning = true; cheatStopRequested = false;
        cheatProgress = { current: 0, total: CONFIG.cheat.totalStages, success: 0, fail: 0 };
        updateCheatUI();
        showToast('全关卡开始', 'success');

        const token = sessionStorage.getItem('s_token');
        const action = 'battle_end';
        const total = CONFIG.cheat.totalStages;
        const delay = CONFIG.cheat.delayMs;
        const name = CONFIG.cheat.studentName;

        for (let stageId = 1; stageId <= total; stageId++) {
            if (cheatStopRequested) break;
            cheatProgress.current = stageId;
            updateCheatUI();

            const payload = { stageId: stageId, isFail: false, mistakes: 0, mode: 'normal' };
            try {
                const ts = Date.now().toString();
                const ps = JSON.stringify(payload);
                const sign = cheatGenerateSign(action, ps, ts);
                const res = await fetch('/api/action', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json', 'x-student-token': token || '', 'x-timestamp': ts, 'x-sign': sign },
                    body: JSON.stringify({ student_name: name, action: action, payload: payload })
                });
                if (res.ok) { cheatProgress.success++; if (CONFIG.debug) console.log('[Cheat] ✅ stage ' + stageId); }
                else { cheatProgress.fail++; if (CONFIG.debug) console.warn('[Cheat] ⛔ stage ' + stageId + ' status ' + res.status); }
            } catch(e) { cheatProgress.fail++; if (CONFIG.debug) console.error('[Cheat] ❌ stage ' + stageId + ': ' + e.message); }

            updateCheatUI();
            if (stageId < total && !cheatStopRequested) await new Promise(r => setTimeout(r, delay));
        }

        cheatRunning = false;
        updateCheatUI();
        showToast('全关卡完成 ✅' + cheatProgress.success + ' ❌' + cheatProgress.fail, cheatProgress.fail === 0 ? 'success' : 'warn');
    }

    function cheatStop() {
        if (!cheatRunning) return;
        cheatStopRequested = true;
        showToast('正在停止全关卡...', 'warn');
    }

    // ====== UI 创建 ======
    function createUI() {
        const style = document.createElement('style');
        style.id = '__pk-auto-styles';
        style.textContent = `
            #__pk-ball{position:fixed;right:24px;bottom:80px;width:50px;height:50px;border-radius:50%;
                background:linear-gradient(135deg,#12121f,#1a1a30);border:2.5px solid #00E676;cursor:grab;
                z-index:2147483647;display:flex;align-items:center;justify-content:center;
                box-shadow:0 4px 24px rgba(0,230,118,.25),inset 0 1px 0 rgba(255,255,255,.05);
                transition:transform .22s cubic-bezier(.34,1.56,.64,1),box-shadow .3s,border-color .35s;
                user-select:none;-webkit-user-select:none;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
            #__pk-ball:hover{transform:scale(1.12);box-shadow:0 6px 32px rgba(0,230,118,.4)}
            #__pk-ball.__pk-running{border-color:#00E676;animation:__pk-p 2.2s ease-in-out infinite}
            #__pk-ball.__pk-idle{border-color:#444;box-shadow:0 4px 14px rgba(0,0,0,.35);animation:none}
            #__pk-ball.__pk-idle .__pk-ball-icon{color:#555}
            #__pk-ball.__pk-cheat{border-color:#FFC107;animation:__pk-p 1.5s ease-in-out infinite}
            @keyframes __pk-p{0%,100%{box-shadow:0 4px 24px rgba(0,230,118,.25)}50%{box-shadow:0 4px 40px rgba(0,230,118,.55)}}
            .__pk-ball-icon{font-size:20px;color:#00E676;pointer-events:none;transition:color .3s;line-height:1}

            #__pk-panel{position:fixed;width:300px;background:#0d0d18;border:1px solid rgba(255,255,255,.06);
                border-radius:18px;z-index:2147483646;overflow:hidden;
                transform:scale(.85) translateY(12px);opacity:0;pointer-events:none;
                transition:transform .32s cubic-bezier(.34,1.56,.64,1),opacity .22s ease;
                box-shadow:0 20px 60px rgba(0,0,0,.65),0 0 0 1px rgba(255,255,255,.03);
                font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
            #__pk-panel.__pk-open{transform:scale(1) translateY(0);opacity:1;pointer-events:all}

            .__pk-hdr{padding:14px 16px 10px;display:flex;align-items:center;justify-content:space-between;
                border-bottom:1px solid rgba(255,255,255,.04);background:linear-gradient(180deg,rgba(255,255,255,.02),transparent)}
            .__pk-hdr-title{font-size:13px;font-weight:700;color:#d8d8e8;letter-spacing:.3px}
            .__pk-status{font-size:10px;font-weight:700;padding:3px 10px;border-radius:20px;letter-spacing:.5px}
            .__pk-s-run{background:rgba(0,230,118,.12);color:#00E676}
            .__pk-s-stop{background:rgba(255,82,82,.12);color:#FF5252}

            .__pk-body{padding:10px 12px 14px;max-height:480px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#333 transparent}
            .__pk-body::-webkit-scrollbar{width:4px} .__pk-body::-webkit-scrollbar-thumb{background:#333;border-radius:2px}

            .__pk-tabs{display:flex;gap:4px;margin-bottom:10px;background:rgba(255,255,255,.02);border-radius:10px;padding:3px}
            .__pk-tab{flex:1;padding:7px 0;border:none;border-radius:8px;background:transparent;color:#666;
                font-size:11.5px;font-weight:700;cursor:pointer;transition:all .2s;font-family:inherit;outline:none}
            .__pk-tab:hover{color:#aaa}
            .__pk-tab.__pk-tab-active{background:rgba(0,230,118,.1);color:#00E676}
            .__pk-tab-sub{font-size:8.5px;font-weight:400;opacity:.6;display:block;margin-top:1px}

            .__pk-acts{display:flex;gap:5px;margin-bottom:6px}
            .__pk-act{flex:1;padding:8px 0;border:1px solid rgba(255,255,255,.04);border-radius:9px;
                background:rgba(255,255,255,.02);color:#b0b0c0;font-size:11.5px;font-weight:600;
                cursor:pointer;transition:all .18s;font-family:inherit;outline:none;text-align:center}
            .__pk-act:hover{background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.08);color:#e8e8f0}
            .__pk-act:active{transform:scale(.96)}
            .__pk-act-s{border-color:rgba(0,230,118,.15)} .__pk-act-s:hover{border-color:rgba(0,230,118,.3);color:#00E676}
            .__pk-act-x{border-color:rgba(255,82,82,.1)} .__pk-act-x:hover{border-color:rgba(255,82,82,.25);color:#FF5252}
            .__pk-act-a{border-color:rgba(255,193,7,.1)} .__pk-act-a:hover{border-color:rgba(255,193,7,.25);color:#FFC107}

            .__pk-div{height:1px;background:rgba(255,255,255,.04);margin:8px 0}

            .__pk-coll-btn{display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;
                border:1px solid rgba(255,255,255,.03);border-radius:9px;background:rgba(255,255,255,.02);
                color:#999;font-size:11.5px;font-weight:600;cursor:pointer;transition:all .18s;
                font-family:inherit;outline:none;margin-bottom:4px}
            .__pk-coll-btn:hover{background:rgba(255,255,255,.04);color:#ccc}
            .__pk-coll-arrow{margin-left:auto;transition:transform .25s;font-size:9px}
            .__pk-coll-btn.__pk-coll-open .__pk-coll-arrow{transform:rotate(180deg)}

            .__pk-coll{max-height:0;overflow:hidden;transition:max-height .35s ease,opacity .25s ease;opacity:0}
            .__pk-coll.__pk-coll-open{max-height:400px;opacity:1}

            .__pk-srow{display:flex;align-items:center;justify-content:space-between;margin-bottom:7px}
            .__pk-slabel{font-size:11.5px;color:#888}
            .__pk-sinput{width:72px;padding:5px 8px;border-radius:7px;border:1px solid rgba(255,255,255,.06);
                background:rgba(255,255,255,.03);color:#d0d0e0;font-size:11.5px;text-align:center;
                font-family:'SF Mono',Consolas,monospace;outline:none;transition:border-color .2s}
            .__pk-sinput:focus{border-color:#00E676}
            .__pk-sinput-wide{width:100%}
            .__pk-sselect{padding:5px 8px;border-radius:7px;border:1px solid rgba(255,255,255,.06);
                background:rgba(255,255,255,.03);color:#d0d0e0;font-size:11px;outline:none;font-family:inherit}

            .__pk-tog{position:relative;width:36px;height:19px;border-radius:10px;background:#252538;
                cursor:pointer;transition:background .3s;flex-shrink:0}
            .__pk-tog-on{background:#00E676}
            .__pk-tog-k{position:absolute;top:2px;left:2px;width:15px;height:15px;border-radius:50%;
                background:#fff;transition:transform .3s cubic-bezier(.34,1.56,.64,1);box-shadow:0 1px 4px rgba(0,0,0,.3)}
            .__pk-tog-on .__pk-tog-k{transform:translateX(17px)}

            .__pk-params{display:none} .__pk-params.__pk-params-active{display:block}

            .__pk-hkrow{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}
            .__pk-hkname{font-size:11px;color:#777}
            .__pk-hkbtn{padding:4px 10px;border-radius:6px;border:1px solid rgba(255,255,255,.06);
                background:rgba(255,255,255,.03);color:#b0b0c0;font-size:10.5px;cursor:pointer;
                transition:all .2s;font-family:'SF Mono',Consolas,monospace;min-width:52px;
                text-align:center;outline:none;font-weight:600}
            .__pk-hkbtn:hover{border-color:#64B5F6;color:#64B5F6}
            .__pk-hkbtn.__pk-listening{border-color:#FFC107;color:#FFC107;animation:__pk-blink .9s ease-in-out infinite}
            @keyframes __pk-blink{0%,100%{opacity:1}50%{opacity:.4}}

            .__pk-cheat-acts{display:flex;gap:5px;margin-bottom:8px}
            .__pk-cheat-act{flex:1;padding:7px 0;border-radius:8px;border:1px solid rgba(255,255,255,.04);
                background:rgba(255,255,255,.02);color:#b0b0c0;font-size:11px;font-weight:600;
                cursor:pointer;transition:all .18s;font-family:inherit;outline:none;text-align:center}
            .__pk-cheat-act:hover{background:rgba(255,255,255,.05)}
            .__pk-cheat-go{border-color:rgba(0,230,118,.15)} .__pk-cheat-go:hover{color:#00E676;border-color:rgba(0,230,118,.3)}
            .__pk-cheat-no{border-color:rgba(255,82,82,.1)} .__pk-cheat-no:hover{color:#FF5252;border-color:rgba(255,82,82,.25)}

            .__pk-prog{margin-bottom:8px}
            .__pk-prog-bar{width:100%;height:4px;border-radius:2px;background:#1a1a2e;overflow:hidden;margin-bottom:4px}
            .__pk-prog-fill{height:100%;border-radius:2px;background:#00E676;transition:width .4s ease;min-width:0}
            .__pk-prog-txt{font-size:10px;color:#666;text-align:center}

            #__pk-toast-container{position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:2147483647;
                display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;
                font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
            .__pk-toast{display:flex;align-items:center;gap:8px;padding:9px 18px;border-radius:10px;
                background:#14142a;color:#d8d8e8;font-size:12.5px;font-weight:500;
                box-shadow:0 10px 40px rgba(0,0,0,.55),0 0 0 1px rgba(255,255,255,.04);
                transform:translateY(-16px) scale(.95);opacity:0;transition:all .32s cubic-bezier(.34,1.56,.64,1);
                pointer-events:auto;white-space:nowrap}
            .__pk-toast-show{transform:translateY(0) scale(1);opacity:1}
            .__pk-toast-hide{transform:translateY(-8px) scale(.95);opacity:0}
            .__pk-ti{width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;
                font-size:10px;font-weight:800;flex-shrink:0}
        `;
        document.head.appendChild(style);

        // Toast 容器
        const tc = document.createElement('div'); tc.id = '__pk-toast-container'; document.body.appendChild(tc);

        // 悬浮球
        const ball = document.createElement('div'); ball.id = '__pk-ball';
        ball.className = '__pk-running'; ball.innerHTML = '<span class="__pk-ball-icon">⚡</span>';
        ball.title = 'PK 自动答题'; document.body.appendChild(ball);

        // 面板
        const panel = document.createElement('div'); panel.id = '__pk-panel';
        panel.innerHTML = `
        <div class="__pk-hdr">
            <span class="__pk-hdr-title">PK 自动答题</span>
            <span id="__pk-status" class="__pk-status __pk-s-run">运行中</span>
        </div>
        <div class="__pk-body">
            <!-- 模式切换 -->
            <div class="__pk-tabs">
                <button class="__pk-tab __pk-tab-active" data-mode="pk">PK<span class="__pk-tab-sub">API拦截</span></button>
                <button class="__pk-tab" data-mode="dom">DOM<span class="__pk-tab-sub">文本匹配</span></button>
                <button class="__pk-tab" data-mode="typer">打字<span class="__pk-tab-sub">键盘模拟</span></button>
            </div>

            <!-- 操作按钮 -->
            <div class="__pk-acts">
                <button class="__pk-act __pk-act-s" id="__pk-btn-start">▶ 开始</button>
                <button class="__pk-act __pk-act-x" id="__pk-btn-stop">⏹ 停止</button>
                <button class="__pk-act __pk-act-a" id="__pk-btn-answer">⚡ 答题</button>
            </div>

            <div class="__pk-div"></div>

            <!-- 全关卡 -->
            <button class="__pk-coll-btn" id="__pk-cheat-toggle">🏁 一键全关卡 <span class="__pk-coll-arrow">▼</span></button>
            <div class="__pk-coll" id="__pk-cheat-section">
                <div style="height:6px"></div>
                <div class="__pk-cheat-acts">
                    <button class="__pk-cheat-act __pk-cheat-go" id="__pk-cheat-start">开始刷关</button>
                    <button class="__pk-cheat-act __pk-cheat-no" id="__pk-cheat-stop">停止</button>
                </div>
                <div class="__pk-prog" id="__pk-cheat-prog" style="display:none">
                    <div class="__pk-prog-bar"><div class="__pk-prog-fill" id="__pk-cheat-fill" style="width:0"></div></div>
                    <div class="__pk-prog-txt" id="__pk-cheat-txt">0 / 0</div>
                </div>
                <div class="__pk-srow"><span class="__pk-slabel">间隔 (ms)</span><input type="number" class="__pk-sinput" id="__pk-c-delay" value="${CONFIG.cheat.delayMs}" min="1000" step="1000"></div>
                <div class="__pk-srow"><span class="__pk-slabel">关卡数</span><input type="number" class="__pk-sinput" id="__pk-c-stages" value="${CONFIG.cheat.totalStages}" min="1" max="200"></div>
                <div class="__pk-srow"><span class="__pk-slabel">姓名</span><input type="text" class="__pk-sinput" id="__pk-c-name" value="${CONFIG.cheat.studentName}" style="width:100px"></div>
            </div>

            <div class="__pk-div"></div>

            <!-- 快捷键 -->
            <button class="__pk-coll-btn" id="__pk-hk-toggle">⌨ 快捷键设置 <span class="__pk-coll-arrow">▼</span></button>
            <div class="__pk-coll" id="__pk-hk-section">
                <div style="height:6px"></div>
                <div class="__pk-hkrow"><span class="__pk-hkname">启动</span><button class="__pk-hkbtn" data-action="start">${CONFIG.hotkeys.start}</button></div>
                <div class="__pk-hkrow"><span class="__pk-hkname">停止</span><button class="__pk-hkbtn" data-action="stop">${CONFIG.hotkeys.stop}</button></div>
                <div class="__pk-hkrow"><span class="__pk-hkname">答题</span><button class="__pk-hkbtn" data-action="answerCurrent">${CONFIG.hotkeys.answerCurrent}</button></div>
                <div class="__pk-hkrow"><span class="__pk-hkname">全关卡</span><button class="__pk-hkbtn" data-action="cheatAll">${CONFIG.hotkeys.cheatAll}</button></div>
            </div>

            <div class="__pk-div"></div>

            <!-- 参数: PK -->
            <div class="__pk-params __pk-params-active" id="__pk-params-pk">
                <div class="__pk-srow"><span class="__pk-slabel">答题延迟 (ms)</span><input type="number" class="__pk-sinput" id="__pk-p-delay" value="${CONFIG.pk.answerDelay}" min="0" max="5000" step="10"></div>
                <div class="__pk-srow"><span class="__pk-slabel">自动下一题</span><div class="__pk-tog ${CONFIG.pk.autoNext?'__pk-tog-on':''}" id="__pk-p-autonext"><div class="__pk-tog-k"></div></div></div>
            </div>
            <!-- 参数: DOM -->
            <div class="__pk-params" id="__pk-params-dom">
                <div class="__pk-srow"><span class="__pk-slabel">自动点击</span><div class="__pk-tog ${CONFIG.dom.autoClick?'__pk-tog-on':''}" id="__pk-d-autoclick"><div class="__pk-tog-k"></div></div></div>
                <div class="__pk-srow"><span class="__pk-slabel">点击延迟 (ms)</span><input type="number" class="__pk-sinput" id="__pk-d-delay" value="${CONFIG.dom.clickDelay}" min="0" max="5000" step="50"></div>
                <div class="__pk-srow"><span class="__pk-slabel">高亮颜色</span><input type="color" id="__pk-d-color" value="${CONFIG.dom.highlightColor}" style="width:36px;height:24px;border:none;border-radius:4px;cursor:pointer;background:transparent"></div>
            </div>
            <!-- 参数: Typer -->
            <div class="__pk-params" id="__pk-params-typer">
                <div class="__pk-srow"><span class="__pk-slabel">按键延迟 (ms)</span><input type="number" class="__pk-sinput" id="__pk-t-delay" value="${CONFIG.typer.keyDelay}" min="10" max="500" step="5"></div>
                <div class="__pk-srow"><span class="__pk-slabel">输入模式</span>
                    <select class="__pk-sselect" id="__pk-t-mode">
                        <option value="keyboard" ${CONFIG.typer.inputMode==='keyboard'?'selected':''}>键盘模拟</option>
                        <option value="input" ${CONFIG.typer.inputMode==='input'?'selected':''}>直接输入</option>
                    </select>
                </div>
                <div class="__pk-srow"><span class="__pk-slabel">中心选择器</span><input type="text" class="__pk-sinput" id="__pk-t-selector" value="${CONFIG.typer.centerSelector}" style="width:110px"></div>
            </div>

            <!-- 全局设置 -->
            <div class="__pk-srow" style="margin-top:4px"><span class="__pk-slabel">调试模式</span><div class="__pk-tog ${CONFIG.debug?'__pk-tog-on':''}" id="__pk-g-debug"><div class="__pk-tog-k"></div></div></div>
        </div>`;
        document.body.appendChild(panel);

        // ====== 拖拽 ======
        let isDragging = false, hasMoved = false, dragSX, dragSY, ballSX, ballSY;
        function onDS(e) {
            e.preventDefault(); isDragging = true; hasMoved = false;
            const p = e.touches ? e.touches[0] : e; dragSX = p.clientX; dragSY = p.clientY;
            const r = ball.getBoundingClientRect(); ballSX = r.left; ballSY = r.top;
            document.addEventListener('mousemove', onDM); document.addEventListener('mouseup', onDE);
            document.addEventListener('touchmove', onDM, {passive:false}); document.addEventListener('touchend', onDE);
        }
        function onDM(e) {
            if (!isDragging) return; e.preventDefault();
            const p = e.touches ? e.touches[0] : e;
            const dx = p.clientX - dragSX, dy = p.clientY - dragSY;
            if (Math.abs(dx) > 4 || Math.abs(dy) > 4) hasMoved = true;
            let nx = Math.max(0, Math.min(window.innerWidth - 50, ballSX + dx));
            let ny = Math.max(0, Math.min(window.innerHeight - 50, ballSY + dy));
            ball.style.left = nx + 'px'; ball.style.top = ny + 'px'; ball.style.right = 'auto'; ball.style.bottom = 'auto';
            if (panelOpen) updatePanelPos();
        }
        function onDE() {
            isDragging = false;
            document.removeEventListener('mousemove', onDM); document.removeEventListener('mouseup', onDE);
            document.removeEventListener('touchmove', onDM); document.removeEventListener('touchend', onDE);
            if (!hasMoved) togglePanel();
        }
        ball.addEventListener('mousedown', onDS); ball.addEventListener('touchstart', onDS, {passive:false});

        function togglePanel() {
            panelOpen = !panelOpen;
            if (panelOpen) { updatePanelPos(); panel.classList.add('__pk-open'); }
            else { panel.classList.remove('__pk-open'); }
        }
        function updatePanelPos() {
            const br = ball.getBoundingClientRect(), pw = 300;
            const ph = panel.offsetHeight || 400;
            let pl = br.left - pw - 14, pt = br.top + br.height/2 - ph/2;
            if (pl < 8) { pl = br.left + br.width/2 - pw/2; pt = br.top - ph - 14; }
            if (pt < 8) { pl = br.right + 14; pt = br.top + br.height/2 - ph/2; }
            pl = Math.max(8, Math.min(window.innerWidth - pw - 8, pl));
            pt = Math.max(8, Math.min(window.innerHeight - ph - 8, pt));
            panel.style.left = pl + 'px'; panel.style.top = pt + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto';
        }

        // ====== 事件绑定 ======

        // 模式切换
        panel.querySelectorAll('.__pk-tab').forEach(tab => {
            tab.addEventListener('click', () => switchMode(tab.dataset.mode));
        });

        // 操作按钮
        document.getElementById('__pk-btn-start').addEventListener('click', () => startMode());
        document.getElementById('__pk-btn-stop').addEventListener('click', () => stopMode());
        document.getElementById('__pk-btn-answer').addEventListener('click', () => answerCurrent());

        // 折叠区
        document.getElementById('__pk-cheat-toggle').addEventListener('click', function() {
            this.classList.toggle('__pk-coll-open');
            document.getElementById('__pk-cheat-section').classList.toggle('__pk-coll-open');
        });
        document.getElementById('__pk-hk-toggle').addEventListener('click', function() {
            this.classList.toggle('__pk-coll-open');
            document.getElementById('__pk-hk-section').classList.toggle('__pk-coll-open');
        });

        // 全关卡
        document.getElementById('__pk-cheat-start').addEventListener('click', () => cheatRun());
        document.getElementById('__pk-cheat-stop').addEventListener('click', () => cheatStop());
        document.getElementById('__pk-c-delay').addEventListener('change', function() {
            CONFIG.cheat.delayMs = Math.max(1000, parseInt(this.value) || 20000); this.value = CONFIG.cheat.delayMs; saveConfig();
        });
        document.getElementById('__pk-c-stages').addEventListener('change', function() {
            CONFIG.cheat.totalStages = Math.max(1, parseInt(this.value) || 60); this.value = CONFIG.cheat.totalStages; saveConfig();
        });
        document.getElementById('__pk-c-name').addEventListener('change', function() {
            CONFIG.cheat.studentName = this.value.trim() || '钟蔚然'; saveConfig();
        });

        // 快捷键按钮
        panel.querySelectorAll('.__pk-hkbtn').forEach(btn => {
            btn.addEventListener('click', function() {
                if (listeningBtn) { listeningBtn.classList.remove('__pk-listening'); listeningBtn.textContent = CONFIG.hotkeys[listeningBtn.dataset.action]; }
                if (listeningBtn === btn) { listeningBtn = null; return; }
                btn.classList.add('__pk-listening'); btn.textContent = '按下...'; listeningBtn = btn;
            });
        });

        // PK 参数
        document.getElementById('__pk-p-delay').addEventListener('change', function() {
            CONFIG.pk.answerDelay = Math.max(0, parseInt(this.value) || 50); this.value = CONFIG.pk.answerDelay; saveConfig();
            showToast('答题延迟: ' + CONFIG.pk.answerDelay + 'ms', 'info');
        });
        document.getElementById('__pk-p-autonext').addEventListener('click', function() {
            CONFIG.pk.autoNext = !CONFIG.pk.autoNext; this.classList.toggle('__pk-tog-on', CONFIG.pk.autoNext); saveConfig();
            showToast('自动下一题: ' + (CONFIG.pk.autoNext ? '开' : '关'), 'info');
        });

        // DOM 参数
        document.getElementById('__pk-d-autoclick').addEventListener('click', function() {
            CONFIG.dom.autoClick = !CONFIG.dom.autoClick; this.classList.toggle('__pk-tog-on', CONFIG.dom.autoClick); saveConfig();
            showToast('自动点击: ' + (CONFIG.dom.autoClick ? '开' : '关'), 'info');
        });
        document.getElementById('__pk-d-delay').addEventListener('change', function() {
            CONFIG.dom.clickDelay = Math.max(0, parseInt(this.value) || 500); this.value = CONFIG.dom.clickDelay; saveConfig();
        });
        document.getElementById('__pk-d-color').addEventListener('input', function() {
            CONFIG.dom.highlightColor = this.value; saveConfig();
        });

        // Typer 参数
        document.getElementById('__pk-t-delay').addEventListener('change', function() {
            CONFIG.typer.keyDelay = Math.max(10, parseInt(this.value) || 45); this.value = CONFIG.typer.keyDelay; saveConfig();
        });
        document.getElementById('__pk-t-mode').addEventListener('change', function() {
            CONFIG.typer.inputMode = this.value; saveConfig();
            showToast('输入模式: ' + (this.value === 'keyboard' ? '键盘模拟' : '直接输入'), 'info');
        });
        document.getElementById('__pk-t-selector').addEventListener('change', function() {
            CONFIG.typer.centerSelector = this.value.trim() || '#word-当前'; saveConfig();
        });

        // 全局
        document.getElementById('__pk-g-debug').addEventListener('click', function() {
            CONFIG.debug = !CONFIG.debug; this.classList.toggle('__pk-tog-on', CONFIG.debug); saveConfig();
            showToast('调试: ' + (CONFIG.debug ? '开' : '关'), 'info');
        });

        // 点击外部关闭
        document.addEventListener('mousedown', function(e) {
            if (panelOpen && !panel.contains(e.target) && !ball.contains(e.target)) {
                panelOpen = false; panel.classList.remove('__pk-open');
                if (listeningBtn) { listeningBtn.classList.remove('__pk-listening'); listeningBtn.textContent = CONFIG.hotkeys[listeningBtn.dataset.action]; listeningBtn = null; }
            }
        });

        window.addEventListener('resize', function() {
            if (panelOpen) updatePanelPos();
            const br = ball.getBoundingClientRect();
            if (br.right > window.innerWidth || br.bottom > window.innerHeight) {
                ball.style.left = Math.min(br.left, window.innerWidth - 50) + 'px';
                ball.style.top = Math.min(br.top, window.innerHeight - 50) + 'px';
                ball.style.right = 'auto'; ball.style.bottom = 'auto';
            }
        });
    }

    // ====== UI 更新 ======
    function updateUIState() {
        const ball = document.getElementById('__pk-ball');
        const status = document.getElementById('__pk-status');
        if (ball) {
            ball.classList.remove('__pk-running', '__pk-idle', '__pk-cheat');
            if (isRunning) ball.classList.add('__pk-running');
            else if (cheatRunning) ball.classList.add('__pk-cheat');
            else ball.classList.add('__pk-idle');
        }
        if (status) {
            if (isRunning) { status.textContent = modeLabel(CONFIG.mode) + ' 运行中'; status.className = '__pk-status __pk-s-run'; }
            else { status.textContent = '已停止'; status.className = '__pk-status __pk-s-stop'; }
        }
    }

    function updateModeUI() {
        // Tab active
        document.querySelectorAll('.__pk-tab').forEach(t => t.classList.toggle('__pk-tab-active', t.dataset.mode === CONFIG.mode));
        // Params
        document.querySelectorAll('.__pk-params').forEach(p => p.classList.remove('__pk-params-active'));
        const paramsEl = document.getElementById('__pk-params-' + CONFIG.mode);
        if (paramsEl) paramsEl.classList.add('__pk-params-active');
        // Answer button label
        const ansBtn = document.getElementById('__pk-btn-answer');
        if (ansBtn) ansBtn.textContent = CONFIG.mode === 'typer' ? '⚡ 输入' : '⚡ 答题';
        updateUIState();
    }

    function updateCheatUI() {
        const prog = document.getElementById('__pk-cheat-prog');
        const fill = document.getElementById('__pk-cheat-fill');
        const txt = document.getElementById('__pk-cheat-txt');
        if (!prog) return;
        if (cheatRunning || cheatProgress.current > 0) {
            prog.style.display = 'block';
            const pct = cheatProgress.total > 0 ? (cheatProgress.current / cheatProgress.total * 100) : 0;
            fill.style.width = pct + '%';
            txt.textContent = cheatProgress.current + ' / ' + cheatProgress.total + '  ✅' + cheatProgress.success + '  ❌' + cheatProgress.fail;
        }
        updateUIState();
    }

    // ====== 快捷键 ======
    function matchHotkey(e, combo) {
        if (!combo) return false;
        const parts = combo.split('+');
        const main = parts[parts.length - 1];
        const needCtrl = parts.includes('Ctrl'), needAlt = parts.includes('Alt'), needShift = parts.includes('Shift');
        const keyMatch = e.key.toLowerCase() === main.toLowerCase() || e.key === main;
        return keyMatch && e.ctrlKey === needCtrl && e.altKey === needAlt && e.shiftKey === needShift;
    }

    document.addEventListener('keydown', function(e) {
        // 快捷键录制
        if (listeningBtn) {
            e.preventDefault(); e.stopPropagation();
            if (['Control','Alt','Shift','Meta'].includes(e.key)) return;
            let combo = '';
            if (e.ctrlKey) combo += 'Ctrl+';
            if (e.altKey) combo += 'Alt+';
            if (e.shiftKey) combo += 'Shift+';
            combo += e.key.length === 1 ? e.key.toUpperCase() : e.key;
            const action = listeningBtn.dataset.action;
            CONFIG.hotkeys[action] = combo;
            listeningBtn.textContent = combo;
            listeningBtn.classList.remove('__pk-listening');
            listeningBtn = null;
            saveConfig();
            showToast('快捷键: ' + combo, 'success');
            return;
        }
        // 功能触发
        Object.keys(CONFIG.hotkeys).forEach(function(action) {
            if (matchHotkey(e, CONFIG.hotkeys[action])) {
                e.preventDefault();
                switch(action) {
                    case 'start': startMode(); break;
                    case 'stop': stopMode(); break;
                    case 'answerCurrent': answerCurrent(); break;
                    case 'cheatAll': cheatRunning ? cheatStop() : cheatRun(); break;
                }
            }
        });
    });

    // ====== 全局导出 ======
    window.__pkAuto = {
        start: startMode, stop: stopMode, answerCurrent: answerCurrent,
        cheatStart: cheatRun, cheatStop: cheatStop, switchMode: switchMode,
        config: CONFIG
    };

    // ====== 初始化 ======
    function init() {
        createUI();
        updateModeUI();
        startMode(true);
    }
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
})();
Replies

Live
1 replies
S
Steven

我替你传给学弟

登录 后参与讨论