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 ? (