PMFlow/backend/app/api/dev_sync.py

303 lines
11 KiB
Python

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})