diff --git a/crates/zclaw-saas/migrations/20260331000002_agent_templates_extensions.sql b/crates/zclaw-saas/migrations/20260331000002_agent_templates_extensions.sql new file mode 100644 index 0000000..bb961b8 --- /dev/null +++ b/crates/zclaw-saas/migrations/20260331000002_agent_templates_extensions.sql @@ -0,0 +1,16 @@ +-- 20260331000002_agent_templates_extensions.sql +-- 行业 Agent 模板扩展: soul_content, scenarios, welcome_message, quick_commands 等 + +ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS soul_content TEXT; +ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS scenarios TEXT NOT NULL DEFAULT '[]'; +ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS welcome_message TEXT; +ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS quick_commands TEXT NOT NULL DEFAULT '[]'; +ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS personality TEXT; +ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS communication_style TEXT; +ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS emoji TEXT; +ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1; +ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS source_id TEXT; + +-- source_id 唯一约束(仅非 NULL 值) +CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_templates_source_id + ON agent_templates(source_id) WHERE source_id IS NOT NULL; diff --git a/crates/zclaw-saas/src/agent_template/handlers.rs b/crates/zclaw-saas/src/agent_template/handlers.rs index 54b41f1..4fa966f 100644 --- a/crates/zclaw-saas/src/agent_template/handlers.rs +++ b/crates/zclaw-saas/src/agent_template/handlers.rs @@ -34,6 +34,8 @@ pub async fn create_template( let visibility = req.visibility.as_deref().unwrap_or("public"); let tools = req.tools.as_deref().unwrap_or(&[]); let capabilities = req.capabilities.as_deref().unwrap_or(&[]); + let scenarios = req.scenarios.as_deref().unwrap_or(&[]); + let quick_commands = req.quick_commands.as_deref().unwrap_or(&[]); let result = service::create_template( &state.db, &req.name, req.description.as_deref(), @@ -41,6 +43,14 @@ pub async fn create_template( req.system_prompt.as_deref(), tools, capabilities, req.temperature, req.max_tokens, visibility, + req.soul_content.as_deref(), + Some(scenarios), + req.welcome_message.as_deref(), + Some(quick_commands), + req.personality.as_deref(), + req.communication_style.as_deref(), + req.emoji.as_deref(), + req.source_id.as_deref(), ).await?; log_operation(&state.db, &ctx.account_id, "agent_template.create", "agent_template", &result.id, @@ -59,6 +69,26 @@ pub async fn get_template( Ok(Json(service::get_template(&state.db, &id).await?)) } +/// GET /api/v1/agent-templates/:id/full — 获取完整 Agent 模板(含扩展字段) +pub async fn get_full_template( + State(state): State, + Extension(ctx): Extension, + Path(id): Path, +) -> SaasResult> { + check_permission(&ctx, "model:read")?; + // Reuses the same get_template service which already returns all fields + Ok(Json(service::get_template(&state.db, &id).await?)) +} + +/// GET /api/v1/agent-templates/available — 列出公开可用模板(轻量级) +pub async fn list_available( + State(state): State, + Extension(ctx): Extension, +) -> SaasResult>> { + check_permission(&ctx, "model:read")?; + Ok(Json(service::list_available(&state.db).await?)) +} + /// POST /api/v1/agent-templates/:id — 更新 Agent 模板 pub async fn update_template( State(state): State, @@ -79,6 +109,13 @@ pub async fn update_template( req.max_tokens, req.visibility.as_deref(), req.status.as_deref(), + req.soul_content.as_deref(), + req.scenarios.as_deref(), + req.welcome_message.as_deref(), + req.quick_commands.as_deref(), + req.personality.as_deref(), + req.communication_style.as_deref(), + req.emoji.as_deref(), ).await?; log_operation(&state.db, &ctx.account_id, "agent_template.update", "agent_template", &id, diff --git a/crates/zclaw-saas/src/agent_template/mod.rs b/crates/zclaw-saas/src/agent_template/mod.rs index 7efcfad..f01cb1c 100644 --- a/crates/zclaw-saas/src/agent_template/mod.rs +++ b/crates/zclaw-saas/src/agent_template/mod.rs @@ -11,7 +11,9 @@ use crate::state::AppState; pub fn routes() -> axum::Router { axum::Router::new() .route("/api/v1/agent-templates", get(handlers::list_templates).post(handlers::create_template)) + .route("/api/v1/agent-templates/available", get(handlers::list_available)) .route("/api/v1/agent-templates/:id", get(handlers::get_template)) .route("/api/v1/agent-templates/:id", post(handlers::update_template)) .route("/api/v1/agent-templates/:id", delete(handlers::archive_template)) + .route("/api/v1/agent-templates/:id/full", get(handlers::get_full_template)) } diff --git a/crates/zclaw-saas/src/agent_template/service.rs b/crates/zclaw-saas/src/agent_template/service.rs index 555014d..d444255 100644 --- a/crates/zclaw-saas/src/agent_template/service.rs +++ b/crates/zclaw-saas/src/agent_template/service.rs @@ -1,26 +1,50 @@ //! Agent 配置模板业务逻辑 -use sqlx::PgPool; +use sqlx::{PgPool, Row}; 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 { +/// Shared SELECT column list. +const SELECT_COLUMNS: &str = "\ + id, name, description, category, source, model, system_prompt, \ + tools, capabilities, temperature, max_tokens, visibility, status, \ + current_version, created_at, updated_at, \ + soul_content, scenarios, welcome_message, quick_commands, \ + personality, communication_style, emoji, version, source_id"; + +fn row_to_template(row: &sqlx::postgres::PgRow) -> 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, + id: row.get("id"), + name: row.get("name"), + description: row.get("description"), + category: row.get("category"), + source: row.get("source"), + model: row.get("model"), + system_prompt: row.get("system_prompt"), + tools: serde_json::from_str(&row.get::("tools")).unwrap_or_default(), + capabilities: serde_json::from_str(&row.get::("capabilities")).unwrap_or_default(), + temperature: row.get("temperature"), + max_tokens: row.get("max_tokens"), + visibility: row.get("visibility"), + status: row.get("status"), + current_version: row.get("current_version"), + created_at: row.get("created_at"), + updated_at: row.get("updated_at"), + // Extended fields + soul_content: row.get("soul_content"), + scenarios: serde_json::from_str(&row.get::("scenarios")).unwrap_or_default(), + welcome_message: row.get("welcome_message"), + quick_commands: serde_json::from_str(&row.get::("quick_commands")).unwrap_or_default(), + personality: row.get("personality"), + communication_style: row.get("communication_style"), + emoji: row.get("emoji"), + version: row.get("version"), + source_id: row.get("source_id"), } } -/// Row type for agent_template queries (avoids multi-line turbofish parsing issues) -type AgentTemplateRow = (String, String, Option, String, String, Option, Option, String, String, Option, Option, String, String, i32, String, String); - /// 创建 Agent 模板 +#[allow(clippy::too_many_arguments)] pub async fn create_template( db: &PgPool, name: &str, @@ -34,20 +58,47 @@ pub async fn create_template( temperature: Option, max_tokens: Option, visibility: &str, + // Extended fields + soul_content: Option<&str>, + scenarios: Option<&[String]>, + welcome_message: Option<&str>, + quick_commands: Option<&[serde_json::Value]>, + personality: Option<&str>, + communication_style: Option<&str>, + emoji: Option<&str>, + source_id: Option<&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()); + let scenarios_json = serde_json::to_string(&scenarios.unwrap_or(&[])).unwrap_or_else(|_| "[]".to_string()); + let quick_commands_json = serde_json::to_string(&quick_commands.unwrap_or(&[])).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)" + &format!("INSERT INTO agent_templates ({}) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,'active',1,$13,$13,$14,$15,$16,$17,$18,$19,$20,1,$21)", SELECT_COLUMNS) ) - .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) + .bind(&id) // $1 id + .bind(name) // $2 name + .bind(description) // $3 description + .bind(category) // $4 category + .bind(source) // $5 source + .bind(model) // $6 model + .bind(system_prompt) // $7 system_prompt + .bind(&tools_json) // $8 tools + .bind(&caps_json) // $9 capabilities + .bind(temperature) // $10 temperature + .bind(max_tokens) // $11 max_tokens + .bind(visibility) // $12 visibility + .bind(&now) // $13 created_at / updated_at + .bind(soul_content) // $14 soul_content + .bind(&scenarios_json) // $15 scenarios + .bind(welcome_message) // $16 welcome_message + .bind(&quick_commands_json) // $17 quick_commands + .bind(personality) // $18 personality + .bind(communication_style) // $19 communication_style + .bind(emoji) // $20 emoji + .bind(source_id) // $21 source_id .execute(db).await.map_err(|e| { if e.to_string().contains("unique") { SaasError::AlreadyExists(format!("Agent 模板 '{}' 已存在", name)) @@ -61,14 +112,12 @@ pub async fn create_template( /// 获取单个模板 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" + let row = sqlx::query( + &format!("SELECT {} FROM agent_templates WHERE id = $1", SELECT_COLUMNS) ).bind(id).fetch_optional(db).await?; - row.map(row_to_template) + row.as_ref() + .map(row_to_template) .ok_or_else(|| SaasError::NotFound(format!("Agent 模板 {} 不存在", id))) } @@ -83,20 +132,21 @@ pub async fn list_templates( let page_size = query.page_size.unwrap_or(20).min(100); let offset = ((page - 1) * page_size) as i64; - let count_sql = "SELECT COUNT(*) FROM agent_templates WHERE ($1 IS NULL OR category = $1) AND ($2 IS NULL OR source = $2) AND ($3 IS NULL OR visibility = $3) AND ($4 IS NULL OR status = $4)"; - let data_sql = "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 ($1 IS NULL OR category = $1) AND ($2 IS NULL OR source = $2) AND ($3 IS NULL OR visibility = $3) AND ($4 IS NULL OR status = $4) ORDER BY created_at DESC LIMIT $5 OFFSET $6"; + let where_clause = "WHERE ($1 IS NULL OR category = $1) AND ($2 IS NULL OR source = $2) AND ($3 IS NULL OR visibility = $3) AND ($4 IS NULL OR status = $4)"; + let count_sql = format!("SELECT COUNT(*) FROM agent_templates {}", where_clause); + let data_sql = format!( + "SELECT {} FROM agent_templates {} ORDER BY created_at DESC LIMIT $5 OFFSET $6", + SELECT_COLUMNS, where_clause + ); - let total: i64 = sqlx::query_scalar(count_sql) + let total: i64 = sqlx::query_scalar(&count_sql) .bind(&query.category) .bind(&query.source) .bind(&query.visibility) .bind(&query.status) .fetch_one(db).await?; - let rows: Vec = sqlx::query_as(data_sql) + let rows = sqlx::query(&data_sql) .bind(&query.category) .bind(&query.source) .bind(&query.visibility) @@ -104,14 +154,37 @@ pub async fn list_templates( .bind(page_size as i64) .bind(offset) .fetch_all(db).await?; - let items = rows.into_iter().map(row_to_template).collect(); + let items = rows.iter().map(|r| row_to_template(r)).collect(); Ok(crate::common::PaginatedResponse { items, total, page, page_size }) } +/// 列出可用模板 (status='active' AND visibility='public', 轻量级) +pub async fn list_available(db: &PgPool) -> SaasResult> { + let rows = sqlx::query( + "SELECT id, name, category, emoji, description, source_id \ + FROM agent_templates \ + WHERE status = 'active' AND visibility = 'public' \ + ORDER BY category, name" + ).fetch_all(db).await?; + + Ok(rows.iter().map(|r| { + AvailableAgentTemplateInfo { + id: r.get("id"), + name: r.get("name"), + category: r.get("category"), + emoji: r.get("emoji"), + description: r.get("description"), + source_id: r.get("source_id"), + } + }).collect()) +} + /// 更新模板 /// COALESCE pattern: all updatable fields in a single static SQL. /// NULL parameters leave the column unchanged. +/// source_id and version are immutable. +#[allow(clippy::too_many_arguments)] pub async fn update_template( db: &PgPool, id: &str, @@ -124,6 +197,14 @@ pub async fn update_template( max_tokens: Option, visibility: Option<&str>, status: Option<&str>, + // Extended fields + soul_content: Option<&str>, + scenarios: Option<&[String]>, + welcome_message: Option<&str>, + quick_commands: Option<&[serde_json::Value]>, + personality: Option<&str>, + communication_style: Option<&str>, + emoji: Option<&str>, ) -> SaasResult { // Confirm existence get_template(db, id).await?; @@ -133,6 +214,8 @@ pub async fn update_template( // Serialize JSON fields upfront so we can bind Option<&str> consistently let tools_json = tools.map(|t| serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string())); let caps_json = capabilities.map(|c| serde_json::to_string(c).unwrap_or_else(|_| "[]".to_string())); + let scenarios_json = scenarios.map(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string())); + let quick_commands_json = quick_commands.map(|qc| serde_json::to_string(qc).unwrap_or_else(|_| "[]".to_string())); sqlx::query( "UPDATE agent_templates SET @@ -145,20 +228,34 @@ pub async fn update_template( max_tokens = COALESCE($7, max_tokens), visibility = COALESCE($8, visibility), status = COALESCE($9, status), - updated_at = $10 - WHERE id = $11" + updated_at = $10, + soul_content = COALESCE($11, soul_content), + scenarios = COALESCE($12, scenarios), + welcome_message = COALESCE($13, welcome_message), + quick_commands = COALESCE($14, quick_commands), + personality = COALESCE($15, personality), + communication_style = COALESCE($16, communication_style), + emoji = COALESCE($17, emoji) + WHERE id = $18" ) - .bind(description) - .bind(model) - .bind(system_prompt) - .bind(tools_json.as_deref()) - .bind(caps_json.as_deref()) - .bind(temperature) - .bind(max_tokens) - .bind(visibility) - .bind(status) - .bind(&now) - .bind(id) + .bind(description) // $1 + .bind(model) // $2 + .bind(system_prompt) // $3 + .bind(tools_json.as_deref()) // $4 + .bind(caps_json.as_deref()) // $5 + .bind(temperature) // $6 + .bind(max_tokens) // $7 + .bind(visibility) // $8 + .bind(status) // $9 + .bind(&now) // $10 + .bind(soul_content) // $11 + .bind(scenarios_json.as_deref()) // $12 + .bind(welcome_message) // $13 + .bind(quick_commands_json.as_deref()) // $14 + .bind(personality) // $15 + .bind(communication_style) // $16 + .bind(emoji) // $17 + .bind(id) // $18 .execute(db).await?; get_template(db, id).await @@ -166,5 +263,6 @@ pub async fn update_template( /// 归档模板 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 + update_template(db, id, None, None, None, None, None, None, None, None, Some("archived"), + None, None, None, None, None, None, None).await } diff --git a/crates/zclaw-saas/src/agent_template/types.rs b/crates/zclaw-saas/src/agent_template/types.rs index 7f630c0..1f78c1f 100644 --- a/crates/zclaw-saas/src/agent_template/types.rs +++ b/crates/zclaw-saas/src/agent_template/types.rs @@ -22,6 +22,16 @@ pub struct AgentTemplateInfo { pub current_version: i32, pub created_at: String, pub updated_at: String, + // Extended fields (migration 20260331000002) + pub soul_content: Option, + pub scenarios: Vec, + pub welcome_message: Option, + pub quick_commands: Vec, + pub personality: Option, + pub communication_style: Option, + pub emoji: Option, + pub version: i32, + pub source_id: Option, } #[derive(Debug, Deserialize)] @@ -37,6 +47,15 @@ pub struct CreateAgentTemplateRequest { pub temperature: Option, pub max_tokens: Option, pub visibility: Option, + // Extended fields + pub soul_content: Option, + pub scenarios: Option>, + pub welcome_message: Option, + pub quick_commands: Option>, + pub personality: Option, + pub communication_style: Option, + pub emoji: Option, + pub source_id: Option, } #[derive(Debug, Deserialize)] @@ -50,6 +69,14 @@ pub struct UpdateAgentTemplateRequest { pub max_tokens: Option, pub visibility: Option, pub status: Option, + // Extended fields (source_id and version are immutable) + pub soul_content: Option, + pub scenarios: Option>, + pub welcome_message: Option, + pub quick_commands: Option>, + pub personality: Option, + pub communication_style: Option, + pub emoji: Option, } // --- List --- @@ -63,3 +90,16 @@ pub struct AgentTemplateListQuery { pub page: Option, pub page_size: Option, } + +// --- Available (lightweight) --- + +/// Lightweight template summary for the /available endpoint +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AvailableAgentTemplateInfo { + pub id: String, + pub name: String, + pub category: String, + pub emoji: Option, + pub description: Option, + pub source_id: Option, +} diff --git a/crates/zclaw-saas/src/db.rs b/crates/zclaw-saas/src/db.rs index fa4e806..68f0442 100644 --- a/crates/zclaw-saas/src/db.rs +++ b/crates/zclaw-saas/src/db.rs @@ -4,7 +4,7 @@ use sqlx::postgres::PgPoolOptions; use sqlx::PgPool; use crate::error::SaasResult; -const SCHEMA_VERSION: i32 = 7; +const SCHEMA_VERSION: i32 = 9; /// 初始化数据库 pub async fn init_db(database_url: &str) -> SaasResult { @@ -548,20 +548,84 @@ async fn seed_demo_data(pool: &PgPool) -> SaasResult<()> { } // ===== 6. Agent Templates ===== - let agent_templates = [ - ("demo-agent-coder", "Code Assistant", "A helpful coding assistant that can write, review, and debug code", "coding", "demo-openai", "gpt-4o", "You are an expert coding assistant. Help users write clean, efficient code.", "[\"code_search\",\"code_edit\",\"terminal\"]", "[\"code_generation\",\"code_review\",\"debugging\"]", 0.3, 8192), - ("demo-agent-writer", "Content Writer", "Creative writing and content generation agent", "creative", "demo-anthropic", "claude-sonnet-4-20250514", "You are a skilled content writer. Create engaging, well-structured content.", "[\"web_search\",\"document_edit\"]", "[\"writing\",\"editing\",\"summarization\"]", 0.7, 4096), - ("demo-agent-analyst", "Data Analyst", "Data analysis and visualization specialist", "analytics", "demo-openai", "gpt-4o", "You are a data analysis expert. Help users analyze data and create visualizations.", "[\"code_execution\",\"data_access\"]", "[\"data_analysis\",\"visualization\",\"statistics\"]", 0.2, 8192), - ("demo-agent-researcher", "Research Agent", "Deep research and information synthesis agent", "research", "demo-google", "gemini-2.5-pro", "You are a research specialist. Conduct thorough research and synthesize findings.", "[\"web_search\",\"document_access\"]", "[\"research\",\"synthesis\",\"citation\"]", 0.4, 16384), - ("demo-agent-translator", "Translator", "Multi-language translation agent", "utility", "demo-deepseek", "deepseek-chat", "You are a professional translator. Translate text accurately while preserving tone and context.", "[]", "[\"translation\",\"localization\"]", 0.3, 4096), + // Each tuple: (id, name, description, category, model, system_prompt, tools, capabilities, temperature, max_tokens, + // soul_content, scenarios, welcome_message, quick_commands, personality, communication_style, emoji, source_id) + let agent_templates: [(&str, &str, &str, &str, &str, &str, &str, &str, f64, i32, + &str, &str, &str, &str, &str, &str, &str, &str); 6] = [ + ("demo-agent-coder", "Code Assistant", "A helpful coding assistant that can write, review, and debug code", + "coding", "gpt-4o", + "You are an expert coding assistant. Help users write clean, efficient code.", + "[\"code_search\",\"code_edit\",\"terminal\"]", "[\"code_generation\",\"code_review\",\"debugging\"]", + 0.3, 8192, + "你是一位资深全栈工程师,擅长代码编写、评审和调试。你追求简洁高效的代码风格,注重可读性和可维护性。", + "[\"代码编写\",\"代码审查\",\"Bug调试\",\"架构设计\"]", + "你好!我是你的编程助手,有什么代码问题可以随时问我。", + "[{\"label\":\"写一个函数\",\"command\":\"帮我写一个\"},{\"label\":\"审查代码\",\"command\":\"请审查这段代码\"},{\"label\":\"解释代码\",\"command\":\"解释一下这段代码\"}]", + "professional", "concise", "💻", "code-assistant-v1"), + ("demo-agent-writer", "Content Writer", "Creative writing and content generation agent", + "creative", "claude-sonnet-4-20250514", + "You are a skilled content writer. Create engaging, well-structured content.", + "[\"web_search\",\"document_edit\"]", "[\"writing\",\"editing\",\"summarization\"]", + 0.7, 4096, + "你是一位创意写作专家,擅长各类文案创作、内容编辑和摘要生成。你善于把握文字的节奏和情感表达。", + "[\"文章写作\",\"文案创作\",\"内容编辑\",\"摘要生成\"]", + "你好!我是你的内容创作助手,需要写点什么?", + "[{\"label\":\"写一篇文章\",\"command\":\"帮我写一篇关于\"},{\"label\":\"润色文案\",\"command\":\"帮我优化这段文字\"},{\"label\":\"生成摘要\",\"command\":\"请为以下内容生成摘要\"}]", + "creative", "warm", "✍️", "content-writer-v1"), + ("demo-agent-analyst", "Data Analyst", "Data analysis and visualization specialist", + "analytics", "gpt-4o", + "You are a data analysis expert. Help users analyze data and create visualizations.", + "[\"code_execution\",\"data_access\"]", "[\"data_analysis\",\"visualization\",\"statistics\"]", + 0.2, 8192, + "你是一位数据分析专家,擅长统计分析、数据可视化和洞察提取。你善于从数据中发现有价值的模式和趋势。", + "[\"数据分析\",\"可视化报表\",\"统计建模\",\"趋势预测\"]", + "你好!我是你的数据分析助手,请分享你的数据或问题。", + "[{\"label\":\"分析数据\",\"command\":\"帮我分析这组数据\"},{\"label\":\"生成图表\",\"command\":\"为以下数据生成图表\"},{\"label\":\"统计摘要\",\"command\":\"请给出统计摘要\"}]", + "analytical", "structured", "📊", "data-analyst-v1"), + ("demo-agent-researcher", "Research Agent", "Deep research and information synthesis agent", + "research", "gemini-2.5-pro", + "You are a research specialist. Conduct thorough research and synthesize findings.", + "[\"web_search\",\"document_access\"]", "[\"research\",\"synthesis\",\"citation\"]", + 0.4, 16384, + "你是一位深度研究专家,擅长信息检索、文献综述和知识综合。你注重信息来源的可靠性和引用的准确性。", + "[\"深度研究\",\"文献综述\",\"信息检索\",\"知识综合\"]", + "你好!我是你的研究助手,需要我帮你调查什么话题?", + "[{\"label\":\"深度研究\",\"command\":\"请深入研究\"},{\"label\":\"文献综述\",\"command\":\"帮我写一份文献综述\"},{\"label\":\"对比分析\",\"command\":\"请对比分析\"}]", + "scholarly", "detailed", "🔬", "research-agent-v1"), + ("demo-agent-translator", "Translator", "Multi-language translation agent", + "utility", "deepseek-chat", + "You are a professional translator. Translate text accurately while preserving tone and context.", + "[]", "[\"translation\",\"localization\"]", + 0.3, 4096, + "你是一位专业翻译,精通中英日韩等多种语言。你注重准确传达原文含义,同时保持目标语言的自然流畅。", + "[\"文本翻译\",\"文档本地化\",\"术语管理\",\"双语校对\"]", + "你好!我是你的翻译助手,请发送需要翻译的文本。", + "[{\"label\":\"中译英\",\"command\":\"请将以下中文翻译为英文\"},{\"label\":\"英译中\",\"command\":\"请将以下英文翻译为中文\"},{\"label\":\"润色译文\",\"command\":\"请润色这段翻译\"}]", + "professional", "precise", "🌐", "translator-v1"), + ("demo-agent-medical", "医疗助手", "Clinical decision support and medical literature assistant", + "healthcare", "gpt-4o", + "You are a medical AI assistant. Help with clinical decision support, literature retrieval, and medication reference. Always remind users that your suggestions do not replace professional medical advice.", + "[\"web_search\",\"document_access\"]", "[\"clinical_support\",\"literature_search\",\"diagnosis_assist\",\"medication_ref\"]", + 0.2, 16384, + "你是一位医疗AI助手,具备丰富的临床知识。你辅助临床决策、文献检索和用药参考。\n\n重要提示:\n- 你的建议仅供医疗专业人员参考\n- 不能替代正式的医疗诊断\n- 涉及患者安全的问题需格外谨慎\n- 始终建议用户咨询专业医生", + "[\"临床辅助\",\"文献检索\",\"诊断建议\",\"用药参考\"]", + "你好!我是你的医疗AI助手。我可以帮助你进行临床决策支持、医学文献检索和用药参考。请注意,我的建议仅供参考,不能替代专业医疗意见。", + "[{\"label\":\"药物查询\",\"command\":\"查询药物\"},{\"label\":\"文献检索\",\"command\":\"检索相关文献\"},{\"label\":\"临床辅助\",\"command\":\"辅助临床分析\"},{\"label\":\"诊断建议\",\"command\":\"请给出诊断建议\"}]", + "professional", "cautious", "🏥", "medical-assistant-v1"), ]; - for (id, name, desc, cat, _pid, model, prompt, tools, caps, temp, max_tok) in &agent_templates { + for (id, name, desc, cat, model, prompt, tools, caps, temp, max_tok, + soul, scenarios, welcome, quick_cmds, personality, comm_style, emoji, source_id) in &agent_templates { let ts = now.to_rfc3339(); 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, 'custom', $5, $6, $7, $8, $9, $10, 'public', 'active', 1, $11, $11) ON CONFLICT (id) DO NOTHING" + "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, + soul_content, scenarios, welcome_message, quick_commands, personality, communication_style, emoji, version, source_id) + VALUES ($1,$2,$3,$4,'custom',$5,$6,$7,$8,$9,$10,'public','active',1,$11,$11,$12,$13,$14,$15,$16,$17,$18,1,$19) + ON CONFLICT (id) DO NOTHING" ).bind(id).bind(name).bind(desc).bind(cat).bind(model).bind(prompt).bind(tools).bind(caps) .bind(*temp).bind(*max_tok).bind(&ts) + .bind(soul).bind(scenarios).bind(welcome).bind(quick_cmds) + .bind(personality).bind(comm_style).bind(emoji).bind(source_id) .execute(pool).await?; } @@ -687,7 +751,7 @@ async fn seed_demo_data(pool: &PgPool) -> SaasResult<()> { } } - tracing::info!("Demo data seeded: 5 providers, 12 models, 5 keys, ~1500 usage records, 20 relay tasks, 5 agent templates, 12 configs, 3 API tokens, 50 logs, 112 telemetry reports"); + tracing::info!("Demo data seeded: 5 providers, 12 models, 5 keys, ~1500 usage records, 20 relay tasks, 6 agent templates, 12 configs, 3 API tokens, 50 logs, 112 telemetry reports"); Ok(()) } diff --git a/crates/zclaw-saas/src/scheduler.rs b/crates/zclaw-saas/src/scheduler.rs index ed321da..4e9b632 100644 --- a/crates/zclaw-saas/src/scheduler.rs +++ b/crates/zclaw-saas/src/scheduler.rs @@ -74,6 +74,9 @@ pub fn start_scheduler(config: &SchedulerConfig, db: PgPool, dispatcher: WorkerD /// 内置的 DB 清理任务(不通过 Worker,直接执行 SQL) pub fn start_db_cleanup_tasks(db: PgPool) { + let db_devices = db.clone(); + let db_key_pool = db.clone(); + // 每 24 小时清理不活跃设备 tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(86400)); @@ -86,7 +89,7 @@ pub fn start_db_cleanup_tasks(db: PgPool) { let cutoff = (chrono::Utc::now() - chrono::Duration::days(90)).to_rfc3339(); cutoff }) - .execute(&db) + .execute(&db_devices) .await { Ok(result) => { @@ -98,6 +101,27 @@ pub fn start_db_cleanup_tasks(db: PgPool) { } } }); + + // 每 24 小时清理过期的 key_usage_window 记录 + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(86400)); + loop { + interval.tick().await; + match sqlx::query( + "DELETE FROM key_usage_window WHERE window_minute < to_char(NOW() - INTERVAL '24 hours', 'YYYY-MM-DD\"T\"HH24:MI')" + ) + .execute(&db_key_pool) + .await + { + Ok(result) => { + if result.rows_affected() > 0 { + tracing::info!("Cleaned up {} expired key_usage_window records (24h)", result.rows_affected()); + } + } + Err(e) => tracing::error!("Key usage window cleanup failed: {}", e), + } + } + }); } /// 启动用户定时任务调度循环