invisible_playwright_mcp 完整接入与配置指南:让 AI 助手用隐身浏览器上网

原创2026-09-30 12:38:401,731 阅读
文章标签:人工智能AI Agent自主智能体虚拟化浏览器控制GUI 自动化后端前端

invisible_playwright_mcp 完整接入与配置指南:让 AI 助手用隐身浏览器上网

invisible_playwright_mcp 是一个把"真实浏览器"交给 AI 驱动的开源项目:用自然语言描述目标,它驱动一台做过反检测加固的 Firefox 引擎,像真人一样移动指针、敲击键盘去点击和输入,目标是"不触发验证码、不被封禁"。本文围绕该项目 README.md 展开,完整覆盖两种使用方式(MCP 接入 AI 助手、独立 Web UI)、全部命令行选项与 .env 配置规则,并结合仓库源码(CLI、引擎下载、会话存储、MCP 服务器)讲清底层原理。读完你既能直接在本机跑起来,也能理解身份、代理、种子、浏览器二进制这些关键概念为什么被设计成现在这个样子。

项目定位:一个"把浏览器当成人来操作"的 Agent

README 开篇对项目的定义只有一句话:"用平实语言告诉它你想做什么,它替你驱动一个真实浏览器,像人一样点击和输入;引擎按"不被检测"来构建,因此不遇验证码、不被封禁。" 换句话说,它解决的痛点不是"能不能自动化浏览器"(Playwright 本身就能),而是"自动化时别被网站识别成机器人"。

从仓库元数据可以印证这一定位:

  • pyproject.toml 中项目描述为 "Anti detect browser and web browsing agent: undetected, no captchas, no blocks",当前版本 0.70.0,要求 Python >= 3.11,运行平台仅声明 Windows 与 Linux;
  • manifest.json 将项目描述为 "AI browser agent: browses, clicks, types, and reads real web pages from plain-English instructions";
  • 引擎本身是"约四分之一 GB 的打过补丁的 Firefox",由核心库从 GitHub Release 下载并缓存到本机(见 engine.py 的模块说明)。

仓库代码结构也呼应这个定位:src/invisible_playwright_mcp/ 下是 MCP 服务器、CLI、会话管理、存储与 Web UI 的全部实现;docs/ 是围绕 Agent 用法的指南文章;articles/ 存放带真实输出截图的工作示例。

两种使用方式:接进你的 AI 助手,或独立打开 Web UI

README 给出了两条平行的使用路径,底层是同一个包:

使用方式 适用场景 核心命令
方式一:通过 MCP 让 Claude Code、Codex、Gemini CLI 等助手获得浏览器工具 uvx invisible-playwright-mcp(无子命令即 MCP 服务器)
方式二:独立 Web UI 自带界面,左侧对话、右侧实时浏览器,需要 OpenRouter 密钥 uvx invisible-playwright-mcp ui --openrouter-key sk-or-...

两条路径都依赖 uv 来安装与执行,所以 README 的第一步永远是装 uv。Windows 在 PowerShell 中:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
$env:Path = "$env:USERPROFILE\.local\bin;$env:Path"

Linux:

curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env

方式一:从你的助手出发,通过 MCP 驱动浏览器

安装好 uv 之后,把本项目的 MCP 服务器注册进你的助手即可。

Claude Code:

claude plugin marketplace add feder-cr/invisible_playwright_mcp
claude plugin install invisible-playwright-mcp@feder-cr

Codex:

codex plugin marketplace add feder-cr/invisible_playwright_mcp
codex plugin add invisible-playwright-mcp@feder-cr

Gemini CLI:

gemini extensions install https://github.com/feder-cr/invisible_playwright_mcp

MCP 服务器的启动形态在仓库里有明确证据:仓库根目录的 mcp.json 声明了一个 stdio 类型的服务器,命令就是 uvx invisible-playwright-mcp;manifest.json 则给出 uv 直跑入口 uv run --directory ... python -m invisible_playwright_mcp。也就是说,无子命令运行 invisible-playwright-mcp 时它就是 MCP 服务器,通过标准输入输出与助手通信——这一点在 cli.py 的 _serve() 中写得很清楚:stdout 是协议通道,任何普通输出都不允许出现在 stdout 上,给终端用户的提示一律走 stderr。

接入之后,你的助手就拥有了一套浏览器工具:打开/关闭浏览器、导航、点击(按选择器或按坐标)、输入、选择下拉项、按键、读取文本/快照/HTML、截图、实时画面、用 JavaScript 读取页面状态。完整工具清单与说明见 mcp/server.py。

方式二:独立运行 Web UI

不想用助手的用户,可以单独把界面跑起来。README 的说法是:"界面我们出,OpenRouter 密钥你带"——左栏是对话,右栏是实时浏览器画面。

Windows PowerShell:

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
$env:Path = "$env:USERPROFILE\.local\bin;$env:Path"
uvx invisible-playwright-mcp ui --openrouter-key sk-or-...

Linux:

curl -LsSf https://astral.sh/uv/install.sh | sh
source $HOME/.local/bin/env
uvx invisible-playwright-mcp ui --openrouter-key sk-or-...

然后打开 http://127.0.0.1:8765,在左栏输入和接进助手时一模一样的自然语言指令即可。

从源码看,这个 UI 命令有几个值得一提的行为(见 cli.py):

  • 默认绑定 127.0.0.1:8765(--host / --port 可改,show_default=True);
  • 没有 OpenRouter 密钥直接拒绝启动,提示信息会点名"没有模型就没有诚实的界面,想免模型手动驱浏览器请用 invisible_playwright 库";
  • 启动顺序上,先解析密钥(省得密钥不合格还白下载浏览器),再在端口开放之前把引擎下载完成(engine_on_disk),这样界面弹出时浏览器已经在磁盘上,避免第一条消息干等一分钟;
  • 模型只来自 OpenRouter(llm.py 中的 BASE_URL = "https://openrouter.ai/api/v1"),并且 OpenRouter 客户端会在请求头带上 HTTP-Referer 与 X-Title 两个应用归属头,用于 OpenRouter 的公开榜单统计。

该让浏览器 Agent 做什么:一个真实的机票询价任务

README 用一段完整示例演示"给浏览器 Agent 下指令"应该长什么样:

Go to <paste the URL>. One way, Milan to Lisbon, economy, one checked bag, one adult. Check every date from the 12th to the 16th of next month, one at a time, and read the cheapest fare for each day. The date field is a calendar widget, so click the days rather than typing them. If a date has no availability, say so. Do not guess a number.

这段指令示范了写好 Agent 任务的三条经验:

  1. 给出明确目标与约束(单程、米兰到里斯本、经济舱、一件托运行李、一位成人);
  2. 描述页面交互方式而不是最终操作("日期字段是日历组件,所以点选日期而不是输入");
  3. 要求如实汇报而非猜测("某天没票就直说,不要编一个数字")。

README 特别强调:它驱动页面的方式和真人一致——指针会移动、按键会被真实按下,而不是注入 JavaScript 改 DOM。这正是它被称为"隐身"的关键:网页端能看到"一次真实的指针轨迹 + 一次真实键盘事件",事件对象的 isTrusted 为 true,这比任何指纹伪装都更难被反爬系统识破。

这一设计在 MCP 服务器内部是强制执行的。服务器给每个模型的指令(mcp/server.py 的 INSTRUCTIONS)规定了四阶梯操作顺序:

  1. 命名工具 + 选择器:browser_click、browser_type、browser_select_option、browser_press_key,选择器直接取自 browser_snapshot,原样透传;
  2. 坐标:browser_snapshot 会给每个元素 at: [x, y](视口像素),browser_click_at 精确移动指针到该点,适合画布、滑块、地图、div 拼出的自定义组件;
  3. 用眼睛看:browser_take_screenshot 截图,人眼(模型视觉)定位后再 browser_click_at;
  4. browser_evaluate:仅用于读取快照看不出的状态。

第 4 档还有一个关键护栏:browser_evaluate 拒绝通过赋值 value/checked/selected 或调用 click()/dispatchEvent()/submit()/requestSubmit() 来改变页面,并在报错里点名该用哪个工具。原因在指令里说得很透:这些做法绕过了键盘与指针,事件到达页面时 isTrusted 为 false——"这是'有什么不是人'最清晰的一个信号,而躲开它正是这个浏览器存在的意义"。

命令行选项全解:proxy、profile、seed 与更多

README 的 Options 一节是全文信息密度最高之处,逐条展开如下(各选项的实现都可在 cli.py 的 BROWSER_OPTIONS 与 llm.py 中找到):

选项 含义 默认值 / 说明
--openrouter-key OpenRouter API 密钥 也可用环境变量 OPENROUTER_API_KEY;Web UI 没有它拒绝启动
--model OpenRouter 模型 id 默认 z-ai/glm-5.3-flash;也可用环境变量 INVISIBLE_MCP_MODEL
--proxy 代理地址 格式 http://user:pass@proxy.example.com:8080 或 socks5://proxy.example.com:1080,host 和 port 都必须写全;时区、locale 与出口流量都跟随它
--binary 你已有的引擎二进制 必须是"封印(seal)"锁定的那个构建,否则启动拒绝——它跳过的是下载,不是版本校验
--seed 整数种子 同一种子 = 同一个浏览器身份,每次运行可复现
--profile-dir 配置目录 登录态与 Cookie 在重启后存活
--headed 显示浏览器窗口 界面本身也会给你看页面
--host / --port 界面监听地址与端口 默认 127.0.0.1 / 8765;README 明确警告:改 host 会把一个没有任何认证的界面暴露出去

几个需要展开的细节:

proxy 的三重跟随。 --proxy 不只是给浏览器配一个出口。README 说"timezone、locale 和 egress 都跟随它"——即代理决定流量出口(egress)、时区(timezone)与语言区域(locale)三者,它们本应是一套自洽的身份信息。从代码看,proxy 最终通过环境变量 STEALTHFOX_PROXY 传给引擎(runner.py 的 child_env),由引擎据此完成整套身份派生。

seed 决定身份。 同样的 seed 产生同样的浏览器指纹,所以 --seed 是"每次运行都保持同一个'人'"的最小手段;而profile 会连 seed 一起保存——browser_open 的工具说明(mcp/server.py)指出:profile 保存 cookie、登录与 seed,"这样登录不会每次访问都换一台硬件回来"。反向推理同样成立:一个 profile 不会固定它的出口,登录从新国家回来,和从新硬件回来一样显眼。

身份汇报是强制的。 服务器会在打开浏览器时把身份一句话说清楚,格式来自 plan.py:identity: seed ... / exit: ... / profile: ... / headless: ...,并且这句话从启动参数反推,而不是从调用参数拼装——设计文档明确记载过"环境变量里设了 profile,回答却说 profile: none"这类两边不同步的缺陷。

.env:把密钥和二进制路径写进文件

与其每次敲 --openrouter-key 和 --binary,README 推荐把它们放进运行目录下的 .env:

OPENROUTER_API_KEY=sk-or-...
STEALTHFOX_BINARY=/path/to/firefox

仓库对 .env 的行为有非常严格的约定(源码见 cli.py 的 load_env_file):

  • 启动时读取,且绝不覆盖已存在的变量,所以优先级铁律是:--flag > 环境变量 > .env > 默认值。设计注释写得很直白:shell 里导出的变量是"用户刚刚做的决定",文件里的一行是"用户几周前做的决定",更近的决定赢;
  • 只读当前目录,不做向上搜索。find_dotenv 那种会向上遍历父目录的做法被明确否决,因为"从子目录运行时可能悄悄捡到别人的文件——不同的密钥、不同的浏览器,而屏幕上没有任何信息告诉你用的是哪一个";
  • 启动那一行只报变量名,绝不打印值(env .env: OPENROUTER_API_KEY 这种格式,见 cli.py),因为值可能正是密钥本身;
  • 用 python-dotenv 中显式声明)。

为什么推荐用环境变量而不是命令行传密钥? README 的理由很实际:--openrouter-key 会把密钥写进你的 shell 历史;在 Linux 上还会出现在进程列表里。而 OPENROUTER_API_KEY 放在环境变量或 .env 里,两者都避免。

环境变量名的"新旧双轨"机制

仓库还维护着一组环境变量的退役兼容(env.py):新名字 INVISIBLE_MCP_HOME(会话存放目录)、INVISIBLE_MCP_MODEL(模型 id)、INVISIBLE_MCP_SESSION_ID(本进程服务的会话),各自对应一个旧名 AIHAWK_HOME、AIHAWK_MODEL、AIHAWK_SESSION_ID。旧名仍然应答(会打印一行告警提示改名),新名优先——"重命名一个配置的最坏方式,是让用户设置的旧值被悄悄忽略",这套机制就是为此保留的。

浏览器引擎:首次下载、缓存与版本封印

整个项目最重的一块是引擎本身:一个约 250MB 的补丁版 Firefox,首次启动时从 GitHub Release 下载并缓存。README 中 --binary 那句"必须是被封印锁定的构建"正是围绕它设计的。

engine.py 的模块说明还原了这段演进史:这个下载曾经有两个发生时机——README 里让人手动跑的 uvx invisible-playwright fetch,或首次打开浏览器时。后者对 MCP 客户端极不友好:一个工具调用干坐几分钟,客户端工具超时一到就报"超时"而不是"正在下载"。现在改为进程自己负责,两条路径各用一种策略:

  • Web UI:前台、端口开放之前下载,终端上有一条实时刷新的进度行(终端里每整百分比一画,管道/日志里每 10% 一画,避免刷屏);
  • MCP 服务器:从 main() 起在守护线程里下载(mcp/server.py),因为客户端建会话时本来就空闲;期间 browser_open 不会阻塞等待,而是回答"下载到 X%"并请你稍后再调,任何客户端超时都不会被触达。

--binary(或环境变量 STEALTHFOX_BINARY)则完全跳过下载——但跳过的是下载,不是校验:启动时仍然要用封印校验该二进制,版本不对就拒绝启动。手动下载命令统一由常量 FETCH_BY_HAND = "uvx invisible-playwright fetch" 提供(engine.py),错误信息里会点名它。

MCP 服务器内部:两个浏览器与一页一浏览器原则

接进助手的 MCP 服务器(mcp/server.py)有几个值得了解的设计决策:

两个固定浏览器:main 与 support。 main 是你的主身份:页面、Cookie、登录、指纹都在它身上,所有不带 browser 参数的调用默认落在这里。support 是专门为"绝不能碰主身份"的场景准备的帮手——指令里给的典型例子是注册账号需要验证邮箱:在 support 里打开临时邮箱站取地址,把地址填进 main 的表单,再回 support 收验证链接。如果在 main 里开第二个标签页,同一个身份会把 Cookie 和指纹带到两个网站,"账号和邮箱就变成同一个人了"。为此,项目刻意删掉了全部标签页工具:一个浏览器只驱动一页,需要第二页就用 support(mcp/server.py 记录了这段删除决定)。

工具名对齐 Microsoft Playwright MCP。 这样提示词在不同客户端间可移植,browser_navigate、browser_click 等名字对模型来说耳熟能详。

无会话概念。 这个服务器进程只服务"一份工作":INVISIBLE_MCP_SESSION_ID 在进程启动时从环境读取一次(mcp/server.py),决定这份工作持久化到哪个文件;模型无法枚举、命名或触达第二份。在 Web UI 里,一个对话 = 一个独立的服务器进程(sessions.py),各自持有自己的 INVISIBLE_MCP_SESSION_ID,从根上保证两个对话的浏览器与磁盘数据互不干扰。

传输方式可切换。 默认 stdio(现有客户端都这么连);设 STEALTHFOX_MCP_TRANSPORT=http 则改用 streamable HTTP,允许多个客户端挂到同一个活浏览器上。注意 HTTP 模式默认端口是 8766 而不是 8765——8765 被 Web UI 占着,这是刻意避开的冲突(mcp/server.py)。

工具的完整清单(均见 mcp/server.py):

类别 工具
浏览器生命周期 browser_open、browser_close、browser_list、browser_status
阅读页面 browser_navigate(返回 HTTP 状态与落地 URL)、browser_read_text、browser_snapshot、browser_read_html、browser_take_screenshot、browser_watch、browser_evaluate
操作页面 browser_click、browser_click_at(支持按住)、browser_type、browser_select_option、browser_press_key

会话数据存在哪里,怎么删除

README 的隐私一节给出了明确的存储位置:会话、profile 与截图都存在本机,路径由 INVISIBLE_MCP_HOME 决定,未设置时落在系统应用数据目录。源码里 storage.py 的实现是:INVISIBLE_MCP_HOME 优先,否则按平台取标准位置(Windows 的 %APPDATA%、macOS 的 ~/Library/Application Support、Linux 的 XDG_DATA_HOME 或 ~/.local/share),目录名固定为 invisible-playwright-mcp。

每个会话拆成两类文件:浏览器状态(store 负责)与对话记录(chats 负责),统一落在 <home>/<kind>/<sanitized-id>.json(storage.py)。写入一律原子替换(先写 .writing 再 os.replace),保证任何时刻都不会留下半个 JSON;读不动/解析不了的文件按"没保存过"处理,而不是让服务器在第一次调用时抛异常。

隐私边界与负责任使用

README 的隐私一节把"什么东西离开你的电脑、去了哪里"列得非常具体:

  • 你访问的网站看到的是一个普通 Firefox(它们本来就会看到浏览器);
  • 你的模型提供商。Web UI 会把对话和 Agent 在页面上读到的东西经你的密钥发给 OpenRouter;走 MCP 时,由你接入的客户端用它所配的模型做同样的事;
  • GitHub。服务器或界面首次启动时从 GitHub Release 下载引擎;设置代理时会下载一个 GeoIP 数据库;每次启动浏览器还会抓一次一行计数文件——这就是启动次数统计的来源,请求不携带任何标识符或你的数据,GitHub 能看到的只是任何 HTTPS 请求都会显示的你的 IP 地址。

除此之外不收集任何东西,也不向作者发送任何数据;本机数据由你随时可删。README 的"负责任使用"一节则提醒:自动化的是你控制下的浏览器,请阅读目标网站的服务条款、尊重它们的速率限制、不要提交任何未经真人阅读的内容。

许可证方面,当前为 MIT(见 LICENSE);2026 年 9 月 2 日之前发布的版本采用 AGPL-3.0 并保持该授权不变。

继续深入:仓库里的指南、示例与测试

综上,invisible_playwright_mcp 的核心价值可以概括为三件事:一个 MCP 服务器让任何主流 AI 助手获得"隐身浏览器"工具集;一个 Web UI让不接助手的用户也能对话式驱动浏览器;以及贯穿两者的身份工程(seed/指纹/profile/proxy 的联动与隔离),把"像一个真实的人"从口号变成可配置、可复现、可审计的默认行为。

登录后查看全文
invisible_dots