DeepSeek防撤回助手

自动缓存并恢复被撤回的AI回答内容

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         DeepSeek防撤回助手
// @namespace    [email protected]
// @version      0.14
// @description  自动缓存并恢复被撤回的AI回答内容
// @author       Deepseek & Byozd
// @license      MIT
// @match        https://chat.deepseek.com/*
// @grant        GM_addStyle
// ==/UserScript==

GM_addStyle(`
  #ai-rescue-button {
    position: fixed !important;
    top: 20px !important;
    right: 20px !important;
    z-index: 99999 !important;
    transition: all 0.3s ease;
    box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  }

  #ai-rescue-button.ds-button--alert {
    box-shadow: 0 0 15px rgba(0,255,0,0.7) !important;
    animation: pulse 1s infinite;
  }
  @keyframes pulse {
    0% { opacity: 0.95; }
    50% { opacity: 1; }
    100% { opacity: 0.95; }
  }
  /* 新增弹出层样式 */
  #ai-rescue-popup {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-30%, -50%);
    background: #fff;
    border-radius: 8px;
    box-shadow: 0 0 20px rgba(0,0,0,0.3);
    z-index: 100000;
    min-width: 800px;
    max-width: 80%;
    padding: 20px;
  }

  #ai-rescue-popup h3 {
    margin: 0 0 15px 0;
    color: #2c3e50;
    font-size: 18px;
  }

  #ai-rescue-popup textarea {
    width: 100%;
    height: 300px;
    padding: 5px;
    border: 1px solid #ddd;
    border-radius: 4px;
    resize: vertical;
    font-family: monospace;
  }

  #ai-rescue-popup button {
    margin-top: 15px;
    padding: 8px 15px;
    background: #3498db;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
  }
  /* 新增渲染模式样式 */
  .content-viewer {
    display: flex;
    gap: 15px;
    min-height: 300px;
    max-height: 80vh;
  }

  .render-pane, .source-pane {
    flex: 1;
    border: 1px solid #ddd;
    border-radius: 4px;
    padding: 10px;
    overflow: auto;
  }

  .render-pane {
    background: #f9f9f9;
    white-space: pre-wrap;
  }

  .render-pane img {
    max-width: 100%;
  }

  .source-pane {
    font-family: monospace;
    display: none;
  }

  .source-pane.active {
    display: block;
  }

  .view-switcher {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 15px;
  }

  .toggle-source {
    background: none;
    border: 1px solid #3498db;
    color: #3498db;
    padding: 5px 10px;
    border-radius: 4px;
    cursor: pointer;
  }
`);

(function() {
    'use strict';

    const config = {
        CONTAINER_SELECTOR: 'div:has(>div>div.ds-markdown--block)', // 对话列表容器
        TARGET_PATTERN: 'div:not(:has(+ div)) > div.ds-markdown--block', // 最新内容框选择器
        TARGET_THOUGHT_PATTERN: 'div:not(:has(+ div)) > div:has(+div.ds-markdown--block)', // 最新内容框选择器
        BUTTON_CONTAINER: 'div:has(>#chat-input)+div', // 原按钮选择器
        CHECK_INTERVAL: 2000, // 容器检查间隔
        MIN_LENGTH: 20
    };

    let state = {
        currentTarget: null,
        currentThought: null,
        containerObserver: null,
        contentObserver: null,
        lastContent: '',
        lastThought: '',
        pendingContent: null,
        pendingThought: null,
        controlButton: null
    };

    function createControlButton() {
        const btn = document.createElement('button');
        btn.id = 'ai-rescue-button'; // 添加唯一ID
        btn.className = 'ds-button ds-button--m';
        btn.textContent = '缓存';

        // 移除原有的marginLeft样式
        btn.style.cssText = `
            min-width: 80px;
            cursor: pointer;
            font-weight: bold;
        `;
        // 点击事件保持原有逻辑
        btn.addEventListener('click', handleButtonClick);

        // 直接添加到body避免被覆盖
        document.body.appendChild(btn);

        return btn;
    }

    // 在showCachePopup函数中进行修改
    function showCachePopup() {
        const existingPopup = document.getElementById('ai-rescue-popup');
        if (existingPopup) existingPopup.remove();

        const popup = document.createElement('div');
        popup.id = 'ai-rescue-popup';
        popup.innerHTML = `
            <div class="view-switcher">
                <h3>已缓存内容 (${state.lastThought.length + state.lastContent.length}字符)</h3>
                <div class="button-panel">
                  <button class="toggle-source">查看源码</button>
                  <button onclick="this.closest('#ai-rescue-popup').remove()">关闭</button>
                </div>
            </div>
            <div class="content-viewer">
                <div class="render-pane"></div>
                <textarea class="source-pane" readonly></textarea>
            </div>
        `;

        // 填充内容
        const renderPane = popup.querySelector('.render-pane');
        const sourcePane = popup.querySelector('.source-pane');
        renderPane.classList.add(...Array.from(findLatestTarget()?.parentElement.classList));
        renderPane.innerHTML = state.lastThought + state.lastContent; // 渲染HTML
        sourcePane.value = state.lastThought + state.lastContent;     // 显示源码

        // 添加切换功能
        const toggleBtn = popup.querySelector('.toggle-source');
        toggleBtn.addEventListener('click', () => {
            const isShowingSource = sourcePane.classList.toggle('active');
            renderPane.style.display = isShowingSource ? 'none' : 'block';
            toggleBtn.textContent = isShowingSource ? '查看渲染' : '查看源码';

            // 自动滚动保持位置
            if (isShowingSource) {
                sourcePane.scrollTop = renderPane.scrollTop;
            } else {
                renderPane.scrollTop = sourcePane.scrollTop;
            }
        });

        // 保持原有点击外部关闭功能
        popup.addEventListener('click', (e) => {
            if (e.target === popup) popup.remove();
        });

        document.body.appendChild(popup);
    }

    function initControlButton() {
        const parent = document.querySelector(config.BUTTON_CONTAINER);
        if (!parent || state.controlButton) return;

        state.controlButton = createControlButton();
        parent.insertAdjacentElement('beforeend', state.controlButton);
    }

    function updateButtonState(isAlert = false) {
        if (!state.controlButton) return;

        state.controlButton.classList.toggle('ds-button--alert', isAlert);
        state.controlButton.textContent = isAlert ? '不许撤回' : '缓存';
    }

    // 获取最新内容框
    function findLatestTarget() {
        return document.querySelector(config.TARGET_PATTERN);
    }
    // 获取最新内容框
    function findLatestThoughtTarget() {
        return document.querySelector(config.TARGET_THOUGHT_PATTERN);
    }

    // 初始化容器观察
    function initContainerObserver() {
        const container = document.querySelector(config.CONTAINER_SELECTOR) || document.body;

        state.containerObserver = new MutationObserver(() => {
            checkTargetUpdate();
        });

        state.containerObserver.observe(container, {
            childList: true,
            subtree: true,
            attributes: false
        });
    }

    // 检查是否需要更新目标
    function checkTargetUpdate() {
        const newTarget = findLatestTarget();
        const newThoughtTarget = findLatestThoughtTarget();
        if (!newTarget || newTarget === state.currentTarget) return;

        // 切换观察目标
        if (state.contentObserver) {
            state.contentObserver.disconnect();
        }

        state.currentTarget = newTarget;
        state.lastContent = newTarget.outerHTML;

        state.currentThought = newThoughtTarget;
        state.lastThought = newThoughtTarget.outerHTML;

        state.contentObserver = new MutationObserver(handleContentMutations);
        state.contentObserver.observe(newTarget, {
            childList: true,
            subtree: true,
            characterData: true
        });

        console.log('切换到新内容框:', newTarget);
    }

    // 处理内容变化
    function handleContentMutations(mutations) {
        const newContent = state.currentTarget.innerHTML;
        const newThoughtContent = state.currentThought.innerHTML;

        // 内容增长时更新缓存
        if (newContent.length > state.lastContent.length) {
            state.lastContent = newContent;
            state.lastThought = newThoughtContent;
            updateButtonState();
        }
        // 检测撤回
        else if (newContent.length < state.lastContent.length - 10) {
            state.pendingContent = state.lastContent;
            state.pendingThought = state.lastThought;
            updateButtonState(true);
        }
    }

    // 控制按钮相关功能(保持原有实现,增加目标检查)
    function handleButtonClick() {
        if (!state.currentTarget) {
            checkTargetUpdate();
            return;
        }

        if (state.pendingContent) {
            restoreContent();
        } else {
            showCachePopup();
        }
    }

    function restoreContent() {
        state.currentTarget.insertAdjacentHTML("beforebegin", state.pendingThought);
        state.lastThought = state.pendingThought;
        state.pendingThought = null;

        state.currentTarget.innerHTML = state.pendingContent;
        state.lastContent = state.pendingContent;
        state.pendingContent = null;

        updateButtonState();
    }

    // 主初始化
    function mainInit() {
        // 确保按钮始终存在
        if (!state.controlButton || !document.body.contains(state.controlButton)) {
            state.controlButton = createControlButton();
        }
        initContainerObserver();
        setInterval(checkTargetUpdate, config.CHECK_INTERVAL);
    }

    setTimeout(mainInit, 500);
})();