kt-template-online-api/docs/superpowers/specs/2026-06-23-qqbot-napcat-source-fork-qr-refresh-design.md

14 KiB
Raw Blame History

QQBot NapCat 源码 Fork 二维码刷新修复设计

背景

线上账号 1914728559 的更新登录链路已经证明 API 侧旧二维码回传被挡住:刷新会话不再把旧 QR 传给 Admin/SSE接口会保持 pending 并清空二维码。但同一个容器内的 NapCat WebUI 仍存在源头问题:

  • 正确通过 /api/auth/login 换取 Credential 后,CheckLoginStatusGetQQLoginQrcode 仍返回旧二维码 hash 47fb616d3f27c93a
  • /app/napcat/cache/qrcode.png 的 mtime、size、sha256 不变化。
  • Docker 日志只有“当前账号(1914728559)已登录,无法重复登录”,没有二维码生成日志。
  • NapCat WebUI 内部 QQLoginStatus=true 与实际 selfInfo.online=false 发生分裂,导致 RefreshQRcode / GetQQLoginQrcode 被旧登录态短路。

上一轮 kt-napcat-desktop-cn:desktop-cn-v2 是在派生镜像里解压 NapCat.Shell.zip 并用脚本 patch bundled JS。这条路验证了方向但它仍是脆弱的产物补丁上游构建压缩、文件结构或字符串变化都会让补丁静默失效。本轮选择 fork NapCat 源码,在源代码层修复登录态与二维码刷新语义,再把源码构建产物注入 KT 受控中文桌面派生镜像。

目标

  1. 建立 KT 可维护的 NapCatQQ 源码 fork不再依赖对 napcat.mjs 的字符串补丁。
  2. 在 NapCat WebUI 源码层修复 QQLoginStatus=true 但真实账号离线时的登录态误判。
  3. RefreshQRcode 的结果可观测:区分“刷新请求已被 QQ 内核接受”和“新的 QR URL 已经到达 WebUI 缓存”。
  4. GetQQLoginQrcode / CheckLoginStatus 不再对 stale QR 做成功式返回。
  5. 保留 API 侧 fresh QR 护栏,避免未来 NapCat 或 QQ 内核异常时 Admin/SSE 再展示旧码。
  6. 生成 KT 受控 NapCat.Shell.zip artifact并接入 kt-napcat-desktop-cn:desktop-cn-v3 镜像。
  7. 完成本地构建验证、镜像验证、线上单账号 canary 验证,再决定是否替换所有账号容器。

非目标

  • 不绕过 QQ/Tencent 验证码、新设备验证或安全验证。
  • 不修改 QQ/NTQQ 私有协议字段,不伪造协议签名。
  • 不重写 NapCat Docker 上游镜像链路;首版继续复用 KT 的中文桌面派生镜像。
  • 不删除 API 侧已完成的旧二维码防护。
  • 不在登录 SSE 流程里恢复 Docker 重建、重启或运行态补 env。
  • 不把 OneBot 反向 WebSocket 在线当作 QQ 账号登录成功证据。

源码仓库与基线

新增一个长期维护的独立源码 fork

D:\MyFiles\KT\GitHub\NapCatQQ

基线使用上游 NapNeko/NapCatQQorigin/main,当前已审计的上游基线 commit 为:

5c18a62530d87dbadf53d267002894faa6ca7e90

实现阶段必须重新拉取并确认实际基线。如果上游已经前进,先把本设计的源改动 rebased 到新的 origin/main,并记录实际基线 commit。D:\MyFiles\KT\.kt-workspace\upstream\NapCatQQ 只允许作为临时只读审计目录,不作为最终 fork 或构建真相源。

fork 分支建议:

codex/qr-refresh-login-state

KT API 仓库只保存构建脚本、镜像集成和验证逻辑,不提交 NapCat.Shell.zip 二进制 artifact。

源码改动设计

登录态统一 helper

packages/napcat-webui-backend/src/helper/Data.ts 增加 WebUI 登录运行态 helper。它读取三类状态

  • WebUI 缓存态:QQLoginStatus
  • 实际核心态:OneBotContext?.core?.selfInfo?.online
  • 当前二维码缓存:QQQRCodeURL

helper 的语义:

  • QQLoginStatus=trueonline=true:真实在线,登录类接口继续返回已登录。
  • QQLoginStatus=trueonline=falsestale 登录态,必须把 QQLoginStatus reconcile 为 false,并允许重新走 quick/password/manual QR。
  • onlineundefined:启动早期或上下文未初始化,不能直接判定离线;保留原行为,避免登录成功到 adapter 初始化之间的短窗口误伤。
  • stale 登录态被 reconcile 时,允许清空旧 QR 或标记旧 QR 为不可用,避免 GetQQLoginQrcode 对外返回历史二维码。

helper 返回结构建议包含:

{
  webuiLoginStatus: boolean;
  online?: boolean;
  isActuallyLogin: boolean;
  isStaleLoginStatus: boolean;
  canStartLoginFlow: boolean;
  qrcodeurl: string;
  qrcodeRevision: number;
  qrcodeUpdatedAt: number;
}

字段命名可按上游风格调整,但语义必须完整保留。

QQLogin handlers 收敛

packages/napcat-webui-backend/src/api/QQLogin.ts 中所有直接用 WebUiDataRuntime.getQQLoginStatus() 短路登录流程的 handler都改为使用统一 helper

  • QQGetQRcodeHandler
  • QQSetQuickLoginHandler
  • QQRefreshQRcodeHandler
  • QQPasswordLoginHandler
  • QQCaptchaLoginHandler
  • QQNewDeviceLoginHandler

QQCheckLoginStatusHandler 当前已经读取 selfInfo.online,但仍可能附带旧 qrcodeurl。它也必须切到同一个 helper保证返回的 isLoginqrcodeurl、错误信息和 revision 一致。

关键约束:

  • 只有 isActuallyLogin=true 时才返回 QQ Is Logined
  • stale 登录态不能阻止 RefreshQRcode
  • stale 登录态不能把旧 QR 作为新二维码返回。
  • quick 登录报“当前账号已登录”后仍要由真实在线态决定是否完成登录,而不是由 WebUI 缓存态决定。

二维码 revision 与刷新可观测性

Data.ts 中给二维码缓存增加 revision/更新时间:

  • setQQLoginQrcodeURL(url) 只有在 URL 变化时递增 qrcodeRevision
  • 每次 QR URL 变化记录 qrcodeUpdatedAt
  • 清空 stale QR 时也更新 revision让调用方能知道旧码已失效。

refreshQRCode() 不再只返回 void。它应返回刷新结果,例如:

{
  accepted: boolean;
  updated: boolean;
  qrcodeurl: string;
  qrcodeRevision: number;
  error?: string;
}

实现语义:

  1. 记录刷新前的 QR URL/revision。
  2. 调用 onRefreshQRCode
  3. 如果底层返回 false,则 accepted=false
  4. 如果底层接受请求,则等待一次 onQRCodeGetPicture 写入新 QR或在短等待窗口后返回 accepted=true, updated=false
  5. 只有 QR URL/revision 变化,才返回 updated=true

等待窗口必须短且有上限,不能把 WebUI HTTP 请求变成长期阻塞。首版建议只在 WebUI 内做短等待API 侧仍保持 SSE/polling pending。

Shell 与 Framework callback

二维码真正进入 WebUI 缓存的位置在:

  • packages/napcat-shell/base.ts
  • packages/napcat-framework/napcat.ts

这两个入口里:

  • onQRCodeGetPicture 继续调用 setQQLoginQrcodeURL,并触发 revision 更新。
  • setRefreshQRCodeCallback 不再吞掉底层返回值,应把 loginService.getQRCodePicture()boolean 返回给 refreshQRCode()

这样 WebUI 能知道 QQ 内核是否接受了取 QR 请求API 也能通过 WebUI 响应判断“待产码”和“已有新码”。

KT 镜像集成

继续使用 Node/kt-template-online-api/ci/napcat-desktop-cn 作为 KT 中文桌面派生镜像入口,但移除 bundled JS patch。

构建输入

实现阶段新增构建脚本,把 fork 仓库构建出的 NapCat.Shell.zip staged 到 .kt-workspace 下的 Docker build context。API 仓库不直接提交二进制 zip。

推荐流程:

NapCatQQ fork
  -> pnpm build shell artifact
  -> .kt-workspace/napcat-desktop-cn-build/NapCat.Shell.zip
  -> ci/napcat-desktop-cn/Dockerfile COPY into /app/NapCat.Shell.zip
  -> kt-napcat-desktop-cn:desktop-cn-v3

Docker COPY 不能引用 build context 之外的文件,所以实现脚本必须显式 staging context而不是在 Dockerfile 里直接读 D:\MyFiles\KT\GitHub\NapCatQQ。 staging context 必须同时包含 Dockerfile、verify.sh、fork artifact marker 和 NapCat.Shell.zip,避免 Dockerfile 在仓库目录、COPY 源却在另一个 context 的路径错位。

Dockerfile 变化

ci/napcat-desktop-cn/Dockerfile 保留:

  • zh_CN.UTF-8
  • Asia/Shanghai
  • 中文字体与 fontconfig
  • XDG/Home 环境
  • verify.sh

删除:

  • ci/napcat-desktop-cn/patches/qq-login-real-online-guard.sh
  • 解压 /app/NapCat.Shell.zip 后用 Perl/字符串改 napcat.mjs 的步骤

新增:

  • 从 staged context 复制源码构建的 NapCat.Shell.zip/app/NapCat.Shell.zip
  • 写入 fork artifact marker例如 /ci/napcat-desktop-cn/fork-artifact.json,包含 upstream base commit、fork commit、build time、artifact sha256

verify.sh 变化

ci/napcat-desktop-cn/verify.sh 不再 grep JS patch 字符串,而是验证:

  • /app/NapCat.Shell.zip 存在且能解压。
  • artifact sha256 与 marker 一致。
  • marker 中存在 upstream base commit 和 fork commit。
  • locale/fontconfig/timezone/XDG 仍通过。
  • 能在解压后的 WebUI backend bundle 中找到 QR revision 或 runtime helper 的可观测 marker。

API 侧边界

API 不再承担 NapCat 内部登录态修复职责,但继续保留防旧码护栏:

  • scan/refresh 继续要求 fresh QR。
  • scan/status 继续拒绝把旧 QR 当成新码返回。
  • scan/qrcode/refresh 继续在未拿到新 QR 时保持 pending
  • QQ 登录态仍以 NapCat WebUI/login logs 为准,不以 OneBot heartbeat 兜底。

需要更新的 API 文件范围:

  • ci/napcat-desktop-cn/Dockerfile
  • ci/napcat-desktop-cn/verify.sh
  • ci/napcat-desktop-cn/README.md
  • ci/napcat-desktop-cn/patches/qq-login-real-online-guard.sh 删除
  • src/modules/qqbot/napcat/application/runtime/napcat-runtime-profile.service.ts
  • test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts
  • test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts
  • 可能涉及 test/qqbot/napcat/qqbot-napcat-container.service.spec.ts
  • docs/qqbot-nas-runtime.md
  • README.md / API.md 中涉及镜像 tag 或 NapCat runtime 的说明

默认 profile 版本升级到:

desktop-cn-v3

生产仍通过 QQBOT_NAPCAT_IMAGE 控制实际镜像,不在 API 代码里硬编码生产 digest。

数据流

sequenceDiagram
  participant Admin
  participant API
  participant NapCatWebUI
  participant QQKernel

  Admin->>API: 更新登录 / scan/refresh
  API->>NapCatWebUI: CheckLoginStatus / GetQQLoginInfo
  NapCatWebUI->>NapCatWebUI: helper reconcile QQLoginStatus + selfInfo.online
  API->>NapCatWebUI: RefreshQRcode
  NapCatWebUI->>QQKernel: getQRCodePicture()
  QQKernel-->>NapCatWebUI: accepted / rejected
  QQKernel-->>NapCatWebUI: onQRCodeGetPicture(new QR)
  NapCatWebUI->>NapCatWebUI: setQQLoginQrcodeURL + revision++
  NapCatWebUI-->>API: accepted, updated, qrcodeRevision, qrcodeurl
  API-->>Admin: pending + new QR 或 pending + 正在生成

若 QQ 内核接受刷新但未产码API/SSE 显示“正在生成二维码/等待 NapCat 返回二维码”,不展示旧码。

错误处理

  • online=undefined 不做 stale reconcile避免启动过程误判。
  • online=falseQQLoginStatus=true 时 reconcile 失败必须写 WebUI 错误信息,并允许后续刷新重试。
  • refreshQRCode.accepted=false 时返回明确错误,不继续使用旧 QR。
  • accepted=true, updated=false 时保持 pendingAPI 继续轮询或允许手动刷新。
  • QR revision 变化但 URL 为空,表示旧码已清空,不能被 API 当成可展示二维码。
  • 源码 fork 构建失败时不得回退到 bundled JS patch 镜像;构建阶段直接失败。
  • 线上 canary 失败时回滚 QQBOT_NAPCAT_IMAGE 到上一版镜像,并保留 API 旧码防护。

验证计划

NapCatQQ fork

需要在 fork 仓库运行:

corepack pnpm install --frozen-lockfile
corepack pnpm run typecheck
corepack pnpm test
corepack pnpm run build:shell

若上游 monorepo 的测试脚本或 package 名称变化,以 package.json 为准,但必须覆盖:

  • WebUI backend 登录态 helper。
  • QQLogin.ts handler 对 stale login status 的行为。
  • refreshQRCode() accepted/updated/revision。
  • shell/framework callback 返回值。

KT API 镜像集成

需要在 API 仓库运行:

corepack pnpm exec jest test/modules/qqbot/napcat/napcat-desktop-cn-image.spec.ts test/modules/qqbot/napcat/runtime-protocol-profile.spec.ts --runTestsByPath --runInBand
corepack pnpm run typecheck
git diff --check

如果改到容器创建逻辑,再追加:

corepack pnpm exec jest test/qqbot/napcat/qqbot-napcat-container.service.spec.ts --runTestsByPath --runInBand

镜像验证

在 NAS 或本地 Docker 构建:

docker build \
  --build-arg NAPCAT_BASE_IMAGE="$NAPCAT_BASE_IMAGE" \
  -t kt-napcat-desktop-cn:desktop-cn-v3 \
  -f .kt-workspace/napcat-desktop-cn-build/ci/napcat-desktop-cn/Dockerfile \
  .kt-workspace/napcat-desktop-cn-build
docker run -d --name kt-napcat-v3-verify kt-napcat-desktop-cn:desktop-cn-v3
docker exec kt-napcat-v3-verify sh /ci/napcat-desktop-cn/verify.sh
docker rm -f kt-napcat-v3-verify

记录镜像 digest、fork commit、artifact sha256。

线上 canary

  1. 只对一个测试账号或用户指定账号切换 QQBOT_NAPCAT_IMAGE=kt-napcat-desktop-cn:desktop-cn-v3
  2. 不在 SSE 流程中重建在线容器;只有用户明确执行容器迁移/重建时才创建 v3 容器。
  3. 触发更新登录,观察:
    • quick 已登录错误后 WebUI 会 reconcile stale 登录态。
    • RefreshQRcode 触发 NapCat 产码日志。
    • /app/napcat/cache/qrcode.png mtime/hash 变化。
    • API scan/status 返回新 QR不返回旧 QR。
    • SSE/Admin 从“扫码”推进到扫码后状态或明确 pending 原因。
  4. 若扫码后触发新设备或验证码,按既有链路继续,不把它当作 fork 失败。

上线顺序

  1. 在 API 仓库提交本设计文档。
  2. 进入 superpowers:writing-plans,把源码 fork、KT 镜像集成、测试和线上 canary 拆成实施任务。
  3. 创建或更新 D:\MyFiles\KT\GitHub\NapCatQQ fork 分支。
  4. 先实现并验证 NapCatQQ 源码修复。
  5. 再修改 API 仓库的派生镜像构建脚本和测试。
  6. 构建 desktop-cn-v3,在 NAS 上验证镜像。
  7. 推送 API 仓库并观察 Jenkins/K8s。
  8. 线上单账号 canary通过后再迁移剩余账号。

完成标准

  • NapCatQQ fork 有明确 base commit、fork commit 和可重复构建命令。
  • desktop-cn-v3 镜像不再包含 bundled JS 字符串 patch。
  • stale QQLoginStatus=true + online=false 不再阻止刷新二维码。
  • RefreshQRcode 能表达 accepted/updated/revision。
  • API/Admin/SSE 不展示旧 QR线上 canary 能看到新 QR 或明确的未产码 pending 原因。
  • 相关测试、镜像 verify、线上 canary 证据完整。