feat: LAMOST DR12-14 与子版本体系接入、观测层安全加固与并发异步化
- LAMOST 新增 DR12/13/14 及子版本(v0/v1.0/v1.1/v2.0)维度,Internal 发布标记需登录认证并前端灰显,release×subtype 交叉约束下沉至 capabilities 统一声明 - ObservationFetcher trait 扩展版本/认证/交叉约束能力声明,version 参数贯穿 client→service→API→Agent tool→前端全链路 - 安全:observation cache SQL 全参数绑定 + LIKE 转义、cone_cache_hash 加长度前缀防碰撞、DESI survey/program 白名单防穿越 - 异步化:persist/cached_files_total_size/maybe_persist_tool_result迁移到 tokio::fs;cancelled_runs 与 session_permission_checkers改用 DashMap;auth 读锁优先 + 60s 节流 - Gaia 去 native-tls 改禁用连接池规避 UnexpectedEof,reqwest 移除 native-tls feature - 重构:Source/ProductType from_str 集中解析、download 模块拆分为 try_download_pdf/html、AgentRuntime::init 抽取共享逻辑 - 部署:新增 deploy.sh 一键打包推送脚本、catch-panic 启用
This commit is contained in:
@@ -253,7 +253,14 @@ impl AgentTool for GetCitationNetworkTool {
|
||||
|
||||
// 有 query → 引用查找模式:在关联文献中按作者+年份搜索
|
||||
if let Some(ref q) = query {
|
||||
match crate::services::citation::search_citations(&state.db, &paper.bibcode, direction, q).await {
|
||||
match crate::services::citation::search_citations(
|
||||
&state.db,
|
||||
&paper.bibcode,
|
||||
direction,
|
||||
q,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(results) => {
|
||||
if results.is_empty() {
|
||||
let dir_label = if direction == "citations" {
|
||||
@@ -315,8 +322,15 @@ impl AgentTool for GetCitationNetworkTool {
|
||||
}
|
||||
} else {
|
||||
// 无 query → 分页浏览模式
|
||||
match crate::services::citation::get_citations_paginated(&state.db, &paper.bibcode, direction, sort, offset, limit)
|
||||
.await
|
||||
match crate::services::citation::get_citations_paginated(
|
||||
&state.db,
|
||||
&paper.bibcode,
|
||||
direction,
|
||||
sort,
|
||||
offset,
|
||||
limit,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((rows, total)) => {
|
||||
let page_count = rows.len();
|
||||
|
||||
@@ -11,9 +11,9 @@ pub mod target;
|
||||
pub mod vizier;
|
||||
|
||||
pub use library::{GetCitationNetworkTool, SearchLocalLibraryTool};
|
||||
pub use observation::FindObservationTool;
|
||||
pub use metadata::GetPaperMetadataTool;
|
||||
pub use note::SaveNoteTool;
|
||||
pub use observation::FindObservationTool;
|
||||
pub use paper::{GetPaperContentTool, GetPaperOutlineTool};
|
||||
pub use rag::RagSearchTool;
|
||||
pub use target::QueryTargetTool;
|
||||
|
||||
@@ -58,6 +58,7 @@ impl AgentTool for SaveNoteTool {
|
||||
let state = &ctx.app_state;
|
||||
|
||||
match crate::services::note::save_note_service(&state.config.library_dir, &title, &content)
|
||||
.await
|
||||
{
|
||||
Ok((filename, filepath, size)) => ToolOutput::success(
|
||||
format!("笔记已保存: {}", filename),
|
||||
|
||||
@@ -52,23 +52,26 @@ impl AgentTool for FindObservationTool {
|
||||
}
|
||||
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
// enum 列表从枚举的 valid_values() 动态生成,新增源/产品时自动同步
|
||||
let source_enum: Vec<&str> = Source::valid_values().to_vec();
|
||||
let product_enum: Vec<&str> = ProductType::valid_values().to_vec();
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": ["lamost", "gaia", "sdss", "desi"],
|
||||
"enum": source_enum,
|
||||
"description": "数据源"
|
||||
},
|
||||
"product": {
|
||||
"type": "string",
|
||||
"enum": ["spectrum", "lightcurve", "photometry", "image"],
|
||||
"enum": product_enum,
|
||||
"description": "产品类型,默认 spectrum",
|
||||
"default": "spectrum"
|
||||
},
|
||||
"subtype": {
|
||||
"type": "string",
|
||||
"description": "产品子类型(可选)。LAMOST spectrum: lrs/mrs;Gaia spectrum: xp_continuous/xp_sampled/rvs;Gaia lightcurve: epoch_photometry;SDSS spectrum: spec/apstar/aspcap;DESI spectrum: coadd"
|
||||
"description": "产品子类型(可选)。各源可选值通过 GET /api/observation/capabilities 查询;LAMOST spectrum: lrs/mrs;Gaia spectrum: xp_continuous/xp_sampled/rvs;Gaia lightcurve: epoch_photometry;SDSS spectrum: spec/apstar/aspcap;DESI spectrum: coadd"
|
||||
},
|
||||
"ra": {
|
||||
"type": "number",
|
||||
@@ -80,7 +83,7 @@ impl AgentTool for FindObservationTool {
|
||||
},
|
||||
"radius_deg": {
|
||||
"type": "number",
|
||||
"description": "检索半径(度),坐标模式用,默认 0.1。各源无硬性 API 上限,但建议值:LAMOST≤5°,Gaia/SDSS/DESI≤1°(主表很大,超出易超时)。硬性上限 30°,超出报错。可通过 GET /api/observation/capabilities 查询各源建议值",
|
||||
"description": "检索半径(度),坐标模式用,默认 0.1。建议值与硬上限可通过 GET /api/observation/capabilities 查询(LAMOST 建议≤5°,其他≤1°;硬上限 30°)",
|
||||
"default": 0.1
|
||||
},
|
||||
"strategy": {
|
||||
@@ -96,7 +99,11 @@ impl AgentTool for FindObservationTool {
|
||||
},
|
||||
"release": {
|
||||
"type": "string",
|
||||
"description": "数据发布版本(可选,留空用各源默认)。可选值与默认值可通过 GET /api/observation/capabilities 查询;LAMOST: dr5..dr11(默认dr10,MRS 需 dr7+);Gaia: dr3;SDSS spec: dr16/dr17(默认dr17);SDSS apstar/aspcap: dr17;DESI: edr/dr1(默认dr1)"
|
||||
"description": "数据发布版本(可选,留空用各源默认)。可选值与默认值通过 GET /api/observation/capabilities 查询"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "数据发布的子版本(可选,仅 LAMOST 有意义)。留空用该 DR 的默认(最新公开)子版本。可选值通过 GET /api/observation/capabilities 的 release_versions 字段查询"
|
||||
},
|
||||
"force": {
|
||||
"type": "boolean",
|
||||
@@ -124,36 +131,41 @@ impl AgentTool for FindObservationTool {
|
||||
let state = &ctx.app_state;
|
||||
|
||||
let source = match args.get("source").and_then(|v| v.as_str()) {
|
||||
Some(s) => match s.to_lowercase().as_str() {
|
||||
"lamost" => Source::Lamost,
|
||||
"gaia" => Source::Gaia,
|
||||
"sdss" => Source::Sdss,
|
||||
"desi" => Source::Desi,
|
||||
other => return ToolOutput::error(format!(
|
||||
"不支持的 source '{}',可选: {:?}", other, Source::valid_values()
|
||||
)),
|
||||
Some(s) => match Source::from_str(s) {
|
||||
Ok(src) => src,
|
||||
Err(e) => return ToolOutput::error(e),
|
||||
},
|
||||
None => return ToolOutput::error("缺少必需参数 'source'(lamost/gaia/sdss/desi)"),
|
||||
};
|
||||
let product_type = match args.get("product").and_then(|v| v.as_str()) {
|
||||
None | Some("spectrum") => ProductType::Spectrum,
|
||||
Some("lightcurve") | Some("light_curve") | Some("lc") => ProductType::LightCurve,
|
||||
Some("photometry") => ProductType::Photometry,
|
||||
Some("image") => ProductType::Image,
|
||||
Some(other) => return ToolOutput::error(format!(
|
||||
"不支持的 product '{}',可选: {:?}", other, ProductType::valid_values()
|
||||
)),
|
||||
None => ProductType::Spectrum,
|
||||
Some(s) => match ProductType::from_str(s) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return ToolOutput::error(e),
|
||||
},
|
||||
};
|
||||
let subtype = args.get("subtype")
|
||||
let subtype = args
|
||||
.get("subtype")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
let product = ProductSpec { product: product_type, subtype };
|
||||
let product = ProductSpec {
|
||||
product: product_type,
|
||||
subtype,
|
||||
};
|
||||
let force = args.get("force").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let release = args.get("release").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let release = args
|
||||
.get("release")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
let version = args
|
||||
.get("version")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
// 标识符模式 vs 坐标模式
|
||||
let request = if let Some(ids) = args.get("source_ids").and_then(|v| v.as_array()) {
|
||||
let identifiers: Vec<String> = ids.iter()
|
||||
let identifiers: Vec<String> = ids
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect();
|
||||
if identifiers.is_empty() {
|
||||
@@ -161,23 +173,38 @@ impl AgentTool for FindObservationTool {
|
||||
}
|
||||
info!(
|
||||
"[FindObservation] by_id source={:?} product={:?} count={}",
|
||||
source, product.product, identifiers.len()
|
||||
source,
|
||||
product.product,
|
||||
identifiers.len()
|
||||
);
|
||||
ObservationRequest::ByIdentifier { source, product, identifiers, release }
|
||||
ObservationRequest::ByIdentifier {
|
||||
source,
|
||||
product,
|
||||
identifiers,
|
||||
release,
|
||||
version,
|
||||
}
|
||||
} else {
|
||||
let ra = match args.get("ra").and_then(|v| v.as_f64()) {
|
||||
Some(v) => v,
|
||||
None => return ToolOutput::error(
|
||||
"坐标模式缺少必需参数 'ra'(赤经,度),或改用 source_ids 标识符模式",
|
||||
),
|
||||
None => {
|
||||
return ToolOutput::error(
|
||||
"坐标模式缺少必需参数 'ra'(赤经,度),或改用 source_ids 标识符模式",
|
||||
)
|
||||
}
|
||||
};
|
||||
let dec = match args.get("dec").and_then(|v| v.as_f64()) {
|
||||
Some(v) => v,
|
||||
None => return ToolOutput::error(
|
||||
"坐标模式缺少必需参数 'dec'(赤纬,度),或改用 source_ids 标识符模式",
|
||||
),
|
||||
None => {
|
||||
return ToolOutput::error(
|
||||
"坐标模式缺少必需参数 'dec'(赤纬,度),或改用 source_ids 标识符模式",
|
||||
)
|
||||
}
|
||||
};
|
||||
let radius = args.get("radius_deg").and_then(|v| v.as_f64()).unwrap_or(0.1);
|
||||
let radius = args
|
||||
.get("radius_deg")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.1);
|
||||
let strategy = match args.get("strategy").and_then(|v| v.as_str()) {
|
||||
Some("all") => FindStrategy::All,
|
||||
_ => FindStrategy::Nearest,
|
||||
@@ -187,7 +214,14 @@ impl AgentTool for FindObservationTool {
|
||||
source, product.product, ra, dec, radius, strategy
|
||||
);
|
||||
ObservationRequest::ByCoordinates {
|
||||
source, product, ra, dec, radius_deg: radius, strategy, release,
|
||||
source,
|
||||
product,
|
||||
ra,
|
||||
dec,
|
||||
radius_deg: radius,
|
||||
strategy,
|
||||
release,
|
||||
version,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -203,7 +237,11 @@ impl AgentTool for FindObservationTool {
|
||||
|
||||
/// 渲染 ObservationBatch 为可读文本(支持多 artifact 展示)
|
||||
fn render_batch(b: &crate::services::observation::ObservationBatch) -> String {
|
||||
let mut content = format!("{} {} 下载", b.source.display(), b.product.product.display());
|
||||
let mut content = format!(
|
||||
"{} {} 下载",
|
||||
b.source.display(),
|
||||
b.product.product.display()
|
||||
);
|
||||
if let (Some(ra), Some(dec), Some(r)) = (b.ra, b.dec, b.radius_deg) {
|
||||
content.push_str(&format!("(ra={}, dec={}, radius={}°)", ra, dec, r));
|
||||
}
|
||||
@@ -219,18 +257,30 @@ fn render_batch(b: &crate::services::observation::ObservationBatch) -> String {
|
||||
|
||||
for p in &b.products {
|
||||
// 多 artifact 展示(如 Gaia 光变 G/BP/RP 三波段)
|
||||
let artifact_summary: Vec<String> = p.artifacts.iter().map(|a| {
|
||||
match &a.band {
|
||||
Some(band) => format!("{}波段 {} ({})",
|
||||
band, a.file_format.to_uppercase(), format_size(a.size_bytes)),
|
||||
None => format!("{} ({})",
|
||||
a.file_format.to_uppercase(), format_size(a.size_bytes)),
|
||||
}
|
||||
}).collect();
|
||||
let artifact_summary: Vec<String> = p
|
||||
.artifacts
|
||||
.iter()
|
||||
.map(|a| match &a.band {
|
||||
Some(band) => format!(
|
||||
"{}波段 {} ({})",
|
||||
band,
|
||||
a.file_format.to_uppercase(),
|
||||
format_size(a.size_bytes)
|
||||
),
|
||||
None => format!(
|
||||
"{} ({})",
|
||||
a.file_format.to_uppercase(),
|
||||
format_size(a.size_bytes)
|
||||
),
|
||||
})
|
||||
.collect();
|
||||
content.push_str(&format!(
|
||||
"\n✓ 已下载({}): {} —— {}\n",
|
||||
if p.artifacts.iter().all(|a| a.cached) { "缓存" } else { "新下载" }
|
||||
.to_string(),
|
||||
if p.artifacts.iter().all(|a| a.cached) {
|
||||
"缓存"
|
||||
} else {
|
||||
"新下载"
|
||||
},
|
||||
p.source_label,
|
||||
artifact_summary.join(", ")
|
||||
));
|
||||
@@ -238,7 +288,10 @@ fn render_batch(b: &crate::services::observation::ObservationBatch) -> String {
|
||||
content.push_str(&format!(
|
||||
" {}{}\n",
|
||||
a.file_path,
|
||||
a.band.as_ref().map(|b| format!(" [{}]", b)).unwrap_or_default()
|
||||
a.band
|
||||
.as_ref()
|
||||
.map(|b| format!(" [{}]", b))
|
||||
.unwrap_or_default()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,10 +185,7 @@ impl AgentTool for CatalogOperationTool {
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_catalog_search(
|
||||
state: &crate::api::AppState,
|
||||
args: &serde_json::Value,
|
||||
) -> ToolOutput {
|
||||
async fn do_catalog_search(state: &crate::api::AppState, args: &serde_json::Value) -> ToolOutput {
|
||||
let keyword = match args.get("keyword").and_then(|v| v.as_str()) {
|
||||
Some(k) => k,
|
||||
None => return ToolOutput::error("search 需要 'keyword' 参数"),
|
||||
@@ -226,10 +223,7 @@ async fn do_catalog_search(
|
||||
ToolOutput::success(content, json!(items))
|
||||
}
|
||||
|
||||
async fn do_catalog_describe(
|
||||
state: &crate::api::AppState,
|
||||
args: &serde_json::Value,
|
||||
) -> ToolOutput {
|
||||
async fn do_catalog_describe(state: &crate::api::AppState, args: &serde_json::Value) -> ToolOutput {
|
||||
let table = match args.get("table").and_then(|v| v.as_str()) {
|
||||
Some(t) => t,
|
||||
None => return ToolOutput::error("describe 需要 'table' 参数"),
|
||||
@@ -259,10 +253,7 @@ async fn do_catalog_describe(
|
||||
ToolOutput::success(content, json!(items))
|
||||
}
|
||||
|
||||
async fn do_catalog_query(
|
||||
state: &crate::api::AppState,
|
||||
args: &serde_json::Value,
|
||||
) -> ToolOutput {
|
||||
async fn do_catalog_query(state: &crate::api::AppState, args: &serde_json::Value) -> ToolOutput {
|
||||
let limit = args
|
||||
.get("limit")
|
||||
.and_then(|v| v.as_i64())
|
||||
@@ -275,8 +266,7 @@ async fn do_catalog_query(
|
||||
limit,
|
||||
adql.chars().take(150).collect::<String>()
|
||||
);
|
||||
crate::services::cds::vizier::query_adql_cached(&state.db, &state.vizier, adql, limit)
|
||||
.await
|
||||
crate::services::cds::vizier::query_adql_cached(&state.db, &state.vizier, adql, limit).await
|
||||
} else if let Some(table) = args.get("table").and_then(|v| v.as_str()) {
|
||||
let columns: Vec<String> = args
|
||||
.get("columns")
|
||||
@@ -284,14 +274,8 @@ async fn do_catalog_query(
|
||||
.map(|c| c.split(',').map(|s| s.trim().to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
info!("[CatalogOp:query] table={} limit={}", table, limit);
|
||||
crate::services::cds::vizier::query_table(
|
||||
&state.db,
|
||||
&state.vizier,
|
||||
table,
|
||||
&columns,
|
||||
limit,
|
||||
)
|
||||
.await
|
||||
crate::services::cds::vizier::query_table(&state.db, &state.vizier, table, &columns, limit)
|
||||
.await
|
||||
} else {
|
||||
return ToolOutput::error("query 需要 'adql' 或 'table' 参数之一");
|
||||
};
|
||||
@@ -305,10 +289,7 @@ async fn do_catalog_query(
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_catalog_cone(
|
||||
state: &crate::api::AppState,
|
||||
args: &serde_json::Value,
|
||||
) -> ToolOutput {
|
||||
async fn do_catalog_cone(state: &crate::api::AppState, args: &serde_json::Value) -> ToolOutput {
|
||||
let coords = match args.get("coords").and_then(|v| v.as_object()) {
|
||||
Some(c) => c,
|
||||
None => return ToolOutput::error("cone 需要 'coords' 参数(含 ra/dec)"),
|
||||
@@ -338,7 +319,11 @@ async fn do_catalog_cone(
|
||||
|
||||
info!(
|
||||
"[CatalogOp:cone] ra={} dec={} radius={}° table={} strategy={}",
|
||||
ra, dec, radius, table, if nearest { "nearest" } else { "all" }
|
||||
ra,
|
||||
dec,
|
||||
radius,
|
||||
table,
|
||||
if nearest { "nearest" } else { "all" }
|
||||
);
|
||||
|
||||
match crate::services::cds::vizier::cone_search(
|
||||
@@ -365,10 +350,7 @@ async fn do_catalog_cone(
|
||||
}
|
||||
}
|
||||
|
||||
async fn do_catalog_export(
|
||||
state: &crate::api::AppState,
|
||||
args: &serde_json::Value,
|
||||
) -> ToolOutput {
|
||||
async fn do_catalog_export(state: &crate::api::AppState, args: &serde_json::Value) -> ToolOutput {
|
||||
let limit = args
|
||||
.get("limit")
|
||||
.and_then(|v| v.as_i64())
|
||||
@@ -384,13 +366,16 @@ async fn do_catalog_export(
|
||||
}
|
||||
|
||||
let catalog = crate::services::cds::vizier::VizierCatalog::new(&state.db, &state.vizier);
|
||||
let result = match catalog.export_to_file(
|
||||
state.config.library_dir.to_str().unwrap_or("."),
|
||||
adql,
|
||||
table,
|
||||
columns,
|
||||
limit,
|
||||
).await {
|
||||
let result = match catalog
|
||||
.export_to_file(
|
||||
state.config.library_dir.to_str().unwrap_or("."),
|
||||
adql,
|
||||
table,
|
||||
columns,
|
||||
limit,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(e) => return ToolOutput::error(format!("导出失败: {}", e)),
|
||||
};
|
||||
@@ -410,10 +395,7 @@ async fn do_catalog_export(
|
||||
)
|
||||
}
|
||||
|
||||
async fn do_catalog_lookup(
|
||||
state: &crate::api::AppState,
|
||||
args: &serde_json::Value,
|
||||
) -> ToolOutput {
|
||||
async fn do_catalog_lookup(state: &crate::api::AppState, args: &serde_json::Value) -> ToolOutput {
|
||||
let bibcode = match args.get("bibcode").and_then(|v| v.as_str()) {
|
||||
Some(b) => b,
|
||||
None => return ToolOutput::error("lookup 需要 'bibcode' 参数"),
|
||||
@@ -424,11 +406,8 @@ async fn do_catalog_lookup(
|
||||
.unwrap_or(10)
|
||||
.clamp(1, 30) as usize;
|
||||
|
||||
let catalog = crate::services::cds::vizier::VizierCatalog::with_ads(
|
||||
&state.db,
|
||||
&state.vizier,
|
||||
&state.ads,
|
||||
);
|
||||
let catalog =
|
||||
crate::services::cds::vizier::VizierCatalog::with_ads(&state.db, &state.vizier, &state.ads);
|
||||
let results = match catalog.lookup(bibcode, limit).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return ToolOutput::error(format!("查找失败: {}", e)),
|
||||
|
||||
Reference in New Issue
Block a user