from __future__ import annotations from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query, Request from pydantic import BaseModel from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import AsyncSession from ..core.database import get_db from ..core.response import success_response from ..dependencies import get_current_user from ..models import Project, User, UserProject from ..services.ai_project_control_service import AIProjectControlService from ..services.auth import has_any_role router = APIRouter(tags=["dev-sync"]) class RepoBindingCreateRequest(BaseModel): project_id: int provider: str = "github" owner: str repo_name: str default_branch: str = "main" external_repo_id: str | None = None installation_id: str | None = None installation_account_login: str | None = None webhook_secret_hint: str | None = None class RepoBindingSelectRequest(BaseModel): project_id: int github_repository_id: int class ProgressEventCreateRequest(BaseModel): project_id: int event_type: str source: str = "manual" summary: str | None = None repo_binding_id: int | None = None workspace_binding_id: int | None = None payload: dict[str, object] | None = None occurred_at: str | None = None @router.get("/integrations/github/app/install-url") async def get_github_app_install_url( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): control = AIProjectControlService(db, tenant_id=int(current_user.tenant_id)) payload = await control.get_github_app_install_center() return success_response( { "install_url": payload.get("install_url"), "configured": bool(payload.get("configured")), "callback_url": payload.get("callback_url"), "webhook_signature_configured": bool(payload.get("webhook_signature_configured")), "app_slug": payload.get("app_slug"), } ) @router.get("/integrations/github/app/installations") async def list_github_app_installations( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): control = AIProjectControlService(db, tenant_id=int(current_user.tenant_id)) payload = await control.get_github_app_install_center() return success_response(payload) @router.post("/integrations/github/app/installations/sync") async def sync_github_app_installations( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): control = AIProjectControlService(db, tenant_id=int(current_user.tenant_id)) try: payload = await control.sync_github_installations() except Exception as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return success_response(payload) @router.get("/integrations/github/repos/available") async def list_available_github_repos( project_id: int | None = Query(default=None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): if project_id is not None: await _ensure_project_member_or_manager( db, tenant_id=int(current_user.tenant_id), project_id=int(project_id), current_user=current_user, ) control = AIProjectControlService(db, tenant_id=int(current_user.tenant_id)) return success_response(await control.list_available_github_repositories()) async def _ensure_project_member_or_manager( db: AsyncSession, *, tenant_id: int, project_id: int, current_user: User, ) -> None: project_result = await db.execute( select(Project).where(and_(Project.id == int(project_id), Project.tenant_id == int(tenant_id))) ) project = project_result.scalar_one_or_none() if project is None: raise HTTPException(status_code=404, detail="项目不存在") if has_any_role(str(getattr(current_user, "role", "") or ""), ["OWNER", "REVIEWER"]): return member_result = await db.execute( select(UserProject).where( and_( UserProject.tenant_id == int(tenant_id), UserProject.project_id == int(project_id), UserProject.user_id == int(current_user.id), ) ) ) if member_result.scalar_one_or_none() is None: raise HTTPException(status_code=403, detail="当前用户无权操作该项目") @router.get("/me/dev-tasks") async def list_my_dev_tasks( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): control = AIProjectControlService(db, tenant_id=int(current_user.tenant_id)) return success_response(await control.get_dev_tasks_for_user(user_id=int(current_user.id))) @router.get("/projects/{project_id}/dev-sync/summary") async def get_project_dev_sync_summary( project_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): await _ensure_project_member_or_manager( db, tenant_id=int(current_user.tenant_id), project_id=int(project_id), current_user=current_user, ) control = AIProjectControlService(db, tenant_id=int(current_user.tenant_id)) return success_response( await control.get_project_dev_sync_summary( project_id=int(project_id), current_user_id=int(current_user.id), ) ) @router.post("/tasks/{task_id}/progress-events") async def create_progress_event( task_id: int, body: ProgressEventCreateRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): await _ensure_project_member_or_manager( db, tenant_id=int(current_user.tenant_id), project_id=int(body.project_id), current_user=current_user, ) control = AIProjectControlService(db, tenant_id=int(current_user.tenant_id)) payload = await control.create_or_apply_progress_event( task_id=int(task_id), project_id=int(body.project_id), event_type=body.event_type, source=body.source, payload=dict(body.payload or {}), summary=body.summary, user_id=int(current_user.id), repo_binding_id=int(body.repo_binding_id) if body.repo_binding_id is not None else None, workspace_binding_id=int(body.workspace_binding_id) if body.workspace_binding_id is not None else None, occurred_at=body.occurred_at, ) return success_response(payload) @router.post("/internal/debug/integrations/github/repos") async def register_github_repo( body: RepoBindingCreateRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): await _ensure_project_member_or_manager( db, tenant_id=int(current_user.tenant_id), project_id=int(body.project_id), current_user=current_user, ) control = AIProjectControlService(db, tenant_id=int(current_user.tenant_id)) payload = await control.register_repo_binding( project_id=int(body.project_id), provider=body.provider, owner=body.owner, repo_name=body.repo_name, default_branch=body.default_branch, external_repo_id=body.external_repo_id, installation_id=body.installation_id, installation_account_login=body.installation_account_login, webhook_secret_hint=body.webhook_secret_hint, ) return success_response(payload) @router.post("/integrations/github/repos/bind") async def bind_github_repo( body: RepoBindingSelectRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): await _ensure_project_member_or_manager( db, tenant_id=int(current_user.tenant_id), project_id=int(body.project_id), current_user=current_user, ) control = AIProjectControlService(db, tenant_id=int(current_user.tenant_id)) try: payload = await control.bind_github_repository( project_id=int(body.project_id), github_repository_id=int(body.github_repository_id), ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return success_response(payload) @router.get("/integrations/github/deliveries") async def list_github_deliveries( project_id: int | None = Query(default=None), limit: int = Query(default=20, ge=1, le=100), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): if project_id is not None: await _ensure_project_member_or_manager( db, tenant_id=int(current_user.tenant_id), project_id=int(project_id), current_user=current_user, ) control = AIProjectControlService(db, tenant_id=int(current_user.tenant_id)) return success_response(await control.list_github_deliveries(project_id=project_id, limit=limit)) @router.post("/integrations/github/webhook") async def ingest_github_webhook( request: Request, db: AsyncSession = Depends(get_db), event_type: str | None = Header(default=None, alias="X-GitHub-Event"), signature_256: str | None = Header(default=None, alias="X-Hub-Signature-256"), delivery_id: str | None = Header(default=None, alias="X-GitHub-Delivery"), ): raw_body = await request.body() try: payload = await request.json() except Exception as exc: raise HTTPException(status_code=400, detail="GitHub webhook body 不是合法 JSON") from exc tenant_id = int((payload.get("tenant_id") or 1)) control = AIProjectControlService(db, tenant_id=tenant_id) try: result = await control.ingest_github_webhook( event_type=str(event_type or payload.get("event_type") or "").strip(), delivery_id=str(delivery_id or payload.get("delivery_id") or "").strip(), signature_256=signature_256, payload=payload, raw_body=raw_body, ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return success_response(result) @router.post("/internal/debug/integrations/github/events") async def ingest_github_event_debug( payload: dict = Body(...), db: AsyncSession = Depends(get_db), event_type: str | None = Header(default=None, alias="X-GitHub-Event"), ): tenant_id = int((payload.get("tenant_id") or 1)) control = AIProjectControlService(db, tenant_id=tenant_id) try: result = await control.ingest_github_event( event_type=str(event_type or payload.get("event_type") or "").strip(), payload=payload, ) except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc return success_response({"debug": True, **result})