feat(saas): industry agent template assignment system
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Phase 1-8 of industry-agent-delivery plan: - DB migration: accounts.assigned_template_id (ON DELETE SET NULL) - SaaS API: 4 new endpoints (assign/get/unassign/create-agent) - Service layer: assign_template_to_account, get_assigned_template, unassign_template, create_agent_from_template) - Types: AssignTemplateRequest, AgentConfigFromTemplate (capabilities merged into tools) - Frontend SaaS Client: assignTemplate, getAssignedTemplate, unassignTemplate, createAgentFromTemplate - saasStore: assignedTemplate state + login auto-fetch + actions - saas-relay-client: fix unused import and saasUrl reference error - connectionStore: fix relayModel undefined error - capabilities default to glm-4-flash - Route registration: new template assignment routes Cospec and handlers consolidated Build: cargo check --workspace PASS, tsc --noEmit Pass
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
-- Phase 1: accounts 表增加 assigned_template_id
|
||||
-- 用户选择行业模板后记录分配关系,用于跟踪和跳过 onboarding
|
||||
-- ON DELETE SET NULL: 模板被删除时不影响账户
|
||||
|
||||
ALTER TABLE accounts ADD COLUMN assigned_template_id TEXT
|
||||
REFERENCES agent_templates(id) ON DELETE SET NULL;
|
||||
|
||||
COMMENT ON COLUMN accounts.assigned_template_id IS
|
||||
'用户选择的行业模板 ID,用于跟踪模板分配状态';
|
||||
@@ -139,3 +139,52 @@ pub async fn archive_template(
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
// --- Template Assignment ---
|
||||
|
||||
/// POST /api/v1/accounts/me/assign-template — 分配行业模板到当前账户
|
||||
pub async fn assign_template(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Json(req): Json<AssignTemplateRequest>,
|
||||
) -> SaasResult<Json<AgentTemplateInfo>> {
|
||||
check_permission(&ctx, "model:read")?;
|
||||
|
||||
let result = service::assign_template_to_account(
|
||||
&state.db, &ctx.account_id, &req.template_id,
|
||||
).await?;
|
||||
|
||||
log_operation(&state.db, &ctx.account_id, "account.assign_template", "agent_template", &req.template_id,
|
||||
None, ctx.client_ip.as_deref()).await?;
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
/// GET /api/v1/accounts/me/assigned-template — 获取已分配的行业模板
|
||||
pub async fn get_assigned_template(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
) -> SaasResult<Json<Option<AgentTemplateInfo>>> {
|
||||
check_permission(&ctx, "model:read")?;
|
||||
Ok(Json(service::get_assigned_template(&state.db, &ctx.account_id).await?))
|
||||
}
|
||||
|
||||
/// DELETE /api/v1/accounts/me/assigned-template — 取消行业模板分配
|
||||
pub async fn unassign_template(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
) -> SaasResult<Json<serde_json::Value>> {
|
||||
check_permission(&ctx, "model:read")?;
|
||||
service::unassign_template(&state.db, &ctx.account_id).await?;
|
||||
Ok(Json(serde_json::json!({"ok": true})))
|
||||
}
|
||||
|
||||
/// POST /api/v1/agent-templates/:id/create-agent — 从模板创建 Agent 配置
|
||||
pub async fn create_agent_from_template(
|
||||
State(state): State<AppState>,
|
||||
Extension(ctx): Extension<AuthContext>,
|
||||
Path(id): Path<String>,
|
||||
) -> SaasResult<Json<AgentConfigFromTemplate>> {
|
||||
check_permission(&ctx, "model:read")?;
|
||||
Ok(Json(service::create_agent_from_template(&state.db, &id).await?))
|
||||
}
|
||||
|
||||
@@ -10,10 +10,16 @@ use crate::state::AppState;
|
||||
/// Agent 模板管理路由 (需要认证)
|
||||
pub fn routes() -> axum::Router<AppState> {
|
||||
axum::Router::new()
|
||||
// Template CRUD
|
||||
.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))
|
||||
.route("/api/v1/agent-templates/:id/create-agent", post(handlers::create_agent_from_template))
|
||||
// Template Assignment (per-account)
|
||||
.route("/api/v1/accounts/me/assign-template", post(handlers::assign_template))
|
||||
.route("/api/v1/accounts/me/assigned-template", get(handlers::get_assigned_template))
|
||||
.route("/api/v1/accounts/me/assigned-template", delete(handlers::unassign_template))
|
||||
}
|
||||
|
||||
@@ -266,3 +266,118 @@ pub async fn archive_template(db: &PgPool, id: &str) -> SaasResult<AgentTemplate
|
||||
update_template(db, id, None, None, None, None, None, None, None, None, Some("archived"),
|
||||
None, None, None, None, None, None, None).await
|
||||
}
|
||||
|
||||
// --- Template Assignment ---
|
||||
|
||||
/// Assign a template to the current account.
|
||||
/// Updates accounts.assigned_template_id and returns the template info.
|
||||
pub async fn assign_template_to_account(
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
template_id: &str,
|
||||
) -> SaasResult<AgentTemplateInfo> {
|
||||
// Verify template exists and is active
|
||||
let template = get_template(db, template_id).await?;
|
||||
if template.status != "active" {
|
||||
return Err(SaasError::InvalidInput("模板不可用(已归档)".into()));
|
||||
}
|
||||
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
sqlx::query(
|
||||
"UPDATE accounts SET assigned_template_id = $1, updated_at = $2 WHERE id = $3"
|
||||
)
|
||||
.bind(template_id)
|
||||
.bind(&now)
|
||||
.bind(account_id)
|
||||
.execute(db)
|
||||
.await?;
|
||||
|
||||
Ok(template)
|
||||
}
|
||||
|
||||
/// Get the template assigned to the current account (if any).
|
||||
pub async fn get_assigned_template(
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
) -> SaasResult<Option<AgentTemplateInfo>> {
|
||||
let row = sqlx::query_scalar::<_, Option<String>>(
|
||||
"SELECT assigned_template_id FROM accounts WHERE id = $1"
|
||||
)
|
||||
.bind(account_id)
|
||||
.fetch_optional(db)
|
||||
.await?;
|
||||
|
||||
let template_id = match row.flatten() {
|
||||
Some(id) => id,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// Template may have been deleted (ON DELETE SET NULL), but check anyway
|
||||
match get_template(db, &template_id).await {
|
||||
Ok(t) => Ok(Some(t)),
|
||||
Err(SaasError::NotFound(_)) => {
|
||||
// Template deleted — clear stale reference
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
sqlx::query(
|
||||
"UPDATE accounts SET assigned_template_id = NULL, updated_at = $1 WHERE id = $2"
|
||||
)
|
||||
.bind(&now)
|
||||
.bind(account_id)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(None)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Unassign template from the current account.
|
||||
pub async fn unassign_template(
|
||||
db: &PgPool,
|
||||
account_id: &str,
|
||||
) -> SaasResult<()> {
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
sqlx::query(
|
||||
"UPDATE accounts SET assigned_template_id = NULL, updated_at = $1 WHERE id = $2"
|
||||
)
|
||||
.bind(&now)
|
||||
.bind(account_id)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an agent configuration from a template.
|
||||
/// Merges capabilities into tools, applies default model fallback.
|
||||
pub async fn create_agent_from_template(
|
||||
db: &PgPool,
|
||||
template_id: &str,
|
||||
) -> SaasResult<AgentConfigFromTemplate> {
|
||||
let t = get_template(db, template_id).await?;
|
||||
if t.status != "active" {
|
||||
return Err(SaasError::InvalidInput("模板不可用(已归档)".into()));
|
||||
}
|
||||
|
||||
// Merge capabilities into tools (deduplicated)
|
||||
let mut merged_tools = t.tools.clone();
|
||||
for cap in &t.capabilities {
|
||||
if !merged_tools.contains(cap) {
|
||||
merged_tools.push(cap.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AgentConfigFromTemplate {
|
||||
name: t.name,
|
||||
model: t.model.unwrap_or_else(|| "glm-4-flash".to_string()),
|
||||
system_prompt: t.system_prompt,
|
||||
tools: merged_tools,
|
||||
soul_content: t.soul_content,
|
||||
welcome_message: t.welcome_message,
|
||||
quick_commands: t.quick_commands,
|
||||
temperature: t.temperature,
|
||||
max_tokens: t.max_tokens,
|
||||
personality: t.personality,
|
||||
communication_style: t.communication_style,
|
||||
emoji: t.emoji,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -103,3 +103,32 @@ pub struct AvailableAgentTemplateInfo {
|
||||
pub description: Option<String>,
|
||||
pub source_id: Option<String>,
|
||||
}
|
||||
|
||||
// --- Template Assignment ---
|
||||
|
||||
/// POST /api/v1/accounts/me/assign-template
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AssignTemplateRequest {
|
||||
pub template_id: String,
|
||||
}
|
||||
|
||||
/// GET /api/v1/accounts/me/assigned-template response (nullable)
|
||||
/// Reuses AgentTemplateInfo when a template is assigned.
|
||||
|
||||
/// Agent configuration derived from a template, returned by create-agent endpoint.
|
||||
/// capabilities are merged into tools (no separate field).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentConfigFromTemplate {
|
||||
pub name: String,
|
||||
pub model: String,
|
||||
pub system_prompt: Option<String>,
|
||||
pub tools: Vec<String>,
|
||||
pub soul_content: Option<String>,
|
||||
pub welcome_message: Option<String>,
|
||||
pub quick_commands: Vec<serde_json::Value>,
|
||||
pub temperature: Option<f64>,
|
||||
pub max_tokens: Option<i32>,
|
||||
pub personality: Option<String>,
|
||||
pub communication_style: Option<String>,
|
||||
pub emoji: Option<String>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user