Skip to main content
Version: TOS 7

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.comAPI 服务地址。仅在指向非生产环境时覆盖。
TDP_API_TOKEN是—API Token,作为 Bearer 凭据。

对每个客户端(包括 AI agent)都有三条规则:

  1. 绝不通过命令行参数传递 Token。 从环境读取。命令行参数会留在 shell 历史、进程列表和日志里。

  2. 绝不打印、记录或回显 Token 值——在输出、回复或对话中都不要。

  3. 也不要让 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值必需
AuthorizationBearer ${TDP_API_TOKEN}是
Content-Typeapplication/json仅带 JSON 请求体的请求
Acceptapplication/json, application/problem+json建议
Accept-Languagezh / 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 密钥页面签发:

  1. 打开 API 密钥页面,选择创建密钥。
  2. 选择有效期与密钥应携带的权限(Scope),提交。
  3. 系统生成 Token。明文值只展示一次——立即复制。对话框关闭后无法再从列表查看。
  4. 把值存入将调用 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 Resultversion: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": {...}}
查看应用或版本列表200JSON 数组;没有数据时为 []
查看或创建应用200App 对象
查看应用的 Release 列表200{"total": N, "items": [...]}
删除应用200{"message":"deleted"}
创建版本创建任务202{"task_id":"<UUID>"}
查询任务200Task 状态对象
取消任务204无响应体
读取 Parse Result200Parse 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_atUnix 秒(整数)。未删除时 deleted_at 为 0。
Version(列表、单个、Confirm 响应)created_atUnix 秒(整数)
Release(GET /v1/apps/{app_id}/releases)created_atUnix 秒(整数)
审核时间线节点(GET /v1/apps/{app_id}/versions/{version_id}/reviews)timeUnix 秒(整数);0 表示未到达该状态
Token 元信息(GET /v1/whoami → token)created_at、expires_at、last_used_atRFC 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"
}
}
字段说明
developerToken 代表的开发者账户。所有资源访问都限定于该开发者。
token.scopesToken 携带的权限,与签发时存储的完全一致。隐含的权限不会回显——见权限规则。
token.created_at、token.expires_at、token.last_used_atToken 元信息,RFC 3339 字符串。last_used_at 在首次使用前为 null。

错误码: 401 110301。

6.2 应用​

GET /v1/apps​

列出开发者的应用。

所需权限: app:read。

查询参数

名称类型默认值说明
kindstringall按包类型过滤。可选 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_idstring是业务标识符,例如 com.example.myapp。
kindstring是deb 或 docker。
platformstring是x86_64 或 aarch64。
repostring是受支持代码托管平台上的 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。

请求体

字段类型必填说明
repostring是新的仓库地址。必须为 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。

查询参数

名称类型说明
pageinteger页码,从 1 开始。
per_pageinteger每页条数。
refreshstring设为 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 为截断后的快照大小。
  • 配额耗尽时:普通拉取有旧缓存则降级返回旧数据,无缓存返回 429 300801;强制刷新一律返回 429。details.retry_after 给出精确可重试秒数(普通拉取 ≤10、强制刷新 ≤60);details.advice 给出修复建议。
  • 上游仓库失败时:普通拉取回落旧缓存;强制刷新直接失败——不会用旧数据掩盖刷新未完成。失败返回 502 340901。

错误码: 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。 应用存在审核中、已上架或下架申请中的版本。已上架的版本不能在开发者不知情的情况下消失,所以平台阻止删除。正确的行为:

  1. 停止。
  2. 告知用户哪些版本阻塞了删除、各自处于什么状态。
  3. 明确询问是否继续。
  4. 不要自动撤回审核、申请下架或重试删除。
  5. 只有用户确认后,才可按第 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 节撤回。

请求体

字段类型必填说明
tagstring是版本 Tag,自由格式,最长 128 字符。
browser_download_urlstring是包文件的直接下载地址。必须是含主机名的绝对 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 取值:

值含义该做什么
0waiting继续轮询。
1checking继续轮询。
2passed读取 Parse Result,然后 Confirm。
3failed停止。检查失败步骤的 message 和 advice。绝不 Confirm 失败的任务。 若没有任何步骤标记为 failed,则问题在平台侧,不在你的包——见 6.5.5。

version_id 仅在 Confirm 成功后出现(非空)。all_status = 2 且 version_id 为空字符串表示:任务已 passed,正在等你 Confirm。

steps 对象。 两个阶段;阶段与步骤的 title 按 Accept-Language 本地化(下表为英文兜底文案):

阶段步骤字段步骤 title执行语义
download_steppackage_download_failedApplication Package Download顺序执行,任一 failed 即终止任务
invalid_package_formatApplication Package Format Validation同上
package_unzip_failedPackage Unzip同上
parse_stepmissing_required_filesRequired Files Completeness并行校验,每次响应携带全部步骤结果
invalid_config_iniconfig.ini JSON Format同上
app_id_mismatchconfig.ini ID Consistency同上
invalid_application_typeconfig.ini Application Type同上
platform_mismatchconfig.ini Platform Consistency同上
invalid_language_filesapp.lang Format同上
icon_validation_failedIcon 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.iniusr/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 GiBpackage_download_failed
.tar.gz 中任一成员声明的体积(含平台不读取的成员)2 GiBpackage_unzip_failed
平台读取的每个归档条目(config.ini、{AppID}.lang、{AppID}.svg)各 1 MiBpackage_unzip_failed
外层归档内嵌 .deb 个数8package_unzip_failed
图标文件内容50 KBicon_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 必须:

  1. 是可解析的 XML;
  2. 体积不超过 50 KB;
  3. XML 起始标签总数不超过 50;
  4. 不含 script、foreignObject、iframe、object、embed(按标签名匹配,忽略命名空间前缀);
  5. 不含以 on 开头的属性(不区分大小写,含 xlink 命名空间下的);
  6. 引用地址(href / xlink:href)不以 javascript:、vbscript:、file:、ftp:、about:、blob: 开头——空值、以 # 开头的片段引用,以及 http、https、data、相对路径都允许;
  7. 不含 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。
  • 任务类接口要求归属。 状态、结果、取消、确认四个接口都要求任务属于当前账号且属于路径中的应用;否则按"任务不存在"处理(404 310502)。
  • 取消有边界。 下载或校验进行中可随时取消;确认已开始的任务不可取消,因为平台不会中断已开始的落库写入。

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"

请求体

字段类型必填说明
commentsstring是撤回原因。

响应 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}"

请求体

字段类型必填说明
commentsstring是下架申请原因。

响应 200:

{"message":"taken down"}

该响应表示申请已提交。不表示版本已下架。不要重复提交。

错误码: 400 220201、401 110301、403 110401、404 310501、409 220601。


7. 错误处理​

7.1 错误格式​

失败响应使用 application/problem+json,遵循 RFC 7807。所有错误都包含以下稳定字段:

字段含义
statusHTTP 状态码——传输元数据;一定是该 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 错误码​

codeHTTP 状态含义
100101400JSON 无法解析或请求结构错误
100102413请求体超过服务器大小上限
110301401Token 缺失、非法、过期,或开发者被冻结
110401403Token 缺少接口要求的权限
110501404API Token 不存在或已被删除
200201400开发者输入违反业务规则;结合 title、detail 判断具体原因
220201400审核操作输入校验失败,例如撤回原因缺失
220501404审核记录不存在或不属于当前开发者
220601409当前审核状态不允许目标转换
220701409审核操作违反业务规则,例如对非已发布版本强制下架
300201400App 字段校验失败,例如必填字段缺失或 Repo 不合法
300501404应用不存在或不属于当前开发者
300601409应用存在审核中、已上架或下架申请中的版本,暂不可删除
300602409该 App ID 已由你在平台上注册过
300603409该 App ID 已用于其他类型
300604409该 App ID 属于其他开发者,任何人都无法再注册
300801429应用 Release 列表请求超过频率限制
300901502/503应用依赖的上游服务失败
310501404版本不存在或不属于指定应用
310502404版本创建任务不存在、不可见或已过期
310601409任务当前状态不允许读取结果、取消或确认
310801429版本创建任务创建达到容量限制
340201400仓库 URL 或主机校验失败:不是可识别的 owner/repo 形式,或主机不受支持
340801429上游 Repo 配额耗尽或请求被限流
340901502/503查询应用 Release 列表时上游 Repo 请求失败
990301503服务正在关闭,暂不接收新任务
991001500未预期内部错误
991002500数据库等内部故障

每个 code 都有固定、注册过的 HTTP 状态集合;例如 200201 恒为 400,绝不会以 409 返回。单个 code 仍可能合法对应多个状态(见 300901、340901);两者都要读,但分支以 code 为准。

7.3 错误处理规则​

按以下顺序处理:

  1. 先读整数 code。 code 是稳定的业务身份;status 是传输元数据,一定是该 code 注册允许的状态之一。不要只匹配 title 或 detail 文本——它们已本地化,不同客户端看到的措辞不同。
  2. 记录 code、title、detail 和 instance。若存在 details 或 errors 一并记录;details.advice 是失败时的修复建议。
  3. 401 / 110301 —— 停止操作,要求提供有效 Token。不要盲目重试;对无效 Token 的重试循环只会烧掉限流配额。
  4. 403 / 110401 —— 停止操作,报告 detail 中缺少的权限。不要通过查询其他资源探测权限;GET /v1/whoami 是查看权限的正规途径。
  5. 404 —— 从应用或版本列表重新获取实体 ID;ID 可能已过期或属于其他资源。任务的 310502 也可能表示任务已取消或过期,此时需要重新创建任务。
  6. 409 —— 用 code 区分普通业务校验失败与任务/审核状态冲突。重新读取资源状态后再决定。绝不无限循环重试。
  7. 429 / 300801 / 340801 —— 等待 details.retry_after 给出的秒数,再重试 Release 列表或上游仓库相关请求。绝不并发轰炸。
  8. 502 / 340901 —— 查询 Release 列表时上游 Repo 失败。保留错误信息并做有限重试。包下载或解析失败不会返回 HTTP 错误——它们表现为任务步骤失败(见第 6.5 节)。
  9. 503 / 990301 —— 服务正在关闭。稍后用相同输入重新创建任务。
  10. 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 的完整接口说明。