From f4b8a7a39a0ccd7b159d4842a699dd883879c607 Mon Sep 17 00:00:00 2001 From: RainySY Date: Thu, 14 May 2026 15:17:56 +0800 Subject: [PATCH] init: astrbot plugin development skill --- .opencode/plugins/astrbot-plugin-dev.js | 17 +++ package.json | 6 + skills/astrbot-plugin-development/SKILL.md | 143 +++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 .opencode/plugins/astrbot-plugin-dev.js create mode 100644 package.json create mode 100644 skills/astrbot-plugin-development/SKILL.md diff --git a/.opencode/plugins/astrbot-plugin-dev.js b/.opencode/plugins/astrbot-plugin-dev.js new file mode 100644 index 0000000..f53e027 --- /dev/null +++ b/.opencode/plugins/astrbot-plugin-dev.js @@ -0,0 +1,17 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const skillsDir = path.resolve(__dirname, '../../skills'); + +export const AstrbotPluginDevPlugin = async () => { + return { + config: async (config) => { + config.skills = config.skills || {}; + config.skills.paths = config.skills.paths || []; + if (!config.skills.paths.includes(skillsDir)) { + config.skills.paths.push(skillsDir); + } + } + }; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..e2a7253 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "name": "astrbot-plugin-dev", + "version": "1.0.0", + "description": "AstrBot plugin development skill", + "private": true +} diff --git a/skills/astrbot-plugin-development/SKILL.md b/skills/astrbot-plugin-development/SKILL.md new file mode 100644 index 0000000..7f0daf4 --- /dev/null +++ b/skills/astrbot-plugin-development/SKILL.md @@ -0,0 +1,143 @@ +--- +name: astrbot-plugin-development +description: Use when developing, debugging, or fixing AstrBot plugins and encountering import errors, async generator issues, plugin loading failures, or configuration problems +--- + +# AstrBot Plugin Development + +## Overview + +Reference for common pitfalls, patterns, and conventions when developing AstrBot plugins. For official API docs, see https://docs.astrbot.app/dev/star/plugin-new.html. + +## Project Structure + +``` +astrbot_plugin_/ +├── main.py # Required entry point +├── metadata.yaml # Required metadata. Include `repo` field for update support. +├── _conf_schema.json # Optional config schema +├── requirements.txt # pip deps (use aiohttp, never requests) +├── logo.png # Optional, 256x256 +├── skills/ # Optional bundled skills +└── pages/ # Optional dashboard pages +``` + +## Critical Pitfalls + +### 1. `star` decorator — v4 only + +**Error:** `cannot import name 'star' from 'astrbot.api.star'` + +```python +# ❌ Breaks on v3 +from astrbot.api.star import Context, Star, star +@star +class MyPlugin(Star): ... + +# ✅ Works on v3 and v4 +from astrbot.api.star import Context, Star +class MyPlugin(Star): ... +``` + +### 2. Async generator delegation + +**Error:** `object async_generator can't be used in 'await' expression` + +```python +# ❌ A handler with yield is an async generator — can't await it +yield await self._handler(event, ...) + +# ✅ Use async for delegation +async for result in self._handler(event, ...): + yield result +``` + +### 3. Missing `repo` in metadata.yaml + +**Error:** "没有指定仓库地址" — plugin cannot auto-update. + +**Fix:** Add `repo: https://github.com/user/astrbot_plugin_xxx` + +### 4. Never use `requests` + +Always use `aiohttp` or `httpx` for HTTP. Synchronous `requests` blocks the event loop. + +## Configuration + +```python +from astrbot.api import AstrBotConfig + +class MyPlugin(Star): + def __init__(self, context: Context, config: AstrBotConfig): + super().__init__(context) + self.config = config + token = config.get("key", "default") +``` + +## Message Handling Quick Reference + +```python +# Filters +@filter.event_message_type(filter.EventMessageType.ALL) # All messages +@filter.event_message_type(filter.EventMessageType.GROUP_MESSAGE) # Group only +@filter.platform_adapter_type(filter.PlatformAdapterType.AIOCQHTTP) # Specific platform +@filter.permission_type(filter.PermissionType.ADMIN) # Admin only + +# Commands +@filter.command("hello") +@filter.command("hello", alias={"hi", "hey"}) # Aliases +@filter.command("add") # /add 1 2 → parsed args +async def add(self, event: AstrMessageEvent, a: int, b: int): ... + +@filter.command_group("math") # /math add 1 2 +def math(): pass +@math.command("add") +async def add(self, event, a: int, b: int): ... + +# Sending +yield event.plain_result("text") +yield event.image_result("https://example.com/img.jpg") +event.stop_event() # Stop propagation, skip LLM + +# Active push (outside handlers) +await self.context.send_message(unified_msg_origin, message_chain) + +# Event hooks (use event.send(), NOT yield) +@filter.on_llm_request() # Before LLM: modify req +@filter.on_llm_response() # After LLM: inspect resp +@filter.on_decorating_result() # Before sending to platform +``` + +## Calling LLM + +```python +prov_id = await self.context.get_current_chat_provider_id(event.unified_msg_origin) +resp = await self.context.llm_generate(chat_provider_id=prov_id, prompt="Hello") + +# Agent with tool loop +resp = await self.context.tool_loop_agent( + event=event, chat_provider_id=prov_id, + prompt="Search for X", tools=ToolSet([MyTool()]), max_steps=30, +) + +# Register tool globally +self.context.add_llm_tools(MyTool()) +``` + +## Storage + +```python +await self.put_kv_data("key", value) +data = await self.get_kv_data("key", default) + +# File storage: always use data/plugin_data// +from pathlib import Path +from astrbot.core.utils.astrbot_path import get_astrbot_data_path +path = Path(get_astrbot_data_path()) / "plugin_data" / self.name +``` + +## Debugging + +- Plugins auto-load from `data/plugins//` +- WebUI → Plugins → `...` → "重载插件" for hot reload +- Use `from astrbot.api import logger` (not `print`)