ChatGPT对话转Markdown

将ChatGPT对话转换为Markdown格式,并提供复制和下载功能

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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
    });
})