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)
This commit is contained in:
674
.kilocode/skills/golang-patterns/SKILL.md
Normal file
674
.kilocode/skills/golang-patterns/SKILL.md
Normal file
@@ -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 代码应该以最好的方式显得“乏味”——可预测、一致且易于理解。如有疑问,保持简单。
|
||||||
720
.kilocode/skills/golang-testing/SKILL.md
Normal file
720
.kilocode/skills/golang-testing/SKILL.md
Normal file
@@ -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.
|
||||||
291
.kilocode/skills/use-modern-go/SKILL.md
Normal file
291
.kilocode/skills/use-modern-go/SKILL.md
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -48,10 +48,10 @@ enabled = true
|
|||||||
|
|
||||||
[bots.onebot11]
|
[bots.onebot11]
|
||||||
connection_type = "ws"
|
connection_type = "ws"
|
||||||
self_id = "123456789"
|
self_id = "3177182228"
|
||||||
nickname = "TestBot"
|
nickname = "TestBot"
|
||||||
ws_url = "ws://192.168.10.148:3001"
|
ws_url = "ws://127.0.0.1:3001"
|
||||||
access_token = "hDeu66@_DDhgMf<9"
|
access_token = "]W4cjp#9W3l:p4_r"
|
||||||
timeout = 30
|
timeout = 30
|
||||||
heartbeat = 30
|
heartbeat = 30
|
||||||
reconnect_interval = 5
|
reconnect_interval = 5
|
||||||
|
|||||||
16
go.mod
16
go.mod
@@ -1,12 +1,10 @@
|
|||||||
module cellbot
|
module cellbot
|
||||||
|
|
||||||
go 1.24.0
|
go 1.26.1
|
||||||
|
|
||||||
toolchain go1.24.2
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.3.2
|
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/fasthttp/websocket v1.5.12
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
@@ -45,18 +43,18 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.4.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/klauspost/compress v1.17.11 // indirect
|
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/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
go.uber.org/dig v1.17.0 // indirect
|
go.uber.org/dig v1.17.0 // indirect
|
||||||
go.uber.org/multierr v1.10.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/net v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.34.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
24
go.sum
24
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/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 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
|
||||||
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
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.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||||
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
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 h1:UQ4AU+BGti3Sy/aLU8KVseYKNALcX9UXY6DfpwQ6J8E=
|
||||||
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
|
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=
|
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/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 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
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 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
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=
|
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/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 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
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.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
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 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ func (a *Adapter) handleEvents(eventChan <-chan []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendAction 发送动作
|
// 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
|
// 调用 API
|
||||||
resp, err := a.apiClient.Call(ctx, string(action.GetType()), action.GetParams())
|
resp, err := a.apiClient.Call(ctx, string(action.GetType()), action.GetParams())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -303,8 +303,8 @@ func (a *Adapter) IsConnected() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetStats 获取统计信息
|
// GetStats 获取统计信息
|
||||||
func (a *Adapter) GetStats() map[string]interface{} {
|
func (a *Adapter) GetStats() map[string]any {
|
||||||
stats := map[string]interface{}{
|
stats := map[string]any{
|
||||||
"protocol": "milky",
|
"protocol": "milky",
|
||||||
"self_id": a.selfID,
|
"self_id": a.selfID,
|
||||||
"event_mode": a.config.EventMode,
|
"event_mode": a.config.EventMode,
|
||||||
@@ -320,7 +320,7 @@ func (a *Adapter) GetStats() map[string]interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CallAPI 直接调用 API(提供给 Bot 使用)
|
// 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)
|
return a.apiClient.Call(ctx, endpoint, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func NewAPIClient(baseURL, accessToken string, timeout time.Duration, retryCount
|
|||||||
// endpoint: API 端点名称(如 "send_private_message")
|
// endpoint: API 端点名称(如 "send_private_message")
|
||||||
// input: 输入参数(会被序列化为 JSON)
|
// 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 inputData []byte
|
||||||
var err error
|
var err error
|
||||||
@@ -126,7 +126,7 @@ func (c *APIClient) doRequest(ctx context.Context, url string, inputData []byte)
|
|||||||
// CallWithoutRetry 调用 API(不重试)
|
// CallWithoutRetry 调用 API(不重试)
|
||||||
// 注意:pkg/net.HTTPClient 的通用请求已经内置了重试机制
|
// 注意: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)
|
return c.Call(ctx, endpoint, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ func (b *Bot) Disconnect(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendAction 发送动作
|
// 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 {
|
if b.status != protocol.BotStatusRunning {
|
||||||
return nil, fmt.Errorf("bot is not running")
|
return nil, fmt.Errorf("bot is not running")
|
||||||
}
|
}
|
||||||
@@ -119,8 +119,8 @@ func (b *Bot) GetAdapter() *Adapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetInfo 获取机器人信息
|
// GetInfo 获取机器人信息
|
||||||
func (b *Bot) GetInfo() map[string]interface{} {
|
func (b *Bot) GetInfo() map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"id": b.id,
|
"id": b.id,
|
||||||
"protocol": "milky",
|
"protocol": "milky",
|
||||||
"status": b.status,
|
"status": b.status,
|
||||||
@@ -144,7 +144,7 @@ func (b *Bot) SetStatus(status protocol.BotStatus) {
|
|||||||
|
|
||||||
// SendPrivateMessage 发送私聊消息
|
// SendPrivateMessage 发送私聊消息
|
||||||
func (b *Bot) SendPrivateMessage(ctx context.Context, userID int64, segments []OutgoingSegment) (*APIResponse, error) {
|
func (b *Bot) SendPrivateMessage(ctx context.Context, userID int64, segments []OutgoingSegment) (*APIResponse, error) {
|
||||||
params := map[string]interface{}{
|
params := map[string]any{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"segments": segments,
|
"segments": segments,
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ func (b *Bot) SendPrivateMessage(ctx context.Context, userID int64, segments []O
|
|||||||
|
|
||||||
// SendGroupMessage 发送群消息
|
// SendGroupMessage 发送群消息
|
||||||
func (b *Bot) SendGroupMessage(ctx context.Context, groupID int64, segments []OutgoingSegment) (*APIResponse, error) {
|
func (b *Bot) SendGroupMessage(ctx context.Context, groupID int64, segments []OutgoingSegment) (*APIResponse, error) {
|
||||||
params := map[string]interface{}{
|
params := map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
"segments": segments,
|
"segments": segments,
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ func (b *Bot) SendGroupMessage(ctx context.Context, groupID int64, segments []Ou
|
|||||||
|
|
||||||
// SendTempMessage 发送临时消息
|
// SendTempMessage 发送临时消息
|
||||||
func (b *Bot) SendTempMessage(ctx context.Context, groupID, userID int64, segments []OutgoingSegment) (*APIResponse, error) {
|
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,
|
"group_id": groupID,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"segments": segments,
|
"segments": segments,
|
||||||
@@ -172,7 +172,7 @@ func (b *Bot) SendTempMessage(ctx context.Context, groupID, userID int64, segmen
|
|||||||
|
|
||||||
// RecallMessage 撤回消息
|
// RecallMessage 撤回消息
|
||||||
func (b *Bot) RecallMessage(ctx context.Context, messageScene string, peerID, messageSeq int64) (*APIResponse, error) {
|
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,
|
"message_scene": messageScene,
|
||||||
"peer_id": peerID,
|
"peer_id": peerID,
|
||||||
"message_seq": messageSeq,
|
"message_seq": messageSeq,
|
||||||
@@ -192,7 +192,7 @@ func (b *Bot) GetGroupList(ctx context.Context) (*APIResponse, error) {
|
|||||||
|
|
||||||
// GetGroupMemberList 获取群成员列表
|
// GetGroupMemberList 获取群成员列表
|
||||||
func (b *Bot) GetGroupMemberList(ctx context.Context, groupID int64) (*APIResponse, error) {
|
func (b *Bot) GetGroupMemberList(ctx context.Context, groupID int64) (*APIResponse, error) {
|
||||||
params := map[string]interface{}{
|
params := map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
}
|
}
|
||||||
return b.adapter.CallAPI(ctx, "get_group_member_list", params)
|
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 获取群成员信息
|
// GetGroupMemberInfo 获取群成员信息
|
||||||
func (b *Bot) GetGroupMemberInfo(ctx context.Context, groupID, userID int64) (*APIResponse, error) {
|
func (b *Bot) GetGroupMemberInfo(ctx context.Context, groupID, userID int64) (*APIResponse, error) {
|
||||||
params := map[string]interface{}{
|
params := map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
}
|
}
|
||||||
@@ -209,7 +209,7 @@ func (b *Bot) GetGroupMemberInfo(ctx context.Context, groupID, userID int64) (*A
|
|||||||
|
|
||||||
// SetGroupAdmin 设置群管理员
|
// SetGroupAdmin 设置群管理员
|
||||||
func (b *Bot) SetGroupAdmin(ctx context.Context, groupID, userID int64, isSet bool) (*APIResponse, error) {
|
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,
|
"group_id": groupID,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"is_set": isSet,
|
"is_set": isSet,
|
||||||
@@ -219,7 +219,7 @@ func (b *Bot) SetGroupAdmin(ctx context.Context, groupID, userID int64, isSet bo
|
|||||||
|
|
||||||
// SetGroupCard 设置群名片
|
// SetGroupCard 设置群名片
|
||||||
func (b *Bot) SetGroupCard(ctx context.Context, groupID, userID int64, card string) (*APIResponse, error) {
|
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,
|
"group_id": groupID,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"card": card,
|
"card": card,
|
||||||
@@ -229,7 +229,7 @@ func (b *Bot) SetGroupCard(ctx context.Context, groupID, userID int64, card stri
|
|||||||
|
|
||||||
// SetGroupName 设置群名
|
// SetGroupName 设置群名
|
||||||
func (b *Bot) SetGroupName(ctx context.Context, groupID int64, groupName string) (*APIResponse, error) {
|
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_id": groupID,
|
||||||
"group_name": groupName,
|
"group_name": groupName,
|
||||||
}
|
}
|
||||||
@@ -238,7 +238,7 @@ func (b *Bot) SetGroupName(ctx context.Context, groupID int64, groupName string)
|
|||||||
|
|
||||||
// KickGroupMember 踢出群成员
|
// KickGroupMember 踢出群成员
|
||||||
func (b *Bot) KickGroupMember(ctx context.Context, groupID, userID int64, rejectAddRequest bool) (*APIResponse, error) {
|
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,
|
"group_id": groupID,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"reject_add_request": rejectAddRequest,
|
"reject_add_request": rejectAddRequest,
|
||||||
@@ -248,7 +248,7 @@ func (b *Bot) KickGroupMember(ctx context.Context, groupID, userID int64, reject
|
|||||||
|
|
||||||
// MuteGroupMember 禁言群成员
|
// MuteGroupMember 禁言群成员
|
||||||
func (b *Bot) MuteGroupMember(ctx context.Context, groupID, userID int64, duration int32) (*APIResponse, error) {
|
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,
|
"group_id": groupID,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"duration": duration,
|
"duration": duration,
|
||||||
@@ -258,7 +258,7 @@ func (b *Bot) MuteGroupMember(ctx context.Context, groupID, userID int64, durati
|
|||||||
|
|
||||||
// MuteGroupWhole 全体禁言
|
// MuteGroupWhole 全体禁言
|
||||||
func (b *Bot) MuteGroupWhole(ctx context.Context, groupID int64, isMute bool) (*APIResponse, error) {
|
func (b *Bot) MuteGroupWhole(ctx context.Context, groupID int64, isMute bool) (*APIResponse, error) {
|
||||||
params := map[string]interface{}{
|
params := map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
"is_mute": isMute,
|
"is_mute": isMute,
|
||||||
}
|
}
|
||||||
@@ -267,7 +267,7 @@ func (b *Bot) MuteGroupWhole(ctx context.Context, groupID int64, isMute bool) (*
|
|||||||
|
|
||||||
// LeaveGroup 退出群
|
// LeaveGroup 退出群
|
||||||
func (b *Bot) LeaveGroup(ctx context.Context, groupID int64) (*APIResponse, error) {
|
func (b *Bot) LeaveGroup(ctx context.Context, groupID int64) (*APIResponse, error) {
|
||||||
params := map[string]interface{}{
|
params := map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
}
|
}
|
||||||
return b.adapter.CallAPI(ctx, "leave_group", params)
|
return b.adapter.CallAPI(ctx, "leave_group", params)
|
||||||
@@ -275,7 +275,7 @@ func (b *Bot) LeaveGroup(ctx context.Context, groupID int64) (*APIResponse, erro
|
|||||||
|
|
||||||
// HandleFriendRequest 处理好友请求
|
// HandleFriendRequest 处理好友请求
|
||||||
func (b *Bot) HandleFriendRequest(ctx context.Context, initiatorUID string, accept bool) (*APIResponse, error) {
|
func (b *Bot) HandleFriendRequest(ctx context.Context, initiatorUID string, accept bool) (*APIResponse, error) {
|
||||||
params := map[string]interface{}{
|
params := map[string]any{
|
||||||
"initiator_uid": initiatorUID,
|
"initiator_uid": initiatorUID,
|
||||||
"accept": accept,
|
"accept": accept,
|
||||||
}
|
}
|
||||||
@@ -284,7 +284,7 @@ func (b *Bot) HandleFriendRequest(ctx context.Context, initiatorUID string, acce
|
|||||||
|
|
||||||
// HandleGroupJoinRequest 处理入群申请
|
// HandleGroupJoinRequest 处理入群申请
|
||||||
func (b *Bot) HandleGroupJoinRequest(ctx context.Context, groupID, notificationSeq int64, accept bool, rejectReason string) (*APIResponse, error) {
|
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,
|
"group_id": groupID,
|
||||||
"notification_seq": notificationSeq,
|
"notification_seq": notificationSeq,
|
||||||
"accept": accept,
|
"accept": accept,
|
||||||
@@ -295,7 +295,7 @@ func (b *Bot) HandleGroupJoinRequest(ctx context.Context, groupID, notificationS
|
|||||||
|
|
||||||
// HandleGroupInvitation 处理群邀请
|
// HandleGroupInvitation 处理群邀请
|
||||||
func (b *Bot) HandleGroupInvitation(ctx context.Context, groupID, invitationSeq int64, accept bool) (*APIResponse, error) {
|
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,
|
"group_id": groupID,
|
||||||
"invitation_seq": invitationSeq,
|
"invitation_seq": invitationSeq,
|
||||||
"accept": accept,
|
"accept": accept,
|
||||||
@@ -305,7 +305,7 @@ func (b *Bot) HandleGroupInvitation(ctx context.Context, groupID, invitationSeq
|
|||||||
|
|
||||||
// UploadFile 上传文件
|
// UploadFile 上传文件
|
||||||
func (b *Bot) UploadFile(ctx context.Context, fileType, filePath string) (*APIResponse, error) {
|
func (b *Bot) UploadFile(ctx context.Context, fileType, filePath string) (*APIResponse, error) {
|
||||||
params := map[string]interface{}{
|
params := map[string]any{
|
||||||
"file_type": fileType,
|
"file_type": fileType,
|
||||||
"file_path": filePath,
|
"file_path": filePath,
|
||||||
}
|
}
|
||||||
@@ -314,7 +314,7 @@ func (b *Bot) UploadFile(ctx context.Context, fileType, filePath string) (*APIRe
|
|||||||
|
|
||||||
// GetFile 获取文件
|
// GetFile 获取文件
|
||||||
func (b *Bot) GetFile(ctx context.Context, fileID string) (*APIResponse, error) {
|
func (b *Bot) GetFile(ctx context.Context, fileID string) (*APIResponse, error) {
|
||||||
params := map[string]interface{}{
|
params := map[string]any{
|
||||||
"file_id": fileID,
|
"file_id": fileID,
|
||||||
}
|
}
|
||||||
return b.adapter.CallAPI(ctx, "get_file", params)
|
return b.adapter.CallAPI(ctx, "get_file", params)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func (c *EventConverter) convertMessageEvent(milkyEvent *Event) (protocol.Event,
|
|||||||
SubType: "",
|
SubType: "",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"peer_id": msgData.PeerID,
|
"peer_id": msgData.PeerID,
|
||||||
"message_seq": msgData.MessageSeq,
|
"message_seq": msgData.MessageSeq,
|
||||||
"sender_id": msgData.SenderID,
|
"sender_id": msgData.SenderID,
|
||||||
@@ -145,7 +145,7 @@ func (c *EventConverter) convertFriendRequestEvent(milkyEvent *Event) (protocol.
|
|||||||
SubType: "",
|
SubType: "",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"initiator_id": data.InitiatorID,
|
"initiator_id": data.InitiatorID,
|
||||||
"initiator_uid": data.InitiatorUID,
|
"initiator_uid": data.InitiatorUID,
|
||||||
"comment": data.Comment,
|
"comment": data.Comment,
|
||||||
@@ -178,7 +178,7 @@ func (c *EventConverter) convertGroupJoinRequestEvent(milkyEvent *Event) (protoc
|
|||||||
SubType: "add",
|
SubType: "add",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"group_id": data.GroupID,
|
"group_id": data.GroupID,
|
||||||
"notification_seq": data.NotificationSeq,
|
"notification_seq": data.NotificationSeq,
|
||||||
"is_filtered": data.IsFiltered,
|
"is_filtered": data.IsFiltered,
|
||||||
@@ -212,7 +212,7 @@ func (c *EventConverter) convertGroupInvitedJoinRequestEvent(milkyEvent *Event)
|
|||||||
SubType: "invite",
|
SubType: "invite",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"group_id": data.GroupID,
|
"group_id": data.GroupID,
|
||||||
"notification_seq": data.NotificationSeq,
|
"notification_seq": data.NotificationSeq,
|
||||||
"initiator_id": data.InitiatorID,
|
"initiator_id": data.InitiatorID,
|
||||||
@@ -245,7 +245,7 @@ func (c *EventConverter) convertGroupInvitationEvent(milkyEvent *Event) (protoco
|
|||||||
SubType: "invite_self",
|
SubType: "invite_self",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"group_id": data.GroupID,
|
"group_id": data.GroupID,
|
||||||
"invitation_seq": data.InvitationSeq,
|
"invitation_seq": data.InvitationSeq,
|
||||||
"initiator_id": data.InitiatorID,
|
"initiator_id": data.InitiatorID,
|
||||||
@@ -277,7 +277,7 @@ func (c *EventConverter) convertMessageRecallEvent(milkyEvent *Event) (protocol.
|
|||||||
SubType: data.MessageScene,
|
SubType: data.MessageScene,
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"message_scene": data.MessageScene,
|
"message_scene": data.MessageScene,
|
||||||
"peer_id": data.PeerID,
|
"peer_id": data.PeerID,
|
||||||
"message_seq": data.MessageSeq,
|
"message_seq": data.MessageSeq,
|
||||||
@@ -310,7 +310,7 @@ func (c *EventConverter) convertBotOfflineEvent(milkyEvent *Event) (protocol.Eve
|
|||||||
SubType: "",
|
SubType: "",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"reason": data.Reason,
|
"reason": data.Reason,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -337,7 +337,7 @@ func (c *EventConverter) convertFriendNudgeEvent(milkyEvent *Event) (protocol.Ev
|
|||||||
SubType: "",
|
SubType: "",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
UserID: strconv.FormatInt(data.UserID, 10),
|
UserID: strconv.FormatInt(data.UserID, 10),
|
||||||
}
|
}
|
||||||
@@ -362,7 +362,7 @@ func (c *EventConverter) convertFriendFileUploadEvent(milkyEvent *Event) (protoc
|
|||||||
SubType: "",
|
SubType: "",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
UserID: strconv.FormatInt(data.UserID, 10),
|
UserID: strconv.FormatInt(data.UserID, 10),
|
||||||
}
|
}
|
||||||
@@ -392,7 +392,7 @@ func (c *EventConverter) convertGroupAdminChangeEvent(milkyEvent *Event) (protoc
|
|||||||
SubType: subType,
|
SubType: subType,
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
GroupID: strconv.FormatInt(data.GroupID, 10),
|
GroupID: strconv.FormatInt(data.GroupID, 10),
|
||||||
UserID: strconv.FormatInt(data.UserID, 10),
|
UserID: strconv.FormatInt(data.UserID, 10),
|
||||||
@@ -423,7 +423,7 @@ func (c *EventConverter) convertGroupEssenceMessageChangeEvent(milkyEvent *Event
|
|||||||
SubType: subType,
|
SubType: subType,
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
GroupID: strconv.FormatInt(data.GroupID, 10),
|
GroupID: strconv.FormatInt(data.GroupID, 10),
|
||||||
}
|
}
|
||||||
@@ -448,7 +448,7 @@ func (c *EventConverter) convertGroupMemberIncreaseEvent(milkyEvent *Event) (pro
|
|||||||
SubType: "",
|
SubType: "",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
GroupID: strconv.FormatInt(data.GroupID, 10),
|
GroupID: strconv.FormatInt(data.GroupID, 10),
|
||||||
UserID: strconv.FormatInt(data.UserID, 10),
|
UserID: strconv.FormatInt(data.UserID, 10),
|
||||||
@@ -478,7 +478,7 @@ func (c *EventConverter) convertGroupMemberDecreaseEvent(milkyEvent *Event) (pro
|
|||||||
SubType: "",
|
SubType: "",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
GroupID: strconv.FormatInt(data.GroupID, 10),
|
GroupID: strconv.FormatInt(data.GroupID, 10),
|
||||||
UserID: strconv.FormatInt(data.UserID, 10),
|
UserID: strconv.FormatInt(data.UserID, 10),
|
||||||
@@ -508,7 +508,7 @@ func (c *EventConverter) convertGroupNameChangeEvent(milkyEvent *Event) (protoco
|
|||||||
SubType: "",
|
SubType: "",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
GroupID: strconv.FormatInt(data.GroupID, 10),
|
GroupID: strconv.FormatInt(data.GroupID, 10),
|
||||||
Operator: strconv.FormatInt(data.OperatorID, 10),
|
Operator: strconv.FormatInt(data.OperatorID, 10),
|
||||||
@@ -539,7 +539,7 @@ func (c *EventConverter) convertGroupMessageReactionEvent(milkyEvent *Event) (pr
|
|||||||
SubType: subType,
|
SubType: subType,
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
GroupID: strconv.FormatInt(data.GroupID, 10),
|
GroupID: strconv.FormatInt(data.GroupID, 10),
|
||||||
UserID: strconv.FormatInt(data.UserID, 10),
|
UserID: strconv.FormatInt(data.UserID, 10),
|
||||||
@@ -570,7 +570,7 @@ func (c *EventConverter) convertGroupMuteEvent(milkyEvent *Event) (protocol.Even
|
|||||||
SubType: subType,
|
SubType: subType,
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
GroupID: strconv.FormatInt(data.GroupID, 10),
|
GroupID: strconv.FormatInt(data.GroupID, 10),
|
||||||
UserID: strconv.FormatInt(data.UserID, 10),
|
UserID: strconv.FormatInt(data.UserID, 10),
|
||||||
@@ -602,7 +602,7 @@ func (c *EventConverter) convertGroupWholeMuteEvent(milkyEvent *Event) (protocol
|
|||||||
SubType: subType,
|
SubType: subType,
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
GroupID: strconv.FormatInt(data.GroupID, 10),
|
GroupID: strconv.FormatInt(data.GroupID, 10),
|
||||||
Operator: strconv.FormatInt(data.OperatorID, 10),
|
Operator: strconv.FormatInt(data.OperatorID, 10),
|
||||||
@@ -628,7 +628,7 @@ func (c *EventConverter) convertGroupNudgeEvent(milkyEvent *Event) (protocol.Eve
|
|||||||
SubType: "",
|
SubType: "",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
GroupID: strconv.FormatInt(data.GroupID, 10),
|
GroupID: strconv.FormatInt(data.GroupID, 10),
|
||||||
UserID: strconv.FormatInt(data.SenderID, 10),
|
UserID: strconv.FormatInt(data.SenderID, 10),
|
||||||
@@ -654,7 +654,7 @@ func (c *EventConverter) convertGroupFileUploadEvent(milkyEvent *Event) (protoco
|
|||||||
SubType: "",
|
SubType: "",
|
||||||
Timestamp: milkyEvent.Time,
|
Timestamp: milkyEvent.Time,
|
||||||
SelfID: selfID,
|
SelfID: selfID,
|
||||||
Data: map[string]interface{}(milkyEvent.Data),
|
Data: map[string]any(milkyEvent.Data),
|
||||||
},
|
},
|
||||||
GroupID: strconv.FormatInt(data.GroupID, 10),
|
GroupID: strconv.FormatInt(data.GroupID, 10),
|
||||||
UserID: strconv.FormatInt(data.UserID, 10),
|
UserID: strconv.FormatInt(data.UserID, 10),
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ type Boolean = bool
|
|||||||
// IncomingSegment 接收消息段(联合类型)
|
// IncomingSegment 接收消息段(联合类型)
|
||||||
type IncomingSegment struct {
|
type IncomingSegment struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Data map[string]interface{} `json:"data"`
|
Data map[string]any `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// OutgoingSegment 发送消息段(联合类型)
|
// OutgoingSegment 发送消息段(联合类型)
|
||||||
type OutgoingSegment struct {
|
type OutgoingSegment struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Data map[string]interface{} `json:"data"`
|
Data map[string]any `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IncomingForwardedMessage 接收转发消息
|
// IncomingForwardedMessage 接收转发消息
|
||||||
@@ -321,7 +321,7 @@ type Event struct {
|
|||||||
EventType string `json:"event_type"`
|
EventType string `json:"event_type"`
|
||||||
Time int64 `json:"time"`
|
Time int64 `json:"time"`
|
||||||
SelfID int64 `json:"self_id"`
|
SelfID int64 `json:"self_id"`
|
||||||
Data map[string]interface{} `json:"data"`
|
Data map[string]any `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 事件类型常量
|
// 事件类型常量
|
||||||
@@ -355,7 +355,7 @@ const (
|
|||||||
type APIResponse struct {
|
type APIResponse struct {
|
||||||
Status string `json:"status"` // ok, failed
|
Status string `json:"status"` // ok, failed
|
||||||
RetCode int `json:"retcode"`
|
RetCode int `json:"retcode"`
|
||||||
Data map[string]interface{} `json:"data,omitempty"`
|
Data map[string]any `json:"data,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,14 +83,14 @@ func ConvertAction(action protocol.Action) string {
|
|||||||
// SendPrivateMessageAction 发送私聊消息动作
|
// SendPrivateMessageAction 发送私聊消息动作
|
||||||
type SendPrivateMessageAction struct {
|
type SendPrivateMessageAction struct {
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
Message interface{} `json:"message"`
|
Message any `json:"message"`
|
||||||
AutoEscape bool `json:"auto_escape,omitempty"`
|
AutoEscape bool `json:"auto_escape,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendGroupMessageAction 发送群消息动作
|
// SendGroupMessageAction 发送群消息动作
|
||||||
type SendGroupMessageAction struct {
|
type SendGroupMessageAction struct {
|
||||||
GroupID int64 `json:"group_id"`
|
GroupID int64 `json:"group_id"`
|
||||||
Message interface{} `json:"message"`
|
Message any `json:"message"`
|
||||||
AutoEscape bool `json:"auto_escape,omitempty"`
|
AutoEscape bool `json:"auto_escape,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ type SetRestartAction struct {
|
|||||||
type ActionResponse struct {
|
type ActionResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
RetCode int `json:"retcode"`
|
RetCode int `json:"retcode"`
|
||||||
Data map[string]interface{} `json:"data,omitempty"`
|
Data map[string]any `json:"data,omitempty"`
|
||||||
Echo string `json:"echo,omitempty"`
|
Echo string `json:"echo,omitempty"`
|
||||||
Message string `json:"message,omitempty"`
|
Message string `json:"message,omitempty"`
|
||||||
Wording string `json:"wording,omitempty"`
|
Wording string `json:"wording,omitempty"`
|
||||||
@@ -245,7 +245,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// BuildActionRequest 构建动作请求
|
// 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{
|
return &OB11Action{
|
||||||
Action: action,
|
Action: action,
|
||||||
Params: params,
|
Params: params,
|
||||||
@@ -254,8 +254,8 @@ func BuildActionRequest(action string, params map[string]interface{}, echo strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildSendPrivateMsg 构建发送私聊消息请求
|
// BuildSendPrivateMsg 构建发送私聊消息请求
|
||||||
func BuildSendPrivateMsg(userID int64, message interface{}, autoEscape bool) map[string]interface{} {
|
func BuildSendPrivateMsg(userID int64, message any, autoEscape bool) map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"message": message,
|
"message": message,
|
||||||
"auto_escape": autoEscape,
|
"auto_escape": autoEscape,
|
||||||
@@ -263,8 +263,8 @@ func BuildSendPrivateMsg(userID int64, message interface{}, autoEscape bool) map
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildSendGroupMsg 构建发送群消息请求
|
// BuildSendGroupMsg 构建发送群消息请求
|
||||||
func BuildSendGroupMsg(groupID int64, message interface{}, autoEscape bool) map[string]interface{} {
|
func BuildSendGroupMsg(groupID int64, message any, autoEscape bool) map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
"message": message,
|
"message": message,
|
||||||
"auto_escape": autoEscape,
|
"auto_escape": autoEscape,
|
||||||
@@ -272,15 +272,15 @@ func BuildSendGroupMsg(groupID int64, message interface{}, autoEscape bool) map[
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildDeleteMsg 构建撤回消息请求
|
// BuildDeleteMsg 构建撤回消息请求
|
||||||
func BuildDeleteMsg(messageID int32) map[string]interface{} {
|
func BuildDeleteMsg(messageID int32) map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"message_id": messageID,
|
"message_id": messageID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildSetGroupBan 构建群组禁言请求
|
// BuildSetGroupBan 构建群组禁言请求
|
||||||
func BuildSetGroupBan(groupID, userID int64, duration int64) map[string]interface{} {
|
func BuildSetGroupBan(groupID, userID int64, duration int64) map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"duration": duration,
|
"duration": duration,
|
||||||
@@ -288,8 +288,8 @@ func BuildSetGroupBan(groupID, userID int64, duration int64) map[string]interfac
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildSetGroupKick 构建群组踢人请求
|
// BuildSetGroupKick 构建群组踢人请求
|
||||||
func BuildSetGroupKick(groupID, userID int64, rejectAddRequest bool) map[string]interface{} {
|
func BuildSetGroupKick(groupID, userID int64, rejectAddRequest bool) map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"reject_add_request": rejectAddRequest,
|
"reject_add_request": rejectAddRequest,
|
||||||
@@ -297,8 +297,8 @@ func BuildSetGroupKick(groupID, userID int64, rejectAddRequest bool) map[string]
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildSetGroupCard 构建设置群名片请求
|
// BuildSetGroupCard 构建设置群名片请求
|
||||||
func BuildSetGroupCard(groupID, userID int64, card string) map[string]interface{} {
|
func BuildSetGroupCard(groupID, userID int64, card string) map[string]any {
|
||||||
return map[string]interface{}{
|
return map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"card": card,
|
"card": card,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package onebot11
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -28,6 +29,10 @@ type Adapter struct {
|
|||||||
wsConnection *net.WebSocketConnection
|
wsConnection *net.WebSocketConnection
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
// 消息去重
|
||||||
|
processedMessages map[int64]time.Time
|
||||||
|
messageMu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config OneBot11配置
|
// Config OneBot11配置
|
||||||
@@ -73,8 +78,12 @@ func NewAdapter(config *Config, logger *zap.Logger, wsManager *net.WebSocketMana
|
|||||||
selfID: config.SelfID,
|
selfID: config.SelfID,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
processedMessages: make(map[int64]time.Time),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启动定期清理过期消息的 goroutine
|
||||||
|
go adapter.cleanupProcessedMessages()
|
||||||
|
|
||||||
// 如果使用HTTP连接,初始化HTTP客户端
|
// 如果使用HTTP连接,初始化HTTP客户端
|
||||||
if config.ConnectionType == "http" && config.HTTPUrl != "" {
|
if config.ConnectionType == "http" && config.HTTPUrl != "" {
|
||||||
adapter.httpClient = NewHTTPClient(config.HTTPUrl, config.AccessToken, timeout, logger)
|
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
|
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 获取协议名称
|
// Name 获取协议名称
|
||||||
func (a *Adapter) Name() string {
|
func (a *Adapter) Name() string {
|
||||||
return "OneBot"
|
return "OneBot"
|
||||||
@@ -174,7 +220,7 @@ func (a *Adapter) GetSelfID() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendAction 发送动作
|
// 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格式
|
// 序列化为OneBot11格式
|
||||||
data, err := a.SerializeAction(action)
|
data, err := a.SerializeAction(action)
|
||||||
if err != nil {
|
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() {
|
for k, v := range action.GetParams() {
|
||||||
// 检查是否是消息链
|
// 检查是否是消息链
|
||||||
if k == "message" {
|
if k == "message" {
|
||||||
@@ -259,10 +305,10 @@ func (a *Adapter) connectWebSocket(ctx context.Context) error {
|
|||||||
zap.String("url", a.config.WSUrl),
|
zap.String("url", a.config.WSUrl),
|
||||||
zap.Bool("has_token", a.config.AccessToken != ""))
|
zap.Bool("has_token", a.config.AccessToken != ""))
|
||||||
|
|
||||||
// 添加访问令牌到URL
|
// 添加访问令牌到URL(需要对token进行URL编码以处理特殊字符)
|
||||||
wsURL := a.config.WSUrl
|
wsURL := a.config.WSUrl
|
||||||
if a.config.AccessToken != "" {
|
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")
|
a.logger.Debug("Added access token to WebSocket URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,7 +388,7 @@ func (a *Adapter) connectHTTPPost(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sendActionWebSocket 通过WebSocket发送动作
|
// 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 {
|
if a.wsConnection == nil {
|
||||||
return nil, fmt.Errorf("websocket connection not established")
|
return nil, fmt.Errorf("websocket connection not established")
|
||||||
}
|
}
|
||||||
@@ -383,7 +429,7 @@ func (a *Adapter) sendActionWebSocket(data []byte) (map[string]interface{}, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sendActionHTTP 通过HTTP发送动作
|
// 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 {
|
if a.httpClient == nil {
|
||||||
return nil, fmt.Errorf("http client not initialized")
|
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)])))
|
zap.String("preview", string(message[:min(len(message), 200)])))
|
||||||
|
|
||||||
// 尝试解析为响应(先检查是否有 echo 字段)
|
// 尝试解析为响应(先检查是否有 echo 字段)
|
||||||
var tempMap map[string]interface{}
|
var tempMap map[string]any
|
||||||
if err := sonic.Unmarshal(message, &tempMap); err == nil {
|
if err := sonic.Unmarshal(message, &tempMap); err == nil {
|
||||||
if echo, ok := tempMap["echo"].(string); ok && echo != "" {
|
if echo, ok := tempMap["echo"].(string); ok && echo != "" {
|
||||||
// 有 echo 字段,说明是API响应
|
// 有 echo 字段,说明是API响应
|
||||||
@@ -478,6 +524,24 @@ func (a *Adapter) handleWebSocketMessages() {
|
|||||||
continue
|
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",
|
a.logger.Info("Publishing event to event bus",
|
||||||
zap.String("event_type", string(event.GetType())),
|
zap.String("event_type", string(event.GetType())),
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ func NewHTTPClient(baseURL, accessToken string, timeout time.Duration, logger *z
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Call 调用API
|
// 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,
|
"action": action,
|
||||||
"params": params,
|
"params": params,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func (a *Adapter) convertToEvent(raw *RawEvent) (protocol.Event, error) {
|
|||||||
baseEvent := &protocol.BaseEvent{
|
baseEvent := &protocol.BaseEvent{
|
||||||
Timestamp: raw.Time,
|
Timestamp: raw.Time,
|
||||||
SelfID: strconv.FormatInt(raw.SelfID, 10),
|
SelfID: strconv.FormatInt(raw.SelfID, 10),
|
||||||
Data: make(map[string]interface{}),
|
Data: make(map[string]any),
|
||||||
}
|
}
|
||||||
|
|
||||||
switch raw.PostType {
|
switch raw.PostType {
|
||||||
@@ -49,7 +49,7 @@ func (a *Adapter) convertMessageEvent(raw *RawEvent, base *protocol.BaseEvent) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if raw.Sender != nil {
|
if raw.Sender != nil {
|
||||||
senderData := map[string]interface{}{
|
senderData := map[string]any{
|
||||||
"user_id": raw.Sender.UserID,
|
"user_id": raw.Sender.UserID,
|
||||||
"nickname": raw.Sender.Nickname,
|
"nickname": raw.Sender.Nickname,
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ func (a *Adapter) convertMessageEvent(raw *RawEvent, base *protocol.BaseEvent) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if raw.Anonymous != nil {
|
if raw.Anonymous != nil {
|
||||||
base.Data["anonymous"] = map[string]interface{}{
|
base.Data["anonymous"] = map[string]any{
|
||||||
"id": raw.Anonymous.ID,
|
"id": raw.Anonymous.ID,
|
||||||
"name": raw.Anonymous.Name,
|
"name": raw.Anonymous.Name,
|
||||||
"flag": raw.Anonymous.Flag,
|
"flag": raw.Anonymous.Flag,
|
||||||
@@ -111,7 +111,7 @@ func (a *Adapter) convertNoticeEvent(raw *RawEvent, base *protocol.BaseEvent) (p
|
|||||||
case NoticeTypeGroupUpload:
|
case NoticeTypeGroupUpload:
|
||||||
// 文件上传信息
|
// 文件上传信息
|
||||||
if raw.File != nil {
|
if raw.File != nil {
|
||||||
base.Data["file"] = map[string]interface{}{
|
base.Data["file"] = map[string]any{
|
||||||
"id": raw.File.ID,
|
"id": raw.File.ID,
|
||||||
"name": raw.File.Name,
|
"name": raw.File.Name,
|
||||||
"size": raw.File.Size,
|
"size": raw.File.Size,
|
||||||
@@ -156,12 +156,12 @@ func (a *Adapter) convertMetaEvent(raw *RawEvent, base *protocol.BaseEvent) (pro
|
|||||||
base.DetailType = raw.MetaType
|
base.DetailType = raw.MetaType
|
||||||
|
|
||||||
if raw.Status != nil {
|
if raw.Status != nil {
|
||||||
statusData := map[string]interface{}{
|
statusData := map[string]any{
|
||||||
"online": raw.Status.Online,
|
"online": raw.Status.Online,
|
||||||
"good": raw.Status.Good,
|
"good": raw.Status.Good,
|
||||||
}
|
}
|
||||||
if raw.Status.Stat != nil {
|
if raw.Status.Stat != nil {
|
||||||
statusData["stat"] = map[string]interface{}{
|
statusData["stat"] = map[string]any{
|
||||||
"packet_received": raw.Status.Stat.PacketReceived,
|
"packet_received": raw.Status.Stat.PacketReceived,
|
||||||
"packet_sent": raw.Status.Stat.PacketSent,
|
"packet_sent": raw.Status.Stat.PacketSent,
|
||||||
"packet_lost": raw.Status.Stat.PacketLost,
|
"packet_lost": raw.Status.Stat.PacketLost,
|
||||||
@@ -183,7 +183,7 @@ func (a *Adapter) convertMetaEvent(raw *RawEvent, base *protocol.BaseEvent) (pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseMessageSegments 解析消息段
|
// parseMessageSegments 解析消息段
|
||||||
func (a *Adapter) parseMessageSegments(message interface{}) ([]MessageSegment, error) {
|
func (a *Adapter) parseMessageSegments(message any) ([]MessageSegment, error) {
|
||||||
if message == nil {
|
if message == nil {
|
||||||
return nil, fmt.Errorf("message is nil")
|
return nil, fmt.Errorf("message is nil")
|
||||||
}
|
}
|
||||||
@@ -193,7 +193,7 @@ func (a *Adapter) parseMessageSegments(message interface{}) ([]MessageSegment, e
|
|||||||
return []MessageSegment{
|
return []MessageSegment{
|
||||||
{
|
{
|
||||||
Type: SegmentTypeText,
|
Type: SegmentTypeText,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"text": str,
|
"text": str,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -215,7 +215,7 @@ func (a *Adapter) parseMessageSegments(message interface{}) ([]MessageSegment, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildMessage 构建OneBot11消息
|
// BuildMessage 构建OneBot11消息
|
||||||
func BuildMessage(segments []MessageSegment) interface{} {
|
func BuildMessage(segments []MessageSegment) any {
|
||||||
if len(segments) == 0 {
|
if len(segments) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -235,7 +235,7 @@ func BuildTextMessage(text string) []MessageSegment {
|
|||||||
return []MessageSegment{
|
return []MessageSegment{
|
||||||
{
|
{
|
||||||
Type: SegmentTypeText,
|
Type: SegmentTypeText,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"text": text,
|
"text": text,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -247,7 +247,7 @@ func BuildImageMessage(file string) []MessageSegment {
|
|||||||
return []MessageSegment{
|
return []MessageSegment{
|
||||||
{
|
{
|
||||||
Type: SegmentTypeImage,
|
Type: SegmentTypeImage,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"file": file,
|
"file": file,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -258,7 +258,7 @@ func BuildImageMessage(file string) []MessageSegment {
|
|||||||
func BuildAtMessage(userID int64) MessageSegment {
|
func BuildAtMessage(userID int64) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeAt,
|
Type: SegmentTypeAt,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"qq": userID,
|
"qq": userID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -268,7 +268,7 @@ func BuildAtMessage(userID int64) MessageSegment {
|
|||||||
func BuildReplyMessage(messageID int32) MessageSegment {
|
func BuildReplyMessage(messageID int32) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeReply,
|
Type: SegmentTypeReply,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"id": messageID,
|
"id": messageID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ func BuildReplyMessage(messageID int32) MessageSegment {
|
|||||||
func BuildFaceMessage(faceID int) MessageSegment {
|
func BuildFaceMessage(faceID int) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeFace,
|
Type: SegmentTypeFace,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"id": faceID,
|
"id": faceID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -288,7 +288,7 @@ func BuildFaceMessage(faceID int) MessageSegment {
|
|||||||
func BuildRecordMessage(file string) MessageSegment {
|
func BuildRecordMessage(file string) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeRecord,
|
Type: SegmentTypeRecord,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"file": file,
|
"file": file,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -298,7 +298,7 @@ func BuildRecordMessage(file string) MessageSegment {
|
|||||||
func BuildVideoMessage(file string) MessageSegment {
|
func BuildVideoMessage(file string) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeVideo,
|
Type: SegmentTypeVideo,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"file": file,
|
"file": file,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -308,7 +308,7 @@ func BuildVideoMessage(file string) MessageSegment {
|
|||||||
func BuildShareMessage(url, title string) MessageSegment {
|
func BuildShareMessage(url, title string) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeShare,
|
Type: SegmentTypeShare,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"url": url,
|
"url": url,
|
||||||
"title": title,
|
"title": title,
|
||||||
},
|
},
|
||||||
@@ -319,7 +319,7 @@ func BuildShareMessage(url, title string) MessageSegment {
|
|||||||
func BuildLocationMessage(lat, lon float64, title, content string) MessageSegment {
|
func BuildLocationMessage(lat, lon float64, title, content string) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeLocation,
|
Type: SegmentTypeLocation,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"lat": lat,
|
"lat": lat,
|
||||||
"lon": lon,
|
"lon": lon,
|
||||||
"title": title,
|
"title": title,
|
||||||
@@ -332,7 +332,7 @@ func BuildLocationMessage(lat, lon float64, title, content string) MessageSegmen
|
|||||||
func BuildMusicMessage(musicType, musicID string) MessageSegment {
|
func BuildMusicMessage(musicType, musicID string) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeMusic,
|
Type: SegmentTypeMusic,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"type": musicType,
|
"type": musicType,
|
||||||
"id": musicID,
|
"id": musicID,
|
||||||
},
|
},
|
||||||
@@ -343,7 +343,7 @@ func BuildMusicMessage(musicType, musicID string) MessageSegment {
|
|||||||
func BuildCustomMusicMessage(url, audio, title, content, image string) MessageSegment {
|
func BuildCustomMusicMessage(url, audio, title, content, image string) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeMusic,
|
Type: SegmentTypeMusic,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"type": "custom",
|
"type": "custom",
|
||||||
"url": url,
|
"url": url,
|
||||||
"audio": audio,
|
"audio": audio,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ConvertMessageChainToOB11 将通用消息链转换为 OneBot11 格式
|
// ConvertMessageChainToOB11 将通用消息链转换为 OneBot11 格式
|
||||||
func ConvertMessageChainToOB11(chain protocol.MessageChain) interface{} {
|
func ConvertMessageChainToOB11(chain protocol.MessageChain) any {
|
||||||
if len(chain) == 0 {
|
if len(chain) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment {
|
|||||||
// 文本消息段
|
// 文本消息段
|
||||||
return &MessageSegment{
|
return &MessageSegment{
|
||||||
Type: SegmentTypeText,
|
Type: SegmentTypeText,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"text": seg.Data["text"],
|
"text": seg.Data["text"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment {
|
|||||||
userID := seg.Data["user_id"]
|
userID := seg.Data["user_id"]
|
||||||
return &MessageSegment{
|
return &MessageSegment{
|
||||||
Type: SegmentTypeAt,
|
Type: SegmentTypeAt,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"qq": userID,
|
"qq": userID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment {
|
|||||||
}
|
}
|
||||||
return &MessageSegment{
|
return &MessageSegment{
|
||||||
Type: SegmentTypeAt,
|
Type: SegmentTypeAt,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"qq": userID,
|
"qq": userID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment {
|
|||||||
}
|
}
|
||||||
return &MessageSegment{
|
return &MessageSegment{
|
||||||
Type: SegmentTypeImage,
|
Type: SegmentTypeImage,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"file": fileID,
|
"file": fileID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment {
|
|||||||
}
|
}
|
||||||
return &MessageSegment{
|
return &MessageSegment{
|
||||||
Type: SegmentTypeRecord,
|
Type: SegmentTypeRecord,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"file": fileID,
|
"file": fileID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment {
|
|||||||
}
|
}
|
||||||
return &MessageSegment{
|
return &MessageSegment{
|
||||||
Type: SegmentTypeRecord,
|
Type: SegmentTypeRecord,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"file": fileID,
|
"file": fileID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment {
|
|||||||
}
|
}
|
||||||
return &MessageSegment{
|
return &MessageSegment{
|
||||||
Type: SegmentTypeVideo,
|
Type: SegmentTypeVideo,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"file": fileID,
|
"file": fileID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment {
|
|||||||
messageID := seg.Data["message_id"]
|
messageID := seg.Data["message_id"]
|
||||||
return &MessageSegment{
|
return &MessageSegment{
|
||||||
Type: SegmentTypeReply,
|
Type: SegmentTypeReply,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"id": messageID,
|
"id": messageID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment {
|
|||||||
faceID := seg.Data["id"]
|
faceID := seg.Data["id"]
|
||||||
return &MessageSegment{
|
return &MessageSegment{
|
||||||
Type: SegmentTypeFace,
|
Type: SegmentTypeFace,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"id": faceID,
|
"id": faceID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ func convertSegmentToOB11(seg protocol.MessageSegment) *MessageSegment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ConvertOB11ToMessageChain 将 OneBot11 消息段转换为通用消息链
|
// ConvertOB11ToMessageChain 将 OneBot11 消息段转换为通用消息链
|
||||||
func ConvertOB11ToMessageChain(ob11Message interface{}) (protocol.MessageChain, error) {
|
func ConvertOB11ToMessageChain(ob11Message any) (protocol.MessageChain, error) {
|
||||||
chain := protocol.MessageChain{}
|
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 {
|
for _, seg := range segments {
|
||||||
if segMap, ok := seg.(map[string]interface{}); ok {
|
if segMap, ok := seg.(map[string]any); ok {
|
||||||
segType, _ := segMap["type"].(string)
|
segType, _ := segMap["type"].(string)
|
||||||
segData, _ := segMap["data"].(map[string]interface{})
|
segData, _ := segMap["data"].(map[string]any)
|
||||||
genericSeg := convertOB11SegmentToGeneric(MessageSegment{
|
genericSeg := convertOB11SegmentToGeneric(MessageSegment{
|
||||||
Type: segType,
|
Type: segType,
|
||||||
Data: segData,
|
Data: segData,
|
||||||
@@ -211,7 +211,7 @@ func convertOB11SegmentToGeneric(seg MessageSegment) protocol.MessageSegment {
|
|||||||
}
|
}
|
||||||
return protocol.MessageSegment{
|
return protocol.MessageSegment{
|
||||||
Type: protocol.SegmentTypeVoice,
|
Type: protocol.SegmentTypeVoice,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"file_id": fileID,
|
"file_id": fileID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -223,7 +223,7 @@ func convertOB11SegmentToGeneric(seg MessageSegment) protocol.MessageSegment {
|
|||||||
case SegmentTypeFace:
|
case SegmentTypeFace:
|
||||||
return protocol.MessageSegment{
|
return protocol.MessageSegment{
|
||||||
Type: protocol.SegmentTypeFace,
|
Type: protocol.SegmentTypeFace,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"id": seg.Data["id"],
|
"id": seg.Data["id"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ type RawEvent struct {
|
|||||||
MessageID int32 `json:"message_id,omitempty"`
|
MessageID int32 `json:"message_id,omitempty"`
|
||||||
UserID int64 `json:"user_id,omitempty"`
|
UserID int64 `json:"user_id,omitempty"`
|
||||||
GroupID int64 `json:"group_id,omitempty"`
|
GroupID int64 `json:"group_id,omitempty"`
|
||||||
Message interface{} `json:"message,omitempty"`
|
Message any `json:"message,omitempty"`
|
||||||
RawMessage string `json:"raw_message,omitempty"`
|
RawMessage string `json:"raw_message,omitempty"`
|
||||||
Font int32 `json:"font,omitempty"`
|
Font int32 `json:"font,omitempty"`
|
||||||
Sender *Sender `json:"sender,omitempty"`
|
Sender *Sender `json:"sender,omitempty"`
|
||||||
@@ -27,7 +27,7 @@ type RawEvent struct {
|
|||||||
File *FileInfo `json:"file,omitempty"` // 群文件上传信息
|
File *FileInfo `json:"file,omitempty"` // 群文件上传信息
|
||||||
TargetID int64 `json:"target_id,omitempty"` // 戳一戳、红包运气王目标ID
|
TargetID int64 `json:"target_id,omitempty"` // 戳一戳、红包运气王目标ID
|
||||||
HonorType string `json:"honor_type,omitempty"` // 群荣誉类型
|
HonorType string `json:"honor_type,omitempty"` // 群荣誉类型
|
||||||
Extra map[string]interface{} `json:"-"`
|
Extra map[string]any `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sender 发送者信息
|
// Sender 发送者信息
|
||||||
@@ -80,7 +80,7 @@ type Stat struct {
|
|||||||
// OB11Action OneBot11动作
|
// OB11Action OneBot11动作
|
||||||
type OB11Action struct {
|
type OB11Action struct {
|
||||||
Action string `json:"action"`
|
Action string `json:"action"`
|
||||||
Params map[string]interface{} `json:"params"`
|
Params map[string]any `json:"params"`
|
||||||
Echo string `json:"echo,omitempty"`
|
Echo string `json:"echo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,14 +88,14 @@ type OB11Action struct {
|
|||||||
type OB11Response struct {
|
type OB11Response struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
RetCode int `json:"retcode"`
|
RetCode int `json:"retcode"`
|
||||||
Data map[string]interface{} `json:"data,omitempty"`
|
Data map[string]any `json:"data,omitempty"`
|
||||||
Echo string `json:"echo,omitempty"`
|
Echo string `json:"echo,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageSegment 消息段
|
// MessageSegment 消息段
|
||||||
type MessageSegment struct {
|
type MessageSegment struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Data map[string]interface{} `json:"data"`
|
Data map[string]any `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息段类型常量
|
// 消息段类型常量
|
||||||
|
|||||||
@@ -149,22 +149,9 @@ func ProvideMilkyBots(cfg *config.Config, logger *zap.Logger, eventBus *engine.E
|
|||||||
bot := milky.NewBot(botCfg.ID, milkyCfg, eventBus, wsManager, logger)
|
bot := milky.NewBot(botCfg.ID, milkyCfg, eventBus, wsManager, logger)
|
||||||
botManager.Add(bot)
|
botManager.Add(bot)
|
||||||
|
|
||||||
|
// 注意:不在这里启动连接,由 RegisterLifecycleHooks 中的 botManager.StartAll() 统一启动
|
||||||
|
// 这样避免重复启动的问题
|
||||||
lc.Append(fx.Hook{
|
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 {
|
OnStop: func(ctx context.Context) error {
|
||||||
logger.Info("Stopping Milky bot", zap.String("bot_id", botCfg.ID))
|
logger.Info("Stopping Milky bot", zap.String("bot_id", botCfg.ID))
|
||||||
return bot.Disconnect(ctx)
|
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)
|
bot := onebot11.NewBot(botCfg.ID, ob11Cfg, logger, wsManager, eventBus)
|
||||||
botManager.Add(bot)
|
botManager.Add(bot)
|
||||||
|
|
||||||
|
// 注意:不在这里启动连接,由 RegisterLifecycleHooks 中的 botManager.StartAll() 统一启动
|
||||||
|
// 这样避免重复启动的问题
|
||||||
lc.Append(fx.Hook{
|
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 {
|
OnStop: func(ctx context.Context) error {
|
||||||
logger.Info("Stopping OneBot11 bot", zap.String("bot_id", botCfg.ID))
|
logger.Info("Stopping OneBot11 bot", zap.String("bot_id", botCfg.ID))
|
||||||
return bot.Disconnect(ctx)
|
return bot.Disconnect(ctx)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package engine
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"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) {
|
func (d *Dispatcher) createHandlerChain(ctx context.Context, event protocol.Event) func(context.Context, protocol.Event) {
|
||||||
return func(ctx context.Context, e protocol.Event) {
|
return func(ctx context.Context, e protocol.Event) {
|
||||||
d.mu.RLock()
|
d.mu.RLock()
|
||||||
handlers := make([]protocol.EventHandler, len(d.handlers))
|
handlers := slices.Clone(d.handlers)
|
||||||
copy(handlers, d.handlers)
|
|
||||||
d.mu.RUnlock()
|
d.mu.RUnlock()
|
||||||
|
|
||||||
for i, handler := range handlers {
|
for i, handler := range handlers {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -63,8 +64,7 @@ func NewEventBus(logger *zap.Logger, bufferSize int) *EventBus {
|
|||||||
|
|
||||||
// Start 启动事件总线
|
// Start 启动事件总线
|
||||||
func (eb *EventBus) Start() {
|
func (eb *EventBus) Start() {
|
||||||
eb.wg.Add(1)
|
eb.wg.Go(eb.dispatch)
|
||||||
go eb.dispatch()
|
|
||||||
eb.logger.Info("Event bus started")
|
eb.logger.Info("Event bus started")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,8 +193,6 @@ func (eb *EventBus) Unsubscribe(eventType protocol.EventType, ch chan protocol.E
|
|||||||
|
|
||||||
// dispatch 分发事件到订阅者
|
// dispatch 分发事件到订阅者
|
||||||
func (eb *EventBus) dispatch() {
|
func (eb *EventBus) dispatch() {
|
||||||
defer eb.wg.Done()
|
|
||||||
|
|
||||||
eb.logger.Info("Event bus dispatch loop started")
|
eb.logger.Info("Event bus dispatch loop started")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -220,8 +218,7 @@ func (eb *EventBus) dispatchEvent(event protocol.Event) {
|
|||||||
key := string(event.GetType())
|
key := string(event.GetType())
|
||||||
subs := eb.subscriptions[key]
|
subs := eb.subscriptions[key]
|
||||||
// 复制订阅者列表避免锁竞争
|
// 复制订阅者列表避免锁竞争
|
||||||
subsCopy := make([]*Subscription, len(subs))
|
subsCopy := slices.Clone(subs)
|
||||||
copy(subsCopy, subs)
|
|
||||||
eb.mu.RUnlock()
|
eb.mu.RUnlock()
|
||||||
|
|
||||||
eb.logger.Info("Dispatching event",
|
eb.logger.Info("Dispatching event",
|
||||||
|
|||||||
@@ -260,14 +260,14 @@ func (m *MetricsMiddleware) Process(ctx context.Context, event protocol.Event, n
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetMetrics 获取指标
|
// GetMetrics 获取指标
|
||||||
func (m *MetricsMiddleware) GetMetrics() map[string]interface{} {
|
func (m *MetricsMiddleware) GetMetrics() map[string]any {
|
||||||
m.mu.RLock()
|
m.mu.RLock()
|
||||||
defer m.mu.RUnlock()
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
metrics := make(map[string]interface{})
|
metrics := make(map[string]any)
|
||||||
for eventType, count := range m.eventCounts {
|
for eventType, count := range m.eventCounts {
|
||||||
avgTime := m.eventTimes[eventType] / time.Duration(count)
|
avgTime := m.eventTimes[eventType] / time.Duration(count)
|
||||||
metrics[eventType] = map[string]interface{}{
|
metrics[eventType] = map[string]any{
|
||||||
"count": count,
|
"count": count,
|
||||||
"avg_time": avgTime.String(),
|
"avg_time": avgTime.String(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -599,12 +599,12 @@ func OnlyToMe() HandlerMiddleware {
|
|||||||
selfID := event.GetSelfID()
|
selfID := event.GetSelfID()
|
||||||
|
|
||||||
// 检查消息段中是否包含@机器人的消息
|
// 检查消息段中是否包含@机器人的消息
|
||||||
if segments, ok := data["message_segments"].([]interface{}); ok {
|
if segments, ok := data["message_segments"].([]any); ok {
|
||||||
for _, seg := range segments {
|
for _, seg := range segments {
|
||||||
if segMap, ok := seg.(map[string]interface{}); ok {
|
if segMap, ok := seg.(map[string]any); ok {
|
||||||
segType, _ := segMap["type"].(string)
|
segType, _ := segMap["type"].(string)
|
||||||
if segType == "at" || segType == "mention" {
|
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 userID, ok := segData["user_id"]; ok {
|
||||||
if userIDStr := fmt.Sprintf("%v", userID); userIDStr == selfID {
|
if userIDStr := fmt.Sprintf("%v", userID); userIDStr == selfID {
|
||||||
|
|||||||
@@ -329,15 +329,13 @@ func (j *IntervalJob) Start(ctx context.Context) error {
|
|||||||
j.nextRun = time.Now().Add(j.interval)
|
j.nextRun = time.Now().Add(j.interval)
|
||||||
j.mu.Unlock()
|
j.mu.Unlock()
|
||||||
|
|
||||||
j.wg.Add(1)
|
j.wg.Go(j.run)
|
||||||
go j.run()
|
|
||||||
|
|
||||||
j.logger.Info("Interval job started", zap.Duration("interval", j.interval))
|
j.logger.Info("Interval job started", zap.Duration("interval", j.interval))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *IntervalJob) run() {
|
func (j *IntervalJob) run() {
|
||||||
defer j.wg.Done()
|
|
||||||
|
|
||||||
// 立即执行一次(可选,根据需求调整)
|
// 立即执行一次(可选,根据需求调整)
|
||||||
// if err := j.handler(j.ctx); err != nil {
|
// 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.nextRun = time.Now().Add(j.delay)
|
||||||
j.mu.Unlock()
|
j.mu.Unlock()
|
||||||
|
|
||||||
j.wg.Add(1)
|
j.wg.Go(j.run)
|
||||||
go j.run()
|
|
||||||
|
|
||||||
j.logger.Info("Once job started", zap.Duration("delay", j.delay))
|
j.logger.Info("Once job started", zap.Duration("delay", j.delay))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *OnceJob) run() {
|
func (j *OnceJob) run() {
|
||||||
defer j.wg.Done()
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-j.timer.C:
|
case <-j.timer.C:
|
||||||
if err := j.handler(j.ctx); err != nil {
|
if err := j.handler(j.ctx); err != nil {
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ func handleMCSBindCommand(ctx context.Context, event protocol.Event, botManager
|
|||||||
|
|
||||||
action := &protocol.BaseAction{
|
action := &protocol.BaseAction{
|
||||||
Type: protocol.ActionTypeSendGroupMessage,
|
Type: protocol.ActionTypeSendGroupMessage,
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
"message": errorMsg,
|
"message": errorMsg,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func Ping(host string, port int, timeout time.Duration) (*ServerStatus, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析 JSON 响应
|
// 解析 JSON 响应
|
||||||
var statusData map[string]interface{}
|
var statusData map[string]any
|
||||||
if err := json.Unmarshal([]byte(statusJSON), &statusData); err != nil {
|
if err := json.Unmarshal([]byte(statusJSON), &statusData); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse JSON: %w", err)
|
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
|
// 解析 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 {
|
if text, ok := desc["text"].(string); ok {
|
||||||
status.Description = text
|
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 数组
|
// 处理 extra 数组
|
||||||
var descText strings.Builder
|
var descText strings.Builder
|
||||||
for _, item := range text {
|
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 {
|
if text, ok := itemMap["text"].(string); ok {
|
||||||
descText.WriteString(text)
|
descText.WriteString(text)
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ func Ping(host string, port int, timeout time.Duration) (*ServerStatus, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析 version
|
// 解析 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 {
|
if name, ok := version["name"].(string); ok {
|
||||||
status.Version.Name = name
|
status.Version.Name = name
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ func Ping(host string, port int, timeout time.Duration) (*ServerStatus, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解析 players
|
// 解析 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 {
|
if online, ok := players["online"].(float64); ok {
|
||||||
status.Players.Online = int(online)
|
status.Players.Online = int(online)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func handleWelcomeEvent(ctx context.Context, event protocol.Event, botManager *p
|
|||||||
zap.Any("user_id", userID))
|
zap.Any("user_id", userID))
|
||||||
|
|
||||||
// 获取操作者ID(邀请者或审批者)
|
// 获取操作者ID(邀请者或审批者)
|
||||||
var operatorID interface{}
|
var operatorID any
|
||||||
if opID, exists := data["operator_id"]; exists {
|
if opID, exists := data["operator_id"]; exists {
|
||||||
operatorID = opID
|
operatorID = opID
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ func handleWelcomeEvent(ctx context.Context, event protocol.Event, botManager *p
|
|||||||
|
|
||||||
action := &protocol.BaseAction{
|
action := &protocol.BaseAction{
|
||||||
Type: protocol.ActionTypeSendGroupMessage,
|
Type: protocol.ActionTypeSendGroupMessage,
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
"message": welcomeChain,
|
"message": welcomeChain,
|
||||||
},
|
},
|
||||||
@@ -285,14 +285,14 @@ const welcomeTemplate = `
|
|||||||
`
|
`
|
||||||
|
|
||||||
// buildWelcomeMessage 构建欢迎消息链(使用HTML模板渲染图片)
|
// 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",
|
logger.Debug("Starting to build welcome message",
|
||||||
zap.Any("user_id", userID),
|
zap.Any("user_id", userID),
|
||||||
zap.Any("operator_id", operatorID),
|
zap.Any("operator_id", operatorID),
|
||||||
zap.String("sub_type", subType))
|
zap.String("sub_type", subType))
|
||||||
|
|
||||||
// 准备模板数据
|
// 准备模板数据
|
||||||
data := map[string]interface{}{
|
data := map[string]any{
|
||||||
"UserID": fmt.Sprintf("%v", userID),
|
"UserID": fmt.Sprintf("%v", userID),
|
||||||
}
|
}
|
||||||
logger.Debug("Template data prepared", zap.Any("data", data))
|
logger.Debug("Template data prepared", zap.Any("data", data))
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ type Action interface {
|
|||||||
// GetType 获取动作类型
|
// GetType 获取动作类型
|
||||||
GetType() ActionType
|
GetType() ActionType
|
||||||
// GetParams 获取动作参数
|
// GetParams 获取动作参数
|
||||||
GetParams() map[string]interface{}
|
GetParams() map[string]any
|
||||||
// Execute 执行动作
|
// Execute 执行动作
|
||||||
Execute(ctx interface{}) (map[string]interface{}, error)
|
Execute(ctx any) (map[string]any, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ActionType 动作类型
|
// ActionType 动作类型
|
||||||
@@ -40,7 +40,7 @@ const (
|
|||||||
// BaseAction 基础动作结构
|
// BaseAction 基础动作结构
|
||||||
type BaseAction struct {
|
type BaseAction struct {
|
||||||
Type ActionType `json:"type"`
|
Type ActionType `json:"type"`
|
||||||
Params map[string]interface{} `json:"params"`
|
Params map[string]any `json:"params"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetType 获取动作类型
|
// GetType 获取动作类型
|
||||||
@@ -49,12 +49,12 @@ func (a *BaseAction) GetType() ActionType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetParams 获取动作参数
|
// GetParams 获取动作参数
|
||||||
func (a *BaseAction) GetParams() map[string]interface{} {
|
func (a *BaseAction) GetParams() map[string]any {
|
||||||
return a.Params
|
return a.Params
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute 执行动作(需子类实现)
|
// Execute 执行动作(需子类实现)
|
||||||
func (a *BaseAction) Execute(ctx interface{}) (map[string]interface{}, error) {
|
func (a *BaseAction) Execute(ctx any) (map[string]any, error) {
|
||||||
return nil, ErrNotImplemented
|
return nil, ErrNotImplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func (b *BaseBotInstance) IsConnected() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendAction 发送动作
|
// 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)
|
return b.protocol.SendAction(ctx, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ type Event interface {
|
|||||||
// GetSelfID 获取机器人自身ID
|
// GetSelfID 获取机器人自身ID
|
||||||
GetSelfID() string
|
GetSelfID() string
|
||||||
// GetData 获取事件数据
|
// GetData 获取事件数据
|
||||||
GetData() map[string]interface{}
|
GetData() map[string]any
|
||||||
// Reply 在消息发生的群/私聊进行回复
|
// Reply 在消息发生的群/私聊进行回复
|
||||||
Reply(ctx context.Context, botManager *BotManager, logger *zap.Logger, message MessageChain) error
|
Reply(ctx context.Context, botManager *BotManager, logger *zap.Logger, message MessageChain) error
|
||||||
// ReplyText 在消息发生的群/私聊进行文本回复(便捷方法)
|
// ReplyText 在消息发生的群/私聊进行文本回复(便捷方法)
|
||||||
@@ -46,10 +46,10 @@ type Event interface {
|
|||||||
type BaseEvent struct {
|
type BaseEvent struct {
|
||||||
Type EventType `json:"type"`
|
Type EventType `json:"type"`
|
||||||
DetailType string `json:"detail_type"`
|
DetailType string `json:"detail_type"`
|
||||||
SubType string `json:"sub_type,omitempty"`
|
SubType string `json:"sub_type,omitzero"`
|
||||||
Timestamp int64 `json:"timestamp"`
|
Timestamp int64 `json:"timestamp"`
|
||||||
SelfID string `json:"self_id"`
|
SelfID string `json:"self_id"`
|
||||||
Data map[string]interface{} `json:"data"`
|
Data map[string]any `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetType 获取事件类型
|
// GetType 获取事件类型
|
||||||
@@ -78,7 +78,7 @@ func (e *BaseEvent) GetSelfID() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetData 获取事件数据
|
// GetData 获取事件数据
|
||||||
func (e *BaseEvent) GetData() map[string]interface{} {
|
func (e *BaseEvent) GetData() map[string]any {
|
||||||
return e.Data
|
return e.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ func (e *BaseEvent) Reply(ctx context.Context, botManager *BotManager, logger *z
|
|||||||
if e.GetDetailType() == "private" {
|
if e.GetDetailType() == "private" {
|
||||||
action = &BaseAction{
|
action = &BaseAction{
|
||||||
Type: ActionTypeSendPrivateMessage,
|
Type: ActionTypeSendPrivateMessage,
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"message": message,
|
"message": message,
|
||||||
},
|
},
|
||||||
@@ -114,7 +114,7 @@ func (e *BaseEvent) Reply(ctx context.Context, botManager *BotManager, logger *z
|
|||||||
// 群聊或其他有 group_id 的事件
|
// 群聊或其他有 group_id 的事件
|
||||||
action = &BaseAction{
|
action = &BaseAction{
|
||||||
Type: ActionTypeSendGroupMessage,
|
Type: ActionTypeSendGroupMessage,
|
||||||
Params: map[string]interface{}{
|
Params: map[string]any{
|
||||||
"group_id": groupID,
|
"group_id": groupID,
|
||||||
"message": message,
|
"message": message,
|
||||||
},
|
},
|
||||||
@@ -151,15 +151,15 @@ type MessageEvent struct {
|
|||||||
BaseEvent
|
BaseEvent
|
||||||
MessageID string `json:"message_id"`
|
MessageID string `json:"message_id"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
AltText string `json:"alt_text,omitempty"`
|
AltText string `json:"alt_text,omitzero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoticeEvent 通知事件
|
// NoticeEvent 通知事件
|
||||||
type NoticeEvent struct {
|
type NoticeEvent struct {
|
||||||
BaseEvent
|
BaseEvent
|
||||||
GroupID string `json:"group_id,omitempty"`
|
GroupID string `json:"group_id,omitzero"`
|
||||||
UserID string `json:"user_id,omitempty"`
|
UserID string `json:"user_id,omitzero"`
|
||||||
Operator string `json:"operator,omitempty"`
|
Operator string `json:"operator,omitzero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestEvent 请求事件
|
// RequestEvent 请求事件
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Protocol interface {
|
|||||||
// IsConnected 检查连接状态
|
// IsConnected 检查连接状态
|
||||||
IsConnected() bool
|
IsConnected() bool
|
||||||
// SendAction 发送动作
|
// SendAction 发送动作
|
||||||
SendAction(ctx context.Context, action Action) (map[string]interface{}, error)
|
SendAction(ctx context.Context, action Action) (map[string]any, error)
|
||||||
// HandleEvent 处理事件
|
// HandleEvent 处理事件
|
||||||
HandleEvent(ctx context.Context, event Event) error
|
HandleEvent(ctx context.Context, event Event) error
|
||||||
// GetSelfID 获取机器人自身ID
|
// GetSelfID 获取机器人自身ID
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import "fmt"
|
|||||||
// 通用消息段结构,适配器负责转换为具体协议格式
|
// 通用消息段结构,适配器负责转换为具体协议格式
|
||||||
type MessageSegment struct {
|
type MessageSegment struct {
|
||||||
Type string `json:"type"` // 消息段类型
|
Type string `json:"type"` // 消息段类型
|
||||||
Data map[string]interface{} `json:"data"` // 消息段数据
|
Data map[string]any `json:"data"` // 消息段数据
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageChain 消息链(基于 OneBot12 设计)
|
// MessageChain 消息链(基于 OneBot12 设计)
|
||||||
@@ -33,17 +33,17 @@ const (
|
|||||||
func NewTextSegment(text string) MessageSegment {
|
func NewTextSegment(text string) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeText,
|
Type: SegmentTypeText,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"text": text,
|
"text": text,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMentionSegment 创建@提及消息段(OneBot12 标准)
|
// NewMentionSegment 创建@提及消息段(OneBot12 标准)
|
||||||
func NewMentionSegment(userID interface{}) MessageSegment {
|
func NewMentionSegment(userID any) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeMention,
|
Type: SegmentTypeMention,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ func NewMentionSegment(userID interface{}) MessageSegment {
|
|||||||
func NewImageSegment(fileID string) MessageSegment {
|
func NewImageSegment(fileID string) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeImage,
|
Type: SegmentTypeImage,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"file_id": fileID,
|
"file_id": fileID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -63,17 +63,17 @@ func NewImageSegment(fileID string) MessageSegment {
|
|||||||
func NewImageSegmentFromBase64(base64Data string) MessageSegment {
|
func NewImageSegmentFromBase64(base64Data string) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeImage,
|
Type: SegmentTypeImage,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"file": fmt.Sprintf("base64://%s", base64Data),
|
"file": fmt.Sprintf("base64://%s", base64Data),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReplySegment 创建回复消息段
|
// NewReplySegment 创建回复消息段
|
||||||
func NewReplySegment(messageID interface{}) MessageSegment {
|
func NewReplySegment(messageID any) MessageSegment {
|
||||||
return MessageSegment{
|
return MessageSegment{
|
||||||
Type: SegmentTypeReply,
|
Type: SegmentTypeReply,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]any{
|
||||||
"message_id": messageID,
|
"message_id": messageID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ func (mc MessageChain) AppendText(text string) MessageChain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AppendMention 追加@提及到消息链
|
// AppendMention 追加@提及到消息链
|
||||||
func (mc MessageChain) AppendMention(userID interface{}) MessageChain {
|
func (mc MessageChain) AppendMention(userID any) MessageChain {
|
||||||
return mc.Append(NewMentionSegment(userID))
|
return mc.Append(NewMentionSegment(userID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
253
plans/go-modernization-plan.md
Normal file
253
plans/go-modernization-plan.md
Normal file
@@ -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()` 优化
|
||||||
|
|
||||||
|
建议按阶段逐步实施,每个阶段完成后进行验证,确保代码质量和功能稳定性。
|
||||||
Reference in New Issue
Block a user