根因: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 层脱敏实现,发现原两条脱敏用例其实钉不住它(值恰好等于已知明文/被形状级顺带吃掉)⇒ 补一条钉住它独有窗口的用例(短值+紧邻空白),变异下必红、还原后绿。
99 lines
5.0 KiB
Go
99 lines
5.0 KiB
Go
package gateway
|
|
|
|
// 本文件承载管理面读取**适配器连接态三态**的那一层:把适配器的 ConnectOutcome
|
|
// 映射成面板与线上契约能用的形状,使「点了连接之后到底怎么样了」不再只有一个布尔。
|
|
//
|
|
// 为什么需要它:`Connect` 返回 nil 只说明「这次调用没有致命失败」。ws+client 的首连
|
|
// 失败刻意非致命(OneBot 实现可能比 Cyrene 后启动),此时 IsConnected() 为 false ——
|
|
// 它与「监听已就绪等对端接入」(server 模式,同样 false)在布尔上无从区分,更与
|
|
// 「配置错、起不来」分不开。网关**绝不**按适配器名或 transport 去猜:口径属于适配器,
|
|
// 它才知道自己的 transport × mode 意味着什么;网关只做映射。
|
|
//
|
|
// 与 accountStatusAware / rescanAware / sessionStatusAware 同一契约风格(见
|
|
// admin_channel_status.go 与 admin_channels_rescan.go):核心 SDK Channel 契约被刻意
|
|
// 不扩展,因此没有这条状态面的平台不受任何改动,也不会凭空多出一个字段。
|
|
|
|
import (
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
|
)
|
|
|
|
// connectOutcomeAware 是渠道适配器暴露「Connect 之后处于哪种状态」这一可选能力的接口。
|
|
// 当前由 *qq_obv11.Adapter 实现(ConnectOutcome):它的 ws+client 首连失败非致命、
|
|
// ws+server 只需监听就绪,两种「没连上」都不是同一件事。
|
|
//
|
|
// 返回的 state 是**闭集**取值(internal/channel/adapter 的 ConnectState* 常量):
|
|
//
|
|
// "connected" —— 真连上;
|
|
// "pending" —— 已进入后台重试、当前未连上,reason 说明原因;
|
|
// "ready" —— 已绑定监听、就绪等对端接入,不是失败;
|
|
// "" —— 无意见(尚未启动),调用方退回只看 IsConnected()。
|
|
//
|
|
// 闭集而不是布尔,是因为「没连上」至少还有两种互不相同的现状,而它们对主人的意义
|
|
// 完全不同:一种是「还在连」,一种是「就等对面来连」。
|
|
type connectOutcomeAware interface {
|
|
// ConnectOutcome 报告连接态;state 取值见上,reason 在未连上时说明原因
|
|
// (如实的中文/技术短句,**绝不含凭据**)。
|
|
ConnectOutcome() (state string, reason string)
|
|
}
|
|
|
|
// asConnectOutcomeAware 报告已注册的渠道值是否暴露连接态三态。与 asRescanAware /
|
|
// asAccountStatusAware 同一套路:注册时会把适配器包进 cmd/cyrene 的 namedAdapter ——
|
|
// 一个内嵌 adapter.PlatformAdapter 的未导出结构体 —— 它只提升接口的方法,因此具体
|
|
// 适配器多出的能力只能通过拆开被内嵌字段才能抵达。直接的适配器值(测试)按原样接受。
|
|
func asConnectOutcomeAware(v any) (connectOutcomeAware, bool) {
|
|
if co, ok := v.(connectOutcomeAware); ok {
|
|
return co, true
|
|
}
|
|
inner, ok := unwrapNamedAdapter(v)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
co, ok := inner.(connectOutcomeAware)
|
|
return co, ok
|
|
}
|
|
|
|
// channelConnectOutcome 是连接态三态在**线上**的投影:三态里只有两种「没连上」需要
|
|
// 额外说明(真连上已经由 connected 布尔说完),reason 只在 pending 时非空。
|
|
//
|
|
// 刻意用可选键而不是把三态塞进 connected:connected 的语义已被老客户端当作「连上了
|
|
// 没有」在读,改写它等于改契约;新增可选键则谁都不吃亏 —— 零值的三态投影让
|
|
// pending / ready / reason **整键缺席**,与 NeedsRescan / LastError 同一口径。
|
|
type channelConnectOutcome struct {
|
|
// Pending 表示「已在后台重试、当前未连上」——面板须把它读作「连接中」,
|
|
// 绝不是「未连接」,也绝不是「已连接」。
|
|
Pending bool
|
|
// Ready 表示「已就绪、等待对端接入」——同样不是失败。
|
|
Ready bool
|
|
// Reason 是未连上时的原因(已脱敏),仅在 Pending 时可能非空。
|
|
Reason string
|
|
}
|
|
|
|
// channelConnectStateOf 报告某渠道适配器当前的连接态:connected 布尔 + 需要额外说明的
|
|
// 三态投影。
|
|
//
|
|
// 适配器实现了 connectOutcomeAware ⇒ **以它为准**:闭集三态优先于 IsConnected() 的
|
|
// 单个布尔(pending 与 ready 的 IsConnected() 都是 false,但两者都不是「未连接」这样
|
|
// 一个静态结论)。没实现,或返回空串「无意见」⇒ 逐字退回今天的行为(只看
|
|
// IsConnected(),三态投影为零值、可选键缺席)。
|
|
func channelConnectStateOf(ch sdk.Channel) (bool, channelConnectOutcome) {
|
|
if ch == nil {
|
|
return false, channelConnectOutcome{}
|
|
}
|
|
aware, ok := asConnectOutcomeAware(ch)
|
|
if !ok {
|
|
return ch.IsConnected(), channelConnectOutcome{}
|
|
}
|
|
state, reason := aware.ConnectOutcome()
|
|
switch state {
|
|
case adapter.ConnectStateConnected:
|
|
return true, channelConnectOutcome{}
|
|
case adapter.ConnectStatePending:
|
|
return false, channelConnectOutcome{Pending: true, Reason: reason}
|
|
case adapter.ConnectStateReady:
|
|
return false, channelConnectOutcome{Ready: true}
|
|
default:
|
|
return ch.IsConnected(), channelConnectOutcome{}
|
|
}
|
|
}
|