From 5f943c1792eed88ed09caed3ef565a0ed529536c Mon Sep 17 00:00:00 2001 From: K2cr2O1 <2221577113@qq.com> Date: Tue, 20 Jan 2026 18:33:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(github=5Fparser):=20=E6=B7=BB=E5=8A=A0GitH?= =?UTF-8?q?ub=E4=BB=93=E5=BA=93=E4=BF=A1=E6=81=AF=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增github_parser插件,支持通过命令或自动解析链接查询GitHub仓库信息 - 添加github_repo.html模板用于渲染仓库信息图片 - 优化图片管理器支持高质量截图和CSS缩放 - 重构消息事件类权限常量定义方式 - 更新帮助页面样式为三列布局并优化响应式设计 --- core/managers/1.py | 32 +++++ core/managers/image_manager.py | 14 +- models/events/message.py | 19 +-- plugins/github_parser.py | 228 +++++++++++++++++++++++++++++++++ templates/github_repo.html | 200 +++++++++++++++++++++++++++++ templates/help.html | 84 +++++++----- 6 files changed, 526 insertions(+), 51 deletions(-) create mode 100644 core/managers/1.py create mode 100644 plugins/github_parser.py create mode 100644 templates/github_repo.html diff --git a/core/managers/1.py b/core/managers/1.py new file mode 100644 index 0000000..aa631a7 --- /dev/null +++ b/core/managers/1.py @@ -0,0 +1,32 @@ + +class 真鸭子: + def 叫(self): + print("嘎嘎嘎") + + def 跑(self): + print("鸭子摇摇摆摆跑") + +class 玩具鸭子: + def 叫(self): + print("玩具鸭发出嘎嘎声") + + def 跑(self): + print("玩具鸭轮子咕噜噜跑") + +class 小猫: + def 叫(self): + print("喵喵喵") + def 跑(self): + print("猫咪跑跑") + +def 逗鸭子(鸭子一样的东西): + 鸭子一样的东西.叫() + 鸭子一样的东西.跑() + +逗鸭子(真鸭子()) + +逗鸭子(玩具鸭子()) + +逗鸭子(小猫()) + +鸭子 = 1 \ No newline at end of file diff --git a/core/managers/image_manager.py b/core/managers/image_manager.py index 6305cf3..b757ca2 100644 --- a/core/managers/image_manager.py +++ b/core/managers/image_manager.py @@ -71,15 +71,21 @@ class ImageManager: return None try: - # 设置视口 - await page.set_viewport_size({"width": 650, "height": 100}) + width = 1920 + height = 1080 + await page.set_viewport_size({"width": width, "height": height}) # 加载内容 await page.set_content(html_content) await page.wait_for_selector("body") - # 截图 - screenshot_args = {'full_page': True, 'type': image_type} + + screenshot_args = { + 'full_page': True, + 'type': image_type, + 'omit_background': False, + 'scale': 'css' + } if image_type == 'jpeg': screenshot_args['quality'] = quality diff --git a/models/events/message.py b/models/events/message.py index e84381a..b458f08 100644 --- a/models/events/message.py +++ b/models/events/message.py @@ -72,20 +72,7 @@ class MessageEvent(OneBotEvent): def post_type(self) -> str: return EventType.MESSAGE - @property - def ADMIN(self) -> Permission: - """权限级别常量,用于装饰器参数""" - return MESSAGE_EVENT_ADMIN - @property - def OP(self) -> Permission: - """权限级别常量,用于装饰器参数""" - return MESSAGE_EVENT_OP - - @property - def USER(self) -> Permission: - """权限级别常量,用于装饰器参数""" - return MESSAGE_EVENT_USER async def reply(self, message: Union[str, "MessageSegment", List["MessageSegment"]], auto_escape: bool = False): """ @@ -97,6 +84,12 @@ class MessageEvent(OneBotEvent): raise NotImplementedError("reply method must be implemented by subclasses") +# 在类定义之后添加权限常量作为类变量 +MessageEvent.ADMIN = MESSAGE_EVENT_ADMIN +MessageEvent.OP = MESSAGE_EVENT_OP +MessageEvent.USER = MESSAGE_EVENT_USER + + @dataclass(slots=True) class PrivateMessageEvent(MessageEvent): """ diff --git a/plugins/github_parser.py b/plugins/github_parser.py new file mode 100644 index 0000000..36e6378 --- /dev/null +++ b/plugins/github_parser.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +import re +import json +import aiohttp +from typing import Optional, Dict, Any, Union +from cachetools import TTLCache + +from core.utils.logger import logger +from core.managers.command_manager import matcher +from core.managers.image_manager import image_manager +from models import MessageEvent, MessageSegment + +# 插件元数据 +__plugin_meta__ = { + "name": "github_parser", + "description": "自动解析GitHub仓库链接,或通过命令查询仓库信息。", + "usage": "(自动触发)当检测到GitHub仓库链接时,自动发送仓库信息。\n(命令触发)/查仓库 作者/仓库名", +} + +# 常量定义 +GITHUB_NICKNAME = "GitHub仓库信息" + +HEADERS = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' +} + +# 全局共享的 ClientSession +_session: Optional[aiohttp.ClientSession] = None + +# 缓存GitHub API响应,避免频繁请求 +api_cache = TTLCache(maxsize=100, ttl=3600) # 100个缓存项,1小时过期 + + +def get_session() -> aiohttp.ClientSession: + """ + 获取或创建全局的aiohttp ClientSession + + Returns: + aiohttp.ClientSession: 客户端会话对象 + """ + global _session + if _session is None or _session.closed: + _session = aiohttp.ClientSession(headers=HEADERS) + return _session + + +async def get_github_repo_info(owner: str, repo: str) -> Optional[Dict[str, Any]]: + """ + 通过GitHub API获取仓库信息 + + Args: + owner (str): 仓库所有者用户名 + repo (str): 仓库名称 + + Returns: + Optional[Dict[str, Any]]: 仓库信息字典,如果失败则返回None + """ + cache_key = f"{owner}/{repo}" + if cache_key in api_cache: + logger.info(f"[github_parser] 使用缓存的仓库信息: {cache_key}") + return api_cache[cache_key] + + api_url = f"https://api.github.com/repos/{owner}/{repo}" + try: + session = get_session() + async with session.get(api_url, timeout=10) as response: + response.raise_for_status() + repo_data = await response.json() + + # 将数据存入缓存 + api_cache[cache_key] = repo_data + logger.info(f"[github_parser] 成功获取仓库信息并缓存: {cache_key}") + return repo_data + + except aiohttp.ClientError as e: + logger.error(f"[github_parser] GitHub API请求失败: {e}") + except json.JSONDecodeError as e: + logger.error(f"[github_parser] 解析GitHub API响应失败: {e}") + except Exception as e: + logger.error(f"[github_parser] 获取仓库信息时发生未知错误: {e}") + + return None + + +async def generate_repo_image(repo_data: Dict[str, Any]) -> Optional[str]: + """ + 使用Jinja2模板渲染仓库信息为图片 + + Args: + repo_data (Dict[str, Any]): 仓库信息字典 + + Returns: + Optional[str]: 生成的图片Base64编码,如果失败则返回None + """ + try: + # 准备模板数据 + template_data = { + "full_name": repo_data.get("full_name", ""), + "description": repo_data.get("description", "暂无描述"), + "owner_avatar": repo_data.get("owner", {}).get("avatar_url", ""), + "stargazers_count": repo_data.get("stargazers_count", 0), + "forks_count": repo_data.get("forks_count", 0), + "open_issues_count": repo_data.get("open_issues_count", 0), + "watchers_count": repo_data.get("watchers_count", 0), + } + + # 渲染模板为图片,使用高质量设置 + base64_image = await image_manager.render_template_to_base64( + template_name="github_repo.html", + data=template_data, + output_name=f"github_{repo_data.get('name', 'repo')}.png", + quality=100, # 使用最高质量 + image_type="png" # PNG格式为无损压缩 + ) + + return base64_image + + except Exception as e: + logger.error(f"[github_parser] 生成仓库信息图片失败: {e}") + return None + + +async def process_github_repo(event: MessageEvent, owner: str, repo: str): + """ + 处理GitHub仓库信息查询,获取信息并回复 + + Args: + event (MessageEvent): 消息事件对象 + owner (str): 仓库所有者用户名 + repo (str): 仓库名称 + """ + try: + # 获取仓库信息 + repo_data = await get_github_repo_info(owner, repo) + if not repo_data: + logger.error(f"[github_parser] 无法获取仓库信息: {owner}/{repo}") + await event.reply("无法获取仓库信息,可能是仓库不存在或网络问题。") + return + + # 生成图片 + image_base64 = await generate_repo_image(repo_data) + if image_base64: + # 发送图片 + await event.reply(MessageSegment.image(image_base64)) + else: + # 如果图片生成失败,发送文本信息 + text_message = ( + f"GitHub 仓库信息\n" + f"--------------------\n" + f"仓库: {repo_data.get('full_name', '')}\n" + f"描述: {repo_data.get('description', '暂无描述')}\n" + f"--------------------\n" + f"数据:\n" + f" 星标: {repo_data.get('stargazers_count', 0)}\n" + f" Fork: {repo_data.get('forks_count', 0)}\n" + f" Issues: {repo_data.get('open_issues_count', 0)}\n" + f" 关注: {repo_data.get('watchers_count', 0)}\n" + ) + await event.reply(text_message) + + except Exception as e: + logger.error(f"[github_parser] 处理仓库信息时发生错误: {e}") + await event.reply("处理仓库信息时发生错误,请稍后再试。") + + +# GitHub仓库链接正则表达式 +GITHUB_URL_PATTERN = re.compile(r"https?://(?:www\.)?github\.com/([\w\-]+)/([\w\-\.]+)(?:/[^\s]*)?") + + +# 注册命令处理器 +@matcher.command("查仓库", "github", "github_repo") +async def handle_github_command(bot, event: MessageEvent): + """ + 处理命令调用:/查仓库 作者/仓库名 + + Args: + bot: 机器人对象 + event (MessageEvent): 消息事件对象 + """ + # 提取命令参数 + command_text = event.raw_message + # 移除命令前缀和命令名 + prefix = command_text.split()[0] if command_text.split() else "" + params = command_text[len(prefix):].strip() + + if not params: + await event.reply("请输入仓库地址,格式:/查仓库 作者/仓库名") + return + + # 解析参数格式 + if "/" in params: + owner, repo = params.split("/", 1) + # 移除可能的.git后缀 + repo = repo.replace(".git", "") + await process_github_repo(event, owner, repo) + else: + await event.reply("参数格式错误,请输入:/查仓库 作者/仓库名") + + +# 注册消息处理器 +@matcher.on_message() +async def handle_github_link(event: MessageEvent): + """ + 处理消息,检测GitHub仓库链接并自动解析 + + Args: + event (MessageEvent): 消息事件对象 + """ + # 忽略机器人自己发送的消息,防止无限循环 + if hasattr(event, "user_id") and hasattr(event, "self_id") and event.user_id == event.self_id: + return + + # 提取消息文本 + message_text = "" + for segment in event.message: + if segment.type == "text": + message_text += segment.data.get("text", "") + + # 查找GitHub仓库链接 + match = GITHUB_URL_PATTERN.search(message_text) + if match: + owner = match.group(1) + repo = match.group(2) + # 移除可能的.git后缀 + repo = repo.replace(".git", "") + + logger.info(f"[github_parser] 检测到GitHub仓库链接: {owner}/{repo}") + await process_github_repo(event, owner, repo) diff --git a/templates/github_repo.html b/templates/github_repo.html new file mode 100644 index 0000000..328da70 --- /dev/null +++ b/templates/github_repo.html @@ -0,0 +1,200 @@ + + +
+ + +{{ description }}
+Dashboard & Command List · {{ plugins|length }} Modules Loaded
- + +