Noi 编程实战:Fable 没那么强,GPT 也没那么弱


今天修了一个很折磨人的 BUG。使用 Fable 5 Max 进行修复时,因为牵扯到从插件源码反推插件运行时边界,直接因安全问题拒绝请求,跳到 Opus 4.8。注:如果自动从 Fable 跳到 Opus,别手动改模型,它会让之前的缓存失效。

Noi 简介

Noi 正在从浏览器架构转向 AgentOS(中间跳过了 AI 浏览器,本来计划做的,但没必要了,它会被 AgentOS 所覆盖,为此我已经重写了 N 次架构)。

浏览器仍然重要,但它只是 Agent 连接网页、插件和在线服务的能力表面,不应该成为产品边界。未来强大的 Agent 会运行在本地桌面或服务器环境中,持续使用文件、终端、浏览器、工具、记忆和外部服务,完成真实任务,并把过程沉淀为可审计、可回放、可恢复、可进化的执行闭环。

因此,Noi 的架构目标也从做“更智能的浏览器”升级为“本地优先的 Agent 运行底座”。浏览器、终端、文件系统、插件、记忆、IM、IDE、网页和移动端,都应围绕同一套 Agent runtime 组织:向内连接真实环境和能力,向外投影不同分发面,最终让 Agent 从回答问题,走向在可信边界内持续完成工作。

背景知识

吹那么大牛,下面进入主题。为了让大家能看懂这篇文章在写什么,以及我为啥如此激动,还是有必要先介绍点背景知识。

要做这样一个 Agent 运行底座,运行 Chrome 插件也是必要能力之一(插件生态太庞大了,在 Agent 时代更不应该被丢弃)。Noi 使用 Electron 作为底层技术栈,它自带 Chromium 内核,理论上可以运行 Chrome 插件。但去看 Electron 文档(Chrome Extension Support[1]),就会发现它对插件 API 的支持并不完整,只覆盖其中一部分。所以,如果要尽可能兼容更多常见插件,就必须在 Electron 中补一个中间层,让插件在调用原生不支持的 API 时可以降级到自实现 API。

插件有很多,我选了几个典型插件来做开发测试。比如 Google Translate、Tampermonkey(油猴脚本)、AdBlock(广告拦截),如果它们可以运行,那基本可以覆盖大部分插件场景。

📌 补充知识点Chrome 插件数量很多,不适合按插件清单逐个验证。更合理的方式,是选择几个能力覆盖面足够大的典型插件,通过它们验证底层插件系统是否具备通用支撑能力。这里选择 Google Translate、Tampermonkey 和 AdBlock,是因为它们分别代表了三类高频插件形态:内容增强型插件、用户脚本平台型插件、网络治理型插件。

Google Translate 代表的是内容增强场景。它需要读取网页内容,在页面中注入交互 UI,例如翻译按钮、浮层和提示框,同时还要和后台运行时通信,调用外部翻译服务,并保存用户语言偏好。它覆盖的是大量“看懂页面并增强页面”的插件能力,比如划词翻译、阅读辅助、网页批注、页面摘要、表单增强、内容采集等。如果这类插件能稳定运行,说明插件系统已经具备基本的页面感知、页面改写、外部服务调用和用户交互能力。

Tampermonkey 代表的是更复杂的用户脚本平台场景。它更像插件里的脚本运行平台,要承载用户脚本的运行、管理和权限控制。它需要根据 URL 规则匹配网页,在不同加载阶段注入脚本,区分 isolated world 和 main world,管理脚本的安装、启用、禁用、更新和持久化配置,还要支持菜单命令、跨域请求、脚本通信和 dashboard 配置页面。如果 Tampermonkey 能跑通,说明插件系统既能支持固定插件逻辑,也能承载“用户可编程”的扩展模型。这会覆盖网页自动化、页面改造、内部工具增强、运营辅助、数据采集、个性化页面定制等大量场景。

AdBlock 代表的是网络治理和后台规则场景。它的复杂度不主要在页面 UI,而在后台生命周期、请求拦截、规则匹配、过滤列表更新、站点白名单、状态恢复和工具栏状态投影。它需要 background 或 service worker 稳定运行,需要 content script 与后台通信,需要 options 页面管理配置,也需要 badge、popup 和站点级开关向用户展示当前状态。如果 AdBlock 能稳定运行,说明插件系统已经能处理后台常驻逻辑、网络请求治理、规则存储、页面通信和可视化状态同步。这类能力可以覆盖广告拦截、隐私保护、请求改写、安全检测、企业内网规则、内容过滤等场景。

因此,这三个插件合起来,覆盖的是三组底层能力。Google Translate 验证插件能看见并增强网页,Tampermonkey 验证插件能成为可编程平台,AdBlock 验证插件能在后台治理网络和状态。它们共同覆盖了页面注入、DOM 读取与修改、popup 和 options 页面、background/service worker、runtime messaging、host permissions、跨域请求、持久化存储、工具栏状态、用户脚本、请求拦截、规则系统和生命周期恢复等核心能力。

如果这三类插件可以稳定运行,大部分普通 Chrome 插件场景都可以落到同一套能力模型里。剩下没有覆盖的,更多是少数专项能力,例如 DevTools 面板、Native Messaging、VPN/proxy、媒体捕获、身份登录、企业策略等。这些可以留给后续专项兼容,不必放进第一阶段插件系统闭环的主路径。

在 Noi 里集成 Chrome 插件这件事其实两年前都有想法了,鉴于实现复杂,就被我搁置了。现在 AI 编程基本可以覆盖绝大多数开发场景,我就想把这部分补上。在插件这块我已经投入了大量精力,和 Codex、Claude Code 掰扯了无数轮,当时心都累了…

注:我发「你自己去 debug吧,我累了」这种 prompt 作为 goal,不建议大家直接学习,因为 Noi 项目本身是 harness 架构设计,已经做好了各种规则约束,它会自己在约束下进行合理探索。

📌 Noi 架构Noi 架构学习了大量 agent 开源项目、个人思考、论文、设计模式,属于复杂的混合体(深度思考:架构腐朽 & Loop Engineering)。

Harness 架构是 Noi 的自进化工程闭环:AI 会先把需求沉淀为架构文档、契约和验收标准,再根据这些约束实现代码。实现过程中产生的测试结果、失败证据和运行反馈,会反向修正架构设计,推动文档、契约和代码一起演进。它也允许 AI 从外部资源中吸收设计模式,但吸收必须经过边界判断和证据验证,不能盲目复制。最终形成“意图 → 文档 → 代码 → 证据 → 架构优化”的持续循环。

BUG 描述

这里牵扯到 Noi 内部架构设计,不方便过多展开,简单来说就是 Noi 在自己造插件运行时过程中,遇到了一系列报错,大概长这样:

… [98131:0704/180618.476242:ERROR:extensions/browser/extensions_browser_client.cc:91] Extension Error: OTR: false Level: 2 Source: chrome-extension://gighmmpiobklfepjocnamgkkbiglidom/abp-background.js Message: Uncaught (in promise) Error: Chrome extension API bridge method handler is unavailable. ID: gighmmpiobklfepjocnamgkkbiglidom Type: RuntimeError Context: chrome-extension://gighmmpiobklfepjocnamgkkbiglidom/abp-background.js Stack Trace: { Line: 19042 Column: 1 URL: chrome-extension://gighmmpiobklfepjocnamgkkbiglidom/abp-background.js Function: (anonymous function) } [98131:0704/180621.844589:ERROR:extensions/browser/extensions_browser_client.cc:91] Extension Error: OTR: false Level: 2 Source: chrome-extension://nngceckbapebfimnlniiiahkandclblb/background.js Message: Uncaught (in promise) Error: Chrome extension API bridge method handler is unavailable. ID: nngceckbapebfimnlniiiahkandclblb Type: RuntimeError Context: chrome-extension://nngceckbapebfimnlniiiahkandclblb/background.js Stack Trace: { Line: 2 Column: 1 URL: chrome-extension://nngceckbapebfimnlniiiahkandclblb/background.js Function: (anonymous function) } [67479:0703/215957.592322:ERROR:extensions/browser/extensions_browser_client.cc:91] Extension Error: OTR: false Level: 2 Source: chrome-extension://gighmmpiobklfepjocnamgkkbiglidom/polyfill.js Message: Uncaught (in promise) Error: Chrome extension API bridge method handler is unavailable. ID: gighmmpiobklfepjocnamgkkbiglidom Type: RuntimeError Context: chrome-extension://gighmmpiobklfepjocnamgkkbiglidom/polyfill.js Stack Trace: { Line: 110 Column: 1 URL: chrome-extension://gighmmpiobklfepjocnamgkkbiglidom/polyfill.js Function: (anonymous function) } … 我需要将这些报错结合插件源码,去反推插件运行时实现。Noi 已经参考了Chrome Extensions - API reference[2] 在做 API 对齐,但文档不太够看,只能结合插件运行反馈来作为补充。

如果某个环节有问题,就会出现下面这些情况,loading 中或白屏,外加上面的一些错误信息:

但错误信息有时候很不准确,无法直接定位到事故点,这就是长链路调试的坑点:不能只追最响的日志,必须回到用户实际看到的现象。

正常情况下,这里应该出现插件的设置界面,比如“常规设置”、“过滤列表”、“自定义规则”等内容,像这样:

可实际看到的就是空白,日志也不友好,都是这类错误信息:

Could not establish connection. Receiving end does not exist. Chrome extension API bridge method handler is unavailable. 第一眼看,这就是消息通道问题:接收端不存在,那就补接收端;handler 不可用,那就补 handler;某个插件报错,那就修这个插件的兼容逻辑。Codex 一开始确实是这么做的,看起来每一步都专业,但方向是偏的。它在追最响的报错,还没有先证明用户看到的空白页到底卡在哪一层。

这就是这次最核心的坑:报错发生在消息层,不代表根因就在消息层。在浏览器插件这种长链路里,一个设置页能显示出来,背后至少要完成一串状态交接:

识别插件页面 URL -> 创建正确的浏览器页面环境 -> 让浏览器加载插件资源 -> 准备插件后台运行时 -> 保留浏览器原生插件 API -> 只在缺失处补兼容层 -> 页面脚本完成初始化 -> 用户看到真实界面 任何一棒掉了,用户看到的都可能是空白页。更麻烦的是,报错通常不会出现在最上游。你看到的是“消息接收端不存在”,缺口可能在更早的后台运行时;你看到的是 handler 不存在,问题可能发生在页面脚本启动时,它依赖的环境还没准备好。

所以前面很多探索都无效。修消息 API,只能证明消息 API 可能对了;补 handler,只能证明调用能进某个入口;单元测试变绿,只能证明某个函数行为正常。这些都必要,但它们不能证明用户看到的页面真的渲染出来了。真实插件页面启动时,会同时依赖 URL 加载、后台运行时、消息通道、存储、页面脚本、浏览器原生能力和应用兼容层。局部变绿,不代表整条链路闭合。

还有一个很容易误导人的现象:某轮探索里,Codex 在加载页面前等了一会儿,页面居然能出来。这个信号很危险,因为它会诱导你加 timeout。但 sleep 只能算证据,不能算修复。它说明等待期间,某个状态从“没准备好”变成了“准备好”。下一步该追的是:到底是后台进程起来了,订阅注册了,还是某个原生 API 变可用了?如果不把 sleep 翻译成明确状态,最后只会留下一个脆弱的延迟。

在多次努力无果下,我就发了这么一句提示词(真心累了):

也可能是我之前和 Codex 交流探讨了多轮,提供各种建议,让上下文覆盖到了足够大的空间,它开窍了。开始了长达一小时的探索尝试,经历 5 次上下文压缩,运行时长堪比 goal。在它运行期间我还在 Coding 群里嘲讽了一下:越努力越心酸…

转折点出现在红框里的这次 A/B 结果。它的重要性超过了运行时长,也超过了后来补上的验证脚本。

A/B 的价值在于停止继续猜。只改变一个关键条件,再看结果是否跟着变化。这里最有价值的结论是:保留浏览器原生的runtime messaging 后,页面已经能渲染出完整的 AdBlock General Options 文案;再等插件后台和连接层稳定后打开 options 页面,单次加载不再 crash,DOM 也完整了。

这句话同时打穿了两个边界。第一,浏览器已经做对的消息能力,不应该被自己的兼容层覆盖。第二,插件页面不能被当作一段孤立的 HTML,打开之前,后台运行时必须已经进入可用状态。前者是所有权问题,后者是生命周期问题。

在这之前,Codex 一直被报错牵着走。日志说消息接收端不存在,就会自然去补消息 API;日志说 handler 不可用,就会自然去补 handler。这些方向都有道理,但大多停在下游补洞。A/B 结果第一次把问题推回上游:原生能力、兼容层、后台启动和页面打开之间,责任边界没排好。

这也解释了 sleep 为什么曾经看起来有效。等 2 秒页面能出来,答案不在 “2 秒”这个数字,而在这 2 秒里发生的状态变化:后台运行时从启动中变成了可用。产品修复不能靠固定 sleep,应该在加载插件和打开插件页面之间,建立一个可观测的就绪检查:后台是否起来了,连接层是否可用,页面依赖的运行时是否已经进入可用状态。只有这些状态被证明了,再打开 options 页面,才算生命周期闭环。

这张截图已经超出了“终于跑通了”的意义。它更像问题被重新命名的瞬间:从那一刻开始,目标从消灭某条日志,变成让页面显示前的责任链闭合。

更准确地说,这个转折并非突然开窍。前面已经有很多动作靠近了真相:看过空白页,追过 DOM,试过延迟加载,修过 handler,也尝试过保留浏览器原生能力。这些动作都有价值,只是当时还停留在散落线索的层面,回答不了最关键的问题:到底是缺 API,是生命周期没准备好,还是自己的兼容层覆盖了原生本来能工作的能力?

红框里的 A/B 结果补上了“判别力”。它只改变一个关键条件,然后结果跟着变化:保留原生runtime messaging,页面能渲染;等后台和连接层稳定后再打开 options,单次加载不再 crash。到这里,证据从“某个尝试好像有效”,推进到具备因果指向:原生能力所有权和后台生命周期,是这次问题的主轴。

这也是长时间探索宝贵的地方。很多无效尝试并没有白费,它们在排除错误解释:协议可以加载,静态资源只是可能性之一,sleep 证明了时序,handler 报错暴露了后台不可用,局部测试通过也不能证明页面可用。前面其实已经接近事实,只差一个能改变判断权重的实验,把这些事实压缩到一起。

后来我让 Codex 自己复盘这次失败过程,最刺眼的一点是:它太容易把日志当成问题定义。日志告诉它哪里在喊痛,但没有告诉它哪里先断。Receiving end does not exist、handler 不存在、页面空白,这些都只是事故现场的声音。缺少一个能区分假设的验证方式,修复就会退化成补洞。补一个 API,少一条日志;再补一个 handler,又少一条日志;但页面仍然可能是空白。

这次最硬的经验是:复杂系统里的 bug,很少只是某一行代码坏了,更多是责任链断了。did-finish-load 只表示浏览器加载过,不等于用户已经看见;sleep 有效只能算时序证据,不能当修复方案;兼容层用于补缺口,不能抢走原生系统的所有权;单元测试只能证明零件,真实 smoke 才能证明系统,A/B 则负责把“看起来有效”推进到“因果上站得住”。谁负责加载资源,谁负责准备运行时,谁负责处理消息,谁负责显示结果,谁负责证明成功,这些问题没回答清楚,系统就会用一堆不同报错提醒你同一个事实:边界没闭合。

所以这次别把结论记成一句“别追报错”。报错要看,局部修复要做,长时间探索也有价值。关键在于:每一次探索有没有让假设变少,有没有把现象变成证据,有没有把证据变成责任边界。调试很少从第一分钟就看见根因,它更像是在一堆半对半错的线索里,设计出那个能改变判断权重的实验。找不到这个实验,就会一直忙;找到它,系统才开始向你交代真相。

结语

写到这里,我还是有点兴奋。这个 bug 修完以后,Chrome 插件在 Noi 里不再只是“能不能跑”的兼容问题,它开始变成 Agent runtime 的能力入口。网页增强、用户脚本、网络规则、页面状态、工具栏交互,这些原本散落在浏览器插件生态里的能力,未来都有机会被 Agent 接住,进入一个更大的执行系统里。

这也是这次经历最有意思的地方。Fable 在分析插件源码时被安全策略拦住,Codex 又在很长时间的探索后把问题跑通。它们都没有呈现出一种简单的“强”或“弱”。Fable 有能力,但会被安全边界挡住(其实没有验证之前,能力是未知的);Codex 会绕路,会犯错,也会在足够长的上下文和反馈里,把问题一点点推到正确的位置。

所以标题里的 “Fable 没那么强,GPT 也没那么弱”,不是为了替谁洗白。它更像是提醒我自己:不要把模型当成一次性答案机器,也不要在它失败时立刻归因成“降智”。复杂工程里,模型能力只是一部分,任务环境、上下文、约束、反馈和验证方式,同样决定它最后能走多远。

Agent 时代真正值得期待的,也许不是某个模型第一次就给出完美答案,而是我们能不能搭出一种工作方式:允许它探索,允许它犯错,但每一步都有证据、有约束、有回放,最后能把混乱的问题一点点逼近本质。

最后放几张成果图,插件已经在 Noi 里加载运行出来了!

References

[1]Chrome Extension Support:https://www.electronjs.org/docs/latest/api/extensions

[2]Chrome Extensions - API reference:https://developer.chrome.com/docs/extensions/reference/api

            预览时标签不可点