feat: Agent 全栈升级——模块化重构、Hooks/Skills/Memory/SubAgent/Team 子系统、审计与任务持久化
架构重构: - Agent Runtime 由单文件拆为 runtime/ 目录 12 模块(熔断/流式执行/Token预算/文件缓存/权限等) - Agent Tools 由单文件拆为 tools/ 目录 20+ 模块(filesystem/astro/memory/skill/subagent/team 等) - 解析器体系重构(common.rs 836行变更),各解析器同步升级 - Download 服务重构(562行),反爬策略强化 - LLM 客户端重构(266行),流式调用优化 新子系统: - Hooks 生命周期系统(9种事件类型,PreToolUse/PostToolUse 支持输入输出拦截) - Skills 双层加载系统(system-reminder 轻量注入 + LoadSkillTool 按需加载,notify 文件监听热更新) - Memory 项目记忆管理(类型/提取/去重/衰减/保活/选择策略/护栏 7 模块) - SubAgent 上下文隔离子代理运行器(独立 ReAct 循环 + Hook 管道) - Team 多智能体团队协作(文件 inbox 通信、lead/teammate 协调) - TaskBoard DAG 任务依赖管理 - Trajectory 会话轨迹、Terminal 终止信号、Autonomous 自主模式、Background 异步通知 数据库: - agent_tasks 表(DAG 依赖模式,blocked_by JSON 数组) - agent_audit_log 表(工具调用审计:名称/状态/耗时/输出预览) - agent_identity 迁移(消息/审计/任务的 agent_name 归属,agent_team_members 团队注册表) API: - GET /chat/metrics 聚合指标端点 - GET /chat/sessions/:id/audit 会话审计查询 - GET /chat/questions + POST /chat/answer 人机交互问答 工程: - 新增依赖:serde_yaml、notify、glob、walkdir、lru - Skills 目录含 methodology/plotting/presentation 三个初始 SKILL.md - CLAUDE.md 完整项目架构文档
This commit is contained in:
+261
-30
@@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
|
||||
use sqlx::Row;
|
||||
use std::convert::Infallible;
|
||||
use std::sync::Arc;
|
||||
use tracing::{info, error};
|
||||
use tracing::{error, info};
|
||||
|
||||
use super::AppState;
|
||||
use crate::agent::runtime::{AgentRuntime, AgentStreamEvent};
|
||||
@@ -32,7 +32,10 @@ pub async fn chat_agent(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<AgentChatRequest>,
|
||||
) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, (StatusCode, String)> {
|
||||
info!("接收到智能体对话请求: question='{}', session_id={:?}", req.question, req.session_id);
|
||||
info!(
|
||||
"接收到智能体对话请求: question='{}', session_id={:?}",
|
||||
req.question, req.session_id
|
||||
);
|
||||
|
||||
let runtime = AgentRuntime::new(Arc::clone(&state));
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<AgentStreamEvent>();
|
||||
@@ -102,24 +105,30 @@ pub async fn list_sessions(
|
||||
FROM agent_sessions \
|
||||
WHERE deleted_at IS NULL \
|
||||
ORDER BY updated_at DESC \
|
||||
LIMIT ? OFFSET ?"
|
||||
LIMIT ? OFFSET ?",
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("查询会话列表失败: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("查询会话列表失败: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let sessions: Vec<SessionSummary> = rows.iter().map(|r| {
|
||||
SessionSummary {
|
||||
let sessions: Vec<SessionSummary> = rows
|
||||
.iter()
|
||||
.map(|r| SessionSummary {
|
||||
session_id: r.get(0),
|
||||
title: r.get(1),
|
||||
model: r.get(2),
|
||||
turn_count: r.get(3),
|
||||
created_at: r.get(4),
|
||||
updated_at: r.get(5),
|
||||
}
|
||||
}).collect();
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(sessions))
|
||||
}
|
||||
@@ -156,12 +165,17 @@ pub async fn get_session(
|
||||
let session_row = sqlx::query(
|
||||
"SELECT session_id, title, model, turn_count, created_at, updated_at \
|
||||
FROM agent_sessions \
|
||||
WHERE session_id = ? AND deleted_at IS NULL"
|
||||
WHERE session_id = ? AND deleted_at IS NULL",
|
||||
)
|
||||
.bind(&session_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("查询会话失败: {}", e)))?
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("查询会话失败: {}", e),
|
||||
)
|
||||
})?
|
||||
.ok_or((StatusCode::NOT_FOUND, format!("会话 {} 不存在", session_id)))?;
|
||||
|
||||
let session = SessionSummary {
|
||||
@@ -185,24 +199,27 @@ pub async fn get_session(
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("查询消息列表失败: {}", e)))?;
|
||||
|
||||
let messages: Vec<MessageRecord> = msg_rows.iter().map(|r| {
|
||||
let tool_calls_json: Option<String> = r.get(6);
|
||||
let metadata_json: Option<String> = r.get(9);
|
||||
let messages: Vec<MessageRecord> = msg_rows
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let tool_calls_json: Option<String> = r.get(6);
|
||||
let metadata_json: Option<String> = r.get(9);
|
||||
|
||||
MessageRecord {
|
||||
id: r.get(0),
|
||||
turn_index: r.get(1),
|
||||
step_index: r.get(2),
|
||||
role: r.get(3),
|
||||
content: r.get(4),
|
||||
thought: r.get(5),
|
||||
tool_calls: tool_calls_json.and_then(|s| serde_json::from_str(&s).ok()),
|
||||
tool_call_id: r.get(7),
|
||||
token_count: r.get(8),
|
||||
metadata: metadata_json.and_then(|s| serde_json::from_str(&s).ok()),
|
||||
created_at: r.get(10),
|
||||
}
|
||||
}).collect();
|
||||
MessageRecord {
|
||||
id: r.get(0),
|
||||
turn_index: r.get(1),
|
||||
step_index: r.get(2),
|
||||
role: r.get(3),
|
||||
content: r.get(4),
|
||||
thought: r.get(5),
|
||||
tool_calls: tool_calls_json.and_then(|s| serde_json::from_str(&s).ok()),
|
||||
tool_call_id: r.get(7),
|
||||
token_count: r.get(8),
|
||||
metadata: metadata_json.and_then(|s| serde_json::from_str(&s).ok()),
|
||||
created_at: r.get(10),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(SessionDetail { session, messages }))
|
||||
}
|
||||
@@ -223,11 +240,16 @@ pub async fn delete_session(
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("删除会话失败: {}", e)))?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err((StatusCode::NOT_FOUND, format!("会话 {} 不存在或已删除", session_id)));
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("会话 {} 不存在或已删除", session_id),
|
||||
));
|
||||
}
|
||||
|
||||
info!("会话已软删除: {}", session_id);
|
||||
Ok(Json(serde_json::json!({ "status": "deleted", "session_id": session_id })))
|
||||
Ok(Json(
|
||||
serde_json::json!({ "status": "deleted", "session_id": session_id }),
|
||||
))
|
||||
}
|
||||
|
||||
// ── POST /api/chat/sessions/:id/stop ──
|
||||
@@ -240,5 +262,214 @@ pub async fn stop_agent(
|
||||
cancelled.insert(session_id.clone());
|
||||
}
|
||||
info!("已接收并记录手动中止请求,会话 ID: {}", session_id);
|
||||
Ok(Json(serde_json::json!({ "status": "stopping", "session_id": session_id })))
|
||||
Ok(Json(
|
||||
serde_json::json!({ "status": "stopping", "session_id": session_id }),
|
||||
))
|
||||
}
|
||||
|
||||
// ── 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>>,
|
||||
) -> Result<Json<AgentMetricsResponse>, (StatusCode, String)> {
|
||||
// 总会话数
|
||||
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);
|
||||
|
||||
// 工具调用统计(从审计日志聚合)
|
||||
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,
|
||||
}))
|
||||
}
|
||||
|
||||
// ── 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>,
|
||||
) -> Result<Json<Vec<AuditLogEntry>>, (StatusCode, String)> {
|
||||
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| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
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();
|
||||
|
||||
Ok(Json(entries))
|
||||
}
|
||||
|
||||
// ── POST /api/chat/answer_question ──
|
||||
// 用户回答 Agent 的提问(ask_user 工具配合使用)
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AnswerQuestionRequest {
|
||||
pub question_id: String,
|
||||
pub answers: Vec<String>,
|
||||
pub free_text: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn answer_question(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<AnswerQuestionRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
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(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let question_id = req.question_id.clone();
|
||||
|
||||
match pending.remove(&question_id) {
|
||||
Some(pq) => {
|
||||
let answer = UserAnswer {
|
||||
question_id: question_id.clone(),
|
||||
answers: req.answers.clone(),
|
||||
free_text: req.free_text.clone(),
|
||||
};
|
||||
match pq.answer_tx.send(answer) {
|
||||
Ok(()) => {
|
||||
info!("[API] 用户回答了问题: id={}", question_id);
|
||||
Ok(Json(
|
||||
serde_json::json!({"status": "ok", "question_id": question_id}),
|
||||
))
|
||||
}
|
||||
Err(_) => Err((StatusCode::GONE, "问题已超时或已被回答".to_string())),
|
||||
}
|
||||
}
|
||||
None => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("未找到待回答问题: {}", question_id),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// ── GET /api/chat/pending_questions ──
|
||||
// 获取当前待回答的问题(前端轮询或初始化)
|
||||
|
||||
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 questions: Vec<serde_json::Value> = pending
|
||||
.iter()
|
||||
.map(|(id, pq)| {
|
||||
serde_json::from_str::<serde_json::Value>(&pq.question_json)
|
||||
.unwrap_or(serde_json::json!({"question_id": id}))
|
||||
})
|
||||
.collect();
|
||||
Json(questions)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user