feat: add asset operation wallet ledger
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
This commit is contained in:
@@ -67,7 +67,7 @@ use tokio::time::sleep;
|
||||
use crate::{
|
||||
ai_generation_drafts::{AiGenerationDraftContext, AiGenerationDraftWriter},
|
||||
api_response::json_success_body,
|
||||
asset_billing::{consume_asset_operation_points, refund_asset_operation_points},
|
||||
asset_billing::execute_billable_asset_operation,
|
||||
auth::AuthenticatedAccessToken,
|
||||
http_error::AppError,
|
||||
prompt::puzzle_image::{PUZZLE_DEFAULT_NEGATIVE_PROMPT, build_puzzle_image_prompt},
|
||||
@@ -442,29 +442,29 @@ pub async fn execute_puzzle_agent_action(
|
||||
let owner_user_id = authenticated.claims().user_id().to_string();
|
||||
let now = current_utc_micros();
|
||||
let action = payload.action.trim().to_string();
|
||||
let billed_asset_kind = match action.as_str() {
|
||||
"compile_puzzle_draft" => Some("puzzle_initial_image"),
|
||||
"generate_puzzle_images" => Some("puzzle_generated_image"),
|
||||
_ => None,
|
||||
};
|
||||
let billing_asset_id = format!("{session_id}:{now}");
|
||||
if let Some(asset_kind) = billed_asset_kind {
|
||||
consume_asset_operation_points(&state, &owner_user_id, asset_kind, &billing_asset_id)
|
||||
let (operation_type, phase_label, phase_detail, session) = match action.as_str() {
|
||||
"compile_puzzle_draft" => {
|
||||
let session = execute_billable_asset_operation(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"puzzle_initial_image",
|
||||
&billing_asset_id,
|
||||
async {
|
||||
compile_puzzle_draft_with_initial_cover(
|
||||
&state,
|
||||
session_id.clone(),
|
||||
owner_user_id.clone(),
|
||||
now,
|
||||
)
|
||||
.await
|
||||
.map_err(map_puzzle_client_error)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error)
|
||||
})?;
|
||||
}
|
||||
|
||||
let (operation_type, phase_label, phase_detail, session) = match action.as_str() {
|
||||
"compile_puzzle_draft" => {
|
||||
let session = compile_puzzle_draft_with_initial_cover(
|
||||
&state,
|
||||
session_id.clone(),
|
||||
owner_user_id.clone(),
|
||||
now,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
(
|
||||
"compile_puzzle_draft",
|
||||
"完整拼图草稿",
|
||||
@@ -473,75 +473,76 @@ pub async fn execute_puzzle_agent_action(
|
||||
)
|
||||
}
|
||||
"generate_puzzle_images" => {
|
||||
let session = state
|
||||
.spacetime_client()
|
||||
.get_puzzle_agent_session(session_id.clone(), owner_user_id.clone())
|
||||
.await;
|
||||
let session = match session {
|
||||
Ok(session) => {
|
||||
let session = execute_billable_asset_operation(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"puzzle_generated_image",
|
||||
&billing_asset_id,
|
||||
async {
|
||||
let session = state
|
||||
.spacetime_client()
|
||||
.get_puzzle_agent_session(session_id.clone(), owner_user_id.clone())
|
||||
.await
|
||||
.map_err(map_puzzle_client_error)?;
|
||||
let draft = session.draft.clone().ok_or_else(|| {
|
||||
SpacetimeClientError::Runtime("拼图结果页草稿尚未生成".to_string())
|
||||
});
|
||||
match draft {
|
||||
Ok(draft) => {
|
||||
let prompt = payload
|
||||
.prompt_text
|
||||
.clone()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| draft.summary.clone());
|
||||
// 拼图结果页从多候选抽卡收口为单图替换,前端传入的旧 candidateCount 只做兼容忽略。
|
||||
let candidate_count = 1;
|
||||
let candidate_start_index = draft.candidates.len();
|
||||
let candidates = generate_puzzle_image_candidates(
|
||||
&state,
|
||||
owner_user_id.as_str(),
|
||||
&session.session_id,
|
||||
&draft.level_name,
|
||||
&prompt,
|
||||
payload.reference_image_src.as_deref(),
|
||||
candidate_count,
|
||||
candidate_start_index,
|
||||
)
|
||||
.await
|
||||
.map_err(SpacetimeClientError::Runtime);
|
||||
match candidates {
|
||||
Ok(candidates) => {
|
||||
let candidates_json = serde_json::to_string(
|
||||
&candidates
|
||||
.iter()
|
||||
.map(to_puzzle_generated_image_candidate)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.map_err(|error| {
|
||||
SpacetimeClientError::Runtime(format!(
|
||||
"拼图候选图序列化失败:{error}"
|
||||
))
|
||||
});
|
||||
match candidates_json {
|
||||
Ok(candidates_json) => {
|
||||
state
|
||||
.spacetime_client()
|
||||
.save_puzzle_generated_images(
|
||||
PuzzleGeneratedImagesSaveRecordInput {
|
||||
session_id: session.session_id,
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
candidates_json,
|
||||
saved_at_micros: now,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
};
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
"message": "拼图结果页草稿尚未生成",
|
||||
}))
|
||||
})?;
|
||||
let prompt = payload
|
||||
.prompt_text
|
||||
.clone()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| draft.summary.clone());
|
||||
// 拼图结果页从多候选抽卡收口为单图替换,前端传入的旧 candidateCount 只做兼容忽略。
|
||||
let candidate_count = 1;
|
||||
let candidate_start_index = draft.candidates.len();
|
||||
let candidates = generate_puzzle_image_candidates(
|
||||
&state,
|
||||
owner_user_id.as_str(),
|
||||
&session.session_id,
|
||||
&draft.level_name,
|
||||
&prompt,
|
||||
payload.reference_image_src.as_deref(),
|
||||
candidate_count,
|
||||
candidate_start_index,
|
||||
)
|
||||
.await
|
||||
.map_err(|message| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
"message": message,
|
||||
}))
|
||||
})?;
|
||||
let candidates_json = serde_json::to_string(
|
||||
&candidates
|
||||
.iter()
|
||||
.map(to_puzzle_generated_image_candidate)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.map_err(|error| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
"message": format!("拼图候选图序列化失败:{error}"),
|
||||
}))
|
||||
})?;
|
||||
state
|
||||
.spacetime_client()
|
||||
.save_puzzle_generated_images(PuzzleGeneratedImagesSaveRecordInput {
|
||||
session_id: session.session_id,
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
candidates_json,
|
||||
saved_at_micros: now,
|
||||
})
|
||||
.await
|
||||
.map_err(map_puzzle_client_error)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error)
|
||||
});
|
||||
(
|
||||
"generate_puzzle_images",
|
||||
"拼图图片生成",
|
||||
@@ -569,7 +570,14 @@ pub async fn execute_puzzle_agent_action(
|
||||
candidate_id,
|
||||
selected_at_micros: now,
|
||||
})
|
||||
.await;
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
map_puzzle_client_error(error),
|
||||
)
|
||||
});
|
||||
(
|
||||
"select_puzzle_image",
|
||||
"正式图确认",
|
||||
@@ -579,43 +587,35 @@ pub async fn execute_puzzle_agent_action(
|
||||
}
|
||||
"publish_puzzle_work" => {
|
||||
let (work_id, profile_id) = build_stable_puzzle_work_ids(&session_id);
|
||||
consume_asset_operation_points(&state, &owner_user_id, "puzzle_publish_work", &work_id)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error)
|
||||
})?;
|
||||
let profile_result = state
|
||||
.spacetime_client()
|
||||
.publish_puzzle_work(PuzzlePublishRecordInput {
|
||||
session_id: session_id.clone(),
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
// 发布沿用 session 派生的稳定作品 ID,避免草稿卡与已发布卡重复。
|
||||
work_id: work_id.clone(),
|
||||
profile_id,
|
||||
author_display_name: resolve_author_display_name(&state, &authenticated),
|
||||
level_name: payload.level_name.clone(),
|
||||
summary: payload.summary.clone(),
|
||||
theme_tags: payload.theme_tags.clone(),
|
||||
published_at_micros: now,
|
||||
})
|
||||
.await;
|
||||
let profile = match profile_result {
|
||||
Ok(profile) => profile,
|
||||
Err(error) => {
|
||||
refund_asset_operation_points(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"puzzle_publish_work",
|
||||
&work_id,
|
||||
)
|
||||
.await;
|
||||
return Err(puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
map_puzzle_client_error(error),
|
||||
));
|
||||
}
|
||||
};
|
||||
let author_display_name = resolve_author_display_name(&state, &authenticated);
|
||||
let profile = execute_billable_asset_operation(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"puzzle_publish_work",
|
||||
&work_id,
|
||||
async {
|
||||
state
|
||||
.spacetime_client()
|
||||
.publish_puzzle_work(PuzzlePublishRecordInput {
|
||||
session_id: session_id.clone(),
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
// 发布沿用 session 派生的稳定作品 ID,避免草稿卡与已发布卡重复。
|
||||
work_id: work_id.clone(),
|
||||
profile_id,
|
||||
author_display_name,
|
||||
level_name: payload.level_name.clone(),
|
||||
summary: payload.summary.clone(),
|
||||
theme_tags: payload.theme_tags.clone(),
|
||||
published_at_micros: now,
|
||||
})
|
||||
.await
|
||||
.map_err(map_puzzle_client_error)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(&request_context, PUZZLE_AGENT_API_BASE_PROVIDER, error)
|
||||
})?;
|
||||
|
||||
let session = state
|
||||
.spacetime_client()
|
||||
@@ -654,29 +654,7 @@ pub async fn execute_puzzle_agent_action(
|
||||
}
|
||||
};
|
||||
|
||||
let session = session.map_err(|error| {
|
||||
if let Some(asset_kind) = billed_asset_kind {
|
||||
tokio::spawn({
|
||||
let state = state.clone();
|
||||
let owner_user_id = owner_user_id.clone();
|
||||
let billing_asset_id = billing_asset_id.clone();
|
||||
async move {
|
||||
refund_asset_operation_points(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
asset_kind,
|
||||
&billing_asset_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
puzzle_error_response(
|
||||
&request_context,
|
||||
PUZZLE_AGENT_API_BASE_PROVIDER,
|
||||
map_puzzle_client_error(error),
|
||||
)
|
||||
})?;
|
||||
let session = session?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
|
||||
Reference in New Issue
Block a user