182 lines
7.1 KiB
Python
182 lines
7.1 KiB
Python
|
|
import json
|
|||
|
|
import logging
|
|||
|
|
import os
|
|||
|
|
|
|||
|
|
from yuxi.channel.extensions.generic_webhook.types import (
|
|||
|
|
AuthConfig,
|
|||
|
|
AuthType,
|
|||
|
|
EndpointConfig,
|
|||
|
|
MappingRule,
|
|||
|
|
OutboundConfig,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
logger = logging.getLogger("yuxi.channel.generic_webhook.config")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class EndpointConfigManager:
|
|||
|
|
ENV_ENDPOINTS = "GENERIC_WEBHOOK_ENDPOINTS"
|
|||
|
|
ENV_PREFIX = "GENERIC_WEBHOOK_"
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
self._endpoints: dict[str, EndpointConfig] = {}
|
|||
|
|
self._load_from_env()
|
|||
|
|
|
|||
|
|
def list_endpoint_ids(self) -> list[str]:
|
|||
|
|
return list(self._endpoints.keys())
|
|||
|
|
|
|||
|
|
def get_endpoint(self, endpoint_id: str) -> EndpointConfig | None:
|
|||
|
|
return self._endpoints.get(endpoint_id)
|
|||
|
|
|
|||
|
|
def get_all_endpoints(self) -> list[EndpointConfig]:
|
|||
|
|
return list(self._endpoints.values())
|
|||
|
|
|
|||
|
|
def add_endpoint(self, config: EndpointConfig):
|
|||
|
|
if config.endpoint_id in self._endpoints:
|
|||
|
|
logger.warning("端点 %s 已存在,覆盖", config.endpoint_id)
|
|||
|
|
self._endpoints[config.endpoint_id] = config
|
|||
|
|
logger.info("端点 %s 已注册", config.endpoint_id)
|
|||
|
|
|
|||
|
|
def remove_endpoint(self, endpoint_id: str) -> bool:
|
|||
|
|
if endpoint_id in self._endpoints:
|
|||
|
|
del self._endpoints[endpoint_id]
|
|||
|
|
return True
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def update_endpoint(self, endpoint_id: str, **kwargs) -> EndpointConfig | None:
|
|||
|
|
config = self._endpoints.get(endpoint_id)
|
|||
|
|
if config is None:
|
|||
|
|
return None
|
|||
|
|
for key, value in kwargs.items():
|
|||
|
|
if hasattr(config, key):
|
|||
|
|
setattr(config, key, value)
|
|||
|
|
return config
|
|||
|
|
|
|||
|
|
def _load_from_env(self):
|
|||
|
|
endpoints_json = os.getenv(self.ENV_ENDPOINTS, "")
|
|||
|
|
if endpoints_json:
|
|||
|
|
try:
|
|||
|
|
configs = json.loads(endpoints_json)
|
|||
|
|
for cfg in configs:
|
|||
|
|
endpoint = self._parse_endpoint(cfg)
|
|||
|
|
self._endpoints[endpoint.endpoint_id] = endpoint
|
|||
|
|
logger.info("从环境变量加载了 %d 个端点", len(configs))
|
|||
|
|
except (json.JSONDecodeError, TypeError) as e:
|
|||
|
|
logger.error("解析 GENERIC_WEBHOOK_ENDPOINTS 失败: %s", e)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
endpoint_id = os.getenv(f"{self.ENV_PREFIX}ENDPOINT_ID", "")
|
|||
|
|
if endpoint_id:
|
|||
|
|
cfg = {
|
|||
|
|
"endpoint_id": endpoint_id,
|
|||
|
|
"label": os.getenv(f"{self.ENV_PREFIX}LABEL", endpoint_id),
|
|||
|
|
"auth_type": os.getenv(f"{self.ENV_PREFIX}AUTH_TYPE", "none"),
|
|||
|
|
"api_key": os.getenv(f"{self.ENV_PREFIX}API_KEY", ""),
|
|||
|
|
"hmac_secret": os.getenv(f"{self.ENV_PREFIX}HMAC_SECRET", ""),
|
|||
|
|
"outbound_url": os.getenv(f"{self.ENV_PREFIX}OUTBOUND_URL", ""),
|
|||
|
|
"outbound_template": os.getenv(f"{self.ENV_PREFIX}OUTBOUND_TEMPLATE", ""),
|
|||
|
|
}
|
|||
|
|
endpoint = self._parse_endpoint(cfg)
|
|||
|
|
self._endpoints[endpoint.endpoint_id] = endpoint
|
|||
|
|
|
|||
|
|
def _parse_endpoint(self, cfg: dict) -> EndpointConfig:
|
|||
|
|
auth_cfg = AuthConfig(
|
|||
|
|
type=AuthType(cfg.get("auth_type", "none")),
|
|||
|
|
api_key=cfg.get("api_key", ""),
|
|||
|
|
api_key_header=cfg.get("api_key_header", "X-API-Key"),
|
|||
|
|
hmac_secret=cfg.get("hmac_secret", ""),
|
|||
|
|
hmac_header=cfg.get("hmac_header", "X-Signature-256"),
|
|||
|
|
custom_header_name=cfg.get("custom_header_name", ""),
|
|||
|
|
custom_header_value=cfg.get("custom_header_value", ""),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
mapping = MappingRule(
|
|||
|
|
msg_id=cfg.get("msg_id_jsonpath", "$.id"),
|
|||
|
|
sender_id=cfg.get("sender_id_jsonpath", "$.sender.id"),
|
|||
|
|
sender_name=cfg.get("sender_name_jsonpath", "$.sender.name"),
|
|||
|
|
content=cfg.get("content_jsonpath", "$.content"),
|
|||
|
|
message_type=cfg.get("message_type", "text"),
|
|||
|
|
defaults=cfg.get("defaults", {}),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
outbound_cfg = None
|
|||
|
|
if cfg.get("outbound_url"):
|
|||
|
|
outbound_cfg = OutboundConfig(
|
|||
|
|
url=cfg.get("outbound_url", ""),
|
|||
|
|
method=cfg.get("outbound_method", "POST"),
|
|||
|
|
content_type=cfg.get("outbound_content_type", "application/json"),
|
|||
|
|
auth_type=cfg.get("outbound_auth_type", "none"),
|
|||
|
|
auth_token=cfg.get("outbound_auth_token", ""),
|
|||
|
|
payload_template=cfg.get("outbound_template", ""),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
return EndpointConfig(
|
|||
|
|
endpoint_id=cfg["endpoint_id"],
|
|||
|
|
label=cfg.get("label", cfg["endpoint_id"]),
|
|||
|
|
mapping=mapping,
|
|||
|
|
auth=auth_cfg,
|
|||
|
|
outbound=outbound_cfg,
|
|||
|
|
dm_policy=cfg.get("dm_policy", "open"),
|
|||
|
|
allow_from=cfg.get("allow_from", []),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def config_schema(self) -> dict:
|
|||
|
|
return {
|
|||
|
|
"type": "object",
|
|||
|
|
"title": "Generic Webhook 端点配置",
|
|||
|
|
"description": "通用 Webhook 管道端点配置(JSONPath 映射规则 + 认证 + 出站)",
|
|||
|
|
"properties": {
|
|||
|
|
"endpoint_id": {
|
|||
|
|
"type": "string",
|
|||
|
|
"title": "端点 ID",
|
|||
|
|
"description": "唯一标识,URL 路径参数,如 ci-jenkins、monitor-grafana",
|
|||
|
|
"pattern": "^[a-zA-Z0-9_-]+$",
|
|||
|
|
},
|
|||
|
|
"label": {
|
|||
|
|
"type": "string",
|
|||
|
|
"title": "显示名称",
|
|||
|
|
"description": "前端展示的友好名称,如 'Jenkins CI Pipeline'",
|
|||
|
|
},
|
|||
|
|
"auth_type": {
|
|||
|
|
"type": "string",
|
|||
|
|
"title": "认证方式",
|
|||
|
|
"enum": ["none", "api_key", "hmac_sha256", "custom_header"],
|
|||
|
|
"default": "none",
|
|||
|
|
},
|
|||
|
|
"api_key": {
|
|||
|
|
"type": "string",
|
|||
|
|
"title": "API Key",
|
|||
|
|
"format": "password",
|
|||
|
|
"description": "当 auth_type=api_key 时的预共享密钥",
|
|||
|
|
},
|
|||
|
|
"hmac_secret": {
|
|||
|
|
"type": "string",
|
|||
|
|
"title": "HMAC Secret",
|
|||
|
|
"format": "password",
|
|||
|
|
"description": "当 auth_type=hmac_sha256 时的预共享密钥",
|
|||
|
|
},
|
|||
|
|
"msg_id_jsonpath": {
|
|||
|
|
"type": "string",
|
|||
|
|
"title": "消息 ID JSONPath",
|
|||
|
|
"default": "$.id",
|
|||
|
|
"description": "JSONPath 表达式,提取消息唯一 ID(用于去重)",
|
|||
|
|
},
|
|||
|
|
"sender_id_jsonpath": {
|
|||
|
|
"type": "string",
|
|||
|
|
"title": "发送者 ID JSONPath",
|
|||
|
|
"default": "$.sender.id",
|
|||
|
|
},
|
|||
|
|
"content_jsonpath": {
|
|||
|
|
"type": "string",
|
|||
|
|
"title": "内容 JSONPath",
|
|||
|
|
"default": "$.content",
|
|||
|
|
},
|
|||
|
|
"dm_policy": {
|
|||
|
|
"type": "string",
|
|||
|
|
"title": "DM 安全策略",
|
|||
|
|
"enum": ["open", "pairing", "allowlist", "disabled"],
|
|||
|
|
"default": "open",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
"required": ["endpoint_id"],
|
|||
|
|
}
|