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 URL,SaaS 模式为 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"], }