diff --git a/plugins/channels/weixin_ilink/login_admin.go b/plugins/channels/weixin_ilink/login_admin.go index 50d9ced..265439a 100644 --- a/plugins/channels/weixin_ilink/login_admin.go +++ b/plugins/channels/weixin_ilink/login_admin.go @@ -8,6 +8,7 @@ package weixin_ilink // concurrent QR binds. import ( + "time" "context" "errors" "fmt" @@ -41,14 +42,20 @@ type LoginQR struct { // loginSession tracks one scan-login run. It is guarded by Adapter.loginMu. type loginSession struct { - qrcode string - image string + qrcode string + image string + startedAt time.Time // cancel stops the poll goroutine early (e.g. Connect's deadline elapsed). cancel context.CancelFunc // done carries the login outcome; the poll goroutine writes exactly once. done chan error } +// qrRefreshTTL bounds how long a WeChat bot QR is usable before StartLogin re-mints +// a fresh one. iLink bot QRs are short-lived; an idempotent StartLogin must not keep +// serving a long-expired QR (the user would scan it and get "二维码过期"). +const qrRefreshTTL = 90 * time.Second + // StartLogin begins a scan-login and returns the QR code to display. It is // idempotent: when a login is already in progress it returns the current QR // without issuing a new get_bot_qrcode, and when a bot token is already present it @@ -63,7 +70,15 @@ func (a *Adapter) StartLogin(ctx context.Context) (LoginQR, error) { return LoginQR{}, ErrAlreadyLoggedIn } if sess := a.loginSession; sess != nil { - return LoginQR{Qrcode: sess.qrcode, Image: sess.image}, nil + // Idempotent for a fresh QR, but re-mint once the current QR has aged past + // qrRefreshTTL so the panel's 刷新二维码 always yields a scannable code. + if time.Since(sess.startedAt) < qrRefreshTTL { + return LoginQR{Qrcode: sess.qrcode, Image: sess.image}, nil + } + if sess.cancel != nil { + sess.cancel() + } + a.loginSession = nil } raw, err := a.fetchQRCode(ctx) @@ -74,10 +89,11 @@ func (a *Adapter) StartLogin(ctx context.Context) (LoginQR, error) { sessCtx, cancel := context.WithCancel(context.Background()) sess := &loginSession{ - qrcode: raw.Qrcode, - image: raw.QrcodeImgContent, - cancel: cancel, - done: make(chan error, 1), + qrcode: raw.Qrcode, + image: raw.QrcodeImgContent, + startedAt: time.Now(), + cancel: cancel, + done: make(chan error, 1), } a.loginSession = sess a.setLoginState(LoginStatusWaiting, "")