"""任务相关 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.integrations.integration_core.plane_service import plane_service from ..services.skill import recommend_personnel from ..services.sstc.charter_data import ( _build_task_breakdown_prompt, _load_available_skill_names, _load_task_breakdown_context, ) 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) context = await _load_task_breakdown_context( db, tenant_id=tenant_id, description=request.description, project_id=request.project_id, charter_id=request.charter_id, ) if request.project_id is not None and not context.get("project"): raise HTTPException(status_code=404, detail="Project not found") if request.charter_id is not None and not context.get("charter"): raise HTTPException(status_code=404, detail="Charter not found") available_skills = await _load_available_skill_names(db, tenant_id=tenant_id) prompt = _build_task_breakdown_prompt( project_id=request.project_id, context=context, available_skills=available_skills, ) try: raw_text = await llm_service.generate( prompt, db=db, tenant_id=tenant_id, function_name="generate_task_breakdown", ) tasks = llm_service._parse_json_array(raw_text) if not tasks: raise ValueError("AI 拆解返回空结果或无法解析为 JSON 数组") except RuntimeError: raise HTTPException(status_code=403, detail="预算超限,AI 调用被阻止(hard limit)") except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to generate task breakdown: {exc}") 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), }