根因:adminChannelOpen 在 ch.Connect 返回 nil 后硬编码 connected=true。而 Connect 返回 nil 只说明「这次调用没有致命失败」—— ws+client 首连失败刻意非致命(OneBot 实现可能比 Cyrene 后启动),ws+server 则只需监听就绪,两种情况 IsConnected() 都是 false。前端又只按「请求成功」写死「已连接」,于是管理面把「后端受理了」说成「渠道连上了」。 改法:三态口径落在适配器自己身上(connected/pending/ready,internal/channel/adapter 的纯函数 ConnectStateOf 判定 + ConnectSignals 结构化入参,避免两个 bool 接反),qq_obv11 实现 ConnectOutcome 并记住脱敏后的失败原因;网关经类型断言探测(与 accountStatusAware/rescanAware 同套路,核心 sdk.Channel 契约刻意不扩展),只做映射不猜 transport/mode。 线上投影:open/close 响应与 GET /admin/channels 的平台行/账号行各新增 pending/ready/reason 三个可选键(零值即整键缺席,同 NeedsRescan/LastError 口径);既有四键逐字不变,未实现该状态面的平台输出逐字节不变。close 报真实 IsConnected(),断开也不乐观。 凭据边界:reason 三层脱敏(形状级 → 已知明文替换 → 剥 access_token= 查询参数),日志同样只用脱敏串。 验收:主会话亲自复跑 go build/vet 全绿、全树 go test 无 FAIL、目标三包 test 全绿、前端 tsc --noEmit 干净、esbuild 逻辑测试 34+46 checks 全过;主会话亲自变异摘掉第 3 层脱敏实现,发现原两条脱敏用例其实钉不住它(值恰好等于已知明文/被形状级顺带吃掉)⇒ 补一条钉住它独有窗口的用例(短值+紧邻空白),变异下必红、还原后绿。
83 lines
4.7 KiB
Go
83 lines
4.7 KiB
Go
package adapter
|
||
|
||
// 连接**就绪三态**的取值与判定:一次 Connect 返回之后,适配器到底处于哪一种状态。
|
||
//
|
||
// 为什么必须三态而不是一个 bool:`Connect` 返回 nil 只说明「这次调用没有致命失败」,
|
||
// 它至少覆盖三件互不相同的事——
|
||
//
|
||
// ConnectStateConnected —— 真连上了(client 拨号成功 / HTTP 传输已装配就绪);
|
||
// ConnectStatePending —— 已进入后台重试、当前还没连上(OneBot 实现比 Cyrene 后
|
||
// 启动是常态,首连失败刻意非致命);
|
||
// ConnectStateReady —— 已绑定监听、就绪等待对端接入(ws+server 模式)。
|
||
//
|
||
// pending 与 ready 的 IsConnected() 都是 false,但它们**不是**「未连接」这样一个静态
|
||
// 结论:前者是「正在连接」,后者是「就绪等接入」。用一个布尔去覆盖三态,管理面板就只能
|
||
// 编一个说法——编出来的正是 2026-10-07 那个「点了连接就弹已连接,而卡片一直显示未连接」
|
||
// 的谎。口径属于适配器(它才知道自己的 transport × mode 意味着什么),网关只做映射
|
||
// (internal/services/gateway 的 connectOutcomeAware,经类型断言探测,核心契约不扩展)。
|
||
//
|
||
// 设计依据:DevDocs/design/08-渠道接入.md §1(PlatformAdapter 契约)与 §4.1(四端
|
||
// transport × mode);立场与 design/26 §2.6「状态面必须区分 WS 连着与账号在线」一致 ——
|
||
// 一个布尔说不了两件事,更说不了三件。
|
||
//
|
||
// 与 SessionState* / AccountStatus 同一口径:核心 PlatformAdapter 契约**刻意不扩展**,
|
||
// 需要这条状态面的适配器自行实现该方法,网关用类型断言探测;没实现的适配器行为逐字不变。
|
||
|
||
const (
|
||
// ConnectStateConnected 表示真连上:会话已建立(client 拨号成功),或传输已装配
|
||
// 就绪而无可观察的长连接(HTTP)。此时 IsConnected() 应为 true。
|
||
ConnectStateConnected = "connected"
|
||
// ConnectStatePending 表示已进入后台重试、当前尚未连上。它既不是配置错误,也不是
|
||
// 「未连接」,而是「连接中」,Reason 应说明原因(脱敏,绝不含凭据)。
|
||
ConnectStatePending = "pending"
|
||
// ConnectStateReady 表示已就绪、等待对端接入:server 模式已绑定监听,但还没有
|
||
// 对端连上来。IsConnected() 此时为 false,却**不是失败**,因此没有 Reason。
|
||
ConnectStateReady = "ready"
|
||
)
|
||
|
||
// ConnectSignals 是判定三态所需的**全部**输入。用结构体而不是几个相邻的布尔参数,
|
||
// 一是让调用点自解释,二是避免两个 bool 被接反(那会静默地骗过所有测试)。
|
||
type ConnectSignals struct {
|
||
// Started 报告 Connect 是否已成功启动该适配器;false 表示尚未启动,无从判断。
|
||
Started bool
|
||
// Connected 报告当前是否有一条活的会话。
|
||
Connected bool
|
||
// Retrying 报告未连上时后台是否有重试循环在跑(ws+client 有一条;HTTP 没有会话
|
||
// 可重连,不算)。
|
||
Retrying bool
|
||
// Listening 报告适配器是否已绑定监听、在等对端接入(server 模式)。
|
||
Listening bool
|
||
// Reason 是最近一次失败的原因(**已脱敏**);没有失败或已连上时为空串。
|
||
Reason string
|
||
}
|
||
|
||
// ConnectStateOf 是**纯函数**:由信号判定三态与原因。它不读时钟、不触网、不加锁,
|
||
// 因此三态可以逐条单测(见 connect_outcome_test.go);适配器侧只负责在同一把锁下把
|
||
// 信号读齐。
|
||
//
|
||
// 判定顺序(自上而下,先匹配者胜):
|
||
//
|
||
// !Started → ("", "") —— 未启动的适配器无从判断:回「无意见」而不是
|
||
// 随便认一个状态,调用方据此让可选键整键缺席。
|
||
// Connected → ConnectStateConnected —— 有活会话就是真连上,先于其它分支。
|
||
// Retrying → ConnectStatePending —— 尚未连上但重试在跑,这才是「连接中」。
|
||
// Listening → ConnectStateReady —— 监听已就绪、等对端接入,不是失败。
|
||
// 否则 → ("", "") —— 已启动、未连上、也没有可解释的就绪面:**不编**
|
||
// 状态,让调用方退回只看 IsConnected()。
|
||
//
|
||
// 只有 pending 会带 Reason:connected 没有任何坏消息要说,ready 本来就不是坏消息。
|
||
func ConnectStateOf(sig ConnectSignals) (state, reason string) {
|
||
switch {
|
||
case !sig.Started:
|
||
return "", ""
|
||
case sig.Connected:
|
||
return ConnectStateConnected, ""
|
||
case sig.Retrying:
|
||
return ConnectStatePending, sig.Reason
|
||
case sig.Listening:
|
||
return ConnectStateReady, ""
|
||
default:
|
||
return "", ""
|
||
}
|
||
}
|