您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
支持Claude和DeepSeek等网站的公式复制,包括点击复制、选择复制和按钮复制,格式化为$和$$包裹的LaTeX
当前为
// ==UserScript== // @name AI网站公式复制Latex // @namespace http://tampermonkey.net/ // @version 0.7 // @description 支持Claude和DeepSeek等网站的公式复制,包括点击复制、选择复制和按钮复制,格式化为$和$$包裹的LaTeX // @author You // @match *://demo.fuclaude.oaifree.com/* // @match *://claude.ai/* // @match *://*.zhihu.com/* // @match *://*.wikipedia.org/* // @match *://*.chatgpt.com/* // @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/* // @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; // 显示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 (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 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('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 const allFormulas = element.querySelectorAll('.katex, .math-inline, .math-display, .katex-display, ms-katex'); 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}`); // 查找公式的LaTeX内容 const annotation = formula.querySelector('annotation[encoding="application/x-tex"]'); // 如果找到了LaTeX内容 if (annotation && annotation.textContent) { // 判断是否是公式块 const 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')); // 创建替换内容 let replacementText; if (isDisplayMode) { replacementText = '\n$$\n' + annotation.textContent.trim() + '\n$$\n'; } else { replacementText = '$' + annotation.textContent.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.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注释 const annotation = element.querySelector('annotation[encoding="application/x-tex"]'); if (!annotation) 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'); const hasFormula = formulaElements.length > 0; console.log('找到公式元素数量:', formulaElements.length); console.log('是否包含公式:', hasFormula); if (hasFormula) { console.log('检测到选中内容包含公式,开始处理...'); try { e.preventDefault(); // 阻止默认复制行为 e.stopPropagation(); // 阻止事件冒泡 // 直接处理DOM fragment,避免HTML字符串操作 const 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(); if (text.includes('$$')) { console.log('检测到按钮复制的公式,正在格式化...'); // 正则表达式匹配所有的 $$ 公式内容 $$ 格式 const formulaRegex = /\$\$(.*?)\$\$/gs; let matchCount = 0; // 替换为换行格式 const modifiedText = text.replace(formulaRegex, function(match, formula) { matchCount++; const trimmedFormula = formula.trim(); return '\n$$\n' + trimmedFormula + '\n$$\n'; }); // 设置修改后的内容到剪贴板 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 (copyButtons.length > 0) { console.log(`找到 ${copyButtons.length} 个复制按钮,添加监听器`); copyButtons.forEach(button => { button.removeEventListener('click', handleButtonClick); button.addEventListener('click', handleButtonClick); }); } } // 页面加载和DOM变化时初始化功能 function initialize() { setupButtonListener(); setupFormulaHandlers(); } // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } 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; } }); if (needsSetup) { initialize(); } }); // 开始观察文档体的变化 observer.observe(document.body, { childList: true, subtree: true }); // 添加安全定时器,定期重新扫描页面上的公式 setInterval(initialize, 5000); // 添加调试辅助函数 window.debugFormulaScript = function() { console.log('=== 公式复制脚本调试信息 ==='); console.log('脚本版本: 0.8 (Google AI Studio 适配版)'); console.log('当前网站:', window.location.href); console.log('支持的网站:', ['Claude', 'DeepSeek', '知乎', 'Google AI Studio', '等等']); 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); }); 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); } })();