跳到正文

Chat On Steroids

一个给 ChatGPT 装上本地工具的桌面工作台。它用 MCP 提供文件、shell、终端和整个桌面,再用一个 Chrome 扩展通过隧道驱动用户自己那一个 ChatGPT 对话页面:模型因此能改真实项目,而 app 记录每一次工具调用,并在旧对话装不下时把会话搬进一个新对话。

Screenshot of Chat On Steroids
编辑截图, 1 Oct 2026Chat On Steroids ↗

这是什么

一个围绕 ChatGPT 做的 Electron 工作台,而不是自己实现一个编码 agent。它在本机跑一个 MCP 监听,再通过隧道把它交给用户自己那一个 ChatGPT 对话,于是模型可以在已批准的文件夹里读写文件、执行命令、一直开着终端、还能操作桌面——与此同时,一个配套的 Chrome 扩展在观察并自动操作 ChatGPT 页面本身:替用户输入、发送、记录每一轮。worker 是这个 app 复用的另外一些 ChatGPT 对话,用来分派独立任务;Goal 继续未完成的工作,Loop 在同一个任务范围内一直往下推;对话太长时 Compact & Resume 把同一个本地会话从旧对话搬到新对话,worker 的历史一并带走。本地工具到底做了什么以 app 为准,扩展从不执行它。它支持 Windows 10 与 11、macOS 13 或更新版本,以及桌面 Linux,许可是 MIT;README 自称独立 beta,与 OpenAI 无关联、也未获其背书,本地工具受用户自己批准的那些文件夹与能力开关限制。

谁做的仓库挂在另一个账号 totec448-spec 名下,工作由两个账号分担:876 次提交里 Maximapple 记 569 次,所有者账号 159 次,合计 730 次;按提交作者名统计,其中 571 次写作 Maxim。两个账号在材料里都没有真名。此外的贡献者名单共 27 人,最多的是 Haz4rdovisk 的 31 次、ayhanmalkoc 的 16 次与 hhh2210 的 15 次。

它是怎么搭起来的

组成 · 6

四块协作平面,每个事实只有一个主人。Core、Desktop 与 Plugins 是同一个本地监听后面的三个逻辑 MCP 面,各由秘密路径寻址,并接受实时权限与调用者凭证的检查;浏览器桥是另一个有独立认证的回环服务;可选的控制 API 是第三个监听,只服务可信的本地调用者,而且即便到那一步也只投影状态、或调用两个已经存在的主人。工具到底做了什么由主进程说了算,扩展只观察页面并编排浏览器,渲染进程既拿不到文件系统、命令、密钥,也没有通用的主进程权限——它只有一套固定的 preload API。这套系统是被设计成按身份而不是按症状去调试的:agent 地图逐条列出每个边界上必须活下来的身份——已批准根加规范真实路径、归一化后的请求 id、请求 id 到对话 id 再到会话 epoch、对话加 Chrome 文档 id 加导航 epoch、outbox id 加选定的浏览器主人——并告诉读者:当四个功能一起坏掉时,要顺着其中一条身份穿过这些边界,修掉第一个错的事实,而不是最后一个显示它的界面。存储寿命遵循同样的规矩:每个仓都被写明它能活过什么、证明不了什么,而任何要比进程活得久的东西,都在被依赖之前先写下去。

src/main/
149 个文件、3.28 MB,装着 src/ 下最大的文件——537,795 字节的 bridge.ts——旁边是 agents.ts(226,494)与 goal.ts(148,955);session/ 里 store.ts(191,166)、recorder.ts(132,649)与 input.ts(123,707)守着耐久身份;mcp/ 里有 tools-core.ts(116,939)与 kernel.ts(79,131),以及浏览器、桌面与插件各自分开的工具面;另有一套兼容 Codex 的补丁与命令执行实现,与 durable.ts。
extension/
14 个文件、1.37 MB:隔离世界里的 content.js(678,238 字节)、chatgpt-dom.js(161,384)与 45,343 字节的浏览器控制,页面世界里的 fiber.js(144,035)与 usage.js(25,547),作为可挂起 service worker 的 background.js(239,061),1,784 字节的 manifest.json,以及 8 个各 38 到 48 KB 的 _locales 目录。
src/renderer/ 与 src/preload/
工作台本身共 90 个文件、4.79 MB:299,278 字节的 chat.ts、95,694 字节的 main.ts、100,995 字节的 index.html 与 206,463 字节的样式表、79,468 字节的文件面板、十个各 142 到 180 KB 的界面语言文件、宠物浮层,以及一个 23,899 字节、作为渲染进程唯一入口的 preload。
test/
按体积算是最大的代码目录:283 个文件、6,420 KB,单个测试文件就有 1,037,734 字节(content-script.test.ts)、740,223(bridge.test.ts)、271,784(renderer-timeline.test.ts)与 203,385(session.test.ts),另有把重新实现的 Codex 补丁、命令执行与看图工具钉在原有行为上的对照测试。
docs/ 与 AGENTS.md
docs/ 下 101 份文档、742 KB,旁边是 303,241 字符的 AGENTS.md、146,821 字符的变更日志与 34,531 字符的贡献者墙。文档大多是带日期的过程笔记——约七十份 worklog、四份 bug audit、两次 bughunt、一份 29,954 字节的 ChatGPT 回合信号研究,以及 docs/pet/:136 个文件、44 MB 的精灵批次与动画帧。
.github/ 与打包
14 个工作流、43 KB——19,312 字节的发布流程、7,140 字节的 publish 作业、持续集成、CodeQL、verify-release、pr-triage、pr-checklist、needs-info、waiting-prs、similar-issues、每周摘要与欢迎——加上三个装好的 git hook、83 个构建与校验脚本、一份 Electron 打包配置、一个 Nix flake、83,126 字节的 Swift 桌面助手,以及一份 10.5 MB 的第三方声明文件。

取舍,以及它替代了什么

  • 借用户自己的 ChatGPT 对话,走隧道进去 替代 调用模型 API,或直接调用 Codex

    README 把这写成产品前提——「Uses your ChatGPT conversation rather than invoking Codex directly」——并补一句 ChatGPT Work 与 Codex 共用额度,所以账号的模型可用性与上下文上限照样生效。本地能力挂到用户本来就有的那个对话上,经由一个通过隧道抵达的 MCP 监听。

  • 给浏览器桥单独一个回环服务与独立认证 替代 让同一个 MCP 监听连浏览器通道一起扛

    agent 地图数出四块平面,并让浏览器桥握有与三个 MCP 面不同的认证,而后者各由秘密路径寻址并接受实时权限检查。面向模型的工具调用与页面编排因此各有各的凭证与失效方式,这也是浏览器那一半能在不碰工具那一半的情况下被重启的原因。

  • 扩展只观察与编排,绝不执行工具 替代 让页面把它请求的那个本地调用跑掉

    这条规则写在 agent 地图里:扩展「never executes the model’s local tool」,本地工具究竟做了什么由主进程说了算。页面随时可能被刷新或跳转,记在页面上的结果算不了证据。

  • 把状态写在崩溃弄不丢的地方 替代 把队列与会话留在内存里

    outbox 在投递前就把输入冻结下来,会话仓与具名状态文件是活过重启的东西,扩展则把日记、标签页登记表与命令回执放进一个能活过 worker 挂起的存储。同一份文档也把每个存储证明不了什么写得一样清楚,于是重启永远不会被误当成页面还活着。

  • 把 Electron 钉死,并让它离开自动更新 替代 接受那次成组的依赖升级

    第三方声明与原生许可文档写明了确切的 Electron 源码标签,而一个打包测试证明发出去的 runner 用的就是那些字节,所以自动升上来的版本即便能构建也会在每个平台上卡住校验。Dependabot 现在跳过 Electron;同样的道理更早就用在了原生图像包上:它们连同许可复核一起手工升级。

依据AGENTS.md——一个 303,241 字符文件的头 12,000 字符,覆盖产品描述、用户的常规路径、功能词汇表、含四块平面的运行时模型、逐边界的身份表和存储寿命表——加上完整的 README.md(7,608 字符)、三十个带评论串的 issue 与 pull request、release 与 tag 清单、提交与贡献者统计,以及完整的 1,035 个文件树(含逐文件体积)与目录汇总。

制作过程

6 个阶段
  1. 01

    五周、876 次提交与二十个 release

    仓库建于 2026-08-22,最早的提交是 2026-08-24 的 Chat On Steroids 2.0.0,最新的是 2026-10-01 的一次合并。两者之间是 39 天里的 876 次提交——8 月 55 次、9 月 813 次、10 月头几个小时 8 次——以及 20 个 release,从 2026-08-26 的 v2.0.2 到 2026-09-30 的 v2.1.22,第 21 个已在一个标题为 Release 2.1.23 的 pull request 里备好。版本号是这份清单里最不有意思的部分:2.0.6 的标题是「I am exhausted.」;2.1.13 的标题是「i am very sad. openai theatend to ban my account for whatever reason. anthropic support blocks me. money tight.」;2.1.14 是「Death to Anthropic and OpenAI. Join the resistance」;v2.0.9 是「they nerfed astra」,v2.0.8 是「Darkex by dark tibo」,相隔两周的两个 release 都叫「6 sol in 2 weeks」。抱怨被当成证据提交进了仓库:docs/images/ 里放着 openai-account-warning-2026-09.png、openai-chatgpt-warning-2026-09.png 和 anthropic-support-reply-2026-09.png。2026-10-01 同一时刻它的周围是:4,217 个星、560 个 fork、12 个 watcher、15 个打开的 issue、元数据体积 75,191 KB,以及一份已经长到 146,821 个字符的变更日志。

  2. 02

    入口是隧道后面的 MCP,而不是 API

    README 把上手写成四步:装好 app 并在 Settings 的 Workspace 里批准一个项目文件夹;在 Settings 的 Setup 里连上 Core,然后在 ChatGPT 的 Plugins 里依次点 Add、Create MCP App 把它加进去;把配套扩展以 unpacked 方式载入,配对是自动的;然后选模型、写任务、发送。被这四步藏起来的东西写在项目自己的 agent 地图里:那里数出四块协作平面——Core、Desktop 与 Plugins 是一个本地 MCP 监听上的三个逻辑面,各由一个秘密路径寻址,并接受实时权限与调用者凭证的检查;浏览器桥是另一个回环服务,有它自己的认证;可选的本地控制 API 是第三个监听,只服务可信的本地调用者。传输靠一个由构建脚本抓取的隧道客户端,app 侧是 src/main/tunnel/。这条路线的理由 README 也写了:「Uses your ChatGPT conversation rather than invoking Codex directly」,并注明 ChatGPT Work 与 Codex 共用额度,所以套餐、模型可用性与上下文上限照样生效。好几个 release 只讲这条链:2.1.19 是「The browser connects on its own again」,2.1.18 是「Recommended skills install again」;而 2026-10-01 的一个 issue 里,扩展面板显示 app 已连上 8769 端口,同时它刚拉起的 worker 却仍然看不到任何本地工具。

  3. 03

    扩展才是真正碰页面的那一半

    这个配套件是一个 Manifest V3 扩展,14 个文件、约 1.4 MB,ChatGPT 页面真正被读和写的地方就在它里面:隔离世界里是 678,238 字节的 content.js 与 161,384 字节的 chatgpt-dom.js,页面自己的世界里是 fiber.js(144,035)与 usage.js(25,547),而 239,061 字节的 background.js 是那个会被挂起的 service worker,握着日志、标签页与文档登记表、认领与回执。agent 地图明确写着扩展「never executes the model’s local tool」,还给浏览器定了一套节约规矩:复用一个可用的文档;一次操作在整个导航与 worker 挂起期间只拥有一个被选中的标签页;已经安定下来的、由 app 打开的页面在停止工作两分钟后可被新对话复用、五分钟后可自动关闭,固定标签页与用户最近的访问都能否决这两条。页面世界这件事不是理论问题:#851 被评审时,附注正是 usage.js 活在页面世界里,所以更新之后开着的 ChatGPT 标签页需要刷新一次。这个扩展带 8 种语言,app 另有 10 种加英文——它自己的 issue #855 把这算成 11 种语言。release 2.1.17 的宣传语是「an extension that updates itself」,1,784 字节的 manifest.json 里带着 app 会去核对的版本号。整个扩展里还有 45,343 字节的浏览器控制与 39,608 字节的浮层样式,8 个 _locales 目录每个 38 到 48 KB。

  4. 04

    所谓耐久,就是在需要之前先写下去

    要比一个进程活得久的东西必须落到盘上,agent 地图写清了落在哪里。最清楚的一例是队列:app 在投递之前先把一条输入冻结进它的 durable outbox,所以「已排队」「已放进输入框」和「ChatGPT 已接受」是三个不同的事实,而不是一个乐观的状态。这份文档随后逐个存储地写明它能活过什么、又证明不了什么——app 的 sessions/ 目录与具名 state/ 文件能活过重启,却证明不了某个旧页面正在运行;扩展的 storage.local 能活过浏览器重启,装着配对意图与命令回执;storage.session 能活过 worker 被挂起,这对日记与标签页登记表已经够用;内容脚本的内存只活过一个文档。本地会话才是耐久身份,ChatGPT 对话是可替换的前端——它的 id 明确不是会话 id——这正是 Compact & Resume 能把同一个会话从对话 A 搬到对话 B 的原因。等待中的对话与休眠的 worker「保留它们耐久的历史与身份,而不是一个无限期的浏览器标签页」;恢复也被刻意收窄:它只帮 app 仍能证明自己欠下的那些工作,而不是任意旧对话。仓库里有 7,973 字节的 src/main/durable.ts 和 6,678 字节的测试;真正微妙的是一个边界——关掉一个对话的标签页会暂停自动修复,而 #853 给用户明确按下的 Compact & resume 开了例外,因为按下它就是用户回来了。这里面有两个文件专门守这件事:191,166 字节的 src/main/session/store.ts,以及那个 7,973 字节的 durable.ts。扩展那边的日记只写到 worker 被挂起为止,整个浏览器重启就得靠 storage.local 里那点配对意图与延后复活的元数据。

  5. 05

    权限:批准过的文件夹,与模型不许碰的那个窗口

    访问权是按文件夹、按能力逐项由用户给出的:全新安装会打开 Core 的能力与两个 worker,Windows 上还会打开 Desktop 权限,而 shell 命令以普通用户权限运行。这些边界是拿真机器量出来的,不是嘴上争出来的。issue #863 记录了平台之间的不一致——macOS 上后端跑在 app 进程里并跳过自己,于是用 app 自己的名字去列窗口时,9 个窗口里返回 0 个,而 app 窗口明明是开着的;Windows 上助手是另一个 PowerShell 进程,于是 CoS 自己的窗口被列了出来,其中就包括那个放着 CoS 自己权限开关的窗口。修法是把 app 的进程号传给助手。issue #844 想要 WSL 项目,答复很窄:本地发行版下一个已存在的文件夹可以经由发行版主机的 UNC 路径获批,任意网络主机继续拒绝,已批准根之外的 UNC 输入在任何查询之前就被拒,只有发行版名做大小写折叠,因为把整个 Windows 都折叠会把只差大小写的两个 Linux 文件夹并成一个。当 #851 开始观察失败的 resume 时,评审加上的条件是只观察那个确切同源的请求——它的对话 id 与 HTTP 状态,「never headers, cookies or content」——并且把信号钉死在单个请求、文档、对话、问题与轮次上。桌面控制在两个平台上也是两套实现:83,126 字节的 Swift 助手给 macOS,Windows 那边是若干独立模块加一个 PowerShell 助手。README 的使用须知把话说明白了:本地工具、浏览器控制与插件都不该被用来绕开服务方基于安全或额度作出的决定,一个本地权限或一个已启用的 MCP 连接器,都不是推翻服务方拒绝的许可。

  6. 06

    社区那一轮修了什么,维护者又收回了什么

    报告里的三十个 issue 与 pull request 是最新的三十个,而它们全部落在 2026-09-30 到 2026-10-01 的十一个小时之内——一个晚上的分诊、评审与发布。其中的修复大多来自外部:AcureroAdrian 报了 #844 到 #847,又提交了回应它们的 #848、#850、#851。评审很具体,而且谈的是成本。在 #849 上,维护者认可那条规则,却反对 errors() 会在每个 recorder tick 上跑、为页面里每一个按钮最多向上走八层祖先去读 innerText,而光侧边栏就可能挂着几百个对话按钮;贡献者因此加了预过滤,并补了一个断言300 个侧边栏按钮产生零次 innerText 读取的测试,894 个测试通过。规则由工作流执行:它用 CONTRIBUTING.md 的简短版迎接第一次来的人——关联 issue、一个 pull request 只讲一件事、一个在没有你的改动时会失败的测试、界面改动要附前后截图。有人来要 Discord,#864 里的回答是没有,也没有计划——GitHub 就是那个地方。同一条串里还留着值得记下的一句认错:在建议用户「开个新对话」之后,维护者写道「starting a new chat is not the real fix. Sorry for that suggestion.」。实机测试是被展示而不是被声称的——#860 记录了把自动压缩阈值降到 10,000 tokens 再改回来,正是这样才发现一轮回答结束之后压缩迟了六分钟。同一条串里还留着一份实机日志:Goal 的草稿请求报 timeout_or_cancelled 失败,而维护者的解释是,Goal 或 Loop 开着的时候 CoS 会按住每一轮回答的结束,直到它决定好下一步要不要继续说。

相关档案

全部档案 →