diff --git a/_conf_schema.json b/_conf_schema.json index 7ea5017..f0be716 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -2,7 +2,7 @@ "github_token": { "description": "GitHub Personal Access Token(可选,用于突破 API 速率限制)", "type": "string", - "hint": "不填则为无认证模式(60次/小时),填写 Token 后提升到 5000次/小时", + "hint": "不填则为无认证模式,填写 Token 后可大幅提升速率限制", "default": "" } } diff --git a/docs/superpowers/plans/2026-05-14-gitparser-plan.md b/docs/superpowers/plans/2026-05-14-gitparser-plan.md new file mode 100644 index 0000000..9ce1d5b --- /dev/null +++ b/docs/superpowers/plans/2026-05-14-gitparser-plan.md @@ -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" +``` + diff --git a/docs/superpowers/specs/2026-05-14-gitparser-design.md b/docs/superpowers/specs/2026-05-14-gitparser-design.md new file mode 100644 index 0000000..8222fbf --- /dev/null +++ b/docs/superpowers/specs/2026-05-14-gitparser-design.md @@ -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/` 下 diff --git a/metadata.yaml b/metadata.yaml index e765b8d..825700c 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -2,3 +2,4 @@ name: Gitparser desc: 自动解析 GitHub 仓库和 Release 链接并展示摘要信息。 version: 1.0.0 author: chena + diff --git a/requirements.txt b/requirements.txt index 3beb7cb..2c510cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -aiohttp>=3.9.0 +aiohttp>=3.9.0,<4.0.0