645 lines
23 KiB
Rust
645 lines
23 KiB
Rust
use super::*;
|
||
|
||
struct BigFishDashScopeSettings {
|
||
base_url: String,
|
||
api_key: String,
|
||
request_timeout_ms: u64,
|
||
}
|
||
|
||
struct BigFishGeneratedImage {
|
||
image_url: String,
|
||
task_id: String,
|
||
}
|
||
|
||
struct BigFishDownloadedImage {
|
||
mime_type: String,
|
||
bytes: Vec<u8>,
|
||
}
|
||
|
||
struct BigFishFormalAssetContext {
|
||
entity_id: String,
|
||
prompt: String,
|
||
negative_prompt: String,
|
||
size: String,
|
||
asset_object_kind: String,
|
||
binding_slot: String,
|
||
path_segments: Vec<String>,
|
||
apply_transparent_background_post_process: bool,
|
||
}
|
||
|
||
const BIG_FISH_TEXT_TO_IMAGE_MODEL: &str = "wan2.2-t2i-flash";
|
||
const BIG_FISH_ENTITY_KIND: &str = "big_fish_session";
|
||
|
||
pub(super) async fn generate_big_fish_formal_asset(
|
||
state: &AppState,
|
||
owner_user_id: &str,
|
||
session_id: &str,
|
||
asset_kind: &str,
|
||
level: Option<u32>,
|
||
motion_key: Option<&str>,
|
||
generated_at_micros: i64,
|
||
) -> Result<String, AppError> {
|
||
let session = state
|
||
.spacetime_client()
|
||
.get_big_fish_session(session_id.to_string(), owner_user_id.to_string())
|
||
.await
|
||
.map_err(map_big_fish_client_error)?;
|
||
let draft = session.draft.as_ref().ok_or_else(|| {
|
||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||
"provider": "big-fish",
|
||
"message": "玩法草稿尚未编译,不能生成正式图片。",
|
||
}))
|
||
})?;
|
||
let context = build_big_fish_formal_asset_context(
|
||
&session,
|
||
draft,
|
||
asset_kind,
|
||
level,
|
||
motion_key,
|
||
generated_at_micros,
|
||
)?;
|
||
let settings = require_big_fish_dashscope_settings(state)?;
|
||
let http_client = build_big_fish_dashscope_http_client(&settings)?;
|
||
let generated = create_big_fish_text_to_image_generation(
|
||
&http_client,
|
||
&settings,
|
||
context.prompt.as_str(),
|
||
context.negative_prompt.as_str(),
|
||
context.size.as_str(),
|
||
)
|
||
.await?;
|
||
let downloaded = download_big_fish_remote_image(
|
||
&http_client,
|
||
generated.image_url.as_str(),
|
||
"下载 Big Fish 正式图片失败",
|
||
context.apply_transparent_background_post_process,
|
||
)
|
||
.await?;
|
||
|
||
persist_big_fish_formal_asset(
|
||
state,
|
||
owner_user_id,
|
||
&context,
|
||
generated,
|
||
downloaded,
|
||
generated_at_micros,
|
||
)
|
||
.await
|
||
}
|
||
|
||
fn build_big_fish_formal_asset_context(
|
||
session: &BigFishSessionRecord,
|
||
draft: &BigFishGameDraftRecord,
|
||
asset_kind: &str,
|
||
level: Option<u32>,
|
||
motion_key: Option<&str>,
|
||
generated_at_micros: i64,
|
||
) -> Result<BigFishFormalAssetContext, AppError> {
|
||
let asset_id = format!("asset-{generated_at_micros}");
|
||
match asset_kind {
|
||
"level_main_image" => {
|
||
let level = find_big_fish_level_blueprint(draft, level)?;
|
||
let level_part = build_big_fish_level_part(Some(level.level));
|
||
Ok(BigFishFormalAssetContext {
|
||
entity_id: session.session_id.clone(),
|
||
prompt: build_big_fish_level_main_image_prompt(draft, level),
|
||
negative_prompt: BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT.to_string(),
|
||
size: "1024*1024".to_string(),
|
||
asset_object_kind: "big_fish_level_main_image".to_string(),
|
||
binding_slot: format!("level_main_image:{level_part}"),
|
||
path_segments: vec![
|
||
sanitize_big_fish_path_segment(session.session_id.as_str(), "session"),
|
||
"level-main-image".to_string(),
|
||
level_part,
|
||
asset_id,
|
||
],
|
||
apply_transparent_background_post_process: true,
|
||
})
|
||
}
|
||
"level_motion" => {
|
||
let level = find_big_fish_level_blueprint(draft, level)?;
|
||
let motion_key = motion_key
|
||
.map(str::trim)
|
||
.filter(|value| matches!(*value, "idle_float" | "move_swim"))
|
||
.ok_or_else(|| {
|
||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||
"provider": "big-fish",
|
||
"message": "motionKey 必须是 idle_float 或 move_swim。",
|
||
}))
|
||
})?;
|
||
let level_part = build_big_fish_level_part(Some(level.level));
|
||
Ok(BigFishFormalAssetContext {
|
||
entity_id: session.session_id.clone(),
|
||
prompt: build_big_fish_level_motion_prompt(draft, level, motion_key),
|
||
negative_prompt: BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT.to_string(),
|
||
size: "1024*1024".to_string(),
|
||
asset_object_kind: "big_fish_level_motion".to_string(),
|
||
binding_slot: format!("level_motion:{level_part}:{motion_key}"),
|
||
path_segments: vec![
|
||
sanitize_big_fish_path_segment(session.session_id.as_str(), "session"),
|
||
"level-motion".to_string(),
|
||
level_part,
|
||
sanitize_big_fish_path_segment(motion_key, "motion"),
|
||
asset_id,
|
||
],
|
||
apply_transparent_background_post_process: true,
|
||
})
|
||
}
|
||
"stage_background" => Ok(BigFishFormalAssetContext {
|
||
entity_id: session.session_id.clone(),
|
||
prompt: build_big_fish_stage_background_prompt(draft),
|
||
negative_prompt: BIG_FISH_DEFAULT_NEGATIVE_PROMPT.to_string(),
|
||
size: "720*1280".to_string(),
|
||
asset_object_kind: "big_fish_stage_background".to_string(),
|
||
binding_slot: "stage_background".to_string(),
|
||
path_segments: vec![
|
||
sanitize_big_fish_path_segment(session.session_id.as_str(), "session"),
|
||
"stage-background".to_string(),
|
||
asset_id,
|
||
],
|
||
apply_transparent_background_post_process: false,
|
||
}),
|
||
_ => Err(
|
||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||
"provider": "big-fish",
|
||
"message": format!("assetKind `{asset_kind}` 不支持正式图片生成。"),
|
||
})),
|
||
),
|
||
}
|
||
}
|
||
|
||
fn find_big_fish_level_blueprint(
|
||
draft: &BigFishGameDraftRecord,
|
||
level: Option<u32>,
|
||
) -> Result<&BigFishLevelBlueprintRecord, AppError> {
|
||
let level = level.ok_or_else(|| {
|
||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||
"provider": "big-fish",
|
||
"message": "level 是等级资产生成的必填项。",
|
||
}))
|
||
})?;
|
||
draft
|
||
.levels
|
||
.iter()
|
||
.find(|blueprint| blueprint.level == level)
|
||
.ok_or_else(|| {
|
||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||
"provider": "big-fish",
|
||
"message": format!("level `{level}` 不存在于当前 Big Fish 草稿。"),
|
||
}))
|
||
})
|
||
}
|
||
|
||
fn require_big_fish_dashscope_settings(
|
||
state: &AppState,
|
||
) -> Result<BigFishDashScopeSettings, AppError> {
|
||
let base_url = state.config.dashscope_base_url.trim().trim_end_matches('/');
|
||
if base_url.is_empty() {
|
||
return Err(
|
||
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
||
"provider": "dashscope",
|
||
"reason": "DASHSCOPE_BASE_URL 未配置",
|
||
})),
|
||
);
|
||
}
|
||
|
||
let api_key = state
|
||
.config
|
||
.dashscope_api_key
|
||
.as_deref()
|
||
.map(str::trim)
|
||
.filter(|value| !value.is_empty())
|
||
.ok_or_else(|| {
|
||
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
||
"provider": "dashscope",
|
||
"reason": "DASHSCOPE_API_KEY 未配置",
|
||
}))
|
||
})?;
|
||
|
||
Ok(BigFishDashScopeSettings {
|
||
base_url: base_url.to_string(),
|
||
api_key: api_key.to_string(),
|
||
request_timeout_ms: state.config.dashscope_image_request_timeout_ms.max(1),
|
||
})
|
||
}
|
||
|
||
fn build_big_fish_dashscope_http_client(
|
||
settings: &BigFishDashScopeSettings,
|
||
) -> Result<reqwest::Client, AppError> {
|
||
reqwest::Client::builder()
|
||
.timeout(Duration::from_millis(settings.request_timeout_ms))
|
||
.build()
|
||
.map_err(|error| {
|
||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||
"provider": "dashscope",
|
||
"message": format!("构造 DashScope HTTP 客户端失败:{error}"),
|
||
}))
|
||
})
|
||
}
|
||
|
||
async fn create_big_fish_text_to_image_generation(
|
||
http_client: &reqwest::Client,
|
||
settings: &BigFishDashScopeSettings,
|
||
prompt: &str,
|
||
negative_prompt: &str,
|
||
size: &str,
|
||
) -> Result<BigFishGeneratedImage, AppError> {
|
||
let mut parameters = Map::from_iter([
|
||
("n".to_string(), json!(1)),
|
||
("size".to_string(), Value::String(size.to_string())),
|
||
("prompt_extend".to_string(), Value::Bool(true)),
|
||
("watermark".to_string(), Value::Bool(false)),
|
||
]);
|
||
if !negative_prompt.trim().is_empty() {
|
||
parameters.insert(
|
||
"negative_prompt".to_string(),
|
||
Value::String(negative_prompt.trim().to_string()),
|
||
);
|
||
}
|
||
|
||
let response = http_client
|
||
.post(format!(
|
||
"{}/services/aigc/text2image/image-synthesis",
|
||
settings.base_url
|
||
))
|
||
.header(
|
||
reqwest::header::AUTHORIZATION,
|
||
format!("Bearer {}", settings.api_key),
|
||
)
|
||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||
.header("X-DashScope-Async", "enable")
|
||
.json(&json!({
|
||
"model": BIG_FISH_TEXT_TO_IMAGE_MODEL,
|
||
"input": {
|
||
"prompt": prompt,
|
||
},
|
||
"parameters": parameters,
|
||
}))
|
||
.send()
|
||
.await
|
||
.map_err(|error| {
|
||
map_big_fish_dashscope_request_error(format!("创建 Big Fish 图片生成任务失败:{error}"))
|
||
})?;
|
||
let status = response.status();
|
||
let response_text = response.text().await.map_err(|error| {
|
||
map_big_fish_dashscope_request_error(format!("读取 Big Fish 图片生成响应失败:{error}"))
|
||
})?;
|
||
if !status.is_success() {
|
||
return Err(map_big_fish_dashscope_upstream_error(
|
||
response_text.as_str(),
|
||
"创建 Big Fish 图片生成任务失败",
|
||
));
|
||
}
|
||
let payload =
|
||
parse_big_fish_json_payload(response_text.as_str(), "解析 Big Fish 图片生成响应失败")?;
|
||
let task_id = extract_big_fish_task_id(&payload).ok_or_else(|| {
|
||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||
"provider": "dashscope",
|
||
"message": "Big Fish 图片生成任务未返回 task_id",
|
||
}))
|
||
})?;
|
||
let deadline = Instant::now() + Duration::from_millis(settings.request_timeout_ms);
|
||
|
||
while Instant::now() < deadline {
|
||
let poll_response = http_client
|
||
.get(format!("{}/tasks/{}", settings.base_url, task_id))
|
||
.header(
|
||
reqwest::header::AUTHORIZATION,
|
||
format!("Bearer {}", settings.api_key),
|
||
)
|
||
.send()
|
||
.await
|
||
.map_err(|error| {
|
||
map_big_fish_dashscope_request_error(format!(
|
||
"查询 Big Fish 图片生成任务失败:{error}"
|
||
))
|
||
})?;
|
||
let poll_status = poll_response.status();
|
||
let poll_text = poll_response.text().await.map_err(|error| {
|
||
map_big_fish_dashscope_request_error(format!(
|
||
"读取 Big Fish 图片生成任务响应失败:{error}"
|
||
))
|
||
})?;
|
||
if !poll_status.is_success() {
|
||
return Err(map_big_fish_dashscope_upstream_error(
|
||
poll_text.as_str(),
|
||
"查询 Big Fish 图片生成任务失败",
|
||
));
|
||
}
|
||
let poll_payload =
|
||
parse_big_fish_json_payload(poll_text.as_str(), "解析 Big Fish 图片生成任务响应失败")?;
|
||
let task_status = find_first_big_fish_string_by_key(&poll_payload, "task_status")
|
||
.unwrap_or_default()
|
||
.trim()
|
||
.to_string();
|
||
if task_status == "SUCCEEDED" {
|
||
let image_url = extract_big_fish_image_urls(&poll_payload)
|
||
.into_iter()
|
||
.next()
|
||
.ok_or_else(|| {
|
||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||
"provider": "dashscope",
|
||
"message": "Big Fish 图片生成成功但未返回图片地址",
|
||
}))
|
||
})?;
|
||
return Ok(BigFishGeneratedImage { image_url, task_id });
|
||
}
|
||
if matches!(task_status.as_str(), "FAILED" | "UNKNOWN") {
|
||
return Err(map_big_fish_dashscope_upstream_error(
|
||
poll_text.as_str(),
|
||
"Big Fish 图片生成任务失败",
|
||
));
|
||
}
|
||
|
||
sleep(Duration::from_secs(2)).await;
|
||
}
|
||
|
||
Err(
|
||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||
"provider": "dashscope",
|
||
"message": "Big Fish 图片生成超时或未返回图片地址",
|
||
})),
|
||
)
|
||
}
|
||
|
||
async fn download_big_fish_remote_image(
|
||
http_client: &reqwest::Client,
|
||
image_url: &str,
|
||
fallback_message: &str,
|
||
apply_transparent_background_post_process: bool,
|
||
) -> Result<BigFishDownloadedImage, AppError> {
|
||
let response = http_client.get(image_url).send().await.map_err(|error| {
|
||
map_big_fish_dashscope_request_error(format!("{fallback_message}:{error}"))
|
||
})?;
|
||
let status = response.status();
|
||
let content_type = response
|
||
.headers()
|
||
.get(reqwest::header::CONTENT_TYPE)
|
||
.and_then(|value| value.to_str().ok())
|
||
.unwrap_or("image/jpeg")
|
||
.to_string();
|
||
let bytes = response.bytes().await.map_err(|error| {
|
||
map_big_fish_dashscope_request_error(format!("{fallback_message}:{error}"))
|
||
})?;
|
||
if !status.is_success() {
|
||
return Err(
|
||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||
"provider": "dashscope",
|
||
"message": fallback_message,
|
||
"status": status.as_u16(),
|
||
})),
|
||
);
|
||
}
|
||
|
||
let mime_type = normalize_big_fish_downloaded_image_mime_type(content_type.as_str());
|
||
let mut normalized_bytes = bytes.to_vec();
|
||
let mut normalized_mime_type = mime_type;
|
||
|
||
// 中文注释:Big Fish 的等级主图与动作关键帧要和 RPG 角色主图保持同一后处理口径。
|
||
// 因此在上游已经输出 PNG 时,统一补一层透明背景 alpha 清理,避免只靠 prompt 约束导致残留底色。
|
||
if apply_transparent_background_post_process
|
||
&& normalized_mime_type == "image/png"
|
||
&& let Some(optimized) = try_apply_background_alpha_to_png(normalized_bytes.as_slice())
|
||
{
|
||
normalized_bytes = optimized;
|
||
normalized_mime_type = "image/png".to_string();
|
||
}
|
||
|
||
Ok(BigFishDownloadedImage {
|
||
mime_type: normalized_mime_type,
|
||
bytes: normalized_bytes,
|
||
})
|
||
}
|
||
|
||
async fn persist_big_fish_formal_asset(
|
||
state: &AppState,
|
||
owner_user_id: &str,
|
||
context: &BigFishFormalAssetContext,
|
||
generated: BigFishGeneratedImage,
|
||
downloaded: BigFishDownloadedImage,
|
||
generated_at_micros: i64,
|
||
) -> Result<String, AppError> {
|
||
let oss_client = state.oss_client().ok_or_else(|| {
|
||
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
||
"provider": "aliyun-oss",
|
||
"reason": "OSS 未完成环境变量配置",
|
||
}))
|
||
})?;
|
||
let http_client = reqwest::Client::new();
|
||
let image_format = normalize_generated_image_asset_mime(downloaded.mime_type.as_str());
|
||
let prepared =
|
||
GeneratedImageAssetAdapter::prepare_put_object(GeneratedImageAssetPersistInput {
|
||
prefix: LegacyAssetPrefix::BigFishAssets,
|
||
path_segments: context.path_segments.clone(),
|
||
file_stem: "image".to_string(),
|
||
image: GeneratedImageAssetDataUrl {
|
||
format: image_format,
|
||
bytes: downloaded.bytes,
|
||
},
|
||
access: OssObjectAccess::Private,
|
||
metadata: GeneratedImageAssetAdapterMetadata {
|
||
asset_kind: Some(context.asset_object_kind.clone()),
|
||
owner_user_id: Some(owner_user_id.to_string()),
|
||
entity_kind: Some(BIG_FISH_ENTITY_KIND.to_string()),
|
||
entity_id: Some(context.entity_id.clone()),
|
||
slot: Some(context.binding_slot.clone()),
|
||
provider: Some("dashscope".to_string()),
|
||
task_id: Some(generated.task_id.clone()),
|
||
},
|
||
extra_metadata: BTreeMap::new(),
|
||
})
|
||
.map_err(map_big_fish_generated_image_asset_error)?;
|
||
let persisted_mime_type = prepared.format.mime_type.clone();
|
||
let put_result = oss_client
|
||
.put_object(&http_client, prepared.request)
|
||
.await
|
||
.map_err(map_big_fish_asset_oss_error)?;
|
||
let head = oss_client
|
||
.head_object(
|
||
&http_client,
|
||
OssHeadObjectRequest {
|
||
object_key: put_result.object_key.clone(),
|
||
},
|
||
)
|
||
.await
|
||
.map_err(map_big_fish_asset_oss_error)?;
|
||
let asset_object = state
|
||
.spacetime_client()
|
||
.confirm_asset_object(
|
||
build_asset_object_upsert_input(
|
||
generate_asset_object_id(generated_at_micros),
|
||
head.bucket,
|
||
head.object_key,
|
||
AssetObjectAccessPolicy::Private,
|
||
head.content_type.or(Some(persisted_mime_type)),
|
||
head.content_length,
|
||
head.etag,
|
||
context.asset_object_kind.clone(),
|
||
Some(generated.task_id),
|
||
Some(owner_user_id.to_string()),
|
||
None,
|
||
Some(context.entity_id.clone()),
|
||
generated_at_micros,
|
||
)
|
||
.map_err(map_big_fish_asset_object_prepare_error)?,
|
||
)
|
||
.await
|
||
.map_err(map_big_fish_asset_spacetime_error)?;
|
||
state
|
||
.spacetime_client()
|
||
.bind_asset_object_to_entity(
|
||
build_asset_entity_binding_input(
|
||
generate_asset_binding_id(generated_at_micros),
|
||
asset_object.asset_object_id,
|
||
BIG_FISH_ENTITY_KIND.to_string(),
|
||
context.entity_id.clone(),
|
||
context.binding_slot.clone(),
|
||
context.asset_object_kind.clone(),
|
||
Some(owner_user_id.to_string()),
|
||
None,
|
||
generated_at_micros,
|
||
)
|
||
.map_err(map_big_fish_asset_binding_prepare_error)?,
|
||
)
|
||
.await
|
||
.map_err(map_big_fish_asset_spacetime_error)?;
|
||
|
||
Ok(put_result.legacy_public_path)
|
||
}
|
||
|
||
fn map_big_fish_generated_image_asset_error(
|
||
error: crate::generated_image_assets::helpers::GeneratedImageAssetHelperError,
|
||
) -> AppError {
|
||
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
|
||
"provider": "generated-image-assets",
|
||
"message": format!("准备 Big Fish 图片资产上传请求失败:{error:?}"),
|
||
}))
|
||
}
|
||
|
||
fn parse_big_fish_json_payload(raw_text: &str, fallback_message: &str) -> Result<Value, AppError> {
|
||
serde_json::from_str::<Value>(raw_text).map_err(|error| {
|
||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||
"provider": "dashscope",
|
||
"message": format!("{fallback_message}:{error}"),
|
||
}))
|
||
})
|
||
}
|
||
|
||
fn extract_big_fish_task_id(payload: &Value) -> Option<String> {
|
||
find_first_big_fish_string_by_key(payload, "task_id")
|
||
}
|
||
|
||
fn extract_big_fish_image_urls(payload: &Value) -> Vec<String> {
|
||
let mut urls = Vec::new();
|
||
collect_big_fish_strings_by_key(payload, "image", &mut urls);
|
||
collect_big_fish_strings_by_key(payload, "url", &mut urls);
|
||
let mut deduped = Vec::new();
|
||
for url in urls {
|
||
if !deduped.contains(&url) {
|
||
deduped.push(url);
|
||
}
|
||
}
|
||
deduped
|
||
}
|
||
|
||
fn find_first_big_fish_string_by_key(payload: &Value, target_key: &str) -> Option<String> {
|
||
let mut results = Vec::new();
|
||
collect_big_fish_strings_by_key(payload, target_key, &mut results);
|
||
results.into_iter().next()
|
||
}
|
||
|
||
fn collect_big_fish_strings_by_key(payload: &Value, target_key: &str, results: &mut Vec<String>) {
|
||
match payload {
|
||
Value::Array(entries) => {
|
||
for entry in entries {
|
||
collect_big_fish_strings_by_key(entry, target_key, results);
|
||
}
|
||
}
|
||
Value::Object(object) => {
|
||
for (key, value) in object {
|
||
if key == target_key
|
||
&& let Some(text) = value.as_str()
|
||
{
|
||
results.push(text.to_string());
|
||
}
|
||
collect_big_fish_strings_by_key(value, target_key, results);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn normalize_big_fish_downloaded_image_mime_type(content_type: &str) -> String {
|
||
let mime_type = content_type
|
||
.split(';')
|
||
.next()
|
||
.map(str::trim)
|
||
.unwrap_or("image/jpeg");
|
||
match mime_type {
|
||
"image/png" | "image/webp" | "image/jpeg" | "image/jpg" | "image/gif" => {
|
||
mime_type.to_string()
|
||
}
|
||
_ => "image/jpeg".to_string(),
|
||
}
|
||
}
|
||
|
||
fn map_big_fish_dashscope_request_error(message: String) -> AppError {
|
||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||
"provider": "dashscope",
|
||
"message": message,
|
||
}))
|
||
}
|
||
|
||
fn map_big_fish_dashscope_upstream_error(raw_text: &str, fallback_message: &str) -> AppError {
|
||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||
"provider": "dashscope",
|
||
"message": parse_big_fish_api_error_message(raw_text, fallback_message),
|
||
}))
|
||
}
|
||
|
||
fn parse_big_fish_api_error_message(raw_text: &str, fallback_message: &str) -> String {
|
||
let trimmed = raw_text.trim();
|
||
if trimmed.is_empty() {
|
||
return fallback_message.to_string();
|
||
}
|
||
if let Ok(payload) = serde_json::from_str::<Value>(trimmed)
|
||
&& let Some(message) = find_first_big_fish_string_by_key(&payload, "message")
|
||
.or_else(|| find_first_big_fish_string_by_key(&payload, "code"))
|
||
{
|
||
return message;
|
||
}
|
||
let excerpt = trimmed.chars().take(240).collect::<String>();
|
||
format!("{fallback_message}:{excerpt}")
|
||
}
|
||
|
||
fn map_big_fish_asset_object_prepare_error(error: AssetObjectFieldError) -> AppError {
|
||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||
"provider": "asset-object",
|
||
"message": error.to_string(),
|
||
}))
|
||
}
|
||
|
||
fn map_big_fish_asset_binding_prepare_error(error: AssetObjectFieldError) -> AppError {
|
||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||
"provider": "asset-entity-binding",
|
||
"message": error.to_string(),
|
||
}))
|
||
}
|
||
|
||
fn map_big_fish_asset_spacetime_error(error: SpacetimeClientError) -> AppError {
|
||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||
"provider": "spacetimedb",
|
||
"message": error.to_string(),
|
||
}))
|
||
}
|
||
|
||
fn map_big_fish_asset_oss_error(error: platform_oss::OssError) -> AppError {
|
||
map_oss_error(error, "aliyun-oss")
|
||
}
|
||
|
||
fn build_big_fish_level_part(level: Option<u32>) -> String {
|
||
level
|
||
.map(|value| format!("level-{value}"))
|
||
.unwrap_or_else(|| "stage".to_string())
|
||
}
|