napcat: NapCatQQ (OneBot 11) Go SDK, initial release

This commit is contained in:
2026-10-18 20:20:29 +08:00
commit 7f1738a987
14 changed files with 2879 additions and 0 deletions

127
caller.go Normal file
View File

@ -0,0 +1,127 @@
package napcat
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("napcat: 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("napcat: 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("napcat: 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("napcat: unmarshal response failed", "err", err)
return false
}
v, ok := w.pending.Load(resp.Echo)
if !ok {
log.Warn("napcat: 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)
}