From b5aa68fc6a01f2160fdf65470b67af9205819d86 Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 6 Jul 2026 18:56:49 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E7=B2=BE=E9=80=89=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=85=AC=E5=BC=80=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增精选活动卡图片 objectKey 存储字段和迁移兼容。 新增后台活动卡图片 OSS 直传凭证接口。 移除素材包精选分类约束,允许未设置分类素材进入全部精选。 同步 admin 和前台读取活动卡 objectKey 的类型契约。 --- apps/admin-web/src/api/adminApiTypes.ts | 27 ++++ server-rs/crates/api-server/src/admin.rs | 124 ++++++++++++++++++ .../crates/api-server/src/editor_project.rs | 2 + .../crates/api-server/src/modules/admin.rs | 7 + .../crates/shared-contracts/src/admin.rs | 28 ++++ .../src/mapper/editor_project.rs | 4 + ..._showcase_campaign_config_snapshot_type.rs | 1 + .../editor_showcase_campaign_config_type.rs | 3 + ...wcase_campaign_config_upsert_input_type.rs | 1 + .../src/editor_project_storage.rs | 43 +++--- .../crates/spacetime-module/src/migration.rs | 8 ++ .../image-editor/editorProjectClient.ts | 1 + 12 files changed, 234 insertions(+), 15 deletions(-) diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index f63ac4588..8119889cb 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -457,6 +457,7 @@ export interface AdminEditorShowcaseCampaignPayload { enabled: boolean; title: string; imageSrc: string; + imageObjectKey?: string | null; prompt: string; author: string; costText: string; @@ -471,11 +472,37 @@ export interface AdminUpsertEditorShowcaseCampaignRequest { enabled: boolean; title: string; imageSrc: string; + imageObjectKey?: string | null; prompt: string; author: string; costText: string; } +export interface AdminDirectUploadTicketPayload { + bucket: string; + host: string; + objectKey: string; + legacyPublicPath: string; + contentType?: string | null; + formFields: Record; +} + +export interface AdminCreateEditorShowcaseCampaignImageUploadTicketRequest { + fileName: string; + contentType: string; + contentLength: number; +} + +export interface AdminCreateEditorShowcaseCampaignImageUploadTicketResponse { + upload: AdminDirectUploadTicketPayload; +} + +export interface AdminUploadedEditorShowcaseCampaignImage { + imageSrc: string; + imageObjectKey: string; + legacyPublicPath: string; +} + export interface AdminUpsertProfileRedeemCodeRequest { code: string; mode: ProfileRedeemCodeMode; diff --git a/server-rs/crates/api-server/src/admin.rs b/server-rs/crates/api-server/src/admin.rs index 2f39d312a..de103847d 100644 --- a/server-rs/crates/api-server/src/admin.rs +++ b/server-rs/crates/api-server/src/admin.rs @@ -18,6 +18,9 @@ use reqwest::Client; use serde::Deserialize; use serde_json::{Map, Value}; use shared_contracts::admin::{ + AdminCreateEditorShowcaseCampaignImageUploadTicketRequest, + AdminCreateEditorShowcaseCampaignImageUploadTicketResponse, + AdminDirectUploadTicketPayload, AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload, AdminDashboardBreakdownRowPayload, AdminDashboardChartBucketPayload, AdminDashboardChartPayload, AdminDashboardMetricsPayload, AdminDashboardOperationMetricPayload, @@ -38,6 +41,9 @@ use shared_contracts::admin::{ AdminUpsertEditorShowcaseCampaignRequest, AdminUpsertPublicWorkInteractionConfigRequest, AdminWorkVisibilityListResponse, }; +use shared_contracts::assets::{ + CreateDirectUploadTicketRequest, DirectUploadObjectAccess, +}; use shared_contracts::creation_entry_config::{ encode_unified_creation_spec_response, validate_unified_creation_spec_for_play, }; @@ -52,6 +58,7 @@ use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use crate::{ api_response::json_success_body, + assets::create_direct_upload_ticket_for_owner, editor_generation_config::EditorGenerationPricingConfig, editor_project::current_utc_micros, http_error::AppError, @@ -83,6 +90,7 @@ const ADMIN_DASHBOARD_MAX_PERIOD_DAYS: i64 = 366; const ADMIN_EDITOR_ASSET_DEFAULT_LIMIT: u32 = 80; const ADMIN_EDITOR_ASSET_MAX_LIMIT: u32 = 200; const ADMIN_DASHBOARD_ACTIVE_WINDOW_MICROS: i64 = 5 * 60 * 1_000_000; +const ADMIN_EDITOR_SHOWCASE_CAMPAIGN_IMAGE_MAX_BYTES: u64 = 20 * 1024 * 1024; #[derive(Clone, Debug)] pub struct AuthenticatedAdmin { @@ -578,6 +586,7 @@ pub async fn admin_upsert_editor_showcase_campaign( enabled: payload.enabled, title: payload.title, image_src: payload.image_src, + image_object_key: payload.image_object_key, prompt: payload.prompt, author: payload.author, cost_text: payload.cost_text, @@ -592,6 +601,79 @@ pub async fn admin_upsert_editor_showcase_campaign( )) } +pub async fn admin_create_editor_showcase_campaign_image_upload_ticket( + State(state): State, + Extension(request_context): Extension, + Extension(admin): Extension, + Json(payload): Json, +) -> Result, AppError> { + let file_name = payload.file_name.trim(); + if file_name.is_empty() { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_message("图片文件名不能为空") + ); + } + let content_type = payload.content_type.trim(); + if !content_type.starts_with("image/") { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_message("活动卡只能上传图片") + ); + } + if payload.content_length == 0 { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_message("图片文件不能为空") + ); + } + if payload.content_length > ADMIN_EDITOR_SHOWCASE_CAMPAIGN_IMAGE_MAX_BYTES { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST) + .with_message("活动卡图片不能超过 20MB") + ); + } + + let ticket = create_direct_upload_ticket_for_owner( + &state, + &request_context, + admin.session().subject.as_str(), + CreateDirectUploadTicketRequest { + legacy_prefix: "generated-character-drafts".to_string(), + path_segments: vec![ + "editor".to_string(), + "showcase-campaign".to_string(), + current_utc_micros().to_string(), + ], + file_name: file_name.to_string(), + content_type: Some(content_type.to_string()), + access: Some(DirectUploadObjectAccess::Private), + metadata: BTreeMap::from([ + ( + "asset_kind".to_string(), + "editor_showcase_campaign_image".to_string(), + ), + ("source".to_string(), "admin_editor_showcase_campaign".to_string()), + ]), + max_size_bytes: Some(payload.content_length), + expire_seconds: None, + success_action_status: Some(200), + }, + ) + .await?; + let axum::Json(value) = ticket; + let payload_value = value.get("data").cloned().unwrap_or(value); + let response: shared_contracts::assets::CreateDirectUploadTicketResponse = + serde_json::from_value(payload_value).map_err(|error| { + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(serde_json::json!({ + "message": format!("活动卡上传凭证响应解析失败:{error}"), + })) + })?; + Ok(json_success_body( + Some(&request_context), + AdminCreateEditorShowcaseCampaignImageUploadTicketResponse { + upload: admin_direct_upload_ticket_payload_from_asset_upload(response.upload), + }, + )) +} + fn build_admin_creation_entry_config_response( config: shared_contracts::creation_entry_config::CreationEntryConfigResponse, ) -> AdminCreationEntryConfigResponse { @@ -716,6 +798,7 @@ fn admin_editor_showcase_campaign_payload_from_record( enabled: record.enabled, title: record.title, image_src: record.image_src, + image_object_key: record.image_object_key, prompt: record.prompt, author: record.author, cost_text: record.cost_text, @@ -723,6 +806,47 @@ fn admin_editor_showcase_campaign_payload_from_record( } } +fn admin_direct_upload_ticket_payload_from_asset_upload( + upload: shared_contracts::assets::DirectUploadTicketPayload, +) -> AdminDirectUploadTicketPayload { + let mut form_fields = BTreeMap::from([ + ("key".to_string(), upload.form_fields.key), + ("policy".to_string(), upload.form_fields.policy), + ( + "x-oss-signature-version".to_string(), + upload.form_fields.signature_version, + ), + ( + "x-oss-credential".to_string(), + upload.form_fields.credential, + ), + ("x-oss-date".to_string(), upload.form_fields.date), + ( + "x-oss-signature".to_string(), + upload.form_fields.signature, + ), + ( + "success_action_status".to_string(), + upload.form_fields.success_action_status, + ), + ]); + if let Some(content_type) = upload.form_fields.content_type { + form_fields.insert("Content-Type".to_string(), content_type); + } + if let Some(cache_control) = upload.form_fields.cache_control { + form_fields.insert("Cache-Control".to_string(), cache_control); + } + form_fields.extend(upload.form_fields.metadata); + + AdminDirectUploadTicketPayload { + bucket: upload.bucket, + host: upload.host, + object_key: upload.object_key, + legacy_public_path: upload.legacy_public_path, + form_fields, + } +} + fn editor_showcase_admin_cursor(record: &EditorShowcaseAssetRecord) -> Option { let submitted_at_micros = parse_timestamp_text_to_micros(record.submitted_at.as_str())?; Some(format!("{submitted_at_micros}:{}", record.showcase_id)) diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 2435ea990..c675aff21 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -617,6 +617,7 @@ pub struct EditorShowcaseCampaignPayload { enabled: bool, title: String, image_src: String, + image_object_key: Option, prompt: String, author: String, cost_text: String, @@ -3350,6 +3351,7 @@ fn editor_showcase_campaign_payload_from_record( enabled: record.enabled, title: record.title, image_src: record.image_src, + image_object_key: record.image_object_key, prompt: record.prompt, author: record.author, cost_text: record.cost_text, diff --git a/server-rs/crates/api-server/src/modules/admin.rs b/server-rs/crates/api-server/src/modules/admin.rs index 025500d85..44e6b5afd 100644 --- a/server-rs/crates/api-server/src/modules/admin.rs +++ b/server-rs/crates/api-server/src/modules/admin.rs @@ -6,6 +6,7 @@ use axum::{ use crate::{ admin::{ admin_dashboard, admin_debug_http, admin_get_creation_entry_config, + admin_create_editor_showcase_campaign_image_upload_ticket, admin_get_editor_generation_pricing, admin_get_editor_showcase_campaign, admin_list_database_table_rows, admin_list_database_tables, admin_list_editor_assets, admin_list_editor_showcase_assets, admin_list_tracking_event_keys, @@ -152,6 +153,12 @@ pub fn router(state: AppState) -> Router { require_admin_auth, )), ) + .route( + "/admin/api/editor-showcase/campaign/image-upload-ticket", + post(admin_create_editor_showcase_campaign_image_upload_ticket).route_layer( + middleware::from_fn_with_state(state.clone(), require_admin_auth), + ), + ) .route( "/admin/api/works/visibility", get(admin_list_work_visibility) diff --git a/server-rs/crates/shared-contracts/src/admin.rs b/server-rs/crates/shared-contracts/src/admin.rs index cb9b83d7b..3f157b185 100644 --- a/server-rs/crates/shared-contracts/src/admin.rs +++ b/server-rs/crates/shared-contracts/src/admin.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -254,6 +256,7 @@ pub struct AdminEditorShowcaseCampaignPayload { pub enabled: bool, pub title: String, pub image_src: String, + pub image_object_key: Option, pub prompt: String, pub author: String, pub cost_text: String, @@ -272,11 +275,36 @@ pub struct AdminUpsertEditorShowcaseCampaignRequest { pub enabled: bool, pub title: String, pub image_src: String, + pub image_object_key: Option, pub prompt: String, pub author: String, pub cost_text: String, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminCreateEditorShowcaseCampaignImageUploadTicketRequest { + pub file_name: String, + pub content_type: String, + pub content_length: u64, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminCreateEditorShowcaseCampaignImageUploadTicketResponse { + pub upload: AdminDirectUploadTicketPayload, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminDirectUploadTicketPayload { + pub bucket: String, + pub host: String, + pub object_key: String, + pub legacy_public_path: String, + pub form_fields: BTreeMap, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AdminLoginResponse { diff --git a/server-rs/crates/spacetime-client/src/mapper/editor_project.rs b/server-rs/crates/spacetime-client/src/mapper/editor_project.rs index 38865c15f..5dce27889 100644 --- a/server-rs/crates/spacetime-client/src/mapper/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/mapper/editor_project.rs @@ -180,6 +180,7 @@ pub struct EditorShowcaseCampaignConfigRecord { pub enabled: bool, pub title: String, pub image_src: String, + pub image_object_key: Option, pub prompt: String, pub author: String, pub cost_text: String, @@ -426,6 +427,7 @@ pub struct EditorShowcaseCampaignConfigUpsertRecordInput { pub author: String, pub cost_text: String, pub updated_at_micros: i64, + pub image_object_key: Option, } impl From for crate::module_bindings::EditorProjectCreateInput { @@ -783,6 +785,7 @@ impl From author: input.author, cost_text: input.cost_text, updated_at_micros: input.updated_at_micros, + image_object_key: input.image_object_key, } } } @@ -1214,6 +1217,7 @@ fn map_editor_showcase_campaign_config_snapshot( enabled: snapshot.enabled, title: snapshot.title, image_src: snapshot.image_src, + image_object_key: snapshot.image_object_key, prompt: snapshot.prompt, author: snapshot.author, cost_text: snapshot.cost_text, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_snapshot_type.rs index 5f71e9552..2850679db 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_snapshot_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_snapshot_type.rs @@ -15,6 +15,7 @@ pub struct EditorShowcaseCampaignConfigSnapshot { pub cost_text: String, pub updated_by_admin_user_id: Option, pub updated_at_micros: i64, + pub image_object_key: Option, } impl __sdk::InModule for EditorShowcaseCampaignConfigSnapshot { diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_type.rs index 44b188966..dbf215e0d 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_type.rs @@ -16,6 +16,7 @@ pub struct EditorShowcaseCampaignConfig { pub cost_text: String, pub updated_by_admin_user_id: Option, pub updated_at: __sdk::Timestamp, + pub image_object_key: Option, } impl __sdk::InModule for EditorShowcaseCampaignConfig { @@ -36,6 +37,7 @@ pub struct EditorShowcaseCampaignConfigCols { pub updated_by_admin_user_id: __sdk::__query_builder::Col>, pub updated_at: __sdk::__query_builder::Col, + pub image_object_key: __sdk::__query_builder::Col>, } impl __sdk::__query_builder::HasCols for EditorShowcaseCampaignConfig { @@ -54,6 +56,7 @@ impl __sdk::__query_builder::HasCols for EditorShowcaseCampaignConfig { "updated_by_admin_user_id", ), updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + image_object_key: __sdk::__query_builder::Col::new(table_name, "image_object_key"), } } } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_upsert_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_upsert_input_type.rs index 813101fd1..b093fc4e0 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_upsert_input_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_upsert_input_type.rs @@ -15,6 +15,7 @@ pub struct EditorShowcaseCampaignConfigUpsertInput { pub author: String, pub cost_text: String, pub updated_at_micros: i64, + pub image_object_key: Option, } impl __sdk::InModule for EditorShowcaseCampaignConfigUpsertInput { diff --git a/server-rs/crates/spacetime-module/src/editor_project_storage.rs b/server-rs/crates/spacetime-module/src/editor_project_storage.rs index ba6ff8e43..a330706c0 100644 --- a/server-rs/crates/spacetime-module/src/editor_project_storage.rs +++ b/server-rs/crates/spacetime-module/src/editor_project_storage.rs @@ -13,7 +13,7 @@ const EDITOR_SHOWCASE_STATUS_PENDING: &str = "pending"; const EDITOR_SHOWCASE_STATUS_APPROVED: &str = "approved"; const EDITOR_SHOWCASE_STATUS_REJECTED: &str = "rejected"; const EDITOR_SHOWCASE_CAMPAIGN_CONFIG_ID: &str = "global"; -const EDITOR_SHOWCASE_CATEGORIES: [&str; 5] = ["packs", "characters", "ui", "music", "marketing"]; +const EDITOR_SHOWCASE_CATEGORIES: [&str; 4] = ["characters", "ui", "music", "marketing"]; #[spacetimedb::table( accessor = editor_project, @@ -205,6 +205,8 @@ pub struct EditorShowcaseCampaignConfig { cost_text: String, updated_by_admin_user_id: Option, updated_at: Timestamp, + #[default(None::)] + image_object_key: Option, } #[derive(Clone, Debug, PartialEq, SpacetimeType)] @@ -463,6 +465,7 @@ pub struct EditorShowcaseCampaignConfigSnapshot { pub cost_text: String, pub updated_by_admin_user_id: Option, pub updated_at_micros: i64, + pub image_object_key: Option, } #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] @@ -617,6 +620,7 @@ pub struct EditorShowcaseCampaignConfigUpsertInput { pub author: String, pub cost_text: String, pub updated_at_micros: i64, + pub image_object_key: Option, } #[derive(Clone, Debug, PartialEq, SpacetimeType)] @@ -1931,10 +1935,6 @@ fn list_public_editor_showcase_assets( && asset.display_enabled && !asset.asset_deleted_while_pending && (asset.refund_mud_points == 0 || asset.refund_completed_at.is_some()) - && asset - .showcase_category - .as_deref() - .is_some_and(is_valid_showcase_category) && !asset.image_src.trim().is_empty() && is_after_public_showcase_cursor( approved_at_micros, @@ -2077,14 +2077,11 @@ fn update_editor_showcase_asset_display( if asset.asset_deleted_while_pending { return Err("素材已删除,不能公开展示".to_string()); } - let showcase_category = input - .showcase_category - .map(|value| normalize_showcase_category(value.as_str())) - .transpose()? - .or_else(|| asset.showcase_category.clone()); - if input.display_enabled && showcase_category.is_none() { - return Err("公开展示前必须先设置精选分类".to_string()); - } + let showcase_category = match input.showcase_category { + Some(value) if value.trim().is_empty() => None, + Some(value) => Some(normalize_showcase_category(value.as_str())?), + None => asset.showcase_category.clone(), + }; if input.display_enabled && asset.refund_mud_points > 0 && asset.refund_completed_at.is_none() { return Err("精选返还完成前不能公开展示".to_string()); } @@ -2203,7 +2200,9 @@ fn upsert_editor_showcase_campaign_config( "editor_showcase_campaign_config.updated_by_admin_user_id", )?; let title = input.title.trim().to_string(); - let image_src = input.image_src.trim().to_string(); + let image_object_key = normalize_optional(input.image_object_key) + .map(|value| value.trim_start_matches('/').to_string()); + let image_src = normalize_campaign_image_src(input.image_src.as_str(), image_object_key.as_deref()); if input.enabled { if title.is_empty() { return Err("editor_showcase_campaign_config.title 不能为空".to_string()); @@ -2238,6 +2237,7 @@ fn upsert_editor_showcase_campaign_config( cost_text: input.cost_text.trim().to_string(), updated_by_admin_user_id: Some(admin_user_id), updated_at: now, + image_object_key, }); ctx.db .editor_showcase_campaign_config() @@ -2614,7 +2614,7 @@ fn normalize_showcase_category(value: &str) -> Result { if is_valid_showcase_category(normalized) { return Ok(normalized.to_string()); } - Err("精选分类只支持 packs、characters、ui、music 或 marketing".to_string()) + Err("精选分类只支持 characters、ui、music 或 marketing".to_string()) } fn is_valid_showcase_category(value: &str) -> bool { @@ -2712,9 +2712,22 @@ fn showcase_campaign_config_snapshot_from_row( cost_text: row.cost_text, updated_by_admin_user_id: row.updated_by_admin_user_id, updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + image_object_key: row.image_object_key, } } +fn normalize_campaign_image_src(raw_image_src: &str, image_object_key: Option<&str>) -> String { + let image_src = raw_image_src.trim(); + if !image_src.is_empty() { + return image_src.to_string(); + } + image_object_key + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(|value| format!("/{}", value.trim_start_matches('/'))) + .unwrap_or_default() +} + fn timestamp_option_micros(value: Option) -> Option { value.map(|timestamp| timestamp.to_micros_since_unix_epoch()) } diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index ad2897b97..1be3b8d9d 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -1265,6 +1265,14 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde .or_insert(serde_json::Value::Null); } } + if table_name == "editor_showcase_campaign_config" { + if let Some(object) = next_value.as_object_mut() { + // 中文注释:活动卡图片 Object Key 晚于活动卡表加入,旧迁移包按仅有图片地址兼容。 + object + .entry("image_object_key".to_string()) + .or_insert(serde_json::Value::Null); + } + } if table_name == "big_fish_creation_session" { if let Some(object) = next_value.as_object_mut() { // 中文注释:旧迁移包没有公开游玩次数字段,导入时按新建作品默认 0 兼容。 diff --git a/src/services/image-editor/editorProjectClient.ts b/src/services/image-editor/editorProjectClient.ts index 18b9020f3..26b6c9320 100644 --- a/src/services/image-editor/editorProjectClient.ts +++ b/src/services/image-editor/editorProjectClient.ts @@ -138,6 +138,7 @@ export type EditorShowcaseCampaignSnapshot = { enabled: boolean; title: string; imageSrc: string; + imageObjectKey?: string | null; prompt: string; author: string; costText: string;