diff --git a/README.md b/README.md index eda1eeb..4aa7548 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ pnpm run build:antdv-next - 系统管理 / 站内信是日志级通知列表,只展示 API 错误、QQBot 下线、NapCat 离线等后端自动捕获事件;页面提供筛选、处理/重新打开、置顶和删除,不提供人工新增或编辑。 - QQBot / 账号连接页拆分 OneBot 连接、QQ 登录、NapCat 运行和运行说明列;更新登录通过 SSE 展示 quick / password / captcha / qrcode 每步进度,密码登录触发 QQ 安全验证时在弹窗内完成腾讯验证码并回交 API。 +- QQBot / 插件平台页保留在线命令能力表,并提供 manifest 校验、本地插件安装、安装记录、运行事件和账号绑定抽屉,接口走 `/qqbot/plugin-platform/*`。 ## 部署说明 diff --git a/apps/web-antdv-next/src/api/qqbot/plugin.ts b/apps/web-antdv-next/src/api/qqbot/plugin.ts new file mode 100644 index 0000000..1c8f44c --- /dev/null +++ b/apps/web-antdv-next/src/api/qqbot/plugin.ts @@ -0,0 +1,164 @@ +import type { Recordable } from '@vben/types'; + +import { requestClient } from '#/api/request'; + +export namespace QqbotPluginPlatformApi { + export type InstallStatus = + | 'disabled' + | 'enabled' + | 'failed' + | 'installed' + | 'uninstalled' + | 'uploaded' + | 'validated'; + + export type RuntimeStatus = + | 'crashed' + | 'healthy' + | 'starting' + | 'stopped' + | 'unhealthy'; + + export interface ManifestValidationResult { + manifest: Recordable; + valid: boolean; + } + + export interface Installation { + createTime?: string; + id: string; + installedPath?: string; + pluginId: string; + runtimeStatus: RuntimeStatus; + status: InstallStatus; + updateTime?: string; + versionId: string; + } + + export interface RuntimeEvent { + createTime?: string; + eventType: string; + id: string; + installationId?: null | string; + level: 'error' | 'info' | 'warn'; + pluginId: string; + safeSummary?: Recordable; + } + + export interface AccountBinding { + accountId: string; + createTime?: string; + enabled: boolean; + id: string; + pluginId: string; + } + + export interface ConfigBody { + configKey: string; + pluginId: string; + value?: any; + } + + export interface PackageBody { + manifest: Recordable; + packageHash?: string; + packagePath?: string; + } +} + +export function getQqbotPluginPlatformInstallations() { + return requestClient.get( + '/qqbot/plugin-platform/installations', + ); +} + +export function uploadQqbotPluginPackage( + data: QqbotPluginPlatformApi.PackageBody, +) { + return requestClient.post( + '/qqbot/plugin-platform/upload', + data, + ); +} + +export function validateQqbotPluginManifest( + manifest: QqbotPluginPlatformApi.PackageBody['manifest'], +) { + return requestClient.post( + '/qqbot/plugin-platform/validate', + { manifest }, + ); +} + +export function installQqbotPluginPackage( + data: QqbotPluginPlatformApi.PackageBody, +) { + return requestClient.post( + '/qqbot/plugin-platform/install', + data, + ); +} + +export function installLocalQqbotPluginPackage( + data: QqbotPluginPlatformApi.PackageBody, +) { + return requestClient.post( + '/qqbot/plugin-platform/install-local', + data, + ); +} + +export function enableQqbotPluginInstallation(id: string) { + return requestClient.post<{ id: string; status: string }>( + '/qqbot/plugin-platform/enable', + { id }, + ); +} + +export function disableQqbotPluginInstallation(id: string) { + return requestClient.post<{ id: string; status: string }>( + '/qqbot/plugin-platform/disable', + { id }, + ); +} + +export function upgradeQqbotPluginInstallation(id: string) { + return requestClient.post<{ id: string; status: string }>( + '/qqbot/plugin-platform/upgrade', + { id }, + ); +} + +export function uninstallQqbotPluginInstallation(id: string) { + return requestClient.post<{ id: string; status: string }>( + '/qqbot/plugin-platform/uninstall', + { id }, + ); +} + +export function updateQqbotPluginConfig( + data: QqbotPluginPlatformApi.ConfigBody, +) { + return requestClient.post( + '/qqbot/plugin-platform/config', + data, + ); +} + +export function getQqbotPluginRuntimeEvents(pluginId?: string) { + return requestClient.get( + '/qqbot/plugin-platform/runtime-events', + { + params: { pluginId }, + }, + ); +} + +export function getQqbotPluginAccountBindings(pluginId?: string) { + return requestClient.get( + '/qqbot/plugin-platform/account-bindings', + { + params: { pluginId }, + }, + ); +} diff --git a/apps/web-antdv-next/src/router/routes/modules/qqbot.ts b/apps/web-antdv-next/src/router/routes/modules/qqbot.ts index ef63786..b890af4 100644 --- a/apps/web-antdv-next/src/router/routes/modules/qqbot.ts +++ b/apps/web-antdv-next/src/router/routes/modules/qqbot.ts @@ -61,7 +61,7 @@ const routes: RouteRecordRaw[] = [ component: () => import('#/views/qqbot/plugin/list'), meta: { icon: 'lucide:plug', - title: '插件能力', + title: '插件平台', }, name: 'QqBotPlugin', path: '/qqbot/plugin', diff --git a/apps/web-antdv-next/src/views/qqbot/plugin/list.tsx b/apps/web-antdv-next/src/views/qqbot/plugin/list.tsx index 927199b..619971c 100644 --- a/apps/web-antdv-next/src/views/qqbot/plugin/list.tsx +++ b/apps/web-antdv-next/src/views/qqbot/plugin/list.tsx @@ -1,23 +1,39 @@ import type { TableColumnType } from 'antdv-next'; import type { QqbotApi } from '#/api/qqbot'; +import type { QqbotPluginPlatformApi } from '#/api/qqbot/plugin'; import type { KtTableApi, KtTableButton } from '#/components/ktTable'; import type { DictOption } from '#/hooks/useDict'; -import { defineComponent, onMounted, ref } from 'vue'; +import { computed, defineComponent, onMounted, ref } from 'vue'; import { Page } from '@vben/common-ui'; -import { message, Tag } from 'antdv-next'; +import { Button, Drawer, message, Modal, Space, Tag } from 'antdv-next'; import { getQqbotPluginHealth, getQqbotPluginList, getQqbotPluginOperationList, } from '#/api/qqbot'; +import { + disableQqbotPluginInstallation, + enableQqbotPluginInstallation, + getQqbotPluginAccountBindings, + getQqbotPluginPlatformInstallations, + getQqbotPluginRuntimeEvents, + installLocalQqbotPluginPackage, + uninstallQqbotPluginInstallation, + uploadQqbotPluginPackage, + validateQqbotPluginManifest, +} from '#/api/qqbot/plugin'; import { KtTable, useKtTable } from '#/components/ktTable'; import { useDict } from '#/hooks/useDict'; +const AButton = Button as any; +const ADrawer = Drawer as any; +const AModal = Modal as any; +const ASpace = Space as any; const AKtTable = KtTable as any; const QQBOT_PLUGIN_TRIGGER_MODE_DICT = 'QQBOT_PLUGIN_TRIGGER_MODE'; const qqbotPluginTriggerModeFallback: Array< @@ -26,12 +42,59 @@ const qqbotPluginTriggerModeFallback: Array< { label: '命令', value: 'command' }, { label: '事件', value: 'event' }, ]; +const defaultManifest = { + assets: [], + configSchema: { type: 'object' }, + entry: 'src/index.ts', + events: [], + minApiSdkVersion: '1.0.0', + name: 'Demo Plugin', + operations: [ + { + handlerName: 'echo', + key: 'demo-plugin.echo', + name: 'Echo', + permissions: ['qqbot.send'], + timeoutMs: 3000, + }, + ], + permissions: ['qqbot.send'], + pluginKey: 'demo-plugin', + runtime: { + maxConcurrency: 1, + memoryMb: 128, + timeoutMs: 5000, + workerType: 'node-worker', + }, + version: '0.1.0', +}; export default defineComponent({ name: 'QqBotPluginList', setup() { + const accountBindings = ref([]); + const drawerMode = ref<'bindings' | 'events' | 'installations'>( + 'installations', + ); + const drawerOpen = ref(false); + const installations = ref([]); + const manifestMode = ref<'install' | 'upload' | 'validate'>('validate'); + const manifestModalOpen = ref(false); + const manifestText = ref(JSON.stringify(defaultManifest, null, 2)); const pluginOptions = ref>([]); const pluginMap = ref>({}); + const platformLoading = ref(false); + const runtimeEvents = ref([]); + const drawerTitle = computed(() => { + if (drawerMode.value === 'events') return '插件运行事件'; + if (drawerMode.value === 'bindings') return '插件账号绑定'; + return '插件安装记录'; + }); + const manifestModalTitle = computed(() => { + if (manifestMode.value === 'install') return '安装插件 Manifest'; + if (manifestMode.value === 'upload') return '上传插件 Manifest'; + return '校验插件 Manifest'; + }); const { labelOf: getTriggerModeLabel, options: triggerModeOptions, @@ -69,6 +132,36 @@ export default defineComponent({ await getQqbotPluginOperationList(params.pluginKey, params.triggerMode), }; const buttons: Array> = [ + { + key: 'manifestValidate', + label: '校验 Manifest', + onClick: () => openManifestModal('validate'), + }, + { + key: 'manifestUpload', + label: '上传插件', + onClick: () => openManifestModal('upload'), + }, + { + key: 'manifestInstall', + label: '本地安装', + onClick: () => openManifestModal('install'), + }, + { + key: 'installations', + label: '安装记录', + onClick: () => void loadInstallations(), + }, + { + key: 'runtimeEvents', + label: '运行事件', + onClick: () => void loadRuntimeEvents(), + }, + { + key: 'accountBindings', + label: '账号绑定', + onClick: () => void loadAccountBindings(), + }, { key: 'health', label: '健康检查', @@ -134,6 +227,194 @@ export default defineComponent({ })); } + function openManifestModal(mode: typeof manifestMode.value) { + manifestMode.value = mode; + manifestText.value = JSON.stringify(defaultManifest, null, 2); + manifestModalOpen.value = true; + } + + function parseManifestText() { + try { + return JSON.parse(manifestText.value); + } catch { + message.error('Manifest JSON 格式不正确'); + return undefined; + } + } + + async function submitManifest() { + const manifest = parseManifestText(); + if (!manifest) return; + + platformLoading.value = true; + try { + if (manifestMode.value === 'upload') { + await uploadQqbotPluginPackage({ manifest }); + message.success('插件包上传校验通过'); + } else if (manifestMode.value === 'install') { + await installLocalQqbotPluginPackage({ manifest }); + message.success('插件已安装'); + await loadInstallations(false); + } else { + await validateQqbotPluginManifest(manifest); + message.success('Manifest 校验通过'); + } + manifestModalOpen.value = false; + } finally { + platformLoading.value = false; + } + } + + async function loadInstallations(openDrawer = true) { + installations.value = await getQqbotPluginPlatformInstallations(); + drawerMode.value = 'installations'; + drawerOpen.value = openDrawer || drawerOpen.value; + } + + async function loadRuntimeEvents() { + runtimeEvents.value = await getQqbotPluginRuntimeEvents(); + drawerMode.value = 'events'; + drawerOpen.value = true; + } + + async function loadAccountBindings() { + accountBindings.value = await getQqbotPluginAccountBindings(); + drawerMode.value = 'bindings'; + drawerOpen.value = true; + } + + async function updateInstallationStatus( + row: QqbotPluginPlatformApi.Installation, + action: 'disable' | 'enable' | 'uninstall', + ) { + if (action === 'enable') { + await enableQqbotPluginInstallation(row.id); + message.success('插件已启用'); + } else if (action === 'disable') { + await disableQqbotPluginInstallation(row.id); + message.success('插件已禁用'); + } else { + await uninstallQqbotPluginInstallation(row.id); + message.success('插件已卸载'); + } + await loadInstallations(false); + } + + const renderStatusTag = (status?: string) => { + let color = 'processing'; + switch (status) { + case 'disabled': { + color = 'warning'; + break; + } + case 'enabled': { + color = 'success'; + break; + } + case 'failed': + case 'uninstalled': { + color = 'error'; + break; + } + } + return {status || '-'}; + }; + + const renderDrawerContent = () => { + if (drawerMode.value === 'events') { + return runtimeEvents.value.length > 0 ? ( +
+ {runtimeEvents.value.map((item) => ( +
+
+ + {item.level} + + {item.eventType} +
+
+                  {JSON.stringify(item.safeSummary || {}, null, 2)}
+                
+
+ ))} +
+ ) : ( + 暂无运行事件 + ); + } + + if (drawerMode.value === 'bindings') { + return accountBindings.value.length > 0 ? ( +
+ {accountBindings.value.map((item) => ( +
+ + {item.enabled ? '已启用' : '已停用'} + + + 插件 {item.pluginId} / 账号 {item.accountId} + +
+ ))} +
+ ) : ( + 暂无账号绑定 + ); + } + + return installations.value.length > 0 ? ( +
+ {installations.value.map((item) => ( +
+
+ {renderStatusTag(item.status)} + {item.runtimeStatus || '-'} + + 插件 {item.pluginId} / 版本 {item.versionId} + +
+ + void updateInstallationStatus(item, 'enable')} + size="small" + > + 启用 + + void updateInstallationStatus(item, 'disable')} + size="small" + > + 禁用 + + + void updateInstallationStatus(item, 'uninstall') + } + size="small" + > + 卸载 + + +
+ ))} +
+ ) : ( + 暂无安装记录 + ); + }; + return () => ( + { + manifestModalOpen.value = false; + }} + onOk={() => void submitManifest()} + open={manifestModalOpen.value} + title={manifestModalTitle.value} + width={760} + > +