Mr._K,_The_Creator / Better ChatGPT Assistant(更好的ChatGPT网页版多功能增强小助手)

// ==UserScript==
// @name         Better ChatGPT Assistant(更好的ChatGPT网页版多功能增强小助手)
// @namespace    https://github.com/3150214587/chatgpt-virtual-scrollGPT-
// @version      8.2.3.5
// @description  Better ChatGPT Assistant powered by Virtual Scroll Engine 6.0 — ultra-smooth long chats, export, token monitor, i18n, and more.
// @description:zh-CN  GPT最强、极致稳定多功能小助手:长对话虚拟化 + 顶栏极简状态灯(绿/黄/红) + 面板(iOS三段模式/暂停/强制优化/新对话/帮助) + 输入淡出 + Ctrl+F + Resize + Markdown导出(UTF-8 BOM) + 折叠代码 + Token估算 + 中英切换
// @license      MIT
// @homepageURL  https://github.com/3150214587/chatgpt-virtual-scrollGPT-
// @supportURL   https://github.com/3150214587/chatgpt-virtual-scrollGPT-/issues
// @contributionURL  https://paypal.me/DKW3588
// @match        https://chat.openai.com/*
// @match        https://chatgpt.com/*
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  /******************************************************************
   * 合并版目标
   * - 4.0 内核:虚拟化、健康灯、面板、钉住、跟随、Ctrl+F、Resize 等
   * - 4.1 功能:导出 Markdown(UTF-8 BOM)、收展代码、Token估算、PayPal、双语切换
   * - 5.0 体验:UI 紧凑化、“强制清理”改为“强制优化”(不影响聊天记录)
   ******************************************************************/

  // ========================== 可调参数(一般不用动) ==========================
  const CHECK_INTERVAL_MS = 1100;
  const ROUTE_GUARD_MS = 800;
  const INPUT_DIM_IDLE_MS = 850;
  const IMAGE_LOAD_RETRY_MS = 250;
  const POS_FOLLOW_MS = 450;
  const POS_FOLLOW_WHEN_OPEN_MS = 250;

  const MODE_TO_MARGIN_SCREENS = {
    performance: 1,
    balanced: 2,
    conservative: 3
  };

  const MEM_STABLE_MB = 220;
  const MEM_WARNING_MB = 520;

  const DOM_OK = 7000;
  const DOM_WARN = 15000;

  // 强制优化:保留的屏幕范围(越小越激进)
  const FORCE_CLEAN_MARGIN_SCREENS = 0.4;

  // ========================== 作者/项目/赞赏 ==========================
  const AUTHOR_GITHUB = 'https://github.com/3150214587';
  const PROJECT_GITHUB = 'https://github.com/3150214587/chatgpt-virtual-scrollGPT-';
  const DONATE_QR_PAGE = 'https://github.com/3150214587/chatgpt-virtual-scrollGPT-/blob/main/donate-wechat.png';

  // ========================== Feature Pack(导出/折叠/Token/语言/PayPal) ==========================
  const LANG_KEY = 'vs_lang';
  let lang = localStorage.getItem(LANG_KEY) || 'zh';

  //  PayPal
  const PAYPAL_URL = 'https://paypal.me/DKW3588';

  const I18N = {
    zh: {
      export: '导出聊天记录',
      fold: '收展代码',
      token: 'Token 估算',
      paypal: 'PayPal 支持',
      lang: 'EN',
      optimize: '强制优化',
      optimizeTip: '立即降低网页负载,不影响聊天内容',
      newChat: '新开对话',
      help: '帮助',
      health: '健康'
    },
    en: {
      export: 'Export Chat Log',
      fold: 'Fold Code',
      token: 'Token Estimate',
      paypal: 'Support via PayPal',
      lang: '中文',
      optimize: 'Optimize Now',
      optimizeTip: 'Reduce page load now, chat content stays safe',
      newChat: 'New chat',
      help: 'Help',
      health: 'Healthy'
    }
  };

  function t(k) {
    return (I18N[lang] && I18N[lang][k]) ? I18N[lang][k] : k;
  }

  // ========================== 持久化 Key ==========================
  const KEY_MODE = 'cgpt_vs_mode';
  const KEY_ENABLED = 'cgpt_vs_enabled';
  const KEY_PINNED = 'cgpt_vs_pinned';
  const KEY_POS = 'cgpt_vs_pos';
  const KEY_MINIMAL = 'cgpt_vs_minimal';
  const KEY_EDGE_SNAP = 'cgpt_vs_edge_snap';
  const KEY_LAST_OPEN = 'cgpt_vs_open';

  // ========================== DOM IDs ==========================
  const STYLE_ID = 'cgpt-vs-style';
  const ROOT_ID = 'cgpt-vs-root';
  const DOT_ID = 'cgpt-vs-dot';
  const BTN_ID = 'cgpt-vs-btn';
  const PANEL_ID = 'cgpt-vs-panel';
  const HELP_ID = 'cgpt-vs-help';
  const ABOUT_DONATE_BTN_ID = 'cgpt-vs-donateBtn';

  // Feature Pack 容器
  const FP_ID = 'cgpt-vs-featurepack';

  // ========================== 状态 ==========================
  let currentMode = loadMode();
  let virtualizationEnabled = loadBool(KEY_ENABLED, true);
  let minimalMode = loadBool(KEY_MINIMAL, true);
  let edgeSnap = loadBool(KEY_EDGE_SNAP, true);
  let pinned = loadBool(KEY_PINNED, false);
  let wasOpen = loadBool(KEY_LAST_OPEN, false);

  let ctrlFFreeze = false;
  let typingDimTimer = null;
  let lastInputAt = 0;

  let rafPending = false;
  let lastVirtualizedCount = 0;
  let lastTurnsCount = 0;

  let followTimer = null;
  let pinnedPos = loadPos();

  // ========================== 工具函数 ==========================
  function loadBool(key, def) {
    const v = localStorage.getItem(key);
    if (v === null || v === undefined) return def;
    return v === '1';
  }

  function saveBool(key, val) {
    localStorage.setItem(key, val ? '1' : '0');
  }

  function loadMode() {
    const v = localStorage.getItem(KEY_MODE);
    return (v === 'performance' || v === 'balanced' || v === 'conservative') ? v : 'balanced';
  }

  function saveMode(mode) {
    currentMode = mode;
    localStorage.setItem(KEY_MODE, mode);
  }

  function loadPos() {
    try {
      const raw = localStorage.getItem(KEY_POS);
      if (!raw) return {
        x: 18,
        y: 64,
        side: 'left',
        hidden: false
      };
      const p = JSON.parse(raw);
      if (typeof p.x === 'number' && typeof p.y === 'number') {
        return {
          x: clamp(p.x, 0, window.innerWidth - 40),
          y: clamp(p.y, 0, window.innerHeight - 40),
          side: p.side === 'right' ? 'right' : 'left',
          hidden: !!p.hidden
        };
      }
    }
    catch {}
    return {
      x: 18,
      y: 64,
      side: 'left',
      hidden: false
    };
  }

  function savePos() {
    localStorage.setItem(KEY_POS, JSON.stringify(pinnedPos));
  }

  function clamp(n, min, max) {
    return Math.max(min, Math.min(max, n));
  }

  function getMarginScreens() {
    return MODE_TO_MARGIN_SCREENS[currentMode] ?? MODE_TO_MARGIN_SCREENS.balanced;
  }

  function getUsedHeapMB() {
    const p = window.performance;
    if (!p || !p.memory || !p.memory.usedJSHeapSize) return null;
    return p.memory.usedJSHeapSize / (1024 * 1024);
  }

  function memoryLevel(usedMB) {
    if (usedMB == null) return {
      label: lang === 'zh' ? '不可用' : 'N/A',
      level: 'na'
    };
    if (usedMB < MEM_STABLE_MB) return {
      label: `${usedMB.toFixed(0)}MB${lang === 'zh' ? '(稳定流畅)' : ' (OK)'}`,
      level: 'ok'
    };
    if (usedMB < MEM_WARNING_MB) return {
      label: `${usedMB.toFixed(0)}MB${lang === 'zh' ? '(偏高微卡)' : ' (High)'}`,
      level: 'warn'
    };
    return {
      label: `${usedMB.toFixed(0)}MB${lang === 'zh' ? '(卡死警告)' : ' (Warn)'}`,
      level: 'bad'
    };
  }

  function domLevel(domNodes) {
    if (domNodes < DOM_OK) return {
      label: `${domNodes}`,
      level: 'ok'
    };
    if (domNodes < DOM_WARN) return {
      label: `${domNodes}`,
      level: 'warn'
    };
    return {
      label: `${domNodes}`,
      level: 'bad'
    };
  }

  function estimateRemainingTurns(usedMB, turns) {
    if (usedMB == null || !turns || turns < 12) return null;
    const avg = usedMB / turns;
    if (!isFinite(avg) || avg <= 0) return null;
    const headroom = MEM_WARNING_MB - usedMB;
    const remaining = Math.floor(headroom / avg);
    return clamp(remaining, 0, 9999);
  }

  function modeLabel(mode) {
    if (lang === 'en') {
      if (mode === 'performance') return 'performance';
      if (mode === 'balanced') return 'balanced';
      if (mode === 'conservative') return 'conservative';
      return 'Balanced';
    }
    if (mode === 'conservative') return '保守c';
    if (mode === 'balanced') return '平衡b';
    if (mode === 'performance') return '性能a';
    return '平衡';
  }

  function suggestionText(domNodes, usedMB, virtCount, turns) {
    const mem = memoryLevel(usedMB).level;
    const dom = domLevel(domNodes).level;

    if (!virtualizationEnabled) {
      return lang === 'zh' ?
        '建议:你已暂停虚拟化,可使用迁移助手导出完整聊天记录,对话会更“完整可见”,但长对话更容易卡顿。需要顺滑时点“启用”。' :
        'Tip: Virtualization is paused. Full history is visible, but long chats may lag. Enable it for smooth scrolling.';
    }

    if (ctrlFFreeze) {
      return lang === 'zh' ?
        '建议:你正在使用浏览器搜索(Ctrl+F),已自动暂停虚拟化以保证能搜到所有历史。结束搜索后会自动恢复。' :
        'Tip: Browser Find (Ctrl+F) is active. Virtualization is paused so you can search all history. It will resume after you exit Find.';
    }

    if (mem === 'bad' || dom === 'bad') {
      return lang === 'zh' ?
        '建议:已进入卡顿区。先点“强制优化”降负载;重要内容请先“使用我小红书坠售卖的迁移助手导出/备份”,再考虑刷新或新开对话。' :
        'Tip: Near lag zone. Click “Optimize Now” to reduce load. Export/backup important content before refreshing or starting a new chat.';
    }
    if (mem === 'warn' || dom === 'warn') {
      return lang === 'zh' ?
        '建议:状态偏高但可继续聊。尽量别一次滚很久历史;要翻旧内容可临时切到“保守”。' :
        'Tip: Load is higher but still OK. Avoid long scroll sessions. Switch to “Conservative” when browsing old history.';
    }
    if (virtCount > 0 && turns > 220) {
      return lang === 'zh' ?
        '建议:状态良好。找旧内容尽量用搜索或导出查看,避免反复拉到最底。' :
        'Tip: Healthy. Use search or export to view old history, instead of repeatedly scrolling to the bottom.';
    }
    return lang === 'zh' ? '建议:状态良好。' : 'Tip: Healthy.';
  }

  // ========================== 选择器:消息节点 ==========================
  function getMessageNodes() {
    let nodes = document.querySelectorAll('div[data-message-id]');
    if (nodes && nodes.length) return Array.from(nodes);

    nodes = document.querySelectorAll('[data-testid="conversation-turn"]');
    if (nodes && nodes.length) return Array.from(nodes);

    const main = document.querySelector('main');
    if (!main) return [];
    nodes = main.querySelectorAll('div[role="presentation"]');
    return nodes && nodes.length ? Array.from(nodes) : [];
  }

  // ========================== 虚拟化:恢复/清理 ==========================
  function unvirtualizeAll() {
    const msgs = getMessageNodes();
    for (const msg of msgs) {
      if (msg.dataset.vsSlimmed) {
        msg.innerHTML = msg.dataset.vsBackup || msg.innerHTML;
        delete msg.dataset.vsSlimmed;
        delete msg.dataset.vsBackup;
        delete msg.dataset.vsH;
      }
    }
  }

  function virtualizeOnce(marginScreensOverride) {
    if (!virtualizationEnabled || ctrlFFreeze) {
      lastVirtualizedCount = 0;
      lastTurnsCount = getMessageNodes().length || 0;
      return;
    }

    const marginScreens = (typeof marginScreensOverride === 'number') ?
      marginScreensOverride :
      getMarginScreens();

    const msgs = getMessageNodes();
    lastTurnsCount = msgs.length;

    const viewportTop = window.scrollY;
    const viewportBottom = viewportTop + window.innerHeight;

    const keepTop = viewportTop - window.innerHeight * marginScreens;
    const keepBottom = viewportBottom + window.innerHeight * marginScreens;

    let slimmedCount = 0;

    for (const msg of msgs) {
      const rect = msg.getBoundingClientRect();
      const top = rect.top + window.scrollY;
      const bottom = top + rect.height;
      const shouldKeep = bottom > keepTop && top < keepBottom;

      if (!shouldKeep) {
        if (!msg.dataset.vsSlimmed) {
          msg.dataset.vsSlimmed = '1';
          msg.dataset.vsBackup = msg.innerHTML;

          const h = Math.max(24, Math.round(rect.height));
          msg.dataset.vsH = String(h);
          msg.innerHTML = `<div class="cgpt-vs-ph" style="height:${h}px"></div>`;
        }
        else {
          const oldH = Number(msg.dataset.vsH || 0);
          const newH = Math.max(24, Math.round(rect.height));
          if (oldH && Math.abs(newH - oldH) > 180) {
            msg.dataset.vsH = String(newH);
            const ph = msg.querySelector('.cgpt-vs-ph');
            if (ph) ph.style.height = `${newH}px`;
          }
        }
        slimmedCount += 1;
      }
      else {
        if (msg.dataset.vsSlimmed) {
          msg.innerHTML = msg.dataset.vsBackup || msg.innerHTML;
          delete msg.dataset.vsSlimmed;
          delete msg.dataset.vsBackup;
          delete msg.dataset.vsH;
        }
      }
    }

    lastVirtualizedCount = slimmedCount;
  }

  function scheduleVirtualize(marginOverride) {
    if (rafPending) return;
    rafPending = true;
    requestAnimationFrame(() => {
      rafPending = false;
      virtualizeOnce(marginOverride);
      updateUI();
    });
  }

  // ========================== Ctrl+F 兼容 ==========================
  function enableCtrlFFreeze() {
    if (ctrlFFreeze) return;
    ctrlFFreeze = true;
    unvirtualizeAll();
    updateUI();
  }

  function disableCtrlFFreeze() {
    if (!ctrlFFreeze) return;
    ctrlFFreeze = false;
    scheduleVirtualize();
  }

  function installFindGuards() {
    window.addEventListener('keydown', (e) => {
      const isFind = ((e.ctrlKey || e.metaKey) && (e.key === 'f' || e.key === 'F'));
      if (isFind) enableCtrlFFreeze();
      if (e.key === 'Escape') setTimeout(() => disableCtrlFFreeze(), 120);
    }, true);
  }

  // ========================== 输入淡出 ==========================
  function installTypingDim() {
    const dim = () => {
      lastInputAt = Date.now();
      const root = ensureRoot();
      root.classList.add('dim');
      if (typingDimTimer) clearTimeout(typingDimTimer);
      typingDimTimer = setTimeout(() => {
        const idle = Date.now() - lastInputAt;
        if (idle >= INPUT_DIM_IDLE_MS) root.classList.remove('dim');
      }, INPUT_DIM_IDLE_MS + 20);
    };

    document.addEventListener('input', (e) => {
      if (!e || !e.target) return;
      const el = e.target;
      const tag = (el.tagName || '').toLowerCase();
      if (tag === 'textarea' || tag === 'input') dim();
    }, true);

    document.addEventListener('focusin', (e) => {
      const el = e.target;
      if (!el) return;
      const tag = (el.tagName || '').toLowerCase();
      if (tag === 'textarea' || tag === 'input') dim();
    }, true);

    document.addEventListener('focusout', () => {
      setTimeout(() => {
        const root = ensureRoot();
        root.classList.remove('dim');
      }, 220);
    }, true);
  }

  // ========================== 图片加载后补一次 ==========================
  function installImageLoadHook() {
    window.addEventListener('load', (e) => {
      const t = e && e.target;
      if (t && t.tagName && t.tagName.toLowerCase() === 'img') {
        setTimeout(() => scheduleVirtualize(), IMAGE_LOAD_RETRY_MS);
      }
    }, true);
  }

  // ========================== Resize 修复 ==========================
  function installResizeFix() {
    window.addEventListener('resize', () => {
      unvirtualizeAll();
      requestAnimationFrame(() => scheduleVirtualize());
    }, {
      passive: true
    });
  }

  // ========================== 跟随“模型切换按钮”定位 ==========================
  function findModelButton() {
    const header = document.querySelector('header');
    if (!header) return null;

    const btns = header.querySelectorAll('button, [role="button"]');
    const candidates = [];
    for (const b of btns) {
      const txt = ((b.innerText || b.textContent || '')).trim();
      if (!txt) continue;

      const hit =
        /chatgpt/i.test(txt) ||
        /\bgpt\b/i.test(txt) ||
        txt.includes('模型') ||
        txt.includes('切换') ||
        txt.includes('ChatGPT');

      if (hit) candidates.push(b);
    }

    if (!candidates.length) return null;
    let best = candidates[0];
    let bestScore = Infinity;
    for (const c of candidates) {
      const r = c.getBoundingClientRect();
      const score = (r.top * 10) + r.left;
      if (score < bestScore) {
        bestScore = score;
        best = c;
      }
    }
    return best;
  }

  function positionNearModelButton() {
    const root = ensureRoot();
    if (pinned) return;

    const btn = findModelButton();
    if (!btn) {
      root.style.left = '12px';
      root.style.top = '10px';
      root.style.right = 'auto';
      root.style.bottom = 'auto';
      root.classList.add('fallback');
      return;
    }

    const r = btn.getBoundingClientRect();
    const x = Math.round(r.left + r.width + 10);
    const y = Math.round(r.top + (r.height - 28) / 2);

    root.style.left = `${clamp(x, 6, window.innerWidth - 360)}px`;
    root.style.top = `${clamp(y, 6, window.innerHeight - 60)}px`;
    root.style.right = 'auto';
    root.style.bottom = 'auto';

    root.classList.remove('fallback');
  }

  function startFollowPositionLoop() {
    stopFollowPositionLoop();
    const tick = () => {
      const root = ensureRoot();
      const open = root.classList.contains('open');
      positionNearModelButton();
      followTimer = setTimeout(tick, open ? POS_FOLLOW_WHEN_OPEN_MS : POS_FOLLOW_MS);
    };
    tick();
  }

  function stopFollowPositionLoop() {
    if (followTimer) clearTimeout(followTimer);
    followTimer = null;
  }

  // ========================== Feature Pack:导出/折叠/Token ==========================
  function estimateTokens() {
    let text = '';
    getMessageNodes().forEach(m => {
      text += (m.innerText || '');
    });
    return Math.round(text.length / 4);
  }

  function exportChatMarkdown() {
    let md = '# ChatGPT Chat Log\n\n';
    getMessageNodes().forEach(m => {
      const chunk = (m.innerText || '').trim();
      if (chunk) md += chunk + '\n\n---\n\n';
    });

    const bom = '';
    const blob = new Blob([bom + md], {
      type: 'text/markdown;charset=utf-8'
    });

    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = 'chatgpt-chat.md';
    a.click();
  }

  let folded = false;

  function toggleCode() {
    document.querySelectorAll('pre').forEach(p => p.style.display = folded ? '' : 'none');
    folded = !folded;
  }

  function toggleLang() {
    lang = (lang === 'zh') ? 'en' : 'zh';
    localStorage.setItem(LANG_KEY, lang);
    // 不强制 reload:直接刷新文案
    updateUI();
    renderFeaturePack(true);
  }

  // ========================== UI:注入样式 ==========================
  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    const style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = `
      #${ROOT_ID}{
        position: fixed;
        z-index: 2147483647;
        font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
        user-select: none;
        -webkit-user-select: none;
        transform: translateZ(0);
        opacity: 1;
        transition: opacity 160ms ease;
      }
      #${ROOT_ID}.dim{ opacity: 0.2; }
      #${ROOT_ID}.fallback{ filter: saturate(1.02); }

      #${BTN_ID}{
        display: inline-flex;
        align-items: center;
        gap: 8px;
        height: 28px;
        padding: 0 10px;
        border-radius: 999px;
        border: 1px solid rgba(0,0,0,0.12);
        background: rgba(255,255,255,0.78);
        backdrop-filter: blur(12px);
        -webkit-backdrop-filter: blur(12px);
        box-shadow: 0 6px 18px rgba(0,0,0,0.10);
        cursor: pointer;
        font-size: 12px;
        color: rgba(0,0,0,0.78);
      }
      #${BTN_ID}:hover{ background: rgba(255,255,255,0.92); }
      #${ROOT_ID}.minimal #cgpt-vs-miniText{ display:none; }
      #cgpt-vs-miniText{
        max-width: 220px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        opacity: 0.9;
      }

      #${DOT_ID}{
        width: 9px;
        height: 9px;
        border-radius: 50%;
        background: #22c55e;
        box-shadow: 0 0 0 3px rgba(34,197,94,0.16);
        transition: transform 140ms ease;
      }
      #${DOT_ID}.warn{
        background: #f59e0b;
        box-shadow: 0 0 0 3px rgba(245,158,11,0.16);
      }
      #${DOT_ID}.bad{
        background: #ef4444;
        box-shadow: 0 0 0 3px rgba(239,68,68,0.16);
      }
      #${DOT_ID}.off{
        background: rgba(0,0,0,0.28);
        box-shadow: 0 0 0 3px rgba(0,0,0,0.08);
      }

      #${PANEL_ID}{
        margin-top: 8px;
        width: 360px;
        max-width: min(420px, calc(100vw - 16px));
        padding: 12px;
        border-radius: 16px;
        border: 1px solid rgba(0,0,0,0.12);
        background: rgba(255,255,255,0.86);
        backdrop-filter: blur(14px);
        -webkit-backdrop-filter: blur(14px);
        box-shadow: 0 14px 40px rgba(0,0,0,0.16);
        display: none;
        color: rgba(0,0,0,0.86);
        font-size: 12px;
        line-height: 1.5;
      }
      #${ROOT_ID}.open #${PANEL_ID}{ display:block; }

      .cgpt-vs-toprow{
        display:flex;
        align-items:center;
        justify-content:space-between;
        gap:10px;
        margin-bottom: 8px;
      }

      .cgpt-vs-seg{
        display:flex;
        align-items:center;
        width: 100%;
        padding: 3px;
        border-radius: 999px;
        background: rgba(0,0,0,0.06);
        border: 1px solid rgba(0,0,0,0.08);
        box-shadow: inset 0 1px 0 rgba(255,255,255,0.7);
      }
      .cgpt-vs-seg button{
        flex:1;
        height: 28px;
        border: 0;
        background: transparent;
        border-radius: 999px;
        cursor: pointer;
        font-size: 12px;
        color: rgba(0,0,0,0.62);
        transition: background 140ms ease, box-shadow 140ms ease, color 140ms ease;
      }
      .cgpt-vs-seg button.active{
        background: rgba(255,255,255,0.92);
        color: rgba(0,0,0,0.86);
        box-shadow: 0 8px 18px rgba(0,0,0,0.10);
      }

      /* ✅ 5.0 UI:控件区允许换行,避免挤爆 */
      .cgpt-vs-controls{
        display:flex;
        align-items:center;
        justify-content:space-between;
        gap:8px;
        margin-top: 10px;
        flex-wrap: wrap;
      }
      .cgpt-vs-chiprow{
        display:flex;
        gap:8px;
        flex-wrap: wrap;
        justify-content:flex-end;
      }
      .cgpt-vs-chip{
        height: 28px;
        padding: 0 10px;
        border-radius: 999px;
        border: 1px solid rgba(0,0,0,0.12);
        background: rgba(255,255,255,0.88);
        cursor: pointer;
        font-size: 12px;
        color: rgba(0,0,0,0.78);
        box-shadow: 0 6px 14px rgba(0,0,0,0.08);
      }
      .cgpt-vs-chip:hover{ background: rgba(255,255,255,0.96); }
      .cgpt-vs-chip.primary{
        border-color: rgba(0,0,0,0.14);
        font-weight: 600;
      }

      .cgpt-vs-row{
        display:flex;
        justify-content:space-between;
        gap: 12px;
        padding: 4px 0;
      }
      .cgpt-vs-k{ color: rgba(0,0,0,0.56); }
      .cgpt-vs-v{ font-variant-numeric: tabular-nums; }

      .mem-ok{ color:#16a34a; font-weight: 600; }
      .mem-warn{ color:#d97706; font-weight: 600; }
      .mem-bad{ color:#dc2626; font-weight: 700; }

      .cgpt-vs-hr{
        height: 1px;
        background: rgba(0,0,0,0.08);
        margin: 10px 0 8px;
      }
      .cgpt-vs-tip{ color: rgba(0,0,0,0.74); }

      /* About / Support block */
      .cgpt-vs-about{
        display:flex;
        align-items:flex-start;
        justify-content:space-between;
        gap:10px;
        padding: 8px 2px 2px;
        flex-wrap: wrap;
      }
      .cgpt-vs-aboutLeft{ min-width: 0; }
      .cgpt-vs-aboutTitle{ font-weight: 800; letter-spacing: 0.1px; }
      .cgpt-vs-aboutSub{ margin-top: 3px; color: rgba(0,0,0,0.62); font-size: 11px; }
      .cgpt-vs-aboutLinks{
        margin-top: 6px;
        display:flex;
        gap:10px;
        flex-wrap: wrap;
        align-items:center;
      }
      .cgpt-vs-link{ color: rgba(37,99,235,0.95); text-decoration:none; font-weight: 600; font-size: 12px; }
      .cgpt-vs-link:hover{ text-decoration: underline; }
      .cgpt-vs-supportHint{ margin-top: 6px; color: rgba(0,0,0,0.66); font-size: 11px; }

      /* ✅ Feature Pack 工具条:紧凑+对齐+不乱 */
      #${FP_ID}{
        margin-top: 8px;
        display:flex;
        align-items:center;
        justify-content:space-between;
        gap:8px;
        flex-wrap: wrap;
        width: 100%;
      }
      #${FP_ID} .fp-left{
        display:flex;
        gap:6px;
        flex-wrap: wrap;
        align-items:center;
      }
      #${FP_ID} .fp-right{
        margin-left:auto;
        display:flex;
        gap:6px;
        flex-wrap: wrap;
        align-items:center;
      }
      #${FP_ID} .fp-token{
        font-size: 12px;
        color: rgba(0,0,0,0.66);
        padding: 0 2px;
      }

      #${HELP_ID}{
        position: fixed;
        inset: 0;
        background: rgba(0,0,0,0.30);
        display:none;
        align-items:center;
        justify-content:center;
        z-index: 2147483647;
      }
      #${HELP_ID}.show{ display:flex; }
      .cgpt-vs-helpCard{
        width: min(720px, calc(100vw - 20px));
        max-height: min(78vh, 680px);
        overflow:auto;
        padding: 16px 16px;
        border-radius: 18px;
        border: 1px solid rgba(0,0,0,0.14);
        background: rgba(255,255,255,0.92);
        backdrop-filter: blur(14px);
        -webkit-backdrop-filter: blur(14px);
        box-shadow: 0 18px 60px rgba(0,0,0,0.24);
        color: rgba(0,0,0,0.86);
        line-height: 1.55;
      }
      .cgpt-vs-helpTitle{ font-size: 14px; font-weight: 800; margin-bottom: 8px; }
      .cgpt-vs-helpClose{
        position: sticky;
        top: 0;
        float: right;
        height: 30px;
        padding: 0 12px;
        border-radius: 999px;
        border: 1px solid rgba(0,0,0,0.14);
        background: rgba(255,255,255,0.94);
        cursor:pointer;
      }

      #${ROOT_ID}.pinned #${BTN_ID}{ cursor: grab; }
      #${ROOT_ID}.pinned.dragging #${BTN_ID}{
        cursor: grabbing;
        box-shadow: 0 18px 44px rgba(0,0,0,0.24);
      }
      #${ROOT_ID}.pinned.hiddenLeft{ transform: translateX(-62%); }
      #${ROOT_ID}.pinned.hiddenRight{ transform: translateX(62%); }
      #${ROOT_ID}.pinned.hiddenLeft:hover,
      #${ROOT_ID}.pinned.hiddenRight:hover{ transform: translateX(0); }
      #${ROOT_ID}.open.hiddenLeft,
      #${ROOT_ID}.open.hiddenRight{ transform: translateX(0); }
    `;
    document.documentElement.appendChild(style);
  }

  // ========================== UI:构建 Root ==========================
  function ensureRoot() {
    injectStyles();

    let root = document.getElementById(ROOT_ID);
    if (root) return root;

    root = document.createElement('div');
    root.id = ROOT_ID;

    root.innerHTML = `
      <div id="${BTN_ID}" role="button" tabindex="0" aria-label="ChatGPT Virtual Scroll Engine">
        <span id="${DOT_ID}"></span>
        <span id="cgpt-vs-miniText">${t('health')}</span>
      </div>

      <div id="${PANEL_ID}">
        <div class="cgpt-vs-toprow">
          <div style="flex:1">
            <div class="cgpt-vs-seg" aria-label="virtualization mode">
              <button type="button" data-mode="performance">${lang === 'zh' ? '性能1' : 'Performance'}</button>
              <button type="button" data-mode="balanced">${lang === 'zh' ? '平衡2' : 'Balanced'}</button>
              <button type="button" data-mode="conservative">${lang === 'zh' ? '保守3' : 'Conservative'}</button>
            </div>
          </div>
        </div>

        <div class="cgpt-vs-controls">
          <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap;">
            <button class="cgpt-vs-chip primary" id="cgpt-vs-toggle">--</button>
            <button class="cgpt-vs-chip" id="cgpt-vs-minimal">--</button>
          </div>

          <div class="cgpt-vs-chiprow">
            <button class="cgpt-vs-chip" id="cgpt-vs-pin">📌</button>
            <button class="cgpt-vs-chip" id="cgpt-vs-helpBtn">?</button>
          </div>
        </div>

        <div class="cgpt-vs-hr"></div>

        <div class="cgpt-vs-row"><span class="cgpt-vs-k">${lang === 'zh' ? '当前模式' : 'Mode'}</span><span class="cgpt-vs-v" data-k="mode">--</span></div>
        <div class="cgpt-vs-row"><span class="cgpt-vs-k">DOM</span><span class="cgpt-vs-v" data-k="dom">--</span></div>
        <div class="cgpt-vs-row"><span class="cgpt-vs-k">${lang === 'zh' ? '内存(JS 堆)' : 'Memory (JS Heap)'}</span><span class="cgpt-vs-v" data-k="mem">--</span></div>
        <div class="cgpt-vs-row"><span class="cgpt-vs-k">${lang === 'zh' ? '虚拟化' : 'Virtualization'}</span><span class="cgpt-vs-v" data-k="virt">--</span></div>
        <div class="cgpt-vs-row"><span class="cgpt-vs-k">${lang === 'zh' ? '已对话轮数' : 'Turns'}</span><span class="cgpt-vs-v" data-k="turns">--</span></div>
        <div class="cgpt-vs-row"><span class="cgpt-vs-k">${lang === 'zh' ? '推荐可聊剩余' : 'Estimated remaining'}</span><span class="cgpt-vs-v" data-k="remain">--</span></div>

        <div class="cgpt-vs-hr"></div>

        <div class="cgpt-vs-controls" style="margin-top:8px;">
          <button class="cgpt-vs-chip" id="cgpt-vs-forceClean" title="${t('optimizeTip')}">${t('optimize')}</button>
          <button class="cgpt-vs-chip" id="cgpt-vs-newChat">${t('newChat')}</button>
        </div>

        <div class="cgpt-vs-hr"></div>
        <div class="cgpt-vs-tip" data-k="tip">--</div>

        <div class="cgpt-vs-hr"></div>

        <div class="cgpt-vs-about">
          <div class="cgpt-vs-aboutLeft" style="width:100%;">
            <div class="cgpt-vs-aboutTitle">ChatGPT Virtual Scroll Engine</div>
            <div class="cgpt-vs-aboutSub">by <a class="cgpt-vs-link" href="${AUTHOR_GITHUB}" target="_blank" rel="noopener noreferrer">3150214587</a></div>

            <div class="cgpt-vs-aboutLinks">
              <a class="cgpt-vs-link" href="${AUTHOR_GITHUB}" target="_blank" rel="noopener noreferrer">GitHub</a>
              <a class="cgpt-vs-link" href="${PROJECT_GITHUB}" target="_blank" rel="noopener noreferrer">Project</a>
              <button class="cgpt-vs-chip" id="${ABOUT_DONATE_BTN_ID}" title="${lang === 'zh' ? '点击打开二维码(新标签页)' : 'Open QR in new tab'}">${lang === 'zh' ? '微信赞赏码' : 'Donate (WeChat)'}</button>
            </div>

            <div class="cgpt-vs-aboutLinks" style="margin-top: 8px;">
              <button class="cgpt-vs-chip" id="cgpt-vs-xhsHome" style="color: #ff2442; font-weight: 500;">
                ${lang === 'zh' ? '更多脚本请看小红书主页' : 'More Scripts (Xiaohongshu)'}
              </button>
              <button class="cgpt-vs-chip" id="cgpt-vs-xhsMigrate">
                ${lang === 'zh' ? '卡死对话记忆迁移脚本' : 'Chat Migration Script'}
              </button>
            </div>

            <div class="cgpt-vs-supportHint">
              ${lang === 'zh'
                ? '导出聊天记录前记得按左上角暂停,如果这个工具帮你把超长对话变顺滑了,欢迎支持作者继续维护 ❤️'
                : 'If this tool makes long chats smooth, consider supporting the author ❤️'}
            </div>

            <div id="${FP_ID}"></div>
          </div>
        </div>
      </div>
    `;

    const help = document.createElement('div');
    help.id = HELP_ID;
    help.innerHTML = `
      <div class="cgpt-vs-helpCard" role="dialog" aria-label="Help">
        <button class="cgpt-vs-helpClose" id="cgpt-vs-helpClose">${lang === 'zh' ? '关闭' : 'Close'}</button>
        <div class="cgpt-vs-helpTitle">${lang === 'zh' ? '长对话加速仪表盘(小白版说明)' : 'Long Chat Accelerator (Quick Guide)'}</div>

        <div style="margin:8px 0 10px;">
          <b>${lang === 'zh' ? '绿/黄/红小圆点是什么?' : 'What is the green/yellow/red dot?'}</b><br/>
          ${lang === 'zh'
            ? '它是网页健康度指示灯:绿色=状态好;黄色=负载偏高;红色=接近卡顿区。'
            : 'It indicates page health: green=good, yellow=high load, red=near lag.'}
        </div>

        <div style="margin:10px 0;">
          <b>${lang === 'zh' ? '三段模式怎么选?' : 'How to choose modes?'}</b><br/>
          ${lang === 'zh'
            ? '性能=最省资源,最强优化,适用于老对话窗口;平衡=日常推荐,适用于中对话窗口;保守=保留更多历史但更吃资源,适用于新对话窗口。'
            : 'Performance=lowest resource; Balanced=recommended; Conservative=keeps more history but uses more resources.'}
        </div>

        <div style="margin:10px 0;">
          <b>${lang === 'zh' ? '暂停/启用有什么区别?' : 'Pause vs Enable?'}</b><br/>
          ${lang === 'zh'
            ? '启用会把屏幕外历史折叠成占位以减负;暂停会完整显示但更容易卡。'
            : 'Enable folds off-screen history to reduce load; Pause shows full history but may lag.'}
        </div>

        <div style="margin:10px 0;">
          <b>${lang === 'zh' ? '“强制优化”会丢内容吗?' : 'Does “Optimize Now” delete content?'}</b><br/>
          ${lang === 'zh'
            ? '不会。它只把更远的历史折叠掉,让网页立刻变轻;滚动到那里会自动恢复。'
            : 'No. It only folds far history to reduce load; scrolling there restores it automatically.'}
        </div>

        <div style="margin:10px 0;">
          <b>${lang === 'zh' ? 'Ctrl+F 搜索为什么会变慢?' : 'Why Find (Ctrl+F) can be slower?'}</b><br/>
          ${lang === 'zh'
            ? '为了让你能搜到所有历史,脚本会临时恢复完整内容;按 Esc 退出后自动恢复。'
            : 'To let you search all history, the script temporarily restores full content; press Esc to resume acceleration.'}
        </div>

        <div style="margin:10px 0;">
          <b>${lang === 'zh' ? '隐私与声明' : 'Privacy'}</b><br/>
          ${lang === 'zh'
            ? '本脚本不上传任何对话内容,所有逻辑均在浏览器本地运行。'
            : 'This script does not upload your chat. Everything runs locally in your browser.'}
        </div>
      </div>
    `;

    document.body.appendChild(root);
    document.body.appendChild(help);

    root.classList.toggle('minimal', minimalMode);
    root.classList.toggle('open', !!wasOpen);

    bindUI(root, help);
    applyPinnedState();

    return root;
  }

  // ========================== Feature Pack 渲染 ==========================
  function renderFeaturePack(forceRebuild) {
    const root = document.getElementById(ROOT_ID);
    if (!root) return;

    const slot = root.querySelector('#' + FP_ID);
    if (!slot) return;

    if (!forceRebuild && slot.childElementCount) return;

    slot.innerHTML = '';

    const left = document.createElement('div');
    left.className = 'fp-left';

    const right = document.createElement('div');
    right.className = 'fp-right';

    const mkBtn = (label, fn, title) => {
      const b = document.createElement('button');
      b.className = 'cgpt-vs-chip';
      b.textContent = label;
      if (title) b.title = title;
      b.addEventListener('click', fn);
      return b;
    };

    const exportBtn = mkBtn(t('export'), exportChatMarkdown);
    const foldBtn = mkBtn(t('fold'), toggleCode);

    const token = document.createElement('span');
    token.className = 'fp-token';

    const paypalBtn = mkBtn(t('paypal'), () => window.open(PAYPAL_URL, '_blank'));
    const langBtn = mkBtn(t('lang'), toggleLang);

    left.append(exportBtn, foldBtn);
    right.append(token, paypalBtn, langBtn);

    slot.append(left, right);

    // token 更新
    const tick = () => {
      if (!document.body.contains(token)) return;
      token.textContent = `${t('token')}: ${estimateTokens()}`;
      setTimeout(tick, 1500);
    };
    tick();
  }

  // ========================== UI:事件绑定 ==========================
  function bindUI(root, help) {
    const btn = root.querySelector('#' + BTN_ID);
    const panel = root.querySelector('#' + PANEL_ID);

    const toggleBtn = root.querySelector('#cgpt-vs-toggle');
    const minimalBtn = root.querySelector('#cgpt-vs-minimal');
    const pinBtn = root.querySelector('#cgpt-vs-pin');
    const helpBtn = root.querySelector('#cgpt-vs-helpBtn');
    const helpClose = help.querySelector('#cgpt-vs-helpClose');

    const forceCleanBtn = root.querySelector('#cgpt-vs-forceClean');
    const newChatBtn = root.querySelector('#cgpt-vs-newChat');
    const donateBtn = root.querySelector('#' + ABOUT_DONATE_BTN_ID);

    // 【新增】获取小红书按钮
    const xhsHomeBtn = root.querySelector('#cgpt-vs-xhsHome');
    const xhsMigrateBtn = root.querySelector('#cgpt-vs-xhsMigrate');

    function setOpen(open) {
      root.classList.toggle('open', open);
      saveBool(KEY_LAST_OPEN, open);
      startFollowPositionLoop();
    }

    btn.addEventListener('click', () => {
      if (root.classList.contains('dragging')) return;
      setOpen(!root.classList.contains('open'));
    });

    btn.addEventListener('mouseenter', () => {
      if (!pinned && minimalMode) setOpen(true);
    });
    root.addEventListener('mouseleave', () => {
      if (!pinned && minimalMode) setOpen(false);
    });

    btn.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        setOpen(!root.classList.contains('open'));
      }
    });

    document.addEventListener('click', (e) => {
      if (!root.classList.contains('open')) return;
      if (root.contains(e.target)) return;
      setOpen(false);
    }, true);

    panel.querySelectorAll('.cgpt-vs-seg button').forEach((b) => {
      b.addEventListener('click', () => {
        const mode = b.getAttribute('data-mode');
        if (mode !== 'performance' && mode !== 'balanced' && mode !== 'conservative') return;
        saveMode(mode);
        refreshSegUI(root);
        scheduleVirtualize();
        updateUI();
      });
    });

    toggleBtn.addEventListener('click', () => {
      virtualizationEnabled = !virtualizationEnabled;
      saveBool(KEY_ENABLED, virtualizationEnabled);

      if (!virtualizationEnabled) unvirtualizeAll();
      else scheduleVirtualize();

      updateUI();
    });

    minimalBtn.addEventListener('click', () => {
      minimalMode = !minimalMode;
      saveBool(KEY_MINIMAL, minimalMode);
      root.classList.toggle('minimal', minimalMode);
      updateUI();
    });

    helpBtn.addEventListener('click', () => help.classList.add('show'));
    helpClose.addEventListener('click', () => help.classList.remove('show'));
    help.addEventListener('click', (e) => {
      if (e.target === help) help.classList.remove('show');
    });

    pinBtn.addEventListener('click', () => {
      pinned = !pinned;
      saveBool(KEY_PINNED, pinned);
      applyPinnedState();
      updateUI();
    });

    // ✅ “强制优化”(原强制清理逻辑不变:只更激进折叠屏幕外)
    forceCleanBtn.addEventListener('click', () => {
      if (!virtualizationEnabled) {
        virtualizationEnabled = true;
        saveBool(KEY_ENABLED, true);
      }
      scheduleVirtualize(FORCE_CLEAN_MARGIN_SCREENS);
      flashDot();
    });

    newChatBtn.addEventListener('click', () => {
      const ok = tryClickNewChat();
      if (!ok) window.open(location.origin + '/', '_blank', 'noopener,noreferrer');
    });

    if (donateBtn) {
      donateBtn.addEventListener('click', () => {
        window.open(DONATE_QR_PAGE, '_blank', 'noopener,noreferrer');
      });
    }

    // 【新增】小红书按钮事件绑定(遵循原代码 CSP 规范)
    if (xhsHomeBtn) {
      xhsHomeBtn.addEventListener('click', () => {
        window.open('https://www.xiaohongshu.com/user/profile/637cd41f000000001f014363', '_blank', 'noopener,noreferrer');
      });
    }
    if (xhsMigrateBtn) {
      xhsMigrateBtn.addEventListener('click', () => {
        window.open('https://xhslink.com/m/85WdwqS0N5l', '_blank', 'noopener,noreferrer');
      });
    }

    installDrag(root);
    refreshSegUI(root);

    // ✅ Feature Pack 一体化渲染
    renderFeaturePack(true);
  }

  function refreshSegUI(root) {
    const panel = root.querySelector('#' + PANEL_ID);
    if (!panel) return;
    panel.querySelectorAll('.cgpt-vs-seg button').forEach((b) => {
      b.classList.toggle('active', b.getAttribute('data-mode') === currentMode);
    });
  }

  function applyPinnedState() {
    const root = ensureRoot();
    root.classList.toggle('pinned', pinned);

    if (pinned) {
      stopFollowPositionLoop();
      root.style.left = `${clamp(pinnedPos.x, 0, window.innerWidth - 60)}px`;
      root.style.top = `${clamp(pinnedPos.y, 0, window.innerHeight - 60)}px`;
      root.style.right = 'auto';
      root.style.bottom = 'auto';
      updatePinnedHiddenClass();
    }
    else {
      root.classList.remove('hiddenLeft', 'hiddenRight');
      startFollowPositionLoop();
      positionNearModelButton();
    }
  }

  function updatePinnedHiddenClass() {
    const root = ensureRoot();
    root.classList.remove('hiddenLeft', 'hiddenRight');
    if (!pinned) return;
    if (!edgeSnap) return;
    if (root.classList.contains('open')) return;

    if (pinnedPos.hidden) {
      if (pinnedPos.side === 'right') root.classList.add('hiddenRight');
      else root.classList.add('hiddenLeft');
    }
  }

  function snapToEdgeIfNeeded() {
    if (!pinned || !edgeSnap) return;

    const root = ensureRoot();
    const rect = root.getBoundingClientRect();

    const leftDist = rect.left;
    const rightDist = window.innerWidth - rect.right;
    const snapLeft = leftDist <= rightDist;

    if (snapLeft) {
      pinnedPos.x = 8;
      pinnedPos.side = 'left';
    }
    else {
      pinnedPos.x = Math.max(8, window.innerWidth - rect.width - 8);
      pinnedPos.side = 'right';
    }

    pinnedPos.hidden = true;
    savePos();
    applyPinnedState();
  }

  function installDrag(root) {
    let dragging = false;
    let startX = 0,
      startY = 0;
    let originX = 0,
      originY = 0;

    const btn = root.querySelector('#' + BTN_ID);
    if (!btn) return;

    btn.addEventListener('pointerdown', (e) => {
      if (!pinned) return;
      if (e.button !== 0) return;

      dragging = true;
      root.classList.add('dragging');
      btn.setPointerCapture(e.pointerId);

      startX = e.clientX;
      startY = e.clientY;

      const rect = root.getBoundingClientRect();
      originX = rect.left;
      originY = rect.top;

      pinnedPos.hidden = false;
      updatePinnedHiddenClass();

      e.preventDefault();
      e.stopPropagation();
    });

    btn.addEventListener('pointermove', (e) => {
      if (!dragging) return;
      const dx = e.clientX - startX;
      const dy = e.clientY - startY;

      const nx = clamp(originX + dx, 0, window.innerWidth - 40);
      const ny = clamp(originY + dy, 0, window.innerHeight - 40);

      pinnedPos.x = nx;
      pinnedPos.y = ny;
      savePos();

      root.style.left = `${nx}px`;
      root.style.top = `${ny}px`;
    });

    btn.addEventListener('pointerup', (e) => {
      if (!dragging) return;
      dragging = false;
      root.classList.remove('dragging');

      snapToEdgeIfNeeded();
      updatePinnedHiddenClass();

      e.preventDefault();
      e.stopPropagation();
    });

    btn.addEventListener('pointercancel', () => {
      dragging = false;
      root.classList.remove('dragging');
      updatePinnedHiddenClass();
    });
  }

  function flashDot() {
    const dot = document.getElementById(DOT_ID);
    if (!dot) return;
    dot.style.transform = 'scale(1.14)';
    setTimeout(() => {
      dot.style.transform = 'scale(1)';
    }, 140);
  }

  function tryClickNewChat() {
    const candidates = document.querySelectorAll('a, button, [role="button"]');
    for (const el of candidates) {
      const tx = ((el.innerText || el.textContent || '')).trim();
      if (!tx) continue;
      if (tx === '新聊天' || tx === 'New chat' || tx.includes('新对话') || tx.includes('New chat')) {
        try {
          el.click();
          return true;
        }
        catch {}
      }
    }
    return false;
  }

  // ========================== UI:刷新面板数据 ==========================
  function updateUI() {
    const root = ensureRoot();

    const domNodes = document.getElementsByTagName('*').length;
    const usedMB = getUsedHeapMB();

    const memInfo = memoryLevel(usedMB);
    const domInfo = domLevel(domNodes);

    const turns = lastTurnsCount || (getMessageNodes().length || 0);
    const virt = virtualizationEnabled ? (lastVirtualizedCount || 0) : 0;

    const remainTurns = estimateRemainingTurns(usedMB, turns);
    const remainText = (remainTurns == null) ? (lang === 'zh' ? '不可估算' : 'N/A') : (lang === 'zh' ? `${remainTurns} 轮左右` : `~${remainTurns} turns`);

    const virtText = (!virtualizationEnabled) ?
      (lang === 'zh' ? '已暂停(完整显示)' : 'Paused (full visible)') :
      (ctrlFFreeze ?
        (lang === 'zh' ? '暂停中(Ctrl+F 搜索兼容)' : 'Paused (Find active)') :
        (virt > 0 ? (lang === 'zh' ? `开启(已虚拟化 ${virt} 条)` : `On (${virt} virtualized)`) : (lang === 'zh' ? '开启(当前无需虚拟化)' : 'On (no need now)'))
      );

    const worst =
      (!virtualizationEnabled) ? 'off' :
      (memInfo.level === 'bad' || domInfo.level === 'bad') ? 'bad' :
      (memInfo.level === 'warn' || domInfo.level === 'warn') ? 'warn' :
      'ok';

    const dot = root.querySelector('#' + DOT_ID);
    if (dot) {
      dot.classList.remove('warn', 'bad', 'off');
      if (worst === 'warn') dot.classList.add('warn');
      if (worst === 'bad') dot.classList.add('bad');
      if (worst === 'off') dot.classList.add('off');
    }

    const mini = root.querySelector('#cgpt-vs-miniText');
    if (mini) {
      const status =
        worst === 'bad' ? (lang === 'zh' ? '危险' : 'Risk') :
        worst === 'warn' ? (lang === 'zh' ? '注意' : 'Caution') :
        worst === 'off' ? (lang === 'zh' ? '暂停' : 'Paused') :
        (lang === 'zh' ? '健康' : 'Healthy');
      mini.textContent = `${modeLabel(currentMode)} · ${status}`;
    }

    const setText = (k, v) => {
      const el = root.querySelector(`[data-k="${k}"]`);
      if (el) el.textContent = v;
    };

    setText('mode', `${modeLabel(currentMode)}(×${getMarginScreens()}屏)`);
    setText('dom', domInfo.label);

    const memEl = root.querySelector(`[data-k="mem"]`);
    if (memEl) {
      memEl.textContent = memInfo.label;
      memEl.classList.remove('mem-ok', 'mem-warn', 'mem-bad');
      if (memInfo.level === 'ok') memEl.classList.add('mem-ok');
      if (memInfo.level === 'warn') memEl.classList.add('mem-warn');
      if (memInfo.level === 'bad') memEl.classList.add('mem-bad');
    }

    setText('virt', virtText);
    setText('turns', `${turns}`);
    setText('remain', remainText);
    setText('tip', suggestionText(domNodes, usedMB, virt, turns));

    const toggleBtn = root.querySelector('#cgpt-vs-toggle');
    if (toggleBtn) toggleBtn.textContent = virtualizationEnabled ? (lang === 'zh' ? '暂停' : 'Pause') : (lang === 'zh' ? '启用' : 'Enable');

    const minimalBtn = root.querySelector('#cgpt-vs-minimal');
    if (minimalBtn) minimalBtn.textContent = minimalMode ? (lang === 'zh' ? '显示数值' : 'Show stats') : (lang === 'zh' ? '极简模式' : 'Minimal');

    const pinBtn = root.querySelector('#cgpt-vs-pin');
    if (pinBtn) pinBtn.textContent = pinned ? (lang === 'zh' ? '📌已钉' : '📌Pinned') : (lang === 'zh' ? '📌钉住' : '📌Pin');

    const optimizeBtn = root.querySelector('#cgpt-vs-forceClean');
    if (optimizeBtn) {
      optimizeBtn.textContent = t('optimize');
      optimizeBtn.title = t('optimizeTip');
    }

    const newBtn = root.querySelector('#cgpt-vs-newChat');
    if (newBtn) newBtn.textContent = t('newChat');

    updatePinnedHiddenClass();
  }

  // ========================== 路由守护:切换对话不消失 ==========================
  function startRouteGuards() {
    setInterval(() => {
      const root = document.getElementById(ROOT_ID);
      if (!root || !document.body.contains(root)) {
        try {
          ensureRoot();
          applyPinnedState();
          updateUI();
          scheduleVirtualize();
          startFollowPositionLoop();
        }
        catch {}
      }
      else {
        if (!pinned) positionNearModelButton();
        // Feature Pack 容器丢失时重绘
        renderFeaturePack(false);
      }
    }, ROUTE_GUARD_MS);
  }

  // ========================== 启动 ==========================
  function boot() {
    ensureRoot();
    applyPinnedState();
    startFollowPositionLoop();

    installFindGuards();
    installTypingDim();
    installImageLoadHook();
    installResizeFix();
    startRouteGuards();

    window.addEventListener('scroll', () => scheduleVirtualize(), {
      passive: true
    });

    scheduleVirtualize();
    updateUI();

    setInterval(() => updateUI(), CHECK_INTERVAL_MS);
  }

  setTimeout(boot, 900);
})();