ForcePilot/backend/package/yuxi/channel/extensions/gitlab/gateway.py
Kris 4a4e256955 feat(gitlab): 新增完整 GitLab 渠道插件支持
实现了 GitLab 全功能集成,包含 Webhook 处理、API 客户端、消息收发、会话管理、安全校验、限流器等完整模块,支持 Issues、Merge Requests、流水线等事件监听与交互。
2026-05-21 10:49:01 +08:00

195 lines
7.3 KiB
Python

from __future__ import annotations
import asyncio
import logging
from fastapi import APIRouter, Request
from fastapi.responses import PlainTextResponse
from yuxi.channel.extensions.gitlab.config import GitLabConfigAdapter
from yuxi.channel.extensions.gitlab.monitor import GitLabMonitor
from yuxi.channel.extensions.gitlab.webhook import GitLabWebhookHandler
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/webhook/gitlab", tags=["gitlab"])
_current_gateway: GitLabGateway | None = None
_target_queue: asyncio.Queue | None = None
def set_target_queue(queue: asyncio.Queue) -> None:
global _target_queue
_target_queue = queue
@router.post("/events")
async def gitlab_webhook_events(request: Request):
if _current_gateway is None:
return PlainTextResponse("", status_code=503)
gateway = _current_gateway
if gateway._queue is None or gateway._webhook_handler is None:
return PlainTextResponse("", status_code=503)
try:
body = await request.body()
except Exception:
logger.exception("GitLab webhook: read body failed")
return PlainTextResponse("", status_code=400)
headers = {k.lower(): v for k, v in request.headers.items()}
result = await gateway._webhook_handler.handle_webhook(body, headers)
status = result.get("status", "error")
if status == "unauthorized":
return PlainTextResponse("", status_code=401)
if status == "invalid_payload":
return PlainTextResponse("", status_code=400)
if status in ("duplicate", "self_message"):
return PlainTextResponse("ok")
if status == "ok" and gateway._monitor and _target_queue:
try:
unified = gateway._monitor.parse_webhook_to_unified(
result.get("payload", {}),
result.get("account_id", "default"),
result.get("event_type", ""),
)
if unified:
_target_queue.put_nowait(unified)
except asyncio.QueueFull:
logger.warning("GitLab target queue full, dropping message")
return PlainTextResponse("ok")
class GitLabGateway:
def __init__(self):
self._running = False
self._tasks: list[asyncio.Task] = []
self._account: dict = {}
self._queue: asyncio.Queue | None = None
self._webhook_handler: GitLabWebhookHandler | None = None
self._monitor: GitLabMonitor | None = None
async def start(self, ctx) -> object:
global _current_gateway
config = getattr(ctx, "config", {}) if ctx else {}
account_id = getattr(ctx, "account_id", "default")
config_adapter = GitLabConfigAdapter()
config_adapter.list_account_ids(config)
self._account = await config_adapter.resolve_account(account_id)
if not config_adapter.is_configured(self._account):
logger.warning("gitlab account %s not configured, skipping start", account_id)
return {"running": False, "reason": "not-configured"}
probe_result = await self._probe_connection(self._account)
if not probe_result.get("healthy"):
logger.warning("gitlab connection probe failed for account %s: %s", account_id, probe_result.get("reason"))
return {"running": False, "reason": f"probe-failed: {probe_result.get('reason', 'unknown')}"}
self._queue = asyncio.Queue(maxsize=1000)
self._webhook_handler = GitLabWebhookHandler(
webhook_token=self._account.get("webhook_token", ""),
bot_username=self._account.get("bot_username", ""),
signing_secret=self._account.get("webhook_signing_secret", ""),
)
self._monitor = GitLabMonitor()
await self._register_webhook_if_needed(self._account)
self._running = True
_current_gateway = self
logger.info(
"gitlab gateway started for project %s (id=%s) on %s",
self._account.get("project_path"),
self._account.get("project_id"),
self._account.get("base_url"),
)
return {
"running": True,
"account_id": account_id,
"project_id": self._account.get("project_id"),
"base_url": self._account.get("base_url"),
}
async def stop(self, ctx) -> None:
global _current_gateway
self._running = False
for task in self._tasks:
task.cancel()
self._tasks.clear()
self._queue = None
self._webhook_handler = None
self._monitor = None
_current_gateway = None
logger.info("gitlab gateway stopped")
@staticmethod
async def _probe_connection(account: dict) -> dict:
try:
from yuxi.channel.extensions.gitlab.api import AsyncGitLabClient
client = AsyncGitLabClient(
base_url=account.get("base_url", "https://gitlab.com"),
private_token=account.get("private_token", ""),
ssl_verify=account.get("ssl_verify", True),
)
version = await client.probe()
if version:
return {"healthy": True, "version": version.get("version", "")}
return {"healthy": False, "reason": "version probe returned empty"}
except Exception as e:
return {"healthy": False, "reason": str(e)}
@staticmethod
async def _register_webhook_if_needed(account: dict) -> None:
project_id = account.get("project_id", 0)
webhook_path = account.get("webhook_path", "/api/webhook/gitlab")
if not project_id:
return
try:
from yuxi.channel.extensions.gitlab.api import AsyncGitLabClient
client = AsyncGitLabClient(
base_url=account.get("base_url", "https://gitlab.com"),
private_token=account.get("private_token", ""),
ssl_verify=account.get("ssl_verify", True),
)
hooks = await client.list_hooks(project_id)
existing = [h for h in hooks if getattr(h, "url", "").endswith(webhook_path)]
if not existing:
logger.info("gitlab webhook not found for project %s, creating...", project_id)
hook_data = {
"url": f"https://{account.get('webhook_domain', 'yuxi.example.com')}{webhook_path}",
"push_events": True,
"issues_events": account.get("listen_issues", True),
"merge_requests_events": account.get("listen_merge_requests", True),
"note_events": account.get("listen_notes", True),
"pipeline_events": account.get("listen_pipelines", True),
"deployment_events": account.get("listen_deployments", True),
"job_events": True,
"tag_push_events": True,
"releases_events": True,
"token": account.get("webhook_token", ""),
"enable_ssl_verification": True,
}
await client.create_hook(project_id, hook_data)
logger.info("gitlab webhook created for project %s", project_id)
else:
logger.info("gitlab webhook already exists for project %s", project_id)
except Exception:
logger.exception("gitlab webhook registration failed, manual setup may be required")