接 M38 的 18 条真泄漏里「渠道适配器 + 出站工具」这一族(#10 #11 #12 #13 #15 #16)。 只动日志与 error 文字:发送/接收、解析口径、重试次数一个都没改。 - httpclient.do:非 2xx 不再把 512B 响应体原文拼进 error,改 trace.UpstreamError (状态/体积/sha256 前缀)+ 信封 code/message(RedactText + 200 rune);error 里的 target 改 trace.HostOf,并剥掉 *url.Error 外壳(它带整条地址) - telegram doJSON:4xx 只解 error_code/description(清洗 + 截断),体其余字段不拼; 传输错误只留来源 —— Bot API 把 token 写在地址路径里 - weixin_ilink doJSON:4096B 体 ⇒ trace.UpstreamError,rawURL ⇒ mediafetch.HostOf; uploadOnce 剥掉预签名上传地址(encrypted_query_param),重试日志补来源主机 - qq_obv11 connectClient 成功 INFO:改走同包已有的 maskAccessTokenQuery (OneBot 惯例把 ?access_token= 写进 remote_url) - plugins/tools/io http 拦截日志:改记 trace.HostOf + 原因码,不记原 URL、也不记错误 原文(解析失败时它包着 *url.Error) 每条都有能变异打红的用例:QQ/iLink 预签名地址的哨兵、三处 4xx 体哨兵、 user:pass@ 与 query 凭据;八处变异逐条验证变红。
84 lines
3.5 KiB
Go
84 lines
3.5 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"
|
||
)
|
||
|
||
// 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=…`,原样 INFO 就是把凭据
|
||
// 写进日志。这里复用同包已有的第二层脱敏(connect_outcome.go 的
|
||
// maskAccessTokenQuery)——不另写一套,也不用 sanitizeConnectReason 的 80 rune
|
||
// 上限,否则地址的路径会被切掉、失去「连到了哪」的诊断价值。
|
||
a.log.Info("connected to onebot ws (client mode)", "url", maskAccessTokenQuery(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)
|
||
}
|