package qq_obv11 // 本文件实现四个端里的 **ws + client**(裁决 1.4):连接由 Cyrene 发起 // (Cyrene→OneBot),主动拨号 OneBot 实现的「正向 WS」端点(remote_url),建立后 // 收发都走这条长连接。access_token 同时以 Authorization: Bearer 头与 // ?access_token= 查询参数发送,兼容只认其中一种的 OneBot 实现。 // // 首连失败**不致命**:对面(NapCat 等)可能比 Cyrene 后启动,失败只记 WARN,重试交给 // readLoop 的同一条重连路径(指数退避 + 抖动 + 上限,见 reconnect.go)。 import ( "context" "errors" "fmt" "net/url" "strings" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace" ) // errClientConfig 是 client 模式配置缺失(remote_url 为空)的错误哨兵:重试治不好 // 配置错,必须让 Connect 直接失败(渠道桥接器的凭据门同样要求 remoteURL 非空,见 // adapter.go 的 configRemoteURL)。 var errClientConfig = errors.New("qq-obv11: client mode requires remoteURL") // connectFirstClient 做一次 ws+client 首连(Connect 同步调用)。 // // 为什么失败不致命:OneBot 实现(NapCat 等)可能比 Cyrene 后启动,或者正在重启—— // 若把首连失败当作渠道起不来,部署顺序就成了硬约束。因此可重试的失败只记一条 WARN // 就返回 nil,真正的重试由 readLoop 复用同一条重连路径(指数退避 + 抖动 + 上限) // 在后台进行。只有配置缺失(errClientConfig)才致命:重试治不好配置错,渠道桥接器 // 的凭据门也要求 remote_url 非空。 func (a *Adapter) connectFirstClient(ctx context.Context) error { // 新一轮启动:退避从初值起步,不继承上一轮会话残留的失败计数。 a.reconnect.reset() err := a.connectClient(ctx) switch { case err == nil: return nil case errors.Is(err, errClientConfig): return err default: a.noteConnectFailure(err) return nil } } // connectClient dials the configured OneBot WebSocket endpoint and installs the // resulting connection as the active session. func (a *Adapter) connectClient(ctx context.Context) error { if a.cfg.RemoteURL == "" { return errClientConfig } target := appendQueryToken(a.cfg.RemoteURL, a.cfg.AccessToken) conn, err := dialWS(ctx, a.cfg.DialContext, target, a.cfg.AccessToken) if err != nil { return fmt.Errorf("qq-obv11: dial onebot ws %s: %w", a.cfg.RemoteURL, err) } // 拨号期间可能已被 Disconnect / ctx 取消:此时绝不把连接挂成「已连接」, // 否则状态撒谎,而且这条连接没有读循环会去消费它。 if cerr := ctx.Err(); cerr != nil { _ = conn.Close() return fmt.Errorf("qq-obv11: dial onebot ws %s: %w", a.cfg.RemoteURL, cerr) } a.setConn(conn) // remote_url 按 OneBot 惯例可以直接写着 `?access_token=…`,也可能是把凭据塞进 // `user:pass@` 或别的键名(`token` / `api_key` / 签名参数)。原样 INFO 就是把凭据 // 写进日志,因此只打 trace.MaskQuery 的形态:凭据类 query/fragment 值与 userinfo // 一律打码,路径原样保留(「连到了哪个端点」是这条 INFO 的全部诊断价值)。 a.log.Info("connected to onebot ws (client mode)", "url", trace.MaskQuery(a.cfg.RemoteURL)) return nil } // appendQueryToken adds an access_token query parameter to a URL, preserving any // existing query string. func appendQueryToken(rawURL, token string) string { if token == "" { return rawURL } sep := "?" if strings.Contains(rawURL, "?") { sep = "&" } return rawURL + sep + "access_token=" + url.QueryEscape(token) }