跳转到主要内容

不给 AI 登录信息的智能体浏览器自动化

一篇面向初学者的教程:用 Selenium、noVNC 手动登录和受限 API 封装,构建按计划或队列运行的智能体浏览器自动化,让智能体在看不到凭证、也不控制你个人电脑的情况下创建网站表单草稿。

9 分钟阅读
分享:
AI 驱动

AI 驱动 · 每小时限 20 次请求

人类保留登录钥匙,AI 只把结构化请求发送给受保护的浏览器容器
核心思路就一句话:登录归人,数据归 AI,浏览器归隔离容器。

太长不看版

如果你只是偶尔要让 AI 帮忙查点东西,直接用 ChatGPT、Codex、Claude Desktop 自带的浏览器就够了,不用往下看了。

这篇教程要解决的是另一回事:需要定时跑、按队列跑的 Agent 自动化。比如每天自动起草表单、定期刷新某个门户、处理内部后台的重复劳动,或者合上笔记本之后还要在服务器上继续跑的工作流。

定时器 / 队列
  -> Agent 工作流
  -> 你写的一层薄 Wrapper API
  -> Selenium WebDriver
  -> 浏览器容器
  -> 已登录的网站

你自己通过 noVNC 打开一个看得见的浏览器去登录。AI 只能向 Wrapper 发结构化请求,比如"帮我填这份草稿"、"截个图给我看看"。登录凭证不会进 AI 对话,也不用让 Computer-Use Agent 直接操作你的电脑。

一句话总结适用场景:网站要登录、没有公开 API、但你又希望 Agent 跑在小服务器上,而不是靠你的笔记本 24 小时开机。

到底要解决什么问题

很多真正有用的流程,都卡在网页背后。

合作方门户、内部后台、学校表单、会员站点,或者某个只能在浏览器里点的老系统——你明明知道 AI 可以帮你准备数据、填好表单,但网站就是没 API。

最容易想到的捷径,往往最危险:

把账号密码给 AI
让 AI 打开网站
让 AI 替我操作浏览器

这么干等于把一堆职责搅在一起:AI 看到了密码,AI 可以乱点一通,你的笔记本也被拖进了生产流程。万一这个任务要半夜跑、要出差时也跑,难道你的个人电脑要 24 小时开机?

更合理的分工是这样:

人负责登录。
浏览器容器负责会话。
Wrapper API 负责哪些动作可以放行。
Agent 负责循环执行的业务流程。

这篇教程要搭的,就是这套架构。

那直接用 ChatGPT 的浏览器不行吗?

AI 自带的浏览器,给交互式任务用挺顺手:你人在旁边,问一句,它打开页面、读内容、点按钮、把结果汇报给你。

但 Agent 自动化是另一种形态。它可能每天早上 8 点自己起来干活,可能从队列里一条条取任务,可能在网站超时时自己重试,可能在你睡觉时把草稿填好,只在会话掉线需要重新登录时才叫你。

场景AI 自带浏览器Wrapper API + Selenium
临时查个资料、浏览一下正合适杀鸡用牛刀
每天跑、或者事件触发的自动化撑不住正合适
电脑关机后还要继续跑做不到可以,跑在服务器上
给 Agent 用的稳定工具接口有限明确的 HTTP API
登录怎么处理容易和聊天内容混在一起人通过 noVNC 自己登

这里不是说自带浏览器不好——它解决交互场景,这套模式解决自动化场景,各管一摊。

拿一个每日自动化场景举例

假设你有一个没有 API 的供应商门户。每个工作日早上,你想让 Agent 根据昨天的运营笔记,自动起草一批请求。

早上 8 点定时触发
  -> 从数据库或邮箱拉出昨天的运营笔记
  -> AI 写好请求的标题和正文
  -> Agent 用 dry_run=true 调用 POST /portal/draft-request
  -> Wrapper 在已登录的浏览器里填好表单
  -> Wrapper 返回截图和状态
  -> 如果需要人工审核、重新登录或批准提交,Agent 通知你

这跟"让聊天工具临时帮我浏览一下"完全不是一回事。Agent 有固定的任务、有重试路径、有明确的工具接口,也有"该停下来等人"的刹车机制。

第一版建议把最后一步留给人。让 Agent 起草,让 Wrapper 返回截图,只要动作有风险,最终点"提交"的还是人。

你要搭什么东西

教程里用的目标网站,是一个有"新建请求"表单的私有供应商门户。表单包含 subjectdetailspriority,以及一个"保存草稿"按钮。你实际的网站肯定不一样,但架构是通用的。

组件干什么
定时器 / 队列每天定时或来任务时启动自动化
Agent 工作流准备数据、调用工具、处理重试和审核状态
Wrapper API把安全的 HTTP 请求翻译成 Selenium 动作
Selenium 容器跑 Chrome,在 4444 暴露 WebDriver
noVNC让你通过 7900 看到并操作浏览器

官方的 Docker Selenium README 里写清楚了独立浏览器镜像、WebDriver 端口、noVNC 端口,以及常见的 --shm-size 2g 配置。下面我们要做的,是在这层浏览器外面再包一个更小的 Wrapper,让 AI 不直接跟 WebDriver 打交道。

你需要准备什么

  • Docker 和 Docker Compose
  • 一个你有权拿来做自动化的网站账号
  • 基本的终端操作经验
  • 一个能发 HTTP 请求的 Agent 运行环境,比如 n8n、cron + worker,或者任意 Agent 框架

第一步:起一个看得见的浏览器容器

先建个目录:

bash
mkdir browser-automation-wrapper
cd browser-automation-wrapper

新建 docker-compose.yml

yaml
services:
  selenium:
    image: selenium/standalone-chrome:latest
    shm_size: "2g"
    ports:
      - "127.0.0.1:4444:4444"
      - "127.0.0.1:7900:7900"
    environment:
      SE_VNC_PASSWORD: "change-this-password"

拉起来:

bash
docker compose up -d

打开实时浏览器:

http://127.0.0.1:7900/?autoconnect=1&resize=scale&password=change-this-password

用这个浏览器自己登录目标网站。不要让 AI 帮你登,更不要把密码粘到 AI 对话框里。从这一刻起,浏览器会话就活在容器里了。

第二步:把 WebDriver 藏好

WebDriver 权限非常大——谁能访问它,谁就能完全控制浏览器。这就是为什么 compose 文件里要把 44447900 都绑到 127.0.0.1

受保护的 API 网关只放行批准的自动化卡片,阻挡任意浏览器命令
Wrapper 就是那道闸门:白名单内的动作放行,任意浏览器控制一律挡住。
要做别做
Selenium 绑定到 localhost4444 直接暴露在公网
noVNC 只用于手动登录和调试把 noVNC 权限交给 Agent
只暴露一个很窄的 Wrapper API让 AI 直接发任意 WebDriver 命令

这是整套方案最关键的安全边界。AI 不应该拿到一个"通用浏览器控制"端点,它应该只有一个小小的 API,里面装的都是你审核过的动作。

第三步:先把允许的流程写清楚

写代码之前,先把任务用文字描述清楚。拿示例门户来说:

允许的动作:
  在供应商门户里创建一份请求草稿。

输入:
  subject
  details
  priority
  dry_run

规则:
  绝不登录。
  绝不动账号设置。
  dry_run 不是 false 时,绝不提交最终表单。
  如果浏览器停在登录页,返回 needs_login。
  返回截图路径,方便调试。

这正是 Wrapper 比 Computer-Use Agent 更安全的地方:Agent 没法临时起意去点"账号设置",因为你的 API 里压根没这个动作。

第四步:让 AI 帮你生成 Wrapper

下面这段 prompt 可以直接丢给你的编程 AI。用之前先打开目标页面看一下,把里面假的门户 URL 和选择器名换成你真实网站的信息。

构建一个小型 FastAPI 应用,用来封装 Selenium Remote WebDriver。

目标:
做一个很窄的 API,用来填写一个已经登录的网站表单。网站本身没有 API。
用户会通过 noVNC 手动登录。应用严禁直接处理用户名、密码、2FA 验证码或 cookies。

技术栈:
- Python 3.12
- FastAPI
- Selenium 的 Python 包
- Remote WebDriver 地址来自 SELENIUM_URL,默认 http://selenium:4444/wd/hub

端点:
- GET /health
- POST /portal/draft-request
- GET /debug/screenshot

安全要求:
- 除 /health 外,所有端点都必须校验 Authorization: Bearer $AUTOMATION_API_TOKEN。
- 不接受调用方传入的任意 CSS 选择器、URL 或 JavaScript。
- 选择器统一放在服务端的 SELECTORS 字典里。
- 如果浏览器在登录页,返回 {"status": "needs_login"}。
- dry_run 默认 true。dry_run 模式下只填字段,不点"保存草稿"。
- 记录动作日志,但绝不记录敏感信息或完整 cookies。

草稿请求的输入:
- subject: 字符串,最多 120 字符
- details: 字符串,最多 4000 字符
- priority: low / normal / high 之一
- dry_run: 布尔值,默认 true

Selenium 行为:
- 应用运行期间复用一个浏览器会话。
- 用 WebDriverWait 和 expected_conditions,不要盲目 sleep。
- 导航到 https://example-portal.invalid/requests/new。
- 填写 subject、details、priority。
- 只有 dry_run 为 false 时才点击"保存草稿"。
- 返回 status、current_url、page_title、screenshot_file。

部署要求:
- 提供 Dockerfile 和 docker-compose.yml。
- docker-compose.yml 里把 Wrapper API 绑到 127.0.0.1:8088,方便本地调试。
- Selenium 和 noVNC 不能对外暴露。
- 只在意图不明显的地方加注释,别灌水。

这段 prompt 本身比用什么框架更重要。它把边界讲清楚了:不碰登录、不接受任意浏览器控制、不公开 WebDriver。

第五步:一个最小的 Wrapper 示例

按上面的 prompt 生成的 Wrapper,大概长这样。新建 form-wrapper/main.py

python
import os
from typing import Literal

from fastapi import FastAPI, Header, HTTPException
from pydantic import BaseModel, Field
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import Select, WebDriverWait

SELENIUM_URL = os.getenv("SELENIUM_URL", "http://selenium:4444/wd/hub")
API_TOKEN = os.environ["AUTOMATION_API_TOKEN"]
PORTAL_DRAFT_URL = "https://example-portal.invalid/requests/new"

SELECTORS = {
    "subject": "#request_subject",
    "details": "#request_details",
    "priority": "#request_priority",
    "save_draft": "button[data-action='save-draft']",
}

app = FastAPI()
driver = None


class DraftRequest(BaseModel):
    subject: str = Field(max_length=120)
    details: str = Field(max_length=4000)
    priority: Literal["low", "normal", "high"] = "normal"
    dry_run: bool = True


def require_token(authorization: str | None) -> None:
    if authorization != f"Bearer {API_TOKEN}":
        raise HTTPException(status_code=401, detail="Unauthorized")


def browser():
    global driver
    if driver is None:
        options = webdriver.ChromeOptions()
        driver = webdriver.Remote(command_executor=SELENIUM_URL, options=options)
    return driver


def on_login_page(current_url: str, title: str) -> bool:
    text = f"{current_url} {title}".lower()
    return "login" in text or "sign-in" in text or "signin" in text


@app.get("/health")
def health():
    return {"status": "ok"}


@app.post("/portal/draft-request")
def draft_request(payload: DraftRequest, authorization: str | None = Header(default=None)):
    require_token(authorization)

    page = browser()
    wait = WebDriverWait(page, 20)
    page.get(PORTAL_DRAFT_URL)

    if on_login_page(page.current_url, page.title):
        return {"status": "needs_login", "current_url": page.current_url}

    wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, SELECTORS["subject"]))).clear()
    page.find_element(By.CSS_SELECTOR, SELECTORS["subject"]).send_keys(payload.subject)

    page.find_element(By.CSS_SELECTOR, SELECTORS["details"]).clear()
    page.find_element(By.CSS_SELECTOR, SELECTORS["details"]).send_keys(payload.details)

    Select(page.find_element(By.CSS_SELECTOR, SELECTORS["priority"])).select_by_value(payload.priority)

    if not payload.dry_run:
        wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, SELECTORS["save_draft"]))).click()

    screenshot_file = "/tmp/latest-screenshot.png"
    page.save_screenshot(screenshot_file)

    return {
        "status": "filled" if payload.dry_run else "draft_saved",
        "current_url": page.current_url,
        "page_title": page.title,
        "screenshot_file": screenshot_file,
    }


@app.get("/debug/screenshot")
def screenshot(authorization: str | None = Header(default=None)):
    require_token(authorization)
    page = browser()
    screenshot_file = "/tmp/latest-screenshot.png"
    page.save_screenshot(screenshot_file)
    return {"status": "ok", "screenshot_file": screenshot_file}

这个示例故意做得很窄:不能浏览任意 URL,不能执行任意 JavaScript,也不能填 AI 临时指定的选择器。选择器要你自己看过目标页面之后,在服务端改。

新建 form-wrapper/Dockerfile

dockerfile
FROM python:3.12-slim

WORKDIR /app
RUN pip install --no-cache-dir fastapi uvicorn selenium pydantic

COPY main.py /app/main.py
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8088"]

更新 docker-compose.yml

yaml
services:
  selenium:
    image: selenium/standalone-chrome:latest
    shm_size: "2g"
    ports:
      - "127.0.0.1:4444:4444"
      - "127.0.0.1:7900:7900"
    environment:
      SE_VNC_PASSWORD: "change-this-password"

  form-wrapper:
    build: ./form-wrapper
    depends_on:
      - selenium
    ports:
      - "127.0.0.1:8088:8088"
    environment:
      SELENIUM_URL: "http://selenium:4444/wd/hub"
      AUTOMATION_API_TOKEN: "${AUTOMATION_API_TOKEN}"

新建 .env

bash
AUTOMATION_API_TOKEN=replace-this-with-a-long-random-token

带上 Wrapper 一起重启:

bash
docker compose up -d --build

第六步:先别让 AI 上场,自己测一遍

先把 token 加载到终端:

bash
set -a
. ./.env
set +a

调一下 Wrapper:

bash
curl -X POST http://127.0.0.1:8088/portal/draft-request \
  -H "Authorization: Bearer $AUTOMATION_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "季度供应商沟通",
    "details": "请为下一次供应商评审准备一份请求草稿。",
    "priority": "normal",
    "dry_run": true
  }'

如果返回 needs_login,就打开 noVNC 自己登一次,再重发同一个请求。

在你亲眼看到浏览器把字段填对之前,dry_run 一直开着。确认没问题了,再允许 Wrapper 去点"保存草稿"。对敏感流程来说,最终那个"提交"按钮最好永远留给人。

第七步:让 Agent 来调 Wrapper

Wrapper 手动跑通之后,Agent 工作流就可以像调普通 HTTP API 一样调它了。这个工作流可以是 n8n、cron job、一个小 worker、队列消费者,或者任何一个支持 HTTP 工具的 Agent 框架。

你可以通过这个 API 创建请求草稿:

POST http://127.0.0.1:8088/portal/draft-request
Authorization: Bearer $AUTOMATION_API_TOKEN
Content-Type: application/json

规则:
- 永远不要问我要网站密码。
- 永远不要问我要 2FA 验证码。
- 永远不要直接调 Selenium 或 noVNC。
- 如果 API 返回 needs_login,告诉我通过 noVNC 登录。
- 除非我明确批准保存草稿,否则一律用 dry_run=true。

分工就清楚了:AI 负责语言和结构,Wrapper 负责浏览器动作,已登录的浏览器负责跟网站打交道。边界一清楚,整个流程就好理解、好排查了。

这里最关键的是 Agent 的"刹车机制"。如果 Wrapper 返回 needs_login,Agent 不应该试图通过问你要凭证来自己解决登录问题——它应该通知你、暂停任务,等你在 noVNC 上登完再继续。

第八步:搬到服务器上

开发者通过私有隧道查看运行在小型远程服务器上的浏览器容器
做 24 小时工作流时,浏览器跑在服务器上;你的笔记本只用来登录和调试。

本地是最稳妥的第一步。真要 24/7 跑,就把同一套 Docker Compose 搬到一台小 VPS 上。

本地服务器
直接访问 127.0.0.1:7900通过 SSH 隧道访问 noVNC
Wrapper API 绑在 localhost通过 HTTPS / VPN / Tailscale 暴露 Wrapper
笔记本得一直开着VPS 保持浏览器会话常驻

在远程服务器上,千万别把 noVNC 直接暴露到公网。需要登录或调试时,用隧道连过去就行:

bash
ssh -L 7900:127.0.0.1:7900 user@your-server

然后在自己电脑上打开:

http://127.0.0.1:7900/?autoconnect=1&resize=scale&password=change-this-password

如果你的 AI 平台需要从服务器外部调 Wrapper,那就只暴露 Wrapper 一个端口,用 HTTPS + 强 bearer token 保护起来,再配合 IP 白名单或私有网络。44447900 继续藏着别动。

会话持久化怎么办

入门版的做法是:只要 Selenium 容器不挂,浏览器会话就一直活着。对第一个工作流来说,这通常够用了。

如果你希望容器重启后登录态还在,可以后面再挂一个 Chrome profile 卷,但要小心测试。有些网站会让 cookie 过期,会要求重新做 2FA,会在 IP 变了之后直接作废会话。Chrome profile 在浏览器非正常退出时还可能留下锁文件,重启时挺烦的。

建议先守住这条简单规则:

容器保持运行。
会话过期就返回 needs_login。
让人通过 noVNC 重新登录。

几条值得守住的护栏

风险护栏
AI 看到登录凭证人通过 noVNC 登录,Wrapper 永远不接收密码
AI 点错地方Wrapper 只暴露白名单动作,不给完整浏览器控制
网站改版用明确的选择器、截图和 needs_review 响应
自动化提交了脏数据默认 dry_run,先存草稿再最终提交
远程控制端点暴露在公网WebDriver 和 noVNC 只绑 localhost 或私有网络

什么时候不该用这套

如果网站有正经 API,那就用 API,别折腾这套。API 更稳、更好监控,也不会因为页面一改就挂。

如果网站明确禁止自动化,或者用 CAPTCHA 画了一条清晰的反自动化红线,又或者某个操作点错一次代价很高,也别硬上。这种情况下,让 AI 准备数据就好,浏览器里的关键动作还是得人来做。

真正有用的是那条边界

这套方案的重点不是 Selenium——Selenium 随时可以换掉。真正有用的是那条边界。

AI 不该拥有你的登录。
AI 不该拥有你的整个浏览器。
AI 可以拥有结构化的草稿数据。
你的 API 决定哪些浏览器动作可以放行。

这是自动化无 API 网站时一种更稳的做法。你照样能拿到 AI 起草的内容、24/7 的服务器端执行、看得见的浏览器调试——但不必把密码交给 Agent,也不必让它操作你的个人电脑。

许可

Article text © 2026 Mark Huang. Licensed under Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) unless otherwise noted. 文章文本可在非商业场景下分享或翻译,但需标注原文 URL。商业使用需事先取得书面许可,并清楚引用原始来源。

代码片段、截图、第三方素材和网站源码可能适用单独条款。

建议署名: Based on "不给 AI 登录信息的智能体浏览器自动化" by Mark Huang, originally published at https://markhuang.ai/zh/blog/ai-browser-automation-without-sharing-login.

订阅更新

Go、AI/LLM 和分布式系统的技术文章,绝不滥发。

评论

正在加载评论...