🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

browserctl-cli

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

browserctl-cli - npm Package Compare versions

Comparing version
0.1.4
to
0.1.5
dist/chunk-2UQK5E5B.js

Sorry, the diff of this file is too big to display

+336
# browserctl-flow JSON 文件格式
## 1. 定位
`browserctl-flow.json` 是浏览器录制插件交给数字员工客户端的通用结构化格式。
它承载固定的 `browserctl` 命令集、业务描述、credentials 建议、业务输入和诊断信息。
数字员工客户端应能读取该 JSON,并结合业务描述生成可用 skill。
`browserctl-cli` 只提供命令参考和格式说明,不负责插件录制实现,也不直接生成业务
技能。
## 2. 推荐文件名
```text
<flow-name>.browserctl-flow.json
```
可选 YAML 形式:
```text
<flow-name>.browserctl-flow.yaml
```
但平台优先支持 JSON。YAML 只建议作为人工编辑或审查格式。
## 3. 顶层结构
```ts
interface BrowserctlFlowFile {
schemaVersion: "1.0.0"
kind: "browserctl-flow"
metadata: FlowMetadata
business?: BusinessDescription
credentials?: CredentialSpec[]
inputs?: BusinessInputSpec[]
runtimeBindings?: RuntimeBindings
commands: BrowserctlCommandSpec[]
warnings?: FlowWarning[]
quality?: FlowQuality
}
```
### 3.1 metadata
```ts
interface FlowMetadata {
name: string
title?: string
source?: string
createdAt?: string
recorderVersion?: string
}
```
### 3.2 business
```ts
interface BusinessDescription {
description: string
trigger?: string
successCriteria?: string
notes?: string
}
```
### 3.3 credentials
字段与项目技能凭证声明规范对齐:
```ts
interface CredentialSpec {
key: string
label?: string
type?: "text" | "url" | "username" | "password" | "token" | "api_key" | "select"
required?: boolean
secret?: boolean
placeholder?: string
help?: string
options?: string[]
}
```
### 3.4 inputs
```ts
interface BusinessInputSpec {
key: string
label?: string
type?: "text" | "number" | "date" | "datetime" | "select" | "boolean"
required?: boolean
defaultValue?: string | number | boolean
help?: string
options?: string[]
}
```
### 3.5 runtimeBindings
`runtimeBindings` 用于声明不能提前确定、必须在运行时解析的引用。它不是业务输入,也不
是 credentials。当前推荐把 iframe 表达为可重放的 `browserctl frame <locator>` 命令,
而不是把录制时的 `frameId` 写进文件;因此 iframe 通常不需要写入
`runtimeBindings.frames`。
```ts
interface RuntimeBindings {
// 保留给未来无法直接用命令表达的运行时对象;当前 iframe 不应依赖这里。
}
```
iframe 命令应直接使用稳定 locator 切换 active frame,后续普通命令都会作用于该 frame。
例如:
```json
{
"commands": [
{
"id": "cmd-008",
"argv": ["frame", "[role=\"dialog\"] iframe"],
"metadata": {
"frameUrlGlob": "http://10.172.246.234:19901/dky/home-assets/*.html*"
}
},
{
"id": "cmd-009",
"argv": ["click", "[data-testid=\"nav-paas\"]"]
},
{
"id": "cmd-010",
"argv": ["frame", "main"]
}
]
}
```
插件应优先输出 CSS locator,例如 `"[role=\"dialog\"] iframe"`;如果 iframe owner 的选择器不稳定,也可以输出
`"url:<glob>"` 或 `"name:<name>"`。动态 URL 场景使用 glob,例如
`url:http://10.172.246.234:19901/dky/home-assets/*.html*`,不要记录一次性的 frame id。
如果无法可靠生成 frame locator,应设置 `quality.status="blocked"`,并返回
`IFRAME_UNRESOLVED`。
### 3.6 commands
```ts
interface BrowserctlCommandSpec {
id: string
argv: string[]
capture?: Record<string, string>
preview?: string
description?: string
sourceStepId?: string
metadata?: Record<string, unknown>
warnings?: FlowWarning[]
}
```
`argv` 是唯一机器执行依据。`preview` 只是可读展示。
`capture` 把当前命令响应的 `result.data` 字段保存为运行时变量。键是
`data` 下的字段名,值是变量名;后续 `argv` 中只有完整的 `${KEY}` token
会被替换,嵌在普通文本中的占位符不会替换。
```json
[
{
"id": "open-detail",
"argv": ["tab", "new", "https://example.com/detail"],
"capture": { "tabId": "DETAIL_TAB" }
},
{ "argv": ["tab", "${DETAIL_TAB}"] },
{ "argv": ["snapshot", "--interactive"] }
]
```
将该数组通过 stdin 传给 `browserctl batch --json`。响应缺少捕获字段时返回
`BATCH_CAPTURE_MISSING`;引用未定义变量时返回
`BATCH_BINDING_UNRESOLVED`。两种错误都会停止后续命令。
多 Tab Flow 应显式表达创建、切换和关闭。不要依赖“当前 Tab”猜测,也不要用数组
下标寻址。创建新 tab 后用 `capture.tabId` 保存实际 `tabId`,后续用完整
`${KEY}` token 切换:
```json
[
{
"id": "create-detail",
"argv": ["tab", "new", "https://example.com/detail"],
"capture": { "tabId": "DETAIL_TAB" }
},
{ "id": "switch-detail", "argv": ["tab", "${DETAIL_TAB}"] },
{ "id": "close-detail", "argv": ["tab", "close", "${DETAIL_TAB}"] }
]
```
如果新 tab 来自点击,插件应导出 `["click", "<selector>", "--new-tab"]` 并捕获
返回的 `data.tabId`。只有宿主无法可靠捕获新 tab,或录制数据无法确定后续步骤
属于哪个 tab 时,才应标记 blocking issue。
### 3.7 warnings
```ts
interface FlowWarning {
code: string
commandId?: string
message: string
severity?: "info" | "warning" | "error"
}
```
### 3.8 quality
```ts
interface FlowQuality {
status: "ready" | "needs_review" | "blocked"
blockingIssues?: FlowWarning[]
}
```
客户端必须尊重 `quality.status`:
| status | 处理建议 |
|---|---|
| `ready` | 可进入自动生成 skill / 自动回放验证流程 |
| `needs_review` | 可导入,但需要人工确认 warnings 后再固化 |
| `blocked` | 不得自动生成可执行 skill,也不得直接 batch 执行 |
## 4. 完整示例
```json
{
"schemaVersion": "1.0.0",
"kind": "browserctl-flow",
"metadata": {
"name": "meter-rebind",
"title": "电表重新关联",
"source": "browser-extension-recorder",
"createdAt": "2026-07-14T10:00:00.000Z"
},
"business": {
"description": "登录同期线损指标跟踪管理系统,按设备 ID 查询并执行电表重新关联。",
"successCriteria": "页面完成重新关联操作并出现成功提示或目标记录状态更新。"
},
"credentials": [
{
"key": "METER_REBIND_BASE_URL",
"label": "系统登录地址",
"type": "url",
"required": true,
"secret": false,
"placeholder": "http://192.168.2.122:30264/#/login"
},
{
"key": "METER_REBIND_USERNAME",
"label": "登录用户名",
"type": "username",
"required": true,
"secret": false
},
{
"key": "METER_REBIND_PASSWORD",
"label": "登录密码",
"type": "password",
"required": true,
"secret": true
}
],
"inputs": [
{
"key": "DEVICE_ID",
"label": "设备 ID",
"type": "text",
"required": true,
"defaultValue": "1000000150601081283"
}
],
"commands": [
{
"id": "cmd-001",
"argv": ["open", "${METER_REBIND_BASE_URL}"],
"preview": "open \"${METER_REBIND_BASE_URL}\""
},
{
"id": "cmd-002",
"argv": ["wait", "--load", "domcontentloaded", "--timeout", "10000"]
},
{
"id": "cmd-003",
"argv": ["fill", "[placeholder=\"用户名\"]", "${METER_REBIND_USERNAME}"]
},
{
"id": "cmd-004",
"argv": ["fill", "[placeholder=\"密码\"]", "${METER_REBIND_PASSWORD}"]
},
{
"id": "cmd-005",
"argv": ["click", "input.anniu.un_butt.buttonClass"]
},
{
"id": "cmd-006",
"argv": ["wait", "--url", "http://192.168.2.122:30264/#/*", "--timeout", "12000"]
}
],
"warnings": [],
"quality": {
"status": "ready",
"blockingIssues": []
}
}
```
## 5. 约束
1. `commands[].argv` 必须是字符串数组。
2. `commands[].argv[0]` 必须是 `browserctl` 支持的子命令。
3. 命令不带 `browserctl` 前缀。
4. 所有可变业务值、凭证值、运行时引用都用 `${KEY}` 占位。
5. 文件中不得包含真实密码、Token、Cookie。
6. 登录配置放入 `credentials`。
7. 业务参数放入 `inputs`。
8. iframe、弹窗、标签页等运行时引用不得放入 `inputs` 或 `credentials`;iframe 优先写成
`frame <locator>` 命令,其他无法命令化的引用才放入 `runtimeBindings` 或标记为 blocking issue。
9. 点击后跳转优先表达为 `click` + `wait --url`。
10. `navigate` 只用于明确的直接导航,不用于替代业务点击。
11. `quality.status=blocked` 时,平台不得自动生成可执行 skill。
## 6. 客户端生成 skill 的建议
数字员工客户端读取该文件后:
1. 将 `credentials` 写入 `SKILL.md` frontmatter。
2. 将 `inputs` 写成技能运行所需业务输入。
3. 将 `business.description` 写入技能说明。
4. 将 `commands[].argv` 固定为执行步骤。
5. 执行前解析 `credentials`、`inputs` 和 `runtimeBindings`,再转成 batch JSON。
6. 在技能正文中声明:不要重排、改写或重新推理浏览器步骤。
7. 缺 credentials 时使用系统凭证配置卡片,不在聊天中索要密码。
# browserctl frame 上下文命令设计规范
状态:Draft,待评审后再进入实现计划。
## 1. 背景
浏览器录制插件已经能导出 `browserctl-flow.json`,但 iframe 步骤目前可能出现这类
命令:
```json
["frame", "click", "${FRAME_41}", "[data-testid=\"nav-paas\"]"]
```
这里的 `${FRAME_41}` 是录制时的临时 frame 引用。它不能长期固化到 skill 中:
1. frame id 会随页面刷新、重新登录、弹框重建而变化。
2. 数字员工客户端不应承担 `${FRAME_41}` 到真实 frameId 的解析逻辑。
3. flow 文件应该尽量是可读、可回放的 browserctl 命令集,而不是平台私有 IR。
本规范定义新的 iframe 表达方式:`browserctl frame <locator>` 切换当前 frame 上下文,
后续普通 `click` / `fill` / `wait` / `get` 等命令作用在该 frame 内,直到执行
`browserctl frame main`。
## 2. 目标
1. 让插件导出的 `commands[].argv` 可以直接进入 `browserctl batch --json`。
2. 移除新产物中 `${FRAME_41}` 这类平台侧绑定需求。
3. 让 iframe 操作和 tab 操作风格一致:先切换上下文,再执行普通命令。
4. 覆盖 iframe URL 动态变化的业务场景,例如详情弹框内 iframe URL 包含数据 id。
## 3. 非目标
1. 不要求插件生成全局唯一、永不变化的 iframe URL。
2. 不要求数字员工客户端理解 iframe frameId 解析。
3. 不在本规范中设计跨多个嵌套 iframe 的完整路径语言;v1 优先覆盖常见弹框 iframe、
页面一级 iframe。iframe owner selector 默认从 main frame 查找。
4. 不改变 tab 命令设计;tab 已通过 `tab new` / `tab <id|label>` /
`capture.tabId` 表达。
## 4. 命令模型
### 4.1 新推荐命令
```bash
browserctl frame <locator> [--timeout <ms>]
browserctl frame main
```
示例:
```bash
browserctl frame "[role=\"dialog\"] iframe"
browserctl click "[data-testid=\"save\"]"
browserctl frame main
```
### 4.2 JSON argv 表达
```json
[
["click", "[data-testid=\"open-detail\"]"],
["wait", "--selector", "[role=\"dialog\"] iframe", "--timeout", "10000"],
["frame", "[role=\"dialog\"] iframe"],
["click", "[data-testid=\"save\"]"],
["frame", "main"]
]
```
### 4.3 命令解析规则
1. `frame main` 表示回到主文档。
2. 其他 `frame <locator>` 走上下文切换逻辑。
3. 插件生成的 locator 推荐使用 `selector:` / `url:` / `name:` / `ref:` 显式
前缀,避免与未来子命令或特殊值冲突。
## 5. Frame locator
### 5.1 推荐优先级
插件端生成 frame locator 时按以下优先级:
1. 弹框或区域内的 iframe 元素 selector。
2. iframe 元素的稳定属性 selector,例如 `name`、`title`、`data-testid`。
3. iframe `src` 的模糊 selector。
4. frame URL glob。
5. frame name。
推荐:
```json
["frame", "[role=\"dialog\"] iframe"]
```
可接受:
```json
["frame", "iframe[src*=\"/dky/home-assets/\"]"]
```
兜底:
```json
["frame", "url:http://10.172.246.234:19901/dky/home-assets/*.html"]
```
不推荐:
```json
["frame", "url:http://10.172.246.234:19901/dky/home-assets/12345.html"]
```
禁止作为新格式导出:
```json
["frame", "click", "${FRAME_41}", "[data-testid=\"nav-paas\"]"]
```
### 5.2 显式前缀
为避免 selector、URL、name 歧义,插件端可以使用显式前缀:
| 前缀 | 含义 | 示例 |
|---|---|---|
| `selector:` | iframe 元素 CSS selector | `selector:[role="dialog"] iframe` |
| `url:` | frame URL glob | `url:http://host/dky/home-assets/*.html` |
| `name:` | frame name 精确匹配 | `name:detail-frame` |
| `ref:` | snapshot 中的 iframe ref | `ref:@e3` |
插件端推荐使用 `selector:` 或裸 CSS selector。裸 locator 保留给人工 CLI 使用。
### 5.3 动态 iframe URL 场景
业务场景:
```text
点击某条数据 -> 打开详情弹框 -> 弹框中 iframe 加载:
http://10.172.246.234:19901/dky/home-assets/{id}.html
```
这种场景不要按完整 URL 固化。插件应导出弹框 iframe 元素定位:
```json
[
["click", "<详情按钮 selector>"],
["wait", "--selector", "[role=\"dialog\"] iframe", "--timeout", "10000"],
["frame", "[role=\"dialog\"] iframe"],
["wait", "--selector", "[data-testid=\"detail-form\"]", "--timeout", "10000"],
["click", "[data-testid=\"save\"]"],
["frame", "main"]
]
```
如果弹框没有稳定容器,再退到 iframe src 模糊 selector:
```json
["frame", "iframe[src*=\"/dky/home-assets/\"]"]
```
## 6. 上下文语义
### 6.1 当前 frame 上下文
`frame <locator>` 成功后,browserctl 在当前 automation tab 中记录 active frame。
后续 selector 类命令默认在 active frame 内执行。
v1 必须受 active frame 影响的命令:
1. `snapshot`
2. `click` / `dblclick` / `hover` / `focus`
3. `fill` / `type` / `press`
4. `check` / `uncheck` / `select`
5. `wait --selector` / `wait --text` / `wait --fn`
6. `get value|text|html|count|box|styles|attr`
7. `is visible|enabled|checked`
8. `eval`
9. `find`
10. `scroll` / `scrollintoview`
URL / tab 级命令不受 active frame 影响:
1. `open`
2. `navigate`
3. `wait --url`
4. `get url`
5. `get title`
6. `back` / `forward` / `reload`
7. `close`
### 6.2 自动重置
以下操作后 active frame 必须重置为 main:
1. `open`
2. `navigate`
3. `reload`
4. `back`
5. `forward`
6. `tab <id|label>`
7. `tab new`
8. `tab close`
插件端仍应显式导出 `["frame", "main"]`,避免人类阅读和异常恢复时产生歧义。
### 6.3 `@eN` ref 行为
如果 `snapshot` 输出的 `@eN` 自带 frame context,则 `click @eN` 可以直接工作。
但录制插件不应把 `@eN` 作为长期 skill 的主 selector,因为 ref 通常只在当前页面快照
周期内稳定。
推荐新 flow 使用稳定 CSS selector + `frame <locator>`。
## 7. 解析与错误
### 7.1 locator 解析
`frame <locator>` 按以下方式解析:
1. `main`:清空 active frame。
2. `ref:@eN` 或 `@eN`:从当前 ref map 中解析 iframe/frame 元素。
3. `selector:<css>` 或 CSS selector:默认从 main frame 查找 iframe/frame owner
元素。v1 不要求在 active iframe 内继续查找子 iframe。
4. `url:<glob>`:在当前 tab frame tree 中按 URL glob 匹配。
5. `name:<name>`:在当前 tab frame tree 中按 name 精确匹配。
6. 裸字符串兜底:先尝试 CSS selector,再尝试 frame name,再尝试 URL contains/glob。
插件端不应依赖裸字符串兜底。插件生成的 flow 应优先使用 CSS selector 或显式前缀。
`--timeout <ms>` 表示等待 locator 在该时间内解析到唯一 frame。超时仍未找到返回
`FRAME_NOT_FOUND`;超时期间持续匹配到多个候选返回 `FRAME_AMBIGUOUS`。
### 7.2 错误码
| code | 含义 |
|---|---|
| `FRAME_NOT_FOUND` | 未找到匹配 frame |
| `FRAME_AMBIGUOUS` | 找到多个匹配 frame,无法确定目标 |
| `FRAME_OWNER_NOT_IFRAME` | selector/ref 命中的元素不是 iframe/frame |
| `FRAME_DETACHED` | frame 在解析后、执行前被移除 |
| `FRAME_CONTEXT_UNAVAILABLE` | 当前宿主不支持 frame 上下文切换 |
| `FRAME_CONTEXT_LOST` | active frame 因 frame detach 等原因失效 |
`FRAME_AMBIGUOUS` 返回结果应包含候选信息,便于插件或人工修正:
```json
{
"ok": false,
"code": "FRAME_AMBIGUOUS",
"error": "multiple frames matched locator",
"data": {
"candidates": [
{ "url": "http://host/a.html", "name": "detail" },
{ "url": "http://host/b.html", "name": "detail" }
]
}
}
```
## 8. browserctl-flow JSON 要求
### 8.1 新插件导出要求
新插件不得导出 `${FRAME_41}` 这类录制时 frame 占位符。
推荐:
```json
{
"id": "cmd-008",
"argv": ["frame", "[role=\"dialog\"] iframe"],
"description": "切换到详情弹框 iframe"
}
```
随后导出普通命令:
```json
{
"id": "cmd-009",
"argv": ["click", "[data-testid=\"nav-paas\"]"]
}
```
离开 iframe 时:
```json
{
"id": "cmd-010",
"argv": ["frame", "main"]
}
```
### 8.2 quality 规则
| 场景 | `quality.status` |
|---|---|
| frame locator 稳定、无阻塞问题 | `ready` |
| 使用长链 selector、src 模糊匹配等较脆弱 locator | `needs_review` |
| 只有录制时 frameId,无法生成 locator | `blocked` |
| locator 可能匹配多个 iframe 且无法消歧 | `blocked` |
### 8.3 warnings
| code | 含义 |
|---|---|
| `FRAME_SELECTOR_UNSTABLE` | frame selector 脆弱 |
| `FRAME_URL_DYNAMIC` | frame URL 包含动态业务 id,不应完整固化 |
| `FRAME_LOCATOR_REVIEW_REQUIRED` | frame locator 需要人工确认 |
### 8.4 blocking issues
| code | 含义 |
|---|---|
| `FRAME_LOCATOR_UNRESOLVED` | 无法从录制数据生成 frame locator |
| `FRAME_LOCATOR_AMBIGUOUS` | locator 可能匹配多个 frame |
| `FRAME_RUNTIME_ID_ONLY` | 只能拿到录制时 frameId,不能生成可回放命令 |
| `FRAME_CONTEXT_UNSUPPORTED` | 当前 browserctl 运行环境不支持 frame 上下文 |
## 9. 插件端采集建议
录制插件在用户操作 iframe 内元素时,应额外采集:
1. iframe owner element 的 selector candidates。
2. iframe 所在弹框/容器 selector candidates。
3. iframe `name`、`title`、`src`。
4. frame URL,但只作为辅助信息。
5. 当前操作元素在 iframe 内的 selector candidates。
selector 优先级:
1. `data-testid`、`data-test`、`data-cy`
2. `role` / `aria-label`
3. `name`
4. 稳定 `id`
5. 弹框容器 + iframe
6. `iframe[src*="..."]`
7. 普通 CSS selector
插件端应避免直接使用长链:
```css
body > div:nth-child(2) > div:nth-child(1) > iframe:nth-child(1)
```
如果只能生成长链 selector,输出 `FRAME_SELECTOR_UNSTABLE` warning。
## 10. 验收标准
文档评审通过后,实现必须满足:
1. `browserctl frame main` 可回到主文档。
2. `browserctl frame <iframe-css-selector>` 可从 main frame 查找 iframe owner,并切换
到 iframe 上下文。
3. `browserctl frame url:<glob>` 可按 frame URL glob 切换。
4. `frame <locator>` 找不到时返回 `FRAME_NOT_FOUND`。
5. `frame <locator>` 匹配多个目标时返回 `FRAME_AMBIGUOUS`。
6. `click` / `fill` / `wait --selector` / `get text` / `eval` / `find` 等 v1
必选命令在 active frame 中执行。
7. `open` / `navigate` / `reload` / `tab` 切换后 active frame 自动回 main。
8. `batch --json` 能执行:
```json
[
["open", "http://example.test"],
["click", "[data-testid=\"open-detail\"]"],
["wait", "--selector", "[role=\"dialog\"] iframe"],
["frame", "[role=\"dialog\"] iframe"],
["fill", "[data-testid=\"name\"]", "测试"],
["click", "[data-testid=\"save\"]"],
["frame", "main"]
]
```
9. 新插件导出的 `browserctl-flow.json` 不再出现 `${FRAME_...}`。
## 11. 已确认结论与待评审问题
已确认:
1. agent-browser 有 `eval` 和 `find` 命令,本规范将两者纳入 v1 active frame
覆盖范围。
2. 不保留 `runtimeBindings.frames`。
3. 不考虑旧 `${FRAME_...}` flow 兼容或迁移。
待评审:
1. 是否需要 `frame parent` 支持嵌套 iframe 返回上一层?
agent-browser 文档未列该命令,本规范 v1 不纳入。
# browserctl frame 上下文实现计划
状态:Draft,基于 `frame-context-command-spec.md`,待评审后实施。
## 1. 实现目标
实现 `browserctl frame <locator>` / `browserctl frame main`,让 iframe 操作符合
agent-browser 风格:
```json
[
["click", "[data-testid=\"open-detail\"]"],
["wait", "--selector", "[role=\"dialog\"] iframe"],
["frame", "[role=\"dialog\"] iframe"],
["fill", "[data-testid=\"name\"]", "测试"],
["click", "[data-testid=\"save\"]"],
["frame", "main"]
]
```
核心要求:
1. 新插件产物不再出现 `${FRAME_...}`。
2. 不保留 `runtimeBindings.frames`。
3. 不做旧 `${FRAME_...}` flow 兼容。
4. `eval` / `find` 纳入 active frame 范围。
5. `frame parent` 不纳入 v1。
6. 不保留旧 `frame snapshot/click/fill/get <frameId>` CLI 入口。
## 2. 影响范围
### 2.1 CLI 层
文件:
- `packages/browserctl/src/index.js`
- `packages/browserctl/src/index.d.ts`
- `packages/browserctl/test/index.test.js`
职责:
1. 解析 `browserctl frame <locator> [--timeout <ms>]`。
2. 解析 `browserctl frame main`。
3. 将新命令映射到 bridge action:
```json
{
"action": "frame-select",
"body": {
"locator": "selector:[role=\"dialog\"] iframe",
"timeout_ms": 10000
}
}
```
4. 保持 `batch --json` 不做额外特殊处理;它只是顺序执行 argv。
### 2.2 Bridge 层
文件:
- `packages/browser-sdk/src/bridge.ts`
- `packages/browser-sdk/test/bridge.test.ts`
职责:
1. 新增 action:
- `frame-select`
- `frame-main`
2. 将 action 转发给当前 tab 的 `BrowserController`。
3. tab 级操作后清空 active frame:
- `tab-create`
- `tab-activate`
- `tab-close`
- `navigate`
- `close`
### 2.3 Controller 层
文件:
- `packages/browser-sdk/src/controller.ts`
- `packages/browser-sdk/src/frame-tree.ts`
- `packages/browser-sdk/test/controller.test.ts`
职责:
1. 增加 active frame 状态:
```ts
private activeFrameId: string | null = null
```
2. 新增方法:
```ts
selectFrame(locator: string, timeoutMs?: number): Promise<CdpResult>
selectMainFrame(): CdpResult
clearActiveFrame(): void
```
3. 新增 frame locator 解析:
- `selector:<css>`
- 裸 CSS selector
- `url:<glob>`
- `name:<name>`
- `ref:@eN`
- `@eN`
- `main`
4. locator 解析行为:
- CSS selector 从 main frame 查找 iframe owner。
- URL/name 从当前 tab frame tree 查找。
- `@eN` 从当前 ref map 查找 iframe owner。
- 找不到返回 `FRAME_NOT_FOUND`。
- 多个候选返回 `FRAME_AMBIGUOUS`。
- 命中非 iframe/frame 元素返回 `FRAME_OWNER_NOT_IFRAME`。
5. active frame 作用范围:
- `snapshot`
- `click` / `dblclick` / `hover` / `focus`
- `fill` / `type` / `press`
- `check` / `uncheck` / `select`
- `wait --selector` / `wait --text` / `wait --fn`
- `get value|text|html|count|box|styles|attr`
- `is visible|enabled|checked`
- `eval`
- `find`
- `scroll` / `scrollintoview`
6. URL/tab 级命令不受 active frame 影响,并会清空 active frame。
## 3. 分阶段实施
### 阶段 1:CLI 命令解析
改动:
1. 更新 usage 文案:
```text
browserctl frame <locator> [--timeout 10000]
browserctl frame main
```
2. 在 `command === "frame"` 分支中:
- `rest[0] === "main"` -> `postAction("frame-main", {})`
- 其他单参数 locator -> `postAction("frame-select", { locator, timeout_ms })`
3. 移除旧低层 `frame snapshot/click/fill/get <frameId>` CLI 入口;对应测试改为
`frame <locator>` + 普通命令。
验收:
1. `browserctl frame main` 命中 `/frame-main`。
2. `browserctl frame "[role=\"dialog\"] iframe"` 命中 `/frame-select`。
3. `browserctl frame "url:http://host/*.html" --timeout 12000` 传递 `timeout_ms=12000`。
4. `batch --json` 能顺序执行 `frame` 与后续普通命令。
5. `browserctl frame click ...` 不再作为合法命令;如需选择 CSS `click` 元素,使用
`browserctl frame selector:click`。
### 阶段 2:Bridge action
改动:
1. `bridge.ts` 新增:
```ts
case "frame-select":
result = await controller.selectFrame(locator, timeoutMs)
case "frame-main":
result = controller.selectMainFrame()
```
2. 在导航、tab 切换、关闭等路径调用 `controller.clearActiveFrame()`。
验收:
1. `frame-select` 正确调用 controller。
2. `frame-main` 清空 active frame。
3. `tab-activate` 后 active frame 不残留。
4. `navigate` 后 active frame 不残留。
### 阶段 3:Frame locator 解析
改动:
1. 复用现有 `listFrames()` / `flattenFrames()` 能力处理 URL/name。
2. 新增 CSS iframe owner 解析:
- main frame `document.querySelectorAll(<selector>)`
- 过滤 `IFRAME` / `FRAME`
- 取 owner element 对应 frame id
3. `url:<glob>` 使用 glob 匹配完整 frame URL。
4. `name:<name>` 使用精确匹配。
5. `ref:@eN` 复用 ref map,确认 ref 指向 iframe/frame owner。
验收:
1. `selector:[role="dialog"] iframe` 命中唯一 iframe。
2. `iframe[src*="/dky/home-assets/"]` 可覆盖动态 URL。
3. `url:http://host/dky/home-assets/*.html` 可按 URL glob 命中。
4. 多个 iframe 命中时返回 `FRAME_AMBIGUOUS`,并带 candidates。
5. selector 命中 div 时返回 `FRAME_OWNER_NOT_IFRAME`。
### 阶段 4:active frame 作用于普通命令
这是最大风险阶段。已有 controller 中很多方法直接在主 session 上做
`Runtime.evaluate` 或 DOM 操作,需要统一进入 active frame。
建议策略:
1. 增加内部 helper:
```ts
private currentSessionId(): string | undefined
private currentFrameId(): string | null
```
2. 对 selector 解析路径统一经过 active frame session:
- 若 active frame 是 OOPIF,使用对应 frame session。
- 若 active frame 是同进程 iframe,需要通过 owner iframe 的 `contentDocument`
或现有 `callInFrame` 能力执行。
3. 优先打通 v1 最关键链路:
- `snapshot`
- `wait --selector`
- `click`
- `fill`
- `get text`
- `eval`
- `find`
4. 再覆盖其余 selector 命令:
- `hover` / `focus` / `dblclick`
- `type` / `press`
- `check` / `uncheck` / `select`
- `is`
- `scroll`
验收:
1. 切入 iframe 后,`fill "#inner" "x"` 操作 iframe 内元素。
2. 切入 iframe 后,`wait --selector "#inner"` 在 iframe 内等待。
3. 切入 iframe 后,`eval "document.location.href"` 返回 iframe URL。
4. 切入 iframe 后,`find first "#inner" fill "x"` 在 iframe 内定位。
5. `frame main` 后,同样 selector 回到主文档语义。
### 阶段 5:错误码与返回结构
改动:
1. controller 返回标准错误码:
- `FRAME_NOT_FOUND`
- `FRAME_AMBIGUOUS`
- `FRAME_OWNER_NOT_IFRAME`
- `FRAME_DETACHED`
- `FRAME_CONTEXT_UNAVAILABLE`
- `FRAME_CONTEXT_LOST`
2. `FRAME_AMBIGUOUS` 返回候选:
```json
{
"ok": false,
"code": "FRAME_AMBIGUOUS",
"data": {
"candidates": [
{ "id": "frame-1", "url": "...", "name": "..." }
]
}
}
```
验收:
1. CLI 失败时 exit code 非 0。
2. batch 遇到 frame 错误时按现有 batch 失败语义停止或记录。
3. 错误码进入 `reference.md`。
### 阶段 6:文档更新
文件:
- `packages/browserctl-cli/docs/reference.md`
- `packages/browserctl-cli/docs/browser-extension-recorder-guide.md`
- `packages/browserctl-cli/docs/browserctl-flow-json-format.md`
- `packages/browserctl-cli/docs/flow-json-to-browserctl-transition-spec.md`
改动:
1. `reference.md` 增加:
```bash
browserctl frame <locator> [--timeout <ms>]
browserctl frame main
```
2. 插件指南明确:
- iframe 用 `frame <locator>` / `frame main`
- 不导出 `${FRAME_...}`
- 不导出 `runtimeBindings.frames`
3. `reference.md` 移除旧低层命令:
- `frame snapshot <frameId>`
- `frame click <frameId> <selector>`
- `frame fill <frameId> <selector> <text>`
- `frame get ... <frameId> <selector>`
4. flow JSON 格式移除 `runtimeBindings.frames` 推荐内容。
验收:
1. 文档中不再推荐 `${FRAME_...}`。
2. 文档中不再推荐 `runtimeBindings.frames`。
3. 示例可直接转成 `batch --json`。
## 4. 测试计划
### 4.1 browserctl CLI 单元测试
文件:`packages/browserctl/test/index.test.js`
新增:
1. `frame main` -> `/frame-main`
2. `frame <selector>` -> `/frame-select`
3. `frame url:<glob> --timeout 12000`
4. `batch --json` 顺序执行 frame select + fill
5. `frame` 参数缺失返回 `CLI_USAGE_ERROR`
### 4.2 browser-sdk bridge 测试
文件:`packages/browser-sdk/test/bridge.test.ts`
新增:
1. `frame-select` 调 controller `selectFrame`
2. `frame-main` 调 controller `selectMainFrame`
3. `navigate` 后清 frame
4. `tab-activate` 后清 frame
### 4.3 controller 单元测试
文件:`packages/browser-sdk/test/controller.test.ts`
新增:
1. CSS selector 解析 iframe owner。
2. URL glob 解析 frame tree。
3. name 解析 frame tree。
4. ref 解析 iframe owner。
5. ambiguous / not found / non-iframe 错误。
6. active frame 下 `fill` / `get text` / `wait --selector`。
7. active frame 下 `eval`。
8. active frame 下 `find`。
### 4.4 daemon 集成测试
文件:
- `packages/browserctl-daemon/test/integration.test.ts`
- `packages/browserctl-daemon/test/fixtures/iframe-page.html`
新增或扩展 fixture:
1. 主页面按钮打开详情弹框。
2. 弹框内 iframe `src` 带动态 id。
3. iframe 内包含 input、button、文本。
验证 batch:
```json
[
["open", "file:///.../iframe-dialog-page.html"],
["click", "[data-testid=\"open-detail\"]"],
["wait", "--selector", "[role=\"dialog\"] iframe"],
["frame", "[role=\"dialog\"] iframe"],
["fill", "#inner-name", "测试"],
["get", "value", "#inner-name"],
["eval", "document.location.href"],
["find", "first", "#save", "click"],
["frame", "main"]
]
```
## 5. 实施顺序
建议顺序:
1. CLI parser + tests。
2. Bridge action + tests。
3. Controller frame selection,不改普通命令。
4. Controller active frame 作用于 `snapshot` / `wait --selector` / `fill` /
`click` / `get text`。
5. 覆盖 `eval` / `find`。
6. 覆盖剩余 selector 命令。
7. daemon 集成测试。
8. docs 收口。
这样每一步都能独立验证,避免一次性改完整个 controller 后难定位问题。
## 6. 自我审核
### 6.1 方案是否覆盖用户场景
覆盖。用户场景是:
```text
点击某条数据 -> 打开详情弹框 -> 弹框中 iframe URL 包含数据 id -> 在 iframe 内操作
```
计划要求插件导出:
```json
[
["click", "<详情按钮 selector>"],
["wait", "--selector", "[role=\"dialog\"] iframe"],
["frame", "[role=\"dialog\"] iframe"],
["..."],
["frame", "main"]
]
```
不依赖完整 iframe URL,因此动态 id 不影响回放。
### 6.2 是否符合 agent-browser 风格
符合。agent-browser 的风格是:
1. `tab <id|label>` 切换 tab,再执行普通命令。
2. `frame <selector|@eN|name/URL>` 切换 frame,再执行普通命令。
3. `frame main` 回主文档。
本计划沿用这个上下文模型。
### 6.3 最大实现风险
最大风险在 controller 层:现有很多方法直接在主 session 执行 `Runtime.evaluate`
或 DOM 命令。active frame 要覆盖 `eval` / `find`,不能只改 click/fill。
降低风险的办法:
1. 先实现 frame selection。
2. 再逐类命令迁移到 active frame helper。
3. 每迁移一类命令补测试。
### 6.4 可能被低估的边界
1. 同进程 iframe 与 OOPIF iframe 执行路径不同。
2. `find` 当前可能强依赖主 document,需要改为按 active frame 的 document 查询。
3. `eval` 在 active frame 内执行后,返回对象/异常格式要保持现有 `eval` 语义。
4. `wait --text` 如果实现为全页面文本搜索,需要明确改成 frame 内文本。
5. active frame detach 后,后续命令应返回 `FRAME_CONTEXT_LOST`,而不是误查 main。
### 6.5 不做的事是否合理
合理。
1. 不做 `runtimeBindings.frames`:避免平台侧 IR。
2. 不兼容旧 `${FRAME_...}` flow:用户明确不需要。
3. 不做 `frame parent`:agent-browser 文档未列,v1 主场景不需要。
4. 不做嵌套 iframe 路径语言:先覆盖弹框 iframe 和页面一级 iframe。
### 6.6 计划缺口
有一个点需要实现前确认:
1. `find` 的 active frame 语义要做到多深:至少 CSS locator 类策略必须覆盖;
role/text/label 也应覆盖,否则插件端可能误用。
+131
-99

@@ -5,3 +5,3 @@ #!/usr/bin/env node

startDaemon
} from "./chunk-Y5QCOL6K.js";
} from "./chunk-2UQK5E5B.js";

@@ -13,3 +13,3 @@ // ../browserctl/src/index.js

import { pathToFileURL } from "url";
var VERSION = "0.1.3";
var VERSION = "0.1.4";
var DEFAULT_BASE_URL = process.env.BROWSER_RUNTIME_BRIDGE_URL || "http://127.0.0.1:34555";

@@ -35,7 +35,9 @@ var activeBaseUrl = DEFAULT_BASE_URL;

browserctl frames [--pretty]
browserctl frame snapshot <frame> [--max-nodes N] [--compact|-c] [--depth N|-d N] [--scope <sel>|-s <sel>] [--tree|--interactive] [--pretty]
browserctl frame click <frame> <@eN|selector> [--confirm "message"] [--pretty]
browserctl frame fill <frame> <@eN|selector> (<text> | --text-file <path> | --text-stdin) [--pretty]
browserctl frame get value|text|html|count|box|styles|attr|attribute <frame> <@eN|selector> [name] [--pretty]
browserctl click <@eN|selector> [--confirm "message"] [--pretty]
browserctl frame <selector|@eN|url:<glob>|name:<name>> [--timeout 10000] [--pretty]
browserctl frame main [--pretty]
browserctl tab
browserctl tab new [url] [--label <name>]
browserctl tab <tN|label>
browserctl tab close [tN|label]
browserctl click <@eN|selector> [--new-tab] [--confirm "message"] [--pretty]
browserctl press <key> [@eN|selector] [--ctrl|--shift|--alt|--meta] [--pretty]

@@ -177,2 +179,4 @@ browserctl scroll [@eN|selector] [--to top|bottom] [--by <px>] [--pretty]

flags.bail = true;
} else if (value === "--new-tab") {
flags.newTab = true;
} else if (value === "--interactive" || value === "-i") {

@@ -409,5 +413,3 @@ flags.interactive = true;

} catch (error) {
throw new Error(
`cannot read --file ${flags.file}: ${error.message}`
);
throw new Error(`cannot read --file ${flags.file}: ${error.message}`);
}

@@ -544,2 +546,35 @@ }

}
if (command === "tab") {
const subcommand = rest[0];
if (!subcommand) return await postAction("tabs", {});
if (subcommand === "new") {
if (rest.length > 2) {
throw new Error("tab new accepts at most one URL");
}
const url = rest[1] ? normalizeUrl(rest[1]) : void 0;
return await postAction("tab-create", {
...url ? { url } : {},
...flags.label ? { label: flags.label } : {}
});
}
if (subcommand === "close") {
if (rest.length > 2) {
throw new Error("tab close accepts at most one tab ref");
}
const ref = rest[1];
if (ref && /^\d+$/.test(ref)) {
throw new Error(`tab refs use stable IDs or labels; try tab t${ref}`);
}
return await postAction("tab-close", ref ? { label: ref } : {});
}
if (rest.length > 1) {
throw new Error("tab switch accepts exactly one tab ref");
}
if (/^\d+$/.test(subcommand)) {
throw new Error(
`tab refs use stable IDs or labels, not numeric indexes; try tab t${subcommand}`
);
}
return await postAction("tab-activate", { label: subcommand });
}
if (command === "snapshot") {

@@ -553,3 +588,6 @@ const result = await postAction("snapshot", {

if ((flags.tree || flags.interactive) && result.ok && result.data && Array.isArray(result.data.refs)) {
const text = formatSnapshotText(result.data.refs, Boolean(flags.interactive));
const text = formatSnapshotText(
result.data.refs,
Boolean(flags.interactive)
);
if (batchMode) {

@@ -568,90 +606,16 @@ return { ok: true, data: { format: "text", text } };

if (command === "frame") {
const subcommand = rest[0];
if (subcommand === "snapshot") {
const frame = rest[1];
if (!frame) throw new Error("frame snapshot requires frame");
const result = await postAction("frame-snapshot", {
frame,
max_nodes: Number.isFinite(flags.maxNodes) ? flags.maxNodes : 200,
compact: Boolean(flags.compact),
max_depth: Number.isFinite(flags.depth) ? flags.depth : void 0,
scope_selector: flags.scope || void 0
});
if ((flags.tree || flags.interactive) && result.ok && result.data && Array.isArray(result.data.refs)) {
const text = formatSnapshotText(
result.data.refs,
Boolean(flags.interactive)
);
if (batchMode) {
return { ok: true, data: { format: "text", text } };
}
process.stdout.write(`${text}
`);
return void 0;
}
return result;
const locator = rest[0];
if (!locator) throw new Error("frame requires locator or main");
if (rest.length > 1) {
throw new Error(
"frame accepts exactly one locator; use selector:<css> when a selector contains spaces"
);
}
if (subcommand === "click") {
const frame = rest[1];
const refOrSelector = rest[2];
if (!frame || !refOrSelector) {
throw new Error("frame click requires frame and ref or selector");
}
return await postAction("frame-click", {
frame,
ref_or_selector: refOrSelector,
confirmation_required: Boolean(flags.confirm),
confirmation_message: flags.confirm || void 0
});
if (locator === "main") {
return await postAction("frame-main", {});
}
if (subcommand === "fill") {
const frame = rest[1];
const refOrSelector = rest[2];
if (!frame || !refOrSelector) {
throw new Error("frame fill requires frame and ref or selector");
}
const text = await resolveFillText(rest.slice(2), flags);
return await postAction("frame-fill", {
frame,
ref_or_selector: refOrSelector,
text
});
}
if (subcommand === "get") {
const kind = rest[1];
const frame = rest[2];
const refOrSelector = rest[3];
const allowed = /* @__PURE__ */ new Set([
"value",
"text",
"html",
"count",
"box",
"styles",
"attr",
"attribute"
]);
if (!allowed.has(kind)) {
throw new Error(
"frame get kind must be value|text|html|count|box|styles|attr|attribute"
);
}
if (!frame || !refOrSelector) {
throw new Error("frame get requires kind, frame, and ref or selector");
}
if (kind === "attr" || kind === "attribute") {
const name = rest[4];
if (!name) throw new Error("frame get attr requires attribute name");
return await postAction("frame-get-attr", {
frame,
ref_or_selector: refOrSelector,
name
});
}
return await postAction(`frame-get-${kind}`, {
frame,
ref_or_selector: refOrSelector
});
}
throw new Error("frame subcommand must be snapshot|click|fill|get");
return await postAction("frame-select", {
locator,
timeout_ms: Number.isFinite(flags.timeout) ? flags.timeout : void 0
});
}

@@ -663,2 +627,3 @@ if (command === "click") {

ref_or_selector: refOrSelector,
new_tab: Boolean(flags.newTab),
confirmation_required: Boolean(flags.confirm),

@@ -968,6 +933,41 @@ confirmation_message: flags.confirm || void 0

const commands = flags.json ? JSON.parse(await readStdin()) : rest;
if (!Array.isArray(commands)) {
throw new Error("batch JSON must be an array");
}
const results = [];
const bindings = /* @__PURE__ */ new Map();
for (let i = 0; i < commands.length; i++) {
const item = commands[i];
const subArgv = Array.isArray(item) ? item : splitBatchLine(String(item));
const structured = item && typeof item === "object" && !Array.isArray(item);
const spec = structured ? item : { argv: item };
if (structured && !Array.isArray(spec.argv)) {
const result2 = {
ok: false,
error: "Structured batch items require an argv array",
code: "CLI_USAGE_ERROR"
};
results.push(result2);
print({ ok: false, data: { failedAt: i, results } }, flags.pretty);
return;
}
const rawArgv = Array.isArray(spec.argv) ? spec.argv.map(String) : splitBatchLine(String(spec.argv));
let bindingError;
const subArgv = rawArgv.map((token) => {
const match = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(token);
if (!match) return token;
if (!bindings.has(match[1])) {
bindingError = {
ok: false,
error: `Batch binding is not defined: ${match[1]}`,
code: "BATCH_BINDING_UNRESOLVED"
};
return token;
}
return bindings.get(match[1]);
});
if (bindingError) {
results.push(bindingError);
print({ ok: false, data: { failedAt: i, results } }, flags.pretty);
return;
}
if (subArgv[0] === "batch") throw new Error("nested batch not allowed");

@@ -980,3 +980,35 @@ let result;

}
results.push(result ?? { ok: true });
result ??= { ok: true };
if (spec.capture && typeof spec.capture === "object") {
if (result.ok === false) {
results.push(result);
print({ ok: false, data: { failedAt: i, results } }, flags.pretty);
return;
}
for (const [field, variable] of Object.entries(spec.capture)) {
const value = result?.data?.[field];
if (value === void 0) {
result = {
ok: false,
error: `Batch capture field is missing: data.${field}`,
code: "BATCH_CAPTURE_MISSING"
};
results.push(result);
print({ ok: false, data: { failedAt: i, results } }, flags.pretty);
return;
}
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(String(variable))) {
result = {
ok: false,
error: `Invalid batch binding name: ${variable}`,
code: "CLI_USAGE_ERROR"
};
results.push(result);
print({ ok: false, data: { failedAt: i, results } }, flags.pretty);
return;
}
bindings.set(String(variable), String(value));
}
}
results.push(result);
if (flags.bail && result && result.ok === false) {

@@ -983,0 +1015,0 @@ print({ ok: false, data: { failedAt: i, results } }, flags.pretty);

@@ -5,5 +5,5 @@ #!/usr/bin/env node

startDaemon
} from "./chunk-Y5QCOL6K.js";
} from "./chunk-2UQK5E5B.js";
// src/daemon-entry.ts
await startDaemon(parseArgs(process.argv.slice(2)));

@@ -1,11 +0,11 @@

# 浏览器录制插件对接 browserctl 命令集指南
# 浏览器录制插件对接 browserctl-flow JSON 指南
## 1. 你要交付什么
浏览器录制插件最终需要导出一份 `browserctl` 命令集。我们不再要求插件导出流程
JSON,也不要求插件调用 `browserctl-cli` 的内部代码。插件怎么录制用户操作、怎么
组织中间数据、怎么生成 selector,都由插件端自行实现。
浏览器录制插件最终需要导出一份结构化 JSON 文件,里面包含固定的 `browserctl`
命令集、业务描述、credentials 建议、业务输入和诊断信息。数字员工客户端读取这个
JSON 后,应能结合业务描述生成一个可用 skill。
你只需要保证最终导出的文件能表达一组固定的浏览器自动化步骤,后续系统会把这组
步骤固化到业务技能中,减少模型临场推理和误操作。
插件怎么录制用户操作、怎么组织中间数据、怎么生成 selector,都由插件端自行实现。
本文档只规定最终导出的通用文件格式。

@@ -15,60 +15,135 @@ 推荐导出文件名:

```text
<业务流程名>.browserctl
<业务流程名>.browserctl-flow.json
```
文件内容是 UTF-8 文本,每一行是一条 `browserctl` 子命令。
不推荐把主产物设计成 `.browserctl` 文本文件,因为数字员工平台需要稳定解析结构化
数据。命令文本可以作为 `preview` 字段存在,但机器消费应以 `argv` 数组为准。
## 2. 最小示例
```browserctl
# browserctl-recording: 1.0
# name: meter-rebind
# source: browser-extension-recorder
#
# credentials:
# - METER_REBIND_BASE_URL url secret=false
# - METER_REBIND_USERNAME username secret=false
# - METER_REBIND_PASSWORD password secret=true
#
# business-inputs:
# - DEVICE_ID default=1000000150601081283 help="设备 ID"
open "${METER_REBIND_BASE_URL}"
wait --load domcontentloaded --timeout 10000
fill "[placeholder=\"用户名\"]" "${METER_REBIND_USERNAME}"
fill "[placeholder=\"密码\"]" "${METER_REBIND_PASSWORD}"
click "input.anniu.un_butt.buttonClass"
wait --url "http://192.168.2.122:30264/#/*" --timeout 12000
fill "[placeholder=\"设备ID\"]" "${DEVICE_ID}"
click "button.el-button.el-button--primary"
```json
{
"schemaVersion": "1.0.0",
"kind": "browserctl-flow",
"metadata": {
"name": "meter-rebind",
"title": "电表重新关联",
"source": "browser-extension-recorder",
"createdAt": "2026-07-14T10:00:00.000Z"
},
"business": {
"description": "登录同期线损指标跟踪管理系统,按设备 ID 查询并执行电表重新关联。",
"successCriteria": "页面完成重新关联操作并出现成功提示或目标记录状态更新。"
},
"credentials": [
{
"key": "METER_REBIND_BASE_URL",
"label": "系统登录地址",
"type": "url",
"required": true,
"secret": false,
"placeholder": "http://192.168.2.122:30264/#/login"
},
{
"key": "METER_REBIND_USERNAME",
"label": "登录用户名",
"type": "username",
"required": true,
"secret": false
},
{
"key": "METER_REBIND_PASSWORD",
"label": "登录密码",
"type": "password",
"required": true,
"secret": true
}
],
"inputs": [
{
"key": "DEVICE_ID",
"label": "设备 ID",
"type": "text",
"required": true,
"defaultValue": "1000000150601081283"
}
],
"commands": [
{
"id": "cmd-001",
"argv": ["open", "${METER_REBIND_BASE_URL}"],
"preview": "open \"${METER_REBIND_BASE_URL}\""
},
{
"id": "cmd-002",
"argv": ["wait", "--load", "domcontentloaded", "--timeout", "10000"],
"preview": "wait --load domcontentloaded --timeout 10000"
},
{
"id": "cmd-003",
"argv": ["fill", "[placeholder=\"用户名\"]", "${METER_REBIND_USERNAME}"],
"preview": "fill \"[placeholder=\\\"用户名\\\"]\" \"${METER_REBIND_USERNAME}\""
},
{
"id": "cmd-004",
"argv": ["fill", "[placeholder=\"密码\"]", "${METER_REBIND_PASSWORD}"],
"preview": "fill \"[placeholder=\\\"密码\\\"]\" \"${METER_REBIND_PASSWORD}\""
},
{
"id": "cmd-005",
"argv": ["click", "input.anniu.un_butt.buttonClass"],
"preview": "click \"input.anniu.un_butt.buttonClass\""
},
{
"id": "cmd-006",
"argv": ["wait", "--url", "http://192.168.2.122:30264/#/*", "--timeout", "12000"],
"preview": "wait --url \"http://192.168.2.122:30264/#/*\" --timeout 12000"
},
{
"id": "cmd-007",
"argv": ["fill", "[placeholder=\"设备ID\"]", "${DEVICE_ID}"],
"preview": "fill \"[placeholder=\\\"设备ID\\\"]\" \"${DEVICE_ID}\""
}
],
"warnings": []
}
```
约定:
## 3. 字段说明
1. 命令行不带 `browserctl` 前缀,例如写 `click "button"`,不要写
`browserctl click "button"`。
2. `#` 开头的是注释,给人和下游技能作者看。
3. 变量占位使用 `${KEY}`。
4. 不要在文件里写真实密码、Token、Cookie。
| 字段 | 必填 | 说明 |
|---|---|---|
| `schemaVersion` | 是 | 当前固定为 `"1.0.0"` |
| `kind` | 是 | 固定为 `"browserctl-flow"` |
| `metadata` | 是 | 流程名称、标题、来源、时间 |
| `business` | 建议 | 业务描述、触发场景、成功标准 |
| `credentials` | 建议 | 登录地址、用户名、密码、Token 等运行配置建议 |
| `inputs` | 建议 | 设备 ID、工单号、日期等业务输入 |
| `runtimeBindings` | 条件必填 | 暂无法直接命令化的运行时引用;iframe 优先导出 `frame <locator>` |
| `commands` | 是 | 固定 `browserctl` 命令集 |
| `warnings` | 否 | selector 脆弱、遮罩点击、iframe 等诊断 |
| `quality` | 建议 | `ready` / `needs_review` / `blocked` 质量状态 |
## 3. 常用动作怎么导出
`commands[].argv` 是唯一的机器执行依据。`preview` 只是给人看,平台可以忽略。
| 用户动作 | 导出命令 |
## 4. 常用动作怎么导出
| 用户动作 | `argv` |
|---|---|
| 打开起始页 | `open "${PREFIX_BASE_URL}"` |
| 等待页面加载 | `wait --load domcontentloaded --timeout 10000` |
| 输入文本 | `fill "<selector>" "<value-or-placeholder>"` |
| 追加输入 | `type "<selector>" "<text>"` |
| 点击 | `click "<selector>"` |
| 双击 | `dblclick "<selector>"` |
| 悬停 | `hover "<selector>"` |
| 下拉选择 | `select "<selector>" "<value>"` |
| 勾选 | `check "<selector>"` |
| 取消勾选 | `uncheck "<selector>"` |
| 按键 | `press "<key>" ["<selector>"]` |
| 滚动 | `scroll --by <px>` |
| 等 URL 变化 | `wait --url "<glob>" --timeout <ms>` |
| 等元素出现 | `wait --selector "<selector>" --timeout <ms>` |
| 等文本出现 | `wait --text "<text>" --timeout <ms>` |
| 固定等待 | `wait --ms <ms>` |
| 打开起始页 | `["open", "${PREFIX_BASE_URL}"]` |
| 等待页面加载 | `["wait", "--load", "domcontentloaded", "--timeout", "10000"]` |
| 输入文本 | `["fill", "<selector>", "<value-or-placeholder>"]` |
| 追加输入 | `["type", "<selector>", "<text>"]` |
| 点击 | `["click", "<selector>"]` |
| 双击 | `["dblclick", "<selector>"]` |
| 悬停 | `["hover", "<selector>"]` |
| 下拉选择 | `["select", "<selector>", "<value>"]` |
| 勾选 | `["check", "<selector>"]` |
| 取消勾选 | `["uncheck", "<selector>"]` |
| 按键 | `["press", "<key>", "<selector>"]` |
| 滚动 | `["scroll", "--by", "<px>"]` |
| 等 URL 变化 | `["wait", "--url", "<glob>", "--timeout", "<ms>"]` |
| 等元素出现 | `["wait", "--selector", "<selector>", "--timeout", "<ms>"]` |
| 等文本出现 | `["wait", "--text", "<text>", "--timeout", "<ms>"]` |
| 固定等待 | `["wait", "--ms", "<ms>"]` |

@@ -83,3 +158,3 @@ 关键规则:

## 4. selector 建议
## 5. selector 建议

@@ -102,31 +177,36 @@ 插件端负责生成 selector。建议优先级:

如果只能导出脆弱 selector,请保留命令,但加 warning 注释:
如果只能导出脆弱 selector,请保留命令,但加入 warning:
```browserctl
# warning: UNSTABLE_SELECTOR reason="selector uses nth-child"
click "body > div:nth-child(1) > div:nth-child(2) ..."
```json
{
"code": "UNSTABLE_SELECTOR",
"commandId": "cmd-012",
"message": "selector uses nth-child and may break after layout changes"
}
```
## 5. credentials 和业务输入要分开
## 6. credentials 和业务输入要分开
登录配置属于 credentials:
登录配置属于 `credentials`:
```browserctl
# credentials:
# - METER_REBIND_BASE_URL url secret=false
# - METER_REBIND_USERNAME username secret=false
# - METER_REBIND_PASSWORD password secret=true
open "${METER_REBIND_BASE_URL}"
fill "[placeholder=\"用户名\"]" "${METER_REBIND_USERNAME}"
fill "[placeholder=\"密码\"]" "${METER_REBIND_PASSWORD}"
```json
{
"key": "METER_REBIND_PASSWORD",
"label": "登录密码",
"type": "password",
"required": true,
"secret": true
}
```
业务参数不属于 credentials,例如设备 ID、工单号、日期范围:
业务参数属于 `inputs`,例如设备 ID、工单号、日期范围:
```browserctl
# business-inputs:
# - DEVICE_ID default=1000000150601081283 help="设备 ID"
fill "[placeholder=\"设备ID\"]" "${DEVICE_ID}"
```json
{
"key": "DEVICE_ID",
"label": "设备 ID",
"type": "text",
"required": true,
"defaultValue": "1000000150601081283"
}
```

@@ -141,11 +221,98 @@

| 密码、Token、Cookie、Secret | credential,且 `secret=true` |
| 设备 ID、工单号、日期、客户编号 | business input |
| 固定菜单名、固定按钮名 | 直接写入命令 |
| 设备 ID、工单号、日期、客户编号 | input |
| 固定菜单名、固定按钮名 | 直接写入 command |
## 6. 不要导出这些内容
## 7. iframe 怎么导出
iframe 的录制时 frame id 不能直接复用。插件应导出 `browserctl frame <locator>`,用
稳定 locator 切换 active frame,然后继续导出普通 `click` / `fill` / `get` / `find`
命令;离开 iframe 时导出 `["frame", "main"]`。
推荐格式:
```json
{
"commands": [
{
"id": "cmd-008",
"argv": ["frame", "[role=\"dialog\"] iframe"],
"metadata": {
"frameUrlGlob": "http://10.172.246.234:19901/dky/home-assets/*.html*"
}
},
{
"id": "cmd-009",
"argv": ["click", "[data-testid=\"nav-paas\"]"]
},
{
"id": "cmd-010",
"argv": ["frame", "main"]
}
]
}
```
规则:
1. `frame <locator>` 的 locator 可以是 iframe owner CSS、`@eN`、`url:<glob>` 或
`name:<name>`;推荐 CSS owner selector,尤其是弹框容器 + iframe。
2. 动态 iframe URL 使用 glob,例如
`url:http://10.172.246.234:19901/dky/home-assets/*.html*`。
3. 切入 active frame 后,后续普通命令作用于该 frame;回主文档必须导出
`["frame", "main"]`。
4. iframe 不应再通过 `${FRAME_*}` 和 `runtimeBindings.frames` 表达。
5. 如果插件无法可靠定位 iframe,应设置 `quality.status="blocked"`,并加入
`IFRAME_UNRESOLVED` 或对应 blocking issue。
## 8. 多 tab 怎么导出
多 tab 不能靠“当前激活标签页”猜。插件必须把 tab 创建、切换、关闭写成明确命令。
用户动作主动打开新页时:
```json
[
{
"id": "open-detail-tab",
"argv": ["tab", "new", "https://example.com/detail"],
"capture": { "tabId": "DETAIL_TAB" }
},
{
"id": "switch-detail-tab",
"argv": ["tab", "${DETAIL_TAB}"]
}
]
```
点击后产生新页时:
```json
[
{
"id": "click-open-detail",
"argv": ["click", "[data-testid=\"open-detail\"]", "--new-tab"],
"capture": { "tabId": "DETAIL_TAB" }
},
{
"id": "switch-detail-tab",
"argv": ["tab", "${DETAIL_TAB}"]
}
]
```
规则:
1. `tab` 引用使用稳定 ID 或 label,不使用数字下标。
2. `capture.tabId` 捕获的是命令响应里的 `data.tabId`。
3. 后续切换必须使用完整 token:`["tab", "${DETAIL_TAB}"]`。
4. 如果无法确认某一步属于哪个 tab,应设置 `quality.status="blocked"`,并加入
`MULTI_TAB_UNRESOLVED` 或对应 blocking issue。
5. 只有确实无法安全表达时才使用 `MULTI_TAB_UNSUPPORTED`。
## 9. 不要导出这些内容
不要导出真实敏感信息:
```browserctl
fill "[placeholder=\"密码\"]" "real-password"
```json
["fill", "[placeholder=\"密码\"]", "real-password"]
```

@@ -155,4 +322,4 @@

```browserctl
fill "[placeholder=\"密码\"]" "${METER_REBIND_PASSWORD}"
```json
["fill", "[placeholder=\"密码\"]", "${METER_REBIND_PASSWORD}"]
```

@@ -162,4 +329,4 @@

```browserctl
navigate "http://example.com/#/target"
```json
["navigate", "http://example.com/#/target"]
```

@@ -169,30 +336,41 @@

```browserctl
click "<触发跳转的按钮 selector>"
wait --url "http://example.com/#/target*" --timeout 12000
```json
["click", "<触发跳转的按钮 selector>"]
["wait", "--url", "http://example.com/#/target*", "--timeout", "12000"]
```
不要默认录制 loading mask 点击:
不要默认录制 loading mask 点击。如果确实发生了这种点击,建议跳过或写 warning。
```browserctl
click "div.el-loading-mask.el-loading-fade-leave"
```
## 10. 数字员工客户端如何使用
如果确实发生了这种点击,建议跳过或写 warning 注释。
数字员工客户端读取该 JSON 后,可以生成 skill:
## 7. 交付检查清单
1. 将 `credentials` 写入 `SKILL.md` frontmatter。
2. 将 `business.description`、`business.successCriteria` 写入技能说明。
3. 将 `inputs` 写成技能运行所需业务输入。
4. 将 `commands[].argv` 固定为浏览器执行步骤。
5. 执行前解析 `credentials`、`inputs` 和必要的 `runtimeBindings`,再提交给 batch。
6. 在技能正文中要求模型不要重新规划页面操作,不要把 `click` 改成 `navigate`。
如果 `quality.status="blocked"`,客户端不得自动生成可执行 skill,也不得直接回放。
## 11. 交付检查清单
导出文件交付前,请检查:
- 文件是 UTF-8 文本。
- 每一条有效行都是 `browserctl` 子命令。
- 文件是合法 JSON。
- `kind` 是 `browserctl-flow`。
- `commands[].argv` 均为字符串数组。
- 没有真实密码、Token、Cookie。
- 登录信息使用 `${...}` 占位。
- credentials 和 business inputs 已在注释里列出。
- credentials 和 inputs 已分开。
- 命令里所有 `${KEY}` 都能在 `credentials`、`inputs` 或必要的 `runtimeBindings` 中找到;iframe 不使用 `${FRAME_*}`。
- 点击后跳转使用 `click + wait --url`。
- 多 tab 流程有明确 `tab` 命令和 `capture.tabId`。
- 关键页面变化有等待命令。
- 脆弱 selector 有 warning 注释。
- 录制结果能被人读懂,方便后续固化为业务技能。
- 脆弱 selector 有 warning。
- `quality.status=blocked` 时必须带 blocking issue。
- 业务描述足够让客户端生成 skill。
## 8. 参考文档
## 12. 参考文档

@@ -206,2 +384,1 @@ 完整命令语法见:

本文档只说明浏览器录制插件应该导出什么。插件内部如何实现录制,不受本文档约束。

@@ -1,19 +0,25 @@

# Flow JSON 到 browserctl 命令集过渡方案
# Flow JSON 到 browserctl-flow.json 完整转换方案
## 1. 背景
长期目标是浏览器录制插件直接导出 `browserctl` 命令集。但为了快速上线,避免立刻
重构插件录制链路,允许保留现有标准化流程 JSON 导出,并增加一个转换程序:
长期目标是浏览器录制插件直接导出 `browserctl-flow.json`。但为了快速上线,避免
立刻重构插件录制链路,允许保留现有标准化流程 JSON 导出,并增加一个转换程序:
```text
Flow JSON -> browserctl 命令集 + 转换报告
Flow JSON -> browserctl-flow.json + 转换报告
```
该方案是过渡方案,不改变最终方向。插件端可以先继续导出 JSON;下游通过转换器把
JSON 转成 `.browserctl` 命令集,用于人工审查、回放和后续业务技能固化。
该方案是过渡方案,不改变最终方向。插件端可以先继续导出现有 JSON;下游通过转换器
把 JSON 转成平台可识别的 `browserctl-flow.json`,用于人工审查、回放和后续业务
技能固化。
注意:转换器必须按完整产品能力实现,不能只覆盖登录、输入、点击等简单路径。
凡是 Flow JSON schema 中定义的步骤类型,都必须有明确转换策略、降级策略或结构化
阻断结果。不能安全转换的流程必须在输出中标记为不可直接生成技能,而不是生成一份
看似可用但实际会失败的半成品。
## 2. 目标
1. 输入现有 `schemaVersion: "1.0.0"` 流程 JSON。
2. 输出一份 `.browserctl` 命令集。
2. 输出一份 `browserctl-flow.json`。
3. 输出一份转换报告,说明步骤映射、变量、credentials 建议、warnings 和人工确认项。

@@ -23,2 +29,5 @@ 4. 尽量保留真实用户操作语义,例如点击后跳转转换为 `click + wait --url`。

6. 对脆弱 selector、重复 navigate、遮罩点击等问题给出诊断。
7. 对 iframe、多 tab、postMessage、popup、scroll、keydown、change 等复杂场景给出
可执行转换或 blocking issue。
8. 输出的 `browserctl-flow.json` 必须包含 `quality.status`,明确可用性。

@@ -29,3 +38,3 @@ 非目标:

2. 不生成业务技能目录。
3. 不保证所有录制流程零人工修改即可稳定回放。
3. 不吞掉无法转换的步骤;无法安全转换时必须输出 blocking issue。

@@ -60,30 +69,67 @@ ## 3. 输入文件

```text
<flow-name>.browserctl
<flow-name>.browserctl-flow.json
<flow-name>.conversion.md
```
`.browserctl` 示例:
`browserctl-flow.json` 示例:
```browserctl
# browserctl-recording: 1.0
# generated-from: flow-json
# source-schema-version: 1.0.0
# name: meter-rebind
#
# credentials:
# - METER_REBIND_BASE_URL url secret=false
# - METER_REBIND_USERNAME username secret=false
# - METER_REBIND_PASSWORD password secret=true
#
# business-inputs:
# - DEVICE_ID default=1000000150601081283 help="设备 ID"
open "${METER_REBIND_BASE_URL}"
wait --load domcontentloaded --timeout 10000
fill "[placeholder=\"用户名\"]" "${METER_REBIND_USERNAME}"
fill "[placeholder=\"密码\"]" "${METER_REBIND_PASSWORD}"
click "input.anniu.un_butt.buttonClass"
wait --url "http://192.168.2.122:30264/#/*" --timeout 12000
fill "[placeholder=\"设备ID\"]" "${DEVICE_ID}"
click "button.el-button.el-button--primary"
```json
{
"schemaVersion": "1.0.0",
"kind": "browserctl-flow",
"metadata": {
"name": "meter-rebind",
"source": "flow-json-converter"
},
"credentials": [
{
"key": "METER_REBIND_BASE_URL",
"label": "系统登录地址",
"type": "url",
"required": true,
"secret": false
},
{
"key": "METER_REBIND_USERNAME",
"label": "登录用户名",
"type": "username",
"required": true,
"secret": false
},
{
"key": "METER_REBIND_PASSWORD",
"label": "登录密码",
"type": "password",
"required": true,
"secret": true
}
],
"inputs": [
{
"key": "DEVICE_ID",
"label": "设备 ID",
"type": "text",
"defaultValue": "1000000150601081283"
}
],
"commands": [
{
"id": "cmd-001",
"argv": ["open", "${METER_REBIND_BASE_URL}"]
},
{
"id": "cmd-002",
"argv": ["wait", "--load", "domcontentloaded", "--timeout", "10000"]
},
{
"id": "cmd-003",
"argv": ["fill", "[placeholder=\"用户名\"]", "${METER_REBIND_USERNAME}"]
}
],
"quality": {
"status": "ready",
"warnings": [],
"blockingIssues": []
}
}
```

@@ -100,2 +146,3 @@

7. 人工确认项。
8. blocking issues。

@@ -121,2 +168,3 @@ ## 5. 推荐 CLI

--max-fixed-wait <ms> # 默认 5000
--fail-on-blocking # 默认 true
```

@@ -133,6 +181,8 @@

C --> D["推断变量和 credentials"]
D --> E["生成 browserctl 命令"]
E --> F["插入 wait"]
F --> G["输出 .browserctl"]
G --> H["输出转换报告"]
D --> E["解析 frame/tab 上下文"]
E --> F["生成 browserctl 命令"]
F --> G["插入 wait"]
G --> H["质量评估"]
H --> I["输出 browserctl-flow.json"]
I --> J["输出转换报告"]
```

@@ -164,13 +214,13 @@

| Flow JSON type | browserctl 输出 |
| Flow JSON type | `commands[].argv` 输出 |
|---|---|
| `input` | `fill "<selector>" "<value-or-placeholder>"` |
| `input` | `["fill", "<selector>", "<value-or-placeholder>"]` |
| `change` | `select` / `check` / `uncheck` / `fill` |
| `click` | `click "<selector>"` |
| `click` | `["click", "<selector>"]` |
| `submit` | `click "<selector>"` 或 `eval "form.requestSubmit()"` |
| `keydown` | `press "<key>" ["<selector>"]` |
| `keydown` | `["press", "<key>", "<selector>"]` |
| `scroll` | `scroll --by <px>` 或 `eval "window.scrollTo(x,y)"` |
| `navigate` | 通常转成 `wait --url` 或跳过;必要时 `navigate "<url>"` |
| `postmessage` | `eval "window.postMessage(...)"` |
| `wait` | `wait --ms <ms>` |
| `wait` | `["wait", "--ms", "<ms>"]` |

@@ -184,2 +234,147 @@ 关键规则:

### 7.1 `input`
转换为:
```json
["fill", "<selector>", "<value-or-placeholder>"]
```
规则:
1. 密码字段必须转成 credential placeholder。
2. 用户名字段默认转成 credential placeholder。
3. 业务值按第 9 节处理。
4. 空字符串输入保留,但输出 warning:`EMPTY_INPUT_VALUE`。
### 7.2 `change`
按目标元素推断:
| 目标 | 输出 |
|---|---|
| `SELECT` | `["select", "<selector>", "<value>"]` |
| checkbox,值为 true | `["check", "<selector>"]` |
| checkbox,值为 false | `["uncheck", "<selector>"]` |
| radio | `["check", "<selector>"]` |
| 普通 input | `["fill", "<selector>", "<value>"]` |
| 无法判断 | `eval` 派发 `input/change`,并 warning |
无法判断元素类型时允许生成:
```json
["eval", "const el=document.querySelector('...'); el.value='...'; el.dispatchEvent(new Event('change',{bubbles:true}))"]
```
但必须输出 warning:`CHANGE_FALLBACK_TO_EVAL`。
### 7.3 `click`
转换为:
```json
["click", "<selector>"]
```
规则:
1. 如果有 `postCondition.topUrl`,点击后追加 `wait --url`。
2. 如果下一步是同 URL `navigate`,该 navigate 跳过。
3. 疑似 loading mask / overlay 点击默认不输出 click,转为 skipped step,并产生 warning。
4. 如果 `--keep-overlay-clicks`,保留命令但 warning。
### 7.4 `submit`
如果 target 是按钮或可点击元素:
```json
["click", "<selector>"]
```
如果 target 是 form:
```json
["eval", "document.querySelector('<selector>').requestSubmit()"]
```
并输出 warning:`SUBMIT_FALLBACK_TO_EVAL`。
### 7.5 `keydown`
转换为:
```json
["press", "<key>", "<selector>"]
```
没有 target 时:
```json
["press", "<key>"]
```
修饰键映射:
| JSON 字段 | argv |
|---|---|
| `ctrlKey` | `--ctrl` |
| `shiftKey` | `--shift` |
| `altKey` | `--alt` |
| `metaKey` | `--meta` |
### 7.6 `scroll`
如果是相对滚动:
```json
["scroll", "--by", "<deltaY>"]
```
如果是绝对位置:
```json
["eval", "window.scrollTo(x,y)"]
```
如果是元素内滚动,优先生成带 selector 的 `scroll`;无法定位元素则 blocking:
`SCROLL_TARGET_UNRESOLVED`。
### 7.7 `navigate`
默认不是直接转 `navigate`。处理规则:
| 场景 | 行为 |
|---|---|
| 起始导航 | 由 `session.startUrl` 生成 `open` |
| click 后 URL 变化 | 转为前一步后的 `wait --url` |
| 重复 navigate | 跳过并 warning |
| 用户显式地址栏跳转 | `["navigate", "<url>"]` |
| `--preserve-navigate` | 保留所有 navigate |
### 7.8 `postmessage`
转换为:
```json
["eval", "window.postMessage(<json>, <targetOrigin>)"]
```
规则:
1. `data` 必须 JSON 序列化。
2. 缺少 `targetOrigin` 时使用 `"*"` 并 warning。
3. iframe 内 postMessage 需要 frame 上下文;无法确定时 blocking:
`POSTMESSAGE_FRAME_UNRESOLVED`。
### 7.9 `wait`
转换为:
```json
["wait", "--ms", "<waitMs>"]
```
如果 wait 太长,输出 `FIXED_WAIT_TOO_LONG`。如果可从下一步推断 selector/url,应报告建议
改为明确等待。
## 8. selector 选择策略

@@ -207,6 +402,6 @@

```browserctl
find fill --placeholder "用户名" "${METER_REBIND_USERNAME}"
find click --text "查询"
find click --testid submit-btn
```json
["find", "fill", "--placeholder", "用户名", "${METER_REBIND_USERNAME}"]
["find", "click", "--text", "查询"]
["find", "click", "--testid", "submit-btn"]
```

@@ -216,4 +411,4 @@

```browserctl
find alt "logo" click
```json
["find", "alt", "logo", "click"]
```

@@ -234,9 +429,10 @@

输出文件头部写建议:
输出到 `credentials`:
```browserctl
# credentials:
# - METER_REBIND_BASE_URL url secret=false
# - METER_REBIND_USERNAME username secret=false
# - METER_REBIND_PASSWORD password secret=true
```json
[
{ "key": "METER_REBIND_BASE_URL", "type": "url", "secret": false },
{ "key": "METER_REBIND_USERNAME", "type": "username", "secret": false },
{ "key": "METER_REBIND_PASSWORD", "type": "password", "secret": true }
]
```

@@ -252,7 +448,19 @@

```browserctl
# business-inputs:
# - DEVICE_ID default=1000000150601081283 help="设备 ID"
fill "[placeholder=\"设备ID\"]" "${DEVICE_ID}"
```json
{
"inputs": [
{
"key": "DEVICE_ID",
"label": "设备 ID",
"type": "text",
"defaultValue": "1000000150601081283"
}
],
"commands": [
{
"id": "cmd-device-id",
"argv": ["fill", "[placeholder=\"设备ID\"]", "${DEVICE_ID}"]
}
]
}
```

@@ -283,3 +491,3 @@

过渡方案先保守处理。
转换器必须识别 iframe 和多 tab。不能静默丢弃相关步骤。

@@ -290,13 +498,46 @@ ### iframe

1. 如果 selector 可以通过普通 `snapshot @eN` 回放,报告说明优先人工确认。
2. 如果需要 CSS selector 进入 frame,输出 `IFRAME_REQUIRES_REVIEW`。
3. 可生成建议命令:
1. 录制 `frameUrl` 应写入 command metadata,优先泛化为 `frameUrlGlob`。
2. 转换器必须生成 `["frame", "<locator>"]` 切换 active frame,然后输出普通
`click` / `fill` / `get` / `find` / `wait` / `eval` 等 argv。
3. 同一个 iframe 内连续步骤不必重复 `frame <locator>`;离开 iframe 或回到 top frame 时
输出 `["frame", "main"]`。
4. 录制时 `frameId` 不能复用,不能生成 `${FRAME_*}` 占位符或
`runtimeBindings.frames`。
5. 如果无法生成可执行 frame locator,输出 blocking issue。
```browserctl
# warning: IFRAME_REQUIRES_REVIEW frameUrl="https://example.com/frame.html"
frame click "<runtime-frame-id>" "<selector>"
```json
{
"commands": [
{
"id": "cmd-frame-001",
"argv": ["frame", "[role=\"dialog\"] iframe"],
"metadata": {
"frameUrl": "https://example.com/frame.html",
"frameUrlGlob": "https://example.com/frame.html*"
}
},
{
"id": "cmd-frame-002",
"argv": ["click", "<selector>"]
},
{
"id": "cmd-frame-003",
"argv": ["frame", "main"]
}
]
}
```
录制时的 `frameId` 不可直接复用。
locator 生成优先级:
1. iframe owner 的稳定 CSS selector,例如 `"[role=\"dialog\"] iframe"`。
2. snapshot 中可确认是 iframe owner 的 `@eN`。
3. `url:<glob>`,适合动态详情 iframe,例如
`url:http://10.172.246.234:19901/dky/home-assets/*.html*`。
4. `name:<name>`,仅在 frame name 稳定且唯一时使用。
转换器不再要求运行器先执行 `browserctl frames` 并替换 frame id。运行器只需要按顺序执行
batch JSON;frame 定位失败时由 `browserctl frame <locator>` 返回 `FRAME_NOT_FOUND`、
`FRAME_AMBIGUOUS` 或 `ELEMENT_NOT_FOUND`。
### 多 tab

@@ -306,6 +547,44 @@

1. 输出 `MULTI_TAB_REQUIRES_REVIEW`。
2. 不默认生成跨 tab 命令。
3. 可在报告中建议拆成多个流程。
1. 必须在报告中列出 tab 分组。
2. 执行器支持 `tab list/new/switch/close`,并可通过 `commands[].capture`
捕获 `tab new` 响应中的 `data.tabId`,供后续完整 `${KEY}` argv token 使用。
3. 如果录制数据能明确表达 tab 创建来源,转换器应输出 `tab new` 或
`click --new-tab`,并用 `capture.tabId` 绑定后续 tab 切换。
4. 如果无法确定某一步属于哪个 tab,输出 blocking issue:
`MULTI_TAB_UNRESOLVED`。
5. 只有当前 `browserctl` 运行环境缺少 tab 能力时,才输出
`MULTI_TAB_UNSUPPORTED`。
6. 可建议拆成多个 flow,但不能把多 tab 步骤静默合并到一个 tab,也不能通过
当前 Tab 或 tabs diff 猜测目标。
显式创建新 tab 时:
```json
{
"commands": [
{
"id": "create-detail-tab",
"argv": ["tab", "new", "https://example.com/detail"],
"capture": { "tabId": "DETAIL_TAB" }
},
{ "id": "switch-detail-tab", "argv": ["tab", "${DETAIL_TAB}"] }
]
}
```
点击产生新 tab 时:
```json
{
"commands": [
{
"id": "click-open-detail",
"argv": ["click", "[data-testid=\"open-detail\"]", "--new-tab"],
"capture": { "tabId": "DETAIL_TAB" }
},
{ "id": "switch-detail-tab", "argv": ["tab", "${DETAIL_TAB}"] }
]
}
```
## 12. 遮罩和无效点击

@@ -341,3 +620,3 @@

## 输出文件
- meter-rebind.browserctl
- meter-rebind.browserctl-flow.json

@@ -378,6 +657,45 @@ ## credentials 建议

## 15. 验收标准
## 15. Blocking issue 列表
Blocking issue 表示转换器不能保证输出 flow 可直接生成可用 skill。默认
`--fail-on-blocking=true` 时,存在 blocking issue 应返回非零退出码,但仍输出报告。
| code | 含义 |
|---|---|
| `UNSUPPORTED_STEP_TYPE` | 未知步骤类型 |
| `TARGET_UNRESOLVED` | 必需 target 无法定位 |
| `IFRAME_UNRESOLVED` | iframe 步骤无法生成可执行命令 |
| `MULTI_TAB_UNRESOLVED` | 无法确定步骤所属 tab 或新 tab 来源 |
| `MULTI_TAB_UNSUPPORTED` | 当前 browserctl 运行环境缺少 tab 能力 |
| `POSTMESSAGE_FRAME_UNRESOLVED` | iframe 内 postMessage 无法定位 frame |
| `SCROLL_TARGET_UNRESOLVED` | 元素内滚动目标无法定位 |
| `SECRET_POLICY_VIOLATION` | secret 策略禁止输出 |
| `COMMAND_UNSUPPORTED_BY_BROWSERCTL` | 需要的 browserctl 命令不存在 |
## 16. 输出质量状态
`browserctl-flow.json` 必须包含:
```json
{
"quality": {
"status": "ready",
"warnings": [],
"blockingIssues": []
}
}
```
状态含义:
| status | 含义 |
|---|---|
| `ready` | 无 blocking issue,可用于生成 skill |
| `needs_review` | 有 warning,但无 blocking issue |
| `blocked` | 存在 blocking issue,不应直接生成 skill |
## 17. 验收标准
1. 能读取现有 `schemaVersion: "1.0.0"` Flow JSON。
2. 能输出 `.browserctl` 命令集。
2. 能输出 `browserctl-flow.json`。
3. 能输出转换报告。

@@ -390,4 +708,7 @@ 4. 不泄露真实密码、Token、Cookie。

9. 脆弱 selector、iframe、多 tab、遮罩点击有 warning。
10. Flow JSON 中每个已知 type 都有转换测试。
11. iframe、tab 归属不明、无法定位 target 等不可安全转换场景必须产生 blocking issue。
12. `quality.status` 与 warnings/blockingIssues 一致。
## 16. 与最终方案的关系
## 18. 与最终方案的关系

@@ -399,3 +720,3 @@ 这是快速上线的过渡方案。

```text
浏览器插件直接导出 browserctl 命令集
浏览器插件直接导出 browserctl-flow.json
```

@@ -406,6 +727,5 @@

```text
浏览器插件继续导出 Flow JSON -> 转换器生成 browserctl 命令集
浏览器插件继续导出 Flow JSON -> 转换器生成 browserctl-flow.json
```
迁移完成后,可以移除转换器,插件直接产出 `.browserctl`。
迁移完成后,可以移除转换器,插件直接产出 `browserctl-flow.json`。

@@ -55,2 +55,10 @@ # browserctl 命令参考

| `ELEMENT_OCCLUDED` | `click`/`dblclick`/`find … click` 时目标中心被其它元素遮挡(Cookie 横幅、模态框等) |
| `TAB_NOT_FOUND` | automation Tab 已关闭或给定 tabId/label 不存在 |
| `TAB_LIMIT_EXCEEDED` | 当前 session 已达到 Tab 上限 |
| `TAB_LABEL_CONFLICT` | Tab label 已被当前 session 占用 |
| `TAB_BUSY` | Tab 正在执行操作或等待确认,暂时不能关闭 |
| `TAB_FOCUS_CHANGED` | 命令开始前 automation Tab 已变化,拒绝在错误页面执行 |
| `NEW_TAB_UNSUPPORTED` | 当前宿主无法可靠捕获 `click --new-tab` 创建的新页面 |
| `BATCH_CAPTURE_MISSING` | batch 响应缺少声明要捕获的 `data` 字段 |
| `BATCH_BINDING_UNRESOLVED` | batch argv 引用了尚未捕获的 `${KEY}` |
| `OPTION_NOT_FOUND` | `select` 下拉项按 value/label 都未匹配到 |

@@ -73,3 +81,3 @@ | `NOT_CHECKABLE` | `check`/`uncheck` 目标非 checkbox/radio,无法勾选 |

- `TIMEOUT`:优先使用明确等待条件替代固定等待,例如 `wait --selector`、`wait --text`、`wait --url`、`wait --load`、`wait --fn`。
- iframe:优先 `snapshot` 取 `@eN` 操作;iframe 内 CSS 选择器用 `frames` + `frame click/fill/get`(见「iframe 与 frame 命令」)。
- iframe:优先用稳定 iframe owner selector 执行 `browserctl frame <locator>`,再用普通 `click` / `fill` / `get` / `find` / `eval`;离开 iframe 用 `browserctl frame main`。

@@ -107,5 +115,9 @@ ## ⚠️ 关键约定:不要把 `health` 当门禁

browserctl frames
browserctl frame snapshot <frame> [--max-nodes N] [--compact|-c] [--depth N|-d N] [--scope <sel>|-s <sel>] [--tree|--interactive]
browserctl click <@eN|selector> [--confirm "确认文案"]
browserctl frame click <frame> <@eN|selector> [--confirm "确认文案"]
browserctl frame <selector|@eN|url:<glob>|name:<name>> [--timeout 10000]
browserctl frame main
browserctl tab
browserctl tab new [url] [--label <name>]
browserctl tab <tN|label>
browserctl tab close [tN|label]
browserctl click <@eN|selector> [--new-tab] [--confirm "确认文案"]
browserctl wait --selector <css> [--state visible|hidden] # 等元素出现/隐藏(默认超时 10s,--timeout 改)

@@ -128,3 +140,2 @@ browserctl wait --text <文本> # 等文本出现在页面

browserctl fill <@eN|selector> --text-stdin # 从管道读取文本(echo ... | browserctl fill ...)
browserctl frame fill <frame> <@eN|selector> (<text> | --text-file <path> | --text-stdin)
browserctl hover <@eN|selector> # 鼠标悬停到元素中心(单次 mouseMoved,不点击)

@@ -148,11 +159,10 @@ browserctl dblclick <@eN|selector> # 双击(clickCount:2 的 pressed+released)

browserctl get html <@eN|selector> # 读元素 innerHTML
browserctl get count <@eN|selector> # @eN→1;CSS 选择器→主 frame querySelectorAll 长度
browserctl get count <@eN|selector> # @eN→1;CSS 选择器→当前 frame querySelectorAll 长度
browserctl get box <@eN|selector> # getBoundingClientRect → { x, y, width, height }
browserctl get styles <@eN|selector> # 全量 computed styles → { styles: { ... } }
browserctl get attr <@eN|selector> <name> # 读元素属性(href/src/aria-* 等),不存在返回 null
browserctl frame get value|text|html|count|box|styles|attr|attribute <frame> <@eN|selector> [name]
browserctl is visible <@eN|selector> # 元素存在且可见 → { result: true };存在但 hidden → { result: false };不存在 → ELEMENT_NOT_FOUND
browserctl is enabled <@eN|selector> # 元素存在且未 disabled → { result: true/false }
browserctl is checked <@eN|selector> # 可勾选 → { result: true/false };非 checkbox/radio → NOT_CHECKABLE
browserctl find role <role> <action> [value] [--name <name>] [--exact] # 语义定位(主 frame;不依赖 snapshot @eN)
browserctl find role <role> <action> [value] [--name <name>] [--exact] # 语义定位(当前 frame;不依赖 snapshot @eN)
browserctl find text|label <text> <action> [value] [--exact]

@@ -172,2 +182,20 @@ browserctl find placeholder|testid|alt|title <query> <action> [value]

### Tab 与操作焦点
命令面与 agent-browser 一致,采用“先切换、再操作”:
```bash
browserctl tab new --label docs https://docs.example.com
browserctl tab docs
browserctl snapshot --interactive
browserctl click @e3
browserctl tab close docs
```
`tab` 列出当前 session 的标签页以及 `automationTabId` / `visibleTabId`。`tab <tN|label>` 同时切换 automation 与 visible;用户在内嵌界面点击 Tab 只改变 visible,不会改变 Agent 后续命令的目标。成功响应包含实际 `tabId`。
Tab 使用稳定 ID `t1`、`t2`……或创建时的唯一 label;`tab 2` 会返回教学型 `CLI_USAGE_ERROR`,不会按数组下标猜测。CLI 不提供公开 `--tab` 或 `BROWSER_RUNTIME_TAB`。
`click <ref> --new-tab` 等待宿主捕获本次点击创建的 PageRuntime;捕获不可靠时明确返回 `NEW_TAB_UNSUPPORTED`。
### 指针与表单交互

@@ -222,3 +250,3 @@

`get count`:`@eN` 存在时恒为 `1`;CSS 选择器在主 frame `document.querySelectorAll`。`get styles` 返回全量 computed 属性,JSON 较大,断言时优先取关键字段。
`get count`:`@eN` 存在时恒为 `1`;CSS 选择器在当前 frame `document.querySelectorAll`。`get styles` 返回全量 computed 属性,JSON 较大,断言时优先取关键字段。

@@ -230,10 +258,11 @@ ### iframe 与 frame 命令

1. **`snapshot` → `@eN`**(推荐):同源与 OOPIF iframe 内节点会出现在 refs 里,后续用普通 `click` / `fill` / `get` + `@eN`。
2. **`browserctl frame …`**:需要在 iframe 内用 **CSS 选择器** 时,先 `frames` 取 `frameId`,再 `frame click/fill/get …`。
2. **`browserctl frame …`**:需要在 iframe 内用 **CSS 选择器** 时,先切换 active frame,再执行普通命令。
```bash
browserctl frames
browserctl frame snapshot <frameId> --interactive
browserctl frame click <frameId> "#submit"
browserctl frame fill <frameId> "input[name=q]" "关键词"
browserctl frame get text <frameId> ".result-title"
browserctl frame "[role=\"dialog\"] iframe"
browserctl snapshot --interactive
browserctl click "#submit"
browserctl fill "input[name=q]" "关键词"
browserctl get text ".result-title"
browserctl frame main
```

@@ -243,4 +272,5 @@

- 普通 `click` / `fill` / `get` 的 CSS 选择器**只查主 document**;iframe 内 selector 用 `frame …`。
- `find` **只在主 frame** 定位,不跨 iframe。
- `frame <locator>` 的 CSS locator 从 main frame 查找 iframe/frame owner;推荐录制插件输出弹框容器 + iframe 的稳定 selector。
- 切入 active frame 后,普通 `click` / `fill` / `get` / `is` / `wait --selector` / `wait --text` / `wait --fn` / `eval` / `find` 作用于该 frame。
- `open` / `navigate` / `back` / `forward` / `reload` 会回到主文档上下文;显式返回主文档用 `frame main`。
- `screenshot --annotate` 时,OOPIF 跨源 iframe 的 `@eN` 可能无法标号(静默跳过)。

@@ -250,3 +280,3 @@

无需先 `snapshot`;在主 frame 内定位后直接走 objectId 路径(**不**转成 `@eN`)。
无需先 `snapshot`;默认在主文档定位,执行 `frame <locator>` 后在 active frame 内定位(**不**转成 `@eN`)。

@@ -308,2 +338,3 @@ **positional 模式(strategy 在前):**

browserctl batch "get url" "snapshot --interactive"
browserctl batch "tab docs" "snapshot" "click @e3"
browserctl batch --bail "open https://example.com" "click @e1"

@@ -315,2 +346,19 @@ echo '[["get","url"],["wait","--load","domcontentloaded"]]' | browserctl batch --json

对象 item 支持 `{ id?, argv, capture? }`。`capture` 的键从当前命令
`result.data` 读取,值是后续完整 argv token 使用的变量名:
```json
[
{
"argv": ["tab", "new", "https://example.com/detail"],
"capture": { "tabId": "DETAIL_TAB" }
},
{ "argv": ["tab", "${DETAIL_TAB}"] },
{ "argv": ["snapshot", "--interactive"] }
]
```
缺字段返回 `BATCH_CAPTURE_MISSING`;未定义变量返回
`BATCH_BINDING_UNRESOLVED`,并停止后续命令。仅替换完整 `${KEY}` token。
### snapshot 与 screenshot 说明

@@ -317,0 +365,0 @@

{
"name": "browserctl-cli",
"version": "0.1.4",
"version": "0.1.5",
"description": "Self-contained browserctl CLI with auto-start Chrome/Edge daemon (no Electron required)",

@@ -5,0 +5,0 @@ "keywords": [

@@ -52,2 +52,6 @@ # browserctl-cli

browserctl eval (<js> | --file <path> | --stdin) [--timeout 10000]
browserctl tab
browserctl tab new [--label <name>] [url]
browserctl tab <tN|label>
browserctl tab close [tN|label]
browserctl click <@eN|selector> [--confirm "确认文案"]

@@ -80,2 +84,8 @@ browserctl fill <@eN|selector> (<text> | --text-file <path> | --text-stdin)

Tabs follow agent-browser's switch-then-operate style. Use stable `tN` IDs or
labels, never numeric indexes; run a fresh snapshot after switching. There is
no public `--tab` option. Structured `batch --json` items may capture
`data.tabId` and reuse it as a complete `${KEY}` argv token; see the command
reference for the schema and errors.
---

@@ -82,0 +92,0 @@

Sorry, the diff of this file is too big to display

# 浏览器录制插件导出 browserctl 命令集约定
## 1. 定位
浏览器录制插件的最终产物是一个 `browserctl` 命令集。插件如何监听事件、如何生成
selector、如何处理内部状态、是否使用 JSON 中间结构,均由插件开发端自行决定。
`browserctl-cli` 在这个方案中的职责只有两个:
1. 提供稳定、清晰、可执行的 `browserctl` 命令集。
2. 提供足够好的说明文档,让插件开发端知道应该导出什么命令、如何表达等待、
参数、凭证占位和业务输入。
`browserctl-cli` 不负责:
1. 提供浏览器插件 SDK。
2. 提供录制事件转换器。
3. 生成业务技能。
4. 规定插件内部数据结构。
## 2. 插件导出目标
插件最终应导出一份人可读、可复制、可固化到业务技能里的命令集。
推荐扩展名:
```text
<flow-name>.browserctl
```
文件内容是 UTF-8 文本:
```browserctl
# browserctl-recording: 1.0
# name: meter-rebind
# source: browser-extension-recorder
#
# credentials:
# - METER_REBIND_BASE_URL url secret=false
# - METER_REBIND_USERNAME username secret=false
# - METER_REBIND_PASSWORD password secret=true
#
# business-inputs:
# - DEVICE_ID default=1000000150601081283
open "${METER_REBIND_BASE_URL}"
wait --load domcontentloaded --timeout 10000
fill "[placeholder=\"用户名\"]" "${METER_REBIND_USERNAME}"
fill "[placeholder=\"密码\"]" "${METER_REBIND_PASSWORD}"
click "input.anniu.un_butt.buttonClass"
wait --url "http://192.168.2.122:30264/#/*" --timeout 12000
fill "[placeholder=\"设备ID\"]" "${DEVICE_ID}"
click "button.el-button.el-button--primary"
```
约定:
1. 每行是一条 `browserctl` 子命令,不带 `browserctl` 前缀。
2. 空行允许存在。
3. `#` 开头的行是说明注释。
4. 占位符使用 `${KEY}`。
5. 文件中不出现真实密码、Token、Cookie。
如果插件端更愿意导出带前缀的 shell 脚本,也可以:
```bash
browserctl open "${METER_REBIND_BASE_URL}"
browserctl wait --load domcontentloaded --timeout 10000
browserctl fill "[placeholder=\"用户名\"]" "${METER_REBIND_USERNAME}"
```
但作为业务技能固化材料,推荐不带前缀的纯命令集,更便于嵌入或批量执行。
## 3. 常用命令映射建议
| 用户动作 | 推荐 browserctl 命令 |
|---|---|
| 打开起始页 | `open "${PREFIX_BASE_URL}"` |
| 输入文本 | `fill "<selector>" "<value-or-placeholder>"` |
| 追加输入 | `type "<selector>" "<text>"` |
| 点击 | `click "<selector>"` |
| 双击 | `dblclick "<selector>"` |
| 悬停 | `hover "<selector>"` |
| 下拉选择 | `select "<selector>" "<value>"` |
| 勾选 | `check "<selector>"` |
| 取消勾选 | `uncheck "<selector>"` |
| 按键 | `press "<key>" ["<selector>"]` |
| 滚动 | `scroll --by <px>` |
| 等待 URL | `wait --url "<glob>" --timeout <ms>` |
| 等待元素 | `wait --selector "<selector>" --timeout <ms>` |
| 等待文本 | `wait --text "<text>" --timeout <ms>` |
| 等待加载 | `wait --load domcontentloaded --timeout <ms>` |
| 固定等待 | `wait --ms <ms>` |
| 执行 JS | `eval "<js>"` |
关键建议:
1. 点击触发跳转时,录为 `click` + `wait --url`。
2. 不要默认把点击后的跳转录为 `navigate`,否则会绕开真实业务事件。
3. 固定等待只作为兜底,能用 `wait --url`、`wait --selector`、`wait --text` 时优先用明确等待。
4. 表单输入优先用 `fill`,不是逐字符 `type`。
## 4. 选择器建议
插件端自行负责 selector 生成,`browserctl-cli` 只给出建议。
优先级:
1. `data-testid`、`data-test`、`data-cy`
2. `name`
3. 稳定 `id`
4. `placeholder`
5. role + accessible name
6. 可见文本
7. CSS selector
应避免:
1. 深层 `body > div:nth-child(...)`。
2. 框架瞬态 class,例如 `fade-leave`、`loading-mask`。
3. loading mask、overlay、modal backdrop 等遮罩点击。
如果插件只能导出脆弱 selector,建议同时写 warning 注释:
```browserctl
# warning: UNSTABLE_SELECTOR reason="selector uses nth-child"
click "body > div:nth-child(1) > div:nth-child(2) ..."
```
## 5. credentials 建议
命令集中不要写真实登录配置。用占位符:
```browserctl
open "${METER_REBIND_BASE_URL}"
fill "[placeholder=\"用户名\"]" "${METER_REBIND_USERNAME}"
fill "[placeholder=\"密码\"]" "${METER_REBIND_PASSWORD}"
```
并在文件头部用注释列出建议:
```browserctl
# credentials:
# - METER_REBIND_BASE_URL url secret=false
# - METER_REBIND_USERNAME username secret=false
# - METER_REBIND_PASSWORD password secret=true
```
下游制作业务技能时,再按项目技能规范写入 `SKILL.md` frontmatter:
```yaml
credentials:
- key: METER_REBIND_BASE_URL
label: 系统登录地址
type: url
required: true
secret: false
- key: METER_REBIND_USERNAME
label: 登录用户名
type: username
required: true
secret: false
- key: METER_REBIND_PASSWORD
label: 登录密码
type: password
required: true
secret: true
```
注意:
1. 插件只输出命令集和建议,不生成业务技能。
2. 缺少 credentials 时,应由业务技能运行环境的凭证配置卡片处理。
3. 不要让模型在聊天中向用户索要密码。
## 6. 业务输入建议
业务输入不是 credentials。例如设备 ID、工单号、日期范围、客户编号。
推荐写法:
```browserctl
# business-inputs:
# - DEVICE_ID default=1000000150601081283 help="设备 ID"
fill "[placeholder=\"设备ID\"]" "${DEVICE_ID}"
```
区分规则:
| 值类型 | 建议 |
|---|---|
| 登录地址 | credential |
| 用户名 | credential |
| 密码、Token、Cookie | credential,且 `secret=true` |
| 设备 ID、工单号、日期 | business input |
| 固定菜单、固定按钮 | 直接写入命令 |
## 7. 下游业务技能固化约束
`.browserctl` 是业务技能固化的输入材料,不是业务技能本身。
技能作者后续应做:
1. 将 credentials 建议写入 `SKILL.md` frontmatter。
2. 在技能正文说明业务输入。
3. 在技能中引用或内置固定 `browserctl` 命令集。
4. 明确要求执行固定步骤,不让模型重新规划页面操作。
5. 失败时只根据具体 selector 或 wait 条件做局部修正。
推荐写入技能正文的约束:
```markdown
## 执行约束
本技能使用录制得到的固定 browserctl 命令集。执行时不要重新规划页面操作,
不要把 click 改成 navigate,不要跳过 wait 条件。缺少 credentials 时交给系统
凭证配置卡片处理,不要在聊天中向用户索要密码。
```
## 8. browserctl-cli 文档职责
`browserctl-cli` 只需要维护以下文档:
1. 完整命令参考:`reference.md`
2. 插件导出命令集约定:本文档
3. 常见录制场景示例:登录、菜单跳转、表单填写、弹窗确认、iframe
实现职责全部在插件端:
1. 如何录制用户事件。
2. 如何生成稳定 selector。
3. 如何判断 credentials 和 business inputs。
4. 如何展示 warning。
5. 如何导出 `.browserctl` 文件。
## 9. 验收标准
对 `browserctl-cli`:
1. `reference.md` 覆盖插件需要导出的所有常用命令。
2. 本文档清楚说明命令集文件形态、占位符、credentials、业务输入和技能固化边界。
3. 不要求 `browserctl-cli` 提供插件 SDK 或录制 API。
对浏览器插件:
1. 最终能导出 browserctl 命令集。
2. 导出结果不包含真实密码、Token、Cookie。
3. click 后跳转优先导出为 `click + wait --url`。
4. 低质量 selector 和可疑遮罩点击有 warning。
5. credentials 和业务输入明确分离。