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:
+33
-268
@@ -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)| {
|
||||
|
||||
Reference in New Issue
Block a user