feat(github_parser): 添加GitHub仓库信息查询功能

- 新增github_parser插件,支持通过命令或自动解析链接查询GitHub仓库信息
- 添加github_repo.html模板用于渲染仓库信息图片
- 优化图片管理器支持高质量截图和CSS缩放
- 重构消息事件类权限常量定义方式
- 更新帮助页面样式为三列布局并优化响应式设计
This commit is contained in:
2026-01-20 18:33:46 +08:00
parent 1ec066c9cb
commit 5f943c1792
6 changed files with 526 additions and 51 deletions

32
core/managers/1.py Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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):
"""

228
plugins/github_parser.py Normal file
View File

@@ -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)

200
templates/github_repo.html Normal file
View File

@@ -0,0 +1,200 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GitHub仓库信息</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap');
body {
font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
margin: 0;
padding: 0;
color: #ffffff;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.container {
max-width: 100%;
width: 100%;
height: 100vh;
margin: 0;
padding: 60px;
background-color: rgba(26, 26, 46, 0.95);
border-radius: 0;
box-shadow: none;
border: none;
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
justify-content: center;
}
.repo-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 80px;
}
.repo-info {
flex: 1;
}
.repo-title {
font-size: 28px;
font-weight: 700;
color: #00d4ff;
margin: 0 0 24px 0;
text-transform: uppercase;
letter-spacing: 2.5px;
}
.repo-full-name {
font-size: 72px;
font-weight: 900;
color: #ffffff;
margin: 0 0 30px 0;
line-height: 1.1;
}
.repo-description {
font-size: 32px;
color: #ffffff;
margin: 0 0 36px 0;
line-height: 1.6;
}
.owner-avatar {
width: 240px;
height: 240px;
border-radius: 50%;
margin-left: 80px;
object-fit: cover;
border: 5px solid #00d4ff;
box-shadow: 0 12px 36px rgba(0, 212, 255, 0.5);
}
.stats-container {
display: flex;
justify-content: space-between;
gap: 40px;
margin-top: 80px;
padding-top: 40px;
border-top: 3px solid rgba(15, 52, 96, 0.8);
}
.stat-item {
text-align: center;
flex: 1;
min-width: 180px;
}
.stat-number {
font-size: 96px;
font-weight: 900;
color: #00d4ff;
display: block;
line-height: 1;
}
.stat-label {
font-size: 24px;
color: #ffffff;
margin-top: 16px;
text-transform: uppercase;
letter-spacing: 1.5px;
font-weight: 600;
}
.footer {
margin-top: 100px;
padding-top: 40px;
border-top: 3px solid rgba(15, 52, 96, 0.8);
text-align: center;
}
.bot-info {
font-size: 30px;
color: rgba(0, 212, 255, 1);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 4px;
}
@media (max-width: 768px) {
body {
padding: 0;
}
.container {
padding: 20px;
}
.repo-header {
flex-direction: column;
align-items: center;
}
.owner-avatar {
margin-left: 0;
margin-bottom: 24px;
width: 160px;
height: 160px;
}
.repo-title {
font-size: 20px;
text-align: center;
}
.repo-full-name {
font-size: 48px;
text-align: center;
}
.repo-description {
text-align: center;
font-size: 24px;
}
.stats-container {
flex-direction: column;
gap: 20px;
}
.stat-item {
margin-bottom: 20px;
}
.stat-number {
font-size: 64px;
}
.stat-label {
font-size: 20px;
}
.bot-info {
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="repo-header">
<div class="repo-info">
<div class="repo-title">GitHub Repository</div>
<h1 class="repo-full-name">{{ full_name }}</h1>
<p class="repo-description">{{ description }}</p>
</div>
{% if owner_avatar %}
<img src="{{ owner_avatar }}" alt="Owner Avatar" class="owner-avatar">
{% endif %}
</div>
<div class="stats-container">
<div class="stat-item">
<span class="stat-number">{{ stargazers_count }}</span>
<div class="stat-label">Stars</div>
</div>
<div class="stat-item">
<span class="stat-number">{{ forks_count }}</span>
<div class="stat-label">Forks</div>
</div>
<div class="stat-item">
<span class="stat-number">{{ open_issues_count }}</span>
<div class="stat-label">Issues</div>
</div>
<div class="stat-item">
<span class="stat-number">{{ watchers_count }}</span>
<div class="stat-label">Watchers</div>
</div>
</div>
<div class="footer">
<div class="bot-info">by CalglauBOT</div>
</div>
</div>
</body>
</html>

View File

@@ -32,19 +32,22 @@
/* 居中布局 */
display: flex;
justify-content: center;
padding: 40px;
min-height: auto;
padding: 0;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* 窗口容器 */
.window {
width: 800px; /* 稍微收窄一点,更像手机/卡片比例 */
width: 100%;
height: 100vh;
background: var(--window-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 20px;
border: 1px solid var(--border-color);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
border-radius: 0;
border: none;
box-shadow: none;
overflow: hidden;
display: flex;
flex-direction: column;
@@ -52,7 +55,7 @@
/* 顶部标题栏 */
.header {
padding: 24px 32px;
padding: 32px 40px;
border-bottom: 1px solid var(--border-color);
background: rgba(255, 255, 255, 0.02);
display: flex;
@@ -67,48 +70,50 @@
.green { background: #10b981; }
.title {
font-size: 14px;
font-size: 18px;
font-weight: 700;
letter-spacing: 1px;
letter-spacing: 1.5px;
color: var(--text-desc);
text-transform: uppercase;
}
/* 内容区域 */
.content {
padding: 32px;
padding: 40px;
display: flex;
flex-direction: column;
gap: 24px; /* 卡片之间的间距 */
gap: 32px; /* 卡片之间的间距 */
}
.page-title {
margin-bottom: 10px;
}
.page-title h1 {
font-size: 32px;
font-size: 48px;
background: linear-gradient(to right, #fff, #94a3b8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.page-title p {
color: var(--text-desc);
font-size: 14px;
margin-top: 8px;
font-size: 20px;
margin-top: 12px;
}
/* 插件卡片 - 改为单列宽卡片 */
/* 插件卡片 - 改为三列布局 */
.plugin-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
border-radius: 14px;
padding: 24px;
display: flex;
flex-direction: column; /* 垂直排列 */
gap: 16px;
transition: transform 0.2s;
position: relative;
overflow: hidden;
flex: 1;
min-width: 320px;
}
/* 左侧装饰线 */
@@ -129,41 +134,41 @@
}
.plugin-name {
font-size: 18px;
font-size: 24px;
font-weight: 700;
color: #fff;
display: flex;
align-items: center;
gap: 10px;
gap: 12px;
}
/* 装饰性Tag */
.plugin-tag {
font-size: 10px;
font-size: 14px;
background: rgba(99, 102, 241, 0.2);
color: #818cf8;
padding: 2px 6px;
border-radius: 4px;
padding: 4px 8px;
border-radius: 6px;
font-weight: 600;
text-transform: uppercase;
}
.plugin-desc {
font-size: 13px;
font-size: 18px;
color: var(--text-desc);
line-height: 1.5;
margin-top: 4px;
line-height: 1.6;
margin-top: 8px;
}
/* 指令代码块 - 核心修改区域 */
.cmd-block {
background: var(--cmd-bg);
border-radius: 8px;
padding: 16px;
border-radius: 10px;
padding: 20px;
border: 1px solid var(--border-color);
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.6;
font-size: 16px;
line-height: 1.8;
color: var(--text-cmd);
/* 处理长文本的关键 CSS */
@@ -179,15 +184,24 @@
color: #64748b;
user-select: none;
}
/* 插件网格布局 */
.plugin-grid {
display: flex;
flex-wrap: wrap;
gap: 24px;
justify-content: flex-start;
width: 100%;
}
/* 页脚 */
.footer {
padding: 20px 32px;
padding: 24px 40px;
border-top: 1px solid var(--border-color);
color: rgba(255, 255, 255, 0.2);
font-size: 12px;
font-size: 16px;
text-align: right;
background: rgba(0,0,0,0.1);
letter-spacing: 1px;
}
</style>
</head>
@@ -210,7 +224,8 @@
<p>Dashboard & Command List · {{ plugins|length }} Modules Loaded</p>
</div>
<!-- 插件列表 - 列流式布局 -->
<!-- 插件列表 - 列流式布局 -->
<div class="plugin-grid">
{% for plugin in plugins %}
<div class="plugin-card">
<div class="card-top">
@@ -227,10 +242,11 @@
<div class="cmd-block">{{ plugin.usage }}</div>
</div>
{% endfor %}
</div>
</div>
<div class="footer">
Generated by NeoBot Render Engine
by CalglauBOT
</div>
</div>
</body>