NQBot 插件 SDK 开发规范
本文档面向第三方插件开发者,约定 NQBot 1.0 的插件契约、生命周期、事件、动作、配置和发布方式。
- SDK 项目:
src/NQBot.Sdk/NQBot.Sdk.csproj - 目标框架:
net9.0(需要 Windows 专属 API 时可用net9.0-windows) - 契约版本:
NQBotSdk.ContractVersion = 2 - 插件只引用
NQBot.Sdk,不要引用NQBot.Core/NQBot.Host/NQBot.App
1. 最小插件
using NQBot.Sdk;
namespace MyCompany.Plugins.Hello;
public sealed class HelloPlugin : BotPluginBase
{
public override string Id => "my-hello";
public override string Name => "示例插件";
public override string Version => "1.0.0";
public override string Author => "你的名字";
public override string Description => "收到“你好”时回复一句话。";
public override void OnEvent(BotEvent evt)
{
if (evt.Kind != BotEventKind.GroupMessage) return;
if (PluginHelpers.CleanText(evt.Text) != "你好") return;
_ = Context.Actions.SendGroupTextAtAsync(evt.GroupId, evt.UserId, "你好呀!");
}
}
项目文件:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\NQBot.Sdk\NQBot.Sdk.csproj" />
</ItemGroup>
</Project>
编译后把 bin/Release/net9.0/MyPlugin.dll 复制到 runtime/plugins/,然后在插件页“重新加载插件”。
2. 元数据
| 成员 | 说明 |
|---|---|
Id |
稳定唯一标识,用于数据目录、日志和启用状态;发布后不要改 |
Name |
插件列表显示名 |
Version |
插件版本,建议语义化版本 |
Author |
作者 |
Description |
插件简介 |
Id 只能包含字母、数字、点、横线、下划线,避免路径分隔符;宿主会拒绝不符合规则的 Id。
3. 生命周期
OnLoad(context) -> OnEvent/OnEventAsync(多次)-> OnUnload()
OnLoad:加载配置、注册出站过滤器/转换器、注册定时任务。异常由宿主记录并隔离。OnEvent:同步处理;耗时逻辑请重写OnEventAsync。OnEventAsync:异步处理;宿主按插件顺序调用,默认单插件超时 5 秒。OnUnload:释放资源、保存配置。宿主卸载/退出时调用。
插件在独立 AssemblyLoadContext 中加载,卸载后可替换 DLL 重新加载。
4. 事件模型
BotEventKind:
| 类型 | 说明 |
|---|---|
GroupMessage |
群消息 |
PrivateMessage |
私聊消息 |
GroupIncrease / GroupDecrease |
群成员增加/减少 |
GroupBan |
禁言/解除禁言 |
GroupCardChange |
群名片变更 |
GroupRecall |
群消息撤回 |
FriendRequest |
好友请求 |
GroupRequest |
加群请求/邀请 |
FriendAdd |
好友已添加 |
BotEvent 常用字段:
| 字段 | 说明 |
|---|---|
Kind |
事件类型 |
SelfId |
机器人 QQ |
GroupId |
群号;私聊为 0 |
UserId |
触发者 QQ |
OperatorId |
操作者 QQ |
MessageId / ReplyMessageId |
消息 ID / 被回复消息 ID |
RequestFlag |
好友/加群请求 flag,处理请求时原样回传 |
Text |
纯文本;图片/表情为占位符 |
SenderNick / SenderCard / SenderRole |
发送者信息;角色为 owner / admin / member |
Segments |
消息段(文本、图片、@、回复等) |
AtTargets / IsAtMe |
被 @ 的 QQ / 是否 @ 机器人 |
DurationSeconds |
禁言时长 |
RawJson |
原始 OneBot 事件 |
Time |
事件时间 |
5. 动作 API
Context.Actions 提供:
Task<ActionResult> SendGroupTextAsync(long groupId, string text, ...);
Task<ActionResult> SendPrivateTextAsync(long userId, string text, ...);
Task<ActionResult> SendGroupTextAtAsync(long groupId, long userId, string text, ...);
Task<ActionResult> SendGroupAtAllAsync(long groupId, string text, ...);
Task<ActionResult> SendGroupSegmentsAsync(long groupId, IReadOnlyList<MessageSegment> segments, ...);
Task<ActionResult> SendPrivateSegmentsAsync(long userId, IReadOnlyList<MessageSegment> segments, ...);
Task<ActionResult> RecallAsync(long messageId, ...);
Task<ActionResult> MuteGroupMemberAsync(long groupId, long userId, int seconds, ...);
Task<ActionResult> KickGroupMemberAsync(long groupId, long userId, bool rejectAddRequest = false, ...);
Task<ActionResult> SetGroupCardAsync(long groupId, long userId, string card, ...);
Task<string> GetGroupMemberNameAsync(long groupId, long userId, ...);
Task<ActionResult> SetFriendAddRequestAsync(string flag, bool approve, string remark = "", ...);
Task<ActionResult> SetGroupAddRequestAsync(string flag, bool approve, string reason = "", ...);
ActionResult 不抛协议异常,失败时带 ErrorCode / Message:
var result = await Context.Actions.SendGroupTextAsync(groupId, "hello");
if (!result.Success) Context.Log.Warn($"{result.ErrorCode}: {result.Message}");
消息段:
new MessageSegment.Text("文字")
new MessageSegment.At(userId)
new MessageSegment.At(0, All: true) // @全体成员
new MessageSegment.Face(faceId)
new MessageSegment.Image("https://...") // URL / 本地路径 / file id
new MessageSegment.Record("...")
new MessageSegment.Video("...")
new MessageSegment.Reply(messageId)
6. 配置与数据
6.1 自动设置页
实现 IConfigurablePlugin:
public IReadOnlyList<PluginOption> GetOptions() => new[]
{
new PluginOption("enabled", "启用插件", PluginOptionType.Bool, "true"),
new PluginOption("reply", "回复内容", PluginOptionType.Text, "你好呀!"),
new PluginOption("mode", "模式", PluginOptionType.Choice, "a",
"选择运行模式", new[] { "a", "b" })
};
支持类型:Text、Number、Bool、Multiline、Json、Choice。
6.2 读取/保存配置
public sealed class MyConfig
{
public bool Enabled { get; set; } = true;
public string Reply { get; set; } = "你好呀!";
}
_config = PluginHelpers.LoadConfig(Context, () => new MyConfig());
PluginHelpers.SaveConfig(Context, _config);
配置文件位于 runtime/data/{插件Id}/config.json。
6.3 键值设置
bool enabled = Context.Settings.GetBool("enabled", true);
Context.Settings.Set("enabled", false);
Context.Settings.Save();
6.4 按群设置
实现 IGroupConfigurablePlugin 声明群级参数,然后:
bool enabled = Context.GroupSettings.GetBool(groupId, "enabled", true);
Context.GroupSettings.Set(groupId, "enabled", false);
未配置的群级键由调用方传入 fallback;通常把全局配置值作为 fallback。
6.5 JSON 数据
var data = Context.Storage.Load<MyData>("data") ?? new MyData();
Context.Storage.Save("data", data);
文件位于 runtime/data/{插件Id}/data.json。
7. 权限
if (Context.Permissions.IsMaster(evt.UserId)) { ... }
if (await Context.Permissions.IsGroupAdminAsync(evt.GroupId, evt.UserId)) { ... }
Masters:宿主设置中的主人 QQ 列表。IsMaster:是否软件主人。IsGroupAdminAsync:是否群主/管理员。
8. 定时任务
Context.Scheduler.Every(TimeSpan.FromMinutes(5), () => { ... });
Context.Scheduler.Daily(TimeSpan.FromHours(9), () => { ... });
返回的 IDisposable 释放即取消。插件停用时宿主会暂停其定时任务,启用后恢复。
9. 出站过滤与转换
9.1 文本过滤
public sealed class ReplacePlugin : BotPluginBase, IOutboundTextFilter
{
public override void OnLoad(IPluginContext context)
{
base.OnLoad(context);
context.AddOutboundFilter(this);
}
public string Filter(string text) => text.Replace("坏词", "***");
}
9.2 整条消息转换
public sealed class ImagePlugin : BotPluginBase, IOutboundMessageTransformer
{
public override void OnLoad(IPluginContext context)
{
base.OnLoad(context);
context.AddOutboundTransformer(this);
}
public IReadOnlyList<MessageSegment> Transform(long targetId, bool isGroup,
IReadOnlyList<MessageSegment> segments)
{
// 返回原列表表示不处理;返回空列表表示取消发送
return segments;
}
}
过滤器/转换器是全局的:任意插件通过 Context.Actions 发送消息时都会按注册顺序执行。插件停用后其过滤器/转换器自动跳过。
10. 错误处理与超时
OnLoad抛异常:插件加载失败,不影响其他插件。OnEvent抛异常:宿主记录日志并继续处理其他插件。- 单插件事件超时(默认 5 秒):宿主取消本次调用并记录警告。
- 动作 API 失败返回
ActionResult,不要依赖异常控制流程。 - 不要在
OnEvent中阻塞等待网络;使用OnEventAsync或Task.Run。
11. 打包与发布
dotnet build MyPlugin.csproj -c Release- 把
bin/Release/net9.0/MyPlugin.dll及同目录依赖复制到runtime/plugins/。 - 插件页“重新加载插件”或重启宿主。
- 插件数据目录会自动创建:
runtime/data/{插件Id}/。
删除插件时文件移动到 runtime/plugins-disabled/,数据目录保留。
12. 与旧版插件共存
旧版 .plugin 由可选的 32 位兼容运行时加载。宿主设置 LegacyTakeover=true 时:
- 旧版插件接管消息时,新版插件 Id 在
LegacyCompat.OverlappingPluginIds中时不接收事件; - 新插件使用全新 Id(如
my-hello)时会与旧版插件同时工作; - 旧版事件由宿主异步转发,不保证与新版插件的严格先后顺序。
建议新插件使用唯一命令前缀,避免和旧版插件重复回复。
13. 安全建议
- 不记录或上传用户隐私、Cookie、Token。
- 不执行未校验的外部命令。
- 文件路径必须限制在插件数据目录或明确允许的目录。
- 网络请求设置超时,处理失败。
- 不在插件中硬编码管理员 QQ、服务器地址或密钥。
14. 示例项目
完整示例:samples/NQBot.Plugin.Hello/。