// ==UserScript== // @name TouGe助手 // @namespace http://tampermonkey.net/ // @version 9.178 // @description 无 // @match https://tg.zcst.edu.cn/* // @match https://www.educoder.net/* // @grant none // @run-at document-end // ==/UserScript== (function () { 'use strict'; // ===== 双区复制整合:可配置的 XPath 与常见选择器兜底 ===== const COPY_A_XPATHS = [ '/html/body/div[1]/div/div/div[1]/div[2]/section[1]/div[2]/div[1]/div/div[1]/div[1]' ]; const COPY_B_XPATHS = [ // 为空将自动使用常见选择器与页面文本兜底 ]; const PANEL_POS_KEY = 'zcst_answer_helper_panel_pos_v2'; const PANEL_DEFAULT_WIDTH = 520; const PANEL_DEFAULT_HEIGHT = 520; const PANEL_MIN_WIDTH = 360; const PANEL_MIN_HEIGHT = 320; const PANEL_MINIMIZED_WIDTH = 168; const PANEL_MINIMIZED_HEIGHT = 56; const CLIPBOARD_PREVIEW_AUTO_HIDE_MS = 15000; let clipboardPreviewTimerId = null; let clipboardPreviewHovering = false; let logPanelExpanded = false; const COMMON_SELECTORS = [ '.content', '.main-content', '#content', 'article', 'main', '.question-content', '.exam-content', '.container .content' ]; // ===== 图片题目扫描器模块 ===== const imageScanner = { imageQuestions: new Set(), isScanning: false, scanIntervalId: null, currentQuestionIndex: 0, checkCurrentImage: function () { const container = document.querySelector('.question_title, .question-content, [id^="Anchor_0_"]'); if (!container) return; if (elementHasLargeVisual(container)) { const target = getCurrentQuestionTarget('image'); if (target.questionKey && !this.imageQuestions.has(target.questionKey)) { log('??????????questionKey=' + target.questionKey, 'success'); this.imageQuestions.add(target.questionKey); rememberVisualQuestionKey(target.questionKey); markQuestionOnSheet(target); } } }, start: function () { if (this.isScanning) { log('【扫描】已在进行中,请勿重复点击', 'info'); return; } log('【扫描】开始遍历所有题目以查找图片...', 'info'); this.isScanning = true; this.imageQuestions.clear(); this.currentQuestionIndex = 0; this.checkCurrentImage(); this.scanIntervalId = setInterval(() => { const hasNext = clickNext(true); if (hasNext) { this.currentQuestionIndex++; setTimeout(() => this.checkCurrentImage(), 1000); } else { this.stop(); log(`【扫描】完成!共找到 ${this.imageQuestions.size} 道图片题。`, 'success'); log(`【扫描】题号: ${Array.from(this.imageQuestions).sort((a, b) => a - b).join(', ')}`, 'info'); setTimeout(() => { this.imageQuestions.forEach(questionKey => markQuestionOnSheet({ styleType: 'image', questionKey })); }, 500); } }, 2000); }, stop: function () { if (this.scanIntervalId) { clearInterval(this.scanIntervalId); this.scanIntervalId = null; } this.isScanning = false; log('【扫描】已停止', 'info'); } }; function firstNodeByXPath(xpath) { try { const res = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null); return res && res.singleNodeValue ? res.singleNodeValue : null; } catch (e) { return null; } } function normalizeText(text) { return String(text) .replace(/\u00A0/g, ' ') .replace(/[\t ]+/g, ' ') .replace(/\n{3,}/g, '\n\n') .trim(); } function getVisibleTextExcludingUI() { try { const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { if (!node || !node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT; const p = node.parentElement; if (!p) return NodeFilter.FILTER_REJECT; if (p.closest('#main-panel')) return NodeFilter.FILTER_REJECT; if (p.closest('#show-main-panel-btn')) return NodeFilter.FILTER_REJECT; const cs = getComputedStyle(p); if (cs.display === 'none' || cs.visibility === 'hidden') return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; } } ); const parts = []; let cur = walker.nextNode(); while (cur) { parts.push(cur.nodeValue.trim()); cur = walker.nextNode(); } return normalizeText(parts.join('\n')); } catch (_) { let t = (document.body && document.body.innerText) ? document.body.innerText : ''; const panel = document.getElementById('main-panel'); if (panel && panel.innerText) t = t.replace(panel.innerText, ''); const showBtn = document.getElementById('show-main-panel-btn'); if (showBtn && showBtn.innerText) t = t.replace(showBtn.innerText, ''); return normalizeText(t); } } function removeUiStrings(text) { try { let t = String(text || ''); const panel = document.getElementById('main-panel'); if (panel && panel.innerText) t = t.replace(panel.innerText, ''); const showBtn = document.getElementById('show-main-panel-btn'); if (showBtn && showBtn.innerText) t = t.replace(showBtn.innerText, ''); const blacklist = [ '读取剪贴板', '读取剪切板', '题目收集', '题目收集:关', '题目收集: 开', '复制题目', '双区复制', '实验一键采集', '实验代码一键采集', '显示助手', '答题助手', '学习采集助手', '日志', '剪贴板预览' ]; for (const w of blacklist) { t = t.replace(new RegExp(w, 'g'), ''); } return normalizeText(t); } catch (_) { return normalizeText(text || ''); } } async function copyText(text) { try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); return true; } } catch (_) { } try { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.focus(); ta.select(); const ok = document.execCommand('copy'); document.body.removeChild(ta); return !!ok; } catch (_) { return false; } } function getNativeValueSetter(el) { if (!el) return null; const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const descriptor = Object.getOwnPropertyDescriptor(proto, 'value'); return descriptor && descriptor.set ? descriptor.set : null; } function setInputValueForFramework(el, value) { const setter = getNativeValueSetter(el); if (setter) { setter.call(el, value); } else { el.value = value; } } function dispatchEditableEvents(el, text) { try { el.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, data: text, inputType: 'insertFromPaste' })); } catch (_) { el.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); } el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); } function isEditableInput(el) { if (!el || !el.tagName) return false; if (el.disabled || el.readOnly) return false; const tag = el.tagName.toUpperCase(); if (tag === 'TEXTAREA') return true; if (tag !== 'INPUT') return false; const type = String(el.type || 'text').toLowerCase(); return [ 'text', 'search', 'url', 'tel', 'password', 'email', 'number' ].includes(type); } function getContentEditableTarget(el) { if (!el || !el.closest) return null; const target = el.closest('[contenteditable="true"], [contenteditable="plaintext-only"], [contenteditable=""]'); if (!target) return null; const editable = target.getAttribute('contenteditable'); return editable !== 'false' ? target : null; } function getPasteTarget(eventTarget) { const target = eventTarget && eventTarget.nodeType === Node.ELEMENT_NODE ? eventTarget : eventTarget && eventTarget.parentElement; if (isEditableInput(target)) return target; const contentEditable = getContentEditableTarget(target); if (contentEditable) return contentEditable; const active = document.activeElement; if (isEditableInput(active)) return active; return getContentEditableTarget(active); } function insertTextIntoInput(el, text) { const value = String(el.value ?? ''); let start = value.length; let end = value.length; try { if (typeof el.selectionStart === 'number') start = el.selectionStart; if (typeof el.selectionEnd === 'number') end = el.selectionEnd; } catch (_) { } const nextValue = `${value.slice(0, start)}${text}${value.slice(end)}`; const nextCursor = start + text.length; setInputValueForFramework(el, nextValue); try { el.setSelectionRange(nextCursor, nextCursor); } catch (_) { } dispatchEditableEvents(el, text); } function insertTextIntoContentEditable(el, text) { el.focus(); const selection = window.getSelection(); if (!selection) return false; let range; if (selection.rangeCount > 0 && el.contains(selection.anchorNode)) { range = selection.getRangeAt(0); } else { range = document.createRange(); range.selectNodeContents(el); range.collapse(false); } range.deleteContents(); const textNode = document.createTextNode(text); range.insertNode(textNode); range.setStartAfter(textNode); range.setEndAfter(textNode); selection.removeAllRanges(); selection.addRange(range); dispatchEditableEvents(el, text); return true; } function insertClipboardTextAtCursor(target, text) { if (!target || !text) return false; if (isEditableInput(target)) { insertTextIntoInput(target, text); return true; } if (getContentEditableTarget(target) || target.isContentEditable) { return insertTextIntoContentEditable(target, text); } return false; } function stopPasteEvent(event) { event.preventDefault(); event.stopPropagation(); if (event.stopImmediatePropagation) { event.stopImmediatePropagation(); } } function handlePasteCapture(event) { const text = event.clipboardData ? event.clipboardData.getData('text/plain') : ''; if (!text) return; stopPasteEvent(event); const target = getPasteTarget(event.target); const inserted = insertClipboardTextAtCursor(target, text); if (inserted) { setAssistantStatus('已通过 Ctrl+V 自动填入', 'success'); log('【粘贴】已写入当前光标位置', 'success'); return; } showClipboardPreview(text); setAssistantStatus('未检测到可输入区域', 'error'); log('【粘贴】未检测到可输入区域,请先点击目标输入框后再 Ctrl + V', 'error'); } function installPasteInterceptor() { // Disabled: no Ctrl+V interception or automatic clipboard insertion. window.__tougePasteInterceptorInstalled = true; } function collectContentTextFrom(xpaths) { for (const xp of xpaths || []) { const node = firstNodeByXPath(xp); if (!node) continue; const t = (node.innerText || node.textContent || '').trim(); if (t) return normalizeText(t); } for (const sel of COMMON_SELECTORS) { const nodes = Array.from(document.querySelectorAll(sel)).filter(n => (n.innerText || '').trim()); if (nodes.length) { const joined = nodes.map(n => (n.innerText || '').trim()).join('\n\n'); if (joined.trim()) return normalizeText(joined); } } const bodyText = getVisibleTextExcludingUI(); return normalizeText(bodyText || ''); } // ===== 原助手逻辑 ===== let allQuestions = []; const questionStore = new Map(); let nextGlobalIndex = 1; let lastRecordedSignature = ''; let lastRecordedAt = 0; let enabled = false; let autoAnswerLoop = false; let loopIntervalId = null; let autoSubmitEnabled = false; let autoSubmitTimerId = null; const SUBMIT_TIMEOUT = 5000; let lastKnownQuestionTitle = ''; let aiAnswerRunning = false; // --- AI Answering Variables --- let aiAnswers = []; const aiAnswerById = new Map(); let currentAiAnswerIndex = 0; const scannedImageIndexes = new Set(); const scannedImageGlobalIndexes = new Set(); const SCANNED_IMAGE_GLOBAL_INDEXES_KEY = 'touge_scanned_image_global_indexes'; // --- 点击延迟设置 --- // 基础等待时间:800ms // 随机增加时间:0~700ms // 最终每次点击后等待约 800ms~1500ms const clickDelay = 800; const clickDelayRandom = 700; function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function getClickDelay() { return clickDelay + Math.floor(Math.random() * clickDelayRandom); } function getLegacyHelperBridge() { const bridge = window.tougeLegacyHelper = window.tougeLegacyHelper || {}; bridge.version = bridge.version || '2.8'; bridge.state = bridge.state || { taskId: null, challengeId: null, taskChallengePath: null, xEduSignature: null, xEduTimestamp: null, xEduType: null, videoId: null, videoLogId: null }; const syncKeys = ['taskId', 'challengeId', 'taskChallengePath', 'xEduSignature', 'xEduTimestamp', 'xEduType', 'videoId', 'videoLogId']; syncKeys.forEach((key) => { if ((bridge.state[key] == null || bridge.state[key] === '') && window[key] != null && window[key] !== '') { bridge.state[key] = window[key]; } }); bridge.getState = bridge.getState || function () { return { ...bridge.state }; }; bridge.collectCode = bridge.collectCode || legacyHelperCopyAllCodeEntry; bridge.collectTaskCode = bridge.collectTaskCode || legacyHelperCopyAllCodeEntry; bridge.copyCode = bridge.copyCode || legacyHelperCopyAllCodeEntry; bridge.copyAllCode = bridge.copyAllCode || legacyHelperCopyAllCodeEntry; window.educoderCopyHelper = String(bridge.version); return bridge; } function updateLegacyState(patch = {}) { const bridge = getLegacyHelperBridge(); const next = {}; Object.entries(patch).forEach(([k, v]) => { if (v == null || v === '') return; next[k] = v; }); if (Object.keys(next).length === 0) return; Object.assign(bridge.state, next); Object.entries(next).forEach(([k, v]) => { try { window[k] = v; } catch (_) { } bridge[k] = v; }); window.educoderCopyHelper = String(bridge.version); log('[LegacyBridge] state updated ' + JSON.stringify(bridge.state), 'info'); } function readLegacyState() { return getLegacyHelperBridge().state; } function setLegacyState(key, value) { updateLegacyState({ [key]: value }); } function parseLegacyTaskInfoFromLocation() { try { const url = new URL(window.location.href); const parts = url.pathname.replace(/^\/+/, '').split('/'); if (parts[0] !== 'tasks') return {}; return { challengeId: parts[2] || null, taskId: parts[3] || null }; } catch (_) { return {}; } } function splitLegacyTaskPaths(raw) { const text = String(raw || ''); if (!text.trim()) return []; return text .split(/[;;\n,]+/g) .map(v => v.trim()) .filter(Boolean); } function decodeLegacyBase64(input) { const base64 = String(input || '').trim(); if (!base64) return ''; try { const binary = atob(base64); const bytes = Uint8Array.from(binary, ch => ch.charCodeAt(0)); return new TextDecoder('utf-8').decode(bytes); } catch (_) { try { return atob(base64); } catch (__ ) { return ''; } } } function splitLegacyTaskPathsStable(raw) { const text = String(raw || ''); if (!text.trim()) return []; return text.split(/[;;\n,]+/g).map(v => v.trim()).filter(Boolean); } function normalizeLegacyCodePath(path) { return String(path || '') .trim() .replace(/[;;]+$/g, '') .replace(/^["'“”‘’]+|["'“”‘’]+$/g, '') .trim(); } function buildLegacyRepContentUrl({ taskId, codePath }) { return 'https://data.educoder.net/api/tasks/' + String(taskId || '').trim() + '/rep_content.json?path=' + encodeURIComponent(codePath); } async function collectCodeViaLegacyApi() { const bridge = getLegacyHelperBridge(); const locationInfo = parseLegacyTaskInfoFromLocation(); if (!bridge.state.taskId && locationInfo.taskId) updateLegacyState({ taskId: locationInfo.taskId }); if (!bridge.state.challengeId && locationInfo.challengeId) updateLegacyState({ challengeId: locationInfo.challengeId }); const state = readLegacyState(); const taskId = String(state.taskId || '').trim(); const pathList = String(state.taskChallengePath || '') .replace(/\uFF1B/g, ';') .split(/[;\n,]+/g) .map(v => normalizeLegacyCodePath(v)) .filter(Boolean); log('[LegacyCode] taskId = ' + taskId, 'info'); log('[LegacyCode] challengeId = ' + String(state.challengeId || ''), 'info'); log('[LegacyCode] taskChallengePath = ' + String(state.taskChallengePath || ''), 'info'); if (!taskId) { log('[LegacyCode] missing taskId', 'error'); return { ok: false, reason: 'missing taskId', text: '' }; } if (pathList.length === 0) { log('[LegacyCode] no taskChallengePath, skip API collect', 'error'); return { ok: false, reason: 'missing taskChallengePath', text: '' }; } const headers = { 'X-EDU-Signature': String(state.xEduSignature || ''), 'X-EDU-Timestamp': String(state.xEduTimestamp || ''), 'X-EDU-Type': String(state.xEduType || 'pc') }; if (!headers['X-EDU-Signature']) log('[LegacyCode] missing signature', 'error'); log('[LegacyCode] request headers = ' + JSON.stringify({ xEduSignature: Boolean(headers['X-EDU-Signature']), xEduTimestamp: Boolean(headers['X-EDU-Timestamp']), xEduType: Boolean(headers['X-EDU-Type']) }), 'info'); const files = []; for (const rawPath of pathList) { const path = normalizeLegacyCodePath(rawPath); log('[LegacyCode] raw path = ' + String(rawPath), 'info'); log('[LegacyCode] normalized path = ' + path, 'info'); if (!path) { log('[LegacyCode] path empty after normalize, skip', 'error'); continue; } const api = buildLegacyRepContentUrl({ taskId, codePath: path }); log('[LegacyCode] request url = ' + api, 'info'); log('[LegacyCode] fetch start', 'info'); try { const resp = await fetch(api, { credentials: 'include', headers }); const contentType = resp.headers ? (resp.headers.get('content-type') || '') : ''; log('[LegacyCode] response status = ' + String(resp.status), resp.ok ? 'info' : 'error'); log('[LegacyCode] response content-type = ' + contentType, 'info'); const bodyText = await resp.clone().text(); log('[LegacyCode] response text preview = ' + bodyText.slice(0, 200), 'info'); if (!resp.ok) { log('[LegacyCode] response non-200 path=' + path, 'error'); continue; } let json = null; try { json = JSON.parse(bodyText); } catch (err) { log('[LegacyCode] response is not JSON path=' + path + ' err=' + String(err && err.message || err), 'error'); continue; } const encoded = json && json.content && json.content.content; if (!encoded) { log('[LegacyCode] JSON missing content.content path=' + path, 'error'); continue; } const decoded = decodeLegacyBase64(encoded); log('[LegacyCode] code length = ' + decoded.length, 'info'); if (!decoded.trim()) { log('[LegacyCode] decoded code empty path=' + path, 'error'); continue; } files.push({ path, content: decoded }); } catch (err) { log('[LegacyCode] API failed path=' + path + ' url=' + api + ' err=' + String(err && err.message || err), 'error'); } } if (files.length === 0) { log('[LegacyCode] no code found', 'error'); return { ok: false, reason: 'no code found', text: '' }; } const merged = files.map(file => file.path + '\n```\n' + file.content + '\n```').join('\n\n'); return { ok: true, reason: '', text: merged, fileCount: files.length, taskId }; } async function legacyHelperCopyAllCodeEntry() { return collectCodeViaLegacyApi(); } function createLegacyResponse(body, response) { return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers }); } function applyLegacyExerciseSetting(json) { if (!json) return; json.is_random = false; json.screen_open = false; json.screen_num = 0; json.screen_sec = 0; json.ip_limit = 'no'; json.ip_bind = false; json.ip_bind_type = false; json.question_random = false; json.choice_random = false; json.check_camera = false; json.open_phone_video_recording = false; json.forbid_screen = false; json.use_white_list = false; json.net_limit = false; json.net_limit_list = null; json.only_on_client = false; json.open_camera = false; json.is_locked = false; json.identity_verify = false; json.open_appraise = true; json.score_open = 0; json.answer_open = true; json.open_score = 0; json.open_total_score = 0; json.screen_shot_open = false; } async function modifyLegacyTaskResponse(request, response) { const url = String(request.url || ''); if (!(url.includes('/api/tasks') || url.includes('json?homework_common_id'))) return response; try { const json = await response.clone().json(); if (json && json.shixun) { json.shixun.can_copy = true; json.shixun.vip = true; json.shixun.forbid_copy = false; json.shixun.copy_for_exercise = true; json.shixun.active_copy = true; json.shixun.copy_for_exercise_save = true; json.shixun.allow_file_upload = true; json.shixun.open_local_evaluate = true; json.shixun.open_self_run = true; json.shixun.code_edit_permission = true; } if (json && json.challenge) { json.challenge.diasble_copy = false; json.challenge.disable_copy = false; } log('[LegacyMerge] Copy_helper original task modifiers applied', 'info'); return createLegacyResponse(JSON.stringify(json), response); } catch (err) { console.warn('[LegacyMerge] modify task failed', err); return response; } } async function modifyLegacyExerciseResponse(request, response) { const url = String(request.url || ''); const isUserInfo = url.includes('/api/exercises') && url.includes('get_exercise_user_info.json'); const isSetting = url.includes('/api/exercises') && (url.includes('start.json') || url.includes('exercise_setting.json')); if (!isUserInfo && !isSetting) return response; try { const json = await response.clone().json(); if (json && json.data) applyLegacyExerciseSetting(json.data); if (json && json.exercise) applyLegacyExerciseSetting(json.exercise); log('[LegacyMerge] Copy_helper original exercise modifiers applied', 'info'); return createLegacyResponse(JSON.stringify(json), response); } catch (err) { console.warn('[LegacyMerge] modify exercise failed', err); return response; } } async function modifyLegacyTaskCopy(request, response) { let res = response.clone(); res = await modifyLegacyTaskResponse(request, res); res = await modifyLegacyExerciseResponse(request, res); return res; } function shouldInspectLegacyFetchUrl(url) { const text = String(url || ''); return text.includes('/api/tasks') || text.includes('json?homework_common_id') || text.includes('rep_content.json') || text.includes('watch_video_histories') || text.includes('/api/exercises'); } function installLegacyFetchHook() { if (window.__tougeLegacyFetchHookInstalled) { log('[LegacyGuard] fetch hook already installed, skip', 'info'); return; } const rawFetch = window.fetch; if (typeof rawFetch !== 'function') { log('[LegacyBridge] fetch unavailable, skip hook', 'error'); return; } getLegacyHelperBridge(); window.__tougeLegacyFetchHookInstalled = true; window.fetch = async function (...args) { let request = null; const response = await rawFetch.apply(this, args); try { const req = new Request(...args); request = req.clone(); const reqUrl = String(req.url || ''); if (!shouldInspectLegacyFetchUrl(reqUrl)) { return response; } const signature = req.headers.get('X-EDU-Signature'); const timestamp = req.headers.get('X-EDU-Timestamp'); const eduType = req.headers.get('X-EDU-Type'); if (signature) updateLegacyState({ xEduSignature: signature }); if (timestamp) updateLegacyState({ xEduTimestamp: timestamp }); if (eduType) updateLegacyState({ xEduType: eduType }); const sig = signature; if (sig) log('[LegacyHook] captured xEduSignature=' + String(sig).slice(0, 12) + '...', 'info'); if (reqUrl.includes('/api/tasks') || reqUrl.includes('json?homework_common_id')) { try { const cloned = response.clone(); const json = await cloned.json(); if (json && json.challenge && json.challenge.path) { updateLegacyState({ taskChallengePath: json.challenge.path }); } if (json && json.challenge && (json.challenge.id || json.challenge.challenge_id)) { const cid = json.challenge.id || json.challenge.challenge_id; updateLegacyState({ challengeId: cid }); log('[LegacyHook] captured challengeId=' + String(cid), 'info'); } } catch (_) { } } if (reqUrl.includes('watch_video_histories.json')) { try { const reqJson = await req.clone().json(); const resJson = await response.clone().json(); if (reqJson && reqJson.video_id) updateLegacyState({ videoId: reqJson.video_id }); if (resJson && resJson.log_id) updateLegacyState({ videoLogId: resJson.log_id }); } catch (_) { } } if (reqUrl.includes('rep_content.json')) { try { const pathSegments = new URL(reqUrl).pathname.split('/'); const taskId = pathSegments[pathSegments.length - 2]; if (taskId) { updateLegacyState({ taskId }); log('[LegacyHook] captured taskId=' + String(taskId), 'info'); } } catch (_) { } } } catch (err) { console.warn('[LegacyHook] parse failed', err); } if (request && shouldInspectLegacyFetchUrl(request.url)) { return modifyLegacyTaskCopy(request, response.clone()); } return response; }; log('[LegacyBridge] Copy_helper hook installed', 'success'); log('[LegacyBridge] educoderCopyHelper ready', 'success'); } /*********************** * Legacy Copy_helper.js ***********************/ function legacyCopyHelperOriginal() { if (window.__tougeLegacyCopyHelperLoaded) { log('[LegacyMerge] Copy_helper already loaded, skip', 'info'); return; } window.__tougeLegacyCopyHelperLoaded = true; getLegacyHelperBridge(); installLegacyFetchHook(); window.educoderCopyHelper = '2.8'; log('[LegacyMerge] Copy_helper original logic loaded', 'info'); } /*********************** * Legacy Helper.js ***********************/ function legacyHelperOriginal() { if (window.__tougeLegacyHelperLoaded) { log('[LegacyMerge] Helper already loaded, skip', 'info'); return; } window.__tougeLegacyHelperLoaded = true; const bridge = getLegacyHelperBridge(); bridge.collectCode = legacyHelperCopyAllCodeEntry; bridge.collectTaskCode = legacyHelperCopyAllCodeEntry; bridge.copyCode = legacyHelperCopyAllCodeEntry; bridge.copyAllCode = legacyHelperCopyAllCodeEntry; try { window.collectCode = legacyHelperCopyAllCodeEntry; } catch (_) { } try { window.copyCode = legacyHelperCopyAllCodeEntry; } catch (_) { } try { window.copyAllCode = legacyHelperCopyAllCodeEntry; } catch (_) { } window.__tougeLegacyUISkipped = true; log('[LegacyMerge] Helper original logic loaded', 'info'); log('[LegacyMerge] legacy UI skipped', 'info'); log('[LegacyMerge] legacy functions ready', 'info'); } async function runExperimentCodeCollect(copyBtn = null) { log('[LegacyCode] button clicked', 'info'); log('[CodeCopy] run original Monaco copy method from experiment button', 'info'); const ok = await copyVisibleMonacoCodeByOriginalMethod(copyBtn, { resetStatus: true }); return { ok, reason: ok ? '' : 'monaco copy failed', text: '' }; } function getCopyUnlockRootWindow() { try { return typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; } catch (_) { return window; } } function getCopyUnlock() { const api = window.tougeCopyUnlock = window.tougeCopyUnlock || {}; api.initialized = !!api.initialized; api.lastCopyText = api.lastCopyText || ''; api.enableUserSelect = enableCopyUnlockUserSelect; api.extractSelectedText = extractCopyUnlockSelectedText; api.extractPageCodeText = extractCopyUnlockReadableText; api.writeClipboardText = writeCopyUnlockClipboardText; api.copyCurrentReadableContent = copyCurrentReadableContent; api.initCopyUnlock = initCopyUnlock; return api; } function isCopyUnlockEditable(el) { if (!el) return false; return !!el.closest('input, textarea, [contenteditable="true"], [contenteditable=""]'); } function enableCopyUnlockUserSelect() { if (document.getElementById('touge-copy-unlock-style')) return; const style = document.createElement('style'); style.id = 'touge-copy-unlock-style'; style.textContent = ` html, body, body * { user-select: text !important; -webkit-user-select: text !important; -moz-user-select: text !important; -ms-user-select: text !important; } button, [role="button"], input[type="button"], input[type="submit"] { user-select: none !important; -webkit-user-select: none !important; } input, textarea, [contenteditable="true"], [contenteditable=""] { user-select: text !important; -webkit-user-select: text !important; } `; document.documentElement.style.setProperty('user-select', 'text', 'important'); if (document.body) document.body.style.setProperty('user-select', 'text', 'important'); document.head.appendChild(style); log('[CopyUnlock] CSS user-select enabled', 'success'); } function extractCopyUnlockSelectedText() { try { const active = document.activeElement; if (active && typeof active.selectionStart === 'number' && typeof active.selectionEnd === 'number') { const value = String(active.value || ''); const text = value.slice(active.selectionStart, active.selectionEnd); if (text.trim()) return text; } } catch (_) { } try { const rootWindow = getCopyUnlockRootWindow(); const selected = String(rootWindow.getSelection && rootWindow.getSelection() || ''); if (selected.trim()) return selected; } catch (_) { } try { const selected = String(window.getSelection && window.getSelection() || ''); if (selected.trim()) return selected; } catch (_) { } return ''; } function normalizeCopyUnlockText(text) { return String(text || '') .replace(/\r\n/g, '\n') .replace(/\r/g, '\n') .replace(/\u00A0/g, ' ') .replace(/\n{4,}/g, '\n\n\n') .trim(); } function logCopyAudit(source, rawText, finalText) { const raw = String(rawText || ''); const finalValue = String(finalText || ''); const lineCount = finalValue ? finalValue.split('\n').length : 0; log('[CopyAudit] source=' + source, 'info'); log('[CopyAudit] raw length=' + raw.length, 'info'); log('[CopyAudit] final length=' + finalValue.length, 'info'); log('[CopyAudit] line count=' + lineCount, 'info'); log('[CopyAudit] preview=' + finalValue.slice(0, 200), 'info'); } function extractMonacoVisibleText() { const editors = Array.from(document.querySelectorAll('.monaco-editor')).filter(isVisibleElement); for (const editor of editors) { const lines = Array.from(editor.querySelectorAll('.view-lines .view-line')) .map(line => String(line.innerText || line.textContent || '').replace(/\u00A0/g, ' ')) .filter(line => line.length > 0); if (lines.length > 0) return lines.join('\n'); } return ''; } function extractCopyUnlockReadableText() { const targets = [ { name: 'selection', getter: extractCopyUnlockSelectedText }, { name: 'monaco', getter: extractMonacoVisibleText }, { name: 'codemirror', getter: () => Array.from(document.querySelectorAll('.CodeMirror-code, .cm-content')) .filter(isVisibleElement) .map(el => el.innerText || el.textContent || '') .filter(Boolean) .join('\n') }, { name: 'ace', getter: () => Array.from(document.querySelectorAll('.ace_content, .ace_text-layer')) .filter(isVisibleElement) .map(el => el.innerText || el.textContent || '') .filter(Boolean) .join('\n') }, { name: 'pre/code', getter: () => Array.from(document.querySelectorAll('pre, code')) .filter(isVisibleElement) .map(el => el.innerText || el.textContent || '') .filter(Boolean) .join('\n\n') }, { name: 'textarea', getter: () => Array.from(document.querySelectorAll('textarea')) .filter(isVisibleElement) .map(el => el.value || el.innerText || '') .filter(Boolean) .join('\n\n') }, { name: 'question/content', getter: () => { const root = getCurrentQuestionRoot && getCurrentQuestionRoot(); if (root && root.innerText) return root.innerText; return collectContentTextFrom([]); } } ]; for (const target of targets) { try { const rawText = target.getter(); const text = normalizeCopyUnlockText(rawText); if (text) { log('[CopyUnlock] fallback target=' + target.name, 'info'); log('[CopyUnlock] extracted length=' + text.length, 'info'); logCopyAudit(target.name, rawText, text); return text; } } catch (_) { } } log('[CopyUnlock] no code block found', 'info'); return ''; } async function writeCopyUnlockClipboardText(text, event) { const finalText = String(text || ''); if (!finalText.trim()) return false; try { if (event && event.clipboardData) { event.clipboardData.setData('text/plain', finalText); } } catch (_) { } try { if (typeof GM_setClipboard === 'function') { GM_setClipboard(finalText); log('[CopyUnlock] clipboard write success', 'success'); return true; } } catch (_) { } const ok = await copyText(finalText); if (ok) log('[CopyUnlock] clipboard write success', 'success'); else log('[CopyUnlock] clipboard write failed', 'error'); return ok; } async function copyCurrentReadableContent(event) { const selected = extractCopyUnlockSelectedText(); log('[CopyUnlock] selected length=' + selected.length, 'info'); const text = selected || extractCopyUnlockReadableText(); if (!text) { log('[CopyUnlock] selected empty', 'info'); log('[CopyUnlock] no readable text found', 'error'); return false; } const ok = await writeCopyUnlockClipboardText(text, event); if (ok) { const api = getCopyUnlock(); api.lastCopyText = text; if (selected) log('[CopyUnlock] copied selection length=' + selected.length, 'success'); } return ok; } function installCopyUnlockGuards() { const api = getCopyUnlock(); if (api.guardsInstalled) return; if (window.__tougeLegacyCopyInstalled && window.__tougeLegacyKeydownInstalled) { api.guardsInstalled = true; log('[LegacyGuard] CopyUnlock guards already covered, skip', 'info'); return; } api.guardsInstalled = true; document.addEventListener('selectstart', (event) => { event.stopPropagation(); }, true); document.addEventListener('contextmenu', (event) => { event.stopPropagation(); }, true); document.addEventListener('copy', (event) => { log('[CopyUnlock] copy event captured', 'info'); const selected = extractCopyUnlockSelectedText(); if (!selected && isCopyUnlockEditable(document.activeElement)) return; const text = selected || extractCopyUnlockReadableText(); if (!text) { log('[CopyUnlock] no readable text found', 'error'); return; } try { if (event.clipboardData) { event.clipboardData.setData('text/plain', text); event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); getCopyUnlock().lastCopyText = text; log('[CopyUnlock] clipboard write success', 'success'); } } catch (_) { } writeCopyUnlockClipboardText(text, event); }, true); log('[CopyUnlock] copy listener installed', 'success'); document.addEventListener('keydown', (event) => { const isCopyKey = (event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey && (event.code === 'KeyC' || String(event.key || '').toLowerCase() === 'c'); if (!isCopyKey) return; if (isCopyUnlockEditable(document.activeElement) && !extractCopyUnlockSelectedText()) return; const selected = extractCopyUnlockSelectedText(); if (!selected && isCopyUnlockEditable(document.activeElement)) return; const text = selected || extractCopyUnlockReadableText(); if (!text) { log('[CopyUnlock] no readable text found', 'error'); return; } event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); writeCopyUnlockClipboardText(text); }, true); log('[CopyUnlock] keydown Ctrl+C listener installed', 'success'); } function initCopyUnlock() { const api = getCopyUnlock(); if (api.initialized) return api; api.initialized = true; log('[CopyUnlock] init', 'info'); enableCopyUnlockUserSelect(); installCopyUnlockGuards(); const helper = window.educoderCopyHelper; const entry = helper && typeof helper === 'object' ? (helper.copyCurrentReadableContent || helper.copyCode || helper.collectCode || helper.copyAllCode) : null; api.copyCurrentReadableContent = copyCurrentReadableContent; if (helper && typeof helper === 'object') { helper.copyCurrentReadableContent = copyCurrentReadableContent; helper.copyPageText = copyCurrentReadableContent; } log('[CopyUnlock] legacy copy entry found: ' + String(entry && entry.name || 'copyCurrentReadableContent'), 'info'); return api; } function getClipboardEnhancer() { const api = window.tougeClipboardEnhancer = window.tougeClipboardEnhancer || {}; api.enabled = api.enabled !== false; api.init = initClipboardEnhancer; api.enable = () => { api.enabled = true; log('[ClipboardEnhancer] enabled=true', 'success'); return true; }; api.disable = () => { api.enabled = false; log('[ClipboardEnhancer] enabled=false', 'info'); return true; }; api.copyText = writeClipboardEnhancerText; api.readClipboardText = readClipboardEnhancerText; api.pasteTextToActiveEditable = pasteTextToActiveEditable; api.getActiveEditableTarget = getActiveClipboardTarget; api.insertTextIntoInput = insertTextIntoInputTarget; api.insertTextIntoTextarea = insertTextIntoTextareaTarget; api.insertTextIntoContentEditable = insertTextIntoContentEditableTarget; api.insertIntoInputOrTextarea = insertTextIntoInputTarget; api.insertIntoContentEditable = insertTextIntoContentEditableTarget; api.insertIntoCodeMirror = insertTextIntoCodeMirrorTarget; api.insertIntoMonaco = insertTextIntoMonacoTarget; api.insertIntoAce = insertTextIntoAceTarget; api.extractSelectedText = extractCopyUnlockSelectedText; api.extractVisibleCodeText = extractCopyUnlockReadableText; api.extractReadablePageText = () => normalizeCopyUnlockText(collectContentTextFrom([])); api.installCopyUnlock = installClipboardCopyUnlock; api.installPasteUnlock = installClipboardPasteUnlock; api.enableUserSelect = enableCopyUnlockUserSelect; api.handleCopyEvent = handleClipboardCopyEvent; api.handlePasteEvent = handleClipboardPasteEvent; api.handleKeydown = handleClipboardKeydown; return api; } function clearClipboardInlineBlocks() { [document, document.body, document.documentElement].filter(Boolean).forEach((node) => { try { node.oncopy = null; } catch (_) { } try { node.onpaste = null; } catch (_) { } try { node.oncut = null; } catch (_) { } try { node.onselectstart = null; } catch (_) { } try { node.oncontextmenu = null; } catch (_) { } }); } function shouldSkipClipboardEnhancer() { if (window.__tougeAnswerApplying) { log('[ClipboardEnhancer] skipped because answer applying', 'info'); return true; } return false; } async function writeClipboardEnhancerText(text) { const value = String(text || ''); if (!value.trim()) { log('[ClipboardEnhancer] copy failed reason=empty text', 'error'); return false; } try { if (typeof GM_setClipboard === 'function') { GM_setClipboard(value); log('[ClipboardEnhancer] write clipboard success', 'success'); return true; } } catch (_) { } const ok = await copyText(value); log(ok ? '[ClipboardEnhancer] write clipboard success' : '[ClipboardEnhancer] clipboard write failed', ok ? 'success' : 'error'); return ok; } async function readClipboardEnhancerText(event) { const eventText = event && event.clipboardData ? event.clipboardData.getData('text/plain') : ''; if (eventText) return eventText; try { if (navigator.clipboard && navigator.clipboard.readText) { return await navigator.clipboard.readText(); } } catch (err) { log('[ClipboardEnhancer] clipboard read failed reason=' + String(err && err.message || err), 'error'); } return ''; } function getActiveClipboardTarget(eventTarget) { const raw = eventTarget && eventTarget.nodeType === Node.ELEMENT_NODE ? eventTarget : eventTarget && eventTarget.parentElement; const active = document.activeElement; const candidates = [raw, active].filter(Boolean); for (const node of candidates) { if (isEditableInput(node)) return { type: node.tagName === 'TEXTAREA' ? 'textarea' : 'input', el: node }; const contentEditable = getContentEditableTarget(node); if (contentEditable) return { type: 'contenteditable', el: contentEditable }; const cm = node.closest && node.closest('.CodeMirror'); if (cm) return { type: 'CodeMirror', el: cm }; const monaco = node.closest && node.closest('.monaco-editor'); if (monaco) return { type: 'Monaco', el: monaco }; const ace = node.closest && node.closest('.ace_editor'); if (ace) return { type: 'Ace', el: ace }; } return { type: 'none', el: null }; } function insertTextIntoInputTarget(el, text) { if (!el || !isEditableInput(el)) return false; insertTextIntoInput(el, text); log('[ClipboardEnhancer] inserted into input length=' + String(text || '').length, 'success'); log('[ClipboardEnhancer] dispatched input/change', 'info'); return true; } function insertTextIntoTextareaTarget(el, text) { if (!el || el.tagName !== 'TEXTAREA') return false; insertTextIntoInput(el, text); log('[ClipboardEnhancer] inserted into textarea length=' + String(text || '').length, 'success'); log('[ClipboardEnhancer] dispatched input/change', 'info'); return true; } function insertTextIntoContentEditableTarget(el, text) { if (!el) return false; const ok = insertTextIntoContentEditable(el, text); if (ok) { log('[ClipboardEnhancer] inserted into contenteditable length=' + String(text || '').length, 'success'); log('[ClipboardEnhancer] dispatched input/change', 'info'); } return ok; } function insertTextIntoCodeMirrorTarget(el, text) { const cmEl = el && (el.classList && el.classList.contains('CodeMirror') ? el : el.closest && el.closest('.CodeMirror')); const cm = cmEl && cmEl.CodeMirror; if (!cm || typeof cm.replaceSelection !== 'function') return false; cm.replaceSelection(String(text || '')); try { cm.focus(); } catch (_) { } log('[ClipboardEnhancer] inserted into CodeMirror length=' + String(text || '').length, 'success'); return true; } function insertTextIntoMonacoTarget(el, text) { const monacoEl = el && (el.classList && el.classList.contains('monaco-editor') ? el : el.closest && el.closest('.monaco-editor')); if (!monacoEl) return false; const textarea = monacoEl.querySelector('textarea.inputarea, textarea'); if (textarea) { textarea.focus(); const ok = document.execCommand && document.execCommand('insertText', false, String(text || '')); if (ok) { log('[ClipboardEnhancer] inserted into Monaco length=' + String(text || '').length, 'success'); return true; } return insertTextIntoInputTarget(textarea, text); } return false; } function insertTextIntoAceTarget(el, text) { const aceEl = el && (el.classList && el.classList.contains('ace_editor') ? el : el.closest && el.closest('.ace_editor')); const editor = aceEl && aceEl.env && aceEl.env.editor; if (!editor || typeof editor.insert !== 'function') return false; editor.insert(String(text || '')); log('[ClipboardEnhancer] inserted into Ace length=' + String(text || '').length, 'success'); return true; } function insertTextByTargetInfo(targetInfo, text) { if (!targetInfo || !targetInfo.el) return false; if (targetInfo.type === 'input') return insertTextIntoInputTarget(targetInfo.el, text); if (targetInfo.type === 'textarea') return insertTextIntoTextareaTarget(targetInfo.el, text); if (targetInfo.type === 'contenteditable') return insertTextIntoContentEditableTarget(targetInfo.el, text); if (targetInfo.type === 'CodeMirror') return insertTextIntoCodeMirrorTarget(targetInfo.el, text); if (targetInfo.type === 'Monaco') return insertTextIntoMonacoTarget(targetInfo.el, text); if (targetInfo.type === 'Ace') return insertTextIntoAceTarget(targetInfo.el, text); return false; } async function pasteTextToActiveEditable(text, eventTarget) { if (shouldSkipClipboardEnhancer()) return false; const targetInfo = getActiveClipboardTarget(eventTarget); log('[ClipboardEnhancer] active target=' + targetInfo.type, 'info'); log('[PasteAudit] target=' + targetInfo.type, 'info'); if (!targetInfo.el) { log('[ClipboardEnhancer] paste skipped: no editable target', 'error'); return false; } const value = String(text || ''); log('[ClipboardEnhancer] clipboard text length=' + value.length, 'info'); log('[PasteAudit] clipboard plain length=' + value.length, 'info'); log('[PasteAudit] html ignored=true', 'info'); if (!value) { log('[ClipboardEnhancer] paste skipped: empty clipboard', 'error'); return false; } const ok = insertTextByTargetInfo(targetInfo, value); if (!ok) log('[ClipboardEnhancer] paste failed reason=insert failed', 'error'); if (ok) { log('[PasteAudit] inserted length=' + value.length, 'info'); log('[PasteAudit] line count=' + value.split('\n').length, 'info'); log('[PasteAudit] dispatched input/change', 'info'); } return ok; } async function handleClipboardPasteEvent(event) { const api = getClipboardEnhancer(); if (!api.enabled) return; if (shouldSkipClipboardEnhancer()) return; log('[LegacyClipboard] paste event detected', 'info'); log('[ClipboardEnhancer] paste requested', 'info'); const targetInfo = getActiveClipboardTarget(event.target); log('[ClipboardEnhancer] active target=' + targetInfo.type, 'info'); if (!targetInfo.el) { log('[ClipboardEnhancer] paste skipped: no editable target', 'error'); return; } const text = await readClipboardEnhancerText(event); if (!text) { log('[ClipboardEnhancer] paste skipped: empty clipboard', 'error'); return; } event.preventDefault(); event.stopPropagation(); if (event.stopImmediatePropagation) event.stopImmediatePropagation(); await pasteTextToActiveEditable(text, targetInfo.el); } async function handleClipboardCopyEvent(event) { const api = getClipboardEnhancer(); if (!api.enabled) return; log('[LegacyClipboard] copy event detected', 'info'); log('[ClipboardEnhancer] copy requested', 'info'); const selected = extractCopyUnlockSelectedText(); log('[ClipboardEnhancer] selected text length=' + selected.length, 'info'); const source = selected ? 'selection/input' : 'fallback'; const text = selected ? String(selected).replace(/\r\n/g, '\n').replace(/\r/g, '\n') : extractCopyUnlockReadableText(); if (!text) { log('[ClipboardEnhancer] copy failed reason=fallback extraction empty', 'error'); return; } logCopyAudit(source, selected || text, text); try { if (event && event.clipboardData) { event.clipboardData.setData('text/plain', text); event.preventDefault(); event.stopPropagation(); if (event.stopImmediatePropagation) event.stopImmediatePropagation(); log('[LegacyClipboard] copied length=' + text.length, 'success'); log('[ClipboardEnhancer] copied length=' + text.length, 'success'); return; } } catch (_) { } const ok = await writeClipboardEnhancerText(text); if (ok) log('[ClipboardEnhancer] copied length=' + text.length, 'success'); } async function handleClipboardKeydown(event) { const api = getClipboardEnhancer(); if (!api.enabled) return; if (shouldSkipClipboardEnhancer()) return; const isCopyKey = (event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey && (event.code === 'KeyC' || String(event.key || '').toLowerCase() === 'c'); const isPasteKey = (event.ctrlKey || event.metaKey) && !event.shiftKey && !event.altKey && (event.code === 'KeyV' || String(event.key || '').toLowerCase() === 'v'); if (!isCopyKey && !isPasteKey) return; if (isCopyKey) { log('[LegacyClipboard] Ctrl+C detected', 'info'); return; } log('[LegacyClipboard] Ctrl+V detected', 'info'); } function installClipboardCopyUnlock() { const api = getClipboardEnhancer(); if (api.copyInstalled || window.__tougeLegacyCopyInstalled) { log('[LegacyGuard] copy listener already installed, skip', 'info'); return; } api.copyInstalled = true; window.__tougeLegacyCopyInstalled = true; document.addEventListener('copy', handleClipboardCopyEvent, true); document.addEventListener('selectstart', (event) => event.stopPropagation(), true); document.addEventListener('contextmenu', (event) => event.stopPropagation(), true); log('[ClipboardEnhancer] copy listener installed', 'success'); } function installClipboardPasteUnlock() { const api = getClipboardEnhancer(); if (api.pasteInstalled || window.__tougeLegacyPasteInstalled) { log('[LegacyGuard] paste listener already installed, skip', 'info'); } else { api.pasteInstalled = true; window.__tougeLegacyPasteInstalled = true; document.addEventListener('paste', handleClipboardPasteEvent, true); log('[ClipboardEnhancer] paste listener installed', 'success'); } if (api.keydownInstalled || window.__tougeLegacyKeydownInstalled) { log('[LegacyGuard] keydown listener already installed, skip', 'info'); } else { api.keydownInstalled = true; window.__tougeLegacyKeydownInstalled = true; document.addEventListener('keydown', handleClipboardKeydown, true); log('[ClipboardEnhancer] keydown listener installed', 'success'); } } function initClipboardEnhancer() { const api = getClipboardEnhancer(); log('[LegacyMerge] tougeClipboardEnhancer disabled; using original legacy helper flow', 'info'); return api; if (window.__tougeClipboardEnhancerInstalled) return api; window.__tougeClipboardEnhancerInstalled = true; api.enabled = api.enabled !== false; log('[ClipboardEnhancer] init', 'info'); clearClipboardInlineBlocks(); enableCopyUnlockUserSelect(); installClipboardCopyUnlock(); installClipboardPasteUnlock(); log('[ClipboardEnhancer] enabled=' + String(api.enabled), 'info'); return api; } function extractFirstJsonArray(raw) { const s = String(raw || ''); const start = s.indexOf('['); if (start < 0) return null; let depth = 0; let inStr = false; let esc = false; for (let i = start; i < s.length; i++) { const ch = s[i]; if (inStr) { if (esc) { esc = false; continue; } if (ch === '\\') { esc = true; continue; } if (ch === '"') inStr = false; continue; } if (ch === '"') { inStr = true; continue; } if (ch === '[') depth++; if (ch === ']') { depth--; if (depth === 0) { const candidate = s.slice(start, i + 1); try { const arr = JSON.parse(candidate); return Array.isArray(arr) ? arr : null; } catch (_) { return null; } } } } return null; } function saveScannedImageState() { try { const normalized = Array.from(scannedImageGlobalIndexes) .map(v => Number(v)) .filter(v => Number.isFinite(v) && v > 0); localStorage.setItem(SCANNED_IMAGE_GLOBAL_INDEXES_KEY, JSON.stringify(normalized)); log('[SAVE_IMAGE_INDEXES] ' + JSON.stringify(normalized.sort((a, b) => a - b)), 'info'); } catch (_) { } } function loadScannedImageState() { try { const saved = JSON.parse(localStorage.getItem(SCANNED_IMAGE_GLOBAL_INDEXES_KEY) || '[]'); scannedImageIndexes.clear(); scannedImageGlobalIndexes.clear(); if (Array.isArray(saved)) { saved.forEach(v => { const n = Number(v); if (Number.isFinite(n) && n > 0) { scannedImageIndexes.add(n); scannedImageGlobalIndexes.add(n); } }); } const restored = Array.from(scannedImageGlobalIndexes).sort((a, b) => a - b); log('[Visual Restore] scannedImageGlobalIndexes=' + JSON.stringify(restored), 'info'); log('[RESTORE_IMAGE_INDEXES] ' + JSON.stringify(restored), 'info'); } catch (_) { scannedImageIndexes.clear(); scannedImageGlobalIndexes.clear(); log('[Visual Restore] scannedImageGlobalIndexes=[]', 'info'); log('[RESTORE_IMAGE_INDEXES] []', 'info'); } } const VISUAL_QUESTION_KEYS_KEY = 'touge_visual_question_keys'; const SUSPICIOUS_QUESTION_KEYS_KEY = 'touge_suspicious_question_keys'; const VISUAL_QUESTION_REASON = '\u9898\u76ee\u5305\u542b\u56fe\u7247/\u56fe\u8868\uff0c\u6587\u672c\u590d\u5236\u53ef\u80fd\u4e0d\u5b8c\u6574\uff0c\u8bf7\u4eba\u5de5\u67e5\u770b\u9875\u9762'; const TYPE_LABELS = { single: '\u5355\u9009\u9898', multiple: '\u591a\u9009\u9898', fill: '\u586b\u7a7a\u9898', judge: '\u5224\u65ad\u9898' }; const visualQuestionKeys = new Set(); const suspiciousQuestionKeys = new Set(); function normalizeQuestionId(id) { if (id === undefined || id === null) return ''; const text = String(id).trim(); const match = text.match(/\d+/); return match ? match[0] : text; } function normalizeQuestionTypeCode(type) { const text = normalizeText(type || '').toLowerCase(); if (!text) return ''; if (['single', 'radio'].includes(text) || text.includes('\u5355\u9009')) return 'single'; if (['multiple', 'multi', 'checkbox'].includes(text) || text.includes('\u591a\u9009')) return 'multiple'; if (['judge', 'truefalse'].includes(text) || text.includes('\u5224\u65ad')) return 'judge'; if (['fill', 'blank'].includes(text) || text.includes('\u586b\u7a7a')) return 'fill'; return ''; } function getQuestionTypeLabel(type) { return TYPE_LABELS[normalizeQuestionTypeCode(type)] || ''; } function parseQuestionKey(key) { const text = String(key || '').trim(); const match = text.match(/^(single|multiple|judge|fill)-(\d+)$/i); if (!match) return null; return { questionKey: match[1].toLowerCase() + '-' + Number(match[2]), questionType: match[1].toLowerCase(), localIndex: Number(match[2]) }; } function normalizeQuestionMeta(q) { if (!q || typeof q !== 'object') return q; const parsed = parseQuestionKey(q.questionKey); if (!parsed) return q; q.questionKey = parsed.questionKey; q.questionType = parsed.questionType; q.localIndex = parsed.localIndex; q.typeLabel = TYPE_LABELS[parsed.questionType] || q.typeLabel || ''; return q; } function saveQuestion(question) { if (!question || typeof question !== 'object') { log('missing question data, skip save', 'error'); return false; } const globalIndex = Number(question.globalIndex) || nextGlobalIndex++; question.globalIndex = globalIndex; questionStore.set(globalIndex, question); allQuestions = Array.from(questionStore.values()); log('[collect] saved #' + globalIndex, 'success'); return true; } function buildQuestionKey(questionType, localIndex) { const typeCode = normalizeQuestionTypeCode(questionType); const index = normalizeQuestionId(localIndex); return typeCode && index ? typeCode + '-' + index : ''; } function getStoredQuestionKeys(storageKey) { try { const parsed = JSON.parse(localStorage.getItem(storageKey) || '[]'); return Array.isArray(parsed) ? parsed.map(parseQuestionKey).filter(Boolean).map(item => item.questionKey) : []; } catch (_) { return []; } } function rememberQuestionRiskKey(storageKey, memorySet, questionKey) { const parsed = parseQuestionKey(questionKey); if (!parsed) return; const key = parsed.questionKey; memorySet.add(key); try { const keys = new Set(getStoredQuestionKeys(storageKey)); keys.add(key); localStorage.setItem(storageKey, JSON.stringify(Array.from(keys))); } catch (_) { } } function rememberVisualQuestionKey(questionKey) { rememberQuestionRiskKey(VISUAL_QUESTION_KEYS_KEY, visualQuestionKeys, questionKey); } function rememberSuspiciousQuestionKey(questionKey) { rememberQuestionRiskKey(SUSPICIOUS_QUESTION_KEYS_KEY, suspiciousQuestionKeys, questionKey); } function isLocalVisualQuestionKey(questionKey) { const parsed = parseQuestionKey(questionKey); if (!parsed) return false; const key = parsed.questionKey; return visualQuestionKeys.has(key) || getStoredQuestionKeys(VISUAL_QUESTION_KEYS_KEY).includes(key) || allQuestions.some(q => q.questionKey === key && q.visualRiskLevel === 'image'); } function isLocalSuspiciousQuestionKey(questionKey) { const parsed = parseQuestionKey(questionKey); if (!parsed) return false; const key = parsed.questionKey; return suspiciousQuestionKeys.has(key) || getStoredQuestionKeys(SUSPICIOUS_QUESTION_KEYS_KEY).includes(key) || allQuestions.some(q => q.questionKey === key && q.visualRiskLevel === 'suspicious'); } function getAnswerQuestionKey(answer) { if (!answer || typeof answer !== 'object') return ''; const parsed = parseQuestionKey(answer.questionKey); if (parsed) return parsed.questionKey; return buildQuestionKey(answer.questionType || answer.type || answer.kind, answer.localIndex ?? answer.id ?? answer.qNum ?? answer.index); } function getCurrentQuestionTarget(styleType = 'scanned') { const current = getCurrentQuestionKey(); if (current && current.questionKey) { return { styleType, questionType: current.questionType, localIndex: current.localIndex, questionKey: current.questionKey }; } const info = getCurrentQuestionInfo(); const questionType = normalizeQuestionTypeCode(info.qType || detectQuestionType()); const localIndex = normalizeQuestionId(info.qNum); const questionKey = buildQuestionKey(questionType, localIndex); return { styleType, questionType, localIndex, questionKey }; } function isVisibleElement(el) { if (!el || el.closest('#main-panel') || el.closest('[class*="answerSheet"]')) return false; try { const style = getComputedStyle(el); if (!style || style.display === 'none' || style.visibility === 'hidden') return false; } catch (_) { } const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null; return !!rect && rect.width > 0 && rect.height > 0; } function getCurrentQuestionRoot() { const isBadRoot = (el) => !el || el === document.body || el === document.documentElement || !!el.closest('#main-panel, [class*="answerSheet"]'); const hasQuestionSignals = (el) => { if (!el || !el.querySelector) return false; const hasTitle = !!el.querySelector('.question_title'); const hasOptions = !!el.querySelector('label.ant-radio-wrapper, .ant-radio-wrapper, label.ant-checkbox-wrapper, .ant-checkbox-wrapper'); const hasInput = !!el.querySelector('input[type="text"], input[type="search"], textarea, [contenteditable="true"]'); return hasTitle || hasOptions || hasInput; }; const typeTitle = document.querySelector('.questionTypeTitle___r6Fo9, [class*="questionTypeTitle"]'); if (typeTitle && !typeTitle.closest('#main-panel, [class*="answerSheet"]')) { let probe = typeTitle.parentElement; while (probe && probe !== document.body) { if (!isBadRoot(probe) && hasQuestionSignals(probe)) return probe; probe = probe.parentElement; } } const title = document.querySelector('.question_title'); if (title && !title.closest('#main-panel, [class*="answerSheet"]')) { let probe = title.parentElement; while (probe && probe !== document.body) { if (!isBadRoot(probe) && hasQuestionSignals(probe)) return probe; probe = probe.parentElement; } } const firstOption = Array.from(document.querySelectorAll('label.ant-radio-wrapper, .ant-radio-wrapper, label.ant-checkbox-wrapper, .ant-checkbox-wrapper')) .find(el => isVisibleElement(el)); if (firstOption) { let probe = firstOption.parentElement; while (probe && probe !== document.body) { if (!isBadRoot(probe) && hasQuestionSignals(probe)) return probe; probe = probe.parentElement; } } return null; } function getVisibleCurrentOptions(root) { if (!root || !root.querySelectorAll) { return { radioOptions: [], checkboxOptions: [] }; } const clean = (list) => Array.from(new Set(list)).filter(el => isVisibleElement(el) && !el.closest('#main-panel, [class*="answerSheet"]')); const radioOptions = clean(root.querySelectorAll('label.ant-radio-wrapper, .ant-radio-wrapper')); const checkboxOptions = clean(root.querySelectorAll('label.ant-checkbox-wrapper, .ant-checkbox-wrapper')); return { radioOptions, checkboxOptions }; } function isJudgePairTexts(texts = []) { if (!Array.isArray(texts) || texts.length !== 2) return false; const hasTrue = texts.some(t => /(^|\b)(\u6b63\u786e|\u5bf9|true|t)($|\b)/i.test(String(t || ''))); const hasFalse = texts.some(t => /(^|\b)(\u9519\u8bef|\u9519|false|f)($|\b)/i.test(String(t || ''))); return hasTrue && hasFalse; } function detectCurrentDomType() { const root = getCurrentQuestionRoot(); const { radioOptions, checkboxOptions } = getVisibleCurrentOptions(root); const radioTexts = radioOptions.map(el => normalizeText(el.innerText || el.textContent || '')); const fillInputs = root ? Array.from(root.querySelectorAll('input:not([type]), input[type="text"], input[type="search"], textarea, [contenteditable="true"]')) .filter(el => !el.disabled && !el.readOnly && isVisibleElement(el)) : []; let type = 'unknown'; if (checkboxOptions.length > 0) { type = 'multiple'; } else if (radioOptions.length > 0) { if (radioOptions.length === 2 && isJudgePairTexts(radioTexts)) { type = 'judge'; } else { type = 'single'; } } else if (fillInputs.length > 0) { type = 'fill'; } return { type, root, radioOptions, checkboxOptions, radioCount: radioOptions.length, checkboxCount: checkboxOptions.length, radioTexts, fillCount: fillInputs.length, isStrictJudgePair: radioOptions.length === 2 && isJudgePairTexts(radioTexts), rootTextHead: normalizeText((root && (root.innerText || root.textContent) || '')).slice(0, 160) }; } function getCurrentQuestionKey() { const typeEl = document.querySelector('.questionTypeTitle___r6Fo9, [class*="questionTypeTitle"]'); const rawType = normalizeText(typeEl ? typeEl.textContent : ''); const cleanType = rawType.replace(/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341]+[\u3001.\uff0e\s]*/, ''); const questionType = normalizeQuestionTypeCode(cleanType); const groupLabel = getQuestionTypeLabel(questionType); if (!questionType || !groupLabel) return null; const groups = Array.from(document.querySelectorAll('[class*="answerSheetWrap"]')); for (const group of groups) { const titleText = normalizeText( (group.querySelector('[class*="answerSheetQuestionTitle"] [class*="c-grey-666"]') || group.querySelector('[class*="answerSheetQuestionTitle"]'))?.textContent || '' ); if (!titleText || titleText !== groupLabel) continue; const selectedItem = group.querySelector('[class*="answerSheetItem"][class*="selected"]'); if (!selectedItem) continue; const indexEl = selectedItem.querySelector('[class*="qindex"]'); const localIndex = normalizeQuestionId(indexEl ? indexEl.textContent : ''); const questionKey = buildQuestionKey(questionType, localIndex); if (!questionKey) return null; return { questionType, localIndex, questionKey }; } return null; } function getCurrentGlobalIndexFromAnswerSheet() { const groups = Array.from(document.querySelectorAll('[class*="answerSheetWrap"]')); let offset = 0; for (let g = 0; g < groups.length; g++) { const group = groups[g]; const titleEl = group.querySelector('[class*="answerSheetQuestionTitle"] [class*="c-grey-666"]') || group.querySelector('[class*="answerSheetQuestionTitle"]'); const groupTitle = normalizeText(titleEl ? titleEl.textContent : ''); const items = Array.from(group.querySelectorAll('[class*="answerSheetItem"]')); for (let i = 0; i < items.length; i++) { const item = items[i]; const className = String(item.className || ''); const hasSelected = /selected/i.test(className) || !!item.querySelector('[class*="selected"]'); if (!hasSelected) continue; const indexEl = item.querySelector('[class*="qindex"]'); const localIndex = Number(normalizeQuestionId(indexEl ? indexEl.textContent : '')) || (i + 1); const globalIndex = offset + localIndex; return { globalIndex, groupIndex: g + 1, localIndex, groupTitle, totalBefore: offset }; } offset += items.length; } log('[Index Debug] cannot find selected answer sheet item', 'error'); return null; } function log(message, type = 'info') { try { const panel = document.getElementById('question-log-content'); if (!panel) { console[(type === 'error' ? 'error' : type === 'success' ? 'log' : 'info')](message); return; } const line = document.createElement('div'); line.style.margin = '2px 0'; line.style.fontSize = '12px'; switch (type) { case 'success': line.style.color = '#7ee787'; break; case 'error': line.style.color = '#ff7b72'; break; default: line.style.color = 'rgba(255,255,255,0.76)'; } const time = new Date().toLocaleTimeString(); line.innerHTML = `[${time}] ${message}`; panel.appendChild(line); panel.scrollTop = panel.scrollHeight; } catch (_) { } } function setAssistantStatus(text, type = 'normal') { const status = document.getElementById('assistant-status'); if (!status) return; status.textContent = text; status.dataset.type = type; } function updateQuestionSwitchButton() { const switchBtn = document.getElementById('question-switch'); if (!switchBtn) return; switchBtn.innerText = enabled ? '题目收集: 开' : '题目收集: 关'; switchBtn.classList.toggle('is-on', enabled); } function isMac() { return /Mac|iPhone|iPad|iPod/i.test(navigator.platform); } function getBestEditor() { const editors = Array.from(document.querySelectorAll('.monaco-editor')); if (editors.length === 0) return null; editors.sort((a, b) => { const aLines = a.querySelectorAll('.view-lines .view-line').length; const bLines = b.querySelectorAll('.view-lines .view-line').length; return bLines - aLines; }); return editors[0]; } function getEditorInput(editor) { if (!editor) return null; return editor.querySelector('textarea.inputarea') || editor.querySelector('textarea') || editor; } function fireKey(target, type, key, code, keyCode, options = {}) { const event = new KeyboardEvent(type, { key, code, keyCode, which: keyCode, bubbles: true, cancelable: true, composed: true, ctrlKey: !!options.ctrlKey, metaKey: !!options.metaKey, shiftKey: !!options.shiftKey, altKey: !!options.altKey }); target.dispatchEvent(event); } async function copyVisibleMonacoCodeByOriginalMethod(copyBtn, options = {}) { const resetStatus = options.resetStatus !== false; const oldText = copyBtn ? copyBtn.textContent : ''; if (copyBtn) { copyBtn.disabled = true; copyBtn.textContent = '复制中'; } setAssistantStatus('正在复制代码', 'normal'); log('[CodeCopy] start original Monaco copy method', 'info'); const editor = getBestEditor(); if (!editor) { setAssistantStatus('未找到代码编辑器', 'error'); log('[CodeCopy] no monaco editor found', 'error'); if (copyBtn) { copyBtn.disabled = false; copyBtn.textContent = oldText; } return false; } log('[CodeCopy] editor found', 'info'); const input = getEditorInput(editor); if (!input) { setAssistantStatus('未找到输入区域', 'error'); log('[CodeCopy] no editor input found', 'error'); if (copyBtn) { copyBtn.disabled = false; copyBtn.textContent = oldText; } return false; } try { editor.scrollIntoView({ block: 'center', inline: 'center' }); } catch (_) { } await sleep(80); try { editor.click(); input.focus(); input.click(); log('[CodeCopy] input focused', 'info'); } catch (_) { } await sleep(80); const useMeta = isMac(); log('[CodeCopy] select all by ' + (useMeta ? 'Meta+A' : 'Ctrl+A'), 'info'); fireKey(input, 'keydown', useMeta ? 'Meta' : 'Control', useMeta ? 'MetaLeft' : 'ControlLeft', useMeta ? 91 : 17, { ctrlKey: !useMeta, metaKey: useMeta }); fireKey(input, 'keydown', 'a', 'KeyA', 65, { ctrlKey: !useMeta, metaKey: useMeta }); fireKey(input, 'keyup', 'a', 'KeyA', 65, { ctrlKey: !useMeta, metaKey: useMeta }); fireKey(input, 'keyup', useMeta ? 'Meta' : 'Control', useMeta ? 'MetaLeft' : 'ControlLeft', useMeta ? 91 : 17, { ctrlKey: false, metaKey: false }); await sleep(120); let ok = false; try { ok = document.execCommand('copy'); } catch (_) { ok = false; } log('[CodeCopy] execCommand copy result=' + String(ok), ok ? 'success' : 'error'); await sleep(80); if (ok) { setAssistantStatus('代码已复制到剪贴板', 'success'); log('[CodeCopy] copied', 'success'); if (copyBtn) copyBtn.textContent = '已复制'; if (resetStatus || copyBtn) { setTimeout(() => { if (resetStatus) setAssistantStatus('Ready', 'normal'); if (copyBtn) { copyBtn.disabled = false; copyBtn.textContent = oldText; } }, 1200); } return true; } setAssistantStatus('复制失败,请手动点进代码区后重试', 'error'); log('[CodeCopy] execCommand copy result=false', 'error'); if (copyBtn) { copyBtn.disabled = false; copyBtn.textContent = oldText; } return false; } async function copyMonacoCode(copyBtn, options = {}) { return copyVisibleMonacoCodeByOriginalMethod(copyBtn, options); } function injectAssistantPanelStyle() { if (document.getElementById('assistant-panel-style')) return; const style = document.createElement('style'); style.id = 'assistant-panel-style'; style.textContent = ` #main-panel { position: fixed; right: 28px; bottom: 28px; width: 520px; height: 520px; min-width: min(360px, calc(100vw - 16px)); min-height: min(320px, calc(100vh - 16px)); max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); z-index: 10000; display: flex; flex-direction: column; color: rgba(255, 255, 255, 0.94); background: rgba(28, 28, 30, 0.58); backdrop-filter: blur(24px) saturate(180%); -webkit-backdrop-filter: blur(24px) saturate(180%); border: 1px solid rgba(255, 255, 255, 0.18); border-radius: 22px; box-shadow: 0 18px 50px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.18); font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", sans-serif; overflow: auto; resize: both; user-select: none; } #main-panel.is-minimized { width: 168px !important; height: 56px !important; min-width: 140px; min-height: 48px; max-width: 180px; max-height: 60px; resize: none; overflow: hidden; border-radius: 999px; } #main-panel.is-minimized #assistant-status, #main-panel.is-minimized #main-panel-actions, #main-panel.is-minimized #main-panel-content { display: none; } #main-panel.is-minimized #main-panel-header { height: 100%; padding: 0 10px 0 18px; } #main-panel.is-minimized #main-panel-title { font-size: 14px; white-space: nowrap; } #main-panel-header { padding: 16px 18px 10px; display: flex; align-items: center; justify-content: space-between; gap: 12px; cursor: move; flex-shrink: 0; } #main-panel-title { font-size: 17px; line-height: 1.2; font-weight: 750; letter-spacing: 0.2px; } #hide-main-panel-btn { width: 30px; height: 30px; border: 1px solid rgba(255,255,255,0.16); border-radius: 999px; color: rgba(255,255,255,0.88); background: rgba(255,255,255,0.12); cursor: pointer; font-size: 18px; line-height: 1; transition: transform .15s ease, background .15s ease, border-color .15s ease; } #hide-main-panel-btn:hover { transform: translateY(-1px); background: rgba(255,255,255,0.18); border-color: rgba(255,255,255,0.24); } #hide-main-panel-btn:active { transform: scale(0.96); } #assistant-status { margin: 0 18px 14px; padding: 9px 12px; border-radius: 14px; background: rgba(255,255,255,0.10); border: 1px solid rgba(255,255,255,0.12); color: rgba(255,255,255,0.68); font-size: 12px; letter-spacing: 0.2px; flex-shrink: 0; } #assistant-status[data-type="success"] { color: #7ee787; } #assistant-status[data-type="error"] { color: #ff7b72; } #main-panel-actions { padding: 0 18px 14px; flex-shrink: 0; } #main-panel-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } #main-panel button.assistant-btn { min-height: 44px; padding: 9px 10px; border: none; border-radius: 14px; color: #fff; background: linear-gradient(180deg, rgba(10,132,255,0.98), rgba(0,95,220,0.96)); box-shadow: 0 8px 18px rgba(0, 122, 255, 0.28), inset 0 1px 0 rgba(255,255,255,0.25); cursor: pointer; font-size: 13px; font-weight: 700; transition: transform .15s ease, box-shadow .15s ease, filter .15s ease, opacity .15s ease; } #main-panel button.assistant-btn:hover { transform: translateY(-1px); filter: brightness(1.08); box-shadow: 0 11px 24px rgba(0, 122, 255, 0.38), inset 0 1px 0 rgba(255,255,255,0.30); } #main-panel button.assistant-btn:active { transform: translateY(1px) scale(0.98); } #main-panel button.assistant-btn:disabled { opacity: 0.68; cursor: default; transform: none; } #main-panel button.assistant-btn.is-on { background: linear-gradient(180deg, rgba(48, 209, 88, 0.98), rgba(25, 135, 68, 0.96)); box-shadow: 0 8px 18px rgba(48, 209, 88, 0.24), inset 0 1px 0 rgba(255,255,255,0.24); } #experiment-collect-btn { min-height: 44px; font-size: 13px; grid-column: auto; } #main-panel-content { display: flex; flex-direction: column; gap: 12px; min-height: 0; flex: 1; padding: 0 18px 18px; overflow: hidden; } .assistant-section { display: flex; flex-direction: column; min-height: 0; padding: 12px; border-radius: 18px; background: rgba(255,255,255,0.10); border: 1px solid rgba(255,255,255,0.12); } #clipboard-section { position: relative; display: none; max-height: 260px; flex: 0 0 auto; } #clipboard-section.is-visible { display: flex; } #clipboard-close-btn { position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; border: 1px solid rgba(255,255,255,0.14); border-radius: 999px; color: rgba(255,255,255,0.76); background: rgba(255,255,255,0.10); cursor: pointer; line-height: 1; transition: transform .15s ease, background .15s ease; } #clipboard-close-btn:hover { transform: translateY(-1px); background: rgba(255,255,255,0.18); } #clipboard-drag-hint { margin: -2px 32px 8px 0; color: rgba(255,255,255,0.56); font-size: 12px; line-height: 1.45; user-select: text; -webkit-user-select: text; } #clipboard-preview { width: 100%; min-height: 86px; max-height: 120px; box-sizing: border-box; padding: 10px; border: 1px solid rgba(255,255,255,0.12); border-radius: 14px; outline: none; resize: vertical; background: rgba(0,0,0,0.18); color: rgba(255,255,255,0.88); font-family: ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono", monospace; font-size: 12px; line-height: 1.55; white-space: pre-wrap; user-select: text; -webkit-user-select: text; cursor: text; } #clipboard-preview::selection, .drag-drop-text-block::selection { background: rgba(10,132,255,0.45); color: #fff; } #clipboard-drag-list { display: grid; gap: 8px; max-height: 94px; overflow-y: auto; margin-top: 10px; padding-right: 2px; scrollbar-width: thin; user-select: text; -webkit-user-select: text; } .drag-drop-text-block { padding: 8px 10px; border: 1px solid rgba(255,255,255,0.13); border-radius: 12px; background: rgba(255,255,255,0.09); color: rgba(255,255,255,0.84); font-size: 12px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; cursor: grab; user-select: text; -webkit-user-select: text; transition: transform .15s ease, background .15s ease, border-color .15s ease; } .drag-drop-text-block:hover { transform: translateY(-1px); background: rgba(255,255,255,0.14); border-color: rgba(255,255,255,0.22); } .drag-drop-text-block:active { cursor: grabbing; transform: scale(0.99); } #log-section { flex: 0 0 auto; padding: 0; overflow: hidden; } #log-section.is-expanded { padding: 12px; } #log-toggle-btn { width: 100%; min-height: 34px; border: 1px solid rgba(255,255,255,0.12); border-radius: 14px; color: rgba(255,255,255,0.78); background: rgba(255,255,255,0.10); cursor: pointer; font-size: 12px; font-weight: 750; letter-spacing: 0.3px; transition: transform .15s ease, background .15s ease; } #log-toggle-btn:hover { transform: translateY(-1px); background: rgba(255,255,255,0.16); } #log-section:not(.is-expanded) #question-log-content { display: none; } #log-section.is-expanded #question-log-content { display: block; height: 150px; max-height: 180px; margin-top: 10px; } .assistant-section-title { margin-bottom: 8px; color: rgba(255,255,255,0.74); font-size: 12px; font-weight: 750; letter-spacing: 0.3px; } #question-log-content { flex: 1; min-height: 0; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; user-select: text; color: rgba(255,255,255,0.82); font-size: 12px; line-height: 1.55; scrollbar-width: thin; } #question-log-content div { color: inherit; } @media (max-width: 560px) { #main-panel { width: calc(100vw - 24px); right: 12px; bottom: 12px; height: 76vh; } #main-panel-actions { grid-template-columns: 1fr; padding-bottom: 12px; } } `; document.head.appendChild(style); } function getMaxPanelWidth() { return Math.max(PANEL_MINIMIZED_WIDTH, window.innerWidth - 16); } function getMaxPanelHeight() { return Math.max(PANEL_MINIMIZED_HEIGHT, window.innerHeight - 16); } function clampPanelSize(width, height) { const maxWidth = getMaxPanelWidth(); const maxHeight = getMaxPanelHeight(); const minWidth = Math.min(PANEL_MIN_WIDTH, maxWidth); const minHeight = Math.min(PANEL_MIN_HEIGHT, maxHeight); return { width: Math.max(minWidth, Math.min(Number(width) || PANEL_DEFAULT_WIDTH, maxWidth)), height: Math.max(minHeight, Math.min(Number(height) || PANEL_DEFAULT_HEIGHT, maxHeight)) }; } function clampPanelToViewport(panel, left, top, width, height) { const rect = panel.getBoundingClientRect(); const panelWidth = width || rect.width; const panelHeight = height || rect.height; const margin = 8; const maxLeft = Math.max(margin, window.innerWidth - panelWidth - margin); const maxTop = Math.max(margin, window.innerHeight - panelHeight - margin); return { left: Math.max(margin, Math.min(left, maxLeft)), top: Math.max(margin, Math.min(top, maxTop)) }; } function readPanelState() { try { const saved = localStorage.getItem(PANEL_POS_KEY); if (!saved) return null; const state = JSON.parse(saved); if (!state || typeof state !== 'object') return null; return state; } catch (_) { return null; } } function getPanelState(panel) { const rect = panel.getBoundingClientRect(); const saved = readPanelState() || {}; const minimized = panel.classList.contains('is-minimized'); const width = minimized ? Number(saved.width) || PANEL_DEFAULT_WIDTH : rect.width; const height = minimized ? Number(saved.height) || PANEL_DEFAULT_HEIGHT : rect.height; return { left: Math.round(rect.left), top: Math.round(rect.top), width: Math.round(width), height: Math.round(height), minimized }; } function savePanelState(panel, patch = {}) { const current = getPanelState(panel); const next = Object.assign({}, current, patch); localStorage.setItem(PANEL_POS_KEY, JSON.stringify({ left: Math.round(Number(next.left) || 8), top: Math.round(Number(next.top) || 8), width: Math.round(Number(next.width) || PANEL_DEFAULT_WIDTH), height: Math.round(Number(next.height) || PANEL_DEFAULT_HEIGHT), minimized: !!next.minimized })); } function restorePanelState(panel) { const saved = readPanelState(); const minimized = !!(saved && saved.minimized); const size = clampPanelSize(saved && saved.width, saved && saved.height); const currentWidth = minimized ? PANEL_MINIMIZED_WIDTH : size.width; const currentHeight = minimized ? PANEL_MINIMIZED_HEIGHT : size.height; const defaultLeft = Math.max(8, window.innerWidth - currentWidth - 28); const defaultTop = Math.max(8, window.innerHeight - currentHeight - 28); const left = saved && Number.isFinite(Number(saved.left)) ? Number(saved.left) : defaultLeft; const top = saved && Number.isFinite(Number(saved.top)) ? Number(saved.top) : defaultTop; const next = clampPanelToViewport(panel, left, top, currentWidth, currentHeight); panel.dataset.expandedWidth = String(size.width); panel.dataset.expandedHeight = String(size.height); panel.style.width = minimized ? `${PANEL_MINIMIZED_WIDTH}px` : `${size.width}px`; panel.style.height = minimized ? `${PANEL_MINIMIZED_HEIGHT}px` : `${size.height}px`; panel.style.left = `${next.left}px`; panel.style.top = `${next.top}px`; panel.style.right = 'auto'; panel.style.bottom = 'auto'; setPanelMinimized(panel, minimized, { preserveSize: true, skipSave: true }); savePanelState(panel, { left: next.left, top: next.top, width: size.width, height: size.height, minimized }); } function makePanelDraggable(panel, handle) { let dragging = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; handle.addEventListener('pointerdown', (e) => { if (e.target.closest('button')) return; dragging = true; const rect = panel.getBoundingClientRect(); startX = e.clientX; startY = e.clientY; startLeft = rect.left; startTop = rect.top; panel.style.left = `${rect.left}px`; panel.style.top = `${rect.top}px`; panel.style.right = 'auto'; panel.style.bottom = 'auto'; document.body.style.userSelect = 'none'; try { handle.setPointerCapture(e.pointerId); } catch (_) { } }); handle.addEventListener('pointermove', (e) => { if (!dragging) return; const next = clampPanelToViewport(panel, startLeft + e.clientX - startX, startTop + e.clientY - startY); panel.style.left = `${next.left}px`; panel.style.top = `${next.top}px`; }); function stopDrag(e) { if (!dragging) return; dragging = false; document.body.style.userSelect = ''; savePanelState(panel); try { handle.releasePointerCapture(e.pointerId); } catch (_) { } } handle.addEventListener('pointerup', stopDrag); handle.addEventListener('pointercancel', stopDrag); window.addEventListener('resize', () => { const rect = panel.getBoundingClientRect(); const saved = readPanelState() || {}; const isMinimized = panel.classList.contains('is-minimized'); const size = clampPanelSize(saved.width || rect.width, saved.height || rect.height); const width = isMinimized ? PANEL_MINIMIZED_WIDTH : size.width; const height = isMinimized ? PANEL_MINIMIZED_HEIGHT : size.height; const next = clampPanelToViewport(panel, rect.left, rect.top, width, height); if (!isMinimized) { panel.style.width = `${size.width}px`; panel.style.height = `${size.height}px`; panel.dataset.expandedWidth = String(size.width); panel.dataset.expandedHeight = String(size.height); } panel.style.left = `${next.left}px`; panel.style.top = `${next.top}px`; panel.style.right = 'auto'; panel.style.bottom = 'auto'; savePanelState(panel, { left: next.left, top: next.top, width: size.width, height: size.height, minimized: isMinimized }); }); } function setPanelMinimized(panel, minimized, options = {}) { const wasMinimized = panel.classList.contains('is-minimized'); if (minimized && !wasMinimized && !options.preserveSize) { const rect = panel.getBoundingClientRect(); const size = clampPanelSize(rect.width, rect.height); panel.dataset.expandedWidth = String(size.width); panel.dataset.expandedHeight = String(size.height); } panel.classList.toggle('is-minimized', minimized); if (minimized) { panel.style.width = `${PANEL_MINIMIZED_WIDTH}px`; panel.style.height = `${PANEL_MINIMIZED_HEIGHT}px`; } else { const saved = readPanelState() || {}; const size = clampPanelSize( panel.dataset.expandedWidth || saved.width, panel.dataset.expandedHeight || saved.height ); panel.style.width = `${size.width}px`; panel.style.height = `${size.height}px`; panel.dataset.expandedWidth = String(size.width); panel.dataset.expandedHeight = String(size.height); } const rect = panel.getBoundingClientRect(); const expandedSize = clampPanelSize(panel.dataset.expandedWidth, panel.dataset.expandedHeight); const next = clampPanelToViewport(panel, rect.left, rect.top, rect.width, rect.height); panel.style.left = `${next.left}px`; panel.style.top = `${next.top}px`; panel.style.right = 'auto'; panel.style.bottom = 'auto'; const btn = document.getElementById('hide-main-panel-btn'); if (btn) { btn.textContent = minimized ? '+' : '-'; btn.title = minimized ? '展开' : '最小化'; } if (!options.skipSave) { savePanelState(panel, { left: next.left, top: next.top, width: expandedSize.width, height: expandedSize.height, minimized }); } } function observePanelResize(panel) { if (!window.ResizeObserver) return; let timer = null; const observer = new ResizeObserver(() => { if (panel.classList.contains('is-minimized')) return; clearTimeout(timer); timer = setTimeout(() => { const rect = panel.getBoundingClientRect(); const size = clampPanelSize(rect.width, rect.height); const next = clampPanelToViewport(panel, rect.left, rect.top, size.width, size.height); if (Math.round(rect.width) !== Math.round(size.width)) { panel.style.width = `${size.width}px`; } if (Math.round(rect.height) !== Math.round(size.height)) { panel.style.height = `${size.height}px`; } panel.style.left = `${next.left}px`; panel.style.top = `${next.top}px`; panel.style.right = 'auto'; panel.style.bottom = 'auto'; panel.dataset.expandedWidth = String(size.width); panel.dataset.expandedHeight = String(size.height); savePanelState(panel, { left: next.left, top: next.top, width: size.width, height: size.height, minimized: false }); }, 150); }); observer.observe(panel); } function scheduleClipboardPreviewCollapse() { clearTimeout(clipboardPreviewTimerId); clipboardPreviewTimerId = setTimeout(() => { if (!clipboardPreviewHovering) { hideClipboardPreview(); } else { scheduleClipboardPreviewCollapse(); } }, CLIPBOARD_PREVIEW_AUTO_HIDE_MS); } function collectAnswerDragItems(answer, answerIndex) { const items = []; const prefix = `答案 ${answerIndex + 1}`; function addValue(label, value) { if (value == null || value === false) return; if (Array.isArray(value)) { value.forEach((item, index) => addValue(`${label} ${index + 1}`, item)); return; } const text = String(value).trim(); if (!text) return; items.push({ title: `${prefix} ${label}`, text }); } if (answer && typeof answer === 'object' && !Array.isArray(answer)) { addValue('choiceValue', answer.choiceValue); addValue('choice', answer.choice); addValue('judge', answer.judge === true ? '正确' : answer.judge === false ? '错误' : null); addValue('填空', answer.fills); } else { addValue('文本', answer); } if (items.length === 0) { const fallback = typeof answer === 'string' ? answer : JSON.stringify(answer, null, 2); addValue('原文', fallback); } return items; } function splitDragTextBlocks(text) { const raw = String(text || '').trim(); if (!raw) return []; try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) { return parsed .flatMap((item, index) => collectAnswerDragItems(item, index)) .filter(item => item.text.trim()) .slice(0, 50); } } catch (_) { } const blocks = raw .split(/\n\s*\n+/) .map(part => part.trim()) .filter(Boolean); if (blocks.length > 1) { return blocks.slice(0, 30).map((part, index) => ({ title: `文本 ${index + 1}`, text: part })); } return raw .split(/\n+/) .map(part => part.trim()) .filter(Boolean) .slice(0, 30) .map((part, index) => ({ title: `文本 ${index + 1}`, text: part })); } function createDragTextBlock(item) { const block = document.createElement('div'); block.className = 'drag-drop-text-block'; block.draggable = true; block.title = '拖拽此文本到目标输入框'; block.dataset.dragText = item.text; block.textContent = `${item.title}:${item.text}`; block.addEventListener('dragstart', (event) => { const selection = String(window.getSelection && window.getSelection() || '').trim(); const dragText = selection || block.dataset.dragText || block.textContent || ''; try { event.dataTransfer.setData('text/plain', dragText); event.dataTransfer.effectAllowed = 'copy'; } catch (_) { } log('【拖拽投放】已准备文本,拖入目标输入框即可填入', 'info'); }); return block; } function showClipboardPreview(text) { const section = document.getElementById('clipboard-section'); const preview = document.getElementById('clipboard-preview'); const dragList = document.getElementById('clipboard-drag-list'); if (!section || !preview) return; if ('value' in preview) { preview.value = text; } else { preview.textContent = text; } if (dragList) { dragList.textContent = ''; const blocks = splitDragTextBlocks(text); blocks.forEach(item => { dragList.appendChild(createDragTextBlock(item)); }); } section.classList.add('is-visible'); scheduleClipboardPreviewCollapse(); } function hideClipboardPreview() { const section = document.getElementById('clipboard-section'); if (!section) return; clearTimeout(clipboardPreviewTimerId); clipboardPreviewTimerId = null; section.classList.remove('is-visible'); } function setLogPanelExpanded(expanded) { logPanelExpanded = !!expanded; const section = document.getElementById('log-section'); const btn = document.getElementById('log-toggle-btn'); const content = document.getElementById('question-log-content'); if (section) section.classList.toggle('is-expanded', logPanelExpanded); if (btn) { btn.textContent = logPanelExpanded ? '日志 - 收起' : '日志'; btn.setAttribute('aria-expanded', logPanelExpanded ? 'true' : 'false'); } if (content && logPanelExpanded) { content.scrollTop = content.scrollHeight; } } async function performDualCopy(actionBtn) { const oldText = actionBtn ? actionBtn.textContent : ''; if (actionBtn) { actionBtn.disabled = true; actionBtn.textContent = '复制中'; } const textA = collectContentTextFrom(COPY_A_XPATHS) || ''; const textB = collectContentTextFrom(COPY_B_XPATHS) || ''; log('[LegacyDualCopy] button clicked', 'info'); log('[LegacyDualCopy] COPY_A length = ' + textA.length, 'info'); log('[LegacyDualCopy] COPY_B length = ' + textB.length, 'info'); let combined = ''; if (textA && textB) { combined = textA === textB ? textA : `${textA}\n\n${textB}`; } else { combined = textA || textB || collectContentTextFrom([]); } if (!textA && !textB) log('[LegacyDualCopy] fallback length = ' + combined.length, 'info'); const finalText = removeUiStrings(combined); log('[LegacyDualCopy] final length = ' + finalText.length, 'info'); if (!finalText) { log('[LegacyDualCopy] no content found', 'error'); if (actionBtn) { actionBtn.disabled = false; actionBtn.textContent = oldText; } return false; } const ok = await copyText(finalText); if (ok) { log('[LegacyDualCopy] copied success', 'success'); log('【复制】实验区域内容已复制', 'success'); } else { log('[LegacyDualCopy] clipboard failed', 'error'); log('【复制】复制失败', 'error'); } if (actionBtn) { actionBtn.disabled = false; actionBtn.textContent = oldText; } return ok; } function injectLegacyDualCopyUI(readClipboardBtn) { const actions = document.getElementById('main-panel-actions'); if (!actions || document.getElementById('legacy-dual-copy-btn')) return; const btn = document.createElement('button'); btn.id = 'legacy-dual-copy-btn'; btn.className = 'assistant-btn'; btn.type = 'button'; btn.textContent = '实验区域复制'; btn.onclick = () => performDualCopy(btn); if (readClipboardBtn && readClipboardBtn.parentElement === actions) { actions.insertBefore(btn, readClipboardBtn); } else { actions.appendChild(btn); } log('[LegacyDualCopy] button injected', 'success'); } async function runExperimentCollect(actionBtn) { const oldText = actionBtn.textContent; log('[CodeCopy] experiment button clicked', 'info'); const legacyResult = await runExperimentCodeCollect(actionBtn); if (legacyResult.ok) { return; } log('[CodeCopy] failed: ' + legacyResult.reason, 'error'); } async function handleReadClipboard() { setAssistantStatus('正在读取剪贴板', 'normal'); try { const text = await navigator.clipboard.readText(); showClipboardPreview(text); log('【剪贴板】读取成功,已展开辅助预览', 'success'); const jsonStrings = text.match(/{[\s\S]*?}/g); let parsedAnswers = []; try { if (jsonStrings) { parsedAnswers = jsonStrings.map(s => JSON.parse(s)); } } catch (err) { parsedAnswers = []; log('【AI】剪贴板已读取,但 JSON 答案解析失败', 'error'); console.error('Clipboard JSON parse failed: ', err); } const extractJsonArray = (raw) => { const s = String(raw || ''); const start = s.indexOf('['); if (start < 0) return null; let depth = 0; let inStr = false; let esc = false; for (let i = start; i < s.length; i++) { const ch = s[i]; if (inStr) { if (esc) { esc = false; continue; } if (ch === '\\') { esc = true; continue; } if (ch === '"') inStr = false; continue; } if (ch === '"') { inStr = true; continue; } if (ch === '[') depth++; if (ch === ']') { depth--; if (depth === 0) { const candidate = s.slice(start, i + 1); try { const arr = JSON.parse(candidate); return Array.isArray(arr) ? arr : null; } catch (_) { return null; } } } } return null; }; parsedAnswers = extractFirstJsonArray(text) || []; aiAnswers = parsedAnswers; aiAnswerById.clear(); for (const ans of aiAnswers) { const id = Number(ans && ans.id); if (Number.isFinite(id)) aiAnswerById.set(id, ans); } const ids = aiAnswers.map(a => Number(a && a.id)).filter(n => Number.isFinite(n)); const imageIds = aiAnswers.filter(a => String(a && a.visualRiskLevel) === 'image').map(a => Number(a && a.id)).filter(n => Number.isFinite(n)); log('[AI Parse] answers count=' + aiAnswers.length, 'info'); log('[AI Parse] ids=' + JSON.stringify(ids), 'info'); log('[AI Parse] image ids from answers=' + JSON.stringify(imageIds), 'info'); log('[AI Parse] scannedImageGlobalIndexes=' + JSON.stringify(Array.from(scannedImageGlobalIndexes).sort((a, b) => a - b)), 'info'); currentAiAnswerIndex = 0; const loopBtnEl = document.getElementById('auto-answer-loop-btn'); if (Array.isArray(aiAnswers) && aiAnswers.length > 0) { setAssistantStatus('Ready', 'success'); log(`【AI】成功加载 ${aiAnswers.length} 个答案`, 'success'); if (!autoAnswerLoop && loopBtnEl) { loopBtnEl.click(); } } else { aiAnswers = []; aiAnswerById.clear(); setAssistantStatus('Ready', 'normal'); log('【AI】未在剪贴板内容中找到有效的 JSON 答案', 'error'); } } catch (err) { aiAnswers = []; aiAnswerById.clear(); setAssistantStatus('读取剪贴板失败', 'error'); log('【剪贴板】读取失败,请检查浏览器剪贴板权限', 'error'); console.error('Clipboard read failed: ', err); } } function startLoop() { enabled = true; autoAnswerLoop = true; updateQuestionSwitchButton(); if (loopIntervalId) clearInterval(loopIntervalId); if (aiAnswers.length > 0) { log('【AI】AI循环作答已开启', 'info'); aiAnswerAndNext(); loopIntervalId = setInterval(aiAnswerAndNext, 8000); } else { log('【收集】循环收集已开启', 'info'); scannedImageIndexes.clear(); scannedImageGlobalIndexes.clear(); try { localStorage.removeItem(SCANNED_IMAGE_GLOBAL_INDEXES_KEY); } catch (_) { } recordQuestion().then(() => { clickNext(); loopIntervalId = setInterval(clickNext, 3000); }); } } function stopLoop() { autoAnswerLoop = false; enabled = false; if (loopIntervalId) { clearInterval(loopIntervalId); loopIntervalId = null; } updateQuestionSwitchButton(); log('【循环】已关闭', 'info'); } function createAutoAnswerLoopButton() { const footer = document.getElementById('main-panel-footer'); const btn = document.createElement('button'); btn.id = 'auto-answer-loop-btn'; btn.style.display = 'none'; btn.onclick = () => { if (autoAnswerLoop) { stopLoop(); } else { startLoop(); } }; if (footer) footer.appendChild(btn); return btn; } function createMainPanel() { if (document.getElementById('main-panel')) return; injectAssistantPanelStyle(); const panel = document.createElement('div'); panel.id = 'main-panel'; panel.innerHTML = `
学习采集助手
Ready
剪贴板预览
Ctrl + V 自动填入已启用;此处仅作辅助预览,也可选中文本或拖动文本块作为备用。
`; document.body.appendChild(panel); const header = document.getElementById('main-panel-header'); const hideBtn = document.getElementById('hide-main-panel-btn'); const experimentCollectBtn = document.getElementById('experiment-collect-btn'); const readClipboardBtn = document.getElementById('read-clipboard-btn'); const questionSwitchBtn = document.getElementById('question-switch'); const copyBtn = document.getElementById('copy-all-questions-btn'); const clipboardSection = document.getElementById('clipboard-section'); const clipboardCloseBtn = document.getElementById('clipboard-close-btn'); const logToggleBtn = document.getElementById('log-toggle-btn'); createAutoAnswerLoopButton(); injectLegacyDualCopyUI(readClipboardBtn); log('[LegacyDualCopy] ready', 'info'); restorePanelState(panel); makePanelDraggable(panel, header); observePanelResize(panel); updateQuestionSwitchButton(); setLogPanelExpanded(false); if (clipboardSection) { clipboardSection.addEventListener('mouseenter', () => { clipboardPreviewHovering = true; clearTimeout(clipboardPreviewTimerId); }); clipboardSection.addEventListener('mouseleave', () => { clipboardPreviewHovering = false; scheduleClipboardPreviewCollapse(); }); } hideBtn.onclick = () => { setPanelMinimized(panel, !panel.classList.contains('is-minimized')); }; clipboardCloseBtn.onclick = () => { hideClipboardPreview(); }; logToggleBtn.onclick = () => { setLogPanelExpanded(!logPanelExpanded); }; experimentCollectBtn.onclick = () => { runExperimentCollect(experimentCollectBtn); }; log('[LegacyBridge] button bound', 'info'); readClipboardBtn.onclick = async () => { await handleReadClipboard(); }; questionSwitchBtn.onclick = function () { enabled = !enabled; autoAnswerLoop = enabled; updateQuestionSwitchButton(); log(enabled ? '【开关】题目收集已开启' : '【开关】题目收集已关闭', 'info'); if (autoAnswerLoop) { if (loopIntervalId) clearInterval(loopIntervalId); if (aiAnswers.length > 0) { log('【AI】AI循环作答已开启', 'info'); aiAnswerAndNext(); loopIntervalId = setInterval(aiAnswerAndNext, 8000); } else { log('【收集】循环收集已开启', 'info'); scannedImageIndexes.clear(); scannedImageGlobalIndexes.clear(); try { localStorage.removeItem(SCANNED_IMAGE_GLOBAL_INDEXES_KEY); } catch (_) { } recordQuestion().then(() => { clickNext(); loopIntervalId = setInterval(clickNext, 3000); }); } } else { log('【循环】已关闭', 'info'); if (loopIntervalId) clearInterval(loopIntervalId); } }; copyBtn.onclick = function () { visualQuestionKeys.clear(); suspiciousQuestionKeys.clear(); try { localStorage.setItem(VISUAL_QUESTION_KEYS_KEY, JSON.stringify([])); } catch (_) { } try { localStorage.setItem(SUSPICIOUS_QUESTION_KEYS_KEY, JSON.stringify([])); } catch (_) { } const TYPE_ORDER = { single: 1, multiple: 2, fill: 3, judge: 4 }; const sorted = Array.from(questionStore.values()) .map(q => normalizeQuestionMeta({ ...q })) .filter(q => q && q.questionKey && q.questionType && Number.isFinite(Number(q.localIndex))) .sort((a, b) => { const ta = TYPE_ORDER[a.questionType] || 99; const tb = TYPE_ORDER[b.questionType] || 99; if (ta !== tb) return ta - tb; return Number(a.localIndex) - Number(b.localIndex); }); let text = sorted.map((q) => { const parsed = parseQuestionKey(q.questionKey) || {}; const questionType = parsed.questionType || q.questionType; const localIndex = parsed.localIndex || Number(q.localIndex) || 0; const questionKey = parsed.questionKey || q.questionKey; const typeLabel = TYPE_LABELS[questionType] || (q.typeLabel || ''); const riskLevel = q.visualRiskLevel === 'image' || q.isVisualQuestion ? 'image' : 'none'; const opts = (q.options || []).map((opt, i) => { const label = opt.label || String.fromCharCode(65 + i); return label + '. ' + (opt.text || '') + ' (value: ' + (opt.value ?? '') + ')'; }).join('\n'); if (riskLevel === 'image') rememberVisualQuestionKey(questionKey); const riskNotice = riskLevel === 'image' ? '[IMAGE_QUESTION: text may be incomplete. Return skipApply=true.]' : ''; return '[questionKey: ' + questionKey + ']\n' + '[questionType: ' + questionType + ']\n' + '[localIndex: ' + localIndex + ']\n' + '[visualRiskLevel: ' + riskLevel + ']\n' + (riskNotice ? riskNotice + '\n' : '') + '[' + typeLabel + '] 第' + localIndex + '题\n' + (q.question || '') + '\n' + opts; }).join('\n\n'); text = sorted.map((q) => { const parsed = parseQuestionKey(q.questionKey); if (!parsed) return ''; const questionType = parsed.questionType; const localIndex = Number(parsed.localIndex); const questionKey = parsed.questionKey; const typeLabel = TYPE_LABELS[questionType] || ''; const riskLevel = q.visualRiskLevel === 'image' || q.isVisualQuestion ? 'image' : 'none'; const opts = (q.options || []).map((opt, i) => { const label = opt.label || String.fromCharCode(65 + i); return label + '. ' + (opt.text || '') + ' (value: ' + (opt.value ?? '') + ')'; }).join('\n'); const riskNotice = riskLevel === 'image' ? '[IMAGE_QUESTION: text may be incomplete. Return skipApply=true.]' : ''; q.questionType = questionType; q.localIndex = localIndex; q.typeLabel = typeLabel; return '[questionKey: ' + questionKey + ']\n' + '[questionType: ' + questionType + ']\n' + '[localIndex: ' + localIndex + ']\n' + '[visualRiskLevel: ' + riskLevel + ']\n' + (riskNotice ? riskNotice + '\n' : '') + '[' + typeLabel + '] \u7b2c' + localIndex + '\u9898\n' + (q.question || '') + '\n' + opts; }).filter(Boolean).join('\n\n'); if (!text) text = '[NO_QUESTIONS_COLLECTED]'; text += '\n\n----\n' + 'Return ONLY a JSON array. Do not output markdown. Each item must use this schema:\n' + '{\n' + ' "id": 1,\n' + ' "questionKey": "single-1",\n' + ' "questionType": "single",\n' + ' "localIndex": 1,\n' + ' "choiceValue": "string or null",\n' + ' "choice": "A/B/C/D or array or null",\n' + ' "judge": true,\n' + ' "fills": [],\n' + ' "visualRiskLevel": "none",\n' + ' "isVisualQuestion": false,\n' + ' "uncertain": false,\n' + ' "skipApply": false,\n' + ' "note": ""\n' + '}\n\n' + 'Rules:\n' + '- questionType must be one of: single, multiple, judge, fill.\n' + '- visualRiskLevel must be one of: none, image.\n' + '- If visualRiskLevel is image, set skipApply=true, uncertain=true, and do not guess answer.\n' + '- If visualRiskLevel is none, answer normally.\n' + '- Return only JSON array.'; navigator.clipboard.writeText(text).then(() => { this.innerText = '已复制'; setAssistantStatus('Ready', 'success'); log('【复制】已复制全部题目到剪贴板,正在刷新页面...', 'success'); // keep state; do not auto reload after copy }, () => { setAssistantStatus('Ready', 'error'); log('【复制】复制失败', 'error'); }); }; } function installStableCopyHandler() { const copyBtn = document.getElementById('copy-all-questions-btn'); if (!copyBtn) return; copyBtn.onclick = function () { const sorted = Array.from(questionStore.values()) .filter(Boolean) .sort((a, b) => { const ai = Number(a.globalIndex); const bi = Number(b.globalIndex); return (Number.isFinite(ai) ? ai : Number.MAX_SAFE_INTEGER) - (Number.isFinite(bi) ? bi : Number.MAX_SAFE_INTEGER); }); let text = sorted.map((q, idx) => { const globalIndex = Number(q.globalIndex) || (idx + 1); const type = normalizeQuestionTypeCode(q.type || q.questionType) || 'unknown'; const riskLevel = scannedImageGlobalIndexes.has(globalIndex) ? 'image' : 'none'; const riskNotice = riskLevel === 'image' ? '[IMAGE_QUESTION: This question was marked as image during scan. Text may be incomplete. Return skipApply=true.]' : ''; const opts = (q.options || []).map((opt, i) => { const label = opt.label || String.fromCharCode(65 + i); return label + '. ' + (opt.text || '') + ' (value: ' + (opt.value ?? '') + ')'; }).join('\n'); return '[globalIndex: ' + globalIndex + ']\n' + '[type: ' + type + ']\n' + '[visualRiskLevel: ' + riskLevel + ']\n' + (riskNotice ? riskNotice + '\n' : '') + '\u7b2c' + globalIndex + '\u9898\n' + (q.question || '') + '\n' + opts; }).join('\n\n'); if (!text) text = '[NO_QUESTIONS_COLLECTED]'; text += '\n\n----\n' + 'Return ONLY a JSON array. Do not output markdown.\n' + 'Each item must use this schema:\n' + '{\n' + ' "id": 1,\n' + ' "type": "single",\n' + ' "choiceValue": "string or null",\n' + ' "choice": "A/B/C/D or array or null",\n' + ' "judge": true,\n' + ' "fills": [],\n' + ' "visualRiskLevel": "none",\n' + ' "skipApply": false,\n' + ' "note": ""\n' + '}\n\n' + 'Rules:\n' + '- id is global sequence index (1..N), not per-type index.\n' + '- type is reference only.\n' + '- visualRiskLevel must be one of: none, image.\n' + '- Copy visualRiskLevel from the question metadata exactly.\n' + '- Do NOT infer image questions by yourself.\n' + '- If visualRiskLevel is image, set skipApply=true and do not guess answer.\n' + '- Return only JSON array.'; navigator.clipboard.writeText(text).then(() => { copyBtn.innerText = 'Copied'; setAssistantStatus('Ready', 'success'); saveScannedImageState(); log('[Copy] questions copied. Reloading page...', 'success'); setTimeout(() => window.location.reload(), 800); }, () => { setAssistantStatus('Ready', 'error'); log('[Copy] copy failed, page will not reload', 'error'); }); }; } function resetAutoSubmitTimer() { if (autoSubmitTimerId) { clearTimeout(autoSubmitTimerId); autoSubmitTimerId = null; } } function startAutoSubmitTimer() { resetAutoSubmitTimer(); if (autoSubmitEnabled) { log(`【计时器】${SUBMIT_TIMEOUT / 1000}秒后将自动提交...`, 'info'); autoSubmitTimerId = setTimeout(() => { log('【计时器】时间到,执行自动提交', 'success'); clickSubmit(); }, SUBMIT_TIMEOUT); } } function clickNext(isScanning = false) { resetAutoSubmitTimer(); const btn = [...document.querySelectorAll('button.ant-btn, button')] .find(el => (el.innerText || '').includes('下一题')); if (btn && !btn.disabled) { btn.click(); if (!isScanning) log('【操作】已点击“下一题”按钮', 'success'); return true; } else { if (!isScanning) { log('【操作】未找到“下一题”按钮或已是最后一题', 'error'); if (autoAnswerLoop) { const loopBtn = document.getElementById('auto-answer-loop-btn'); if (loopBtn) loopBtn.click(); } } return false; } } function clickSubmit() { resetAutoSubmitTimer(); const submitKeywords = ['提交', '确认', '交卷']; const btn = [...document.querySelectorAll('button.ant-btn')] .find(el => submitKeywords.some(kw => (el.innerText || '').includes(kw) && !(el.innerText || '').includes('下一题'))); if (btn) { btn.click(); log('【操作】已点击“提交”按钮', 'success'); return true; } else { log('【操作】未找到“提交”按钮', 'error'); return false; } } function detectQuestionType() { const type = detectCurrentDomType().type; if (type === 'multiple') return '\u591a\u9009\u9898'; if (type === 'single') return '\u5355\u9009\u9898'; if (type === 'judge') return '\u5224\u65ad\u9898'; if (type === 'fill') return '\u586b\u7a7a\u9898'; return '\u672a\u77e5'; } function normalizeChoiceToken(token) { return String(token ?? '') .replace(/^["'`]+|["'`]+$/g, '') .replace(/[.。::、\s]/g, '') .trim() .toUpperCase(); } function parseChoiceTokens(raw) { if (raw == null || raw === false) return []; if (Array.isArray(raw)) { return raw.flatMap(item => parseChoiceTokens(item)); } if (typeof raw === 'number') { return [String(raw)]; } let text = String(raw).trim(); if (!text) return []; if ((text.startsWith('[') && text.endsWith(']')) || (text.startsWith('{') && text.endsWith('}'))) { try { const parsed = JSON.parse(text); return parseChoiceTokens(parsed); } catch (_) { } } text = text .replace(/[,;;|/\\]+/g, ',') .replace(/\s+/g, ',') .replace(/^\[|\]$/g, '') .replace(/["'`]/g, ''); const commaParts = text.split(',').map(normalizeChoiceToken).filter(Boolean); const tokens = []; for (const part of commaParts) { if (/^[A-Z]{2,}$/.test(part)) { tokens.push(...part.split('')); } else { tokens.push(part); } } return Array.from(new Set(tokens)); } function isAllChoiceAnswer(raw) { if (raw == null || raw === false) return false; if (Array.isArray(raw)) { return raw.some(item => isAllChoiceAnswer(item)); } const text = String(raw).trim().toUpperCase(); return /^(全选|全部|所有|ALL|全部选项)$/.test(text); } function getOptionLabel(optionEl, index) { const text = normalizeText(optionEl.innerText || optionEl.textContent || ''); const labelMatch = text.match(/^([A-Z])\s*[.。、:]?\s*/i); if (labelMatch) return labelMatch[1].toUpperCase(); return String.fromCharCode(65 + index); } function isOptionSelected(optionEl) { if (!optionEl) return false; const input = optionEl.matches('input') ? optionEl : optionEl.querySelector('input[type="radio"], input[type="checkbox"]'); if (input && input.checked) return true; const ariaNode = optionEl.matches('[aria-checked]') ? optionEl : optionEl.querySelector('[aria-checked]'); if (ariaNode && ariaNode.getAttribute('aria-checked') === 'true') return true; const selectedNode = optionEl.closest('.ant-radio-wrapper-checked, .ant-checkbox-wrapper-checked, .checked, .selected, .active, [data-checked="true"], [data-selected="true"]') || optionEl.querySelector('.ant-radio-checked, .ant-checkbox-checked, .checked, .selected, .active, [data-checked="true"], [data-selected="true"]'); return !!selectedNode; } function getChoiceOptions(type) { const selector = type === '多选题' ? '.ant-checkbox-wrapper' : '.ant-radio-wrapper'; return Array.from(document.querySelectorAll(selector)).map((el, index) => { const input = el.querySelector('input[type="radio"], input[type="checkbox"]'); const label = getOptionLabel(el, index); const value = input ? String(input.value ?? '') : ''; const text = normalizeText(el.innerText || el.textContent || ''); return { el, input, label, value, text, key: label, selected: isOptionSelected(el) }; }); } function findChoiceOption(options, token) { const normalized = normalizeChoiceToken(token); if (!normalized) return null; return options.find(opt => normalizeChoiceToken(opt.value) === normalized) || options.find(opt => normalizeChoiceToken(opt.label) === normalized) || options.find(opt => normalizeChoiceToken(opt.text).startsWith(normalized)); } function clickChoiceOption(option) { if (!option) return; option.el.click(); } function getMatchedChoiceTargets(options, raw) { const tokens = parseChoiceTokens(raw); const matched = []; const missed = []; for (const token of tokens) { const option = findChoiceOption(options, token); if (option) { matched.push({ token, option }); } else { missed.push(token); } } return { tokens, matched, missed }; } function resolveChoiceTargets(options, answer) { const valueTargets = getMatchedChoiceTargets(options, answer.choiceValue); const labelTargets = getMatchedChoiceTargets(options, answer.choice); const explicitChoiceTokens = parseChoiceTokens(answer && answer.choice).filter(t => /^[A-F]$/.test(t)); const canFallbackByChoice = explicitChoiceTokens.length > 0; if (valueTargets.matched.length > 0 && valueTargets.missed.length === 0) { return { source: 'choiceValue', ...valueTargets }; } if (canFallbackByChoice && labelTargets.matched.length > 0 && labelTargets.missed.length === 0) { if (valueTargets.tokens.length > 0 && valueTargets.matched.length === 0) { log('【AI】choiceValue 未匹配当前页面,已改用 choice 字母答案', 'info'); } return { source: 'choice', ...labelTargets }; } if (valueTargets.matched.length > 0) { return { source: 'choiceValue', ...valueTargets }; } if (canFallbackByChoice && labelTargets.matched.length > 0) { return { source: 'choice', ...labelTargets }; } return valueTargets.tokens.length > 0 ? { source: 'choiceValue', ...valueTargets } : (canFallbackByChoice ? { source: 'choice', ...labelTargets } : { source: 'none', tokens: [], matched: [], missed: [] }); } async function syncSingleChoice(answer, meta = {}) { const currentGlobalIndex = Number(meta.currentGlobalIndex); const workingAnswer = { ...(answer || {}) }; if ((workingAnswer.choice == null || workingAnswer.choice === '') && typeof workingAnswer.answer === 'string') { workingAnswer.choice = workingAnswer.answer; } if ((workingAnswer.choice == null || workingAnswer.choice === '') && typeof workingAnswer.value === 'string') { workingAnswer.choice = workingAnswer.value; } if ((workingAnswer.choice == null || workingAnswer.choice === '') && typeof workingAnswer.text === 'string') { workingAnswer.choice = workingAnswer.text; } let options = getChoiceOptions('单选题'); const resolved = resolveChoiceTargets(options, workingAnswer); if (resolved && resolved.source === 'choice') { log('[AI Option] choiceValue not matched, fallback to explicit choice=' + JSON.stringify(resolved.tokens || []), 'info'); } const targetMatch = resolved.matched[0]; const targetToken = resolved.tokens[0]; log('[AI Single] normalized target = ' + String(targetToken || (targetMatch && targetMatch.option && targetMatch.option.label) || 'null'), 'info'); if (!targetToken && !targetMatch) { log('[AI Single] missing target answer', 'error'); log('[ANSWER_FOR_CURRENT] ' + JSON.stringify({ currentGlobalIndex: Number.isFinite(currentGlobalIndex) ? currentGlobalIndex : null, answer: answer || null }), 'error'); log('【AI】单选题缺少目标答案', 'error'); return false; } let target = targetMatch ? targetMatch.option : null; if (!target) { log('[AI Option] no reliable option target, stop without clicking', 'error'); return false; } if (!target) { log(`【AI】未找到单选目标选项: ${targetToken}`, 'error'); return false; } const targetKey = target.key; const targetLabel = target.label; let selectedOptions = options.filter(opt => opt.selected); log('[AI Single] current checked = ' + (selectedOptions[0] ? selectedOptions[0].label : 'none'), 'info'); if (target.selected && selectedOptions.every(opt => opt.key === targetKey)) { log(`【AI】单选已是目标选项: ${targetLabel}`, 'success'); return true; } if (!target.selected) { clickChoiceOption(target); log('[AI Single] clicked option ' + targetLabel, 'info'); await sleep(getClickDelay()); } options = getChoiceOptions('单选题'); target = findChoiceOption(options, targetKey) || findChoiceOption(options, targetLabel); selectedOptions = options.filter(opt => opt.selected); if (target && target.selected && selectedOptions.some(opt => opt.key !== targetKey)) { const extraOptions = selectedOptions.filter(opt => opt.key !== targetKey && (!opt.input || opt.input.type !== 'radio')); for (const option of extraOptions) { clickChoiceOption(option); log(`【AI】已清理单选残留选项: ${option.label}`, 'info'); await sleep(getClickDelay()); } } options = getChoiceOptions('单选题'); target = findChoiceOption(options, targetKey) || findChoiceOption(options, targetLabel); selectedOptions = options.filter(opt => opt.selected); if (target && target.selected && selectedOptions.every(opt => opt.key === targetKey)) { log('[APPLY_SUCCESS] currentGlobalIndex=' + String(Number.isFinite(currentGlobalIndex) ? currentGlobalIndex : ''), 'success'); log(`【AI】已同步单选目标: ${target.label}`, 'success'); return true; } log(`【AI】单选点击后未检测到选中: ${targetToken}`, 'error'); return false; } async function syncMultipleChoice(answer) { let options = getChoiceOptions('多选题'); const shouldSelectAll = isAllChoiceAnswer(answer.choiceValue) || isAllChoiceAnswer(answer.choice); const resolved = resolveChoiceTargets(options, answer); if (!shouldSelectAll && resolved.tokens.length === 0) { log('【AI】多选题缺少目标答案', 'error'); return false; } const targetKeys = new Set(); const targetLabels = []; if (shouldSelectAll) { for (const option of options) { targetKeys.add(option.key); targetLabels.push(option.label); } log('【AI】检测到全选答案,目标设为当前全部选项', 'info'); } else { for (const { option } of resolved.matched) { targetKeys.add(option.key); targetLabels.push(option.label); } } if (!shouldSelectAll) { for (const token of resolved.missed) { log(`【AI】未找到多选目标选项: ${token}`, 'error'); } } if (targetKeys.size === 0) return false; const selectedOptions = options.filter(opt => opt.selected); const extraOptions = selectedOptions.filter(opt => !targetKeys.has(opt.key)); for (const option of extraOptions) { clickChoiceOption(option); log(`【AI】已取消多余选项: ${option.label}`, 'info'); await sleep(getClickDelay()); } options = getChoiceOptions('多选题'); for (const targetKey of targetKeys) { options = getChoiceOptions('多选题'); const option = options.find(opt => opt.key === targetKey); if (!option || option.selected) continue; clickChoiceOption(option); log(`【AI】已选择缺失选项: ${option.label}`, 'success'); await sleep(getClickDelay()); } options = getChoiceOptions('多选题'); const currentKeys = new Set(options.filter(opt => opt.selected).map(opt => opt.key)); const hasEveryTarget = Array.from(targetKeys).every(key => currentKeys.has(key)); const hasNoExtra = Array.from(currentKeys).every(key => targetKeys.has(key)); if (hasEveryTarget && hasNoExtra) { log(`【AI】多选已同步为: ${targetLabels.join('')}`, 'success'); return true; } log(`【AI】多选同步后状态不一致,目标: ${targetLabels.join('')}`, 'error'); return false; } async function setFillValue(el, value) { const text = String(value ?? ''); const isContentEditable = el.isContentEditable || el.getAttribute('contenteditable') === 'true'; if (isContentEditable) { el.focus(); el.textContent = ''; el.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'deleteContentBackward' })); el.textContent = text; el.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, data: text, inputType: 'insertText' })); el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); el.blur(); return; } const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const descriptor = Object.getOwnPropertyDescriptor(proto, 'value'); el.focus(); if (descriptor && descriptor.set) { descriptor.set.call(el, ''); } else { el.value = ''; } el.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'deleteContentBackward' })); el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); if (descriptor && descriptor.set) { descriptor.set.call(el, text); } else { el.value = text; } el.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, data: text, inputType: 'insertText' })); el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); el.blur(); } function getFillInputs(root) { if (!root || !root.querySelectorAll) return []; const selector = 'input:not([type]), input[type="text"], input[type="search"], textarea, [contenteditable="true"]'; return Array.from(root.querySelectorAll(selector)).filter(el => { if (!el || !isVisibleElement(el)) return false; if (el.closest('#main-panel, [class*="answerSheet"]')) return false; if (el.disabled || el.readOnly) return false; const tag = String(el.tagName || '').toLowerCase(); const type = String(el.getAttribute('type') || '').toLowerCase(); if (tag === 'input' && ['radio', 'checkbox', 'button', 'submit', 'hidden'].includes(type)) return false; return true; }); } function normalizeFills(answer) { if (!answer || typeof answer !== 'object') return []; if (Array.isArray(answer.fills)) { const values = answer.fills.map(v => String(v ?? '').trim()).filter(Boolean); if (values.length > 0) return values; } const fallbackKeys = ['fill', 'text', 'answer', 'value']; for (const key of fallbackKeys) { const v = answer[key]; if (typeof v === 'string' && v.trim()) return [v.trim()]; } return []; } async function syncFillQuestion(answer, domInfo) { log('[AI Fill] current DOM type=fill', 'info'); const fills = normalizeFills(answer); log('[AI Fill] fills=' + JSON.stringify(fills), 'info'); if (fills.length === 0) { log('[AI Fill] failed: fills empty', 'error'); return { success: false, reason: 'fills empty', filledCount: 0, inputCount: 0 }; } const root = domInfo?.root || getCurrentQuestionRoot() || document; const inputs = getFillInputs(root); log('[AI Fill] inputCount=' + inputs.length, 'info'); if (inputs.length === 0) { log('[AI Fill] failed: no visible fill inputs', 'error'); return { success: false, reason: 'no inputs', filledCount: 0, inputCount: 0 }; } let filledCount = 0; const maxWrite = Math.min(inputs.length, fills.length); for (let i = 0; i < maxWrite; i++) { const targetText = String(fills[i] ?? ''); log('[AI Fill] writing input ' + (i + 1) + ': ' + targetText, 'info'); await setFillValue(inputs[i], targetText); await sleep(180); let afterValue = String(inputs[i].value ?? inputs[i].textContent ?? '').trim(); if (!afterValue) { await sleep(120); afterValue = String(inputs[i].value ?? inputs[i].textContent ?? '').trim(); } log('[AI Fill] after write value=' + afterValue, 'info'); const sameNoSpace = afterValue.replace(/\s+/g, '') === targetText.trim().replace(/\s+/g, ''); if (afterValue.length > 0 && (targetText.trim().length === 0 || sameNoSpace || afterValue.includes(targetText.trim()) || targetText.trim().includes(afterValue))) { filledCount++; } } if (filledCount >= 1) { log('[AI Fill] success filledCount=' + filledCount, 'success'); return { success: true, filledCount, inputCount: inputs.length }; } log('[AI Fill] failed: write did not persist', 'error'); return { success: false, reason: 'write failed', filledCount, inputCount: inputs.length }; } async function executeAnswer(answer) { window.__tougeAnswerApplying = true; try { if (!answer || typeof answer !== 'object') { log('[ANSWER_MISSING] empty answer object for current question', 'error'); return { success: false, skipped: false }; } const currentInfo = getCurrentGlobalIndexFromAnswerSheet(); const fallbackGlobalIndex = currentAiAnswerIndex + 1; const currentGlobalIndex = Number(currentInfo?.globalIndex || fallbackGlobalIndex); if (!currentInfo) { log('[AI Index Debug] cannot resolve currentGlobalIndex from answer sheet, fallback to currentAiAnswerIndex+1=' + fallbackGlobalIndex, 'error'); } const fromAnswerSkip = !!(answer && (answer.skipApply === true || answer.isVisualQuestion === true || answer.visualRiskLevel === 'image')); const isScannedImage = scannedImageGlobalIndexes.has(currentGlobalIndex); log('[AI Guard] currentAiAnswerIndex=' + currentAiAnswerIndex + ' currentGlobalIndex=' + String(currentGlobalIndex) + ' answer.id=' + String(answer?.id) + ' answer.type=' + String(answer?.type || answer?.questionType || '') + ' answer.choice=' + JSON.stringify(answer?.choice ?? null) + ' answer.choiceValue=' + JSON.stringify(answer?.choiceValue ?? null) + ' answer.visualRiskLevel=' + String(answer?.visualRiskLevel) + ' answer.skipApply=' + String(answer?.skipApply) + ' scannedImageGlobalIndexes=' + JSON.stringify(Array.from(scannedImageGlobalIndexes).sort((a, b) => a - b)) + ' isScannedImage=' + String(isScannedImage), 'info'); log('[AI Index Debug] currentAiAnswerIndex=' + currentAiAnswerIndex + ' currentGlobalIndex=' + String(currentGlobalIndex) + ' currentGroupTitle=' + String(currentInfo?.groupTitle || '') + ' currentLocalIndex=' + String(currentInfo?.localIndex || '') + ' answer.id=' + String(answer?.id) + ' answer.type=' + String(answer?.type || answer?.questionType || '') + ' answer.visualRiskLevel=' + String(answer?.visualRiskLevel) + ' answer.skipApply=' + String(answer?.skipApply) + ' scannedImageGlobalIndexes=' + JSON.stringify(Array.from(scannedImageGlobalIndexes).sort((a, b) => a - b)) + ' isScannedImage=' + String(isScannedImage), 'info'); log('[AI Visual] answer.visualRiskLevel=' + String(answer?.visualRiskLevel) + ' skipApply=' + String(answer?.skipApply) + ' isVisualQuestion=' + String(answer?.isVisualQuestion), 'info'); if (isScannedImage) { markCurrentAnswerSheetItem('image'); log('[AI Visual] currentGlobalIndex=' + currentGlobalIndex + ' was marked image during scan, skip apply', 'info'); return { success: true, skipped: true, reason: 'scan-image' }; } if (fromAnswerSkip) { log('[AI Visual] answer says image/skipApply, but scan stage did not mark currentGlobalIndex=' + currentGlobalIndex + ' as image. Ignore it.', 'info'); } const domInfo = detectCurrentDomType(); const domType = domInfo.type; const aiType = normalizeQuestionTypeCode(answer?.type || answer?.questionType || ''); let finalType = domType; if (domType === 'unknown' && aiType) { finalType = aiType; } else if (aiType && domType && aiType !== domType) { if (domType === 'judge' && domInfo.radioCount >= 3) { finalType = aiType; } else if (aiType === 'single' && domInfo.radioCount >= 3) { finalType = 'single'; } else if (aiType === 'judge' && domInfo.radioCount === 2 && domInfo.isStrictJudgePair) { finalType = 'judge'; } log('[AI] type conflict: answer=' + aiType + ', dom=' + domType + ', final=' + finalType, 'info'); } log('[Type Debug] answerType=' + (aiType || 'unknown') + ' domType=' + (domType || 'unknown') + ' finalType=' + (finalType || 'unknown') + ' radioCount=' + domInfo.radioCount + ' checkboxCount=' + domInfo.checkboxCount + ' radioTexts=' + JSON.stringify(domInfo.radioTexts || []) + ' rootTextHead=' + JSON.stringify(domInfo.rootTextHead || ''), 'info'); log('[CURRENT] currentGlobalIndex=' + String(currentGlobalIndex) + ' finalType=' + String(finalType || 'unknown'), 'info'); log('[ANSWER_FOR_CURRENT] ' + JSON.stringify(answer || {}), 'info'); let answered = false; let fillResult = null; if (finalType === 'fill') { const normalizedFills = normalizeFills(answer || {}); if (normalizedFills.length === 0) { log('[AI Fill Debug] currentGlobalIndex=' + currentGlobalIndex + ' answer.id=' + String(answer?.id) + ' answer.type=' + String(answer?.type || answer?.questionType || '') + ' answer.fills=' + JSON.stringify(answer?.fills || []) + ' answer object=' + JSON.stringify(answer || {}), 'info'); } fillResult = await syncFillQuestion(answer || {}, domInfo); answered = !!fillResult.success; } else if (finalType === 'judge' && typeof answer?.judge === 'boolean') { const labels = domInfo.radioOptions && domInfo.radioOptions.length > 0 ? domInfo.radioOptions : Array.from(((domInfo.root || getCurrentQuestionRoot() || document).querySelectorAll('.ant-radio-wrapper'))).filter(isVisibleElement); const target = labels.find(l => answer.judge ? new RegExp('\\u6b63\\u786e|\\u5bf9|True|TRUE|true|T').test(l.innerText || '') : new RegExp('\\u9519\\u8bef|\\u9519|False|FALSE|false|F').test(l.innerText || '')) || labels[answer.judge ? 0 : 1]; if (target) { if (!isOptionSelected(target)) { target.click(); await sleep(getClickDelay()); } answered = true; } } else if (finalType === 'single') { answered = await syncSingleChoice(answer || {}, { currentGlobalIndex }); } else if (finalType === 'multiple') { answered = await syncMultipleChoice(answer || {}); } if (answered) { startAutoSubmitTimer(); return { success: true, skipped: false }; } if (fillResult && !fillResult.success) { log('[AI Fill] failed reason=' + (fillResult.reason || 'unknown'), 'error'); } log('[AI] failed to apply answer on current question, finalType=' + finalType, 'error'); return { success: false, skipped: false }; } catch (e) { log('[AI] answer execution error', 'error'); console.error('Answer execution error:', e); return { success: false, skipped: false }; } finally { window.__tougeAnswerApplying = false; } } async function aiAnswerAndNext() { if (aiAnswerRunning) { log('?AI?????????????????', 'info'); return; } aiAnswerRunning = true; try { if (aiAnswers.length === 0) { log('?AI????????????', 'error'); stopLoop(); return; } if (currentAiAnswerIndex >= aiAnswers.length) { log('[AI] no more answers, stop loop', 'info'); stopLoop(); return; } const currentInfo = getCurrentGlobalIndexFromAnswerSheet(); const currentGlobalIndex = Number(currentInfo?.globalIndex || (currentAiAnswerIndex + 1)); const indexedAnswer = aiAnswers[currentGlobalIndex - 1]; const currentAnswer = aiAnswerById.get(currentGlobalIndex) || (indexedAnswer && Number(indexedAnswer?.id) === currentGlobalIndex ? indexedAnswer : null); if (!currentAnswer) { const availableIds = Array.from(aiAnswerById.keys()).sort((a, b) => a - b); log('[ANSWER_MISSING] currentGlobalIndex=' + currentGlobalIndex + ' availableIds=' + JSON.stringify(availableIds), 'error'); stopLoop(); return; } log('[AI] run answer #' + (currentAiAnswerIndex + 1) + ' currentGlobalIndex=' + currentGlobalIndex, 'info'); const result = await executeAnswer(currentAnswer); if (result && (result.success || result.skipped)) { if (result.success) markCurrentAnswerSheetItem('answered'); currentAiAnswerIndex++; await sleep(300); clickNext(); } else { log('[AI] current question apply failed, stop loop', 'error'); stopLoop(); } } finally { aiAnswerRunning = false; } } function getElementVisualSize(el) { if (!el) return { width: 0, height: 0 }; let width = 0; let height = 0; try { const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null; if (rect) { width = Math.max(width, rect.width || 0); height = Math.max(height, rect.height || 0); } } catch (_) { } width = Math.max(width, Number(el.naturalWidth) || 0, Number(el.getAttribute && el.getAttribute('width')) || 0); height = Math.max(height, Number(el.naturalHeight) || 0, Number(el.getAttribute && el.getAttribute('height')) || 0); return { width, height }; } function isLargeVisualElement(el) { const size = getElementVisualSize(el); return size.width >= 80 && size.height >= 40; } function elementHasLargeVisual(root) { if (!root || !root.querySelectorAll) return false; const visualEls = Array.from(root.querySelectorAll('img, picture, svg, canvas, object, embed, table')) .filter(el => !el.closest('#main-panel, .ant-radio-wrapper, .ant-checkbox-wrapper, button')); if (visualEls.some(isLargeVisualElement)) return true; return Array.from(root.querySelectorAll('*')).some(el => { try { const style = getComputedStyle(el); return style && style.backgroundImage && style.backgroundImage !== 'none' && isLargeVisualElement(el); } catch (_) { return false; } }); } function getMatchedVisualPrompts(text) { const source = String(text || ''); const prompts = [ '\u5982\u56fe', '\u5982\u4e0b\u56fe', '\u4e0b\u56fe', '\u4e0a\u56fe', '\u56fe\u4e2d', '\u56fe\u793a', '\u56fe\u6240\u793a', '\u6839\u636e\u56fe', '\u89c2\u5bdf\u56fe', '\u5982\u4e0b\u8868', '\u4e0b\u8868', '\u8868\u4e2d', '\u5206\u6790\u8868\u5982\u4e0b' ]; return prompts.filter(prompt => source.includes(prompt)); } function getOptionVisualRisk(optionEl, optionData = {}) { const rawText = normalizeText(optionEl ? (optionEl.innerText || optionEl.textContent || '') : ''); const cleanText = normalizeText((optionData.text || rawText).replace(/^([A-Z])\s*[.\u3002:\uff1a\u3001]?\s*/, '')); const html = String(optionEl && optionEl.innerHTML || '').toLowerCase(); const visualNodes = optionEl ? Array.from(optionEl.querySelectorAll('img, svg, canvas, picture, object, embed, mjx-container, .MathJax, .katex')) : []; const hasVisualNode = visualNodes.length > 0; const hasLargeVisual = visualNodes.some(isLargeVisualElement); const htmlLooksVisual = /mathjax|katex|mjx| getOptionVisualRisk(el, options[index] || {})); const optionCount = optionRisks.length; const emptyOptionCount = optionRisks.filter(r => r.textWeak).length; const visualOptionCount = optionRisks.filter(r => r.definite).length; const optionsWithVisualAndWeakText = optionRisks.filter(r => (r.hasVisualNode || r.htmlLooksVisual) && r.textWeak).length; let level = 'none'; let reason = ''; const hasStrongVisualInStem = stemHasLargeVisual || areaHasLargeVisual; const hasMultiWeakVisualOptions = optionsWithVisualAndWeakText >= 2 && optionCount >= 2; if (hasStrongVisualInStem || hasMultiWeakVisualOptions) { level = 'image'; reason = hasMultiWeakVisualOptions ? '\u591a\u4e2a\u9009\u9879\u4e3a\u89c6\u89c9\u7ed3\u6784\u4e14\u6587\u672c\u7f3a\u5931' : hasStrongVisualInStem ? '\u9898\u5e72\u533a\u57df\u5b58\u5728\u5927\u5c3a\u5bf8\u89c6\u89c9\u5143\u7d20' : ''; } return { level, reason, optionCount, emptyOptionCount, visualOptionCount, suspiciousOptionCount: 0, stemHasLargeVisual: stemHasLargeVisual || areaHasLargeVisual, matchedKeywords: [] }; } async function recordQuestion(recordOptions = {}) { if (!enabled && !recordOptions.force) return null; const type = detectQuestionType(); const questionType = normalizeQuestionTypeCode(type); if (!questionType) return null; const root = getCurrentQuestionRoot() || document; let question = ''; const questionEl = root.querySelector && root.querySelector('.question_title') || document.querySelector('.question_title'); if (questionEl) question = normalizeText(questionEl.innerText || questionEl.textContent || ''); const optionEls = Array.from(root.querySelectorAll('.ant-radio-wrapper, .ant-checkbox-wrapper')).filter(isVisibleElement); const options = optionEls.map((el, i) => { const text = normalizeText(el.innerText || el.textContent || ''); const input = el.querySelector('input'); const labelMatch = text.match(/^([A-Z])\s*\.?\s*/); const label = labelMatch ? labelMatch[1] : String.fromCharCode(65 + i); return { label, text: text.replace(/^([A-Z])\s*\.?\s*/, ''), value: input ? input.value : null }; }); const risk = detectVisualRisk(question, root, options, optionEls, type); const selectedSignature = normalizeQuestionId((document.querySelector('[class*="answerSheetItem"][class*="selected"] [class*="qindex"]') || {}).textContent || ''); const signature = [questionType, question, selectedSignature].join('|'); const now = Date.now(); if (!recordOptions.force && signature && signature === lastRecordedSignature && (now - lastRecordedAt) < 1500) return null; if (!recordOptions.force) { lastRecordedSignature = signature; lastRecordedAt = now; } log('\u3010Visual Debug\u3011signature=' + signature, 'info'); log('\u3010Visual Debug\u3011optionCount=' + risk.optionCount, 'info'); log('\u3010Visual Debug\u3011emptyOptionCount=' + risk.emptyOptionCount, 'info'); log('\u3010Visual Debug\u3011visualOptionCount=' + risk.visualOptionCount, 'info'); log('\u3010Visual Debug\u3011stemHasLargeVisual=' + risk.stemHasLargeVisual, 'info'); log('\u3010Visual Debug\u3011matchedKeywords=' + JSON.stringify(risk.matchedKeywords || []), 'info'); log('\u3010Visual Debug\u3011level=' + risk.level, 'info'); log('\u3010Visual Debug\u3011reason=' + (risk.reason || '(none)'), 'info'); const currentInfo = getCurrentGlobalIndexFromAnswerSheet(); const currentGlobalIndex = Number(currentInfo?.globalIndex); const finalQuestion = question || normalizeText(root.innerText || root.textContent || '') || ('Question text missing'); const entry = { question: finalQuestion, options, type, questionType, globalIndex: Number.isFinite(currentGlobalIndex) && currentGlobalIndex > 0 ? currentGlobalIndex : undefined, imageLike: risk.level === 'image', isVisualQuestion: risk.level === 'image', skipApply: risk.level === 'image', visualRiskLevel: risk.level, visualReason: risk.reason || '' }; if (!saveQuestion(entry)) return null; if (risk.level === 'image') { const idx = Number(entry.globalIndex); if (Number.isFinite(idx) && idx > 0) { scannedImageIndexes.add(idx); scannedImageGlobalIndexes.add(idx); } } const marker = risk.level === 'image' ? 'image' : 'scanned'; markCurrentAnswerSheetItem(marker); log('[SCAN] globalIndex=' + entry.globalIndex + ' type=' + questionType + ' isImage=' + String(risk.level === 'image') + ' title=' + finalQuestion.slice(0, 50), 'info'); log('\u3010\u626b\u63cf\u3011\u5df2\u8bb0\u5f55 #' + entry.globalIndex + ' [' + type + '] risk=' + risk.level, risk.level === 'image' ? 'error' : 'success'); return entry; } function createLeftNavCollector() { const MAX_TEXT = 80; function navText(el, max = MAX_TEXT) { const text = String((el && (el.innerText || el.textContent)) || '') .replace(/\u00A0/g, ' ') .replace(/[ \t]+\n/g, '\n') .replace(/\n{3,}/g, '\n\n') .trim(); return text.length > max ? text.slice(0, max) : text; } function isNavVisible(el) { if (!el || !(el instanceof Element)) return false; if (el.closest('#main-panel, #show-main-panel-btn')) return false; const rect = el.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return false; const style = getComputedStyle(el); if (style.display === 'none' || style.visibility === 'hidden') return false; if (Number(style.opacity) === 0) return false; return true; } function navCssEscape(value) { if (window.CSS && CSS.escape) return CSS.escape(value); return String(value).replace(/([ #;?%&,.+*~':"!^$[\]()=>|/@])/g, '\\$1'); } function navSimpleSelector(el) { if (!el || !(el instanceof Element)) return ''; const tag = el.tagName.toLowerCase(); if (el.id) return tag + '#' + navCssEscape(el.id); const attrs = ['data-testid', 'data-test', 'data-id', 'data-key', 'data-index', 'aria-label', 'name', 'role']; for (const attr of attrs) { const val = el.getAttribute(attr); if (val && String(val).length < 80) { return tag + '[' + attr + '="' + String(val).replace(/"/g, '\\"') + '"]'; } } const cls = Array.from(el.classList || []).filter(Boolean).slice(0, 3).map(c => '.' + navCssEscape(c)).join(''); return tag + cls; } function navCssPath(el, maxDepth = 6) { if (!el || !(el instanceof Element)) return ''; const parts = []; let cur = el; while (cur && cur.nodeType === 1 && parts.length < maxDepth) { let part = navSimpleSelector(cur); if (!cur.id) { const parent = cur.parentElement; if (parent) { const sameTag = Array.from(parent.children).filter(x => x.tagName === cur.tagName); if (sameTag.length > 1) part += ':nth-of-type(' + (sameTag.indexOf(cur) + 1) + ')'; } } parts.unshift(part); if (cur.id) break; cur = cur.parentElement; } return parts.join(' > '); } function navRect(el) { const rect = el.getBoundingClientRect(); return { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) }; } function isClickableLike(el) { if (!el || !(el instanceof Element)) return false; const style = getComputedStyle(el); return el.tagName === 'BUTTON' || el.tagName === 'A' || el.getAttribute('role') === 'button' || typeof el.onclick === 'function' || style.cursor === 'pointer' || el.tabIndex >= 0; } function getClickableNavElement(el) { let cur = el; let depth = 0; while (cur && cur instanceof Element && depth < 4) { if (isClickableLike(cur)) return cur; cur = cur.parentElement; depth++; } return el; } function getNumericCandidates(root = document) { return Array.from(root.querySelectorAll('button,a,div,span,li,p')) .filter(isNavVisible) .map(el => { const text = navText(el, 20); if (!/^\d{1,3}$/.test(text)) return null; const rect = el.getBoundingClientRect(); if (rect.width > 140 || rect.height > 90) return null; return { el, text, clickable: getClickableNavElement(el), rect }; }) .filter(Boolean); } function findBestNavContainer() { const candidates = getNumericCandidates(); const countMap = new Map(); for (const item of candidates) { let parent = item.el.parentElement; let depth = 0; while (parent && parent instanceof Element && depth < 8) { const tag = parent.tagName; if (tag !== 'BODY' && tag !== 'HTML' && isNavVisible(parent) && !parent.closest('#main-panel, #show-main-panel-btn')) { const old = countMap.get(parent) || { el: parent, count: 0, items: new Set() }; old.items.add(item.clickable || item.el); old.count = old.items.size; countMap.set(parent, old); } parent = parent.parentElement; depth++; } } const containers = Array.from(countMap.values()) .filter(x => x.count >= 3) .map(x => { const rect = x.el.getBoundingClientRect(); const area = Math.max(1, rect.width * rect.height); const leftBonus = Math.max(0, window.innerWidth - rect.x); return { ...x, rect, score: x.count * 100000 + leftBonus - area / 100 }; }) .sort((a, b) => b.score - a.score); return containers[0] ? containers[0].el : null; } function getAllNavItems() { const container = findBestNavContainer(); const root = container || document; const seen = new Set(); const items = getNumericCandidates(root) .map(item => { const itemEl = item.clickable || item.el; if (!itemEl || seen.has(itemEl)) return null; seen.add(itemEl); return { localNoText: item.text, itemEl, selector: navCssPath(itemEl), rect: navRect(itemEl) }; }) .filter(Boolean) .sort((a, b) => { const dy = a.rect.y - b.rect.y; if (Math.abs(dy) > 5) return dy; return a.rect.x - b.rect.x; }) .map((item, index) => ({ ...item, globalIndex: index + 1 })); console.log('[LeftNavCollector] nav items count =', items.length); console.log('[LeftNavCollector] nav items =', items.map(x => ({ globalIndex: x.globalIndex, localNoText: x.localNoText, selector: x.selector, rect: x.rect }))); return items; } async function clickNavItem(item) { if (!item || !item.itemEl) return false; item.itemEl.scrollIntoView({ block: 'center', inline: 'nearest' }); await sleep(80); item.itemEl.click(); await sleep(600); return true; } function formatExistingRightEntry(entry) { if (!entry) return ''; const opts = (entry.options || []).map((opt, i) => { const label = opt.label || String.fromCharCode(65 + i); return label + '. ' + (opt.text || '') + (opt.value != null ? ' (value: ' + opt.value + ')' : ''); }).join('\n'); return [entry.question || '', opts].filter(Boolean).join('\n'); } async function callExistingRightExtractor(item) { const entry = await recordQuestion({ force: true }); return formatExistingRightEntry(entry); } async function collectAllByExistingRightExtractor() { const items = getAllNavItems(); const result = []; for (const item of items) { console.log('[LeftNavCollector] click', item.globalIndex); await clickNavItem(item); const text = await callExistingRightExtractor(item); result.push({ globalIndex: item.globalIndex, localNoText: item.localNoText, text }); console.log('[LeftNavCollector] collected', { globalIndex: item.globalIndex, length: text ? text.length : 0 }); } return result; } function formatAll(items) { return (items || []).map(x => [ '[globalIndex: ' + x.globalIndex + ']', '[localNoText: ' + x.localNoText + ']', '', x.text || '[EMPTY]' ].join('\n')).join('\n\n---\n\n'); } async function collectAllAndCopy() { const items = await collectAllByExistingRightExtractor(); const text = formatAll(items); await navigator.clipboard.writeText(text); console.log('[LeftNavCollector] copied all', { count: items.length, length: text.length }); return text; } return { getAllNavItems, clickNavItem, callExistingRightExtractor, collectAllByExistingRightExtractor, formatAll, collectAllAndCopy }; } // Disabled by request: left-nav auto click collection is no longer used. // The legacy dual-area copy flow below is the active experiment-area copy path. function getCurrentQuestionInfo() { const typeEl = document.querySelector('.questionTypeTitle___r6Fo9, [class*="questionTypeTitle"]'); const rawType = normalizeText(typeEl ? typeEl.textContent : ''); const qType = rawType.replace(/^[\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d\u5341]+[\u3001.\uff0e\s]*/, ''); const numEl = document.querySelector('.font16.noWrap___X6AS3, [class*="noWrap"]'); const rawNum = normalizeText(numEl ? numEl.textContent : ''); const numMatch = rawNum.match(/\d+/); const qNum = numMatch ? numMatch[0] : ''; return { qType, qNum }; } function injectQuestionMarkerStyle() { if (document.getElementById('touge-question-marker-style')) return; const style = document.createElement('style'); style.id = 'touge-question-marker-style'; style.textContent = ` @keyframes tougeRainbow { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } [class*="answerSheetItem"].touge-scanned { background: #ffd93d !important; border: 2px solid #f1c40f !important; box-shadow: 0 0 12px rgba(255,217,61,0.85) !important; border-radius: 6px !important; } [class*="answerSheetItem"].touge-answered { background: linear-gradient(90deg,#ff4d4f,#faad14,#fadb14,#52c41a,#1677ff,#722ed1,#eb2f96,#ff4d4f) !important; background-size: 400% 400% !important; animation: tougeRainbow 3.5s linear infinite !important; border: 2px solid rgba(255,255,255,0.95) !important; box-shadow: 0 0 14px rgba(24,144,255,0.85) !important; border-radius: 6px !important; } [class*="answerSheetItem"].touge-image { background: #ff3333 !important; border: 2px solid rgba(255,255,255,0.95) !important; box-shadow: 0 0 15px 5px rgba(255,51,51,0.85) !important; border-radius: 6px !important; } [class*="answerSheetItem"].touge-suspicious { background: #fa8c16 !important; border: 2px solid #d46b08 !important; box-shadow: 0 0 12px rgba(250,140,22,0.75) !important; border-radius: 6px !important; } [class*="answerSheetItem"].touge-scanned [class*="qindex"], [class*="answerSheetItem"].touge-answered [class*="qindex"], [class*="answerSheetItem"].touge-image [class*="qindex"], [class*="answerSheetItem"].touge-suspicious [class*="qindex"], [class*="qindex"].touge-scanned, [class*="qindex"].touge-answered, [class*="qindex"].touge-image, [class*="qindex"].touge-suspicious { background: transparent !important; } `; document.head.appendChild(style); } function clearTougeMarkerInlineStyle(el) { if (!el || !el.style) return; ['background', 'color', 'border', 'box-shadow', 'border-radius', 'font-weight', 'background-size', 'animation', 'text-shadow'].forEach(prop => el.style.removeProperty(prop)); } function applyTougeMarkerInlineStyle(itemEl, indexEl, markerType) { if (!itemEl || !indexEl || !itemEl.style || !indexEl.style) return; clearTougeMarkerInlineStyle(itemEl); clearTougeMarkerInlineStyle(indexEl); itemEl.style.setProperty('border-radius', '6px', 'important'); indexEl.style.setProperty('background', 'transparent', 'important'); indexEl.style.setProperty('text-align', 'center', 'important'); if (markerType === 'image') { itemEl.style.setProperty('background', '#ff3333', 'important'); itemEl.style.setProperty('border', '2px solid rgba(255,255,255,0.95)', 'important'); itemEl.style.setProperty('box-shadow', '0 0 15px 5px rgba(255,51,51,0.85)', 'important'); indexEl.style.setProperty('color', '#fff', 'important'); indexEl.style.setProperty('font-weight', '900', 'important'); indexEl.style.setProperty('text-shadow', '0 0 3px rgba(0,0,0,0.55)', 'important'); return; } if (markerType === 'suspicious') { itemEl.style.setProperty('background', '#fa8c16', 'important'); itemEl.style.setProperty('border', '2px solid #d46b08', 'important'); itemEl.style.setProperty('box-shadow', '0 0 12px rgba(250,140,22,0.75)', 'important'); indexEl.style.setProperty('color', '#111', 'important'); indexEl.style.setProperty('font-weight', '800', 'important'); return; } if (markerType === 'answered') { itemEl.style.setProperty('background', 'linear-gradient(90deg,#ff4d4f,#faad14,#fadb14,#52c41a,#1677ff,#722ed1,#eb2f96,#ff4d4f)', 'important'); itemEl.style.setProperty('background-size', '400% 400%', 'important'); itemEl.style.setProperty('animation', 'tougeRainbow 3.5s linear infinite', 'important'); itemEl.style.setProperty('border', '2px solid rgba(255,255,255,0.95)', 'important'); itemEl.style.setProperty('box-shadow', '0 0 14px rgba(24,144,255,0.85)', 'important'); indexEl.style.setProperty('color', '#fff', 'important'); indexEl.style.setProperty('font-weight', '900', 'important'); indexEl.style.setProperty('text-shadow', '0 0 3px rgba(0,0,0,0.55)', 'important'); return; } itemEl.style.setProperty('background', '#ffd93d', 'important'); itemEl.style.setProperty('border', '2px solid #f1c40f', 'important'); itemEl.style.setProperty('box-shadow', '0 0 12px rgba(255,217,61,0.85)', 'important'); indexEl.style.setProperty('color', '#111', 'important'); indexEl.style.setProperty('font-weight', '800', 'important'); } function markCurrentAnswerSheetItem(styleType = 'scanned') { injectQuestionMarkerStyle(); const markerType = styleType || 'scanned'; const stateClass = markerType === 'image' ? 'touge-image' : markerType === 'suspicious' ? 'touge-suspicious' : markerType === 'answered' ? 'touge-answered' : 'touge-scanned'; const itemEl = document.querySelector('[class*="answerSheetItem"][class*="selected"]'); if (!itemEl) { log('\u3010\u6807\u8bb0\u3011\u672a\u627e\u5230\u5f53\u524d\u9009\u4e2d\u7684\u7b54\u9898\u5361\u9898\u53f7\uff0c\u8df3\u8fc7\u6807\u8bb0', 'info'); return false; } const indexEl = itemEl.querySelector('[class*="qindex"]') || itemEl; const alreadyImage = itemEl.dataset.tougeMarker === 'image' || indexEl.dataset.tougeMarker === 'image' || itemEl.classList.contains('touge-image') || indexEl.classList.contains('touge-image'); if (alreadyImage && markerType !== 'image') { log('\u3010\u6807\u8bb0\u3011\u5f53\u524d\u9898\u5df2\u662f image\uff0c\u62d2\u7edd\u88ab ' + markerType + ' \u8986\u76d6', 'info'); return true; } itemEl.classList.remove('touge-scanned', 'touge-suspicious', 'touge-answered', 'touge-image'); indexEl.classList.remove('touge-scanned', 'touge-suspicious', 'touge-answered', 'touge-image'); itemEl.classList.add(stateClass); indexEl.classList.add(stateClass); applyTougeMarkerInlineStyle(itemEl, indexEl, markerType); itemEl.dataset.tougeMarker = markerType; indexEl.dataset.tougeMarker = markerType; return true; } function markQuestionOnSheet(styleType = 'scanned', questionNum = null) { const target = typeof styleType === 'object' ? styleType : { styleType, questionKey: questionNum }; return markCurrentAnswerSheetItem(target.styleType || 'scanned'); } function restoreQuestionRiskMarkers() { } function init() { log('[LegacyBridge] init state', 'info'); legacyCopyHelperOriginal(); legacyHelperOriginal(); log('[LegacyBridge] Helper functions ready', 'info'); loadScannedImageState(); installPasteInterceptor(); try { localStorage.removeItem('touge_visual_question_ids'); } catch (_) { } createMainPanel(); installStableCopyHandler(); setTimeout(restoreQuestionRiskMarkers, 600); setTimeout(restoreQuestionRiskMarkers, 1800); setInterval(() => { const questionEl = document.querySelector('.question_title'); const currentQuestionTitle = questionEl ? questionEl.innerText.trim() : ''; if (currentQuestionTitle && currentQuestionTitle !== lastKnownQuestionTitle) { lastKnownQuestionTitle = currentQuestionTitle; resetAutoSubmitTimer(); const type = detectQuestionType(); if (type !== '未知') { log(`检测到题目变化: ${type}`, 'info'); if (imageScanner.isScanning) { setTimeout(() => imageScanner.checkCurrentImage(), 500); } } } }, 1000); setInterval(() => { if (enabled && aiAnswers.length === 0) { recordQuestion(); } }, 2000); } init(); })();