面向长对话与重度使用的跨平台 AI 聊天增强用户脚本。在受支持的 AI 对话页面上叠加一层轻量增强,解决四类实际痛点:长会话渲染变卡、超长代码块挤占阅读空间、大段文本撑爆输入框、上下文难以迁移到新会话。
当前版本:0.13.8 许可证:MIT 运行方式:单文件用户脚本,无外部依赖,无网络请求,不申请任何 GM 权限。
脚本按主机名自动识别平台,并加载对应的选择器配置。
| 平台 | 域名 |
|---|---|
| ChatGPT | chatgpt.com、chat.openai.com |
| Google Gemini | gemini.google.com |
| Grok | grok.com、x.com/i/grok |
| Claude | claude.ai |
| DeepSeek | chat.deepseek.com |
| Microsoft Copilot | copilot.microsoft.com |
| Perplexity | perplexity.ai、www.perplexity.ai |
| Poe | poe.com |
| 豆包 | doubao.com、www.doubao.com |
| 腾讯元宝 | yuanbao.tencent.com |
| Qwen Chat | chat.qwen.ai |
| 千问 | qianwen.com、www.qianwen.com |
| ChatGLM / 智谱清言 | chatglm.cn |
| Z.ai (GLM) | chat.z.ai |
平台识别失败时脚本直接退出,不会在未适配站点产生任何副作用。
前置条件:Chrome / Edge / Firefox / Safari + 任一用户脚本管理器(Tampermonkey、Violentmonkey、Greasemonkey)。
Turbo 悬浮球即表示生效。脚本以 @run-at document-start 注入,@noframes,@grant none。头部声明了 @updateURL(jsDelivr)与 @downloadURL(ghfast 代理),不依赖在部分网络下会超时的 raw.githubusercontent.com;从没有这两个字段的旧版本升级需重装一次,详见仓库 README。
悬浮球默认位于右下角,可拖拽,位置保存在 localStorage。
当前消息数 / 60,颜色随比例由绿转黄再转红。仅为消息数量启发式,不代表模型真实上下文容量或回答质量。| 指标 | 含义 |
|---|---|
| 消息 | 当前会话检测到的消息条数 |
| 脱水 | 已被冻结渲染的消息数 |
| 折叠 | 已被折叠的代码块数 |
| 文件 | 待随下一条消息发送的虚拟文件数 |
点击悬浮球(或按 Alt + T)打开主面板。
对超出保留轮次的旧消息追加 .ai-turbo-dehydrated,实现方式:
content-visibility: hidden;
contain: layout paint style;
contain-intrinsic-size: auto var(--turbo-cached-h, 120px);
设计取舍:
display: none,content-visibility: hidden 保留节点的 textContent 可读性,因此历史检索、上下文导出、交接包提取仍能覆盖全部消息。contain-intrinsic-size 用折叠前缓存的高度占位,避免滚动条抖动。保留轮次可在面板中调整,范围 2 至 50,默认 8。
助手代码块高度超过 300px 时自动折叠为 200px 预览高度,并在上方插入一条工具栏:
language-* 类名、data-language / lang 属性,或原生头部装饰中识别输出中的代码块会显示流式状态徽标;若 1500ms 内内容无变化,则自动切换为静态行数。
ChatGPT 的代码卡片结构较特殊,脚本会向上最多追溯 10 层祖先以定位真正的外层代码卡片容器,并以 data-turn-key、article[data-turn]、li[data-message-role] 等作为硬边界,避免把整条助手消息当作折叠目标。无法安全识别边界时放弃折叠并恢复原样。
在输入框粘贴 >1500 字符的纯文本时触发。
处理流程:
pasted_text_{KB}kb.txt。[[TURBO_VFILE_START:...|行数|字节数:TURBO_VFILE_END]] token 包裹文件内容。每个虚拟文件条目提供:
平台原生附件预览插在输入框上方时,会撑高 composer 并上移输入框。为了让伪文件托盘贴合包含原生附件在内的整体视觉上缘,脚本做了以下处理:
getInputContainer:从候选容器中挑选"完整包裹输入框且面积最小"的那一个,避免把整页 wrapper 误判为 composer。
getComposerVisualBounds:从 anchor 起向上最多追溯 5 层父节点,将紧贴上方(垂直间隙 ≤ 48px)、水平方向与当前区域重叠 ≥ 35% 的兄弟节点(原生附件预览、工具条、按钮行)并入视觉边界。
视觉边界过滤(0.13.8):在把兄弟节点并入边界之前,先剔除以下类型,避免误把悬浮控件或过度外扩的容器当作 composer 的一部分:
position: fixed / sticky 的元素(如"跳回输入框"等悬浮按钮)button / [role="button"]rect.top < 8)的节点,命中后立即终止向上扩展max(baseRect.height * 3, 320px) 的巨型容器(通常代表整段对话容器)rAF 持续追踪循环:以 90ms 间隔调用位置更新,捕捉 ResizeObserver 无法感知的纯平移。循环仅在存在虚拟文件时运行。
位置签名去重:每帧计算 left|bottom|width|maxHeight 组合签名,未变化时不写样式,避免打断 CSS 过渡。
输入框引用缓存:仅在失效时才走完整 getChatInput(),避免每帧全量查询。
ResizeObserver 扩展:同时观察输入框、anchor、以及向上 5 层的祖先及其父节点,覆盖"祖先 wrapper 被附件撑高"的场景。
visualTop 钳制(0.13.8):在 positionVirtualFileTray 中,对最终计算出的 visualTop 做上下限夹取:
min(visualTop, inputRect.top),绝不高于输入框顶部max(viewportPadding + verticalGap + 60, …),确保托盘不会因视觉边界异常而跑出视口这一层钳制用于兜底,即使 getComposerVisualBounds 因为某些未知结构算出了异常值,托盘位置仍保持在视口内。
完整的"发送意图识别 → 按钮查找 → 事件模拟 → 兜底提交"链路,解决平台改版或按钮不在预期位置时消息发不出去的问题。
findSendControl按以下顺序尝试,命中即返回:
sendButtonSelectors 中任一选择器命中的可用按钮(且与输入框共享 composer 容器)。form:先找 button[type="submit"] 与非 type="button" 的按钮,再在所有按钮中按 isSendLikeButton 过滤。[data-chatgpt-composer]、.input-area-container、[class*='composer']、[class*='input-area']、[class*='input-container']、[class*='query-bar']、rich-textarea、fieldset 中按 isSendLikeButton 查找。isSendLikeButton 查找。isSendLikeButton 基于 aria-label / title / data-testid / data-tooltip 判断,命中 send、submit、发送、提交、prompt、ask;显式排除 stop、停止、cancel、abort、中止、取消,避免 AI 生成中误触停止按钮。
isButtonUsable 还会过滤掉隐藏、零尺寸、以及挂在脚本自身 UI 内的按钮。
triggerSendButton现代前端框架的发送按钮多绑定 onPointerDown / onMouseDown,直接 btn.click() 往往无效。triggerSendButton 依次派发:
pointerdown → mousedown → pointerup → mouseup → click
并携带 bubbles: true, cancelable: true, view: window, button: 0, pointerId: 1, pointerType: "mouse", isPrimary: true,确保 React 与 Pointer Events 监听都能识别为一次真实点击。
sendWithRetry以 50ms 间隔最多轮询 40 次(约 2 秒):
disabled 或 aria-disabled="true",此时脚本会先 removeAttribute("disabled") 并置 aria-disabled="false",再调用 triggerSendButton。fallbackKeyboardSubmit:先尝试 form.requestSubmit();若无 form 或该方法不可用,则派发带 keyCode / which = 13 的 keydown + keyup Enter 事件。handleSendInterceptEnter 键:存在虚拟文件且未处于提交中时,拦截 Enter(不含 Shift、非输入法组合状态),合并虚拟文件并触发 sendWithRetry。
点击:判断用户是否点击了"发送按钮"时,不再强制依赖 isSendControlForInput,而是:
currentPlatform.sendButtonSelectors 做 closest() 匹配;isSendLikeButton 兜底识别。命中即视为发送意图,preventDefault + stopPropagation 后合并虚拟文件并触发 sendWithRetry。
把当前对话的工作状态迁移到新会话或新模型,避免从零复述。
0.13.8 起,交接区从原来的四个按钮(发送指令 / 恢复到新对话 / 仅复制 YAML / 仅复制恢复指令)合并为一个:
| 按钮 | 行为 |
|---|---|
| 一键交接(生成并恢复) | 见下节流程 |
| 复制原始上下文(回退) | 直接把整段对话导出为 Markdown 到剪贴板 |
startOneClickHandoff 内部按优先级处理:
proceedOneClickHandoff:构建恢复指令 → 通过 sessionStorage 跨页暂存 → 点击新对话入口 → resumePendingHandoffPrompt 在新页面填入。sendWithRetry 发送:waitForHandoffPackage 轮询。waitForHandoffPackage 以 600ms 间隔轮询,超时阈值 3 分钟:
turns.length <= baselineTurns)时不误判;extractLatestHandoffText 命中 → 关闭轮询并进入 proceedOneClickHandoff;以上所有异常分支都统一走 copyHandoffFallback(text, statusMessage, alertMessage):
navigator.clipboard.writeText 写入进度文本;handoff-status);"当前进度" 的含义:
buildHandoffGenerationPrompt);buildHandoffRestorePrompt,即"交接包 + 前置要求" 完整模板)。这样即使自动化全程中断,用户仍能从剪贴板直接粘贴接续。
交接包实际是纯文本结构化格式(非严格 YAML),要求模型直接输出、不使用 Markdown 代码块,从 [META] 到 [END]、字段固定、每字段形如 字段名: 内容:
[META] version / generated_at / source_model / language
[TASK] objective / success_criteria / current_status
[DECISIONS] confirmed / rejected / assumptions
[PROGRESS] done / in_progress / next_actions
[ROLE] persona / tone / response_format / boundaries
[CONTEXT] tools / tech_stack / files / data / constraints
[OPEN_ISSUES] questions / risks / blockers
[KEY_ENTITIES] people / projects / terms
[HANDOFF_NOTES] 自由补充
[END]
提取逻辑优先兼容旧版 YAML 格式(meta: 等顶层键),两种格式都会按纯文本处理,不依赖严格 YAML 解析。
buildHandoffRestorePrompt 要求模型:
next_actions 首项;questions 非空,先按优先级列出并询问;sessionStorage 时,同样回退为复制到剪贴板。sessionStorage 暂存超过 5 分钟自动失效;新对话页面识别不到输入框时也会回退为复制。面板中的检索框在输入后经 100ms 防抖执行全文匹配,最多返回 10 条,每条显示消息序号与 180 字符预览。
点击结果会关闭面板、临时恢复脱水状态、平滑滚动定位并高亮约 1.8 秒。检索覆盖全部已索引消息,包括处于脱水状态的消息。
复制原始上下文(回退) 按钮会把全部消息按 ### 用户 / 助手 · Message N 分段,输出为 Markdown 写入剪贴板。用于脱离脚本、把完整上下文粘贴到任意位置。
| 快捷键 / 操作 | 行为 |
|---|---|
Alt + T |
呼出 / 收起主面板 |
Esc |
关闭当前弹层(文件预览或主面板) |
Enter |
存在虚拟文件时触发合并发送(已输入内容 + 全部虚拟文件) |
| 拖拽悬浮球 | 移动位置,松手即保存 |
面板内所有开关即时生效并写入配置,无需刷新页面。
配置项存储在 localStorage 的 ai_turbo_engine_config。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
keepTurns |
number | 8 | 保留最近 N 轮不脱水,范围 2 至 50 |
pruneEnabled |
boolean | true | 是否启用视口脱水 |
collapseCode |
boolean | true | 是否启用长代码折叠 |
virtualFileEnabled |
boolean | true | 是否启用长文本伪文件 |
悬浮球位置存储在 ai_turbo_pill_pos;跨页交接暂存使用 sessionStorage 的 ai_turbo_handoff_restore。
页面变更由 MutationObserver 捕获。为避免观察自身写入导致的自激循环,脚本在修改 DOM 时进入 selfMutationDepth > 0 状态,观察回调直接跳过。渲染更新统一经 requestAnimationFrame 调度,最短间隔 80ms。
所有布局读取(offsetHeight、getBoundingClientRect)集中在写入之前完成,写入阶段不触发回流读取,避免 Layout Thrashing。伪文件托盘的位置计算同样读写分离:一次性读取 anchor、input 与相邻兄弟节点的几何信息,算出最终位置后统一写入。
turnIndex 记录已索引消息。结构变化(消息增删、顺序改变、选择器组切换)触发全量同步,其余情况只处理 dirty 集合。选择器组按命中优先级依次尝试并缓存,SPA 路由变化时失效重选。
伪文件托盘的位置更新由三套机制协同:
ResizeObserver:捕捉尺寸变化(原生附件插入、输入框变高、composer 撑开)ResizeObserver 无法感知的平移)visualTop 钳制(0.13.8):前者剔除悬浮控件和过大的兄弟容器,后者兜底把 visualTop 钳制在合理区间,防止托盘因异常几何值消失或跳到视口外三者都通过 requestAnimationFrame 合并到同一入口 positionVirtualFileTray,配合位置签名去重,避免重复写入与抖动。
insertTextAtomictextarea / input:使用 Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype | HTMLInputElement.prototype, "value").set 调用原生 setter 写入,保证 React 受控组件能感知变化,然后派发 input + change 事件。contenteditable(ProseMirror 等):优先用 document.createRange() + Selection 选中编辑器全部内容,再 document.execCommand("insertHTML", ...) 插入,最后补派一次 InputEvent。execCommand 失败时回退为 textContent = text。凡是涉及改写平台原生结构的操作(代码折叠、交接流程、发送按钮定位、消息回显胶囊)都遵循同一原则:无法可靠识别目标时不做修改,保持原生行为,并通过状态提示告知用户。交接流程额外保证:任何一步失败都把当前进度写入剪贴板,允许手动接续。
在 PLATFORMS 中新增条目,并在 detectPlatform() 中补充域名分支即可接入新平台。条目需要提供的字段:
| 字段 | 用途 |
|---|---|
turnSelectorGroups |
多套消息节点选择器,按命中优先级尝试 |
turnSelectors |
兜底消息选择器 |
userTurnSelectors |
识别用户消息 |
userContentSelectors |
用户消息内容容器 |
contentSelectors |
助手消息内容容器 |
codeSelectors |
代码块选择器 |
inputSelectors |
输入框选择器 |
sendButtonSelectors |
发送按钮选择器(会被 findSendControl 优先使用,未命中则走通用兜底) |
findSendControl 的通用兜底对所有平台生效,因此新平台未提供精确的 sendButtonSelectors 也可能通过 form / composer 容器 / 向上 8 层的 isSendLikeButton 命中。
findSendControl 的多级兜底可覆盖部分小规模改版,但无法覆盖彻底重构的 DOM 结构。isSendLikeButton 的排除规则跳过;请等生成结束后再发送。修复更新链路:补全 @updateURL / @downloadURL,ScriptCat 恢复自动检测更新。
@updateURL(cdn.jsdelivr.net/gh/...@main/...,检查更新)与 @downloadURL(ghfast.top 代理 raw,下载新版本)。此前两者缺失,管理器退回安装地址 raw.githubusercontent.com,该域名在部分网络下超时,表现为「永远检测不到更新」。.github/workflows/purge-jsdelivr.yml:推送 main 后 purge CDN 缓存并校验 CDN 上的 @version 与仓库一致,消除 @main 分支最长 12 小时的缓存窗口。修复伪文件托盘在 DeepSeek 等页面上的定位异常,交接改为单一「一键交接」按钮,并把失败进度自动复制到剪贴板。
getComposerVisualBounds 新增四类过滤:position: fixed / sticky 的兄弟节点(如"跳回输入框"悬浮按钮),避免视觉边界被错误地扩展到屏幕中部甚至上方button / [role="button"]rect.top < 8(已滚出视口上方)时立即终止向上扩展max(baseRect.height * 3, 320px) 时终止扩展,避免把整段对话容器并入边界positionVirtualFileTray 新增 visualTop 钳制:Math.min(visualTop, inputRect.top)Math.max(viewportPadding + verticalGap + 60, …)一键交接(生成并恢复)startOneClickHandoff:sendWithRetry,发送成功后进入 waitForHandoffPackagewaitForHandoffPackage:baselineTurns 避免在助手消息尚未出现时误判proceedOneClickHandoff:sessionStorage 暂存 → 点击新对话 → resumePendingHandoffPromptcopyHandoffFallback:sessionStorage 不可用 —— 均把当前进度(生成指令或恢复指令)复制到剪贴板修复"点击发送按钮后消息发不出去"的问题。根因是 handleSendIntercept 的 click 分支过度依赖 isSendControlForInput,以及 sendWithRetry 的发送按钮查找缺少兜底。
sendButtonSelectors(ChatGPT)扩展:新增 composer-send-button、button[data-testid*="send" i]、button[aria-label*="Send prompt" i]、button[title*="Send" i] 等,覆盖新旧结构isSendLikeButton:基于 aria-label / title / data-testid / data-tooltip 识别发送意图,显式排除 stop / 取消 等停止按钮isButtonUsable / isButtonDisabled:统一过滤隐藏、零尺寸、脚本自身 UI 内的按钮,并集中判断 disabled 状态findSendControl:四级查找(平台选择器 → form → composer 容器 → 向上 8 层)triggerSendButton:模拟完整的 pointerdown → mousedown → pointerup → mouseup → click 序列fallbackKeyboardSubmit:优先 form.requestSubmit(),再派发带 keyCode / which = 13 的 keydown + keyupsendWithRetry:40 次轮询,前 15 次只接受可用按钮,第 16 次起允许 disabled 并先清除 disabled 属性再点击,第 40 次走键盘兜底handleSendIntercept 的 click 分支:选择器未命中时向上 4 层按 isSendLikeButton 识别insertTextAtomic:textarea / input 使用原生 setter 写值以兼容 React 受控组件;contenteditable 用 Range + Selection 选中全部内容,再 execCommand("insertHTML")cleanupAndResume 后置重置 isSubmitting修复原生附件存在时伪文件托盘定位错误与输入框变形问题。
getInputContainer 由"取 rect.top 最小"改为"完整包裹输入框且面积最小"getComposerVisualBounds:向上追溯最多 5 层,将紧贴上方、水平重叠 ≥ 35%、垂直间隙 ≤ 48px 的兄弟节点并入视觉顶部ResizeObserver 观察范围扩展到输入框、anchor 及向上 5 层祖先及其父节点bottom / left 120ms cubic-bezier(0.22, 1, 0.36, 1),max-height 90ms,新增 will-change: bottom, left, max-height完整消息提取、流式代码感知折叠、纯文本交接包(兼容旧版 YAML)、多平台适配、会话检索、视口脱水。
MIT License
Copyright (c) Yoko