config.devices.yaml 里(由 config.yaml
顶部的 INCLUDE 列表引进来),不在 config.yaml 本身:一套从不接穿戴设备的部署,
根本不会看到它们。那个文件发布时凭证是空的,各家的 endpoint 默认值已经填好;把你的
凭证写进去,或者写进你的 config.{env}.yaml 覆盖层,后者优先级更高(下面的 YAML 块
放哪个里都行)。
Apple Health 不在这个列表里,也不可能在。 HealthKit 只能由签名过的 iOS App 在 设备本机、并且用户按类型逐项授权之后读取,没有网页 OAuth 流程,也没有服务器到服务 器的 API。这个服务端是接收方(/apple/health、/apple/statistics、/apple/cda),数据由一个已经拿到它的客户端交过来。见collect/providers/apple/。
开始之前
一个公网可达的 HTTPS 地址。 用户点了同意之后,每一家厂商都会把浏览器重定向回你 的服务器,而且没有一家接受localhost 或者裸 HTTP 作为注册的回调地址。本地开发就开
个隧道:
config.{env}.yaml 里把它设成 MCP_PUBLIC_URL,
好让服务端其余部分对「自己的地址是什么」有一致的认识。
回调路由本来就有。 它挂在:
platform 永远是 theta。slug 是 theta_garmin、theta_oura、
theta_whoop,是完整的 slug,不是光秃秃的厂商名;处理器就是拿这个字符串精确查
provider 的(ProviderPlatform.get_provider)。
所以你在厂商那边注册的重定向地址,和你写进配置的那个,是同一个字符串,长这样:
Oura
OAuth 2.0。 到 cloud.ouraring.com/oauth/applications 注册。重定向地址填上面那条、slug 用theta_oura。
Whoop
OAuth 2.0。 到 developer.whoop.com 注册,重定向 地址的 slug 用theta_whoop。
WHOOP_AUTH_URL、WHOOP_TOKEN_URL、WHOOP_API_BASE_URL、WHOOP_SCOPES、
WHOOP_REQUEST_TIMEOUT、WHOOP_CONCURRENT_REQUESTS、WHOOP_MAX_DETAIL_RECORDS。
Garmin
OAuth 1.0a,不是 2.0。流程是 request-token → 用户授权 →oauth_verifier →
access-token,回调处理器按这个分支走(LinkType.OAUTH1)。通过
Garmin Connect 开发者计划申请;
审批是人工的,通常也是最花时间的一环。
GARMIN_AUTH_URL、GARMIN_TOKEN_URL、
GARMIN_ACCESS_TOKEN_URL、GARMIN_API_BASE_URL、OAUTH_TEMP_TTL_SECONDS。
密钥会自动静态加密:任何名字里含_SECRET、_KEY、_TOKEN、_PASSWORD等等的 键,服务端第一次读它的时候就会用.env里的CONFIG_ENCRYPTION_KEY加密。明文只 用粘贴一次,文件会自己改写自己。
验证
1. provider 起来了。 重启,然后看启动日志:Failed to load provider … 是另一回事:那是 import
错误,有回归测试守着。
2. 它被提供给用户了。
POST /api/v1/pulse/user/providers/link(需要鉴权)会返回厂商的
授权地址;打开、同意,厂商就会把浏览器送到你的回调上。成功之后,用户已绑定的
provider 会出现在:
POST /api/v1/pulse/user/providers/unlink。
4. 数据到了。 要么按 provider 启动时注册的拉取计划来,要么走 webhook:
POST /api/v1/pulse/{platform}/{provider}/webhook,这就是你给厂商填的那个推送地址。
没设 COLLECT_WEBHOOK_SECRET 时 webhook 是关着的(404);设了之后,给厂商填的地址要带
?secret=<值>,或者在 X-Webhook-Secret 头里带上这个值。推送用厂商自己的用户 id
指明是谁的数据,再通过已关联的账户对上本地用户。
排查
只有loaded 0 providers,再没别的行。 所有 provider 都拒绝了。对着这一页核一遍
键名:OURA_CLIENT_ID 打错一个字母,和根本没配长得一模一样,因为 create_provider
两种情况都返回 None。
厂商说重定向地址不对。 它必须和你注册的那条逐字节一致,包括协议、主机、路径,
以及结尾有没有斜杠。最常见的漏是 slug:是 theta_oura,不是 oura。
回调返回 “provider not available”。 provider 在启动时就拒绝了,所以平台那边根本
没有东西注册在这个 slug 下。先修凭证,回调在注册的下游。
一开始好好的,一小时后不动了。 access token 会过期,靠 refresh_access_token 刷
新;如果当初根本没存下 refresh token,那是厂商那边的授权缺了 offline/refresh 权限。
配好正确的 scope 之后重新绑一次。
自己写一个
provider 的契约就是一个目录:mirobody_<slug>/provider_<slug>.py,导出一个
BasePullProvider 的子类,并且 create_provider(config) 在没配置时返回 None。
mirobody_whoop/ 是 OAuth2 的参考实
现;mirobody_oura/ 是同样的形状换了
一家厂商。完整指南:provider-guide.md。
包外面的 provider 放进 PROVIDER_DIRS,它们是按文件位置加载的,所以里面要用绝对
import。