1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use axum::{
|
||||
@@ -912,7 +912,11 @@ pub async fn execute_puzzle_agent_action(
|
||||
let generated_level_name = target_level.level_name.clone();
|
||||
let levels_json_with_generated_name =
|
||||
Some(serialize_puzzle_level_records_for_module(
|
||||
&build_puzzle_levels_with_primary_name(&draft, &target_level),
|
||||
&build_puzzle_levels_with_primary_update(
|
||||
&draft,
|
||||
&target_level,
|
||||
payload.reference_image_src.as_deref(),
|
||||
),
|
||||
)?);
|
||||
let candidates_json = serde_json::to_string(
|
||||
&candidates
|
||||
@@ -962,6 +966,7 @@ pub async fn execute_puzzle_agent_action(
|
||||
),
|
||||
target_level.level_id.as_str(),
|
||||
candidates.into_records(),
|
||||
payload.reference_image_src.as_deref(),
|
||||
now,
|
||||
))
|
||||
}
|
||||
@@ -3081,9 +3086,10 @@ fn build_fallback_puzzle_first_level_name(picture_description: &str) -> String {
|
||||
"奇境初见".to_string()
|
||||
}
|
||||
|
||||
fn build_puzzle_levels_with_primary_name(
|
||||
fn build_puzzle_levels_with_primary_update(
|
||||
draft: &PuzzleResultDraftRecord,
|
||||
target_level: &PuzzleDraftLevelRecord,
|
||||
picture_reference: Option<&str>,
|
||||
) -> Vec<PuzzleDraftLevelRecord> {
|
||||
let mut levels = draft.levels.clone();
|
||||
if let Some(index) = levels
|
||||
@@ -3092,6 +3098,11 @@ fn build_puzzle_levels_with_primary_name(
|
||||
.or_else(|| (!levels.is_empty()).then_some(0))
|
||||
{
|
||||
levels[index].level_name = target_level.level_name.clone();
|
||||
if let Some(picture_reference) =
|
||||
picture_reference.map(str::trim).filter(|value| !value.is_empty())
|
||||
{
|
||||
levels[index].picture_reference = Some(picture_reference.to_string());
|
||||
}
|
||||
}
|
||||
levels
|
||||
}
|
||||
@@ -3161,7 +3172,7 @@ async fn compile_puzzle_draft_with_initial_cover(
|
||||
}
|
||||
let generated_level_name = target_level.level_name.clone();
|
||||
let levels_json_with_generated_name = Some(serialize_puzzle_level_records_for_module(
|
||||
&build_puzzle_levels_with_primary_name(&draft, &target_level),
|
||||
&build_puzzle_levels_with_primary_update(&draft, &target_level, reference_image_src),
|
||||
)?);
|
||||
let candidates_json = serde_json::to_string(
|
||||
&candidates
|
||||
@@ -3208,6 +3219,7 @@ async fn compile_puzzle_draft_with_initial_cover(
|
||||
),
|
||||
target_level.level_id.as_str(),
|
||||
candidates.into_records(),
|
||||
reference_image_src,
|
||||
now,
|
||||
);
|
||||
Ok((session, true))
|
||||
@@ -3311,7 +3323,7 @@ async fn compile_puzzle_draft_with_uploaded_cover(
|
||||
}
|
||||
let generated_level_name = target_level.level_name.clone();
|
||||
let levels_json_with_generated_name = Some(serialize_puzzle_level_records_for_module(
|
||||
&build_puzzle_levels_with_primary_name(&draft, &target_level),
|
||||
&build_puzzle_levels_with_primary_update(&draft, &target_level, reference_image_src),
|
||||
)?);
|
||||
let persisted_upload = persist_puzzle_generated_asset(
|
||||
state,
|
||||
@@ -3374,6 +3386,7 @@ async fn compile_puzzle_draft_with_uploaded_cover(
|
||||
),
|
||||
target_level.level_id.as_str(),
|
||||
vec![candidate.clone()],
|
||||
reference_image_src,
|
||||
now,
|
||||
);
|
||||
Ok((session, true))
|
||||
@@ -3413,6 +3426,7 @@ fn apply_generated_puzzle_candidates_to_session_snapshot(
|
||||
mut session: PuzzleAgentSessionRecord,
|
||||
target_level_id: &str,
|
||||
candidates: Vec<PuzzleGeneratedImageCandidateRecord>,
|
||||
picture_reference: Option<&str>,
|
||||
updated_at_micros: i64,
|
||||
) -> PuzzleAgentSessionRecord {
|
||||
let Some(draft) = session.draft.as_mut() else {
|
||||
@@ -3443,6 +3457,12 @@ fn apply_generated_puzzle_candidates_to_session_snapshot(
|
||||
level.selected_candidate_id = Some(selected.candidate_id.clone());
|
||||
level.cover_image_src = Some(selected.image_src.clone());
|
||||
level.cover_asset_id = Some(selected.asset_id.clone());
|
||||
if let Some(picture_reference) = picture_reference
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
level.picture_reference = Some(picture_reference.to_string());
|
||||
}
|
||||
level.generation_status = "ready".to_string();
|
||||
if target_index == 0 {
|
||||
sync_puzzle_primary_draft_fields_from_level(draft);
|
||||
@@ -3892,9 +3912,13 @@ fn is_missing_puzzle_form_draft_procedure_error(error: &SpacetimeClientError) ->
|
||||
|
||||
fn map_puzzle_compile_error(error: SpacetimeClientError) -> AppError {
|
||||
let message = error.to_string();
|
||||
// 中文注释:历史运行态或旧 SpacetimeDB 错误快照可能仍带 APIMart 图片网关文案;当前 GPT-image-2 已统一迁移到 VectorEngine,返回给前端前先归一,避免误导排障。
|
||||
let is_legacy_apimart_image_error =
|
||||
message.contains("APIMart") || message.contains("apimart") || message.contains("APIMART");
|
||||
let provider = if message.contains("VectorEngine")
|
||||
|| message.contains("vector-engine")
|
||||
|| message.contains("VECTOR_ENGINE")
|
||||
|| is_legacy_apimart_image_error
|
||||
{
|
||||
VECTOR_ENGINE_PROVIDER
|
||||
} else if message.contains("OSS") || message.contains("oss") || message.contains("参考图") {
|
||||
@@ -3905,6 +3929,8 @@ 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
|
||||
@@ -3920,6 +3946,7 @@ fn map_puzzle_compile_error(error: SpacetimeClientError) -> AppError {
|
||||
|| message.contains("VectorEngine")
|
||||
|| message.contains("vector-engine")
|
||||
|| message.contains("VECTOR_ENGINE")
|
||||
|| is_legacy_apimart_image_error
|
||||
|| message.contains("参考图")
|
||||
|| message.contains("图片")
|
||||
|| message.contains("OSS")
|
||||
@@ -3949,13 +3976,28 @@ fn map_puzzle_compile_error(error: SpacetimeClientError) -> AppError {
|
||||
_ => StatusCode::BAD_GATEWAY,
|
||||
}
|
||||
};
|
||||
let user_message = normalize_legacy_puzzle_image_error_message(message.as_str());
|
||||
|
||||
AppError::from_status(status).with_details(json!({
|
||||
"provider": provider,
|
||||
"message": message,
|
||||
"message": user_message,
|
||||
}))
|
||||
}
|
||||
|
||||
fn normalize_legacy_puzzle_image_error_message(message: &str) -> String {
|
||||
message
|
||||
.replace(
|
||||
"APIMart 图片生成密钥未配置",
|
||||
"VectorEngine 图片生成密钥未配置",
|
||||
)
|
||||
.replace(
|
||||
"APIMart 图片生成地址未配置",
|
||||
"VectorEngine 图片生成地址未配置",
|
||||
)
|
||||
.replace("APIMART_API_KEY", "VECTOR_ENGINE_API_KEY")
|
||||
.replace("APIMART_BASE_URL", "VECTOR_ENGINE_BASE_URL")
|
||||
}
|
||||
|
||||
fn puzzle_error_response(
|
||||
request_context: &RequestContext,
|
||||
provider: &str,
|
||||
@@ -4021,10 +4063,15 @@ async fn generate_puzzle_image_candidates(
|
||||
candidate_count: u32,
|
||||
candidate_start_index: usize,
|
||||
) -> Result<Vec<GeneratedPuzzleImageCandidate>, AppError> {
|
||||
let total_started_at = Instant::now();
|
||||
let count = candidate_count.clamp(1, 1);
|
||||
let resolved_model = resolve_puzzle_image_model(image_model);
|
||||
let actual_prompt = build_puzzle_image_prompt(level_name, prompt);
|
||||
let http_client = build_puzzle_image_http_client(state, resolved_model)?;
|
||||
let has_reference_image = reference_image_src
|
||||
.map(str::trim)
|
||||
.map(|value| !value.is_empty())
|
||||
.unwrap_or(false);
|
||||
tracing::info!(
|
||||
provider = resolved_model.provider_name(),
|
||||
image_model = resolved_model.request_model_name(),
|
||||
@@ -4032,24 +4079,45 @@ async fn generate_puzzle_image_candidates(
|
||||
level_name,
|
||||
prompt_chars = prompt.chars().count(),
|
||||
actual_prompt_chars = actual_prompt.chars().count(),
|
||||
has_reference_image = reference_image_src
|
||||
.map(str::trim)
|
||||
.map(|value| !value.is_empty())
|
||||
.unwrap_or(false),
|
||||
has_reference_image,
|
||||
"拼图图片生成请求已准备"
|
||||
);
|
||||
let reference_image_started_at = Instant::now();
|
||||
let reference_image = match reference_image_src
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
Some(source) => {
|
||||
Some(resolve_puzzle_reference_image_as_data_url(state, &http_client, source).await?)
|
||||
let resolved =
|
||||
resolve_puzzle_reference_image_as_data_url(state, &http_client, source).await?;
|
||||
tracing::info!(
|
||||
provider = resolved_model.provider_name(),
|
||||
image_model = resolved_model.request_model_name(),
|
||||
session_id,
|
||||
level_name,
|
||||
reference_mime = %resolved.mime_type,
|
||||
reference_bytes = resolved.bytes_len,
|
||||
elapsed_ms = reference_image_started_at.elapsed().as_millis() as u64,
|
||||
"拼图参考图解析完成"
|
||||
);
|
||||
Some(resolved)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if !has_reference_image {
|
||||
tracing::info!(
|
||||
provider = resolved_model.provider_name(),
|
||||
image_model = resolved_model.request_model_name(),
|
||||
session_id,
|
||||
level_name,
|
||||
elapsed_ms = reference_image_started_at.elapsed().as_millis() as u64,
|
||||
"拼图参考图解析跳过"
|
||||
);
|
||||
}
|
||||
// 中文注释:SpacetimeDB reducer 不能做外部 I/O,参考图读取与外部生图都必须停留在 api-server。
|
||||
// 中文注释:拼图作品资产统一按 1:1 正方形生成,前端运行时也按正方形棋盘切块承载。
|
||||
let settings = require_puzzle_vector_engine_settings(state)?;
|
||||
let vector_engine_started_at = Instant::now();
|
||||
let generated = create_puzzle_vector_engine_image_generation(
|
||||
&http_client,
|
||||
&settings,
|
||||
@@ -4058,10 +4126,21 @@ async fn generate_puzzle_image_candidates(
|
||||
PUZZLE_DEFAULT_NEGATIVE_PROMPT,
|
||||
PUZZLE_VECTOR_ENGINE_GENERATED_IMAGE_SIZE,
|
||||
count,
|
||||
reference_image.as_deref(),
|
||||
reference_image
|
||||
.as_ref()
|
||||
.map(|image| image.data_url.as_str()),
|
||||
)
|
||||
.await
|
||||
.map_err(map_puzzle_generation_endpoint_error)?;
|
||||
tracing::info!(
|
||||
provider = resolved_model.provider_name(),
|
||||
image_model = resolved_model.request_model_name(),
|
||||
session_id,
|
||||
level_name,
|
||||
generated_image_count = generated.images.len(),
|
||||
elapsed_ms = vector_engine_started_at.elapsed().as_millis() as u64,
|
||||
"拼图 VectorEngine 生图与下载完成"
|
||||
);
|
||||
let mut items = Vec::with_capacity(generated.images.len());
|
||||
|
||||
for (index, image) in generated.images.into_iter().enumerate() {
|
||||
@@ -4070,6 +4149,7 @@ async fn generate_puzzle_image_candidates(
|
||||
candidate_start_index + index + 1
|
||||
);
|
||||
let downloaded_image = image.clone();
|
||||
let persist_started_at = Instant::now();
|
||||
let asset = persist_puzzle_generated_asset(
|
||||
state,
|
||||
owner_user_id,
|
||||
@@ -4082,6 +4162,17 @@ async fn generate_puzzle_image_candidates(
|
||||
)
|
||||
.await
|
||||
.map_err(map_puzzle_generation_endpoint_error)?;
|
||||
tracing::info!(
|
||||
provider = resolved_model.provider_name(),
|
||||
image_model = resolved_model.request_model_name(),
|
||||
session_id,
|
||||
level_name,
|
||||
candidate_id = %candidate_id,
|
||||
image_bytes = downloaded_image.bytes.len(),
|
||||
image_mime = %downloaded_image.mime_type,
|
||||
elapsed_ms = persist_started_at.elapsed().as_millis() as u64,
|
||||
"拼图生成图片已写入 OSS 与资产索引"
|
||||
);
|
||||
items.push(GeneratedPuzzleImageCandidate {
|
||||
record: PuzzleGeneratedImageCandidateRecord {
|
||||
candidate_id,
|
||||
@@ -4097,6 +4188,16 @@ async fn generate_puzzle_image_candidates(
|
||||
});
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
provider = resolved_model.provider_name(),
|
||||
image_model = resolved_model.request_model_name(),
|
||||
session_id,
|
||||
level_name,
|
||||
candidate_count = items.len(),
|
||||
has_reference_image,
|
||||
elapsed_ms = total_started_at.elapsed().as_millis() as u64,
|
||||
"拼图图片候选生成完成"
|
||||
);
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
@@ -4144,6 +4245,30 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn puzzle_compile_error_normalizes_legacy_apimart_image_message() {
|
||||
let error = map_puzzle_compile_error(SpacetimeClientError::Runtime(
|
||||
"APIMart 图片生成密钥未配置".to_string(),
|
||||
));
|
||||
|
||||
let response = error.into_response();
|
||||
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
|
||||
let body = response.into_body();
|
||||
let bytes = axum::body::to_bytes(body, usize::MAX)
|
||||
.await
|
||||
.expect("body bytes should read");
|
||||
let payload: Value =
|
||||
serde_json::from_slice(&bytes).expect("error response should be valid json");
|
||||
assert_eq!(
|
||||
payload["error"]["details"]["provider"],
|
||||
Value::String(VECTOR_ENGINE_PROVIDER.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
payload["error"]["details"]["message"],
|
||||
Value::String("VectorEngine 图片生成密钥未配置".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_image_generation_builds_fallback_session_from_levels_snapshot() {
|
||||
let levels_json = serde_json::to_string(&vec![json!({
|
||||
@@ -4293,6 +4418,116 @@ mod tests {
|
||||
assert_eq!(draft.levels[0].level_name, "雨夜猫街");
|
||||
}
|
||||
|
||||
fn test_puzzle_anchor_pack_record() -> PuzzleAnchorPackRecord {
|
||||
let item = PuzzleAnchorItemRecord {
|
||||
key: "visualSubject".to_string(),
|
||||
label: "画面".to_string(),
|
||||
value: "雨夜猫街".to_string(),
|
||||
status: "confirmed".to_string(),
|
||||
};
|
||||
|
||||
PuzzleAnchorPackRecord {
|
||||
theme_promise: item.clone(),
|
||||
visual_subject: item.clone(),
|
||||
visual_mood: item.clone(),
|
||||
composition_hooks: item.clone(),
|
||||
tags_and_forbidden: item,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_puzzle_draft_record() -> PuzzleResultDraftRecord {
|
||||
let anchor_pack = test_puzzle_anchor_pack_record();
|
||||
PuzzleResultDraftRecord {
|
||||
work_title: "雨夜猫街".to_string(),
|
||||
work_description: "一只猫在雨夜灯牌下回头。".to_string(),
|
||||
level_name: "猫画面".to_string(),
|
||||
summary: "一只猫在雨夜灯牌下回头。".to_string(),
|
||||
theme_tags: vec![],
|
||||
forbidden_directives: vec![],
|
||||
creator_intent: None,
|
||||
anchor_pack,
|
||||
candidates: vec![],
|
||||
selected_candidate_id: None,
|
||||
cover_image_src: None,
|
||||
cover_asset_id: None,
|
||||
generation_status: "idle".to_string(),
|
||||
levels: vec![PuzzleDraftLevelRecord {
|
||||
level_id: "puzzle-level-1".to_string(),
|
||||
level_name: "猫画面".to_string(),
|
||||
picture_description: "一只猫在雨夜灯牌下回头。".to_string(),
|
||||
picture_reference: None,
|
||||
candidates: vec![],
|
||||
selected_candidate_id: None,
|
||||
cover_image_src: None,
|
||||
cover_asset_id: None,
|
||||
generation_status: "idle".to_string(),
|
||||
}],
|
||||
form_draft: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_primary_level_update_preserves_reference_for_regeneration() {
|
||||
let draft = test_puzzle_draft_record();
|
||||
let mut target_level = draft.levels[0].clone();
|
||||
target_level.level_name = "雨夜猫街".to_string();
|
||||
|
||||
let levels = build_puzzle_levels_with_primary_update(
|
||||
&draft,
|
||||
&target_level,
|
||||
Some("data:image/png;base64,abcd"),
|
||||
);
|
||||
|
||||
assert_eq!(levels[0].level_name, "雨夜猫街");
|
||||
assert_eq!(
|
||||
levels[0].picture_reference.as_deref(),
|
||||
Some("data:image/png;base64,abcd")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puzzle_generated_fallback_snapshot_preserves_picture_reference() {
|
||||
let anchor_pack = test_puzzle_anchor_pack_record();
|
||||
let session = PuzzleAgentSessionRecord {
|
||||
session_id: "puzzle-session-1".to_string(),
|
||||
seed_text: "雨夜猫街".to_string(),
|
||||
current_turn: 1,
|
||||
progress_percent: 0,
|
||||
stage: "draft_ready".to_string(),
|
||||
anchor_pack: anchor_pack.clone(),
|
||||
draft: Some(test_puzzle_draft_record()),
|
||||
messages: Vec::new(),
|
||||
last_assistant_reply: None,
|
||||
published_profile_id: None,
|
||||
suggested_actions: Vec::new(),
|
||||
result_preview: None,
|
||||
updated_at: "2024-01-01T00:00:00Z".to_string(),
|
||||
};
|
||||
let candidate = PuzzleGeneratedImageCandidateRecord {
|
||||
candidate_id: "puzzle-session-1-candidate-1".to_string(),
|
||||
image_src: "/generated-puzzle-assets/puzzle-session-1/1/cover.png".to_string(),
|
||||
asset_id: "puzzle-cover-1".to_string(),
|
||||
prompt: "雨夜猫街".to_string(),
|
||||
actual_prompt: Some("雨夜猫街".to_string()),
|
||||
source_type: "generated:gpt-image-2".to_string(),
|
||||
selected: true,
|
||||
};
|
||||
|
||||
let session = apply_generated_puzzle_candidates_to_session_snapshot(
|
||||
session,
|
||||
"puzzle-level-1",
|
||||
vec![candidate],
|
||||
Some("data:image/png;base64,abcd"),
|
||||
1_713_686_401_234_568,
|
||||
);
|
||||
|
||||
let draft = session.draft.expect("draft");
|
||||
assert_eq!(
|
||||
draft.levels[0].picture_reference.as_deref(),
|
||||
Some("data:image/png;base64,abcd")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freeze_boundary_sync_only_matches_freeze_invalid_operation() {
|
||||
let invalid_operation =
|
||||
@@ -4347,6 +4582,12 @@ struct PuzzleGeneratedImages {
|
||||
images: Vec<PuzzleDownloadedImage>,
|
||||
}
|
||||
|
||||
struct PuzzleResolvedReferenceImage {
|
||||
data_url: String,
|
||||
mime_type: String,
|
||||
bytes_len: usize,
|
||||
}
|
||||
|
||||
struct GeneratedPuzzleImageCandidate {
|
||||
record: PuzzleGeneratedImageCandidateRecord,
|
||||
downloaded_image: PuzzleDownloadedImage,
|
||||
@@ -4490,8 +4731,14 @@ async fn create_puzzle_vector_engine_image_generation(
|
||||
candidate_count,
|
||||
reference_image,
|
||||
);
|
||||
let request_url = puzzle_vector_engine_images_generation_url(settings);
|
||||
let has_reference_image = reference_image
|
||||
.map(str::trim)
|
||||
.map(|value| !value.is_empty())
|
||||
.unwrap_or(false);
|
||||
let request_started_at = Instant::now();
|
||||
let response = http_client
|
||||
.post(puzzle_vector_engine_images_generation_url(settings))
|
||||
.post(request_url.as_str())
|
||||
.header(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
format!("Bearer {}", settings.api_key),
|
||||
@@ -4507,6 +4754,18 @@ async fn create_puzzle_vector_engine_image_generation(
|
||||
))
|
||||
})?;
|
||||
let status = response.status();
|
||||
let upstream_elapsed_ms = request_started_at.elapsed().as_millis() as u64;
|
||||
tracing::info!(
|
||||
provider = VECTOR_ENGINE_PROVIDER,
|
||||
image_model = image_model.request_model_name(),
|
||||
endpoint = %request_url,
|
||||
status = status.as_u16(),
|
||||
prompt_chars = prompt.chars().count(),
|
||||
size,
|
||||
has_reference_image,
|
||||
elapsed_ms = upstream_elapsed_ms,
|
||||
"拼图 VectorEngine 图片生成 HTTP 返回"
|
||||
);
|
||||
let response_text = response.text().await.map_err(|error| {
|
||||
map_puzzle_vector_engine_request_error(format!(
|
||||
"读取拼图 VectorEngine 图片生成响应失败:{error}"
|
||||
@@ -4526,13 +4785,22 @@ async fn create_puzzle_vector_engine_image_generation(
|
||||
)?;
|
||||
let image_urls = extract_puzzle_image_urls(&payload);
|
||||
if !image_urls.is_empty() {
|
||||
return download_puzzle_images_from_urls(
|
||||
let download_started_at = Instant::now();
|
||||
let images = download_puzzle_images_from_urls(
|
||||
http_client,
|
||||
format!("vector-engine-{}", current_utc_micros()),
|
||||
image_urls,
|
||||
candidate_count,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
tracing::info!(
|
||||
provider = VECTOR_ENGINE_PROVIDER,
|
||||
image_model = image_model.request_model_name(),
|
||||
image_count = images.images.len(),
|
||||
elapsed_ms = download_started_at.elapsed().as_millis() as u64,
|
||||
"拼图 VectorEngine 图片下载完成"
|
||||
);
|
||||
return Ok(images);
|
||||
}
|
||||
|
||||
Err(
|
||||
@@ -4612,7 +4880,7 @@ async fn resolve_puzzle_reference_image_as_data_url(
|
||||
state: &AppState,
|
||||
http_client: &reqwest::Client,
|
||||
source: &str,
|
||||
) -> Result<String, AppError> {
|
||||
) -> Result<PuzzleResolvedReferenceImage, AppError> {
|
||||
let trimmed = source.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(
|
||||
@@ -4625,11 +4893,17 @@ async fn resolve_puzzle_reference_image_as_data_url(
|
||||
}
|
||||
|
||||
if let Some(parsed) = parse_puzzle_image_data_url(trimmed) {
|
||||
return Ok(format!(
|
||||
let bytes_len = parsed.bytes.len();
|
||||
let data_url = format!(
|
||||
"data:{};base64,{}",
|
||||
parsed.mime_type,
|
||||
BASE64_STANDARD.encode(parsed.bytes)
|
||||
));
|
||||
BASE64_STANDARD.encode(&parsed.bytes)
|
||||
);
|
||||
return Ok(PuzzleResolvedReferenceImage {
|
||||
data_url,
|
||||
mime_type: parsed.mime_type,
|
||||
bytes_len,
|
||||
});
|
||||
}
|
||||
|
||||
if !trimmed.starts_with('/') {
|
||||
@@ -4699,11 +4973,13 @@ async fn resolve_puzzle_reference_image_as_data_url(
|
||||
);
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"data:{};base64,{}",
|
||||
content_type,
|
||||
BASE64_STANDARD.encode(body)
|
||||
))
|
||||
let mime_type = normalize_puzzle_downloaded_image_mime_type(content_type.as_str());
|
||||
let bytes_len = body.len();
|
||||
Ok(PuzzleResolvedReferenceImage {
|
||||
data_url: format!("data:{};base64,{}", mime_type, BASE64_STANDARD.encode(body)),
|
||||
mime_type,
|
||||
bytes_len,
|
||||
})
|
||||
}
|
||||
|
||||
async fn download_puzzle_remote_image(
|
||||
|
||||
Reference in New Issue
Block a user