303 lines
11 KiB
Python
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})
|