您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
将ChatGPT对话转换为Markdown格式,并提供复制和下载功能
// ==UserScript== // @name ChatGPT对话转Markdown // @namespace http://tampermonkey.net/ // @version 1.0 // @description 将ChatGPT对话转换为Markdown格式,并提供复制和下载功能 // @author Xiang0731 // @license MIT // @match https://chat.openai.com/* // @match https://chatgpt.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=openai.com // @grant none // ==/UserScript== (function () { 'use strict'; // 创建按钮样式 const style = document.createElement('style'); style.textContent = ` .gpt-md-btn { position: fixed; right: 20px; z-index: 1000; background-color: #10a37f; color: white; border: none; border-radius: 4px; padding: 8px 12px; font-size: 14px; cursor: pointer; margin-top: 5px; transition: background-color 0.3s; } .gpt-md-btn:hover { background-color: #0d8a6c; } .copy-btn { top: 70px; } .download-btn { top: 110px; } `; document.head.appendChild(style); // 创建按钮 function createButtons() { const copyButton = document.createElement('button'); copyButton.textContent = '复制对话为Markdown'; copyButton.className = 'gpt-md-btn copy-btn'; copyButton.addEventListener('click', copyToClipboard); const downloadButton = document.createElement('button'); downloadButton.textContent = '下载对话为Markdown'; downloadButton.className = 'gpt-md-btn download-btn'; downloadButton.addEventListener('click', downloadMarkdown); // 添加调试按钮 const debugButton = document.createElement('button'); debugButton.textContent = '调试信息'; debugButton.className = 'gpt-md-btn debug-btn'; debugButton.style.top = '150px'; debugButton.addEventListener('click', debugStructure); document.body.appendChild(copyButton); document.body.appendChild(downloadButton); document.body.appendChild(debugButton); } // 提取对话内容并转换为Markdown function getConversationAsMarkdown() { // 尝试多种可能的选择器来找到对话容器 let threadContainer = null; // 选择器列表,按优先级排序 const selectors = [ 'main div[class*="react-scroll-to-bottom"]', 'div[class*="chat-container"]', 'div[class*="conversation-container"]', 'main div[class*="overflow-y-auto"]', 'div[role="presentation"] div[class*="overflow-y-auto"]', 'div.flex.flex-col.text-sm', 'main .flex-1.overflow-hidden', 'div[class*="chat-pg-"]' ]; // 尝试所有可能的选择器 for (const selector of selectors) { const container = document.querySelector(selector); if (container) { threadContainer = container; console.log('找到对话容器,使用选择器:', selector); break; } } if (!threadContainer) { console.error('无法找到对话容器'); // 提供更详细的调试信息 console.log('页面结构:', document.body.innerHTML.substring(0, 5000)); // 输出前5000个字符 // 建议用户使用调试功能 alert('无法找到对话内容。 请点击"调试信息"按钮,然后将控制台输出发送给开发者以帮助修复问题。 '); return "无法找到对话内容"; } const conversationTitle = document.title.replace(' - ChatGPT', '').trim(); let markdown = `# ${conversationTitle}\n\n`; // 尝试多种选择器找对话块 let messageNodes = threadContainer.querySelectorAll('div[data-message-author-role]'); if (!messageNodes || messageNodes.length === 0) { messageNodes = threadContainer.querySelectorAll('div[data-testid*="conversation-turn-"]'); } if (!messageNodes || messageNodes.length === 0) { messageNodes = threadContainer.querySelectorAll('.group.w-full'); } if (!messageNodes || messageNodes.length === 0) { console.error('无法找到对话消息'); // 提供更详细的调试信息,输出找到的容器 console.log('找到的容器:', threadContainer); console.log('容器HTML:', threadContainer.innerHTML.substring(0, 5000)); // 输出前5000个字符 alert('无法找到对话消息。 请点击"调试信息"按钮获取更多信息。 '); return "无法找到对话消息"; } console.log(`找到 ${messageNodes.length} 条消息`); messageNodes.forEach((node, index) => { // 尝试多种方式确定角色 let isUser = false; if (node.hasAttribute('data-message-author-role')) { isUser = node.getAttribute('data-message-author-role') === 'user'; } else if (node.querySelector('.flex.items-center.justify-center.p-1.rounded-md')) { // 用户通常有头像图标 isUser = true; } else if (node.querySelector('img[alt*="User"]')) { isUser = true; } else { // 如果无法确定,根据索引奇偶判断(通常用户在奇数位) isUser = index % 2 === 0; } // 尝试多种选择器找到内容 let content = node.querySelector('div[class*="prose"]'); if (!content) { content = node.querySelector('.markdown'); } if (!content) { content = node.querySelector('.text-message'); } if (!content) { // 找不到特定内容容器,使用整个节点 content = node; } if (!content) return; // 添加角色标题 markdown += `## ${isUser ? '用户' : 'ChatGPT'}\n\n`; // 获取HTML内容并进行处理 const htmlContent = content.innerHTML; const tempDiv = document.createElement('div'); tempDiv.innerHTML = htmlContent; // 处理标题 for (let i = 1; i <= 6; i++) { const headings = tempDiv.querySelectorAll(`h${i}`); headings.forEach(heading => { // 将HTML标题转换为Markdown格式 const hashes = '#'.repeat(i); heading.outerHTML = `\n\n${hashes} ${heading.textContent.trim()}\n\n`; }); } // 处理删除线 const strikeElements = tempDiv.querySelectorAll('del, s'); strikeElements.forEach(element => { element.outerHTML = `~~${element.textContent}~~`; }); // 处理引用块 const blockquotes = tempDiv.querySelectorAll('blockquote'); blockquotes.forEach(blockquote => { // 获取引用块内容,并在每行前添加>符号 const content = blockquote.innerHTML .replace(/<p>/g, '\n') .replace(/<\/p>/g, '') .split('\n') .filter(line => line.trim() !== '') .map(line => `> ${line.trim()}`) .join('\n'); blockquote.outerHTML = `\n${content}\n\n`; }); // 处理表格 const tables = tempDiv.querySelectorAll('table'); tables.forEach(table => { let markdownTable = '\n'; // 处理表头 const headerRow = table.querySelector('thead tr'); if (headerRow) { const headerCells = headerRow.querySelectorAll('th'); if (headerCells.length > 0) { // 添加表头行 markdownTable += '| ' + Array.from(headerCells) .map(cell => cell.textContent.trim()) .join(' | ') + ' |\n'; // 添加分隔行 markdownTable += '| ' + Array.from(headerCells) .map(() => '---') .join(' | ') + ' |\n'; } } // 处理表格主体 const rows = table.querySelectorAll('tbody tr'); rows.forEach(row => { const cells = row.querySelectorAll('td'); if (cells.length > 0) { markdownTable += '| ' + Array.from(cells) .map(cell => cell.textContent.trim()) .join(' | ') + ' |\n'; } }); // 如果没有找到表头,但有表格行,则创建一个没有表头的表格 if (!headerRow && rows.length > 0) { const firstRow = rows[0]; const cellCount = firstRow.querySelectorAll('td').length; if (cellCount > 0) { // 在第一行前插入分隔行 const separatorIndex = markdownTable.indexOf('\n') + 1; const separator = '| ' + Array(cellCount).fill('---').join(' | ') + ' |\n'; markdownTable = markdownTable.slice(0, separatorIndex) + separator + markdownTable.slice(separatorIndex); } } markdownTable += '\n'; table.outerHTML = markdownTable; }); // 处理特殊的例子格式(如带有缩进的示例文本) const examplePatterns = tempDiv.querySelectorAll('p > em, li > em'); examplePatterns.forEach(em => { const parent = em.parentElement; // 如果这是个例子,将其包装在正确的格式中 if (parent.textContent.includes('例如') || parent.textContent.includes('example')) { // 确保例子前有短横线,适当格式化 if (parent.tagName.toLowerCase() !== 'li') { const exampleText = em.outerHTML; parent.innerHTML = parent.innerHTML.replace(em.outerHTML, `\n - ${exampleText}`); } } }); // 处理代码块 const codeBlocks = tempDiv.querySelectorAll('pre'); codeBlocks.forEach(codeBlock => { // 获取代码元素及其语言 const codeElement = codeBlock.querySelector('code'); if (!codeElement) return; // 尝试从类名中提取语言 let language = ''; const classNames = codeElement.className.split(' '); for (const className of classNames) { if (className.startsWith('language-')) { language = className.replace('language-', ''); // 确保只提取实际语言名称,不包含额外的标记 if (language.includes(' ')) { language = language.split(' ')[0]; } break; } } // 特殊处理:如果没有找到语言标识但有copy-btn,尝试从其他元素获取 if (!language) { const copyBtn = codeBlock.querySelector('.copy-btn'); if (copyBtn && copyBtn.getAttribute('data-code-type')) { language = copyBtn.getAttribute('data-code-type'); } // 从pre标签的类名中寻找语言标识 const preClasses = codeBlock.className.split(' '); for (const cls of preClasses) { if (cls.startsWith('language-')) { language = cls.replace('language-', ''); break; } } } // 清理获取的代码内容 let codeContent = codeElement.textContent.trim(); // 移除可能的按钮文本("复制"、"编辑"等) codeContent = codeContent .replace(/^(复制|编辑|copy|edit|markdown)[\s\n]*/i, '') .replace(/^(whitespace-pre!)[\s\n]*/i, ''); // 移除可能在第一行的语言标识 if (codeContent.startsWith(language) && (codeContent[language.length] === ' ' || codeContent[language.length] === '\n')) { codeContent = codeContent.substring(language.length).trim(); } // 创建markdown代码块 const markdownCodeBlock = `\`\`\`${language}\n${codeContent}\n\`\`\``; codeBlock.outerHTML = markdownCodeBlock; }); // 处理内联代码 const inlineCodeBlocks = tempDiv.querySelectorAll('code:not(pre code)'); inlineCodeBlocks.forEach(inlineCode => { inlineCode.outerHTML = `\`${inlineCode.textContent}\``; }); // 处理链接 const links = tempDiv.querySelectorAll('a'); links.forEach(link => { link.outerHTML = `[${link.textContent}](${link.href})`; }); // 处理粗体 const bold = tempDiv.querySelectorAll('strong'); bold.forEach(b => { b.outerHTML = `**${b.textContent}**`; }); // 处理斜体 const italic = tempDiv.querySelectorAll('em'); italic.forEach(i => { i.outerHTML = `*${i.textContent}*`; }); // 改进的列表处理 function processLists(element) { const lists = element.querySelectorAll('ol, ul'); // 从最深层嵌套的列表开始处理 for (let i = lists.length - 1; i >= 0; i--) { const list = lists[i]; // 检查是否已经处理过 if (list.hasAttribute('data-processed')) continue; const isOrdered = list.tagName.toLowerCase() === 'ol'; const items = list.querySelectorAll(':scope > li'); let listContent = '\n'; items.forEach((item, index) => { // 确定缩进级别 let indentLevel = 0; let parent = list.parentElement; while (parent) { if (parent.tagName.toLowerCase() === 'li') { indentLevel++; } parent = parent.parentElement; } // 添加适当的缩进 const indent = ' '.repeat(indentLevel); // 添加正确的列表符号 const prefix = isOrdered ? `${index + 1}. ` : '- '; // 获取纯文本并保留内部HTML结构 const itemContent = item.innerHTML .replace(/<\/?ol>/g, '') .replace(/<\/?ul>/g, '') .replace(/<li>/g, '') .replace(/<\/li>/g, '\n'); listContent += `${indent}${prefix}${itemContent.trim()}\n`; }); list.outerHTML = listContent; list.setAttribute('data-processed', 'true'); } } // 处理嵌套列表结构 processLists(tempDiv); // 处理剩余的任何单个列表项 const remainingItems = tempDiv.querySelectorAll('li'); remainingItems.forEach(item => { const isInList = item.parentElement && (item.parentElement.tagName.toLowerCase() === 'ol' || item.parentElement.tagName.toLowerCase() === 'ul'); if (!isInList) { // 孤立的列表项转换为段落 item.outerHTML = `<p>${item.innerHTML}</p>`; } }); markdown += tempDiv.textContent.trim() + '\n\n'; }); // 添加生成时间脚注 const date = new Date().toLocaleString(); markdown += `---\n*保存时间: ${date}*`; // 最终清理Markdown内容 markdown = markdown // 删除多余的空行 .replace(/\n{3,}/g, '\n\n') // 修复列表格式中可能的问题 .replace(/(\d+\.\s.*\n)\n(?=\s+[-*])/g, '$1') // 确保列表后有适当的空行 .replace(/(\n\s*[-*].+\n)(?=[^\s])/g, '$1\n'); return markdown; } // 复制到剪贴板 function copyToClipboard() { const markdown = getConversationAsMarkdown(); // 记录转换结果的前500个字符(用于调试) console.log('转换结果预览:', markdown.substring(0, 500)); navigator.clipboard.writeText(markdown) .then(() => { alert('对话已复制到剪贴板'); }) .catch(err => { console.error('复制失败:', err); alert('复制失败,请查看控制台获取详细信息'); }); } // 下载Markdown文件 function downloadMarkdown() { const markdown = getConversationAsMarkdown(); // 记录转换结果的前500个字符(用于调试) console.log('转换结果预览:', markdown.substring(0, 500)); const conversationTitle = document.title.replace(' - ChatGPT', '').trim() || 'ChatGPT对话'; const fileName = `${conversationTitle.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '_')}.md`; const blob = new Blob([markdown], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); // 清理 setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); } // 调试当前页面结构 function debugStructure() { console.log('===== ChatGPT页面结构调试 ====='); // 尝试识别可能的对话容器 for (const selector of [ 'main div[class*="react-scroll-to-bottom"]', 'div[class*="chat-container"]', 'div[class*="conversation-container"]', 'main div[class*="overflow-y-auto"]', 'div[role="presentation"] div[class*="overflow-y-auto"]', 'div.flex.flex-col.text-sm', 'main .flex-1.overflow-hidden', 'div[class*="chat-pg-"]', // 添加更多可能的选择器 'main', 'div[role="main"]', '.overflow-y-auto', '.flex-1' ]) { const elements = document.querySelectorAll(selector); console.log(`选择器 "${selector}": 找到 ${elements.length} 个元素`); if (elements.length > 0) { console.log('第一个元素:', elements[0]); } } // 尝试识别消息块 console.log('===== 可能的消息块 ====='); for (const selector of [ 'div[data-message-author-role]', 'div[data-testid*="conversation-turn-"]', '.group.w-full', 'div[class*="message"]', 'div[class*="chat-message"]' ]) { const elements = document.querySelectorAll(selector); console.log(`选择器 "${selector}": 找到 ${elements.length} 个元素`); if (elements.length > 0) { console.log('第一个元素:', elements[0]); } } alert('调试信息已输出到控制台。 请按F12打开开发者工具查看。 '); } // 页面加载完成后创建按钮 window.addEventListener('load', () => { // 给ChatGPT页面一些时间加载 setTimeout(createButtons, 2000); }); // 监听页面变化,确保在导航到新对话时按钮仍然存在 const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { if (!document.querySelector('.gpt-md-btn')) { createButtons(); break; } } } }); observer.observe(document.body, { childList: true, subtree: true }); })
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址