fix wooden fish author and title display

This commit is contained in:
kdletters
2026-05-28 14:31:13 +08:00
parent 41568099c4
commit c8b36cf799
10 changed files with 176 additions and 22 deletions

View File

@@ -20,6 +20,7 @@ pub fn map_public_user_summary_payload(user: AuthUser) -> PublicUserSummaryPaylo
PublicUserSummaryPayload {
id: user.id,
public_user_code: user.public_user_code,
username: user.username,
display_name: user.display_name,
avatar_url: user.avatar_url,
}

View File

@@ -67,6 +67,7 @@ const DEFAULT_HIT_OBJECT_REFERENCE_BYTES: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../../public/wooden-fish/default-hit-object.png"
));
const WOODEN_FISH_AUTHOR_FALLBACK_DISPLAY_NAME: &str = "玩家";
pub async fn create_wooden_fish_session(
State(state): State<AppState>,
@@ -80,7 +81,7 @@ pub async fn create_wooden_fish_session(
let owner_user_id = authenticated.claims().user_id().to_string();
let session_id = build_prefixed_uuid_id("wooden-fish-session-");
let now = current_utc_micros();
let draft = build_wooden_fish_draft(&payload);
let draft = build_wooden_fish_draft(&payload, &state).await?;
let session = WoodenFishSessionSnapshotResponse {
session_id,
owner_user_id,
@@ -145,6 +146,7 @@ pub async fn execute_wooden_fish_action(
let Json(mut payload) =
wooden_fish_json(payload, &request_context, WOODEN_FISH_CREATION_PROVIDER)?;
let owner_user_id = authenticated.claims().user_id().to_string();
let author_display_name = resolve_author_display_name(&state, &authenticated);
maybe_generate_hit_object_asset(
&state,
&request_context,
@@ -156,7 +158,7 @@ pub async fn execute_wooden_fish_action(
maybe_generate_hit_sound_asset(&mut payload);
let response = state
.spacetime_client()
.execute_wooden_fish_action(session_id, owner_user_id, payload)
.execute_wooden_fish_action(session_id, owner_user_id, author_display_name, payload)
.await
.map_err(|error| {
wooden_fish_error_response(
@@ -366,12 +368,20 @@ pub async fn get_wooden_fish_gallery_detail(
))
}
fn build_wooden_fish_draft(payload: &WoodenFishWorkspaceCreateRequest) -> WoodenFishDraftResponse {
WoodenFishDraftResponse {
async fn build_wooden_fish_draft(
payload: &WoodenFishWorkspaceCreateRequest,
state: &AppState,
) -> Result<WoodenFishDraftResponse, Response> {
Ok(WoodenFishDraftResponse {
template_id: WOODEN_FISH_TEMPLATE_ID.to_string(),
template_name: WOODEN_FISH_TEMPLATE_NAME.to_string(),
profile_id: None,
work_title: payload.work_title.trim().to_string(),
work_title: resolve_wooden_fish_work_title(
state,
&payload.work_description,
&payload.hit_object_prompt,
)
.await?,
work_description: payload.work_description.trim().to_string(),
theme_tags: normalize_tags(payload.theme_tags.clone()),
hit_object_prompt: clean_string(&payload.hit_object_prompt, DEFAULT_HIT_OBJECT_PROMPT),
@@ -391,14 +401,13 @@ fn build_wooden_fish_draft(payload: &WoodenFishWorkspaceCreateRequest) -> Wooden
.or_else(|| Some(default_wooden_fish_hit_sound_asset())),
cover_image_src: None,
generation_status: WoodenFishGenerationStatus::Draft,
}
})
}
fn validate_workspace_request(
request_context: &RequestContext,
payload: &WoodenFishWorkspaceCreateRequest,
) -> Result<(), Response> {
ensure_non_empty(request_context, &payload.work_title, "workTitle")?;
if payload.template_id.trim() != WOODEN_FISH_TEMPLATE_ID {
return Err(wooden_fish_error_response(
request_context,
@@ -412,6 +421,77 @@ fn validate_workspace_request(
Ok(())
}
fn resolve_author_display_name(
state: &AppState,
authenticated: &AuthenticatedAccessToken,
) -> String {
state
.auth_user_service()
.get_user_by_id(authenticated.claims().user_id())
.ok()
.flatten()
.map(|user| user.display_name)
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| WOODEN_FISH_AUTHOR_FALLBACK_DISPLAY_NAME.to_string())
}
async fn resolve_wooden_fish_work_title(
state: &AppState,
work_description: &str,
hit_object_prompt: &str,
) -> Result<String, Response> {
let description = clean_string(work_description, hit_object_prompt);
if description.is_empty() {
return Ok(WOODEN_FISH_TEMPLATE_NAME.to_string());
}
let Some(llm_client) = state.llm_client() else {
return Ok(WOODEN_FISH_TEMPLATE_NAME.to_string());
};
let request = platform_llm::LlmTextRequest::new(vec![
platform_llm::LlmMessage::system(
"你是中文作品标题编辑。请根据敲木鱼作品描述生成一个适合卡片展示的简短中文标题,只输出纯文本,不要 JSON、标点解释或引号。",
),
platform_llm::LlmMessage::user(format!(
"作品描述:{description}\n\n请生成 2 到 8 个中文字符为主的标题。"
)),
])
.with_model(crate::llm_model_routing::CREATION_TEMPLATE_LLM_MODEL)
.with_responses_api();
let response = llm_client.request_text(request).await;
match response {
Ok(response) => {
let title = normalize_wooden_fish_generated_work_title(response.content.as_str());
if title.is_empty() {
Ok(WOODEN_FISH_TEMPLATE_NAME.to_string())
} else {
Ok(title)
}
}
Err(_) => Ok(WOODEN_FISH_TEMPLATE_NAME.to_string()),
}
}
fn normalize_wooden_fish_generated_work_title(value: &str) -> String {
let normalized = value
.trim()
.trim_matches(|ch: char| {
ch.is_ascii_punctuation()
|| matches!(
ch,
'' | '。' | '、' | '' | '' | '' | '' | '“' | '”' | '《' | '》'
)
})
.chars()
.filter(|ch| !ch.is_control())
.collect::<String>();
let chars = normalized.chars().collect::<Vec<_>>();
if chars.len() <= 8 {
normalized
} else {
chars.into_iter().take(8).collect()
}
}
async fn maybe_generate_hit_object_asset(
state: &AppState,
request_context: &RequestContext,
@@ -585,7 +665,10 @@ async fn generate_wooden_fish_image_assets(
prompt: &str,
hit_object_reference_image_src: Option<&str>,
) -> Result<WoodenFishGeneratedImageAssets, AppError> {
let settings = require_openai_image_settings(state)?;
let settings = require_openai_image_settings(state)?.with_external_api_audit_context(
Some(owner_user_id.to_string()),
Some(profile_id.to_string()),
);
let http_client = build_openai_image_http_client(&settings)?;
let clean_reference_image_src = hit_object_reference_image_src
.map(str::trim)