fix: apply code review improvements

This commit is contained in:
RainySY
2026-05-14 10:49:14 +08:00
parent 1db706168b
commit 6288f6f2df
5 changed files with 394 additions and 2 deletions

View File

@@ -2,7 +2,7 @@
"github_token": { "github_token": {
"description": "GitHub Personal Access Token可选用于突破 API 速率限制)", "description": "GitHub Personal Access Token可选用于突破 API 速率限制)",
"type": "string", "type": "string",
"hint": "不填则为无认证模式60次/小时),填写 Token 后提升到 5000次/小时", "hint": "不填则为无认证模式,填写 Token 后可大幅提升速率限制",
"default": "" "default": ""
} }
} }

View File

@@ -0,0 +1,284 @@
# astrbot_plugin_Gitparser Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build an AstrBot plugin that auto-detects GitHub URLs in messages and replies with repo/release summary as plain text.
**Architecture:** Single-file plugin (`main.py`) using regex for URL matching and aiohttp for GitHub REST API calls. Configuration via `_conf_schema.json` (optional token). Output formatted plain text.
**Tech Stack:** Python 3.10+, aiohttp, AstrBot Star API
---
## File Map
| File | Purpose |
|------|---------|
| `metadata.yaml` | Plugin metadata (name, desc, version, author) |
| `_conf_schema.json` | Config schema: `github_token` (optional string) |
| `requirements.txt` | `aiohttp` dependency |
| `main.py` | All plugin logic in one class |
---
### Task 1: Plugin Metadata
**Files:**
- Create: `metadata.yaml`
- [ ] **Step 1: Write metadata.yaml**
```yaml
name: Gitparser
desc: 自动解析 GitHub 仓库和 Release 链接并展示摘要信息。
version: 1.0.0
author: chena
```
- [ ] **Step 2: Commit**
```bash
git add metadata.yaml
git commit -m "feat: add plugin metadata"
```
---
### Task 2: Plugin Configuration Schema
**Files:**
- Create: `_conf_schema.json`
- [ ] **Step 1: Write _conf_schema.json**
```json
{
"github_token": {
"description": "GitHub Personal Access Token可选用于突破 API 速率限制)",
"type": "string",
"hint": "不填则为无认证模式60次/小时),填写 Token 后提升到 5000次/小时",
"default": ""
}
}
```
- [ ] **Step 2: Commit**
```bash
git add _conf_schema.json
git commit -m "feat: add config schema with optional github_token"
```
---
### Task 3: Dependencies
**Files:**
- Create: `requirements.txt`
- [ ] **Step 1: Write requirements.txt**
```
aiohttp>=3.9.0
```
- [ ] **Step 2: Commit**
```bash
git add requirements.txt
git commit -m "feat: add aiohttp dependency"
```
---
### Task 4: Main Plugin Logic
**Files:**
- Create: `main.py`
- [ ] **Step 1: Write main.py**
```python
import re
import aiohttp
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.api.star import Context, Star, star
from astrbot.api import logger, AstrBotConfig
GITHUB_API_BASE = "https://api.github.com"
# Regex: match github.com/{owner}/{repo} with optional sub-path
_REPO_PATTERN = re.compile(
r'github\.com/([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)'
r'(?:\.git)?'
r'(?:\s|$|[^\w./-])'
)
_RELEASE_TAG_PATTERN = re.compile(
r'github\.com/([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)/releases/tag/([^\s/]+)'
)
_RELEASES_PAGE_PATTERN = re.compile(
r'github\.com/([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)/releases'
r'(?:\s|$|[^\w./-])'
)
def _find_first_url(text: str, pattern: re.Pattern) -> re.Match | None:
for m in pattern.finditer(text):
return m
return None
@star
class GitparserPlugin(Star):
def __init__(self, context: Context, config: AstrBotConfig):
super().__init__(context)
self.config = config
@filter.event_message_type(filter.EventMessageType.ALL)
async def parse_github_link(self, event: AstrMessageEvent):
text = event.message_str
# 1) Release tag URL: github.com/owner/repo/releases/tag/xxx
m = _find_first_url(text, _RELEASE_TAG_PATTERN)
if m:
yield await self._handle_release_by_tag(event, m.group(1), m.group(2), m.group(3))
return
# 2) Releases page: github.com/owner/repo/releases
m = _find_first_url(text, _RELEASES_PAGE_PATTERN)
if m:
yield await self._handle_latest_release(event, m.group(1), m.group(2))
return
# 3) Repo URL: github.com/owner/repo
m = _find_first_url(text, _REPO_PATTERN)
if m:
owner, repo = m.group(1), m.group(2)
# if it happens to be a releases page (overlap with pattern 2's partial match),
# skip because releases page was already handled above.
# The _REPO_PATTERN might also match releases/... so check:
remaining = text[m.end():].strip()
if remaining.startswith('releases/'):
return
yield await self._handle_repo(event, owner, repo)
async def _fetch_api(self, path: str) -> dict | None:
headers = {"Accept": "application/vnd.github+json"}
token = self.config.get("github_token", "").strip()
if token:
headers["Authorization"] = f"Bearer {token}"
url = f"{GITHUB_API_BASE}{path}"
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp:
if resp.status == 404:
return None
if resp.status == 429:
logger.warning(f"GitHub API rate limited: {path}")
return {"error": "rate_limited"}
if resp.status != 200:
logger.warning(f"GitHub API error {resp.status}: {path}")
return None
return await resp.json()
except aiohttp.ClientError as e:
logger.error(f"HTTP error fetching {path}: {e}")
return None
except Exception as e:
logger.error(f"Unexpected error fetching {path}: {e}")
return None
async def _handle_repo(self, event: AstrMessageEvent, owner: str, repo: str):
data = await self._fetch_api(f"/repos/{owner}/{repo}")
if data is None:
return
if isinstance(data, dict) and data.get("error") == "rate_limited":
yield event.plain_result("GitHub API 限流,请稍后再试")
return
full_name = data.get("full_name", f"{owner}/{repo}")
description = data.get("description") or "(无描述)"
stars = data.get("stargazers_count", 0)
forks = data.get("forks_count", 0)
language = data.get("language") or "未知"
updated_at = data.get("updated_at", "")[:10]
license_info = data.get("license")
license_name = license_info["spdx_id"] if license_info and isinstance(license_info, dict) else ""
lines = [
f"\U0001f4e6 {full_name}",
f"{description}",
f"\u2b50 Stars: {stars} | \U0001f354 Forks: {forks} | \U0001f524 语言: {language}",
f"\U0001f4c5 最后更新: {updated_at} | \U0001f513 {license_name}",
]
yield event.plain_result("\n".join(lines))
async def _handle_latest_release(self, event: AstrMessageEvent, owner: str, repo: str):
data = await self._fetch_api(f"/repos/{owner}/{repo}/releases/latest")
if data is None:
return
if isinstance(data, dict) and data.get("error") == "rate_limited":
yield event.plain_result("GitHub API 限流,请稍后再试")
return
tag_name = data.get("tag_name", "unknown")
name = data.get("name") or tag_name
published_at = data.get("published_at", "")[:10]
zip_url = data.get("zipball_url", "")
lines = [
f"\U0001f680 {owner}/{repo} - {tag_name}",
f"\U0001f4dd {name}",
f"\U0001f4c5 发布于: {published_at}",
f"\U0001f4e6 下载: {zip_url}",
]
yield event.plain_result("\n".join(lines))
async def _handle_release_by_tag(self, event: AstrMessageEvent, owner: str, repo: str, tag: str):
data = await self._fetch_api(f"/repos/{owner}/{repo}/releases/tags/{tag}")
if data is None:
return
if isinstance(data, dict) and data.get("error") == "rate_limited":
yield event.plain_result("GitHub API 限流,请稍后再试")
return
tag_name = data.get("tag_name", tag)
name = data.get("name") or tag_name
published_at = data.get("published_at", "")[:10]
zip_url = data.get("zipball_url", "")
lines = [
f"\U0001f680 {owner}/{repo} - {tag_name}",
f"\U0001f4dd {name}",
f"\U0001f4c5 发布于: {published_at}",
f"\U0001f4e6 下载: {zip_url}",
]
yield event.plain_result("\n".join(lines))
```
- [ ] **Step 2: Commit**
```bash
git add main.py
git commit -m "feat: implement GitHub URL parsing and summary"
```
---
### Task 5: Final Verification
- [ ] **Step 1: Review file structure**
```bash
Get-ChildItem -Recurse -File | Select-Object -ExpandProperty FullName
```
- [ ] **Step 2: Commit design doc**
```bash
git add docs/
git commit -m "docs: add design spec and implementation plan"
```

View File

@@ -0,0 +1,107 @@
# astrbot_plugin_Gitparser 设计文档
## 概述
AstrBot 插件,自动检测消息中的 GitHub 链接,解析仓库和 Release 信息并以纯文本回复。
## 需求摘要
- **功能**: 仓库摘要卡片 + Release/下载信息
- **GitHub API Token**: 可选配置
- **输出格式**: 纯文本
- **触发方式**: 自动检测消息中的 GitHub URL
## 架构
```
消息 → @filter.event_message_type(ALL) → 正则匹配GH URL
→ 调用 GitHub REST API → 格式化纯文本 → 回复
```
## URL 解析规则
### 匹配的链接
| 类型 | 正则模式 | 行为 |
|------|----------|------|
| 仓库 | `github.com/{owner}/{repo}` | 调用 `/repos/{owner}/{repo}` |
| 仓库(.git) | `github.com/{owner}/{repo}.git` | 同上 |
| Release | `github.com/{owner}/{repo}/releases/tag/{tag}` | 调用 `/repos/{owner}/{repo}/releases/tags/{tag}` |
| Release(latest) | `github.com/{owner}/{repo}/releases` | 调用 `/repos/{owner}/{repo}/releases/latest` |
### 忽略的链接类型
- Issue/PR: `github.com/{owner}/{repo}/issues/{n}`, `/pull/{n}`
- Commit: `github.com/{owner}/{repo}/commit/{sha}`
- 文件: `github.com/{owner}/{repo}/blob/...`
- Gist: `gist.github.com/...`
- 其他(如 explore、marketplace 等)
## API 调用
| 类型 | API |
|------|-----|
| 仓库 | `GET /repos/{owner}/{repo}` |
| Release (latest) | `GET /repos/{owner}/{repo}/releases/latest` |
| Release (by tag) | `GET /repos/{owner}/{repo}/releases/tags/{tag}` |
Token 从配置读取,有则添加 `Authorization: Bearer` 头,无则不添加。
## 输出格式
### 仓库摘要
```
📦 {owner}/{repo}
{description}
⭐ Stars: {stars} | 🍴 Forks: {forks} | 🔤 语言: {language}
📅 最后更新: {updated_at} | 🔓 {license}
```
### Release 信息
```
🚀 {owner}/{repo} - {tag_name}
📝 {name}
📅 发布于: {published_at}
📦 下载: {zip_url}
```
## 插件配置 (_conf_schema.json)
```json
{
"github_token": {
"type": "string",
"description": "GitHub Personal Access Token可选",
"hint": "填写 Token 可将 API 速率限制从 60次/小时提升到 5000次/小时",
"default": ""
}
}
```
## 文件结构
```
astrbot_plugin_Gitparser/
├── main.py
├── metadata.yaml
├── _conf_schema.json
├── requirements.txt
└── logo.png (可选)
```
## 依赖
- `aiohttp` - 异步 HTTP 请求,调用 GitHub API
## 错误处理
- API 404 → 静默,不回复
- API 限流(429) → 回复「GitHub API 限流,请稍后再试」
- 网络超时 → 静默
- 其他异常 → 记录日志,不回复
## 开发原则
- 使用 `aiohttp` 异步请求库,不使用 `requests`
- 使用 `astrbot.api.logger` 记录日志
- 配置文件持久化在 `data/config/`

View File

@@ -2,3 +2,4 @@ name: Gitparser
desc: 自动解析 GitHub 仓库和 Release 链接并展示摘要信息。 desc: 自动解析 GitHub 仓库和 Release 链接并展示摘要信息。
version: 1.0.0 version: 1.0.0
author: chena author: chena

View File

@@ -1 +1 @@
aiohttp>=3.9.0 aiohttp>=3.9.0,<4.0.0