基于 Omi 的 Whoop 集成插件:OAuth 授权、Chat Tools 部署与健身数据查询实战指南
【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend
本文以仓库中的 plugins/omi-whoop-app 插件为核心,系统讲解如何构建并部署一个接入 Whoop 开发者 API 的 Omi 聊天工具插件:从 Whoop 开发者应用创建、Railway 部署、OAuth2 授权回调,到七类 Chat Tools 端点与指标解读的完整实现。读完本文,你将掌握 Whoop 数据通过自然语言对话接入 Omi 的完整链路,并能基于源码定位每个端点的底层数据流。
一、插件定位与功能总览
plugins/omi-whoop-app是 Omi 生态中的第三方数据集成插件,解决的核心问题是:把 Whoop 手环产生的恢复、压力、睡眠与训练数据,通过 Omi 的 AI 对话能力直接以自然语言查询。用户在聊天中输入"我今天恢复得怎么样?",Omi 的 Chat Tools 机制即可路由到该插件的 HTTP 端点,返回结构化的健身数据摘要。
其功能清单在 README 中定义如下:
- Recovery Score(恢复评分):查询每日恢复分数与 HRV(心率变异性)
- Strain Score(压力评分):查看每日压力水平
- Sleep Data(睡眠数据):查看睡眠时长、睡眠阶段与睡眠质量
- Workouts(训练记录):回顾最近的训练会话
- Weekly Summary(周度总结):获取趋势与平均值
- Body Measurements(身体测量数据):查看身高、体重、最大心率
- Profile(个人资料):访问 Whoop 账户资料信息
从实现角度看,这是一个基于 FastAPI 的独立 Web 服务(见 main.py),通过 OAuth2 授权码模式(Authorization Code Flow)获取 Whoop 用户授权,再用访问令牌调用 Whoop Developer API,最终以 Omi Chat Tools Manifest 协议对外暴露能力。
二、整体架构与 OAuth 数据流
2.1 组件拓扑
插件由四个核心文件组成,职责清晰:
| 文件 | 职责 |
|---|---|
| main.py | FastAPI 应用:OAuth 路由、Chat Tools 端点、数据格式化、Manifest 生成 |
| db.py | 令牌与用户设置存储层(Redis 优先,本地 JSON 文件兜底) |
| models.py | Pydantic 响应模型ChatToolResponse |
| railway.toml | Railway 平台部署配置(Nixpacks 构建 + uvicorn 启动命令) |
| requirements.txt | 依赖清单:fastapi、uvicorn、python-dotenv、requests、pydantic、redis |
2.2 OAuth2 授权码流程
整个授权链路在 main.py 中实现,包含三个阶段:
阶段一:发起授权(GET /auth/whoop?uid=<uid>)
state = secrets.token_urlsafe(16) store_oauth_state(state, uid) params = { "client_id": WHOOP_CLIENT_ID, "redirect_uri": WHOOP_REDIRECT_URI, "response_type": "code", "scope": " ".join(WHOOP_SCOPES), "state": state } auth_url = f"{WHOOP_AUTH_URL}?{urlencode(params)}" return RedirectResponse(url=auth_url)服务端生成随机state并与用户uid绑定存储(10 分钟过期,见 db.py),用于回调时校验,防止 CSRF 攻击。同时携带七项 scopes 发起跳转:
read:recovery read:cycles read:sleep read:workout read:profile read:body_measurement offline其中offline作用域用于换取refresh_token,保证长期免重新授权。
阶段二:回调换令牌(GET /auth/whoop/callback)
response = requests.post( WHOOP_TOKEN_URL, data={ "client_id": WHOOP_CLIENT_ID, "client_secret": WHOOP_CLIENT_SECRET, "code": code, "grant_type": "authorization_code", "redirect_uri": WHOOP_REDIRECT_URI } )用授权码交换access_token、refresh_token与expires_in,计算expires_at后经store_whoop_tokens落库,随后展示连接成功页。
阶段三:令牌自动续期
每次访问 Whoop API 前都会先经get_valid_access_token检查过期时间(提前 5 分钟触发刷新),过期则调用refresh_access_token用refresh_token换取新令牌并回写存储(main.py)。这就是 README 中"只需授权一次"的底层保障。
2.3 令牌存储的双模式设计
db.py 实现了生产/开发双存储策略:
- Redis 模式:读取
REDIS_URL(或REDIS_PRIVATE_URL、REDIS_PUBLIC_URL)建立连接;令牌以whoop:tokens:{uid}为键存储,过期时间 90 天;OAuth state 键whoop:oauth_state:{state}过期 10 分钟;用户设置以whoop:settings:{uid}存储。 - 文件兜底模式:Redis 不可用时自动降级为
data/目录下的tokens.json、oauth_states.json、user_settings.json三个 JSON 文件,便于本地开发。
三、从零部署:Whoop 开发者应用 + Railway
3.1 创建 Whoop 开发者应用
- 打开 Whoop 开发者门户(Whoop Developer Portal),创建新应用;
- 填写应用信息:名称(如 "Omi Integration")、描述,以及回调地址(Redirect URI,见下文);
- 申请以下 scopes:
read:recovery、read:cycles、read:sleep、read:workout、read:profile、read:body_measurement、offline; - 复制生成的Client ID与Client Secret。
注意:Whoop 对
state参数有至少 8 个字符的长度要求。源码使用secrets.token_urlsafe(16)生成 16 字节随机串,并额外做了state -> uid的映射存储(db.py 注释明确说明),这比直接透传 uid 更安全。
3.2 部署到 Railway
按 README 的步骤:
- 在 Railway 创建新项目,连接 GitHub 仓库或直接从插件目录部署;
- 为项目添加一个Redis服务;
- 设置环境变量:
WHOOP_CLIENT_ID=your_client_id WHOOP_CLIENT_SECRET=your_client_secret WHOOP_REDIRECT_URI=https://your-app.up.railway.app/auth/whoop/callback- 触发部署。Railway 会自动完成三件事:
- 依据
requirements.txt安装依赖; - 依据 railway.toml 启动服务:
uvicorn main:app --host 0.0.0.0 --port $PORT,并使用/health作为健康检查路径(超时 100s,失败重启最多 3 次); - 自动注入
PORT与REDIS_URL环境变量。
- 依据
部署完成后,回到 Whoop 开发者应用后台,将 Redirect URI 更新为:
https://your-app.up.railway.app/auth/whoop/callback3.3 环境变量参考表
| 变量 | 说明 | 是否必填 |
|---|---|---|
WHOOP_CLIENT_ID | Whoop OAuth Client ID | 是 |
WHOOP_CLIENT_SECRET | Whoop OAuth Client Secret | 是 |
WHOOP_REDIRECT_URI | OAuth 回调 URL | 是 |
PORT | 服务端口(默认 8080) | 否 |
REDIS_URL | Redis 连接 URL(未设置时降级为文件存储) | 否 |
从源码看,PORT默认值 8080 与WHOOP_REDIRECT_URI默认值http://localhost:8080/auth/whoop/callback在 main.py 中定义,本地开发无需显式设置。此外 db.py 还会读取REDIS_PRIVATE_URL/REDIS_PUBLIC_URL作为备选连接串,兼容不同托管平台的 Redis 注入方式。
四、Omi App 配置:Chat Tools 接入的关键三 URL
在 Omi 后台创建/更新应用时,需要把以下三个 URL 填入对应字段(来自 README):
| 字段 | 值 |
|---|---|
| Setup URL | https://your-app.up.railway.app/?uid={{uid}} |
| Setup Completed URL | https://your-app.up.railway.app/setup/whoop?uid={{uid}} |
| Chat Tools Manifest URL | https://your-app.up.railway.app/.well-known/omi-tools.json |
这三个 URL 的底层语义对应三条 GET 路由:
/:带uid访问时渲染连接/已连接页面(无uid时返回 JSON 服务信息,见 main.py);/setup/whoop?uid=<uid>:返回{"is_setup_completed": true/false},Omi 据此判断用户是否已完成授权(main.py);/.well-known/omi-tools.json:Chat Tools Manifest,Omi 的 AI 层据此发现可用工具及其参数 schema(main.py)。
4.1 Manifest 如何驱动对话路由
Manifest 中每个工具都声明了name、description、endpoint、method、parameters与auth_required。以恢复评分工具为例:
{ "name": "get_recovery", "description": "Get the user's recovery score and metrics from Whoop. Use this when the user asks about their recovery, readiness, HRV, or how recovered they are.", "endpoint": "/tools/get_recovery", "method": "POST", "parameters": { "properties": { "date": { "type": "string", "description": "Date in YYYY-MM-DD format. Defaults to today." } }, "required": [] }, "auth_required": true, "status_message": "Getting your recovery data..." }description是给 LLM 的"意图匹配说明书"——它显式罗列了用户说 recovery、readiness、HRV、"how recovered they are" 时应触发该工具,这就是自然语言查询能被正确路由的关键机制。status_message则在工具执行期间向用户展示过程反馈。
五、API 端点全览
5.1 Chat Tools(POST,供 Omi 调用)
| 端点 | 功能 | 可传参数 |
|---|---|---|
/tools/get_recovery | 恢复评分与 HRV | date(YYYY-MM-DD,默认今天) |
/tools/get_strain | 每日压力评分 | date |
/tools/get_sleep | 睡眠数据 | date(获取"该日结束"的睡眠) |
/tools/get_workouts | 近期训练记录 | days(默认 7,上限 30)、max_results(默认 10,上限 50) |
/tools/get_weekly_summary | 近 7 天周度总结 | 无 |
/tools/get_body_measurements | 身体测量数据 | 无 |
/tools/get_profile | Whoop 资料 | 无 |
所有工具端点都遵循统一处理模式:读取请求体中的uid→ 获取有效访问令牌(未授权则返回"Please connect your Whoop first in the app settings.")→ 构造 Whoop API 请求 → 格式化结果 → 返回ChatToolResponse(result或error二选一,见 models.py)。
5.2 OAuth 与设置(GET)
| 端点 | 功能 |
|---|---|
/ | 首页 / 设置 UI(带uid时渲染 HTML 页面) |
/auth/whoop?uid=<uid> | 发起 OAuth 流程 |
/auth/whoop/callback | OAuth 回调 |
/setup/whoop?uid=<uid> | 检查设置完成状态 |
/disconnect?uid=<uid> | 解除 Whoop 账号绑定(删除令牌并重定向回首页) |
/health | 健康检查(Railway 探活用) |
/.well-known/omi-tools.json | Chat Tools Manifest |
5.3 底层 Whoop API 映射
从工具端点的实现可整理出插件调用的 Whoop Developer API(基础地址https://api.prod.whoop.com/developer/v1):
| 插件端点 | Whoop API |
|---|---|
/tools/get_recovery | GET /recovery |
/tools/get_strain | GET /cycle |
/tools/get_sleep | GET /activity/sleep |
/tools/get_workouts | GET /activity/workout |
/tools/get_body_measurements | GET /body_measurement |
/tools/get_profile | GET /user/profile/basic |
日期过滤统一构造为{date}T00:00:00.000Z至{date}T23:59:59.999Z的时间窗口,并设置limit取最新一条记录(main.py)。
六、本地开发
按 README 的步骤:
- 将
.env.example复制为.env并填入凭据; - 设置
WHOOP_REDIRECT_URI=http://localhost:8080/auth/whoop/callback; - 在 Whoop 开发者应用后台的 Redirect URI 列表中加入该地址;
- 安装依赖:
pip install -r requirements.txt; - 启动服务:
python main.py。
python main.py会读取PORT(默认 8080)与HOST(默认0.0.0.0)并启动 uvicorn(reload=True便于开发调试,见 main.py)。本地开发时未配置REDIS_URL会自动走 JSON 文件存储,无需额外基础设施。
七、示例聊天命令
部署并授权完成后,可在 Omi 聊天中直接输入:
- "What's my recovery today?"
- "How did I sleep last night?"
- "What's my strain level?"
- "Show my recent workouts"
- "Give me my weekly summary"
- "What's my HRV?"
八、Whoop 指标解读
8.1 Recovery Score(0-100%)
| 区间 | 状态 | 含义 |
|---|---|---|
| 67-100% | 绿色 | 恢复充分,可承受训练压力 |
| 34-66% | 黄色 | 恢复一般,谨慎安排训练 |
| 0-33% | 红色 | 恢复不足,优先休息 |
源码中的分区间逻辑与文档一致:>= 67绿、>= 34黄、其余红(main.py),并在结果中附带hrv_rmssd_milli(HRV)、resting_heart_rate(静息心率)、spo2_percentage(血氧)、skin_temp_celsius(皮肤温度)。
8.2 Strain Score(0-21)
| 区间 | 等级 |
|---|---|
| 0-9 | 轻度(Light day) |
| 10-13 | 中度(Moderate strain) |
| 14-17 | 高强度(High strain) |
| 18-21 | 过度训练(Overreaching,极高) |
对应源码分档:>= 18极高、>= 14高、>= 10中、其余低(main.py),并附带千焦(自动换算为 kcal,系数 0.239006)、平均心率与最大心率。
8.3 关键指标
- HRV(心率变异性):通常越高越好
- RHR(静息心率):通常越低越好
- Sleep Performance(睡眠表现):满足睡眠需求的程度
- Sleep Efficiency(睡眠效率):实际睡眠时间与在床时间之比
睡眠结果中还包含分阶段统计(浅睡/深睡/REM,单位为小时)、呼吸频率等,由format_sleep从stage_summary中的毫秒值换算(main.py)。
九、源码中的工程化细节
9.1 分页兜底:周度总结的完整性保证
get_weekly_summary需要聚合近 7 天四种数据,源码专门实现了whoop_fetch_all_records分页函数(main.py):循环跟随next_token/nextToken翻页,单集合最多 20 页兜底;任一页失败即返回(None, error),使调用方能区分"空数据"与"拉取不完整"。
对应的回归测试在 test_weekly_summary.py 中验证了三种行为:
- 翻页聚合:两页 recovery(50、70)平均为 60%,两页 workout(7+3)总数为 10;
- 无续页令牌即停止:单页返回即结束;
- 分页失败标记为不可用:第二页失败时输出
**Workouts:** Temporarily unavailable而非错误的 7 条,避免把部分结果当最终结果。
9.2 分页失败时的降级表现
当某类数据拉取失败时,周度总结不会整体报错,而是对单项输出Temporarily unavailable,其余维度照常汇总(main.py),提升了对话场景下的容错性。
9.3 统一的 API 请求封装
whoop_api_request统一注入Authorization: Bearer <token>头并处理错误(main.py),任何工具端点都无需重复鉴权逻辑;get_valid_access_token的 5 分钟提前刷新窗口则降低了对话过程中令牌过期的概率。
9.4 多运动类型映射
format_workout内置了常见sport_id到运动名称的映射(1=Running、16=Cycling、32=HIIT、33=Strength Training、48=Swimming、71=Walking、82=Yoga 等),未知 ID 显示为Activity {id}(main.py)。
十、常见问题与排查建议
Please connect your Whoop first:用户未完成 OAuth 授权。检查get_whoop_tokens(uid)是否返回空,引导用户访问/页面完成连接。- 回调报错 / Token exchange failed:核对
WHOOP_CLIENT_ID、WHOOP_CLIENT_SECRET与WHOOP_REDIRECT_URI是否与 Whoop 开发者后台配置完全一致(含协议与路径)。 - Redis 连接失败:服务会打印
Redis connection failed: ... falling back to file storage并降级为文件存储;Railway 上建议确认 Redis 服务已绑定到应用并注入了REDIS_URL。 - 本地 OAuth 无法回调:确认
.env中WHOOP_REDIRECT_URI为http://localhost:8080/auth/whoop/callback,且该地址已加入 Whoop 后台的 Redirect URI 白名单。
结语
plugins/omi-whoop-app完整演示了 Omi 第三方集成插件的标准范式:OAuth2 授权码 + 刷新令牌续期、Chat Tools Manifest 声明式工具路由、Redis/文件双模式存储、以及面向对话场景的健壮数据聚合。通过本文的部署步骤与源码对照,你可以将同样的模式复用到任意具备开放 API 的可穿戴设备或健康平台,让健身数据真正"开口说话"。
【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考