// ==UserScript== // @name 重庆市法治理论知识考试系统答题助手 // @namespace https://card.wlxy.live/details/1937B460 // @version 1.0 // @description 重庆法治网考(cq.12348.gov.cn)答题助手:题库自动作答;免费体验额度,Pro Token 本账号绑定不限。 // @icon https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png // @match *://cq.12348.gov.cn/* // @match *://*.12348.gov.cn/* // @connect oa50.ahzsksw.cn // @connect card.wlxy.live // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @author 柠檬真酸 // @antifeature payment 免费体验查题额度,升级Pro不限 // @antifeature membership 需云端授权 // @tag 重庆法治 // @tag 网考 // @tag 自动答题 // @license All Rights Reserved // @run-at document-idle // ==/UserScript== (() => { 'use strict'; const STORE = { token: 'cq12348_cloud_token_v1', lease: 'cq12348_cloud_lease_v1', base: 'cq12348_cloud_base_v1', last: 'cq12348_cloud_last_v1', account: 'cq12348_account_id_v1', accountName: 'cq12348_account_name_v1', }; const DEFAULT_BASE = 'https://oa50.ahzsksw.cn'; const SCRIPT_ICON = 'https://huaweicloudobs.ahjxjy.cn/895789f9086469785b846d30c0ed95f9.png'; const PRO_BUY_URL = 'https://card.wlxy.live/details/1937B460'; const DEFAULT_PANEL_NOTICE = '\u91cd\u5e86\u6cd5\u6cbb\u7f51\u8003\u52a9\u624b'; const PANEL_TAGS = ['\u81ea\u52a8\u7b54\u9898', '\u4e00\u6b21\u901a\u8fc7', '3\u5206\u949f\u5b8c\u6210']; const gmGet = (key, fallback) => { try { if (typeof GM_getValue === 'function') return GM_getValue(key, fallback); } catch {} try { const raw = localStorage.getItem(key); return raw == null ? fallback : JSON.parse(raw); } catch { return fallback; } }; const gmSet = (key, value) => { try { if (typeof GM_setValue === 'function') return GM_setValue(key, value); } catch {} localStorage.setItem(key, JSON.stringify(value)); }; const gmGetStr = (key, fallback = '') => { const v = gmGet(key, fallback); return typeof v === 'string' ? v : (v == null ? fallback : String(v)); }; const state = { baseUrl: DEFAULT_BASE, cloudToken: gmGetStr(STORE.token, ''), cloudLease: '', cloudLeaseExp: 0, cloudLeaseLuid: '', cloudTier: 'unknown', freeUsed: 0, freeLimit: 20, proExpiresText: '', proExpiresAt: 0, panelNotice: DEFAULT_PANEL_NOTICE, proBuyUrl: PRO_BUY_URL, running: false, mode: 'exam', stop: false, done: 0, matched: 0, missed: 0, total: 0, message: '\u5f85\u547d', accountId: '', accountName: '', logs: [], projectName: '', examName: '', scoreText: '-', scoreNum: NaN, delayMs: 700, optionDelayMs: 180, }; const SCRIPT_VERSION = '1.0.0'; const PASS_SCORE = 80; const sleep = ms => new Promise(r => setTimeout(r, ms)); const norm = v => { let text = String(v || '') .replace(/[\uff0c]/g, ',') .replace(/[\u3002]/g, '.') .replace(/[\uff1f]/g, '?') .replace(/[\uff01]/g, '!') .replace(/[\uff1a]/g, ':') .replace(/[\uff1b]/g, ';') .replace(/[\u3001]/g, ',') .replace(/[\uff08]/g, '(') .replace(/[\uff09]/g, ')') .replace(/\s+/g, '') .trim(); text = text.replace(/\(\)$/g, '').replace(/[()]+$/g, ''); return text; }; const isTrueText = v => /^(\u6b63\u786e|\u5bf9|\u662f|true|√)$/i.test(norm(v)); const isFalseText = v => /^(\u9519\u8bef|\u9519|\u5426|false|×|x)$/i.test(norm(v)); function gmXhr(details) { const xhr = (typeof GM_xmlhttpRequest === 'function' && GM_xmlhttpRequest) || (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest === 'function' && GM.xmlHttpRequest); if (!xhr) return Promise.reject(new Error('\u5f53\u524d\u6269\u5c55\u4e0d\u652f\u6301\u8de8\u57df\u8bf7\u6c42')); return new Promise((resolve, reject) => { let settled = false; const done = (fn, value) => { if (!settled) { settled = true; fn(value); } }; try { const ret = xhr({ ...details, onload: response => done(resolve, response), onerror: err => done(reject, new Error((err && err.error) || '\u7f51\u7edc\u8fde\u63a5\u5931\u8d25')), ontimeout: () => done(reject, new Error('\u8bf7\u6c42\u8d85\u65f6')), onabort: () => done(reject, new Error('\u8bf7\u6c42\u5df2\u53d6\u6d88')), }); if (ret && typeof ret.then === 'function') { ret.then(r => done(resolve, r), e => done(reject, e instanceof Error ? e : new Error(String(e)))); } } catch (error) { done(reject, error instanceof Error ? error : new Error(String(error))); } }); } function normalizeIdNumber(value) { return String(value || '').trim().toUpperCase().replace(/\s+/g, ''); } function saveAccount(idnumber, name = '') { const id = normalizeIdNumber(idnumber); if (!id || id.length < 4) return ''; gmSet(STORE.account, id); if (name) gmSet(STORE.accountName, String(name).trim()); state.accountId = id; if (name) state.accountName = String(name).trim(); return id; } function parseWatermarkAccount() { const text = document.querySelector('#gxWatermark')?.getAttribute('data-text') || ''; const m = String(text).trim().match(/^(.+?)\s+\*{2,}(\w{2,8})(?:\s+\d{4}-\d{2}-\d{2})?$/); if (!m) return null; return { name: m[1].trim(), last4: m[2].trim().toUpperCase() }; } function readLoginFormAccount() { const form = document.querySelector('#userQuizOnlineForm') || document.querySelector('form'); const idInput = document.querySelector('input[name="idnumber"]') || document.querySelector('#idnumber') || form?.querySelector('input[name="idnumber"]'); const nameInput = document.querySelector('input[name="name"]') || document.querySelector('#name') || form?.querySelector('input[name="name"]'); const idnumber = normalizeIdNumber(idInput?.value || ''); const name = String(nameInput?.value || '').trim(); if (idnumber.length >= 15) return saveAccount(idnumber, name); return ''; } function hookLoginAccountCapture() { const idInput = document.querySelector('input[name="idnumber"]'); const nameInput = document.querySelector('input[name="name"]'); const form = document.querySelector('#userQuizOnlineForm'); const save = () => readLoginFormAccount(); idInput?.addEventListener('change', save); idInput?.addEventListener('blur', save); nameInput?.addEventListener('change', save); nameInput?.addEventListener('blur', save); form?.addEventListener('submit', save, true); document.querySelectorAll('#userQuizOnlineForm button, button').forEach(btn => { if (/\u767b\u5f55/.test(btn.textContent || '')) btn.addEventListener('click', save, true); }); } function currentUserId() { readLoginFormAccount(); let id = normalizeIdNumber(gmGetStr(STORE.account, '') || state.accountId || ''); let name = gmGetStr(STORE.accountName, '') || state.accountName || ''; if (id.length >= 15) { state.accountId = id; state.accountName = name; return `idcard:${id}`; } const wm = parseWatermarkAccount(); if (wm) { state.accountName = wm.name || name; if (id.length >= 15 && id.slice(-wm.last4.length) === wm.last4) { state.accountId = id; return `idcard:${id}`; } state.accountId = `****${wm.last4}`; return `idcard:${wm.name}:${wm.last4}`; } if (id) { state.accountId = id; return `idcard:${id}`; } return ''; } function accountLabel() { const name = String(state.accountName || '').trim(); if (name) return name; if (!currentUserId()) return '\u672a\u8bc6\u522b\u8d26\u53f7'; return '\u5df2\u7ed1\u5b9a\uff08\u672a\u8bc6\u522b\u59d3\u540d\uff09'; } function formatProExpireText(ts, fallback = '') { const n = Number(ts); if (Number.isFinite(n) && n > 1000) { try { return new Date(n * 1000).toLocaleString('sv-SE', { timeZone: 'Asia/Shanghai' }).replace('T', ' '); } catch {} } return String(fallback || '').trim(); } async function cloudRq(path, method = 'GET', payload) { const headers = { Accept: 'application/json' }; const luid = currentUserId(); if (luid) headers['x-learning-user-id'] = luid; if (state.cloudToken) headers.Authorization = `Bearer ${state.cloudToken}`; const opts = { url: `${String(state.baseUrl || '').replace(/\/+$/, '')}${path}`, method, timeout: 30000, headers, }; if (method !== 'GET') { headers['Content-Type'] = 'application/json'; const body = Object.assign({}, payload || {}); if (path !== '/api/cq12348/lease' && state.cloudLease && body.lease == null) body.lease = state.cloudLease; if (body.learning_user_id == null) body.learning_user_id = luid; opts.data = JSON.stringify(body); } const response = await gmXhr(opts); let wire = null; try { wire = JSON.parse(response.responseText || '{}'); } catch { wire = null; } if (!response.status) throw new Error('\u4e91\u7aef\u65e0\u54cd\u5e94'); if (response.status === 401 || response.status === 403) { const msg = (wire && (wire.message || wire.detail)) || `HTTP ${response.status}`; const err = new Error(msg); err.status = response.status; err.wire = wire; throw err; } if (!wire || wire.ok === false) { throw new Error((wire && (wire.message || wire.detail)) || `\u8bf7\u6c42\u5931\u8d25 HTTP ${response.status}`); } return wire; } function restoreLeaseCache() { const cached = gmGet(STORE.lease, null); if (!cached || typeof cached !== 'object') return; const luid = currentUserId(); if (cached.luid && cached.luid !== luid) return; state.cloudLease = cached.lease || ''; state.cloudLeaseExp = Number(cached.exp || 0); state.cloudLeaseLuid = cached.luid || ''; state.cloudTier = cached.tier || state.cloudTier; state.freeUsed = Number(cached.freeUsed || 0); state.freeLimit = Number(cached.freeLimit || state.freeLimit); state.proExpiresAt = Number(cached.proExpiresAt || 0) || 0; state.proExpiresText = formatProExpireText(state.proExpiresAt, cached.proExpiresText || ''); } function saveLeaseCache() { gmSet(STORE.lease, { lease: state.cloudLease, exp: state.cloudLeaseExp, luid: state.cloudLeaseLuid, tier: state.cloudTier, freeUsed: state.freeUsed, freeLimit: state.freeLimit, proExpiresAt: state.proExpiresAt, proExpiresText: state.proExpiresText, }); } function authFailMessage(msg) { const m = String(msg || ''); if (/token_bound|learning_user_mismatch/i.test(m)) { return 'Token \u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7\uff0c\u8bf7\u6362\u65b0\u5361\u6216\u5728\u540e\u53f0\u5220\u9664\u65e7\u7ed1\u5b9a\u540e\u518d\u6821\u9a8c'; } if (/revoked/i.test(m)) return 'Token \u5df2\u88ab\u540e\u53f0\u7981\u7528'; if (/invalid token/i.test(m)) return 'Token \u65e0\u6548'; if (/expired/i.test(m)) return 'Token \u5df2\u8fc7\u671f'; return m || '\u6388\u6743\u5931\u8d25'; } async function cloudVerifyToken() { if (!state.cloudToken) return false; try { await cloudRq('/api/license/verify', 'GET'); return true; } catch (error) { const msg = error.message || ''; if (/revoked|invalid token|expired/i.test(msg)) state.cloudTier = 'revoked'; else if (/token_bound|learning_user_mismatch/i.test(msg)) state.cloudTier = 'bound'; state.message = authFailMessage(msg); render(); return false; } } async function ensureLease(forceRefresh = false) { const luid = currentUserId(); if (!luid) { state.message = '\u672a\u8bc6\u522b\u767b\u5f55\u8d26\u53f7\uff1a\u8bf7\u5148\u7528\u59d3\u540d+\u8eab\u4efd\u8bc1\u767b\u5f55\uff0c\u6216\u8fdb\u5165\u7b54\u9898\u9875\u540e\u91cd\u8bd5'; render(); return false; } if (state.cloudLeaseLuid && state.cloudLeaseLuid !== luid) { state.cloudLease = ''; state.cloudLeaseExp = 0; state.cloudTier = 'unknown'; } const now = Math.floor(Date.now() / 1000); if (!forceRefresh && state.cloudLease && state.cloudLeaseLuid === luid && state.cloudLeaseExp - now > 60) { return true; } try { const data = await cloudRq('/api/cq12348/lease', 'POST', { learning_user_id: luid, name: state.accountName || '', userName: state.accountId || '', }); state.cloudLease = data.lease || ''; state.cloudLeaseExp = Number(data.exp || 0); state.cloudLeaseLuid = luid; state.cloudTier = String(data.tier || 'free').toLowerCase(); state.freeLimit = Number(data.free_resolve_limit ?? state.freeLimit); state.freeUsed = Number(data.free_used_resolves ?? 0); state.proExpiresAt = Number(data.pro_expires_at || 0) || 0; state.proExpiresText = formatProExpireText(state.proExpiresAt, data.pro_expires_at_text || ''); saveLeaseCache(); return true; } catch (error) { const msg = error.message || ''; if (/revoked|invalid token|expired/i.test(msg)) state.cloudTier = 'revoked'; else if (/token_bound|learning_user_mismatch/i.test(msg)) state.cloudTier = 'bound'; state.message = `\u6388\u6743\u5931\u8d25\uff1a${authFailMessage(msg)}`; render(); return false; } } async function ensureCloudReady() { restoreLeaseCache(); if (state.cloudTier === 'revoked' || state.cloudTier === 'bound') { state.cloudLease = ''; state.cloudLeaseExp = 0; state.cloudTier = 'unknown'; } if (!currentUserId()) { state.message = '\u672a\u8bc6\u522b\u767b\u5f55\u8d26\u53f7\uff1a\u8bf7\u5148\u767b\u5f55\u8003\u751f\u8d26\u53f7\uff08\u59d3\u540d+\u8eab\u4efd\u8bc1\uff09'; render(); return false; } if (state.cloudToken) { const verified = await cloudVerifyToken(); if (!verified) { render(); return false; } } const ok = await ensureLease(true); if (!ok) return false; if (state.cloudTier === 'free' && state.freeLimit > 0 && state.freeUsed >= state.freeLimit) { state.message = '\u514d\u8d39\u67e5\u9898\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u8bf7\u5347\u7ea7 Pro \u6216\u586b\u5199 Token'; render(); return false; } return true; } function openProBuyPage() { const url = String(state.proBuyUrl || PRO_BUY_URL).trim() || PRO_BUY_URL; try { window.open(url, '_blank', 'noopener,noreferrer'); } catch {} } function typeOf(name) { if (/\u591a\u9009/.test(name || '')) return '1'; if (/\u5224\u65ad/.test(name || '')) return '3'; if (/\u5355\u9009/.test(name || '')) return '0'; return '8'; } function collectQuestions() { return Array.from(document.querySelectorAll('.qlist')).map((el, index) => { const typeName = el.getAttribute('type_name') || el.querySelector('.gx-q-type')?.textContent || ''; const type = typeOf(typeName); const stem = (el.querySelector('.gx-q-stem')?.innerText || '').trim(); const labels = Array.from(el.querySelectorAll('label.gx-opt')); const choices = labels.map(node => (node.querySelector('.gx-opt-text')?.innerText || node.innerText || '').trim()).filter(Boolean); const inputs = labels.map(node => node.querySelector('input.option')).filter(Boolean); return { el, id: el.id, num: Number(el.getAttribute('num') || index + 1), type, stem, choices, inputs }; }).filter(row => row.stem && ['0', '1', '3'].includes(row.type)); } function pageWindow() { try { if (typeof unsafeWindow !== 'undefined' && unsafeWindow) return unsafeWindow; } catch {} return window; } function pageJq() { const w = pageWindow(); return w.jQuery || w.$ || window.jQuery || window.$ || null; } function markAnswerCard(num, answered) { const btn = document.querySelector(`.item-round[num="${num}"]`); if (!btn) return; if (answered) { btn.classList.remove('btn-outline-secondary'); btn.classList.add('btn-warning'); } else { btn.classList.remove('btn-warning'); btn.classList.add('btn-outline-secondary'); } refreshPageAnswered(); } function refreshPageAnswered() { const n = document.querySelectorAll('.item-round.btn-warning').length; const el = document.getElementById('gxAnswered'); if (el) el.textContent = String(n); return n; } function pageAnsweredCount() { return document.querySelectorAll('.item-round.btn-warning').length; } function checkInput(input, on) { if (!input) return false; if (!on && input.type === 'radio') return !!input.checked === false; const jq = pageJq(); try { if (jq && jq.fn && typeof jq.fn.iCheck === 'function') { const $input = jq(input); if (!$input.data('iCheck')) { try { $input.iCheck({ checkboxClass: 'icheckbox_square-yellow', radioClass: 'iradio_square-yellow', increaseArea: '20%', }); } catch {} } $input.iCheck(on ? 'check' : 'uncheck'); } } catch {} if (on && !input.checked) { try { const wrap = input.closest('.iradio_square-yellow, .icheckbox_square-yellow'); const helper = wrap?.querySelector('ins.iCheck-helper') || wrap?.querySelector('ins'); if (helper) helper.click(); } catch {} } if (on && !input.checked) { try { input.closest('label.gx-opt')?.querySelector('.gx-opt-text')?.click(); } catch {} } if (on && !input.checked) { try { input.checked = true; const wrap = input.closest('.iradio_square-yellow, .icheckbox_square-yellow'); if (wrap) wrap.classList.add('checked'); if (jq) { try { jq(input).trigger('ifChecked'); } catch {} try { jq(input).trigger('ifChanged'); } catch {} } input.dispatchEvent(new Event('change', { bubbles: true })); } catch {} } if (!on && input.type !== 'radio') { try { input.checked = false; input.closest('.iradio_square-yellow, .icheckbox_square-yellow')?.classList.remove('checked'); } catch {} } return !!input.checked === !!on; } function optionScore(a, b) { const x = norm(a), y = norm(b); if (!x || !y) return 0; if (x === y) return 1; if (x.includes(y) || y.includes(x)) { const shorter = x.length <= y.length ? x : y; const longer = x.length <= y.length ? y : x; return 0.86 + 0.08 * (shorter.length / Math.max(longer.length, 1)); } const n = x.length, m = y.length; const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0)); for (let i = 1; i <= n; i += 1) { for (let j = 1; j <= m; j += 1) { dp[i][j] = x[i - 1] === y[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]); } } return (2 * dp[n][m]) / (n + m); } function fillAnswers(question, answers) { const wantedRaw = (answers || []).map(item => { if (typeof item === 'boolean') return item ? '\u6b63\u786e' : '\u9519\u8bef'; return String(item ?? ''); }).filter(Boolean); const wanted = wantedRaw.map(t => norm(t.replace(/\u5bf9\u53c8\u652f\u63f4/g, '\u5bf9\u53e3\u652f\u63f4').replace(/\u4e13\u4e1a\u5bf9\u53c8/g, '\u4e13\u4e1a\u5bf9\u53e3'))).filter(Boolean); if (!wanted.length) return false; const targets = []; question.inputs.forEach((input, index) => { const text = question.choices[index] || ''; const key = norm(text); let match = wanted.includes(key); if (question.type === '3') { match = wanted.some(w => (isTrueText(w) && isTrueText(text)) || (isFalseText(w) && isFalseText(text)) || w === key); } if (match) targets.push(input); }); if (!targets.length && question.type !== '3') { wantedRaw.forEach(ans => { let bestIdx = -1, best = 0, second = 0; question.choices.forEach((text, index) => { const s = Math.max(optionScore(ans, text), optionScore(String(ans).replace(/\u5bf9\u53c8/g, '\u5bf9\u53e3'), text)); if (s > best) { second = best; best = s; bestIdx = index; } else if (s > second) second = s; }); if (bestIdx >= 0 && best >= 0.86 && (best - second) >= 0.04 && question.inputs[bestIdx]) { targets.push(question.inputs[bestIdx]); } }); } if (!targets.length) return false; if (question.type === '1') { question.inputs.forEach(input => checkInput(input, false)); } targets.forEach(input => checkInput(input, true)); const ok = targets.some(x => x.checked) || question.inputs.some(x => x.checked && targets.includes(x)); if (!ok) { targets.forEach(input => { try { input.closest('label.gx-opt')?.click(); } catch {} }); } const finalOk = targets.some(x => x.checked); markAnswerCard(question.num, finalOk); question.inputs.forEach(input => { input.closest('.gx-opt')?.classList.toggle('is-checked', !!input.checked); }); return finalOk; } function goQuestion(num) { const el = document.querySelector(`.qlist[num="${num}"]`); const w = pageWindow(); if (el?.id && typeof w.change === 'function') { try { w.change(el.id); return; } catch {} } if (el) { document.querySelectorAll('.qlist').forEach(node => { node.style.display = 'none'; }); el.style.display = ''; } } function goNext() { const w = pageWindow(); if (typeof w.next === 'function') { try { w.next(); return; } catch {} } } function pushLog(text) { const line = `${new Date().toLocaleTimeString()} ${text}`; state.logs.unshift(line); if (state.logs.length > 80) state.logs.length = 80; } function escHtml(text) { return String(text || '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); } async function fetchPanelNotice() { const base = String(state.baseUrl || DEFAULT_BASE).replace(/\/+$/, ''); try { const resp = await gmXhr({ url: `${base}/api/cq12348/panel-notice`, method: 'GET', timeout: 15000, headers: { Accept: 'text/plain,*/*' }, }); const t = String(resp.responseText || '').trim(); if (t) state.panelNotice = t; } catch { } try { const resp = await gmXhr({ url: `${base}/api/cq12348/client-config`, method: 'GET', timeout: 15000, headers: { Accept: 'application/json' }, }); const cfg = JSON.parse(String(resp.responseText || '{}') || '{}'); const buy = String(cfg.proBuyUrl || '').trim(); if (buy) state.proBuyUrl = buy; } catch { } } async function resolveOne(question) { await ensureLease(false); const wire = await cloudRq('/api/cq12348/answers/resolve', 'POST', { type: question.type, question: question.stem, options: question.type === '3' ? [] : question.choices, learning_user_id: currentUserId(), }); if (wire.free_used_resolves != null) state.freeUsed = Number(wire.free_used_resolves); if (wire.free_resolve_limit != null) state.freeLimit = Number(wire.free_resolve_limit); if (wire.tier) state.cloudTier = String(wire.tier).toLowerCase(); saveLeaseCache(); return wire; } async function run() { if (state.running) return; refreshExamInfo(); if (isExamFinished()) { state.message = `\u5df2\u5b8c\u6210\uff08\u5f97\u5206 ${state.scoreText} ≥ ${PASS_SCORE}\uff09\uff0c\u65e0\u9700\u518d\u7b54`; pushLog(state.message); render(); return; } state.mode = 'exam'; state.message = '\u6b63\u5728\u6821\u9a8c\u6388\u6743…'; render(); if (!(await ensureCloudReady())) { if (!state.message || state.message === '\u6b63\u5728\u6821\u9a8c\u6388\u6743…') { state.message = '\u6388\u6743\u672a\u901a\u8fc7\uff0c\u8bf7\u5148\u5728\u9762\u677f\u586b\u5199 Token \u5e76\u4fdd\u5b58\u6821\u9a8c'; } pushLog(state.message); render(); return; } if (!pageJq() || !pageJq().fn || !pageJq().fn.iCheck) { state.message = '\u672a\u68c0\u6d4b\u5230\u9875\u9762 iCheck\uff0c\u8bf7\u5237\u65b0\u8003\u8bd5\u9875\u540e\u91cd\u8bd5'; pushLog(state.message); render(); return; } const list = collectQuestions(); if (!list.length) { state.message = '\u672a\u627e\u5230\u9898\u76ee\uff0c\u8bf7\u786e\u8ba4\u5df2\u8fdb\u5165\u7b54\u9898\u9875\u4e14\u9898\u76ee\u5df2\u52a0\u8f7d'; pushLog(state.message); render(); return; } state.running = true; state.stop = false; state.done = 0; state.matched = 0; state.missed = 0; state.total = list.length; state.message = '\u7b54\u9898\u4e2d…'; pushLog(`\u5f00\u59cb\u7b54\u9898\uff0c\u5171 ${list.length} \u9898`); render(); for (const question of list) { if (state.stop) break; goQuestion(question.num); await sleep(120); state.message = `\u7b2c ${question.num}/${state.total} \u9898\u67e5\u8be2\u4e2d`; render(); try { const result = await resolveOne(question); const filled = fillAnswers(question, result.answer || []); const ok = result.matched && filled; if (ok) { state.matched += 1; state.message = `\u7b2c ${question.num} \u9898\u5df2\u52fe\u9009\uff08\u9875\u9762\u5df2\u7b54 ${pageAnsweredCount()}\uff09`; pushLog(`\u7b2c ${question.num} \u9898\u52fe\u9009\u6210\u529f`); } else { state.missed += 1; if (!result.matched) state.message = `\u7b2c ${question.num} \u9898\u9898\u5e93\u672a\u547d\u4e2d`; else if (!filled) state.message = `\u7b2c ${question.num} \u9898\u547d\u4e2d\u4f46\u9009\u9879\u5bf9\u4e0d\u4e0a\u9875\u9762\uff08\u53ef\u80fd\u662f\u9898\u5e93\u9519\u5b57\uff09`; else state.message = `\u7b2c ${question.num} \u9898\u52fe\u9009\u5931\u8d25`; pushLog(state.message); } } catch (error) { state.missed += 1; state.message = `\u7b2c ${question.num} \u9898\u5931\u8d25\uff1a${error.message || error}`; pushLog(state.message); if (/free_quota_exhausted/i.test(error.message || '') || (error.wire && error.wire.message === 'free_quota_exhausted')) { state.message = '\u514d\u8d39\u989d\u5ea6\u5df2\u7528\u5b8c\uff0c\u5df2\u505c\u6b62'; pushLog(state.message); break; } } state.done += 1; render(); await sleep(state.delayMs + Math.floor(Math.random() * 300)); goNext(); await sleep(state.optionDelayMs); } state.running = false; refreshPageAnswered(); state.message = state.stop ? `\u5df2\u505c\u6b62 · \u9875\u9762\u5df2\u7b54 ${pageAnsweredCount()}/${state.total}` : `\u5b8c\u6210 · \u52fe\u9009\u6210\u529f ${state.matched} · \u5931\u8d25 ${state.missed} · \u9875\u9762\u5df2\u7b54 ${pageAnsweredCount()}`; pushLog(state.message); render(); } function stop() { state.stop = true; state.message = '\u6b63\u5728\u505c\u6b62…'; pushLog('\u7528\u6237\u505c\u6b62'); render(); } function tierText() { if (state.cloudTier === 'pro') return state.proExpiresText ? `Pro\uff08\u81f3 ${state.proExpiresText}\uff09` : 'Pro'; if (state.cloudTier === 'revoked') return '\u5df2\u7981\u7528'; if (state.cloudTier === 'bound') return '\u5df2\u7ed1\u5b9a\u5176\u4ed6\u8d26\u53f7'; if (state.cloudTier === 'free') return `\u514d\u8d39 ${state.freeUsed}/${state.freeLimit}`; return '\u672a\u6821\u9a8c'; } function isExamPage() { return document.querySelectorAll('.qlist').length > 0; } function cleanExamTitle(text) { const t = String(text || '').replace(/\s+/g, ' ').trim(); if (!t) return ''; if (/\u8003\u8bd5\u8bb0\u5f55|\u6700\u8fd1\s*\d+|\u63d0\u4ea4\u65f6\u95f4|\u4e0b\u4e00\u6b65|\u89c2\u770b\u5b66\u4e60\u89c6\u9891|\u5df2\u5408\u683c|\u6210\u7ee9\u751f\u6548/.test(t)) return ''; if (/\u6cd5\u6cbb\u7406\u8bba\u77e5\u8bc6\u8003\u8bd5|\d{4}\u5e74\u5ea6.+\u8003\u8bd5/.test(t) && t.length <= 80) return t; if (/\u8003\u8bd5/.test(t) && t.length >= 8 && t.length <= 60 && !/\u6b21\u6570|\u65f6\u957f|\u622a\u6b62/.test(t)) return t; return ''; } function parseScoreNumber(text) { const m = String(text || '').match(/(\d+(?:\.\d+)?)/); if (!m) return NaN; const n = Number(m[1]); return Number.isFinite(n) ? n : NaN; } function formatScoreText(scoreNum, raw = '') { if (Number.isFinite(scoreNum)) return `${scoreNum}\u5206`; const t = String(raw || '').trim(); return t || '-'; } function isPassedByScore(scoreNum) { return Number.isFinite(scoreNum) && scoreNum >= PASS_SCORE; } function readExamProjectInfo() { const info = { projectName: '', examName: '', scoreText: '-', scoreNum: NaN }; let bestScore = -1; let bestName = ''; const rows = Array.from(document.querySelectorAll('table tr, .gx-table tr, .gx-record tr, .gx-card table tr')); for (const row of rows) { const cells = Array.from(row.querySelectorAll('td, th')).map(td => String(td.textContent || '').replace(/\s+/g, ' ').trim()); if (cells.length < 2) continue; if (/\u63d0\u4ea4\u65f6\u95f4|\u8003\u8bd5\u540d\u79f0|\u5f97\u5206/.test(cells.join('|'))) continue; const nameCell = cells.map(cleanExamTitle).find(Boolean) || ''; const scoreCell = cells.find(c => /\d+(?:\.\d+)?\s*\u5206/.test(c) && !/20\d{2}-\d{2}-\d{2}/.test(c)) || ''; const scoreNum = parseScoreNumber(scoreCell); if (Number.isFinite(scoreNum) && scoreNum > bestScore) { bestScore = scoreNum; if (nameCell) bestName = nameCell; } else if (!bestName && nameCell) { bestName = nameCell; } } if (bestName) { info.examName = bestName; info.projectName = bestName; } if (bestScore >= 0) { info.scoreNum = bestScore; info.scoreText = formatScoreText(bestScore); } const titleNodes = Array.from(document.querySelectorAll( '.gx-bar-name, .gx-exam-title, .gx-page-title, .gx-home-title, .gx-main h1, .gx-main h2, .gx-main h3, h1, h2, h3, .gx-title, strong', )); for (const el of titleNodes) { const t = cleanExamTitle(el.textContent); if (t) { info.projectName = t; if (!info.examName) info.examName = t; break; } } if (!info.projectName) { const body = String(document.body?.innerText || ''); const m = body.match(/(\d{4}\u5e74\u5ea6\u6cd5\u6cbb\u7406\u8bba\u77e5\u8bc6\u8003\u8bd5[^\n]{0,30})/); if (m) { info.projectName = m[1].replace(/\s+/g, ' ').trim(); if (!info.examName) info.examName = info.projectName; } } if (!Number.isFinite(info.scoreNum)) { const body = String(document.body?.innerText || ''); const m = body.match(/(?:\u6700\u9ad8\u6210\u7ee9|\u4f60\u7684\u6700\u9ad8\u6210\u7ee9|\u5f97\u5206)[^\d\n]{0,16}(\d+(?:\.\d+)?)\s*\u5206?/); if (m) { info.scoreNum = Number(m[1]); info.scoreText = formatScoreText(info.scoreNum); } else if (/\u672a\u770b\u5b66\u4e60\u89c6\u9891|\u672a\u89c2\u770b/.test(body)) { info.scoreText = '\u672a\u770b\u5b66\u4e60\u89c6\u9891'; } } if (!info.projectName && info.examName) info.projectName = info.examName; if (!info.examName && info.projectName) info.examName = info.projectName; return info; } function refreshExamInfo() { const info = readExamProjectInfo(); state.projectName = info.projectName || state.projectName || ''; state.examName = info.examName || state.examName || state.projectName || ''; state.scoreText = info.scoreText || state.scoreText || '-'; state.scoreNum = Number.isFinite(info.scoreNum) ? info.scoreNum : (parseScoreNumber(state.scoreText) || state.scoreNum); return info; } function isExamFinished() { return isPassedByScore(state.scoreNum); } async function startByPage() { refreshExamInfo(); if (isExamFinished()) { state.message = `\u5df2\u5b8c\u6210\uff08\u5f97\u5206 ${state.scoreText} ≥ ${PASS_SCORE}\uff09\uff0c\u65e0\u9700\u518d\u7b54`; pushLog(state.message); render(); return; } if (isExamPage()) return run(); state.message = '\u8bf7\u6253\u5f00\u7b54\u9898\u9875\u540e\u518d\u5f00\u59cb'; pushLog(state.message); render(); } function ensurePanel() { if (document.getElementById('cq-auto-panel')) return; const style = document.createElement('style'); style.id = 'cq-panel-style'; style.textContent = ` #cq-auto-panel{position:fixed;right:20px;top:80px;z-index:2147483000;width:412px;display:flex;flex-direction:column;background:#f4f7fb;border:1px solid #dbe4f0;border-radius:16px;box-shadow:0 16px 40px rgba(15,23,42,.17);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;font-size:12px;color:#0f172a;overflow:hidden;max-height:min(92vh,780px)} #cq-auto-panel.cq-min #cq-panel-body,#cq-auto-panel.cq-min #cq-panel-actions{display:none!important} #cq-panel-header{padding:8px 11px;background:linear-gradient(180deg,#f8ecd5,#f4e8cf);border-bottom:1px solid #e5dbc6;display:flex;justify-content:space-between;align-items:flex-start;cursor:move;user-select:none;gap:8px} #cq-panel-brand{display:flex;align-items:flex-start;gap:8px;min-width:0;flex:1} #cq-panel-logo{width:28px;height:28px;border-radius:8px;object-fit:cover;background:#fff;border:1px solid #e5dbc6;flex:0 0 auto} #cq-panel-title{font-size:13px;font-weight:900;color:#9a3412;display:flex;align-items:center;gap:6px;flex-wrap:wrap} .cq-ver{font-size:11px;font-weight:900;color:#64748b;padding:2px 7px;border-radius:999px;background:#f1f5f9;border:1px solid #e2e8f0} #cq-panel-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px} .cq-chip{font-size:10px;font-weight:800;padding:2px 7px;border-radius:999px;color:#1d4ed8;background:#eff6ff;border:1px solid #bfdbfe} .cq-chip-em{color:#9a3412;background:#fff7ed;border-color:#fdba74} .cq-chip-pro{color:#166534;background:#f0fdf4;border-color:#86efac} .cq-chip-free{color:#334155;background:#f8fafc;border-color:#cbd5e1} .cq-ctl{border:none;background:#fff;color:#64748b;width:28px;height:28px;border-radius:999px;cursor:pointer;box-shadow:0 1px 2px rgba(15,23,42,.1);font-weight:900;flex:0 0 auto} #cq-panel-body{flex:1;overflow-y:auto;padding:8px;min-height:0} .cq-card{background:#fff;border:1px solid #d9e2ee;border-radius:12px;padding:8px 10px;margin-bottom:7px} .cq-status-row{display:flex;justify-content:space-between;align-items:center;gap:7px} .cq-status-label{font-size:11px;color:#64748b;font-weight:700} #cq-auto-status{padding:2px 7px;border-radius:999px;font-weight:900;font-size:11px;background:#fff;border:1px solid #cbd5e1} .cq-metrics{display:flex;gap:8px;flex-wrap:wrap;margin-top:6px;color:#475569} .cq-metrics em{font-style:normal;font-weight:900;color:#0f172a} .cq-progress-bar{height:5px;border-radius:999px;background:#e2e8f0;overflow:hidden;margin-top:6px} .cq-progress-bar>span{display:block;height:100%;width:0;background:linear-gradient(90deg,#22d3ee,#2563eb);transition:width .2s} .cq-meta-row{display:flex;justify-content:space-between;gap:7px;font-size:12px;margin-bottom:6px;align-items:flex-start} .cq-meta-label{color:#64748b;flex:0 0 72px}.cq-meta-value{font-weight:700;text-align:right;word-break:break-all;flex:1} .cq-input{width:100%;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:8px;padding:6px 8px;font-size:12px;margin:4px 0 8px} .cq-btn-row{display:flex;gap:7px} .cq-btn{flex:1;border:none;color:#fff;padding:8px 9px;border-radius:10px;cursor:pointer;font-weight:800;font-size:12px} .cq-btn-start{background:#16a34a}.cq-btn-start:disabled{opacity:.55;cursor:not-allowed}.cq-btn-stop{background:#ef4444}.cq-btn-ghost{border:1px solid #cbd5e1;background:#fff;color:#0f172a} .cq-btn-pro{background:linear-gradient(135deg,#f59e0b,#ea580c);color:#fff} .cq-token-actions{display:flex;gap:7px;margin-bottom:4px} .cq-token-actions .cq-btn{flex:1} #cq-auto-status.is-done{color:#166534;border-color:#86efac;background:#f0fdf4} #cq-run-log{max-height:160px;overflow:auto;background:#f8fafc;border:1px solid #dbe4f0;border-radius:11px;padding:6px;margin-top:6px} .cq-log-row{padding:4px 6px;border-bottom:1px dashed #d4deea;line-height:1.4} #cq-panel-actions{padding:8px 10px;background:#eef2f7;border-top:1px solid #dbe4f0} .cq-msg{margin-top:6px;color:#64748b;white-space:pre-wrap;min-height:16px} .cq-exam-name{font-size:12px;font-weight:800;color:#0f172a;line-height:1.45;margin-top:6px} .cq-exam-score{margin-top:4px;color:#b45309;font-weight:800} .cq-section-label{font-size:11px;color:#64748b;font-weight:700;margin:8px 0 4px} .cq-ann{margin-top:8px;padding:7px 9px;border-radius:10px;background:#fffbeb;border:1px solid #fde68a;color:#92400e;font-size:11px;line-height:1.45;white-space:pre-wrap;word-break:break-word} .cq-ann-label{font-weight:800;margin-right:4px} `; document.documentElement.appendChild(style); const tagsHtml = PANEL_TAGS.map((t, i) => `${escHtml(t)}`).join(''); const panel = document.createElement('div'); panel.id = 'cq-auto-panel'; panel.innerHTML = `