// ==UserScript== // @name DeepSeek 对话导出器 优化版(Optimized version of dialogue exporter) // @namespace http://tampermonkey.net/ // @version 1.0.2 // @description 1. 优化了旧版UI,现在不在阻挡视野 2. 针对ds缓冲机制进行了优化,现在无需 重新登陆/继续对话 也可以导出对话 // @author 口吃者 // @match https://chat.deepseek.com/a/chat/s/* // @icon https://www.google.com/s2/favicons?sz=64&domain=deepseek.com // @require https://scriptcat.org/lib/2691/1.0.0/sweetalert2.all.min-11.15.10.js // @grant none // @run-at document-start // @license MIT // ==/UserScript== var comment_params = { "chat_session_id": '' }; var headersAuthorization = ''; class DsExportTool { constructor(sessionId = '', jsonData = '', markdownData = '') { this.sessionId = sessionId; this.jsonData = jsonData; this.markdownData = markdownData; } // 导出JSON文件方法 exportDsJsonData() { if (!this.jsonData) { console.error('No JSON data to export'); return; } let outputData; if (typeof this.jsonData === 'string') { // 如果数据是字符串,尝试解析为对象(确保有效性) try { outputData = JSON.parse(this.jsonData); } catch (e) { console.error('Invalid JSON string:', e); return; } } else { // 如果已经是对象/数组,直接使用 outputData = this.jsonData; } // 生成格式化的JSON(仅需一次序列化) const jsonString = JSON.stringify(outputData, null, 2); // 创建JSON类型Blob(修正MIME类型) const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8' }); // 生成带时间戳和会话ID的文件名(示例:chat_export_12345_20230815.json) const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, ''); const filename = `chat_export_${this.sessionId || 'unknown'}_${timestamp}.json`; // 创建下载链接 const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; anchor.style.display = 'none'; // 触发下载 document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); URL.revokeObjectURL(url); } // 新增 Markdown 导出方法 exportDsMarkdownData() { // 注意方法名驼峰式命名 if (!this.markdownData) { console.error('No Markdown data to export'); return; } // 创建标准 Markdown Blob(指定 MIME 类型) const blob = new Blob([this.markdownData], { type: 'text/markdown;charset=utf-8' // 或使用 text/plain }); // 生成带时间戳的文件名(示例:chat_history_12345_20230815.md) const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, ''); const filename = `chat_history_${this.sessionId || 'unknown'}_${timestamp}.md`; // 创建并触发下载链接 const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; anchor.style.display = 'none'; document.body.appendChild(anchor); anchor.click(); // 清理资源 document.body.removeChild(anchor); URL.revokeObjectURL(url); } } const dsExportTool = new DsExportTool(); (function () { 'use strict'; const originalOpen = XMLHttpRequest.prototype.open; const authorizationParamsReady = new Promise((resolve) => { // 保存原始 open 方法 const originalOpen = XMLHttpRequest.prototype.open; // 保存原始 send 方法(关键!) const originalSend = XMLHttpRequest.prototype.send; // 保存原始 setRequestHeader 方法 const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; // 重写 setRequestHeader 以捕获请求头 XMLHttpRequest.prototype.setRequestHeader = function (name, value) { this._requestHeaders = this._requestHeaders || {}; this._requestHeaders[name.toLowerCase()] = value; originalSetRequestHeader.apply(this, arguments); }; // 重写 open 方法 XMLHttpRequest.prototype.open = function (method, url) { // 先调用原始 open(确保兼容性) originalOpen.apply(this, arguments); // 仅监听目标 URL if (['chat/history_messages'].some(substring => url.includes(substring))) { // 重写 send 方法以在请求发送时捕获 Authorization const _this = this; this.send = function (body) { // 从缓存的请求头中获取 Authorization const authHeader = _this._requestHeaders?.authorization; if (authHeader) { headersAuthorization = authHeader; // 监听请求完成 _this.addEventListener('readystatechange', function () { if (this.readyState === 4 && this.status === 200) { resolve({ authorization: authHeader }); } }); } // 调用原始 send originalSend.call(this, body); }; } }; }); window.addEventListener('load', addPanel); })(); function addPanel() { function genButton(text, foo, id, fooParams = {}) { let b = document.createElement('button'); b.textContent = text; b.style.verticalAlign = 'inherit'; // 使用箭头函数创建闭包来保存 fooParams 并传递给 foo b.addEventListener('click', () => { foo.call(b, ...Object.values(fooParams)); // 使用 call 方法确保 this 指向按钮对象 }); if (id) { b.id = id }; return b; } function changeRangeDynamics() { const value = parseInt(this.value, 10); const roundedValue = Math.ceil(value / 10) * 10; targetAmountGlobal = roundedValue; // 只能通过 DOM 方法改变 document.querySelector('#swal-range > output').textContent = roundedValue; } async function openPanelFunc() { let isLoadEnd = false; const { value: formValues } = await Swal.fire({ title: "选择导出类型", showCancelButton: true, cancelButtonText: '取消', confirmButtonText: '确定', //class="swal2-range" swalalert框架可能会对其有特殊处理,导致其内标签的id声明失效 html: `
`, focusConfirm: false, didOpen: () => { // const swalRange = document.querySelector('#swal-range input'); // swalRange.addEventListener('input', changeRangeDynamics); document.querySelector('.swal2-radio > input[type=radio]:nth-child(1)').checked = true; }, willClose: () => { // 在关闭前清除事件监听器以防止内存泄漏 // const swalRange = document.querySelector('#swal-range input'); // swalRange.removeEventListener('input', changeRangeDynamics); }, preConfirm: () => { return [ document.querySelector('.swal2-radio>input[name="options"]:checked').value ]; } }); if (formValues) { dsExportOption = formValues[0]; exportDsByOption(dsExportOption); } } let myButton = genButton('DsExport', openPanelFunc, 'DsExport'); document.body.appendChild(myButton); var css_text = ` #DsExport { position: fixed; color: rgb(211, 67, 235); top: 70%; left: -20px;/* 初始状态下左半部分隐藏 */ transform: translateY(-50%); z-index: 1000; /* 确保按钮在最前面 */ padding: 10px 24px; border-radius: 5px; cursor: pointer; border: 0; background-color: white; box-shadow: rgb(0 0 0 / 5%) 0 0 8px; letter-spacing: 1.5px; text-transform: uppercase; font-size: 9px; transition: all 0.5s ease; } #DsExport:hover { left: 0%; /* 鼠标悬停时完整显示 */ letter-spacing: 3px; background-image: linear-gradient(to top, #fad0c4 0%, #fad0c4 1%, #ffd1ff 100%); box-shadow: rgba(211, 67, 235, 0.7) 0px 7px 29px 0px; /* 更柔和的紫色阴影,带透明度 */ } #DsExport:active { letter-spacing: 3px; background-image: linear-gradient(to top, #fad0c4 0%, #fad0c4 1%, #ffd1ff 100%); box-shadow: rgba(211, 67, 235, 0.5) 0px 0px 0px 0px; /* 活动状态下的阴影,保持一致性 */ transition: 100ms; } ` GMaddStyle(css_text); } function getFinalCommentUrl(params) { // 指定参数的顺序 const orderKeys = ["chat_session_id"]; // 按照指定顺序构建参数列表 const orderedParams = orderKeys .filter(key => params.hasOwnProperty(key)) .map(key => key === 'pagination_str' ? `${key}=${encodeURIComponent(params[key])}` : `${key}=${params[key]}`); // 构建新的URL const newUrl = 'https://chat.deepseek.com/api/v0/chat/history_messages?' + orderedParams.join('&'); return newUrl; } async function fetchChatMessage() { const finalUrl = getFinalCommentUrl(comment_params); const response = await fetch(finalUrl, { headers: { 'authorization': headersAuthorization }, credentials: 'include' // 明确指定携带cookies }); return await response.json(); } function GMaddStyle(css) { var myStyle = document.createElement('style'); myStyle.textContent = css; var doc = document.head || document.documentElement; doc.appendChild(myStyle); } async function exportDsByOption(dsExportOption) { const currentUrl = window.location.href; const currentUrlParts = currentUrl.split('/'); const currentUrlLastPart = currentUrlParts[currentUrlParts.length - 1]; if (dsExportTool.sessionId != currentUrlLastPart) { dsExportTool.sessionId = currentUrlLastPart; comment_params["chat_session_id"] = dsExportTool.sessionId; const chatMessage = await fetchChatMessage(); dsExportTool.markdownData = convertJsonToMd(chatMessage); dsExportTool.jsonData = JSON.stringify(chatMessage); } if (dsExportOption === 'option1') { dsExportTool.exportDsJsonData(); } else if (dsExportOption === 'option2') { dsExportTool.exportDsMarkdownData(); } } function convertJsonToMd(data) { let mdContent = []; const title = data.data.biz_data.chat_session.title || 'Untitled Chat'; const totalTokens = data.data.biz_data.chat_messages.reduce((acc, msg) => acc + msg.accumulated_token_usage, 0); mdContent.push(`# DeepSeek - ${title} (Total Tokens: ${totalTokens})\n`); data.data.biz_data.chat_messages.forEach(msg => { const role = msg.role === 'USER' ? 'Human' : 'Assistant'; mdContent.push(`### ${role}`); const timestamp = new Date(msg.inserted_at * 1000).toISOString(); mdContent.push(`*${timestamp}*\n`); if (msg.files && msg.files.length > 0) { msg.files.forEach(file => { const insertTime = new Date(file.inserted_at * 1000).toISOString(); const updateTime = new Date(file.updated_at * 1000).toISOString(); mdContent.push(`### File Information`); mdContent.push(`- Name: ${file.file_name}`); mdContent.push(`- Size: ${file.file_size} bytes`); mdContent.push(`- Token Usage: ${file.token_usage}`); mdContent.push(`- Upload Time: ${insertTime}`); mdContent.push(`- Last Update: ${updateTime}\n`); }); } let content = msg.content; if (msg.search_results && msg.search_results.length > 0) { const citations = {}; msg.search_results.forEach((result, index) => { if (result.cite_index !== null) { citations[result.cite_index] = result.url; } }); content = content.replace(/\[citation:(\d+)\]/g, (match, p1) => { const url = citations[parseInt(p1)]; return url ? ` [${p1}](${url})` : match; }); content = content.replace(/\s+,/g, ',').replace(/\s+\./g, '.'); } if (msg.thinking_content) { const thinkingTime = msg.thinking_elapsed_secs ? `(${msg.thinking_elapsed_secs}s)` : ''; content += `\n\n**Thinking Process ${thinkingTime}:**\n${msg.thinking_content}`; } content = content.replace(/\$\$(.*?)\$\$/gs, (match, formula) => { return formula.includes('\n') ? `\n$$\n${formula}\n$$\n` : `$$${formula}$$`; }); mdContent.push(content + '\n'); }); return mdContent.join('\n'); }