重构 API 路由并新增工作流编排功能

后端:
- 重构 agents, heartbeats, locks, meetings, resources, roles, workflows 路由
- 新增 orchestrator 和 providers 路由
- 新增 CLI 调用器和流程编排服务
- 添加日志配置和依赖项

前端:
- 更新 AgentsPage、SettingsPage、WorkflowPage 页面
- 扩展 api.ts 新增 API 接口

其他:
- 清理测试 agent 数据文件
- 新增示例工作流和项目审计报告

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Code
2026-03-10 16:36:25 +08:00
parent 7a5a58b4e5
commit 1719d1f1f9
54 changed files with 3175 additions and 612 deletions
+95 -2
View File
@@ -11,8 +11,23 @@ import type {
AgentResourceStatus,
} from '../types';
// API 基础地址
const API_BASE = 'http://localhost:8000/api';
// API 基础地址:优先读取 localStorageSettings 页面配置),否则使用默认值
function getApiBase(): string {
try {
const saved = localStorage.getItem('swarm-settings');
if (saved) {
const settings = JSON.parse(saved);
if (settings.apiBaseUrl) {
return settings.apiBaseUrl.replace(/\/+$/, '');
}
}
} catch {
// ignore parse errors
}
return 'http://localhost:8000/api';
}
const API_BASE = getApiBase();
// 通用请求函数
async function request<T>(
@@ -336,6 +351,47 @@ export const roleApi = {
}),
};
// ==================== Agent 控制 API ====================
export const agentControlApi = {
// 启动 Agent
start: (agentId: string, agentType: string, model?: string) =>
request<{ success: boolean; agent_id: string; status: string }>('/agents/control/start', {
method: 'POST',
body: JSON.stringify({ agent_id: agentId, agent_type: agentType, model }),
}),
// 停止 Agent
stop: (agentId: string) =>
request<{ success: boolean; agent_id: string }>('/agents/control/stop', {
method: 'POST',
body: JSON.stringify({ agent_id: agentId }),
}),
// 重启 Agent
restart: (agentId: string) =>
request<{ success: boolean; agent_id: string }>('/agents/control/restart', {
method: 'POST',
body: JSON.stringify({ agent_id: agentId }),
}),
// 获取 Agent 运行状态
getStatus: (agentId: string) =>
request<{ agent_id: string; status: string; pid?: number; uptime?: number }>(`/agents/control/status/${agentId}`),
// 获取所有运行中的 Agent
list: () =>
request<{ agents: Array<{ agent_id: string; status: string; pid?: number; agent_type?: string }> }>('/agents/control/list'),
// 获取进程管理器摘要
summary: () =>
request<{ total: number; running: number; stopped: number }>('/agents/control/summary'),
// 健康检查
health: () =>
request<{ status: string }>('/agents/control/health'),
};
// ==================== 系统 API ====================
export const systemApi = {
@@ -424,9 +480,45 @@ export const humanApi = {
}),
};
// ==================== Provider API ====================
export const providerApi = {
list: () =>
request<{
cli: Array<{
id: string;
type: string;
display_name: string;
description: string;
installed: boolean;
path: string;
models: string[];
}>;
api: Array<{
id: string;
type: string;
display_name: string;
env_key: string;
configured: boolean;
models: string[];
}>;
}>('/providers'),
models: () =>
request<{
models: Array<{
value: string;
label: string;
provider: string;
type: string;
}>;
}>('/providers/models'),
};
// 导出所有 API
export const api = {
agent: agentApi,
agentControl: agentControlApi,
lock: lockApi,
heartbeat: heartbeatApi,
meeting: meetingApi,
@@ -435,6 +527,7 @@ export const api = {
role: roleApi,
system: systemApi,
human: humanApi,
provider: providerApi,
};
export default api;
+85 -58
View File
@@ -1,8 +1,15 @@
import { useState, useEffect } from 'react';
import { Plus, Users, Activity, Cpu, RefreshCw, Play, Square, Power } from 'lucide-react';
import { api } from '../lib/api';
import { Plus, Users, Activity, Cpu, RefreshCw, Play, Square, Trash2 } from 'lucide-react';
import { api, agentControlApi } from '../lib/api';
import type { Agent, AgentState } from '../types';
interface ModelOption {
value: string;
label: string;
provider: string;
type: string;
}
// 注册 Agent 模态框
function RegisterModal({
isOpen,
@@ -23,9 +30,21 @@ function RegisterModal({
agent_id: '',
name: '',
role: 'developer',
model: 'claude-opus-4.6',
model: '',
description: '',
});
const [modelOptions, setModelOptions] = useState<ModelOption[]>([]);
useEffect(() => {
if (isOpen) {
api.provider.models().then((data) => {
setModelOptions(data.models);
if (data.models.length > 0 && !form.model) {
setForm((f) => ({ ...f, model: data.models[0].value }));
}
}).catch(() => {});
}
}, [isOpen]);
if (!isOpen) return null;
@@ -167,13 +186,11 @@ function RegisterModal({
marginBottom: 6,
}}
>
/ CLI
</label>
<input
type="text"
<select
value={form.model}
onChange={(e) => setForm({ ...form, model: e.target.value })}
placeholder="模型名称"
style={{
width: '100%',
padding: '10px 14px',
@@ -184,7 +201,17 @@ function RegisterModal({
fontSize: 14,
outline: 'none',
}}
/>
>
{modelOptions.length > 0 ? (
modelOptions.map((m) => (
<option key={m.value} value={m.value}>
{m.label}
</option>
))
) : (
<option value="">...</option>
)}
</select>
</div>
</div>
@@ -236,14 +263,14 @@ function RegisterModal({
</button>
<button
onClick={() => {
if (form.agent_id && form.name) {
if (form.agent_id && form.name && form.model) {
onSubmit(form);
onClose();
setForm({
agent_id: '',
name: '',
role: 'developer',
model: 'claude-opus-4.6',
model: modelOptions.length > 0 ? modelOptions[0].value : '',
description: '',
});
}
@@ -504,9 +531,11 @@ function AgentDetailPanel({
interface RunningAgent {
agent_id: string;
status: string;
is_alive: boolean;
uptime: number | null;
restart_count: number;
is_alive?: boolean;
uptime?: number | null;
restart_count?: number;
pid?: number;
agent_type?: string;
}
export function AgentsPage() {
@@ -518,9 +547,6 @@ export function AgentsPage() {
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 {
@@ -538,15 +564,12 @@ export function AgentsPage() {
// 加载运行中的 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);
}
const data = await agentControlApi.list();
const runningMap: Record<string, RunningAgent> = {};
(data.agents || []).forEach((agent: RunningAgent) => {
runningMap[agent.agent_id] = agent;
});
setRunningAgents(runningMap);
} catch (err) {
console.error('加载运行状态失败:', err);
}
@@ -555,24 +578,8 @@ export function AgentsPage() {
// 启动 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 || '未知错误'}`);
}
await agentControlApi.start(agentId, 'native_llm', agent.model);
await loadRunningAgents();
} catch (err) {
alert(`启动失败: ${err instanceof Error ? err.message : '未知错误'}`);
}
@@ -581,25 +588,24 @@ export function AgentsPage() {
// 停止 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('停止失败');
}
await agentControlApi.stop(agentId);
await loadRunningAgents();
} catch (err) {
alert(`停止失败: ${err instanceof Error ? err.message : '未知错误'}`);
}
};
// 删除 Agent
const deleteAgent = async (agentId: string) => {
if (!confirm(`确认删除 Agent "${agentId}" ?`)) return;
try {
await fetch(`http://localhost:8000/api/agents/${agentId}`, { method: 'DELETE' });
loadAgents();
} catch (err) {
alert(`删除失败: ${err instanceof Error ? err.message : '未知错误'}`);
}
};
// 加载 Agent 状态
const loadAgentState = async (agentId: string) => {
try {
@@ -883,7 +889,7 @@ export function AgentsPage() {
<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
@@ -934,6 +940,27 @@ export function AgentsPage() {
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
deleteAgent(agent.agent_id);
}}
title="删除 Agent"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '8px 10px',
background: 'rgba(255,255,255,0.03)',
border: '1px solid rgba(255,255,255,0.08)',
borderRadius: 6,
color: 'rgba(255,255,255,0.3)',
fontSize: 12,
cursor: 'pointer',
}}
>
<Trash2 size={14} />
</button>
</div>
{/* 显示运行时长 */}
{runningAgents[agent.agent_id]?.uptime && (
+221
View File
@@ -9,9 +9,31 @@ import {
Database,
FileJson,
FolderOpen,
Terminal,
Key,
Cpu,
} from 'lucide-react';
import { api } from '../lib/api';
interface CliProvider {
id: string;
type: string;
display_name: string;
description: string;
installed: boolean;
path: string;
models: string[];
}
interface ApiProvider {
id: string;
type: string;
display_name: string;
env_key: string;
configured: boolean;
models: string[];
}
interface Config {
apiBaseUrl: string;
refreshInterval: number;
@@ -32,6 +54,8 @@ export function SettingsPage() {
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
const [backendInfo, setBackendInfo] = useState<{ status: string; version: string } | null>(null);
const [cliProviders, setCliProviders] = useState<CliProvider[]>([]);
const [apiProviders, setApiProviders] = useState<ApiProvider[]>([]);
// 从 localStorage 加载配置
useEffect(() => {
@@ -43,8 +67,19 @@ export function SettingsPage() {
// 忽略解析错误
}
}
loadProviders();
}, []);
const loadProviders = async () => {
try {
const data = await api.provider.list();
setCliProviders(data.cli);
setApiProviders(data.api);
} catch {
// 后端未运行时忽略
}
};
// 保存配置
const handleSave = () => {
localStorage.setItem('swarm-config', JSON.stringify(config));
@@ -197,6 +232,192 @@ export function SettingsPage() {
</div>
</div>
{/* AI Provider 配置 */}
<div
style={{
padding: 24,
background: 'rgba(17, 24, 39, 0.7)',
borderRadius: 12,
border: '1px solid rgba(0, 240, 255, 0.1)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
<div
style={{
width: 40,
height: 40,
borderRadius: 10,
background: 'rgba(139, 92, 246, 0.1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#8b5cf6',
}}
>
<Cpu size={20} />
</div>
<div>
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#fff', margin: 0 }}>
AI Provider
</h3>
<p style={{ fontSize: 12, color: 'rgba(255, 255, 255, 0.4)', margin: '4px 0 0 0' }}>
CLI API
</p>
</div>
<button
onClick={loadProviders}
style={{
marginLeft: 'auto',
padding: '6px 12px',
background: 'rgba(255,255,255,0.05)',
border: '1px solid rgba(255,255,255,0.1)',
borderRadius: 6,
color: 'rgba(255,255,255,0.6)',
fontSize: 12,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 4,
}}
>
<RefreshCw size={12} />
</button>
</div>
{/* CLI 工具 */}
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10 }}>
<Terminal size={14} color="rgba(255,255,255,0.5)" />
<span style={{ fontSize: 13, color: 'rgba(255,255,255,0.6)', fontWeight: 500 }}>
CLI
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{cliProviders.map((cli) => (
<div
key={cli.id}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '10px 14px',
background: cli.installed ? 'rgba(0, 255, 157, 0.06)' : 'rgba(255,255,255,0.02)',
border: `1px solid ${cli.installed ? 'rgba(0, 255, 157, 0.15)' : 'rgba(255,255,255,0.06)'}`,
borderRadius: 8,
}}
>
<div
style={{
width: 8,
height: 8,
borderRadius: '50%',
background: cli.installed ? '#00ff9d' : '#666',
boxShadow: cli.installed ? '0 0 6px #00ff9d' : 'none',
flexShrink: 0,
}}
/>
<div style={{ flex: 1 }}>
<span style={{ fontSize: 14, color: '#fff', fontWeight: 500 }}>
{cli.display_name}
</span>
<span style={{ fontSize: 11, color: 'rgba(255,255,255,0.3)', marginLeft: 8 }}>
{cli.description}
</span>
</div>
<span
style={{
fontSize: 11,
padding: '3px 8px',
borderRadius: 4,
background: cli.installed ? '#00ff9d20' : '#ff006e15',
color: cli.installed ? '#00ff9d' : '#ff006e',
}}
>
{cli.installed ? '已安装' : '未安装'}
</span>
</div>
))}
{cliProviders.length === 0 && (
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', margin: 0 }}>
CLI
</p>
)}
</div>
</div>
{/* API Provider */}
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 10 }}>
<Key size={14} color="rgba(255,255,255,0.5)" />
<span style={{ fontSize: 13, color: 'rgba(255,255,255,0.6)', fontWeight: 500 }}>
API
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{apiProviders.map((prov) => (
<div
key={prov.id}
style={{
display: 'flex',
alignItems: 'center',
gap: 12,
padding: '10px 14px',
background: prov.configured ? 'rgba(0, 240, 255, 0.06)' : 'rgba(255,255,255,0.02)',
border: `1px solid ${prov.configured ? 'rgba(0, 240, 255, 0.15)' : 'rgba(255,255,255,0.06)'}`,
borderRadius: 8,
}}
>
<div
style={{
width: 8,
height: 8,
borderRadius: '50%',
background: prov.configured ? '#00f0ff' : '#666',
boxShadow: prov.configured ? '0 0 6px #00f0ff' : 'none',
flexShrink: 0,
}}
/>
<div style={{ flex: 1 }}>
<span style={{ fontSize: 14, color: '#fff', fontWeight: 500 }}>
{prov.display_name}
</span>
<span
style={{
fontSize: 11,
color: 'rgba(255,255,255,0.3)',
marginLeft: 8,
fontFamily: 'monospace',
}}
>
{prov.env_key}
</span>
</div>
<span
style={{
fontSize: 11,
padding: '3px 8px',
borderRadius: 4,
background: prov.configured ? '#00f0ff20' : '#ff950015',
color: prov.configured ? '#00f0ff' : '#ff9500',
}}
>
{prov.configured ? '已配置' : '未配置'}
</span>
</div>
))}
{apiProviders.length === 0 && (
<p style={{ fontSize: 12, color: 'rgba(255,255,255,0.3)', margin: 0 }}>
API
</p>
)}
</div>
<p style={{ fontSize: 11, color: 'rgba(255,255,255,0.3)', margin: '10px 0 0 0' }}>
API Key ANTHROPIC_API_KEY
</p>
</div>
</div>
{/* Refresh Settings */}
<div
style={{
+11 -4
View File
@@ -476,13 +476,20 @@ export function WorkflowPage() {
setUploading(true);
try {
await file.text();
// 这里应该调用后端 API 上传文件
// 暂时模拟成功
const formData = new FormData();
formData.append('file', file);
const res = await fetch('http://localhost:8000/api/workflows/upload', {
method: 'POST',
body: formData,
});
if (!res.ok) {
const err = await res.json().catch(() => ({ detail: '上传失败' }));
throw new Error(err.detail || '上传失败');
}
alert(`文件 ${file.name} 上传成功`);
loadWorkflows();
} catch (err) {
alert('上传失败');
alert(err instanceof Error ? err.message : '上传失败');
} finally {
setUploading(false);
}