Files
multiAgentTry/frontend/src/pages/AgentsPage.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

973 lines
29 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 { Plus, Users, Activity, Cpu, RefreshCw, Play, Square, Power } from 'lucide-react';
import { api } from '../lib/api';
import type { Agent, AgentState } from '../types';
// 注册 Agent 模态框
function RegisterModal({
isOpen,
onClose,
onSubmit,
}: {
isOpen: boolean;
onClose: () => void;
onSubmit: (data: {
agent_id: string;
name: string;
role: string;
model: string;
description: string;
}) => void;
}) {
const [form, setForm] = useState({
agent_id: '',
name: '',
role: 'developer',
model: 'claude-opus-4.6',
description: '',
});
if (!isOpen) return null;
return (
<div
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}
onClick={onClose}
>
<div
style={{
width: 480,
background: 'rgba(17, 24, 39, 0.95)',
borderRadius: 12,
border: '1px solid rgba(0, 240, 255, 0.2)',
padding: 24,
}}
onClick={(e) => e.stopPropagation()}
>
<h2
style={{
fontSize: 20,
fontWeight: 700,
color: '#fff',
margin: 0,
marginBottom: 20,
}}
>
Agent
</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div>
<label
style={{
display: 'block',
fontSize: 12,
color: 'rgba(255, 255, 255, 0.6)',
marginBottom: 6,
}}
>
Agent ID
</label>
<input
type="text"
value={form.agent_id}
onChange={(e) => setForm({ ...form, agent_id: e.target.value })}
placeholder="例如: claude-001"
style={{
width: '100%',
padding: '10px 14px',
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(0, 240, 255, 0.2)',
borderRadius: 8,
color: '#fff',
fontSize: 14,
outline: 'none',
}}
/>
</div>
<div>
<label
style={{
display: 'block',
fontSize: 12,
color: 'rgba(255, 255, 255, 0.6)',
marginBottom: 6,
}}
>
</label>
<input
type="text"
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="例如: Claude Code"
style={{
width: '100%',
padding: '10px 14px',
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(0, 240, 255, 0.2)',
borderRadius: 8,
color: '#fff',
fontSize: 14,
outline: 'none',
}}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<div>
<label
style={{
display: 'block',
fontSize: 12,
color: 'rgba(255, 255, 255, 0.6)',
marginBottom: 6,
}}
>
</label>
<select
value={form.role}
onChange={(e) => setForm({ ...form, role: e.target.value })}
style={{
width: '100%',
padding: '10px 14px',
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(0, 240, 255, 0.2)',
borderRadius: 8,
color: '#fff',
fontSize: 14,
outline: 'none',
}}
>
<option value="architect"> (architect)</option>
<option value="pm"> (pm)</option>
<option value="developer"> (developer)</option>
<option value="qa"> (qa)</option>
<option value="reviewer"> (reviewer)</option>
<option value="human"> (human)</option>
</select>
</div>
<div>
<label
style={{
display: 'block',
fontSize: 12,
color: 'rgba(255, 255, 255, 0.6)',
marginBottom: 6,
}}
>
</label>
<input
type="text"
value={form.model}
onChange={(e) => setForm({ ...form, model: e.target.value })}
placeholder="模型名称"
style={{
width: '100%',
padding: '10px 14px',
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(0, 240, 255, 0.2)',
borderRadius: 8,
color: '#fff',
fontSize: 14,
outline: 'none',
}}
/>
</div>
</div>
<div>
<label
style={{
display: 'block',
fontSize: 12,
color: 'rgba(255, 255, 255, 0.6)',
marginBottom: 6,
}}
>
</label>
<textarea
value={form.description}
onChange={(e) => setForm({ ...form, description: e.target.value })}
placeholder="Agent 的职责描述"
rows={3}
style={{
width: '100%',
padding: '10px 14px',
background: 'rgba(0, 0, 0, 0.3)',
border: '1px solid rgba(0, 240, 255, 0.2)',
borderRadius: 8,
color: '#fff',
fontSize: 14,
outline: 'none',
resize: 'none',
}}
/>
</div>
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
<button
onClick={onClose}
style={{
flex: 1,
padding: '12px 20px',
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',
}}
>
</button>
<button
onClick={() => {
if (form.agent_id && form.name) {
onSubmit(form);
onClose();
setForm({
agent_id: '',
name: '',
role: 'developer',
model: 'claude-opus-4.6',
description: '',
});
}
}}
style={{
flex: 1,
padding: '12px 20px',
background: 'linear-gradient(135deg, #00f0ff 0%, #8b5cf6 100%)',
border: 'none',
borderRadius: 8,
color: '#000',
fontSize: 14,
fontWeight: 600,
cursor: 'pointer',
}}
>
</button>
</div>
</div>
</div>
</div>
);
}
// Agent 详情面板
function AgentDetailPanel({
agent,
state,
onClose,
}: {
agent: Agent;
state: AgentState | null;
onClose: () => void;
}) {
const getStatusColor = (status: string) => {
switch (status) {
case 'working':
return '#00ff9d';
case 'idle':
return '#00f0ff';
case 'waiting':
return '#ff9500';
case 'error':
return '#ff006e';
default:
return '#666';
}
};
return (
<div
style={{
position: 'fixed',
right: 0,
top: 0,
bottom: 0,
width: 400,
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: 24 }}>
<h2 style={{ fontSize: 20, fontWeight: 700, color: '#fff', margin: 0 }}>
Agent
</h2>
<button
onClick={onClose}
style={{
background: 'none',
border: 'none',
color: 'rgba(255, 255, 255, 0.5)',
fontSize: 24,
cursor: 'pointer',
}}
>
×
</button>
</div>
<div
style={{
padding: 20,
background: 'rgba(0, 0, 0, 0.3)',
borderRadius: 12,
marginBottom: 20,
}}
>
<div
style={{
width: 64,
height: 64,
borderRadius: 12,
background: `linear-gradient(135deg, ${getStatusColor(agent.status)}40 0%, ${getStatusColor(agent.status)}10 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
}}
>
<Users size={32} color={getStatusColor(agent.status)} />
</div>
<h3 style={{ fontSize: 18, fontWeight: 600, color: '#fff', margin: '0 0 4px 0' }}>
{agent.name}
</h3>
<p style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}>
{agent.agent_id}
</p>
<div
style={{
display: 'flex',
gap: 8,
marginTop: 12,
}}
>
<span
style={{
padding: '4px 12px',
borderRadius: 12,
background: `${getStatusColor(agent.status)}20`,
color: getStatusColor(agent.status),
fontSize: 12,
textTransform: 'capitalize',
}}
>
{agent.status}
</span>
<span
style={{
padding: '4px 12px',
borderRadius: 12,
background: 'rgba(139, 92, 246, 0.2)',
color: '#8b5cf6',
fontSize: 12,
}}
>
{agent.role}
</span>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div
style={{
padding: 16,
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<Cpu size={16} color="#00f0ff" />
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}></span>
</div>
<p style={{ fontSize: 14, color: '#fff', margin: 0 }}>{agent.model}</p>
</div>
{state && (
<>
<div
style={{
padding: 16,
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<Activity size={16} color="#00ff9d" />
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}></span>
</div>
<p style={{ fontSize: 14, color: '#fff', margin: 0 }}>{state.current_task || '无'}</p>
</div>
<div
style={{
padding: 16,
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}></span>
</div>
<div
style={{
height: 8,
background: 'rgba(255, 255, 255, 0.1)',
borderRadius: 4,
overflow: 'hidden',
}}
>
<div
style={{
width: `${state.progress}%`,
height: '100%',
background: 'linear-gradient(90deg, #00f0ff, #8b5cf6)',
borderRadius: 4,
}}
/>
</div>
<p style={{ fontSize: 12, color: '#00f0ff', margin: '8px 0 0 0' }}>
{state.progress}%
</p>
</div>
{state.working_files.length > 0 && (
<div
style={{
padding: 16,
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}></span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{state.working_files.map((file, i) => (
<span
key={i}
style={{
fontSize: 13,
color: '#00f0ff',
fontFamily: 'monospace',
}}
>
{file}
</span>
))}
</div>
</div>
)}
<div
style={{
padding: 16,
background: 'rgba(0, 0, 0, 0.2)',
borderRadius: 8,
}}
>
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.6)' }}></span>
<p style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
{new Date(state.last_update).toLocaleString()}
</p>
</div>
</>
)}
</div>
</div>
);
}
// 运行中的 Agent 状态
interface RunningAgent {
agent_id: string;
status: string;
is_alive: boolean;
uptime: number | null;
restart_count: number;
}
export function AgentsPage() {
const [agents, setAgents] = useState<Agent[]>([]);
const [runningAgents, setRunningAgents] = useState<Record<string, RunningAgent>>({});
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
const [agentState, setAgentState] = useState<AgentState | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// API 基础 URL
const API_BASE = 'http://localhost:8000/api';
// 加载 Agent 列表
const loadAgents = async () => {
try {
setLoading(true);
const res = await api.agent.list();
setAgents(res.agents);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败');
} finally {
setLoading(false);
}
};
// 加载运行中的 Agent
const loadRunningAgents = async () => {
try {
const res = await fetch(`${API_BASE}/agents/control/list`);
if (res.ok) {
const data = await res.json();
const runningMap: Record<string, RunningAgent> = {};
data.forEach((agent: RunningAgent) => {
runningMap[agent.agent_id] = agent;
});
setRunningAgents(runningMap);
}
} catch (err) {
console.error('加载运行状态失败:', err);
}
};
// 启动 Agent
const startAgent = async (agentId: string, agent: Agent) => {
try {
const res = await fetch(`${API_BASE}/agents/control/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: agentId,
name: agent.name,
role: agent.role,
model: agent.model,
agent_type: 'native_llm'
})
});
if (res.ok) {
await loadRunningAgents();
} else {
const data = await res.json();
alert(`启动失败: ${data.message || '未知错误'}`);
}
} catch (err) {
alert(`启动失败: ${err instanceof Error ? err.message : '未知错误'}`);
}
};
// 停止 Agent
const stopAgent = async (agentId: string) => {
try {
const res = await fetch(`${API_BASE}/agents/control/stop`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: agentId,
graceful: true
})
});
if (res.ok) {
await loadRunningAgents();
} else {
alert('停止失败');
}
} catch (err) {
alert(`停止失败: ${err instanceof Error ? err.message : '未知错误'}`);
}
};
// 加载 Agent 状态
const loadAgentState = async (agentId: string) => {
try {
const state = await api.agent.getState(agentId);
setAgentState(state);
} catch (err) {
setAgentState(null);
}
};
useEffect(() => {
loadAgents();
loadRunningAgents();
const interval = setInterval(() => {
loadAgents();
loadRunningAgents();
}, 10000);
return () => clearInterval(interval);
}, []);
// 当选中 Agent 时加载状态
useEffect(() => {
if (selectedAgent) {
loadAgentState(selectedAgent.agent_id);
}
}, [selectedAgent]);
// 注册 Agent
const handleRegister = async (data: {
agent_id: string;
name: string;
role: string;
model: string;
description: string;
}) => {
try {
await api.agent.register(data as Omit<Agent, 'status' | 'created_at'>);
loadAgents();
} catch (err) {
alert(err instanceof Error ? err.message : '注册失败');
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'working':
return '#00ff9d';
case 'idle':
return '#00f0ff';
case 'waiting':
return '#ff9500';
case 'error':
return '#ff006e';
default:
return '#666';
}
};
const getRoleLabel = (role: string) => {
const labels: Record<string, string> = {
architect: '架构师',
pm: '产品经理',
developer: '开发者',
qa: '测试工程师',
reviewer: '审查者',
human: '人类',
};
return labels[role] || role;
};
if (loading && agents.length === 0) {
return (
<div style={{ display: 'flex', justifyContent: 'center', padding: 100 }}>
<div style={{ color: '#00f0ff' }}>...</div>
</div>
);
}
return (
<div>
{/* 添加脉动动画样式 */}
<style>{`
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
`}</style>
{/* Header */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 24,
}}
>
<div>
<h1 style={{ fontSize: 28, fontWeight: 700, color: '#fff', margin: 0 }}>Agent </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={loadAgents}
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={() => setIsModalOpen(true)}
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: 'pointer',
}}
>
<Plus size={18} />
Agent
</button>
</div>
</div>
{error && (
<div
style={{
padding: '12px 16px',
background: '#ff006e20',
border: '1px solid #ff006e50',
borderRadius: 8,
color: '#ff006e',
marginBottom: 24,
}}
>
: {error}
</div>
)}
{/* Agent Grid */}
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: 16,
}}
>
{agents.map((agent) => (
<div
key={agent.agent_id}
onClick={() => setSelectedAgent(agent)}
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', alignItems: 'flex-start', gap: 12 }}>
<div
style={{
width: 48,
height: 48,
borderRadius: 10,
background: `linear-gradient(135deg, ${getStatusColor(agent.status)}40 0%, ${getStatusColor(agent.status)}10 100%)`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
}}
>
<Users size={24} color={getStatusColor(agent.status)} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<h3
style={{
fontSize: 16,
fontWeight: 600,
color: '#fff',
margin: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{agent.name}
</h3>
<p
style={{
fontSize: 12,
color: 'rgba(255, 255, 255, 0.4)',
margin: '4px 0 0 0',
}}
>
{agent.agent_id}
</p>
</div>
<div
style={{
width: 8,
height: 8,
borderRadius: '50%',
background: getStatusColor(agent.status),
boxShadow: `0 0 8px ${getStatusColor(agent.status)}`,
flexShrink: 0,
}}
/>
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
<span
style={{
padding: '4px 10px',
borderRadius: 12,
background: `${getStatusColor(agent.status)}20`,
color: getStatusColor(agent.status),
fontSize: 11,
}}
>
{agent.status}
</span>
<span
style={{
padding: '4px 10px',
borderRadius: 12,
background: 'rgba(139, 92, 246, 0.2)',
color: '#8b5cf6',
fontSize: 11,
}}
>
{getRoleLabel(agent.role)}
</span>
</div>
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid rgba(255, 255, 255, 0.05)' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: 0 }}>
: {agent.model}
</p>
{/* 运行状态指示 */}
{runningAgents[agent.agent_id] ? (
<span style={{ fontSize: 11, color: '#00ff9d', display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#00ff9d', animation: 'pulse 2s infinite' }} />
</span>
) : (
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.3)' }}>
</span>
)}
</div>
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.3)', margin: '4px 0 0 0' }}>
: {new Date(agent.created_at).toLocaleDateString()}
</p>
{/* 启动/停止按钮 */}
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
{runningAgents[agent.agent_id] ? (
<button
onClick={(e) => {
e.stopPropagation();
stopAgent(agent.agent_id);
}}
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
padding: '8px 12px',
background: 'rgba(255, 0, 110, 0.2)',
border: '1px solid rgba(255, 0, 110, 0.3)',
borderRadius: 6,
color: '#ff006e',
fontSize: 12,
cursor: 'pointer',
}}
>
<Square size={14} />
</button>
) : (
<button
onClick={(e) => {
e.stopPropagation();
startAgent(agent.agent_id, agent);
}}
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
padding: '8px 12px',
background: 'rgba(0, 255, 157, 0.2)',
border: '1px solid rgba(0, 255, 157, 0.3)',
borderRadius: 6,
color: '#00ff9d',
fontSize: 12,
cursor: 'pointer',
}}
>
<Play size={14} />
</button>
)}
</div>
{/* 显示运行时长 */}
{runningAgents[agent.agent_id]?.uptime && (
<p style={{ fontSize: 11, color: 'rgba(0, 255, 157, 0.6)', margin: '8px 0 0 0' }}>
: {Math.floor(runningAgents[agent.agent_id].uptime! / 60)}
</p>
)}
</div>
</div>
))}
</div>
{agents.length === 0 && !loading && (
<div style={{ textAlign: 'center', padding: 80 }}>
<Users size={48} color="rgba(255, 255, 255, 0.2)" style={{ marginBottom: 16 }} />
<p style={{ color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}> Agent</p>
<p style={{ color: 'rgba(255, 255, 255, 0.3)', fontSize: 14 }}>"注册 Agent"</p>
</div>
)}
{/* Modals */}
<RegisterModal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} onSubmit={handleRegister} />
{selectedAgent && (
<AgentDetailPanel
agent={selectedAgent}
state={agentState}
onClose={() => {
setSelectedAgent(null);
setAgentState(null);
}}
/>
)}
</div>
);
}