refactor: 全栈架构重构与质量硬化——API 错误统一、工具域重组、安全加固、前端组件化
后端核心变更: - API 层: 新增 AppError 枚举统一错误类型,替代散落的 (StatusCode, String) - Agent 工具域: 重组为 astro/system/ 和 astro/research/ 两级域,新增 ProcessPaperTool 流水线工具 - 安全: 新增 SSRF 双层防护 (同步字符串级 + 异步 DNS 解析级),覆盖 IPv4/IPv6 私网段 - 弱密码检测: 扩展弱密码列表并增加最小长度检查 - LLM 客户端: 新增 ChatCompleter/Embedder trait,支持依赖注入与批量向量化 embed_batch - 批量处理: AssetBatch 从串行改为 Semaphore 并发池 (BATCH_CONCURRENCY=3) - 分块器: 重写为三阶段结构化管线 (章节解析→短节合并→带标题路径子块) - RAG: embedding 计算移出事务,RetrievalResult 新增 headings/section_index 字段 - 检索: ADS/arXiv 并行检索 (tokio::join!),去重改用 HashSet,本地库回填批量 IN 查询 - 天体查询: Sesame API 升级到 v4,新增视差误差/自行/视向速度/多波段测光字段 - 迁移: 14 个增量文件合并为单一 init.sql,支持 sqlx::migrate! 内存库集成测试 - 测试: circuit_breaker/hooks/task_board/session/memory/streaming_executor 新增修正 15+ 测试 前端架构重构: - 目录重组: features/ → pages/ + components/ + hooks/ 三层分离 - App.tsx 从 1181 行压缩至 ~174 行 (逻辑抽入 9 个自定义 Hook) - Agent 面板拆分为 AgentSessionSidebar/AgentMessageList/AgentInputArea 子组件 - 新增 GlobalDialog/PaperDetailModal/UncachedPaperModal 通用对话框组件 - 工具函数抽取: celestial.ts (天体坐标格式), paper.tsx (文献信息渲染)
This commit is contained in:
+57
-88
@@ -5,7 +5,6 @@
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::sse::{Event, Sse},
|
||||
Json,
|
||||
};
|
||||
@@ -16,6 +15,7 @@ use std::convert::Infallible;
|
||||
use std::sync::Arc;
|
||||
use tracing::{error, info};
|
||||
|
||||
use super::error::{ApiResult, AppError};
|
||||
use super::AppState;
|
||||
use crate::agent::runtime::{AgentRuntime, AgentStreamEvent};
|
||||
|
||||
@@ -84,7 +84,7 @@ pub async fn get_agent_modes() -> Json<Vec<AgentModeDto>> {
|
||||
pub async fn chat_agent(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<AgentChatRequest>,
|
||||
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, (StatusCode, String)> {
|
||||
) -> ApiResult<Sse<impl Stream<Item = Result<Event, Infallible>>>> {
|
||||
info!(
|
||||
"接收到智能体对话请求: question='{}', session_id={:?}, has_image={}",
|
||||
req.question,
|
||||
@@ -103,25 +103,24 @@ pub async fn chat_agent(
|
||||
info!("重试复用已有图片: {}", existing_path);
|
||||
existing_path.clone()
|
||||
} else {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("图片文件不存在: {}", existing_path),
|
||||
));
|
||||
return Err(AppError::bad_request(format!(
|
||||
"图片文件不存在: {}",
|
||||
existing_path
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
if img.data.is_empty() {
|
||||
return Err((StatusCode::BAD_REQUEST, "图片数据为空".to_string()));
|
||||
return Err(AppError::bad_request("图片数据为空"));
|
||||
}
|
||||
if !img.mime_type.starts_with("image/") {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("不支持的图片类型: {}", img.mime_type),
|
||||
));
|
||||
return Err(AppError::bad_request(format!(
|
||||
"不支持的图片类型: {}",
|
||||
img.mime_type
|
||||
)));
|
||||
}
|
||||
if state.vision_llm.is_none() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"图片分析功能未启用。请配置 LLM_VISION_MODEL 环境变量后重试。".to_string(),
|
||||
return Err(AppError::bad_request(
|
||||
"图片分析功能未启用。请配置 LLM_VISION_MODEL 环境变量后重试。",
|
||||
));
|
||||
}
|
||||
let ext = img.mime_type.strip_prefix("image/").unwrap_or("png");
|
||||
@@ -130,27 +129,16 @@ pub async fn chat_agent(
|
||||
.library_dir
|
||||
.join(".agent_images")
|
||||
.join("uploads");
|
||||
std::fs::create_dir_all(&upload_dir).map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("创建上传目录失败: {}", e),
|
||||
)
|
||||
})?;
|
||||
std::fs::create_dir_all(&upload_dir)
|
||||
.map_err(|e| AppError::internal(format!("创建上传目录失败: {}", e)))?;
|
||||
let filename = format!("{}.{}", uuid::Uuid::new_v4(), ext);
|
||||
let filepath = upload_dir.join(&filename);
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let bytes = general_purpose::STANDARD.decode(&img.data).map_err(|e| {
|
||||
(
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("图片 base64 解码失败: {}", e),
|
||||
)
|
||||
})?;
|
||||
std::fs::write(&filepath, &bytes).map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("保存图片失败: {}", e),
|
||||
)
|
||||
})?;
|
||||
let bytes = general_purpose::STANDARD
|
||||
.decode(&img.data)
|
||||
.map_err(|e| AppError::bad_request(format!("图片 base64 解码失败: {}", e)))?;
|
||||
std::fs::write(&filepath, &bytes)
|
||||
.map_err(|e| AppError::internal(format!("保存图片失败: {}", e)))?;
|
||||
let rel = filepath
|
||||
.strip_prefix(&state.config.library_dir)
|
||||
.unwrap_or(&filepath)
|
||||
@@ -244,7 +232,7 @@ pub struct SessionSummary {
|
||||
pub async fn list_sessions(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(params): Query<SessionListParams>,
|
||||
) -> Result<Json<Vec<SessionSummary>>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<Vec<SessionSummary>>> {
|
||||
let limit = params.limit.unwrap_or(50);
|
||||
let offset = params.offset.unwrap_or(0);
|
||||
|
||||
@@ -259,12 +247,7 @@ pub async fn list_sessions(
|
||||
.bind(offset)
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("查询会话列表失败: {}", e),
|
||||
)
|
||||
})?;
|
||||
.map_err(|e| AppError::internal(format!("查询会话列表失败: {}", e)))?;
|
||||
|
||||
let sessions: Vec<SessionSummary> = rows
|
||||
.iter()
|
||||
@@ -311,7 +294,7 @@ pub struct MessageRecord {
|
||||
pub async fn get_session(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> Result<Json<SessionDetail>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<SessionDetail>> {
|
||||
// 查询会话元信息
|
||||
let session_row = sqlx::query(
|
||||
"SELECT session_id, title, model, mode, turn_count, summary, created_at, updated_at \
|
||||
@@ -321,13 +304,8 @@ pub async fn get_session(
|
||||
.bind(&session_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("查询会话失败: {}", e),
|
||||
)
|
||||
})?
|
||||
.ok_or((StatusCode::NOT_FOUND, format!("会话 {} 不存在", session_id)))?;
|
||||
.map_err(|e| AppError::internal(format!("查询会话失败: {}", e)))?
|
||||
.ok_or_else(|| AppError::not_found(format!("会话 {} 不存在", session_id)))?;
|
||||
|
||||
let session = SessionSummary {
|
||||
session_id: session_row.get(0),
|
||||
@@ -350,7 +328,7 @@ pub async fn get_session(
|
||||
.bind(&session_id)
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("查询消息列表失败: {}", e)))?;
|
||||
.map_err(|e| AppError::internal(format!("查询消息列表失败: {}", e)))?;
|
||||
|
||||
let messages: Vec<MessageRecord> = msg_rows
|
||||
.iter()
|
||||
@@ -384,20 +362,20 @@ pub async fn get_session(
|
||||
pub async fn delete_session(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<serde_json::Value>> {
|
||||
let result = sqlx::query(
|
||||
"UPDATE agent_sessions SET deleted_at = CURRENT_TIMESTAMP WHERE session_id = ? AND deleted_at IS NULL"
|
||||
)
|
||||
.bind(&session_id)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("删除会话失败: {}", e)))?;
|
||||
.map_err(|e| AppError::internal(format!("删除会话失败: {}", e)))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("会话 {} 不存在或已删除", session_id),
|
||||
));
|
||||
return Err(AppError::not_found(format!(
|
||||
"会话 {} 不存在或已删除",
|
||||
session_id
|
||||
)));
|
||||
}
|
||||
|
||||
info!("会话已软删除: {}", session_id);
|
||||
@@ -411,7 +389,7 @@ pub async fn delete_session(
|
||||
pub async fn stop_agent(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<serde_json::Value>> {
|
||||
if let Ok(mut cancelled) = state.cancelled_runs.lock() {
|
||||
cancelled.insert(session_id.clone());
|
||||
}
|
||||
@@ -435,7 +413,7 @@ pub struct AgentMetricsResponse {
|
||||
|
||||
pub async fn get_agent_metrics(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<AgentMetricsResponse>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<AgentMetricsResponse>> {
|
||||
// 总会话数
|
||||
let total_sessions: i64 =
|
||||
sqlx::query_scalar("SELECT COUNT(*) FROM agent_sessions WHERE deleted_at IS NULL")
|
||||
@@ -524,7 +502,7 @@ pub struct AuditLogEntry {
|
||||
pub async fn get_session_audit(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> Result<Json<Vec<AuditLogEntry>>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<Vec<AuditLogEntry>>> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT id, step, tool_name, status, elapsed_ms, output_preview, created_at \
|
||||
FROM agent_audit_log \
|
||||
@@ -534,12 +512,7 @@ pub async fn get_session_audit(
|
||||
.bind(&session_id)
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("查询审计日志失败: {}", e),
|
||||
)
|
||||
})?;
|
||||
.map_err(|e| AppError::internal(format!("查询审计日志失败: {}", e)))?;
|
||||
|
||||
let entries: Vec<AuditLogEntry> = rows
|
||||
.iter()
|
||||
@@ -570,16 +543,13 @@ pub struct AnswerQuestionRequest {
|
||||
pub async fn answer_question(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<AnswerQuestionRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<serde_json::Value>> {
|
||||
use crate::agent::tools::ask_user::UserAnswer;
|
||||
|
||||
let mut pending = match state.pending_questions.lock() {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"服务器内部状态异常,请稍后重试".to_string(),
|
||||
));
|
||||
return Err(AppError::internal("服务器内部状态异常,请稍后重试"));
|
||||
}
|
||||
};
|
||||
let question_id = req.question_id.clone();
|
||||
@@ -598,13 +568,13 @@ pub async fn answer_question(
|
||||
serde_json::json!({"status": "ok", "question_id": question_id}),
|
||||
))
|
||||
}
|
||||
Err(_) => Err((StatusCode::GONE, "问题已超时或已被回答".to_string())),
|
||||
Err(_) => Err(AppError::gone("问题已超时或已被回答")),
|
||||
}
|
||||
}
|
||||
None => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("未找到待回答问题: {}", question_id),
|
||||
)),
|
||||
None => Err(AppError::not_found(format!(
|
||||
"未找到待回答问题: {}",
|
||||
question_id
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,10 +605,10 @@ pub async fn respond_permission(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
Json(req): Json<super::PermissionResponse>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<serde_json::Value>> {
|
||||
let mut perms = match state.pending_permissions.lock() {
|
||||
Ok(p) => p,
|
||||
Err(_) => return Err((StatusCode::INTERNAL_SERVER_ERROR, "内部状态异常".into())),
|
||||
Err(_) => return Err(AppError::internal("内部状态异常")),
|
||||
};
|
||||
|
||||
// 按 tool_call_id 查找匹配的权限请求
|
||||
@@ -658,12 +628,11 @@ pub async fn respond_permission(
|
||||
);
|
||||
Ok(Json(serde_json::json!({"status": "ok"})))
|
||||
}
|
||||
Err(_) => Err((StatusCode::GONE, "权限请求已超时或已处理".into())),
|
||||
Err(_) => Err(AppError::gone("权限请求已超时或已处理")),
|
||||
}
|
||||
}
|
||||
None => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
"未找到该权限请求(可能已超时或已处理)".into(),
|
||||
None => Err(AppError::not_found(
|
||||
"未找到该权限请求(可能已超时或已处理)",
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -706,10 +675,10 @@ pub struct BranchResponse {
|
||||
pub async fn branch_session(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> Result<Json<BranchResponse>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<BranchResponse>> {
|
||||
let result = crate::agent::runtime::session::branch_session(&state.db, &session_id)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
|
||||
.map_err(|e| AppError::bad_request(e.to_string()))?;
|
||||
|
||||
Ok(Json(BranchResponse {
|
||||
branch_session_id: result.branch_session_id,
|
||||
@@ -735,11 +704,11 @@ pub struct RetryResponse {
|
||||
pub async fn retry_session(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> Result<Json<RetryResponse>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<RetryResponse>> {
|
||||
let (retried_message, new_turn_index, image_path) =
|
||||
crate::agent::runtime::session::retry_last_turn(&state.db, &session_id)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
|
||||
.map_err(|e| AppError::bad_request(e.to_string()))?;
|
||||
|
||||
Ok(Json(RetryResponse {
|
||||
retried_message,
|
||||
@@ -777,16 +746,16 @@ pub async fn rewind_session(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
Json(req): Json<RewindRequest>,
|
||||
) -> Result<Json<RewindResponse>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<RewindResponse>> {
|
||||
let result = if let Some(msg_id) = req.message_id {
|
||||
crate::agent::runtime::session::rewind_to_message(&state.db, &session_id, msg_id)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?
|
||||
.map_err(|e| AppError::bad_request(e.to_string()))?
|
||||
} else {
|
||||
let n = req.n.unwrap_or(1);
|
||||
crate::agent::runtime::session::rewind_n_turns(&state.db, &session_id, n)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?
|
||||
.map_err(|e| AppError::bad_request(e.to_string()))?
|
||||
};
|
||||
|
||||
Ok(Json(RewindResponse {
|
||||
@@ -810,10 +779,10 @@ pub struct RestoreResponse {
|
||||
pub async fn restore_rewound_session(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(session_id): Path<String>,
|
||||
) -> Result<Json<RestoreResponse>, (StatusCode, String)> {
|
||||
) -> ApiResult<Json<RestoreResponse>> {
|
||||
let count = crate::agent::runtime::session::restore_rewound(&state.db, &session_id)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::CONFLICT, e.to_string()))?;
|
||||
.map_err(|e| AppError::conflict(e.to_string()))?;
|
||||
|
||||
Ok(Json(RestoreResponse {
|
||||
restored_count: count,
|
||||
|
||||
Reference in New Issue
Block a user