1. 为什么我最终选择在侧边栏里同时塞进 6 个 AI:先说痛点
我之前的工作状态,估计很多重度 AI 用户都有共鸣:桌面上开着五六个标签页,分别是 ChatGPT、Claude、Gemini、Kimi、文心一言、通义千问之类的对话窗口,来回切换比对答案。有时候为了验证一个问题的不同回答,我得在标签页之间点来点去,光是找对应窗口就得花掉好几秒,更别提一不留神关错标签页,上下文全丢了。让人烦心的不只是切来切去,而是当你真正想对比多个 AI 回答时,多标签页模式本质上是不适合的——因为一次只能看到一个窗口。所以我做了这个开源项目:一个 Chrome 侧边栏扩展,把 6 个主流 AI 服务整合进侧边栏,支持一键切换,还能同时以并排分栏的方式呈现多个 AI 的回复。
这个项目并不是什么高深莫测的黑科技,它依赖的核心能力是 Chrome 自带的chrome.sidePanelAPI。但这玩意儿从 Chrome 114 开始才稳定支持,到现在依然有很多人没意识到它能把侧边栏玩出花来。我的项目做的事情很简单:在浏览器侧边栏里实现一套可插拔的 AI 多路面板,默认配置接入 6 个公开可访问的 AI Web 应用,每个以独立 iframe 方式加载,互不干扰。然后通过面板顶部的切换器,你可以瞬间把一个 AI 切换成另一个,也可以拖动分栏把手,让两个、三个甚至更多 AI 并排同时出现在侧边栏里。
适合谁来用?一是跟我一样的 AI 重度用户——每天要面对不同模型回答同一问题的场景;二是做 Prompt 对比、模型评估、竞品分析的产品或技术同学;三是想学习 Chrome 扩展开发、特别是研究sidePanelAPI 和 iframe 多实例隔离机制的开发者。这项目代码量不大,结构清晰,我尽量做了详细注释,改造成本很低。
在往下展开之前先说个前提:如果你本身对 AI 服务一无所知,也完全不会写代码,这个扩展你也能直接安装使用——它只是个壳,负责把网页加载到侧边栏里,不涉及任何代理和转发,所有 AI 服务的账号体系、聊天记录都还是你自己的。这一点很重要,后面我会详细解释为什么这么设计。
2. 技术选型复盘:sidePanel API、iframe 隔离与 Manifest V3 的取舍
2.1 为什么是 sidePanel 而不是 action popup 或 new tab
我先对比了三条路线:默认弹窗(action.popup)、新标签页、侧边栏。不做 popup 的原因很简单——它本质是个临时浮层,鼠标一移开就没了,AI 对话动辄要滚动阅读长回复,体验非常差。新标签页又回到了多标签页混乱的老路,而且占用的屏幕空间太大,干扰主工作流。侧边栏是最好的折中:它常驻、可折叠、不遮挡主页面,宽度可以自定义,正好适合对话这种纵向信息密度高、横向要求不高的内容形态。
Manifest V3 已经是 Chrome 扩展的唯一标准,这没什么好纠结的。需要提的是,chrome.sidePanel在 MV3 里并非默认开放,需要在manifest.json中显式声明"sidePanel"权限,并且面板页面路径要配置在side_panel.default_path字段中。另外,不同 Chrome 版本的 API 行为有些微差别,比如早期版本面板打开后无法通过 JS 控制关闭,只能靠用户手动点击浏览器自带的关闭按钮,后来版本才补充了sidePanel.close()这类方法。我建议在开发时锁定一个较新的 Chrome 版本(我用的是 126+),避免踩到旧版本 API 不全的坑。
2.2 manifest.json 里必须注意的几个配置项
配置本身并不复杂,但有三个地方很容易被忽视,我直接把示例贴出来:
{ "manifest_version": 3, "name": "Multi AI Sidebar", "version": "0.1.0", "permissions": ["sidePanel", "storage", "tabs"], "host_permissions": [ "https://chat.openai.com/*", "https://claude.ai/*", "https://gemini.google.com/*" ], "background": { "service_worker": "src/background.js" }, "side_panel": { "default_path": "src/sidepanel.html" }, "action": { "default_title": "打开 AI 侧边栏" } }你没看错,我没有用"default_popup",而是让点击扩展图标后走 background 脚本去调sidePanel.open()。为什么要绕一步?因为sidePanel.open()必须由用户手势触发,且最好在action.onClicked事件里调用,这样才能保证浏览器把"点击图标"当作合法手势,否则很多版本会静默失败。这是个非常隐蔽的坑,我一开始直接把default_popup和侧边栏混用,结果弹窗和侧边栏同时出现,关了弹窗侧边栏也怪怪的。后来彻底砍掉 popup,只保留图标点击触发侧边栏,逻辑干净多了。
还有一个容易忽略的点:side_panel字段配置的默认面板是全局的,但你可以通过chrome.sidePanel.setOptions()给特定标签页设置面板路径,也就是说同一个扩展在不同标签页可以显示不同的 UI。我在做多 AI 并排功能时就用到了这个能力——在并排模式下面板会加载一个多栏布局的 HTML,而普通模式只加载单栏页面,两者通过 setOptions 切换。动态改面板路径这个能力,文档里没有大篇幅讲,实际用起来很顺手,在"一个扩展兼容多种形态"的需求里非常值得尝试。
2.3 iframe 三实例隔离背后的存储分区机制
这个项目的核心难点不在 API 调用,而在如何让多个 AI Web 应用共存于同一个侧边栏页面。直接把六个服务的网址通过<iframe src="...">塞进去,你会发现三个典型问题:Cookie 共享导致登录态互踢、LocalStorage 冲突、以及某些站点通过frame-ancestors或 CSP 头阻止被嵌入。
第一个问题最致命。像 ChatGPT 和 Claude 这类服务,如果它们恰好共用了同一域名的存储(虽然实际不太会发生,但比如同一套 SSO 体系下的子产品),互相挤掉登录态就很难受。更普遍的情况是这些 AI 平台会通过第三方 Cookie 跟踪用户,嵌入在同一个页面里时浏览器可能统一拦截,导致某些平台点击登录后无反应。解决方案是给每个 iframe 指定独立的存储分区:
<iframe src="https://chat.openai.com" partition="trusted-openai"></iframe> <iframe src="https://claude.ai" partition="trusted-claude"></iframe>这个partition属性划定了 iframe 的"存储分区",从 Chromium 92 开始支持。简单理解就是:每个分区相当于一个独立的浏览器配置文件,Cookie、localStorage、Cache 都互相隔离,谁也不会污染谁。但注意,这并不能解决 CSPframe-ancestors的限制——如果目标站点在响应头里明确声明禁止被嵌入,那无论如何都嵌不进去。
关于这一点,我的处理方式是"能嵌就嵌,不能嵌就走代理模式"。在项目的config里给每个 AI 服务加了一个embedMode字段,值为iframe还是proxy。对于 CSP 限制严格的服务,走扩展 background 里发起的 fetch 请求,把目标页面作为普通 HTTP 资源拉回来再渲染。这等于自己实现了一个轻量代理——但只用于绕过框架限制,不修改响应内容、不做注入,确保本质上还是访问原站。这是项目里最复杂的一部分,我放在第 4 章单独讲。
3. 三个核心击破点:一键切换、并排对比、Prompt 自动分发
3.1 一键切换的交互逻辑和 Tab 状态记忆
标题里说的"一键切换",实现上并没有用很高深的技术。我在侧边栏顶部设计了一排图标按钮,每个对应一个 AI 服务。点击某个按钮时,主内容区切换到对应的 iframe 显示。同时我用chrome.storage.session记录当前激活的服务 id,这样哪怕你折叠了侧边栏再展开,它也能恢复成上次用的那个 AI,不需要重新找。
这部分看似简单,但优化空间藏在细节里:
- 懒加载:六个 iframe 如果全部同时渲染,侧边栏一打开内存直接飙升,甚至会出现掉帧。所以我只在第一次点击某服务时才创建 iframe,创建后保留 DOM 节点不再销毁,这样后续切换只是 display 显隐,无需重新加载页面。
- 加载状态:AI Web 应用动辄几百 KB 的 JS 资源,首次加载需要时间。我在 iframe 上方盖了一个 loading 层,监听 iframe 的
load事件来隐藏它。实际测试下来 GPT 系页面加载大概 2~3 秒,Claude 稍快,Gemini 有时候能到 5 秒以上,没有 loading 提示用户会以为扩展坏了。 - 已打开状态标识:每个图标右下角有个小圆点,绿点代表这个 AI 已经初始化过、可以秒切;灰点代表还没打开过,点击会有首次加载等待。这个小细节用户反馈很好,降低了不确定感。
切换逻辑的核心代码大致是:
async function switchToProvider(providerId) { const provider = PROVIDERS.find(p => p.id === providerId); if (!provider) return; if (!provider.iframeCreated) { const iframe = document.createElement('iframe'); iframe.src = provider.url; iframe.partition = `trusted-${provider.id}`; iframe.className = 'ai-frame'; iframe.dataset.providerId = provider.id; mainContent.appendChild(iframe); provider.iframeCreated = true; } document.querySelectorAll('.ai-frame').forEach(f => { f.style.display = f.dataset.providerId === providerId ? 'block' : 'none'; }); await chrome.storage.session.set({ activeProvider: providerId }); updateActiveIndicator(providerId); }3.2 并排模式:分栏容器与等比缩放陷阱
多 AI 并排是我自己最常用的功能。点一下顶部的"分栏"按钮,侧边栏会从单栏切换为多栏 Grid 布局。在 Grid 容器里,每个被加载过的 iframe 占据一列,所有列宽度相等。由于侧边栏本身不宽,我通常开 2~3 栏,再多的话每个 AI 的实际可读宽度不足 300px,对话排版会变得很怪。
这里有一个必须要处理的细节:iframe 内容有自己的 CSS 媒体查询,过窄的视口会触发移动端布局。比如 Claude 在 400px 以下宽度时会切换到窄屏样式,字体变大、按钮堆叠,反而更占空间。这没法从根本上禁止,因为样式在对方站点控制范围内,但可以用transform: scale()做等比缩放入一个容器里,视觉上模拟出"完整桌面布局缩小后"的效果。我试过这个方法,副作用是缩放后的事件坐标要对齐,处理起来有点划不来,所以最后放弃,接受了窄屏布局、并在分栏时给每个容器加了最小宽度禁下限。如果确实需要多个 AI 在同一屏精确对比,不如把侧边栏拉宽到 800px 以上,这个场景下三栏并排的体验是能接受的。
并排模式的精髓在于,你可以左半个屏幕看 ChatGPT 的回答,右半个屏幕看 Claude 的回答,对照阅读。而不需要像以前那样在标签页之间来回跳。结合下一节说的 Prompt 同步,这个功能真正解决了我"用一个问题时总想知道别的模型怎么看"的执念。
3.3 Prompt 一键分发到所有 AI:自动化模拟输入
这是我做这个项目时临时加的功能,结果成了用户最常夸的一点。并排模式下,如果你想给 6 个 AI 同时提同一个问题,手动一个个粘贴太折磨人。于是我加了一个"同步输入"模式:在主 AI(当前激活的那个)输入框里输入内容时,其余 AI 的输入框也会自动填入相同内容。
实现思路不复杂:在主 iframe 内注入一个内容脚本,监听输入框的input事件,拿到用户输入的值后通过postMessage传给侧边栏的父页面,父页面收到后再分发给其他 iframe 内的内容脚本,由它们设置对应输入框的值,并触发input事件让前端框架感知更新。这里有两个坑要注意:
- 不同站点的输入框选择器完全不同,ChatGPT 用的是
#prompt-textarea,Claude 是div[contenteditable="true"],Gemini 又不一样。所以选择器必须每个服务单独配置。 - React/Vue 等框架的受控组件不认直接赋值,如果你只设置
input.value而不触发原生input事件,框架内部状态根本不会更新。正确姿势是先执行nativeInputValueSetter修改队列值,再派发new Event('input', { bubbles: true })。这段代码我统一封装成一个setNativeValue函数,是 Can't 不能省的。
Prompt 同步本身不是新概念,但在侧边栏多 AI 场景下,它把"并排对比"的使用效率又提高了一个台阶。否则并排只是视觉上的排列,操作上还是一切割裂的。
4. 绕不开的那些硬骨头:跨域、登录态与 CSP 限制的破解路径
4.1 跨域约束:为什么说"纯前端 iframe 永远做不到 100% 通吃"
必须先给读者打个预防针:不经过后端代理,终究会有网站嵌不进来。道理不复杂,现代网站普遍带X-Frame-Options或 CSPframe-ancestors响应头,浏览器看到这些头就会拒绝 iframe 加载。这是站点安全策略,从产品设计上是防点击劫持的,不能一概视为恶意的"封锁"。
在我的项目里,6 个默认 AI 服务中,能用纯 iframe 嵌入的大概只有一半,剩余的就需要走代理模式。代理模式做的事:让 background script 向目标站点发起 fetch,拿到 HTML 响应后改写内部的相对链接为绝对链接,再把改写后的 HTML 塞进 iframe 的srcdoc或直接渲染。听起来有戏,但执行起来会发现一个接一个的问题:
- 很多站点的 HTML 里有大量相对路径(比如
/static/chunk.js),必须补全为https://domain/static/chunk.js,否则子资源全挂。 - 动态加载的脚本或 API 请求往往带有校验 token,这些 token 在服务端渲染时写进了页面,重写后依然有效,但也可能因为 CSRF 校验而失败。
- 后续 XHR/fetch 请求如果触发了 CORS 预检,而扩展的 origin 不是目标站点 origin,就会被浏览器拦下。
所以代理模式是一种"尽力而为"的方案,我明确把它定位为 fallback,不承诺所有动态交互都 100% 可用。好在这类 AI Web 应用最核心的"发消息、看回复"行为通常涉及 websocket 或流式接口,一旦 CORS 卡住就容易失败,这也是某些服务最后依然需要用户在专门的标签页打开的原因之一。诚实地说:目前六个服务全部能跑通主流程,少数冷门功能(比如文件上传)偶尔要回主站操作。
4.2 登录态维护:靠独立分区不如直接在正常窗口登录
很多人在初次使用我这个项目时会遇到"侧边栏里没登录,要我扫码/输入密码"的情况,然后产生质疑:我不是刚在主窗口登录过吗?原因我在 2.3 节提到过——iframe 的存储分区默认不允许访问父页面的 Cookie。打开一个新的 AI 面板 iframe,它内部是全新的存储环境,相当于第一次来这个站点,自然没登录态。
这里有一个我踩了不少坑才理清的决策点:要不要给 iframe 设置allow="same-origin"并试图共享会话?不同站点策略不同,有些服务允许第三方上下文访问主站 Cookie,但越来越多的站点把 Cookie 设为SameSite=Lax甚至SameSite=Strict,跨 iframe 上下文里根本带不过去。与其跟浏览器较劲,不如让用户直接在主窗口打开一次目标站点并保持登录,然后扩展侧边栏里的 iframe 通过partition分区的机制登录一次并让浏览器记住该分区的 Cookie。实测这样最稳定——首次在侧边栏打开 ChatGPT 时会弹出登录页,登录完成后 Cookie 保存在该分区里,之后长期有效,不必反复登录。
如果用户连这一步都嫌麻烦,那还有一条终极省事路线:在扩展的options里打开"无分区模式",这时 iframe 不设partition,它会直接走父页面相同站点上下文,主窗口登录过就能直接用。代价是各 AI 服务之间可能产生 Cookie 干扰,但从我测试看,六个默认服务之间基本没有域名重叠,所以这种模式实际用起来问题也不大。两种模式我用一个开关切换,默认推荐"独立分区",省心不打架。
4.3 后端中转服务:一个可选的轻量 Node 转发层
为了给技术背景更强的用户留一条后路,项目里还带了一个完全可选的 Node.js 中转服务(server/目录)。它做的事情很简单:接收扩展端发来的请求,携带目标 AI 服务的 URL 和请求体,由 Node 服务端发起请求,等到完整响应后再原样返回给扩展。因为请求是从服务端发出的,不存在浏览器 CORS 限制,也绕过了大部分frame-ancestors问题——注意我这里的"绕过"指的是服务端代理,不是修改响应头欺骗站点。
这个中转层的代码量不大,核心就几十行:
const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const app = express(); app.use('/proxy/*', createProxyMiddleware({ target: 'https://target-ai-service.example.com', changeOrigin: true, pathRewrite: (path) => path.replace(/^\/proxy/, ''), onProxyReq(proxyReq, req, res) { proxyReq.setHeader('User-Agent', req.headers['user-agent'] || 'Mozilla/5.0'); proxyReq.setHeader('Cookie', req.headers['cookie'] || ''); }, onError(err, req, res) { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end('Proxy error: ' + err.message); } })); app.listen(8787, () => { console.log('AI sidebar proxy listening on http://localhost:8787'); });但我要真诚地提醒一句:启用中转意味着所有对话内容会经过你自己搭的服务器,存在隐私泄密的额外风险。所以我默认关闭这个能力,只开放给能自行部署、了解风险的技术用户。大多数情况下,纯 iframe + 独立分区已经够用来完成核心功能。
5. 开源项目实战指南:从克隆到接入你自己的 AI 服务
5.1 代码结构和二次开发入口
项目目录结构刻意保持精简,方便二次开发:
multi-ai-sidebar/ ├─ manifest.json ├─ src/ │ ├─ background.js │ ├─ sidepanel.html │ ├─ sidepanel.js │ ├─ providers/ │ │ ├─ config.js # 所有 AI 服务配置(URL、方式、选择器) │ │ ├─ iframeController.js │ │ └─ promptSync.js │ └─ lib/ │ └─ domUtils.js ├─ server/ │ └─ proxy.js └─ docs/ └─ CUSTOM_PROVIDER.md想接你自己公司内部的 AI 服务,或者换成其他公开服务,只需要改providers/config.js。配置字段包括:
id:唯一标识,必须是英文小写+短横线name:显示在图标上的名字url:目标 AI 服务的地址embedMode:iframe或proxypartitionKey:存储分区键inputSelector:主输入框的 CSS 选择器submitSelector:发送按钮的选择器
拿添加一个新的服务举例:假设你想接入某内部 AI 助手,它没有 CSP 限制能直接 iframe 嵌入,配置大概是:
{ id: 'internal-ai', name: '内部助手', url: 'https://internal-ai.example.com', embedMode: 'iframe', partitionKey: 'trusted-internal', inputSelector: '#chat-input', submitSelector: '#send-btn' }改完配置,在sidepanel.html的图标栏里加一个对应的<button>即可。整个流程不涉及编译打包,改完刷新扩展就能看到新图标。这个简明接入方式对非前端背景的人也友好,基本就是复制粘贴改字段。
5.2 加载未打包扩展的步骤与调试技巧
本地开发调试时,不需要提交到 Chrome 应用商店,也不需要付费开发者账号,直接"加载已解压的扩展程序"。步骤是:打开chrome://extensions/,右上角开启"开发者模式",左上角点"加载已解压的扩展程序",选择项目根目录即可。
有几个调试工具值得提前了解,不然出问题会非常抓瞎:
- service worker 调试:点击扩展卡片上的 "service worker" 链接,打开 background 的 DevTools,能看到所有后台日志。
- 侧边栏面板的 DevTools:在侧边栏页面上右键选择"检查",可以调试面板自身的 DOM、网络请求。
- 主要看 Network 面板:iframe 内的请求会展示在面板的 Network 中,重点看有没有 CORS 报错、302 跳转异常、403 或 401 状态码。遇到 401 或 403,基本就是登录态或 Cookie 隔离问题;遇到
Refused to connect,就是 CSP 限制,考虑改用 proxy 模式。
调试过程里,我个人最常用的技巧是:在background.js里加一段监听所有tabs.onUpdated的日志,确认每个 iframe 的实际加载 URL 和最终状态。因为有些服务会在加载后做几次 302 跳转,比如从chat.openai.com跳到auth.openai.com再跳回,你不看跳转链路就永远想不通为什么白屏。
5.3 我对项目做过的两个性能优化
最后补两个我实测有效的性能优化点。
第一个是iframe 预连接。第一次打开侧边栏时,六个服务的域名都在后台触发chrome.tabs.create({ url: provider.url, active: false })进行预热吗?不是的,这样会真的创建不可见标签页,非常愚蠢。正确做法是用chrome.networking相关的 API 做 DNS 预解析?可惜扩展里没有这个 API。我的方案退而求其次:只在用户点击图标时先快速创建 iframe 但不放在 DOM 里,靠preload方法触发资源加载。这样做后首次点击的加载感知时间大约缩短 30%。效果不算惊艳,但聊胜于无。
第二个优化是内存回收。长时间开着侧边栏,6 个 iframe 加上各自内部繁重的 JS 运行,内存占用能达到 1.5GB 以上,这在小内存机器上会有明显卡顿。我的策略是:切到某个 iframe 后,如果超过 15 分钟没有操作,就临时把它的src设为about:blank并从 DOM 里移除;下次点图标时重新加载。这等于"降级保留"——你有记录但页面不驻留,换取内存腾退。折中的结果是内存占用被控制在 400MB 以内,对主流电脑压力不大。为了防止用户觉得"怎么又要重新加载",我在对应图标上保留一个半透明的蓝点提示"已休眠,点击恢复"。
6. 真实体验一个月后的总结与几个必须说的注意事项
这个项目断断续续用了有一个多月,我自己的实际体验是:单一 AI 的日常咨询我基本都在侧边栏里完成了,不需要打开完整标签页。主窗口保持干净,只干正经活;AI 对话放在侧边栏,随时调用、随时收起。对比多个模型回答时,开并排模式并启用 Prompt 同步,一次提问拿六个回答,效率非常高。客观讲,这也带来一个副作用:选择变多反而更容易陷入"缝合答案"的纠结,一件事情问六个模型其实大多数时候结论高度一致,真正值得对比的通常只有观点类、代码 bug 类、文本润色类这三类场景。
几个要跟后来者明确交代的注意事项:
- 存储分区的登录态不等于安全隔离。
partition只是隔离数据,不代表这些网站无法通过浏览器的其他途径获取你的指纹信息。别以为侧边栏打开 AI 服务就跟主站"完全无关"了。 - Prompt 同步不保证在所有 AI 上输出完全一致。不同模型的输入框可能有多行占位符、Markdown 快捷指令等差异,我尽量做了兼容,但依然有用户反馈某些特殊字符(比如较长代码块)粘贴后被目标站点截断。如果你每天要用这个功能,请尽量让 Prompt 控制在 3000 字以内,超过这个长度建议手动确认一遍。
- 扩展的更新与稳定性之间需要取舍。作为开源项目,我会持续跟进各家 AI Web 应用的 DOM 结构变化,更新选择器和配置。但 Web 前端的变动属于常态,不可避免会有某一天某个 AI 服务改版后同步输入失效。遇到这种情况,打开扩展的 issues 页面报告一下,我通常会在几个工作日内修复。
最后分享一个小技巧:在并排模式下,如果某个 AI 的回复特别长、挤占了其他 AI 的可视空间,可以双击它的标题栏单独放大该栏、其他栏隐藏,再次双击恢复并排。这个小功能代码只有十来行,但几乎每天都会用到。以我的个人习惯,这个项目最大的价值并不是"能嵌 6 个 AI",而是让我从标签页焦虑里解放出来,更专注地思考问题本身。建议下载源码后先不改任何代码,直接加载运行,感受一下侧边栏 AI 工作流是否适合你,再决定要不要改造。如果你有其他想接入的服务,或者遇到什么奇怪的嵌入问题,欢迎在项目仓库的 issues 里交流。