// ==UserScript==
// @name Lyra's Universal AI Exporter (ChromeXt兼容版)
// @namespace userscript://lyra-universal-exporter-chromext
// @version 0.1
// @description ChromeXt优化版:支持Claude、Gemini、NotebookLM、Google AI Studio的AI对话导出工具
// @author Yalums
// @match https://pro.easychat.top/*
// @match https://claude.ai/*
// @match https://gemini.google.com/*
// @match https://notebooklm.google.com/*
// @match https://aistudio.google.com/*
// @run-at document-end
// @license GNU General Public License v3.0
// ==/UserScript==
(function() {
'use strict';
// ChromeXt兼容性配置
const CHROMEXT_CONFIG = {
INIT_DELAY: 5000, // 增加初始化延迟
RETRY_TIMES: 10, // 重试次数
RETRY_INTERVAL: 2000, // 重试间隔
USE_LEGACY_API: true, // 使用传统API
DEBUG_MODE: true // 调试模式
};
function debugLog(msg) {
if (CHROMEXT_CONFIG.DEBUG_MODE) {
console.log(`[Lyra ChromeXt] ${msg}`);
}
}
// 安全的样式注入(ChromeXt兼容)
function injectStyleSafely(cssText) {
try {
// 方法1:尝试创建style标签
const style = document.createElement('style');
style.textContent = cssText;
// 尝试多个注入点
const targets = [
document.head,
document.body,
document.documentElement
];
for (const target of targets) {
if (target) {
target.appendChild(style);
debugLog('Style injected to: ' + target.tagName);
return true;
}
}
} catch (e) {
debugLog('Style injection failed: ' + e.message);
}
// 方法2:如果失败,尝试内联样式
return false;
}
// 安全的元素创建(ChromeXt兼容)
function createElementSafely(tag, properties = {}, innerHTML = '') {
try {
const element = document.createElement(tag);
// 使用setAttribute而不是直接赋值(更兼容)
for (const [key, value] of Object.entries(properties)) {
if (key === 'style' && typeof value === 'object') {
// 处理样式对象
for (const [styleProp, styleValue] of Object.entries(value)) {
element.style[styleProp] = styleValue;
}
} else if (key === 'className') {
element.className = value;
} else {
element.setAttribute(key, value);
}
}
if (innerHTML) {
element.innerHTML = innerHTML;
}
return element;
} catch (e) {
debugLog('Element creation failed: ' + e.message);
return null;
}
}
// 等待元素出现(ChromeXt优化版)
function waitForElement(selector, timeout = 30000) {
return new Promise((resolve) => {
const startTime = Date.now();
function check() {
const element = document.querySelector(selector);
if (element) {
debugLog(`Element found: ${selector}`);
resolve(element);
return;
}
if (Date.now() - startTime > timeout) {
debugLog(`Timeout waiting for: ${selector}`);
resolve(null);
return;
}
// 使用requestAnimationFrame代替setTimeout(更可靠)
if (window.requestAnimationFrame) {
requestAnimationFrame(check);
} else {
setTimeout(check, 100);
}
}
check();
});
}
// 核心初始化函数(ChromeXt优化)
async function initializeScript() {
debugLog('Starting ChromeXt compatible initialization...');
// 检测平台
const hostname = window.location.hostname;
let platform = '';
if (hostname.includes('easychat.top') || hostname.includes('claude.ai')) {
platform = 'claude';
} else if (hostname.includes('gemini.google.com')) {
platform = 'gemini';
} else if (hostname.includes('notebooklm.google.com')) {
platform = 'notebooklm';
} else if (hostname.includes('aistudio.google.com')) {
platform = 'aistudio';
}
debugLog(`Platform detected: ${platform}`);
if (!platform) {
debugLog('Unsupported platform');
return;
}
// 等待页面基本结构加载
if (platform === 'gemini') {
await waitForElement('c-wiz[jsrenderer], main[data-test-id], .chat-container, body');
} else if (platform === 'notebooklm') {
await waitForElement('mat-sidenav-content, router-outlet, body');
} else if (platform === 'aistudio') {
await waitForElement('ms-app, mat-sidenav-content, body');
}
// 创建简化的浮动按钮(内联样式,更兼容)
const button = createElementSafely('div', {
id: 'lyra-chromext-button',
style: {
position: 'fixed',
right: '20px',
bottom: '80px',
width: '56px',
height: '56px',
backgroundColor: '#1a73e8',
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
zIndex: '2147483647',
color: 'white',
fontSize: '24px',
fontWeight: 'bold',
userSelect: 'none'
}
}, '↓');
// 添加悬停效果(使用事件而不是CSS)
button.addEventListener('mouseenter', function() {
this.style.backgroundColor = '#1557b0';
this.style.transform = 'scale(1.1)';
});
button.addEventListener('mouseleave', function() {
this.style.backgroundColor = '#1a73e8';
this.style.transform = 'scale(1)';
});
// 点击事件
button.addEventListener('click', function() {
debugLog('Export button clicked');
// 根据平台执行不同的导出逻辑
if (platform === 'gemini') {
exportGeminiChat();
} else if (platform === 'notebooklm') {
exportNotebookLMChat();
} else if (platform === 'aistudio') {
exportAIStudioChat();
}
});
// 尝试多次添加按钮到页面
let added = false;
for (let i = 0; i < CHROMEXT_CONFIG.RETRY_TIMES; i++) {
if (document.body) {
document.body.appendChild(button);
debugLog('Button added to page');
added = true;
break;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
if (!added) {
debugLog('Failed to add button to page');
}
// 添加提示文字
const tooltip = createElementSafely('div', {
id: 'lyra-chromext-tooltip',
style: {
position: 'fixed',
right: '80px',
bottom: '90px',
backgroundColor: 'rgba(0,0,0,0.8)',
color: 'white',
padding: '8px 12px',
borderRadius: '4px',
fontSize: '14px',
whiteSpace: 'nowrap',
display: 'none',
zIndex: '2147483646'
}
}, 'Export Chat to JSON');
if (document.body) {
document.body.appendChild(tooltip);
}
// 显示/隐藏提示
button.addEventListener('mouseenter', function() {
tooltip.style.display = 'block';
});
button.addEventListener('mouseleave', function() {
tooltip.style.display = 'none';
});
}
// 简化的导出函数
function exportGeminiChat() {
debugLog('Starting Gemini export...');
const messages = [];
const turns = document.querySelectorAll('div.conversation-turn, div.single-turn');
turns.forEach(turn => {
const userQuery = turn.querySelector('.query-text, user-query')?.innerText?.trim();
const modelResponse = turn.querySelector('.model-response-text, .markdown-content')?.innerText?.trim();
if (userQuery || modelResponse) {
messages.push({
human: userQuery || '',
assistant: modelResponse || ''
});
}
});
downloadJSON(messages, 'Gemini_Chat');
}
function exportNotebookLMChat() {
debugLog('Starting NotebookLM export...');
const messages = [];
const turns = document.querySelectorAll('.chat-message-pair, chat-message');
turns.forEach(turn => {
const userMsg = turn.querySelector('.from-user-container')?.innerText?.trim();
const botMsg = turn.querySelector('.to-user-container')?.innerText?.trim();
if (userMsg || botMsg) {
messages.push({
human: userMsg || '',
assistant: botMsg || ''
});
}
});
downloadJSON(messages, 'NotebookLM_Chat');
}
function exportAIStudioChat() {
debugLog('Starting AI Studio export...');
const messages = [];
const turns = document.querySelectorAll('ms-chat-turn');
turns.forEach(turn => {
const userTurn = turn.querySelector('.user-prompt-container')?.innerText?.trim();
const modelTurn = turn.querySelector('.model-response-container')?.innerText?.trim();
if (userTurn || modelTurn) {
messages.push({
human: userTurn || '',
assistant: modelTurn || ''
});
}
});
downloadJSON(messages, 'AIStudio_Chat');
}
// 下载JSON文件
function downloadJSON(data, prefix) {
try {
const timestamp = new Date().toISOString().replace(/:/g, '-').slice(0, 19);
const filename = `${prefix}_${timestamp}.json`;
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
debugLog(`Downloaded: ${filename}`);
// 显示成功提示
showToast('Export successful!');
} catch (e) {
debugLog('Download failed: ' + e.message);
alert('Export failed: ' + e.message);
}
}
// 简单的Toast提示
function showToast(message) {
const toast = createElementSafely('div', {
style: {
position: 'fixed',
bottom: '150px',
right: '20px',
backgroundColor: 'rgba(0,0,0,0.8)',
color: 'white',
padding: '12px 20px',
borderRadius: '4px',
zIndex: '2147483648'
}
}, message);
if (document.body) {
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
toast.remove();
}, 300);
}, 2000);
}
}
// 启动脚本
debugLog('Script loaded, waiting for initialization...');
// 使用多种方式确保脚本执行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(initializeScript, CHROMEXT_CONFIG.INIT_DELAY);
});
} else {
setTimeout(initializeScript, CHROMEXT_CONFIG.INIT_DELAY);
}
// 备用:监听页面变化
if (window.MutationObserver) {
let initialized = false;
const observer = new MutationObserver(() => {
if (!initialized && document.body) {
initialized = true;
setTimeout(initializeScript, CHROMEXT_CONFIG.INIT_DELAY);
observer.disconnect();
}
});
observer.observe(document.documentElement, { childList: true, subtree: true });
}
})();