<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>技术拆解 on AI博士 万戈</title>
        <link>https://www.yesmiracle.net/categories/%E6%8A%80%E6%9C%AF%E6%8B%86%E8%A7%A3/</link>
        <description>AI博士万戈的技术博客，聚焦 Agentic AI、AI Infra 与 Agent Security，分享 AI 基础设施与工程落地实践。</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <managingEditor>admin@yesmiracle.net (万戈)</managingEditor>
        <webMaster>admin@yesmiracle.net (万戈)</webMaster>
        <lastBuildDate>Sat, 24 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.yesmiracle.net/categories/%E6%8A%80%E6%9C%AF%E6%8B%86%E8%A7%A3/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>Lightpanda 源码级拆解：无头 Chrome 对 agent 贵在哪，以及「不渲染」换来和丢掉了什么</title>
        <link>https://www.yesmiracle.net/post/20261024-lightpanda-headless-browser-teardown/</link>
        <pubDate>Sat, 24 Oct 2026 00:00:00 +0000</pubDate>
        <author>admin@yesmiracle.net (万戈)</author>
        <guid>https://www.yesmiracle.net/post/20261024-lightpanda-headless-browser-teardown/</guid>
        <description>&lt;img src="https://www.yesmiracle.net/post/20261024-lightpanda-headless-browser-teardown/cover.svg" alt="Featured image of post Lightpanda 源码级拆解：无头 Chrome 对 agent 贵在哪，以及「不渲染」换来和丢掉了什么" /&gt;&lt;p&gt;一个用 Zig 从零写的浏览器，没有 Chromium 分支，没有 WebKit 补丁，官方口径是比无头 Chrome 省 16 倍内存、快 9 倍。仓库 36.9k star，AGPL-3.0。&lt;/p&gt;
&lt;p&gt;Lightpanda 的背景故事很直接：创始团队做了多年大规模网页抓取，撞上无头 Chrome 的成本和稳定性上限。实例开几百上千个，内存和 CPU 都吃不住，还得频繁重启才能稳住。&lt;/p&gt;
&lt;p&gt;但如果你只把它读成「更省资源的无头 Chrome」，会漏掉它真正动刀的位置。这篇拆的是那一刀。&lt;/p&gt;
&lt;h2 id=&#34;一贵在哪被像素定义的交互协议&#34;&gt;一、贵在哪：被像素定义的交互协议&lt;/h2&gt;
&lt;p&gt;这是我认为整篇里最重要的一段，因为它把因果顺序说反了就会理解错这个项目。&lt;/p&gt;
&lt;p&gt;常见说法是「因为不渲染所以快」。真实顺序是反的：&lt;strong&gt;因为先把交互从像素上解耦了，才敢不渲染。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Lightpanda 官方文档自己点出了 CDP 的结构性缺陷。CDP 能在 DOM 上直接查询和修改，但&lt;strong&gt;它没有 DOM 级的用户交互命令&lt;/strong&gt;。要点击一个元素，客户端必须：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;查出这个元素的渲染像素坐标&lt;/li&gt;
&lt;li&gt;把虚拟鼠标移到那个坐标&lt;/li&gt;
&lt;li&gt;派发一个点击事件&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;三次往返，外加一个渲染依赖，去做一件概念上只有一个动作的事。文档的原话是，CDP 是为人坐在显示器前调试浏览器设计的，不是为「一个已经知道要操作哪个 DOM 节点的脚本」设计的。&lt;/p&gt;
&lt;p&gt;结论就出来了：&lt;strong&gt;无头 Chrome 之所以必须把 style resolution、layout、paint、compositing 整条流水线跑起来，是因为它的交互协议是用像素定义的。&lt;/strong&gt; 没有像素坐标，Playwright 点不了按钮。&lt;/p&gt;
&lt;p&gt;Lightpanda 的做法是把交互改成 DOM 级，于是渲染流水线成了可删项。&lt;/p&gt;
&lt;h2 id=&#34;二删掉之后剩下什么&#34;&gt;二、删掉之后剩下什么&lt;/h2&gt;
&lt;p&gt;架构上留下的是自动化真正用到的那部分：网络层、HTML 解析器、JavaScript 引擎、Web API 和 DOM。&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;组件&lt;/th&gt;
          &lt;th&gt;实现&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;控制面&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;fetch&lt;/code&gt;、&lt;code&gt;serve&lt;/code&gt;（CDP 或 WebDriver classic/BiDi）、&lt;code&gt;agent&lt;/code&gt;、&lt;code&gt;mcp&lt;/code&gt;、云端 HTTP API&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;网络层&lt;/td&gt;
          &lt;td&gt;libcurl、robots.txt、WebSocket&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;HTML 解析&lt;/td&gt;
          &lt;td&gt;把取回的 HTML 变成 DOM&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;JavaScript 引擎&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;V8，和 Chrome 同一个引擎&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Web API 与 DOM&lt;/td&gt;
          &lt;td&gt;脚本读写和修改的内存文档&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最后一行值得停一下：它没有自己造 JS 引擎，用的是 V8。这意味着&lt;strong&gt;跑不了的原因不会是「JS 引擎不支持某个语法」&lt;/strong&gt;，而是缺某个 Web API、或者某个行为依赖真实布局。这个区别在排查问题的时候很有用。&lt;/p&gt;
&lt;p&gt;进程模型上，它编译成单个原生二进制，运行时&lt;strong&gt;只有一个进程&lt;/strong&gt;。这个进程持有一份跨整个运行周期共享的状态（代码里叫 &lt;code&gt;App&lt;/code&gt;），浏览器实例按需创建，每个实例拥有自己的一个 V8 isolate。仓库结构直接对应这套分层：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;路径&lt;/th&gt;
          &lt;th&gt;对应组件&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;src/cdp/&lt;/code&gt;、&lt;code&gt;src/agent/&lt;/code&gt;、&lt;code&gt;src/mcp/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;控制面&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;src/main.zig&lt;/code&gt;、&lt;code&gt;src/App.zig&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;App&lt;/code&gt; 与进程级服务&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;src/network/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;App&lt;/code&gt; 的网络层（libcurl、robots.txt、WebSocket）&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;src/browser/&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Browser、Session、Page、Frame&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;同一个引擎后面挂了五个入口，这是它设计上比较聪明的地方：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda fetch --dump html https://example.com   &lt;span style=&#34;color:#75715e&#34;&gt;# 一次性取页，输出 HTML 或 Markdown&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda serve --obey-robots                     &lt;span style=&#34;color:#75715e&#34;&gt;# 起服务，默认 CDP，127.0.0.1:9222&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda mcp                                     &lt;span style=&#34;color:#75715e&#34;&gt;# MCP server（stdio）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda agent                                   &lt;span style=&#34;color:#75715e&#34;&gt;# 交互式 agent REPL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda run script.js                           &lt;span style=&#34;color:#75715e&#34;&gt;# 重放已保存的脚本，不需要 LLM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;serve&lt;/code&gt; 的协议选择值得单独说：默认讲 CDP，加 &lt;code&gt;--protocol webdriver&lt;/code&gt; 改讲 WebDriver（classic 走 HTTP，BiDi 走 WebSocket），而 &lt;code&gt;--protocol cdp --protocol webdriver&lt;/code&gt; 可以在&lt;strong&gt;同一个端口上同时&lt;/strong&gt;提供两套。对已经有一套 Selenium 测试栈的团队，这个兼容层省掉了重写。&lt;/p&gt;
&lt;p&gt;日志是结构化的，一眼能看出它在干什么：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$time=1785335600862 $scope=app $level=note $msg=&amp;#34;server running&amp;#34; address=127.0.0.1:9222
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$time=1785335543068 $scope=browser $level=info $msg=&amp;#34;executing script&amp;#34; src=.../script.js kind=javascript
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$time=1785335543139 $scope=http $level=info $msg=&amp;#34;request complete&amp;#34; source=xhr status=200 len=4770
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;三代价一组看起来有实际没有的能力&#34;&gt;三、代价：一组「看起来有、实际没有」的能力&lt;/h2&gt;
&lt;p&gt;这一节是我认为用它之前必须知道的，因为它的失败方式很特殊。&lt;/p&gt;
&lt;p&gt;不渲染意味着&lt;strong&gt;没有真实布局&lt;/strong&gt;。官方文档写得很清楚：元素尺寸是从 CSS 和 HTML 属性做的合理近似，而&lt;strong&gt;元素位置是从 DOM 深度和兄弟顺序模拟出来的&lt;/strong&gt;（对应 &lt;code&gt;calculateDocumentPosition&lt;/code&gt;），不是真实布局算出来的。&lt;/p&gt;
&lt;p&gt;更要紧的是这一条：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Page.captureScreenshot&lt;/code&gt; 返回一张&lt;strong&gt;占位图&lt;/strong&gt;，不是真实渲染。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;原文的收尾是一句提醒：需要真实像素的时候，请改用完整浏览器。&lt;/p&gt;
&lt;p&gt;把这两条放在一起看，会得到一个不太妙的性质：&lt;strong&gt;它在这类能力上不会报错，它会返回一个看起来正常的替代品。&lt;/strong&gt; 截图请求成功返回了 200 和一张图片，只是图里没有你的页面。&lt;/p&gt;
&lt;p&gt;对 agent 来说这比报错危险。一个拿到占位图的 agent 不会停下来，它会继续基于那张图往下推理，然后给你一个基于空白内容得出的结论。&lt;/p&gt;
&lt;p&gt;所以边界要自己划清。以下这些能力不在它的范围内：视觉回归测试、布局断言（元素到底在哪、有没有重叠）、截图给多模态模型看、验证码图像识别、PDF 生成。&lt;/p&gt;
&lt;h2 id=&#34;四省了多少数字和它的适用边界&#34;&gt;四、省了多少：数字和它的适用边界&lt;/h2&gt;
&lt;p&gt;官方基准是在 AWS m5.large 上请求 933 个真实网页，通过真实网络：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;指标&lt;/th&gt;
          &lt;th&gt;Lightpanda&lt;/th&gt;
          &lt;th&gt;无头 Chrome&lt;/th&gt;
          &lt;th&gt;差距&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;峰值内存（100 页）&lt;/td&gt;
          &lt;td&gt;123 MB&lt;/td&gt;
          &lt;td&gt;2 GB&lt;/td&gt;
          &lt;td&gt;约 16 倍&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;执行时间（100 页）&lt;/td&gt;
          &lt;td&gt;5 秒&lt;/td&gt;
          &lt;td&gt;46 秒&lt;/td&gt;
          &lt;td&gt;约 9 倍&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这两个数字的意义不一样，我想分开说。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内存那 16 倍才是决定性的。&lt;/strong&gt; 它决定的是「一台机器上能同时跑多少个实例」，也就是你的单页成本。做大规模抓取或者给每个 agent 会话开一个浏览器进程时，这个数字直接换算成账单。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;执行时间那 9 倍在单页任务上你基本感知不到。&lt;/strong&gt; 46 秒和 5 秒都是 100 页的累计值，摊到单页是零点几秒的差别，而网络往返往往比这更慢。这个数字真正起作用的地方是批量任务。&lt;/p&gt;
&lt;p&gt;顺便提醒一句这两个数字的适用边界：它是&lt;strong&gt;爬取场景&lt;/strong&gt;的基准（取页、执行 JS、拿 DOM），不是「跑一个多步交互的 agent 任务」的基准。后者的耗时更多取决于页面数量、等待策略和模型推理，浏览器本身省下的那部分会被摊薄。&lt;/p&gt;
&lt;h2 id=&#34;五agent-模式把-llm-放进了浏览器进程&#34;&gt;五、agent 模式：把 LLM 放进了浏览器进程&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;lightpanda agent&lt;/code&gt; 这个入口的设计和常见的「agent 通过 CDP 驱动浏览器」不一样：&lt;strong&gt;它跑在浏览器同一个进程里，不走 CDP。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;它叠了三层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;浏览器&lt;/strong&gt;：加载页面、执行 JavaScript、处理 DOM，和 CDP server 用的是同一个引擎&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行时&lt;/strong&gt;：一组原生工具，&lt;code&gt;goto&lt;/code&gt;、&lt;code&gt;click&lt;/code&gt;、&lt;code&gt;fill&lt;/code&gt;、&lt;code&gt;extract&lt;/code&gt;、&lt;code&gt;evaluate&lt;/code&gt;、&lt;code&gt;search&lt;/code&gt;、&lt;code&gt;detectForms&lt;/code&gt;、&lt;code&gt;selectOption&lt;/code&gt;、&lt;code&gt;waitForState&lt;/code&gt; 等，每个都有对应的斜杠命令&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM&lt;/strong&gt;：把你的自然语言请求翻译成工具调用，&lt;strong&gt;这一层是可选的&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第三层可选这点很有意思。&lt;code&gt;--no-llm&lt;/code&gt; 之后它退化成只有斜杠命令的 REPL，你仍然可以手工驱动浏览器。也就是说 LLM 在这里是「把自然语言编译成工具调用」的便利层，不是必需品。&lt;/p&gt;
&lt;p&gt;provider 的解析顺序是四级，值得记一下因为它决定你第一次跑会不会卡住：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;记住的&lt;/strong&gt;：上次用 &lt;code&gt;/provider&lt;/code&gt; 选的，按目录持久化在 &lt;code&gt;.lp-agent.zon&lt;/code&gt; 里，前提是它的 key 还在&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动探测&lt;/strong&gt;：环境变量里第一个找到的 key，按固定优先级顺序&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地&lt;/strong&gt;：没有云端 key 时，探测本机在跑的 Ollama 或 llama.cpp。本地服务不会被自动探测到，必须显式指定 &lt;code&gt;--provider ollama&lt;/code&gt; 或 &lt;code&gt;--provider llama_cpp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;没有&lt;/strong&gt;：&lt;code&gt;--no-llm&lt;/code&gt; 退化成基础 REPL，自然语言和 &lt;code&gt;/login&lt;/code&gt;、&lt;code&gt;/logout&lt;/code&gt; 这类由 LLM 驱动的命令不可用&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;还有一个反直觉的开关：&lt;code&gt;--effort&lt;/code&gt; 设的是每轮推理预算（&lt;code&gt;none&lt;/code&gt; 到 &lt;code&gt;xhigh&lt;/code&gt;，REPL 里默认 &lt;code&gt;low&lt;/code&gt;，一次性 &lt;code&gt;--task&lt;/code&gt; 模式默认 &lt;code&gt;medium&lt;/code&gt;）。官方给的说明是，&lt;strong&gt;更高的 effort 可能意味着每个任务更少的工具调用&lt;/strong&gt;，因为模型规划得更好。所以它不单纯是「更慢更聪明」的滑块，而是一个真实的取舍。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--attach&lt;/code&gt; 可以把本地文件喂给模型，文本内联（单个上限 512 KiB），图片、音频、PDF 走 base64（单个上限 20 MiB），每个 &lt;code&gt;--attach&lt;/code&gt; 带一个文件，可重复。&lt;/p&gt;
&lt;h2 id=&#34;六为什么-agent-模式要放进同一个进程&#34;&gt;六、为什么 agent 模式要放进同一个进程&lt;/h2&gt;
&lt;p&gt;回到第一节那个 CDP 的缺陷，就能看出 &lt;code&gt;lightpanda agent&lt;/code&gt; 那个设计选择的理由。&lt;/p&gt;
&lt;p&gt;如果 agent 通过 CDP 驱动浏览器，那么每一个动作都要过一次协议：客户端发命令，浏览器执行，结果回来，客户端再决定下一步。第一节里那个点击的例子是三次往返；一个多步任务就是几十次。每次往返都是延迟、都是状态同步的机会、也都是「客户端和服务端对当前状态的理解可能不一致」的窗口。&lt;/p&gt;
&lt;p&gt;而 &lt;code&gt;agent&lt;/code&gt; 模式把运行时工具放进了浏览器进程里。&lt;code&gt;goto&lt;/code&gt;、&lt;code&gt;click&lt;/code&gt;、&lt;code&gt;fill&lt;/code&gt; 这些工具直接操作内存里的 DOM，&lt;strong&gt;不过协议&lt;/strong&gt;。省下的不只是那几次往返的延迟，更是一个根本性的简化：不再有两个进程各自维护一份「页面现在长什么样」的理解。&lt;/p&gt;
&lt;p&gt;这也解释了为什么它的工具集里有 &lt;code&gt;detectForms&lt;/code&gt;、&lt;code&gt;selectOption&lt;/code&gt;、&lt;code&gt;waitForState&lt;/code&gt; 这种&lt;strong&gt;语义级&lt;/strong&gt;的操作。它们的存在前提就是能直接看到 DOM 结构。用像素坐标的协议做不出 &lt;code&gt;detectForms&lt;/code&gt;，因为「表单」这个概念在像素层面不存在。&lt;/p&gt;
&lt;p&gt;代价是耦合：这套工具只有 &lt;code&gt;lightpanda agent&lt;/code&gt; 能用，你在自己的 agent 框架里通过 CDP 连过去是拿不到的。想用就得把任务交给它的 REPL，或者用 &lt;code&gt;--task&lt;/code&gt; 跑一次性任务。&lt;/p&gt;
&lt;h2 id=&#34;七迁移一份已有的-playwright-脚本要改什么&#34;&gt;七、迁移一份已有的 Playwright 脚本要改什么&lt;/h2&gt;
&lt;p&gt;对已经有自动化栈的团队，Lightpanda 的迁移成本比较低，因为它的策略是「在你已经用的接口上等你」：Puppeteer、Playwright、Chromedp 通过 CDP 连它的方式和连 Chrome 一样，Selenium 走 WebDriver BiDi。把客户端指向它的端点，脚本基本不用动。&lt;/p&gt;
&lt;p&gt;但有几类操作会出问题，这是迁移时该先盘一遍的清单：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;你的脚本里有什么&lt;/th&gt;
          &lt;th&gt;在 Lightpanda 上会怎样&lt;/th&gt;
          &lt;th&gt;处理方式&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;截图 / 视觉断言&lt;/td&gt;
          &lt;td&gt;拿到占位图，不报错&lt;/td&gt;
          &lt;td&gt;这段留在 Chrome 上&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;依赖真实布局的定位（按位置、重叠判断）&lt;/td&gt;
          &lt;td&gt;位置是模拟值&lt;/td&gt;
          &lt;td&gt;改成 DOM 语义定位&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;PDF 生成&lt;/td&gt;
          &lt;td&gt;不支持&lt;/td&gt;
          &lt;td&gt;留在 Chrome 上&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;文件上传&lt;/td&gt;
          &lt;td&gt;不支持&lt;/td&gt;
          &lt;td&gt;留在 Chrome 上&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;多标签页流程&lt;/td&gt;
          &lt;td&gt;一条 CDP 连接一个页面&lt;/td&gt;
          &lt;td&gt;拆成多次运行，或改用 &lt;code&gt;goto_url&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;依赖冷门 Web API 的页面&lt;/td&gt;
          &lt;td&gt;取决于该 API 有没有实现&lt;/td&gt;
          &lt;td&gt;报错就回退完整浏览器&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;看这张表的方式是：&lt;strong&gt;能过一遍 DOM 语义完成的部分交出去，剩下需要像素的部分留给自己。&lt;/strong&gt; 这也是混合部署的划分线。&lt;/p&gt;
&lt;h2 id=&#34;八pandascript被低估的那一环&#34;&gt;八、PandaScript：被低估的那一环&lt;/h2&gt;
&lt;p&gt;这个设计我认为在 agent 工具里不常见，值得单独讲。&lt;/p&gt;
&lt;p&gt;流程是这样的：你用自然语言让 agent 把一件事跑通，然后 &lt;code&gt;/save&lt;/code&gt; 把它导出成一个普通的 JavaScript 重放脚本。之后：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;lightpanda run script.js    &lt;span style=&#34;color:#75715e&#34;&gt;# 没有 LLM 调用，不需要 API key&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它做的事是&lt;strong&gt;把 agent 的探索成本摊薄成脚本的运行成本&lt;/strong&gt;。第一次调用的代价是模型推理加试错，第二次开始是纯执行。&lt;/p&gt;
&lt;p&gt;在 agent 流水线里这是一种少见的「确定性降级」：一个任务一旦被探索明白，就可以从「每次都要模型决策」降级成「一段可重放、可版本管理、可 CI 的代码」。稳定性、成本和可审计性三样一起改善。&lt;/p&gt;
&lt;p&gt;官方也给了对应的警告：&lt;code&gt;.js&lt;/code&gt; 脚本里可以包含 &lt;code&gt;evaluate(...)&lt;/code&gt; 调用，那是&lt;strong&gt;在页面里执行任意 JavaScript&lt;/strong&gt;，只跑你信任的脚本，标准跟对待 shell 脚本一样。&lt;/p&gt;
&lt;h2 id=&#34;九三个会咬人的细节&#34;&gt;九、三个会咬人的细节&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. 一条 CDP 连接只持有一个页面。&lt;/strong&gt; 第一次 &lt;code&gt;new_tab(url)&lt;/code&gt; 正常，第二次会失败并报 &lt;code&gt;TargetAlreadyLoaded&lt;/code&gt;。这不只是个 bug，它的会话模型没有跟上「agent 会开很多标签页」这个现实。用的时候要按「一个连接一个页面」来组织，或者用 &lt;code&gt;goto_url&lt;/code&gt; 复用当前页。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. AGPL-3.0 是架构决策，不是法务细节。&lt;/strong&gt; 一个以「替换无头 Chrome」为定位的浏览器选了这个许可，直接决定它能不能被嵌进闭源产品。如果那件事是真的，AGPL-3.0 会关掉这条路径，把采用方式推向「自托管」和「用它的云服务」两端。评估它的时候，这一条应该和内存数字放在同一张表上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. robots.txt 要自己开。&lt;/strong&gt; 官方给了 &lt;code&gt;--obey-robots&lt;/code&gt; 这个参数，同时提了一句「尊重 robots.txt，避免对小站点高频请求」。要注意的是&lt;strong&gt;它默认不开&lt;/strong&gt;，也就是默认行为不遵守 robots.txt。把抓取跑在别人的站点上时，这是一个需要主动处理的合规点，&lt;code&gt;fetch&lt;/code&gt; 和 &lt;code&gt;serve&lt;/code&gt; 都接受这个参数。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. 真实部署形态通常是混合的。&lt;/strong&gt; Hermes Agent 的官方文档里写着 &lt;code&gt;browser.engine: lightpanda&lt;/code&gt;，配的是&lt;strong&gt;自动 Chrome 兜底&lt;/strong&gt;：navigate、snapshot、click、type、scroll、back、press、eval 走 Lightpanda，而&lt;strong&gt;截图、&lt;code&gt;browser_vision&lt;/code&gt;、PDF 生成、文件上传，以及任何在 Lightpanda 上报错的操作，自动重试到 Chrome&lt;/strong&gt;。这等于官方承认了那个边界的存在，并给出了工程上的处理方式。&lt;/p&gt;
&lt;h2 id=&#34;写在最后&#34;&gt;写在最后&lt;/h2&gt;
&lt;p&gt;我的判断是 Lightpanda 的价值不在「更便宜的无头浏览器」这个定位上，而在于它把无头浏览器的成本结构&lt;strong&gt;归因清楚了&lt;/strong&gt;。过去几年大家默认无头 Chrome 重是必然代价，它指出核心依赖是交互协议定义在像素上，于是渲染成了不可删项。删掉那个依赖，渲染就可以删。&lt;/p&gt;
&lt;p&gt;代价同样清楚：没有真实布局，截图是占位图，而失败方式是返回替代品而不是报错。用它的正确姿势是按能力分层，需要像素的活儿交给完整浏览器。&lt;/p&gt;
&lt;p&gt;最后交代一下本文的证据边界：以上全部来自 Lightpanda 的官方文档、仓库结构和源码目录，&lt;strong&gt;我没有实机跑过&lt;/strong&gt;。要动手验证，最小的一组是三条命令：&lt;code&gt;lightpanda fetch --dump html &amp;lt;url&amp;gt;&lt;/code&gt; 看它取回的是不是最终 DOM；&lt;code&gt;lightpanda serve&lt;/code&gt; 之后用 Playwright 连上去，试一次 &lt;code&gt;captureScreenshot&lt;/code&gt; 看返回的占位图长什么样；以及开第二个 &lt;code&gt;new_tab&lt;/code&gt; 复现 &lt;code&gt;TargetAlreadyLoaded&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;相关阅读：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261024-neko-virtual-browser-teardown/&#34; &gt;《Neko 源码级拆解：把浏览器变成一条视频流》&lt;/a&gt;：姊妹篇。这篇讲的是把浏览器压到最轻、砍掉一切与人有关的部分；那篇讲的是反过来，把浏览器整个变成一条给人看的流。&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20260910-browser-agent-trio-comparison/&#34; &gt;《browser-use 114k⭐ 称王！浏览器 Agent 三强争霸》&lt;/a&gt;：把这篇放进那个坐标系里看，Lightpanda 属于「驱动层」，和三强不是同一层的东西。&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261019-opencode-byok-console-credential-breach/&#34; &gt;《OpenCode 被「蒸馏攻击」打穿》&lt;/a&gt;：同样涉及浏览器与凭据，那篇的教训是凭据放在哪比工具选哪个更值钱。&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        <item>
        <title>Neko 源码级拆解：把浏览器变成一条视频流，以及「只出像素」为什么是关键</title>
        <link>https://www.yesmiracle.net/post/20261024-neko-virtual-browser-teardown/</link>
        <pubDate>Sat, 24 Oct 2026 00:00:00 +0000</pubDate>
        <author>admin@yesmiracle.net (万戈)</author>
        <guid>https://www.yesmiracle.net/post/20261024-neko-virtual-browser-teardown/</guid>
        <description>&lt;img src="https://www.yesmiracle.net/post/20261024-neko-virtual-browser-teardown/cover.svg" alt="Featured image of post Neko 源码级拆解：把浏览器变成一条视频流，以及「只出像素」为什么是关键" /&gt;&lt;p&gt;Neko 在 GitHub 上的自我介绍是「一个自托管的虚拟浏览器，跑在 Docker 里，用 WebRTC」。22.5k star，Apache-2.0，Go 写的，2020 年 3 月建仓，到现在 13 个 release、59 个 tag。&lt;/p&gt;
&lt;p&gt;按这个描述，它很容易被归到「远程浏览器」或者「看片派对工具」那一类里去。但如果你仔细看它的架构，会发现它做的动作比那两句话大得多，而且这个动作恰好补上了 agent 流水线里一个空位。&lt;/p&gt;
&lt;h2 id=&#34;一核心动作把桌面降维成一条媒体流&#34;&gt;一、核心动作：把桌面降维成一条媒体流&lt;/h2&gt;
&lt;p&gt;Neko 的架构文档里有一句关键描述：它把一个&lt;strong&gt;虚拟桌面环境&lt;/strong&gt;从 Docker 容器里串流到浏览器，走 WebRTC。注意是虚拟桌面，不是只串一个浏览器窗口。&lt;/p&gt;
&lt;p&gt;系统分四层：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;层&lt;/th&gt;
          &lt;th&gt;职责&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;客户端&lt;/td&gt;
          &lt;td&gt;Vue.js 单页应用，负责把流解码播放，把用户的输入事件回传&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;服务端&lt;/td&gt;
          &lt;td&gt;Go 写的进程，管会话、管房间、管权限&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;媒体采集管线&lt;/td&gt;
          &lt;td&gt;从虚拟桌面抓画面、编码、广播给所有观看者&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;桌面控制&lt;/td&gt;
          &lt;td&gt;把输入事件送进虚拟 X 桌面&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;仓库里的文件直接对应这套分层，读一遍就能看出重心在哪：&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;路径&lt;/th&gt;
          &lt;th&gt;职责&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;server/internal/capture/manager.go&lt;/code&gt;、&lt;code&gt;broadcast.go&lt;/code&gt;、&lt;code&gt;streamsink.go&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;采集与广播管线，整个项目最核心的一块&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;server/internal/webrtc/peer.go&lt;/code&gt;、&lt;code&gt;metrics.go&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;WebRTC 连接与质量指标&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;server/internal/session/manager.go&lt;/code&gt;、&lt;code&gt;session.go&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;会话与房间&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;server/internal/desktop/manager.go&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;虚拟桌面控制&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;server/pkg/types/codec/codecs.go&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;编解码器类型&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;server/cmd/serve.go&lt;/code&gt;、&lt;code&gt;server/neko.go&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;启动入口&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;client/src/store/remote.ts&lt;/code&gt;、&lt;code&gt;client/src/components/video.vue&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;前端输入回传与视频播放&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;三个 &lt;code&gt;capture&lt;/code&gt; 文件加一个 &lt;code&gt;codec&lt;/code&gt; 目录，占了最重的分量。这说明 Neko 本质上是&lt;strong&gt;一个媒体系统，附带了一个浏览器&lt;/strong&gt;，反过来看会理解错它的重心。&lt;/p&gt;
&lt;h3 id=&#34;为什么是桌面而不是浏览器&#34;&gt;为什么是「桌面」而不是「浏览器」&lt;/h3&gt;
&lt;p&gt;这个区别不是措辞上的讲究。因为串的是整个虚拟桌面，容器里可以放任何 GUI 程序，浏览器只是其中最常见的一个。这解释了它的镜像为什么按 &lt;code&gt;&amp;lt;application&amp;gt;&lt;/code&gt; 组织、还带 &lt;code&gt;&amp;lt;flavor&amp;gt;&lt;/code&gt; 变体：&lt;code&gt;firefox&lt;/code&gt; 是默认别名，此外还有基于其他浏览器、甚至其他应用镜像的版本。你想让用户通过一条流访问的东西，可以是一个网页，也可以是一个桌面软件，架构上不需要改任何东西。&lt;/p&gt;
&lt;p&gt;也正因为是桌面，输入事件的处理必须是通用的（鼠标、键盘、剪贴板），抽象层次停在坐标上。这跟浏览器自动化工具差了一整层：后者知道什么叫「一个按钮」，前者只知道「坐标 (x, y) 上发生了一次点击」。这个层次差别决定了后面所有的能力边界，包括为什么它能接住一个真人在操作。&lt;/p&gt;
&lt;h2 id=&#34;二这个选择解锁了什么&#34;&gt;二、这个选择解锁了什么&lt;/h2&gt;
&lt;p&gt;把桌面降维成媒体流之后，一堆原本很难做的能力变成了「流」的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多用户共享同一个浏览器实例。&lt;/strong&gt; 因为输出是一条流，扇出（fan-out）就是分发流，不需要为每个观看者复制一份浏览器。十个人看同一个浏览器，成本几乎等于一个人。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;录制与转播。&lt;/strong&gt; 官方支持通过 RTMP 把房间内容推到 Twitch、YouTube 这类平台，而且&lt;strong&gt;没有人在线时流也照跑&lt;/strong&gt;，所以 24 小时无人值守广播是它能干的活。这条能力之所以成立，同样是因为内部已经是一条媒体管线，往外推一份到 RTMP 只是多接一个 sink。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;权限回收变成断流。&lt;/strong&gt; 访问控制在这个架构里很干净：不再给你推流，你的画面就停了。不需要撤销 cookie、不需要踢会话、不需要清理对方机器上的任何东西。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;和 VNC 那类方案的分野也在这里。&lt;/strong&gt; 远程桌面协议的设计目标通常是「一个操作者控制一台机器」，所以它的抽象是控制通道。Neko 的抽象是媒体流，而流的天然属性是多人、可录制、可转发、可中断。同一份底层技术（抓画面 + 编码 + 传输），抽象选得不同，能长出来的产品形状就完全不同。&lt;/p&gt;
&lt;h2 id=&#34;三只出像素的信任模型&#34;&gt;三、「只出像素」的信任模型&lt;/h2&gt;
&lt;p&gt;这是我认为 Neko 最被低估的一点，而且它对 agent 场景的意义比它宣传的那个卖点更大。&lt;/p&gt;
&lt;p&gt;官方的说法是把这条当知识产权保护来讲：WebRTC 只共享视频和音频，不共享真实数据；别人没法反编译你的代码，因为代码根本不在他的机器上运行；访问权你可以随时收回。&lt;/p&gt;
&lt;p&gt;这个说法是对的，但放到 agent 场景里，它还有一层更实际的推论：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当人和 agent 协作时，人的那台机器上永远不会有页面代码、token、cookie。只有像素。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这件事为什么重要，看看替代方案就清楚了。如果让用户在自己的浏览器里登录一个站点，然后把会话交给 agent，那么要么你要求用户把凭据交给你（大多数人不该这么做，你也不该要），要么你让用户在自己的浏览器里操作、再想办法把会话搬出来（cookie 导出，安全上很脏）。&lt;/p&gt;
&lt;p&gt;而 Neko 这条路是：真浏览器在服务器上，用户看到的是它的画面，用户在画面上敲的东西变成输入事件回给那个浏览器。&lt;strong&gt;凭据从头到尾没有离开过服务器，用户的机器上只有一段视频。&lt;/strong&gt; 于是「让用户自己登录」第一次在安全上说得通。&lt;/p&gt;
&lt;p&gt;需要说清的边界：这条性质保护的是&lt;strong&gt;数据和代码的传输路径&lt;/strong&gt;，不保护&lt;strong&gt;画面内容&lt;/strong&gt;。用户屏幕上看到的仍然是一个真实的浏览器窗口，所以页面上可见的东西（表单里已填的内容、页面上显示的验证码、错误提示）照样被看到。它解决的是「凭据不落地到用户机器」，不解决「画面里有没有敏感信息」。别把它当成一个更宽的承诺。&lt;/p&gt;
&lt;h2 id=&#34;四可拦截的自动化人和-agent-操作同一个实例&#34;&gt;四、可拦截的自动化：人和 agent 操作同一个实例&lt;/h2&gt;
&lt;p&gt;Neko 的官网在功能列表里有一行「Automated Browser」，写的是：装 Playwright 或 Puppeteer 来自动化任务，&lt;strong&gt;同时能够实时介入&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这一行值得展开，因为它对应的是 agent 出问题时最省事的一种兜底形态。&lt;/p&gt;
&lt;p&gt;常见的兜底做法是：agent 跑在无头浏览器里，遇到需要人的步骤就把状态导出，交给一个真浏览器让用户操作，用户做完再把会话导回来。这个流程里有两个进程、两次状态交接，每一步都可能丢东西（会话、页面状态、DOM 与视觉的不一致）。&lt;/p&gt;
&lt;p&gt;Neko 的形态把这件事消掉了：&lt;strong&gt;Playwright 跑在容器里，人看着同一条流，随时可以接管。&lt;/strong&gt; 人和 agent 操作的是同一个浏览器实例，不存在「交接」，因为从来就没有分开过。用户接手时页面已经在正确的状态上，用户做完 agent 继续，中间没有同步动作。&lt;/p&gt;
&lt;p&gt;对一个要求「agent 出错时人能马上接管」的产品，这个架构省掉的是整整一层状态同步逻辑。&lt;/p&gt;
&lt;h2 id=&#34;五代价单位成本与-codec-的隐藏复杂度&#34;&gt;五、代价：单位成本与 codec 的隐藏复杂度&lt;/h2&gt;
&lt;p&gt;Neko 的代价也很清楚，而且我认为比多数介绍里说的更实在。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;单位成本降不下来。&lt;/strong&gt; 每个实例里跑的是一个真浏览器加一个虚拟 X 桌面，再加视频编码。这套东西的内存占用不可能接近无头方案。所以它的合理位置是「需要人的那一段」，把它当成全流程方案会贵得没必要。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;codec 是隐藏的复杂度。&lt;/strong&gt; 从源码结构看，&lt;code&gt;pkg/types/codec/codecs.go&lt;/code&gt; 加整条 capture 管线是项目最重的一块，这不是偶然。WebRTC 串流要在画质、延迟、CPU 三者之间取舍，而这三者的最优解取决于你的场景（交互式操作要低延迟，看片要画质）。这块调优是运维层的主要工作，但在多数介绍里被一句「用 WebRTC」带过了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;镜像矩阵本身也是个决策。&lt;/strong&gt; 它的镜像命名规则是：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ghcr.io/m1k1o/neko/[&amp;lt;flavor&amp;gt;-]&amp;lt;application&amp;gt;:&amp;lt;version&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;&amp;lt;application&amp;gt;&lt;/code&gt; 是浏览器或基础镜像，&lt;code&gt;&amp;lt;flavor&amp;gt;&lt;/code&gt; 是可选变体，版本走语义化（&lt;code&gt;latest&lt;/code&gt;、&lt;code&gt;MAJOR&lt;/code&gt;、&lt;code&gt;MAJOR.MINOR&lt;/code&gt;、完整版本号）。官方提供两个注册表：GHCR 有全部 flavor 和多架构的稳定版，Docker Hub 只有不带 flavor 的 AMD64 开发版。&lt;/p&gt;
&lt;p&gt;这里有一条容易被忽略的文档提示：&lt;strong&gt;Firefox 系的浏览器不需要额外的 capabilities 或更大的共享内存就能稳定运行，Chromium 系的需要。&lt;/strong&gt; 这不是小事，它直接决定你的容器配置和稳定性。官方把 &lt;code&gt;m1k1o/neko:latest&lt;/code&gt; 保留为 &lt;code&gt;firefox&lt;/code&gt; 的别名，多半也是因为这个默认更稳。&lt;/p&gt;
&lt;h2 id=&#34;六一个端到端的登录场景&#34;&gt;六、一个端到端的登录场景&lt;/h2&gt;
&lt;p&gt;把上面几节拼成一个具体流程，会更清楚它的位置。假设 agent 要替用户去一个需要登录的后台取数据。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;agent 先在便宜的那一层把能做的做完，跑到「需要登录」这一步停下。&lt;/li&gt;
&lt;li&gt;起一个 Neko 房间，里面是一个真浏览器和真桌面。&lt;/li&gt;
&lt;li&gt;让浏览器停在登录页，把房间链接给用户。&lt;/li&gt;
&lt;li&gt;用户在自己浏览器里看到的是画面，在画面上输入账号密码。凭据只存在于服务器上的那个浏览器里。&lt;/li&gt;
&lt;li&gt;用户说一声好了，agent 从&lt;strong&gt;同一个实例&lt;/strong&gt;继续，此时页面已经在正确的状态上。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这个流程里最值得看的是第 3 步和第 5 步。如果实现方式是在无头层和真浏览器之间来回切换，那么第 3 步要复现页面状态、第 5 步要把会话搬回去，中间每一步都可能丢东西（cookie、已填的表单、DOM 与视觉的不一致）。而在「一开始就用真浏览器」的形态里，这两步都不存在，因为浏览器从来没有换过。&lt;/p&gt;
&lt;p&gt;代价是你为整个过程付了真浏览器的钱，包括那几步本来不需要人的。所以这里有个真实的取舍：&lt;strong&gt;要么付全价换掉交接逻辑，要么省成本但自己把交接写对。&lt;/strong&gt; 没有第三种。&lt;/p&gt;
&lt;h2 id=&#34;七接入自己的-agent-时要自己决定的三件事&#34;&gt;七、接入自己的 agent 时要自己决定的三件事&lt;/h2&gt;
&lt;p&gt;Neko 自己不是一个 agent 工具。它提供的是一个「可以被驱动的浏览器加一条流」，剩下的要你决定：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一，谁驱动浏览器。&lt;/strong&gt; 官方给的路是在容器里装 Playwright 或 Puppeteer。这意味着 agent 的驱动逻辑要跑在容器内部，而不是从外面连进去。至于容器内浏览器的 CDP 端口能不能暴露给外部客户端，我在官方文档里没有找到明确说明，这一点要实测确认。如果只能内部驱动，你的 agent 框架就要有「把代码送进容器执行」的能力，而不是「连一个远程浏览器端点」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二，流给谁看。&lt;/strong&gt; 房间是多人共享的，所以「用户看」和「审计看」可以是同一条流。需要留证的时候，接一个录制就同时满足了两边，不需要额外做一遍截图或录屏。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三，接管期间 agent 干什么。&lt;/strong&gt; 官方说「可实时介入」，但接管时 agent 是暂停、还是继续操作、还是排队等待，这是产品语义，得自己定义。这块我没有在文档里找到现成的实现，属于「架构允许、但要自己搭」的部分。&lt;/p&gt;
&lt;p&gt;这三点合起来说明一件事：Neko 是一个很好的底座，但把它变成 agent 的人机协作界面，中间还有一层要自己写。&lt;/p&gt;
&lt;h2 id=&#34;八它该放在流水线的哪一段&#34;&gt;八、它该放在流水线的哪一段&lt;/h2&gt;
&lt;p&gt;把这些拼起来，我的判断是 Neko 的位置很明确：&lt;strong&gt;它不是 agent 的浏览器，它是 agent 需要人时的那个界面。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;一次典型的 agent 任务里，绝大部分步骤不需要人：抓页面、点元素、填表单、提取数据。这些交给无头方案更便宜（同一批文档里，从零写的无头浏览器可以把峰值内存压到无头 Chrome 的十六分之一）。真正需要人的只有几个节点：登录、双因素、验证码、支付确认、以及 agent 判断不了的分叉。&lt;/p&gt;
&lt;p&gt;Neko 覆盖的就是这几个节点，而且覆盖得比别的方式干净：用户看到的是真浏览器画面，凭据不出服务器，agent 和人共用同一个实例所以不需要交接。&lt;/p&gt;
&lt;h2 id=&#34;写在最后&#34;&gt;写在最后&lt;/h2&gt;
&lt;p&gt;我读完 Neko 架构的主要收获是，它的价值不在「远程浏览器」这个功能描述里，而在那个抽象选择上：把桌面降维成媒体流。这个选择让它同时能做看片派对、远程办公和 agent 的人在环，因为这三件事在底层都是「把画面推给人、把输入收回来」。&lt;/p&gt;
&lt;p&gt;另一个收获是它的信任模型比它的定位更值钱。「只出像素」在官方口径里是防抄袭，但对 agent 场景，它恰好解决了「用户凭据怎么不经过第三方」这个一直很难处理的问题。&lt;/p&gt;
&lt;p&gt;最后交代证据边界：以上来自 Neko 的官方文档、架构说明和仓库文件结构，&lt;strong&gt;我没有实机跑过&lt;/strong&gt;。要验证最小的一组是：起一个 &lt;code&gt;ghcr.io/m1k1o/neko/firefox:latest&lt;/code&gt; 容器，用两个浏览器标签页连进同一个房间，确认两边看到的是同一个画面；然后在容器里装 Playwright 跑一个多步任务，中途从另一个标签页接管，看接管时页面状态有没有丢。&lt;/p&gt;
&lt;p&gt;相关阅读：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20261024-lightpanda-headless-browser-teardown/&#34; &gt;《Lightpanda 源码级拆解：无头 Chrome 对 agent 贵在哪》&lt;/a&gt;：姊妹篇。那篇讲的是把浏览器压到最轻、砍掉一切与人有关的部分；这篇讲的是反过来，把浏览器整个变成一条给人看的流。两篇合起来是 agent 浏览器栈的两端。&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20260910-browser-agent-trio-comparison/&#34; &gt;《browser-use 114k⭐ 称王！浏览器 Agent 三强争霸》&lt;/a&gt;：那篇比的是「谁在驱动浏览器」，Neko 不在那个坐标系里，它是被驱动的那一层。&lt;/li&gt;
&lt;li&gt;&lt;a class=&#34;link&#34; href=&#34;https://www.yesmiracle.net/post/20260721-ai-agent-attack-surface-panorama/&#34; &gt;《AI Agent 攻击面全景：从 Prompt 到内核的四层防御》&lt;/a&gt;：Neko 这种「凭据不出服务器」的形态，是那篇里「凭据层」问题的另一种解法。&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        
    </channel>
</rss>
