diff --git a/apps/web-antdv-next/src/api/qqbot/napcat.spec.ts b/apps/web-antdv-next/src/api/qqbot/napcat.spec.ts index 1cf3384..63166a9 100644 --- a/apps/web-antdv-next/src/api/qqbot/napcat.spec.ts +++ b/apps/web-antdv-next/src/api/qqbot/napcat.spec.ts @@ -4,9 +4,11 @@ import { requestClient } from '#/api/request'; import { cancelQqbotAccountScan, + getNapcatLoginProgressLabel, getNapcatNewDeviceStatusMessage, getQqbotAccountScanEventsUrl, getQqbotAccountScanStatus, + mergeNapcatAccountScanResult, NAPCAT_LOGIN_PROGRESS_LABELS, refreshQqbotAccountScanQrcode, resolveNapcatLoginDisplayQrcode, @@ -63,6 +65,78 @@ describe('napcat login display helpers', () => { ); }); + it('preserves pending captcha and new-device evidence when later polls omit URLs', () => { + expect( + mergeNapcatAccountScanResult( + { + captchaUrl: 'https://captcha.qq.com/proof', + mode: 'refresh', + sessionId: 'scan-1', + status: 'pending', + }, + { + mode: 'refresh', + sessionId: 'scan-1', + status: 'pending', + }, + ), + ).toMatchObject({ + captchaUrl: 'https://captcha.qq.com/proof', + status: 'pending', + }); + + expect( + mergeNapcatAccountScanResult( + { + deviceVerifyUrl: 'https://ti.qq.com/device', + mode: 'refresh', + newDeviceQrcode: 'data:image/png;base64,new-device', + newDeviceStatus: 'qr-pending', + sessionId: 'scan-1', + status: 'pending', + }, + { + mode: 'refresh', + newDeviceStatus: 'scanned', + sessionId: 'scan-1', + status: 'pending', + }, + ), + ).toMatchObject({ + deviceVerifyUrl: 'https://ti.qq.com/device', + newDeviceQrcode: 'data:image/png;base64,new-device', + newDeviceStatus: 'scanned', + status: 'pending', + }); + }); + + it('clears stale verification evidence when scan reaches a terminal state', () => { + expect( + mergeNapcatAccountScanResult( + { + captchaUrl: 'https://captcha.qq.com/proof', + deviceVerifyUrl: 'https://ti.qq.com/device', + mode: 'refresh', + newDeviceQrcode: 'data:image/png;base64,new-device', + newDeviceStatus: 'confirming', + sessionId: 'scan-1', + status: 'pending', + }, + { + mode: 'refresh', + sessionId: 'scan-1', + status: 'success', + }, + ), + ).toMatchObject({ + captchaUrl: undefined, + deviceVerifyUrl: undefined, + newDeviceQrcode: undefined, + newDeviceStatus: undefined, + status: 'success', + }); + }); + it('keeps the complete new-device progress key set in caller helpers', () => { expect( Object.keys(NAPCAT_LOGIN_PROGRESS_LABELS) @@ -77,6 +151,21 @@ describe('napcat login display helpers', () => { ]); }); + it('falls back to Chinese progress labels by SSE step key', () => { + expect( + getNapcatLoginProgressLabel({ + message: 'Need new device', + step: 'new-device-required', + }), + ).toBe('需要新设备验证二维码'); + expect( + getNapcatLoginProgressLabel({ + message: '', + step: 'new-device-confirming', + }), + ).toBe('新设备确认中'); + }); + it('owns create, refresh, status, QR refresh, captcha, cancel, and SSE caller routes', async () => { const scanResult = { mode: 'refresh' as const, diff --git a/apps/web-antdv-next/src/api/qqbot/napcat.ts b/apps/web-antdv-next/src/api/qqbot/napcat.ts index d2b553b..3a6139f 100644 --- a/apps/web-antdv-next/src/api/qqbot/napcat.ts +++ b/apps/web-antdv-next/src/api/qqbot/napcat.ts @@ -49,6 +49,21 @@ export type NapcatLoginDisplayQrcodeSource = { qrcode?: string; }; +export type NapcatLoginScanStateSnapshot = Partial< + Pick< + QqbotNapcatApi.AccountScanResult, + | 'captchaUrl' + | 'deviceVerifyUrl' + | 'newDeviceQrcode' + | 'newDeviceStatus' + | 'qrcode' + > +> & { + mode: QqbotNapcatApi.AccountScanResult['mode']; + sessionId?: string; + status: 'idle' | QqbotNapcatApi.AccountScanResult['status']; +}; + export const NAPCAT_LOGIN_PROGRESS_LABELS = { 'captcha-submit': '验证码已提交,等待确认', 'login-failed': '登录失败', @@ -75,6 +90,43 @@ export function resolveNapcatLoginDisplayQrcode( return source.newDeviceQrcode || source.qrcode || ''; } +export function mergeNapcatAccountScanResult( + current: NapcatLoginScanStateSnapshot, + result: QqbotNapcatApi.AccountScanResult, +): QqbotNapcatApi.AccountScanResult { + if (result.status !== 'pending') { + return { + ...result, + captchaUrl: result.captchaUrl, + deviceVerifyUrl: result.deviceVerifyUrl, + newDeviceQrcode: result.newDeviceQrcode, + newDeviceStatus: result.newDeviceStatus, + }; + } + + const hasCaptcha = !!result.captchaUrl; + const hasNewDevice = + !!result.newDeviceQrcode || + !!result.newDeviceStatus || + !!result.deviceVerifyUrl; + + return { + ...result, + captchaUrl: hasNewDevice + ? undefined + : result.captchaUrl || current.captchaUrl, + deviceVerifyUrl: hasCaptcha + ? undefined + : result.deviceVerifyUrl || current.deviceVerifyUrl, + newDeviceQrcode: hasCaptcha + ? undefined + : result.newDeviceQrcode || current.newDeviceQrcode, + newDeviceStatus: hasCaptcha + ? undefined + : result.newDeviceStatus || current.newDeviceStatus, + }; +} + export function getNapcatNewDeviceStatusMessage( status?: NapcatLoginNewDeviceStatus, ) { @@ -86,6 +138,16 @@ export function getNapcatNewDeviceStatusMessage( return '新设备二维码待扫码'; } +export function getNapcatLoginProgressLabel( + event: Pick, +) { + const label = + NAPCAT_LOGIN_PROGRESS_LABELS[ + event.step as keyof typeof NAPCAT_LOGIN_PROGRESS_LABELS + ]; + return label || event.message || event.step || '登录处理中'; +} + export function startQqbotAccountScanCreate() { return requestClient.post( '/qqbot/account/scan/create', diff --git a/apps/web-antdv-next/src/api/qqbot/plugin.spec.ts b/apps/web-antdv-next/src/api/qqbot/plugin.spec.ts index 92091a7..13d5186 100644 --- a/apps/web-antdv-next/src/api/qqbot/plugin.spec.ts +++ b/apps/web-antdv-next/src/api/qqbot/plugin.spec.ts @@ -59,7 +59,10 @@ describe('qqbot plugin API wrappers', () => { it('owns plugin-platform management caller routes', async () => { const manifest = { capabilities: [], key: 'demo' }; - const packageBody = { manifest, packagePath: '/tmp/demo.zip' }; + const packageBody = { + packageHash: 'sha256-demo', + packagePath: '.kt-workspace/qqbot-plugin-packages/demo.qqbot-plugin.json', + }; vi.mocked(requestClient.get).mockResolvedValue([]); vi.mocked(requestClient.post).mockResolvedValue({}); diff --git a/apps/web-antdv-next/src/api/qqbot/plugin.ts b/apps/web-antdv-next/src/api/qqbot/plugin.ts index 821992d..601a90c 100644 --- a/apps/web-antdv-next/src/api/qqbot/plugin.ts +++ b/apps/web-antdv-next/src/api/qqbot/plugin.ts @@ -26,6 +26,12 @@ export namespace QqbotPluginPlatformApi { valid: boolean; } + export interface PackageValidationResult extends ManifestValidationResult { + packageHash: string; + packagePath: string; + packageSizeBytes?: number; + } + export interface Installation { createTime?: string; id: string; @@ -61,10 +67,13 @@ export namespace QqbotPluginPlatformApi { value?: any; } - export interface PackageBody { + export interface ManifestBody { manifest: Recordable; + } + + export interface PackageBody { packageHash?: string; - packagePath?: string; + packagePath: string; } } @@ -131,14 +140,14 @@ export function getQqbotPluginPlatformInstallations() { export function uploadQqbotPluginPackage( data: QqbotPluginPlatformApi.PackageBody, ) { - return requestClient.post( + return requestClient.post( '/qqbot/plugin-platform/upload', data, ); } export function validateQqbotPluginManifest( - manifest: QqbotPluginPlatformApi.PackageBody['manifest'], + manifest: QqbotPluginPlatformApi.ManifestBody['manifest'], ) { return requestClient.post( '/qqbot/plugin-platform/validate', diff --git a/apps/web-antdv-next/src/router/access-codes.spec.ts b/apps/web-antdv-next/src/router/access-codes.spec.ts new file mode 100644 index 0000000..6b7fa60 --- /dev/null +++ b/apps/web-antdv-next/src/router/access-codes.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { refreshAccessCodes } from './access-codes'; + +describe('router access code refresh', () => { + it('overwrites persisted access codes with the latest backend codes', async () => { + const setAccessCodes = vi.fn(); + const loadAccessCodes = vi + .fn() + .mockResolvedValue(['QqBot:Command:Test', 'QqBot:Account:RefreshLogin']); + + await expect( + refreshAccessCodes({ + loadAccessCodes, + setAccessCodes, + }), + ).resolves.toEqual(['QqBot:Command:Test', 'QqBot:Account:RefreshLogin']); + + expect(loadAccessCodes).toHaveBeenCalledOnce(); + expect(setAccessCodes).toHaveBeenCalledWith([ + 'QqBot:Command:Test', + 'QqBot:Account:RefreshLogin', + ]); + }); +}); diff --git a/apps/web-antdv-next/src/router/access-codes.ts b/apps/web-antdv-next/src/router/access-codes.ts new file mode 100644 index 0000000..d60a9c8 --- /dev/null +++ b/apps/web-antdv-next/src/router/access-codes.ts @@ -0,0 +1,13 @@ +interface RefreshAccessCodesOptions { + loadAccessCodes: () => Promise; + setAccessCodes: (codes: string[]) => void; +} + +export async function refreshAccessCodes({ + loadAccessCodes, + setAccessCodes, +}: RefreshAccessCodesOptions) { + const accessCodes = await loadAccessCodes(); + setAccessCodes(accessCodes); + return accessCodes; +} diff --git a/apps/web-antdv-next/src/router/guard.ts b/apps/web-antdv-next/src/router/guard.ts index 3ac15f2..dbd8ce9 100644 --- a/apps/web-antdv-next/src/router/guard.ts +++ b/apps/web-antdv-next/src/router/guard.ts @@ -5,10 +5,12 @@ import { preferences } from '@vben/preferences'; import { useAccessStore, useUserStore } from '@vben/stores'; import { startProgress, stopProgress } from '@vben/utils'; +import { getAccessCodesApi } from '#/api'; import { accessRoutes, coreRouteNames } from '#/router/routes'; import { useAuthStore } from '#/store'; import { generateAccess } from './access'; +import { refreshAccessCodes } from './access-codes'; function decodeRedirect(redirect?: string) { if (!redirect) return null; @@ -131,7 +133,13 @@ function setupAccessGuard(router: Router) { // 生成路由表 // 当前登录用户拥有的角色标识列表 - const userInfo = userStore.userInfo || (await authStore.fetchUserInfo()); + const [userInfo] = await Promise.all([ + userStore.userInfo || authStore.fetchUserInfo(), + refreshAccessCodes({ + loadAccessCodes: getAccessCodesApi, + setAccessCodes: (codes) => accessStore.setAccessCodes(codes), + }), + ]); const userRoles = userInfo.roles ?? []; // 生成菜单和路由 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 73d0e8f..c718102 100644 --- a/apps/web-antdv-next/src/views/qqbot/account/list.tsx +++ b/apps/web-antdv-next/src/views/qqbot/account/list.tsx @@ -38,9 +38,11 @@ import { } from '#/api/qqbot'; import { cancelQqbotAccountScan, + getNapcatLoginProgressLabel, getNapcatNewDeviceStatusMessage, getQqbotAccountScanEventsUrl, getQqbotAccountScanStatus, + mergeNapcatAccountScanResult, refreshQqbotAccountScanQrcode, resolveNapcatLoginDisplayQrcode, startQqbotAccountScanCreate, @@ -134,7 +136,7 @@ export default defineComponent({ scanEvents.value.map((event) => ({ description: formatEventTime(event.createdAt), status: getScanStepStatus(event.status), - title: event.message, + title: getNapcatLoginProgressLabel(event), })), ); const scanProgressCurrent = computed(() => @@ -440,20 +442,21 @@ export default defineComponent({ result: QqbotNapcatApi.AccountScanResult, options: { reloadQrcode?: boolean } = {}, ) { - 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 = resolveNapcatLoginDisplayQrcode(result); + const nextState = mergeNapcatAccountScanResult(scanState, result); + scanState.captchaUrl = nextState.captchaUrl; + scanState.containerId = nextState.containerId; + scanState.containerName = nextState.containerName; + scanState.deviceVerifyUrl = nextState.deviceVerifyUrl; + scanState.errorMessage = nextState.errorMessage; + scanState.expiresAt = nextState.expiresAt; + scanState.mode = nextState.mode; + scanState.newDeviceQrcode = nextState.newDeviceQrcode; + scanState.newDeviceStatus = nextState.newDeviceStatus; + scanState.selfId = nextState.selfId; + scanState.sessionId = nextState.sessionId; + scanState.status = nextState.status; + scanState.webuiPort = nextState.webuiPort; + const nextQrcode = resolveNapcatLoginDisplayQrcode(nextState); const qrcodeChanged = nextQrcode !== scanQrcodeText.value; if (qrcodeChanged) { scanQrcodeImageFailed.value = false; diff --git a/apps/web-antdv-next/src/views/qqbot/plugin/components/PluginManifestModal.tsx b/apps/web-antdv-next/src/views/qqbot/plugin/components/PluginManifestModal.tsx index d8b3937..366cc75 100644 --- a/apps/web-antdv-next/src/views/qqbot/plugin/components/PluginManifestModal.tsx +++ b/apps/web-antdv-next/src/views/qqbot/plugin/components/PluginManifestModal.tsx @@ -1,8 +1,11 @@ +import type { PropType } from 'vue'; + import { defineComponent } from 'vue'; -import { Modal } from 'antdv-next'; +import { Input, Modal } from 'antdv-next'; const AModal = Modal as any; +const AInput = Input as any; export default defineComponent({ name: 'QqBotPluginManifestModal', @@ -15,6 +18,18 @@ export default defineComponent({ default: false, type: Boolean, }, + mode: { + default: 'validate', + type: String as PropType<'install' | 'upload' | 'validate'>, + }, + packageHash: { + default: '', + type: String, + }, + packagePath: { + default: '', + type: String, + }, title: { default: '', type: String, @@ -24,7 +39,13 @@ export default defineComponent({ type: String, }, }, - emits: ['close', 'submit', 'update:value'], + emits: [ + 'close', + 'submit', + 'update:packageHash', + 'update:packagePath', + 'update:value', + ], setup(props, { emit }) { return () => ( -