3D 生成请求允许缺省 textureQuality 并按 standard 计价
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled

- PricingParamView::validate 不再要求 texture=true 时显式给出 textureQuality,缺省按 standard 计价
- 只有显式 detailed / extreme 才命中 hdTexture / ultraTexture 加价,缺省不改变报价
- v2.5 拒绝任何显式 texture_quality,必填规则会让 H25 + texture=true 永远不可达
- texture=false 仍禁止出现贴图档位,原有冲突校验与用例保持不变
- 同步技术方案:说明 geometryQuality / textureQuality 两个字段允许缺省的共同理由
This commit is contained in:
2026-09-23 16:30:22 +08:00
parent 427b9f19a1
commit 6a7cf294a0
2 changed files with 27 additions and 20 deletions
@@ -60,13 +60,13 @@ assetLabel? 素材名称,缺省用平台默认名
canvasCompletion? 画布占位框回填,只在项目资源落点下生效
```
`projectId``assetFolderId` 至少给一个,并且允许同时给:两个都不给直接 400;同时给时画布资源行与素材行两条都写与图片等画布生成工具完全一致(画布链路本来就会把“当前素材夹”一起发出去,见下)。`assetLabel` 只在素材库落点下生效,缺省用本次结果标题;`canvasCompletion` 是项目资源落点的回填载荷,“没有 `projectId` 却带占位框”被拒绝。不存在“都不给就默认落素材库”的口径。
`projectId``assetFolderId` 至少给一个,并且允许同时给:两个都不给直接 400;同时给时画布资源行与素材行两条都写;只给 `assetFolderId` 时结果只落素材库,不要求调用方先有画布工程。以上三条与图片等画布生成工具完全一致(画布链路本来就会把“当前素材夹”一起发出去,见下)。`assetLabel` 只在素材库落点下生效,缺省用本次结果标题;`canvasCompletion` 是项目资源落点的回填载荷,“没有 `projectId` 却带占位框”被拒绝。不存在“都不给就默认落素材库”的口径。
落点预检与其它付费编辑器生成共用同一条路径:提交时调只读 `preflight_editor_generation_target_and_return`,按认证 owner 对给出的落点逐个定点归属校验(两个都给就两个都查,不再为了一个目录 ID 读整个素材库),跨 owner / 已删除 / 不存在收敛成同一句 400。
**归一在预检之前完成并写回请求**:项目 ID 与素材夹 ID 都 trim,`project` / 旧 `folder-*` 走与图片画布同一个 `normalize_generated_asset_folder_id` 映射到当前 owner 的默认素材夹,素材名走同一个 `resolve_editor_generated_asset_label`trim + 截断 + 缺省「3D 模型」)。入队的就是归一后的请求,因此预检值 = 队列载荷 = 落库值;模块侧落库原有的 `normalize_editor_generation_default_asset_folders` 继续兜底老队列载荷里的 `project`
**定价相关参数在 API 层做组合校验**`texture``textureQuality``quad``smartLowPoly``generateParts` 必须显式给出;`geometryQuality` 允许缺省,缺省按 `standard` 计价 —— provider 对 v2.5 / P1 / P2 不支持该参数v3.x 的缺省也是 `standard`,两边同价,因此只有显式 `detailed` 才命中 `hdGeometry` 加价(写死必填只会让定价表里已有的 v2.5 / P1 / P2 永远不可达)。`texture=false` 时禁止出现 `textureQuality`,且 `pbr` 必须显式 `false``texture=true``textureQuality` 必填。其余会改价的参数仍必须显式给出:provider 的隐式默认值会直接改变价格,一旦依赖默认值,报价与扣费会在“调用方少传字段”时分叉。
**定价相关参数在 API 层做组合校验**`texture``quad``smartLowPoly``generateParts` 必须显式给出;`geometryQuality``textureQuality` 允许缺省,缺省按 `standard` 计价 —— provider 对 v2.5 不支持 `geometry_quality`、并拒绝任何 `texture_quality`v3.x 的缺省也是 `standard`,两边同价,因此只有显式 `detailed` / `extreme` 才命中 `hdGeometry` / `hdTexture` / `ultraTexture` 加价(把这两个字段写死必填只会让定价表里已有的 v2.5 / P1 / P2 永远不可达,或让 `texture=true` 的 v2.5 无论填什么都被 provider 预检拒绝)。`texture=false` 时禁止出现 `textureQuality`,且 `pbr` 必须显式 `false`。其余会改价的参数仍必须显式给出:provider 的隐式默认值会直接改变价格,一旦依赖默认值,报价与扣费会在“调用方少传字段”时分叉。
两个 submit 都必须携带 `Idempotency-Key`,复用现有头部校验;缺失或格式非法直接 400,不静默生成键。
@@ -94,7 +94,7 @@ ImageToModelResult = { modelArtifact, renderedPreview }
每个 artifact 只携带正式资源引用与对象元数据,不携带 provider 临时 URL。
**完成结果的读取**:与其它画布生成任务同形——结果落在画布 / 素材行,客户端靠读回拿到。项目资源落点的 worker 写画布 placement(预览图层),客户端在任务终态后用 `loadEditorProject` 复读项目快照,按 `layer.objectKey` 打开模型;素材库落点写素材行(画布链路两个落点都发,所以一次 3D 生成通常两条都写)。结果里的落点引用与请求落点同形:`target` 不再是被判别结构包着的分支,而是平坦的 `resourceId` / `assetId`,哪个落点写了行就带哪个 ID。前端画布链路的结果消费沿用既有画布路径(项目快照 + `applyGeneratedProjectSnapshot` 顺带刷新素材库),素材库行的 3D 缩略图按预览图渲染、不拿模型对象换签名地址。标准消费者的队列结果只回来源身份,**不透传 `result`**,因此 `/api/runtime/external-generation/jobs/{jobId}``result` 对 3D 为空——图片等其它画布生成任务也是如此。上面那份严格 tagged enum 是 worker 落库时构造的内部结果值:它只携带正式资源引用与对象元数据,不携带 provider 事实。
**完成结果的读取**:与其它画布生成任务同形——结果落在画布 / 素材行,客户端靠读回拿到(没有 `projectId` 时只有素材行可读回,与其它画布工具在无工程时的行为一致)。项目资源落点的 worker 写画布 placement(预览图层),客户端在任务终态后用 `loadEditorProject` 复读项目快照,按 `layer.objectKey` 打开模型;素材库落点写素材行(画布链路两个落点都发,所以一次 3D 生成通常两条都写)。结果里的落点引用与请求落点同形:`target` 不再是被判别结构包着的分支,而是平坦的 `resourceId` / `assetId`,哪个落点写了行就带哪个 ID。前端画布链路的结果消费沿用既有画布路径(项目快照 + `applyGeneratedProjectSnapshot` 顺带刷新素材库),素材库行的 3D 缩略图按预览图渲染、不拿模型对象换签名地址。标准消费者的队列结果只回来源身份,**不透传 `result`**,因此 `/api/runtime/external-generation/jobs/{jobId}``result` 对 3D 为空——图片等其它画布生成任务也是如此。上面那份严格 tagged enum 是 worker 落库时构造的内部结果值:它只携带正式资源引用与对象元数据,不携带 provider 事实。
## 定价与扣费
@@ -1,8 +1,8 @@
//! Tripo 生成请求的平台侧组合校验。
//!
//! 这里只判两类事:
//! 1. 平台自己新增的口径——会改价的参数必须显式给出(`geometryQuality` 例外:缺省等价于
//! provider 的 standard,不产生加价),贴图、贴图档位与 pbr 不能互相矛盾;
//! 1. 平台自己新增的口径——会改价的参数必须显式给出(`geometryQuality` 与 `textureQuality`
//! 例外:缺省等价于 provider 的 standard,不产生加价),贴图、贴图档位与 pbr 不能互相矛盾;
//! 2. 模型档位与参数的能力组合——直接复用 `platform-tripo` 的请求预检,不维护第二份规则。
//!
//! 校验必须在扣费与任何 provider 调用之前完成;拿不到合法参数与价格就不提交、不入队。
@@ -125,20 +125,17 @@ impl PricingParamView {
let texture_quality = match (texture, self.texture_quality) {
(true, Some(quality)) => quality,
(true, None) => {
return Err(Model3dRequestError::invalid(
"generation.textureQuality",
"texture=true 时必须显式给出贴图档位",
));
}
(false, Some(_)) => {
return Err(Model3dRequestError::invalid(
"generation.textureQuality",
"texture=false 时不允许出现贴图档位",
));
}
// 不生成贴图时贴图档位无意义,add-on 判定又被 texture 挡住,因此取标准档占位。
(false, None) => Model3dTextureQuality::Standard,
// 两处缺省都按 standard 计价:texture=true 时 standard 是 provider 的缺省档,
// 也是唯一覆盖全部模型族的档位(v2.5 拒绝任何显式 texture_quality,写死必填就会
// 让 H25 + texture=true 永远不可达);texture=false 时贴图档位本身无意义,
// add-on 判定已被 texture 挡住。
(_, None) => Model3dTextureQuality::Standard,
};
if !texture && self.pbr != Some(false) {
return Err(Model3dRequestError::invalid(
@@ -322,15 +319,25 @@ mod tests {
}
#[test]
fn texture_on_requires_texture_quality() {
fn texture_on_defaults_texture_quality_to_standard() {
let request = text_request(generation_with(json!({ "textureQuality": null })));
let error =
validate_text_to_model_request(&request).expect_err("texture=true 必须有贴图档位");
assert_eq!(
error.field().as_deref(),
Some("generation.textureQuality"),
"实际报错:{error}"
let query = validate_text_to_model_request(&request)
.unwrap_or_else(|error| panic!("texture=true 缺省贴图档位应通过校验:{error}"));
assert!(
!query.add_ons.contains(Model3dAddOn::HdTexture)
&& !query.add_ons.contains(Model3dAddOn::UltraTexture),
"缺省贴图档位按 standard 计价,不给任何贴图加价"
);
// v2.5 拒绝任何显式 texture_quality:必填规则会让 H25 + texture=true 永远不可达。
let request = text_request(generation_with(json!({
"model": "v2.5-20250123",
"textureQuality": null,
"geometryQuality": null
})));
let query = validate_text_to_model_request(&request)
.unwrap_or_else(|error| panic!("v2.5 缺省贴图档位应通过校验:{error}"));
assert!(query.texture, "v2.5 缺省贴图档位仍是带贴图请求");
}
#[test]