[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-1020":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":22,"archived":23,"fork":23,"defaultBranch":24,"hasWiki":25,"hasPages":23,"topics":26,"createdAt":10,"pushedAt":10,"updatedAt":27,"readmeContent":28,"aiSummary":29,"trendingCount":16,"starSnapshotCount":16,"syncStatus":30,"lastSyncTime":31,"discoverSource":32},1020,"ruyipage","LoseNine\u002Fruyipage","LoseNine","下一代Python的web自动化过检测框架，通过一切网站检测的Firefox内核浏览器。RuyiPage is a Python-based Firefox automation framework built on the next-generation WebDriver BiDi protocol.","",null,"Python",923,168,3,1,0,100,198,513,300,10.68,"BSD 3-Clause \"New\" or \"Revised\" License",false,"main",true,[],"2026-06-12 02:00:22","# ruyiPage\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"images\u002Fruyipage.png\" width=\"320\" alt=\"ruyiPage logo\" \u002F>\n\u003C\u002Fp>\n\n[简体中文](.\u002FREADME.md) | [English](.\u002FREADME_EN.md)\n\n> 专用于 **AI 分析** 和 **数据采集** 场景，可拦截任意请求响应包。自带好用的过检测 Win\u002FLinux 火狐指纹浏览器内核。\n>\n> Built for **AI analysis** and **data capture** workflows, with the ability to intercept arbitrary request and response packets. Ships with a battle-tested anti-detection Firefox fingerprint browser engine for Windows and Linux.\n\n> **下一代自动化框架**\n>\n> - 自带**过检测火狐内核**\n> - 大量 **`isTrusted`** 原生动作，**无自动化检测点**\n> - 支持多种 JS 事件构造附加 **`ruyi: true`**，让 `Event` \u002F `InputEvent` \u002F `MouseEvent` \u002F `KeyboardEvent` 等事件的 **`isTrusted`** 更贴近真实交互\n> - 支持 **ADS** 等指纹浏览器**直接自动化接管**\n> - 基于 **Firefox + WebDriver BiDi**\n> - 更适合**高风控场景**\n\n[![PyPI version](https:\u002F\u002Fimg.shields.io\u002Fpypi\u002Fv\u002FruyiPage.svg)](https:\u002F\u002Fpypi.org\u002Fproject\u002FruyiPage\u002F)\n[![Python Versions](https:\u002F\u002Fimg.shields.io\u002Fpypi\u002Fpyversions\u002FruyiPage)](https:\u002F\u002Fpypi.org\u002Fproject\u002FruyiPage\u002F)\n[![Last Commit](https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Flast-commit\u002FLoseNine\u002Fruyipage)](https:\u002F\u002Fgithub.com\u002FLoseNine\u002Fruyipage\u002Fcommits\u002Fmain)\n[![GitHub stars](https:\u002F\u002Fimg.shields.io\u002Fgithub\u002Fstars\u002FLoseNine\u002Fruyipage?style=social)](https:\u002F\u002Fgithub.com\u002FLoseNine\u002Fruyipage\u002Fstargazers)\n[![Downloads](https:\u002F\u002Fstatic.pepy.tech\u002Fbadge\u002Fruyipage)](https:\u002F\u002Fpepy.tech\u002Fproject\u002Fruyipage)\n\n## 请我喝咖啡\n\n如果这个项目对你有帮助，欢迎请我喝杯咖啡，支持我继续完善 `ruyiPage`。\n\n\u003Ctable>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\n      \u003Cb>公众号\u003C\u002Fb>\u003Cbr>\n      \u003Cimg src=\"images\u002Fgzh.jpg\" width=\"220\" alt=\"公众号二维码\" \u002F>\n    \u003C\u002Ftd>\n    \u003Ctd align=\"center\">\n      \u003Cb>QQ 社群\u003C\u002Fb>\u003Cbr>\n      \u003Cimg src=\"images\u002Fqq.jpg\" width=\"220\" alt=\"QQ 社群二维码\" \u002F>\n    \u003C\u002Ftd>\n    \u003Ctd align=\"center\">\n      \u003Cb>联系我 \u002F 个人微信\u003C\u002Fb>\u003Cbr>\n      \u003Cimg src=\"images\u002Fweixin.jpg\" width=\"220\" alt=\"个人微信二维码\" \u002F>\n    \u003C\u002Ftd>\n    \u003Ctd align=\"center\">\n      \u003Cb>请我喝咖啡\u003C\u002Fb>\u003Cbr>\n      \u003Cimg src=\"images\u002Fweixingoot.jpg\" width=\"220\" alt=\"收款码\" \u002F>\n    \u003C\u002Ftd>\n  \u003C\u002Ftr>\n\u003C\u002Ftable>\n\n---\n\n## 配套项目\n\n如果你准备把 `ruyiPage` 用在 AI 自动化分析、复杂网页采集或高风控页面场景，建议先看这些配套项目：\n\n- 📘 **官方文档 \u002F 自动化文档**\n  更系统地查看 `ruyiPage` 相关自动化说明、接入方式和配套能力说明：\u003Chttps:\u002F\u002F0xshoulderlab.site\u002Fautomation>\n- 🦊 **Firefox 指纹浏览器项目**\n  用于需要 Firefox 指纹环境、浏览器接管或更高真实度自动化场景，适合和 `ruyiPage` 搭配使用：\u003Chttps:\u002F\u002Fgithub.com\u002FLoseNine\u002Ffirefox-fingerprintBrowser>\n- 🟨 **JavaScript 实现：ruyipage-js**\n  面向 JavaScript \u002F Node.js 生态的配套实现，适合希望在 JS 项目里接入 `ruyiPage` 思路与能力的场景：\u003Chttps:\u002F\u002Fgithub.com\u002FGanFish404\u002Fruyipage-js>\n- 🐹 **Go 语言实现：ruyipage-go**\n  由社区实现的 Go 版本，适合需要在 Go 项目中接入 Firefox 自动化能力的场景。感谢 @pll177 的实现与维护：\u003Chttps:\u002F\u002Fgithub.com\u002Fpll177\u002Fruyipage-go>\n- 🖥️ **桌面端 GUI 管理工具：ruyiBrowser-GUI**\n  基于 Electron + Vue3 的 Firefox 指纹浏览器图形化管理工具，无需命令行即可创建、管理和启动多个独立指纹环境。感谢 @jacklaigougou 的实现与维护：\u003Chttps:\u002F\u002Fgithub.com\u002Fjacklaigougou\u002FruyiBrowser-GUI>\n\n---\n\n## 实战展示\n\n下面这些图放的是实际场景展示。为了在 GitHub 首页里更紧凑，我这里用两列表格展示。\n\n\u003Ctable>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Cb>可直接通过 Cloudflare 5s 盾\u003C\u002Fb>\u003Cbr>\u003Cimg src=\"images\u002Fcloudfare.jpg\" width=\"320\" alt=\"Cloudflare 5s challenge\" \u002F>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Cb>可直接通过 hCaptcha\u003C\u002Fb>\u003Cbr>\u003Cimg src=\"images\u002Fhcapture.jpg\" width=\"320\" alt=\"hCaptcha\" \u002F>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Cb>可直接通过 DataDome\u003C\u002Fb>\u003Cbr>\u003Cimg src=\"images\u002Fdatadome.jpg\" width=\"320\" alt=\"DataDome\" \u002F>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Cb>可直接进入 Outlook Mail\u003C\u002Fb>\u003Cbr>\u003Cimg src=\"images\u002Foutlook.jpg\" width=\"320\" alt=\"Outlook Mail\" \u002F>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Cb>可直接进入 Google Mail\u003C\u002Fb>\u003Cbr>\u003Cimg src=\"images\u002Fgoogle.jpg\" width=\"320\" alt=\"Google Mail\" \u002F>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Cb>bet365 实战展示\u003C\u002Fb>\u003Cbr>\u003Cimg src=\"images\u002Fbet365.png\" width=\"320\" alt=\"bet365 Demo\" \u002F>\u003C\u002Ftd>\n  \u003C\u002Ftr>\n  \u003Ctr>\n    \u003Ctd align=\"center\">\u003Cb>指纹浏览器指纹页展示\u003C\u002Fb>\u003Cbr>\u003Cimg src=\"images\u002Ffingerprint.png\" width=\"320\" alt=\"Fingerprint Browser Demo\" \u002F>\u003C\u002Ftd>\n    \u003Ctd align=\"center\">\u003Cb>Firefox 路线真实场景能力\u003C\u002Fb>\u003Cbr>更适合高风控页面、登录流、验证码与真实交互场景\u003C\u002Ftd>\n  \u003C\u002Ftr>\n\u003C\u002Ftable>\n\n> 这些展示图用于说明 `ruyiPage` 在 Firefox 路线下的真实场景能力。\n> 如果目标站点风控更强，仍建议优先配合本项目推荐的 Firefox 内核方案，或任意可用的火狐指纹浏览器使用。\n\n---\n\n## 安装与使用\n\n### 安装\n\n```bash\npip install ruyiPage --upgrade\n```\n\n如果你是首次安装，也可以直接用上面的命令获取最新版。\n\n如果需要**异步（async\u002Fawait）支持**：\n\n```bash\npip install ruyiPage[async] --upgrade\n```\n\n这会额外安装 `greenlet` 和 `websockets`，同步 API 完全不受影响。\n\n如果你是从源码运行，或给学员分发项目源码，建议同时安装项目依赖：\n\n```bash\npip install -r requirements.txt\n```\n\n安装后建议先确认：\n\n```bash\npython -c \"import ruyipage; print(ruyipage.__version__)\"\n```\n\n### 最简单启动\n\n```python\nfrom ruyipage import FirefoxPage\n\npage = FirefoxPage()\npage.get(\"https:\u002F\u002Fwww.example.com\")\nprint(page.title)\npage.quit()\n```\n\n### 异步（async\u002Fawait）启动\n\n```python\nimport asyncio\nfrom ruyipage.aio import launch\n\nasync def main():\n    page = await launch()\n    await page.get(\"https:\u002F\u002Fwww.example.com\")\n    title = await page.get_title()\n    print(title)\n\n    el = await page.ele(\"#search\")\n    await el.click_self()\n    await el.input(\"hello async\")\n\n    await page.quit()\n\nasyncio.run(main())\n```\n\n异步 API 的方法名与同步版完全一致，只需加 `async\u002Fawait`。\n属性（如 `page.title`）变为异步方法（如 `await page.get_title()`）。\n完整示例见根目录 `quickstart_bing_search_async.py` 和 `quickstart_cloudflare_async.py`。\n\n### JS 事件 `isTrusted` 对比能力\n\n`ruyiPage` 不只是支持原生点击、输入、悬停这类高 `isTrusted` 动作，也支持在多种 JS 事件构造里附加 `ruyi: true`，用于让事件的 `isTrusted` 表现与真实交互更一致。\n\n例如：\n\n```javascript\nnew Event('change', { bubbles: true, ruyi: true })\nnew InputEvent('input', { bubbles: true, data: 'A', inputType: 'insertText', ruyi: true })\nnew MouseEvent('click', { bubbles: true, clientX: 12, clientY: 24, ruyi: true })\nnew KeyboardEvent('keydown', { bubbles: true, key: 'Enter', code: 'Enter', ruyi: true })\n```\n\n可直接运行综合示例：\n\n```bash\npython examples\u002F45_js_setter_untrusted_input.py\n```\n\n这个示例会对比普通 JS 事件与 `ruyi: true` 事件的 `isTrusted`，覆盖：\n\n- `Event`\n- `InputEvent`\n- `KeyboardEvent`\n- `MouseEvent`\n- `FocusEvent`\n- `CustomEvent`\n- `PointerEvent`\n- `WheelEvent`\n\n### 指定 Firefox 路径和 userdir\n\n```python\nfrom ruyipage import FirefoxOptions, FirefoxPage\n\nopts = FirefoxOptions()\nopts.set_browser_path(r\"D:\\Firefox\\firefox.exe\")\nopts.set_user_dir(r\"D:\\ruyipage_userdir\")\n\npage = FirefoxPage(opts)\npage.get(\"https:\u002F\u002Fwww.example.com\")\nprint(page.title)\npage.quit()\n```\n\n其中：\n\n- **`browser_path`**：Firefox 可执行文件路径。适合 Firefox 不在默认安装目录、有多个版本或使用便携版的情况。\n- **`user_dir`**：Firefox 的 profile \u002F 用户目录。适合想复用登录状态、保留 Cookie \u002F 本地存储、复用扩展和首选项的情况。如果不设置，`ruyiPage` 会自动创建临时 profile，适合一次性测试，关闭后通常会被清理。\n\n### 更适合新手的 launch\n\n```python\nfrom ruyipage import launch\n\npage = launch(\n    browser_path=r\"D:\\Firefox\\firefox.exe\",\n    user_dir=r\"D:\\ruyipage_userdir\",\n    headless=False,\n    close_on_exit=True,\n    port=9222,\n)\n\npage.get(\"https:\u002F\u002Fwww.example.com\")\nprint(page.title)\npage.quit()\n```\n\n其中：\n\n- `close_on_exit=True` 表示 Python 程序退出时，自动关闭由 `ruyiPage` 启动的浏览器。\n- 如果你希望脚本退出后保留浏览器窗口继续手动操作，可以改成 `close_on_exit=False`。\n- 如果你用的是 `attach()` 或 `existing_only(True)` 接管已有浏览器，即使开启 `close_on_exit=True`，退出时也只会断开连接，不会误关外部浏览器。\n\n### FirefoxOptions 常用 API\n\n如果你准备把浏览器启动行为写得更可控，推荐直接使用 `FirefoxOptions`。\n\n先看一个覆盖常见选项的完整例子：\n\n```python\nfrom ruyipage import FirefoxOptions, FirefoxPage\n\nopts = FirefoxOptions()\nopts.set_browser_path(r\"D:\\Firefox\\firefox.exe\")\nopts.set_user_dir(r\"D:\\ruyipage_userdir\")\nopts.set_port(9222)\nopts.set_proxy(\"http:\u002F\u002F127.0.0.1:7890\")\nopts.set_window_size(1440, 900)\nopts.headless(False)\nopts.private_mode(False)\nopts.close_on_exit(True)\n\npage = FirefoxPage(opts)\npage.get(\"https:\u002F\u002Fwww.example.com\")\nprint(page.title)\npage.quit()\n```\n\n下面这张表把目前用户可直接使用的 `opt` 选项做一个集中说明。\n\n| 方法 | 作用 | 常见场景 |\n| --- | --- | --- |\n| `set_browser_path(path)` | 指定 Firefox 可执行文件路径 | Firefox 不在默认目录、使用便携版、机器上装了多个 Firefox |\n| `set_address(address)` | 设置调试地址 `host:port` | 你已经有固定调试地址，想直接连指定实例 |\n| `set_port(port)` | 设置远程调试端口 | 同机多开、避免和别的浏览器端口冲突 |\n| `set_auto_port(True)` | 自动寻找可用端口 | 不想自己手动挑端口，适合脚本批量启动 |\n| `existing_only(True)` | 只接管已有浏览器，不启动新浏览器 | 连接手动启动的 Firefox、ADS、指纹浏览器 |\n| `set_retry(times, interval)` | 设置连接重试次数和间隔 | 启动慢、远程环境抖动、端口就绪较慢 |\n| `set_profile(path)` | 指定 Firefox profile 目录 | 想长期复用登录态、Cookie、扩展、首选项 |\n| `set_user_dir(path)` | `set_profile()` 的新手友好别名 | 教程或团队脚本里更习惯写 `user_dir` |\n| `close_on_exit(True\u002FFalse)` | 设置 Python 退出时是否自动关闭浏览器 | 默认 `True`，适合脚本跑完自动收尾；传 `False` 可保留浏览器继续手动操作 |\n| `private_mode(True\u002FFalse)` | 开启 Firefox 原生私密模式 | 不想带上普通窗口历史状态，想走私密会话 |\n| `headless(True\u002FFalse)` | 设置无头模式 | 服务器运行、后台任务、无需显示界面 |\n| `set_argument(arg, value=None)` | 追加自定义启动参数 | 需要透传 Firefox 原生启动参数 |\n| `remove_argument(arg)` | 移除之前设置过的启动参数 | 复用配置对象时撤销某个参数 |\n| `set_pref(key, value)` | 写入 Firefox 首选项 | 调整 about:config、代理策略、下载行为等 |\n| `set_window_size(width, height)` | 设置启动窗口大小 | 控制初始分辨率、适配目标站点布局 |\n| `set_proxy(proxy)` | 设置 HTTP \u002F HTTPS \u002F SOCKS 代理 | 需要代理出口、IP 切换、地域访问 |\n| `set_download_path(path)` | 设置默认下载目录 | 自动下载文件并落盘到固定目录 |\n| `set_load_mode(mode)` | 设置页面加载等待策略 | 在速度和稳定性之间做取舍 |\n| `set_timeouts(base, page_load, script)` | 设置元素查找、页面加载、脚本执行超时 | 页面慢、接口慢、脚本执行时间较长 |\n| `set_user_prompt_handler(handler)` | 设置 alert \u002F confirm \u002F prompt 默认处理策略 | 自动接受或取消弹窗，避免流程被阻塞 |\n| `set_fpfile(path)` | 通过 `--fpfile` 传入指纹配置文件 | 配合支持该参数的 Firefox \u002F 指纹浏览器使用 |\n| `enable_xpath_picker(True\u002FFalse)` | 启用页面 XPath 选择浮窗 | 录元素、看 XPath、生成定位代码 |\n| `enable_action_visual(True\u002FFalse)` | 启用鼠标行为可视化调试 | 调试拟人移动、点击轨迹、键盘输入 |\n| `quick_start(...)` | 一次性设置常用启动参数 | 给新手脚本或快速演示准备统一入口 |\n\n说明：\n\n- `close_on_exit(True)` 默认开启，但只会自动关闭 **ruyiPage 自己启动的浏览器**。\n- 如果你是通过 `existing_only(True)` 或 `attach()` 接管外部浏览器，Python 退出时只会断开连接，不会误关用户手动打开的浏览器。\n- 不设置 `user_dir` \u002F `profile` 时，`ruyiPage` 会自动创建临时 profile，更适合一次性脚本。\n- `set_fpfile()` 当前主要是把路径通过 `--fpfile=...` 传给浏览器，并读取其中的代理认证字段；它不是一个自动填充所有浏览器指纹参数的万能入口。\n- `quick_start()` 适合快速开始，但不是全部配置项的替代品；需要精细控制时，仍建议直接组合 `FirefoxOptions` 的各个方法。\n\n如果你只是想快速启动，优先用 `launch()`；如果你想把浏览器行为写得更明确、更适合对外给用户使用，优先用 `FirefoxOptions`。\n\n### 开启隐私模式\n\n```python\nfrom ruyipage import FirefoxOptions, FirefoxPage, launch\n\n# 方式一：在配置对象上开启 Firefox 私密浏览模式\nopts = FirefoxOptions()\nopts.private_mode(True)\n\npage = FirefoxPage(opts)\npage.get(\"https:\u002F\u002Fwww.example.com\")\npage.quit()\n\n# 方式二：直接用 launch()\npage = launch(private=True)\npage.get(\"https:\u002F\u002Fwww.example.com\")\npage.quit()\n```\n\n说明：\n\n- `private=True` \u002F `opts.private_mode(True)` 会为 Firefox 增加 `-private` 启动参数\n- 这和默认的临时 `profile` 不是一回事\n- 如果你只是想要一次性会话，不复用历史数据，不传 `user_dir` 也可以\n- 完整示例可参考 `examples\u002F` 目录\n\n### 启用 XPath Picker\n\n\u003Cp align=\"center\">\n  \u003Cimg src=\"images\u002Fxpath.png\" width=\"900\" alt=\"XPath Picker with ruyiPage code generation\" \u002F>\n\u003C\u002Fp>\n\n```python\nfrom ruyipage import FirefoxOptions, FirefoxPage, launch\n\n# 方式一：在 FirefoxOptions 上开启\nopts = FirefoxOptions()\nopts.enable_xpath_picker(True)\n\npage = FirefoxPage(opts)\npage.get(\"https:\u002F\u002Fwww.example.com\")\n\n# 方式二：直接用 launch()\npage = launch(xpath_picker=True)\npage.get(\"https:\u002F\u002Fwww.example.com\")\n```\n\n启用后，页面右下角会出现一个半透明磨砂玻璃浮窗：\n\n- 点击页面元素时，会锁定当前结果\n- 浮窗会显示元素名字、文本、XPath 绝对路径、XPath 相对路径、元素中心 `(x, y)`\n- 内置 `ruyiPage代码生成` 选项卡，会自动生成对应元素获取代码\n- iframe、嵌套 iframe、open shadow root 场景会自动拼好访问链\n- `XPath (absolute)`、`XPath (relative)`、`ruyiPage代码生成` 都支持一键复制\n- 锁定后不会继续切换到其他元素\n- 点击浮窗里的 `继续选择` 后，才会重新允许选择其他元素\n- 点击浮窗里的 `暂停选择` 可暂时停止拦截页面点击\n- 点击浮窗里的 `收起` 可折叠为右下角小胶囊，再点击即可展开\n\n推荐直接运行用户示例：\n\n```bash\npython examples\u002F42_xpath_picker_complex_showcase.py\n```\n\n这个示例会打开一套专门的复杂测试页面，覆盖：\n\n- 普通页面元素\n- 同源 iframe \u002F 嵌套 iframe\n- open shadow root\n- 复杂文本节点与 SVG 节点\n\n### 鼠标行为可视化调试\n\n`ruyiPage` 现在支持 `action_visual=True` 的鼠标行为可视化调试模式，适合排查自动化流程里“鼠标到底移动到了哪里、实际点到了哪里”这类问题。\n\n开启后会显示：\n\n- BiDi 鼠标移动轨迹可视化\n- BiDi 点击位置高亮 \u002F 闪烁提示\n- 当前鼠标坐标\n- 当前点击目标元素高亮\n- 框架内置 JS click \u002F JS input 的鼠标反馈\n\n当前这套调试模式聚焦在**鼠标行为**，主要覆盖：\n\n- `page.actions.move_to()` \u002F `move()` \u002F `human_move()`\n- `page.actions.click()` \u002F `double_click()` \u002F `human_click()`\n- `page.actions.drag_to()` \u002F `hold()` \u002F `release()`\n- `ele.click.left()` \u002F `click_self()` \u002F `double_click()`\n- `ele.click.by_js()`\n- `ele.input(..., by_js=True)` 的鼠标定位反馈\n\n最简单启动方式：\n\n```python\nfrom ruyipage import launch\n\npage = launch(action_visual=True, headless=False)\n```\n\n如果你是通过 `options` 配置，也可以这样开启：\n\n```python\nfrom ruyipage import FirefoxOptions, FirefoxPage\n\nopts = FirefoxOptions()\nopts.enable_action_visual(True)\nopts.headless(False)\n\npage = FirefoxPage(opts)\npage.get('https:\u002F\u002Fwww.example.com')\n```\n\n如果你想直接看本地完整演示，可以运行：\n\n```bash\npython examples\u002F42_2_action_visual_showcase.py\n```\n\n如果你想专门演示 `human_move()` \u002F `human_click()` 的拟人轨迹，并同时观察\n两套算法（`bezier` \u002F `windmouse`）的鼠标路径差异，可以运行：\n\n```bash\npython examples\u002F46_human_behavior_showcase.py\n```\n\n这个示例会：\n\n- 自动打开专门的本地 HTML 演示页\n- 开启 `action_visual=True`，直接显示鼠标轨迹\n- 依次演示 `bezier` 和 `windmouse` 两套拟人轨迹算法\n- 再演示 `human_type()` 的拟人输入效果\n\n相关接口：\n\n```python\nopts.set_human_algorithm(\"windmouse\")\n\npage.actions.human_move(ele, algorithm=\"bezier\", style=\"arc\").perform()\npage.actions.human_move(ele, algorithm=\"windmouse\").perform()\npage.actions.human_click(ele, algorithm=\"windmouse\").perform()\n```\n\n说明：\n\n- `algorithm` 可选 `\"bezier\"` 或 `\"windmouse\"`\n- `style` 仅对 `bezier` 生效\n- 如果不传 `algorithm`，会优先使用 `FirefoxOptions.set_human_algorithm()` 的默认值\n\n该示例会使用专门的本地鼠标演示页，集中展示：\n\n- BiDi 鼠标轨迹\n- 点击位置与目标高亮\n- 拖拽轨迹\n- JS click 的可视化反馈\n\n### 接管已打开的浏览器\n\n如果 Firefox 已经是你手动打开的，或者是指纹浏览器先打开的，也可以直接接管现有实例。\n\n这套方式适用于任意 **Firefox 内核指纹浏览器**，包括 ADS \u002F FlowerBrowser 这类产品。\n如果浏览器允许固定启动参数，建议加入：\n\n```text\n--remote-debugging-port=9222\n```\n\n如果后台会把它改写成随机端口，也可以直接使用按进程特征的自动探测接管。\n\n```python\nfrom ruyipage import auto_attach_exist_browser_by_process\n\npage = auto_attach_exist_browser_by_process(\n    latest_tab=True,\n)\n\nprint(page.browser.address)\nprint(page.title)\nprint(page.url)\n```\n\n适合这些情况：\n\n- 你想先手动打开 Firefox，再让 `ruyiPage` 接管\n- 你想先启动指纹浏览器，再从业务脚本里连进去\n- 你使用的是 ADS \u002F FlowerBrowser，真实调试端口会随机变化\n- 你不想手动维护端口范围，希望直接按 Firefox 进程特征自动探测\n\n---\n\n## 项目定位与技术路线\n\n`ruyiPage` 是一个面向 **Firefox 浏览器自动化** 的 Python 库，底层协议来自：\n\n- WebDriver BiDi: https:\u002F\u002Fw3c.github.io\u002Fwebdriver-bidi\u002F\n\n> 面向 **Firefox** 的高层自动化框架，核心思想是 **用 WebDriver BiDi 做底层、用新手易用 API 做上层**。\n\n与大量依赖 CDP（Chrome DevTools Protocol）的自动化库不同，`ruyiPage`：\n\n- 以 **Firefox** 为核心浏览器，以 **WebDriver BiDi** 为核心协议，**不依赖 CDP**\n- 天然没有 CDP 路线的暴露面，更贴近 W3C 新一代浏览器自动化协议方向\n- **原生动作链优先**，尽量让输入、拖拽、点击等行为保持 `isTrusted`\n- **内置拟人行为能力**，更适合高风控页面的真实交互场景\n- **支持网络劫持、拦截、mock、collector 等能力**\n- **支持 user context 隔离**，适合同浏览器多账号、多会话并行\n- **高层 API 可直接上手**，更适合新手和团队统一维护\n\n### 高风控场景推荐\n\n如果你的目标站点对自动化非常敏感，优先推荐使用本项目提供的 Firefox 内核方案，或配合任意 Firefox 指纹浏览器使用：\n\n- https:\u002F\u002Fgithub.com\u002FLoseNine\u002Ffirefox-fingerprintBrowser\n\n建议流程：1) 优先使用 Firefox 内核方案 → 2) 再用 `ruyiPage` 做自动化控制，整体效果更稳定。\n\n---\n\n## 能力总览\n\n在看详细文档前，你可以先看这张总表，快速了解 `ruyiPage` 现在已经能做什么。\n\n| 能力大类 | 高层入口 | 典型能力 |\n| --- | --- | --- |\n| 页面导航 | `page.get()` \u002F `page.back()` \u002F `page.forward()` | 打开页面、刷新、前进后退 |\n| 元素查找 | `page.ele()` \u002F `page.eles()` \u002F `ele.ele()` | CSS\u002FXPath\u002FText 定位、容器内继续查找 |\n| 元素交互 | `ele.click_self()` \u002F `ele.input()` \u002F `ele.attr()` \u002F `ele.text` | 点击、输入、取属性、读文本 |\n| 动作链 | `page.actions` | 键盘、鼠标、拖拽、滚轮、拟人动作 |\n| 触摸输入 | `page.touch` | tap、long press 等触摸操作 |\n| Cookies | `page.get_cookies()` \u002F `page.set_cookies()` \u002F `page.delete_cookies()` | 读取、设置、删除 Cookie |\n| 下载 | `page.downloads` | 设置下载目录、等待下载事件、验证落盘 |\n| PDF \u002F 截图 | `page.save_pdf()` \u002F `page.screenshot()` | 页面打印 PDF、保存截图 |\n| 弹窗处理 | `page.wait_prompt()` \u002F `page.accept_prompt()` \u002F `page.set_prompt_handler()` | alert \u002F confirm \u002F prompt |\n| 导航事件 | `page.navigation` | navigationStarted、load、historyUpdated 等 |\n| 通用事件 | `page.events` | browsingContext \u002F network \u002F script \u002F input \u002F log 事件 |\n| 网络控制 | `page.network` \u002F `page.intercept` | 请求头、缓存控制、拦截、mock、fail、collector |\n| 浏览上下文 | `page.contexts` | getTree、create tab\u002Fwindow、reload、viewport |\n| 浏览器级能力 | `page.browser_tools` | user context、client window |\n| 脚本能力 | `page.get_realms()` \u002F `page.eval_handle()` \u002F `page.disown_handles()` | realms、远程对象句柄、preload script |\n| Emulation | `page.emulation` | UA、viewport、screen、orientation、JS 开关 |\n| WebExtension | `page.extensions` | 安装目录扩展、安装 xpi、卸载 |\n| 本地存储 | `page.local_storage` \u002F `page.session_storage` | 读写本地存储和会话存储 |\n\n---\n\n## 和其他框架怎么选\n\n下面这个表不讨论“谁绝对更强”，只突出你最关心的几个点：\n\n- 各自主要偏向什么浏览器\n- 是否依赖 CDP\n- CDP 暴露面强不强\n- Firefox \u002F BiDi 支持度怎么样\n- 针对性被检测情况怎么样\n\n| 框架 | 主要浏览器方向 | 底层协议 | CDP 暴露面 | Firefox \u002F BiDi 支持度 | 针对性被检测 |\n| --- | --- | --- | --- | --- | --- |\n| `ruyiPage` | **Firefox** | **WebDriver BiDi** | **无 CDP 暴露面** | **高**，主路线就是 Firefox + BiDi | **低**，原生 BiDi + `isTrusted` 行为 + 拟人操作，更适合高风控场景；配合本项目推荐的 Firefox 内核方案或任意火狐指纹浏览器会更稳定 |\n| Playwright | Chromium \u002F Firefox \u002F WebKit | 自有协议，很多能力仍偏 Chromium | 中到高 | 中，支持 Firefox，但不是以 Firefox BiDi 为核心设计 | 中到高，很多站点会优先针对主流自动化指纹做识别 |\n| Selenium | 多浏览器 | WebDriver Classic + 部分 BiDi | 低到中 | 中，兼容广，但高层 BiDi 能力不算强 | 中，传统自动化特征和使用面都比较广 |\n| Puppeteer | Chromium | CDP | **高** | 低，基本不是 Firefox 主战场 | **高**，CDP 路线暴露面更明显，也更容易被针对性检测 |\n| DrissionPage | Chromium | 混合驱动思路，核心仍偏 Chromium | 中到高 | 低，Firefox 不是主方向 | 中到高，更偏 Chromium 自动化场景，同样容易落入主流检测面 |\n\n### 一句话建议\n\n- 你主做 **Firefox 自动化**：优先 `ruyiPage`\n- 你要 **多浏览器统一自动化**：优先 Playwright \u002F Selenium\n- 你主做 **Chromium\u002FCDP**：优先 Puppeteer \u002F Playwright\n- 你想要 **Firefox + 不依赖 CDP + BiDi 高层封装**：`ruyiPage` 是更对路的选择\n\n---\n\n## 根目录快速开始示例\n\n### 1. Bing 搜索示例\n\n文件：`quickstart_bing_search.py`\n\n它会：\n\n- 打开 Bing\n- 输入关键词\n- 回车搜索\n- 抓取前 3 页结果\n- 打印标题、URL、摘要\n\n核心写法：\n\n```python\nfrom ruyipage import FirefoxOptions, FirefoxPage, Keys\n\nopts = FirefoxOptions()\npage = FirefoxPage(opts)\n\npage.get(\"https:\u002F\u002Fcn.bing.com\u002F\")\npage.ele(\"#sb_form_q\").input(\"小肩膀教育\")\npage.actions.press(Keys.ENTER).perform()\n\nfor item in page.eles(\"css:#b_results > li.b_algo\"):\n    title_ele = item.ele(\"css:h2 a\")\n    title = title_ele.text\n    url = title_ele.attr(\"href\")\n```\n\n### 2. Cloudflare \u002F Copilot 示例\n\n文件：`quickstart_cloudfare.py`\n\n它会：\n\n- 打开 Copilot\n- 尝试寻找输入框并发问\n- 自动尝试处理 Cloudflare\n- 最后打印完整 Cookie\n\n这个示例更适合你理解：\n\n- `page.handle_cloudflare_challenge()`\n- `page.get_cookies(all_info=True)`\n- `FirefoxOptions` 如何写进新手脚本\n\n### 3. 指纹浏览器示例\n\n文件：`quickstart_fingerprint_browser.py`\n\n它会：\n\n- 启动 Firefox 指纹浏览器\n- 通过 `--fpfile=...` 加载指纹文件\n- 打开 `browserscan` 检查指纹结果\n- 叠加地理位置、时区、语言、请求头、屏幕尺寸模拟\n\n核心写法：\n\n```python\nfrom ruyipage import FirefoxOptions, FirefoxPage\n\nopts = FirefoxOptions()\nopts.set_browser_path(r\"C:\\Program Files\\Mozilla Firefox\\firefox.exe\")\nopts.set_fpfile(r\"C:\\fingerprints\\profile1.txt\")\n\npage = FirefoxPage(opts)\npage.get(\"https:\u002F\u002Fwww.browserscan.net\u002Fzh\")\n\npage.emulation.set_geolocation(39.9042, 116.4074, accuracy=100)\npage.emulation.set_timezone(\"Asia\u002FTokyo\")\npage.emulation.set_locale([\"ja-JP\", \"ja\"])\npage.network.set_extra_headers({\n    \"Accept-Language\": \"ja-JP,ja;q=0.9\"\n})\npage.emulation.set_screen_size(1366, 768, device_pixel_ratio=2.0)\npage.refresh()\n```\n\n适用场景：\n\n- 需要把 Firefox 指纹浏览器和 `ruyiPage` 配合使用\n- 希望把指纹文件、语言、请求头、屏幕参数一起带上\n- 想直接验证 `browserscan` 等站点上的指纹表现\n\n### 4. HTTP 密码代理示例\n\n如果你使用的是本项目自己的 Firefox 内核，那么内核已经支持从 `fpfile` 自动读取 HTTP 代理用户名密码。\n\n也就是说，业务层只需要：\n\n- `opts.set_proxy(\"http:\u002F\u002Fhost:port\")`\n- `opts.set_fpfile(\"...\")`\n\n当 `fpfile` 中存在以下字段时，内核会自动处理 HTTP 代理认证，不需要再额外调用认证 API：\n\n```text\nhttpauth.username:your-proxy-username\nhttpauth.password:your-proxy-password\n```\n\n完整示例见：`examples\u002F38_proxy_auth_ipinfo.py`\n\n核心写法：\n\n```python\nfrom ruyipage import FirefoxOptions, FirefoxPage\n\nopts = FirefoxOptions()\nopts.set_proxy(\"http:\u002F\u002Fyour-proxy-host:8080\")\nopts.set_fpfile(r\"C:\\path\\to\\your\\profile1.txt\")\n\npage = FirefoxPage(opts)\npage.get(\"http:\u002F\u002Fipinfo.io\u002Fjson\")\n```\n\n适用场景：\n\n- 你在自己的 Firefox 内核里已经实现了 `fpfile` 驱动的 HTTP 代理认证\n- 希望业务层只保留最小代理配置入口\n- 想让代理用户名密码完全留在 `fpfile` 中，而不是写进业务脚本\n\n---\n\n## 最常用 API 文档\n\n下面不是底层 BiDi 命令表，而是 **新手最常直接用到的高层 API**。\n\n阅读建议：\n\n1. 先看 `FirefoxPage`\n   - 这是最核心的页面对象，绝大多数操作都从这里开始。\n2. 再看 `ele()` \u002F `eles()`\n   - 元素定位是最常用的基础能力。\n3. 再看 `actions \u002F downloads \u002F network \u002F events`\n   - 这些是自动化中最常扩展的高级能力。\n\n文档风格说明：\n\n- 这里优先写 **最常用、最实用** 的高层接口。\n- 不会把底层 BiDi 命令原样堆出来让新手自己拼参数。\n- 每个能力尽量说明：\n  - 它是做什么的\n  - 什么时候该用\n  - 最常见的写法是什么\n  - 返回值你能继续怎么用\n\n---\n\n## 1. 页面对象：`FirefoxPage`\n\n### 创建页面\n\n```python\nfrom ruyipage import FirefoxPage, FirefoxOptions\n\npage = FirefoxPage()\n\nopts = FirefoxOptions()\npage = FirefoxPage(opts)\n```\n\n### 常用属性\n\n| API | 说明 | 返回值 |\n| --- | --- | --- |\n| `page.title` | 当前页面标题 | `str` |\n| `page.url` | 当前页面 URL | `str` |\n| `page.html` | 当前页面 HTML | `str` |\n| `page.tab_id` | 当前 tab 的 browsingContext ID | `str` |\n| `page.cookies` | 当前页面可见 Cookie 列表 | `list[CookieInfo]` |\n\n### 常用导航\n\n```python\npage.get(\"https:\u002F\u002Fwww.example.com\")\npage.refresh()\npage.back()\npage.forward()\npage.quit()\n```\n\n#### `page.get(url, wait='complete')`\n\n打开一个页面。\n\n```python\npage.get(\"https:\u002F\u002Fwww.example.com\")\npage.get(\"https:\u002F\u002Fwww.example.com\", wait=\"interactive\")\n```\n\n参数说明：\n\n- `url`\n  - 要访问的地址\n  - 常见值：`https:\u002F\u002F...`、`file:\u002F\u002F\u002F...`、`data:text\u002Fhtml,...`\n- `wait`\n  - 页面等待策略\n  - 常见值：\n    - `complete`：等页面完全加载\n    - `interactive`：等 DOMContentLoaded\n    - `none`：发出导航后立即返回\n\n适用场景：\n\n- 日常页面打开：用默认 `complete`\n- 页面很慢但你只想先拿 DOM：用 `interactive`\n- 你后面会自己监听事件或手动等：用 `none`\n\n#### `page.back()` \u002F `page.forward()` \u002F `page.refresh()`\n\n这些分别用于：\n\n- 后退\n- 前进\n- 刷新\n\n```python\npage.back()\npage.forward()\npage.refresh()\n```\n\n如果你需要验证导航事件，建议和 `page.navigation` 配合使用。\n\n---\n\n## 2. 元素查找：`ele()` \u002F `eles()`\n\n### `page.ele(locator)`\n\n查找一个元素。\n\n最常用写法：\n\n```python\npage.ele(\"#kw\")\npage.ele(\"css:.item\")\npage.ele(\"css:div.card > a\")\npage.ele(\"xpath:\u002F\u002Fbutton[text()='登录']\")\npage.ele(\"tag:input\")\npage.ele(\"text:登录\")\n```\n\n新手建议优先顺序：\n\n1. `#id`\n2. `css:...`\n3. `xpath:...`\n\n### `page.eles(locator)`\n\n查找所有匹配元素。\n\n```python\nitems = page.eles(\"css:.card\")\nrows = page.eles(\"css:table tbody tr\")\nlinks = page.eles(\"tag:a\")\n```\n\n### 在元素内部继续查找\n\n```python\ncard = page.ele(\"css:.card\")\ntitle = card.ele(\"css:h2 a\")\ndesc = card.ele(\"css:.desc\")\n```\n\n### 常用元素 API\n\n| API | 说明 | 返回值 |\n| --- | --- | --- |\n| `ele.text` | 元素文本 | `str` |\n| `ele.html` | outerHTML | `str` |\n| `ele.value` | 表单值 | `str | None` |\n| `ele.attr(\"href\")` | 属性值 | `str` |\n| `ele.click_self()` | 直接点击元素 | `self` |\n| `ele.input(\"abc\")` | 输入文本 | `self` |\n| `ele.clear()` | 清空内容 | `self` |\n| `ele.hover()` | 鼠标悬停 | `self` |\n| `ele.drag_to(target)` | 拖到目标 | `self` |\n\n#### `ele.text`\n\n读取元素文本。\n\n```python\ntitle = page.ele(\"css:h1\").text\n```\n\n适合读取：\n\n- 标题\n- 按钮文案\n- 搜索结果摘要\n\n#### `ele.attr(name)`\n\n读取元素属性。\n\n```python\nurl = page.ele(\"css:a\").attr(\"href\")\nsrc = page.ele(\"css:img\").attr(\"src\")\n```\n\n常见属性：\n\n- `href`\n- `src`\n- `value`\n- `placeholder`\n- `class`\n- `id`\n\n#### `ele.click_self()`\n\n直接点击元素。\n\n```python\npage.ele(\"text:登录\").click_self()\n```\n\n这是最推荐新手使用的点击方法。\n\n#### `ele.input(text, clear=True)`\n\n给输入框输入内容。\n\n```python\npage.ele(\"#kw\").input(\"ruyiPage\")\npage.ele(\"#kw\").input(\"ruyiPage\", clear=True)\n```\n\n适用场景：\n\n- 文本输入\n- 搜索框输入\n- 文件输入框上传文件\n\n如果元素本身是 `\u003Cinput type=\"file\">`，传文件路径即可：\n\n```python\npage.ele(\"#upload\").input(r\"D:\\test.txt\")\npage.ele(\"#upload\").input([r\"D:\\1.txt\", r\"D:\\2.txt\"])\n```\n\n---\n\n## 3. 动作链：`page.actions`\n\n用于原生 BiDi 输入动作。\n\n```python\npage.actions.press(Keys.ENTER).perform()\npage.actions.move_to(page.ele(\"#btn\")).click().perform()\npage.actions.drag(page.ele(\"#a\"), page.ele(\"#b\")).perform()\npage.actions.release()\n```\n\n常见用途：\n\n- 键盘输入\n- 鼠标点击\n- 拖拽\n- 滚轮滚动\n- 拟人化移动和点击\n\n### 常见写法\n\n#### 回车\n\n```python\nfrom ruyipage import Keys\n\npage.actions.press(Keys.ENTER).perform()\n```\n\n#### 点击某个元素\n\n```python\npage.actions.move_to(page.ele(\"#btn\")).click().perform()\n```\n\n#### 拖拽\n\n```python\npage.actions.drag(page.ele(\"#source\"), page.ele(\"#target\")).perform()\npage.actions.release()\n```\n\n### 为什么推荐 `page.actions`\n\n因为这条链更接近原生 BiDi 输入模型，很多动作事件能保持更真实的浏览器输入行为。\n\n---\n\n## 4. Cookies\n\n### 获取 Cookie\n\n```python\ncookies = page.get_cookies()\nfor cookie in cookies:\n    print(cookie.name, cookie.value)\n```\n\n返回对象通常是 `CookieInfo`，常用字段：\n\n- `cookie.name`\n- `cookie.value`\n- `cookie.domain`\n- `cookie.path`\n- `cookie.http_only`\n- `cookie.secure`\n- `cookie.same_site`\n- `cookie.expiry`\n\n### 按条件过滤 Cookie\n\n```python\ncookies = page.get_cookies_filtered(name=\"session_id\", all_info=True)\n```\n\n### 设置 Cookie\n\n`page.set_cookies()` 支持直接回放 `browser.cookies(all_info=True)` 返回的完整 Cookie，\n并会在当前浏览上下文与 Cookie 域不匹配时自动避免错误的 BiDi context partition，\n以保证跨站登录 Cookie 能正确落地。\n\n```python\npage.set_cookies({\n    \"name\": \"token\",\n    \"value\": \"abc\",\n    \"domain\": \"127.0.0.1\",\n    \"path\": \"\u002F\",\n})\n```\n\n也可以一次传多个：\n\n```python\npage.set_cookies([\n    {\"name\": \"a\", \"value\": \"1\", \"domain\": \"127.0.0.1\", \"path\": \"\u002F\"},\n    {\"name\": \"b\", \"value\": \"2\", \"domain\": \"127.0.0.1\", \"path\": \"\u002F\"},\n])\n```\n\n### 删除 Cookie\n\n```python\npage.delete_cookies(name=\"token\")\npage.delete_cookies()\n```\n\n---\n\n## 5. 下载\n\n高层入口：`page.downloads`\n\n```python\npage.downloads.set_behavior(\"allow\", path=r\"D:\\downloads\")\npage.downloads.start()\n\npage.ele(\"#download\").click_self()\n\nevent = page.downloads.wait(\"browsingContext.downloadEnd\", timeout=5)\nprint(event.status)\n```\n\n常用方法：\n\n- `set_behavior()`\n- `set_path()`\n- `start()`\n- `stop()`\n- `wait()`\n- `wait_chain()`\n- `wait_file()`\n\n### 典型下载流程\n\n```python\npage.downloads.set_behavior(\"allow\", path=r\"D:\\downloads\")\npage.downloads.start()\n\npage.ele(\"#download\").click_self()\n\nbegin = page.downloads.wait(\"browsingContext.downloadWillBegin\", timeout=5)\nend = page.downloads.wait(\"browsingContext.downloadEnd\", timeout=5)\n\nprint(begin.suggested_filename)\nprint(end.status)\n```\n\n---\n\n## 6. 导航事件\n\n高层入口：`page.navigation`\n\n```python\npage.navigation.start()\npage.get(\"https:\u002F\u002Fwww.example.com\")\n\nevent = page.navigation.wait(\"browsingContext.load\", timeout=5)\nprint(event.url)\n\npage.navigation.stop()\n```\n\n适合验证：\n\n- `navigationStarted`\n- `domContentLoaded`\n- `load`\n- `historyUpdated`\n- `navigationCommitted`\n\n---\n\n## 7. 通用事件监听\n\n高层入口：`page.events`\n\n```python\npage.events.start([\"network.beforeRequestSent\"], contexts=[page.tab_id])\nevent = page.events.wait(\"network.beforeRequestSent\", timeout=5)\npage.events.stop()\n```\n\n适合统一承接：\n\n- `browsingContext.*`\n- `network.*`\n- `script.*`\n- `input.*`\n- `log.*`\n\n返回对象：`BidiEvent`\n\n常用字段：\n\n- `method`\n- `context`\n- `url`\n- `request`\n- `response`\n- `error_text`\n- `channel`\n- `data`\n- `multiple`\n- `message`\n\n### 什么时候用 `page.events`\n\n当你想直接监听协议事件，而不是只关心页面最终状态时，用它最合适。\n\n例如：\n\n- 监听 `network.beforeRequestSent`\n- 监听 `browsingContext.contextCreated`\n- 监听 `script.message`\n- 监听 `input.fileDialogOpened`\n\n---\n\n## 8. 网络能力\n\n高层入口：`page.intercept`（拦截）、`page.listen`（监听）、`page.network`（配置）\n\n### 请求拦截\n\n拦截请求阶段（`beforeRequestSent`），可修改、Mock 或阻止请求：\n\n```python\n# 回调模式：拦截并 Mock 响应\ndef handler(req):\n    if '\u002Fapi\u002Fdata' in req.url:\n        req.mock(\n            '{\"status\":\"ok\",\"data\":\"mocked\"}',\n            headers={\"content-type\": \"application\u002Fjson\",\n                     \"access-control-allow-origin\": \"*\"},\n        )\n    else:\n        req.continue_request()\n\npage.intercept.start_requests(handler)\npage.get(\"https:\u002F\u002Fexample.com\")\npage.intercept.stop()\n```\n\n```python\n# 修改请求头（headers 支持 dict 简洁格式）\ndef handler(req):\n    req.continue_request(headers={\n        \"X-Token\": \"abc123\",\n        \"User-Agent\": \"RuyiPage\u002F1.0\",\n    })\n\npage.intercept.start_requests(handler)\n```\n\n```python\n# 阻止请求\ndef handler(req):\n    if req.url.endswith(('.png', '.jpg', '.gif')):\n        req.fail()\n    else:\n        req.continue_request()\n\npage.intercept.start_requests(handler)\n```\n\n```python\n# 队列模式：手动处理\npage.intercept.start_requests()\n# ... 触发网络请求 ...\nreq = page.intercept.wait(timeout=5)\nprint(req.method, req.url, req.body)\nreq.continue_request()\npage.intercept.stop()\n```\n\n### 响应拦截\n\n拦截响应阶段（`responseStarted`），可读取、修改响应信息：\n\n```python\n# 读取原始响应状态码、头和响应体\ndef handler(req):\n    print(f\"状态码: {req.response_status}\")\n    print(f\"Content-Type: {req.response_headers.get('content-type')}\")\n    req.continue_response()\n    # start_responses 默认 collect_response=True，\n    # continue_response 后可直接读取响应体\n    print(f\"响应体: {req.response_body}\")\n\npage.intercept.start_responses(handler)\n```\n\n```python\n# 修改响应状态码\ndef handler(req):\n    if '\u002Fapi' in req.url:\n        req.continue_response(status_code=200, reason_phrase=\"OK\")\n    else:\n        req.continue_response()\n\npage.intercept.start_responses(handler)\n```\n\n### 一步读取响应体\n\n启用 `collect_response=True` 后，可通过 `req.response_body` 一步读取响应体，无需手动编排 DataCollector：\n\n```python\npage.intercept.start_requests(collect_response=True)\n# ... 触发网络请求 ...\nreq = page.intercept.wait(timeout=5)\nreq.continue_request()\nbody = req.response_body  # 自动等待响应完成 + 解码\nprint(body)\npage.intercept.stop()     # 自动清理内部 collector\n```\n\n### 设置额外请求头\n\n```python\npage.network.set_extra_headers({\"X-Test\": \"yes\"})\n```\n\n这通常用于：\n\n- 给接口加测试请求头\n- 做环境标记\n- 配合拦截验证请求头是否真的发出\n\n### 设置缓存行为\n\n```python\npage.network.set_cache_behavior(\"bypass\")\n```\n\n其中：\n\n- `default`: 浏览器默认缓存策略，命中缓存时可能不再发真实请求\n- `bypass`: 尽量绕过缓存，强制重新请求资源\n\n### Data Collector\n\n```python\ncollector = page.network.add_data_collector(\n    [\"responseCompleted\"],\n    data_types=[\"response\"],\n)\n\ndata = collector.get(request_id, data_type=\"response\")\ncollector.disown(request_id, data_type=\"response\")\ncollector.remove()\n```\n\n其中：\n\n- `events`\n  - `beforeRequestSent`：在请求发出阶段采集\n  - `responseCompleted`：在响应完成阶段采集\n- `data_types`\n  - `request`：收集请求体\n  - `response`：收集响应体\n\n---\n\n## 9. 浏览上下文\n\n高层入口：`page.contexts`\n\n```python\ntree = page.contexts.get_tree()\nprint(len(tree.contexts))\n\ntab_id = page.contexts.create_tab()\npage.contexts.close(tab_id)\n\npage.contexts.reload()\npage.contexts.set_viewport(800, 600)\n```\n\n常用方法：\n\n- `get_tree()`\n- `create_tab()`\n- `create_window()`\n- `close()`\n- `reload()`\n- `set_viewport()`\n- `set_bypass_csp()`\n\n### `tree = page.contexts.get_tree()`\n\n返回的不是裸 dict，而是高层结果对象。\n\n```python\ntree = page.contexts.get_tree()\nprint(len(tree.contexts))\n\nfirst = tree.contexts[0]\nprint(first.context)\nprint(first.url)\n```\n\n---\n\n## 10. 浏览器级能力\n\n高层入口：`page.browser_tools`\n\n```python\nuser_context = page.browser_tools.create_user_context()\ncontexts = page.browser_tools.get_user_contexts()\npage.browser_tools.remove_user_context(user_context)\n\nwindows = page.browser_tools.get_client_windows()\npage.browser_tools.set_window_state(windows[0][\"clientWindow\"], state=\"maximized\")\n```\n\n适合做：\n\n- user context 管理\n- client window 管理\n\n### 典型用法\n\n```python\nctx = page.browser_tools.create_user_context()\ntab_id = page.browser_tools.create_tab(user_context=ctx)\npage.contexts.close(tab_id)\npage.browser_tools.remove_user_context(ctx)\n```\n\n---\n\n## 11. Script 能力\n\n### 获取 realms\n\n```python\nrealms = page.get_realms()\nfor realm in realms:\n    print(realm.type, realm.context)\n```\n\n### 执行脚本并拿 handle\n\n```python\nresult = page.eval_handle(\"({a: 1, b: 2})\")\nprint(result.success)\nprint(result.result.handle)\n\npage.disown_handles([result.result.handle])\n```\n\n这个流程适合：\n\n- 需要拿远程 JS 对象句柄\n- 用完后再手动释放 handle\n\n### preload script\n\n```python\npreload = page.add_preload_script(\"\"\"\n() => {\n    window.__ready = 'ok';\n}\n\"\"\")\n\npage.get(\"https:\u002F\u002Fwww.example.com\")\nprint(page.run_js(\"return window.__ready\"))\n\npage.remove_preload_script(preload)\n```\n\n适用场景：\n\n- 在页面脚本执行前先注入一段初始化逻辑\n- 给页面预先挂钩子、打标记、注入辅助函数\n\n---\n\n## 12. 弹窗\n\n高层入口：\n\n- `page.wait_prompt()`\n- `page.accept_prompt()`\n- `page.dismiss_prompt()`\n- `page.input_prompt(text)`\n- `page.set_prompt_handler(...)`\n- `page.clear_prompt_handler()`\n\n### 典型写法\n\n#### 等待后手动处理\n\n```python\npage.run_js(\"alert('hello')\", as_expr=False)\nprompt = page.wait_prompt(timeout=3)\npage.accept_prompt()\n```\n\n#### 自动处理 prompt\n\n```python\npage.set_prompt_handler(prompt=\"ignore\", prompt_text=\"张三\")\npage.run_js(\"prompt('请输入姓名')\", as_expr=False)\npage.clear_prompt_handler()\n```\n\n---\n\n## 13. Emulation\n\n高层入口：`page.emulation`\n\n```python\npage.emulation.set_network_offline(True)\npage.emulation.set_javascript_enabled(False)\npage.emulation.set_scrollbar_type(\"overlay\")\npage.emulation.apply_mobile_preset(\n    width=390,\n    height=844,\n    device_pixel_ratio=3,\n    user_agent=\"...\",\n)\n```\n\n注意：\n\n- 某些 emulation 命令在当前 Firefox 版本中可能未实现\n- 示例里会区分“成功”和“不支持”\n\n### 典型用法\n\n```python\npage.emulation.apply_mobile_preset(\n    width=390,\n    height=844,\n    device_pixel_ratio=3,\n    user_agent=\"Mozilla\u002F5.0 ...\",\n)\n```\n\n---\n\n## 14. WebExtension\n\n高层入口：`page.extensions`\n\n```python\next_id = page.extensions.install_dir(r\"D:\\my_extension\")\npage.extensions.uninstall(ext_id)\n```\n\n适用场景：\n\n- 验证 content script 是否生效\n- 测试目录扩展和 xpi 安装流程\n\n---\n\n## 15. 代表性示例\n\n仓库里已经包含大量示例，建议按编号学习。\n\n推荐顺序：\n\n### 入门\n\n- `01_basic_navigation.py`\n- `02_element_finding.py`\n- `03_element_interaction.py`\n- `05_actions_chain.py`\n- `06_screenshot.py`\n\n### 页面与脚本\n\n- `07_javascript.py`\n- `08_cookies.py`\n- `09_tabs.py`\n- `13_iframe.py`\n- `14_shadow_dom.py`\n\n### 高级能力\n\n- `17_user_prompts.py`\n- `18_advanced_network.py`\n- `19_pdf_printing.py`\n- `20_advanced_input.py`\n- `21_emulation.py`\n\n### 严格结果版示例\n\n- `23_download.py`\n- `24_navigation_events.py`\n- `25_browser_user_context.py`\n- `37_three_isolated_user_context_tabs.py` 单浏览器多 tab 使用不同 user context，实现 Cookie 隔离\n- `26_browsing_context_advanced.py`\n- `27_emulation_advanced.py`\n- `28_network_data_collector.py`\n- `29_script_input_advanced.py`\n- `30_browsing_context_events.py`\n- `31_network_events.py`\n- `32_script_events.py`\n- `33_log_input_events.py`\n- `34_remaining_commands.py`\n- `35_native_bidi_drag.py`\n- `36_native_bidi_select.py`\n- `39_attach_exist_browser.py` 自动探测可接管实例，再接管已打开的 Firefox\u002F指纹浏览器\n- `42_xpath_picker_complex_showcase.py` 启动 XPath picker，并打开包含复杂节点、shadow root、嵌套 iframe 的综合展示页\n- `42_3_debug_px_context_probe.py` 直接打开 `debug_px.html`，打印 PX challenge iframe 的 browsing context 树，并尝试 attach 到 child context 做最小 DOM \u002F canvas 诊断\n- `46_human_behavior_showcase.py` 演示 bezier \u002F windmouse 两套拟人轨迹算法，并开启鼠标行为可视化\n- `48_smart_fingerprint.py` 演示 `apply_smart_fingerprint()` 一站式智能指纹（geo 探测 + 内核 fpfile + BiDi 仿真）\n\n---\n\n## 智能指纹一站式 API\n\n`ruyiPage` 在 [`firefox-fingerprintBrowser`](https:\u002F\u002Fgithub.com\u002FLoseNine\u002Ffirefox-fingerprintBrowser)\n内核之上提供了开箱即用的 **智能指纹** 能力：一行代码完成「探测出口 IP →\n匹配语言\u002F时区\u002F语音 → 抽取 22 套真机硬件特征 → 写出 `fpfile.txt` → 配置\n`FirefoxOptions`」全流程。\n\n### 一行链式调用\n\n```python\nfrom ruyipage import FirefoxOptions, FirefoxPage, CountryMismatchError\n\nopts = FirefoxOptions().set_port(9222)\nopts.set_browser_path(r\"C:\u002FProgram Files\u002FMozilla Firefox\u002Ffirefox.exe\")\n\nctx = opts.smart_fingerprint(\n    proxy_host=\"proxy.example.com\", proxy_port=8080,\n    proxy_user=\"u\", proxy_pwd=\"p\",\n    require_country=\"US\",      # 出口 IP 国家不一致 → CountryMismatchError\n    logger=print,\n)\n\npage = FirefoxPage(opts)\nctx.apply_emulation(page)       # 内核 fpfile 之上再叠加 BiDi 仿真\npage.get(\"https:\u002F\u002Fbrowserleaks.com\u002Fwebgl\")\n```\n\n或者直接调用顶层函数 `apply_smart_fingerprint(opts, ...)`，效果一致。\n\n### 流水线说明\n\n1. `build_proxies_dict(...)` — 组装 `requests` 风格的 proxies。\n2. `fetch_geo_info(...)` — 5 数据源回退（geojs \u002F ipapi \u002F ipwho \u002F ip-api \u002F\n   ipinfo）；`require_country` 不匹配立即抛 `CountryMismatchError`。\n3. `fetch_public_ipv6(...)` — best-effort，失败则 `*_webrtc_ipv6` 整行省略。\n4. 自动生成 \u002F 复用 `userdir`，写入符合内核字段顺序的 `fpfile.txt`。\n5. 在 `FirefoxOptions` 上自动 `set_proxy \u002F set_user_dir \u002F set_fpfile \u002F\n   set_window_size`，每一步都可单独关闭。\n6. 返回 `FingerprintContext`：\n   - `ctx.summary()` — 单行日志；\n   - `ctx.apply_emulation(page)` — geolocation \u002F locale \u002F timezone \u002F\n     Accept-Language 四重 BiDi 仿真覆盖（每一步独立 `try\u002Fexcept`，\n     旧版 ruyipage 优雅降级）；\n   - `ctx.to_dict()` — 持久化指纹身份（账号库等）。\n\n### 内置数据资产\n\n- 22 套 Windows 真机硬件特征（NVIDIA RTX 系 + AMD RX 系 + Intel UHD\u002FArc）。\n- 30+ 国语言 \u002F Accept-Language \u002F 微软语音映射，含 `_default` 兜底。\n- Firefox 主版本锁定 151，仅在 minor 上 `±2` 抖动，避免 UA 主号穿帮。\n\n### 异常体系\n\n```\nFingerprintError\n├── FingerprintConfigError      # 内置 JSON 损坏（部署期错误）\n└── GeoError                    # 5 个 geo 数据源全部失败\n    └── CountryMismatchError    # 出口 IP 国家与 require_country 不一致\n        # 属性：actual \u002F required\n```\n\n完整字段定义、低层接口（`pick_fingerprint` \u002F `write_fpfile` \u002F\n`list_hardware_profiles` \u002F `get_country_profile`）见\n[`ruyipage\u002F_fingerprint\u002FREADME.md`](ruyipage\u002F_fingerprint\u002FREADME.md)\n与示例 `examples\u002F48_smart_fingerprint.py`。\n\n---\n\n## 协议来源\n\n`ruyiPage` 的底层核心能力对照并基于：\n\n- WebDriver BiDi: https:\u002F\u002Fw3c.github.io\u002Fwebdriver-bidi\u002F\n\n这也是本项目很多高层 API 的设计来源，例如：\n\n- `browsingContext.*`\n- `network.*`\n- `script.*`\n- `input.*`\n- `browser.*`\n- `emulation.*`\n\n---\n\n## Star History\n\n\u003Ca href=\"https:\u002F\u002Fwww.star-history.com\u002F?repos=LoseNine%2Fruyipage&type=timeline&legend=top-left\">\n \u003Cpicture>\n   \u003Csource media=\"(prefers-color-scheme: dark)\" srcset=\"https:\u002F\u002Fapi.star-history.com\u002Fchart?repos=LoseNine\u002Fruyipage&type=timeline&theme=dark&legend=top-left\" \u002F>\n   \u003Csource media=\"(prefers-color-scheme: light)\" srcset=\"https:\u002F\u002Fapi.star-history.com\u002Fchart?repos=LoseNine\u002Fruyipage&type=timeline&legend=top-left\" \u002F>\n   \u003Cimg alt=\"Star History Chart\" src=\"https:\u002F\u002Fapi.star-history.com\u002Fchart?repos=LoseNine\u002Fruyipage&type=timeline&legend=top-left\" \u002F>\n \u003C\u002Fpicture>\n\u003C\u002Fa>\n\n---\n\n## 使用声明与免责声明\n\n本项目仅用于：\n\n- 探索下一代自动化框架\n- 学习 Firefox 自动化能力\n- 学习 WebDriver BiDi 协议\n- 学习浏览器自动化高层 API 设计\n- 合法、合规、非盈利的个人研究与技术交流\n\n### 授权范围\n\n允许任何人以个人身份使用或分发本项目源代码，但仅限于：\n\n- 学习目的\n- 技术研究目的\n- 合法、合规、非盈利目的\n\n个人或组织如未获得版权持有人授权，不得将本项目以源代码或二进制形式用于商业行为。\n\n### 使用条款\n\n使用本项目需满足以下条款，如使用过程中出现违反任意一项条款的情形，授权自动失效。\n\n- 禁止将 `ruyiPage` 应用于任何可能违反当地法律规定和道德约束的项目中\n- 禁止将 `ruyiPage` 用于任何可能有损他人利益的项目中\n- 禁止将 `ruyiPage` 用于攻击、骚扰、批量滥用、恶意注册、撞库、刷量等行为\n- 禁止将 `ruyiPage` 用于规避平台安全机制后实施违法行为\n- 使用者应遵守目标网站或系统的 Robots、服务条款及当地法律法规\n- 禁止将 `ruyiPage` 用于采集法律、条款或 Robots 协议明确不允许的数据\n\n### 风险与责任\n\n使用 `ruyiPage` 发生的一切行为，均由使用人自行负责。\n\n因使用 `ruyiPage` 进行任何行为所产生的一切纠纷及后果，均与版权持有人无关。\n\n版权持有人不承担任何因使用 `ruyiPage` 带来的风险、损失、封号、限制、数据问题、法律后果或间接损失。\n\n版权持有人也不对 `ruyiPage` 可能存在的缺陷、兼容性问题、误操作风险或目标网站策略变化导致的任何损失承担责任。\n\n### 特别说明\n\n本项目强调：\n\n- Firefox 自动化\n- BiDi 协议能力\n- `isTrusted` 行为\n- 拟人化行为能力\n- 高风控场景适配\n\n但这些能力仅限于**合法、合规、正当**的技术研究和自动化应用场景。\n","ruyiPage 是一个基于 Python 的下一代 Web 自动化检测框架，专为 AI 分析和数据采集场景设计。它利用 WebDriver BiDi 协议构建，具备拦截任意请求响应包的能力，并自带了适用于 Windows 和 Linux 的过检测火狐浏览器内核。该框架支持大量原生动作以规避自动化检测，同时允许通过附加 `ruyi: true` 来构造更真实的 JS 事件（如 `Event`, `InputEvent`, `MouseEvent`, `KeyboardEvent` 等）。此外，ruyiPage 能够直接自动化接管 ADS 等指纹浏览器，非常适合在高风控环境中使用。",2,"2026-06-11 02:41:07","CREATED_QUERY"]