← 返回部落格
·14 min 閱讀·技術筆記

Web Log:把 Chrome 的 HTTP / WebSocket 流量灌進 ring buffer,讓 Claude Code 直接查

做 Schat 反向工程 Google Chat 私有協定時,DevTools Network panel 完全不夠用——它跟 chrome.debugger 互斥、查詢不了、留不下來。所以做了 web-log:Chrome extension 持續攔截 HTTP(S) 與 WebSocket → in-memory ring buffer → Claude Code skill 自然語言查詢。本文記錄它的設計、與 Schat 開發時的實際搭配,以及 chrome.debugger API 那些討厭的硬性限制。

Chrome ExtensionMV3Claude Code反向工程開發工具
GP Wang
GP Wang
GWP4 STUDIO 創辦人 · 軟體 / 資料工程師

從 Schat 的反向工程說起

上一篇寫了 Schat——一個用 Chrome extension 橋接、把 Google Chat 包成 Slack 風格的本地前端。那個專案最痛的不是寫前端,是反向工程 Google Chat 的私有協定 Dynamite

每一個你想實作的功能——送訊息、加 reaction、列頻道、訂閱即時推送——都要先在 Google 原生網頁裡實際操作一次,看 Network panel 它送了什麼、收了什麼,把 payload 形狀抓出來、再去 inject-main 裡面照著重現。

聽起來只是「打開 DevTools 看一下」,但實際做起來會撞到一連串問題:

  1. DevTools Network panel 跟 chrome.debugger 互斥——Schat 的 extension 自己也要用 chrome.debugger 看 chat 分頁的請求(為了學 session 參數),結果我每次開 DevTools 想看流量,extension 那邊就斷線。
  2. 每次重整就清空。在 Google Chat 點來點去、Network 累積幾百筆,重整一次重來。
  3. 不能程式化查詢。我想跟 Claude Code 講「剛才那筆 /messages/scheduled 的 response 是什麼形狀」,總不能截圖貼進去。
  4. WebSocket-like 的 streaming POST。Google Chat 的即時推送不是真正的 ws,是一條長時間開著的 POST,內容是切片進來的 nested-array 訊息。DevTools 對這種看得到,但沒有任何方式可以 export 出來

於是同時做 Schat 的時候,順手做了 Web Log——一個專門解決這些痛點的工具。這篇就是它的設計筆記。

它在做什麼

一句話:把 Chrome 的 HTTP(S) 與 WebSocket 流量持續灌進一個本地 ring buffer,給 Claude Code 用自然語言查。

組成有三塊:

Chrome extension (MV3)
  ├ webRequest API           攔 HTTP 請求 metadata(method, headers, request body)
  └ chrome.debugger API      抓 HTTP response body 與 WebSocket frame(含 payload)
        │
        │ POST /ingest
        ▼
Node.js collector (localhost:9999)
  └ in-memory ring buffer 預設 2 GiB,丟最舊
        │
        │ HTTP query
        ▼
Claude Code skills
  ├ web-log-parse    「找剛剛 api.foo.com 的 POST」
  └ web-log-watch    「等 /login response 出現再繼續」

設計目的不是當第二個 DevTools,是當一個留得下來、查得到、Claude 看得懂的流量黑盒子。你在前面操作網頁,Claude 在後面隨時可以撈。

為什麼一定要 chrome.debugger

chrome.webRequest API 看似全能,但有兩件事它死也做不到

  • 看不到 HTTP response body——只能拿 status、headers,不能讀 body。
  • 看不到 WebSocket frame——連有沒有 frame 都不知道。

這兩件事 Chrome 故意不開給普通 extension,因為太敏感。要拿到,必須走 chrome.debugger API——也就是 Chrome DevTools Protocol(CDP)的程式介面。Web Log 在 background.js 裡會對被監聽的 tab attach 一個 debugger session:

chrome.debugger.attach({ tabId }, '1.3', () => {
  chrome.debugger.sendCommand({ tabId }, 'Network.enable')
  chrome.debugger.sendCommand({ tabId }, 'Page.enable')
})

chrome.debugger.onEvent.addListener((source, method, params) => {
  if (method === 'Network.responseReceived') { /* 配對 requestId */ }
  if (method === 'Network.loadingFinished')  { /* 抓 body */ }
  if (method === 'Network.webSocketFrameReceived') { /* server → client */ }
  if (method === 'Network.webSocketFrameSent')     { /* client → server */ }
})

代價是兩件事:

  • Chrome 上方會跳一條「DevTools 正在 debug 此分頁」的橫條。沒辦法消,CDP 的設計就是這樣,避免使用者不知道。
  • 同一個 tab 同時只能有一個 debugger client。如果你打開 DevTools,Web Log 就斷;反過來如果 Web Log 在抓,你點開 DevTools 它就被踢掉。

這個互斥就是為什麼開頭那段「Schat 的 extension 自己也要用 chrome.debugger,DevTools 一開就壞」會發生——CDP 是獨佔的。Web Log 在 popup 裡會列出哪些 tab 已被它 attach、可以一鍵 detach 換回 DevTools。

Ring buffer 為什麼放記憶體

Collector 是純 Node.js(連 npm install 都不用,只用內建 module),listen 127.0.0.1:9999,buffer 直接放記憶體:

  • process 結束就清空,不落地。Authorization header、Cookie、Set-Cookie 都會原樣存進來,我不想讓這些東西意外躺到硬碟上。
  • 大小用環境變數 WEB_LOG_MAX_BYTES 控制,預設 2 GiB。我之前抓滿一晚的 Google Chat 流量大概 700 MB,2 GiB 給普通開發 session 夠用。
  • 滿了就丟最舊,/status.totalDropped 會持續累加,方便你知道有沒有開始丟資料。
  • 單筆 response/payload 超過 256 KiB 在 extension 端就先 truncate,避免一個 video stream 把整個 buffer 占掉。

不寫硬碟、不做永續化的決定回頭看是對的——它讓「重啟一切就乾淨」變成一個簡單動作。對 debug 工具來說重要的不是歷史紀錄,是「我剛才那個動作的封包」。

Capture filter:你不會想全收

我第一版沒做 filter,直接全收。打開 chat.google.com 五分鐘就吃掉 600 MB——Google 自己的 telemetry、Hangouts 心跳、auth 續期、各種 chrome.googleapis 的雜訊比有用的 Dynamite 流量多 10 倍。

於是有了 rules:

每條 rule 的欄位:

  • actionallowdeny
  • host — host 子字串
  • pathPrefix — pathname 前綴
  • method — HTTP method(WebSocket 用 WS
  • urlRegex — 完整 URL regex

Rules 從上往下檢查,第一個所有條件都命中的決定 allow / deny;都沒命中走 defaultAction

調 Schat 時最常用的 preset 是「只抓 Google Chat 的 mutation 與 streaming POST」:

default: deny
1. allow  host=chat.google.com  pathPrefix=/u/0/api/  method=POST
2. allow  host=chat.google.com  urlRegex=batchexecute

這樣 buffer 裡只剩我要看的東西,2 GiB 撐好幾天都沒問題。

順帶一提,rules 可以用 CLI 改:

curl -X POST http://127.0.0.1:9999/config \
  -H 'Content-Type: application/json' \
  -d '{
    "rules": [
      {"action":"allow","host":"chat.google.com","pathPrefix":"/u/0/api/","method":"POST"}
    ],
    "defaultAction":"deny"
  }'

extension 每 5 秒會 poll 回來 sync。所以你可以叫 Claude 直接幫你改 filter——它讀過 SKILL.md,知道這個 API 怎麼打。

Skill:web-log-parse 與 web-log-watch

Buffer 有了之後,剩下的問題是怎麼讓 Claude 用得順手。

裝 skills 是兩個 symlink:

ln -s ~/project/web-log/skills/web-log-parse ~/.claude/skills/web-log-parse
ln -s ~/project/web-log/skills/web-log-watch ~/.claude/skills/web-log-watch

兩個 skill 解的問題不一樣:

web-log-parse:事後查詢

最普通的「我剛才做了一個動作,看看那個封包」。對話像這樣:

我:剛剛我在 Google Chat 點了 reaction,幫我把那個 mutation request 找出來 Claude:(call GET /events?host=chat.google.com&method=POST&limit=20)找到了,是這筆… 我:response body 完整貼給我 Claude:(call GET /event/847{ ... }

Skill 知道有 collector,會自己組 query。我做 Schat 時這個流程大概一天跑 50 次以上。

另一個它幫我省下的力氣是 mask:Authorization、Cookie 預設展示時會被 mask,除非我明確說「展開那個 header」。一不小心把 session cookie 貼進對話的事不會發生。

一個沒明講但很重要的小細節:collector 沒在跑的時候,skill 不會默默失敗——它會偵測到 offline,問你要不要它幫你在背景把 node collector/server.js 跑起來。同意了它就跑、確認起來之後再繼續查詢。所以日常用根本不會記得「我要先去開 collector」這件事。

web-log-watch:阻塞等待

這個比較好玩。它的場景是:

我:等使用者登入完,拿到 /login 的 200 response 之後,再開始實作下一段 Claude:(call wait-for.sh --host api.foo.com --method POST --path /login --status 2xx --timeout 180) [Claude 卡住等] [我手動去 app 登入] Claude:(拿到 matched event)OK,response 是 { token: ..., expiresAt: ... },繼續往下做 X、Y、Z

底層是一個 shell 腳本去 poll collector:

skills/web-log-watch/scripts/wait-for.sh \
  --host chat.google.com --method POST --path /batchexecute --timeout 180
# exit 0 + matched events JSON / exit 124 timeout / exit 2 collector offline

Flags 支援 --host --method --path --url --status --source --kind --direction --contains(payload 文本 grep)。

對於需要「使用者實際操作後才會發生的事件」這類任務特別有用——traditional 寫 code 你會等不到,因為事件還沒發生;用 web-log-watch 就是「Claude 等你動,你動完它接著做」。

跟 Schat 的實際搭配

把這兩個工具放在一起用,做 Schat 的時候真實的工作流是這樣:

情境 1:實作一個新的 op

我想做「列出排程訊息」。Google Chat 網頁有這個畫面,但我不知道它打哪個 endpoint:

  1. 打開 Web Log popup,filter 設成 chat.google.com POST。
  2. 在原生 Google Chat 點「排程訊息」分頁。
  3. 跟 Claude 說:看剛剛那個動作打的 endpoint 跟 response 形狀。
  4. Claude 用 web-log-parse 撈,把 batchexecutef.req payload 拆開,告訴我這個 op 的 RPC ID 是什麼、回傳的 nested array 第 N 個元素是排程列表。
  5. 我去 Schat 的 inject-main.js 照著實作 listScheduled,跑起來測。

如果做得不對,就再 trigger 一次原生網頁,再撈一次比對。整個 loop 從「貼截圖給 Claude 一筆筆讀」變成「Claude 自己拿」。

情境 2:debug 一個壞掉的 mutation

Schat 送排程訊息突然開始 500。

  1. 在 Schat 觸發一次送出 → fail。
  2. 跟 Claude 說:找剛剛 5 分鐘內 chat.google.com 的 5xx。
  3. Claude 撈出來:是 batchexecute 的某條 RPC 收到 INVALID_ARGUMENT
  4. 比對「我送的 payload」跟「上一次原生網頁送的 payload」(buffer 裡都有),發現某個欄位的型別變了。
  5. 修。

這種 diff 工作要不是 Web Log,得開兩個 DevTools 視窗、手動 copy-paste、肉眼比對。

情境 3:訂閱事件 stream 改了 wire format

Google Chat 每隔一陣子會調整推送格式。某天 Schat 收不到新訊息了:

  1. 跟 Claude 說:等下次有任何 chat.google.com 的 streaming POST 收到 frame 就告訴我。
  2. Claude web-log-watch --host chat.google.com --kind frame --direction received,卡在那等。
  3. 我去原生網頁讓別人發個訊息過來。
  4. Claude 拿到那筆 frame,跟舊版本對比,告訴我 nested array 結構少了一層 wrapper。
  5. 改 inject-main 對應的 parser。

邊界與不會做的事

  • 不會抓到 chrome://devtools://、Web Store 等特權頁面——Chrome 不准。
  • 已經有人 attach DevTools 的 tab,Web Log 就上不去(CDP 獨佔)。要切換時去 popup 按 detach / re-attach。
  • 原生 TCP/UDP 看不到——瀏覽器本來就摸不到。
  • WebRTC 的 media frame 看不到,只看得到 signaling。

還有一個比較重要的:Cookie 與 Authorization 是原樣記錄到 buffer。Skill 展示時預設 mask,但實體還是在 RAM 裡。不要對著有正式環境敏感資料的分頁長時間開著;要清就按 Clear 或重啟 collector。

一些感想

Web Log 一開始只是 Schat 開發過程的副產品——做著做著發現「我需要一個可以叫 Claude 幫我看 Chrome 流量的工具」,就停下來把它獨立成一個 repo。

它特別適合的場景,是反向工程任何 web app 的私有介面——這在我這個 Schat 的例子最明顯,但任何「我想做一個第三方 client 串某某網站」的情境都會撞上同樣的問題:DevTools 不夠用、需要持續觀察、需要程式化查詢、需要 Claude 看得懂封包。

兩個專案的關係:

  • Schat:把 Google Chat 換掉。用反向工程的私有協定。
  • Web Log:做反向工程時的眼睛和耳朵。把流量留下來給 Claude 查。

兩個都 MV3 Chrome extension + 本地起服務 + 跟 Claude 緊密整合的 pattern——這可能就是我近期的開發風格。


專案連結github.com/gpwork4u/web-log

MIT。安裝指令、capture rules 範例、API spec 完整放在 README 裡。

關於作者

GP Wang
GP Wang

GWP4 STUDIO 創辦人,超過 8 年軟體開發經驗,專精資料工程、全端開發與系統架構。 持續在部落格分享專案實作經驗與技術心得。

了解更多 →