feat(gateway): 新增 /api/tools 与 /api/subagents/update 端点

- /api/tools: 只读访问 ToolRegistry,返回 builtin + MCP 工具的 name/description/source

- /api/subagents/update: PUT 接口,调用 SubagentRuntime::update_subagent 写回 SUBAGENT.md frontmatter

- 路由表两处同步注册新端点
This commit is contained in:
oudecheng 2026-07-30 16:54:19 +08:00
parent 12f1094426
commit 89c444ad3f
2 changed files with 123 additions and 1 deletions

View File

@ -5,6 +5,7 @@ use std::sync::Arc;
use super::GatewayState; use super::GatewayState;
use crate::config::{Config, get_default_config_path}; use crate::config::{Config, get_default_config_path};
use crate::domain::CapabilityPolicy;
use crate::experts::{Expert, ExpertScope, ExpertWithStatus}; use crate::experts::{Expert, ExpertScope, ExpertWithStatus};
use crate::skills::SkillWithStatus; use crate::skills::SkillWithStatus;
use crate::tools::task::runtime::{SubagentScope, SubagentWithStatus}; use crate::tools::task::runtime::{SubagentScope, SubagentWithStatus};
@ -248,6 +249,52 @@ pub async fn skills_list(
}) })
} }
#[derive(Serialize)]
pub struct ToolInfo {
pub name: String,
pub description: String,
/// "builtin" 或 "mcp:{server_key}"
pub source: String,
}
#[derive(Serialize)]
pub struct ToolsListResponse {
pub total: usize,
pub tools: Vec<ToolInfo>,
}
/// GET /api/tools — Return all registered tools (builtin + MCP) with name/description/source.
/// 通过 SessionManager::tools() 只读访问 ToolRegistry不修改状态。
pub async fn tools_list(
State(state): State<Arc<GatewayState>>,
) -> Json<ToolsListResponse> {
let registry = state.session_manager.tools();
let tools: Vec<ToolInfo> = registry
.get_definitions()
.into_iter()
.map(|t| {
let source = if t.function.name.starts_with("mcp_") {
// mcp_{server}_{tool} → mcp:{server}
let parts: Vec<&str> = t.function.name.splitn(3, '_').collect();
if parts.len() == 3 {
format!("mcp:{}", parts[1])
} else {
"mcp".to_string()
}
} else {
"builtin".to_string()
};
ToolInfo {
name: t.function.name,
description: t.function.description,
source,
}
})
.collect();
let total = tools.len();
Json(ToolsListResponse { total, tools })
}
/// POST /api/skills/toggle — Enable or disable a specific skill /// POST /api/skills/toggle — Enable or disable a specific skill
pub async fn skills_toggle( pub async fn skills_toggle(
State(state): State<Arc<GatewayState>>, State(state): State<Arc<GatewayState>>,
@ -423,6 +470,69 @@ pub async fn subagents_toggle(
} }
} }
#[derive(Deserialize)]
pub struct SubagentUpdateRequest {
pub name: String,
pub description: Option<String>,
pub body: Option<String>,
#[serde(default)]
pub capability: Option<CapabilityPolicy>,
}
#[derive(Serialize)]
pub struct SubagentUpdateResponse {
pub success: bool,
pub subagent: Option<SubagentWithStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
/// PUT /api/subagents/update — Update subagent capability/description/body (writes back SUBAGENT.md)
pub async fn subagents_update(
State(state): State<Arc<GatewayState>>,
Json(req): Json<SubagentUpdateRequest>,
) -> Result<Json<SubagentUpdateResponse>, (StatusCode, String)> {
let updated = state
.subagent_runtime
.update_subagent(
&req.name,
req.description.as_deref(),
req.body.as_deref(),
req.capability.as_ref(),
true,
)
.map_err(|err| {
let status = if err.contains("not found") {
StatusCode::NOT_FOUND
} else if err.contains("builtin") {
StatusCode::BAD_REQUEST
} else {
StatusCode::INTERNAL_SERVER_ERROR
};
(status, err)
})?;
// 返回更新后的状态(含 disabled_in_scopes
let status = state
.subagent_runtime
.list_with_status()
.into_iter()
.find(|s| s.name == updated.name)
.unwrap_or_else(|| SubagentWithStatus {
name: updated.name.clone(),
description: updated.description.clone(),
source: updated.source.as_str().to_string(),
disabled_in_scopes: vec![],
capability: updated.capability.clone(),
});
Ok(Json(SubagentUpdateResponse {
success: true,
subagent: Some(status),
error: None,
}))
}
// ===================== Experts ===================== // ===================== Experts =====================
#[derive(Deserialize)] #[derive(Deserialize)]
@ -458,6 +568,8 @@ pub struct ExpertCreateRequest {
pub description: String, pub description: String,
pub body: String, pub body: String,
pub scope: String, pub scope: String,
#[serde(default)]
pub capability: CapabilityPolicy,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -466,6 +578,8 @@ pub struct ExpertUpdateRequest {
pub scope: String, pub scope: String,
pub description: Option<String>, pub description: Option<String>,
pub body: Option<String>, pub body: Option<String>,
#[serde(default)]
pub capability: Option<CapabilityPolicy>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -505,6 +619,8 @@ pub struct ExpertResponse {
pub body: String, pub body: String,
pub source: String, pub source: String,
pub path: String, pub path: String,
#[serde(default)]
pub capability: CapabilityPolicy,
} }
impl From<Expert> for ExpertResponse { impl From<Expert> for ExpertResponse {
@ -515,6 +631,7 @@ impl From<Expert> for ExpertResponse {
body: expert.body, body: expert.body,
source: expert.source.as_str().to_string(), source: expert.source.as_str().to_string(),
path: expert.path.display().to_string(), path: expert.path.display().to_string(),
capability: expert.capability,
} }
} }
} }
@ -625,7 +742,7 @@ pub async fn experts_create(
let expert = state let expert = state
.experts .experts
.create_expert(scope, &req.name, &req.description, &req.body, true) .create_expert(scope, &req.name, &req.description, &req.body, &req.capability, true)
.map_err(|err| { .map_err(|err| {
let status = if err.contains("already exists") { let status = if err.contains("already exists") {
StatusCode::CONFLICT StatusCode::CONFLICT
@ -653,6 +770,7 @@ pub async fn experts_update(
&req.name, &req.name,
req.description.as_deref(), req.description.as_deref(),
req.body.as_deref(), req.body.as_deref(),
req.capability.as_ref(),
true, true,
) )
.map_err(|err| { .map_err(|err| {

View File

@ -241,8 +241,10 @@ pub async fn run(
.route("/api/mcp/status", routing::get(http::mcp_status)) .route("/api/mcp/status", routing::get(http::mcp_status))
.route("/api/skills", routing::get(http::skills_list)) .route("/api/skills", routing::get(http::skills_list))
.route("/api/skills/toggle", routing::post(http::skills_toggle)) .route("/api/skills/toggle", routing::post(http::skills_toggle))
.route("/api/tools", routing::get(http::tools_list))
.route("/api/subagents", routing::get(http::subagents_list)) .route("/api/subagents", routing::get(http::subagents_list))
.route("/api/subagents/toggle", routing::post(http::subagents_toggle)) .route("/api/subagents/toggle", routing::post(http::subagents_toggle))
.route("/api/subagents/update", routing::put(http::subagents_update))
.route("/api/experts", routing::get(http::experts_list)) .route("/api/experts", routing::get(http::experts_list))
.route("/api/experts/toggle", routing::post(http::experts_toggle)) .route("/api/experts/toggle", routing::post(http::experts_toggle))
.route("/api/experts/create", routing::post(http::experts_create)) .route("/api/experts/create", routing::post(http::experts_create))
@ -262,8 +264,10 @@ pub async fn run(
.route("/api/mcp/status", routing::get(http::mcp_status)) .route("/api/mcp/status", routing::get(http::mcp_status))
.route("/api/skills", routing::get(http::skills_list)) .route("/api/skills", routing::get(http::skills_list))
.route("/api/skills/toggle", routing::post(http::skills_toggle)) .route("/api/skills/toggle", routing::post(http::skills_toggle))
.route("/api/tools", routing::get(http::tools_list))
.route("/api/subagents", routing::get(http::subagents_list)) .route("/api/subagents", routing::get(http::subagents_list))
.route("/api/subagents/toggle", routing::post(http::subagents_toggle)) .route("/api/subagents/toggle", routing::post(http::subagents_toggle))
.route("/api/subagents/update", routing::put(http::subagents_update))
.route("/api/experts", routing::get(http::experts_list)) .route("/api/experts", routing::get(http::experts_list))
.route("/api/experts/toggle", routing::post(http::experts_toggle)) .route("/api/experts/toggle", routing::post(http::experts_toggle))
.route("/api/experts/create", routing::post(http::experts_create)) .route("/api/experts/create", routing::post(http::experts_create))