<!-- Source: https://deepseek-browser-use.io.nexus/08 -->

# 表单与多层弹窗排障：防止重复提交

本文对应浏览器工程的当前源码，说明表单定位、点击降级、对象释放异常和人工验证的处理方式。升级源码后需要重新构建并重启服务；旧进程不会自动获得这些行为。

支付产品申请等业务可参考[支付宝接入指南](https://tio-boot.com/zh/50_payment/03.html)。本章只讲浏览器操作，不重复支付接口实现。示例中的任务 ID、企业名称和域名均为占位信息。

## 1. 可见不等于能收到点击

背景表单和前景短信弹窗可能都有 `button[type=submit]`。两个按钮的 `isVisible` 都可能为真，但背景按钮被弹窗遮挡，不能接收真实点击。

使用选择器定位动作时，`resolveActionTarget` 会统计匹配数量，在前 20 个匹配中优先选择可见且中心点命中自身或子元素的控件。如果找不到这样的控件，保留首个可见匹配交给后续可操作性检查；全部隐藏时保留首个匹配，以兼容隐藏字段的显式操作。这不是“自动理解哪个提交按钮正确”，仍应使用当前弹窗范围内的精确选择器。

多个匹配时可查看回执中的 `matched`、`chosenIndex`、`scanned` 和 `selectorNote`。`chosenIndex` 从 0 开始，属于选择器匹配列表，不是页面快照中的交互索引。

推荐流程：

1. 用 `get_browser_state` 读取当前弹窗和控件。
2. 根据最新索引操作，或使用能限定当前弹窗的选择器。
3. 填写后重新读取状态，核对字段值和按钮文字。
4. 提交后查看业务结果，再决定下一步。

页面变化后不要复用旧索引。自定义下拉框的 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`：

```json
{
  "id":"1001",
  "method":"execute_js",
  "params":{
    "body":"() => ({url: location.href, text: document.body.innerText})",
    "retryOnSpurious":true
  }
}
```

不要给包含 `.click()`、表单提交、数据写入或网络变更请求的脚本加这个参数。`changed=true` 只表示页面变化，`ok=true` 也不能替代“申请成功”“密钥已保存”等业务结果。

同一个开关现在也对**动作类命令**开放，处置顺序是「先读、后判、再决定」：先用只读命令确认上一次到底生效没有；确认**没有生效**，再带 `retryOnSpurious:true` 重发一次；如果连伪故障都过不去，改用**按坐标点击**（先 `get_element_box` 或页面内取 `getBoundingClientRect()` 算中心点，再 `mouse_move` + `mouse_click x y`），它不依赖 DOM 节点句柄。默认行为没有变：动作类命令不声明这个参数就一次都不重发。

## 4. Windows 中文与引号参数

通过 `.cmd` 传递中文、多行脚本或带引号的 CSS 选择器时，可能发生编码或转义丢失。回执中的 `committed=true` 表示输入动作完成，不代表传入内容一定与调用方原意一致。

复杂参数使用 UTF-8 JSON 文件，以下命令在浏览器项目根目录执行：

```powershell
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` 并明确告知用户；等待期间保留页签，不刷新、不重复发码、不猜测验证结果。详细流程见[人机协作](<https://deepseek-browser-use.io.nexus/04>)。

密钥由本地授权流程生成时，私钥仅保留在本地或服务器；网页通常只需要应用公钥。保存后核对平台成功信息和公钥配对，不以短信验证通过代替配置保存成功。

## 6. 脱敏与状态记录

终端输出、trace JSON、页面文本、截图、临时参数文件是不同产物。某一层做了脱敏，不能推断其他层也安全。尤其是 `data/<id>/` 原始留档中的页签地址和表单值，可能保留实际账号、AppID、验证码或带签名的链接。

公共文档使用“示例科技有限公司”、`example.com`、`YOUR_APP_ID` 等占位符。不复制真实手机号、证照、短信码、私钥、公钥、完整交易号、二维码或个人资料路径。需要共享截图时，检查页头、地址栏、弹窗和图片内容；仅替换正文中的姓名不够。

记录结果时区分“表单已填写”“申请已提交”“审核中”“配置已保存”“正式功能已验证”。例如产品审核、应用审核与真实收款是不同状态，不能合并写成“全部开通”。

## 7. 对应回归验证

`BrowserActionUpgradeTest` 验证前景按钮选择、auto 不穿透遮挡、对象释放异常不补点，并保留显式 JS 模式的测试。`BrowserFrictionUpgradeTest` 验证动作异常与页面变化的关联。这些测试使用本地页面，不会向真实商户平台提交申请。
