计算资产 API
管理项目的计算资产(分组、资产、连接、远程桌面、容器控制、磁盘),支持 managed(平台托管)和 external(外部连接)两种模式。
基础路径
/projects/:projectId/assets认证方式
所有端点均需要 JWT Bearer Token 或 API Key 认证(combinedAuth 中间件)。
权限说明
- 读取操作(GET):需要项目访问权限
- 写入操作(POST / PATCH / DELETE):需要项目写入权限(
write) - 删除分组:需要项目管理员权限(
admin)
分组 API
GET /projects/:projectId/assets/groups
获取项目的分组列表。
响应 200
{
"groups": [
{
"id": "uuid",
"name": "生产环境",
"description": "...",
"ai_accessible": true,
"sort_order": 0
}
]
}POST /projects/:projectId/assets/groups
创建分组。
请求体
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 分组名称 |
description | string | 否 | 分组描述 |
ai_accessible | boolean | 否 | 是否允许 AI 访问 |
响应 201
{
"group": { ... }
}| 状态码 | 说明 |
|---|---|
| 409 | 分组名称已存在 |
PATCH /projects/:projectId/assets/groups/reorder
批量排序分组。
请求体
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
orderedIds | string[] | 是 | 按新顺序排列的分组 ID 数组 |
响应 200
{
"success": true
}PATCH /projects/:projectId/assets/groups/:groupId
更新分组信息。
请求体
可选字段:name、description、ai_accessible。
响应 200
{
"group": { ... }
}DELETE /projects/:projectId/assets/groups/:groupId
删除分组(需要项目管理员权限)。
响应 200
{
"success": true
}资产 API
GET /projects/:projectId/assets/templates
获取可用的镜像模板列表(按类型分组),用于创建 managed 资产时选择模板。
响应 200
{
"templates": [ ... ]
}GET /projects/:projectId/assets
获取资产列表,支持按分组过滤。
Query 参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
groupId | string | 否 | 按分组 ID 过滤 |
响应 200
{
"assets": [
{
"id": "uuid",
"name": "开发服务器",
"type": "docker",
"source": "managed",
"status": "running",
"group_id": "uuid"
}
]
}POST /projects/:projectId/assets
创建资产。
请求体
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 资产名称 |
type | string | 是 | 类型:docker / browser(desktop 暂未开放) |
source | string | 否 | 来源:managed(平台托管)/ external(外部连接) |
templateId | string | 条件必填 | 模板 slug(managed 资产必填) |
groupId | string | 否 | 所属分组 ID |
description | string | 否 | 资产描述 |
region | string | 否 | 区域 |
config | object | 否 | 额外配置 |
diskId | string | 否 | 磁盘 ID(仅 selfhosted 模式) |
connection | object | 否 | 连接信息(external 资产需提供 protocol/hostname/port) |
前置校验
- managed 资产需要 Worker 就绪,否则返回 503
- external 资产的 hostname 需通过 SSRF 校验
- 模板存在且已启用,类型匹配
响应 201
{
"asset": { ... }
}| 状态码 | 说明 |
|---|---|
| 400 | 输入校验失败 |
| 409 | 资产名称已存在 |
| 503 | Assets Worker 未就绪 |
PATCH /projects/:projectId/assets/:assetId
更新资产信息,支持磁盘绑定更新。
请求体
可选字段:name、description、groupId、diskId(仅 selfhosted 模式,需资产已停止)。
响应 200
{
"asset": { ... }
}DELETE /projects/:projectId/assets/:assetId
软删除资产。managed 资产若有容器需要 Worker 回收。
响应 200
{
"success": true
}| 状态码 | 说明 |
|---|---|
| 404 | 资产不存在 |
| 503 | Worker 未就绪,无法回收容器 |
连接凭证 API
POST /projects/:projectId/assets/:assetId/connections
保存外部资产的连接信息。
请求体
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
protocol | string | 是 | 协议:vnc / rdp / ssh / telnet(隧道资产仅 ssh / vnc) |
hostname | string | 是* | 主机名(需通过 SSRF 校验)。*隧道资产不需要传:服务端钉死 127.0.0.1(连接器视角的本机) |
port | number | 是 | 端口(1-65535) |
username | string | 否 | 用户名 |
password | string | 否 | 密码 |
private_key | string | 否 | SSH 私钥 |
settings | object | 否 | 额外连接设置 |
特殊行为
- 保存成功后自动将 pending/error 状态的资产设为 running
- 隧道资产(
access_channel = 'tunnel')修改端口后,已签发的设备凭据 allowlist 不会自动更新——需重新签发凭据(re-pair)才放行新端口(widening scope 必须重新配对)
响应 201
{
"success": true
}GET /projects/:projectId/assets/:assetId/connections
获取连接信息(脱敏,仅外部资产)。
响应 200
{
"connections": [ ... ]
}本地设备(隧道)API
本地设备接入(反向隧道):NAT 后的本地电脑作为 external 资产接入(access_channel = 'tunnel'), 本机运行 WaiNao Connect 连接器出站建立 WSS 反向隧道,网页经隧道桥接直连本机 SSH / VNC。 协议与安全不变量详见仓库文档 docs/features/assets/agent-tunnel.md(docs-site 未收录该页)。
三个端点均要求:资产存在且
source = 'external'、access_channel = 'tunnel',否则 400not_tunnel_asset; 需要项目写入权限。支持 JWT 与 OAuth(scopeassets:agent)两种鉴权。
POST /projects/:projectId/assets/:assetId/agent-device
签发(或轮换)连接器设备凭据。明文 token 仅此一次返回,服务端只存 sha256。
前置条件
- 当前节点配置了
AGENT_TUNNEL_NODE_ORIGIN(未配置 → 503tunnel_disabled) - 资产已配置主连接(无 → 400
connection_required),协议为ssh/vnc(否则 400tunnel_protocol_unsupported)
特殊行为
- 服务端 allowlist(
allowed_services)从资产主连接快照{protocol, port},与连接器本地 allowlist 双端强制 - 轮换语义:重复调用即轮换——旧 token 立即失效,若旧连接器在线会被立即断开、资产置 stopped
响应 200
{
"token": "<prefix>.<secret>",
"prefix": "ab12cd34",
"nodeOrigin": "https://block2.wainao.chat",
"pairCommand": "wainao-connect pair --server https://block2.wainao.chat --asset <assetId> --token <prefix>.<secret> --service ssh:22"
}GET /projects/:projectId/assets/:assetId/agent-device
查询设备配对与在线状态(不含 token hash / 明文)。
响应 200(未配对时仅 { "paired": false })
{
"paired": true,
"prefix": "ab12cd34",
"allowedServices": [{ "protocol": "ssh", "port": 22 }],
"platform": "darwin",
"online": true,
"connectedNode": "https://block2.wainao.chat",
"lastSeenAt": "2026-07-11T12:00:00Z",
"revokedAt": null,
"createdAt": "2026-07-11T10:00:00Z"
}
online反映本节点进程内是否持有该设备隧道;多节点部署下设备连在其他节点时本节点返回online: false,以connectedNode判断实际归属节点。
DELETE /projects/:projectId/assets/:assetId/agent-device
吊销设备凭据(软删 revoked_at)。若设备在线,立即断开隧道并把资产置 stopped; 连接器收到 401/403 后清除本地配置退出。
响应 200
{
"success": true
}远程桌面连接 API
POST /projects/:projectId/assets/:assetId/connect
发起远程桌面连接,返回加密 Token 和 WebSocket URL。
前置检查
- Guacamole 代理就绪
- 资产状态为 running
- 并发会话数不超过 5
- 无连接锁定(指数退避)
响应 200
{
"wsUrl": "wss://host/ws/guacamole",
"token": "encrypted-token",
"sessionId": "uuid",
"protocol": "vnc",
"mode": "control"
}| 字段 | 类型 | 说明 |
|---|---|---|
wsUrl | string | WebSocket 连接地址 |
token | string | 加密连接令牌(10 秒过期) |
sessionId | string | 会话 ID |
protocol | string | 连接协议 |
mode | string | 会话模式:control(控制)/ observe(观察),VNC/RDP 协议同一资产仅一个 control 会话 |
| 状态码 | 说明 |
|---|---|
| 400 | 资产未运行 / 未配置连接信息 |
| 429 | 已达最大并发连接数 / 连接锁定中(含 Retry-After 头) |
| 503 | Guacamole 代理未就绪 |
隧道资产附加检查(access_channel = 'tunnel',在上述检查之外)
| 状态码 | code | 说明 |
|---|---|---|
| 400 | device_unpaired | 设备未配对或凭据已吊销 |
| 400 | device_offline | 设备离线(连接器未运行) |
| 409 | tunnel_on_other_node | 设备连接在其他节点(响应含 connectedNode),需到对应节点访问。DB connected_node 为 owner 权威——即使本节点进程仍残留旧隧道 socket 也不放行 |
POST /projects/:projectId/assets/:assetId/control/claim
同账号主动接管控制权:把当前 observe(或被提升但物理仍只读的)会话升级为 control,返回新的 control 连接凭证。仅 managed 资产、仅 vnc/rdp 协议。 客户端拿到新 token 后需重连才真正获得读写(observe 的 read-only 在 guacd 连接时固定,运行时不可变)。
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
sessionId | string | 是 | 请求方当前会话 ID(须为本人、本资产、open 状态) |
响应 200(结构与 connect 一致)
{
"wsUrl": "wss://host/ws/guacamole",
"token": "encrypted-token",
"sessionId": "uuid",
"protocol": "vnc",
"mode": "control",
"terminalMode": "guacamole"
}| 状态码 | code | 说明 |
|---|---|---|
| 400 | — | 缺 sessionId / 资产非 managed 或未运行 / 协议非 vnc/rdp / 无效的接管会话 |
| 403 | — | 缺少项目写入权限 |
| 404 | — | 资产不存在 |
| 409 | CONTROL_HELD_BY_OTHER | 他人正在操作,无法接管(非 retryable) |
| 409 | CONTROL_CONTENTION / CONTROL_STATE_CHANGED | 同账号竞争 / 控制权状态已变化(retryable,可重试) |
| 503 | — | Guacamole 代理未就绪 |
接管成功后,被降级的旧 control 端会通过 Realtime(
assets_sessions的mode='observe'广播)收到通知并断开。状态机为「先预创建新 control → 收敛旧 control → 最终 invariant 复读 → 广播降级 → 签 token」,无 DDL、best-effort 收敛。
会话 API
GET /projects/:projectId/assets/:assetId/sessions
获取指定资产的会话列表(W5 云桌面 / 终端会话)。需要项目写入权限(与 connect 一致):只读成员不可枚举他人会话,避免泄露 issued 会话 ID、成员在线状态与连接历史。
路径参数
| 参数 | 说明 |
|---|---|
projectId | 项目 ID |
assetId | 资产 ID(会校验归属于该项目) |
Query 参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
status | string | 否 | 按状态过滤:issued / open / closed / failed |
limit | number | 否 | 返回条数,默认 50,范围 1-200 |
响应 200
{
"sessions": [
{
"id": "uuid",
"user_id": "uuid",
"status": "open",
"mode": "control",
"protocol": "vnc",
"issued_at": "2026-06-22T08:00:00.000Z",
"opened_at": "2026-06-22T08:00:05.000Z",
"closed_at": null,
"duration_ms": null,
"last_heartbeat_at": "2026-06-22T08:05:00.000Z"
}
]
}| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 会话 ID |
user_id | string | 会话所属用户 ID |
status | string | 会话状态:issued / open / closed / failed |
mode | string | 会话模式:control(控制)/ observe(观察) |
protocol | string | 连接协议,如 vnc / rdp / ssh |
issued_at | string | 会话签发时间(ISO 8601),列表按此倒序 |
opened_at | string | null | 会话建立时间 |
closed_at | string | null | 会话关闭时间 |
duration_ms | number | null | 会话时长(毫秒) |
last_heartbeat_at | string | null | 最近心跳时间 |
出于安全考虑,响应不回传
client_info(含 IP/UA),token_nonce永不出库。
| 状态码 | 说明 |
|---|---|
| 400 | status 取值非法 / limit 非正整数或超出 1-200 范围 |
| 403 | 缺少项目写入权限 |
| 404 | 资产不存在(或不属于该项目) |
容器控制 API(Managed 资产)
POST /projects/:projectId/assets/:assetId/start
启动 managed 资产。设置 desired_status = 'running',由 Worker 异步执行。
前置检查:资产为 managed 类型且未在运行中,Worker 就绪。
响应 200
{
"success": true,
"message": "启动请求已提交"
}POST /projects/:projectId/assets/:assetId/stop
停止 managed 资产。
响应 200
{
"success": true,
"message": "停止请求已提交"
}POST /projects/:projectId/assets/:assetId/restart
重启 managed 资产(通过 restart_requested 标记实现)。
响应 200
{
"success": true,
"message": "重启请求已提交"
}磁盘 API(仅 selfhosted 模式)
GET /projects/:projectId/assets/disks
获取项目的磁盘列表。
响应 200
{
"disks": [ ... ]
}POST /projects/:projectId/assets/disks
创建磁盘。
请求体
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 磁盘名称 |
description | string | 否 | 磁盘描述 |
响应 201
{
"disk": { ... }
}PATCH /projects/:projectId/assets/disks/:diskId
更新磁盘信息。
请求体
可选字段:name、description。
响应 200
{
"disk": { ... }
}DELETE /projects/:projectId/assets/disks/:diskId
删除磁盘(不能正在被资产使用)。
响应 200
{
"success": true
}| 状态码 | 说明 |
|---|---|
| 400 | 磁盘正在使用 |
| 404 | 磁盘不存在 |
| 501 | 非 selfhosted 模式不可用 |
补充端点
本节补充 plans/need-update-api.md 中尚未覆盖到 docs-site 的接口。端点标题保持严格的 METHOD /path 格式,便于后台文档覆盖率服务识别。
GET /projects/:projectId/assets/capability
读取项目计算资产能力开关与当前可用资源能力。
认证:JWT Bearer Token 或 API Key(combinedAuth)。 路径参数
| 参数 | 说明 |
|---|---|
projectId | 项目 ID |
请求:无请求体。Query 参数用于分页、过滤、搜索或状态筛选;未传时按后端默认排序与分页返回。
响应:成功时返回目标资源详情、状态或配置对象。
常见错误:400 参数非法,401 未认证,403 权限不足,404 资源不存在;服务端或上游异常返回 500。
GET /projects/:projectId/assets/compute-specs
可用计算规格
认证:JWT Bearer Token 或 API Key(combinedAuth)。 路径参数
| 参数 | 说明 |
|---|---|
projectId | 项目 ID |
请求:无请求体。Query 参数用于分页、过滤、搜索或状态筛选;未传时按后端默认排序与分页返回。
响应:成功时返回目标资源详情、状态或配置对象。
常见错误:400 参数非法,401 未认证,403 权限不足,404 资源不存在;服务端或上游异常返回 500。
GET /projects/:projectId/assets/resource-catalog
读取创建资产时需要的资源目录,包括规格、区域、模板或供应商能力。
认证:JWT Bearer Token 或 API Key(combinedAuth)。 路径参数
| 参数 | 说明 |
|---|---|
projectId | 项目 ID |
请求:无请求体。Query 参数用于分页、过滤、搜索或状态筛选;未传时按后端默认排序与分页返回。
响应:成功时返回目标资源详情、状态或配置对象。
常见错误:400 参数非法,401 未认证,403 权限不足,404 资源不存在;服务端或上游异常返回 500。