Files
mi-fitness-health/docs/PROJECT_DOCUMENTATION.md
T
binglua 2dd32f1ee6 feat: REST API with API keys, QR login, and web dashboard (v0.2.0)
- REST API (FastAPI): all data endpoints, sync (foreground/background),
  coverage stats, interactive docs at /docs
- API key system: exchange Xiaomi credentials for mif_sk_* keys
  (multi-account contexts, last-used tracking, prefix revocation);
  static key via MI_FITNESS_API_KEY, admin endpoints via MI_FITNESS_ADMIN_KEY
- QR code login: reverse-engineered Xiaomi universal QR flow
  (sid=xiaomiio), auto-issues API key after scan confirmation
- Built-in web dashboard (Flask): visual console for all endpoints,
  key management, QR login; reverse proxy with server-side key relay
- Standalone testpage/ console (python app.py, no install needed)
- Fix: pin mcp<2 (SDK 2.x removed Server.list_tools, broke startup)
- CI: install [all,dev] extras; docs: full technical documentation
  and QR login implementation guide
2026-09-03 22:28:31 +08:00

53 KiB
Raw Blame History

mi-fitness-mcp-cn 项目完整技术文档

文档目的:作为重构/二次开发的唯一权威参考。内容基于源码逐行整理(基线 commit 7fcd069),所有行为描述均与代码实际实现一一对应,不包含猜测。 阅读对象:执行重构的 AI 或工程师。重构约束与已知问题见第 15、16 章,动手前必读。


目录

  1. 项目定位与背景
  2. 快速事实卡
  3. 总体架构
  4. 目录结构与代码地图
  5. 配置层 config.py
  6. 凭据与认证 auth
  7. 数据模型 models
  8. 小米健康云协议(逆向核心)
  9. 适配器层 mi_fitness_cloud.py
  10. 同步服务 sync_service.py
  11. 存储层 storage
  12. 查询服务 query_service.py
  13. MCP Server server.py
  14. CLI、辅助脚本与测试
  15. 已知问题清单(重构目标)
  16. 重构方案建议
  17. 本地环境备注

1. 项目定位与背景

mi-fitness-mcp-cn 是一个本地运行的 Python MCP(Model Context Protocol)Server,用于把用户自己的「小米运动健康」App 云端数据同步到本地 SQLite,并通过 MCP 工具供 AI 客户端(如 Claude Desktop)查询。

  • 改造自 kubulashvili/mi-fitness-mcp(国际区),本 fork 的增量:
    1. 支持中国区云端(--region cn,API host hlth.io.mi.com,时区 UTC+8);
    2. 新增睡眠(sleep)与运动记录(workouts)同步;
    3. 额外逆向并接入 4 个数据 key:resting_heart_rate、spo2、stress、abnormal_heart_beat;
    4. CLI 输出与注释中文化。
  • 非官方项目,接口全部为逆向所得,无任何官方保证,随时可能因小米改版而失效。
  • 仅用于读取分析用户自己的健康数据。MIT 协议。

2. 快速事实卡

项 值
语言 / 最低版本 Python ≥ 3.11(用了 datetime.UTC)
核心依赖 mcp(必须 <2,2.x 移除了 Server.list_tools)、httpx、pydantic ≥2、platformdirs、keyring、click(声明了但未使用)、rich(声明了但未使用)
构建 hatchling;入口 mi-fitness-mcp = mi_fitness_mcp.cli_mi_fitness:main
源码规模 src 约 3800 行 + 测试 370 行 + 独立探测脚本 268 行
异步模型 全 asyncio(httpx.AsyncClient + MCP stdio);但 DB 层是同步 sqlite3
配置文件 <user_config_dir>/mi-fitness-mcp/config.json(Windows: C:\Users\<u>\AppData\Local\mi-fitness-mcp\mi-fitness-mcp\config.json)
凭据存储 OS keyring,服务名 mi-fitness-mcp,账号 mi_fitness_auth_user_id / mi_fitness_auth_pass_token
数据库 <user_data_dir>/mi-fitness-mcp/mi_fitness.db(Windows 同上双目录),SQLite,8 张业务表 + 1 张同步状态表
认证方式 小米账号 Cookie 中的 userId + passToken(用户手工从浏览器获取),换取 serviceToken + ssecurity
区域 已知 ru, cn, de, i2, sg, us;cn 用 hlth.io.mi.com + UTC+8,其余用 {region}.hlth.io.mi.com + UTC

3. 总体架构

                       ┌──────────────────────────────────────────────┐
                       │                MCP 客户端 (AI)                │
                       └───────────────▲──────────────────────────────┘
                                       │ stdio (JSON-RPC)
┌──────────────────────────────────────┴──────────────────────────────┐
│  cli_mi_fitness.py ──► main.py (argparse CLI: serve/setup/doctor/sync)
│         serve ─────────────► server.py  (mcp.server.Server, 模块级单例)│
│                                  │ list_tools / call_tool            │
│         sync ──┐                 ▼                                   │
│                │      ┌─────────────────────┐  ┌──────────────────┐ │
│                └─────►│   SyncService       │  │   QueryService   │ │
│                       │  分块/增量/去重     │  │  聚合/过滤(内存) │ │
│                       └─────────┬───────────┘  └────────┬─────────┘ │
│                                 ▼                       ▼           │
│                       ┌──────────────────────────────────┐          │
│                       │   Database (sqlite3, 同步)        │          │
│                       └──────────────────────────────────┘          │
└─────────────▲───────────────────────────────────────────────────────┘
              │ AsyncIterator[Pydantic Model]
│  ┌──────────┴───────────────────────┐
│  │ MiFitnessCloudAdapter            │
│  │  登录换取 serviceToken/ssecurity │
│  │  RC4 加密 + SHA1 签名            │
│  │  分页拉取 + 解析为模型            │
│  └──────────┬───────────────────────┘
└─────────────┼───────────────────────────────────────────────────────┘
              │ HTTPS (加密表单)
   ┌──────────┴──────────────────┐        ┌─────────────────────────┐
   │ account.xiaomi.com 登录      │        │ hlth.io.mi.com 数据接口  │
   └─────────────────────────────┘        └─────────────────────────┘

分层职责一句话:

  • 协议层(adapter 内嵌):登录、加密签名、分页、把云端 JSON 解析成 Pydantic 模型。
  • 服务层:SyncService 负责「按 7 天分块拉取→落库→记录增量游标」;QueryService 负责从本地库读出后做内存聚合/过滤。
  • 接口层:server.py 暴露 14 个 MCP 工具;main.py 暴露 4 个 CLI 子命令。
  • 存储层:sqlite3 同步连接,UPSERT 写入,字符串日期过滤查询。

4. 目录结构与代码地图

mi-fitness-mcp-cn/
├── pyproject.toml              # 构建/依赖/工具链(mcp>=1.0.0,<2 为本地修复,上游仍是 >=1.0.0)
├── probe_mifitness.py          # 独立交互式探测脚本:复制了一份登录+加密代码(268 行,代码重复)
├── .env.example                # 声明了 MI_FITNESS_USER_ID/PASS_TOKEN/REGION —— 但代码里【没有任何 env 加载逻辑】
├── mcp-server-config.example.json
├── docs/PROJECT_DOCUMENTATION.md  # 本文档
├── tests/                      # pytest + pytest-asyncio(auto) + respx
│   ├── test_config.py          # 14 行:配置存取 roundtrip
│   ├── test_mi_fitness_cloud.py# 102 行:适配器纯函数(解析/时区/分页辅助)
│   ├── test_server_sync.py     # 77 行:server 同步编排(mock adapter)
│   ├── test_storage_query.py   # 47 行:入库/查询
│   └── test_sync_service.py    # 130 行:分块、增量、异常→partial
└── src/mi_fitness_mcp/
    ├── __init__.py             # 3 行
    ├── cli_mi_fitness.py       # 5 行:entry point 转发器 → main.main
    ├── main.py                 # 193 行:argparse CLI(serve/setup/doctor/sync)⚠ 名字叫 main 实为 CLI
    ├── server.py               # 657 行:MCP Server(tools + handlers + 后台同步 + 生命周期)
    ├── config.py               # 76 行:Config 模型 + JSON 读写
    ├── auth/__init__.py        # 45 行:keyring 存取 userId/passToken
    ├── models/__init__.py      # 190 行:全部 Pydantic 模型
    ├── adapters/
    │   ├── base.py             # 116 行:DataAdapter 抽象基类(8 个 iter_* + 4 个基础方法)
    │   └── mi_fitness_cloud.py # 805 行:★核心★ 协议实现 + 8 类数据解析器
    ├── services/
    │   ├── sync_service.py     # 290 行:分块增量同步引擎
    │   └── query_service.py    # 366 行:读侧聚合/过滤
    └── storage/__init__.py     # 878 行:Database 类(建表/UPSERT/查询/覆盖统计)

模块依赖方向(重构时保持或理顺):cli → server/main、server → services + adapter + storage + auth + config、services → adapter + storage、adapter → models + base、storage → models。无循环依赖。

5. 配置层

Config(pydantic BaseModel)字段全表,含实际使用状态:

字段 类型/默认 实际用途 状态
mode Literal["mi_fitness_cloud","not_configured"] = not_configured 判定是否已配置 使用中
region str = "ru" API host 与时区选择 使用中;⚠ 默认值与 README 推荐的 cn 不一致
timezone str = "UTC" 仅 get_profile 返回值里出现 准死代码
database_path Path = platformdirs DB 位置 使用中
logs_path Path = platformdirs —— 完全未使用(无 logging 配置代码)
auto_sync_on_start bool = True —— 未使用(server 启动明确不连接不同步)
stale_after_minutes int = 60 —— 未使用
store_raw_payloads bool = True —— 未使用(原始报文从未落库)
default_lookback_days 30 (1–3650) 未给 start_date 时的回溯天数 使用中
sync_chunk_days 7 (1–90) 分块大小 使用中
http_timeout_seconds 20.0 (0–120] httpx 超时 使用中
request_retries 3 (1–10] 请求重试次数 使用中
health_check_timeout_seconds 10.0 状态工具的健康检查 wait_for 使用中
sync_type_timeout_seconds 180.0 单类型同步 asyncio.wait_for 上限 使用中(仅 MCP 路径;CLI 路径无超时)
max_pages 200 (1–5000) 单 key 分页安全上限 使用中
  • 存取:load_config() 无文件则生成默认配置并立即写盘;save_config() 把 Path 序列化为字符串。
  • 路径:platformdirs.user_config_dir("mi-fitness-mcp") / user_data_dir(...) / user_log_dir(...)。Windows 实测为 AppData\Local\mi-fitness-mcp\mi-fitness-mcp\(appauthor 与 appname 双层目录)。
  • ⚠ .env.example 是孤儿文件:代码不读取任何环境变量。

6. 凭据与认证

auth/__init__.py,基于系统 keyring:

  • save_mi_fitness_token(user_id, pass_token):写两条密码项,服务名 mi-fitness-mcp,账号名 mi_fitness_auth_user_id / mi_fitness_auth_pass_token。
  • load_mi_fitness_token():读取,任何异常吞掉并返回 (None, None)。
  • delete_mi_fitness_token():删除。没有任何 CLI/MCP 命令调用它(无 logout 功能)。
  • README 提示:无系统 keyring 时可装 keyrings.alt(可能明文存盘)。

7. 数据模型

models/__init__.py。所有业务实体继承 BaseEntity:

公共字段 说明
id 确定性主键,格式 mi_fitness_{类别}_{业务键}(详见 §11 UPSERT)
provider 恒为 mi_fitness
source_type 恒为 cloud_session
source_record_id 云端记录 time 字符串,可空
user_id / device_id device_id 恒为 None(云端未解析设备)
timezone 记录级时区名(zone_name),默认 UTC
collected_at 记录时间(带记录自身 offset)
created_at/updated_at 模型层默认 now(UTC) 去掉 tzinfo;落库时并不写入这两列

业务实体:

模型 关键字段 备注
DailyActivity date(YYYY-MM-DD)、steps、distance_m、active_kcal;可选 total_kcal/floors/active_minutes 云端只解析前 3+2 项
SleepSession sleep_id、start_at/end_at、duration_minutes、time_asleep/time_awake_minutes、sleep_score(0-100)、is_nap、stages: list[SleepStage] stages 落库为 JSON 字符串
SleepStage stage ∈ {deep,light,rem,awake}、minutes state 映射见 §9.4
Workout workout_id、activity_type、start_at/end_at、duration_minutes、距离/卡路里/平均最高心率/配速/步数 可选字段 0 值会被转 None
BodyMeasurement timestamp、weight_kg(必填>0)、bmi/体脂/肌肉/水分/骨量/内脏脂肪/基础代谢/代谢年龄
HeartRateSample timestamp、bpm、sample_type ∈ {resting,active,passive,workout} workout 枚举永远不会被赋值
SpO2Sample timestamp、spo2_pct(0-100)
StressSample timestamp、stress_score(0-100)、level ∈ {low,medium,high} level 阈值 <30 low,<60 medium,否则 high
AbnormalHeartBeatEvent event_id、start_at/end_at、duration_seconds

响应/辅助模型:

  • QueryResponse:所有 MCP 查询工具的统一信封 {status, source, generated_at, timezone, data, error}。
  • ConnectionStatus:连接状态工具用(代码里还手工 merge 了 connection_state/region/last_health_check_at/last_connection_error/sync_in_progress 等额外键)。
  • SyncResult、UserProfile、DeviceInfo、DataCoverage:已定义但基本未使用(SyncResult 的字段被 server 手工拼 dict;UserProfile 仅部分字段复用;DataCoverage.gaps_detected 从未计算)。

8. 小米健康云协议

本章是本项目最有价值的资产,重构时协议行为必须保持逐字节兼容,否则登录/签名会失效。

8.1 登录(passToken → serviceToken + ssecurity)

GET https://account.xiaomi.com/pass/serviceLogin?_json=true&sid=miothealth
Cookie: userId={user_id}; passToken={pass_token}
  • 响应体以 &&&START&&& 前缀开头,去掉前缀后为 JSON,关键字段:
    • passToken(新 token,登录成功后回写覆盖 self.pass_token)
    • userId(回写,转 str)
    • ssecurity(base64,解码后保存为 bytes,用于后续签名)
    • location(重定向 URL)
  • 随后 GET {location},收集响应的 set-cookie(取每条的 k=v 部分),拼接为会话 Cookie(含 serviceToken 等)。
  • ⚠ httpx.AsyncClient(follow_redirects=False):location 是手工 GET 的;该响应的 cookie 不会自动进 cookie jar,而是拼成字符串 self._cookies 手工携带。

8.2 请求加密与签名(RC4 + 双签名)

对数据端点的每次 POST:

  1. 明文表单:data = json.dumps(payload, separators=(",", ":"))(紧凑 JSON)。
  2. nonce = os.urandom(8) + struct.pack(">I", unix_ts // 60)(12 字节,后 4 字节是分钟级时间戳,大端)。
  3. signed_nonce = sha256(ssecurity + nonce)(32 字节原始摘要)。
  4. RC4 加密:以 signed_nonce 为 key,KSA 标准初始化后 PRGA 先丢弃 1024 字节,再异或明文。
  5. 签名函数 _gen_signature(method, path, values, signed_nonce): base = method + "&" + path + "&data=" + values["data"](若 values 里有 rc4_hash__ 则追加 &rc4_hash__=...)+ "&" + base64(signed_nonce);结果 base64(sha1(base))。
  6. 组装最终表单(全部再 urlencode):
    • data:RC4(明文 JSON)
    • rc4_hash__:对明文表单计算的签名
    • signature:对密文表单(含 data、rc4_hash__)计算的签名
    • _nonce:base64(nonce)
  7. POST {base_url}{api_path},Content-Type: application/x-www-form-urlencoded,Cookie: {会话cookie}。
  8. 响应体是 base64 文本,用 signed_nonce RC4 解密得 JSON:{code, message, result}。
    • code != 0 → 抛错;code ∈ {401,403,-6,-10001} 或 message 命中 auth 标记(unauthorized / not logged in / session expired / invalid pass token / login required / ...,大小写折叠匹配)→ 判定为认证失败。

8.3 端点与分页

端点 用途 请求 payload 响应关键字段
POST /app/v1/data/get_fitness_data_by_time 健康数据(steps/calories/heart_rate/resting_heart_rate/weight/sleep/spo2/stress/abnormal_heart_beat) {start_time, end_time, key, next_key?}(epoch 秒) data_list[]、has_more、next_key
POST /app/v1/data/get_sport_records_by_time 运动记录 {start_time, end_time, limit: 50, next_key?} sport_records[]、has_more、next_key
  • host:region 为 cn 或空 → https://hlth.io.mi.com;否则 https://{region}.hlth.io.mi.com。
  • 分页:循环携带 next_key 直到 has_more 为假;用 seen_keys 集合检测游标成环(报 pagination cursor loop detected);页数超过 max_pages(默认 200)报 pagination exceeded safety limit。
  • 列表项公共字段:time(epoch 秒,记录时间)、zone_offset(秒)、zone_name(如 Asia/Shanghai)、sid(设备/来源 ID)、value(JSON 字符串或 dict)。
  • 时间换算:请求范围由「日期字符串 + 区域时区」决定——cn/空 → UTC+8,其他 → UTC;end_date 取到当天 23:59:59。

8.4 各数据 key 的载荷格式与字段映射

value 内字段 → 模型字段(括号内为备选字段名):

steps / calories(daily_activity 两次请求)

  • steps:{steps, distance, calories} → 按天累加 steps/distance_m/active_kcal(初值)
  • calories:{calories} → 按天累加后整体覆盖该日的 active_kcal
  • 日归属:以记录 time + zone_offset 格式化为 YYYY-MM-DD

sleep

  • 入睡:bedtime(备选 device_bedtime、bed_timestamp)
  • 醒来:wake_up_time(备选 device_wake_up_time、out_bed_timestamp、兜底 time)
  • duration(分钟;缺省用 end-start);清醒 awake_duration(备选 sleep_awake_duration);入睡分钟 = duration - awake
  • score(备选 sleep_score)→ sleep_score;is_nap
  • 分段 items[]:{start_time, end_time, state},state 映射:1→deep, 2→light, 3→light, 4→awake, 5→rem,其他→light;分钟数=(end-start)//60,0 分钟丢弃
  • sleep_id = f"{sid or user_id}_{item.time or int(睡眠结束ts)}"

heart_rate

  • {type, bpm}:type==0 → sample_type="passive",否则 "active"
  • resting_heart_rate(独立 key):{date_time, bpm} → sample_type="resting";时间戳优先 date_time

weight(body_measurements)

  • weight;bmi;body_fat_rate→body_fat_pct;muscle_rate→muscle_mass_kg;moisture_rate→water_pct;bone_mass;visceral_fat→visceral_fat_score;basal_metabolism→basal_metabolism_kcal;body_age→metabolic_age
  • id:mi_fitness_weight_{item.time}

workouts(sport 端点)

  • start_time(兜底 time)、end_time(缺省 = start+duration 秒)、duration(秒)
  • distance、calories(备选 total_cal)、avg_hrm、max_hrm、avg_pace、max_pace、steps(备选 total_steps)、sport_type
  • activity_type = item.category or item.key or sport_type or "workout"
  • workout_id = f"{sid or user_id}_{item.key or 'workout'}_{item.time or start_ts}"

spo2:{time, spo2 | value};timestamp 优先 value 内 time stress:{time, stress | score | value};level 按分数分段 abnormal_heart_beat:{start_time, end_time};event_id = f"{sid or user_id}_{int(start_ts)}"

数值清洗:_optional_float/_optional_int 把 0 值转 None(缺省语义与“真实 0”被合并,见 §15-I6)。

8.5 错误处理与重试(_request 循环)

  • 重试 request_retries(默认 3)次,指数退避 min(4s, 0.5*2^attempt) + rand(0..0.1)。
  • 可重试:httpx 超时/网络错误;HTTP 429、≥500。
  • 认证失败(401/403 或业务码/message 命中):_connected=False,若还有重试余量且持有凭据 → 用 passToken 重新登录并继续重试;重新登录失败 → 立即终止。
  • 其他业务错误:立即终止。
  • 最终失败统一抛 RuntimeError("Mi Fitness request failed: ...")(认证错误原样抛 MiFitnessAuthenticationError)。
  • ⚠ 全部错误被包装后,上层无法再区分认证失败与普通失败(MiFitnessAuthenticationError 在 _request 末尾被重新抛出,这一条保留;但 connect() 会把一切折叠成 False + 字符串 last_error)。

8.6 区域发现与时区

  • _discover_region(preferred):对候选区域依次试拉 weight/steps/heart_rate(固定区间 2025-04-01~2025-05-31),有数据即选中。当前永远不会被调用(region 恒为非空,见 §9.1 注释)——准死代码。
  • _request_timezone:cn/空 → UTC+8;其余 → UTC。Config.timezone 字段不参与此逻辑。

9. 适配器层

adapters/base.py:DataAdapter(ABC) 抽象 4 个基础方法(connect/is_connected/get_user_id/get_available_data_types)+ 8 个 iter_*(同步或异步迭代器二选一,签名返回 Iterator | AsyncIterator 联合类型——重构时建议收敛为纯 async)。

adapters/mi_fitness_cloud.py 实现要点:

  • 懒连接:MCP 启动时只构造不连接(server.main 注释:stdio 必须快速可用);所有工具按需 connect()。connect() 有 asyncio.Lock 防并发,成功后 _discover_data_types() 返回固定 8 类型列表(不再探测,注释说明了原因:固定历史区间探测会静默漏数据)。
  • health_check():真实调用 _fetch_key("steps", today, today)(1 天小请求验证认证+数据通路);瞬时网络失败不会使有效会话失效。
  • iter_* 的共同骨架:if not connected or 没日期: return; yield(空迭代器)→ 拉取 → 解析 yield。注意 return; yield 写法是为了把生成函数变为 async generator 的惯用法。
  • 分页辅助 _fetch_key(fitness)/ _fetch_sport_records_by_time(sport):见 §8.3。
  • _record_datetime:datetime.fromtimestamp(item.time, tz=timezone(item.zone_offset)) —— 所有记录时间都带记录自身的时区偏移。
  • 每次 connect() 都会新建 httpx.AsyncClient 并重登录;close() 关闭并置 _connected=False。
  • 可调参数(由 server 从 Config 注入):http_timeout / request_retries / max_pages。

10. 同步服务

services/sync_service.py。SyncService(adapter, db, default_lookback_days=30, chunk_days=7)。

10.1 并发控制

  • sync_data_type():if self._sync_active or self._sync_lock.locked(): raise RuntimeError → 置 _sync_active=True(无 await 的检查+置位,在单事件循环内原子)→ async with lock 执行 → finally 复位。
  • server.py 另有全局 sync_active 布尔(拒绝并发 sync_data 工具调用)——两把锁叠加。

10.2 单类型同步算法 _sync_data_type_unlocked

  1. 增量游标:非 force_full 时读 sync_state.last_record_timestamp(datetime.fromisoformat 后 replace(tzinfo=None) —— ⚠ 丢偏移量)。
  2. 日期缺省:end_date 默认今天(本地时区 naive);start_date 缺省 = 游标日期(若有)否则 end - (lookback-1) 天。
  3. 校验 start ≤ end,格式必须 YYYY-MM-DD。
  4. 分块循环:以 chunk_days(7)切分区间,逐块调 _sync_range;单块异常 → 记录 chunk error 并立即停止后续块,返回 status=partial(若已有成功块)或 error。
  5. 汇总 {status, data_type, added, updated, skipped, start_date, end_date, chunks[]};每个 chunk 记录 added/updated/skipped。
  6. skipped 恒为 0(计数器从未自增)。

10.3 _sync_range(单块落库)

  • 按 data_type 分派到 8 个 iter_*,逐条 db.insert_*;返回 True 计 added,False 计 updated(实际永远 True,见 §15-I2)。
  • 逐条跟踪 last_ts(各类型用不同字段:activity 用 collected_at、sleep/workout/abnormal 用 start_at、其余用 timestamp)。
  • 块结束时 db.update_sync_state(data_type, last_ts)(仅当 last_ts 非空)。
  • 未知类型抛 ValueError。
  • sync_data_type_sync():asyncio.run 包装的同步版本 —— 在已有事件循环内会炸,当前无人调用(死代码)。

10.4 性能特征(实测)

  • 8 个月全类型同步 ≈ 每类型 ceil(天数/7) 次端点调用(heart_rate 还要 ×2:active+resting keys),单线程串行,总耗时数分钟。首次全量 2026-01-01→08-30 实测约 5–9 分钟;昨天+今天增量 <10 秒。

11. 存储层

storage/__init__.py(878 行)。同步 sqlite3,sqlite3.Row,每次操作独立连接(contextmanager 打开/关闭),每条 INSERT 单独 conn.commit()。

11.1 表结构(8 业务表 + sync_state)

所有业务表公共列:id TEXT PK, provider, source_type, source_record_id, user_id, device_id, timezone, collected_at, created_at, updated_at, …业务列。时间值以 ISO 字符串(带原 offset) 存储。

表 业务列 业务 UNIQUE 约束 UPSERT 冲突目标
daily_activity date, steps, distance_m, active_kcal, total_kcal, floors, active_minutes (user_id, date, device_id) ON CONFLICT(id) → 更新 steps/distance/active_kcal/timezone/collected_at 等
sleep_sessions sleep_id, start_at, end_at, duration_minutes, time_asleep_minutes, time_awake_minutes, sleep_score, is_nap, stages(JSON TEXT) (user_id, sleep_id) ON CONFLICT(user_id, sleep_id) → 不更新 start_at/end_at(⚠ 后端变化不会修正时间)
workouts workout_id, activity_type, start_at, end_at, duration_minutes, distance_m, calories_kcal, avg/max_heart_rate_bpm, avg/max_pace_sec_per_km, total_steps (user_id, workout_id) ON CONFLICT(user_id, workout_id) → 不更新 activity_type/start_at/end_at/pace/steps
body_measurements timestamp, weight_kg, bmi, body_fat_pct, muscle_mass_kg, water_pct, bone_mass_kg, visceral_fat_score, basal_metabolism_kcal, metabolic_age (user_id, timestamp, device_id) ON CONFLICT(id) → 只更新 weight/bmi/body_fat/muscle/water
heart_rate_samples timestamp, bpm, sample_type (user_id, timestamp, sample_type) ON CONFLICT(id) → 全字段更新;⚠ id=mi_fitness_hr_{time} 与业务约束不一致:同秒不同类型 → id 不同但撞业务 UNIQUE → IntegrityError
spo2_samples timestamp, spo2_pct (user_id, timestamp) ON CONFLICT(id)
stress_samples timestamp, stress_score, level (user_id, timestamp) ON CONFLICT(id)
abnormal_heart_beat_events event_id, start_at, end_at, duration_seconds (user_id, event_id) ON CONFLICT(user_id, event_id)
sync_state id PK, data_type UNIQUE, last_sync_at, last_record_timestamp, records_count —— ON CONFLICT(data_type);⚠ records_count 永远是 0(从不更新)

索引:8 个 (user_id, 日期/时间列) 索引。无迁移机制(仅 CREATE TABLE IF NOT EXISTS)。

11.2 关键语义(重构必须知道)

  1. insert 返回值不可信:cursor.rowcount > 0 在 SQLite 的 INSERT … ON CONFLICT DO UPDATE 下插入和更新都返回 1 → added/updated 统计失真(实测重复同步显示「新增 242、更新 0」)。要区分需比较 total_changes() 或先查后写。
  2. 日期过滤是字符串比较:查询用 substr(时间列,1,10) >= ? AND <= ?,比较的是 ISO 字符串前 10 位——即记录自身时区的日期。混合 +08:00 与 Z 记录时,同一“本地日”可能被切错;边界记录(23:xx / 00:xx)可能落入相邻日期桶。
  3. created_at/updated_at 落库靠 SQLite 默认值(CURRENT_TIMESTAMP,UTC),模型字段不传入。
  4. 阻塞风险:所有 DB 调用是同步的,且被 async 服务直接调用(无 to_thread)——大查询会卡 MCP 事件循环。
  5. get_data_coverage():对每表做 MIN/MAX/COUNT(DISTINCT date) 统计;daily 用 date 列,sleep/workouts/measurements/HR 用 substr(...,1,10),spo2/stress/abnormal 用 date(expr)(三种风格并存)。

12. 查询服务

services/query_service.py。QueryService(db, user_id) —— 全部为本地库读后内存加工,不做 SQL 聚合。

方法 行为
get_daily_summaries(start, end) 拉区间内 daily_activity,按日期分组求和(steps/distance/active_kcal/total_kcal/floors/active_minutes)
get_metric_series(metric, start, end, granularity, aggregation) 基于 summaries 取单指标序列;granularity=week/month 时内存聚合(周以周一为起点);metric 枚举里的 weight_kg 永远取不到值(summaries 没有该字段)
get_sleep_sessions(..., include_naps) 过滤 nap;stages 从 JSON 解析;按 start_at 排序
get_workouts(...) 内存过滤 activity_types(lower 比较)/min_duration/min_distance_km
get_body_measurements(...) 只输出非 None 指标;metrics 参数做字段白名单过滤
get_heart_rate_samples(...) 内存过滤 sample_type;limit 在 Python 侧切片(全量拉出后截断,大区间低效)
get_spo2_samples / get_stress_samples / get_abnormal_heart_beat_events 同上模式(stress 支持 level 过滤)
get_data_coverage(data_types?) 转发 db 统计,可按类型过滤

13. MCP Server

server.py。架构特征:模块级全局单例 config/db/adapter/sync_service/query_service/sync_tasks/sync_active,由 main() 初始化。

13.1 生命周期

  • main():加载配置 → 建 Database(建表)→ mode==mi_fitness_cloud 且 keyring 有凭据时构造 adapter 并注入 http_timeout/retries/max_pages → 构造两个 service → stdio_server() 启动。启动时不 connect(保证 MCP 握手不被网络阻塞)。
  • 退出 finally:取消所有后台 sync task(gather 吞异常)→ adapter.close()。
  • ⚠ config 为 None 或 mode=not_configured 时:连接类工具报错,查询类工具走空库(QueryService 依然创建)。

13.2 工具清单(14 个)

工具 必填参数 行为要点
get_connection_status — 同步中→is_connected();否则 health_check()(带 health_check_timeout)。合并 last_sync(8 类型里最新的 sync_state.last_sync_at)、available_types(连接成功用 adapter 的,否则从 sync_state 推断)、附加 connection_state/region/last_health_check_at/last_connection_error/sync_in_progress
sync_data — data_types[](缺省=8 类型全量)、start_date/end_date、force_full_sync、background。空数组报错;未知类型报错。全局 sync_active 防重入
get_sync_status sync_id 查后台任务表(剔除 task 对象)
get_profile — 静态拼装 {user_id, timezone, devices:[]}
get_daily_summary date 或 start_date+end_date date 同时充当 start/end
query_metric_series metric, start_date, end_date metric 枚举 steps/distance_m/active_kcal/weight_kg(weight_kg 死枚举);granularity day/week/month;aggregation sum/avg/min/max/latest(⚠ latest 在服务层落到 else 分支=sum)
query_heart_rate start_date, end_date sample_type 枚举 resting/active/passive/workout;limit
query_body_measurements 同上 metrics[] 白名单;latest_only 取最后一条
query_sleep 同上 include_naps 默认 True
query_workouts 同上 activity_types/min_duration/min_distance_km
query_spo2 / query_stress / query_abnormal_heart_beat 同上 stress 支持 level 枚举过滤
get_data_coverage — data_types[] 可选过滤

完整 inputSchema JSON 见附录 A。响应统一为 [TextContent(json.dumps(result))],异常捕获后返回 {"status":"error","error":str}(不抛给 MCP 层)。

13.3 后台同步(background=true)

  • _prune_sync_tasks():清理已完成任务至多保留 MAX_SYNC_TASKS=100。
  • sync_id=uuid4;task 表条目 {sync_id, status: queued→running→ok/partial/error/cancelled, created_at, started_at, …结果}。
  • 前台/后台共用 _run_sync_data:按需 adapter.connect() → 校验/展开 data_types → 逐类型 asyncio.wait_for(sync_service.sync_data_type(...), sync_type_timeout_seconds)(超时只标记该类型 error,不中断其他类型)→ 汇总 status: ok/partial/error。
  • 取消传播:asyncio.CancelledError 单独处理置 cancelled。
  • ⚠ 任务表是内存 dict:MCP 进程重启即丢失;get_sync_status 对未知 id 返回 error。

14. CLI、辅助脚本与测试

14.1 CLI(main.py;注意:文件名叫 main,职责是 CLI)

子命令 行为
serve(或无参数) asyncio.run(server.main()) —— ⚠ 裸跑 mi-fitness-mcp 会直接启动 MCP server
setup --mode mi_fitness_cloud --user-id --pass-token --region(mode 未给时进入交互式 input 向导);保存 keyring + config.json。⚠ 无任何参数校验(非空校验仅在交互路径)
doctor 打印配置路径/加载配置/读凭据/真实 connect() 测连通(打印区域+8 类型)/检查 DB 文件存在性;配置缺失时 sys.exit(1)
sync --type(单类型)+ --start-date/--end-date(缺省 lookback 30 天);连接失败 exit(1);逐类型打印 新增/更新 条数;没有分块超时,也没有 --force-full 参数

14.2 probe_mifitness.py

独立交互式诊断脚本(268 行):复制粘贴了一份登录/RC4/签名代码(与 adapter 重复),支持对任意 key/区间做原始请求打印,用于逆向调试。重构候选:改为调用 adapter 的导出函数。

14.3 测试(18 个,全部通过)

  • test_config.py:配置 roundtrip。
  • test_mi_fitness_cloud.py:_optional_*、_parse_value(JSON 字符串与 dict 双形态)、_record_datetime zone_offset、分页游标环检测、_is_authentication_error 标记匹配等纯函数;respx mock HTTP。
  • test_sync_service.py:mock adapter 的分块/增量/partial 语义。
  • test_storage_query.py:入库+查询。
  • test_server_sync.py:server 层编排(mock adapter)。
  • 覆盖缺口:CLI 无测试;_request 加密路径无 golden-vector 测试;MCP 工具 handler 仅覆盖 sync 部分。

15. 已知问题清单

分级:P0=破坏正确性/可用性;P1=行为缺陷或明显技术债;P2=清理项。重构时按此核对。

P0

  • I1 mcp 依赖无上界:mcp>=1.0.0 会拉到 2.x,Server.list_tools 不存在 → server 与测试收集直接崩(已在本地 pyproject 修为 >=1.0.0,<2,上游未修,重构时保留上界或迁移到新 API)。
  • I2 added/updated 统计失真:UPSERT 下 cursor.rowcount>0 恒真 → 永远报“新增”(§11.2-1),连带 sync_state.records_count 恒 0。
  • I3 同步游标时区混用:last_record_ts 存的是带 offset 的 ISO 串,续传时 replace(tzinfo=None) 直接剥掉 → 跨时区/夏令时场景增量起点可能偏移数小时(边界记录重复或遗漏)。
  • I4 HR 唯一约束与 id 冲突目标不一致:UNIQUE(user_id, timestamp, sample_type) vs ON CONFLICT(id),同秒双类型样本会抛 IntegrityError 中断整块同步(sync_range 无 per-record 容错)。

P1

  • I5 事件循环阻塞:同步 sqlite3 + 内存聚合直接在 async 上下文执行;大区间 heart_rate 查询(16k+ 行)会冻结 MCP 心跳/其他工具调用。
  • I6 0 值被吞:_optional_float/int(0) → None,把合法 0(如 0 步运动、0 kcal)当缺省丢弃。
  • I7 日期过滤语义:substr(iso,1,10) 字符串比较混合多 offset 记录(§11.2-2),查询/覆盖统计的“日”归属不稳定。
  • I8 死枚举/死指标:query_metric_series.weight_kg 永远无数据;aggregation=latest 落到 sum;sample_type=workout 永不产生;skipped 恒 0。
  • I9 双重并发闸门:server sync_active + SyncService 锁叠加;sync_tasks 内存态不可恢复。
  • I10 协议层无测试保护:RC4/签名/登录无 golden vectors,重构协议层极易悄悄破坏兼容性。
  • I11 无迁移机制:改表只能靠手工删库。
  • I12 CLI 与 MCP 路径行为不一致:CLI 无 per-type 超时/force_full;serve 是无参数默认行为(危险默认)。
  • I13 每 chunk 单独 commit:非原子块,中途断电留半块数据(靠 UPSERT 幂等兜底,但 sync_state 已前移时可能漏补)。

P2

  • I14 死代码/孤儿:logs_path、auto_sync_on_start、stale_after_minutes、store_raw_payloads、timezone 配置;delete_mi_fitness_token(无 logout 命令);SyncResult/UserProfile/DeviceInfo/DataCoverage(gaps_detected) 模型;_discover_region;sync_data_type_sync;click/rich 依赖;.env.example(无 env 加载)。
  • I15 代码重复:probe_mifitness.py 复制协议代码;query_service 周/月聚合两份近似函数;server 里 14 个 handler 高度模板化。
  • I16 可观测性:logger 无 handler(日志不知去向);last_error 只进工具响应。
  • I17 命名混乱:main.py 实为 CLI;server.py 里 main() 实为 MCP 入口。
  • I18 Config 默认 region="ru" 与文档/实际用法(cn)不一致。
  • I19 base.py 抽象签名 Iterator | AsyncIterator 联合类型增加调用方分支(_iterate_records 就是为兼容它存在)。

16. 重构方案建议

16.1 硬性约束(不可破坏的兼容契约)

  1. MCP 工具名与 inputSchema 保持不变(14 个工具;客户端可能已固化调用方式)。响应 JSON 顶层字段保持兼容。
  2. 协议层逐字节兼容:nonce 构造、RC4 drop-1024、双签名 base 串、登录前缀 &&&START&&&、分页参数——改协议必须先有 §16.4 的 golden vectors。
  3. 本地 DB 平滑升级:老库文件必须能被新版本打开(或提供一次性迁移脚本),sync_state 游标不能丢。
  4. CLI 四个子命令与关键 flag 保持(可新增不可删除)。
  5. 保持 Python ≥3.11、mcp>=1,<2 上界。

16.2 目标架构(建议)

src/mi_fitness_mcp/
├── app.py               # AppContext:配置、db、adapter、services 集中装配,替代 server 模块级全局
├── cli/                 # main.py 拆分:cli/{__main__,setup,doctor,sync}.py
├── mcp_layer/
│   ├── tools.py         # 14 个工具的 schema 声明(数据驱动,替代 300 行 if-elif)
│   └── handlers.py      # 薄 handler:解析参数→调 service→包 QueryResponse
├── protocol/            # ★从 adapter 抽出,纯函数化、可独立测试
│   ├── xiaomi_auth.py   # 登录(返回 serviceToken/ssecurity/cookies)
│   ├── crypto.py        # nonce/rc4/signature(golden vectors)
│   └── client.py        # XiaomiHealthClient:request/fetch_key/fetch_sport_records(重试/分页/错误分类)
├── adapters/
│   ├── base.py          # 纯 AsyncIterator 抽象
│   └── mi_fitness_cloud.py  # 仅做 payload→model 映射
├── services/
│   ├── sync_service.py  # 分块/增量/游标(tz-aware)
│   └── query_service.py
├── storage/
│   ├── db.py            # aiosqlite(或 run_in_executor)+ 事务化 chunk commit
│   ├── schema.py        # DDL + schema_version 迁移表
│   └── repo.py          # 按类型的仓储(UPSERT 用 total_changes 判定 added/updated)
├── config.py            # pydantic-settings:JSON + 真实 env/.env 支持(落地 .env.example)
└── models/              # 现有模型 + 删除未用模型或接线

16.3 优先修复映射(问题 → 方案)

问题 方案
I2 UPSERT 后用 conn.total_changes() 差值或 RETURNING 判定 inserted/updated;顺带维护 records_count
I3 游标统一存 UTC(或带 offset 的完整 ISO),续传转 aware 再比较;默认日期一律 aware
I4 HR 的 UPSERT 冲突目标改为业务 UNIQUE(user_id,timestamp,sample_type),id 与约束二选一统一
I5 storage 全链路 aiosqlite/线程池;查询下推 SQL(LIMIT/聚合)
I7 日期列改为存 UTC + 原始 offset 两列,或查询时用 unixepoch() 换算到用户时区再比较
I6 _optional_* 只把 None/空转 None,保留 0
I8 删 weight_kg 枚举或实现体重序列(join body_measurements);latest 聚合单独实现;HR workout 类型留待协议支持或删枚举
I9 AppContext 单一同步闸门;后台任务可选落盘(sync_state 里记 running 状态)
I13 每 chunk 一个事务(数据+游标同 commit)
I11 schema_version 表 + 线性迁移函数链

16.4 验收标准

  1. pytest 通过,且新增:crypto golden vectors(固定 ssecurity/nonce → 固定密文/签名)、8 类 payload fixture 解析快照测试、分块边界(chunk=1 天/跨月)、UPSERT added/updated 正确性、时区边界(+08:00 与 Z 混合查询)。
  2. 手工验收序列(当前环境可直接跑):
    • mi-fitness-mcp doctor → 连接 ✅、8 类型齐全;
    • sync --start-date <昨天> --end-date <今天> → 与重构前逐表 rowcount/内容 diff 一致;
    • serve + MCP 客户端调用全部 14 工具成功;
    • 老库文件(本机 AppData\Local\mi-fitness-mcp\mi-fitness-mcp\mi_fitness.db,约 19k 行数据)可直接打开且覆盖统计正确。
  3. 行为不回退:8 个月全量同步耗时不显著劣化;单类型超时(180s)语义保留。

17. 开发环境备注

  • 要求 Python ≥ 3.11;开发安装 pip install -e '.[dev]'(含 pytest/ruff/build)。
  • 跑测试:python -m pytest -q(18 passed,pytest-asyncio auto 模式)。
  • Lint:python -m ruff check src tests(CI 同款规则集,见 pyproject [tool.ruff])。
  • 构建:python -m build(hatchling)。
  • 运行时产物均在系统用户目录(platformdirs),不在仓库内:config.json、SQLite 库、keyring 凭据;.gitignore 已排除 .venv/ *.db .env 等。

18. HTTP API 扩展(新增模块)

在 MCP 之外提供同能力的 REST 封装,供普通程序直接调用:

  • 模块:src/mi_fitness_mcp/api.py(FastAPI + uvicorn,可选依赖组 pip install -e '.[api]')。
  • 启动:mi-fitness-mcp api [--host 127.0.0.1] [--port 8321](新 CLI 子命令;绑定非本机地址且未设 key 时打印警告)。
  • 鉴权:设置环境变量 MI_FITNESS_API_KEY 后,全部接口要求请求头 X-API-Key;未设置则不鉴权(仅建议 127.0.0.1 场景)。已全局启用 CORS。
  • 生命周期:lifespan 中装配 config/Database/adapter/SyncService/QueryService(与 server.main 相同的懒连接策略),退出时取消后台同步任务并关闭 adapter。查询端点为同步 def(FastAPI 线程池执行,不阻塞事件循环)。
  • 端点一览(均为 /api 前缀,响应信封 {"status","count","data"} 或同步结果对象):
    • GET / 服务信息;GET /docs Swagger UI
    • GET /api/status?deep=true|false(deep=true 真实健康检查)
    • POST /api/sync body {data_types?, start_date?, end_date?, force_full_sync?, background?};前台等待返回汇总,background=true 返回 {status:"accepted", sync_id}
    • GET /api/sync/{sync_id} 后台任务状态(内存态,重启即失)
    • GET /api/summary?start_date&end_date
    • GET /api/metric-series?metric&start_date&end_date&granularity&aggregation(metric 限 steps/distance_m/active_kcal,已在 API 层校验)
    • GET /api/heart-rate?start_date&end_date&sample_type&limit
    • GET /api/sleep?start_date&end_date&include_naps
    • GET /api/workouts?start_date&end_date&activity_types&min_duration&min_distance_km
    • GET /api/body-measurements?start_date&end_date&metrics&latest_only
    • GET /api/spo2 / GET /api/stress?level / GET /api/abnormal-heart-beat
    • GET /api/coverage?data_types
  • 错误约定:参数格式错→400,重复同步→409,未初始化→503,云连接失败→502,未知 sync_id→404,鉴权失败→401。
  • 已知取舍:sync_tasks 为内存态;查询与同步并发时 sqlite 写入可能偶发 busy(上游 I5/I13 修复后自然消除)。

19. API Key 体系与扫码登录(v0.2 新增)

19.1 鉴权模型(类大模型平台)

三级访问,实现在 api.py 的 _gate(入口闸门)+ _resolve_context(凭据解析):

  1. 默认凭据:不带头 = 使用本机配置(keyring)的凭据,仅建议 127.0.0.1。
  2. 静态 Key:环境变量 MI_FITNESS_API_KEY 设置后强制全员带头;该值本身也始终有效(管理员通道)。
  3. 发放 Key:mif_sk_<40 hex>,存主库 api_keys 表(key/label/user_id/pass_token/region/created_at/last_used_at/revoked)。带 Key 请求按 Key 绑定的小米凭据解析出独立 UserContext(adapter/sync/query 三服务按凭据缓存),实现多账号隔离;每次使用更新 last_used_at。

管理端点(keys/qr)鉴权:环境变量 MI_FITNESS_ADMIN_KEY + X-Admin-Key 头;未设置该变量时仅允许本机调用。

19.2 Key 管理端点

  • POST /api/auth/keys body {user_id, pass_token, region?, label?} → 真实登录小米验证凭据后发放 Key(失败 400 返回原因)。完整 Key 仅返回一次。
  • GET /api/auth/keys → 列表(Key 打码,如 mif_sk_ab12cd…ef34)。
  • DELETE /api/auth/keys/{prefix} → 按完整 Key 或唯一前缀吊销(软删 revoked=1)。

19.3 扫码登录(逆向自 account.xiaomi.com 通用 QR 流程)

流程与端点(参考 python-miio cloud_qr.py,MIT):

  1. GET https://account.xiaomi.com/longPolling/loginUrl,参数必须 sid=xiaomiio + callback=https://sts.api.io.mi.com/sts(其他组合返回 code=10025「Callback连接不合法」;miothealth 不被扫码接口接受)。响应剥 &&&START&&& 后含 qr(二维码图片 URL)、lp(长轮询 URL)、loginUrl(浏览器确认链接)、timeout(300 秒)。
  2. 用户用小米账号/米家 App 扫码确认,或浏览器打开 loginUrl 登录确认。
  3. GET {lp} 长轮询,确认后返回 userId + 账号级 passToken(passToken 与 sid 无关,可再用 serviceLogin 换 miothealth 会话)。
  4. 服务端校验凭据可登录后自动发放 API Key。

API 封装:POST /api/auth/qr/start?region=cn(返回 qr_token、PNG 地址、login_url)→ GET /api/auth/qr/{token}.png → GET /api/auth/qr/poll?token=...(waiting / confirmed{api_key,user_id} / expired)。qr_sessions 为内存态,重启失效。

19.4 Python Flask Web 仪表盘与反向代理(web.py / testpage)

基于 Python Flask 构建的现代化 Web 仪表盘与反向代理服务(无需 Rust 工具链,纯 Python 生态一键启动):

  • GET /:提供极富设计感的现代化中文健康数据仪表盘(全部数据端点 + 同步中心 + API Key 发放/列表/吊销 + 扫码二维码登录弹窗 + 多维图表 + 实时响应检视器)。
  • /proxy/*:反向代理到 Python FastAPI 后端,服务端自动中继 X-API-Key 与 X-Admin-Key(凭据与密钥不进浏览器),4xx/5xx 状态码与耗时(X-Upstream-Time-Ms 响应头)原样透传。
  • 启动命令:mi-fitness-mcp web 或 python testpage/app.py。
  • 环境变量:BIND(默认 127.0.0.1:8322)、MI_FITNESS_API_URL(默认 127.0.0.1:8321)、MI_FITNESS_API_KEY、MI_FITNESS_ADMIN_KEY。

附录 A:MCP 工具 inputSchema 全量 JSON

{
  "get_connection_status": {"type": "object", "properties": {}},
  "sync_data": {
    "type": "object",
    "properties": {
      "data_types": {"type": "array", "items": {"type": "string"}},
      "start_date": {"type": "string"},
      "end_date": {"type": "string"},
      "force_full_sync": {"type": "boolean"},
      "background": {"type": "boolean", "default": false}
    }
  },
  "get_sync_status": {
    "type": "object",
    "properties": {"sync_id": {"type": "string"}},
    "required": ["sync_id"]
  },
  "get_profile": {"type": "object", "properties": {}},
  "get_daily_summary": {
    "type": "object",
    "properties": {"date": {"type": "string"}, "start_date": {"type": "string"}, "end_date": {"type": "string"}}
  },
  "query_metric_series": {
    "type": "object",
    "properties": {
      "metric": {"type": "string", "enum": ["steps", "distance_m", "active_kcal", "weight_kg"]},
      "start_date": {"type": "string"},
      "end_date": {"type": "string"},
      "granularity": {"type": "string", "enum": ["day", "week", "month"]},
      "aggregation": {"type": "string", "enum": ["sum", "avg", "min", "max", "latest"]}
    },
    "required": ["metric", "start_date", "end_date"]
  },
  "query_heart_rate": {
    "type": "object",
    "properties": {
      "start_date": {"type": "string"},
      "end_date": {"type": "string"},
      "sample_type": {"type": "string", "enum": ["resting", "active", "passive", "workout"]},
      "limit": {"type": "integer"}
    },
    "required": ["start_date", "end_date"]
  },
  "query_body_measurements": {
    "type": "object",
    "properties": {
      "start_date": {"type": "string"},
      "end_date": {"type": "string"},
      "metrics": {
        "type": "array",
        "items": {"type": "string", "enum": ["weight_kg", "bmi", "body_fat_pct", "muscle_mass_kg", "water_pct"]}
      },
      "latest_only": {"type": "boolean"}
    },
    "required": ["start_date", "end_date"]
  },
  "query_sleep": {
    "type": "object",
    "properties": {
      "start_date": {"type": "string"},
      "end_date": {"type": "string"},
      "include_naps": {"type": "boolean"}
    },
    "required": ["start_date", "end_date"]
  },
  "query_workouts": {
    "type": "object",
    "properties": {
      "start_date": {"type": "string"},
      "end_date": {"type": "string"},
      "activity_types": {"type": "array", "items": {"type": "string"}},
      "min_duration": {"type": "integer"},
      "min_distance_km": {"type": "number"}
    },
    "required": ["start_date", "end_date"]
  },
  "query_spo2": {
    "type": "object",
    "properties": {
      "start_date": {"type": "string"},
      "end_date": {"type": "string"},
      "limit": {"type": "integer"}
    },
    "required": ["start_date", "end_date"]
  },
  "query_stress": {
    "type": "object",
    "properties": {
      "start_date": {"type": "string"},
      "end_date": {"type": "string"},
      "level": {"type": "string", "enum": ["low", "medium", "high"]},
      "limit": {"type": "integer"}
    },
    "required": ["start_date", "end_date"]
  },
  "query_abnormal_heart_beat": {
    "type": "object",
    "properties": {
      "start_date": {"type": "string"},
      "end_date": {"type": "string"},
      "limit": {"type": "integer"}
    },
    "required": ["start_date", "end_date"]
  },
  "get_data_coverage": {
    "type": "object",
    "properties": {"data_types": {"type": "array", "items": {"type": "string"}}}
  }
}

附录 B:响应信封示例

查询类(QueryResponse):

{"status": "ok", "source": "cache", "generated_at": "2026-08-31T10:00:00", "timezone": "UTC", "data": {"samples": [{"timestamp": "2026-08-08T04:09:00+08:00", "bpm": 93, "sample_type": "passive"}], "count": 1}, "error": null}

同步类(_run_sync_data):

{
  "status": "ok | partial | error",
  "sync_id": "uuid",
  "started_at": "...", "finished_at": "...",
  "records_added": 0, "records_updated": 0, "records_skipped": 0,
  "data_types_synced": ["daily_activity"],
  "results": [{"data_type": "daily_activity", "status": "ok", "added": 1, "updated": 0, "skipped": 0, "start_date": "...", "end_date": "...", "chunks": [{"start_date": "...", "end_date": "...", "status": "ok", "added": 1, "updated": 0, "skipped": 0}], "duration_seconds": 1.2}]
}

附录 C:config.json 实例(本机实际生成)

{
  "mode": "mi_fitness_cloud",
  "region": "cn",
  "timezone": "UTC",
  "database_path": "C:\\Users\\<user>\\AppData\\Local\\mi-fitness-mcp\\mi-fitness-mcp\\mi_fitness.db",
  "logs_path": "C:\\Users\\<user>\\AppData\\Local\\mi-fitness-mcp\\mi-fitness-mcp\\logs\\mi_fitness.log",
  "auto_sync_on_start": true,
  "stale_after_minutes": 60,
  "store_raw_payloads": true,
  "default_lookback_days": 30,
  "sync_chunk_days": 7,
  "http_timeout_seconds": 20.0,
  "request_retries": 3,
  "health_check_timeout_seconds": 10.0,
  "sync_type_timeout_seconds": 180.0,
  "max_pages": 200
}