P2-1 telegram doJSON 的**请求构造**失败分支:http.NewRequestWithContext 解析失败的 err 同样是 *url.Error(里面是整条 /bot<token>/… 地址),而 poll.go 的 long poll failed 会原样落日志。改成与传输层分支同形:trace.HostOf(endpoint) + callCause。 P2-2 qq_obv11 connectClient 的成功 INFO 从 maskAccessTokenQuery 换成 trace.MaskQuery:后者认得 access%5Ftoken / token / api_key / 签名参数 / userinfo, 且保留 EscapedPath(路径可读)与不截断。maskAccessTokenQuery 仍被 sanitizeConnectReason 使用,未成死代码。 P2-3 删掉 maxEnvelopeMessageRunes / maxDescriptionRunes(各 200):trace.RedactText 内部已 truncate 到 maxSummaryRunes=80,这两层永不命中,注释口径与实际不符。改为 直接 return trace.RedactText(压平空白),注释写明 80 rune 由 RedactText 负责。 用例(全部变异打红,逐条验证):telegram 构造分支 token、QQ 八形态凭据矩阵 (含四种 maskAccessTokenQuery 漏掉的形态 + 路径可读反向断言)、两处 80 rune 上限。
86 lines
3.6 KiB
Go
86 lines
3.6 KiB
Go
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)
|
||
}
|