From f3220a238191d0f1300dd4e56557dfd90c85c1b3 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Wed, 18 Mar 2026 01:22:49 +0800 Subject: [PATCH] refactor: modernize codebase for Go 1.26 and fix adapter issues - Upgrade Go version from 1.24 to 1.26.1 with updated dependencies - Replace interface{} with any type throughout codebase - Add message deduplication in OneBot11 adapter to prevent duplicate event processing - URL-encode access token in WebSocket connections to handle special characters - Remove duplicate bot startup hooks in DI providers (now handled by botManager.StartAll) - Use slices.Clone and errgroup.Go patterns for cleaner concurrent code - Apply omitzero JSON tags for optional fields (Go 1.24+ feature) --- .kilocode/skills/golang-patterns/SKILL.md | 674 ++++++++++++++++++++ .kilocode/skills/golang-testing/SKILL.md | 720 ++++++++++++++++++++++ .kilocode/skills/use-modern-go/SKILL.md | 291 +++++++++ configs/config.toml | 6 +- go.mod | 16 +- go.sum | 24 +- internal/adapter/milky/adapter.go | 8 +- internal/adapter/milky/api_client.go | 4 +- internal/adapter/milky/bot.go | 42 +- internal/adapter/milky/event.go | 38 +- internal/adapter/milky/types.go | 24 +- internal/adapter/onebot11/action.go | 50 +- internal/adapter/onebot11/adapter.go | 94 ++- internal/adapter/onebot11/client.go | 4 +- internal/adapter/onebot11/event.go | 40 +- internal/adapter/onebot11/message.go | 32 +- internal/adapter/onebot11/types.go | 70 +-- internal/di/providers.go | 34 +- internal/engine/dispatcher.go | 4 +- internal/engine/eventbus.go | 9 +- internal/engine/middleware.go | 6 +- internal/engine/plugin.go | 6 +- internal/engine/scheduler.go | 9 +- internal/plugins/mcstatus/mcstatus.go | 2 +- internal/plugins/mcstatus/ping.go | 12 +- internal/plugins/welcome/welcome.go | 8 +- internal/protocol/action.go | 26 +- internal/protocol/bot.go | 2 +- internal/protocol/event.go | 28 +- internal/protocol/interface.go | 2 +- internal/protocol/message.go | 20 +- plans/go-modernization-plan.md | 253 ++++++++ 32 files changed, 2262 insertions(+), 296 deletions(-) create mode 100644 .kilocode/skills/golang-patterns/SKILL.md create mode 100644 .kilocode/skills/golang-testing/SKILL.md create mode 100644 .kilocode/skills/use-modern-go/SKILL.md create mode 100644 plans/go-modernization-plan.md diff --git a/.kilocode/skills/golang-patterns/SKILL.md b/.kilocode/skills/golang-patterns/SKILL.md new file mode 100644 index 0000000..056ef56 --- /dev/null +++ b/.kilocode/skills/golang-patterns/SKILL.md @@ -0,0 +1,674 @@ +--- +name: golang-patterns +description: 用于构建健壮、高效且可维护的Go应用程序的惯用Go模式、最佳实践和约定。 +origin: ECC +--- + +# Go 开发模式 + +用于构建健壮、高效和可维护应用程序的惯用 Go 模式与最佳实践。 + +## 何时激活 + +* 编写新的 Go 代码时 +* 审查 Go 代码时 +* 重构现有 Go 代码时 +* 设计 Go 包/模块时 + +## 核心原则 + +### 1. 简洁与清晰 + +Go 推崇简洁而非精巧。代码应该显而易见且易于阅读。 + +```go +// Good: Clear and direct +func GetUser(id string) (*User, error) { + user, err := db.FindUser(id) + if err != nil { + return nil, fmt.Errorf("get user %s: %w", id, err) + } + return user, nil +} + +// Bad: Overly clever +func GetUser(id string) (*User, error) { + return func() (*User, error) { + if u, e := db.FindUser(id); e == nil { + return u, nil + } else { + return nil, e + } + }() +} +``` + +### 2. 让零值变得有用 + +设计类型时,应使其零值无需初始化即可立即使用。 + +```go +// Good: Zero value is useful +type Counter struct { + mu sync.Mutex + count int // zero value is 0, ready to use +} + +func (c *Counter) Inc() { + c.mu.Lock() + c.count++ + c.mu.Unlock() +} + +// Good: bytes.Buffer works with zero value +var buf bytes.Buffer +buf.WriteString("hello") + +// Bad: Requires initialization +type BadCounter struct { + counts map[string]int // nil map will panic +} +``` + +### 3. 接受接口,返回结构体 + +函数应该接受接口参数并返回具体类型。 + +```go +// Good: Accepts interface, returns concrete type +func ProcessData(r io.Reader) (*Result, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + return &Result{Data: data}, nil +} + +// Bad: Returns interface (hides implementation details unnecessarily) +func ProcessData(r io.Reader) (io.Reader, error) { + // ... +} +``` + +## 错误处理模式 + +### 带上下文的错误包装 + +```go +// Good: Wrap errors with context +func LoadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("load config %s: %w", path, err) + } + + var cfg Config + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parse config %s: %w", path, err) + } + + return &cfg, nil +} +``` + +### 自定义错误类型 + +```go +// Define domain-specific errors +type ValidationError struct { + Field string + Message string +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message) +} + +// Sentinel errors for common cases +var ( + ErrNotFound = errors.New("resource not found") + ErrUnauthorized = errors.New("unauthorized") + ErrInvalidInput = errors.New("invalid input") +) +``` + +### 使用 errors.Is 和 errors.As 检查错误 + +```go +func HandleError(err error) { + // Check for specific error + if errors.Is(err, sql.ErrNoRows) { + log.Println("No records found") + return + } + + // Check for error type + var validationErr *ValidationError + if errors.As(err, &validationErr) { + log.Printf("Validation error on field %s: %s", + validationErr.Field, validationErr.Message) + return + } + + // Unknown error + log.Printf("Unexpected error: %v", err) +} +``` + +### 永不忽略错误 + +```go +// Bad: Ignoring error with blank identifier +result, _ := doSomething() + +// Good: Handle or explicitly document why it's safe to ignore +result, err := doSomething() +if err != nil { + return err +} + +// Acceptable: When error truly doesn't matter (rare) +_ = writer.Close() // Best-effort cleanup, error logged elsewhere +``` + +## 并发模式 + +### 工作池 + +```go +func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) { + var wg sync.WaitGroup + + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for job := range jobs { + results <- process(job) + } + }() + } + + wg.Wait() + close(results) +} +``` + +### 用于取消和超时的 Context + +```go +func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("create request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("fetch %s: %w", url, err) + } + defer resp.Body.Close() + + return io.ReadAll(resp.Body) +} +``` + +### 优雅关闭 + +```go +func GracefulShutdown(server *http.Server) { + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + <-quit + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + + log.Println("Server exited") +} +``` + +### 用于协调 Goroutine 的 errgroup + +```go +import "golang.org/x/sync/errgroup" + +func FetchAll(ctx context.Context, urls []string) ([][]byte, error) { + g, ctx := errgroup.WithContext(ctx) + results := make([][]byte, len(urls)) + + for i, url := range urls { + i, url := i, url // Capture loop variables + g.Go(func() error { + data, err := FetchWithTimeout(ctx, url) + if err != nil { + return err + } + results[i] = data + return nil + }) + } + + if err := g.Wait(); err != nil { + return nil, err + } + return results, nil +} +``` + +### 避免 Goroutine 泄漏 + +```go +// Bad: Goroutine leak if context is cancelled +func leakyFetch(ctx context.Context, url string) <-chan []byte { + ch := make(chan []byte) + go func() { + data, _ := fetch(url) + ch <- data // Blocks forever if no receiver + }() + return ch +} + +// Good: Properly handles cancellation +func safeFetch(ctx context.Context, url string) <-chan []byte { + ch := make(chan []byte, 1) // Buffered channel + go func() { + data, err := fetch(url) + if err != nil { + return + } + select { + case ch <- data: + case <-ctx.Done(): + } + }() + return ch +} +``` + +## 接口设计 + +### 小而专注的接口 + +```go +// Good: Single-method interfaces +type Reader interface { + Read(p []byte) (n int, err error) +} + +type Writer interface { + Write(p []byte) (n int, err error) +} + +type Closer interface { + Close() error +} + +// Compose interfaces as needed +type ReadWriteCloser interface { + Reader + Writer + Closer +} +``` + +### 在接口使用处定义接口 + +```go +// In the consumer package, not the provider +package service + +// UserStore defines what this service needs +type UserStore interface { + GetUser(id string) (*User, error) + SaveUser(user *User) error +} + +type Service struct { + store UserStore +} + +// Concrete implementation can be in another package +// It doesn't need to know about this interface +``` + +### 使用类型断言实现可选行为 + +```go +type Flusher interface { + Flush() error +} + +func WriteAndFlush(w io.Writer, data []byte) error { + if _, err := w.Write(data); err != nil { + return err + } + + // Flush if supported + if f, ok := w.(Flusher); ok { + return f.Flush() + } + return nil +} +``` + +## 包组织 + +### 标准项目布局 + +```text +myproject/ +├── cmd/ +│ └── myapp/ +│ └── main.go # Entry point +├── internal/ +│ ├── handler/ # HTTP handlers +│ ├── service/ # Business logic +│ ├── repository/ # Data access +│ └── config/ # Configuration +├── pkg/ +│ └── client/ # Public API client +├── api/ +│ └── v1/ # API definitions (proto, OpenAPI) +├── testdata/ # Test fixtures +├── go.mod +├── go.sum +└── Makefile +``` + +### 包命名 + +```go +// Good: Short, lowercase, no underscores +package http +package json +package user + +// Bad: Verbose, mixed case, or redundant +package httpHandler +package json_parser +package userService // Redundant 'Service' suffix +``` + +### 避免包级状态 + +```go +// Bad: Global mutable state +var db *sql.DB + +func init() { + db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL")) +} + +// Good: Dependency injection +type Server struct { + db *sql.DB +} + +func NewServer(db *sql.DB) *Server { + return &Server{db: db} +} +``` + +## 结构体设计 + +### 函数式选项模式 + +```go +type Server struct { + addr string + timeout time.Duration + logger *log.Logger +} + +type Option func(*Server) + +func WithTimeout(d time.Duration) Option { + return func(s *Server) { + s.timeout = d + } +} + +func WithLogger(l *log.Logger) Option { + return func(s *Server) { + s.logger = l + } +} + +func NewServer(addr string, opts ...Option) *Server { + s := &Server{ + addr: addr, + timeout: 30 * time.Second, // default + logger: log.Default(), // default + } + for _, opt := range opts { + opt(s) + } + return s +} + +// Usage +server := NewServer(":8080", + WithTimeout(60*time.Second), + WithLogger(customLogger), +) +``` + +### 使用嵌入实现组合 + +```go +type Logger struct { + prefix string +} + +func (l *Logger) Log(msg string) { + fmt.Printf("[%s] %s\n", l.prefix, msg) +} + +type Server struct { + *Logger // Embedding - Server gets Log method + addr string +} + +func NewServer(addr string) *Server { + return &Server{ + Logger: &Logger{prefix: "SERVER"}, + addr: addr, + } +} + +// Usage +s := NewServer(":8080") +s.Log("Starting...") // Calls embedded Logger.Log +``` + +## 内存与性能 + +### 当大小已知时预分配切片 + +```go +// Bad: Grows slice multiple times +func processItems(items []Item) []Result { + var results []Result + for _, item := range items { + results = append(results, process(item)) + } + return results +} + +// Good: Single allocation +func processItems(items []Item) []Result { + results := make([]Result, 0, len(items)) + for _, item := range items { + results = append(results, process(item)) + } + return results +} +``` + +### 为频繁分配使用 sync.Pool + +```go +var bufferPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +func ProcessRequest(data []byte) []byte { + buf := bufferPool.Get().(*bytes.Buffer) + defer func() { + buf.Reset() + bufferPool.Put(buf) + }() + + buf.Write(data) + // Process... + return buf.Bytes() +} +``` + +### 避免在循环中进行字符串拼接 + +```go +// Bad: Creates many string allocations +func join(parts []string) string { + var result string + for _, p := range parts { + result += p + "," + } + return result +} + +// Good: Single allocation with strings.Builder +func join(parts []string) string { + var sb strings.Builder + for i, p := range parts { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(p) + } + return sb.String() +} + +// Best: Use standard library +func join(parts []string) string { + return strings.Join(parts, ",") +} +``` + +## Go 工具集成 + +### 基本命令 + +```bash +# Build and run +go build ./... +go run ./cmd/myapp + +# Testing +go test ./... +go test -race ./... +go test -cover ./... + +# Static analysis +go vet ./... +staticcheck ./... +golangci-lint run + +# Module management +go mod tidy +go mod verify + +# Formatting +gofmt -w . +goimports -w . +``` + +### 推荐的 Linter 配置 (.golangci.yml) + +```yaml +linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - gofmt + - goimports + - misspell + - unconvert + - unparam + +linters-settings: + errcheck: + check-type-assertions: true + govet: + check-shadowing: true + +issues: + exclude-use-default: false +``` + +## 快速参考:Go 惯用法 + +| 惯用法 | 描述 | +|-------|-------------| +| 接受接口,返回结构体 | 函数接受接口参数,返回具体类型 | +| 错误即值 | 将错误视为一等值,而非异常 | +| 不要通过共享内存来通信 | 使用通道在 goroutine 之间进行协调 | +| 让零值变得有用 | 类型应无需显式初始化即可工作 | +| 少量复制优于少量依赖 | 避免不必要的外部依赖 | +| 清晰优于精巧 | 优先考虑可读性而非精巧性 | +| gofmt 虽非最爱,但却是每个人的朋友 | 始终使用 gofmt/goimports 格式化代码 | +| 提前返回 | 先处理错误,保持主逻辑路径无缩进 | + +## 应避免的反模式 + +```go +// Bad: Naked returns in long functions +func process() (result int, err error) { + // ... 50 lines ... + return // What is being returned? +} + +// Bad: Using panic for control flow +func GetUser(id string) *User { + user, err := db.Find(id) + if err != nil { + panic(err) // Don't do this + } + return user +} + +// Bad: Passing context in struct +type Request struct { + ctx context.Context // Context should be first param + ID string +} + +// Good: Context as first parameter +func ProcessRequest(ctx context.Context, id string) error { + // ... +} + +// Bad: Mixing value and pointer receivers +type Counter struct{ n int } +func (c Counter) Value() int { return c.n } // Value receiver +func (c *Counter) Increment() { c.n++ } // Pointer receiver +// Pick one style and be consistent +``` + +**记住**:Go 代码应该以最好的方式显得“乏味”——可预测、一致且易于理解。如有疑问,保持简单。 diff --git a/.kilocode/skills/golang-testing/SKILL.md b/.kilocode/skills/golang-testing/SKILL.md new file mode 100644 index 0000000..f1824ec --- /dev/null +++ b/.kilocode/skills/golang-testing/SKILL.md @@ -0,0 +1,720 @@ +--- +name: golang-testing +description: Go testing patterns including table-driven tests, subtests, benchmarks, fuzzing, and test coverage. Follows TDD methodology with idiomatic Go practices. +origin: ECC +--- + +# Go Testing Patterns + +Comprehensive Go testing patterns for writing reliable, maintainable tests following TDD methodology. + +## When to Activate + +- Writing new Go functions or methods +- Adding test coverage to existing code +- Creating benchmarks for performance-critical code +- Implementing fuzz tests for input validation +- Following TDD workflow in Go projects + +## TDD Workflow for Go + +### The RED-GREEN-REFACTOR Cycle + +``` +RED → Write a failing test first +GREEN → Write minimal code to pass the test +REFACTOR → Improve code while keeping tests green +REPEAT → Continue with next requirement +``` + +### Step-by-Step TDD in Go + +```go +// Step 1: Define the interface/signature +// calculator.go +package calculator + +func Add(a, b int) int { + panic("not implemented") // Placeholder +} + +// Step 2: Write failing test (RED) +// calculator_test.go +package calculator + +import "testing" + +func TestAdd(t *testing.T) { + got := Add(2, 3) + want := 5 + if got != want { + t.Errorf("Add(2, 3) = %d; want %d", got, want) + } +} + +// Step 3: Run test - verify FAIL +// $ go test +// --- FAIL: TestAdd (0.00s) +// panic: not implemented + +// Step 4: Implement minimal code (GREEN) +func Add(a, b int) int { + return a + b +} + +// Step 5: Run test - verify PASS +// $ go test +// PASS + +// Step 6: Refactor if needed, verify tests still pass +``` + +## Table-Driven Tests + +The standard pattern for Go tests. Enables comprehensive coverage with minimal code. + +```go +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + expected int + }{ + {"positive numbers", 2, 3, 5}, + {"negative numbers", -1, -2, -3}, + {"zero values", 0, 0, 0}, + {"mixed signs", -1, 1, 0}, + {"large numbers", 1000000, 2000000, 3000000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Add(tt.a, tt.b) + if got != tt.expected { + t.Errorf("Add(%d, %d) = %d; want %d", + tt.a, tt.b, got, tt.expected) + } + }) + } +} +``` + +### Table-Driven Tests with Error Cases + +```go +func TestParseConfig(t *testing.T) { + tests := []struct { + name string + input string + want *Config + wantErr bool + }{ + { + name: "valid config", + input: `{"host": "localhost", "port": 8080}`, + want: &Config{Host: "localhost", Port: 8080}, + }, + { + name: "invalid JSON", + input: `{invalid}`, + wantErr: true, + }, + { + name: "empty input", + input: "", + wantErr: true, + }, + { + name: "minimal config", + input: `{}`, + want: &Config{}, // Zero value config + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseConfig(tt.input) + + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("got %+v; want %+v", got, tt.want) + } + }) + } +} +``` + +## Subtests and Sub-benchmarks + +### Organizing Related Tests + +```go +func TestUser(t *testing.T) { + // Setup shared by all subtests + db := setupTestDB(t) + + t.Run("Create", func(t *testing.T) { + user := &User{Name: "Alice"} + err := db.CreateUser(user) + if err != nil { + t.Fatalf("CreateUser failed: %v", err) + } + if user.ID == "" { + t.Error("expected user ID to be set") + } + }) + + t.Run("Get", func(t *testing.T) { + user, err := db.GetUser("alice-id") + if err != nil { + t.Fatalf("GetUser failed: %v", err) + } + if user.Name != "Alice" { + t.Errorf("got name %q; want %q", user.Name, "Alice") + } + }) + + t.Run("Update", func(t *testing.T) { + // ... + }) + + t.Run("Delete", func(t *testing.T) { + // ... + }) +} +``` + +### Parallel Subtests + +```go +func TestParallel(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"case1", "input1"}, + {"case2", "input2"}, + {"case3", "input3"}, + } + + for _, tt := range tests { + tt := tt // Capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() // Run subtests in parallel + result := Process(tt.input) + // assertions... + _ = result + }) + } +} +``` + +## Test Helpers + +### Helper Functions + +```go +func setupTestDB(t *testing.T) *sql.DB { + t.Helper() // Marks this as a helper function + + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + + // Cleanup when test finishes + t.Cleanup(func() { + db.Close() + }) + + // Run migrations + if _, err := db.Exec(schema); err != nil { + t.Fatalf("failed to create schema: %v", err) + } + + return db +} + +func assertNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func assertEqual[T comparable](t *testing.T, got, want T) { + t.Helper() + if got != want { + t.Errorf("got %v; want %v", got, want) + } +} +``` + +### Temporary Files and Directories + +```go +func TestFileProcessing(t *testing.T) { + // Create temp directory - automatically cleaned up + tmpDir := t.TempDir() + + // Create test file + testFile := filepath.Join(tmpDir, "test.txt") + err := os.WriteFile(testFile, []byte("test content"), 0644) + if err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + // Run test + result, err := ProcessFile(testFile) + if err != nil { + t.Fatalf("ProcessFile failed: %v", err) + } + + // Assert... + _ = result +} +``` + +## Golden Files + +Testing against expected output files stored in `testdata/`. + +```go +var update = flag.Bool("update", false, "update golden files") + +func TestRender(t *testing.T) { + tests := []struct { + name string + input Template + }{ + {"simple", Template{Name: "test"}}, + {"complex", Template{Name: "test", Items: []string{"a", "b"}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Render(tt.input) + + golden := filepath.Join("testdata", tt.name+".golden") + + if *update { + // Update golden file: go test -update + err := os.WriteFile(golden, got, 0644) + if err != nil { + t.Fatalf("failed to update golden file: %v", err) + } + } + + want, err := os.ReadFile(golden) + if err != nil { + t.Fatalf("failed to read golden file: %v", err) + } + + if !bytes.Equal(got, want) { + t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want) + } + }) + } +} +``` + +## Mocking with Interfaces + +### Interface-Based Mocking + +```go +// Define interface for dependencies +type UserRepository interface { + GetUser(id string) (*User, error) + SaveUser(user *User) error +} + +// Production implementation +type PostgresUserRepository struct { + db *sql.DB +} + +func (r *PostgresUserRepository) GetUser(id string) (*User, error) { + // Real database query +} + +// Mock implementation for tests +type MockUserRepository struct { + GetUserFunc func(id string) (*User, error) + SaveUserFunc func(user *User) error +} + +func (m *MockUserRepository) GetUser(id string) (*User, error) { + return m.GetUserFunc(id) +} + +func (m *MockUserRepository) SaveUser(user *User) error { + return m.SaveUserFunc(user) +} + +// Test using mock +func TestUserService(t *testing.T) { + mock := &MockUserRepository{ + GetUserFunc: func(id string) (*User, error) { + if id == "123" { + return &User{ID: "123", Name: "Alice"}, nil + } + return nil, ErrNotFound + }, + } + + service := NewUserService(mock) + + user, err := service.GetUserProfile("123") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if user.Name != "Alice" { + t.Errorf("got name %q; want %q", user.Name, "Alice") + } +} +``` + +## Benchmarks + +### Basic Benchmarks + +```go +func BenchmarkProcess(b *testing.B) { + data := generateTestData(1000) + b.ResetTimer() // Don't count setup time + + for i := 0; i < b.N; i++ { + Process(data) + } +} + +// Run: go test -bench=BenchmarkProcess -benchmem +// Output: BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op +``` + +### Benchmark with Different Sizes + +```go +func BenchmarkSort(b *testing.B) { + sizes := []int{100, 1000, 10000, 100000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + data := generateRandomSlice(size) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + // Make a copy to avoid sorting already sorted data + tmp := make([]int, len(data)) + copy(tmp, data) + sort.Ints(tmp) + } + }) + } +} +``` + +### Memory Allocation Benchmarks + +```go +func BenchmarkStringConcat(b *testing.B) { + parts := []string{"hello", "world", "foo", "bar", "baz"} + + b.Run("plus", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var s string + for _, p := range parts { + s += p + } + _ = s + } + }) + + b.Run("builder", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var sb strings.Builder + for _, p := range parts { + sb.WriteString(p) + } + _ = sb.String() + } + }) + + b.Run("join", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = strings.Join(parts, "") + } + }) +} +``` + +## Fuzzing (Go 1.18+) + +### Basic Fuzz Test + +```go +func FuzzParseJSON(f *testing.F) { + // Add seed corpus + f.Add(`{"name": "test"}`) + f.Add(`{"count": 123}`) + f.Add(`[]`) + f.Add(`""`) + + f.Fuzz(func(t *testing.T, input string) { + var result map[string]interface{} + err := json.Unmarshal([]byte(input), &result) + + if err != nil { + // Invalid JSON is expected for random input + return + } + + // If parsing succeeded, re-encoding should work + _, err = json.Marshal(result) + if err != nil { + t.Errorf("Marshal failed after successful Unmarshal: %v", err) + } + }) +} + +// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s +``` + +### Fuzz Test with Multiple Inputs + +```go +func FuzzCompare(f *testing.F) { + f.Add("hello", "world") + f.Add("", "") + f.Add("abc", "abc") + + f.Fuzz(func(t *testing.T, a, b string) { + result := Compare(a, b) + + // Property: Compare(a, a) should always equal 0 + if a == b && result != 0 { + t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result) + } + + // Property: Compare(a, b) and Compare(b, a) should have opposite signs + reverse := Compare(b, a) + if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) { + if result != 0 || reverse != 0 { + t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent", + a, b, result, b, a, reverse) + } + } + }) +} +``` + +## Test Coverage + +### Running Coverage + +```bash +# Basic coverage +go test -cover ./... + +# Generate coverage profile +go test -coverprofile=coverage.out ./... + +# View coverage in browser +go tool cover -html=coverage.out + +# View coverage by function +go tool cover -func=coverage.out + +# Coverage with race detection +go test -race -coverprofile=coverage.out ./... +``` + +### Coverage Targets + +| Code Type | Target | +|-----------|--------| +| Critical business logic | 100% | +| Public APIs | 90%+ | +| General code | 80%+ | +| Generated code | Exclude | + +### Excluding Generated Code from Coverage + +```go +//go:generate mockgen -source=interface.go -destination=mock_interface.go + +// In coverage profile, exclude with build tags: +// go test -cover -tags=!generate ./... +``` + +## HTTP Handler Testing + +```go +func TestHealthHandler(t *testing.T) { + // Create request + req := httptest.NewRequest(http.MethodGet, "/health", nil) + w := httptest.NewRecorder() + + // Call handler + HealthHandler(w, req) + + // Check response + resp := w.Result() + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK) + } + + body, _ := io.ReadAll(resp.Body) + if string(body) != "OK" { + t.Errorf("got body %q; want %q", body, "OK") + } +} + +func TestAPIHandler(t *testing.T) { + tests := []struct { + name string + method string + path string + body string + wantStatus int + wantBody string + }{ + { + name: "get user", + method: http.MethodGet, + path: "/users/123", + wantStatus: http.StatusOK, + wantBody: `{"id":"123","name":"Alice"}`, + }, + { + name: "not found", + method: http.MethodGet, + path: "/users/999", + wantStatus: http.StatusNotFound, + }, + { + name: "create user", + method: http.MethodPost, + path: "/users", + body: `{"name":"Bob"}`, + wantStatus: http.StatusCreated, + }, + } + + handler := NewAPIHandler() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var body io.Reader + if tt.body != "" { + body = strings.NewReader(tt.body) + } + + req := httptest.NewRequest(tt.method, tt.path, body) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + + if w.Code != tt.wantStatus { + t.Errorf("got status %d; want %d", w.Code, tt.wantStatus) + } + + if tt.wantBody != "" && w.Body.String() != tt.wantBody { + t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody) + } + }) + } +} +``` + +## Testing Commands + +```bash +# Run all tests +go test ./... + +# Run tests with verbose output +go test -v ./... + +# Run specific test +go test -run TestAdd ./... + +# Run tests matching pattern +go test -run "TestUser/Create" ./... + +# Run tests with race detector +go test -race ./... + +# Run tests with coverage +go test -cover -coverprofile=coverage.out ./... + +# Run short tests only +go test -short ./... + +# Run tests with timeout +go test -timeout 30s ./... + +# Run benchmarks +go test -bench=. -benchmem ./... + +# Run fuzzing +go test -fuzz=FuzzParse -fuzztime=30s ./... + +# Count test runs (for flaky test detection) +go test -count=10 ./... +``` + +## Best Practices + +**DO:** +- Write tests FIRST (TDD) +- Use table-driven tests for comprehensive coverage +- Test behavior, not implementation +- Use `t.Helper()` in helper functions +- Use `t.Parallel()` for independent tests +- Clean up resources with `t.Cleanup()` +- Use meaningful test names that describe the scenario + +**DON'T:** +- Test private functions directly (test through public API) +- Use `time.Sleep()` in tests (use channels or conditions) +- Ignore flaky tests (fix or remove them) +- Mock everything (prefer integration tests when possible) +- Skip error path testing + +## Integration with CI/CD + +```yaml +# GitHub Actions example +test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Run tests + run: go test -race -coverprofile=coverage.out ./... + + - name: Check coverage + run: | + go tool cover -func=coverage.out | grep total | awk '{print $3}' | \ + awk -F'%' '{if ($1 < 80) exit 1}' +``` + +**Remember**: Tests are documentation. They show how your code is meant to be used. Write them clearly and keep them up to date. diff --git a/.kilocode/skills/use-modern-go/SKILL.md b/.kilocode/skills/use-modern-go/SKILL.md new file mode 100644 index 0000000..3755a1a --- /dev/null +++ b/.kilocode/skills/use-modern-go/SKILL.md @@ -0,0 +1,291 @@ +--- +name: use-modern-go +description: Apply modern Go syntax guidelines based on project's Go version. Use when user ask for modern Go code guidelines. +--- + +# Modern Go Guidelines + +## Detected Go Version + +!`grep -rh "^go " --include="go.mod" . 2>/dev/null | cut -d' ' -f2 | sort | uniq -c | sort -nr | head -1 | xargs | cut -d' ' -f2 | grep . || echo unknown` + +## How to Use This Skill + +DO NOT search for go.mod files or try to detect the version yourself. Use ONLY the version shown above. + +**If version detected (not "unknown"):** +- Say: "This project is using Go X.XX, so I’ll stick to modern Go best practices and freely use language features up to and including this version. If you’d prefer a different target version, just let me know." +- Do NOT list features, do NOT ask for confirmation + +**If version is "unknown":** +- Say: "Could not detect Go version in this repository" +- Use AskUserQuestion: "Which Go version should I target?" → [1.23] / [1.24] / [1.25] / [1.26] + +**When writing Go code**, use ALL features from this document up to the target version: +- Prefer modern built-ins and packages (`slices`, `maps`, `cmp`) over legacy patterns +- Never use features from newer Go versions than the target +- Never use outdated patterns when a modern alternative is available + +--- + +## Features by Go Version + +### Go 1.0+ + +- `time.Since`: `time.Since(start)` instead of `time.Now().Sub(start)` + +### Go 1.8+ + +- `time.Until`: `time.Until(deadline)` instead of `deadline.Sub(time.Now())` + +### Go 1.13+ + +- `errors.Is`: `errors.Is(err, target)` instead of `err == target` (works with wrapped errors) + +### Go 1.18+ + +- `any`: Use `any` instead of `interface{}` +- `bytes.Cut`: `before, after, found := bytes.Cut(b, sep)` instead of Index+slice +- `strings.Cut`: `before, after, found := strings.Cut(s, sep)` + +### Go 1.19+ + +- `fmt.Appendf`: `buf = fmt.Appendf(buf, "x=%d", x)` instead of `[]byte(fmt.Sprintf(...))` +- `atomic.Bool`/`atomic.Int64`/`atomic.Pointer[T]`: Type-safe atomics instead of `atomic.StoreInt32` + +```go +var flag atomic.Bool +flag.Store(true) +if flag.Load() { ... } + +var ptr atomic.Pointer[Config] +ptr.Store(cfg) +``` + +### Go 1.20+ + +- `strings.Clone`: `strings.Clone(s)` to copy string without sharing memory +- `bytes.Clone`: `bytes.Clone(b)` to copy byte slice +- `strings.CutPrefix/CutSuffix`: `if rest, ok := strings.CutPrefix(s, "pre:"); ok { ... }` +- `errors.Join`: `errors.Join(err1, err2)` to combine multiple errors +- `context.WithCancelCause`: `ctx, cancel := context.WithCancelCause(parent)` then `cancel(err)` +- `context.Cause`: `context.Cause(ctx)` to get the error that caused cancellation + +### Go 1.21+ + +**Built-ins:** +- `min`/`max`: `max(a, b)` instead of if/else comparisons +- `clear`: `clear(m)` to delete all map entries, `clear(s)` to zero slice elements + +**slices package:** +- `slices.Contains`: `slices.Contains(items, x)` instead of manual loops +- `slices.Index`: `slices.Index(items, x)` returns index (-1 if not found) +- `slices.IndexFunc`: `slices.IndexFunc(items, func(item T) bool { return item.ID == id })` +- `slices.SortFunc`: `slices.SortFunc(items, func(a, b T) int { return cmp.Compare(a.X, b.X) })` +- `slices.Sort`: `slices.Sort(items)` for ordered types +- `slices.Max`/`slices.Min`: `slices.Max(items)` instead of manual loop +- `slices.Reverse`: `slices.Reverse(items)` instead of manual swap loop +- `slices.Compact`: `slices.Compact(items)` removes consecutive duplicates in-place +- `slices.Clip`: `slices.Clip(s)` removes unused capacity +- `slices.Clone`: `slices.Clone(s)` creates a copy + +**maps package:** +- `maps.Clone`: `maps.Clone(m)` instead of manual map iteration +- `maps.Copy`: `maps.Copy(dst, src)` copies entries from src to dst +- `maps.DeleteFunc`: `maps.DeleteFunc(m, func(k K, v V) bool { return condition })` + +**sync package:** +- `sync.OnceFunc`: `f := sync.OnceFunc(func() { ... })` instead of `sync.Once` + wrapper +- `sync.OnceValue`: `getter := sync.OnceValue(func() T { return computeValue() })` + +**context package:** +- `context.AfterFunc`: `stop := context.AfterFunc(ctx, cleanup)` runs cleanup on cancellation +- `context.WithTimeoutCause`: `ctx, cancel := context.WithTimeoutCause(parent, d, err)` +- `context.WithDeadlineCause`: Similar with deadline instead of duration + +### Go 1.22+ + +**Loops:** +- `for i := range n`: `for i := range len(items)` instead of `for i := 0; i < len(items); i++` +- Loop variables are now safe to capture in goroutines (each iteration has its own copy) + +**cmp package:** +- `cmp.Or`: `cmp.Or(flag, env, config, "default")` returns first non-zero value + +```go +// Instead of: +name := os.Getenv("NAME") +if name == "" { + name = "default" +} +// Use: +name := cmp.Or(os.Getenv("NAME"), "default") +``` + +**reflect package:** +- `reflect.TypeFor`: `reflect.TypeFor[T]()` instead of `reflect.TypeOf((*T)(nil)).Elem()` + +**net/http:** +- Enhanced `http.ServeMux` patterns: `mux.HandleFunc("GET /api/{id}", handler)` with method and path params +- `r.PathValue("id")` to get path parameters + +### Go 1.23+ + +- `maps.Keys(m)` / `maps.Values(m)` return iterators +- `slices.Collect(iter)` not manual loop to build slice from iterator +- `slices.Sorted(iter)` to collect and sort in one step + +```go +keys := slices.Collect(maps.Keys(m)) // not: for k := range m { keys = append(keys, k) } +sortedKeys := slices.Sorted(maps.Keys(m)) // collect + sort +for k := range maps.Keys(m) { process(k) } // iterate directly +``` + +**time package** + +- `time.Tick`: Use `time.Tick` freely — as of Go 1.23, the garbage collector can recover unreferenced tickers, even if they haven't been stopped. The Stop method is no longer necessary to help the garbage collector. There is no longer any reason to prefer NewTicker when Tick will do. + +### Go 1.24+ + +- `t.Context()` not `context.WithCancel(context.Background())` in tests. + ALWAYS use t.Context() when a test function needs a context. + +Before: +```go +func TestFoo(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + result := doSomething(ctx) +} +``` +After: +```go +func TestFoo(t *testing.T) { + ctx := t.Context() + result := doSomething(ctx) +} +``` + +- `omitzero` not `omitempty` in JSON struct tags. + ALWAYS use omitzero for time.Duration, time.Time, structs, slices, maps. + +Before: +```go +type Config struct { + Timeout time.Duration `json:"timeout,omitempty"` // doesn't work for Duration! +} +``` +After: +```go +type Config struct { + Timeout time.Duration `json:"timeout,omitzero"` +} +``` + +- `b.Loop()` not `for i := 0; i < b.N; i++` in benchmarks. + ALWAYS use b.Loop() for the main loop in benchmark functions. + +Before: +```go +func BenchmarkFoo(b *testing.B) { + for i := 0; i < b.N; i++ { + doWork() + } +} +``` +After: +```go +func BenchmarkFoo(b *testing.B) { + for b.Loop() { + doWork() + } +} +``` + +- `strings.SplitSeq` not `strings.Split` when iterating. + ALWAYS use SplitSeq/FieldsSeq when iterating over split results in a for-range loop. + +Before: +```go +for _, part := range strings.Split(s, ",") { + process(part) +} +``` +After: +```go +for part := range strings.SplitSeq(s, ",") { + process(part) +} +``` +Also: `strings.FieldsSeq`, `bytes.SplitSeq`, `bytes.FieldsSeq`. + +### Go 1.25+ + +- `wg.Go(fn)` not `wg.Add(1)` + `go func() { defer wg.Done(); ... }()`. + ALWAYS use wg.Go() when spawning goroutines with sync.WaitGroup. + +Before: +```go +var wg sync.WaitGroup +for _, item := range items { + wg.Add(1) + go func() { + defer wg.Done() + process(item) + }() +} +wg.Wait() +``` +After: +```go +var wg sync.WaitGroup +for _, item := range items { + wg.Go(func() { + process(item) + }) +} +wg.Wait() +``` + +### Go 1.26+ + +- `new(val)` not `x := val; &x` — returns pointer to any value. + Go 1.26 extends new() to accept expressions, not just types. + Type is inferred: new(0) → *int, new("s") → *string, new(T{}) → *T. + DO NOT use `x := val; &x` pattern — always use new(val) directly. + DO NOT use redundant casts like new(int(0)) — just write new(0). + Common use case: struct fields with pointer types. + +Before: +```go +timeout := 30 +debug := true +cfg := Config{ + Timeout: &timeout, + Debug: &debug, +} +``` +After: +```go +cfg := Config{ + Timeout: new(30), // *int + Debug: new(true), // *bool +} +``` + +- `errors.AsType[T](err)` not `errors.As(err, &target)`. + ALWAYS use errors.AsType when checking if error matches a specific type. + +Before: +```go +var pathErr *os.PathError +if errors.As(err, &pathErr) { + handle(pathErr) +} +``` +After: +```go +if pathErr, ok := errors.AsType[*os.PathError](err); ok { + handle(pathErr) +} +``` diff --git a/configs/config.toml b/configs/config.toml index 2aae18c..f25194f 100644 --- a/configs/config.toml +++ b/configs/config.toml @@ -48,10 +48,10 @@ enabled = true [bots.onebot11] connection_type = "ws" -self_id = "123456789" +self_id = "3177182228" nickname = "TestBot" -ws_url = "ws://192.168.10.148:3001" -access_token = "hDeu66@_DDhgMf<9" +ws_url = "ws://127.0.0.1:3001" +access_token = "]W4cjp#9W3l:p4_r" timeout = 30 heartbeat = 30 reconnect_interval = 5 diff --git a/go.mod b/go.mod index b0440ff..5f3079f 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,10 @@ module cellbot -go 1.24.0 - -toolchain go1.24.2 +go 1.26.1 require ( github.com/BurntSushi/toml v1.3.2 - github.com/bytedance/sonic v1.14.2 + github.com/bytedance/sonic v1.15.0 github.com/fasthttp/websocket v1.5.12 github.com/fsnotify/fsnotify v1.9.0 github.com/glebarez/sqlite v1.11.0 @@ -45,18 +43,18 @@ require ( require ( github.com/andybalholm/brotli v1.1.1 // indirect - github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/bytedance/gopkg v0.1.4 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.17.11 // indirect - github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect go.uber.org/dig v1.17.0 // indirect go.uber.org/multierr v1.10.0 // indirect - golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect + golang.org/x/arch v0.25.0 // indirect golang.org/x/net v0.33.0 // indirect - golang.org/x/sys v0.34.0 // indirect + golang.org/x/sys v0.42.0 // indirect ) diff --git a/go.sum b/go.sum index 17a8c8c..69958c2 100644 --- a/go.sum +++ b/go.sum @@ -6,12 +6,12 @@ github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7X github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= -github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= -github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= -github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= -github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 h1:UQ4AU+BGti3Sy/aLU8KVseYKNALcX9UXY6DfpwQ6J8E= github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM= @@ -51,8 +51,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= -github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= -github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= @@ -97,14 +97,14 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= diff --git a/internal/adapter/milky/adapter.go b/internal/adapter/milky/adapter.go index 7923291..75572d9 100644 --- a/internal/adapter/milky/adapter.go +++ b/internal/adapter/milky/adapter.go @@ -225,7 +225,7 @@ func (a *Adapter) handleEvents(eventChan <-chan []byte) { } // SendAction 发送动作 -func (a *Adapter) SendAction(ctx context.Context, action protocol.Action) (map[string]interface{}, error) { +func (a *Adapter) SendAction(ctx context.Context, action protocol.Action) (map[string]any, error) { // 调用 API resp, err := a.apiClient.Call(ctx, string(action.GetType()), action.GetParams()) if err != nil { @@ -303,8 +303,8 @@ func (a *Adapter) IsConnected() bool { } // GetStats 获取统计信息 -func (a *Adapter) GetStats() map[string]interface{} { - stats := map[string]interface{}{ +func (a *Adapter) GetStats() map[string]any { + stats := map[string]any{ "protocol": "milky", "self_id": a.selfID, "event_mode": a.config.EventMode, @@ -320,7 +320,7 @@ func (a *Adapter) GetStats() map[string]interface{} { } // CallAPI 直接调用 API(提供给 Bot 使用) -func (a *Adapter) CallAPI(ctx context.Context, endpoint string, params map[string]interface{}) (*APIResponse, error) { +func (a *Adapter) CallAPI(ctx context.Context, endpoint string, params map[string]any) (*APIResponse, error) { return a.apiClient.Call(ctx, endpoint, params) } diff --git a/internal/adapter/milky/api_client.go b/internal/adapter/milky/api_client.go index 51faf0a..acfaff9 100644 --- a/internal/adapter/milky/api_client.go +++ b/internal/adapter/milky/api_client.go @@ -47,7 +47,7 @@ func NewAPIClient(baseURL, accessToken string, timeout time.Duration, retryCount // endpoint: API 端点名称(如 "send_private_message") // input: 输入参数(会被序列化为 JSON) // 返回: 响应数据和错误 -func (c *APIClient) Call(ctx context.Context, endpoint string, input interface{}) (*APIResponse, error) { +func (c *APIClient) Call(ctx context.Context, endpoint string, input any) (*APIResponse, error) { // 序列化输入参数 var inputData []byte var err error @@ -126,7 +126,7 @@ func (c *APIClient) doRequest(ctx context.Context, url string, inputData []byte) // CallWithoutRetry 调用 API(不重试) // 注意:pkg/net.HTTPClient 的通用请求已经内置了重试机制 // 这个方法保留是为了向后兼容,但仍然会使用底层的重试机制 -func (c *APIClient) CallWithoutRetry(ctx context.Context, endpoint string, input interface{}) (*APIResponse, error) { +func (c *APIClient) CallWithoutRetry(ctx context.Context, endpoint string, input any) (*APIResponse, error) { return c.Call(ctx, endpoint, input) } diff --git a/internal/adapter/milky/bot.go b/internal/adapter/milky/bot.go index 58e1d2d..6045597 100644 --- a/internal/adapter/milky/bot.go +++ b/internal/adapter/milky/bot.go @@ -105,7 +105,7 @@ func (b *Bot) Disconnect(ctx context.Context) error { } // SendAction 发送动作 -func (b *Bot) SendAction(ctx context.Context, action protocol.Action) (map[string]interface{}, error) { +func (b *Bot) SendAction(ctx context.Context, action protocol.Action) (map[string]any, error) { if b.status != protocol.BotStatusRunning { return nil, fmt.Errorf("bot is not running") } @@ -119,8 +119,8 @@ func (b *Bot) GetAdapter() *Adapter { } // GetInfo 获取机器人信息 -func (b *Bot) GetInfo() map[string]interface{} { - return map[string]interface{}{ +func (b *Bot) GetInfo() map[string]any { + return map[string]any{ "id": b.id, "protocol": "milky", "status": b.status, @@ -144,7 +144,7 @@ func (b *Bot) SetStatus(status protocol.BotStatus) { // SendPrivateMessage 发送私聊消息 func (b *Bot) SendPrivateMessage(ctx context.Context, userID int64, segments []OutgoingSegment) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "user_id": userID, "segments": segments, } @@ -153,7 +153,7 @@ func (b *Bot) SendPrivateMessage(ctx context.Context, userID int64, segments []O // SendGroupMessage 发送群消息 func (b *Bot) SendGroupMessage(ctx context.Context, groupID int64, segments []OutgoingSegment) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, "segments": segments, } @@ -162,7 +162,7 @@ func (b *Bot) SendGroupMessage(ctx context.Context, groupID int64, segments []Ou // SendTempMessage 发送临时消息 func (b *Bot) SendTempMessage(ctx context.Context, groupID, userID int64, segments []OutgoingSegment) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, "user_id": userID, "segments": segments, @@ -172,7 +172,7 @@ func (b *Bot) SendTempMessage(ctx context.Context, groupID, userID int64, segmen // RecallMessage 撤回消息 func (b *Bot) RecallMessage(ctx context.Context, messageScene string, peerID, messageSeq int64) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "message_scene": messageScene, "peer_id": peerID, "message_seq": messageSeq, @@ -192,7 +192,7 @@ func (b *Bot) GetGroupList(ctx context.Context) (*APIResponse, error) { // GetGroupMemberList 获取群成员列表 func (b *Bot) GetGroupMemberList(ctx context.Context, groupID int64) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, } return b.adapter.CallAPI(ctx, "get_group_member_list", params) @@ -200,7 +200,7 @@ func (b *Bot) GetGroupMemberList(ctx context.Context, groupID int64) (*APIRespon // GetGroupMemberInfo 获取群成员信息 func (b *Bot) GetGroupMemberInfo(ctx context.Context, groupID, userID int64) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, "user_id": userID, } @@ -209,7 +209,7 @@ func (b *Bot) GetGroupMemberInfo(ctx context.Context, groupID, userID int64) (*A // SetGroupAdmin 设置群管理员 func (b *Bot) SetGroupAdmin(ctx context.Context, groupID, userID int64, isSet bool) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, "user_id": userID, "is_set": isSet, @@ -219,7 +219,7 @@ func (b *Bot) SetGroupAdmin(ctx context.Context, groupID, userID int64, isSet bo // SetGroupCard 设置群名片 func (b *Bot) SetGroupCard(ctx context.Context, groupID, userID int64, card string) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, "user_id": userID, "card": card, @@ -229,7 +229,7 @@ func (b *Bot) SetGroupCard(ctx context.Context, groupID, userID int64, card stri // SetGroupName 设置群名 func (b *Bot) SetGroupName(ctx context.Context, groupID int64, groupName string) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, "group_name": groupName, } @@ -238,7 +238,7 @@ func (b *Bot) SetGroupName(ctx context.Context, groupID int64, groupName string) // KickGroupMember 踢出群成员 func (b *Bot) KickGroupMember(ctx context.Context, groupID, userID int64, rejectAddRequest bool) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, "user_id": userID, "reject_add_request": rejectAddRequest, @@ -248,7 +248,7 @@ func (b *Bot) KickGroupMember(ctx context.Context, groupID, userID int64, reject // MuteGroupMember 禁言群成员 func (b *Bot) MuteGroupMember(ctx context.Context, groupID, userID int64, duration int32) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, "user_id": userID, "duration": duration, @@ -258,7 +258,7 @@ func (b *Bot) MuteGroupMember(ctx context.Context, groupID, userID int64, durati // MuteGroupWhole 全体禁言 func (b *Bot) MuteGroupWhole(ctx context.Context, groupID int64, isMute bool) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, "is_mute": isMute, } @@ -267,7 +267,7 @@ func (b *Bot) MuteGroupWhole(ctx context.Context, groupID int64, isMute bool) (* // LeaveGroup 退出群 func (b *Bot) LeaveGroup(ctx context.Context, groupID int64) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, } return b.adapter.CallAPI(ctx, "leave_group", params) @@ -275,7 +275,7 @@ func (b *Bot) LeaveGroup(ctx context.Context, groupID int64) (*APIResponse, erro // HandleFriendRequest 处理好友请求 func (b *Bot) HandleFriendRequest(ctx context.Context, initiatorUID string, accept bool) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "initiator_uid": initiatorUID, "accept": accept, } @@ -284,7 +284,7 @@ func (b *Bot) HandleFriendRequest(ctx context.Context, initiatorUID string, acce // HandleGroupJoinRequest 处理入群申请 func (b *Bot) HandleGroupJoinRequest(ctx context.Context, groupID, notificationSeq int64, accept bool, rejectReason string) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, "notification_seq": notificationSeq, "accept": accept, @@ -295,7 +295,7 @@ func (b *Bot) HandleGroupJoinRequest(ctx context.Context, groupID, notificationS // HandleGroupInvitation 处理群邀请 func (b *Bot) HandleGroupInvitation(ctx context.Context, groupID, invitationSeq int64, accept bool) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "group_id": groupID, "invitation_seq": invitationSeq, "accept": accept, @@ -305,7 +305,7 @@ func (b *Bot) HandleGroupInvitation(ctx context.Context, groupID, invitationSeq // UploadFile 上传文件 func (b *Bot) UploadFile(ctx context.Context, fileType, filePath string) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "file_type": fileType, "file_path": filePath, } @@ -314,7 +314,7 @@ func (b *Bot) UploadFile(ctx context.Context, fileType, filePath string) (*APIRe // GetFile 获取文件 func (b *Bot) GetFile(ctx context.Context, fileID string) (*APIResponse, error) { - params := map[string]interface{}{ + params := map[string]any{ "file_id": fileID, } return b.adapter.CallAPI(ctx, "get_file", params) diff --git a/internal/adapter/milky/event.go b/internal/adapter/milky/event.go index 4134ff2..be33e18 100644 --- a/internal/adapter/milky/event.go +++ b/internal/adapter/milky/event.go @@ -101,7 +101,7 @@ func (c *EventConverter) convertMessageEvent(milkyEvent *Event) (protocol.Event, SubType: "", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}{ + Data: map[string]any{ "peer_id": msgData.PeerID, "message_seq": msgData.MessageSeq, "sender_id": msgData.SenderID, @@ -145,7 +145,7 @@ func (c *EventConverter) convertFriendRequestEvent(milkyEvent *Event) (protocol. SubType: "", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}{ + Data: map[string]any{ "initiator_id": data.InitiatorID, "initiator_uid": data.InitiatorUID, "comment": data.Comment, @@ -178,7 +178,7 @@ func (c *EventConverter) convertGroupJoinRequestEvent(milkyEvent *Event) (protoc SubType: "add", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}{ + Data: map[string]any{ "group_id": data.GroupID, "notification_seq": data.NotificationSeq, "is_filtered": data.IsFiltered, @@ -212,7 +212,7 @@ func (c *EventConverter) convertGroupInvitedJoinRequestEvent(milkyEvent *Event) SubType: "invite", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}{ + Data: map[string]any{ "group_id": data.GroupID, "notification_seq": data.NotificationSeq, "initiator_id": data.InitiatorID, @@ -245,7 +245,7 @@ func (c *EventConverter) convertGroupInvitationEvent(milkyEvent *Event) (protoco SubType: "invite_self", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}{ + Data: map[string]any{ "group_id": data.GroupID, "invitation_seq": data.InvitationSeq, "initiator_id": data.InitiatorID, @@ -277,7 +277,7 @@ func (c *EventConverter) convertMessageRecallEvent(milkyEvent *Event) (protocol. SubType: data.MessageScene, Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}{ + Data: map[string]any{ "message_scene": data.MessageScene, "peer_id": data.PeerID, "message_seq": data.MessageSeq, @@ -310,7 +310,7 @@ func (c *EventConverter) convertBotOfflineEvent(milkyEvent *Event) (protocol.Eve SubType: "", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}{ + Data: map[string]any{ "reason": data.Reason, }, }, @@ -337,7 +337,7 @@ func (c *EventConverter) convertFriendNudgeEvent(milkyEvent *Event) (protocol.Ev SubType: "", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, UserID: strconv.FormatInt(data.UserID, 10), } @@ -362,7 +362,7 @@ func (c *EventConverter) convertFriendFileUploadEvent(milkyEvent *Event) (protoc SubType: "", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, UserID: strconv.FormatInt(data.UserID, 10), } @@ -392,7 +392,7 @@ func (c *EventConverter) convertGroupAdminChangeEvent(milkyEvent *Event) (protoc SubType: subType, Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, GroupID: strconv.FormatInt(data.GroupID, 10), UserID: strconv.FormatInt(data.UserID, 10), @@ -423,7 +423,7 @@ func (c *EventConverter) convertGroupEssenceMessageChangeEvent(milkyEvent *Event SubType: subType, Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, GroupID: strconv.FormatInt(data.GroupID, 10), } @@ -448,7 +448,7 @@ func (c *EventConverter) convertGroupMemberIncreaseEvent(milkyEvent *Event) (pro SubType: "", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, GroupID: strconv.FormatInt(data.GroupID, 10), UserID: strconv.FormatInt(data.UserID, 10), @@ -478,7 +478,7 @@ func (c *EventConverter) convertGroupMemberDecreaseEvent(milkyEvent *Event) (pro SubType: "", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, GroupID: strconv.FormatInt(data.GroupID, 10), UserID: strconv.FormatInt(data.UserID, 10), @@ -508,7 +508,7 @@ func (c *EventConverter) convertGroupNameChangeEvent(milkyEvent *Event) (protoco SubType: "", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, GroupID: strconv.FormatInt(data.GroupID, 10), Operator: strconv.FormatInt(data.OperatorID, 10), @@ -539,7 +539,7 @@ func (c *EventConverter) convertGroupMessageReactionEvent(milkyEvent *Event) (pr SubType: subType, Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, GroupID: strconv.FormatInt(data.GroupID, 10), UserID: strconv.FormatInt(data.UserID, 10), @@ -570,7 +570,7 @@ func (c *EventConverter) convertGroupMuteEvent(milkyEvent *Event) (protocol.Even SubType: subType, Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, GroupID: strconv.FormatInt(data.GroupID, 10), UserID: strconv.FormatInt(data.UserID, 10), @@ -602,7 +602,7 @@ func (c *EventConverter) convertGroupWholeMuteEvent(milkyEvent *Event) (protocol SubType: subType, Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, GroupID: strconv.FormatInt(data.GroupID, 10), Operator: strconv.FormatInt(data.OperatorID, 10), @@ -628,7 +628,7 @@ func (c *EventConverter) convertGroupNudgeEvent(milkyEvent *Event) (protocol.Eve SubType: "", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, GroupID: strconv.FormatInt(data.GroupID, 10), UserID: strconv.FormatInt(data.SenderID, 10), @@ -654,7 +654,7 @@ func (c *EventConverter) convertGroupFileUploadEvent(milkyEvent *Event) (protoco SubType: "", Timestamp: milkyEvent.Time, SelfID: selfID, - Data: map[string]interface{}(milkyEvent.Data), + Data: map[string]any(milkyEvent.Data), }, GroupID: strconv.FormatInt(data.GroupID, 10), UserID: strconv.FormatInt(data.UserID, 10), diff --git a/internal/adapter/milky/types.go b/internal/adapter/milky/types.go index 96657bc..d03100c 100644 --- a/internal/adapter/milky/types.go +++ b/internal/adapter/milky/types.go @@ -25,14 +25,14 @@ type Boolean = bool // IncomingSegment 接收消息段(联合类型) type IncomingSegment struct { - Type string `json:"type"` - Data map[string]interface{} `json:"data"` + Type string `json:"type"` + Data map[string]any `json:"data"` } // OutgoingSegment 发送消息段(联合类型) type OutgoingSegment struct { - Type string `json:"type"` - Data map[string]interface{} `json:"data"` + Type string `json:"type"` + Data map[string]any `json:"data"` } // IncomingForwardedMessage 接收转发消息 @@ -318,10 +318,10 @@ type GroupFileUploadEventData struct { // Event Milky 事件(联合类型,使用 event_type 区分) type Event struct { - EventType string `json:"event_type"` - Time int64 `json:"time"` - SelfID int64 `json:"self_id"` - Data map[string]interface{} `json:"data"` + EventType string `json:"event_type"` + Time int64 `json:"time"` + SelfID int64 `json:"self_id"` + Data map[string]any `json:"data"` } // 事件类型常量 @@ -353,10 +353,10 @@ const ( // APIResponse API 响应 type APIResponse struct { - Status string `json:"status"` // ok, failed - RetCode int `json:"retcode"` - Data map[string]interface{} `json:"data,omitempty"` - Message string `json:"message,omitempty"` + Status string `json:"status"` // ok, failed + RetCode int `json:"retcode"` + Data map[string]any `json:"data,omitempty"` + Message string `json:"message,omitempty"` } // 响应状态码 diff --git a/internal/adapter/onebot11/action.go b/internal/adapter/onebot11/action.go index b4e1e82..d2cd815 100644 --- a/internal/adapter/onebot11/action.go +++ b/internal/adapter/onebot11/action.go @@ -82,16 +82,16 @@ func ConvertAction(action protocol.Action) string { // SendPrivateMessageAction 发送私聊消息动作 type SendPrivateMessageAction struct { - UserID int64 `json:"user_id"` - Message interface{} `json:"message"` - AutoEscape bool `json:"auto_escape,omitempty"` + UserID int64 `json:"user_id"` + Message any `json:"message"` + AutoEscape bool `json:"auto_escape,omitempty"` } // SendGroupMessageAction 发送群消息动作 type SendGroupMessageAction struct { - GroupID int64 `json:"group_id"` - Message interface{} `json:"message"` - AutoEscape bool `json:"auto_escape,omitempty"` + GroupID int64 `json:"group_id"` + Message any `json:"message"` + AutoEscape bool `json:"auto_escape,omitempty"` } // DeleteMessageAction 撤回消息动作 @@ -224,12 +224,12 @@ type SetRestartAction struct { // ActionResponse API响应 type ActionResponse struct { - Status string `json:"status"` - RetCode int `json:"retcode"` - Data map[string]interface{} `json:"data,omitempty"` - Echo string `json:"echo,omitempty"` - Message string `json:"message,omitempty"` - Wording string `json:"wording,omitempty"` + Status string `json:"status"` + RetCode int `json:"retcode"` + Data map[string]any `json:"data,omitempty"` + Echo string `json:"echo,omitempty"` + Message string `json:"message,omitempty"` + Wording string `json:"wording,omitempty"` } // 响应状态码常量 @@ -245,7 +245,7 @@ const ( ) // BuildActionRequest 构建动作请求 -func BuildActionRequest(action string, params map[string]interface{}, echo string) *OB11Action { +func BuildActionRequest(action string, params map[string]any, echo string) *OB11Action { return &OB11Action{ Action: action, Params: params, @@ -254,8 +254,8 @@ func BuildActionRequest(action string, params map[string]interface{}, echo strin } // BuildSendPrivateMsg 构建发送私聊消息请求 -func BuildSendPrivateMsg(userID int64, message interface{}, autoEscape bool) map[string]interface{} { - return map[string]interface{}{ +func BuildSendPrivateMsg(userID int64, message any, autoEscape bool) map[string]any { + return map[string]any{ "user_id": userID, "message": message, "auto_escape": autoEscape, @@ -263,8 +263,8 @@ func BuildSendPrivateMsg(userID int64, message interface{}, autoEscape bool) map } // BuildSendGroupMsg 构建发送群消息请求 -func BuildSendGroupMsg(groupID int64, message interface{}, autoEscape bool) map[string]interface{} { - return map[string]interface{}{ +func BuildSendGroupMsg(groupID int64, message any, autoEscape bool) map[string]any { + return map[string]any{ "group_id": groupID, "message": message, "auto_escape": autoEscape, @@ -272,15 +272,15 @@ func BuildSendGroupMsg(groupID int64, message interface{}, autoEscape bool) map[ } // BuildDeleteMsg 构建撤回消息请求 -func BuildDeleteMsg(messageID int32) map[string]interface{} { - return map[string]interface{}{ +func BuildDeleteMsg(messageID int32) map[string]any { + return map[string]any{ "message_id": messageID, } } // BuildSetGroupBan 构建群组禁言请求 -func BuildSetGroupBan(groupID, userID int64, duration int64) map[string]interface{} { - return map[string]interface{}{ +func BuildSetGroupBan(groupID, userID int64, duration int64) map[string]any { + return map[string]any{ "group_id": groupID, "user_id": userID, "duration": duration, @@ -288,8 +288,8 @@ func BuildSetGroupBan(groupID, userID int64, duration int64) map[string]interfac } // BuildSetGroupKick 构建群组踢人请求 -func BuildSetGroupKick(groupID, userID int64, rejectAddRequest bool) map[string]interface{} { - return map[string]interface{}{ +func BuildSetGroupKick(groupID, userID int64, rejectAddRequest bool) map[string]any { + return map[string]any{ "group_id": groupID, "user_id": userID, "reject_add_request": rejectAddRequest, @@ -297,8 +297,8 @@ func BuildSetGroupKick(groupID, userID int64, rejectAddRequest bool) map[string] } // BuildSetGroupCard 构建设置群名片请求 -func BuildSetGroupCard(groupID, userID int64, card string) map[string]interface{} { - return map[string]interface{}{ +func BuildSetGroupCard(groupID, userID int64, card string) map[string]any { + return map[string]any{ "group_id": groupID, "user_id": userID, "card": card, diff --git a/internal/adapter/onebot11/adapter.go b/internal/adapter/onebot11/adapter.go index 3152741..d7b9481 100644 --- a/internal/adapter/onebot11/adapter.go +++ b/internal/adapter/onebot11/adapter.go @@ -3,6 +3,7 @@ package onebot11 import ( "context" "fmt" + "net/url" "sync" "time" @@ -28,6 +29,10 @@ type Adapter struct { wsConnection *net.WebSocketConnection ctx context.Context cancel context.CancelFunc + + // 消息去重 + processedMessages map[int64]time.Time + messageMu sync.RWMutex } // Config OneBot11配置 @@ -65,16 +70,20 @@ func NewAdapter(config *Config, logger *zap.Logger, wsManager *net.WebSocketMana } adapter := &Adapter{ - config: config, - logger: logger.Named("onebot11"), - wsManager: wsManager, - wsWaiter: NewWSResponseWaiter(timeout, logger), - eventBus: eventBus, - selfID: config.SelfID, - ctx: ctx, - cancel: cancel, + config: config, + logger: logger.Named("onebot11"), + wsManager: wsManager, + wsWaiter: NewWSResponseWaiter(timeout, logger), + eventBus: eventBus, + selfID: config.SelfID, + ctx: ctx, + cancel: cancel, + processedMessages: make(map[int64]time.Time), } + // 启动定期清理过期消息的 goroutine + go adapter.cleanupProcessedMessages() + // 如果使用HTTP连接,初始化HTTP客户端 if config.ConnectionType == "http" && config.HTTPUrl != "" { adapter.httpClient = NewHTTPClient(config.HTTPUrl, config.AccessToken, timeout, logger) @@ -83,6 +92,43 @@ func NewAdapter(config *Config, logger *zap.Logger, wsManager *net.WebSocketMana return adapter } +// cleanupProcessedMessages 定期清理过期的已处理消息记录 +func (a *Adapter) cleanupProcessedMessages() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-a.ctx.Done(): + return + case <-ticker.C: + a.messageMu.Lock() + now := time.Now() + for msgID, processedAt := range a.processedMessages { + if now.Sub(processedAt) > 10*time.Minute { + delete(a.processedMessages, msgID) + } + } + a.messageMu.Unlock() + } + } +} + +// isMessageProcessed 检查消息是否已处理 +func (a *Adapter) isMessageProcessed(messageID int64) bool { + a.messageMu.RLock() + _, exists := a.processedMessages[messageID] + a.messageMu.RUnlock() + return exists +} + +// markMessageProcessed 标记消息为已处理 +func (a *Adapter) markMessageProcessed(messageID int64) { + a.messageMu.Lock() + a.processedMessages[messageID] = time.Now() + a.messageMu.Unlock() +} + // Name 获取协议名称 func (a *Adapter) Name() string { return "OneBot" @@ -174,7 +220,7 @@ func (a *Adapter) GetSelfID() string { } // SendAction 发送动作 -func (a *Adapter) SendAction(ctx context.Context, action protocol.Action) (map[string]interface{}, error) { +func (a *Adapter) SendAction(ctx context.Context, action protocol.Action) (map[string]any, error) { // 序列化为OneBot11格式 data, err := a.SerializeAction(action) if err != nil { @@ -228,7 +274,7 @@ func (a *Adapter) SerializeAction(action protocol.Action) ([]byte, error) { } // 复制参数并转换消息链 - params := make(map[string]interface{}) + params := make(map[string]any) for k, v := range action.GetParams() { // 检查是否是消息链 if k == "message" { @@ -259,10 +305,10 @@ func (a *Adapter) connectWebSocket(ctx context.Context) error { zap.String("url", a.config.WSUrl), zap.Bool("has_token", a.config.AccessToken != "")) - // 添加访问令牌到URL + // 添加访问令牌到URL(需要对token进行URL编码以处理特殊字符) wsURL := a.config.WSUrl if a.config.AccessToken != "" { - wsURL += "?access_token=" + a.config.AccessToken + wsURL += "?access_token=" + url.QueryEscape(a.config.AccessToken) a.logger.Debug("Added access token to WebSocket URL") } @@ -342,7 +388,7 @@ func (a *Adapter) connectHTTPPost(ctx context.Context) error { } // sendActionWebSocket 通过WebSocket发送动作 -func (a *Adapter) sendActionWebSocket(data []byte) (map[string]interface{}, error) { +func (a *Adapter) sendActionWebSocket(data []byte) (map[string]any, error) { if a.wsConnection == nil { return nil, fmt.Errorf("websocket connection not established") } @@ -383,7 +429,7 @@ func (a *Adapter) sendActionWebSocket(data []byte) (map[string]interface{}, erro } // sendActionHTTP 通过HTTP发送动作 -func (a *Adapter) sendActionHTTP(data []byte) (map[string]interface{}, error) { +func (a *Adapter) sendActionHTTP(data []byte) (map[string]any, error) { if a.httpClient == nil { return nil, fmt.Errorf("http client not initialized") } @@ -439,7 +485,7 @@ func (a *Adapter) handleWebSocketMessages() { zap.String("preview", string(message[:min(len(message), 200)]))) // 尝试解析为响应(先检查是否有 echo 字段) - var tempMap map[string]interface{} + var tempMap map[string]any if err := sonic.Unmarshal(message, &tempMap); err == nil { if echo, ok := tempMap["echo"].(string); ok && echo != "" { // 有 echo 字段,说明是API响应 @@ -478,6 +524,24 @@ func (a *Adapter) handleWebSocketMessages() { continue } + // 消息去重检查(基于 message_id) + data := event.GetData() + // message_id 可能是 int32 或 int64 + var messageID int64 + if mid, ok := data["message_id"].(int32); ok && mid > 0 { + messageID = int64(mid) + } else if mid, ok := data["message_id"].(int64); ok && mid > 0 { + messageID = mid + } + if messageID > 0 { + if a.isMessageProcessed(messageID) { + a.logger.Warn("Duplicate message detected, skipping", + zap.Int64("message_id", messageID)) + continue + } + a.markMessageProcessed(messageID) + } + // 发布事件到事件总线 a.logger.Info("Publishing event to event bus", zap.String("event_type", string(event.GetType())), diff --git a/internal/adapter/onebot11/client.go b/internal/adapter/onebot11/client.go index 4c955b0..1503faf 100644 --- a/internal/adapter/onebot11/client.go +++ b/internal/adapter/onebot11/client.go @@ -41,9 +41,9 @@ func NewHTTPClient(baseURL, accessToken string, timeout time.Duration, logger *z } // Call 调用API -func (c *HTTPClient) Call(ctx context.Context, action string, params map[string]interface{}) (*OB11Response, error) { +func (c *HTTPClient) Call(ctx context.Context, action string, params map[string]any) (*OB11Response, error) { // 构建请求数据 - reqData := map[string]interface{}{ + reqData := map[string]any{ "action": action, "params": params, } diff --git a/internal/adapter/onebot11/event.go b/internal/adapter/onebot11/event.go index c1eea54..a961cbf 100644 --- a/internal/adapter/onebot11/event.go +++ b/internal/adapter/onebot11/event.go @@ -14,7 +14,7 @@ func (a *Adapter) convertToEvent(raw *RawEvent) (protocol.Event, error) { baseEvent := &protocol.BaseEvent{ Timestamp: raw.Time, SelfID: strconv.FormatInt(raw.SelfID, 10), - Data: make(map[string]interface{}), + Data: make(map[string]any), } switch raw.PostType { @@ -49,7 +49,7 @@ func (a *Adapter) convertMessageEvent(raw *RawEvent, base *protocol.BaseEvent) ( } if raw.Sender != nil { - senderData := map[string]interface{}{ + senderData := map[string]any{ "user_id": raw.Sender.UserID, "nickname": raw.Sender.Nickname, } @@ -69,7 +69,7 @@ func (a *Adapter) convertMessageEvent(raw *RawEvent, base *protocol.BaseEvent) ( } if raw.Anonymous != nil { - base.Data["anonymous"] = map[string]interface{}{ + base.Data["anonymous"] = map[string]any{ "id": raw.Anonymous.ID, "name": raw.Anonymous.Name, "flag": raw.Anonymous.Flag, @@ -111,7 +111,7 @@ func (a *Adapter) convertNoticeEvent(raw *RawEvent, base *protocol.BaseEvent) (p case NoticeTypeGroupUpload: // 文件上传信息 if raw.File != nil { - base.Data["file"] = map[string]interface{}{ + base.Data["file"] = map[string]any{ "id": raw.File.ID, "name": raw.File.Name, "size": raw.File.Size, @@ -156,12 +156,12 @@ func (a *Adapter) convertMetaEvent(raw *RawEvent, base *protocol.BaseEvent) (pro base.DetailType = raw.MetaType if raw.Status != nil { - statusData := map[string]interface{}{ + statusData := map[string]any{ "online": raw.Status.Online, "good": raw.Status.Good, } if raw.Status.Stat != nil { - statusData["stat"] = map[string]interface{}{ + statusData["stat"] = map[string]any{ "packet_received": raw.Status.Stat.PacketReceived, "packet_sent": raw.Status.Stat.PacketSent, "packet_lost": raw.Status.Stat.PacketLost, @@ -183,7 +183,7 @@ func (a *Adapter) convertMetaEvent(raw *RawEvent, base *protocol.BaseEvent) (pro } // parseMessageSegments 解析消息段 -func (a *Adapter) parseMessageSegments(message interface{}) ([]MessageSegment, error) { +func (a *Adapter) parseMessageSegments(message any) ([]MessageSegment, error) { if message == nil { return nil, fmt.Errorf("message is nil") } @@ -193,7 +193,7 @@ func (a *Adapter) parseMessageSegments(message interface{}) ([]MessageSegment, e return []MessageSegment{ { Type: SegmentTypeText, - Data: map[string]interface{}{ + Data: map[string]any{ "text": str, }, }, @@ -215,7 +215,7 @@ func (a *Adapter) parseMessageSegments(message interface{}) ([]MessageSegment, e } // BuildMessage 构建OneBot11消息 -func BuildMessage(segments []MessageSegment) interface{} { +func BuildMessage(segments []MessageSegment) any { if len(segments) == 0 { return "" } @@ -235,7 +235,7 @@ func BuildTextMessage(text string) []MessageSegment { return []MessageSegment{ { Type: SegmentTypeText, - Data: map[string]interface{}{ + Data: map[string]any{ "text": text, }, }, @@ -247,7 +247,7 @@ func BuildImageMessage(file string) []MessageSegment { return []MessageSegment{ { Type: SegmentTypeImage, - Data: map[string]interface{}{ + Data: map[string]any{ "file": file, }, }, @@ -258,7 +258,7 @@ func BuildImageMessage(file string) []MessageSegment { func BuildAtMessage(userID int64) MessageSegment { return MessageSegment{ Type: SegmentTypeAt, - Data: map[string]interface{}{ + Data: map[string]any{ "qq": userID, }, } @@ -268,7 +268,7 @@ func BuildAtMessage(userID int64) MessageSegment { func BuildReplyMessage(messageID int32) MessageSegment { return MessageSegment{ Type: SegmentTypeReply, - Data: map[string]interface{}{ + Data: map[string]any{ "id": messageID, }, } @@ -278,7 +278,7 @@ func BuildReplyMessage(messageID int32) MessageSegment { func BuildFaceMessage(faceID int) MessageSegment { return MessageSegment{ Type: SegmentTypeFace, - Data: map[string]interface{}{ + Data: map[string]any{ "id": faceID, }, } @@ -288,7 +288,7 @@ func BuildFaceMessage(faceID int) MessageSegment { func BuildRecordMessage(file string) MessageSegment { return MessageSegment{ Type: SegmentTypeRecord, - Data: map[string]interface{}{ + Data: map[string]any{ "file": file, }, } @@ -298,7 +298,7 @@ func BuildRecordMessage(file string) MessageSegment { func BuildVideoMessage(file string) MessageSegment { return MessageSegment{ Type: SegmentTypeVideo, - Data: map[string]interface{}{ + Data: map[string]any{ "file": file, }, } @@ -308,7 +308,7 @@ func BuildVideoMessage(file string) MessageSegment { func BuildShareMessage(url, title string) MessageSegment { return MessageSegment{ Type: SegmentTypeShare, - Data: map[string]interface{}{ + Data: map[string]any{ "url": url, "title": title, }, @@ -319,7 +319,7 @@ func BuildShareMessage(url, title string) MessageSegment { func BuildLocationMessage(lat, lon float64, title, content string) MessageSegment { return MessageSegment{ Type: SegmentTypeLocation, - Data: map[string]interface{}{ + Data: map[string]any{ "lat": lat, "lon": lon, "title": title, @@ -332,7 +332,7 @@ func BuildLocationMessage(lat, lon float64, title, content string) MessageSegmen func BuildMusicMessage(musicType, musicID string) MessageSegment { return MessageSegment{ Type: SegmentTypeMusic, - Data: map[string]interface{}{ + Data: map[string]any{ "type": musicType, "id": musicID, }, @@ -343,7 +343,7 @@ func BuildMusicMessage(musicType, musicID string) MessageSegment { func BuildCustomMusicMessage(url, audio, title, content, image string) MessageSegment { return MessageSegment{ Type: SegmentTypeMusic, - Data: map[string]interface{}{ + Data: map[string]any{ "type": "custom", "url": url, "audio": audio, diff --git a/internal/adapter/onebot11/message.go b/internal/adapter/onebot11/message.go index a50c42d..7bdb4d8 100644 --- a/internal/adapter/onebot11/message.go +++ b/internal/adapter/onebot11/message.go @@ -7,7 +7,7 @@ import ( ) // ConvertMessageChainToOB11 将通用消息链转换为 OneBot11 格式 -func ConvertMessageChainToOB11(chain protocol.MessageChain) interface{} { +func ConvertMessageChainToOB11(chain protocol.MessageChain) any { if len(chain) == 0 { return "" } @@ -38,7 +38,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment { // 文本消息段 return &MessageSegment{ Type: SegmentTypeText, - Data: map[string]interface{}{ + Data: map[string]any{ "text": seg.Data["text"], }, } @@ -48,7 +48,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment { userID := seg.Data["user_id"] return &MessageSegment{ Type: SegmentTypeAt, - Data: map[string]interface{}{ + Data: map[string]any{ "qq": userID, }, } @@ -61,7 +61,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment { } return &MessageSegment{ Type: SegmentTypeAt, - Data: map[string]interface{}{ + Data: map[string]any{ "qq": userID, }, } @@ -75,7 +75,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment { } return &MessageSegment{ Type: SegmentTypeImage, - Data: map[string]interface{}{ + Data: map[string]any{ "file": fileID, }, } @@ -88,7 +88,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment { } return &MessageSegment{ Type: SegmentTypeRecord, - Data: map[string]interface{}{ + Data: map[string]any{ "file": fileID, }, } @@ -101,7 +101,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment { } return &MessageSegment{ Type: SegmentTypeRecord, - Data: map[string]interface{}{ + Data: map[string]any{ "file": fileID, }, } @@ -114,7 +114,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment { } return &MessageSegment{ Type: SegmentTypeVideo, - Data: map[string]interface{}{ + Data: map[string]any{ "file": fileID, }, } @@ -124,7 +124,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment { messageID := seg.Data["message_id"] return &MessageSegment{ Type: SegmentTypeReply, - Data: map[string]interface{}{ + Data: map[string]any{ "id": messageID, }, } @@ -134,7 +134,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment { faceID := seg.Data["id"] return &MessageSegment{ Type: SegmentTypeFace, - Data: map[string]interface{}{ + Data: map[string]any{ "id": faceID, }, } @@ -149,7 +149,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment { } // ConvertOB11ToMessageChain 将 OneBot11 消息段转换为通用消息链 -func ConvertOB11ToMessageChain(ob11Message interface{}) (protocol.MessageChain, error) { +func ConvertOB11ToMessageChain(ob11Message any) (protocol.MessageChain, error) { chain := protocol.MessageChain{} // 如果是字符串,转换为文本消息段 @@ -167,11 +167,11 @@ func ConvertOB11ToMessageChain(ob11Message interface{}) (protocol.MessageChain, } // 如果是接口数组,尝试转换 - if segments, ok := ob11Message.([]interface{}); ok { + if segments, ok := ob11Message.([]any); ok { for _, seg := range segments { - if segMap, ok := seg.(map[string]interface{}); ok { + if segMap, ok := seg.(map[string]any); ok { segType, _ := segMap["type"].(string) - segData, _ := segMap["data"].(map[string]interface{}) + segData, _ := segMap["data"].(map[string]any) genericSeg := convertOB11SegmentToGeneric(MessageSegment{ Type: segType, Data: segData, @@ -211,7 +211,7 @@ func convertOB11SegmentToGeneric(seg MessageSegment) protocol.MessageSegment { } return protocol.MessageSegment{ Type: protocol.SegmentTypeVoice, - Data: map[string]interface{}{ + Data: map[string]any{ "file_id": fileID, }, } @@ -223,7 +223,7 @@ func convertOB11SegmentToGeneric(seg MessageSegment) protocol.MessageSegment { case SegmentTypeFace: return protocol.MessageSegment{ Type: protocol.SegmentTypeFace, - Data: map[string]interface{}{ + Data: map[string]any{ "id": seg.Data["id"], }, } diff --git a/internal/adapter/onebot11/types.go b/internal/adapter/onebot11/types.go index 40ecad9..b25ea09 100644 --- a/internal/adapter/onebot11/types.go +++ b/internal/adapter/onebot11/types.go @@ -2,32 +2,32 @@ package onebot11 // RawEvent OneBot11原始事件 type RawEvent struct { - Time int64 `json:"time"` - SelfID int64 `json:"self_id"` - PostType string `json:"post_type"` - MessageType string `json:"message_type,omitempty"` - SubType string `json:"sub_type,omitempty"` - MessageID int32 `json:"message_id,omitempty"` - UserID int64 `json:"user_id,omitempty"` - GroupID int64 `json:"group_id,omitempty"` - Message interface{} `json:"message,omitempty"` - RawMessage string `json:"raw_message,omitempty"` - Font int32 `json:"font,omitempty"` - Sender *Sender `json:"sender,omitempty"` - Anonymous *Anonymous `json:"anonymous,omitempty"` - NoticeType string `json:"notice_type,omitempty"` - OperatorID int64 `json:"operator_id,omitempty"` - Duration int64 `json:"duration,omitempty"` - RequestType string `json:"request_type,omitempty"` - Comment string `json:"comment,omitempty"` - Flag string `json:"flag,omitempty"` - MetaType string `json:"meta_event_type,omitempty"` - Status *Status `json:"status,omitempty"` - Interval int64 `json:"interval,omitempty"` - File *FileInfo `json:"file,omitempty"` // 群文件上传信息 - TargetID int64 `json:"target_id,omitempty"` // 戳一戳、红包运气王目标ID - HonorType string `json:"honor_type,omitempty"` // 群荣誉类型 - Extra map[string]interface{} `json:"-"` + Time int64 `json:"time"` + SelfID int64 `json:"self_id"` + PostType string `json:"post_type"` + MessageType string `json:"message_type,omitempty"` + SubType string `json:"sub_type,omitempty"` + MessageID int32 `json:"message_id,omitempty"` + UserID int64 `json:"user_id,omitempty"` + GroupID int64 `json:"group_id,omitempty"` + Message any `json:"message,omitempty"` + RawMessage string `json:"raw_message,omitempty"` + Font int32 `json:"font,omitempty"` + Sender *Sender `json:"sender,omitempty"` + Anonymous *Anonymous `json:"anonymous,omitempty"` + NoticeType string `json:"notice_type,omitempty"` + OperatorID int64 `json:"operator_id,omitempty"` + Duration int64 `json:"duration,omitempty"` + RequestType string `json:"request_type,omitempty"` + Comment string `json:"comment,omitempty"` + Flag string `json:"flag,omitempty"` + MetaType string `json:"meta_event_type,omitempty"` + Status *Status `json:"status,omitempty"` + Interval int64 `json:"interval,omitempty"` + File *FileInfo `json:"file,omitempty"` // 群文件上传信息 + TargetID int64 `json:"target_id,omitempty"` // 戳一戳、红包运气王目标ID + HonorType string `json:"honor_type,omitempty"` // 群荣誉类型 + Extra map[string]any `json:"-"` } // Sender 发送者信息 @@ -79,23 +79,23 @@ type Stat struct { // OB11Action OneBot11动作 type OB11Action struct { - Action string `json:"action"` - Params map[string]interface{} `json:"params"` - Echo string `json:"echo,omitempty"` + Action string `json:"action"` + Params map[string]any `json:"params"` + Echo string `json:"echo,omitempty"` } // OB11Response OneBot11响应 type OB11Response struct { - Status string `json:"status"` - RetCode int `json:"retcode"` - Data map[string]interface{} `json:"data,omitempty"` - Echo string `json:"echo,omitempty"` + Status string `json:"status"` + RetCode int `json:"retcode"` + Data map[string]any `json:"data,omitempty"` + Echo string `json:"echo,omitempty"` } // MessageSegment 消息段 type MessageSegment struct { - Type string `json:"type"` - Data map[string]interface{} `json:"data"` + Type string `json:"type"` + Data map[string]any `json:"data"` } // 消息段类型常量 diff --git a/internal/di/providers.go b/internal/di/providers.go index c39bdaf..4c21e18 100644 --- a/internal/di/providers.go +++ b/internal/di/providers.go @@ -149,22 +149,9 @@ func ProvideMilkyBots(cfg *config.Config, logger *zap.Logger, eventBus *engine.E bot := milky.NewBot(botCfg.ID, milkyCfg, eventBus, wsManager, logger) botManager.Add(bot) + // 注意:不在这里启动连接,由 RegisterLifecycleHooks 中的 botManager.StartAll() 统一启动 + // 这样避免重复启动的问题 lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - logger.Info("Starting Milky bot", zap.String("bot_id", botCfg.ID)) - // 在后台启动连接,失败时只记录错误,不终止应用 - go func() { - if err := bot.Connect(context.Background()); err != nil { - logger.Error("Failed to connect Milky bot, will retry in background", - zap.String("bot_id", botCfg.ID), - zap.Error(err)) - // 可以在这里实现重试逻辑 - } else { - logger.Info("Milky bot connected successfully", zap.String("bot_id", botCfg.ID)) - } - }() - return nil - }, OnStop: func(ctx context.Context) error { logger.Info("Stopping Milky bot", zap.String("bot_id", botCfg.ID)) return bot.Disconnect(ctx) @@ -200,22 +187,9 @@ func ProvideOneBot11Bots(cfg *config.Config, logger *zap.Logger, wsManager *net. bot := onebot11.NewBot(botCfg.ID, ob11Cfg, logger, wsManager, eventBus) botManager.Add(bot) + // 注意:不在这里启动连接,由 RegisterLifecycleHooks 中的 botManager.StartAll() 统一启动 + // 这样避免重复启动的问题 lc.Append(fx.Hook{ - OnStart: func(ctx context.Context) error { - logger.Info("Starting OneBot11 bot", zap.String("bot_id", botCfg.ID)) - // 在后台启动连接,失败时只记录错误,不终止应用 - go func() { - if err := bot.Connect(context.Background()); err != nil { - logger.Error("Failed to connect OneBot11 bot, will retry in background", - zap.String("bot_id", botCfg.ID), - zap.Error(err)) - // 可以在这里实现重试逻辑 - } else { - logger.Info("OneBot11 bot connected successfully", zap.String("bot_id", botCfg.ID)) - } - }() - return nil - }, OnStop: func(ctx context.Context) error { logger.Info("Stopping OneBot11 bot", zap.String("bot_id", botCfg.ID)) return bot.Disconnect(ctx) diff --git a/internal/engine/dispatcher.go b/internal/engine/dispatcher.go index ce93990..a43b751 100644 --- a/internal/engine/dispatcher.go +++ b/internal/engine/dispatcher.go @@ -3,6 +3,7 @@ package engine import ( "context" "runtime/debug" + "slices" "sort" "sync" "sync/atomic" @@ -238,8 +239,7 @@ func (d *Dispatcher) handleEvent(ctx context.Context, event protocol.Event) { func (d *Dispatcher) createHandlerChain(ctx context.Context, event protocol.Event) func(context.Context, protocol.Event) { return func(ctx context.Context, e protocol.Event) { d.mu.RLock() - handlers := make([]protocol.EventHandler, len(d.handlers)) - copy(handlers, d.handlers) + handlers := slices.Clone(d.handlers) d.mu.RUnlock() for i, handler := range handlers { diff --git a/internal/engine/eventbus.go b/internal/engine/eventbus.go index 9a168d8..6cb043e 100644 --- a/internal/engine/eventbus.go +++ b/internal/engine/eventbus.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "encoding/hex" + "slices" "sync" "sync/atomic" "time" @@ -63,8 +64,7 @@ func NewEventBus(logger *zap.Logger, bufferSize int) *EventBus { // Start 启动事件总线 func (eb *EventBus) Start() { - eb.wg.Add(1) - go eb.dispatch() + eb.wg.Go(eb.dispatch) eb.logger.Info("Event bus started") } @@ -193,8 +193,6 @@ func (eb *EventBus) Unsubscribe(eventType protocol.EventType, ch chan protocol.E // dispatch 分发事件到订阅者 func (eb *EventBus) dispatch() { - defer eb.wg.Done() - eb.logger.Info("Event bus dispatch loop started") for { @@ -220,8 +218,7 @@ func (eb *EventBus) dispatchEvent(event protocol.Event) { key := string(event.GetType()) subs := eb.subscriptions[key] // 复制订阅者列表避免锁竞争 - subsCopy := make([]*Subscription, len(subs)) - copy(subsCopy, subs) + subsCopy := slices.Clone(subs) eb.mu.RUnlock() eb.logger.Info("Dispatching event", diff --git a/internal/engine/middleware.go b/internal/engine/middleware.go index c19f223..97a96e4 100644 --- a/internal/engine/middleware.go +++ b/internal/engine/middleware.go @@ -260,14 +260,14 @@ func (m *MetricsMiddleware) Process(ctx context.Context, event protocol.Event, n } // GetMetrics 获取指标 -func (m *MetricsMiddleware) GetMetrics() map[string]interface{} { +func (m *MetricsMiddleware) GetMetrics() map[string]any { m.mu.RLock() defer m.mu.RUnlock() - metrics := make(map[string]interface{}) + metrics := make(map[string]any) for eventType, count := range m.eventCounts { avgTime := m.eventTimes[eventType] / time.Duration(count) - metrics[eventType] = map[string]interface{}{ + metrics[eventType] = map[string]any{ "count": count, "avg_time": avgTime.String(), } diff --git a/internal/engine/plugin.go b/internal/engine/plugin.go index c7ce139..4a4c188 100644 --- a/internal/engine/plugin.go +++ b/internal/engine/plugin.go @@ -599,12 +599,12 @@ func OnlyToMe() HandlerMiddleware { selfID := event.GetSelfID() // 检查消息段中是否包含@机器人的消息 - if segments, ok := data["message_segments"].([]interface{}); ok { + if segments, ok := data["message_segments"].([]any); ok { for _, seg := range segments { - if segMap, ok := seg.(map[string]interface{}); ok { + if segMap, ok := seg.(map[string]any); ok { segType, _ := segMap["type"].(string) if segType == "at" || segType == "mention" { - segData, _ := segMap["data"].(map[string]interface{}) + segData, _ := segMap["data"].(map[string]any) // 检查是否@了机器人 if userID, ok := segData["user_id"]; ok { if userIDStr := fmt.Sprintf("%v", userID); userIDStr == selfID { diff --git a/internal/engine/scheduler.go b/internal/engine/scheduler.go index dd51c9c..5cb41d0 100644 --- a/internal/engine/scheduler.go +++ b/internal/engine/scheduler.go @@ -329,15 +329,13 @@ func (j *IntervalJob) Start(ctx context.Context) error { j.nextRun = time.Now().Add(j.interval) j.mu.Unlock() - j.wg.Add(1) - go j.run() + j.wg.Go(j.run) j.logger.Info("Interval job started", zap.Duration("interval", j.interval)) return nil } func (j *IntervalJob) run() { - defer j.wg.Done() // 立即执行一次(可选,根据需求调整) // if err := j.handler(j.ctx); err != nil { @@ -428,16 +426,13 @@ func (j *OnceJob) Start(ctx context.Context) error { j.nextRun = time.Now().Add(j.delay) j.mu.Unlock() - j.wg.Add(1) - go j.run() + j.wg.Go(j.run) j.logger.Info("Once job started", zap.Duration("delay", j.delay)) return nil } func (j *OnceJob) run() { - defer j.wg.Done() - select { case <-j.timer.C: if err := j.handler(j.ctx); err != nil { diff --git a/internal/plugins/mcstatus/mcstatus.go b/internal/plugins/mcstatus/mcstatus.go index e0261e6..5eb8c86 100644 --- a/internal/plugins/mcstatus/mcstatus.go +++ b/internal/plugins/mcstatus/mcstatus.go @@ -192,7 +192,7 @@ func handleMCSBindCommand(ctx context.Context, event protocol.Event, botManager action := &protocol.BaseAction{ Type: protocol.ActionTypeSendGroupMessage, - Params: map[string]interface{}{ + Params: map[string]any{ "group_id": groupID, "message": errorMsg, }, diff --git a/internal/plugins/mcstatus/ping.go b/internal/plugins/mcstatus/ping.go index f71a99e..11efb1e 100644 --- a/internal/plugins/mcstatus/ping.go +++ b/internal/plugins/mcstatus/ping.go @@ -76,7 +76,7 @@ func Ping(host string, port int, timeout time.Duration) (*ServerStatus, error) { } // 解析 JSON 响应 - var statusData map[string]interface{} + var statusData map[string]any if err := json.Unmarshal([]byte(statusJSON), &statusData); err != nil { return nil, fmt.Errorf("failed to parse JSON: %w", err) } @@ -88,14 +88,14 @@ func Ping(host string, port int, timeout time.Duration) (*ServerStatus, error) { } // 解析 description - if desc, ok := statusData["description"].(map[string]interface{}); ok { + if desc, ok := statusData["description"].(map[string]any); ok { if text, ok := desc["text"].(string); ok { status.Description = text - } else if text, ok := desc["extra"].([]interface{}); ok && len(text) > 0 { + } else if text, ok := desc["extra"].([]any); ok && len(text) > 0 { // 处理 extra 数组 var descText strings.Builder for _, item := range text { - if itemMap, ok := item.(map[string]interface{}); ok { + if itemMap, ok := item.(map[string]any); ok { if text, ok := itemMap["text"].(string); ok { descText.WriteString(text) } @@ -108,7 +108,7 @@ func Ping(host string, port int, timeout time.Duration) (*ServerStatus, error) { } // 解析 version - if version, ok := statusData["version"].(map[string]interface{}); ok { + if version, ok := statusData["version"].(map[string]any); ok { if name, ok := version["name"].(string); ok { status.Version.Name = name } @@ -118,7 +118,7 @@ func Ping(host string, port int, timeout time.Duration) (*ServerStatus, error) { } // 解析 players - if players, ok := statusData["players"].(map[string]interface{}); ok { + if players, ok := statusData["players"].(map[string]any); ok { if online, ok := players["online"].(float64); ok { status.Players.Online = int(online) } diff --git a/internal/plugins/welcome/welcome.go b/internal/plugins/welcome/welcome.go index 62a537d..fd9f474 100644 --- a/internal/plugins/welcome/welcome.go +++ b/internal/plugins/welcome/welcome.go @@ -49,7 +49,7 @@ func handleWelcomeEvent(ctx context.Context, event protocol.Event, botManager *p zap.Any("user_id", userID)) // 获取操作者ID(邀请者或审批者) - var operatorID interface{} + var operatorID any if opID, exists := data["operator_id"]; exists { operatorID = opID } @@ -95,7 +95,7 @@ func handleWelcomeEvent(ctx context.Context, event protocol.Event, botManager *p action := &protocol.BaseAction{ Type: protocol.ActionTypeSendGroupMessage, - Params: map[string]interface{}{ + Params: map[string]any{ "group_id": groupID, "message": welcomeChain, }, @@ -285,14 +285,14 @@ const welcomeTemplate = ` ` // buildWelcomeMessage 构建欢迎消息链(使用HTML模板渲染图片) -func buildWelcomeMessage(ctx context.Context, userID, operatorID interface{}, subType string, logger *zap.Logger) (protocol.MessageChain, error) { +func buildWelcomeMessage(ctx context.Context, userID, operatorID any, subType string, logger *zap.Logger) (protocol.MessageChain, error) { logger.Debug("Starting to build welcome message", zap.Any("user_id", userID), zap.Any("operator_id", operatorID), zap.String("sub_type", subType)) // 准备模板数据 - data := map[string]interface{}{ + data := map[string]any{ "UserID": fmt.Sprintf("%v", userID), } logger.Debug("Template data prepared", zap.Any("data", data)) diff --git a/internal/protocol/action.go b/internal/protocol/action.go index 11a3129..1595a1c 100644 --- a/internal/protocol/action.go +++ b/internal/protocol/action.go @@ -6,9 +6,9 @@ type Action interface { // GetType 获取动作类型 GetType() ActionType // GetParams 获取动作参数 - GetParams() map[string]interface{} + GetParams() map[string]any // Execute 执行动作 - Execute(ctx interface{}) (map[string]interface{}, error) + Execute(ctx any) (map[string]any, error) } // ActionType 动作类型 @@ -27,20 +27,20 @@ const ( ActionTypeGetGroupMemberList ActionType = "get_group_member_list" // 群组相关动作 - ActionTypeSetGroupKick ActionType = "set_group_kick" - ActionTypeSetGroupBan ActionType = "set_group_ban" - ActionTypeSetGroupAdmin ActionType = "set_group_admin" - ActionTypeSetGroupWholeBan ActionType = "set_group_whole_ban" + ActionTypeSetGroupKick ActionType = "set_group_kick" + ActionTypeSetGroupBan ActionType = "set_group_ban" + ActionTypeSetGroupAdmin ActionType = "set_group_admin" + ActionTypeSetGroupWholeBan ActionType = "set_group_whole_ban" // 其他动作 - ActionTypeGetStatus ActionType = "get_status" - ActionTypeGetVersion ActionType = "get_version" + ActionTypeGetStatus ActionType = "get_status" + ActionTypeGetVersion ActionType = "get_version" ) // BaseAction 基础动作结构 type BaseAction struct { - Type ActionType `json:"type"` - Params map[string]interface{} `json:"params"` + Type ActionType `json:"type"` + Params map[string]any `json:"params"` } // GetType 获取动作类型 @@ -49,19 +49,19 @@ func (a *BaseAction) GetType() ActionType { } // GetParams 获取动作参数 -func (a *BaseAction) GetParams() map[string]interface{} { +func (a *BaseAction) GetParams() map[string]any { return a.Params } // Execute 执行动作(需子类实现) -func (a *BaseAction) Execute(ctx interface{}) (map[string]interface{}, error) { +func (a *BaseAction) Execute(ctx any) (map[string]any, error) { return nil, ErrNotImplemented } // SendPrivateMessageAction 发送私聊消息动作 type SendPrivateMessageAction struct { BaseAction - UserID string `json:"user_id"` + UserID string `json:"user_id"` Message string `json:"message"` } diff --git a/internal/protocol/bot.go b/internal/protocol/bot.go index f5f25cd..2f80e2d 100644 --- a/internal/protocol/bot.go +++ b/internal/protocol/bot.go @@ -91,7 +91,7 @@ func (b *BaseBotInstance) IsConnected() bool { } // SendAction 发送动作 -func (b *BaseBotInstance) SendAction(ctx context.Context, action Action) (map[string]interface{}, error) { +func (b *BaseBotInstance) SendAction(ctx context.Context, action Action) (map[string]any, error) { return b.protocol.SendAction(ctx, action) } diff --git a/internal/protocol/event.go b/internal/protocol/event.go index d83f188..3ddb170 100644 --- a/internal/protocol/event.go +++ b/internal/protocol/event.go @@ -35,7 +35,7 @@ type Event interface { // GetSelfID 获取机器人自身ID GetSelfID() string // GetData 获取事件数据 - GetData() map[string]interface{} + GetData() map[string]any // Reply 在消息发生的群/私聊进行回复 Reply(ctx context.Context, botManager *BotManager, logger *zap.Logger, message MessageChain) error // ReplyText 在消息发生的群/私聊进行文本回复(便捷方法) @@ -44,12 +44,12 @@ type Event interface { // BaseEvent 基础事件结构 type BaseEvent struct { - Type EventType `json:"type"` - DetailType string `json:"detail_type"` - SubType string `json:"sub_type,omitempty"` - Timestamp int64 `json:"timestamp"` - SelfID string `json:"self_id"` - Data map[string]interface{} `json:"data"` + Type EventType `json:"type"` + DetailType string `json:"detail_type"` + SubType string `json:"sub_type,omitzero"` + Timestamp int64 `json:"timestamp"` + SelfID string `json:"self_id"` + Data map[string]any `json:"data"` } // GetType 获取事件类型 @@ -78,7 +78,7 @@ func (e *BaseEvent) GetSelfID() string { } // GetData 获取事件数据 -func (e *BaseEvent) GetData() map[string]interface{} { +func (e *BaseEvent) GetData() map[string]any { return e.Data } @@ -105,7 +105,7 @@ func (e *BaseEvent) Reply(ctx context.Context, botManager *BotManager, logger *z if e.GetDetailType() == "private" { action = &BaseAction{ Type: ActionTypeSendPrivateMessage, - Params: map[string]interface{}{ + Params: map[string]any{ "user_id": userID, "message": message, }, @@ -114,7 +114,7 @@ func (e *BaseEvent) Reply(ctx context.Context, botManager *BotManager, logger *z // 群聊或其他有 group_id 的事件 action = &BaseAction{ Type: ActionTypeSendGroupMessage, - Params: map[string]interface{}{ + Params: map[string]any{ "group_id": groupID, "message": message, }, @@ -151,15 +151,15 @@ type MessageEvent struct { BaseEvent MessageID string `json:"message_id"` Message string `json:"message"` - AltText string `json:"alt_text,omitempty"` + AltText string `json:"alt_text,omitzero"` } // NoticeEvent 通知事件 type NoticeEvent struct { BaseEvent - GroupID string `json:"group_id,omitempty"` - UserID string `json:"user_id,omitempty"` - Operator string `json:"operator,omitempty"` + GroupID string `json:"group_id,omitzero"` + UserID string `json:"user_id,omitzero"` + Operator string `json:"operator,omitzero"` } // RequestEvent 请求事件 diff --git a/internal/protocol/interface.go b/internal/protocol/interface.go index a965b9a..11d6c81 100644 --- a/internal/protocol/interface.go +++ b/internal/protocol/interface.go @@ -18,7 +18,7 @@ type Protocol interface { // IsConnected 检查连接状态 IsConnected() bool // SendAction 发送动作 - SendAction(ctx context.Context, action Action) (map[string]interface{}, error) + SendAction(ctx context.Context, action Action) (map[string]any, error) // HandleEvent 处理事件 HandleEvent(ctx context.Context, event Event) error // GetSelfID 获取机器人自身ID diff --git a/internal/protocol/message.go b/internal/protocol/message.go index cd308da..1c0bd57 100644 --- a/internal/protocol/message.go +++ b/internal/protocol/message.go @@ -5,8 +5,8 @@ import "fmt" // MessageSegment 消息段(基于 OneBot12 设计) // 通用消息段结构,适配器负责转换为具体协议格式 type MessageSegment struct { - Type string `json:"type"` // 消息段类型 - Data map[string]interface{} `json:"data"` // 消息段数据 + Type string `json:"type"` // 消息段类型 + Data map[string]any `json:"data"` // 消息段数据 } // MessageChain 消息链(基于 OneBot12 设计) @@ -33,17 +33,17 @@ const ( func NewTextSegment(text string) MessageSegment { return MessageSegment{ Type: SegmentTypeText, - Data: map[string]interface{}{ + Data: map[string]any{ "text": text, }, } } // NewMentionSegment 创建@提及消息段(OneBot12 标准) -func NewMentionSegment(userID interface{}) MessageSegment { +func NewMentionSegment(userID any) MessageSegment { return MessageSegment{ Type: SegmentTypeMention, - Data: map[string]interface{}{ + Data: map[string]any{ "user_id": userID, }, } @@ -53,7 +53,7 @@ func NewMentionSegment(userID interface{}) MessageSegment { func NewImageSegment(fileID string) MessageSegment { return MessageSegment{ Type: SegmentTypeImage, - Data: map[string]interface{}{ + Data: map[string]any{ "file_id": fileID, }, } @@ -63,17 +63,17 @@ func NewImageSegment(fileID string) MessageSegment { func NewImageSegmentFromBase64(base64Data string) MessageSegment { return MessageSegment{ Type: SegmentTypeImage, - Data: map[string]interface{}{ + Data: map[string]any{ "file": fmt.Sprintf("base64://%s", base64Data), }, } } // NewReplySegment 创建回复消息段 -func NewReplySegment(messageID interface{}) MessageSegment { +func NewReplySegment(messageID any) MessageSegment { return MessageSegment{ Type: SegmentTypeReply, - Data: map[string]interface{}{ + Data: map[string]any{ "message_id": messageID, }, } @@ -95,7 +95,7 @@ func (mc MessageChain) AppendText(text string) MessageChain { } // AppendMention 追加@提及到消息链 -func (mc MessageChain) AppendMention(userID interface{}) MessageChain { +func (mc MessageChain) AppendMention(userID any) MessageChain { return mc.Append(NewMentionSegment(userID)) } diff --git a/plans/go-modernization-plan.md b/plans/go-modernization-plan.md new file mode 100644 index 0000000..d9b508a --- /dev/null +++ b/plans/go-modernization-plan.md @@ -0,0 +1,253 @@ +# Go 现代化升级计划 + +## 项目概述 + +- **项目名称**: cellbot +- **当前 Go 版本**: 1.24.0 +- **目标 Go 版本**: 1.26.1 +- **分析日期**: 2026-03-17 + +## 升级摘要 + +通过代码扫描,识别出以下可使用现代 Go 特性升级的代码点: + +| 升级类型 | 影响文件数 | 修改点数 | 优先级 | +|---------|-----------|---------|-------| +| `interface{}` → `any` | 22 | 134 | 高 | +| `omitempty` → `omitzero` | 6 | 32+ | 高 | +| `wg.Add(1)` + goroutine → `wg.Go()` | 2 | 3 | 中 | +| Slice/Map 工具函数优化 | 多处 | 若干 | 中 | + +--- + +## 详细升级计划 + +### 1. 高优先级:`interface{}` 替换为 `any` + +**Go 版本要求**: 1.18+ + +项目中发现 **134 处** `interface{}` 使用,可全部替换为更简洁的 `any`。 + +#### 涉及文件列表 + +##### 协议层 (`internal/protocol/`) + +| 文件 | 修改点 | 说明 | +|-----|-------|-----| +| [`interface.go`](internal/protocol/interface.go) | 2 | 接口定义 | +| [`message.go`](internal/protocol/message.go) | 5 | 消息段数据 | +| [`action.go`](internal/protocol/action.go) | 4 | 动作参数 | +| [`event.go`](internal/protocol/event.go) | 4 | 事件数据 | +| [`bot.go`](internal/protocol/bot.go) | 1 | SendAction 返回值 | + +##### OneBot11 适配器 (`internal/adapter/onebot11/`) + +| 文件 | 修改点 | 说明 | +|-----|-------|-----| +| [`types.go`](internal/adapter/onebot11/types.go) | 8 | 类型定义 | +| [`adapter.go`](internal/adapter/onebot11/adapter.go) | 6 | 适配器方法 | +| [`action.go`](internal/adapter/onebot11/action.go) | 15 | 动作构建函数 | +| [`event.go`](internal/adapter/onebot11/event.go) | 14 | 事件解析 | +| [`message.go`](internal/adapter/onebot11/message.go) | 12 | 消息转换 | +| [`client.go`](internal/adapter/onebot11/client.go) | 2 | HTTP 客户端 | + +##### Milky 适配器 (`internal/adapter/milky/`) + +| 文件 | 修改点 | 说明 | +|-----|-------|-----| +| [`types.go`](internal/adapter/milky/types.go) | 4 | 类型定义 | +| [`adapter.go`](internal/adapter/milky/adapter.go) | 4 | 适配器方法 | +| [`bot.go`](internal/adapter/milky/bot.go) | 15 | Bot 方法 | +| [`event.go`](internal/adapter/milky/event.go) | 20+ | 事件转换 | +| [`api_client.go`](internal/adapter/milky/api_client.go) | 2 | API 客户端 | + +##### 引擎模块 (`internal/engine/`) + +| 文件 | 修改点 | 说明 | +|-----|-------|-----| +| [`middleware.go`](internal/engine/middleware.go) | 3 | 指标数据 | +| [`plugin.go`](internal/engine/plugin.go) | 4 | 插件处理 | + +##### 插件模块 (`internal/plugins/`) + +| 文件 | 修改点 | 说明 | +|-----|-------|-----| +| [`welcome/welcome.go`](internal/plugins/welcome/welcome.go) | 4 | 欢迎消息 | +| [`mcstatus/mcstatus.go`](internal/plugins/mcstatus/mcstatus.go) | 1 | 状态命令 | +| [`mcstatus/ping.go`](internal/plugins/mcstatus/ping.go) | 5 | ping 解析 | + +#### 示例修改 + +```go +// 修改前 +func (a *BaseAction) Execute(ctx interface{}) (map[string]interface{}, error) + +// 修改后 +func (a *BaseAction) Execute(ctx any) (map[string]any, error) +``` + +--- + +### 2. 高优先级:`omitempty` 替换为 `omitzero` + +**Go 版本要求**: 1.24+ + +对于 `time.Duration`、`time.Time`、结构体、切片、map 类型,应使用 `omitzero` 替代 `omitempty`,因为 `omitempty` 对这些类型不生效。 + +#### 涉及文件列表 + +| 文件 | 建议修改字段 | +|-----|------------| +| [`internal/adapter/onebot11/types.go`](internal/adapter/onebot11/types.go) | `Duration int64`, `Interval int64` 等数值字段 | +| [`internal/adapter/onebot11/action.go`](internal/adapter/onebot11/action.go) | `Duration int64`, `Delay int` 等时长字段 | +| [`internal/adapter/milky/types.go`](internal/adapter/milky/types.go) | `ExpireTime *int64`, `ShutUpEndTime *int64` 等时间字段 | +| [`internal/protocol/event.go`](internal/protocol/event.go) | `GroupID string`, `UserID string` 等可选字段 | + +#### 示例修改 + +```go +// 修改前 +type SetGroupBanAction struct { + GroupID int64 `json:"group_id"` + Duration int64 `json:"duration,omitempty"` // 0 表示取消禁言 +} + +// 修改后 +type SetGroupBanAction struct { + GroupID int64 `json:"group_id"` + Duration int64 `json:"duration,omitzero"` // 0 表示取消禁言 +} +``` + +--- + +### 3. 中优先级:`wg.Go()` 替换 `wg.Add(1)` + goroutine + +**Go 版本要求**: 1.25+ + +项目中发现 **3 处** 可使用更简洁的 `wg.Go()` 模式。 + +#### 涉及文件 + +| 文件 | 行号 | 当前代码 | +|-----|-----|---------| +| [`internal/engine/scheduler.go`](internal/engine/scheduler.go:332) | 332-333 | `j.wg.Add(1)` + `go j.run()` | +| [`internal/engine/scheduler.go`](internal/engine/scheduler.go:431) | 431-432 | `j.wg.Add(1)` + `go j.run()` | +| [`internal/engine/eventbus.go`](internal/engine/eventbus.go:66) | 66-67 | `eb.wg.Add(1)` + `go eb.dispatch()` | + +#### 示例修改 + +```go +// 修改前 (scheduler.go:332-333) +j.wg.Add(1) +go j.run() + +// 修改后 +j.wg.Go(j.run) +``` + +**注意**: 需要调整 `run()` 方法的签名,移除 `defer j.wg.Done()` 调用。 + +--- + +### 4. 中优先级:使用 `slices` 和 `maps` 包 + +**Go 版本要求**: 1.21+ + +#### 可优化的代码模式 + +项目中存在一些可以优化的 slice 操作: + +| 文件 | 当前模式 | 建议优化 | +|-----|---------|---------| +| [`internal/engine/eventbus.go:222-224`](internal/engine/eventbus.go:222) | 手动复制切片 | `slices.Clone()` | +| [`internal/engine/dispatcher.go:240-242`](internal/engine/dispatcher.go:240) | 手动复制切片 | `slices.Clone()` | + +#### 示例修改 + +```go +// 修改前 +subsCopy := make([]*Subscription, len(subs)) +copy(subsCopy, subs) + +// 修改后 +subsCopy := slices.Clone(subs) +``` + +--- + +### 5. 低优先级:其他现代 Go 特性 + +#### 5.1 使用 `time.Since` 替换 `time.Now().Sub()` + +项目已正确使用 `time.Since`,无需修改。 + +#### 5.2 使用 `min`/`max` 内置函数 + +项目中未发现明显的 if-else 比较可替换为 `min`/`max`。 + +#### 5.3 使用 `cmp.Or` 替换空值检查 + +项目中未发现明显的空字符串检查可替换为 `cmp.Or`。 + +--- + +## 执行计划 + +### 阶段 1:升级 Go 版本 + +1. 更新 [`go.mod`](go.mod) 文件: + ```go + go 1.26.1 + toolchain go1.26.1 + ``` + +2. 运行 `go mod tidy` 更新依赖 + +### 阶段 2:高优先级修改 + +1. 批量替换 `interface{}` → `any`(134 处) +2. 更新 `omitempty` → `omitzero`(32+ 处) + +### 阶段 3:中优先级修改 + +1. 重构 `scheduler.go` 和 `eventbus.go` 使用 `wg.Go()` +2. 使用 `slices.Clone()` 优化切片复制 + +### 阶段 4:验证 + +1. 运行 `go build ./...` 确保编译通过 +2. 运行 `go test ./...` 确保测试通过 +3. 运行 `go vet ./...` 进行静态分析 + +--- + +## 风险评估 + +| 风险 | 等级 | 缓解措施 | +|-----|-----|---------| +| `any` 替换导致类型推断问题 | 低 | 编译器会捕获所有类型错误 | +| `omitzero` 行为差异 | 中 | 仔细测试 JSON 序列化/反序列化 | +| `wg.Go()` API 不兼容 | 低 | Go 1.25+ 已稳定支持 | + +--- + +## 预期收益 + +1. **代码可读性提升**:`any` 比 `interface{}` 更简洁 +2. **JSON 序列化正确性**:`omitzero` 正确处理零值 +3. **并发代码简化**:`wg.Go()` 减少样板代码 +4. **性能优化**:`slices.Clone()` 可能使用内部优化 + +--- + +## 总结 + +本升级计划将项目从 Go 1.24 升级到 Go 1.26.1,并应用现代 Go 语法特性。主要修改集中在: + +1. **134 处** `interface{}` → `any` 替换 +2. **32+ 处** `omitempty` → `omitzero` 更新 +3. **3 处** `wg.Go()` 简化 +4. **多处** `slices.Clone()` 优化 + +建议按阶段逐步实施,每个阶段完成后进行验证,确保代码质量和功能稳定性。