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

363 lines
16 KiB
TypeScript

import { useState, useEffect } from 'react';
import { HardDrive, Lock, Unlock, Activity, Clock, FileText, RefreshCw } from 'lucide-react';
import { api } from '../lib/api';
import type { FileLock, Heartbeat, AgentResourceStatus } from '../types';
export function ResourcesPage() {
const [locks, setLocks] = useState<FileLock[]>([]);
const [heartbeats, setHeartbeats] = useState<Record<string, Heartbeat>>({});
const [statuses, setStatuses] = useState<AgentResourceStatus[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'locks' | 'heartbeats' | 'status'>('locks');
const loadData = async () => {
try {
setLoading(true);
const [locksRes, heartbeatsRes, statusRes] = await Promise.all([
api.lock.list().catch(() => ({ locks: [] })),
api.heartbeat.list().catch(() => ({ heartbeats: {} })),
api.resource.getAllStatus().catch(() => ({ agents: [] })),
]);
setLocks(locksRes.locks);
setHeartbeats(heartbeatsRes.heartbeats);
setStatuses(statusRes.agents);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : '加载失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadData();
const interval = setInterval(loadData, 5000); // 5秒刷新一次
return () => clearInterval(interval);
}, []);
const handleReleaseLock = async (filePath: string, agentId: string) => {
if (!confirm(`确定要释放 ${filePath} 的锁吗?`)) return;
try {
await api.lock.release(filePath, agentId);
loadData();
} 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';
}
};
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' }}>
Agent
</p>
</div>
{loading && (
<span style={{ fontSize: 12, color: '#00f0ff', marginRight: 12 }}>...</span>
)}
<button
onClick={loadData}
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>
</div>
{error && (
<div
style={{
padding: '12px 16px',
background: '#ff006e20',
border: '1px solid #ff006e50',
borderRadius: 8,
color: '#ff006e',
marginBottom: 24,
}}
>
: {error}
</div>
)}
{/* Tabs */}
<div style={{ display: 'flex', gap: 8, marginBottom: 24 }}>
{[
{ id: 'locks', label: '文件锁', icon: Lock, count: locks.length },
{ id: 'heartbeats', label: '心跳状态', icon: Activity, count: Object.keys(heartbeats).length },
{ id: 'status', label: 'Agent 状态', icon: HardDrive, count: statuses.length },
].map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as typeof activeTab)}
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '12px 20px',
background: activeTab === tab.id ? 'rgba(0, 240, 255, 0.1)' : 'rgba(255, 255, 255, 0.05)',
border: `1px solid ${activeTab === tab.id ? 'rgba(0, 240, 255, 0.3)' : 'rgba(255, 255, 255, 0.1)'}`,
borderRadius: 8,
color: activeTab === tab.id ? '#00f0ff' : 'rgba(255, 255, 255, 0.7)',
fontSize: 14,
cursor: 'pointer',
}}
>
<tab.icon size={16} />
{tab.label}
<span
style={{
marginLeft: 4,
padding: '2px 8px',
background: activeTab === tab.id ? 'rgba(0, 240, 255, 0.2)' : 'rgba(255, 255, 255, 0.1)',
borderRadius: 10,
fontSize: 12,
}}
>
{tab.count}
</span>
</button>
))}
</div>
{/* Content */}
{activeTab === 'locks' && (
<div style={{ background: 'rgba(17, 24, 39, 0.7)', borderRadius: 12, border: '1px solid rgba(0, 240, 255, 0.1)' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid rgba(0, 240, 255, 0.1)' }}>
<th style={{ padding: '16px 20px', textAlign: 'left', fontSize: 12, color: 'rgba(255, 255, 255, 0.5)', fontWeight: 500 }}></th>
<th style={{ padding: '16px 20px', textAlign: 'left', fontSize: 12, color: 'rgba(255, 255, 255, 0.5)', fontWeight: 500 }}></th>
<th style={{ padding: '16px 20px', textAlign: 'left', fontSize: 12, color: 'rgba(255, 255, 255, 0.5)', fontWeight: 500 }}></th>
<th style={{ padding: '16px 20px', textAlign: 'center', fontSize: 12, color: 'rgba(255, 255, 255, 0.5)', fontWeight: 500 }}></th>
</tr>
</thead>
<tbody>
{locks.map((lock, index) => (
<tr key={index} style={{ borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
<td style={{ padding: '14px 20px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<FileText size={16} color="#00f0ff" />
<span style={{ fontSize: 14, color: '#fff', fontFamily: 'monospace' }}>{lock.file_path}</span>
</div>
</td>
<td style={{ padding: '14px 20px' }}>
<span style={{ fontSize: 14, color: '#fff' }}>{lock.agent_name}</span>
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', marginLeft: 8 }}>({lock.agent_id})</span>
</td>
<td style={{ padding: '14px 20px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<Clock size={14} color="rgba(255, 255, 255, 0.4)" />
<span style={{ fontSize: 13, color: 'rgba(255, 255, 255, 0.7)' }}>{lock.elapsed_display}</span>
</div>
</td>
<td style={{ padding: '14px 20px', textAlign: 'center' }}>
<button
onClick={() => handleReleaseLock(lock.file_path, lock.agent_id)}
style={{
padding: '6px 12px',
background: '#ff006e20',
border: '1px solid #ff006e50',
borderRadius: 6,
color: '#ff006e',
fontSize: 12,
cursor: 'pointer',
}}
>
</button>
</td>
</tr>
))}
</tbody>
</table>
{locks.length === 0 && (
<div style={{ textAlign: 'center', padding: 60 }}>
<Unlock size={48} color="rgba(255, 255, 255, 0.2)" style={{ marginBottom: 16 }} />
<p style={{ color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}></p>
</div>
)}
</div>
)}
{activeTab === 'heartbeats' && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: 16 }}>
{Object.entries(heartbeats).map(([agentId, hb]) => (
<div
key={agentId}
style={{
padding: 20,
background: 'rgba(17, 24, 39, 0.7)',
borderRadius: 12,
border: `1px solid ${hb.is_timeout ? '#ff006e50' : 'rgba(0, 240, 255, 0.1)'}`,
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<span style={{ fontSize: 14, fontWeight: 600, color: '#fff' }}>{agentId}</span>
{hb.is_timeout ? (
<span style={{ padding: '4px 10px', borderRadius: 12, background: '#ff006e20', color: '#ff006e', fontSize: 11 }}>
</span>
) : (
<span style={{ padding: '4px 10px', borderRadius: 12, background: '#00ff9d20', color: '#00ff9d', fontSize: 11 }}>
</span>
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}></span>
<span style={{ fontSize: 12, color: getStatusColor(hb.status) }}>{hb.status}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}></span>
<span style={{ fontSize: 12, color: '#fff' }}>{hb.current_task || '-'}</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}></span>
<span style={{ fontSize: 12, color: '#00f0ff' }}>{hb.progress}%</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.5)' }}></span>
<span style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.7)' }}>{hb.elapsed_display}</span>
</div>
</div>
<div style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid rgba(255, 255, 255, 0.05)' }}>
<div
style={{
height: 4,
background: 'rgba(255, 255, 255, 0.1)',
borderRadius: 2,
overflow: 'hidden',
}}
>
<div
style={{
width: `${hb.progress}%`,
height: '100%',
background: hb.is_timeout ? '#ff006e' : '#00ff9d',
borderRadius: 2,
}}
/>
</div>
</div>
</div>
))}
{Object.keys(heartbeats).length === 0 && (
<div style={{ textAlign: 'center', padding: 60, gridColumn: '1 / -1' }}>
<Activity size={48} color="rgba(255, 255, 255, 0.2)" style={{ marginBottom: 16 }} />
<p style={{ color: 'rgba(255, 255, 255, 0.5)', margin: 0 }}></p>
</div>
)}
</div>
)}
{activeTab === 'status' && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))', gap: 16 }}>
{statuses.map((status) => (
<div
key={status.agent_id}
style={{
padding: 20,
background: 'rgba(17, 24, 39, 0.7)',
borderRadius: 12,
border: '1px solid rgba(0, 240, 255, 0.1)',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<div>
<h4 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: 0 }}>{status.info.name}</h4>
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>{status.agent_id}</p>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<span
style={{
padding: '4px 10px',
borderRadius: 12,
background: `${getStatusColor(status.heartbeat.status)}20`,
color: getStatusColor(status.heartbeat.status),
fontSize: 11,
}}
>
{status.heartbeat.status}
</span>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
<div style={{ padding: 12, background: 'rgba(0, 0, 0, 0.2)', borderRadius: 8 }}>
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.5)', margin: '0 0 4px 0' }}></p>
<p style={{ fontSize: 13, color: '#fff', margin: 0 }}>{status.info.role}</p>
</div>
<div style={{ padding: 12, background: 'rgba(0, 0, 0, 0.2)', borderRadius: 8 }}>
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.5)', margin: '0 0 4px 0' }}></p>
<p style={{ fontSize: 13, color: '#fff', margin: 0 }}>{status.info.model}</p>
</div>
</div>
<div style={{ marginTop: 12, padding: 12, background: 'rgba(0, 0, 0, 0.2)', borderRadius: 8 }}>
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.5)', margin: '0 0 4px 0' }}></p>
<p style={{ fontSize: 13, color: '#00f0ff', margin: 0 }}>{status.state.task || '-'}</p>
</div>
{status.locks.length > 0 && (
<div style={{ marginTop: 12 }}>
<p style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.5)', margin: '0 0 8px 0' }}>
({status.locks.length})
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{status.locks.map((lock, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Lock size={12} color="#ff9500" />
<span style={{ fontSize: 12, color: '#ff9500', fontFamily: 'monospace' }}>{lock.file}</span>
<span style={{ fontSize: 11, color: 'rgba(255, 255, 255, 0.4)' }}>({lock.elapsed})</span>
</div>
))}
</div>
</div>
)}
</div>
))}
{statuses.length === 0 && (
<div style={{ textAlign: 'center', padding: 60, gridColumn: '1 / -1' }}>
<HardDrive 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>
</div>
)}
</div>
)}
</div>
);
}