fix: 修复NapCat扫码登录QQ号回读
This commit is contained in:
parent
bb2d799efb
commit
28506e64a0
4
API.md
4
API.md
@ -361,7 +361,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
|||||||
|
|
||||||
账号保存支持可选 `encryptedLoginPassword`,用于 NapCat 密码登录。前端必须先通过 `/auth/password-public-key` 获取公钥并使用 RSA-OAEP 加密,不传明文 `loginPassword`;后端必须使用显式配置的 `QQBOT_ACCOUNT_SECRET_KEY`(或非默认 `ADMIN_TOKEN_SECRET`)二次加密落库,空值和公开默认值会被拒绝,不在列表/详情中返回。账号列表里的 `connectStatus` 只表示 OneBot 反向 WS;`napcat.oneBotOnline`、`napcat.containerOnline`、`napcat.webuiOnline`、`napcat.qqLoginStatus`、`napcat.qqLoginMessage` 分别表示 OneBot、容器、WebUI 和 QQ 登录态,`webuiOnline=null` 表示本次使用缓存且未重新探测 WebUI;`qqLoginMessage` 只承载真实 QQ 登录态消息,WebUI 配置缺失或请求异常只放在 `lastError`。
|
账号保存支持可选 `encryptedLoginPassword`,用于 NapCat 密码登录。前端必须先通过 `/auth/password-public-key` 获取公钥并使用 RSA-OAEP 加密,不传明文 `loginPassword`;后端必须使用显式配置的 `QQBOT_ACCOUNT_SECRET_KEY`(或非默认 `ADMIN_TOKEN_SECRET`)二次加密落库,空值和公开默认值会被拒绝,不在列表/详情中返回。账号列表里的 `connectStatus` 只表示 OneBot 反向 WS;`napcat.oneBotOnline`、`napcat.containerOnline`、`napcat.webuiOnline`、`napcat.qqLoginStatus`、`napcat.qqLoginMessage` 分别表示 OneBot、容器、WebUI 和 QQ 登录态,`webuiOnline=null` 表示本次使用缓存且未重新探测 WebUI;`qqLoginMessage` 只承载真实 QQ 登录态消息,WebUI 配置缺失或请求异常只放在 `lastError`。
|
||||||
|
|
||||||
扫码链路返回 `sessionId`,前端应使用 SSE 查看步骤进度,而不是等待长 HTTP 请求完成;新增账号扫码会先预留容器和临时设备身份后立即返回 pending,会话后台再启动远端 Docker 和生成二维码。已有账号的更新登录不会通过 Docker 重建、重启或补 env 来刷新 QQ 登录态;如果目标容器仍在线,即使 QQ 账号已离线,也会保持同一容器并通过 NapCat WebUI 依次调用 `SetQuickLogin`、`PasswordLogin`、`RefreshQRcode` / `GetQQLoginQrcode` 推进原有弹窗流程。只有 Docker 容器离线或缺失时,容器准备阶段才会创建/重建容器,并在创建时一次性注入 `ACCOUNT` 和必要登录 env;已在线的源容器不补 env。快速登录失败后,如果账号保存了登录密码,后端使用解密后的密码计算 MD5 调用 `/api/QQLogin/PasswordLogin`,不会把密码写回运行态 env,也没有成功后的 env 清理步骤;密码登录结果按 `QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS` / `QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS` 轮询。准备阶段的扫码会话会持续续期,避免后台登录未完成时前端先判过期。同一账号已有 pending 更新登录会话时,重复调用 `/qqbot/account/scan/refresh` 会返回原 `sessionId`,不会再次启动 quick/password/二维码准备。若 API Pod 在准备阶段重启,持久化的 `preparingRelogin` 超过 `QQBOT_NAPCAT_RELOGIN_PREPARING_STALE_MS`(留空使用密码等待窗口加缓冲)后,`/qqbot/account/scan/status` 会自动恢复普通登录态检测,不再永久停留在“正在尝试密码登录”;`/qqbot/account/scan/events` 在进程内事件缓存丢失时会先推送当前会话快照。pending refresh 会话如果没有二维码、验证码或新设备挑战,`/qqbot/account/scan/status` 会按 `NAPCAT_LOGIN_QR_AUTO_REFRESH_COOLDOWN_MS` 冷却在同一容器自动重试 `RefreshQRcode/GetQQLoginQrcode`,避免 SSE 长时间卡在“二维码生成中”。密码登录触发 QQ 安全验证时,接口返回的 `captchaUrl` 只用于前端拉起腾讯验证码;前端必须把腾讯验证码返回的 `ticket`、`randstr`、`sid` 连同 `sessionId` 提交到 `/qqbot/account/scan/captcha/submit`,后端再代理到同一 NapCat 容器的 `/api/QQLogin/CaptchaLogin` 继续密码登录第二步。验证码和新设备验证这类真人交互态使用 `NAPCAT_LOGIN_HUMAN_VERIFY_EXPIRE_MS`(默认 15 分钟,且至少不短于普通二维码 TTL)续期;普通登录二维码仍使用 `NAPCAT_LOGIN_QR_EXPIRE_MS`。`/qqbot/account/scan/status` 遇到 NapCat 只返回“需要验证码/继续完成验证/安全验证”但不带 URL 时,会先从当前容器日志提取 `proofWaterUrl`,提取不到则保持验证码处理中而不切到二维码兜底;会话已有 `captchaUrl` 后,同类状态仍保持 `pending` 和原 `captchaUrl`。密码登录仍失败、验证码未完成、离线、账号不匹配或缺少 QQ 号时,直接通过 WebUI 二维码接口进入扫码兜底,不 reset 登录态。看门狗只做离线巡检、账号错误写入和 `super` 站内信告警,不会触发 quick/password 登录或扫码登录。
|
扫码链路返回 `sessionId`,前端应使用 SSE 查看步骤进度,而不是等待长 HTTP 请求完成;新增账号扫码会先预留容器和临时设备身份后立即返回 pending,会话后台再启动远端 Docker 和生成二维码。`CheckLoginStatus.isLogin=true` 只表示 NapCat 登录阳性,新增账号必须继续等 `GetQQLoginInfo` 返回 `uin/selfId` 后才允许创建和绑定真实 QQ 号;短暂缺号时 `/qqbot/account/scan/status` 保持同一会话 pending 并显示正在读取 QQ 号,等待 `NAPCAT_LOGIN_SELF_ID_WAIT_MS`,不得重建容器、补 env 或从容器元数据猜号。已有账号的更新登录不会通过 Docker 重建、重启或补 env 来刷新 QQ 登录态;如果目标容器仍在线,即使 QQ 账号已离线,也会保持同一容器并通过 NapCat WebUI 依次调用 `SetQuickLogin`、`PasswordLogin`、`RefreshQRcode` / `GetQQLoginQrcode` 推进原有弹窗流程。只有 Docker 容器离线或缺失时,容器准备阶段才会创建/重建容器,并在创建时一次性注入 `ACCOUNT` 和必要登录 env;已在线的源容器不补 env。快速登录失败后,如果账号保存了登录密码,后端使用解密后的密码计算 MD5 调用 `/api/QQLogin/PasswordLogin`,不会把密码写回运行态 env,也没有成功后的 env 清理步骤;密码登录结果按 `QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS` / `QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS` 轮询。准备阶段的扫码会话会持续续期,避免后台登录未完成时前端先判过期。同一账号已有 pending 更新登录会话时,重复调用 `/qqbot/account/scan/refresh` 会返回原 `sessionId`,不会再次启动 quick/password/二维码准备。若 API Pod 在准备阶段重启,持久化的 `preparingRelogin` 超过 `QQBOT_NAPCAT_RELOGIN_PREPARING_STALE_MS`(留空使用密码等待窗口加缓冲)后,`/qqbot/account/scan/status` 会自动恢复普通登录态检测,不再永久停留在“正在尝试密码登录”;`/qqbot/account/scan/events` 在进程内事件缓存丢失时会先推送当前会话快照。pending refresh 会话如果没有二维码、验证码或新设备挑战,`/qqbot/account/scan/status` 会按 `NAPCAT_LOGIN_QR_AUTO_REFRESH_COOLDOWN_MS` 冷却在同一容器自动重试 `RefreshQRcode/GetQQLoginQrcode`,避免 SSE 长时间卡在“二维码生成中”。密码登录触发 QQ 安全验证时,接口返回的 `captchaUrl` 只用于前端拉起腾讯验证码;前端必须把腾讯验证码返回的 `ticket`、`randstr`、`sid` 连同 `sessionId` 提交到 `/qqbot/account/scan/captcha/submit`,后端再代理到同一 NapCat 容器的 `/api/QQLogin/CaptchaLogin` 继续密码登录第二步。验证码和新设备验证这类真人交互态使用 `NAPCAT_LOGIN_HUMAN_VERIFY_EXPIRE_MS`(默认 15 分钟,且至少不短于普通二维码 TTL)续期;普通登录二维码仍使用 `NAPCAT_LOGIN_QR_EXPIRE_MS`。`/qqbot/account/scan/status` 遇到 NapCat 只返回“需要验证码/继续完成验证/安全验证”但不带 URL 时,会先从当前容器日志提取 `proofWaterUrl`,提取不到则保持验证码处理中而不切到二维码兜底;会话已有 `captchaUrl` 后,同类状态仍保持 `pending` 和原 `captchaUrl`。密码登录仍失败、验证码未完成、离线、账号不匹配或缺少 QQ 号时,直接通过 WebUI 二维码接口进入扫码兜底,不 reset 登录态。看门狗只做离线巡检、账号错误写入和 `super` 站内信告警,不会触发 quick/password 登录或扫码登录。
|
||||||
|
|
||||||
密码验证码通过后如果 NapCat 返回 `needNewDevice`,后端不会只把 `jumpUrl` 透给 Admin,而是在同一会话中继续调用 `/api/QQLogin/GetNewDeviceQRCode` 生成新设备验证二维码;`/qqbot/account/scan/status` 后续轮询会代理 `/api/QQLogin/PollNewDeviceQR`,状态映射为 `newDeviceStatus=qr-pending|scanned|confirming|verified|expired|failed`,进入确认态后再调用 `/api/QQLogin/NewDeviceLogin` 并回到密码登录完成检查。扫码会话结果新增 `newDeviceQrcode`、`newDeviceStatus`、`deviceVerifyUrl` 字段;`captchaUrl` 和 `newDeviceQrcode` 分别表示腾讯安全验证码和 QQ 新设备验证二维码,前端必须分开展示。SSE 进度文案包含快速登录、密码登录、验证码、新设备二维码、已扫码、确认中、二维码兜底、登录成功/失败。
|
密码验证码通过后如果 NapCat 返回 `needNewDevice`,后端不会只把 `jumpUrl` 透给 Admin,而是在同一会话中继续调用 `/api/QQLogin/GetNewDeviceQRCode` 生成新设备验证二维码;`/qqbot/account/scan/status` 后续轮询会代理 `/api/QQLogin/PollNewDeviceQR`,状态映射为 `newDeviceStatus=qr-pending|scanned|confirming|verified|expired|failed`,进入确认态后再调用 `/api/QQLogin/NewDeviceLogin` 并回到密码登录完成检查。扫码会话结果新增 `newDeviceQrcode`、`newDeviceStatus`、`deviceVerifyUrl` 字段;`captchaUrl` 和 `newDeviceQrcode` 分别表示腾讯安全验证码和 QQ 新设备验证二维码,前端必须分开展示。SSE 进度文案包含快速登录、密码登录、验证码、新设备二维码、已扫码、确认中、二维码兜底、登录成功/失败。
|
||||||
|
|
||||||
@ -377,7 +377,7 @@ QQBot 运行态包括 NapCat 容器登录、OneBot v11 反向 WebSocket、MQTT
|
|||||||
|
|
||||||
该接口只返回脱敏后的运行态证据,供 Admin 排查镜像、locale、shm、配置 hash、漂移状态、风险模式和 watchdog 巡检告警状态;不会返回 WebUI token、reverse WS token、QQ 登录密码、SSH 私钥或运行态密码环境。账号列表只挂载 `napcat.profileStatus`、`napcat.runtimeProfile` 等摘要字段,不触发登录、重建或修复动作。watchdog 不执行登录恢复:遇到 QQ 登录态离线只记录离线原因并通知 `super`,登录恢复统一由 Admin 手动「更新登录」触发;session behavior profile 只做冷启动、housekeeping、presence 和自动能力分阶段降载,不实现账号级每小时/每日累计发送预算。
|
该接口只返回脱敏后的运行态证据,供 Admin 排查镜像、locale、shm、配置 hash、漂移状态、风险模式和 watchdog 巡检告警状态;不会返回 WebUI token、reverse WS token、QQ 登录密码、SSH 私钥或运行态密码环境。账号列表只挂载 `napcat.profileStatus`、`napcat.runtimeProfile` 等摘要字段,不触发登录、重建或修复动作。watchdog 不执行登录恢复:遇到 QQ 登录态离线只记录离线原因并通知 `super`,登录恢复统一由 Admin 手动「更新登录」触发;session behavior profile 只做冷启动、housekeeping、presence 和自动能力分阶段降载,不实现账号级每小时/每日累计发送预算。
|
||||||
|
|
||||||
NapCat Chinese Desktop Runtime 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact,并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset,明确二维码过期或扫码确认窗口失效的 QR session failure 会自动换码,其他非自动重试 QR failure 会标记下次 WebUI 登录动作先重置 native login service。v14 运行时会为 QQ/NapCat/Xvfb 长期进程做 PID 级 `/proc/<pid>/mountinfo` 遮蔽,避免 QQCore 通过 `/proc/self/mountinfo` 看到 `overlay`、`/vol1/docker`、`docker-init`、`/docker/containers`、`napcat-instances`、`btrfs`、`/dev/mapper/trim` 等宿主路径。构建前必须运行 `scripts/napcat-desktop-cn-stage-build.mjs` 生成 Docker build context;生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:*` digest。生产 K8s manifest 保留 `kt-napcat-desktop-cn:desktop-cn-v14` / `desktop-cn-v14` 安全默认值;Jenkins `QQBOT_NAPCAT_IMAGE_OVERRIDE` 与 `QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE` 只有非空时才覆盖 API Deployment env,空值会保持 manifest/default env。运行时回滚应重新运行 Jenkins 并填入上一版镜像 digest/profile,或清空两个 override 后重新部署回 manifest 默认值。
|
NapCat Chinese Desktop Runtime 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact,并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset,明确二维码过期或扫码确认窗口失效的 QR session failure 会自动换码,其他非自动重试 QR failure 会标记下次 WebUI 登录动作先重置 native login service。v14 起运行时会为 QQ/NapCat/Xvfb 长期进程做 PID 级 `/proc/<pid>/mountinfo` 遮蔽,避免 QQCore 通过 `/proc/self/mountinfo` 看到 `overlay`、`/vol1/docker`、`docker-init`、`/docker/containers`、`napcat-instances`、`btrfs`、`/dev/mapper/trim` 等宿主路径;v15 在扫码登录成功回调中先写入 `QQLoginInfo` 再写登录态,避免 API 读到 `isLogin=true` 但 QQ 号为空的短暂不一致。构建前必须运行 `scripts/napcat-desktop-cn-stage-build.mjs` 生成 Docker build context;生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:*` digest。生产 K8s manifest 保留 `kt-napcat-desktop-cn:desktop-cn-v15` / `desktop-cn-v15` 安全默认值;Jenkins `QQBOT_NAPCAT_IMAGE_OVERRIDE` 与 `QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE` 只有非空时才覆盖 API Deployment env,空值会保持 manifest/default env。运行时回滚应重新运行 Jenkins 并填入上一版镜像 digest/profile,或清空两个 override 后重新部署回 manifest 默认值。
|
||||||
|
|
||||||
API 仓库不提交 `NapCat.Shell.zip`;生产镜像必须从 staged context 构建,且 `fork-artifact.json` 必须包含完整 marker metadata:upstream release tag/commit、fork commit、base image digest、Jenkins URL 和 artifact hashes。NapCat base image 在 release evidence 中必须 pin 到 digest。API Jenkins 只做显式参数推广,不负责自动合并上游、自动构建运行时镜像或在 override 为空时隐式改写 NapCat env。
|
API 仓库不提交 `NapCat.Shell.zip`;生产镜像必须从 staged context 构建,且 `fork-artifact.json` 必须包含完整 marker metadata:upstream release tag/commit、fork commit、base image digest、Jenkins URL 和 artifact hashes。NapCat base image 在 release evidence 中必须 pin 到 digest。API Jenkins 只做显式参数推广,不负责自动合并上游、自动构建运行时镜像或在 override 为空时隐式改写 NapCat env。
|
||||||
|
|
||||||
|
|||||||
@ -77,7 +77,7 @@ QQBot 插件定时任务由 manifest 的 `tasks` 声明,平台持久化到 `qq
|
|||||||
|
|
||||||
Admin 环境总览面板使用 `ENV_DASHBOARD_*` 只读配置聚合 local-dev、NAS 线上、腾讯云和 r4se 状态。`ENV_DASHBOARD_ADMIN_LOCAL_URL` / `ENV_DASHBOARD_ADMIN_PUBLIC_URL` 只用于展示 Admin 本机与线上入口证据。HTTP 快照提供当前拓扑,后端 local/MQTT 事件总线通过 SSE 推送增量事件给 Admin;前端不直连 MQTT,也不轮询刷新。Jenkins、K8s、Tencent Cloud、Caddy、WireGuard、Mihomo/OpenClash 未配置时会显示 `unwired` 证据,不能渲染成健康假象;第一版不暴露重启、部署、迁移、容器重建、插件启停或代理切换等写操作。
|
Admin 环境总览面板使用 `ENV_DASHBOARD_*` 只读配置聚合 local-dev、NAS 线上、腾讯云和 r4se 状态。`ENV_DASHBOARD_ADMIN_LOCAL_URL` / `ENV_DASHBOARD_ADMIN_PUBLIC_URL` 只用于展示 Admin 本机与线上入口证据。HTTP 快照提供当前拓扑,后端 local/MQTT 事件总线通过 SSE 推送增量事件给 Admin;前端不直连 MQTT,也不轮询刷新。Jenkins、K8s、Tencent Cloud、Caddy、WireGuard、Mihomo/OpenClash 未配置时会显示 `unwired` 证据,不能渲染成健康假象;第一版不暴露重启、部署、迁移、容器重建、插件启停或代理切换等写操作。
|
||||||
|
|
||||||
NapCat Runtime/Protocol Profile 已完成本地 API/Admin 实施,线上发布和账号闭环按 `docs/superpowers/plans/2026-06-18-qqbot-napcat-runtime-protocol-profile-implementation-plan.md` 的 Task 10 执行。当前实现覆盖运行态/协议/会话行为/历史登录事件兼容表/风险模式表,真实物理设备风格 hostname/MAC,NapCat/OneBot 配置 hash,KT `zh_CN.UTF-8` 中国桌面派生镜像资产,只读 `/qqbot/napcat/runtime/detail` 证据接口,watchdog 离线巡检告警,以及 Admin 账号页“运行态”抽屉;不绕过 QQ/Tencent 验证码、不修改 QQ/NTQQ 签名协议、不启用 privileged/host network,也不做账号级每小时/每日累计发送预算。NapCat Chinese Desktop Runtime v14 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact,并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset,明确二维码过期或扫码确认窗口失效的 QR session failure 会自动换码,其他非自动重试 QR failure 会标记下次 WebUI 登录动作先重置 native login service;v14 还会对 QQ/NapCat/Xvfb 长期进程的 `/proc/<pid>/mountinfo` 做 PID 级遮蔽,防止 `overlay`、`/vol1/docker`、`docker-init`、`/docker/containers`、`napcat-instances` 等宿主路径泄露。镜像必须先用 `scripts/napcat-desktop-cn-stage-build.mjs` staged build context,生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:desktop-cn-v14` digest。`k8s/prod/api.yaml` 保留 `desktop-cn-v14` 安全默认值;Jenkins `QQBOT_NAPCAT_IMAGE_OVERRIDE` 和 `QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE` 仅在填写时通过 `kubectl set env` 推广已验证运行时镜像/profile,空值会继续使用 manifest/default env。回滚时重新运行 Jenkins 并填入上一版 digest/profile,或清空两个 override 后重新部署 manifest 默认值。
|
NapCat Runtime/Protocol Profile 已完成本地 API/Admin 实施,线上发布和账号闭环按 `docs/superpowers/plans/2026-06-18-qqbot-napcat-runtime-protocol-profile-implementation-plan.md` 的 Task 10 执行。当前实现覆盖运行态/协议/会话行为/历史登录事件兼容表/风险模式表,真实物理设备风格 hostname/MAC,NapCat/OneBot 配置 hash,KT `zh_CN.UTF-8` 中国桌面派生镜像资产,只读 `/qqbot/napcat/runtime/detail` 证据接口,watchdog 离线巡检告警,以及 Admin 账号页“运行态”抽屉;不绕过 QQ/Tencent 验证码、不修改 QQ/NTQQ 签名协议、不启用 privileged/host network,也不做账号级每小时/每日累计发送预算。NapCat Chinese Desktop Runtime v15 使用 KT `NapCatQQ` fork 源码构建出的 `NapCat.Shell` artifact,并在 QQ `KickedOffLine` 后重置 native login service 再请求二维码;同一次踢下线事件只消费一次 reset,明确二维码过期或扫码确认窗口失效的 QR session failure 会自动换码,其他非自动重试 QR failure 会标记下次 WebUI 登录动作先重置 native login service;v14 起还会对 QQ/NapCat/Xvfb 长期进程的 `/proc/<pid>/mountinfo` 做 PID 级遮蔽,防止 `overlay`、`/vol1/docker`、`docker-init`、`/docker/containers`、`napcat-instances` 等宿主路径泄露;v15 还修复扫码成功时 `QQLoginInfo` 晚于登录态写入造成的 QQ 号回读空窗。镜像必须先用 `scripts/napcat-desktop-cn-stage-build.mjs` staged build context,生产 `QQBOT_NAPCAT_IMAGE` 应指向验证过的 `kt-napcat-desktop-cn:desktop-cn-v15` digest。`k8s/prod/api.yaml` 保留 `desktop-cn-v15` 安全默认值;Jenkins `QQBOT_NAPCAT_IMAGE_OVERRIDE` 和 `QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE` 仅在填写时通过 `kubectl set env` 推广已验证运行时镜像/profile,空值会继续使用 manifest/default env。回滚时重新运行 Jenkins 并填入上一版 digest/profile,或清空两个 override 后重新部署 manifest 默认值。
|
||||||
|
|
||||||
运行时发布时,API 仓库不提交 `NapCat.Shell.zip`;生产镜像必须从 staged context 构建,`fork-artifact.json` 必须带完整 marker metadata,包括 upstream release tag/commit、fork commit、base image digest、Jenkins URL 和 artifact hashes。release evidence 里的 NapCat base image 必须用 digest pin。API Jenkins 只消费人工确认后的运行时推广参数,不自动合并上游、不自动构建隐藏镜像,也不在 override 为空时覆盖 K8s manifest 中的默认 env。
|
运行时发布时,API 仓库不提交 `NapCat.Shell.zip`;生产镜像必须从 staged context 构建,`fork-artifact.json` 必须带完整 marker metadata,包括 upstream release tag/commit、fork commit、base image digest、Jenkins URL 和 artifact hashes。release evidence 里的 NapCat base image 必须用 digest pin。API Jenkins 只消费人工确认后的运行时推广参数,不自动合并上游、不自动构建隐藏镜像,也不在 override 为空时覆盖 K8s manifest 中的默认 env。
|
||||||
|
|
||||||
@ -171,16 +171,16 @@ API 暴露 `GET /health/runtime` 作为本地 smoke、Jenkins/K8s 和 ktWorkflow
|
|||||||
- WordPress 自动登录失败不会阻断 Admin 主登录,会通过菜单和权限码过滤不可用的 Blog 管理入口。
|
- WordPress 自动登录失败不会阻断 Admin 主登录,会通过菜单和权限码过滤不可用的 Blog 管理入口。
|
||||||
- 系统日志由 pino 输出,Loki 查询统一通过后端 `/system/logs/*` 代理,前端不直连 Loki。
|
- 系统日志由 pino 输出,Loki 查询统一通过后端 `/system/logs/*` 代理,前端不直连 Loki。
|
||||||
- 日志级站内信只承接运行期事件:接口 5xx、QQBot 下线 notice、NapCat 容器最新离线日志会自动聚合通知 `super` 角色;服务端强制 `super` 访问,Admin 不再暴露人工新增/编辑入口;长路径接口错误会压缩 `dedupeKey/title` 到表字段长度内,避免通知入库失败。
|
- 日志级站内信只承接运行期事件:接口 5xx、QQBot 下线 notice、NapCat 容器最新离线日志会自动聚合通知 `super` 角色;服务端强制 `super` 访问,Admin 不再暴露人工新增/编辑入口;长路径接口错误会压缩 `dedupeKey/title` 到表字段长度内,避免通知入库失败。
|
||||||
- QQBot 扫码登录通过 SSE `/qqbot/account/scan/events` 暴露进度,耗时链路不应阻塞普通 HTTP 响应;新增账号扫码会先返回 pending `sessionId`,后台再创建 NapCat 容器并生成二维码。
|
- QQBot 扫码登录通过 SSE `/qqbot/account/scan/events` 暴露进度,耗时链路不应阻塞普通 HTTP 响应;新增账号扫码会先返回 pending `sessionId`,后台再创建 NapCat 容器并生成二维码。`CheckLoginStatus.isLogin=true` 只代表 NapCat 登录阳性,创建账号必须继续等 `GetQQLoginInfo` 返回 `uin/selfId` 后才绑定真实 QQ 号;短暂缺号时会话保持 pending 并显示正在读取 QQ 号,不能重建容器或猜号。
|
||||||
- QQBot 外发统一走发送排队:默认全局间隔 `2500ms`、同会话间隔 `8000ms`、排队抖动 `0-800ms`,超过 `QQBOT_SEND_MAX_QUEUE_WAIT_MS` 时拒绝本次发送,避免高频自动回复形成突发流量。
|
- QQBot 外发统一走发送排队:默认全局间隔 `2500ms`、同会话间隔 `8000ms`、排队抖动 `0-800ms`,超过 `QQBOT_SEND_MAX_QUEUE_WAIT_MS` 时拒绝本次发送,避免高频自动回复形成突发流量。
|
||||||
- QQBot 在线命令和自动回复规则都有运行时保底冷却:默认命令 `5000ms`、规则 `30000ms`;即使数据库里旧数据冷却值更低,也按保底值判定,降低频繁触发风控的概率。
|
- QQBot 在线命令和自动回复规则都有运行时保底冷却:默认命令 `5000ms`、规则 `30000ms`;即使数据库里旧数据冷却值更低,也按保底值判定,降低频繁触发风控的概率。
|
||||||
- QQBot 复读机默认阈值为 4,同一会话默认 10 分钟只复读一次,默认只复读 120 字以内普通文本,避免群聊重复内容导致机器人过于频繁地模拟真人发言。
|
- QQBot 复读机默认阈值为 4,同一会话默认 10 分钟只复读一次,默认只复读 120 字以内普通文本,避免群聊重复内容导致机器人过于频繁地模拟真人发言。
|
||||||
- QQBot 插件平台统一使用 `plugin.json` manifest 描述插件 key、版本、操作、事件、权限、运行预算和包入口;CLI 负责 create/validate/pack/install-local,后端只暴露受控 SDK 能力并通过插件维度记录安装、配置、账号绑定和运行事件。
|
- QQBot 插件平台统一使用 `plugin.json` manifest 描述插件 key、版本、操作、事件、权限、运行预算和包入口;CLI 负责 create/validate/pack/install-local,后端只暴露受控 SDK 能力并通过插件维度记录安装、配置、账号绑定和运行事件。
|
||||||
- Bilibili Card 是事件型内置插件:`bilibili-card.message` 只在账号绑定后监听 QQ/NapCat `share/json/xml/lightapp` 卡片或文本里的 Bilibili 链接,`b23.tv` 短链通过平台 `resolveRedirect` 受控 host 能力解析,视频信息从 Bilibili `x/web-interface/view` 获取后回复纯文本摘要。
|
- Bilibili Card 是事件型内置插件:`bilibili-card.message` 只在账号绑定后监听 QQ/NapCat `share/json/xml/lightapp` 卡片或文本里的 Bilibili 链接,`b23.tv` 短链通过平台 `resolveRedirect` 受控 host 能力解析,视频信息从 Bilibili `x/web-interface/view` 获取后回复纯文本摘要。
|
||||||
- QQBot 同一账号只允许一个有效 NapCat 主容器;绑定新容器时会释放旧绑定和不再共享的旧容器,机器人下线 notice、`isOnline:false` 和 NapCat 容器最新离线日志都会写入账号 `lastError`,普通群成员 kick 不属于账号离线信号;写入 `last_error` 前按 500 字符截断,后续无错误的普通断连不能清空该原因;账号列表拆开展示 OneBot、容器、WebUI 和 QQ 登录态,心跳只代表 OneBot/容器通信,不能推导 QQ 登录态;近期连接只用于避免重连瞬间被旧缓存误伤,后续仍必须以 NapCat WebUI/日志检查判断 QQ 登录态;`qqLoginMessage` 只展示 QQ 登录态消息,WebUI 配置或请求错误留在 `lastError`。
|
- QQBot 同一账号只允许一个有效 NapCat 主容器;绑定新容器时会释放旧绑定和不再共享的旧容器,机器人下线 notice、`isOnline:false` 和 NapCat 容器最新离线日志都会写入账号 `lastError`,普通群成员 kick 不属于账号离线信号;写入 `last_error` 前按 500 字符截断,后续无错误的普通断连不能清空该原因;账号列表拆开展示 OneBot、容器、WebUI 和 QQ 登录态,心跳只代表 OneBot/容器通信,不能推导 QQ 登录态;近期连接只用于避免重连瞬间被旧缓存误伤,后续仍必须以 NapCat WebUI/日志检查判断 QQ 登录态;`qqLoginMessage` 只展示 QQ 登录态消息,WebUI 配置或请求错误留在 `lastError`。
|
||||||
- NapCat 托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像;生产切换镜像前先 pin 明确版本或 digest 并单账号观察。`desktop-cn-v14` 镜像从 KT `NapCatQQ` fork 的 source-built `NapCat.Shell` 构建,不再在镜像内对上游 bundle 做字符串 patch,并修复踢下线后原 native login service 不产新二维码、失败回调反复重置、非自动重试 QR failure 后下次 WebUI 登录动作不重置,以及 QQCore 通过进程级 mountinfo 探针看到 Docker/宿主路径的问题。
|
- NapCat 托管容器必须显式配置 `QQBOT_NAPCAT_IMAGE`,不要依赖 `latest` 默认镜像;生产切换镜像前先 pin 明确版本或 digest 并单账号观察。`desktop-cn-v15` 镜像从 KT `NapCatQQ` fork 的 source-built `NapCat.Shell` 构建,不再在镜像内对上游 bundle 做字符串 patch,并修复踢下线后原 native login service 不产新二维码、失败回调反复重置、非自动重试 QR failure 后下次 WebUI 登录动作不重置、QQCore 通过进程级 mountinfo 探针看到 Docker/宿主路径,以及扫码登录成功后 API 立即读不到 QQ 号的问题。
|
||||||
- NapCat 账号新增/编辑支持可选 QQ 登录密码:Admin 只提交 RSA-OAEP 加密后的 `encryptedLoginPassword`,后端解密后必须用显式配置的 `QQBOT_ACCOUNT_SECRET_KEY`(或非默认 `ADMIN_TOKEN_SECRET`)二次加密保存到 `qqbot_account.napcat_login_password_secret`;空值、`change-me` 和历史公开默认值会被拒绝;列表和详情不回显密码,日志会脱敏密码字段。
|
- NapCat 账号新增/编辑支持可选 QQ 登录密码:Admin 只提交 RSA-OAEP 加密后的 `encryptedLoginPassword`,后端解密后必须用显式配置的 `QQBOT_ACCOUNT_SECRET_KEY`(或非默认 `ADMIN_TOKEN_SECRET`)二次加密保存到 `qqbot_account.napcat_login_password_secret`;空值、`change-me` 和历史公开默认值会被拒绝;列表和详情不回显密码,日志会脱敏密码字段。
|
||||||
- NapCat 容器为已知 `selfId` 创建/重建时会一次性注入 `ACCOUNT` 等必要 env;容器重启(崩溃/重启策略/宿主重启)可复用持久化会话,但硬踢 `登录已失效` 仍需人工登录。Admin「更新登录」不通过 Docker 重建、重启或补 env 刷新登录态:只要源容器在线,就保持同一容器并通过 NapCat WebUI `SetQuickLogin -> PasswordLogin -> RefreshQRcode/GetQQLoginQrcode` 推进原弹窗流程;只有 Docker 容器离线或缺失时,容器准备阶段才创建/重建并一次性注入 env。快速登录失败后,如果账号保存了登录密码,后端使用解密密码计算 MD5 调 `/api/QQLogin/PasswordLogin`,不会把密码写入运行态 env,也没有成功后的 env 清理步骤;密码登录按 `QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS` / `QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS` 轮询结果。准备中的扫码会话会续期,重复调用更新登录会复用同一 pending `sessionId`,不会再次启动 quick/password/二维码准备。若 API Pod 在准备阶段重启,持久化的 `preparingRelogin` 超过 `QQBOT_NAPCAT_RELOGIN_PREPARING_STALE_MS` 后会自动恢复为普通状态检测。pending refresh 会话没有二维码、验证码或新设备挑战时,状态轮询会按 `NAPCAT_LOGIN_QR_AUTO_REFRESH_COOLDOWN_MS` 冷却在同一容器自动重试二维码刷新,避免 SSE 长时间停在生成中。验证码和新设备验证保持同一会话 pending:腾讯验证码结果 `ticket`/`randstr`/`sid` 通过 `/qqbot/account/scan/captcha/submit` 回交到同一容器的 `/api/QQLogin/CaptchaLogin`;状态轮询遇到验证码文案但缺少 URL 时会先从当前容器日志恢复 `proofWaterUrl`,没有 URL 也保持验证码处理中而不切到二维码兜底。密码登录仍失败、验证码未完成、离线、账号不匹配或缺少 QQ 号时,直接通过 WebUI 二维码接口进入扫码兜底,不 reset 登录态。Admin SSE 步骤顺序按实际路径为 `quick-login-*` -> `password-login-*` / `password-login-captcha` / 新设备验证 -> `qrcode/waiting-scan` -> `login-success|login-failed`;SSE 事件缓存因 Pod 重启丢失时,新订阅会收到当前会话快照。
|
- NapCat 容器为已知 `selfId` 创建/重建时会一次性注入 `ACCOUNT` 等必要 env;容器重启(崩溃/重启策略/宿主重启)可复用持久化会话,但硬踢 `登录已失效` 仍需人工登录。Admin「更新登录」不通过 Docker 重建、重启或补 env 刷新登录态:只要源容器在线,就保持同一容器并通过 NapCat WebUI `SetQuickLogin -> PasswordLogin -> RefreshQRcode/GetQQLoginQrcode` 推进原弹窗流程;只有 Docker 容器离线或缺失时,容器准备阶段才创建/重建并一次性注入 env。快速登录失败后,如果账号保存了登录密码,后端使用解密密码计算 MD5 调 `/api/QQLogin/PasswordLogin`,不会把密码写入运行态 env,也没有成功后的 env 清理步骤;密码登录按 `QQBOT_NAPCAT_PASSWORD_LOGIN_WAIT_MS` / `QQBOT_NAPCAT_LOGIN_POLL_INTERVAL_MS` 轮询结果。准备中的扫码会话会续期,重复调用更新登录会复用同一 pending `sessionId`,不会再次启动 quick/password/二维码准备。若 API Pod 在准备阶段重启,持久化的 `preparingRelogin` 超过 `QQBOT_NAPCAT_RELOGIN_PREPARING_STALE_MS` 后会自动恢复为普通状态检测。pending refresh 会话没有二维码、验证码或新设备挑战时,状态轮询会按 `NAPCAT_LOGIN_QR_AUTO_REFRESH_COOLDOWN_MS` 冷却在同一容器自动重试二维码刷新,避免 SSE 长时间停在生成中。验证码和新设备验证保持同一会话 pending:腾讯验证码结果 `ticket`/`randstr`/`sid` 通过 `/qqbot/account/scan/captcha/submit` 回交到同一容器的 `/api/QQLogin/CaptchaLogin`;状态轮询遇到验证码文案但缺少 URL 时会先从当前容器日志恢复 `proofWaterUrl`,没有 URL 也保持验证码处理中而不切到二维码兜底。扫码或密码链路拿到 NapCat 登录阳性但 QQ 号暂不可读时,会按 `NAPCAT_LOGIN_SELF_ID_WAIT_MS` 保持 pending,超过窗口才失败。密码登录仍失败、验证码未完成、离线、账号不匹配或缺少 QQ 号时,直接通过 WebUI 二维码接口进入扫码兜底,不 reset 登录态。Admin SSE 步骤顺序按实际路径为 `quick-login-*` -> `password-login-*` / `password-login-captcha` / 新设备验证 -> `qrcode/waiting-scan` -> `login-success|login-failed`;SSE 事件缓存因 Pod 重启丢失时,新订阅会收到当前会话快照。
|
||||||
- NapCat 设备身份按账号持久化到 `napcat_device_identity`:同一账号重建容器会复用数据目录、`pc-<8hex>` hostname、machine-id 和实体 OUI 风格 MAC,明确排除 Docker `02:42`、QEMU/KVM `52:54:00`、VMware、Hyper-V 等虚拟化前缀;新增账号首次扫码会先用预留容器 id 创建临时设备身份并应用到第一次 Docker run,扫码成功后归属到真实账号并同步 runtime/protocol profile;Docker run 会注入 `--hostname`、`--mac-address`、只读 `/etc/machine-id`,并同步写入 QQNT Linux `machine-info`,使 `/etc/machine-id`、Docker MAC 和 QQNT 本地设备缓存保持一致。当前策略名为 `qqnt-visible-hostname-v1` / `physical-oui-mac-v1`。
|
- NapCat 设备身份按账号持久化到 `napcat_device_identity`:同一账号重建容器会复用数据目录、`pc-<8hex>` hostname、machine-id 和实体 OUI 风格 MAC,明确排除 Docker `02:42`、QEMU/KVM `52:54:00`、VMware、Hyper-V 等虚拟化前缀;新增账号首次扫码会先用预留容器 id 创建临时设备身份并应用到第一次 Docker run,扫码成功后归属到真实账号并同步 runtime/protocol profile;Docker run 会注入 `--hostname`、`--mac-address`、只读 `/etc/machine-id`,并同步写入 QQNT Linux `machine-info`,使 `/etc/machine-id`、Docker MAC 和 QQNT 本地设备缓存保持一致。当前策略名为 `qqnt-visible-hostname-v1` / `physical-oui-mac-v1`。
|
||||||
- NapCat 新设备验证走同一 scan session:`CaptchaLogin` 返回 `needNewDevice` 后,后端继续调用 `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin`,Admin/SSE 分开展示 `captchaUrl`、`newDeviceQrcode`、已扫码、确认中、验证成功、登录成功/失败等中文进度,不把 `jumpUrl` 当作唯一完成入口。
|
- NapCat 新设备验证走同一 scan session:`CaptchaLogin` 返回 `needNewDevice` 后,后端继续调用 `GetNewDeviceQRCode -> PollNewDeviceQR -> NewDeviceLogin`,Admin/SSE 分开展示 `captchaUrl`、`newDeviceQrcode`、已扫码、确认中、验证成功、登录成功/失败等中文进度,不把 `jumpUrl` 当作唯一完成入口。
|
||||||
- NapCat 离线看门狗按 `QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS`(默认 `120000`,最小 `30000`,`QQBOT_NAPCAT_WATCHDOG_ENABLED=false` 关闭)定时巡检在线账号,使掉线/被踢无需管理员打开列表页即可及时发现;检测到离线后只写入离线原因并复用 `super` 站内信告警,恢复登录必须由管理员在 Admin 手动触发「更新登录」。
|
- NapCat 离线看门狗按 `QQBOT_NAPCAT_WATCHDOG_INTERVAL_MS`(默认 `120000`,最小 `30000`,`QQBOT_NAPCAT_WATCHDOG_ENABLED=false` 关闭)定时巡检在线账号,使掉线/被踢无需管理员打开列表页即可及时发现;检测到离线后只写入离线原因并复用 `super` 站内信告警,恢复登录必须由管理员在 Admin 手动触发「更新登录」。
|
||||||
|
|||||||
@ -33,9 +33,9 @@ spec:
|
|||||||
- name: DB_TIMEZONE
|
- name: DB_TIMEZONE
|
||||||
value: "+08:00"
|
value: "+08:00"
|
||||||
- name: QQBOT_NAPCAT_IMAGE
|
- name: QQBOT_NAPCAT_IMAGE
|
||||||
value: kt-napcat-desktop-cn:desktop-cn-v14
|
value: kt-napcat-desktop-cn:desktop-cn-v15
|
||||||
- name: QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION
|
- name: QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION
|
||||||
value: desktop-cn-v14
|
value: desktop-cn-v15
|
||||||
- name: QQBOT_NAPCAT_SSH_KEY_PATH
|
- name: QQBOT_NAPCAT_SSH_KEY_PATH
|
||||||
value: /app/secrets/napcat-ssh/id_rsa
|
value: /app/secrets/napcat-ssh/id_rsa
|
||||||
- name: QQBOT_PLUGIN_TASK_QUEUE_REDIS_PREFIX
|
- name: QQBOT_PLUGIN_TASK_QUEUE_REDIS_PREFIX
|
||||||
|
|||||||
@ -337,6 +337,7 @@ export type QqbotLoginScanSession = {
|
|||||||
expectedSelfId?: string;
|
expectedSelfId?: string;
|
||||||
id: string;
|
id: string;
|
||||||
lastCaptchaLookupAt?: number;
|
lastCaptchaLookupAt?: number;
|
||||||
|
loginSelfIdMissingSince?: number;
|
||||||
lastQrcodeRefreshAt?: number;
|
lastQrcodeRefreshAt?: number;
|
||||||
lastRestartedAt?: number;
|
lastRestartedAt?: number;
|
||||||
mode: QqbotLoginScanMode;
|
mode: QqbotLoginScanMode;
|
||||||
|
|||||||
@ -863,8 +863,9 @@ export class QqbotNapcatLoginService {
|
|||||||
|
|
||||||
const selfId = this.toolsService.pickNapcatSelfId(loginInfo);
|
const selfId = this.toolsService.pickNapcatSelfId(loginInfo);
|
||||||
if (!selfId) {
|
if (!selfId) {
|
||||||
return this.failSession(session, 'NapCat 已登录但未返回 QQ 号');
|
return this.keepLoginSelfIdPending(session);
|
||||||
}
|
}
|
||||||
|
session.loginSelfIdMissingSince = undefined;
|
||||||
if (session.expectedSelfId && session.expectedSelfId !== selfId) {
|
if (session.expectedSelfId && session.expectedSelfId !== selfId) {
|
||||||
return this.failSession(
|
return this.failSession(
|
||||||
session,
|
session,
|
||||||
@ -2078,6 +2079,39 @@ export class QqbotNapcatLoginService {
|
|||||||
return this.toResult(session);
|
return this.toResult(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps a login-positive session alive while NapCat finishes exposing the logged-in QQ number.
|
||||||
|
* @param session - Scan session whose WebUI status is already logged in but whose `GetQQLoginInfo` payload lacks `uin`/`selfId`.
|
||||||
|
* @returns Pending result during the bounded wait window, or a terminal failure once the missing-self-id window is exhausted.
|
||||||
|
*/
|
||||||
|
private async keepLoginSelfIdPending(session: QqbotLoginScanSession) {
|
||||||
|
const now = Date.now();
|
||||||
|
session.loginSelfIdMissingSince ??= now;
|
||||||
|
if (now - session.loginSelfIdMissingSince > this.getLoginSelfIdWaitMs()) {
|
||||||
|
return this.failSession(session, 'NapCat 已登录但未返回 QQ 号');
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = 'NapCat 已登录,正在读取 QQ 号';
|
||||||
|
const shouldPublish = session.errorMessage !== message;
|
||||||
|
session.status = 'pending';
|
||||||
|
session.captchaUrl = undefined;
|
||||||
|
session.errorMessage = message;
|
||||||
|
session.preparingContainer = false;
|
||||||
|
session.preparingRelogin = false;
|
||||||
|
session.qrcode = undefined;
|
||||||
|
this.renewSessionExpiry(session);
|
||||||
|
this.persistLoginSession(session);
|
||||||
|
if (shouldPublish) {
|
||||||
|
this.publishScanResultEvent(
|
||||||
|
session,
|
||||||
|
'login-self-id-wait',
|
||||||
|
'processing',
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.toResult(session);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops delayed background login work from mutating a pending session after it expired or was replaced.
|
* Stops delayed background login work from mutating a pending session after it expired or was replaced.
|
||||||
* @param session - Pending scan session captured by an async relogin task; its cache identity and TTL decide whether writes are still valid.
|
* @param session - Pending scan session captured by an async relogin task; its cache identity and TTL decide whether writes are still valid.
|
||||||
@ -2109,6 +2143,8 @@ export class QqbotNapcatLoginService {
|
|||||||
session.errorMessage = errorMessage;
|
session.errorMessage = errorMessage;
|
||||||
session.passwordMd5 = undefined;
|
session.passwordMd5 = undefined;
|
||||||
session.preparingRelogin = false;
|
session.preparingRelogin = false;
|
||||||
|
this.persistLoginSession(session);
|
||||||
|
await this.loginSessionStore.flushSessionWrites(session.id);
|
||||||
this.publishScanResultEvent(session, 'login-error', 'error', errorMessage);
|
this.publishScanResultEvent(session, 'login-error', 'error', errorMessage);
|
||||||
this.loginSessionStore.delete(session.id);
|
this.loginSessionStore.delete(session.id);
|
||||||
await this.closeSession(session);
|
await this.closeSession(session);
|
||||||
@ -2461,6 +2497,17 @@ export class QqbotNapcatLoginService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the bounded wait window for NapCat to expose QQ number after WebUI already reports login-positive.
|
||||||
|
* @returns Milliseconds allowed for `GetQQLoginInfo` to start returning `uin`/`selfId` before treating the state as inconsistent.
|
||||||
|
*/
|
||||||
|
private getLoginSelfIdWaitMs() {
|
||||||
|
return this.getPositiveConfigNumber(
|
||||||
|
'NAPCAT_LOGIN_SELF_ID_WAIT_MS',
|
||||||
|
30_000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询 NapCat 登录运行态数据。
|
* 查询 NapCat 登录运行态数据。
|
||||||
* @param container - container 输入;限定 NapCat查询范围。
|
* @param container - container 输入;限定 NapCat查询范围。
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export class NapcatRuntimeProfileService {
|
|||||||
dataDir: input.dataDir,
|
dataDir: input.dataDir,
|
||||||
desktopProfileVersion: this.getString(
|
desktopProfileVersion: this.getString(
|
||||||
'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION',
|
'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION',
|
||||||
'desktop-cn-v14',
|
'desktop-cn-v15',
|
||||||
),
|
),
|
||||||
deviceIdentityId: input.deviceIdentityId,
|
deviceIdentityId: input.deviceIdentityId,
|
||||||
imageRef: this.getString('QQBOT_NAPCAT_IMAGE', ''),
|
imageRef: this.getString('QQBOT_NAPCAT_IMAGE', ''),
|
||||||
|
|||||||
@ -158,13 +158,13 @@ describe('NapCat Chinese Desktop Runtime image assets', () => {
|
|||||||
expect(verify).not.toContain('selfInfo?.online !== false');
|
expect(verify).not.toContain('selfInfo?.online !== false');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deploys the production API with the verified desktop-cn-v14 runtime profile', () => {
|
it('deploys the production API with the verified desktop-cn-v15 runtime profile', () => {
|
||||||
const manifest = readSource('k8s/prod/api.yaml');
|
const manifest = readSource('k8s/prod/api.yaml');
|
||||||
|
|
||||||
expect(manifest).toContain('name: QQBOT_NAPCAT_IMAGE');
|
expect(manifest).toContain('name: QQBOT_NAPCAT_IMAGE');
|
||||||
expect(manifest).toContain('value: kt-napcat-desktop-cn:desktop-cn-v14');
|
expect(manifest).toContain('value: kt-napcat-desktop-cn:desktop-cn-v15');
|
||||||
expect(manifest).toContain('name: QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION');
|
expect(manifest).toContain('name: QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION');
|
||||||
expect(manifest).toContain('value: desktop-cn-v14');
|
expect(manifest).toContain('value: desktop-cn-v15');
|
||||||
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v8');
|
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v8');
|
||||||
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v7');
|
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v7');
|
||||||
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v4');
|
expect(manifest).not.toContain('kt-napcat-desktop-cn:desktop-cn-v4');
|
||||||
|
|||||||
@ -33,7 +33,7 @@ describe('NapCat runtime promotion contract', () => {
|
|||||||
it('keeps a safe default runtime profile in the K8s manifest', () => {
|
it('keeps a safe default runtime profile in the K8s manifest', () => {
|
||||||
const manifest = readSource('k8s/prod/api.yaml');
|
const manifest = readSource('k8s/prod/api.yaml');
|
||||||
|
|
||||||
expect(manifest).toContain('value: kt-napcat-desktop-cn:desktop-cn-v14');
|
expect(manifest).toContain('value: kt-napcat-desktop-cn:desktop-cn-v15');
|
||||||
expect(manifest).toContain('value: desktop-cn-v14');
|
expect(manifest).toContain('value: desktop-cn-v15');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -123,7 +123,7 @@ describe('NapCat runtime profile generation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(profile).toMatchObject({
|
expect(profile).toMatchObject({
|
||||||
desktopProfileVersion: 'desktop-cn-v14',
|
desktopProfileVersion: 'desktop-cn-v15',
|
||||||
imageRef: 'kt-napcat-desktop-cn@sha256:profiledigest',
|
imageRef: 'kt-napcat-desktop-cn@sha256:profiledigest',
|
||||||
locale: 'zh_CN.UTF-8',
|
locale: 'zh_CN.UTF-8',
|
||||||
runtimeGid: 1101,
|
runtimeGid: 1101,
|
||||||
|
|||||||
@ -1234,6 +1234,116 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps scan pending when NapCat is login-positive before QQ number is readable', async () => {
|
||||||
|
const container = {
|
||||||
|
baseUrl: 'http://127.0.0.1:6103/',
|
||||||
|
id: 'container-login-wait-self-id',
|
||||||
|
name: 'napcat-login-wait-self-id',
|
||||||
|
};
|
||||||
|
const accountService = {
|
||||||
|
ensureScannedAccount: jest.fn().mockResolvedValue('account-1'),
|
||||||
|
};
|
||||||
|
const containerService = {
|
||||||
|
bindAccount: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
const loginService = new QqbotNapcatLoginService(
|
||||||
|
{ get: jest.fn() } as unknown as ConfigService,
|
||||||
|
accountService as unknown as QqbotAccountService,
|
||||||
|
containerService as unknown as QqbotNapcatContainerService,
|
||||||
|
new ToolsService(),
|
||||||
|
);
|
||||||
|
const session = (loginService as any).createSession({
|
||||||
|
container,
|
||||||
|
mode: 'create',
|
||||||
|
qrcode: 'https://txz.qq.com/p?k=ready&f=1600001615',
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
(loginService as any).sessions.set(session.id, session);
|
||||||
|
jest
|
||||||
|
.spyOn(loginService as any, 'getSessionContainer')
|
||||||
|
.mockResolvedValue(container);
|
||||||
|
jest
|
||||||
|
.spyOn(loginService as any, 'getLoginStatus')
|
||||||
|
.mockResolvedValue({ isLogin: true });
|
||||||
|
jest.spyOn(loginService as any, 'getLoginInfo').mockResolvedValue({
|
||||||
|
nick: 'Mirror',
|
||||||
|
online: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await loginService.status(session.id);
|
||||||
|
|
||||||
|
expect(result.status).toBe('pending');
|
||||||
|
expect(result.errorMessage).toBe('NapCat 已登录,正在读取 QQ 号');
|
||||||
|
expect((loginService as any).sessions.get(session.id)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errorMessage: 'NapCat 已登录,正在读取 QQ 号',
|
||||||
|
status: 'pending',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(accountService.ensureScannedAccount).not.toHaveBeenCalled();
|
||||||
|
expect(containerService.bindAccount).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('completes scan after a login-positive session later exposes QQ number', async () => {
|
||||||
|
const container = {
|
||||||
|
baseUrl: 'http://127.0.0.1:6103/',
|
||||||
|
id: 'container-login-late-self-id',
|
||||||
|
name: 'napcat-login-late-self-id',
|
||||||
|
};
|
||||||
|
const accountService = {
|
||||||
|
ensureScannedAccount: jest.fn().mockResolvedValue('account-1'),
|
||||||
|
};
|
||||||
|
const containerService = {
|
||||||
|
bindAccount: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
const loginService = new QqbotNapcatLoginService(
|
||||||
|
{ get: jest.fn() } as unknown as ConfigService,
|
||||||
|
accountService as unknown as QqbotAccountService,
|
||||||
|
containerService as unknown as QqbotNapcatContainerService,
|
||||||
|
new ToolsService(),
|
||||||
|
);
|
||||||
|
const session = (loginService as any).createSession({
|
||||||
|
container,
|
||||||
|
mode: 'create',
|
||||||
|
qrcode: 'https://txz.qq.com/p?k=ready&f=1600001615',
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
(loginService as any).sessions.set(session.id, session);
|
||||||
|
jest
|
||||||
|
.spyOn(loginService as any, 'getSessionContainer')
|
||||||
|
.mockResolvedValue(container);
|
||||||
|
jest
|
||||||
|
.spyOn(loginService as any, 'getLoginStatus')
|
||||||
|
.mockResolvedValue({ isLogin: true });
|
||||||
|
jest
|
||||||
|
.spyOn(loginService as any, 'getLoginInfo')
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
nick: 'Mirror',
|
||||||
|
online: true,
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
nick: 'Mirror',
|
||||||
|
online: true,
|
||||||
|
uin: '1914728559',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pending = await loginService.status(session.id);
|
||||||
|
const completed = await loginService.status(session.id);
|
||||||
|
|
||||||
|
expect(pending.status).toBe('pending');
|
||||||
|
expect(completed.status).toBe('success');
|
||||||
|
expect(accountService.ensureScannedAccount).toHaveBeenCalledWith({
|
||||||
|
name: 'Mirror',
|
||||||
|
selfId: '1914728559',
|
||||||
|
});
|
||||||
|
expect(containerService.bindAccount).toHaveBeenCalledWith(
|
||||||
|
'account-1',
|
||||||
|
'container-login-late-self-id',
|
||||||
|
'1914728559',
|
||||||
|
);
|
||||||
|
expect(session.loginSelfIdMissingSince).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('does not let the server-side qrcode monitor extend the original qrcode deadline on temporary errors', async () => {
|
it('does not let the server-side qrcode monitor extend the original qrcode deadline on temporary errors', async () => {
|
||||||
const container = {
|
const container = {
|
||||||
baseUrl: 'http://127.0.0.1:6103/',
|
baseUrl: 'http://127.0.0.1:6103/',
|
||||||
@ -3629,6 +3739,62 @@ describe('QqbotNapcatLoginService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('persists failed scan sessions before cleaning runtime state', async () => {
|
||||||
|
const loginSessionRepository = createLoginSessionRepository();
|
||||||
|
const loginStateStore = new NapcatLoginStateStoreService(
|
||||||
|
loginSessionRepository as any,
|
||||||
|
);
|
||||||
|
const failService = new QqbotNapcatLoginService(
|
||||||
|
{ get: jest.fn() } as unknown as ConfigService,
|
||||||
|
{
|
||||||
|
ensureScannedAccount: jest.fn(),
|
||||||
|
} as unknown as QqbotAccountService,
|
||||||
|
{
|
||||||
|
removeUnboundCreateContainer: jest.fn().mockResolvedValue(undefined),
|
||||||
|
} as unknown as QqbotNapcatContainerService,
|
||||||
|
new ToolsService(),
|
||||||
|
loginStateStore,
|
||||||
|
);
|
||||||
|
const session = (failService as any).createSession({
|
||||||
|
container: {
|
||||||
|
id: 'container-fail-persist',
|
||||||
|
name: 'napcat-fail-persist',
|
||||||
|
webuiPort: 6110,
|
||||||
|
},
|
||||||
|
mode: 'create',
|
||||||
|
qrcode: 'login-qrcode',
|
||||||
|
status: 'pending',
|
||||||
|
});
|
||||||
|
(failService as any).sessions.set(session.id, session);
|
||||||
|
await loginStateStore.flushSessionWrites(session.id);
|
||||||
|
const flushSessionWrites = jest.spyOn(
|
||||||
|
loginStateStore,
|
||||||
|
'flushSessionWrites',
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await (failService as any).failSession(
|
||||||
|
session,
|
||||||
|
'NapCat 已登录但未返回 QQ 号',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.status).toBe('error');
|
||||||
|
expect(result.errorMessage).toBe('NapCat 已登录但未返回 QQ 号');
|
||||||
|
expect(flushSessionWrites).toHaveBeenCalledWith(session.id);
|
||||||
|
expect(loginSessionRepository.rows[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
progressMessage: 'NapCat 已登录但未返回 QQ 号',
|
||||||
|
sessionKey: session.id,
|
||||||
|
status: 'error',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(loginSessionRepository.rows[0].completedAt).toEqual(
|
||||||
|
expect.any(Date),
|
||||||
|
);
|
||||||
|
expect(loginSessionRepository.rows[0].sessionPayload?.status).toBe(
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps an API-expired refresh session pending while NapCat still exposes the same live qrcode', async () => {
|
it('keeps an API-expired refresh session pending while NapCat still exposes the same live qrcode', async () => {
|
||||||
const loginSessionRepository = createLoginSessionRepository();
|
const loginSessionRepository = createLoginSessionRepository();
|
||||||
const loginStateStore = new NapcatLoginStateStoreService(
|
const loginStateStore = new NapcatLoginStateStoreService(
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user