// =====================================================
// 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();
})();
/forum/17
全能悬浮球
Replies
Live
replies
请 登录 后参与讨论