browserctl-cli
Advanced tools
Sorry, the diff of this file is too big to display
| # 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); |
+1
-1
@@ -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`。 |
+66
-18
@@ -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 @@ |
+1
-1
| { | ||
| "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": [ |
+10
-0
@@ -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 和业务输入明确分离。 | ||
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
309435
33.6%14
16.67%6524
19.86%147
7.3%18
-5.26%6
20%