chatgpt对话时间长了特别卡顿。
油猴脚本直接粘进去就能使用

// ==UserScript==// @name         ChatGPT 用户消息真·折叠(50字预览,按钮在“已折叠”前)// @namespace    https://example.com/tm// @version      1.2.1// @description  折叠“您说:”的长用户消息:从DOM摘除原内容,仅保留前50字预览;顶部显示“[展开] 已折叠”;展开时再还原// @author       you// @match        https://chat.openai.com/*// @match        https://chatgpt.com/*// @run-at       document-end// @grant        none// ==/UserScript==(function () {  'use strict';  const TITLE_TEXT = '您说:';   // h5 的定位文本  const THRESHOLD = 50;          // 触发折叠的字数阈值  const PREVIEW_CHARS = 50;      // 预览字符数  const ARTICLE_FLAG = 'data-yc-fold-processed';  // key: div.whitespace-pre-wrap -> value: { frag, bar, label, preview }  const store = new WeakMap();  injectStyle();  function injectStyle() {    if (document.getElementById('yc-fold-style-v3')) return;    const s = document.createElement('style');    s.id = 'yc-fold-style-v3';    s.textContent = `      .yc-toggle-btn {        cursor: pointer; user-select: none;        font-size: 12px; padding: 2px 6px; border-radius: 6px;        border: 1px solid rgba(0,0,0,.15); background: rgba(0,0,0,.04);        margin-right: 8px;      }      .yc-toggle-btn:hover { background: rgba(0,0,0,.08); }      .yc-bar {        display: flex; align-items: center; gap: 6px;        font-size: 12px; opacity: .8; margin-bottom: 6px;      }      .yc-preview {        max-height: 4.2em; /* 2~3行 */        overflow: hidden; position: relative; line-height: 1.4;        white-space: pre-wrap;      }      .yc-preview::after {        content: ""; position: absolute; left:0; right:0; bottom:0; height:2em;        pointer-events:none; background: linear-gradient(to bottom, rgba(255,255,255,0), var(--bg, #fff));      }      @media (prefers-color-scheme: dark) {        .yc-preview::after { background: linear-gradient(to bottom, rgba(0,0,0,0), var(--bg, #000)); }        .yc-toggle-btn { border-color: rgba(255,255,255,.2); background: rgba(255,255,255,.06); }        .yc-toggle-btn:hover { background: rgba(255,255,255,.12); }      }    `;    document.head.appendChild(s);  }  function getTextLen(article) {    const nodes = article.querySelectorAll('div.whitespace-pre-wrap');    let len = 0;    nodes.forEach(n => len += (n.innerText || '').trim().length);    return { len, nodes };  }  function makePreview(text) {    const preview = document.createElement('div');    preview.className = 'yc-preview';    const t = (text || '').trim();    const snippet = t.length > PREVIEW_CHARS ? (t.slice(0, PREVIEW_CHARS) + '…') : t;    preview.textContent = snippet || '(空内容)';    return preview;  }  function ensureBar(containerDiv) {    // 顶部控制条:[按钮] 已折叠    let bar = containerDiv.querySelector(':scope > .yc-bar');    if (!bar) {      bar = document.createElement('div');      bar.className = 'yc-bar';      const btn = document.createElement('button');      btn.className = 'yc-toggle-btn';      btn.type = 'button';      btn.textContent = '展开';      const label = document.createElement('span');      label.textContent = '已折叠';      bar.appendChild(btn);      bar.appendChild(label);      containerDiv.insertBefore(bar, containerDiv.firstChild);      btn.addEventListener('click', () => {        if (store.has(containerDiv)) {          // 处于折叠 => 展开          expand(containerDiv);          btn.textContent = '收起';          label.textContent = ''; // 展开后不显示“已折叠”        } else {          // 处于展开 => 折叠          collapse(containerDiv);          btn.textContent = '展开';          label.textContent = '已折叠';        }      });    }    return bar;  }  // —— 辅助:从一组节点提取纯文本(不含按钮区)——  function textFromNodes(nodes) {    let s = '';    for (const n of nodes) {      s += (n.innerText ?? n.textContent ?? '');    }    return s.trim();  }  // 折叠:移出原内容,仅留顶部条与 50 字预览(不包含按钮文字)  function collapse(containerDiv) {    if (store.has(containerDiv)) return;    const frag = document.createDocumentFragment();    // 先确保有 bar,再把 bar 以外的内容移动到 frag    const bar = ensureBar(containerDiv);    const keep = new Set([bar]);                      // bar 留在 DOM    const toMove = Array.from(containerDiv.childNodes).filter(n => !keep.has(n));    // 预览文本只来自将要被移走的内容    const previewText = textFromNodes(toMove);    // 真正移走内容    toMove.forEach(n => frag.appendChild(n));    // 创建 50 字预览并挂上    const preview = makePreview(previewText);    containerDiv.appendChild(preview);    // 记录状态    store.set(containerDiv, { frag, bar, label: bar.querySelector('span'), preview });    // 更新 bar 文案    const btn = bar.querySelector('button');    const label = bar.querySelector('span');    btn.textContent = '展开';    label.textContent = '已折叠';  } // ←←← 关键:补上 collapse 的收尾大括号  // 展开:还原原内容,移除预览  function expand(containerDiv) {    const rec = store.get(containerDiv);    if (!rec) return;    const { frag, preview } = rec;    // 移除预览    preview?.remove();    // 还原原内容(追加在 bar 后面)    containerDiv.appendChild(frag);    store.delete(containerDiv);    // 更新 bar 文案    const bar = ensureBar(containerDiv);    const btn = bar.querySelector('button');    const label = bar.querySelector('span');    btn.textContent = '收起';    label.textContent = '';  }  function processArticle(article) {    if (article.hasAttribute(ARTICLE_FLAG)) return;    const h5 = article.querySelector('h5');    if (!h5) return;    if ((h5.textContent || '').trim() !== TITLE_TEXT) return;    const { len, nodes } = getTextLen(article);    if (!nodes.length) return;    const target = nodes[0];    const shouldCollapse = len > THRESHOLD;    // 为容器添加 bar(按钮始终在内容顶部;短消息隐藏按钮)    const bar = ensureBar(target);    const btn = bar.querySelector('button');    const label = bar.querySelector('span');    if (shouldCollapse) {      collapse(target); // 会设置为“展开 / 已折叠”      btn.style.display = 'inline-block';    } else {      // 短内容:不折叠、不显示按钮与“已折叠”      if (store.has(target)) expand(target);      btn.style.display = 'none';      label.textContent = '';    }    article.setAttribute(ARTICLE_FLAG, '1');  }  function scan() {    document.querySelectorAll('article').forEach(processArticle);  }  const mo = new MutationObserver(muts => {    for (const m of muts) {      for (const n of m.addedNodes) {        if (!(n instanceof HTMLElement)) continue;        if (n.tagName === 'ARTICLE') {          processArticle(n);        } else {          const arts = n.querySelectorAll?.('article');          arts && arts.forEach(processArticle);        }      }    }  });  function init() {    scan();    mo.observe(document.body, { childList: true, subtree: true });  }  if (document.readyState === 'loading') {    document.addEventListener('DOMContentLoaded', init);  } else {    init();  }})();