Files
multiAgentTry/frontend/src/pages/WorkflowPage.tsx
Claude Code dc398d7c7b 完整实现 Swarm 多智能体协作系统
- 新增 CLIPluginAdapter 统一接口 (backend/app/core/agent_adapter.py)
- 新增 LLM 服务层,支持 Anthropic/OpenAI/DeepSeek/Ollama (backend/app/services/llm_service.py)
- 新增 Agent 执行引擎,支持文件锁自动管理 (backend/app/services/agent_executor.py)
- 新增 NativeLLMAgent 原生 LLM 适配器 (backend/app/adapters/native_llm_agent.py)
- 新增进程管理器 (backend/app/services/process_manager.py)
- 新增 Agent 控制 API (backend/app/routers/agents_control.py)
- 新增 WebSocket 实时通信 (backend/app/routers/websocket.py)
- 更新前端 AgentsPage,支持启动/停止 Agent
- 测试通过:Agent 启动、批量操作、栅栏同步

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 17:32:11 +08:00

735 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import {
Workflow,
CheckCircle,
FileText,
Upload,
ChevronRight,
RefreshCw,
Zap,
Users,
ArrowRight,
AlertTriangle,
} from 'lucide-react';
import { api } from '../lib/api';
import type { Workflow as WorkflowType, WorkflowMeeting } from '../types';
// 工作流详情面板
function WorkflowDetailPanel({
workflow,
onClose,
onReload,
}: {
workflow: WorkflowType;
onClose: () => void;
onReload: () => void;
}) {
const [loading, setLoading] = useState<string | null>(null);
const [executionStatus, setExecutionStatus] = useState<Record<string, any>>({});
// 刷新执行节点状态
const refreshExecutionStatus = async (meetingId: string) => {
if (workflow.meetings.find(m => m.meeting_id === meetingId)?.node_type === 'execution') {
try {
const status = await api.workflow.getExecutionStatus(workflow.workflow_id, meetingId);
setExecutionStatus(prev => ({ ...prev, [meetingId]: status }));
} catch (err) {
// 忽略错误
}
}
};
useEffect(() => {
// 刷新所有执行节点的状态
workflow.meetings.forEach(m => {
if (m.node_type === 'execution') {
refreshExecutionStatus(m.meeting_id);
}
});
}, [workflow]);
const handleCompleteMeeting = async (meetingId: string) => {
setLoading(meetingId);
try {
await api.workflow.complete(workflow.workflow_id, meetingId);
onReload();
} catch (err) {
alert('标记完成失败');
} finally {
setLoading(null);
}
};
const handleFailAndJump = async (meetingId: string) => {
const meeting = workflow.meetings.find(m => m.meeting_id === meetingId);
if (!meeting?.on_failure) {
alert('此节点未配置失败跳转目标');
return;
}
if (!confirm(`确定要跳转到 "${meeting.on_failure}" 吗?`)) {
return;
}
setLoading(`fail-${meetingId}`);
try {
const result = await api.workflow.handleFailure(workflow.workflow_id, meetingId);
if (result.target) {
onReload();
} else {
alert('未配置失败跳转');
}
} catch (err) {
alert('操作失败');
} finally {
setLoading(null);
}
};
const getMeetingStatus = (meeting: WorkflowMeeting) => {
if (meeting.completed) return 'completed';
const allPrevCompleted = workflow.meetings
.slice(0, workflow.meetings.findIndex((m) => m.meeting_id === meeting.meeting_id))
.every((m) => m.completed);
return allPrevCompleted ? 'active' : 'pending';
};
return (
<div
style={{
position: 'fixed',
right: 0,
top: 0,
bottom: 0,
width: 480,
background: 'rgba(17, 24, 39, 0.98)',
borderLeft: '1px solid rgba(0, 240, 255, 0.1)',
padding: 24,
zIndex: 100,
overflow: 'auto',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 20 }}>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#fff', margin: 0 }}></h2>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
color: 'rgba(255, 255, 255, 0.5)',
fontSize: 24,
cursor: 'pointer',
}}
>
×
</button>
</div>
{/* Workflow Info */}
<div
style={{
padding: 20,
background: 'rgba(0, 0, 0, 0.3)',
borderRadius: 12,
marginBottom: 20,
}}
>
<h3 style={{ fontSize: 18, fontWeight: 600, color: '#fff', margin: '0 0 8px 0' }}>
{workflow.name}
</h3>
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: 0 }}>
{workflow.workflow_id}
</p>
<p style={{ fontSize: 14, color: 'rgba(255, 255, 255, 0.6)', margin: '12px 0 0 0' }}>
{workflow.description}
</p>
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
<span
style={{
padding: '4px 12px',
borderRadius: 12,
background:
workflow.status === 'completed'
? '#00ff9d20'
: workflow.status === 'in_progress'
? '#00f0ff20'
: '#ff950020',
color:
workflow.status === 'completed'
? '#00ff9d'
: workflow.status === 'in_progress'
? '#00f0ff'
: '#ff9500',
fontSize: 12,
}}
>
{workflow.status === 'completed' ? '已完成' : workflow.status === 'in_progress' ? '进行中' : '等待中'}
</span>
<span
style={{
padding: '4px 12px',
borderRadius: 12,
background: 'rgba(139, 92, 246, 0.2)',
color: '#8b5cf6',
fontSize: 12,
}}
>
: {workflow.progress}
</span>
</div>
</div>
{/* Progress Bar */}
<div style={{ marginBottom: 20 }}>
<div
style={{
height: 8,
background: 'rgba(255, 255, 255, 0.1)',
borderRadius: 4,
overflow: 'hidden',
}}
>
<div
style={{
width: workflow.progress,
height: '100%',
background: 'linear-gradient(90deg, #00f0ff, #8b5cf6)',
borderRadius: 4,
transition: 'width 0.5s ease',
}}
/>
</div>
</div>
{/* Meetings */}
<div>
<h4 style={{ fontSize: 14, fontWeight: 600, color: 'rgba(255, 255, 255, 0.7)', margin: '0 0 12px 0' }}>
({workflow.meetings.length})
</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{workflow.meetings.map((meeting, index) => {
const status = getMeetingStatus(meeting);
const isExecution = meeting.node_type === 'execution';
const execStatus = executionStatus[meeting.meeting_id];
return (
<div
key={meeting.meeting_id}
style={{
padding: 16,
background:
status === 'completed'
? 'rgba(0, 255, 157, 0.1)'
: status === 'active'
? 'rgba(0, 240, 255, 0.1)'
: 'rgba(0, 0, 0, 0.2)',
borderRadius: 10,
border:
status === 'active'
? '1px solid rgba(0, 240, 255, 0.3)'
: '1px solid transparent',
}}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
{/* 节点图标 */}
<div
style={{
width: 28,
height: 28,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background:
status === 'completed'
? '#00ff9d'
: status === 'active'
? isExecution ? '#ff9500' : '#00f0ff'
: 'rgba(255, 255, 255, 0.1)',
color: status === 'pending' ? 'rgba(255, 255, 255, 0.4)' : '#000',
fontSize: 12,
fontWeight: 600,
flexShrink: 0,
}}
>
{status === 'completed' ? <CheckCircle size={14} /> : isExecution ? <Zap size={14} /> : index + 1}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
{/* 标题和类型 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
<p style={{ fontSize: 14, fontWeight: 500, color: '#fff', margin: 0 }}>
{meeting.title}
</p>
{/* 节点类型标签 */}
<span
style={{
padding: '2px 8px',
borderRadius: 4,
background: isExecution ? '#ff950020' : '#00f0ff20',
color: isExecution ? '#ff9500' : '#00f0ff',
fontSize: 10,
fontWeight: 500,
}}
>
{isExecution ? '执行' : '会议'}
</span>
{/* 失败跳转标签 */}
{meeting.on_failure && (
<span
style={{
display: 'flex',
alignItems: 'center',
gap: 4,
padding: '2px 8px',
borderRadius: 4,
background: '#ff006e20',
color: '#ff006e',
fontSize: 10,
}}
>
<AlertTriangle size={10} />
{meeting.on_failure}
</span>
)}
</div>
{/* ID 和参会者 */}
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
{meeting.meeting_id}
</p>
{/* 参会者列表 */}
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 6, flexWrap: 'wrap' }}>
<Users size={12} color="rgba(255, 255, 255, 0.4)" />
{meeting.attendees.map((attendee, i) => (
<span
key={i}
style={{
padding: '2px 6px',
borderRadius: 4,
background: 'rgba(255, 255, 255, 0.1)',
color: 'rgba(255, 255, 255, 0.6)',
fontSize: 10,
}}
>
{attendee}
</span>
))}
</div>
{/* 执行节点进度 */}
{isExecution && execStatus && (
<div style={{ marginTop: 8 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.5)' }}>
: {execStatus.progress}
</span>
<span style={{ fontSize: 11, color: execStatus.is_ready ? '#00ff9d' : '#ff9500' }}>
{execStatus.is_ready ? '就绪' : '进行中'}
</span>
</div>
{execStatus.missing && execStatus.missing.length > 0 && (
<div style={{ fontSize: 10, color: 'rgba(255, 255, 255, 0.4)' }}>
: {execStatus.missing.join(', ')}
</div>
)}
</div>
)}
{/* 依赖关系 */}
{meeting.depends_on && meeting.depends_on.length > 0 && (
<div style={{ marginTop: 6, fontSize: 10, color: 'rgba(255, 255, 255, 0.3)' }}>
: {meeting.depends_on.join(', ')}
</div>
)}
</div>
{/* 操作按钮 */}
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, alignItems: 'flex-end' }}>
{status === 'active' && !meeting.completed && (
<button
onClick={() => handleCompleteMeeting(meeting.meeting_id)}
disabled={loading === meeting.meeting_id}
style={{
padding: '6px 12px',
background: '#00ff9d',
border: 'none',
borderRadius: 6,
color: '#000',
fontSize: 12,
fontWeight: 600,
cursor: loading === meeting.meeting_id ? 'not-allowed' : 'pointer',
opacity: loading === meeting.meeting_id ? 0.6 : 1,
}}
>
{loading === meeting.meeting_id ? '...' : '完成'}
</button>
)}
{meeting.completed && meeting.on_failure && (
<button
onClick={() => handleFailAndJump(meeting.meeting_id)}
disabled={loading === `fail-${meeting.meeting_id}`}
style={{
padding: '4px 10px',
background: '#ff006e20',
border: '1px solid #ff006e50',
borderRadius: 6,
color: '#ff006e',
fontSize: 11,
cursor: 'pointer',
}}
>
</button>
)}
{meeting.completed && (
<span
style={{
padding: '4px 10px',
background: '#00ff9d20',
borderRadius: 12,
color: '#00ff9d',
fontSize: 11,
}}
>
</span>
)}
</div>
</div>
{/* 节点间的连接线 */}
{index < workflow.meetings.length - 1 && (
<div style={{ display: 'flex', justifyContent: 'center', marginTop: 8 }}>
<ArrowRight size={16} color="rgba(255, 255, 255, 0.2)" />
</div>
)}
</div>
);
})}
</div>
</div>
</div>
);
}
export function WorkflowPage() {
const [workflows, setWorkflows] = useState<WorkflowType[]>([]);
const [selectedWorkflow, setSelectedWorkflow] = useState<WorkflowType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const [starting, setStarting] = useState<string | null>(null);
const loadWorkflows = async () => {
try {
setLoading(true);
// 加载已加载的工作流列表
const listRes = await api.workflow.list();
const loadedWorkflows: WorkflowType[] = [];
// 从已加载的工作流中获取详情
for (const wf of listRes.workflows) {
try {
const detail = await api.workflow.get(wf.workflow_id);
loadedWorkflows.push(detail);
} catch {
// 忽略加载失败的
}
}
setWorkflows(loadedWorkflows);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败');
} finally {
setLoading(false);
}
};
const startDefaultWorkflow = async () => {
setStarting('default-dev-flow');
try {
const workflow = await api.workflow.start('default-dev-flow.yaml');
setWorkflows(prev => [...prev.filter(w => w.workflow_id !== workflow.workflow_id), workflow]);
setSelectedWorkflow(workflow);
} catch (err) {
alert('启动默认工作流失败: ' + (err instanceof Error ? err.message : '未知错误'));
} finally {
setStarting(null);
}
};
useEffect(() => {
loadWorkflows();
const interval = setInterval(loadWorkflows, 30000);
return () => clearInterval(interval);
}, []);
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.name.endsWith('.yaml') && !file.name.endsWith('.yml')) {
alert('请上传 YAML 文件');
return;
}
setUploading(true);
try {
await file.text();
// 这里应该调用后端 API 上传文件
// 暂时模拟成功
alert(`文件 ${file.name} 上传成功`);
loadWorkflows();
} catch (err) {
alert('上传失败');
} finally {
setUploading(false);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return '#00ff9d';
case 'in_progress':
return '#00f0ff';
default:
return '#ff9500';
}
};
if (loading && workflows.length === 0) {
return (
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
<div style={{ color: '#00f0ff' }}>...</div>
</div>
);
}
return (
<div>
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<div>
<h1 style={{ fontSize: 28, fontWeight: 700, color: '#fff', margin: 0 }}></h1>
<p style={{ fontSize: 14, color: 'rgba(255, 255, 255, 0.5)', margin: '8px 0 0 0' }}>
-
</p>
</div>
<div style={{ display: 'flex', gap: 12 }}>
<button
onClick={loadWorkflows}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '10px 16px',
background: 'rgba(255, 255, 255, 0.05)',
border: '1px solid rgba(255, 255, 255, 0.1)',
borderRadius: 8,
color: 'rgba(255, 255, 255, 0.7)',
fontSize: 14,
cursor: 'pointer',
}}
>
<RefreshCw size={16} />
</button>
<button
onClick={startDefaultWorkflow}
disabled={starting === 'default-dev-flow'}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '10px 16px',
background: 'rgba(0, 240, 255, 0.1)',
border: '1px solid rgba(0, 240, 255, 0.3)',
borderRadius: 8,
color: '#00f0ff',
fontSize: 14,
cursor: starting === 'default-dev-flow' ? 'not-allowed' : 'pointer',
opacity: starting === 'default-dev-flow' ? 0.6 : 1,
}}
>
<Zap size={16} />
{starting === 'default-dev-flow' ? '启动中...' : '启动默认工作流'}
</button>
<label
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '10px 16px',
background: 'linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%)',
border: 'none',
borderRadius: 8,
color: '#000',
fontSize: 14,
fontWeight: 600,
cursor: uploading ? 'not-allowed' : 'pointer',
opacity: uploading ? 0.6 : 1,
}}
>
<Upload size={18} />
{uploading ? '上传中...' : '上传工作流'}
<input type="file" accept=".yaml,.yml" onChange={handleFileUpload} disabled={uploading} style={{ display: 'none' }} />
</label>
</div>
</div>
{error && (
<div
style={{
padding: '12px 16px',
background: '#ff006e20',
border: '1px solid #ff006e50',
borderRadius: 8,
color: '#ff006e',
marginBottom: 24,
}}
>
: {error}
</div>
)}
{/* Workflows Grid */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
gap: 16,
}}
>
{workflows.map((workflow) => (
<div
key={workflow.workflow_id}
onClick={() => setSelectedWorkflow(workflow)}
style={{
padding: 20,
background: 'rgba(17, 24, 39, 0.7)',
borderRadius: 12,
border: '1px solid rgba(0, 240, 255, 0.1)',
cursor: 'pointer',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'rgba(0, 240, 255, 0.3)';
e.currentTarget.style.transform = 'translateY(-2px)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'rgba(0, 240, 255, 0.1)';
e.currentTarget.style.transform = 'translateY(0)';
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12 }}>
<div
style={{
width: 48,
height: 48,
borderRadius: 10,
background: `linear-gradient(135deg, ${getStatusColor(workflow.status)}40 0%, ${getStatusColor(workflow.status)}10 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Workflow size={24} color={getStatusColor(workflow.status)} />
</div>
<span
style={{
padding: '4px 10px',
borderRadius: 12,
background: `${getStatusColor(workflow.status)}20`,
color: getStatusColor(workflow.status),
fontSize: 11,
}}
>
{workflow.status === 'completed' ? '已完成' : workflow.status === 'in_progress' ? '进行中' : '等待中'}
</span>
</div>
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: '0 0 4px 0' }}>
{workflow.name}
</h3>
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '0 0 12px 0' }}>
{workflow.workflow_id}
</p>
<p
style={{
fontSize: 13,
color: 'rgba(255, 255, 255, 0.6)',
margin: '0 0 16px 0',
lineHeight: 1.5,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{workflow.description}
</p>
{/* Progress */}
<div style={{ marginBottom: 12 }}>
<div
style={{
height: 6,
background: 'rgba(255, 255, 255, 0.1)',
borderRadius: 3,
overflow: 'hidden',
}}
>
<div
style={{
width: workflow.progress,
height: '100%',
background: 'linear-gradient(90deg, #00f0ff, #8b5cf6)',
borderRadius: 3,
}}
/>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<FileText size={14} color="rgba(255, 255, 255, 0.4)" />
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}>
{workflow.meetings.length}
</span>
{/* 统计节点类型 */}
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.3)' }}>
({workflow.meetings.filter(m => m.node_type === 'execution').length} + {workflow.meetings.filter(m => m.node_type === 'meeting').length} )
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}></span>
<ChevronRight size={14} color="rgba(255, 255, 255, 0.4)" />
</div>
</div>
</div>
))}
</div>
{workflows.length === 0 && !loading && (
<div style={{ textAlign: 'center', padding: 80 }}>
<Workflow size={48} color="rgba(255, 255, 255, 0.2)" style={{ marginBottom: 16 }} />
<p style={{ color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}></p>
<p style={{ color: 'rgba(255, 255, 255, 0.3)', fontSize: 14 }}> YAML </p>
</div>
)}
{/* Detail Panel */}
{selectedWorkflow && (
<WorkflowDetailPanel
workflow={selectedWorkflow}
onClose={() => setSelectedWorkflow(null)}
onReload={loadWorkflows}
/>
)}
</div>
);
}