// ==UserScript== // @name 智能考试助手(拾取选择器版·已修复) // @namespace http://tampermonkey.net/ // @version 2.5 // @description 支持悬停题目、点击题目、划词选中、拾取选择器(无需控制台)、自定义选择器即时生效 // @author You // @icon https://tse2-mm.cn.bing.net/th/id/OIP-C.s8WZvq7biii2N7NkGdXGTwAAAA?rs=1 // @connect www.baidu.com // @connect deepseek.1633252822.workers.dev // @connect api.deepseek.com // @match *://*/* // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @run-at document-end // @require https://scriptcat.org/lib/6127/1.0.0/%E8%87%AA%E7%94%A8%E9%A2%98%E5%BA%93.js?sha384-tA0g0VB5wMm1xGTWB7za3lNR5p6gmRPVzXGuJSEfEcYZNNKrP7URHnx4NrhCkkNi // ==/UserScript== (function() { 'use strict'; // ============= 题库数据 ============= // 外部题库(通过 @require 引用) const EXTERNAL_QUESTION_DATABASE = window.QUESTION_DATABASE || []; // 内置题库(直接写在脚本里) const BUILTIN_QUESTION_DATABASE = [ // 示例数据 //{ //"question": "你的问题", //"options": [], //"answer": "你的答案", //"type": "choice" //}, ]; // 合并外部题库和内置题库 const QUESTION_DATABASE = [...EXTERNAL_QUESTION_DATABASE, ...BUILTIN_QUESTION_DATABASE]; console.log('题库加载完成:外部', EXTERNAL_QUESTION_DATABASE.length, '题 + 内置', BUILTIN_QUESTION_DATABASE.length, '题 = 共', QUESTION_DATABASE.length, '题'); // ============= 脚本配置 ============= const DEBUG = false; let lastClickedQuestion = null; let lastHoveredQuestion = null; let lastClickedQuestionElem = null; let lastHoveredQuestionElem = null; let customQuestionSelector = localStorage.getItem('customQuestionSelector') || ''; let pickMode = false; // 拾取模式标志 let originalCursor = ''; // 保存原始光标 let highlightElement = null; // 高亮元素 // ============= 按需验证功能 ============= const TARGET_HOST = 'cloud.italent.cn'; // 仅在此域名下验证 const CORRECT_PASSWORD = 'admin123'; // 请修改为你的密码 const STORAGE_KEY = 'search_assistant_verified'; function isTargetSite() { return window.location.hostname === TARGET_HOST; } function isVerified() { return sessionStorage.getItem(STORAGE_KEY) === 'true'; } function setVerified() { sessionStorage.setItem(STORAGE_KEY, 'true'); } function log(...args) { if (DEBUG) console.log('[辅助工具]', ...args); } log('脚本已加载,题库大小:', QUESTION_DATABASE.length); if (customQuestionSelector) log('使用自定义选择器:', customQuestionSelector); // ============= 工具函数 ============= // 从文本中提取纯净的题干(去掉选项部分) function extractPureQuestion(text) { if (!text) return ''; // 1. 去掉开头的题型标签(如【单选题】、【多选题】等) let cleaned = text.replace(/^【[^】]+】\s*/, ''); // 2. 查找第一个选项字母的位置(如 A. A、等) const optionPattern = /\s+[A-D][\.、]/; const match = cleaned.match(optionPattern); if (match) { const index = match.index; if (index > 0) { cleaned = cleaned.substring(0, index).trim(); } } // 3. 如果还有换行,取第一行(作为题干) const lines = cleaned.split('\n'); if (lines.length > 1 && lines[0].trim().length > 5) { return lines[0].trim(); } return cleaned; } // 从元素中智能提取题干(优先查找常见子元素,否则分析文本) function getQuestionTextFromElement(elem) { if (!elem) return ''; // 优先查找可能的题干子元素(类名包含 title, stem, question 等) const titleSelectors = [ '.exam-topic-item-title-name', '.question-title', '.topic-title', '.question-stem', '.exam-question-title', '[class*="title"]', '[class*="stem"]', '[class*="question"]' ]; for (const sel of titleSelectors) { const sub = elem.querySelector(sel); if (sub && sub.textContent.trim()) { let text = sub.textContent.trim(); text = extractPureQuestion(text); if (text) return text; } } // 如果没有找到子元素,获取整个元素文本并提取 const fullText = elem.textContent.trim(); if (fullText) { // 先尝试直接提取纯净题干 let cleaned = extractPureQuestion(fullText); if (cleaned) return cleaned; // 如果文本较长(包含很多内容),尝试取第一段 if (fullText.length > 200) { const lines = fullText.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (/^\d+[\.、]/.test(line) && line.length > 5 && line.length < 200) { cleaned = extractPureQuestion(line); if (cleaned) return cleaned; } } if (lines[0] && lines[0].trim().length > 5) { cleaned = extractPureQuestion(lines[0]); if (cleaned) return cleaned; } } return cleaned || fullText; } return ''; } // 从 DOM 元素中提取题目文本(含选项,供 AI 使用) function getFullQuestionTextFromElem(elem) { if (!elem) return { question: '', options: [], questionType: '' }; // 提取题型标签(单选/多选/判断等) let questionType = ''; const typeMatch = elem.textContent.match(/^【([^】]+)】/); if (typeMatch) questionType = typeMatch[1]; // 提取纯题干 const question = extractPureQuestion(elem.textContent); // 从 radio/checkbox input 提取选项(与 autoFillAnswer 共用逻辑) const options = []; const seen = new Set(); const checkboxes = elem.querySelectorAll('input[type="checkbox"]'); if (checkboxes.length > 0) { checkboxes.forEach(cb => { const label = getLabelForInput(cb); const text = label ? label.textContent.trim() : cb.nextSibling?.textContent?.trim() || ''; const clean = text.replace(/^\s*[A-Za-z][\.、::]\s*/, '').trim(); if (clean && clean.length > 0 && clean.length < 200 && !seen.has(clean)) { seen.add(clean); const letter = seen.size <= 4 ? String.fromCharCode(64 + seen.size) : ''; options.push(letter ? `${letter}. ${clean}` : clean); } }); } const radios = elem.querySelectorAll('input[type="radio"]'); if (radios.length > 0 && options.length === 0) { radios.forEach(radio => { const label = getLabelForInput(radio); const text = label ? label.textContent.trim() : radio.nextSibling?.textContent?.trim() || ''; const clean = text.replace(/^\s*[A-Za-z][\.、::]\s*/, '').trim(); if (clean && clean.length > 0 && clean.length < 200 && !seen.has(clean)) { seen.add(clean); const letter = seen.size <= 4 ? String.fromCharCode(64 + seen.size) : ''; options.push(letter ? `${letter}. ${clean}` : clean); } }); } // 如果 input 方式没找到,用纯文本方式兜底 if (options.length === 0) { const lines = elem.textContent.split('\n').map(l => l.trim()).filter(l => l.length > 0); for (const line of lines) { const optMatch = line.match(/^([A-Za-z])[..、::]\s*(.+)/); if (optMatch) { const clean = optMatch[2].trim(); if (clean && !seen.has(clean)) { seen.add(clean); options.push(`${optMatch[1].toUpperCase()}. ${clean}`); } } } } return { question, options, questionType }; } // 根据搜索文本,在页面上实时查找匹配的题目 DOM 元素 function findCurrentQuestionElem(query) { if (!query) return null; const cleanQuery = normalizeText(query); // 优先用自定义选择器 if (customQuestionSelector) { const candidates = document.querySelectorAll(customQuestionSelector); for (const elem of candidates) { const text = getQuestionTextFromElement(elem); if (normalizeText(text) === cleanQuery) return elem; if (calculateSimilarity(normalizeText(text), cleanQuery) >= 0.8) return elem; } } // 用 .exam-topic-item 兜底 const items = document.querySelectorAll('.exam-topic-item'); for (const item of items) { const titleElem = item.querySelector('.exam-topic-item-title-name'); const text = titleElem ? titleElem.textContent.trim() : item.textContent; if (normalizeText(text) === cleanQuery) return item; if (calculateSimilarity(normalizeText(text), cleanQuery) >= 0.8) return item; } // 后备:通用查找(向上找带题号的父元素) const allElems = document.querySelectorAll('*'); for (const el of allElems) { if (el.children.length > 0) continue; // 跳过容器元素 const text = el.textContent?.trim() || ''; if (calculateSimilarity(normalizeText(text), cleanQuery) >= 0.85) { let parent = el.parentElement; let depth = 0; while (parent && depth < 5) { if (parent.querySelector('input[type="radio"], input[type="checkbox"]')) { return parent; } parent = parent.parentElement; depth++; } } } return null; } // 生成元素的唯一CSS选择器(优先id,其次class组合,最后标签+索引) function generateSelector(element) { if (!element) return ''; // 优先使用 id if (element.id) { return `#${element.id}`; } // 尝试获取元素的主要 class(优先取第一个) const getMainClass = (el) => { if (!el.className || typeof el.className !== 'string') return ''; const classes = el.className.trim().split(/\s+/); // 过滤掉常见的表示位置、大小、颜色等无关类(可自定义) const blacklist = ['active', 'selected', 'hover', 'focus', 'clearfix', 'left', 'right', 'content', 'info']; const valid = classes.filter(c => !blacklist.includes(c)); return valid.length ? valid[0] : classes[0]; }; // 向上查找可能包含题目特征的父容器(最大深度5) let path = []; let current = element; let depth = 0; while (current && current !== document.body && depth < 5) { const tag = current.tagName.toLowerCase(); let selector = tag; const mainClass = getMainClass(current); if (mainClass) { selector += `.${mainClass}`; } else if (current.id) { selector = `#${current.id}`; } // 如果该选择器在当前上下文唯一,则停止继续向上(避免过度组合) if (depth === 0) { // 当前元素本身 if (selector !== tag) { // 有 class 或 id,检查唯一性 if (document.querySelectorAll(selector).length === 1) { return selector; } } } // 组合进路径 path.unshift(selector); current = current.parentElement; depth++; } // 如果路径为空,返回原始标签名(最坏情况) if (path.length === 0) return element.tagName.toLowerCase(); // 组合最终选择器,去掉可能出现的 :nth-of-type(我们避免使用) let finalSelector = path.join(' > '); // 移除可能存在的 :nth-of-type(我们不用) finalSelector = finalSelector.replace(/:nth-of-type\(\d+\)/g, ''); // 检查是否唯一,如果不唯一,提示用户手动修改 if (document.querySelectorAll(finalSelector).length > 1) { // 不唯一,尝试简化(取最后一段) const lastPart = path[path.length - 1]; if (document.querySelectorAll(lastPart).length === 1) { return lastPart; } // 否则返回带提示 return finalSelector + ' (可能不唯一,建议手动修改)'; } return finalSelector; } // 退出拾取模式 function exitPickMode() { if (!pickMode) return; pickMode = false; document.body.style.cursor = originalCursor; if (highlightElement) { highlightElement.style.outline = ''; highlightElement = null; } // 移除临时事件监听 document.removeEventListener('mouseover', pickMouseOver); document.removeEventListener('click', pickClick); document.removeEventListener('keydown', pickKeydown); log('退出拾取模式'); } // 拾取模式鼠标悬停高亮 function pickMouseOver(e) { const target = e.target; if (highlightElement === target) return; if (highlightElement) { highlightElement.style.outline = ''; } highlightElement = target; target.style.outline = '2px solid #ff6b6b'; } // 拾取模式点击 async function pickClick(e) { e.preventDefault(); e.stopPropagation(); if (e.target === pickBtn || pickBtn.contains(e.target)) return; const target = e.target; let selector = generateSelector(target); let preview = target.textContent.trim().substring(0, 50); if (preview) preview = '文本预览: ' + preview; let message = `已捕获元素选择器:\n${selector}\n${preview}\n是否应用为题目选择器?`; let dialogTitle = '拾取确认'; if (selector.includes('(可能不唯一)')) { message = `注意:该选择器可能不唯一!\n${selector}\n${preview}\n\n建议手动修改选择器。`; dialogTitle = '选择器可能不唯一'; } const result = await customConfirm(message, dialogTitle); if (result?.confirmed) { customQuestionSelector = selector.replace(/\s*\(可能不唯一.*\)\s*$/, '').trim(); localStorage.setItem('customQuestionSelector', customQuestionSelector); showToast(`选择器已保存并立即生效!\n${customQuestionSelector}`, 'success'); log('自定义选择器更新为:', customQuestionSelector); lastHoveredQuestion = null; lastClickedQuestion = null; lastHoveredQuestionElem = null; lastClickedQuestionElem = null; } else { const cleanSelector = selector.replace(/\s*\(可能不唯一.*?\)\s*$/, '').trim(); const manualSelector = await customInput( '请输入CSS选择器:', cleanSelector, '.exam-topic-item' ); if (manualSelector && manualSelector.trim()) { customQuestionSelector = manualSelector.trim(); localStorage.setItem('customQuestionSelector', customQuestionSelector); showToast(`选择器已保存!\n${customQuestionSelector}`, 'success'); lastHoveredQuestion = null; lastClickedQuestion = null; lastHoveredQuestionElem = null; lastClickedQuestionElem = null; } } exitPickMode(); } // 拾取模式下按ESC退出 function pickKeydown(e) { if (e.key === 'Escape') { exitPickMode(); } } // 进入拾取模式 function enterPickMode() { if (pickMode) return; pickMode = true; originalCursor = document.body.style.cursor; document.body.style.cursor = 'crosshair'; document.addEventListener('mouseover', pickMouseOver); document.addEventListener('click', pickClick); document.addEventListener('keydown', pickKeydown); log('进入拾取模式,点击任意元素生成选择器'); } // ============= 题库搜索引擎 ============= // OLD normalizeText REMOVED - using new version below function __old_normalizeText__(text) { // 1. 统一中英文括号:中文括号 -> 英文括号 let normalized = text.replace(/(/g, '(').replace(/)/g, ')'); // 2. 统一中英文引号:中文引号 -> 英文双引号 normalized = normalized.replace(/[‘’“”]/g, '"'); // 3. 去除空格和常见标点(包括所有括号和引号) normalized = normalized.replace(/\s+/g, '') .replace(/[,,.。??!!::;;(())\[\]【】"'']/g, '') .toLowerCase(); return normalized; } // OLD calculateSimilarity REMOVED function __old_calculateSimilarity__(str1, str2) { const set1 = new Set(str1); const set2 = new Set(str2); let intersection = 0; for (const char of set1) { if (set2.has(char)) intersection++; } const union = set1.size + set2.size - intersection; return intersection / union; } // OLD findQuestionInDatabase REMOVED function __old_findQuestionInDatabase__(query) { if (!query) return null; const cleanQuery = normalizeText(query); log('搜索查询:', cleanQuery); let bestMatch = null; let highestSimilarity = 0.7; for (const item of QUESTION_DATABASE) { const cleanQuestion = normalizeText(item.question); if (cleanQuestion === cleanQuery) { log('精确匹配'); return item; } if (cleanQuestion.includes(cleanQuery) || cleanQuery.includes(cleanQuestion)) { bestMatch = item; highestSimilarity = 0.8; continue; } const similarity = calculateSimilarity(cleanQuery, cleanQuestion); if (similarity > highestSimilarity) { highestSimilarity = similarity; bestMatch = item; } } if (highestSimilarity >= 0.7) log('相似匹配,相似度:', highestSimilarity); return bestMatch; } // ============= 模糊搜索核心(多策略) ============= function fuzzySearch(query, maxResults = 20) { if (!query) return []; const rawQuery = query.trim(); if (!rawQuery) return []; const resultsMap = new Map(); // itemKey -> { item, score } for (const item of QUESTION_DATABASE) { const dbQuestion = item.question; const dbText = normalizeText(dbQuestion); const queryText = normalizeText(rawQuery); // 策略0:完全相等 if (dbText === queryText) { resultsMap.set(dbQuestion, { item, score: 1.0 }); continue; } // 策略1:包含关系(查询词被题目包含,或题目被查询词包含) if (dbText.includes(queryText) || queryText.includes(dbText)) { const score = 0.85 + 0.1 * (Math.min(queryText.length, dbText.length) / Math.max(queryText.length, dbText.length)); const prev = resultsMap.get(dbQuestion); if (!prev || prev.score < Math.min(score, 0.95)) { resultsMap.set(dbQuestion, { item, score: Math.min(score, 0.95) }); } continue; } // 策略2:宽松包含(去除所有空格和标点后的包含) const queryNoPunct = queryText.replace(/[,,.。??!!::;;]+/g, ''); const dbNoPunct = dbText.replace(/[,,.。??!!::;;]+/g, ''); if (dbNoPunct.includes(queryNoPunct) || queryNoPunct.includes(dbNoPunct)) { const score = 0.75 + 0.1 * (Math.min(queryNoPunct.length, dbNoPunct.length) / Math.max(queryNoPunct.length, dbNoPunct.length)); const prev = resultsMap.get(dbQuestion); if (!prev || prev.score < Math.min(score, 0.88)) { resultsMap.set(dbQuestion, { item, score: Math.min(score, 0.88) }); } continue; } // 策略3:N-gram 词片段匹配(中文友好) const queryNgrams = getNgrams(queryText, 2).concat(getNgrams(queryText, 3)); const dbNgrams = new Set(getNgrams(dbText, 2).concat(getNgrams(dbText, 3))); let ngramScore = 0; let ngramHits = 0; for (const ng of queryNgrams) { if (dbNgrams.has(ng)) { ngramHits++; } } if (queryNgrams.length > 0) { ngramScore = ngramHits / queryNgrams.length; } if (ngramScore >= 0.35) { const score = 0.4 + ngramScore * 0.45; const prev = resultsMap.get(dbQuestion); if (!prev || prev.score < score) { resultsMap.set(dbQuestion, { item, score }); } continue; } // 策略4:关键词匹配(提取关键区分词,排除常见字) const queryKeywords = extractKeywords(rawQuery, 5); if (queryKeywords.length > 0) { let keywordScore = 0; for (const kw of queryKeywords) { const kwNorm = normalizeText(kw); if (dbText.includes(kwNorm) || dbNoPunct.includes(kwNorm)) { keywordScore += kw.length * 1.5; // 关键词越长分越高 } } const kwNormScore = keywordScore / (queryText.length * 2); if (kwNormScore >= 0.25) { const score = 0.35 + kwNormScore * 0.35; const prev = resultsMap.get(dbQuestion); if (!prev || prev.score < score) { resultsMap.set(dbQuestion, { item, score }); } continue; } } // 策略5:改进的字符级相似度(考虑顺序) const charSim = improvedCharSimilarity(queryText, dbText); if (charSim >= 0.38) { const score = 0.3 + charSim * 0.5; const prev = resultsMap.get(dbQuestion); if (!prev || prev.score < score) { resultsMap.set(dbQuestion, { item, score }); } } } // 排序并返回 const results = Array.from(resultsMap.values()); results.sort((a, b) => b.score - a.score); return results.slice(0, maxResults); } // 生成 N-gram 片段(用于中文分词效果) function getNgrams(text, n) { const result = []; for (let i = 0; i <= text.length - n; i++) { result.push(text.substring(i, i + n)); } return result; } // 提取关键词(去除停用词后取有区分力的片段) function extractKeywords(text, maxCount) { const stopWords = new Set([ '的', '了', '是', '在', '和', '与', '或', '等', '以下', '关于', '对于', '根据', '按照', '为了', '通过', '进行', '具有', '可以', '一个', '哪些', '哪个', '什么', '如何', '怎样', '多少', '几个', '属于', '不是', '是的', '就是', '这种', '那种', '上述', '下列', '包括', '以及', '其中', '主要', '能够', '应该', '必须', '可能', '问题', '题目', '选项', '答案', '正确', '错误', '选择', '内容', '单选', '多选', '判断', '填空', '论述', '名词', '计算', '分析', '简答', '要求', '分析', '比较', '说明', '论述', '判断', '论述题' ]); // 提取连续的有意义字符片段(2-6个字符,去除纯数字和常见符号) const chunks = []; let i = 0; while (i < text.length) { const code = text.charCodeAt(i); // 中文字符范围 if ((code >= 0x4E00 && code <= 0x9FFF) || (code >= 0x3400 && code <= 0x4DBF)) { let end = i + 1; while (end < text.length) { const c2 = text.charCodeAt(end); if ((c2 >= 0x4E00 && c2 <= 0x9FFF) || (c2 >= 0x3400 && c2 <= 0x4DBF)) { end++; } else { break; } } const chunk = text.substring(i, end); if (chunk.length >= 2 && !stopWords.has(chunk)) { chunks.push(chunk); } i = end; } else { i++; } } // 按长度降序(越长的词越有区分力) chunks.sort((a, b) => b.length - a.length); return chunks.slice(0, maxCount); } // 改进的字符相似度(考虑顺序和连续匹配) function improvedCharSimilarity(s1, s2) { if (!s1 || !s2) return 0; // LCS 最长公共子序列(简化版,限制长度提升性能) const lcsLen = lcsLength(s1, s2, Math.min(s1.length, s2.length, 50)); // 连续匹配加分 const contigScore = longestContiguousMatch(s1, s2) / Math.max(s1.length, s2.length); // 综合 const lcsRatio = (2.0 * lcsLen) / (s1.length + s2.length); return Math.max(lcsRatio * 0.7 + contigScore * 0.3, lcsRatio); } // 限制长度的 LCS(避免性能问题) function lcsLength(s1, s2, limit) { if (!s1.length || !s2.length) return 0; const m = s1.length; const n = s2.length; // 限制尺寸,大的话用近似 if (m > limit || n > limit) { // 降采样近似 const step = Math.max(1, Math.floor(Math.max(m, n) / limit)); let count = 0; for (const ch of s1) { if (s2.includes(ch)) count++; } return Math.floor(count * Math.min(m, n) / Math.max(m, n)); } let prev = new Uint16Array(n + 1); let curr = new Uint16Array(n + 1); for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (s1[i - 1] === s2[j - 1]) { curr[j] = prev[j - 1] + 1; } else { curr[j] = Math.max(prev[j], curr[j - 1]); } } [prev, curr] = [curr, prev]; } return prev[n]; } // 最长连续匹配 function longestContiguousMatch(s1, s2) { let max = 0, current = 0; for (const ch of s1) { if (s2.includes(ch)) { current++; if (current > max) max = current; } else { current = 0; } } return max; } // ============= 高亮匹配(模糊搜索结果展示) ============= function highlightMatch(text, query) { if (!query) return escapeHtml(text); const cleanQuery = normalizeText(query); const cleanText = normalizeText(text); if (cleanText.includes(cleanQuery)) { const idx = cleanText.indexOf(cleanQuery); let realIdx = 0, cleanIdx = 0; while (cleanIdx < idx && realIdx < text.length) { const c = text[realIdx]; const nc = text.charCodeAt(realIdx); if ((nc >= 0x4E00 && nc <= 0x9FFF) || (nc >= 0x3400 && nc <= 0x4DBF)) { cleanIdx++; } else if (/[a-zA-Z0-9]/.test(c)) { cleanIdx++; } realIdx++; } let end = realIdx; let qi = 0; while (qi < cleanQuery.length && end < text.length) { const c = text[end]; const nc = text.charCodeAt(end); if ((nc >= 0x4E00 && nc <= 0x9FFF) || (nc >= 0x3400 && nc <= 0x4DBF)) { if (qi < cleanQuery.length) qi++; } else if (/[a-zA-Z0-9]/.test(c)) { if (qi < cleanQuery.length) qi++; } end++; } return escapeHtml(text.slice(0, realIdx)) + '' + escapeHtml(text.slice(realIdx, end)) + '' + escapeHtml(text.slice(end)); } return escapeHtml(text); } // ============= 核心搜索函数 ============= function normalizeText(text) { let normalized = text.replace(/(/g, '(').replace(/)/g, ')'); normalized = normalized.replace(/[''""]/g, '"'); normalized = normalized.replace(/\s+/g, '') .replace(/[,,.。??!!::;;(())\[\]【】"'']/g, '') .toLowerCase(); return normalized; } function calculateSimilarity(str1, str2) { if (!str1 || !str2) return 0; // 精确包含则完全匹配 if (str1.includes(str2) || str2.includes(str1)) return 1; // 子串匹配:统计 str1 中有多少个连续 5 字片段在 str2 中 let matchCount = 0; let totalCount = 0; for (let i = 0; i <= str1.length - 5; i++) { const chunk = str1.substring(i, i + 5); totalCount++; if (str2.includes(chunk)) matchCount++; } if (totalCount === 0) return 0; return matchCount / totalCount; } function findQuestionInDatabase(query) { if (!query) return null; const cleanQuery = normalizeText(query); log('搜索查询:', cleanQuery); let bestMatch = null; let highestSimilarity = 0.7; for (const item of QUESTION_DATABASE) { const cleanQuestion = normalizeText(item.question); if (cleanQuestion === cleanQuery) { log('精确匹配'); return item; } if (cleanQuestion.includes(cleanQuery) || cleanQuery.includes(cleanQuestion)) { bestMatch = item; highestSimilarity = 0.8; continue; } const similarity = calculateSimilarity(cleanQuery, cleanQuestion); if (similarity > highestSimilarity) { highestSimilarity = similarity; bestMatch = item; } } if (highestSimilarity >= 0.7) log('相似匹配,相似度:', highestSimilarity); return bestMatch; } // ============= 样式设置 ============= function __old_normalizeText__(text) { let normalized = text.replace(/(/g, '(').replace(/)/g, ')'); normalized = normalized.replace(/[''""]/g, '"'); normalized = normalized.replace(/\s+/g, '') .replace(/[,,.。??!!::;;(())\[\]【】"'']/g, '') .toLowerCase(); return normalized; } // OLD findQuestionInDatabase REMOVED function __old_findQuestionInDatabase__(query) { if (!query) return null; const cleanQuery = normalizeText(query); log('搜索查询:', cleanQuery); let bestMatch = null; let highestSimilarity = 0.7; for (const item of QUESTION_DATABASE) { const cleanQuestion = normalizeText(item.question); if (cleanQuestion === cleanQuery) { log('精确匹配'); return item; } if (cleanQuestion.includes(cleanQuery) || cleanQuery.includes(cleanQuestion)) { bestMatch = item; highestSimilarity = 0.8; continue; } const similarity = calculateSimilarity(cleanQuery, cleanQuestion); if (similarity > highestSimilarity) { highestSimilarity = similarity; bestMatch = item; } } if (highestSimilarity >= 0.7) log('相似匹配,相似度:', highestSimilarity); return bestMatch; } // OLD calculateSimilarity REMOVED function __old_calculateSimilarity__(str1, str2) { const set1 = new Set(str1); const set2 = new Set(str2); let intersection = 0; for (const char of set1) { if (set2.has(char)) intersection++; } const union = set1.size + set2.size - intersection; return intersection / union; } // ============= 样式设置 ============= GM_addStyle(` #search-panel { position: fixed; background: rgba(242, 244, 247, 0.96); border: 1px solid rgba(0,0,0,0.08); border-radius: 10px; box-shadow: 0 8px 32px rgba(0,0,0,0.1), 0 2px 8px rgba(0,0,0,0.06); padding: 0; z-index: 999999; display: none; font-size: 13px; color: #4a5568; width: 420px; max-height: 540px; overflow: hidden; opacity: 0.97; top: 80px; left: 100px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } #search-panel .panel-body { padding: 14px 16px; max-height: 480px; overflow-y: auto; } #search-panel .panel-body::-webkit-scrollbar { width: 4px; } #search-panel .panel-body::-webkit-scrollbar-track { background: transparent; } #search-panel .panel-body::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.12); border-radius: 4px; } #search-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 14px 11px; cursor: move; user-select: none; background: rgba(235, 238, 242, 0.98); border-bottom: 1px solid rgba(0,0,0,0.06); border-radius: 10px 10px 0 0; } #header-controls { display: flex; align-items: center; gap: 4px; } .header-ctrl-btn { cursor: pointer; font-size: 13px; padding: 4px; border-radius: 6px; transition: all 0.2s; background: rgba(0,0,0,0.04); color: rgba(0,0,0,0.38); border: none; line-height: 1; display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; } .header-ctrl-btn:hover { background: rgba(0,0,0,0.08); color: rgba(0,0,0,0.62); } .header-ctrl-btn.pinned { background: rgba(0,0,0,0.12); color: rgba(0,0,0,0.7); } .header-ctrl-btn svg { width: 14px; height: 14px; } #opacity-control { display: flex; align-items: center; margin-bottom: 12px; padding: 7px 10px; border-radius: 7px; background: rgba(0,0,0,0.03); border: 1px solid rgba(0,0,0,0.06); font-size: 12px; color: rgba(0,0,0,0.38); } #opacity-slider { flex: 1; margin: 0 10px; cursor: pointer; accent-color: #8895a7; height: 3px; } #search-title { font-weight: 500; font-size: 13px; color: rgba(0,0,0,0.55); letter-spacing: 0.3px; } #search-close { cursor: pointer; color: rgba(0,0,0,0.35); font-size: 18px; padding: 4px; border-radius: 6px; transition: all 0.2s; background: rgba(0,0,0,0.04); border: none; line-height: 1; display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; } #search-close:hover { background: rgba(0,0,0,0.08); color: rgba(0,0,0,0.6); } #search-display { font-weight: 500; margin-bottom: 10px; white-space: normal; word-break: break-all; background: rgba(0,0,0,0.03); color: rgba(0,0,0,0.62); padding: 10px 12px; border-radius: 8px; font-size: 13px; line-height: 1.6; border-left: 2px solid rgba(0,0,0,0.12); max-height: 120px; overflow-y: auto; } #search-display::-webkit-scrollbar { width: 3px; } #search-display::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 3px; } #search-input { width: 100%; padding: 11px 14px; padding-left: 40px; border: 1px solid rgba(0,0,0,0.1); border-radius: 10px; font-size: 13.5px; color: rgba(0,0,0,0.75); background: rgba(255,255,255,0.8); outline: none; box-sizing: border-box; transition: all 0.25s ease; box-shadow: inset 0 1px 2px rgba(0,0,0,0.04); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23889aaa' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: 12px center; background-size: 16px; } #search-input::placeholder { color: rgba(0,0,0,0.25); } #search-input:focus { border-color: rgba(0,0,0,0.2); background-color: #fff; box-shadow: inset 0 1px 2px rgba(0,0,0,0.04), 0 0 0 2px rgba(0,0,0,0.06); } #search-input:not(:placeholder-shown) { border-color: rgba(0,0,0,0.15); background-color: #fff; } .action-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 10px; } .action-button { cursor: pointer; padding: 8px 6px; background: rgba(255,255,255,0.7); border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; font-size: 12.5px; font-weight: 500; color: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; gap: 5px; transition: all 0.18s; } .action-button:hover { background: rgba(255,255,255,0.95); border-color: rgba(0,0,0,0.14); color: rgba(0,0,0,0.72); } .action-button svg { width: 13px; height: 13px; flex-shrink: 0; } #search-result { padding-top: 8px; } .result-item { margin-bottom: 10px; padding: 12px; border-radius: 10px; background: rgba(255,255,255,0.6); border: 1px solid rgba(0,0,0,0.06); } .result-item:last-child { border-bottom: none; } .loading { text-align: center; color: rgba(0,0,0,0.28); padding: 20px 0; font-size: 13px; } .loading::after { content: ''; display: inline-block; width: 16px; height: 16px; border: 2px solid rgba(0,0,0,0.08); border-top-color: rgba(0,0,0,0.35); border-radius: 50%; animation: spin 0.7s linear infinite; margin-left: 8px; vertical-align: middle; } @keyframes spin { to { transform: rotate(360deg); } } .no-result { color: rgba(0,0,0,0.25); text-align: center; padding: 24px 0; font-size: 13px; } .no-result-icon { font-size: 28px; margin-bottom: 8px; display: block; } .options-list { margin-top: 5px; padding-left: 20px; } .option { margin-bottom: 3px; } .fuzzy-match-list { max-height: 380px; overflow-y: auto; } .fuzzy-match-list::-webkit-scrollbar { width: 4px; } .fuzzy-match-list::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 4px; } .fuzzy-match-item { padding: 10px 12px; border-bottom: 1px solid rgba(0,0,0,0.04); cursor: pointer; border-radius: 8px; margin-bottom: 4px; transition: background 0.15s; } .fuzzy-match-item:hover { background: rgba(255,255,255,0.85); } .fuzzy-match-item.selected { background: rgba(255,255,255,0.95); border-left: 3px solid rgba(0,0,0,0.18); } .fuzzy-match-item:last-child { border-bottom: none; } .fuzzy-match-item .fuzzy-match-question { max-height: 80px; overflow-y: auto; } .fuzzy-match-item .fuzzy-match-question::-webkit-scrollbar { width: 3px; } .fuzzy-match-item .fuzzy-match-question::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.1); border-radius: 3px; } .fuzzy-match-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 4px; flex-wrap: wrap; gap: 4px; } .fuzzy-match-question { font-size: 13px; line-height: 1.5; flex: 1; color: rgba(0,0,0,0.65); word-break: break-all; } .fuzzy-match-question mark { background: rgba(0,0,0,0.08); color: rgba(0,0,0,0.65); padding: 0 2px; border-radius: 3px; } .fuzzy-match-score { font-size: 10px; color: rgba(0,0,0,0.32); white-space: nowrap; margin-left: 8px; min-width: 32px; text-align: right; background: rgba(0,0,0,0.04); padding: 2px 5px; border-radius: 4px; } .fuzzy-match-answer { font-size: 12px; color: rgba(50, 120, 70, 0.8); margin-top: 3px; display: flex; align-items: center; gap: 4px; } .fuzzy-match-answer::before { content: '答案'; background: rgba(50,120,70,0.08); color: rgba(50,120,70,0.75); font-size: 10px; padding: 1px 5px; border-radius: 4px; } .answer { font-weight: 500; color: rgba(40, 110, 60, 0.85); margin-top: 8px; font-size: 13.5px; background: rgba(50,120,70,0.06); padding: 8px 12px; border-radius: 8px; border: 1px solid rgba(50,120,70,0.1); } .hint { color: rgba(0,0,0,0.32); font-size: 12px; margin: 5px 0; font-style: italic; } .result-actions { margin-top: 10px; display: flex; gap: 12px; padding-top: 8px; border-top: 1px dashed rgba(0,0,0,0.07); } .result-action { cursor: pointer; color: rgba(0,0,0,0.45); font-size: 12px; font-weight: 500; background: none; border: none; padding: 0; } .result-action:hover { text-decoration: underline; color: rgba(0,0,0,0.65); } #cached-answers { font-size: 11px; color: rgba(0,0,0,0.22); margin-top: 6px; text-align: right; } #db-stats { font-size: 11px; color: rgba(0,0,0,0.22); margin-top: 2px; text-align: right; } /* 悬浮按钮样式 - 极低调 */ .button-container { position: fixed; bottom: 24px; right: 24px; display: flex; flex-direction: column; gap: 10px; z-index: 999998; opacity: 0; transition: opacity 0.25s; } .button-container:hover { opacity: 1; } #activate-button, #settings-button, #pick-button { background: rgba(242, 244, 247, 0.9); color: rgba(0,0,0,0.5); padding: 9px 14px; border-radius: 22px; cursor: pointer; z-index: 999998; font-size: 12px; font-weight: 500; box-shadow: 0 3px 12px rgba(0,0,0,0.1), 0 1px 3px rgba(0,0,0,0.06); transition: all 0.2s; display: flex; align-items: center; gap: 6px; border: 1px solid rgba(0,0,0,0.08); white-space: nowrap; } #activate-button { width: 0; height: 0; } #settings-button { background: rgba(242, 244, 247, 0.9); color: rgba(0,0,0,0.5); } #pick-button { background: rgba(242, 244, 247, 0.9); color: rgba(0,0,0,0.5); } #settings-button svg, #pick-button svg { width: 14px; height: 14px; } #activate-button:hover, #settings-button:hover, #pick-button:hover { background: rgba(255,255,255,0.95); box-shadow: 0 4px 16px rgba(0,0,0,0.12); transform: translateY(-1px); color: rgba(0,0,0,0.68); } /* 自定义模态框样式 */ .modal-overlay { position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; background: rgba(0,0,0,0.25) !important; z-index: 2147483647 !important; display: none !important; justify-content: center !important; align-items: center !important; backdrop-filter: blur(3px); opacity: 0; transition: opacity 0.2s ease; } /* Toast 提示样式 */ .toast-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1000001; display: flex; flex-direction: column; gap: 8px; align-items: center; pointer-events: none; } .toast { background: rgba(50, 55, 62, 0.9); color: rgba(255,255,255,0.88); padding: 10px 20px; border-radius: 6px; font-size: 13px; max-width: 300px; text-align: center; white-space: pre-wrap; word-break: break-word; animation: toastIn 0.25s ease, toastOut 0.2s ease forwards; animation-delay: 0s, 2.3s; border: 1px solid rgba(255,255,255,0.08); } .toast.success { background: rgba(40, 110, 60, 0.92); border-color: rgba(50,120,70,0.3); } .toast.error { background: rgba(150, 45, 45, 0.92); border-color: rgba(200,80,80,0.3); } .toast.info { background: rgba(60, 70, 85, 0.92); border-color: rgba(120,130,150,0.3); } .toast.warning { background: rgba(130, 100, 30, 0.92); border-color: rgba(180,150,60,0.3); } @keyframes toastIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } @keyframes toastOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-10px); } } .modal-container { background: rgba(248, 250, 252, 0.98); border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.15); width: 420px; max-width: 92%; padding: 22px; position: relative; transform: scale(0.94); transition: transform 0.2s ease; z-index: 2147483647; max-height: 80vh; overflow-y: auto; border: 1px solid rgba(0,0,0,0.07); } .modal-overlay.active { opacity: 1; display: flex !important; } .modal-overlay.active .modal-container { transform: scale(1); } .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid rgba(0,0,0,0.06); } .modal-title { font-size: 15px; font-weight: 500; color: rgba(0,0,0,0.7); } .modal-close { cursor: pointer; font-size: 20px; color: rgba(0,0,0,0.3); padding: 2px 6px; border-radius: 4px; transition: all 0.15s; line-height: 1; background: none; border: none; } .modal-close:hover { background: rgba(0,0,0,0.06); color: rgba(0,0,0,0.55); } .modal-message { font-size: 13px; color: rgba(0,0,0,0.52); margin-bottom: 14px; line-height: 1.6; } .modal-input { width: 100%; padding: 9px 12px; border: 1px solid rgba(0,0,0,0.1); border-radius: 8px; font-size: 13px; outline: none; box-sizing: border-box; transition: border-color 0.2s, box-shadow 0.2s; background: rgba(255,255,255,0.8); color: rgba(0,0,0,0.72); } .modal-input:focus { border-color: rgba(0,0,0,0.2); background: #fff; box-shadow: 0 0 0 2px rgba(0,0,0,0.06); } .modal-buttons { display: flex; justify-content: flex-end; gap: 8px; margin-top: 18px; } .modal-button { cursor: pointer; padding: 8px 20px; border-radius: 8px; font-size: 13px; font-weight: 500; border: none; transition: all 0.18s; } .modal-button.cancel { background: rgba(0,0,0,0.04); color: rgba(0,0,0,0.52); } .modal-button.cancel:hover { background: rgba(0,0,0,0.08); } .modal-button.confirm { background: rgba(0,0,0,0.1); color: rgba(0,0,0,0.72); } .modal-button.confirm:hover { background: rgba(0,0,0,0.16); } `); // ============= 界面元素创建 ============= const activateButton = document.createElement('div'); activateButton.id = 'activate-button'; activateButton.textContent = '搜题'; document.body.appendChild(activateButton); // 创建设置按钮 const settingsBtn = document.createElement('div'); settingsBtn.id = 'settings-button'; settingsBtn.textContent = '设置'; // 创建拾取按钮 const pickBtn = document.createElement('div'); pickBtn.id = 'pick-button'; pickBtn.textContent = '拾取'; // 创建按钮容器 const buttonContainer = document.createElement('div'); buttonContainer.className = 'button-container'; buttonContainer.appendChild(settingsBtn); buttonContainer.appendChild(pickBtn); document.body.appendChild(buttonContainer); // ============= 联网搜索开关样式 ============= GM_addStyle(` #net-toggle-wrap { display: flex; align-items: center; gap: 5px; font-size: 12px; color: rgba(0,0,0,0.45); } .net-toggle-switch { position: relative; display: inline-block; width: 32px; height: 18px; cursor: pointer; } .net-toggle-switch input { opacity: 0; width: 0; height: 0; } .net-toggle-slider { position: absolute; inset: 0; background: rgba(0,0,0,0.12); border-radius: 9px; transition: background 0.2s; } .net-toggle-slider::before { content: ''; position: absolute; width: 14px; height: 14px; left: 2px; top: 2px; background: #fff; border-radius: 50%; transition: transform 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); } #net-toggle:checked + .net-toggle-slider { background: #4caf50; } #net-toggle:checked + .net-toggle-slider::before { transform: translateX(14px); } #net-search-btn { background: rgba(76,175,80,0.1) !important; color: #2e7d32 !important; border: 1px solid rgba(76,175,80,0.25) !important; } #net-search-btn:hover { background: rgba(76,175,80,0.18) !important; } `); const searchPanel = document.createElement('div'); searchPanel.id = 'search-panel'; searchPanel.innerHTML = `
辅助工具
联网
透明度: 97%
题库: ${QUESTION_DATABASE.length}题
`; document.body.appendChild(searchPanel); // 创建模态框容器(隐藏) const modalOverlay = document.createElement('div'); modalOverlay.className = 'modal-overlay'; modalOverlay.innerHTML = ` `; document.body.appendChild(modalOverlay); // ============= Toast 提示 ============= const toastContainer = document.createElement('div'); toastContainer.className = 'toast-container'; document.body.appendChild(toastContainer); function showToast(message, type = 'info', duration = 2500) { const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; toastContainer.appendChild(toast); setTimeout(() => toast.remove(), duration + 200); } // ============= 模态框交互逻辑(必须在 DOM 创建后定义) ============= function showModal(options) { return new Promise((resolve) => { const overlay = modalOverlay; const titleEl = overlay.querySelector('#modal-title'); const messageEl = overlay.querySelector('#modal-message'); const inputEl = overlay.querySelector('#modal-input'); const confirmBtn = overlay.querySelector('#modal-confirm'); const cancelBtn = overlay.querySelector('#modal-cancel'); titleEl.textContent = options.title || '提示'; messageEl.textContent = options.message || ''; if (options.input) { inputEl.style.display = 'block'; inputEl.value = options.input.defaultValue ?? ''; inputEl.placeholder = options.input.placeholder || '请输入'; inputEl.type = options.input.type || 'text'; } else { inputEl.style.display = 'none'; } overlay.classList.add('active'); const cleanup = (result) => { overlay.classList.remove('active'); confirmBtn.removeEventListener('click', onConfirm); cancelBtn.removeEventListener('click', onCancel); document.getElementById('modal-close-btn').removeEventListener('click', onCancel); document.removeEventListener('keydown', onKeydown); resolve(result); }; const onConfirm = () => { if (options.input) { cleanup({ confirmed: true, value: inputEl.value }); } else { cleanup({ confirmed: true }); } }; const onCancel = () => { cleanup({ confirmed: false }); }; const onKeydown = (e) => { if (e.key === 'Enter') { onConfirm(); } else if (e.key === 'Escape') { onCancel(); } }; confirmBtn.addEventListener('click', onConfirm); cancelBtn.addEventListener('click', onCancel); document.getElementById('modal-close-btn').addEventListener('click', onCancel); document.addEventListener('keydown', onKeydown); setTimeout(() => { if (options.input) { inputEl.focus(); } else { confirmBtn.focus(); } }, 50); }); } function customAlert(message, title = '提示') { return showModal({ title, message }); } function customConfirm(message, title = '确认') { return showModal({ title, message }); } function customInput(message, defaultValue = '', placeholder = '请输入') { return showModal({ title: '输入', message, input: { type: 'text', placeholder, defaultValue } }).then(result => { if (result?.confirmed) return result.value; else return null; }); } function customPrompt(message, placeholder = '请输入') { return showModal({ title: '验证', message, input: { type: 'password', placeholder } }).then(result => { if (result?.confirmed) return result.value; else return null; }); } async function requestVerification() { const password = await customPrompt('🔐 请输入搜题助手验证密码:'); if (password === CORRECT_PASSWORD) { setVerified(); return true; } else { await customAlert('❌ 密码错误,无法使用搜题助手!', '验证失败'); return false; } } // ============= 面板状态 ============= let isPinned = false; let panelVisible = false; let lastPanelPosition = { left: 100, top: 100 }; const answerCache = {}; let netSearchEnabled = localStorage.getItem('netSearchEnabled') === 'true'; // 初始化联网开关 UI const netToggle = document.getElementById('net-toggle'); if (netToggle) { netToggle.checked = netSearchEnabled; netToggle.addEventListener('change', () => { netSearchEnabled = netToggle.checked; localStorage.setItem('netSearchEnabled', netSearchEnabled); }); } function closePanel() { searchPanel.style.display = 'none'; activateButton.style.display = 'block'; panelVisible = false; isPinned = false; pinButton.classList.remove('pinned'); } function showPanelWithContent(text) { if (!panelVisible) { searchPanel.style.left = `${lastPanelPosition.left}px`; searchPanel.style.top = `${lastPanelPosition.top}px`; searchPanel.style.display = 'block'; panelVisible = true; activateButton.style.display = 'none'; } const searchInput = document.getElementById('search-input'); const searchDisplay = document.getElementById('search-display'); if (searchInput) searchInput.value = text || ''; if (searchDisplay) { searchDisplay.textContent = text || ''; searchDisplay.style.display = text ? 'block' : 'none'; } if (text && text !== "请选中题目文本") { setTimeout(searchAnswer, 300); } } // ============= 提取题目(优先使用悬停/点击记录) ============= function extractCurrentQuestion() { try { if (lastHoveredQuestion) { log('使用悬停记录:', lastHoveredQuestion); return lastHoveredQuestion; } if (lastClickedQuestion) { log('使用点击记录:', lastClickedQuestion); return lastClickedQuestion; } if (customQuestionSelector) { const elem = document.querySelector(customQuestionSelector); if (elem) { const text = getQuestionTextFromElement(elem); if (text) { log('使用自定义选择器获取题目:', text); return text; } } } const titleElement = document.querySelector('.exam-topic-item-title-name'); if (titleElement && titleElement.textContent) { const text = titleElement.textContent.trim(); if (text) return text; } return null; } catch (e) { log('提取题目失败', e); return null; } } function searchAnswer() { const input = document.getElementById('search-input'); const query = input ? input.value.trim() : ''; if (!query) return; document.getElementById('search-result').innerHTML = '
正在搜索答案...
'; if (answerCache[query]) { displayResults(answerCache[query]); return; } setTimeout(() => { const result = findQuestionInDatabase(query); if (result) { answerCache[query] = result; displayResults(result); } else { if (netSearchEnabled) { doNetSearch(query); } else { document.getElementById('search-result').innerHTML = '
本地题库未命中,请开启顶部「联网」开关后重试,或修改关键词
'; } } updateCacheStats(); }, 300); } function doNetSearch(query) { document.getElementById('search-result').innerHTML = '
正在联网搜索...
'; const encoded = encodeURIComponent(query); GM_xmlhttpRequest({ method: 'GET', url: `https://www.baidu.com/s?wd=${encoded}&rn=10`, timeout: 15000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36', 'Accept': 'text/html' }, onload(resp) { if (resp.status === 200) { const pageUrls = extractBaiduResultUrls(resp.responseText); if (pageUrls.length === 0) { document.getElementById('search-result').innerHTML = '
联网搜索未找到结果
'; return; } fetchAndExtractAnswers(pageUrls, query); } else { document.getElementById('search-result').innerHTML = '
联网搜索失败(HTTP ' + resp.status + ')
'; } }, onerror() { document.getElementById('search-result').innerHTML = '
联网搜索请求失败,请检查网络
'; }, ontimeout() { document.getElementById('search-result').innerHTML = '
联网搜索超时
'; } }); } function extractBaiduResultUrls(html) { const urls = []; const seen = new Set(); const div = document.createElement('div'); div.innerHTML = html; div.querySelectorAll('h3.t a, .c-title a, a[href*="www.baidu.com/link"]').forEach(a => { let href = a.getAttribute('href') || ''; const m = href.match(/url=([^&]+)/); if (m) href = decodeURIComponent(m[1]); if (href && href.startsWith('http') && !seen.has(href) && !href.includes('baidu.com') && !href.includes('zhihu.com')) { seen.add(href); urls.push(href); } }); return urls.slice(0, 5); } function fetchAndExtractAnswers(pageUrls, query) { let foundAnswers = []; let pending = pageUrls.length; let hasError = false; if (pending === 0) { document.getElementById('search-result').innerHTML = '
联网搜索未找到答案
'; return; } pageUrls.forEach(url => { GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 12000, headers: { 'User-Agent': 'Mozilla/5.0' }, onload(resp) { if (resp.status === 200) { const extracted = extractExamAnswers(resp.responseText, query); foundAnswers = foundAnswers.concat(extracted); } }, onerror() { hasError = true; }, ontimeout() { hasError = true; }, onloadend() { pending--; if (pending === 0) { showNetSearchResults(query, foundAnswers, hasError && foundAnswers.length === 0); } } }); }); } function extractExamAnswers(html, query) { const results = []; const seen = new Set(); const div = document.createElement('div'); div.innerHTML = html; const text = div.textContent || ''; // 精准模式:同一行内既有题目关键词又有答案 const lines = text.split('\n'); const queryWords = query.replace(/[()()()【】\[\]]/g, '').split(/\s+/).filter(w => w.length >= 2); for (const line of lines) { const clean = line.trim(); if (clean.length < 5 || clean.length > 300) continue; let hasKW = queryWords.filter(w => clean.includes(w)).length >= Math.min(2, queryWords.length); if (!hasKW) continue; const ansMatch = clean.match(/(?:(? 1 && !seen.has(a)) { seen.add(a); results.push({ answer: a, source: '全文搜索', score: 30 }); } if (results.length >= 3) break; } if (results.length >= 3) break; } } results.sort((a, b) => b.score - a.score); return results.slice(0, 5); } const PROXY_KEY = atob('bWp5YXFr'); const DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions'; const DEEPSEEK_MODEL = 'deepseek-chat'; const USE_CORS_PROXY = false; const CORS_PROXY = 'https://corsproxy.io/?'; // Cloudflare Worker 代理地址,部署 Worker 后替换这里 const WORKER_PROXY_URL = 'https://deepseek.1633252822.workers.dev'; const USE_WORKER_PROXY = true; function doDeepSeekSearch(query) { document.getElementById('search-result').innerHTML = '
正在询问 DeepSeek AI...
'; let questionText = query; let optionsText = ''; let questionType = ''; // 优先:从已捕获的 DOM 元素提取题目和选项 const capturedElem = lastClickedQuestionElem || lastHoveredQuestionElem; if (capturedElem) { const extracted = getFullQuestionTextFromElem(capturedElem); // 校验:只有元素里的题目和当前查询相关时才使用,防止用旧题目的选项 const elemQuestion = extracted.question || ''; const elemSimilarity = normalizeText(elemQuestion) === normalizeText(query) ? 1 : calculateSimilarity(normalizeText(elemQuestion), normalizeText(query)); console.log('[AI答题] DOM元素提取结果:', { elemQuestion: extracted.question, options: extracted.options, questionType: extracted.questionType, similarity: elemSimilarity, elemTag: capturedElem.tagName, elemClass: capturedElem.className, }); if (elemSimilarity >= 0.8 && extracted.question) { questionText = extracted.question; } if (extracted.options.length > 0 && elemSimilarity >= 0.8) { optionsText = '\n选项:\n' + extracted.options.join('\n'); } if (extracted.questionType) { questionType = extracted.questionType; } // 用完后清空,防止下次搜索不同题目时还用旧元素 lastClickedQuestionElem = null; lastHoveredQuestionElem = null; } // DOM 提取不到选项时,尝试本地题库 if (!optionsText) { let matchedItem = null; if (answerCache[query]) { matchedItem = answerCache[query]; } else { const cleanQuery = normalizeText(query); for (const item of QUESTION_DATABASE) { const cleanQuestion = normalizeText(item.question); const similarity = calculateSimilarity(cleanQuery, cleanQuestion); if (similarity >= 0.6) { matchedItem = item; break; } } } if (matchedItem && matchedItem.options && matchedItem.options.length > 0) { optionsText = '\n选项:\n' + matchedItem.options.map((o, i) => `${String.fromCharCode(65 + i)}. ${o}` ).join('\n'); } } // 兜底:从搜索文本本身解析选项(适用于从外部粘贴带选项的题目文本) if (!optionsText) { const seen = new Set(); const lines = query.split('\n').map(l => l.trim()).filter(l => l.length > 0); const parsedOptions = []; let lastQuestionPart = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // 匹配 A. B. A、 B、 等选项格式 const optMatch = line.match(/^([A-Za-z])[..、::]\s*(.+)/); if (optMatch) { const letter = optMatch[1].toUpperCase(); const content = optMatch[2].trim(); const key = `${letter}|${content}`; if (!seen.has(key) && content.length > 0 && content.length < 200) { seen.add(key); parsedOptions.push(`${letter}. ${content}`); } } else { lastQuestionPart = line; } } if (parsedOptions.length >= 2) { questionText = lastQuestionPart || questionText; optionsText = '\n选项:\n' + parsedOptions.join('\n'); } } const prompt = `你是一个专业的考试答题助手。请仔细阅读以下题目和选项,给出最准确的答案。 ${questionText}${optionsText} 回答要求: - 仔细分析每个选项,选出正确答案 - 多选题必须选出所有正确选项,连写字母(如 ABC) - 判断题只回答"对"或"错" - 简答/填空题给出最准确答案 - 只输出一个答案,不要解释 请用以下格式回答: 你的答案`; const requestUrl = USE_WORKER_PROXY ? WORKER_PROXY_URL : USE_CORS_PROXY ? `${CORS_PROXY}${encodeURIComponent(DEEPSEEK_API_URL)}` : DEEPSEEK_API_URL; GM_xmlhttpRequest({ method: 'POST', url: requestUrl, timeout: 30000, headers: { 'Content-Type': 'application/json', 'X-Proxy-Key': PROXY_KEY }, data: JSON.stringify({ model: DEEPSEEK_MODEL, messages: [ { role: 'system', content: '你是一个专业的考试答题助手,只回答最准确的答案。' }, { role: 'user', content: prompt } ], max_tokens: 200, temperature: 1 }), onload(resp) { if (resp.status === 200) { try { const data = JSON.parse(resp.responseText); const msg = data.choices && data.choices[0] && data.choices[0].message; const reasoning = msg.reasoning || ''; let rawContent = msg.content || ''; // 提取 标签中的答案(R1 模型格式) const answerTagMatch = rawContent.match(/([\s\S]*?)<\/answer>/i); const cleanAnswer = answerTagMatch ? answerTagMatch[1].trim() : rawContent.trim(); // 从思考过程中提取关键词作为参考 const shortReasoning = reasoning.length > 200 ? reasoning.substring(0, 200).replace(/\n/g, ' ') + '...' : reasoning.replace(/\n/g, ' '); if (cleanAnswer) { const result = { question: questionText, answer: cleanAnswer, explanation: shortReasoning || '由 DeepSeek R1 分析后给出' }; answerCache[questionText] = result; displayResults(result); showToast('DeepSeek AI 答题成功', 'success'); } else { document.getElementById('search-result').innerHTML = '
DeepSeek AI 未返回有效答案
'; } } catch (e) { document.getElementById('search-result').innerHTML = '
DeepSeek AI 返回解析失败
'; } } else { let errMsg = 'DeepSeek AI 请求失败(' + resp.status + ')'; try { const errData = JSON.parse(resp.responseText); if (errData.error && errData.error.message) errMsg += ':' + errData.error.message; } catch (_) {} document.getElementById('search-result').innerHTML = `
${errMsg}
`; } }, onerror() { document.getElementById('search-result').innerHTML = '
DeepSeek AI 请求失败,请检查网络
'; }, ontimeout() { document.getElementById('search-result').innerHTML = '
DeepSeek AI 请求超时
'; } }); } function showNetSearchResults(query, answers, isError) { if (answers.length === 0) { doDeepSeekSearch(query); return; } const result = { question: query, answer: answers[0].answer, explanation: answers.slice(0, 3).map(a => `[${a.source}] ${a.answer}`).join('\n') }; answerCache[query] = result; displayResults(result); showToast('联网搜索成功', 'success'); } function displayResults(result, isFuzzy) { const container = document.getElementById('search-result'); if (!result) return; if (isFuzzy && Array.isArray(result)) { if (result.length === 0) { container.innerHTML = '
模糊搜索未找到匹配结果
'; return; } const query = document.getElementById('search-input')?.value || ''; let html = `
找到 ${result.length} 个匹配结果(点击条目查看详情)
`; html += '
'; result.forEach((entry, idx) => { const item = entry.item; const score = Math.round(entry.score * 100); const highlighted = highlightMatch(item.question, query); const scoreColor = score >= 80 ? '#2c7a4d' : score >= 60 ? '#b7791f' : '#888'; html += `
${highlighted}
${score}%
答案: ${escapeHtml(item.answer)}
`; }); html += '
'; container.innerHTML = html; container.querySelectorAll('.fuzzy-match-item').forEach(item => { item.addEventListener('click', () => { container.querySelectorAll('.fuzzy-match-item').forEach(i => i.classList.remove('selected')); item.classList.add('selected'); const idx = parseInt(item.getAttribute('data-index')); const entry = result[idx]; if (entry) displaySingleResult(entry.item); }); }); return; } displaySingleResult(result, container); } function displaySingleResult(result, container) { if (!container) container = document.getElementById('search-result'); let html = '
'; html += `
题目: ${escapeHtml(result.question)}
`; if (result.options && result.options.length) { html += '
'; result.options.forEach(opt => { const isAnswer = opt.includes(result.answer) || opt.startsWith(result.answer + '.'); html += `
${escapeHtml(opt)}
`; }); html += '
'; } html += `
答案: ${escapeHtml(result.answer)}
`; if (result.explanation) { html += `
解析: ${escapeHtml(result.explanation)}
`; } html += `
复制答案 复制全部
`; container.innerHTML = html; container.querySelector('.copy-answer')?.addEventListener('click', () => { GM_setClipboard(result.answer); showToast('答案已复制', 'success'); }); container.querySelector('.copy-all')?.addEventListener('click', () => { let text = `题目: ${result.question}\n`; if (result.options?.length) text += result.options.join('\n') + '\n'; text += `答案: ${result.answer}`; if (result.explanation) text += `\n解析: ${result.explanation}`; GM_setClipboard(text); showToast('已复制题目和答案', 'success'); }); } function escapeHtml(str) { return str.replace(/[&<>]/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }); } // 自动填答案 function autoFillAnswer(answer) { if (!answer) return false; log('自动填写答案:', answer); let cleanAnswer = answer.trim().replace(/^【正确答案】/, '').replace(/^答案[::]/, '').trim(); const trueValues = ['正确', '对', '是', '√', 'A', 'A.']; const falseValues = ['错误', '错', '否', '×', 'B', 'B.']; const radios = document.querySelectorAll('input[type="radio"]'); const checkboxes = document.querySelectorAll('input[type="checkbox"]'); if (radios.length > 0) { if (trueValues.some(v => cleanAnswer.includes(v)) && falseValues.some(v => cleanAnswer.includes(v))) { for (let radio of radios) { const label = getLabelForInput(radio); if (label && label.textContent.includes(cleanAnswer)) { radio.click(); return true; } } } else { for (let radio of radios) { const label = getLabelForInput(radio); if (!label) continue; const labelText = label.textContent.trim(); if (labelText.startsWith(cleanAnswer + '.') || labelText === cleanAnswer || (cleanAnswer.length === 1 && 'ABCD'.includes(cleanAnswer.toUpperCase()) && labelText.toUpperCase().startsWith(cleanAnswer.toUpperCase()))) { radio.click(); return true; } if (labelText.includes(cleanAnswer)) { radio.click(); return true; } } } } if (checkboxes.length > 0) { const parts = cleanAnswer.split(/[,,]/).map(p => p.trim()); for (let part of parts) { for (let checkbox of checkboxes) { const label = getLabelForInput(checkbox); if (!label) continue; const labelText = label.textContent.trim(); if (labelText.startsWith(part + '.') || labelText === part || (part.length === 1 && 'ABCD'.includes(part.toUpperCase()) && labelText.toUpperCase().startsWith(part.toUpperCase()))) { checkbox.click(); } else if (labelText.includes(part)) { checkbox.click(); } } } return true; } const textInputs = document.querySelectorAll('input[type="text"], textarea'); if (textInputs.length) { textInputs[0].value = cleanAnswer; textInputs[0].dispatchEvent(new Event('input', { bubbles: true })); return true; } return false; } function getLabelForInput(input) { if (input.labels && input.labels.length) return input.labels[0]; const id = input.id; if (id) { const label = document.querySelector(`label[for="${id}"]`); if (label) return label; } let parent = input.parentElement; while (parent) { if (parent.tagName === 'LABEL') return parent; parent = parent.parentElement; } return null; } function getCurrentAnswer() { const answerDiv = document.querySelector('#search-result .answer'); if (answerDiv) { let text = answerDiv.textContent; const match = text.match(/答案[::]\s*(.+)/); return match ? match[1].trim() : text.trim(); } const fuzzyAnswerDiv = document.querySelector('#search-result .fuzzy-match-item.selected .fuzzy-match-answer'); if (fuzzyAnswerDiv) { let text = fuzzyAnswerDiv.textContent; const match = text.match(/答案[::]\s*(.+)/); return match ? match[1].trim() : text.trim(); } return null; } function updateCacheStats() { const count = Object.keys(answerCache).length; const elem = document.getElementById('cached-answers'); if (count > 0) elem.textContent = `已缓存 ${count} 个答案`; else elem.textContent = ''; } // ============= 事件监听 ============= const pinButton = document.getElementById('pin-button'); pinButton.addEventListener('click', () => { isPinned = !isPinned; pinButton.classList.toggle('pinned', isPinned); pinButton.title = isPinned ? '取消置顶' : '置顶'; }); document.getElementById('search-close').addEventListener('click', closePanel); document.addEventListener('mousedown', (e) => { if (!isPinned && panelVisible && !searchPanel.contains(e.target) && !activateButton.contains(e.target) && !settingsBtn.contains(e.target) && !pickBtn.contains(e.target)) { closePanel(); } }); // 快捷键 let qPressCount = 0; let qTimer = null; const Q_THRESHOLD = 3; const Q_TIMEOUT = 500; document.addEventListener('keydown', (e) => { // ESC 关闭面板 if (!isPinned && e.key === 'Escape' && panelVisible) { closePanel(); e.preventDefault(); return; } // q 键处理:未验证需连按 3 次,已验证直接搜索 if (e.key === 'q' && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { const activeEl = document.activeElement; const isInputField = activeEl && (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl.isContentEditable || activeEl.getAttribute('contenteditable') === 'true'); if (!isInputField) { e.preventDefault(); if (isTargetSite() && !isVerified()) { // 未验证:累加计数 qPressCount++; if (qTimer) clearTimeout(qTimer); qTimer = setTimeout(() => { qPressCount = 0; qTimer = null; }, Q_TIMEOUT); if (qPressCount >= Q_THRESHOLD) { // 达到阈值,弹出验证框 qPressCount = 0; if (qTimer) clearTimeout(qTimer); qTimer = null; requestVerification().then(verified => { if (verified) { const question = extractCurrentQuestion(); if (question) showPanelWithContent(question); else showPanelWithContent("请选中题目文本"); } }); } // 未达到阈值,什么都不做 } else { // 已验证 或 非目标网站:单次 q 直接搜索 const question = extractCurrentQuestion(); if (question) showPanelWithContent(question); else showPanelWithContent("请选中题目文本"); } } } // Alt+A 快速搜索(保持原功能) if (e.altKey && e.key === 'a' && panelVisible) { document.getElementById('search-btn').click(); e.preventDefault(); } }); activateButton.addEventListener('click', () => { const question = extractCurrentQuestion(); if (question) { showPanelWithContent(question); } else { showPanelWithContent("请选中题目文本"); } }); settingsBtn.addEventListener('click', async () => { const newSelector = await customInput( '请输入题目文本的CSS选择器\n(例如 .question-title)\n\n当前选择器:' + (customQuestionSelector || '未设置'), customQuestionSelector, '例如 .exam-topic-item' ); if (newSelector !== null) { customQuestionSelector = newSelector.trim(); localStorage.setItem('customQuestionSelector', customQuestionSelector); showToast('选择器已保存并立即生效!无需刷新页面。', 'success'); lastHoveredQuestion = null; lastClickedQuestion = null; lastHoveredQuestionElem = null; lastClickedQuestionElem = null; } }); // 拾取按钮(修复:不会捕获按钮自身) pickBtn.addEventListener('click', (e) => { e.stopPropagation(); if (pickMode) { exitPickMode(); } else { enterPickMode(); } }); document.getElementById('copy-btn').addEventListener('click', () => { const text = document.getElementById('search-input')?.value; if (text) { GM_setClipboard(text); const btn = document.getElementById('copy-btn'); const orig = btn.textContent; btn.textContent = '已复制!'; setTimeout(() => btn.textContent = orig, 1500); } }); document.getElementById('search-btn').addEventListener('click', searchAnswer); document.getElementById('net-search-btn').addEventListener('click', () => { const query = document.getElementById('search-input')?.value.trim(); if (!query) return; doNetSearch(query); }); document.getElementById('ai-search-btn').addEventListener('click', () => { const query = document.getElementById('search-input')?.value.trim(); if (!query) return; // 实时从页面重新抓取当前题目 DOM,不用旧记录 const currentElem = findCurrentQuestionElem(query); if (currentElem) { lastClickedQuestionElem = currentElem; lastHoveredQuestionElem = null; console.log('[AI答题] 找到当前题目DOM元素:', currentElem.tagName, currentElem.className); } else { console.log('[AI答题] 未找到匹配的题目DOM元素,当前query:', query); } doDeepSeekSearch(query); }); document.getElementById('fuzzy-btn').addEventListener('click', () => { const query = document.getElementById('search-input')?.value.trim(); if (!query) return; document.getElementById('search-result').innerHTML = '
正在模糊搜索...
'; setTimeout(() => { const results = fuzzySearch(query); displayResults(results, true); }, 50); }); // Allow Enter key in search input to trigger fuzzy search document.getElementById('search-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') { const query = e.target.value.trim(); if (query) { document.getElementById('search-result').innerHTML = '
正在模糊搜索...
'; setTimeout(() => { const results = fuzzySearch(query); displayResults(results, true); }, 50); } } }); document.getElementById('auto-answer').addEventListener('click', () => { const answer = getCurrentAnswer(); if (answer) { autoFillAnswer(answer); } else { showToast('请先搜索答案', 'info'); } }); // ============= 划词选中自动搜索 ============= let selectionTimer = null; document.addEventListener('mouseup', (e) => { if (searchPanel.contains(e.target) || activateButton.contains(e.target) || settingsBtn.contains(e.target) || pickBtn.contains(e.target) || modalOverlay?.contains(e.target)) return; if (selectionTimer) clearTimeout(selectionTimer); selectionTimer = setTimeout(() => { const selection = window.getSelection(); const selectedText = selection.toString().trim(); if (selectedText && selectedText.length >= 3) { let rect = null; if (selection.rangeCount > 0) { rect = selection.getRangeAt(0).getBoundingClientRect(); } const pos = rect ? { left: rect.left + window.scrollX, top: rect.bottom + window.scrollY + 5 } : { left: window.innerWidth / 2 - 150, top: window.innerHeight / 3 }; if (!panelVisible) { searchPanel.style.left = `${pos.left}px`; searchPanel.style.top = `${pos.top}px`; } showPanelWithContent(selectedText); } }, 50); }); // ============= 题目捕获辅助函数(悬停/点击复用) ============= function captureQuestion(targetElem, setResult, logPrefix, inputSelector, setElem) { if (customQuestionSelector) { let parent = targetElem; let depth = 0; while (parent && parent !== document.body && depth < 10) { if (parent.matches && parent.matches(customQuestionSelector)) { const question = getQuestionTextFromElement(parent); if (question) { setResult(question); if (setElem) setElem(parent); log(logPrefix + '(自定义选择器):', question); return true; } } parent = parent.parentElement; depth++; } } const topicItem = targetElem.closest('.exam-topic-item'); if (topicItem) { const titleElem = topicItem.querySelector('.exam-topic-item-title-name'); if (titleElem && titleElem.textContent) { const question = titleElem.textContent.trim(); setResult(question); if (setElem) setElem(topicItem); log(logPrefix + '(类名):', question); return true; } } let target = targetElem; let depth = 0; while (target && target !== document.body && depth < 10) { if (target.nodeType === Node.ELEMENT_NODE) { const text = target.textContent ? target.textContent.trim() : ''; const hasNoInput = inputSelector ? !target.querySelector(inputSelector) : !target.querySelector('input[type="radio"], input[type="checkbox"]'); if (/^\d+[\.、]/.test(text) && text.length > 10 && text.length < 500 && hasNoInput) { const pure = extractPureQuestion(text); if (pure) { setResult(pure); if (setElem) setElem(target); log(logPrefix + '(后备):', pure); return true; } } } target = target.parentElement; depth++; } return false; } // ============= 鼠标悬停捕获题目 ============= document.addEventListener('mouseenter', (e) => { if (pickMode) return; const targetElem = e.target.nodeType === Node.ELEMENT_NODE ? e.target : e.target.parentElement; if (targetElem) captureQuestion(targetElem, q => { lastHoveredQuestion = q; }, '悬停捕获题目', null, e => { lastHoveredQuestionElem = e; }); }, true); // ============= 点击捕获题目 ============= document.addEventListener('click', (e) => { if (pickMode) return; const targetElem = e.target.nodeType === Node.ELEMENT_NODE ? e.target : e.target.parentElement; if (targetElem) captureQuestion(targetElem, q => { lastClickedQuestion = q; }, '点击捕获题目', 'input', e => { lastClickedQuestionElem = e; }); }); // ============= 面板拖拽 ============= let dragActive = false; let dragStartX = 0, dragStartY = 0; let panelStartLeft = 0, panelStartTop = 0; function onDragStart(e) { if (e.target.closest('#search-result, .action-buttons, #opacity-control')) return; if (e.target === searchPanel || e.target.closest('#search-header')) { dragActive = true; dragStartX = e.clientX; dragStartY = e.clientY; const style = window.getComputedStyle(searchPanel); panelStartLeft = parseFloat(style.left); panelStartTop = parseFloat(style.top); e.preventDefault(); } } function onDragMove(e) { if (!dragActive) return; e.preventDefault(); const dx = e.clientX - dragStartX; const dy = e.clientY - dragStartY; let newLeft = panelStartLeft + dx; let newTop = panelStartTop + dy; newLeft = Math.max(0, Math.min(window.innerWidth - searchPanel.offsetWidth, newLeft)); newTop = Math.max(0, Math.min(window.innerHeight - searchPanel.offsetHeight, newTop)); searchPanel.style.left = `${newLeft}px`; searchPanel.style.top = `${newTop}px`; } function onDragEnd() { if (dragActive) { lastPanelPosition.left = parseFloat(searchPanel.style.left); lastPanelPosition.top = parseFloat(searchPanel.style.top); dragActive = false; } } searchPanel.addEventListener('mousedown', onDragStart); document.addEventListener('mousemove', onDragMove); document.addEventListener('mouseup', onDragEnd); // 透明度控制(持久化) const slider = document.getElementById('opacity-slider'); const opacityVal = document.getElementById('opacity-value'); const savedOpacity = localStorage.getItem('panelOpacity'); if (savedOpacity) { const val = parseInt(savedOpacity); slider.value = val; searchPanel.style.opacity = val / 100; opacityVal.textContent = `${val}%`; } slider.addEventListener('input', (e) => { const val = e.target.value; searchPanel.style.opacity = val / 100; opacityVal.textContent = `${val}%`; localStorage.setItem('panelOpacity', val); }); log('初始化完成,题库数量:', QUESTION_DATABASE.length); })();