您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Export Claude chat conversations with code artifacts into individual files with timestamp prefixes
// ==UserScript== // @name Enhanced Claude Chat & Code Exporter 4.2 // @namespace https://fsfarimani.dev/ // @version 4.2 // @description Export Claude chat conversations with code artifacts into individual files with timestamp prefixes // @author Foad S. Farimani (fsfarimani) <[email protected]> // @match https://claude.ai/chat/* // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant GM_download // @grant GM_getValue // @grant GM_setValue // @license MIT // @homepageURL https://gf.qytechs.cn/en/scripts/534219-enhanced-claude-chat-code-exporter-4-1 // @source https://gf.qytechs.cn/en/scripts/534219-enhanced-claude-chat-code-exporter-4-1/code // ==/UserScript== (function () { "use strict"; // Add export buttons to the UI // Add export buttons to the UI (mounted outside the React tree via Shadow DOM) function addExportButtons() { // If our host already exists, bail if (document.getElementById("claude-export-host")) return; // Create a host attached to <body> const host = document.createElement("div"); host.id = "claude-export-host"; host.style.all = "initial"; // defensive reset in case of inherited styles host.style.position = "fixed"; host.style.zIndex = "2147483647"; // max-ish host.style.bottom = "16px"; host.style.right = "16px"; host.style.pointerEvents = "auto"; document.body.appendChild(host); // Shadow root for isolation const shadow = host.attachShadow({ mode: "open" }); // Styles scoped to shadow const style = document.createElement("style"); style.textContent = ` * { box-sizing: border-box; font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; } .wrap { display: flex; gap: 8px; background: rgba(17,17,17,0.85); border: 1px solid rgba(255,255,255,0.08); backdrop-filter: blur(6px); padding: 8px; border-radius: 10px; align-items: center; box-shadow: 0 8px 24px rgba(0,0,0,0.25); } button { appearance: none; border: 0; border-radius: 8px; height: 32px; padding: 0 10px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; color: #fff; font-size: 13px; font-weight: 600; cursor: pointer; transition: transform 0.06s ease, opacity 0.2s ease, filter 0.2s ease; will-change: transform; } button:active { transform: translateY(1px) scale(0.98); } .btn-all { background: #4a6ee0; } .btn-md { background: #9e6ee0; } .icon { display: inline-flex; } `; shadow.appendChild(style); // Container const container = document.createElement("div"); container.className = "wrap"; // Export All button const btnAll = document.createElement("button"); btnAll.className = "btn-all"; btnAll.setAttribute("aria-label", "Export All"); btnAll.innerHTML = ` <span class="icon" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"> <path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112a8,8,0,0,1,16,0v96H200V112a8,8,0,0,1,16,0ZM80,80a8,8,0,0,1,8-8h32V36a4,4,0,0,1,4-4h8a4,4,0,0,1,4,4V72h32a8,8,0,0,1,5.66,13.66l-40,40a8,8,0,0,1-11.32,0l-40-40A8,8,0,0,1,80,80Z"></path> </svg> </span> <span>Export All</span> `; btnAll.addEventListener("click", exportConversation); // Markdown-only button const btnMd = document.createElement("button"); btnMd.className = "btn-md"; btnMd.setAttribute("aria-label", "Export Markdown Only"); btnMd.innerHTML = ` <span class="icon" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 256 256"> <path d="M216,112v96a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V112a8,8,0,0,1,16,0v96H200V112a8,8,0,0,1,16,0ZM80,80a8,8,0,0,1,8-8h32V36a4,4,0,0,1,4-4h8a4,4,0,0,1,4,4V72h32a8,8,0,0,1,5.66,13.66l-40,40a8,8,0,0,1-11.32,0l-40-40A8,8,0,0,1,80,80Z"></path> </svg> </span> <span>Md Only</span> `; btnMd.addEventListener("click", exportMarkdownOnly); // Mount container.appendChild(btnAll); container.appendChild(btnMd); shadow.appendChild(container); } // Helper function to download a file function downloadFile(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); // Cleanup setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); } // Generate a timestamp in the format yyyyMMddHHmmss function generateTimestamp() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, "0"); const day = String(now.getDate()).padStart(2, "0"); const hours = String(now.getHours()).padStart(2, "0"); const minutes = String(now.getMinutes()).padStart(2, "0"); const seconds = String(now.getSeconds()).padStart(2, "0"); return `${year}${month}${day}${hours}${minutes}${seconds}`; } // Function to export only the markdown content async function exportMarkdownOnly() { try { logDebug("Starting markdown-only export process..."); // Show loading indicator showLoadingIndicator("Extracting conversation..."); // Generate timestamp prefix for this export const timestampPrefix = generateTimestamp(); logDebug(`Generated timestamp prefix: ${timestampPrefix}`); // Get the chat title const chatTitle = getChatTitle(); const safeChatTitle = sanitizeFileName( chatTitle || "Claude Conversation" ); logDebug(`Chat title: ${chatTitle} (sanitized: ${safeChatTitle})`); // Extract the conversation as markdown logDebug("Extracting conversation as markdown"); const markdown = extractConversationMarkdown(); // Download the markdown file with timestamp prefix const markdownFilename = `${timestampPrefix}_${safeChatTitle}.md`; const markdownBlob = new Blob([markdown], { type: "text/markdown" }); downloadFile(markdownBlob, markdownFilename); logDebug(`Downloaded markdown file: ${markdownFilename}`); // Show success message hideLoadingIndicator(); showNotification( `Exported Claude conversation as markdown successfully!`, "success" ); } catch (error) { logDebug(`Error in exportMarkdownOnly: ${error.message}`); console.error("Error exporting markdown:", error); hideLoadingIndicator(); showNotification( "Error exporting markdown. Check console for details.", "error" ); } } // Main function to export the conversation with artifacts async function exportConversation() { try { logDebug("Starting export process..."); // Initialize a variable to store clipboard content for artifact extraction let savedClipboardContent = ""; // Try to save current clipboard content so we can restore it later try { savedClipboardContent = await navigator.clipboard.readText(); logDebug("Saved original clipboard content"); } catch (error) { logDebug("Could not read original clipboard content: " + error.message); } // Show loading indicator showLoadingIndicator("Processing chat and artifacts..."); // Generate timestamp prefix for this export session const timestampPrefix = generateTimestamp(); logDebug(`Generated timestamp prefix: ${timestampPrefix}`); // Get the chat title const chatTitle = getChatTitle(); const safeChatTitle = sanitizeFileName( chatTitle || "Claude Conversation" ); logDebug(`Chat title: ${chatTitle} (sanitized: ${safeChatTitle})`); // Extract the conversation as markdown logDebug("Extracting conversation as markdown"); const markdown = extractConversationMarkdown(); // Download the markdown file with timestamp prefix const markdownFilename = `${timestampPrefix}_00_${safeChatTitle}.md`; const markdownBlob = new Blob([markdown], { type: "text/markdown" }); downloadFile(markdownBlob, markdownFilename); logDebug(`Downloaded markdown file: ${markdownFilename}`); // Small delay before processing artifacts await new Promise((resolve) => setTimeout(resolve, 300)); // Find all artifact containers in the DOM const artifactButtons = document.querySelectorAll( 'button[aria-label="Preview contents"]' ); logDebug(`Found ${artifactButtons.length} artifact buttons`); // Process all artifacts sequentially showLoadingIndicator( `Found ${artifactButtons.length} artifacts, processing...` ); for (let i = 0; i < artifactButtons.length; i++) { const artifactButton = artifactButtons[i]; try { // Update loading indicator with progress showLoadingIndicator( `Processing artifact ${i + 1} of ${artifactButtons.length}...` ); // First, extract metadata without opening the artifact const initialArtifact = extractArtifactMetadataFromPreview( artifactButton, i ); if (!initialArtifact) { logDebug(`Failed to extract metadata for artifact ${i + 1}`); continue; } // Click the artifact button to open the code panel logDebug(`Clicking artifact button ${i + 1} to open code panel`); artifactButton.click(); // Wait for the code panel to load await new Promise((resolve) => setTimeout(resolve, 1000)); // Now extract the full content using keyboard shortcut method const fullArtifact = await extractArtifactUsingKeyboardCopy( initialArtifact ); if (fullArtifact && fullArtifact.content) { const artifactNumber = String(i + 1).padStart(2, "0"); const fileName = `${timestampPrefix}_${artifactNumber}_${sanitizeFileName( fullArtifact.title )}${getFileExtension(fullArtifact.language)}`; // Download the artifact const blob = new Blob([fullArtifact.content], { type: "text/plain", }); downloadFile(blob, fileName); logDebug( `Downloaded artifact ${i + 1}: ${fileName} (${ fullArtifact.content.length } chars)` ); // Close the code panel by clicking outside or on close button const closeButton = document.querySelector( 'button svg[width="18"][height="18"] path[d*="205.66,194.34"]' ); if ( closeButton && closeButton.parentElement && closeButton.parentElement.parentElement ) { closeButton.parentElement.parentElement.click(); } else { // If can't find the close button, try clicking elsewhere const header = document.querySelector("header"); if (header) header.click(); } // Small delay between artifacts to prevent browser throttling await new Promise((resolve) => setTimeout(resolve, 800)); } else { logDebug(`Failed to extract content for artifact ${i + 1}`); } } catch (error) { logDebug(`Error processing artifact ${i + 1}: ${error.message}`); console.error(`Error processing artifact ${i + 1}:`, error); // Try to close any open panels before continuing const closeButton = document.querySelector( 'button svg[width="18"][height="18"] path[d*="205.66,194.34"]' ); if ( closeButton && closeButton.parentElement && closeButton.parentElement.parentElement ) { closeButton.parentElement.parentElement.click(); } } } // Try to restore original clipboard content if (savedClipboardContent) { try { await navigator.clipboard.writeText(savedClipboardContent); logDebug("Restored original clipboard content"); } catch (error) { logDebug("Could not restore clipboard: " + error.message); } } // Show success message hideLoadingIndicator(); showNotification( `Exported Claude conversation and ${artifactButtons.length} artifacts successfully!`, "success" ); } catch (error) { logDebug(`Error in exportConversation: ${error.message}`); console.error("Error exporting conversation:", error); hideLoadingIndicator(); showNotification( "Error exporting conversation. Check console for details.", "error" ); } } // Extract only metadata from an artifact preview without opening it function extractArtifactMetadataFromPreview(button, index) { try { // Extract metadata from the preview const titleElement = button.querySelector(".leading-tight.text-sm"); const typeElement = button.querySelector(".text-sm.text-text-300"); let title = `artifact_${index + 1}`; let type = "Code"; if (titleElement) { title = titleElement.textContent.trim(); } if (typeElement) { type = typeElement.textContent.trim(); } // Return metadata without content return { title: title, type: type, language: determineLanguage(type, title, ""), content: null, // We'll get the content later }; } catch (err) { logDebug(`Error in extractArtifactMetadataFromPreview: ${err.message}`); console.error("Error extracting artifact metadata from preview:", err); return null; } } // Extract artifact content by reading text directly (no clipboard dependency) async function extractArtifactUsingKeyboardCopy(artifactMetadata) { try { // Be liberal about code containers/selectors const codeBlock = document.querySelector(".code-block__code") || document.querySelector('[data-testid="code-block"] code') || document.querySelector("pre code") || document.querySelector("pre") || document.querySelector("code"); if (!codeBlock) { logDebug("No code block found in panel"); return null; } // Determine language from classnames or data attributes let language = artifactMetadata.language || "plaintext"; const langGuessers = [ (el) => (el.getAttribute && el.getAttribute("data-language")) || null, (el) => { const cls = (el.className || "").toString(); const m = cls.match(/\blanguage-([\w+-]+)\b/i); return m ? m[1] : null; }, (el) => { // Some UIs put language on the parent container const parent = el.parentElement; if (!parent) return null; const cls = (parent.className || "").toString(); const m = cls.match(/\blanguage-([\w+-]+)\b/i); return m ? m[1] : null; }, ]; for (const g of langGuessers) { const v = g(codeBlock); if (v) { language = v; break; } } // Extract plain text reliably; innerText preserves visual line breaks let extractedText = ( codeBlock.innerText || codeBlock.textContent || "" ).replace(/\r\n/g, "\n"); // If extraction is unexpectedly empty, try a broader read if (!extractedText.trim()) { const fallback = document.querySelector("pre") || document.querySelector("code"); if (fallback && fallback !== codeBlock) { extractedText = ( fallback.innerText || fallback.textContent || "" ).replace(/\r\n/g, "\n"); } } // Final safety net if (!extractedText.trim()) { extractedText = "// Unable to extract code from the artifact panel"; } return { title: artifactMetadata.title, type: artifactMetadata.type, language: language, content: extractedText, }; } catch (err) { logDebug(`Error in extractArtifactUsingKeyboardCopy: ${err.message}`); console.error("Error extracting artifact without clipboard:", err); // Last resort: try reading any visible pre/code text const anyCode = document.querySelector("pre, code"); const txt = anyCode ? anyCode.innerText || anyCode.textContent || "" : ""; return { title: artifactMetadata.title, type: artifactMetadata.type, language: artifactMetadata.language || "plaintext", content: txt || "// Error extracting content", }; } } // Extract all text from an element including all child nodes, maintaining line breaks function extractAllTextFromElement(element) { if (!element) return ""; let text = ""; const childNodes = element.childNodes; for (let i = 0; i < childNodes.length; i++) { const node = childNodes[i]; if (node.nodeType === Node.TEXT_NODE) { text += node.textContent; } else if (node.nodeType === Node.ELEMENT_NODE) { // Process element nodes if ( node.tagName === "BR" || node.tagName === "DIV" || node.tagName === "P" ) { text += "\n"; // Add newline for line break elements } // Recursively process child elements text += extractAllTextFromElement(node); // Add newline after certain block elements if ( node.tagName === "DIV" || node.tagName === "P" || node.tagName === "LI" || node.tagName === "TR" ) { text += "\n"; } } } return text; } // Extract the conversation as markdown (more tolerant selectors) function extractConversationMarkdown() { let markdown = ""; // Title (from robust getter) const chatTitle = getChatTitle(); if (chatTitle) { markdown += `# ${chatTitle}\n\n`; } // Export timestamp const now = new Date(); markdown += `*Exported on: ${now.toLocaleString()}*\n\n`; // Try to find a message list container (best effort) const root = document.querySelector('[data-testid="chat"]') || document.querySelector('[role="main"]') || document.body; // Collect likely message nodes const messageNodes = root.querySelectorAll( [ '[data-testid="chat-message"]', '[data-testid="message"]', "[data-message]", 'article[role="article"]', 'div[role="listitem"]', // fallback for generic message groups (Claude UI often nests blocks) ".group, .prose, .markdown", ].join(", ") ); // Helper to convert a block to basic markdown const toMarkdown = (el) => { if (!el) return ""; // Prefer innerText to keep line breaks; trim trailing spaces return (el.innerText || el.textContent || "") .replace(/\r\n/g, "\n") .replace(/\n{3,}/g, "\n\n") .trim(); }; // Identify role (user/assistant) per node messageNodes.forEach((node) => { // Heuristics/markers seen across Claude revisions const isUser = node.querySelector('[data-testid="user-message"]') || node.getAttribute("data-message") === "user" || node.querySelector('[data-role="user"]') || node.querySelector('[aria-label="User"]'); const isAssistant = node.querySelector(".font-claude-message") || node.querySelector('[data-testid="assistant-message"]') || node.getAttribute("data-message") === "assistant" || node.querySelector('[data-role="assistant"]') || node.querySelector('[aria-label="Claude"]'); // Skip containers that are clearly empty const contentText = toMarkdown(node); if (!contentText) return; if (isUser) { markdown += `## User\n\n${contentText}\n\n`; return; } if (isAssistant) { markdown += `## Claude\n\n${contentText}\n\n`; // Attach artifact references if present const artifactButtons = node.querySelectorAll( 'button[aria-label="Preview contents"]' ); artifactButtons.forEach((button, index) => { const titleElement = button.querySelector(".leading-tight.text-sm"); const typeElement = button.querySelector(".text-sm.text-text-300"); const title = titleElement ? titleElement.textContent.trim() : `artifact_${index + 1}`; const type = typeElement ? typeElement.textContent.trim() : "Code"; const artifactNumber = String(index + 1).padStart(2, "0"); markdown += `\n**Code Artifact:** \`${artifactNumber}_${title}\` (${type})\n`; markdown += `*See separate file with corresponding timestamp prefix*\n\n`; }); return; } // If we cannot confidently tell, include as a generic assistant message, // because most content blocks belong to assistant in Claude’s UI. markdown += `## Claude\n\n${contentText}\n\n`; }); return markdown; } // Determine the language of a code artifact based on context clues function determineLanguage(type, title, content) { // If it's not code, return as document if (type.toLowerCase() !== "code") { return "markdown"; } // Check title for language hints const titleLower = title.toLowerCase(); if (titleLower.includes("java")) return "java"; if (titleLower.includes("python") || titleLower.includes(".py")) return "python"; if (titleLower.includes("javascript") || titleLower.includes("js")) return "javascript"; if (titleLower.includes("html")) return "html"; if (titleLower.includes("css")) return "css"; if ( titleLower.includes("bash") || titleLower.includes("shell") || titleLower.includes(".sh") ) return "bash"; if (titleLower.includes("powershell") || titleLower.includes(".ps1")) return "powershell"; if (titleLower.includes("sql")) return "sql"; if (titleLower.includes("c#")) return "csharp"; if (titleLower.includes("c++")) return "cpp"; if (titleLower.includes("go")) return "go"; if (titleLower.includes("rust")) return "rust"; // Check content for language clues if content is provided if (content) { if (content.includes("public class") || content.includes("import java.")) return "java"; if (content.includes("def ") && content.includes(":")) return "python"; if (content.includes("function") && content.includes("{")) return "javascript"; if (content.includes("<html") || content.includes("<!DOCTYPE html")) return "html"; if (content.includes("#!/bin/bash")) return "bash"; if (content.includes("#!/bin/sh")) return "bash"; if (content.includes("#!powershell")) return "powershell"; } // Default to plaintext if we can't determine return "plaintext"; } // Get the appropriate file extension for a language function getFileExtension(language) { const extensions = { java: ".java", python: ".py", javascript: ".js", html: ".html", css: ".css", bash: ".sh", powershell: ".ps1", sql: ".sql", csharp: ".cs", cpp: ".cpp", go: ".go", rust: ".rs", markdown: ".md", plaintext: ".txt", }; return extensions[language.toLowerCase()] || ".txt"; } // Get the chat title (robust + derives from first user message if generic) function getChatTitle() { // 1) Try common title locations const selectors = [ '[data-testid="conversation-title"]', '[data-testid="chat-title"]', "header h1", 'h1[role="heading"]', "h1.truncate", "h1", ]; for (const sel of selectors) { const el = document.querySelector(sel); if (el && el.textContent && el.textContent.trim()) { const t = el.textContent.trim(); if (!isGenericTitle(t)) return t; } } // 2) Fallback: document.title (strip common decorations) const dt = (document.title || "") .trim() .replace(/^\s*Claude\s*[–-]\s*/i, "") .replace(/\s*[–-]\s*Claude\s*$/i, "") .trim(); if (dt && !isGenericTitle(dt)) return dt; // 3) Last-resort: derive from the first user message snippet const root = document.querySelector('[data-testid="chat"]') || document.querySelector('[role="main"]') || document.body; // Prefer explicit user markers; fall back to plausible user blocks const userCandidates = root.querySelectorAll( [ '[data-testid="user-message"]', '[data-role="user"]', '[aria-label="User"]', '[data-message="user"]', // plausible fallbacks (Claude often renders user text in simple blocks) 'article[role="article"]', 'div[role="listitem"]', ".prose, .markdown", ].join(", ") ); for (const node of userCandidates) { const txt = (node.innerText || node.textContent || "") .replace(/\s+/g, " ") .trim(); if (txt && !isLikelyNonContent(txt)) { const snippet = txt.slice(0, 60); const clean = snippet.replace(/[\\/:*?"<>|]+/g, " ").trim(); if (clean) return clean; } } // If all else fails, let caller use its own fallback return null; // Helpers function isGenericTitle(s) { // Titles like "Claude" or empty are not helpful const v = (s || "").trim().toLowerCase(); return !v || v === "claude" || v === "chat" || v === "conversation"; } function isLikelyNonContent(s) { // Ignore very short or purely decorative strings return s.length < 4; } } // Sanitize a string to be used as a filename function sanitizeFileName(name) { return name .replace(/[\\/:*?"<>|]/g, "_") // Replace invalid filename chars .replace(/\s+/g, "_") // Replace spaces with underscores .replace(/__+/g, "_") // Replace multiple underscores with a single one .replace(/^_+|_+$/g, "") // Remove leading/trailing underscores .slice(0, 100); // Limit length to 100 chars } // Log debug information to console with prefix function logDebug(message) { console.log(`[Claude Exporter] ${message}`); } // Show loading indicator with message function showLoadingIndicator(message) { // Remove existing indicator if any hideLoadingIndicator(); const indicator = document.createElement("div"); indicator.id = "claude-export-loading"; indicator.style.position = "fixed"; indicator.style.top = "50%"; indicator.style.left = "50%"; indicator.style.transform = "translate(-50%, -50%)"; indicator.style.padding = "20px"; indicator.style.backgroundColor = "rgba(0, 0, 0, 0.8)"; indicator.style.color = "white"; indicator.style.borderRadius = "8px"; indicator.style.zIndex = "10000"; indicator.style.fontSize = "16px"; indicator.style.fontFamily = "system-ui, -apple-system, sans-serif"; // Add a spinner and message for better visual feedback indicator.innerHTML = ` <div style="display: flex; align-items: center; gap: 10px;"> <div class="spinner" style="border: 3px solid rgba(255,255,255,.3); border-radius: 50%; border-top: 3px solid white; width: 20px; height: 20px; animation: spin 1s linear infinite;"></div> <div>${message || "Processing..."}</div> </div> `; // Add animation style const style = document.createElement("style"); style.id = "claude-export-style"; style.textContent = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; if (!document.getElementById("claude-export-style")) { document.head.appendChild(style); } document.body.appendChild(indicator); } // Hide loading indicator function hideLoadingIndicator() { const indicator = document.getElementById("claude-export-loading"); if (indicator) { document.body.removeChild(indicator); } } // Show a notification function showNotification(message, type = "info") { // Remove any existing notification const existingNotification = document.getElementById( "claude-export-notification" ); if (existingNotification) { document.body.removeChild(existingNotification); } const notification = document.createElement("div"); notification.id = "claude-export-notification"; notification.style.position = "fixed"; notification.style.bottom = "20px"; notification.style.left = "50%"; notification.style.transform = "translateX(-50%)"; notification.style.padding = "10px 20px"; notification.style.borderRadius = "4px"; notification.style.zIndex = "10000"; notification.style.fontSize = "14px"; notification.style.fontFamily = "system-ui, -apple-system, sans-serif"; notification.style.textAlign = "center"; notification.style.maxWidth = "80%"; notification.style.boxShadow = "0 2px 10px rgba(0, 0, 0, 0.2)"; if (type === "error") { notification.style.backgroundColor = "#f44336"; notification.style.color = "white"; } else if (type === "success") { notification.style.backgroundColor = "#4CAF50"; notification.style.color = "white"; } else { notification.style.backgroundColor = "#2196F3"; notification.style.color = "white"; } notification.textContent = message; document.body.appendChild(notification); // Remove after 5 seconds setTimeout(() => { if (document.getElementById("claude-export-notification")) { document.body.removeChild(notification); } }, 5000); } // Initialize the script function init() { logDebug("Initializing Enhanced Claude Exporter 4.1"); // Add export buttons when the page loads addExportButtons(); // Create a MutationObserver to watch for DOM changes const observer = new MutationObserver(() => { // Check if we need to add the export buttons after DOM changes addExportButtons(); }); // Start observing the document body for changes observer.observe(document.body, { childList: true, subtree: true, }); // Also register menu commands GM_registerMenuCommand( "Export Claude Conversation with Artifacts", exportConversation ); GM_registerMenuCommand( "Export Claude Conversation as Markdown Only", exportMarkdownOnly ); logDebug("Initialization complete"); } // Run the initialization init(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址