// ==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);
}
})();