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

196 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import hashlib
import logging
import os
logger = logging.getLogger(__name__)
class GitLabConfigAdapter:
def __init__(self):
self._config: dict = {}
@property
def _gl_cfg(self) -> dict:
return self._config.get("channels", {}).get("gitlab", {})
def list_account_ids(self, config: dict) -> list[str]:
self._config = config
accounts = self._gl_cfg.get("accounts", {})
if accounts:
return list(accounts.keys())
if os.environ.get("GITLAB_PRIVATE_TOKEN"):
return ["default"]
return []
async def resolve_account(self, account_id: str) -> dict:
return self._build_account(account_id)
def is_configured(self, account: dict) -> bool:
return bool(
account.get("base_url")
and account.get("private_token")
and account.get("webhook_token")
and account.get("project_id")
)
def is_enabled(self, account: dict) -> bool:
return account.get("enabled", True)
def disabled_reason(self, account: dict) -> str:
if not self.is_configured(account):
return "GitLab 账户未完整配置: base_url, private_token, webhook_token, project_id 均为必填"
return ""
async def resolve_allow_from(self, config: dict, account_id: str) -> list[str] | None:
self._config = config
account = self._build_account(account_id)
return account.get("allow_from", [])
def describe_account(self, account: dict) -> dict:
return {
"account_id": account.get("account_id", ""),
"name": account.get("name", ""),
"base_url": account.get("base_url", ""),
"project_id": account.get("project_id", 0),
"project_path": account.get("project_path", ""),
"bot_username": account.get("bot_username", ""),
"dm_policy": account.get("dm_policy", "open"),
"configured": self.is_configured(account),
}
def default_account_id(self) -> str:
return self._gl_cfg.get("defaultAccount", "default")
@staticmethod
def credential_fingerprint(base_url: str, token: str) -> str:
return hashlib.sha256(f"{base_url}:{token}".encode()).hexdigest()[:8]
def _build_account(self, account_id: str) -> dict:
gl_cfg = self._gl_cfg
accounts = gl_cfg.get("accounts", {})
raw = accounts.get(account_id, {}) if account_id != "default" else gl_cfg
suffix = f"_{account_id.upper()}" if account_id != "default" else ""
def _env_or(name: str, default=None):
return os.environ.get(f"GITLAB_{name}{suffix}", default)
def _get(key: str, env_key: str | None = None, default=None):
val = raw.get(key, gl_cfg.get(key))
if not val and env_key:
val = _env_or(env_key)
return val if val else default
return {
"account_id": account_id,
"base_url": _get("baseUrl", "BASE_URL", "https://gitlab.com"),
"private_token": _get("privateToken", "PRIVATE_TOKEN", ""),
"webhook_token": _get("webhookToken", "WEBHOOK_TOKEN", ""),
"webhook_signing_secret": _get("webhookSigningSecret", "WEBHOOK_SIGNING_SECRET", ""),
"webhook_path": _get("webhookPath", None, "/api/webhook/gitlab"),
"bot_username": _get("botUsername", "BOT_USERNAME", ""),
"project_id": int(_get("projectId", "PROJECT_ID", 0) or 0),
"project_path": _get("projectPath", "PROJECT_PATH", ""),
"listen_issues": _get("listenIssues", None, True),
"listen_merge_requests": _get("listenMergeRequests", None, True),
"listen_notes": _get("listenNotes", None, True),
"listen_pipelines": _get("listenPipelines", None, True),
"listen_deployments": _get("listenDeployments", None, True),
"ssl_verify": _get("sslVerify", None, True),
"ca_bundle_path": _get("caBundlePath", None, ""),
"dm_policy": _get("dmPolicy", None, "open"),
"allow_from": raw.get("allowFrom", gl_cfg.get("allowFrom", [])),
"text_chunk_limit": int(_get("textChunkLimit", None, 65000) or 65000),
"streaming_mode": _get("streamingMode", None, "block"),
"name": raw.get("name", f"GitLab-{account_id}"),
"enabled": raw.get("enabled", gl_cfg.get("enabled", True)),
}
def config_schema(self) -> dict:
return {
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GitLab 渠道配置",
"properties": {
"baseUrl": {
"type": "string",
"title": "GitLab 实例地址",
"description": "GitLab 实例的 Base URLSaaS 模式为 https://gitlab.com自托管为 https://your-gitlab.example.com",
},
"privateToken": {
"type": "string",
"title": "Access Token (PAT / Project Access Token)",
"description": "GitLab Personal Access Token 或 Project Access Token需要 api scope",
"x-ui-password": True,
},
"webhookToken": {
"type": "string",
"title": "Webhook Secret Token",
"description": "用于验证 X-Gitlab-Token Header 的密钥",
"x-ui-password": True,
},
"botUsername": {
"type": "string",
"title": "Bot GitLab 用户名",
"description": "用于过滤 Bot 自身消息,防止无限循环",
},
"projectId": {
"type": "integer",
"title": "项目 ID",
"description": "GitLab 项目的全局 ID非 iid在项目首页可见",
},
"projectPath": {
"type": "string",
"title": "项目路径",
"description": "如 group/project",
},
"listenIssues": {
"type": "boolean",
"title": "监听 Issue 事件",
"default": True,
},
"listenMergeRequests": {
"type": "boolean",
"title": "监听 MR 事件",
"default": True,
},
"listenNotes": {
"type": "boolean",
"title": "监听评论事件",
"default": True,
},
"listenPipelines": {
"type": "boolean",
"title": "监听 Pipeline 事件",
"default": True,
},
"listenDeployments": {
"type": "boolean",
"title": "监听 Deployment 事件",
"default": True,
},
"sslVerify": {
"type": "boolean",
"title": "SSL 证书验证",
"description": "自托管实例可能使用自签名证书,内网可关闭",
"default": True,
},
"dmPolicy": {
"type": "string",
"title": "消息安全策略",
"enum": ["open", "pairing", "allowlist", "disabled"],
"default": "open",
},
"textChunkLimit": {
"type": "integer",
"title": "文本分块上限",
"default": 65000,
"minimum": 100,
"maximum": 950000,
},
},
"required": ["baseUrl", "privateToken", "webhookToken", "projectId"],
}