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:
fmq
2026-07-11 14:57:40 +08:00
parent 8f1ed6d08c
commit eaf85707b5
142 changed files with 6349 additions and 3470 deletions
+9 -1
View File
@@ -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
+5 -1
View File
@@ -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
+10 -2
View File
@@ -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))
+10 -2
View File
@@ -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,
)
+11 -2
View File
@@ -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() {
+4
View File
@@ -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' 等。"
+32 -10
View File
@@ -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)),