Skip to main content
Glama
README.md
# 佳明中国区训练数据 → 本地 SQLite → AI 直接分析

把佳明手表的数据变成 AI 能读的数据库,然后在 WorkBuddy / Claude 里用人话提问:「最近四周负荷是不是加太快了」「昨晚睡得怎么样」。

**中国区账号(connect.garmin.cn)可以直接用,不需要换区、不需要跨区桥接工具。**

```
手表 ──蓝牙──> 手机 Garmin Connect ──> connect.garmin.cn
                                          │  garminconnect + is_cn=True
                                          ▼
                              sync.py 增量同步(按需触发)
                                          ▼
                                  data/garmin.db (SQLite)
                                          ▼
                              mcp_server.py(本地 MCP 服务,9 个工具)
                                          ▼
                              WorkBuddy / Claude Desktop 对话
```

数据全程只在本机。密码只在首次登录用一次,之后走 token,不上传任何第三方。

---

## 一、这件事为什么值得做

佳明官方的数据出口有三个问题:

| 渠道 | 问题 |
|---|---|
| Garmin Connect 网页 | 图表浅,只能看单次,做不了趋势对比和自定义统计 |
| 官方 Health / Training API | 需要公司身份申请商业审批,个人基本拿不到 |
| 第三方平台(Strava / Runalyze / Intervals.icu) | **中国区账号拿不到授权**,绑上也读不到数据 |

而中国区账号的唯一官方客户端 `connect.garmin.cn` 又是封闭的。所以路子只剩一条:**在本机模拟网页登录,把数据拉下来自己存。**

好消息是 `garminconnect` 这个库支持中国区,只差一个参数。

---

## 二、和其它方案比

| 方案 | 中国区可用 | 历史数据 | 能否被 AI 读 | 成本 |
|---|---|---|---|---|
| 官方全量导出 FIT | ✅ | 全部 | ❌ 要自己解析 | 免费 |
| GarminDB | ✅ | 全部 | 部分 | 免费 |
| 托管 MCP(AthleteData / stats.training) | ❌ 需国际区 | 30 天 | ✅ | $9-10/月 |
| **本项目** | ✅ | 全部 | ✅ | 免费 |

本项目 ≈ `garminconnect` + SQLite + 一个 MCP 服务,一共约 1400 行 Python。

---

## 三、快速开始

### 1. 环境

```bash
python -m venv .venv
.venv/bin/pip install -r requirements.txt      # Windows: .venv\Scripts\pip.exe
```

### 2. 登录(唯一需要手动的一步)

```bash
# Windows
.venv\Scripts\python.exe sync.py --login
# macOS / Linux
.venv/bin/python sync.py --login
```

输入**佳明中国区**账号(邮箱或手机号)和密码,开了双重验证会要求输验证码。

成功后 token 存在 `data/tokens/`,之后再也不会问你要密码。

> 密码只在这一次登录里使用,不落盘。如果脚本在非交互环境跑(比如被 AI 调用),会直接读 `GARMIN_EMAIL` / `GARMIN_PASSWORD` 环境变量。

### 3. 拉历史

```bash
.venv\Scripts\python.exe sync.py --full
```

活动默认一路往回翻到最早一条;健康数据每天要 5 个请求,默认只拉 30 天(想补更早的用 `--health-days 90`)。

### 4. 接到 AI

MCP 服务配置写进 `~/.workbuddy/mcp.json`(Claude Desktop 是 `claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "garmin-cn": {
      "command": "/绝对路径/到本项目/.venv/bin/python",
      "args": ["/绝对路径/到本项目/mcp_server.py"],
      "env": { "PYTHONUTF8": "1" }
    }
  }
}
```

> Windows 下把 `command` 换成 `D:/你的路径/.venv/Scripts/python.exe`,路径用正斜杠可以省掉转义。

> **写完必须完全重启客户端**(不是关窗口,是从托盘退出)。本地 MCP 不会出现在「连接器市场」里,它在设置页的 MCP Servers 区块,需要手动信任一次。

---

## 四、四个文件在干什么

### `store.py` — 数据落地

四张表,全部保留原始 JSON,方便以后解析新字段而不用重新拉数据:

| 表 | 内容 |
|---|---|
| `activities` | 活动列表:距离、时长、配速、心率、爬升、训练效应、VO2max |
| `splits` | 单次活动的分段明细(按需拉取,只拉看过的) |
| `daily` | 每日健康:静息心率、HRV、睡眠分期、Body Battery、准备度、负荷 |
| `meta` | 键值对,主要存 `last_sync` |

```python
conn = sqlite3.connect(DB_PATH)
conn.execute("PRAGMA journal_mode=WAL")   # 关键:同步写入时读不阻塞
```

### `sync.py` — 增量同步

三条原则:

1. **token 优先**。有 token 就直接用,绝不再问密码
2. **只补缺的**。先查库里已有的日期,只拉缺口
3. **慢一点**。每次请求间隔 0.8 秒,遇 429 指数退避

```python
client = Garmin(email, password, is_cn=True)   # ← 中国区唯一的开关
client.login(str(TOKENS))                      # 有 token 走这条
```

### `mcp_server.py` — 把库暴露成工具

9 个工具,AI 按需调用:

| 工具 | 作用 |
|---|---|
| `db_info` | 库状态:多少条、数据到哪天、上次同步 |
| `sync_now` | 立刻增量同步 |
| `list_activities` | 最近 N 次训练清单,可按运动类型筛 |
| `activity_detail` | 单次训练分段明细(首次调用才去拉) |
| `daily_health` | 静息心率 / HRV / 睡眠 / Body Battery / 负荷 |
| `training_summary` | **主入口**:按周汇总 + 负荷比 + 配速心率漂移 |
| `sleep_report` | 多日睡眠:明细、睡眠债、作息规律性、红旗 |
| `morning_briefing` | 早起汇报:昨晚恢复 + 今日建议 |
| `query_sql` | 只读 SQL,自定义统计 |

**设计上最有用的一个决定**:查询类工具在开始前会自动检查 `last_sync`——

- 距上次同步 < 6 小时 → 直接读本地,秒回
- 超过 6 小时 → 先增量同步一次再回答
- 同步失败 → 不报错,用旧数据回答 + 开头加一句提示

所以**不需要任何定时任务**。你什么时候问,它就什么时候保证数据是新的。

### `briefing.py` — 口语化的恢复汇报

早上想看「昨晚睡得怎么样」,最没用的做法是把一堆字段推给你。这个文件把数字翻译成人话:

```
昨晚 7 小时 30 分,比你这周的平均(6.8 小时)好一点。够数了。
00:39 躺下、08:12 起,这个入睡时间算你近期偏早的。
夜间 HRV 152,比你 28 天的基线(132)高了一截,恢复是往上走的。
今天准备度 91,是最近少见的高位。昨天没怎么动,别浪费。
```

每个维度分 4-6 档(睡眠 5 档、准备度 4 档、HRV 6 档),所以连着看一周每天说的话不一样,不是填空模板。

---

## 五、踩过的坑(这部分最有价值)

### 1. 中国区只差一个参数

```python
Garmin(email, password, is_cn=True)
```

设了 `is_cn=True`,登录域名就切到 `connect.garmin.cn`;否则一律打到国际站,中国区账号必然失败。

也可以用环境变量 `GARMIN_IS_CN=1`。

### 2. token 优先于密码,顺序反了就会卡住

第一版写成这样:

```python
email = os.getenv("GARMIN_EMAIL") or input("账号: ")      # ← 先问密码
password = os.getenv("GARMIN_PASSWORD") or getpass()
has_token = tokens_exist()                                # ← 才看 token
```

结果明明已经登录过,脚本还是停下来等输入,在非交互环境直接挂死。

正确顺序:**先看有没有 token,有就直接 `login(token_dir)`,失败了才回退到账号密码。**

### 3. MCP Python SDK 2.x 换了 API

网上绝大多数教程写的是:

```python
from mcp.server.fastmcp import FastMCP     # 2.x 里已不存在
mcp = FastMCP("garmin-cn")
```

2.x 里改成了:

```python
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("garmin-cn")
```

装之前先 `pip show mcp` 看版本,否则会对着不存在的模块调试半天。

### 4. 日志绝对不能打 stdout

MCP 走 stdio 上的 JSON-RPC,**任何一行 `print()` 都会污染协议**,客户端表现为「服务连接失败」但看不到任何报错。

```python
print(line, file=sys.stderr, flush=True)   # 所有日志都走 stderr
```

### 5. Windows 下中文乱码

MCP 服务的 env 里加 `PYTHONUTF8=1`(或 `PYTHONIOENCODING=utf-8`),否则返回的中文全是问号。

### 6. SQLite 并发锁

同步正在写的时候 AI 来读,会直接 `database is locked`。开 WAL 就好:

```sql
PRAGMA journal_mode=WAL;
```

实测写入事务未提交时读照样成功。

### 7. 限流是真的

佳明对频繁登录很敏感。登录成功一次就要把 token 落盘,所有后续同步复用 token。请求之间留 0.8 秒,遇 429 指数退避。

**别把同步设成几分钟一次**,这个接口扛不住。

### 8. 健康数据 = 一天 5 个请求

活动列表一次能翻很多条,但每日健康是拼出来的(睡眠、HRV、准备度、Body Battery、训练状态各一个请求)。补 90 天 ≈ 450 次请求,大概 5-8 分钟,中间容易撞限流。所以默认只拉 30 天,按需往前补。

### 9. 睡眠时间戳是 UTC,不是本地时间

`sleepStartTimestampLocal` 这个名字极具误导性——它给你的是 **UTC 毫秒**。直接 `fromtimestamp()` 会差 8 小时,算出来的「入睡时间」完全是错的。

```python
t = dt.datetime.fromtimestamp(ms / 1000, dt.timezone.utc)   # 必须指定 UTC
```

### 10. 国行字段比想象中全

网上说法是国行缺训练准备度、HRV 状态这些。实测(Forerunner 265 + 中国区)**17 个字段几乎全满**,训练准备度、急性/慢性负荷、HRV 状态都在。VO2max 只在部分活动中产生,属于正常。

所以别预设「国行数据不全」,先拉一遍看实际填充率再决定分析口径。

### 11. MCP 配置写完要完全重启

不是关窗口。Windows 上点 × 只是缩到托盘,配置根本没重读。要从托盘图标右键退出,再打开。

---

## 六、数据安全

- 密码只在首次登录时用一次,不写进任何文件
- token 存在 `data/tokens/`,`.gitignore` 已排除
- 数据库 `data/garmin.db` 含全部原始 JSON,同样已排除
- 所有请求直连 `connect.garmin.cn`,不经过任何中转服务器

`.gitignore` 的核心几条:

```
data/
.env
.venv/
__pycache__/
```

**注意**:`data/` 目录里同时有 token 和你的全部训练记录,分享 `garmin.db` 前记得先清空。

---

## 七、能问什么

信任 MCP 之后直接说人话,AI 会自己调工具取数:

```
看看我最近四周,负荷是不是加太快了
昨晚睡得怎么样
这周三的间歇跑,配速有没有掉
我 HRV 连着一周在降,是不是该减量
下个月有半马,基于我现在数据给个三周安排
我这一年哪几个月跑量掉得最厉害
```

比通用建议准的地方在于:**它看的是你的真实数据**,不是你描述的数据。

---

## 八、局限

1. **库会失效**。`garminconnect` 靠模拟网页登录,佳明一改版就可能断。社区通常几天内修好,`pip install -U garminconnect` 升级即可。数据在本地,不会丢。
2. **强度分布是估算的**。库里存的是活动平均心率,不是逐秒心率区间。要精确的 Z1-Z5 占比,得额外拉每次活动的 `hr_in_timezones`。
3. **更早的历史需要官方导出兜底**。部分接口只回补最近 30 天。

---

## License

MIT