您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Mass export library for Claude API Exporter
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/543706/1639691/Claude%20Mass%20Exporter%20Library.js
// ==UserScript== // @name Claude Mass Exporter Library // @namespace http://tampermonkey.net/ // @version 4.0 // @description Mass export library for Claude API Exporter 4.0+ // @author MRL // @license MIT // ==/UserScript== (function() { 'use strict'; // ============================================= // DEPENDENCY CHECK AND UTILITIES // ============================================= function checkDependency() { const mainScript = window.claudeExporter; if (typeof mainScript === 'undefined') { console.error('[Claude Mass Exporter] Claude API Exporter not found!'); return false; } // Initialize utilities after successful dependency check initializeUtilities(); return true; } // Utilities from main script (initialized after main script loads) let showNotification, sanitizeFileName, formatDate, ArchiveManager; function initializeUtilities() { const mainScript = window.claudeExporter; showNotification = mainScript.showNotification; sanitizeFileName = mainScript.sanitizeFileName; formatDate = mainScript.formatDate; ArchiveManager = mainScript.ArchiveManager; } function getOrgId() { const match = document.cookie.match(/lastActiveOrg=([^;]+)/); if (!match) throw new Error('Could not find organization ID'); return match[1]; } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function formatDateString(dateString) { if (!dateString) return 'N/A'; return new Date(dateString).toLocaleDateString(); } // ============================================= // CONTEXT AND ROUTING // ============================================= function getCurrentContext() { const path = window.location.pathname; if (path === '/projects') return { type: 'projects' }; if (path.match(/^\/project\/[^/]+$/)) return { type: 'project', projectId: path.split('/')[2] }; if (path === '/recents') return { type: 'recents' }; if (path.match(/^\/chat\/[^/]+$/)) return { type: 'chat' }; return { type: 'unknown' }; } function getExportChatFolderTemplate(exportType, settings) { // Determines the correct chat folder template based on export type switch (exportType) { case 'projects': return settings.massExportProjectsChatFolderName; case 'project': case 'recents': return settings.massExportSingleChatFolderName; default: return settings.massExportProjectsChatFolderName; } } // ============================================= // SORTING AND PERSISTENCE // ============================================= function sortItemsByDate(items, sortBy = 'updated', direction = 'desc') { return [...items].sort((a, b) => { const dateA = new Date(sortBy === 'created' ? a.created_at : a.updated_at); const dateB = new Date(sortBy === 'created' ? b.created_at : b.updated_at); return direction === 'desc' ? dateB - dateA : dateA - dateB; }); } function saveSortSettings(sortBy, sortDirection, isProject = false) { const prefix = isProject ? 'claudeProject' : 'claude'; localStorage.setItem(`${prefix}SortBy`, sortBy); localStorage.setItem(`${prefix}SortDirection`, sortDirection); } function loadSortSettings(isProject = false) { const prefix = isProject ? 'claudeProject' : 'claude'; return { sortBy: localStorage.getItem(`${prefix}SortBy`) || 'updated', sortDirection: localStorage.getItem(`${prefix}SortDirection`) || 'desc' }; } // ============================================= // API FUNCTIONS // ============================================= async function getAllProjects() { const orgId = getOrgId(); const response = await fetch(`/api/organizations/${orgId}/projects?include_harmony_projects=true&creator_filter=is_creator`); if (!response.ok) throw new Error(`Failed to fetch projects: ${response.status}`); return await response.json(); } async function getProjectConversations(projectUuid) { const orgId = getOrgId(); const response = await fetch(`/api/organizations/${orgId}/projects/${projectUuid}/conversations`); if (!response.ok) throw new Error(`Failed to fetch project conversations: ${response.status}`); return await response.json(); } async function getAllRecentConversations() { const orgId = getOrgId(); const response = await fetch(`/api/organizations/${orgId}/chat_conversations?limit=10000`); if (!response.ok) throw new Error(`Failed to fetch recent conversations: ${response.status}`); return await response.json(); } // Use getConversationData from main script async function getConversationData(conversationId) { return await window.claudeExporter.getConversationData(conversationId); } // ============================================= // SELECTION UI - HTML CREATION // ============================================= function createSelectionModal(title, isProjectSelection, sortSettings, items) { return ` <div class="claude-selection-overlay"> <div class="claude-selection-modal"> <div class="claude-selection-header"> <h3>📋 ${title}</h3> <button class="claude-selection-close" type="button">×</button> </div> <div class="claude-selection-content"> ${createControlsSection(isProjectSelection, sortSettings)} <div class="claude-selection-list"> ${isProjectSelection ? createProjectList(items) : createSimpleList(items, sortSettings)} </div> </div> <div class="claude-selection-footer"> <button class="claude-btn claude-btn-secondary" type="button" id="cancelSelection">Cancel</button> <button class="claude-btn claude-btn-primary" type="button" id="exportSelected" disabled>Export Selected</button> </div> </div> </div> `; } function createControlsSection(isProjectSelection, sortSettings) { const selectAllId = isProjectSelection ? 'selectAllProjects' : 'selectAll'; const selectAllText = isProjectSelection ? 'Select All Projects' : 'Select All'; const sortLabel = isProjectSelection ? 'Sort chats:' : 'Sort by:'; const sortById = isProjectSelection ? 'projectChatSortBy' : 'sortBy'; const sortDirId = isProjectSelection ? 'projectChatSortDirection' : 'sortDirection'; const countText = isProjectSelection ? '0 chats selected' : '0 selected'; return ` <div class="claude-selection-controls"> <button class="claude-btn claude-btn-secondary" type="button" id="${selectAllId}">${selectAllText}</button> <button class="claude-btn claude-btn-secondary" type="button" id="selectNone">Select None</button> <div class="claude-sort-controls"> <label class="claude-sort-label">${sortLabel}</label> <select id="${sortById}" class="claude-sort-select"> <option value="updated" ${sortSettings.sortBy === 'updated' ? 'selected' : ''}>Updated</option> <option value="created" ${sortSettings.sortBy === 'created' ? 'selected' : ''}>Created</option> </select> <select id="${sortDirId}" class="claude-sort-select"> <option value="desc" ${sortSettings.sortDirection === 'desc' ? 'selected' : ''}>Newest first</option> <option value="asc" ${sortSettings.sortDirection === 'asc' ? 'selected' : ''}>Oldest first</option> </select> </div> <span class="claude-selection-count">${countText}</span> </div> `; } function createProjectList(items) { return items.map((project, index) => ` <div class="claude-project-item" data-project-index="${index}"> <div class="claude-project-header"> <button class="claude-project-toggle" type="button" data-project="${index}"> <span class="claude-toggle-icon">▶</span> </button> <input type="checkbox" id="project-${index}" class="claude-project-checkbox" data-project="${index}"> <label for="project-${index}" class="claude-project-label"> <div class="claude-project-name">📁 ${project.name}</div> <div class="claude-project-meta">Updated: ${formatDateString(project.updated_at)} | Created: ${formatDateString(project.created_at)} | <span class="chat-count">Click to load chats</span></div> </label> </div> <div class="claude-project-chats" id="chats-${index}" style="display: none;"> <div class="claude-loading">Loading conversations...</div> </div> </div> `).join(''); } function createSimpleList(items, sortSettings) { return sortItemsByDate(items, sortSettings.sortBy, sortSettings.sortDirection).map((item, index) => ` <div class="claude-selection-item"> <input type="checkbox" id="item-${index}" value="${items.indexOf(item)}" class="claude-selection-checkbox"> <label for="item-${index}" class="claude-selection-label"> <div class="claude-selection-name">${item.name}</div> <div class="claude-selection-meta">Updated: ${formatDateString(item.updated_at)} | Created: ${formatDateString(item.created_at)}${item.project?.name ? ` | ${item.project.name}` : ''}</div> </label> </div> `).join(''); } function createChatList(conversations, projectIndex) { return conversations.map((chat, chatIndex) => ` <div class="claude-chat-item"> <input type="checkbox" id="chat-${projectIndex}-${chatIndex}" class="claude-chat-checkbox" data-project="${projectIndex}" data-chat="${chatIndex}"> <label for="chat-${projectIndex}-${chatIndex}" class="claude-chat-label"> <div class="claude-chat-name">💬 ${chat.name}</div> <div class="claude-chat-meta">Updated: ${formatDateString(chat.updated_at)} | Created: ${formatDateString(chat.created_at)}</div> </label> </div> `).join(''); } // ============================================= // SELECTION UI - LOGIC // ============================================= function createSelectionUI(title, items, onExport) { document.getElementById('claude-selection-ui')?.remove(); const isProjectSelection = title.includes('Projects') && items.length > 0 && items[0].uuid; const sortSettings = loadSortSettings(isProjectSelection); const selectionOverlay = document.createElement('div'); selectionOverlay.id = 'claude-selection-ui'; selectionOverlay.innerHTML = createSelectionModal(title, isProjectSelection, sortSettings, items); document.head.insertAdjacentHTML('beforeend', getSelectionStyles()); document.body.appendChild(selectionOverlay); if (isProjectSelection) { setupProjectSelection(items, onExport); } else { setupSimpleSelection(items, onExport); } setupCommonHandlers(); } function setupProjectSelection(items, onExport) { const state = { selectedChats: new Map(), projectChats: new Map(), loadedProjects: new Set() }; items.forEach((_, index) => state.selectedChats.set(index, new Set())); const updateUI = () => { let totalSelectedChats = 0; state.selectedChats.forEach(chatSet => totalSelectedChats += chatSet.size); document.querySelector('.claude-selection-count').textContent = `${totalSelectedChats} chats selected`; document.getElementById('exportSelected').disabled = totalSelectedChats === 0; // Update project checkboxes items.forEach((_, projectIndex) => { const projectCheckbox = document.getElementById(`project-${projectIndex}`); const chats = state.projectChats.get(projectIndex) || []; const selectedChatSet = state.selectedChats.get(projectIndex); if (chats.length === 0) { // Don't change checked state while loading if (!projectCheckbox.checked) { projectCheckbox.indeterminate = false; } } else if (selectedChatSet.size === 0) { projectCheckbox.indeterminate = false; projectCheckbox.checked = false; } else if (selectedChatSet.size === chats.length) { projectCheckbox.indeterminate = false; projectCheckbox.checked = true; } else { projectCheckbox.indeterminate = true; } }); }; const loadProjectChats = async (projectIndex, forceReload = false) => { if (!forceReload && state.loadedProjects.has(projectIndex)) return; const project = items[projectIndex]; const chatsContainer = document.getElementById(`chats-${projectIndex}`); try { const conversations = await getProjectConversations(project.uuid); // Sort conversations based on current settings const sortSettings = loadSortSettings(true); const sortedConversations = sortItemsByDate(conversations, sortSettings.sortBy, sortSettings.sortDirection); state.projectChats.set(projectIndex, sortedConversations); state.loadedProjects.add(projectIndex); // Update count document.querySelector(`[data-project-index="${projectIndex}"] .chat-count`).textContent = `${sortedConversations.length} chats`; // Generate chat list chatsContainer.innerHTML = createChatList(sortedConversations, projectIndex); // Add chat checkbox handlers chatsContainer.querySelectorAll('.claude-chat-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const projectIdx = parseInt(e.target.dataset.project); const chatIdx = parseInt(e.target.dataset.chat); const selectedChatSet = state.selectedChats.get(projectIdx); if (e.target.checked) { selectedChatSet.add(chatIdx); } else { selectedChatSet.delete(chatIdx); } updateUI(); }); }); } catch (error) { console.error(`Failed to load chats for project ${project.name}:`, error); chatsContainer.innerHTML = '<div class="claude-error">❌ Failed to load conversations</div>'; document.querySelector(`[data-project-index="${projectIndex}"] .chat-count`).textContent = 'Error'; } }; // Setup all event handlers setupProjectEventHandlers(state, items, updateUI, loadProjectChats, onExport); updateUI(); } function setupProjectEventHandlers(state, items, updateUI, loadProjectChats, onExport) { // Toggle handlers document.querySelectorAll('.claude-project-toggle').forEach(toggle => { toggle.addEventListener('click', async (e) => { e.stopPropagation(); const projectIndex = parseInt(e.currentTarget.dataset.project); const chatsContainer = document.getElementById(`chats-${projectIndex}`); const toggleIcon = e.currentTarget.querySelector('.claude-toggle-icon'); if (chatsContainer.style.display === 'none') { chatsContainer.style.display = 'block'; toggleIcon.classList.add('expanded'); await loadProjectChats(projectIndex, false); } else { chatsContainer.style.display = 'none'; toggleIcon.classList.remove('expanded'); } }); }); // Project checkbox handlers document.querySelectorAll('.claude-project-checkbox').forEach(checkbox => { checkbox.addEventListener('change', async (e) => { const projectIndex = parseInt(e.target.dataset.project); const selectedChatSet = state.selectedChats.get(projectIndex); if (e.target.checked) { // Load chats if needed if (!state.loadedProjects.has(projectIndex)) { e.target.disabled = true; await loadProjectChats(projectIndex, false); e.target.disabled = false; } // Select all chats const chats = state.projectChats.get(projectIndex) || []; chats.forEach((_, chatIndex) => { selectedChatSet.add(chatIndex); const chatCheckbox = document.getElementById(`chat-${projectIndex}-${chatIndex}`); if (chatCheckbox) chatCheckbox.checked = true; }); } else { // Deselect all chats selectedChatSet.clear(); const chats = state.projectChats.get(projectIndex) || []; chats.forEach((_, chatIndex) => { const chatCheckbox = document.getElementById(`chat-${projectIndex}-${chatIndex}`); if (chatCheckbox) chatCheckbox.checked = false; }); } updateUI(); }); }); // Control buttons document.getElementById('selectAllProjects').addEventListener('click', async () => { for (let i = 0; i < items.length; i++) { const projectCheckbox = document.getElementById(`project-${i}`); if (!projectCheckbox.checked) { projectCheckbox.checked = true; projectCheckbox.dispatchEvent(new Event('change')); } } }); document.getElementById('selectNone').addEventListener('click', () => { state.selectedChats.forEach((chatSet, projectIndex) => { chatSet.clear(); document.getElementById(`project-${projectIndex}`).checked = false; const chats = state.projectChats.get(projectIndex) || []; chats.forEach((_, chatIndex) => { const chatCheckbox = document.getElementById(`chat-${projectIndex}-${chatIndex}`); if (chatCheckbox) chatCheckbox.checked = false; }); }); updateUI(); }); // Sort handlers const resortAllProjects = async () => { for (const projectIndex of state.loadedProjects) { state.selectedChats.get(projectIndex).clear(); await loadProjectChats(projectIndex, true); } updateUI(); }; // Auto-sort when selection changes document.getElementById('projectChatSortBy').addEventListener('change', (e) => { saveSortSettings(e.target.value, document.getElementById('projectChatSortDirection').value, true); resortAllProjects(); }); document.getElementById('projectChatSortDirection').addEventListener('change', (e) => { saveSortSettings(document.getElementById('projectChatSortBy').value, e.target.value, true); resortAllProjects(); }); // Export handler document.getElementById('exportSelected').addEventListener('click', () => { const selectedData = []; state.selectedChats.forEach((chatSet, projectIndex) => { if (chatSet.size > 0) { const project = items[projectIndex]; const chats = state.projectChats.get(projectIndex) || []; const selectedProjectChats = Array.from(chatSet).map(chatIndex => chats[chatIndex]); selectedData.push({ project: project, chats: selectedProjectChats }); } }); closeModal(); onExport(selectedData); }); } function setupSimpleSelection(items, onExport) { const selectedItems = new Set(); const updateUI = () => { const count = selectedItems.size; document.querySelector('.claude-selection-count').textContent = `${count} selected`; document.getElementById('exportSelected').disabled = count === 0; }; // Checkbox handlers document.querySelectorAll('.claude-selection-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const index = parseInt(e.target.value); if (e.target.checked) { selectedItems.add(index); } else { selectedItems.delete(index); } updateUI(); }); }); // Control buttons document.getElementById('selectAll').addEventListener('click', () => { document.querySelectorAll('.claude-selection-checkbox').forEach(checkbox => { checkbox.checked = true; selectedItems.add(parseInt(checkbox.value)); }); updateUI(); }); document.getElementById('selectNone').addEventListener('click', () => { document.querySelectorAll('.claude-selection-checkbox').forEach(checkbox => { checkbox.checked = false; }); selectedItems.clear(); updateUI(); }); // Sort handlers const resortList = () => { const sortBy = document.getElementById('sortBy').value; const sortDirection = document.getElementById('sortDirection').value; const sortedItems = sortItemsByDate(items, sortBy, sortDirection); const listContainer = document.querySelector('.claude-selection-list'); listContainer.innerHTML = sortedItems.map((item, index) => ` <div class="claude-selection-item"> <input type="checkbox" id="item-${index}" value="${items.indexOf(item)}" class="claude-selection-checkbox"> <label for="item-${index}" class="claude-selection-label"> <div class="claude-selection-name">${item.name}</div> <div class="claude-selection-meta">Updated: ${formatDateString(item.updated_at)} | Created: ${formatDateString(item.created_at)}${item.project?.name ? ` | ${item.project.name}` : ''}</div> </label> </div> `).join(''); // Re-setup checkbox handlers document.querySelectorAll('.claude-selection-checkbox').forEach(checkbox => { checkbox.addEventListener('change', (e) => { const index = parseInt(e.target.value); if (e.target.checked) { selectedItems.add(index); } else { selectedItems.delete(index); } updateUI(); }); }); }; // Auto-sort when selection changes document.getElementById('sortBy').addEventListener('change', (e) => { saveSortSettings(e.target.value, document.getElementById('sortDirection').value, false); resortList(); }); document.getElementById('sortDirection').addEventListener('change', (e) => { saveSortSettings(document.getElementById('sortBy').value, e.target.value, false); resortList(); }); // Export handler document.getElementById('exportSelected').addEventListener('click', () => { const selected = Array.from(selectedItems).map(index => items[index]); closeModal(); onExport(selected); }); updateUI(); } function setupCommonHandlers() { document.querySelector('.claude-selection-close').addEventListener('click', closeModal); document.getElementById('cancelSelection').addEventListener('click', closeModal); document.querySelector('.claude-selection-overlay').addEventListener('click', (e) => { if (e.target.classList.contains('claude-selection-overlay')) closeModal(); }); } function closeModal() { document.getElementById('claude-selection-ui')?.remove(); document.getElementById('claude-selection-styles')?.remove(); } // ============================================= // PROGRESS UI // ============================================= function createProgressUI(title) { document.getElementById('claude-mass-export-progress')?.remove(); const progressOverlay = document.createElement('div'); progressOverlay.id = 'claude-mass-export-progress'; progressOverlay.innerHTML = ` <div class="claude-progress-overlay"> <div class="claude-progress-modal"> <div class="claude-progress-header"> <h3>📦 ${title}</h3> <button class="claude-progress-close" type="button">×</button> </div> <div class="claude-progress-content"> <div class="claude-progress-bar"> <div class="claude-progress-fill" style="width: 0%"></div> </div> <div class="claude-progress-text">Initializing...</div> <div class="claude-progress-details"></div> </div> </div> </div> `; document.head.insertAdjacentHTML('beforeend', getProgressStyles()); document.body.appendChild(progressOverlay); let cancelled = false; const closeProgressModal = () => { cancelled = true; document.getElementById('claude-mass-export-progress')?.remove(); document.getElementById('claude-progress-styles')?.remove(); }; document.querySelector('.claude-progress-close').addEventListener('click', closeProgressModal); document.querySelector('.claude-progress-overlay').addEventListener('click', (e) => { if (e.target.classList.contains('claude-progress-overlay')) closeProgressModal(); }); return { updateProgress: (current, total, text, details = '') => { if (cancelled) return false; const percentage = Math.round((current / total) * 100); document.querySelector('.claude-progress-fill').style.width = `${percentage}%`; document.querySelector('.claude-progress-text').textContent = text; document.querySelector('.claude-progress-details').textContent = details; if (current === total) { setTimeout(closeProgressModal, 2000); } return true; }, isCancelled: () => cancelled, close: closeProgressModal }; } // ============================================= // EXPORT ORCHESTRATION // ============================================= async function exportSingleConversation(conversationData, exportMode = 'final', archiveManager = null, projectFolderName = '', useChatFolders = false, exportType = 'projects', mainBranchOnly = false) { const mainScript = window.claudeExporter; if (!mainScript) throw new Error('Main exporter not available'); // Determine folder structure const settings = mainScript.loadSettings(); let finalFolderPath = ''; if (archiveManager && projectFolderName) { // Mass export with project folders (only for exporting all projects) if (useChatFolders) { // Project/Chat/ structure const template = getExportChatFolderTemplate('projects', settings); const chatFolderName = mainScript.generateChatFolderName( conversationData, { name: projectFolderName }, template ); finalFolderPath = chatFolderName; } else { // Project/ structure (files directly in project folder) finalFolderPath = projectFolderName; } } else if (archiveManager && useChatFolders) { // Single export with chat folders (for a separate project or recent ones) const template = getExportChatFolderTemplate(exportType, settings); finalFolderPath = mainScript.generateChatFolderName(conversationData, null, template); } // Extract and process artifacts from all branches - use main script's export logic const { branchArtifacts, branchInfo, mainBranchUuids } = mainScript.extractAllArtifacts(conversationData); // Filter data based on export mode and main branch settings let filteredConversationData = conversationData; let filteredBranchArtifacts = branchArtifacts; const needMainBranchFiltering = (mainBranchOnly && !settings.mainBranchOnlyIncludeAllMessages) || (exportMode === 'none' && settings.conversationOnlyArtifactMode === 'main_branch_only'); if (needMainBranchFiltering) { filteredConversationData = { ...conversationData, name: conversationData.name, chat_messages: conversationData.chat_messages.filter(message => mainBranchUuids && mainBranchUuids.has(message.uuid) ) }; } // Filter artifacts if main branch only if (mainBranchOnly) { const filteredMap = new Map(); // Only include artifacts from messages in main branch for (const [branchId, artifactsMap] of branchArtifacts) { const filteredArtifactsMap = new Map(); for (const [artifactId, versions] of artifactsMap) { // Filter versions to only those from main branch messages const mainVersions = versions.filter(version => mainBranchUuids && mainBranchUuids.has(version.messageUuid) ); if (mainVersions.length > 0) { filteredArtifactsMap.set(artifactId, mainVersions); } } if (filteredArtifactsMap.size > 0) { filteredMap.set(branchId, filteredArtifactsMap); } } filteredBranchArtifacts = filteredMap; } // For 'none' mode (conversation only), use conversationOnlyArtifactMode setting - handle conversation-only export if (exportMode === 'none') { let conversationOnlyMode = settings.conversationOnlyArtifactMode; let conversationOnlyBranchArtifacts = filteredBranchArtifacts; let conversationOnlyData = filteredConversationData; // Filter artifacts for main_branch_only mode if (conversationOnlyMode === 'main_branch_only') { conversationOnlyMode = 'all'; conversationOnlyBranchArtifacts = new Map(); // Only include artifacts from messages in main branch for (const [branchId, artifactsMap] of branchArtifacts) { const filteredArtifactsMap = new Map(); for (const [artifactId, versions] of artifactsMap) { // Filter versions to only those from main branch messages const mainVersions = versions.filter(version => mainBranchUuids && mainBranchUuids.has(version.messageUuid) ); if (mainVersions.length > 0) { filteredArtifactsMap.set(artifactId, mainVersions); } } if (filteredArtifactsMap.size > 0) { conversationOnlyBranchArtifacts.set(branchId, filteredArtifactsMap); } } // Use full conversation data if mainBranchOnlyIncludeAllMessages is true if (settings.mainBranchOnlyIncludeAllMessages) { conversationOnlyData = conversationData; } } const conversationMarkdown = mainScript.generateConversationMarkdown(conversationOnlyData, conversationOnlyMode, conversationOnlyBranchArtifacts, branchInfo, mainBranchUuids); let filename = mainScript.generateConversationFilename(conversationData); // Add full path for archive if (finalFolderPath) { filename = `${finalFolderPath}/${filename}`; } if (archiveManager) { // Use full path in filename, no additional folder processing await archiveManager.addFile(filename, conversationMarkdown, false, ''); } else { mainScript.downloadFile(filename, conversationMarkdown); } return 1; } // Determine include mode for conversation markdown - regular export with artifacts - use main script logic let includeMode; if (mainBranchOnly) { includeMode = 'all'; } else if (exportMode === 'latest_per_message') { includeMode = 'latest_per_message'; } else if (exportMode === 'final') { includeMode = 'final'; } else { includeMode = 'all'; } let conversationMarkdown, shouldExportSeparateFiles = false; // Determine behavior based on setting switch (settings.artifactExportMode) { case 'embed': conversationMarkdown = mainScript.generateConversationMarkdown(filteredConversationData, includeMode, filteredBranchArtifacts, branchInfo, mainBranchUuids); break; case 'files': conversationMarkdown = mainScript.generateConversationMarkdown(filteredConversationData, 'none', filteredBranchArtifacts, branchInfo, mainBranchUuids); shouldExportSeparateFiles = true; break; case 'both': conversationMarkdown = mainScript.generateConversationMarkdown(filteredConversationData, includeMode, filteredBranchArtifacts, branchInfo, mainBranchUuids); shouldExportSeparateFiles = true; break; } // Generate filename with full path for archive let finalFilename = mainScript.generateConversationFilename(conversationData); if (finalFolderPath) { finalFilename = `${finalFolderPath}/${finalFilename}`; } // Add conversation file if (archiveManager) { // Use full path in filename, no additional folder processing await archiveManager.addFile(finalFilename, conversationMarkdown, false, ''); } else { mainScript.downloadFile(finalFilename, conversationMarkdown); } let exportedCount = 1; // Conversation file // Export artifacts if needed if (shouldExportSeparateFiles && filteredBranchArtifacts.size > 0) { // For latest per message mode, build set of latest artifact timestamps let latestArtifactTimestamps = new Set(); if (exportMode === 'latest_per_message') { filteredConversationData.chat_messages.forEach(message => { const latestInMessage = new Map(); message.content.forEach(content => { if (content.type === 'tool_use' && content.name === 'artifacts' && content.input) { latestInMessage.set(content.input.id, content); } }); latestInMessage.forEach((content) => { if (content.stop_timestamp) { latestArtifactTimestamps.add(content.stop_timestamp); } }); }); } for (const [branchId, artifactsMap] of filteredBranchArtifacts) { const branchData = branchInfo.find(b => b.branchId === branchId); const branchLabel = branchData ? branchData.branchIndex.toString() : 'unknown'; for (const [artifactId, versions] of artifactsMap) { let versionsToExport = versions; if (exportMode === 'latest_per_message') { versionsToExport = versions.filter(version => latestArtifactTimestamps.has(version.content_stop_timestamp)); } else if (exportMode === 'final' && !mainBranchOnly) { versionsToExport = [versions[versions.length - 1]]; } for (const version of versionsToExport) { if (settings.excludeCanceledArtifacts && version.stop_reason === 'user_canceled') continue; const correctIsMain = mainBranchUuids && mainBranchUuids.has(version.messageUuid); const artifactFilename = mainScript.generateArtifactFilename(version, conversationData, branchLabel, correctIsMain, artifactId); // Add full path for archive let fullArtifactFilename = artifactFilename; if (finalFolderPath) { fullArtifactFilename = `${finalFolderPath}/${artifactFilename}`; } const metadata = mainScript.formatArtifactMetadata(version, artifactId, branchLabel, correctIsMain); let processedContent = version.fullContent; if (version.finalType === 'text/markdown' && settings.removeDoubleNewlinesFromMarkdown) { processedContent = mainScript.processArtifactContent(version.fullContent, version.finalType, true); } const content = metadata ? metadata + '\n' + processedContent : processedContent; if (archiveManager) { // Use full path in filename, no additional folder processing await archiveManager.addFile(fullArtifactFilename, content, false, ''); } else { mainScript.downloadFile(fullArtifactFilename, content); } exportedCount++; } } } } return exportedCount; } async function performMassExport(selectedData, exportMode, mainBranchOnly, exportType, progressTitle) { const progress = createProgressUI(progressTitle); try { const mainScript = window.claudeExporter; const settings = mainScript.loadSettings(); // Determine if we should use archive const useArchive = settings.forceArchiveForMassExport; const useChatFolders = settings.forceChatFoldersForMassExport; let archiveManager = null; if (useArchive) { archiveManager = new ArchiveManager(); await archiveManager.initialize(); } let totalConversations = 0; if (exportType === 'projects') { totalConversations = selectedData.reduce((total, projectData) => total + projectData.chats.length, 0); } else { totalConversations = selectedData.length; } if (totalConversations === 0) { showNotification('No conversations selected for export', 'info'); progress.close(); return; } // Export conversations let currentConversation = 0; let totalExported = 0; if (exportType === 'projects') { for (const projectData of selectedData) { if (progress.isCancelled()) return; const project = projectData.project; const chats = projectData.chats; const projectFolderName = sanitizeFileName(project.name); for (const chat of chats) { if (progress.isCancelled()) return; currentConversation++; progress.updateProgress(currentConversation, totalConversations, `Exporting conversation ${currentConversation}/${totalConversations}`, `Project: ${project.name} | Chat: ${chat.name}`); try { const fullConversationData = await getConversationData(chat.uuid); const exportedCount = await exportSingleConversation(fullConversationData, exportMode, archiveManager, projectFolderName, useChatFolders, 'projects', mainBranchOnly); totalExported += exportedCount; } catch (error) { console.warn(`Failed to export conversation ${chat.name}:`, error); } await delay(200); } } } else { for (const conversation of selectedData) { if (progress.isCancelled()) return; currentConversation++; progress.updateProgress(currentConversation, totalConversations, `Exporting conversation ${currentConversation}/${totalConversations}`, `Chat: ${conversation.name}`); try { const fullConversationData = await getConversationData(conversation.uuid); const exportedCount = await exportSingleConversation(fullConversationData, exportMode, archiveManager, '', useChatFolders, exportType, mainBranchOnly); totalExported += exportedCount; } catch (error) { console.warn(`Failed to export conversation ${conversation.name}:`, error); } await delay(200); } } if (archiveManager && archiveManager.fileCount > 0) { let exportName; if (exportType === 'projects') { exportName = `${selectedData.length} Projects Export`; } else { exportName = `${selectedData.length} Conversations Export`; } const archiveName = mainScript.generateArchiveName({ name: exportName, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), uuid: 'mass-export' }, settings.massExportArchiveName, true, exportType === 'projects' ? 'Projects' : 'Conversations'); await archiveManager.downloadArchive(archiveName); } if (exportType === 'projects') { showNotification(`✅ Mass export completed! Downloaded ${totalExported} files from ${totalConversations} conversations across ${selectedData.length} projects`, 'success'); } else { showNotification(`✅ Export completed! Downloaded ${totalExported} files from ${selectedData.length} conversations`, 'success'); } } catch (error) { console.error('Mass export failed:', error); showNotification(`❌ Mass export failed: ${error.message}`, 'error'); } } // ============================================= // MAIN EXPORT FUNCTIONS // ============================================= async function exportAllProjects(exportMode = 'final', mainBranchOnly = false) { try { showNotification('Fetching projects...', 'info'); const projects = await getAllProjects(); if (projects.length === 0) { showNotification('No projects found to export', 'info'); return; } const projectItems = projects.map(project => ({ ...project, name: project.name })); createSelectionUI('Select Projects to Export', projectItems, async (selectedData) => { const modeText = mainBranchOnly ? 'main branch' : (exportMode === 'none' ? 'conversations only' : exportMode); await performMassExport(selectedData, exportMode, mainBranchOnly, 'projects', `Mass Export - Selected Projects (${modeText})`); }); } catch (error) { console.error('Failed to fetch projects:', error); showNotification(`❌ Failed to fetch projects: ${error.message}`, 'error'); } } async function exportCurrentProject(exportMode = 'final', mainBranchOnly = false) { const context = getCurrentContext(); if (context.type !== 'project') { showNotification('❌ Not in a project page. Please navigate to a project first.', 'error'); return; } try { showNotification('Fetching project conversations...', 'info'); const conversations = await getProjectConversations(context.projectId); if (conversations.length === 0) { showNotification('No conversations found in this project', 'info'); return; } // Sort conversations using saved settings const sortSettings = loadSortSettings(false); const sortedConversations = sortItemsByDate(conversations, sortSettings.sortBy, sortSettings.sortDirection); // Show selection UI const conversationItems = sortedConversations.map(conv => ({ ...conv, name: conv.name })); createSelectionUI('Select Conversations to Export', conversationItems, async (selectedConversations) => { const modeText = mainBranchOnly ? 'main branch' : (exportMode === 'none' ? 'conversations only' : exportMode); await performMassExport(selectedConversations, exportMode, mainBranchOnly, 'project', `Export Selected Conversations (${modeText})`); }); } catch (error) { console.error('Failed to fetch conversations:', error); showNotification(`❌ Failed to fetch conversations: ${error.message}`, 'error'); } } async function exportAllRecentConversations(exportMode = 'final', mainBranchOnly = false) { try { showNotification('Fetching recent conversations...', 'info'); const conversations = await getAllRecentConversations(); if (conversations.length === 0) { showNotification('No recent conversations found', 'info'); return; } const conversationItems = conversations.map(conv => ({ ...conv, name: conv.name })); createSelectionUI('Select Recent Conversations to Export', conversationItems, async (selectedConversations) => { const modeText = mainBranchOnly ? 'main branch' : (exportMode === 'none' ? 'conversations only' : exportMode); await performMassExport(selectedConversations, exportMode, mainBranchOnly, 'recents', `Export Selected Recent Conversations (${modeText})`); }); } catch (error) { console.error('Failed to fetch conversations:', error); showNotification(`❌ Failed to fetch conversations: ${error.message}`, 'error'); } } // ============================================= // STYLES (MINIMAL - ONLY SELECTION SPECIFIC) // ============================================= function getSelectionStyles() { return `<style id="claude-selection-styles"> .claude-selection-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;z-index:99999;font-family:system-ui,-apple-system,sans-serif} .claude-selection-modal{background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,0.3);width:90%;max-width:700px;max-height:80vh;overflow:hidden;display:flex;flex-direction:column} .claude-selection-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;padding:20px 24px;display:flex;align-items:center;justify-content:space-between} .claude-selection-header h3{margin:0;font-size:18px;font-weight:600} .claude-selection-close{background:none;border:none;color:white;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background-color 0.2s} .claude-selection-close:hover{background:rgba(255,255,255,0.2)} .claude-selection-content{flex:1;overflow-y:auto;padding:24px} .claude-selection-controls{display:flex;gap:8px;align-items:center;margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid #e2e8f0;flex-wrap:wrap} .claude-selection-count{margin-left:auto;font-size:14px;color:#718096;font-weight:500} .claude-sort-controls{display:flex;gap:6px;align-items:center;margin-left:16px;padding-left:16px;border-left:1px solid #e2e8f0} .claude-sort-label{font-size:12px;color:#718096;font-weight:500} .claude-sort-select{padding:4px 8px;border:1px solid #e2e8f0;border-radius:4px;font-size:12px;background:white} .claude-sort-select:focus{outline:none;border-color:#667eea} .claude-selection-list{display:grid;gap:8px} .claude-selection-item{display:flex;align-items:flex-start;gap:12px;padding:12px;border:1px solid #e2e8f0;border-radius:8px;transition:all 0.2s} .claude-selection-item:hover{background:#f8fafc;border-color:#667eea} .claude-selection-checkbox{margin-top:2px;transform:scale(1.2)} .claude-selection-label{flex:1;cursor:pointer;line-height:1.4} .claude-selection-name{font-weight:500;color:#2d3748;margin-bottom:4px} .claude-selection-meta{font-size:13px;color:#718096} .claude-project-item{border:1px solid #e2e8f0;border-radius:8px;overflow:hidden;transition:all 0.2s} .claude-project-item:hover{border-color:#667eea} .claude-project-header{display:flex;align-items:center;gap:8px;padding:12px;background:#f8fafc} .claude-project-toggle{background:none;border:none;color:#718096;cursor:pointer;font-size:12px;padding:4px;border-radius:4px;transition:all 0.2s;min-width:20px} .claude-project-toggle:hover{background:#e2e8f0} .claude-toggle-icon{transition:transform 0.2s;display:inline-block} .claude-toggle-icon.expanded{transform:rotate(90deg)} .claude-project-checkbox{transform:scale(1.2)} .claude-project-label{flex:1;cursor:pointer;line-height:1.4} .claude-project-name{font-weight:600;color:#2d3748;margin-bottom:4px} .claude-project-meta{font-size:13px;color:#718096} .chat-count{font-weight:500;color:#667eea} .claude-project-chats{background:#fff;border-top:1px solid #e2e8f0} .claude-chat-item{display:flex;align-items:center;gap:12px;padding:8px 16px 8px 48px;transition:all 0.2s;border-bottom:1px solid #f1f5f9} .claude-chat-item:last-child{border-bottom:none} .claude-chat-item:hover{background:#f8fafc} .claude-chat-checkbox{transform:scale(1.1)} .claude-chat-label{flex:1;cursor:pointer;line-height:1.3} .claude-chat-name{font-weight:500;color:#374151;margin-bottom:2px} .claude-chat-meta{font-size:12px;color:#9ca3af} .claude-loading{padding:16px;text-align:center;color:#718096;font-style:italic} .claude-error{padding:16px;text-align:center;color:#ef4444;font-size:13px} .claude-btn{padding:8px 16px;border:none;border-radius:6px;font-size:13px;font-weight:500;cursor:pointer;transition:all 0.2s} .claude-btn-primary{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white} .claude-btn-primary:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px rgba(102,126,234,0.4)} .claude-btn-primary:disabled{opacity:0.5;cursor:not-allowed} .claude-btn-secondary{background:#e2e8f0;color:#2d3748;font-size:12px;padding:6px 12px} .claude-btn-secondary:hover{background:#cbd5e0} .claude-selection-footer{background:#f8fafc;padding:20px 24px;border-top:1px solid #e2e8f0;display:flex;gap:12px;justify-content:flex-end} @media (prefers-color-scheme: dark){.claude-sort-select{color:#1f2937!important}.claude-sort-select option{color:#1f2937!important}} </style>`; } function getProgressStyles() { return `<style id="claude-progress-styles"> .claude-progress-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.8);display:flex;align-items:center;justify-content:center;z-index:99999;font-family:system-ui,-apple-system,sans-serif} .claude-progress-modal{background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,0.3);width:90%;max-width:500px;overflow:hidden} .claude-progress-header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;padding:20px 24px;display:flex;align-items:center;justify-content:space-between} .claude-progress-header h3{margin:0;font-size:18px;font-weight:600} .claude-progress-close{background:none;border:none;color:white;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;transition:background-color 0.2s} .claude-progress-close:hover{background:rgba(255,255,255,0.2)} .claude-progress-content{padding:24px} .claude-progress-bar{width:100%;height:8px;background:#e2e8f0;border-radius:4px;overflow:hidden;margin-bottom:16px} .claude-progress-fill{height:100%;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);transition:width 0.3s ease;border-radius:4px} .claude-progress-text{font-size:16px;font-weight:500;color:#2d3748;margin-bottom:8px} .claude-progress-details{font-size:14px;color:#718096;line-height:1.5;min-height:20px} </style>`; } // ============================================= // INITIALIZATION // ============================================= async function waitForMainScript() { return new Promise((resolve) => { let attempts = 0; const maxAttempts = 50; const checkInterval = setInterval(() => { attempts++; if (typeof window.claudeExporter !== 'undefined') { console.log(`[Claude Mass Exporter] Main script found after ${attempts} attempts`); clearInterval(checkInterval); resolve(true); } if (attempts >= maxAttempts) { clearInterval(checkInterval); resolve(false); } }, 100); }); } async function init() { console.log('[Claude Mass Exporter] Initializing...'); const mainScriptLoaded = await waitForMainScript(); if (!mainScriptLoaded) { console.error('[Claude Mass Exporter] Main script not detected after 5 seconds'); return; } if (!checkDependency()) return; console.log('[Claude Mass Exporter] Main script detected, exposing mass export functions...'); window.claudeMassExporter = { exportAllProjects, exportCurrentProject, exportAllRecentConversations, }; console.log('[Claude Mass Exporter] Enhanced export functionality activated!'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址