简介
BAL(Browser Automation Language)是一门面向浏览器自动化的领域特定语言。它把「找元素」和「做什么」彻底解耦:语言本身只描述意图,由执行器翻译成 Playwright 调用。
核心目标
- 站点无关:语言不内置任何站点逻辑,站点差异放在 .page 配置中。
- 输入优先兼容:所有输入默认走多策略降级 + 验证,不假设 fill 有效。
- 上传全覆盖:图片、视频、附件、目录、拖拽、粘贴、分片,七种路径。
- 标签独立抽象:把「输入 + 联想 + 选择 + 去重 + 验证」封装成一条命令。
- 可扩展:命令、选择器策略、上传策略均通过注册表扩展,不改语言核心。
参数声明
声明输入参数:key / 名称 / 类型 / 必填标记,运行时按 {key} 替换并自动校验。
@input · 输入参数
# @input prompt 提示词 text 必填声明输入参数:key / 名称 / 类型 / 必填标记,运行时按 {key} 替换并自动校验。
@output · 输出参数
# @output images 图片列表 text声明输出参数:key / 名称 / 类型,运行结果写入工作日志并可被下一节点引用。
设计原则
| 原则 | 说明 |
|---|---|
| 站点无关 | 语言不内置任何站点逻辑,差异放 .page 配置 |
| 输入优先兼容 | 所有输入默认走多策略降级 + 验证,不假设 fill 有效 |
| 显式等待 | 不靠 sleep,用 wait_until 等待真实状态 |
| 可验证 | 每个写操作都可 verify:true,失败降级而非静默 |
| 可扩展 | 命令、选择器策略、上传策略均通过注册表扩展 |
| 可读可 diff | 纯文本行式语法,支持注释与模块化 |
目录
1. 词法规范
基本单位
- 一行一条命令,# 开头为注释
- 命令名小写蛇形:wait_until、set_input_files
- 字符串双引号,支持 \" \\ \n \t 转义
- 参数形式:位置参数 "值"、键值参数 key:value、选项 [opt=val]
- 变量 $name,常量 UPPER_CASE 约定为全局
注释与空行
# 整行注释
goto "https://example.com" # 行尾注释续行
# 长命令用 \ 续行
upload css:"input[type=file]" \
files:"a.jpg","b.png" \
mode:auto \
verify:true字符串插值
set $name = "世界"
echo "你好,${name}" # 你好,世界
echo "你好,$name" # 同上,简单变量可省花括号2. 选择器语法
统一形式:<策略>:<值>[修饰符]
策略表
| 策略 | 示例 | 对应 Playwright |
|---|---|---|
| text | text:"登录" | get_by_text |
| role | role:button["提交"] | get_by_role |
| label | label:"邮箱" | get_by_label |
| placeholder | placeholder:"搜索" | get_by_placeholder |
| testid | testid:"submit-btn" | get_by_test_id |
| title | title:"关闭" | get_by_title |
| alt | alt:"封面" | get_by_alt_text |
| css | css:".btn.primary" | locator |
| xpath | xpath:"//button[@type='submit']" | locator |
| nth | nth:3 | 当前上下文第 4 个 |
| ref | ref:$saved | 引用之前保存的元素句柄 |
修饰符
click text:"删除" [nth=2] # 第 3 个匹配
click text:"删除" [last]
click text:"删除" [first]
click text:"删除" [visible=true] # 只匹配可见元素
click css:".item" [filter="含关键词"]
click role:button["提交"] [exact=true]链式定位
# 在某个容器内查找
click css:".modal" >> text:"确认"
# 父子、兄弟关系
click css:".form" > placeholder:"用户名"
click text:"用户名" + css:".error"元素句柄保存
find placeholder:"标题" -> $title_input
input $title_input value:"测试"3. 变量与表达式
赋值
set $count = 0
set $url = $BASE + "/page/" + $count
set $pwd = env("APP_PASSWORD")
set $user = input("请输入用户名")
set $now = now()
set $id = uuid()
set $n = random(1, 100)内置函数
| 函数 | 说明 |
|---|---|
| now([format]) | 当前时间 |
| uuid() | UUID v4 |
| random(min, max) | 随机整数 |
| env(name) | 环境变量 |
| input(prompt) | 运行时输入 |
| file(path) | 读取文件内容 |
| glob(pattern) | 文件通配 |
| trim(s) / lower(s) / upper(s) | 字符串处理 |
| split(s, sep) / join(list, sep) | 列表处理 |
| len(list) / index(list, i) | 列表操作 |
| json(str) / to_json(obj) | JSON 处理 |
| base64_encode(s) / base64_decode(s) | 编码 |
| sleep(ms) | 暂停(表达式内用) |
表达式
set $total = $price * $qty
set $ok = $count > 3 and $name != ""
set $msg = "共 " + str($count) + " 条"支持:+ - * / %、== != > < >= <=、and or not、括号。
4. 指令总览
【配置与会话】 config session context page viewport user_agent locale timezone
【导航】 goto back forward reload open switch_tab close_tab wait_for_load
【查找】 find find_all exists count
【输入】 input fill type type_slow paste clear press press_seq ime_input
【点击】 click dblclick right_click hover focus blur tap
【表单】 check uncheck select select_multi set_date set_time set_range
【上传】 upload upload_dir upload_drag upload_paste upload_chunked cancel_upload
【标签】 tag_add tag_remove tag_select tag_clear tag_list
【鼠标】 drag drag_to scroll_to scroll_by scroll_into_view
【键盘】 keyboard_press keyboard_type keyboard_down keyboard_up
【等待】 wait wait_until wait_for_url wait_for_response wait_for_download
【断言】 assert assert_exists assert_text assert_url assert_count
【提取】 get_text get_attr get_value get_count get_url get_title get_html
【流程】 if/elif/else/endif loop/endloop foreach/endforeach while/endwhile
break continue return
【函数】 func/endfunc call import
【错误】 try/catch/finally on_error retry
【弹窗】 dialog_accept dialog_dismiss dialog_handle
【框架】 frame frame_exit frame_main
【窗口】 new_window switch_window close_window
【网络】 intercept mock block unblock route unroute
【存储】 storage_save storage_load cookie_set cookie_get cookie_clear
【调试】 screenshot echo log pause trace_start trace_stop dump5. 配置与会话
config(全局配置)
config timeout=30000
config screenshot_on_error=true
config input_mode=auto # auto | fast | compat | human
config input_delay=30
config input_verify=true
config input_max_retry=2
config upload_timeout=180000
config headless=false
config slow_mo=0session(会话)
session new
session load "account.json"
session save "account.json"
session closecontext / page
context new [viewport=1920x1080] [locale=zh-CN] [timezone=Asia/Shanghai]
context close
page new
page close
page switch 0
page list -> $pagesviewport / user_agent / locale
viewport 1440 900
user_agent "Mozilla/5.0 ..."
locale "zh-CN"
timezone "Asia/Shanghai"
geolocation 31.23 121.47
permissions ["geolocation", "notifications"]7. 查找与存在性
find <selector> -> $var
find_all <selector> -> $list
exists <selector> -> $bool
count <selector> -> $nfind placeholder:"标题" -> $title
find_all css:".item" -> $items
exists text:"加载更多" -> $has_more
count css:".comment" -> $n8. 输入命令族核心
设计要点:所有输入默认走多策略降级 + 验证。不假设 fill() 有效,因为 React/Vue 受控组件、contenteditable 富文本、有输入联动的输入框经常不响应直接赋值。
统一入口 input
input <selector> value:"..."
mode:auto|fast|type|type_slow|keyboard|native|richtext|ime
delay:30
clear:true
verify:true
on_fail:abort|continue|retry(3,1000)模式说明
| mode | 行为 | 适用 |
|---|---|---|
| fast | 直接 fill | 静态表单、非受控 |
| type | press_sequentially(delay) | 普通 React 组件 |
| type_slow | 逐字符,delay≥100 | 有联想的搜索框 |
| keyboard | click + Ctrl+A + Delete + keyboard.type | 强风控、IME |
| native | native setter + 派发 input/change | React/Vue 受控 |
| richtext | contenteditable 专用链 | 富文本编辑器 |
| ime | 派发 composition 事件 | 中日韩输入法 |
| auto | 检测类型 → 策略链降级 → 验证 | 默认推荐 |
自动降级流程
input mode:auto
├─ 检测元素类型
│ ├─ input/textarea → 表单策略链
│ ├─ contenteditable → 富文本策略链
│ └─ 未知 → 先试表单,再试富文本
├─ 表单策略链:fill → press_sequentially → keyboard.type → native_setter
├─ 富文本链:press_seq → insert_text → execCommand → innerText
├─ 每步验证:input_value() 或 inner_text()
├─ 失败降级,全部失败 → 截图 + trace + on_fail
└─ 返回实际生效策略名(写入日志)专用命令
fill placeholder:"标题" value:"测试"
type placeholder:"标题" value:"测试" delay:50
type_slow placeholder:"搜索" value:"Playwright" delay:150
paste css:".editor" value:"很长的正文..."
clear placeholder:"标题"
press css:".editor" key:"Enter"
press css:".editor" key:"Control+A"
press_seq css:".editor" value:"逐字符输入" delay:30
ime_input css:".editor" value:"中文输入法内容"只读 / 禁用自动解锁
# mode:auto 检测到 readOnly/disabled 时自动执行:
el.readOnly = false; el.disabled = false;
unlock placeholder:"只读字段"
lock placeholder:"字段"输入验证
input placeholder:"标题" value:"测试" verify:true
assert_value placeholder:"标题" equals "测试"
assert_value placeholder:"标题" contains "测"9. 文件上传核心
统一入口 upload
upload <selector>
files:"a.jpg","b.png"
mode:auto|input|drag|paste|chunked|dir
cover:"a.jpg"
verify:true
timeout:180000
on_fail:abort|continue|retry(3,1000)模式说明
| mode | 行为 | 适用 |
|---|---|---|
| input | 直接 set_input_files | 标准 input[type=file] |
| drag | 模拟拖拽到目标区域 | 无 input、纯拖拽上传 |
| paste | 模拟粘贴文件 | 支持粘贴上传的编辑器 |
| chunked | 分片上传大文件 | 视频、大附件 |
| dir | 上传整个目录 | webkitdirectory |
| auto | 检测 → 策略链降级 | 默认推荐 |
策略链
upload mode:auto
├─ 查找 input[type=file]
│ ├─ 存在且 visible/attached → set_input_files
│ └─ 隐藏 → 直接 set_input_files(无需可见)
├─ 无 input → 点击上传触发区 → 等待 input 出现 → set_input_files
├─ 仍无 input → 尝试 drag 策略
├─ 仍失败 → 尝试 paste 策略
├─ 大文件(>50MB)→ 自动切 chunked
├─ 等待上传完成:缩略图数量 / 进度条消失 / 网络空闲
└─ 校验:文件数量、封面、缩略图存在图片上传
upload css:"input[type=file]" files:"cover.jpg","detail1.png","detail2.png"
upload css:"input[type=file]" files:glob("assets/*.jpg")
upload css:".upload-area" files:"cover.jpg" mode:drag
upload css:".editor" files:"screenshot.png" mode:paste
upload css:"input[type=file]" files:"a.jpg" cover:"a.jpg" verify:true视频上传
upload css:"input[type=file]" files:"video.mp4" mode:auto timeout:600000
upload css:"input[type=file]" files:"video.mp4" mode:chunked chunk_size:5242880
assert_uploaded css:".video-item" count:1
wait_until text:"视频上传完成" timeout:600000附件上传
upload css:"input[type=file]" files:"doc.pdf","sheet.xlsx"
upload css:"input[type=file]" files:"archive.zip" verify:true目录上传
upload_dir css:"input[type=file]" dir:"assets/images"拖拽 / 粘贴 / 分片
upload_drag css:".drop-zone" files:"cover.jpg"
drag_file css:".drop-zone" path:"cover.jpg"
upload_paste css:".editor" path:"screenshot.png"
upload_chunked css:"input[type=file]" path:"big-video.mp4" chunk_size:5242880上传控制
cancel_upload # 取消当前上传
pause_upload # 暂停(若页面支持)
resume_upload
assert_uploaded <selector> count:<n>
wait_upload_complete timeout:18000010. 标签与话题核心
设计要点:标签交互是「输入 + 联想 + 选择」的组合。单独抽象成命令族,把「输入关键词 → 等待联想 → 选择候选 → 去重 → 验证」封装成一条命令。
tag_add
tag_add <selector>
value:"Playwright"
mode:auto|type|select|enter|comma
wait_suggest:true
select:nth(0)|first|exact|contains
verify:true
dedup:true交互流程
tag_add mode:auto
├─ 点击标签输入入口(或 # 触发)
├─ 输入关键词(走 input mode:auto)
├─ 等待联想候选出现
├─ 选择候选:
│ ├─ select:first → 第一个
│ ├─ select:nth(2) → 第三个
│ ├─ select:exact → 文本完全匹配
│ └─ select:contains → 文本包含
├─ 或按 Enter/逗号 直接创建
├─ 验证标签 chip 出现
└─ 去重(已存在则跳过)示例
# 小红书话题
tag_add placeholder:"搜索话题" value:"Playwright" select:exact
tag_add placeholder:"搜索话题" value:"自动化测试" select:first
tag_add placeholder:"搜索话题" value:"小红书运营" select:contains
# 批量
foreach $t in ["Playwright", "自动化", "测试"]
tag_add placeholder:"搜索话题" value:$t select:first
endforeach其他标签命令
tag_remove placeholder:"搜索话题" value:"Playwright"
tag_clear placeholder:"搜索话题"
tag_list -> $tags # 提取当前所有标签
tag_select css:".tag-list" value:"已有标签" # 从已有列表中选择
assert_tag placeholder:"搜索话题" contains "Playwright"
assert_tag_count placeholder:"搜索话题" equals 3标签触发方式
# 通过 # 触发
tag_add css:".editor" value:"Playwright" trigger:"#"
# 通过按钮触发
tag_add css:".editor" value:"Playwright" trigger:click
# 直接输入 + 回车
tag_add css:".tag-input" value:"Playwright" mode:enter11. 表单控件
复选框 / 单选
check css:"#agree"
uncheck css:"#subscribe"
check css:".radio-option" [value="A"]
is_checked css:"#agree" -> $checked下拉选择
select css:"#city" value:"上海"
select css:"#city" label:"上海市"
select css:"#city" index:2
select_multi css:"#tags" values:"A","B","C"
deselect css:"#city" value:"上海"
select_native css:"#city" value:"上海" # 原生 select
select_custom css:".dropdown" value:"上海" # 自定义下拉日期 / 时间 / 范围
set_date css:"#date" value:"2026-09-29"
set_time css:"#time" value:"14:30"
set_range css:"#price" value:500
set_color css:"#color" value:"#ff0000"滑块 / 评分
set_slider css:".slider" value:75
set_rating css:".rating" value:512. 点击与鼠标
click <selector> [button=left|right|middle] [click_count=1] [force=false]
dblclick <selector>
right_click <selector>
hover <selector>
focus <selector>
blur <selector>
tap <selector> # 触摸
click_at x:100 y:200
click_position <selector> x:10 y:10
drag <from> to <to>
drag_to <selector> target:<selector>
scroll_to <selector>
scroll_by x:0 y:500
scroll_into_view <selector>
scroll_top / scroll_bottom13. 键盘
keyboard_press "Enter"
keyboard_press "Control+A"
keyboard_type "Hello" delay:50
keyboard_down "Shift"
keyboard_up "Shift"
keyboard_insert_text "中文文本"14. 等待与同步
wait 1000
wait_until <selector> [state=visible|hidden|attached|detached] [timeout=30000]
wait_until url contains "/dashboard"
wait_until url equals "https://..."
wait_until text:"加载完成"
wait_until title contains "首页"
wait_until count css:".item" >= 10
wait_for_url "**/dashboard"
wait_for_response url contains "/api/user"
wait_for_request url contains "/api/submit"
wait_for_download to:"file.pdf"
wait_for_popup -> $new_page
wait_for_function "() => window.ready === true"
wait_for_load state:networkidle
wait_for_timeout 300015. 断言
assert exists <selector>
assert not_exists <selector>
assert visible <selector>
assert hidden <selector>
assert enabled <selector>
assert disabled <selector>
assert text <selector> equals "预期"
assert text <selector> contains "包含"
assert text <selector> matches "/正则/"
assert value <selector> equals "值"
assert attr <selector> name:"href" contains "/home"
assert url contains "/dashboard"
assert title equals "首页"
assert count <selector> equals 5
assert count <selector> >= 3
assert checked css:"#agree"
assert uploaded <selector> count:3
assert tag <selector> contains "Playwright"
assert no_console_error
assert no_network_error# 失败策略
assert exists css:".ad" on_fail:continue
assert exists css:".required" on_fail:abort
assert exists css:".optional" on_fail:retry(3, 1000)16. 数据提取
get_text <selector> -> $var
get_text_all <selector> -> $list
get_attr <selector> name:"href" -> $url
get_value <selector> -> $val
get_html <selector> -> $html
get_inner_html <selector> -> $html
get_count <selector> -> $n
get_url -> $url
get_title -> $title
get_cookie name:"session" -> $cookie
get_local_storage key:"token" -> $token
get_all_text -> $page_text
get_screenshot <selector> to:"el.png"# 批量提取
get_all css:".item" -> $items {
text: "get_text(css:'.title')",
link: "get_attr(css:'a', 'href')",
price: "get_text(css:'.price')"
}17. 流程控制
条件
if $count > 3
echo "超过阈值"
elif $count == 0
echo "零"
else
echo "正常"
endif计数循环
loop 5 times
click text:"下一页"
wait 1000
endloop遍历
foreach $item in ["a", "b", "c"]
echo $item
endforeach
foreach $file in glob("assets/*.jpg")
upload css:"input[type=file]" files:$file
endforeach条件循环
while exists text:"加载更多"
click text:"加载更多"
wait 1000
endwhile循环控制
loop 10 times
if exists text:"结束"
break
endif
if not_exists text:"继续"
continue
endif
endloop返回
func check()
if not_exists css:".ok"
return false
endif
return true
endfunc18. 函数与模块
定义与调用
func login(user, pass)
goto $BASE + "/login"
input placeholder:"用户名" value:$user
input placeholder:"密码" value:$pass mode:keyboard
click role:button["登录"]
wait_until url contains "/home"
endfunc
call login("admin", env("APP_PASSWORD"))默认参数
func publish(title, body, tags = [])
input placeholder:"标题" value:$title
input role:textbox[first] value:$body mode:richtext
foreach $t in $tags
tag_add placeholder:"搜索话题" value:$t select:first
endforeach
click text:"发布"
endfunc模块导入
import "lib/xhs.bal"
import "lib/common.bal"
call xhs_publish("标题", "正文", ["标签1", "标签2"])模块导出
# lib/xhs.bal
export func xhs_login(session_file)
...
endfunc
export func xhs_publish(title, body, tags)
...
endfunc19. 错误处理
try / catch / finally
try
click text:"提交"
wait_until text:"成功"
catch e
echo "失败: " + $e.message
screenshot "error.png"
finally
echo "清理"
endtryon_error 修饰
click text:"提交" on_error:retry(3, 1000)
click text:"可选广告" on_error:continue
upload css:"input[type=file]" files:"a.jpg" on_error:abort全局错误策略
config on_error=abort # abort | continue | retry
config screenshot_on_error=true
config trace_on_error=true20. 弹窗 · 框架 · 窗口
原生弹窗
dialog_accept
dialog_dismiss
dialog_handle accept
dialog_handle dismiss
dialog_prompt "输入内容"
on_dialog acceptiframe
frame css:"iframe#login"
input placeholder:"用户名" value:"admin"
click text:"登录"
frame_exit
frame_main多窗口
new_window "https://example.com" -> $page
switch_window 1
switch_window title contains "新窗口"
close_window21. 网络与存储
拦截与模拟
intercept url contains "/api/user" -> $response
mock url contains "/api/user" body:{"name": "test"} status:200
block url contains ".png"
unblock url contains ".png"
route url contains "/api/**" handler:"mock_handler"
unroute
wait_for_response url contains "/api/submit"存储
storage_save "state.json"
storage_load "state.json"
cookie_set name:"token" value:"xxx" domain:".example.com"
cookie_get name:"token" -> $token
cookie_clear
local_storage_set key:"token" value:"xxx"
local_storage_get key:"token" -> $val22. 调试
screenshot "step1.png"
screenshot <selector> to:"element.png"
echo "当前 URL: " + get_url()
log info "步骤完成"
pause # 打开 Inspector
trace_start "trace.zip"
trace_stop
dump html -> "page.html"
dump text -> "page.txt"
dump console -> "console.log"
dump network -> "network.har"
highlight <selector>23. 扩展机制
注册自定义命令
# Python 侧
@executor.register("xhs_publish_note")
async def publish_note(ctx, title, body, tags):
...
# BAL 侧
xhs_publish_note title:"标题" body:"正文" tags:["a","b"]注册选择器策略
STRATEGIES["data"] = lambda p, v: p.locator(f'[data-{v}]')
click data:action="submit"注册上传策略
UPLOAD_STRATEGIES["websocket"] = upload_via_ws
upload css:"#file" files:"a.jpg" mode:websocket站点配置 .page
site: xiaohongshu
base_url: "https://www.xiaohongshu.com"
selectors:
creator_button: 'text:"创作中心"'
title_input: 'placeholder:"添加标题"'
body_editor:
selector: 'role:textbox[first]'
input_mode: richtext
upload_input: 'css:input[type="file"]'
hashtag_entry: 'text:"#添加话题"'
hashtag_search: 'placeholder:"搜索话题"'
input_defaults:
mode: auto
verify: true
upload_defaults:
mode: auto
timeout: 180000
load_page "xiaohongshu.page"
goto creator
input $title_input value:"标题"24. 完整示例
小红书图文发布
# xhs_publish.bal
config input_mode=auto
config input_verify=true
config screenshot_on_error=true
load_page "xiaohongshu.page"
session load "account.json"
goto creator
# 上传图片(第一张为封面)
upload $upload_input \
files:"cover.jpg","detail1.png","detail2.png" \
mode:auto \
cover:"cover.jpg" \
verify:true
wait_upload_complete timeout:120000
# 标题(React 受控 input)
input $title_input value:"Playwright 自动化发布测试" mode:auto verify:true
# 正文(contenteditable 富文本)
input $body_editor value:"这是正文内容,测试富文本输入。" mode:richtext delay:30
# 标签
foreach $t in ["Playwright", "自动化测试", "小红书运营"]
tag_add $hashtag_search value:$t select:first verify:true
endforeach
assert_tag_count $hashtag_search equals 3
# 发布
click text:"发布"
wait_until url contains "/publish/success" timeout:60000
screenshot "publish_result.png"
session save "account.json"视频上传 + 附件
config upload_timeout=600000
upload css:"input[type=file]" files:"video.mp4" mode:chunked chunk_size:5242880
wait_until text:"视频上传完成" timeout:600000
assert_uploaded css:".video-item" count:1
upload css:"input[type=file]" files:"cover.jpg" mode:auto
upload css:"input[type=file]" files:"doc.pdf","sheet.xlsx"带错误恢复的批量操作
foreach $item in $items
try
input placeholder:"搜索" value:$item mode:type_slow delay:150
wait_until css:".result-item" state:visible
click css:".result-item" [first]
click text:"收藏"
wait_until text:"已收藏"
catch e
log error "处理失败: " + $item + " - " + $e.message
screenshot "error_" + $item + ".png"
continue
endtry
endforeach25. 附录速查
选择器策略速查
text · role · label · placeholder · testid · title · alt · css · xpath · nth · ref
输入模式速查
fast · type · type_slow · keyboard · native · richtext · ime · auto
上传模式速查
input · drag · paste · chunked · dir · auto
错误码
| 错误码 | 含义 |
|---|---|
| E_SELECTOR_NOT_FOUND | 选择器无匹配 |
| E_TIMEOUT | 等待超时 |
| E_INPUT_FAILED | 所有输入策略失败 |
| E_UPLOAD_FAILED | 上传失败 |
| E_ASSERT_FAILED | 断言失败 |
| E_DIALOG_UNHANDLED | 弹窗未处理 |
| E_FRAME_NOT_FOUND | iframe 未找到 |
| E_NETWORK_ERROR | 网络错误 |
| E_PERMISSION_DENIED | 权限不足 |
覆盖度清单
| 类别 | 覆盖 |
|---|---|
| 导航 | goto/back/forward/reload/多标签/多窗口 |
| 元素 | 查找/点击/悬停/拖拽/滚动/焦点 |
| 输入 | 表单/富文本/只读/IME/受控组件/验证 |
| 上传 | 图片/视频/附件/目录/拖拽/粘贴/分片 |
| 标签 | 输入/联想/选择/删除/去重/批量 |
| 表单 | 复选/单选/下拉/日期/时间/范围/滑块 |
| 等待 | 元素/URL/文本/网络/下载/弹窗/函数 |
| 断言 | 存在/可见/文本/属性/数量/上传/标签 |
| 提取 | 文本/属性/HTML/Cookie/Storage |
| 流程 | 条件/循环/遍历/break/continue/return |
| 函数 | 定义/调用/默认参/导入/导出 |
| 错误 | try/catch/finally/retry/on_error |
| 弹窗 | alert/confirm/prompt |
| 框架 | iframe 进出 |
| 网络 | 拦截/模拟/屏蔽/等待 |
| 存储 | session/cookie/localStorage |
| 调试 | 截图/日志/暂停/trace/dump |
结语
三大核心设计
- 输入不假设:input mode:auto 把兼容性做到最高,任何输入框都有降级路径。
- 上传不假设:upload mode:auto 覆盖 input、拖拽、粘贴、分片、目录五种路径。
- 标签单独抽象:tag_add 把「输入 + 联想 + 选择 + 去重 + 验证」封装成一条命令。
配合 .page 站点配置和命令注册表,这套 DSL 可以在不改语言核心的前提下,适配任意站点、任意控件,覆盖从导航到上传到提取的完整网页操作链路。
