// ==UserScript==
// @name AI对话导出word/json/md - DeepSeek/纳米/腾讯元宝/Kimi/通义千问/讯飞星火/豆包
// @namespace http://tampermonkey.net/
// @version 2026.10.8-2.scrollmerge
// @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 *://qianwen.com/*
// @match *://*.qianwen.com/*
// @match *://*.xfyun.cn/*
// @match *://*.doubao.com/*
// @run-at document-start
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
console.log('🚀 脚本开始加载', new Date().toISOString());
// 配置常量
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: /(\/apiv2\/kimi\.chat\.v1\.ChatService\/ListMessages|\/api\/chat\/[^\/]+\/messages|\/api\/chat|chat_session_id)/,///\/apiv2\/kimi\.chat\.v1\.ChatService\/ListMessages/
kimi_title: /\/apiv2\/kimi\.chat\.v1\.ChatService\/GetChat/,//kimi标题信息API
tongyi: /\/dialog\/chat\/list/,
// 千问(qianwen.com,原通义千问新版):历史消息列表 + 会话信息(含标题)
qianwen: /\/api\/v1\/session\/msg\/list/,
qianwen_title: /\/api\/v1\/session\/get/,
iflytek: /\/iflygpt\/u\/chat_history\/all\//,
doubao:/\/im\/chain\/single/, //过期alice\/message\/list/
doubao_title:/\/im\/conversation\/info/ // 豆包标题会话信息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,
qianwenSessionId: null,
qianwenTitleCache: null,
authToken: null,
// 分页历史记录累积池:key为会话标识,map以消息ID去重存储所有已拦截的消息
mergeStore: {
key: null,
map: new Map(),
title: 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 时间戳) const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp;
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);
dt = new Date(parseInt(timestamp) < 1e12 ? parseInt(timestamp) * 1000 : parseInt(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;
}
}
};
// ============ 分页历史记录合并(解决滚动加载的旧轮次丢失问题) ============
// 各平台历史API是分页返回的:页面打开只拿到最近几轮,向上滚动才会请求更早的轮次。
// 这里把每次拦截到的“一页”数据按消息ID去重合并到累积池,导出时使用完整数据。
const MERGE_CONFIG = {
deepseek: {
getList: d => d?.data?.biz_data?.chat_messages,
setList: (d, list) => { d.data.biz_data.chat_messages = list; },
getId: m => m.message_id ?? m.id ?? m.inserted_at,
sortKey: m => m.inserted_at ?? m.message_id ?? 0,
getTitle: d => d?.data?.biz_data?.chat_session?.title,
setTitle: (d, t) => { if (d?.data?.biz_data) { d.data.biz_data.chat_session = d.data.biz_data.chat_session || {}; d.data.biz_data.chat_session.title = t; } }
},
ncn: {
getList: d => d?.data?.messages,
setList: (d, list) => { d.data.messages = list; },
getId: m => m.id ?? m.msg_id ?? m.qid ?? m.create_time ?? m.prompt,
sortKey: m => m.create_time ?? m.id ?? 0,
getTitle: d => d?.data?.title,
setTitle: (d, t) => { if (d?.data) d.data.title = t; }
},
yuanbao: {
getList: d => d?.convs,
setList: (d, list) => { d.convs = list; },
getId: m => m.id ?? `${m.index}|${m.speaker}`,
sortKey: m => m.index ?? 0,
getTitle: d => d?.sessionTitle || d?.title,
setTitle: (d, t) => { d.sessionTitle = t; }
},
kimi: {
getList: d => d?.messages,
setList: (d, list) => { d.messages = list; },
getId: m => m.id ?? m.messageId ?? m.createTime,
sortKey: m => m.createTime ?? 0
},
tongyi: {
getList: d => d?.data,
setList: (d, list) => { d.data = list; },
getId: m => m.msgId ?? m.id ?? m.createTime,
sortKey: m => m.createTime ?? 0
},
qianwen: {
// 千问每轮是 {req_id, pos, request_messages, response_messages},pos为毫秒时间戳字符串
getList: d => d?.data?.list,
setList: (d, list) => { d.data.list = list; },
getId: m => m.req_id ?? m.pos ?? m.request_timestamp,
sortKey: m => Number(m.pos) || m.request_timestamp || 0
},
iflytek: {
getList: d => d?.data?.[0]?.historyList,
setList: (d, list) => { if (d?.data?.[0]) d.data[0].historyList = list; },
getId: m => m.id ?? m.sid ?? m.createTime,
sortKey: m => m.createTime ?? m.id ?? 0
},
doubao: {
getList: d => d?.downlink_body?.pull_singe_chain_downlink_body?.messages,
setList: (d, list) => { d.downlink_body.pull_singe_chain_downlink_body.messages = list; },
getId: m => m.message_id ?? m.server_message_id ?? m.local_message_id ?? `${m.create_time}|${m.user_type}`,
sortKey: m => m.create_time ?? 0
}
};
// 比较排序键:数字直接比,字符串尝试按日期比,否则按字典序
function compareSortKeys(a, b) {
if (a == null || b == null) return 0;
if (typeof a === 'number' && typeof b === 'number') return a - b;
const da = Date.parse(a), db = Date.parse(b);
if (!isNaN(da) && !isNaN(db)) return da - db;
return String(a).localeCompare(String(b));
}
// 会话标识:切换到别的对话时清空累积池,避免混入上一个对话的内容
function getConversationKey(platform, url) {
const m = String(url).match(/(?:chat_session_id|conversation_id|sessionId|session_id|chat_id)=([^&]+)/);
if (m) return `${platform}|${m[1]}`;
return `${platform}|${location.pathname}${location.search}`;
}
function mergeHistoryData(platform, jsonData, url) {
const cfg = MERGE_CONFIG[platform];
if (!cfg) return jsonData;
const newList = cfg.getList(jsonData);
if (!Array.isArray(newList)) return jsonData;
const convKey = getConversationKey(platform, url);
if (state.mergeStore.key !== convKey) {
state.mergeStore.key = convKey;
state.mergeStore.map = new Map();
state.mergeStore.title = null;
logger.info(`检测到新会话,重置历史消息累积池: ${convKey}`);
}
newList.forEach(msg => {
const id = cfg.getId(msg);
if (id !== undefined && id !== null) {
state.mergeStore.map.set(String(id), msg);
}
});
const merged = [...state.mergeStore.map.values()]
.sort((a, b) => compareSortKeys(cfg.sortKey(a), cfg.sortKey(b)));
cfg.setList(jsonData, merged);
// 分页响应里可能没有标题,沿用之前拿到的标题
if (cfg.getTitle) {
const t = cfg.getTitle(jsonData);
if (t) {
state.mergeStore.title = t;
} else if (state.mergeStore.title && cfg.setTitle) {
cfg.setTitle(jsonData, state.mergeStore.title);
}
}
logger.info(`历史消息合并: 本次响应 ${newList.length} 条,累积共 ${merged.length} 条`);
return jsonData;
}
// ============ 分页合并模块结束 ============
// 平台特定的转换器
const platformConverters = {
deepseek: (data) => {
let mdContent = [];
// 1. 安全的数据访问
const bizData = data?.data?.biz_data || {};
const chatSession = bizData.chat_session || {};
const chatMessages = bizData.chat_messages || [];
const title = chatSession.title || CONFIG.DEFAULT_TITLE;
const totalTokens = chatMessages.reduce((acc, msg) =>
acc + (msg.accumulated_token_usage || 0), 0);
// 2. 标题和统计信息
mdContent.push(`# DeepSeek对话 - ${title}`);
mdContent.push(`\n> 统计信息:累计Token用量 ${totalTokens}`);
mdContent.push(`> 生成时间:${new Date().toLocaleString()}\n`);
// 3. 处理消息(用户提问和AI回答都导出)
chatMessages.forEach((msg,index) => {
const role = msg.role === 'ASSISTANT' ? '🤖 Assistant' : '👤 用户';
const timestamp = utils.formatTimestamp(msg.inserted_at);
mdContent.push(`## ${role}`);
mdContent.push(`*${timestamp}*\n`);
// 4. 处理文件信息
if (msg.files?.length > 0) {
mdContent.push('### 文件信息');
msg.files.forEach(file => {
mdContent.push(`- 名称: ${file.file_name}`);
mdContent.push(`- 大小: ${(file.file_size / 1024).toFixed(1)}KB`);
mdContent.push(`- Token用量: ${file.token_usage}`);
});
mdContent.push('');
}
// 5. 处理消息片段(思考过程和回复内容)
let content = '';
let thinkingContent = '';
if (msg.fragments?.length > 0) {
const citations = {};
msg.fragments.forEach(fragment => {
if (fragment.cite_index !== null) {
citations[fragment.cite_index] = fragment.url;
}
switch (fragment.type) {
case 'THINK':
thinkingContent += fragment.content + '\n';
break;
case 'RESPONSE':
content += fragment.content + '\n';
break;
case 'TIP':
content += `\n> 💡 ${fragment.content}\n`;
break;
case 'REQUEST':
content += fragment.content + '\n';
break;
}
});
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, '.');
}
// 5.5 用户提问通常没有fragments,正文直接在content字段里
if (!content.trim() && typeof msg.content === 'string' && msg.content.trim()) {
content = msg.content;
}
// 6. 添加思考过程(如果存在)
if (thinkingContent.trim()) {
const thinkFragment = msg.fragments.find(f => f.type === 'THINK');
const thinkTime = thinkFragment?.elapsed_secs ? `(${thinkFragment.elapsed_secs.toFixed(1)}s)` : '';
content += `\n**思考过程 ${thinkTime}:**\n${thinkingContent.trim()}\n`;
}
// 7. 数学公式格式化 - 智能处理
content = content.replace(/(\\\([\s\S]*?\\\)|\\\[[\s\S]*?\\\]|\$\$.*?\$\$)/g, (match) => {
// 提取公式内容(移除各种边界符号)
let formula = match.replace(/^\\\(|\\\)$|^\\\[|\\\]$|\$\$/g, '');
// 转换为单行
formula = formula.replace(/\s+/g, ' ').trim();
// 返回统一的 $...$ 格式
return `$${formula}$`;
});
mdContent.push(content.trim() + '\n');
mdContent.push('---\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 => {
const timestamp = utils.formatTimestamp(turn.createTime);
const index = turn.index !== undefined ? turn.index : 'N/A';
// 用户提问轮次
if (turn.speaker === 'human') {
markdownContent.push(`\n## 👤 用户 (轮次 ${index})\n`);
markdownContent.push(`*时间: ${timestamp}*\n`);
let userText = '';
const collectBlocks = (blocks) => {
blocks?.forEach(block => {
if (block.type === 'text' && block.msg) {
userText += block.msg + '\n\n';
} else if (['image', 'code', 'pdf', 'file'].includes(block.type)) {
userText += `*附件:* [${block.fileName || '未知文件'}](${block.url || '#'}) (类型: ${block.type})\n\n`;
}
});
};
// 用户内容可能在 turn.content 数组里,也可能在 speechesV2 里
if (Array.isArray(turn.content)) {
collectBlocks(turn.content);
}
if (!userText.trim()) {
turn.speechesV2?.forEach(speech => collectBlocks(speech.content));
}
if (!userText.trim() && typeof turn.content === 'string') {
userText = turn.content;
}
if (!userText.trim() && typeof turn.displayPrompt === 'string') {
userText = turn.displayPrompt;
}
markdownContent.push((userText.trim() || '(无内容)') + '\n');
markdownContent.push('\n---\n');
return;
}
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对话记录';
//const title = state.currentTitle || 'Kimi对话记录';
mdContent.push(`# ${title}\n`);
// 处理消息数组
data.messages?.forEach((message, index) => {
const role = message.role === 'user' ? '👤 用户' : '🤖 Kimi';
const timestamp = utils.formatTimestamp(message.createTime);
mdContent.push(`## ${role} - ${timestamp}\n`);
// 处理blocks中的内容
message.blocks?.forEach(block => {
if (block.text?.content) {
// 文本内容
mdContent.push(block.text.content + '\n');
} else if (block.artifact) {
// 特殊内容(如PPT、文件等)
if (block.artifact.type === 'ARTIFACT_TYPE_SLIDES_JSON') {
mdContent.push('### 📊 生成的PPT内容\n');
try {
const slidesData = JSON.parse(block.artifact.content);
mdContent.push(`**标题:** ${slidesData.outline?.pptTitle || '无标题'}\n`);
mdContent.push(`**页数:** ${slidesData.outline?.slidesCount || 0}\n`);
// 可以进一步解析PPT内容
} catch (e) {
mdContent.push('*[PPT内容解析失败]*\n');
}
}
} else if (block.slidesView) {
// 幻灯片视图信息
mdContent.push(`### 🎨 幻灯片视图\n`);
mdContent.push(`- 名称: ${block.slidesView.name}\n`);
mdContent.push(`- 状态: ${block.slidesView.status}\n`);
if (block.slidesView.coverUrl) {
mdContent.push(`- 封面: \n`);
}
}
});
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) => {
if (content.contentType === 'text' && content.content) {
return sum + content.content.length;
} else if (content.contentType === 'plugin' && content.content) {
// 计算插件内容中的文本长度
try {
const pluginData = JSON.parse(content.content);
if (pluginData.pluginCall) {
const callData = JSON.parse(pluginData.pluginCall);
return sum + (callData.prompt?.length || 0);
}
} catch (e) {
return sum;
}
}
return sum;
}, 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 ? utils.formatTimestamp(msg.createTime) :
new Date(msg.createTime).toLocaleString();
const role = msg.senderType === 'USER' ? '用户' : '助手';
mdContent.push(`## ${role} (${time})\n`);
msg.contents?.forEach(content => {
if (content.content) {
if (content.contentType === 'text') {
// 处理普通文本
let text = content.content.replace(/\n/g, '\n\n');
text = text.replace(/```([\s\S]*?)```/g, '\n```$1```\n');
mdContent.push(text);
} else if (content.contentType === 'plugin') {
// 处理插件内容(文生图等)
try {
const pluginData = JSON.parse(content.content);
mdContent.push('### 插件调用信息');
if (pluginData.pluginCall) {
const callData = JSON.parse(pluginData.pluginCall);
mdContent.push(`**功能:** ${callData.toolName || '未知'}`);
mdContent.push(`**提示词:** ${callData.prompt || '无'}`);
mdContent.push(`**尺寸:** ${callData.size || '未指定'}`);
}
if (pluginData.pluginResult?.taskResult?.wanx_text_to_image?.imageList) {
const images = pluginData.pluginResult.taskResult.wanx_text_to_image.imageList;
mdContent.push('### 生成的图片');
images.forEach((img, index) => {
mdContent.push(`**图片 ${index + 1}:**`);
mdContent.push(`- 分辨率:${img.resolution}`);
mdContent.push(`- 状态:${img.isSecurity ? '安全' : '待审核'}`);
mdContent.push(`- 预览:`);
mdContent.push(`- 原图:[点击下载](${img.url})`);
});
}
if (pluginData.pluginResult?.status) {
mdContent.push(`**任务状态:** ${pluginData.pluginResult.status}`);
}
} catch (e) {
mdContent.push('`[插件内容解析错误]`');
}
}
}
});
mdContent.push('\n---\n');
});
} else {
mdContent.push('\n暂无对话内容');
}
return mdContent.join('\n');
},
qianwen: (data) => {
let mdContent = [];
mdContent.push(`# 通义千问对话记录`);
const turns = data.data?.list || [];
const totalChars = turns.reduce((acc, turn) => {
const q = (turn.request_messages || []).reduce(
(s, m) => s + (m.mime_type === 'text/plain' ? String(m.content || '').length : 0), 0);
const a = (turn.response_messages || []).reduce(
(s, m) => s + (m.mime_type === 'multi_load/iframe' && m.content ? String(m.content).length : 0), 0);
return acc + q + a;
}, 0);
mdContent.push(`\n> 统计信息:对话轮次 ${turns.length} | 总字数 ${totalChars}`);
if (turns.length) {
const sortedTurns = [...turns].sort((a, b) =>
(Number(a.pos) || a.request_timestamp || 0) - (Number(b.pos) || b.request_timestamp || 0));
sortedTurns.forEach(turn => {
const time = utils.formatTimestamp(Number(turn.pos) || turn.request_timestamp);
// 用户提问(文本 + 图片等附件)
const reqParts = [];
(turn.request_messages || []).forEach(m => {
if (m.mime_type === 'text/plain' && m.content) {
reqParts.push(String(m.content));
} else if (m.mime_type === 'image/url') {
try {
const res = JSON.parse(m.meta_data?.resource_infos || '[]');
const names = (Array.isArray(res) ? res : [])
.map(r => r.file_name || r.url || r.id).filter(Boolean);
reqParts.push(`[附件图片: ${names.join(', ') || '图片'}]`);
} catch (e) {
reqParts.push('[附件图片]');
}
}
});
if (reqParts.length) {
mdContent.push(`## 👤 用户 (${time})\n`);
reqParts.forEach(p => mdContent.push(p));
mdContent.push('');
}
// AI 回答:正文在 multi_load/iframe 类型消息的 content 里(其余类型为进度条/信号等UI事件,无正文)
const answers = (turn.response_messages || [])
.filter(m => m.mime_type === 'multi_load/iframe' && m.content && String(m.content).trim())
.map(m => String(m.content));
if (answers.length) {
mdContent.push(`## 🤖 助手 (${time})\n`);
answers.forEach(a => mdContent.push(a));
mdContent.push('');
}
mdContent.push('\n---\n');
});
} else {
mdContent.push('\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.downlink_body?.get_conv_info_downlink_body?.conversation_info?.name||'豆包对话记录';
const title = state.currentTitle || '豆包对话记录';
// 获取正确的消息数组
const messages = data.downlink_body?.pull_singe_chain_downlink_body?.messages || [];
mdContent.push(`# ${title}\n`);
// 统计信息
const totalMessages = messages.length;
let totalChars = 0;
// 处理消息内容 - 按时间正序排列
const sortedMessages = [...messages].sort((a, b) => a.create_time - b.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 = '';
let messageChars = 0;
try {
if (msg.content_type === 1) { // 用户文本消息
const contentObj = typeof msg.content === 'string' ? JSON.parse(msg.content || '{}') : msg.content;
content = contentObj.text || JSON.stringify(contentObj) || '';
messageChars = content.length;
} else if (msg.content_type === 9999) { // AI复合消息
// 方法1: 解析content字段(JSON数组)
if (msg.content) {
const contentArray = typeof msg.content === 'string' ? JSON.parse(msg.content || '[]') : msg.content;
if (Array.isArray(contentArray)) {
contentArray.forEach(item => {
if (item.block_type === 10000 && item.content?.text_block?.text) {
// 文本块
content += item.content.text_block.text + '\n\n';
messageChars += item.content.text_block.text.length;
} else if (item.block_type === 2074 && item.content?.creation_block) {
// 图片块
const mediaContent = platformConverters.processDoubaoMediaContent(item);
content += mediaContent + '\n\n';
}
});
}
}
// 方法2: 使用content_block(如果存在)
if (!content.trim() && msg.content_block?.length) {
msg.content_block.forEach(block => {
if (block.block_type === 10000 && block.content?.text_block?.text) {
content += block.content.text_block.text + '\n\n';
messageChars += block.content.text_block.text.length;
} else if (block.block_type === 2074) {
const mediaContent = platformConverters.processDoubaoMediaContent(block);
content += mediaContent + '\n\n';
}
});
}
}
totalChars += messageChars;
} catch (e) {
console.error('解析消息内容失败', e);
content = `解析错误: ${e.message}`;
}
// 清理内容格式
content = content.replace(/```([\s\S]*?)```/g, '\n```$1```\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
mdContent.push(content || '(无内容)');
mdContent.push('\n---\n');
});
// 插入统计信息到合适位置
if (mdContent.length > 1) {
mdContent.splice(1, 0, `> 统计信息:消息数 ${totalMessages} | 总字数 ${totalChars}\n`);
}
return mdContent.join('\n');
},
// 处理豆包的媒体内容 - 修正版本
processDoubaoMediaContent: (blockOrMsg) => {
let content = [];
try {
let contentObj;
if (blockOrMsg.content && typeof blockOrMsg.content === 'string') {
contentObj = JSON.parse(blockOrMsg.content);
} else {
contentObj = blockOrMsg.content || {};
}
// 从creation_block或直接获取creations
const creations = contentObj.creation_block?.creations || contentObj.creations || [];
creations.forEach(creation => {
if (creation.type === 1 && creation.image) {
const img = creation.image;
// 获取图片URL(按优先级)
const imageUrl = img.image_ori_raw?.url || img.image_raw?.url ||
img.image_ori?.url || img.image_preview?.url;
if (imageUrl) {
content.push(``);
// 添加生成信息
if (img.gen_params?.prompt) {
content.push(`**提示词:** ${img.gen_params.prompt}`);
}
}
}
});
} catch (e) {
console.error('处理媒体内容失败', e);
content.push('*[图片内容解析失败]*');
}
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 {
//console.log('=== 开始处理响应 ===');
//console.log('URL:', url);
//console.log('响应长度:', text.length);
let detectedPlatform = null;
for (const [platform, pattern] of Object.entries(PLATFORM_PATTERNS)) {
if (pattern.test(url)) {
detectedPlatform = platform;
break;
}
}
//console.log(detectedPlatform);
if (!detectedPlatform) {
console.log('未匹配到任何平台,退出处理');
return;
}
/* if (detectedPlatform==='kimi') {
text = await fetchkimiMessage();
} */
state.platformType = detectedPlatform;
state.lastUpdateTime = new Date().toLocaleTimeString();
// 重置统计信息
state.messageStats = {
totalTokens: 0,
totalChars: 0,
fileCount: 0,
questions: 0,
convTurns: 0
};
// 千问会话信息API(session/get):仅提取标题,不覆盖已捕获的消息数据
if (detectedPlatform === 'qianwen_title') {
try {
const j = JSON.parse(text);
if (j.data?.title && j.data?.session_id) {
state.qianwenTitleCache = { sid: j.data.session_id, title: j.data.title };
// 已知当前会话时,仅当会话匹配才更新标题,避免侧边栏操作其他会话时串标题
if (!state.qianwenSessionId || state.qianwenSessionId === j.data.session_id) {
state.currentTitle = j.data.title;
}
logger.info(`更新千问会话标题: ${j.data.title}`);
}
} catch (e) {
logger.error('解析千问标题响应失败', e);
}
return;
}
// 关键改动:不再用本次响应直接覆盖旧数据,
// 而是把分页返回的消息合并进累积池,得到完整历史后再保存
const jsonData = mergeHistoryData(detectedPlatform, JSON.parse(text), url);
state.targetResponse = JSON.stringify(jsonData);
const match = url.match(/(\/apiv2\/kimi\.chat\.v1\.ChatService\/ListMessages|\/api\/chat\/[^\/]+\/messages|\/api\/chat|chat_session_id)/);//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':
case 'kimi_title':
if (match?.[1]) {
state.kimiSessionId = match[1];
state.currentTitle = await fetchKimiChatTitle();
//console.log('测试Kimi_case');
}
//console.log('测试Kimicase_title');
// 如果是标题API,只更新标题不处理内容
if (state.platformType === 'kimi_title') {
state.currentTitle = jsonData.chat?.name || 'kimi对话';
logger.info(`更新kimi标题: ${state.currentTitle}`);
return; // 标题API不需要处理消息内容
}
state.convertedMd = await platformConverters.kimi(jsonData);
state.messageStats.totalChars = jsonData.messages?.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 'qianwen': {
// 从请求URL提取会话ID(msg/list 的 session_id 在查询参数里)
const sidMatch = url.match(/[?&]session_id=([^&]+)/);
if (sidMatch) state.qianwenSessionId = sidMatch[1];
state.convertedMd = platformConverters.qianwen(jsonData);
state.messageStats.convTurns = jsonData.data?.list?.length || 0;
state.messageStats.totalChars = jsonData.data?.list?.reduce((acc, turn) => {
const q = (turn.request_messages || []).reduce(
(s, m) => s + (m.mime_type === 'text/plain' ? String(m.content || '').length : 0), 0);
const a = (turn.response_messages || []).reduce(
(s, m) => s + (m.mime_type === 'multi_load/iframe' && m.content ? String(m.content).length : 0), 0);
return acc + q + a;
}, 0) || 0;
// 标题来自 session/get 的缓存;缓存不属于本会话时用默认值
state.currentTitle = (state.qianwenTitleCache && state.qianwenTitleCache.sid === state.qianwenSessionId)
? state.qianwenTitleCache.title : '通义千问对话';
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_title':
// 如果是标题API,只更新标题不处理内容
if (state.platformType === 'doubao_title') {
state.currentTitle = jsonData.downlink_body?.get_conv_info_downlink_body?.conversation_info?.name || '豆包对话';
logger.info(`更新豆包标题: ${state.currentTitle}`);
return; // 标题API不需要处理消息内容
}
// 正常处理对话内容
state.convertedMd = platformConverters.doubao(jsonData);
state.messageStats.totalChars = jsonData.downlink_body?.pull_singe_chain_downlink_body?.messages?.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.downlink_body?.pull_singe_chain_downlink_body?.messages?.length || 0;
// 如果当前标题还是默认值,尝试从对话数据中获取
if (state.currentTitle === CONFIG.DEFAULT_TITLE || state.currentTitle === '豆包对话') {
state.currentTitle = jsonData.downlink_body?.get_conv_info_downlink_body?.conversation_info?.name || '豆包对话';
}
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;
//console.log('Kimi标题获取测试');
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响应类型:', typeof data);
logger.info('API响应:', data);
//const kimidata =await fetchkimiMessage();
//console.log('标题里面输出对话:',kimidata);
state.kimiTitleCache = data.name || "Kimi对话";
return state.kimiTitleCache;
} catch (error) {
logger.error('获取标题失败:', error);
return "Kimi对话";
}
}
// 获取Kimi对话信息
async function fetchkimiMessage() {
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/apiv2/kimi.chat.v1.ChatService/ListMessages`;
logger.info(`请求信息API: ${url}`);
// 修正:使用POST方法并添加请求体
const requestBody = {
chat_id: state.kimiSessionId, // 需要传递chatId参数
// 可能还需要其他参数,如:
// limit: 100,
// cursor: ""
};
const response = await fetch(url, {
method: 'POST', // 改为POST方法
headers: {
'Authorization': state.authToken,
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody), // 添加请求体
mode: 'cors',
referrer: window.location.href, // 使用当前页面作为referrer
credentials: 'include'
});
if (!response.ok) {
logger.warn(`API错误: ${response.status}`);
return "Kimi信息";
}
// 检查Content-Type确认是JSON
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
const rawText = await response.text();
console.warn('响应不是JSON:', rawText.substring(0, 200));
return null;
}
const data = await response.json();
//const data = JSON.stringify(restext, null, 2)
logger.info('Kimi_API响应类型:', typeof data);
logger.info('kimi_API响应:', data);
//console.log('kimi_API响应:', JSON.stringify(data, null, 2));
//state.kimiTitleCache = data.name || "Kimi对话";
return data;
} catch (error) {
logger.error('获取kimi信息失败:', 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 'qianwen':
platformPrefix = 'Qianwen';
defaultSuffix = 'Qianwen-Chat';
break;
case 'iflytek':
platformPrefix = 'Iflytek';
defaultSuffix = 'Iflytek-Chat';
break;
case 'doubao':
case 'doubao_title':
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 ||jsonData.downlink_body?.get_conv_info_downlink_body?.conversation_info?.name||'豆包对话'||(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 = '生成中...';
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);
}
}
};
// ============ 自动滚动加载全部历史记录 ============
// 反复把聊天区域滚到顶部,触发平台去请求更早的轮次;
// 通过“滚动高度”和“累积池消息数”是否还在增长来判断是否已加载完。
const autoScroller = {
running: false,
stopRequested: false,
sleep: (ms) => new Promise(resolve => setTimeout(resolve, ms)),
// 寻找聊天消息所在的滚动容器:可滚动、在视口内、面积够大、可滚动距离最长的元素
findScrollContainer: () => {
let best = null;
let bestScore = 0;
const elements = document.querySelectorAll('div, main, section, ul, ol');
for (const el of elements) {
const scrollable = el.scrollHeight - el.clientHeight;
if (scrollable < 100) continue;
const style = getComputedStyle(el);
if (!/(auto|scroll|overlay)/.test(style.overflowY)) continue;
const rect = el.getBoundingClientRect();
if (rect.width < 250 || rect.height < 150) continue;
if (rect.bottom < 0 || rect.top > window.innerHeight) continue;
const score = scrollable * Math.sqrt(rect.width * rect.height);
if (score > bestScore) {
bestScore = score;
best = el;
}
}
// 有些页面是整个窗口在滚动,作为兜底
const root = document.scrollingElement;
if (!best && root && root.scrollHeight - root.clientHeight > 100) {
best = root;
}
return best;
},
start: async function(button) {
// 运行中再点一次 = 请求停止
if (this.running) {
this.stopRequested = true;
button.innerText = '⏹ 正在停止...';
return;
}
let container = this.findScrollContainer();
if (!container) {
alert('没有找到可滚动的聊天区域。\n请确认页面里已经打开了一个对话,且对话内容超过一屏。');
return;
}
this.running = true;
this.stopRequested = false;
const originalText = '⏫ 加载全部';
button.style.backgroundColor = '#dc3545';
logger.info('开始自动滚动加载历史记录...');
let idleRounds = 0; // 连续无变化的轮数
let rounds = 0;
let lastHeight = container.scrollHeight;
let lastCount = state.mergeStore.map.size;
while (!this.stopRequested && idleRounds < 4 && rounds < 300) {
// 页面重新渲染可能导致容器失效,重新查找
if (!container.isConnected) {
container = this.findScrollContainer();
if (!container) break;
}
container.scrollTop = 0;
// 有些虚拟列表靠wheel事件触发加载,补发一个向上滚动事件
container.dispatchEvent(new WheelEvent('wheel', { deltaY: -600, bubbles: true, cancelable: true }));
await this.sleep(800); // 等待平台请求并渲染更早的消息
rounds++;
const height = container.scrollHeight;
const count = state.mergeStore.map.size;
if (height > lastHeight + 5 || count > lastCount) {
idleRounds = 0;
} else {
idleRounds++;
}
lastHeight = height;
lastCount = count;
button.innerText = `⏳ 已捕获 ${count} 条(点击停止)`;
}
this.running = false;
const finalCount = state.mergeStore.map.size;
logger.info(`自动滚动结束:共捕获 ${finalCount} 条消息,滚动 ${rounds} 轮`);
button.innerText = this.stopRequested ? `⏹ 已停止,共 ${finalCount} 条` : `✅ 完成,共 ${finalCount} 条`;
button.style.backgroundColor = '#28a745';
setTimeout(() => {
button.innerText = originalText;
button.style.backgroundColor = '#007bff';
}, 4000);
}
};
// ============ 自动滚动模块结束 ============
// 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 'qianwen':
platformName = `Qianwen_${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_title':
platformName = state.currentTitle ||jsonData.downlink_body?.get_conv_info_downlink_body?.conversation_info?.name ||'豆包对话';
statsText = state.platformType === 'doubao_title'
? `对话数: ${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 loadAllButton = document.createElement('button');
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'
};
// 设置按钮属性和样式
loadAllButton.id = 'loadAllHistoryButton';
loadAllButton.innerText = '⏫ 加载全部';
loadAllButton.title = '自动向上滚动,把被隐藏的历史轮次全部加载出来';
jsonButton.id = 'downloadJsonButton';
jsonButton.innerText = 'JSON';
mdButton.id = 'downloadMdButton';
mdButton.innerText = 'MD';
wordButton.id = 'downloadWordButton';
wordButton.innerHTML = `
Word`;
// 添加按钮的 classname
[loadAllButton, jsonButton, mdButton, wordButton].forEach(btn => {
btn.className = 'export-button';
});
Object.assign(loadAllButton.style, buttonStyles);
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 冒泡影响拖动
[loadAllButton, jsonButton, mdButton, wordButton].forEach(btn => {
btn.addEventListener('mousedown', (e) => e.stopPropagation());
});
// 按钮点击事件
loadAllButton.onclick = () => autoScroller.start(loadAllButton);
jsonButton.onclick = exportHandlers.json;
mdButton.onclick = exportHandlers.markdown;
wordButton.onclick = exportHandlers.word;
// 组装按钮
buttonContainer.appendChild(loadAllButton);
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(() => {
// 自动滚动加载进行中时保持展开,方便查看进度
if (autoScroller.running) {
autoCollapse();
return;
}
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() {
// 千问(qianwen.com)等站点的 CSP 含 nonce + 'Strict-Dynamic',可能阻止篡改猴
// 把脚本注入页面主环境,脚本会退到沙箱/隔离环境运行:此时 DOM 操作(悬浮按钮)正常,
// 但直接改写 window.fetch 钩不到页面的真实请求。因此优先挂钩页面真实 window(unsafeWindow)。
const sandboxed = (typeof unsafeWindow !== 'undefined' && unsafeWindow !== window);
const pageWindow = sandboxed ? unsafeWindow : window;
if (sandboxed) {
logger.warn('当前运行在沙箱环境,改用 unsafeWindow 挂钩网络请求');
}
// 拦截XHR请求
const originalOpen = pageWindow.XMLHttpRequest.prototype.open;
pageWindow.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];
const url = args[1];
//console.log('XHR.open被调用:', url);
this.addEventListener('load', () => {
if (this.responseURL) {
//console.log('XHR请求完成:', 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 = pageWindow.fetch;
pageWindow.fetch = async function(...args) {
const url = args[0] instanceof Request ? args[0].url : args[0];
let response;
//console.log('🔍 Fetch请求被调用:', url);
//console.log('⏰ 时间线调试 - 脚本启动:', performance.now());
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;
};
// 启动自检:确认钩子确实挂到了页面环境(延迟3秒,避开页面自身初始化)
setTimeout(() => {
try {
const hooked = String(pageWindow.fetch).indexOf('native code') === -1;
if (hooked) {
logger.info('网络拦截钩子已挂载到页面环境,等待捕获对话数据...');
} else {
logger.warn('⚠️ 网络钩子未能挂载到页面环境(页面fetch仍为原生实现),导出功能将无法捕获数据。' +
'可尝试:1) 篡改猴 设置→安全 中检查沙箱模式;2) chrome://extensions 开启"开发者模式"后重试。');
}
} catch (e) { /* 自检失败不影响主流程 */ }
}, 3000);
}
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);
}
})();