标签 浏览器自动化 下的文章

第一步:开启 Chrome 远程调试权限

这是所有操作的前提,必须手动开启。

  1. 打开 Chrome 浏览器,在地址栏输入:chrome://inspect/#remote-debugging
  2. 勾选 “Allow remote debugging for this browser instance”
  1. 确认下方出现:Server running at: 127.0.0.1:9222(这表示“门”已打开)。


第二步:配置 MCP 服务

AionUI 图形界面配置

  1. 进入 AionUI 的 Settings(设置)Tools Settings(工具设置)

  2. MCP Management 区域点击 Add MCP Service

  3. 选择 Add via JSON,粘贴以下配置:

{ "mcpServers": { "chrome-devtools": { "command": "npx", "args": [ "-y", "chrome-devtools-mcp@latest", "--autoConnect", "--channel=stable" ] } } } 


第三步:激活连接(关键动作)

配置完成后,Gemini 并不会立刻接管浏览器,需要一次“握手”:

  1. 在 Gemini CLI 或 AionUI 中输入第一个指令,例如:截取当前网页的屏幕

  2. 切换回 Chrome 浏览器:你会看到页面顶部弹出一个系统确认框,询问 “Allow incoming debugging connection?”

  3. 点击 Allow(允许)

  4. 浏览器顶部出现横幅(显示“正在受自动测试软件控制”),说明连接成功。


第四步:实战常用指令

连接成功后,你可以直接用自然语言指挥 Gemini 处理你当前看到的网页:

  • 视觉分析
    • “帮我截个图,分析一下现在的 UI 布局有没有错位?”
  • 性能诊断
    • “分析当前页面的性能,告诉我 LCP(最大内容绘制)是多少,怎么优化?”
  • 代码调试
    • “检查当前页面的 Console 控制台,有没有报错?如果有,帮我解释原因。”
  • 网页抓取与操作
    • “提取当前网页中所有商品的价格并列成表格。”
    • “帮我点击页面上的‘提交’按钮。”(AI 会通过 DOM 树自动定位元素)


推荐模型:Gemini 3 Flash、Gemini 3 Pro、GLM 4.7(英伟达无限免费 API)
使用案例:



感谢 的AionUI
AionUi V1.7.4 更新:兼容了Newapi(Cowork开源版可以用公益站/中转站了)

也欢迎使用我的 AMC,这个教程的方法也都是我用 AMC 的深度搜索功能学会的
AMC更新:支持Markdown 转 PDF、划词 TTS


推荐使用 @bbbugg 佬的AIStudioToAPI 构建自己的API 池
https://linux.do/t/topic/1371269

【二改】二改build反代(AIStudio to API),优化云端部署


📌 转载信息
原作者: yeahhe
转载时间: 2026/1/25 08:06:46

https://linux.do/t/topic/1465569?u=yeahhe




📌 转载信息
原作者: yeahhe
转载时间: 2026/1/25 08:05:53

作为一名求职中的中年软件工程师,由于地域和年龄限制,我的选择空间其实就那么几家。我经常需要反复查看自己感兴趣公司的招聘页面。这一过程既耗时又枯燥,尤其是在需要同时跟踪多家公司职位的情况下。虽然许多招聘网站都提供基于邮件的职位提醒,但这些提醒通常要么依赖于对已提交简历进行不透明的 AI 匹配,要么只是简单的关键词匹配。在这两种情况下,我对实际的匹配条件几乎没有控制权。

为了解决这个问题,我决定利用一个 AI Agent 来自动 Watching 招聘页面,并在发布符合我自己定义条件的新职位时通知我。在本文中,我将介绍一个用于验证这一想法的概念验证(PoC)。

在这个 PoC 中,我展示了如何使用 AI Agent 来 Watching 一家公司招聘页面上的软件开发岗位。该 Agent 能够自动浏览招聘网站、搜索相关职位、提取结构化信息,并将结果存储到 SQLite 数据库中,以便后续查询和跟踪。

PoC

职位数据来源

在本次实验中,我选择了一家其招聘页面基于 Eightfold AI 平台构建的公司。如果你的目标公司同样使用 Eightfold AI,那么只需做很少的修改即可复用该 PoC。

Eightfold AI 是一个人才智能平台,利用人工智能支持招聘、员工留任以及劳动力发展。它基于技能和经验将候选人与开放职位进行匹配,目前已被包括 Vodafone、Morgan Stanley 和 Chevron 在内的 100 多家公司使用。该平台覆盖 155 多个国家。

尽管 Eightfold AI 平台本身已经提供了基于 AI 的职位提醒订阅功能,但我仍希望对匹配逻辑和采集到的数据拥有更细粒度的控制,因此才希望有一套自定义解决方案。

Agent 设计

我在 VS Code Copilot Chat 环境中实现了该 PoC,并使用了以下工具和提示词。

MCP 工具

  • 浏览器工具browsermcp:用于导航和操作招聘网站页面。
  • SQLite 数据库工具genai-toolbox:用于持久化存储提取到的职位数据。

./vscode/mcp.json

{
  "servers": {
    "browsermcp": {
      "type": "stdio",
      "command": "npx",
      "args": [
        "@browsermcp/mcp@latest"
      ]
    },
    "sqlite": {
      "command": "~/genai-toolbox/toolbox",
      "args": [
        "--prebuilt",
        "sqlite",
        "--stdio"
      ],
      "env": {
        "SQLITE_DATABASE": "~/jobs/jobs.db"
      }
    }
  }
}

数据库 Schema

CREATE TABLE IF NOT EXISTS xyz_company_jobs (
    job_id TEXT PRIMARY KEY, -- 职位的唯一标识
    req_id TEXT,             -- 招聘需求 ID
    job_title TEXT,          -- 职位名称
    location TEXT,
    date_posted TEXT,        -- 格式:'YYYY-MM-DD'
    business_department TEXT,
    job_description_url TEXT,
    job_description TEXT     -- 职位描述的主要内容
);

Agent 提示词

你是一个可以访问浏览器工具和 SQLite 数据库工具的 AI Agent。你的任务是从 XYZ_Company 的招聘网站中收集与软件开发相关的职位信息,并将提取到的数据存储到 SQLite 数据库中。当前浏览器中打开的页面是 XYZ_Company 的职位搜索门户。

数据库:
```sql
CREATE TABLE IF NOT EXISTS xyz_company_jobs (
    job_id TEXT PRIMARY KEY, -- 职位的唯一标识
    req_id TEXT,             -- 招聘需求 ID
    job_title TEXT,          -- 职位名称
    location TEXT,
    date_posted TEXT,        -- 格式:'YYYY-MM-DD'
    business_department TEXT,
    job_description_url TEXT,
    job_description TEXT     -- 职位描述的主要内容
)
```

规则:
- 在调用一次 `click()` 操作后,必须等待 10 秒,确保页面完全加载,然后再调用 `snapshot` 捕获当前页面状态。
- 在向 `xyz_company_jobs` 表插入新记录之前,需要检查 `job_id` 是否已经存在,以避免重复数据。
- 在生成 INSERT SQL 语句时,确保对值中的单引号进行正确转义。
- 不要收集或点击位于 `document > main > group Similar Position` 区域下的职位。

用户已经准备的环境:
- 浏览器中打开的页面是 XYZ_Company 的职位搜索门户。

你的安装过程:
1. 如果 `xyz_company_jobs` 表不存在,则创建该表。在执行 SQL 时,需保留 SQL 代码块中的注释行。

执行过程:
1. 每一个文本匹配 "$Job_Title$ 于 $time_since_publication$ 前发布" 模式的按钮都代表一个职位。通过 `今天 - time_since_publication` 计算 `Date Posted`。
2. 点击职位按钮以打开职位详情页面,该页面的 URL 即为 `Job Description URL`。
3. 从职位详情页面中提取:职位名称、工作地点、Job ID、业务部门(可选)、Req ID 以及职位描述的主要内容。
4. 仅收集与软件开发相关的职位。
5. 将每个收集到的职位插入 `xyz_company_jobs` 表,并基于 `job_id` 确保不产生重复记录。
6. 如有需要,点击 `更多职位` 按钮以加载更多职位。
7. 至少收集并存储 10 个职位。

运行

  1. 打开一个 Chrome 浏览器标签页,访问 XYZ 公司的招聘门户网站。在该标签页上激活 Browser MCP Chrome 扩展程序。
  2. 在 VS Code 中启动一个新的 Copilot Chat 会话,并使用上述 MCP 配置和提示。

总结

通过上述配置,我成功实现了一个 AI Agent,它能够自动 Watching 目标公司的招聘页面,发现新的软件开发职位。该 Agent 可以自动浏览招聘网站、识别相关职位、提取结构化数据,并将其存储到 SQLite 数据库中,从而方便后续访问和长期跟踪。

后续工作

该 PoC 还可以在多个方面进行扩展:

  • 引入更复杂的匹配逻辑,例如简历解析和基于语义的技能匹配。
  • 将收集到的职位信息导出为 RSS 或邮件摘要,从而构建一个完全自托管的职位提醒系统。
  • 添加通知机制,在发现新的匹配职位时立即提醒我。

作为一名求职中的中年软件工程师,由于地域和年龄限制,我的选择空间其实就那么几家。我经常需要反复查看自己感兴趣公司的招聘页面。这一过程既耗时又枯燥,尤其是在需要同时跟踪多家公司职位的情况下。虽然许多招聘网站都提供基于邮件的职位提醒,但这些提醒通常要么依赖于对已提交简历进行不透明的 AI 匹配,要么只是简单的关键词匹配。在这两种情况下,我对实际的匹配条件几乎没有控制权。

为了解决这个问题,我决定利用一个 AI Agent 来自动 Watching 招聘页面,并在发布符合我自己定义条件的新职位时通知我。在本文中,我将介绍一个用于验证这一想法的概念验证(PoC)。

在这个 PoC 中,我展示了如何使用 AI Agent 来 Watching 一家公司招聘页面上的软件开发岗位。该 Agent 能够自动浏览招聘网站、搜索相关职位、提取结构化信息,并将结果存储到 SQLite 数据库中,以便后续查询和跟踪。

PoC

职位数据来源

在本次实验中,我选择了一家其招聘页面基于 Eightfold AI 平台构建的公司。如果你的目标公司同样使用 Eightfold AI,那么只需做很少的修改即可复用该 PoC。

Eightfold AI 是一个人才智能平台,利用人工智能支持招聘、员工留任以及劳动力发展。它基于技能和经验将候选人与开放职位进行匹配,目前已被包括 Vodafone、Morgan Stanley 和 Chevron 在内的 100 多家公司使用。该平台覆盖 155 多个国家。

尽管 Eightfold AI 平台本身已经提供了基于 AI 的职位提醒订阅功能,但我仍希望对匹配逻辑和采集到的数据拥有更细粒度的控制,因此才希望有一套自定义解决方案。

Agent 设计

我在 VS Code Copilot Chat 环境中实现了该 PoC,并使用了以下工具和提示词。

MCP 工具

  • 浏览器工具browsermcp:用于导航和操作招聘网站页面。
  • SQLite 数据库工具genai-toolbox:用于持久化存储提取到的职位数据。

./vscode/mcp.json

{
  "servers": {
    "browsermcp": {
      "type": "stdio",
      "command": "npx",
      "args": [
        "@browsermcp/mcp@latest"
      ]
    },
    "sqlite": {
      "command": "~/genai-toolbox/toolbox",
      "args": [
        "--prebuilt",
        "sqlite",
        "--stdio"
      ],
      "env": {
        "SQLITE_DATABASE": "~/jobs/jobs.db"
      }
    }
  }
}

数据库 Schema

CREATE TABLE IF NOT EXISTS xyz_company_jobs (
    job_id TEXT PRIMARY KEY, -- 职位的唯一标识
    req_id TEXT,             -- 招聘需求 ID
    job_title TEXT,          -- 职位名称
    location TEXT,
    date_posted TEXT,        -- 格式:'YYYY-MM-DD'
    business_department TEXT,
    job_description_url TEXT,
    job_description TEXT     -- 职位描述的主要内容
);

Agent 提示词

你是一个可以访问浏览器工具和 SQLite 数据库工具的 AI Agent。你的任务是从 XYZ_Company 的招聘网站中收集与软件开发相关的职位信息,并将提取到的数据存储到 SQLite 数据库中。当前浏览器中打开的页面是 XYZ_Company 的职位搜索门户。

数据库:
```sql
CREATE TABLE IF NOT EXISTS xyz_company_jobs (
    job_id TEXT PRIMARY KEY, -- 职位的唯一标识
    req_id TEXT,             -- 招聘需求 ID
    job_title TEXT,          -- 职位名称
    location TEXT,
    date_posted TEXT,        -- 格式:'YYYY-MM-DD'
    business_department TEXT,
    job_description_url TEXT,
    job_description TEXT     -- 职位描述的主要内容
)
```

规则:
- 在调用一次 `click()` 操作后,必须等待 10 秒,确保页面完全加载,然后再调用 `snapshot` 捕获当前页面状态。
- 在向 `xyz_company_jobs` 表插入新记录之前,需要检查 `job_id` 是否已经存在,以避免重复数据。
- 在生成 INSERT SQL 语句时,确保对值中的单引号进行正确转义。
- 不要收集或点击位于 `document > main > group Similar Position` 区域下的职位。

用户已经准备的环境:
- 浏览器中打开的页面是 XYZ_Company 的职位搜索门户。

你的安装过程:
1. 如果 `xyz_company_jobs` 表不存在,则创建该表。在执行 SQL 时,需保留 SQL 代码块中的注释行。

执行过程:
1. 每一个文本匹配 "$Job_Title$ 于 $time_since_publication$ 前发布" 模式的按钮都代表一个职位。通过 `今天 - time_since_publication` 计算 `Date Posted`。
2. 点击职位按钮以打开职位详情页面,该页面的 URL 即为 `Job Description URL`。
3. 从职位详情页面中提取:职位名称、工作地点、Job ID、业务部门(可选)、Req ID 以及职位描述的主要内容。
4. 仅收集与软件开发相关的职位。
5. 将每个收集到的职位插入 `xyz_company_jobs` 表,并基于 `job_id` 确保不产生重复记录。
6. 如有需要,点击 `更多职位` 按钮以加载更多职位。
7. 至少收集并存储 10 个职位。

运行

  1. 打开一个 Chrome 浏览器标签页,访问 XYZ 公司的招聘门户网站。在该标签页上激活 Browser MCP Chrome 扩展程序。
  2. 在 VS Code 中启动一个新的 Copilot Chat 会话,并使用上述 MCP 配置和提示。

总结

通过上述配置,我成功实现了一个 AI Agent,它能够自动 Watching 目标公司的招聘页面,发现新的软件开发职位。该 Agent 可以自动浏览招聘网站、识别相关职位、提取结构化数据,并将其存储到 SQLite 数据库中,从而方便后续访问和长期跟踪。

后续工作

该 PoC 还可以在多个方面进行扩展:

  • 引入更复杂的匹配逻辑,例如简历解析和基于语义的技能匹配。
  • 将收集到的职位信息导出为 RSS 或邮件摘要,从而构建一个完全自托管的职位提醒系统。
  • 添加通知机制,在发现新的匹配职位时立即提醒我。

1 月 13 日发布的 Chrome 144 稳定版正式支持在 chrome://inspect/#remote-debugging 页面直接启动 remote debugging ,这对于 Chrome Devtools MCP 以及其他希望通过 CDP 协议 来对 Chrome 默认 profile 进行自动化控制的用户是重大利好。Edge 144 也同样支持了该特性。


比如你的工作流中需要 LLM 使用 Chrome Devtools MCP 控制浏览器,在此前的 Chrome 版本中,一般的配置方案如下:

  1. 关闭所有 Chrome 实例,使用 chrome --remote-debugging-port=9222 --user-data-dir=xxx 命令启动一个全新的 Chrome 实例。由于 --user-data-dir 不允许使用用户默认的 profile 路径,用户或 LLM 需要在全新的 profile 下重新登录以访问受限资源。

  2. 使用默认的 Chrome Devtools MCP 配置,比如

    "chrome-devtools": {
      "command": "npx",
      "args": [
        "-y",
        "chrome-devtools-mcp@latest",
        "--browser-url=http://127.0.0.1:9222"
      ]
    }
    

更新 Chrome 144 后,只需做如下配置 LLM 即可通过 Chrome Devtools MCP 控制当前 正在运行 的 Chrome 默认 profile:

  1. 在 Chrome 中访问 chrome://inspect/#remote-debugging,勾选 Allow remote debugging for this browser instance

  2. 使用如下 Chrome Devtools MCP 配置

    "chrome-devtools": {
      "command": "npx",
      "args": [
        "-y",
        "chrome-devtools-mcp@latest",
        "--auto-connect"
      ]
    }
    

这一特性带来很多好处:

  • 你不必关闭正在访问的页面就能立即让 LLM 或其他 CDP 工具控制浏览器
  • LLM 可以复用已经你登陆、已有 cookie 的浏览器会话
  • 在 Web 开发测试过程中,网站出现问题时 LLM 能够随时接管并共享所有的 Devtools 上下文

想要更好地利用 CDP 协议并发现更多工具,可以参考 ChromeDevTools/awesome-chrome-devtools


📌 转载信息
原作者:
carlpayne
转载时间:
2026/1/18 19:06:50

版本号:v0.0.2-alpha

修复:

详细参数无法修改

Profile 与启动页信息不同步的 BUG

更新

添加 Camoufox 支持:

  • Kiro 人工注册测试:Passed
  • Outlook 注册:Failed(

目前(有点难)的点

  • Camoufox 怎么做到开箱即用?
  • Outlook 注册账户失败(

仓库

欢迎 PR、Issues 并提出改进意见!!!!


📌 转载信息
转载时间:
2026/1/15 10:20:42

AIPex 最新发布了新版本,其中最重要的能力之一,是浏览器任务可以在后台运行,而不打断用户的正常工作流

这一能力并非来自某个“技巧”,而是源于一个明确的工程选择:
我们有意识地避免将浏览器控制建立在 debugger ( Chrome DevTools Protocol )之上。

本文将解释为什么主流方案普遍选择 debugger ,以及 AIPex 为什么在多数智能代理与日常自动化场景中,选择了一条不同的路线。

为什么大多数浏览器控制方案选择 debugger ( CDP )

在当前无需迁移的浏览器自动化插件或 Agent 中,常见方案包括:

  • Manus 的 Manus Browser Operator
  • Claude 推出的 Claude in Chrome
  • 开源社区的 nano browser
  • 以及 Puppeteer / Playwright 等自动化工具的扩展形态

这些方案通常基于 Chrome DevTools Protocol ( CDP ),尤其是其 debugger 能力来实现浏览器控制,原因并不复杂:

1. 能力覆盖完整

CDP 提供了浏览器内部几乎所有关键能力,包括:

  • 页面导航与生命周期控制
  • DOM 与 AXTree ( Accessibility Tree )访问
  • 事件注入(鼠标、键盘、滚轮)
  • 网络拦截与修改
  • 截图、录屏、性能采样

对于复杂自动化而言,CDP 是一个“开箱即用”的全能力接口。


2. 可访问性树( AXTree )高度语义化

通过 CDP ,可以直接获取浏览器构建的 Accessibility Tree

  • 每个节点都具备 role / name / state
  • 天然适合语音辅助与 AI 理解
  • 在理想 ARIA 实现下,语义质量很高

因此,AXTree 成为了许多 AI Agent 的主要页面表达形式。


3. 工程生态成熟

围绕 CDP 已经形成成熟工具链:

  • Puppeteer 、Playwright 等底层实现
  • 完整的文档、示例与社区经验
  • 对自动化工程师而言,学习与接入成本明确


debugger ( CDP )在桌面场景中的现实代价

尽管 CDP 能力强大,但在“与用户并行工作的桌面场景”中,它也带来了一些难以忽视的问题。

1. 前台焦点与用户体验问题

CDP 并非以“后台无打扰”为设计目标。

在真实桌面环境中:

  • debugger attach 往往会触发 Tab 激活或窗口前置
  • 输入与视觉焦点可能被强制抢占
  • 即使通过 headless 或参数规避,也难以在不同平台与浏览器上保证一致行为

结果是:
当用户正在使用其他应用或标签页时,自动化任务可能打断其当前操作,严重影响体验。


2. 浏览器与运行环境耦合

使用 CDP 通常意味着:

  • 需要启用调试端口
  • 强绑定 Chrome / Chromium
  • 对部分嵌入式 WebView 、受限环境或非 Chromium 浏览器支持不佳

在企业环境或多浏览器生态中,这种耦合会显著增加部署与维护成本。


3. 安全与权限摩擦

调试端口、进程权限、证书配置等问题,在企业与受管环境中常常触发:

  • 安全策略拦截
  • 合规审查
  • IT 运维阻力

这类问题并非技术不可解,而是部署摩擦成本过高


为什么浏览器控制不一定需要 debugger

AIPex 的核心设计目标是:

让浏览器任务像“背景思考”一样运行,而不是像“远程操控”一样打断用户。

为此,我们选择了一条不以 debugger 为中心的路径。


AIPex 的方案:DOM 语义快照 + 轻量交互

在页面侧,AIPex 采用纯 JavaScript / TypeScript 能力,实现:

  • 语义化页面快照
  • 稳定节点映射
  • 轻量级事件交互

而不是依赖 CDP 的 AXTree 与调试通道。

1. 语义快照,而非调试树

AIPex 基于 @aipexstudio/dom-snapshot

  • 直接遍历 DOM Tree
  • 提取可访问性相关语义( role / name / state )
  • 不依赖 CDP 的 Accessibility Tree ( AXTree )

该库在 README 中明确说明:
它是一个纯 DOM 方案,而非 CDP 的替代封装。


2. 稳定、可复用的节点 ID

自动为页面元素生成稳定的:data-aipex-nodeid

这使得:

  • “语义快照中的节点”与“真实 DOM 元素”之间的映射可长期复用
  • 避免调试态下常见的选择器漂移问题
  • 支持从文本命中直接反查到可操作元素


3. 面向可交互对象的快照策略

语义快照优先关注:

  • 按钮、链接、输入框等可操作元素
  • 对话与任务相关的界面子集

并过滤:

  • display: none
  • visibility: hidden
  • aria-hidden
  • inert

从而避免将无意义或不可见节点暴露给 Agent 。


4. 文本化表达与语义搜索

快照可被转换为可朗读、可搜索的文本形式( TextSnapshot ):

→uid=dom_abc123 RootWebArea "My Page" <body>
uid=dom_def456 button "提交" <button>
uid=dom_ghi789 textbox "邮箱" <input> desc="请输入邮箱"
StaticText "欢迎回来"
*uid=dom_jkl012 link "了解更多" <a>

其中:

  • 表示当前聚焦元素

→ 表示焦点祖先

该表示既适合 TTS / 语音播报,也支持自然语言驱动的检索。

  1. 语义搜索示例
    支持管道分隔与 glob 搜索:
searchSnapshotText(formatted, '登录 | Login | Sign In');
searchSnapshotText(formatted, 'button* | *submit*', {
  useGlob: true,
  contextLevels: 2
});

命中的文本行可通过 data-aipex-nodeid 精确映射回 DOM 元素。

  1. 页面侧事件,而非调试注入

交互通过页面侧事件完成(如 click 、focus 、input ):

  • 通过内容脚本或扩展消息通道触发

  • 与后台任务调度通信

  • 无需调试端口

  • 不强制拉起前台窗口

网页语义表达的工程视角

在浏览器自动化与 AI Agent 场景中,最常被用作页面表达的主要有两类:

DOM Tree

来源:浏览器原生文档对象模型

特点:信息完整但冗余,语义弱

直接使用不利于 AI 理解与操作

Accessibility Tree ( AXTree )

来源:ARIA 语义派生

特点:高度语义化

局限:

  • 依赖站点 ARIA 实现质量

-节点信息并不完备

  • 远程访问通常依赖 CDP

在实践中,如果完全依赖 AXTree ,Agent 的“感知能力”往往受限于目标网站的可访问性水平——这在现实 Web 中并不理想。

AIPex 的选择与边界

通过对 DOM Tree 进行语义化处理,AIPex 在不依赖 debugger 的前提下,实现了:

  • 后台运行、不打断用户

  • 更完整的页面信息表达

需要说明的是:

对于涉及浏览器特权能力的场景(如网络拦截、性能采样、权限弹窗、文件系统访问等),CDP 仍然具有不可替代的价值。

AIPex 并非否定 debugger ,而是在日常自动化与智能代理场景中,优先选择对用户体验更友好的工程解法。

参考与来源

我一直用 Windsurf,寸止是我必不可少的工具。因为日常写代码,交互很频繁,没这个工具,点数很快就用完了。

但是这两天 Windsurf 封了寸止等工具,所以自己写了一个。

不知道 Windsurf 是怎么检测的,所以也不能保证自己的工具能活多少天。

目前功能比较简单:

  • 能保持持续对话
  • 能发送图片
  • Mac / Windows 都支持

方案比较简单,会通过打开浏览器标签进行交互,体验上可能差点。

安装

{ "mcpServers": { "hold-on": { "command": "npx", "args": ["-y", "hold-on-mcp"], "env": { "HOLD_ON_THEME": "auto" } } } } 

主题:auto(跟随系统)/ light / dark

使用

windsurf 中添加全局规则

**强制交互协议:** 1. 你现在拥有 `request_approval` 工具。
2. **在完成每次实质性输出后**(如回答问题、写代码、改文件),必须调用 `request_approval` 简述你做了什么,等待用户确认。
3. 如果用户通过弹窗给了你调整指令,你必须在**当前这一轮对话**中立即修改,不准重新开启新对话。
4. 用户留空点确认 = 满意;用户输入内容 = 需要调整。

测试情况

我在 Mac 和 Win 上都测试过了,不过 Win 上只是简单测试了一下。

有问题欢迎大家反馈。

项目地址

欢迎大家 star

还有,这个 mcp 也是用 windsurf 写的


📌 转载信息
原作者:
man9527
转载时间:
2026/1/11 08:35:12

背景

2025 年底的时候,我想要找一个能够帮我自动化执行浏览器任务的软件。但是在 agent 元年各种 agent 盛行,大吹特吹,演示视频似乎很厉害,但没有一个真正能用的。不是体验差,就是 token 消耗多价格高执行慢或者执行不稳定,成功率低。一怒之下,我就开始自己开发了,开发了几周经过了一小部分种子用户的体验和改进。现在可以这么说,browserwing 已经可以完成很多自动化任务了,通过 AI 辅助和 AI 的调度,帮你每天省下 x 个小时。

截屏 2026-01-10 11.59.28.png

ai_extract_mode.png

使用

演示

下面的演示,你也可以在 20 分钟内复刻实现,不是那种花里胡哨的但没用的演示。

// ==UserScript==
// @name         Grok/X.ai 自动化注册机 (集成自动清理与循环版)
// @namespace    http://tampermonkey.net/
// @version      5.0
// @description  全自动流程:注册 -> 提取Token -> 清理Cookie -> 循环重启
// @author       Bytebender
// @match        *://*/*
// @match        https://x.ai/*
// @match        https://www.x.ai/*
// @match        https://accounts.x.ai/*
// @match        https://grok.com/*
// @match        https://www.grok.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_notification
// @grant        GM_xmlhttpRequest
// @grant        GM_setClipboard
// @grant        GM_cookie
// @connect      mail.chatgpt.org.uk
// @connect      100.64.0.101
// @connect      api.x.ai
// @connect      x.ai
// @connect      grok.com
// @connect      www.grok.com
// @connect      accounts.x.ai
// ==/UserScript==

(function() {
    'use strict';

    // ========================================================
    // 1. 随机数据生成工具
    // ========================================================

    // 生成随机姓名 (首字母大写)
    function getRandomName() {
        const chars = 'abcdefghijklmnopqrstuvwxyz';
        const len = Math.floor(Math.random() * 5) + 4; // 长度 4-8
        let result = '';
        for (let i = 0; i < len; i++) {
            result += chars.charAt(Math.floor(Math.random() * chars.length));
        }
        return result.charAt(0).toUpperCase() + result.slice(1);
    }

    // 生成强密码 (12位,包含大小写+数字+特殊符号)
    function getRandomPassword() {
        const lower = "abcdefghijklmnopqrstuvwxyz";
        const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
        const nums = "0123456789";
        const symbols = "!@#$%^&*";

        // 1. 确保每种字符至少有一个
        let pass = "";
        pass += lower[Math.floor(Math.random() * lower.length)];
        pass += upper[Math.floor(Math.random() * upper.length)];
        pass += nums[Math.floor(Math.random() * nums.length)];
        pass += symbols[Math.floor(Math.random() * symbols.length)];

        // 2. 补足剩余长度
        const allChars = lower + upper + nums + symbols;
        for (let i = 0; i < 8; i++) {
            pass += allChars[Math.floor(Math.random() * allChars.length)];
        }

        // 3. 打乱顺序 (洗牌)
        return pass.split('').sort(() => 0.5 - Math.random()).join('');
    }

    // ========================================================
    // 2. 任务编排配置
    // ========================================================
    const actions = [
        // --- 阶段一:Grok 首页跳转 ---
        {
            "step_name": "1. 点击 Grok 首页入口",
            "type": "click",
            "selector": "html > body > div:nth-of-type(2) > div > div > div > main > div > div > div:nth-of-type(3) > div:nth-of-type(2) > a:nth-of-type(2)",
            "url_keyword": "grok.com"
        },
        // --- 阶段二:进入注册页 (X.ai) ---
        {
            "step_name": "2. 点击 X.ai 注册/登录按钮",
            "type": "click",
            "selector": "html > body > div:nth-of-type(2) > div > div > div:nth-of-type(2) > div > div:nth-of-type(2) > button",
            "url_keyword": "accounts.x.ai"
        },
        // --- 阶段三:自动化邮箱 ---
        {
            "step_name": "3. 自动申请并填写临时邮箱",
            "type": "get_email",
            "selector": "html > body > div:nth-of-type(2) > div > div > div:nth-of-type(2) > div > form > div > div > input",
        },
        {
            "step_name": "4. 点击下一步 (提交邮箱)",
            "type": "click",
            "selector": "html > body > div:nth-of-type(2) > div > div > div:nth-of-type(2) > div > form > div:nth-of-type(2) > button"
        },
        // --- 阶段四:验证码 ---
        {
            "step_name": "5. 等待邮件验证码并自动填写",
            "type": "fill_code",
            "selector": "input[name='code']"
        },
        // --- 阶段五:填写个人信息 (全随机) ---
        {
            "step_name": "6. 填写名 (First Name)",
            "type": "input",
            "selector": "input[name='givenName']",
            "value": "__RANDOM__"
        },
        {
            "step_name": "7. 填写姓 (Last Name)",
            "type": "input",
            "selector": "input[name='familyName']",
            "value": "__RANDOM__"
        },
        {
            "step_name": "8. 填写密码 (强密码)",
            "type": "input",
            "selector": "input[name='password']",
            "value": "__RANDOM_PASS__"
        },
        // --- 阶段六:提交 ---
        {
            "step_name": "9. 点击最终提交",
            "type": "click",
            "selector": "button[type='submit']"
        },
        // --- 阶段七:提取 Token ---
        {
            "step_name": "10. 检查跳转并上传 Token",
            "type": "wait_url_and_upload",
            "target_url": "grok.com"
        },
        // --- 阶段八:清理环境并循环 ---
        {
            "step_name": "11. 清理 Cookie 并重启循环",
            "type": "clean_and_restart",
            "clean_targets": [
                "https://x.ai/",
                "https://www.x.ai/",
                "https://accounts.x.ai/",
                "https://grok.com/",
                "https://www.grok.com/"
            ]
        }
    ];

    // ========================================================
    // 3. 核心功能类 (邮箱/网络/Cookie)
    // ========================================================

    // 3.1 网络请求封装
    function gmFetch(url, options) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                url: url,
                method: options.method || 'GET',
                headers: options.headers || {},
                data: options.body || null,
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        try { resolve(JSON.parse(response.responseText)); }
                        catch (e) { resolve(response.responseText); }
                    } else { reject(new Error(`HTTP Error: ${response.status}`)); }
                },
                onerror: () => reject(new Error('Network Error')),
                ontimeout: () => reject(new Error('Timeout'))
            });
        });
    }

    // 3.2 临时邮箱客户端
    class TempMailClient {
        constructor() {
            this.baseUrl = "https://mail.chatgpt.org.uk/api";
            this.headers = {
                "User-Agent": "Mozilla/5.0",
                "Origin": "https://mail.chatgpt.org.uk",
                "Referer": "https://mail.chatgpt.org.uk/"
            };
        }
        async getEmail() {
            const result = await gmFetch(`${this.baseUrl}/generate-email`, {
                method: "GET",
                headers: { ...this.headers, "content-type": "application/json" }
            });
            if (result && result.success && result.data?.email) return result.data.email;
            throw new Error("邮箱API返回异常");
        }
        async fetchMessages(email) {
            const url = `${this.baseUrl}/emails?email=${encodeURIComponent(email)}`;
            const result = await gmFetch(url, {
                method: "GET",
                headers: { ...this.headers, "cache-control": "no-cache" }
            });
            return (result.success && result.data?.emails) ? result.data.emails : [];
        }
        async waitForCode(email, timeoutSec = 120) {
            console.log(`[Mail] 开始监听 ${email} ...`);
            const startTime = Date.now();
            const codeRegex = /\b[A-Z0-9]{3}-[A-Z0-9]{3}\b|\b\d{6}\b/;
            return new Promise((resolve, reject) => {
                const timer = setInterval(async () => {
                    if (Date.now() - startTime > timeoutSec * 1000) {
                        clearInterval(timer);
                        reject(new Error("等待验证码超时"));
                    }
                    try {
                        const msgs = await this.fetchMessages(email);
                        if (msgs.length > 0) {
                            for (const msg of msgs) {
                                const content = (msg.subject || "") + " " + (msg.html_content || "");
                                const match = content.match(codeRegex);
                                if (match) {
                                    clearInterval(timer);
                                    resolve(match[0]);
                                    return;
                                }
                            }
                        }
                    } catch(e) { console.warn("Polling error:", e); }
                }, 3000);
            });
        }
    }

    // 3.3 Token 上传逻辑
    async function extractAndUploadToken() {
        return new Promise((resolve, reject) => {
            GM_cookie.list({ name: "sso" }, (cookies, error) => {
                if (error || !cookies || cookies.length === 0) {
                    return reject(new Error("SSO Cookie missing"));
                }
                const ssoToken = cookies[0].value;
                console.log("获取到 Token:", ssoToken.substring(0, 10) + "...");

                GM_xmlhttpRequest({
                    url: "http://xxx/api/tokens/add",
                    method: "POST",
                    headers: {
                        "content-type": "application/json",
                        "authorization": "Bearer xxxxx"
                    },
                    data: JSON.stringify({ tokens: [ssoToken], token_type: "sso" }),
                    onload: (response) => {
                        if (response.status >= 200 && response.status < 300) {
                            console.log("Token 上传成功!");
                            resolve();
                        } else {
                            reject(new Error("Upload failed: " + response.responseText));
                        }
                    },
                    onerror: (err) => reject(err)
                });
            });
        });
    }

    // 3.4 Cookie 清理逻辑
    function executeCleanCookies(targetUrls) {
        return new Promise((resolve) => {
            if (!targetUrls || targetUrls.length === 0) return resolve();
            let completed = 0;
            targetUrls.forEach(url => {
                GM_cookie.list({ url: url }, function(cookies, error) {
                    if (cookies && cookies.length > 0) {
                        cookies.forEach(c => {
                            GM_cookie.delete({ name: c.name, url: url }, () => {});
                        });
                    }
                    completed++;
                    if (completed === targetUrls.length) {
                        setTimeout(resolve, 800); // 缓冲
                    }
                });
            });
        });
    }

    // 3.5 Native 输入模拟 (绕过 React/Vue 绑定)
    function setNativeValue(element, value) {
        const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set;
        const prototype = Object.getPrototypeOf(element);
        const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;
        if (valueSetter && valueSetter !== prototypeValueSetter) {
            prototypeValueSetter.call(element, value);
        } else {
            valueSetter.call(element, value);
        }
        element.dispatchEvent(new Event('input', { bubbles: true }));
    }

    // 3.6 元素等待
    const waitForElement = (selector, timeout = 10000) => {
        return new Promise((resolve, reject) => {
            const el = document.querySelector(selector);
            if (el) return resolve(el);
            const observer = new MutationObserver(() => {
                const el = document.querySelector(selector);
                if (el) { observer.disconnect(); resolve(el); }
            });
            observer.observe(document.body, { childList: true, subtree: true });
            setTimeout(() => { observer.disconnect(); reject(new Error('元素超时: ' + selector)); }, timeout);
        });
    };

    // ========================================================
    // 4. 自动化执行引擎
    // ========================================================
    const mailClient = new TempMailClient();
    let isRunning = GM_getValue('script_is_running', false);
    let currentIndex = GM_getValue('script_step_index', 0);

    console.log(`🚀 [注册机状态] Running: ${isRunning} | Step: ${currentIndex}`);

    GM_registerMenuCommand(`▶️ 启动/继续`, () => {
        GM_setValue('script_is_running', true);
        isRunning = true;
        runCurrentStep();
    });

    GM_registerMenuCommand("🔄 强制重置", () => {
        GM_setValue('script_step_index', 0);
        GM_setValue('script_is_running', false);
        GM_setValue('current_temp_email', '');
        location.reload();
    });

    async function runCurrentStep() {
        if (!GM_getValue('script_is_running', false)) return;

        // 异常保护:索引越界重置
        if (currentIndex >= actions.length) {
            GM_setValue('script_step_index', 0);
            return location.reload();
        }

        const action = actions[currentIndex];
        console.log(`[Step ${currentIndex + 1}] ${action.step_name} (${action.type})`);

        // URL 检查 (如果不在目标域名,等待跳转)
        if (action.url_keyword && !location.href.includes(action.url_keyword)) {
            console.log(`等待跳转到 ${action.url_keyword}...`);
            return setTimeout(runCurrentStep, 2000);
        }

        try {
            await new Promise(r => setTimeout(r, 3000)); // 基础缓冲
            let el = null;
            if (action.selector) el = await waitForElement(action.selector);

            // --- 动作分发 ---
            if (action.type === 'get_email') {
                const email = await mailClient.getEmail();
                console.log("获取邮箱:", email);
                GM_setValue('current_temp_email', email);
                GM_setClipboard(email);

                el.click(); el.focus();
                setNativeValue(el, email);
                el.dispatchEvent(new Event('change', { bubbles: true }));
                el.blur();
            }
            else if (action.type === 'fill_code') {
                const email = GM_getValue('current_temp_email');
                const rawCode = await mailClient.waitForCode(email);
                const code = rawCode.replace(/-/g, ''); // 清洗连字符
                console.log('填入验证码:', code);

                el.scrollIntoView({block: "center"});
                el.click(); el.focus();
                const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
                nativeSetter.call(el, code);
                el.dispatchEvent(new Event('input', { bubbles: true }));
                el.dispatchEvent(new Event('change', { bubbles: true }));
                await new Promise(r => setTimeout(r, 500));
                el.blur();
            }
            else if (action.type === 'input') {
                el.focus();
                let val = action.value;

                // 处理随机变量
                if (val === '__RANDOM__') val = getRandomName();
                if (val === '__RANDOM_PASS__') val = getRandomPassword();

                console.log(`[Input] 填写值: ${val}`);
                setNativeValue(el, val);
                el.blur();
            }
            else if (action.type === 'wait_url_and_upload') {
                if (!location.href.includes(action.target_url)) {
                    return setTimeout(runCurrentStep, 1500); // URL不对,继续等待
                }

                // 尝试多次上传,防止 Cookie 未即时写入
                let retry = 0;
                while (retry < 5) {
                    try {
                        await extractAndUploadToken();
                        GM_notification({ text: 'Token 上传成功!准备清理...', title: '成功' });
                        break;
                    } catch (e) {
                        console.warn("Token提取失败,重试中...", e);
                        await new Promise(r => setTimeout(r, 2000));
                        retry++;
                    }
                }
                // 继续下一步
            }
            else if (action.type === 'clean_and_restart') {
                GM_notification({ text: '清理 Cookie 并重启循环...', title: '系统维护' });
                await executeCleanCookies(action.clean_targets);

                GM_setValue('script_step_index', 0);
                GM_setValue('current_temp_email', '');

                console.log(">>> 循环重置完成,3秒后刷新");
                setTimeout(() => {
                    window.location.href = "https://grok.com/";
                }, 3000);
                return; // 结束本次执行栈
            }
            else if (action.type === 'click') {
                el.click();
            }

            // --- 步进逻辑 ---
            currentIndex++;
            GM_setValue('script_step_index', currentIndex);
            setTimeout(runCurrentStep, 1500);

        } catch (e) {
            console.error("执行出错:", e);
            // 遇到严重错误可以考虑刷新页面重试
            // setTimeout(() => location.reload(), 5000);
        }
    }

    // 启动检测
    function tryStart() {
        if (isRunning) {
            setTimeout(runCurrentStep, 1500);
        }
    }

    if (document.readyState === 'complete' || document.readyState === 'interactive') {
        tryStart();
    } else {
        window.addEventListener('load', tryStart);
    }

})();

修改 extractAndUploadToken 填入自己的 Grok2api 地址和凭证

开启无痕窗口打开 Grok,接着启动脚本即可

会自动生成邮箱、填写注册信息和验证码

IP 不好 CF 验证不会自动过,需要手动继续和点击下一步

IP 好就全自动了啦

循环注册 w