workflow-design · git:20260821.c0c683c · 2026-08-21 · sha256 240874060dc11ce1
workflow-design git:20260821.c0c683cC
Immutable. This exact content is served forever at /api/v1/blob/240874060dc11ce1.
---
title: 工作流设计经验手册
id: workflow-design
type: skill
tags: [workflow, 设计, 经验, 调试, 闭环]
---
# 工作流设计经验手册
> **阶段门禁、产出规范、步骤 schema、检查清单、验收标准均已内置在 WorkAgent L2 提示词中,本文档不复制。**
> 本文档只提供 L2 没有的差量内容:实战经验 + 检索/提炼方法 + 陷阱详情 + 场景模式。
---
## 〇、闭环总览
> 四阶段流程(查→探→固→验)和门禁条件见 L2,此处强调经验积累闭环:
```
接收任务
│
├─→ [查] ui_maps_search 检索同类应用/操作经验
│ 已有经验?→ 直接复用布局参数和操作链,跳过重复探索
│
├─→ [探] 逐屏探索 → ui_maps_save_screen 固化布局
│
├─→ [固] 生成 params.json + workflow.json + guide.md
│
├─→ [验] dry_run 校验 → workflow_run 执行验证
│ 跑通?→ 提炼经验 → ui_maps_save_experience → 闭环
│ 失败?→ 分析原因 → 修正参数 → 重新执行
│
└─→ [馈] 异常回写 exceptions,新发现补充到经验
```
**核心原则**:每次设计都是对经验库的充实。不重复探索已被验证的路径。
---
## 一、阶段差量要点
> 各阶段的目标/退出条件见 L2。此处只补充 L2 没写的实战细节。
### 阶段 0:复用检索
检索方法与决策见「二、经验检索指南」。骨架有 screen → 跳过阶段一布局解析;有 experience → 参考 tool_chain 调参复用;无结果 → 完整探索。
### 阶段 1:探索跑通
**探索前先对齐**(硬性门禁,逐项确认后才能进入探索):
```
对齐清单:
□ 目标应用/页面的启动/打开方式?(需用户手动打开还是工作流自动?)
□ 目标界面的触发方式?(快捷键/点击/URL/API?)
□ 目标界面的关闭/退出方式?(Esc/遮罩/X按钮/自动消失?)
□ 操作流程中哪些步骤需要用户介入?(登录/验证码/确认对话框?)
□ 是否有不可逆操作需要用户确认?(删除/发送/提交?)
□ 是否有已知的异常状态或边界情况?(弹窗/加载延迟/权限不足?)
```
确认后记录到 `params.json` 的 `exceptions` 或其他对应字段。禁止把功能约束误判为坐标/定位问题。
**浏览器页面(snapshot + screenshot 互补)**:
1. `browser_snapshot` 获取页面骨架和 @eN ref
2. snapshot 未返回目标元素 ref → 立即 `browser_screenshot` → `desktop_vision` 分析
3. 两个工具互相补盲,缺什么补什么。**不换 JS 策略重试。**
**反爬预检(浏览器第一屏必须执行)**:
`browser_screenshot` → `desktop_vision` 检测页面是否正常渲染。
检测词:登录/验证码/Captcha/空空如也/请先登录。
命中 → 立即标记 exceptions,预设 `wait` 步骤让用户介入。不尝试绕过。
1688/淘宝/京东/拼多多等电商平台默认视为反爬平台。
### 阶段 2:params.json 字段规范
| 字段 | 内容 |
|------|------|
| `workflow_id` | 工作流唯一标识 |
| `window` | 尺寸、URL、标题模式 |
| `login_detection` | 登录态判定逻辑 |
| `regions` | 区域定义 + 定位特征 |
| `navigation_graph` | 屏间跳转关系 |
| `exceptions` | 异常路径 + 降级策略 |
---
## 二、经验检索指南
**两级搜索**:先骨架后详情,不会爆 output。
**第一级 — 骨架**(发现有哪些可用资源):
```
ui_maps_search(query="微信 im")
→ [{ app_name: "微信", app_category: "im",
matched_screens: [{ screen_name: "chat-list", region_count: 3 }, ...],
matched_experiences: [{ id: "send-msg", name: "发消息", keywords: [...] }, ...]
}]
```
**第二级 — 详情**(精确获取完整数据):
```
ui_maps_search(query="微信", screen_name="chat-list")
→ [{ app_name: "微信",
matched_screens: [{ screen_name: "chat-list", regions: [{ name, rect, elements: [...], anchor: {...} }, ...] }],
matched_experiences: [{ id, name, tool_chain, summary: "完整原文", ... }] // 仅 screen_ref=="chat-list" 的经验
}]
```
### 什么时候查
| 时机 | 示例 |
|------|------|
| 开始新设计前 | `ui_maps_search(query="微信 im")` — 查 app 有没有已有 screen |
| 遇到类似操作 | `ui_maps_search(query="发送消息 enter")` — 查操作经验 |
| 定位困难 | `ui_maps_search(query="图标 tooltip hover")` — 查定位经验 |
| 跨应用参考 | `ui_maps_search(app_category="im", query="发送")` — 限定类别 |
### 怎么用查到的结果
1. **有 matched_screens** → 直接用 regions 数据(name/rect/description/elements),跳过阶段一布局解析
2. **有 matched_experiences** → 参考 tool_chain 和 summary,调整参数复用
3. **都没匹配** → 进入完整探索,结束后 save_screen + save_experience 补充经验库
---
## 三、经验提炼指南
### 什么值得存为经验
- ✅ 非显而易见的操作序列(如 hover→等2s→小范围OCR→点击)
- ✅ 跨应用可复用的模式(如登录检测模式)
- ✅ 曾被踩过的坑及解决方案
- ✅ 特殊定位技巧
- ❌ 常规操作(如 browser_navigate 打开网页)
- ❌ 一次性的特定操作
- ❌ 已经在提示词中充分覆盖的内容
### 经验摘要(summary)写法
好的 summary 遵循:**做什么 → 怎么做 → 为什么这样做 → 跨应用适用性**
```
微信桌面版发送文字消息:
搜索框 desktop_input 输入联系人名称 → enter 直接进入对话窗口
(非传统思路:省略了 OCR 定位联系人列表 → click 选中 → click chat-area 获焦,省 2-3 步)
chat-area 已自动获焦,直接 desktop_input 输入文字 → enter 发送。
关键判断:微信搜索框输入正确联系人后 enter,会直接跳转对话且 chat-area 自动获焦。
可跨应用:即时通讯桌面应用中,凡搜索框支持 enter 直达对话的,此模式适用。
```
差的 summary:`"微信发送消息:点击联系人,输入文字,回车发送。"`
---
## 四、产出规范
### 目录结构(严格约定)
```
plugin/workflows/{workflow.id}/
├── workflow.json # 主工作流定义,id 字段 = 目录名
├── params.json # 固化参数(可选,UIMap 兼容格式)
└── guide.md # 使用文档(可选)
```
- 目录名 = `workflow.json` 中的 `id` 字段
- `params.json` 若存在,需包含 `app_name`、`process_name`、`window.title_pattern` 等窗口定位字段(UIMap 兼容格式)
- 不要依赖 `wf.name` 作为目录名(name 可能含空格/中文)
- runtime 自动在根目录生成的 `{name}.json`/`{name}.md` 为内部缓存,不手动创建
## 五、常见陷阱
> 以下 5 条的警示已内置 L2(关键警示 + 检查清单),此处不再展开:
> 跳过布局解析直接找元素 / 窗口尺寸未固化 / 探索阶段写步骤 / 用 if contains 文案做登录检测 / 忘记 SPA 状态残留。
### 把 tooltip OCR 截图截到其他窗口
**症状**:hover 图标后 OCR 识别出桌面其他窗口的文字。
**正确做法**:先 `desktop_window_info` 获取客户区边界,截图范围约束在客户区内。
### 降级路径导致数据丢失
**症状**:重试时重复提交表单。
**正确做法**:重试前检测当前状态,已完成步骤跳过。
---
## 六、复杂场景模式
### 模式 A:登录态检测与自动登录
```json
{ "id": "login_guard", "name": "登录保障", "do": { "seq": [
{ "id": "check", "name": "判断登录态",
"do": { "chat": "根据 login_detection 特征判断是否已登录", "with": {
"agent_id": "login-checker",
"screenshot": true,
"requirements": ["只输出 LOGGED_IN 或 LOGIN_REQUIRED"]
} },
"capture": "login_status" },
{ "id": "branch", "name": "按登录态分支",
"do": { "if": {
"condition": { "equals": [ { "var": "login_status" }, "LOGIN_REQUIRED" ] },
"then": [
{ "id": "goto", "name": "打开登录页",
"do": { "tool": "browser_navigate", "with": { "url": "{params.login.url}" } } },
{ "id": "manual", "name": "手动登录",
"do": { "wait": "请完成登录后点击继续" } }
],
"else": []
} } }
] } }
```
要点:语义判断(chat 步骤 + screenshot)而非文案匹配;登录动作优先 `wait` 让用户完成,不碰凭据。
### 模式 B:for_each 循环遍历
```json
{
"id": "tour",
"name": "面板巡览",
"do": {
"loop": {
"for_each": { "items": { "var": "panels" }, "as": "p" },
"max": 100,
"do": [
{ "id": "click", "name": "点击",
"do": { "tool": "desktop_mouse",
"with": { "action": "click", "x": "{{p | get \"ix\"}}", "y": "{{p | get \"iy\"}}" } },
"on_error": "abort" }
]
}
}
}
```
要点:
- `for_each.items` 引用变量(`{ "var": "panels" }`),`for_each.as` 指定当前迭代项变量名,子步骤中通过 `{{p | get "field"}}` 引用
- `max` 可选,默认 100,防止死循环
- `with` 字段支持 `| get` 管道;`code` 字段(脚本步骤)同样支持(v0.1 起)
### 模式 C:表单提交 + 结果验证
```
seq: 提交
├─ 填写 + 提交
├─ browser_wait_for(结果页元素)
├─ assert: 成功标志存在
└─ if: 失败 → screenshot → chat 步骤分析 → 决定重试或终止
```
### 模式 D:桌面应用多窗口操作
```
seq: 跨窗口
├─ desktop_windows_list → 记录所有 hwnd
├─ 窗口 A 操作 → desktop_window_activate(hwnd_a) → ...
├─ 窗口 B 操作 → desktop_window_activate(hwnd_b) → ...
```
hwnd 会变化,每次操作前重新 windows_list。
### 模式 E:大工作流拆分
**何时拆**:单层步骤 > 15 个或嵌套深度 > 3 层时,拆分为子工作流。
```
主工作流: 子工作流 login-flow:
seq seq
├─ call: login-flow ├─ browser_navigate(login)
├─ call: fetch-data ├─ browser_type(user)
├─ call: send-msg ├─ browser_type(pass)
└─ call: logout └─ browser_click(submit)
```
**产出方式**:先 Write 主 workflow.json(含 call 步骤),再逐个 Write 子工作流 JSON 到同目录。不要试图在一轮对话中输出所有 JSON。
### 模式 F:SPA 搜索框提交
**优先级从高到低**(视觉定位优先,不依赖页面 JS 实现):
1. `browser_snapshot` 找到搜索按钮 `@eN` → `browser_click`
2. 无 ref → `browser_screenshot` → OCR 找按钮坐标 → JS 模拟点击坐标
3. 以上都不行 → `browser_evaluate` 触发 `form.submit()`(最后手段)
---
## 七、工具使用要点
`desktop_input` 输入和发送是一次调用,不要拆成两步。普通文本直接用,不用剪切板。
剪切板只用于 >500 字的大段文本,用后清空。敏感内容禁用剪切板。
### 坐标体系
| 工具 | 输入坐标系 | 输出坐标系 |
|------|-----------|-----------|
| `desktop_perceive` | 图片像素坐标(取决于截图来源) | 同左 |
| `desktop_window_screenshot` | — | 客户区像素 + `screen_x`/`screen_y` 偏移 |
| `desktop_screenshot(region)` | — | 区域像素 + `screen_x`/`screen_y` 偏移 |
| `desktop_mouse` (无 hwnd) | **屏幕绝对坐标** | — |
| `desktop_mouse` (有 hwnd) | 屏幕绝对坐标 | — |
**规则:永远用屏幕坐标调用 `desktop_mouse`。**
perceive 结果 = 客户区坐标时,手动加截图返回的 `screen_x`/`screen_y` 偏移。
### 工具选择速查
| 场景 | 首选 | 备选 |
|------|------|------|
| 定位网页元素 | `browser_snapshot` → @eN ref | `browser_screenshot` + OCR |
| 桌面布局解析 | Vision 全窗口语义分析 | `desktop_find_text`(需字库) |
| 定位桌面文字坐标 | 按 Vision 划定的功能区精准定位 | `desktop_find_text`(需字库) |
| 定位桌面图标 | hover + 小范围 Vision OCR tooltip | `request_user_input(icon_confirm)` |
| 等待网页加载 | `browser_wait_for(selector)` | `system_sleep`(不得已) |
| 验证页面状态 | `browser_snapshot` + `chat` 步骤 | `browser_extract` 文本匹配 |
| 验证桌面状态 | `desktop_window_screenshot` + OCR | `desktop_find_image` 模板匹配 |
| 查经验/布局 | `ui_maps_search(query="微信 im")` — 返回 screens + experiences | `Read` 直接读 ui-maps JSON |
| 跨应用参考 | `ui_maps_search(app_category="im", query="发送")` | 不限类别模糊搜索 |
### 浏览器 snapshot + screenshot 配合流程
```
browser_snapshot(骨架 + ref)
↓
目标元素有 ref?
├─ 有 → browser_click / browser_type
└─ 无 → browser_screenshot → desktop_vision 分析
```
**snapshot 拿不到 ref,立刻截图。不换 JS 策略重试。**
### DOM 数据提取规则
- **卡片边界**:用语义 class 限定(如 `[class*="offer-item"]`),不用固定层级 `closest`
- **图片提取**:按 `data-src` → `data-original` → `src` 顺序 fallback
- **数据隔离**:先 `querySelector` 限定卡片范围再正则。禁止全卡片 `textContent` 跨产品污染
### 连续失败熔断
同类操作失败 ≥3 次 → 停止 → 判断类别:
| 类别 | 动作 |
|------|------|
| 工具限制 | 换工具配合链 |
| 定位失败 | `request_user_input(region)` |
| 反爬拦截 | `request_user_input` 让用户处理 |
### browser_extract 返回空
不重复调用。改用:
`browser_screenshot` → `desktop_vision(prompt:"提取页面所有可见文本")`
---
## 八、Step JSON 速查附录
### loop(for_each)
```json
{ "id": "l1", "name": "遍历列表",
"do": { "loop": { "for_each": { "items": { "var": "items" }, "as": "it" },
"max": 100, "do": [/* 子步骤中用 {{it | get "field"}} 引用迭代项字段 */] } } }
```
### loop(until)
```json
{ "id": "l2", "name": "翻页循环",
"do": { "loop": { "until": { "equals": [ { "var": "has_next" }, "false" ] },
"max": 100, "do": [/* 子步骤,可用 break / continue */] } } }
```
### loop(repeat)
```json
{ "id": "l3", "name": "重试3次",
"do": { "loop": { "repeat": 3, "do": [/* 子步骤 */] } } }
```
### if(条件分支)
```json
{ "id": "i1", "name": "需滚动?",
"do": { "if": {
"condition": { "equals": [ { "var": "need_scroll" }, "yes" ] },
"then": [/* matched steps */],
"else": [/* else steps,可省略 */]
} } }
```
条件 `var` 支持点号路径下钻:`{ "var": "coords.need_scroll" }` → 取 `variables["coords"]["need_scroll"]`。
可用条件操作:`equals` / `not_equals` / `contains` / `starts_with` / `regex`(取 `VarRef[]`)、`not_empty` / `empty`(取 `VarRef`)、`gt` / `lt` / `gte` / `lte`(取 `VarRef[]`)、`always`(取 boolean)。
### script
```json
{ "id": "s1", "name": "算坐标",
"do": { "script": { "runtime": "python",
"code": "import json\nwinfo=json.loads('''{{winfo}}''')\nprint(json.dumps({'ix':winfo['wx']+587}))" } },
"capture": "coords", "on_error": "abort" }
```
`code` 字段支持 `{{var}}` 替换和 `| get` / `| default` 管道(v0.1 起)。
### tool
```json
{ "id": "t1", "name": "点击",
"do": { "tool": "desktop_mouse", "with": { "action": "click", "x": "{{coords | get \"ix\"}}" } },
"capture": "result", "on_error": "abort" }
```
### call(子工作流)
```json
{ "id": "c1", "name": "登录",
"do": { "call": "login-flow", "with": { "inputs": { "user": "{{username}}" }, "outputs": { "token": "auth_token" } } } }
```
### wait(人工介入)
```json
{ "id": "w1", "name": "手动登录",
"do": { "wait": "请在浏览器中完成登录后点击继续", "auto": [/* 可选:等待期间可执行的检查步骤 */] } }
```
### chat(LLM 决策)
**ChatAgent 配置四段法**:
ChatAgent 的 persona 必须按四层独立语义设计,每层职责不交叉:
| 层次 | 字段 | 含义 | 允许内容 | 禁止内容 |
|------|------|------|----------|----------|
| 身份定义 | `persona` | 它是谁 | 角色、专业领域、工作方式 | 任务目标、操作步骤 |
| 任务目标 | `goal` | 要达成什么 | 可衡量的目标描述 | 具体怎么做 |
| 约束条件 | `constraints` | 不能做什么 | 边界、红线、禁止行为 | 正向操作指令 |
| 操作规范 | `requirements` | 怎么做 | 工具使用规范、操作流程 | 任务细节、对话内容 |
**正确示例** — 终端操作员:
```json
{
"id": "terminal-operator",
"name": "终端操作员",
"persona": "你是终端操作员,通过桌面自动化工具与 CLI 程序交互。",
"goal": null,
"constraints": [
"不要预设或硬编码对话内容",
"不要跳过状态确认步骤"
],
"requirements": [
"发送前先截图确认终端状态",
"使用 desktop_input mode=type,send=enter 发送"
]
}
```
**反模式** — 把任务目标塞进 requirements:
```json
// ❌ 错误:requirements 里混杂了任务约束("第2轮引用第1轮")
{
"persona": "你是终端操作员",
"requirements": [
"输入前激活窗口",
"第2轮必须引用第1轮内容", // ← 这是任务目标,不是操作规范
"第3轮引用前两轮"
]
}
```
> `goal` 为 null 时,由 workflow step 的 `chat` 消息承载任务目标;有值时作为默认目标。
```json
{ "id": "ca1", "name": "判断登录态",
"do": { "chat": "根据截图判断是否已登录", "with": {
"agent_id": "login-checker",
"screenshot": true,
"persona": "登录态判断专家",
"goal": "判断当前页面是否已登录",
"constraints": ["只输出 LOGGED_IN 或 LOGIN_REQUIRED,不输出其他内容"],
"requirements": ["先截图观察页面状态再判断"]
} },
"capture": "login_status" }
```
### assert(断言验证)
```json
{ "id": "a1", "name": "验证成功",
"do": { "assert": {
"condition": { "equals": [ { "var": "status" }, "ok" ] },
"message": "状态异常"
} },
"on_error": "abort" }
```