表单与多层弹窗排障:防止重复提交
本文对应浏览器工程的当前源码,说明表单定位、点击降级、对象释放异常和人工验证的处理方式。升级源码后需要重新构建并重启服务;旧进程不会自动获得这些行为。
支付产品申请等业务可参考支付宝接入指南。本章只讲浏览器操作,不重复支付接口实现。示例中的任务 ID、企业名称和域名均为占位信息。
1. 可见不等于能收到点击
背景表单和前景短信弹窗可能都有 button[type=submit]。两个按钮的 isVisible 都可能为真,但背景按钮被弹窗遮挡,不能接收真实点击。
使用选择器定位动作时,resolveActionTarget 会统计匹配数量,在前 20 个匹配中优先选择可见且中心点命中自身或子元素的控件。如果找不到这样的控件,保留首个可见匹配交给后续可操作性检查;全部隐藏时保留首个匹配,以兼容隐藏字段的显式操作。这不是“自动理解哪个提交按钮正确”,仍应使用当前弹窗范围内的精确选择器。
多个匹配时可查看回执中的 matched、chosenIndex、scanned 和 selectorNote。chosenIndex 从 0 开始,属于选择器匹配列表,不是页面快照中的交互索引。
推荐流程:
- 用
get_browser_state读取当前弹窗和控件。 - 根据最新索引操作,或使用能限定当前弹窗的选择器。
- 填写后重新读取状态,核对字段值和按钮文字。
- 提交后查看业务结果,再决定下一步。
页面变化后不要复用旧索引。自定义下拉框的 input 值为空不一定代表没有选择,应同时检查展示的企业名称或选项文本。
2. auto 点击不会穿透遮挡层
clickWithMode 的当前行为如下:
| 模式 | 行为 |
|---|---|
auto | 先尝试原生点击;普通失败后,未发现遮挡时才按配置尝试真实鼠标及 JS 降级 |
native | 只尝试原生点击,不降级 |
mouse | 显式用真实鼠标点击目标盒子中心,不提供原生可操作性检查的保证 |
js | 显式派发事件,可能绕过遮挡,但页面框架不一定接受 |
auto 原生点击失败后,若点击前已发现遮挡,或失败后再次检测到遮挡,则停止降级,返回 ELEMENT_OBSCURED。不再用 JS 点击背景提交按钮,也不把鼠标点击落到遮挡物上。
此时应该完成当前弹窗,或者重新定位当前弹窗中的控件。不要仅为了让错误消失而切到 mode=js。显式 mouse 和 js 仍保留给调用方使用,但不能用来证明业务已成功。
3. 对象释放异常不能靠再点一次解决
Object doesn't exist: request@…、Object doesn't exist: response@… 等异常可能来自浏览器事件处理,也可能表示对象已经失效。异常出现时,前面的点击可能已经提交。
当前代码在原生点击抛出这类异常后,不继续自动补发鼠标或 JS 点击,而是交给观察层判断。若观察到页面变化,响应可能包含 warning、actionError、changedByActionError;没有充分证据时则保留失败或不确定信息。无论哪种情况,都不能仅凭异常就重发创建、提交或下载操作。
| 场景 | 下一步 |
|---|---|
| 创建应用后报异常 | 查看应用列表或详情,确认是否已经存在 |
| 提交申请后报异常 | 查看审核状态或申请记录 |
| 点击后新页签出现 | 用 get_tabs 确认,再用 switch_tab 的 pageIndex 切换 |
| 页面正在导航 | 等页面稳定后先读取状态,确认动作是否已经生效 |
| 只读查询遇到对象释放 | 服务会对允许安全重试的只读命令进行有限重试;仍失败时检查页面状态 |
execute_js 默认不属于自动重试的只读命令,因为脚本可能有副作用。只有确认脚本纯读取时,才可以显式传 retryOnSpurious:true:
{
"id":"1001",
"method":"execute_js",
"params":{
"body":"() => ({url: location.href, text: document.body.innerText})",
"retryOnSpurious":true
}
}
不要给包含 .click()、表单提交、数据写入或网络变更请求的脚本加这个参数。changed=true 只表示页面变化,ok=true 也不能替代“申请成功”“密钥已保存”等业务结果。
4. Windows 中文与引号参数
通过 .cmd 传递中文、多行脚本或带引号的 CSS 选择器时,可能发生编码或转义丢失。回执中的 committed=true 表示输入动作完成,不代表传入内容一定与调用方原意一致。
复杂参数使用 UTF-8 JSON 文件,以下命令在浏览器项目根目录执行:
New-Item -ItemType Directory -Force tmp | Out-Null
@{
selector = '#appName'
text = '示例应用'
mode = 'type'
} | ConvertTo-Json | Set-Content tmp/app-name.json -Encoding utf8
client\dsb.cmd --port 10049 --id 1001 run input_text_by_selector --params '@tmp/app-name.json'
client\dsb.cmd --port 10049 --id 1001 run get_form_state
1001 应替换为实际任务 ID。涉及引号的选择器也放进文件,不要依赖多层 shell 转义。关键字段填写后要回读,但不要将验证码或私钥写进公共示例与日志。
5. 文件上传与人工验证
文件上传返回 filesLength=0 可能是框架已经消费文件并重置 input,不一定是失败。结合 consumed、页面预览及上传状态判断。图标上传常有裁剪弹窗,必须先确认裁剪,再继续主表单;不要直接点击被裁剪窗口遮挡的按钮。
联系人验证、产品申请验证和密钥保存验证可能是不同流程。每次使用对应流程的新验证码,不能把前一步成功的码再次使用。需要本人协助时,调用 request_human_input 并明确告知用户;等待期间保留页签,不刷新、不重复发码、不猜测验证结果。详细流程见人机协作。
密钥由本地授权流程生成时,私钥仅保留在本地或服务器;网页通常只需要应用公钥。保存后核对平台成功信息和公钥配对,不以短信验证通过代替配置保存成功。
6. 脱敏与状态记录
终端输出、trace JSON、页面文本、截图、临时参数文件是不同产物。某一层做了脱敏,不能推断其他层也安全。尤其是 data/<id>/ 原始留档中的页签地址和表单值,可能保留实际账号、AppID、验证码或带签名的链接。
公共文档使用“示例科技有限公司”、example.com、YOUR_APP_ID 等占位符。不复制真实手机号、证照、短信码、私钥、公钥、完整交易号、二维码或个人资料路径。需要共享截图时,检查页头、地址栏、弹窗和图片内容;仅替换正文中的姓名不够。
记录结果时区分“表单已填写”“申请已提交”“审核中”“配置已保存”“正式功能已验证”。例如产品审核、应用审核与真实收款是不同状态,不能合并写成“全部开通”。
7. 对应回归验证
BrowserActionUpgradeTest 验证前景按钮选择、auto 不穿透遮挡、对象释放异常不补点,并保留显式 JS 模式的测试。BrowserFrictionUpgradeTest 验证动作异常与页面变化的关联。这些测试使用本地页面,不会向真实商户平台提交申请。
