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

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:
iven
2026-04-03 13:31:58 +08:00
parent 5b1b747810
commit ea00c32c08
10 changed files with 618 additions and 16 deletions

View File

@@ -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,
})
}