插件开发
欢迎加群 322154837 讨论。
如何开发
- clone AstrBot 项目本体到本地。
- 打开 helloworld,这个是一个插件模板,接下来在这个模板上二次开发。
- 点击右上角的 Use this template,然后点击 Create new repository。
- 在 Repository name 处输入你的插件名字,不要中文。建议以
astrbot_plugin_
开头,如astrbot_plugin_yuanshen
。 - clone 创建好的仓库。(有关怎么 clone,请自行参考其他网络教程)
- 将 clone 好的插件工程复制到 AstrBot 的 addons/plugins/ 目录下。
- 使用 VSCode、PyCharm 等 IDE 打开 AstrBot 项目。
- 打开
addons/plugins/<你的插件名>/metadata.yaml
。你应该能看到如下内容:name: helloworld # 这是你的插件的唯一识别名。
desc: 这是 AstrBot 的默认插件。
help:
version: v1.3 # 插件版本号。格式:v1.1.1 或者 v1.1
author: Soulter # 作者
repo: https://github.com/Soulter/helloworld # 插件的仓库地址
开发后的插件如何使用?
- 开发完毕后,将插件推送至 GitHub。
- 上传好后使用指令
plugin i 仓库地址
进行导入。如plugin i https://github.com/Soulter/helloworld
- 开发好的插件可以通过 issue 提交至此项目,我们会将其放至相应位置供用户安装。
最小实例
from util.plugin_dev.api.v1.bot import Context, AstrMessageEvent, CommandResult
from util.plugin_dev.api.v1.config import *
class Main:
"""
AstrBot 会传递 context 给插件。
- context.register_commands: 注册指令
- context.register_task: 注册任务
- context.message_handler: 消息处理器(平台类插件用)
"""
def __init__(self, context: Context) -> None:
self.context = context
self.context.register_commands("helloworld", "helloworld", "内置测试指令。", 1, self.helloworld)
"""
指令处理函数。
- 需要接收两个参数:message: AstrMessageEvent, context: Context
- 返回 CommandResult 对象
"""
def helloworld(self, message: AstrMessageEvent, context: Context):
return CommandResult().message("Hello, World!")
Context
插件处理函数的参数之一。
它是一个上下文对象,包含了 AstrBot 实例中的一些共享数据。
在插件类的 __init__
方法中,你可以通过 context
参数获取到这个对象。
属性
这里仅筛选出对插件开发有帮助的部分属性和方法。
class Context:
def __init__(self):
self.base_config: dict = None # 配置(期望启动机器人后是不变的)
self.config_helper: CmdConfig = None # 配置获取器
self.cached_plugins: List[RegisteredPlugin] = [] # 注册的插件
self.platforms: List[RegisteredPlatform] = [] # 注册的平台
self.llms: List[RegisteredLLM] = [] # 注册的大语言模型
self.unique_session = False # 是否开启了独立会话
self.version: str = None # 机器人当前版本
self.nick = None # gocq 的唤醒词,列表
self.t2i_mode = False # 是否开启了文本转图片
self.web_search = False # 是否开启了网页搜索
self.reply_prefix = "" # 回复前缀
self.image_renderer = TextToImageRenderer() # 文本转图片渲染器
self.image_uploader = ImageUploader() # 图片上传器
self.message_handler = None # see astrbot/message/handler.py
context.cached_plugins
这是一个列表 List[RegisteredPlugin]
,包含了所有已注册的插件。
@dataclass
class RegisteredPlugin:
'''
注册在 AstrBot 中的插件。
'''
metadata: PluginMetadata
plugin_instance: object
module_path: str
module: ModuleType
root_dir_name: str
trig_cnt: int = 0
@dataclass
class PluginMetadata:
'''
插件的元数据。
'''
plugin_name: str # 插件名
plugin_type: PluginType # 插件类型
author: str # 插件作者
desc: str # 插件简介
version: str # 插件版本
class PluginType(Enum):
PLATFORM = 'platform' # 平台类插件。
LLM = 'llm' # 大语言模型类插件
COMMON = 'common' # 其他插件
context.platforms
这是一个列表 List[RegisteredPlatform]
,包含了所有已注册的平台。
@dataclass
class RegisteredPlatform:
'''
注册在 AstrBot 中的平台。平台应当实现 Platform 接口。
'''
platform_name: str # 平台名
platform_instance: Platform # 平台实例
origin: str = None # 注册来源
借此可以获取到所有已注册的平台,然后通过平台实例 platform_instance 的 send_msg 方法发送消息。详见下文:主动发送消息
。
context.llms
这是一个列表 List[RegisteredLLM]
,包含了所有已注册的大语言模型。
@dataclass
class RegisteredLLM:
'''
注册在 AstrBot 中的大语言模型调用。大语言模型应当实现 LLMProvider 接口。
'''
llm_name: str
llm_instance: LLMProvider
origin: str = None # 注册来源
向大语言模型发送消息
在 llm_instance
上调用 text_chat
方法,传入一个字符串,即可向大语言模型发送消息。注意这是一个异步方法。
前提是至少有一个大语言模型已经注册。注意
context.llms
是一个列表,可能有多个大语言模型。内置的 OpenAI API 对应的 llm_name 为internal_openai
。
# 调用内置的 OpenAI API 发送消息。
# session_id 是一个整数,用于标识会话。任意字符串。
llm_instance = None
for llm in context.llms:
if llm.llm_name == 'internal_openai':
llm_instance = llm.llm_instance
break
ret = await llm_instance.text_chat("你好", session_id)
# 如果你在使用 gpt-4o 等具有图片理解能力的大语言模型,可以:
ret = await llm_instance.text_chat("你好", session_id, image_url="https://xxxxx")
context.image_uploader
这是一个 ImageUploader
对象,用于上传图片。注意这是一个异步方法。
image_url = await context.image_uploader.upload_image("path/to/xxx.jpg")
context.image_renderer
这是一个 TextToImageRenderer
对象,用于将 markdown 格式的文本渲染成图片。注意这是一个异步方法。
# 获得图片路径
image_path = await context.image_renderer.render("Hello, World!")
# 获得图片的 url
image_url = await context.image_renderer.render("Hello, World!", return_url=True)
未来将支持插件自定义渲染风格。
注册异步任务 context.register_task()
如果你需要接入一个其他的平台,或者需要定时任务等,除了直接使用 threading.Thread 之外,你还可以使用 context.register_task 注册一个任务。
一般来说,需要用到主动发送消息的场景一般是后台长任务。这时候,你可以使用 context.register_task
注册一个任务。
在下面的例子中,将演示一个每隔 5 秒发送一次消息的任务。
import asyncio
from util.plugin_dev.api.v1.bot import Context, AstrMessageEvent, CommandResult
from util.plugin_dev.api.v1.config import *
from util.plugin_dev.api.v1.platform import *
class HelloWorldPlugin:
def __init__(self, context: Context) -> None:
self.context = context
self.context.register_task(self.send_msg_per_minute(), "send_msg_per_minute") # 注册任务
async def send_msg_per_minute(self):
while True:
await asyncio.sleep(5)
for platform in self.context.platforms: # 遍历所有平台
if platform.platform_name == 'aiocqhttp': # aiocqhttp
inst = platform.platform_instance # 获取平台实例
await inst.send_msg({"user_id": 905617992}, CommandResult().message("你好")) # 发送消息给qq号为905617992的用户
上面的例子使用了异步的方式来实现定时任务。当然,你也可以使用 threading.Thread 来实现。
注册指令 context.register_commands()
用户可以通过输入指令来调用插件的功能。
使用 context.register_commands 注册指令。接收如下参数:
- plugin_name: str 插件名。与 metadata.yaml 中的 name 一致。
- command_name: str 指令名。
- description: str 指令简短描述。
- priority: int 优先级。数字越大,优先级越高。
- handler: callable 指令处理函数。
- use_regex: bool 是否使用正则表达式识别指令。默认为 False。
- ignore_prefix: bool 是否忽略指令前缀。默认为 False。
当用户输入以 command_name
开头的指令时,会调用 handler
函数。
from util.plugin_dev.api.v1.bot import CommandResult
def __init__(self, context: Context) -> None:
self.context = context
# 插件名、指令名、描述、优先级、处理函数
self.context.register_commands("helloworld", "helloworld", "内置测试指令。", 1, self.helloworld)
def helloworld(self, message: AstrMessageEvent, context: Context):
return CommandResult().message("Hello, World!")
指令的 handler 的返回值应为一个 CommandResult 对象。有关具体使用,请看下节。
在 v3.3.7 之后的版本中,支持通过正则表达式来识别指令。
# 以 world 结尾的消息都会被识别为指令
self.context.register_commands("helloworld", "world$", "内置测试指令。", 1, self.helloworld, use_regex=True)
在 v3.3.8 版本之后,支持忽略指令前缀。
# 忽略指令前缀
self.context.register_commands("helloworld", "haha", "内置测试指令。", 1, self.helloworld, ignore_prefix=True)
指令前缀指的不是第二个参数,而是类似于
/
这样的用户设置的指令前缀。(wake指令)
忽略指令前缀后,用户发送的消息中如果不包含指令前缀,也会被触发指令。
注册 LLM Tools
LLM Tools 让模型能够调用外部工具,来增强自身能力。借此,你可以实现由用户自然语言输入触发你的插件功能。
在 v3.3.11 版本及之后,支持 注册 LLM Tools。
下面是一个查询天气的例子:
from util.plugin_dev.api.v1.bot import Context, AstrMessageEvent, CommandResult
from util.plugin_dev.api.v1.config import *
class ExamplePlugin:
def __init__(self, context: Context) -> None:
self.context = context
self.context.register_llm_tool("get_weather", [{
"type": "string", # 参数的类型。支持的类型请前往 OpenAI Function-Calling API 查看
"name": "location", # 参数名
"description": "城市名或县名" # 参数描述
}], "获取一个地区的天气,用户应该提供一个城市名或县名。", # 工具描述
self.query_weather) # 工具函数
async def query_weather(self, location: str) -> CommandResult: # 工具函数,参数名应与注册时的参数名一致
# 查询天气
weather = await query_weather(location) # 你的查询天气函数
return CommandResult().message(f"{location} 的天气是 {weather}!")
激活插件后,一旦用户输入了 获取北京的天气
这样的消息,大语言模型就会调用这个插件提供的 query_weather
函数,并返回结果。
工具函数应当是一个异步函数
,返回值应为一个 CommandResult 对象。工具函数的参数支持传入 ame: AstrMessageEvent, context: Context
,以方便获取上下文信息。
async def query_weather(self, ame: AstrMessageEvent, context: Context, location: str) -> CommandResult: # 工具函数,参数名应与注册时的参数名一致
message_str = ame.message_str # 获取用户发送的消息字符串
# 查询天气
weather = await query_weather(location) # 你的查询天气函数
return CommandResult().message(f"{location} 的天气是 {weather}!")
自定义文转图渲染模板
在 v3.3.11 版本及以后,支持自定义文转图渲染模板。
AstrBot 使用 HTML + Jinja2 的方式来渲染文转图模板。
比如你可以自定义一个 HTML 模板:
<h1 style="color: red">{{title}}</h1>
<p>{{content}}</p>
然后传递一个字典:
{
"title": "警报!",
"content": "10 分钟后课程大作业截止!"
}
这样,AstrBot 就会按照这个 HTML 模板生成一张图片,图片内容为 警报!
和 10 分钟后课程大作业截止!
。
TMPL = '''
<div style="font-size: 32px;">
<h1 style="color: red">{{title}}</h1>
<p>{{content}}</p>
</div>
'''
class HelloWorldPlugin:
"""
AstrBot 会传递 context 给插件。
- context.register_commands: 注册指令
- context.register_task: 注册任务
- context.message_handler: 消息处理器(平台类插件用)
"""
def __init__(self, context: Context) -> None:
self.context = context
self.context.register_commands("helloworld", "helloworld", "内置测试指令。", 1, self.helloworld)
"""
指令处理函数。
- 需要接收两个参数:message: AstrMessageEvent, context: Context
- 返回 CommandResult 对象
"""
async def helloworld(self, message: AstrMessageEvent, context: Context):
# return CommandResult().message("Hello, World!")
url = await self.context.image_renderer.render_custom_template(TMPL, {
"title": "警报!",
"content": "10 分钟后课程大作业截止!"
}, return_url=True)
return CommandResult().url_image(url)
返回的结果:
这只是一个简单的例子。得益于 HTML 和 DOM 渲染器的强大性,你可以进行更复杂和更美观的的设计。除此之外,Jinja2 支持循环、条件等语法以适应列表、字典等数据结构。你可以从网上了解更多关于 Jinja2 的知识。
指令返回值 CommandResult
CommandResult 是插件指令处理函数的返回值。
快捷调用:
CommandResult().message("Hello, World!") # 返回一个纯文本消息
CommandResult().error("Hello, World!") # 返回一个错误消息
CommandResult().url_image("https://xxxxx") # 通过 url 返回一个图片消息
CommandResult().file_image("path/to/xxx.jpg") # 通过文件路径返回一个图片消息
当然,你也可以自己构造 CommandResult 对象。
from util.plugin_dev.api.v1.types import Image, Plain
cmd_result = CommandResult(
message_chain=[Plain("Hello, World!"), Image.fromURL("https://xxxxx")], # 消息链
)
关闭或开启文转图
默认情况下,CommandResult() 是否文转图取决于用户 t2i 指令的设置。你可以通过 use_t2i 方法来强行在这个回复中关闭或开启文转图。
CommandResult().message("Hello, World!").use_t2i(False) # 关闭文转图
AstrBot 触发文转图只会在回复操作(也就是指令处理函数的return)中生效,如果你在插件中主动发送消息,文转图永远不会生效。
AstrMessageEvent
插件处理函数的参数之一。
当用户发送消息时,AstrBot 会封装好消息,形成 AstrMessageEvent 对象,传递给插件。
- context: Context 一些公用数据
- message_str: str 纯消息字符串
- message_obj: AstrBotMessage 消息对象
- platform: RegisteredPlatform 来源平台
- role: str 基本身份。
admin
或member
- session_id: int 会话 id
AstrBotMessage
这 是机器人内部的消息对象(v3.1.10 后),会对每个平台原本的消息类进行转译。原平台的消息类在 raw_message
中。
tag: str # 消息来源标签
type: MessageType # 消息类型,GROUP_MESSAGE、FRIEND_MESSAGE、GUILD_MESSAGE
self_id: str # 机器人的识别id
session_id: str # 会话id
message_id: str # 消息id
sender: MessageMember # 发送者,user_id 和 nickname
message: List[BaseMessageComponent] # 消息链使用 Nakuru 的消息链格式
message_str: str # 最直观的纯文本消息字符串
raw_message: object 原平台的消息类
timestamp: int # 消息时间戳
主动发送消息
快捷主动回复
此功能不支持 qqchan 平台,也就是官方机器人API 。
AstrBot 封装了一个统一的快捷回复方法,可以直接在插件中调用。(>=v3.3.8)
unified_msg_origin
: 这是一个字符串,用于标识消息来源。它的格式类似 aiocqhttp:GroupMessage:123456
。其中 aiocqhttp
是平台名,GroupMessage
是消息类型,123456
是群号。
通过保存(因为是字符串,因此也可以持久化保存)这个标识,可以在后续的消息(甚至重启之后),通过指定标识来主动发送消息给对应的消息来源。
此值在 AstrMessageEvent
中可以找到。
context.register_commands("sub", "sub", "内置测试指令。", 1, self.sub)
def sub(self, message: AstrMessageEvent, context: Context):
unified_msg_origin = message.unified_msg_origin
# 保存此值 ...
def send(self, context: Context):
# 通过 unified_msg_origin 回复消息
await context.send_message(unified_msg_origin, CommandResult().message("Hello, World!"))
有了这个功能,你可以实现一些复杂的插件逻辑,比如订阅、定时任务等。
主动发送消息的其他方法
插件可以主动给受支持的平台发送消息。(>=v3.3.3)
采用 context.platforms 获取所有已注册(启用)的平台。
platforms = context.platforms
platfroms
为一个列表,列表中的每个元素都是一个 RegisteredPlatform 对象。RegisteredPlatform 对象包含以下属性:
- platform_name: str
- platform_instance: Platform
- origin: str = None # 注册来源
当前 AstrBot 内部支持的平台的 platform_name 有:
- aiocqhttp
- qqchan
- nakuru
其中,aiocqhttp 的稳定性最高,qqchan 和 nakuru 为实验性支持。
然后通过平台实例 platform_instance
的 send_msg 方法发送消息。send_msg 方法接收两个参数:target
和 message
。target
为目标,message
为 CommandResult
对象。
aiocqhttp
target
接收一个 dict 类型的值引用。
- 要发给 QQ 下的某个用户,请添加 key
user_id
,值为 int 类型的 qq 号; - 要发给某个群聊,请添加 key
group_id
,值为 int 类型的 qq 群号;
nakuru
target
接收一个 dict 类型的值引用。
- 要发给 QQ 下的某个用户,请添加 key
user_id
,值为 int 类型的 qq 号; - 要发给某个群聊,请添加 key
group_id
,值为 int 类型的 qq 群号; - 要发给某个频道,请添加 key
guild_id
,channel_id
。均为 int 类型。
guild_id 不是频道号。
qqchan
target
接收一个 dict 类型的值引用。
- 如果目标是 QQ 群,请添加 key
group_openid
。 - 如果目标是 频道消息,请添加 key
channel_id
。 - 如果目标是 频道私聊,请添加 key
guild_id
。
一个简单的示例:
import threading
from util.plugin_dev.api.v1.bot import Context, AstrMessageEvent, CommandResult
from util.plugin_dev.api.v1.config import *
class HelloWorldPlugin:
"""
AstrBot 会传递 context 给插件。
- context.register_commands: 注册指令
- context.register_task: 注册任务
- context.message_handler: 消息处理器(平台类插件用)
"""
def __init__(self, context: Context) -> None:
self.context = context
self.context.register_commands("helloworld", "helloworld", "内置测试指令。", 1, self.helloworld)
async def helloworld(self, message: AstrMessageEvent, context: Context):
await self.send_msg()
async def send_msg(self):
platforms = self.context.platforms
platform = None
for p in platforms:
if p.platform_name == 'aiocqhttp':
platform = p
break
if platform:
inst = message.platform.platform_instance
await inst.send_msg({"user_id": 123456}, CommandResult().message("Hello, World!"))
🚧 施工中 🚧