uVersion
简体中文
下载 →

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 字段。 两者绝不同时出现。

分页

返回可能较长列表的路由接受查询参数 limitoffset?limit=20&offset=40)。响应包含 totalhas_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 模式的用户看不到它。

查询参数: limitoffset

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_repos capability
  • 409:该 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"
    }
  ]
}

可能的动作:addedmodifieddeleted。 对于 addedmodifiedchunks 数组包含 /upload-chunks 返回的哈希。 对于 deleted,省略 chunks

Response 200:

{
  "success": true,
  "data": {
    "commit_hash": "7f3a9b1c2d3e4f...",
    "files_changed": 3,
    "bytes_uploaded": 88080384,
    "bytes_deduped": 4194304,
    "revision": 47
  }
}

错误:

  • 400:消息为空、文件没有动作、引用了不存在的 chunk
  • 403:没有 checkin capability,或对其中某个文件没有 write permission
  • 409:某个被修改文件的锁已被另一用户持有,或并发提交(同一文件上的竞态条件)

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_holderlock_acquired_at 字段会被填充)
  • PERMISSION_DENIED:对该 path 没有 write permission
  • INVALID_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 过滤)、limitoffset

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:approvedchanges_requestedpending

关注列表

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_usersmanage_permissionsmanage_rules 等)。 下面是简要文档。大多数遵循标准 REST 模式(GET/POST/PUT/DELETE)。

路由Capability描述
GET/POST /api/admin/usersmanage_users列出 / 创建用户
PUT/DELETE /api/admin/users/{id}manage_users更新 / 删除
POST /api/admin/users/{id}/reset-passwordmanage_users重置密码,递增 token_version
GET/POST /api/admin/groupsmanage_users组管理
GET/POST /api/admin/permissions/{repo_id}manage_permissions按路径的 glob permission 规则
GET/POST /api/admin/repositories/{repo_id}/rulesmanage_rulescheckin 前的校验规则
GET/POST /api/admin/repositories/{repo_id}/webhooksmanage_rulesDiscord / Slack / Teams / custom
GET /api/admin/auditview_all_activity已过滤并分页的 audit log
GET /api/admin/statsview_all_activityStorage metrics、dedup ratio、增长
GET /api/admin/repositories/{repo_id}/gc/previewadmin预览 GC 而不执行
POST /api/admin/repositories/{repo_id}/gcadmin运行 garbage collection
POST /api/admin/locks/{id}/force-releaseforce_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含义何时
400Bad request参数无效、JSON 格式错误、请求体过大(在硬性 413 限制之前)
401Not authenticated令牌缺失、已过期、签名无效,或 token_version 不匹配
403Permission denied缺少 capability,或对该 path / 仓库没有 permission
404Resource not found仓库 / 文件 / 提交 / 用户不存在或不可访问
409Conflict锁已被持有、并发提交、违反唯一约束
413Payload too large请求体超过限制(/files 上为 1 GB,/builds/upload 上为 8 GB)
429Too many requests仅在 /auth/login 上(按用户的速率限制)
500Internal server errorDB 错误、IO 错误。已在服务器端记录日志;报告 bug 时请附上 timestamp。
503Service unavailable数据库无法访问(health check)或正在进行迁移