feat(channel): 微信 iLink 账号状态三态(诚实口径)

state ∈ 有效 / **被拒(需重扫)** / 未知;🔴 -14 是**被拒不是过期**,且**未知原因绝不
写成「过期」**(面板文案与错误码都如实)。三态判定为纯函数 + 三态各有用例。

⚠️ 未做(如实登记): 三态(每账号逐条三态)本片未做。
This commit is contained in:
AskaEth committed 2026-10-06 09:03:44 +08:00
1 parent f75e7a9c9b
commit 7505c46b70
8 files changed
+808 -17

No files matched your search

@@ -135,3 +135,77 @@ func (s *Server) liveAccountOnline() map[string]map[string]bool {
}
return out
}
// sessionStatusAware 是渠道适配器暴露「**会话凭据三态**」(有效 / 被拒 / 未知)的
// 可选能力。*weixin_ilink.Adapter 实现了它(SessionStatus)。
//
// 为什么需要它:rescanAware(admin_channels_rescan.go)只回答「要不要重新扫码」,于是
// 「凭据确实被 iLink 接受过」与「有凭据、但从没被接受过」在面板上长得一模一样 —— 而
// 后者只是**还没被否定**。三态把这两者分开,并把如实的说明(detail)一起交给面板,
// 使面板不必自己编造措辞(编造正是「未知」被写成「过期」的来源)。
//
// 与 accountStatusAware 同一契约风格:核心 SDK 契约被刻意不扩展,因此没有这条状态面
// 的适配器不受任何改动,也不会凭空多出一个字段。
type sessionStatusAware interface {
// SessionStatus 报告会话凭据状态。state 是闭集取值(当前为
// "active"/"rejected"/"unknown",定义在 plugins/channels/weixin_ilink 的
// SessionState* 常量),detail 是如实的中文说明(绝不含凭据)。
SessionStatus() (state string, detail string)
}
// asSessionStatusAware 报告已注册的渠道值是否暴露会话三态能力。与 asAccountStatusAware
// 同一套路:注册时会把适配器包进 cmd/cyrene 的 namedAdapter —— 一个内嵌
// adapter.PlatformAdapter 的未导出结构体 —— 它只提升接口的方法,因此具体适配器多出的
// 能力只能通过拆开被内嵌字段才能抵达。
func asSessionStatusAware(v any) (sessionStatusAware, bool) {
if ss, ok := v.(sessionStatusAware); ok {
return ss, true
}
inner, ok := unwrapNamedAdapter(v)
if !ok {
return nil, false
}
ss, ok := inner.(sessionStatusAware)
return ss, ok
}
// sessionStatus 是一个账号的会话凭据三态快照:State ∈ {"active","rejected","unknown"}
// (定义在 plugins/channels/weixin_ilink 的 SessionState* 常量),Detail 是如实的说明。
type sessionStatus struct {
State string
Detail string
}
// liveAccountAuthStates 收集每个平台各账号的**会话凭据三态**,键与
// liveAccountConnections 相同(裸平台名 ⇒ 空账号 id)。
//
// 与 liveAccountOnline 的关键差别:三态里的「未知」是一个**值**("unknown"),不是
// 缺席 —— 它必须带着「为什么还未知」一起上屏,否则面板只能写一句它自己编的话(而
// 「未知」被编成「过期」正是本切片要根除的那句话)。缺席只留给「该账号没有这条状态面」
// (适配器没实现 sessionStatusAware),两者不可互相冒充。
func (s *Server) liveAccountAuthStates() map[string]map[string]sessionStatus {
out := make(map[string]map[string]sessionStatus)
if s.caps == nil {
return out
}
for _, cap := range s.caps.List(capreg.KindChannel) {
ch, ok := cap.Value.(sdk.Channel)
if !ok {
continue
}
ss, ok := asSessionStatusAware(cap.Value)
if !ok {
continue
}
state, detail := ss.SessionStatus()
if state == "" {
continue
}
platform, acct := splitAdapterName(ch.Name())
if out[platform] == nil {
out[platform] = make(map[string]sessionStatus)
}
out[platform][acct] = sessionStatus{State: state, Detail: detail}
}
return out
}