package snowluma import ( "context" "encoding/json" "fmt" "strconv" "sync" "sync/atomic" "time" ) // Caller 是一条可执行 OneBot action 的通道(WS 会话或 HTTP API)。 type Caller interface { Call(ctx context.Context, action string, params any, out any) error } // CallAPI 通过指定 Caller 调用一个 action 并把 data 解码为 T。 // ctx 为 nil 时使用 30 秒超时。 func CallAPI[T any](c Caller, ctx context.Context, action string, params any) (T, error) { var zero T if c == nil { return zero, ErrNotConnected } if ctx == nil { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second) defer cancel() } var out T if err := c.Call(ctx, action, params, &out); err != nil { return zero, err } return out, nil } // wsRequest 是 WS 上发出的 action 请求。 type wsRequest struct { Action string `json:"action"` Params any `json:"params,omitempty"` Echo string `json:"echo"` } // wsCaller 基于一条 WebSocket 连接实现 Caller: // 发出的请求带自增 echo,读循环把响应按 echo 路由回等待方。 type wsCaller struct { wmu sync.Mutex writer messageWriter pending sync.Map // echo(string) -> chan Response seq atomic.Int64 done <-chan struct{} // 连接关闭时被关闭 } // messageWriter 抽象 gorilla/websocket 的写方法,便于单测替换。 type messageWriter interface { WriteMessage(messageType int, data []byte) error } func newWSCaller(w messageWriter, done <-chan struct{}) *wsCaller { return &wsCaller{writer: w, done: done} } func (w *wsCaller) Call(ctx context.Context, action string, params any, out any) error { if ctx == nil { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(context.Background(), 30*time.Second) defer cancel() } echo := strconv.FormatInt(w.seq.Add(1), 10) payload, err := json.Marshal(wsRequest{Action: action, Params: params, Echo: echo}) if err != nil { return fmt.Errorf("snowluma: marshal action %s: %w", action, err) } ch := make(chan Response, 1) w.pending.Store(echo, ch) defer w.pending.Delete(echo) w.wmu.Lock() err = w.writer.WriteMessage(1 /* websocket.TextMessage */, payload) w.wmu.Unlock() if err != nil { return fmt.Errorf("snowluma: send action %s: %w", action, err) } select { case resp := <-ch: if err := resp.Err(); err != nil { return err } if err := resp.Decode(out); err != nil { return fmt.Errorf("snowluma: decode data of %s: %w", action, err) } return nil case <-ctx.Done(): return ctx.Err() case <-w.done: return ErrNotConnected } } // routeResponse 由读循环调用:把带 echo 的响应分发给等待者,返回是否路由成功。 func (w *wsCaller) routeResponse(data []byte, log logger) bool { var resp Response if err := json.Unmarshal(data, &resp); err != nil { log.Warn("snowluma: unmarshal response failed", "err", err) return false } v, ok := w.pending.Load(resp.Echo) if !ok { log.Warn("snowluma: no pending action for echo", "echo", resp.Echo) return false } ch := v.(chan Response) select { case ch <- resp: default: } return true } // logger 是框架依赖的最小日志接口(*slog.Logger 满足它)。 type logger interface { Warn(msg string, args ...any) Info(msg string, args ...any) Error(msg string, args ...any) Debug(msg string, args ...any) }