Timmy 3f2c03d4bb docs: INFRA.md 更新生產 29 個 target 清單與覆蓋實測
- 取代舊 5 target 記錄(台北/內湖/板橋/新竹/宜蘭)
- 完整列出大台北 11 點、新北 12 點、新竹 1 點、宜蘭 5 點共 29 個座標
- FETCH_LOCK_TTL 從 400 調整到 600(29 點序列 ~5 分鐘)
- 新增實測數據:760 筆 filtered(台北 577 / 新北 713 / 宜蘭 64)

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

pokemon-radar-api

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

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

跨 repo / 跨機器的部署地圖前端、API、webhook 自動部署、NPM 反代、Gitea放在 docs/INFRA.md,回憶「當初怎麼設定的」從那篇下手。


最常用的指令

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

# 起本機 Redis一次性
docker run -d --name pokemon-radar-redis --restart unless-stopped -p 6379:6379 redis:7-alpine

# 起 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}'

通常不用手動跑 fetcher。API 採 stale-while-revalidate/scan 永遠秒回當前快取,若快取剩餘 TTL 低於 STALE_THRESHOLD(預設 120s或 key 不存在,會在背景 thread 觸發 radar_fetcher.main() 更新;同時用 Redis lockpokemon:radar:fetchingTTL 180s避免重複觸發。沒人打 /scan 時不會動,閒置零呼叫。

需要偵錯或手動拉資料時:

uv run python radar_fetcher.py     # 手動拉一次寫入 Redis
uv run python reader.py            # rich 表格看目前快取
uv run python check_redis.py       # raw JSON dump

生產環境 cron./fetch_and_upload.sh — 走的是舊 pipelinepokemon_location_fetcher.py 直接輸出本地 JSONscp192.168.42.104 的 caddy不經 Redis不經 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 單點掃描中心fallbackRADAR_TARGETS 未設才用)
RADAR_TARGETS fetcher 多點掃描,格式 name:lat,lng;name2:lat2,lng2;...;搭配 RADAR_ZOOM=15 街區級密度
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
STALE_THRESHOLD API 120 快取剩餘 TTL 低於此值(秒)就在背景觸發 fetcher
FETCH_LOCK_KEY API pokemon:radar:fetching 背景刷新互斥 lock 的 Redis key
FETCH_LOCK_TTL API 180 lock 自動過期時間,應 >= fetcher 最長執行時間

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,
  "generated_at": "2026-04-20T11:14:32+08:00",
  "items": [ /* id, name, gender, iv_pct, cp, level, latitude, longitude,
                 remaining, expire_at, fast_move, charge_move, is_perfect, ... */ ],
  "debug": { "notes": [...], "redis_ttl": 281 }
}

generated_at 是 fetcher 寫入 Redis 那份資料時的時間戳ISO 8601台北時區。空資料分支會是 null

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+ readerAPI 讀快取)後壓力從「每 request」降到「每 TTL」。
  • 為什麼 stale-while-revalidatev2.1:原本的 cron 方案「沒人用也一直打」,會被 twpkinfo 標記、也浪費 Function Server 資源。現在改為 API 端觸發:/scan 秒回當前 Redis 快取(即使快過期),若 TTL < STALE_THRESHOLD 或 key 不存在,用 threading.Thread 呼叫 radar_fetcher.main() 非同步更新;以 Redis SET NX EX 做互斥 lock 避免並發。閒置時完全靜止。
  • 為什麼兩條 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%