diff --git a/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md b/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md index 128f99d70..d62fd65f5 100644 --- a/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md +++ b/docs/technical/【技术方案】Tripo 3D生成API集成-2026-09-21.md @@ -66,7 +66,8 @@ canvasCompletion? 画布占位框回填,只在项目资源落点下生效 **归一在预检之前完成并写回请求**:项目 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`、`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 的隐式默认值会直接改变价格,一旦依赖默认值,报价与扣费会在“调用方少传字段”时分叉。 +**定价相关参数在 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 预检只拒绝会启用能力的取值**:`geometry_quality` 对非 v3.1 / v3.0 家族只在取值 `detailed` 时拒绝,`standard` 放行;并且 `platform-tripo` 的请求映射会把「不支持的家族 + standard」收敛成「不发该字段」——不支持的家族传 `standard` 与不传等价(provider 默认档就是它,定价也同价),发出去只会换来「不支持该字段」的整单拒绝。 +其余会改价的参数仍必须显式给出:provider 的隐式默认值会直接改变价格,一旦依赖默认值,报价与扣费会在“调用方少传字段”时分叉。 两个 submit 都必须携带 `Idempotency-Key`,复用现有头部校验;缺失或格式非法直接 400,不静默生成键。 diff --git a/server-rs/crates/platform-tripo/src/common/mod.rs b/server-rs/crates/platform-tripo/src/common/mod.rs index 87a4fe4f0..30424cf3f 100644 --- a/server-rs/crates/platform-tripo/src/common/mod.rs +++ b/server-rs/crates/platform-tripo/src/common/mod.rs @@ -19,5 +19,6 @@ pub use types::{ }; pub(crate) use validation::{ TripoGenerationOptions, submitted_task_handle, validate_generation_options, validate_task_id, + wire_geometry_quality, }; pub(crate) use wire::{wire, wire_option}; diff --git a/server-rs/crates/platform-tripo/src/common/validation.rs b/server-rs/crates/platform-tripo/src/common/validation.rs index 9630ee3ec..d24e307ba 100644 --- a/server-rs/crates/platform-tripo/src/common/validation.rs +++ b/server-rs/crates/platform-tripo/src/common/validation.rs @@ -108,13 +108,18 @@ pub(crate) fn validate_generation_options( )); } - if options.geometry_quality.is_some() && !matches!(family, ModelFamily::H31 | ModelFamily::H30) + // 只拒绝「启用加价」的取值,与 quad / smart_low_poly / generate_parts 的处理对称: + // 非 v3 家族把 standard 传出去只会换来 provider 的「不支持该字段」,它等价于不传。 + if options + .geometry_quality + .is_some_and(|quality| matches!(quality, Model3dGeometryQuality::Detailed)) + && !family.supports_geometry_quality() { return Err(invalid( Some(TripoField::GeometryQuality), TripoValidationReason::InvalidCombination, &format!( - "geometry_quality is only supported by {}", + "geometry_quality=detailed is only supported by {}", v3_model_labels() ), )); @@ -238,6 +243,32 @@ impl From for ModelFamily { } } +impl ModelFamily { + /// 是否支持 `geometry_quality`:只有 v3.x 家族有这一档。 + const fn supports_geometry_quality(self) -> bool { + matches!(self, Self::H31 | Self::H30) + } +} + +/// 把「等于 provider 默认档」的几何质量收敛成「不发」。 +/// +/// 不支持的家族传 `standard` 与不传完全等价(两边的默认档都是 standard,定价也同价), +/// 但传出去会让 provider 以「不支持该字段」拒绝整次提交,所以这里直接不发。 +/// 非默认档(`detailed`)在预检里已经被拒,不会走到这里。 +pub(crate) fn wire_geometry_quality( + model: Model3dModelVersion, + quality: Option, +) -> Option { + match quality { + Some(Model3dGeometryQuality::Standard) + if !ModelFamily::from(model).supports_geometry_quality() => + { + None + } + other => other, + } +} + fn face_limit_bounds(family: ModelFamily, options: &TripoGenerationOptions) -> (i64, i64) { if options.smart_low_poly == Some(true) { return if options.quad == Some(true) { @@ -538,6 +569,60 @@ mod tests { } } + /// 几何质量只拒绝「会启用高清几何」的 detailed:非 v3 家族的 standard 必须放行, + /// 否则定价表里已定价的 v2.5 / P1 / P2 又会被这条规则挡回去。 + #[test] + fn geometry_quality_only_rejects_the_enabling_value_for_non_v3_families() { + for model in Model3dModelVersion::ALL { + let supported = matches!(model, Model3dModelVersion::H31 | Model3dModelVersion::H30); + + let mut options = minimal_options(*model); + options.geometry_quality = Some(Model3dGeometryQuality::Standard); + assert!( + validate_generation_options(&options).is_ok(), + "{model:?} 的 geometry_quality=standard 必须放行" + ); + + options.geometry_quality = Some(Model3dGeometryQuality::Detailed); + let result = validate_generation_options(&options); + assert_eq!( + result.is_ok(), + supported, + "{model:?} detailed 的期望结果不符" + ); + if let Err(error) = result { + assert_eq!( + rejection(error), + Some(( + Some(TripoField::GeometryQuality), + TripoValidationReason::InvalidCombination + )), + "{model:?}" + ); + } + } + } + + /// 不支持该参数的家族把 standard 收敛成「不发」:传出去只会换来 provider 的 + /// 「不支持该字段」,而它与不传完全等价(两边默认档都是 standard,定价也同价)。 + #[test] + fn wire_geometry_quality_drops_the_default_for_families_without_the_flag() { + for model in Model3dModelVersion::ALL { + let supported = matches!(model, Model3dModelVersion::H31 | Model3dModelVersion::H30); + assert_eq!( + wire_geometry_quality(*model, Some(Model3dGeometryQuality::Standard)), + supported.then_some(Model3dGeometryQuality::Standard), + "{model:?}" + ); + assert_eq!(wire_geometry_quality(*model, None), None, "{model:?}"); + assert_eq!( + wire_geometry_quality(*model, Some(Model3dGeometryQuality::Detailed)), + Some(Model3dGeometryQuality::Detailed), + "{model:?} 非默认档不在这里处理,交给预检拒绝" + ); + } + } + /// 空白 task id 是 provider 违约,按 OutputSchema 失败而不是参数错误。 #[test] fn blank_submitted_task_id_is_an_output_schema_error() { diff --git a/server-rs/crates/platform-tripo/src/image_to_model/client.rs b/server-rs/crates/platform-tripo/src/image_to_model/client.rs index 275c47dd8..d2fcd1cc7 100644 --- a/server-rs/crates/platform-tripo/src/image_to_model/client.rs +++ b/server-rs/crates/platform-tripo/src/image_to_model/client.rs @@ -3,7 +3,7 @@ use tripo3d_sdk::{models::FileInput, params::ImageToModelParams}; use crate::common::{ TripoError, TripoField, TripoProviderClient, TripoTaskHandle, TripoValidationReason, - extra_fields, submitted_task_handle, wire, wire_option, + extra_fields, submitted_task_handle, wire, wire_geometry_quality, wire_option, }; use super::validation::validate_image_to_model_params; @@ -123,7 +123,9 @@ fn to_sdk_params( texture_quality: wire_option(params.texture_quality.as_ref())?, extra: extra_fields(params.texture_version, params.delight)?, texture_alignment: wire_option(params.texture_alignment.as_ref())?, - geometry_quality: wire_option(params.geometry_quality.as_ref())?, + geometry_quality: wire_option( + wire_geometry_quality(params.model, params.geometry_quality).as_ref(), + )?, face_limit: params.face_limit, auto_size: params.auto_size, orientation: wire_option(params.orientation.as_ref())?, diff --git a/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs b/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs index 082d55459..4eb2393fe 100644 --- a/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs +++ b/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs @@ -6,7 +6,7 @@ use tripo3d_sdk::{models::FileInput, params::MultiviewToModelParams}; use crate::common::{ TripoError, TripoField, TripoGenerationOptions, TripoProviderClient, TripoTaskHandle, TripoValidationReason, extra_fields, submitted_task_handle, validate_generation_options, - validate_task_id, wire, wire_option, + validate_task_id, wire, wire_geometry_quality, wire_option, }; impl TripoProviderClient { @@ -89,7 +89,8 @@ fn to_sdk_params( params.pbr = request.pbr; params.texture_quality = wire_option(request.texture_quality.as_ref())?; params.extra = extra_fields(request.texture_version, request.delight)?; - params.geometry_quality = wire_option(request.geometry_quality.as_ref())?; + params.geometry_quality = + wire_option(wire_geometry_quality(request.model, request.geometry_quality).as_ref())?; params.texture_alignment = wire_option(request.texture_alignment.as_ref())?; params.face_limit = request.face_limit; params.auto_size = request.auto_size; diff --git a/server-rs/crates/platform-tripo/src/text_to_model/client.rs b/server-rs/crates/platform-tripo/src/text_to_model/client.rs index 63c1f9f12..fa5f9909b 100644 --- a/server-rs/crates/platform-tripo/src/text_to_model/client.rs +++ b/server-rs/crates/platform-tripo/src/text_to_model/client.rs @@ -1,6 +1,6 @@ use crate::common::{ TripoError, TripoProviderClient, TripoTaskHandle, extra_fields, submitted_task_handle, wire, - wire_option, + wire_geometry_quality, wire_option, }; use shared_contracts::model3d::text_to_model::Model3dTextToModelParams; @@ -37,7 +37,9 @@ fn to_sdk_params( pbr: params.pbr, texture_quality: wire_option(params.texture_quality.as_ref())?, extra: extra_fields(params.texture_version, params.delight)?, - geometry_quality: wire_option(params.geometry_quality.as_ref())?, + geometry_quality: wire_option( + wire_geometry_quality(params.model, params.geometry_quality).as_ref(), + )?, face_limit: params.face_limit, auto_size: params.auto_size, quad: params.quad,