ChatGPT对话转Markdown

将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或关注我们的公众号极客氢云获取最新地址