Use when writing an OpenCLI adapter for a new site or adding a new command to an existing site. Guides end-to-end from first recon through field decoding, adapter coding, and verify. Replaces opencli-oneshot / opencli-explorer. For ad-hoc browser driving (no adapter), see opencli-browser instead; for a top-level orientation to opencli, see opencli-usage.
opencli-adapter-author
你是要為某個站點寫 adapter 的 agent。這份 skill 的目標:從零開始,在 30 分鐘內通過 opencli browser verify 閉環。
全程使用現有工具:opencli browser * / opencli doctor / opencli browser init / opencli browser verify。沒有新指令。
除錯瀏覽器型 adapter 時,優先直接加上 --trace on --keep-tab true --window foreground。--trace on 每輪都會產生 trace artifact,summary.md 是失敗/成功復盤的入口;--keep-tab true --window foreground 讓 tab lease 保留且瀏覽器視窗在前景,方便核對最終頁面狀態。
前置:看你落在哪
先拿 coverage-matrix.md 快速自測。三個問題:
- 資料在瀏覽器裡看得到嗎?(否 → 先解決驗證)
- 資料是 HTTP/JSON/HTML 嗎?(否 → 不在 skill 範圍)
- 需要即時推送嗎?(是 → 找同資料的 HTTP 介面;沒有就放棄)
三個都 yes 繼續。
頂層決策樹
先決定 strategy,再寫 adapter。 每次進入 Step 3/4 後、寫程式碼前,必須產出一段 strategy note。沒有這段 note,不要開始寫 clis/<site>/<name>.js。
核心判斷不是「API 比 DOM 高級」,而是 資料來源有沒有外部契約。實測維護成本顯示:公開/官方介面最穩;UI/DOM 語意通常也有使用者可見契約;站內未文件化的 XHR/GraphQL/signature endpoint 最容易漂移。不要為了「API-first」就把穩定的 UI/DOM 實作盲目遷到無契約的內部介面。
Strategy: PUBLIC_API | COOKIE_API | PAGE_FETCH | INTERCEPT | DOM_STATE | UI_SELECTOR
Contract: stable | visible-ui | internal-unstable
Evidence:
- observed request/state: <endpoint / state global / UI-only signal>
- auth source: <none / browser cookie / csrf from meta / localStorage / page runtime>
- replay result: <status + content-type + non-empty sample shape>
If Strategy is PAGE_FETCH or INTERCEPT:
- why PUBLIC_API / COOKIE_API are unavailable:
- why UI_SELECTOR / DOM_STATE are not safer:
- why the maintenance cost is acceptable:
Strategy classes:
| Strategy | 契約級別 | 用在什麼時候 | 證據要求 |
|---|---|---|---|
PUBLIC_API |
stable | 不需要登入,Node-side fetch 直接拿到目標資料 |
200 + JSON/HTML 含目標資料,不是埋點/廣告 |
COOKIE_API |
stable | Node-side fetch + page.getCookies() / header helper 能拿資料 |
cookie/CSRF 來源清楚,replay 非空 |
UI_SELECTOR |
visible-ui | publish/upload/click/表單,或頁面語意比內部介面更穩 | selector 有語意錨點;錯誤路徑是 typed error |
DOM_STATE |
visible-ui | 資料在 hydration state / bootstrap JSON / SSR HTML 裡 | state key / script JSON / HTML 結構明確 |
PAGE_FETCH |
internal-unstable | 只能在頁面上下文 fetch 才能複用 same-origin/session/runtime |
opencli browser eval fetch(...) 非空;必須解釋為什麼避不開內部介面 |
INTERCEPT |
internal-unstable | 請求簽章複雜,但頁面自己能自然發出請求 | 觸發 UI 後能攔到目標 response;必須解釋為什麼 UI/DOM 不夠 |
選擇規則:優先 PUBLIC_API / COOKIE_API。如果 UI/DOM 語意穩定,不要強行升級到 PAGE_FETCH / INTERCEPT。只有公開/官方介面不可用、UI/DOM 無法表達目標資料或操作時,才承擔無契約內部介面的維護成本。
實測:PAGE_FETCH / INTERCEPT 的修復頻率約為 PUBLIC_API 的 7-8 倍,UI_SELECTOR 跟 COOKIE_API 同級。詳細 ladder 推導、api_candidates 證據怎麼填、booking #1680 等反例請見 references/strategy-selection.md。
邊界:只複用頁面自己已經合法獲得的資料/能力。不教破解簽章、不繞驗證碼/風控/存取控制;遇到不可複用簽章(如必須由頁面 runtime 產生且不能安全抽象)就降級到 UI_SELECTOR / DOM_STATE / INTERCEPT。
START
│
▼
┌──────────────────────────┐
│ opencli doctor 通? │── no ──→ 修橋接(doctor 輸出裡的提示)
└──────────────────────────┘
│ yes
▼
┌────────────────────────────────────────────────────┐
│ 讀站點記憶: │
│ 1. ~/.opencli/sites/<site>/endpoints.json │
│ 2. ~/.opencli/sites/<site>/notes.md │
│ 3. references/site-memory/<site>.md │
└────────────────────────────────────────────────────┘
│ 命中 endpoint + 欄位 → 直接跳到【endpoint 驗證】(不跳寫 adapter!memory 可能過期)
│ 沒命中 → 繼續
▼
┌──────────────────────────┐
│ 站點偵察(site-recon) │ → Pattern A/B/C/D/E
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ API 發現(api-discovery)│ §1 network → §2 state → §3 bundle → §4 token → §5 intercept
└──────────────────────────┘
│ 拿到候選 endpoint
▼
┌────────────────────────────────────────────┐
│ 直接 fetch 驗證 endpoint(memory 命中也要跑)│── 401/403 ──→ 回到 §4 排 token
│ 資料非空 + 200 │── 空/HTML ──→ 回到 site-recon 換 Pattern
│ memory 裡的值還活著嗎? │── 站點改版 ──→ 標記舊 endpoint,回 api-discovery
└────────────────────────────────────────────┘
│ OK
▼
┌───────────────────────────────────────┐
│ 欄位解碼(memory 裡的 field-map 也要抽查)│ 自解釋 → 直接 / 已知代號 → field-conventions / 未知 → decode-playbook
│ 比對一條已知欄位和網頁肉眼值,確認沒對錯 │
└───────────────────────────────────────┘
│
▼
┌──────────────────────────┐
│ 設計 columns (output) │ 對照 output-design.md 的命名 / 型別 / 順序
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ opencli browser init │ 產生 ~/.opencli/clis/<site>/<name>.js 骨架
│ 複製最像的鄰居 adapter │
│ 改 name / URL / 對映三處 │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ opencli browser verify │── 失敗 ──→ autofix skill,用 --trace retain-on-failure 回對應步驟
└──────────────────────────┘
│ 成功
▼
┌──────────────────────────┐
│ 欄位 vs 網頁肉眼對一遍 │── 數值不對 ──→ 回欄位解碼
└──────────────────────────┘
│ 對得上
▼
┌──────────────────────────┐
│ 回寫 ~/.opencli/sites/ │ endpoints / field-map / notes / fixtures
└──────────────────────────┘
│
▼
DONE
Runbook(一步一步勾選)
[ ] 1. opencli doctor 回傳 "Everything looks good"
[ ] 2. 讀站點記憶:
[ ] ~/.opencli/sites/<site>/endpoints.json 存在?裡面有想要的 endpoint?
[ ] references/site-memory/<site>.md 存在?看「已知 endpoint」節
[ ] 命中後:**跳到第 5(endpoint 驗證) + 第 7(欄位核對)**,不能直接跳第 9 寫 adapter
[ ] memory 寫入超過 30 天(看 `verified_at`)→ 當作過期,按冷啟動走 Step 3 → 4
[ ] 3. 偵察(site-recon.md):
[ ] **首選**:`opencli browser analyze <url>` 一步拿 pattern + 反爬 + 最近 adapter + next step
[ ] `analyze` 結論模糊時再手跑:`open` → `wait time 2` (或 `wait xhr <regex>`) → `network`
[ ] 定 Pattern(A / B / C / D / E)
[ ] 4. API 發現(api-discovery.md)按 Pattern 選 §:
[ ] Pattern A → §1 network 精讀
[ ] Pattern B → §2 state 抽取 + §1 深層資料
[ ] Pattern C → §3 bundle / script src 搜尋
[ ] Pattern D → §4 token 來源 + 降級 §5
[ ] Pattern E → 找 HTTP 輪詢介面;找不到才 §5
[ ] 5. 直接 fetch 候選 endpoint 驗證:
[ ] 回傳 200
[ ] 回應含目標資料(不是 HTML / 廣告)
[ ] 6. 寫 strategy note(寫程式碼前的強制產物):
[ ] 從 `PUBLIC_API / COOKIE_API / PAGE_FETCH / INTERCEPT / DOM_STATE / UI_SELECTOR` 選一個
[ ] 填 Contract:`stable / visible-ui / internal-unstable`
[ ] 填 Evidence:observed request/state、auth source、replay result
[ ] 如果選 `PAGE_FETCH` / `INTERCEPT`,必須解釋為什麼 `PUBLIC_API` / `COOKIE_API` / `UI_SELECTOR` / `DOM_STATE` 都不適合
[ ] 如果選 `UI_SELECTOR` / `DOM_STATE`,不需要為「為什麼不是 API」過度辯護;只要說明語意錨點和 typed error 路徑
[ ] 7. 欄位解碼:
[ ] 自解釋 → 直接用 key
[ ] 已知代號 → field-conventions.md 查表
[ ] 未知代號 → field-decode-playbook.md(排序鍵比對 / 結構差分 / 常數排查)
[ ] 8. 設計 columns(output-design.md):
[ ] 命名 camelCase 且對齊鄰居 adapter
[ ] 型別 / 單位 / 百分比格式清楚
[ ] 順序:識別列 → 業務數字 → metadata
[ ] 9. 寫 adapter(adapter-template.md):
[ ] opencli browser init <site>/<name>
[ ] 找同站點或同型別最像的 adapter,cp 過來
[ ] 改 name / URL / 欄位對映
[ ] 10. opencli browser verify <site>/<name>
[ ] 首輪通過後立刻 `--write-fixture` 產生 `~/.opencli/sites/<site>/verify/<cmd>.json` 種子
[ ] 手改種子:加 `patterns`(URL / 日期 / ID 格式)+ `notEmpty`(核心欄位)+ 收緊 `rowCount`
[ ] 再跑一次 `opencli browser verify <site>/<name>`,確認 ✓ matches fixture
[ ] 11. 欄位值 vs 網頁肉眼比對(別只看「Adapter works!」)
[ ] 12. 回寫站點記憶(**verify 通過 + 肉眼比對對得上之後**,schema 見 `references/site-memory.md`):
[ ] `endpoints.json`:以 endpoint 的短名為 key,value = `{url, method, params.{required,optional}, response, verified_at: YYYY-MM-DD, notes}`
[ ] `field-map.json`:只追加新代號。key = 欄位代號,value = `{meaning, verified_at: YYYY-MM-DD, source}`;**已存在的 key 不要覆蓋**,有衝突先和網頁肉眼值對齊再寫
[ ] `notes.md`:頂部追加一段 `## YYYY-MM-DD by <agent/user>`,寫本次寫 adapter 時遇到的新坑 / 新結論
[ ] `verify/<cmd>.json`:**必填。** `opencli browser verify` 的期望值(args / rowCount / columns / types / patterns / notEmpty),Step 10 已經讓你產生了,這裡只是 checklist
[ ] `fixtures/<cmd>-<YYYYMMDDHHMM>.json`:存一份該 endpoint 的完整回應樣本(去掉 cookie / token / 使用者私有欄位再存),給後續欄位比對 / 離線 replay 用
[ ] 除錯過程中如果在 repo / adapter 目錄 dump 過暫存檔(`.dbg-*.html` / `raw-*.json` / 等),**在 commit 前清乾淨**——這些本來就該落在 `~/.opencli/sites/<site>/fixtures/` 或 `/tmp/`
降級路徑(某步卡住跳到哪)
| 卡在 | 現象 | 跳去 |
|---|---|---|
| Step 4 API 發現 | network 空,__INITIAL_STATE__ 也空 |
§3 bundle 搜 baseURL |
| bundle 搜不到 baseURL | §5 intercept | |
| Step 5 endpoint 驗證 | 401 / 403 | §4 token 排查 |
| 200 但回應是 HTML | 回 Step 3 換 Pattern 判斷 | |
200 但 data: [] 空 |
參數傳錯 / 介面改版,回 §1 看 network 裡真實請求頭 | |
| Step 7 欄位解碼 | 排序鍵比對推不出 | field-decode-playbook.md §3 結構差分 |
| 還推不出 | 先輸出 raw,adapter 跑起來再迭代 | |
| Step 10 verify 失敗 | fltt 漏了 / 欄位對映錯 |
autofix skill;重現指令加 --trace retain-on-failure |
某列永遠是 null |
欄位路徑錯了,回 Step 7 | |
| Step 10 verify fixture mismatch | [pattern] row[i] 報錯 |
先肉眼比對網頁值;值對 → 是 fixture pattern 太嚴,放寬;值不對 → 欄位對映錯 |
[column] missing column "X" |
實際 response 沒這列(站點改版 or args 影響);重新 --update-fixture 或修 adapter |
|
[type] actual null / undefined |
欄位提取失敗,回 Step 7 重抽;暫時 fallback 用 union type string|null 只有在語意真的可空時用 |
|
| Step 11 數值不對 | 差 10000 倍 | 單位不統一(「萬」 vs 「元」) |
| 百分比小 100 倍 | 回應已是 0.025,不要 × 100 |
參考文件
| 文件 | 什麼時候翻 |
|---|---|
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 之前:契約模型 + 實測修復頻率 + api_candidates 證據用法 + 反例 |
references/field-conventions.md |
Step 7 查已知欄位代號 |
references/field-decode-playbook.md |
Step 7 欄位不在辭典時 |
references/output-design.md |
Step 8 命名 / 型別 / 順序 |
references/adapter-template.md |
Step 9 檔案結構 + 活例子 convertible.js |
references/site-memory.md |
總覽:in-repo 種子 + 本地 ~/.opencli/sites/ 的兩層結構 |
references/site-memory/<site>.md |
Step 2 讀站點公共知識(eastmoney / xueqiu / bilibili / tonghuashun 已鋪) |
references/success-rate-pitfalls.md |
Step 7 / 11 踩坑前翻:11 種「verify 能過但資料是錯的」靜默失敗(含 aria-label locale-dependence) |
references/jsdom-fixture-pattern.md |
當 adapter 走 page.evaluate 內 DOM 抽取、且 mocked-evaluate 單測漏 silent bug 時——把 HTML 凍進 clis/<site>/__fixtures__/ 用 JSDOM 跑(含 fixture 建立 mandatory awk 'NF>0' 收緊 + reverse-validate 紀律) |
references/typed-errors.md |
寫 func 主體之前必讀:5 類 typed error 落點表(ArgumentError / EmptyResultError / CommandExecutionError / AuthRequiredError / TimeoutError)+ 三大 silent anti-pattern(silent-clamp / sentinel-row / generic CliError)的反例修法 |
關鍵約定
- adapter 只引用
@jackwener/opencli/registry+@jackwener/opencli/errors,不用第三方 columns陣列和func回傳物件 keys 完全對齊(含順序)- 中間解析物件 key 不能跟
columns任一項重疊(否則 silent-column-drop audit 誤判,PR #1329 R1 真的踩過;改成專屬命名 + push row 時 destructure aliasing) browser:field 決定 func 簽章:browser:false → (args),browser:true → (page, args)。搞反時args實際是 debug flag,所有外部參數 silent fallback 到 default(PR #1329 upstream 之前 8 個 non-browser adapter 全踩過這個)- 已知失敗按
references/typed-errors.md5-classification 拋對應 typed error;不要 silentreturn [],不要 silentreturn [{sentinel}],不要Math.max/minsilent clamp 外部參數 - 寫私人 adapter 用
~/.opencli/clis/<site>/<name>.js(免 build);要提 PR 才 copy 到clis/<site>/<name>.js - 站點記憶每輪回寫:沒記憶 → 用 skill → 產生記憶 → 下次變 5 分鐘
- 除錯過程中的原始 dump / 抓包 / HTML 樣本只能落在
~/.opencli/sites/<site>/fixtures/或/tmp/。嚴禁在 repo 根目錄、clis/<site>/或當前工作目錄留.dbg-*.html / raw-*.json / sample.*這類暫存檔(PR diff 會帶上去,別人 review 時很煩)。 - JSDOM unit-test fixture(
clis/<site>/__fixtures__/<command>.html)是上面那條的例外——它是有意 commit 進 repo 的 review artifact,不是暫存 dump。但因此 quality bar 要更高:必須按references/jsdom-fixture-pattern.md的 5 步做完(含 mandatoryawk 'NF>0'空白行收緊),並 reverse-validate 一道證明 regression guard 真能掛。
卡住了
- 診斷類:
opencli doctor→ 看notes.md→ 搜 autofix skill - 欄位解碼類:
field-decode-playbook.md全三節走完 → 先輸出 raw 迭代 - endpoint 找不到:api-discovery §5 intercept 兜底
不要猜。猜錯了 verify 能通過但資料是錯的,使用者看到亂碼才發現。






