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
@@ -144,7 +144,9 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
|
||||
|
||||
// completeLogin persists the confirmed credentials and marks the session live.
|
||||
// It writes cfg under a.mu, then clears the session under loginMu (never nesting
|
||||
// the two locks), and finally signals the waiting Connect path.
|
||||
// the two locks), and finally signals the waiting Connect path. A confirmed scan
|
||||
// delivers a fresh bot_token, which is the canonical way OUT of the expired state
|
||||
// (session_state.go): the rescan flag is cleared here.
|
||||
func (a *Adapter) completeLogin(sess *loginSession, status qrcodeStatusResp) {
|
||||
a.mu.Lock()
|
||||
a.cfg.BotToken = status.BotToken
|
||||
@@ -153,6 +155,7 @@ func (a *Adapter) completeLogin(sess *loginSession, status qrcodeStatusResp) {
|
||||
if status.BaseURL != "" {
|
||||
a.cfg.BaseURL = status.BaseURL
|
||||
}
|
||||
a.clearRescanLocked()
|
||||
a.connected = true
|
||||
a.loginStatus = LoginStatusConfirmed
|
||||
a.loginDetail = ""
|
||||
@@ -171,11 +174,15 @@ func (a *Adapter) completeLogin(sess *loginSession, status qrcodeStatusResp) {
|
||||
|
||||
// completeBindedRedirect treats binded_redirect as an already-bound outcome: the
|
||||
// scanned bot is already linked to this instance, so the server issues no new
|
||||
// credentials and any existing token remains valid.
|
||||
// credentials and any existing token remains valid. Because NO new token arrives,
|
||||
// an expired session stays expired here — binded_redirect carries no proof that a
|
||||
// token iLink already rejected with errcode=-14 works again, so the adapter does
|
||||
// not claim a live session on the strength of it. A confirmed scan (completeLogin)
|
||||
// is what clears the expired state.
|
||||
func (a *Adapter) completeBindedRedirect(sess *loginSession) {
|
||||
a.log.Info("weixin_ilink: login already bound (binded_redirect)")
|
||||
a.mu.Lock()
|
||||
a.connected = a.cfg.BotToken != ""
|
||||
a.connected = a.cfg.BotToken != "" && !a.expiredLocked()
|
||||
a.loginStatus = LoginStatusConfirmed
|
||||
a.loginDetail = loginBindedRedirectDetail
|
||||
a.mu.Unlock()
|
||||
@@ -211,8 +218,14 @@ func (a *Adapter) failLogin(sess *loginSession, err error) {
|
||||
// localTokenList returns the locally-held bot tokens to advertise on
|
||||
// get_bot_qrcode, so an already-bound bot can be detected (binded_redirect). On a
|
||||
// fresh bind it returns an empty (non-nil) slice so the wire carries `[]` rather
|
||||
// than `null`, matching the official client.
|
||||
// than `null`, matching the official client. An EXPIRED token is deliberately not
|
||||
// advertised: it is not a usable bind, and offering it lets the server answer
|
||||
// binded_redirect (no new credentials) — which would leave the owner stuck with a
|
||||
// dead token and no way back. Reporting `[]` asks for a real rebind.
|
||||
func (a *Adapter) localTokenList() []string {
|
||||
if expired, _ := a.expiredState(); expired {
|
||||
return []string{}
|
||||
}
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.cfg.BotToken == "" {
|
||||
|
||||
Reference in new issue
Block a user