MiniMax Dialogue Exporter

导出 MiniMax Agent 对话内容为 Markdown 格式,包括对话、Task 和 Thinking

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         MiniMax Dialogue Exporter
// @namespace    https://agent.minimaxi.com/
// @version      3.2.1
// @description  导出 MiniMax Agent 对话内容为 Markdown 格式,包括对话、Task 和 Thinking
// @author       AIPD01
// @match        https://agent.minimaxi.com/*
// @icon         https://agent.minimaxi.com/favicon.ico
// @grant        GM_download
// @grant        GM_setClipboard
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 配置
    const CONFIG = {
        containerId: 'minimax-export-container',
        exportBtnId: 'minimax-export-btn',
        copyBtnId: 'minimax-copy-btn'
    };

    // 创建按钮容器
    function createButtonContainer() {
        if (document.getElementById(CONFIG.containerId)) return;

        // 创建容器
        const container = document.createElement('div');
        container.id = CONFIG.containerId;
        container.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 9999;
            display: flex;
            flex-direction: column;
            gap: 10px;
        `;

        // 创建导出按钮
        const exportBtn = createButton(CONFIG.exportBtnId, '📥 导出文件', '#4F46E5', exportToFile);
        
        // 创建复制按钮
        const copyBtn = createButton(CONFIG.copyBtnId, '📋 复制内容', '#10B981', copyToClipboard);

        container.appendChild(exportBtn);
        container.appendChild(copyBtn);
        document.body.appendChild(container);
    }

    // 创建单个按钮
    function createButton(id, text, bgColor, onClick) {
        const button = document.createElement('button');
        button.id = id;
        button.textContent = text;
        button.style.cssText = `
            padding: 12px 20px;
            background-color: ${bgColor};
            color: #ffffff;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 600;
            box-shadow: 0 4px 12px ${bgColor}66;
            transition: all 0.3s ease;
            white-space: nowrap;
        `;

        const hoverColor = adjustColor(bgColor, -20);
        
        button.addEventListener('mouseenter', () => {
            button.style.backgroundColor = hoverColor;
            button.style.transform = 'translateY(-2px)';
        });

        button.addEventListener('mouseleave', () => {
            button.style.backgroundColor = bgColor;
            button.style.transform = 'translateY(0)';
        });

        button.addEventListener('click', onClick);
        return button;
    }

    // 调整颜色亮度
    function adjustColor(hex, amount) {
        const num = parseInt(hex.replace('#', ''), 16);
        const r = Math.max(0, Math.min(255, (num >> 16) + amount));
        const g = Math.max(0, Math.min(255, ((num >> 8) & 0x00FF) + amount));
        const b = Math.max(0, Math.min(255, (num & 0x0000FF) + amount));
        return `#${(1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)}`;
    }

    // 获取对话标题
    function getDialogueTitle() {
        // 从页面标题获取,格式通常是 "标题 - MiniMax Agent"
        const pageTitle = document.title;
        const titleMatch = pageTitle.match(/^(.+?)\s*-\s*MiniMax Agent$/);
        if (titleMatch) {
            return titleMatch[1].trim();
        }
        // 备用:从 URL 的 share ID 生成
        const urlMatch = window.location.pathname.match(/\/share\/(\d+)/);
        if (urlMatch) {
            return `MiniMax 对话_${urlMatch[1]}`;
        }
        return `MiniMax 对话_${new Date().toISOString().slice(0, 10)}`;
    }

    // 清理文本
    function cleanText(text) {
        return text
            .replace(/\s+/g, ' ')
            .trim();
    }

    // 获取元素的缩进层级 (基于 padding-left)
    function getIndentLevel(element) {
        const style = element.getAttribute('style') || '';
        const paddingMatch = style.match(/padding-left:\s*(\d+)px/);
        if (paddingMatch) {
            const padding = parseInt(paddingMatch[1]);
            if (padding >= 64) return 2;  // 二级子内容
            if (padding >= 32) return 1;  // 一级子任务
        }
        return 0;  // 顶层
    }

    // 主提取函数 - 基于实际 DOM 结构,支持层级
    function extractDialogueFromDOM() {
        const items = [];
        
        // 找到主对话容器
        const messagesContainer = document.querySelector('.messages-container');
        if (!messagesContainer) {
            console.warn('未找到 .messages-container');
            return items;
        }

        // 获取所有消息块 - 直接子元素中包含 .message 的
        const allMessages = messagesContainer.querySelectorAll('.message.sent, .message.received');
        
        if (allMessages.length === 0) {
            console.warn('未找到消息元素');
            return items;
        }

        const processedTexts = new Set(); // 用于去重

        allMessages.forEach((messageEl, index) => {
            const isSent = messageEl.classList.contains('sent');
            const isReceived = messageEl.classList.contains('received');
            
            // 获取层级
            const level = getIndentLevel(messageEl);

            if (isSent) {
                // 用户消息
                const userContent = extractUserMessageContent(messageEl);
                if (userContent && !processedTexts.has(userContent)) {
                    items.push({
                        type: 'user',
                        content: userContent,
                        level: 0  // 用户消息始终是顶层
                    });
                    processedTexts.add(userContent);
                }
            } else if (isReceived) {
                // AI 响应 - 可能包含多种内容
                const receivedItems = extractReceivedContent(messageEl, processedTexts, level);
                items.push(...receivedItems);
            }
        });

        return items;
    }

    // 提取用户消息内容
    function extractUserMessageContent(messageEl) {
        // 用户消息结构:.message.sent > .message-content > .text-pretty
        const textPretty = messageEl.querySelector('.text-pretty');
        if (textPretty) {
            return cleanText(textPretty.textContent || '');
        }
        
        // 备用:直接获取 .message-content 的文本
        const messageContent = messageEl.querySelector('.message-content');
        if (messageContent) {
            return cleanText(messageContent.textContent || '');
        }
        
        return '';
    }

    // 提取 AI 响应内容(received 消息)
    function extractReceivedContent(messageEl, processedTexts, level) {
        const items = [];

        // 1. 检查是否是思考块
        const thinkContainer = messageEl.querySelector('.think-container');
        if (thinkContainer) {
            const thinkingItem = extractThinkingBlock(thinkContainer, processedTexts, level);
            if (thinkingItem) {
                items.push(thinkingItem);
            }
            
            // 思考块后面可能还有正文内容
            const matrixMarkdown = messageEl.querySelector('.matrix-markdown');
            if (matrixMarkdown) {
                const textItems = extractMarkdownContent(matrixMarkdown, processedTexts, true, level);
                items.push(...textItems);
            }
            return items;
        }

        // 2. 检查是否是工具调用
        const toolName = messageEl.querySelector('.tool-name');
        if (toolName) {
            const toolItem = extractToolBlock(messageEl, processedTexts, level);
            if (toolItem) {
                items.push(toolItem);
            }
            return items;
        }

        // 3. 普通 AI 响应 - 提取 matrix-markdown 内容
        const matrixMarkdown = messageEl.querySelector('.matrix-markdown');
        if (matrixMarkdown) {
            const textItems = extractMarkdownContent(matrixMarkdown, processedTexts, false, level);
            items.push(...textItems);
        }

        return items;
    }

    // 提取思考块内容
    function extractThinkingBlock(thinkContainer, processedTexts, level = 0) {
        // 获取思考时间
        let duration = '';
        const durationSpan = thinkContainer.querySelector('span');
        if (durationSpan) {
            const timeText = durationSpan.textContent;
            const timeMatch = timeText.match(/(\d+\.?\d*)s/);
            if (timeMatch) {
                duration = `${timeMatch[1]}s`;
            }
        }

        // 获取思考内容 - 在 .hidden 内的 .relative.pl-5 中
        let content = '';
        const hiddenDiv = thinkContainer.querySelector('.hidden');
        if (hiddenDiv) {
            const contentDiv = hiddenDiv.querySelector('.relative.pl-5, [class*="pl-5"]');
            if (contentDiv) {
                content = extractTextFromElement(contentDiv);
            } else {
                // 备用:直接获取 hidden div 的文本
                content = extractTextFromElement(hiddenDiv);
            }
        }

        const key = `thinking:${duration}:${content.slice(0, 50)}`;
        if (processedTexts.has(key)) return null;
        processedTexts.add(key);

        return {
            type: 'thinking',
            duration: duration,
            content: content || null,
            level: level
        };
    }

    // 提取工具调用块
    function extractToolBlock(messageEl, processedTexts, level = 0) {
        const toolNameEl = messageEl.querySelector('.tool-name');
        if (!toolNameEl) return null;

        const fullText = toolNameEl.textContent.trim();
        
        // 检查是否是高级任务(深度研究任务、浏览器代理等)
        const isAgentTask = toolNameEl.classList.contains('tool-agent-name');
        if (isAgentTask) {
            // 高级任务 - 作为章节标题
            const agentTaskName = fullText;
            const key = `agent:${agentTaskName}`;
            if (processedTexts.has(key)) return null;
            processedTexts.add(key);
            
            return {
                type: 'agent_task',
                name: agentTaskName,
                level: level
            };
        }
        
        // 判断状态
        const isCompleted = fullText.includes('已完成') || fullText.includes('Completed');
        const isOngoing = fullText.includes('正在进行') || fullText.includes('Ongoing');
        
        if (!isCompleted && !isOngoing) return null;

        // 提取动作名称 - 在 span 中
        let action = '';
        const actionSpans = toolNameEl.querySelectorAll('span');
        actionSpans.forEach(span => {
            const spanText = span.textContent.trim();
            if (spanText && !spanText.match(/^\d/) && spanText.length > 2) {
                if (spanText.includes('已完成') || spanText.includes('正在进行')) {
                    action = spanText;
                }
            }
        });

        if (!action) {
            // 从全文提取
            action = fullText
                .replace(/已完成|正在进行|Completed|Ongoing/g, '')
                .trim()
                .split('\n')[0]
                .trim();
        }

        // 提取详细信息(如文件路径)
        let detail = '';
        const detailDiv = toolNameEl.querySelector('[class*="text-col_text01"]');
        if (detailDiv) {
            detail = detailDiv.textContent.trim();
        } else {
            // 从全文提取路径
            const pathMatch = fullText.match(/(\/[\w\-\/\.]+)/);
            if (pathMatch) {
                detail = pathMatch[1];
            }
        }

        // 清理 action
        action = action.replace(detail, '').trim();
        if (!action || action.length < 2) {
            action = fullText.split('\n')[0].replace(/已完成|正在进行/g, '').trim();
        }

        const key = `task:${action}:${detail}`;
        if (processedTexts.has(key)) return null;
        processedTexts.add(key);

        return {
            type: 'task',
            status: isCompleted ? 'completed' : 'ongoing',
            action: action,
            detail: detail,
            level: level
        };
    }

    // 提取Markdown内容
    function extractMarkdownContent(matrixMarkdown, processedTexts, skipThinking, level = 0) {
        const items = [];
        
        // 提取纯文本内容(排除思考块)
        const text = extractTextFromElementExcluding(matrixMarkdown, skipThinking ? '.think-container' : null);
        if (text && text.length > 5) {
            const key = `assistant:${text.slice(0, 100)}`;
            if (!processedTexts.has(key)) {
                items.push({
                    type: 'assistant',
                    content: text,
                    level: level
                });
                processedTexts.add(key);
            }
        }

        return items;
    }
    
    // 从元素提取文本(可排除指定选择器)
    function extractTextFromElementExcluding(element, excludeSelector) {
        if (!element) return '';
        
        let text = '';
        const walker = document.createTreeWalker(
            element,
            NodeFilter.SHOW_TEXT,
            {
                acceptNode: function(node) {
                    // 跳过 SVG 内的文本
                    if (node.parentElement?.closest('svg')) {
                        return NodeFilter.FILTER_REJECT;
                    }
                    // 跳过排除选择器内的文本
                    if (excludeSelector && node.parentElement?.closest(excludeSelector)) {
                        return NodeFilter.FILTER_REJECT;
                    }
                    // 跳过空文本
                    if (!node.textContent.trim()) {
                        return NodeFilter.FILTER_REJECT;
                    }
                    return NodeFilter.FILTER_ACCEPT;
                }
            }
        );

        let currentNode;
        while (currentNode = walker.nextNode()) {
            const nodeText = currentNode.textContent.trim();
            // 跳过仅包含时间格式的节点
            if (nodeText && !nodeText.match(/^\d+\.?\d*s$/)) {
                text += nodeText + ' ';
            }
        }

        return cleanText(text);
    }

    // 从元素提取文本(处理嵌套结构)
    function extractTextFromElement(element) {
        if (!element) return '';
        
        let text = '';
        const walker = document.createTreeWalker(
            element,
            NodeFilter.SHOW_TEXT,
            {
                acceptNode: function(node) {
                    // 跳过 SVG 内的文本
                    if (node.parentElement?.closest('svg')) {
                        return NodeFilter.FILTER_REJECT;
                    }
                    // 跳过空文本
                    if (!node.textContent.trim()) {
                        return NodeFilter.FILTER_REJECT;
                    }
                    return NodeFilter.FILTER_ACCEPT;
                }
            }
        );

        let node;
        while (node = walker.nextNode()) {
            const nodeText = node.textContent.trim();
            // 跳过仅包含时间格式的节点
            if (nodeText && !nodeText.match(/^\d+\.?\d*s$/)) {
                text += nodeText + ' ';
            }
        }

        return cleanText(text);
    }

    // 去重
    function deduplicateItems(items) {
        const seen = new Set();
        return items.filter(item => {
            const key = item.type + ':' + (item.content?.slice(0, 50) || item.action || '');
            if (seen.has(key)) return false;
            if (key.length < 5) return false;
            seen.add(key);
            return true;
        });
    }

    // 转换为Markdown
    function convertToMarkdown(title, items) {
        let markdown = `# ${title}\n\n`;
        markdown += `> 导出时间: ${new Date().toLocaleString('zh-CN')}\n`;
        markdown += `> 导出条数: ${items.length} 条\n`;
        markdown += `> 来源: ${window.location.href}\n\n`;
        markdown += `---\n\n`;

        let currentRole = '';
        let currentLevel = 0;
        let assistantContentBuffer = [];

        const flushAssistantBuffer = () => {
            if (assistantContentBuffer.length > 0) {
                markdown += assistantContentBuffer.join('\n\n') + '\n\n';
                assistantContentBuffer = [];
            }
        };

        // 根据层级获取标题前缀
        const getHeadingPrefix = (baseLevel, itemLevel) => {
            // baseLevel: 2 = ##, 3 = ###, 4 = ####
            const totalLevel = baseLevel + itemLevel;
            return '#'.repeat(Math.min(totalLevel, 6)); // 最多 6 级标题
        };

        items.forEach((item, index) => {
            const level = item.level || 0;
            
            switch (item.type) {
                case 'user':
                    flushAssistantBuffer();
                    markdown += `## 👤 用户\n\n`;
                    markdown += `${item.content}\n\n`;
                    currentRole = 'user';
                    currentLevel = 0;
                    break;

                case 'agent_task':
                    // 高级任务(深度研究任务、浏览器代理等)- 作为三级标题
                    flushAssistantBuffer();
                    const agentHeading = getHeadingPrefix(3, level);
                    markdown += `${agentHeading} 🔄 ${item.name}\n\n`;
                    currentRole = 'agent';
                    currentLevel = level;
                    break;

                case 'assistant':
                    // 根据层级决定是否需要新的标题
                    if (currentRole !== 'assistant' || level !== currentLevel) {
                        flushAssistantBuffer();
                        if (level === 0) {
                            markdown += `## 🤖 AI助手\n\n`;
                        } else if (level === 1) {
                            // 一级子任务的 AI 响应
                            markdown += `#### 📌 子任务响应\n\n`;
                        }
                        // level >= 2 的内容不加标题,直接作为正文
                        currentRole = 'assistant';
                        currentLevel = level;
                    }
                    assistantContentBuffer.push(item.content);
                    break;

                case 'thinking':
                    flushAssistantBuffer();
                    markdown += `<details>\n`;
                    markdown += `<summary>💭 思考过程 ${item.duration || ''}</summary>\n\n`;
                    if (item.content) {
                        markdown += `${item.content}\n\n`;
                    } else {
                        markdown += `*(思考内容未展开)*\n\n`;
                    }
                    markdown += `</details>\n\n`;
                    currentRole = '';
                    break;

                case 'task':
                    flushAssistantBuffer();
                    const statusIcon = item.status === 'completed' ? '✅' : '🔄';
                    // 根据层级添加缩进
                    const indent = level > 0 ? '  '.repeat(level) : '';
                    markdown += `${indent}${statusIcon} **${item.action}**`;
                    if (item.detail) {
                        markdown += ` \`${item.detail}\``;
                    }
                    markdown += `\n\n`;
                    currentRole = '';
                    break;
            }
        });

        flushAssistantBuffer();
        return markdown;
    }

    // 备用提取方法 - 基于类名扫描
    function extractDialogueFromClasses() {
        const items = [];
        const processedTexts = new Set();

        // 1. 提取所有 .text-pretty 作为可能的用户消息
        document.querySelectorAll('.message.sent .text-pretty').forEach(el => {
            const text = cleanText(el.textContent || '');
            if (text && text.length > 2 && !processedTexts.has(text)) {
                items.push({ type: 'user', content: text, level: 0 });
                processedTexts.add(text);
            }
        });

        // 2. 提取高级任务(深度研究任务等)
        document.querySelectorAll('.tool-agent-name').forEach(el => {
            const text = el.textContent.trim();
            const key = `agent:${text}`;
            if (text && !processedTexts.has(key)) {
                const messageEl = el.closest('.message');
                const level = messageEl ? getIndentLevel(messageEl) : 0;
                items.push({ type: 'agent_task', name: text, level: level });
                processedTexts.add(key);
            }
        });

        // 3. 提取思考块
        document.querySelectorAll('.think-container').forEach(el => {
            const messageEl = el.closest('.message');
            const level = messageEl ? getIndentLevel(messageEl) : 0;
            const item = extractThinkingBlock(el, processedTexts, level);
            if (item) items.push(item);
        });

        // 4. 提取工具调用
        document.querySelectorAll('.tool-name:not(.tool-agent-name)').forEach(el => {
            const messageEl = el.closest('.message');
            if (messageEl) {
                const level = getIndentLevel(messageEl);
                const item = extractToolBlock(messageEl, processedTexts, level);
                if (item) items.push(item);
            }
        });

        // 5. 提取 AI 响应文本
        document.querySelectorAll('.message.received .matrix-markdown').forEach(el => {
            // 跳过思考块内的
            if (el.closest('.think-container')) return;
            
            const messageEl = el.closest('.message');
            const level = messageEl ? getIndentLevel(messageEl) : 0;
            
            const text = extractTextFromElement(el);
            if (text && text.length > 10 && !processedTexts.has(text.slice(0, 100))) {
                items.push({ type: 'assistant', content: text, level: level });
                processedTexts.add(text.slice(0, 100));
            }
        });

        return items;
    }

    // 获取提取结果
    function getExtractedContent() {
        const title = getDialogueTitle();
        
        // 首先尝试 DOM 结构提取
        let items = extractDialogueFromDOM();
        
        // 如果结果太少,使用备用方法
        if (items.length < 3) {
            console.log('DOM提取结果较少,尝试备用方法...');
            items = extractDialogueFromClasses();
        }

        // 去重
        items = deduplicateItems(items);

        return { title, items };
    }

    // 导出到文件
    function exportToFile() {
        try {
            const { title, items } = getExtractedContent();

            if (items.length === 0) {
                alert('未能提取到对话内容。\n\n⚠️ 提示:\n1. 此页面是演示动画,请等待动画播放完成后再导出\n2. 确保页面已完全加载\n3. 如果仍无法导出,请尝试刷新页面');
                return;
            }

            const markdown = convertToMarkdown(title, items);
            
            // 下载文件
            downloadMarkdown(title, markdown);

            console.log(`✅ 成功导出 ${items.length} 条对话内容到文件`);
            showToast(`已导出 ${items.length} 条内容到文件`);
            
        } catch (error) {
            console.error('导出失败:', error);
            alert('导出失败: ' + error.message);
        }
    }

    // 复制到剪贴板
    function copyToClipboard() {
        try {
            const { title, items } = getExtractedContent();

            if (items.length === 0) {
                alert('未能提取到对话内容。\n\n⚠️ 提示:\n1. 此页面是演示动画,请等待动画播放完成后再导出\n2. 确保页面已完全加载\n3. 如果仍无法导出,请尝试刷新页面');
                return;
            }

            const markdown = convertToMarkdown(title, items);
            
            // 复制到剪贴板
            if (typeof GM_setClipboard !== 'undefined') {
                GM_setClipboard(markdown, 'text');
                console.log(`✅ 成功复制 ${items.length} 条对话内容到剪贴板`);
                showToast(`已复制 ${items.length} 条内容到剪贴板`);
            } else {
                navigator.clipboard.writeText(markdown).then(() => {
                    console.log(`✅ 成功复制 ${items.length} 条对话内容到剪贴板`);
                    showToast(`已复制 ${items.length} 条内容到剪贴板`);
                }).catch(e => {
                    console.error('复制到剪贴板失败:', e);
                    alert('复制失败,请重试');
                });
            }
            
        } catch (error) {
            console.error('复制失败:', error);
            alert('复制失败:' + error.message);
        }
    }

    // 显示提示
    function showToast(message) {
        const toast = document.createElement('div');
        toast.textContent = message;
        toast.style.cssText = `
            position: fixed;
            bottom: 80px;
            right: 20px;
            background: #10B981;
            color: white;
            padding: 12px 24px;
            border-radius: 8px;
            font-size: 14px;
            font-weight: 500;
            z-index: 10000;
            box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
            animation: fadeInOut 2s ease forwards;
        `;
        
        // 添加动画
        const style = document.createElement('style');
        style.textContent = `
            @keyframes fadeInOut {
                0% { opacity: 0; transform: translateY(10px); }
                15% { opacity: 1; transform: translateY(0); }
                85% { opacity: 1; transform: translateY(0); }
                100% { opacity: 0; transform: translateY(-10px); }
            }
        `;
        document.head.appendChild(style);
        
        document.body.appendChild(toast);
        setTimeout(() => {
            toast.remove();
            style.remove();
        }, 2000);
    }

    // 下载 Markdown 文件
    function downloadMarkdown(title, content) {
        const filename = sanitizeFilename(title) + '.md';
        
        // 使用 Data URL 方式下载(兼容性最好)
        try {
            // 将内容转换为 Base64
            const base64Content = btoa(unescape(encodeURIComponent(content)));
            const dataUrl = `data:text/markdown;charset=utf-8;base64,${base64Content}`;
            
            const a = document.createElement('a');
            a.href = dataUrl;
            a.download = filename;
            a.style.display = 'none';
            document.body.appendChild(a);
            a.click();
            
            // 延迟移除元素
            setTimeout(() => {
                document.body.removeChild(a);
            }, 100);
            
            console.log(`📥 正在下载:${filename}`);
        } catch (e) {
            console.error('Data URL 下载失败,尝试 Blob 方式:', e);
            
            // 备用方案:使用 Blob URL
            const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;
            a.style.display = 'none';
            document.body.appendChild(a);
            a.click();
            
            setTimeout(() => {
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
            }, 100);
        }
    }

    // 清理文件名
    function sanitizeFilename(name) {
        return name
            .replace(/[<>:"/\\|?*]/g, '_')
            .replace(/\s+/g, '_')
            .slice(0, 100);
    }

    // 初始化
    function init() {
        // 等待页面加载完成
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', createButtonContainer);
        } else {
            createButtonContainer();
        }

        // 监听 URL 变化(SPA 应用)
        let lastUrl = location.href;
        new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                setTimeout(createButtonContainer, 1000);
            }
        }).observe(document.body, { subtree: true, childList: true });
    }

    init();
})();