Files
pokemon-radar-api/README.md
Timmy c95d6074d5 docs: 合併成單一 README(寫給一年後回來看的自己)
- 常用指令放最上面,.env 和 API 速查在中間,架構與故障排除在最下面
- QUICKSTART / SUMMARY 砍掉,WHY 融進架構段
- 移除「給新人入門」的 step-by-step 路徑

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

167 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pokemon-radar-api
把 twpkinfo.com 的寶可夢雷達資料,透過 Redis 快取成一支 FastAPI 給前端 / n8n 呼叫。
這份 README 是給**一年後回來看的你自己**:最常用的指令放最上面,架構與決策放最下面。`CLAUDE.md` 是給 AI 助手用的,可以忽略。
---
## 最常用的指令
全部用 `uv`Python 3.12)。設定讀 `.env`(不在 git 裡,本機有)。
```bash
# 起本機 Redis一次性
docker run -d --name pokemon-radar-redis --restart unless-stopped -p 6379:6379 redis:7-alpine
# 跑一次 fetcher把資料抓進 RedisTTL 300s
uv run python radar_fetcher.py
# 看 Redis 目前有什麼rich 表格)
uv run python reader.py
# 看 Redis raw JSON
uv run python check_redis.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
# 測 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` | 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
### `GET /ping`
```
200 → {"status":"ok","backend":"redis"}
```
### `POST /scan`
從 Redis 讀取 → 依 body 參數即時二次篩選 → 回傳。**永遠回 200**Redis 空或不可用都回空陣列)。
**Body**
| 欄位 | 說明 |
|---|---|
| `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 模式下忽略** |
**Response**
```json
{
"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多人同時打直接壞。拆成 writerfetcher 定期 cron+ readerAPI 讀快取)後壓力從「每 request」降到「每 TTL」。
- **為什麼兩條 pipeline 並存**caddy 前端走靜態 JSON舊 CLI 的產物),沒理由為它引 Redis。所以 A 走 Redis 給 APIB 走本地檔給靜態前端,**兩條都還在跑**。
- **`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 是空 / 登入頁 DOMpipeline 不會炸只會吐 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` | 所有 secretgitignore |
| `logging.ini` | uvicorn logging |
| `CLAUDE.md` | 給 AI 助手的指引 |