docs: 合併成單一 README(寫給一年後回來看的自己)
- 常用指令放最上面,.env 和 API 速查在中間,架構與故障排除在最下面 - QUICKSTART / SUMMARY 砍掉,WHY 融進架構段 - 移除「給新人入門」的 step-by-step 路徑 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
224
README.md
224
README.md
@@ -1,69 +1,63 @@
|
||||
# pokemon-radar-api
|
||||
|
||||
把 twpkinfo.com 的寶可夢雷達資料包成 Redis-backed 的 HTTP API,給前端 / n8n 呼叫。
|
||||
把 twpkinfo.com 的寶可夢雷達資料,透過 Redis 快取成一支 FastAPI 給前端 / n8n 呼叫。
|
||||
|
||||
- 上手操作:`QUICKSTART.md`
|
||||
- 設計脈絡與演進:`SUMMARY.md`
|
||||
- Claude Code 指引:`CLAUDE.md`
|
||||
|
||||
本檔為參考手冊:組件一覽、指令、環境變數、HTTP API、Redis schema。
|
||||
這份 README 是給**一年後回來看的你自己**:最常用的指令放最上面,架構與決策放最下面。`CLAUDE.md` 是給 AI 助手用的,可以忽略。
|
||||
|
||||
---
|
||||
|
||||
## 組件
|
||||
## 最常用的指令
|
||||
|
||||
| 檔案 | 角色 |
|
||||
|---|---|
|
||||
| `radar_fetcher.py` | **現行 fetcher**:呼叫 Function Server 抓資料 → 篩選 → 寫入 Redis(`SETEX`,預設 TTL 300s) |
|
||||
| `pokemon_radar_api.py` | **FastAPI 服務**(`:8008`):`/ping`、`/scan`;從 Redis 讀取,依 request 參數即時二次篩選 |
|
||||
| `twpk_radar.mjs` | Function Server 上跑的 Puppeteer worker,負責開瀏覽器拉 `twpkinfo.com/ipoke.aspx` 的資料 |
|
||||
| `pokemon_location_fetcher.py` | **舊版 CLI**:直接呼叫 Function Server,輸出本地 `pokemon_data.{json,csv}` + `screenshot.png`。API 仍重用它的 `PokemonRadarClient.postprocess` 做二次篩選 |
|
||||
| `pokemon_radar_example.py` | 舊 CLI 的 Python 範例程式(非 CLI argparse 介面) |
|
||||
| `reader.py` | 從 Redis 讀取並用 `rich` 表格顯示目前快取內容 |
|
||||
| `check_redis.py` | 從 Redis 讀取並 dump 原始 JSON |
|
||||
| `fetch_and_upload.sh` | 生產環境 cron:以 `flock` 鎖定,呼叫**舊 CLI**產 `pokemon_data.json` 後 `scp` 到 `192.168.42.104:/opt/caddy/www/pokemon/spots.json` |
|
||||
| `run_pokemon_radar_api.sh` | 啟動 FastAPI(`uvicorn`,port 8008)的 shell 腳本 |
|
||||
| `state.json` | twpkinfo.com 登入後的 cookies / localStorage / UA / langs 快照,注入到 MJS 繞人機驗證 |
|
||||
| `logging.ini` | uvicorn 的 logging 設定 |
|
||||
全部用 `uv`(Python 3.12)。設定讀 `.env`(不在 git 裡,本機有)。
|
||||
|
||||
## 執行指令
|
||||
```bash
|
||||
# 起本機 Redis(一次性)
|
||||
docker run -d --name pokemon-radar-redis --restart unless-stopped -p 6379:6379 redis:7-alpine
|
||||
|
||||
全部以 `uv` 跑(Python 3.12,見 `.python-version`、`pyproject.toml`)。
|
||||
# 跑一次 fetcher,把資料抓進 Redis(TTL 300s)
|
||||
uv run python radar_fetcher.py
|
||||
|
||||
| 目的 | 指令 |
|
||||
|---|---|
|
||||
| 啟動 API | `./run_pokemon_radar_api.sh`(= `uv run uvicorn pokemon_radar_api:app --host 0.0.0.0 --port 8008 --log-config logging.ini`) |
|
||||
| 跑 fetcher(寫入 Redis) | `uv run python radar_fetcher.py` |
|
||||
| 跑舊 CLI(輸出本地檔案) | `uv run python pokemon_location_fetcher.py --script twpk_radar.mjs --lat 25.0478 --lng 121.5170 --zoom 16 --state state.json --ua-from-state --min-remaining 300 --min-iv 90` |
|
||||
| 查 Redis 內容(表格) | `uv run python reader.py` |
|
||||
| 查 Redis 內容(raw JSON) | `uv run python check_redis.py` |
|
||||
| 生產排程(含 scp 上傳) | `./fetch_and_upload.sh` |
|
||||
# 看 Redis 目前有什麼(rich 表格)
|
||||
uv run python reader.py
|
||||
|
||||
本專案沒有 test / lint / build 流程。
|
||||
# 看 Redis raw JSON
|
||||
uv run python check_redis.py
|
||||
|
||||
## 環境變數(`.env`)
|
||||
# 起 API
|
||||
./run_pokemon_radar_api.sh
|
||||
# 內部執行:uv run uvicorn pokemon_radar_api:app --host 0.0.0.0 --port 8008 --log-config logging.ini
|
||||
|
||||
| 變數 | 預設 | 誰用 | 說明 |
|
||||
# 測 API
|
||||
curl -s http://127.0.0.1:8008/ping
|
||||
curl -s -X POST http://127.0.0.1:8008/scan -H "Content-Type: application/json" \
|
||||
-d '{"lat":25.0478,"lng":121.5170,"min_iv":90}'
|
||||
```
|
||||
|
||||
**生產環境 cron**:`./fetch_and_upload.sh` — 走的是**舊 pipeline**(`pokemon_location_fetcher.py` 直接輸出本地 JSON,`scp` 到 `192.168.42.104` 的 caddy),不經 Redis,不經 API。跟 fetcher/API 這條線沒關係、請不要混淆。
|
||||
|
||||
沒有 test / lint / build。
|
||||
|
||||
---
|
||||
|
||||
## `.env` 速查
|
||||
|
||||
Secrets 放 `.env`(gitignore)。本機與生產共用同一份格式、只改 `REDIS_HOST` / `REDIS_PASSWORD`。
|
||||
|
||||
| 變數 | 誰讀 | 預設 | 備註 |
|
||||
|---|---|---|---|
|
||||
| `FUNCTION_URL` | `http://192.168.42.124:13000/function` | `radar_fetcher` | Function Server endpoint |
|
||||
| `FUNCTION_TOKEN` | (必填) | `radar_fetcher` | 未設會直接 `raise` |
|
||||
| `SCRIPT_PATH` | `twpk_radar.mjs` | `radar_fetcher` | JS worker 檔名 |
|
||||
| `STATE_PATH` | `state.json` | `radar_fetcher` | 登入狀態快照路徑 |
|
||||
| `UA_FROM_STATE` | `true` | `radar_fetcher` | 是否從 state 帶 UA / langs |
|
||||
| `RADAR_LAT` / `RADAR_LNG` | `25.0478` / `121.5170` | `radar_fetcher` | 掃描中心座標 |
|
||||
| `RADAR_ZOOM` | `11` | `radar_fetcher` | 地圖縮放 |
|
||||
| `RADAR_SHOT` | `false` | `radar_fetcher` | 是否擷取畫面(base64) |
|
||||
| `MIN_REMAINING` | `300` | `radar_fetcher` | 剩餘秒數門檻 |
|
||||
| `MIN_IV` | 空 | `radar_fetcher` | IV% 下限 |
|
||||
| `PERFECT_ONLY` | `false` | `radar_fetcher` | 只保留 100% IV |
|
||||
| `SPECIES` | 空 | `radar_fetcher` | 逗號分隔的 dex id 或名稱關鍵字(例:`133,伊布`) |
|
||||
| `HTTP_TIMEOUT` | `160` | `radar_fetcher` | 呼叫 Function Server 的 HTTP timeout(秒) |
|
||||
| `REDIS_HOST` | `localhost`(fetcher)/ `192.168.42.211`(API、reader) | 全部 | 注意兩邊預設不同,統一在 `.env` 指定 |
|
||||
| `REDIS_PORT` | `6379` | 全部 | |
|
||||
| `REDIS_DB` | `0` | 全部 | |
|
||||
| `REDIS_PASSWORD` | 空 | 全部 | 空字串視為無密碼 |
|
||||
| `REDIS_TTL` | `300` | `radar_fetcher` | 寫入 TTL |
|
||||
| `REDIS_KEY` | `pokemon:radar:latest` | 全部 | |
|
||||
| `FUNCTION_URL` | fetcher / 舊 CLI | `http://192.168.42.124:13000/function` | Function Server endpoint |
|
||||
| `FUNCTION_TOKEN` | fetcher / 舊 CLI | (必填) | 空字串 fetcher 會 `raise` |
|
||||
| `SCRIPT_PATH` | fetcher | `twpk_radar.mjs` | JS worker 檔名 |
|
||||
| `STATE_PATH` | fetcher | `state.json` | twpkinfo.com 登入快照,**最常壞的地方** |
|
||||
| `UA_FROM_STATE` | fetcher | `true` | 是否從 state 帶 UA / langs |
|
||||
| `RADAR_LAT` / `RADAR_LNG` / `RADAR_ZOOM` | fetcher | `25.0478` / `121.5170` / `11` | 掃描中心 |
|
||||
| `MIN_REMAINING` | fetcher | `300` | 寫入前先做一輪粗篩,低於此值不進 Redis |
|
||||
| `MIN_IV` / `PERFECT_ONLY` / `SPECIES` | fetcher | 空 / `false` / 空 | fetcher 端篩選(通常留空,由 API 端即時篩) |
|
||||
| `REDIS_HOST` / `REDIS_PORT` / `REDIS_DB` / `REDIS_PASSWORD` | 全部 | 本機 `127.0.0.1` / 生產 `192.168.42.211` | 預設本機無密碼 |
|
||||
| `REDIS_TTL` | fetcher | `300` | 寫入 TTL |
|
||||
| `REDIS_KEY` | 全部 | `pokemon:radar:latest` | |
|
||||
|
||||
---
|
||||
|
||||
## HTTP API
|
||||
|
||||
@@ -73,50 +67,100 @@
|
||||
```
|
||||
|
||||
### `POST /scan`
|
||||
從 Redis 讀取目前快取,依 body 篩選後回傳。**永遠回 200**,Redis 連不上或空資料都回空陣列,不會 500。
|
||||
從 Redis 讀取 → 依 body 參數即時二次篩選 → 回傳。**永遠回 200**(Redis 空或不可用都回空陣列)。
|
||||
|
||||
**Request body**
|
||||
|
||||
| 欄位 | 型別 | 說明 |
|
||||
|---|---|---|
|
||||
| `lat`, `lng`, `zoom`, `shot`, `state_path`, `ua_from_state`, `save_files` | — | 相容性欄位,Redis 模式下**忽略** |
|
||||
| `min_remaining` | `int ≥ 0` | 剩餘秒數下限 |
|
||||
| `min_iv` | `int 0-100` | IV% 下限 |
|
||||
| `perfect_only` | `bool` | 只保留 100% IV |
|
||||
| `species` | `str` | 逗號分隔 dex id 或名稱關鍵字 |
|
||||
|
||||
**Response**
|
||||
**Body**
|
||||
|
||||
| 欄位 | 說明 |
|
||||
|---|---|
|
||||
| `source` | `"redis"` / `"redis (empty)"` / `"redis (unreachable)"` |
|
||||
| `ttl` | Redis key 剩餘秒數 |
|
||||
| `count_raw` | Redis 內筆數 |
|
||||
| `count_filtered` | 經 request 參數篩選後筆數 |
|
||||
| `items` | 物件陣列,見下 |
|
||||
| `debug` | 含 `notes`(MJS 輸出)、`redis_ttl`、`note`(錯誤訊息) |
|
||||
| `min_remaining` (int) | 剩餘秒數下限 |
|
||||
| `min_iv` (int 0-100) | IV% 下限 |
|
||||
| `perfect_only` (bool) | 只保留 100% IV |
|
||||
| `species` (str) | 逗號分隔 dex id 或名稱關鍵字,如 `"133,伊布"` |
|
||||
| `lat / lng / zoom / shot / state_path / ua_from_state / save_files` | 相容性欄位,**Redis 模式下忽略** |
|
||||
|
||||
**`items[]` 欄位**
|
||||
`id / name / gender / remaining / expire_time / expire_at / latitude / longitude / iv_pct / iv_atk / iv_def / iv_sta / cp / level / is_perfect / fast_move / charge_move`
|
||||
|
||||
## Redis schema
|
||||
|
||||
| 項目 | 值 |
|
||||
|---|---|
|
||||
| Key | `pokemon:radar:latest` |
|
||||
| 型別 | String(JSON) |
|
||||
| TTL | `REDIS_TTL`(預設 300s) |
|
||||
| Writer | `radar_fetcher.py` |
|
||||
| Readers | `pokemon_radar_api.py`、`reader.py`、`check_redis.py` |
|
||||
|
||||
**Value 結構**
|
||||
**Response**
|
||||
```json
|
||||
{
|
||||
"source": "fetcher",
|
||||
"generated_at": "2026-04-20T09:06:36+08:00",
|
||||
"count_raw": 120,
|
||||
"count_filtered": 79,
|
||||
"items": [ /* 同上方 items[] */ ],
|
||||
"debug": { "notes": ["..."], "api_source": "Fresh from Function Server" }
|
||||
"source": "redis" | "redis (empty)" | "redis (unreachable)",
|
||||
"ttl": 281,
|
||||
"count_raw": 79,
|
||||
"count_filtered": 24,
|
||||
"items": [ /* id, name, gender, iv_pct, cp, level, latitude, longitude,
|
||||
remaining, expire_at, fast_move, charge_move, is_perfect, ... */ ],
|
||||
"debug": { "notes": [...], "redis_ttl": 281 }
|
||||
}
|
||||
```
|
||||
|
||||
### Redis schema
|
||||
Key `pokemon:radar:latest`,String(JSON),TTL 由 `REDIS_TTL` 控制。Value 結構:
|
||||
```json
|
||||
{ "source":"fetcher", "generated_at":"2026-04-20T09:06:36+08:00",
|
||||
"count_raw":120, "count_filtered":79,
|
||||
"items":[...], "debug":{ "notes":[...] } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 架構(一年後回來看用)
|
||||
|
||||
### 資料流
|
||||
|
||||
```
|
||||
twpkinfo.com/ipoke.aspx
|
||||
▲
|
||||
│ Puppeteer (stealth + 等黑影消退 + 解析 DOM)
|
||||
│
|
||||
Function Server (http://192.168.42.124:13000/function)
|
||||
▲
|
||||
│ POST { code: <注入後的 twpk_radar.mjs> }
|
||||
│
|
||||
┌─────┴─────────────────────────┐
|
||||
│ │
|
||||
│ A) radar_fetcher.py │ B) pokemon_location_fetcher.py(舊 CLI)
|
||||
│ → Redis (SETEX, 300s) │ → pokemon_data.{json,csv} + screenshot.png
|
||||
│ │ → fetch_and_upload.sh 跑這條,scp 給 caddy
|
||||
└─────┬─────────────────────────┘
|
||||
▼
|
||||
pokemon_radar_api.py :8008
|
||||
POST /scan → 從 Redis 讀,依 request 即時二次篩選
|
||||
```
|
||||
|
||||
### 非看不可的設計決策
|
||||
|
||||
- **為什麼要 Redis**:v1 是每個 `/scan` 都打 Function Server,一次 browser automation 要 30-60s,多人同時打直接壞。拆成 writer(fetcher 定期 cron)+ reader(API 讀快取)後壓力從「每 request」降到「每 TTL」。
|
||||
- **為什麼兩條 pipeline 並存**:caddy 前端走靜態 JSON(舊 CLI 的產物),沒理由為它引 Redis。所以 A 走 Redis 給 API,B 走本地檔給靜態前端,**兩條都還在跑**。
|
||||
- **`twpk_radar.mjs` 靠 regex 注入參數**:Function Server 的契約是「你丟 JS 過來我跑」,所以參數化只能在字串層做。三個錨點別改形狀:
|
||||
- `const TARGET = { lat: ..., lng: ..., zoom: ... };`
|
||||
- `const DO_SCREENSHOT = true|false;`
|
||||
- `globalThis.__POKE_STATE__ = ...;`
|
||||
動到形狀 regex 會靜默失敗、用錯參數跑。
|
||||
- **`pokemon_radar_api.py` 重用舊 CLI 的 postprocess**:為了省一份篩選邏輯,API import `PokemonRadarClient` 但傳空 url/token,只用它的 `postprocess` 方法。這是 workaround,理想做法是把篩選抽成獨立模組,兩份 fetcher 共用。
|
||||
- **fetcher 端先做一輪粗篩再寫 Redis**(`MIN_REMAINING` 等),API 端再做第二輪 per-request 篩選。Redis 裡不是「原始資料」而是「粗篩後資料」,如果要在 API 端篩出 fetcher 濾掉的東西是做不到的。
|
||||
|
||||
### 常見故障(依機率排序)
|
||||
|
||||
1. **`state.json` 過期** → Function Server 回 200 但 results 是空 / 登入頁 DOM,pipeline 不會炸只會吐 0 筆。`radar_fetcher.py` 有診斷:`raw_items` 非空但篩完為 0 時 log `remaining_min / remaining_max / perfect` 分佈。重登 twpkinfo.com 匯出新 state。
|
||||
2. **Redis 連不上** → API 回 `source:"redis (unreachable)"`(不會 500)、`reader.py` 回 `Timeout connecting to server`。確認 docker container / `.env` 的 `REDIS_HOST`。
|
||||
3. **`FUNCTION_TOKEN` 未設定** → fetcher `raise RuntimeError`。檢查 `.env`。
|
||||
4. **`/scan` 回空但 Redis 其實有資料** → body 的篩選條件太嚴,或 fetcher 的 `MIN_REMAINING` 已把你要的過濾掉了。
|
||||
|
||||
---
|
||||
|
||||
## 組件索引
|
||||
|
||||
| 檔案 | 角色 |
|
||||
|---|---|
|
||||
| `radar_fetcher.py` | 現行 fetcher,寫入 Redis |
|
||||
| `pokemon_radar_api.py` | FastAPI,讀 Redis |
|
||||
| `twpk_radar.mjs` | Function Server 上跑的 Puppeteer worker |
|
||||
| `pokemon_location_fetcher.py` | 舊 CLI,`fetch_and_upload.sh` 還在用 |
|
||||
| `pokemon_radar_example.py` | 舊 CLI 的 Python 呼叫範例 |
|
||||
| `reader.py` | Redis 內容表格檢視 |
|
||||
| `check_redis.py` | Redis raw JSON dump |
|
||||
| `fetch_and_upload.sh` | 生產 cron:舊 CLI → scp 到 caddy |
|
||||
| `run_pokemon_radar_api.sh` | 啟動 uvicorn |
|
||||
| `state.json` | twpkinfo.com 登入快照(gitignore) |
|
||||
| `.env` | 所有 secret(gitignore) |
|
||||
| `logging.ini` | uvicorn logging |
|
||||
| `CLAUDE.md` | 給 AI 助手的指引 |
|
||||
|
||||
Reference in New Issue
Block a user