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:
@@ -1,3 +1,5 @@
|
||||
use std::future::Future;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use serde_json::json;
|
||||
use spacetime_client::SpacetimeClientError;
|
||||
@@ -6,15 +8,36 @@ use crate::{http_error::AppError, state::AppState};
|
||||
|
||||
pub(crate) const ASSET_OPERATION_POINTS_COST: u64 = 1;
|
||||
|
||||
/// 资产操作统一执行入口:业务层只声明操作类型与资源 ID,钱包扣退费由服务层收口。
|
||||
pub(crate) async fn execute_billable_asset_operation<T, Fut>(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
asset_kind: &str,
|
||||
asset_id: &str,
|
||||
operation: Fut,
|
||||
) -> Result<T, AppError>
|
||||
where
|
||||
Fut: Future<Output = Result<T, AppError>>,
|
||||
{
|
||||
consume_asset_operation_points(state, owner_user_id, asset_kind, asset_id).await?;
|
||||
match operation.await {
|
||||
Ok(value) => Ok(value),
|
||||
Err(error) => {
|
||||
refund_asset_operation_points(state, owner_user_id, asset_kind, asset_id).await;
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 资产操作统一预扣叙世币;扣费流水 ID 由业务资源 ID 参与构造,保证重试幂等。
|
||||
pub(crate) async fn consume_asset_operation_points(
|
||||
async fn consume_asset_operation_points(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
asset_kind: &str,
|
||||
asset_id: &str,
|
||||
) -> Result<(), AppError> {
|
||||
let ledger_id = format!(
|
||||
"asset_generation_consume:{}:{}:{}",
|
||||
"asset_operation_consume:{}:{}:{}",
|
||||
owner_user_id, asset_kind, asset_id
|
||||
);
|
||||
state
|
||||
@@ -31,14 +54,14 @@ pub(crate) async fn consume_asset_operation_points(
|
||||
}
|
||||
|
||||
/// 外部生成或发布 mutation 失败后补偿退款;退款失败只记日志,避免覆盖原始业务错误。
|
||||
pub(crate) async fn refund_asset_operation_points(
|
||||
async fn refund_asset_operation_points(
|
||||
state: &AppState,
|
||||
owner_user_id: &str,
|
||||
asset_kind: &str,
|
||||
asset_id: &str,
|
||||
) {
|
||||
let ledger_id = format!(
|
||||
"asset_generation_refund:{}:{}:{}",
|
||||
"asset_operation_refund:{}:{}:{}",
|
||||
owner_user_id, asset_kind, asset_id
|
||||
);
|
||||
if let Err(error) = state
|
||||
|
||||
@@ -46,7 +46,7 @@ use crate::{
|
||||
AiGenerationDraftContext, AiGenerationDraftSink, 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,
|
||||
request_context::RequestContext,
|
||||
@@ -507,182 +507,118 @@ pub async fn execute_big_fish_action(
|
||||
_ => 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)
|
||||
.await
|
||||
.map_err(|error| big_fish_error_response(&request_context, error))?;
|
||||
}
|
||||
|
||||
let session_result = match action.as_str() {
|
||||
"big_fish_compile_draft" => {
|
||||
compile_big_fish_draft_with_all_assets(
|
||||
let session_operation = async {
|
||||
match action.as_str() {
|
||||
"big_fish_compile_draft" => compile_big_fish_draft_with_all_assets(
|
||||
&state,
|
||||
session_id.clone(),
|
||||
owner_user_id.clone(),
|
||||
now,
|
||||
)
|
||||
.await
|
||||
}
|
||||
"big_fish_generate_level_main_image" => {
|
||||
let asset_url = generate_big_fish_formal_asset(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
&session_id,
|
||||
"level_main_image",
|
||||
payload.level,
|
||||
None,
|
||||
now,
|
||||
)
|
||||
.await
|
||||
.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;
|
||||
}
|
||||
});
|
||||
}
|
||||
big_fish_error_response(&request_context, error)
|
||||
})?;
|
||||
state
|
||||
.spacetime_client()
|
||||
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
session_id: session_id.clone(),
|
||||
asset_kind: "level_main_image".to_string(),
|
||||
level: payload.level,
|
||||
motion_key: None,
|
||||
asset_url: Some(asset_url),
|
||||
generated_at_micros: now,
|
||||
})
|
||||
.await
|
||||
}
|
||||
"big_fish_generate_level_motion" => {
|
||||
let asset_url = generate_big_fish_formal_asset(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
&session_id,
|
||||
"level_motion",
|
||||
payload.level,
|
||||
payload.motion_key.as_deref(),
|
||||
now,
|
||||
)
|
||||
.await
|
||||
.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;
|
||||
}
|
||||
});
|
||||
}
|
||||
big_fish_error_response(&request_context, error)
|
||||
})?;
|
||||
state
|
||||
.spacetime_client()
|
||||
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
session_id: session_id.clone(),
|
||||
asset_kind: "level_motion".to_string(),
|
||||
level: payload.level,
|
||||
motion_key: payload.motion_key,
|
||||
asset_url: Some(asset_url),
|
||||
generated_at_micros: now,
|
||||
})
|
||||
.await
|
||||
}
|
||||
"big_fish_generate_stage_background" => {
|
||||
let asset_url = generate_big_fish_formal_asset(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
&session_id,
|
||||
"stage_background",
|
||||
None,
|
||||
None,
|
||||
now,
|
||||
)
|
||||
.await
|
||||
.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;
|
||||
}
|
||||
});
|
||||
}
|
||||
big_fish_error_response(&request_context, error)
|
||||
})?;
|
||||
state
|
||||
.spacetime_client()
|
||||
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
session_id: session_id.clone(),
|
||||
asset_kind: "stage_background".to_string(),
|
||||
level: None,
|
||||
motion_key: None,
|
||||
asset_url: Some(asset_url),
|
||||
generated_at_micros: now,
|
||||
})
|
||||
.await
|
||||
}
|
||||
"big_fish_publish_game" => {
|
||||
state
|
||||
.map_err(map_big_fish_client_error),
|
||||
"big_fish_generate_level_main_image" => {
|
||||
let asset_url = generate_big_fish_formal_asset(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
&session_id,
|
||||
"level_main_image",
|
||||
payload.level,
|
||||
None,
|
||||
now,
|
||||
)
|
||||
.await?;
|
||||
state
|
||||
.spacetime_client()
|
||||
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
session_id: session_id.clone(),
|
||||
asset_kind: "level_main_image".to_string(),
|
||||
level: payload.level,
|
||||
motion_key: None,
|
||||
asset_url: Some(asset_url),
|
||||
generated_at_micros: now,
|
||||
})
|
||||
.await
|
||||
.map_err(map_big_fish_client_error)
|
||||
}
|
||||
"big_fish_generate_level_motion" => {
|
||||
let asset_url = generate_big_fish_formal_asset(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
&session_id,
|
||||
"level_motion",
|
||||
payload.level,
|
||||
payload.motion_key.as_deref(),
|
||||
now,
|
||||
)
|
||||
.await?;
|
||||
state
|
||||
.spacetime_client()
|
||||
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
session_id: session_id.clone(),
|
||||
asset_kind: "level_motion".to_string(),
|
||||
level: payload.level,
|
||||
motion_key: payload.motion_key,
|
||||
asset_url: Some(asset_url),
|
||||
generated_at_micros: now,
|
||||
})
|
||||
.await
|
||||
.map_err(map_big_fish_client_error)
|
||||
}
|
||||
"big_fish_generate_stage_background" => {
|
||||
let asset_url = generate_big_fish_formal_asset(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
&session_id,
|
||||
"stage_background",
|
||||
None,
|
||||
None,
|
||||
now,
|
||||
)
|
||||
.await?;
|
||||
state
|
||||
.spacetime_client()
|
||||
.generate_big_fish_asset(BigFishAssetGenerateRecordInput {
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
session_id: session_id.clone(),
|
||||
asset_kind: "stage_background".to_string(),
|
||||
level: None,
|
||||
motion_key: None,
|
||||
asset_url: Some(asset_url),
|
||||
generated_at_micros: now,
|
||||
})
|
||||
.await
|
||||
.map_err(map_big_fish_client_error)
|
||||
}
|
||||
"big_fish_publish_game" => state
|
||||
.spacetime_client()
|
||||
.publish_big_fish_game(session_id, owner_user_id.clone(), now)
|
||||
.await
|
||||
}
|
||||
other => {
|
||||
return Err(big_fish_bad_request(
|
||||
&request_context,
|
||||
format!("action `{other}` is not supported").as_str(),
|
||||
));
|
||||
.map_err(map_big_fish_client_error),
|
||||
other => Err(
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "big-fish",
|
||||
"message": format!("action `{other}` is not supported"),
|
||||
})),
|
||||
),
|
||||
}
|
||||
};
|
||||
let session = match session_result {
|
||||
Ok(session) => session,
|
||||
Err(error) => {
|
||||
if let Some(asset_kind) = billed_asset_kind {
|
||||
refund_asset_operation_points(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
asset_kind,
|
||||
&billing_asset_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return Err(big_fish_error_response(
|
||||
&request_context,
|
||||
map_big_fish_client_error(error),
|
||||
));
|
||||
}
|
||||
let session_result = if let Some(asset_kind) = billed_asset_kind {
|
||||
execute_billable_asset_operation(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
asset_kind,
|
||||
&billing_asset_id,
|
||||
session_operation,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
session_operation.await
|
||||
};
|
||||
let session =
|
||||
session_result.map_err(|error| big_fish_error_response(&request_context, error))?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
|
||||
@@ -51,7 +51,7 @@ use crate::{
|
||||
AiGenerationDraftContext, AiGenerationDraftSink, AiGenerationDraftWriter,
|
||||
},
|
||||
api_response::json_success_body,
|
||||
asset_billing::{consume_asset_operation_points, refund_asset_operation_points},
|
||||
asset_billing::execute_billable_asset_operation,
|
||||
auth::AuthenticatedAccessToken,
|
||||
character_visual_assets::generate_character_primary_visual_for_profile,
|
||||
custom_world_agent_entities::generate_custom_world_agent_entities,
|
||||
@@ -351,37 +351,31 @@ pub async fn publish_custom_world_library_profile(
|
||||
));
|
||||
}
|
||||
|
||||
consume_asset_operation_points(&state, &owner_user_id, "custom_world_publish", &profile_id)
|
||||
.await
|
||||
.map_err(|error| custom_world_error_response(&request_context, error))?;
|
||||
|
||||
let mutation_result = state
|
||||
.spacetime_client()
|
||||
.publish_custom_world_profile(
|
||||
profile_id.clone(),
|
||||
owner_user_id.clone(),
|
||||
None,
|
||||
resolve_author_public_user_code(&state, &authenticated, &request_context)?,
|
||||
resolve_author_display_name(&state, &authenticated),
|
||||
current_utc_micros(),
|
||||
)
|
||||
.await;
|
||||
let mutation = match mutation_result {
|
||||
Ok(mutation) => mutation,
|
||||
Err(error) => {
|
||||
refund_asset_operation_points(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"custom_world_publish",
|
||||
&profile_id,
|
||||
)
|
||||
.await;
|
||||
return Err(custom_world_error_response(
|
||||
&request_context,
|
||||
map_custom_world_client_error(error),
|
||||
));
|
||||
}
|
||||
};
|
||||
let author_public_user_code =
|
||||
resolve_author_public_user_code(&state, &authenticated, &request_context)?;
|
||||
let author_display_name = resolve_author_display_name(&state, &authenticated);
|
||||
let mutation = execute_billable_asset_operation(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"custom_world_publish",
|
||||
&profile_id,
|
||||
async {
|
||||
state
|
||||
.spacetime_client()
|
||||
.publish_custom_world_profile(
|
||||
profile_id.clone(),
|
||||
owner_user_id.clone(),
|
||||
None,
|
||||
author_public_user_code,
|
||||
author_display_name,
|
||||
current_utc_micros(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_custom_world_client_error)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| custom_world_error_response(&request_context, error))?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
@@ -1246,46 +1240,33 @@ pub async fn execute_custom_world_agent_action(
|
||||
};
|
||||
|
||||
let should_bill_publish = action == "publish_world";
|
||||
if should_bill_publish {
|
||||
consume_asset_operation_points(
|
||||
let operation_future = async {
|
||||
state
|
||||
.spacetime_client()
|
||||
.execute_custom_world_agent_action(CustomWorldAgentActionExecuteRecordInput {
|
||||
session_id: session_id.clone(),
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
operation_id: build_prefixed_uuid_id("operation-"),
|
||||
action: action.clone(),
|
||||
payload_json: Some(payload_json),
|
||||
submitted_at_micros,
|
||||
})
|
||||
.await
|
||||
.map_err(map_custom_world_client_error)
|
||||
};
|
||||
let result = if should_bill_publish {
|
||||
execute_billable_asset_operation(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"custom_world_agent_publish",
|
||||
&session_id,
|
||||
operation_future,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| custom_world_error_response(&request_context, error))?;
|
||||
}
|
||||
|
||||
let result = match state
|
||||
.spacetime_client()
|
||||
.execute_custom_world_agent_action(CustomWorldAgentActionExecuteRecordInput {
|
||||
session_id: session_id.clone(),
|
||||
owner_user_id: owner_user_id.clone(),
|
||||
operation_id: build_prefixed_uuid_id("operation-"),
|
||||
action: action.clone(),
|
||||
payload_json: Some(payload_json),
|
||||
submitted_at_micros,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
if should_bill_publish {
|
||||
refund_asset_operation_points(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"custom_world_agent_publish",
|
||||
&session_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return Err(custom_world_error_response(
|
||||
&request_context,
|
||||
map_custom_world_client_error(error),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
operation_future.await
|
||||
};
|
||||
let result = result.map_err(|error| custom_world_error_response(&request_context, error))?;
|
||||
|
||||
if matches!(
|
||||
action.as_str(),
|
||||
|
||||
@@ -28,6 +28,7 @@ use webp::Encoder as WebpEncoder;
|
||||
|
||||
use crate::{
|
||||
api_response::json_success_body,
|
||||
asset_billing::execute_billable_asset_operation,
|
||||
auth::AuthenticatedAccessToken,
|
||||
custom_world_result_prompts::{
|
||||
build_result_entity_system_prompt, build_result_entity_user_prompt,
|
||||
@@ -441,126 +442,111 @@ pub async fn generate_custom_world_scene_image(
|
||||
let normalized = normalize_scene_image_request(payload)
|
||||
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
||||
let asset_id = format!("custom-scene-{}", current_utc_millis());
|
||||
crate::asset_billing::consume_asset_operation_points(
|
||||
let asset = execute_billable_asset_operation(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"scene_image",
|
||||
asset_id.as_str(),
|
||||
async {
|
||||
let settings = require_dashscope_settings(&state)?;
|
||||
let http_client = build_dashscope_http_client(&settings)?;
|
||||
let reference_image =
|
||||
if let Some(reference_image_src) = normalized.reference_image_src.as_deref() {
|
||||
Some(
|
||||
resolve_reference_image_as_data_url(
|
||||
&state,
|
||||
&http_client,
|
||||
reference_image_src,
|
||||
"referenceImageSrc",
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let generated = if let Some(reference_image) = reference_image.as_deref() {
|
||||
create_reference_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
state.config.dashscope_reference_image_model.as_str(),
|
||||
normalized.prompt.as_str(),
|
||||
normalized.size.as_str(),
|
||||
&[reference_image.to_string()],
|
||||
Some(normalized.negative_prompt.as_str()),
|
||||
"创建参考图场景编辑任务失败",
|
||||
"参考图场景编辑未返回图片地址",
|
||||
"scene-edit",
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
create_text_to_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
state.config.dashscope_scene_image_model.as_str(),
|
||||
normalized.prompt.as_str(),
|
||||
Some(normalized.negative_prompt.as_str()),
|
||||
normalized.size.as_str(),
|
||||
"创建场景图片生成任务失败",
|
||||
"查询场景图片任务失败",
|
||||
"场景图片生成任务失败",
|
||||
"场景图片生成超时或未返回图片地址",
|
||||
)
|
||||
.await
|
||||
}?;
|
||||
let scene_model = if reference_image.is_some() {
|
||||
state.config.dashscope_reference_image_model.clone()
|
||||
} else {
|
||||
state.config.dashscope_scene_image_model.clone()
|
||||
};
|
||||
let downloaded = download_remote_image(
|
||||
&http_client,
|
||||
generated.image_url.as_str(),
|
||||
"下载生成图片失败",
|
||||
)
|
||||
.await?;
|
||||
let upload = PreparedAssetUpload {
|
||||
prefix: LegacyAssetPrefix::CustomWorldScenes,
|
||||
path_segments: vec![
|
||||
sanitize_storage_segment(
|
||||
normalized
|
||||
.profile_id
|
||||
.as_deref()
|
||||
.unwrap_or(normalized.world_name.as_str()),
|
||||
"world",
|
||||
),
|
||||
sanitize_storage_segment(normalized.entity_id.as_str(), "scene"),
|
||||
asset_id.clone(),
|
||||
],
|
||||
file_name: format!("scene.{}", downloaded.extension),
|
||||
content_type: downloaded.mime_type,
|
||||
body: downloaded.bytes,
|
||||
asset_kind: "scene_image",
|
||||
entity_kind: "custom_world_landmark",
|
||||
entity_id: normalized.entity_id.clone(),
|
||||
profile_id: normalized.profile_id.clone(),
|
||||
slot: "scene_image",
|
||||
source_job_id: Some(generated.task_id.clone()),
|
||||
};
|
||||
persist_custom_world_asset(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
upload,
|
||||
GeneratedAssetResponse {
|
||||
image_src: String::new(),
|
||||
asset_id: asset_id.clone(),
|
||||
source_type: "generated".to_string(),
|
||||
model: Some(scene_model),
|
||||
size: Some(normalized.size),
|
||||
task_id: Some(generated.task_id),
|
||||
prompt: Some(normalized.prompt),
|
||||
actual_prompt: generated.actual_prompt,
|
||||
},
|
||||
)
|
||||
.await
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
||||
let asset_result = async {
|
||||
let settings = require_dashscope_settings(&state)?;
|
||||
let http_client = build_dashscope_http_client(&settings)?;
|
||||
let reference_image =
|
||||
if let Some(reference_image_src) = normalized.reference_image_src.as_deref() {
|
||||
Some(
|
||||
resolve_reference_image_as_data_url(
|
||||
&state,
|
||||
&http_client,
|
||||
reference_image_src,
|
||||
"referenceImageSrc",
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let generated = if let Some(reference_image) = reference_image.as_deref() {
|
||||
create_reference_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
state.config.dashscope_reference_image_model.as_str(),
|
||||
normalized.prompt.as_str(),
|
||||
normalized.size.as_str(),
|
||||
&[reference_image.to_string()],
|
||||
Some(normalized.negative_prompt.as_str()),
|
||||
"创建参考图场景编辑任务失败",
|
||||
"参考图场景编辑未返回图片地址",
|
||||
"scene-edit",
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
create_text_to_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
state.config.dashscope_scene_image_model.as_str(),
|
||||
normalized.prompt.as_str(),
|
||||
Some(normalized.negative_prompt.as_str()),
|
||||
normalized.size.as_str(),
|
||||
"创建场景图片生成任务失败",
|
||||
"查询场景图片任务失败",
|
||||
"场景图片生成任务失败",
|
||||
"场景图片生成超时或未返回图片地址",
|
||||
)
|
||||
.await
|
||||
}?;
|
||||
let scene_model = if reference_image.is_some() {
|
||||
state.config.dashscope_reference_image_model.clone()
|
||||
} else {
|
||||
state.config.dashscope_scene_image_model.clone()
|
||||
};
|
||||
let downloaded = download_remote_image(
|
||||
&http_client,
|
||||
generated.image_url.as_str(),
|
||||
"下载生成图片失败",
|
||||
)
|
||||
.await?;
|
||||
let upload = PreparedAssetUpload {
|
||||
prefix: LegacyAssetPrefix::CustomWorldScenes,
|
||||
path_segments: vec![
|
||||
sanitize_storage_segment(
|
||||
normalized
|
||||
.profile_id
|
||||
.as_deref()
|
||||
.unwrap_or(normalized.world_name.as_str()),
|
||||
"world",
|
||||
),
|
||||
sanitize_storage_segment(normalized.entity_id.as_str(), "scene"),
|
||||
asset_id.clone(),
|
||||
],
|
||||
file_name: format!("scene.{}", downloaded.extension),
|
||||
content_type: downloaded.mime_type,
|
||||
body: downloaded.bytes,
|
||||
asset_kind: "scene_image",
|
||||
entity_kind: "custom_world_landmark",
|
||||
entity_id: normalized.entity_id.clone(),
|
||||
profile_id: normalized.profile_id.clone(),
|
||||
slot: "scene_image",
|
||||
source_job_id: Some(generated.task_id.clone()),
|
||||
};
|
||||
persist_custom_world_asset(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
upload,
|
||||
GeneratedAssetResponse {
|
||||
image_src: String::new(),
|
||||
asset_id: asset_id.clone(),
|
||||
source_type: "generated".to_string(),
|
||||
model: Some(scene_model),
|
||||
size: Some(normalized.size),
|
||||
task_id: Some(generated.task_id),
|
||||
prompt: Some(normalized.prompt),
|
||||
actual_prompt: generated.actual_prompt,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
.await;
|
||||
|
||||
let asset = match asset_result {
|
||||
Ok(asset) => asset,
|
||||
Err(error) => {
|
||||
crate::asset_billing::refund_asset_operation_points(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"scene_image",
|
||||
&asset_id,
|
||||
)
|
||||
.await;
|
||||
return Err(custom_world_ai_error_response(&request_context, error));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(json_success_body(Some(&request_context), asset))
|
||||
}
|
||||
@@ -717,127 +703,112 @@ pub async fn generate_custom_world_cover_image(
|
||||
let entity_id = profile_id.clone().unwrap_or_else(|| world_name.clone());
|
||||
let size = trim_to_option(payload.size.as_deref()).unwrap_or_else(|| "1600*900".to_string());
|
||||
let asset_id = format!("custom-cover-{}", current_utc_millis());
|
||||
crate::asset_billing::consume_asset_operation_points(
|
||||
let asset = execute_billable_asset_operation(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"custom_world_cover",
|
||||
asset_id.as_str(),
|
||||
async {
|
||||
let settings = require_dashscope_settings(&state)?;
|
||||
let http_client = build_dashscope_http_client(&settings)?;
|
||||
let reference_sources = collect_cover_reference_image_sources(
|
||||
&payload.profile,
|
||||
&payload.character_role_ids,
|
||||
payload.reference_image_src.as_deref().unwrap_or_default(),
|
||||
);
|
||||
let prompt = build_custom_world_cover_image_prompt(
|
||||
&payload.profile,
|
||||
&payload.character_role_ids,
|
||||
payload.user_prompt.as_deref().unwrap_or_default(),
|
||||
!reference_sources.is_empty(),
|
||||
);
|
||||
let mut reference_images = Vec::with_capacity(reference_sources.len());
|
||||
for source in &reference_sources {
|
||||
reference_images.push(
|
||||
resolve_reference_image_as_data_url(
|
||||
&state,
|
||||
&http_client,
|
||||
source.as_str(),
|
||||
"referenceImageSrc",
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
let generated = if reference_images.is_empty() {
|
||||
create_text_to_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
state.config.dashscope_cover_image_model.clone().as_str(),
|
||||
prompt.as_str(),
|
||||
None,
|
||||
size.as_str(),
|
||||
"创建作品封面生成任务失败",
|
||||
"查询作品封面任务失败",
|
||||
"作品封面生成任务失败",
|
||||
"作品封面生成超时或未返回图片地址",
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
create_reference_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
state.config.dashscope_reference_image_model.as_str(),
|
||||
prompt.as_str(),
|
||||
size.as_str(),
|
||||
&reference_images,
|
||||
None,
|
||||
"创建参考图封面任务失败",
|
||||
"封面生成未返回图片地址",
|
||||
"cover-edit",
|
||||
)
|
||||
.await
|
||||
}?;
|
||||
let downloaded = download_remote_image(
|
||||
&http_client,
|
||||
generated.image_url.as_str(),
|
||||
"下载作品封面失败",
|
||||
)
|
||||
.await?;
|
||||
let upload = PreparedAssetUpload {
|
||||
prefix: LegacyAssetPrefix::CustomWorldCovers,
|
||||
path_segments: vec![
|
||||
sanitize_storage_segment(entity_id.as_str(), "world"),
|
||||
asset_id.clone(),
|
||||
],
|
||||
file_name: format!("cover.{}", downloaded.extension),
|
||||
content_type: downloaded.mime_type,
|
||||
body: downloaded.bytes,
|
||||
asset_kind: "custom_world_cover",
|
||||
entity_kind: "custom_world_profile",
|
||||
entity_id,
|
||||
profile_id,
|
||||
slot: "cover",
|
||||
source_job_id: Some(generated.task_id.clone()),
|
||||
};
|
||||
persist_custom_world_asset(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
upload,
|
||||
GeneratedAssetResponse {
|
||||
image_src: String::new(),
|
||||
asset_id: asset_id.clone(),
|
||||
source_type: "generated".to_string(),
|
||||
model: Some(if reference_images.is_empty() {
|
||||
state.config.dashscope_cover_image_model.clone()
|
||||
} else {
|
||||
state.config.dashscope_reference_image_model.clone()
|
||||
}),
|
||||
size: Some(size),
|
||||
task_id: Some(generated.task_id),
|
||||
prompt: Some(prompt),
|
||||
actual_prompt: generated.actual_prompt,
|
||||
},
|
||||
)
|
||||
.await
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| custom_world_ai_error_response(&request_context, error))?;
|
||||
let asset_result = async {
|
||||
let settings = require_dashscope_settings(&state)?;
|
||||
let http_client = build_dashscope_http_client(&settings)?;
|
||||
let reference_sources = collect_cover_reference_image_sources(
|
||||
&payload.profile,
|
||||
&payload.character_role_ids,
|
||||
payload.reference_image_src.as_deref().unwrap_or_default(),
|
||||
);
|
||||
let prompt = build_custom_world_cover_image_prompt(
|
||||
&payload.profile,
|
||||
&payload.character_role_ids,
|
||||
payload.user_prompt.as_deref().unwrap_or_default(),
|
||||
!reference_sources.is_empty(),
|
||||
);
|
||||
let mut reference_images = Vec::with_capacity(reference_sources.len());
|
||||
for source in &reference_sources {
|
||||
reference_images.push(
|
||||
resolve_reference_image_as_data_url(
|
||||
&state,
|
||||
&http_client,
|
||||
source.as_str(),
|
||||
"referenceImageSrc",
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
let generated = if reference_images.is_empty() {
|
||||
create_text_to_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
state.config.dashscope_cover_image_model.clone().as_str(),
|
||||
prompt.as_str(),
|
||||
None,
|
||||
size.as_str(),
|
||||
"创建作品封面生成任务失败",
|
||||
"查询作品封面任务失败",
|
||||
"作品封面生成任务失败",
|
||||
"作品封面生成超时或未返回图片地址",
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
create_reference_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
state.config.dashscope_reference_image_model.as_str(),
|
||||
prompt.as_str(),
|
||||
size.as_str(),
|
||||
&reference_images,
|
||||
None,
|
||||
"创建参考图封面任务失败",
|
||||
"封面生成未返回图片地址",
|
||||
"cover-edit",
|
||||
)
|
||||
.await
|
||||
}?;
|
||||
let downloaded = download_remote_image(
|
||||
&http_client,
|
||||
generated.image_url.as_str(),
|
||||
"下载作品封面失败",
|
||||
)
|
||||
.await?;
|
||||
let upload = PreparedAssetUpload {
|
||||
prefix: LegacyAssetPrefix::CustomWorldCovers,
|
||||
path_segments: vec![
|
||||
sanitize_storage_segment(entity_id.as_str(), "world"),
|
||||
asset_id.clone(),
|
||||
],
|
||||
file_name: format!("cover.{}", downloaded.extension),
|
||||
content_type: downloaded.mime_type,
|
||||
body: downloaded.bytes,
|
||||
asset_kind: "custom_world_cover",
|
||||
entity_kind: "custom_world_profile",
|
||||
entity_id,
|
||||
profile_id,
|
||||
slot: "cover",
|
||||
source_job_id: Some(generated.task_id.clone()),
|
||||
};
|
||||
persist_custom_world_asset(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
upload,
|
||||
GeneratedAssetResponse {
|
||||
image_src: String::new(),
|
||||
asset_id: asset_id.clone(),
|
||||
source_type: "generated".to_string(),
|
||||
model: Some(if reference_images.is_empty() {
|
||||
state.config.dashscope_cover_image_model.clone()
|
||||
} else {
|
||||
state.config.dashscope_reference_image_model.clone()
|
||||
}),
|
||||
size: Some(size),
|
||||
task_id: Some(generated.task_id),
|
||||
prompt: Some(prompt),
|
||||
actual_prompt: generated.actual_prompt,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
.await;
|
||||
|
||||
let asset = match asset_result {
|
||||
Ok(asset) => asset,
|
||||
Err(error) => {
|
||||
crate::asset_billing::refund_asset_operation_points(
|
||||
&state,
|
||||
&owner_user_id,
|
||||
"custom_world_cover",
|
||||
&asset_id,
|
||||
)
|
||||
.await;
|
||||
return Err(custom_world_ai_error_response(&request_context, error));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(json_success_body(Some(&request_context), asset))
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -13,8 +13,8 @@ use module_runtime::{
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::runtime::{
|
||||
CreateProfileRechargeOrderRequest, CreateProfileRechargeOrderResponse,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_CONSUME,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE,
|
||||
@@ -112,11 +112,11 @@ fn format_profile_wallet_ledger_source_type(
|
||||
RuntimeProfileWalletLedgerSourceType::PointsRecharge => {
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE
|
||||
}
|
||||
RuntimeProfileWalletLedgerSourceType::AssetGenerationConsume => {
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_CONSUME
|
||||
RuntimeProfileWalletLedgerSourceType::AssetOperationConsume => {
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME
|
||||
}
|
||||
RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund => {
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND
|
||||
RuntimeProfileWalletLedgerSourceType::AssetOperationRefund => {
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -417,18 +417,18 @@ mod tests {
|
||||
use crate::{app::build_router, config::AppConfig, state::AppState};
|
||||
|
||||
#[test]
|
||||
fn profile_wallet_ledger_source_type_formats_asset_generation_values() {
|
||||
fn profile_wallet_ledger_source_type_formats_asset_operation_values() {
|
||||
assert_eq!(
|
||||
format_profile_wallet_ledger_source_type(
|
||||
RuntimeProfileWalletLedgerSourceType::AssetGenerationConsume
|
||||
RuntimeProfileWalletLedgerSourceType::AssetOperationConsume
|
||||
),
|
||||
shared_contracts::runtime::PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_CONSUME
|
||||
shared_contracts::runtime::PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME
|
||||
);
|
||||
assert_eq!(
|
||||
format_profile_wallet_ledger_source_type(
|
||||
RuntimeProfileWalletLedgerSourceType::AssetGenerationRefund
|
||||
RuntimeProfileWalletLedgerSourceType::AssetOperationRefund
|
||||
),
|
||||
shared_contracts::runtime::PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_GENERATION_REFUND
|
||||
shared_contracts::runtime::PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user