- 新增 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>
973 lines
29 KiB
TypeScript
973 lines
29 KiB
TypeScript
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>
|
||
);
|
||
}
|