您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
支持DeepSeek,纳米AI,腾讯元宝,kimi,通义千问和讯飞星火的对话导出功能,支持JSON、Markdown和word格式,豆包中的图片也能保存
// ==UserScript== // @name AI对话导出word/json/md - DeepSeek/纳米/腾讯元宝/Kimi/通义千问/讯飞星火/豆包 // @namespace http://tampermonkey.net/ // @version 2025.7.13-2 // @description 支持DeepSeek,纳米AI,腾讯元宝,kimi,通义千问和讯飞星火的对话导出功能,支持JSON、Markdown和word格式,豆包中的图片也能保存 // @author 各位大神 + deepseek + 春秋 // @match *://chat.deepseek.com/* // @match *://bot.n.cn/* // @match *://yuanbao.tencent.com/* // @match *://*.kimi.com/* // @match *://*.tongyi.com/* // @match *://*.xfyun.cn/* // @match *://*.doubao.com/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @license MIT // ==/UserScript== (function() { 'use strict'; // 配置常量 const CONFIG = { API_ENDPOINT_WORD: 'https://api.any2card.com/api/md-to-word', DEFAULT_TITLE: '未命名对话', BUTTON_OPACITY: 0.3, BUTTON_HOVER_OPACITY: 1, SPINNER_ANIMATION_DURATION: '1s', REL_AND_KNOWLEDGE: false // 默认值为false,表示默认不导出参考链接和知识库 }; // 平台URL模式 const PLATFORM_PATTERNS = { deepseek: /chat_session_id=/, ncn: /conversation\/info\?conversation_id=/, yuanbao: /\/api\/user\/agent\/conversation\/v1\/detail/, kimi: /\/api\/chat\/.+\/segment\/scroll/, tongyi: /\/dialog\/chat\/list/, iflytek: /\/iflygpt\/u\/chat_history\/all\//, doubao: /\/alice\/message\/list/, doubao_chat: /\/api\/chat\/list/ // 备用豆包聊天列表API }; // 状态管理 const state = { targetResponse: null, lastUpdateTime: null, convertedMd: null, platformType: null, messageStats: { totalTokens: 0, totalChars: 0, fileCount: 0, questions: 0, convTurns: 0 }, currentTitle: CONFIG.DEFAULT_TITLE, kimiSessionId: null, kimiTitleCache: null, authToken: null }; // 日志工具 const logger = { info: (msg, data) => console.log(`[AI对话导出] INFO: ${msg}`, data || ''), warn: (msg, data) => console.warn(`[AI对话导出] WARN: ${msg}`, data || ''), error: (msg, error) => console.error(`[AI对话导出] ERROR: ${msg}`, error || '') }; // 工具函数 const utils = { formatTimestamp: (timestamp) => { if (!timestamp) return 'N/A'; let dt; try { // 1. 检查是否是数字(Unix 时间戳) if (typeof timestamp === 'number') { // 判断是秒级还是毫秒级(通常毫秒级时间戳 >= 1e12) dt = new Date(timestamp < 1e12 ? timestamp * 1000 : timestamp); } // 2. 检查是否是字符串(ISO 8601 或类似格式) else if (typeof timestamp === 'string') { dt = new Date(timestamp); } // 3. 其他情况(如已经是 Date 对象) else { dt = new Date(timestamp); } // 检查日期是否有效 if (isNaN(dt.getTime())) { return 'Invalid Date'; } // 使用瑞典格式 (YYYY-MM-DD HH:MM:SS) 并替换特殊字符 return dt.toLocaleString('sv').replace(/[T \/]/g, '_'); } catch (e) { logger.error('时间戳格式化错误', e); return 'Invalid Date'; } }, adjustHeaderLevels: (text, increaseBy = 1) => { if (!text) return ''; return text.replace(/^(#+)(\s*)(.*?)\s*$/gm, (match, hashes, space, content) => { return '#'.repeat(hashes.length + increaseBy) + ' ' + content.trim(); }); }, getLocalTimestamp: () => { const d = new Date(); const offset = d.getTimezoneOffset() * 60 * 1000; return new Date(d.getTime() - offset).toISOString() .slice(0, 19) .replace(/T/g, '_') .replace(/:/g, '-'); }, sanitizeFilename: (name) => { return name.replace(/[\/\\?%*:|"<>]/g, '-'); }, createBlobDownload: (content, type, filename) => { try { const blob = new Blob([content], { type }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); return true; } catch (e) { logger.error('创建Blob下载失败', e); return false; } } }; // 平台特定的转换器 const platformConverters = { deepseek: (data) => { let mdContent = []; const title = data.data?.biz_data?.chat_session?.title || CONFIG.DEFAULT_TITLE; const totalTokens = data.data?.biz_data?.chat_messages?.reduce((acc, msg) => acc + (msg.accumulated_token_usage || 0), 0) || 0; mdContent.push(`# DeepSeek对话 - ${title}`); mdContent.push(`\n> 统计信息:累计Token用量 ${totalTokens}`); data.data?.biz_data?.chat_messages?.forEach(msg => { if (msg.role === 'USER') return; const role = msg.role === 'USER' ? 'Human' : 'Assistant'; mdContent.push(`### ${role}`); mdContent.push(`*${utils.formatTimestamp(msg.inserted_at)}*\n`); if (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?.length > 0) { const citations = {}; msg.search_results.forEach((result) => { 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'); }, ncn: (data) => { let mdContent = []; const meta = data.data || {}; mdContent.push(`# 纳米AI对话记录 - ${meta.title || CONFIG.DEFAULT_TITLE}`); mdContent.push(`**生成时间**: ${utils.formatTimestamp(data.timestamp) || '未知'}`); const totalChars = meta.messages?.reduce((acc, msg) => acc + (msg.result?.length || 0), 0) || 0; const questions = meta.messages?.reduce((acc, msg) => acc + (msg.ask_further?.length || 0), 0) || 0; mdContent.push(`\n> 统计信息:总字数 ${totalChars} | 后续问题 ${questions} 个`); meta.messages?.forEach(msg => { if (msg.file?.length) { mdContent.push('### 附件信息'); msg.file.forEach(file => { mdContent.push(`- ${file.title} (${(file.size / 1024).toFixed(1)}KB)`); }); } // 用户提问和AI回复 mdContent.push(`\n## 用户提问\n${msg.prompt || '无内容'}`); // 处理AI回复内容,移除引用标记 let cleanResult = msg.result || '无内容'; // 移除类似[[3]()][[7]()]的引用标记 cleanResult = cleanResult.replace(/\[\[\d+\]\(\)\]/g, ''); // 移除多余的换行和空格 cleanResult = cleanResult.replace(/\n{3,}/g, '\n').trim(); mdContent.push(`\n## AI回复\n${cleanResult}`); //mdContent.push(`## AI回复\n${msg.result || '无内容'}`); // 推荐追问 if (msg.ask_further?.length) { mdContent.push('### 推荐追问'); msg.ask_further.forEach(q => { mdContent.push(`- ${q.content}`); }); } //是否需要导出,通过全局配置进行选择 if(CONFIG.REL_AND_KNOWLEDGE){ // 参考链接 if (msg.refer_search?.length) { mdContent.push('\n### 参考来源'); msg.refer_search.forEach(ref => { mdContent.push(`- [${ref.title || '无标题'}](${ref.url}) - ${ref.summary || '无摘要'}`); if (ref.date) mdContent.push(` - 发布日期: ${ref.date}`); if (ref.site) mdContent.push(` - 来源网站: ${ref.site}`); }); } // 用户知识库引用 if (msg.user_knowledge?.length) { mdContent.push('\n### 知识库引用'); const uniqueFiles = new Map(); msg.user_knowledge.forEach(knowledge => { if (knowledge.file_name && !uniqueFiles.has(knowledge.file_id)) { uniqueFiles.set(knowledge.file_id, knowledge); mdContent.push(`- ${knowledge.file_name} (来自文件夹: ${knowledge.folder_id || '未知'})`); } }); } } mdContent.push('\n---\n'); }); return mdContent.join('\n'); }, yuanbao: (data) => { if (!data?.convs || !Array.isArray(data.convs)) { logger.error('无效的元宝数据', data); return '# 错误:无效的JSON数据\n\n无法解析对话内容。'; } let markdownContent = []; const title = data.sessionTitle || data.title || '元宝对话记录'; markdownContent.push(`# ${title}\n`); if (data.multiMediaInfo?.length > 0) { markdownContent.push('**包含的多媒体文件:**\n'); data.multiMediaInfo.forEach(media => { markdownContent.push(`* [${media.fileName || '未知文件'}](${media.url || '#'}) (${media.type || '未知类型'})\n`); }); markdownContent.push('---\n'); } const sortedConvs = [...data.convs].sort((a, b) => (a.index || 0) - (b.index || 0)); sortedConvs.forEach(turn => { if (turn.speaker === 'human') return; const timestamp = utils.formatTimestamp(turn.createTime); const index = turn.index !== undefined ? turn.index : 'N/A'; if (turn.speaker === 'ai') { markdownContent.push(`\n## AI (轮次 ${index})\n`); let modelDisplay = '未知模型'; let primaryPluginId = '无插件'; if (turn.speechesV2?.length > 0) { const firstSpeech = turn.speechesV2[0]; const modelIdRaw = firstSpeech.chatModelId; primaryPluginId = firstSpeech.pluginId || primaryPluginId; if (modelIdRaw && String(modelIdRaw).trim() !== '') { modelDisplay = `\`${modelIdRaw}\``; } } markdownContent.push(`*时间: ${timestamp} | 模型: ${modelDisplay} | 插件: \`${primaryPluginId}\`*\n\n`); turn.speechesV2?.forEach(speech => { speech.content?.forEach(block => { switch (block.type) { case 'text': markdownContent.push(`${utils.adjustHeaderLevels(block.msg || '', 1)}\n\n`); break; case 'think': markdownContent.push(`> **[思考过程]** ${block.title || ''}\n>\n`); (block.content || '无思考内容').split('\n').forEach(line => { markdownContent.push(`> ${line}\n`); }); markdownContent.push('\n'); break; case 'searchGuid': markdownContent.push(`**${block.title || '搜索结果'}** (查询: \`${block.botPrompt || 'N/A'}\` | 主题: ${block.topic || 'N/A'})\n`); block.docs?.forEach((doc, docIndex) => { markdownContent.push(`* [${docIndex + 1}] [${doc.title || '无标题'}](${doc.url || '#'}) (${doc.sourceName || '未知来源'})\n * > ${doc.quote || '无引用'}\n`); }); markdownContent.push('\n'); break; case 'image': case 'code': case 'pdf': markdownContent.push(`*文件:* [${block.fileName || '未知文件'}](${block.url || '#'}) (类型: ${block.type})\n\n`); break; } }); }); } markdownContent.push('\n---\n'); }); return markdownContent.join('\n').replace(/\n---\n$/, '').trim(); }, kimi: async (data) => { let mdContent = []; const title = await fetchKimiChatTitle() || 'Kimi对话记录'; mdContent.push(`# ${title}\n`); data.items?.forEach(item => { const role = item.role === 'user' ? 'Human' : 'Assistant'; const timestamp = utils.formatTimestamp(item.created_at); mdContent.push(`### ${role}`); 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'); } 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'); }, tongyi: (data) => { let mdContent = []; mdContent.push(`# 通义千问对话记录`); const convTurns = data.data?.length || 0; const totalChars = data.data?.reduce((acc, msg) => { return acc + (msg.contents?.reduce((sum, content) => sum + (content.content?.length || 0), 0) || 0); }, 0) || 0; mdContent.push(`\n> 统计信息:对话轮次 ${convTurns} | 总字数 ${totalChars}`); if (data.data?.length) { const sortedMessages = [...data.data].sort((a, b) => a.createTime - b.createTime); sortedMessages.forEach(msg => { const time = utils.formatTimestamp(msg.createTime); const role = msg.senderType === 'USER' ? '用户' : '助手'; mdContent.push(`## ${role} (${time})\n`); msg.contents?.forEach(content => { if (content.content) { let text = content.content.replace(/\n/g, '\n\n'); text = text.replace(/```([\s\S]*?)```/g, '\n```$1```\n'); mdContent.push(text); } }); mdContent.push('\n---\n'); }); } return mdContent.join('\n'); }, iflytek: (data) => { let mdContent = []; mdContent.push(`# 讯飞星火对话记录`); const convTurns = data.data?.[0]?.historyList?.length || 0; const totalChars = data.data?.[0]?.historyList?.reduce((acc, msg) => { return acc + (msg.message?.length || 0) + (msg.answer?.length || 0); }, 0) || 0; mdContent.push(`\n> 统计信息:对话轮次 ${convTurns} | 总字数 ${totalChars}`); data.data?.[0]?.historyList?.forEach(msg => { if (msg.type === 0) { const time = utils.formatTimestamp(msg.createTime); mdContent.push(`## 用户 (${time})\n\n${msg.message}\n`); } else if (msg.message) { const time = utils.formatTimestamp(msg.createTime); let sourceInfo = ''; if (msg.traceSource) { try { const sources = JSON.parse(msg.traceSource); if (Array.isArray(sources)) { sources.forEach(source => { if (source.type === 'searchSource' && source.data) { sourceInfo += '\n\n**参考来源**:\n'; source.data.forEach(item => { sourceInfo += `- [${item.docid}](${item.source})\n`; }); } }); } } catch (e) { logger.error('解析来源信息失败', e); } } mdContent.push(`## AI助手 (${time})\n\n${msg.message}${sourceInfo}\n`); } mdContent.push('---\n'); }); return mdContent.join('\n'); }, //豆包消息转换 doubao:(data) => { let mdContent = []; const title = data.data?.title || '豆包对话记录'; mdContent.push(`# ${title}\n`); // 统计信息 const totalMessages = data.data?.message_list?.length || 0; const totalChars = data.data?.message_list?.reduce((acc, msg) => { try { if (msg.content_type === 1) { const contentObj = typeof msg.content === 'string' ? JSON.parse(msg.content || '{}') : msg.content; return acc + (contentObj.text?.length || 0); } else if (msg.content_type === 9999) { let length = 0; msg.content_block?.forEach(block => { try { const blockContent = typeof block.content === 'string' ? JSON.parse(block.content || '{}') : block.content; if (block.block_type === 10000) { length += (blockContent.text?.length || 0); } else if (block.block_type === 2074) { blockContent.creations?.forEach(creation => { if (creation.type === 1 && creation.image?.gen_params?.prompt) { length += creation.image.gen_params.prompt.length; } }); } } catch (e) { console.error('解析内容块失败', e); } }); return acc + length; } return acc + (msg.content?.length || 0); } catch { return acc + (msg.content?.length || 0); } }, 0) || 0; mdContent.push(`> 统计信息:消息数 ${totalMessages} | 总字数 ${totalChars}\n`); // 处理消息内容 - 按时间倒序排列 const sortedMessages = [...(data.data?.message_list || [])].sort((a, b) => b.create_time - a.create_time); sortedMessages.forEach(msg => { const time = utils.formatTimestamp(msg.create_time); const role = msg.user_type === 1 ? '用户' : '豆包AI'; mdContent.push(`## ${role} (${time})`); // 处理消息内容 let content = ''; try { if (msg.content_type === 1) { // 文本消息 const contentObj = typeof msg.content === 'string' ? JSON.parse(msg.content || '{}') : msg.content; content = contentObj.text || msg.content || ''; } else if (msg.content_type === 9999) { // 复合消息 if (msg.content_block?.length) { msg.content_block.forEach(block => { try { const blockContent = typeof block.content === 'string' ? JSON.parse(block.content || '{}') : block.content; if (block.block_type === 10000) { // 文本块 content += (blockContent.text || '') + '\n\n'; } else if (block.block_type === 2074) { // 图片块 content += platformConverters.processDoubaoMediaContent(block) + '\n\n'; } } catch (e) { console.error('解析内容块失败', e); } }); } } else if (msg.content_type === 6) { // 图片消息 content = platformConverters.processDoubaoMediaContent(msg); } } catch (e) { console.error('解析消息内容失败', e); content = msg.content || ''; } // 处理代码块和换行 content = content.replace(/```([\s\S]*?)```/g, '\n```$1```\n') .replace(/\n{3,}/g, '\n\n'); mdContent.push(content); // 处理语音转文字内容 if (msg.tts_content && msg.tts_content !== content) { mdContent.push('\n**语音转文字:**\n' + msg.tts_content); } mdContent.push('\n---\n'); }); return mdContent.join('\n'); }, // 处理豆包的媒体内容(图片等) processDoubaoMediaContent:(blockOrMsg) => { let content = []; try { // 获取内容对象 const contentObj = typeof blockOrMsg.content === 'string' ? JSON.parse(blockOrMsg.content || '{}') : blockOrMsg.content; // 如果是复合消息中的图片块(block_type=2074) if (blockOrMsg.block_type === 2074) { const creations = contentObj.creations || []; creations.forEach(creation => { if (creation.type === 1 && creation.image) { const img = creation.image; const originalUrl = img.image_raw?.url || img.image_ori?.url; if (originalUrl) { content.push(``); if (img.gen_params?.prompt) { content.push(`**提示词:** ${img.gen_params.prompt}`); } } } }); } // 如果是单独的图片消息(content_type=6) else if (blockOrMsg.content_type === 6) { if (contentObj.image_list) { contentObj.image_list.forEach(image => { const url = image.image_raw?.url || image.image_ori?.url; if (url) { content.push(``); } }); } } } catch (e) { console.error('处理媒体内容失败', e); } return content.join('\n\n'); } , // 豆包聊天列表转换器 doubao_chat: (data) => { let mdContent = []; mdContent.push(`# 豆包对话列表\n`); // 统计信息 const totalChats = data.data?.length || 0; mdContent.push(`> 统计信息:共 ${totalChats} 个对话\n`); // 处理对话列表 data.data?.forEach(chat => { const time = utils.formatTimestamp(chat.create_time); mdContent.push(`## ${chat.title || '未命名对话'} (${time})`); mdContent.push(`- 对话ID: ${chat.conversation_id}`); mdContent.push(`- 最后更新时间: ${new Date(chat.update_time * 1000).toLocaleString()}`); mdContent.push(`- 消息数: ${chat.message_count || 0}`); mdContent.push('\n---\n'); }); return mdContent.join('\n'); } }; // 核心处理函数 async function processTargetResponse(text, url) { try { let detectedPlatform = null; for (const [platform, pattern] of Object.entries(PLATFORM_PATTERNS)) { if (pattern.test(url)) { detectedPlatform = platform; break; } } if (!detectedPlatform) return; state.targetResponse = text; state.platformType = detectedPlatform; state.lastUpdateTime = new Date().toLocaleTimeString(); // 重置统计信息 state.messageStats = { totalTokens: 0, totalChars: 0, fileCount: 0, questions: 0, convTurns: 0 }; const jsonData = JSON.parse(text); const match = url.match(/\/api\/chat\/(.+?)\/segment\/scroll/);//Kimi // 平台特定处理 switch(detectedPlatform) { case 'deepseek': state.convertedMd = platformConverters.deepseek(jsonData); state.messageStats.totalTokens = jsonData.data?.biz_data?.chat_messages?.reduce( (acc, msg) => acc + (msg.accumulated_token_usage || 0), 0) || 0; state.currentTitle = jsonData.data?.biz_data?.chat_session?.title || 'deepseek-Chat'; break; case 'ncn': state.convertedMd = platformConverters.ncn(jsonData); state.messageStats.totalChars = jsonData.data?.messages?.reduce( (acc, msg) => acc + (msg.result?.length || 0), 0) || 0; state.messageStats.questions = jsonData.data?.messages?.reduce( (acc, msg) => acc + (msg.ask_further?.length || 0), 0) || 0; state.currentTitle = jsonData.data?.title || 'AI-Chat'; break; case 'yuanbao': state.convertedMd = platformConverters.yuanbao(jsonData); state.messageStats.convTurns = jsonData.convs?.length || 0; state.currentTitle = jsonData.sessionTitle || jsonData.title || 'Yuanbao-Chat'; state.messageStats.fileCount = jsonData.multiMediaInfo?.length || 0; break; case 'kimi': //const match = url.match(/\/api\/chat\/(.+?)\/segment\/scroll/); if (match?.[1]) { state.kimiSessionId = match[1]; state.currentTitle = await fetchKimiChatTitle(); } state.convertedMd = await platformConverters.kimi(jsonData); state.messageStats.totalChars = jsonData.items?.reduce( (acc, item) => acc + (JSON.stringify(item.contents).length || 0), 0) || 0; break; case 'tongyi': state.convertedMd = platformConverters.tongyi(jsonData); state.messageStats.convTurns = jsonData.data?.length || 0; state.messageStats.totalChars = jsonData.data?.reduce((acc, msg) => { return acc + (msg.contents?.reduce( (sum, content) => sum + (content.content?.length || 0), 0) || 0); }, 0) || 0; state.currentTitle = '通义千问对话'; break; case 'iflytek': state.convertedMd = platformConverters.iflytek(jsonData); state.messageStats.convTurns = jsonData.data?.[0]?.historyList?.length || 0; state.messageStats.totalChars = jsonData.data?.[0]?.historyList?.reduce((acc, msg) => { return acc + (msg.message?.length || 0) + (msg.answer?.length || 0); }, 0) || 0; state.currentTitle = '讯飞星火对话'; break; case 'doubao': case 'doubao_chat': state.convertedMd = platformConverters.doubao(jsonData); state.messageStats.totalChars = jsonData.data?.message_list?.reduce((acc, msg) => { try { const contentObj = JSON.parse(msg.content || '{}'); return acc + (contentObj.text?.length || 0); } catch { return acc + (msg.content?.length || 0); } }, 0) || 0; state.messageStats.convTurns = jsonData.data?.message_list?.length || 0; state.currentTitle = '豆包对话'; break; } ui.updateButtonStatus(); logger.info(`成功处理${detectedPlatform.toUpperCase()}响应`); } catch (e) { logger.error('响应处理错误', e); } } // Kimi相关功能 // sessionId提取 function getKimiSessionId() { const currentUrl = window.location.href; // 匹配多种可能的URL格式 const match = currentUrl.match(/(?:chat|chat_session)\/([a-zA-Z0-9-]+)(?:\/|$)/); return match ? match[1] : null; } // 拦截XHR请求获取授权令牌 function setupAuthInterceptor() { const originalOpen = XMLHttpRequest.prototype.open; const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.open = function(method, url) { this._requestURL = url; if (url.includes('/api/chat/') && !url.includes('/api/chat/list')) { logger.info(`拦截到Kimi API请求: ${method} ${url}`); this._isChatAPI = true; } return originalOpen.apply(this, arguments); }; XMLHttpRequest.prototype.setRequestHeader = function(header, value) { if (this._isChatAPI && header.toLowerCase() === 'authorization') { state.authToken = value; logger.info('获取到Authorization头'); } return originalSetRequestHeader.apply(this, arguments); }; } // 获取Kimi对话标题 async function fetchKimiChatTitle() { if (state.kimiTitleCache) return state.kimiTitleCache; state.kimiSessionId = getKimiSessionId(); if (!state.kimiSessionId) { logger.warn('无法获取sessionId'); return "Kimi对话"; } // 等待获取授权令牌 let retry = 0; while (!state.authToken && retry < 5) { logger.info('等待获取授权令牌...'); await new Promise(resolve => setTimeout(resolve, 500)); retry++; } if (!state.authToken) { logger.warn('未能获取授权令牌'); return "Kimi对话"; } try { const url = `https://www.kimi.com/api/chat/${state.kimiSessionId}`; logger.info(`请求标题API: ${url}`); const response = await fetch(url, { headers: { 'Authorization': state.authToken, 'Content-Type': 'application/json', }, credentials: 'include' }); if (!response.ok) { logger.warn(`API错误: ${response.status}`); return "Kimi对话"; } const data = await response.json(); logger.info('API响应:', data); state.kimiTitleCache = data.name || "Kimi对话"; return state.kimiTitleCache; } catch (error) { logger.error('获取标题失败:', error); return "Kimi对话"; } } // 导出功能 // 提取获取文件名的公共逻辑 function getExportFilename(extension = '') { const jsonData = JSON.parse(state.targetResponse || '{}'); let platformPrefix = ''; let defaultSuffix = 'Chat'; switch(state.platformType) { case 'deepseek': platformPrefix = 'DeepSeek'; defaultSuffix = 'deepseek-Chat'; break; case 'ncn': platformPrefix = 'AI-N'; defaultSuffix = 'AI-Chat'; break; case 'yuanbao': platformPrefix = 'Yuanbao'; defaultSuffix = 'Yuanbao-Chat'; break; case 'kimi': platformPrefix = 'Kimi'; defaultSuffix = 'Kimi-Chat'; break; case 'tongyi': platformPrefix = 'Tongyi'; defaultSuffix = 'Tongyi-Chat'; break; case 'iflytek': platformPrefix = 'Iflytek'; defaultSuffix = 'Iflytek-Chat'; break; case 'doubao': case 'doubao_chat': platformPrefix = 'Doubao'; defaultSuffix = state.platformType === 'doubao_chat' ? 'ChatList' : 'Chat'; break; default: platformPrefix = 'AI'; defaultSuffix = 'Chat'; } const title = state.currentTitle || jsonData.data?.biz_data?.chat_session?.title || jsonData.data?.title || jsonData.sessionTitle ||(state.platformType === 'doubao_chat' ? '豆包列表' : '豆包对话')|| defaultSuffix; const sanitizedTitle = utils.sanitizeFilename(`${platformPrefix}_${title}`); return `${sanitizedTitle}_${utils.getLocalTimestamp()}${extension ? '.' + extension : ''}`; } const exportHandlers = { json: () => { if (!state.targetResponse) { alert('还没有发现有效的对话记录。\n请等待目标响应或进行一些对话。'); return false; } try { const fileName = getExportFilename('json'); return utils.createBlobDownload( state.targetResponse, 'application/json', fileName ); } catch (e) { logger.error('JSON导出失败', e); alert('导出过程中发生错误,请查看控制台了解详情。'); return false; } }, markdown: () => { if (!state.convertedMd) { alert('还没有发现有效的对话记录。\n请等待目标响应或进行一些对话。'); return false; } try { const fileName = getExportFilename('md'); return utils.createBlobDownload( state.convertedMd, 'text/markdown', fileName ); } catch (e) { logger.error('Markdown导出失败', e); alert(`导出失败: ${e.message}`); return false; } }, word: () => { if (!state.convertedMd) { alert('还没有发现有效的对话记录。\n请等待目标响应或进行一些对话。'); return; } try { const fileName = getExportFilename('docx'); const chatName = fileName.replace(/_[\d_-]+\.docx$/, ''); // 移除时间戳部分作为标题 const wordButton = document.getElementById('downloadWordButton'); const originalText = wordButton.innerHTML; wordButton.innerHTML = '<span class="gm-spinner"></span>生成中...'; wordButton.disabled = true; GM_xmlhttpRequest({ method: "POST", url: CONFIG.API_ENDPOINT_WORD, headers: { "Content-Type": "application/json" }, data: JSON.stringify({ markdown: state.convertedMd, title: chatName }), responseType: 'blob', onload: function(response) { wordButton.innerHTML = originalText; wordButton.disabled = false; if (response.status >= 200 && response.status < 300) { try { const blob = response.response; const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); logger.info(`成功下载Word文件: ${fileName}`); } catch (e) { alert('下载文件时出错: ' + e.message); logger.error("处理Word下载时出错", e); } } else { const reader = new FileReader(); reader.onload = function() { try { const errorResult = JSON.parse(this.result); alert(`导出失败: ${errorResult.error || '未知错误'}`); } catch (e) { alert(`导出失败,状态码: ${response.status}。无法解析错误信息。`); } }; reader.readAsText(response.response); } }, onerror: function(response) { wordButton.innerHTML = originalText; wordButton.disabled = false; alert(`请求错误: ${response.statusText || '无法连接到服务器'}`); } }); } catch (e) { logger.error('Word导出初始化失败', e); alert('导出初始化失败: ' + e.message); } } }; // UI相关功能 const ui = { updateButtonStatus: () => { const jsonButton = document.getElementById('downloadJsonButton'); const mdButton = document.getElementById('downloadMdButton'); const wordButton = document.getElementById('downloadWordButton'); if (!jsonButton || !mdButton || !wordButton) return; const hasResponse = state.targetResponse !== null; let platformName = 'AI对话'; let statsText = ''; try { const jsonData = JSON.parse(state.targetResponse || '{}'); switch(state.platformType) { case 'deepseek': platformName = `DeepSeek_${jsonData.data?.biz_data?.chat_session?.title || 'deepseek-Chat'}`; statsText = `Token用量: ${state.messageStats.totalTokens}`; break; case 'ncn': platformName = `AI-N_${jsonData.data?.title || 'AI-Chat'}`; statsText = `字数: ${state.messageStats.totalChars} | 附件: ${state.messageStats.fileCount}`; break; case 'yuanbao': platformName = `Yuanbao_${jsonData.sessionTitle || jsonData.title || 'Yuanbao-Chat'}`; statsText = `对话轮次: ${state.messageStats.convTurns} | 文件: ${state.messageStats.fileCount}`; break; case 'kimi': platformName = `Kimi_${state.currentTitle}`; statsText = `消息数: ${state.messageStats.convTurns} | 字数: ${state.messageStats.totalChars}`; break; case 'tongyi': platformName = `Tongyi_${state.currentTitle}`; statsText = `对话轮次: ${state.messageStats.convTurns} | 字数: ${state.messageStats.totalChars}`; break; case 'iflytek': platformName = `xinghuo_${state.currentTitle}`; statsText = `对话轮次: ${state.messageStats.convTurns} | 字数: ${state.messageStats.totalChars}`; break; case 'doubao': case 'doubao_chat': platformName = `豆包${state.platformType === 'doubao_chat' ? '对话列表' : '对话'}`; statsText = state.platformType === 'doubao_chat' ? `对话数: ${state.messageStats.convTurns}` : `消息数: ${state.messageStats.convTurns} | 字数: ${state.messageStats.totalChars} | 附件: ${state.messageStats.fileCount}`; break; } } catch (e) { logger.error('更新按钮状态时解析JSON失败', e); } // 更新按钮状态和样式 [jsonButton, mdButton, wordButton].forEach(button => { button.style.backgroundColor = hasResponse ? '#28a745' : '#007bff'; button.dataset.tooltip = `${platformName}数据已就绪\n${statsText}\n最后更新: ${state.lastUpdateTime}`; // 悬停效果 button.onmouseenter = () => { button.style.transform = 'translateY(-2px)'; button.style.boxShadow = '0 6px 8px rgba(0,0,0,0.15)'; }; button.onmouseleave = () => { button.style.transform = ''; button.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'; }; }); // 特殊处理MD和Word按钮 mdButton.style.backgroundColor = state.convertedMd ? '#28a745' : '#007bff'; wordButton.style.backgroundColor = state.convertedMd ? '#28a745' : '#007bff'; // 禁用状态 jsonButton.disabled = !hasResponse; mdButton.disabled = !state.convertedMd; wordButton.disabled = !state.convertedMd; }, createDownloadButtons: () => { // 如果按钮已存在,则不再创建 if (document.getElementById('downloadJsonButton')) return; const buttonContainer = document.createElement('div'); buttonContainer.id = 'exportButtonsContainer'; const jsonButton = document.createElement('button'); const mdButton = document.createElement('button'); const wordButton = document.createElement('button'); // 容器样式 Object.assign(buttonContainer.style, { position: 'fixed', top: '45%', right: '10px', zIndex: '9999', display: 'flex', flexDirection: 'column', gap: '10px', opacity: CONFIG.BUTTON_OPACITY, transition: 'opacity 0.3s ease', cursor: 'move' }); // 按钮通用样式 const buttonStyles = { padding: '8px 12px', 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', fontSize: '14px' }; // 设置按钮属性和样式 jsonButton.id = 'downloadJsonButton'; jsonButton.innerText = 'JSON'; mdButton.id = 'downloadMdButton'; mdButton.innerText = 'MD'; wordButton.id = 'downloadWordButton'; wordButton.innerHTML = ` <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="vertical-align: middle; margin-right: 5px;"> <path d="M4 4.5V19C4 20.1046 4.89543 21 6 21H18C19.1046 21 20 20.1046 20 19V8.2468C20 7.61538 19.7893 7.00372 19.4029 6.5L16.5 3H6C4.89543 3 4 3.89543 4 5V5.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M16 3V7C16 7.55228 16.4477 8 17 8H20" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M8 13L10 17L12 13L14 17L16 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path> </svg>Word`; // 添加按钮的 classname [jsonButton, mdButton, wordButton].forEach(btn => { btn.className = 'export-button'; }); Object.assign(jsonButton.style, buttonStyles); Object.assign(mdButton.style, buttonStyles); Object.assign(wordButton.style, buttonStyles); // 鼠标悬停效果 buttonContainer.onmouseenter = () =>{ buttonContainer.style.opacity = CONFIG.BUTTON_HOVER_OPACITY; buttonContainer.style.boxShadow = '0 8px 16px rgba(0,0,0,0.15)'; }; buttonContainer.onmouseleave = () =>{ buttonContainer.style.opacity = CONFIG.BUTTON_OPACITY; buttonContainer.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)'; } // 拖动功能 let isDragging = false; let initialX, initialY, xOffset = 0, yOffset = 0; buttonContainer.addEventListener('mousedown', (e) => { initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; if (e.target === buttonContainer) { isDragging = true; } }); document.addEventListener('mousemove', (e) => { if (isDragging) { e.preventDefault(); xOffset = e.clientX - initialX; yOffset = e.clientY - initialY; buttonContainer.style.transform = `translate(${xOffset}px, ${yOffset}px)`; } }); document.addEventListener('mouseup', () => { isDragging = false; }); // 阻止按钮的 mousedown 冒泡影响拖动 [jsonButton, mdButton, wordButton].forEach(btn => { btn.addEventListener('mousedown', (e) => e.stopPropagation()); }); // 按钮点击事件 jsonButton.onclick = exportHandlers.json; mdButton.onclick = exportHandlers.markdown; wordButton.onclick = exportHandlers.word; // 组装按钮 buttonContainer.appendChild(jsonButton); buttonContainer.appendChild(mdButton); buttonContainer.appendChild(wordButton); document.body.appendChild(buttonContainer); // 自动折叠/展开功能 let isCollapsed = false; let collapseTimeout; const collapseDelay = 2000; // 2秒后自动折叠 // 自动折叠函数 const autoCollapse = () => { collapseTimeout = setTimeout(() => { buttonContainer.classList.add('collapsed'); isCollapsed = true; }, collapseDelay); }; // 初始设置自动折叠 autoCollapse(); // 鼠标进入展开 buttonContainer.addEventListener('mouseenter', () => { clearTimeout(collapseTimeout); if (isCollapsed) { buttonContainer.classList.remove('collapsed'); isCollapsed = false; } }); // 鼠标离开后延迟折叠 buttonContainer.addEventListener('mouseleave', () => { if (!isCollapsed) { autoCollapse(); } }); // 点击折叠按钮也可展开 buttonContainer.addEventListener('click', (e) => { if (isCollapsed && e.target === buttonContainer) { buttonContainer.classList.remove('collapsed'); isCollapsed = false; clearTimeout(collapseTimeout); } }); // 确保拖动时不折叠 buttonContainer.addEventListener('mousedown', () => { clearTimeout(collapseTimeout); }); // 添加加载动画样式 GM_addStyle(` .export-button { transition: all 0.2s ease; } .export-button:hover { background-color: #0056b3 !important; } .export-button:disabled { opacity: 0.6; cursor: not-allowed; background-color: #6c757d !important; } .gm-spinner { border: 2px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: #fff; width: 12px; height: 12px; animation: spin ${CONFIG.SPINNER_ANIMATION_DURATION} linear infinite; display: inline-block; margin-right: 8px; vertical-align: middle; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } /* 添加折叠/展开相关样式 */ #exportButtonsContainer.collapsed { width: 40px; height: 40px; overflow: hidden; padding: 5px; } #exportButtonsContainer.collapsed .export-button { display: none; } #exportButtonsContainer.collapsed::before { content: "↓↓↓"; position: absolute; width: 30px; height: 30px; background: #007bff; color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 16px; cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.2); } #exportButtonsContainer:not(.collapsed)::before { display: none; } #exportButtonsContainer { transition: all 0.3s ease; } /* 标题相关样式 */ [data-tooltip] { position: relative; } [data-tooltip]:hover::after { content: attr(data-tooltip); position: absolute; right: 100%; top: 50%; transform: translateY(-50%); background:rgba(228,17,136,0.7); color: #fff; padding: 8px 12px; border-radius: 4px; font-size: 16px; font-family: "方正小标宋简体","黑体", Arial, sans-serif; white-space: pre; margin-right: 10px; pointer-events: none; opacity: 0.1; transition: opacity 0.3s; } [data-tooltip]:hover::after { opacity: 1; } `); ui.updateButtonStatus(); } }; // 网络拦截 function setupNetworkInterception() { // 拦截XHR请求 const originalOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(...args) { // 处理缓存参数 if (args[1]?.includes('history_messages?chat_session_id')) { args[1] = args[1].split('&cache_version=')[0]; } else if (args[1]?.includes('conversation/info?conversation_id')) { args[1] = args[1].split('&cache_version=')[0]; } else if (args[1]?.includes('/api/user/agent/conversation/v1/detail')) { args[1] = args[1].split('&cacheBust=')[0]; } this._requestURL = args[1]; this.addEventListener('load', () => { if (this.responseURL) { for (const [platform, pattern] of Object.entries(PLATFORM_PATTERNS)) { if (pattern.test(this.responseURL)) { processTargetResponse(this.responseText, this.responseURL); break; } } } }); originalOpen.apply(this, args); }; // 拦截Fetch请求 const originalFetch = window.fetch; window.fetch = async function(...args) { const url = args[0] instanceof Request ? args[0].url : args[0]; let response; try { response = await originalFetch.apply(this, args); if (typeof url === 'string') { for (const [platform, pattern] of Object.entries(PLATFORM_PATTERNS)) { if (pattern.test(url)) { const contentType = response.headers.get('content-type'); if (contentType?.includes('application/json')) { const clonedResponse = response.clone(); clonedResponse.text().then(text => { processTargetResponse(text, url); }).catch(e => { logger.error(`解析fetch响应文本时出错`, { url, error: e }); }); } break; } } } } catch (error) { logger.error('Fetch请求失败', error); throw error; } return response; }; } setupNetworkInterception(); setupAuthInterceptor(); // Kimi标题授权拦截器 // 初始化 function initialize() { // setupNetworkInterception(); ui.createDownloadButtons(); logger.info('增强版导出脚本已启动'); // 使用MutationObserver确保按钮存在 const observer = new MutationObserver((mutations) => { if (!document.getElementById('downloadJsonButton') || !document.getElementById('downloadMdButton')) { logger.info('检测到按钮丢失,正在重新创建...'); ui.createDownloadButtons(); } }); observer.observe(document.body, { childList: true, subtree: true }); } // 页面加载完成后初始化 if (document.readyState === 'complete') { initialize(); } else { window.addEventListener('load', initialize); } })();