您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export Claude conversations using API
当前为
// ==UserScript== // @name Claude Exporter 0.9+ // @namespace http://tampermonkey.net/ // @version 0.9+ // @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 // ============================================= /** * Generates timestamp in format YYYYMMDDHHMMSS for file naming * @returns {string} Formatted timestamp */ 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'); } /** * Sanitizes filename by removing invalid characters and limiting length * @param {string} name - Original filename * @returns {string} Sanitized filename safe for file system */ function sanitizeFileName(name) { return name.replace(/[\\/:*?"<>|]/g, '_') .replace(/\s+/g, '_') .replace(/__+/g, '_') .replace(/^_+|_+$/g, '') .slice(0, 100); } /** * Downloads content as a file using browser's download functionality * @param {string} filename - Name of the file to download * @param {string} content - Content to save in the file */ 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); } /** * Shows temporary notification to the user * @param {string} message - Message to display * @param {string} type - Type of notification (info, success, error) */ 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 // ============================================= /** * Extracts conversation ID from current URL * @returns {string|null} Conversation ID or null if not found */ function getConversationId() { const match = window.location.pathname.match(/\/chat\/([^/?]+)/); return match ? match[1] : null; } /** * Gets organization ID from browser cookies * @returns {string} Organization ID * @throws {Error} If organization ID not found */ 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'); } /** * Fetches conversation data from Claude API * @returns {Promise<Object>} Complete conversation data including messages and metadata */ 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 // ============================================= /** * Recursively extracts text content from nested content structures * @param {Object} content - Content object to process * @returns {Promise<Array<string>>} Array of text pieces found */ 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 // ============================================= /** * Extracts all artifacts from conversation data and organizes them by ID * @param {Object} conversationData - Complete conversation data * @returns {Map} Map of artifact ID to array of artifact versions */ 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; } /** * Applies update command to previous content by replacing old_str with new_str * @param {string} previousContent - Content before update * @param {string} oldStr - String to be replaced * @param {string} newStr - String to replace with * @returns {string} Updated content */ 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); // Try to find similar strings for debugging 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; } /** * Builds complete artifact versions by applying updates sequentially * @param {Map} artifacts - Raw artifacts from extractArtifacts() * @returns {Map} Map of artifact ID to processed versions with full content */ 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); // Create more informative change description 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}"`; // Add information about character count changes 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; } // ============================================= // VERSION TRACKING FUNCTIONS // ============================================= /** * Builds version information for messages with alternatives (same parent) * @param {Array} messages - Array of chat messages * @returns {Map} Map of message UUID to version info {version, total} */ function buildVersionInfo(messages) { const versionInfo = new Map(); // Group messages by parent_message_uuid const parentGroups = new Map(); messages.forEach(message => { if (message.parent_message_uuid) { if (!parentGroups.has(message.parent_message_uuid)) { parentGroups.set(message.parent_message_uuid, []); } parentGroups.get(message.parent_message_uuid).push(message); } }); // Process groups with more than one message (alternatives) parentGroups.forEach((siblings, parentUuid) => { if (siblings.length > 1) { // Sort by created_at to determine version numbers siblings.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); siblings.forEach((message, index) => { versionInfo.set(message.uuid, { version: index + 1, total: siblings.length }); }); } }); return versionInfo; } // ============================================= // EXPORT FUNCTIONS // ============================================= /** * Generates markdown content for the entire conversation * @param {Object} conversationData - Complete conversation data from API * @returns {string} Formatted markdown content */ function generateConversationMarkdown(conversationData) { let markdown = ''; // Header with conversation metadata 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`; // Build version info for messages with alternatives const versionInfo = buildVersionInfo(conversationData.chat_messages); // Process each message 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`; // Add version info if this message has alternatives if (versionInfo.has(message.uuid)) { const info = versionInfo.get(message.uuid); markdown += `*Version:* ${info.version} of ${info.total}\n`; } markdown += `\n`; // Process message content 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; } /** * Exports conversation with artifacts (all versions or final versions only) * @param {boolean} finalVersionsOnly - If true, exports only final artifact versions */ 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`; // Add change information 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'); } } /** * Exports only the conversation without any artifacts */ async function exportConversationOnly() { try { showNotification('Fetching conversation data...', 'info'); const conversationData = await getConversationData(); const timestamp = generateTimestamp(); const conversationId = conversationData.uuid; const safeTitle = sanitizeFileName(conversationData.name); // Export only main conversation const conversationMarkdown = generateConversationMarkdown(conversationData); const conversationFilename = `${timestamp}_${conversationId}__${safeTitle}.md`; downloadFile(conversationFilename, conversationMarkdown); showNotification('Conversation exported successfully!', 'success'); } catch (error) { console.error('Export failed:', error); showNotification(`Export failed: ${error.message}`, 'error'); } } // ============================================= // INITIALIZATION // ============================================= /** * Initializes the script and registers menu commands */ function init() { console.log('[Claude API Exporter] Initializing...'); // Register menu commands GM_registerMenuCommand('Export Conversation Only', exportConversationOnly); GM_registerMenuCommand('Export Conversation + All Artifact Versions', () => exportConversation(false)); } // Start when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();