Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions skills/opencli-adapter-author/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Strategy classes:

选择规则:优先 `PUBLIC_API` / `COOKIE_API`。如果 UI/DOM 语义稳定,不要强行升级到 `PAGE_FETCH` / `INTERCEPT`。只有公开/官方接口不可用、UI/DOM 无法表达目标数据或操作时,才承担无契约内部接口的维护成本。

实测:`PAGE_FETCH` / `INTERCEPT` 的 fix 频率约为 `PUBLIC_API` 的 7-8 倍,`UI_SELECTOR` 跟 `COOKIE_API` 同档。详细 ladder 推导、`api_candidates` 证据怎么填、booking #1680 等反例见 [`references/strategy-selection.md`](./references/strategy-selection.md)。

边界:只复用页面自己已经合法获得的数据/能力。不教破解签名、不绕验证码/风控/访问控制;遇到不可复用签名(如必须由页面 runtime 生成且不能安全抽象)就降级到 `UI_SELECTOR` / `DOM_STATE` / `INTERCEPT`。

```
Expand Down Expand Up @@ -218,6 +220,7 @@ DONE
| `references/coverage-matrix.md` | 动手前做"是否在范围内"自测 |
| `references/site-recon.md` | Step 3 定站点类型 |
| `references/api-discovery.md` | Step 4 找 endpoint |
| `references/strategy-selection.md` | Step 6 填 strategy note 之前:契约模型 + 实测 fix 频率 + `api_candidates` 证据用法 + 反例 |
| `references/field-conventions.md` | Step 7 查已知字段代号 |
| `references/field-decode-playbook.md` | Step 7 字段不在词典时 |
| `references/output-design.md` | Step 8 命名 / 类型 / 顺序 |
Expand Down
10 changes: 6 additions & 4 deletions skills/opencli-adapter-author/references/coverage-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ skill 明确承诺能搞定什么、搞不定什么。动手前先看一眼这
| | SSR(HTML with inline data) | 🟡 | `site-recon.md` Pattern B + `api-discovery.md` §state |
| | JSONP / push/script[src] | ✅ | `site-recon.md` Pattern C + `api-discovery.md` §bundle(eastmoney / tonghuashun 已覆盖) |
| | SPA + 独立 BFF domain | 🟡 | `api-discovery.md` §bundle §suffix |
| 鉴权 | 裸 `fetch()` 拿到(PUBLIC) | ✅ | `Strategy.PUBLIC + browser:false`(coingecko dry run 验证) |
| | cookie | ✅ | `Strategy.COOKIE + browser:true` + `credentials:'include'`(xueqiu / bilibili 已覆盖) |
| | Bearer + X-Csrf-Token | 🟡 | `Strategy.COOKIE` + 在 `page.evaluate` 拼 header |
| | 页面能发但独立 fetch 不行 | 🟡 | `Strategy.INTERCEPT` |
| Strategy(详见 `strategy-selection.md`) | 裸 `fetch()` 拿到 | ✅ | `PUBLIC_API`(一方文档化接口,最稳:fixes/adapter-year=1.18) |
| | cookie 透传 | ✅ | `COOKIE_API`(官方 web 接口 + 用户登录态,fixes/adapter-year=2.01) |
| | publish / upload / click / 表单 | ✅ | `UI_SELECTOR`(DOM 的 a11y / semantic 也是契约,fixes/adapter-year=1.92) |
| | hydration state / inline JSON | 🟡 | `DOM_STATE`(fixes/adapter-year=0.91 小样本 N=11,按 UI_SELECTOR 同档) |
| | page-context fetch(CORS / same-origin runtime) | 🟡 | `PAGE_FETCH`(无契约内部 endpoint,fixes/adapter-year=8.41,必须正向论证) |
| | 触发 UI 拦截响应 | 🟡 | `INTERCEPT`(无契约,fixes/adapter-year=8.69,必须正向论证) |
| 字段形态 | 自解释(`title / price / current`) | ✅ | 直接映射 |
| | 已登记代号 | ✅ | `field-conventions.md` 查表 |
| | 未登记代号 | 🟡 | `field-decode-playbook.md` 排序键对比法 |
Expand Down
5 changes: 5 additions & 0 deletions skills/opencli-adapter-author/references/site-recon.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ opencli browser network

**下一步**:`api-discovery.md` §1(network 精读)

**注意 — Pattern A 命中不等于 strategy 选 `PAGE_FETCH`**:
- 先看 `opencli browser analyze` 输出的 `api_candidates[]`:`verdict=likely_data` 的条目才是真候选;`verdict=noise`(analytics / beacon / personalization)不能算 API 信号
- booking #1680 反例:17 个 JSON XHR 看起来像 Pattern A,但全是 analytics side-channel,最终走 `DOM_STATE` / `UI_SELECTOR`
- replay 候选 endpoint 后,按 `strategy-selection.md` 的契约模型选 strategy;`PUBLIC_API` / `COOKIE_API` 都不通才考虑 `PAGE_FETCH`

---

## Pattern B — SSR / inline data
Expand Down
169 changes: 169 additions & 0 deletions skills/opencli-adapter-author/references/strategy-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Strategy Selection

SKILL.md 顶层已给出 strategy gate 的 enum、表格和必填字段。本文件展开**为什么**这套 ladder 是按"契约"而不是"接口高度"组织的,以及具体怎么用 `opencli browser analyze` 的 `api_candidates` 证据填 strategy note。

进入条件:你已经按 `site-recon.md` 跑过 `opencli browser analyze`、按 `api-discovery.md` 抓过候选 endpoint。本文件是写 note 之前的最后一站。

---

## 1. 核心模型:契约 vs 无契约

普遍假设 "API > DOM" — **数据不支持**。

837 个内置 adapter 在 30 天观察窗(2026-04-20 → 2026-05-20)按 6 档 strategy 分类后的实测 fix 频率:

| Strategy | 契约级别 | fixes/adapter-year | 解读 |
|---|---|---|---|
| `PUBLIC_API` | stable | **1.18** | 一方文档化 API,最稳 |
| `COOKIE_API` | stable | 2.01 | 官方 web 接口 + 用户 cookie |
| `UI_SELECTOR` | visible-ui | 1.92 | DOM 的 a11y / semantic 约定也是契约 |
| `DOM_STATE` | visible-ui | 0.91 (N=11, 小样本) | hydration JSON 半契约 |
| `PAGE_FETCH` | **internal-unstable** | **8.41** | 站内未文档化 endpoint,最易漂 |
| `INTERCEPT` | **internal-unstable** | **8.69** | 拦截内部 XHR,签名/字段 silent drift |

含义:

- 选 `PAGE_FETCH` / `INTERCEPT` 的 adapter 平均维护成本是 `PUBLIC_API` 的 **~7-8 倍**
- `UI_SELECTOR` 在 1.92/year,跟 `COOKIE_API` 同档 — 不是"漂得最快"
- `DOM_STATE` 在 0.91/year 但 N=11 小样本,按 `UI_SELECTOR` 的近邻处理

**Selection bias caveat**:`PAGE_FETCH` / `INTERCEPT` 高 fix 率部分来自 selection bias — 用这俩的本身就是难站(Twitter GraphQL、xhs signed URL)。但这不改变 practical implication:能用契约层就用契约层,别把稳定的 UI/DOM 实现盲目迁到无契约 endpoint。

数据观察窗局限:30 天窗口是近似不是长尾;`PAGE_FETCH`/`INTERCEPT`/`DOM_STATE` 样本量小(N=32/9/11),二期数据足时会单独评估 `DOM_STATE`。

---

## 2. Ladder 心智模型

```
契约层(首选,互相平级,按 surface 适配):
PUBLIC_API ─┬─ COOKIE_API ─┬─ UI_SELECTOR ≈ DOM_STATE
(read) (write/click/upload)

无契约层(被迫才用,必须正向论证 8x 维护成本):
PAGE_FETCH ──── INTERCEPT
```

注意:**ladder 不是从上往下降级**。`UI_SELECTOR` 不是 `PUBLIC_API` 失败后的"惩罚选项"。如果数据/操作本来就是 UI 表面的事(publish、click、upload、表单),`UI_SELECTOR` 是首选,不需要为"为什么不是 API"过度辩护。

---

## 3. 怎么把 `api_candidates` 转化为 strategy note 证据

`opencli browser analyze <url>` 的输出里 `api_candidates[]` 字段,每条带:

```json
{
"url": "https://example.com/api/list",
"status": 200,
"contentType": "application/json",
"real_data_score": 0.82,
"verdict": "likely_data",
"reasons": ["json content-type", "non-empty top-level array", "3 business-like keys"],
"sample_paths": ["$.data.items:array(20)", "$.data.items[0].title:string"]
}
```

按 `verdict` 决策:

| Verdict | 含义 | strategy 信号 |
|---|---|---|
| `likely_data` (score ≥ 0.65) | 看起来是业务数据 | 优先 replay 这条做 `PUBLIC_API` / `COOKIE_API` 候选 |
| `maybe_data` (score 0.35-0.65) | 可能业务数据但有 telemetry / 空字段嫌疑 | replay 必须人工核对字段是不是目标数据 |
| `noise` | analytics / beacon / personalization | 不是 API 候选;Pattern A 不能基于这类条目成立 |
| `blocked` (401/403) | auth-gated | 先排 cookie / token / CSRF,**不要**直接退到 `UI_SELECTOR` |

**关键**:`real_data_score` 是证据,不是 strategy。你最终在 strategy note 里仍要写 replay 出来的 status / content-type / sample shape,不是把 score 直接当结论。

### 反例:booking #1680

```
Site: booking.com (酒店搜索)
analyze 输出:17 个 JSON XHR,原 Pattern A
但 api_candidates 全部 verdict=noise(analytics + personalization + experiment)
```

按 1.0.17 前的旧判定,agent 会按 Pattern A 写 `PAGE_FETCH` adapter,replay 拿到 noise data → adapter silent-fail。**新判定**:`real_data_candidates = 0` → Pattern 落到 C → 提示 SSR HTML scrape → 正确的 strategy 是 `DOM_STATE` / `UI_SELECTOR`。

`browser analyze` 的 `recommended_next_step` 也已更新为 "Inspect api_candidates, then replay the best endpoint" — 不再按 XHR count 推 API。

---

## 4. Strategy note 的关键字段填法

### `Contract` 字段

不是直接从 strategy enum 抄,而是反映"这个 source 有多稳":

- `stable`:一方文档化 API、官方 web 接口(PUBLIC_API、COOKIE_API)
- `visible-ui`:用户可见的 DOM、a11y / semantic 标记(UI_SELECTOR、DOM_STATE)
- `internal-unstable`:站内未文档化 endpoint、签名 / queryId 漂移、字段 silent rename(PAGE_FETCH、INTERCEPT)

### `Evidence` 三行

每行都是事实,不是猜测:

```md
- observed request/state: GET /api/v2/list (sample_paths: $.data.items:array(20), $.data.items[0].title:string)
- auth source: browser cookie (sessionid),无 CSRF
- replay result: 200 / application/json / 20 items / 非空
```

`observed request/state` 在 `DOM_STATE` 时写 state global key(`window.__INITIAL_STATE__.feed.items`);在 `UI_SELECTOR` 时写 selector path 或 a11y locator(`role=list[name="Trending"] > listitem`)。

### `If PAGE_FETCH or INTERCEPT` 三行论证

```md
Why PUBLIC_API / COOKIE_API are unavailable: <因为 a_bogus signature 必须 page runtime 生成 / 公开 API 缺少 since 字段 / 接口仅在登录态曝露但 cookie 透传会触发 anti-bot>
Why UI_SELECTOR / DOM_STATE are not safer: <因为数据是无限滚动 + 增量加载,DOM 一次只能拿 1 屏 / 因为目标是 write action,UI 无对应操作>
Why the maintenance cost is acceptable: <因为业务需求要 raw timeline cursor / 因为已经接受漂时 autofix 流程兜底>
```

**反模式**:

- ❌ "因为 API 比 DOM 高级" — 不是论证,是假设
- ❌ "因为 selector 不可靠" — 数据不支持(UI_SELECTOR 跟 COOKIE_API 同档)
- ❌ "因为我看到 17 XHR" — 不是论证,是 booking #1680 反例

正确论证须基于:endpoint 的**真实**不可达 / 操作语义本质 / 维护成本承担方有明确接收方。

### `If UI_SELECTOR / DOM_STATE`

```md
- semantic anchor: <a11y role / data-testid / framework-stable class>
- typed error path: <selector 失配时抛 EmptyResultError / CommandExecutionError>
```

不需要"why not API"过度辩护。如果你能简短说一句"目标是 publish,没有公开 write API"或"数据在 SSR HTML 直接 inline 了"就够了。

---

## 5. 与其他 reference 的关系

| 文件 | 关系 |
|---|---|
| [`api-discovery.md`](./api-discovery.md) | §1-5 是 endpoint 发现的具体方法。本文件指它,但本文件管"用 endpoint 证据填 strategy note",那边管"怎么先找到 endpoint" |
| [`site-recon.md`](./site-recon.md) | Pattern A-E 是 site classification。Pattern A 命中 ≠ `PAGE_FETCH` 必然合适 — 还要看 `api_candidates` 是不是 `likely_data` |
| [`coverage-matrix.md`](./coverage-matrix.md) | 鉴权列已对齐 6 档 strategy enum |
| [`adapter-template.md`](./adapter-template.md) | 写代码模板。strategy note 应该在打开 template 之前已经定好 |
| [`success-rate-pitfalls.md`](./success-rate-pitfalls.md) | 11 种 silent failure 模式 — 多数发生在 strategy 选错时(比如把 noise endpoint 当业务数据) |

---

## 6. 反例案例库

### booking #1680 — Pattern A 误判

旧判定按 XHR count 推 Pattern A,实际 17 XHR 全是 analytics / personalization side channel。新 `verdict` 系统能识别为 `noise`,落到 Pattern C → SSR HTML scrape。

### Twitter GraphQL — PAGE_FETCH 高维护成本的典型

`queryId` 每隔 1-2 月漂一次,字段名 silent rename(`legacy.user_screen_name` → `core.user.screen_name`)。30 天 9 个 fix PR。`Why the maintenance cost is acceptable` 的合理论证:业务需要 raw timeline cursor、autofix 流程已接住、fixed 时间窗口可控。

### xiaohongshu signed URL — INTERCEPT 必要场景

`a_bogus` signature 由 page runtime 即时生成,无法在 Node 端复现也不能拷贝 cookie 跨 origin replay。合理 strategy 是 `INTERCEPT`:触发 UI 让页面自己发请求,从 response 取数据。

### weread-official — PUBLIC_API 首选

WeRead 官方 Agent Gateway 有 Bearer auth + 文档化 schema。一方契约 + 不依赖 cookie / 不依赖浏览器 — 最理想的 strategy。维护成本最低。
Loading