fix(saas): deep audit round industry template system - critical fixes
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
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
This commit is contained in:
@@ -368,7 +368,8 @@ pub async fn create_agent_from_template(
|
|||||||
|
|
||||||
Ok(AgentConfigFromTemplate {
|
Ok(AgentConfigFromTemplate {
|
||||||
name: t.name,
|
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,
|
system_prompt: t.system_prompt,
|
||||||
tools: merged_tools,
|
tools: merged_tools,
|
||||||
soul_content: t.soul_content,
|
soul_content: t.soul_content,
|
||||||
|
|||||||
@@ -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,
|
"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,
|
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)
|
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"
|
ON CONFLICT (id) DO NOTHING"
|
||||||
).bind(id).bind(name).bind(desc).bind(cat).bind(model).bind(prompt).bind(tools).bind(caps)
|
).bind(id).bind(name).bind(desc).bind(cat).bind(model).bind(prompt).bind(tools).bind(caps)
|
||||||
.bind(*temp).bind(*max_tok).bind(&ts)
|
.bind(*temp).bind(*max_tok).bind(&ts)
|
||||||
|
|||||||
@@ -1,144 +1,9 @@
|
|||||||
/**
|
// Persist template assignment to SaaS backend (fire-and-forget, let wizard continue with fallback flow)
|
||||||
* AgentOnboardingWizard - Guided Agent creation wizard
|
try {
|
||||||
*
|
await useSaaSStore.getState().assignTemplate(t.id);
|
||||||
* A 5-step wizard for creating new Agents with personality settings.
|
} catch (err: unknown) {
|
||||||
* Inspired by OpenClaw's quick configuration modal.
|
log.warn('Template assignment failed:', err);
|
||||||
*/
|
}
|
||||||
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<WizardFormData>(initialFormData);
|
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
||||||
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<AgentTemplateFull | null>(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 = <K extends keyof WizardFormData>(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);
|
|
||||||
});
|
|
||||||
|
|
||||||
setCurrentStep(1);
|
setCurrentStep(1);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -146,620 +11,3 @@ export function AgentOnboardingWizard({ isOpen, onClose, onSuccess }: AgentOnboa
|
|||||||
setCurrentStep(1);
|
setCurrentStep(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate current step
|
|
||||||
const validateStep = useCallback((step: number): boolean => {
|
|
||||||
const newErrors: Record<string, string> = {};
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
||||||
{/* Backdrop */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
|
||||||
onClick={onClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Modal */}
|
|
||||||
<div className="relative bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-hidden flex flex-col">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center">
|
|
||||||
<CurrentStepIcon className="w-5 h-5 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
创建新 Agent
|
|
||||||
</h2>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
步骤 {currentStep + 1}/{steps.length}: {steps[currentStep]?.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress Bar */}
|
|
||||||
<div className="px-4 py-2 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
{steps.map((step, index) => {
|
|
||||||
const StepIcon = step.icon;
|
|
||||||
const isActive = currentStep === step.id;
|
|
||||||
const isCompleted = currentStep > step.id;
|
|
||||||
return (
|
|
||||||
<div key={step.id} className="flex items-center flex-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => currentStep > step.id && setCurrentStep(step.id)}
|
|
||||||
disabled={currentStep <= step.id}
|
|
||||||
className={cn(
|
|
||||||
'flex items-center justify-center w-8 h-8 rounded-full text-xs font-medium transition-all',
|
|
||||||
isActive && 'bg-primary text-white',
|
|
||||||
isCompleted && 'bg-primary/20 text-primary cursor-pointer',
|
|
||||||
!isActive && !isCompleted && 'bg-gray-100 dark:bg-gray-700 text-gray-400'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isCompleted ? <Check className="w-4 h-4" /> : <StepIcon className="w-4 h-4" />}
|
|
||||||
</button>
|
|
||||||
{index < steps.length - 1 && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex-1 h-1 rounded-full mx-1',
|
|
||||||
isCompleted ? 'bg-primary' : 'bg-gray-200 dark:bg-gray-700'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content */}
|
|
||||||
<div className="flex-1 overflow-y-auto p-6">
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
<motion.div
|
|
||||||
key={currentStep}
|
|
||||||
initial={{ opacity: 0, x: 20 }}
|
|
||||||
animate={{ opacity: 1, x: 0 }}
|
|
||||||
exit={{ opacity: 0, x: -20 }}
|
|
||||||
transition={{ duration: 0.2 }}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
{/* Step 0: 行业模板 */}
|
|
||||||
{currentStep === 0 && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-sm text-muted-foreground">选择一个行业预设快速开始,或创建空白 Agent</p>
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setSelectedTemplate(null); setCurrentStep(1); }}
|
|
||||||
className="p-4 rounded-lg border-2 border-dashed border-muted-foreground/25 hover:border-primary/50 transition-colors text-center"
|
|
||||||
>
|
|
||||||
<div className="text-2xl mb-2">✨</div>
|
|
||||||
<div className="font-medium text-sm">空白 Agent</div>
|
|
||||||
<div className="text-xs text-muted-foreground">从零配置</div>
|
|
||||||
</button>
|
|
||||||
{(availableTemplates ?? []).map(t => (
|
|
||||||
<button
|
|
||||||
key={t.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleSelectTemplate(t)}
|
|
||||||
className="p-4 rounded-lg border-2 hover:border-primary/50 transition-colors text-center"
|
|
||||||
>
|
|
||||||
<div className="text-2xl mb-2">{t.emoji || '🤖'}</div>
|
|
||||||
<div className="font-medium text-sm">{t.name}</div>
|
|
||||||
<div className="text-xs text-muted-foreground line-clamp-2">{t.description}</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step 1: 认识用户 */}
|
|
||||||
{currentStep === 1 && (
|
|
||||||
<>
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
|
||||||
让我们认识一下
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
请告诉我们您的名字,让助手更好地为您服务
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
您的名字 <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.userName}
|
|
||||||
onChange={(e) => 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 && (
|
|
||||||
<p className="mt-1 text-xs text-red-500 flex items-center gap-1">
|
|
||||||
<AlertCircle className="w-3 h-3" />
|
|
||||||
{errors.userName}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
您的角色(可选)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.userRole}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step 2: Agent 身份 */}
|
|
||||||
{currentStep === 2 && (
|
|
||||||
<>
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
|
||||||
给您的助手起个名字
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
这将是您助手的身份标识
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Agent 名称 <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.agentName}
|
|
||||||
onChange={(e) => 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 && (
|
|
||||||
<p className="mt-1 text-xs text-red-500 flex items-center gap-1">
|
|
||||||
<AlertCircle className="w-3 h-3" />
|
|
||||||
{errors.agentName}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Agent 角色(可选)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.agentRole}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
昵称(可选)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.agentNickname}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step 3: 人格风格 */}
|
|
||||||
{currentStep === 3 && (
|
|
||||||
<>
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
|
||||||
选择人格风格
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
这决定了助手的沟通方式和性格特点
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
选择一个 Emoji <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<EmojiPicker
|
|
||||||
value={formData.emoji}
|
|
||||||
onChange={(emoji) => updateField('emoji', emoji)}
|
|
||||||
/>
|
|
||||||
{errors.emoji && (
|
|
||||||
<p className="mt-1 text-xs text-red-500 flex items-center gap-1">
|
|
||||||
<AlertCircle className="w-3 h-3" />
|
|
||||||
{errors.emoji}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
||||||
选择人格风格 <span className="text-red-500">*</span>
|
|
||||||
</label>
|
|
||||||
<PersonalitySelector
|
|
||||||
value={formData.personality}
|
|
||||||
onChange={(personality) => updateField('personality', personality)}
|
|
||||||
/>
|
|
||||||
{errors.personality && (
|
|
||||||
<p className="mt-1 text-xs text-red-500 flex items-center gap-1">
|
|
||||||
<AlertCircle className="w-3 h-3" />
|
|
||||||
{errors.personality}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step 4: 使用场景 */}
|
|
||||||
{currentStep === 4 && (
|
|
||||||
<>
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
|
||||||
选择使用场景
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
选择您希望 Agent 协助的领域(最多5个)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScenarioTags
|
|
||||||
value={formData.scenarios}
|
|
||||||
onChange={(scenarios) => updateField('scenarios', scenarios)}
|
|
||||||
maxSelections={5}
|
|
||||||
/>
|
|
||||||
{errors.scenarios && (
|
|
||||||
<p className="mt-2 text-xs text-red-500 flex items-center gap-1">
|
|
||||||
<AlertCircle className="w-3 h-3" />
|
|
||||||
{errors.scenarios}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Step 5: 工作环境 */}
|
|
||||||
{currentStep === 5 && (
|
|
||||||
<>
|
|
||||||
<div className="text-center mb-6">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-1">
|
|
||||||
配置工作环境
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
设置 Agent 的工作目录和权限
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
工作目录(可选)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={formData.workspaceDir}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
<p className="mt-1 text-xs text-gray-400">
|
|
||||||
Agent 将在此目录下工作,留空则使用默认目录
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-3 mt-4">
|
|
||||||
<div className="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800/50 rounded-lg">
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
|
||||||
限制文件访问
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
仅允许访问工作目录内的文件
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => updateField('restrictFiles', !formData.restrictFiles)}
|
|
||||||
className={cn(
|
|
||||||
'w-11 h-6 rounded-full transition-colors relative',
|
|
||||||
formData.restrictFiles ? 'bg-primary' : 'bg-gray-300 dark:bg-gray-600'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'absolute top-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform',
|
|
||||||
)}
|
|
||||||
style={{ left: formData.restrictFiles ? '22px' : '2px' }}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
备注(可选)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={formData.notes}
|
|
||||||
onChange={(e) => updateField('notes', e.target.value)}
|
|
||||||
placeholder="关于此 Agent 的备注信息..."
|
|
||||||
rows={3}
|
|
||||||
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 resize-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Summary Preview */}
|
|
||||||
<div className="p-4 bg-primary/5 rounded-lg mt-4">
|
|
||||||
<h4 className="text-sm font-medium text-gray-900 dark:text-white mb-3">
|
|
||||||
配置预览
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-2xl">{formData.emoji || '🤖'}</span>
|
|
||||||
<span className="font-medium">{formData.agentName || '未命名'}</span>
|
|
||||||
{formData.agentNickname && (
|
|
||||||
<span className="text-gray-500">({formData.agentNickname})</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-600 dark:text-gray-400">
|
|
||||||
用户:{formData.userName}
|
|
||||||
{formData.userRole && ` (${formData.userRole})`}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-1 mt-2">
|
|
||||||
{formData.scenarios.map((id) => (
|
|
||||||
<span
|
|
||||||
key={id}
|
|
||||||
className="px-2 py-0.5 bg-primary/10 text-primary rounded text-xs"
|
|
||||||
>
|
|
||||||
{id}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status Messages */}
|
|
||||||
{submitStatus === 'success' && (
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg text-green-700 dark:text-green-400 mt-4">
|
|
||||||
<Check className="w-5 h-5 flex-shrink-0" />
|
|
||||||
<span className="text-sm">Agent 创建成功!</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{submitStatus === 'error' && (
|
|
||||||
<div className="flex items-center gap-2 p-3 bg-red-50 dark:bg-red-900/20 rounded-lg text-red-700 dark:text-red-400 mt-4">
|
|
||||||
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
|
||||||
<span className="text-sm">{error || '创建失败,请重试'}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="flex items-center justify-between p-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={prevStep}
|
|
||||||
disabled={currentStep === 0}
|
|
||||||
className="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-4 h-4" />
|
|
||||||
上一步
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{currentStep < steps.length ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={nextStep}
|
|
||||||
className="px-4 py-2 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors flex items-center gap-1"
|
|
||||||
>
|
|
||||||
下一步
|
|
||||||
<ChevronRight className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isLoading || submitStatus === 'success'}
|
|
||||||
className="px-4 py-2 text-sm bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-colors disabled:opacity-50 flex items-center gap-2"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
|
||||||
创建中...
|
|
||||||
</>
|
|
||||||
) : submitStatus === 'success' ? (
|
|
||||||
<>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
已创建
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
完成
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AgentOnboardingWizard;
|
|
||||||
|
|||||||
@@ -7,8 +7,12 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import type { GatewayClient } from '../lib/gateway-client';
|
import type { GatewayClient } from '../lib/gateway-client';
|
||||||
import type { AgentTemplateFull } from '../lib/saas-client';
|
import type { AgentTemplateFull } from '../lib/saas-client';
|
||||||
|
import { saasClient } from '../lib/saas-client';
|
||||||
import { useChatStore } from './chatStore';
|
import { useChatStore } from './chatStore';
|
||||||
import { useConversationStore } from './chat/conversationStore';
|
import { useConversationStore } from './chat/conversationStore';
|
||||||
|
import { createLogger } from '../lib/logger';
|
||||||
|
|
||||||
|
const log = createLogger('AgentStore');
|
||||||
|
|
||||||
// === Types ===
|
// === Types ===
|
||||||
|
|
||||||
@@ -134,7 +138,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|||||||
loadClones: async () => {
|
loadClones: async () => {
|
||||||
const client = getClient();
|
const client = getClient();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
console.warn('[AgentStore] Client not initialized, skipping loadClones');
|
log.warn('[AgentStore] Client not initialized, skipping loadClones');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +167,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|||||||
createClone: async (opts: CloneCreateOptions) => {
|
createClone: async (opts: CloneCreateOptions) => {
|
||||||
const client = getClient();
|
const client = getClient();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
console.warn('[AgentStore] Client not initialized');
|
log.warn('[AgentStore] Client not initialized');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,76 +185,79 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|||||||
|
|
||||||
createFromTemplate: async (template: AgentTemplateFull) => {
|
createFromTemplate: async (template: AgentTemplateFull) => {
|
||||||
const client = getClient();
|
const client = getClient();
|
||||||
if (!client) return undefined;
|
if (!client) {
|
||||||
|
set({ error: 'Client not initialized' });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
try {
|
try {
|
||||||
|
// Step 1: Call backend to get server-processed config (tools merge, model fallback)
|
||||||
|
const config = await saasClient.createAgentFromTemplate(template.id);
|
||||||
|
|
||||||
|
// Step 2: Create clone with merged data from backend
|
||||||
const result = await client.createClone({
|
const result = await client.createClone({
|
||||||
name: template.name,
|
name: config.name,
|
||||||
emoji: template.emoji,
|
emoji: config.emoji,
|
||||||
personality: template.personality,
|
personality: config.personality,
|
||||||
scenarios: template.scenarios,
|
scenarios: template.scenarios,
|
||||||
communicationStyle: template.communication_style,
|
communicationStyle: config.communication_style,
|
||||||
model: template.model,
|
model: config.model,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cloneId = result?.clone?.id;
|
const cloneId = result?.clone?.id;
|
||||||
|
|
||||||
if (cloneId) {
|
if (cloneId) {
|
||||||
// Persist SOUL.md via identity system
|
// Persist SOUL.md via identity system
|
||||||
if (template.soul_content) {
|
if (config.soul_content) {
|
||||||
try {
|
try {
|
||||||
const { intelligenceClient } = await import('../lib/intelligence-client');
|
const { intelligenceClient } = await import('../lib/intelligence-client');
|
||||||
await intelligenceClient.identity.updateFile(cloneId, 'soul', template.soul_content);
|
await intelligenceClient.identity.updateFile(cloneId, 'soul', config.soul_content);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to persist soul_content:', e);
|
log.warn('Failed to persist soul_content:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist system_prompt via identity system
|
// Persist system_prompt via identity system
|
||||||
if (template.system_prompt) {
|
if (config.system_prompt) {
|
||||||
try {
|
try {
|
||||||
const { intelligenceClient } = await import('../lib/intelligence-client');
|
const { intelligenceClient } = await import('../lib/intelligence-client');
|
||||||
await intelligenceClient.identity.updateFile(cloneId, 'system', template.system_prompt);
|
await intelligenceClient.identity.updateFile(cloneId, 'system', config.system_prompt);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to persist system_prompt:', e);
|
log.warn('Failed to persist system_prompt:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist temperature / max_tokens if supported
|
// Persist temperature / max_tokens / tools / source_template_id / welcomeMessage / quickCommands
|
||||||
if (template.temperature != null || template.max_tokens != null) {
|
const metadata: Record<string, unknown> = {};
|
||||||
try {
|
if (config.temperature != null) metadata.temperature = config.temperature;
|
||||||
await client.updateClone(cloneId, {
|
if (config.max_tokens != null) metadata.maxTokens = config.max_tokens;
|
||||||
temperature: template.temperature,
|
if (config.tools?.length) metadata.tools = config.tools;
|
||||||
maxTokens: template.max_tokens,
|
metadata.source_template_id = template.id;
|
||||||
});
|
if (config.welcome_message) metadata.welcomeMessage = config.welcome_message;
|
||||||
} catch (e) {
|
if (config.quick_commands?.length) metadata.quickCommands = config.quick_commands;
|
||||||
console.warn('Failed to persist temperature/max_tokens:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persist welcome_message + quick_commands as clone metadata
|
if (Object.keys(metadata).length > 0) {
|
||||||
if (template.welcome_message || (template.quick_commands && template.quick_commands.length > 0)) {
|
|
||||||
try {
|
try {
|
||||||
await client.updateClone(cloneId, {
|
await client.updateClone(cloneId, metadata);
|
||||||
welcomeMessage: template.welcome_message || '',
|
|
||||||
quickCommands: template.quick_commands || [],
|
|
||||||
});
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to persist welcome_message/quick_commands:', e);
|
log.warn('Failed to persist clone metadata:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await get().loadClones();
|
await get().loadClones();
|
||||||
|
|
||||||
// Merge template welcome/quick data into the returned clone for immediate use
|
// Return a fresh clone from the store (immutable — no in-place mutation)
|
||||||
const createdClone = result?.clone as Record<string, unknown> | undefined;
|
const freshClone = get().clones.find((c) => c.id === cloneId);
|
||||||
if (createdClone) {
|
if (freshClone) {
|
||||||
if (template.welcome_message) createdClone.welcomeMessage = template.welcome_message;
|
return {
|
||||||
if (template.quick_commands?.length) createdClone.quickCommands = template.quick_commands;
|
...freshClone,
|
||||||
|
...(config.welcome_message ? { welcomeMessage: config.welcome_message } : {}),
|
||||||
|
...(config.quick_commands?.length ? { quickCommands: config.quick_commands } : {}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return createdClone as Clone | undefined;
|
return result?.clone as Clone | undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: String(error) });
|
set({ error: String(error) });
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -262,7 +269,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|||||||
updateClone: async (id: string, updates: Partial<Clone>) => {
|
updateClone: async (id: string, updates: Partial<Clone>) => {
|
||||||
const client = getClient();
|
const client = getClient();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
console.warn('[AgentStore] Client not initialized');
|
log.warn('[AgentStore] Client not initialized');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +288,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|||||||
deleteClone: async (id: string) => {
|
deleteClone: async (id: string) => {
|
||||||
const client = getClient();
|
const client = getClient();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
console.warn('[AgentStore] Client not initialized');
|
log.warn('[AgentStore] Client not initialized');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +332,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|||||||
loadPluginStatus: async () => {
|
loadPluginStatus: async () => {
|
||||||
const client = getClient();
|
const client = getClient();
|
||||||
if (!client) {
|
if (!client) {
|
||||||
console.warn('[AgentStore] Client not initialized, skipping loadPluginStatus');
|
log.warn('[AgentStore] Client not initialized, skipping loadPluginStatus');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -397,8 +397,10 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
|
|||||||
authToken: null,
|
authToken: null,
|
||||||
connectionMode: 'tauri',
|
connectionMode: 'tauri',
|
||||||
availableModels: [],
|
availableModels: [],
|
||||||
|
availableTemplates: [],
|
||||||
|
assignedTemplate: null,
|
||||||
error: null,
|
error: null,
|
||||||
totpRequired: false,
|
toTopRequired: false,
|
||||||
totpSetupData: null,
|
totpSetupData: null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -720,6 +722,8 @@ export const useSaaSStore = create<SaaSStore>((set, get) => {
|
|||||||
connectionMode: loadConnectionMode() === 'saas' ? 'saas' : 'tauri',
|
connectionMode: loadConnectionMode() === 'saas' ? 'saas' : 'tauri',
|
||||||
});
|
});
|
||||||
get().fetchAvailableModels().catch(() => {});
|
get().fetchAvailableModels().catch(() => {});
|
||||||
|
get().fetchAvailableTemplates().catch(() => {});
|
||||||
|
get().fetchAssignedTemplate().catch(() => {});
|
||||||
get().syncConfigFromSaaS().then(() => {
|
get().syncConfigFromSaaS().then(() => {
|
||||||
get().pushConfigToSaaS().catch(() => {});
|
get().pushConfigToSaaS().catch(() => {});
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
|||||||
Reference in New Issue
Block a user