From 33ebdac389cef4ebea02278b31f5ff3e80173cb6 Mon Sep 17 00:00:00 2001 From: sunlei Date: Mon, 15 Jun 2026 06:16:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90NapCat=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E8=BF=9B=E5=BA=A6=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- apps/web-antdv-next/src/api/qqbot/index.ts | 5 ++ .../src/api/qqbot/napcat.spec.ts | 44 +++++++++++++ apps/web-antdv-next/src/api/qqbot/napcat.ts | 50 +++++++++++++++ .../src/views/qqbot/account/list.tsx | 62 +++++++++++++++++-- 5 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 apps/web-antdv-next/src/api/qqbot/napcat.spec.ts create mode 100644 apps/web-antdv-next/src/api/qqbot/napcat.ts diff --git a/README.md b/README.md index 4aa7548..f573479 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ pnpm run build:antdv-next ## 业务页面 - 系统管理 / 站内信是日志级通知列表,只展示 API 错误、QQBot 下线、NapCat 离线等后端自动捕获事件;页面提供筛选、处理/重新打开、置顶和删除,不提供人工新增或编辑。 -- QQBot / 账号连接页拆分 OneBot 连接、QQ 登录、NapCat 运行和运行说明列;更新登录通过 SSE 展示 quick / password / captcha / qrcode 每步进度,密码登录触发 QQ 安全验证时在弹窗内完成腾讯验证码并回交 API。 +- QQBot / 账号连接页拆分 OneBot 连接、QQ 登录、NapCat 运行和运行说明列;更新登录通过 SSE 展示 quick / password / captcha / new-device / qrcode 每步中文进度,密码登录触发 QQ 安全验证时在弹窗内完成腾讯验证码并回交 API,新设备验证二维码和腾讯验证码分开展示。 - QQBot / 插件平台页保留在线命令能力表,并提供 manifest 校验、本地插件安装、安装记录、运行事件和账号绑定抽屉,接口走 `/qqbot/plugin-platform/*`。 ## 部署说明 diff --git a/apps/web-antdv-next/src/api/qqbot/index.ts b/apps/web-antdv-next/src/api/qqbot/index.ts index c9f9611..85febbc 100644 --- a/apps/web-antdv-next/src/api/qqbot/index.ts +++ b/apps/web-antdv-next/src/api/qqbot/index.ts @@ -1,5 +1,7 @@ import type { Recordable } from '@vben/types'; +import type { NapcatLoginNewDeviceStatus } from './napcat'; + import { encryptPassword } from '#/api/core/auth'; import { requestClient } from '#/api/request'; @@ -96,9 +98,12 @@ export namespace QqbotApi { captchaUrl?: string; containerId?: string; containerName?: string; + deviceVerifyUrl?: string; errorMessage?: string; expiresAt?: number; mode: 'create' | 'refresh'; + newDeviceQrcode?: string; + newDeviceStatus?: NapcatLoginNewDeviceStatus; qrcode?: string; selfId?: string; sessionId?: string; diff --git a/apps/web-antdv-next/src/api/qqbot/napcat.spec.ts b/apps/web-antdv-next/src/api/qqbot/napcat.spec.ts new file mode 100644 index 0000000..0064cfe --- /dev/null +++ b/apps/web-antdv-next/src/api/qqbot/napcat.spec.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; + +import { + getNapcatNewDeviceStatusMessage, + NAPCAT_LOGIN_PROGRESS_LABELS, + resolveNapcatLoginDisplayQrcode, +} from './napcat'; + +describe('napcat login display helpers', () => { + it('keeps all Chinese progress labels required by the login flow', () => { + expect(NAPCAT_LOGIN_PROGRESS_LABELS).toMatchObject({ + 'captcha-submit': '验证码已提交,等待确认', + 'login-failed': '登录失败', + 'login-success': '登录成功', + 'manual-qr-required': '正在生成手动二维码', + 'new-device-confirming': '新设备确认中', + 'new-device-qrcode-ready': '新设备二维码待扫码', + 'new-device-required': '需要新设备验证二维码', + 'new-device-scanned': '新设备二维码已扫码', + 'new-device-verified': '新设备验证成功,继续登录', + 'password-login': '正在密码登录', + 'password-login-captcha': '需要验证码', + 'password-login-captcha-submit': '验证码已提交,等待确认', + 'password-login-failed': '登录失败', + 'password-login-start': '正在密码登录', + 'quick-login-fallback': '快速登录失败,进入密码登录', + 'quick-login-start': '正在快速登录', + 'runtime-cleanup-failed': '运行态清理失败', + }); + }); + + it('shows new-device QR separately before normal login QR or captcha', () => { + expect( + resolveNapcatLoginDisplayQrcode({ + captchaUrl: 'https://captcha.qq.com/', + newDeviceQrcode: 'data:image/png;base64,new-device', + qrcode: 'normal-login-qrcode', + }), + ).toBe('data:image/png;base64,new-device'); + expect(getNapcatNewDeviceStatusMessage('scanned')).toBe( + '新设备二维码已扫码', + ); + }); +}); diff --git a/apps/web-antdv-next/src/api/qqbot/napcat.ts b/apps/web-antdv-next/src/api/qqbot/napcat.ts new file mode 100644 index 0000000..93b5147 --- /dev/null +++ b/apps/web-antdv-next/src/api/qqbot/napcat.ts @@ -0,0 +1,50 @@ +export type NapcatLoginNewDeviceStatus = + | 'confirming' + | 'expired' + | 'failed' + | 'qr-pending' + | 'scanned' + | 'verified'; + +export type NapcatLoginDisplayQrcodeSource = { + captchaUrl?: string; + newDeviceQrcode?: string; + qrcode?: string; +}; + +export const NAPCAT_LOGIN_PROGRESS_LABELS = { + 'captcha-submit': '验证码已提交,等待确认', + 'login-failed': '登录失败', + 'login-success': '登录成功', + 'manual-qr-required': '正在生成手动二维码', + 'new-device-confirming': '新设备确认中', + 'new-device-qrcode-ready': '新设备二维码待扫码', + 'new-device-required': '需要新设备验证二维码', + 'new-device-scanned': '新设备二维码已扫码', + 'new-device-verified': '新设备验证成功,继续登录', + 'password-login': '正在密码登录', + 'password-login-captcha': '需要验证码', + 'password-login-captcha-submit': '验证码已提交,等待确认', + 'password-login-failed': '登录失败', + 'password-login-start': '正在密码登录', + 'quick-login-fallback': '快速登录失败,进入密码登录', + 'quick-login-start': '正在快速登录', + 'runtime-cleanup-failed': '运行态清理失败', +} as const; + +export function resolveNapcatLoginDisplayQrcode( + source: NapcatLoginDisplayQrcodeSource, +) { + return source.newDeviceQrcode || source.qrcode || ''; +} + +export function getNapcatNewDeviceStatusMessage( + status?: NapcatLoginNewDeviceStatus, +) { + if (status === 'scanned') return '新设备二维码已扫码'; + if (status === 'confirming') return '新设备确认中'; + if (status === 'verified') return '新设备验证成功,继续登录'; + if (status === 'expired') return '新设备二维码已过期'; + if (status === 'failed') return '新设备验证失败'; + return '新设备二维码待扫码'; +} diff --git a/apps/web-antdv-next/src/views/qqbot/account/list.tsx b/apps/web-antdv-next/src/views/qqbot/account/list.tsx index d407979..5acc8ae 100644 --- a/apps/web-antdv-next/src/views/qqbot/account/list.tsx +++ b/apps/web-antdv-next/src/views/qqbot/account/list.tsx @@ -1,6 +1,7 @@ import type { TableColumnType } from 'antdv-next'; import type { QqbotApi } from '#/api/qqbot'; +import type { NapcatLoginNewDeviceStatus } from '#/api/qqbot/napcat'; import type { KtTableApi, KtTableButton, @@ -39,6 +40,10 @@ import { submitQqbotAccountScanCaptcha, updateQqbotAccount, } from '#/api/qqbot'; +import { + getNapcatNewDeviceStatusMessage, + resolveNapcatLoginDisplayQrcode, +} from '#/api/qqbot/napcat'; import { KtTable, useKtTable } from '#/components/ktTable'; const AKtTable = KtTable as any; @@ -87,9 +92,12 @@ export default defineComponent({ captchaUrl?: string; containerId?: string; containerName?: string; + deviceVerifyUrl?: string; errorMessage?: string; expiresAt?: number; mode: 'create' | 'refresh'; + newDeviceQrcode?: string; + newDeviceStatus?: NapcatLoginNewDeviceStatus; selfId?: string; sessionId?: string; status: 'error' | 'expired' | 'idle' | 'pending' | 'success'; @@ -130,6 +138,9 @@ export default defineComponent({ Math.max(scanProgressItems.value.length - 1, 0), ); const scanQrcodePlaceholderText = computed(() => { + if (scanState.newDeviceStatus) { + return getNapcatNewDeviceStatusMessage(scanState.newDeviceStatus); + } if (scanState.captchaUrl) { return '等待安全验证'; } @@ -429,14 +440,17 @@ export default defineComponent({ scanState.captchaUrl = result.captchaUrl; scanState.containerId = result.containerId; scanState.containerName = result.containerName; + scanState.deviceVerifyUrl = result.deviceVerifyUrl; scanState.errorMessage = result.errorMessage; scanState.expiresAt = result.expiresAt; scanState.mode = result.mode; + scanState.newDeviceQrcode = result.newDeviceQrcode; + scanState.newDeviceStatus = result.newDeviceStatus; scanState.selfId = result.selfId; scanState.sessionId = result.sessionId; scanState.status = result.status; scanState.webuiPort = result.webuiPort; - const nextQrcode = result.qrcode || ''; + const nextQrcode = resolveNapcatLoginDisplayQrcode(result); const qrcodeChanged = nextQrcode !== scanQrcodeText.value; if (qrcodeChanged) { scanQrcodeImageFailed.value = false; @@ -572,7 +586,9 @@ export default defineComponent({ } if (event.result) { void applyScanResult(event.result, { - reloadQrcode: event.step === 'qrcode-ready', + reloadQrcode: ['new-device-qrcode-ready', 'qrcode-ready'].includes( + event.step, + ), }); } } catch { @@ -587,9 +603,12 @@ export default defineComponent({ captchaUrl: undefined, containerId: undefined, containerName: undefined, + deviceVerifyUrl: undefined, errorMessage: undefined, expiresAt: undefined, mode, + newDeviceQrcode: undefined, + newDeviceStatus: undefined, selfId: undefined, sessionId: undefined, status: 'idle', @@ -621,6 +640,12 @@ export default defineComponent({ return 'info'; } + function getNewDeviceAlertType(status?: NapcatLoginNewDeviceStatus) { + if (status === 'failed') return 'error'; + if (status === 'verified') return 'success'; + return 'warning'; + } + function getScanMessage() { if (scanState.status === 'success') return '扫码登录成功'; if (scanState.status === 'error') { @@ -1021,7 +1046,11 @@ export default defineComponent({ 关闭 , ) : null} - {scanState.captchaUrl ? ( + {scanState.newDeviceStatus ? ( + + + 请使用手机 QQ + 扫描下方新设备验证二维码,并在手机端确认登录。 + + {scanState.deviceVerifyUrl ? ( + + 打开新设备验证链接 + + ) : null} + + } + message={getNapcatNewDeviceStatusMessage( + scanState.newDeviceStatus, + )} + showIcon + type={getNewDeviceAlertType(scanState.newDeviceStatus)} + /> + ) : null} + {scanState.captchaUrl && !scanState.newDeviceStatus ? (