Skip to content

Beyond Bot

MarkChai edited this page Jul 16, 2026 · 3 revisions

超越机器人:yes-core 的通用架构发散

"A very tiny framework for any extendable event-driven system."

虽然 yes-core 最初的诞生是为了解决复杂的多平台机器人适配问题,但当我们剥开“机器人”这层业务外衣,会发现它的本质是一个极度纯粹的、基于事件驱动和依赖注入的微内核。 在 yes-core 的视角里,没有 QQ、没有消息段、没有群聊。它的世界里只有:

  1. 插件:负责干活的结构体。
  2. 生命周期Init -> Start -> Stop 的标准流转。
  3. 事件总线PublishSubscribe 的异步解耦广播。
  4. 注册中心:基于 DependsOn 拓扑排序的强类型依赖注入。 这意味着,任何需要模块化、插件化、且模块间需要相互通信的 Go 并发应用,都可以直接拿 yes-core 当底座,而且极度方便。 下面我们通过一个严谨的示例,展示如何用 yes-core 在几分钟内搓出一个“与机器人毫无关系”的系统。

示例:插件化 Web API 与定时调度系统

在这个示例中,我们将看到 yes-core 如何优雅地管理 HTTP Server 的生命周期,并将其与业务逻辑彻底解耦。

1. 核心 API 插件

这是系统的心脏,提供具体的业务能力。它不关心是谁调用了它(是 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
}

2. Web Server 插件

这个插件负责提供 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()
}

3. 定时调度插件

这个插件负责定时触发任务,它同样依赖 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
}

4. 启动你的复合系统

你的 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 会做如下调度:

  1. 检测到 web-servercron-scheduler 都依赖 api-service,于是先初始化并启动 api-service
  2. 启动 web-server,监听 8080 端口。
  3. 启动 cron-scheduler,开启 10 秒的定时器。 此时,你可以用浏览器访问 http://localhost:8080/trigger?task=manual,会看到返回了执行结果;同时,控制台每 10 秒会打印一次定时任务的执行记录。当你按下 Ctrl+C 时,内核会依次调用 Stop(),HTTP 服务会优雅关闭,定时器会停止退出,整个进程干净利落地结束。

示例:构建一个分布式日志处理与告警流水线

假设我们有这样一个需求:

  • 采集节点:不断生成原始日志。
  • 清洗节点:接收原始日志,提取级别和内容,转为标准格式。
  • 告警节点:如果日志级别是 ERROR,触发报警机制(如调用外部接口)。
  • 通知节点:提供统一的发邮件/发微信能力,供告警节点调用。 使用 yes-core,我们不需要写复杂的 main.go 逻辑去拼凑这些组件,只需要把它们全部变成平级插件,让内核来组装。

1. 通知服务插件

这是一个底层基础插件,不依赖任何其他插件,只提供服务。

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)
}

2. 日志采集插件

负责产生数据,并抛出事件。它完全不知道谁会去处理这些日志。

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
}

3. 告警处理插件

这个插件需要监听日志,并且在遇到 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 }

4. 像搭积木一样启动系统

依然是极度清爽的组装方式:

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 秒产生日志,一旦产生 ERRORalerter 会立即响应并调用 notifier 打印告警。


思维发散:还能做什么?

只要遵循“事件发布/订阅”和“服务注册/发现”的模式,yes-core 能做到的事情远超想象:

  1. 游戏服务端模组系统:写一个 adapter-rcon 插件连接游戏,写一个 economy-plugin 处理金币。不想用经济系统了?直接在 main.go 删掉一行匿名导入,游戏完全不受影响。
  2. 爬虫与数据清洗流水线fetcher 插件拉取网页 -> 发布 html.raw 事件 -> parser 插件解析数据 -> 发布 data.cleaned 事件 -> storage 插件监听并存入数据库。各司其职,随时可替换某个环节的实现。
  3. IoT 智能家居中控adapter-mqtt 监听传感器数据 -> 规则引擎插件订阅数据 -> 当温度过高时,通过依赖注入调用 adapter-switch 插件打开空调。

架构优势总结

通过上面两个示例,我们可以清晰地看到 yes-core 带来的架构红利:

优势 说明 对比传统模式
避免 main.go 灾难 所有初始化和连接逻辑封装在插件内部,main.go 只负责“导入” 传统模式:main.go 充斥大量初始化代码,难以维护
优雅的生命周期管理 无论是 Goroutine 退出还是 HTTP Server 关闭,都统一纳入 Stop() 管理 传统模式:协程泄露、强制退出导致数据丢失
测试极度友好 插件间松耦合,可轻松编写 mock 插件进行单元测试 传统模式:模块高度耦合,难以隔离测试
插件化与可扩展性 新功能即新插件,通过依赖声明自动组装,无需修改核心代码 传统模式:添加功能需修改大量现有代码
团队协作友好 插件接口清晰,不同团队可并行开发不同插件,通过事件总线协作 传统模式:代码冲突频繁,集成困难

yes-core 并不是一个为你写好所有业务的“脚手架”,而是一套架构的语法规则。它约束了模块之间沟通的方式(事件总线+依赖注入),使得多人协作、代码维护、系统扩展变得前所未有的可预测和可控。当你习惯了这种思维方式,你会发现自己再也回不去那种把所有逻辑揉在一个 main.go 里的开发模式了。