您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
支持Claude、DeepSeek、Google Gemini等网站的公式复制,包括点击复制、选择复制和按钮复制,格式化为$和$$包裹的LaTeX
// ==UserScript== // @name AI网站公式复制Latex // @namespace http://tampermonkey.net/ // @version 0.9 // @description 支持Claude、DeepSeek、Google Gemini等网站的公式复制,包括点击复制、选择复制和按钮复制,格式化为$和$$包裹的LaTeX // @license MIT // @author fanxing // @match *://demo.fuclaude.oaifree.com/* // @match *://claude.ai/* // @match *://*.zhihu.com/* // @match *://*.wikipedia.org/* // @match *://*.chatgpt.com/* // @match *://*.x.liaox.ai/* // @match *://*.moonshot.cn/* // @match *://*.stackexchange.com/* // @match *://*.oi-wiki.org/* // @match *://*.luogu.com/* // @match *://*.doubao.com/* // @match *://*.deepseek.com/* // @match *://chat.deepseek.com/* // @match *://aistudio.google.com/* // @match *://gemini.google.com/* // @grant none // ==/UserScript== (function() { 'use strict'; console.log('公式复制格式调整脚本已加载'); console.log('当前网站:', window.location.href); console.log('浏览器剪贴板API支持:', { clipboard: !!navigator.clipboard, writeText: !!(navigator.clipboard && navigator.clipboard.writeText), write: !!(navigator.clipboard && navigator.clipboard.write), ClipboardItem: !!window.ClipboardItem }); // 安全检查:确保脚本只加载一次 if (window.formulaScriptLoaded) { console.log('脚本已经加载过,跳过重复加载'); return; } window.formulaScriptLoaded = true; // 创建样式 const style = document.createElement('style'); style.textContent = ` .formula-toast { position: fixed; bottom: 20px; right: 20px; background-color: #333; color: white; padding: 12px 20px; border-radius: 4px; z-index: 10000; font-family: Arial, sans-serif; font-size: 14px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); transition: opacity 0.3s, transform 0.3s; opacity: 0; transform: translateY(20px); } .formula-toast.show { opacity: 1; transform: translateY(0); } .formula-toast.success { background-color: #4caf50; } .formula-toast.error { background-color: #f44336; } .formula-toast.info { background-color: #2196F3; } /* LaTeX提示框样式 */ .latex-tooltip { position: fixed; background-color: #333; color: #fff; padding: 8px 12px; border-radius: 4px; font-size: 12px; z-index: 10001; display: none; opacity: 0; transition: opacity 0.2s; max-width: 350px; word-break: break-all; white-space: pre-wrap; box-shadow: 0 2px 8px rgba(0,0,0,0.4); font-family: monospace; pointer-events: none; } /* 复制成功提示 */ .latex-copy-success { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: rgba(0, 0, 0, 0.7); color: white; padding: 10px 20px; border-radius: 4px; font-size: 14px; z-index: 10002; transition: opacity 0.2s; opacity: 1; } /* 高亮样式 */ .formula-hover { cursor: pointer !important; box-shadow: 0 0 0 1px #007bff !important; background-color: rgba(0, 123, 255, 0.1) !important; } `; document.head.appendChild(style); // 创建提示框元素 const tooltip = document.createElement('div'); tooltip.classList.add('latex-tooltip'); document.body.appendChild(tooltip); // 声明全局变量 let tooltipTimeout; let activeFormulaElement = null; // Gemini专用:KaTeX Hook系统 const allKatexGemini = {}; let isGeminiKatexHooked = false; // Gemini专用:Hook KaTeX render方法 function hookKatexRender(katexObj) { if (!katexObj || typeof katexObj.render !== 'function') { console.warn('katex.render not found, skipping hook'); return false; } const originalRender = katexObj.render; katexObj.render = new Proxy(originalRender, { apply: function(target, thisArg, args) { let result = target.apply(thisArg, args); if (args.length >= 2) { const latexStr = args[0]; const element = args[1]; const katexHtml = element.querySelector('.katex-html'); if (element instanceof Element && katexHtml !== null) { allKatexGemini[katexHtml.outerHTML] = latexStr; console.log('Gemini KaTeX记录:', latexStr); } } return result; } }); console.log('Successfully hooked katex.render for Gemini'); return true; } // Gemini专用:设置KaTeX Hook function setupGeminiKatexHook() { if (isGeminiKatexHooked) return; // 1. 检查现有katex if (window.katex) { isGeminiKatexHooked = hookKatexRender(window.katex); return; } // 2. 监听katex赋值 let originalKatex = window.katex; Object.defineProperty(window, 'katex', { set: function(newKatex) { console.log('Detected katex assignment for Gemini, hooking render...'); originalKatex = newKatex; if (!isGeminiKatexHooked) { isGeminiKatexHooked = hookKatexRender(originalKatex); } return originalKatex; }, get: function() { return originalKatex; }, configurable: true }); } // Gemini专用:处理选择复制时的KaTeX替换 function katexReplaceWithTexGemini(fragment) { const katexHtml = fragment.querySelectorAll('.katex-html'); for (let i = 0; i < katexHtml.length; i++) { const element = katexHtml[i]; const texSource = document.createElement('annotation'); if (element.outerHTML && allKatexGemini[element.outerHTML]) { const latexStr = allKatexGemini[element.outerHTML]; // 判断是否为显示模式(块级公式) const isDisplayMode = element.closest('.katex-display') || element.closest('.math-block'); if (isDisplayMode) { texSource.textContent = `\n$$\n${latexStr}\n$$\n`; } else { texSource.textContent = `$${latexStr}$`; } if (element.replaceWith) { element.replaceWith(texSource); } else if (element.parentNode) { element.parentNode.replaceChild(texSource, element); } } } return fragment; } // Gemini专用:查找包含节点的最近KaTeX元素 function closestKatex(node) { const element = (node instanceof Element ? node : node.parentElement); return element && element.closest('.katex'); } // 显示Toast提示函数 function showToast(message, type = 'success', duration = 3000) { // 移除现有的toast,避免重叠 const existingToast = document.querySelector('.formula-toast'); if (existingToast) { existingToast.remove(); } // 创建新的toast const toast = document.createElement('div'); toast.className = `formula-toast ${type}`; toast.textContent = message; document.body.appendChild(toast); // 显示toast setTimeout(() => { toast.classList.add('show'); }, 10); // 设置自动消失 setTimeout(() => { toast.classList.remove('show'); setTimeout(() => { toast.remove(); }, 300); }, duration); } // 显示复制成功提示 function showCopySuccessTooltip() { const copyTooltip = document.createElement("div"); copyTooltip.className = "latex-copy-success"; copyTooltip.innerText = "已复制LaTeX公式"; document.body.appendChild(copyTooltip); setTimeout(() => { copyTooltip.style.opacity = "0"; setTimeout(() => { document.body.removeChild(copyTooltip); }, 200); }, 1000); } // 设置剪贴板为纯文本 async function setClipboardToPlainText(text) { console.log('开始设置剪贴板,文本长度:', text.length); // 方法1:使用ClipboardItem(更可靠的纯文本格式) try { if (navigator.clipboard && window.ClipboardItem) { const blob = new Blob([text], { type: 'text/plain' }); const data = new ClipboardItem({ 'text/plain': blob }); await navigator.clipboard.write([data]); console.log('已成功将内容设置为纯文本格式到剪贴板(方法1)'); return true; } } catch (err) { console.error('ClipboardItem方法失败:', err); } // 方法2:使用writeText(备用方法) try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); console.log('已成功使用writeText设置剪贴板(方法2)'); return true; } } catch (err) { console.error('writeText方法失败:', err); } // 方法3:使用传统的execCommand(最后备用) try { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); const successful = document.execCommand('copy'); document.body.removeChild(textArea); if (successful) { console.log('已成功使用execCommand设置剪贴板(方法3)'); return true; } } catch (err) { console.error('execCommand方法失败:', err); } console.error('所有剪贴板方法都失败了'); return false; } // 隐藏提示框的函数 function hideTooltip() { tooltip.style.display = 'none'; tooltip.style.opacity = '0'; if (activeFormulaElement) { activeFormulaElement.classList.remove('formula-hover'); activeFormulaElement = null; } } // 全局点击和滚动事件强制隐藏提示框 document.addEventListener('click', function(e) { // 检查点击是否在公式上,如果不是,隐藏提示框 if (activeFormulaElement && !activeFormulaElement.contains(e.target)) { hideTooltip(); } }); document.addEventListener('scroll', hideTooltip); window.addEventListener('resize', hideTooltip); // 获取对象和公式方法 function getTarget(url) { let target = { elementSelector: '', getLatexString: null, isDisplayMode: null } // 检查元素是否是公式块 function isDisplayModeFormula(element) { // Claude if (element.classList.contains('math-display') || element.closest('.math-display') !== null) { return true; } // KaTeX相关网站 if (element.classList.contains('katex-display') || element.closest('.katex-display') !== null) { return true; } // DeepSeek if (element.closest('.ds-markdown-math') !== null) { return true; } // Google AI Studio - 检查ms-katex元素是否为块级公式 if (element.tagName === 'MS-KATEX' && !element.classList.contains('inline')) { return true; } if (element.closest('ms-katex') && !element.closest('ms-katex').classList.contains('inline')) { return true; } return false; } // 格式化latex function formatLatex(input, isDisplayMode) { if (!input) return null; // 清理可能的多余字符 input = input.trim(); while (input.endsWith(' ') || input.endsWith('\\')) { input = input.slice(0, -1).trim(); } // 如果输入已经有$或$$包裹,先去除 if (input.startsWith('$') && input.endsWith('$')) { // 判断是否是$$公式块 if (input.startsWith('$$') && input.endsWith('$$')) { input = input.slice(2, -2).trim(); } else { input = input.slice(1, -1).trim(); } } // 额外剥离 \( ... \) 与 \[ ... \] 定界,兼容豆包等站点 if ((input.startsWith('\\(') && input.endsWith('\\)')) || (input.startsWith('\\[') && input.endsWith('\\]'))) { // \( ... \) 或 \[ ... \] input = input.slice(2, -2).trim(); } // 根据显示模式添加适当的分隔符 if (isDisplayMode) { return '\n$$\n' + input + '\n$$\n'; } else { return '$' + input + '$'; } } // Claude.ai if (url.includes('claude.ai') || url.includes('fuclaude.oaifree.com')) { target.elementSelector = 'span.katex, span.math-inline, span.math-display, div.math-display'; target.getLatexString = (element) => { const annotation = element.querySelector('annotation[encoding="application/x-tex"]'); const isDisplay = isDisplayModeFormula(element); return annotation ? formatLatex(annotation.textContent, isDisplay) : null; }; target.isDisplayMode = isDisplayModeFormula; return target; } // DeepSeek else if (url.includes('deepseek.com')) { target.elementSelector = 'span.katex'; target.getLatexString = (element) => { const annotation = element.querySelector('annotation[encoding="application/x-tex"]'); // 检查是否是公式块 const isDisplay = isDisplayModeFormula(element); return annotation ? formatLatex(annotation.textContent, isDisplay) : null; }; target.isDisplayMode = isDisplayModeFormula; return target; } // Google Gemini else if (url.includes('gemini.google.com')) { target.elementSelector = 'span.katex, span.katex-html, .katex-html, span.math-inline, div.math-block span.katex, span.math-display, div.math-block, .math-inline, .math-display'; target.getLatexString = (element) => { // 使用Gemini的hook数据 const katexHtml = element.classList.contains('katex-html') ? element : element.querySelector('.katex-html'); if (katexHtml && allKatexGemini[katexHtml.outerHTML]) { const latexStr = allKatexGemini[katexHtml.outerHTML]; const isDisplay = isDisplayModeFormula(element); return formatLatex(latexStr, isDisplay); } return null; }; target.isDisplayMode = isDisplayModeFormula; return target; } // Google AI Studio else if (url.includes('aistudio.google.com')) { target.elementSelector = 'ms-katex, span.katex, span.math-inline, span.math-display, div.math-display'; target.getLatexString = (element) => { const annotation = element.querySelector('annotation[encoding="application/x-tex"]'); const isDisplay = isDisplayModeFormula(element); return annotation ? formatLatex(annotation.textContent, isDisplay) : null; }; target.isDisplayMode = isDisplayModeFormula; return target; } // 豆包 else if (url.includes('doubao.com')) { target.elementSelector = 'span.container-rkuXQi, span.katex, span.math-inline, span.math-display'; target.getLatexString = (element) => { // 优先从最近的容器读取 data-custom-copy-text(兼容事件绑定在 .katex 等子节点的情况) const container = element.closest('.container-rkuXQi') || element; const customCopyText = container.getAttribute('data-custom-copy-text') || element.getAttribute('data-custom-copy-text'); if (customCopyText) { // 显示模式:优先依据是否存在 .katex-display,其次依据容器/元素的行内/块级标记 const isDisplay = !!(container.querySelector('.katex-display') || element.closest('.katex-display')) || container.classList.contains('math-display') || (!container.classList.contains('math-inline') && !element.classList.contains('math-inline')); return formatLatex(customCopyText, isDisplay); } // 回退到标准annotation方法 const annotation = element.querySelector('annotation[encoding="application/x-tex"]'); const isDisplay = isDisplayModeFormula(element); return annotation ? formatLatex(annotation.textContent, isDisplay) : null; }; target.isDisplayMode = (element) => !!(element.querySelector && element.querySelector('.katex-display')) || !!element.closest('.katex-display') || element.classList.contains('math-display') || !element.classList.contains('math-inline'); return target; } // 知乎 else if (url.includes('zhihu.com')) { target.elementSelector = 'span.ztext-math'; target.getLatexString = (element) => { const isDisplay = element.classList.contains('ztext-math-block'); return formatLatex(element.getAttribute('data-tex'), isDisplay); }; target.isDisplayMode = (element) => element.classList.contains('ztext-math-block'); return target; } // 默认KaTeX检测 target.elementSelector = 'span.katex, span.math'; target.getLatexString = (element) => { const annotation = element.querySelector('annotation[encoding="application/x-tex"]'); const isDisplay = isDisplayModeFormula(element); return annotation ? formatLatex(annotation.textContent, isDisplay) : null; }; target.isDisplayMode = isDisplayModeFormula; return target; } // 重构:直接处理DOM fragment,避免Trusted Types问题 function processFormulaContentFromFragment(fragment) { if (!fragment) return ''; console.log('处理选中的DOM fragment开始'); // 克隆fragment以避免修改原始选择 const workingFragment = fragment.cloneNode(true); // 创建临时容器来处理fragment const tempDiv = document.createElement('div'); tempDiv.appendChild(workingFragment); return processFormulaFromElement(tempDiv); } // 新函数:直接从DOM元素处理公式,完全避免HTML字符串操作 function processFormulaFromElement(element) { console.log('开始处理元素中的公式'); // 找出所有KaTeX公式,包括Google AI Studio的ms-katex和豆包的container-rkuXQi const allFormulas = element.querySelectorAll('.katex, .math-inline, .math-display, .katex-display, ms-katex, .container-rkuXQi'); console.log(`找到 ${allFormulas.length} 个公式元素`); // 打印找到的公式元素信息 allFormulas.forEach((formula, index) => { console.log(`公式 ${index + 1}:`, { tagName: formula.tagName, className: formula.className, hasAnnotation: !!formula.querySelector('annotation[encoding="application/x-tex"]') }); }); // 处理每个公式 allFormulas.forEach((formula, index) => { try { console.log(`处理公式 ${index + 1}`); let latexContent = null; let isDisplayMode = false; // 豆包:优先使用data-custom-copy-text属性 const customCopyText = formula.getAttribute('data-custom-copy-text'); if (customCopyText) { latexContent = customCopyText; isDisplayMode = formula.classList.contains('math-display') || !formula.classList.contains('math-inline'); } else { // 其他网站:查找annotation元素 const annotation = formula.querySelector('annotation[encoding="application/x-tex"]'); if (annotation && annotation.textContent) { latexContent = annotation.textContent; // 判断是否是公式块 isDisplayMode = formula.classList.contains('math-display') || formula.classList.contains('katex-display') || formula.closest('.math-display') !== null || formula.closest('.katex-display') !== null || formula.closest('.ds-markdown-math') !== null || (formula.tagName === 'MS-KATEX' && !formula.classList.contains('inline')) || (formula.closest('ms-katex') && !formula.closest('ms-katex').classList.contains('inline')); } } // 如果找到了LaTeX内容 if (latexContent) { // 创建替换内容 let replacementText; if (isDisplayMode) { replacementText = '\n$$\n' + latexContent.trim() + '\n$$\n'; } else { replacementText = '$' + latexContent.trim() + '$'; } // 替换公式元素 const textNode = document.createTextNode(replacementText); // 找到最合适的父节点进行替换 let targetNode = formula; if (formula.closest('.katex-display')) { targetNode = formula.closest('.katex-display'); } else if (formula.closest('.math-display')) { targetNode = formula.closest('.math-display'); } else if (formula.closest('.ds-markdown-math')) { targetNode = formula.closest('.ds-markdown-math'); } else if (formula.closest('.container-rkuXQi')) { targetNode = formula.closest('.container-rkuXQi'); } else if (formula.tagName === 'MS-KATEX') { targetNode = formula; } else if (formula.closest('ms-katex')) { targetNode = formula.closest('ms-katex'); } if (targetNode.parentNode) { targetNode.parentNode.replaceChild(textNode, targetNode); } } } catch (e) { console.error('处理公式时出错:', e); } }); // 返回处理后的文本内容 let result; try { result = element.textContent || element.innerText || ''; } catch (e) { console.error('获取文本内容失败:', e); // 如果连textContent都无法访问,尝试手动提取 result = extractTextFromElement(element); } console.log('处理后的文本:', result); return result; } // 辅助函数:安全地从元素中提取文本 function extractTextFromElement(element) { let text = ''; try { // 递归遍历所有子节点 for (let node of element.childNodes) { if (node.nodeType === Node.TEXT_NODE) { text += node.textContent || ''; } else if (node.nodeType === Node.ELEMENT_NODE) { text += extractTextFromElement(node); } } } catch (e) { console.error('手动文本提取也失败:', e); return ''; } return text; } // 为公式元素添加事件处理 function setupFormulaHandlers() { const target = getTarget(window.location.href); if (!target) return; const formulaElements = document.querySelectorAll(target.elementSelector); if (formulaElements.length === 0) return; console.log(`找到 ${formulaElements.length} 个公式元素,添加事件处理器`); formulaElements.forEach(element => { // 防止重复添加 if (element.hasAttribute('data-formula-handled')) return; // 为了处理嵌套元素,检查父元素是否已经处理过 let parent = element.parentElement; while (parent) { if (parent.hasAttribute('data-formula-handled')) return; parent = parent.parentElement; } // 标记为已处理 element.setAttribute('data-formula-handled', 'true'); // 检查元素是否包含有效的LaTeX内容 let hasValidLatex = false; if (window.location.href.includes('gemini.google.com')) { // Gemini:检查hook数据 const katexHtml = element.classList.contains('katex-html') ? element : element.querySelector('.katex-html'); hasValidLatex = katexHtml && allKatexGemini[katexHtml.outerHTML]; } else if (window.location.href.includes('doubao.com')) { // 豆包:检查data-custom-copy-text属性 hasValidLatex = !!element.getAttribute('data-custom-copy-text') || !!element.querySelector('annotation[encoding="application/x-tex"]'); } else { // 其他网站:检查annotation元素 const annotation = element.querySelector('annotation[encoding="application/x-tex"]'); hasValidLatex = !!annotation; } if (!hasValidLatex) return; // 鼠标进入事件 element.addEventListener('mouseenter', function() { clearTimeout(tooltipTimeout); // 设置活动元素 if (activeFormulaElement) { activeFormulaElement.classList.remove('formula-hover'); } activeFormulaElement = element; element.classList.add('formula-hover'); // 准备显示LaTeX提示 tooltipTimeout = setTimeout(function() { const latexString = target.getLatexString(element); if (latexString) { tooltip.textContent = latexString; // 计算位置 const rect = element.getBoundingClientRect(); tooltip.style.display = 'block'; tooltip.style.opacity = '0'; // 确保提示框不会超出视窗 let leftPos = rect.left; if (leftPos + 350 > window.innerWidth) { leftPos = window.innerWidth - 350; } if (leftPos < 10) leftPos = 10; // 在元素上方或下方显示 if (rect.top > 100) { tooltip.style.top = `${rect.top - tooltip.offsetHeight - 5}px`; } else { tooltip.style.top = `${rect.bottom + 5}px`; } tooltip.style.left = `${leftPos}px`; tooltip.style.opacity = '0.9'; } }, 300); }); // 鼠标离开事件 element.addEventListener('mouseleave', function() { clearTimeout(tooltipTimeout); element.classList.remove('formula-hover'); tooltipTimeout = setTimeout(function() { if (activeFormulaElement === element) { hideTooltip(); } }, 100); }); // 点击事件 - 复制公式 element.addEventListener('click', function(e) { const latexString = target.getLatexString(element); if (latexString) { navigator.clipboard.writeText(latexString).then(() => { showCopySuccessTooltip(); console.log(`已复制公式: ${latexString}`); }).catch(err => { console.error('复制公式失败:', err); showToast('复制公式失败: ' + err.message, 'error'); }); // 阻止事件冒泡 e.stopPropagation(); e.preventDefault(); } }); }); } // 监听复制事件(用户使用Ctrl+C或右键复制) document.addEventListener('copy', async function(e) { console.log('复制事件触发'); // 检查是否有选中的内容 const selection = window.getSelection(); if (!selection || selection.isCollapsed) { console.log('没有选中内容,跳过处理'); return; } console.log('选中文本:', selection.toString()); // 获取选中的DOM内容(直接处理DOM,避免Trusted Types错误) const range = selection.getRangeAt(0); const fragment = range.cloneContents(); // 直接检查DOM fragment中是否有公式元素 const tempDiv = document.createElement('div'); tempDiv.appendChild(fragment.cloneNode(true)); // 直接在DOM中检测公式元素 const formulaElements = tempDiv.querySelectorAll('.katex, .math-inline, .math-display, .katex-display, ms-katex, .katex-html, .container-rkuXQi'); const hasFormula = formulaElements.length > 0; console.log('找到公式元素数量:', formulaElements.length); console.log('是否包含公式:', hasFormula); if (hasFormula) { console.log('检测到选中内容包含公式,开始处理...'); try { e.preventDefault(); // 阻止默认复制行为 e.stopPropagation(); // 阻止事件冒泡 let processedText; // Gemini网站特殊处理 if (window.location.href.includes('gemini.google.com')) { console.log('使用Gemini专用复制逻辑'); // 扩展选择范围到完整的katex元素 const startKatex = closestKatex(range.startContainer); if (startKatex) { range.setStartBefore(startKatex); } const endKatex = closestKatex(range.endContainer); if (endKatex) { range.setEndAfter(endKatex); } // 重新获取扩展后的fragment const expandedFragment = range.cloneContents(); // 使用Gemini专用的替换函数 katexReplaceWithTexGemini(expandedFragment); processedText = expandedFragment.textContent; // 阻止Gemini的默认处理 e.stopImmediatePropagation(); } else { // 其他网站使用原有逻辑 processedText = processFormulaContentFromFragment(fragment); } console.log('处理后的文本:', processedText); // 设置到剪贴板 const success = await setClipboardToPlainText(processedText); if (success) { showToast('已格式化选中的公式内容', 'success'); console.log('复制成功'); } else { // 备用复制方法 try { await navigator.clipboard.writeText(processedText); showToast('已格式化选中的公式内容(备用方法)', 'success'); console.log('备用复制方法成功'); } catch (fallbackError) { console.error('备用复制方法也失败:', fallbackError); showToast('复制失败,请手动复制。处理后的内容已打印到控制台', 'error'); console.log('请手动复制以下内容:', processedText); } } } catch (error) { console.error('处理复制事件时出错:', error); showToast('处理复制事件时出错: ' + error.message, 'error'); } } }, { capture: true, passive: false }); // 处理按钮点击事件 function handleButtonClick() { console.log('复制按钮被点击'); setTimeout(async function() { try { const text = await navigator.clipboard.readText(); // 统一规范化三种常见定界:$$...$$, \[...\], \(...\) let modifiedText = text; let matchCount = 0; // $$...$$ → 换行块级 modifiedText = modifiedText.replace(/\$\$(.*?)\$\$/gs, (m, f) => { matchCount++; return `\n$$\n${f.trim()}\n$$\n`; }); // \[...\] → 换行块级(避免与 $$ 已替换的重复,这里直接处理剩余) modifiedText = modifiedText.replace(/\\\[(.*?)\\\]/gs, (m, f) => { matchCount++; return `\n$$\n${f.trim()}\n$$\n`; }); // \(...\) → 行内 modifiedText = modifiedText.replace(/\\\((.*?)\\\)/gs, (m, f) => { matchCount++; return `$${f.trim()}$`; }); if (matchCount > 0) { const success = await setClipboardToPlainText(modifiedText); if (success) { showToast(`已格式化 ${matchCount} 个公式`, 'success'); } else { showToast('写入纯文本格式到剪贴板失败', 'error'); } } } catch (err) { console.error('处理剪贴板失败:', err); showToast('处理剪贴板失败', 'error'); } }, 100); } // 查找并监听复制按钮 function setupButtonListener() { // 查找所有可能的复制按钮 const copyButtons = document.querySelectorAll('button[data-testid="action-bar-copy"], button:has(svg[data-testid="action-bar-copy"])'); // DeepSeek特定按钮 if (window.location.href.includes('deepseek.com')) { const deepseekButtons = document.querySelectorAll('button.copy-btn, button:has(svg[data-icon="copy"])'); if (deepseekButtons.length > 0) { deepseekButtons.forEach(button => { button.removeEventListener('click', handleButtonClick); button.addEventListener('click', handleButtonClick); }); } } // 豆包特定按钮 if (window.location.href.includes('doubao.com')) { const doubaoButtons = document.querySelectorAll('button[data-testid="message_action_copy"]'); if (doubaoButtons.length > 0) { doubaoButtons.forEach(button => { button.removeEventListener('click', handleButtonClick); button.addEventListener('click', handleButtonClick); }); console.log(`找到 ${doubaoButtons.length} 个豆包复制按钮,添加监听器`); } } if (copyButtons.length > 0) { console.log(`找到 ${copyButtons.length} 个复制按钮,添加监听器`); copyButtons.forEach(button => { button.removeEventListener('click', handleButtonClick); button.addEventListener('click', handleButtonClick); }); } } // 页面加载和DOM变化时初始化功能 function initialize() { setupButtonListener(); setupFormulaHandlers(); } // Gemini专用延迟初始化逻辑 function initializeForGemini() { let retryCount = 0; const maxRetries = 3; const baseDelay = 1000; // 1秒基础延迟 function attemptInitialization() { console.log(`Gemini初始化尝试 ${retryCount + 1}/${maxRetries}`); // 首先设置KaTeX Hook setupGeminiKatexHook(); // 检查是否有对话容器存在 const conversationContainer = document.querySelector('.response-container, message-content, .model-response-text'); if (conversationContainer || retryCount >= maxRetries) { initialize(); // 为Gemini对话容器添加特殊监听 if (conversationContainer) { const geminiObserver = new MutationObserver(function() { // 确保hook依然有效 if (!isGeminiKatexHooked) { setupGeminiKatexHook(); } setTimeout(initialize, 500); // 短延迟后重新初始化 }); geminiObserver.observe(conversationContainer, { childList: true, subtree: true }); } return; } retryCount++; const delay = baseDelay * retryCount; // 递增延迟 setTimeout(attemptInitialization, delay); } attemptInitialization(); } // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { if (window.location.href.includes('gemini.google.com')) { initializeForGemini(); } else { initialize(); } }); } else { if (window.location.href.includes('gemini.google.com')) { initializeForGemini(); } else { initialize(); } } // 使用MutationObserver监视DOM变化 const observer = new MutationObserver(function(mutations) { let needsSetup = false; mutations.forEach(function(mutation) { if (mutation.addedNodes && mutation.addedNodes.length > 0) { needsSetup = true; } // 针对Gemini添加属性变化监听 if (mutation.type === 'attributes' && (mutation.attributeName === 'class' || mutation.attributeName === 'data-formula-handled')) { needsSetup = true; } }); if (needsSetup) { initialize(); } }); // 开始观察文档体的变化,针对Gemini增强监听 const observerConfig = { childList: true, subtree: true, attributes: true, attributeFilter: ['class', 'data-formula-handled'] }; observer.observe(document.body, observerConfig); // 添加安全定时器,定期重新扫描页面上的公式 setInterval(initialize, 5000); // 添加调试辅助函数 window.debugFormulaScript = function() { console.log('=== 公式复制脚本调试信息 ==='); console.log('脚本版本: 0.9 (豆包网站新增支持)'); console.log('当前网站:', window.location.href); console.log('支持的网站:', ['Claude', 'DeepSeek', '知乎', 'Google AI Studio', 'Google Gemini', '豆包', '等等']); const target = getTarget(window.location.href); console.log('当前网站配置:', target); const formulas = document.querySelectorAll(target.elementSelector); console.log(`页面上找到 ${formulas.length} 个公式元素`); formulas.forEach((formula, index) => { const latex = target.getLatexString(formula); console.log(`公式 ${index + 1}:`, latex); }); // Gemini特殊调试信息 if (window.location.href.includes('gemini.google.com')) { console.log('=== Gemini特殊调试信息 ==='); console.log('KaTeX Hook状态:', isGeminiKatexHooked); console.log('已记录的KaTeX映射数量:', Object.keys(allKatexGemini).length); const containers = document.querySelectorAll('.response-container, message-content, .model-response-text'); console.log(`找到 ${containers.length} 个对话容器`); const mathBlocks = document.querySelectorAll('div.math-block'); console.log(`找到 ${mathBlocks.length} 个数学块容器`); const katexHtmlElements = document.querySelectorAll('.katex-html'); console.log(`找到 ${katexHtmlElements.length} 个katex-html元素`); // 显示已记录的KaTeX映射 console.log('KaTeX映射详情:'); Object.entries(allKatexGemini).forEach(([html, latex], index) => { console.log(` ${index + 1}. ${latex}`); }); } // 豆包特殊调试信息 if (window.location.href.includes('doubao.com')) { console.log('=== 豆包特殊调试信息 ==='); const doubaoContainers = document.querySelectorAll('.container-rkuXQi'); console.log(`找到 ${doubaoContainers.length} 个豆包公式容器`); const copyButtons = document.querySelectorAll('button[data-testid="message_action_copy"]'); console.log(`找到 ${copyButtons.length} 个豆包复制按钮`); // 显示豆包公式的data-custom-copy-text属性 console.log('豆包公式data-custom-copy-text属性:'); doubaoContainers.forEach((container, index) => { const customText = container.getAttribute('data-custom-copy-text'); if (customText) { console.log(` ${index + 1}. ${customText}`); } }); } console.log('如果复制不工作,请检查浏览器控制台的错误信息'); console.log('=== 调试信息结束 ==='); }; // 在页面上显示初始化成功提示 showToast('公式复制格式调整脚本已加载', 'info', 2000); // 如果是Google AI Studio,显示特殊提示 if (window.location.href.includes('aistudio.google.com')) { setTimeout(() => { showToast('Google AI Studio适配已启用,控制台输入debugFormulaScript()查看调试信息', 'info', 4000); }, 2500); } // 如果是Google Gemini,显示特殊提示 if (window.location.href.includes('gemini.google.com')) { setTimeout(() => { showToast('Google Gemini适配已启用,支持动态加载内容,控制台输入debugFormulaScript()查看调试信息', 'info', 4000); }, 2500); } // 如果是豆包,显示特殊提示 if (window.location.href.includes('doubao.com')) { setTimeout(() => { showToast('豆包适配已启用,支持data-custom-copy-text属性,控制台输入debugFormulaScript()查看调试信息', 'info', 4000); }, 2500); } })();