refactor: modularize api server assets and handlers

This commit is contained in:
2026-05-14 22:54:52 +08:00
parent 4ba1ebbbdf
commit 1b54db4f92
47 changed files with 8081 additions and 6142 deletions

View File

@@ -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,