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:
@@ -1,8 +1,11 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Terminal, RefreshCw, SendHorizontal, LayoutList, GitFork, Radio } from 'lucide-react';
|
||||
import { Terminal, RefreshCw, SendHorizontal, LayoutList, GitFork, Radio, PlayCircle, ListChecks } from 'lucide-react';
|
||||
import apiClient from '../../api/client';
|
||||
import { connectSSE } from '../../api/sse';
|
||||
import type { SSEConnection } from '../../api/sse';
|
||||
import type { WorkflowDetail } from '../../types';
|
||||
import { ErrorBoundary } from '../ErrorBoundary';
|
||||
import { WorkflowDiagram } from './WorkflowDiagram';
|
||||
|
||||
interface RightPanelProps {
|
||||
@@ -16,8 +19,9 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [sseConnected, setSseConnected] = useState(false);
|
||||
const [replyText, setReplyText] = useState('');
|
||||
const [activeTab, setActiveTab] = useState<'chat' | 'diagram'>('chat');
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const [resuming, setResuming] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'chat' | 'steps' | 'diagram'>('chat');
|
||||
const eventSourceRef = useRef<SSEConnection | null>(null);
|
||||
const logsEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const fetchDetail = async (traceId: string) => {
|
||||
@@ -43,14 +47,28 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
setLogs([]);
|
||||
|
||||
const apiBase = import.meta.env.VITE_API_BASE_URL || `${window.location.protocol}//${window.location.host}`;
|
||||
const es = new EventSource(`${apiBase}/api/v1/workflow/sse/${selectedWorkflow}`);
|
||||
eventSourceRef.current = es;
|
||||
es.onopen = () => setSseConnected(true);
|
||||
es.onmessage = (event) => setLogs((prev) => [...prev, event.data]);
|
||||
es.onerror = () => setSseConnected(false);
|
||||
// 用 fetch-based SSE,token 走标准 Authorization header,不进 URL
|
||||
const token = localStorage.getItem('token') || '';
|
||||
const conn = connectSSE(
|
||||
`${apiBase}/api/v1/workflow/sse/${selectedWorkflow}`,
|
||||
token,
|
||||
{
|
||||
onOpen: () => setSseConnected(true),
|
||||
onMessage: (data) => setLogs((prev) => [...prev, data]),
|
||||
onError: () => setSseConnected(false),
|
||||
onReconnect: (delayMs) => {
|
||||
setSseConnected(false);
|
||||
setLogs((prev) => [
|
||||
...prev,
|
||||
`[System]: ${t('workflow.sseReconnecting', { seconds: Math.round(delayMs / 1000) })}`,
|
||||
]);
|
||||
},
|
||||
},
|
||||
);
|
||||
eventSourceRef.current = conn;
|
||||
|
||||
const interval = setInterval(() => fetchDetail(selectedWorkflow), 3000);
|
||||
return () => { es.close(); eventSourceRef.current = null; clearInterval(interval); };
|
||||
return () => { conn.close(); eventSourceRef.current = null; clearInterval(interval); };
|
||||
}, [selectedWorkflow]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -72,6 +90,27 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleResume = async () => {
|
||||
if (!selectedWorkflow || resuming) return;
|
||||
setResuming(true);
|
||||
try {
|
||||
await apiClient.post(`/api/v1/workflow/${selectedWorkflow}/resume`);
|
||||
setLogs((prev) => [...prev, `[System]: ${t('workflow.resumeTriggered')}`]);
|
||||
fetchDetail(selectedWorkflow);
|
||||
} catch (err: any) {
|
||||
const detailMsg = err?.response?.data?.detail || t('workflow.resumeFailed');
|
||||
setLogs((prev) => [...prev, `[System Error]: ${detailMsg}`]);
|
||||
} finally {
|
||||
setResuming(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 只有非终态(未 completed/failed)的工作流才允许 resume
|
||||
const canResume =
|
||||
!!detail &&
|
||||
detail.status !== 'completed' &&
|
||||
detail.status !== 'failed';
|
||||
|
||||
if (!selectedWorkflow) return null;
|
||||
|
||||
return (
|
||||
@@ -93,6 +132,9 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
<button onClick={() => setActiveTab('chat')} className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all ${activeTab === 'chat' ? 'bg-bg-card text-accent shadow-sm' : 'text-text-muted hover:text-text-secondary'}`}>
|
||||
<LayoutList size={12} /> {t('workflow.chatLog')}
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('steps')} className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all ${activeTab === 'steps' ? 'bg-bg-card text-accent shadow-sm' : 'text-text-muted hover:text-text-secondary'}`}>
|
||||
<ListChecks size={12} /> {t('workflow.steps')}
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('diagram')} className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium transition-all ${activeTab === 'diagram' ? 'bg-bg-card text-accent shadow-sm' : 'text-text-muted hover:text-text-secondary'}`}>
|
||||
<GitFork size={12} /> {t('workflow.diagram')}
|
||||
</button>
|
||||
@@ -100,6 +142,11 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
<button onClick={() => fetchDetail(selectedWorkflow)} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all">
|
||||
<RefreshCw size={14} className={loading ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
{canResume && (
|
||||
<button onClick={handleResume} disabled={resuming} title={t('workflow.resume')} className="p-1.5 text-text-muted hover:text-accent hover:bg-accent-light rounded-lg transition-all disabled:opacity-40">
|
||||
<PlayCircle size={14} className={resuming ? 'animate-pulse' : ''} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -107,11 +154,58 @@ export function RightPanel({ selectedWorkflow }: RightPanelProps) {
|
||||
{activeTab === 'diagram' ? (
|
||||
<div className="absolute inset-0">
|
||||
{detail?.steps && detail.steps.length > 0 ? (
|
||||
<WorkflowDiagram steps={detail.steps} currentStep={0} status={detail.status} />
|
||||
) : (
|
||||
<ErrorBoundary>
|
||||
<WorkflowDiagram steps={detail.steps} currentStep={detail.current_step ?? 0} status={detail.status} />
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-text-muted text-sm">Workflow steps are not yet generated.</div>
|
||||
)}
|
||||
</div>
|
||||
) : activeTab === 'steps' ? (
|
||||
<div className="flex-1 overflow-y-auto p-5 space-y-3">
|
||||
{detail?.steps && detail.steps.length > 0 ? (
|
||||
detail.steps.map((step: any, idx: number) => {
|
||||
const isCurrent = idx === (detail.current_step ?? 0);
|
||||
const isDone = idx < (detail.current_step ?? 0);
|
||||
return (
|
||||
<div key={idx} className={`bg-bg-card border rounded-xl p-4 transition-all ${isCurrent ? 'border-accent shadow-sm' : isDone ? 'border-border-primary opacity-70' : 'border-border-primary opacity-50'}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className={`w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold ${isCurrent ? 'bg-accent text-white' : isDone ? 'bg-success/20 text-success' : 'bg-bg-secondary text-text-muted'}`}>
|
||||
{isDone ? '✓' : idx + 1}
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-text-primary">{step.node_name || step.name || `Step ${idx + 1}`}</span>
|
||||
{isCurrent && <span className="text-[10px] px-2 py-0.5 rounded-full bg-accent-light text-accent font-medium">{t('workflow.status.running')}</span>}
|
||||
</div>
|
||||
{step.output && (
|
||||
<pre className="mt-2 p-3 bg-bg-secondary rounded-lg text-xs text-text-secondary font-mono overflow-x-auto whitespace-pre-wrap max-h-48">
|
||||
{typeof step.output === 'string' ? step.output : JSON.stringify(step.output, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
{step.error && (
|
||||
<div className="mt-2 p-3 bg-error/10 border border-error/20 rounded-lg text-xs text-error">
|
||||
{step.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-text-muted text-sm">
|
||||
{t('workflow.noStepsYet')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : loading && !detail ? (
|
||||
<div className="flex-1 flex flex-col p-5 gap-4 animate-pulse">
|
||||
<div className="h-16 bg-bg-card border border-border-primary rounded-xl" />
|
||||
<div className="flex-1 bg-bg-card border border-border-primary rounded-xl p-4 space-y-3">
|
||||
<div className="h-4 w-3/4 bg-bg-secondary rounded" />
|
||||
<div className="h-4 w-1/2 bg-bg-secondary rounded" />
|
||||
<div className="h-4 w-5/6 bg-bg-secondary rounded" />
|
||||
<div className="h-4 w-2/3 bg-bg-secondary rounded" />
|
||||
</div>
|
||||
<div className="h-10 bg-bg-card border border-border-primary rounded-xl" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col p-5 overflow-hidden">
|
||||
{detail?.command && (
|
||||
|
||||
Reference in New Issue
Block a user