Skill: lanyasheng/browser-ops (version 3.0.0)

--- name: browser-ops description: >- AI Agent 的网页访问路由决策指南。全 CLI 架构，零 MCP 依赖，不占常驻上下文 token。 按成本逐级升级: WebFetch($0) → opencli web read($0,带Cookie) → Firecrawl → agent-browser → browser-use。 IMPORTANT: For sites with opencli adapters (74 sites), use `opencli <platform>` directly — NEVER WebFetch/web read. For other sites, start with WebFetch. NEVER jump to browser-use/agent-browser first. 覆盖四层场景: 搜索(Tavily/Brave/Exa/WebSearch/opencli 75站点) → 提取(WebFetch/opencli/Firecrawl) → 交互(opencli operate/agent-browser/browser-use) → 反爬(Zendriver)。 触发场景: 搜索 抓取 爬取 网页 打不开 403 拦截 截图 表单 填表 Cookie 登录态 内部网站 SSO 反爬 Cloudflare。 参见 deep-research (用于多源深度研究报告)、security-review (用于认证安全审计)。 不适用于: 跨主机远程浏览器控制、高并发爬取(>10页/分钟)、非 DOM 界面(Canvas/桌面软件)。 triggers: - 搜索|抓取|爬取|网页|打不开|403|拦截|截图|表单|填表|网站 - Cookie|登录态|内部网站|SSO|反爬|Cloudflare|内网 - 股价|热榜|Twitter|微博|知乎|小红书|HackerNews|B站 - scrape|crawl|fetch|browse|screenshot|cookie|anti-bot|web page - 打开.*链接|打开.*URL|打开.*网址|读.*链接|读.*网页|看.*网站|查.*网站 - 帮我看|帮我读|帮我搜|帮我查|帮我打开|帮我访问|帮我下载 - open.*url|read.*url|visit.*site|access.*page|download.*page license: MIT --- # Browser Operations — 网页访问路由决策指南 > 全 CLI 架构。所有工具通过 Bash 调用，零 MCP 依赖，不占常驻上下文。 > 核心原则：能用 HTTP 就不开浏览器，能用 opencli 就不用 browser-use。 ## 核心规则 MUST 从免费层开始。NEVER 直接跳到浏览器工具。 每次网页任务按这个顺序判断，**命中就停**： 0. **目标是 opencli 已适配平台？→ 直接 `opencli <platform> <command>`，跳过 WebFetch** 判断方法：URL 域名或用户意图命中已适配站点（`opencli list` 查看完整列表，74 个站点 454 条命令）。 常见已适配平台：twitter/x.com, zhihu, weibo, bilibili, hackernews, xiaohongshu, douyin, reddit, youtube, github, arxiv, bloomberg, linkedin, douban, jd, wikipedia, v2ex, tieba, spotify, steam, medium, substack, producthunt, stackoverflow, 36kr, weread, xueqiu 等。 这些平台有专属适配器，返回结构化数据，比 web read 更准确更高效。 1. WebFetch/WebSearch 能搞定（非已适配平台）？→ 用它，$0 2. 403/SSO/空？→ `opencli web read`，$0，Cookie 零配置 3. 需要 JS 渲染/结构化？→ `firecrawl scrape "url"` 4. 需要交互（≤3 步）？→ `opencli operate`，Cookie 零配置 5. 需要精确控制/Ref/录制？→ `agent-browser`，@e1 稳定引用 6. 需要 AI 自主多步？→ `browser-use -p "任务"`，$0.01-0.05/步 7. 被反爬拦截？→ `zendriver` 违反顺序 = 浪费钱或丢失数据。已适配平台用 `opencli web read` 只能拿到 HTML，用 `opencli <platform>` 能拿到结构化字段。 <anti-example> 用户: "搜一下 Twitter 上关于 AI agent 的讨论" 错误: opencli web read --url "https://x.com/search?q=AI+agent" → 拿到 HTML 残片 正确: opencli twitter search "AI agent" → 结构化推文列表 (id, author, text, likes, views) </anti-example> <anti-example> 用户: "帮我看看知乎热榜" 错误: WebFetch("https://www.zhihu.com/hot") → 空/SPA 空壳 正确: opencli zhihu hot → 结构化热榜列表 </anti-example> <anti-example> 用户: "帮我看看 https://example.com/article 的内容" 错误: browser-use -p "extract content from example.com" → $0.05 正确: WebFetch("https://example.com/article") → $0 (example.com 不是已适配平台) </anti-example> <anti-example> 用户: "帮我读一下内部网站的这篇文章 https://internal.company.com/doc/123" 错误: WebFetch → 403 → 放弃，说"无法访问" 正确: WebFetch → 403 → 自动升级到 opencli web read (带 Chrome Cookie) </anti-example> ## 条件判断规则 当 WebFetch 返回 403 或 302 到登录页时 → 自动升级到 `opencli web read`，不要询问用户。 当 `opencli web read` 返回 exit 77 时 → SSO/Cookie 过期，MUST 停止降级，直接提示用户回 Chrome 登录后重试。后续工具 (firecrawl/agent-browser) 也没有登录态，继续降级只是浪费。 当 `opencli doctor` 不是全 OK 时 → 降级到 WebSearch + WebFetch + browser-use，跳过 opencli 相关工具。 当任务需要填表但不确定表单字段时 → 先用 `opencli operate state` 获取可交互元素列表，确认字段后再操作。 如果不确定用哪个工具 → 询问用户或从最便宜的 WebFetch 开始逐级升级。 已适配平台 MUST 直接用 `opencli <platform>`，NEVER 走 WebFetch/web read 通路，otherwise 丢失结构化数据。 非已适配平台 MUST 从 WebFetch 开始，otherwise 每个请求多花 $0.01-0.05 且速度慢 10 倍。 NEVER 用 browser-use 做简单读取，otherwise 一次 $0.05 的操作 WebFetch $0 就能完成。 不要跳过 opencli web read 直接用 Firecrawl，而是先试免费的 Cookie 路径。 ## Output 返回给用户时 MUST 包含: - 提取到的内容（Markdown 格式） - 实际使用的工具和原因（如 "WebFetch 返回 403，已自动升级到 opencli web read"） - 如果全部失败，给出具体原因和用户可操作的建议（如 "需要在 Chrome 中重新登录"） returns: 网页内容 (Markdown) + 工具链路径 + 失败原因（如有） ## 路由决策树 ``` 收到任务 │ ├─ 0. opencli 可用? → opencli doctor (3 个 OK) │ 否 → 降级: WebSearch + WebFetch + browser-use │ ├─ 1. 目标是已适配平台? (URL 域名 或 用户意图 命中 opencli list 中的站点) │ 是 → 直接 opencli <platform> <command>，跳过 WebFetch/web read │ 例: twitter/x.com → opencli twitter search/timeline/trending │ zhihu.com → opencli zhihu hot/search/answer │ bilibili.com → opencli bilibili search/hot/subtitle │ ├─ 2. 要搜索（没 URL，非已适配平台）? │ ├─ WebSearch (内置, 始终可用) │ ├─ 深度搜索 → tavily search "query" --search-depth advanced │ ├─ 独立索引 → curl brave API │ └─ fallback → opencli google search │ ├─ 3. 有 URL，非已适配平台? │ ├─ WebFetch ($0) → 403/SSO? → opencli web read ($0, Cookie 直连) │ │ ├─ exit 77 (SSO 过期) → 停止降级，提示用户回 Chrome 登录 │ │ └─ 其他失败 (JS 不足/内容太短) → 继续降级到 firecrawl │ └─ JS 渲染/PDF/结构化 → firecrawl scrape "url" │ ├─ 4. 要交互? │ ├─ ≤3 步 → opencli operate (Cookie 零配置, 17 个命令) │ │ open → state → click/type/select/scroll → screenshot → close │ ├─ 需要稳定引用/录制/标注截图 → agent-browser (60+ 命令) │ │ open → snapshot -i → click @e1 → screenshot --annotate │ ├─ AI 自主多步 → browser-use -p "自然语言任务" │ └─ 未知 DOM → Stagehand act("点击登录") │ ├─ 5. 被反爬? → python -c "import zendriver as zd; ..." │ └─ 全失败 → 告知用户具体原因和建议，NEVER 静默失败 ``` ## 升级信号 | 当前工具返回 | 升级到 | 命令 | |------------|--------|------| | WebFetch → 403/302 登录页 | opencli web read | `opencli web read --url "url"` | | WebFetch → 空/SPA 空壳 | Firecrawl | `firecrawl scrape "url"` | | opencli → exit 77 | 停止降级，提示用户 | SSO 过期，后续工具也没登录态，直接提示用户回 Chrome 登录 | | 需要点击/填表 | opencli operate | `opencli operate open "url" && state` | | 编号 [N] 不稳定 | agent-browser | `agent-browser snapshot -i` → `click @e1` | | 多步复杂任务 | browser-use | `browser-use -p "任务描述"` | | Cloudflare 拦截页 | Zendriver | `python3 -c "import zendriver..."` | ## 搜索工具 ```bash # 内置 (始终可用) WebSearch → Claude Code 内置，直接调用 # Tavily — AI 原生搜索，返回 answer + results (免费 1000 次/月) tavily search "query" # pip install tavily-python tavily search "query" --search-depth advanced # 深度模式 tavily extract "https://url" # URL 内容提取 # Brave — 独立索引，不依赖 Google/Bing curl -s "https://api.search.brave.com/res/v1/web/search?q=query" \ -H "X-Subscription-[REDACTED_SECRET]" # Firecrawl — JS 渲染 + Markdown 提取 (免费 500 次) firecrawl scrape "https://url" # pip install firecrawl firecrawl crawl "https://url" --limit 10 # 批量 firecrawl map "https://url" # URL 发现 # 平台搜索 — 75 站点结构化数据 opencli twitter trending / zhihu hot / hackernews top / xiaohongshu search "旅行" opencli list # 查看所有适配器 ``` ## 浏览器交互工具 ```bash # opencli operate — 简单交互，Cookie 零配置 (通过 Chrome Extension 直连) opencli operate open "url" opencli operate state # 可交互元素 [1][2][3] opencli operate click 5 / type 3 "hello" / select 2 "选项A" opencli operate scroll down / keys Enter / wait text "Success" opencli operate screenshot /tmp/shot.png / network / close # agent-browser — 复杂交互，@e1 Ref 引用 (基于 Accessibility Tree，页面重渲染不变) agent-browser open "url" && agent-browser snapshot -i agent-browser click @e2 / fill @e3 "hello" # Ref 引用 agent-browser screenshot --annotate /tmp/a.png # 标注截图 agent-browser record start # 录制操作 agent-browser batch "click @e1 && wait 1000 && screenshot" agent-browser --auto-connect snapshot # 连接已运行 Chrome # 搭配 Lightpanda 省 [REDACTED_SECRET] ~500 token vs Chrome ~3000 token # ./lightpanda serve --port 9222 && agent-browser connect 9222 # browser-use — AI 自主操作 (LLM 决策循环，每步 $0.01-0.05) browser-use -p "去 example.com 注册账号" # 自然语言驱动 browser-use --connect -p "任务" # 连接已运行 Chrome browser-use run --remote "任务" # 云端执行 ``` ## opencli operate vs agent-browser | 维度 | opencli operate | agent-browser | |------|----------------|---------------| | 场景 | ≤3 步简单操作 | 复杂/精确操作 | | 元素定位 | [N] 编号（页面变了会变） | @e1 Ref（稳定，基于 ARIA role+name） | | Cookie | Chrome Extension 直连，零配置 | --profile / --auto-connect | | 标注截图 | ❌ | ✅ --annotate（给视觉模型用） | | 录制回放 | ❌ | ✅ record（固定流程自动化） | | 批量/DOM diff | ❌ | ✅ batch / diff | | 内核替换 | ❌ | ✅ Lightpanda（省 80% token） | | 命令数 | 17 | 60+ | | iOS 测试 | ❌ | ✅ -p ios | ## 前置条件 ```bash # 必装 npm i -g @jackwener/opencli # Chrome 装 OpenCLI Browser Bridge: $(npm root -g)/@jackwener/opencli/extension opencli doctor # 3 个 OK 才能用 # 按需 (全 CLI，不需要配 MCP) npm i -g agent-browser # Ref 引用/录制/标注截图 > See references/ for extended content.

path: README.md
content: # browser-ops

给 AI Agent 的网页访问路由表。全 CLI 架构，零 MCP 依赖，省 75% 上下文 token。

[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![Version](https://img.shields.io/badge/version-1.0.0-blue)](SKILL.md)

```
WebFetch ($0) → opencli web read ($0) → Firecrawl → agent-browser → browser-use ($0.05/步)
```

能用 HTTP 就不开浏览器，能用 opencli 就不用 browser-use。

## Quick Start

```bash
npm i -g @jackwener/opencli
# Chrome 装 OpenCLI Browser Bridge 扩展
opencli doctor  # 三个 OK 才能用
```

```bash
# 读内网页面（Cookie 零配置，直连 Chrome）
opencli web read --url "https://internal-site.com"

# 75 站点结构化数据
opencli twitter trending
opencli zhihu hot
opencli hackernews top
```

只需装 opencli。其他工具全部按需。

## 四层路由

| 层 | 场景 | 工具 | 费用 |
|----|------|------|------|
| **搜索** | 没有 URL，要找信息 | WebSearch → Tavily → Brave API → opencli search | $0-0.008 |
| **提取** | 有 URL，要内容 | WebFetch → opencli web read → Firecrawl | $0-0.001 |
| **交互** | 有 URL，要操作页面 | opencli operate → agent-browser → browser-use | $0-0.05 |
| **反爬** | 被拦截了 | Zendriver | $0 |

每次网页任务从最便宜的开始，命中就停。违反顺序 = 浪费钱。

## 核心工具

### opencli — Cookie 零配置，75 站点

通过 Chrome Extension Bridge 直连浏览器，天然复用 Cookie/登录态。

```bash
opencli web read --url "https://internal-site.com"     # 读内网页面
opencli operate open "url" && opencli operate state    # 浏览器交互
opencli list                                           # 查看所有 75 站点适配器
```

### agent-browser — @e1 Ref 引用，录制，标注截图

元素定位基于 Accessibility Tree，页面重渲染后 `@e1` 引用不变。

```bash
agent-browser open "url" && agent-browser snapshot -i  # 打开 + 快照
agent-browser click @e2 / fill @e3 "hello"             # Ref 操作
agent-browser screenshot --annotate /tmp/a.png         # 标注截图
agent-browser record start                            # 录制
```

### browser-use — AI 自主操作

自然语言驱动，LLM 自主规划步骤。每步 $0.01-0.05。

```bash
browser-use -p "去 example.com 注册账号"
browser-use --connect -p "任务"                        # 连接已运行 Chrome
```

## 交互工具对比

| 维度 | opencli operate | agent-browser | browser-use |
|------|----------------|---------------|-------------|
| 适合 | ≤3 步简单操作 | 复杂/精确操作 | AI 自主多步 |
| 元素定位 | [N] 编号 | @e1 Ref (稳定) | LLM 自主识别 |
| Cookie | Chrome 直连 | --profile | --connect |
| 标注截图 | — | `--annotate` | — |
| 录制回放 | — | `record` | — |
| 命令数 | 17 | 60+ | CLI 模式 |
| 费用/步 | $0 | $0 | $0.01-0.05 |

## 升级信号

| 当前工具返回 | 升级到 | 命令 |
|------------|--------|------|
| WebFetch → 403/302 登录页 | opencli web read | `opencli web read --url "url"` |
| WebFetch → 空/SPA 空壳 | Firecrawl | `firecrawl scrape "url"` |
| opencli → exit 77 | 手动登录 | Chrome 里重新登录后重试 |
| 需要点击/填表 | opencli operate | `opencli operate open "url"` |
| 编号 [N] 不稳定 | agent-browser | `agent-browser snapshot -i` |
| 多步复杂任务 | browser-use | `browser-use -p "任务"` |
| Cloudflare 拦截 | Zendriver | `python3 -c "import zendriver..."` |

## 为什么全 CLI 不用 MCP

MCP 工具定义常驻上下文（每个 ~250 tokens）。Playwright MCP 21 工具 = 5,250 tokens 每轮都占着。CLI 方式命令写在 SKILL.md 里，只在触发时加载。省 75% 上下文。

## Project Structure

```
browser-ops/
├── SKILL.md              # AI 路由决策指南（核心）
├── task_suite.yaml        # 21 个评估任务
├── scripts/
│   ├── sync-cookies.sh    # Cookie 同步/导入/导出/健康检查
│   ├── web-read.sh        # 三层读取回退链
│   ├── web-search.sh      # 三层搜索回退链
│   └── web-trending.sh    # 8 平台热榜回退链
├── references/
│   ├── routing.md         # 路由决策树详解
│   ├── setup.md           # 安装与配置
│   ├── opencli-usage.md   # opencli 三层用法
│   ├── state-management.md # Cookie 状态管理
│   └── anti-detection.md  # 反爬策略
├── evals/                 # 触发匹配评估
└── tests/                 # 9 个测试模块
```

## 已知限制

- opencli 依赖 Chrome Extension + Chrome 运行
- Tavily/Firecrawl 需 API key，未配置时回退 WebSearch/WebFetch
- agent-browser 和 opencli operate 可以同时运行，但操作同一个 tab 时注意冲突
- Cookie = 快照，SSO token 过期需回 Chrome 重新登录

## License

[MIT](LICENSE)


path: evals/compare_with_baseline.sh
content: #!/usr/bin/env bash
# browser-ops with-skill vs baseline comparison
# Usage: bash evals/compare_with_baseline.sh
# NOTE: This script is a reference template. The --skill-file flag requires Claude Code Pro.

set -euo pipefail

SKILL_DIR="/tmp/browser-ops"
RESULTS_DIR="$SKILL_DIR/evals/comparison_results"
mkdir -p "$RESULTS_DIR"

QUERIES=(
  "帮我抓取 https://paulgraham.com/worked.html 的正文"
  "获取 HackerNews 今天的热门"
  "查一下招商银行今天的股价"
  "帮我在小红书上搜索旅行攻略"
  "打开 https://example.com 并截图"
)

EXPECTED_TOOLS=(
  "jina|curl|r.jina.ai"
  "opencli|hackernews"
  "opencli|sinajs|akshare|stock"
  "opencli|xiaohongshu"
  "agent-browser|screenshot|playwright"
)

echo "=== browser-ops Comparison Test ==="
echo "Date: $(date)"
echo ""

pass=0
fail=0
total=${#QUERIES[@]}

for i in "${!QUERIES[@]}"; do
  query="${QUERIES[$i]}"
  expected="${EXPECTED_TOOLS[$i]}"
  echo "--- Test $((i+1))/$total ---"
  echo "Query: $query"
  echo "Expected tools: $expected"

  # Run WITH skill (using --skill-file to load SKILL.md)
  echo "  Running with skill..."
  result=$(timeout 30 env CLAUDE_SKILLS_PATH="$SKILL_DIR" claude -p "$query" --allowedTools '' 2>/dev/null || echo "TIMEOUT")

  # Check if expected tool/approach was mentioned
  matched=false
  IFS='|' read -ra patterns <<< "$expected"
  for pat in "${patterns[@]}"; do
    if echo "$result" | grep -qi "$pat"; then
      matched=true
      break
    fi
  done

  if $matched; then
    echo "  PASS: Correct tool selected"
    ((pass++))
  else
    echo "  FAIL: Expected pattern not found"
    echo "  Response preview: $(echo "$result" | head -3)"
    ((fail++))
  fi

  # Save full result
  echo "$result" > "$RESULTS_DIR/test_$((i+1))_with_skill.txt"
  echo ""
done

echo "=== Results ==="
echo "Passed: $pass/$total"
echo "Failed: $fail/$total"
echo "Results saved to: $RESULTS_DIR/"


path: evals/eval_results.json
content: {
  "skill_name": "browser-ops",
  "description": "Reset after SKILL.md v4.1.0 trigger keywords expansion (2026-04-04)",
  "results": [],
  "summary": {
    "total": 0,
    "passed": 0,
    "failed": 0,
    "pending_reeval": true
  }
}


path: evals/evals.json
content: {
  "skill_name": "browser-ops",
  "evals": [
    {
      "id": 1,
      "prompt": "帮我抓取 https://paulgraham.com/worked.html 的正文内容",
      "expected_output": "使用 WebFetch 或 Jina 提取文章正文为 Markdown",
      "expectations": [
        "选择轻量抓取（WebFetch/Jina/curl），不启动浏览器",
        "返回文章正文内容"
      ]
    },
    {
      "id": 2,
      "prompt": "搜索 Twitter 上关于 AI agent 的最新讨论",
      "expected_output": "使用 opencli twitter search 搜索",
      "expectations": [
        "使用 opencli 平台适配器",
        "调用 opencli twitter search 命令",
        "返回搜索结果"
      ]
    },
    {
      "id": 3,
      "prompt": "查一下招商银行今天的股价",
      "expected_output": "使用 opencli 或新浪 API 查询股价",
      "expectations": [
        "使用 opencli xueqiu stock 或 sinafinance stock",
        "返回包含价格的结构化数据"
      ]
    },
    {
      "id": 4,
      "prompt": "帮我同时查 100 只 A 股的实时行情",
      "expected_output": "识别为批量场景，使用批量 API 而非 opencli 串行",
      "expectations": [
        "识别出批量数据拉取场景（>10次/分钟）",
        "选择新浪 API 或 AKShare 批量接口",
        "不使用 opencli 串行 100 次"
      ]
    },
    {
      "id": 5,
      "prompt": "这个网站被 Cloudflare 拦截了，打不开: https://target-site.com",
      "expected_output": "使用 Zendriver 或 Camoufox 绕过反爬",
      "expectations": [
        "识别为反爬场景",
        "选择反爬工具（Zendriver 或 Camoufox）",
        "如果仍失败，建议添加住宅代理"
      ]
    },
    {
      "id": 6,
      "prompt": "帮我在小红书上搜索旅行攻略",
      "expected_output": "使用 opencli xiaohongshu search",
      "expectations": [
        "使用 opencli 而非浏览器",
        "调用 opencli xiaohongshu search 命令"
      ]
    },
    {
      "id": 7,
      "prompt": "打开 https://internal.company.com/dashboard 并截图",
      "expected_output": "使用 browser-use CLI 截图",
      "expectations": [
        "识别为需要登录态的内部站点",
        "使用 browser-use --connect 或 opencli web read 复用登录态",
        "使用 browser-use screenshot 截图"
      ]
    },
    {
      "id": 8,
      "prompt": "我需要在一个我从没用过的网站上填一个复杂表单",
      "expected_output": "使用 browser-use CLI 或 Stagehand",
      "expectations": [
        "识别为未知 DOM + 需要交互的场景",
        "选择 browser-use CLI 或 Stagehand"
      ]
    },
    {
      "id": 9,
      "prompt": "获取知乎热榜前 20 条",
      "expected_output": "使用 opencli zhihu hot",
      "expectations": [
        "使用 opencli zhihu hot",
        "返回热榜数据"
      ]
    },
    {
      "id": 10,
      "prompt": "帮我写一段 Python 代码来排序数组",
      "expected_output": "不触发 browser-ops，这是编码任务不是浏览器任务",
      "expectations": [
        "不触发 browser-ops skill",
        "直接回答编码问题"
      ],
      "should_not_trigger": true
    },
    {
      "id": 11,
      "prompt": "这个URL打不开 https://broken-link.invalid/page",
      "expected_output": "识别为访问失败场景，尝试替代方案或报告错误",
      "expectations": [
        "识别为访问失败场景",
        "尝试 opencli web read 或 browser-use 替代方案"
      ]
    },
    {
      "id": 12,
      "prompt": "帮我抓取这个网页但是它加载很慢经常超时",
      "expected_output": "识别为超时场景，使用适当的超时配置",
      "expectations": [
        "识别为超时场景",
        "使用适当的超时配置"
      ]
    },
    {
      "id": 13,
      "prompt": "帮我批量下载这50个网页的内容",
      "expected_output": "识别为批量场景，选择并发方案而非串行",
      "expectations": [
        "识别为批量场景",
        "选择并发方案而非串行"
      ]
    },
    {
      "id": 14,
      "prompt": "用浏览器打开 https://example.com 然后点击登录按钮",
      "expected_output": "选择 browser-use CLI 执行导航和点击操作",
      "expectations": [
        "选择 browser-use CLI",
        "执行导航和点击操作"
      ]
    }
  ]
}


path: evals/trigger_evals.json
content: query: 帮我抓取 https://paulgraham.com/worked.html 的正文内容
should_trigger: yes

query: 搜索 Twitter 上关于 AI agent 的最新讨论
should_trigger: yes

query: 查一下招商银行今天的股价
should_trigger: yes

query: 帮我同时查 100 只 A 股的实时行情
should_trigger: yes

query: 这个网站被 Cloudflare 拦截了打不开
should_trigger: yes

query: 帮我在小红书上搜索旅行攻略
should_trigger: yes

query: 打开内部网站并截图
should_trigger: yes

query: 获取知乎热榜前 20 条
should_trigger: yes

query: 帮我看看 HackerNews 今天的热门
should_trigger: yes

query: 下载这个网页的所有图片
should_trigger: yes

query: 这个URL打不开 https://broken-link.invalid/page
should_trigger: yes

query: 帮我抓取这个网页但是它加载很慢经常超时
should_trigger: yes

query: 这个网站需要验证码才能访问
should_trigger: yes

query: 帮我批量下载这50个网页的内容
should_trigger: yes

query: 用浏览器打开 https://example.com 然后点击登录按钮
should_trigger: yes

query: 帮我爬取这个电商网站的商品价格
should_trigger: yes

query: 微博热搜前十是什么
should_trigger: yes

query: 帮我用 cookie 登录这个网站
should_trigger: yes

query: 帮我写一段 Python 代码来排序数组
should_trigger: no

query: 这个 C++ 编译报错怎么修
should_trigger: no

query: 帮我重构这个函数
should_trigger: no

query: 解释一下什么是递归
should_trigger: no

path: references/anti-detection.md
content: # 反爬策略

## 反爬类型与方案

| 反爬类型 | 方案 | 状态 |
|---------|------|------|
| 普通网站 | opencli web read / agent-browser | ✅ |
| 需要登录 | CDP + Cookie 持久化 | ✅ 已验证 |
| 简单验证码 | 手动点一次，后续复用 Cookie | ✅ |
| Cloudflare 盾 | Zendriver / Camoufox | ✅ 已验证 |
| 强指纹检测 | Camoufox（Firefox 内核） | ✅ 已验证 |
| IP 封禁 | 代理池 | ✅ |

## Zendriver（首选，~90% bypass）

Nodriver 同作者的继任项目（async-first 重写），Chrome/Chromium 内核，自动规避常见反爬检测。相比 Nodriver 新增内置 session/cookie 持久化。

```python
import asyncio
import zendriver as zd

async def scrape(url: str):
    browser = await zd.start()
    page = await browser.get(url)
    await page.sleep(3)  # 等待 Cloudflare 验证通过
    content = await page.get_content()
    browser.stop()
    return content

content = asyncio.run(scrape("https://target-site.com"))
```

安装: `pip install zendriver`（需要 Python 3.10+）

> **Note**: Nodriver 已被 Zendriver 取代（同作者 ultrafunkamsterdam）。如已安装 nodriver 可继续使用，但新项目应使用 zendriver。

## Camoufox（备选，~80% bypass）

修改版 Firefox，更难被指纹识别。

```python
from camoufox.sync_api import Camoufox

with Camoufox(headless=True) as browser:
    page = browser.new_page()
    page.goto("https://target-site.com")
    page.wait_for_timeout(3000)
    content = page.content()
```

安装: `pip install camoufox && python3 -m camoufox fetch`

## 云端 fallback

当本地反爬不够时，使用云浏览器：

1. **Zyte**（首选）— 智能代理 + 浏览器渲染
2. **Browserless**（次选）— Docker 化浏览器
3. **Hyperbrowser**（第三）

## 代理配置

```bash
# 按环境配置代理，示例：
export HTTP_PROXY="http://your-proxy-host:port"
export HTTPS_PROXY="http://your-proxy-host:port"
export SOCKS_PROXY="socks5://your-proxy-host:port"

# agent-browser 使用代理
agent-browser --proxy $HTTP_PROXY open https://target-site.com

# Zendriver 使用代理
browser = await zd.start(browser_args=[f"--proxy-server={proxy_url}"])
```

## 住宅代理（Residential Proxy）

高级反爬场景（IP 封禁、地域限制）需要住宅代理。住宅代理使用真实家庭 IP，比数据中心代理更难被检测。

### 推荐供应商

| 供应商 | 价格 | IP 池 | 适用场景 |
|--------|------|-------|---------|
| **Smartproxy/Decodo** | $3-4.5/GB | 55M+ IPs | 性价比最佳，覆盖大多数场景 |
| **Bright Data** | $4-5/GB | 72M+ IPs | 最难目标（银行/政府/大厂反爬） |
| **IPRoyal** | $3.5-5.5/GB | — | 预算有限的备选 |

### 各工具配置住宅代理

**凭证通过环境变量传递**（不要在命令行参数中暴露明文密码）：

```bash
export PROXY_URL="socks5://user:[REDACTED_SECRET]:7777"
```

```python
import os
proxy_url = os.environ["PROXY_URL"]

# Camoufox + 住宅代理
with Camoufox(proxy={"server": proxy_url}) as browser:
    page = browser.new_page()
    page.goto("https://hard-target.com")

# Zendriver + 住宅代理
browser = await zd.start(browser_args=[f"--proxy-server={proxy_url}"])
```

```bash
# agent-browser + 住宅代理
agent-browser --proxy "$PROXY_URL" open https://hard-target.com
```

### "畅通无阻"组合

最高成功率组合：**Camoufox + 住宅代理**（Firefox 指纹伪装 + 真实家庭 IP）。

适用：被常规反爬拦截且 IP 也被封禁的高难度目标。

### Cloudflare AI Labyrinth (2026)

Cloudflare 于 2026 年推出 AI Labyrinth：不再直接封锁爬虫，而是返回 AI 生成的假页面引诱爬虫进入"迷宫"。症状：内容看似正常但数据完全虚假。应对：对抓取结果做内容校验（对比已知真实数据），检测到假页面时切换住宅代理 + 不同指纹重试。

> See also: `setup.md`（安装）, `routing.md`（路由升级条件）


path: references/extended-content.md
content: 
> See references/ for extended content.


path: references/opencli-usage.md
content: # opencli 使用指南

## 三层能力

### 1. web read — 读任意网页（Cookie 零配置）

```bash
opencli web read --url "https://internal-site.com"
```

通过 Chrome Extension Bridge 让浏览器自身发请求，天然带 Cookie。

### 2. operate — 浏览器交互（17 个命令）

```bash
opencli operate open "url"
opencli operate state                    # 可交互元素 [1][2][3]
opencli operate click 5 / type 3 "hello" / select 2 "选项"
opencli operate scroll down / keys Enter / wait text "Success"
opencli operate screenshot /tmp/shot.png / network / close
```

### 3. 平台适配器 — 75 站点结构化数据

```bash
opencli list                             # 查看所有适配器
opencli twitter trending / zhihu hot / hackernews top
opencli xiaohongshu search "旅行攻略"
opencli web read --url "https://any-url"  # 万能回退
```

### 自动生成适配器

```bash
opencli explore "https://new-site.com"   # 探测接口和策略
opencli generate "https://new-site.com"  # 生成 YAML 适配器
```

## 健康检查

```bash
opencli doctor    # 三个 OK: Daemon + Extension + Connectivity
```

## 常见问题

| 问题 | 解决 |
|------|------|
| exit 77 | Chrome 里重新登录目标网站 |
| Extension MISSING | 加载扩展: `$(npm root -g)/@jackwener/opencli/extension` |
| google search CAPTCHA | 回退到 WebSearch 或 Tavily |


path: references/routing.md
content: # 路由决策树

> 与 SKILL.md 保持一致。SKILL.md 是权威源。

## 四层模型

| 层 | 工具 | 场景 |
|----|------|------|
| **搜索层** | Tavily / Exa / Brave / WebSearch / opencli \<platform\> | 没有 URL，需要找信息 |
| **提取层** | WebFetch / opencli web read / Firecrawl | 有 URL，要内容 |
| **交互层** | opencli operate / agent-browser / browser-use | 有 URL，要操作页面 |
| **反爬层** | Zendriver | 被拦截 |

## 搜索层路由

```
要搜索？
├─ 实时新闻/深度 → Tavily (search_depth: advanced, 返回 answer + results)
├─ 语义/概念 → Exa MCP (neural search, 代码/论文质量高)
├─ 通用 → Brave Search (独立索引) / WebSearch (内置免费)
├─ 平台内 → opencli <platform> search (75 站点)
└─ fallback → opencli google search
```

### 搜索工具选择依据

- **Tavily**: AI Agent 事实标准，LangChain 默认，返回结构化 answer，1000 次/月免费
- **Exa**: 语义搜索最强，适合概念性问题和代码搜索
- **Brave**: Anthropic 官方推荐 MCP，独立索引不依赖 Google/Bing
- **WebSearch**: 内置零成本，通用性最好
- **opencli google search**: 实际 Google 结果，但高频 CAPTCHA

## 提取层路由

```
有 URL？
├─ 公开网页 → WebFetch ($0)
├─ 403/SSO → opencli web read (Chrome 登录态)
├─ 要深度提取 (JS 渲染/PDF/结构化) → Firecrawl scrape
├─ 批量站点 → Firecrawl crawl (异步)
└─ 已知平台 → opencli <platform> <cmd>
```

## 交互层路由

```
要交互？
├─ 简单 (点击/填表/截图, Cookie 直连) → opencli operate
├─ 复杂 (Ref 引用/录制/批量/标注截图) → agent-browser
├─ AI 自主多步 → browser-use -p "任务"
└─ 未知 DOM → Stagehand act/extract
```

### opencli operate vs agent-browser

| 维度 | opencli operate | agent-browser |
|------|----------------|---------------|
| 命令数 | 17 个 | 60+ 个 |
| 元素定位 | `[1]` 编号（可能变） | `@e1` Ref 引用（稳定） |
| Cookie | Chrome 直连零配置 | `--profile` / `--auto-connect` |
| 标注截图 | ❌ | ✅ `--annotate` |
| 录制回放 | ❌ | ✅ `record` |
| 批量命令 | ❌ | ✅ `batch` |
| DOM diff | ❌ | ✅ `diff` |
| iOS 测试 | ❌ | ✅ `-p ios` |
| 安装依赖 | 同 opencli | 独立 npm 包 |

**简单规则**: 3 步以内用 opencli operate，需要 Ref 引用或录制用 agent-browser。

## 降级模式

opencli 不可用时（Extension 没装 / Chrome 没开）:
- 搜索: Tavily / Exa / Brave / WebSearch
- 提取: WebFetch / Firecrawl
- 交互: browser-use / agent-browser（独立 Chromium）
- 反爬: Zendriver

> See also: `setup.md`（安装）, `state-management.md`（Cookie）


path: references/setup.md
content: # 安装与验证

## 必装

```bash
npm i -g @jackwener/opencli
# Chrome 装扩展: $(npm root -g)/@jackwener/opencli/extension
opencli doctor  # 三个 OK
```

## 按需

```bash
npm i -g agent-browser               # 复杂交互
pip install browser-use               # AI Agent (或 uv pip install)
pip install zendriver                  # 反爬
pip install tavily-python              # 搜索 (需 TAVILY_API_KEY)
pip install firecrawl                  # 提取 (需 FIRECRAWL_API_KEY)
```

## 健康检查

```bash
bash scripts/sync-cookies.sh health
```

## 故障排查

| 问题 | 解决 |
|------|------|
| Extension MISSING | Chrome 加载扩展: `$(npm root -g)/@jackwener/opencli/extension` |
| Daemon not running | `opencli daemon restart` |
| web read 返回 302/403 | Chrome 里重新登录目标网站 |
| tavily/firecrawl API error | 检查环境变量: `echo $TAVILY_API_KEY` |


path: references/state-management.md
content: # Cookie 与登录态

## 零配置优先

| 工具 | Cookie 来源 | 配置 |
|------|-----------|------|
| opencli | Chrome Extension 直连 | 零配置 |
| agent-browser | --profile / --auto-connect | 指定参数 |
| browser-use | --connect | 需 Chrome 开调试端口 |
| Stagehand / Zendriver | 文件注入 | sync-cookies.sh export |

## opencli 的 Cookie 机制

Extension Bridge 让浏览器自身发请求，自动带所有 Cookie。SSO token 过期时返回 302 或 exit 77，回 Chrome 重新登录即可。

## 文件导出（仅按需）

```bash
bash scripts/sync-cookies.sh export     # 导出到 unified-state.json
bash scripts/sync-cookies.sh status     # 查看
bash scripts/sync-cookies.sh health     # 健康检查
```

存储: `~/.browser-ops/cookie-store/unified-state.json`


path: scripts/sync-cookies.sh
content: #!/usr/bin/env bash
# browser-ops Cookie 同步
# 统一 Cookie 存储 → 所有工具复用
#
# 用法:
#   sync-cookies.sh export              # 从 agent-browser 导出到统一存储
#   sync-cookies.sh health              # 检查所有工具健康状态
#   sync-cookies.sh import-bu           # 从统一存储导入到 browser-use CLI
#   sync-cookies.sh inject-py <url>     # 用 Python 注入 Cookie 到 Playwright 页面
#   sync-cookies.sh login <url>         # 打开 URL 手动登录 → 自动导出
#   sync-cookies.sh status              # 查看当前存储的域名和 Cookie 数量
#   sync-cookies.sh verify              # 端到端验证所有路径

set -uo pipefail

STORE_DIR="$HOME/.browser-ops/cookie-store"
PROFILE_DIR="$HOME/.browser-ops/profiles/shared"
UNIFIED_STATE="$STORE_DIR/unified-state.json"

mkdir -p "$STORE_DIR" "$PROFILE_DIR"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

_count_cookies() {
  python3 -c "import json; print(len(json.load(open('$UNIFIED_STATE')).get('cookies',[])))" 2>/dev/null || echo "0"
}

_list_domains() {
  python3 -c "
import json
cookies = json.load(open('$UNIFIED_STATE')).get('cookies',[])
domains = sorted(set(c.get('domain','') for c in cookies))
print(', '.join(d for d in domains if d))
" 2>/dev/null
}

case "${1:-help}" in

  export)
    echo -e "${GREEN}从 agent-browser 导出 Cookie 到统一存储...${NC}"
    if ! command -v agent-browser &>/dev/null; then
      echo -e "${RED}agent-browser 未安装。${NC}"
      echo "安装: npm i -g agent-browser"
      exit 1
    fi
    agent-browser --profile "$PROFILE_DIR" open "about:blank" 2>/dev/null || true
    sleep 1
    agent-browser state save "$UNIFIED_STATE" 2>/dev/null
    agent-browser close 2>/dev/null

    if [[ -f "$UNIFIED_STATE" && -s "$UNIFIED_STATE" ]]; then
      echo -e "${GREEN}已导出 $(_count_cookies) 条 Cookie 到 $UNIFIED_STATE${NC}"
      echo -e "域名: $(_list_domains)"
    else
      echo -e "${RED}导出失败${NC}"
      exit 1
    fi
    ;;

  health)
    echo -e "${CYAN}=== browser-ops 健康检查 ===${NC}"
    echo ""
    FAIL=0

    # 1. opencli
    if command -v opencli &>/dev/null; then
      DOCTOR=$(opencli doctor 2>&1)
      if echo "$DOCTOR" | grep -q "\[OK\] Connectivity"; then
        echo -e "${GREEN}[OK]${NC} opencli: daemon + extension + connectivity"
      elif echo "$DOCTOR" | grep -q "\[OK\] Daemon"; then
        echo -e "${YELLOW}[WARN]${NC} opencli: daemon running but extension not connected"
        echo "      → Chrome 里装 OpenCLI Browser Bridge 扩展"
        FAIL=1
      else
        echo -e "${RED}[FAIL]${NC} opencli: daemon not running"
        echo "      → 运行: opencli daemon restart"
        FAIL=1
      fi
    else
      echo -e "${RED}[FAIL]${NC} opencli 未安装"
      echo "      → npm i -g @jackwener/opencli"
      FAIL=1
    fi

    # 2. browser-use (按需)
    if command -v browser-use &>/dev/null; then
      echo -e "${GREEN}[OK]${NC} browser-use 已安装"
    else
      echo -e "${YELLOW}[SKIP]${NC} browser-use 未安装 (按需: pip install browser-use)"
    fi

    # 3. zendriver (按需)
    python3 -c "import zendriver" 2>/dev/null \
      && echo -e "${GREEN}[OK]${NC} zendriver 已安装" \
      || echo -e "${YELLOW}[SKIP]${NC} zendriver 未安装 (按需: pip install zendriver)"

    # 4. Cookie store
    echo ""
    if [[ -f "$UNIFIED_STATE" && -s "$UNIFIED_STATE" ]]; then
      AGE_HOURS=$(python3 -c "import os,time; print(f'{(time.time()-os.path.getmtime(os.path.expanduser(\"$UNIFIED_STATE\")))/3600:.0f}')")
      echo -e "${GREEN}[OK]${NC} Cookie 存储: $(_count_cookies) cookies (${AGE_HOURS}h ago)"
      if [[ "$AGE_HOURS" -gt 24 ]]; then
        echo -e "${YELLOW}      → Cookie 超过 24 小时未更新，建议: sync-cookies.sh export${NC}"
      fi
    else
      echo -e "${YELLOW}[SKIP]${NC} Cookie 存储为空 (仅 Stagehand/Zendriver 需要)"
    fi

    echo ""
    if [[ $FAIL -eq 0 ]]; then
      echo -e "${GREEN}=== 全部正常 ===${NC}"
    else
      echo -e "${YELLOW}=== 有问题需要修复 ===${NC}"
    fi
    ;;

  import-bu)
    echo -e "${GREEN}从统一存储导入 Cookie 到 browser-use CLI...${NC}"
    if [[ ! -f "$UNIFIED_STATE" ]]; then
      echo -e "${RED}统一存储不存在: $UNIFIED_STATE${NC}"
      echo "先运行: sync-cookies.sh export 或 sync-cookies.sh login <url>"
      exit 1
    fi

    # Convert unified-state.json (Playwright storageState) to browser-use flat array
    TMP_COOKIES="/tmp/browser-ops-bu-cookies.json"
    python3 -c "
import json, os
data = json.load(open(os.path.expanduser('$UNIFIED_STATE')))
cookies = [{k: v for k, v in c.items() if k not in ('size', 'session')} for c in data['cookies']]
json.dump(cookies, open('$TMP_COOKIES', 'w'), indent=2)
print(f'Converted {len(cookies)} cookies to browser-use format: $TMP_COOKIES')
"
    echo -e "${CYAN}用法: browser-use cookies import $TMP_COOKIES${NC}"
    echo -e "${CYAN}  或: 在 Python 中用 Playwright context.add_cookies() 注入${NC}"
    ;;

  inject-py)
    URL="${2:-}"
    if [[ -z "$URL" ]]; then
      echo -e "${RED}用法: sync-cookies.sh inject-py <url>${NC}"
      exit 1
    fi
    echo -e "${GREEN}通过 Playwright 注入统一 Cookie 并访问 $URL...${NC}"

    if [[ ! -f "$UNIFIED_STATE" ]]; then
      echo -e "${RED}统一存储不存在: $UNIFIED_STATE${NC}"
      exit 1
    fi

    python3 << PYEOF
import json, os, asyncio

async def main():
    from playwright.async_api import async_playwright
    state_file = os.path.expanduser("$UNIFIED_STATE")
    data = json.load(open(state_file))
    cookies = data.get("cookies", [])

    # Filter cookies for target domain
    from urllib.parse import urlparse
    target = urlparse("$URL")
    domain = target.hostname or ""

    matching = [c for c in cookies if domain.endswith(c.get("domain","").lstrip("."))]
    print(f"Injecting {len(matching)} cookies for domain {domain} (of {len(cookies)} total)")

    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        context = await browser.new_context()

        # Convert to Playwright format
        pw_cookies = []
        for c in matching:
            cookie = {
                "name": c["name"],
                "value": c.get("value", ""),
                "domain": c["domain"],
                "path": c.get("path", "/"),
            }
            if c.get("expires", -1) > 0:
                cookie["expires"] = c["expires"]
            if c.get("secure"):
                cookie["secure"] = True
            if c.get("httpOnly"):
                cookie["httpOnly"] = True
            pw_cookies.append(cookie)

        if pw_cookies:
            await context.add_cookies(pw_cookies)

        page = await context.new_page()
        response = await page.goto("$URL", wait_until="domcontentloaded", timeout=15000)
        status = response.status if response else "N/A"
        title = await page.title()
        print(f"Status: {status}")
        print(f"Title: {title}")

        # Check if we landed on login page (indicates cookies didn't work)
        url_after = page.url
        if "login" in url_after.lower() or "sso" in url_after.lower():
            print(f"WARN: Redirected to login: {url_after}")
            print("Cookie injection may not have worked — cookies might be expired")
        else:
            print(f"SUCCESS: Accessed {url_after}")

        await browser.close()

asyncio.run(main())
PYEOF
    ;;

  login)
    URL="${2:-https://example.com}"
    echo -e "${GREEN}打开 $URL 手动登录...${NC}"
    echo -e "${YELLOW}登录完成后，按 Enter 键继续（会自动保存 Cookie）${NC}"

    if command -v agent-browser &>/dev/null; then
      agent-browser close 2>/dev/null; sleep 1
      agent-browser --headed --profile "$PROFILE_DIR" open "$URL" 2>/dev/null || true

      read -r -p "登录完成了吗？按 Enter 继续..."

      agent-browser state save "$UNIFIED_STATE" 2>/dev/null
    else
      echo -e "${YELLOW}agent-browser 未安装，使用 Playwright 打开...${NC}"
      python3 << PYEOF
import asyncio, json, os

async def main():
    from playwright.async_api import async_playwright
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        context = await browser.new_context()
        page = await context.new_page()
        await page.goto("$URL")
        input("登录完成后按 Enter...")

        # Export cookies
        cookies = await context.cookies()
        state = {"cookies": cookies, "origins": []}
        store = os.path.expanduser("$UNIFIED_STATE")
        json.dump(state, open(store, "w"), indent=2)
        print(f"Saved {len(cookies)} cookies")
        await browser.close()

asyncio.run(main())
PYEOF
    fi

    if [[ -f "$UNIFIED_STATE" && -s "$UNIFIED_STATE" ]]; then
      echo -e "${GREEN}已保存 $(_count_cookies) 条 Cookie${NC}"
      echo -e "域名: $(_list_domains)"
      chmod 600 "$UNIFIED_STATE"
    fi
    echo -e "${GREEN}Cookie 已存储到 $UNIFIED_STATE${NC}"
    ;;

  status)
    if [[ ! -f "$UNIFIED_STATE" ]]; then
      echo -e "${YELLOW}统一存储为空${NC}"
      echo "运行: sync-cookies.sh export 或 sync-cookies.sh login <url>"
      exit 0
    fi

    python3 << 'PYEOF'
import json, os
from datetime import datetime

state_file = os.path.expanduser("~/.browser-ops/cookie-store/unified-state.json")
data = json.load(open(state_file))
cookies = data.get("cookies", [])

domains = {}
expired = 0
no_value = 0
import time
now = time.time()

for c in cookies:
    d = c.get("domain", "unknown")
    domains.setdefault(d, []).append(c)
    if c.get("expires", -1) > 0 and c["expires"] < now:
        expired += 1
    if not c.get("value"):
        no_value += 1

mtime = os.path.getmtime(state_file)
age_hours = (now - mtime) / 3600

print(f"统一 Cookie 存储: {len(cookies)} 条")
print(f"覆盖 {len(domains)} 个域名")
print(f"文件: {state_file}")
print(f"大小: {os.path.getsize(state_file):,} bytes")
print(f"更新时间: {datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S')} ({age_hours:.1f} 小时前)")
if expired > 0:
    print(f"\033[1;33m警告: {expired} 条 Cookie 已过期\033[0m")
if no_value > 0:
    print(f"\033[1;33m警告: {no_value} 条 Cookie 无 value（可能只有 metadata）\033[0m")
print()
for domain in sorted(domains.keys()):
    items = domains[domain]
    session_count = sum(1 for c in items if c.get("session", False))
    persistent_count = len(items) - session_count
    has_value = sum(1 for c in items if c.get("value"))
    value_tag = f" ✅ {has_value} with value" if has_value > 0 else " ⚠️  metadata only"
    print(f"  {domain}: {len(items)} cookies ({persistent_count}p/{session_count}s){value_tag}")
PYEOF
    ;;

  verify)
    echo -e "${CYAN}=== browser-ops Cookie 端到端验证 ===${NC}"
    echo ""

    # 1. Check unified store exists
    if [[ -f "$UNIFIED_STATE" && -s "$UNIFIED_STATE" ]]; then
      echo -e "${GREEN}[PASS]${NC} 统一存储存在: $UNIFIED_STATE ($(_count_cookies) cookies)"
    else
      echo -e "${RED}[FAIL]${NC} 统一存储不存在或为空"
      echo "运行: sync-cookies.sh export 或 sync-cookies.sh login <url>"
      exit 1
    fi

    # 2. Check cookies have values
    HAS_VALUES=$(python3 -c "
import json, os
data = json.load(open(os.path.expanduser('$UNIFIED_STATE')))
cookies_with_values = [c for c in data.get('cookies',[]) if c.get('value')]
print(len(cookies_with_values))
")
    TOTAL=$(_count_cookies)
    if [[ "$HAS_VALUES" -gt 0 ]]; then
      echo -e "${GREEN}[PASS]${NC} Cookie 有 value: $HAS_VALUES / $TOTAL"
    else
      echo -e "${RED}[FAIL]${NC} Cookie 无 value (全是 metadata)"
      echo "运行: sync-cookies.sh export 重新导出（需要 agent-browser）"
    fi

    # 3. Check file permissions
    PERMS=$(stat -f "%Lp" "$UNIFIED_STATE" 2>/dev/null || stat -c "%a" "$UNIFIED_STATE" 2>/dev/null)
    if [[ "$PERMS" == "600" ]]; then
      echo -e "${GREEN}[PASS]${NC} 文件权限: $PERMS (安全)"
    else
      echo -e "${YELLOW}[WARN]${NC} 文件权限: $PERMS (建议 chmod 600)"
    fi

    # 4. Check cookie expiry
    EXPIRED=$(python3 -c "
import json, os, time
data = json.load(open(os.path.expanduser('$UNIFIED_STATE')))
now = time.time()
expired = [c for c in data.get('cookies',[]) if c.get('expires',-1) > 0 and c['expires'] < now]
print(len(expired))
")
    if [[ "$EXPIRED" -eq 0 ]]; then
      echo -e "${GREEN}[PASS]${NC} 无过期 Cookie"
    else
      echo -e "${YELLOW}[WARN]${NC} $EXPIRED 条 Cookie 已过期"
    fi

    # 5. Format conversion test
    echo ""
    echo -e "${CYAN}--- 格式转换验证 ---${NC}"
    TMP="/tmp/browser-ops-verify-cookies.json"
    python3 -c "
import json, os
data = json.load(open(os.path.expanduser('$UNIFIED_STATE')))
cookies = [{k: v for k, v in c.items() if k not in ('size', 'session')} for c in data['cookies']]
json.dump(cookies, open('$TMP', 'w'))
print(f'Playwright → flat array: {len(cookies)} cookies')
import json as j
loaded = j.load(open('$TMP'))
assert isinstance(loaded, list), 'Should be array'
assert all('name' in c and 'domain' in c for c in loaded), 'Each cookie needs name+domain'
print('Format validation: OK')
" && echo -e "${GREEN}[PASS]${NC} unified-state.json → browser-use 格式转换正常" \
  || echo -e "${RED}[FAIL]${NC} 格式转换失败"
    rm -f "$TMP"

    # 6. Playwright injection test (headless)
    echo ""
    echo -e "${CYAN}--- Playwright 注入验证 ---${NC}"
    python3 << 'PYEOF' && echo -e "${GREEN}[PASS]${NC} Playwright Cookie 注入成功" \
  || echo -e "${RED}[FAIL]${NC} Playwright Cookie 注入失败（playwright 可能未安装）"
import json, os, asyncio, sys

async def test():
    try:
        from playwright.async_api import async_playwright
    except ImportError:
        print("playwright not installed — skipping")
        sys.exit(0)

    data = json.load(open(os.path.expanduser("~/.browser-ops/cookie-store/unified-state.json")))
    cookies = data.get("cookies", [])
    if not cookies:
        print("No cookies to inject")
        sys.exit(1)

    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)
        context = await browser.new_context()

        pw_cookies = []
        for c in cookies:
            cookie = {"name": c["name"], "value": c.get("value",""), "domain": c["domain"], "path": c.get("path","/")}
            if c.get("expires", -1) > 0:
                cookie["expires"] = c["expires"]
            if c.get("secure"):
                cookie["secure"] = True
            if c.get("httpOnly"):
                cookie["httpOnly"] = True
            pw_cookies.append(cookie)

        await context.add_cookies(pw_cookies)
        stored = await context.cookies()
        print(f"Injected {len(pw_cookies)} → retrieved {len(stored)} cookies from context")
        assert len(stored) > 0, "No cookies in context after injection"
        await browser.close()

asyncio.run(test())
PYEOF

    # 7. Tool availability
    echo ""
    echo -e "${CYAN}--- 工具可用性 ---${NC}"
    for tool in agent-browser browser-use; do
      if command -v "$tool" &>/dev/null; then
        echo -e "${GREEN}[PASS]${NC} $tool 已安装"
      else
        echo -e "${YELLOW}[SKIP]${NC} $tool 未安装"
      fi
    done

    python3 -c "import zendriver" 2>/dev/null && echo -e "${GREEN}[PASS]${NC} zendriver 已安装" \
      || echo -e "${YELLOW}[SKIP]${NC} zendriver 未安装"

    echo ""
    echo -e "${CYAN}=== 验证完成 ===${NC}"
    ;;

  *)
    echo "browser-ops Cookie 同步工具"
    echo ""
    echo "用法:"
    echo "  sync-cookies.sh login <url>     打开 URL 登录 → 自动保存 Cookie"
    echo "  sync-cookies.sh export          从 agent-browser 导出到统一存储"
    echo "  sync-cookies.sh health          检查所有工具健康状态"
    echo "  sync-cookies.sh import-bu       转换为 browser-use 格式"
    echo "  sync-cookies.sh inject-py <url> 通过 Playwright 注入 Cookie 并访问"
    echo "  sync-cookies.sh status          查看存储状态"
    echo "  sync-cookies.sh verify          端到端验证所有路径"
    ;;
esac


path: scripts/web-read.sh
content: #!/bin/bash
# web-read.sh — 网页抓取降级链（opencli web read → firecrawl → agent-browser）
# 用法: ./web-read.sh "https://example.com"

set -euo pipefail

URL="${1:?Usage: web-read.sh \"url\"}"

# Layer 1: opencli web read (Cookie 零配置)
opencli_exit=0
result=$(opencli web read --url "$URL" -f md 2>&1) || opencli_exit=$?
if [ -n "$result" ] && [ ${#result} -gt 200 ] && ! echo "$result" | grep -qiE "error|failed|exit code"; then
    echo "$result"
    exit 0
fi
# exit 77 = SSO/Cookie 过期，后续层也没有登录态，直接 bail out
if [ "$opencli_exit" -eq 77 ]; then
    echo "# [web-read] SSO/Cookie expired (exit 77). Please re-login in Chrome, then retry."
    echo "# 后续工具 (firecrawl/agent-browser) 也没有登录态，跳过降级。"
    exit 77
fi
echo "# [web-read] opencli web read failed (exit $opencli_exit), trying firecrawl..." >&2

# Layer 2: Firecrawl (JS 渲染 + 结构化提取)
if command -v firecrawl &>/dev/null && [ -n "${FIRECRAWL_API_KEY:-}" ]; then
    fc_result=$(firecrawl scrape "$URL" 2>&1) || true
    if [ -n "$fc_result" ] && [ ${#fc_result} -gt 200 ]; then
        echo "$fc_result"
        exit 0
    fi
    echo "# [web-read] firecrawl failed, trying agent-browser..." >&2
else
    echo "# [web-read] firecrawl not available, trying agent-browser..." >&2
fi

# Layer 3: agent-browser snapshot
if command -v agent-browser &>/dev/null; then
    agent-browser --auto-connect open "$URL" 2>/dev/null || true
    sleep 2
    snapshot=$(agent-browser snapshot -i 2>&1) || true
    agent-browser close 2>/dev/null || true
    if [ -n "$snapshot" ] && [ ${#snapshot} -gt 100 ]; then
        echo "$snapshot"
        exit 0
    fi
fi

echo "# [web-read] ALL LAYERS FAILED for url: $URL"
echo "# Tried: opencli web read, firecrawl, agent-browser"
exit 1


path: scripts/web-search.sh
content: #!/bin/bash
# web-search.sh — 搜索降级链（tavily → opencli google → agent-browser）
# 用法: ./web-search.sh "search query" [limit]

set -euo pipefail

QUERY="${1:?Usage: web-search.sh \"query\" [limit]}"
LIMIT="${2:-5}"

# Layer 1: Tavily (如果可用)
if command -v tavily &>/dev/null && [ -n "${TAVILY_API_KEY:-}" ]; then
    result=$(tavily search "$QUERY" --max-results "$LIMIT" 2>&1) || true
    if [ -n "$result" ] && ! echo "$result" | grep -qiE "error|failed"; then
        echo "$result"
        exit 0
    fi
    echo "# [web-search] tavily failed, trying opencli..." >&2
fi

# Layer 2: opencli google search
result=$(opencli google search "$QUERY" --limit "$LIMIT" -f md 2>&1) || true
if [ -n "$result" ] && ! echo "$result" | grep -qiE "error|no.*result|captcha|exit code"; then
    echo "$result"
    exit 0
fi
echo "# [web-search] opencli google failed, trying agent-browser..." >&2

# Layer 3: agent-browser open Google
if command -v agent-browser &>/dev/null; then
    agent-browser --auto-connect open "https://www.google.com/search?q=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$QUERY'))")" 2>/dev/null || true
    sleep 2
    snapshot=$(agent-browser snapshot -i 2>&1) || true
    agent-browser close 2>/dev/null || true
    if [ -n "$snapshot" ] && [ ${#snapshot} -gt 100 ]; then
        echo "$snapshot"
        exit 0
    fi
fi

echo "# [web-search] ALL LAYERS FAILED for query: $QUERY"
exit 1


path: scripts/web-trending.sh
content: #!/bin/bash
# web-trending.sh — 热榜降级链（opencli <platform> → agent-browser）
# 用法: ./web-trending.sh <platform> [limit]
# 支持平台: twitter, zhihu, hackernews, bilibili, weibo, lobsters, producthunt, reddit

set -euo pipefail

PLATFORM="${1:?Usage: web-trending.sh <platform> [limit]}"
LIMIT="${2:-10}"

# 平台 → 命令映射
case "$PLATFORM" in
    twitter)    CMD="opencli twitter trending --limit $LIMIT -f md" ; URL="https://x.com/explore/tabs/trending" ;;
    zhihu)      CMD="opencli zhihu hot --limit $LIMIT -f md"       ; URL="https://www.zhihu.com/hot" ;;
    hackernews) CMD="opencli hackernews top --limit $LIMIT -f md"  ; URL="https://news.ycombinator.com" ;;
    bilibili)   CMD="opencli bilibili hot --limit $LIMIT -f md"    ; URL="https://www.bilibili.com/v/popular/rank/all" ;;
    weibo)      CMD="opencli weibo hot --limit $LIMIT -f md"       ; URL="https://s.weibo.com/top/summary" ;;
    lobsters)   CMD="opencli lobsters hot --limit $LIMIT -f md"    ; URL="https://lobste.rs" ;;
    producthunt) CMD="opencli producthunt today --limit $LIMIT -f md" ; URL="https://www.producthunt.com" ;;
    reddit)     CMD="opencli reddit hot --limit $LIMIT -f md"      ; URL="https://www.reddit.com/r/popular" ;;
    *)          echo "# Unknown platform: $PLATFORM"; echo "# Supported: twitter zhihu hackernews bilibili weibo lobsters producthunt reddit"; exit 1 ;;
esac

# Layer 1: opencli
result=$(eval "$CMD" 2>&1) || true
if [ -n "$result" ] && [ ${#result} -gt 50 ] && ! echo "$result" | grep -qiE "error|unexpected|exit code|no.*result"; then
    echo "$result"
    exit 0
fi
echo "# [web-trending] opencli $PLATFORM failed, trying agent-browser..." >&2

# Layer 2: agent-browser
agent-browser --auto-connect open "$URL" 2>/dev/null || true
sleep 3
snapshot=$(agent-browser snapshot -i 2>&1) || true
if [ -n "$snapshot" ] && [ ${#snapshot} -gt 100 ]; then
    echo "$snapshot"
    exit 0
fi

echo "# [web-trending] ALL LAYERS FAILED for platform: $PLATFORM"
echo "# Tried: opencli $PLATFORM, agent-browser $URL"
exit 1


path: task_suite.yaml
content: # browser-ops evaluation task suite
# Tests routing decisions, tool selection, and trigger accuracy

name: browser-ops
version: "1.0.0"
description: "Web access router skill evaluation"

tasks:
  # --- Positive triggers (should activate skill) ---
  - id: fetch-article
    prompt: "帮我抓取 https://paulgraham.com/worked.html 的正文内容"
    should_trigger: true
    expected_behavior: "选择 WebFetch/Jina 轻量抓取，不启动浏览器"
    pass_criteria:
      - "uses WebFetch or Jina or curl"
      - "does not launch browser-use or Stagehand"

  - id: twitter-search
    prompt: "搜索 Twitter 上关于 AI agent 的最新讨论"
    should_trigger: true
    expected_behavior: "使用 opencli twitter search"
    pass_criteria:
      - "uses opencli twitter search"

  - id: stock-price
    prompt: "查一下招商银行今天的股价"
    should_trigger: true
    expected_behavior: "使用 opencli 或金融 API 查询股价"
    pass_criteria:
      - "uses opencli or financial API"
      - "returns price data"

  - id: batch-stocks
    prompt: "帮我同时查 100 只 A 股的实时行情"
    should_trigger: true
    expected_behavior: "识别批量场景，使用批量 API"
    pass_criteria:
      - "identifies batch scenario"
      - "does not use opencli 100 times serially"

  - id: cloudflare-blocked
    prompt: "这个网站被 Cloudflare 拦截了打不开"
    should_trigger: true
    expected_behavior: "使用 Zendriver 或 Camoufox 反爬"
    pass_criteria:
      - "identifies anti-bot scenario"
      - "uses Zendriver or Camoufox"

  - id: xiaohongshu-search
    prompt: "帮我在小红书上搜索旅行攻略"
    should_trigger: true
    expected_behavior: "使用 opencli xiaohongshu search"
    pass_criteria:
      - "uses opencli xiaohongshu"

  - id: internal-screenshot
    prompt: "打开内部网站并截图"
    should_trigger: true
    expected_behavior: "使用 opencli operate screenshot (首选) 或 browser-use screenshot"
    pass_criteria:
      - "uses opencli operate screenshot or browser-use screenshot"

  - id: zhihu-hot
    prompt: "获取知乎热榜前 20 条"
    should_trigger: true
    expected_behavior: "使用 opencli zhihu hot"
    pass_criteria:
      - "uses opencli zhihu hot"

  - id: hackernews-trending
    prompt: "帮我看看 HackerNews 今天的热门"
    should_trigger: true
    expected_behavior: "使用 opencli hackernews top"
    pass_criteria:
      - "uses opencli hackernews"

  - id: download-images
    prompt: "下载这个网页的所有图片"
    should_trigger: true
    expected_behavior: "使用 browser-use 或 WebFetch 提取图片"
    pass_criteria:
      - "identifies web content download scenario"

  - id: url-broken
    prompt: "这个URL打不开 https://broken-link.invalid/page"
    should_trigger: true
    expected_behavior: "尝试替代方案（opencli/browser-use）"
    pass_criteria:
      - "tries fallback chain"

  - id: slow-page
    prompt: "帮我抓取这个网页但是它加载很慢经常超时"
    should_trigger: true
    expected_behavior: "识别超时场景，调整超时配置"
    pass_criteria:
      - "identifies timeout scenario"

  - id: captcha-site
    prompt: "这个网站需要验证码才能访问"
    should_trigger: true
    expected_behavior: "识别反爬场景，升级到 Zendriver"
    pass_criteria:
      - "identifies anti-bot scenario"

  - id: batch-download
    prompt: "帮我批量下载这50个网页的内容"
    should_trigger: true
    expected_behavior: "选择并发方案"
    pass_criteria:
      - "identifies batch scenario"

  - id: browser-click
    prompt: "用浏览器打开 https://example.com 然后点击登录按钮"
    should_trigger: true
    expected_behavior: "使用 opencli operate (首选) 或 browser-use CLI"
    pass_criteria:
      - "uses opencli operate open + click or browser-use"

  - id: form-fill
    prompt: "帮我在这个网页上填写表单"
    should_trigger: true
    expected_behavior: "使用 opencli operate type/select"
    pass_criteria:
      - "uses opencli operate for form filling"

  - id: opencli-unavailable
    prompt: "opencli 没装，帮我读一下这个内网页面"
    should_trigger: true
    expected_behavior: "降级到 browser-use --connect 或告知需要安装 opencli"
    pass_criteria:
      - "falls back to browser-use or suggests installing opencli"

  # --- Negative triggers (should NOT activate skill) ---
  - id: python-coding
    prompt: "帮我写一段 Python 代码来排序数组"
    should_trigger: false
    expected_behavior: "不触发 browser-ops"

  - id: cpp-error
    prompt: "这个 C++ 编译报错怎么修"
    should_trigger: false
    expected_behavior: "不触发 browser-ops"

  - id: refactor
    prompt: "帮我重构这个函数"
    should_trigger: false
    expected_behavior: "不触发 browser-ops"

  - id: explain-concept
    prompt: "解释一下什么是递归"
    should_trigger: false
    expected_behavior: "不触发 browser-ops"


path: tests/run_tests.sh
content: #!/usr/bin/env bash
# browser-ops 测试主入口
# 用法:
#   bash tests/run_tests.sh           # 运行全部测试
#   bash tests/run_tests.sh structure # 只跑结构校验
#   bash tests/run_tests.sh l2        # 只跑 L2 (Jina)
#   bash tests/run_tests.sh l3        # 只跑 L3 (agent-browser)
#   bash tests/run_tests.sh l4        # 只跑 L4 (Stagehand)
#   bash tests/run_tests.sh l6        # 只跑 L6 (反爬)
#   bash tests/run_tests.sh session   # 只跑 Session 持久化

set -uo pipefail
# 不用 set -e: 测试中的失败命令由测试逻辑自行处理

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"

# macOS 没有 timeout 命令，用 perl 替代
if ! command -v timeout &>/dev/null; then
  timeout() {
    local secs="$1"; shift
    perl -e "alarm $secs; exec @ARGV" -- "$@"
  }
  export -f timeout
fi

# 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

PASSED=0
FAILED=0
SKIPPED=0
RESULTS=()

pass() {
  PASSED=$((PASSED + 1))
  RESULTS+=("${GREEN}✅ PASS${NC}: $1")
  echo -e "${GREEN}✅ PASS${NC}: $1"
}

fail() {
  FAILED=$((FAILED + 1))
  RESULTS+=("${RED}❌ FAIL${NC}: $1 — $2")
  echo -e "${RED}❌ FAIL${NC}: $1 — $2"
}

skip() {
  SKIPPED=$((SKIPPED + 1))
  RESULTS+=("${YELLOW}⏭️  SKIP${NC}: $1 — $2")
  echo -e "${YELLOW}⏭️  SKIP${NC}: $1 — $2"
}

section() {
  echo ""
  echo -e "${BLUE}━━━ $1 ━━━${NC}"
}

# 判断要跑哪些测试
TARGET="${1:-all}"

run_test_file() {
  local test_file="$1"
  if [[ -f "$SCRIPT_DIR/$test_file" ]]; then
    source "$SCRIPT_DIR/$test_file"
  else
    echo -e "${RED}测试文件不存在: $test_file${NC}"
  fi
}

echo -e "${BLUE}╔═══════════════════════════════════════╗${NC}"
echo -e "${BLUE}║   browser-ops 测试套件 v3.2.0        ║${NC}"
echo -e "${BLUE}╚═══════════════════════════════════════╝${NC}"
echo "测试目标: $TARGET"
echo "仓库路径: $REPO_DIR"
echo ""

case "$TARGET" in
  all)
    run_test_file "test_structure.sh"
    run_test_file "test_opencli.sh"
    run_test_file "# deleted (jina removed)"
    run_test_file "test_agent_browser.sh"
    run_test_file "test_stagehand.sh"
    run_test_file "test_anti_detection.sh"
    run_test_file "test_session.sh"
    ;;
  structure) run_test_file "test_structure.sh" ;;
  opencli)   run_test_file "test_opencli.sh" ;;
  agent-browser) run_test_file "test_agent_browser.sh" ;;
  stagehand)    run_test_file "test_stagehand.sh" ;;
  anti-detect)  run_test_file "test_anti_detection.sh" ;;
  session)   run_test_file "test_session.sh" ;;
  *)
    echo "未知目标: $TARGET"
    echo "可选: all, structure, l2, l3, l4, l6, session"
    exit 1
    ;;
esac

# 汇总
section "测试结果汇总"
echo ""
for r in "${RESULTS[@]}"; do
  echo -e "  $r"
done
echo ""
echo -e "通过: ${GREEN}$PASSED${NC} | 失败: ${RED}$FAILED${NC} | 跳过: ${YELLOW}$SKIPPED${NC} | 合计: $((PASSED + FAILED + SKIPPED))"

if [[ $FAILED -gt 0 ]]; then
  echo -e "\n${RED}有测试失败！${NC}"
  exit 1
else
  echo -e "\n${GREEN}全部通过！${NC}"
  exit 0
fi


path: tests/test_agent_browser.sh
content: #!/usr/bin/env bash
# L3: agent-browser 工具可用性测试

section "L3: agent-browser"

# 0. 检测是否安装
if ! command -v agent-browser &>/dev/null; then
  skip "agent-browser 未安装" "运行: npm install -g agent-browser && agent-browser install"
  skip "agent-browser 导航" "依赖 agent-browser"
  skip "agent-browser 快照" "依赖 agent-browser"
  skip "agent-browser 截图" "依赖 agent-browser"
  skip "agent-browser 版本" "依赖 agent-browser"
  return 0 2>/dev/null || true
fi

pass "agent-browser 已安装: $(agent-browser --version 2>/dev/null || echo 'unknown')"

# 1. 导航测试
NAV_OUTPUT=$(agent-browser open https://example.com 2>&1) || true
if echo "$NAV_OUTPUT" | grep -qiE "example|navigat|loaded|success"; then
  pass "agent-browser 导航: example.com"
else
  # 有些版本不输出成功消息，尝试 snapshot 验证
  SNAP=$(agent-browser snapshot 2>&1) || true
  if [[ -n "$SNAP" && ${#SNAP} -gt 50 ]]; then
    pass "agent-browser 导航: example.com(通过 snapshot 验证)"
  else
    fail "agent-browser 导航" "无法打开 example.com"
    agent-browser close 2>/dev/null || true
    return 0 2>/dev/null || true
  fi
fi

# 2. 快照测试(accessibility tree)
SNAPSHOT=$(agent-browser snapshot 2>&1) || true
if [[ -n "$SNAPSHOT" && ${#SNAPSHOT} -gt 50 ]]; then
  pass "agent-browser snapshot: ${#SNAPSHOT} 字符"
else
  fail "agent-browser snapshot" "快照为空或过短"
fi

# 3. 交互元素快照
SNAPSHOT_I=$(agent-browser snapshot -i 2>&1) || true
if [[ -n "$SNAPSHOT_I" ]]; then
  pass "agent-browser snapshot -i: 交互元素列表"
else
  skip "agent-browser snapshot -i" "无交互元素(example.com 页面简单)"
fi

# 4. 截图测试
SCREENSHOT_FILE="/tmp/browser-ops-test-screenshot.png"
rm -f "$SCREENSHOT_FILE"
agent-browser screenshot --output "$SCREENSHOT_FILE" 2>/dev/null || \
  agent-browser screenshot > "$SCREENSHOT_FILE" 2>/dev/null || true
if [[ -f "$SCREENSHOT_FILE" && -s "$SCREENSHOT_FILE" ]]; then
  FSIZE=$(wc -c < "$SCREENSHOT_FILE")
  pass "agent-browser screenshot: ${FSIZE} bytes"
  rm -f "$SCREENSHOT_FILE"
else
  skip "agent-browser screenshot" "截图文件未生成(可能需要 --output 参数)"
fi

# 5. JS 执行
TITLE=$(agent-browser eval "document.title" 2>&1) || true
if [[ -n "$TITLE" ]]; then
  pass "agent-browser eval: 页面标题 = $TITLE"
else
  skip "agent-browser eval" "JS 执行无输出"
fi

# 6. 获取当前 URL
CURRENT_URL=$(agent-browser get url 2>&1) || true
if echo "$CURRENT_URL" | grep -qi "example.com"; then
  pass "agent-browser get url: $CURRENT_URL"
else
  skip "agent-browser get url" "输出: $CURRENT_URL"
fi

# 清理
agent-browser close 2>/dev/null || true


path: tests/test_anti_detection.sh
content: #!/usr/bin/env bash
# L6: 反爬工具可用性测试

section "L6: 反爬工具"

# 自动选择可用的 Python (zendriver 需要 3.12+, 但 3.14 有兼容性问题)
# 优先检测 zendriver (Nodriver 继任者)，回退到 nodriver
ZENDRIVER_PY=""
ZENDRIVER_LEGACY=false
for py in python3.13 python3.12 python3; do
  if command -v "$py" &>/dev/null; then
    if "$py" -c "import zendriver" 2>/dev/null; then
      ZENDRIVER_PY="$py"
      break
    elif "$py" -c "import nodriver" 2>/dev/null; then
      ZENDRIVER_PY="$py"
      ZENDRIVER_LEGACY=true
      break
    fi
  fi
done

CAMO_PY=""
for py in python3.13 python3.12 python3; do
  if command -v "$py" &>/dev/null && "$py" -c "import camoufox" 2>/dev/null; then
    CAMO_PY="$py"
    break
  fi
done

# === Zendriver (Nodriver 继任者) ===

if [[ -z "$ZENDRIVER_PY" ]]; then
  skip "Zendriver 未安装" "运行: pip install zendriver (需要 Python 3.12/3.13)"
else
  PY_VER=$($ZENDRIVER_PY -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
  if [[ "$ZENDRIVER_LEGACY" == "true" ]]; then
    pass "Nodriver 已安装 (via $ZENDRIVER_PY, Python $PY_VER) — 建议迁移到 zendriver"
    ZD_IMPORT="import nodriver as uc"
    ZD_START="uc.start"
    ZD_LABEL="Nodriver(legacy)"
  else
    pass "Zendriver 已安装 (via $ZENDRIVER_PY, Python $PY_VER)"
    ZD_IMPORT="import zendriver as zd"
    ZD_START="zd.start"
    ZD_LABEL="Zendriver"
  fi

  # 检测 Chrome 路径 (agent-browser 安装的 Chrome for Testing 或系统 Chrome)
  CHROME_PATH=""
  for p in \
    "$HOME/.agent-browser/browsers"/chrome-*/Google\ Chrome\ for\ Testing.app/Contents/MacOS/Google\ Chrome\ for\ Testing \
    "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
    "$(command -v google-chrome 2>/dev/null)" \
    "$(command -v chromium 2>/dev/null)"; do
    if [[ -x "$p" ]]; then
      CHROME_PATH="$p"
      break
    fi
  done

  if [[ -z "$CHROME_PATH" ]]; then
    skip "$ZD_LABEL 端到端" "找不到 Chrome 二进制"
    return 0 2>/dev/null || true
  fi

  # Zendriver/Nodriver 端到端测试
  ZD_RESULT=$(timeout 30 $ZENDRIVER_PY -c "
import asyncio, sys
${ZD_IMPORT}

async def test():
    try:
        browser = await ${ZD_START}(headless=True, browser_executable_path=sys.argv[1])
        page = await browser.get('https://example.com')
        await page.sleep(2)
        content = await page.get_content()
        if len(content) > 100:
            print(f'SUCCESS:{len(content)}')
        else:
            print('FAIL:content_too_short')
        browser.stop()
    except Exception as e:
        print(f'ERROR:{e}')

asyncio.run(test())
" "$CHROME_PATH" 2>/dev/null || echo "TIMEOUT")

  if echo "$ZD_RESULT" | grep -q "^SUCCESS:"; then
    CHARS=$(echo "$ZD_RESULT" | grep "^SUCCESS:" | sed 's/^SUCCESS://')
    pass "$ZD_LABEL 端到端: example.com (${CHARS} chars)"
  elif [[ "$ZD_RESULT" == "TIMEOUT" ]]; then
    fail "$ZD_LABEL 端到端" "超时 (30s)"
  else
    ERROR_MSG=$(echo "$ZD_RESULT" | grep "^ERROR:" | sed 's/^ERROR://' | head -1)
    fail "$ZD_LABEL 端到端" "${ERROR_MSG:-未知错误}"
  fi
fi

# === Camoufox ===

if [[ -z "$CAMO_PY" ]]; then
  skip "Camoufox 未安装" "运行: pip install camoufox && python3 -m camoufox fetch"
else
  pass "Camoufox 已安装 (via $CAMO_PY)"

  # 检查 Firefox 二进制是否已下载
  CAMO_CHECK=$($CAMO_PY -c "
from camoufox.pkgman import get_path
try:
    p = get_path()
    print(f'PATH:{p}')
except:
    print('MISSING')
" 2>/dev/null || echo "MISSING")

  if echo "$CAMO_CHECK" | grep -q "^PATH:"; then
    pass "Camoufox Firefox 二进制已就绪"
  else
    skip "Camoufox Firefox 二进制" "运行: $CAMO_PY -m camoufox fetch"
    return 0 2>/dev/null || true
  fi

  # Camoufox 端到端测试
  CAMO_RESULT=$(timeout 30 $CAMO_PY -c "
from camoufox.sync_api import Camoufox
try:
    with Camoufox(headless=True) as browser:
        page = browser.new_page()
        page.goto('https://example.com', timeout=15000)
        content = page.content()
        if len(content) > 100:
            print(f'SUCCESS:{len(content)}')
        else:
            print('FAIL:content_too_short')
except Exception as e:
    print(f'ERROR:{e}')
" 2>/dev/null || echo "TIMEOUT")

  if echo "$CAMO_RESULT" | grep -q "^SUCCESS:"; then
    CHARS=$(echo "$CAMO_RESULT" | grep "^SUCCESS:" | sed 's/^SUCCESS://')
    pass "Camoufox 端到端: example.com (${CHARS} chars)"
  elif [[ "$CAMO_RESULT" == "TIMEOUT" ]]; then
    fail "Camoufox 端到端" "超时 (30s)"
  else
    ERROR_MSG=$(echo "$CAMO_RESULT" | grep "^ERROR:" | sed 's/^ERROR://' | head -1)
    fail "Camoufox 端到端" "${ERROR_MSG:-未知错误}"
  fi
fi


path: tests/test_antibot.py
content: #!/usr/bin/env python3.13
"""
Parallel anti-bot bypass test: Nodriver vs Camoufox
Tests against real anti-bot sites with 8s wait per page.
"""

import asyncio
import time
import os
import traceback

CHROME_PATH = os.path.expanduser(
    "~/.agent-browser/browsers/chrome-147.0.7727.50/"
    "Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing"
)

SITES = [
    ("nowsecure.nl", "https://nowsecure.nl"),
    ("cloudflare.com", "https://www.cloudflare.com"),
    ("fingerprint.com", "https://fingerprint.com/demo"),
]

WAIT_SECONDS = 8


# ── Nodriver tests ──────────────────────────────────────────────

async def _run_nodriver(name: str, url: str) -> dict:
    """Test a single site with nodriver (undetected-chromedriver successor)."""
    import nodriver as uc

    result = {"tool": "Nodriver", "site": name, "title": "", "length": 0,
              "status": "FAIL", "error": "", "time_s": 0}
    t0 = time.monotonic()
    browser = None
    try:
        browser = await uc.start(
            headless=True,
            browser_executable_path=CHROME_PATH,
            browser_args=["--no-sandbox", "--disable-gpu"],
        )
        page = await browser.get(url)
        await asyncio.sleep(WAIT_SECONDS)

        title = await page.evaluate("document.title")
        length = await page.evaluate("document.body.innerText.length")

        result["title"] = str(title)[:80]
        result["length"] = int(length)
        result["status"] = "PASS" if int(length) > 200 else "PARTIAL"
    except Exception as e:
        result["error"] = f"{type(e).__name__}: {str(e)[:120]}"
        result["status"] = "ERROR"
    finally:
        result["time_s"] = round(time.monotonic() - t0, 1)
        if browser:
            try:
                browser.stop()
            except Exception:
                pass
    return result


async def run_nodriver_tests() -> list[dict]:
    """Run nodriver tests sequentially (shares browser context)."""
    results = []
    for name, url in SITES:
        r = await _run_nodriver(name, url)
        results.append(r)
    return results


# ── Camoufox tests ──────────────────────────────────────────────

def _run_camoufox_single(name: str, url: str) -> dict:
    """Test a single site with Camoufox (anti-fingerprint Firefox)."""
    from camoufox.sync_api import Camoufox

    result = {"tool": "Camoufox", "site": name, "title": "", "length": 0,
              "status": "FAIL", "error": "", "time_s": 0}
    t0 = time.monotonic()
    try:
        with Camoufox(headless=True) as browser:
            page = browser.new_page()
            page.goto(url, timeout=45000, wait_until="domcontentloaded")
            page.wait_for_timeout(WAIT_SECONDS * 1000)

            title = page.title()
            length = page.evaluate("document.body.innerText.length")

            result["title"] = str(title)[:80]
            result["length"] = int(length)
            result["status"] = "PASS" if int(length) > 200 else "PARTIAL"
    except Exception as e:
        result["error"] = f"{type(e).__name__}: {str(e)[:120]}"
        result["status"] = "ERROR"
    finally:
        result["time_s"] = round(time.monotonic() - t0, 1)
    return result


def run_camoufox_tests() -> list[dict]:
    """Run camoufox tests sequentially."""
    results = []
    for name, url in SITES:
        r = _run_camoufox_single(name, url)
        results.append(r)
    return results


# ── Main: run both in parallel ──────────────────────────────────

async def main():
    print("=" * 72)
    print("Anti-Bot Bypass Test: Nodriver vs Camoufox")
    print("=" * 72)
    print(f"Wait per page: {WAIT_SECONDS}s\n")

    # Run Camoufox in a thread (sync API) while Nodriver runs async
    loop = asyncio.get_event_loop()

    nodriver_task = asyncio.create_task(run_nodriver_tests())
    camoufox_task = loop.run_in_executor(None, run_camoufox_tests)

    nodriver_results, camoufox_results = await asyncio.gather(
        nodriver_task, camoufox_task
    )

    all_results = nodriver_results + camoufox_results

    # Print results table
    print("\n" + "=" * 72)
    print(f"{'Tool':<12} {'Site':<20} {'Status':<8} {'Title':<35} {'Len':>6} {'Time':>6}")
    print("-" * 72)
    for r in all_results:
        title_short = r["title"][:33] + ".." if len(r["title"]) > 35 else r["title"]
        print(f"{r['tool']:<12} {r['site']:<20} {r['status']:<8} {title_short:<35} {r['length']:>6} {r['time_s']:>5}s")
        if r["error"]:
            print(f"{'':>12} ERROR: {r['error']}")
    print("=" * 72)

    # Summary
    print("\n── Analysis ──")
    for name, _ in SITES:
        nd = next((r for r in nodriver_results if r["site"] == name), None)
        cf = next((r for r in camoufox_results if r["site"] == name), None)
        print(f"\n{name}:")
        if nd:
            bypass = "Bypassed" if nd["status"] == "PASS" else "Blocked/Error"
            print(f"  Nodriver:  {bypass} (len={nd['length']}, title=\"{nd['title'][:50]}\")")
        if cf:
            bypass = "Bypassed" if cf["status"] == "PASS" else "Blocked/Error"
            print(f"  Camoufox:  {bypass} (len={cf['length']}, title=\"{cf['title'][:50]}\")")


if __name__ == "__main__":
    asyncio.run(main())


path: tests/test_opencli.sh
content: #!/usr/bin/env bash
# opencli 桥接层可用性测试

section "opencli 桥接层"

# 0. 检测是否安装
if ! command -v opencli &>/dev/null; then
  skip "opencli 未安装" "运行: npm install -g @jackwener/opencli + 安装 Chrome 扩展"
  skip "opencli doctor" "依赖 opencli"
  skip "opencli list" "依赖 opencli"
  skip "opencli 平台测试" "依赖 opencli"
  return 0 2>/dev/null || true
fi

pass "opencli 已安装"

# 1. doctor 检查 (Bridge 连通性)
DOCTOR_OUTPUT=$(opencli doctor 2>&1) || true
if echo "$DOCTOR_OUTPUT" | grep -qi "connected\|ok\|ready\|healthy"; then
  pass "opencli doctor: Bridge 连通"
else
  skip "opencli Bridge 未连通" "需要 Chrome 运行 + 扩展加载 (chrome://extensions)"
  skip "opencli 平台测试" "Bridge 未连通"
  return 0 2>/dev/null || true
fi

# 2. 适配器列表
LIST_OUTPUT=$(opencli list 2>&1) || true
if [[ -n "$LIST_OUTPUT" && ${#LIST_OUTPUT} -gt 100 ]]; then
  # 统计适配器数量
  ADAPTER_COUNT=$(echo "$LIST_OUTPUT" | grep -cE "^\s+\w+" || echo "unknown")
  pass "opencli list: ${ADAPTER_COUNT} 个适配器"
else
  skip "opencli list" "输出为空"
fi

# 3. 公开内容测试 (不需要登录)
HACKERNEWS=$(opencli hackernews top --format json 2>&1) || true
if echo "$HACKERNEWS" | python3 -c "import json,sys; json.load(sys.stdin)" 2>/dev/null; then
  pass "opencli hackernews top: 返回合法 JSON"
elif [[ -n "$HACKERNEWS" && ${#HACKERNEWS} -gt 50 ]]; then
  pass "opencli hackernews top: 有内容返回 (${#HACKERNEWS} chars)"
else
  skip "opencli hackernews top" "无输出或错误"
fi

# 4. cascade 认证探测
CASCADE=$(opencli cascade https://example.com 2>&1)
CASCADE_EXIT=$?
if [[ $CASCADE_EXIT -eq 0 ]]; then
  pass "opencli cascade: example.com 可公开访问"
elif [[ $CASCADE_EXIT -eq 77 ]]; then
  pass "opencli cascade: example.com 需要登录 (exit 77)"
else
  skip "opencli cascade" "exit code $CASCADE_EXIT"
fi

# 5. 输出格式测试
for fmt in json table md; do
  FMT_OUTPUT=$(opencli hackernews top --format $fmt 2>&1) || true
  if [[ -n "$FMT_OUTPUT" && ${#FMT_OUTPUT} -gt 20 ]]; then
    pass "opencli --format $fmt: 有输出"
  else
    skip "opencli --format $fmt" "无输出"
  fi
done


path: tests/test_session.sh
content: #!/usr/bin/env bash
# Session/Cookie 持久化测试

section "Session 持久化"

TEST_PROFILE_DIR="/tmp/browser-ops-test-profile"
TEST_STATE_FILE="/tmp/browser-ops-test-state.json"
rm -rf "$TEST_PROFILE_DIR" "$TEST_STATE_FILE"

# === agent-browser Session 测试 ===

if ! command -v agent-browser &>/dev/null; then
  skip "agent-browser Session" "agent-browser 未安装"
  skip "Profile 持久化" "依赖 agent-browser"
  skip "State 导出/导入" "依赖 agent-browser"
  skip "Cookie 操作" "依赖 agent-browser"
else
  # 1. --profile 创建并写入
  agent-browser --profile "$TEST_PROFILE_DIR" open https://example.com 2>/dev/null || true
  sleep 2
  agent-browser close 2>/dev/null || true
  sleep 1

  if [[ -d "$TEST_PROFILE_DIR" ]]; then
    PROFILE_SIZE=$(du -sh "$TEST_PROFILE_DIR" 2>/dev/null | cut -f1)
    pass "Profile 目录创建: $TEST_PROFILE_DIR($PROFILE_SIZE)"
  else
    fail "Profile 目录创建" "$TEST_PROFILE_DIR 未生成"
  fi

  # 2. Cookie 查看
  COOKIES=$(agent-browser cookies 2>&1) || true
  if [[ -n "$COOKIES" ]]; then
    pass "Cookie 查看: 有输出"
  else
    skip "Cookie 查看" "无 Cookie(example.com 可能不设置 Cookie)"
  fi

  # 3. State 导出
  agent-browser state save "$TEST_STATE_FILE" 2>/dev/null || true
  if [[ -f "$TEST_STATE_FILE" && -s "$TEST_STATE_FILE" ]]; then
    STATE_SIZE=$(wc -c < "$TEST_STATE_FILE")
    # 验证是合法 JSON
    if python3 -c "import json; json.load(open('$TEST_STATE_FILE'))" 2>/dev/null; then
      pass "State 导出: $TEST_STATE_FILE(${STATE_SIZE} bytes，合法 JSON)"
    else
      fail "State 导出" "文件不是合法 JSON"
    fi
  else
    skip "State 导出" "state save 未生成文件(可能需要 CDP 模式)"
  fi

  agent-browser close 2>/dev/null || true

  # 4. State 导入(另起 session)
  if [[ -f "$TEST_STATE_FILE" && -s "$TEST_STATE_FILE" ]]; then
    agent-browser state load "$TEST_STATE_FILE" 2>/dev/null || true
    LOAD_RESULT=$(agent-browser open https://example.com 2>&1) || true
    if [[ $? -eq 0 || -n "$LOAD_RESULT" ]]; then
      pass "State 导入: 加载成功"
    else
      skip "State 导入" "加载命令未报错但结果未知"
    fi
    agent-browser close 2>/dev/null || true
  else
    skip "State 导入" "无导出文件可导入"
  fi

  # 5. Profile 复用验证(二次打开应保留状态)
  if [[ -d "$TEST_PROFILE_DIR" ]]; then
    agent-browser --profile "$TEST_PROFILE_DIR" open https://example.com 2>/dev/null || true
    SNAP=$(agent-browser snapshot 2>&1) || true
    if [[ -n "$SNAP" && ${#SNAP} -gt 50 ]]; then
      pass "Profile 复用: 二次打开正常(${#SNAP} 字符)"
    else
      skip "Profile 复用" "快照为空"
    fi
    agent-browser close 2>/dev/null || true
  fi
fi

# === 目录结构测试 ===

# 6. 标准目录结构创建
mkdir -p ~/.browser-ops/profiles/default ~/.browser-ops/states ~/.browser-ops/stagehand-cache
if [[ -d ~/.browser-ops/profiles && -d ~/.browser-ops/states && -d ~/.browser-ops/stagehand-cache ]]; then
  pass "标准目录结构: ~/.browser-ops/ 已就绪"
else
  fail "标准目录结构" "~/.browser-ops/ 子目录创建失败"
fi

# 清理测试文件
rm -rf "$TEST_PROFILE_DIR" "$TEST_STATE_FILE"


path: tests/test_smoke.py
content: """Smoke test for browser-ops skill."""

def test_skill_directory_exists():
    from pathlib import Path
    import os
    skill_dir = Path(os.path.expanduser("~/.claude/skills/browser-ops"))
    assert skill_dir.exists()

def test_skill_md_exists():
    from pathlib import Path
    import os
    skill_md = Path(os.path.expanduser("~/.claude/skills/browser-ops/SKILL.md"))
    assert skill_md.exists()


path: tests/test_stagehand.sh
content: #!/usr/bin/env bash
# L4: Stagehand v3 工具可用性测试

section "L4: Stagehand v3"

# 0. 检测是否安装
if ! node -e "require('@browserbasehq/stagehand')" 2>/dev/null; then
  skip "Stagehand 未安装" "运行: npm install @browserbasehq/stagehand"
  skip "Stagehand init" "依赖 Stagehand"
  skip "Stagehand extract" "依赖 Stagehand"
  return 0 2>/dev/null || true
fi

pass "Stagehand 已安装"

# 检测 LLM API key
HAS_LLM_KEY=false
if [[ -n "${ANTHROPIC_API_KEY:-}" || -n "${OPENAI_API_KEY:-}" || -n "${GEMINI_API_KEY:-}" ]]; then
  HAS_LLM_KEY=true
  pass "LLM API key 已配置"
else
  skip "LLM API key 未配置" "需要 ANTHROPIC_API_KEY/OPENAI_API_KEY/GEMINI_API_KEY 之一"
  skip "Stagehand 功能测试" "无 LLM key 无法执行"
  return 0 2>/dev/null || true
fi

# 1. Stagehand v3 版本检测
SH_VERSION=$(node -e "const p = require('@browserbasehq/stagehand/package.json'); console.log(p.version)" 2>/dev/null || echo "unknown")
pass "Stagehand 版本: v$SH_VERSION"

# 2. v3 API 检测（stagehand.act 在实例上，不在 page 上）
V3_CHECK=$(node -e "
const { Stagehand } = require('@browserbasehq/stagehand');
const s = new Stagehand({ env: 'LOCAL', model: 'anthropic/claude-sonnet-4-5' });
console.log('act:', typeof s.act);
console.log('extract:', typeof s.extract);
console.log('observe:', typeof s.observe);
" 2>/dev/null || echo "error")

if echo "$V3_CHECK" | grep -q "act: function"; then
  pass "Stagehand v3 API: act/extract/observe 在实例上"
else
  fail "Stagehand v3 API" "act/extract/observe 不可用，可能是 v2 版本"
fi

# 3. Stagehand init + extract 端到端测试（需要 headless Chrome）
STAGEHAND_TEST_FILE="/tmp/browser-ops-stagehand-test.mjs"
cat > "$STAGEHAND_TEST_FILE" << 'STAGEHAND_EOF'
import { Stagehand } from "@browserbasehq/stagehand";

const model = process.env.ANTHROPIC_API_KEY
  ? "anthropic/claude-sonnet-4-5"
  : process.env.OPENAI_API_KEY
  ? "openai/gpt-4o"
  : "google/gemini-2.0-flash";

try {
  const stagehand = new Stagehand({
    env: "LOCAL",
    model: model,
    localBrowserLaunchOptions: { headless: true },
  });
  await stagehand.init();
  const page = stagehand.context.pages()[0];
  await page.goto("https://example.com");
  const title = await page.title();
  console.log("TITLE:" + title);
  await stagehand.close();
  console.log("SUCCESS");
} catch (e) {
  console.log("ERROR:" + e.message);
}
STAGEHAND_EOF

STAGEHAND_RESULT=$(timeout 30 node "$STAGEHAND_TEST_FILE" 2>/dev/null || echo "TIMEOUT")
rm -f "$STAGEHAND_TEST_FILE"

if echo "$STAGEHAND_RESULT" | grep -q "SUCCESS"; then
  STAGEHAND_TITLE=$(echo "$STAGEHAND_RESULT" | grep "^TITLE:" | sed 's/^TITLE://')
  pass "Stagehand 端到端: init → navigate → title='$STAGEHAND_TITLE' → close"
else
  ERROR_MSG=$(echo "$STAGEHAND_RESULT" | grep "^ERROR:" | sed 's/^ERROR://' | head -1)
  if [[ "$STAGEHAND_RESULT" == "TIMEOUT" ]]; then
    fail "Stagehand 端到端" "超时（30s），可能是 Chrome 启动问题"
  else
    fail "Stagehand 端到端" "${ERROR_MSG:-未知错误}"
  fi
fi


path: tests/test_structure.sh
content: #!/usr/bin/env bash
# Skill 结构完整性校验

section "结构校验"

# 1. 必要文件存在
for f in SKILL.md README.md LICENSE; do
  if [[ -f "$REPO_DIR/$f" ]]; then
    pass "文件存在: $f"
  else
    fail "文件缺失: $f" "必要文件不存在"
  fi
done

# 2. references/ 中所有文件存在
EXPECTED_REFS=(
  "references/setup.md"
  "references/routing.md"
  "references/state-management.md"
  "references/anti-detection.md"
  "references/opencli-usage.md"
)
for ref in "${EXPECTED_REFS[@]}"; do
  if [[ -f "$REPO_DIR/$ref" ]]; then
    pass "Reference 存在: $ref"
  else
    fail "Reference 缺失: $ref" "SKILL.md 中引用但文件不存在"
  fi
done

# 3. frontmatter 字段检查
SKILL_HEAD=$(head -30 "$REPO_DIR/SKILL.md")
for field in "name:" "description:"; do
  if echo "$SKILL_HEAD" | grep -q "$field"; then
    pass "Frontmatter 字段: $field"
  else
    fail "Frontmatter 缺失: $field" "SKILL.md frontmatter 不完整"
  fi
done

# 4. SKILL.md 中引用的 references 文件都存在
while IFS= read -r line; do
  ref_path=$(echo "$line" | grep -oE 'references/[a-z_-]+\.md' | head -1)
  if [[ -n "$ref_path" && -f "$REPO_DIR/$ref_path" ]]; then
    pass "引用有效: $ref_path"
  elif [[ -n "$ref_path" ]]; then
    fail "死引用: $ref_path" "SKILL.md 引用了不存在的文件"
  fi
done < <(grep "references/" "$REPO_DIR/SKILL.md")

# 5. 无硬编码个人路径
if grep -rn "/Users/" "$REPO_DIR/references/" "$REPO_DIR/SKILL.md" 2>/dev/null | grep -v ".git"; then
  fail "硬编码路径" "发现 /Users/ 硬编码路径"
else
  pass "无硬编码 /Users/ 路径"
fi

# 6. 无模板残留
if grep -q "schedule sends\|manage subscribers\|delivery platforms" "$REPO_DIR/SKILL.md"; then
  fail "模板残留" "Operator Notes 仍有 newsletter 模板内容"
else
  pass "无模板残留内容"
fi

# 7. SKILL.md 行数合理（<200行）
LINE_COUNT=$(wc -l < "$REPO_DIR/SKILL.md")
if [[ $LINE_COUNT -le 200 ]]; then
  pass "SKILL.md 行数合理: ${LINE_COUNT}行"
else
  fail "SKILL.md 过长: ${LINE_COUNT}行" "建议 <200 行，详情放 references"
fi

# 8. references 间交叉引用检查
for ref in "${EXPECTED_REFS[@]}"; do
  if grep -q "See also" "$REPO_DIR/$ref" 2>/dev/null; then
    pass "交叉引用: $ref"
  else
    # setup.md 不强制要求
    if [[ "$ref" == "references/setup.md" ]]; then
      pass "交叉引用: $ref（有 See also）"
    else
      skip "缺少交叉引用: $ref" "建议添加 See also 链接"
    fi
  fi
done


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)