// ==UserScript==
// @name Lyra's Exporter Fetch
// @name:en Lyra's Exporter Fetch
// @namespace userscript://lyra-conversation-exporter
// @version 6.3
// @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://github.com/Yalums/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;
// ===== i18n 国际化系统 =====
const i18n = {
languages: {
'zh': {
name: '中文',
short: '简体中文',
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',
short: '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;
},
getLanguageShort() {
return this.languages[this.currentLang]?.short || this.currentLang.toUpperCase();
}
};
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';
}
// ===== 全局变量 =====
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') {
const savedUserId = localStorage.getItem('lyraClaudeUserId');
if (savedUserId) {
capturedUserId = savedUserId;
}
// 方法1:解决沙箱隔离问题
const interceptorScript = document.createElement('script');
interceptorScript.textContent = `
(function() {
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);
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);
};
})();
`;
(document.head || document.documentElement).appendChild(interceptorScript);
interceptorScript.remove();
// 监听来自页面的用户ID捕获事件
window.addEventListener('lyraUserIdCaptured', (event) => {
const userId = event.detail.userId;
if (userId) {
capturedUserId = userId;
}
});
// 方法2:定期检查 localStorage(作为备用)
const checkLocalStorage = () => {
const storedId = localStorage.getItem('lyraClaudeUserId');
if (storedId && storedId !== capturedUserId) {
capturedUserId = storedId;
}
};
setInterval(checkLocalStorage, 2000);
// 方法3:DOM加载后尝试从页面提取
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
if (!capturedUserId) {
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);
}
}
});
}
}, 2000);
});
// 定期检查状态
setInterval(() => {
if (!capturedUserId) {
checkLocalStorage();
}
}, 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'));
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) {
// 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: ${currentPlatform === 'claude' ? '20px' : currentPlatform === 'gemini' ? '12px' : '12px'} !important;
bottom: ${currentPlatform === 'claude' ? '80px' : currentPlatform === 'gemini' ? '120px' : '100px'} !important; /* Claude往下移,Gemini往上移 */
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: ${currentPlatform === 'claude' ? '160px' : currentPlatform === 'gemini' ? '120px' : '140px'} !important; /* Claude恢复原宽度,Gemini继续缩短 */
max-width: ${currentPlatform === 'claude' ? '150px' : currentPlatform === 'gemini' ? '120px' : '120px'} !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: ${currentPlatform === 'claude' ? '5px' : currentPlatform === 'gemini' ? '3px' : '4px'} !important;
width: ${currentPlatform === 'claude' ? '36px' : currentPlatform === 'gemini' ? '24px' : '28px'} !important; /* Claude变大,Gemini变小 */
height: ${currentPlatform === 'claude' ? '36px' : currentPlatform === 'gemini' ? '24px' : '28px'} !important;
min-width: ${currentPlatform === 'claude' ? '36px' : currentPlatform === 'gemini' ? '24px' : '28px'} !important;
justify-content: center !important;
align-items: center !important;
overflow: hidden !important;
}
#${TOGGLE_ID} {
position: absolute !important;
left: ${currentPlatform === 'claude' ? '-22px' : '-18px'} !important; /* Claude的按钮往左移 */
top: 50% !important;
transform: translateY(-50%) !important;
width: ${currentPlatform === 'claude' ? '22px' : currentPlatform === 'gemini' ? '18px' : '20px'} !important;
height: ${currentPlatform === 'claude' ? '22px' : currentPlatform === 'gemini' ? '18px' : '20px'} !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: ${currentPlatform === 'claude' ? '116px' : currentPlatform === 'gemini' ? '100px' : '116px'} !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: 4px 8px !important; border-radius: 12px !important;
background: #f1f3f4 !important; cursor: pointer !important; transition: all 0.2s !important;
width: 100% !important; box-sizing: border-box !important;
white-space: nowrap !important; overflow: hidden !important;
text-overflow: ellipsis !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函数 =====
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;
}
// ===== 图片获取函数 =====
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 failed with status: ${response.status}`));
}
},
onerror: function(error) {
reject(new Error(`GM_xmlhttpRequest network error: ${error.statusText}`));
}
});
});
}
// ===== Gemini图片处理函数 =====
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) {
// Error parsing URL
}
}
}
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) {
return null;
}
}
// ===== Gemini数据提取函数 =====
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数据提取函数 =====
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相关函数 =====
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;
}
// ===== 获取对话标题函数 =====
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) {
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) {
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`;
const response = await fetch(apiUrl);
if (!response.ok) {
throw new Error(`${i18n.t('fetchFailed')}: ${response.status}`);
}
return await response.json();
} catch (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) {
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' : ''}`;
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) {
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) {
// Error sending handshake
}
}, 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);
}, 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) {
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) {
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);
} 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.getLanguageShort()}`;
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();
})();