diff --git a/.env.example b/.env.example index 1a116fc9..6a09d029 100644 --- a/.env.example +++ b/.env.example @@ -1,75 +1,84 @@ -# Telegram Bot -TELEGRAM_BOT_TOKEN=your_bot_token_here -TELEGRAM_CHAT_ID=your_chat_id_here -# Optional multi-chat target (comma-separated). If set, it will be merged with TELEGRAM_CHAT_ID. -# Example: TELEGRAM_CHAT_IDS=-1003586303099,-1003539418691 +# PolyWeather backend/bot minimal reproducible config +# Full configuration guide: +# docs/CONFIGURATION_ZH.md +# Sensitive-only template: +# .env.secrets.example + +######################################## +# 1) Runtime paths and base behavior +######################################## +ENV=production +LOG_LEVEL=INFO +POLYWEATHER_MAP_URL=https://polyweather-pro.vercel.app/ +POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather +POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db +OPEN_METEO_DISK_CACHE_PATH=/var/lib/polyweather/open_meteo_cache.json +# Optional: host user/group mapping for Docker on Linux. +# Windows / macOS can usually keep the defaults. +UID=1000 +GID=1000 + +######################################## +# 2) Telegram bot minimal +######################################## +TELEGRAM_BOT_TOKEN= +TELEGRAM_CHAT_ID= TELEGRAM_CHAT_IDS= -# Optional: route /city and /deb outputs to a fixed forum topic. -# If TELEGRAM_QUERY_TOPIC_CHAT_ID is empty, fallback to command source chat. TELEGRAM_QUERY_TOPIC_CHAT_ID= TELEGRAM_QUERY_TOPIC_ID= -# Optional per-group topic routing (higher priority than fixed topic above): -# format: chat_id:topic_id,chat_id:topic_id -# Example: TELEGRAM_QUERY_TOPIC_MAP=-1003586303099:25513,-1003539418691:25514 TELEGRAM_QUERY_TOPIC_MAP= -TELEGRAM_ALERT_PUSH_ENABLED=true -TELEGRAM_ALERT_PUSH_INTERVAL_SEC=300 -TELEGRAM_ALERT_PUSH_COOLDOWN_SEC=1800 -TELEGRAM_ALERT_MIN_TRIGGER_COUNT=2 -TELEGRAM_ALERT_MIN_SEVERITY=medium -# Mispricing radar: skip push when YES buy price is above this cap (10c = 0.10) -TELEGRAM_ALERT_MISPRICING_MAX_YES_BUY=0.10 -TELEGRAM_ALERT_CITIES=ankara,london,paris,seoul,hong kong,shanghai,singapore,tokyo,tel aviv,toronto,buenos aires,wellington,new york,chicago,dallas,miami,atlanta,seattle,lucknow,sao paulo,munich +POLYWEATHER_BOT_GROUP_INVITE_URL= -# Open-Meteo (forecast data changes ~hourly, no need to refresh more often) +######################################## +# 3) Weather + cache +######################################## OPEN_METEO_CACHE_TTL_SEC=7200 OPEN_METEO_ENSEMBLE_CACHE_TTL_SEC=7200 OPEN_METEO_MULTI_MODEL_CACHE_TTL_SEC=7200 OPEN_METEO_MULTI_MODEL_CACHE_VERSION=v2 +OPEN_METEO_RATE_LIMIT_COOLDOWN_SEC=900 +OPEN_METEO_RATE_CACHE_TTL_SEC=3600 +OPEN_METEO_MIN_CALL_INTERVAL_SEC=3 +METAR_CACHE_TTL_SEC=600 +METEOBLUE_CACHE_TTL_SEC=7200 -# Proxy Setting (optional) -HTTPS_PROXY=http://127.0.0.1:7890 -HTTP_PROXY=http://127.0.0.1:7890 - -# Other Settings -LOG_LEVEL=INFO -ENV=production -POLYWEATHER_MAP_URL=https://polyweather-pro.vercel.app/ -# Runtime data directory (host path mounted into container at /var/lib/polyweather and /app/data) -POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather -# Recommended: keep SQLite outside repository workspace -POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db -# Recommended disk cache/state paths (optional; defaults are still /app/data/*) -OPEN_METEO_DISK_CACHE_PATH=/var/lib/polyweather/open_meteo_cache.json -# Unified Auth (Supabase + Google/Email) +######################################## +# 4) Auth / entitlement +######################################## POLYWEATHER_AUTH_ENABLED=false -# If true: website APIs require login; if false: guest access, login optional. POLYWEATHER_AUTH_REQUIRED=false -# If true, authenticated users must also have an active row in `subscriptions`. POLYWEATHER_AUTH_REQUIRE_SUBSCRIPTION=false +POLYWEATHER_REQUIRE_ENTITLEMENT=false SUPABASE_URL= SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= SUPABASE_HTTP_TIMEOUT_SEC=8 SUPABASE_AUTH_CACHE_TTL_SEC=30 SUPABASE_SUB_CACHE_TTL_SEC=60 -# Frontend wallet connection (WalletConnect v2) -# Apply in Vercel env as NEXT_PUBLIC_* +POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN= + +######################################## +# 5) Alerts / operations +######################################## +TELEGRAM_ALERT_PUSH_ENABLED=true +TELEGRAM_ALERT_PUSH_INTERVAL_SEC=300 +TELEGRAM_ALERT_PUSH_COOLDOWN_SEC=1800 +TELEGRAM_ALERT_MIN_TRIGGER_COUNT=2 +TELEGRAM_ALERT_MIN_SEVERITY=medium +TELEGRAM_ALERT_MISPRICING_MAX_YES_BUY=0.10 +TELEGRAM_ALERT_CITIES=ankara,london,paris,seoul,hong kong,shanghai,singapore,tokyo,tel aviv,toronto,buenos aires,wellington,new york,chicago,dallas,miami,atlanta,seattle,lucknow,sao paulo,munich + +######################################## +# 6) Frontend-facing shared values +######################################## NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com -# Bot command access guard (/city, /deb): -# - Pro entitlement removed -# - user only needs to be a member of any configured group (TELEGRAM_CHAT_IDS / TELEGRAM_CHAT_ID) -POLYWEATHER_BOT_GROUP_INVITE_URL= -# Group message points (anti-spam + ranking) -POLYWEATHER_BOT_MESSAGE_POINTS=4 -POLYWEATHER_BOT_MESSAGE_DAILY_CAP=40 -POLYWEATHER_BOT_MESSAGE_MIN_LENGTH=3 -POLYWEATHER_BOT_MESSAGE_COOLDOWN_SEC=30 -POLYWEATHER_BOT_CITY_QUERY_COST=1 -POLYWEATHER_BOT_DEB_QUERY_COST=1 -# Weekly leaderboard reward settlement -# settle_weekday: 1=Mon ... 7=Sun + +######################################## +# 7) Optional modules +######################################## + +# Weekly reward / leaderboard POLYWEATHER_WEEKLY_REWARD_ENABLED=true POLYWEATHER_WEEKLY_REWARD_TIMEZONE=Asia/Shanghai POLYWEATHER_WEEKLY_REWARD_SETTLE_WEEKDAY=1 @@ -78,24 +87,22 @@ POLYWEATHER_WEEKLY_REWARD_SETTLE_MINUTE=5 POLYWEATHER_WEEKLY_REWARD_CHECK_INTERVAL_SEC=300 POLYWEATHER_WEEKLY_REWARD_HTTP_TIMEOUT_SEC=10 POLYWEATHER_WEEKLY_REWARD_ANNOUNCE_ENABLED=true -# Backend entitlement guard (for /api/cities, /api/city/*, /api/history/*) -POLYWEATHER_REQUIRE_ENTITLEMENT=false -POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN= -# P1 Contract Checkout (MetaMask + Polygon USDC) +# Group message points +POLYWEATHER_BOT_MESSAGE_POINTS=4 +POLYWEATHER_BOT_MESSAGE_DAILY_CAP=40 +POLYWEATHER_BOT_MESSAGE_MIN_LENGTH=3 +POLYWEATHER_BOT_MESSAGE_COOLDOWN_SEC=30 +POLYWEATHER_BOT_CITY_QUERY_COST=1 +POLYWEATHER_BOT_DEB_QUERY_COST=1 + +# Payments POLYWEATHER_PAYMENT_ENABLED=false POLYWEATHER_PAYMENT_CHAIN_ID=137 POLYWEATHER_PAYMENT_RPC_URL=https://polygon-rpc.com -# Legacy single-token fallback (still supported) POLYWEATHER_PAYMENT_RECEIVER_CONTRACT= POLYWEATHER_PAYMENT_TOKEN_ADDRESS=0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 POLYWEATHER_PAYMENT_TOKEN_DECIMALS=6 -# Recommended multi-token config (supports USDC.e + Native USDC at the same time) -# Example: -# [ -# {"code":"usdc_e","symbol":"USDC.e","name":"USDC.e (PoS)","address":"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174","decimals":6,"receiver_contract":"0xYourCheckoutContract","is_default":true}, -# {"code":"usdc","symbol":"USDC","name":"Native USDC","address":"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359","decimals":6,"receiver_contract":"0xYourCheckoutContract"} -# ] POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON= POLYWEATHER_PAYMENT_CONFIRMATIONS=2 POLYWEATHER_PAYMENT_INTENT_TTL_SEC=1800 @@ -104,18 +111,13 @@ POLYWEATHER_PAYMENT_HTTP_TIMEOUT_SEC=10 POLYWEATHER_PAYMENT_POLL_INTERVAL_SEC=4 POLYWEATHER_PAYMENT_MAX_WAIT_SEC=50 POLYWEATHER_PAYMENT_TELEGRAM_NOTIFY_ENABLED=true -# Payment points redemption POLYWEATHER_PAYMENT_POINTS_ENABLED=true POLYWEATHER_PAYMENT_POINTS_PER_USDC=500 POLYWEATHER_PAYMENT_POINTS_MAX_DISCOUNT_USDC=3 -# Comma-separated allowed plans for checkout UI + backend validation. -# Default is monthly-only launch. POLYWEATHER_PAYMENT_ALLOWED_PLAN_CODES=pro_monthly -# JSON object -# Example: {"pro_monthly":{"plan_id":101,"amount_usdc":"5","duration_days":30}} POLYWEATHER_PAYMENT_PLAN_CATALOG_JSON= -# Polymarket P0 Read-Only Market Layer +# Polymarket market scan POLYMARKET_MARKET_SCAN_ENABLED=true POLYMARKET_GAMMA_URL=https://gamma-api.polymarket.com POLYMARKET_CLOB_URL=https://clob.polymarket.com @@ -128,10 +130,10 @@ POLYMARKET_DISCOVERY_LIMIT=200 POLYMARKET_SIGNAL_MIN_LIQUIDITY=500 POLYMARKET_SIGNAL_EDGE_PCT=2 -# Polygon Wallet Watcher (Single Chain P0) +# Polygon watcher POLYGON_WALLET_WATCH_ENABLED=false POLYGON_RPC_URL=https://polygon-rpc.com -POLYGON_WALLET_WATCH_ADDRESSES=0x0000000000000000000000000000000000000000 +POLYGON_WALLET_WATCH_ADDRESSES= POLYGON_WALLET_WATCH_INTERVAL_SEC=8 POLYGON_WALLET_WATCH_CONFIRMATIONS=2 POLYGON_WALLET_WATCH_MAX_BLOCKS_PER_CYCLE=30 @@ -141,24 +143,15 @@ POLYGON_WALLET_WATCH_TX_BASE=https://polygonscan.com/tx POLYGON_WALLET_WATCH_ADDR_BASE=https://polygonscan.com/address POLYGON_WALLET_WATCH_POLYMARKET_ONLY=true POLYGON_WALLET_WATCH_INCLUDE_DEFAULT_PM_CONTRACTS=true -# Optional custom Polymarket contracts, format: LABEL:0x...,LABEL2:0x... POLYGON_WALLET_WATCH_POLYMARKET_CONTRACTS= -# Polymarket Wallet Activity Watcher (all markets, not weather-only) +# Polymarket wallet activity POLYMARKET_WALLET_ACTIVITY_ENABLED=false -POLYMARKET_WALLET_ACTIVITY_USERS=0x0000000000000000000000000000000000000000 -# Optional dedicated chat targets for wallet activity push (recommended) -# If unset, fallback to TELEGRAM_CHAT_IDS / TELEGRAM_CHAT_ID. +POLYMARKET_WALLET_ACTIVITY_USERS= POLYMARKET_WALLET_ACTIVITY_CHAT_ID= POLYMARKET_WALLET_ACTIVITY_CHAT_IDS= -# Optional: mirror wallet activity push to a forum topic, while keeping existing chat targets unchanged. POLYMARKET_WALLET_ACTIVITY_TOPIC_CHAT_ID= POLYMARKET_WALLET_ACTIVITY_TOPIC_ID= -# Optional wallet nicknames: -# - CSV: 0xabc...=Whale_A,0xdef...=Main_Account -# - JSON: {"0xabc...":"Whale A","0xdef...":"Main Account"} -# - Env key: POLYMARKET_WALLET_ACTIVITY_USER_ALIASES -# (legacy typo POLYMARKET_WALLET_ACTIVITY_USERS_ALIASES is also accepted) POLYMARKET_WALLET_ACTIVITY_USER_ALIASES= POLYMARKET_WALLET_ACTIVITY_DATA_API_URL=https://data-api.polymarket.com POLYMARKET_WALLET_ACTIVITY_INTERVAL_SEC=20 @@ -177,10 +170,11 @@ POLYMARKET_WALLET_ACTIVITY_UPDATE_DEBOUNCE_SEC=30 POLYMARKET_WALLET_ACTIVITY_UPDATE_MAX_HOLD_SEC=120 POLYMARKET_WALLET_ACTIVITY_AVG_PRICE_SHOW_MIN=0.01 POLYMARKET_WALLET_ACTIVITY_AVG_PRICE_SHOW_MAX=0.99 -# Skip wallet activity pushes when position value is below this floor (USD). -# Set 0 to disable. POLYMARKET_WALLET_ACTIVITY_MIN_POSITION_VALUE_USD=0 -# Comma-separated wallet addresses exempt from min value floor. -# Example: 0x849d9a4dd64829b8b0141ea53e7caca7e99529ec POLYMARKET_WALLET_ACTIVITY_MIN_VALUE_EXEMPT_USERS= +######################################## +# 8) Optional proxies +######################################## +HTTPS_PROXY= +HTTP_PROXY= diff --git a/.env.secrets.example b/.env.secrets.example new file mode 100644 index 00000000..6c93ffd3 --- /dev/null +++ b/.env.secrets.example @@ -0,0 +1,44 @@ +# PolyWeather secrets-only template +# Copy the required lines into your real `.env` / platform secret manager. +# Never commit actual values. + +######################################## +# Telegram +######################################## +TELEGRAM_BOT_TOKEN= + +######################################## +# Supabase +######################################## +SUPABASE_URL= +SUPABASE_ANON_KEY= +SUPABASE_SERVICE_ROLE_KEY= +NEXT_PUBLIC_SUPABASE_URL= +NEXT_PUBLIC_SUPABASE_ANON_KEY= + +######################################## +# Entitlement / dashboard +######################################## +POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN= +POLYWEATHER_DASHBOARD_ACCESS_TOKEN= + +######################################## +# Meteoblue / third-party APIs +######################################## +METEOBLUE_API_KEY= + +######################################## +# Wallet / payments +######################################## +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= +POLYWEATHER_PAYMENT_RECEIVER_CONTRACT= +POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON= +POLYWEATHER_PAYMENT_PLAN_CATALOG_JSON= + +######################################## +# Optional exchange / market secrets +######################################## +POLYMARKET_API_KEY= +POLYMARKET_SECRET_KEY= +POLYMARKET_PASSPHRASE= +POLYMARKET_WALLET_ADDRESS= diff --git a/README_ZH.md b/README_ZH.md index 27faa02f..d1b255b2 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -127,6 +127,8 @@ docker compose logs -f polyweather | egrep "polymarket wallet activity watcher s - 商业化说明:[docs/COMMERCIALIZATION.md](docs/COMMERCIALIZATION.md) - Open-Core 边界:[docs/OPEN_CORE_POLICY.md](docs/OPEN_CORE_POLICY.md) - Supabase 接入:[docs/SUPABASE_SETUP_ZH.md](docs/SUPABASE_SETUP_ZH.md) +- 配置与密钥管理:[docs/CONFIGURATION_ZH.md](docs/CONFIGURATION_ZH.md) +- 前端部署(Vercel):[docs/FRONTEND_DEPLOYMENT_ZH.md](docs/FRONTEND_DEPLOYMENT_ZH.md) - 技术债(中文镜像):[docs/TECH_DEBT_ZH.md](docs/TECH_DEBT_ZH.md) - 技术债(主文档):[docs/TECH_DEBT.md](docs/TECH_DEBT.md) - 支付合约验证:[docs/payments/POLYGONSCAN_VERIFY.md](docs/payments/POLYGONSCAN_VERIFY.md) diff --git a/docker-compose.yml b/docker-compose.yml index 433c82e9..bbda1c04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,11 +7,13 @@ services: - .env volumes: # Persist runtime data outside git workspace. - # Host path defaults to /var/lib/polyweather and can be overridden by POLYWEATHER_RUNTIME_DATA_DIR. + # Host path defaults to /var/lib/polyweather and can be overridden in .env. - ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather # Keep /app/data compatibility for existing cache/state defaults. - ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data - ./bot.log:/app/bot.log # 挂载日志文件 + # UID/GID are mainly useful on Linux hosts to avoid root-owned output files. + # Windows / macOS can usually keep the fallback values. user: "${UID:-1000}:${GID:-1000}" polyweather_web: @@ -22,8 +24,10 @@ services: env_file: - .env volumes: + # Web service shares the same runtime data directory as bot/state tasks. - ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather - ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data ports: - "8000:8000" + # UID/GID are mainly useful on Linux hosts to avoid root-owned output files. user: "${UID:-1000}:${GID:-1000}" diff --git a/docs/CONFIGURATION_ZH.md b/docs/CONFIGURATION_ZH.md new file mode 100644 index 00000000..0b3d8c20 --- /dev/null +++ b/docs/CONFIGURATION_ZH.md @@ -0,0 +1,286 @@ +# 配置与密钥管理(中文) + +## 1. 目标 + +PolyWeather 的环境变量很多,但不是所有变量都属于同一层级。 + +当前推荐做法是把配置拆成三类: + +1. 可复现基础配置 + 放在:[.env.example](/E:/web/PolyWeather/.env.example) + +2. 敏感密钥模板 + 放在:[.env.secrets.example](/E:/web/PolyWeather/.env.secrets.example) + +3. 平台侧真实密钥 + 放在: + - VPS / Docker `.env` + - Vercel Environment Variables + - GitHub Secrets(如需要) + +## 2. 为什么要拆 + +如果把所有变量都平铺在一个 `.env` 里,会有三个问题: + +1. 新环境很难知道“最小启动到底需要哪些变量” +2. 敏感密钥和普通开关混在一起,容易误泄露 +3. 调优参数太多时,团队很难区分“必须填”和“保持默认即可” + +所以正确做法不是“减少变量数量”,而是: + +- 保留变量能力 +- 按职责分层 +- 给出最小启动路径 + +## 3. 文件职责 + +### 3.1 根 `.env.example` + +文件: + +- [.env.example](/E:/web/PolyWeather/.env.example) + +用途: + +- 后端 / Bot / Docker 的可复现配置模板 +- 只放变量名、默认值、开关与非敏感示例 + +### 3.2 根 `.env.secrets.example` + +文件: + +- [.env.secrets.example](/E:/web/PolyWeather/.env.secrets.example) + +用途: + +- 只列敏感项 +- 帮助运维明确哪些值必须从密钥系统注入 + +### 3.3 前端 `.env.example` + +文件: + +- [frontend/.env.example](/E:/web/PolyWeather/frontend/.env.example) + +用途: + +- 前端本地开发与 Vercel 环境变量模板 + +## 4. 配置分级 + +### 4.1 L1:最小启动必需项 + +这是“服务能跑起来”的最小集合。 + +后端 / Bot: + +- `TELEGRAM_BOT_TOKEN` +- `TELEGRAM_CHAT_ID` +- `POLYWEATHER_RUNTIME_DATA_DIR` +- `POLYWEATHER_DB_PATH` + +前端: + +- `POLYWEATHER_API_BASE_URL` + +如果启用登录: + +- `NEXT_PUBLIC_SUPABASE_URL` +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` +- `SUPABASE_URL` +- `SUPABASE_ANON_KEY` +- `SUPABASE_SERVICE_ROLE_KEY` + +### 4.2 L2:功能开关 + +这些变量一般不敏感,但会决定功能是否启用。 + +例如: + +- `POLYWEATHER_AUTH_ENABLED` +- `POLYWEATHER_AUTH_REQUIRED` +- `POLYWEATHER_AUTH_REQUIRE_SUBSCRIPTION` +- `POLYWEATHER_PAYMENT_ENABLED` +- `POLYMARKET_MARKET_SCAN_ENABLED` +- `POLYGON_WALLET_WATCH_ENABLED` +- `POLYMARKET_WALLET_ACTIVITY_ENABLED` + +### 4.3 L3:运行调优项 + +这些一般不需要在第一天就改。 + +例如: + +- 各类 `*_TTL_SEC` +- 各类 `*_TIMEOUT_SEC` +- 各类 `*_COOLDOWN_SEC` +- 各类 `*_INTERVAL_SEC` + +策略: + +- 先用默认值 +- 出现性能或运维问题时再调 + +### 4.4 L4:敏感项 + +这些变量不应写进公开文档截图,也不应提交到仓库。 + +例如: + +- `TELEGRAM_BOT_TOKEN` +- `SUPABASE_SERVICE_ROLE_KEY` +- `POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN` +- `POLYWEATHER_DASHBOARD_ACCESS_TOKEN` +- `METEOBLUE_API_KEY` +- `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` +- `POLYMARKET_SECRET_KEY` + +## 5. 推荐部署矩阵 + +### 5.1 VPS / Docker(后端 + Bot) + +建议放这些: + +- 根 `.env` 的后端项 +- 所有 secrets +- Bot / 支付 / watcher 配置 + +### 5.2 Vercel(前端) + +建议只放前端真正需要的变量: + +- `POLYWEATHER_API_BASE_URL` +- `NEXT_PUBLIC_SUPABASE_URL` +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` +- `POLYWEATHER_AUTH_ENABLED` +- `POLYWEATHER_AUTH_REQUIRED` +- `POLYWEATHER_DASHBOARD_ACCESS_TOKEN` +- `POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN` +- `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` +- `NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL` + +不要把后端专用密钥全搬进 Vercel。 + +### 5.3 GitHub Actions + +当前 CI 不需要大规模 secrets。 + +如果未来要做自动部署,再考虑: + +- `VERCEL_TOKEN` +- `VERCEL_ORG_ID` +- `VERCEL_PROJECT_ID` + +## 6. 最小部署示例 + +### 6.1 前端最小变量 + +```env +POLYWEATHER_API_BASE_URL=https://your-backend.example.com +NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key +POLYWEATHER_AUTH_ENABLED=true +POLYWEATHER_AUTH_REQUIRED=true +``` + +### 6.2 后端最小变量 + +```env +TELEGRAM_BOT_TOKEN=... +TELEGRAM_CHAT_ID=... +POLYWEATHER_RUNTIME_DATA_DIR=/var/lib/polyweather +POLYWEATHER_DB_PATH=/var/lib/polyweather/polyweather.db +UID=1000 +GID=1000 +POLYWEATHER_AUTH_ENABLED=true +POLYWEATHER_AUTH_REQUIRED=false +SUPABASE_URL=https://your-project.supabase.co +SUPABASE_ANON_KEY=... +SUPABASE_SERVICE_ROLE_KEY=... +POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN=... +``` + +说明: + +- `UID` / `GID` 主要给 Linux Docker 主机用,避免容器把运行文件写成 root 所有。 +- Windows / macOS 一般可以直接保留默认值。 +- `POLYWEATHER_RUNTIME_DATA_DIR` 建议放在仓库外,例如 `/var/lib/polyweather`。 +- `docker-compose.yml` 会把这个目录同时挂载到容器内的 `/var/lib/polyweather` 和 `/app/data`,兼容现有缓存与 SQLite 路径。 + +## 7. 当前建议的运维规则 + +### 7.1 仓库中允许存在 + +- `.env.example` +- `.env.secrets.example` +- `frontend/.env.example` + +### 7.2 仓库中不应提交 + +- `.env` +- `.env.local` +- 任何带真实 token / key 的配置文件 + +### 7.3 截图与共享规则 + +以下值一旦出现在截图或聊天里,建议视为泄露并轮换: + +- `SUPABASE_SERVICE_ROLE_KEY` +- `POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN` +- `TELEGRAM_BOT_TOKEN` +- 第三方私有 API Key + +## 8. 如何收口配置复杂度 + +如果你觉得变量仍然太多,正确的做法不是一刀删掉,而是: + +1. 把“功能开关”和“调优参数”分开看 +2. 保持 `.env.example` 中: + - 最小启动项 + - 常用功能开关 + - 默认调优值 +3. 让不常改的高阶参数继续留默认 + +也就是说: + +- 使用者只需要先关心 10-20 个关键变量 +- 其余变量保持默认即可 + +## 9. 推荐的下一步 + +当前已经完成: + +1. 根 `.env.example` 收口 +2. `.env.secrets.example` 新增 +3. 本文档新增 +4. 运行时配置校验脚本新增 + +## 10. 配置校验命令 + +在不启动服务的情况下,你可以直接检查配置: + +检查 Web: + +```bash +python scripts/validate_runtime_env.py --component web +``` + +检查 Bot: + +```bash +python scripts/validate_runtime_env.py --component bot +``` + +返回规则: + +- 退出码 `0`:当前配置通过 +- 退出码 `1`:当前配置存在关键缺失 + +如果某个功能已启用但缺关键变量,脚本会直接报错。 + +## 11. 推荐的下一步 + +后续最值得继续做的是: + +1. 在 GitHub / Vercel / VPS 三侧固化同一套变量命名 +2. 给生产部署增加一次性配置审计清单 diff --git a/docs/FRONTEND_DEPLOYMENT_ZH.md b/docs/FRONTEND_DEPLOYMENT_ZH.md new file mode 100644 index 00000000..0b3fcfca --- /dev/null +++ b/docs/FRONTEND_DEPLOYMENT_ZH.md @@ -0,0 +1,178 @@ +# 前端部署配置(Vercel) + +本文只覆盖 `frontend` 目录对应的 Next.js 前端部署。 + +## 一、部署目标 + +推荐方案: + +1. GitHub Actions 负责 `CI` +2. Vercel 负责前端 `CD` +3. FastAPI 后端单独部署在 VPS / Docker 主机 + +前端本身不直接访问天气源,而是通过 Next Route Handlers 转发到后端: + +1. 浏览器 -> Vercel 上的 Next.js 前端 +2. Next `/api/*` -> `POLYWEATHER_API_BASE_URL` +3. FastAPI 后端 -> 分析 / 支付 / 鉴权服务 + +## 二、Vercel 项目设置 + +在 Vercel 导入 GitHub 仓库后,使用下面的设置: + +- Framework Preset: `Next.js` +- Root Directory: `frontend` +- Build Command: `npm run build` +- Install Command: `npm install` + +如果仓库已经连接过 Vercel,通常只需要确认 `Root Directory` 仍然是 `frontend`。 + +## 三、最小必填环境变量 + +只部署天气看板和基础登录时,先填下面 4 项: + +```env +POLYWEATHER_API_BASE_URL=https:// +NEXT_PUBLIC_SUPABASE_URL=https://.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY= +POLYWEATHER_AUTH_ENABLED=true +``` + +建议显式补: + +```env +POLYWEATHER_AUTH_REQUIRED=true +``` + +说明: + +- `POLYWEATHER_API_BASE_URL`:前端所有 `/api/*` Route Handler 转发时依赖它,没填会直接返回 500。 +- `NEXT_PUBLIC_SUPABASE_URL` / `NEXT_PUBLIC_SUPABASE_ANON_KEY`:Supabase 客户端依赖它们。 +- `POLYWEATHER_AUTH_ENABLED`:关闭时,前端不会启用登录能力。 +- `POLYWEATHER_AUTH_REQUIRED`:控制 middleware 是否强制登录。 + +## 四、按功能启用的可选环境变量 + +### 1. 分享式看板 + +```env +POLYWEATHER_DASHBOARD_ACCESS_TOKEN= +``` + +设置后,可通过 `/?access_token=` 打开带令牌的看板入口。 + +### 2. 前后端 entitlement 校验 + +```env +POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN= +``` + +仅当后端开启 entitlement / 订阅校验时需要。 + +### 3. 钱包支付 + +```env +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= +NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com +``` + +如果不启用钱包支付,可以留空。 + +### 4. Telegram 入口 + +```env +NEXT_PUBLIC_TELEGRAM_GROUP_URL=https://t.me/ +NEXT_PUBLIC_TELEGRAM_BOT_URL=https://t.me/WeatherQuant_bot +``` + +只影响按钮跳转,不影响核心页面加载。 + +## 五、推荐的三套配置口径 + +### 1. 公开游客模式 + +```env +POLYWEATHER_API_BASE_URL=https://api.example.com +POLYWEATHER_AUTH_ENABLED=false +POLYWEATHER_AUTH_REQUIRED=false +``` + +适合公开演示站。 + +### 2. 正常登录模式 + +```env +POLYWEATHER_API_BASE_URL=https://api.example.com +NEXT_PUBLIC_SUPABASE_URL=https://.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY= +POLYWEATHER_AUTH_ENABLED=true +POLYWEATHER_AUTH_REQUIRED=true +``` + +适合正式前端站点。 + +### 3. 登录 + entitlement 联动 + +```env +POLYWEATHER_API_BASE_URL=https://api.example.com +NEXT_PUBLIC_SUPABASE_URL=https://.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY= +POLYWEATHER_AUTH_ENABLED=true +POLYWEATHER_AUTH_REQUIRED=true +POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN= +``` + +适合前后端都启用了会员/订阅保护的生产环境。 + +## 六、不要放进 Vercel 的变量 + +这些属于后端私密配置,不应该放到前端项目: + +- `SUPABASE_SERVICE_ROLE_KEY` +- `TELEGRAM_BOT_TOKEN` +- `POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN` 以外的后端 secret +- 支付签名私钥 / 交易私钥 / 任何 bot 凭据 + +特别注意: + +- `NEXT_PUBLIC_*` 会暴露给浏览器 +- 只有明确允许前端公开使用的值,才应加 `NEXT_PUBLIC_` + +## 七、上线前检查 + +Vercel 部署前至少确认: + +1. `POLYWEATHER_API_BASE_URL` 指向可访问的后端生产地址 +2. `frontend/.env.example` 和 Vercel Project Settings 中的实际值一致 +3. GitHub Actions 中 `frontend-quality` 已通过 +4. 如果启用鉴权,Supabase redirect URL 已包含前端域名 + +## 八、常见问题 + +### 1. 页面打开后 API 全部 500 + +先检查: + +```env +POLYWEATHER_API_BASE_URL +``` + +这是最常见原因。 + +### 2. Vercel 构建通过,但登录失败 + +先检查: + +- `NEXT_PUBLIC_SUPABASE_URL` +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` +- Supabase 项目里的站点 URL / redirect URL + +### 3. 钱包入口显示未配置 + +先检查: + +```env +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID +``` + +这是钱包连接的必需项。 \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example index b349d2ff..20aaf84e 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,12 +1,33 @@ +# PolyWeather 前端最小配置(本地 / Vercel) +# 只部署天气看板时,先填下面 4 项即可。 + +# 必填:后端 FastAPI 基础地址 POLYWEATHER_API_BASE_URL=http://127.0.0.1:8000 -# Supabase Auth (Google + Email) + +# 必填:Supabase 前端公钥(鉴权开启时必须) NEXT_PUBLIC_SUPABASE_URL= NEXT_PUBLIC_SUPABASE_ANON_KEY= + +# 常用:前端鉴权开关 +# true: 启用 Supabase 登录 +# false: 关闭登录能力,访客模式 POLYWEATHER_AUTH_ENABLED=false -# If true: middleware forces login; if false: optional login mode (guests allowed). + +# 常用:是否强制登录 +# true: middleware 强制登录后才能访问主页面 +# false: 登录可选,访客可浏览 POLYWEATHER_AUTH_REQUIRED=false -# Optional dashboard guard (Next.js middleware) -# If set, open dashboard with: /?access_token= + +# 可选:分享式看板访问令牌 +# 设置后,可通过 /?access_token= 打开受保护看板 POLYWEATHER_DASHBOARD_ACCESS_TOKEN= -# Shared secret forwarded by Next API routes to backend + +# 可选:前端 API Route 转发到后端时附带的共享令牌 +# 仅当后端启用了 entitlement / 订阅校验时需要 POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN= + +# 可选:钱包支付 / Telegram 入口 +NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= +NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com +NEXT_PUBLIC_TELEGRAM_GROUP_URL=https://t.me/your_group +NEXT_PUBLIC_TELEGRAM_BOT_URL=https://t.me/WeatherQuant_bot \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md index b615da02..29820d4d 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -28,30 +28,53 @@ npm install npm run dev ``` -## 必需环境变量 +## Vercel 最小部署配置 + +只跑看板和基础鉴权时,先填这 4 项: ```env POLYWEATHER_API_BASE_URL=https:// -NEXT_PUBLIC_SUPABASE_URL= -NEXT_PUBLIC_SUPABASE_ANON_KEY= +NEXT_PUBLIC_SUPABASE_URL=https://.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY= POLYWEATHER_AUTH_ENABLED=true -POLYWEATHER_AUTH_REQUIRED=false -POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN= ``` -WalletConnect: +建议显式补: ```env +POLYWEATHER_AUTH_REQUIRED=true +``` + +如果你只是开放游客浏览,可改成: + +```env +POLYWEATHER_AUTH_ENABLED=false +POLYWEATHER_AUTH_REQUIRED=false +``` + +## 可选环境变量 + +仅在对应功能启用时填写: + +```env +# 看板分享令牌 +POLYWEATHER_DASHBOARD_ACCESS_TOKEN= + +# 前端 API 转发到后端时使用的共享令牌 +POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN= + +# 钱包支付 NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID= NEXT_PUBLIC_WALLETCONNECT_POLYGON_RPC_URL=https://polygon-bor-rpc.publicnode.com -``` -浮层链接: - -```env +# 社群入口 NEXT_PUBLIC_TELEGRAM_GROUP_URL=https://t.me/ +NEXT_PUBLIC_TELEGRAM_BOT_URL=https://t.me/WeatherQuant_bot ``` +更完整的 Vercel 配置说明见: +- [docs/FRONTEND_DEPLOYMENT_ZH.md](/E:/web/PolyWeather/docs/FRONTEND_DEPLOYMENT_ZH.md) + ## 路由处理器 天气: @@ -90,4 +113,4 @@ NEXT_PUBLIC_TELEGRAM_GROUP_URL=https://t.me/ 详见根目录策略文档:`docs/OPEN_CORE_POLICY.md` -最后更新:`2026-03-14` +最后更新:`2026-03-20` \ No newline at end of file diff --git a/scripts/validate_runtime_env.py b/scripts/validate_runtime_env.py new file mode 100644 index 00000000..25992f01 --- /dev/null +++ b/scripts/validate_runtime_env.py @@ -0,0 +1,28 @@ +import argparse +import json +import os +import sys + +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +if PROJECT_ROOT not in sys.path: + sys.path.insert(0, PROJECT_ROOT) + +from src.utils.config_validation import validate_runtime_env # noqa: E402 + + +def main(): + parser = argparse.ArgumentParser(description="Validate PolyWeather runtime environment variables.") + parser.add_argument( + "--component", + choices=["bot", "web"], + default="web", + ) + args = parser.parse_args() + + report = validate_runtime_env(args.component) + print(json.dumps(report.as_dict(), ensure_ascii=False, indent=2)) + raise SystemExit(0 if report.ok else 1) + + +if __name__ == "__main__": + main() diff --git a/src/bot/orchestrator.py b/src/bot/orchestrator.py index 42ad378b..5e8a111f 100644 --- a/src/bot/orchestrator.py +++ b/src/bot/orchestrator.py @@ -16,6 +16,7 @@ from src.bot.io_layer import BotIOLayer from src.bot.runtime_coordinator import StartupCoordinator from src.bot.services.city_command_service import CityCommandService from src.bot.services.deb_command_service import DebCommandService +from src.utils.config_validation import validate_or_raise from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env @@ -58,6 +59,7 @@ def start_bot() -> None: from src.database.db_manager import DBManager from src.utils.config_loader import load_config + validate_or_raise("bot") config = load_config() token = os.getenv("TELEGRAM_BOT_TOKEN") if not token: diff --git a/src/utils/config_validation.py b/src/utils/config_validation.py new file mode 100644 index 00000000..b68eb372 --- /dev/null +++ b/src/utils/config_validation.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import os +from dataclasses import dataclass, field +from typing import Dict, Iterable, List + +from dotenv import load_dotenv + + +TRUTHY = {"1", "true", "yes", "on"} + + +def _env(name: str) -> str: + return str(os.getenv(name) or "").strip() + + +def _env_bool(name: str, default: bool = False) -> bool: + raw = _env(name) + if not raw: + return default + return raw.lower() in TRUTHY + + +def _has(name: str) -> bool: + value = _env(name) + if not value: + return False + lowered = value.lower() + return "your_" not in lowered and lowered not in {"changeme", "example"} + + +@dataclass +class ConfigValidationReport: + component: str + errors: List[str] = field(default_factory=list) + warnings: List[str] = field(default_factory=list) + + @property + def ok(self) -> bool: + return not self.errors + + def as_dict(self) -> Dict[str, object]: + return { + "component": self.component, + "ok": self.ok, + "errors": list(self.errors), + "warnings": list(self.warnings), + } + + +def _missing(required_keys: Iterable[str]) -> List[str]: + return [key for key in required_keys if not _has(key)] + + +def validate_runtime_env( + component: str, + *, + load_env_file: bool = True, +) -> ConfigValidationReport: + if load_env_file: + load_dotenv() + component_key = str(component or "").strip().lower() + report = ConfigValidationReport(component=component_key or "unknown") + + auth_enabled = _env_bool("POLYWEATHER_AUTH_ENABLED", False) + auth_required = _env_bool("POLYWEATHER_AUTH_REQUIRED", auth_enabled) + auth_require_subscription = _env_bool( + "POLYWEATHER_AUTH_REQUIRE_SUBSCRIPTION", + False, + ) + entitlement_guard = _env_bool("POLYWEATHER_REQUIRE_ENTITLEMENT", False) + payment_enabled = _env_bool("POLYWEATHER_PAYMENT_ENABLED", False) + weekly_reward_enabled = _env_bool("POLYWEATHER_WEEKLY_REWARD_ENABLED", False) + wallet_activity_enabled = _env_bool("POLYMARKET_WALLET_ACTIVITY_ENABLED", False) + polygon_watch_enabled = _env_bool("POLYGON_WALLET_WATCH_ENABLED", False) + + if component_key == "bot": + missing = _missing(["TELEGRAM_BOT_TOKEN"]) + if missing: + report.errors.append(f"Bot 启动缺少必填变量: {', '.join(missing)}") + if not (_has("TELEGRAM_CHAT_ID") or _has("TELEGRAM_CHAT_IDS")): + report.warnings.append("未配置 TELEGRAM_CHAT_ID / TELEGRAM_CHAT_IDS,机器人推送目标为空") + + if auth_enabled: + missing = _missing(["SUPABASE_URL", "SUPABASE_ANON_KEY"]) + if missing: + report.errors.append(f"已启用鉴权,但缺少变量: {', '.join(missing)}") + if auth_required or auth_require_subscription or weekly_reward_enabled: + missing = _missing(["SUPABASE_SERVICE_ROLE_KEY"]) + if missing: + report.errors.append(f"当前鉴权/订阅能力需要变量: {', '.join(missing)}") + + if entitlement_guard: + missing = _missing(["POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN"]) + if missing: + report.errors.append(f"已启用 backend entitlement guard,但缺少变量: {', '.join(missing)}") + + if payment_enabled: + payment_missing = _missing(["POLYWEATHER_PAYMENT_RPC_URL"]) + if payment_missing: + report.errors.append(f"已启用支付,但缺少变量: {', '.join(payment_missing)}") + has_receiver = _has("POLYWEATHER_PAYMENT_RECEIVER_CONTRACT") + has_tokens_json = _has("POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON") + if not (has_receiver or has_tokens_json): + report.errors.append( + "已启用支付,但未配置 POLYWEATHER_PAYMENT_RECEIVER_CONTRACT 或 POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON" + ) + + if wallet_activity_enabled: + if not _has("POLYMARKET_WALLET_ACTIVITY_USERS"): + report.warnings.append("已启用 wallet activity watcher,但未配置 POLYMARKET_WALLET_ACTIVITY_USERS") + + if polygon_watch_enabled: + if not _has("POLYGON_WALLET_WATCH_ADDRESSES"): + report.warnings.append("已启用 polygon watcher,但未配置 POLYGON_WALLET_WATCH_ADDRESSES") + + if component_key == "web": + if auth_enabled and not auth_required: + report.warnings.append("当前 Web 鉴权为 optional 模式,未强制登录") + + return report + + +def validate_or_raise(component: str) -> ConfigValidationReport: + report = validate_runtime_env(component, load_env_file=True) + if report.errors: + raise RuntimeError(" | ".join(report.errors)) + return report diff --git a/tests/test_config_validation.py b/tests/test_config_validation.py new file mode 100644 index 00000000..0453ca2d --- /dev/null +++ b/tests/test_config_validation.py @@ -0,0 +1,31 @@ +from src.utils.config_validation import validate_runtime_env + + +def test_validate_runtime_env_bot_requires_telegram_token(monkeypatch): + monkeypatch.delenv("TELEGRAM_BOT_TOKEN", raising=False) + report = validate_runtime_env("bot", load_env_file=False) + assert not report.ok + assert any("TELEGRAM_BOT_TOKEN" in err for err in report.errors) + + +def test_validate_runtime_env_web_auth_requires_supabase(monkeypatch): + monkeypatch.setenv("POLYWEATHER_AUTH_ENABLED", "true") + monkeypatch.delenv("SUPABASE_URL", raising=False) + monkeypatch.delenv("SUPABASE_ANON_KEY", raising=False) + + report = validate_runtime_env("web", load_env_file=False) + + assert not report.ok + assert any("SUPABASE_URL" in err for err in report.errors) + + +def test_validate_runtime_env_payment_requires_receiver_or_tokens(monkeypatch): + monkeypatch.setenv("POLYWEATHER_PAYMENT_ENABLED", "true") + monkeypatch.setenv("POLYWEATHER_PAYMENT_RPC_URL", "https://polygon-rpc.com") + monkeypatch.delenv("POLYWEATHER_PAYMENT_RECEIVER_CONTRACT", raising=False) + monkeypatch.delenv("POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON", raising=False) + + report = validate_runtime_env("web", load_env_file=False) + + assert not report.ok + assert any("POLYWEATHER_PAYMENT_RECEIVER_CONTRACT" in err for err in report.errors) diff --git a/web/core.py b/web/core.py index 7c32d531..09eab165 100644 --- a/web/core.py +++ b/web/core.py @@ -11,6 +11,7 @@ from pydantic import BaseModel, Field from loguru import logger from src.utils.config_loader import load_config +from src.utils.config_validation import validate_runtime_env from src.data_collection.weather_sources import WeatherDataCollector from src.data_collection.city_risk_profiles import CITY_RISK_PROFILES # noqa: F401 from src.data_collection.polymarket_readonly import PolymarketReadOnlyLayer @@ -34,6 +35,11 @@ app.add_middleware( ) _config = load_config() +_config_validation = validate_runtime_env("web") +for _warning in _config_validation.warnings: + logger.warning(f"[config:web] {_warning}") +if _config_validation.errors: + raise RuntimeError(" | ".join(_config_validation.errors)) _weather = WeatherDataCollector(_config) _market_layer = PolymarketReadOnlyLayer() _account_db = DBManager()