124 lines
3.9 KiB
Python
124 lines
3.9 KiB
Python
"""任务相关 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),
|
||
}
|