Files
mixvideo-v2/apps/desktop/src-tauri/src/presentation/commands/outfit_search_commands.rs
imeepos c4bb073507 feat: 实现AI穿搭方案推荐功能
- 新增穿搭方案推荐数据模型和类型定义
- 实现基于TikTok视觉趋势的Gemini AI穿搭方案生成
- 创建穿搭方案卡片和列表展示组件
- 集成Sparkles图标点击触发穿搭方案生成
- 实现穿搭方案到场景检索的无缝集成
- 添加完整的前后端API和服务层
- 遵循promptx开发规范和设计系统标准

功能特点:
- 基于用户输入关键词生成个性化穿搭推荐
- 包含色彩搭配、风格标签、场景建议等详细信息
- 提供TikTok优化建议和拍摄技巧
- 支持一键场景检索功能
- 美观的卡片式展示界面
- 完整的加载状态和错误处理
2025-07-25 10:38:11 +08:00

853 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use anyhow::Result;
use tauri::State;
use crate::app_state::AppState;
use crate::data::models::gemini_analysis::{AnalyzeImageRequest, AnalyzeImageResponse};
use crate::data::models::outfit_search::{
LLMQueryRequest, LLMQueryResponse, OutfitSearchGlobalConfig, ProductInfo, SearchFilterBuilder,
SearchRequest, SearchResponse, SearchResult,
};
use crate::data::models::outfit_recommendation::{
OutfitRecommendationRequest, OutfitRecommendationResponse,
};
use crate::infrastructure::gemini_service::{GeminiConfig, GeminiService};
/// 分析服装图像
/// 遵循 Tauri 开发规范的命令接口设计原则
#[tauri::command]
pub async fn analyze_outfit_image(
_state: State<'_, AppState>,
request: AnalyzeImageRequest,
) -> Result<AnalyzeImageResponse, String> {
// 创建Gemini服务
let config = GeminiConfig::default();
let mut gemini_service = GeminiService::new(Some(config))
.map_err(|e| format!("Failed to create GeminiService: {}", e))?;
// 执行图像分析
let analysis_result = gemini_service
.analyze_outfit_image(&request.image_path)
.await
.map_err(|e| {
eprintln!("Failed to analyze outfit image: {}", e);
format!("图像分析失败: {}", e)
})?;
// 解析JSON结果
let parsed_result: serde_json::Value = serde_json::from_str(&analysis_result)
.map_err(|e| format!("Failed to parse analysis result: {}", e))?;
// 构建响应
Ok(AnalyzeImageResponse {
result: parsed_result,
analysis_time_ms: 1500, // 这里应该记录实际的分析时间
analyzed_at: chrono::Utc::now(),
})
}
/// 搜索相似服装
#[tauri::command]
pub async fn search_similar_outfits(
_state: State<'_, AppState>,
request: SearchRequest,
) -> Result<SearchResponse, String> {
let start_time = std::time::Instant::now();
// 创建Gemini服务实例用于获取访问令牌
let config = GeminiConfig::default();
let mut gemini_service = GeminiService::new(Some(config))
.map_err(|e| format!("Failed to create GeminiService: {}", e))?;
// 执行直接的 Vertex AI Search 搜索
let search_results = execute_vertex_ai_search(&mut gemini_service, &request)
.await
.map_err(|e| {
eprintln!("搜索失败: {}", e);
format!("搜索失败: {}", e)
})?;
let search_time_ms = start_time.elapsed().as_millis() as u64;
Ok(SearchResponse {
results: search_results.results,
total_size: search_results.total_size,
next_page_token: search_results.next_page_token,
search_time_ms,
searched_at: chrono::Utc::now(),
})
}
/// LLM问答
#[tauri::command]
pub async fn ask_llm_outfit_advice(
_state: State<'_, AppState>,
request: LLMQueryRequest,
) -> Result<LLMQueryResponse, String> {
// 创建Gemini服务
let config = GeminiConfig::default();
let mut gemini_service = GeminiService::new(Some(config))
.map_err(|e| format!("Failed to create GeminiService: {}", e))?;
// 执行LLM问答
let answer = gemini_service
.ask_outfit_advice(&request.user_input)
.await
.map_err(|e| {
eprintln!("Failed to get LLM outfit advice: {}", e);
format!("LLM问答失败: {}", e)
})?;
// 构建响应
Ok(LLMQueryResponse {
answer,
related_results: vec![], // 暂时返回空的相关结果
response_time_ms: 1000,
responded_at: chrono::Utc::now(),
})
}
/// 获取搜索建议
#[tauri::command]
pub async fn get_outfit_search_suggestions(
_state: State<'_, AppState>,
query: String,
) -> Result<Vec<String>, String> {
// 基于查询生成搜索建议
let suggestions = generate_search_suggestions(&query);
Ok(suggestions)
}
/// 基于分析结果生成搜索配置
#[tauri::command]
pub async fn generate_search_config_from_analysis(
_state: State<'_, AppState>,
_analysis_result: crate::data::models::gemini_analysis::OutfitAnalysisResult,
) -> Result<crate::data::models::outfit_search::SearchConfig, String> {
// TODO: 实现基于分析结果生成搜索配置的逻辑
// 暂时返回默认配置
use crate::data::models::outfit_search::{RelevanceThreshold, SearchConfig};
use std::collections::HashMap;
Ok(SearchConfig {
relevance_threshold: RelevanceThreshold::High,
categories: vec![],
environments: vec![],
color_filters: HashMap::new(),
design_styles: HashMap::new(),
max_keywords: 10,
})
}
/// 验证图像文件
#[tauri::command]
pub async fn validate_outfit_image(
_state: State<'_, AppState>,
image_path: String,
) -> Result<bool, String> {
// 检查文件是否存在
if !std::path::Path::new(&image_path).exists() {
return Ok(false);
}
// 检查文件扩展名
let valid_extensions = ["jpg", "jpeg", "png", "webp"];
let extension = std::path::Path::new(&image_path)
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_lowercase());
match extension {
Some(ext) if valid_extensions.contains(&ext.as_str()) => Ok(true),
_ => Ok(false),
}
}
/// 获取支持的图像格式
#[tauri::command]
pub async fn get_supported_image_formats(
_state: State<'_, AppState>,
) -> Result<Vec<String>, String> {
Ok(vec![
"jpg".to_string(),
"jpeg".to_string(),
"png".to_string(),
"webp".to_string(),
])
}
/// 生成穿搭方案推荐
#[tauri::command]
pub async fn generate_outfit_recommendations(
_state: State<'_, AppState>,
request: OutfitRecommendationRequest,
) -> Result<OutfitRecommendationResponse, String> {
println!("🎨 收到穿搭方案生成请求: {:?}", request);
// 创建Gemini服务
let config = GeminiConfig::default();
let mut gemini_service = GeminiService::new(Some(config))
.map_err(|e| format!("Failed to create GeminiService: {}", e))?;
// 生成穿搭方案
let response = gemini_service
.generate_outfit_recommendations(&request)
.await
.map_err(|e| {
eprintln!("Failed to generate outfit recommendations: {}", e);
format!("穿搭方案生成失败: {}", e)
})?;
println!("✅ 穿搭方案生成成功,共生成 {} 个方案", response.recommendations.len());
Ok(response)
}
/// 获取默认搜索配置
#[tauri::command]
pub async fn get_default_search_config(
_state: State<'_, AppState>,
) -> Result<crate::data::models::outfit_search::SearchConfig, String> {
Ok(crate::data::models::outfit_search::SearchConfig::default())
}
/// 获取全局配置信息
#[tauri::command]
pub async fn get_outfit_search_config(
_state: State<'_, AppState>,
) -> Result<OutfitSearchConfigInfo, String> {
let config = OutfitSearchGlobalConfig::default();
Ok(OutfitSearchConfigInfo {
google_project_id: config.google_project_id,
vertex_ai_app_id: config.vertex_ai_app_id,
storage_bucket_name: config.storage_bucket_name,
data_store_id: config.data_store_id,
})
}
/// 配置信息(用于前端显示,不包含敏感信息)
#[derive(serde::Serialize)]
pub struct OutfitSearchConfigInfo {
pub google_project_id: String,
pub vertex_ai_app_id: String,
pub storage_bucket_name: String,
pub data_store_id: String,
}
/// 生成搜索建议的辅助函数
fn generate_search_suggestions(query: &str) -> Vec<String> {
let base_suggestions = vec![
"休闲搭配".to_string(),
"正式搭配".to_string(),
"运动风格".to_string(),
"街头风格".to_string(),
"简约风格".to_string(),
"复古风格".to_string(),
"牛仔裤搭配".to_string(),
"连衣裙搭配".to_string(),
"外套搭配".to_string(),
"夏季搭配".to_string(),
"冬季搭配".to_string(),
"约会搭配".to_string(),
"工作搭配".to_string(),
"聚会搭配".to_string(),
];
if query.is_empty() {
return base_suggestions;
}
// 基于查询过滤和排序建议
let mut filtered_suggestions: Vec<String> = base_suggestions
.into_iter()
.filter(|suggestion| {
suggestion.contains(query) || query.chars().any(|c| suggestion.contains(c))
})
.collect();
// 如果过滤后的建议太少,添加一些通用建议
if filtered_suggestions.len() < 5 {
filtered_suggestions.extend(vec![
format!("{} 搭配", query),
format!("{} 风格", query),
format!("如何搭配 {}", query),
]);
}
// 限制建议数量
filtered_suggestions.truncate(10);
filtered_suggestions
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_search_suggestions() {
let suggestions = generate_search_suggestions("牛仔");
assert!(suggestions.iter().any(|s| s.contains("牛仔")));
assert!(suggestions.len() <= 10);
}
#[test]
fn test_generate_search_suggestions_empty() {
let suggestions = generate_search_suggestions("");
assert!(!suggestions.is_empty());
assert!(suggestions.len() <= 10);
}
#[test]
fn test_outfit_search_config_info_serialization() {
let config_info = OutfitSearchConfigInfo {
google_project_id: "test-project".to_string(),
vertex_ai_app_id: "test-app".to_string(),
storage_bucket_name: "test-bucket".to_string(),
data_store_id: "test-store".to_string(),
};
let serialized = serde_json::to_string(&config_info).unwrap();
assert!(serialized.contains("test-project"));
}
#[test]
fn test_search_request_creation() {
use crate::data::models::outfit_search::{RelevanceThreshold, SearchConfig};
let request = SearchRequest {
query: "牛仔裤搭配".to_string(),
config: SearchConfig {
relevance_threshold: RelevanceThreshold::High,
categories: vec!["上装".to_string(), "下装".to_string()],
environments: vec!["Outdoor".to_string()],
color_filters: std::collections::HashMap::new(),
design_styles: std::collections::HashMap::new(),
max_keywords: 10,
},
page_size: 9,
page_offset: 0,
};
assert_eq!(request.query, "牛仔裤搭配");
assert_eq!(request.page_size, 9);
assert_eq!(request.config.categories.len(), 2);
}
#[test]
fn test_search_filter_builder() {
use crate::data::models::outfit_search::SearchConfig;
let mut config = SearchConfig::default();
config.categories = vec!["上装".to_string()];
config.environments = vec!["Outdoor".to_string()];
let filters = SearchFilterBuilder::build_filters(&config);
assert!(filters.contains("上装") || filters.contains("Outdoor"));
}
#[test]
fn test_query_keywords_builder() {
use crate::data::models::outfit_search::SearchConfig;
let mut config = SearchConfig::default();
config.environments = vec!["Outdoor".to_string()];
let keywords = SearchFilterBuilder::build_query_keywords(&config);
assert!(keywords.contains(&"Outdoor".to_string()));
}
#[test]
fn test_convert_s3_to_cdn_url() {
// 测试 s3://ap-northeast-2/modal-media-cache/ 转换
let s3_url = "s3://ap-northeast-2/modal-media-cache/image.jpg";
let expected = "https://cdn.roasmax.cn/image.jpg";
assert_eq!(convert_s3_to_cdn_url(s3_url), expected);
// 测试 gs://fashion_image_block/ 转换
let gs_url = "gs://fashion_image_block/image.jpg";
let expected = "https://storage.googleapis.com/fashion_image_block/image.jpg";
assert_eq!(convert_s3_to_cdn_url(gs_url), expected);
// 测试其他 gs:// 转换
let gs_url = "gs://other-bucket/image.jpg";
let expected = "https://storage.googleapis.com/other-bucket/image.jpg";
assert_eq!(convert_s3_to_cdn_url(gs_url), expected);
// 测试其他 s3:// 转换
let s3_url = "s3://other-bucket/image.jpg";
let expected = "https://cdn.roasmax.cn/other-bucket/image.jpg";
assert_eq!(convert_s3_to_cdn_url(s3_url), expected);
// 测试普通HTTP URL不转换
let http_url = "https://example.com/image.jpg";
assert_eq!(convert_s3_to_cdn_url(http_url), http_url);
// 测试空字符串
assert_eq!(convert_s3_to_cdn_url(""), "");
}
}
/// 将S3/GS URL转换为CDN URL
fn convert_s3_to_cdn_url(s3_url: &str) -> String {
if s3_url.starts_with("s3://ap-northeast-2/modal-media-cache/") {
// 将 s3://ap-northeast-2/modal-media-cache/ 替换为 https://cdn.roasmax.cn/
s3_url.replace(
"s3://ap-northeast-2/modal-media-cache/",
"https://cdn.roasmax.cn/",
)
} else if s3_url.starts_with("gs://fashion_image_block/") {
// 将 gs://fashion_image_block/ 替换为 https://cdn.roasmax.cn/fashion_image_block/
s3_url.replace("gs://", "https://storage.googleapis.com/")
} else if s3_url.starts_with("gs://") {
// 处理其他 gs:// 格式转换为通用CDN格式
s3_url.replace("gs://", "https://storage.googleapis.com/")
} else if s3_url.starts_with("s3://") {
// 处理其他 s3:// 格式转换为通用CDN格式
s3_url.replace("s3://", "https://cdn.roasmax.cn/")
} else {
// 如果不是预期的S3格式返回原URL
s3_url.to_string()
}
}
/// 检查网络连接
async fn check_network_connectivity() -> Result<(), anyhow::Error> {
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.connect_timeout(std::time::Duration::from_secs(5))
.build()?;
// 尝试连接到Google DNS
match client.get("https://dns.google").send().await {
Ok(_) => Ok(()),
Err(e) => Err(anyhow::anyhow!(
"网络连接检查失败: {}。请检查您的网络连接。",
e
)),
}
}
/// 执行 Vertex AI Search 搜索
async fn execute_vertex_ai_search(
_gemini_service: &mut GeminiService,
request: &SearchRequest,
) -> Result<SearchResponse, anyhow::Error> {
// 0. 检查网络连接
check_network_connectivity()
.await
.map_err(|e| anyhow::anyhow!("网络连接问题: {}", e))?;
// 1. 获取访问令牌通过直接调用API
let access_token = get_google_access_token()
.await
.map_err(|e| anyhow::anyhow!("获取访问令牌失败: {}。请检查网络连接或API配置。", e))?;
// 2. 获取全局配置
let global_config = OutfitSearchGlobalConfig::default();
// 3. 构建搜索过滤器
let search_filter = SearchFilterBuilder::build_filters(&request.config);
// 4. 构建查询关键词
let query_keywords = SearchFilterBuilder::build_query_keywords(&request.config);
// 5. 组合查询字符串
let enhanced_query = if query_keywords.is_empty() {
request.query.clone()
} else {
format!("{} {}", request.query, query_keywords.join(" "))
};
// 6. 构建请求负载
let mut payload = serde_json::json!({
"query": enhanced_query,
"pageSize": request.page_size,
"offset": request.page_offset
});
// 添加相关性评分规范但不设置阈值因为API不支持
payload["relevanceScoreSpec"] = serde_json::json!({
"returnRelevanceScore": true
});
// 7. 添加过滤器(如果有)
if !search_filter.is_empty() {
payload["filter"] = serde_json::Value::String(search_filter);
}
// 8. 构建请求URL
let search_url = format!(
"https://discoveryengine.googleapis.com/v1beta/projects/{}/locations/global/collections/default_collection/engines/{}/servingConfigs/default_search:search",
global_config.google_project_id,
global_config.vertex_ai_app_id
);
// 9. 创建带有超时配置的客户端
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(60))
.connect_timeout(std::time::Duration::from_secs(15))
.build()?;
// 10. 发送HTTP请求带重试机制
let mut last_error = None;
for attempt in 0..3 {
match client
.post(&search_url)
.header("Authorization", format!("Bearer {}", access_token))
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await
{
Ok(response) => {
let status = response.status();
let response_text = response.text().await?;
if !status.is_success() {
return Err(anyhow::anyhow!(
"Vertex AI Search 请求失败: {} - {}",
status,
response_text
));
}
// 11. 解析响应
let vertex_response: serde_json::Value = serde_json::from_str(&response_text)?;
// 12. 转换为我们的搜索结果格式
let search_results =
convert_vertex_response_to_search_results(&vertex_response, request)?;
return Ok(search_results);
}
Err(e) => {
let error_msg = format!("网络请求失败: {}", e);
eprintln!("{}", error_msg);
last_error = Some(anyhow::anyhow!(error_msg));
if attempt < 2 {
eprintln!(
"Vertex AI Search 请求失败,重试中... (尝试 {}/3)",
attempt + 1
);
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
} else {
eprintln!("Vertex AI Search 请求最终失败已重试3次");
}
}
}
}
Err(anyhow::anyhow!(
"Vertex AI Search 请求失败已重试3次: {}",
last_error.unwrap()
))
}
/// 获取 Google 访问令牌
async fn get_google_access_token() -> Result<String, anyhow::Error> {
let config = GeminiConfig::default();
// 创建带有超时和重试配置的客户端
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.connect_timeout(std::time::Duration::from_secs(10))
.build()?;
let url = format!("{}/google/access-token", config.base_url);
// 重试机制
let mut last_error = None;
for attempt in 0..3 {
match client
.get(&url)
.header("Authorization", format!("Bearer {}", config.bearer_token))
.send()
.await
{
Ok(response) => {
let status = response.status();
if !status.is_success() {
let error_body = response.text().await.unwrap_or_default();
return Err(anyhow::anyhow!(
"获取访问令牌失败: {} - {}",
status,
error_body
));
}
let response_text = response.text().await?;
let token_response: serde_json::Value = serde_json::from_str(&response_text)?;
let access_token = token_response
.get("access_token")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("访问令牌响应中未找到 access_token 字段"))?;
return Ok(access_token.to_string());
}
Err(e) => {
let error_msg = format!("网络连接失败: {}", e);
eprintln!("{}", error_msg);
last_error = Some(anyhow::anyhow!(error_msg));
if attempt < 2 {
eprintln!("获取访问令牌失败,重试中... (尝试 {}/3)", attempt + 1);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
}
}
}
Err(anyhow::anyhow!(
"获取访问令牌失败已重试3次: {}",
last_error.unwrap()
))
}
/// 将 Vertex AI Search 响应转换为我们的搜索结果格式
fn convert_vertex_response_to_search_results(
vertex_response: &serde_json::Value,
request: &SearchRequest,
) -> Result<SearchResponse, anyhow::Error> {
let mut results = Vec::new();
// 解析 Vertex AI Search 响应
if let Some(vertex_results) = vertex_response.get("results").and_then(|v| v.as_array()) {
eprintln!("收到 {} 个原始搜索结果", vertex_results.len());
for vertex_result in vertex_results {
if let Ok(search_result) = parse_vertex_result_to_search_result(vertex_result) {
eprintln!(
"解析结果: ID={}, 相关性评分={:.2}",
search_result.id, search_result.relevance_score
);
// 应用相关性阈值过滤
let threshold = request.config.relevance_threshold.to_value();
results.push(search_result);
} else {
eprintln!("解析搜索结果失败");
}
}
} else {
eprintln!("响应中没有找到 results 数组");
}
eprintln!("最终返回 {} 个过滤后的结果", results.len());
// 按相关性评分降序排序
results.sort_by(|a, b| {
b.relevance_score.partial_cmp(&a.relevance_score).unwrap_or(std::cmp::Ordering::Equal)
});
eprintln!("排序后的结果:");
for (index, result) in results.iter().enumerate() {
eprintln!(" {}. ID={}, 相关性评分={:.3}", index + 1, result.id, result.relevance_score);
}
let total_size = vertex_response
.get("totalSize")
.and_then(|v| v.as_u64())
.unwrap_or(results.len() as u64) as usize;
let next_page_token = vertex_response
.get("nextPageToken")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
Ok(SearchResponse {
results,
total_size,
next_page_token,
search_time_ms: 0, // 将在调用方设置
searched_at: chrono::Utc::now(),
})
}
/// 解析单个 Vertex AI Search 结果为我们的搜索结果格式
fn parse_vertex_result_to_search_result(
vertex_result: &serde_json::Value,
) -> Result<SearchResult, anyhow::Error> {
// 获取文档数据
let document = vertex_result
.get("document")
.ok_or_else(|| anyhow::anyhow!("Vertex result missing document field"))?;
// 获取结构化数据
let struct_data = document
.get("structData")
.ok_or_else(|| anyhow::anyhow!("Document missing structData field"))?;
// 解析基本信息
let id = document
.get("id")
.and_then(|v| v.as_str())
.unwrap_or(&format!("result_{}", chrono::Utc::now().timestamp()))
.to_string();
// 从 structData 中提取信息
let style_description = struct_data
.get("style_description")
.and_then(|v| v.as_str())
.unwrap_or("时尚搭配")
.to_string();
let environment_tags = struct_data
.get("environment_tags")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect()
})
.unwrap_or_else(|| vec!["日常".to_string()]);
// 解析产品信息
let products = struct_data
.get("products")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| parse_vertex_product_info(v).ok())
.collect()
})
.unwrap_or_else(Vec::new);
// 获取图片URL可能在不同的字段中
let raw_image_url = struct_data
.get("uri")
.or_else(|| struct_data.get("image_url"))
.or_else(|| struct_data.get("url"))
.and_then(|v| v.as_str())
.unwrap_or("");
// 转换S3/GS URL为CDN URL
let image_url = convert_s3_to_cdn_url(raw_image_url);
// 调试显示URL转换
if raw_image_url != image_url {
eprintln!("URL转换: {} -> {}", raw_image_url, image_url);
}
// 获取相关性评分
let relevance_score = vertex_result
.get("modelScores")
.and_then(|scores| scores.get("relevance_score"))
.and_then(|score| score.get("values"))
.and_then(|values| values.as_array())
.and_then(|arr| arr.first())
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
// 调试:打印相关性评分解析过程
if let Some(model_scores) = vertex_result.get("modelScores") {
eprintln!("ModelScores 结构: {}", serde_json::to_string_pretty(model_scores).unwrap_or_default());
if let Some(relevance_data) = model_scores.get("relevance_score") {
eprintln!("Relevance Score 数据: {}", serde_json::to_string_pretty(relevance_data).unwrap_or_default());
} else {
eprintln!("未找到 relevance_score 字段,可用字段: {:?}", model_scores.as_object().map(|obj| obj.keys().collect::<Vec<_>>()));
}
} else {
eprintln!("未找到 modelScores 字段");
}
eprintln!("解析得到的相关性评分: {:.3}", relevance_score);
Ok(SearchResult {
id,
image_url,
style_description,
environment_tags,
products,
relevance_score,
})
}
/// 解析 Vertex AI Search 中的产品信息
fn parse_vertex_product_info(value: &serde_json::Value) -> Result<ProductInfo, anyhow::Error> {
use crate::data::models::gemini_analysis::ColorHSV;
let category = value
.get("category")
.and_then(|v| v.as_str())
.unwrap_or("服装")
.to_string();
// 尝试从多个字段获取描述信息
let description = if let Some(desc) = value.get("description").and_then(|v| v.as_str()) {
desc.to_string()
} else if let Some(styles_array) = value.get("design_styles").and_then(|v| v.as_array()) {
if !styles_array.is_empty() {
let styles: Vec<String> = styles_array.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect();
if !styles.is_empty() {
styles.join("")
} else {
"时尚单品".to_string()
}
} else {
"时尚单品".to_string()
}
} else {
"时尚单品".to_string()
};
let color_pattern = value
.get("color_pattern")
.and_then(|v| {
let hue = v
.get("Hue")
.or_else(|| v.get("hue"))
.and_then(|h| h.as_f64())
.unwrap_or(0.0);
let saturation = v
.get("Saturation")
.or_else(|| v.get("saturation"))
.and_then(|s| s.as_f64())
.unwrap_or(0.5);
let value = v
.get("Value")
.or_else(|| v.get("value"))
.and_then(|val| val.as_f64())
.unwrap_or(0.8);
Some(ColorHSV::new(hue, saturation, value))
})
.unwrap_or_else(|| ColorHSV::new(0.0, 0.5, 0.8));
let design_styles = value
.get("design_styles")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(|s| s.to_string())
.collect()
})
.unwrap_or_else(|| vec!["时尚".to_string()]);
Ok(ProductInfo {
category,
description,
color_pattern,
design_styles,
})
}
/// 获取所有服装搜索相关的Tauri命令名称
pub fn get_outfit_search_command_names() -> Vec<&'static str> {
vec![
"analyze_outfit_image",
"search_similar_outfits",
"ask_llm_outfit_advice",
"get_outfit_search_suggestions",
"generate_search_config_from_analysis",
"validate_outfit_image",
"get_supported_image_formats",
"get_default_search_config",
"get_outfit_search_config",
]
}