// ==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 = `