重构 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:
+52
-120
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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()
|
||||
@@ -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 Provider(CLI + 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}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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():
|
||||
"""获取已加载的工作流列表"""
|
||||
|
||||
Reference in New Issue
Block a user