${Store.isVerifyValid() ? '✅ 已验证' : '🔐 验证码激活'}
悬停放大 / 点击新窗口
扫码看广告 → 获取 4 位验证码 → 输入下方激活,验证后免费使用 24 小时
${Store.isVerifyValid()
? `✅ 有效期至 ${Store.getValidUntilStr()}`
: '⏳ 未验证,启动答题前需先激活'}
🤖 AI 模型
选择模型
${Config.models[curModel].hint}
📊 总 0
✅ 0
❌ 0
⏭ 0
`;
document.body.appendChild(panel);
this.panelEl = panel;
this.logEl = panel.querySelector('#ouchn-log');
// 拖动
(() => {
const header = panel.querySelector('.oc-header');
let dragging = false, ox = 0, oy = 0;
header.addEventListener('mousedown', (e) => {
if (e.target.closest('.oc-iconbtn')) return;
dragging = true;
ox = e.clientX - panel.getBoundingClientRect().left;
oy = e.clientY - panel.getBoundingClientRect().top;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!dragging) return;
let x = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, e.clientX - ox));
let y = Math.max(0, Math.min(window.innerHeight - 40, e.clientY - oy));
panel.style.left = x + 'px'; panel.style.top = y + 'px';
panel.style.right = 'auto'; panel.style.bottom = 'auto';
});
document.addEventListener('mouseup', () => { dragging = false; });
})();
this.bindEvents(panel);
Announce.fetch(panel);
this.addLog('✅ 国开答题助手已就绪');
if (!apiKey) this.addLog('⚠️ 请先在上方填入 DeepSeek API Key', 'warn');
if (Store.isVerifyValid()) {
this.addLog(`🔓 验证有效期至 ${Store.getValidUntilStr()}`);
} else {
this.addLog('🔒 未验证,扫码获取 4 位验证码后激活', 'warn');
}
},
bindEvents(panel) {
panel.querySelector('#oc-close').onclick = () => { panel.style.display = 'none'; };
panel.querySelector('#oc-min').onclick = () => { panel.style.display = 'none'; };
// 微信复制
const wxEl = panel.querySelector('#oc-wechat');
wxEl.onclick = async () => {
const ok = await Utils.copyText(Config.wechat);
wxEl.classList.add('copied');
wxEl.textContent = ok ? `✅ 已复制 ${Config.wechat}` : '❌ 复制失败';
setTimeout(() => { wxEl.classList.remove('copied'); wxEl.textContent = `${Config.wechat} 📋`; }, 1800);
};
// 模型切换
const modelEl = panel.querySelector('#oc-model');
const hintEl = panel.querySelector('#oc-model-hint');
modelEl.onchange = () => {
Store.setModel(modelEl.value);
hintEl.textContent = Config.models[modelEl.value].hint;
this.addLog(`✅ 模型:${Config.models[modelEl.value].name}`);
};
// Key 输入
const keyEl = panel.querySelector('#oc-key');
keyEl.onblur = () => {
const v = keyEl.value.trim();
Store.setApiKey(v);
if (v) this.addLog('✅ API Key 已保存');
};
panel.querySelector('#oc-key-toggle').onclick = () => {
keyEl.type = keyEl.type === 'password' ? 'text' : 'password';
};
// 自动翻页
const autoEl = panel.querySelector('#oc-autoNext');
autoEl.onchange = () => {
Store.setAutoNext(autoEl.checked);
this.addLog(`✅ 自动翻页:${autoEl.checked ? '开' : '关'}`);
};
// 题间间隔
const interEl = panel.querySelector('#oc-interMs');
interEl.onblur = () => {
Store.setInterMs(interEl.value);
interEl.value = Store.getInterMs();
this.addLog(`✅ 题间间隔:${interEl.value} ms`);
};
// 开始 / 暂停 / 继续 / 停止 / 当前页
const startBtn = panel.querySelector('#oc-start');
const onePageBtn = panel.querySelector('#oc-onepage');
const pauseBtn = panel.querySelector('#oc-pause');
const resumeBtn = panel.querySelector('#oc-resume');
const stopBtn = panel.querySelector('#oc-stop');
const statEl = panel.querySelector('#oc-stat');
// running=false 时只显示 启动 按钮组;running=true 时显示 暂停/继续/停止
const showRunning = (running, paused = false) => {
startBtn.style.display = running ? 'none' : 'inline-flex';
onePageBtn.style.display = running ? 'none' : 'inline-flex';
pauseBtn.style.display = running && !paused ? 'inline-flex' : 'none';
resumeBtn.style.display = running && paused ? 'inline-flex' : 'none';
stopBtn.style.display = running ? 'inline-flex' : 'none';
statEl.style.display = (running || stats.total > 0) ? 'flex' : 'none';
const dot = this.panelEl.querySelector('.oc-dot');
dot.classList.toggle('running', running);
dot.style.background = paused ? '#f59e0b' : '';
dot.style.boxShadow = paused ? '0 0 8px #f59e0b' : '';
this.ballEl?.classList.toggle('running', running);
};
const updateStat = () => {
panel.querySelector('#oc-stat-total').textContent = stats.total;
panel.querySelector('#oc-stat-ok').textContent = stats.success;
panel.querySelector('#oc-stat-fail').textContent = stats.failed;
panel.querySelector('#oc-stat-skip').textContent = stats.skipped;
};
const logger = (msg, level) => {
this.addLog(msg, level);
updateStat();
statEl.style.display = 'flex';
};
// 启动前的前置校验:API Key + 验证码
const preflight = () => {
if (!Store.getApiKey()) { this.addLog('❌ 请先填写 DeepSeek API Key', 'err'); return false; }
if (!Store.isVerifyValid()) {
this.addLog('❌ 请先在顶部"验证码激活"区扫码并输入 4 位验证码', 'err');
// 闪烁提示
const sec = panel.querySelector('#oc-verify-section');
if (sec) {
sec.scrollIntoView({ behavior: 'smooth', block: 'center' });
sec.style.transition = 'box-shadow .3s';
sec.style.boxShadow = '0 0 0 3px rgba(245,158,11,.5)';
setTimeout(() => { sec.style.boxShadow = ''; }, 1500);
}
return false;
}
return true;
};
startBtn.onclick = async () => {
if (!preflight()) return;
showRunning(true, false);
try { await Runner.runAll(logger); }
finally { showRunning(false); updateStat(); }
};
onePageBtn.onclick = async () => {
if (!preflight()) return;
showRunning(true, false);
try { await Runner.runCurrentPage(logger); }
finally { showRunning(false); updateStat(); }
};
pauseBtn.onclick = () => {
Runner.pause();
showRunning(true, true);
this.addLog('⏸ 已暂停(点击 ▶️ 继续 恢复)', 'warn');
};
resumeBtn.onclick = () => {
Runner.resume();
showRunning(true, false);
this.addLog('▶️ 已继续答题');
};
stopBtn.onclick = () => {
Runner.stop();
this.addLog('⏹ 已请求停止,等待当前题完成...', 'warn');
};
panel.querySelector('#oc-clear').onclick = () => { this.logEl.innerHTML = ''; };
// ---- 验证码 ----
const verifySection = panel.querySelector('#oc-verify-section');
const verifyTitle = panel.querySelector('#oc-verify-title');
const verifyCodeEl = panel.querySelector('#oc-verify-code');
const verifyBtn = panel.querySelector('#oc-verify-btn');
const verifyStatus = panel.querySelector('#oc-verify-status');
const verifyQr = panel.querySelector('#oc-verify-qr');
const setVerifySuccess = (untilStr) => {
verifySection.classList.remove('locked');
verifyTitle.textContent = '✅ 已验证';
verifyCodeEl.disabled = true;
verifyBtn.disabled = true;
verifyBtn.style.opacity = '.5';
verifyBtn.style.cursor = 'not-allowed';
verifyStatus.innerHTML = `