1. 这个「油猴」到底解决了什么问题
先说说我为什么会折腾这个东西。日常用 Claude Code 或者 Codex 这类 AI Agent 写代码、查资料、做自动化任务的时候,最让人抓狂的场景之一就是:Agent 明明能写代码,却没法直接访问那些需要登录才能看的页面。比如你让它去某个后台系统拉一份数据、去某个文档平台查一个接口说明、去某个内部工具页面确认一个配置项,它要么告诉你“我无法访问需要认证的页面”,要么让你手动把内容复制粘贴给它。一次两次还行,次数多了就非常烦。
浏览器这边呢,你其实已经登录好了,Cookie、Session、LocalStorage 全都是现成的。人手动打开页面就能看到内容,但 AI Agent 就是进不去。这个割裂感就像你家里明明有电,但每个电器都得自己带个发电机。
所以我做的这个东西,本质上就是一个「油猴」式的桥接层:让 Claude Code、Codex 这类 AI Agent 能够直接复用你浏览器里已经登录的会话状态,去访问那些需要认证的页面,把内容拿回来。它不是一个浏览器插件,也不是一个独立的爬虫工具,而是一个运行在本地的中间层,一头连着 AI Agent,一头连着你已经登录的浏览器。
核心关键词在这里体现得很清楚:AI Agent、Claude Code、Codex、已登录浏览器、会话复用、本地桥接。这几个词串起来就是整个项目的骨架。
适合谁来参考呢?如果你日常已经在用 AI Agent 做开发辅助、资料检索、自动化流程,并且经常遇到“需要登录才能访问”的墙,那这个东西对你价值很大。如果你只是偶尔用 AI 聊聊天,那可能感受不深。另外,做自动化测试、做内部工具集成的同学,也能从这个思路里拿到一些可复用的方案。
需要提前说明的是,下面涉及的具体实现细节,有一部分是基于常见工程实践做的合理补全,因为原始描述比较零散,我会把“为什么这么选”和“怎么落地”都讲清楚,方便你按自己的环境调整。
2. 整体设计思路与方案选型
2.1 为什么不做成浏览器插件
很多人第一反应是:直接写个浏览器插件不就行了?插件确实能拿到 Cookie,也能发请求。但问题在于,浏览器插件运行在浏览器沙箱里,它和本地的 AI Agent 之间没有天然的通信通道。你要么让插件暴露一个本地端口,要么让 Agent 去轮询插件写的数据,这两种方式都别扭。
更关键的是,浏览器插件的生命周期和浏览器绑定。你关了浏览器,插件就没了。但 AI Agent 可能是在终端里跑的一个长时间任务,它需要的是一个稳定的、随时可调用的服务。所以插件方案在“常驻服务”这个需求上天然吃亏。
我最终选的是「本地 HTTP 服务 + 浏览器会话复用」的组合。本地服务负责接收 Agent 的请求,浏览器负责提供已登录的会话状态,两者通过一个轻量的协议对接。这样 Agent 那边只需要发一个普通的 HTTP 请求,剩下的脏活累活都在本地服务里完成。
2.2 会话复用的三种可行路径
要让本地服务用上浏览器的登录态,常见有三条路:
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 读取 Cookie 文件 | 直接从浏览器配置目录读 Cookie 数据库 | 实现简单,不依赖浏览器运行 | 加密存储难解密,浏览器运行时可能锁库 |
| 通过调试协议对接 | 让浏览器开放调试端口,服务通过协议操作页面 | 能拿到完整会话,可执行 JS | 需要浏览器以特定方式启动 |
| 本地代理转发 | 浏览器流量走本地代理,服务截获并复用 | 对浏览器无侵入 | 配置复杂,证书问题多 |
我实际用的是第二条路为主、第一条路为辅。原因很直接:调试协议能拿到的不只是 Cookie,还有完整的页面上下文、LocalStorage、SessionStorage,甚至能直接在页面里执行脚本。这对于“让 Agent 像人一样操作页面”这个目标来说,能力上限高得多。
注意:无论走哪条路,核心原则都是「复用你自己已经登录的会话」,而不是去破解或绕过任何认证机制。你登录了,服务只是帮你把这个登录态用起来,这个边界要清楚。
2.3 为什么 Agent 侧要做得足够“薄”
Claude Code 和 Codex 这类工具,它们的扩展方式各不相同。有的支持自定义工具调用,有的支持 MCP 协议,有的只能通过命令行交互。如果我在 Agent 侧做很重的逻辑,那每换一个 Agent 就要重写一遍。
所以我的设计原则是:Agent 侧只做一件事——发一个 HTTP 请求,把「我要访问哪个页面」和「我要提取什么内容」传过来。剩下的会话管理、页面加载、内容解析、错误重试,全部在本地服务里完成。这样无论你用的是 Claude Code 还是 Codex,甚至是你自己写的一个脚本,只要能发 HTTP 请求,就能用上这个能力。
这个「薄客户端、厚服务端」的思路,和很多 API 网关的设计是一致的。好处是扩展成本低,坏处是本地服务要足够健壮,不然它挂了所有 Agent 都跟着挂。
3. 核心细节解析与实操要点
3.1 浏览器调试端口的正确打开方式
要让本地服务能操作浏览器,第一步是让浏览器开放调试能力。以常见的 Chromium 内核浏览器为例,启动时需要带上特定的参数。这里我不写具体命令,因为不同系统、不同浏览器版本差异较大,但核心逻辑是:指定一个调试端口,并指定一个独立的用户数据目录。
为什么要独立用户数据目录?因为如果你用默认目录,浏览器已经在运行的情况下,新启动的实例会直接复用已有进程,调试端口不会生效。用一个独立目录,相当于开了一个“专用浏览器”,你在这个浏览器里登录一次,之后服务就一直能用这个会话。
实操心得:我建议专门开一个浏览器实例给这个服务用,不要和你日常主力浏览器混在一起。主力浏览器里插件多、标签页多,调试协议连上去之后状态很复杂,容易出各种奇怪问题。专用实例干净,登录一次管很久。
登录态怎么保持?第一次启动专用实例后,你手动打开目标网站,完成登录。之后只要不清理这个实例的用户数据目录,Cookie 和 Session 就会一直保留。服务每次连上去,用的都是这个已登录状态。
3.2 本地服务的接口设计
本地服务的接口不需要多,三四个就够用。我实际设计的是这样一组:
POST /fetch:传入 URL 和可选的等待条件,返回页面渲染后的内容POST /extract:传入 URL 和提取规则,返回结构化数据POST /exec:传入 URL 和一段脚本,在页面上下文里执行并返回结果GET /health:健康检查,确认服务和浏览器连接是否正常
为什么把fetch和extract分开?因为很多时候 Agent 只是想要页面文本,不需要结构化。分开之后,fetch可以做得很快,extract才走完整的解析流程。这个区分在批量任务里能省不少时间。
exec这个接口要特别小心。它能力最强,但也最危险。我的做法是:默认关闭,需要显式开启,并且只允许在本地回环地址上调用。即便如此,也不建议在无人值守的场景下开放这个接口。
3.3 页面加载完成的判断逻辑
这是整个项目里最容易踩坑的地方。很多人以为page.goto()返回了就是加载完了,其实不是。现代网页大量使用异步加载,goto返回的时候,主体内容可能还没渲染出来。
我试过几种判断方式:
第一种是等固定时间,比如sleep(3000)。简单粗暴,但要么等太久浪费时间,要么等太短内容没出来。实测下来很不稳。
第二种是等某个特定元素出现。这个比固定时间靠谱,但需要你提前知道目标页面的结构。对于通用服务来说,这个前提不成立。
第三种是等网络空闲。调试协议里有networkidle这样的状态,意思是网络请求在一段时间内没有新增。这个对大多数页面有效,但对那些有轮询、有长连接的页面会一直不空闲。
我最后用的是组合策略:先等domcontentloaded,再等一个较短的时间窗口内网络请求数下降,最后再等一个可配置的额外延迟。这个组合在实测中覆盖了绝大多数场景。如果某个页面特别顽固,还可以通过接口传入自定义的等待选择器,做针对性处理。
注意:等待逻辑不要写死。我见过有人把等待时间硬编码成 5 秒,结果遇到慢页面就失败,遇到快页面就浪费。把等待参数暴露到接口层,让调用方按需调整,才是可持续的做法。
3.4 内容提取的几种模式
页面拿到了,接下来是把内容变成 Agent 能用的格式。我实现了三种提取模式:
纯文本模式:把页面的可见文本抽出来,去掉标签、脚本、样式。适合让 Agent 阅读文章、文档、说明。实现上要注意去掉隐藏元素,不然会把display:none的内容也带出来。
结构化模式:根据 CSS 选择器或 XPath,把特定区域的内容抽成 JSON。适合表格、列表、卡片这类有规律的内容。这个模式需要调用方提供选择器,服务本身不做智能推断。
原始 HTML 模式:直接把渲染后的 HTML 返回。适合调用方自己有解析逻辑的情况。这个模式返回的数据量大,一般只在调试时用。
三种模式通过接口参数切换,默认走纯文本。为什么默认纯文本?因为大多数 Agent 场景下,Agent 需要的是“读懂内容”,而不是“解析结构”。纯文本的 token 消耗也最低。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
整个项目跑起来需要两部分环境:浏览器侧和服务侧。
浏览器侧,你需要一个支持调试协议的 Chromium 内核浏览器。服务侧,我用的是 Node.js 环境,主要依赖两个库:一个负责和浏览器调试协议通信,一个负责 HTTP 服务。为什么选 Node.js?因为调试协议的生态在 Node 侧最成熟,文档和示例最多,遇到问题好查。
安装过程不复杂,初始化一个项目,装上依赖,写一个启动脚本。启动脚本里做两件事:一是拉起专用浏览器实例,二是启动本地 HTTP 服务。这两件事有先后依赖,服务启动时要确认浏览器已经就绪,所以脚本里要加一个重试逻辑。
// 伪代码示意,展示启动顺序 async function bootstrap() { const browser = await launchBrowser({ port: 9222, userDataDir: './profile' }); await waitForBrowserReady(browser); const server = await startServer({ port: 3456 }); console.log('服务已就绪'); }实操心得:
userDataDir这个目录要放在一个不会被系统清理的地方。我一开始放在临时目录里,结果系统重启后登录态全丢了,又得重新登录。后来改到项目目录下的profile文件夹,就稳定了。
4.2 会话连接的建立与保持
浏览器起来了,服务要连上去。调试协议连接的方式是:先访问一个特定的 HTTP 端点,拿到 WebSocket 地址,然后通过 WebSocket 建立长连接。
这里有个细节:浏览器可能同时开了多个页面,你要连的是哪个?我的做法是连到浏览器级别的连接,然后按需创建新页面或复用已有页面。复用已有页面的好处是,如果那个页面已经登录了,新任务直接就能用。
连接建立之后,要保持心跳。调试协议的长连接如果长时间没有消息,可能会被断开。我加了一个定时 ping 的逻辑,每隔一段时间发一个轻量请求,确认连接还活着。如果发现断了,就自动重连。
// 心跳示意 setInterval(async () => { try { await connection.send('Runtime.evaluate', { expression: '1+1' }); } catch (e) { await reconnect(); } }, 30000);4.3 一次完整的请求处理流程
假设 Agent 发来一个请求:访问某个需要登录的页面,提取正文内容。服务这边的处理流程是这样的:
第一步,校验请求参数。URL 是否合法、提取模式是否支持、等待参数是否在合理范围内。这一步不能省,不然一个畸形请求可能让整个服务卡住。
第二步,获取或创建页面。如果当前有可复用的页面,直接用;没有就新建一个。新建页面后,要等页面初始化完成。
第三步,导航到目标 URL。这里要设置超时,防止某些页面一直加载不完。超时时间我设的是 30 秒,超过就返回错误,让调用方决定是否重试。
第四步,等待内容就绪。按前面说的组合策略,等 DOM、等网络、等额外延迟。
第五步,提取内容。按请求指定的模式,走对应的提取逻辑。
第六步,返回结果。把内容、状态码、耗时一起返回,方便调用方判断。
第七步,清理。如果这个页面是临时创建的,用完就关掉,避免页面越开越多。如果是复用的,就保留。
这个流程看起来简单,但每一步都有坑。比如第三步导航的时候,如果目标页面有重定向,调试协议会跟着跳,但跳转后的 URL 可能和你预期的不一样。我在返回结果里会带上最终 URL,让调用方知道实际访问的是哪个页面。
4.4 和 Claude Code / Codex 的对接方式
服务跑起来了,怎么让 Agent 用上?这取决于 Agent 的扩展能力。
如果 Agent 支持自定义工具或函数调用,那就把上面那几个接口包装成工具描述,注册进去。Agent 在需要访问网页的时候,会自动调用这个工具。
如果 Agent 支持 MCP 协议,那就写一个 MCP 服务端,把接口暴露成 MCP 工具。这种方式更标准,兼容性也更好。
如果 Agent 只支持命令行,那就写一个命令行包装脚本,Agent 通过执行命令来调用服务。
我三种方式都试过,实测下来 MCP 方式的体验最好,因为工具描述清晰,Agent 知道什么时候该用、怎么用。命令行方式最通用,但 Agent 需要自己拼参数,容易出错。
注意:无论哪种对接方式,都要在工具描述里写清楚「这个工具会使用你已登录的浏览器会话」。有些 Agent 会因为这个描述而更谨慎地使用它,这其实是好事,能避免误操作。
5. 常见问题与排查技巧实录
5.1 连接不上浏览器怎么办
这是最常见的问题。排查顺序是这样的:
先确认浏览器是不是真的带调试端口启动了。有时候你以为启动了,其实启动的是普通实例,调试端口根本没开。可以在浏览器里访问一个特定的调试端点,看能不能返回页面列表。
再确认端口有没有被占用。如果 9222 端口已经被别的程序占了,浏览器会启动失败或者用别的端口。启动脚本里要检查端口占用情况。
最后确认防火墙或安全软件有没有拦截本地回环连接。有些安全软件会拦截本地程序之间的通信,这种情况需要加白名单。
5.2 页面内容拿不全怎么办
内容拿不全,九成是等待逻辑的问题。先看返回的内容里有没有目标数据,如果没有,说明页面还没渲染完就提取了。
解决办法是调整等待参数。可以先把额外延迟调大,确认是不是时间不够。如果调大之后能拿到,再逐步调小,找到合适的值。
如果调大也不行,那可能是页面用了懒加载,需要滚动才能触发。这种情况需要在提取前先执行一段滚动脚本,把页面滚到底,再等一会儿,然后提取。
还有一种可能是内容在 iframe 里。调试协议默认只操作主框架,iframe 里的内容需要单独处理。这个要在提取逻辑里做框架遍历。
5.3 登录态失效了怎么办
登录态失效的表现是:访问目标页面时,被重定向到登录页,或者返回的内容是登录页的内容。
原因通常是 Cookie 过期了。解决办法很简单:打开专用浏览器实例,手动重新登录一次。登录之后,新的 Cookie 会保存到用户数据目录,服务下次请求就能用上。
如果想减少手动登录的频率,可以在服务里加一个检测逻辑:每次请求后检查最终 URL 是不是登录页,如果是,就返回一个明确的错误码,提醒调用方需要重新登录。这样至少不会让 Agent 拿到错误的内容还以为是正常的。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决方式 |
|---|---|---|---|
| 服务启动报错 | 端口被占用 | 检查端口占用 | 换端口或结束占用进程 |
| 连接浏览器失败 | 调试端口未开 | 检查启动参数 | 确认带调试参数启动 |
| 内容为空 | 等待时间不足 | 调大等待参数 | 增加延迟或等待选择器 |
| 内容为登录页 | 登录态失效 | 检查 Cookie | 手动重新登录 |
| 请求超时 | 页面加载慢 | 检查目标页面 | 调大超时或优化等待 |
| 返回乱码 | 编码问题 | 检查页面编码 | 指定编码或做转码 |
| 页面越开越多 | 未清理临时页面 | 检查清理逻辑 | 用完即关或限制数量 |
5.5 几个我踩过的坑
第一个坑是页面复用带来的状态污染。我一开始为了省事,所有请求都复用同一个页面。结果发现,前一个请求在页面里留下的状态(比如滚动位置、表单内容)会影响后一个请求。后来改成:只读类请求复用页面,写操作类请求每次新建页面。
第二个坑是并发请求把浏览器搞崩。我试过同时发十几个请求,浏览器直接卡死。后来加了并发限制,最多同时处理三个请求,超出的排队。这个数字可以根据机器性能调整,但不要不限制。
第三个坑是错误处理不完整导致服务假死。有一次一个请求卡在某个环节,既没返回成功也没返回失败,后面的请求全堵住了。后来给每个环节都加了超时,任何一个环节超时都走统一的错误返回,保证请求一定会结束。
6. 这个方案还能怎么扩展
基础版本跑通之后,我陆续加了一些扩展能力,这里挑几个实用的说说。
批量任务支持:一次传入多个 URL,服务按顺序处理,返回结果数组。这个在需要抓取多个页面时很有用,Agent 不用一个个发请求。
结果缓存:对相同 URL 和相同提取参数的请求,在一定时间内返回缓存结果。这个能显著减少重复请求,尤其是在调试阶段反复访问同一个页面的时候。
请求日志:把每次请求的 URL、耗时、结果大小记下来。这个对排查问题很有帮助,也能让你知道 Agent 到底在访问什么。
选择器模板:把常用的提取规则存成模板,调用时只传模板名。这样 Agent 不用每次都想选择器怎么写,降低使用门槛。
多浏览器实例:如果你有多个账号需要同时使用,可以启动多个浏览器实例,每个实例对应一个账号,服务根据请求参数路由到不同的实例。这个在需要对比不同账号视角的场景下很有用。
这些扩展都不是必须的,但加上之后,整个工具的实用性会提升一个档次。我的建议是先把基础版本跑稳,再按实际需求逐步加,不要一上来就堆功能。
最后分享一个我在实际使用中的体会:这个工具最大的价值不是“能访问登录页面”,而是“让 Agent 的工作流不再被登录墙打断”。以前 Agent 遇到登录页面就卡住,需要人工介入,整个自动化流程就断了。现在它能自己拿到内容继续往下走,虽然背后还是用的你的登录态,但对 Agent 来说,体验是连贯的。这个连贯性,才是它真正解决问题的地方。