feat: 完成NapCat登录进度展示
This commit is contained in:
parent
15480ca705
commit
33ebdac389
@ -47,7 +47,7 @@ pnpm run build:antdv-next
|
|||||||
## 业务页面
|
## 业务页面
|
||||||
|
|
||||||
- 系统管理 / 站内信是日志级通知列表,只展示 API 错误、QQBot 下线、NapCat 离线等后端自动捕获事件;页面提供筛选、处理/重新打开、置顶和删除,不提供人工新增或编辑。
|
- 系统管理 / 站内信是日志级通知列表,只展示 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/*`。
|
- QQBot / 插件平台页保留在线命令能力表,并提供 manifest 校验、本地插件安装、安装记录、运行事件和账号绑定抽屉,接口走 `/qqbot/plugin-platform/*`。
|
||||||
|
|
||||||
## 部署说明
|
## 部署说明
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import type { Recordable } from '@vben/types';
|
import type { Recordable } from '@vben/types';
|
||||||
|
|
||||||
|
import type { NapcatLoginNewDeviceStatus } from './napcat';
|
||||||
|
|
||||||
import { encryptPassword } from '#/api/core/auth';
|
import { encryptPassword } from '#/api/core/auth';
|
||||||
import { requestClient } from '#/api/request';
|
import { requestClient } from '#/api/request';
|
||||||
|
|
||||||
@ -96,9 +98,12 @@ export namespace QqbotApi {
|
|||||||
captchaUrl?: string;
|
captchaUrl?: string;
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
containerName?: string;
|
containerName?: string;
|
||||||
|
deviceVerifyUrl?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
expiresAt?: number;
|
expiresAt?: number;
|
||||||
mode: 'create' | 'refresh';
|
mode: 'create' | 'refresh';
|
||||||
|
newDeviceQrcode?: string;
|
||||||
|
newDeviceStatus?: NapcatLoginNewDeviceStatus;
|
||||||
qrcode?: string;
|
qrcode?: string;
|
||||||
selfId?: string;
|
selfId?: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
|
|||||||
44
apps/web-antdv-next/src/api/qqbot/napcat.spec.ts
Normal file
44
apps/web-antdv-next/src/api/qqbot/napcat.spec.ts
Normal file
@ -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(
|
||||||
|
'新设备二维码已扫码',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
50
apps/web-antdv-next/src/api/qqbot/napcat.ts
Normal file
50
apps/web-antdv-next/src/api/qqbot/napcat.ts
Normal file
@ -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 '新设备二维码待扫码';
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import type { TableColumnType } from 'antdv-next';
|
import type { TableColumnType } from 'antdv-next';
|
||||||
|
|
||||||
import type { QqbotApi } from '#/api/qqbot';
|
import type { QqbotApi } from '#/api/qqbot';
|
||||||
|
import type { NapcatLoginNewDeviceStatus } from '#/api/qqbot/napcat';
|
||||||
import type {
|
import type {
|
||||||
KtTableApi,
|
KtTableApi,
|
||||||
KtTableButton,
|
KtTableButton,
|
||||||
@ -39,6 +40,10 @@ import {
|
|||||||
submitQqbotAccountScanCaptcha,
|
submitQqbotAccountScanCaptcha,
|
||||||
updateQqbotAccount,
|
updateQqbotAccount,
|
||||||
} from '#/api/qqbot';
|
} from '#/api/qqbot';
|
||||||
|
import {
|
||||||
|
getNapcatNewDeviceStatusMessage,
|
||||||
|
resolveNapcatLoginDisplayQrcode,
|
||||||
|
} from '#/api/qqbot/napcat';
|
||||||
import { KtTable, useKtTable } from '#/components/ktTable';
|
import { KtTable, useKtTable } from '#/components/ktTable';
|
||||||
|
|
||||||
const AKtTable = KtTable as any;
|
const AKtTable = KtTable as any;
|
||||||
@ -87,9 +92,12 @@ export default defineComponent({
|
|||||||
captchaUrl?: string;
|
captchaUrl?: string;
|
||||||
containerId?: string;
|
containerId?: string;
|
||||||
containerName?: string;
|
containerName?: string;
|
||||||
|
deviceVerifyUrl?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
expiresAt?: number;
|
expiresAt?: number;
|
||||||
mode: 'create' | 'refresh';
|
mode: 'create' | 'refresh';
|
||||||
|
newDeviceQrcode?: string;
|
||||||
|
newDeviceStatus?: NapcatLoginNewDeviceStatus;
|
||||||
selfId?: string;
|
selfId?: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
status: 'error' | 'expired' | 'idle' | 'pending' | 'success';
|
status: 'error' | 'expired' | 'idle' | 'pending' | 'success';
|
||||||
@ -130,6 +138,9 @@ export default defineComponent({
|
|||||||
Math.max(scanProgressItems.value.length - 1, 0),
|
Math.max(scanProgressItems.value.length - 1, 0),
|
||||||
);
|
);
|
||||||
const scanQrcodePlaceholderText = computed(() => {
|
const scanQrcodePlaceholderText = computed(() => {
|
||||||
|
if (scanState.newDeviceStatus) {
|
||||||
|
return getNapcatNewDeviceStatusMessage(scanState.newDeviceStatus);
|
||||||
|
}
|
||||||
if (scanState.captchaUrl) {
|
if (scanState.captchaUrl) {
|
||||||
return '等待安全验证';
|
return '等待安全验证';
|
||||||
}
|
}
|
||||||
@ -429,14 +440,17 @@ export default defineComponent({
|
|||||||
scanState.captchaUrl = result.captchaUrl;
|
scanState.captchaUrl = result.captchaUrl;
|
||||||
scanState.containerId = result.containerId;
|
scanState.containerId = result.containerId;
|
||||||
scanState.containerName = result.containerName;
|
scanState.containerName = result.containerName;
|
||||||
|
scanState.deviceVerifyUrl = result.deviceVerifyUrl;
|
||||||
scanState.errorMessage = result.errorMessage;
|
scanState.errorMessage = result.errorMessage;
|
||||||
scanState.expiresAt = result.expiresAt;
|
scanState.expiresAt = result.expiresAt;
|
||||||
scanState.mode = result.mode;
|
scanState.mode = result.mode;
|
||||||
|
scanState.newDeviceQrcode = result.newDeviceQrcode;
|
||||||
|
scanState.newDeviceStatus = result.newDeviceStatus;
|
||||||
scanState.selfId = result.selfId;
|
scanState.selfId = result.selfId;
|
||||||
scanState.sessionId = result.sessionId;
|
scanState.sessionId = result.sessionId;
|
||||||
scanState.status = result.status;
|
scanState.status = result.status;
|
||||||
scanState.webuiPort = result.webuiPort;
|
scanState.webuiPort = result.webuiPort;
|
||||||
const nextQrcode = result.qrcode || '';
|
const nextQrcode = resolveNapcatLoginDisplayQrcode(result);
|
||||||
const qrcodeChanged = nextQrcode !== scanQrcodeText.value;
|
const qrcodeChanged = nextQrcode !== scanQrcodeText.value;
|
||||||
if (qrcodeChanged) {
|
if (qrcodeChanged) {
|
||||||
scanQrcodeImageFailed.value = false;
|
scanQrcodeImageFailed.value = false;
|
||||||
@ -572,7 +586,9 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
if (event.result) {
|
if (event.result) {
|
||||||
void applyScanResult(event.result, {
|
void applyScanResult(event.result, {
|
||||||
reloadQrcode: event.step === 'qrcode-ready',
|
reloadQrcode: ['new-device-qrcode-ready', 'qrcode-ready'].includes(
|
||||||
|
event.step,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@ -587,9 +603,12 @@ export default defineComponent({
|
|||||||
captchaUrl: undefined,
|
captchaUrl: undefined,
|
||||||
containerId: undefined,
|
containerId: undefined,
|
||||||
containerName: undefined,
|
containerName: undefined,
|
||||||
|
deviceVerifyUrl: undefined,
|
||||||
errorMessage: undefined,
|
errorMessage: undefined,
|
||||||
expiresAt: undefined,
|
expiresAt: undefined,
|
||||||
mode,
|
mode,
|
||||||
|
newDeviceQrcode: undefined,
|
||||||
|
newDeviceStatus: undefined,
|
||||||
selfId: undefined,
|
selfId: undefined,
|
||||||
sessionId: undefined,
|
sessionId: undefined,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
@ -621,6 +640,12 @@ export default defineComponent({
|
|||||||
return 'info';
|
return 'info';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNewDeviceAlertType(status?: NapcatLoginNewDeviceStatus) {
|
||||||
|
if (status === 'failed') return 'error';
|
||||||
|
if (status === 'verified') return 'success';
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
|
||||||
function getScanMessage() {
|
function getScanMessage() {
|
||||||
if (scanState.status === 'success') return '扫码登录成功';
|
if (scanState.status === 'success') return '扫码登录成功';
|
||||||
if (scanState.status === 'error') {
|
if (scanState.status === 'error') {
|
||||||
@ -1021,7 +1046,11 @@ export default defineComponent({
|
|||||||
关闭
|
关闭
|
||||||
</AButton>,
|
</AButton>,
|
||||||
<AButton
|
<AButton
|
||||||
disabled={!scanState.sessionId}
|
disabled={
|
||||||
|
!scanState.sessionId ||
|
||||||
|
!!scanState.captchaUrl ||
|
||||||
|
!!scanState.newDeviceStatus
|
||||||
|
}
|
||||||
key="refresh"
|
key="refresh"
|
||||||
loading={scanLoading.value}
|
loading={scanLoading.value}
|
||||||
onClick={refreshScanQrcode}
|
onClick={refreshScanQrcode}
|
||||||
@ -1057,7 +1086,32 @@ export default defineComponent({
|
|||||||
type="info"
|
type="info"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{scanState.captchaUrl ? (
|
{scanState.newDeviceStatus ? (
|
||||||
|
<Alert
|
||||||
|
description={
|
||||||
|
<Space direction="vertical">
|
||||||
|
<ATypographyText>
|
||||||
|
请使用手机 QQ
|
||||||
|
扫描下方新设备验证二维码,并在手机端确认登录。
|
||||||
|
</ATypographyText>
|
||||||
|
{scanState.deviceVerifyUrl ? (
|
||||||
|
<ATypographyLink
|
||||||
|
href={scanState.deviceVerifyUrl}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
打开新设备验证链接
|
||||||
|
</ATypographyLink>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
message={getNapcatNewDeviceStatusMessage(
|
||||||
|
scanState.newDeviceStatus,
|
||||||
|
)}
|
||||||
|
showIcon
|
||||||
|
type={getNewDeviceAlertType(scanState.newDeviceStatus)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{scanState.captchaUrl && !scanState.newDeviceStatus ? (
|
||||||
<Alert
|
<Alert
|
||||||
description={
|
description={
|
||||||
<Space>
|
<Space>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user