Wiki
REST API
uVersion 服务器 HTTP 端点的完整参考:auth、repos、files、locks、comments、watchlist、modification requests、builds、admin。
uVersion 服务器提供一个基于 JWT 认证的 JSON HTTP API。本页面记录了桌面客户端、编辑器插件
以及 uversion CLI 所使用的全部端点。你可以直接调用它们,将 uVersion 集成到你的内部工具
(自定义仪表盘、审计脚本、webhook 等)中。
约定
基础 URL
所有记录的路径都相对于你实例的 URL。示例使用
https://uversion.mygamestudio.com。请替换为你自己的地址。
必需的请求头
| 请求头 | 值 |
|---|---|
Authorization | 在除 /api/auth/* 和 /api/server-info(以及 /health)之外的所有 /api/* 路由上为 Bearer <jwt> |
Content-Type | 带 JSON 请求体的 POST/PUT 使用 application/json。二进制上传(build files)使用 application/octet-stream。 |
Accept | 推荐 application/json(服务器默认返回 JSON) |
统一的响应格式
所有路由都以下面的信封响应:
{
"success": true,
"data": { /* payload */ }
}
出错时:
{
"success": false,
"error": "Permission denied: capability 'manage_users' required"
}
success 字段始终存在,指示请求是否成功。
成功时存在 data 字段。失败时存在 error 字段。
两者绝不同时出现。
分页
返回可能较长列表的路由接受查询参数 limit 和 offset
(?limit=20&offset=40)。响应包含 total 和 has_more
以便于迭代。最大 limit:100。
速率限制
全局速率限制已被移除(见 CLAUDE.md 的 Security 部分)。只有 /api/auth/login
受速率限制:每个 username 每 15 分钟 5 次失败尝试。有效尝试不计入其中。
令牌版本控制
每个 JWT 都带有一个 tv 字段(token version),它对应于数据库中的 users.token_version。
auth 中间件在每个请求上都会检查该值。当用户执行 POST /api/auth/logout、
管理员重置其密码、或管理员停用其账户时,token_version 会递增,
从而在该用户的所有机器上立即使所有现有令牌失效。
Curl
要从终端调用 API:
TOKEN=$(curl -s -X POST https://uversion.mygamestudio.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"secret"}' | jq -r '.data.token')
curl -H "Authorization: Bearer $TOKEN" \
https://uversion.mygamestudio.com/api/repositories
认证
POST /api/auth/login
对用户进行认证并返回一个 30 天的 JWT。
请求体:
{
"username": "alice",
"password": "secret"
}
Response 200:
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"expires_at": "2026-06-13T14:30:00Z",
"user": {
"id": 12,
"username": "alice",
"email": "alice@mygamestudio.com",
"role": "lead",
"is_active": true
}
}
}
错误:
401:凭据无效或用户处于非活动状态429:该 username 在 15 分钟窗口内达到 5 次失败尝试
服务器始终执行 argon2id::verify_password(当用户不存在时对一个虚拟哈希执行),以抵御计时攻击。
无论用户是否存在,响应所需的时间都相同。
POST /api/auth/refresh
无需再次输入密码即可更新 JWT。
请求头: Authorization: Bearer <current_token>。无请求体。
Response 200: 与 /login 相同,过期时间延后 30 天并返回一个新令牌。
错误:
401:当前令牌无效、已过期,或token_version不匹配
POST /api/auth/logout
通过递增 users.token_version,使当前用户的所有令牌(在所有机器上)失效。
用户将不得不在所有地方重新登录。
Response 200: { "success": true }。
POST /api/auth/validate
检查令牌是否仍然有效。对于距离过期不足 7 天的令牌,服务器会在响应中包含一个
refreshed_token(自动延期)。客户端必须用它来替换旧令牌进行持久化。
Response 200:
{
"success": true,
"data": {
"valid": true,
"user_id": 12,
"expires_at": "2026-06-13T14:30:00Z",
"refreshed_token": "eyJhbGciOiJIUzI1NiIs..."
}
}
POST /api/auth/register
创建一个用户账户(公开路由,无需认证)。
POST /api/auth/change-password
更改当前用户的密码。递增 token_version,从而使所有旧令牌失效。
仓库
GET /api/repositories
列出当前用户可访问的仓库(通过 permissions 表过滤)。对某个仓库没有任何 permission 模式的用户看不到它。
查询参数: limit、offset。
Response 200:
{
"success": true,
"data": [
{
"id": 1,
"name": "hero-rpg",
"owner": "acme",
"description": "Main RPG project",
"current_revision": "7f3a9b1c2d...",
"file_count": 8432,
"size_bytes": 14211938405,
"created_at": "2026-01-15T10:00:00Z"
},
{
"id": 2,
"name": "shared-assets",
"owner": "acme",
"description": "Shared asset library",
"current_revision": "3a4b5c6d...",
"file_count": 412,
"size_bytes": 980000000,
"created_at": "2026-02-01T09:00:00Z"
}
]
}
GET /api/repositories/{repo_id}
某个仓库的详情,包括聚合统计。
Response 200:
{
"success": true,
"data": {
"id": 1,
"name": "hero-rpg",
"owner": "acme",
"description": "Main RPG project",
"current_revision": "7f3a9b1c2d...",
"file_count": 8432,
"size_bytes": 14211938405,
"dedup_ratio": 4.2,
"created_at": "2026-01-15T10:00:00Z",
"last_commit_at": "2026-05-15T08:30:00Z"
}
}
错误:
403:无权访问该仓库404:仓库不存在
POST /api/repositories
创建一个新仓库。需要 create_repos capability(默认为 admin)。
请求体:
{
"name": "new-project",
"description": "Optional description"
}
校验:
- name:1 到 64 个字符,字母数字 + 连字符 + 下划线。每个 owner 内唯一。
- description:0 到 1024 个字符。可选。
错误:
400:名称无效或过长403:缺少create_reposcapability409:该 owner 已存在同名仓库
文件
POST /api/files/{repo_id}/upload-chunks
上传一批二进制 chunk。相同的 chunk(按 SHA-256)会被自动去重。
服务器不会重新存储已存在的 chunk。响应会为每个 chunk 返回其哈希 + 大小 + 压缩大小,
供后续的 POST /commit 使用。
请求体:
{
"chunks": [
{ "data": "<base64-encoded chunk bytes>" },
{ "data": "<base64-encoded chunk bytes>" }
]
}
Response 200:
{
"success": true,
"data": {
"chunks": [
{ "hash": "abc123...", "size": 1048576, "compressed_size": 423152 },
{ "hash": "def456...", "size": 2097152, "compressed_size": 891204 }
]
}
}
限制: 请求体最大 1 GB(可通过 security.max_body_size_files 配置)。对于更大的文件,请拆分为多个批次。
POST /api/files/{repo_id}/commit
用一组文件及其 chunk 创建一个原子提交。要么所有文件都通过,要么都不通过。
请求体:
{
"message": "Updated main level + hero pose pass",
"files": [
{
"path": "Content/Maps/MainLevel.umap",
"action": "modified",
"chunks": ["abc123...", "def456..."]
},
{
"path": "Content/Characters/NewVillain.uasset",
"action": "added",
"chunks": ["fed789..."]
},
{
"path": "Content/OldAsset.uasset",
"action": "deleted"
}
]
}
可能的动作:added、modified、deleted。
对于 added 和 modified,chunks 数组包含 /upload-chunks 返回的哈希。
对于 deleted,省略 chunks。
Response 200:
{
"success": true,
"data": {
"commit_hash": "7f3a9b1c2d3e4f...",
"files_changed": 3,
"bytes_uploaded": 88080384,
"bytes_deduped": 4194304,
"revision": 47
}
}
错误:
400:消息为空、文件没有动作、引用了不存在的 chunk403:没有checkincapability,或对其中某个文件没有 write permission409:某个被修改文件的锁已被另一用户持有,或并发提交(同一文件上的竞态条件)
GET /api/files/{repo_id}/snapshot
返回仓库在当前 revision 的完整状态:所有文件、它们的 revision 及其 chunk。 客户端在 clone 和强制 sync 操作中使用。
查询参数:
revision(可选):某个历史 revision 的快照。默认:HEAD。
Response 200:
{
"success": true,
"data": {
"revision": 47,
"files": [
{
"path": "Content/Maps/MainLevel.umap",
"revision": 12,
"size_bytes": 84934656,
"chunks": ["abc123...", "def456..."]
}
]
}
}
GET /api/files/{repo_id}/content
下载某个文件在给定 revision 的内容(服务器会重新组装 chunk)。clone 和 sync 使用。
查询参数:
path:文件路径(相对于仓库根目录)revision(可选):revision 编号。默认:最新。
Response 200: 文件的二进制内容。
GET /api/files/{repo_id}/history
仓库提交的分页历史。
查询参数:
limit(1 到 100,默认 20)offset(默认 0)path(可选):按文件路径过滤(修改过该文件的提交)author(可选):按 username 过滤since(可选):ISO 8601 timestamp
Response 200:
{
"success": true,
"data": {
"commits": [
{
"hash": "7f3a9b1c...",
"author": "alice",
"author_id": 12,
"date": "2026-05-15T08:30:00Z",
"message": "Fixed lighting in main level",
"files_changed": 1,
"bytes_uploaded": 84934656
}
],
"total": 423,
"limit": 20,
"offset": 0,
"has_more": true
}
}
增量 sync
桌面客户端用于高效同步的端点:
GET /api/files/{repo_id}/sync:自某个 revision 以来变更的文件,用于增量 sync。GET /api/files/{repo_id}/deletions:服务器端删除的文件,用于将删除传播到本地。GET /api/files/{repo_id}/list:仓库的文件列表。POST /api/files/{repo_id}/checkout:获取锁并准备编辑。
锁
POST /api/locks/{repo_id}/acquire
对一组路径获取锁。各次获取相互独立:acquired 列表包含成功项,
failed 列表包含失败项及其原因。
请求体:
{
"paths": [
"Content/Maps/MainLevel.umap",
"Content/Characters/Hero.uasset"
]
}
Response 200:
{
"success": true,
"data": {
"acquired": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"file_id": 12345,
"file_path": "Content/Maps/MainLevel.umap",
"user_id": 12,
"username": "alice",
"acquired_at": "2026-05-15T14:30:00Z",
"last_heartbeat_at": "2026-05-15T14:30:00Z",
"expires_at": "2026-05-15T15:30:00Z"
}
],
"failed": [
{
"path": "Content/Characters/Hero.uasset",
"reason": "ALREADY_LOCKED",
"lock_holder": "bob",
"lock_acquired_at": "2026-05-15T13:00:00Z"
}
]
}
}
锁在没有 heartbeat 的情况下 60 分钟后过期。expires_at 字段反映该期限。
定期调用 /heartbeat 以续订锁,或调用 /release 以释放它。
failed 的原因:
ALREADY_LOCKED:另一用户持有该锁(lock_holder和lock_acquired_at字段会被填充)PERMISSION_DENIED:对该 path 没有 write permissionINVALID_PATH:路径格式错误(绝对路径、包含..等)
POST /api/locks/{repo_id}/release
释放你拥有的锁。请求体:
{
"paths": ["Content/Maps/MainLevel.umap"],
"force": false
}
force: true 需要 force_unlock capability。该操作会被审计。
POST /api/locks/{repo_id}/heartbeat
更新你的锁上的 last_heartbeat_at。对于希望管理员看到活动的长时间运行 CI 任务,
建议每 5 分钟调用一次。
GET /api/locks/{repo_id}/status
列出仓库中所有活动的锁,并 join users 和 files 以直接返回名称。
查询参数: user(按 username 过滤)、limit、offset。
Response 200:
{
"success": true,
"data": {
"locks": [
{
"file_path": "Content/Maps/MainLevel.umap",
"user_id": 12,
"username": "alice",
"acquired_at": "2026-05-15T14:30:00Z",
"last_heartbeat_at": "2026-05-15T16:00:00Z"
}
],
"total": 1
}
}
评论与审阅
GET /api/comments/{repo_id}/comments?commit_hash=<hash>
列出某个提交的评论,以线程形式(父 → 子)。
Response 200:
{
"success": true,
"data": [
{
"id": 42,
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"author_id": 12,
"author_username": "alice",
"body": "LGTM, ship it",
"parent_id": null,
"created_at": "2026-05-15T15:00:00Z",
"replies": [
{
"id": 43,
"author_username": "bob",
"body": "Thanks!",
"parent_id": 42,
"created_at": "2026-05-15T15:05:00Z"
}
]
}
]
}
POST /api/comments/{repo_id}/comments
创建一条评论。file_path 是可选的(提交评论 vs. 文件评论)。使用 parent_id 回复现有线程。
请求体:
{
"commit_hash": "7f3a9b1c...",
"file_path": "Content/Maps/MainLevel.umap",
"body": "LGTM, ship it",
"parent_id": null
}
POST /api/comments/{repo_id}/reviews
对某个提交提交审阅。需要 approve_changes capability。
请求体:
{
"commit_hash": "7f3a9b1c...",
"status": "approved",
"comment": "Lighting looks great, approving"
}
接受的 status:approved、changes_requested、pending。
关注列表
GET /api/watchlist/{repo_id}/watchlist
列出当前用户在该仓库上的 watch 模式。
Response 200:
{
"success": true,
"data": [
{
"id": 1,
"user_id": 12,
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"],
"created_at": "2026-04-10T10:00:00Z"
}
]
}
POST /api/watchlist/{repo_id}/watchlist
添加一个 watch 模式。请求体:
{
"pattern": "Content/Characters/Hero/**",
"notify_on": ["commit", "lock"]
}
可能的事件:commit(某次提交修改了匹配的文件)、
lock(获取了一个锁)、review(发布了一条审阅)。
DELETE /api/watchlist/{repo_id}/watchlist/{watch_id}
移除一个 watch 模式。返回 { "success": true }。
生产看板(任务)
旧的 modification-requests API 已被移除:生产看板吸收了它
(请求变成卡片,origin='request')。这些端点嵌套在
/api/tasks/{repo_id} 之下。
| 路由 | 描述 |
|---|---|
GET /api/tasks/{repo_id}/board | 完整看板:列 + 卡片 |
POST /api/tasks/{repo_id}/tasks | 创建一张卡片 |
PUT/DELETE /api/tasks/{repo_id}/tasks/{task_id} | 更新 / 删除一张卡片 |
PUT /api/tasks/{repo_id}/tasks/{task_id}/assignees | 为卡片分配用户 |
GET /api/tasks/{repo_id}/assignable-users | 可分配用户列表 |
POST /api/tasks/{repo_id}/columns | 配置看板的列 |
同样可用的子路由:卡片评论、指向 assets 和提交的链接,以及附件
(带封面图)。配置列需要 manage_board capability(admin / lead)。
构建
POST /api/builds/{repo_id}/upload?hash=<sha256>
将一个 build 文件上传到 <storage_path>/builds/<hash>。
服务器会用查询参数中提供的 SHA-256 与接收到的内容进行校验。如果该哈希在服务器端已存在,
会立即返回 200 而不重新复制(build storage 端去重)。
请求头: Content-Type: application/octet-stream。
请求体: 原始二进制(无 JSON 包装)。
限制: 每个文件 8 GB。
错误:
400:hash 查询参数缺失或格式错误,或计算出的 SHA-256 与提供的哈希不一致413:文件 > 8 GB
POST /api/builds/{repo_id}/publish
在上传完某个 build 的所有文件后注册其 manifest。需要 publish_builds capability。
请求体:
{
"version": "0.1.5-nightly",
"config": "Development",
"platform": "Win64",
"executable_path": "HeroRPG/Binaries/Win64/HeroRPG.exe",
"release_notes": "Nightly build of main branch, 2026-05-15",
"files": [
{ "path": "HeroRPG.exe", "hash": "abc123...", "size_bytes": 12345678 },
{ "path": "HeroRPG/Content/Paks/pak0.pak", "hash": "def456...", "size_bytes": 2147483648 }
]
}
GET /api/builds/{repo_id}
列出已发布的 build。查看下载链接需要 download_builds capability。
Response 200:
{
"success": true,
"data": [
{
"id": 5,
"version": "0.1.5-nightly",
"config": "Development",
"platform": "Win64",
"published_by": "ci-nightly",
"published_at": "2026-05-15T03:00:00Z",
"size_bytes": 2159829326,
"file_count": 142,
"release_notes": "Nightly build of main branch, 2026-05-15"
}
]
}
GET /api/builds/{repo_id}/{build_id}/file
从某个 build 下载一个文件。需要 download_builds capability。某个 build 的 manifest 可通过 GET /api/builds/{repo_id}/{build_id}/manifest 获取。
管理
所有 /api/admin/* 路由都要求用户至少拥有一项 admin capability
(视端点而定:manage_users、manage_permissions、manage_rules 等)。
下面是简要文档。大多数遵循标准 REST 模式(GET/POST/PUT/DELETE)。
| 路由 | Capability | 描述 |
|---|---|---|
GET/POST /api/admin/users | manage_users | 列出 / 创建用户 |
PUT/DELETE /api/admin/users/{id} | manage_users | 更新 / 删除 |
POST /api/admin/users/{id}/reset-password | manage_users | 重置密码,递增 token_version |
GET/POST /api/admin/groups | manage_users | 组管理 |
GET/POST /api/admin/permissions/{repo_id} | manage_permissions | 按路径的 glob permission 规则 |
GET/POST /api/admin/repositories/{repo_id}/rules | manage_rules | checkin 前的校验规则 |
GET/POST /api/admin/repositories/{repo_id}/webhooks | manage_rules | Discord / Slack / Teams / custom |
GET /api/admin/audit | view_all_activity | 已过滤并分页的 audit log |
GET /api/admin/stats | view_all_activity | Storage metrics、dedup ratio、增长 |
GET /api/admin/repositories/{repo_id}/gc/preview | admin | 预览 GC 而不执行 |
POST /api/admin/repositories/{repo_id}/gc | admin | 运行 garbage collection |
POST /api/admin/locks/{id}/force-release | force_unlock | 强制解锁他人的锁,会被审计 |
健康检查
GET /health
无需认证的端点,如果服务器能够访问数据库则返回 200 OK。
用于负载均衡器或监控的健康检查(Prometheus blackbox、Datadog synthetic 等)。
Response 200: OK(text/plain)。
Response 503: 如果数据库无法访问。
GET /api/server-info
公开且无需认证的端点,返回服务器元数据(版本等)。
与 /health 一样,它不受 auth 中间件保护。
错误格式
出错时,响应为 { "success": false, "error": "..." },并带有相应的 HTTP status。
error 字段是一条人类可读的消息。对于需要稳定的机器可读代码的客户端,
请解析其前缀(例如 "Permission denied: ..." 始终以 "Permission denied" 开头)。
| HTTP | 含义 | 何时 |
|---|---|---|
| 400 | Bad request | 参数无效、JSON 格式错误、请求体过大(在硬性 413 限制之前) |
| 401 | Not authenticated | 令牌缺失、已过期、签名无效,或 token_version 不匹配 |
| 403 | Permission denied | 缺少 capability,或对该 path / 仓库没有 permission |
| 404 | Resource not found | 仓库 / 文件 / 提交 / 用户不存在或不可访问 |
| 409 | Conflict | 锁已被持有、并发提交、违反唯一约束 |
| 413 | Payload too large | 请求体超过限制(/files 上为 1 GB,/builds/upload 上为 8 GB) |
| 429 | Too many requests | 仅在 /auth/login 上(按用户的速率限制) |
| 500 | Internal server error | DB 错误、IO 错误。已在服务器端记录日志;报告 bug 时请附上 timestamp。 |
| 503 | Service unavailable | 数据库无法访问(health check)或正在进行迁移 |