feat(saas): extend agent templates with soul_content, add /available endpoint, key pool cleanup, and industry seed templates
- Add 9 extended fields to AgentTemplateInfo: soul_content, scenarios, welcome_message, quick_commands, personality, communication_style, emoji, version, source_id - Refactor service.rs to use sqlx::Row (manual column extraction) to avoid the 16-element tuple FromRow limit - Add /api/v1/agent-templates/available (lightweight public listing) and /api/v1/agent-templates/:id/full endpoints - Add 24h key_usage_window cleanup task in scheduler - Update seed data with extended fields for all 5 existing templates plus new Medical Assistant template (healthcare category)
This commit is contained in:
@@ -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;
|
||||
@@ -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<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Path(id): Path<String>,
|
||||
) -> SaasResult<Json<AgentTemplateInfo>> {
|
||||
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<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
) -> SaasResult<Json<Vec<AvailableAgentTemplateInfo>>> {
|
||||
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<AppState>,
|
||||
@@ -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,
|
||||
|
||||
@@ -11,7 +11,9 @@ use crate::state::AppState;
|
||||
pub fn routes() -> axum::Router<AppState> {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -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, String, Option<String>, Option<String>,
|
||||
String, String, Option<f64>, Option<i32>, 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::<String, _>("tools")).unwrap_or_default(),
|
||||
capabilities: serde_json::from_str(&row.get::<String, _>("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::<String, _>("scenarios")).unwrap_or_default(),
|
||||
welcome_message: row.get("welcome_message"),
|
||||
quick_commands: serde_json::from_str(&row.get::<String, _>("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, String, Option<String>, Option<String>, String, String, Option<f64>, Option<i32>, 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<f64>,
|
||||
max_tokens: Option<i32>,
|
||||
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<AgentTemplateInfo> {
|
||||
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<AgentTemplateInfo> {
|
||||
let row: Option<AgentTemplateRow> = 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<AgentTemplateRow> = 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<Vec<AvailableAgentTemplateInfo>> {
|
||||
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<i32>,
|
||||
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<AgentTemplateInfo> {
|
||||
// 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<AgentTemplateInfo> {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
pub scenarios: Vec<String>,
|
||||
pub welcome_message: Option<String>,
|
||||
pub quick_commands: Vec<serde_json::Value>,
|
||||
pub personality: Option<String>,
|
||||
pub communication_style: Option<String>,
|
||||
pub emoji: Option<String>,
|
||||
pub version: i32,
|
||||
pub source_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -37,6 +47,15 @@ pub struct CreateAgentTemplateRequest {
|
||||
pub temperature: Option<f64>,
|
||||
pub max_tokens: Option<i32>,
|
||||
pub visibility: Option<String>,
|
||||
// Extended fields
|
||||
pub soul_content: Option<String>,
|
||||
pub scenarios: Option<Vec<String>>,
|
||||
pub welcome_message: Option<String>,
|
||||
pub quick_commands: Option<Vec<serde_json::Value>>,
|
||||
pub personality: Option<String>,
|
||||
pub communication_style: Option<String>,
|
||||
pub emoji: Option<String>,
|
||||
pub source_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -50,6 +69,14 @@ pub struct UpdateAgentTemplateRequest {
|
||||
pub max_tokens: Option<i32>,
|
||||
pub visibility: Option<String>,
|
||||
pub status: Option<String>,
|
||||
// Extended fields (source_id and version are immutable)
|
||||
pub soul_content: Option<String>,
|
||||
pub scenarios: Option<Vec<String>>,
|
||||
pub welcome_message: Option<String>,
|
||||
pub quick_commands: Option<Vec<serde_json::Value>>,
|
||||
pub personality: Option<String>,
|
||||
pub communication_style: Option<String>,
|
||||
pub emoji: Option<String>,
|
||||
}
|
||||
|
||||
// --- List ---
|
||||
@@ -63,3 +90,16 @@ pub struct AgentTemplateListQuery {
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
|
||||
// --- 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<String>,
|
||||
pub description: Option<String>,
|
||||
pub source_id: Option<String>,
|
||||
}
|
||||
|
||||
@@ -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<PgPool> {
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 启动用户定时任务调度循环
|
||||
|
||||
Reference in New Issue
Block a user