---
name: qqbrowser-playbook
description: Playbook management skill for generating, listing, and running parameterized browser automation replay scripts. Use after completing a browser automation task with qqbrowser-skill to convert the recorded steps into a reusable, parameterized playbook.
source: local
homepage: https://browser.qq.com/
permissions:
  - filesystem: Required for reading/writing playbook files in the playbooks directory
  - network: Required for executing replay via qqbrowser-skill
---

# qqbrowser-playbook

Convert raw `qqbrowser-skill` task recordings into reusable, parameterized replay scripts.

## Critical Rules

1. A playbook **MUST** be generated from actual task recording (`task_latest`). NEVER fabricate steps.
2. Only use actions from `task_latest` — copy action names and index values **verbatim**.
3. Only replace **user data** (`text`, `url`, file path) with `{{param_name}}`. NEVER replace `action` or `index`.
4. Save to `~/.qqbrowser-skill/playbooks/<kebab-case-name>.json`. Create dir first: `mkdir -p ~/.qqbrowser-skill/playbooks`.
5. JSON structure: `version` + `metadata` + `params` + `settings` + `steps`. Key is `params` not `parameters`.

## Valid Actions

### ✅ Allowed (replayable)

| Action | Parameterizable Fields |
|--------|----------------------|
| `browser_go_to_url` | `url` |
| `browser_click_element` / `browser_dblclick_element` / `browser_focus_element` | — |
| `browser_input_text` / `browser_keyboard_op` | `text` |
| `browser_keypress` / `browser_keydown` / `browser_keyup` | — |
| `browser_select_dropdown_option` | `text` |
| `browser_check_op` | — |
| `browser_scroll_down` / `browser_scroll_up` / `browser_scroll_to_top` / `browser_scroll_to_bottom` / `browser_scroll_by` / `browser_scroll_into_view` | — |
| `browser_scroll_to_text` | `text` |
| `browser_wait` | — |
| `browser_go_back` | — |
| `browser_dialog` | `text` |
| `browser_tab_open` | `url`, `as` (alias). Use semantic refs, NOT physical `tabId` |
| `browser_tab_switch` / `browser_tab_close` | `tab` (`"origin"` / `"current"` / `"latest"` / custom alias) |
| `browser_download_file` / `browser_download_url` | — |
| `browser_eval_content_js` | `script` |
| `browser_find_and_act` | `value`, `actionValue`, `name`, `nth` |
| `loop` | `count`, `variable`, `start`, `on_error`, nested `steps` |

### ❌ Excluded (filter out from recording)

`browser_snapshot`, `browser_screenshot`, `browser_get_info`, `browser_check_state`, `browser_get_dropdown_options`, `browser_tab_list`, `task_begin`/`task_end`/`task_latest`, `browser_done`

## Playbook JSON Format

```json
{
  "version": "1.0",
  "metadata": {
    "name": "小红书发布笔记",
    "description": "自动打开小红书并发布一篇笔记",
    "created_at": "2026-05-21T16:59:00Z",
    "url": "https://www.xiaohongshu.com/explore",
    "requires_ai": false
  },
  "params": {
    "notes_title": { "description": "笔记标题", "required": true },
    "notes_content": { "description": "笔记正文内容", "required": true }
  },
  "settings": { "default_delay_ms": 1000, "default_retry_count": 1, "step_timeout_ms": 30000 },
  "steps": [
    { "action": "browser_go_to_url", "params": { "url": "https://www.xiaohongshu.com/explore" }, "description": "打开小红书探索页" },
    { "action": "browser_input_text", "params": { "index": "6_2vl4_txwg", "text": "{{notes_title}}" }, "description": "输入笔记标题" },
    { "action": "browser_click_element", "params": { "index": "19_oxgq_ttmr" }, "description": "点击发布" }
  ]
}
```

**Fields**: `version`(always "1.0"), `metadata.name`, `metadata.description`, `metadata.created_at`(ISO8601) are required. `metadata.url`, `metadata.requires_ai`(default false) are optional. Each param needs `description`(string) and `required`(bool), optional `default`. Steps need `action`, `params`, `description`; optional `condition: {"param":"xx"}` for conditional execution.

## Parameter Extraction

**Parameterize**: user text, emails, usernames, quantities, file paths, user-specific URL segments.
**Never parameterize**: `index`, `action`, fixed workflow URLs, fixed UI buttons, `settings`.
**Naming**: `snake_case`, descriptive (`notes_title` not `title`).

## Conditional Steps

Add `condition: {"param": "cc"}` to ALL steps dependent on an optional param. If param absent, step is skipped.

## Loop Steps

Use `loop` when recording shows **2+ repetitions** of the same action sequence differing only in target element, and count is user-controlled.

**Do NOT use loop** for fixed form fields or when a single `browser_eval_content_js` can extract all data at once.

### Loop JSON

```json
{
  "action": "loop",
  "params": { "count": "{{count}}", "variable": "i", "start": 1 },
  "description": "逐个访问前N篇文章",
  "steps": [ ... ]
}
```

Fields: `count`(string/number, supports `{{param}}`), `variable`(loop var name), `start`(default 1, 1-based for `nth`), `on_error`(optional, see below).

### `on_error` — Dynamic List Handling

When the list length is **unknown or dynamic** (e.g., "claim all free games", "process all search results"), use `on_error: "break"`:

```json
{
  "action": "loop",
  "params": { "count": "100", "variable": "i", "start": 1, "on_error": "break" },
  "description": "处理所有列表项（自动检测结束）",
  "steps": [ ... ]
}
```

| `on_error` | Behavior |
|---|---|
| `"fail"` (default) | Sub-step failure → entire replay fails |
| `"break"` | Sub-step failure → **exit loop gracefully**, replay continues as success |

**When to use `on_error: "break"`:**
- List length is unknown at script generation time
- Task is "process ALL items" (not a fixed count)
- Set `count` to a large upper bound (e.g., 100), loop breaks when `find_and_act` can't find the Nth element

### Key Rules for Loop Steps

1. **Use `browser_find_and_act` with `by: "css"` + `nth: "{{i}}"`** inside loops — NOT `browser_click_element` with hardcoded index.
2. **`by` MUST be `"css"` when `nth` is used** — NEVER `by: "text"` or `by: "label"` with `nth`. Text/label is for single unique elements, not list iteration.
3. **`value` must be a list-level CSS selector** matching ALL items (e.g., `.ContentItem h2 a`), NOT specific text content.
4. **CSS selector MUST come from actual DOM**, with this priority:
   - **Best**: Extract from `__list_probe__` in recording (see Step 2b)
   - **Good**: Use `by: "css"` selector already in recording's `find_and_act`
   - **Fallback**: Use `browser_get_info --type list_selector --index <id>` to auto-detect, or run `browser_eval_content_js` probe to discover (before generating playbook)
   - **NEVER**: Fabricate or guess. If all above fail, set `metadata.requires_ai: true`.
5. **`{{i}}` is 1-based** (start=1), aligning with `nth` (nth=1 = first element).
6. **Check recording for tab operations in each iteration** — if recording has `tab_switch`/`tab_close`, loop body MUST use `tab_switch("latest")` → ... → `tab_close("current")` → `tab_switch("origin")`. **NEVER silently convert tab-based recording to `go_back`.**
7. **Use `browser_go_back` ONLY** if iteration stays in the **same tab** (no tab ops in recording).
8. **Include `browser_wait`** between iterations for stability.

### Decision Tables

**Loop vs Single JS Extraction:**

| Scenario | Method |
|----------|--------|
| List page data only | Single `browser_eval_content_js` |
| Need detail page content | `loop` (navigate in/out per item) |
| Pagination + extract | `loop` |
| Fixed different form fields | Linear steps |
| Process ALL items (unknown count) | `loop` with `on_error: "break"` + large `count` |

**`go_back` vs `tab_close` + `tab_switch`:**

| Scenario | Method |
|----------|--------|
| Link opens in **same tab** | `browser_go_back` |
| Link opens in **new tab** | `browser_tab_close` + `browser_tab_switch` |
| Unsure | Prefer `tab_close` + `tab_switch` (safer) |

## Tab Management

Playbooks **MUST use semantic tab references** — physical `tabId` changes every session.

| Reference | Meaning |
|-----------|---------|
| `"origin"` | Tab active when replay started (always available) |
| `"current"` | Currently active tab (dynamic) |
| `"latest"` | Most recently opened tab (after any `tab_open`) |
| Custom alias | Named via `"as"` param in `tab_open` |

**Tab action examples:**
```json
{ "action": "browser_tab_open", "params": { "url": "https://example.com", "as": "detail" } }
{ "action": "browser_tab_switch", "params": { "tab": "origin" } }
{ "action": "browser_tab_close", "params": { "tab": "current" } }
```

**Rules:**
1. NEVER use physical `tabId` — always semantic references.
2. `browser_go_back` does NOT work across tabs — use `tab_close` + `tab_switch`.
3. After `tab_close`, explicitly `tab_switch` — don't assume Chrome's active tab.

## Workflow

```bash
# 1. Get recording
qqbrowser-skill task_latest

# 2. AI analyzes → generates playbook:
#    2a: Filter excluded actions (snapshot, screenshot, tab_list, etc.)
#    2b: Extract CSS selector from __list_probe__ (see below)
#    2c: Detect loop patterns (repeated sequences → loop, using selector from 2b)
#    2d: Detect tab patterns (tabId → semantic refs). NEVER silently drop tab ops.
#    2e: Cross-check loop+tab — if loop iteration has tab ops, use tab_switch/close NOT go_back
#    2f: Identify parameterizable values → {{param}}
#    2g: Add descriptions, metadata, settings

# 3. Save
mkdir -p ~/.qqbrowser-skill/playbooks
# write_to_file tool

# 4. Run
# ⚠️ browser_replay can take UP TO 10 MINUTES for complex playbooks with loops.
# You MUST wait patiently for the command to finish. Do NOT assume it has timed out.
qqbrowser-skill browser_replay --script ~/.qqbrowser-skill/playbooks/<name>.json \
  --variables '{"param1": "value1"}'
```

> **⚠️ IMPORTANT**: `browser_replay` executes all playbook steps sequentially in the browser. Depending on step count, delays, and network conditions, **execution can take up to 10 minutes**. Wait for the command to return a result before proceeding — do NOT interrupt or retry prematurely.

### Step 2b: Extract CSS Selector from Recording

**Primary source**: Look for `browser_eval_content_js` calls whose output contains `__list_probe__: true`.
```
Recording data example:
  browser_eval_content_js → output: {"__list_probe__": true, "selector": ".SearchResult-Card .ContentItem h2 a", "count": 10, "samples": ["文章A", "文章B", "文章C"]}
```
Extract the `selector` value — this is the **verified CSS selector** from actual DOM, use it directly in the loop's `find_and_act`.

**Fallback** (if no `__list_probe__` in recording):
1. Check if recording's `find_and_act` already uses `by: "css"` — use that selector directly
2. If recording only has `by: "text"` or `click_element(index)`, the selector is unknown
3. Use `browser_get_info --type list_selector --index <any_list_item_index>` to auto-detect the selector
4. If `list_selector` also fails, run `browser_eval_content_js` with a probe script to discover the selector
5. **NEVER** guess or fabricate selectors

### Step 2c: Loop Pattern Detection

```
1. Group consecutive steps into sequences
2. If 2+ adjacent sequences share the SAME action pattern → convert to `loop`
3. What varies between iterations:
   - `nth` increments → use `{{i}}`
   - Different index values → replace with `find_and_act` + `nth: "{{i}}"`
   - Different text/title in `find_and_act(by="text")` → convert to `by: "css"` + `nth: "{{i}}"`
4. Use the CSS selector from Step 2b (from __list_probe__ or fallback probe)
5. Convert `browser_click_element(index=...)` → `find_and_act(by="css", nth="{{i}}")`
6. Count source: user-mentioned quantity → `{{count}}` param; fixed → literal
```

**Recording patterns → playbook conversion:**

| Recording Pattern | Conversion |
|---|---|
| Repeated sequences with different text targets | `loop` + `by: "css"` + `nth: "{{i}}"` |
| Repeated sequences with different `index` values | `loop` + `by: "css"` + `nth: "{{i}}"` |
| `find_and_act(by="text", value="标题A")` → ... → `find_and_act(by="text", value="标题B")` | **MUST** convert `by: "text"` → `by: "css"` + `nth: "{{i}}"` |

### Step 2d: Tab Pattern Detection

```
1. `browser_tab_open` → discard physical tabId, add semantic `"as"` alias
2. `browser_tab_switch(tabId=N)` → replace with "origin"/"latest"/custom alias
3. `browser_tab_close(tabId=N)` → replace with semantic ref
4. `browser_go_back` after new tab → replace with tab_close("current") + tab_switch("origin")
```

## Complete Example: Loop + New Tab

This example demonstrates ALL key conversions (text→css, tabId→semantic, linear→loop):

Recording (raw):
```
eval_content_js → {"__list_probe__": true, "selector": ".ContentItem h2 a", "count": 10, "samples": ["文章标题A","文章标题B","文章标题C"]}
find_and_act(by="text", value="文章标题A", action="click")
click_element(index="197_3i4c_uv7n")        ← opened new tab
tab_switch(tabId=1412776911)
eval_content_js(script="...")
tab_close(tabId=1412776911)
find_and_act(by="text", value="文章标题B", action="click")
click_element(index="206_90r2_rddo")
tab_switch(tabId=1412776913)
eval_content_js(script="...")
tab_close(tabId=1412776913)
```

Playbook generator extracts selector `.ContentItem h2 a` from the `__list_probe__` line.

Playbook (generated):
```json
{
  "version": "1.0",
  "metadata": {
    "name": "搜索并提取文章(新Tab场景)",
    "description": "搜索主题，逐个在新Tab打开文章提取内容",
    "created_at": "2026-06-02T15:00:00Z",
    "requires_ai": false
  },
  "params": {
    "topic": { "description": "搜索关键词", "required": true },
    "count": { "description": "提取数量", "required": false, "default": "3" }
  },
  "settings": { "default_delay_ms": 2000, "default_retry_count": 1, "step_timeout_ms": 30000 },
  "steps": [
    { "action": "browser_go_to_url", "params": { "url": "https://example.com/search?q={{topic}}" }, "description": "搜索主题" },
    { "action": "browser_wait", "params": { "seconds": 3 }, "description": "等待搜索结果" },
    {
      "action": "loop",
      "params": { "count": "{{count}}", "variable": "i", "start": 1 },
      "description": "逐个在新Tab打开文章并提取",
      "steps": [
        { "action": "browser_find_and_act", "params": { "by": "css", "value": ".ContentItem h2 a", "nth": "{{i}}", "action": "click" }, "description": "点击第{{i}}篇文章" },
        { "action": "browser_tab_switch", "params": { "tab": "latest" }, "description": "切到文章Tab" },
        { "action": "browser_wait", "params": { "seconds": 2 }, "description": "等待加载" },
        { "action": "browser_eval_content_js", "params": { "script": "JSON.stringify({title:document.querySelector('h1')?.textContent?.trim(),content:document.querySelector('.RichText')?.textContent?.substring(0,2000)})" }, "description": "提取内容" },
        { "action": "browser_tab_close", "params": { "tab": "current" }, "description": "关闭文章Tab" },
        { "action": "browser_tab_switch", "params": { "tab": "origin" }, "description": "切回列表页" },
        { "action": "browser_wait", "params": { "seconds": 1 }, "description": "等待恢复" }
      ]
    }
  ]
}
```

**Key conversions:** `by:"text"`+title → `by:"css"`+`nth:"{{i}}"` | `click_element(index)` → merged into `find_and_act` | `tabId:N` → `"latest"`/`"current"`/`"origin"` | 2× linear → `loop`

## `requires_ai` Tasks

Set `metadata.requires_ai: true` when recording contains AI-dependent steps (summarization, comparison, conditional decisions). Include only replayable portions as steps.

## `browser_find_and_act` Usage

For single-element targeting (NOT in loops): `by: "text"` or `by: "label"` with known stable text.
For list iteration (in loops): `by: "css"` + `nth: "{{i}}"` with list-level selector.

## `browser_eval_content_js` Usage

Primary data extraction method. Supports `{{param}}` in script for selectors, counts, keywords.
Selector priority: `id` > `data-*` > ARIA > semantic class > structural path. NEVER use dynamic/hashed classes.

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Including `browser_snapshot` etc. in steps | Filter out — AI-decision only |
| Fabricating steps without `task_latest` | Always base on actual recording |
| Parameterizing `index` or `action` | Only parameterize value fields (`text`, `url`, `script`) |
| Using `parameters` instead of `params` | Schema key is `params` |
| Expanding N repetitions as linear steps | Use `loop` with `{{count}}` |
| Using `click_element` + hardcoded index in loops | Use `find_and_act` + `nth: "{{i}}"` |
| Using `by: "text"`/`"label"` with `nth` in loops | `nth` requires `by: "css"` |
| Keeping `by: "text"` + specific content in loops | Recording's `find_and_act(text, "标题")` MUST convert to `by: "css"` + `nth` in loop |
| Fabricating CSS selectors | Must come from `__list_probe__` in recording, or existing `find_and_act(css)`, or a live probe — never guess |
| Using search keyword as `value` in loop | `value` is CSS selector (`.ContentItem h2 a`), not the keyword |
| `find_and_act(text)` + `click_element(index)` for same target | Merge into single `find_and_act(css, nth)` in loop |
| Hardcoding physical `tabId` | Use semantic refs: `"origin"`, `"current"`, `"latest"`, custom alias |
| `browser_go_back` after new tab opened | Use `tab_close` + `tab_switch` — new tab has no history |
| Silently dropping tab ops from recording | If recording has tab ops, playbook MUST include tab management |
| Loop with `go_back` when recording had tab ops | Check recording — if tab_switch/close exists, use tab management not go_back |
