diff --git a/server-rs/crates/api-server/src/tripo3d/job.rs b/server-rs/crates/api-server/src/tripo3d/job.rs index 480e69589..1545823f8 100644 --- a/server-rs/crates/api-server/src/tripo3d/job.rs +++ b/server-rs/crates/api-server/src/tripo3d/job.rs @@ -5,6 +5,7 @@ use serde_json::Value; use shared_contracts::editor_canvas::EditorCanvasGenerationCompletionPayload as EditorCanvasGenerationCompletionRequest; +use shared_contracts::model3d::common::Model3dGenerationSource; use shared_contracts::model3d::image_to_model::Model3dImageToModelRequest; use shared_contracts::model3d::text_to_model::Model3dTextToModelRequest; @@ -85,6 +86,21 @@ impl Model3dJobRequest { } } + /// 本次生成派生自哪个画布资源;素材库对象与文生 3D 没有可登记的来源资源。 + /// + /// 资源 / 素材记录的 `source_resource_id` 记的是「派生来源」,所以只能从请求的 + /// `source` 取:本次新生成的输出资源 id 是产物自己,写进去等于自证来源。 + pub(crate) fn source_resource_id(&self) -> Option { + match self { + Self::TextToModel(_) => None, + Self::ImageToModel(request) => match &request.source { + Model3dGenerationSource::Resource { resource_id } => Some(resource_id.clone()), + // 素材库对象没有画布资源 id,没有可登记的来源资源。 + Model3dGenerationSource::Asset { .. } => None, + }, + } + } + /// 审计与落库用的模型版本线上取值;取值只由契约枚举的 serde rename 定义。 /// /// 该值会持久化进资源 / 素材记录的 `model` 字段,因此走严格的 @@ -340,6 +356,61 @@ mod tests { ); } + /// 派生来源只认请求里的 `source`:图生 3D 的 resource 变体带出资源 id, + /// 素材库来源与文生 3D 都没有可登记的来源资源。 + #[test] + fn source_resource_id_comes_from_the_request_source_only() { + let image_request = |source: Value| { + Model3dJobRequest::ImageToModel( + serde_json::from_value(json!({ + "source": source, + "generation": { + "model": "v3.1-20260211", + "texture": true, + "pbr": true, + "textureQuality": "standard", + "geometryQuality": "standard", + "quad": false, + "smartLowPoly": false, + "generateParts": false + }, + "projectId": "project-1" + })) + .expect("测试请求应可反序列化"), + ) + }; + + assert_eq!( + image_request(json!({ "kind": "resource", "resourceId": "resource-source" })) + .source_resource_id() + .as_deref(), + Some("resource-source") + ); + assert_eq!( + image_request(json!({ "kind": "asset", "assetId": "asset-1" })).source_resource_id(), + None + ); + + let text = Model3dJobRequest::TextToModel( + serde_json::from_value(json!({ + "generation": { + "prompt": "一只木箱", + "model": "v3.1-20260211", + "texture": true, + "pbr": true, + "textureQuality": "standard", + "geometryQuality": "standard", + "quad": false, + "smartLowPoly": false, + "generateParts": false + }, + "projectId": "project-1" + })) + .expect("测试请求应可反序列化"), + ); + assert_eq!(text.source_resource_id(), None); + } + #[test] fn job_request_is_parsed_after_stripping_platform_fingerprint() { let payload = json!({ diff --git a/server-rs/crates/api-server/src/tripo3d/worker.rs b/server-rs/crates/api-server/src/tripo3d/worker.rs index aa445cce7..a102b5eb8 100644 --- a/server-rs/crates/api-server/src/tripo3d/worker.rs +++ b/server-rs/crates/api-server/src/tripo3d/worker.rs @@ -298,6 +298,9 @@ async fn persist_result( ); let has_resource = project_id.is_some(); let has_asset = folder_id.is_some(); + // 派生来源只来自请求:资源 / 素材记录的 `source_resource_id` 记的是「从哪个画布资源 + // 派生出来的」,与本次新生成的输出资源 id 无关(图生 3D 的 source,文生 3D 没有)。 + let source_resource_id = request.source_resource_id(); let project_resource = project_id.map(|project_id| EditorProjectResourceCreateRecordInput { resource_id: resource_id.clone(), project_id, @@ -314,7 +317,7 @@ async fn persist_result( provider: Some(MODEL3D_PROVIDER_KIND.to_string()), // 平台自己的 operation ID;provider task ID 只留在 checkpoint,不落业务行。 task_id: Some(job.job_id.clone()), - source_resource_id: None, + source_resource_id: source_resource_id.clone(), asset_kind: Some(MODEL3D_ASSET_KIND.to_string()), generation_inputs_json: None, updated_at_micros: completed_at_micros, @@ -340,7 +343,11 @@ async fn persist_result( task_id: Some(job.job_id.clone()), asset_kind: Some(MODEL3D_ASSET_KIND.to_string()), generation_inputs_json: None, - source_resource_id: has_resource.then(|| resource_id.clone()), + // 素材行与画布资源行是同一份产物的两种落点:有本项目的资源行时指向它(与其它 + // 生成工具一致),只落素材库时退回请求里的来源资源,不把溯源信息丢掉。 + source_resource_id: has_resource + .then(|| resource_id.clone()) + .or_else(|| source_resource_id.clone()), generation_cost_mud_points: job.price_mud_points, now_micros: completed_at_micros, thumbnail_src: Some(preview_src.clone()),