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:
+62
-12
@@ -80,13 +80,44 @@ pub async fn get_agent_modes() -> Json<Vec<AgentModeDto>> {
|
||||
Json(modes)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AgentToolDto {
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub is_internal: bool,
|
||||
}
|
||||
|
||||
// ── GET /api/chat/tools ──
|
||||
// 获取系统注册的工具元数据列表
|
||||
pub async fn get_agent_tools(State(state): State<Arc<AppState>>) -> Json<Vec<AgentToolDto>> {
|
||||
use crate::agent::tools::ToolRegistry;
|
||||
let skill_registry = state.skill_registry.clone();
|
||||
let registry = ToolRegistry::new(skill_registry);
|
||||
let tools = registry
|
||||
.list()
|
||||
.iter()
|
||||
.map(|t| AgentToolDto {
|
||||
name: t.name().to_string(),
|
||||
display_name: t.display_name().to_string(),
|
||||
is_internal: t.is_internal(),
|
||||
})
|
||||
.collect();
|
||||
Json(tools)
|
||||
}
|
||||
|
||||
pub async fn chat_agent(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<AgentChatRequest>,
|
||||
) -> ApiResult<Sse<impl Stream<Item = Result<Event, Infallible>>>> {
|
||||
// 截断日志中的问题内容,避免打印敏感信息
|
||||
let question_preview = if req.question.len() > 50 {
|
||||
format!("{}...", &req.question[..50])
|
||||
} else {
|
||||
req.question.clone()
|
||||
};
|
||||
info!(
|
||||
"接收到智能体对话请求: question='{}', session_id={:?}, has_image={}",
|
||||
req.question,
|
||||
question_preview,
|
||||
req.session_id,
|
||||
req.image.is_some()
|
||||
);
|
||||
@@ -97,7 +128,7 @@ pub async fn chat_agent(
|
||||
Some(ref img) => {
|
||||
// 如果带有 path 字段(重试场景),复用已有文件,不重新保存
|
||||
let relative_path: String = if let Some(ref existing_path) = img.path {
|
||||
let full = state.config.library_dir.join(existing_path);
|
||||
let full = state.config.storage.library_dir.join(existing_path);
|
||||
if full.exists() {
|
||||
info!("重试复用已有图片: {}", existing_path);
|
||||
existing_path.clone()
|
||||
@@ -117,7 +148,7 @@ pub async fn chat_agent(
|
||||
img.mime_type
|
||||
)));
|
||||
}
|
||||
if state.vision_llm.is_none() {
|
||||
if state.llm.vision.is_none() {
|
||||
return Err(AppError::bad_request(
|
||||
"图片分析功能未启用。请配置 LLM_VISION_MODEL 环境变量后重试。",
|
||||
));
|
||||
@@ -125,6 +156,7 @@ pub async fn chat_agent(
|
||||
let ext = img.mime_type.strip_prefix("image/").unwrap_or("png");
|
||||
let upload_dir = state
|
||||
.config
|
||||
.storage
|
||||
.library_dir
|
||||
.join(".agent")
|
||||
.join("images")
|
||||
@@ -142,7 +174,7 @@ pub async fn chat_agent(
|
||||
.await
|
||||
.map_err(|e| AppError::internal(format!("保存图片失败: {}", e)))?;
|
||||
let rel = filepath
|
||||
.strip_prefix(&state.config.library_dir)
|
||||
.strip_prefix(&state.config.storage.library_dir)
|
||||
.unwrap_or(&filepath)
|
||||
.display()
|
||||
.to_string();
|
||||
@@ -171,7 +203,7 @@ pub async fn chat_agent(
|
||||
let session_id = req.session_id.clone();
|
||||
|
||||
// 在后台 tokio 任务中执行 Agent 循环
|
||||
tokio::spawn(async move {
|
||||
let agent_handle = tokio::spawn(async move {
|
||||
match runtime
|
||||
.run_turn_with_image_context(
|
||||
session_id,
|
||||
@@ -197,11 +229,17 @@ pub async fn chat_agent(
|
||||
|
||||
// 将 mpsc 通道转换为 SSE 事件流(带 10 分钟超时)
|
||||
const SSE_TIMEOUT_SECS: u64 = 600;
|
||||
let cancelled_runs = state.session.cancelled_runs.clone();
|
||||
let stream = async_stream::stream! {
|
||||
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(SSE_TIMEOUT_SECS);
|
||||
loop {
|
||||
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
|
||||
if remaining.is_zero() {
|
||||
// 超时:通知 CancellationHook 停止 Agent,并中止后台任务
|
||||
if let Some(sid) = &req.session_id {
|
||||
cancelled_runs.insert(sid.clone(), ());
|
||||
}
|
||||
agent_handle.abort();
|
||||
let timeout_event = AgentStreamEvent::Error {
|
||||
message: "Agent 执行超时(10 分钟),请重试。".to_string(),
|
||||
};
|
||||
@@ -220,6 +258,11 @@ pub async fn chat_agent(
|
||||
}
|
||||
Ok(None) => break, // channel closed
|
||||
Err(_) => {
|
||||
// 超时:通知 CancellationHook 停止 Agent,并中止后台任务
|
||||
if let Some(sid) = &req.session_id {
|
||||
cancelled_runs.insert(sid.clone(), ());
|
||||
}
|
||||
agent_handle.abort();
|
||||
let timeout_event = AgentStreamEvent::Error {
|
||||
message: "Agent 执行超时(10 分钟),请重试。".to_string(),
|
||||
};
|
||||
@@ -247,8 +290,8 @@ pub async fn list_sessions(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(params): Query<SessionListParams>,
|
||||
) -> ApiResult<Json<Vec<crate::services::session::SessionSummary>>> {
|
||||
let limit = params.limit.unwrap_or(50);
|
||||
let offset = params.offset.unwrap_or(0);
|
||||
let limit = params.limit.unwrap_or(50).clamp(1, 200);
|
||||
let offset = params.offset.unwrap_or(0).max(0);
|
||||
|
||||
let sessions = crate::services::session::list_sessions_service(&state.db, limit, offset)
|
||||
.await
|
||||
@@ -299,7 +342,7 @@ pub async fn stop_agent(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> ApiResult<Json<serde_json::Value>> {
|
||||
state.cancelled_runs.insert(session_id.clone(), ());
|
||||
state.session.cancelled_runs.insert(session_id.clone(), ());
|
||||
info!("已接收并记录手动中止请求,会话 ID: {}", session_id);
|
||||
Ok(Json(
|
||||
serde_json::json!({ "status": "stopping", "session_id": session_id }),
|
||||
@@ -349,7 +392,7 @@ pub async fn answer_question(
|
||||
) -> ApiResult<Json<serde_json::Value>> {
|
||||
use crate::agent::tools::ask_user::UserAnswer;
|
||||
|
||||
let mut pending = state.pending_questions.lock().await;
|
||||
let mut pending = state.session.pending_questions.lock().await;
|
||||
let question_id = req.question_id.clone();
|
||||
|
||||
match pending.remove(&question_id) {
|
||||
@@ -379,10 +422,15 @@ pub async fn answer_question(
|
||||
// ── GET /api/chat/pending_questions ──
|
||||
// 获取当前待回答的问题(前端轮询或初始化)
|
||||
|
||||
/// 待回答问题的 TTL(10 分钟),超过此时间自动清理
|
||||
const PENDING_TTL_SECS: u64 = 600;
|
||||
|
||||
pub async fn get_pending_questions(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Json<Vec<serde_json::Value>> {
|
||||
let pending = state.pending_questions.lock().await;
|
||||
let mut pending = state.session.pending_questions.lock().await;
|
||||
// 清理过期条目(agent 崩溃后不会被 answer_question 清理)
|
||||
pending.retain(|_, pq| pq.created_at.elapsed().as_secs() < PENDING_TTL_SECS);
|
||||
let questions: Vec<serde_json::Value> = pending
|
||||
.iter()
|
||||
.map(|(id, pq)| {
|
||||
@@ -401,7 +449,7 @@ pub async fn respond_permission(
|
||||
Path(session_id): Path<String>,
|
||||
Json(req): Json<super::PermissionResponse>,
|
||||
) -> ApiResult<Json<serde_json::Value>> {
|
||||
let mut perms = state.pending_permissions.lock().await;
|
||||
let mut perms = state.session.pending_permissions.lock().await;
|
||||
|
||||
// 按 tool_call_id 查找匹配的权限请求
|
||||
let perm_id = perms
|
||||
@@ -440,7 +488,9 @@ pub async fn respond_permission(
|
||||
pub async fn get_pending_permissions(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Json<Vec<serde_json::Value>> {
|
||||
let perms = state.pending_permissions.lock().await;
|
||||
let mut perms = state.session.pending_permissions.lock().await;
|
||||
// 清理过期条目(agent 崩溃后不会被 respond_permission 清理)
|
||||
perms.retain(|_, p| p.created_at.elapsed().as_secs() < PENDING_TTL_SECS);
|
||||
let result: Vec<serde_json::Value> = perms
|
||||
.iter()
|
||||
.map(|(id, p)| {
|
||||
|
||||
Reference in New Issue
Block a user