Greasy Fork

来自缓存

Greasy Fork is available in English.

Poe 聊天记录导出工具

导出 poe.com 的聊天记录为文本格式,支持 Markdown 和纯文本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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