# OpenCLI Advanced Patterns & Best Practices

## 设计原则

### 1. 状态驱动架构（State-Driven Architecture）

每个操作都遵循这个流程：

```
1. 获取状态 (Get State)
   └─→ 查看当前页面的元素和结构
   
2. 分析选项 (Analyze Options)
   └─→ 根据元素 ID 决定下一步
   
3. 执行操作 (Execute Action)
   └─→ 点击、输入、选择等
   
4. 循环迭代 (Loop)
   └─→ 返回第 1 步，继续下一步
```

**为什么这样做？**
- **零 Vision 成本**：状态查询不需要截图或视觉模型
- **确定性**：同一页面永远返回相同的元素 ID
- **快速**：毫秒级响应，无需等待 AI 模型
- **可靠**：不依赖 OCR 或视觉识别，避免误识别

### 2. 元素 ID vs 选择器

**使用 ID（优先）**：
```bash
# ✅ 好 - 来自 state 的 ID
opencli operate click 5
opencli operate type 10 "text"
```

**使用选择器（必要时）**：
```bash
# ⚠️ 仅在 state 中的元素不完整时使用
opencli operate wait "css-selector"
opencli operate eval 'document.querySelector("selector")'
```

### 3. 批量操作优化

**不好的方式**：每个操作后都获取状态
```bash
# ❌ 5 次往返，很慢
opencli operate click 0
opencli operate state                 # 不必要
opencli operate type 5 "text"
opencli operate state                 # 不必要
opencli operate click 10
```

**好的方式**：批量操作后再获取状态
```bash
# ✅ 1 次往返，高效
opencli operate click 0
opencli operate type 5 "text"
opencli operate click 10
opencli operate state                 # 一次性获取
```

## 常见任务模式

### 模式 1：分页遍历

```bash
#!/bin/bash
# 遍历所有页面并提取数据

for page in {1..5}; do
    echo "Page $page:"
    
    # 获取当前页面数据
    bash opencli_operate.sh --operation eval --script '
        Array.from(document.querySelectorAll("article")).map(article => ({
            title: article.querySelector("h2")?.textContent.trim(),
            url: article.querySelector("a")?.href
        }))
    '
    
    # 点击下一页按钮（假设 ID=20）
    if [ $page -lt 5 ]; then
        bash opencli_operate.sh --operation click --element-id 20
        bash opencli_operate.sh --operation wait --selector ".pagination.active"
    fi
done
```

### 模式 2：动态内容加载（Infinite Scroll）

```bash
#!/bin/bash
# 处理无限滚动的页面

# 初始化集合
touch /tmp/collected_items.json
TOTAL_ITEMS=0
LAST_COUNT=0

# 循环滚动
for i in {1..10}; do
    echo "Scroll iteration $i..."
    
    # 获取当前数据
    bash opencli_operate.sh --operation eval --script '
        JSON.stringify(
            Array.from(document.querySelectorAll(".item")).map(el => el.id)
        )
    ' > /tmp/current_items.json
    
    CURRENT_COUNT=$(cat /tmp/current_items.json | jq 'length')
    
    # 检查是否新增数据
    if [ "$CURRENT_COUNT" -eq "$LAST_COUNT" ]; then
        echo "没有新数据，停止滚动"
        break
    fi
    
    LAST_COUNT=$CURRENT_COUNT
    echo "当前收集: $CURRENT_COUNT 条"
    
    # 滚动到底部
    bash opencli_operate.sh --operation eval --script '
        window.scrollBy(0, window.innerHeight)
    '
    
    # 等待加载
    sleep 2
done
```

### 模式 3：表单自动填充

```bash
#!/bin/bash
# 自动填充复杂表单

# 第一步：获取表单状态
bash opencli_operate.sh --operation eval --script '
    const form = document.querySelector("form");
    const fields = {};
    form.querySelectorAll("input, textarea, select").forEach(field => {
        fields[field.name || field.id] = {
            type: field.type,
            value: field.value,
            placeholder: field.placeholder
        };
    });
    JSON.stringify(fields)
' > /tmp/form_fields.json

cat /tmp/form_fields.json | jq .

# 第二步：填充表单
# （根据返回的字段结构进行对应的填充）

declare -A form_data=(
    [email]="user@example.com"
    [password]="secret123"
    [name]="John Doe"
    [country]="CN"
)

# 第三步：定位和填充每个字段
bash opencli_operate.sh --operation type --element-id 2 "${form_data[email]}"
bash opencli_operate.sh --operation type --element-id 3 "${form_data[password]}"
bash opencli_operate.sh --operation type --element-id 4 "${form_data[name]}"
bash opencli_operate.sh --operation select --element-id 5 "${form_data[country]}"

# 第四步：提交表单
bash opencli_operate.sh --operation click --element-id 10

# 第五步：等待成功
bash opencli_operate.sh --operation wait --selector ".success-message"
```

### 模式 4：处理弹窗和 Modal

```bash
#!/bin/bash
# 检测和处理弹窗

# 获取页面状态
STATE=$(bash opencli_operate.sh --operation eval --script '
    JSON.stringify({
        hasModal: !!document.querySelector(".modal, .dialog, [role=dialog]"),
        modalText: document.querySelector(".modal")?.textContent.substring(0, 100),
        closeButtonId: Array.from(document.querySelectorAll("button")).findIndex(
            b => b.textContent.includes("关闭") || b.textContent.includes("Close")
        )
    })
')

echo "$STATE" | jq .

# 如果有弹窗，关闭它
if echo "$STATE" | jq -e '.hasModal' > /dev/null; then
    CLOSE_ID=$(echo "$STATE" | jq '.closeButtonId')
    bash opencli_operate.sh --operation click --element-id $CLOSE_ID
    sleep 1
fi
```

### 模式 5：多浏览器标签页管理

```bash
#!/bin/bash
# 在多个标签页间切换操作

# 获取所有标签页
TABS=$(bash opencli_operate.sh --operation eval --script '
    JSON.stringify({
        count: window.length,
        urls: Array.from(document.querySelectorAll("a")).map(a => a.href)
    })
')

# 新建标签页
bash opencli_operate.sh --operation eval --script 'window.open("https://example.com", "_blank")'

# 切换标签页（假设有 3 个标签）
for tab_index in {0..2}; do
    bash opencli_operate.sh --operation eval --script "window.open('', '_blank'); window.focus();"
done

# 关闭不需要的标签
bash opencli_operate.sh --operation eval --script 'window.close()'
```

### 模式 6：API 请求监听

```bash
#!/bin/bash
# 监听并提取网络请求

# 安装拦截器
bash opencli_operate.sh --operation eval --script '
    window.__captured_requests = [];
    
    // 监听 fetch
    const originalFetch = window.fetch;
    window.fetch = function(...args) {
        window.__captured_requests.push({
            method: "fetch",
            url: args[0],
            timestamp: new Date().toISOString()
        });
        return originalFetch.apply(this, args);
    };
    
    // 监听 XMLHttpRequest
    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        window.__captured_requests.push({
            method: method,
            url: url,
            timestamp: new Date().toISOString()
        });
        return originalOpen.apply(this, arguments);
    };
    
    "监听器已安装"
'

# 执行操作（会自动记录请求）
bash opencli_operate.sh --operation click --element-id 5

# 等待请求完成
sleep 2

# 获取记录的请求
bash opencli_operate.sh --operation eval --script '
    JSON.stringify(window.__captured_requests)
' > /tmp/captured_requests.json

cat /tmp/captured_requests.json | jq .
```

## 性能优化技巧

### 1. 减少状态查询

```bash
# ❌ 频繁调用 state（慢）
for i in {1..100}; do
    bash opencli_operate.sh --operation state
done

# ✅ 使用 JavaScript 批量操作
bash opencli_operate.sh --operation eval --script '
    const results = [];
    for (let i = 0; i < 100; i++) {
        results.push({
            id: i,
            text: document.querySelector(".item-" + i)?.textContent
        });
    }
    JSON.stringify(results)
'
```

### 2. 缓存静态内容

```bash
# 获取一次，多次使用
PAGE_STATE=$(bash opencli_operate.sh --operation state --format json)

# 从缓存中查询
echo "$PAGE_STATE" | jq '.state.buttons[0]'
echo "$PAGE_STATE" | jq '.state.inputs[0]'
```

### 3. 并行操作（谨慎）

```bash
# ⚠️ 只能用于不冲突的操作
bash opencli_operate.sh --operation type --element-id 2 "text1" &
bash opencli_operate.sh --operation type --element-id 3 "text2" &
wait  # 等待完成
```

## 错误处理和恢复

### 1. 重试机制

```bash
#!/bin/bash
# 失败自动重试

retry_operation() {
    local operation=$1
    local element_id=$2
    local value=$3
    local max_attempts=3
    local attempt=0
    
    while [ $attempt -lt $max_attempts ]; do
        if bash opencli_operate.sh --operation "$operation" \
           --element-id "$element_id" \
           --value "$value" 2>/dev/null; then
            return 0
        fi
        
        attempt=$((attempt + 1))
        echo "Attempt $attempt failed, retrying..."
        sleep 2
    done
    
    echo "Operation failed after $max_attempts attempts"
    return 1
}

# 使用
retry_operation "type" 5 "retry_text"
```

### 2. 超时处理

```bash
#!/bin/bash
# 设置超时限制

timeout_operation() {
    local timeout=10
    local command=$@
    
    timeout $timeout bash opencli_operate.sh $command
    
    if [ $? -eq 124 ]; then
        echo "Operation timed out after ${timeout}s"
        return 1
    fi
}
```

### 3. 状态验证

```bash
#!/bin/bash
# 执行前后验证状态

verify_action() {
    local action=$1
    local expected_state=$2
    
    # 执行操作
    bash opencli_operate.sh --operation $action
    
    # 等待状态变化
    sleep 1
    
    # 验证
    STATE=$(bash opencli_operate.sh --operation state --format json)
    
    if echo "$STATE" | jq -e "$expected_state" > /dev/null; then
        echo "✅ Action succeeded"
        return 0
    else
        echo "❌ Action failed"
        return 1
    fi
}

# 使用
verify_action "click 5" '.state.title | contains("Success")'
```

## 调试技巧

### 1. 保存页面快照用于分析

```bash
# 保存完整 HTML
bash opencli_operate.sh --operation eval --script '
    JSON.stringify({
        html: document.documentElement.outerHTML,
        title: document.title,
        url: window.location.href
    })
' > /tmp/page_dump.json

# 提取 HTML
cat /tmp/page_dump.json | jq -r '.html' > /tmp/page.html

# 在浏览器中查看
# open /tmp/page.html
```

### 2. 记录所有操作

```bash
#!/bin/bash
# 操作日志

log_operation() {
    echo "[$(date +%T)] $@" >> /tmp/opencli.log
}

log_operation "Started automation"
bash opencli_operate.sh --operation click --element-id 5
log_operation "Clicked element 5"

# 查看日志
tail -f /tmp/opencli.log
```

### 3. 条件测试

```bash
# 测试选择器是否存在
bash opencli_operate.sh --operation eval --script '
    !!document.querySelector(".target-element")
'

# 检查元素可见性
bash opencli_operate.sh --operation eval --script '
    const el = document.querySelector(".target-element");
    el && el.offsetParent !== null  // 可见性检查
'
```

## 安全考虑

### 1. 敏感数据处理

```bash
# ❌ 不要在日志中记录密码
# bash opencli_operate.sh --operation type 5 "$PASSWORD" 2>&1 | tee -a log.txt

# ✅ 安全地处理敏感数据
bash opencli_operate.sh --operation type 5 "$PASSWORD" > /dev/null 2>&1
echo "[$(date +%T)] Entered password field" >> /tmp/opencli.log
```

### 2. Cookie 和认证

```bash
# 保护 Cookie 文件
bash opencli_operate.sh --operation eval --script '
    document.cookie
' > /tmp/cookies.json

chmod 600 /tmp/cookies.json  # 仅所有者可读
```

### 3. 验证脚本来源

```bash
# 只执行经过验证的 JavaScript
# 避免从不信任的源执行 eval

# ✅ 好
bash opencli_operate.sh --operation eval --script 'document.title'

# ❌ 差 - 从变量执行任意代码
# bash opencli_operate.sh --operation eval --script "$untrusted_script"
```

## 参考资源

- 官方项目：https://github.com/jackwener/opencli
- 支持的适配器列表：运行 `opencli list`
- 诊断工具：运行 `opencli doctor`
- 问题报告：https://github.com/jackwener/opencli/issues
