From 699f321651f08f9b3a3d4ef9934c5e8a9ed0bc39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 21 Sep 2026 12:29:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9ETripo=E7=94=9F=E6=88=90API?= =?UTF-8?q?=E9=9B=86=E6=88=90=E4=B8=BB=E8=A7=84=E8=8C=83=E4=B8=8E=E5=86=B3?= =?UTF-8?q?=E7=AD=96=E8=AE=B0=E5=BD=95=20-=20=E6=96=B0=E5=A2=9E=E4=B8=BB?= =?UTF-8?q?=E8=A7=84=E8=8C=83=EF=BC=8C=E5=9B=BA=E5=AE=9A=E4=B8=A4=E4=B8=AA?= =?UTF-8?q?=E7=AB=AF=E7=82=B9=E7=9A=84=E8=AF=B7=E6=B1=82=E3=80=81=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E6=89=A7=E8=A1=8C=E3=80=81=E7=BB=93=E6=9E=9C=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96=E4=B8=8E=E6=B3=A5=E7=82=B9=E6=89=A3=E8=B4=B9?= =?UTF-8?q?=E5=90=88=E5=90=8C=20-=20=E6=96=B0=E5=A2=9E=20ADR=200002?= =?UTF-8?q?=EF=BC=8C=E8=AE=B0=E5=BD=95=E4=B8=8D=E5=A4=8D=E7=94=A8=20Hyper3?= =?UTF-8?q?D=E3=80=81at-most-once=20submit=E3=80=81=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E6=97=B6=E5=AE=9A=E4=BB=B7=E4=B8=8E=E8=90=BD=E5=BA=93=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E9=80=80=E6=AC=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ipo生成API集成边界与提交时定价-2026-09-21.md | 16 ++ ...技术方案】Tripo 3D生成API集成-2026-09-21.md | 174 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 docs/adr/【ADR】0002-Tripo生成API集成边界与提交时定价-2026-09-21.md create mode 100644 docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md diff --git a/docs/adr/【ADR】0002-Tripo生成API集成边界与提交时定价-2026-09-21.md b/docs/adr/【ADR】0002-Tripo生成API集成边界与提交时定价-2026-09-21.md new file mode 100644 index 000000000..664341735 --- /dev/null +++ b/docs/adr/【ADR】0002-Tripo生成API集成边界与提交时定价-2026-09-21.md @@ -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 处理。 + diff --git a/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md b/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md new file mode 100644 index 000000000..b356d5e1d --- /dev/null +++ b/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md @@ -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": { "": { "noTexture": 泥点, "texture": 泥点 } }, + "image-to-model": { "": { "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` 的接入另行安排,不在本规范范围。