// src/services/observation/dispatch.rs // // 统一编排入口 —— 把 (坐标/标识符) + (Source, ProductType) 翻译成对 fetcher 的调用 // // 这是 observation 服务的最上层:api/observation.rs 和 agent tools 都通过本模块调用。 // 本模块只负责编排(选源、并发控制、失败聚合),不关心各源 HTTP 细节(那是 fetcher 的职责)。 use crate::api::AppState; use crate::services::observation::fetcher::{pick_nearest, Candidate}; use crate::services::observation::registry::ObservationRegistry; use crate::services::observation::types::{ DownloadFailure, FindStrategy, ObservationBatch, ObservationProduct, ObservationRequest, ProductSpec, }; use anyhow::Result; use tracing::{info, warn}; /// 统一检索入口:按坐标 cone 检索候选源(**不下载**) /// /// 与 `download_observation` 的 ByCoordinates 模式相比,本函数只执行 cone_search_cached /// 这一步,把命中的候选源列表返回给调用方预览,由调用方决定是否下载、下载哪些。 /// /// 标识符模式无需"检索"——标识符本身就是确定源,调用方直接走 download 即可。 pub async fn search_observation( state: &AppState, registry: &ObservationRegistry, source: crate::services::observation::types::Source, product: &ProductSpec, ra: f64, dec: f64, radius_deg: f64, release: Option<&str>, version: Option<&str>, ) -> Result> { let subtype = product.subtype.as_deref(); let fetcher = registry.get(source, product.product, subtype)?; info!( "[Observation] search source={:?} product={:?} ra={:.4} dec={:.4} radius={:.4}°", source, product.product, ra, dec, radius_deg ); let matched = fetcher .cone_search_cached(state, ra, dec, radius_deg, release, subtype, version) .await?; Ok(matched) } /// 统一下载入口:按坐标或按标识符下载观测数据 /// /// 这是观测数据的唯一上层入口。调用方通过 `ObservationRequest` 选择模式,force 控制是否忽略缓存。 pub async fn download_observation( state: &AppState, registry: &ObservationRegistry, request: &ObservationRequest, force: bool, ) -> Result { match request { ObservationRequest::ByCoordinates { source, product, ra, dec, radius_deg, strategy, release, version, } => { download_by_coordinates( state, registry, *source, product, *ra, *dec, *radius_deg, *strategy, release.as_deref(), version.as_deref(), force, ) .await } ObservationRequest::ByIdentifier { source, product, identifiers, release, version, } => { download_by_identifiers( state, registry, *source, product, identifiers, release.as_deref(), version.as_deref(), force, ) .await } } } /// 按坐标:cone 检索 → 选源 → 逐个下载 async fn download_by_coordinates( state: &AppState, registry: &ObservationRegistry, source: crate::services::observation::types::Source, product: &ProductSpec, ra: f64, dec: f64, radius_deg: f64, strategy: FindStrategy, release: Option<&str>, version: Option<&str>, force: bool, ) -> Result { let subtype = product.subtype.as_deref(); let fetcher = registry.get(source, product.product, subtype)?; info!( "[Observation] by_coords source={:?} product={:?} ra={:.4} dec={:.4} radius={:.4}° strategy={:?}", source, product.product, ra, dec, radius_deg, strategy ); let matched = fetcher .cone_search_cached(state, ra, dec, radius_deg, release, subtype, version) .await?; let matched_count = matched.len(); if matched_count == 0 { return Ok(ObservationBatch { source, product: product.clone(), ra: Some(ra), dec: Some(dec), radius_deg: Some(radius_deg), matched_count: 0, products: Vec::new(), failures: Vec::new(), }); } let targets: Vec = match strategy { FindStrategy::Nearest => vec![pick_nearest(&matched, ra, dec)], FindStrategy::All => (0..matched.len()).collect(), }; let mut products = Vec::with_capacity(targets.len()); let mut failures = Vec::new(); for idx in targets { let candidate = &matched[idx]; match fetcher .fetch(state, candidate, release, subtype, version, force) .await { Ok(p) => products.push(p), Err(e) => { let label = candidate.label.clone(); warn!("[Observation] 下载失败 {}: {}", label, e); failures.push(DownloadFailure { source_label: label, error: format!("{:#}", e), }); } } } Ok(ObservationBatch { source, product: product.clone(), ra: Some(ra), dec: Some(dec), radius_deg: Some(radius_deg), matched_count, products, failures, }) } /// 按标识符:解析各源标识字符串 → 逐个下载(跳过 cone 检索) async fn download_by_identifiers( state: &AppState, registry: &ObservationRegistry, source: crate::services::observation::types::Source, product: &ProductSpec, identifiers: &[String], release: Option<&str>, version: Option<&str>, force: bool, ) -> Result { let subtype = product.subtype.as_deref(); let fetcher = registry.get(source, product.product, subtype)?; if identifiers.is_empty() { return Err(anyhow::anyhow!( "标识符列表不能为空(source={:?} product={:?})", source, product.product )); } info!( "[Observation] by_id source={:?} product={:?} count={} release={:?}", source, product.product, identifiers.len(), release ); let mut products = Vec::with_capacity(identifiers.len()); let mut failures = Vec::new(); for id in identifiers { // 先把标识符解析为 Candidate(验证格式 + 提取字段) let candidate = match fetcher .resolve_identifier(id, release, subtype, version) .await { Ok(c) => c, Err(e) => { warn!("[Observation] 标识符解析失败 {}: {}", id, e); failures.push(DownloadFailure { source_label: id.clone(), error: format!("{:#}", e), }); continue; } }; match fetcher .fetch(state, &candidate, release, subtype, version, force) .await { Ok(p) => products.push(p), Err(e) => { warn!("[Observation] 下载失败 {}: {}", id, e); failures.push(DownloadFailure { source_label: id.clone(), error: format!("{:#}", e), }); } } } Ok(ObservationBatch { source, product: product.clone(), ra: None, dec: None, radius_deg: None, matched_count: products.len() + failures.len(), products, failures, }) } /// 便捷:从单个标识符构造请求并下载(供 CLI / 简单场景使用) pub async fn download_one( state: &AppState, registry: &ObservationRegistry, source: crate::services::observation::types::Source, product: ProductSpec, identifier: &str, release: Option<&str>, version: Option<&str>, force: bool, ) -> Result { let subtype = product.subtype.as_deref(); let fetcher = registry.get(source, product.product, subtype)?; let candidate = fetcher .resolve_identifier(identifier, release, subtype, version) .await?; fetcher .fetch(state, &candidate, release, subtype, version, force) .await }