feat: 新增工具插件、系统日志、workflow配置及前端优化
1. 新增工具插件(edit_file, python_executor, search_file, shell_executor, write_file) 2. 新增系统事件日志模块和API 3. 新增workflow配置文件和详情API 4. 前端增加SSE、错误边界、设置引导等组件 5. 优化认证加密、速率限制、配置加载等工具模块 6. 删除废弃的cluster和health API 7. 补充单元测试和集成测试 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useAppStore } from '../../store/useAppStore';
|
||||
import { ProvidersSettings } from './ProvidersSettings';
|
||||
import { WorkerIndividualSettings } from './WorkerIndividualSettings';
|
||||
import { WorkflowConfigSettings } from './WorkflowConfigSettings';
|
||||
import { SystemLogsView } from './SystemLogsView';
|
||||
|
||||
export function AgentLayout() {
|
||||
const { t } = useTranslation();
|
||||
@@ -10,6 +12,8 @@ export function AgentLayout() {
|
||||
const tabs = [
|
||||
{ key: 'worker', label: t('agent.individual') },
|
||||
{ key: 'providers', label: t('agent.providerManagement') },
|
||||
{ key: 'config', label: t('agent.config') },
|
||||
{ key: 'logs', label: t('agent.systemLogs') },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -32,6 +36,8 @@ export function AgentLayout() {
|
||||
<div className="flex-1 overflow-y-auto p-8">
|
||||
{innerAgentTab === 'worker' && <WorkerIndividualSettings />}
|
||||
{innerAgentTab === 'providers' && <ProvidersSettings />}
|
||||
{innerAgentTab === 'config' && <WorkflowConfigSettings />}
|
||||
{innerAgentTab === 'logs' && <SystemLogsView />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RefreshCw, Search } from 'lucide-react';
|
||||
|
||||
interface EventLog {
|
||||
id: number;
|
||||
trace_id: string;
|
||||
event_type: string;
|
||||
level: string;
|
||||
node_name: string | null;
|
||||
message: string;
|
||||
metadata: Record<string, any> | null;
|
||||
created_at: string | null;
|
||||
}
|
||||
|
||||
const LEVEL_STYLES: Record<string, { bg: string; text: string; label: string }> = {
|
||||
error: { bg: 'bg-[rgba(196,145,122,0.12)]', text: 'text-[#a0705a]', label: 'ERROR' },
|
||||
warn: { bg: 'bg-[rgba(196,168,130,0.15)]', text: 'text-[#9a7d5e]', label: 'WARN' },
|
||||
info: { bg: 'bg-[rgba(156,175,136,0.12)]', text: 'text-[#7a8e6a]', label: 'INFO' },
|
||||
};
|
||||
|
||||
export function SystemLogsView() {
|
||||
const { t } = useTranslation();
|
||||
const [logs, setLogs] = useState<EventLog[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [traceFilter, setTraceFilter] = useState('');
|
||||
const [typeFilter, setTypeFilter] = useState('');
|
||||
const [levelFilter, setLevelFilter] = useState('');
|
||||
|
||||
const fetchLogs = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (traceFilter) params.set('trace_id', traceFilter);
|
||||
if (typeFilter) params.set('event_type', typeFilter);
|
||||
if (levelFilter) params.set('level', levelFilter);
|
||||
params.set('limit', '200');
|
||||
|
||||
const resp = await fetch(`/api/v1/system/logs?${params.toString()}`, {
|
||||
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` },
|
||||
});
|
||||
if (!resp.ok) throw new Error('Failed to fetch logs');
|
||||
const data = await resp.json();
|
||||
setLogs(data.logs || []);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { fetchLogs(); }, []);
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
fetchLogs();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-xl font-bold text-text-primary">{t('agent.systemLogs')}</h2>
|
||||
<button onClick={fetchLogs} disabled={loading} className="p-2 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all">
|
||||
<RefreshCw size={16} className={loading ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSearch} className="grid grid-cols-4 gap-3 mb-5">
|
||||
<input
|
||||
type="text"
|
||||
value={traceFilter}
|
||||
onChange={(e) => setTraceFilter(e.target.value)}
|
||||
placeholder={t('agent.logFilterTraceId')}
|
||||
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary placeholder:text-text-muted/50 focus:outline-none focus:ring-2 focus:ring-accent/15"
|
||||
/>
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15"
|
||||
>
|
||||
<option value="">{t('agent.logFilterAllTypes')}</option>
|
||||
<option value="workflow_start">workflow_start</option>
|
||||
<option value="step_enter">step_enter</option>
|
||||
<option value="step_complete">step_complete</option>
|
||||
<option value="step_error">step_error</option>
|
||||
<option value="workflow_complete">workflow_complete</option>
|
||||
<option value="workflow_fail">workflow_fail</option>
|
||||
<option value="system">system</option>
|
||||
</select>
|
||||
<select
|
||||
value={levelFilter}
|
||||
onChange={(e) => setLevelFilter(e.target.value)}
|
||||
className="px-3 py-2 bg-bg-card border border-border-primary rounded-lg text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-accent/15"
|
||||
>
|
||||
<option value="">{t('agent.logFilterAllLevels')}</option>
|
||||
<option value="info">INFO</option>
|
||||
<option value="warn">WARN</option>
|
||||
<option value="error">ERROR</option>
|
||||
</select>
|
||||
<button type="submit" className="flex items-center justify-center gap-2 px-4 py-2 bg-accent text-white text-sm font-medium rounded-lg hover:bg-accent-hover transition-colors">
|
||||
<Search size={14} /> {t('agent.logSearch')}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="bg-bg-card border border-border-primary rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto max-h-[60vh] overflow-y-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-bg-secondary sticky top-0">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logLevel')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logType')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">Trace ID</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logNode')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logMessage')}</th>
|
||||
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.logTime')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-border-primary">
|
||||
{logs.length === 0 ? (
|
||||
<tr><td colSpan={6} className="px-4 py-12 text-center text-text-muted">{t('agent.noLogs')}</td></tr>
|
||||
) : (
|
||||
logs.map((log) => {
|
||||
const style = LEVEL_STYLES[log.level] || LEVEL_STYLES.info;
|
||||
return (
|
||||
<tr key={log.id} className="hover:bg-bg-secondary/50 transition-colors">
|
||||
<td className="px-4 py-2.5">
|
||||
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${style.bg} ${style.text}`}>{style.label}</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-text-secondary font-mono">{log.event_type}</td>
|
||||
<td className="px-4 py-2.5 text-text-muted font-mono">{log.trace_id.slice(-8)}</td>
|
||||
<td className="px-4 py-2.5 text-text-secondary">{log.node_name || '-'}</td>
|
||||
<td className="px-4 py-2.5 text-text-primary max-w-xs truncate" title={log.message}>{log.message}</td>
|
||||
<td className="px-4 py-2.5 text-text-muted whitespace-nowrap">{log.created_at ? new Date(log.created_at).toLocaleString() : '-'}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface WorkflowConfig {
|
||||
retry: {
|
||||
max_attempts: number;
|
||||
};
|
||||
}
|
||||
|
||||
export function WorkflowConfigSettings() {
|
||||
const { t } = useTranslation();
|
||||
const [config, setConfig] = useState<WorkflowConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await fetch('/api/v1/system/config/workflow', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load config: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setConfig(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load configuration');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!config) return;
|
||||
|
||||
try {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setSuccessMessage(null);
|
||||
const response = await fetch('/api/v1/system/config/workflow', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(config),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || `Failed to save: ${response.statusText}`);
|
||||
}
|
||||
setSuccessMessage(t('agent.configSaved'));
|
||||
setTimeout(() => setSuccessMessage(null), 3000);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to save configuration');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMaxAttemptsChange = (value: string) => {
|
||||
const numValue = parseInt(value, 10);
|
||||
if (!isNaN(numValue) && numValue >= 1 && numValue <= 100) {
|
||||
setConfig((prev) => prev ? {
|
||||
...prev,
|
||||
retry: { ...prev.retry, max_attempts: numValue }
|
||||
} : null);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-text-muted">{t('common.loading')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-error">{error || 'No configuration available'}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl">
|
||||
<h2 className="text-xl font-bold text-text-primary mb-6">
|
||||
{t('agent.workflowConfig')}
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-error/10 border border-error/20 rounded-lg text-error text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{successMessage && (
|
||||
<div className="mb-4 p-4 bg-success/10 border border-success/20 rounded-lg text-success text-sm">
|
||||
{successMessage}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-bg-card border border-border-primary rounded-lg p-6 space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">
|
||||
{t('agent.maxRetryAttempts')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="100"
|
||||
value={config.retry.max_attempts}
|
||||
onChange={(e) => handleMaxAttemptsChange(e.target.value)}
|
||||
className="w-full px-4 py-2 bg-bg-secondary border border-border-primary rounded-lg text-text-primary focus:outline-none focus:ring-2 focus:ring-accent"
|
||||
/>
|
||||
<p className="mt-2 text-xs text-text-muted">
|
||||
{t('agent.maxRetryAttemptsDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
<button
|
||||
onClick={loadConfig}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 text-sm font-medium text-text-secondary bg-bg-secondary border border-border-primary rounded-lg hover:bg-bg-hover transition-colors disabled:opacity-50"
|
||||
>
|
||||
{t('common.reset')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-lg hover:bg-accent-dark transition-colors disabled:opacity-50"
|
||||
>
|
||||
{saving ? t('common.saving') : t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user