# RoboCloud Customer Open API v2.1 与 Runner v14 接入指南

发布日期：2026-08-26  
Base URL：`https://shop.robocloud.store/api/open/v2`

## 1. 凭据边界

v2.1 使用四种互不替代的凭据：

| 凭据 | 用途 | 可调用业务接口 | 保存规则 |
| --- | --- | --- | --- |
| 账户密码 | 仅创建 Open Session | 否 | 不交给第三方消费端 |
| Customer API Key | 仅创建 Open Session | 否 | 完整值只显示一次 |
| Open Session `rbl_sess_…` | 账户资源、Runner 管理与命令提交 | 是 | 短期、可主动注销 |
| Runner Token `rbl_run_…` | 定位和撤销已配对 Runner | 仅 Runner transport | 完整值只在配对成功时返回一次，使用 `Runner` 认证 scheme，且每次请求仍须 Ed25519 签名 |

`pairing_secret` 是五分钟有效、单次消费的临时秘密。设备私钥只保存在 Runner 所在设备。任何原始 Key、Session、Runner Token、配对秘密、私钥、配置 payload 或节点凭据都不得放入 URL、普通日志、截图、遥测或错误详情。

## 2. 权限矩阵

新建“完整控制”Key 会显式保存以下 14 项 scope；密码 Session 的有效 scope 与它一致。受限自定义 Key 只获得用户明确选择的项，后端不会补齐或隐式升级。

| Scope | 能力 |
| --- | --- |
| `account.read` | 读取账户身份 |
| `wallet.read` | 读取余额 |
| `plans.read` | 读取套餐 |
| `services.read` | 读取服务与云端 service operation |
| `connection.read` | 读取脱敏连接状态 |
| `configuration.read` | 读取敏感配置 payload |
| `nodes.read` | 读取脱敏节点元数据 |
| `usage.read` | 读取用量与计费快照 |
| `active_dynamic.read` | 读取主动动态线路状态 |
| `active_dynamic.control` | 切换地区、轮转及线路调整 |
| `payg.open` | 开通 PAYG 服务；付费加线仍同时要求报价 |
| `runner.read` | 读取 Runner、operation 和已确认出口 |
| `runner.pair` | 创建配对、查询配对和解绑 Runner |
| `runner.control` | 向同账户、在线 Runner 提交固定命令 |

Session 响应必须同时返回：

```json
{
  "data": {
    "account": {"id": "acct_example"},
    "expiresAt": "2026-08-26T12:00:00Z",
    "effectiveScopes": ["account.read", "runner.read"],
    "capabilities": {"accountRead": true, "runnerRead": true, "runnerControl": false},
    "integrationStatus": "limited",
    "missingRequiredScopes": ["runner.pair", "runner.control"]
  }
}
```

`capabilities` 的正式 schema 包含全部 14 个布尔字段，以上只为节选。程序应以 `effectiveScopes` 和机器可读的缺失项为准，不能因 Session 创建成功就假定它是完整权限。

### 旧 Key 升级

旧 `client:read` / `payg:open` Key 继续按原授权映射，只会得到 `integrationStatus=limited`，不会获得 `configuration.read` 或任何 Runner 权限。迁移步骤：

1. 在账户页创建新的“完整控制（推荐）”Key，或创建精确的受限自定义 Key。
2. 用新 Key 创建 Session，并在 `required_scopes` 中声明消费端实际需要的 scope。
3. 验证 `requestedScopesSatisfied=true`、`integrationStatus` 和 `missingRequiredScopes`。
4. 切换消费端后撤销旧 Key；不要在原 Key 上假设权限被自动升级。

配置读取在 v2.0 兼容窗口内可继续接受既有 `connection.read` Key，但新 Key 应显式申请 `configuration.read`。这项兼容不展开 Runner scope。

## 3. 最小 Open Session 示例

下面所有秘密均为占位符：

```sh
curl -X POST 'https://shop.robocloud.store/api/open/v2/auth/sessions' \
  -H 'Authorization: Bearer rbl_live_REPLACE_WITH_API_KEY' \
  -H 'Content-Type: application/json' \
  --data '{
    "auth_method":"api_key",
    "device_summary":"example-integration",
    "required_scopes":["runner.read","runner.pair","runner.control"]
  }'
```

后续账户业务请求只接受 `Authorization: Bearer rbl_sess_REPLACE_WITH_SESSION`。原始 API Key 和浏览器 Cookie 均不能直接调用这些接口。

PAYG、地区/轮转和加删线等客户异步 operation 会持久化首次成功接收请求时的 `requestId`；幂等重放只返回原 operation，不覆盖该关联值。查询响应固定提供 `requestId`、`lastErrorCode` 和脱敏 `lastError`，成功或尚未失败时后二者为 `null`。迁移前的历史任务可能没有 `requestId`，新提交任务不得缺失。

## 4. Runner 配对

RoboCloud Runner 最低版本为 **v14.0.0**。v13.2.0 及更早版本不包含安全配对协议，配对时稳定返回 `runner_pairing_unsupported`；系统不会把旧进程伪装成已配对。

账户侧创建配对：

```sh
curl -X POST 'https://shop.robocloud.store/api/open/v2/runner-pairings' \
  -H 'Authorization: Bearer rbl_sess_REPLACE_WITH_SESSION' \
  -H 'Content-Type: application/json' \
  --data '{"displayName":"office-runner"}'
```

响应中的 `pairingSecret` 只显示一次。通过本机标准输入或系统凭据存储交给 Runner，不要放在命令行 URL。Runner 调用 `POST /runners/pair` 时提交顶层 `pairingId`、`pairingSecret`、`device` 和 `proof`；`device` 内含稳定展示用 `id`、名称、平台、版本、固定能力列表和 Ed25519 公钥，`proof` 内含时间戳、nonce 与私钥持有证明。

配对证明 canonical 文本为以下字段逐行拼接，末尾不额外添加换行；`proof` 为无填充 base64url Ed25519 签名：

```text
pair
PAIRING_ID
DEVICE_ID
PROOF_TIMESTAMP
PROOF_NONCE
```

配对成功响应固定包含：`runnerId`、`accountId`、只显示一次的 `runnerToken`、`tokenType=Runner`、`heartbeatIntervalSeconds`、`offlineAfterSeconds`。设备 ID 只用于展示和去重，设备公钥才是持有证明的信任根。

解绑 `DELETE /runners/{runner_id}`、账户禁用或 Runner Token 轮换会撤销设备凭据。来源 Session 注销或 API Key 撤销会阻止新的控制请求，取消由该 Session 提交的 `queued` / `running` 命令、清除租约，并拒绝随后到达的结果；它不会把已经配对的设备悄悄转移或重新绑定。撤销瞬间若本机副作用已经发生，云端无法倒转该事实，后续 heartbeat 仍会按实际本机状态收敛。

## 5. Runner 请求签名与短轮询

Runner transport 仅有三个立即返回的 HTTPS POST；没有阻塞长轮询：

```text
POST /runner/heartbeat
POST /runner/commands/claim
POST /runner/operations/{operation_id}/results
```

请求须同时携带：

```text
Authorization: Runner rbl_run_REPLACE_WITH_RUNNER_TOKEN
X-RoboCloud-Runner-Timestamp: UNIX_SECONDS
X-RoboCloud-Runner-Nonce: UNIQUE_16_TO_128_CHAR_VALUE
X-RoboCloud-Runner-Signature: BASE64URL_ED25519_SIGNATURE_WITHOUT_PADDING
```

签名输入使用精确 UTF-8 body 字节的 SHA-256 小写十六进制摘要。canonical 文本为：

```text
METHOD
/api/open/v2/NORMALIZED_ABSOLUTE_PATH
X-ROBOCLOUD-RUNNER-TIMESTAMP
X-ROBOCLOUD-RUNNER-NONCE
SHA256_HEX_OF_EXACT_BODY_BYTES
```

时间戳过期、nonce 重用、body 变动、Token 与公钥不匹配均拒绝。重试必须重新生成 timestamp、nonce 和签名，但 operation 的本地执行结果要按 `operationId` 保存；租约重领不能重复执行本地副作用。

### Heartbeat

请求字段为 `deviceId`、`accountId`、`version`、`platform`、`capabilities`、`stateRevision`、`status`、`exports`、`observedAt`；服务端仍以已验证 Runner Token 对应的数据库记录作为归属真源，并对上报的设备/账户 ID 做恒等校验，错配即拒绝且不更新状态。未选择服务时省略 `selectedServiceId`。一旦存在 `selectedServiceId`，必须同时发送 `selectedServiceRevision`，它表示 Runner 本机实际应用的配置 revision，云端不得用当前最新 revision 猜测或覆盖。`capabilities` 只能从下文八个固定 command type 名中选择。每个 export 显式包含回环 host、port、`networkScope=runner_local` 与本机探测得到的 `confirmed`；探测失败时允许上传 `confirmed=false` 供状态诊断，但公共 exports 只返回 `confirmed=true`。响应为 `serverTime`、`nextHeartbeatInSeconds`、`runnerStatus`。心跳覆盖 Runner 当前行，不创建无限增长的心跳历史。

### Claim 与租约

`POST /runner/commands/claim` 请求只有 `{"limit":1}`。每条命令固定包含：

```text
operationId, leaseToken, type, serviceId,
expectedServiceRevision, expectedRunnerRevision,
deadline, payload
```

Runner 不得执行已过 `deadline` 的命令。`leaseToken` 只适用于对应 operation 的当前租约。配置仅在领取命令时即时生成并通过此通道传递，不持久化到普通 operation payload 或日志。

### Result

结果请求固定包含 `leaseToken`、`status`、`stateRevision`、`result`；失败时还必须包含脱敏 `error`，成功时省略或置空。响应为 `operationId`、`status`、`completedAt`。只有有效租约、未过期命令和符合 revision 的结果才更新 confirmed projection。租约是 Runner transport 内部机制；账户侧 operation 在收到并校验成功结果前只能报告 `queued` / `running`，不能声称本地操作成功。

## 6. 固定命令清单

仅允许以下八种类型：

| 命令 | 对外控制接口或用途 |
| --- | --- |
| `service.select` | `POST /runners/{runner_id}/services/{service_id}/select` |
| `service.status` | Runner 刷新/状态投影 |
| `nodes.read` | Runner 获取所选服务节点 |
| `nodes.test` | `POST /runners/{runner_id}/node-tests` |
| `node.start` | `POST /runners/{runner_id}/node-starts` |
| `node.stop` | `POST /runners/{runner_id}/node-stops` |
| `exports.read` | `GET /runners/{runner_id}/exports` |
| `runner.refresh` | `POST /runners/{runner_id}/refreshes` |

不存在 shell、脚本、自由 URL 或任意命令入口。所有写操作要求 `Idempotency-Key`、Runner/服务归属、在线状态和提交时 revision。同一键同一请求返回原 operation；同键不同请求返回 `idempotency_key_reused`。

同一 Runner revision 同时只接受一条未完成命令；上一条仍为 `queued` / `running` 时，以相同 `expectedRunnerRevision` 提交另一条命令会返回 `runner_revision_conflict`。调用方应等待 operation 进入终态，重新读取 Runner 的 `stateRevision` 后再提交下一条命令。

`node-tests`、`node-starts` 与 `node-stops` 使用去重的 `nodeIds` 数组（1–50 项）。每个 ID 必须属于该账户、该服务和提交时 service revision 的云端节点白名单；Runner 不接受调用方自带节点配置或自由 URL。

## 7. 本地出口语义

`GET /runners/{runner_id}/exports` 只返回 Runner 已确认的投影。host 仅允许 `127.0.0.1` 或 `::1`，并固定返回：

```json
{"networkScope":"runner_local","host":"127.0.0.1","port":7890}
```

该地址只能由运行在已配对 Runner 同一设备上的进程使用。云端消费端不能因为看到回环地址就直接连接它，也不能把云端节点元数据当作本地出口。

## 8. 错误与重试

错误结构统一为：

```json
{"error":{"code":"runner_offline","message":"Runner is offline","requestId":"req_example","details":{}}}
```

常用 Runner 错误：

- 配对：`runner_pairing_not_found`、`runner_pairing_invalid`、`runner_pairing_expired`、`runner_pairing_consumed`、`runner_pairing_revoked`、`runner_pairing_source_revoked`、`runner_pairing_unsupported`、`runner_pairing_limit_reached`、`device_identity_conflict`。
- Runner 认证：`runner_auth_invalid`、`runner_token_revoked`、`runner_signature_invalid`、`runner_request_replayed`、`runner_version_unsupported`。
- 归属与状态：`runner_not_found`、`runner_offline`、`runner_capability_required`、`runner_revision_conflict`、`runner_observation_stale`、`service_revision_conflict`、`runner_node_invalid`。
- operation 与投影：`runner_operation_not_found`、`runner_operation_expired`、`runner_operation_limit_reached`、`runner_lease_invalid`、`runner_result_invalid`、`runner_result_conflict`、`source_session_revoked`、`runner_export_unavailable`、`runner_projection_not_found`。
- 通用：`robocloud_scope_required`、`idempotency_key_required`、`idempotency_key_reused`、`request_too_large`、`robocloud_rate_limited`、`database_busy`、`robocloud_upstream_unavailable`。

不存在或不属于当前账户的 Runner、operation 和服务使用相同的 404 语义，避免跨账户枚举。仅对明确的临时错误采用带抖动退避；写操作重试必须复用原 `Idempotency-Key`。`runner_offline`、revision 冲突、权限不足和参数错误都不是可盲目重试的成功状态。

完整机器可读 schema、响应码与错误目录见 `storefront/assets/openapi/customer-api-v2.json`。
