Developer Platform API
Developer Platform API 让你以编程方式管理平台上的应用与版本。它面向 AI agent、命令行工具和 CI/CD 流水线,无需经过网页门户即可发布和维护应用。
通过这套 API 你可以:
- 查看和创建应用、修改应用的仓库地址、删除应用
- 查看应用的仓库 Release 列表与版本列表,包括每个版本的审核时间线
- 创建版本(异步、两阶段:先校验、后确认)
- 撤回审核中的版本,或为已发布的版本申请下架
本文描述的每个接口均已实现。本文档即完整的接口说明。
1. 概览
服务地址
https://api-developer.terra-master.com
所有接口都位于 /v1 路径前缀下。
认证方式一览
每个 /v1/* 请求都通过 API Token 作为 Bearer 凭据认证:
-H "Authorization: Bearer ${TDP_API_TOKEN}"
仅接受 API Token。 不接受 Web 会话令牌。API Token 必须由开发者预先签发,完整值只在签发时展示一次。
Token 能访问什么
Token 代表签发它的开发者行事,只能访问该开发者自己的应用与版本。它可执行的操作集合由其权限(Scope)限定。
2. 服务地址与认证
2.1 通过环境变量配置
客户端从环境读取配置。使用两个变量:
| 变量 | 必需 | 默认值 | 说明 |
|---|---|---|---|
TDP_BASE_URL | 否 | https://api-developer.terra-master.com | API 服务地址。仅在指向非生产环境时覆盖。 |
TDP_API_TOKEN | 是 | — | API Token,作为 Bearer 凭据。 |
对每个客户端(包括 AI agent)都有三条规则:
-
绝不通过命令行参数传递 Token。 从环境读取。命令行参数会留在 shell 历史、进程列表和日志里。
-
绝不打印、记录或回显 Token 值——在输出、回复或对话中都不要。
-
也不要让 Token 出现在进程列表里。 在 shell 中
${TDP_API_TOKEN}会在curl启动之前展开,值就落在进程的argv里,而它是全局可读的(Linux 下/proc/<pid>/cmdline)。本参考中的示例为了可读性仍用内联 Header;在多用户机器上,建议先生成0600配置文件,再用curl --config:
umask 077
python3 -c 'import os, pathlib; p = pathlib.Path.home() / ".tdp_curlrc"; p.write_text("header = \"Authorization: Bearer %s\"\n" % os.environ["TDP_API_TOKEN"]); p.chmod(0o600)'
curl --fail-with-body --silent --show-error --config ~/.tdp_curlrc "${TDP_BASE_URL}/v1/apps"
TDP_BASE_URL 未设置时回退到生产地址。只有当 TDP_API_TOKEN 缺失时才要求用户配置:
export TDP_BASE_URL="${TDP_BASE_URL:-https://api-developer.terra-master.com}"
: "${TDP_API_TOKEN:?TDP_API_TOKEN is required}"
2.2 请求头
| Header | 值 | 必需 |
|---|---|---|
Authorization | Bearer ${TDP_API_TOKEN} | 是 |
Content-Type | application/json | 仅带 JSON 请求体的请求 |
Accept | application/json, application/problem+json | 建议 |
Accept-Language | zh / en(任意 zh* 变体 → 中文,其余 → 英文) | 可选 |
Accept-Language 控制人类可读响应文本的语言——错误的 title/detail/details.advice,以及任务步骤的 title/message/advice。机器可读字段(code、status、步骤字段名、details.retry_after)永远不做本地化。
2.3 先验证身份
执行任何其他操作之前,先调用 GET /v1/whoami,确认 Token 代表哪个开发者、携带哪些权限:
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/whoami"
developer 字段标识开发者;token.scopes 列出 Token 的权限。先做这一步,能把工作流深处一个费解的 403 变成立刻可处理的答案。
2.4 签发 API Token
API Token 在网页门户的开发者平台 API 密钥页面签发:
- 打开 API 密钥页面,选择创建密钥。
- 选择有效期与密钥应携带的权限(Scope),提交。
- 系统生成 Token。明文值只展示一次——立即复制。对话框关闭后无法再从列表查看。
- 把值存入将调用 API 的机器或 CI 运行器的
TDP_API_TOKEN环境变量。
2.5 Token 安全
- 把 Token 当密码对待。任何持有它的人都能以签发它的开发者身份行事(受其权限范围限制)。
- 绝不把 Token 提交到仓库、嵌入客户端或公开环境,也不出现在截图或聊天里。
- 没有"禁用/启用"开关。已泄露或不再需要的 Token 必须删除。
- Token 的有效期创建后不可修改。要换有效期,删除后新建。
- 只授予工作负载实际需要的最小权限集合。
3. 权限(Scope)
每个 API Token 携带一组创建时选定的权限。请求缺少接口所需权限时会被拒绝。
3.1 操作与权限的映射
| 操作 | 所需权限 |
|---|---|
| 查看应用 | app:read |
| 查看应用的 Release 列表 | app:read |
| 创建应用 | app:write |
| 修改应用的仓库地址 | app:write |
| 删除应用 | app:delete |
| 查看版本、版本创建任务或读取 Parse Result | version:read |
| 查看版本的审核时间线 | version:read |
| 创建、确认或取消版本创建任务 | version:write |
| 撤回审核或申请版本下架 | version:delete |
3.2 权限规则
- 调用
GET /v1/apps带app:read权限要求——但app:read是全局最小权限:任何至少持有一个有效权限的 Token 都自动满足它。 例如只勾了version:read的 Token 也能列出和读取应用,调用GET /v1/apps返回200。 - 要求被满足不等于 Token 被改变。
GET /v1/whoami返回的token.scopes就是签发时存储的内容,不会因此补上app:read。token.scopes里没有app:read的 Token 依然可以读应用。 - 通过平台 UI 签发的 Token 一定包含
app:read(UI 将其固定为常选项)。 version:write与version:delete各自包含version:read。 能创建或撤回版本的 Token 也能读取版本与任务。version:delete不包含version:write。 撤回审核与申请下架是破坏性操作,与创建分开授予。- App 权限不包含任何 Version 权限。 能管理应用不等于能访问版本。
Token 必须至少携带一个权限。在门户中,权限按组(App / Version)多选;同组内勾选了 write 或 delete 时 read 不可取消。
4. 标识符与状态值
4.1 标识符
这套 API 里有三种不同的标识符。混用它们是最常见的集成错误。
| 标识符 | 出现位置 | 含义 |
|---|---|---|
{app_id} | App 路径 | 应用实体 UUID,取自 App 响应的 id 字段。既不是业务字段 app_id,也不是应用名称。 |
{version_id} | Version 路径 | 版本实体 UUID,取自 Version 响应的 id 字段。不是版本 Tag。 |
{task_id} | Task 路径 | 创建版本时返回的 task_id。 |
Version Tag 是自由格式字符串,最长 128 字符,不要求递增或唯一——同一个 Tag 可以出现在多个版本上。请始终用实体 UUID 选择并操作版本。
4.2 Version 状态
| 值 | 含义 | 可用的破坏性操作 |
|---|---|---|
0 | 审核中 | 撤回审核 |
1 | 已通过(历史值——现行流程不产生、接口不返回) | 无 |
2 | 已发布 | 申请下架 |
3 | 已驳回 | 无 |
4 | 已撤回 | 无 |
5 | 下架申请中 | 等待管理员审核 |
6 | 已下架 | 无 |
7 | 强制下架 | 无 |
状态 1 是历史值。现行流程不会产生它,也不存在会让接口返回它的数据。它保留在表中是为了万一遇到时知道它是什么:把它当终态处理——不要对它发起任何破坏性操作,也不要把它当"已发布"。动作逻辑只建立在 0 和 2 上。
5. 响应约定
成功响应使用 application/json,取消版本创建任务除外(无响应体)。
| 操作 | HTTP 状态 | 响应体 |
|---|---|---|
| 查询身份 | 200 | {"developer": {...}, "token": {...}} |
| 查看应用或版本列表 | 200 | JSON 数组;没有数据时为 [] |
| 查看或创建应用 | 200 | App 对象 |
| 查看应用的 Release 列表 | 200 | {"total": N, "items": [...]} |
| 删除应用 | 200 | {"message":"deleted"} |
| 创建版本创建任务 | 202 | {"task_id":"<UUID>"} |
| 查询任务 | 200 | Task 状态对象 |
| 取消任务 | 204 | 无响应体 |
| 读取 Parse Result | 200 | Parse Result 对象 |
| 确认任务 | 200 | 已创建的 Version 对象,实体 ID 位于 id |
| 撤回审核 | 200 | {"message":"withdrawn"} |
| 申请下架 | 200 | {"message":"taken down"} |
两点值得注意:
- 从 JSON 字段判断结果,而不是只看状态码。 多个操作不论结果细节都返回
200。 - 只有
204保证响应体为空。 其余成功响应都必须读取。202尤其只代表工作被受理,不代表完成。
时间字段
整套 API 的时间格式有意不做统一。规则在每个对象内部一致,客户端类型定义不会出现 string | number 联合——但必须按对象用对格式:
| 对象 | 字段 | 格式 |
|---|---|---|
App(GET /v1/apps、GET /v1/apps/{app_id}、POST /v1/apps、PUT /v1/apps/{app_id}/repo) | created_at、deleted_at | Unix 秒(整数)。未删除时 deleted_at 为 0。 |
| Version(列表、单个、Confirm 响应) | created_at | Unix 秒(整数) |
Release(GET /v1/apps/{app_id}/releases) | created_at | Unix 秒(整数) |
审核时间线节点(GET /v1/apps/{app_id}/versions/{version_id}/reviews) | time | Unix 秒(整数);0 表示未到达该状态 |
Token 元信息(GET /v1/whoami → token) | created_at、expires_at、last_used_at | RFC 3339 字符串(UTC、Z 后缀、可能带小数秒)。last_used_at 可为 null。 |
整套 API 没有 updated_at——后端不对外暴露更新时间。
6. 接口参考
十七个接口,分五组。
| 分组 | 接口 |
|---|---|
| 身份 | GET /v1/whoami |
| 应用 | GET /v1/apps、GET /v1/apps/{app_id}、POST /v1/apps、PUT /v1/apps/{app_id}/repo、GET /v1/apps/{app_id}/releases、DELETE /v1/apps/{app_id} |
| 版本 | GET /v1/apps/{app_id}/versions、GET /v1/apps/{app_id}/versions/{version_id}、GET /v1/apps/{app_id}/versions/{version_id}/reviews |
| 版本创建 | POST /v1/apps/{app_id}/versions、GET .../tasks/{task_id}、GET .../tasks/{task_id}/result、POST .../tasks/{task_id}/confirm、POST .../tasks/{task_id}/cancel |
| 版本处置 | POST .../versions/{version_id}/withdraw、DELETE .../versions/{version_id} |
下文响应示例均为节选:只展示本文描述的字段。不要对未列出的字段建立逻辑。
6.1 身份
GET /v1/whoami
返回 Token 代表的开发者,以及 Token 自身的元信息。
所需权限: 有效 API Token(无额外权限要求)。
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/whoami"
响应 200 —— 节选:
{
"developer": {
"id": 12345,
"name": "Example Developer"
},
"token": {
"scopes": ["app:read", "app:write", "version:read", "version:write"],
"created_at": "2026-01-10T08:00:00Z",
"expires_at": "2027-01-10T08:00:00Z",
"last_used_at": "2026-01-10T10:12:00Z"
}
}
| 字段 | 说明 |
|---|---|
developer | Token 代表的开发者账户。所有资源访问都限定于该开发者。 |
token.scopes | Token 携带的权限,与签发时存储的完全一致。隐含的权限不会回显——见权限规则。 |
token.created_at、token.expires_at、token.last_used_at | Token 元信息,RFC 3339 字符串。last_used_at 在首次使用前为 null。 |
错误码: 401 110301。
6.2 应用
GET /v1/apps
列出开发者的应用。
所需权限: app:read。
查询参数
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
kind | string | all | 按包类型过滤。可选 all、deb、docker。 |
只接受普通形式的值。 请传
all、deb或docker。本接口响应里的展示格式(.deb/docker image)不是合法的过滤值,不要回填。该参数未定义其他取值。
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps?kind=all"
响应 200 —— JSON 数组;开发者没有应用时为 []。
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"app_id": "com.example.myapp",
"name": "My App",
"kind": ".deb",
"created_at": "<unix_seconds>"
}
]
kind有两种写法。 在本接口的响应里kind是展示格式——.deb或docker image。其他所有接口(创建应用、查看单个应用、修改仓库)的请求与响应都用普通形式deb/docker。比较或过滤时,不要把本列表的kind值与其他接口的kind值直接混用,先做归一化。
created_at 为 Unix 秒。保存 id 字段——所有应用和版本路径都用这个实体 UUID。按业务 app_id 选择应用:
export APP_ID="$({
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps?kind=all"
} | jq -r '.[] | select(.app_id == "com.example.myapp") | .id' | sed -n '1p')"
test -n "${APP_ID}" && test "${APP_ID}" != 'null'
printf 'APP_ID=%s\n' "${APP_ID}"
错误码: 401 110301、403 110401。
GET /v1/apps/{app_id}
返回单个应用。
所需权限: app:read。
| 路径参数 | 说明 |
|---|---|
app_id | 应用实体 UUID。 |
export APP_ID='550e8400-e29b-41d4-a716-446655440000'
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}"
响应 200 —— App 对象。与列表响应不同,此处 kind 用普通形式 deb / docker。created_at 为 Unix 秒;存活应用的 deleted_at 为 0。
错误码: 401 110301、403 110401、404 300501。
POST /v1/apps
创建应用。
所需权限: app:write。
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
app_id | string | 是 | 业务标识符,例如 com.example.myapp。 |
kind | string | 是 | deb 或 docker。 |
platform | string | 是 | x86_64 或 aarch64。 |
repo | string | 是 | 受支持代码托管平台上的 HTTPS 仓库地址。 |
APP_JSON="$({
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
-H 'Content-Type: application/json' \
--data '{
"app_id": "com.example.myapp",
"kind": "deb",
"platform": "x86_64",
"repo": "https://github.com/example/myapp"
}' \
"${TDP_BASE_URL}/v1/apps"
})"
printf '%s\n' "${APP_JSON}" | jq .
export APP_ID="$(printf '%s\n' "${APP_JSON}" | jq -er '.id')"
响应 200 —— 创建出的 App 对象。
保留响应中的 id。业务 app_id 只是标签,不能在后续请求路径中替代实体 UUID。
错误码: 400 100101、400 200201 / 300201(输入校验)、401 110301、403 110401、409 300602(该 App ID 已由你注册)、409 300603(该 App ID 已用于其他类型)、409 300604(该 App ID 属于其他开发者,任何人都无法再注册)。
PUT /v1/apps/{app_id}/repo
修改应用拉取包文件的来源仓库地址。
所需权限: app:write。
| 路径参数 | 说明 |
|---|---|
app_id | 应用实体 UUID。 |
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
repo | string | 是 | 新的仓库地址。必须为 HTTPS 协议、受支持代码托管平台上可识别的 owner/repo 形式。 |
curl --fail-with-body --silent --show-error \
-X PUT \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
-H 'Content-Type: application/json' \
--data '{"repo":"https://github.com/example/myapp-v2"}' \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/repo"
响应 200 —— 更新后的 App 对象(字段与查看单个应用一致,repo 为修改后的地址)。
两个值得知道的行为:
- 已有版本不受影响——包文件已落在对象存储。只有修改之后创建的版本才从新仓库抓取。
- 提交与当前值相同的地址是幂等操作。
错误码: 400 200201(JSON 格式错误或仓库地址无效)、401 110301、403 110401、404 300501。
GET /v1/apps/{app_id}/releases
拉取应用代码仓库(GitHub/Gitee)的 Release 列表(新到旧),每个 Release 附带文件(assets)列表。创建版本前可用它核对真实的 browser_download_url。
所需权限: app:read。
查询参数
| 名称 | 类型 | 说明 |
|---|---|---|
page | integer | 页码,从 1 开始。 |
per_page | integer | 每页条数。 |
refresh | string | 设为 true 时忽略缓存立即重新拉取。每个开发者每分钟限用 1 次。 |
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/releases?page=1&per_page=5"
响应 200 —— {"total": N, "items": [...]}。每个 item 携带上游 Release 的元数据——tag、name、created_at、prerelease、author——以及 files[],每个 file 携带 name 与 browser_download_url。items[].files[].browser_download_url 是创建版本时 browser_download_url 字段的合法取值。Release 的 created_at 为 Unix 秒。不要按仓库平台的命名习惯去猜(tag_name、assets)——本接口不使用它们。
能优先用仓库平台官方 API 就优先用——GitHub:GET https://api.github.com/repos/{owner}/{repo}/releases,Gitee:GET https://gitee.com/api/v5/repos/{owner}/{repo}/releases({owner}/{repo} 取自应用的 repo 地址)。只有官方 API 不可用(例如没有仓库访问凭据)时才调用本接口,且注意它有严格限流。
限流与缓存
- 服务端按 App 缓存完整列表 5 分钟。
- 只有触发完整拉取的请求消耗配额:每个开发者每 10 秒 1 次。缓存命中不限流。
- 单次完整拉取最多覆盖最近 5000 个 Release;超出时
total为截断后的快照大小。 - 配额耗尽时:普通拉取有旧缓存则降级返回旧数据,无缓存返回
429300801;强制刷新一律返回429。details.retry_after给出精确可重试秒数(普通拉取 ≤10、强制刷新 ≤60);details.advice给出修复建议。 - 上游仓库失败时:普通拉取回落旧缓存;强制刷新直接失败——不会用旧数据掩盖刷新未完成。失败返回
502340901。
错误码: 401 110301、403 110401、404 300501、429 300801 / 340801、502/503 340901。
DELETE /v1/apps/{app_id}
立即删除应用。这是破坏性且不可逆的操作。
所需权限: app:delete。
先读取应用,确认其 id 和 app_id 是目标对象:
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}" | jq '{id, app_id, name, status}'
确认后删除:
curl --fail-with-body --silent --show-error \
-X DELETE \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}"
响应 200:
{"message":"deleted"}
收到该响应即表示删除完成。不要重复请求。
错误码: 401 110301、403 110401、404 300501、409 300601。
无法删除时——409 300601。 应用存在审核中、已上架或下架申请中的版本。已上架的版本不能在开发者不知情的情况下消失,所以平台阻止删除。正确的行为:
- 停止。
- 告知用户哪些版本阻塞了删除、各自处于什么状态。
- 明确询问是否继续。
- 不要自动撤回审核、申请下架或重试删除。
- 只有用户确认后,才可按第 6.5 节处置相关版本,再在其状态允许后重新发起删除。
6.3 版本
GET /v1/apps/{app_id}/versions
列出一个应用的全部版本。
所需权限: version:read。
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions"
响应 200 —— Version 对象的 JSON 数组;应用没有版本时为 []。
[
{
"id": "9c8b7a6f-0000-0000-0000-000000000000",
"version": "1.0.0",
"status": 2,
"created_at": "<unix_seconds>"
}
]
created_at 为 Unix 秒。保存 id 字段以操作特定版本:
export VERSION_ID="$({
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions"
} | jq -r '.[] | select(.version == "1.0.0") | .id' | sed -n '1p')"
test -n "${VERSION_ID}" && test "${VERSION_ID}" != 'null'
printf 'VERSION_ID=%s\n' "${VERSION_ID}"
Tag 可以重复。 按 .version 选择可能命中多个版本。执行任何破坏性操作前,请重新读取目标版本,核对它的 id、version、status 和 created_at。
错误码: 401 110301、403 110401、404 300501。
GET /v1/apps/{app_id}/versions/{version_id}
返回单个版本。
所需权限: version:read。
| 路径参数 | 说明 |
|---|---|
app_id | 应用实体 UUID。 |
version_id | 版本实体 UUID。 |
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/${VERSION_ID}"
响应 200 —— Version 对象,包含 id、version、status、created_at。请先读取 status 再决定操作——见第 6.5 节。
错误码: 401 110301、403 110401、404 310501。
GET /v1/apps/{app_id}/versions/{version_id}/reviews
返回版本的审核时间线:覆盖其全部审核工单的时间线节点数组。
所需权限: version:read。
| 路径参数 | 说明 |
|---|---|
app_id | 应用实体 UUID。 |
version_id | 版本实体 UUID。 |
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/${VERSION_ID}/reviews"
响应 200 —— 时间线节点的 JSON 数组。
| 字段 | 说明 |
|---|---|
node | 节点类型。取值:SubmitVersion、SubmitVersionReviewing、SubmitVersionApproved、SubmitVersionRejected、WithdrawVersion、TakeDown、TakeDownReviewing、TakeDownApproved、TakeDownRejected、TakeDownDeListed、ForceTakeDown、VersionPublishing、VersionPublished。 |
comments | 审核意见;无意见时为空字符串。 |
time | 节点时间,Unix 秒;0 表示未到达该状态。 |
details.reviewer | 审核员名称;未分配时为空字符串。 |
details.handler | 开发者侧始终为空字符串。 不要依赖它。 |
错误码: 401 110301、403 110401、404 300501 / 310501。
6.4 版本创建
版本创建是异步、两阶段流程。它是这套 API 里最复杂的工作流,也是最容易让集成者意外的地方:发起它本身不会创建任何东西。
POST /v1/apps/{app_id}/versions → 202,返回 task_id(尚无 Version 实体)
GET .../tasks/{task_id} → 轮询 all_status
GET .../tasks/{task_id}/result → 核对包元数据(仅 all_status = 2 时)
POST .../tasks/{task_id}/confirm → 200,此时才产生 Version
任务保留时间:passed 但未 Confirm 的任务保留 1 小时;failed 或已 Confirm 的任务保留 10 分钟。每个开发者同时只能运行一个版本创建任务。
POST /v1/apps/{app_id}/versions
发起版本创建任务。
所需权限: version:write。
前置条件: 该应用不能已有审核中(status=0)的版本。若有,调用返回 400 200201——等该审核结束,或先按第 6.5 节撤回。
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
tag | string | 是 | 版本 Tag,自由格式,最长 128 字符。 |
browser_download_url | string | 是 | 包文件的直接下载地址。必须是含主机名的绝对 URL,最长 2048 字符,且为 https——仅当应用自身的 Repo 地址就是 http 时才接受 http。其 host 必须与应用的 Repo host 一致。 |
TASK_JSON="$({
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
-H 'Content-Type: application/json' \
--data '{
"tag": "1.0.0",
"browser_download_url": "https://github.com/example/myapp/releases/download/1.0.0/com.example.myapp_x86_64.deb"
}' \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions"
})"
printf '%s\n' "${TASK_JSON}" | jq .
export TASK_ID="$(printf '%s\n' "${TASK_JSON}" | jq -er '.task_id')"
响应 202:
{"task_id":"b6a1c9de-0000-0000-0000-000000000000"}
此时版本实体尚未创建。202 只代表任务被受理,不代表包有效。
错误码: 400 100101、400 200201(该应用已有版本在审核中,或其他输入校验失败)、401 110301、403 110401、404 300501、409(该开发者已有进行中的创建任务)、429 310801、503 990301。
GET /v1/apps/{app_id}/versions/tasks/{task_id}
轮询版本创建任务。
所需权限: version:read。
| 路径参数 | 说明 |
|---|---|
app_id | 应用实体 UUID。 |
task_id | 创建调用返回的任务 UUID。 |
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/tasks/${TASK_ID}" | jq .
响应 200 —— 任务状态对象。失败的任务不会产生 HTTP 错误:状态轮询始终返回 200,失败原因只在响应体里(见下)。
顶层字段: all_status、steps,以及 version_id(仅在 Confirm 成功后出现——见下)。
all_status 取值:
| 值 | 含义 | 该做什么 |
|---|---|---|
0 | waiting | 继续轮询。 |
1 | checking | 继续轮询。 |
2 | passed | 读取 Parse Result,然后 Confirm。 |
3 | failed | 停止。检查失败步骤的 message 和 advice。绝不 Confirm 失败的任务。 若没有任何步骤标记为 failed,则问题在平台侧,不在你的包——见 6.5.5。 |
version_id 仅在 Confirm 成功后出现(非空)。all_status = 2 且 version_id 为空字符串表示:任务已 passed,正在等你 Confirm。
steps 对象。 两个阶段;阶段与步骤的 title 按 Accept-Language 本地化(下表为英文兜底文案):
| 阶段 | 步骤字段 | 步骤 title | 执行语义 |
|---|---|---|---|
download_step | package_download_failed | Application Package Download | 顺序执行,任一 failed 即终止任务 |
invalid_package_format | Application Package Format Validation | 同上 | |
package_unzip_failed | Package Unzip | 同上 | |
parse_step | missing_required_files | Required Files Completeness | 并行校验,每次响应携带全部步骤结果 |
invalid_config_ini | config.ini JSON Format | 同上 | |
app_id_mismatch | config.ini ID Consistency | 同上 | |
invalid_application_type | config.ini Application Type | 同上 | |
platform_mismatch | config.ini Platform Consistency | 同上 | |
invalid_language_files | app.lang Format | 同上 | |
icon_validation_failed | Icon Compliance | 同上 |
每个步骤具体校验什么、失败阈值是多少,见第 6.5 节。
两级键都是固定枚举——上述键名永不改变,每次响应都携带全部键。每个步骤对象始终包含四个字段:
| 字段 | 说明 |
|---|---|
title | 本地化的步骤标题。 |
status | 与 all_status 同语义:0 waiting / 1 checking / 2 passed / 3 failed。 |
message | 出了什么问题。仅 status=3 时非空;否则为 ""。 |
advice | 该怎么处理。仅 status=3 时非空;可能多行。否则为 ""。 |
示例——一个失败的任务:
{
"all_status": 3,
"steps": {
"download_step": {
"title": "Download Step",
"package_download_failed": {
"title": "Application Package Download",
"status": 3,
"message": "download failed after repeated attempts",
"advice": "check that browser_download_url is reachable and its host matches the app's repo"
},
"invalid_package_format": {
"title": "Application Package Format Validation",
"status": 0,
"message": "",
"advice": ""
},
"package_unzip_failed": {
"title": "Package Unzip",
"status": 0,
"message": "",
"advice": ""
}
},
"parse_step": {
"title": "Parse Step",
"missing_required_files": {
"title": "Required Files Completeness",
"status": 0,
"message": "",
"advice": ""
}
}
}
}
(示例对 parse_step 做了截断——真实响应携带全部七个键。)
键集固定,因此可以直接索引失败步骤,不必按字典遍历——但不要依赖键的顺序:JSON 对象无序。把 message 和 advice 都展示给用户,而不是一句干巴巴的"提交失败"。
不要高频轮询。 建议 2 秒一次。all_status 是判断任务是否结束的唯一依据;不要从单个步骤的状态推断完成。
错误码: 401 110301、403 110401、404 310502。
GET /v1/apps/{app_id}/versions/tasks/{task_id}/result
返回 Parse Result——平台从包中提取出的元数据。仅在 all_status == 2 时调用。 提前调用会失败并返回 409 310601。
所需权限: version:read。
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/tasks/${TASK_ID}/result" | jq .
响应 200 —— Parse Result 对象。字段包括:
| 字段 | 说明 |
|---|---|
app_name | 从包中读取的应用名称。 |
app_desc | 从包中读取的应用描述。 |
version_number | 从包中读取的版本号。 |
platform | 目标架构。 |
file_size | 平台下载到的包字节数。 |
file_hash | 下载到的包字节的 SHA-256。 |
release_notes | 发布说明。 |
category | 应用分类。 |
icon | 图标。可能是较大的内联 data URL——不要假定它是短链接,也不要在终端完整打印。 |
这些值的来源:app_name、app_desc、release_notes 取自语言文件的 en-us 段(name、descript、release_note);version_number 取自 config.ini 的 version;category 取自 config.ini 的 category;file_hash 与 file_size 取自下载到的字节;platform 取自应用自身配置,不是包内 config.ini 的值。icon 是 {AppID}.svg 的原始字节,在 Confirm 时上传对象存储。version_number 以及你提交的资产信息(tag、browser_download_url)也只在 Confirm 时才写入版本记录。
Confirm 前核对这些字段。 这是"还没有创建任何东西"的最后时点:在这里发现架构或版本号错误,代价只是重试;之后才发现,代价是撤回加重新提交。
错误码: 401 110301、403 110401、404 310502、409 310601。
POST /v1/apps/{app_id}/versions/tasks/{task_id}/confirm
确认任务并创建版本。
所需权限: version:write。
只在 Parse Result 符合预期时才 Confirm。
VERSION_JSON="$({
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/tasks/${TASK_ID}/confirm"
})"
printf '%s\n' "${VERSION_JSON}" | jq .
export VERSION_ID="$(printf '%s\n' "${VERSION_JSON}" | jq -er '.id')"
响应 200 —— 创建出的 Version 对象;实体 UUID 位于 id。
Confirm 是幂等的。 网络结果不明后,用同一 task_id 重试;成功的重试返回首次确认创建的那个版本,不会重复。设计客户端时让"结果未知"导向重试,而不是另起任务。
新确认的版本从审核中(status=0)进入审核流程。
错误码: 401 110301、403 110401、404 310502、409 310601。
POST /v1/apps/{app_id}/versions/tasks/{task_id}/cancel
取消未确认的任务。
所需权限: version:write。
当 Parse Result 不符合预期、又不希望创建版本时使用。
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/tasks/${TASK_ID}/cancel"
响应 204 —— 无响应体。
任务立即删除;此后对该 task_id 的请求返回 404。已 Confirm 的任务不能取消。
错误码: 401 110301、403 110401、404 310502、409 310601。
6.5 应用包校验
包不由开发者上传:你只提供资产直链,由平台自行拉取字节。随后每个版本创建任务都会对该包执行十项固定校验。本节说明平台校验什么、阈值是多少,以及某项不通过时对应哪个 Step Key——让失败的任务直接告诉你该改什么,而不是让你猜。
任何一项不通过都会终止任务,且不会写入任何版本记录。不存在"警告级"的判定。
6.5.1 请求级校验
这些校验在任务产生之前执行,因此失败时返回错误响应,不会有 task_id。
| 校验项 | 规则 | 失败结果 |
|---|---|---|
| 应用归属 | 应用必须存在且属于 Token 所属开发者。 | 404 300501 |
| 请求体 | 必须是合法 JSON。 | 400 100101 |
tag | 必填,最长 128 字符。 | 400 200201 |
| 提交锁 | 该应用不能已有审核中的版本。 | 400 200201 |
browser_download_url | 必填;最长 2048 字符;必须是含主机名的绝对 URL;必须为 https(仅当应用自身的 Repo 地址就是 http 时才接受 http);主机名必须与应用仓库的主机名一致。 | 400 200201 |
| 单飞 | 同一开发者同时只允许一个进行中的创建任务。 | 409 |
| 并发容量 | 平台处理中的创建任务总量已达上限。 | 429 310801 |
| 关机排水 | 服务正在关闭,不再受理新任务。 | 503 990301 |
单飞的 409 在第 7.2 节中没有对应的 code;这一条请按 HTTP 状态码分支。
平台从不校验包的文件名、扩展名与 Content-Type。 包形态只由文件头决定(见 6.5.2)。
6.5.2 下载阶段 download_step
三个下载步骤顺序执行。第一次失败即终止任务:它之前的步骤报 passed,失败步骤及其之后的全部步骤——包括解析阶段的七项——报 waiting。
下载。 平台以 HTTP GET 拉取 browser_download_url。最终响应不是 200,或下载字节数超过 2 GiB,都会导致 package_download_failed。
格式识别仅对 deb 类应用执行,且只读文件头:
| 文件头 | 判定形态 |
|---|---|
AR(!<arch>) | 单包 .deb |
| gzip | 双包外层归档 .tar.gz |
| 其他 | 失败于 invalid_package_format |
docker 类应用没有识别步骤:一律按 gzip 归档解包。
结构与解包按形态分三种情况。
(a)deb 类应用 + 单包 .deb —— 包内必须存在下列路径(大小写敏感):
| 必要文件 | 包内路径 |
|---|---|
config.ini | usr/local/{AppID}/config.ini |
| 语言文件 | usr/local/{AppID}/{AppID}.lang |
| 图标 | usr/local/{AppID}/images/icons/{AppID}.svg |
文件未在对应路径找到,按缺失(missing_required_files)处理,而不是内容不合规。包损坏、截断或使用不支持的压缩方式,则失败于 package_unzip_failed。
(b)deb 类应用 + 双包 .tar.gz —— 外层归档内必须内嵌一个 .deb。平台优先取名为 {AppID}.deb 的成员;找不到精确名时,取第一个内部含 usr/local/{AppID}/ 目录的 .deb 成员。没有任何可用内嵌包则失败于 invalid_package_format;内嵌 .deb 成员超过 8 个,或任一成员(含平台不读取的成员)声明体积超过 2 GiB,则失败于 package_unzip_failed。
(c)docker 类应用 + .tar.gz —— 不要求任何目录结构;三个文件按文件名在归档内任意位置识别。归档损坏(含 gzip 流损坏)失败于 package_unzip_failed;任一成员声明体积超过 2 GiB 同样如此。
6.5.3 体积上限
| 对象 | 上限 | 超出时失败步骤 |
|---|---|---|
| 整包(下载到的字节数) | 2 GiB | package_download_failed |
.tar.gz 中任一成员声明的体积(含平台不读取的成员) | 2 GiB | package_unzip_failed |
平台读取的每个归档条目(config.ini、{AppID}.lang、{AppID}.svg) | 各 1 MiB | package_unzip_failed |
外层归档内嵌 .deb 个数 | 8 | package_unzip_failed |
| 图标文件内容 | 50 KB | icon_validation_failed |
图标有两级上限,含义不同:超过 1 MiB 属于条目超限,失败于解压;超过 50 KB 但不超过 1 MiB 属于图标内容不合规,失败于 icon_validation_failed。
这些是硬性拒绝阈值,不是警告:超过任一项即任务失败、不写入任何版本记录。2 GiB 与 8 个内嵌包这两条上限存在的目的,是让单个超大或畸形归档无法耗尽平台处理能力——因此触发它们的包会被直接拒绝,无论其内容本身是否合规。
6.5.4 解析阶段 parse_step
七项解析校验彼此独立;每次状态响应都携带全部七项结果,未执行到的报 waiting。某一项失败不会压制其他项——缺少 config.ini 时,平台仍会给出语言文件与图标的结论。
missing_required_files —— 三个必要文件都在包内(按 6.5.2 定位)。失败时一次性报告全部缺失文件,且这些文件对应的内容项保持 waiting。
invalid_config_ini —— 文件名为 config.ini,但内容按 JSON 解析。它必须是合法 JSON 对象,且下列十个字段必须存在且非空(null 与 "" 算空;false、0、[]、{} 算有效值),类型需与约定一致(例如布尔字段不能写成字符串):
| 必填字段 | 含义 |
|---|---|
id | 应用 ID |
icon | 应用图标 |
publisher | 发布者 |
exec | 是否需要启动 |
version | 应用版本——版本号(Version Number)的来源 |
low_version | 应用支持的最低系统版本 |
category | 应用类别,字符串数组 |
platform | 架构 |
application_type | 应用种类 |
user | 平台仅要求非空 |
此项失败时,id、platform、application_type 不给结论(保持 waiting),因为它们以 config.ini 可解析为前提。
app_id_mismatch —— config.ini 的 id 必须与应用的 App ID 完全相等(区分大小写)。失败时报告两个值。
invalid_application_type —— deb 类应用允许 deb 或 deb-TarGz;docker 类应用允许 docker。deb 类的两种取值都允许,与该次实际提交的是单包还是双包无关。
platform_mismatch —— 必须与应用的架构一致(x86_64 或 aarch64)。失败时报告两个值。
invalid_language_files —— {AppID}.lang 为 INI 格式,且必须含全部 14 个语言段:zh-cn、zh-hk、en-us、fr-fr、de-de、it-it、es-es、hu-hu、ja-jp、ko-kr、pl-pl、ru-ru、tr-tr、pt-pt。每个语言段的 name、auth、descript 必须非空;release_note 可选。不在这 14 种之内的语言段不校验,也不报错。
icon_validation_failed —— {AppID}.svg 必须:
- 是可解析的 XML;
- 体积不超过 50 KB;
- XML 起始标签总数不超过 50;
- 不含
script、foreignObject、iframe、object、embed(按标签名匹配,忽略命名空间前缀); - 不含以
on开头的属性(不区分大小写,含xlink命名空间下的); - 引用地址(
href/xlink:href)不以javascript:、vbscript:、file:、ftp:、about:、blob:开头——空值、以#开头的片段引用,以及http、https、data、相对路径都允许; - 不含 DOCTYPE、ENTITY 等 XML 指令;唯一允许的处理指令是
<?xml ...?>(目标名不区分大小写)。
注释与文本节点不限制。图标的语义不校验:根元素不必是 <svg>,viewBox、尺寸、颜色都不检查。
6.5.5 没有任何步骤失败的 failed
all_status = 3 且没有任何步骤标记为 failed,说明是平台自身故障,不是你的包——即 Task Fault。这种情况下没有失败步骤,也没有 advice。停止操作并上报;不要试图"修包",相同输入重试可能成功。
真正的包问题一定会指向唯一一个 Step Key。
6.5.6 平台不校验的事项
以下内容不在校验范围内,不要依赖平台替你把关:
tag与包内版本号 ——tag是自由标签,无格式、无排序、无唯一性要求,从不与config.ini的version比对。- 版本号的格式与查重 —— 版本号(Version Number)只要求非空,没有语义化版本约束,重复版本号也可以提交。
- 包的文件名、扩展名与
Content-Type—— 形态只看文件头。 - 包的签名、构建工具链、
.deb控制信息与可执行内容 —— 只有 6.5.2 列出的文件会被读取,其余成员一律不解析(但仍计入体积上限)。 config.ini中除id、application_type、platform之外的字段值 —— 其余必填字段只要求非空,不比对内容。特别地,config.ini的icon字段与实际图标文件不做比对。application_type与实际包形态的对应关系 —— 只校验取值属于该应用类型的允许集合,不校验它与单包/双包形态是否匹配。- 图标的语义正确性 —— 见 6.5.4。
- 多架构 / 多包 —— 一次任务只校验一个包文件,平台不合并或交叉校验多个架构的包。
- 语言文件的内容质量 —— 只校验完整性与非空性,且只有
en-us段会被采集进版本。 - 内容去重 —— 同一份包可以反复提交(只要该应用没有审核中的版本)。
6.5.7 容易误解的边界
- 下载阶段失败时,解析阶段七项全为
waiting—— 包还没能打开,谈不上内容校验。 - 双包
.tar.gz外层归档损坏,归入下载失败而不是解压失败。 格式识别只读文件头,因此损坏的归档失败于package_download_failed。 - "缺失"与"不合规"是两类失败。 文件根本没找到 →
missing_required_files;文件在但内容不合规 → 对应的内容项(invalid_config_ini、invalid_language_files或icon_validation_failed)。 config.ini不合规会连带三项不给结论 —— 见 6.5.4。- 任务类接口要求归属。 状态、结果、取消、确认四个接口都要求任务属于当前账号且属于路径中的应用;否则按"任务不存在"处理(
404310502)。 - 取消有边界。 下载或校验进行中可随时取消;确认已开始的任务不可取消,因为平台不会中断已开始的落库写入。
6.6 版本处置
没有"无条件立即删除版本"的接口。 删除版本等于移除用户可能已安装的包。因此处置依赖状态:先读取版本的 status,再选择有效操作。
VERSION_JSON="$({
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/${VERSION_ID}"
})"
printf '%s\n' "${VERSION_JSON}" | jq '{id, version, status, created_at}'
| 当前状态 | 有效操作 |
|---|---|
0 审核中 | 撤回审核 |
2 已发布 | 申请下架 |
| 其他 | 无可用的破坏性操作 |
撤回审核中的 Version
接口: POST /v1/apps/{app_id}/versions/{version_id}/withdraw
仅适用于 status=0。
所需权限: version:delete。
撤回不可逆,成功后版本变为已撤回(status=4)。
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
-H 'Content-Type: application/json' \
--data '{"comments":"需要修改部分功能,暂时撤回审核"}' \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/${VERSION_ID}/withdraw"
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
comments | string | 是 | 撤回原因。 |
响应 200:
{"message":"withdrawn"}
错误码: 400 220201(输入校验失败,例如缺少撤回原因)、401 110301、403 110401、404 310501、409 220601。
申请下架已发布的 Version
接口: DELETE /v1/apps/{app_id}/versions/{version_id}
仅适用于 status=2。
所需权限: version:delete。
此处的 DELETE 不会删除版本。它创建下架申请,把版本置为下架申请中(status=5),等待管理员审核。
curl --fail-with-body --silent --show-error \
-X DELETE \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
-H 'Content-Type: application/json' \
--data '{"comments":"发现严重安全漏洞,需要下架修复"}' \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/${VERSION_ID}"
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
comments | string | 是 | 下架申请原因。 |
响应 200:
{"message":"taken down"}
该响应表示申请已提交。不表示版本已下架。不要重复提交。
错误码: 400 220201、401 110301、403 110401、404 310501、409 220601。
7. 错误处理
7.1 错误格式
失败响应使用 application/problem+json,遵循 RFC 7807。所有错误都包含以下稳定字段:
| 字段 | 含义 |
|---|---|
status | HTTP 状态码——传输元数据;一定是该 code 注册允许的状态之一。 |
code | 稳定的六位、机器可读业务错误码。优先按它分支。 |
title | 简短错误类型。 |
detail | 本次错误的具体原因。 |
instance | 发生错误的请求路径。 |
Domain 错误可能额外包含 details(其中 details.advice 是修复建议,限流错误带 details.retry_after)。字段校验错误可能额外包含 errors。两者不是所有错误都具备——不要假定它们一定存在。
本地化。 自然语言字段(title、detail、details.advice)按请求的 Accept-Language 头渲染:任意 zh* 变体 → 中文,其余 → 英文。不同客户端看到的措辞不同,绝不要按它分支。机器字段(code、status、details.retry_after、步骤字段名)永不随语言变化。
无效或缺失的 API Token:
{
"title": "Invalid Token",
"status": 401,
"code": 110301,
"detail": "invalid api token",
"instance": "/v1/apps"
}
缺少权限:
{
"title": "Permission Denied",
"status": 403,
"code": 110401,
"detail": "missing scope \"app:read\"",
"instance": "/v1/apps"
}
版本创建任务不存在、不属于当前开发者/应用、已取消或已过期:
{
"title": "Version Creation Task Not Found",
"status": 404,
"code": 310502,
"detail": "version creation task is not found",
"instance": "/v1/apps/550e8400-e29b-41d4-a716-446655440000/versions/tasks/550e8400-e29b-41d4-a716-446655440001"
}
7.2 错误码
code | HTTP 状态 | 含义 |
|---|---|---|
100101 | 400 | JSON 无法解析或请求结构错误 |
100102 | 413 | 请求体超过服务器大小上限 |
110301 | 401 | Token 缺失、非法、过期,或开发者被冻结 |
110401 | 403 | Token 缺少接口要求的权限 |
110501 | 404 | API Token 不存在或已被删除 |
200201 | 400 | 开发者输入违反业务规则;结合 title、detail 判断具体原因 |
220201 | 400 | 审核操作输入校验失败,例如撤回原因缺失 |
220501 | 404 | 审核记录不存在或不属于当前开发者 |
220601 | 409 | 当前审核状态不允许目标转换 |
220701 | 409 | 审核操作违反业务规则,例如对非已发布版本强制下架 |
300201 | 400 | App 字段校验失败,例如必填字段缺失或 Repo 不合法 |
300501 | 404 | 应用不存在或不属于当前开发者 |
300601 | 409 | 应用存在审核中、已上架或下架申请中的版本,暂不可删除 |
300602 | 409 | 该 App ID 已由你在平台上注册过 |
300603 | 409 | 该 App ID 已用于其他类型 |
300604 | 409 | 该 App ID 属于其他开发者,任何人都无法再注册 |
300801 | 429 | 应用 Release 列表请求超过频率限制 |
300901 | 502/503 | 应用依赖的上游服务失败 |
310501 | 404 | 版本不存在或不属于指定应用 |
310502 | 404 | 版本创建任务不存在、不可见或已过期 |
310601 | 409 | 任务当前状态不允许读取结果、取消或确认 |
310801 | 429 | 版本创建任务创建达到容量限制 |
340201 | 400 | 仓库 URL 或主机校验失败:不是可识别的 owner/repo 形式,或主机不受支持 |
340801 | 429 | 上游 Repo 配额耗尽或请求被限流 |
340901 | 502/503 | 查询应用 Release 列表时上游 Repo 请求失败 |
990301 | 503 | 服务正在关闭,暂不接收新任务 |
991001 | 500 | 未预期内部错误 |
991002 | 500 | 数据库等内部故障 |
每个 code 都有固定、注册过的 HTTP 状态集合;例如 200201 恒为 400,绝不会以 409 返回。单个 code 仍可能合法对应多个状态(见 300901、340901);两者都要读,但分支以 code 为准。
7.3 错误处理规则
按以下顺序处理:
- 先读整数
code。code是稳定的业务身份;status是传输元数据,一定是该code注册允许的状态之一。不要只匹配title或detail文本——它们已本地化,不同客户端看到的措辞不同。 - 记录
code、title、detail和instance。若存在details或errors一并记录;details.advice是失败时的修复建议。 401/110301—— 停止操作,要求提供有效 Token。不要盲目重试;对无效 Token 的重试循环只会烧掉限流配额。403/110401—— 停止操作,报告detail中缺少的权限。不要通过查询其他资源探测权限;GET /v1/whoami是查看权限的正规途径。404—— 从应用或版本列表重新获取实体 ID;ID 可能已过期或属于其他资源。任务的310502也可能表示任务已取消或过期,此时需要重新创建任务。409—— 用code区分普通业务校验失败与任务/审核状态冲突。重新读取资源状态后再决定。绝不无限循环重试。429/300801/340801—— 等待details.retry_after给出的秒数,再重试 Release 列表或上游仓库相关请求。绝不并发轰炸。502/340901—— 查询 Release 列表时上游 Repo 失败。保留错误信息并做有限重试。包下载或解析失败不会返回 HTTP 错误——它们表现为任务步骤失败(见第 6.5 节)。503/990301—— 服务正在关闭。稍后用相同输入重新创建任务。500/991001—— 停止自动化操作并上报。响应不会暴露数据库、网络、凭据等内部错误。
8. 完整工作流
以下脚本端到端地创建一个版本。需要 curl 与 jq。
set -euo pipefail
: "${TDP_BASE_URL:?TDP_BASE_URL is required}"
: "${TDP_API_TOKEN:?TDP_API_TOKEN is required}"
: "${APP_ID:?APP_ID is required}"
TAG='1.0.0'
BROWSER_DOWNLOAD_URL='https://github.com/example/myapp/releases/download/1.0.0/com.example.myapp_x86_64.deb'
TASK_JSON="$({
jq -n \
--arg tag "${TAG}" \
--arg browser_download_url "${BROWSER_DOWNLOAD_URL}" \
'{tag: $tag, browser_download_url: $browser_download_url}' |
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
-H 'Content-Type: application/json' \
--data-binary @- \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions"
})"
TASK_ID="$(printf '%s\n' "${TASK_JSON}" | jq -er '.task_id')"
printf 'Version Creation Task: %s\n' "${TASK_ID}"
while true; do
STATUS_JSON="$({
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/tasks/${TASK_ID}"
})"
ALL_STATUS="$(printf '%s\n' "${STATUS_JSON}" | jq -er '.all_status')"
case "${ALL_STATUS}" in
0|1)
sleep 2
;;
2)
break
;;
3)
printf '%s\n' "${STATUS_JSON}" | jq . >&2
printf 'Version Creation Task failed; do not confirm it.\n' >&2
exit 1
;;
*)
printf 'Unexpected all_status: %s\n' "${ALL_STATUS}" >&2
exit 1
;;
esac
done
RESULT_JSON="$({
curl --fail-with-body --silent --show-error \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/tasks/${TASK_ID}/result"
})"
printf 'Parse Result:\n'
printf '%s\n' "${RESULT_JSON}" | jq .
VERSION_JSON="$({
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/tasks/${TASK_ID}/confirm"
})"
VERSION_ID="$(printf '%s\n' "${VERSION_JSON}" | jq -er '.id')"
printf 'Created VERSION_ID=%s\n' "${VERSION_ID}"
printf '%s\n' "${VERSION_JSON}" | jq .
如果 Parse Result 不符合预期,不要 Confirm。取消任务:
curl --fail-with-body --silent --show-error \
-X POST \
-H "Authorization: Bearer ${TDP_API_TOKEN}" \
"${TDP_BASE_URL}/v1/apps/${APP_ID}/versions/tasks/${TASK_ID}/cancel"
9. 标准与约定
| 方面 | 约定 |
|---|---|
| 传输 | HTTPS |
| 认证 | Authorization 头中的 Bearer 方案(RFC 6750) |
| API 版本 | 路径前缀中的主版本号(/v1) |
| 内容类型 | 请求与成功响应使用 application/json |
| 错误 | application/problem+json,RFC 7807 |
| 错误识别 | 六位整数 code;title 与 detail 是已本地化的人类可读文本,不同客户端可能看到不同措辞 |
| 时间戳 | 不统一。 业务对象(App、Version、Release、审核时间线)用 Unix 秒整数;Token 元信息(GET /v1/whoami → token)用 RFC 3339 字符串。见时间字段。无 updated_at。 |
有意偏离常规的地方
以下逐条点明,避免意外。每条都是这套 API 的有意设计,不是疏漏:
- 多数集合接口不分页。
GET /v1/apps与GET /v1/apps/{app_id}/versions以裸 JSON 数组返回完整列表——没有{items, pagination}包装、没有分页参数。唯一例外是GET /v1/apps/{app_id}/releases,它接受page/per_page且有严格限流。 - 枚举用整数,不用字符串。见 Version 状态。
- 时间戳有两种形态。 业务对象用 Unix 秒,Token 元信息用 RFC 3339 字符串。这是有意为之;见时间字段。
kind有两种写法。 应用列表响应里是展示格式.deb/docker image;其他所有接口用deb/docker。见第 6.2 节。- 路径用 UUID,请求体用业务 ID。 路径中的
{app_id}永远是实体 UUID;人类可读的业务标识以app_id请求体字段传递。两者绝不可互换。 - 对版本的
DELETE不是删除。 它创建下架申请。见第 6.5 节。 - 没有幂等键。 Confirm 按
task_id幂等,重复提交当前仓库地址也是幂等的;其余操作请避免盲目重试,先重新读取状态。
10. 资源
| 资源 | 说明 |
|---|---|
| API 密钥页面 | 在开发者平台网页门户签发和删除 API Token。 |
| Accept-Language | 控制人类可读响应文本的语言。见第 2.2 节。 |
本文档即 Agent API 的完整接口说明。