refactor: 服务层抽象下沉、异步锁全栈迁移、客户端韧性加固与移动端适配

- 服务层拆分:删除 api/helpers.rs,新增 citation/note/session/pipeline/paper/vision 独立服务模块
  - Agent 工具精简:paper_content+paper_outline 合并为 paper.rs,图片分析逻辑下沉至 services/vision
  - 并发模型升级:std::sync::{Mutex,RwLock} → tokio::sync::{Mutex,RwLock},消除 async
  上下文中的阻塞风险
  - 客户端加固:HTTP 客户端统一超时配置、ADS 429 / arXiv 503 自动重试、构造函数返回 Result
  - 启动安全:全局 panic hook 日志化、空密码拒绝启动、向量表维度不匹配需显式确认
  - CLI 扩展:构建完整 AppState 复用服务层,新增 Content/Outline/Citations/Search/Process 子命令
  - 前端:移动端汉堡菜单、侧栏滑出面板、引用星系触屏手势(单指拖拽/双指缩放)
This commit is contained in:
fmq
2026-06-30 19:26:01 +08:00
parent c5fd5b0d66
commit f885c0a4a8
90 changed files with 5184 additions and 3916 deletions
+33 -268
View File
@@ -10,7 +10,6 @@ use axum::{
};
use futures_util::stream::Stream;
use serde::{Deserialize, Serialize};
use sqlx::Row;
use std::convert::Infallible;
use std::sync::Arc;
use tracing::{error, info};
@@ -129,7 +128,8 @@ pub async fn chat_agent(
.library_dir
.join(".agent_images")
.join("uploads");
std::fs::create_dir_all(&upload_dir)
tokio::fs::create_dir_all(&upload_dir)
.await
.map_err(|e| AppError::internal(format!("创建上传目录失败: {}", e)))?;
let filename = format!("{}.{}", uuid::Uuid::new_v4(), ext);
let filepath = upload_dir.join(&filename);
@@ -137,7 +137,8 @@ pub async fn chat_agent(
let bytes = general_purpose::STANDARD
.decode(&img.data)
.map_err(|e| AppError::bad_request(format!("图片 base64 解码失败: {}", e)))?;
std::fs::write(&filepath, &bytes)
tokio::fs::write(&filepath, &bytes)
.await
.map_err(|e| AppError::internal(format!("保存图片失败: {}", e)))?;
let rel = filepath
.strip_prefix(&state.config.library_dir)
@@ -241,143 +242,30 @@ pub struct SessionListParams {
pub offset: Option<i64>,
}
#[derive(Debug, Serialize)]
pub struct SessionSummary {
pub session_id: String,
pub title: String,
pub model: String,
pub mode: String,
pub turn_count: i32,
pub summary: Option<String>,
pub created_at: String,
pub updated_at: String,
}
pub async fn list_sessions(
State(state): State<Arc<AppState>>,
Query(params): Query<SessionListParams>,
) -> ApiResult<Json<Vec<SessionSummary>>> {
) -> ApiResult<Json<Vec<crate::services::session::SessionSummary>>> {
let limit = params.limit.unwrap_or(50);
let offset = params.offset.unwrap_or(0);
let rows = sqlx::query(
"SELECT session_id, title, model, mode, turn_count, summary, created_at, updated_at \
FROM agent_sessions \
WHERE deleted_at IS NULL \
ORDER BY updated_at DESC \
LIMIT ? OFFSET ?",
)
.bind(limit)
.bind(offset)
.fetch_all(&state.db)
.await
.map_err(|e| AppError::internal(format!("查询会话列表失败: {}", e)))?;
let sessions: Vec<SessionSummary> = rows
.iter()
.map(|r| SessionSummary {
session_id: r.get(0),
title: r.get(1),
model: r.get(2),
mode: r.get(3),
turn_count: r.get(4),
summary: r.get(5),
created_at: r.get(6),
updated_at: r.get(7),
})
.collect();
let sessions = crate::services::session::list_sessions_service(&state.db, limit, offset)
.await
.map_err(|e| AppError::internal(format!("查询会话列表失败: {}", e)))?;
Ok(Json(sessions))
}
// ── GET /api/chat/sessions/:id ──
// 获取单个会话的全部消息历史
#[derive(Debug, Serialize)]
pub struct SessionDetail {
pub session: SessionSummary,
pub messages: Vec<MessageRecord>,
}
#[derive(Debug, Serialize)]
pub struct MessageRecord {
pub id: i64,
pub agent_name: String,
pub turn_index: i32,
pub step_index: i32,
pub role: String,
pub content: String,
pub thought: Option<String>,
pub tool_calls: Option<serde_json::Value>,
pub tool_call_id: Option<String>,
pub token_count: i32,
pub metadata: Option<serde_json::Value>,
pub created_at: String,
}
pub async fn get_session(
State(state): State<Arc<AppState>>,
Path(session_id): Path<String>,
) -> ApiResult<Json<SessionDetail>> {
// 查询会话元信息
let session_row = sqlx::query(
"SELECT session_id, title, model, mode, turn_count, summary, created_at, updated_at \
FROM agent_sessions \
WHERE session_id = ? AND deleted_at IS NULL",
)
.bind(&session_id)
.fetch_optional(&state.db)
.await
.map_err(|e| AppError::internal(format!("查询会话失败: {}", e)))?
.ok_or_else(|| AppError::not_found(format!("会话 {} 不存在", session_id)))?;
) -> ApiResult<Json<crate::services::session::SessionDetail>> {
let detail = crate::services::session::get_session_detail_service(&state.db, &session_id)
.await
.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),
title: session_row.get(1),
model: session_row.get(2),
mode: session_row.get(3),
turn_count: session_row.get(4),
summary: session_row.get(5),
created_at: session_row.get(6),
updated_at: session_row.get(7),
};
// 查询消息列表(包含 lead 和 subagent 消息,前端按 agent_name/metadata 区分渲染)
let msg_rows = sqlx::query(
"SELECT id, agent_name, turn_index, step_index, role, content, thought, tool_calls, tool_call_id, token_count, metadata, created_at \
FROM agent_messages \
WHERE session_id = ? AND active = 1 \
ORDER BY id ASC"
)
.bind(&session_id)
.fetch_all(&state.db)
.await
.map_err(|e| AppError::internal(format!("查询消息列表失败: {}", e)))?;
let messages: Vec<MessageRecord> = msg_rows
.iter()
.map(|r| {
let tool_calls_json: Option<String> = r.get(7);
let metadata_json: Option<String> = r.get(10);
MessageRecord {
id: r.get(0),
agent_name: r.get(1),
turn_index: r.get(2),
step_index: r.get(3),
role: r.get(4),
content: r.get(5),
thought: r.get(6),
tool_calls: tool_calls_json.and_then(|s| serde_json::from_str(&s).ok()),
tool_call_id: r.get(8),
token_count: r.get(9),
metadata: metadata_json.and_then(|s| serde_json::from_str(&s).ok()),
created_at: r.get(11),
}
})
.collect();
Ok(Json(SessionDetail { session, messages }))
Ok(Json(detail))
}
// ── DELETE /api/chat/sessions/:id ──
@@ -387,15 +275,11 @@ pub async fn delete_session(
State(state): State<Arc<AppState>>,
Path(session_id): Path<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| AppError::internal(format!("删除会话失败: {}", e)))?;
let success = crate::services::session::delete_session_service(&state.db, &session_id)
.await
.map_err(|e| AppError::internal(format!("删除会话失败: {}", e)))?;
if result.rows_affected() == 0 {
if !success {
return Err(AppError::not_found(format!(
"会话 {} 不存在或已删除",
session_id
@@ -414,9 +298,8 @@ pub async fn stop_agent(
State(state): State<Arc<AppState>>,
Path(session_id): Path<String>,
) -> ApiResult<Json<serde_json::Value>> {
if let Ok(mut cancelled) = state.cancelled_runs.lock() {
cancelled.insert(session_id.clone());
}
let mut cancelled = state.cancelled_runs.lock().await;
cancelled.insert(session_id.clone());
info!("已接收并记录手动中止请求,会话 ID: {}", session_id);
Ok(Json(
serde_json::json!({ "status": "stopping", "session_id": session_id }),
@@ -426,130 +309,26 @@ pub async fn stop_agent(
// ── GET /api/chat/metrics ──
// 返回聚合的智能体运行指标
#[derive(Debug, Serialize)]
pub struct AgentMetricsResponse {
pub total_sessions: i64,
pub total_tool_calls: i64,
pub tool_call_breakdown: serde_json::Value,
pub avg_steps_per_session: f64,
pub error_rate: f64,
}
pub async fn get_agent_metrics(
State(state): State<Arc<AppState>>,
) -> ApiResult<Json<AgentMetricsResponse>> {
// 总会话数
let total_sessions: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM agent_sessions WHERE deleted_at IS NULL")
.fetch_one(&state.db)
.await
.unwrap_or(0);
) -> ApiResult<Json<crate::services::session::AgentMetrics>> {
let metrics = crate::services::session::get_agent_metrics_service(&state.db)
.await
.map_err(|e| AppError::internal(format!("获取系统指标失败: {}", e)))?;
// 工具调用统计(从审计日志聚合)
let total_tool_calls: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM agent_audit_log WHERE status IN ('OK', 'FAIL')")
.fetch_one(&state.db)
.await
.unwrap_or(0);
// 各工具调用次数
let breakdown_rows: Vec<(String, i64)> = sqlx::query_as(
"SELECT COALESCE(tool_name, 'unknown'), COUNT(*) as cnt \
FROM agent_audit_log \
WHERE status IN ('OK', 'FAIL') \
GROUP BY tool_name \
ORDER BY cnt DESC",
)
.fetch_all(&state.db)
.await
.unwrap_or_default();
let tool_call_breakdown: serde_json::Value = breakdown_rows
.iter()
.map(|(name, cnt)| serde_json::json!({ name: cnt }))
.fold(serde_json::json!({}), |mut acc, v| {
if let serde_json::Value::Object(map) = &mut acc {
if let serde_json::Value::Object(v_map) = v {
for (k, val) in v_map {
map.insert(k.clone(), val.clone());
}
}
}
acc
});
// 平均步数
let avg_steps: f64 = sqlx::query_scalar(
"SELECT COALESCE(AVG(CAST(turn_count AS REAL)), 0.0) \
FROM agent_sessions WHERE deleted_at IS NULL",
)
.fetch_one(&state.db)
.await
.unwrap_or(0.0);
// 错误率
let total_errors: i64 =
sqlx::query_scalar("SELECT COUNT(*) FROM agent_audit_log WHERE status = 'FAIL'")
.fetch_one(&state.db)
.await
.unwrap_or(0);
let error_rate = if total_tool_calls > 0 {
total_errors as f64 / total_tool_calls as f64
} else {
0.0
};
Ok(Json(AgentMetricsResponse {
total_sessions,
total_tool_calls,
tool_call_breakdown,
avg_steps_per_session: avg_steps,
error_rate,
}))
Ok(Json(metrics))
}
// ── GET /api/chat/sessions/:id/audit ──
// 返回指定会话的审计日志
#[derive(Debug, Serialize)]
pub struct AuditLogEntry {
pub id: i64,
pub step: i32,
pub tool_name: Option<String>,
pub status: String,
pub elapsed_ms: i32,
pub output_preview: Option<String>,
pub created_at: String,
}
pub async fn get_session_audit(
State(state): State<Arc<AppState>>,
Path(session_id): Path<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 \
WHERE session_id = ? \
ORDER BY id ASC",
)
.bind(&session_id)
.fetch_all(&state.db)
.await
.map_err(|e| AppError::internal(format!("查询审计日志失败: {}", e)))?;
let entries: Vec<AuditLogEntry> = rows
.iter()
.map(|r| AuditLogEntry {
id: r.get(0),
step: r.get(1),
tool_name: r.get(2),
status: r.get(3),
elapsed_ms: r.get(4),
output_preview: r.get(5),
created_at: r.get(6),
})
.collect();
) -> ApiResult<Json<Vec<crate::services::session::AuditLogEntry>>> {
let entries = crate::services::session::get_session_audit_service(&state.db, &session_id)
.await
.map_err(|e| AppError::internal(format!("查询审计日志失败: {}", e)))?;
Ok(Json(entries))
}
@@ -570,12 +349,7 @@ pub async fn answer_question(
) -> 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(AppError::internal("服务器内部状态异常,请稍后重试"));
}
};
let mut pending = state.pending_questions.lock().await;
let question_id = req.question_id.clone();
match pending.remove(&question_id) {
@@ -608,10 +382,7 @@ pub async fn answer_question(
pub async fn get_pending_questions(
State(state): State<Arc<AppState>>,
) -> Json<Vec<serde_json::Value>> {
let pending = match state.pending_questions.lock() {
Ok(p) => p,
Err(_) => return Json(Vec::new()),
};
let pending = state.pending_questions.lock().await;
let questions: Vec<serde_json::Value> = pending
.iter()
.map(|(id, pq)| {
@@ -630,10 +401,7 @@ pub async fn respond_permission(
Path(session_id): Path<String>,
Json(req): Json<super::PermissionResponse>,
) -> ApiResult<Json<serde_json::Value>> {
let mut perms = match state.pending_permissions.lock() {
Ok(p) => p,
Err(_) => return Err(AppError::internal("内部状态异常")),
};
let mut perms = state.pending_permissions.lock().await;
// 按 tool_call_id 查找匹配的权限请求
let perm_id = perms
@@ -667,10 +435,7 @@ pub async fn respond_permission(
pub async fn get_pending_permissions(
State(state): State<Arc<AppState>>,
) -> Json<Vec<serde_json::Value>> {
let perms = match state.pending_permissions.lock() {
Ok(p) => p,
Err(_) => return Json(Vec::new()),
};
let perms = state.pending_permissions.lock().await;
let result: Vec<serde_json::Value> = perms
.iter()
.map(|(id, p)| {