Skip to content

插件注册面板服务

AniaBot 的 Web 控制面板与插件有两种协作机制,不要混淆

机制作用接口
配置表单在「配置管理」页渲染你的插件配置项plugin.ConfigSchemaProvider / plugin.ConfigRegistrar(见 第一个插件 · 声明自己的配置
面板功能页让你的插件成为「消息日志 / 任务日志 / 记忆管理 / 知识库 …」等页面的数据源adminpanel.XxxSource 可选接口(本章)

面板的功能页面不是写死的:core 启动面板前会遍历插件,用类型断言探测 adminpanel.XxxSource 可选接口(与 bot.QQ 同一套「可选接口 + 类型断言」惯例)。插件实现了哪个接口,对应页面就自动接上它的数据;没有任何插件实现时,页面返回空数据,面板照常运行。

发现机制

bot/core/core.gostartAdminPanel() 负责接线:

go
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.TaskLogSourceAI 对话插件(clock)
定时任务adminpanel.ClockTaskSourceAI 对话插件(clock)
技能管理adminpanel.SkillSourceAI 对话插件
记忆管理adminpanel.MemorySourceAI 对话插件
知识库adminpanel.KnowledgeBaseSourceAI 对话插件
Agent 团队adminpanel.TeamSourceAI 对话插件
配额管理adminpanel.QuotaSourceAI 对话插件
Query 日志adminpanel.QueryLogSourceAI 对话插件

接口签名

MsgLogSource —— 消息日志页

go
type MsgLogSource interface {
    // 分页返回消息日志(新在前);beforeID>0 时仅返回 ID 小于它的更旧日志(滚动分页游标)
    MsgLogPage(limit int, beforeID uint64) []msglog.Entry
}

msglog.Entry 字段:ID uint64TimeTypeGroupIdUserIdNicknameTitleText。可用 bot/component/msglogRecorder 记录与分页。

TaskLogSource —— 任务日志页

go
type TaskLogSource interface {
    TaskLogQuery(f tasklog.Filter) []tasklog.Entry
}

tasklog.Filter 支持按 TargetType(group/friend)、TargetIDTaskIDStatus、时间范围、标题关键词过滤与 Before 游标分页;tasklog.Entry 包含任务标题、触发内容、状态(running/success/timeout/error/interrupted)、耗时、回复与 token 用量等。推荐直接用 bot/component/tasklogLogger(内部已实现过滤与分页)。

ClockTaskSource —— 定时任务页

go
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/Updatecommon/plugininfo/clock.go

SkillSource —— 技能管理页

go
type SkillSource interface {
    SkillList() (skills []plugininfo.SkillInfo, dir string, whitelist []string)
    SkillDelete(name string) error
    SkillUpload(filename string, data []byte) error // 从 zip 内容安装并热重载
}

MemorySource —— 记忆管理页

go
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 —— 知识库页

go
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 团队页

go
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 —— 配额管理页

go
type QuotaSource interface {
    QuotaSummary() (plugininfo.QuotaSummaryInfo, error)
    QuotaReset(scope string) error // scope="all" 清空当日全部,否则仅清除指定会话
}

QueryLogSource —— Query 日志页

go
type QueryLogSource interface {
    QueryLogRecent(f querylog.Filter) []querylog.Entry
}

完整示例:自定义「消息日志」提供者

下面实现一个记录插件自身操作的面板数据源(比如一个「群投票插件」把每次投票结果记入消息日志页)。完整代码骨架:

go
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 冲突):

go
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 同款惯例:这是「可选接口 + 类型断言」能力的又一应用,探测不到就优雅降级(页面空数据),框架与面板代码零改动

下一步

基于 MIT 许可证发布