Featured image of post Neko 源码级拆解:把浏览器变成一条视频流,以及「只出像素」为什么是关键

Neko 源码级拆解:把浏览器变成一条视频流,以及「只出像素」为什么是关键

Neko 在 GitHub 上的自我介绍是「一个自托管的虚拟浏览器,跑在 Docker 里,用 WebRTC」。22.5k star,Apache-2.0,Go 写的,2020 年 3 月建仓,到现在 13 个 release、59 个 tag。

按这个描述,它很容易被归到「远程浏览器」或者「看片派对工具」那一类里去。但如果你仔细看它的架构,会发现它做的动作比那两句话大得多,而且这个动作恰好补上了 agent 流水线里一个空位。

一、核心动作:把桌面降维成一条媒体流

Neko 的架构文档里有一句关键描述:它把一个虚拟桌面环境从 Docker 容器里串流到浏览器,走 WebRTC。注意是虚拟桌面,不是只串一个浏览器窗口。

系统分四层:

层 职责
客户端 Vue.js 单页应用,负责把流解码播放,把用户的输入事件回传
服务端 Go 写的进程,管会话、管房间、管权限
媒体采集管线 从虚拟桌面抓画面、编码、广播给所有观看者
桌面控制 把输入事件送进虚拟 X 桌面

仓库里的文件直接对应这套分层,读一遍就能看出重心在哪:

路径 职责
server/internal/capture/manager.go、broadcast.go、streamsink.go 采集与广播管线,整个项目最核心的一块
server/internal/webrtc/peer.go、metrics.go WebRTC 连接与质量指标
server/internal/session/manager.go、session.go 会话与房间
server/internal/desktop/manager.go 虚拟桌面控制
server/pkg/types/codec/codecs.go 编解码器类型
server/cmd/serve.go、server/neko.go 启动入口
client/src/store/remote.ts、client/src/components/video.vue 前端输入回传与视频播放

三个 capture 文件加一个 codec 目录,占了最重的分量。这说明 Neko 本质上是一个媒体系统,附带了一个浏览器,反过来看会理解错它的重心。

为什么是「桌面」而不是「浏览器」

这个区别不是措辞上的讲究。因为串的是整个虚拟桌面,容器里可以放任何 GUI 程序,浏览器只是其中最常见的一个。这解释了它的镜像为什么按 <application> 组织、还带 <flavor> 变体:firefox 是默认别名,此外还有基于其他浏览器、甚至其他应用镜像的版本。你想让用户通过一条流访问的东西,可以是一个网页,也可以是一个桌面软件,架构上不需要改任何东西。

也正因为是桌面,输入事件的处理必须是通用的(鼠标、键盘、剪贴板),抽象层次停在坐标上。这跟浏览器自动化工具差了一整层:后者知道什么叫「一个按钮」,前者只知道「坐标 (x, y) 上发生了一次点击」。这个层次差别决定了后面所有的能力边界,包括为什么它能接住一个真人在操作。

二、这个选择解锁了什么

把桌面降维成媒体流之后,一堆原本很难做的能力变成了「流」的问题。

多用户共享同一个浏览器实例。 因为输出是一条流,扇出(fan-out)就是分发流,不需要为每个观看者复制一份浏览器。十个人看同一个浏览器,成本几乎等于一个人。

录制与转播。 官方支持通过 RTMP 把房间内容推到 Twitch、YouTube 这类平台,而且没有人在线时流也照跑,所以 24 小时无人值守广播是它能干的活。这条能力之所以成立,同样是因为内部已经是一条媒体管线,往外推一份到 RTMP 只是多接一个 sink。

权限回收变成断流。 访问控制在这个架构里很干净:不再给你推流,你的画面就停了。不需要撤销 cookie、不需要踢会话、不需要清理对方机器上的任何东西。

和 VNC 那类方案的分野也在这里。 远程桌面协议的设计目标通常是「一个操作者控制一台机器」,所以它的抽象是控制通道。Neko 的抽象是媒体流,而流的天然属性是多人、可录制、可转发、可中断。同一份底层技术(抓画面 + 编码 + 传输),抽象选得不同,能长出来的产品形状就完全不同。

三、「只出像素」的信任模型

这是我认为 Neko 最被低估的一点,而且它对 agent 场景的意义比它宣传的那个卖点更大。

官方的说法是把这条当知识产权保护来讲:WebRTC 只共享视频和音频,不共享真实数据;别人没法反编译你的代码,因为代码根本不在他的机器上运行;访问权你可以随时收回。

这个说法是对的,但放到 agent 场景里,它还有一层更实际的推论:

当人和 agent 协作时,人的那台机器上永远不会有页面代码、token、cookie。只有像素。

这件事为什么重要,看看替代方案就清楚了。如果让用户在自己的浏览器里登录一个站点,然后把会话交给 agent,那么要么你要求用户把凭据交给你(大多数人不该这么做,你也不该要),要么你让用户在自己的浏览器里操作、再想办法把会话搬出来(cookie 导出,安全上很脏)。

而 Neko 这条路是:真浏览器在服务器上,用户看到的是它的画面,用户在画面上敲的东西变成输入事件回给那个浏览器。凭据从头到尾没有离开过服务器,用户的机器上只有一段视频。 于是「让用户自己登录」第一次在安全上说得通。

需要说清的边界:这条性质保护的是数据和代码的传输路径,不保护画面内容。用户屏幕上看到的仍然是一个真实的浏览器窗口,所以页面上可见的东西(表单里已填的内容、页面上显示的验证码、错误提示)照样被看到。它解决的是「凭据不落地到用户机器」,不解决「画面里有没有敏感信息」。别把它当成一个更宽的承诺。

四、可拦截的自动化:人和 agent 操作同一个实例

Neko 的官网在功能列表里有一行「Automated Browser」,写的是:装 Playwright 或 Puppeteer 来自动化任务,同时能够实时介入。

这一行值得展开,因为它对应的是 agent 出问题时最省事的一种兜底形态。

常见的兜底做法是:agent 跑在无头浏览器里,遇到需要人的步骤就把状态导出,交给一个真浏览器让用户操作,用户做完再把会话导回来。这个流程里有两个进程、两次状态交接,每一步都可能丢东西(会话、页面状态、DOM 与视觉的不一致)。

Neko 的形态把这件事消掉了:Playwright 跑在容器里,人看着同一条流,随时可以接管。 人和 agent 操作的是同一个浏览器实例,不存在「交接」,因为从来就没有分开过。用户接手时页面已经在正确的状态上,用户做完 agent 继续,中间没有同步动作。

对一个要求「agent 出错时人能马上接管」的产品,这个架构省掉的是整整一层状态同步逻辑。

五、代价:单位成本与 codec 的隐藏复杂度

Neko 的代价也很清楚,而且我认为比多数介绍里说的更实在。

单位成本降不下来。 每个实例里跑的是一个真浏览器加一个虚拟 X 桌面,再加视频编码。这套东西的内存占用不可能接近无头方案。所以它的合理位置是「需要人的那一段」,把它当成全流程方案会贵得没必要。

codec 是隐藏的复杂度。 从源码结构看,pkg/types/codec/codecs.go 加整条 capture 管线是项目最重的一块,这不是偶然。WebRTC 串流要在画质、延迟、CPU 三者之间取舍,而这三者的最优解取决于你的场景(交互式操作要低延迟,看片要画质)。这块调优是运维层的主要工作,但在多数介绍里被一句「用 WebRTC」带过了。

镜像矩阵本身也是个决策。 它的镜像命名规则是:

ghcr.io/m1k1o/neko/[<flavor>-]<application>:<version>

<application> 是浏览器或基础镜像,<flavor> 是可选变体,版本走语义化(latest、MAJOR、MAJOR.MINOR、完整版本号)。官方提供两个注册表:GHCR 有全部 flavor 和多架构的稳定版,Docker Hub 只有不带 flavor 的 AMD64 开发版。

这里有一条容易被忽略的文档提示:Firefox 系的浏览器不需要额外的 capabilities 或更大的共享内存就能稳定运行,Chromium 系的需要。 这不是小事,它直接决定你的容器配置和稳定性。官方把 m1k1o/neko:latest 保留为 firefox 的别名,多半也是因为这个默认更稳。

六、一个端到端的登录场景

把上面几节拼成一个具体流程,会更清楚它的位置。假设 agent 要替用户去一个需要登录的后台取数据。

  1. agent 先在便宜的那一层把能做的做完,跑到「需要登录」这一步停下。
  2. 起一个 Neko 房间,里面是一个真浏览器和真桌面。
  3. 让浏览器停在登录页,把房间链接给用户。
  4. 用户在自己浏览器里看到的是画面,在画面上输入账号密码。凭据只存在于服务器上的那个浏览器里。
  5. 用户说一声好了,agent 从同一个实例继续,此时页面已经在正确的状态上。

这个流程里最值得看的是第 3 步和第 5 步。如果实现方式是在无头层和真浏览器之间来回切换,那么第 3 步要复现页面状态、第 5 步要把会话搬回去,中间每一步都可能丢东西(cookie、已填的表单、DOM 与视觉的不一致)。而在「一开始就用真浏览器」的形态里,这两步都不存在,因为浏览器从来没有换过。

代价是你为整个过程付了真浏览器的钱,包括那几步本来不需要人的。所以这里有个真实的取舍:要么付全价换掉交接逻辑,要么省成本但自己把交接写对。 没有第三种。

七、接入自己的 agent 时要自己决定的三件事

Neko 自己不是一个 agent 工具。它提供的是一个「可以被驱动的浏览器加一条流」,剩下的要你决定:

第一,谁驱动浏览器。 官方给的路是在容器里装 Playwright 或 Puppeteer。这意味着 agent 的驱动逻辑要跑在容器内部,而不是从外面连进去。至于容器内浏览器的 CDP 端口能不能暴露给外部客户端,我在官方文档里没有找到明确说明,这一点要实测确认。如果只能内部驱动,你的 agent 框架就要有「把代码送进容器执行」的能力,而不是「连一个远程浏览器端点」。

第二,流给谁看。 房间是多人共享的,所以「用户看」和「审计看」可以是同一条流。需要留证的时候,接一个录制就同时满足了两边,不需要额外做一遍截图或录屏。

第三,接管期间 agent 干什么。 官方说「可实时介入」,但接管时 agent 是暂停、还是继续操作、还是排队等待,这是产品语义,得自己定义。这块我没有在文档里找到现成的实现,属于「架构允许、但要自己搭」的部分。

这三点合起来说明一件事:Neko 是一个很好的底座,但把它变成 agent 的人机协作界面,中间还有一层要自己写。

八、它该放在流水线的哪一段

把这些拼起来,我的判断是 Neko 的位置很明确:它不是 agent 的浏览器,它是 agent 需要人时的那个界面。

一次典型的 agent 任务里,绝大部分步骤不需要人:抓页面、点元素、填表单、提取数据。这些交给无头方案更便宜(同一批文档里,从零写的无头浏览器可以把峰值内存压到无头 Chrome 的十六分之一)。真正需要人的只有几个节点:登录、双因素、验证码、支付确认、以及 agent 判断不了的分叉。

Neko 覆盖的就是这几个节点,而且覆盖得比别的方式干净:用户看到的是真浏览器画面,凭据不出服务器,agent 和人共用同一个实例所以不需要交接。

写在最后

我读完 Neko 架构的主要收获是,它的价值不在「远程浏览器」这个功能描述里,而在那个抽象选择上:把桌面降维成媒体流。这个选择让它同时能做看片派对、远程办公和 agent 的人在环,因为这三件事在底层都是「把画面推给人、把输入收回来」。

另一个收获是它的信任模型比它的定位更值钱。「只出像素」在官方口径里是防抄袭,但对 agent 场景,它恰好解决了「用户凭据怎么不经过第三方」这个一直很难处理的问题。

最后交代证据边界:以上来自 Neko 的官方文档、架构说明和仓库文件结构,我没有实机跑过。要验证最小的一组是:起一个 ghcr.io/m1k1o/neko/firefox:latest 容器,用两个浏览器标签页连进同一个房间,确认两边看到的是同一个画面;然后在容器里装 Playwright 跑一个多步任务,中途从另一个标签页接管,看接管时页面状态有没有丢。

相关阅读:

By AI博士 万戈