From a358553d2bf766d4b3023b7eddcc4edf77f76224 Mon Sep 17 00:00:00 2001 From: suzmii Date: Mon, 24 Aug 2026 12:25:52 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=B4=A0=E6=9D=90=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E8=B4=A6=E5=8F=B7=E7=BB=91=E5=AE=9A=E4=B8=8E=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E4=BA=8B=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除独立 canonical 参考缓存并统一使用账号资源 binding 配置缺失时保留可恢复私有账本并允许登录后绑定 失效精修引用仅失效当前主体对应的资源 binding 更新生成恢复测试以锁定 CandidateReady 和切号边界 --- .../src-tauri/src/agent/runtime_driver.rs | 6 +- .../src/project/asset_canvas/generation.rs | 634 +++--------------- 2 files changed, 112 insertions(+), 528 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs index 4ed06adc9..3d36dd892 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs @@ -263,9 +263,9 @@ pub(crate) use entrypoints::{ configure_game_creator_manifest_invalidation_event_sink, emit_direct_game_creator_progress, emit_game_creator_agent_runtime_update, emit_game_creator_manifest_invalidated, game_creator_agent_runtime_update_event, generate_local_game_draft_at, - install_game_creator_manifest_invalidation_event_sink, - read_game_creator_agent_runtime_at, read_game_creator_agent_runtime_for_session_at, - read_game_creator_agent_runtimes_at, set_game_creator_agent_runtime_update_app_handle, + install_game_creator_manifest_invalidation_event_sink, read_game_creator_agent_runtime_at, + read_game_creator_agent_runtime_for_session_at, read_game_creator_agent_runtimes_at, + set_game_creator_agent_runtime_update_app_handle, start_game_creator_manifest_invalidation_event_sink, validate_game_creator_manifest_invalidation_event_sink, }; diff --git a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs index 3f5e58d8c..f45604b4e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/project/asset_canvas/generation.rs @@ -12,18 +12,12 @@ const ASSET_CANVAS_SERVICE_IDENTITY_CONFIRMATION_TTL_MILLIS: u64 = 10 * 60 * 1_0 const ASSET_CANVAS_RESOURCE_EDIT_QUEUE_SOURCE: &str = "game-creator-resource-editor"; const ASSET_CANVAS_REFERENCE_UPLOAD_LEGACY_PREFIX: &str = "generated-character-drafts"; const ASSET_CANVAS_REFERENCE_UPLOAD_NAMESPACE: &str = "asset-canvas-references"; -const ASSET_CANVAS_CANONICAL_REFERENCE_CACHE_SCHEMA_VERSION: &str = - "game-creator-asset-canvas-canonical-reference.v1"; -const ASSET_CANVAS_CANONICAL_REFERENCE_CACHE_MAX_BYTES: usize = 32 * 1024; const ASSET_CANVAS_PLATFORM_ACCESS_SCHEME: &str = "official-platform-v1"; const ASSET_CANVAS_DEVELOPER_ACCESS_SCHEME: &str = "external-developer-v1"; static ASSET_CANVAS_GENERATION_LOCKS: OnceLock< tokio::sync::Mutex>>>, > = OnceLock::new(); -static ASSET_CANVAS_CANONICAL_REFERENCE_LOCKS: OnceLock< - tokio::sync::Mutex>>>, -> = OnceLock::new(); #[cfg(test)] #[derive(Clone)] @@ -304,30 +298,6 @@ struct PrivateReferenceState { upload_completed: bool, } -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(deny_unknown_fields, rename_all = "camelCase")] -struct PrivateCanonicalReferenceIdentity { - service_identity_fingerprint: String, - platform_owner_binding_sha256: String, - editor_project_id: String, - source_sha256: String, - canonical_asset_kind: String, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(deny_unknown_fields, rename_all = "camelCase")] -struct PrivateCanonicalReferenceCache { - schema_version: String, - cache_key_sha256: String, - identity: PrivateCanonicalReferenceIdentity, - resource_id: String, - object_key: String, - asset_object_id: String, - width: u32, - height: u32, - created_at: u64, -} - #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct PrivateRemoteResult { @@ -391,7 +361,8 @@ struct AssetCanvasGenerationLedger { reference_states: Vec, resolved_reference_ids: Vec, #[serde(default)] - canonical_reference_repair_attempted: bool, + #[serde(alias = "canonicalReferenceRepairAttempted")] + reference_binding_repair_attempted: bool, access_scheme: Option, #[serde(default)] api_identity_scheme: Option, @@ -868,182 +839,6 @@ fn generation_ledger_relative_path(generation_id: &str) -> String { format!("{ASSET_CANVAS_ROOT}/generations/{generation_id}.json") } -fn canonical_reference_cache_relative_path(cache_key_sha256: &str) -> String { - format!("{ASSET_CANVAS_ROOT}/references/canonical/{cache_key_sha256}.json") -} - -fn canonical_reference_platform_owner_binding_sha256( - ledger: &AssetCanvasGenerationLedger, - api_mode: &CanvasGenerationApiMode, -) -> Result { - let binding = match ( - ledger.platform_owner_user_id.as_deref(), - ledger.platform_owner_api_base_url.as_deref(), - ) { - (Some(user_id), Some(api_base_url)) => serde_json::json!({ - "scheme": "platform-session", - "userId": user_id, - "apiBaseUrl": api_base_url, - }), - (None, None) => serde_json::json!({ - "scheme": "external-api-key", - "credentialSha256": asset_canvas_sha256(api_mode.bearer_token().as_bytes()), - }), - _ => return Err("素材画布 canonical 参考的账号绑定不完整".to_string()), - }; - let bytes = serde_json::to_vec(&binding) - .map_err(|_| "序列化素材画布 canonical 参考账号绑定失败".to_string())?; - Ok(asset_canvas_sha256(&bytes)) -} - -fn canonical_reference_identity( - ledger: &AssetCanvasGenerationLedger, - api_base_url: &str, - api_mode: &CanvasGenerationApiMode, - material: &ReferenceMaterial, -) -> Result { - let context = ledger - .canvas_context - .as_ref() - .ok_or_else(|| "素材画布 canonical 参考缺少 External canvas context".to_string())?; - let source_sha256 = material - .sha256 - .as_deref() - .ok_or_else(|| "素材画布 canonical 参考缺少本地源摘要".to_string())?; - let source_asset_kind = material - .asset_kind - .as_deref() - .unwrap_or(ledger.asset_kind.as_str()); - let canonical_asset_kind = editor_reference_asset_kind(source_asset_kind) - .unwrap_or("default-image") - .to_string(); - Ok(PrivateCanonicalReferenceIdentity { - service_identity_fingerprint: canvas_api_identity_fingerprint(api_base_url, api_mode, None), - platform_owner_binding_sha256: canonical_reference_platform_owner_binding_sha256( - ledger, api_mode, - )?, - editor_project_id: context.project_id.clone(), - source_sha256: source_sha256.to_string(), - canonical_asset_kind, - }) -} - -fn canonical_reference_cache_key_sha256( - identity: &PrivateCanonicalReferenceIdentity, -) -> Result { - let bytes = serde_json::to_vec(identity) - .map_err(|_| "序列化素材画布 canonical 参考身份失败".to_string())?; - Ok(asset_canvas_sha256(&bytes)) -} - -fn validate_canonical_reference_cache( - cache: &PrivateCanonicalReferenceCache, -) -> Result<(), String> { - let expected_key = canonical_reference_cache_key_sha256(&cache.identity)?; - if cache.schema_version != ASSET_CANVAS_CANONICAL_REFERENCE_CACHE_SCHEMA_VERSION - || cache.cache_key_sha256 != expected_key - || cache.identity.service_identity_fingerprint.len() != 64 - || !cache - .identity - .service_identity_fingerprint - .chars() - .all(|character| character.is_ascii_hexdigit()) - || cache.identity.platform_owner_binding_sha256.len() != 64 - || !cache - .identity - .platform_owner_binding_sha256 - .chars() - .all(|character| character.is_ascii_hexdigit()) - || cache.identity.editor_project_id.trim().is_empty() - || cache.identity.editor_project_id.len() > 512 - || cache - .identity - .editor_project_id - .chars() - .any(char::is_control) - || cache.identity.source_sha256.len() != 64 - || !cache - .identity - .source_sha256 - .chars() - .all(|character| character.is_ascii_hexdigit()) - || cache.identity.canonical_asset_kind.trim().is_empty() - || cache.identity.canonical_asset_kind.len() > 128 - || cache - .identity - .canonical_asset_kind - .chars() - .any(char::is_control) - || !is_registered_editor_reference_id(&cache.resource_id) - || cache.object_key.trim().is_empty() - || cache.object_key.len() > 2048 - || cache.object_key.chars().any(char::is_control) - || cache.asset_object_id.trim().is_empty() - || cache.asset_object_id.len() > 512 - || cache.asset_object_id.chars().any(char::is_control) - || cache.width == 0 - || cache.height == 0 - { - return Err("素材画布 canonical 参考缓存无效".to_string()); - } - validate_safe_revision(cache.created_at, "canonicalReference.createdAt") -} - -fn read_canonical_reference_cache( - root: &Path, - identity: &PrivateCanonicalReferenceIdentity, -) -> Result, String> { - let cache_key_sha256 = canonical_reference_cache_key_sha256(identity)?; - let cache = match read_agent_runtime_json_sidecar_with_max_bytes::( - root, - &canonical_reference_cache_relative_path(&cache_key_sha256), - "素材画布 canonical 参考缓存", - ASSET_CANVAS_CANONICAL_REFERENCE_CACHE_MAX_BYTES, - ) { - Ok(cache) => cache, - Err(_) => None, - }; - let Some(cache) = cache else { - return Ok(None); - }; - if validate_canonical_reference_cache(&cache).is_err() - || cache.cache_key_sha256 != cache_key_sha256 - || cache.identity != *identity - { - return Ok(None); - } - Ok(Some(cache)) -} - -fn write_canonical_reference_cache( - root: &Path, - cache: &PrivateCanonicalReferenceCache, -) -> Result<(), String> { - validate_canonical_reference_cache(cache)?; - write_agent_runtime_json_sidecar_with_max_bytes( - root, - &canonical_reference_cache_relative_path(&cache.cache_key_sha256), - "素材画布 canonical 参考缓存", - cache, - ASSET_CANVAS_CANONICAL_REFERENCE_CACHE_MAX_BYTES, - ) -} - -async fn canonical_reference_singleflight_lock( - cache_key_sha256: &str, -) -> tokio::sync::OwnedMutexGuard<()> { - let locks = ASSET_CANVAS_CANONICAL_REFERENCE_LOCKS - .get_or_init(|| tokio::sync::Mutex::new(HashMap::new())); - let lock = { - let mut locks = locks.lock().await; - locks - .entry(cache_key_sha256.to_string()) - .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))) - .clone() - }; - lock.lock_owned().await -} - fn generation_progress_event( ledger: &AssetCanvasGenerationLedger, phase: &str, @@ -2133,7 +1928,7 @@ fn validate_and_prepare_ledger( requested_reference_resource_ids: references, reference_states: Vec::new(), resolved_reference_ids: Vec::new(), - canonical_reference_repair_attempted: false, + reference_binding_repair_attempted: false, access_scheme: None, api_identity_scheme: None, api_identity_fingerprint: None, @@ -2159,7 +1954,11 @@ fn validate_and_prepare_ledger( created_at: now, updated_at: now, }; - initialize_generation_access_identity(root, &mut ledger, api_base_url, platform_session)?; + if api_base_url.is_empty() && platform_session.is_none() { + write_generation_ledger(root, &mut ledger)?; + } else { + initialize_generation_access_identity(root, &mut ledger, api_base_url, platform_session)?; + } Ok(ledger) } @@ -2447,10 +2246,6 @@ async fn register_uploaded_image_reference( let height = material .height .ok_or(ReferencePreparationError::MaterialInvalid)?; - let asset_kind = material - .asset_kind - .as_deref() - .unwrap_or(ledger.asset_kind.as_str()); let endpoint = format!( "/api/external/v1/editor/projects/{}/resources", percent_encode_query_component(&context.project_id) @@ -2469,12 +2264,15 @@ async fn register_uploaded_image_reference( "sourceSha256": material.sha256, }, }); - if let Some(asset_kind) = editor_reference_asset_kind(source_asset_kind) { - request_body - .as_object_mut() - .ok_or(ReferencePreparationError::Internal)? - .insert("assetKind".to_string(), serde_json::json!(asset_kind)); - } + let registered_asset_kind = + editor_reference_asset_kind(source_asset_kind).unwrap_or(source_asset_kind); + request_body + .as_object_mut() + .ok_or(ReferencePreparationError::Internal)? + .insert( + "assetKind".to_string(), + serde_json::json!(registered_asset_kind), + ); let payload = external_editor_json_request( authorize_canvas_request( client @@ -2500,39 +2298,12 @@ async fn register_uploaded_image_reference( json_string_field(resource, "resourceId").ok_or(ReferencePreparationError::ConfirmFailed) } -fn cache_registered_canonical_reference( - root: &Path, - identity: PrivateCanonicalReferenceIdentity, - material: &ReferenceMaterial, - resource_id: &str, - object_key: &str, - asset_object_id: &str, -) -> Result<(), ReferencePreparationError> { - let cache_key_sha256 = canonical_reference_cache_key_sha256(&identity) - .map_err(|_| ReferencePreparationError::Internal)?; - let cache = PrivateCanonicalReferenceCache { - schema_version: ASSET_CANVAS_CANONICAL_REFERENCE_CACHE_SCHEMA_VERSION.to_string(), - cache_key_sha256, - identity, - resource_id: resource_id.to_string(), - object_key: object_key.to_string(), - asset_object_id: asset_object_id.to_string(), - width: material - .width - .ok_or(ReferencePreparationError::MaterialInvalid)?, - height: material - .height - .ok_or(ReferencePreparationError::MaterialInvalid)?, - created_at: asset_canvas_now(), - }; - write_canonical_reference_cache(root, &cache).map_err(|_| ReferencePreparationError::Internal) -} - -fn invalidate_canonical_reference_cache_for_ledger( +fn invalidate_reference_binding_for_ledger( root: &Path, ledger: &AssetCanvasGenerationLedger, api_base_url: &str, api_mode: &CanvasGenerationApiMode, + platform_session: Option<&PlatformSessionSnapshot>, ) -> Result { if ledger.intent != AssetCanvasIntent::Refine { return Ok(false); @@ -2542,21 +2313,40 @@ fn invalidate_canonical_reference_cache_for_ledger( .as_deref() .ok_or_else(|| "精修生成缺少源资源 ID".to_string())?; let material = reference_material_at(root, ledger, source_resource_id)?; - let identity = canonical_reference_identity(ledger, api_base_url, api_mode, &material)?; - let cache_key_sha256 = canonical_reference_cache_key_sha256(&identity)?; - let path = resolve_local_project_path( - root, - &canonical_reference_cache_relative_path(&cache_key_sha256), + let source_identity = new_external_editor_source_identity( + material + .local_asset_id + .as_deref() + .ok_or_else(|| "精修生成缺少本地源素材 ID".to_string())?, + material + .sha256 + .as_deref() + .ok_or_else(|| "精修生成缺少本地源摘要".to_string())?, + material + .media_type + .as_deref() + .ok_or_else(|| "精修生成缺少本地源媒体类型".to_string())?, + material + .asset_kind + .as_deref() + .ok_or_else(|| "精修生成缺少本地源素材类型".to_string())?, )?; - match fs::remove_file(path) { - Ok(()) => Ok(true), - Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false), - Err(error) => Err(format!( - "删除失效的素材画布 canonical 参考缓存失败: {error}" - )), - } + let context = ledger + .canvas_context + .as_ref() + .ok_or_else(|| "精修生成缺少 External canvas context".to_string())?; + let access = + ExternalEditorBindingAccess::new(api_base_url, api_mode.bearer_token(), platform_session)?; + access.validate_frozen_session()?; + let principal = external_editor_binding_principal(&access)?; + invalidate_external_editor_resource_binding_at( + root, + &ledger.project_id, + &principal, + &context.project_id, + &source_identity, + ) } - async fn ensure_reference_states( root: &Path, ledger: &mut AssetCanvasGenerationLedger, @@ -3607,8 +3397,11 @@ async fn reconcile_generation( finish_generation_failure_settlement_pending_at(root, &mut ledger, emit)?; return Err(sanitized_classified_generation_error(&code, reconciliation)); } - bind_generation_platform_owner(&ledger, platform_session)?; migrate_retryable_credential_failure(root, &mut ledger)?; + if !generation_has_service_identity_evidence(&ledger) { + initialize_generation_access_identity(root, &mut ledger, api_base_url, platform_session)?; + } + bind_generation_platform_owner(&ledger, platform_session)?; if ledger.phase == GenerationLedgerPhase::Failed { return Err(sanitized_generation_error( ledger.error_code.as_deref().unwrap_or("generation-failed"), @@ -3771,15 +3564,16 @@ async fn reconcile_generation( preserve_submit_reconciliation(&ledger.phase, code, reconciliation); if code == "source-reference-invalid" && ledger.intent == AssetCanvasIntent::Refine - && !ledger.canonical_reference_repair_attempted + && !ledger.reference_binding_repair_attempted && !reconciliation { - ledger.canonical_reference_repair_attempted = true; - if invalidate_canonical_reference_cache_for_ledger( + ledger.reference_binding_repair_attempted = true; + if invalidate_reference_binding_for_ledger( root, &ledger, api_base_url, api_mode, + platform_session, )? { ledger.endpoint = None; ledger.request_body_sha256 = None; @@ -4004,7 +3798,9 @@ async fn reconcile_generation( "candidate-reconciliation-required", emit, )?; - return Err(sanitized_generation_error("candidate-reconciliation-required")); + return Err(sanitized_generation_error( + "candidate-reconciliation-required", + )); } let (generation, draft, image) = persist_generation_candidate_at(root, &mut ledger)?; set_private_phase( @@ -5003,7 +4799,7 @@ mod tests { requested_reference_resource_ids: Vec::new(), reference_states: Vec::new(), resolved_reference_ids: Vec::new(), - canonical_reference_repair_attempted: false, + reference_binding_repair_attempted: false, access_scheme: Some(ASSET_CANVAS_DEVELOPER_ACCESS_SCHEME.to_string()), api_identity_scheme: None, api_identity_fingerprint: Some( @@ -5106,7 +4902,7 @@ mod tests { input.source_layer_id = Some(source_layer_id.clone()); input.placeholder = Some(placeholder.clone()); - let mut ledger = validate_and_prepare_ledger(directory.path(), &input) + let mut ledger = validate_and_prepare_ledger(directory.path(), &input, "", None) .expect("prepare placement ledger"); assert_eq!( ledger.source_layer_id.as_deref(), @@ -5295,9 +5091,11 @@ mod tests { width: 200.0, height: 200.0, }); - assert!(validate_and_prepare_ledger(directory.path(), &unsafe_input) - .expect_err("reject out-of-bounds placeholder") - .contains("安全画布边界")); + assert!( + validate_and_prepare_ledger(directory.path(), &unsafe_input, "", None) + .expect_err("reject out-of-bounds placeholder") + .contains("安全画布边界") + ); } #[tokio::test] @@ -5318,7 +5116,7 @@ mod tests { listener .set_nonblocking(true) .expect("set auth refresh fixture nonblocking"); - for request_index in 0..7 { + for request_index in 0..9 { let mut stream = accept_generation_fixture_connection( &listener, "auth refresh fixture", @@ -5347,6 +5145,15 @@ mod tests { "title": project_name, }]}}), ); + } else if request.starts_with("POST /api/external/v1/editor/projects ") { + write_json( + &mut stream, + "200 OK", + serde_json::json!({"data": {"project": { + "projectId": "auth-refresh-remote-project", + "title": project_name, + }}}), + ); } else if request.starts_with("GET /api/external/v1/editor/assets/library ") { write_json( &mut stream, @@ -5356,6 +5163,15 @@ mod tests { "label": project_name, }]}}}), ); + } else if request.starts_with("POST /api/external/v1/editor/assets/folders ") { + write_json( + &mut stream, + "200 OK", + serde_json::json!({"data": {"folder": { + "folderId": "auth-refresh-remote-folder", + "label": project_name, + }}}), + ); } else if request.starts_with("POST /api/external/v1/editor/images/generations ") { write_json( &mut stream, @@ -5454,7 +5270,7 @@ mod tests { ); let requests = std::iter::from_fn(|| receiver.try_recv().ok()).collect::>(); - assert_eq!(requests.len(), 7); + assert_eq!(requests.len(), 9); assert!(requests[1] .to_ascii_lowercase() .contains("authorization: bearer refreshed-key")); @@ -5733,8 +5549,8 @@ mod tests { &draft, "验证失败结算不会留下运行中任务", ); - let mut ledger = - validate_and_prepare_ledger(directory.path(), &input).expect("prepare generation"); + let mut ledger = validate_and_prepare_ledger(directory.path(), &input, "", None) + .expect("prepare generation"); set_private_phase( directory.path(), &mut ledger, @@ -5826,8 +5642,8 @@ mod tests { &draft, "无账本失败占位", ); - let mut ledger = - validate_and_prepare_ledger(directory.path(), &input).expect("prepare placeholder"); + let mut ledger = validate_and_prepare_ledger(directory.path(), &input, "", None) + .expect("prepare placeholder"); set_private_phase( directory.path(), &mut ledger, @@ -5880,8 +5696,8 @@ mod tests { &draft, "恢复生成流程内部失败的 pending 结算", ); - let mut ledger = - validate_and_prepare_ledger(directory.path(), &input).expect("prepare generation"); + let mut ledger = validate_and_prepare_ledger(directory.path(), &input, "", None) + .expect("prepare generation"); set_private_phase( directory.path(), &mut ledger, @@ -5960,8 +5776,8 @@ mod tests { &draft, "恢复尚未写入公开草稿的失败结算", ); - let mut ledger = - validate_and_prepare_ledger(directory.path(), &input).expect("prepare generation"); + let mut ledger = validate_and_prepare_ledger(directory.path(), &input, "", None) + .expect("prepare generation"); set_private_phase( directory.path(), &mut ledger, @@ -6019,8 +5835,8 @@ mod tests { &draft, "恢复已写公开草稿但未写终态账本的对账结算", ); - let mut ledger = - validate_and_prepare_ledger(directory.path(), &input).expect("prepare generation"); + let mut ledger = validate_and_prepare_ledger(directory.path(), &input, "", None) + .expect("prepare generation"); set_private_phase( directory.path(), &mut ledger, @@ -6112,8 +5928,8 @@ mod tests { &draft, "恢复已推进 staging 但未写终态账本的失败结算", ); - let mut ledger = - validate_and_prepare_ledger(directory.path(), &input).expect("prepare generation"); + let mut ledger = validate_and_prepare_ledger(directory.path(), &input, "", None) + .expect("prepare generation"); set_private_phase( directory.path(), &mut ledger, @@ -6205,8 +6021,8 @@ mod tests { &draft, "创建一个会明确失败的测试图片", ); - let mut ledger = - validate_and_prepare_ledger(directory.path(), &input).expect("prepare failed ledger"); + let mut ledger = validate_and_prepare_ledger(directory.path(), &input, "", None) + .expect("prepare failed ledger"); mark_generation_error( directory.path(), &mut ledger, @@ -6381,7 +6197,7 @@ mod tests { &draft, "创建一个不能归档的测试图片", ); - let mut ledger = validate_and_prepare_ledger(directory.path(), &input) + let mut ledger = validate_and_prepare_ledger(directory.path(), &input, "", None) .expect("prepare non-failed ledger"); if reconciliation { mark_generation_error( @@ -6435,235 +6251,6 @@ mod tests { } } - #[test] - fn canonical_reference_cache_key_binds_every_remote_and_source_identity_dimension() { - let identity = PrivateCanonicalReferenceIdentity { - service_identity_fingerprint: "1".repeat(64), - platform_owner_binding_sha256: "2".repeat(64), - editor_project_id: "editor-project-a".to_string(), - source_sha256: "3".repeat(64), - canonical_asset_kind: "scene".to_string(), - }; - let expected = canonical_reference_cache_key_sha256(&identity).expect("cache key"); - for changed in [ - PrivateCanonicalReferenceIdentity { - service_identity_fingerprint: "4".repeat(64), - ..identity.clone() - }, - PrivateCanonicalReferenceIdentity { - platform_owner_binding_sha256: "5".repeat(64), - ..identity.clone() - }, - PrivateCanonicalReferenceIdentity { - editor_project_id: "editor-project-b".to_string(), - ..identity.clone() - }, - PrivateCanonicalReferenceIdentity { - source_sha256: "6".repeat(64), - ..identity.clone() - }, - PrivateCanonicalReferenceIdentity { - canonical_asset_kind: "character".to_string(), - ..identity.clone() - }, - ] { - assert_ne!( - canonical_reference_cache_key_sha256(&changed).expect("changed cache key"), - expected - ); - } - } - - #[tokio::test] - async fn refine_replaces_legacy_registered_source_with_cached_canonical_reference() { - let project_id = "asset-canvas-canonical-reference-project"; - let project_name = "素材画布 canonical 精修来源测试"; - let legacy_resource_id = "editor-resource-legacy-game-background"; - let (directory, draft, source_png) = create_refine_generation_fixture_with_source( - project_id, - project_name, - "game-background", - Some(legacy_resource_id), - ); - let listener = TcpListener::bind("127.0.0.1:0").expect("bind canonical reference server"); - let base_url = format!( - "http://{}", - listener.local_addr().expect("canonical reference address") - ); - let upload_url = format!("{base_url}/oss-upload"); - let object_key = "canonical-references/game-background.png".to_string(); - let server_object_key = object_key.clone(); - let source_png_len = source_png.len(); - let (sender, receiver) = mpsc::channel(); - let server = std::thread::spawn(move || { - listener - .set_nonblocking(true) - .expect("set canonical reference fixture nonblocking"); - for request_index in 0..4 { - let mut stream = accept_generation_fixture_connection( - &listener, - "canonical reference fixture", - request_index, - ); - let request = read_http_request(&mut stream); - sender - .send(request.clone()) - .expect("capture canonical reference request"); - if request.starts_with("POST /api/assets/direct-upload-tickets ") - || request.starts_with("POST /api/external/v1/assets/direct-upload-tickets ") - { - write_json( - &mut stream, - "200 OK", - serde_json::json!({"data": {"upload": { - "host": upload_url, - "bucket": "canonical-reference-bucket", - "objectKey": server_object_key.clone(), - "successActionStatus": 204, - "maxSizeBytes": source_png_len, - "formFields": { - "key": server_object_key.clone(), - "success_action_status": "204", - "Content-Type": "image/png" - } - }}}), - ); - } else if request.starts_with("POST /oss-upload ") { - write_empty_response(&mut stream, "204 No Content"); - } else if request.starts_with("POST /api/assets/objects/confirm ") - || request.starts_with("POST /api/external/v1/assets/objects/confirm ") - { - write_json( - &mut stream, - "200 OK", - serde_json::json!({"data": {"assetObject": { - "objectKey": server_object_key.clone(), - "assetObjectId": "canonical-reference-object" - }}}), - ); - } else if request.starts_with("POST /api/editor/projects/remote-project/resources ") - || request.starts_with( - "POST /api/external/v1/editor/projects/remote-project/resources ", - ) - { - write_json( - &mut stream, - "200 OK", - serde_json::json!({"data": {"resource": { - "resourceId": "editor-resource-canonical-scene", - "projectId": "remote-project", - "objectKey": server_object_key.clone(), - "assetObjectId": "canonical-reference-object" - }}}), - ); - } else { - panic!("unexpected canonical reference request: {request}"); - } - } - }); - - let api_mode = CanvasGenerationApiMode { - api_key: "canonical-reference-private-api-key".to_string(), - }; - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(5)) - .build() - .expect("canonical reference client"); - let mut first_input = - generation_input(directory.path(), project_id, &draft, "把背景调整成黄昏光线"); - first_input.asset_kind = "game-background".to_string(); - let mut first_ledger = - validate_and_prepare_ledger(directory.path(), &first_input).expect("first ledger"); - first_ledger.canvas_context = Some(PrivateCanvasContext { - project_id: "remote-project".to_string(), - asset_folder_id: "remote-folder".to_string(), - canvas_name: project_name.to_string(), - }); - write_generation_ledger(directory.path(), &mut first_ledger) - .expect("persist first context"); - ensure_reference_states( - directory.path(), - &mut first_ledger, - &client, - &base_url, - &api_mode, - ) - .await - .expect("prepare first canonical reference"); - server.join().expect("join canonical reference server"); - - assert_eq!( - first_ledger.resolved_reference_ids, - vec!["editor-resource-canonical-scene".to_string()] - ); - assert!(!first_ledger - .resolved_reference_ids - .iter() - .any(|resource_id| resource_id == legacy_resource_id)); - let requests = std::iter::from_fn(|| receiver.try_recv().ok()).collect::>(); - assert_eq!(requests.len(), 4); - let confirm = requests - .iter() - .find(|request| { - request.starts_with("POST /api/assets/objects/confirm ") - || request.starts_with("POST /api/external/v1/assets/objects/confirm ") - }) - .expect("canonical confirm request"); - assert!(confirm.contains("\"assetKind\":\"scene\"")); - let register = requests - .iter() - .find(|request| request.contains("/remote-project/resources ")) - .expect("canonical registration request"); - assert!(register.contains("\"assetKind\":\"scene\"")); - - let mut second_input = - generation_input(directory.path(), project_id, &draft, "把背景调整成清晨光线"); - second_input.asset_kind = "game-background".to_string(); - let mut second_ledger = - validate_and_prepare_ledger(directory.path(), &second_input).expect("second ledger"); - second_ledger.canvas_context = first_ledger.canvas_context.clone(); - write_generation_ledger(directory.path(), &mut second_ledger) - .expect("persist second context"); - ensure_reference_states( - directory.path(), - &mut second_ledger, - &client, - &base_url, - &api_mode, - ) - .await - .expect("reuse canonical reference cache without remote calls"); - assert_eq!( - second_ledger.resolved_reference_ids, - vec!["editor-resource-canonical-scene".to_string()] - ); - assert_eq!( - second_ledger.reference_states[0].asset_object_id.as_deref(), - Some("canonical-reference-object") - ); - - let material = reference_material_at( - directory.path(), - &second_ledger, - second_ledger - .source_resource_id - .as_deref() - .expect("refine source id"), - ) - .expect("read canonical source material"); - let identity = - canonical_reference_identity(&second_ledger, &base_url, &api_mode, &material) - .expect("canonical cache identity"); - let cache = read_canonical_reference_cache(directory.path(), &identity) - .expect("read canonical cache") - .expect("canonical cache exists"); - assert_eq!(cache.identity.canonical_asset_kind, "scene"); - assert_eq!(cache.object_key, object_key); - let cache_json = serde_json::to_string(&cache).expect("serialize canonical cache"); - assert!(!cache_json.contains("canonical-reference-private-api-key")); - assert!(!cache_json.contains(legacy_resource_id)); - } - #[tokio::test] async fn local_refine_uploads_confirms_then_stages_candidate_and_preserves_the_source_asset() { let project_id = "asset-canvas-reference-upload-project"; @@ -6876,7 +6463,7 @@ mod tests { .contains(&format!("\"imageSrc\":\"/{}\"", expected_object_key))); assert!(requests[register_index].contains(&format!("\"width\":{source_width}"))); assert!(requests[register_index].contains(&format!("\"height\":{source_height}"))); - assert!(!requests[register_index].contains("\"assetKind\"")); + assert!(requests[register_index].contains("\"assetKind\":\"illustration\"")); assert!(requests[confirm_index].contains("\"assetKind\":\"image\"")); assert!(requests[submit_index].contains("\"sourceReferenceId\":\"editor-resource-source\"")); assert!(!requests[submit_index].contains("\"assetKind\"")); @@ -8364,7 +7951,7 @@ mod tests { "editor-resource-source".to_string(), "objects/style.png".to_string(), ], - canonical_reference_repair_attempted: false, + reference_binding_repair_attempted: false, access_scheme: Some(ASSET_CANVAS_DEVELOPER_ACCESS_SCHEME.to_string()), api_identity_scheme: Some(PLATFORM_ART_GENERATION_SERVICE_IDENTITY_SCHEME.to_string()), api_identity_fingerprint: Some("configuration".to_string()), @@ -9017,7 +8604,7 @@ mod tests { let ledger = read_generation_ledger(directory.path(), &generation_id) .expect("read session gate ledger") .expect("session gate ledger exists"); - assert_eq!(ledger.phase, GenerationLedgerPhase::ContextPreparing); + assert_eq!(ledger.phase, GenerationLedgerPhase::ReconciliationRequired); assert!(ledger.operation_id.is_none()); } @@ -9197,11 +8784,8 @@ mod tests { ) .expect("read resumed accepted ledger") .expect("resumed accepted ledger exists"); - assert_eq!( - committed.phase, - GenerationLedgerPhase::AssetDurableCommitted - ); - assert!(committed.commit_result.is_some()); + assert_eq!(committed.phase, GenerationLedgerPhase::CandidateReady); + assert!(committed.candidate_media_ref.is_some()); } #[test]