Skip to content

fix: handle unsequenced QQ authentication failures - #856

Open
HLfromZ wants to merge 2 commits into
LLOneBot:mainfrom
HLfromZ:codex/fix-direct-session-auth
Open

fix: handle unsequenced QQ authentication failures#856
HLfromZ wants to merge 2 commits into
LLOneBot:mainfrom
HLfromZ:codex/fix-direct-session-auth

Conversation

@HLfromZ

@HLfromZ HLfromZ commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

问题与影响

LLBot Direct 模式下,QQ 服务端拒绝当前会话时,可以返回 retCode=-10001seq=0cmd="" 的 SSO 响应。原实现先按请求序号查找等待中的请求,因此这类无法匹配请求的鉴权错误会进入 push 分支,随后因命令为空而未被处理。

结果是:实际业务请求持续超时,QQ 消息停止上报,但 TCP 连接、连接级心跳和 OneBot WebSocket 仍可保持工作,get_status 也仍返回 online=true。使用者看到的是机器人长时间不响应,却没有明确的重新登录提示。

本 PR 在请求匹配前处理会话鉴权失败,立即使失效会话退出在线状态,并进入已有的二维码登录流程。

现场环境与脱敏说明

  • 故障版本:LLBot 8.1.10,Docker 部署,Direct 模式(原生签名 + TCP)。
  • 下游:AstrBot,通过 OneBot v11 反向 WebSocket 接入。
  • 故障期间 WebUI 和本地 OneBot 连接仍可访问;容器存活不能证明 QQ 会话有效。
  • 以下时间统一为 2026-09-06,Asia/Shanghai(UTC+8);抓包工具原先输出 UTC,展示时已换算。
  • 原始日志仅节选与诊断有关的行。账号、UID、群号、昵称、IP、主机名及部署端口均删除或替换为 <...>。未附带消息正文、Authorization、会话凭据、密钥、二维码、配置文件或原始抓包文件。
  • seq=0、错误码、命令名和超时时长属于定位问题所需的协议字段,予以保留。抓包解析结果与 LLBot 原生日志分开展示。

故障时间线与日志证据

时间 观察结果
16:07:59 首次记录到会话心跳 SsoHeartBeat 超时。
16:10:07 群历史查询相关请求开始记录 15 秒超时。
16:20:02 本次排查所见的最后一条 QQ 群消息上报;因此不能把首次心跳超时直接当作消息完全中断的时刻。
19:25:59 会话心跳仍在超时,故障持续数小时。
19:28 左右 get_status 仍显示在线;绕过缓存查询群信息却在约 15 秒后失败。
19:39–19:42 抓包确认 TCP 双向传输仍在继续,并解析出服务端明确的鉴权失败响应;连接级 Heartbeat.Alive 仍成功。
19:46:15 手动重新扫码登录成功;后续只读群信息查询恢复成功。此恢复发生在安装本 PR 补丁之前。

1. LLBot 原生日志节选

以下日志只替换了部署地址与身份字段;中间无关行已省略:

2026-09-06 16:07:59 [E] heartbeat [Heartbeat] Failed: Command trpc.qq_new_tech.status_svc.StatusService.SsoHeartBeat timed out after 5000ms
2026-09-06 16:10:07 [E] onebot11-adapter 发生错误 Error: Command OidbSvcTrpcTcp.0x88d_0 timed out after 15000ms
2026-09-06 16:20:02 [I] onebot11-adapter WebSocket 事件上报 ws://<ASTRBOT_HOST>:<ONEBOT_WS_PORT>/ws message.group
2026-09-06 19:25:59 [E] heartbeat [Heartbeat] Failed: Command trpc.qq_new_tech.status_svc.StatusService.SsoHeartBeat timed out after 5000ms
2026-09-06 19:46:15 [I] qq-protocol Login successful! UID: <REDACTED>, nick: <REDACTED>

单凭这些超时日志无法区分网络故障和会话鉴权失败,也不能证明早期每一次超时都由相同原因引起。鉴权失败的直接证据来自下面的响应解析。

2. 故障期间的 OneBot 只读探测

get_status 耗时约 0.013 秒,以下只保留状态字段:

{
  "status": "ok",
  "retcode": 0,
  "data": { "online": true, "good": true }
}

随后对脱敏目标 <GROUP_ID> 调用 get_group_info,设置 no_cache=true,耗时约 15.307 秒,返回:

{
  "status": "failed",
  "retcode": 200,
  "data": null,
  "message": "Command OidbSvcTrpcTcp.0xfe5_2 timed out after 15000ms",
  "wording": "Command OidbSvcTrpcTcp.0xfe5_2 timed out after 15000ms"
}

这里的 OneBot retcode=200 是业务 API 的失败结果,与下文 QQ SSO 层的 retCode=-10001 属于不同层级。

3. TCP 传输与 SSO 响应解析

诊断期间被动观察 LLBot 网络命名空间中的现有连接,并配合只读 API 请求;没有为抓包重启 LLBot。下面是去除地址和 TCP 序号后的传输节选:

2026-09-06T19:39:00.567+08:00 LLBot -> QQ  payload_len=540  frame_len=540
2026-09-06T19:39:00.668+08:00 QQ -> LLBot  payload_len=136  frame_len=136

该片段证明 TCP 连接仍有双向数据交换,不能单独证明业务请求成功,也不能仅凭时序认定它对应哪一个业务命令。

下面是抓包后按协议解析得到的字段,不是 LLBot 原有日志。保留了长度、协议版本、加密类型、错误码及命令;正常心跳的非零序号替换为占位符:

{
  "at": "2026-09-06T19:41:44.710+08:00",
  "len": 136,
  "version": 12,
  "encryption": 2,
  "head_len": 89,
  "seq": 0,
  "retCode": -10001,
  "extraMsg": "身份验证失败,请你重新登录。(s20)",
  "cmd": ""
}
{
  "at": "2026-09-06T19:41:57.502+08:00",
  "len": 89,
  "version": 13,
  "encryption": 0,
  "head_len": 57,
  "seq": "<NONZERO_SEQ>",
  "retCode": 0,
  "extraMsg": "",
  "cmd": "Heartbeat.Alive"
}

这说明服务端已经拒绝当前会话,但连接级存活检测仍然成功。Heartbeat.Alive 与需要会话鉴权的 SsoHeartBeat 不能互相替代。

能够确认的是当前凭据被服务端判定为无效,以及 LLBot 漏处理了该响应。仅凭现有证据无法确定服务端为何使凭据失效,例如自然到期或主动撤销;本修复不依赖任何假定的固定有效期。

源码中的漏处理路径

  1. DirectProtocolClient.handlePacket() 原先先执行 pendingPackets.get(parsed.seq),仅在匹配到请求后检查错误码。
  2. 现场响应的 seq=0 无法匹配等待中的请求,因此被作为 push 发出;真正的请求仍等待到 5 秒或 15 秒超时。
  3. DirectQQProtocol 转交 push 时只保留命令和载荷,没有将 retCode / extraMsg 转为会话失效事件;空命令也没有对应的 dispatcher 处理分支。
  4. 会话心跳失败只记日志,连接本身又未断开,因此已保存的会话和在线状态未被清除。get_status 继续读取旧的 selfInfo.online,不能反映这次鉴权失败。

修复后的行为

  • 在请求序号匹配之前处理 retCode=-10001,同时覆盖可匹配和无法匹配请求的鉴权失败。
  • 清除当前会话,以明确的鉴权错误拒绝所有等待中的请求,并清理相关计时器。
  • 检查异步签名期间会话是否发生变化,阻止已失效会话对应的命令继续发送。
  • 将账号标记为离线,停止会话心跳和待执行的重连;仅删除被拒绝账号的保存会话,保留设备身份和原生客户端。
  • 重置旧二维码及轮询状态,进入已有的二维码登录流程;此前已在线时发送协议断开事件,并通知邮件服务及 Milky 消费者。
  • 二维码登录收尾携带本轮 qrPollToken,在查询 UIN、登录请求和上线注册等异步步骤后重新校验;底层登录响应在安装凭据前也校验该标记。手动登出同样取消旧流程。
  • 保存会话的时机移到上线注册成功且当前会话仍有效之后;旧流程不能启动心跳、重新标记在线、覆盖登录错误,或清除后续新登录的会话。保存会话恢复路径的注册步骤也具有相同保护。
  • 异步补查昵称时校验登录轮次与会话,防止旧结果覆盖新账号状态。

二维码登录仍需要使用者扫码确认。此次改动处理的是明确的鉴权失败;普通业务错误和普通传输断开继续沿用现有逻辑。

原有掉线处理与本次通知衔接

此前的 protocol/disconnect 监听器只记录断开日志,并不直接推送掉线通知。已核对的其他处理如下:

原有处理 触发方式 本次修复如何衔接
掉线邮件 nt/kicked-offline 时立即通知;协议持续异常达到 10 秒时,5 秒巡检触发断线回调。 已在线会话被明确拒绝时发出 qq/session-expired,复用邮件服务的 onOffline(reason),附带实际鉴权失败原因,遵守已有启用开关、通知去重及重新登录后复位逻辑。
Milky 离线推送 nt/kicked-offline 转换成 bot_offline,通过既有 HTTP/WebSocket 与 webhook 广播路径分发。 监听 qq/session-expired 并发送相同的 bot_offline 事件结构,data.reason 为鉴权失败原因。
Direct 协议恢复 被踢下线会停止心跳并断开连接;特定异地登录代码 1001 还会清除设备身份。 会话鉴权失败继续使用本 PR 的清理和二维码恢复流程,保留设备身份及原生客户端。通知使用单独事件,避免误触发 KickNT 的断连或设备清理。

只有此前已上报在线的账号才发送即时会话失效通知;首次登录注册失败不会被误报为已在线机器人掉线。会话清除后,仍收到的未鉴权 push 不再刷新 lastConnectedTime,避免反复错误包推迟原有断线超时回调。

自动评审问题的处理

已处理 Sourcery 指出的登录收尾竞态,对应提交 bedb57f8

复现过程是:让二维码登录的 registerOnline() 暂停,注入 retCode=-10001 使当前会话失效,再让旧注册请求完成。原 PR 提交 7b6eec55 会把 selfInfo.online 重新写为 true。另一个场景是旧注册请求稍后失败,原实现会清除已经替换的新会话。现已对成功和异常路径同时增加登录轮次与会话校验,并覆盖保存会话恢复路径中的同类竞态。

验证结果与边界

  • npm test17 个测试文件、175 项测试通过;本 PR 共包含 24 项相关回归测试,其中本次评审后补充 15 项。
  • 协议回归覆盖:seq=0 与匹配序号的鉴权失败、等待请求清理、普通错误/推送/断线、签名中失效、二维码登录各异步阶段失效、保存会话恢复的迟到成功/失败、保护替换会话、主动登出取消、正常扫码登录,以及未鉴权数据持续到达时的超时回调。
  • 通知回归覆盖:新会话失效事件与原 KickNT 事件均能携带原因触发邮件和 Milky;邮件禁用时不发送;成功发送后的重复通知被现有逻辑抑制,并在重新登录后按现有规则复位。
  • 将相关源码临时恢复为评审时的 7b6eec55,选取上述竞态、超时回调和通知场景,得到 7 项预期失败;恢复本次补丁后全部通过。
  • 使用同一条无法匹配请求的鉴权失败回归测试检查未修改的上游代码,能够复现失败;应用补丁后通过。
  • npm run build:通过。
  • 两个回归测试文件的 Prettier 检查及 git diff --check:通过。
  • npm run check:遇到已有的类型错误,已在基线提交 1cadea38 上复现相同结果:
src/ntqqapi/helper/messageBuilding.ts(74,9): error TS2322: Type 'number | undefined' is not assignable to type 'string | undefined'.
  Type 'number' is not assignable to type 'string'.

本地验证使用 Node.js 26.8.1。回归测试使用合成协议帧,并模拟网络、签名、会话存储、邮件发送和 Milky 广播端点,没有把真实账号、凭据或抓包载荷写入测试,也没有向真实收件人或 webhook 发送测试通知。补丁尚未部署到在线 QQ 实例进行端到端验证;上面的重新登录成功日志仅证明现场手动恢复,不能作为补丁线上验证结果。

Sourcery 摘要

通过使过期会话失效,并在未匹配的响应被误判为推送消息之前发起二维码重新登录,恢复 QQ 认证失败后的可靠恢复能力。

错误修复:

  • 在请求匹配之前处理 QQ 会话认证失败,包括无序响应,从而拒绝无效会话,避免其无限期超时。
  • 当 QQ 服务器报告认证失败时,自动将账号标记为离线,清除被拒绝的会话,并重新启动现有的二维码登录流程。

增强功能:

  • 防止在会话失效后异步签名完成时发送命令。

测试:

  • 增加回归测试,覆盖已匹配和未匹配的认证失败、待处理请求清理、签名期间会话失效、二维码恢复,以及普通错误、推送和传输关闭行为的保留。
Original summary in English

Sourcery 总结

通过使过期会话失效并提示通过二维码重新认证,恢复从 QQ 身份验证失败中可靠恢复的能力。

错误修复:

  • 在请求匹配之前处理 QQ 会话身份验证失败,包括未按序响应,从而拒绝无效会话,而不是让请求超时。
  • 自动将被拒绝的账号标记为离线,仅移除其无效的已保存会话,并重新启动二维码登录流程。
  • 防止在底层会话已失效或被替代后,命令和登录完成操作继续生效。

改进:

  • 当在线 QQ 会话过期时通知电子邮件和 Milky 集成,同时保留现有的传输断开行为。

测试:

  • 增加回归测试,覆盖匹配和未匹配的身份验证失败、待处理请求清理、过期的异步签名和登录流程、二维码恢复、通知,以及现有普通错误、推送和断开行为的保留。
Original summary in English

Summary by Sourcery

Restore reliable recovery from QQ authentication failures by invalidating stale sessions and prompting QR-code reauthentication.

Bug Fixes:

  • Handle QQ session authentication failures before request matching, including unsequenced responses, so invalid sessions are rejected instead of leaving requests to time out.
  • Automatically mark rejected accounts offline, remove only their invalid saved session, and restart the QR-code login flow.
  • Prevent commands and login completions from applying after the underlying session has been invalidated or superseded.

Enhancements:

  • Notify email and Milky integrations when an online QQ session expires while preserving existing transport-disconnect behavior.

Tests:

  • Add regression coverage for matched and unmatched authentication failures, pending-request cleanup, stale asynchronous signing and login flows, QR recovery, notifications, and preserved ordinary error, push, and disconnect behavior.

@sourcery-ai

sourcery-ai Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

审查者指南

该 PR 在序列匹配之前检测 QQ 的 retCode=-10001 身份验证失败,即使对于 seq=0/空命令响应,也会一致地使会话失效,拒绝正在进行的工作,并通过现有的二维码登录流程让账号重新登录。同时,它还防止异步签名使用过期会话,并添加了全面的回归测试,同时保持普通错误、推送和传输关闭行为不变。

QQ 会话身份验证失败恢复的时序图

sequenceDiagram
    participant QQ as QQ Server
    participant Client as DirectProtocolClient
    participant Protocol as DirectQQProtocol
    participant Login as QR Login Flow
    participant OneBot as OneBot Client

    QQ-->>Client: SSO response retCode=-10001, seq=0, cmd=""
    Client->>Client: clearSession()
    Client->>Client: reject pending requests and clear timers
    Client->>Protocol: session-expired(uin, error)
    Protocol->>Protocol: mark selfInfo.online=false
    Protocol->>Protocol: stop heartbeat and pending reconnect
    Protocol->>Protocol: deleteSession(uin)
    Protocol->>Protocol: resetQrState()
    Protocol->>Login: ensureQrLoop()
    Protocol-->>OneBot: protocol/disconnect
Loading

Direct QQ 身份验证失败的状态图

stateDiagram-v2
    [*] --> Online
    Online --> SessionExpired: retCode=-10001
    SessionExpired --> Offline: clearSession and reject pending requests
    Offline --> NeedQRCode: deleteSession and resetQrState
    NeedQRCode --> QRLogin: ensureQrLoop
    QRLogin --> Online: successful login
Loading

防止过期会话命令的流程图

flowchart LR
    A[sendCommand] --> B[Capture current session]
    B --> C[Async signing]
    C --> D{Session changed?}
    D -- Yes --> E[Throw stale-session error]
    D -- No --> F[buildServicePacket]
    F --> G[Send command]
Loading

文件级变更

变更 详情 文件
在正常响应匹配之前处理未按序的 QQ 会话身份验证失败。
  • 无论序列或命令为何,均检测 retCode=-10001。
  • 清除活动会话并拒绝所有待处理请求,同时移除计时器。
  • 触发 session-expired 事件,而不是将该失败作为推送转发。
src/main/qqProtocol/direct-lib/client.ts
将失效账号转换为明确的离线状态并重新启动二维码登录。
  • 停止心跳和待处理的重连任务,仅清除被拒绝账号的会话,并重置二维码状态。
  • 更新登录状态/错误状态;如果账号之前在线,则触发断开连接事件。
  • 启动现有的二维码登录循环,同时保留设备身份和客户端复用。
src/main/qqProtocol/direct.ts
增加身份验证失效和恢复行为的回归测试覆盖。
  • 覆盖匹配的失败和 seq=0 失败、空闲及重复失败、待处理请求拒绝以及计时器清理。
  • 验证会话失效后签名无法发送,并确认普通错误、推送和传输关闭仍保持原有行为。
  • 验证离线状态、凭据清理、二维码刷新、重连抑制以及替换会话恢复。
test/unit/qqProtocol/directSession.test.ts

提示和命令

与 Sourcery 交互

  • 触发新的审查: 在拉取请求中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 回复审查评论,请 Sourcery 根据该评论创建 issue。你也可以回复审查评论并使用 @sourcery-ai issue,从该评论创建 issue。
  • 生成拉取请求标题: 在拉取请求标题中的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在拉取请求中评论 @sourcery-ai title,随时生成或重新生成标题。
  • 生成拉取请求摘要: 在拉取请求正文中的任意位置写入 @sourcery-ai summary,即可在你指定的位置随时生成 PR 摘要。你也可以在拉取请求中评论 @sourcery-ai summary,随时生成或重新生成摘要。
  • 生成审查者指南: 在拉取请求中评论 @sourcery-ai guide,即可随时生成或重新生成审查者指南。
  • 解决所有 Sourcery 评论: 在拉取请求中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,这会非常有用。
  • 忽略所有 Sourcery 审查: 在拉取请求中评论 @sourcery-ai dismiss,即可忽略所有现有的 Sourcery 审查。如果你想从全新的审查开始,这尤其有用——别忘了评论 @sourcery-ai review 来触发新的审查!

自定义你的使用体验

访问你的控制面板以:

  • 启用或禁用审查功能,例如 Sourcery 生成的拉取请求摘要、审查者指南及其他功能。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

The PR detects QQ’s retCode=-10001 authentication failures before sequence matching, invalidates the session consistently even for seq=0/empty-command responses, rejects in-flight work, and drives the account back through the existing QR login flow. It also guards asynchronous signing against stale sessions and adds comprehensive regression tests while preserving ordinary error, push, and transport-close behavior.

Sequence diagram for QQ session authentication failure recovery

sequenceDiagram
    participant QQ as QQ Server
    participant Client as DirectProtocolClient
    participant Protocol as DirectQQProtocol
    participant Login as QR Login Flow
    participant OneBot as OneBot Client

    QQ-->>Client: SSO response retCode=-10001, seq=0, cmd=""
    Client->>Client: clearSession()
    Client->>Client: reject pending requests and clear timers
    Client->>Protocol: session-expired(uin, error)
    Protocol->>Protocol: mark selfInfo.online=false
    Protocol->>Protocol: stop heartbeat and pending reconnect
    Protocol->>Protocol: deleteSession(uin)
    Protocol->>Protocol: resetQrState()
    Protocol->>Login: ensureQrLoop()
    Protocol-->>OneBot: protocol/disconnect
Loading

State diagram for Direct QQ authentication failure

stateDiagram-v2
    [*] --> Online
    Online --> SessionExpired: retCode=-10001
    SessionExpired --> Offline: clearSession and reject pending requests
    Offline --> NeedQRCode: deleteSession and resetQrState
    NeedQRCode --> QRLogin: ensureQrLoop
    QRLogin --> Online: successful login
Loading

Flow diagram for stale-session command prevention

flowchart LR
    A[sendCommand] --> B[Capture current session]
    B --> C[Async signing]
    C --> D{Session changed?}
    D -- Yes --> E[Throw stale-session error]
    D -- No --> F[buildServicePacket]
    F --> G[Send command]
Loading

File-Level Changes

Change Details Files
Handle unsequenced QQ session-authentication failures before normal response matching.
  • Detect retCode=-10001 regardless of sequence or command.
  • Clear the active session and reject all pending requests while removing timers.
  • Emit a session-expired event instead of forwarding the failure as a push.
src/main/qqProtocol/direct-lib/client.ts
Transition invalidated accounts to an explicit offline state and restart QR login.
  • Stop heartbeat and pending reconnect work, clear only the rejected account session, and reset QR state.
  • Update login status/error state and emit disconnect when the account was online.
  • Start the existing QR-code login loop while preserving device identity and client reuse.
src/main/qqProtocol/direct.ts
Add regression coverage for authentication invalidation and recovery behavior.
  • Cover matched and seq=0 failures, idle and duplicate failures, pending-request rejection, and timer cleanup.
  • Verify signing cannot send after session invalidation and that ordinary errors, pushes, and transport closes retain existing behavior.
  • Verify offline status, credential cleanup, QR refresh, reconnect suppression, and replacement-session recovery.
test/unit/qqProtocol/directSession.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@HLfromZ
HLfromZ marked this pull request as ready for review September 6, 2026 12:24

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你好——我发现了 1 个问题

面向 AI Agent 的提示
请处理本次代码审查中的评论:

## 单条评论

### 评论 1
<location path="src/main/qqProtocol/direct.ts" line_range="356-366" />
<code_context>
+        clearTimeout(this.reconnectTimer)
+        this.reconnectTimer = null
+      }
+      // Remove only the rejected account's credentials; retain the device identity and native client.
+      deleteSession(uin)
+      this.runtimeUinOverride = null
+      this.qrPollToken++
+      this.directQrResult = null
+      this.directPollResult = null
+      this.resetQrState()
+      setLoginState({ state: 'need_qrcode', qrcode_png_base64: undefined })
+      if (wasOnline) this.ctx.parallel('protocol/disconnect')
+      this.ensureQrLoop()
+    })
     client.on('error', (err: Error) => {
       this.logger.warn('Direct client error:', err.message)
</code_context>
<issue_to_address>
**问题 (bug_risk):**进行中的 `completeDirectLogin()``qrPollToken++` 之后没有被取消或重新验证。如果会话在二维码登录或 `registerOnline()` 等待期间过期,旧的登录流程仍会继续、启动心跳、设置 `selfInfo.online = true`,并在过期处理程序已将账号标记为离线且重置二维码状态之后调用 `maybeEmitOnline()`**触发条件:**二维码登录完成过程与会话身份验证失败重叠时。

**建议修复:**将二维码/会话代数传入 `completeDirectLogin()`,并在每个等待的登录步骤之后,在保存会话、启动心跳或将账号标记为在线之前,连同 `directClient.isLoggedIn` 一起对其进行检查。
</issue_to_address>

Sourcery 对开源项目免费——如果您喜欢我们的审查结果,欢迎分享 ✨
Original comment in English

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/main/qqProtocol/direct.ts" line_range="356-366" />
<code_context>
+        clearTimeout(this.reconnectTimer)
+        this.reconnectTimer = null
+      }
+      // Remove only the rejected account's credentials; retain the device identity and native client.
+      deleteSession(uin)
+      this.runtimeUinOverride = null
+      this.qrPollToken++
+      this.directQrResult = null
+      this.directPollResult = null
+      this.resetQrState()
+      setLoginState({ state: 'need_qrcode', qrcode_png_base64: undefined })
+      if (wasOnline) this.ctx.parallel('protocol/disconnect')
+      this.ensureQrLoop()
+    })
     client.on('error', (err: Error) => {
       this.logger.warn('Direct client error:', err.message)
</code_context>
<issue_to_address>
**issue (bug_risk):** An in-flight `completeDirectLogin()` is not cancelled or revalidated after `qrPollToken++`. If session expiration occurs while QR login or `registerOnline()` is awaiting, the old login flow continues, starts a heartbeat, sets `selfInfo.online = true`, and calls `maybeEmitOnline()` after the expiration handler has already marked the account offline and reset the QR state.

**Triggers:** When a QR login completion overlaps a session-authentication failure.

**Suggested fix:** Pass the QR/session generation into `completeDirectLogin()` and check it, along with `directClient.isLoggedIn`, after every awaited login step before saving the session, starting the heartbeat, or marking the account online.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/main/qqProtocol/direct.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant