Greasy Fork

Greasy Fork is available in English.

Claude Exporter 0.8

Export Claude conversations using API

当前为 2025-07-09 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Claude Exporter 0.8
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  Export Claude conversations using API
// @author       MRL
// @match        https://claude.ai/chat/*
// @grant        GM_registerMenuCommand
// @grant        GM_download
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Utility functions
    function generateTimestamp() {
        const now = new Date();
        return now.getFullYear() +
               String(now.getMonth() + 1).padStart(2, '0') +
               String(now.getDate()).padStart(2, '0') +
               String(now.getHours()).padStart(2, '0') +
               String(now.getMinutes()).padStart(2, '0') +
               String(now.getSeconds()).padStart(2, '0');
    }

    function sanitizeFileName(name) {
        return name.replace(/[\\/:*?"<>|]/g, '_')
                  .replace(/\s+/g, '_')
                  .replace(/__+/g, '_')
                  .replace(/^_+|_+$/g, '')
                  .slice(0, 100);
    }

    function downloadFile(filename, content) {
        const blob = new Blob([content], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const link = document.createElement('a');
        link.href = url;
        link.download = filename;
        link.click();
        setTimeout(() => {
            URL.revokeObjectURL(url);
        }, 100);
    }

    function showNotification(message, type = "info") {
        const notification = document.createElement('div');
        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 15px 20px;
            border-radius: 5px;
            color: white;
            font-family: system-ui, -apple-system, sans-serif;
            font-size: 14px;
            z-index: 10000;
            max-width: 400px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
        `;

        if (type === "error") {
            notification.style.backgroundColor = '#f44336';
        } else if (type === "success") {
            notification.style.backgroundColor = '#4CAF50';
        } else {
            notification.style.backgroundColor = '#2196F3';
        }

        notification.textContent = message;
        document.body.appendChild(notification);

        setTimeout(() => {
            if (notification.parentNode) {
                notification.parentNode.removeChild(notification);
            }
        }, 5000);
    }

    // API functions
    function getConversationId() {
        const match = window.location.pathname.match(/\/chat\/([^/?]+)/);
        return match ? match[1] : null;
    }

    function getOrgId() {
        const cookies = document.cookie.split(';');
        for (const cookie of cookies) {
            const [name, value] = cookie.trim().split('=');
            if (name === 'lastActiveOrg') {
                return value;
            }
        }
        throw new Error('Could not find organization ID');
    }

    async function getConversationData() {
        const conversationId = getConversationId();
        if (!conversationId) {
            throw new Error('Not in a conversation');
        }

        const orgId = getOrgId();
        const response = await fetch(`/api/organizations/${orgId}/chat_conversations/${conversationId}?tree=true&rendering_mode=messages&render_all_tools=true`);

        if (!response.ok) {
            throw new Error(`API request failed: ${response.status}`);
        }

        return await response.json();
    }

    // Text processing functions
    async function getTextFromContent(content) {
        let textPieces = [];

        if (content.text) {
            textPieces.push(content.text);
        }
        if (content.input) {
            textPieces.push(JSON.stringify(content.input));
        }
        if (content.content) {
            if (Array.isArray(content.content)) {
                for (const nestedContent of content.content) {
                    textPieces = textPieces.concat(await getTextFromContent(nestedContent));
                }
            } else if (typeof content.content === 'object') {
                textPieces = textPieces.concat(await getTextFromContent(content.content));
            }
        }
        return textPieces;
    }

    // Artifact processing functions
    function extractArtifacts(conversationData) {
        const artifacts = new Map(); // Map<artifactId, Array<{version, command, uuid, content, title, old_str, new_str}>>

        conversationData.chat_messages.forEach(message => {
            message.content.forEach(content => {
                if (content.type === 'tool_use' && content.name === 'artifacts' && content.input) {
                    const input = content.input;
                    const artifactId = input.id;

                    if (!artifacts.has(artifactId)) {
                        artifacts.set(artifactId, []);
                    }

                    const versions = artifacts.get(artifactId);
                    versions.push({
                        version: versions.length + 1,
                        command: input.command,
                        uuid: input.version_uuid,
                        content: input.content || '',
                        old_str: input.old_str || '',
                        new_str: input.new_str || '',
                        title: input.title || `Artifact ${artifactId}`,
                        timestamp: message.created_at
                    });
                }
            });
        });

        return artifacts;
    }

    // Artifact processing functions for command update
    function applyUpdate(previousContent, oldStr, newStr) {
        if (!previousContent || !oldStr) {
            console.warn('Cannot apply update: missing previousContent or oldStr');
            return previousContent || '';
        }

        // Apply the string replacement
        const updatedContent = previousContent.replace(oldStr, newStr);

        if (updatedContent === previousContent) {
            console.warn('Update did not change content - old string not found');
            console.warn('Looking for:', oldStr.substring(0, 100) + '...');
            console.warn('In content length:', previousContent.length);
            
            // Попробуем найти похожие строки для отладки
            const lines = previousContent.split('\n');
            const oldLines = oldStr.split('\n');
            if (oldLines.length > 0) {
                const firstOldLine = oldLines[0].trim();
                const foundLine = lines.find(line => line.includes(firstOldLine));
                if (foundLine) {
                    console.warn('Found similar line:', foundLine);
                }
            }
        }

        return updatedContent;
    }

    function buildArtifactVersions(artifacts) {
        const processedArtifacts = new Map();

        artifacts.forEach((versions, artifactId) => {
            const processedVersions = [];
            let currentContent = '';

            versions.forEach((version, index) => {
                let changeDescription = '';
                
                switch (version.command) {
                    case 'create':
                        currentContent = version.content;
                        changeDescription = 'Created';
                        break;
                    case 'rewrite':
                        currentContent = version.content;
                        changeDescription = 'Rewritten';
                        break;
                    case 'update':
                        const oldContent = currentContent;
                        currentContent = applyUpdate(currentContent, version.old_str, version.new_str);
                        
                        // Создаем более информативное описание изменений
                        const oldPreview = version.old_str ? version.old_str.substring(0, 100) + '...' : '';
                        const newPreview = version.new_str ? version.new_str.substring(0, 100) + '...' : '';
                        changeDescription = `Updated: "${oldPreview}" → "${newPreview}"`;
                        
                        // Добавляем информацию о том, сколько символов изменилось
                        const oldLength = oldContent.length;
                        const newLength = currentContent.length;
                        const lengthDiff = newLength - oldLength;
                        if (lengthDiff > 0) {
                            changeDescription += ` (+${lengthDiff} chars)`;
                        } else if (lengthDiff < 0) {
                            changeDescription += ` (${lengthDiff} chars)`;
                        }
                        break;
                    default:
                        console.warn(`Unknown command: ${version.command}`);
                        break;
                }

                processedVersions.push({
                    ...version,
                    fullContent: currentContent,
                    changeDescription: changeDescription
                });
            });

            processedArtifacts.set(artifactId, processedVersions);
        });

        return processedArtifacts;
    }

    // Export functions
    function generateConversationMarkdown(conversationData) {
        let markdown = '';

        // Header
        markdown += `# ${conversationData.name}\n\n`;
        markdown += `*URL: https://claude.ai/chat/${conversationData.uuid} *\n`;
        
        // Project info (if available)
        if (conversationData.project) {
            markdown += `*Project:* [${conversationData.project.name}] (https://claude.ai/project/${conversationData.project.uuid})\n`;
        }
        
        markdown += `*Сreated: ${conversationData.created_at}*\n`;
        markdown += `*Updated: ${conversationData.updated_at}*\n`;
        markdown += `*Exported on: ${new Date().toLocaleString()}*\n`;
        
        if (conversationData.model) {
            markdown += `*Model: ${conversationData.model}*\n`;
        }
        
        markdown += `\n`;
        
        // Messages
        conversationData.chat_messages.forEach(message => {
            const role = message.sender === 'human' ? 'Human' : 'Claude';
            markdown += `## ${role}\n`;
            markdown += `*UUID:* \`${message.uuid}\`\n`;
            markdown += `*Created:* ${message.created_at}\n\n`;

            message.content.forEach(content => {
                if (content.type === 'text') {
                    markdown += content.text + '\n\n';
                } else if (content.type === 'tool_use' && content.name === 'artifacts') {
                    const input = content.input;
                    markdown += `**Artifact Created:** ${input.title}\n`;
                    markdown += `*ID:* \`${input.id}\`\n`;
                    markdown += `*Command:* \`${input.command}\`\n\n`;
                } else if (content.type === 'thinking') {
                    if (content.thinking) {
                        markdown += `*[Claude thinking...]*\n\n`;
                        markdown += `<details>\n<summary>Thinking process</summary>\n\n`;
                        markdown += content.thinking + '\n\n';
                        markdown += `</details>\n\n`;
                    } else {
                        markdown += `*[Claude thinking...]*\n\n`;
                    }
                }
            });
            
            // Process attachments if present
            if (message.attachments && message.attachments.length > 0) {
                message.attachments.forEach(attachment => {
                    markdown += `**Attachment:** ${attachment.file_name}\n`;
                    markdown += `*ID:* \`${attachment.id}\`\n\n`;
                    
                    if (attachment.extracted_content) {
                        markdown += `<details>\n<summary>File content</summary>\n\n`;
                        markdown += '```\n';
                        markdown += attachment.extracted_content + '\n';
                        markdown += '```\n\n';
                        markdown += `</details>\n\n`;
                    }
                });
            }
        });

        return markdown;
    }

    async function exportConversation(finalVersionsOnly = false) {
        try {
            showNotification('Fetching conversation data...', 'info');

            const conversationData = await getConversationData();
            const timestamp = generateTimestamp();
            const conversationId = conversationData.uuid;
            const safeTitle = sanitizeFileName(conversationData.name);

            // Export main conversation
            const conversationMarkdown = generateConversationMarkdown(conversationData);
            const conversationFilename = `${timestamp}_${conversationId}__${safeTitle}.md`;
            downloadFile(conversationFilename, conversationMarkdown);

            // Extract and process artifacts
            const rawArtifacts = extractArtifacts(conversationData);
            const processedArtifacts = buildArtifactVersions(rawArtifacts);

            if (processedArtifacts.size === 0) {
                showNotification('No artifacts found in conversation', 'info');
                return;
            }

            let totalExported = 0;

            // Export artifacts
            processedArtifacts.forEach((versions, artifactId) => {
                const versionsToExport = finalVersionsOnly ?
                    [versions[versions.length - 1]] : // Only last version
                    versions; // All versions

                versionsToExport.forEach(version => {
                    const safeArtifactTitle = sanitizeFileName(version.title);
                    const filename = `${timestamp}_${conversationId}_${artifactId}_v${version.version}_${safeArtifactTitle}.md`;

                    let content = `# ${version.title}\n\n`;
                    content += `*Artifact ID:* \`${artifactId}\`\n`;
                    content += `*Version:* ${version.version}\n`;
                    content += `*Command:* \`${version.command}\`\n`;
                    content += `*UUID:* \`${version.uuid}\`\n`;
                    content += `*Created:* ${version.timestamp}\n`;
                    
                    // Добавляем информацию об изменениях
                    if (version.changeDescription) {
                        content += `*Change:* ${version.changeDescription}\n`;
                    }
                    
                    content += '\n---\n\n';
                    content += version.fullContent;

                    downloadFile(filename, content);
                    totalExported++;
                });
            });

            const mode = finalVersionsOnly ? 'final versions' : 'all versions';
            showNotification(`Export completed! Downloaded conversation + ${totalExported} artifacts (${mode})`, 'success');

        } catch (error) {
            console.error('Export failed:', error);
            showNotification(`Export failed: ${error.message}`, 'error');
        }
    }

    // Initialize
    function init() {
        console.log('[Claude API Exporter] Initializing...');

        // Register menu commands
        GM_registerMenuCommand('Export Conversation + Final Artifact Versions', () => exportConversation(true));
        GM_registerMenuCommand('Export Conversation + All Artifact Versions', () => exportConversation(false));
    }

    // Start when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();