内容
# 健康管理工具
*fettle(n.)— 状况,状态: "in fine fettle."*
拥有您的 Fitbit / Pixel 健康数据。fettle 将其从 **Google Health API** 同步到本地 SQLite 文件中,计算"高级"指标的透明版本(准备度,睡眠评分,训练负荷),并在上面添加仪表盘,洞察引擎和 **零成本 AI 助手** — 所有这些都在您的机器上运行,您的数据不会离开,除非与 Google 通信。
> Google Health API 取代了旧的 Fitbit Web API(2026 年 9 月完全关闭)。
> 该项目是为 **个人,单用户** 使用而构建的:您的 OAuth 同意屏幕在 *测试* 模式下运行,您是唯一的测试用户,因此没有第三方安全审查,也没有服务器组件。
## 截图
所有截图都显示应用程序运行在真实的同步数据上。
**概览** — 今日准备度评分,其组件驱动因素,28 天历史和目标状态:

**AI 助手** — 答案包括实时内联小部件。此处的一个问题会产生一个指标历史图表,一个统计瓷砖和一个同行基准带,每个都由前端渲染和获取:

**工具协调和每日简报** — 后续问题继续会话。工具芯片显示每个引擎调用的双轴比较,回复框架将相关性(`r = -0.50`)作为关联,而不是原因。右侧:每次同步后由引擎计算的证据生成的简报:
<p>
<img src="docs/screenshots/coach-mid.png" width="49.5%" alt="教练比较图表与工具芯片" />
<img src="docs/screenshots/insights.png" width="49.5%" alt="LLM 每日简报" />
</p>
**指标钻取和命令调色板** — 每个指标的统计数据(7/28 天平均值,范围,个人最佳)适用于每个同步类型,以及 ⌘K 模糊搜索所有 40 个注册指标,搜索结果中带有实时火花线:
<p>
<img src="docs/screenshots/drilldown.png" width="49.5%" alt="指标钻取抽屉" />
<img src="docs/screenshots/palette.png" width="49.5%" alt="命令调色板" />
</p>
**锻炼钻取和教练在一个会话** — 任何会话都扩展为其心率跟踪与 %HRmax 时间区域;询问一个锻炼,教练拉取会话日志,在内联渲染当日跟踪,并将记住的上下文(前面聊天中提到的伤害)折叠到建议中:
<p>
<img src="docs/screenshots/workout-drilldown.png" width="49.5%" alt="扩展的工作锻炼会话与心率跟踪和区域" />
<img src="docs/screenshots/coach-intraday.png" width="49.5%" alt="教练分析一个会话与日内小部件" />
</p>
**教练记忆和每周回顾** — 记忆面板列出教练保存的每个持久事实,每个都可以删除;周日结束一周,并附有书面回顾:
<p>
<img src="docs/screenshots/coach-memory.png" width="49.5%" alt="教练记忆面板" />
<img src="docs/screenshots/weekly-retro.png" width="49.5%" alt="每周回顾简报" />
</p>
**睡眠分析和同行基准** — 阶段混合与已发布的目标,14晚睡眠债务和一致性;带有下一个阈值注释的参考带和每个引用的值:
<p>
<img src="docs/screenshots/sleep.png" width="49.5%" alt="睡眠深度潜水" />
<img src="docs/screenshots/standing.png" width="49.5%" alt="同行基准" />
</p>
**锻炼和目标** — 每周量,30 天活动混合和每个会话日志;目标卡片带有连胜和 28 天遵守情况,按状态排序:
<p>
<img src="docs/screenshots/workouts.png" width="49.5%" alt="锻炼视图" />
<img src="docs/screenshots/goals.png" width="49.5%" alt="目标视图" />
</p>
**浅色主题** — 跟随系统设置,具有手动覆盖:
<p>
<img src="docs/screenshots/metrics-light.png" width="49.5%" alt="浅色主题下的指标" />
<img src="docs/screenshots/overview-light.png" width="49.5%" alt="浅色主题下的概览" />
</p>
## 内部
- **同步引擎** — 30 多种 Google Health 数据类型的增量类型水印:每日汇总,当日样本(分钟级心率,SpO2,HRV)和完整的睡眠/锻炼会话,包括每个锻炼的详细信息。
- **仪表盘** (Next.js)— 概览包含准备度英雄和您的目标,然后是洞察,睡眠深度潜水,锻炼,同行基准(Standing)和每个同步类型的指标钻取。浅色/深色主题,⌘K 命令调色板和可深链接状态(`?v=` 视图,`?m=` 指标抽屉,`/coach?c=` 对话,`?theme=` 覆盖)。
- **派生指标,公开公式** — 准备度(0–100 恢复指数与您自己的 28 天基线),睡眠评分,TRIMP 式心血管负荷。每个阈值和权重追踪到 [`docs/health-metrics-spec.md`](docs/health-metrics-spec.md) 中的引用。
- **洞察引擎** — 确定性检测器:趋势,z 分数异常,ACWR 训练负荷平衡,14晚睡眠债务,斯皮尔曼相关性(诚实地表示为关联),目标连胜和仅在 ≥2 个生命体征同时漂移时触发的生命体征早期警告。
- **锻炼钻取** — 每个会话行扩展为其日内心率跟踪,%HRmax 时间区域和与您该项活动的中间会话的比较。
- **AI 助手** (`/coach`)— 类似 ChatGPT 的聊天,基于 **您** 的数据:会话历史,附件,模型选择,流式回复与 **内联生成小部件** (图表,比较,内联跟踪,准备度环,睡眠阶段,基准带,目标)。教练可以创建,更新和删除您的目标 — 并 **记住**:您提到的持久事实(伤害,计划,事件)被保存,在每次对话开始时调用,并在您可以在任何时候修剪的记忆面板中列出。
- **每日简报 + 每周回顾** — 每次同步后,分析师模型将当天的计算证据转化为早晨阅读(标题,叙述,3–5 张卡片,每个数字可追溯)。昨天的简报作为上下文继续,因此叙述继续而不是重新开始。周日添加每周回顾:发生了什么,哪些目标发生变化,下周需要解决一个问题。
- **通知** — 预定同步后,仅当需要您关注时才会触发 macOS 通知:7 天令牌即将过期,多个生命体征同时漂移或目标连胜被破坏。
## AI 层的工作原理
```
Next.js 聊天界面 ──SSE──▶ FastAPI /api/chat ──子进程──▶ opencode 运行(免费 Zen 模型)
│ MCP(stdio)
▼
backend/mcp_server.py — 25 个类型化工具
│
▼
SQLite + 确定性分析引擎
```
- 应用程序 **从不持有 LLM API 密钥**。它调用您已经登录的 opencode CLI,使用 opencode Zen 的免费模型,因此没有每次对话的费用。
- `backend/mcp_server.py` 将分析引擎公开为 **25 个 MCP 工具**(12 个读取,5 个写入 — 目标和记忆 — 和 8 个显示)。指标参数是封闭枚举,从数据类型注册表中生成,因此模型 *无法* 胡乱编造指标名称。
- **LLM 协调和叙述;它从不进行数学运算**。趋势,异常,相关性和分数都来自确定性引擎 — 模型的工作是调用正确的工具并解释结果。
- 显示工具(`show_chart`,`show_readiness`,...)仅返回确认;SSE 桥将它们转换为小部件事件,前端实时渲染 Recharts 组件,恰好在模型调用的位置。
- 简报的想法是颠倒的:引擎计算证据包,工具无关的分析代理返回严格的 JSON,后端验证它(真实的指标名称,封顶的卡片)并按证据摘要缓存它,因此不变的数据永远不会重新生成。
## 安装
前提:一个迁移到 Google 的 Fitbit 帐户(自 2026-05-19 起强制),数据流入 Google Health / Fitbit 应用程序,以及 Python 3.11+ 和 Node 18+。
### 快速入门
```bash
git clone https://github.com/Deekshith-Dade/fettle.git && cd fettle
ops/bootstrap.sh # venv + 所有依赖(安全重新运行)
ops/dev.sh # 后端:8400 + 仪表盘:3400 — Ctrl-C 停止两者
```
打开 **http://localhost:3400**。在全新安装时,仪表盘打开为 **设置向导**,引导您完成一次性的 Google 部分 — 创建您自己的免费云项目,铸造 OAuth 客户端,连接,首次同步 — 并实时检查每个步骤。大约十分钟。
> **为什么是自己的 Google Cloud 项目?** 每个 Google Health API 范围都是 **受限的**:将一个共享应用程序交付给公众需要 Google 的 OAuth 验证和年度付费安全评估。个人应用程序使用控制台的 **测试** 通道 — 您自己的项目,您自己的密钥,您是唯一的用户。您的数据从 Google 直接流到您的机器;没有其他人的服务器或 OAuth 客户端参与。
> ⚠️ **一个重复的琐事:** 测试模式的同意屏幕每 **7 天** 过期一次。仪表盘在顶部栏中倒数,简报在到期前提醒您,并且重新连接是一个单击的握手 — 无需重新勾选。预定同步以代码 2 退出时,日志中意味着 7 天令牌已死亡。
### 向导引导您完成的操作
对于好奇的人 — 或厌恶向导的人;每个步骤也适用于无头模式:
1. **创建 Google Cloud 项目** (免费,任意名称)并在其上启用 Google Health API。
2. **OAuth 同意屏幕**:用户类型 *外部*,发布状态留为 **测试**;添加您自己的 Gmail 作为 **测试用户**;在 **数据访问**下,添加四个 `googlehealth.*.readonly` 范围(活动,健康指标,睡眠,营养 — 向导有一个复制按钮)。
3. **创建 OAuth 客户端** (类型 *Web 应用程序*),其唯一授权重定向 URI 为 `http://localhost:8400/auth/callback`,并下载其 JSON。
4. **移交 JSON** — 粘贴或拖入向导,验证(如果重定向 URI 看起来不正确,则发出警告),存储为 `backend/credentials.json`(gitignored)。
5. **连接 Google** — 一个同意屏幕;勾选每个范围框。
6. **首次同步** — ~90 天的日常记录 + 最近的全分辨率日内记录,然后直接进入仪表盘。
终端等效:`cd backend && .venv/bin/python cli.py auth` 然后 `cli.py sync`;`cli.py status` 显示每个类型的水印,`cli.py sync steps sleep` 同步特定类型。移动机器?复制 `backend/credentials.json` + `backend/token.json` 跨机器并跳到同步。
### 手动运行(dev.sh 执行的操作)
```bash
cd backend && .venv/bin/python -m uvicorn app.main:app --reload --host :: --port 8400
cd frontend && npm run dev -- -p 3400
```
`--host ::` 绑定 IPv4 + IPv6。没有它,uvicorn 是 IPv4 独有的,Safari — 解析 `localhost` 为 ::1 首先 — 加载仪表盘但永远不会填充。端口和来源现在都正确默认为;`backend/.env.example` 和 `frontend/.env.example` 记录了您需要的覆盖项。
### 4. AI 助手(可选 — 其他功能无需它即可正常工作)
```bash
# 安装 opencode 并一次性登录(免费的 opencode Zen 层就足够了):
curl -fsSL https://opencode.ai/install | bash # 或:brew install sst/tap/opencode
opencode auth login
# MCP 服务器需要自己的 venv — `mcp` 包的依赖项(较新的 starlette/pydantic)与固定的 FastAPI 冲突。不要在主后端 venv 中安装 mcp。
cd backend
python3 -m venv .venv-mcp
.venv-mcp/bin/pip install mcp pydantic-settings
```
无需其他配置:`opencode.json` (存储库根目录)使用存储库相对路径注册 MCP 服务器,后端始终从存储库根目录启动 opencode。(手动运行 opencode?请从存储库根目录运行,以便路径解析。)代理角色位于 `.opencode/agent/` (`fettle-coach` 用于聊天,`fettle-analyst` 用于简报);两者默认为免费模型,后端在免费模型阵容轮换时自动回落。
### 预定同步 + 通知(可选)
```bash
ops/install-sync.sh # 为此存储库生成 + 加载 6 小时一次的 launchd 同步
```
每 6 小时同步一次。每次运行还会刷新每日简报(和周日回顾),然后在需要关注时发送 macOS 通知:令牌即将过期,多生命体征同时漂移或目标连胜被破坏。
日志位于 `~/Library/Logs/fettle-sync.log`; 日志中的退出代码 2 表示 7 天令牌已死亡 — 从仪表盘重新连接。随时使用 `launchctl bootout gui/$(id -u)/com.fettle.sync` 删除作业。
### 从手机访问(可选)
将机器添加到 [Tailscale](https://tailscale.com) 尾网(`brew install --cask tailscale-app`,登录并使用相同的帐户在手机上安装 Tailscale 应用程序)。然后从任何地方打开 `http://<machine-name>.<tailnet>.ts.net:3400` — 仪表盘从其 API 基数派生,后端接受私有尾网/局域网来源(`cors_origin_regex` 在 `backend/app/config.py`)。无需公开互联网;这是您自己的设备之间的 WireGuard。
一个注意事项:Google OAuth 重定向 URI 是 `localhost`,因此 **连接/重新认证** 发生在机器本身上 — 在那里进行每周握手;阅读和教练适用于任何设备。
## 存储库地图
```
backend/
app/
config.py 设置 + 数据类型注册表(唯一真相来源)
auth.py Google OAuth 流程,令牌存储,自动刷新
health_client.py Google Health API 上的薄客户端(列表 + dailyRollUp)
store.py SQLite 架构,插入,查询助手
sync.py 增量同步引擎 + 派生指标处理器
readiness.py 0–100 恢复指数 vs 您自己的 28 天基线
insights.py 确定性检测器(趋势,异常,ACWR,相关性...)
sleep_analysis.py 阶段混合与目标,债务,一致性
benchmarks.py 同行规范带(“站立”)
goals.py 目标 CRUD + 遵守评估
coach.py 确定性日计划建议
briefing.py 证据包 → 分析师模型 → 每日简报 + 每周回顾
workouts.py 会话详细信息:日内心率跟踪 + 时间区域
notify.py 同步后 macOS 警报(令牌 / 生命体征 / 连胜破坏)
chat.py SSE 桥:/api/chat ↔ opencode CLI(工具 → 小部件)
chat_store.py 会话 + 消息持久性
mcp_server.py 25 个 MCP 工具,教练模型调用
cli.py auth / sync / status 命令
frontend/
app/page.tsx 仪表盘(所有视图)
app/coach/page.tsx 教练聊天页面
components/ 聊天界面,生成小部件,洞察视图,⌘K 调色板
docs/
health-metrics-spec.md 每个公式和阈值的引用证据库
ops/
bootstrap.sh 从全新克隆到可运行的应用程序
dev.sh 启动两个服务器(后端:8400,仪表盘:3400)
install-sync.sh 生成 + 加载此存储库的 6 小时同步
```## 注意事项
- **Safari 显示空仪表盘** → 使用 `--host ::` 启动 uvicorn(双栈)。Chrome 静默回落到 IPv4 并隐藏问题。
- **永远不要在主后端 venv 中 `pip install mcp`** — 它将 starlette 升级到 FastAPI 支持的版本之外。那就是 `.venv-mcp` 存在的原因。
- **免费模型阵容轮换**(“限时 beta”) — 后端将配置的模型与实际可用性解析并顺利回落。
- **7 天令牌** — 测试模式的同意屏幕每周硬过期令牌。重新认证需要 ~20 秒;简报的第一张卡片在剩余 ≤2 天时提醒您。