feat(gateway): 账号在线三态(后端 AccountStatus 可选接口 + 面板第二枚标签 + 契约账目更正)
- known=false ⇒ accountOnline 键**缺席**(未知 ≠ 离线),与既有 connected 语义并列不合并 - 面板:账号在线/账号离线/账号状态未知 三态 + 中性灰 tone + 「这不等于离线」悬停说明 - 测试:gateway 跨层锁「键缺席」(用真 qq_obv11.Adapter)+ 面板 61/40 断言 - 变异:known 当离线 ⇒ 两条测试同时红(已复跑确认) - Docs/api-client.md:§7.2 那行由未实现改已实现(留表,注明下版移入 §7.1)
This commit is contained in:
1 parent
7ae4298871
commit
52dac4575f
10 files changed
+456
-16
No files matched your search
@@ -38,16 +38,100 @@ func (s *Server) liveAccountConnections() map[string]map[string]bool {
|
||||
// 配对,与适配器注册名一致)。没有实时适配器的账号保持 Connected == nil(未知),
|
||||
// 因此面板能在「没连上」与「读不到」之间如实区分。入参切片不被修改。
|
||||
func enrichAccountsWithConnection(accts []AdminChannelAccount, by map[string]bool) []AdminChannelAccount {
|
||||
return enrichAccountBool(accts, by, func(a *AdminChannelAccount, v bool) { a.Connected = &v })
|
||||
}
|
||||
|
||||
// enrichAccountsWithAccountOnline 把**确知的**账号在线状态合并进平台中匹配的账号
|
||||
// (按账号 id 配对)。不在 by 里的账号保持 AccountOnline == nil ⇒ 字段缺席 ⇒ 未知,
|
||||
// 因此面板能在「确知离线」与「没探到」之间如实区分。入参切片不被修改。
|
||||
func enrichAccountsWithAccountOnline(accts []AdminChannelAccount, by map[string]bool) []AdminChannelAccount {
|
||||
return enrichAccountBool(accts, by, func(a *AdminChannelAccount, v bool) { a.AccountOnline = &v })
|
||||
}
|
||||
|
||||
// enrichAccountBool 是上面两个「可选布尔状态」装配器的共同实现:只对 by 里出现过的
|
||||
// 账号调用 set(缺席即字段保持 nil = 未知),因此调用方不必各自重写这段三态逻辑 ——
|
||||
// 三态口径只写一遍,谁也别想在其中一处悄悄把「未知」写成 false。
|
||||
func enrichAccountBool(
|
||||
accts []AdminChannelAccount,
|
||||
by map[string]bool,
|
||||
set func(a *AdminChannelAccount, v bool),
|
||||
) []AdminChannelAccount {
|
||||
if len(by) == 0 {
|
||||
return accts
|
||||
}
|
||||
out := make([]AdminChannelAccount, 0, len(accts))
|
||||
for _, a := range accts {
|
||||
if connected, ok := by[a.ID]; ok {
|
||||
value := connected
|
||||
a.Connected = &value
|
||||
item := a
|
||||
if v, ok := by[item.ID]; ok {
|
||||
set(&item, v)
|
||||
}
|
||||
out = append(out, a)
|
||||
out = append(out, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// accountStatusAware 是渠道适配器暴露「账号在平台上是否在线」这一**独立状态面**的
|
||||
// 可选能力。*qq_obv11.Adapter 实现了它(AccountStatus):OneBot 实现(NapCat 等)的
|
||||
// 进程与正向 WS 可以一直连着,而它背后的 QQ 账号已经掉线 —— 此时「我们连着」
|
||||
// (connected)为真,消息却既收不到也发不出(2026-10-05 事故)。
|
||||
//
|
||||
// 该能力返回**两个**布尔而不是一个,是因为 `known` 必须能传到线上:调用方要把
|
||||
// 「尚未探到」处理成**字段缺席**(未知),绝不能用 false 冒充「确知离线」。
|
||||
//
|
||||
// 核心的 SDK Channel 契约被刻意不扩展,因此没有这条状态面的平台(telegram、webhook……)
|
||||
// 不受任何改动,也不会凭空多出一个字段。
|
||||
type accountStatusAware interface {
|
||||
// AccountStatus 报告账号在线状态;known=false 表示尚未探到结果
|
||||
// (探活未接线 / 刚启动 / 一直失败)。
|
||||
AccountStatus() (online bool, known bool)
|
||||
}
|
||||
|
||||
// asAccountStatusAware 报告已注册的渠道值是否暴露账号在线状态能力。与 asRescanAware
|
||||
// 同一套路(见 admin_channels_rescan.go):注册时会把适配器包进 cmd/cyrene 的
|
||||
// namedAdapter —— 一个内嵌 adapter.PlatformAdapter 的未导出结构体 —— 它只提升接口的
|
||||
// 方法,因此具体适配器多出的能力只能通过拆开被内嵌字段才能抵达。
|
||||
func asAccountStatusAware(v any) (accountStatusAware, bool) {
|
||||
if as, ok := v.(accountStatusAware); ok {
|
||||
return as, true
|
||||
}
|
||||
inner, ok := unwrapNamedAdapter(v)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
as, ok := inner.(accountStatusAware)
|
||||
return as, ok
|
||||
}
|
||||
|
||||
// liveAccountOnline 收集每个平台各账号**在平台侧**的在线状态,键与
|
||||
// liveAccountConnections 相同(裸平台名 ⇒ 空账号 id)。
|
||||
//
|
||||
// 与 liveAccountConnections 的关键差别:**只收「确知」的结果**。适配器没有这条独立
|
||||
// 状态面,或实现了但 known=false(刚启动 / 还没探到 / 一直失败)时该账号**不进 map** ——
|
||||
// 缺席 = 状态未知。补一个 false 顶上去,等于把「没探到」谎报成「账号离线」,与本次
|
||||
// 事故(面板一片绿 ↔ 账号已掉线)同属一类:画出来的与事实无关。
|
||||
func (s *Server) liveAccountOnline() map[string]map[string]bool {
|
||||
out := make(map[string]map[string]bool)
|
||||
if s.caps == nil {
|
||||
return out
|
||||
}
|
||||
for _, cap := range s.caps.List(capreg.KindChannel) {
|
||||
ch, ok := cap.Value.(sdk.Channel)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
as, ok := asAccountStatusAware(cap.Value)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
online, known := as.AccountStatus()
|
||||
if !known {
|
||||
continue
|
||||
}
|
||||
platform, acct := splitAdapterName(ch.Name())
|
||||
if out[platform] == nil {
|
||||
out[platform] = make(map[string]bool)
|
||||
}
|
||||
out[platform][acct] = online
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user