1
This commit is contained in:
@@ -121,7 +121,7 @@ pub async fn generate_character_visual(
|
||||
"sourceMode": payload.source_mode,
|
||||
"size": size,
|
||||
"referenceImageCount": payload.reference_image_data_urls.len(),
|
||||
"provider": "apimart",
|
||||
"provider": "vector-engine",
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
@@ -193,7 +193,7 @@ pub async fn generate_character_visual(
|
||||
),
|
||||
structured_payload_json: Some(
|
||||
json!({
|
||||
"provider": "apimart",
|
||||
"provider": "vector-engine",
|
||||
"taskId": generated.task_id,
|
||||
"model": model,
|
||||
"imageCount": generated.images.len(),
|
||||
@@ -1172,7 +1172,7 @@ fn map_character_visual_oss_error(error: platform_oss::OssError) -> AppError {
|
||||
|
||||
fn map_image_request_error(message: String) -> AppError {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"provider": "vector-engine",
|
||||
"message": message,
|
||||
}))
|
||||
}
|
||||
@@ -1184,7 +1184,7 @@ fn map_image_upstream_error(raw_text: &str, fallback_message: &str) -> AppError
|
||||
value => value.to_string(),
|
||||
};
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"provider": "vector-engine",
|
||||
"message": message,
|
||||
"raw": raw_text.trim(),
|
||||
}))
|
||||
|
||||
@@ -583,7 +583,7 @@ pub async fn generate_custom_world_scene_image(
|
||||
.map(downloaded_openai_to_custom_world_image)
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"provider": "vector-engine",
|
||||
"message": "场景图片生成成功但未返回图片。",
|
||||
}))
|
||||
})?;
|
||||
@@ -687,7 +687,7 @@ pub(crate) async fn generate_custom_world_scene_image_for_profile(
|
||||
.map(downloaded_openai_to_custom_world_image)
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"provider": "vector-engine",
|
||||
"message": "场景图片生成成功但未返回图片。",
|
||||
}))
|
||||
})?;
|
||||
@@ -1200,7 +1200,7 @@ async fn generate_opening_cg_storyboard(
|
||||
.map(downloaded_openai_to_custom_world_image)
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"provider": "vector-engine",
|
||||
"message": "开局 CG 故事板生成成功但未返回图片。",
|
||||
}))
|
||||
})?;
|
||||
@@ -3274,9 +3274,10 @@ mod tests {
|
||||
serde_json::from_slice(&body).expect("body should be valid json")
|
||||
}
|
||||
|
||||
fn build_state_without_apimart_key() -> AppState {
|
||||
fn build_state_without_vector_engine_key() -> AppState {
|
||||
let mut config = AppConfig::default();
|
||||
config.apimart_api_key = None;
|
||||
config.vector_engine_base_url = "https://api.vectorengine.test".to_string();
|
||||
config.vector_engine_api_key = None;
|
||||
AppState::new(config).expect("state should build")
|
||||
}
|
||||
|
||||
@@ -3287,8 +3288,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scene_image_returns_service_unavailable_when_apimart_missing() {
|
||||
let state = build_state_without_apimart_key();
|
||||
async fn scene_image_returns_service_unavailable_when_vector_engine_missing() {
|
||||
let state = build_state_without_vector_engine_key();
|
||||
let request_context = build_request_context("POST /api/runtime/custom-world/scene-image");
|
||||
let authenticated = build_authenticated(&state);
|
||||
|
||||
@@ -3311,7 +3312,7 @@ mod tests {
|
||||
})),
|
||||
)
|
||||
.await
|
||||
.expect_err("missing apimart should fail");
|
||||
.expect_err("missing vector engine should fail");
|
||||
|
||||
let payload = read_error_response(response).await;
|
||||
assert_eq!(
|
||||
@@ -3320,7 +3321,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
payload["error"]["details"]["provider"],
|
||||
Value::String("apimart".to_string())
|
||||
Value::String("vector-engine".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -149,10 +149,12 @@ pub(crate) async fn create_openai_image_generation(
|
||||
return Ok(generated);
|
||||
}
|
||||
|
||||
Err(AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"message": format!("{failure_context}:VectorEngine 未返回图片地址"),
|
||||
})))
|
||||
Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"message": format!("{failure_context}:VectorEngine 未返回图片地址"),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn build_openai_image_request_body(
|
||||
@@ -200,8 +202,8 @@ fn build_prompt_with_negative(prompt: &str, negative_prompt: Option<&str>) -> St
|
||||
fn normalize_image_size(size: &str) -> String {
|
||||
match size.trim() {
|
||||
"1024*1024" | "1024x1024" | "1:1" => "1024x1024",
|
||||
"1280*720" | "1280x720" | "1600*900" | "1600x900" | "16:9"
|
||||
| "1536x1024" | "2048x1152" | "2k" => "1536x1024",
|
||||
"1280*720" | "1280x720" | "1600*900" | "1600x900" | "16:9" | "1536x1024" | "2048x1152"
|
||||
| "2k" => "1536x1024",
|
||||
"1024*1536" | "1024x1536" | "9:16" => "1024x1536",
|
||||
value if !value.is_empty() => value,
|
||||
_ => "1024x1024",
|
||||
|
||||
@@ -3877,9 +3877,6 @@ fn map_puzzle_compile_error(error: SpacetimeClientError) -> AppError {
|
||||
let provider = if message.contains("VectorEngine")
|
||||
|| message.contains("vector-engine")
|
||||
|| message.contains("VECTOR_ENGINE")
|
||||
|| message.contains("APIMart")
|
||||
|| message.contains("apimart")
|
||||
|| message.contains("APIMART")
|
||||
{
|
||||
VECTOR_ENGINE_PROVIDER
|
||||
} else if message.contains("OSS") || message.contains("oss") || message.contains("参考图") {
|
||||
@@ -3890,8 +3887,6 @@ fn map_puzzle_compile_error(error: SpacetimeClientError) -> AppError {
|
||||
let status = if provider == VECTOR_ENGINE_PROVIDER
|
||||
&& (message.contains("VECTOR_ENGINE_API_KEY")
|
||||
|| message.contains("VECTOR_ENGINE_BASE_URL")
|
||||
|| message.contains("APIMART_API_KEY")
|
||||
|| message.contains("APIMART_BASE_URL")
|
||||
|| message.contains("未配置"))
|
||||
{
|
||||
StatusCode::SERVICE_UNAVAILABLE
|
||||
@@ -3907,9 +3902,6 @@ fn map_puzzle_compile_error(error: SpacetimeClientError) -> AppError {
|
||||
|| message.contains("VectorEngine")
|
||||
|| message.contains("vector-engine")
|
||||
|| message.contains("VECTOR_ENGINE")
|
||||
|| message.contains("APIMart")
|
||||
|| message.contains("apimart")
|
||||
|| message.contains("APIMART")
|
||||
|| message.contains("参考图")
|
||||
|| message.contains("图片")
|
||||
|| message.contains("OSS")
|
||||
@@ -4039,14 +4031,14 @@ async fn generate_puzzle_image_candidates(
|
||||
};
|
||||
// 中文注释:SpacetimeDB reducer 不能做外部 I/O,参考图读取与外部生图都必须停留在 api-server。
|
||||
// 中文注释:拼图作品资产统一按 1:1 正方形生成,前端运行时也按正方形棋盘切块承载。
|
||||
let settings = require_puzzle_apimart_settings(state)?;
|
||||
let generated = create_puzzle_apimart_image_generation(
|
||||
let settings = require_puzzle_vector_engine_settings(state)?;
|
||||
let generated = create_puzzle_vector_engine_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
resolved_model,
|
||||
actual_prompt.as_str(),
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_APIMART_GENERATED_IMAGE_SIZE,
|
||||
PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE,
|
||||
count,
|
||||
reference_image.as_deref(),
|
||||
)
|
||||
@@ -4097,26 +4089,25 @@ mod tests {
|
||||
#[test]
|
||||
fn puzzle_generated_image_size_is_square_1_1() {
|
||||
assert_eq!(PUZZLE_GENERATED_IMAGE_SIZE, "1024*1024");
|
||||
assert_eq!(PUZZLE_APIMART_GENERATED_IMAGE_SIZE, "1:1");
|
||||
assert_eq!(PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE, "1024x1024");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_apimart_request_uses_selected_model_and_reference_images() {
|
||||
let body = build_puzzle_apimart_image_request_body(
|
||||
fn puzzle_vector_engine_request_uses_gpt_image_2_all_and_reference_images() {
|
||||
let body = build_puzzle_vector_engine_image_request_body(
|
||||
PuzzleImageModel::Gemini31FlashPreview,
|
||||
"一只猫在雨夜灯牌下回头。",
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_APIMART_GENERATED_IMAGE_SIZE,
|
||||
PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE,
|
||||
4,
|
||||
Some("data:image/png;base64,abcd"),
|
||||
);
|
||||
|
||||
assert_eq!(body["model"], PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW);
|
||||
assert_eq!(body["size"], PUZZLE_APIMART_GENERATED_IMAGE_SIZE);
|
||||
assert_eq!(body["resolution"], PUZZLE_APIMART_GEMINI_RESOLUTION);
|
||||
assert_eq!(body["model"], VECTOR_ENGINE_GPT_IMAGE_2_MODEL);
|
||||
assert_eq!(body["size"], PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE);
|
||||
assert_eq!(body["n"], 1);
|
||||
assert_eq!(body["official_fallback"], true);
|
||||
assert_eq!(body["image_urls"][0], "data:image/png;base64,abcd");
|
||||
assert!(body.get("official_fallback").is_none());
|
||||
assert_eq!(body["image"][0], "data:image/png;base64,abcd");
|
||||
assert!(
|
||||
body["prompt"]
|
||||
.as_str()
|
||||
@@ -4126,9 +4117,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_compile_error_preserves_apimart_unavailable_status() {
|
||||
fn puzzle_compile_error_preserves_vector_engine_unavailable_status() {
|
||||
let error = map_puzzle_compile_error(SpacetimeClientError::Runtime(
|
||||
"APIMART_API_KEY 未配置".to_string(),
|
||||
"VECTOR_ENGINE_API_KEY 未配置".to_string(),
|
||||
));
|
||||
|
||||
let response = error.into_response();
|
||||
@@ -4313,14 +4304,11 @@ enum PuzzleImageModel {
|
||||
|
||||
impl PuzzleImageModel {
|
||||
fn provider_name(self) -> &'static str {
|
||||
"apimart"
|
||||
VECTOR_ENGINE_PROVIDER
|
||||
}
|
||||
|
||||
fn request_model_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::GptImage2 => PUZZLE_IMAGE_MODEL_GPT_IMAGE_2,
|
||||
Self::Gemini31FlashPreview => PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW,
|
||||
}
|
||||
VECTOR_ENGINE_GPT_IMAGE_2_MODEL
|
||||
}
|
||||
|
||||
fn candidate_source_type(self) -> &'static str {
|
||||
@@ -4331,10 +4319,9 @@ impl PuzzleImageModel {
|
||||
}
|
||||
}
|
||||
|
||||
struct PuzzleApimartSettings {
|
||||
struct PuzzleVectorEngineSettings {
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
request_timeout_ms: u64,
|
||||
}
|
||||
|
||||
struct PuzzleGeneratedImages {
|
||||
@@ -4384,41 +4371,53 @@ struct GeneratedPuzzleAssetResponse {
|
||||
|
||||
fn resolve_puzzle_image_model(value: Option<&str>) -> PuzzleImageModel {
|
||||
match value.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
Some(PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW) => PuzzleImageModel::Gemini31FlashPreview,
|
||||
Some(PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW) => {
|
||||
tracing::warn!(
|
||||
requested_model = PUZZLE_IMAGE_MODEL_GEMINI_31_FLASH_PREVIEW,
|
||||
effective_model = VECTOR_ENGINE_GPT_IMAGE_2_MODEL,
|
||||
"拼图 nanobanana2 历史选项已回落到 VectorEngine GPT-image-2-all"
|
||||
);
|
||||
PuzzleImageModel::Gemini31FlashPreview
|
||||
}
|
||||
_ => PuzzleImageModel::GptImage2,
|
||||
}
|
||||
}
|
||||
|
||||
fn require_puzzle_apimart_settings(state: &AppState) -> Result<PuzzleApimartSettings, AppError> {
|
||||
let base_url = state.config.apimart_base_url.trim().trim_end_matches('/');
|
||||
fn require_puzzle_vector_engine_settings(
|
||||
state: &AppState,
|
||||
) -> Result<PuzzleVectorEngineSettings, AppError> {
|
||||
let base_url = state
|
||||
.config
|
||||
.vector_engine_base_url
|
||||
.trim()
|
||||
.trim_end_matches('/');
|
||||
if base_url.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::SERVICE_UNAVAILABLE).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"message": "APIMart 图片生成地址未配置",
|
||||
"reason": "APIMART_BASE_URL 未配置",
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"message": "VectorEngine 图片生成地址未配置",
|
||||
"reason": "VECTOR_ENGINE_BASE_URL 未配置",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
let api_key = state
|
||||
.config
|
||||
.apimart_api_key
|
||||
.vector_engine_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": "apimart",
|
||||
"message": "APIMart 图片生成密钥未配置",
|
||||
"reason": "APIMART_API_KEY 未配置",
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"message": "VectorEngine 图片生成密钥未配置",
|
||||
"reason": "VECTOR_ENGINE_API_KEY 未配置",
|
||||
}))
|
||||
})?;
|
||||
|
||||
Ok(PuzzleApimartSettings {
|
||||
Ok(PuzzleVectorEngineSettings {
|
||||
base_url: base_url.to_string(),
|
||||
api_key: api_key.to_string(),
|
||||
request_timeout_ms: state.config.apimart_image_request_timeout_ms.max(1),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4427,7 +4426,7 @@ fn build_puzzle_image_http_client(
|
||||
image_model: PuzzleImageModel,
|
||||
) -> Result<reqwest::Client, AppError> {
|
||||
let provider = image_model.provider_name();
|
||||
let request_timeout_ms = state.config.apimart_image_request_timeout_ms;
|
||||
let request_timeout_ms = state.config.vector_engine_image_request_timeout_ms;
|
||||
|
||||
reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(request_timeout_ms.max(1)))
|
||||
@@ -4455,9 +4454,9 @@ fn to_puzzle_generated_image_candidate(
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_puzzle_apimart_image_generation(
|
||||
async fn create_puzzle_vector_engine_image_generation(
|
||||
http_client: &reqwest::Client,
|
||||
settings: &PuzzleApimartSettings,
|
||||
settings: &PuzzleVectorEngineSettings,
|
||||
image_model: PuzzleImageModel,
|
||||
prompt: &str,
|
||||
negative_prompt: &str,
|
||||
@@ -4465,7 +4464,7 @@ async fn create_puzzle_apimart_image_generation(
|
||||
candidate_count: u32,
|
||||
reference_image: Option<&str>,
|
||||
) -> Result<PuzzleGeneratedImages, AppError> {
|
||||
let request_body = build_puzzle_apimart_image_request_body(
|
||||
let request_body = build_puzzle_vector_engine_image_request_body(
|
||||
image_model,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
@@ -4474,61 +4473,59 @@ async fn create_puzzle_apimart_image_generation(
|
||||
reference_image,
|
||||
);
|
||||
let response = http_client
|
||||
.post(format!("{}/images/generations", settings.base_url))
|
||||
.post(puzzle_vector_engine_images_generation_url(settings))
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
format!("Bearer {}", settings.api_key),
|
||||
)
|
||||
.header(reqwest::header::ACCEPT, "application/json")
|
||||
.header(reqwest::header::CONTENT_TYPE, "application/json")
|
||||
.json(&request_body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
map_puzzle_apimart_request_error(format!("创建拼图 APIMart 图片生成任务失败:{error}"))
|
||||
map_puzzle_vector_engine_request_error(format!(
|
||||
"创建拼图 VectorEngine 图片生成任务失败:{error}"
|
||||
))
|
||||
})?;
|
||||
let status = response.status();
|
||||
let response_text = response.text().await.map_err(|error| {
|
||||
map_puzzle_apimart_request_error(format!("读取拼图 APIMart 图片生成响应失败:{error}"))
|
||||
map_puzzle_vector_engine_request_error(format!(
|
||||
"读取拼图 VectorEngine 图片生成响应失败:{error}"
|
||||
))
|
||||
})?;
|
||||
if !status.is_success() {
|
||||
return Err(map_puzzle_apimart_upstream_error(
|
||||
return Err(map_puzzle_vector_engine_upstream_error(
|
||||
status,
|
||||
response_text.as_str(),
|
||||
"创建拼图 APIMart 图片生成任务失败",
|
||||
"创建拼图 VectorEngine 图片生成任务失败",
|
||||
));
|
||||
}
|
||||
|
||||
let payload =
|
||||
parse_puzzle_json_payload(response_text.as_str(), "解析拼图 APIMart 图片生成响应失败")?;
|
||||
let payload = parse_puzzle_json_payload(
|
||||
response_text.as_str(),
|
||||
"解析拼图 VectorEngine 图片生成响应失败",
|
||||
)?;
|
||||
let image_urls = extract_puzzle_image_urls(&payload);
|
||||
if !image_urls.is_empty() {
|
||||
return download_puzzle_images_from_urls(
|
||||
http_client,
|
||||
format!("apimart-{}", current_utc_micros()),
|
||||
format!("vector-engine-{}", current_utc_micros()),
|
||||
image_urls,
|
||||
candidate_count,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let task_id = extract_puzzle_task_id(&payload).ok_or_else(|| {
|
||||
Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"message": "拼图 APIMart 图片生成未返回 task_id 或图片地址",
|
||||
}))
|
||||
})?;
|
||||
|
||||
wait_puzzle_apimart_generated_images(
|
||||
http_client,
|
||||
settings,
|
||||
task_id.as_str(),
|
||||
candidate_count,
|
||||
"拼图 APIMart 图片生成任务失败",
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"message": "拼图 VectorEngine 图片生成未返回图片地址",
|
||||
})),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn build_puzzle_apimart_image_request_body(
|
||||
fn build_puzzle_vector_engine_image_request_body(
|
||||
image_model: PuzzleImageModel,
|
||||
prompt: &str,
|
||||
negative_prompt: &str,
|
||||
@@ -4543,34 +4540,23 @@ fn build_puzzle_apimart_image_request_body(
|
||||
),
|
||||
(
|
||||
"prompt".to_string(),
|
||||
Value::String(build_puzzle_apimart_prompt(prompt, negative_prompt)),
|
||||
Value::String(build_puzzle_vector_engine_prompt(prompt, negative_prompt)),
|
||||
),
|
||||
("n".to_string(), json!(candidate_count.clamp(1, 1))),
|
||||
("official_fallback".to_string(), Value::Bool(true)),
|
||||
("size".to_string(), Value::String(size.to_string())),
|
||||
]);
|
||||
body.insert(
|
||||
"resolution".to_string(),
|
||||
Value::String(
|
||||
match image_model {
|
||||
PuzzleImageModel::Gemini31FlashPreview => PUZZLE_APIMART_GEMINI_RESOLUTION,
|
||||
_ => "1k",
|
||||
}
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
|
||||
if let Some(reference_image) = reference_image
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
body.insert("image_urls".to_string(), json!([reference_image]));
|
||||
body.insert("image".to_string(), json!([reference_image]));
|
||||
}
|
||||
|
||||
Value::Object(body)
|
||||
}
|
||||
|
||||
fn build_puzzle_apimart_prompt(prompt: &str, negative_prompt: &str) -> String {
|
||||
fn build_puzzle_vector_engine_prompt(prompt: &str, negative_prompt: &str) -> String {
|
||||
let prompt = prompt.trim();
|
||||
let negative_prompt = negative_prompt.trim();
|
||||
if negative_prompt.is_empty() {
|
||||
@@ -4580,88 +4566,12 @@ fn build_puzzle_apimart_prompt(prompt: &str, negative_prompt: &str) -> String {
|
||||
format!("{prompt}\n避免:{negative_prompt}")
|
||||
}
|
||||
|
||||
async fn wait_puzzle_apimart_generated_images(
|
||||
http_client: &reqwest::Client,
|
||||
settings: &PuzzleApimartSettings,
|
||||
task_id: &str,
|
||||
candidate_count: u32,
|
||||
failure_message: &str,
|
||||
) -> Result<PuzzleGeneratedImages, AppError> {
|
||||
let deadline = Instant::now() + Duration::from_millis(settings.request_timeout_ms);
|
||||
sleep(Duration::from_secs(10)).await;
|
||||
|
||||
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_puzzle_apimart_request_error(format!(
|
||||
"查询拼图 APIMart 图片生成任务失败:{error}"
|
||||
))
|
||||
})?;
|
||||
let poll_status = poll_response.status();
|
||||
let poll_text = poll_response.text().await.map_err(|error| {
|
||||
map_puzzle_apimart_request_error(format!(
|
||||
"读取拼图 APIMart 图片生成任务响应失败:{error}"
|
||||
))
|
||||
})?;
|
||||
if !poll_status.is_success() {
|
||||
return Err(map_puzzle_apimart_upstream_error(
|
||||
poll_status,
|
||||
poll_text.as_str(),
|
||||
"查询拼图 APIMart 图片生成任务失败",
|
||||
));
|
||||
}
|
||||
|
||||
let poll_payload =
|
||||
parse_puzzle_json_payload(poll_text.as_str(), "解析拼图 APIMart 图片生成任务响应失败")?;
|
||||
let task_status = find_first_puzzle_string_by_key(&poll_payload, "status")
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase();
|
||||
if matches!(task_status.as_str(), "completed" | "succeeded" | "success") {
|
||||
let image_urls = extract_puzzle_image_urls(&poll_payload);
|
||||
if image_urls.is_empty() {
|
||||
return Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"message": "拼图 APIMart 图片生成成功但未返回图片地址",
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
return download_puzzle_images_from_urls(
|
||||
http_client,
|
||||
task_id.to_string(),
|
||||
image_urls,
|
||||
candidate_count,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
if matches!(
|
||||
task_status.as_str(),
|
||||
"failed" | "error" | "canceled" | "cancelled"
|
||||
) {
|
||||
return Err(map_puzzle_apimart_upstream_error(
|
||||
poll_status,
|
||||
poll_text.as_str(),
|
||||
failure_message,
|
||||
));
|
||||
}
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
fn puzzle_vector_engine_images_generation_url(settings: &PuzzleVectorEngineSettings) -> String {
|
||||
if settings.base_url.ends_with("/v1") {
|
||||
format!("{}/images/generations", settings.base_url)
|
||||
} else {
|
||||
format!("{}/v1/images/generations", settings.base_url)
|
||||
}
|
||||
|
||||
Err(
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"message": "拼图 APIMart 图片生成超时或未返回图片地址",
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
async fn download_puzzle_images_from_urls(
|
||||
@@ -4965,7 +4875,7 @@ fn build_puzzle_asset_metadata(
|
||||
fn parse_puzzle_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": "apimart",
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"message": format!("{fallback_message}:{error}"),
|
||||
}))
|
||||
})
|
||||
@@ -5011,10 +4921,6 @@ fn decode_puzzle_base64(value: &str) -> Option<Vec<u8>> {
|
||||
Some(output)
|
||||
}
|
||||
|
||||
fn extract_puzzle_task_id(payload: &Value) -> Option<String> {
|
||||
find_first_puzzle_string_by_key(payload, "task_id")
|
||||
}
|
||||
|
||||
fn extract_puzzle_image_urls(payload: &Value) -> Vec<String> {
|
||||
let mut urls = Vec::new();
|
||||
collect_puzzle_strings_by_key(payload, "image", &mut urls);
|
||||
@@ -5095,14 +5001,14 @@ fn map_puzzle_image_request_error(message: String) -> AppError {
|
||||
}))
|
||||
}
|
||||
|
||||
fn map_puzzle_apimart_request_error(message: String) -> AppError {
|
||||
fn map_puzzle_vector_engine_request_error(message: String) -> AppError {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"message": message,
|
||||
}))
|
||||
}
|
||||
|
||||
fn map_puzzle_apimart_upstream_error(
|
||||
fn map_puzzle_vector_engine_upstream_error(
|
||||
upstream_status: reqwest::StatusCode,
|
||||
raw_text: &str,
|
||||
fallback_message: &str,
|
||||
@@ -5110,15 +5016,15 @@ fn map_puzzle_apimart_upstream_error(
|
||||
let message = parse_puzzle_api_error_message(raw_text, fallback_message);
|
||||
let raw_excerpt = trim_puzzle_upstream_excerpt(raw_text, 800);
|
||||
tracing::warn!(
|
||||
provider = "apimart",
|
||||
provider = VECTOR_ENGINE_PROVIDER,
|
||||
upstream_status = upstream_status.as_u16(),
|
||||
message = %message,
|
||||
raw_excerpt = %raw_excerpt,
|
||||
"拼图 APIMart 上游请求失败"
|
||||
"拼图 VectorEngine 上游请求失败"
|
||||
);
|
||||
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"provider": VECTOR_ENGINE_PROVIDER,
|
||||
"upstreamStatus": upstream_status.as_u16(),
|
||||
"message": message,
|
||||
"rawExcerpt": raw_excerpt,
|
||||
|
||||
@@ -1517,7 +1517,7 @@ async fn generate_square_hole_image_data_url(
|
||||
.await?;
|
||||
let image = generated.images.into_iter().next().ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||||
"provider": "apimart",
|
||||
"provider": "vector-engine",
|
||||
"message": format!("{failure_context}:上游未返回图片"),
|
||||
}))
|
||||
})?;
|
||||
|
||||
Reference in New Issue
Block a user