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
View File
@@ -111,19 +111,28 @@ pub(super) async fn process_single_result(
additional_contexts: &mut Vec<String>,
db: &SqlitePool,
turn_index: i32,
tool_registry: &crate::agent::tools::ToolRegistry,
) {
use crate::agent::tools::persist::maybe_persist_tool_result;
let elapsed_ms = exec_start.elapsed().as_millis() as u64;
let (is_internal, display_name) = if let Some(tool) = tool_registry.get(tool_name) {
(tool.is_internal(), tool.display_name().to_string())
} else {
(false, tool_name.to_string())
};
// SSE 事件 — 立即推送到前端
let _ = tx.send(AgentStreamEvent::ToolResult {
tool_call_id: tool_call_id.to_string(),
name: tool_name.to_string(),
display_name,
output: output.content.clone(),
is_error: output.is_error,
metadata: output.metadata.clone(),
step,
is_internal,
});
// 输出处理:小结果直接传递,大结果持久化到磁盘并返回 stub
+60 -5
View File
@@ -41,6 +41,7 @@ pub fn validate_and_prepare(
duplicate_detector: &mut DuplicateDetector,
duplicate_threshold: usize,
messages: &mut Vec<ChatMessage>,
tool_registry: &ToolRegistry,
tx: &mpsc::UnboundedSender<AgentStreamEvent>,
db: &SqlitePool,
session_id: &str,
@@ -88,13 +89,20 @@ pub fn validate_and_prepare(
Ok(v) => v,
Err(e) => {
let error_output = format!("工具参数 JSON 解析失败: {}", e);
let (is_internal, display_name) = if let Some(tool) = tool_registry.get(tool_name) {
(tool.is_internal(), tool.display_name().to_string())
} else {
(false, tool_name.clone())
};
let _ = tx.send(AgentStreamEvent::ToolResult {
tool_call_id: call_id.clone(),
name: tool_name.clone(),
display_name,
output: error_output.clone(),
is_error: true,
metadata: serde_json::json!({}),
step,
is_internal,
});
let tool_msg = ChatMessage::tool_result(&call_id, &error_output);
save_tool_message_sync(db, session_id, turn_index, step, &tool_msg);
@@ -158,11 +166,18 @@ pub async fn execute_parallel(
// Phase 1: 发送 ToolCall SSE 事件
for prep in prepared_calls {
let (is_internal, display_name) = if let Some(tool) = tool_registry.get(&prep.tool_name) {
(tool.is_internal(), tool.display_name().to_string())
} else {
(false, prep.tool_name.clone())
};
let _ = tx.send(AgentStreamEvent::ToolCall {
id: prep.tool_call_id.clone(),
name: prep.tool_name.clone(),
display_name,
arguments: prep.args.clone(),
step,
is_internal,
});
}
@@ -260,9 +275,16 @@ pub async fn execute_parallel(
hardline_result.reason
);
let err_output = hardline_result.reason.clone();
let (is_internal, display_name) = if let Some(tool) = tool_registry.get(&prep.tool_name)
{
(tool.is_internal(), tool.display_name().to_string())
} else {
(false, prep.tool_name.clone())
};
let _ = tx.send(AgentStreamEvent::ToolResult {
tool_call_id: prep.tool_call_id.clone(),
name: prep.tool_name.clone(),
display_name,
output: err_output.clone(),
is_error: true,
metadata: serde_json::json!({
@@ -270,6 +292,7 @@ pub async fn execute_parallel(
"hardline_category": hardline_result.category,
}),
step,
is_internal,
});
let err_msg = ChatMessage::tool_result(&prep.tool_call_id, &err_output);
save_tool_message_sync(db, &sid, turn_index, step, &err_msg);
@@ -364,13 +387,21 @@ pub async fn execute_parallel(
);
let err_output =
format!("工具 {} 被权限规则拒绝执行: {}", prep.tool_name, reason);
let (is_internal, display_name) =
if let Some(tool) = tool_registry.get(&prep.tool_name) {
(tool.is_internal(), tool.display_name().to_string())
} else {
(false, prep.tool_name.clone())
};
let _ = tx.send(AgentStreamEvent::ToolResult {
tool_call_id: prep.tool_call_id.clone(),
name: prep.tool_name.clone(),
display_name,
output: err_output.clone(),
is_error: true,
metadata: serde_json::json!({}),
step,
is_internal,
});
let err_msg = ChatMessage::tool_result(&prep.tool_call_id, &err_output);
save_tool_message_sync(db, &sid, turn_index, step, &err_msg);
@@ -413,7 +444,7 @@ pub async fn execute_parallel(
// 存储待处理的权限请求
{
let mut perms = app_state.pending_permissions.lock().await;
let mut perms = app_state.session.pending_permissions.lock().await;
perms.insert(
perm_id.clone(),
PendingPermission {
@@ -422,6 +453,7 @@ pub async fn execute_parallel(
message: message.clone(),
arguments: prep.args.clone(),
response_tx: resp_tx,
created_at: std::time::Instant::now(),
},
);
}
@@ -431,7 +463,12 @@ pub async fn execute_parallel(
let perm_result = tokio::time::timeout(timeout_dur, resp_rx).await;
// 清理待处理的权限请求
app_state.pending_permissions.lock().await.remove(&perm_id);
app_state
.session
.pending_permissions
.lock()
.await
.remove(&perm_id);
match perm_result {
Ok(Ok(response)) if response.allowed => {
@@ -451,13 +488,21 @@ pub async fn execute_parallel(
// 用户拒绝
info!("[Executor] 用户拒绝了工具 {}", prep.tool_name);
let err_output = format!("用户拒绝了工具 {} 的执行", prep.tool_name);
let (is_internal, display_name) =
if let Some(tool) = tool_registry.get(&prep.tool_name) {
(tool.is_internal(), tool.display_name().to_string())
} else {
(false, prep.tool_name.clone())
};
let _ = tx.send(AgentStreamEvent::ToolResult {
tool_call_id: prep.tool_call_id.clone(),
name: prep.tool_name.clone(),
display_name,
output: err_output.clone(),
is_error: true,
metadata: serde_json::json!({}),
step,
is_internal,
});
let err_msg = ChatMessage::tool_result(&prep.tool_call_id, &err_output);
save_tool_message_sync(db, &sid, turn_index, step, &err_msg);
@@ -482,13 +527,21 @@ pub async fn execute_parallel(
warn!("[Executor] 权限请求超时或取消: {}", prep.tool_name);
let err_output =
format!("权限请求超时 (120s): {} 未获得用户确认", prep.tool_name);
let (is_internal, display_name) =
if let Some(tool) = tool_registry.get(&prep.tool_name) {
(tool.is_internal(), tool.display_name().to_string())
} else {
(false, prep.tool_name.clone())
};
let _ = tx.send(AgentStreamEvent::ToolResult {
tool_call_id: prep.tool_call_id.clone(),
name: prep.tool_name.clone(),
display_name,
output: err_output.clone(),
is_error: true,
metadata: serde_json::json!({}),
step,
is_internal,
});
let err_msg = ChatMessage::tool_result(&prep.tool_call_id, &err_output);
save_tool_message_sync(db, &sid, turn_index, step, &err_msg);
@@ -538,7 +591,7 @@ pub async fn execute_parallel(
let cancel_handle = tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_millis(250)).await;
if app_state_ref.cancelled_runs.contains_key(&sid_ref) {
if app_state_ref.session.cancelled_runs.contains_key(&sid_ref) {
cancel_flag.store(true, Ordering::SeqCst);
return;
}
@@ -658,7 +711,7 @@ pub async fn execute_parallel(
exec_start,
tx,
hook_registry,
&app_state.config.library_dir,
&app_state.config.storage.library_dir,
&sid,
agent_name,
step,
@@ -667,6 +720,7 @@ pub async fn execute_parallel(
&mut additional_contexts,
db,
turn_index,
tool_registry,
)
.await;
}
@@ -713,7 +767,7 @@ pub async fn execute_parallel(
exec_start,
tx,
hook_registry,
&app_state.config.library_dir,
&app_state.config.storage.library_dir,
&sid,
agent_name,
step,
@@ -722,6 +776,7 @@ pub async fn execute_parallel(
&mut additional_contexts,
db,
turn_index,
tool_registry,
)
.await;