Greasy Fork 支持简体中文。

Poe Chat Exporter

Export chat conversations from poe.com to text format, supports Markdown and plain text. Export or Save Chats from poe.com AI.

// ==UserScript==
// @name         Poe Chat Exporter
// @name:zh-CN   Poe 聊天记录导出工具
// @namespace    https://github.com/KoriIku/poe-exporter
// @version      0.3
// @description  Export chat conversations from poe.com to text format, supports Markdown and plain text. Export or Save Chats from poe.com AI.
// @description:zh-CN  导出 poe.com 的聊天记录为文本格式,支持 Markdown 和纯文本
// @author       KoriIku
// @homepage     https://github.com/KoriIku/poe-exporter
// @supportURL   https://github.com/KoriIku/poe-exporter/issues
// @match        https://poe.com/*
// @grant        none
// @require      https://unpkg.com/turndown/dist/turndown.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 创建 TurndownService 实例
    const turndownService = new TurndownService({
        headingStyle: 'atx',
        bulletListMarker: '-',
        codeBlockStyle: 'fenced'
    });

    // 配置 turndown 规则
    turndownService.addRule('preserveCode', {
        filter: ['pre', 'code'],
        replacement: function(content, node) {
            if (node.nodeName === 'PRE') {
                let language = '';
                const codeBlock = node.querySelector('code');
                if (codeBlock && codeBlock.className) {
                    const match = codeBlock.className.match(/language-(\w+)/);
                    if (match) {
                        language = match[1];
                    }
                }
                return '\n```' + language + '\n' + content + '\n```\n';
            }
            return '`' + content + '`';
        }
    });

    // 语言配置
    const i18n = {
        zh: {
            extract: '提取内容',
            copy: '复制内容',
            download: '下载文本',
            copied: '已复制!',
            downloaded: '已下载!',
            assistant: 'Assistant:\n',
            user: 'User:\n',
            close: '关闭',
            toggleFormat: '切换格式', // 新增
            markdown: 'Markdown格式', // 新增
            plainText: '纯文本格式'  // 新增
        },
        en: {
            extract: 'Extract',
            copy: 'Copy',
            download: 'Download',
            copied: 'Copied!',
            downloaded: 'Downloaded!',
            assistant: 'Assistant:\n',
            user: 'User:\n',
            close: 'Close',
            toggleFormat: 'Toggle Format', // 新增
            markdown: 'Markdown Format', // 新增
            plainText: 'Plain Text Format' // 新增
        }
    };

    // 获取语言设置
    const userLang = (navigator.language || navigator.userLanguage).startsWith('zh') ? 'zh' : 'en';
    const text = i18n[userLang];

    // 存储当前格式状态
    let isMarkdownFormat = true;

    // 创建按钮
    const btn = document.createElement('button');
    btn.textContent = text.extract;
    btn.style.cssText = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 10000;
        padding: 10px;
        background: #4CAF50;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        box-shadow: 0 2px 5px rgba(0,0,0,0.2);
    `;

    // 创建悬浮窗
    const floatingWindow = document.createElement('div');
    floatingWindow.style.cssText = `
        position: fixed;
        top: 80px;
        right: 20px;
        width: 300px;
        max-height: 80vh;
        background: #2d2d2d;
        color: #e0e0e0;
        padding: 15px;
        border-radius: 8px;
        box-shadow: 0 0 10px rgba(0,0,0,0.3);
        z-index: 10000;
        display: none;
    `;

    // 创建标题栏
    const titleBar = document.createElement('div');
    titleBar.style.cssText = `
        display: flex;
        justify-content: space-between;
        margin-bottom: 10px;
        align-items: center;
    `;

    // 创建格式指示器
    const formatIndicator = document.createElement('span');
    formatIndicator.style.cssText = `
        font-size: 12px;
        color: #888;
    `;
    formatIndicator.textContent = text.markdown;

    // 创建关闭按钮
    const closeBtn = document.createElement('button');
    closeBtn.textContent = text.close;
    closeBtn.style.cssText = `
        background: transparent;
        border: none;
        color: #e0e0e0;
        cursor: pointer;
        padding: 5px;
        font-size: 14px;
        border-radius: 4px;
    `;
    closeBtn.addEventListener('mouseover', function() {
        this.style.background = 'rgba(255,255,255,0.1)';
    });
    closeBtn.addEventListener('mouseout', function() {
        this.style.background = 'transparent';
    });

    // 创建内容容器
    const contentContainer = document.createElement('div');
    contentContainer.style.cssText = `
        display: flex;
        flex-direction: column;
        height: calc(80vh - 80px);
    `;

    // 创建内容区域
    const contentArea = document.createElement('div');
    contentArea.style.cssText = `
        white-space: pre-wrap;
        margin-bottom: 10px;
        padding: 10px;
        background: #363636;
        border-radius: 4px;
        font-family: monospace;
        line-height: 1.5;
        flex: 1;
        overflow-y: auto;
    `;

    // 创建按钮容器
    const buttonContainer = document.createElement('div');
    buttonContainer.style.cssText = `
        display: flex;
        gap: 10px;
        padding-top: 10px;
        border-top: 1px solid #444;
    `;

    // 创建复制按钮
    const copyBtn = document.createElement('button');
    copyBtn.textContent = text.copy;
    copyBtn.style.cssText = `
        padding: 8px 12px;
        background: #2196F3;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        flex: 1;
        transition: background 0.2s;
    `;

    // 创建下载按钮
    const downloadBtn = document.createElement('button');
    downloadBtn.textContent = text.download;
    downloadBtn.style.cssText = copyBtn.style.cssText;
    downloadBtn.style.background = '#FF9800';

    // 创建格式切换按钮
    const toggleFormatBtn = document.createElement('button');
    toggleFormatBtn.textContent = text.toggleFormat;
    toggleFormatBtn.style.cssText = copyBtn.style.cssText;
    toggleFormatBtn.style.background = '#9C27B0';

    // 添加按钮悬停效果
    [btn, copyBtn, downloadBtn, toggleFormatBtn].forEach(button => {
        button.addEventListener('mouseover', function() {
            this.style.filter = 'brightness(1.1)';
        });
        button.addEventListener('mouseout', function() {
            this.style.filter = 'brightness(1)';
        });
    });

    // 组装UI
    titleBar.appendChild(formatIndicator);
    titleBar.appendChild(closeBtn);
    buttonContainer.appendChild(copyBtn);
    buttonContainer.appendChild(downloadBtn);
    buttonContainer.appendChild(toggleFormatBtn);
    contentContainer.appendChild(contentArea);
    contentContainer.appendChild(buttonContainer);
    floatingWindow.appendChild(titleBar);
    floatingWindow.appendChild(contentContainer);
    document.body.appendChild(btn);
    document.body.appendChild(floatingWindow);

    // 提取内容函数
    function extractContent(useMarkdown = true) {
        let result = '';
        const markdownContainers = document.querySelectorAll('[class^="Markdown_markdownContainer"]');
        
        markdownContainers.forEach(container => {
            let parent = container;
            while (parent && !parent.className.includes('MessageBubble')) {
                parent = parent.parentElement;
            }
            
            if (parent) {
                let messageContent;
                if (useMarkdown) {
                    messageContent = turndownService.turndown(container.innerHTML);
                } else {
                    messageContent = container.textContent;
                }
                
                if (parent.className.includes('leftSide')) {
                    result += text.assistant + messageContent + '\n\n';
                } else if (parent.className.includes('rightSide')) {
                    result += text.user + messageContent + '\n\n';
                }
            }
        });

        return result;
    }

    // HTML解码函数
    function decodeHTML(html) {
        const txt = document.createElement('textarea');
        txt.innerHTML = html;
        return txt.value;
    }

    // 复制内容
    function copyContent() {
        navigator.clipboard.writeText(contentArea.textContent).then(() => {
            const originalText = copyBtn.textContent;
            copyBtn.textContent = text.copied;
            copyBtn.style.background = '#45a049';
            setTimeout(() => {
                copyBtn.textContent = originalText;
                copyBtn.style.background = '#2196F3';
            }, 1000);
        });
    }

    // 下载内容
    function downloadContent() {
        const blob = new Blob([contentArea.textContent], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'conversation.txt';
        a.click();
        URL.revokeObjectURL(url);
        
        const originalText = downloadBtn.textContent;
        downloadBtn.textContent = text.downloaded;
        setTimeout(() => {
            downloadBtn.textContent = originalText;
        }, 1000);
    }

    // 切换格式并刷新内容
    function toggleFormat() {
        isMarkdownFormat = !isMarkdownFormat;
        formatIndicator.textContent = isMarkdownFormat ? text.markdown : text.plainText;
        const content = extractContent(isMarkdownFormat);
        contentArea.textContent = content;
    }

    // 事件监听
    btn.addEventListener('click', () => {
        const content = extractContent(isMarkdownFormat);
        contentArea.textContent = content;
        floatingWindow.style.display = 'block';
    });

    copyBtn.addEventListener('click', copyContent);
    downloadBtn.addEventListener('click', downloadContent);
    toggleFormatBtn.addEventListener('click', toggleFormat);
    closeBtn.addEventListener('click', () => {
        floatingWindow.style.display = 'none';
    });

    // 添加拖拽功能
    let isDragging = false;
    let currentX;
    let currentY;
    let initialX;
    let initialY;
    let xOffset = 0;
    let yOffset = 0;

    titleBar.addEventListener('mousedown', dragStart);
    document.addEventListener('mousemove', drag);
    document.addEventListener('mouseup', dragEnd);

    function dragStart(e) {
        initialX = e.clientX - xOffset;
        initialY = e.clientY - yOffset;

        if (e.target === titleBar) {
            isDragging = true;
        }
    }

    function drag(e) {
        if (isDragging) {
            e.preventDefault();
            currentX = e.clientX - initialX;
            currentY = e.clientY - initialY;

            xOffset = currentX;
            yOffset = currentY;

            setTranslate(currentX, currentY, floatingWindow);
        }
    }

    function dragEnd() {
        isDragging = false;
    }

    function setTranslate(xPos, yPos, el) {
        el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`;
    }
})();