Java 8老系统Browser Agent实战:三层拦截把AI操作后台变成可审计流程
源码路径:
chapter08-browser-test-agent/src/main/java/com/ynzz/lab/chapter08/agent/
0. 先问三个问题,再决定要不要上 Browser Agent
Browser Agent 做出一个能自动打开浏览器、点点填填的 Demo 并不难,网上随便一搜都是这类炫酷演示。但企业真正落地时,工程师们问的从来不是"AI 能不能操作浏览器",而是:
- AI 只能操作哪个环境? 生产?测试?还是 localhost?
- AI 能点"删除"吗? 能点"审批"吗?能点"支付"吗?
- 操作之前要不要人确认? 怎么确认?谁来确认?
这三个问题回答不清楚,Browser Agent 上线第一天就会出事。
本讲我们就围绕这三个问题来展开,从安全策略设计出发,把 Browser Agent 的核心逻辑讲透,再配合 Demo 场景让各位真正理解代码背后的决策路径。
前置知识: 本讲依赖第7讲 AI 工单助手的多 Agent 协作框架,以及第3讲 SQL Agent 的参数提取逻辑。
extractOrderId的设计与 SQL Agent 中的参数提取思路一脉相承,建议先回顾。
1. 核心问题:AI 操作后台的三大风险
在动手写代码之前,我们先把风险分清楚:
| 风险类型 | 典型场景 | 后果 |
|---|---|---|
| 环境风险 | AI 直接操作生产数据库/后台 | 数据污染、线上故障 |
| 动作风险 | AI 执行了"删除订单"、"审批通过"、"确认支付" | 不可逆的业务操作 |
| 确认风险 | 没人知道 AI 今天点了什么、什么时候点的 | 审计黑洞 |
传统做法靠人来挡:测试环境人工测,生产操作审批流。但 AI 来了之后,这套机制被绕过去了------你给 AI 一个任务,它自己去点,如果没有人明确告诉它"哪里能点、什么不能点",它就会按照自己的理解去操作。
Browser Agent 的设计思路就是:把安全策略做成代码逻辑,让每一次 AI 操作都经过显式检查。
2. 安全策略:三层拦截决策树
这是整讲的核心。请先把这个决策树记在心里,再去看代码。
yaml
用户发起 BrowserTestRequest
│
▼
┌───────────────────────────────────────┐
│ 第一层:请求级拦截(BrowserTestRequest)│
│ rejectReason(request) │
└───────────────────────────────────────┘
│
[有原因?]
Yes ↓ No
返 │
回 ▼
rejected ┌───────────────────────────────────────┐
plan │ 第二层:URL 白名单检查 │
│ targetUrl 必须以 http://localhost │
│ 或 http://127.0.0.1 开头 │
└───────────────────────────────────────┘
│
[有原因?]
Yes ↓ No
返 │
回 ▼
plan ┌───────────────────────────────────────┐
│ 第三层:危险任务关键词检查 │
│ task 文本含 "删除" / "审批" / "支付" │
└───────────────────────────────────────┘
│
[有原因?]
Yes ↓ No
返 │
回 ▼
plan 生成 4 步执行计划
(OPEN → TYPE → CLICK → SCREENSHOT)
│
▼
┌─────────────────┐
│ 等待 confirmedBy │ ← 执行时再判断
└─────────────────┘
│
[confirmedBy 为空?]
Yes ↓ No
FAILED │
/CONFIRMA ▼
TION_REQUIRED 执行每个 Step
│
▼
┌────────────────────────┐
│ 第四层:动作级兜底拦截 │
│ rejectReason(step) │
│ 拦截 DELETE/PAY/APPROVE │
└────────────────────────┘
│
[有原因?]
Yes ↓ No
FAILED PASSED
/HIGH_RISK
2.1 三层拦截代码
请求级拦截(第一~三层):
java
// BrowserSafetyPolicy.java - 请求级拦截
public String rejectReason(BrowserTestRequest request) {
// 第一层:环境校验
if (!"test".equals(request.getEnvironment())) {
return "ONLY_TEST_ENVIRONMENT_ALLOWED"; // 拦截所有非测试环境
}
// 第二层:URL 白名单(仅限 localhost)
String targetUrl = request.getTargetUrl();
boolean isLocalhost =
targetUrl.startsWith("http://localhost") ||
targetUrl.startsWith("http://127.0.0.1");
if (!isLocalhost) {
return "TARGET_URL_NOT_IN_TEST_ALLOWLIST"; // 拦截所有外部 URL
}
// 第三层:危险任务关键词
String task = request.getTask();
if (task.contains("删除") || task.contains("审批") || task.contains("支付")) {
return "HIGH_RISK_ACTION_NOT_ALLOWED"; // 拦截高危操作
}
return null; // 三层全过,返回 null = 放行
}
动作级兜底拦截(第四层):
java
// BrowserSafetyPolicy.java - 单步兜底
public String rejectReason(BrowserStep step) {
String action = step.getAction().name();
if ("DELETE".equals(action) || "PAY".equals(action) || "APPROVE".equals(action)) {
return "HIGH_RISK_STEP_NOT_ALLOWED";
}
return null;
}
2.2 为什么需要两层拦截?
这里有个设计细节值得单独说一下:请求级拦截和动作级拦截不是重复的,它们各自承担不同的职责。
-
请求级拦截 :在计划生成阶段执行,目的是提前阻止危险请求进入执行队列 。如果一个请求被请求级拦截,
createPlan直接返回 rejected plan,根本不会生成任何 step。 -
动作级兜底 :在执行阶段逐个 step 执行,目的是兜住请求级过滤的漏网之鱼。比如某些危险操作可能不会在 task 文本中直接写"删除",但 LLM 在生成计划时把某个动作生成了 DELETE,此时动作级拦截就能把它拦住。
换句话说:请求级是"进门查身份",动作级是"出门再验一遍票"。 两层缺一不可------没有请求级,大量的危险请求会直接涌进来;没有动作级,LLM 生成的 step 可能绕过高危关键词检查。
3. 计划生成:从任务文本到可执行步骤
3.1 参数提取:extractOrderId
当用户说"搜索订单 O202606050001,并截图订单详情页"时,LLM 并不知道"O202606050001"是一个需要提取的参数。BrowserPlanService 需要先把这个参数解析出来:
java
// BrowserPlanService.java
private String extractOrderId(String task) {
// 从 task 文本中查找以 "O" 开头、接续字母数字的订单号
// 格式约定:以 "O2026" 开头,后面跟数字
Pattern pattern = Pattern.compile("O[0-9]{12}");
Matcher matcher = pattern.matcher(task);
if (matcher.find()) {
return matcher.group(); // 返回 "O202606050001"
}
return null;
}
这段逻辑的假设是:业务系统中的订单号有固定格式(以"O"开头,年月日+序号,共14位),通过正则提取比让 LLM 去"理解"更可靠。在企业场景中,结构化提取参数永远是第一选择,而不是让 LLM 自由发挥。
对比第3讲 SQL Agent 的参数提取,两者的思路是一致的:先用规则/正则提取确定性参数,再把剩余的模糊意图交给 LLM 处理。
3.2 四步计划生成
参数提取完成之后,createPlan 生成一个包含 4 个 step 的执行计划:
| Step # | Action | 输入 | 输出/目标 |
|---|---|---|---|
| 1 | OPEN |
targetUrl |
打开浏览器,导航到目标页面 |
| 2 | TYPE |
orderIdInput, orderId |
在订单号输入框填入订单号 |
| 3 | CLICK |
searchButton |
点击搜索按钮 |
| 4 | SCREENSHOT |
orderDetailPanel |
对订单详情区域截图,保存为 artifacts/screenshots/order-detail.png |
这里的设计遵循了原子化原则:每个 step 只做一件事。OPEN 负责导航,TYPE 负责填值,CLICK 负责触发,SCREENSHOT 负责记录证据。拆得越细,安全检查点就越多,出问题时越容易定位。
4. 四个 Demo 场景:输入→判断→输出
场景一:安全计划生成(完全通过)
输入:
json
{
"environment": "test",
"targetUrl": "http://localhost:8080/admin/orders",
"task": "搜索订单 O202606050001,并截图订单详情页"
}
拦截判断链路:
csharp
第一层 (environment) → null ✓ test 环境,放行
第二层 (URL) → null ✓ localhost,放行
第三层 (task 关键词) → null ✓ 不含删除/审批/支付,放行
extractOrderId → "O202606050001"
生成计划 → 4步计划
输出:
json
{
"status": "PENDING_CONFIRMATION",
"steps": [
{"action": "OPEN", "target": "http://localhost:8080/admin/orders"},
{"action": "TYPE", "inputRef": "orderIdInput", "value": "O202606050001"},
{"action": "CLICK", "ref": "searchButton"},
{"action": "SCREENSHOT", "element": "orderDetailPanel", "path": "artifacts/screenshots/order-detail.png"}
]
}
状态是 PENDING_CONFIRMATION,意味着计划已生成,但还需要人来确认才能执行。
场景二:确认后执行(完全通过)
输入: 场景一的计划 + confirmedBy = "qa-user"
执行判断链路:
vbscript
plan.rejected → false ✓ 不需要拦截
confirmedBy → "qa-user" ✓ 非空,有授权人
Step 1 OPEN → null ✓ 通过
Step 2 TYPE → null ✓ 通过
Step 3 CLICK → null ✓ 通过
Step 4 SCREENSHOT → null ✓ 通过,ScreenshotRecorder 生成截图
输出:
json
{
"status": "PASSED",
"confirmedBy": "qa-user",
"screenshotPath": "artifacts/screenshots/plan-001-orderDetailPanel.png",
"executedSteps": 4
}
截图路径由系统自动生成,plan-001 表示这是 plan 编号 001 的截图。
场景三:生产环境拦截
输入:
json
{
"environment": "prod",
"targetUrl": "https://admin.example.com/orders",
"task": "搜索订单 O202606050001,并截图订单详情页"
}
拦截判断链路:
arduino
第一层 (environment) → "ONLY_TEST_ENVIRONMENT_ALLOWED" ✗ 第一层就拦住了
输出:
json
{
"status": "REJECTED",
"reason": "ONLY_TEST_ENVIRONMENT_ALLOWED",
"rejectedAt": "layer-1-request"
}
关键点:第一层就返回了,不会再检查后面的逻辑。 这是防御深度(defense in depth)的体现------越前面的关卡越严格,越早拦截越好。
场景四:危险任务拦截
输入:
json
{
"environment": "test",
"targetUrl": "http://localhost:8080/admin/orders",
"task": "删除订单 O202606050001"
}
拦截判断链路:
csharp
第一层 (environment) → null ✓ test 环境,放行
第二层 (URL) → null ✓ localhost,放行
第三层 (task 关键词) → "HIGH_RISK_ACTION_NOT_ALLOWED" ✗ 命中"删除"关键词
输出:
json
{
"status": "REJECTED",
"reason": "HIGH_RISK_ACTION_NOT_ALLOWED",
"rejectedAt": "layer-3-task-keyword",
"matchedKeyword": "删除"
}
注意:这个请求虽然环境是 test、URL 是 localhost,但仍然被第三层拦住了。测试环境不等于可以随便操作,危险动作在任何环境下都需要额外审批。
5. 执行引擎:BrowserRunService
计划生成之后,由 BrowserRunService 负责执行:
java
public BrowserRunResult run(BrowserPlan plan, String confirmedBy) {
// 1. 检查 plan 是否被拦截
if (plan.isRejected()) {
return BrowserRunResult.failed("PLAN_REJECTED", plan.getRejectReason());
}
// 2. 检查是否有人确认
if (confirmedBy == null || confirmedBy.isBlank()) {
return BrowserRunResult.failed("CONFIRMATION_REQUIRED",
"此操作需要人工确认,请提供 confirmedBy 参数");
}
// 3. 逐个执行 step,每步都做动作级安全检查
for (BrowserStep step : plan.getSteps()) {
String reason = safetyPolicy.rejectReason(step);
if (reason != null) {
return BrowserRunResult.failed("HIGH_RISK_STEP_NOT_ALLOWED", reason);
}
// 执行 step
executeStep(step);
// 4. 截图 step 特殊处理:记录证据
if (step.getAction() == Action.SCREENSHOT) {
ScreenshotRecorder.record(step.getPath());
}
}
return BrowserRunResult.passed(confirmedBy);
}
这里有一个设计值得注意:confirmedBy 的检查是在执行阶段做的,而不是在计划生成阶段。这意味着:即使 AI 生成了一个看似安全的计划,也必须有人来"认领"这个操作。这个设计把责任从"AI 的能力"转移到了"人的授权"。
6. 三种触发场景:代码链路差异
Browser Agent 在企业中可能有三种触发方式,每种方式的调用链路和适用场景不同:
场景 A:测试用例生成后自动触发
ini
LLM 生成测试用例(自然语言)
↓
用例评估系统判断:需要操作后台验证?
↓
构造 BrowserTestRequest(environment=test)
↓
BrowserPlanService.createPlan(request)
↓
BrowserRunService.run(plan, confirmedBy="auto-test-runner")
↓
截图 → 存入测试报告
这种场景适用于自动化回归测试:AI 生成用例 → 自动执行 → 自动截图存证。全程无人干预,适合高频执行的冒烟测试。
注意事项: confirmedBy 字段不能为空,此时传入一个系统级的虚拟用户名(如 auto-test-runner),并在日志中记录该操作由哪个测试用例触发,便于事后审计。
场景 B:人工发起(推荐初始阶段)
markdown
测试工程师 → 打开操作界面 → 填写任务描述
↓
系统返回待确认的 Plan(状态=PENDING_CONFIRMATION)
↓
测试工程师查看计划,确认操作安全
↓
输入 confirmedBy,提交执行
↓
执行并截图 → 人工复审截图
这是最安全的接入方式。Browser Agent 在人工确认模式下,本质上是一个"高级录屏工具"------人决定做什么,AI 来执行并记录。
场景 C:CI/CD 流水线触发
css
CI Pipeline → 调用 REST API
↓
构造 BrowserTestRequest
(带环境标识、目标 URL、任务描述、CI 触发者身份)
↓
BrowserPlanService + BrowserRunService
↓
结果写入 CI 日志,截图作为制品存档
↓
Pipeline 根据结果决定:通过 / 失败
这种场景适合 CI 阶段的 UI 自动化测试替代 。但要注意:CI 触发的 confirmedBy 通常是 CI 系统账号(如 jenkins-agent),必须有完整的操作日志和截图留存,否则出了事故无法回溯。
7. 截图的价值:测试证据的复盘意义
很多团队用 Browser Agent 只关注"AI 能不能点对",忽略了截图作为测试证据的复盘价值。
一个截图能告诉我们什么?
- 操作结果是否正确:AI 点完之后,页面显示的是不是预期内容?
- 页面是否有异常:有没有报错信息、加载失败、权限提示?
- AI 的理解是否正确:AI 认为它点到了"A按钮",但截图显示它点的是"B按钮"------这一步的偏差对后续步骤有什么影响?
- 回归测试的基线:每次截图都是一次快照,下次执行时可以对比,页面结构是否发生了变化?
在企业实践中,建议把截图路径纳入测试报告的结构化字段中,而不是简单存成文件:
json
{
"testCaseId": "TC-ORDER-001",
"executedAt": "2026-06-15T10:30:00Z",
"screenshotPath": "artifacts/screenshots/plan-001-orderDetailPanel.png",
"planStatus": "PASSED",
"confirmedBy": "qa-user",
"plan": { ... }
}
这样每次执行都有完整的证据链可查。
8. 企业避坑:五条血泪经验
坑一:不要把 Browser Agent 做成"通用网页操作工具"
Browser Agent 的能力是通用的,但企业的使用场景一定是受限的。如果不做好环境限制和操作限制,AI 迟早会访问到它不该访问的页面。限制比能力更重要。
坑二:localhost 白名单在小团队够用,大规模部署会变成瓶颈
startsWith("http://localhost") 这个白名单在本地开发时很方便,但如果你的测试环境有多个服务分布在不同机器上,这个白名单就不够用了。建议从一开始就设计一个可配置的 URL 白名单服务,而不是硬编码 localhost。
坑三:confirmedBy 检查不等于权限控制
confirmedBy 只是一个字符串,任何人都可以传 confirmedBy="admin"。需要结合 SSO/权限系统,验证 confirmedBy 对应的账号是否有执行该操作的权限,而不是仅靠一个字段名来挡事。
坑四:LLM 生成 step 的不确定性是最大的隐患
即使请求级拦截全部通过,LLM 在生成具体 step 时仍然可能生成危险动作(如把"查看详情"误解为"删除记录")。动作级兜底拦截是最后的防线,但不是银弹。 建议在高风险操作场景下,保留人工审核 step 列表的环节,而不是全自动执行。
坑五:截图不存储元数据,时间长了就是一堆废图
单独存截图而不记录"这是哪个 Plan、谁触发的、哪个环节出问题",三个月后这些截图就变成了无法解读的垃圾文件。截图必须和执行上下文一起存储,至少包含 Plan ID、触发者、执行时间、Plan 状态。
9. 从 Demo 到落地:五条工程路径
路径一:集成 Playwright,实现真实浏览器操作
Demo 阶段的截图和操作是抽象的,实际落地需要接入 Playwright(或 Selenium)来实现真实的浏览器控制:
scss
BrowserRunService.executeStep(step)
↓
PlaywrightClient.click(locator) // CLICK
PlaywrightClient.fill(locator, value) // TYPE
PlaywrightClient.screenshot(element) // SCREENSHOT
Playwright 的 locator 策略建议使用 ARIA role + name,而不是 XPath,这样对页面结构变化的鲁棒性更强。
路径二:CI 流水线集成,实现夜间自动化回归
把 Browser Agent 嵌入现有的 CI 流水线(推荐在集成测试阶段,而非单元测试阶段):
yaml
# .github/workflows/browser-test.yml
- name: Run Browser Agent Tests
run: |
curl -X POST http://browser-agent-api/run \
-H "Content-Type: application/json" \
-d '{
"environment": "test",
"targetUrl": "http://localhost:8080/admin/orders",
"task": "搜索订单 O202606050001,并截图订单详情页",
"confirmedBy": "ci-pipeline"
}'
artifacts:
- artifacts/screenshots/
截图作为 CI 制品存档,失败时自动附加到 PR 评论中。
路径三:构建测试用例库,覆盖核心业务流程
不要让 AI 每次都从零理解任务。建议预置一套测试用例库,每条用例包含:
- 用例 ID 和描述
- 标准化的 task 模板(支持参数化)
- 预期的截图特征(用于自动化比对)
- 执行频率(每日/每周/按需)
AI 的工作是填充参数并执行,而不是每次都重新设计操作流程。
路径四:失败告警体系,不是"跑完就行"
Browser Agent 执行失败后,截图是异步生成的,工程师可能不会主动去查看。建议:
- 失败即告警:Plan 状态为 FAILED 时,通过企业微信/钉钉/邮件通知负责人
- 告警内容包含截图:让负责人看一眼截图就能判断是 AI 操作失误还是应用本身的问题
- 告警分级:PLAN_REJECTED(配置问题,HIGH 优先级)/ HIGH_RISK_STEP_NOT_ALLOWED(高危操作拦截,HIGH)/ CONFIRMATION_REQUIRED(流程问题,MEDIUM)
路径五:操作重试与容错设计
网络波动、页面加载慢、元素定位失败都可能导致 step 执行失败。重试策略建议:
arduino
executeStep(step)
↓ 失败
重试 1(等待 2s)→ 执行
↓ 失败
重试 2(等待 5s)→ 执行
↓ 失败
标记 step 为 FAILED,记录错误截图,终止 plan
同时要注意:重试不等于重复执行同一个 step,如果是因为页面状态问题导致的失败,重复执行相同的操作可能产生累积效应(如重复提交表单)。建议在重试前先截一张图,确认页面状态后再决定下一步。
10. 本讲小结
Browser Agent 的核心价值不是"让 AI 帮你点网页",而是把 AI 操作后台的风险变成可配置、可审计、可拦截的确定性流程。掌握这个思路,你就能理解为什么安全策略要前置到计划生成阶段,为什么需要请求级和动作级两层拦截,为什么 confirmedBy 不是可选参数而是必填项。
下一讲我们将把视角从"测试"转向"生产",看看在非测试环境下,如何用受控的方式让 AI 真正参与业务决策------包括本讲中被拦截的那些高风险操作,在什么条件下才能被安全地解锁。
相关源码文件:
| 文件 | 职责 |
|---|---|
BrowserSafetyPolicy.java |
三层请求级拦截 + 单步动作级拦截 |
BrowserPlanService.java |
计划生成、参数提取、四步计划组装 |
BrowserRunService.java |
执行引擎、确认检查、截图记录 |
BrowserTestRequest.java |
请求数据结构 |
BrowserPlan.java |
计划数据结构(含 rejected 标记) |
BrowserStep.java |
单步操作数据 |
ScreenshotRecorder.java |
截图持久化 |
前置回顾:
- 第3讲
SQL Agent:参数提取的设计思路与本讲extractOrderId一脉相承 - 第7讲
AI 工单助手:多 Agent 协作框架是 Browser Agent 的基础设施