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

太长不看版
如果你只是偶尔要让 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 返回截图,只要动作有风险,最终点"提交"的还是人。
你要搭什么东西
教程里用的目标网站,是一个有"新建请求"表单的私有供应商门户。表单包含 subject、details、priority,以及一个"保存草稿"按钮。你实际的网站肯定不一样,但架构是通用的。
| 组件 | 干什么 |
|---|---|
| 定时器 / 队列 | 每天定时或来任务时启动自动化 |
| 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 框架
第一步:起一个看得见的浏览器容器
先建个目录:
mkdir browser-automation-wrapper
cd browser-automation-wrapper新建 docker-compose.yml:
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"拉起来:
docker compose up -d打开实时浏览器:
http://127.0.0.1:7900/?autoconnect=1&resize=scale&password=change-this-password用这个浏览器自己登录目标网站。不要让 AI 帮你登,更不要把密码粘到 AI 对话框里。从这一刻起,浏览器会话就活在容器里了。
第二步:把 WebDriver 藏好
WebDriver 权限非常大——谁能访问它,谁就能完全控制浏览器。这就是为什么 compose 文件里要把 4444 和 7900 都绑到 127.0.0.1。

| 要做 | 别做 |
|---|---|
| Selenium 绑定到 localhost | 把 4444 直接暴露在公网 |
| 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:
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:
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:
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:
AUTOMATION_API_TOKEN=replace-this-with-a-long-random-token带上 Wrapper 一起重启:
docker compose up -d --build第六步:先别让 AI 上场,自己测一遍
先把 token 加载到终端:
set -a
. ./.env
set +a调一下 Wrapper:
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/7 跑,就把同一套 Docker Compose 搬到一台小 VPS 上。
| 本地 | 服务器 |
|---|---|
直接访问 127.0.0.1:7900 | 通过 SSH 隧道访问 noVNC |
| Wrapper API 绑在 localhost | 通过 HTTPS / VPN / Tailscale 暴露 Wrapper |
| 笔记本得一直开着 | VPS 保持浏览器会话常驻 |
在远程服务器上,千万别把 noVNC 直接暴露到公网。需要登录或调试时,用隧道连过去就行:
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 白名单或私有网络。4444 和 7900 继续藏着别动。
会话持久化怎么办
入门版的做法是:只要 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.
相关文章

5 分钟试用 Dense-Mem 托管演示
一篇快速教程:使用托管的 Dense-Mem 测试实例,把 Claude Code 和 Codex 接到同一份临时记忆,并观察共享上下文如何让 AI 更聪明地工作。
阅读文章
Dense-Mem 快速开始:让 Claude Code 和 Codex 使用同一份记忆
一篇面向初学者的教程:启动本地 Dense-Mem 服务器,创建第一把 memory key,并把 Claude Code 和 Codex 接到同一个共享 AI 记忆大脑。
阅读文章
用 Traefik 在 Vultr 上安全部署 Dense-Mem
一篇非技术读者也能跟上的 walkthrough:在 Vultr 云服务器上启动 Dense-Mem,配置 Traefik、HTTPS、私有控制台访问,以及给个人、家庭或工作 AI 工具使用的共享记忆。
阅读文章订阅更新
Go、AI/LLM 和分布式系统的技术文章,绝不滥发。
评论
正在加载评论...