重构 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
+52 -120
View File
@@ -1,26 +1,16 @@
"""
Agent 管理 API 路由
接入 AgentRegistry 服务,提供 Agent 注册、查询、状态管理
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import time
from typing import Optional
from dataclasses import asdict
from ..services.agent_registry import get_agent_registry
router = APIRouter()
# 内存存储,实际应用应该使用持久化存储
agents_db = {}
class Agent(BaseModel):
agent_id: str
name: str
role: str
model: str
description: Optional[str] = None
status: str = "idle"
created_at: float = 0
class AgentCreate(BaseModel):
agent_id: str
@@ -30,138 +20,80 @@ class AgentCreate(BaseModel):
description: Optional[str] = None
# Agent状态存储
agent_states_db = {}
class AgentStateUpdate(BaseModel):
task: Optional[str] = ""
progress: Optional[int] = 0
working_files: Optional[list] = None
status: Optional[str] = "idle"
@router.get("")
@router.get("/")
async def list_agents():
"""获取所有 Agent 列表"""
# 合并数据库和默认agent
default_agents = [
{
"agent_id": "claude-001",
"name": "Claude Code",
"role": "developer",
"model": "claude-opus-4.6",
"status": "working",
"description": "主开发 Agent",
"created_at": time.time() - 86400
},
{
"agent_id": "kimi-001",
"name": "Kimi CLI",
"role": "architect",
"model": "kimi-k2",
"status": "idle",
"description": "架构设计 Agent",
"created_at": time.time() - 72000
},
{
"agent_id": "opencode-001",
"name": "OpenCode",
"role": "reviewer",
"model": "opencode-v1",
"status": "idle",
"description": "代码审查 Agent",
"created_at": time.time() - 36000
}
]
# 使用数据库中的agent(覆盖默认的)
agents_map = {a["agent_id"]: a for a in default_agents}
agents_map.update(agents_db)
return {"agents": list(agents_map.values())}
registry = get_agent_registry()
agents = await registry.list_agents()
return {
"agents": [asdict(agent) for agent in agents]
}
@router.post("/register")
async def register_agent(agent: AgentCreate):
"""注册新 Agent"""
agent_data = {
"agent_id": agent.agent_id,
"name": agent.name,
"role": agent.role,
"model": agent.model,
"description": agent.description or "",
"status": "idle",
"created_at": time.time()
}
agents_db[agent.agent_id] = agent_data
return agent_data
registry = get_agent_registry()
agent_info = await registry.register_agent(
agent_id=agent.agent_id,
name=agent.name,
role=agent.role,
model=agent.model,
description=agent.description or ""
)
return asdict(agent_info)
@router.get("/{agent_id}")
async def get_agent(agent_id: str):
"""获取指定 Agent 信息"""
if agent_id in agents_db:
return agents_db[agent_id]
raise HTTPException(status_code=404, detail="Agent not found")
registry = get_agent_registry()
agent_info = await registry.get_agent(agent_id)
if not agent_info:
raise HTTPException(status_code=404, detail="Agent not found")
return asdict(agent_info)
@router.delete("/{agent_id}")
async def delete_agent(agent_id: str):
"""删除 Agent"""
if agent_id in agents_db:
del agents_db[agent_id]
return {"message": "Agent deleted"}
raise HTTPException(status_code=404, detail="Agent not found")
registry = get_agent_registry()
success = await registry.unregister_agent(agent_id)
if not success:
raise HTTPException(status_code=404, detail="Agent not found")
return {"message": "Agent deleted"}
@router.get("/{agent_id}/state")
async def get_agent_state(agent_id: str):
"""获取 Agent 状态"""
# 如果存在真实状态,返回真实状态
if agent_id in agent_states_db:
return agent_states_db[agent_id]
# 默认mock状态
default_states = {
"claude-001": {
"agent_id": agent_id,
"task": "修复用户登录bug",
"progress": 65,
"working_files": ["src/auth/login.py", "src/auth/jwt.py"],
"status": "working",
"last_update": time.time() - 120
},
"kimi-001": {
"agent_id": agent_id,
"task": "等待会议开始",
"progress": 0,
"working_files": [],
"status": "waiting",
"last_update": time.time() - 300
},
"opencode-001": {
"agent_id": agent_id,
"task": "代码审查",
"progress": 30,
"working_files": ["src/components/Button.tsx"],
"status": "working",
"last_update": time.time() - 60
}
}
return default_states.get(agent_id, {
"agent_id": agent_id,
"task": "空闲",
"progress": 0,
"working_files": [],
"status": "idle",
"last_update": time.time()
})
registry = get_agent_registry()
state = await registry.get_state(agent_id)
if not state:
raise HTTPException(status_code=404, detail="Agent state not found")
return asdict(state)
@router.post("/{agent_id}/state")
async def update_agent_state(agent_id: str, data: dict):
async def update_agent_state(agent_id: str, data: AgentStateUpdate):
"""更新 Agent 状态"""
agent_states_db[agent_id] = {
"agent_id": agent_id,
"task": data.get("task", ""),
"progress": data.get("progress", 0),
"working_files": data.get("working_files", []),
"status": data.get("status", "idle"),
"last_update": time.time()
}
registry = get_agent_registry()
agent_info = await registry.get_agent(agent_id)
if not agent_info:
raise HTTPException(status_code=404, detail="Agent not found")
await registry.update_state(
agent_id=agent_id,
task=data.task or "",
progress=data.progress or 0,
working_files=data.working_files
)
return {"success": True}
+43 -27
View File
@@ -1,48 +1,64 @@
"""
心跳管理 API 路由
接入 HeartbeatService 服务,监控 Agent 活跃状态
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import Dict
import time
from typing import Optional
from ..services.heartbeat import get_heartbeat_service
router = APIRouter()
heartbeats_db = {}
class Heartbeat(BaseModel):
agent_id: str
timestamp: float
is_timeout: bool = False
class HeartbeatUpdate(BaseModel):
status: str = "idle"
current_task: Optional[str] = ""
progress: Optional[int] = 0
@router.get("")
@router.get("/")
async def list_heartbeats():
"""获取所有 Agent 心跳"""
return {
"heartbeats": {
"claude-001": {
"agent_id": "claude-001",
"timestamp": time.time() - 30,
"is_timeout": False
},
"kimi-001": {
"agent_id": "kimi-001",
"timestamp": time.time() - 60,
"is_timeout": False
}
service = get_heartbeat_service()
all_hb = await service.get_all_heartbeats()
heartbeats = {}
for agent_id, hb in all_hb.items():
heartbeats[agent_id] = {
"agent_id": hb.agent_id,
"last_heartbeat": hb.last_heartbeat,
"status": hb.status,
"current_task": hb.current_task,
"progress": hb.progress,
"elapsed_display": hb.elapsed_display,
"is_timeout": hb.is_timeout()
}
}
return {"heartbeats": heartbeats}
@router.post("/{agent_id}")
async def update_heartbeat(agent_id: str):
async def update_heartbeat(agent_id: str, data: HeartbeatUpdate = None):
"""更新 Agent 心跳"""
heartbeats_db[agent_id] = {
"agent_id": agent_id,
"timestamp": time.time(),
"is_timeout": False
}
service = get_heartbeat_service()
if data is None:
data = HeartbeatUpdate()
await service.update_heartbeat(
agent_id=agent_id,
status=data.status,
current_task=data.current_task or "",
progress=data.progress or 0
)
return {"success": True}
@router.get("/timeouts")
async def check_timeouts(timeout_seconds: int = 60):
"""检查超时的 Agent"""
service = get_heartbeat_service()
timeout_agents = await service.check_timeout(timeout_seconds)
return {
"timeout_seconds": timeout_seconds,
"timeout_agents": timeout_agents,
"count": len(timeout_agents)
}
+51 -58
View File
@@ -1,89 +1,82 @@
"""
文件锁 API 路由
接入 FileLockService 服务,管理文件的排他锁
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import List, Optional
import time
from typing import Optional
from dataclasses import asdict
from ..services.file_lock import get_file_lock_service
router = APIRouter()
locks_db = [
{
"file_path": "src/main.py",
"agent_id": "claude-001",
"agent_name": "Claude Code",
"locked_at": time.time() - 3600
},
{
"file_path": "src/utils.py",
"agent_id": "kimi-001",
"agent_name": "Kimi CLI",
"locked_at": time.time() - 1800
}
]
class FileLock(BaseModel):
class LockAcquireRequest(BaseModel):
file_path: str
agent_id: str
agent_name: str = ""
locked_at: float
agent_name: Optional[str] = ""
def format_elapsed(locked_at: float) -> str:
"""格式化已锁定时间"""
elapsed = time.time() - locked_at
if elapsed < 60:
return f"{int(elapsed)}"
elif elapsed < 3600:
return f"{int(elapsed / 60)}分钟"
else:
return f"{elapsed / 3600:.1f}小时"
class LockReleaseRequest(BaseModel):
file_path: str
agent_id: str
@router.get("")
@router.get("/")
async def list_locks():
"""获取所有文件锁列表"""
locks_with_display = []
for lock in locks_db:
lock_copy = lock.copy()
lock_copy["elapsed_display"] = format_elapsed(lock["locked_at"])
locks_with_display.append(lock_copy)
return {"locks": locks_with_display}
service = get_file_lock_service()
locks = await service.get_locks()
return {
"locks": [
{
"file_path": lock.file_path,
"agent_id": lock.agent_id,
"agent_name": lock.agent_name,
"acquired_at": lock.acquired_at,
"elapsed_display": lock.elapsed_display
}
for lock in locks
]
}
@router.post("/acquire")
async def acquire_lock(lock: FileLock):
async def acquire_lock(request: LockAcquireRequest):
"""获取文件锁"""
# 检查是否已被锁定
for existing in locks_db:
if existing["file_path"] == lock.file_path:
return {"success": False, "message": "File already locked"}
locks_db.append({
"file_path": lock.file_path,
"agent_id": lock.agent_id,
"agent_name": lock.agent_name or lock.agent_id,
"locked_at": time.time()
})
return {"success": True, "message": "Lock acquired"}
service = get_file_lock_service()
success = await service.acquire_lock(
file_path=request.file_path,
agent_id=request.agent_id,
agent_name=request.agent_name or ""
)
if success:
return {"success": True, "message": "Lock acquired"}
return {"success": False, "message": "File already locked by another agent"}
@router.post("/release")
async def release_lock(data: dict):
async def release_lock(request: LockReleaseRequest):
"""释放文件锁"""
file_path = data.get("file_path", "")
agent_id = data.get("agent_id", "")
global locks_db
locks_db = [l for l in locks_db if not (l["file_path"] == file_path and l["agent_id"] == agent_id)]
return {"success": True, "message": "Lock released"}
service = get_file_lock_service()
success = await service.release_lock(
file_path=request.file_path,
agent_id=request.agent_id
)
if success:
return {"success": True, "message": "Lock released"}
return {"success": False, "message": "Lock not found or not owned by this agent"}
@router.get("/check")
async def check_lock(file_path: str):
"""检查文件锁定状态"""
for lock in locks_db:
if lock["file_path"] == file_path:
return {"file_path": file_path, "locked": True, "locked_by": lock["agent_id"]}
return {"file_path": file_path, "locked": False}
service = get_file_lock_service()
locked_by = await service.check_locked(file_path)
return {
"file_path": file_path,
"locked": locked_by is not None,
"locked_by": locked_by
}
+197 -132
View File
@@ -1,195 +1,260 @@
"""
会议管理 API 路由
接入 MeetingScheduler(栅栏同步)+ MeetingRecorder(会议记录)
"""
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from dataclasses import asdict
from datetime import datetime
import time
from ..services.meeting_scheduler import get_meeting_scheduler
from ..services.meeting_recorder import get_meeting_recorder
router = APIRouter()
meetings_db = []
class Meeting(BaseModel):
meeting_id: str
title: str
status: str
attendees: List[str]
agenda: str
progress_summary: str
created_at: float
class MeetingCreate(BaseModel):
title: str
agenda: str
meeting_type: str = "design_review"
agenda: Optional[str] = ""
meeting_type: Optional[str] = "design_review"
attendees: List[str] = []
steps: Optional[List[str]] = None
class MeetingWaitRequest(BaseModel):
agent_id: str
timeout: Optional[int] = 300
class DiscussionRequest(BaseModel):
agent_id: str
agent_name: Optional[str] = ""
content: str
step: Optional[str] = ""
class ProgressRequest(BaseModel):
step: str
class FinishRequest(BaseModel):
consensus: Optional[str] = ""
def _meeting_to_dict(meeting) -> dict:
"""将 MeetingInfo 转为前端友好的 dict"""
return {
"meeting_id": meeting.meeting_id,
"title": meeting.title,
"date": meeting.date,
"status": meeting.status,
"attendees": meeting.attendees,
"steps": [
{"step_id": s.step_id, "label": s.label, "status": s.status}
for s in meeting.steps
],
"discussions": [
{
"agent_id": d.agent_id,
"agent_name": d.agent_name,
"content": d.content,
"timestamp": d.timestamp,
"step": d.step
}
for d in meeting.discussions
],
"progress_summary": meeting.progress_summary,
"consensus": meeting.consensus,
"created_at": meeting.created_at,
"ended_at": meeting.ended_at
}
@router.get("")
@router.get("/")
async def list_meetings():
"""获取所有会议列表"""
return {
"meetings": [
{
"meeting_id": "meeting-001",
"title": "架构设计评审",
"status": "in_progress",
"attendees": ["claude-001", "kimi-001"],
"agenda": "讨论系统架构设计",
"progress_summary": "50%",
"created_at": time.time() - 7200
},
{
"meeting_id": "meeting-002",
"title": "代码审查会议",
"status": "completed",
"attendees": ["claude-001"],
"agenda": "审查前端组件代码",
"progress_summary": "100%",
"created_at": time.time() - 86400
}
]
}
async def list_meetings(date: Optional[str] = None):
"""获取会议列表(默认今天)"""
recorder = get_meeting_recorder()
meetings = await recorder.list_meetings(date)
return {"meetings": [_meeting_to_dict(m) for m in meetings]}
@router.get("/today")
async def list_today_meetings():
"""获取今日会议"""
today = datetime.now().strftime("%Y-%m-%d")
return {
"meetings": [
{
"meeting_id": "meeting-001",
"title": "架构设计评审",
"date": today,
"status": "in_progress",
"attendees": ["claude-001", "kimi-001"],
"steps": [
{"step_id": "step-1", "label": "收集想法", "status": "completed"},
{"step_id": "step-2", "label": "讨论迭代", "status": "active"},
{"step_id": "step-3", "label": "生成共识", "status": "pending"}
],
"discussions": [
{
"agent_id": "claude-001",
"agent_name": "Claude Code",
"content": "建议采用微服务架构",
"timestamp": datetime.now().isoformat(),
"step": "讨论迭代"
}
],
"progress_summary": "50%",
"consensus": ""
},
{
"meeting_id": "meeting-002",
"title": "代码审查会议",
"date": today,
"status": "completed",
"attendees": ["claude-001"],
"steps": [
{"step_id": "step-1", "label": "代码检查", "status": "completed"},
{"step_id": "step-2", "label": "问题讨论", "status": "completed"}
],
"discussions": [],
"progress_summary": "100%",
"consensus": "代码质量良好,可以合并"
}
]
}
recorder = get_meeting_recorder()
meetings = await recorder.list_meetings()
return {"meetings": [_meeting_to_dict(m) for m in meetings]}
@router.post("/")
async def create_meeting(meeting: MeetingCreate):
"""创建新会议"""
meeting_id = f"meeting-{int(time.time())}"
meeting_data = {
"meeting_id": meeting_id,
"title": meeting.title,
"status": "waiting",
"attendees": meeting.attendees,
"agenda": meeting.agenda,
"progress_summary": "0%",
"created_at": time.time()
}
meetings_db.append(meeting_data)
return meeting_data
"""创建新会议(同时创建调度记录和会议记录)"""
recorder = get_meeting_recorder()
scheduler = get_meeting_scheduler()
meeting_id = f"meeting-{int(datetime.now().timestamp())}"
@router.get("/{meeting_id}")
async def get_meeting(meeting_id: str):
"""获取会议详情"""
for meeting in meetings_db:
if meeting["meeting_id"] == meeting_id:
return meeting
# 返回模拟数据
return {
"meeting_id": meeting_id,
"title": "测试会议",
"status": "in_progress",
"attendees": ["claude-001"],
"agenda": "测试议程",
"progress_summary": "50%",
"created_at": time.time()
}
# 在调度器中创建(用于栅栏同步)
await scheduler.create_meeting(
meeting_id=meeting_id,
title=meeting.title,
expected_attendees=meeting.attendees
)
# 在记录器中创建(用于记录内容)
meeting_info = await recorder.create_meeting(
meeting_id=meeting_id,
title=meeting.title,
attendees=meeting.attendees,
steps=meeting.steps
)
return _meeting_to_dict(meeting_info)
@router.post("/create")
async def create_meeting_api(meeting: MeetingCreate):
"""创建会议 API(前端使用的端点)"""
async def create_meeting_alt(meeting: MeetingCreate):
"""创建会议 API(前端使用的端点,与 POST / 相同"""
return await create_meeting(meeting)
@router.get("/{meeting_id}")
async def get_meeting(meeting_id: str, date: Optional[str] = None):
"""获取会议详情"""
recorder = get_meeting_recorder()
meeting_info = await recorder.get_meeting(meeting_id, date)
if not meeting_info:
raise HTTPException(status_code=404, detail="Meeting not found")
return _meeting_to_dict(meeting_info)
@router.get("/{meeting_id}/queue")
async def get_meeting_queue(meeting_id: str):
"""获取会议等待队列"""
scheduler = get_meeting_scheduler()
queue = await scheduler.get_queue(meeting_id)
if not queue:
raise HTTPException(status_code=404, detail="Meeting queue not found")
return {
"meeting_id": queue.meeting_id,
"title": queue.title,
"status": queue.status,
"expected_attendees": queue.expected_attendees,
"arrived_attendees": queue.arrived_attendees,
"missing_attendees": queue.missing_attendees,
"progress": queue.progress,
"is_ready": queue.is_ready
}
@router.post("/{meeting_id}/wait")
async def wait_for_meeting(meeting_id: str, request: MeetingWaitRequest):
"""栅栏同步等待(阻塞直到所有参会者到齐或超时)"""
scheduler = get_meeting_scheduler()
status = await scheduler.wait_for_meeting(
agent_id=request.agent_id,
meeting_id=meeting_id,
timeout=request.timeout or 300
)
return {"meeting_id": meeting_id, "status": status}
@router.post("/{meeting_id}/end")
async def end_meeting(meeting_id: str):
"""结束会议(调度层)"""
scheduler = get_meeting_scheduler()
success = await scheduler.end_meeting(meeting_id)
if not success:
raise HTTPException(status_code=404, detail="Meeting not found")
return {"success": True, "meeting_id": meeting_id}
@router.post("/{meeting_id}/join")
async def join_meeting(meeting_id: str, data: dict):
"""Agent 加入会议"""
agent_id = data.get("agent_id", "")
scheduler = get_meeting_scheduler()
await scheduler.add_attendee(meeting_id, agent_id)
return {"success": True, "meeting_id": meeting_id, "agent_id": agent_id}
@router.post("/{meeting_id}/discuss")
async def add_discussion(meeting_id: str, data: dict):
async def add_discussion(meeting_id: str, data: DiscussionRequest):
"""添加讨论内容"""
recorder = get_meeting_recorder()
await recorder.add_discussion(
meeting_id=meeting_id,
agent_id=data.agent_id,
agent_name=data.agent_name or data.agent_id,
content=data.content,
step=data.step or ""
)
return {"success": True, "meeting_id": meeting_id}
@router.post("/{meeting_id}/finish")
async def finish_meeting(meeting_id: str, data: dict):
"""完成会议"""
async def finish_meeting(meeting_id: str, data: FinishRequest):
"""完成会议(记录层 - 保存共识并标记完成)"""
recorder = get_meeting_recorder()
success = await recorder.end_meeting(
meeting_id=meeting_id,
consensus=data.consensus or ""
)
if not success:
raise HTTPException(status_code=404, detail="Meeting not found")
# 同时结束调度
scheduler = get_meeting_scheduler()
await scheduler.end_meeting(meeting_id)
return {"success": True, "meeting_id": meeting_id}
@router.post("/{meeting_id}/progress")
async def update_progress(meeting_id: str, data: dict):
"""更新进度"""
async def update_progress(meeting_id: str, data: ProgressRequest):
"""更新会议进度"""
recorder = get_meeting_recorder()
await recorder.update_progress(
meeting_id=meeting_id,
step_label=data.step
)
return {"success": True, "meeting_id": meeting_id}
@router.post("/record/create")
async def create_meeting_record(data: dict):
"""创建会议记录(前端使用的端点)"""
meeting_id = f"meeting-{int(time.time())}"
meeting_data = {
"meeting_id": meeting_id,
"title": data.get("title", "未命名会议"),
"agenda": data.get("agenda", ""),
"attendees": data.get("attendees", []),
"status": "waiting",
"progress_summary": "0%",
"steps": data.get("steps", []),
"discussions": [],
"created_at": time.time()
}
meetings_db.append(meeting_data)
return meeting_data
recorder = get_meeting_recorder()
meeting_id = data.get("meeting_id", f"meeting-{int(datetime.now().timestamp())}")
meeting_info = await recorder.create_meeting(
meeting_id=meeting_id,
title=data.get("title", "未命名会议"),
attendees=data.get("attendees", []),
steps=data.get("steps", [])
)
# 同时在调度器中注册
scheduler = get_meeting_scheduler()
await scheduler.create_meeting(
meeting_id=meeting_id,
title=data.get("title", "未命名会议"),
expected_attendees=data.get("attendees", [])
)
return _meeting_to_dict(meeting_info)
@router.post("/record/{meeting_id}/discussion")
async def add_meeting_discussion(meeting_id: str, data: dict):
"""添加会议讨论(前端使用的端点)"""
recorder = get_meeting_recorder()
await recorder.add_discussion(
meeting_id=meeting_id,
agent_id=data.get("agent_id", ""),
agent_name=data.get("agent_name", data.get("agent_id", "")),
content=data.get("content", ""),
step=data.get("step", "")
)
return {"success": True, "meeting_id": meeting_id, "discussion": data}
+57
View File
@@ -0,0 +1,57 @@
"""
工作流编排器 API
提供启动自动工作流、查看运行状态的端点
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import Dict, Optional
from ..services.workflow_orchestrator import get_workflow_orchestrator
router = APIRouter(prefix="/orchestrator", tags=["orchestrator"])
class StartWorkflowRequest(BaseModel):
"""启动工作流请求"""
workflow_path: str # YAML 文件名,如 dinner-decision.yaml
agent_overrides: Optional[Dict[str, str]] = None # agent_id → model 覆盖
@router.post("/start")
async def start_workflow(request: StartWorkflowRequest):
"""启动一个工作流的自动编排(后台异步执行)"""
orchestrator = get_workflow_orchestrator()
try:
run = await orchestrator.start_workflow(
workflow_path=request.workflow_path,
agent_overrides=request.agent_overrides,
)
return {
"success": True,
"message": f"工作流已启动: {run.workflow_name}",
"run_id": run.run_id,
"workflow_id": run.workflow_id,
}
except FileNotFoundError:
raise HTTPException(status_code=404, detail=f"工作流文件不存在: {request.workflow_path}")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/runs")
async def list_runs():
"""列出所有编排运行"""
orchestrator = get_workflow_orchestrator()
return {"runs": orchestrator.list_runs()}
@router.get("/runs/{run_id}")
async def get_run(run_id: str):
"""获取指定运行的详细状态"""
orchestrator = get_workflow_orchestrator()
run = orchestrator.get_run(run_id)
if not run:
raise HTTPException(status_code=404, detail=f"运行不存在: {run_id}")
return run.to_dict()
+129
View File
@@ -0,0 +1,129 @@
"""
Provider / CLI 检测与配置 API
提供系统可用的 AI CLI 工具检测和 LLM Provider 状态查询
"""
import os
from fastapi import APIRouter
from ..services.cli_invoker import detect_available_clis, CLI_REGISTRY
router = APIRouter(prefix="/providers", tags=["providers"])
# 支持的 CLI 工具元信息
CLI_META = {
"claude": {
"display_name": "Claude Code",
"description": "Anthropic Claude CLI",
"models": ["claude", "claude-sonnet", "claude-opus"],
},
"kimi": {
"display_name": "Kimi CLI",
"description": "Moonshot Kimi CLI",
"models": ["kimi", "kimi-k2", "moonshot"],
},
"opencode": {
"display_name": "OpenCode",
"description": "OpenCode CLI (支持多种模型)",
"models": ["opencode"],
},
}
# 支持的 LLM API Provider
API_PROVIDERS = {
"anthropic": {
"display_name": "Anthropic",
"env_key": "ANTHROPIC_API_KEY",
"models": ["claude-opus-4.6", "claude-sonnet-4.6", "claude-haiku-4.6"],
},
"openai": {
"display_name": "OpenAI",
"env_key": "OPENAI_API_KEY",
"models": ["gpt-4o", "gpt-4-turbo", "gpt-3.5-turbo"],
},
"deepseek": {
"display_name": "DeepSeek",
"env_key": "DEEPSEEK_API_KEY",
"models": ["deepseek-chat", "deepseek-coder"],
},
"google": {
"display_name": "Google Gemini",
"env_key": "GOOGLE_API_KEY",
"models": ["gemini-2.5-pro", "gemini-2.5-flash"],
},
}
@router.get("/")
async def list_providers():
"""
列出所有可用的 AI ProviderCLI + API
前端用于填充 Agent 注册的模型下拉框和 Settings 的 Provider 配置区
"""
available_clis = detect_available_clis()
cli_list = []
for name, meta in CLI_META.items():
installed = name in available_clis
cli_list.append({
"id": name,
"type": "cli",
"display_name": meta["display_name"],
"description": meta["description"],
"installed": installed,
"path": available_clis.get(name, ""),
"models": meta["models"],
})
api_list = []
for name, meta in API_PROVIDERS.items():
has_key = bool(os.environ.get(meta["env_key"]))
api_list.append({
"id": name,
"type": "api",
"display_name": meta["display_name"],
"env_key": meta["env_key"],
"configured": has_key,
"models": meta["models"],
})
return {
"cli": cli_list,
"api": api_list,
}
@router.get("/models")
async def list_available_models():
"""
列出当前可用的所有模型(已安装 CLI + 已配置 API Key 的模型)
前端 Agent 注册弹窗的模型下拉框直接使用此接口
"""
available_clis = detect_available_clis()
models = []
for name in available_clis:
meta = CLI_META.get(name, {})
display = meta.get("display_name", name)
for model in meta.get("models", [name]):
models.append({
"value": model,
"label": f"{model} ({display})",
"provider": name,
"type": "cli",
})
for name, meta in API_PROVIDERS.items():
if os.environ.get(meta["env_key"]):
for model in meta["models"]:
models.append({
"value": model,
"label": f"{model} ({meta['display_name']} API)",
"provider": name,
"type": "api",
})
return {"models": models}
+23 -41
View File
@@ -1,10 +1,12 @@
"""
资源管理 API 路由
接入 ResourceManager 服务,提供声明式任务执行
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import List, Optional
import time
from typing import Optional
from ..services.resource_manager import get_resource_manager
router = APIRouter()
@@ -21,55 +23,35 @@ class TaskParseRequest(BaseModel):
@router.post("/execute")
async def execute_task(request: TaskRequest):
"""执行任务"""
"""执行任务(自动管理文件锁和心跳)"""
manager = get_resource_manager()
result = await manager.execute_task(
agent_id=request.agent_id,
task_description=request.task,
timeout=request.timeout or 300
)
return {
"success": True,
"message": f"任务 '{request.task}' 已执行",
"files_locked": ["src/main.py"],
"duration_seconds": 5.5
"success": result.success,
"message": result.message,
"files_locked": result.files_locked,
"duration_seconds": round(result.duration_seconds, 2)
}
@router.get("/status")
async def get_all_status():
"""获取所有 Agent 状态"""
from ..services.agent_registry import get_agent_registry
from ..services.heartbeat import get_heartbeat_service
registry = get_agent_registry()
heartbeat_service = get_heartbeat_service()
# 获取所有已注册的 Agent
all_agents = await registry.list_agents()
agent_map = {a.agent_id: a for a in all_agents}
# 获取所有心跳
heartbeats_data = await heartbeat_service.get_all_heartbeats()
result = []
for agent_id, agent in agent_map.items():
heartbeat = heartbeats_data.get(agent_id)
result.append({
"agent_id": agent_id,
"info": {
"name": agent.name,
"role": agent.role,
"model": agent.model
},
"heartbeat": {
"status": heartbeat.status if heartbeat else "offline",
"current_task": heartbeat.current_task if heartbeat else "",
"progress": heartbeat.progress if heartbeat else 0
}
})
return {"agents": result}
"""获取所有 Agent 状态(整合注册、心跳、锁信息)"""
manager = get_resource_manager()
statuses = await manager.get_all_status()
return {"agents": statuses}
@router.post("/parse-task")
async def parse_task(request: TaskParseRequest):
"""解析任务文件"""
"""解析任务中涉及的文件路径"""
manager = get_resource_manager()
files = await manager.parse_task_files(request.task)
return {
"task": request.task,
"files": ["src/main.py", "src/utils.py"]
"files": files
}
+23 -17
View File
@@ -1,9 +1,12 @@
"""
角色分配 API 路由
接入 RoleAllocator 服务,基于任务分析分配角色
"""
from fastapi import APIRouter
from pydantic import BaseModel
from typing import List, Dict
from typing import List
from ..services.role_allocator import get_role_allocator
router = APIRouter()
@@ -20,29 +23,28 @@ class RoleAllocateRequest(BaseModel):
@router.post("/primary")
async def get_primary_role(request: RoleRequest):
"""获取任务主要角色"""
allocator = get_role_allocator()
primary = allocator.get_primary_role(request.task)
role_scores = allocator._analyze_task_roles(request.task)
return {
"task": request.task,
"primary_role": "developer",
"role_scores": {
"developer": 0.8,
"architect": 0.6,
"qa": 0.4,
"pm": 0.2
}
"primary_role": primary,
"role_scores": {k: round(v, 2) for k, v in role_scores.items()}
}
@router.post("/allocate")
async def allocate_roles(request: RoleAllocateRequest):
"""分配角色"""
allocation = {}
for i, agent in enumerate(request.agents):
roles = ["developer", "architect", "qa"]
allocation[agent] = roles[i % len(roles)]
allocator = get_role_allocator()
allocation = await allocator.allocate_roles(
task=request.task,
available_agents=request.agents
)
primary = allocator.get_primary_role(request.task)
return {
"task": request.task,
"primary_role": "developer",
"primary_role": primary,
"allocation": allocation
}
@@ -50,6 +52,10 @@ async def allocate_roles(request: RoleAllocateRequest):
@router.post("/explain")
async def explain_roles(request: RoleAllocateRequest):
"""解释角色分配"""
return {
"explanation": f"基于任务 '{request.task}' 的分析,推荐了最适合的角色分配方案。"
}
allocator = get_role_allocator()
allocation = await allocator.allocate_roles(
task=request.task,
available_agents=request.agents
)
explanation = allocator.explain_allocation(request.task, allocation)
return {"explanation": explanation}
+23 -1
View File
@@ -1,7 +1,7 @@
"""
工作流管理 API 路由
"""
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, UploadFile, File
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from pathlib import Path
@@ -80,6 +80,28 @@ async def list_workflow_files():
return {"files": files}
@router.post("/upload")
async def upload_workflow(file: UploadFile = File(...)):
"""上传工作流 YAML 文件"""
if not file.filename or not file.filename.endswith(('.yaml', '.yml')):
raise HTTPException(status_code=400, detail="仅支持 .yaml 或 .yml 文件")
engine = get_workflow_engine()
workflow_dir = Path(engine._storage.base_path) / engine.WORKFLOWS_DIR
workflow_dir.mkdir(parents=True, exist_ok=True)
dest = workflow_dir / file.filename
content = await file.read()
dest.write_bytes(content)
return {
"success": True,
"message": f"已上传 {file.filename}",
"path": f"workflow/{file.filename}",
"size": len(content)
}
@router.get("/list")
async def list_workflows():
"""获取已加载的工作流列表"""