Files
AskaEth b4d55f412e
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(channel): 连接动作不再乐观 —— 适配器自报连接态三态 + 面板如实呈现
根因: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 层脱敏实现,发现原两条脱敏用例其实钉不住它(值恰好等于已知明文/被形状级顺带吃掉)⇒ 补一条钉住它独有窗口的用例(短值+紧邻空白),变异下必红、还原后绿。
2026-10-13 19:38:56 +08:00

83 lines
4.7 KiB
Go
Raw Permalink 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 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 "", ""
}
}