Greasy Fork is available in English.

Claude Conversation Exporter Plus

优雅导出 Claude 对话记录,支持 JSON 和 Markdown 格式。Elegantly export Claude conversation records, supporting JSON and Markdown formats.

// ==UserScript==
// @name         Claude Conversation Exporter Plus
// @namespace    http://tampermonkey.net/
// @version      4.6.3
// @description  优雅导出 Claude 对话记录,支持 JSON 和 Markdown 格式。Elegantly export Claude conversation records, supporting JSON and Markdown formats.
// @author       Gao + GPT-4 + Claude
// @license      Custom License
// @match        https://*.claudesvip.top/chat/*
// @match        https://*.claude.ai/chat/*
// @match        https://*.fuclaude.com/chat/*
// @match        https://*.aikeji.vip/chat/*
// @grant        none
// ==/UserScript==

/*
 您可以在个人设备上使用和修改该代码。
 不得将该代码或其修改版本重新分发、再发布或用于其他公众渠道。
 保留所有权利,未经授权不得用于商业用途。
*/

/*
You may use and modify this code on your personal devices.
You may not redistribute, republish, or use this code or its modified versions in other public channels.
All rights reserved. Unauthorized commercial use is prohibited.
*/

(function() {
    'use strict';

    // 状态追踪
    let state = {
        targetResponse: null,
        lastUpdateTime: null,
        convertedMd: null
    };

    // 日志函数
    const log = {
        info: (msg) => console.log(`[Claude Saver] ${msg}`),
        error: (msg, e) => console.error(`[Claude Saver] ${msg}`, e)
    };

    // 正则表达式用于匹配目标 URL
    const targetUrlPattern = /\/chat_conversations\/[\w-]+\?tree=True&rendering_mode=messages&render_all_tools=true/;

    // 响应处理函数(处理符合匹配模式的响应)
    function processTargetResponse(text, url) {
        try {
            if (targetUrlPattern.test(url)) {
                state.targetResponse = text;
                state.lastUpdateTime = new Date().toLocaleTimeString();
                updateButtonStatus();
                log.info(`成功捕获目标响应 (${text.length} bytes) 来自: ${url}`);

                // 转换为Markdown
                state.convertedMd = convertJsonToMd(JSON.parse(text));
                log.info('成功将JSON转换为Markdown');
            }
        } catch (e) {
            log.error('处理目标响应时出错:', e);
        }
    }

    // 更新按钮状态
    function updateButtonStatus() {
        const jsonButton = document.getElementById('downloadJsonButton');
        const mdButton = document.getElementById('downloadMdButton');
        if (jsonButton && mdButton) {
            const hasResponse = state.targetResponse !== null;
            jsonButton.style.backgroundColor = hasResponse ? '#28a745' : '#007bff';
            mdButton.style.backgroundColor = state.convertedMd ? '#28a745' : '#007bff';
            const statusText = hasResponse ? `最后更新: ${state.lastUpdateTime}
数据已准备好` : '等待目标响应中...';
            jsonButton.title = statusText;
            mdButton.title = statusText;
        }
    }

    // 创建下载按钮
    function createDownloadButtons() {
        // JSON 下载按钮
        const jsonButton = document.createElement('button');
        const mdButton = document.createElement('button');

        const buttonStyles = {
            padding: '10px 15px',
            backgroundColor: '#007bff',
            color: '#ffffff',
            border: 'none',
            borderRadius: '5px',
            cursor: 'pointer',
            transition: 'all 0.3s ease',
            fontFamily: 'Arial, sans-serif',
            boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
            whiteSpace: 'nowrap',
            marginRight: '10px'
        };

        jsonButton.id = 'downloadJsonButton';
        jsonButton.innerText = '下载 JSON';
        mdButton.id = 'downloadMdButton';
        mdButton.innerText = '下载 Markdown';

        Object.assign(jsonButton.style, buttonStyles);
        Object.assign(mdButton.style, buttonStyles);

        // 鼠标悬停效果
        const onMouseOver = (button) => {
            button.style.transform = 'scale(1.05)';
            button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
        };
        const onMouseOut = (button) => {
            button.style.transform = 'scale(1)';
            button.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
        };

        jsonButton.onmouseover = () => onMouseOver(jsonButton);
        jsonButton.onmouseout = () => onMouseOut(jsonButton);
        mdButton.onmouseover = () => onMouseOver(mdButton);
        mdButton.onmouseout = () => onMouseOut(mdButton);

        // 下载 JSON 功能
        jsonButton.onclick = function() {
            if (!state.targetResponse) {
                alert(`还没有发现有效的对话记录。
请等待目标响应或进行一些对话。`);
                return;
            }

            try {
                const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
                const chatName = document.title.trim().replace(/\s+-\s+Claude$/, '').replace(/[\/\\?%*:|"<>]/g, '-');
                const fileName = `${chatName}_${timestamp}.json`;

                const blob = new Blob([state.targetResponse], { type: 'application/json' });
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob);
                link.download = fileName;
                link.click();

                log.info(`成功下载文件: ${fileName}`);
            } catch (e) {
                log.error('下载过程中出错:', e);
                alert('下载过程中发生错误,请查看控制台了解详情。');
            }
        };

        // 下载 Markdown 功能
        mdButton.onclick = function() {
            if (!state.convertedMd) {
                alert(`还没有发现有效的对话记录。
请等待目标响应或进行一些对话。`);
                return;
            }

            try {
                const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
                const chatName = document.title.trim().replace(/\s+-\s+Claude$/, '').replace(/[\/\\?%*:|"<>]/g, '-');
                const fileName = `${chatName}_${timestamp}.md`;

                const blob = new Blob([state.convertedMd], { type: 'text/markdown' });
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob);
                link.download = fileName;
                link.click();

                log.info(`成功下载文件: ${fileName}`);
            } catch (e) {
                log.error('下载过程中出错:', e);
                alert('下载过程中发生错误,请查看控制台了解详情。');
            }
        };

        const buttonContainer = document.createElement('div');
        buttonContainer.style.position = 'fixed';
        buttonContainer.style.top = '45%';
        buttonContainer.style.right = '10px';
        buttonContainer.style.transform = 'translateY(-50%)';
        buttonContainer.style.zIndex = '9999';
        buttonContainer.style.display = 'flex';
        buttonContainer.style.flexDirection = 'row';

        buttonContainer.appendChild(jsonButton);
        buttonContainer.appendChild(mdButton);
        document.body.appendChild(buttonContainer);

        updateButtonStatus();
    }

    function convertJsonToMd(data) {
        let mdContent = [];
        const title = document.title.trim().replace(/\s+-\s+Claude$/, '');
        mdContent.push(`# ${title}\n`);

        // 获取当前页面的完整 URL
        const currentUrl = window.location.href;

        // 提取 URL 前缀(去掉 chat/* 部分)
        const baseUrl = currentUrl.replace(/\/chat\/.*$/, '');

        for (const message of data['chat_messages']) {
            const sender = message['sender'].charAt(0).toUpperCase() + message['sender'].slice(1);
            mdContent.push(`## ${sender}`);

            const createdAt = message['created_at'] || '';
            const updatedAt = message['updated_at'] || '';
            const timestamp = createdAt === updatedAt ? `*${createdAt}*` : `*${createdAt} (updated)*`;
            mdContent.push(timestamp);

            const content = processContent(message['content']);
            mdContent.push(`${content}\n`);

            // === 处理附加文件部分开始 ===
            if (message['attachments'] && message['attachments'].length > 0) {
                mdContent.push(`## 附加文件:`);

                for (const attachment of message['attachments']) {
                    // 判断文件是否有 preview_url 或 document_asset
                    if (attachment.preview_url) {
                        // 使用 preview_url 生成链接
                        const previewLink = `${baseUrl}${attachment.preview_url}`;
                        mdContent.push(`[${attachment.file_name}](${previewLink})\n`);
                    } else if (attachment.document_asset && attachment.document_asset.url) {
                        // 使用 document_asset.url 生成链接
                        const documentLink = `${baseUrl}${attachment.document_asset.url}`;
                        mdContent.push(`[${attachment.file_name}](${documentLink})\n`);
                    } else if (attachment.extracted_content) {
                        // 有具体内容的文件
                        mdContent.push(`${attachment.file_name}\n`);
                        mdContent.push("```\n");
                        mdContent.push(`${attachment.extracted_content}\n`);
                        mdContent.push("```\n");
                    } else {
                        // 无法提取内容或生成链接的文件
                        mdContent.push(`${attachment.file_name} (无法提取内容或生成链接)\n`);
                    }
                }
            }

            // === 处理 `files_v2` 部分开始 ===
            if (message['files_v2'] && message['files_v2'].length > 0) {
                mdContent.push(`## 附加文件:`);

                for (const file of message['files_v2']) {
                    if (file.document_asset && file.document_asset.url) {
                        // 处理 `document_asset` 链接
                        const documentLink = `${baseUrl}${file.document_asset.url}`;
                        mdContent.push(`[${file.file_name}](${documentLink})\n`);
                    } else if (file.preview_url) {
                        // 处理常规的 `preview_url` 链接
                        const previewLink = `${baseUrl}${file.preview_url}`;
                        mdContent.push(`[${file.file_name}](${previewLink})\n`);
                    } else {
                        mdContent.push(`${file.file_name} (无法生成预览链接)\n`);
                    }
                }
            }
            // === 处理 `files_v2` 部分结束 ===
        }

        return mdContent.join('\n');
    }

    // 调整Markdown标题级别
    function adjustHeadingLevel(text, increaseLevel = 2) {
        const codeBlockPattern = /```[\s\S]*?```/g;
        let segments = [];
        let match;

        // 提取代码块,并用占位符替代
        let lastIndex = 0;
        while ((match = codeBlockPattern.exec(text)) !== null) {
            segments.push(text.substring(lastIndex, match.index));
            segments.push(match[0]); // 保留代码块原样
            lastIndex = codeBlockPattern.lastIndex;
        }
        segments.push(text.substring(lastIndex));

        // 调整标题级别
        segments = segments.map(segment => {
            if (segment.startsWith('```')) {
                return segment; // 保留代码块原样
            } else {
                let lines = segment.split('\n');
                lines = lines.map(line => {
                    if (line.trim().startsWith('#')) {
                        const currentLevel = (line.match(/^#+/) || [''])[0].length;
                        return '#'.repeat(currentLevel + increaseLevel) + line.slice(currentLevel);
                    }
                    return line;
                });
                return lines.join('\n');
            }
        });

        return segments.join('');
    }

    // 处理消息内容,提取纯文本并处理LaTeX公式
    function processContent(content) {
        if (Array.isArray(content)) {
            let textParts = [];
            for (const item of content) {
                if (item.type === 'text') {
                    let text = item.text || '';
                    text = processLatex(text);
                    text = text.replace(/(?<!\n)(\n\| .*? \|\n\|[-| ]+\|\n(?:\| .*? \|\n)+)/g, '\n$1'); // 在表格前插入一个空行
                    textParts.push(text);
                }
            }
            return textParts.join('\n');
        }
        return String(content);
    }

    // 处理LaTeX公式
    function processLatex(text) {
        // 区分行内公式和独立公式
        text = text.replace(/\$\$(.+?)\$\$/gs, (match, formula) => {
            if (formula.includes('\n')) {
                // 这是独立公式
                return `$$${formula}$$`;
            } else {
                // 这是行内公式
                return `$${formula}$`;
            }
        });
        return text;
    }

    // 监听 fetch 请求
    const originalFetch = window.fetch;
    window.fetch = async function(...args) {
        const response = await originalFetch.apply(this, args);
        const url = args[0];

        log.info(`捕获到 fetch 请求: ${url}`);

        if (targetUrlPattern.test(url)) {
            try {
                log.info(`匹配到目标 URL: ${url}`);
                const clonedResponse = response.clone();
                clonedResponse.text().then(text => {
                    processTargetResponse(text, url);
                }).catch(e => {
                    log.error('解析fetch响应时出错:', e);
                });
            } catch (e) {
                log.error('克隆fetch响应时出错:', e);
            }
        }
        return response;
    };

    // 页面加载完成后立即创建按钮
    window.addEventListener('load', function() {
        createDownloadButtons();

        // 使用 MutationObserver 确保按钮始终存在
        const observer = new MutationObserver(() => {
            if (!document.getElementById('downloadJsonButton') || !document.getElementById('downloadMdButton')) {
                log.info('检测到按钮丢失,正在重新创建...');
                createDownloadButtons();
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        log.info('Claude 保存脚本已启动');
    });
})();