- // ==UserScript==
- // @name Claude 对话导出器 | Claude Conversation Exporter Plus
- // @namespace http://tampermonkey.net/
- // @version 4.6.4
- // @description 优雅导出 Claude 对话记录,支持 JSON 和 Markdown 格式。Elegantly export Claude conversation records, supporting JSON and Markdown formats.
- // @author Gao + GPT-4 + Claude
- // @license Custom License
- // @match https://*.claudesvip.top/chat/*
- // @match https://*.claude.ai/chat/*
- // @match https://*.fuclaude.com/chat/*
- // @match https://*.aikeji.vip/chat/*
- // @grant none
- // ==/UserScript==
-
- /*
- 您可以在个人设备上使用和修改该代码。
- 不得将该代码或其修改版本重新分发、再发布或用于其他公众渠道。
- 保留所有权利,未经授权不得用于商业用途。
- */
-
- /*
- You may use and modify this code on your personal devices.
- You may not redistribute, republish, or use this code or its modified versions in other public channels.
- All rights reserved. Unauthorized commercial use is prohibited.
- */
-
- (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();
- }
-
- function convertJsonToMd(data) {
- let mdContent = [];
- const title = document.title.trim().replace(/\s+-\s+Claude$/, '');
- mdContent.push(`# ${title}\n`);
-
- // 获取当前页面的完整 URL
- const currentUrl = window.location.href;
-
- // 提取 URL 前缀(去掉 chat/* 部分)
- const baseUrl = currentUrl.replace(/\/chat\/.*$/, '');
-
- 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']);
- mdContent.push(`${content}\n`);
-
- // === 处理附加文件部分开始 ===
- if (message['attachments'] && message['attachments'].length > 0) {
- mdContent.push(`## 附加文件:`);
-
- for (const attachment of message['attachments']) {
- // 判断文件是否有 preview_url 或 document_asset
- if (attachment.preview_url) {
- // 使用 preview_url 生成链接
- const previewLink = `${baseUrl}${attachment.preview_url}`;
- mdContent.push(`[${attachment.file_name}](${previewLink})\n`);
- } else if (attachment.document_asset && attachment.document_asset.url) {
- // 使用 document_asset.url 生成链接
- const documentLink = `${baseUrl}${attachment.document_asset.url}`;
- mdContent.push(`[${attachment.file_name}](${documentLink})\n`);
- } else if (attachment.extracted_content) {
- // 有具体内容的文件
- mdContent.push(`${attachment.file_name}\n`);
- mdContent.push("```\n");
- mdContent.push(`${attachment.extracted_content}\n`);
- mdContent.push("```\n");
- } else {
- // 无法提取内容或生成链接的文件
- mdContent.push(`${attachment.file_name} (无法提取内容或生成链接)\n`);
- }
- }
- }
-
- // === 处理 `files_v2` 部分开始 ===
- if (message['files_v2'] && message['files_v2'].length > 0) {
- mdContent.push(`## 附加文件:`);
-
- for (const file of message['files_v2']) {
- if (file.document_asset && file.document_asset.url) {
- // 处理 `document_asset` 链接
- const documentLink = `${baseUrl}${file.document_asset.url}`;
- mdContent.push(`[${file.file_name}](${documentLink})\n`);
- } else if (file.preview_url) {
- // 处理常规的 `preview_url` 链接
- const previewLink = `${baseUrl}${file.preview_url}`;
- mdContent.push(`[${file.file_name}](${previewLink})\n`);
- } else {
- mdContent.push(`${file.file_name} (无法生成预览链接)\n`);
- }
- }
- }
- // === 处理 `files_v2` 部分结束 ===
- }
-
- 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 保存脚本已启动');
- });
- })();