// ==UserScript==
// @name Claude Conversation Exporter Plus
// @namespace http://tampermonkey.net/
// @version 4.6
// @description 优雅导出 Claude 对话记录,支持 JSON 和 Markdown 格式
// @author Gao + GPT-4 + Claude
// @license Custom License
// @match https://*.claudesvip.top/chat/*
// @match https://*.claude.ai/chat/*
// @match https://*.fuclaude.com/chat/*
// @grant none
// ==/UserScript==
/*
您可以在个人设备上使用和修改该代码。
不得将该代码或其修改版本重新分发、再发布或用于其他公众渠道。
保留所有权利,未经授权不得用于商业用途。
*/
(function() {
'use strict';
// 状态追踪
let state = {
targetResponse: null,
lastUpdateTime: null,
convertedMd: null
};
// 日志函数
const log = {
info: (msg) => console.log(`[Claude Saver] ${msg}`),
error: (msg, e) => console.error(`[Claude Saver] ${msg}`, e)
};
// 正则表达式用于匹配目标 URL
const targetUrlPattern = /\/chat_conversations\/[\w-]+\?tree=True&rendering_mode=messages&render_all_tools=true/;
// 响应处理函数(处理符合匹配模式的响应)
function processTargetResponse(text, url) {
try {
if (targetUrlPattern.test(url)) {
state.targetResponse = text;
state.lastUpdateTime = new Date().toLocaleTimeString();
updateButtonStatus();
log.info(`成功捕获目标响应 (${text.length} bytes) 来自: ${url}`);
// 转换为Markdown
state.convertedMd = convertJsonToMd(JSON.parse(text));
log.info('成功将JSON转换为Markdown');
}
} catch (e) {
log.error('处理目标响应时出错:', e);
}
}
// 更新按钮状态
function updateButtonStatus() {
const jsonButton = document.getElementById('downloadJsonButton');
const mdButton = document.getElementById('downloadMdButton');
if (jsonButton && mdButton) {
const hasResponse = state.targetResponse !== null;
jsonButton.style.backgroundColor = hasResponse ? '#28a745' : '#007bff';
mdButton.style.backgroundColor = state.convertedMd ? '#28a745' : '#007bff';
const statusText = hasResponse ? `最后更新: ${state.lastUpdateTime}
数据已准备好` : '等待目标响应中...';
jsonButton.title = statusText;
mdButton.title = statusText;
}
}
// 创建下载按钮
function createDownloadButtons() {
// JSON 下载按钮
const jsonButton = document.createElement('button');
const mdButton = document.createElement('button');
const buttonStyles = {
padding: '10px 15px',
backgroundColor: '#007bff',
color: '#ffffff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
transition: 'all 0.3s ease',
fontFamily: 'Arial, sans-serif',
boxShadow: '0 2px 5px rgba(0,0,0,0.2)',
whiteSpace: 'nowrap',
marginRight: '10px'
};
jsonButton.id = 'downloadJsonButton';
jsonButton.innerText = '下载 JSON';
mdButton.id = 'downloadMdButton';
mdButton.innerText = '下载 Markdown';
Object.assign(jsonButton.style, buttonStyles);
Object.assign(mdButton.style, buttonStyles);
// 鼠标悬停效果
const onMouseOver = (button) => {
button.style.transform = 'scale(1.05)';
button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
};
const onMouseOut = (button) => {
button.style.transform = 'scale(1)';
button.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
};
jsonButton.onmouseover = () => onMouseOver(jsonButton);
jsonButton.onmouseout = () => onMouseOut(jsonButton);
mdButton.onmouseover = () => onMouseOver(mdButton);
mdButton.onmouseout = () => onMouseOut(mdButton);
// 下载 JSON 功能
jsonButton.onclick = function() {
if (!state.targetResponse) {
alert(`还没有发现有效的对话记录。
请等待目标响应或进行一些对话。`);
return;
}
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
const chatName = document.title.trim().replace(/\s+-\s+Claude$/, '').replace(/[\/\\?%*:|"<>]/g, '-');
const fileName = `${chatName}_${timestamp}.json`;
const blob = new Blob([state.targetResponse], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.click();
log.info(`成功下载文件: ${fileName}`);
} catch (e) {
log.error('下载过程中出错:', e);
alert('下载过程中发生错误,请查看控制台了解详情。');
}
};
// 下载 Markdown 功能
mdButton.onclick = function() {
if (!state.convertedMd) {
alert(`还没有发现有效的对话记录。
请等待目标响应或进行一些对话。`);
return;
}
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
const chatName = document.title.trim().replace(/\s+-\s+Claude$/, '').replace(/[\/\\?%*:|"<>]/g, '-');
const fileName = `${chatName}_${timestamp}.md`;
const blob = new Blob([state.convertedMd], { type: 'text/markdown' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.click();
log.info(`成功下载文件: ${fileName}`);
} catch (e) {
log.error('下载过程中出错:', e);
alert('下载过程中发生错误,请查看控制台了解详情。');
}
};
const buttonContainer = document.createElement('div');
buttonContainer.style.position = 'fixed';
buttonContainer.style.top = '45%';
buttonContainer.style.right = '10px';
buttonContainer.style.transform = 'translateY(-50%)';
buttonContainer.style.zIndex = '9999';
buttonContainer.style.display = 'flex';
buttonContainer.style.flexDirection = 'row';
buttonContainer.appendChild(jsonButton);
buttonContainer.appendChild(mdButton);
document.body.appendChild(buttonContainer);
updateButtonStatus();
}
// JSON 转 Markdown 转换函数
function convertJsonToMd(data) {
let mdContent = [];
const title = document.title.trim().replace(/\s+-\s+Claude$/, '');
mdContent.push(`# ${title}\n`);
for (const message of data['chat_messages']) {
const sender = message['sender'].charAt(0).toUpperCase() + message['sender'].slice(1);
mdContent.push(`## ${sender}`);
const createdAt = message['created_at'] || '';
const updatedAt = message['updated_at'] || '';
const timestamp = createdAt === updatedAt ? `*${createdAt}*` : `*${createdAt} (updated)*`;
mdContent.push(timestamp);
const content = processContent(message['content']);
// 如果是assistant的消息,调整其中的标题级别
if (sender.toLowerCase() === 'assistant') {
mdContent.push(adjustHeadingLevel(content));
} else {
mdContent.push(`${content}\n`);
}
}
return mdContent.join('\n');
}
// 调整Markdown标题级别
function adjustHeadingLevel(text, increaseLevel = 2) {
const codeBlockPattern = /```[\s\S]*?```/g;
let segments = [];
let match;
// 提取代码块,并用占位符替代
let lastIndex = 0;
while ((match = codeBlockPattern.exec(text)) !== null) {
segments.push(text.substring(lastIndex, match.index));
segments.push(match[0]); // 保留代码块原样
lastIndex = codeBlockPattern.lastIndex;
}
segments.push(text.substring(lastIndex));
// 调整标题级别
segments = segments.map(segment => {
if (segment.startsWith('```')) {
return segment; // 保留代码块原样
} else {
let lines = segment.split('\n');
lines = lines.map(line => {
if (line.trim().startsWith('#')) {
const currentLevel = (line.match(/^#+/) || [''])[0].length;
return '#'.repeat(currentLevel + increaseLevel) + line.slice(currentLevel);
}
return line;
});
return lines.join('\n');
}
});
return segments.join('');
}
// 处理消息内容,提取纯文本并处理LaTeX公式
function processContent(content) {
if (Array.isArray(content)) {
let textParts = [];
for (const item of content) {
if (item.type === 'text') {
let text = item.text || '';
text = processLatex(text);
text = text.replace(/(?<!\n)(\n\| .*? \|\n\|[-| ]+\|\n(?:\| .*? \|\n)+)/g, '\n$1'); // 在表格前插入一个空行
textParts.push(text);
}
}
return textParts.join('\n');
}
return String(content);
}
// 处理LaTeX公式
function processLatex(text) {
// 区分行内公式和独立公式
text = text.replace(/\$\$(.+?)\$\$/gs, (match, formula) => {
if (formula.includes('\n')) {
// 这是独立公式
return `$$${formula}$$`;
} else {
// 这是行内公式
return `$${formula}$`;
}
});
return text;
}
// 监听 fetch 请求
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const response = await originalFetch.apply(this, args);
const url = args[0];
log.info(`捕获到 fetch 请求: ${url}`);
if (targetUrlPattern.test(url)) {
try {
log.info(`匹配到目标 URL: ${url}`);
const clonedResponse = response.clone();
clonedResponse.text().then(text => {
processTargetResponse(text, url);
}).catch(e => {
log.error('解析fetch响应时出错:', e);
});
} catch (e) {
log.error('克隆fetch响应时出错:', e);
}
}
return response;
};
// 页面加载完成后立即创建按钮
window.addEventListener('load', function() {
createDownloadButtons();
// 使用 MutationObserver 确保按钮始终存在
const observer = new MutationObserver(() => {
if (!document.getElementById('downloadJsonButton') || !document.getElementById('downloadMdButton')) {
log.info('检测到按钮丢失,正在重新创建...');
createDownloadButtons();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
log.info('Claude 保存脚本已启动');
});
})();