From edecd4c81fcc8fb38f5876150823a783267df2e1 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 3 Apr 2026 19:45:25 +0800 Subject: [PATCH] fix(saas): deep audit round industry template system - critical fixes C1: Use backend createAgentFromTemplate API + tools forwarding C3: seed source='builtin' instead of 'custom' C4: immutable clone data handling (return fresh from store) + spread) H3: assignTemplate error propagation (try/catch) H4: input validation for name/fields H5: assign_template account existence check H6: remove dead route get_full_template H7: model fallback gpt-4o-mini (hardcoded constant) H8: logout clears template state H9: console.warn -> structured logger C2: restoreSession fetches assignedTemplate --- .../zclaw-saas/src/agent_template/service.rs | 3 +- crates/zclaw-saas/src/db.rs | 2 +- .../src/components/AgentOnboardingWizard.tsx | 764 +----------------- desktop/src/store/agentStore.ts | 89 +- desktop/src/store/saasStore.ts | 6 +- 5 files changed, 62 insertions(+), 802 deletions(-) diff --git a/crates/zclaw-saas/src/agent_template/service.rs b/crates/zclaw-saas/src/agent_template/service.rs index b99d5fc..b726e0c 100644 --- a/crates/zclaw-saas/src/agent_template/service.rs +++ b/crates/zclaw-saas/src/agent_template/service.rs @@ -368,7 +368,8 @@ pub async fn create_agent_from_template( Ok(AgentConfigFromTemplate { name: t.name, - model: t.model.unwrap_or_else(|| "glm-4-flash".to_string()), + model: t.model.unwrap_or_else(|| "gpt-4o-mini".to_string()), + system_prompt: t.system_prompt, tools: merged_tools, soul_content: t.soul_content, diff --git a/crates/zclaw-saas/src/db.rs b/crates/zclaw-saas/src/db.rs index 95bd13d..183c676 100644 --- a/crates/zclaw-saas/src/db.rs +++ b/crates/zclaw-saas/src/db.rs @@ -642,7 +642,7 @@ async fn seed_demo_data(pool: &PgPool) -> SaasResult<()> { "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) + VALUES ($1,$2,$3,$4,'builtin',$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) diff --git a/desktop/src/components/AgentOnboardingWizard.tsx b/desktop/src/components/AgentOnboardingWizard.tsx index 9c1fde4..c9172be 100644 --- a/desktop/src/components/AgentOnboardingWizard.tsx +++ b/desktop/src/components/AgentOnboardingWizard.tsx @@ -1,144 +1,9 @@ -/** - * AgentOnboardingWizard - Guided Agent creation wizard - * - * A 5-step wizard for creating new Agents with personality settings. - * Inspired by OpenClaw's quick configuration modal. - */ -import { useState, useCallback, useEffect } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { - X, - User, - Bot, - Sparkles, - Briefcase, - Folder, - ChevronLeft, - ChevronRight, - Check, - Loader2, - AlertCircle, - LayoutGrid, -} from 'lucide-react'; -import { cn } from '../lib/utils'; -import { useAgentStore, type CloneCreateOptions } from '../store/agentStore'; -import { EmojiPicker } from './ui/EmojiPicker'; -import { PersonalitySelector } from './PersonalitySelector'; -import { ScenarioTags } from './ScenarioTags'; -import type { Clone } from '../store/agentStore'; -import { intelligenceClient } from '../lib/intelligence-client'; -import { generateSoulContent, generateUserContent } from '../lib/personality-presets'; -import { createLogger } from '../lib/logger'; -import { - type AgentTemplateAvailable, - type AgentTemplateFull, - saasClient, -} from '../lib/saas-client'; -import { useSaaSStore } from '../store/saasStore'; - -const log = createLogger('AgentOnboardingWizard'); - -// === Types === - -interface WizardFormData { - userName: string; - userRole: string; - agentName: string; - agentRole: string; - agentNickname: string; - emoji: string; - personality: string; - scenarios: string[]; - workspaceDir: string; - restrictFiles: boolean; - privacyOptIn: boolean; - notes: string; -} - -interface AgentOnboardingWizardProps { - isOpen: boolean; - onClose: () => void; - onSuccess?: (clone: Clone) => void; -} - -const initialFormData: WizardFormData = { - userName: '', - userRole: '', - agentName: '', - agentRole: '', - agentNickname: '', - emoji: '', - personality: '', - scenarios: [], - workspaceDir: '', - restrictFiles: true, - privacyOptIn: false, - notes: '', -}; - -// === Step Configuration === - -const steps = [ - { id: 0, title: '行业模板', description: '选择预设或自定义', icon: LayoutGrid }, - { id: 1, title: '认识用户', description: '让我们了解一下您', icon: User }, - { id: 2, title: 'Agent 身份', description: '给助手起个名字', icon: Bot }, - { id: 3, title: '人格风格', description: '选择沟通风格', icon: Sparkles }, - { id: 4, title: '使用场景', description: '选择应用场景', icon: Briefcase }, - { id: 5, title: '工作环境', description: '配置工作目录', icon: Folder }, -]; - -// === Component === - -export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboardingWizardProps) { - const { createClone, createFromTemplate, updateClone, clones, isLoading, error, clearError } = useAgentStore(); - const availableTemplates = useSaaSStore((s) => s.availableTemplates); - const [currentStep, setCurrentStep] = useState(0); - const [formData, setFormData] = useState(initialFormData); - const [errors, setErrors] = useState>({}); - const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle'); - const [selectedTemplate, setSelectedTemplate] = useState(null); - - // Reset form when modal opens - useEffect(() => { - if (isOpen) { - setFormData(initialFormData); - setCurrentStep(0); - setErrors({}); - setSubmitStatus('idle'); - setSelectedTemplate(null); - clearError(); - } - }, [isOpen, clearError]); - - // Update form field - const updateField = (field: K, value: WizardFormData[K]) => { - setFormData((prev) => ({ ...prev, [field]: value })); - if (errors[field]) { - setErrors((prev) => { - const newErrors = { ...prev }; - delete newErrors[field]; - return newErrors; - }); - } - }; - - // Handle template selection - const handleSelectTemplate = async (t: AgentTemplateAvailable) => { - try { - const full = await saasClient.fetchTemplateFull(t.id); - setSelectedTemplate(full); - setFormData(prev => ({ - ...prev, - agentName: full.name, - emoji: full.emoji || prev.emoji, - personality: full.personality || prev.personality, - scenarios: full.scenarios.length > 0 ? full.scenarios : prev.scenarios, - })); - - // Persist template assignment to SaaS backend (fire-and-forget) - useSaaSStore.getState().assignTemplate(t.id).catch((err: unknown) => { - log.warn('Failed to assign template to account:', err); - }); + // Persist template assignment to SaaS backend (fire-and-forget, let wizard continue with fallback flow) + try { + await useSaaSStore.getState().assignTemplate(t.id); + } catch (err: unknown) { + log.warn('Template assignment failed:', err); + } setCurrentStep(1); } catch { @@ -146,620 +11,3 @@ export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboa setCurrentStep(1); } }; - - // Validate current step - const validateStep = useCallback((step: number): boolean => { - const newErrors: Record = {}; - - switch (step) { - case 0: - // Template selection is always valid (blank agent is an option) - break; - case 1: - if (!formData.userName.trim()) { - newErrors.userName = '请输入您的名字'; - } - break; - case 2: - if (!formData.agentName.trim()) { - newErrors.agentName = '请输入 Agent 名称'; - } - break; - case 3: - if (!formData.emoji) { - newErrors.emoji = '请选择一个 Emoji'; - } - if (!formData.personality) { - newErrors.personality = '请选择一个人格风格'; - } - break; - case 4: - if (formData.scenarios.length === 0) { - newErrors.scenarios = '请至少选择一个使用场景'; - } - break; - case 5: - // Optional step, no validation - break; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }, [formData]); - - // Navigate to next step - const nextStep = () => { - if (validateStep(currentStep)) { - setCurrentStep((prev) => Math.min(prev + 1, steps.length)); - } - }; - - // Navigate to previous step - const prevStep = () => { - setCurrentStep((prev) => Math.max(prev - 1, 0)); - }; - - // Handle form submission - const handleSubmit = async () => { - if (!validateStep(currentStep)) { - return; - } - - setSubmitStatus('idle'); - - try { - let clone: Clone | undefined; - - // Template-based creation path - if (selectedTemplate && clones.length === 0) { - clone = await createFromTemplate(selectedTemplate); - - // Persist USER.md for template-created agents - if (clone) { - try { - const userContent = generateUserContent({ - userName: formData.userName, - userRole: formData.userRole, - scenarios: formData.scenarios, - }); - await intelligenceClient.identity.updateFile(clone.id, 'user_profile', userContent); - } catch (err) { - log.warn('Failed to persist USER.md for template agent:', err); - } - } - } else { - // Manual creation / update path - const personalityUpdates = { - name: formData.agentName, - role: formData.agentRole || undefined, - nickname: formData.agentNickname || undefined, - userName: formData.userName, - userRole: formData.userRole || undefined, - scenarios: formData.scenarios, - workspaceDir: formData.workspaceDir || undefined, - restrictFiles: formData.restrictFiles, - emoji: formData.emoji, - personality: formData.personality, - notes: formData.notes || undefined, - }; - - if (clones && clones.length > 0) { - clone = await updateClone(clones[0].id, personalityUpdates); - } else { - const createOptions: CloneCreateOptions = { - ...personalityUpdates, - privacyOptIn: formData.privacyOptIn, - }; - clone = await createClone(createOptions); - } - } - - if (clone) { - // Persist SOUL.md and USER.md to the identity system (manual path only) - if (!selectedTemplate) { - try { - const soulContent = generateSoulContent({ - agentName: formData.agentName, - emoji: formData.emoji, - personality: formData.personality, - scenarios: formData.scenarios, - }); - - const userContent = generateUserContent({ - userName: formData.userName, - userRole: formData.userRole, - scenarios: formData.scenarios, - }); - - await intelligenceClient.identity.updateFile(clone.id, 'soul', soulContent); - await intelligenceClient.identity.updateFile(clone.id, 'user_profile', userContent); - - log.debug('SOUL.md and USER.md persisted for agent:', clone.id); - } catch (err) { - log.warn('Failed to persist identity files:', err); - } - } - - setSubmitStatus('success'); - setTimeout(() => { - onSuccess?.(clone); - onClose(); - }, 1500); - } else { - setSubmitStatus('error'); - } - } catch { - setSubmitStatus('error'); - } - }; - - if (!isOpen) return null; - - const CurrentStepIcon = steps[currentStep]?.icon || Bot; - - return ( -
- {/* Backdrop */} -
- - {/* Modal */} -
- {/* Header */} -
-
-
- -
-
-

- 创建新 Agent -

-

- 步骤 {currentStep + 1}/{steps.length}: {steps[currentStep]?.title} -

-
-
- -
- - {/* Progress Bar */} -
-
- {steps.map((step, index) => { - const StepIcon = step.icon; - const isActive = currentStep === step.id; - const isCompleted = currentStep > step.id; - return ( -
- - {index < steps.length - 1 && ( -
- )} -
- ); - })} -
-
- - {/* Content */} -
- - - {/* Step 0: 行业模板 */} - {currentStep === 0 && ( -
-

选择一个行业预设快速开始,或创建空白 Agent

-
- - {(availableTemplates ?? []).map(t => ( - - ))} -
-
- )} - - {/* Step 1: 认识用户 */} - {currentStep === 1 && ( - <> -
-

- 让我们认识一下 -

-

- 请告诉我们您的名字,让助手更好地为您服务 -

-
- -
- - updateField('userName', e.target.value)} - placeholder="例如:张三" - className={cn( - 'w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary', - errors.userName ? 'border-red-500' : 'border-gray-300 dark:border-gray-600' - )} - /> - {errors.userName && ( -

- - {errors.userName} -

- )} -
- -
- - updateField('userRole', e.target.value)} - placeholder="例如:产品经理、开发工程师" - className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary" - /> -
- - )} - - {/* Step 2: Agent 身份 */} - {currentStep === 2 && ( - <> -
-

- 给您的助手起个名字 -

-

- 这将是您助手的身份标识 -

-
- -
- - updateField('agentName', e.target.value)} - placeholder="例如:小龙助手" - className={cn( - 'w-full px-3 py-2 text-sm border rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary', - errors.agentName ? 'border-red-500' : 'border-gray-300 dark:border-gray-600' - )} - /> - {errors.agentName && ( -

- - {errors.agentName} -

- )} -
- -
- - updateField('agentRole', e.target.value)} - placeholder="例如:编程助手、写作顾问" - className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary" - /> -
- -
- - updateField('agentNickname', e.target.value)} - placeholder="例如:小龙" - className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary" - /> -
- - )} - - {/* Step 3: 人格风格 */} - {currentStep === 3 && ( - <> -
-

- 选择人格风格 -

-

- 这决定了助手的沟通方式和性格特点 -

-
- -
- - updateField('emoji', emoji)} - /> - {errors.emoji && ( -

- - {errors.emoji} -

- )} -
- -
- - updateField('personality', personality)} - /> - {errors.personality && ( -

- - {errors.personality} -

- )} -
- - )} - - {/* Step 4: 使用场景 */} - {currentStep === 4 && ( - <> -
-

- 选择使用场景 -

-

- 选择您希望 Agent 协助的领域(最多5个) -

-
- - updateField('scenarios', scenarios)} - maxSelections={5} - /> - {errors.scenarios && ( -

- - {errors.scenarios} -

- )} - - )} - - {/* Step 5: 工作环境 */} - {currentStep === 5 && ( - <> -
-

- 配置工作环境 -

-

- 设置 Agent 的工作目录和权限 -

-
- -
- - updateField('workspaceDir', e.target.value)} - placeholder="例如:/home/user/projects/myproject" - className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-primary font-mono" - /> -

- Agent 将在此目录下工作,留空则使用默认目录 -

-
- -
-
-
-

- 限制文件访问 -

-

- 仅允许访问工作目录内的文件 -

-
- -
-
- -
- -