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>
This commit is contained in:
2026-04-20 09:13:59 +08:00
commit f7ef105436
18 changed files with 2919 additions and 0 deletions

92
SUMMARY.md Normal file
View File

@@ -0,0 +1,92 @@
# 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.sh``flock` 避免重入,跑完後 `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=localhost``pokemon_radar_api.py``reader.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 RedisError``scan()``err` 切換 `source``redis (unreachable)` 並把錯誤訊息放進 `debug.note`。三種情況連線成功、key 不存在、連不上)都回 200前端不會炸。
### 備份檔清理
repo 曾保留多份 `*_BACKUP_<timestamp>` 檔案當作版本保險,但有 git 就沒必要。已清除。
## 執行環境現況
- **本機開發**Docker 起 `redis:7-alpine``127.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 並存。