fix: 对齐QQBot页面权限与插件元数据
This commit is contained in:
parent
b549e30dd5
commit
788aba00a8
@ -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,
|
||||
|
||||
@ -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<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() {
|
||||
return requestClient.post<QqbotNapcatApi.AccountScanResult>(
|
||||
'/qqbot/account/scan/create',
|
||||
|
||||
@ -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({});
|
||||
|
||||
|
||||
@ -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<any>;
|
||||
}
|
||||
|
||||
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<QqbotPluginPlatformApi.ManifestValidationResult>(
|
||||
return requestClient.post<QqbotPluginPlatformApi.PackageValidationResult>(
|
||||
'/qqbot/plugin-platform/upload',
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
export function validateQqbotPluginManifest(
|
||||
manifest: QqbotPluginPlatformApi.PackageBody['manifest'],
|
||||
manifest: QqbotPluginPlatformApi.ManifestBody['manifest'],
|
||||
) {
|
||||
return requestClient.post<QqbotPluginPlatformApi.ManifestValidationResult>(
|
||||
'/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 { 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 ?? [];
|
||||
|
||||
// 生成菜单和路由
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 () => (
|
||||
<AModal
|
||||
@ -35,6 +56,7 @@ export default defineComponent({
|
||||
title={props.title}
|
||||
width={760}
|
||||
>
|
||||
{props.mode === 'validate' ? (
|
||||
<textarea
|
||||
class="w-full resize-y rounded border border-solid border-gray-200 p-3 font-mono text-sm outline-none"
|
||||
onInput={(event) => {
|
||||
@ -43,6 +65,30 @@ export default defineComponent({
|
||||
rows={18}
|
||||
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>
|
||||
);
|
||||
},
|
||||
|
||||
@ -32,6 +32,7 @@ import { useDict } from '#/hooks/useDict';
|
||||
|
||||
import PluginManifestModal from './components/PluginManifestModal';
|
||||
import PluginPlatformStateDrawer from './components/PluginPlatformStateDrawer';
|
||||
import { loadQqbotPluginMetadata } from './metadata';
|
||||
|
||||
const AKtTable = KtTable as any;
|
||||
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 manifestModalOpen = ref(false);
|
||||
const manifestText = ref(JSON.stringify(defaultManifest, null, 2));
|
||||
const packageHashText = ref('');
|
||||
const packagePathText = ref('');
|
||||
const pluginOptions = ref<Array<{ label: string; value: string }>>([]);
|
||||
const pluginMap = ref<Record<string, QqbotApi.Plugin>>({});
|
||||
const platformLoading = ref(false);
|
||||
@ -88,8 +91,8 @@ export default defineComponent({
|
||||
return '插件安装记录';
|
||||
});
|
||||
const manifestModalTitle = computed(() => {
|
||||
if (manifestMode.value === 'install') return '安装插件 Manifest';
|
||||
if (manifestMode.value === 'upload') return '上传插件 Manifest';
|
||||
if (manifestMode.value === 'install') return '本地安装插件包';
|
||||
if (manifestMode.value === 'upload') return '上传插件包';
|
||||
return '校验插件 Manifest';
|
||||
});
|
||||
const {
|
||||
@ -208,24 +211,20 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
async function loadMetadata() {
|
||||
const [plugins] = await Promise.all([
|
||||
getQqbotPluginList(),
|
||||
reloadTriggerModeDict(),
|
||||
]);
|
||||
const nextPluginMap: Record<string, QqbotApi.Plugin> = {};
|
||||
for (const item of plugins) {
|
||||
nextPluginMap[item.key] = item;
|
||||
}
|
||||
pluginMap.value = nextPluginMap;
|
||||
pluginOptions.value = plugins.map((item) => ({
|
||||
label: `${item.name} (${item.key} / ${getTriggerModeLabel(item.triggerMode, '-')})`,
|
||||
value: item.key,
|
||||
}));
|
||||
const metadata = await loadQqbotPluginMetadata({
|
||||
labelOf: getTriggerModeLabel,
|
||||
loadPlugins: () => getQqbotPluginList(),
|
||||
reloadTriggerModes: () => reloadTriggerModeDict(),
|
||||
});
|
||||
pluginMap.value = metadata.pluginMap;
|
||||
pluginOptions.value = metadata.pluginOptions;
|
||||
}
|
||||
|
||||
function openManifestModal(mode: typeof manifestMode.value) {
|
||||
manifestMode.value = mode;
|
||||
manifestText.value = JSON.stringify(defaultManifest, null, 2);
|
||||
packageHashText.value = '';
|
||||
packagePathText.value = '';
|
||||
manifestModalOpen.value = true;
|
||||
}
|
||||
|
||||
@ -239,19 +238,26 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
async function submitManifest() {
|
||||
const manifest = parseManifestText();
|
||||
if (!manifest) return;
|
||||
|
||||
platformLoading.value = true;
|
||||
try {
|
||||
if (manifestMode.value === 'upload') {
|
||||
await uploadQqbotPluginPackage({ manifest });
|
||||
message.success('插件包上传校验通过');
|
||||
const body = parsePackageBody();
|
||||
if (!body) return;
|
||||
const result = await uploadQqbotPluginPackage(body);
|
||||
message.success(
|
||||
result.packageHash
|
||||
? `插件包上传校验通过:${result.packageHash.slice(0, 12)}`
|
||||
: '插件包上传校验通过',
|
||||
);
|
||||
} else if (manifestMode.value === 'install') {
|
||||
await installLocalQqbotPluginPackage({ manifest });
|
||||
const body = parsePackageBody();
|
||||
if (!body) return;
|
||||
await installLocalQqbotPluginPackage(body);
|
||||
message.success('插件已安装');
|
||||
await loadInstallations(false);
|
||||
} else {
|
||||
const manifest = parseManifestText();
|
||||
if (!manifest) return;
|
||||
await validateQqbotPluginManifest(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) {
|
||||
installations.value = await getQqbotPluginPlatformInstallations();
|
||||
drawerMode.value = 'installations';
|
||||
@ -329,14 +350,23 @@ export default defineComponent({
|
||||
/>
|
||||
<PluginManifestModal
|
||||
loading={platformLoading.value}
|
||||
mode={manifestMode.value}
|
||||
onClose={() => {
|
||||
manifestModalOpen.value = false;
|
||||
}}
|
||||
onSubmit={() => void submitManifest()}
|
||||
onUpdate:packageHash={(value: string) => {
|
||||
packageHashText.value = value;
|
||||
}}
|
||||
onUpdate:packagePath={(value: string) => {
|
||||
packagePathText.value = value;
|
||||
}}
|
||||
onUpdate:value={(value: string) => {
|
||||
manifestText.value = value;
|
||||
}}
|
||||
open={manifestModalOpen.value}
|
||||
packageHash={packageHashText.value}
|
||||
packagePath={packagePathText.value}
|
||||
title={manifestModalTitle.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