2026-03-09 17:32:11 +08:00
|
|
|
|
"""
|
|
|
|
|
|
会议管理 API 路由
|
2026-03-10 16:36:25 +08:00
|
|
|
|
接入 MeetingScheduler(栅栏同步)+ MeetingRecorder(会议记录)
|
2026-03-09 17:32:11 +08:00
|
|
|
|
"""
|
2026-03-10 16:36:25 +08:00
|
|
|
|
from fastapi import APIRouter, HTTPException
|
2026-03-09 17:32:11 +08:00
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
from typing import List, Optional
|
2026-03-10 16:36:25 +08:00
|
|
|
|
from dataclasses import asdict
|
2026-03-09 17:32:11 +08:00
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
2026-03-10 16:36:25 +08:00
|
|
|
|
from ..services.meeting_scheduler import get_meeting_scheduler
|
|
|
|
|
|
from ..services.meeting_recorder import get_meeting_recorder
|
2026-03-09 17:32:11 +08:00
|
|
|
|
|
2026-03-10 16:36:25 +08:00
|
|
|
|
router = APIRouter()
|
2026-03-09 17:32:11 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MeetingCreate(BaseModel):
|
|
|
|
|
|
title: str
|
2026-03-10 16:36:25 +08:00
|
|
|
|
agenda: Optional[str] = ""
|
|
|
|
|
|
meeting_type: Optional[str] = "design_review"
|
2026-03-09 17:32:11 +08:00
|
|
|
|
attendees: List[str] = []
|
2026-03-10 16:36:25 +08:00
|
|
|
|
steps: Optional[List[str]] = None
|
2026-03-09 17:32:11 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-03-10 16:36:25 +08:00
|
|
|
|
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"""
|
2026-03-09 17:32:11 +08:00
|
|
|
|
return {
|
2026-03-10 16:36:25 +08:00
|
|
|
|
"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": [
|
2026-03-09 17:32:11 +08:00
|
|
|
|
{
|
2026-03-10 16:36:25 +08:00
|
|
|
|
"agent_id": d.agent_id,
|
|
|
|
|
|
"agent_name": d.agent_name,
|
|
|
|
|
|
"content": d.content,
|
|
|
|
|
|
"timestamp": d.timestamp,
|
|
|
|
|
|
"step": d.step
|
2026-03-09 17:32:11 +08:00
|
|
|
|
}
|
2026-03-10 16:36:25 +08:00
|
|
|
|
for d in meeting.discussions
|
|
|
|
|
|
],
|
|
|
|
|
|
"progress_summary": meeting.progress_summary,
|
|
|
|
|
|
"consensus": meeting.consensus,
|
|
|
|
|
|
"created_at": meeting.created_at,
|
|
|
|
|
|
"ended_at": meeting.ended_at
|
2026-03-09 17:32:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-10 16:36:25 +08:00
|
|
|
|
@router.get("")
|
|
|
|
|
|
@router.get("/")
|
|
|
|
|
|
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]}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-09 17:32:11 +08:00
|
|
|
|
@router.get("/today")
|
|
|
|
|
|
async def list_today_meetings():
|
|
|
|
|
|
"""获取今日会议"""
|
2026-03-10 16:36:25 +08:00
|
|
|
|
recorder = get_meeting_recorder()
|
|
|
|
|
|
meetings = await recorder.list_meetings()
|
|
|
|
|
|
return {"meetings": [_meeting_to_dict(m) for m in meetings]}
|
2026-03-09 17:32:11 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/")
|
|
|
|
|
|
async def create_meeting(meeting: MeetingCreate):
|
2026-03-10 16:36:25 +08:00
|
|
|
|
"""创建新会议(同时创建调度记录和会议记录)"""
|
|
|
|
|
|
recorder = get_meeting_recorder()
|
|
|
|
|
|
scheduler = get_meeting_scheduler()
|
|
|
|
|
|
|
|
|
|
|
|
meeting_id = f"meeting-{int(datetime.now().timestamp())}"
|
|
|
|
|
|
|
|
|
|
|
|
# 在调度器中创建(用于栅栏同步)
|
|
|
|
|
|
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_alt(meeting: MeetingCreate):
|
|
|
|
|
|
"""创建会议 API(前端使用的端点,与 POST / 相同)"""
|
|
|
|
|
|
return await create_meeting(meeting)
|
2026-03-09 17:32:11 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{meeting_id}")
|
2026-03-10 16:36:25 +08:00
|
|
|
|
async def get_meeting(meeting_id: str, date: Optional[str] = None):
|
2026-03-09 17:32:11 +08:00
|
|
|
|
"""获取会议详情"""
|
2026-03-10 16:36:25 +08:00
|
|
|
|
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")
|
2026-03-09 17:32:11 +08:00
|
|
|
|
return {
|
2026-03-10 16:36:25 +08:00
|
|
|
|
"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
|
2026-03-09 17:32:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-10 16:36:25 +08:00
|
|
|
|
@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}
|
2026-03-09 17:32:11 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{meeting_id}/join")
|
|
|
|
|
|
async def join_meeting(meeting_id: str, data: dict):
|
|
|
|
|
|
"""Agent 加入会议"""
|
|
|
|
|
|
agent_id = data.get("agent_id", "")
|
2026-03-10 16:36:25 +08:00
|
|
|
|
scheduler = get_meeting_scheduler()
|
|
|
|
|
|
await scheduler.add_attendee(meeting_id, agent_id)
|
2026-03-09 17:32:11 +08:00
|
|
|
|
return {"success": True, "meeting_id": meeting_id, "agent_id": agent_id}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{meeting_id}/discuss")
|
2026-03-10 16:36:25 +08:00
|
|
|
|
async def add_discussion(meeting_id: str, data: DiscussionRequest):
|
2026-03-09 17:32:11 +08:00
|
|
|
|
"""添加讨论内容"""
|
2026-03-10 16:36:25 +08:00
|
|
|
|
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 ""
|
|
|
|
|
|
)
|
2026-03-09 17:32:11 +08:00
|
|
|
|
return {"success": True, "meeting_id": meeting_id}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{meeting_id}/finish")
|
2026-03-10 16:36:25 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2026-03-09 17:32:11 +08:00
|
|
|
|
return {"success": True, "meeting_id": meeting_id}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/{meeting_id}/progress")
|
2026-03-10 16:36:25 +08:00
|
|
|
|
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
|
|
|
|
|
|
)
|
2026-03-09 17:32:11 +08:00
|
|
|
|
return {"success": True, "meeting_id": meeting_id}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/record/create")
|
|
|
|
|
|
async def create_meeting_record(data: dict):
|
|
|
|
|
|
"""创建会议记录(前端使用的端点)"""
|
2026-03-10 16:36:25 +08:00
|
|
|
|
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)
|
2026-03-09 17:32:11 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/record/{meeting_id}/discussion")
|
|
|
|
|
|
async def add_meeting_discussion(meeting_id: str, data: dict):
|
|
|
|
|
|
"""添加会议讨论(前端使用的端点)"""
|
2026-03-10 16:36:25 +08:00
|
|
|
|
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", "")
|
|
|
|
|
|
)
|
2026-03-09 17:32:11 +08:00
|
|
|
|
return {"success": True, "meeting_id": meeting_id, "discussion": data}
|