refactor: 全栈架构重构与质量硬化
核心架构重构: - Config 按职责拆分为 LlmConfig/EmbeddingConfig/VisionConfig/CdsConfig/StorageConfig 五个子结构 - AppState 拆分为 LlmState/DataSourceState/SessionState 三个子结构,消除 50+ 平铺字段 - 新增 ServiceError 结构化错误类型替代 handler 中的 msg.contains() 字符串匹配 - 移除 api::handlers 兼容命名空间,路由直接引用 agent/auth/papers 等模块 认证性能优化: - login_rate_limiter/upload_rate_limiter 从 Mutex<HashMap> 迁移为 DashMap(无锁) - 新增 session_last_active: DashMap<String, AtomicU64>,auth 中间件快速路径免写锁 - 会话过期清理改为按间隔触发(300s),避免每次请求全表扫描 - MAX_SESSIONS 1000→10000,SSE 广播通道 256→1024 Agent 工具增强: - AgentTool trait 新增 is_internal()/display_name(),SSE 事件携带工具元数据 - 新增 GET /chat/tools 端点暴露注册工具列表 - pending_questions/pending_permissions 增加 created_at 时间戳,自动清理过期条目(10min TTL) - Agent 超时现在正确 abort 后台任务并设置取消令牌 观测数据源修复: - FITS 解析: APOGEE/DESI 改用 read_image+切片替代 read_rows(修复 fitsio panic) - ZTF: CIRCLE 参数分隔符 +→空格(修复 IRSA 400),半径自动裁剪至硬上限 - MAST TESS: parse_tic_json 兼容数组/对象两种 API 响应格式 - 统一检索: per_target_limit 默认 50→1,sources 支持 per-source release/version - Gaia 测光从 VizieR 镜像切换至官方 TAP 服务 RAG 并发优化: - 向量化降级从串行改为并发 5 条/批(buffer_unordered) - 混合检索 RRF 合并从借用改为 owned RetrievalResult 安全加固: - PDF 中间件: URL 解码 %2F/%2E 后判扩展名;文件名过滤非 ASCII + 禁 \ 防头注入 - chat_agent 日志截断问题内容至 50 字符;list_sessions 强制 limit clamp 前端双主题: - 设计令牌三层架构: primitive→semantic→component,浅色暖纸张学术/暗色 Night Indigo - useTheme hook + ThemeToggle 侧边栏组件 + main.tsx 防 FOUC 初始化 - 全组件从硬编码 slate 色迁移至语义令牌(bg-surface/text-content/border-subtle 等) - 新增 ToastContainer 非阻塞通知系统 部署优化: - deploy.sh 引入 SSH ControlMaster 单次密码复用
This commit is contained in:
@@ -19,6 +19,10 @@ impl AgentTool for SearchLocalLibraryTool {
|
||||
"search_local_library"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"本地文献库检索"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"在本地文献库中进行全文检索。搜索范围包括标题、作者、关键词、摘要和期刊名。\
|
||||
使用 BM25 相关性排序,返回匹配度最高的结果。适用于:查找已入库的文献、按关键词或作者浏览本地馆藏。"
|
||||
@@ -148,6 +152,10 @@ impl AgentTool for GetCitationNetworkTool {
|
||||
"get_citation_network"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"文献引用网络"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"浏览和查找文献的引用关系。两种用法:\
|
||||
1. 引用查找——提供 query 参数(如 'Lei 2023'),在参考文献/被引列表中按作者+年份模糊匹配,返回匹配的文献信息;\
|
||||
@@ -237,7 +245,7 @@ impl AgentTool for GetCitationNetworkTool {
|
||||
// 验证源文献存在
|
||||
let paper = match crate::services::paper::get_paper_from_db(
|
||||
&state.db,
|
||||
&state.config.library_dir,
|
||||
&state.config.storage.library_dir,
|
||||
&bibcode,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -15,6 +15,10 @@ impl AgentTool for GetPaperMetadataTool {
|
||||
"get_paper_metadata"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"获取文献信息"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"获取指定文献的完整元数据信息(包括完整标题、所有作者、出版期刊、关键字、引用数、完整摘要等)。\
|
||||
适用于:需要查看某篇文献的详细信息、阅读完整摘要以评估文献相关性。"
|
||||
@@ -56,7 +60,7 @@ impl AgentTool for GetPaperMetadataTool {
|
||||
|
||||
match crate::services::paper::get_paper_from_db(
|
||||
&state.db,
|
||||
&state.config.library_dir,
|
||||
&state.config.storage.library_dir,
|
||||
&bibcode,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -15,6 +15,10 @@ impl AgentTool for SaveNoteTool {
|
||||
"save_note"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"保存笔记"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"将研究中间结果或最终结论保存为 Markdown 笔记文件。适用于记录阅读摘要、研究思路、文献分析等。"
|
||||
}
|
||||
@@ -57,8 +61,12 @@ impl AgentTool for SaveNoteTool {
|
||||
info!("[SaveNote] 保存笔记: {}", title);
|
||||
let state = &ctx.app_state;
|
||||
|
||||
match crate::services::note::save_note_service(&state.config.library_dir, &title, &content)
|
||||
.await
|
||||
match crate::services::note::save_note_service(
|
||||
&state.config.storage.library_dir,
|
||||
&title,
|
||||
&content,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((filename, filepath, size)) => ToolOutput::success(
|
||||
format!("笔记已保存: {}", filename),
|
||||
|
||||
@@ -44,6 +44,10 @@ impl AgentTool for FindObservationTool {
|
||||
"find_observation"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"观测数据获取"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"下载天文观测数据(LAMOST/Gaia/SDSS/DESI 的光谱/光变/测光/图像)。\n\
|
||||
坐标模式:给 ra/dec + source + product,自动 cone 检索并下载最近(或全部)命中源。\n\
|
||||
@@ -225,7 +229,9 @@ impl AgentTool for FindObservationTool {
|
||||
}
|
||||
};
|
||||
|
||||
match download_observation(state, &state.observation_registry, &request, force).await {
|
||||
match download_observation(state, &state.sources.observation_registry, &request, force)
|
||||
.await
|
||||
{
|
||||
Ok(batch) => {
|
||||
let content = render_batch(&batch);
|
||||
ToolOutput::success(content, json!(batch))
|
||||
|
||||
@@ -21,6 +21,10 @@ impl AgentTool for GetPaperOutlineTool {
|
||||
"get_paper_outline"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"文献提纲提取"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"获取文献的章节大纲目录,列出所有章节的序号和标题。\n\
|
||||
在读取文献内容前,强烈建议先使用此工具了解章节结构,以便按需读取,节约 token。"
|
||||
@@ -52,7 +56,7 @@ impl AgentTool for GetPaperOutlineTool {
|
||||
|
||||
match crate::services::paper::read_paper_content(
|
||||
&state.db,
|
||||
&state.config.library_dir,
|
||||
&state.config.storage.library_dir,
|
||||
&bibcode,
|
||||
mode,
|
||||
)
|
||||
@@ -108,6 +112,10 @@ impl AgentTool for GetPaperContentTool {
|
||||
"get_paper_content"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"文献内容读取"
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"获取文献的具体内容(全文或指定章节内容)。\n\
|
||||
可传入 section_index 或 section_name 指定仅读取某一章节。\n\
|
||||
@@ -176,7 +184,7 @@ impl AgentTool for GetPaperContentTool {
|
||||
|
||||
match crate::services::paper::read_paper_content(
|
||||
&state.db,
|
||||
&state.config.library_dir,
|
||||
&state.config.storage.library_dir,
|
||||
&bibcode,
|
||||
mode,
|
||||
)
|
||||
|
||||
@@ -15,6 +15,10 @@ impl AgentTool for RagSearchTool {
|
||||
"rag_search"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"文献库RAG检索"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"在向量化文献库中执行混合语义检索。同时利用稠密向量(语义相似)和稀疏 BM25(关键词精确匹配),\
|
||||
用 RRF(倒数秩融合)算法合并两路结果,兼顾同义表达和精确术语。\
|
||||
@@ -64,8 +68,13 @@ impl AgentTool for RagSearchTool {
|
||||
);
|
||||
let state = &ctx.app_state;
|
||||
|
||||
match crate::services::rag::retrieve_hybrid(&state.db, &state.embedding, &question, top_k)
|
||||
.await
|
||||
match crate::services::rag::retrieve_hybrid(
|
||||
&state.db,
|
||||
&state.llm.embedding,
|
||||
&question,
|
||||
top_k,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(chunks) => {
|
||||
if chunks.is_empty() {
|
||||
|
||||
@@ -15,6 +15,10 @@ impl AgentTool for QueryTargetTool {
|
||||
"query_target"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"天体解析"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"查询天体目标的基本物理参数(坐标 RA/Dec、视星等、光谱类型、视差等)。数据来源为 CDS SIMBAD/Sesame 名称解析服务,结果自动缓存。\
|
||||
支持常见天体名称,如 'NGC 6752'、'GD 358'、'HD 209458'、'M 31' 等。"
|
||||
|
||||
@@ -90,6 +90,10 @@ impl AgentTool for CatalogOperationTool {
|
||||
"catalog_operation"
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"星表检索"
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"VizieR 星表统一操作。通过 action 选择:\n\
|
||||
search — 按关键词搜索星表目录;\n\
|
||||
@@ -196,7 +200,8 @@ async fn do_catalog_search(state: &crate::api::AppState, args: &serde_json::Valu
|
||||
.unwrap_or(10)
|
||||
.clamp(1, 30) as usize;
|
||||
|
||||
let catalog = crate::services::cds::vizier::VizierCatalog::new(&state.db, &state.vizier);
|
||||
let catalog =
|
||||
crate::services::cds::vizier::VizierCatalog::new(&state.db, &state.sources.vizier);
|
||||
let results = match catalog.search(keyword, limit).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return ToolOutput::error(format!("搜索失败: {}", e)),
|
||||
@@ -229,7 +234,8 @@ async fn do_catalog_describe(state: &crate::api::AppState, args: &serde_json::Va
|
||||
None => return ToolOutput::error("describe 需要 'table' 参数"),
|
||||
};
|
||||
|
||||
let catalog = crate::services::cds::vizier::VizierCatalog::new(&state.db, &state.vizier);
|
||||
let catalog =
|
||||
crate::services::cds::vizier::VizierCatalog::new(&state.db, &state.sources.vizier);
|
||||
let columns = match catalog.describe(table).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return ToolOutput::error(format!("查询表结构失败: {}", e)),
|
||||
@@ -266,7 +272,13 @@ async fn do_catalog_query(state: &crate::api::AppState, args: &serde_json::Value
|
||||
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.sources.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")
|
||||
@@ -274,8 +286,14 @@ async fn do_catalog_query(state: &crate::api::AppState, args: &serde_json::Value
|
||||
.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.sources.vizier,
|
||||
table,
|
||||
&columns,
|
||||
limit,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
return ToolOutput::error("query 需要 'adql' 或 'table' 参数之一");
|
||||
};
|
||||
@@ -328,7 +346,7 @@ async fn do_catalog_cone(state: &crate::api::AppState, args: &serde_json::Value)
|
||||
|
||||
match crate::services::cds::vizier::cone_search(
|
||||
&state.db,
|
||||
&state.vizier,
|
||||
&state.sources.vizier,
|
||||
ra,
|
||||
dec,
|
||||
radius,
|
||||
@@ -365,10 +383,11 @@ async fn do_catalog_export(state: &crate::api::AppState, args: &serde_json::Valu
|
||||
return ToolOutput::error("export 需要 'adql' 或 'table' 参数");
|
||||
}
|
||||
|
||||
let catalog = crate::services::cds::vizier::VizierCatalog::new(&state.db, &state.vizier);
|
||||
let catalog =
|
||||
crate::services::cds::vizier::VizierCatalog::new(&state.db, &state.sources.vizier);
|
||||
let result = match catalog
|
||||
.export_to_file(
|
||||
state.config.library_dir.to_str().unwrap_or("."),
|
||||
state.config.storage.library_dir.to_str().unwrap_or("."),
|
||||
adql,
|
||||
table,
|
||||
columns,
|
||||
@@ -406,8 +425,11 @@ async fn do_catalog_lookup(state: &crate::api::AppState, args: &serde_json::Valu
|
||||
.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.sources.vizier,
|
||||
&state.sources.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