feat: Agent 思考模式前端可控、子代理全链路持久化、权限系统、工具 ID 追踪体系、前端面板与文档架构重构

- AgentConfig/LlmClient 新增 enable_thinking 参数,前端 SSE 请求传递 thinking
  开关,仅千问/DashScope 时启用
  - 完善权限系统,支持细粒度的权限控制和用户权限申请
  - delegate_research 工具重命名为 subagent,SubAgentTool/SubAgentRunner 重构
  - 子代理消息(system/user/assistant/tool)持久化到 agent_messages 表,带 agent_name 标识
  - 子代理活动日志(工具调用列表+思考摘要)注入返回结果,Hooks 获得正确 session_id 和 subagent_name
  - LLM 工具调用 ID 回退生成 UUID(llm.rs),ToolCall/ToolResult SSE 事件增加 id/tool_call_id 双字段
  - ToolContext 扩展 session_id/sse_tx/enable_thinking 字段,executor 统一注入而非构造函数传参
  - agent_messages 新增 metadata+raw_json 列,agent_sessions 暴露 summary 字段
  - 删除文件级 transcript 快照(compact.rs),改为依赖 DB 持久化
  - ResearchAgentPanel 重写:TimelineItem 类型替代 StreamStep,支持会话历史回放
  - 新增 AgentMetricsPanel/AskUserQuestionCard/AuditLogViewer 三个前端组件,types.ts 完整类型定义
  - docs/architecture/ 分层重组:概览/核心模块/核心工作流 + agent/ 子目录 11 篇专题文档
  - docs/api.md 补充 RAG/Target/Agent 接口,docs/development.md 新建开发指南
  - .env.example 完全重写,补充 FALLBACK_MODEL 等变量说明
This commit is contained in:
fmq
2026-06-18 01:21:02 +08:00
parent 49784739fa
commit f6df9d8136
60 changed files with 9913 additions and 1844 deletions
+92 -19
View File
@@ -26,6 +26,9 @@ use crate::agent::runtime::{AgentRuntime, AgentStreamEvent};
pub struct AgentChatRequest {
pub question: String,
pub session_id: Option<String>,
/// 是否启用 LLM 思考模式(默认关闭)
#[serde(default)]
pub thinking: bool,
}
pub async fn chat_agent(
@@ -37,7 +40,7 @@ pub async fn chat_agent(
req.question, req.session_id
);
let runtime = AgentRuntime::new(Arc::clone(&state));
let runtime = AgentRuntime::new(Arc::clone(&state)).with_thinking(req.thinking);
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<AgentStreamEvent>();
let question = req.question.clone();
@@ -89,6 +92,7 @@ pub struct SessionSummary {
pub title: String,
pub model: String,
pub turn_count: i32,
pub summary: Option<String>,
pub created_at: String,
pub updated_at: String,
}
@@ -101,7 +105,7 @@ pub async fn list_sessions(
let offset = params.offset.unwrap_or(0);
let rows = sqlx::query(
"SELECT session_id, title, model, turn_count, created_at, updated_at \
"SELECT session_id, title, model, turn_count, summary, created_at, updated_at \
FROM agent_sessions \
WHERE deleted_at IS NULL \
ORDER BY updated_at DESC \
@@ -125,8 +129,9 @@ pub async fn list_sessions(
title: r.get(1),
model: r.get(2),
turn_count: r.get(3),
created_at: r.get(4),
updated_at: r.get(5),
summary: r.get(4),
created_at: r.get(5),
updated_at: r.get(6),
})
.collect();
@@ -145,6 +150,7 @@ pub struct SessionDetail {
#[derive(Debug, Serialize)]
pub struct MessageRecord {
pub id: i64,
pub agent_name: String,
pub turn_index: i32,
pub step_index: i32,
pub role: String,
@@ -163,7 +169,7 @@ pub async fn get_session(
) -> Result<Json<SessionDetail>, (StatusCode, String)> {
// 查询会话元信息
let session_row = sqlx::query(
"SELECT session_id, title, model, turn_count, created_at, updated_at \
"SELECT session_id, title, model, turn_count, summary, created_at, updated_at \
FROM agent_sessions \
WHERE session_id = ? AND deleted_at IS NULL",
)
@@ -183,13 +189,14 @@ pub async fn get_session(
title: session_row.get(1),
model: session_row.get(2),
turn_count: session_row.get(3),
created_at: session_row.get(4),
updated_at: session_row.get(5),
summary: session_row.get(4),
created_at: session_row.get(5),
updated_at: session_row.get(6),
};
// 查询消息列表
// 查询消息列表(包含 lead 和 subagent 消息,前端按 agent_name/metadata 区分渲染)
let msg_rows = sqlx::query(
"SELECT id, turn_index, step_index, role, content, thought, tool_calls, tool_call_id, token_count, metadata, created_at \
"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 = ? \
ORDER BY id ASC"
@@ -202,21 +209,22 @@ pub async fn get_session(
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 tool_calls_json: Option<String> = r.get(7);
let metadata_json: Option<String> = r.get(10);
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),
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(7),
token_count: r.get(8),
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(10),
created_at: r.get(11),
}
})
.collect();
@@ -473,3 +481,68 @@ pub async fn get_pending_questions(
.collect();
Json(questions)
}
// ── POST /api/chat/sessions/:id/permissions/respond ──
// 用户响应权限请求
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)> {
let mut perms = match state.pending_permissions.lock() {
Ok(p) => p,
Err(_) => return Err((StatusCode::INTERNAL_SERVER_ERROR, "内部状态异常".into())),
};
// 按 tool_call_id 查找匹配的权限请求
let perm_id = perms
.iter()
.find(|(_, p)| p.tool_call_id == req.tool_call_id)
.map(|(id, _)| id.clone());
match perm_id {
Some(id) => {
let perm = perms.remove(&id).unwrap();
match perm.response_tx.send(req) {
Ok(()) => {
info!(
"[API] 用户响应了权限请求: session={} tool_call_id={}",
session_id, perm.tool_call_id
);
Ok(Json(serde_json::json!({"status": "ok"})))
}
Err(_) => Err((StatusCode::GONE, "权限请求已超时或已处理".into())),
}
}
None => Err((
StatusCode::NOT_FOUND,
"未找到该权限请求(可能已超时或已处理)".into(),
)),
}
}
// ── GET /api/chat/sessions/:id/permissions ──
// 获取当前待处理的权限请求(前端轮询)
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 result: Vec<serde_json::Value> = perms
.iter()
.map(|(id, p)| {
serde_json::json!({
"permission_id": id,
"tool_call_id": p.tool_call_id,
"tool_name": p.tool_name,
"message": p.message,
"arguments": p.arguments,
})
})
.collect();
Json(result)
}