插件注册面板服务
AniaBot 的 Web 控制面板与插件有两种协作机制,不要混淆:
| 机制 | 作用 | 接口 |
|---|---|---|
| 配置表单 | 在「配置管理」页渲染你的插件配置项 | plugin.ConfigSchemaProvider / plugin.ConfigRegistrar(见 第一个插件 · 声明自己的配置) |
| 面板功能页 | 让你的插件成为「消息日志 / 任务日志 / 记忆管理 / 知识库 …」等页面的数据源 | adminpanel.XxxSource 可选接口(本章) |
面板的功能页面不是写死的:core 启动面板前会遍历插件,用类型断言探测 adminpanel.XxxSource 可选接口(与 bot.QQ 同一套「可选接口 + 类型断言」惯例)。插件实现了哪个接口,对应页面就自动接上它的数据;没有任何插件实现时,页面返回空数据,面板照常运行。
发现机制
bot/core/core.go 的 startAdminPanel() 负责接线:
for _, p := range ania.plugins {
if src, ok := p.(adminpanel.TaskLogSource); ok {
taskLogFn = src.TaskLogQuery
}
if src, ok := p.(adminpanel.ClockTaskSource); ok {
clockSrc = src
}
// ... MsgLogSource / SkillSource / MemorySource / KnowledgeBaseSource /
// TeamSource / QuotaSource / QueryLogSource 同理
if taskLogFn != nil && clockSrc != nil && /* ... */ {
break // 全部接口都集齐后停止遍历
}
}关键语义:
- 单提供者:每个接口只有一个插件接线,后遍历到的插件会覆盖前面的(同样
break发生在集齐全部接口时) - 内置插件已占用全部接口:默认
cmd/main.go里,AI 对话插件实现了 8 个接口、日志插件实现了MsgLogSource,在第 7 个插件(AI 对话)处就集齐并break—— 之后注册的自定义插件不会成为面板数据源 - 要让自定义插件接管某个页面:从
cmd/main.go移除占用该接口的内置插件(典型做法是移除pluginaichat.NewAIChatPlugin()),或完全按自己的插件组合构建(见下文示例)
单提供者的实际影响
面板服务是「最后集齐者胜」的单例接线,不是多插件聚合。如果你自研了定时任务/日志类插件,并希望面板「任务日志」「消息日志」页展示你的数据,需要在 main.go 中移除对应的内置提供者(pluginaichat / pluginlog),否则面板展示的是内置插件的数据。
接口总览
| 面板页面 | 接口 | 内置提供者 |
|---|---|---|
| 消息日志 | adminpanel.MsgLogSource | 日志打印插件(pluginlog) |
| 任务日志 | adminpanel.TaskLogSource | AI 对话插件(clock) |
| 定时任务 | adminpanel.ClockTaskSource | AI 对话插件(clock) |
| 技能管理 | adminpanel.SkillSource | AI 对话插件 |
| 记忆管理 | adminpanel.MemorySource | AI 对话插件 |
| 知识库 | adminpanel.KnowledgeBaseSource | AI 对话插件 |
| Agent 团队 | adminpanel.TeamSource | AI 对话插件 |
| 配额管理 | adminpanel.QuotaSource | AI 对话插件 |
| Query 日志 | adminpanel.QueryLogSource | AI 对话插件 |
接口签名
MsgLogSource —— 消息日志页
type MsgLogSource interface {
// 分页返回消息日志(新在前);beforeID>0 时仅返回 ID 小于它的更旧日志(滚动分页游标)
MsgLogPage(limit int, beforeID uint64) []msglog.Entry
}msglog.Entry 字段:ID uint64、Time、Type、GroupId、UserId、Nickname、Title、Text。可用 bot/component/msglog 的 Recorder 记录与分页。
TaskLogSource —— 任务日志页
type TaskLogSource interface {
TaskLogQuery(f tasklog.Filter) []tasklog.Entry
}tasklog.Filter 支持按 TargetType(group/friend)、TargetID、TaskID、Status、时间范围、标题关键词过滤与 Before 游标分页;tasklog.Entry 包含任务标题、触发内容、状态(running/success/timeout/error/interrupted)、耗时、回复与 token 用量等。推荐直接用 bot/component/tasklog 的 Logger(内部已实现过滤与分页)。
ClockTaskSource —— 定时任务页
type ClockTaskSource interface {
ClockTasks() []plugininfo.ClockTaskInfo
CreateClockTask(t plugininfo.ClockTaskCreate) (string, error)
UpdateClockTask(id string, f plugininfo.ClockTaskUpdate) error
DeleteClockTask(id string) error
}面板对插件管理的动态定时任务做增删改查与启停;plugininfo.ClockTaskInfo/Create/Update 见 common/plugininfo/clock.go。
SkillSource —— 技能管理页
type SkillSource interface {
SkillList() (skills []plugininfo.SkillInfo, dir string, whitelist []string)
SkillDelete(name string) error
SkillUpload(filename string, data []byte) error // 从 zip 内容安装并热重载
}MemorySource —— 记忆管理页
type MemorySource interface {
MemoryScopes() []plugininfo.MemoryScopeInfo
MemoryList(scope string) ([]plugininfo.MemoryEntryInfo, error)
MemoryCreate(up plugininfo.MemoryEntryUpsert) (string, error)
MemoryUpdate(up plugininfo.MemoryEntryUpsert) error
MemoryDelete(scope, id string) error
}KnowledgeBaseSource —— 知识库页
type KnowledgeBaseSource interface {
KnowledgeScopes() []plugininfo.KnowledgeScopeInfo
KnowledgeList(scope string) ([]plugininfo.KnowledgeDocInfo, error)
KnowledgeCreate(up plugininfo.KnowledgeDocUpsert) (string, error)
KnowledgeUpdate(up plugininfo.KnowledgeDocUpsert) error
KnowledgeDelete(scope, id string) error
KnowledgeImportURL(scope, url string) (string, error)
}TeamSource —— Agent 团队页
type TeamSource interface {
TeamRoles() []plugininfo.TeamRoleInfo
TeamScopes() []plugininfo.TeamScopeInfo
TeamList(scope string) ([]plugininfo.TeamInfo, error)
TeamCreate(up plugininfo.TeamUpsert) error
TeamUpdate(up plugininfo.TeamUpsert) error
TeamDelete(scope, name string) error
}QuotaSource —— 配额管理页
type QuotaSource interface {
QuotaSummary() (plugininfo.QuotaSummaryInfo, error)
QuotaReset(scope string) error // scope="all" 清空当日全部,否则仅清除指定会话
}QueryLogSource —— Query 日志页
type QueryLogSource interface {
QueryLogRecent(f querylog.Filter) []querylog.Entry
}完整示例:自定义「消息日志」提供者
下面实现一个记录插件自身操作的面板数据源(比如一个「群投票插件」把每次投票结果记入消息日志页)。完整代码骨架:
package pluginpoll
import (
"context"
"time"
"github.com/jeanhua/AniaBot/bot/component/msglog"
"github.com/jeanhua/AniaBot/common/bot"
"github.com/jeanhua/AniaBot/common/model/command"
"github.com/jeanhua/AniaBot/common/model/message"
"github.com/jeanhua/AniaBot/common/msgchain"
"github.com/jeanhua/AniaBot/common/plugin"
"github.com/jeanhua/AniaBot/common/plugininfo"
"github.com/spf13/viper"
)
type PollPlugin struct {
plugin.Meta
recorder *msglog.Recorder // 消息日志记录器
}
func NewPlugin() *PollPlugin {
return &PollPlugin{
Meta: plugin.Meta{
Name: "群投票插件",
HelpWords: "@我发送 /poll 选项A 选项B 发起投票",
Order: plugin.LevelNormal,
ShowFor: plugininfo.ShowForGroup,
},
}
}
func (p *PollPlugin) Start(ctx context.Context, cfg *viper.Viper) error {
// 记录器挂在缓存存储上;写入上限 0 = 不限制
p.recorder = msglog.New(p.Storage, 0)
return nil
}
// ---- 面板服务:实现 adminpanel.MsgLogSource ----
// MsgLogPage 分页返回本插件记录的消息日志(新在前)。
func (p *PollPlugin) MsgLogPage(limit int, beforeID uint64) []msglog.Entry {
if p.recorder == nil {
return nil
}
return p.recorder.Page(limit, beforeID)
}
func (p *PollPlugin) OnGroupMsg(ctx context.Context, b bot.Bot, cmd command.Command, msg message.Message) (bool, error) {
if !cmd.Mention || cmd.Name != "poll" {
return true, nil
}
// 记录一条日志:面板「消息日志」页可看到
if p.recorder != nil {
p.recorder.Add(msglog.Entry{
Time: time.Now(),
Type: msglog.TypeGroup,
GroupId: msg.GroupId.String(),
UserId: msg.Sender.UserId.String(),
Nickname: msg.Sender.Nickname,
Text: "发起投票: " + msg.RawMessage,
})
}
chain := msgchain.Builder().Group()
chain.Mention(msg.Sender.UserId)
chain.Text(" 投票已发起,请选择选项~")
b.SendGroupMsg(msg.GroupId, chain.Build())
return false, nil
}真实实现可直接参考内置插件:
- 消息日志:
bot/plugins/pluginlog/plugin.go - 任务日志 / 定时任务:
bot/plugins/pluginaichat/clock.go - 记忆 / 知识库 / 技能 / 团队 / 配额:
bot/plugins/pluginaichat/下的memorypanel.go/knowledgepanel.go/skillpanel.go/teampanel.go/quota.go
如何让自定义实现生效
默认 cmd/main.go 注册了全部内置插件,其中 pluginaichat 会在第 7 个插件处集齐全部接口并终止发现。要让上面的插件成为「消息日志」页提供者,需要移除内置日志插件(它与 MsgLogSource 冲突):
func main() {
bot := core.NewAniaBot()
bot.AddPlugin(pluginsys.NewPluginSys())
// bot.AddPlugin(pluginlog.NewPlugin()) // ← 移除:把「消息日志」页让给自定义插件
// bot.AddPlugin(pluginaichat.NewAIChatPlugin()) // ← 若你的插件接管的是 AI 相关页面,同样移除
bot.AddPlugin(pluginpoll.NewPlugin())
bot.Run()
}注意事项
- 方法缺一不可:
adminpanel.XxxSource是完整接口,实现时必须实现全部方法(不需要的能力返回空值/错误即可) - nil 安全:面板会在数据源为 nil 时返回空数据并正常渲染,插件内部初始化失败时返回 nil 即可,不要 panic
- 即时生效:
MemorySource/SkillSource/KnowledgeBaseSource/TeamSource/QuotaSource的改动由插件自己落盘,无需重启;TaskLogSource/MsgLogSource/QueryLogSource是查询接口,同样实时反映插件当前数据 - 数据量控制:分页类接口遵循「新在前 +
before游标」约定,返回条数不要超过limit;记录型组件(msglog/tasklog/querylog)内部已实现容量淘汰,直接复用即可 - 与
bot.QQ同款惯例:这是「可选接口 + 类型断言」能力的又一应用,探测不到就优雅降级(页面空数据),框架与面板代码零改动
下一步
- 从零开发一个完整插件 —— 把配置、存储、定时任务与面板服务串起来
- 插件系统概览 —— 生命周期与事件模型
- 配置注册 —— 面板配置表单机制
