fix: 对齐QQBot页面权限与插件元数据
This commit is contained in:
parent
b549e30dd5
commit
788aba00a8
@ -4,9 +4,11 @@ import { requestClient } from '#/api/request';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
cancelQqbotAccountScan,
|
cancelQqbotAccountScan,
|
||||||
|
getNapcatLoginProgressLabel,
|
||||||
getNapcatNewDeviceStatusMessage,
|
getNapcatNewDeviceStatusMessage,
|
||||||
getQqbotAccountScanEventsUrl,
|
getQqbotAccountScanEventsUrl,
|
||||||
getQqbotAccountScanStatus,
|
getQqbotAccountScanStatus,
|
||||||
|
mergeNapcatAccountScanResult,
|
||||||
NAPCAT_LOGIN_PROGRESS_LABELS,
|
NAPCAT_LOGIN_PROGRESS_LABELS,
|
||||||
refreshQqbotAccountScanQrcode,
|
refreshQqbotAccountScanQrcode,
|
||||||
resolveNapcatLoginDisplayQrcode,
|
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', () => {
|
it('keeps the complete new-device progress key set in caller helpers', () => {
|
||||||
expect(
|
expect(
|
||||||
Object.keys(NAPCAT_LOGIN_PROGRESS_LABELS)
|
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 () => {
|
it('owns create, refresh, status, QR refresh, captcha, cancel, and SSE caller routes', async () => {
|
||||||
const scanResult = {
|
const scanResult = {
|
||||||
mode: 'refresh' as const,
|
mode: 'refresh' as const,
|
||||||
|
|||||||
@ -49,6 +49,21 @@ export type NapcatLoginDisplayQrcodeSource = {
|
|||||||
qrcode?: string;
|
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 = {
|
export const NAPCAT_LOGIN_PROGRESS_LABELS = {
|
||||||
'captcha-submit': '验证码已提交,等待确认',
|
'captcha-submit': '验证码已提交,等待确认',
|
||||||
'login-failed': '登录失败',
|
'login-failed': '登录失败',
|
||||||
@ -75,6 +90,43 @@ export function resolveNapcatLoginDisplayQrcode(
|
|||||||
return source.newDeviceQrcode || source.qrcode || '';
|
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(
|
export function getNapcatNewDeviceStatusMessage(
|
||||||
status?: NapcatLoginNewDeviceStatus,
|
status?: NapcatLoginNewDeviceStatus,
|
||||||
) {
|
) {
|
||||||
@ -86,6 +138,16 @@ export function getNapcatNewDeviceStatusMessage(
|
|||||||
return '新设备二维码待扫码';
|
return '新设备二维码待扫码';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNapcatLoginProgressLabel(
|
||||||
|
event: Pick<QqbotNapcatApi.AccountScanEvent, 'message' | 'step'>,
|
||||||
|
) {
|
||||||
|
const label =
|
||||||
|
NAPCAT_LOGIN_PROGRESS_LABELS[
|
||||||
|
event.step as keyof typeof NAPCAT_LOGIN_PROGRESS_LABELS
|
||||||
|
];
|
||||||
|
return label || event.message || event.step || '登录处理中';
|
||||||
|
}
|
||||||
|
|
||||||
export function startQqbotAccountScanCreate() {
|
export function startQqbotAccountScanCreate() {
|
||||||
return requestClient.post<QqbotNapcatApi.AccountScanResult>(
|
return requestClient.post<QqbotNapcatApi.AccountScanResult>(
|
||||||
'/qqbot/account/scan/create',
|
'/qqbot/account/scan/create',
|
||||||
|
|||||||
@ -59,7 +59,10 @@ describe('qqbot plugin API wrappers', () => {
|
|||||||
|
|
||||||
it('owns plugin-platform management caller routes', async () => {
|
it('owns plugin-platform management caller routes', async () => {
|
||||||
const manifest = { capabilities: [], key: 'demo' };
|
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.get).mockResolvedValue([]);
|
||||||
vi.mocked(requestClient.post).mockResolvedValue({});
|
vi.mocked(requestClient.post).mockResolvedValue({});
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,12 @@ export namespace QqbotPluginPlatformApi {
|
|||||||
valid: boolean;
|
valid: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PackageValidationResult extends ManifestValidationResult {
|
||||||
|
packageHash: string;
|
||||||
|
packagePath: string;
|
||||||
|
packageSizeBytes?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Installation {
|
export interface Installation {
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
id: string;
|
id: string;
|
||||||
@ -61,10 +67,13 @@ export namespace QqbotPluginPlatformApi {
|
|||||||
value?: any;
|
value?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PackageBody {
|
export interface ManifestBody {
|
||||||
manifest: Recordable<any>;
|
manifest: Recordable<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PackageBody {
|
||||||
packageHash?: string;
|
packageHash?: string;
|
||||||
packagePath?: string;
|
packagePath: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,14 +140,14 @@ export function getQqbotPluginPlatformInstallations() {
|
|||||||
export function uploadQqbotPluginPackage(
|
export function uploadQqbotPluginPackage(
|
||||||
data: QqbotPluginPlatformApi.PackageBody,
|
data: QqbotPluginPlatformApi.PackageBody,
|
||||||
) {
|
) {
|
||||||
return requestClient.post<QqbotPluginPlatformApi.ManifestValidationResult>(
|
return requestClient.post<QqbotPluginPlatformApi.PackageValidationResult>(
|
||||||
'/qqbot/plugin-platform/upload',
|
'/qqbot/plugin-platform/upload',
|
||||||
data,
|
data,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateQqbotPluginManifest(
|
export function validateQqbotPluginManifest(
|
||||||
manifest: QqbotPluginPlatformApi.PackageBody['manifest'],
|
manifest: QqbotPluginPlatformApi.ManifestBody['manifest'],
|
||||||
) {
|
) {
|
||||||
return requestClient.post<QqbotPluginPlatformApi.ManifestValidationResult>(
|
return requestClient.post<QqbotPluginPlatformApi.ManifestValidationResult>(
|
||||||
'/qqbot/plugin-platform/validate',
|
'/qqbot/plugin-platform/validate',
|
||||||
|
|||||||
25
apps/web-antdv-next/src/router/access-codes.spec.ts
Normal file
25
apps/web-antdv-next/src/router/access-codes.spec.ts
Normal file
@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
13
apps/web-antdv-next/src/router/access-codes.ts
Normal file
13
apps/web-antdv-next/src/router/access-codes.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
interface RefreshAccessCodesOptions {
|
||||||
|
loadAccessCodes: () => Promise<string[]>;
|
||||||
|
setAccessCodes: (codes: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshAccessCodes({
|
||||||
|
loadAccessCodes,
|
||||||
|
setAccessCodes,
|
||||||
|
}: RefreshAccessCodesOptions) {
|
||||||
|
const accessCodes = await loadAccessCodes();
|
||||||
|
setAccessCodes(accessCodes);
|
||||||
|
return accessCodes;
|
||||||
|
}
|
||||||
@ -5,10 +5,12 @@ import { preferences } from '@vben/preferences';
|
|||||||
import { useAccessStore, useUserStore } from '@vben/stores';
|
import { useAccessStore, useUserStore } from '@vben/stores';
|
||||||
import { startProgress, stopProgress } from '@vben/utils';
|
import { startProgress, stopProgress } from '@vben/utils';
|
||||||
|
|
||||||
|
import { getAccessCodesApi } from '#/api';
|
||||||
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
import { accessRoutes, coreRouteNames } from '#/router/routes';
|
||||||
import { useAuthStore } from '#/store';
|
import { useAuthStore } from '#/store';
|
||||||
|
|
||||||
import { generateAccess } from './access';
|
import { generateAccess } from './access';
|
||||||
|
import { refreshAccessCodes } from './access-codes';
|
||||||
|
|
||||||
function decodeRedirect(redirect?: string) {
|
function decodeRedirect(redirect?: string) {
|
||||||
if (!redirect) return null;
|
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 ?? [];
|
const userRoles = userInfo.roles ?? [];
|
||||||
|
|
||||||
// 生成菜单和路由
|
// 生成菜单和路由
|
||||||
|
|||||||
@ -38,9 +38,11 @@ import {
|
|||||||
} from '#/api/qqbot';
|
} from '#/api/qqbot';
|
||||||
import {
|
import {
|
||||||
cancelQqbotAccountScan,
|
cancelQqbotAccountScan,
|
||||||
|
getNapcatLoginProgressLabel,
|
||||||
getNapcatNewDeviceStatusMessage,
|
getNapcatNewDeviceStatusMessage,
|
||||||
getQqbotAccountScanEventsUrl,
|
getQqbotAccountScanEventsUrl,
|
||||||
getQqbotAccountScanStatus,
|
getQqbotAccountScanStatus,
|
||||||
|
mergeNapcatAccountScanResult,
|
||||||
refreshQqbotAccountScanQrcode,
|
refreshQqbotAccountScanQrcode,
|
||||||
resolveNapcatLoginDisplayQrcode,
|
resolveNapcatLoginDisplayQrcode,
|
||||||
startQqbotAccountScanCreate,
|
startQqbotAccountScanCreate,
|
||||||
@ -134,7 +136,7 @@ export default defineComponent({
|
|||||||
scanEvents.value.map((event) => ({
|
scanEvents.value.map((event) => ({
|
||||||
description: formatEventTime(event.createdAt),
|
description: formatEventTime(event.createdAt),
|
||||||
status: getScanStepStatus(event.status),
|
status: getScanStepStatus(event.status),
|
||||||
title: event.message,
|
title: getNapcatLoginProgressLabel(event),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
const scanProgressCurrent = computed(() =>
|
const scanProgressCurrent = computed(() =>
|
||||||
@ -440,20 +442,21 @@ export default defineComponent({
|
|||||||
result: QqbotNapcatApi.AccountScanResult,
|
result: QqbotNapcatApi.AccountScanResult,
|
||||||
options: { reloadQrcode?: boolean } = {},
|
options: { reloadQrcode?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
scanState.captchaUrl = result.captchaUrl;
|
const nextState = mergeNapcatAccountScanResult(scanState, result);
|
||||||
scanState.containerId = result.containerId;
|
scanState.captchaUrl = nextState.captchaUrl;
|
||||||
scanState.containerName = result.containerName;
|
scanState.containerId = nextState.containerId;
|
||||||
scanState.deviceVerifyUrl = result.deviceVerifyUrl;
|
scanState.containerName = nextState.containerName;
|
||||||
scanState.errorMessage = result.errorMessage;
|
scanState.deviceVerifyUrl = nextState.deviceVerifyUrl;
|
||||||
scanState.expiresAt = result.expiresAt;
|
scanState.errorMessage = nextState.errorMessage;
|
||||||
scanState.mode = result.mode;
|
scanState.expiresAt = nextState.expiresAt;
|
||||||
scanState.newDeviceQrcode = result.newDeviceQrcode;
|
scanState.mode = nextState.mode;
|
||||||
scanState.newDeviceStatus = result.newDeviceStatus;
|
scanState.newDeviceQrcode = nextState.newDeviceQrcode;
|
||||||
scanState.selfId = result.selfId;
|
scanState.newDeviceStatus = nextState.newDeviceStatus;
|
||||||
scanState.sessionId = result.sessionId;
|
scanState.selfId = nextState.selfId;
|
||||||
scanState.status = result.status;
|
scanState.sessionId = nextState.sessionId;
|
||||||
scanState.webuiPort = result.webuiPort;
|
scanState.status = nextState.status;
|
||||||
const nextQrcode = resolveNapcatLoginDisplayQrcode(result);
|
scanState.webuiPort = nextState.webuiPort;
|
||||||
|
const nextQrcode = resolveNapcatLoginDisplayQrcode(nextState);
|
||||||
const qrcodeChanged = nextQrcode !== scanQrcodeText.value;
|
const qrcodeChanged = nextQrcode !== scanQrcodeText.value;
|
||||||
if (qrcodeChanged) {
|
if (qrcodeChanged) {
|
||||||
scanQrcodeImageFailed.value = false;
|
scanQrcodeImageFailed.value = false;
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
import { Modal } from 'antdv-next';
|
import { Input, Modal } from 'antdv-next';
|
||||||
|
|
||||||
const AModal = Modal as any;
|
const AModal = Modal as any;
|
||||||
|
const AInput = Input as any;
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'QqBotPluginManifestModal',
|
name: 'QqBotPluginManifestModal',
|
||||||
@ -15,6 +18,18 @@ export default defineComponent({
|
|||||||
default: false,
|
default: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
mode: {
|
||||||
|
default: 'validate',
|
||||||
|
type: String as PropType<'install' | 'upload' | 'validate'>,
|
||||||
|
},
|
||||||
|
packageHash: {
|
||||||
|
default: '',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
packagePath: {
|
||||||
|
default: '',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
default: '',
|
default: '',
|
||||||
type: String,
|
type: String,
|
||||||
@ -24,7 +39,13 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
emits: ['close', 'submit', 'update:value'],
|
emits: [
|
||||||
|
'close',
|
||||||
|
'submit',
|
||||||
|
'update:packageHash',
|
||||||
|
'update:packagePath',
|
||||||
|
'update:value',
|
||||||
|
],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
return () => (
|
return () => (
|
||||||
<AModal
|
<AModal
|
||||||
@ -35,6 +56,7 @@ export default defineComponent({
|
|||||||
title={props.title}
|
title={props.title}
|
||||||
width={760}
|
width={760}
|
||||||
>
|
>
|
||||||
|
{props.mode === 'validate' ? (
|
||||||
<textarea
|
<textarea
|
||||||
class="w-full resize-y rounded border border-solid border-gray-200 p-3 font-mono text-sm outline-none"
|
class="w-full resize-y rounded border border-solid border-gray-200 p-3 font-mono text-sm outline-none"
|
||||||
onInput={(event) => {
|
onInput={(event) => {
|
||||||
@ -43,6 +65,30 @@ export default defineComponent({
|
|||||||
rows={18}
|
rows={18}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="block">
|
||||||
|
<span class="mb-1 block text-sm text-gray-600">插件包路径</span>
|
||||||
|
<AInput
|
||||||
|
onUpdate:value={(value: string) => {
|
||||||
|
emit('update:packagePath', value);
|
||||||
|
}}
|
||||||
|
placeholder=".kt-workspace/qqbot-plugin-packages/demo.qqbot-plugin.json"
|
||||||
|
value={props.packagePath}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="block">
|
||||||
|
<span class="mb-1 block text-sm text-gray-600">包 Hash</span>
|
||||||
|
<AInput
|
||||||
|
onUpdate:value={(value: string) => {
|
||||||
|
emit('update:packageHash', value);
|
||||||
|
}}
|
||||||
|
placeholder="上传校验可留空,安装时用于校验包内容"
|
||||||
|
value={props.packageHash}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</AModal>
|
</AModal>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import { useDict } from '#/hooks/useDict';
|
|||||||
|
|
||||||
import PluginManifestModal from './components/PluginManifestModal';
|
import PluginManifestModal from './components/PluginManifestModal';
|
||||||
import PluginPlatformStateDrawer from './components/PluginPlatformStateDrawer';
|
import PluginPlatformStateDrawer from './components/PluginPlatformStateDrawer';
|
||||||
|
import { loadQqbotPluginMetadata } from './metadata';
|
||||||
|
|
||||||
const AKtTable = KtTable as any;
|
const AKtTable = KtTable as any;
|
||||||
const QQBOT_PLUGIN_TRIGGER_MODE_DICT = 'QQBOT_PLUGIN_TRIGGER_MODE';
|
const QQBOT_PLUGIN_TRIGGER_MODE_DICT = 'QQBOT_PLUGIN_TRIGGER_MODE';
|
||||||
@ -78,6 +79,8 @@ export default defineComponent({
|
|||||||
const manifestMode = ref<'install' | 'upload' | 'validate'>('validate');
|
const manifestMode = ref<'install' | 'upload' | 'validate'>('validate');
|
||||||
const manifestModalOpen = ref(false);
|
const manifestModalOpen = ref(false);
|
||||||
const manifestText = ref(JSON.stringify(defaultManifest, null, 2));
|
const manifestText = ref(JSON.stringify(defaultManifest, null, 2));
|
||||||
|
const packageHashText = ref('');
|
||||||
|
const packagePathText = ref('');
|
||||||
const pluginOptions = ref<Array<{ label: string; value: string }>>([]);
|
const pluginOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||||
const pluginMap = ref<Record<string, QqbotApi.Plugin>>({});
|
const pluginMap = ref<Record<string, QqbotApi.Plugin>>({});
|
||||||
const platformLoading = ref(false);
|
const platformLoading = ref(false);
|
||||||
@ -88,8 +91,8 @@ export default defineComponent({
|
|||||||
return '插件安装记录';
|
return '插件安装记录';
|
||||||
});
|
});
|
||||||
const manifestModalTitle = computed(() => {
|
const manifestModalTitle = computed(() => {
|
||||||
if (manifestMode.value === 'install') return '安装插件 Manifest';
|
if (manifestMode.value === 'install') return '本地安装插件包';
|
||||||
if (manifestMode.value === 'upload') return '上传插件 Manifest';
|
if (manifestMode.value === 'upload') return '上传插件包';
|
||||||
return '校验插件 Manifest';
|
return '校验插件 Manifest';
|
||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
@ -208,24 +211,20 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function loadMetadata() {
|
async function loadMetadata() {
|
||||||
const [plugins] = await Promise.all([
|
const metadata = await loadQqbotPluginMetadata({
|
||||||
getQqbotPluginList(),
|
labelOf: getTriggerModeLabel,
|
||||||
reloadTriggerModeDict(),
|
loadPlugins: () => getQqbotPluginList(),
|
||||||
]);
|
reloadTriggerModes: () => reloadTriggerModeDict(),
|
||||||
const nextPluginMap: Record<string, QqbotApi.Plugin> = {};
|
});
|
||||||
for (const item of plugins) {
|
pluginMap.value = metadata.pluginMap;
|
||||||
nextPluginMap[item.key] = item;
|
pluginOptions.value = metadata.pluginOptions;
|
||||||
}
|
|
||||||
pluginMap.value = nextPluginMap;
|
|
||||||
pluginOptions.value = plugins.map((item) => ({
|
|
||||||
label: `${item.name} (${item.key} / ${getTriggerModeLabel(item.triggerMode, '-')})`,
|
|
||||||
value: item.key,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function openManifestModal(mode: typeof manifestMode.value) {
|
function openManifestModal(mode: typeof manifestMode.value) {
|
||||||
manifestMode.value = mode;
|
manifestMode.value = mode;
|
||||||
manifestText.value = JSON.stringify(defaultManifest, null, 2);
|
manifestText.value = JSON.stringify(defaultManifest, null, 2);
|
||||||
|
packageHashText.value = '';
|
||||||
|
packagePathText.value = '';
|
||||||
manifestModalOpen.value = true;
|
manifestModalOpen.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,19 +238,26 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submitManifest() {
|
async function submitManifest() {
|
||||||
const manifest = parseManifestText();
|
|
||||||
if (!manifest) return;
|
|
||||||
|
|
||||||
platformLoading.value = true;
|
platformLoading.value = true;
|
||||||
try {
|
try {
|
||||||
if (manifestMode.value === 'upload') {
|
if (manifestMode.value === 'upload') {
|
||||||
await uploadQqbotPluginPackage({ manifest });
|
const body = parsePackageBody();
|
||||||
message.success('插件包上传校验通过');
|
if (!body) return;
|
||||||
|
const result = await uploadQqbotPluginPackage(body);
|
||||||
|
message.success(
|
||||||
|
result.packageHash
|
||||||
|
? `插件包上传校验通过:${result.packageHash.slice(0, 12)}`
|
||||||
|
: '插件包上传校验通过',
|
||||||
|
);
|
||||||
} else if (manifestMode.value === 'install') {
|
} else if (manifestMode.value === 'install') {
|
||||||
await installLocalQqbotPluginPackage({ manifest });
|
const body = parsePackageBody();
|
||||||
|
if (!body) return;
|
||||||
|
await installLocalQqbotPluginPackage(body);
|
||||||
message.success('插件已安装');
|
message.success('插件已安装');
|
||||||
await loadInstallations(false);
|
await loadInstallations(false);
|
||||||
} else {
|
} else {
|
||||||
|
const manifest = parseManifestText();
|
||||||
|
if (!manifest) return;
|
||||||
await validateQqbotPluginManifest(manifest);
|
await validateQqbotPluginManifest(manifest);
|
||||||
message.success('Manifest 校验通过');
|
message.success('Manifest 校验通过');
|
||||||
}
|
}
|
||||||
@ -261,6 +267,21 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parsePackageBody():
|
||||||
|
| QqbotPluginPlatformApi.PackageBody
|
||||||
|
| undefined {
|
||||||
|
const packagePath = packagePathText.value.trim();
|
||||||
|
const packageHash = packageHashText.value.trim();
|
||||||
|
if (!packagePath) {
|
||||||
|
message.error('请输入受控插件包路径');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...(packageHash ? { packageHash } : {}),
|
||||||
|
packagePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function loadInstallations(openDrawer = true) {
|
async function loadInstallations(openDrawer = true) {
|
||||||
installations.value = await getQqbotPluginPlatformInstallations();
|
installations.value = await getQqbotPluginPlatformInstallations();
|
||||||
drawerMode.value = 'installations';
|
drawerMode.value = 'installations';
|
||||||
@ -329,14 +350,23 @@ export default defineComponent({
|
|||||||
/>
|
/>
|
||||||
<PluginManifestModal
|
<PluginManifestModal
|
||||||
loading={platformLoading.value}
|
loading={platformLoading.value}
|
||||||
|
mode={manifestMode.value}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
manifestModalOpen.value = false;
|
manifestModalOpen.value = false;
|
||||||
}}
|
}}
|
||||||
onSubmit={() => void submitManifest()}
|
onSubmit={() => void submitManifest()}
|
||||||
|
onUpdate:packageHash={(value: string) => {
|
||||||
|
packageHashText.value = value;
|
||||||
|
}}
|
||||||
|
onUpdate:packagePath={(value: string) => {
|
||||||
|
packagePathText.value = value;
|
||||||
|
}}
|
||||||
onUpdate:value={(value: string) => {
|
onUpdate:value={(value: string) => {
|
||||||
manifestText.value = value;
|
manifestText.value = value;
|
||||||
}}
|
}}
|
||||||
open={manifestModalOpen.value}
|
open={manifestModalOpen.value}
|
||||||
|
packageHash={packageHashText.value}
|
||||||
|
packagePath={packagePathText.value}
|
||||||
title={manifestModalTitle.value}
|
title={manifestModalTitle.value}
|
||||||
value={manifestText.value}
|
value={manifestText.value}
|
||||||
/>
|
/>
|
||||||
|
|||||||
55
apps/web-antdv-next/src/views/qqbot/plugin/metadata.spec.ts
Normal file
55
apps/web-antdv-next/src/views/qqbot/plugin/metadata.spec.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { loadQqbotPluginMetadata } from './metadata';
|
||||||
|
|
||||||
|
describe('qqbot plugin page metadata', () => {
|
||||||
|
it('keeps plugin page metadata loading from rejecting when plugin list fails', async () => {
|
||||||
|
const error = { code: 500, msg: 'plugin list failed' };
|
||||||
|
const onError = vi.fn();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
loadQqbotPluginMetadata({
|
||||||
|
labelOf: (value, fallback) => fallback ?? String(value),
|
||||||
|
loadPlugins: () => Promise.reject(error),
|
||||||
|
onError,
|
||||||
|
reloadTriggerModes: () =>
|
||||||
|
Promise.resolve([{ label: '命令', value: 'command' }]),
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({
|
||||||
|
pluginMap: {},
|
||||||
|
pluginOptions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onError).toHaveBeenCalledWith(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds plugin map and options after trigger mode dictionary is reloaded', async () => {
|
||||||
|
const plugin = {
|
||||||
|
description: 'BangDream commands',
|
||||||
|
key: 'bangdream',
|
||||||
|
name: 'BangDream',
|
||||||
|
operationCount: 12,
|
||||||
|
triggerMode: 'command',
|
||||||
|
version: '1.0.0',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
loadQqbotPluginMetadata({
|
||||||
|
labelOf: (value) => (value === 'command' ? '命令' : String(value)),
|
||||||
|
loadPlugins: () => Promise.resolve([plugin]),
|
||||||
|
reloadTriggerModes: () =>
|
||||||
|
Promise.resolve([{ label: '命令', value: 'command' }]),
|
||||||
|
}),
|
||||||
|
).resolves.toEqual({
|
||||||
|
pluginMap: {
|
||||||
|
bangdream: plugin,
|
||||||
|
},
|
||||||
|
pluginOptions: [
|
||||||
|
{
|
||||||
|
label: 'BangDream (bangdream / 命令)',
|
||||||
|
value: 'bangdream',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
40
apps/web-antdv-next/src/views/qqbot/plugin/metadata.ts
Normal file
40
apps/web-antdv-next/src/views/qqbot/plugin/metadata.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import type { QqbotApi } from '#/api/qqbot';
|
||||||
|
|
||||||
|
export interface QqbotPluginMetadata {
|
||||||
|
pluginMap: Record<string, QqbotApi.Plugin>;
|
||||||
|
pluginOptions: Array<{ label: string; value: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadQqbotPluginMetadataOptions {
|
||||||
|
labelOf: (value: unknown, fallback?: string) => string;
|
||||||
|
loadPlugins: () => Promise<QqbotApi.Plugin[]>;
|
||||||
|
onError?: (error: unknown) => void;
|
||||||
|
reloadTriggerModes: () => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadQqbotPluginMetadata(
|
||||||
|
options: LoadQqbotPluginMetadataOptions,
|
||||||
|
): Promise<QqbotPluginMetadata> {
|
||||||
|
const [plugins] = await Promise.all([
|
||||||
|
options.loadPlugins().catch((error: unknown) => {
|
||||||
|
options.onError?.(error);
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
options.reloadTriggerModes().catch((error: unknown) => {
|
||||||
|
options.onError?.(error);
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const pluginMap: Record<string, QqbotApi.Plugin> = {};
|
||||||
|
for (const item of plugins) {
|
||||||
|
pluginMap[item.key] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pluginMap,
|
||||||
|
pluginOptions: plugins.map((item) => ({
|
||||||
|
label: `${item.name} (${item.key} / ${options.labelOf(item.triggerMode, '-')})`,
|
||||||
|
value: item.key,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user