# Garmin Health Lab — 项目指南 佳明(Garmin)健康数据分析平台:同步 Garmin Connect 数据,做多维度健康分析、 可视化与 AI 解读(晨报 / 趋势归因 / Copilot)。 > **阅读顺序**:先读本文件(协作约定与现状速览)→ [PROGRESS.md](PROGRESS.md) > (功能与部署进度,**排查前必读**)→ [README.md](README.md)(API 与使用)→ > [docs/](docs/)(架构 / 开发 / 需求)。判断"部署在哪里、跑没跑、什么版本"时 > 一律以 `PROGRESS.md` 和 `deploy/` 脚本为准,**不要凭记忆断言**。 ## 技术栈(现状,2026-09) - **后端**:Python 3.10+ / Flask(应用工厂),Gunicorn 生产运行。原 Node/Express 后端早已重写为 Python,仓库中**不存在 `server/` 目录**。 - **前端**:React 18 + TypeScript,用 **CRA(react-scripts)**构建,UI 组件库为 **Framework7 9**(`framework7-react`,iOS 主题)+ Recharts 图表。根目录 `package.json` 是 npm workspaces(含 `client`)。 - **数据库**:可插拔数据层(`backend/db.py`),`DB_TYPE` 切换—— 本地开发 SQLite(`backend/data/health.db`,默认);生产 MariaDB(甲骨文云主机 本机,`garmin_health_lab` 库,连接配置见该机 `/opt/garmin-health-lab/backend/.env`)。 - **认证**:**auth-hub SSO**(OAuth2/OIDC,`services/auth_hub_client.py`), 本地邮箱密码注册/登录已移除;应用内 JWT 由后端签发。 - **AI**:自建 ai-gateway(OpenAI 兼容,`https://ai.zichuan.xyz/v1`)为唯一上游 (NVIDIA 推理模型,单次生成可达 2~5 分钟);生产 `AI_MODEL_CHAIN=gateway`。 ## 目录结构 ``` GarminHealthLab/ ├── backend/ # Flask 后端(当前唯一后端) │ ├── app.py # 应用工厂:装配蓝图 + 静态 UI + scheduler/jobs │ ├── wsgi.py # Gunicorn 入口 │ ├── config.py # 集中配置(读 backend/.env) │ ├── auth.py # JWT 签发/校验 + require_auth │ ├── db.py # 可插拔数据层(SQLite ↔ MariaDB)+ 幂等迁移 │ ├── routes/ # API 蓝图(blueprint) │ │ ├── auth.py # /api/auth auth-hub SSO: callback / logout / refresh / auth-hub/start │ │ ├── garmin.py # /api/garmin 绑定/同步/MFA/退出/活动详情/同步详情 │ │ ├── health.py # /api/health 摘要/睡眠/心率/运动/身体成分/血氧/设备/挑战等 │ │ ├── analysis.py # /api/analysis 趋势/建议/briefing/trend-insight/copilot/insight │ │ └── settings.py # /api/settings 个人资料/单位/同步频率/评分依据 │ ├── services/ # 业务逻辑(纯函数/无 Flask 依赖,可单测) │ │ ├── garmin.py # Garmin API 同步核心(限流退避 24h 写 DB) │ │ ├── garmin_auth.py # Garmin OAuth 登录 + MFA 会话 │ │ ├── garmin_extras.py # 身体成分/血压/血氧/设备/徽章等扩展数据 │ │ ├── scheduler.py # 后台自动同步(DB 锁,多 worker 只跑一次) │ │ ├── ai.py / coach.py / insights.py / jobs.py # AI 教练 + 生产者/消费者队列 │ │ ├── analysis.py / health.py / fitness_age.py / settings.py │ │ └── auth_hub_client.py / scopes.py │ ├── data/ # SQLite(本地开发,git 忽略) │ ├── static/ # 前端构建产物(生产由 Flask 同端口提供) │ ├── tests/ # pytest 测试(446+ 项) │ └── .env.example # 环境变量样例(真实 .env 不提交) ├── client/ # React 前端(CRA + Framework7) │ ├── src/pages/ # 今日/健康/趋势/运动/设置/每日/睡眠/指标详情等页面 │ ├── src/components/ # AI 晨报 / Copilot 浮窗 / 趋势归因 / Screen 骨架 │ ├── src/lib/ # day.ts(本地日期)/ metrics.ts(指标注册表) │ ├── src/features.ts # 功能开关(FEATURES.ai) │ └── .env.production # REACT_APP_API_URL=/api(同源,防 Network Error) ├── deploy/ # push_oracle.sh 现役;其余(S99garmin/start/stop/ │ # push/deploy)是 NAS 时代脚本,已废弃保留参考 ├── docs/ # 架构 / 开发 / 需求文档 ├── PROGRESS.md # 进度与部署事实(排查必读) └── README.md # 项目说明 + API 文档 ``` ## 常用命令 ```bash # 后端依赖(首次):在 backend/ 下建 .venv 并装 requirements-dev.txt npm run setup:backend # 同时起前后端开发(backend:5000 由 BACKEND_PORT 固定;client CRA 代理到 5000) npm run dev # 单独起后端 / 前端 / 测试 / 类型检查 / 构建 npm run dev:backend npm run dev:client npm run test # backend/.venv/bin/python -m pytest(446+ 项) npm run typecheck npm run build # client/react-scripts build → client/build/ # 冒烟测试(后端单测入口) cd backend && .venv/bin/python tests/smoke.py ``` ## 部署(生产 = 甲骨文云主机,2026-09-12 起;此前是 NAS,已下线) - **位置**:`129.146.26.249` `/opt/garmin-health-lab`,**`ubuntu` 用户**跑 gunicorn `127.0.0.1:5500`(2 workers / 4 threads / --timeout 300), systemd 单元 `garmin-health-lab.service`(`enabled`,随机器开机自启, `Restart=always`)。和这台机器上其它服务(ai-gateway / auth-hub / fam-edge) 同一套约定:`/opt/<项目>` 下独立部署 + 独立 venv + Caddy 按子域名反代。 - **公网**:`https://garmin.zichuan.xyz` → Caddy → `127.0.0.1:5500`。 DNS(腾讯云 DNSPod,A 记录)与 TLS(Caddy 自动签发)都已配好。 ~~旧的 `http://129.146.26.249:8124`~~ 已下线(走 NAS frpc 转发,隧道已拆)。 - **一键部署**:本地 `./deploy/push_oracle.sh`(tar 经 ssh key 认证同步 backend + 清空重推 client/build + pip install + systemctl restart, **校验 main PID 变化 + health 200**)。前置:先 `npm run build`。 `deploy/push.sh` / `start.sh` / `stop.sh` / `S99garmin.sh` / `deploy.sh` 是 NAS 时代的脚本,已废弃保留仅供参考。 - **DB**:甲骨文本机 MariaDB 10.3.39(`127.0.0.1:3306`),独立账号 `garmin`(**注意**:`garmin@localhost` 与 `garmin@127.0.0.1` 是两个不同账号, 必须同密码建两份,否则 TCP 连接用的是另一个密码),库 `garmin_health_lab`。 和其它项目(`chat_relay`、`zhongyuan`)共用同一个 MariaDB 实例,各自独立库 独立账号。**这台机器磁盘 96% 已满**,改动前留意剩余空间。 - **AI 网关**:`AI_GATEWAY_BASE_URL` 现在是**本地回环** `http://127.0.0.1:5100/v1` (不再经 Caddy/公网 —— 网关和这个服务同机了)。 - **auth-hub**:client `996aLPw4T5gl-rYZ`;回调只保留 `https://garmin.zichuan.xyz/auth/callback` 一条(NAS/旧公网端口那几条已用 `manage_clients remove-redirect-uri` 清掉)。改注册用 `/opt/auth-hub` 下 `PYTHONPATH=/opt/auth-hub/src venv/bin/python -m auth_hub.manage_clients`。 - **NAS 现状**:`garmin_health_lab` 库已 `DROP DATABASE`(备份在本地 `~/Desktop/Work/backups/garmin_health_lab_nas_backup_20260912.sql.gz`, 20 张表逐条精确计数核对过一致后才删的),`S99garmin.sh` 开机项已移除, frpc 配置里 `garmin-health` 转发段已删(`gitea`/`wordpress`/`fam-core`/ `nexusai` 那几条没动——**Gitea 还在 NAS 上,这个仓库的 git remote 仍然指 向它**,这次迁移只搬了应用和数据,没搬源码托管)。 - **部署/排障前**:先读 `PROGRESS.md` 与 `deploy/` 脚本确认事实(曾经凭旧记忆 断言"无线上环境"而误判,后来又把"生产在 NAS"当成默认事实——**两次都错在 没有先查,部署位置是会变的**)。 ## 关键约定与坑(写代码/改样式前看) 1. **日期一律本地日历日期**,别用 `toISOString()`(UTC,UTC+8 每天前 8 小时 查的是昨天)。前端统一走 `client/src/lib/day.ts`。 2. **阈值全部来自公开参考值,AI 只解读不定阈值**(设计原则,勿破)。 3. **Framework7 全局 `button { width: 100% }`** 会把任何 `