fix: apply code review improvements
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"github_token": {
|
||||
"description": "GitHub Personal Access Token(可选,用于突破 API 速率限制)",
|
||||
"type": "string",
|
||||
"hint": "不填则为无认证模式(60次/小时),填写 Token 后提升到 5000次/小时",
|
||||
"hint": "不填则为无认证模式,填写 Token 后可大幅提升速率限制",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
|
||||
284
docs/superpowers/plans/2026-05-14-gitparser-plan.md
Normal file
284
docs/superpowers/plans/2026-05-14-gitparser-plan.md
Normal 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"
|
||||
```
|
||||
|
||||
107
docs/superpowers/specs/2026-05-14-gitparser-design.md
Normal file
107
docs/superpowers/specs/2026-05-14-gitparser-design.md
Normal 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/` 下
|
||||
@@ -2,3 +2,4 @@ name: Gitparser
|
||||
desc: 自动解析 GitHub 仓库和 Release 链接并展示摘要信息。
|
||||
version: 1.0.0
|
||||
author: chena
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
aiohttp>=3.9.0
|
||||
aiohttp>=3.9.0,<4.0.0
|
||||
|
||||
Reference in New Issue
Block a user