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

pokemon-radar-api

把 twpkinfo.com 的寶可夢雷達資料,透過 Redis 快取成一支 FastAPI 給前端 / n8n 呼叫。

這份 README 是給一年後回來看的你自己:最常用的指令放最上面,架構與決策放最下面。CLAUDE.md 是給 AI 助手用的,可以忽略。


最常用的指令

全部用 uvPython 3.12)。設定讀 .env(不在 git 裡,本機有)。

# 起本機 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 — 走的是舊 pipelinepokemon_location_fetcher.py 直接輸出本地 JSONscp192.168.42.104 的 caddy不經 Redis不經 API。跟 fetcher/API 這條線沒關係、請不要混淆。

沒有 test / lint / build。


.env 速查

Secrets 放 .envgitignore。本機與生產共用同一份格式、只改 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 參數即時二次篩選 → 回傳。永遠回 200Redis 空或不可用都回空陣列)。

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

{
  "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:latestString(JSON)TTL 由 REDIS_TTL 控制。Value 結構:

{ "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 即時二次篩選

非看不可的設計決策

  • 為什麼要 Redisv1 是每個 /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 端先做一輪粗篩再寫 RedisMIN_REMAININGAPI 端再做第二輪 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)"(不會 500reader.pyTimeout connecting to server。確認 docker container / .envREDIS_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 舊 CLIfetch_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 助手的指引
Description
Redis-backed FastAPI wrapper for twpkinfo.com Pokemon radar
Readme 258 KiB
Languages
Python 70.4%
JavaScript 25.4%
Shell 4.2%