feat(weixin-ilink,gateway): 会话过期显式化 — 不再静默变哑/假 OK 重连,面板可见「需重扫」
- 适配器新增过期状态机(plugins/channels/weixin_ilink/session_state.go):getupdates/sendmessage/getuploadurl 收到 errcode=-14 即 markExpired(ERROR 仅一次,只带 bot_id,token 永不入日志),扫码登录拿到新 token 或配置换新 token 时清除(惰性比对) - Connect/HealthCheck 在过期时返回 ErrSessionExpired(可与 ErrNotConnected 区分):不再对死 token 设 connected=true 报假成功;发送路径同样返回该错误而不是笼统 ErrNotConnected - 网关:POST /admin/channels/open 遇过期 → 409 session_expired + 明确文案(不再吞错回 200);GET /admin/channels 摘要新增 needsRescan/lastError(经可选接口类型断言读取,取不到不伪造) - channelbridge:读循环非取消退出时把自身记账的 connected 置 false(热重载被替换者退出不抹掉新实例) - login/login_admin:过期时 StartLogin 允许重新出码、LoginStatus 如实报 failed(原先可能把死 token 当 confirmed 重新落盘) - Docs/api-client v0.39;新增 5 组测试 + 变异验证(去掉标记/恢复假成功/网关不填字段 均 FAIL)
This commit is contained in:
1 parent
2092094d0a
commit
cdc370dc82
14 files changed
+975
-30
No files matched your search
@@ -0,0 +1,62 @@
|
||||
package gateway
|
||||
|
||||
import "reflect"
|
||||
|
||||
// This file holds the optional "the adapter needs the owner to re-authorize it"
|
||||
// capability surfaced by GET /api/v1/admin/channels (v0.39). It lives next to —
|
||||
// not inside — the channel summary itself because the summary list is one of
|
||||
// admin.go's larger handlers and the unwrap helper is reusable.
|
||||
|
||||
// rescanAware is the optional capability a channel adapter exposes when its
|
||||
// session needs owner re-authorization that a reconnect cannot provide.
|
||||
// *weixin_ilink.Adapter implements it (RescanRequired): iLink answering
|
||||
// errcode=-14 means the bot token is dead and the owner must scan a fresh QR. The
|
||||
// core adapter.PlatformAdapter contract is deliberately NOT extended, so adapters
|
||||
// that do not need the capability stay untouched.
|
||||
//
|
||||
// A value is only ever reported when the adapter really produced it — a platform
|
||||
// without the capability keeps the fields omitted rather than shipping a guess.
|
||||
type rescanAware interface {
|
||||
// RescanRequired reports whether a new scan-code login is required, and why
|
||||
// (reason is "" when it is not).
|
||||
RescanRequired() (required bool, reason string)
|
||||
}
|
||||
|
||||
// asRescanAware reports whether a registered channel value exposes the rescan
|
||||
// capability. The registration wraps an adapter in cmd/cyrene's namedAdapter — an
|
||||
// unexported struct embedding adapter.PlatformAdapter — which promotes only the
|
||||
// interface's methods, so the concrete adapter's extra capability is reachable
|
||||
// only by unwrapping the embedded field. A direct adapter value (tests,
|
||||
// single-account registrations) is accepted as-is.
|
||||
func asRescanAware(v any) (rescanAware, bool) {
|
||||
if ra, ok := v.(rescanAware); ok {
|
||||
return ra, true
|
||||
}
|
||||
inner, ok := unwrapNamedAdapter(v)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
ra, ok := inner.(rescanAware)
|
||||
return ra, ok
|
||||
}
|
||||
|
||||
// unwrapNamedAdapter returns the adapter a registration wrapper embeds in its
|
||||
// exported PlatformAdapter field. asKookAdmin and asWeixinLoginAdmin perform the
|
||||
// same unwrap for their own capability subsets and could share this helper.
|
||||
func unwrapNamedAdapter(v any) (any, bool) {
|
||||
rv := reflect.ValueOf(v)
|
||||
if !rv.IsValid() {
|
||||
return nil, false
|
||||
}
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
if rv.Kind() != reflect.Struct {
|
||||
return nil, false
|
||||
}
|
||||
f := rv.FieldByName("PlatformAdapter")
|
||||
if !f.IsValid() || !f.CanInterface() {
|
||||
return nil, false
|
||||
}
|
||||
return f.Interface(), true
|
||||
}
|
||||
Reference in new issue
Block a user