Grok Chat对话记录导出为.md(Markdown)

将 https://grok.com/chat/* 页面中的聊天记录导出为 .md 文件

Per 19-06-2025. Zie de nieuwste versie.

// ==UserScript==
// @name         Grok Chat对话记录导出为.md(Markdown)
// @namespace    https://grok.com/
// @version      0.2
// @description  将 https://grok.com/chat/* 页面中的聊天记录导出为 .md 文件
// @author       GPT
// @match        https://grok.com/chat/*
// @grant        none
// @license      MIT
// ==/UserScript==

/*
 * 使用方法:
 * 1. 在 Tampermonkey 中安装此脚本并启用。
 * 2. 打开任意 Grok 对话页面(https://grok.com/chat/xxx)。
 * 3. 页面**右下角**会出现“导出 Markdown”按钮,点击即可下载 .md 文件。
 *
 * 如果未来 Grok 前端 DOM 结构发生改变,可能需要修改下方 messageSelectors
 * 或者调整 authorDetect 逻辑以正确识别消息作者。
 */

(function () {
    'use strict';

    /* ---------------- 配置区 ---------------- */
    // 根据实际 DOM 结构修改选择器,确保能选出所有消息节点,顺序必须与页面展示一致
    const messageSelectors = [
        '[data-testid*="chat-message"]',   // 通用 data-testid
        '[class*="Message"]',             // 类名包含 Message
        '[class*="message" i]'            // 类名包含 message(不区分大小写)
    ].join(',');

    // 导出按钮的样式(已调整到右下角,尺寸略微缩小)
    const buttonStyle = `
        position: fixed;
        bottom: 12px;
        right: 12px;
        z-index: 9999;
        padding: 4px 10px;
        font-size: 12px;
        background: #4caf50;
        color: #fff;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        box-shadow: 0 2px 6px rgba(0,0,0,.15);
    `;

    /* -------------- 逻辑实现区 -------------- */
    const BUTTON_ID = 'grok-md-export-btn';

    // 创建导出按钮
    function createButton() {
        if (document.getElementById(BUTTON_ID)) return; // 已存在
        const btn = document.createElement('button');
        btn.id = BUTTON_ID;
        btn.textContent = '导出 Markdown';
        btn.setAttribute('style', buttonStyle);
        btn.addEventListener('click', exportChat);
        document.body.appendChild(btn);
    }

    // 监听 DOM 变化,确保按钮在 SPA 路由切换后依旧存在
    const observer = new MutationObserver(createButton);
    observer.observe(document.body, { childList: true, subtree: true });
    // 初始插入
    createButton();

    // 导出主函数
    function exportChat() {
        const nodes = Array.from(document.querySelectorAll(messageSelectors));
        if (!nodes.length) {
            alert('未找到任何聊天记录,可能需要调整脚本中的 messageSelectors。');
            return;
        }

        const messages = nodes.map(processNode).filter(Boolean);
        if (!messages.length) {
            alert('无法解析聊天内容,可能需要调整脚本。');
            return;
        }

        const mdContent = buildMarkdown(messages);
        downloadMarkdown(mdContent);
    }

    // 解析单条消息节点,返回 {author, text}
    function processNode(node) {
        // 尝试通过 aria-label、类名、文本前缀等方式判断作者
        let author = 'Unknown';
        const aria = (node.getAttribute('aria-label') || '').toLowerCase();
        if (/assistant|grok/.test(aria)) author = 'Grok';
        else if (/user|you/.test(aria)) author = 'User';

        // Fallback: 根据类名或文本判断
        if (author === 'Unknown') {
            const cls = node.className.toLowerCase();
            if (cls.includes('assistant') || cls.includes('grok')) author = 'Grok';
            else if (cls.includes('user')) author = 'User';
        }

        const text = node.innerText.trim();
        if (!text) return null;

        return { author, text };
    }

    // 构建 Markdown 字符串
    function buildMarkdown(messages) {
        const lines = [];
        messages.forEach(m => {
            lines.push(`**${m.author}:**`);
            lines.push('');
            // 保留换行,使用两个空格 + 换行以符合 Markdown 换行语法
            lines.push(m.text.replace(/\r?\n/g, '  \n'));
            lines.push(''); // 留空行分隔
        });
        return lines.join('\n');
    }

    // 触发下载
    function downloadMarkdown(content) {
        const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        const ts = new Date().toISOString().slice(0, 19).replace(/[T:]/g, '-');
        a.download = `grok-chat-${ts}.md`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }
})();