feat: 初始化多机器人服务端项目框架

基于Go语言构建多机器人服务端框架,包含配置管理、事件总线、依赖注入等核心模块
添加项目基础结构、README、gitignore和初始代码实现
This commit is contained in:
2026-01-04 21:19:17 +08:00
commit ac0dfb64c9
22 changed files with 2385 additions and 0 deletions

143
internal/config/config.go Normal file
View File

@@ -0,0 +1,143 @@
package config
import (
"fmt"
"sync"
"github.com/BurntSushi/toml"
"github.com/fsnotify/fsnotify"
"go.uber.org/zap"
)
// Config 应用配置结构
type Config struct {
Server ServerConfig `toml:"server"`
Log LogConfig `toml:"log"`
Protocol ProtocolConfig `toml:"protocol"`
}
// ServerConfig 服务器配置
type ServerConfig struct {
Host string `toml:"host"`
Port int `toml:"port"`
}
// LogConfig 日志配置
type LogConfig struct {
Level string `toml:"level"`
Output string `toml:"output"`
MaxSize int `toml:"max_size"`
MaxBackups int `toml:"max_backups"`
MaxAge int `toml:"max_age"`
}
// ProtocolConfig 协议配置
type ProtocolConfig struct {
Name string `toml:"name"`
Version string `toml:"version"`
Options map[string]string `toml:"options"`
}
// ConfigManager 配置管理器
type ConfigManager struct {
configPath string
config *Config
logger *zap.Logger
mu sync.RWMutex
callbacks []func(*Config)
}
// NewConfigManager 创建配置管理器
func NewConfigManager(configPath string, logger *zap.Logger) *ConfigManager {
return &ConfigManager{
configPath: configPath,
logger: logger,
callbacks: make([]func(*Config), 0),
}
}
// Load 加载配置文件
func (cm *ConfigManager) Load() error {
cm.mu.Lock()
defer cm.mu.Unlock()
var cfg Config
if _, err := toml.DecodeFile(cm.configPath, &cfg); err != nil {
return fmt.Errorf("failed to decode config: %w", err)
}
cm.config = &cfg
cm.logger.Info("Config loaded successfully",
zap.String("path", cm.configPath),
zap.String("server", fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)),
)
// 触发回调
for _, cb := range cm.callbacks {
cb(cm.config)
}
return nil
}
// Get 获取当前配置
func (cm *ConfigManager) Get() *Config {
cm.mu.RLock()
defer cm.mu.RUnlock()
return cm.config
}
// Reload 重新加载配置
func (cm *ConfigManager) Reload() error {
return cm.Load()
}
// RegisterCallback 注册配置变更回调
func (cm *ConfigManager) RegisterCallback(callback func(*Config)) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.callbacks = append(cm.callbacks, callback)
}
// Watch 监听配置文件变化
func (cm *ConfigManager) Watch() error {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return fmt.Errorf("failed to create watcher: %w", err)
}
if err := watcher.Add(cm.configPath); err != nil {
return fmt.Errorf("failed to watch config file: %w", err)
}
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
if event.Op&fsnotify.Write == fsnotify.Write {
cm.logger.Info("Config file changed, reloading...",
zap.String("file", event.Name))
if err := cm.Reload(); err != nil {
cm.logger.Error("Failed to reload config",
zap.Error(err))
}
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
cm.logger.Error("Watcher error", zap.Error(err))
}
}
}()
return nil
}
// Close 关闭配置管理器
func (cm *ConfigManager) Close() error {
return nil
}

View File

@@ -0,0 +1,113 @@
package config
import (
"os"
"path/filepath"
"testing"
"go.uber.org/zap"
)
func TestConfigManager_Load(t *testing.T) {
// 创建临时配置文件
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "test_config.toml")
configContent := `
[server]
host = "127.0.0.1"
port = 8080
[log]
level = "debug"
output = "stdout"
max_size = 100
max_backups = 3
max_age = 7
[protocol]
name = "test"
version = "1.0"
[protocol.options]
key = "value"
`
err := os.WriteFile(configPath, []byte(configContent), 0644)
if err != nil {
t.Fatalf("Failed to create config file: %v", err)
}
logger := zap.NewNop()
cm := NewConfigManager(configPath, logger)
err = cm.Load()
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}
cfg := cm.Get()
if cfg == nil {
t.Fatal("Config is nil")
}
if cfg.Server.Host != "127.0.0.1" {
t.Errorf("Expected host '127.0.0.1', got '%s'", cfg.Server.Host)
}
if cfg.Server.Port != 8080 {
t.Errorf("Expected port 8080, got %d", cfg.Server.Port)
}
if cfg.Log.Level != "debug" {
t.Errorf("Expected log level 'debug', got '%s'", cfg.Log.Level)
}
if cfg.Protocol.Name != "test" {
t.Errorf("Expected protocol name 'test', got '%s'", cfg.Protocol.Name)
}
}
func TestInitLogger(t *testing.T) {
tests := []struct {
name string
cfg *LogConfig
wantErr bool
}{
{
name: "stdout logger",
cfg: &LogConfig{
Level: "info",
Output: "stdout",
},
wantErr: false,
},
{
name: "stderr logger",
cfg: &LogConfig{
Level: "error",
Output: "stderr",
},
wantErr: false,
},
{
name: "file logger",
cfg: &LogConfig{
Level: "debug",
Output: filepath.Join(t.TempDir(), "test.log"),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
logger, err := InitLogger(tt.cfg)
if (err != nil) != tt.wantErr {
t.Errorf("InitLogger() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && logger == nil {
t.Error("Expected non-nil logger")
}
})
}
}

69
internal/config/logger.go Normal file
View File

@@ -0,0 +1,69 @@
package config
import (
"fmt"
"os"
"path/filepath"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// InitLogger 初始化日志
func InitLogger(cfg *LogConfig) (*zap.Logger, error) {
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
var writer zapcore.WriteSyncer
switch cfg.Output {
case "stdout":
writer = zapcore.AddSync(os.Stdout)
case "stderr":
writer = zapcore.AddSync(os.Stderr)
default:
// 创建日志目录
if err := os.MkdirAll(filepath.Dir(cfg.Output), 0755); err != nil {
return nil, fmt.Errorf("failed to create log directory: %w", err)
}
file, err := os.OpenFile(cfg.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %w", err)
}
writer = zapcore.AddSync(file)
}
// 解析日志级别
var level zapcore.Level
switch cfg.Level {
case "debug":
level = zapcore.DebugLevel
case "info":
level = zapcore.InfoLevel
case "warn":
level = zapcore.WarnLevel
case "error":
level = zapcore.ErrorLevel
default:
level = zapcore.InfoLevel
}
core := zapcore.NewCore(
zapcore.NewJSONEncoder(encoderConfig),
writer,
level,
)
logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
return logger, nil
}