完整实现 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>
This commit is contained in:
Claude Code
2026-03-09 17:32:11 +08:00
commit dc398d7c7b
118 changed files with 23120 additions and 0 deletions

View File

@@ -0,0 +1,972 @@
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>
);
}