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
@@ -22,11 +22,41 @@ import (
|
||||
// deliver through the same pipeline as push-style adapters. Message processing
|
||||
// uses the bridge-lifetime context (not readCtx) so a reload leaves in-flight
|
||||
// messages alone.
|
||||
//
|
||||
// When the read path returns with readCtx still alive, the adapter stopped
|
||||
// reading on its own — today that is the weixin-iLink expired session
|
||||
// (errcode=-14, see plugins/channels/weixin_ilink/session_state.go), which used to
|
||||
// leave the bridge claiming the channel was connected while it could no longer
|
||||
// deliver anything. A cancelled readCtx is a deliberate stop (bridge Stop or a
|
||||
// hot reload of this name) and is handled by the caller that cancelled it.
|
||||
func (b *Bridge) readLoop(a adapter.PlatformAdapter, readCtx context.Context) {
|
||||
defer b.wg.Done()
|
||||
a.ReadMessages(readCtx, func(msg adapter.UnifiedMessage) {
|
||||
b.onMessage(b.ctx, msg, a)
|
||||
})
|
||||
if readCtx.Err() == nil {
|
||||
b.markReadStopped(a)
|
||||
}
|
||||
}
|
||||
|
||||
// markReadStopped records that an adapter's read loop ended by itself while the
|
||||
// adapter reports no live session, so the per-name accounting (IsConnected /
|
||||
// ConnectedCount) stops claiming a channel that cannot deliver. It only ever
|
||||
// clears the flag — never sets it — so it cannot make a connected channel look
|
||||
// alive, and it is a no-op when the adapter still reports a live session (a
|
||||
// reconnect that already happened is never undone by an older loop's exit).
|
||||
func (b *Bridge) markReadStopped(a adapter.PlatformAdapter) {
|
||||
if a.IsConnected() {
|
||||
return
|
||||
}
|
||||
name := a.Name()
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if !b.connected[name] {
|
||||
return
|
||||
}
|
||||
b.connected[name] = false
|
||||
b.log.Warn("channel read loop ended, adapter is no longer connected", "channel", name)
|
||||
}
|
||||
|
||||
// onMessage is the adapter-facing inbound callback. It runs the pipeline in a
|
||||
|
||||
Reference in new issue
Block a user