NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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);
})();