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
+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)),