Skip to main content

插件开发

info

欢迎加群 322154837 讨论。

如何开发

  • clone AstrBot 项目本体到本地。
  • 打开 helloworld,这个是一个插件模板,接下来在这个模板上二次开发。
  • 点击右上角的 Use this template,然后点击 Create new repository。
  • 在 Repository name 处输入你的插件名字,不要中文。建议以 astrbot_plugin_ 开头,如 astrbot_plugin_yuanshen
    • image
  • 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)

返回的结果:

custom_t2i_tmpl

这只是一个简单的例子。得益于 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) # 关闭文转图
tip

AstrBot 触发文转图只会在回复操作(也就是指令处理函数的return)中生效,如果你在插件中主动发送消息,文转图永远不会生效。

AstrMessageEvent

插件处理函数的参数之一。

当用户发送消息时,AstrBot 会封装好消息,形成 AstrMessageEvent 对象,传递给插件。

  • context: Context 一些公用数据
  • message_str: str 纯消息字符串
  • message_obj: AstrBotMessage 消息对象
  • platform: RegisteredPlatform 来源平台
  • role: str 基本身份。adminmember
  • 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 方法接收两个参数:targetmessagetarget 为目标,messageCommandResult 对象。

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!"))

🚧 施工中 🚧