ForcePilot/backend/package/yuxi/channel/extensions/generic_webhook/config.py
Kris 71e90075e2 feat(generic-webhook): 新增通用Webhook管道插件
实现了完整的通用Webhook通道插件,支持入站请求接收、JSONPath字段映射、多种认证方式、出站Webhook推送,以及端点配置管理、去重、安全校验等功能
2026-05-21 10:47:58 +08:00

182 lines
7.1 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.

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"],
}