PMFlow/backend/app/api/tasks.py

124 lines
3.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""任务相关 API。"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
from ..database import get_db
from ..dependencies import get_current_user, require_manager_user
from ..models import Task, User, UserProject
from ..schemas import TaskBreakdownRequest, TaskBreakdownResponse
from ..services.llm import llm_service
from ..services.plane import plane_service
from ..services.skill_service import recommend_personnel
router = APIRouter(prefix="/tasks", tags=["tasks"])
@router.post("/breakdown", response_model=TaskBreakdownResponse)
async def breakdown_tasks(
request: TaskBreakdownRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(require_manager_user),
):
"""AI 任务拆解"""
tenant_id = int(current_user.tenant_id)
try:
tasks = await llm_service.generate_task_breakdown_async(
request.description,
db=db,
tenant_id=tenant_id,
)
except RuntimeError:
raise HTTPException(status_code=403, detail="预算超限,AI 调用被阻止(hard limit)")
if not tasks:
raise HTTPException(status_code=500, detail="Failed to generate task breakdown")
sync_result = None
if request.sync_to_plane:
sync_result = await plane_service.sync_tasks(tasks)
return {"tasks": tasks, "sync_result": sync_result}
@router.get("/recommend-assignees")
async def recommend_assignees(
task_id: int = Query(..., description="任务 ID"),
limit: int = Query(5, ge=1, le=20),
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
推荐任务负责人(按技能匹配 + 熟练度 + 负载)。
仅在任务所属项目成员中选择,且排除禁用用户。
"""
tenant_id = int(current_user.tenant_id)
task = (
await db.execute(
select(Task).where(
and_(
Task.id == task_id,
Task.tenant_id == tenant_id,
)
)
)
).scalar_one_or_none()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
required_skills = [str(x) for x in (task.required_skill_tag_ids or [])]
recommendations = await recommend_personnel(
db=db,
tenant_id=tenant_id,
required_skills=required_skills,
task_description=task.description or task.title,
task_id=str(task.id),
limit=limit * 2,
)
project_user_ids_result = await db.execute(
select(UserProject.user_id).where(
and_(
UserProject.tenant_id == tenant_id,
UserProject.project_id == int(task.project_id),
)
)
)
project_user_ids = {int(uid) for uid in project_user_ids_result.scalars().all()}
active_users_result = await db.execute(
select(User.id).where(
and_(
User.tenant_id == tenant_id,
User.status == "active",
User.is_active.is_(True),
)
)
)
active_user_ids = {int(uid) for uid in active_users_result.scalars().all()}
filtered = []
for item in recommendations.get("recommendations", []):
try:
uid = int(item.get("user_id"))
except (TypeError, ValueError):
continue
if uid in project_user_ids and uid in active_user_ids:
filtered.append(item)
filtered.sort(
key=lambda x: (
float(x.get("workload_rate", 9999)) >= 70.0,
float(x.get("workload_rate", 9999)),
-float(x.get("match_score", 0)),
)
)
final_items = filtered[:limit]
return {
"task_id": str(task.id),
"required_skills": required_skills,
"recommendations": final_items,
"total_count": len(final_items),
}