diff --git a/skills/opencli-adapter-author/SKILL.md b/skills/opencli-adapter-author/SKILL.md index 8af3eee7a..e753660d5 100644 --- a/skills/opencli-adapter-author/SKILL.md +++ b/skills/opencli-adapter-author/SKILL.md @@ -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`。 ``` @@ -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 命名 / 类型 / 顺序 | diff --git a/skills/opencli-adapter-author/references/coverage-matrix.md b/skills/opencli-adapter-author/references/coverage-matrix.md index 77a089c84..620dd0202 100644 --- a/skills/opencli-adapter-author/references/coverage-matrix.md +++ b/skills/opencli-adapter-author/references/coverage-matrix.md @@ -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` 排序键对比法 | diff --git a/skills/opencli-adapter-author/references/site-recon.md b/skills/opencli-adapter-author/references/site-recon.md index 7ed642e6c..bcb7f3c78 100644 --- a/skills/opencli-adapter-author/references/site-recon.md +++ b/skills/opencli-adapter-author/references/site-recon.md @@ -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 diff --git a/skills/opencli-adapter-author/references/strategy-selection.md b/skills/opencli-adapter-author/references/strategy-selection.md new file mode 100644 index 000000000..2c23728f7 --- /dev/null +++ b/skills/opencli-adapter-author/references/strategy-selection.md @@ -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 ` 的输出里 `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: +- typed error path: +``` + +不需要"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。维护成本最低。