您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Lyra's Exporter - AI对话导出器的配套脚本,专业的AI对话导出器 - 支持Claude、Gemini、NotebookLM等多平台,轻松管理数百个对话窗口,导出完整时间线、附加图片、思考过程、附件、工具调用和Artifacts。
// ==UserScript== // @name 支持Claude、Gemini、NotebookLM等多平台的全功能AI对话导出管理工具 // @name:en Lyra's Exporter Fetch - Enhanced // @namespace userscript://lyra-conversation-exporter // @version 1.4 // @description Lyra's Exporter - AI对话导出器的配套脚本,专业的AI对话导出器 - 支持Claude、Gemini、NotebookLM等多平台,轻松管理数百个对话窗口,导出完整时间线、附加图片、思考过程、附件、工具调用和Artifacts。 // @description:en The essential companion script for Lyra's Exporter, designed for unified management and export of your conversation histories across multiple platforms, including Claude, Gemini, NotebookLM, and more. // @homepage https://yalums.github.io/lyra-exporter/ // @supportURL https://github.com/Yalums/lyra-exporter/issues // @author Yalums // @match https://pro.easychat.top/* // @match https://claude.ai/* // @match https://gemini.google.com/app/* // @match https://notebooklm.google.com/* // @match https://aistudio.google.com/* // @include *://gemini.google.com/* // @include *://notebooklm.google.com/* // @include *://aistudio.google.com/* // @run-at document-start // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @license GNU General Public License v3.0 // ==/UserScript== (function() { 'use strict'; // 防止重复初始化 if (window.lyraFetchInitialized) { return; } window.lyraFetchInitialized = true; // ===== 调试模式 ===== const DEBUG_MODE = true; const debugLog = (...args) => { if (DEBUG_MODE) { console.log('[Lyra Debug]', ...args); } }; // ===== i18n 国际化系统 ===== const i18n = { languages: { 'zh': { name: '中文', translations: { loading: '加载中...', exporting: '导出中...', processing: '处理中...', preparing: '准备中...', scanning: '扫描中...', scanComplete: '扫描完成,正在提取...', scrollToTop: '正在滚动到顶部...', startScanning: '开始向下扫描...', exportSuccess: '导出成功!', exportCancelled: '导出已取消。', operationCancelled: '操作已取消。', noContent: '没有可导出的对话内容。', saveConversation: '导出JSON', viewOnline: '预览对话', exportCurrentJSON: '导出JSON', exportAllConversations: '导出所有', branchMode: '分支模式', includeImages: '包含图片', claudeTitle: 'Claude 会话导出', geminiTitle: 'Gemini 会话导出', notebooklmTitle: 'NotebookLM', aistudioTitle: 'AI Studio 导出', defaultTitle: 'AI 导出器', enterTitle: '请输入对话标题:', defaultChatTitle: '对话', untitledChat: '未命名对话', uuidNotFound: '未找到对话UUID!', userIdNotFound: '未能自动获取用户ID,请手动输入或刷新页面重试', fetchFailed: '获取对话数据失败', exportFailed: '导出失败: ', loadFailed: '加载失败: ', cannotOpenExporter: '无法打开 Lyra Exporter,请检查', connectionTimeout: '连接超时,请检查 Lyra Exporter 是否正常运行', gettingConversation: '获取对话', withImages: ' (处理图片中...)', foundItems: '已发现', items: '条', successExported: '成功导出', conversations: '个对话!', manualUserId: '手动设置ID', enterUserId: '请输入您的组织ID (settings/account):', userIdSaved: '用户ID已保存!', autoDetecting: '自动检测中...', detectedUserId: '检测到用户: ' } }, 'en': { name: 'English', translations: { loading: 'Loading...', exporting: 'Exporting...', processing: 'Processing...', preparing: 'Preparing...', scanning: 'Scanning...', scanComplete: 'Scan complete, extracting...', scrollToTop: 'Scrolling to top...', startScanning: 'Starting to scan...', exportSuccess: 'Export successful!', exportCancelled: 'Export cancelled.', operationCancelled: 'Operation cancelled.', noContent: 'No conversation content to export.', saveConversation: 'Save File', viewOnline: 'Preview', exportCurrentJSON: 'Export JSON', exportAllConversations: 'Export All', branchMode: 'Branch', includeImages: 'Images', claudeTitle: 'Claude Exporter', geminiTitle: 'Gemini Exporter', notebooklmTitle: 'NotebookLM', aistudioTitle: 'AI Studio', defaultTitle: 'Exporter', enterTitle: 'Enter conversation title:', defaultChatTitle: 'Chat', untitledChat: 'Untitled Chat', uuidNotFound: 'UUID not found!', userIdNotFound: 'Unable to auto-detect user ID, please enter manually or refresh the page', fetchFailed: 'Failed to fetch conversation data', exportFailed: 'Export failed: ', loadFailed: 'Load failed: ', cannotOpenExporter: 'Cannot open Lyra Exporter, please check popup blocker', connectionTimeout: 'Connection timeout, please check if Lyra Exporter is running', gettingConversation: 'Getting conversation', withImages: ' (processing images...)', foundItems: 'Found', items: 'items', successExported: 'Successfully exported', conversations: 'conversations!', manualUserId: 'Input Account ID', enterUserId: 'Organization ID can be found in settings/account', userIdSaved: 'User ID saved!', autoDetecting: 'Auto-detecting...', detectedUserId: 'Account ' } } }, currentLang: null, init() { const savedLang = localStorage.getItem('lyraExporterLanguage'); if (savedLang && this.languages[savedLang]) { this.currentLang = savedLang; } else { const browserLang = navigator.language || navigator.userLanguage; this.currentLang = browserLang.startsWith('zh') ? 'zh' : 'en'; } }, t(key) { const translations = this.languages[this.currentLang]?.translations; return translations?.[key] || key; }, setLanguage(lang) { if (this.languages[lang]) { this.currentLang = lang; localStorage.setItem('lyraExporterLanguage', lang); return true; } return false; }, getLanguage() { return this.currentLang; } }; i18n.init(); // ===== 平台检测 ===== let currentPlatform = ''; const hostname = window.location.hostname; if (hostname.includes('easychat.top') || hostname.includes('claude.ai')) { currentPlatform = 'claude'; } else if (hostname.includes('gemini.google.com')) { currentPlatform = 'gemini'; } else if (hostname.includes('notebooklm.google.com')) { currentPlatform = 'notebooklm'; } else if (hostname.includes('aistudio.google.com')) { currentPlatform = 'aistudio'; } debugLog('Platform detected:', currentPlatform); // ===== 全局变量 ===== const LYRA_EXPORTER_URL = 'https://yalums.github.io/lyra-exporter/'; const LYRA_EXPORTER_ORIGIN = 'https://yalums.github.io'; let capturedUserId = ''; let isPanelCollapsed = localStorage.getItem('lyraExporterCollapsed') === 'true'; let includeImages = localStorage.getItem('lyraIncludeImages') === 'true'; const CONTROL_ID = "lyra-universal-exporter-container"; const TOGGLE_ID = "lyra-toggle-button"; const TREE_SWITCH_ID = "lyra-tree-mode"; const IMAGE_SWITCH_ID = "lyra-image-mode"; const LANG_SWITCH_ID = "lyra-lang-switch"; const MANUAL_ID_BTN = "lyra-manual-id-btn"; let panelInjected = false; const SCROLL_DELAY_MS = 250; const SCROLL_TOP_WAIT_MS = 1000; let collectedData = new Map(); // ===== Claude用户ID捕获增强版 ===== if (currentPlatform === 'claude') { debugLog('Initializing Claude user ID capture...'); const savedUserId = localStorage.getItem('lyraClaudeUserId'); if (savedUserId) { capturedUserId = savedUserId; debugLog('Restored user ID from localStorage:', capturedUserId); } // 方法1:解决沙箱隔离问题 const interceptorScript = document.createElement('script'); interceptorScript.textContent = ` (function() { console.log('[Lyra Page Script] Installing interceptors in page context...'); const patterns = [ /api\\/organizations\\/([a-zA-Z0-9-]+)/, /api\\/accounts\\/([a-zA-Z0-9-]+)/, /organizations\\/([a-zA-Z0-9-]+)\\/chat/, /user\\/([a-zA-Z0-9-]+)/ ]; function captureUserId(url) { for (const pattern of patterns) { const match = url.match(pattern); if (match && match[1]) { const userId = match[1]; localStorage.setItem('lyraClaudeUserId', userId); console.log('[Lyra Page Script] ✨ Captured user ID:', userId); // 触发自定义事件通知用户脚本 window.dispatchEvent(new CustomEvent('lyraUserIdCaptured', { detail: { userId: userId } })); return userId; } } return null; } const originalXHROpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url) { captureUserId(url); return originalXHROpen.apply(this, arguments); }; const originalFetch = window.fetch; window.fetch = function(resource, options) { const url = typeof resource === 'string' ? resource : (resource.url || ''); if (url) { captureUserId(url); } return originalFetch.apply(this, arguments); }; console.log('[Lyra Page Script] Interceptors installed successfully'); })(); `; (document.head || document.documentElement).appendChild(interceptorScript); interceptorScript.remove(); // 监听来自页面的用户ID捕获事件 window.addEventListener('lyraUserIdCaptured', (event) => { const userId = event.detail.userId; if (userId) { capturedUserId = userId; debugLog('Received user ID from page context:', capturedUserId); } }); // 方法2:定期检查 localStorage(作为备用) const checkLocalStorage = () => { const storedId = localStorage.getItem('lyraClaudeUserId'); if (storedId && storedId !== capturedUserId) { capturedUserId = storedId; debugLog('Updated user ID from localStorage:', capturedUserId); } }; setInterval(checkLocalStorage, 2000); // 方法3:DOM加载后尝试从页面提取 document.addEventListener('DOMContentLoaded', () => { setTimeout(() => { if (!capturedUserId) { debugLog('Attempting to extract user ID from page...'); checkLocalStorage(); const scripts = document.querySelectorAll('script'); scripts.forEach(script => { const content = script.textContent; if (content.includes('organization') || content.includes('account')) { const match = content.match(/["']([a-zA-Z0-9-]{36})["']/); if (match) { capturedUserId = match[1]; localStorage.setItem('lyraClaudeUserId', capturedUserId); debugLog('Captured user ID from script:', capturedUserId); } } }); } }, 2000); }); // 调试:定期显示状态 setInterval(() => { if (capturedUserId) { debugLog('User ID status: OK -', capturedUserId); } else { debugLog('User ID status: Not captured yet'); checkLocalStorage(); // 再次尝试从 localStorage 获取 } }, 5000); } // ===== 手动输入用户ID功能 ===== function promptForUserId() { const currentId = capturedUserId || localStorage.getItem('lyraClaudeUserId') || ''; const newId = prompt(i18n.t('enterUserId'), currentId); if (newId && newId.trim()) { capturedUserId = newId.trim(); localStorage.setItem('lyraClaudeUserId', capturedUserId); alert(i18n.t('userIdSaved')); debugLog('Manual user ID set:', capturedUserId); const panel = document.getElementById(CONTROL_ID); if (panel) { panel.remove(); panelInjected = false; createFloatingPanel(); } } } // ===== 获取用户ID(带备用方案)===== async function ensureUserId() { if (capturedUserId) { return capturedUserId; } const savedId = localStorage.getItem('lyraClaudeUserId'); if (savedId) { capturedUserId = savedId; return capturedUserId; } await sleep(1000); if (capturedUserId) { return capturedUserId; } const result = confirm(i18n.t('userIdNotFound') + '\n\n' + '是否手动输入组织ID?'); if (result) { promptForUserId(); return capturedUserId; } return null; } // ===== TrustedTypes处理 ===== let trustedPolicy; if (typeof window.trustedTypes !== 'undefined' && window.trustedTypes.createPolicy) { try { trustedPolicy = window.trustedTypes.createPolicy('lyra-exporter-policy', { createHTML: (input) => input }); } catch (e) { try { trustedPolicy = window.trustedTypes.defaultPolicy || window.trustedTypes.createPolicy('default', { createHTML: (input) => input }); } catch (e2) { debugLog('TrustedTypes not available'); } } } function safeSetInnerHTML(element, html) { if (trustedPolicy) { element.innerHTML = trustedPolicy.createHTML(html); } else { element.innerHTML = html; } } // ===== 样式注入 ===== function injectCustomStyle() { let primaryColor = '#1a73e8'; let buttonColor = '#1a73e8'; let buttonHoverColor = '#1765c2'; switch(currentPlatform) { case 'claude': primaryColor = '#d97706'; buttonColor = '#EA580C'; buttonHoverColor = '#DC2626'; break; case 'notebooklm': primaryColor = '#374151'; buttonColor = '#4B5563'; buttonHoverColor = '#1F2937'; break; } const styleContent = ` #${CONTROL_ID} { position: fixed !important; right: 12px !important; bottom: 60px !important; display: flex !important; flex-direction: column !important; gap: 6px !important; z-index: 2147483647 !important; transition: all 0.3s ease !important; background: white !important; border-radius: 10px !important; box-shadow: 0 3px 12px rgba(0,0,0,0.15) !important; padding: 8px !important; border: 1px solid ${currentPlatform === 'notebooklm' ? '#9CA3AF' : currentPlatform === 'claude' ? 'rgba(217, 119, 6, 0.3)' : '#e0e0e0'} !important; width: auto !important; min-width: 140px !important; font-family: 'Google Sans', Roboto, Arial, sans-serif !important; color: #3c4043 !important; } #${CONTROL_ID}.collapsed .lyra-main-controls { display: none !important; } #${CONTROL_ID}.collapsed { padding: 6px !important; width: 36px !important; height: 36px !important; min-width: 36px !important; justify-content: center !important; align-items: center !important; overflow: hidden !important; } #${TOGGLE_ID} { position: absolute !important; left: -18px !important; top: 50% !important; transform: translateY(-50%) !important; width: 24px !important; height: 24px !important; border-radius: 50% !important; display: flex !important; align-items: center !important; justify-content: center !important; background: #f1f3f4 !important; color: #2C84DB !important; cursor: pointer !important; border: 1px solid #dadce0 !important; transition: all 0.3s !important; box-shadow: 0 1px 3px rgba(0,0,0,0.1) !important; } #${CONTROL_ID}.collapsed #${TOGGLE_ID} { position: static !important; transform: none !important; left: auto !important; top: auto !important; } #${TOGGLE_ID}:hover { background: #e8eaed !important; } .lyra-main-controls { display: flex !important; flex-direction: column !important; gap: 8px !important; align-items: stretch !important; width: 100% !important; } .lyra-button { display: inline-flex !important; align-items: center !important; justify-content: center !important; padding: 6px 12px !important; border-radius: 16px !important; cursor: pointer !important; font-size: 12px !important; font-weight: 500 !important; background-color: ${buttonColor} !important; color: white !important; border: none !important; transition: all 0.3s !important; box-shadow: 0 1px 2px rgba(0,0,0,0.1) !important; font-family: 'Google Sans', Roboto, Arial, sans-serif !important; white-space: nowrap !important; width: 100% !important; gap: 6px !important; min-width: 124px !important; } .lyra-button:hover { background-color: ${buttonHoverColor} !important; box-shadow: 0 2px 4px rgba(0,0,0,0.15) !important; } .lyra-button:disabled { opacity: 0.6 !important; cursor: not-allowed !important; } .lyra-button.lyra-manual-btn { background-color: #6B7280 !important; font-size: 11px !important; padding: 4px 8px !important; } .lyra-button.lyra-manual-btn:hover { background-color: #4B5563 !important; } .lyra-button svg { flex-shrink: 0 !important; width: 16px !important; height: 16px !important; } .lyra-title { font-size: 11px !important; font-weight: 500 !important; color: ${primaryColor} !important; margin-bottom: 6px !important; text-align: center !important; } .lyra-status { font-size: 10px !important; color: #6B7280 !important; text-align: center !important; padding: 2px 4px !important; background: #F3F4F6 !important; border-radius: 4px !important; margin-bottom: 4px !important; } .lyra-status.success { background: #D1FAE5 !important; color: #065F46 !important; } .lyra-status.error { background: #FEE2E2 !important; color: #991B1B !important; } .lyra-toggle { display: flex !important; align-items: center !important; justify-content: space-between !important; font-size: 11px !important; margin-bottom: 4px !important; gap: 4px !important; color: #5f6368 !important; width: 100% !important; padding: 0 2px !important; } .lyra-toggle span { flex: 1 !important; text-align: left !important; } .lyra-switch { position: relative !important; display: inline-block !important; width: 28px !important; height: 14px !important; flex-shrink: 0 !important; } .lyra-switch input { opacity: 0 !important; width: 0 !important; height: 0 !important; } .lyra-slider { position: absolute !important; cursor: pointer !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; background-color: #ccc !important; transition: .4s !important; border-radius: 34px !important; } .lyra-slider:before { position: absolute !important; content: "" !important; height: 10px !important; width: 10px !important; left: 2px !important; bottom: 2px !important; background-color: white !important; transition: .4s !important; border-radius: 50% !important; } input:checked + .lyra-slider { background-color: ${buttonColor} !important; } input:checked + .lyra-slider:before { transform: translateX(14px) !important; } .lyra-loading { display: inline-block !important; width: 16px !important; height: 16px !important; border: 2px solid rgba(255, 255, 255, 0.3) !important; border-radius: 50% !important; border-top-color: #fff !important; animation: lyra-spin 1s linear infinite !important; } @keyframes lyra-spin { to { transform: rotate(360deg); } } .lyra-progress { font-size: 10px !important; color: #5f6368 !important; margin-top: 4px !important; text-align: center !important; width: 100%; } .lyra-lang-toggle { display: flex !important; align-items: center !important; justify-content: center !important; font-size: 10px !important; margin-top: 4px !important; gap: 4px !important; padding: 2px 6px !important; border-radius: 12px !important; background: #f1f3f4 !important; cursor: pointer !important; transition: all 0.2s !important; width: 100% !important; } .lyra-lang-toggle:hover { background: #e8eaed !important; } `; if (typeof GM_addStyle === 'function') { GM_addStyle(styleContent); } else { const existingStyle = document.querySelector('style[data-lyra-styles]'); if (!existingStyle) { const style = document.createElement('style'); style.textContent = styleContent; style.setAttribute('data-lyra-styles', 'true'); document.head.appendChild(style); } } } // ===== 工具函数 ===== function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result.split(',')[1]); reader.onerror = reject; reader.readAsDataURL(blob); }); } function toggleCollapsed() { const container = document.getElementById(CONTROL_ID); const toggleButton = document.getElementById(TOGGLE_ID); if (container && toggleButton) { isPanelCollapsed = !isPanelCollapsed; container.classList.toggle('collapsed', isPanelCollapsed); safeSetInnerHTML(toggleButton, isPanelCollapsed ? '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>'); localStorage.setItem('lyraExporterCollapsed', isPanelCollapsed); } } // ===== HTML转Markdown函数(从1.1恢复)===== function htmlToMarkdown(element) { if (!element) return ''; let result = ''; function processNode(node) { if (node.nodeType === Node.TEXT_NODE) { return node.textContent; } if (node.nodeType !== Node.ELEMENT_NODE) { return ''; } const tagName = node.tagName.toLowerCase(); const children = Array.from(node.childNodes).map(processNode).join(''); switch(tagName) { case 'h1': return `\n# ${children}\n`; case 'h2': return `\n## ${children}\n`; case 'h3': return `\n### ${children}\n`; case 'h4': return `\n#### ${children}\n`; case 'h5': return `\n##### ${children}\n`; case 'h6': return `\n###### ${children}\n`; case 'strong': case 'b': return `**${children}**`; case 'em': case 'i': return `*${children}*`; case 'code': if (children.includes('\n')) { return `\n\`\`\`\n${children}\n\`\`\`\n`; } return `\`${children}\``; case 'pre': const codeChild = node.querySelector('code'); if (codeChild) { const lang = codeChild.className.match(/language-(\w+)/)?.[1] || ''; const codeContent = codeChild.textContent; return `\n\`\`\`${lang}\n${codeContent}\n\`\`\`\n`; } return `\n\`\`\`\n${children}\n\`\`\`\n`; case 'hr': return '\n---\n'; case 'br': return '\n'; case 'p': return `\n${children}\n`; case 'div': return `${children}\n`; case 'a': const href = node.getAttribute('href'); if (href) { return `[${children}](${href})`; } return children; case 'ul': case 'ol': return `\n${children}\n`; case 'li': const parent = node.parentElement; if (parent && parent.tagName.toLowerCase() === 'ol') { const index = Array.from(parent.children).indexOf(node) + 1; return `${index}. ${children}\n`; } return `- ${children}\n`; case 'blockquote': return `\n> ${children.split('\n').join('\n> ')}\n`; case 'table': return `\n${children}\n`; case 'thead': return `${children}`; case 'tbody': return `${children}`; case 'tr': return `${children}|\n`; case 'th': return `| **${children}** `; case 'td': return `| ${children} `; default: return children; } } result = processNode(element); result = result.replace(/\n{3,}/g, '\n\n'); result = result.trim(); return result; } // ===== 图片获取函数(从1.1恢复)===== function fetchViaGM(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, responseType: "blob", onload: function(response) { if (response.status >= 200 && response.status < 300) { resolve(response.response); } else { reject(new Error(`GM_xmlhttpRequest 失败,状态码: ${response.status}`)); } }, onerror: function(error) { reject(new Error(`GM_xmlhttpRequest 网络错误: ${error.statusText}`)); } }); }); } // ===== Gemini图片处理函数(从1.1恢复)===== async function processImageElement(imgElement) { if (!imgElement) return null; let imageUrlToFetch = null; const previewContainer = imgElement.closest('user-query-file-preview'); if (previewContainer) { const lensLinkElement = previewContainer.querySelector('a[href*="lens.google.com"]'); if (lensLinkElement && lensLinkElement.href) { try { const urlObject = new URL(lensLinkElement.href); const realImageUrl = urlObject.searchParams.get('url'); if (realImageUrl) { imageUrlToFetch = realImageUrl; } } catch (e) { debugLog('Error parsing Lens URL:', e); } } } if (!imageUrlToFetch) { const fallbackSrc = imgElement.src; if (fallbackSrc && !fallbackSrc.startsWith('data:')) { imageUrlToFetch = fallbackSrc; } } if (!imageUrlToFetch) { return null; } try { const blob = await fetchViaGM(imageUrlToFetch); const base64 = await blobToBase64(blob); return { type: 'image', format: blob.type, size: blob.size, data: base64, original_src: imageUrlToFetch }; } catch (error) { debugLog('Error processing image:', error); return null; } } // ===== Gemini数据提取函数(从1.1恢复)===== async function processGeminiContainer(container) { const userQueryElement = container.querySelector("user-query .query-text") || container.querySelector(".query-text-line"); const modelResponseContainer = container.querySelector("model-response") || container; const modelResponseElement = modelResponseContainer.querySelector("message-content .markdown-main-panel"); const questionText = userQueryElement ? userQueryElement.innerText.trim() : ""; let answerText = ""; if (modelResponseElement) { answerText = htmlToMarkdown(modelResponseElement); } const userImageElements = container.querySelectorAll("user-query img"); const modelImageElements = modelResponseContainer.querySelectorAll("model-response img"); const userImagesPromises = Array.from(userImageElements).map(img => processImageElement(img)); const modelImagesPromises = Array.from(modelImageElements).map(img => processImageElement(img)); const userImages = (await Promise.all(userImagesPromises)).filter(Boolean); const modelImages = (await Promise.all(modelImagesPromises)).filter(Boolean); if (questionText || answerText || userImages.length > 0 || modelImages.length > 0) { return { human: { text: questionText, images: userImages }, assistant: { text: answerText, images: modelImages } }; } return null; } async function extractGeminiConversationData() { const conversationTurns = document.querySelectorAll("div.conversation-turn"); let conversationData = []; if (conversationTurns.length > 0) { for (const turn of conversationTurns) { const data = await processGeminiContainer(turn); if (data) conversationData.push(data); } } else { const legacyContainers = document.querySelectorAll("div.single-turn, div.conversation-container"); for (const container of legacyContainers) { const data = await processGeminiContainer(container); if (data) conversationData.push(data); } } return conversationData; } // ===== NotebookLM数据提取函数(从1.1恢复)===== function extractNotebookLMConversationData() { const conversationTurns = document.querySelectorAll("div.chat-message-pair"); let conversationData = []; conversationTurns.forEach((turnContainer) => { let questionText = ""; const userQueryEl = turnContainer.querySelector("chat-message .from-user-container .message-text-content"); if (userQueryEl) { questionText = userQueryEl.innerText.trim(); if (questionText.startsWith('[Preamble] ')) { questionText = questionText.substring('[Preamble] '.length).trim(); } } let answerText = ""; const modelResponseContent = turnContainer.querySelector("chat-message .to-user-container .message-text-content"); if (modelResponseContent) { const answerParts = []; const structuralElements = modelResponseContent.querySelectorAll('labs-tailwind-structural-element-view-v2'); structuralElements.forEach(structEl => { const bulletEl = structEl.querySelector('.bullet'); const paragraphEl = structEl.querySelector('.paragraph'); let lineText = ''; if (bulletEl) { lineText += bulletEl.innerText.trim() + ' '; } if (paragraphEl) { let paragraphText = ''; paragraphEl.childNodes.forEach(node => { if (node.nodeType === Node.TEXT_NODE) { paragraphText += node.textContent; } else if (node.nodeType === Node.ELEMENT_NODE) { if (node.querySelector && node.querySelector('button.citation-marker')) { return; } if (node.tagName === 'SPAN' && node.classList.contains('bold')) { paragraphText += `**${node.innerText}**`; } else { paragraphText += node.innerText || node.textContent || ''; } } }); lineText += paragraphText; } if (lineText.trim()) { answerParts.push(lineText.trim()); } }); answerText = answerParts.join('\n\n'); } if (questionText || answerText) { conversationData.push({ human: questionText, assistant: answerText }); } }); return conversationData; } // ===== AI Studio相关函数(从1.1恢复)===== function getAIStudioScroller() { const selectors = [ 'ms-chat-session ms-autoscroll-container', 'mat-sidenav-content', '.chat-view-container' ]; for (const selector of selectors) { const el = document.querySelector(selector); if (el && (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth)) { return el; } } return document.documentElement; } function extractDataIncremental_AiStudio() { const turns = document.querySelectorAll('ms-chat-turn'); turns.forEach(turn => { if (collectedData.has(turn)) { return; } const isUserTurn = turn.querySelector('.chat-turn-container.user'); const isModelTurn = turn.querySelector('.chat-turn-container.model'); let turnData = { type: 'unknown', text: '' }; if (isUserTurn) { const userPromptNode = isUserTurn.querySelector('.user-prompt-container .turn-content'); if (userPromptNode) { const userText = userPromptNode.innerText.trim(); if (userText) { turnData.type = 'user'; turnData.text = userText; } } } else if (isModelTurn) { const responseChunks = isModelTurn.querySelectorAll('ms-prompt-chunk'); let responseTexts = []; responseChunks.forEach(chunk => { if (!chunk.querySelector('ms-thought-chunk')) { const cmarkNode = chunk.querySelector('ms-cmark-node'); if (cmarkNode) { const markdownText = htmlToMarkdown(cmarkNode); if (markdownText) { responseTexts.push(markdownText); } } } }); const responseText = responseTexts.join('\n\n').trim(); if (responseText) { turnData.type = 'model'; turnData.text = responseText; } } if (turnData.type !== 'unknown') { collectedData.set(turn, turnData); } }); } async function autoScrollAndCaptureAIStudio(onProgress) { collectedData.clear(); const scroller = getAIStudioScroller(); onProgress(i18n.t('scrollToTop'), false); scroller.scrollTop = 0; await sleep(SCROLL_TOP_WAIT_MS); let lastScrollTop = -1; onProgress(i18n.t('startScanning'), false); while (true) { extractDataIncremental_AiStudio(); onProgress(`${i18n.t('scanning')} ${Math.round((scroller.scrollTop + scroller.clientHeight) / scroller.scrollHeight * 100)}% (${i18n.t('foundItems')} ${collectedData.size} ${i18n.t('items')})`, false); if (scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 10) { break; } lastScrollTop = scroller.scrollTop; scroller.scrollTop += scroller.clientHeight * 0.85; await sleep(SCROLL_DELAY_MS); if (scroller.scrollTop === lastScrollTop) { break; } } onProgress(i18n.t('scanComplete'), false); extractDataIncremental_AiStudio(); await sleep(500); const finalTurnsInDom = document.querySelectorAll('ms-chat-turn'); let sortedData = []; finalTurnsInDom.forEach(turnNode => { if (collectedData.has(turnNode)) { sortedData.push(collectedData.get(turnNode)); } }); const pairedData = []; let lastHuman = null; sortedData.forEach(item => { if (item.type === 'user') { lastHuman = item.text; } else if (item.type === 'model' && lastHuman) { pairedData.push({ human: lastHuman, assistant: item.text }); lastHuman = null; } else if (item.type === 'model' && !lastHuman) { pairedData.push({ human: "[No preceding user prompt found]", assistant: item.text }); } }); if (lastHuman) { pairedData.push({ human: lastHuman, assistant: "[Model response is pending]" }); } return pairedData; } // ===== 获取对话标题函数(从1.1恢复)===== function getConversationTitle() { let title = ''; try { switch (currentPlatform) { case 'gemini': const defaultTitle = `Gemini ${i18n.t('defaultChatTitle')} ${new Date().toISOString().slice(0,10)}`; title = prompt(i18n.t('enterTitle'), defaultTitle); if (title === null) { return null; } return title.trim() || defaultTitle; case 'notebooklm': const nblmTitleInput = document.querySelector('input.title-input.mat-title-large') || document.querySelector('.title-input.mat-title-large.ng-pristine.ng-valid.ng-touched') || document.querySelector('h1.notebook-title'); if (nblmTitleInput) { title = nblmTitleInput.value || nblmTitleInput.innerText || nblmTitleInput.textContent; title = title.trim(); } return title || `${i18n.t('untitledChat')} NotebookLM`; case 'aistudio': const studioTitleEl = document.querySelector('div.page-title h1'); title = studioTitleEl ? studioTitleEl.innerText.trim() : `${i18n.t('untitledChat')} AI Studio`; return title; default: return `${i18n.t('defaultChatTitle')} ${new Date().toISOString().slice(0,10)}`; } } catch (error) { debugLog('Error getting title:', error); return `${i18n.t('untitledChat')} ${new Date().toISOString().slice(0,10)}`; } } // ===== Claude相关函数 ===== function getCurrentChatUUID() { const url = window.location.href; const match = url.match(/\/chat\/([a-zA-Z0-9-]+)/); return match ? match[1] : null; } function checkUrlForTreeMode() { return window.location.href.includes('?tree=True&rendering_mode=messages&render_all_tools=true') || window.location.href.includes('&tree=True&rendering_mode=messages&render_all_tools=true'); } async function getAllConversations() { const userId = await ensureUserId(); if (!userId) { debugLog('Cannot get conversations without user ID'); return null; } try { const baseUrl = window.location.hostname.includes('claude.ai') ? 'https://claude.ai' : 'https://pro.easychat.top'; const apiUrl = `${baseUrl}/api/organizations/${userId}/chat_conversations`; debugLog('Fetching conversations from:', apiUrl); const response = await fetch(apiUrl); if (!response.ok) { throw new Error(`${i18n.t('fetchFailed')}: ${response.status}`); } return await response.json(); } catch (error) { debugLog('Error fetching conversations:', error); return null; } } async function processImageAttachment(imageUrl, debugInfo = '') { try { if (!imageUrl.startsWith('http')) { const baseUrl = window.location.hostname.includes('claude.ai') ? 'https://claude.ai' : 'https://pro.easychat.top'; imageUrl = baseUrl + imageUrl; } const response = await fetch(imageUrl); if (!response.ok) { throw new Error(`Failed to fetch image: ${response.status}`); } const blob = await response.blob(); const base64 = await blobToBase64(blob); return { type: 'image', format: blob.type, size: blob.size, data: base64, original_url: imageUrl }; } catch (error) { debugLog('Error processing image:', error); return null; } } async function getConversationDetailsWithImages(uuid, includeImagesParam) { const userId = await ensureUserId(); if (!userId) { return null; } try { const baseUrl = window.location.hostname.includes('claude.ai') ? 'https://claude.ai' : 'https://pro.easychat.top'; const treeMode = document.getElementById(TREE_SWITCH_ID) ? document.getElementById(TREE_SWITCH_ID).checked : false; const apiUrl = `${baseUrl}/api/organizations/${userId}/chat_conversations/${uuid}${treeMode ? '?tree=True&rendering_mode=messages&render_all_tools=true' : ''}`; debugLog('Fetching conversation details from:', apiUrl); const response = await fetch(apiUrl); if (!response.ok) { throw new Error(`${i18n.t('fetchFailed')}: ${response.status}`); } const data = await response.json(); if (!includeImagesParam) { return data; } let processedImageCount = 0; if (data.chat_messages && Array.isArray(data.chat_messages)) { for (let msgIndex = 0; msgIndex < data.chat_messages.length; msgIndex++) { const message = data.chat_messages[msgIndex]; const fileArrays = ['files', 'files_v2', 'attachments']; for (const key of fileArrays) { if (message[key] && Array.isArray(message[key])) { for (let i = 0; i < message[key].length; i++) { const file = message[key][i]; let imageUrl = null; let isImage = false; if (file.file_kind === 'image' || (file.file_type && file.file_type.startsWith('image/'))) { isImage = true; imageUrl = file.preview_url || file.thumbnail_url || file.file_url; } if (isImage && imageUrl && !file.embedded_image) { const imageData = await processImageAttachment( imageUrl, `消息${msgIndex + 1}-${key}-${i + 1}` ); if (imageData) { message[key][i].embedded_image = imageData; processedImageCount++; } } } } } } } data._debug_info = { images_processed: processedImageCount, processing_time: new Date().toISOString() }; return data; } catch (error) { debugLog('Error fetching conversation details:', error); return null; } } async function exportCurrentConversationWithImages() { const uuid = getCurrentChatUUID(); if (!uuid) { alert(i18n.t('uuidNotFound')); return; } const userId = await ensureUserId(); if (!userId) { return; } try { const shouldIncludeImages = document.getElementById(IMAGE_SWITCH_ID)?.checked || false; const data = await getConversationDetailsWithImages(uuid, shouldIncludeImages); if (!data) { throw new Error(i18n.t('fetchFailed')); } const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `claude_${uuid.substring(0, 8)}_${shouldIncludeImages ? 'with_images_' : ''}${new Date().toISOString().slice(0,10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (error) { alert(`${i18n.t('exportFailed')} ${error.message}`); } } async function openLyraExporterWithData(jsonData, filename) { try { const exporterWindow = window.open(LYRA_EXPORTER_URL, '_blank'); if (!exporterWindow) { alert(i18n.t('cannotOpenExporter')); return false; } const checkInterval = setInterval(() => { try { exporterWindow.postMessage({ type: 'LYRA_HANDSHAKE', source: 'lyra-fetch-script' }, LYRA_EXPORTER_ORIGIN); } catch (e) { debugLog('Error sending handshake:', e); } }, 1000); const handleMessage = (event) => { if (event.origin !== LYRA_EXPORTER_ORIGIN) { return; } if (event.data && event.data.type === 'LYRA_READY') { clearInterval(checkInterval); const dataToSend = { type: 'LYRA_LOAD_DATA', source: 'lyra-fetch-script', data: { content: jsonData, filename: filename || `${currentPlatform}_export_${new Date().toISOString().slice(0,10)}.json` } }; exporterWindow.postMessage(dataToSend, LYRA_EXPORTER_ORIGIN); window.removeEventListener('message', handleMessage); } }; window.addEventListener('message', handleMessage); setTimeout(() => { clearInterval(checkInterval); window.removeEventListener('message', handleMessage); debugLog('Connection timeout'); }, 45000); return true; } catch (error) { alert(`${i18n.t('cannotOpenExporter')}: ${error.message}`); return false; } } // ===== 创建浮动面板 ===== function createFloatingPanel() { if (document.getElementById(CONTROL_ID) || panelInjected) { return false; } let savedTreeMode = false; let savedImageMode = includeImages; const existingTreeSwitch = document.getElementById(TREE_SWITCH_ID); const existingImageSwitch = document.getElementById(IMAGE_SWITCH_ID); if (existingTreeSwitch) { savedTreeMode = existingTreeSwitch.checked; } if (existingImageSwitch) { savedImageMode = existingImageSwitch.checked; } const container = document.createElement('div'); container.id = CONTROL_ID; if (isPanelCollapsed) container.classList.add('collapsed'); const toggleButton = document.createElement('div'); toggleButton.id = TOGGLE_ID; safeSetInnerHTML(toggleButton, isPanelCollapsed ? '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>' : '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>'); toggleButton.addEventListener('click', toggleCollapsed); container.appendChild(toggleButton); const controlsArea = document.createElement('div'); controlsArea.className = 'lyra-main-controls'; const title = document.createElement('div'); title.className = 'lyra-title'; switch (currentPlatform) { case 'claude': title.textContent = i18n.t('claudeTitle'); break; case 'gemini': title.textContent = i18n.t('geminiTitle'); break; case 'notebooklm': title.textContent = i18n.t('notebooklmTitle'); break; case 'aistudio': title.textContent = i18n.t('aistudioTitle'); break; default: title.textContent = i18n.t('defaultTitle'); } controlsArea.appendChild(title); // Claude专用功能 if (currentPlatform === 'claude') { const statusDiv = document.createElement('div'); statusDiv.className = 'lyra-status'; if (capturedUserId) { statusDiv.classList.add('success'); statusDiv.textContent = i18n.t('detectedUserId') + capturedUserId.substring(0, 8) + '...'; } else { statusDiv.classList.add('error'); statusDiv.textContent = i18n.t('autoDetecting'); } controlsArea.appendChild(statusDiv); const manualIdBtn = document.createElement('button'); manualIdBtn.id = MANUAL_ID_BTN; manualIdBtn.className = 'lyra-button lyra-manual-btn'; manualIdBtn.textContent = i18n.t('manualUserId'); manualIdBtn.addEventListener('click', promptForUserId); controlsArea.appendChild(manualIdBtn); const toggleContainer = document.createElement('div'); toggleContainer.className = 'lyra-toggle'; const treeModeChecked = savedTreeMode || checkUrlForTreeMode(); safeSetInnerHTML(toggleContainer, ` <span>${i18n.t('branchMode')}</span> <label class="lyra-switch"> <input type="checkbox" id="${TREE_SWITCH_ID}" ${treeModeChecked ? 'checked' : ''}> <span class="lyra-slider"></span> </label> `); controlsArea.appendChild(toggleContainer); const imageToggleContainer = document.createElement('div'); imageToggleContainer.className = 'lyra-toggle'; safeSetInnerHTML(imageToggleContainer, ` <span>${i18n.t('includeImages')}</span> <label class="lyra-switch"> <input type="checkbox" id="${IMAGE_SWITCH_ID}" ${savedImageMode ? 'checked' : ''}> <span class="lyra-slider"></span> </label> `); controlsArea.appendChild(imageToggleContainer); document.addEventListener('change', function(e) { if (e.target.id === IMAGE_SWITCH_ID) { includeImages = e.target.checked; localStorage.setItem('lyraIncludeImages', includeImages); } }); const uuidButton = document.createElement('button'); uuidButton.className = 'lyra-button'; safeSetInnerHTML(uuidButton, ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="11" cy="11" r="8"></circle> <path d="m21 21-4.35-4.35"></path> </svg> ${i18n.t('viewOnline')} `); uuidButton.addEventListener('click', async () => { const uuid = getCurrentChatUUID(); if (!uuid) { alert(i18n.t('uuidNotFound')); return; } const userId = await ensureUserId(); if (!userId) { return; } const originalContent = uuidButton.innerHTML; safeSetInnerHTML(uuidButton, `<div class="lyra-loading"></div><span>${i18n.t('loading')}</span>`); uuidButton.disabled = true; try { const shouldIncludeImages = document.getElementById(IMAGE_SWITCH_ID)?.checked || false; const data = await getConversationDetailsWithImages(uuid, shouldIncludeImages); if (!data) { throw new Error(i18n.t('fetchFailed')); } const jsonString = JSON.stringify(data, null, 2); const filename = `claude_${uuid.substring(0, 8)}_${shouldIncludeImages ? 'with_images_' : ''}${new Date().toISOString().slice(0,10)}.json`; await openLyraExporterWithData(jsonString, filename); } catch (error) { alert(`${i18n.t('loadFailed')} ${error.message}`); } finally { safeSetInnerHTML(uuidButton, originalContent); uuidButton.disabled = false; } }); controlsArea.appendChild(uuidButton); const exportButton = document.createElement('button'); exportButton.className = 'lyra-button'; safeSetInnerHTML(exportButton, ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="7 10 12 15 17 10"></polyline> <line x1="12" y1="15" x2="12" y2="3"></line> </svg> ${i18n.t('exportCurrentJSON')} `); exportButton.addEventListener('click', exportCurrentConversationWithImages); controlsArea.appendChild(exportButton); const exportAllButton = document.createElement('button'); exportAllButton.className = 'lyra-button'; safeSetInnerHTML(exportAllButton, ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <rect x="2" y="7" width="20" height="14" rx="2" ry="2"></rect> <path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"></path> </svg> ${i18n.t('exportAllConversations')} `); exportAllButton.addEventListener('click', async function(event) { const userId = await ensureUserId(); if (!userId) { return; } const shouldIncludeImages = document.getElementById(IMAGE_SWITCH_ID)?.checked || false; const progressElem = document.createElement('div'); progressElem.className = 'lyra-progress'; progressElem.textContent = i18n.t('preparing'); controlsArea.appendChild(progressElem); const originalContent = this.innerHTML; safeSetInnerHTML(this, `<div class="lyra-loading"></div><span>${i18n.t('exporting')}</span>`); this.disabled = true; try { const allConversations = await getAllConversations(); if (!allConversations || !Array.isArray(allConversations)) { throw new Error(i18n.t('fetchFailed')); } const result = { exportedAt: new Date().toISOString(), totalConversations: allConversations.length, conversations: [] }; for (let i = 0; i < allConversations.length; i++) { const conversation = allConversations[i]; progressElem.textContent = `${i18n.t('gettingConversation')} ${i + 1}/${allConversations.length}${shouldIncludeImages ? i18n.t('withImages') : ''}`; if (i > 0) await sleep(500); const details = await getConversationDetailsWithImages(conversation.uuid, shouldIncludeImages); if (details) result.conversations.push(details); } const blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `claude_all_conversations_${shouldIncludeImages ? 'with_images_' : ''}${new Date().toISOString().slice(0,10)}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); alert(`${i18n.t('successExported')} ${result.conversations.length} ${i18n.t('conversations')}`); } catch (error) { alert(`${i18n.t('exportFailed')} ${error.message}`); } finally { safeSetInnerHTML(this, originalContent); this.disabled = false; if (progressElem.parentNode) progressElem.parentNode.removeChild(progressElem); } }); controlsArea.appendChild(exportAllButton); } else { // 非Claude平台:Gemini, AI Studio, NotebookLM // 先添加预览按钮(除NotebookLM外) if (currentPlatform !== 'notebooklm') { const onlineViewButton = document.createElement('button'); onlineViewButton.className = 'lyra-button'; safeSetInnerHTML(onlineViewButton, ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="11" cy="11" r="8"></circle> <path d="m21 21-4.35-4.35"></path> </svg> ${i18n.t('viewOnline')} `); onlineViewButton.addEventListener('click', async function() { const title = getConversationTitle(); if (title === null) { debugLog(i18n.t('operationCancelled')); return; } this.disabled = true; const originalContent = this.innerHTML; safeSetInnerHTML(this, `<div class="lyra-loading"></div><span>${i18n.t('loading')}</span>`); let progressElem = null; if (currentPlatform === 'aistudio') { progressElem = document.createElement('div'); progressElem.className = 'lyra-progress'; controlsArea.appendChild(progressElem); } try { let conversationData = []; if (currentPlatform === 'aistudio') { conversationData = await autoScrollAndCaptureAIStudio((message) => { if (progressElem) progressElem.textContent = message; }); } else if (currentPlatform === 'gemini') { conversationData = await extractGeminiConversationData(); } if (conversationData.length > 0) { const finalJson = { title: title, platform: currentPlatform, exportedAt: new Date().toISOString(), conversation: conversationData }; const timestamp = new Date().toISOString().replace(/:/g, '-').slice(0, 19); const sanitizedTitle = title.replace(/[^a-z0-9\u4e00-\u9fa5]/gi, '_'); const filename = `${currentPlatform}_${sanitizedTitle}_${timestamp}.json`; const jsonString = JSON.stringify(finalJson, null, 2); await openLyraExporterWithData(jsonString, filename); } else { alert(i18n.t('noContent')); } } catch (error) { alert(`${i18n.t('loadFailed')} ${error.message}`); } finally { this.disabled = false; safeSetInnerHTML(this, originalContent); if (progressElem && progressElem.parentNode) { progressElem.parentNode.removeChild(progressElem); } } }); controlsArea.appendChild(onlineViewButton); } // 然后添加保存按钮 const exportButton = document.createElement('button'); exportButton.className = 'lyra-button'; safeSetInnerHTML(exportButton, ` <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> <polyline points="7 10 12 15 17 10"></polyline> <line x1="12" y1="15" x2="12" y2="3"></line> </svg> ${i18n.t('saveConversation')} `); exportButton.addEventListener('click', async function() { const title = getConversationTitle(); if (title === null) { debugLog(i18n.t('exportCancelled')); return; } this.disabled = true; const originalContent = this.innerHTML; safeSetInnerHTML(this, `<div class="lyra-loading"></div><span>${i18n.t('exporting')}</span>`); let progressElem = null; if (currentPlatform === 'aistudio') { progressElem = document.createElement('div'); progressElem.className = 'lyra-progress'; controlsArea.appendChild(progressElem); } try { let conversationData = []; if (currentPlatform === 'aistudio') { conversationData = await autoScrollAndCaptureAIStudio((message) => { if (progressElem) progressElem.textContent = message; }); } else if (currentPlatform === 'gemini') { conversationData = await extractGeminiConversationData(); } else if (currentPlatform === 'notebooklm') { conversationData = extractNotebookLMConversationData(); } if (conversationData.length > 0) { const finalJson = { title: title, platform: currentPlatform, exportedAt: new Date().toISOString(), conversation: conversationData }; const timestamp = new Date().toISOString().replace(/:/g, '-').slice(0, 19); const sanitizedTitle = title.replace(/[^a-z0-9\u4e00-\u9fa5]/gi, '_'); const filename = `${currentPlatform}_${sanitizedTitle}_${timestamp}.json`; const jsonData = JSON.stringify(finalJson, null, 2); const blob = new Blob([jsonData], { type: 'application/json;charset=utf-8' }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); debugLog(i18n.t('exportSuccess')); } else { alert(i18n.t('noContent')); } } catch (error) { alert(`${i18n.t('exportFailed')} ${error.message}`); } finally { this.disabled = false; safeSetInnerHTML(this, originalContent); if (progressElem && progressElem.parentNode) { progressElem.parentNode.removeChild(progressElem); } } }); controlsArea.appendChild(exportButton); } // 语言切换按钮 const langToggle = document.createElement('div'); langToggle.className = 'lyra-lang-toggle'; langToggle.id = LANG_SWITCH_ID; langToggle.textContent = `🌐 ${i18n.languages[i18n.getLanguage()].name}`; langToggle.addEventListener('click', function() { const currentLang = i18n.getLanguage(); const newLang = currentLang === 'zh' ? 'en' : 'zh'; i18n.setLanguage(newLang); const existingPanel = document.getElementById(CONTROL_ID); if (existingPanel) { existingPanel.remove(); panelInjected = false; createFloatingPanel(); } }); controlsArea.appendChild(langToggle); container.appendChild(controlsArea); document.body.appendChild(container); panelInjected = true; // 定期更新状态 if (currentPlatform === 'claude') { setInterval(() => { const statusDiv = container.querySelector('.lyra-status'); if (statusDiv) { if (capturedUserId) { statusDiv.classList.remove('error'); statusDiv.classList.add('success'); statusDiv.textContent = i18n.t('detectedUserId') + capturedUserId.substring(0, 8) + '...'; } else { statusDiv.classList.remove('success'); statusDiv.classList.add('error'); statusDiv.textContent = i18n.t('autoDetecting'); } } }, 2000); } return true; } // ===== 脚本初始化 ===== function initScript() { if (!currentPlatform) { return; } injectCustomStyle(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { setTimeout(() => initializePanel(), 2000); }); } else { setTimeout(() => initializePanel(), 2000); } } function initializePanel() { if (currentPlatform === 'claude') { if (/\/chat\/[a-zA-Z0-9-]+/.test(window.location.href)) { createFloatingPanel(); } let lastUrl = window.location.href; const observer = new MutationObserver(() => { if (window.location.href !== lastUrl) { lastUrl = window.location.href; setTimeout(() => { if (/\/chat\/[a-zA-Z0-9-]+/.test(lastUrl) && !document.getElementById(CONTROL_ID)) { createFloatingPanel(); } }, 1000); } }); observer.observe(document.body, { childList: true, subtree: true }); } else { createFloatingPanel(); } } initScript(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址