Add config validation and deployment docs

This commit is contained in:
2569718930@qq.com
2026-03-20 22:00:02 +08:00
parent 3196552c78
commit f6771247a6
13 changed files with 849 additions and 102 deletions
+79 -85
View File
@@ -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=
+44
View File
@@ -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=
+2
View File
@@ -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)
+5 -1
View File
@@ -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}"
+286
View File
@@ -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. 给生产部署增加一次性配置审计清单
+178
View File
@@ -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://<your-fastapi-host>
NEXT_PUBLIC_SUPABASE_URL=https://<your-project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your-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=<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/<your_group>
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://<project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<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://<project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key>
POLYWEATHER_AUTH_ENABLED=true
POLYWEATHER_AUTH_REQUIRED=true
POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN=<shared-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
```
这是钱包连接的必需项。
+26 -5
View File
@@ -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=<token>
# 可选:分享式看板访问令牌
# 设置后,可通过 /?access_token=<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
+34 -11
View File
@@ -28,30 +28,53 @@ npm install
npm run dev
```
## 必需环境变量
## Vercel 最小部署配置
只跑看板和基础鉴权时,先填这 4 项:
```env
POLYWEATHER_API_BASE_URL=https://<your-fastapi-host>
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
NEXT_PUBLIC_SUPABASE_URL=https://<your-supabase-project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your-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/<your_group>
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/<your_group>
详见根目录策略文档:`docs/OPEN_CORE_POLICY.md`
最后更新:`2026-03-14`
最后更新:`2026-03-20`
+28
View File
@@ -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()
+2
View File
@@ -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:
+128
View File
@@ -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
+31
View File
@@ -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)
+6
View File
@@ -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()