跳转至

类开发模式

类开发模式通过直接继承接口和手写实现类来完成插件开发。适合逻辑复杂、需要精细控制生命周期、混合多种扩展类型的插件。

以下以 erii 内置的 animal(虚拟宠物养成)插件为真实示例进行说明。

1. 创建插件项目

erii-plugins/ 下创建新目录:

erii-plugins/my-plugin/
├── build.gradle.kts
└── src/main/kotlin/
    └── myplugin/
        ├── MyPlugin.kt
        └── MyExtension.kt

2. 注册模块

erii-plugins/settings.gradle.kts 中添加:

include("my-plugin")

3. 配置构建脚本

// erii-plugins/my-plugin/build.gradle.kts
plugins {
    id("uesugi.erii-plugin")
}

version = "0.0.1"

uesugi.erii-plugin 是 erii 提供的 Gradle 约定插件,自动配置了 PF4J 打包、Kotlin 编译、依赖管理等功能。

4. 定义插件主类

使用 @PluginDefinition 类注解标注插件主类,继承 AgentPlugin()

package uesugi.plugin.animal

import uesugi.spi.AgentPlugin
import uesugi.spi.PluginDefinition

@PluginDefinition(
    pluginId = "animal",
    version = "0.0.1",
    description = "虚拟宠物养成插件"
)
class Animal : AgentPlugin()

@PluginDefinition 的参数:

参数 说明 必需
pluginId 插件唯一标识符,PF4J 通过此 ID 管理插件
version 语义化版本号
requires 依赖的其他插件 ID
dependencies 外部依赖声明
description 插件描述文本
provider 插件作者/组织
license 许可证类型

编译时 PluginDefinitionProcessor(javax.annotation.processing)读取此注解并生成 META-INF/plugin.properties 文件。

5. 实现扩展

5.1 同时实现多种扩展接口

animal 插件的 AnimalExtension 同时实现了 PassiveExtensionCmdExtension 两种接口,还扮演了 RouteExtension 的角色(通过 context.chain 注册):

package uesugi.plugin.animal

import org.pf4j.Extension
import uesugi.spi.*

@Extension
class AnimalExtension : PassiveExtension<Animal>,
    CmdExtension<AnimalContext, AnimalArgParser, Animal> {

    override val cmd: String = "animal"

    private lateinit var context: PluginContext
    private lateinit var store: AnimalStore
    private lateinit var service: AnimalService
    private lateinit var dailyTaskService: DailyTaskService
    private lateinit var htmlRenderer: AnimalHtmlRenderer
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private val serverPort = 8888

注意:扩展类必须标注 @Extension(PF4J 的 org.pf4j.Extension 注解),PF4J 通过 SPI 服务发现机制(META-INF/services/org.pf4j.Extension)自动加载。

5.2 CmdExtension 的三个泛型参数

interface CmdExtension<Context, Arg : ArgParserHolder<Context>, Plugin : AgentPlugin>
    : AgentExtension<Plugin>
泛型参数 说明 animal 示例
Context 命令执行上下文类型 AnimalContext
Arg Clikt 参数解析器类型(继承 ArgParserHolder<Context> AnimalArgParser
Plugin 插件主类类型 Animal

ArgParserHolder<Context> 继承 CliktCommand,可利用 Clikt 库的参数声明式定义子命令和选项。在 Meta.parser(context) 中自动解析 /animal <子命令> <参数>

5.3 onLoad() — 初始化

override fun onLoad(context: PluginContext) {
    this.context = context

    // 1. 初始化持久化存储
    store = AnimalStore(context.kv)

    // 2. 初始化业务服务层
    service = AnimalService(store)

    // 3. 注册日常定时任务
    dailyTaskService = DailyTaskService(store, service, context.scheduler)
    dailyTaskService.startDailyTasks()

    // 4. 注册自定义 HTTP 路由(通过 context.server.route {})
    htmlRenderer = AnimalHtmlRenderer(store, context)
    htmlRenderer.registerHtmlRoutes()

    // 5. 注册命令处理器
    registerCommandHandler()

    // 6. 注册 LLM 工具集
    registerTools()

    log.info { "AnimalExtension loaded" }
}

注册命令处理链

private fun registerCommandHandler() {
    context.chain { meta ->
        AnimalCommandHandler(
            store = store,
            service = service,
            serverPort = serverPort,
            serverBasePath = context.server.url.pathSegments.joinToString("/", "/"),
            scope = scope
        ).handleWithError(meta) { ctx ->
            meta.parser(ctx)  // Clikt 参数解析,匹配合适的子命令
        }
    }
}

context.chain { meta -> ... } 注册一个消息处理链: - 每次群组收到消息时被调用 - meta.input 包含用户消息文本 - meta.parser(ctx) 匹配 CmdExtension 的 cmd/alias 并调用 Clikt 解析参数

注册 LLM 工具集

private fun registerTools() {
    context.tool {
        {
            AnimalToolSet(
                store,
                service,
                serverPort,
                context.server.url.pathSegments.joinToString("/", "/"),
            )
        }
    }
}

context.tool { { ... } } 注册一个返回 MetaToolSetCreator 的工厂函数。Agent 在 Function Calling 时可调用 AnimalToolSet 中标注了 @Tool 的方法。

5.4 ToolSet 实现示例

class AnimalToolSet(
    private val store: AnimalStore,
    private val service: AnimalService,
    private val serverPort: Int,
    private val serverBasePath: String,
) : MetaToolSet {

    // 创建命令执行上下文
    private fun createAnimalContext(): AnimalContext {
        return AnimalContextFactory.createFromMeta(
            meta = MetaToolSet.meta,
            store = store, service = service,
            serverPort = serverPort, serverBasePath = serverBasePath
        )
    }

    // 通用命令执行
    private fun runCommand(argv: List<String>): String? {
        return try {
            val ctx = createAnimalContext()
            val parser = AnimalArgParser()
            parser.init(MetaToolSet.meta, ctx)
            parser.main(argv)
            null
        } catch (e: Exception) {
            log.error(e) { "Failed to run command" }
            "执行失败:${e.message}"
        }
    }

    @Tool
    @LLMDescription("注册用户,获取一只随机宠物")
    fun registerAnimal(): String? = runCommand(listOf("register"))

    @Tool
    @LLMDescription("查看用户的宠物列表")
    fun listAnimals(): String? = runCommand(listOf("list"))

    @Tool
    @LLMDescription("使用金币抽宠物,100金币抽1次,1000金币抽10次")
    fun drawAnimal(count: Int = 1): String? = runCommand(listOf("draw", count.toString()))

    @Tool
    @LLMDescription("售卖宠物,需要宠物ID参数")
    fun sellAnimal(petId: Long): String? = runCommand(listOf("sell", petId.toString()))
}

MetaToolSet 通过 MetaToolSet.meta 静态属性获取当前消息的 Meta 对象。

5.5 自定义 HTTP 路由(Server.route)

// AnimalHtmlRenderer 中
fun registerHtmlRoutes() {
    context.server.route {
        get("/animal/{petId}") {
            // 渲染宠物详情 HTML 页面
        }
        get("/animal/farm") {
            // 渲染宠物农场 HTML 页面
        }
    }
}

context.server.route {} 使用 Ktor 的 Route DSL,完全等价于在 Ktor 模块中注册路由。

5.6 onUnload() — 清理

override fun onUnload() {
    dailyTaskService.stopDailyTasks()
    scope.cancel()  // 取消协程作用域
    log.info { "AnimalExtension unloaded" }
}

6. 手动注册 PF4J Extensions 服务文件

类开发模式下,需要在插件项目的 src/main/resources/META-INF/services/org.pf4j.Extension 文件中手动列出所有扩展类的全限定名:

uesugi.plugin.animal.AnimalExtension

注解模式下此文件由 KSP 自动生成。

完整开发流程总结

步骤 操作 文件位置
1 创建插件目录 erii-plugins/my-plugin/
2 注册 Gradle 子项目 erii-plugins/settings.gradle.kts
3 配置构建 erii-plugins/my-plugin/build.gradle.kts
4 定义插件主类 @PluginDefinition class MyPlugin : AgentPlugin()
5 实现扩展 实现 AgentExtension<T> / RouteExtension<T> / CmdExtension / PassiveExtension<T>
6 实现 onLoad() 初始化存储、注册 chain/tool/scheduler/route
7 实现 onUnload() 清理资源、取消协程
8 添加 services 文件 META-INF/services/org.pf4j.Extension
9 构建 cd erii-plugins && ./gradlew :my-plugin:build
10 部署 build/plugin/my-plugin-*.zip 放入 plugins/ 目录
11 重载 erii reload && erii refresh