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. 打包与发布

  1. dotnet build MyPlugin.csproj -c Release
  2. 把 bin/Release/net9.0/MyPlugin.dll 及同目录依赖复制到 runtime/plugins/。
  3. 插件页“重新加载插件”或重启宿主。
  4. 插件数据目录会自动创建: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/。