OpenStrap/backend

GitHub: OpenStrap/backend

OpenStrap 后端是一个基于 Cloudflare Worker 的自托管服务,负责接收、解码和分析 WHOOP 4.0 腕带的原始数据帧,生成恢复程度、睡眠、运动负荷等每日健康指标。

Stars: 4 | Forks: 5

# OpenStrap 后端 这是进行实际思考的部分。手机从腕带中提取原始字节,并将它们发送到这里。该服务器解码这些字节,将原始数据原封不动地归档,并按计划将它们转换为你实际查看的数字:恢复程度、运动负荷、睡眠、压力等等。你手机上的应用主要只是一个屏幕;实际的工作都在这里完成。 它是一个 Cloudflare Worker。不是一堆微服务,也不是 Kubernetes,而是一个挂载了 SQLite 数据库 (D1) 和对象存储桶 (R2) 的 Worker。这是刻意为之的,这样既能保证整体运行成本足够低,可以在 Cloudflare 的免费套餐中运行,又能保持足够的简单,让一个人就能在大脑中完全掌控它的全貌。 ## 字节是如何变成数字的 以下是你数据的完整生命周期,从头到尾。 你的手机连接到腕带,耗尽它缓冲在闪存中的所有记录,并将它们作为原始十六进制字符串 POST 到 `/ingest/batch`。它永远不会向我发送“心率 72”这样的信息,而是发送腕带发出的实际数据帧,由我自己算出 72 这个数字。这很重要,因为如果我以后在解码方面变得更聪明,我可以利用已经拥有的字节重新运行所有内容。 当一批数据到达时,`ingest.ts` 会按顺序执行四件事: 1. **对你进行速率限制。** 每个用户一个令牌桶,每秒 0.5 个令牌,突发上限为 30。防止失控的客户端对系统进行狂轰滥炸。数据存放在 `rate_limit` 表中。 2. **解码数据帧。** `decode.ts` 遍历每个十六进制字符串,解析出数据包类型和记录类型,并尽可能提取出它能提取的内容:时间戳、心率、运动幅度、步数增量、以及腕带是否戴在你的手腕上。1 Hz 记录(`parse_r24`,借用自协议包)和带有 IMU 的 R10 是携带真实信号的数据。 3. **将原始字节保存到 R2**,路径为 `raw/{you}/{device}/{when}-{first}-{last}.txt`,每行一帧。这是我永远不会丢弃的部分。按分钟汇总的数据会在 90 天后被清理;但原始数据帧会被永久保留,以便它们可以被永远重新解码。 4. **将所有内容汇总为分钟级数据。** `rollup.ts` 按 `floor(ts/60)*60` 将解码后的样本分桶,并将它们写入 `minute` 表。 `minute` 表是你在进行任何操作之前值得了解的一个巧妙设计。它不存储平均心率,而是存储用于计算的累加部分:`hr_sum`、`hr_n`、`act_sum`、`act_n`,以及最小值和最大值。upsert 操作会将新的累加部分添加到已有的数据上。原因是上传的数据并不纯净。手机会重试,批次会重叠,同一个数据帧会出现两次。如果我存储的是平均值,每次重复发送时我都会破坏数据。而存储累加和意味着,重新上传完全相同的数据最终会收敛到完全相同的结果。免费实现了幂等性。不要破坏这个机制。 分钟级数据写入完成后,用户会被标记为脏数据(如果你使用的是付费套餐,则会被推入队列),这就是 ingest 流程的终点。繁重的计算被刻意排除在请求路径之外。 ## 指标实际在哪里计算 `analytics.ts` 是系统的大脑,它运行在 cron 上。有两个计划任务,都在 `wrangler.toml` 中定义: - **每小时**(`runAnalytics`,带有 3 天的时间窗口):重新计算每个带有脏数据的用户。这是安全保障网,用于捕获队列遗漏的任何用户。 - **每晚 3:30**(2 天的时间窗口):一次更全面的计算,包括为最近睡过觉的任何人计算呼吸频率,并清理超过 90 天的分钟级行数据。 `processUser` 是针对单个用户进行处理的地方。它会读取该用户的分钟级数据,提取他们的基线,并针对每个指标调用 [analytics 包](https://github.com/OpenStrap/analytics):静息心率、运动负荷、心率区间、卡路里、睡眠检测、睡眠规律性、锻炼检测、训练负荷、健身趋势、准备状态、异常信号、教练计划、压力和夜间心率。计算结果存放在 `daily`、`sleep`、`sessions` 和 `baselines` 中。我每天都会在一个滚动窗口中保留这些数据,这样数字实际上会在一天天之间产生变动,而不是坍缩成一个平坦的值,这是早期出现的一个真实 bug。 每个返回的数字都会被包装起来:一个数值、一个单位、一个介于 0 和 1 之间的置信度、一个层级和一个标签。如果没有对应的输入数据,数值将为 `null`,置信度将为 `0`。我宁愿向你展示一个破折号,也不愿凭空捏造。整个项目一旦开始捏造数字就会彻底崩溃,所以它绝不会这么做。 ## src 目录包含什么 | 文件 | 用途 | |------|---------------| | `index.ts` | Hono 应用、路由表和 cron 处理程序 | | `auth.ts` | JWT 签名/验证、邮箱 OTP 流程、发送邮件 | | `ingest.ts` | 上文提到的整个数据摄取路径 | | `decode.ts` | 将十六进制数据帧转换为解码后的样本 | | `rollup.ts` | 将解码后的样本转换为按分钟分桶的数据 | | `analytics.ts` | cron 大脑,`processUser` / `runAnalytics` | | `query.ts` | 读取端点:今日数据、睡眠、运动负荷、趋势、图表、历史记录 | | `daydetail.ts` | 单日深度钻取(运动负荷曲线、睡眠同期图、压力区间) | | `history.ts` | 区间聚合和日历热力图 | | `records.ts` | 个人最佳记录、连续打卡记录、静息心率漂移 | | `journal.ts` | 你的标签和笔记,以及一个用于查看标签如何影响你数据的关联引擎 | | `notifications.ts` | 通知信息流和将内容标记为已读 | | `resp.ts` | 基于光学 PPG 记录得出的呼吸频率(有条件触发,仅在存在真实 PPG 数据时得出) | | `seed.ts` | 用于测试的合成数据生成器,分阶段运行以保持在免费套餐的请求上限内 | | `db/schema.sql` | 整个数据库,支持幂等操作,因此你可以重复运行它 | ## API 除了注册之外,所有操作都需要 `Authorization: Bearer `,并且你只能看到自己的数据,这些数据由内置在 token 中的用户 ID 进行范围限定。`/admin/*` 路由需要改为使用管理员 token。 登录是无密码的:`POST /auth/register`,然后 `/auth/request-otp` 会向你发送一封包含六位数字验证码的邮件,接着 `/auth/verify-otp` 将该验证码换取为访问 token (24h) 和刷新 token (30d)。`/auth/refresh` 用于轮换它们。如果未配置邮箱,验证码将直接在响应中返回,因此你在设置期间永远不会被锁定。 推送数据:使用带有 `{device_id, records: [hex...]}` 的 `POST /ingest/batch`,以及用于设备事件的 `/ingest/events`。 读取数据:`/today`、`/sleep`、`/strain`、`/sessions`、`/trends`、`/chart`、`/history`、`/day/{strain,sleep,timeline,stress}` 深度钻取、`/records`、`/journal`(以及 `/journal/insights`)和 `/notifications`。还有一个你可以对其执行 GET 和 PATCH 操作的 `/profile`。 当你运行自己的实例时的管理功能:`/admin/run-analytics`、`/admin/run-resp`、`/admin/seed-demo`、`/admin/issue-token`、`/admin/wipe-raw`、`/admin/prune`。 ## 数据库 包含十三张表。你可能会关心的几张:`minute`(进行累加和汇总的数据,90 天后会被清理),`daily`、`sleep` 和 `sessions`(派生输出,主要使用 JSON 列来存储结构化的信息位,如教练计划和心率区间),`baselines`(你的静息心率、最大心率、睡眠需求,以及其他一切指标的衡量基准),以及认证三件套(`users`、`otps`、`refresh_tokens`)。完整的 DDL 位于 `src/db/schema.sql` 中,里面带有注释,去读读看吧。 ## 运行你自己的实例 ``` npm install npx wrangler d1 create openstrap-db # paste the id into wrangler.toml npx wrangler r2 bucket create openstrap-raw npx wrangler d1 execute openstrap-db --file src/db/schema.sql npx wrangler secret put JWT_SECRET # any long random string npx wrangler secret put ADMIN_TOKEN # another one, for /admin/* npx wrangler deploy ``` 如果你希望登录邮件能够真正发送出去,请添加 `BREVO_API_KEY` 或 `RESEND_API_KEY` 以及 `EMAIL_FROM`。如果没有它们,登录功能仍然可用,只不过验证码会直接在 API 响应中返回。密钥请通过 `wrangler secret` 或 `.dev.vars` 管理,切勿提交到代码仓库中。 这里的 `wrangler.toml` 中有一个占位符用于填写 D1 id,请将其替换为你自己的。 ## 坦诚地谈谈你的数据 你可以自己运行所有这一切。这就是它的全部意义所在:它是开源的,且后端 URL 是一个设置项,而不是硬编码的常量。搭建你自己的 Worker、你自己的 D1、你自己的 R2,这样你的健康数据就永远不会接触到不属于你的机器。 或者不这么做,使用我的。如果你这么做了:我能拿你的心率怎么样呢?什么也做不了。我保证,我对其唯一会做的事情就是随着时间的推移让解码器和计算逻辑变得更好。我不会出售它,也不会为你建立个人档案,我确实根本不在乎你星期二跑了 5 公里。但你不必只听我的一面之词,这就是提供自托管路径的原因。 ## 尚未完工 这里存在一些 bug。我知道其中一些,但不知道另一些。压力阈值尚未校准好,睡眠阶段评估器过于依赖 REM,呼吸频率只有在有实时的 PPG 数据可处理时才会显示。如果你发现了什么问题,请提交 issue,我会逐一解决它们。参与的人越多,它就会变得越好,而不是越差。
标签:SQLite, 健康监测, 可穿戴设备, 后端开发, 程序员工具, 自托管, 高对比度