浏览器 Agent 卡在登录和验证码?用“人工接管 + 断点续跑”保住任务现场
浏览器 Agent 卡在登录和验证码?用“人工接管 + 断点续跑”保住任务现场
Agent 已经填完十几项内容,文件也上传好了。就在最后提交前,页面突然跳回登录界面。
你重新登录,Agent 却像失忆一样从第一步开始重跑。更糟的是,它可能再次上传文件、重复发布内容,甚至把刚才已经生效的操作再提交一遍。
很多浏览器 Agent Demo 看起来很聪明:会打开网页、填写表单、点击按钮。但真正投入使用后,最容易让系统崩掉的,往往不是按钮没找到,而是任务进行到一半,网站突然要求“必须由本人处理”。
例如:
- 登录状态过期;
- 出现图形验证码、滑块或人机验证;
- 要求输入短信验证码;
- 需要扫码、App 授权或支付确认;
- 敏感操作前弹出二次确认。
这类问题不应该通过“让 Agent 多试几次”解决。可靠的做法是:识别阻塞、安全暂停、保存现场、交给人工处理,然后从原任务状态继续。
一个成熟的浏览器 Agent,不是遇到所有页面都敢点,而是知道什么时候该停下来,把控制权还给人。
本文讨论的是合规的 Human-in-the-loop(人在回路)机制,不提供自动破解验证码、规避二次验证或绕过网站风控的方法。
---
一、先别堆异常处理,把任务画成状态机
不少浏览器自动化项目一开始只有两种状态:
成功 / 失败
遇到登录过期就抛异常,遇到验证码就重试,连续失败后再把整个任务重新启动。
问题在于,登录过期并不代表任务失败。它更像开车经过收费站:车没坏,路线也没错,只是有一个节点需要人来确认身份。
因此,更合理的状态机应该是:
RUNNING
↓
BLOCKED_DETECTED
↓
PAUSING
↓
WAITING_FOR_HUMAN
↓
HUMAN_COMPLETED
↓
VALIDATING
↓
RESUMING
↓
RUNNING / FAILED
其中最重要的不是 WAITING_FOR_HUMAN,而是前后的两个阶段:
- 暂停前,是否完整保存了任务现场;
- 恢复前,是否验证了页面和业务状态。
如果只是暂停浏览器,却没有保存“做到哪一步”,人工回来后 Agent 仍然不知道该从哪里继续。
三类常见人工阻塞
#### 1. 登录过期
常见信号包括:
- 页面跳转到包含
login、signin或auth的地址; - 出现“会话已失效”“请重新登录”等提示;
- 业务接口返回
401; - 页面原本存在的业务内容消失,转而出现登录表单。
需要注意,403 不一定代表登录过期,也可能是权限不足、访问策略限制或风控拦截,因此不能单独作为判断依据。
#### 2. 验证码或人机验证
包括图形验证码、滑块、行为验证、第三方人机确认组件等。
系统应识别并暂停,而不是尝试自动破解。频繁点击、刷新或重复提交,反而可能增加验证难度,甚至触发账号限制。
#### 3. 二次确认
例如:
- 短信验证码;
- 扫码确认;
- 在手机 App 中授权;
- 发布、删除前的敏感操作弹窗;
- 支付确认。
这里必须区分两件事:用户完成确认,不等于业务动作已经完成。
用户扫码后,订单可能已经支付,也可能只是完成授权。如果 Agent 不检查结果,直接再次点击“支付”,就可能造成重复操作。
---
二、阻塞检测要靠多信号,不能只盯一个选择器
最小版本可以从 URL、页面文字和特定元素开始判断。
async function detectHumanBlocker(page) {
const url = page.url();
const loginExpired =
/login|signin|auth/i.test(url) ||
(await page.getByText(
/登录已过期|会话失效|请重新登录/
).count()) > 0;
const captcha =
(await page.locator(
'iframe[src="captcha"], [class="captcha"], [id*="captcha"]'
).count()) > 0;
const secondFactor =
(await page.getByText(
/短信验证码|扫码确认|二次验证|在手机上确认/
).count()) > 0;
if (loginExpired) return { type: "login_expired" };
if (captcha) return { type: "captcha" };
if (secondFactor) return { type: "second_factor" };
return null;
}
这段代码适合 Demo,但不能直接被视为生产方案。
网站改一次类名,[class*="captcha"] 就可能失效;某篇文章正文里出现“短信验证码”几个字,也可能造成误判。
生产环境应综合以下信号:
- 当前 URL 和跳转历史;
- 页面标题、提示文字和登录表单;
- DOM 元素及其可见性;
- 接口响应状态;
- 页面关键业务区域是否消失;
- 最近一次点击后发生了什么变化;
- 多个信号的组合评分。
例如,只有“URL 进入登录页”“业务区域消失”“出现密码输入框”同时成立时,才将其判断为登录过期。
阻塞检测可以借助模型理解页面,但最终是否暂停,最好由可审计的确定性规则决定。截图建议 1:Agent 正常填写后台表单的页面。 截图建议 2:提交前跳转到登录页,并标注“检测到会话失效”。 截图建议 3:出现验证码后,Agent 自动停止点击的界面。
所有截图都应隐去账号、手机号、订单号、Cookie、二维码和页面中的个人信息。
---
三、暂停不是截张图,而是完整保存现场
假设 Agent 正在后台发布内容,已经完成:
1. 进入发布页面;
2. 填写标题和正文;
3. 上传封面及附件;
4. 设置分类;
5. 准备点击发布。
此时登录失效。如果系统只保存一张截图,恢复时仍然不知道:
- 草稿是否已经保存;
- 上传是否完成;
- 当前浏览器里有几个标签页;
- 下一步原计划做什么;
- “发布”按钮是否已经被点击过。
因此,暂停时至少要保存四类信息:
- 任务上下文:目标、已完成步骤和下一步计划;
- 浏览器上下文:URL、活动标签页、会话引用;
- 阻塞上下文:阻塞类型、发现时间和页面证据;
- 交接上下文:用户需要做什么、完成后如何通知系统。
可以用下面的数据结构保存 Checkpoint:
type Checkpoint = {
taskId: string;
status: "waiting_for_human";
blocker: {
type: "login_expired" | "captcha" | "second_factor";
detectedAt: string;
message: string;
};
browser: {
pageUrl: string;
activeTab: number;
sessionRef: string;
};
progress: {
completedSteps: string[];
nextStep: number;
nextAction: {
name: string;
risk: "low" | "reversible" | "irreversible";
};
};
expectedState: {
accountHint?: string;
pageType: string;
requiredElements: string[];
};
handoff: {
instruction: string;
screenshotRef?: string;
expiresAt: string;
};
};
一个实际保存的交接对象可能是:
{
"taskId": "publish-20250308-001",
"status": "waiting_for_human",
"blocker": {
"type": "login_expired",
"detectedAt": "2025-03-08T10:30:00+08:00",
"message": "登录状态已失效,请重新登录"
},
"browser": {
"pageUrl": "https://example.com/editor",
"activeTab": 2,
"sessionRef": "encrypted-session-reference"
},
"progress": {
"completedSteps": [
"填写标题与正文",
"上传封面",
"设置分类"
],
"nextStep": 4,
"nextAction": {
"name": "检查草稿并发布",
"risk": "irreversible"
}
},
"expectedState": {
"accountHint": "内容运营账号",
"pageType": "content_editor",
"requiredElements": ["标题输入框", "正文编辑区", "发布按钮"]
},
"handoff": {
"instruction": "请在当前浏览器窗口重新登录,完成后点击“继续任务”",
"screenshotRef": "artifacts/task-001/blocker.png",
"expiresAt": "2025-03-08T10:45:00+08:00"
}
}
这里的时间和任务编号仅用于展示数据格式,生产环境应根据实际任务生成。
暂停与通知逻辑
async function pauseForHuman(page, task, blocker) {
await task.lock();
const checkpoint = await buildCheckpoint(page, task, blocker);
await saveCheckpoint(checkpoint);
await notifyUser({
taskId: checkpoint.taskId,
title: "浏览器任务需要人工处理",
message: checkpoint.handoff.instruction,
screenshotRef: checkpoint.handoff.screenshotRef,
action: {
label: "继续任务",
url: /tasks/${checkpoint.taskId}/resume
}
});
return {
status: "waiting_for_human",
taskId: checkpoint.taskId
};
}
task.lock() 很重要。它用于阻止其他 Worker 接手同一个任务,否则可能出现一个执行器在等待人工,另一个执行器却仍在点击页面。
截图建议 4:人工接管通知,展示任务摘要、阻塞原因和“继续任务”按钮。
交接数据的安全边界
无论是日志、截图还是发送给模型的上下文,都不要包含:
- 明文密码;
- 短信验证码;
- 完整 Cookie;
- 可直接使用的登录令牌;
- 完整银行卡或支付信息;
- 与当前任务无关的账户数据。
浏览器会话应该保存为受控的加密引用,而不是把 Cookie 原样写进 Checkpoint。
截图也需要脱敏,并设置保存期限。任务结束或审计期届满后,应自动删除不再需要的截图和页面快照。
---
四、人工完成后,Agent 不能立刻继续点
现在,用户已经在原浏览器窗口完成登录,并点击了“继续任务”。
最危险的实现是:
await page.click("text=发布");
因为暂停期间可能发生很多变化:
- 登录后被跳转到了首页;
- 用户登录了另一个账号;
- 草稿内容没有保留;
- 验证完成后,原动作已自动提交;
- 用户手动点击了发布;
- 页面被切换到了其他标签页。
恢复前必须依次回答五个问题:
1. 阻塞是否真的已经解除?
2. 当前账号是否正确?
3. 页面是否仍在预期业务节点?
4. 暂停前的操作是否已经生效?
5. 下一步是否属于不可逆操作?
async function resumeTask(page, checkpoint) {
const stillBlocked = await detectHumanBlocker(page);
if (stillBlocked) {
return {
status: "waiting_for_human",
reason: "阻塞状态仍未解除"
};
}
const currentState = await inspectBusinessState(page);
if (!isExpectedAccount(currentState, checkpoint.expectedState)) {
return {
status: "needs_review",
reason: "当前登录账号与任务要求不一致"
};
}
if (!isCompatible(currentState, checkpoint.expectedState)) {
return {
status: "needs_review",
reason: "页面状态与暂停前预期不一致"
};
}
const previousActionCompleted =
await verifyPreviousAction(page, checkpoint);
if (previousActionCompleted) {
checkpoint.progress.nextStep += 1;
await saveCheckpoint(checkpoint);
}
if (
checkpoint.progress.nextAction.risk === "irreversible"
) {
return {
status: "waiting_for_confirmation",
reason: "即将执行提交、支付、删除或发布操作"
};
}
return executeFromCheckpoint(page, checkpoint);
}
让恢复操作具备幂等性
所谓幂等,可以简单理解为:同一步骤即使被重复触发,也不会产生第二份业务结果。
async function executeStepIdempotently(task, step) {
const operationKey = ${task.taskId}:${step.id};
const existing = await operationLog.find(operationKey);
if (existing?.status === "completed") {
return {
status: "skipped",
reason: "该步骤已经完成"
};
}
await operationLog.createIfAbsent({
operationKey,
status: "running"
});
const alreadyEffective = await step.verify();
if (alreadyEffective) {
await operationLog.markCompleted(operationKey);
return {
status: "completed",
source: "verified_existing_result"
};
}
const result = await step.execute();
await step.verifyOrThrow(result);
await operationLog.markCompleted(operationKey);
return {
status: "completed",
source: "new_execution"
};
}
关键原则是:先查结果,再决定是否重做。
---
五、三种阻塞,恢复方式并不相同
场景一:后台发布前登录过期
这是贯穿全文的案例。
Agent 已经填写内容并上传素材,点击发布前进入登录页。系统暂停并保留原浏览器会话,用户重新登录后点击继续。
此时 Agent 应该:
1. 返回暂停前的编辑页面;
2. 检查当前账号;
3. 检查标题、正文和素材是否仍然存在;
4. 查询草稿或发布记录;
5. 确认尚未发布;
6. 在再次获得人工确认后执行发布。
如果内容丢失,应进入人工复核,而不是自动从头填写。
场景二:企业系统要求短信验证
Agent 在企业后台提交申请时遇到短信验证,应暂停并提示用户在原页面完成输入。
用户处理后,系统需要等待页面稳定,再判断申请是否已经自动提交。如果已经出现申请编号或成功状态,就不能再次点击提交。
场景三:支付前二次确认
支付场景的自动化边界必须更严格。
Agent 可以准备订单、核对金额并停在确认页面,但应把支付确认交给用户。用户完成后,Agent只负责:
- 查询订单状态;
- 核验支付结果;
- 保存业务凭证引用;
- 在状态不明确时进入人工复核。
它不应自动获取短信验证码,也不应替用户确认支付。
---
六、完整流程图:三个安全出口不能自动放行
flowchart TD
A[Agent 正常执行] --> B{检测到人工阻塞?}
B -- 否 --> A
B -- 是 --> C[锁定任务并保存 Checkpoint]
C --> D[生成交接摘要与脱敏截图]
D --> E[通知用户在原会话中处理]
E --> F{是否在期限内完成?}
F -- 否 --> G[安全退出或取消任务]
F -- 是 --> H[恢复校验]
H --> I{页面状态是否一致?}
I -- 否 --> J[进入人工复核]
I -- 是 --> K{上一步是否已经生效?}
K -- 是 --> L[跳过已完成步骤]
K -- 否 --> M[准备执行下一步]
L --> N{下一步是否不可逆?}
M --> N
N -- 是 --> O[再次请求人工确认]
N -- 否 --> P[从 Checkpoint 继续执行]
O --> P
P --> Q[记录结果并更新任务状态]
这套流程有三个明确的安全出口:
- 人工处理超时:安全退出,不能无限占用浏览器和任务锁;
- 页面状态不一致:进入复核,不能猜测页面发生了什么;
- 即将执行不可逆动作:再次确认,不能因为“恢复任务”而默认获得操作授权。
还可以增加一张前后对比图:
错误方案:
遇到阻塞 → 重启 Agent → 重复填写 → 可能重复提交
正确方案:
遇到阻塞 → 保存现场 → 人工处理 → 校验状态 → 断点续跑
---
七、从 Demo 到长期运行,还缺这份工程清单
要让这套机制长期运行,建议逐项检查:
- 每个任务生成唯一
task_id; - 每个关键步骤生成唯一操作标识;
- 暂停、恢复和通知接口必须幂等;
- 为人工处理设置可配置的超时时间;
- 统一分类登录失效、验证码和二次确认;
- 单独记录多标签页、弹窗及跨域登录的焦点状态;
- 恢复前检查账号、页面、业务结果和下一步风险;
- 对提交、发布、支付、删除增加人工确认;
- 恢复失败后进入复核,而不是无限重试;
- 日志、截图和页面快照均进行脱敏;
- 不把密码、验证码或完整 Cookie 发送给大模型;
- 遵守目标网站的服务条款和自动化政策。
最小可用架构可以保持简单:
浏览器执行器
↓
阻塞检测器
↓
任务状态存储
↓
人工接管通知
↓
用户在原会话中处理
↓
恢复校验器
↓
断点续跑执行器
如果暂时没有真实测试数据,不要用主观印象代替指标。可以先建立测试表格:
| 场景 | 是否正确暂停 | 是否保留现场 | 是否成功恢复 | 是否重复执行 | |---|---:|---:|---:|---:| | 登录过期 | 是/否 | 是/否 | 是/否 | 是/否 | | 图形验证码 | 是/否 | 是/否 | 是/否 | 是/否 | | 短信验证 | 是/否 | 是/否 | 是/否 | 是/否 | | 页面被用户切换 | 是/否 | 是/否 | 是/否 | 是/否 | | 用户登录错误账号 | 是/否 | 是/否 | 是/否 | 是/否 | | 原操作已自动提交 | 是/否 | 是/否 | 是/否 | 是/否 |有条件时,再持续记录阻塞识别准确率、恢复成功率、重复提交率、人工处理耗时和超时率。没有实测,就不要填写看似漂亮的数字。
---
八、模型负责理解,执行器负责确定性动作
阻塞检测的底层规则应尽量确定性,但大模型可以辅助处理更难结构化的部分:
- 将暂停前的执行历史压缩成交接摘要;
- 根据脱敏后的页面信息说明用户需要做什么;
- 对比暂停前后的页面状态;
- 生成候选恢复计划;
- 识别提交、支付、删除、发布等高风险动作。
如果准备把这套机制接入自己的浏览器 Agent,可以通过 api.884819.xyz 调用模型,让模型负责“状态理解和恢复规划”,浏览器执行器负责确定性的点击、查询与结果校验。
curl https://api.884819.xyz/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "YOUR_MODEL",
"messages": [
{
"role": "system",
"content": "你是浏览器 Agent 的恢复规划器。不得尝试绕过验证码或二次验证,不得替用户执行支付确认。"
},
{
"role": "user",
"content": "请根据暂停前的任务状态与人工处理后的页面状态,生成安全的恢复步骤,并标记不可逆操作。"
}
]
}'
实际接口参数应以平台文档为准。API Key 应保存在服务端环境变量中,不要写进浏览器脚本,也不要把密码、短信验证码或完整 Cookie 发送给模型。
8848AI 使用用户名和密码即可注册,不需要邮箱验证;平台内置 AI 对话功能,注册后可以直接使用。国产模型如 Deepseek、千问等完全免费,没有月租和订阅,其他服务按量付费。
访问地址:api.884819.xyz
---
最后:检查你的 Agent 是否真的会“停”
现在可以用下面五个问题检查自己的系统:
1. 遇到登录、验证码和二次确认时,它会停止点击吗?
2. 暂停时,它保存的是完整任务状态,还是只有截图?
3. 用户处理后,它会检查账号和页面是否正确吗?
4. 它会先验证上一步结果,再决定是否重做吗?
5. 面对发布、支付和删除,它会重新请求确认吗?
如果其中任何一个答案是否定的,那么你的 Agent 可能“能跑”,但还谈不上可靠。
真正成熟的浏览器 Agent,不是永远不需要人,而是知道什么时候必须停、应该把什么交给人,以及人处理后怎样安全地继续。
但这里还有一个最容易出事故的问题:人工处理完以后,Agent 怎么确定“上一步到底有没有成功”?
下一篇,我们会专门拆解 《浏览器 Agent 最怕重复提交:用幂等键、结果校验和操作日志实现安全重试》,讲清楚页面刷新、网络超时和任务恢复后,如何避免多下单、多发布和多支付。
本文由8848AI原创,转载请注明出处。关注8848AI,带你从零开始学AI。#浏览器Agent #AI教程 #自动化测试 #Playwright #人工智能 #8848AI #Agent开发 #人在回路