feat: 接入 VizieR 星表检索与 LAMOST/Gaia/SDSS/DESI 跨源光谱下载

新增天文观测数据获取能力,覆盖星表查询与一维光谱下载两大场景:

星表检索(CDS VizieR)
- VizieR TAP 客户端(JSON 优先 + VOTable 降级),共享 IVOA VOTable 解析层
- 业务层支持自由 ADQL、锥形检索、交叉证认、星表发现与 CSV 导出
- ADQL 注入防护(标识符清洗 + 字符串字面量转义),TTL 缓存(7 天)

跨望远镜光谱下载(统一入口)
- 接入 LAMOST(ConeSearch + FITS.gz)、Gaia(TAP + DataLink ZIP)、
  SDSS(Data Lab TAP + SAS)、DESI(HEALPix coadd)四源
- 双模式:坐标模式(cone 检索 → 选源 → 下载)/ 标识符模式(直按 ID 下载)
- 光谱文件永久缓存(不可变),按 source+source_id 去重

Agent 与 API
- +4 工具:query_vizier / cone_search / find_spectrum / catalog_operation(22 → 26)
- +6 路由:/catalog/vizier、/cone、/crossmatch、/spectrum/{download,list}
- 前端新增 VizierResultCard / FindSpectrumCard 可视化卡片

工程重构
- services/target.rs (832 行) 拆分为 services/cds/{target,vizier}.rs + clients/cds/sesame.rs,
  贯彻 client(通信)/ service(缓存+编排)分层
- ADS 返回字段新增 data(关联数据表 URL),与星表功能联动
This commit is contained in:
fmq
2026-07-06 00:07:25 +08:00
parent 0ba85d7749
commit a156252bc3
48 changed files with 8837 additions and 388 deletions
+22
View File
@@ -553,6 +553,7 @@ mod tests {
use crate::api::AppState;
use crate::clients::ads::AdsClient;
use crate::clients::arxiv::ArxivClient;
use crate::clients::cds::vizier::VizierClient;
use crate::clients::llm::{EmbeddingClient, LlmClient};
use crate::clients::qiniu::QiniuClient;
use crate::services::batch::asset::AssetBatchStatus;
@@ -572,6 +573,8 @@ mod tests {
EmbeddingClient::new("tk".into(), "http://localhost".into(), "e".into()).unwrap();
let ads = AdsClient::new("tk".into()).unwrap();
let arxiv = ArxivClient::new().unwrap();
let vizier =
VizierClient::new("https://tapvizier.cds.unistra.fr/TAPVizieR/tap", 60).unwrap();
let qiniu = QiniuClient::new(
"ak".into(),
"sk".into(),
@@ -586,6 +589,25 @@ mod tests {
qiniu,
ads,
arxiv,
vizier,
lamost: crate::clients::lamost::LamostClient::new(
"https://www.lamost.org",
60,
)
.unwrap(),
gaia: crate::clients::gaia::GaiaClient::new(
"https://gea.esac.esa.int/tap-server/tap",
"https://gea.esac.esa.int/data-server",
90,
)
.unwrap(),
sdss: crate::clients::sdss::SdssClient::new("https://datalab.noirlab.edu/tap/sync", 90)
.unwrap(),
desi: crate::clients::desi::DesiClient::new(
"https://datalab.noirlab.edu/tap/sync",
120,
)
.unwrap(),
llm: llm.clone(),
medium_llm: llm.clone(),
fast_llm: llm.clone(),