Puzzle: support history images & partial generation
Allow history-generated image paths to be submitted where Data URLs were previously required and avoid treating partial/result-page generations as blocking the whole draft. Backend: resolve history /generated-* references via resolve_puzzle_reference_image_as_data_url and convert to PuzzleDownloadedImage; add PuzzleDownloadedImage::from_resolved_reference_image; extend draft handling to apply generated level metadata (auto-naming) and normalize generation_status to treat levels with images as ready. API: add shouldAutoNameLevel to action contracts and use it to request/refine generated level names. Spacetime/module and mappers: normalize completed level statuses when saving/reading so result-page background or per-level generation doesn't mask completed drafts. Frontend: expose resolver helpers, only mark a work as generating when no usable cover or ready level exists, keep level controls enabled during UI-background regeneration, and add tests covering history-image submission, auto-naming, and UI-background/partial-generation behaviors.
This commit is contained in:
@@ -1415,13 +1415,22 @@ pub(crate) async fn compile_puzzle_draft_with_uploaded_cover(
|
||||
"message": "关闭 AI 重绘时必须上传拼图图片。",
|
||||
}))
|
||||
})?;
|
||||
let uploaded_image = parse_puzzle_image_data_url(uploaded_image_src).ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
"field": "referenceImageSrc",
|
||||
"message": "关闭 AI 重绘时上传图必须是图片 Data URL。",
|
||||
}))
|
||||
})?;
|
||||
let http_client = reqwest::Client::new();
|
||||
let uploaded_downloaded_image =
|
||||
resolve_puzzle_reference_image_as_data_url(state, &http_client, uploaded_image_src)
|
||||
.await
|
||||
.map(PuzzleDownloadedImage::from_resolved_reference_image)
|
||||
.map_err(|error| {
|
||||
if error.status_code() == StatusCode::BAD_REQUEST {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
"field": "referenceImageSrc",
|
||||
"message": "关闭 AI 重绘时上传图必须是图片 Data URL 或历史生成图片路径。",
|
||||
}))
|
||||
} else {
|
||||
error
|
||||
}
|
||||
})?;
|
||||
let compiled_session = state
|
||||
.spacetime_client()
|
||||
.compile_puzzle_agent_draft(session_id.clone(), owner_user_id.clone(), now)
|
||||
@@ -1446,11 +1455,6 @@ pub(crate) async fn compile_puzzle_draft_with_uploaded_cover(
|
||||
compiled_session.session_id,
|
||||
target_level.candidates.len() + 1
|
||||
);
|
||||
let uploaded_downloaded_image = PuzzleDownloadedImage {
|
||||
extension: puzzle_mime_to_extension(uploaded_image.mime_type.as_str()).to_string(),
|
||||
mime_type: normalize_puzzle_downloaded_image_mime_type(uploaded_image.mime_type.as_str()),
|
||||
bytes: uploaded_image.bytes,
|
||||
};
|
||||
let level_name_future =
|
||||
generate_puzzle_first_level_name(state, &target_level.picture_description);
|
||||
let image_level_name_future = generate_puzzle_first_level_name_from_image(
|
||||
@@ -1807,6 +1811,45 @@ pub(crate) fn apply_generated_puzzle_initial_metadata_to_session_snapshot(
|
||||
session
|
||||
}
|
||||
|
||||
pub(crate) fn apply_generated_puzzle_metadata_to_session_snapshot(
|
||||
mut session: PuzzleAgentSessionRecord,
|
||||
target_level_id: &str,
|
||||
metadata: &PuzzleLevelNaming,
|
||||
previous_level_name: &str,
|
||||
updated_at_micros: i64,
|
||||
) -> PuzzleAgentSessionRecord {
|
||||
let Some(draft) = session.draft.as_mut() else {
|
||||
return session;
|
||||
};
|
||||
let Some(target_index) = draft
|
||||
.levels
|
||||
.iter()
|
||||
.position(|level| level.level_id == target_level_id)
|
||||
.or_else(|| (!draft.levels.is_empty()).then_some(0))
|
||||
else {
|
||||
return session;
|
||||
};
|
||||
|
||||
draft.levels[target_index].level_name = metadata.level_name.clone();
|
||||
if metadata.ui_background_prompt.is_some() {
|
||||
draft.levels[target_index].ui_background_prompt = metadata.ui_background_prompt.clone();
|
||||
}
|
||||
|
||||
if target_index == 0 {
|
||||
apply_generated_puzzle_initial_metadata_to_draft(
|
||||
draft,
|
||||
metadata,
|
||||
previous_level_name,
|
||||
updated_at_micros,
|
||||
);
|
||||
} else {
|
||||
sync_puzzle_primary_draft_fields_from_level(draft);
|
||||
}
|
||||
|
||||
session.updated_at = format_timestamp_micros(updated_at_micros);
|
||||
session
|
||||
}
|
||||
|
||||
pub(crate) fn apply_generated_puzzle_initial_metadata_to_draft(
|
||||
draft: &mut PuzzleResultDraftRecord,
|
||||
metadata: &PuzzleLevelNaming,
|
||||
|
||||
@@ -769,6 +769,21 @@ pub async fn execute_puzzle_agent_action(
|
||||
payload.prompt_text.as_deref(),
|
||||
&target_level.picture_description,
|
||||
);
|
||||
let should_auto_name_level = payload
|
||||
.should_auto_name_level
|
||||
.unwrap_or_else(|| target_level.level_name.trim().is_empty());
|
||||
let mut generated_naming = if should_auto_name_level {
|
||||
let naming = generate_puzzle_first_level_name(
|
||||
&state,
|
||||
target_level.picture_description.as_str(),
|
||||
)
|
||||
.await;
|
||||
target_level.level_name = naming.level_name.clone();
|
||||
target_level.ui_background_prompt = naming.ui_background_prompt.clone();
|
||||
Some(naming)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let reference_image_sources = collect_puzzle_reference_image_sources(
|
||||
payload.reference_image_src.as_deref(),
|
||||
payload.reference_image_srcs.as_slice(),
|
||||
@@ -806,11 +821,14 @@ pub async fn execute_puzzle_agent_action(
|
||||
&candidates[0].downloaded_image,
|
||||
)
|
||||
.await
|
||||
.filter(|_| should_auto_name_level)
|
||||
{
|
||||
target_level.level_name = refined_naming.level_name;
|
||||
target_level.level_name = refined_naming.level_name.clone();
|
||||
if refined_naming.ui_background_prompt.is_some() {
|
||||
target_level.ui_background_prompt = refined_naming.ui_background_prompt;
|
||||
target_level.ui_background_prompt =
|
||||
refined_naming.ui_background_prompt.clone();
|
||||
}
|
||||
generated_naming = Some(refined_naming);
|
||||
}
|
||||
let generated_level_name = target_level.level_name.clone();
|
||||
let levels_json_with_generated_name =
|
||||
@@ -859,19 +877,36 @@ pub async fn execute_puzzle_agent_action(
|
||||
);
|
||||
let fallback_session =
|
||||
replace_puzzle_session_draft_snapshot(session, draft, now);
|
||||
Ok(apply_generated_puzzle_candidates_to_session_snapshot(
|
||||
let fallback_session = if should_auto_name_level {
|
||||
apply_generated_puzzle_first_level_name_to_session_snapshot(
|
||||
fallback_session,
|
||||
target_level.level_id.as_str(),
|
||||
generated_level_name.as_str(),
|
||||
fallback_level_name.as_str(),
|
||||
now,
|
||||
),
|
||||
target_level.level_id.as_str(),
|
||||
candidates.into_records(),
|
||||
primary_reference_image_src,
|
||||
now,
|
||||
))
|
||||
)
|
||||
} else {
|
||||
fallback_session
|
||||
};
|
||||
let mut fallback_session =
|
||||
apply_generated_puzzle_candidates_to_session_snapshot(
|
||||
fallback_session,
|
||||
target_level.level_id.as_str(),
|
||||
candidates.into_records(),
|
||||
primary_reference_image_src,
|
||||
now,
|
||||
);
|
||||
if let Some(generated_naming) = generated_naming.as_ref() {
|
||||
fallback_session =
|
||||
apply_generated_puzzle_metadata_to_session_snapshot(
|
||||
fallback_session,
|
||||
target_level.level_id.as_str(),
|
||||
generated_naming,
|
||||
fallback_level_name.as_str(),
|
||||
now,
|
||||
);
|
||||
}
|
||||
Ok(fallback_session)
|
||||
}
|
||||
Err(error) => Err(map_puzzle_client_error(error)),
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ pub(super) fn map_puzzle_form_draft_response(
|
||||
pub(super) fn map_puzzle_draft_level_response(
|
||||
level: PuzzleDraftLevelRecord,
|
||||
) -> PuzzleDraftLevelResponse {
|
||||
let generation_status = resolve_puzzle_level_generation_status(&level);
|
||||
PuzzleDraftLevelResponse {
|
||||
level_id: level.level_id,
|
||||
level_name: level.level_name,
|
||||
@@ -115,7 +116,7 @@ pub(super) fn map_puzzle_draft_level_response(
|
||||
selected_candidate_id: level.selected_candidate_id,
|
||||
cover_image_src: level.cover_image_src,
|
||||
cover_asset_id: level.cover_asset_id,
|
||||
generation_status: level.generation_status,
|
||||
generation_status,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,23 +280,66 @@ pub(super) fn map_puzzle_result_preview_finding_response(
|
||||
}
|
||||
|
||||
fn resolve_puzzle_work_generation_status(item: &PuzzleWorkProfileRecord) -> Option<String> {
|
||||
let has_viewable_result = item
|
||||
.cover_image_src
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
|| item.levels.iter().any(has_puzzle_level_image);
|
||||
if has_viewable_result {
|
||||
return Some("ready".to_string());
|
||||
}
|
||||
|
||||
item.levels
|
||||
.iter()
|
||||
.map(|level| level.generation_status.trim())
|
||||
.find(|status| *status == "generating")
|
||||
.map(resolve_puzzle_level_generation_status)
|
||||
.find(|status| status.as_str() == "generating")
|
||||
.or_else(|| {
|
||||
item.levels
|
||||
.iter()
|
||||
.map(|level| level.generation_status.trim())
|
||||
.find(|status| *status == "ready")
|
||||
.map(resolve_puzzle_level_generation_status)
|
||||
.find(|status| status.as_str() == "ready")
|
||||
})
|
||||
.or_else(|| {
|
||||
item.levels
|
||||
.iter()
|
||||
.map(|level| level.generation_status.trim())
|
||||
.map(resolve_puzzle_level_generation_status)
|
||||
.find(|status| !status.is_empty())
|
||||
})
|
||||
.map(str::to_string)
|
||||
}
|
||||
|
||||
fn resolve_puzzle_level_generation_status(level: &PuzzleDraftLevelRecord) -> String {
|
||||
if level.generation_status.trim() == "generating" && has_puzzle_level_image(level) {
|
||||
return "ready".to_string();
|
||||
}
|
||||
|
||||
level.generation_status.trim().to_string()
|
||||
}
|
||||
|
||||
fn has_puzzle_level_image(level: &PuzzleDraftLevelRecord) -> bool {
|
||||
let has_cover = level
|
||||
.cover_image_src
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
let has_selected_candidate = level
|
||||
.selected_candidate_id
|
||||
.as_deref()
|
||||
.and_then(|candidate_id| {
|
||||
level
|
||||
.candidates
|
||||
.iter()
|
||||
.find(|candidate| candidate.candidate_id == candidate_id)
|
||||
})
|
||||
.map(|candidate| candidate.image_src.trim())
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
let has_fallback_candidate = level
|
||||
.candidates
|
||||
.last()
|
||||
.map(|candidate| candidate.image_src.trim())
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
|
||||
has_cover || has_selected_candidate || has_fallback_candidate
|
||||
}
|
||||
|
||||
pub(super) fn map_puzzle_work_summary_response(
|
||||
@@ -338,7 +382,11 @@ pub(super) fn map_puzzle_work_summary_response(
|
||||
.saturating_sub(item.point_incentive_claimed_points),
|
||||
publish_ready: item.publish_ready,
|
||||
generation_status,
|
||||
levels: item.levels.iter().map(|x|map_puzzle_draft_level_response(x.clone())).collect(),
|
||||
levels: item
|
||||
.levels
|
||||
.iter()
|
||||
.map(|x| map_puzzle_draft_level_response(x.clone()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,4 +651,4 @@ pub(super) fn build_puzzle_welcome_text(seed_text: &str) -> String {
|
||||
}
|
||||
|
||||
"拼图创作信息已准备好。".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,6 +253,7 @@ fn puzzle_image_generation_builds_fallback_session_from_levels_snapshot() {
|
||||
image_model: Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2.to_string()),
|
||||
ai_redraw: None,
|
||||
candidate_count: Some(1),
|
||||
should_auto_name_level: None,
|
||||
candidate_id: None,
|
||||
level_id: Some("puzzle-level-1".to_string()),
|
||||
work_title: Some("暖灯猫街作品".to_string()),
|
||||
@@ -376,6 +377,21 @@ fn puzzle_level_name_image_data_url_downsizes_generated_image() {
|
||||
assert!(data_url.len() > "data:image/png;base64,".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_uploaded_cover_can_reuse_resolved_history_image() {
|
||||
let resolved = PuzzleResolvedReferenceImage {
|
||||
mime_type: "image/png".to_string(),
|
||||
bytes_len: 8,
|
||||
bytes: b"pngbytes".to_vec(),
|
||||
};
|
||||
|
||||
let downloaded = PuzzleDownloadedImage::from_resolved_reference_image(resolved);
|
||||
|
||||
assert_eq!(downloaded.extension, "png");
|
||||
assert_eq!(downloaded.mime_type, "image/png");
|
||||
assert_eq!(downloaded.bytes, b"pngbytes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_first_level_name_snapshot_defaults_work_title() {
|
||||
let levels_json = serde_json::to_string(&vec![json!({
|
||||
@@ -397,6 +413,7 @@ fn puzzle_first_level_name_snapshot_defaults_work_title() {
|
||||
image_model: Some(PUZZLE_IMAGE_MODEL_GPT_IMAGE_2.to_string()),
|
||||
ai_redraw: None,
|
||||
candidate_count: Some(1),
|
||||
should_auto_name_level: None,
|
||||
candidate_id: None,
|
||||
level_id: Some("puzzle-level-1".to_string()),
|
||||
work_title: Some("猫画面".to_string()),
|
||||
@@ -619,7 +636,7 @@ fn puzzle_work_summary_response_keeps_levels_for_shelf_cover() {
|
||||
selected_candidate_id: Some("candidate-1".to_string()),
|
||||
cover_image_src: Some("/generated-puzzle-assets/session/cover.png".to_string()),
|
||||
cover_asset_id: Some("asset-1".to_string()),
|
||||
generation_status: "ready".to_string(),
|
||||
generation_status: "generating".to_string(),
|
||||
};
|
||||
|
||||
let response = map_puzzle_work_summary_response(
|
||||
@@ -654,6 +671,7 @@ fn puzzle_work_summary_response_keeps_levels_for_shelf_cover() {
|
||||
|
||||
assert_eq!(response.levels.len(), 1);
|
||||
assert_eq!(response.generation_status.as_deref(), Some("ready"));
|
||||
assert_eq!(response.levels[0].generation_status, "ready");
|
||||
assert_eq!(
|
||||
response.levels[0].cover_image_src.as_deref(),
|
||||
Some("/generated-puzzle-assets/session/cover.png")
|
||||
|
||||
@@ -69,6 +69,16 @@ pub(crate) struct PuzzleDownloadedImage {
|
||||
pub(crate) bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PuzzleDownloadedImage {
|
||||
pub(crate) fn from_resolved_reference_image(image: PuzzleResolvedReferenceImage) -> Self {
|
||||
Self {
|
||||
extension: puzzle_mime_to_extension(image.mime_type.as_str()).to_string(),
|
||||
mime_type: normalize_puzzle_downloaded_image_mime_type(image.mime_type.as_str()),
|
||||
bytes: image.bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ParsedPuzzleImageDataUrl {
|
||||
pub(crate) mime_type: String,
|
||||
pub(crate) bytes: Vec<u8>,
|
||||
|
||||
@@ -49,6 +49,8 @@ pub struct ExecutePuzzleAgentActionRequest {
|
||||
#[serde(default)]
|
||||
pub candidate_count: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub should_auto_name_level: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub candidate_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub level_id: Option<String>,
|
||||
|
||||
@@ -1063,6 +1063,7 @@ fn save_puzzle_generated_images_tx(
|
||||
if let Some(levels) = deserialize_optional_levels_input(input.levels_json.as_deref())? {
|
||||
// 中文注释:结果页新增关卡可能还没等到自动保存,生成图时以本次 action 携带的关卡快照作为写回目标。
|
||||
draft.levels = levels;
|
||||
draft = normalize_completed_puzzle_level_generation_status(draft);
|
||||
module_puzzle::sync_primary_level_fields(&mut draft);
|
||||
// 中文注释:入口直创会在 api-server 生成首关名后随 levels_json 写入;作品名仍是旧首关名或空值时才跟随首关名,避免覆盖用户手动命名。
|
||||
sync_generated_primary_level_name_as_default_work_title(
|
||||
@@ -1092,6 +1093,7 @@ fn save_puzzle_generated_images_tx(
|
||||
next_level.cover_asset_id = Some(selected.asset_id);
|
||||
}
|
||||
draft = replace_puzzle_level(&draft, next_level).map_err(|error| error.to_string())?;
|
||||
draft = normalize_completed_puzzle_level_generation_status(draft);
|
||||
|
||||
let saved_at = Timestamp::from_micros_since_unix_epoch(input.saved_at_micros);
|
||||
let next_stage = if build_result_preview(&draft, Some("陶泥儿主")).publish_ready {
|
||||
@@ -1143,6 +1145,7 @@ fn save_puzzle_ui_background_tx(
|
||||
if let Some(levels) = deserialize_optional_levels_input(input.levels_json.as_deref())? {
|
||||
// 中文注释:UI 背景可以在自动保存前立即生成,写回前优先使用本次 action 携带的关卡快照。
|
||||
draft.levels = levels;
|
||||
draft = normalize_completed_puzzle_level_generation_status(draft);
|
||||
module_puzzle::sync_primary_level_fields(&mut draft);
|
||||
}
|
||||
let target_level = selected_puzzle_level(&draft, input.level_id.as_deref())
|
||||
@@ -1155,6 +1158,7 @@ fn save_puzzle_ui_background_tx(
|
||||
(!trimmed.is_empty()).then_some(trimmed)
|
||||
});
|
||||
let draft = replace_puzzle_level(&draft, next_level).map_err(|error| error.to_string())?;
|
||||
let draft = normalize_completed_puzzle_level_generation_status(draft);
|
||||
|
||||
let saved_at = Timestamp::from_micros_since_unix_epoch(input.saved_at_micros);
|
||||
let next_stage = if build_result_preview(&draft, Some("陶泥儿主")).publish_ready {
|
||||
@@ -1208,6 +1212,52 @@ fn sync_generated_primary_level_name_as_default_work_title(
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_completed_puzzle_level_generation_status(
|
||||
mut draft: PuzzleResultDraft,
|
||||
) -> PuzzleResultDraft {
|
||||
draft.levels = normalize_completed_puzzle_levels_generation_status(draft.levels);
|
||||
module_puzzle::sync_primary_level_fields(&mut draft);
|
||||
draft
|
||||
}
|
||||
|
||||
fn normalize_completed_puzzle_levels_generation_status(
|
||||
mut levels: Vec<module_puzzle::PuzzleDraftLevel>,
|
||||
) -> Vec<module_puzzle::PuzzleDraftLevel> {
|
||||
for level in &mut levels {
|
||||
if level.generation_status.trim() == "generating" && has_completed_puzzle_level_image(level)
|
||||
{
|
||||
level.generation_status = "ready".to_string();
|
||||
}
|
||||
}
|
||||
levels
|
||||
}
|
||||
|
||||
fn has_completed_puzzle_level_image(level: &module_puzzle::PuzzleDraftLevel) -> bool {
|
||||
let has_cover = level
|
||||
.cover_image_src
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
let has_selected_candidate = level
|
||||
.selected_candidate_id
|
||||
.as_deref()
|
||||
.and_then(|candidate_id| {
|
||||
level
|
||||
.candidates
|
||||
.iter()
|
||||
.find(|candidate| candidate.candidate_id == candidate_id)
|
||||
})
|
||||
.map(|candidate| candidate.image_src.trim())
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
let has_fallback_candidate = level
|
||||
.candidates
|
||||
.last()
|
||||
.map(|candidate| candidate.image_src.trim())
|
||||
.is_some_and(|value| !value.is_empty());
|
||||
|
||||
has_cover || has_selected_candidate || has_fallback_candidate
|
||||
}
|
||||
|
||||
fn select_puzzle_cover_image_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleSelectCoverImageInput,
|
||||
@@ -1391,12 +1441,13 @@ fn update_puzzle_work_tx(
|
||||
if theme_tags.len() > PUZZLE_MAX_TAG_COUNT {
|
||||
return Err("拼图标签数量不合法".to_string());
|
||||
}
|
||||
let levels = deserialize_optional_levels_input(input.levels_json.as_deref())?
|
||||
let mut levels = deserialize_optional_levels_input(input.levels_json.as_deref())?
|
||||
.map(|levels| {
|
||||
normalize_puzzle_levels(levels, &theme_tags).map_err(|error| error.to_string())
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| build_profile_levels_from_row(&row).unwrap_or_default());
|
||||
levels = normalize_completed_puzzle_levels_generation_status(levels);
|
||||
let preview_draft = PuzzleResultDraft {
|
||||
work_title: input.work_title.clone(),
|
||||
work_description: input.work_description.clone(),
|
||||
@@ -2547,6 +2598,10 @@ fn build_puzzle_gallery_card_view_row(
|
||||
fn resolve_puzzle_gallery_generation_status(
|
||||
levels: &[module_puzzle::PuzzleDraftLevel],
|
||||
) -> Option<String> {
|
||||
if levels.iter().any(has_completed_puzzle_level_image) {
|
||||
return Some("ready".to_string());
|
||||
}
|
||||
|
||||
levels
|
||||
.iter()
|
||||
.map(|level| level.generation_status.trim())
|
||||
@@ -2822,6 +2877,7 @@ fn replace_puzzle_work_profile(
|
||||
}
|
||||
|
||||
fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Result<(), String> {
|
||||
let levels = normalize_completed_puzzle_levels_generation_status(profile.levels);
|
||||
if let Some(existing) = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
@@ -2844,7 +2900,7 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
|
||||
theme_tags_json: serialize_json(&profile.theme_tags),
|
||||
cover_image_src: profile.cover_image_src,
|
||||
cover_asset_id: profile.cover_asset_id,
|
||||
levels_json: serialize_json(&profile.levels),
|
||||
levels_json: serialize_json(&levels),
|
||||
publication_status: profile.publication_status,
|
||||
// 二次编辑发布同一个 profile 时,作品内容可以覆盖,但历史游玩数属于
|
||||
// 广场消费数据,不能因为重新发布被清零。
|
||||
@@ -2882,7 +2938,7 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
|
||||
theme_tags_json: serialize_json(&profile.theme_tags),
|
||||
cover_image_src: profile.cover_image_src,
|
||||
cover_asset_id: profile.cover_asset_id,
|
||||
levels_json: serialize_json(&profile.levels),
|
||||
levels_json: serialize_json(&levels),
|
||||
publication_status: profile.publication_status,
|
||||
play_count: profile.play_count,
|
||||
remix_count: profile.remix_count,
|
||||
@@ -3532,7 +3588,9 @@ fn deserialize_levels_json(value: &str) -> Result<Vec<module_puzzle::PuzzleDraft
|
||||
if value.trim().is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
json_from_str(value).map_err(|error| format!("拼图 levels JSON 非法: {error}"))
|
||||
json_from_str(value)
|
||||
.map(normalize_completed_puzzle_levels_generation_status)
|
||||
.map_err(|error| format!("拼图 levels JSON 非法: {error}"))
|
||||
}
|
||||
|
||||
fn deserialize_optional_levels_input(
|
||||
|
||||
Reference in New Issue
Block a user