fix(weixin_ilink): 已扫待确认不重新取码 + scaned 状态暴露 + 扫码链路日志
StartLogin 换码保护: 当 runLogin 观测到 wire 状态已进入 scaned/scaned_but_redirect/need_verifycode(confirmingPhase)时, 复用当前 loginSession 的 QR 而不重新 fetchQRCode/cancel 旧 poll, 避免 45s 自动刷新/恢复前台把已扫未确认的会话砍掉导致微信 confirm 丢失。 仍保留 qrReuseWindow: 未扫描(等待中)且超窗口才重取, 保留显式刷新二维码要新码。 runLogin 补 case scaned: setLoginState(LoginStatusWaiting, loginScannedDetail), 沿用 Waiting 不新增破坏性状态; 顺带在会话上记录 wire phase 供 StartLogin 判别。 可观测性: fetchQRCode 取码成功记 get_bot_qrcode ok(url 脱敏, 不含 qrcode), pollQRStatus 状态变化才记 get_qrcode_status(status), completeLogin/failLogin/ completeBindedRedirect 记结果; 均不落 qrcode/bot_token/user_id。 failLogin 修正: 被 re-mint superseded 的旧会话不再 clobber 新会话状态(不置 failed)。 新增测试: scaned 暴露到 LoginStatus; scaned 超窗口复用 QR 不重取; 等待中超窗口 重取; 日志脱敏断言。
This commit is contained in:
1 parent
4223f5a19a
commit
c515de28bf
3 files changed
+293
-10
No files matched your search
@@ -74,6 +74,7 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
|
||||
ticker := time.NewTicker(a.cfg.LoginInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
prevStatus := ""
|
||||
for {
|
||||
status, err := a.pollQRStatus(timeoutCtx, sess.qrcode)
|
||||
if err != nil {
|
||||
@@ -86,6 +87,15 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
|
||||
}
|
||||
return
|
||||
}
|
||||
// Track the raw wire phase so StartLogin can protect a scanned/confirming bind
|
||||
// from being re-minted. Log only on a status change to avoid noise, and never
|
||||
// the qrcode identifier or tokens.
|
||||
sess.setPhase(status.Status)
|
||||
if status.Status != prevStatus {
|
||||
prevStatus = status.Status
|
||||
a.log.Info("weixin_ilink: get_qrcode_status", "status", status.Status)
|
||||
}
|
||||
|
||||
switch status.Status {
|
||||
case "confirmed":
|
||||
if status.BotToken == "" {
|
||||
@@ -102,6 +112,12 @@ func (a *Adapter) runLogin(sess *loginSession, sessCtx context.Context) {
|
||||
case "verify_code_blocked":
|
||||
a.failLogin(sess, fmt.Errorf("weixin_ilink: verify code blocked, too many failed attempts"))
|
||||
return
|
||||
case "scaned":
|
||||
// The code has been scanned and awaits confirmation on the phone. Keep the
|
||||
// session and keep polling — re-minting here would discard the pending
|
||||
// confirmation. Expose the state through the backward-compatible waiting
|
||||
// status plus a detail so the panel can prompt the user to confirm.
|
||||
a.setLoginState(LoginStatusWaiting, loginScannedDetail)
|
||||
case "scaned_but_redirect":
|
||||
if status.RedirectHost != "" {
|
||||
a.setPollBaseURL(status.RedirectHost)
|
||||
@@ -142,6 +158,8 @@ func (a *Adapter) completeLogin(sess *loginSession, status qrcodeStatusResp) {
|
||||
a.loginDetail = ""
|
||||
a.mu.Unlock()
|
||||
|
||||
a.log.Info("weixin_ilink: login confirmed", "bot_id", status.BotID)
|
||||
|
||||
a.loginMu.Lock()
|
||||
if a.loginSession == sess {
|
||||
a.loginSession = nil
|
||||
@@ -155,6 +173,7 @@ func (a *Adapter) completeLogin(sess *loginSession, status qrcodeStatusResp) {
|
||||
// scanned bot is already linked to this instance, so the server issues no new
|
||||
// credentials and any existing token remains valid.
|
||||
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.loginStatus = LoginStatusConfirmed
|
||||
@@ -170,16 +189,22 @@ func (a *Adapter) completeBindedRedirect(sess *loginSession) {
|
||||
sess.done <- nil
|
||||
}
|
||||
|
||||
// failLogin records a failed scan-login and signals the waiting Connect path.
|
||||
// failLogin records a failed scan-login and signals the waiting Connect path. A
|
||||
// superseded session (one cancelled so StartLogin could mint a fresh QR for an
|
||||
// unscanned code) never clobbers the newer session's state, but its Connect wait
|
||||
// must still be released.
|
||||
func (a *Adapter) failLogin(sess *loginSession, err error) {
|
||||
a.setLoginState(LoginStatusFailed, err.Error())
|
||||
|
||||
a.loginMu.Lock()
|
||||
if a.loginSession == sess {
|
||||
isCurrent := a.loginSession == sess
|
||||
if isCurrent {
|
||||
a.loginSession = nil
|
||||
}
|
||||
a.loginMu.Unlock()
|
||||
|
||||
if isCurrent {
|
||||
a.log.Warn("weixin_ilink: login failed", "err", err)
|
||||
a.setLoginState(LoginStatusFailed, err.Error())
|
||||
}
|
||||
sess.done <- err
|
||||
}
|
||||
|
||||
@@ -212,6 +237,9 @@ func (a *Adapter) fetchQRCode(ctx context.Context) (qrcodeResp, error) {
|
||||
if out.Qrcode == "" {
|
||||
return qrcodeResp{}, fmt.Errorf("weixin_ilink: get_bot_qrcode returned empty qrcode")
|
||||
}
|
||||
// Log only the renderable QR URL, never the raw qrcode identifier (it is a secret
|
||||
// echoed to get_qrcode_status). The timestamp is attached by the logger.
|
||||
a.log.Info("weixin_ilink: get_bot_qrcode ok", "url", out.QrcodeImgContent)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user