from __future__ import annotations from .shared import * from ...services.auth import has_any_role def _as_dict(value: object) -> dict[str, Any]: if isinstance(value, dict): return value if isinstance(value, str) and value.strip(): try: parsed = json.loads(value) return parsed if isinstance(parsed, dict) else {"text": value} except (TypeError, ValueError): return {"text": value} return {} def _serialize_project_row(project: Project) -> dict: return { "id": str(project.id), "title": project.title or project.name, "description": project.description, "status": project.status, "domain_id": str(project.domain_id) if project.domain_id is not None else None, "assigned_role": project.assigned_role, "signal_count": int(project.signal_count or 0), "created_at": project.created_at.isoformat() if project.created_at else None, "updated_at": project.updated_at.isoformat() if project.updated_at else None, } async def _build_project_summary( db: AsyncSession, *, tenant_id: int, project: Project, ) -> dict: base = _serialize_project_row(project) signal_count_result = await db.execute( select(func.count(ProjectSignalLink.id)).where( and_( ProjectSignalLink.tenant_id == tenant_id, ProjectSignalLink.project_id == int(project.id), ) ) ) signal_count = int(signal_count_result.scalar() or 0) base["signal_count"] = signal_count task_rows = ( await db.execute( select(Task).where( and_( Task.tenant_id == tenant_id, Task.project_id == int(project.id), ) ) ) ).scalars().all() total_tasks = len(task_rows) completed_tasks = 0 in_progress_tasks = 0 pending_tasks = 0 task_items: list[dict] = [] for task in task_rows: status_value = (task.status or "").lower() if status_value in DONE_STATUSES: completed_tasks += 1 elif status_value in {"in_progress", "review", "blocked"}: in_progress_tasks += 1 else: pending_tasks += 1 task_items.append( { "task_name": task.title, "type": "task", "status": task.status, "signal_id": str(task.truth_node_id) if task.truth_node_id is not None else "", } ) base["progress"] = round((completed_tasks / total_tasks) * 100, 2) if total_tasks else 0.0 base["total_tasks"] = total_tasks base["completed_tasks"] = completed_tasks base["in_progress_tasks"] = in_progress_tasks base["pending_tasks"] = pending_tasks base["tasks"] = task_items return base async def _fetch_project_member_user_ids( db: AsyncSession, *, tenant_id: int, project_id: int, ) -> list[int]: rows = await db.execute( select(UserProject.user_id).where( and_( UserProject.tenant_id == int(tenant_id), UserProject.project_id == int(project_id), ) ) ) return sorted(int(user_id) for user_id in rows.scalars().all() if user_id is not None) async def _get_project_or_404( db: AsyncSession, *, tenant_id: int, project_id: int, ) -> Project: project = ( await db.execute( select(Project).where( and_( Project.tenant_id == tenant_id, Project.id == project_id, ) ) ) ).scalar_one_or_none() if not project: raise HTTPException(status_code=404, detail="Project not found") return project def _is_project_manager(user: User) -> bool: return has_any_role(str(getattr(user, "role", "") or ""), ["OWNER", "REVIEWER"]) def _visible_project_filter(*, current_user: User): if _is_project_manager(current_user): return True return or_( Project.owner_id == int(current_user.id), Project.id.in_( select(UserProject.project_id).where( and_( UserProject.tenant_id == int(current_user.tenant_id), UserProject.user_id == int(current_user.id), ) ) ), ) async def _get_visible_project_or_404( db: AsyncSession, *, current_user: User, project_id: int, ) -> Project: tenant_id = int(current_user.tenant_id) query = select(Project).where( and_( Project.tenant_id == tenant_id, Project.id == int(project_id), _visible_project_filter(current_user=current_user), ) ) project = (await db.execute(query)).scalar_one_or_none() if not project: raise HTTPException(status_code=404, detail="Project not found") return project async def _cleanup_project_dependencies( db: AsyncSession, *, tenant_id: int, project_id: int, ) -> None: task_ids_subquery = ( select(Task.id) .where( and_( Task.tenant_id == tenant_id, Task.project_id == project_id, ) ) .scalar_subquery() ) # 先断开任务子依赖,避免删除 tasks 时触发约束错误。 await db.execute( update(CostTracking) .where( and_( CostTracking.tenant_id == tenant_id, CostTracking.task_id.in_(task_ids_subquery), ) ) .values(task_id=None) ) # 强关联依赖:删除项目时同步清理关联运行/执行记录。 await db.execute( delete(AIWorkerExecution).where( and_( AIWorkerExecution.tenant_id == tenant_id, AIWorkerExecution.project_id == project_id, ) ) ) await db.execute( delete(AIOrchestrationRun).where( and_( AIOrchestrationRun.tenant_id == tenant_id, AIOrchestrationRun.project_id == project_id, ) ) ) await db.execute( delete(SignalSourceRegistry).where( and_( SignalSourceRegistry.tenant_id == tenant_id, SignalSourceRegistry.project_id == project_id, ) ) ) await db.execute( delete(ProjectSignalLink).where( and_( ProjectSignalLink.tenant_id == tenant_id, ProjectSignalLink.project_id == project_id, ) ) ) await db.execute( delete(Task).where( and_( Task.tenant_id == tenant_id, Task.project_id == project_id, ) ) ) await db.execute( delete(UserProject).where( and_( UserProject.tenant_id == tenant_id, UserProject.project_id == project_id, ) ) ) # 可空外键:保留记录,将 project 解绑为 null。 await db.execute( update(WorkItem) .where( and_( WorkItem.tenant_id == tenant_id, WorkItem.project_id == project_id, ) ) .values(project_id=None) ) await db.execute( update(OperationLog) .where( and_( OperationLog.tenant_id == tenant_id, OperationLog.project_id == project_id, ) ) .values(project_id=None) ) await db.execute( update(OperationExecutionLedger) .where( and_( OperationExecutionLedger.tenant_id == tenant_id, OperationExecutionLedger.project_id == project_id, ) ) .values(project_id=None) ) await db.execute( update(Experiment) .where( and_( Experiment.tenant_id == tenant_id, Experiment.project_id == project_id, ) ) .values(project_id=None) ) await db.execute( update(Budget) .where( and_( Budget.tenant_id == tenant_id, Budget.project_id == project_id, ) ) .values(project_id=None) ) await db.execute( update(ConversationSession) .where( and_( ConversationSession.tenant_id == tenant_id, ConversationSession.current_project_id == project_id, ) ) .values(current_project_id=None) )