refactor: modularize api server assets and handlers
This commit is contained in:
@@ -2770,383 +2770,9 @@ async fn upsert_custom_world_draft_foundation_progress(
|
||||
})
|
||||
}
|
||||
|
||||
fn map_custom_world_library_entry_response(
|
||||
state: &AppState,
|
||||
entry: CustomWorldLibraryEntryRecord,
|
||||
) -> CustomWorldLibraryEntryResponse {
|
||||
let author = resolve_work_author_by_user_id(
|
||||
state,
|
||||
&entry.owner_user_id,
|
||||
Some(&entry.author_display_name),
|
||||
entry.author_public_user_code.as_deref(),
|
||||
);
|
||||
CustomWorldLibraryEntryResponse {
|
||||
owner_user_id: entry.owner_user_id,
|
||||
profile_id: entry.profile_id,
|
||||
public_work_code: entry.public_work_code,
|
||||
author_public_user_code: author.public_user_code.or(entry.author_public_user_code),
|
||||
profile: entry.profile,
|
||||
visibility: entry.visibility,
|
||||
published_at: entry.published_at,
|
||||
updated_at: entry.updated_at,
|
||||
author_display_name: author.display_name,
|
||||
world_name: entry.world_name,
|
||||
subtitle: entry.subtitle,
|
||||
summary_text: entry.summary_text,
|
||||
cover_image_src: entry.cover_image_src,
|
||||
theme_mode: entry.theme_mode,
|
||||
playable_npc_count: entry.playable_npc_count,
|
||||
landmark_count: entry.landmark_count,
|
||||
play_count: entry.play_count,
|
||||
remix_count: entry.remix_count,
|
||||
like_count: entry.like_count,
|
||||
recent_play_count_7d: 0,
|
||||
}
|
||||
}
|
||||
mod mappers;
|
||||
|
||||
fn map_custom_world_library_entry_response_from_work_summary(
|
||||
state: &AppState,
|
||||
item: CustomWorldWorkSummaryRecord,
|
||||
owner_user_id: &str,
|
||||
) -> Option<CustomWorldLibraryEntryResponse> {
|
||||
let profile_id = item.profile_id.as_ref()?.clone();
|
||||
let profile = build_custom_world_library_list_profile_payload(&item, &profile_id);
|
||||
let author = resolve_work_author_by_user_id(state, owner_user_id, None, None);
|
||||
Some(CustomWorldLibraryEntryResponse {
|
||||
owner_user_id: owner_user_id.to_string(),
|
||||
public_work_code: (item.status == "published")
|
||||
.then(|| build_public_work_code_from_profile_id(&profile_id)),
|
||||
profile_id,
|
||||
author_public_user_code: author.public_user_code,
|
||||
profile,
|
||||
visibility: item.status,
|
||||
published_at: item.published_at,
|
||||
updated_at: item.updated_at,
|
||||
author_display_name: author.display_name,
|
||||
world_name: item.title,
|
||||
subtitle: item.subtitle,
|
||||
summary_text: item.summary,
|
||||
cover_image_src: item.cover_image_src,
|
||||
theme_mode: "mythic".to_string(),
|
||||
playable_npc_count: item.playable_npc_count,
|
||||
landmark_count: item.landmark_count,
|
||||
play_count: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
recent_play_count_7d: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_public_work_code_from_profile_id(profile_id: &str) -> String {
|
||||
let digits = profile_id
|
||||
.chars()
|
||||
.filter(|character| character.is_ascii_digit())
|
||||
.collect::<String>();
|
||||
let normalized_digits = if digits.is_empty() {
|
||||
let checksum = profile_id.bytes().fold(0u32, |accumulator, value| {
|
||||
accumulator.wrapping_mul(131) + u32::from(value)
|
||||
});
|
||||
format!("{:08}", checksum % 100_000_000)
|
||||
} else {
|
||||
format!("{:0>8}", &digits[digits.len().saturating_sub(8)..])
|
||||
};
|
||||
|
||||
format!("CW-{normalized_digits}")
|
||||
}
|
||||
|
||||
fn build_custom_world_library_list_profile_payload(
|
||||
item: &CustomWorldWorkSummaryRecord,
|
||||
profile_id: &str,
|
||||
) -> Value {
|
||||
json!({
|
||||
"id": profile_id,
|
||||
"name": item.title,
|
||||
"subtitle": item.subtitle,
|
||||
"summary": item.summary,
|
||||
"tone": "",
|
||||
"playerGoal": "",
|
||||
"settingText": "",
|
||||
"themeMode": "mythic",
|
||||
"templateWorldType": "WUXIA",
|
||||
"compatibilityTemplateWorldType": Value::Null,
|
||||
"cover": item.cover_image_src.as_ref().map(|image_src| json!({
|
||||
"sourceType": "generated",
|
||||
"imageSrc": image_src,
|
||||
})),
|
||||
"majorFactions": [],
|
||||
"coreConflicts": [],
|
||||
"playableNpcs": [],
|
||||
"storyNpcs": [],
|
||||
"items": [],
|
||||
"camp": Value::Null,
|
||||
"landmarks": [],
|
||||
"ownedSettingLayers": Value::Null,
|
||||
})
|
||||
}
|
||||
|
||||
fn map_custom_world_gallery_card_response(
|
||||
state: &AppState,
|
||||
entry: CustomWorldGalleryEntryRecord,
|
||||
) -> CustomWorldGalleryCardResponse {
|
||||
let author = resolve_work_author_by_user_id(
|
||||
state,
|
||||
&entry.owner_user_id,
|
||||
Some(&entry.author_display_name),
|
||||
Some(&entry.author_public_user_code),
|
||||
);
|
||||
CustomWorldGalleryCardResponse {
|
||||
owner_user_id: entry.owner_user_id,
|
||||
profile_id: entry.profile_id,
|
||||
public_work_code: entry.public_work_code,
|
||||
author_public_user_code: author
|
||||
.public_user_code
|
||||
.unwrap_or(entry.author_public_user_code),
|
||||
visibility: entry.visibility,
|
||||
published_at: entry.published_at,
|
||||
updated_at: entry.updated_at,
|
||||
author_display_name: author.display_name,
|
||||
world_name: entry.world_name,
|
||||
subtitle: entry.subtitle,
|
||||
summary_text: entry.summary_text,
|
||||
cover_image_src: entry.cover_image_src,
|
||||
theme_mode: entry.theme_mode,
|
||||
playable_npc_count: entry.playable_npc_count,
|
||||
landmark_count: entry.landmark_count,
|
||||
play_count: entry.play_count,
|
||||
remix_count: entry.remix_count,
|
||||
like_count: entry.like_count,
|
||||
recent_play_count_7d: entry.recent_play_count_7d,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_custom_world_work_summary_response(
|
||||
item: CustomWorldWorkSummaryRecord,
|
||||
) -> CustomWorldWorkSummaryResponse {
|
||||
CustomWorldWorkSummaryResponse {
|
||||
work_id: item.work_id,
|
||||
source_type: item.source_type,
|
||||
status: item.status,
|
||||
title: item.title,
|
||||
subtitle: item.subtitle,
|
||||
summary: item.summary,
|
||||
cover_image_src: item.cover_image_src,
|
||||
cover_render_mode: item.cover_render_mode,
|
||||
cover_character_image_srcs: item.cover_character_image_srcs,
|
||||
updated_at: item.updated_at,
|
||||
published_at: item.published_at,
|
||||
stage: item.stage,
|
||||
stage_label: item.stage_label,
|
||||
playable_npc_count: item.playable_npc_count,
|
||||
landmark_count: item.landmark_count,
|
||||
role_visual_ready_count: item.role_visual_ready_count,
|
||||
role_animation_ready_count: item.role_animation_ready_count,
|
||||
role_asset_summary_label: item.role_asset_summary_label,
|
||||
session_id: item.session_id,
|
||||
profile_id: item.profile_id,
|
||||
can_resume: item.can_resume,
|
||||
can_enter_world: item.can_enter_world,
|
||||
blocker_count: item.blocker_count,
|
||||
publish_ready: item.publish_ready,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_custom_world_agent_session_response(
|
||||
session: CustomWorldAgentSessionRecord,
|
||||
) -> CustomWorldAgentSessionSnapshotResponse {
|
||||
CustomWorldAgentSessionSnapshotResponse {
|
||||
session_id: session.session_id,
|
||||
current_turn: session.current_turn,
|
||||
anchor_content: session.anchor_content,
|
||||
progress_percent: session.progress_percent,
|
||||
last_assistant_reply: session.last_assistant_reply,
|
||||
stage: session.stage,
|
||||
focus_card_id: session.focus_card_id,
|
||||
creator_intent: session.creator_intent,
|
||||
creator_intent_readiness: session.creator_intent_readiness,
|
||||
anchor_pack: session.anchor_pack,
|
||||
lock_state: session.lock_state,
|
||||
draft_profile: session.draft_profile,
|
||||
messages: session
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(map_custom_world_agent_message_response)
|
||||
.collect(),
|
||||
draft_cards: session
|
||||
.draft_cards
|
||||
.into_iter()
|
||||
.map(map_custom_world_draft_card_response)
|
||||
.collect(),
|
||||
pending_clarifications: session.pending_clarifications,
|
||||
suggested_actions: session.suggested_actions,
|
||||
recommended_replies: session.recommended_replies,
|
||||
quality_findings: session.quality_findings,
|
||||
asset_coverage: session.asset_coverage,
|
||||
checkpoints: session
|
||||
.checkpoints
|
||||
.into_iter()
|
||||
.map(map_custom_world_agent_checkpoint_response)
|
||||
.collect(),
|
||||
supported_actions: session
|
||||
.supported_actions
|
||||
.into_iter()
|
||||
.map(map_custom_world_supported_action_response)
|
||||
.collect(),
|
||||
publish_gate: session
|
||||
.publish_gate
|
||||
.map(map_custom_world_publish_gate_response),
|
||||
result_preview: session.result_preview,
|
||||
updated_at: session.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_custom_world_creation_result_view_response(
|
||||
session: CustomWorldAgentSessionRecord,
|
||||
) -> CustomWorldCreationResultViewResponse {
|
||||
let profile_from_preview = session
|
||||
.result_preview
|
||||
.as_ref()
|
||||
.and_then(|preview| preview.get("preview"))
|
||||
.and_then(normalize_json_object_value);
|
||||
let profile_from_draft =
|
||||
if profile_from_preview.is_none() && is_agent_result_stage(session.stage.as_str()) {
|
||||
normalize_json_object_value(&session.draft_profile)
|
||||
// 中文注释:legacyResultProfile 只在服务端作为历史会话恢复兜底,
|
||||
// 前端不再直接解释 legacy 字段的真相优先级。
|
||||
.or_else(|| {
|
||||
session
|
||||
.draft_profile
|
||||
.get("legacyResultProfile")
|
||||
.and_then(normalize_json_object_value)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (profile, profile_source) = match (profile_from_preview, profile_from_draft) {
|
||||
(Some(profile), _) => (Some(profile), "result_preview"),
|
||||
(None, Some(profile)) => (Some(profile), "draft_profile"),
|
||||
(None, None) => (None, "none"),
|
||||
};
|
||||
let publish_ready = session
|
||||
.publish_gate
|
||||
.as_ref()
|
||||
.map(|gate| gate.publish_ready)
|
||||
.or_else(|| {
|
||||
session
|
||||
.result_preview
|
||||
.as_ref()
|
||||
.and_then(|preview| preview.get("publishReady"))
|
||||
.and_then(Value::as_bool)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let can_enter_world = session
|
||||
.publish_gate
|
||||
.as_ref()
|
||||
.map(|gate| gate.can_enter_world)
|
||||
.or_else(|| {
|
||||
session
|
||||
.result_preview
|
||||
.as_ref()
|
||||
.and_then(|preview| preview.get("canEnterWorld"))
|
||||
.and_then(Value::as_bool)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let blocker_count = session
|
||||
.publish_gate
|
||||
.as_ref()
|
||||
.map(|gate| gate.blocker_count)
|
||||
.or_else(|| {
|
||||
session
|
||||
.result_preview
|
||||
.as_ref()
|
||||
.and_then(|preview| preview.get("blockers"))
|
||||
.and_then(Value::as_array)
|
||||
.map(|items| items.len() as u32)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let has_profile = profile.is_some();
|
||||
let generation_failed = session.stage == "error"
|
||||
|| session
|
||||
.messages
|
||||
.iter()
|
||||
.any(|message| message.kind == "warning" && message.text.contains("失败"));
|
||||
let result_stage = is_agent_result_stage(session.stage.as_str());
|
||||
let (
|
||||
target_stage,
|
||||
generation_view_source,
|
||||
result_view_source,
|
||||
recovery_action,
|
||||
recovery_reason,
|
||||
) = if has_profile && result_stage {
|
||||
(
|
||||
"custom-world-result",
|
||||
None,
|
||||
Some("agent-draft"),
|
||||
"open_result",
|
||||
None,
|
||||
)
|
||||
} else if generation_failed {
|
||||
(
|
||||
"custom-world-generating",
|
||||
Some("agent-draft-foundation"),
|
||||
None,
|
||||
"resume_generation",
|
||||
Some("当前草稿生成失败或缺少结果预览,需要回到生成过程页继续处理。"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"agent-workspace",
|
||||
None,
|
||||
None,
|
||||
"continue_agent",
|
||||
Some("当前会话还没有可打开的结果页真相源。"),
|
||||
)
|
||||
};
|
||||
let can_sync_result_profile = is_agent_result_profile_sync_stage(session.stage.as_str());
|
||||
|
||||
CustomWorldCreationResultViewResponse {
|
||||
session: map_custom_world_agent_session_response(session),
|
||||
profile,
|
||||
profile_source: profile_source.to_string(),
|
||||
target_stage: target_stage.to_string(),
|
||||
generation_view_source: generation_view_source.map(ToOwned::to_owned),
|
||||
result_view_source: result_view_source.map(ToOwned::to_owned),
|
||||
can_autosave_library: has_profile && result_stage,
|
||||
can_sync_result_profile,
|
||||
publish_ready,
|
||||
can_enter_world,
|
||||
blocker_count,
|
||||
recovery_action: recovery_action.to_string(),
|
||||
recovery_reason: recovery_reason.map(ToOwned::to_owned),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_agent_result_stage(stage: &str) -> bool {
|
||||
matches!(
|
||||
stage,
|
||||
"object_refining"
|
||||
| "visual_refining"
|
||||
| "long_tail_review"
|
||||
| "ready_to_publish"
|
||||
| "published"
|
||||
)
|
||||
}
|
||||
|
||||
fn is_agent_result_profile_sync_stage(stage: &str) -> bool {
|
||||
matches!(
|
||||
stage,
|
||||
"object_refining" | "visual_refining" | "long_tail_review" | "ready_to_publish"
|
||||
)
|
||||
}
|
||||
|
||||
fn normalize_json_object_value(value: &Value) -> Option<Value> {
|
||||
value.as_object().and_then(|object| {
|
||||
if object.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Value::Object(object.clone()))
|
||||
}
|
||||
})
|
||||
}
|
||||
use mappers::*;
|
||||
|
||||
fn log_custom_world_publish_gate_diagnostics(
|
||||
source: &str,
|
||||
|
||||
Reference in New Issue
Block a user