<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>网页效率工具 | VISTA Research Group</title><link>https://vista-research-group.pages.dev/tags/%E7%BD%91%E9%A1%B5%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/</link><atom:link href="https://vista-research-group.pages.dev/tags/%E7%BD%91%E9%A1%B5%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/index.xml" rel="self" type="application/rss+xml"/><description>网页效率工具</description><generator>Hugo Blox Builder (https://hugoblox.com)</generator><language>en</language><lastBuildDate>Mon, 22 Jun 2026 00:00:00 +0000</lastBuildDate><image><url>https://vista-research-group.pages.dev/media/icon_hu_831f3df7c45fd3fa.png</url><title>网页效率工具</title><link>https://vista-research-group.pages.dev/tags/%E7%BD%91%E9%A1%B5%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/</link></image><item><title>给Aizex-ChatGPT 装一个对话导航面板</title><link>https://vista-research-group.pages.dev/post/aizex-nav/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://vista-research-group.pages.dev/post/aizex-nav/</guid><description>&lt;h2 id="aizex-panel">Aizex Panel&lt;/h2>
&lt;p>Aizex是一个基于GPT原生UI的“GPT X 聚合多系列模型”使用方案。&lt;/p>
&lt;p>提供GPT、Claude、Gemini、Grok等一系列LLM。&lt;/p>
&lt;p>
&lt;/p>
&lt;hr>
&lt;h2 id="痛点">痛点&lt;/h2>
&lt;p>在对话时，一个对话往往会积累几十轮问答。想回头看某个问题，只能靠鼠标滚轮一路翻，或者拖动右侧滚动条盲猜位置。内容一多，这个过程非常低效。&lt;/p>
&lt;p>理想的状态是：有一个侧边目录，把我所有的提问列出来，点一下就跳过去。&lt;/p>
&lt;hr>
&lt;h2 id="思路">思路&lt;/h2>
&lt;p>Aizex Panel-ChatGPT 是 React 单页应用，每一轮对话都有稳定的 DOM 标记：&lt;/p>
&lt;pre tabindex="0">&lt;code>data-testid=&amp;#34;conversation-turn-1&amp;#34;
data-testid=&amp;#34;conversation-turn-2&amp;#34;
...
&lt;/code>&lt;/pre>&lt;p>用户消息和 AI 回复通过 &lt;code>data-message-author-role&lt;/code> 区分：&lt;/p>
&lt;pre tabindex="0">&lt;code>data-message-author-role=&amp;#34;user&amp;#34;
data-message-author-role=&amp;#34;assistant&amp;#34;
&lt;/code>&lt;/pre>&lt;p>有了这两个锚点，就可以在不修改网站源码的情况下，用油猴脚本向页面注入一个浮动导航面板。&lt;/p>
&lt;hr>
&lt;h2 id="实现">实现&lt;/h2>
&lt;h3 id="安装方式">安装方式&lt;/h3>
&lt;ol>
&lt;li>浏览器安装
扩展（Chrome / Edge 均可）&lt;/li>
&lt;li>新建脚本，把 &lt;code>chatgpt-nav.user.js&lt;/code> 的内容粘贴进去，保存&lt;/li>
&lt;li>访问 &lt;code>https://mana-x.aizex.net/&lt;/code> 或 &lt;code>https://chatgpt.com/&lt;/code> 任意对话页面，面板自动出现&lt;/li>
&lt;/ol>
&lt;h3 id="脚本结构">脚本结构&lt;/h3>
&lt;pre tabindex="0">&lt;code>chatgpt-nav.user.js
├── 样式注入 — 暗色主题浮动面板，与 ChatGPT 界面协调
├── getUserTurns() — 扫描 DOM，提取所有用户提问
├── renderList() — 渲染列表，支持关键词过滤
├── createPanel() — 创建面板，绑定交互事件
├── init() — 初始化，启动 MutationObserver 监听新消息
└── waitAndInit() — 等待对话内容加载完成后再初始化
&lt;/code>&lt;/pre>&lt;h3 id="核心功能">核心功能&lt;/h3>
&lt;p>&lt;strong>提问列表&lt;/strong>&lt;/p>
&lt;p>扫描页面所有 &lt;code>conversation-turn-*&lt;/code> 节点，过滤出 &lt;code>role=&amp;quot;user&amp;quot;&lt;/code> 的轮次，提取前 80 字作预览，去掉 &amp;ldquo;你说：&amp;rdquo; 等界面前缀后显示为 &lt;code>Q1&lt;/code>、&lt;code>Q2&lt;/code>&amp;hellip; 的编号列表。点击任意条目，页面平滑滚动到对应位置。&lt;/p>
&lt;p>&lt;strong>实时搜索&lt;/strong>&lt;/p>
&lt;p>面板顶部有搜索框，输入关键词后列表实时过滤，大小写不敏感。&lt;/p>
&lt;p>&lt;strong>收起 / 展开&lt;/strong>&lt;/p>
&lt;p>点击标题栏可以将面板收起为一个 40px 的圆形气泡（只显示 💬 图标），不遮挡阅读区域。状态存入 &lt;code>localStorage&lt;/code>，刷新页面后保持。&lt;/p>
&lt;p>&lt;strong>收起状态可拖拽&lt;/strong>&lt;/p>
&lt;p>收起后的气泡支持拖动到屏幕任意位置。松手后位置自动保存，下次收起时恢复到上次拖放的位置。展开状态始终回到右侧居中的默认位置。&lt;/p>
&lt;p>用 4px 的移动阈值区分&amp;quot;点击展开&amp;quot;和&amp;quot;拖动移位&amp;quot;，两个操作不会互相干扰。&lt;/p>
&lt;p>&lt;strong>自动刷新&lt;/strong>&lt;/p>
&lt;p>用 &lt;code>MutationObserver&lt;/code> 监听对话区域的 DOM 变化，新消息出现后 500ms 内自动刷新导航列表。&lt;/p>
&lt;p>&lt;strong>SPA 路由感知&lt;/strong>&lt;/p>
&lt;p>ChatGPT 切换对话不会刷新页面，脚本通过对比 &lt;code>location.href&lt;/code> 检测路由变化，打开新对话时自动重新初始化面板。&lt;/p>
&lt;hr>
&lt;h2 id="关键细节">关键细节&lt;/h2>
&lt;p>&lt;strong>拖拽与点击的区分&lt;/strong>&lt;/p>
&lt;p>mousedown 时记录起始坐标，mousemove 时判断位移是否超过 4px，超过才标记为拖拽。mouseup 时如果是拖拽则保存位置、阻止 click 触发展开；如果没有移动则正常展开。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>panel.addEventListener(&lt;span style="color:#f1fa8c">&amp;#39;mousedown&amp;#39;&lt;/span>, (e) =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">const&lt;/span> startX &lt;span style="color:#ff79c6">=&lt;/span> e.clientX, startY &lt;span style="color:#ff79c6">=&lt;/span> e.clientY;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> panel._dragged &lt;span style="color:#ff79c6">=&lt;/span> &lt;span style="color:#ff79c6">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8be9fd;font-style:italic">function&lt;/span> onMove(e) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (&lt;span style="color:#8be9fd;font-style:italic">Math&lt;/span>.abs(e.clientX &lt;span style="color:#ff79c6">-&lt;/span> startX) &lt;span style="color:#ff79c6">&amp;gt;&lt;/span> &lt;span style="color:#bd93f9">4&lt;/span> &lt;span style="color:#ff79c6">||&lt;/span> &lt;span style="color:#8be9fd;font-style:italic">Math&lt;/span>.abs(e.clientY &lt;span style="color:#ff79c6">-&lt;/span> startY) &lt;span style="color:#ff79c6">&amp;gt;&lt;/span> &lt;span style="color:#bd93f9">4&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> panel._dragged &lt;span style="color:#ff79c6">=&lt;/span> &lt;span style="color:#ff79c6">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#6272a4">// ...移动面板
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6272a4">&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#6272a4">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6272a4">&lt;/span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>header.addEventListener(&lt;span style="color:#f1fa8c">&amp;#39;click&amp;#39;&lt;/span>, (e) =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff79c6">if&lt;/span> (panel._dragged) { panel._dragged &lt;span style="color:#ff79c6">=&lt;/span> &lt;span style="color:#ff79c6">false&lt;/span>; &lt;span style="color:#ff79c6">return&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#6272a4">// ...切换展开/收起
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6272a4">&lt;/span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>收起气泡的尺寸问题&lt;/strong>&lt;/p>
&lt;p>最初把标题 &amp;ldquo;💬 对话导航&amp;rdquo; 也放在收起状态里，但 40px 的宽度装不下，文字会被 &lt;code>overflow: hidden&lt;/code> 截断。解决方案是在 HTML 里维护两套内容：展开时显示标题和&amp;quot;收起&amp;quot;按钮，收起时只显示独立的 &lt;code>.__nav-icon&lt;/code> 元素，通过 CSS 互相切换显隐。&lt;/p>
&lt;hr>
&lt;h2 id="适配范围">适配范围&lt;/h2>
&lt;p>脚本的 &lt;code>@match&lt;/code> 规则覆盖两个域名：&lt;/p>
&lt;pre tabindex="0">&lt;code>https://mana-x.aizex.net/* — Aizex 合租面板代理的 ChatGPT
https://chatgpt.com/* — ChatGPT 官网
&lt;/code>&lt;/pre>&lt;p>两者的 DOM 结构相同（均为 ChatGPT 前端），无需额外适配。&lt;/p>
&lt;hr>
&lt;h2 id="效果">效果&lt;/h2>
&lt;p>
&lt;figure >
&lt;div class="flex justify-center ">
&lt;div class="w-full" >&lt;img src="./1.jpg" alt="效果图" loading="lazy" data-zoomable />&lt;/div>
&lt;/div>&lt;/figure>
&lt;/p>
&lt;hr>
&lt;h2 id="文件">文件&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>文件&lt;/th>
&lt;th>说明&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>chatgpt-nav.user.js&lt;/code>&lt;/td>
&lt;td>油猴脚本本体，安装到 Tampermonkey 即可使用。&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>
&lt;/p>
&lt;hr>
&lt;p>&lt;em>开发于 2026-06-22，使用 Kiro + playwright-cli 辅助调试页面结构。&lt;/em>&lt;/p></description></item></channel></rss>