fix(channel): 连接动作不再乐观 —— 适配器自报连接态三态 + 面板如实呈现
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

根因: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 层脱敏实现,发现原两条脱敏用例其实钉不住它(值恰好等于已知明文/被形状级顺带吃掉)⇒ 补一条钉住它独有窗口的用例(短值+紧邻空白),变异下必红、还原后绿。
This commit is contained in:
AskaEth committed 2026-10-13 19:38:56 +08:00
1 parent d81635672e
commit b4d55f412e
23 files changed
+1440 -27

No files matched your search

@@ -135,11 +135,38 @@ func (s *Server) firstConfiguredChannel(name string) (sdk.Channel, bool) {
// channelActionResult 是 open/close 的响应体:name 保持请求里的名字(老客户端
// 契约不变),resolved 是**实际被驱动**的适配器注册名(多账号平台上它与请求的
// 平台名不同,面板据此显示「连的是哪一条」),accountId 在指定时回显。凭据永不出现。
func channelActionResult(target channelTarget, resolved string, connected bool) map[string]any {
//
// 除既有四个键外,另按适配器自报的**连接态三态**(见 admin_channel_connect_outcome.go)
// 补三个**可选**键,且取不到就整键缺席(同 NeedsRescan / LastError 口径):
//
// pending: true —— 已在后台重试、当前未连上 ⇒ 此时 connected=false。面板须读作
// 「连接中(reason)」,**不是**「未连接」,更不是「已连接」;
// ready: true —— 已就绪、等待对端接入(server 模式)⇒ 同样 connected=false,
// 但它不是失败;
// reason: "<已脱敏>" —— 未连上时的原因,仅在 pending 时可能出现。
//
// 真连上时这三个键一律缺席:connected=true 已经把话说完了,再补一个「没有坏消息」的
// 空串只会让老客户端多出一个要处理的键。适配器没实现这条状态面时同理 —— 响应与今天
// 逐字节一致。
func channelActionResult(
target channelTarget,
resolved string,
connected bool,
outcome channelConnectOutcome,
) map[string]any {
out := map[string]any{"name": target.Name, "resolved": resolved, "connected": connected}
if target.AccountID != "" {
out["accountId"] = target.AccountID
}
if outcome.Pending {
out["pending"] = true
}
if outcome.Ready {
out["ready"] = true
}
if outcome.Reason != "" {
out["reason"] = outcome.Reason
}
return out
}