返回 DeepSeek-Reasonix
adapter.go
根目录 / internal / bot / qq / adapter.go
1 // Package qq 实现 QQ 官方 Bot API v2 适配器。
2 // 参考 Hermes Agent 的 qqbot adapter 实现:
3 // - app token 获取与刷新
4 // - WebSocket gateway 连接、heartbeat、resume
5 // - REST API 回复消息
6 // - C2C / group / guild / direct message 支持
7 // - inline keyboard 审批
8 package qq
9
10 import (
11 "context"
12 "log/slog"
13 "sync"
14 "time"
15
16 "reasonix/internal/bot"
17 "reasonix/internal/config"
18
19 "golang.org/x/net/websocket"
20 )
21
22 // New 创建 QQ Bot 适配器。
23 func New(cfg config.QQBotConfig, logger *slog.Logger) bot.Adapter {
24 return &adapter{
25 cfg: cfg,
26 logger: logger.With("platform", "qq"),
27 }
28 }
29
30 type adapter struct {
31 cfg config.QQBotConfig
32 logger *slog.Logger
33 msgCh chan bot.InboundMessage
34 cancel context.CancelFunc
35 loopWG sync.WaitGroup
36
37 // gateway 状态
38 connMu sync.Mutex
39 conn *websocket.Conn // live gateway connection, closed by Stop to unblock reads
40 sessionID string
41 seq int64
42 token string
43 tokenExpiry time.Time
44 tokenMu sync.Mutex
45
46 sendMu sync.Mutex
47 nextOutboundMsgSeq int
48 markdownDisabled bool
49 }
50
51 func (a *adapter) Platform() bot.Platform { return bot.PlatformQQ }
52 func (a *adapter) Name() string { return "qq" }
53
54 func (a *adapter) Start(ctx context.Context) error {
55 a.msgCh = make(chan bot.InboundMessage, 64)
56 startupCtx, startupCancel := context.WithTimeout(ctx, qqStartupValidationTimeout)
57 defer startupCancel()
58 token, err := a.getAccessToken(startupCtx)
59 if err != nil {
60 return err
61 }
62 if _, err := a.getGatewayURL(startupCtx, token); err != nil {
63 return err
64 }
65 ctx, a.cancel = context.WithCancel(ctx)
66
67 a.loopWG.Add(1)
68 go func() {
69 defer a.loopWG.Done()
70 a.gatewayLoop(ctx)
71 }()
72 return nil
73 }
74
75 // Stop 取消 gateway context、关闭当前 WebSocket 连接并等待 gatewayLoop 退出。
76 // websocket 的阻塞读不响应 context,只有关闭连接才能解除阻塞;不等待就返回
77 // 会在宿主重建 bot runtime 后留下仍占用 QQ gateway session 的僵尸连接。
78 func (a *adapter) Stop() error {
79 if a.cancel != nil {
80 a.cancel()
81 }
82 a.closeConn()
83 a.loopWG.Wait()
84 return nil
85 }
86
87 func (a *adapter) Send(ctx context.Context, msg bot.OutboundMessage) (bot.SendResult, error) {
88 return a.sendMessage(ctx, msg)
89 }
90
91 func (a *adapter) SendTyping(ctx context.Context, chatID string) error {
92 return nil // QQ Bot 暂不支持 typing 指示器
93 }
94
95 func (a *adapter) Messages() <-chan bot.InboundMessage {
96 return a.msgCh
97 }
98
98 lines GO