站点配方、技能与异步作业
本文讲三件与「让智能体少踩坑」有关的事:把站点经验固化成可执行序列的配方、给人读也给智能体读的技能文档,以及长批次用的异步作业。
一、站点配方(recipes)
把「某个站点上必须这么点」的经验固化成服务端的 JSON 命令序列。每个配方一个 .json 文件,文件名(不含扩展名)就是配方名。
| 项 | 说明 |
|---|---|
| 目录 | 默认 <服务启动目录>/recipes,可用 browser.recipes.dir 改;get_config 能查到实际目录 |
| 列出 | list_recipes(返回名称、说明、参数、默认值、步数、命令序列) |
| 执行 | {"method":"run_recipe","params":{"name":"配方名","vars":{...}}} |
| 生效方式 | 必须显式点名才执行,不做「看到这个域名就自动套用」的隐式推断 |
文件格式
{
"name": "配方名(可省,默认取文件名)",
"description": "一句话说明它解决什么问题",
"params": { "变量名": "给人看的说明" },
"defaults": { "变量名": "默认值,调用方不传时用这个" },
"commands": [
{ "命令名": { "参数": "值" } },
{ "close_modal": { "title": "{{title}}" } }
]
}
commands与批量接口commands的格式完全一致:每项一个键(外加可选的expect断言)。所以配方里的每一步都能单独贴出来手跑,排障时不用整段重来。{{变量}}由vars注入,走 JSON 编码:字符串自动带引号并转义,中文、引号、换行都不用自己转义。带引号的写法("{{x}}")与裸写法({{x}})都支持。- 变量先取
defaults,再用调用方vars覆盖 —— 写好了默认值的配方可以不传参数直接跑。 - 回执与
commands批量一致(count/succeeded/failed/results),另加recipe与recipeDescription。
仓库自带的配方
| 配方 | 用途 |
|---|---|
close-all-modals | 清掉页面上所有可见的 DOM 弹窗(ant 确认框、用户服务协议层、抽屉)。这类按钮只认真实鼠标事件,JS 派发点了没反应 |
query-and-read-table | 点「查询」→ 等表格内容稳定 → 读表格。搜索结果是异步刷新的,点完立刻读会读到上一次的结果 |
cnipa-list-drafts | 中国商标网:「我的账户 → 申请管理 → 未提交」并把日期筛选切到「近三个月」再读列表。默认筛选是「今天」,只看默认视图很容易得出「没保存成功」的错误结论 |
open-console-from-iframe | 主站把第三方控制台套在跨域 iframe 里时的通用套路 |
wework-qykit-open-console | 企业微信后台的「邮件 / 微盘 / 文档 / 会议」:现取一个未被消费的 token,把第三方控制台当顶层页面打开 |
套路:主站套第三方控制台
企业微信后台把「邮件」套在第三方域名的跨域 iframe 里(微盘 / 文档 / 会议同理)。这是一整类站点,不是个例。症状是 get_browser_state 只拿到主站外壳、iframe 内部一个元素都没有。
先用正规解法:get_browser_state 传 includeFrames: true,或先 list_frames 看有哪些 frame。Playwright 能跨 frame(走浏览器协议取内容,不依赖往页面里注入脚本),拿到 frame 之后读、点、填都能做。
为什么还需要这个配方:有些控制台在 iframe 里的可用性受主站外壳影响(iframe 尺寸小、控件被裁掉、主站轮询把状态重置),这时「把控制台当顶层页面打开」更稳。步骤是:
- 从
iframe.src反查第三方 URL 与参数(list_frames,或用execute_js列出页面上所有iframe的src); - 在
get_requests里按token/oauth/sso等关键字找出主站发 token 或换登录态的接口; - 用同源
execute_js调它(credentials: 'same-origin'会自动带上主站的登录 Cookie),拿一个新的、还没被消费的 token; - 用这个 token 把控制台当顶层页面打开,之后就能当普通页面读写。
直接
go_to_url打开 iframe 的src会失败:那里面的 token 是一次性的,已经被 iframe 消费掉了。这是这类站点最容易踩的一步。
写配方的纪律
- 只固化稳定的步骤:菜单文本、按钮文本这类会随站点改版变化的东西尽量放进
params或defaults,别硬编码在commands里; - 每一步都要能解释「为什么必须这样」;靠猜试出来的步骤不要写进配方;
- 涉及不可逆操作的站点,配方里不要带自动提交类步骤。
二、技能文档(skills)
技能是给智能体读的方法论,配方是给服务执行的命令序列。两者互补:
| 技能 | 配方 | |
|---|---|---|
| 形态 | Markdown 文档 | JSON 命令序列 |
| 谁来用 | 智能体读进去,自己决定怎么做 | 服务端按名字执行 |
| 适用 | 需要判断、分支、排障的流程 | 步骤固定、可以照抄的操作 |
| 目录 | .agents/skills/ | recipes/ |
当前仓库的技能组织方式:
| 位置 | 内容 |
|---|---|
.agents/skills/deepseek-browser-use/SKILL.md | 总纲:核心命令用法与参考文档入口 |
.agents/skills/deepseek-browser-use/references/ | 协议、客户端、人机协作、排障等按需读取的分册;商户表单经验位于 payment-onboarding.md |
.agents/skills/<技能名>/SKILL.md | 具体站点的实操手册,记录该站点特有的坑与验证过的步骤 |
技能能否被自动发现取决于宿主支持的目录约定;当前仓库使用 .agents/skills/。技能只提供使用规则,后端服务仍需单独启动。公共教程中的操作经验见表单与多层弹窗排障。
技能文档与命令表有防漂移检查:测试会校验技能文档覆盖了命令表里的每一个方法,且文档里不出现已废弃的方法名。所以新增命令时不要忘了补技能文档,否则构建会失败。
三、异步作业
批次可能跑几十秒以上,超过客户端自己的超时时间。在批次参数里加 "async": true:
{
"id":"1001",
"method":"commands",
"params":{"async":true,"commands":[
{"go_to_url":{"url":"https://example.com"}},
{"wait_for_stable":{"selector":".vxe-table--body-wrapper","quietMs":900,"timeoutSeconds":20}},
{"get_browser_state":{}}
]}
}
接口立刻返回:
{"data":{"jobId":"<作业 ID>","status":"running","browserId":"1001"}}
之后用 get_job 取结果、list_jobs 看全部、cancel_job 取消。
| 项 | 说明 |
|---|---|
| 状态 | running / done / failed / cancelled |
| 取消 | 协作式的:批次会在每一步之间检查取消标记,所以取消在「下一步之前」生效,当前这一步会跑完 |
| 保留数量 | 只保留最近 50 个,更早的被淘汰 |
| 存储 | 结果在内存里,服务重启即丢。它是「一次调用的结果暂存」,不是持久化的任务队列 |
| 异常 | 批次内部抛异常会被记成 failed,不会把线程池打挂 |
最大的好处是:客户端超时不再等于任务失败。 超时之后批次其实还在服务端继续跑,重新用 get_job 取结果即可,不必重发整批 —— 重发在有副作用的操作(提交、下单)上会出问题。
需要跨服务重启保留任务、或需要人工在长时间等待后接着跑的场景,不要依赖作业:把进度写进任务自己的上下文(例如用 execute_js 存进 LocalStorage,或让智能体自己记录),或用站点配方把这段固定步骤固化下来。
