新增Tripo生成API集成主规范与决策记录
- 新增主规范,固定两个端点的请求、异步执行、结果持久化与泥点扣费合同 - 新增 ADR 0002,记录不复用 Hyper3D、at-most-once submit、提交时定价与落库失败退款
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
# 【ADR】0002-Tripo 生成 API 集成边界与提交时定价-2026-09-21
|
||||
|
||||
状态:已接受
|
||||
|
||||
Tripo 3D 生成以**全新 API** 接入 api-server,不复用 `platform-hyper3d` 的路由、契约、错误映射或实现,也不做 provider 静默切换;旧 Hyper3D 能力保持原样。两个首期端点是 text-to-model 与 image-to-model,multiview 与 Splat 不接入。异步执行复用现有 `external_generation_job` 队列、worker、租约与状态查询,不新建平行队列、状态枚举或查询接口。对外只暴露 Genarrative 自己的 operation 标识,Tripo task 仅以服务端 checkpoint 形式存在,任何 API 都不返回 provider task ID、SDK 类型或带签名的临时 URL。
|
||||
|
||||
选择这个边界是因为第三方 SDK 的任务与错误模型会随 provider 变化,一旦穿透到 API 与前端就会把手第三方约束固化成产品契约;同时 Hyper3D 与 Tripo 的参数、状态与计费形态都不同,把它们塞进同一组路径会让“路径名”与“实际 provider”不一致,调用方无法分辨。
|
||||
|
||||
**at-most-once submit**:worker 的崩溃恢复模型是“租约过期后重新 claim,handler 从头重跑”,这对可重跑的 provider 安全,但 Tripo 重跑会二次 submit、二次消耗额度。因此 `external_generation_job` 追加 provider checkpoint 字段并新增受租约栅栏保护的写入 procedure:没有 checkpoint 才允许 submit,submit 成功后必须先落 checkpoint 再轮询,已有 checkpoint 只允许查询、下载与落库。submit 成功但 checkpoint 落库失败的 attempt 只能终态失败,不得退回可重试队列。checkpoint 不写入 `request_payload_json`,因为该字段是请求真相并参与请求指纹与压缩逻辑。
|
||||
|
||||
**提交时定价与真实扣费**:Tripo 计费形态是“底价 + 可叠加 add-on”,现有“模型 → 档位 → 单价”查表表达不了,因此新增 `model3d` 定价段,全部以泥点计价,底价按 `endpoint × modelVersion × 是否有贴图` 拆分,add-on 按请求参数判定叠加;配置加载即校验全部底价键存在,运行期缺键直接拒绝提交,不复用现有 `unwrap_or(0)` 兜底。扣费依据固定为提交时定价,而不是 provider 返回的实际消耗:用户在提交前即可得到确定价格,失败退款就是 attempt 级单次冲正,不需要按实际用量退差额。provider 的实际消耗只写日志用于成本对账,不进入契约、API 响应或资源行。
|
||||
|
||||
**provider 成功但落库失败按失败处理并退款**:没有正式资源引用就不算交付成功。这会带来“provider 已消耗、用户已退款”的净亏损,是明确接受的成本,换取的是“用户看到成功就一定拿得到资源”的确定性。首期不提供取消,因为 SDK 没有取消能力,提供本地取消会让调用方误以为 provider 任务已停止。
|
||||
|
||||
代价与已知取舍:复用队列意味着要在一个被多个 job kind 共用的表上追加 provider 专用列;真实扣费意味着上线前必须先把 20 个底价键与 add-on 价格确认到位,否则接口无法开放;产物下载沿用完整字节缓冲,40MiB 级模型会占用内存,流式上传作为后续 TODO 处理。
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# 【技术方案】Tripo 3D 生成 API 集成-2026-09-21
|
||||
|
||||
更新时间:`2026-09-21`
|
||||
文档状态:`current`
|
||||
父规范:`docs/technical/【技术方案】Tripo 3D模型Provider集成-2026-09-18.md`(provider 层合同,本文件承接应用层)
|
||||
|
||||
## 目标
|
||||
|
||||
在 `platform-tripo` provider 已就绪的前提下,把 Tripo 的 text-to-model 与 image-to-model 接入 api-server 内部 API:鉴权提交、异步执行、产物落 OSS 并登记为正式资源、按显式泥点定价真实扣费。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不接 multiview-to-model、Gaussian Splat、rig、animation、texture、convert 等其它 Tripo 能力。
|
||||
- 不进入 `/api/external/v1`,不改 External OpenAPI,不新增外部 scope 或 API Key 语义。
|
||||
- 不复用、不修改、不删除 `platform-hyper3d` 及其 `/api/assets/hyper3d/*` 路由与契约;本能力是全新 API,不是 Hyper3D 的 provider 替换。
|
||||
- 不接前端与 `packages/model3d-viewer`。
|
||||
- 不提供取消能力:客户端停止查询,worker 继续推进到 provider 终态。
|
||||
- 本期不改 `platform-oss` 为流式或分片上传,沿用现有完整字节写入路径;流式上传作为后续 TODO。
|
||||
|
||||
## 参与入口与分层
|
||||
|
||||
| 入口 | 方法 | 鉴权 | 职责 |
|
||||
| --- | --- | --- | --- |
|
||||
| `/api/assets/tripo/text-to-model` | POST | Bearer | 校验、定价、入队,返回 operation |
|
||||
| `/api/assets/tripo/image-to-model` | POST | Bearer | 校验、输入解析、定价、入队 |
|
||||
| `/api/runtime/external-generation/jobs/{jobId}` | GET | Bearer | 复用现有任务查询,返回 operation 与严格结果 |
|
||||
|
||||
分层职责固定为:
|
||||
|
||||
```text
|
||||
api-server HTTP、鉴权、参数组合校验、泥点定价、入队、错误映射
|
||||
platform-tripo SDK 隔离、submit、单次 get_task、下载、provider 错误归一
|
||||
platform-oss / 资源登记 产物持久化与正式资源引用
|
||||
```
|
||||
|
||||
Tripo task 只以 checkpoint 形式存在于服务端,任何 API 都不返回 provider task ID、SDK 类型或带签名的临时 URL。
|
||||
|
||||
## 请求契约
|
||||
|
||||
两个端点的请求 = 现有 `shared-contracts::model3d` 请求 + 平台字段,全部经 ts-rs 目录化生成 TypeScript binding。
|
||||
|
||||
**图片输入**(仅 image-to-model)使用 tagged enum,服务端按分支做归属校验并解析 OSS 对象:
|
||||
|
||||
```text
|
||||
source = { kind: "resource", resourceId } | { kind: "asset", assetId }
|
||||
```
|
||||
|
||||
不接受裸 `input` 字符串、任意远程 URL、data URL,也不接受同时给两个 ID。
|
||||
|
||||
**结果落点**(两个端点都必填)使用 tagged enum:
|
||||
|
||||
```text
|
||||
target = { kind: "projectResource", projectId, canvasCompletion? }
|
||||
| { kind: "assetLibrary", folderId, label }
|
||||
```
|
||||
|
||||
`projectResource` 分支必须给 `projectId`,可选 `canvasCompletion` 表达画布占位框回填;`assetLibrary` 分支的 `folderId` 与 `label` 必填。不存在“都不给就默认落素材库”的口径。
|
||||
|
||||
**定价相关参数在 API 层必填并做组合校验**:`texture`、`textureQuality`、`geometryQuality`、`quad`、`smartLowPoly`、`generateParts` 必须显式给出。`texture=false` 时禁止出现 `textureQuality`,且 `pbr` 必须显式 `false`;`texture=true` 时 `textureQuality` 必填。理由是 provider 的隐式默认值会直接改变价格,一旦依赖默认值,报价与扣费会在“调用方少传字段”时分叉。
|
||||
|
||||
两个 submit 都必须携带 `Idempotency-Key`,复用现有头部校验;缺失或格式非法直接 400,不静默生成键。
|
||||
|
||||
## 异步执行与状态
|
||||
|
||||
本能力复用现有 `external_generation_job` 队列、worker、租约与 owner 过滤,不新建平行队列、不新增状态枚举、不新增查询接口。状态对外仍是 `queued` / `running` / `completed` / `failed`,阶段提示沿用现有 `phase` 与 `phaseLabel` / `phaseDetail`。
|
||||
|
||||
**at-most-once submit** 是本能力与其它生成任务的关键差异。现有 worker 的崩溃恢复模型是“租约过期后由别的 worker 重新 claim,handler 从头重跑”,这对可重跑的 provider 安全,但 Tripo 重跑会二次 submit、二次消耗额度。因此:
|
||||
|
||||
1. 没有 `providerTaskId` 的 job 才允许 submit;
|
||||
2. submit 成功后必须先把 `providerTaskId` 写回 checkpoint,再进入轮询;
|
||||
3. 已有 `providerTaskId` 的 job 只允许 `get_task`、下载与落库,绝不允许再次 submit;
|
||||
4. submit 成功但 checkpoint 写入失败的 attempt 只能终态失败,不得退回可重试队列,否则重试会二次 submit;该 job 保留脱敏对账信息供人工处理。
|
||||
|
||||
失败重试沿用现有语义:`fail` 先按 attempt 冲正扣费,`attempt < maxAttempts` 时回到 `pending` 并延迟重试,否则终态 `failed`。
|
||||
|
||||
**provider 成功但落库失败按失败处理**:job 失败、该 attempt 退款,客户端看到 failed。没有正式资源引用就不算交付成功;checkpoint 保留,人工对账可证明这次生成确实发生过。由此产生的“provider 已消耗、用户已退款”净亏损是已知并接受的成本。
|
||||
|
||||
**完成结果使用按端点的严格 tagged enum**,不使用由多个可选字段拼成的宽松结果:
|
||||
|
||||
```text
|
||||
TextToModelResult = { modelArtifact, renderedPreview }
|
||||
ImageToModelResult = { modelArtifact, renderedPreview }
|
||||
```
|
||||
|
||||
每个 artifact 只携带正式资源引用与对象元数据,不携带 provider 临时 URL。
|
||||
|
||||
## 定价与扣费
|
||||
|
||||
定价是**真实扣费**,不是计量占位。Tripo 的计费形态是“底价 + 可叠加 add-on”,现有图片/视频那套“模型 → 档位 → 单价”查表表达不了,因此在同一份定价配置中新增 `model3d` 段,全部以泥点计价:
|
||||
|
||||
```json
|
||||
"model3d": {
|
||||
"unit": "perGeneration",
|
||||
"basePrices": {
|
||||
"text-to-model": { "<modelVersion>": { "noTexture": 泥点, "texture": 泥点 } },
|
||||
"image-to-model": { "<modelVersion>": { "noTexture": 泥点, "texture": 泥点 } }
|
||||
},
|
||||
"addOnPrices": {
|
||||
"hdTexture": 泥点, "ultraTexture": 泥点, "hdGeometry": 泥点,
|
||||
"quadMesh": 泥点, "smartLowPoly": 泥点, "generateParts": 泥点
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
模型版本键使用 `Model3dModelVersion` 的枚举值,不另造字符串。add-on 由请求参数判定,判定规则固定为:
|
||||
|
||||
```text
|
||||
hdTexture ← texture=true && textureQuality=detailed
|
||||
ultraTexture ← texture=true && textureQuality=extreme
|
||||
hdGeometry ← geometryQuality=detailed
|
||||
quadMesh ← quad=true
|
||||
smartLowPoly ← smartLowPoly=true
|
||||
generateParts ← generateParts=true
|
||||
textureQuality=fast | standard → 不给任何贴图加价
|
||||
```
|
||||
|
||||
最终价格:
|
||||
|
||||
```text
|
||||
price = basePrices[endpoint][modelVersion][texture ? "texture" : "noTexture"]
|
||||
+ Σ addOnPrices[命中的 add-on]
|
||||
```
|
||||
|
||||
两条硬约束:
|
||||
|
||||
1. **加载即校验**:两个 endpoint × 每个受支持模型版本 × 两种贴图态必须全部存在,缺一即配置非法、服务拒绝启动;运行期不存在“查不到价”的分支。
|
||||
2. **提交即拒绝**:价格计算发生在调用 provider 之前;拿不到价格就不提交、不扣费、不入队。
|
||||
|
||||
扣费依据固定为**提交时定价**:用户在提交前就能得到确定价格,失败退款是单次冲正,不需要按实际消耗退差额。provider 返回的实际消耗只写日志用于成本对账,不进入契约、不进 API 响应、不进资源行。
|
||||
|
||||
价格随 job 写入 `price_mud_points`,扣费与冲正沿用现有资产操作账务(attempt 级 consume / refund ledger);素材侧继续用 `generation_cost_mud_points` 记录单次成本。同一 `Idempotency-Key` 命中已有 job 时直接返回原 operation,不新建、不重复扣费。
|
||||
|
||||
## 数据与契约
|
||||
|
||||
**队列表**:`external_generation_job` 追加 provider checkpoint 字段(`providerKind`、`providerTaskId`),字段追加在结构体末尾并给显式默认值;新增一个受租约栅栏保护的写入 procedure,供 worker 在 submit 成功后落 checkpoint。checkpoint 不写入 `request_payload_json`,因为该字段是请求真相,参与请求指纹与压缩逻辑。
|
||||
|
||||
**资源表不新增列**:`EditorAsset` / `EditorProjectResource` 复用既有字段表达 3D 结果:
|
||||
|
||||
```text
|
||||
assetKind = "model3d"
|
||||
objectKey = 模型对象
|
||||
imageSrc = 预览图稳定引用(该列非空,必须填预览)
|
||||
thumbnailSrc = 预览图
|
||||
width/height = 预览图像素尺寸
|
||||
```
|
||||
|
||||
模型格式与体积由 `AssetObject` 承担(`content_type`、`content_length`、`content_hash`),因此不新增 `model_format`、`size_bytes`、`poly_count` 等列。
|
||||
|
||||
`external_generation_job.phase` 的取值集合不变,仍只允许现有两种执行阶段;阶段文案由 api-server 映射,不扩展 schema 常量。
|
||||
|
||||
## 兼容与迁移
|
||||
|
||||
- 旧 `/api/assets/hyper3d/*` 与 `platform-hyper3d` 保持原样,不因本能力上线而改变行为,也不做 provider 静默切换。
|
||||
- schema 变更必须同步 `migration.rs`、表目录、生成绑定,并运行 `npm run check:spacetime-schema`。
|
||||
- 新契约继续由 Rust `ts-rs` 目录化生成到 `packages/shared/src/contracts/model3d/`,不手写 TypeScript。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 两个 submit 路由要求 Bearer 与 `Idempotency-Key`;缺头或非法头返回 400,且不产生 job、不扣费。
|
||||
2. 同一 `Idempotency-Key` 重复提交返回同一 operation,钱包只有一条扣费流水。
|
||||
3. 定价配置缺任一底价键时服务启动失败;请求命中 add-on 后扣费等于底价加全部命中 add-on;`fast` 与 `standard` 不加价。
|
||||
4. 组合校验前置于 provider 副作用:`texture=false` + `textureQuality`、`generateParts=true` 与 `texture=true` 的同现请求被拒绝且不扣费。
|
||||
5. worker 崩溃后重新 claim 不会产生第二次 submit:已有 checkpoint 的 job 只轮询、下载与落库。
|
||||
6. submit 成功但 checkpoint 写入失败的 attempt 终态失败且不自动重提。
|
||||
7. provider 成功但落库失败时 job 失败、该 attempt 退款,checkpoint 保留供对账。
|
||||
8. 完成结果按端点严格类型返回,只含模型与预览的正式资源引用,不含 provider task ID 与带签名的临时 URL。
|
||||
9. `assetLibrary` 分支产出 `assetId`;`projectResource` 分支产出 `resourceId`,且 `assetKind=model3d`、`AssetObject.content_type` 为模型 mime、`content_length` 与实际对象一致。
|
||||
10. 真实 Provider smoke 覆盖 text-to-model 与 image-to-model 各一次,下载字节数与 `Content-Length` 一致。
|
||||
11. 定向测试、`cargo check`、`npm run check:spacetime-schema`、`npm run check:encoding`、`node scripts/check-doc-index.mjs`、`git diff --check` 全部通过。
|
||||
|
||||
## 未决问题
|
||||
|
||||
- add-on 价格目前是跨模型统一一份;若 Tripo 后续对某个模型版本差异化加价,再补“按模型版本覆盖”一层,现在不预留。
|
||||
- 前端与 `packages/model3d-viewer` 的接入另行安排,不在本规范范围。
|
||||
Reference in New Issue
Block a user