6f1bc27101
1. 人设外键重构:persona_template 成为 system_prompt 唯一权威来源, agent/系统节点通过 persona_id FK 引用,含数据迁移脚本 2. Chat UI:去掉底部AI提示、加号改为弹出菜单、新建对话乐观跳转 3. 意识节点:无可用worker时禁止编造agent_id,只能自行完成或拒绝 4. 日志页面:双tab布局(系统日志 + 工作流日志列表选择) 5. 其他:SSE流式聊天、对话删除/重命名、standalone模式修复 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
302 lines
14 KiB
TypeScript
302 lines
14 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { RefreshCw, Search, Server, GitBranch } from 'lucide-react';
|
|
import apiClient from '../../api/client';
|
|
|
|
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;
|
|
}
|
|
|
|
interface WorkflowSummary {
|
|
trace_id: string;
|
|
title: string;
|
|
status: string;
|
|
created_at: string;
|
|
}
|
|
|
|
interface WorkflowStep {
|
|
name: string;
|
|
step: number;
|
|
node: string;
|
|
action: string;
|
|
status: string;
|
|
agent_id: 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' },
|
|
};
|
|
|
|
const STATUS_STYLES: Record<string, { bg: string; text: string }> = {
|
|
completed: { bg: 'bg-success-bg', text: 'text-success' },
|
|
failed: { bg: 'bg-[rgba(196,145,122,0.12)]', text: 'text-[#a0705a]' },
|
|
working: { bg: 'bg-[rgba(156,175,136,0.12)]', text: 'text-[#7a8e6a]' },
|
|
pending: { bg: 'bg-bg-secondary', text: 'text-text-muted' },
|
|
};
|
|
|
|
export function SystemLogsView() {
|
|
const { t } = useTranslation();
|
|
const [tab, setTab] = useState<'system' | 'workflow'>('system');
|
|
|
|
// System logs state
|
|
const [logs, setLogs] = useState<EventLog[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [traceFilter, setTraceFilter] = useState('');
|
|
const [typeFilter, setTypeFilter] = useState('');
|
|
const [levelFilter, setLevelFilter] = useState('');
|
|
|
|
// Workflow logs state
|
|
const [workflows, setWorkflows] = useState<WorkflowSummary[]>([]);
|
|
const [selectedTrace, setSelectedTrace] = useState<string | null>(null);
|
|
const [workflowSteps, setWorkflowSteps] = useState<WorkflowStep[]>([]);
|
|
const [wfLoading, setWfLoading] = useState(false);
|
|
|
|
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 apiClient.get(`/api/v1/system/logs?${params.toString()}`);
|
|
setLogs(resp.data.logs || []);
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const fetchWorkflows = async () => {
|
|
try {
|
|
const resp = await apiClient.get('/api/v1/workflow/list');
|
|
setWorkflows(resp.data.workflows || []);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
};
|
|
|
|
const fetchWorkflowDetail = async (traceId: string) => {
|
|
setWfLoading(true);
|
|
try {
|
|
const resp = await apiClient.get(`/api/v1/workflow/${traceId}`);
|
|
setWorkflowSteps(resp.data.steps || []);
|
|
} catch (err) {
|
|
console.error(err);
|
|
setWorkflowSteps([]);
|
|
} finally {
|
|
setWfLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (tab === 'system') fetchLogs();
|
|
else fetchWorkflows();
|
|
}, [tab]);
|
|
|
|
useEffect(() => {
|
|
if (selectedTrace) fetchWorkflowDetail(selectedTrace);
|
|
}, [selectedTrace]);
|
|
|
|
const handleSearch = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
fetchLogs();
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-6xl">
|
|
{/* Tab Switcher */}
|
|
<div className="flex items-center gap-6 mb-6">
|
|
<button
|
|
onClick={() => setTab('system')}
|
|
className={`flex items-center gap-2 pb-2 text-sm font-medium border-b-2 transition-colors ${tab === 'system' ? 'border-accent text-accent' : 'border-transparent text-text-muted hover:text-text-primary'}`}
|
|
>
|
|
<Server size={14} />
|
|
{t('agent.systemLogs')}
|
|
</button>
|
|
<button
|
|
onClick={() => setTab('workflow')}
|
|
className={`flex items-center gap-2 pb-2 text-sm font-medium border-b-2 transition-colors ${tab === 'workflow' ? 'border-accent text-accent' : 'border-transparent text-text-muted hover:text-text-primary'}`}
|
|
>
|
|
<GitBranch size={14} />
|
|
{t('agent.workflowLogs')}
|
|
</button>
|
|
<div className="flex-1" />
|
|
<button
|
|
onClick={() => tab === 'system' ? fetchLogs() : fetchWorkflows()}
|
|
disabled={loading || wfLoading}
|
|
className="p-2 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all"
|
|
>
|
|
<RefreshCw size={16} className={(loading || wfLoading) ? 'animate-spin' : ''} />
|
|
</button>
|
|
</div>
|
|
|
|
{tab === 'system' ? (
|
|
<>
|
|
<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 className="flex gap-4 h-[calc(100vh-220px)]">
|
|
{/* Workflow List */}
|
|
<div className="w-72 shrink-0 bg-bg-card border border-border-primary rounded-xl overflow-hidden flex flex-col">
|
|
<div className="px-4 py-3 border-b border-border-primary bg-bg-secondary">
|
|
<span className="text-xs font-semibold text-text-muted uppercase tracking-wider">{t('agent.workflowLogList')}</span>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto divide-y divide-border-secondary">
|
|
{workflows.length === 0 ? (
|
|
<div className="px-4 py-8 text-center text-text-muted text-xs">{t('workflow.noWorkflows')}</div>
|
|
) : (
|
|
workflows.map((wf) => (
|
|
<button
|
|
key={wf.trace_id}
|
|
onClick={() => setSelectedTrace(wf.trace_id)}
|
|
className={`w-full text-left px-4 py-3 hover:bg-bg-hover transition-colors ${selectedTrace === wf.trace_id ? 'bg-accent-light border-l-2 border-accent' : ''}`}
|
|
>
|
|
<div className="text-xs font-medium text-text-primary truncate">{wf.title}</div>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<span className={`px-1.5 py-0.5 rounded text-[10px] font-bold ${(STATUS_STYLES[wf.status] || STATUS_STYLES.pending).bg} ${(STATUS_STYLES[wf.status] || STATUS_STYLES.pending).text}`}>
|
|
{wf.status}
|
|
</span>
|
|
<span className="text-[10px] text-text-muted">{new Date(wf.created_at).toLocaleDateString()}</span>
|
|
</div>
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Workflow Steps Detail */}
|
|
<div className="flex-1 bg-bg-card border border-border-primary rounded-xl overflow-hidden flex flex-col">
|
|
{!selectedTrace ? (
|
|
<div className="flex-1 flex items-center justify-center text-text-muted text-sm">
|
|
{t('agent.selectWorkflowToView')}
|
|
</div>
|
|
) : wfLoading ? (
|
|
<div className="flex-1 flex items-center justify-center text-text-muted text-sm">
|
|
{t('common.loading')}
|
|
</div>
|
|
) : (
|
|
<div className="flex-1 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">#</th>
|
|
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.name')}</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('common.status')}</th>
|
|
<th className="px-4 py-3 text-left font-semibold text-text-muted uppercase tracking-wider">{t('agent.workflowAction')}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-border-primary">
|
|
{workflowSteps.length === 0 ? (
|
|
<tr><td colSpan={5} className="px-4 py-12 text-center text-text-muted">{t('workflow.noStepsYet')}</td></tr>
|
|
) : (
|
|
workflowSteps.map((step, idx) => {
|
|
const ss = STATUS_STYLES[step.status] || STATUS_STYLES.pending;
|
|
return (
|
|
<tr key={idx} className="hover:bg-bg-secondary/50 transition-colors">
|
|
<td className="px-4 py-2.5 text-text-muted">{step.step || idx + 1}</td>
|
|
<td className="px-4 py-2.5 text-text-primary font-medium">{step.name}</td>
|
|
<td className="px-4 py-2.5 text-text-secondary">{step.node}</td>
|
|
<td className="px-4 py-2.5">
|
|
<span className={`px-2 py-0.5 rounded text-[10px] font-bold ${ss.bg} ${ss.text}`}>{step.status}</span>
|
|
</td>
|
|
<td className="px-4 py-2.5 text-text-secondary max-w-sm truncate" title={step.action}>{step.action}</td>
|
|
</tr>
|
|
);
|
|
})
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|