Greasy Fork

Greasy Fork is available in English.

ChatGPT长对话加速

通过智能隐藏旧对话节点,显著降低 ChatGPT 长对话页面的内存占用与渲染卡顿。提供“加速”开关,开启后仅保留最近的对话内容,关闭后瞬间恢复全部历史。安全无损,不修改网页内容。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         ChatGPT长对话加速
// @namespace    npm/chatgpt-conversation-speedup
// @version      1.0
// @description  通过智能隐藏旧对话节点,显著降低 ChatGPT 长对话页面的内存占用与渲染卡顿。提供“加速”开关,开启后仅保留最近的对话内容,关闭后瞬间恢复全部历史。安全无损,不修改网页内容。
// @match        https://chatgpt.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  /**
   * Configuration Constants
   * 可动态修改的状态参数
   */
  let KEEP_VISIBLE = 8;
  let HIDE_BEYOND = 10;
  let IS_OPTIMIZED = true; // 默认开启优化
  const BOOT_CHECK_INTERVAL = 500;

  /**
   * LocalStorage Key
   * UI 状态持久化键名
   */
  const UI_STATE_KEY = 'cgpt_pruner_ui_state_v2';


  /**
   * Get Conversation Turns
   * 获取当前页面所有的对话节点DOM
   */
  function getTurns() {
    return Array.from(
      document.querySelectorAll('article[data-testid^="conversation-turn"]')
    );
  }

  /**
   * Prune / Optimize Logic
   * 核心优化逻辑:通过 display:none 隐藏非活跃区的对话节点
   */
  function prune() {
    const turns = getTurns();
    const total = turns.length;

    // 如果未开启优化,或者数量未达到阈值,则全部显示
    if (!IS_OPTIMIZED || total <= HIDE_BEYOND) {
      turns.forEach(el => {
        if (el.style.display === 'none') el.style.display = '';
      });
      return;
    }

    // 开启优化:隐藏旧消息
    const hideBefore = total - KEEP_VISIBLE;

    for (let i = 0; i < total; i++) {
      const el = turns[i];
      if (i < hideBefore) {
        if (el.style.display !== 'none') el.style.display = 'none';
      } else {
        if (el.style.display === 'none') el.style.display = '';
      }
    }
  }

  /**
   * Start Observer
   * 启动 DOM 监听,当新消息出现时触发优化
   */
  function startObserver() {
    const observer = new MutationObserver(prune);
    observer.observe(document.body, { childList: true, subtree: true });
    console.log('[ChatGPT Speedup] Observer started');
  }

  /**
   * Wait for Chat Load
   * 等待对话加载完成后初始化
   */
  function waitForChat() {
    const timer = setInterval(() => {
      if (getTurns().length > 0) {
        clearInterval(timer);
        prune();
        startObserver();
      }
    }, BOOT_CHECK_INTERVAL);
  }

  /**
   * Create Settings Panel
   * 创建配置面板 (Shadow DOM isolated)
   */
  function createPanel() {
    const host = document.createElement('div');
    host.style.position = 'fixed';
    host.style.bottom = '20px';
    host.style.right = '20px';
    host.style.zIndex = '10000'; // Ensure visibility
    document.body.appendChild(host);

    const shadow = host.attachShadow({ mode: 'open' });

    shadow.innerHTML = `
      <style>
        * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif; }
        .panel, .mini {
          background: #ffffff;
          color: #111827;
          border-radius: 10px;
          box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
          border: 1px solid rgba(0, 0, 0, 0.08);
        }
        .panel {
          width: 190px;
          padding: 0;
          font-size: 12px;
          overflow: hidden;
          display: flex;
          flex-direction: column;
        }
        
        /* Highlighted Switch Row */
        .switch-row {
          background: #f0fdf4; /* Very light green */
          border-bottom: 1px solid #dcfce7;
          padding: 10px 12px;
          display: flex;
          align-items: center;
          justify-content: space-between;
        }
        
        .iconBtn {
          width: 20px; height: 20px;
          display: inline-flex;
          align-items: center;
          justify-content: center;
          border-radius: 4px;
          transition: all 0.2s;
          color: #10a37f;
          cursor: pointer;
          user-select: none;
          font-size: 14px;
          margin-left: 8px;
        }
        .iconBtn:hover { background: rgba(16, 163, 127, 0.1); }
        
        .settings-body { padding: 12px; }

        .row { margin-bottom: 10px; display: flex; align-items: center; justify-content: space-between; }
        .row:last-child { margin-bottom: 0; }
        
        .label { color: #4b5563; font-weight: 500; font-size: 12px; }
        .main-label { color: #065f46; font-weight: 600; font-size: 12px; }
        
        input[type="number"] {
          width: 44px;
          background: #fff;
          color: #111827;
          border: 1px solid #e5e7eb;
          border-radius: 4px;
          padding: 2px 0;
          outline: none;
          text-align: center;
          font-size: 12px;
          transition: all 0.2s;
        }
        input[type="number"]:focus { border-color: #10a37f; box-shadow: 0 0 0 2px rgba(16, 163, 127, 0.1); }

        /* Toggle Switch */
        .switch {
            position: relative;
            display: inline-block;
            width: 30px;
            height: 16px;
        }
        .switch input { opacity: 0; width: 0; height: 0; }
        .slider {
            position: absolute;
            cursor: pointer;
            top: 0; left: 0; right: 0; bottom: 0;
            background-color: #9ca3af;
            transition: .3s;
            border-radius: 16px;
        }
        .slider:before {
            position: absolute;
            content: "";
            height: 12px;
            width: 12px;
            left: 2px;
            bottom: 2px;
            background-color: white;
            transition: .3s;
            border-radius: 50%;
            box-shadow: 0 1px 2px rgba(0,0,0,0.1);
        }
        input:checked + .slider { background-color: #10a37f; }
        input:checked + .slider:before { transform: translateX(14px); }
        
        .controls-group { display: flex; align-items: center; }

        button.apply { display: none; }
        .hint { display: none; }

        .mini {
          width: 32px;
          height: 32px;
          display: flex;
          align-items: center;
          justify-content: center;
          cursor: pointer;
          user-select: none;
          transition: transform 0.2s;
          border-radius: 8px;
        }
        .mini:hover { transform: scale(1.05); }
        .miniDot {
          width: 10px;
          height: 10px;
          border-radius: 50%;
          background: #10a37f;
          box-shadow: 0 0 0 2px rgba(16, 163, 127, 0.2);
        }
        .miniDot.off { background: #9ca3af; box-shadow: none; }

        .hidden { display: none !important; }
      </style>

      <div class="panel" id="panel">
        <div class="switch-row">
            <span class="main-label">加速开关</span>
            <div class="controls-group">
                <label class="switch">
                    <input type="checkbox" id="masterToggle">
                    <span class="slider"></span>
                </label>
                <div class="iconBtn" id="minBtn" title="最小化">−</div>
            </div>
        </div>

        <div class="settings-body">
          <div class="row">
            <span class="label">保留最近</span>
            <input type="number" id="keepVisible" min="1">
          </div>

          <div class="row">
            <span class="label">优化阈值</span>
            <input type="number" id="hideBeyond" min="1">
          </div>
        </div>
      </div>

      <div class="mini hidden" id="mini" title="点击展开设置">
        <div class="miniDot" id="miniDot"></div>
      </div>
    `;

    const $ = (id) => shadow.getElementById(id);

    function setMinimized(minimized) {
      $('panel').classList.toggle('hidden', minimized);
      $('mini').classList.toggle('hidden', !minimized);
      updateMiniDot();
      saveState();
    }

    function updateMiniDot() {
      if (IS_OPTIMIZED) $('miniDot').classList.remove('off');
      else $('miniDot').classList.add('off');
    }

    function saveState() {
      const state = {
        minimized: $('panel').classList.contains('hidden'),
        optimized: IS_OPTIMIZED,
        keep: KEEP_VISIBLE,
        threshold: HIDE_BEYOND
      };
      localStorage.setItem(UI_STATE_KEY, JSON.stringify(state));
    }

    function loadState() {
      try {
        const raw = localStorage.getItem(UI_STATE_KEY);
        if (raw) {
          const state = JSON.parse(raw);
          setMinimized(state.minimized);
          IS_OPTIMIZED = state.optimized;
          KEEP_VISIBLE = state.keep;
          HIDE_BEYOND = state.threshold;
        } else {
          setMinimized(false); // Default open first time
        }
      } catch (e) { console.error('Load state failed', e); }
    }

    // Apply Logic
    function applySettings() {
      IS_OPTIMIZED = $('masterToggle').checked;
      KEEP_VISIBLE = parseInt($('keepVisible').value, 10) || 8;
      HIDE_BEYOND = parseInt($('hideBeyond').value, 10) || 10;

      prune();
      updateMiniDot();
      saveState();
    }

    // Events
    $('masterToggle').onchange = applySettings; // Instant toggle
    $('keepVisible').onchange = applySettings;  // Auto apply
    $('hideBeyond').onchange = applySettings;   // Auto apply
    $('minBtn').onclick = () => setMinimized(true);
    $('mini').onclick = () => setMinimized(false);

    // Init
    loadState();

    // Sync UI with loaded state
    $('masterToggle').checked = IS_OPTIMIZED;
    $('keepVisible').value = KEEP_VISIBLE;
    $('hideBeyond').value = HIDE_BEYOND;
    updateMiniDot();

    // Initial Prune
    prune();
  }

  waitForChat();
  createPanel();
})();