Files
pokemon-radar-api/SUMMARY.md
Timmy f7ef105436 init: Redis-backed Pokémon 雷達 API
- radar_fetcher.py:呼叫 Function Server 抓 twpkinfo.com 資料後寫入 Redis (TTL 300s)
- pokemon_radar_api.py:FastAPI /scan,從 Redis 讀取並依 request 即時二次篩選;Redis 不可用時回 200 空結果,不再 500
- pokemon_location_fetcher.py:舊版 CLI,仍由 fetch_and_upload.sh 使用
- 文件:README (參考手冊)、QUICKSTART (操作指南)、SUMMARY (故事線)、CLAUDE.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 09:13:59 +08:00

5.7 KiB
Raw Blame History

SUMMARY

這份文件講為什麼長這樣:設計脈絡、演進、取捨、目前還沒處理乾淨的地方。要查指令看 README.md,要跑起來看 QUICKSTART.md

一句話

把一個只能「給座標、即時回寶可夢清單」的網站twpkinfo.com變成「前端 / n8n 能隨時打 API 拿最新資料」的服務。

為什麼需要這個專案

twpkinfo.com 的雷達頁(/ipoke.aspx)有完整的物種 / IV / CP / 技能 / 座標資訊,但:

  1. 沒有 API資料只在網頁 DOM 裡
  2. 有人機驗證與前端黑影遮罩,直接 curl 拿不到
  3. 前端(地圖標點)和 n8n通知高 IV 寶可夢)都需要結構化資料

所以這個專案做兩件事:把網頁變成 JSON把 JSON 包成 API

架構演進

這條 pipeline 不是一次設計出來的,是三個階段堆疊出來的,中間每個階段的產物都還在 repo 裡。

第一階段CLI 一把梭

pokemon_location_fetcher.py + twpk_radar.mjs + fetch_and_upload.sh

  • twpk_radar.mjs 送到 Function Server一台 browserless / 類似的 browser automation 服務)執行,繞 stealth、等黑影消退、解析資料回傳 JSON
  • pokemon_location_fetcher.py 是 Python CLI讀 MJS、用 regex 注入座標和 state.json、POST 到 Function Server、對回來的資料做 IV%/remaining/species 篩選,最後輸出 pokemon_data.json + pokemon_data.csv + screenshot.png
  • fetch_and_upload.shflock 避免重入,跑完後 scp 把 JSON 丟到另一台 caddy 主機,由前端靜態讀取

為什麼 regex 注入Function Server 的契約是「你丟一段 JS 過來,我幫你跑在無頭瀏覽器」。參數化只能在字串層做 — 所以 twpk_radar.mjs 裡的 const TARGET = {...};const DO_SCREENSHOT = ...;globalThis.__POKE_STATE__ = ...; 三個錨點變成 Python / JS 之間的介面。改 MJS 時若動到這些行的形狀Python regex 會失效但不會報錯,只會靜默用錯誤參數跑。

這階段到今天仍在生產跑,因為 caddy 前端是靜態檔,沒理由為了它多引入 Redis。

第二階段FastAPI v1已不存在

pokemon_radar_api.py_BACKUP_20251208225502(已清除)是第一版 API每個 /scan request 都呼叫一次 Function Server。

問題:

  • Function Server 單次 browser automation 要 30-60 秒,前端打一次就等一次
  • 多人同時打會超過 Function Server 的 concurrency
  • 資料源本身是頁面抓取3 秒打一次跟 3 分鐘打一次拿到的結果幾乎一樣(寶可夢生成間隔長)

第三階段Redis backed API現行

把 fetch 和 serve 拆開:

  • radar_fetcher.py 當 writercron 定期跑,結果 SETEX 到 RedisTTL 300s
  • pokemon_radar_api.py 當 reader每個 request 從 Redis 讀快取資料,依 request 參數(min_iv / perfect_only / species)做二次篩選後回傳
  • API 的 ScanRequest 保留了 lat / lng / zoom / shot 欄位但忽略它們,純粹相容舊前端不必改 client

Reader 端沒有 breaking change、Writer 端可以獨立調速Function Server 壓力從「每 request 一次」降到「每 TTL 一次」。

目前還沒處理乾淨的地方

兩條 pipeline 共存

radar_fetcher.py(新)和 pokemon_location_fetcher.py(舊)都能抓資料,篩選邏輯各寫了一份_postprocess vs PokemonRadarClient.postprocess)。修改篩選規則要兩邊同步改。

合併不是單純 refactor舊 CLI 還在 fetch_and_upload.sh 裡給前端生靜態檔,短期不會下掉。

API 重用舊 client 的 postprocess

pokemon_radar_api.py 為了不重寫一次篩選,import PokemonRadarClient 然後 _filter_helper = PokemonRadarClient(url="", token=""),只用它的 postprocess。這個 workaround 讓新 API 與舊 CLI 有了非必要的耦合。長期方向應該把 _postprocess 抽成獨立模組,兩邊共用。

.env 的環境耦合

radar_fetcher.py 預設 REDIS_HOST=localhostpokemon_radar_api.pyreader.py 預設 192.168.42.211 — 因為 fetcher 歷史上跑在 Redis 本機API 跑在遠端。實務上一定要透過 .env 顯式設定,不要依賴程式內預設。

state.json 的脆弱性

整條 pipeline 最常壞的點是 state 過期。過期時 Function Server 會正常 200但回來的資料是空陣列或登入頁 DOM — pipeline 不會炸,只會靜默吐 0 筆。radar_fetcher._postprocess 有個診斷分支:當 raw_items 非空但篩完為 0 時會 log remaining_min / remaining_max / perfect 分佈,優先看這個判斷是過濾太嚴還是資料本身有問題。

最近處理過的問題

/scan 在 Redis 不可用時會 500

get_data_from_redis() 原本只 try/except json.JSONDecodeError,沒處理 redis.exceptions.RedisError。結果:

  • Redis key 不存在 → 照原本設計回空陣列OK
  • Redis 連不上 → 例外沒接FastAPI 直接 500

修法:get_data_from_redis() 改回傳 (data, ttl, err) 三元組,整段 Redis 操作包 try/except RedisErrorscan()err 切換 sourceredis (unreachable) 並把錯誤訊息放進 debug.note。三種情況連線成功、key 不存在、連不上)都回 200前端不會炸。

備份檔清理

repo 曾保留多份 *_BACKUP_<timestamp> 檔案當作版本保險,但有 git 就沒必要。已清除。

執行環境現況

  • 本機開發Docker 起 redis:7-alpine127.0.0.1:6379.env 指本機、無密碼。Function Server 走 VPN 或同網段連 192.168.42.124
  • 生產Redis 在 192.168.42.211有密碼、API 在某台 8008 port、fetch_and_upload.sh 由 cron 觸發並 scp 到 192.168.42.104 的 caddy。兩條 pipeline 並存。