-
Notifications
You must be signed in to change notification settings - Fork 0
Beyond Bot
"A very tiny framework for any extendable event-driven system."
虽然 yes-core 最初的诞生是为了解决复杂的多平台机器人适配问题,但当我们剥开“机器人”这层业务外衣,会发现它的本质是一个极度纯粹的、基于事件驱动和依赖注入的微内核。
在 yes-core 的视角里,没有 QQ、没有消息段、没有群聊。它的世界里只有:
- 插件:负责干活的结构体。
-
生命周期:
Init->Start->Stop的标准流转。 -
事件总线:
Publish与Subscribe的异步解耦广播。 -
注册中心:基于
DependsOn拓扑排序的强类型依赖注入。 这意味着,任何需要模块化、插件化、且模块间需要相互通信的 Go 并发应用,都可以直接拿yes-core当底座,而且极度方便。 下面我们通过一个严谨的示例,展示如何用yes-core在几分钟内搓出一个“与机器人毫无关系”的系统。
在这个示例中,我们将看到 yes-core 如何优雅地管理 HTTP Server 的生命周期,并将其与业务逻辑彻底解耦。
这是系统的心脏,提供具体的业务能力。它不关心是谁调用了它(是 HTTP 请求还是定时器),它只负责处理并广播结果。
package plugin_api
import (
"fmt"
"github.com/yeswearebot/yes-core/core"
)
type APIPlugin struct{}
func init() {
core.Register(func() core.Plugin { return &APIPlugin{} })
}
func (p *APIPlugin) Name() string { return "api-service" }
func (p *APIPlugin) DependsOn() []string { return nil }
func (p *APIPlugin) Init(ctx *core.SystemContext) error { return nil }
func (p *APIPlugin) Start(ctx *core.SystemContext) error { return nil }
func (p *APIPlugin) Stop(ctx *core.SystemContext) error { return nil }
// 对外暴露的强类型业务方法
func (p *APIPlugin) ExecuteTask(taskName string) string {
result := fmt.Sprintf("任务 [%s] 执行完毕,结果码: 200", taskName)
// 执行完毕后,通过事件总线通知其他插件(比如日志插件)
ctx.Events.Publish("task.completed", result)
return result
}这个插件负责提供 HTTP 接口。它依赖 api-service,当收到 HTTP 请求时,直接调用 APIPlugin 的方法。
package plugin_web
import (
"encoding/json"
"net/http"
"github.com/yeswearebot/yes-core/core"
"plugin_api" // 引入 api 插件包用于类型断言
)
type WebPlugin struct {
server *http.Server
}
func init() {
core.Register(func() core.Plugin { return &WebPlugin{} })
}
func (p *WebPlugin) Name() string { return "web-server" }
func (p *WebPlugin) DependsOn() []string { return []string{"api-service"} }
func (p *WebPlugin) Init(ctx *core.SystemContext) error {
mux := http.NewServeMux()
mux.HandleFunc("/trigger", func(w http.ResponseWriter, r *http.Request) {
// 从依赖注入获取 API 插件实例
rawAPI, ok := ctx.Registry.Get("api-service")
if !ok {
http.Error(w, "服务未就绪", http.StatusInternalServerError)
return
}
api := rawAPI.(*plugin_api.APIPlugin)
// 调用业务逻辑
taskName := r.URL.Query().Get("task")
if taskName == "" {
taskName = "default"
}
result := api.ExecuteTask(taskName)
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "result": result})
})
p.server = &http.Server{Addr: ":8080", Handler: mux}
return nil
}
func (p *WebPlugin) Start(ctx *core.SystemContext) error {
go func() {
fmt.Println("[WebPlugin] 🌐 HTTP 服务启动在 :8080")
if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("[WebPlugin] HTTP 服务异常: %v\n", err)
}
}()
return nil
}
func (p *WebPlugin) Stop(ctx *core.SystemContext) error {
fmt.Println("[WebPlugin] 正在优雅关闭 HTTP 服务...")
return p.server.Close()
}这个插件负责定时触发任务,它同样依赖 api-service,证明了 api-service 的逻辑可以被高度复用。
package plugin_cron
import (
"time"
"github.com/yeswearebot/yes-core/core"
"plugin_api"
)
type CronPlugin struct {
stopChan chan struct{}
}
func init() {
core.Register(func() core.Plugin { return &CronPlugin{} })
}
func (p *CronPlugin) Name() string { return "cron-scheduler" }
func (p *CronPlugin) DependsOn() []string { return []string{"api-service"} }
func (p *CronPlugin) Init(ctx *core.SystemContext) error {
p.stopChan = make(chan struct{})
return nil
}
func (p *CronPlugin) Start(ctx *core.SystemContext) error {
// 获取 API 实例
rawAPI, _ := ctx.Registry.Get("api-service")
api := rawAPI.(*plugin_api.APIPlugin)
go func() {
ticker := time.NewTicker(10 * time.Second) // 每 10 秒执行一次
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("[CronPlugin] ⏰ 定时任务触发")
api.ExecuteTask("scheduled-cleanup")
case <-p.stopChan:
return
}
}
}()
return nil
}
func (p *CronPlugin) Stop(ctx *core.SystemContext) error {
close(p.stopChan)
return nil
}你的 main.go 不需要写任何业务逻辑,只需要告诉内核你要用哪些积木:
package main
import (
"github.com/yeswearebot/yes-core/core"
_ "plugin_api"
_ "plugin_web"
_ "plugin_cron"
)
func main() {
app := core.NewApp()
if err := app.Run(); err != nil {
panic(err)
}
}当你启动这个系统时,yes-core 会做如下调度:
- 检测到
web-server和cron-scheduler都依赖api-service,于是先初始化并启动api-service。 - 启动
web-server,监听 8080 端口。 - 启动
cron-scheduler,开启 10 秒的定时器。 此时,你可以用浏览器访问http://localhost:8080/trigger?task=manual,会看到返回了执行结果;同时,控制台每 10 秒会打印一次定时任务的执行记录。当你按下Ctrl+C时,内核会依次调用Stop(),HTTP 服务会优雅关闭,定时器会停止退出,整个进程干净利落地结束。
假设我们有这样一个需求:
- 采集节点:不断生成原始日志。
- 清洗节点:接收原始日志,提取级别和内容,转为标准格式。
-
告警节点:如果日志级别是
ERROR,触发报警机制(如调用外部接口)。 -
通知节点:提供统一的发邮件/发微信能力,供告警节点调用。
使用
yes-core,我们不需要写复杂的main.go逻辑去拼凑这些组件,只需要把它们全部变成平级插件,让内核来组装。
这是一个底层基础插件,不依赖任何其他插件,只提供服务。
package plugin_notifier
import (
"fmt"
"github.com/yeswearebot/yes-core/core"
)
type NotifierPlugin struct{}
func init() {
core.Register(func() core.Plugin { return &NotifierPlugin{} })
}
func (p *NotifierPlugin) Name() string { return "notifier" }
func (p *NotifierPlugin) DependsOn() []string { return nil }
func (p *NotifierPlugin) Init(ctx *core.SystemContext) error { return nil }
func (p *NotifierPlugin) Start(ctx *core.SystemContext) error { return nil }
func (p *NotifierPlugin) Stop(ctx *core.SystemContext) error { return nil }
// 对外暴露的强类型方法
func (p *NotifierPlugin) SendAlert(message string) {
// 实际场景这里可能是调用 SMTP 或企业微信 webhook
fmt.Printf("[Notifier] 📧 发送告警邮件: %s\n", message)
}负责产生数据,并抛出事件。它完全不知道谁会去处理这些日志。
package plugin_collector
import (
"fmt"
"math/rand"
"time"
"github.com/yeswearebot/yes-core/core"
)
type CollectorPlugin struct {
stopChan chan struct{}
}
func init() {
core.Register(func() core.Plugin { return &CollectorPlugin{} })
}
func (p *CollectorPlugin) Name() string { return "collector" }
func (p *CollectorPlugin) DependsOn() []string { return nil }
func (p *CollectorPlugin) Init(ctx *core.SystemContext) error {
p.stopChan = make(chan struct{})
return nil
}
func (p *CollectorPlugin) Start(ctx *core.SystemContext) error {
go func() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
levels := []string{"INFO", "WARN", "ERROR"}
for {
select {
case <-ticker.C:
log := fmt.Sprintf("系统发生事件 #%d", rand.Intn(1000))
level := levels[rand.Intn(len(levels))]
// 将原始日志通过事件总线广播
payload := map[string]string{
"level": level,
"content": log,
}
ctx.Events.Publish("log.raw", payload)
case <-p.stopChan:
return
}
}
}()
return nil
}
func (p *CollectorPlugin) Stop(ctx *core.SystemContext) error {
close(p.stopChan)
return nil
}这个插件需要监听日志,并且在遇到 ERROR 时调用 Notifier 插件。因此它依赖 notifier。
package plugin_alerter
import (
"github.com/yeswearebot/yes-core/core"
"plugin_notifier" // 引入 notifier 包以进行类型断言
)
type AlerterPlugin struct{}
func init() {
core.Register(func() core.Plugin { return &AlerterPlugin{} })
}
func (p *AlerterPlugin) Name() string { return "alerter" }
// 声明依赖:内核会保证 notifier 先启动
func (p *AlerterPlugin) DependsOn() []string { return []string{"notifier"} }
func (p *AlerterPlugin) Init(ctx *core.SystemContext) error {
// 订阅原始日志事件
ctx.Events.Subscribe("log.raw", func(payload any) {
data, ok := payload.(map[string]string)
if !ok {
return
}
if data["level"] == "ERROR" {
// 遇到错误,通过依赖注入获取 Notifier 实例并调用
if rawNotifier, ok := ctx.Registry.Get("notifier"); ok {
notifier := rawNotifier.(*plugin_notifier.NotifierPlugin)
notifier.SendAlert("严重错误: " + data["content"])
}
}
})
return nil
}
func (p *AlerterPlugin) Start(ctx *core.SystemContext) error { return nil }
func (p *AlerterPlugin) Stop(ctx *core.SystemContext) error { return nil }依然是极度清爽的组装方式:
package main
import (
"github.com/yeswearebot/yes-core/core"
_ "plugin_notifier"
_ "plugin_collector"
_ "plugin_alerter"
)
func main() {
app := core.NewApp()
if err := app.Run(); err != nil {
panic(err)
}
}内核会自动按照 notifier -> collector / alerter 的顺序初始化它们。collector 每 2 秒产生日志,一旦产生 ERROR,alerter 会立即响应并调用 notifier 打印告警。
只要遵循“事件发布/订阅”和“服务注册/发现”的模式,yes-core 能做到的事情远超想象:
-
游戏服务端模组系统:写一个
adapter-rcon插件连接游戏,写一个economy-plugin处理金币。不想用经济系统了?直接在main.go删掉一行匿名导入,游戏完全不受影响。 -
爬虫与数据清洗流水线:
fetcher插件拉取网页 -> 发布html.raw事件 ->parser插件解析数据 -> 发布data.cleaned事件 ->storage插件监听并存入数据库。各司其职,随时可替换某个环节的实现。 -
IoT 智能家居中控:
adapter-mqtt监听传感器数据 -> 规则引擎插件订阅数据 -> 当温度过高时,通过依赖注入调用adapter-switch插件打开空调。
通过上面两个示例,我们可以清晰地看到 yes-core 带来的架构红利:
- 避免main.go灾难:所有的初始化和连接逻辑都被封装在插件内部,main.go 永远只负责“导入”。
- 优雅的生命周期:无论是 Goroutine 的退出,还是 HTTP Server 的关闭,都被统一纳入 Stop() 管理,告别 Go 语言中常见的“协程泄露”和“强制退出导致数据丢失”。
- 测试极度友好:因为插件间是松耦合的,你可以单独写一个测试用的 mock-notifier 插件,在测试环境替换掉真实的发邮件插件,对业务逻辑进行完美的单元测试。 yes-core 并不是一个为你写好所有业务的“脚手架”,而是一套架构的语法规则。它约束了模块之间沟通的方式(事件总线+依赖注入),使得多人协作、代码维护、系统扩展变得前所未有的可预测和可控。当你习惯了这种思维方式,你会发现自己再也回不去那种把所有逻辑揉在一个 main.go 里的开发模式了。