Files
Cyrene_v2/plugins/channels/qq_obv11/ws_client.go
T
AskaEth 8c437866e7
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(qq-obv11): client 模式首连失败不再致命(复用既有重连循环 + 指数退避)
痛点:mode=client 时首次拨号失败会让 Connect 直接返回 error ⇒ 渠道起不来;
而「连上后再断」本来就有重连。⇒ 把 readLoop 的入口条件放宽为「conn==nil 也算」,
**不新造第二套循环**。
新增 reconnect.go:退避 2s → ×2 → 上限 60s、抖动 ±25%(具名常量),失败计数跨
Connect 与 readLoop 共享。
ws_client:新增 connectFirstClient —— 可重试失败只记 WARN 并返回 nil;
配置缺失(remote_url 空)仍致命(闸门口径不变)。
connected 语义不许撒谎:始终由真实连接决定;HealthCheck 未连上返回 ErrNotConnected;
仅 http 传输保留「已装配即可服务」旧口径(逐字未改)。
ctx 感知等待(sleepCtx)+ 等待后复查 ctx ⇒ Disconnect 能立即中止,不泄漏 goroutine。
server 模式与 http 三端行为逐字不变(既有用例零改动全绿 + TestServerModeUnaffectedByClientRetry)。
测试:client_retry_test.go(428) + reconnect_test.go(83),用可注入 dialer + gate 通道造确定性时序。
变异 5 条(首连 fail-fast / 退避无上限 / 取消失效 / 未连上就报已连 / 抖动未接线)。
诚实边界:60s 上限对聊天渠道可能偏慢(改 30s 是一行常量);仓库内仍存第二份指数退避
(wsutil.Backoff 无抖动能力,收敛需改 internal/**,本切片禁碰)。
2026-10-05 10:12:23 +08:00

80 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
)
// 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)
a.log.Info("connected to onebot ws (client mode)", "url", 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)
}