契约程序集:FlexBot.Abstractions,命名空间 FlexBot.PluginApi(插件只需引用这个程序集;宿主程序集与 OneBotLib 由宿主提供,不要在插件里自带副本)。
消息模型来自程序集 FlexBot.OneBotLib,命名空间 OneBotLib.Models(MessageObject / SenderInfo 等)、OneBotLib.MessageSegment(MessageSegment)、OneBotLib.Events(事件参数)。
using FlexBot.PluginApi;
using OneBotLib.MessageSegment;
using OneBotLib.Models;宿主按目录扫描插件,找实现 IBotPlugin 的类型并实例化(需要公开无参构造函数)。
public interface IBotPlugin
{
string Name { get; } // 插件名(唯一;同时是目录名)
string Version { get; } // 展示用版本
string Description { get; } // 一行说明(插件页显示)
Task OnLoadAsync(IBotContext context); // 加载:注册消息订阅/命令/定时任务
Task OnUnloadAsync(); // 卸载:释放自己持有的资源
IReadOnlyList<PluginSettingDef> SettingDefs => []; // 设置项声明(可省)
Task OnSettingsChangedAsync() => Task.CompletedTask; // 设置保存后回调(可省)
}OnLoadAsync抛异常 = 加载失败(插件页显示为未加载;Agent 插件缺 ApiKey 时就是这种预期失败)。- 卸载时宿主会先卸载 ALC;请在
OnUnloadAsync里停掉自己的线程/计时器,别再引用宿主对象。 - 不要自己
Task.Run长期循环而不留取消通道:宿主卸载后延续会跳进已释放的代码段。
public interface IBotContext : IDisposable
{
IBotApi Api { get; } // 发消息/群操作/查询
IBotConfig Config { get; } // 宿主配置快照(主人、目录、命令前缀、模型参数…)
string PluginDir { get; } // plugins/<名>/ 的绝对路径
string DataDir { get; } // plugins/<名>/data/(已创建)
IMessageHub Messages { get; } // 消息订阅
IEventHub Events { get; } // 原始事件订阅
ILog Log { get; } // 写宿主日志
IBotScheduler Scheduler { get; } // 定时任务
PluginKeyValueStore KV { get; } // 持久化键值(data/kv.json)
SharedHttp Http { get; } // 共享 HttpClient
ICompilerService Compiler { get; } // Roslyn 编译服务(脚本插件用)
T GetSetting<T>(string key, T? defaultValue = default);
IReadOnlyDictionary<string, object?> GetAllSettings();
IDisposable RegisterCommand(string name, string description, CommandHandler handler, string usage = "");
Task<string?> TryInvokeCommandAsync(string name, string args = ""); // 触发别的插件注册的命令
IReadOnlyList<CommandInfo> ListCommands();
}
public delegate Task<string> CommandHandler(string args); // 返回值即回给用户的内容
public sealed record CommandInfo(string Name, string Description, string PluginName, string Usage = "");命令名不含前缀(前缀由 IBotConfig.CommandPrefix 决定,默认 !);RegisterCommand 返回的 IDisposable 在释放时注销该命令。
IBotConfig 关键成员:
long OwnerUin { get; } // 主人 QQ(最高权限)
IReadOnlyCollection<long> AdminUins { get; } // 主人 + 额外管理员
string MemoryDir { get; } // 记忆/运行数据根目录
string CommandPrefix { get; } // 命令前缀(默认 "!")
string ActivePersonaInstructions { get; } // 当前启用的人格提示词
string ApiKey { get; } string BaseUrl { get; } string Model { get; }
bool IsVisionModel { get; } // 主模型是否支持图片
IReadOnlyList<ModelEndpoint> FallbackModels { get; }
int MaxContextTurns { get; } int MaxMsgChars { get; } int MaxImageBytes { get; } int GroupHistoryCount { get; }所有方法返回 ApiResult / ApiResult<T>:bool Success、string ErrorMessage(失败原因,官方通道的不支持能力会写明"官方通道不支持:…"),ApiResult<T> 另有数据载荷。
| 方法 | 说明 | 官方通道 |
|---|---|---|
SendPrivateMsgAsync(long userId, object message) |
发私聊(message 可传字符串或 List<MessageSegment>) |
✓ |
SendGroupMsgAsync(long groupId, object message) |
发群消息 | ✓ |
SendGroupForwardMsgAsync(long groupId, List<ForwardNode>) |
群合并转发 | ✗ |
SendPrivateForwardMsgAsync(long userId, List<ForwardNode>) |
私聊合并转发 | ✗ |
DeleteMsgAsync(long messageId) |
撤回 | ✓(只能撤回自己发的) |
SetMsgEmojiLikeAsync(long messageId, string emojiId, bool set = true) |
贴表情回应 | ✗ |
SetGroupBanAsync(long groupId, long userId, long duration = 0) |
禁言(0=解除) | ✗ |
SetGroupWholeBanAsync(long groupId, long duration = 0) |
全员禁言 | ✗ |
SetGroupKickAsync(long groupId, long userId, bool rejectAddRequest = false) |
踢人 | ✗ |
SetGroupCardAsync(long groupId, long userId, string? card = null) |
改群名片 | ✗ |
SetGroupSpecialTitleAsync(long groupId, long userId, string? title = null) |
设专属头衔 | ✗ |
SendGroupNoticeAsync(long groupId, string content, string? image = null) |
发群公告 | ✗ |
SetGroupLeaveAsync(long groupId, bool isDismiss = false) |
退群/解散 | ✗ |
GetGroupInfoAsync(long groupId) |
群信息 | ✗ |
GetFriendListAsync() |
好友列表 | ✗ |
UploadGroupFileAsync(long groupId, string file, string name, string? folder = null) |
传群文件 | ✗ |
SetFriendAddRequestAsync(string flag, bool approve = true, string? remark = null) |
处理好友请求 | ✗ |
SetGroupAddRequestAsync(string flag, string subType, bool approve = true, string? reason = null) |
处理加群请求 | ✗ |
GetMsgAsync(long messageId) |
单条消息 | ✗ |
GetGroupMsgHistoryAsync(long groupId, long? messageId = null, int count = 20) |
群历史 | ✗ |
GetGroupMemberListAsync(long groupId) |
群成员列表 | ✓ |
GroupPokeAsync(long groupId, long userId) |
群戳一戳 | ✗ |
GetLoginInfoAsync() |
登录号信息 | ✓ |
CallApiAsync(string action, Dictionary<string, object>? parameters = null) |
透传任意 OneBot 动作(官方通道返回失败) | ✗ |
long SelfId { get; } |
机器人自身 id | ✓ |
降级建议:先判断 Success,失败时回一句人话或改走别的路径;不要因为官方通道不支持就让整个功能崩掉。
public class MessageObject // 收到的消息
{
long SelfId, MessageId, RealId, MessageSeq, Time;
long? UserId, GroupId; // 群消息有 GroupId;单聊 GroupId 为 null
string MessageType; // "group" / "private"
string RawMessage; // 原始 CQ 文本(OneBot)
List<MessageSegmentData> MessageSegments; // 结构化段(text/at/image/face…)
SenderInfo Sender;
}
public class SenderInfo
{
long UserId; string Nickname, Card, Sex, Area, Level, Role; int Age;
}发消息时的消息段(FlexBot.OneBotLib.MessageSegment,object message 可传 string 或 List<MessageSegment>):
| 常用(两条通道均可用文本表达) | 仅 OneBot 通道有效(官方通道会被忽略/降级) |
|---|---|
Text(string)、At(long)、AtAll()、AtWithNick(long, string?)、Image(string file, …)、Face(int)、Reply(long messageId)、Mention(string)、Emoji(string)、Link(string url, string text) |
Record / Video / Node(合并转发节点)/ Forward / Xml / Json / Location / Share / Contact / ContactGroup / Dice / Rps / Shake / Poke / Anonymous / Music / MusicCustom / File / Mface / Markdown / MiniApp / Ark / Keyboard / KeyboardButton / RichText / Gift / Card / Enotify / Bubble / Tts / MultiMsg / LongMsg / InlineKeyboard |
BotHelpers(FlexBot.Abstractions)——常用判定与组装:
| 成员 | 用途 |
|---|---|
PlainText(MessageObject) / MsgToText(object) |
取出纯文本(丢掉 at/图片等) |
StripMentions(string) |
去掉文本里的 @ 片段 |
HasText(MessageObject) |
是否含文本 |
GetAtTargets(MessageObject) |
被 @ 的 QQ 号列表 |
IsAtMe / IsMentionedMe / IsAtAll / MentionsOthers(MessageObject, long selfId) |
@ 相关判定 |
At(long) / TextWithAt(string, params long[]) |
组装消息段 |
GetReplyToId(MessageObject) |
取被引用消息 id |
GetQuotedMessageAsync(IBotApi, MessageObject) / GetQuotedTextAsync(…) |
取被引用的那条消息/文本 |
Quote(long messageId, string text) |
组装「引用 + 文本」消息段(等价 Msg.Quote) |
FindMembersAsync(IBotApi, long groupId, string keyword, int limit) |
按昵称/名片模糊找成员 |
GetMemberNameAsync(IBotApi, long groupId, long userId) |
取群名片或昵称 |
GetMemberListTextAsync(IBotApi, long groupId, int max = 200) |
成员列表文本 |
GetRecentGroupTextAsync(IBotApi, long groupId, int count = 20) |
近期群消息文本(上下文用) |
Msg 静态类目前提供 Msg.Quote(long messageId, string text)。
public enum Handled { Continue, Stop } // Stop = 不再传给优先级更低的订阅者
public interface IMessageHub
{
IDisposable OnPrivate(Func<PrivateMessageEventArgs, Task<Handled>> handler, int priority = 0, string? tag = null);
IDisposable OnGroup (Func<GroupMessageEventArgs, Task<Handled>> handler, int priority = 0, string? tag = null);
}
public interface IEventHub
{
// 订阅宿主原始事件(连接状态、群成员变动、戳一戳、加群请求…):事件参数类型来自 FlexBot.OneBotLib
IDisposable On<TEvent>(Func<TEvent, Task> handler, string? tag = null) where TEvent : EventArgs;
}priority越大越先执行;tag便于调试与批量注销。- 返回
Handled.Stop会阻止后续订阅者(以及宿主的默认处理)看到这条消息——只在确实想"吃掉"消息时用。 - 返回的
IDisposable释放即取消订阅;插件卸载时宿主会一并清理。
| 服务 | 成员 | 说明 |
|---|---|---|
IBotScheduler(ctx.Scheduler) |
Every(TimeSpan, Func<Task>, string? name)、DailyAt("HH:mm", …)、After(TimeSpan, …) |
都返回 IDisposable(释放即取消)。Every 最小间隔 1 秒;任务异常被宿主捕获并写日志,不会中断调度 |
PluginKeyValueStore(ctx.KV) |
Get<T>(key, default)、Set<T>(key, value)、Delete(key)、Keys |
存 plugins/<名>/data/kv.json,键名大小写不敏感,写入 2 秒防抖合并落盘 |
SharedHttp(ctx.Http) |
Client、GetStringAsync、GetByteArrayAsync、GetAsync、PostAsync |
进程级共享 HttpClient(60s 超时、自动解压、最多 16 连接)。不要 new HttpClient() |
ICompilerService(ctx.Compiler) |
内存编译 C# 源码,返回 PluginCompileResult(bool Ok, byte[]? AssemblyBytes, byte[]? SymbolBytes, string Diagnostics, …) |
脚本插件与 PluginBuilder 在用;完整签名见 FlexBot.Abstractions/ICompilerService.cs |
ILog(ctx.Log) |
Info / Warn / Error(string) / Error(string, Exception) |
写入宿主日志(WebUI 日志页可见) |
| 类型 | 说明 |
|---|---|
IBotTransport : IBotApi, IAsyncDisposable |
通道抽象:TransportName、ConnectAsync(url, token)、DisconnectAsync()、SyncCommandPanelsAsync(...)、OnInteraction 与全部消息事件。宿主按配置在 OneBot 与官方通道间二选一 |
BotCommandSpec(Name, Description, Usage, OnlyAdmin) |
对外注册命令的规格(官方通道的"指令面板"用) |
BotInteraction(Text, UserId, GroupId, IsGroup, Id) |
官方通道的按钮/快捷菜单回调(宿主会做 3 秒内 ack 并派发为命令) |
IIdentityBinder |
官方通道的 openid ↔ QQ 号绑定(!bind qq <QQ号> <口令>,绑定表持久化) |
- 不要跨卸载持有宿主对象:卸载后 ALC 会被回收,残留的引用会导致
ObjectDisposedException或更隐蔽的崩溃。 - 在途请求要能取消:长任务用
CancellationTokenSource+OnUnloadAsync里Cancel();宿主会等一段时间的在途任务归零。 - 共享状态要用并发安全的容器或
AsyncLocal:工具集这类"进程内单份"的对象上挂可变字段,会在多人并发时串号(Agent 插件就用AsyncLocal做轮内隔离)。 - 异常处理:消息处理里抛出的异常由宿主捕获记日志,但会导致该条消息的后续订阅者看不到它——自己 try/catch 更稳。
ctx与设置:GetSetting<T>读的是缓存值;设置页保存后会先写文件再回调OnSettingsChangedAsync(),在那里刷新自己的派生状态(例如重建模型客户端)。