内容
# Loopback 服务器
[](https://github.com/aderaaij/loopback-training-server/actions/workflows/ci.yml)
[](https://github.com/aderaaij/loopback-training-server/actions/workflows/docker-publish.yml)
**Loopback – Run Coach** 背后的自托管后端,一个 iOS 跑步应用程序。它存储锻炼历史,排队结构化的锻炼以通过 WorkoutKit 发送到 Apple Watch,管理训练计划和每日健康指标,并为运动员和管理员提供一个身份验证的 Web 仪表板。
使用 FastAPI、PostgreSQL、SQLAlchemy 和 React 构建。包括一个可选的 MCP(模型上下文协议)服务器,以便 AI 助手可以作为您的跑步教练使用相同的数据。
> **这是两部分系统中的服务器部分 — 您还需要 [Loopback iOS 应用程序](https://github.com/aderaaij/loopback-training-app)。** 该应用程序是向服务器提供数据的内容:它同步 Apple HealthKit 锻炼和每日健康指标到服务器,并将排队的锻炼安装到 Apple Watch。服务器单独运行(API、仪表板、MCP),但如果没有应用程序,服务器就无法接收数据或将锻炼发送到您的手表。
> **命名说明:** 这是 Loopback 跑步应用程序的配套服务器 — 与 LoopBack Node.js 框架或 Rogue Amoeba 的 Loopback 音频工具无关。代码库中使用的技术名称(包、容器、DB)是 `training-api`。

| 锻炼详情 | 健康趋势 |
|---|---|
|  |  |
*演示数据所示;每个屏幕都限定在已登录的运动员。*
## 功能
- **锻炼存储** — 具有活动类型、距离、持续时间、心率、分段和任意 JSONB 数据的 CRUD API
- **分析** — 按周、月或年汇总的端点
- **训练队列** — 为 Apple Watch 排队结构化锻炼(间隔、热身/冷却、配速提醒)以通过 iOS 配套应用程序同步
- **锻炼操作** — 编辑或删除已同步到 Apple Watch 的锻炼,通过待处理操作队列
- **设备库存** — 跟踪用户 Apple Watch 上当前有哪些锻炼
- **错过锻炼反馈** — 记录和查询用户错过计划锻炼时的反馈,具有模式检测功能用于教练
- **训练计划** — 创建和管理具有目标、约束、阶段和运动员上下文的训练计划,存储为灵活的 JSONB 元数据,以及明确的完成流程(评分 + 反馈反馈给教练上下文)
- **调度日历** — 将每周重复节奏附加到计划,并查询一个统一的日历,合并排队的跑步和计划的力量训练,会标记冲突
- **计划验证** — 即将到来的计划的确定性“lint”检查:每周坡道和逐渐减少检查、缺少休息周、连续高强度训练日、约束违规、力量训练冲突 — 警告,而不是阻止(`POST /api/plans/{id}/validate`,在排队时也会返回)
- **健康指标** — 批量更新每日 HealthKit 指标(睡眠、心率、HRV、体重、VO2Max、步数、身体成分),按日期更新
- **计划-锻炼链接** — 将排队的锻炼链接到计划(`plan_id`)和记录的锻炼链接到其计划对应项(`plan_workout_id`),用于计划与实际分析
- **多用户** — 具有每个设备的 API 令牌的用户名/密码帐户(`POST /api/auth/login`);所有数据均按用户范围限定
- **Web 仪表板** — 由 API 在同一来源提供的身份验证 React SPA:概览、日历、锻炼、计划、健康图表和运动员的队列;管理员的用户管理和系统监控
- **管理员和监控** — 每个用户的令牌检查/撤销(切断一个被盗设备)、运动员的同步新鲜度、身份验证审计跟踪(登录、密码/令牌/用户更改)和一个报告数据库大小和备份新鲜度的系统屏幕
- **MCP 服务器** — 让 AI 助手查询您的训练数据、创建锻炼和计划,并通过自然语言关联健康指标
## 快速开始
### 先决条件
- Docker 和 Docker Compose v2.24 或更高版本
### 1. 克隆和配置
```bash
git clone https://github.com/aderaaij/loopback-training-server.git
cd loopback-training-server
cp .env.example .env
```
编辑 `.env` 并设置 `BOOTSTRAP_ADMIN_PASSWORD` — 首次运行的管理员密码。其他一切都有一个合理的默认值(文件中的注释解释了每个值)。
### 2. 启动
```bash
docker compose up -d # 或:make up
```
第一次启动会从 GHCR 拉取预构建的多架构镜像(amd64/arm64;如果拉取失败,则从源代码构建,这需要几分钟 — `docker compose build` 强制执行),运行数据库迁移并创建 `admin` 帐户。然后 API 和仪表板位于 `http://localhost:8001`(使用 `curl http://localhost:8001/api/health` 验证)。
### 3. 登录
打开 `http://localhost:8001` 并以 `admin` 身份登录,密码为您设置的密码。管理员可以访问管理控制台(用户 + 系统);普通帐户可以访问训练仪表板。
如果您将 `BOOTSTRAP_ADMIN_PASSWORD` 留空,则登录功能保持禁用,容器日志会告诉您如何修复它(`docker compose logs app`)。
### 4. 添加您的家庭
一个实例服务于一个家庭,类似 Audiobookshelf:几个帐户,所有数据均按用户范围限定,没有开放注册 — 管理员创建每个帐户。
1. 作为 `admin`,打开 **用户** 并为每个运动员创建帐户(角色 `user`)。
2. 每个运动员使用其用户名 + 密码登录 **仪表板**。
3. 在 **[iOS 应用程序](https://github.com/aderaaij/loopback-training-app)** 中,每个运动员输入服务器 URL 以及其用户名 + 密码;应用程序登录并存储每个设备的令牌。
4. 可选的 AI 教练访问:每个运动员在仪表板上铸造一个令牌(**设置 → 创建令牌**)供其自己的 MCP 客户端使用(参见 [MCP 服务器](#mcp-服务器可选))。
密码、每个设备的令牌(撤销一个被盗设备而不影响其他设备)和停用都在仪表板上进行管理。相同的操作作为 CLI 备用方案存在:
```bash
docker compose exec app python -m app.cli --help
```
## 暴露服务器
iOS 应用程序和仪表板只需要访问端口 8001 — 通过 HTTPS 或在您信任的网络上通过 HTTP。选项,最安全的方式是:
- **Tailscale(推荐):** 在服务器和每个手机上安装 Tailscale;客户端在 tailnet 内使用 `http://<machine-name>:8001`,无需将任何内容暴露给互联网。如果应用程序还必须适用于不在您的 tailnet 中的人,[Tailscale Funnel](https://tailscale.com/kb/1223/funnel) 可以发布端口(例如 `tailscale funnel --bg --https 8443 8001`)。
- **反向代理:** 在前面使用 Caddy / nginx / Traefik 和真正的证书,将请求转发到 `127.0.0.1:8001`。
- **仅限局域网:** 适用于试用,但应用程序在后台同步,因此离开房屋的手机需要上述选项之一。
如果服务器最终可公开访问:登录受到速率限制(5 次/分钟/IP),没有注册端点,管理员系统屏幕显示身份验证审计跟踪 — 但公开暴露仍然意味着 **每个帐户都需要强密码**。
## 备份
Postgres 卷是唯一值得备份的状态。主机上的每日 `pg_dump` 足够:
```bash
# crontab -e
30 3 * * * docker exec postgres__training-api pg_dump -U training-api training-api | gzip > /path/to/backups/training-api-$(date +\%F).sql.gz
```
将 `BACKUP_HOST_DIR` 指向 `.env` 中的该目录,管理员 **系统** 屏幕报告备份新鲜度(绿色 < 26 小时旧)。使用 `gunzip -c <dump>.sql.gz | docker exec -i postgres__training-api psql -U training-api training-api` 恢复。
## 配置
所有内容都通过 Docker Compose 中的环境变量进行配置:
| 变量 | 默认值 | 用途 |
|----------|---------|---------|
| `BOOTSTRAP_ADMIN_PASSWORD` | *(未设置)* | 首次运行的管理员密码;仅在管理员无密码时应用,因此稍后轮换它是一个空操作 |
| `BOOTSTRAP_ADMIN_USERNAME` | `admin` | 首次运行的管理员用户名 |
| `POSTGRES_PASSWORD` / `POSTGRES_USER` / `POSTGRES_DB` | `training-api` | 数据库凭证(不在 compose 网络外部发布;也作为 `DATABASE_URL` 提供给应用程序) |
| `API_PORT` | `8001` | API + 仪表板的宿主端口 |
| `BACKUP_HOST_DIR` | `./backups` | 具有 `pg_dump` 文件的主机目录,为系统屏幕只读安装 |
在 Docker 之外运行后端会读取 `backend/config/.env` 而不是 `.env`(模板:`backend/config/.env.example`)。
**从预认证版本升级:** 保留旧的 `API_KEY` 设置在 `backend/config/.env` 中 — 迁移将它作为管理员帐户拥有的令牌进行播种,因此现有客户端继续工作,同时您将它们迁移到每个用户令牌。
## Web 仪表板
`frontend/` 目录包含 React 19 + TypeScript SPA(Vite、TanStack Query、手滚 SVG 图表、Leaflet 路线地图)。Docker 构建将 `frontend/dist` 构建到镜像中,FastAPI 在 `/` 处提供 SPA 回退 — 相同来源,无 CORS。
作为普通用户登录会显示运动员屏幕(概览、日历、锻炼、计划、笔记、健康、队列)。管理员可以访问管理控制台:**用户**(帐户 CRUD、每个设备的令牌撤销、同步新鲜度)和 **系统**(备份新鲜度、DB 大小、身份验证活动日志)。
```bash
cd frontend
npm run dev # Vite 开发服务器在 :5173 上,代理 /api → localhost:8001
npx tsc -b # 类型检查
npm run build # 生产构建(也在 Docker 构建中运行)
```
## API
除 `/api/health` 和 `/api/auth/login` 之外的所有端点都需要 `Bearer` 令牌作为 `Authorization` 头中的令牌。令牌通过 `POST /api/auth/login` 发行(用户名 + 密码,速率限制 5 次/分钟/IP);`/api/admin/*` 还需要 `admin` 角色。
> **线路包装:** JSON 包装因资源而异 — 身份验证、管理员、反馈和日历使用 camelCase;锻炼、队列、计划和健康指标使用 snake_case。
### 身份验证
| 方法 | 端点 | 描述 |
|--------|----------|-------------|
| `POST` | `/api/auth/login` | 登录,铸造每个设备的令牌(错误凭证 → 401) |
| `GET` | `/api/auth/me` | 当前用户 + 他们的令牌 |
| `POST` | `/api/auth/password` | 更改自己的密码(撤销其他所有令牌) |
| `POST` | `/api/auth/tokens` | 铸造命名令牌(显示一次;可选 `expiresAt`) |
| `DELETE` | `/api/auth/tokens/{id}` | 撤销您自己的令牌之一 |
### 管理员
| 方法 | 端点 | 描述 |
|--------|----------|-------------|
| `GET` | `/api/admin/users` | 列出用户令牌统计信息 + 同步新鲜度 |
| `POST` | `/api/admin/users` | 创建用户 |
| `PATCH` | `/api/admin/users/{id}` | 激活/停用(停用会撤销所有令牌) |
| `POST` | `/api/admin/users/{id}/password` | 重置用户的密码 |
| `GET` | `/api/admin/users/{id}/tokens` | 列出用户的令牌 |
| `DELETE` | `/api/admin/users/{id}/tokens/{tid}` | 撤销单个令牌(一个被盗设备) |
| `GET` | `/api/admin/events` | 身份验证审计跟踪(登录、密码/令牌/用户更改;修剪 >365d 读取) |
| `GET` | `/api/admin/system` | 备份新鲜度、DB 大小、行计数、迁移头 |
### 锻炼
| 方法 | 端点 | 描述 |
|--------|----------|-------------|
| `POST` | `/api/workouts` | 创建或更新锻炼 |
| `GET` | `/api/workouts` | 列出锻炼(过滤器:`activity_type`、`start_after`、`start_before`、`plan_workout_id`、`limit`、`offset`) |
| `GET` | `/api/workouts/summary` | 按期间(`week`/`month`/`year`)和活动类型汇总统计 |
| `GET` | `/api/workouts/{id}` | 获取锻炼详情 |
| `GET` | `/api/workouts/{id}/splits` | 获取每个分段的分解 |
| `GET` | `/api/workouts/{id}/heartrate` | 获取心率样本 |
| `DELETE` | `/api/workouts/{id}` | 删除锻炼 |
### 训练队列
| 方法 | 端点 | 描述 |
|--------|----------|-------------|
| `POST` | `/api/queue` | 排队结构化锻炼(响应带有附加的 `validation` 键与计划警告) |
| `POST` | `/api/queue/batch` | 一次排队多个锻炼 → `{items, validation}` 信封 |
| `GET` | `/api/queue` | 列队项目(按 `status` 过滤) |
| `GET` | `/api/queue/pending` | 列队项目 |
| `PATCH` | `/api/queue/{id}` | 编辑队列项目的字段 |
| `PATCH` | `/api/queue/{id}/status` | 更新项目状态(`pending` / `fetched` / `synced` / `completed` / `skipped`) |
| `DELETE` | `/api/queue/{id}` | 删除队列项目 |
| `GET` | `/api/workouts/queue` | 应用程序端:获取待练作为 WorkoutKit 组合 |
| `PATCH` | `/api/workouts/queue/{id}` | 应用程序端:确认安装 → `synced` (从未降级为已完成/跳过) |
| `DELETE` | `/api/workouts/queue/{id}` | 应用程序端:标记项目为已同步(保留记录) |
### 锻炼操作
| 方法 | 端点 | 描述 |
|--------|----------|-------------|
| `GET` | `/api/workouts/actions` | 列出待处理编辑/删除操作 |
| `POST` | `/api/workouts/actions` | 创建编辑或删除操作 |
| `POST` | `/api/workouts/actions/batch` | 一次创建多个操作 |
| `DELETE` | `/api/workouts/actions/{id}` | 确认已处理的操作 |
### 设备库存
| 方法 | 端点 | 描述 |
|--------|----------|-------------|
| `PUT` | `/api/workouts/inventory` | 同步完整的设备端锻炼快照(幂等替换) |
| `GET` | `/api/workouts/inventory` | 获取存储的库存 |
### 错过锻炼反馈
| 方法 | 端点 | 描述 |
|--------|----------|-------------|
| `POST` | `/api/workouts/feedback` | 为错过的锻炼记录反馈(按 `workoutId` 更新;`action: "skip"` 退休队列项目) |
| `GET` | `/api/workouts/feedback` | 检索反馈历史(过滤器:`since`、`limit`、`action`) |
### 训练计划
| 方法 | 端点 | 描述 |
|--------|----------|-------------|
| `POST` | `/api/plans` | 创建训练计划 |
| `GET` | `/api/plans` | 列出计划(状态过滤器;包括计算出的 `progress` + `finishable`) |
| `GET` | `/api/plans/{id}` | 获取计划及其元数据 |
| `PATCH` | `/api/plans/{id}` | 更新计划字段 |
| `POST` | `/api/plans/{id}/complete` | 完成活跃计划(存储评级/反馈作为教练笔记) |
| `POST` | `/api/plans/{id}/validate` | 确定性计划检查 → `{plan_id, warnings, weeks}`(坡道、逐渐减少、冲突、约束违规) |
| `DELETE` | `/api/plans/{id}` | 删除计划(队列项目将 `plan_id` 设置为 null) |
| `GET` | `/api/plans/{id}/workouts` | 获取计划的所有排队锻炼 |
| `GET` | `/api/plans/{id}/schedule` | 读取计划的重复每周节奏,按日期解析为会议 |
| `PUT` | `/api/plans/{id}/schedule` | 设置节奏(警告排队冲突,不阻塞) |
| `DELETE` | `/api/plans/{id}/schedule` | 清除节奏 |
### 日历
| 方法 | 端点 | 描述 |
|--------|----------|-------------|
| `GET` | `/api/schedule/calendar?from=&to=` | 合并排队跑步和计划的力量训练的统一时间线,每个都有 `conflict` 标志 |
### 计划笔记
| 方法 | 端点 | 描述 |
|--------|----------|-------------|
| `POST` | `/api/plan-notes` | 添加跨对话的教练笔记 |
| `GET` | `/api/plan-notes` | 列出笔记(过滤器:`plan_id`、`kind`、`conversation_id`、`since_days`、`limit`) |
| `GET` | `/api/plan-notes/context` | 用于 LLM 消耗的压缩教练上下文(计划 + 笔记) |
| `GET` | `/api/plan-notes/{id}` | 获取笔记 |
| `PATCH` | `/api/plan-notes/{id}` | 更新笔记 |
| `DELETE` | `/api/plan-notes/{id}` | 删除笔记 |
### 健康指标
| 方法 | 端点 | 描述 |
|--------|----------|-------------|
| `POST` | `/api/health/metrics` | 批量更新每日健康指标(保留空字段) |
| `GET` | `/api/health/metrics` | 查询指标(必填:`start_date`,可选:`end_date`) |
## MCP 服务器(可选)
`mcp/` 目录包含一个 [FastMCP](https://github.com/jlowin/fastmcp) 服务器,用于向 AI 助手公开训练数据(例如 Claude)。
### 设置
```bash
cd mcp
cp config/.env.example config/.env
```
编辑 `mcp/config/.env` — 将 `TRAINING_API_KEY` 设置为帐户的令牌(在仪表板上铸造:**设置 → 创建令牌**):
```ini
TRAINING_API_URL=http://localhost:8001
TRAINING_API_KEY=your-api-token-here
```
### 运行
```bash
uv run start
```
默认传输方式是 stdio(适用于直接 MCP 客户端,如 Claude Desktop)。设置 `MCP_TRANSPORT=http` (带有可选 `MCP_HOST` / `MCP_PORT`)改为在 `/mcp` 处提供可流式 HTTP 服务 — 在反向代理或家庭服务器上很有用。
**多用户:** 来自用户的传入 HTTP MCP 请求的 `Authorization` 头直接转发到后端,因此每个调用者都作为自己的用户。设置 `REQUIRE_AUTH_HEADER=true` 完全禁用 `TRAINING_API_KEY` 回退。
## 开发
```bash
make up # 启动容器
make down # 停止容器
make build # 重建镜像
make logs # 尾随容器日志
make migrate # 手动运行数据库迁移
# 创建模型更改后创建新迁移
make create_migration m="添加新列"
# 后端测试(在容器内)
docker compose exec app python -m pytest
```
### 项目结构
```
├── docker-compose.yml # PostgreSQL + API 协调
├── .env.example # Compose 配置模板(复制到 .env)
├── .github/ # CI + GHCR 镜像工作流,README 截图
├── Makefile # 开发快捷方式
├── backend/
│ ├── Dockerfile # 多阶段构建:Node(前端)+ Python 3.13(uv);上下文 = 存储库根目录
│ ├── pyproject.toml # 依赖项(uv/hatch)
│ ├── config/.env.example # 外部运行时的环境模板
│ ├── app/
│ │ ├── main.py # FastAPI 应用程序(+ 提供 SPA 构建)
│ │ ├── config.py # 设置(pydantic-settings)
│ │ ├── auth.py # Bearer 令牌身份验证
│ │ ├── auth_events.py # 审计跟踪记录助手
│ │ ├── cli.py # 用户/令牌管理员 CLI
│ │ ├── database.py # SQLAlchemy 设置
│ │ ├── models/ # ORM 模型
│ │ ├── routes/ # API 端点
│ │ └── schemas/ # Pydantic 请求/响应模型
│ ├── migrations/ # Alembic 迁移
│ └── tests/ # API 测试(在容器内运行)
├── frontend/
│ └── src/
│ ├── components/ # 布局、共享 UI、图表基元、路线图
│ ├── lib/ # API 客户端、线路类型、身份验证上下文、查询钩子
│ ├── screens/ # 每个屏幕一个文件(运动员 + 管理员)
│ └── styles/ # 设计令牌 + 每个屏幕的 CSS
└── mcp/
├── pyproject.toml # MCP 依赖项
├── config/.env.example # MCP 环境模板
└── app/
├── main.py # FastMCP 服务器(stdio 或流式 HTTP)
├── tools/ # MCP 工具定义
└── services/ # API 客户端
```
## 许可
MIT
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器