From df6694c4a9869f022589e64b4379e72218048eac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 23 Sep 2026 18:51:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E7=94=9F=203D=20=E7=9A=84=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E6=B4=BE=E7=94=9F=E6=9D=A5=E6=BA=90=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=8F=96=E8=87=AA=E8=AF=B7=E6=B1=82=E7=9A=84=20source=20-=20jo?= =?UTF-8?q?b.rs=20=E6=96=B0=E5=A2=9E=20Model3dJobRequest::source=5Fresourc?= =?UTF-8?q?e=5Fid()=EF=BC=9Aresource=20=E6=9D=A5=E6=BA=90=E5=B8=A6?= =?UTF-8?q?=E5=87=BA=E7=94=BB=E5=B8=83=E8=B5=84=E6=BA=90=20id=EF=BC=8C?= =?UTF-8?q?=E7=B4=A0=E6=9D=90=E5=BA=93=E6=9D=A5=E6=BA=90=E4=B8=8E=E6=96=87?= =?UTF-8?q?=E7=94=9F=203D=20=E6=B2=A1=E6=9C=89=E5=8F=AF=E7=99=BB=E8=AE=B0?= =?UTF-8?q?=E7=9A=84=E6=9D=A5=E6=BA=90=E8=B5=84=E6=BA=90=20-=20worker.rs?= =?UTF-8?q?=20=E8=90=BD=E5=BA=93=E6=97=B6=E8=B5=84=E6=BA=90=E8=A1=8C?= =?UTF-8?q?=E4=B8=8E=E7=B4=A0=E6=9D=90=E8=A1=8C=E9=83=BD=E6=94=B9=E7=94=A8?= =?UTF-8?q?=E5=AE=83=EF=BC=9A=E8=B5=84=E6=BA=90=E8=A1=8C=E8=AE=B0=E6=B4=BE?= =?UTF-8?q?=E7=94=9F=E6=9D=A5=E6=BA=90=EF=BC=8C=E7=B4=A0=E6=9D=90=E8=A1=8C?= =?UTF-8?q?=E4=BC=98=E5=85=88=E6=8C=87=E5=90=91=E6=9C=AC=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=9A=84=E8=B5=84=E6=BA=90=E8=A1=8C=E3=80=81=E5=8F=AA=E8=90=BD?= =?UTF-8?q?=E7=B4=A0=E6=9D=90=E5=BA=93=E6=97=B6=E9=80=80=E5=9B=9E=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E9=87=8C=E7=9A=84=E6=9D=A5=E6=BA=90=E8=B5=84=E6=BA=90?= =?UTF-8?q?=20-=20=E6=96=B0=E5=A2=9E=E5=8D=95=E6=B5=8B=E9=92=89=E4=BD=8F?= =?UTF-8?q?=E4=B8=89=E7=A7=8D=E6=9D=A5=E6=BA=90=EF=BC=88resource=20/=20ass?= =?UTF-8?q?et=20/=20=E6=96=87=E7=94=9F=EF=BC=89=E7=9A=84=E5=8F=96=E5=80=BC?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=86=8D=E6=8B=BF=E6=9C=AC=E6=AC=A1=E6=96=B0?= =?UTF-8?q?=E7=94=9F=E6=88=90=E7=9A=84=E8=BE=93=E5=87=BA=E8=B5=84=E6=BA=90?= =?UTF-8?q?=20id=20=E5=86=92=E5=85=85=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crates/api-server/src/tripo3d/job.rs | 71 +++++++++++++++++++ .../crates/api-server/src/tripo3d/worker.rs | 11 ++- 2 files changed, 80 insertions(+), 2 deletions(-) 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()),