// ==UserScript== // @name 智能考试助手(拾取选择器版·已修复) // @namespace http://tampermonkey.net/ // @version 2.4 // @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 = `