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>
|
|||
|
|
);
|
|||
|
|
}
|