// ==UserScript==
// @name ChatGPT Conversation Exporter Plus
// @namespace http://tampermonkey.net/
// @version 2.2
// @description 优雅导出 ChatGPT 对话记录,支持 JSON 和 Markdown 格式
// @author Gao + GPT-4 + Claude
// @license Custom License
// @match https://*.dawuai.buzz/*
// @match https://*.dwai.world/*
// @match https://chatgpt.com/*
// @grant none
// ==/UserScript==
/*
您可以在个人设备上使用和修改该代码。
不得将该代码或其修改版本重新分发、再发布或用于其他公众渠道。
保留所有权利,未经授权不得用于商业用途。
*/
(function() {
'use strict';
// Python转换函数移植
function formatTimestamp(timestamp) {
if (!timestamp) return null;
const dt = new Date(timestamp * 1000);
return dt.toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
}
function getModelInfo(message) {
if (!message || !message.metadata) return "";
const { model_slug, default_model_slug } = message.metadata;
if (model_slug && default_model_slug && model_slug !== default_model_slug) {
return ` [使用模型: ${model_slug}]`;
}
return "";
}
function generateFootnotes(text, contentReferences) {
const footnotes = [];
let footnoteIndex = 1;
let updatedText = text;
for (const ref of contentReferences || []) {
if (ref.type === 'webpage' && ref.url) {
const title = ref.title || ref.url;
updatedText += ` [^${footnoteIndex}]`;
footnotes.push(`[^${footnoteIndex}]: [${title}](${ref.url})`);
footnoteIndex++;
}
}
return [footnotes, updatedText];
}
function extractMessageParts(message) {
if (!message || !message.message) return [null, null, []];
const msg = message.message;
const timestamp = formatTimestamp(msg.create_time);
const authorName = msg.author?.name;
if (authorName && (authorName.startsWith('canmore.') || authorName.startsWith('dalle.'))) {
return [null, null, []];
}
const parts = msg.content?.parts || [];
if (!parts.length) return [null, null, []];
let text = parts.join(' ');
if (!text) return [null, null, []];
if (parts[0] && typeof parts[0] === 'string' && parts[0].includes("DALL-E displayed")) {
return [null, null, []];
}
const cleanedText = text.replace(/citeturn0news\d+/g, '');
const contentReferences = msg.metadata?.content_references || [];
const [footnotes, updatedText] = generateFootnotes(cleanedText, contentReferences);
return [updatedText, timestamp, footnotes];
}
function isCanvasRelated(node) {
if (!node?.message) return false;
const message = node.message;
const authorName = message.author?.name;
const recipient = message.recipient;
if ((authorName && String(authorName).includes('canmore.')) ||
(recipient && String(recipient).includes('canmore.'))) {
return true;
}
const metadata = message.metadata || {};
return metadata.canvas || String(metadata.command || '').includes('canvas');
}
function adjustHeaderLevels(text, increaseBy = 2) {
return text.replace(/^(#+)(.*?)$/gm, (match, hashes, rest) => {
return '#'.repeat(hashes.length + increaseBy) + rest;
});
}
// 状态追踪
let state = {
largestResponse: null,
largestResponseSize: 0,
largestResponseUrl: null,
lastUpdateTime: null
};
// 日志函数
const log = {
info: (msg) => console.log(`[Conversation Saver] ${msg}`),
error: (msg, e) => console.error(`[Conversation Saver] ${msg}`, e)
};
// 对话转换为Markdown的核心函数
function buildConversationTree(mapping, nodeId, indent = 0) {
if (!mapping[nodeId]) return [];
const node = mapping[nodeId];
const conversation = [];
if (!isCanvasRelated(node)) {
const [messageContent, timestamp, footnotes] = extractMessageParts(node);
if (messageContent) {
const role = node.message?.author?.role;
if (role === 'user' || role === 'assistant') {
const modelInfo = getModelInfo(node.message);
const timestampInfo = timestamp ? `\n\n*${timestamp}*` : "";
const prefix = role === 'user' ?
`## Human${timestampInfo}\n\n` :
`## Assistant${modelInfo}${timestampInfo}\n\n`;
const adjustedContent = adjustHeaderLevels(messageContent);
conversation.push(prefix + adjustedContent);
if (footnotes.length) {
conversation.push("\n" + footnotes.join("\n"));
}
}
}
}
for (const childId of (node.children || [])) {
conversation.push(...buildConversationTree(mapping, childId, indent + 1));
}
return conversation;
}
// 移除特殊标记
function removeCiteTurnAndNavlistMarkers(data) {
if (typeof data === 'object' && data !== null) {
if (data.content && data.content.parts) {
data.content.parts = data.content.parts
.filter(part => typeof part === 'string' && !part.includes("video 袁娅维"));
}
for (let key in data) {
data[key] = removeCiteTurnAndNavlistMarkers(data[key]);
}
return data;
} else if (Array.isArray(data)) {
return data.map(item => removeCiteTurnAndNavlistMarkers(item));
} else if (typeof data === 'string') {
let result = data.replace(/(citeturn|turn)0(news|search)\d+/g, '')
.replace(/^video.*?《.*?》MV\s*/gm, '')
.replace(/navlist.*?(?=\n|$)/g, '')
.replace(/\n\s*\n/g, '\n\n');
return result.trim();
}
return data;
}
// 获取默认模型
function getDefaultModel(data) {
if (!data?.mapping) return 'unknown';
for (const node of Object.values(data.mapping)) {
if (node.message?.author?.role === 'assistant') {
return node.message.metadata?.default_model_slug || 'unknown';
}
}
return 'unknown';
}
// 转换JSON到Markdown
function convertJsonToMarkdown(jsonData) {
// 移除PUA字符
const cleanData = JSON.parse(JSON.stringify(jsonData).replace(/[\uE000-\uF8FF]/g, ''));
// 移除特定标记
const processedData = removeCiteTurnAndNavlistMarkers(cleanData);
// 获取标题和默认模型
const title = processedData.title || 'Conversation';
const defaultModel = getDefaultModel(processedData);
// 获取根节点
const mapping = processedData.mapping;
const rootId = Object.keys(mapping).find(nodeId => !mapping[nodeId].parent);
// 构建对话
const conversation = buildConversationTree(mapping, rootId);
// 生成最终的Markdown内容
let markdownContent = `# ${title}-${defaultModel}\n\n`;
markdownContent += conversation.join('\n\n').replace(/\n{3,}/g, '\n\n');
return markdownContent;
}
// 响应处理函数
function processResponse(text, url) {
try {
const responseSize = text.length;
if (responseSize > state.largestResponseSize) {
state.largestResponse = text;
state.largestResponseSize = responseSize;
state.largestResponseUrl = url;
state.lastUpdateTime = new Date().toLocaleTimeString();
updateButtonsStatus();
log.info(`发现更大的响应 (${responseSize} bytes) 来自: ${url}`);
}
} catch (e) {
log.error('处理响应时出错:', e);
}
}
// 监听 fetch 请求
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const response = await originalFetch.apply(this, args);
const url = args[0];
if (url.includes('conversation/')) {
try {
const clonedResponse = response.clone();
clonedResponse.text().then(text => {
processResponse(text, url);
}).catch(e => {
log.error('解析fetch响应时出错:', e);
});
} catch (e) {
log.error('克隆fetch响应时出错:', e);
}
}
return response;
};
// 监听传统的 XHR 请求
const originalXhrOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
if (url.includes('conversation/')) {
this.addEventListener('load', function() {
try {
processResponse(this.responseText, url);
} catch (e) {
log.error('处理XHR响应时出错:', e);
}
});
}
return originalXhrOpen.apply(this, arguments);
};
// 更新按钮状态
function updateButtonsStatus() {
const jsonButton = document.getElementById('downloadJsonButton');
const mdButton = document.getElementById('downloadMdButton');
[jsonButton, mdButton].forEach(button => {
if (button) {
button.style.backgroundColor = state.largestResponse ? '#28a745' : '#007bff';
button.title = state.largestResponse
? `最后更新: ${state.lastUpdateTime}\n来源: ${state.largestResponseUrl}\n大小: ${(state.largestResponseSize / 1024).toFixed(2)}KB`
: '等待响应中...';
}
});
}
// 创建下载按钮
function createDownloadButtons() {
const buttonStyles = {
position: 'fixed',
top: '45%',
right: '0px',
zIndex: '9999',
padding: '10px',
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'
};
// JSON下载按钮
const jsonButton = document.createElement('button');
jsonButton.id = 'downloadJsonButton';
jsonButton.innerText = '下载JSON';
Object.assign(jsonButton.style, buttonStyles);
// MD下载按钮
const mdButton = document.createElement('button');
mdButton.id = 'downloadMdButton';
mdButton.innerText = '下载MD';
Object.assign(mdButton.style, buttonStyles);
mdButton.style.right = '100px'; // 设置MD按钮在JSON按钮左边
// 鼠标悬停效果
[jsonButton, mdButton].forEach(button => {
button.onmouseover = () => {
button.style.transform = 'scale(1.05)';
button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.3)';
};
button.onmouseout = () => {
button.style.transform = 'scale(1)';
button.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
};
});
// 下载功能
jsonButton.onclick = function() {
if (!state.largestResponse) {
alert('还没有发现有效的会话记录。\n请等待页面加载完成或进行一些对话。');
return;
}
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
const chatName = document.title.trim().replace(/[/\\?%*:|"<>]/g, '-');
const fileName = `${chatName}_${timestamp}.json`;
const blob = new Blob([state.largestResponse], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.click();
log.info(`成功下载JSON文件: ${fileName}`);
} catch (e) {
log.error('下载JSON过程中出错:', e);
alert('下载过程中发生错误,请查看控制台了解详情。');
}
};
mdButton.onclick = function() {
if (!state.largestResponse) {
alert('还没有发现有效的会话记录。\n请等待页面加载完成或进行一些对话。');
return;
}
try {
const jsonData = JSON.parse(state.largestResponse);
const markdownContent = convertJsonToMarkdown(jsonData);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
const chatName = document.title.trim().replace(/[/\\?%*:|"<>]/g, '-');
const fileName = `${chatName}_${timestamp}.md`;
const blob = new Blob([markdownContent], { type: 'text/markdown' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.click();
log.info(`成功下载MD文件: ${fileName}`);
} catch (e) {
log.error('下载MD过程中出错:', e);
alert('下载过程中发生错误,请查看控制台了解详情。');
}
};
document.body.appendChild(jsonButton);
document.body.appendChild(mdButton);
updateButtonsStatus();
}
// 页面加载完成后初始化
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('增强版会话保存脚本已启动');
});
})();