From d07a1ad5c90765e3f415d304a8acdc1e2ff082c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 14:20:33 +0000 Subject: [PATCH] Add GitHub webhook platform adapter with event handlers Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> --- astrbot/core/platform/manager.py | 4 + .../github_webhook/github_webhook_adapter.py | 266 ++++++++++++++++++ .../github_webhook/github_webhook_event.py | 22 ++ 3 files changed, 292 insertions(+) create mode 100644 astrbot/core/platform/sources/github_webhook/github_webhook_adapter.py create mode 100644 astrbot/core/platform/sources/github_webhook/github_webhook_event.py diff --git a/astrbot/core/platform/manager.py b/astrbot/core/platform/manager.py index f4313f642..39d7fd567 100644 --- a/astrbot/core/platform/manager.py +++ b/astrbot/core/platform/manager.py @@ -112,6 +112,10 @@ class PlatformManager: from .sources.satori.satori_adapter import ( SatoriPlatformAdapter, # noqa: F401 ) + case "github_webhook": + from .sources.github_webhook.github_webhook_adapter import ( + GitHubWebhookPlatformAdapter, # noqa: F401 + ) except (ImportError, ModuleNotFoundError) as e: logger.error( f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->平台日志->安装Pip库 中安装依赖库。", diff --git a/astrbot/core/platform/sources/github_webhook/github_webhook_adapter.py b/astrbot/core/platform/sources/github_webhook/github_webhook_adapter.py new file mode 100644 index 000000000..24be375aa --- /dev/null +++ b/astrbot/core/platform/sources/github_webhook/github_webhook_adapter.py @@ -0,0 +1,266 @@ +import asyncio +from typing import Any, cast + +from astrbot import logger +from astrbot.api.event import MessageChain +from astrbot.api.message_components import Plain +from astrbot.api.platform import ( + AstrBotMessage, + MessageMember, + MessageType, + Platform, + PlatformMetadata, +) +from astrbot.core.platform.astr_message_event import MessageSesion +from astrbot.core.platform.platform import PlatformStatus +from astrbot.core.utils.webhook_utils import log_webhook_info + +from ...register import register_platform_adapter +from .github_webhook_event import GitHubWebhookMessageEvent + + +@register_platform_adapter( + "github_webhook", + "GitHub Webhook 适配器", + support_streaming_message=False, +) +class GitHubWebhookPlatformAdapter(Platform): + """GitHub Webhook 平台适配器 + + 支持的事件: + - issues (created) + - issue_comment (created) + - pull_request (opened) + """ + + def __init__( + self, + platform_config: dict, + platform_settings: dict, + event_queue: asyncio.Queue, + ) -> None: + super().__init__(platform_config, event_queue) + + self.unified_webhook_mode = platform_config.get("unified_webhook_mode", True) + self.webhook_secret = platform_config.get("webhook_secret", "") + self.shutdown_event = asyncio.Event() + + async def send_by_session( + self, + session: MessageSesion, + message_chain: MessageChain, + ): + """GitHub Webhook 是单向接收,不支持主动发送消息""" + logger.warning("GitHub Webhook 适配器不支持 send_by_session") + + def meta(self) -> PlatformMetadata: + return PlatformMetadata( + name="github_webhook", + description="GitHub Webhook 适配器", + id=cast(str, self.config.get("id")), + ) + + async def run(self): + """运行适配器""" + self.status = PlatformStatus.RUNNING + + # 如果启用统一 webhook 模式 + webhook_uuid = self.config.get("webhook_uuid") + if self.unified_webhook_mode and webhook_uuid: + log_webhook_info(f"{self.meta().id}(GitHub Webhook)", webhook_uuid) + # 保持运行状态,等待 shutdown + await self.shutdown_event.wait() + else: + logger.warning("GitHub Webhook 适配器需要启用统一 webhook 模式") + await self.shutdown_event.wait() + + async def webhook_callback(self, request: Any) -> Any: + """统一 Webhook 回调入口 + + 处理 GitHub webhook 事件 + + Args: + request: Quart 请求对象 + + Returns: + 响应数据 + """ + try: + # 获取事件类型 + event_type = request.headers.get("X-GitHub-Event", "") + + # 获取请求数据 + payload = await request.json + + logger.debug(f"收到 GitHub Webhook 事件: {event_type}") + + # 处理不同类型的事件 + if event_type == "issues": + await self._handle_issue_event(payload) + elif event_type == "issue_comment": + await self._handle_issue_comment_event(payload) + elif event_type == "pull_request": + await self._handle_pull_request_event(payload) + elif event_type == "ping": + # GitHub webhook 验证事件 + return {"message": "pong"} + else: + logger.debug(f"忽略不支持的 GitHub 事件类型: {event_type}") + + return {"status": "ok"} + + except Exception as e: + logger.error(f"处理 GitHub webhook 回调时发生错误: {e}", exc_info=True) + return {"error": str(e)}, 500 + + async def _handle_issue_event(self, payload: dict): + """处理 issue 事件""" + action = payload.get("action", "") + + # 只处理创建事件 + if action != "created" and action != "opened": + return + + issue = payload.get("issue", {}) + repo = payload.get("repository", {}) + sender = payload.get("sender", {}) + + # 构造消息文本 + message_text = ( + f"📝 新 Issue 创建\n" + f"仓库: {repo.get('full_name', 'unknown')}\n" + f"标题: {issue.get('title', 'No title')}\n" + f"作者: {sender.get('login', 'unknown')}\n" + f"链接: {issue.get('html_url', '')}\n" + f"内容:\n{issue.get('body', 'No description')[:200]}" + ) + + # 创建 AstrBotMessage + abm = self._create_message( + message_text, + sender.get("login", "unknown"), + sender.get("login", "unknown"), + repo.get("full_name", "unknown"), + ) + + # 提交事件 + self.commit_event( + GitHubWebhookMessageEvent( + message_text, + abm, + self.meta(), + repo.get("full_name", "unknown"), + "issues", + payload, + ) + ) + + async def _handle_issue_comment_event(self, payload: dict): + """处理 issue 评论事件""" + action = payload.get("action", "") + + # 只处理创建事件 + if action != "created": + return + + issue = payload.get("issue", {}) + comment = payload.get("comment", {}) + repo = payload.get("repository", {}) + sender = payload.get("sender", {}) + + # 构造消息文本 + message_text = ( + f"💬 新 Issue 评论\n" + f"仓库: {repo.get('full_name', 'unknown')}\n" + f"Issue: {issue.get('title', 'No title')}\n" + f"评论者: {sender.get('login', 'unknown')}\n" + f"链接: {comment.get('html_url', '')}\n" + f"内容:\n{comment.get('body', 'No comment')[:200]}" + ) + + # 创建 AstrBotMessage + abm = self._create_message( + message_text, + sender.get("login", "unknown"), + sender.get("login", "unknown"), + repo.get("full_name", "unknown"), + ) + + # 提交事件 + self.commit_event( + GitHubWebhookMessageEvent( + message_text, + abm, + self.meta(), + repo.get("full_name", "unknown"), + "issue_comment", + payload, + ) + ) + + async def _handle_pull_request_event(self, payload: dict): + """处理 pull request 事件""" + action = payload.get("action", "") + + # 只处理打开事件 + if action != "opened": + return + + pr = payload.get("pull_request", {}) + repo = payload.get("repository", {}) + sender = payload.get("sender", {}) + + # 构造消息文本 + message_text = ( + f"🔀 新 Pull Request\n" + f"仓库: {repo.get('full_name', 'unknown')}\n" + f"标题: {pr.get('title', 'No title')}\n" + f"作者: {sender.get('login', 'unknown')}\n" + f"链接: {pr.get('html_url', '')}\n" + f"内容:\n{pr.get('body', 'No description')[:200]}" + ) + + # 创建 AstrBotMessage + abm = self._create_message( + message_text, + sender.get("login", "unknown"), + sender.get("login", "unknown"), + repo.get("full_name", "unknown"), + ) + + # 提交事件 + self.commit_event( + GitHubWebhookMessageEvent( + message_text, + abm, + self.meta(), + repo.get("full_name", "unknown"), + "pull_request", + payload, + ) + ) + + def _create_message( + self, + message_text: str, + user_id: str, + nickname: str, + session_id: str, + ) -> AstrBotMessage: + """创建 AstrBotMessage 对象""" + abm = AstrBotMessage() + abm.type = MessageType.GROUP_MESSAGE + abm.self_id = self.client_self_id + abm.session_id = session_id + abm.message_id = "" + abm.sender = MessageMember(user_id=user_id, nickname=nickname) + abm.message = [Plain(message_text)] + abm.message_str = message_text + abm.raw_message = message_text + + return abm + + async def terminate(self): + """终止适配器运行""" + self.shutdown_event.set() + logger.info("GitHub Webhook 适配器已经被优雅地关闭") diff --git a/astrbot/core/platform/sources/github_webhook/github_webhook_event.py b/astrbot/core/platform/sources/github_webhook/github_webhook_event.py new file mode 100644 index 000000000..d6a280353 --- /dev/null +++ b/astrbot/core/platform/sources/github_webhook/github_webhook_event.py @@ -0,0 +1,22 @@ +from astrbot.api.platform import AstrBotMessage, PlatformMetadata + +from ...astr_message_event import AstrMessageEvent + + +class GitHubWebhookMessageEvent(AstrMessageEvent): + """GitHub Webhook 消息事件""" + + def __init__( + self, + message_str: str, + message_obj: AstrBotMessage, + platform_meta: PlatformMetadata, + session_id: str, + event_type: str, + event_data: dict, + ): + super().__init__(message_str, message_obj, platform_meta, session_id) + self.event_type = event_type + """GitHub 事件类型: issues, issue_comment, pull_request""" + self.event_data = event_data + """原始事件数据"""