"""任务相关 API。""" from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import AsyncSession from ..core.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_service import llm_service from ..services.integration_core.plane_service import plane_service from ..services.skill 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), }