//! Agent 配置模板业务逻辑 use sqlx::PgPool; use crate::error::{SaasError, SaasResult}; use super::types::*; fn row_to_template( row: (String, String, Option, String, String, Option, Option, String, String, Option, Option, String, String, i32, String, String), ) -> AgentTemplateInfo { AgentTemplateInfo { id: row.0, name: row.1, description: row.2, category: row.3, source: row.4, model: row.5, system_prompt: row.6, tools: serde_json::from_str(&row.7).unwrap_or_default(), capabilities: serde_json::from_str(&row.8).unwrap_or_default(), temperature: row.9, max_tokens: row.10, visibility: row.11, status: row.12, current_version: row.13, created_at: row.14, updated_at: row.15, } } /// 创建 Agent 模板 pub async fn create_template( db: &PgPool, name: &str, description: Option<&str>, category: &str, source: &str, model: Option<&str>, system_prompt: Option<&str>, tools: &[String], capabilities: &[String], temperature: Option, max_tokens: Option, visibility: &str, ) -> SaasResult { let id = uuid::Uuid::new_v4().to_string(); let now = chrono::Utc::now().to_rfc3339(); let tools_json = serde_json::to_string(tools).unwrap_or_else(|_| "[]".to_string()); let caps_json = serde_json::to_string(capabilities).unwrap_or_else(|_| "[]".to_string()); sqlx::query( "INSERT INTO agent_templates (id, name, description, category, source, model, system_prompt, tools, capabilities, temperature, max_tokens, visibility, status, current_version, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'active', 1, $13, $13)" ) .bind(&id).bind(name).bind(description).bind(category).bind(source) .bind(model).bind(system_prompt).bind(&tools_json).bind(&caps_json) .bind(temperature).bind(max_tokens).bind(visibility).bind(&now) .execute(db).await.map_err(|e| { if e.to_string().contains("unique") { SaasError::AlreadyExists(format!("Agent 模板 '{}' 已存在", name)) } else { SaasError::Database(e) } })?; get_template(db, &id).await } /// 获取单个模板 pub async fn get_template(db: &PgPool, id: &str) -> SaasResult { let row: Option<_> = sqlx::query_as( "SELECT id, name, description, category, source, model, system_prompt, tools, capabilities, temperature, max_tokens, visibility, status, current_version, created_at, updated_at FROM agent_templates WHERE id = $1" ).bind(id).fetch_optional(db).await?; row.map(row_to_template) .ok_or_else(|| SaasError::NotFound(format!("Agent 模板 {} 不存在", id))) } /// 列出模板(分页 + 过滤) /// 使用动态参数化查询,安全拼接 WHERE 条件。 pub async fn list_templates( db: &PgPool, query: &AgentTemplateListQuery, ) -> SaasResult> { let page = query.page.unwrap_or(1).max(1); let page_size = query.page_size.unwrap_or(20).min(100); let offset = ((page - 1) * page_size) as i64; // 动态构建参数化 WHERE 子句 let mut conditions: Vec = vec!["1=1".to_string()]; let mut param_idx = 1u32; let mut cat_bind: Option = None; let mut src_bind: Option = None; let mut vis_bind: Option = None; let mut st_bind: Option = None; if let Some(ref cat) = query.category { param_idx += 1; conditions.push(format!("category = ${}", param_idx)); cat_bind = Some(cat.clone()); } if let Some(ref src) = query.source { param_idx += 1; conditions.push(format!("source = ${}", param_idx)); src_bind = Some(src.clone()); } if let Some(ref vis) = query.visibility { param_idx += 1; conditions.push(format!("visibility = ${}", param_idx)); vis_bind = Some(vis.clone()); } if let Some(ref st) = query.status { param_idx += 1; conditions.push(format!("status = ${}", param_idx)); st_bind = Some(st.clone()); } let where_clause = conditions.join(" AND "); // COUNT 查询: WHERE 参数绑定 ($1..$N) let count_idx = param_idx; let count_sql = format!( "SELECT COUNT(*) FROM agent_templates WHERE {}", where_clause ); let count_limit_idx = count_idx + 1; let count_offset_idx = count_limit_idx + 1; let data_sql = format!( "SELECT id, name, description, category, source, model, system_prompt, tools, capabilities, temperature, max_tokens, visibility, status, current_version, created_at, updated_at FROM agent_templates WHERE {} ORDER BY created_at DESC LIMIT ${} OFFSET ${}", where_clause, count_limit_idx, count_offset_idx ); // 构建 COUNT 查询并绑定参数 let mut count_q = sqlx::query_scalar::<_, i64>(&count_sql); if let Some(ref v) = cat_bind { count_q = count_q.bind(v); } if let Some(ref v) = src_bind { count_q = count_q.bind(v); } if let Some(ref v) = vis_bind { count_q = count_q.bind(v); } if let Some(ref v) = st_bind { count_q = count_q.bind(v); } let total: i64 = count_q.fetch_one(db).await?; // 构建数据查询并绑定参数 let mut data_q = sqlx::query_as::<_, ( String, String, Option, String, String, Option, Option, String, String, Option, Option, String, String, i32, String, String )>(&data_sql); if let Some(ref v) = cat_bind { data_q = data_q.bind(v); } if let Some(ref v) = src_bind { data_q = data_q.bind(v); } if let Some(ref v) = vis_bind { data_q = data_q.bind(v); } if let Some(ref v) = st_bind { data_q = data_q.bind(v); } data_q = data_q.bind(page_size as i64).bind(offset); let rows = data_q.fetch_all(db).await?; let items = rows.into_iter().map(row_to_template).collect(); Ok(crate::common::PaginatedResponse { items, total, page, page_size }) } /// 更新模板 /// 使用动态参数化查询,安全拼接 SET 子句。 pub async fn update_template( db: &PgPool, id: &str, description: Option<&str>, model: Option<&str>, system_prompt: Option<&str>, tools: Option<&[String]>, capabilities: Option<&[String]>, temperature: Option, max_tokens: Option, visibility: Option<&str>, status: Option<&str>, ) -> SaasResult { // 确认存在 get_template(db, id).await?; let now = chrono::Utc::now().to_rfc3339(); let mut set_clauses: Vec = vec![]; let mut param_idx = 1u32; // 收集需要绑定的值(按顺序) let mut desc_val: Option = None; let mut model_val: Option = None; let mut sp_val: Option = None; let mut tools_val: Option = None; let mut caps_val: Option = None; let mut temp_val: Option = None; let mut mt_val: Option = None; let mut vis_val: Option = None; let mut st_val: Option = None; if let Some(desc) = description { param_idx += 1; set_clauses.push(format!("description = ${}", param_idx)); desc_val = Some(desc.to_string()); } if let Some(m) = model { param_idx += 1; set_clauses.push(format!("model = ${}", param_idx)); model_val = Some(m.to_string()); } if let Some(sp) = system_prompt { param_idx += 1; set_clauses.push(format!("system_prompt = ${}", param_idx)); sp_val = Some(sp.to_string()); } if let Some(t) = tools { let json = serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string()); param_idx += 1; set_clauses.push(format!("tools = ${}", param_idx)); tools_val = Some(json); } if let Some(c) = capabilities { let json = serde_json::to_string(c).unwrap_or_else(|_| "[]".to_string()); param_idx += 1; set_clauses.push(format!("capabilities = ${}", param_idx)); caps_val = Some(json); } if let Some(t) = temperature { param_idx += 1; set_clauses.push(format!("temperature = ${}", param_idx)); temp_val = Some(t); } if let Some(m) = max_tokens { param_idx += 1; set_clauses.push(format!("max_tokens = ${}", param_idx)); mt_val = Some(m); } if let Some(v) = visibility { param_idx += 1; set_clauses.push(format!("visibility = ${}", param_idx)); vis_val = Some(v.to_string()); } if let Some(s) = status { param_idx += 1; set_clauses.push(format!("status = ${}", param_idx)); st_val = Some(s.to_string()); } if set_clauses.is_empty() { return get_template(db, id).await; } // updated_at param_idx += 1; set_clauses.push(format!("updated_at = ${}", param_idx)); // WHERE id = $N let id_idx = param_idx + 1; let sql = format!( "UPDATE agent_templates SET {} WHERE id = ${}", set_clauses.join(", "), id_idx ); let mut q = sqlx::query(&sql); if let Some(ref v) = desc_val { q = q.bind(v); } if let Some(ref v) = model_val { q = q.bind(v); } if let Some(ref v) = sp_val { q = q.bind(v); } if let Some(ref v) = tools_val { q = q.bind(v); } if let Some(ref v) = caps_val { q = q.bind(v); } if let Some(v) = temp_val { q = q.bind(v); } if let Some(v) = mt_val { q = q.bind(v); } if let Some(ref v) = vis_val { q = q.bind(v); } if let Some(ref v) = st_val { q = q.bind(v); } q = q.bind(&now); q = q.bind(id); q.execute(db).await?; get_template(db, id).await } /// 归档模板 pub async fn archive_template(db: &PgPool, id: &str) -> SaasResult { update_template(db, id, None, None, None, None, None, None, None, None, Some("archived")).await }