您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
kimi聊天记录导出, 支持 json 和 markdown 格式, 包括思维过程,不含搜索链接
// ==UserScript== // @name Kimi 对话导出器 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description kimi聊天记录导出, 支持 json 和 markdown 格式, 包括思维过程,不含搜索链接 // @author snowsoul // @match https://kimi.moonshot.cn/chat/* // @require https://update.greasyfork.org/scripts/498507/1398070/sweetalert2.js // @license MIT // ==/UserScript== var comment_params = { "chat_session_id": '' }; var headersAuthorization = ''; class DsExportTool { constructor(sessionId = '', title, jsonData = '', markdownData = '') { this.sessionId = sessionId; this.title = title; 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.title || 'Undefined'}_${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.title || 'Undefined'}_${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 sessionId = window.location.pathname.split('/').pop(); const currentUrl = window.location.href; const currentUrlParts = currentUrl.split('/'); const sessionId = currentUrlParts[currentUrlParts.length - 1]; const open = XMLHttpRequest.prototype.open; const send = XMLHttpRequest.prototype.send; const setRequestHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.open = function(method, url) { this._requestURL = url; this._requestMethod = method; this._intercept = url.includes(`/api/chat/${sessionId}/segment/scroll`); // 只标记匹配的请求 return open.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function(header, value) { if (this._intercept && header.toLowerCase() === 'authorization') { headersAuthorization = value; // console.log('Intercepted Authorization:', value); } return setRequestHeader.apply(this, arguments); }; XMLHttpRequest.prototype.send = function(body) { if (this._intercept) { this.addEventListener('readystatechange', function() { // if (this.readyState === 4) { // console.log('Intercepted XHR Request:'); // console.log('Method:', this._requestMethod); // console.log('URL:', this._requestURL); // console.log('Response:', this.responseText); // } }); } return send.apply(this, arguments); }; // 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 ([`api/chat/${sessionId}`].some(substring => url.includes(substring)) && url.endswith(`scroll`)) { // // 重写 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: ` <div class="swal2-radio"> <input type="radio" id="option1" name="options" value="option1" checked> <label for="option1"><span class="swal2-label" checked>Json</span></label> <input type="radio" id="option2" name="options" value="option2"> <label for="option2"><span class="swal2-label">Markdown</span></label> </div> `, 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('&'); const newUrl = `https://kimi.moonshot.cn/api/chat/${params.chat_session_id}/segment/scroll`; return newUrl; } async function fetchChatMessage() { const finalUrl = getFinalCommentUrl(comment_params); const response = await fetch(finalUrl, { method: 'POST', headers: { 'authorization': headersAuthorization, "Content-Type": "application/json", 'Content-Length': '11', }, body: JSON.stringify({}), // 必须包含请求体 credentials: 'include' // 明确指定携带cookies }); return await response.json(); } async function fetchTitleMessage() { const titleUrl = `https://kimi.moonshot.cn/api/chat/${comment_params.chat_session_id}` const response = await fetch(titleUrl, { // method: 'POST', headers: { 'authorization': headersAuthorization, // "Content-Type": "application/json", // 'Content-Length': '11', }, // body: JSON.stringify({}), // 必须包含请求体 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(); const titleMessage = await fetchTitleMessage(); // console.log(titleMessage); dsExportTool.title = titleMessage.name || 'Untitled Chat'; dsExportTool.markdownData = convertJsonToMd(chatMessage, titleMessage); dsExportTool.jsonData = JSON.stringify(chatMessage); } if (dsExportOption === 'option1') { dsExportTool.exportDsJsonData(); } else if (dsExportOption === 'option2') { dsExportTool.exportDsMarkdownData(); } } function convertJsonToMd(data, titleMessage) { let mdContent = []; const title = titleMessage.name || 'Undefined'; // const totalTokens = data.data.biz_data.chat_messages.reduce((acc, msg) => acc + msg.accumulated_token_usage, 0); mdContent.push(`# ${title}\n`); data.items.forEach(item => { const role = item.role === 'user' ? 'Human' : 'Assistant'; mdContent.push(`### ${role}`); const timestamp = item.created_at; mdContent.push(`*${timestamp}*\n`); // 解析内容结构 item.contents.zones.forEach(zone => { zone.sections.forEach(section => { // 提取思考过程 if (section.view === 'k1' && section.k1?.text) { mdContent.push("**推理过程**\n> "); mdContent.push(section.k1.text .replace(/^\n+|\n+$/g, '') .replace(/\n{2,}/g, '\n') + '\n'); } // 处理正式回复 // mdContent.push("**正式回复**\n"); if (section.view === 'cmpl' && section.cmpl) { let content = section.cmpl .replace(/\[citation:\d+\]/g, '') // 移除引用标记 .replace(/\n{3,}/g, '\n\n'); // 压缩多余空行 // 保留代码块格式 content = content.replace(/```([\s\S]*?)```/g, '\n```$1```\n'); mdContent.push(content); } }); }); mdContent.push('\n---\n'); // 消息分隔线 }); return mdContent.join('\n'); }