feat(agent): 移除control_node实例化,新增系统节点命名与人设管理前端

当前阶段只保留regulatory+consciousness两个系统节点,control_node代码保留但不再实例化。
系统节点新增display_name字段支持自定义显示名称,前端新增人设管理Tab支持模板CRUD。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 06:03:28 +00:00
parent 9a7a5edd6e
commit f3a92a793e
11 changed files with 567 additions and 47 deletions
@@ -4,6 +4,7 @@ import { ProvidersSettings } from './ProvidersSettings';
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
import { WorkflowConfigSettings } from './WorkflowConfigSettings';
import { SystemLogsView } from './SystemLogsView';
import { PersonaTemplateSettings } from './PersonaTemplateSettings';
export function AgentLayout() {
const { t } = useTranslation();
@@ -11,6 +12,7 @@ export function AgentLayout() {
const tabs = [
{ key: 'worker', label: t('agent.individual') },
{ key: 'persona', label: t('agent.personaManagement') },
{ key: 'providers', label: t('agent.providerManagement') },
{ key: 'config', label: t('agent.config') },
{ key: 'logs', label: t('agent.systemLogs') },
@@ -35,6 +37,7 @@ export function AgentLayout() {
</div>
<div className="flex-1 overflow-y-auto p-8">
{innerAgentTab === 'worker' && <WorkerIndividualSettings />}
{innerAgentTab === 'persona' && <PersonaTemplateSettings />}
{innerAgentTab === 'providers' && <ProvidersSettings />}
{innerAgentTab === 'config' && <WorkflowConfigSettings />}
{innerAgentTab === 'logs' && <SystemLogsView />}
@@ -0,0 +1,276 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import apiClient from '../../api/client';
import { Plus, Edit2, Trash2, X, Save, Loader2, FileText, Copy } from 'lucide-react';
import type { Provider } from '../../types';
interface PersonaTemplate {
template_id: string;
name: string;
description: string;
system_prompt: string;
agent_type: string;
provider_title: string | null;
model_id: string | null;
tools: string[];
tags: string[];
is_builtin: boolean;
owner_id: string | null;
}
export function PersonaTemplateSettings() {
const { t } = useTranslation();
const [templates, setTemplates] = useState<PersonaTemplate[]>([]);
const [providers, setProviders] = useState<Provider[]>([]);
const [availableTools, setAvailableTools] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState<Partial<PersonaTemplate>>({});
const [isNew, setIsNew] = useState(false);
const [modalMessage, setModalMessage] = useState('');
const [submitLoading, setSubmitLoading] = useState(false);
const fetchData = async () => {
setLoading(true);
try {
const [tplRes, provRes, toolsRes] = await Promise.all([
apiClient.get('/api/v1/agent/template'),
apiClient.get('/api/v1/provider/list'),
apiClient.get('/api/v1/resource/tool')
]);
setTemplates(tplRes.data.templates || []);
setProviders(Object.values(provRes.data.provider_list || {}));
setAvailableTools(toolsRes.data.tools || []);
} catch { setError(t('agent.loadFailed')); }
finally { setLoading(false); }
};
useEffect(() => { fetchData(); }, []);
const handleAddNew = () => {
setEditData({ name: '', description: '', system_prompt: '', agent_type: 'ordinary_individual',
provider_title: providers.length > 0 ? providers[0].provider_title : '',
model_id: '', tools: [], tags: [] });
setIsNew(true);
setIsEditing(true);
setModalMessage('');
};
const handleEdit = (tpl: PersonaTemplate) => {
setEditData({ ...tpl });
setIsNew(false);
setIsEditing(true);
setModalMessage('');
};
const handleDelete = async (id: string) => {
if (!confirm(t('agent.deleteTemplateConfirm'))) return;
try { await apiClient.delete(`/api/v1/agent/template/${id}`); fetchData(); }
catch { alert(t('common.deleteFailed')); }
};
const handleCreateWorker = async (id: string) => {
try {
await apiClient.post(`/api/v1/agent/worker/from-template/${id}`);
alert(t('agent.workerCreatedFromTemplate'));
} catch (err: any) {
alert(err.response?.data?.detail || t('common.saveFailed'));
}
};
const handleModalSave = async (e: React.FormEvent) => {
e.preventDefault();
setModalMessage('');
setSubmitLoading(true);
try {
const payload = { name: editData.name, description: editData.description,
system_prompt: editData.system_prompt, agent_type: editData.agent_type,
provider_title: editData.provider_title || null,
model_id: editData.model_id || null,
tools: editData.tools || [], tags: editData.tags || [] };
if (isNew) await apiClient.post('/api/v1/agent/template', payload);
else await apiClient.put(`/api/v1/agent/template/${editData.template_id}`, payload);
setIsEditing(false);
fetchData();
} catch (err: any) {
setModalMessage(err.response?.data?.detail || t('common.saveFailed'));
} finally { setSubmitLoading(false); }
};
const getTypeBadge = (type: string) => {
const colors: Record<string, string> = {
ordinary_individual: 'bg-bg-secondary text-text-muted',
skill_individual: 'bg-success-bg text-success',
special_individual: 'bg-warning-bg text-warning',
};
return <span className={`px-2 py-0.5 rounded-md text-[10px] font-medium ${colors[type] || colors.ordinary_individual}`}>{t(`agent.type.${type}`, type)}</span>;
};
return (
<div className="max-w-5xl space-y-6">
<div className="flex justify-between items-end">
<div>
<h1 className="text-lg font-bold text-text-primary">{t('agent.personaManagement')}</h1>
<p className="text-sm text-text-muted mt-0.5">{t('agent.personaManagementDesc')}</p>
</div>
<button onClick={handleAddNew} className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium">
<Plus size={14} /> {t('agent.addTemplate')}
</button>
</div>
{error && <div className="text-sm text-danger bg-danger-bg border border-danger/20 rounded-xl p-3">{error}</div>}
<div className="bg-bg-card rounded-2xl border border-border-primary shadow-sm overflow-hidden">
{loading ? (
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
<Loader2 size={24} className="animate-spin mb-3" />
<span className="text-sm">{t('common.loading')}</span>
</div>
) : templates.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
<FileText size={32} className="mb-3 opacity-40" />
<span className="text-sm">{t('agent.noTemplates')}</span>
</div>
) : (
<table className="w-full text-left text-sm">
<thead>
<tr className="bg-bg-secondary border-b border-border-primary text-text-muted text-xs uppercase tracking-wider">
<th className="px-5 py-3 font-semibold">{t('agent.templateName')}</th>
<th className="px-5 py-3 font-semibold">{t('agent.type')}</th>
<th className="px-5 py-3 font-semibold">{t('agent.tags')}</th>
<th className="px-5 py-3 font-semibold text-right">{t('common.actions')}</th>
</tr>
</thead>
<tbody className="divide-y divide-border-secondary">
{templates.map((tpl) => (
<tr key={tpl.template_id} className="hover:bg-bg-hover transition-colors">
<td className="px-5 py-3">
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-lg bg-bg-secondary border border-border-primary flex items-center justify-center">
<FileText size={14} className="text-text-muted" />
</div>
<div className="flex flex-col">
<span className="font-medium text-text-primary text-xs">{tpl.name}</span>
{tpl.is_builtin && <span className="text-[10px] text-accent">{t('agent.builtin')}</span>}
</div>
</div>
</td>
<td className="px-5 py-3">{getTypeBadge(tpl.agent_type)}</td>
<td className="px-5 py-3">
<div className="flex flex-wrap gap-1">
{(tpl.tags || []).map(tag => (
<span key={tag} className="px-1.5 py-0.5 rounded text-[10px] bg-bg-secondary text-text-muted">{tag}</span>
))}
</div>
</td>
<td className="px-5 py-3 text-right">
<button onClick={() => handleCreateWorker(tpl.template_id)} title={t('agent.createFromTemplate')} className="p-1.5 text-text-muted hover:text-success hover:bg-success-bg rounded-lg transition-all mr-0.5"><Copy size={14} /></button>
{!tpl.is_builtin && <button onClick={() => handleEdit(tpl)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all mr-0.5"><Edit2 size={14} /></button>}
{!tpl.is_builtin && <button onClick={() => handleDelete(tpl.template_id)} className="p-1.5 text-text-muted hover:text-danger hover:bg-danger-bg rounded-lg transition-all"><Trash2 size={14} /></button>}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
{isEditing && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto border border-border-primary animate-fade-in-scale">
<div className="flex justify-between items-center p-5 border-b border-border-primary sticky top-0 bg-bg-card z-10">
<h2 className="text-base font-bold text-text-primary">{isNew ? t('agent.addTemplate') : t('agent.editTemplate')}</h2>
<button onClick={() => setIsEditing(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={20} /></button>
</div>
<form onSubmit={handleModalSave} className="p-5 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.templateName')}</label>
<input type="text" required value={editData.name || ''} onChange={(e) => setEditData({...editData, name: e.target.value})}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.type')}</label>
<select value={editData.agent_type || 'ordinary_individual'} onChange={(e) => setEditData({...editData, agent_type: e.target.value})}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
<option value="ordinary_individual">{t('agent.type.ordinary_individual')}</option>
<option value="skill_individual">{t('agent.type.skill_individual')}</option>
<option value="special_individual">{t('agent.type.special_individual')}</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.provider')}</label>
<select value={editData.provider_title || ''} onChange={(e) => setEditData({...editData, provider_title: e.target.value, model_id: ''})}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
<option value="">{t('common.none')}</option>
{providers.map((p) => (<option key={p.provider_title} value={p.provider_title}>{p.provider_title}</option>))}
</select>
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.model')}</label>
{(() => {
const sp = providers.find(p => p.provider_title === editData.provider_title);
const models = sp?.provider_models || [];
return (
<select value={editData.model_id || ''} onChange={(e) => setEditData({...editData, model_id: e.target.value})}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent">
<option value="">{t('common.none')}</option>
{models.map(m => <option key={m} value={m}>{m}</option>)}
</select>
);
})()}
</div>
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.description')}</label>
<textarea value={editData.description || ''} onChange={(e) => setEditData({...editData, description: e.target.value})} rows={2}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.systemPrompt')}</label>
<textarea value={editData.system_prompt || ''} onChange={(e) => setEditData({...editData, system_prompt: e.target.value})} rows={4}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.tags')}</label>
<input type="text" value={(editData.tags || []).join(', ')}
onChange={(e) => setEditData({...editData, tags: e.target.value.split(',').map(s => s.trim()).filter(Boolean)})}
placeholder={t('agent.tagsPlaceholder')}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.tools')}</label>
<div className="flex flex-wrap gap-1.5 p-3 bg-bg-input border border-border-primary rounded-xl max-h-40 overflow-y-auto">
{availableTools.map(tool => {
const currentTools: string[] = editData.tools || [];
const isSelected = currentTools.includes(tool);
return (
<button key={tool} type="button" onClick={() => {
const updated = isSelected ? currentTools.filter(x => x !== tool) : [...currentTools, tool];
setEditData({...editData, tools: updated});
}}
className={`px-2.5 py-1 rounded-lg text-xs font-medium transition-all ${isSelected ? 'bg-accent-light text-accent border border-accent/20' : 'bg-bg-secondary text-text-muted border border-border-primary hover:border-text-muted'}`}>
{tool}
</button>
);
})}
{availableTools.length === 0 && <span className="text-xs text-text-muted">{t('agent.noTools')}</span>}
</div>
</div>
{modalMessage && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{modalMessage}</div>}
<div className="pt-3 flex justify-end gap-2 border-t border-border-primary">
<button type="button" onClick={() => setIsEditing(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">{t('common.cancel')}</button>
<button type="submit" disabled={submitLoading} className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">
<Save size={14} /> {submitLoading ? t('common.saving') : t('common.save')}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}
@@ -51,15 +51,17 @@ export function WorkerIndividualSettings() {
const providersList = Object.values(provRes.data.provider_list || {}) as Provider[];
const defaultProvider = providersList.length > 0 ? providersList[0].provider_title : '';
const sysNodesData = sysRes.data.system_nodes || [];
const defaultSysNodes = ['regulatory_node', 'consciousness_node', 'control_node'];
const defaultSysNodes = ['regulatory_node', 'consciousness_node'];
setSystemNodes(defaultSysNodes.map(nodeName => {
const found = sysNodesData.find((n: any) => n.node_name === nodeName);
return {
agent_id: nodeName, agent_name: nodeName, agent_type: 'System Node',
display_name: found?.display_name || '',
provider_title: found?.provider_title || defaultProvider,
model_id: found?.model_id || '',
tools: found?.tools ? JSON.stringify(found.tools) : '[]',
custom_system_prompt: found?.custom_system_prompt || '',
is_system: true
};
}));
@@ -109,7 +111,9 @@ export function WorkerIndividualSettings() {
individual_name: editData.agent_name,
provider_title: editData.provider_title,
model_id: editData.model_id,
tools: JSON.parse(editData.tools || '[]')
tools: JSON.parse(editData.tools || '[]'),
custom_system_prompt: (editData as any).custom_system_prompt || null,
display_name: (editData as any).display_name || null
});
} else {
const payload = {
@@ -184,7 +188,10 @@ export function WorkerIndividualSettings() {
<div className="w-7 h-7 rounded-lg bg-accent-light flex items-center justify-center">
<Bot size={14} className="text-accent" />
</div>
<span className="font-medium text-text-primary text-xs">{w.agent_name}</span>
<div className="flex flex-col">
<span className="font-medium text-text-primary text-xs">{w.display_name || w.agent_name}</span>
{w.display_name && <span className="text-[10px] text-text-muted">{w.agent_name}</span>}
</div>
</div>
</td>
<td className="px-5 py-3">{getTypeBadge(w.agent_type, true)}</td>
@@ -228,9 +235,13 @@ export function WorkerIndividualSettings() {
<form onSubmit={handleModalSave} className="p-5 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.name')}</label>
<input type="text" required value={editData.agent_name || ''} onChange={(e) => setEditData({...editData, agent_name: e.target.value})}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" disabled={(editData as any).is_system} />
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{(editData as any).is_system ? t('agent.displayName') : t('agent.name')}</label>
<input type="text" required={!(editData as any).is_system} value={(editData as any).is_system ? ((editData as any).display_name || '') : (editData.agent_name || '')} onChange={(e) => {
if ((editData as any).is_system) setEditData({...editData, display_name: e.target.value} as any);
else setEditData({...editData, agent_name: e.target.value});
}}
placeholder={(editData as any).is_system ? editData.agent_name : ''}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.type')}</label>
@@ -267,6 +278,14 @@ export function WorkerIndividualSettings() {
})()}
</div>
</div>
{(editData as any).is_system && (
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.customPrompt')}</label>
<textarea value={(editData as any).custom_system_prompt || ''} onChange={(e) => setEditData({...editData, custom_system_prompt: e.target.value} as any)} rows={3}
placeholder={t('agent.customPromptPlaceholder')}
className="w-full px-3 py-2 bg-bg-input border border-border-primary rounded-xl text-sm text-text-primary font-mono focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent" />
</div>
)}
{!(editData as any).is_system && (
<>
<div>