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
@@ -159,7 +159,15 @@ type Adapter struct {
|
||||
|
||||
mu sync.RWMutex
|
||||
connected bool
|
||||
qrcode string
|
||||
// expired marks a session iLink rejected with errcode=-14: the bot token is
|
||||
// dead and only a fresh scan-code login re-binds it (session_state.go).
|
||||
// expiredToken records WHICH token was rejected, so a subsequently configured
|
||||
// token clears the state; rescanDetail is the panel-visible reason. All three
|
||||
// are guarded by mu.
|
||||
expired bool
|
||||
expiredToken string
|
||||
rescanDetail string
|
||||
qrcode string
|
||||
// pollBaseURL optionally overrides cfg.BaseURL for get_qrcode_status polling.
|
||||
// It is set when the server returns scaned_but_redirect with a redirect_host,
|
||||
// so subsequent status polls follow the IDC host. Guarded by mu.
|
||||
@@ -230,9 +238,19 @@ func (a *Adapter) Config() map[string]any {
|
||||
}
|
||||
|
||||
// Connect establishes the iLink session. It is idempotent. When a bot_token is
|
||||
// already configured it is treated as a live session; otherwise it runs the
|
||||
// scan-code login flow (see login.go).
|
||||
// already configured and usable it is treated as a live session; otherwise it
|
||||
// runs the scan-code login flow (see login.go).
|
||||
//
|
||||
// A token iLink already rejected with errcode=-14 is NOT usable: Connect refuses
|
||||
// it with ErrSessionExpired (re-scan required) instead of reporting a session
|
||||
// that the very next poll would tear down, and the scan-code login flow is the
|
||||
// way back (see login_admin.go StartLogin).
|
||||
func (a *Adapter) Connect(ctx context.Context) error {
|
||||
if expired, reason := a.expiredState(); expired {
|
||||
a.log.Error("ilink connect refused: session expired, 需要重新扫码登录",
|
||||
"bot_id", a.botID(), "reason", reason)
|
||||
return fmt.Errorf("%w: %s", ErrSessionExpired, reason)
|
||||
}
|
||||
a.mu.RLock()
|
||||
connected := a.connected
|
||||
token := a.cfg.BotToken
|
||||
@@ -250,10 +268,18 @@ func (a *Adapter) Connect(ctx context.Context) error {
|
||||
a.mu.Lock()
|
||||
a.connected = true
|
||||
a.mu.Unlock()
|
||||
a.log.Info("ilink session established", "bot_id", a.cfg.BotID)
|
||||
a.log.Info("ilink session established", "bot_id", a.botID())
|
||||
return nil
|
||||
}
|
||||
|
||||
// botID returns the configured bot identifier; it is the only account identifier
|
||||
// safe to log (the bot token is a credential and never reaches a log line).
|
||||
func (a *Adapter) botID() string {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
return a.cfg.BotID
|
||||
}
|
||||
|
||||
// Disconnect tears down the session. It is idempotent.
|
||||
func (a *Adapter) Disconnect(ctx context.Context) error {
|
||||
a.mu.Lock()
|
||||
@@ -274,9 +300,15 @@ func (a *Adapter) IsConnected() bool {
|
||||
}
|
||||
|
||||
// HealthCheck verifies the session is usable: it must be connected and carry a
|
||||
// bot_token. It does not probe the network so callers can use it cheaply after
|
||||
// a reconnect without incurring a request.
|
||||
// bot_token. An expired session (iLink rejected the token with errcode=-14) is
|
||||
// reported as ErrSessionExpired — "re-scan required" — so a caller can tell a
|
||||
// dead credential apart from a plain disconnected adapter. It does not probe the
|
||||
// network so callers can use it cheaply after a reconnect without incurring a
|
||||
// request.
|
||||
func (a *Adapter) HealthCheck(ctx context.Context) error {
|
||||
if expired, reason := a.expiredState(); expired {
|
||||
return fmt.Errorf("%w: %s", ErrSessionExpired, reason)
|
||||
}
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if !a.connected || a.cfg.BotToken == "" {
|
||||
|
||||
Reference in new issue
Block a user