import { Inject, Injectable, OnModuleInit, Optional } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Between, LessThanOrEqual, MoreThanOrEqual, Repository } from 'typeorm'; import { formatKtDateTime, throwVbenError } from '@/common'; import { type QqbotPluginEventDispatchInput, type QqbotPluginExecutionInput, type QqbotPluginExecutionPort, type QqbotPluginOperationLookup, } from '@/modules/qqbot/core/domain/plugin-execution.port'; import type { QqbotEventPluginDefinition, QqbotPluginHealth, QqbotPluginOperationSummary, QqbotPluginSummary, QqbotPluginTriggerMode, } from '@/modules/qqbot/core/contract/qqbot.types'; import { parseQqbotPluginManifest, type QqbotPluginManifest, } from '../domain/manifest'; import type { QqbotPluginRuntimeEvent as QqbotPluginWorkerRuntimeEvent, QqbotPluginWorkerRuntime, } from '../infrastructure/integration/runtime'; import { QqbotPluginTaskManifestSynchronizer } from './task/qqbot-plugin-task-manifest.synchronizer'; import { QqbotPluginTaskSchedulerService } from './task/qqbot-plugin-task-scheduler.service'; import type { QqbotPluginTaskTriggerType } from './task/qqbot-plugin-task.types'; import { QqbotPluginArgumentParserService } from './argument/qqbot-plugin-argument-parser.service'; import { QqbotPluginPackageReaderService } from '../infrastructure/integration/package/plugin-package-reader.service'; import { QqbotPluginPackageSourceService } from '../infrastructure/integration/package/plugin-package-source.service'; import type { QqbotPluginPackageDescriptor } from '../infrastructure/integration/package/plugin-package.types'; import { QqbotEventPluginRegistryService } from './registry/qqbot-event-plugin-registry.service'; import { resolveInactivePluginKeys } from './registry/plugin-installation-state'; import { QqbotPluginRegistryService } from './registry/qqbot-plugin-registry.service'; import { QqbotPlugin, QqbotPluginAccountBinding, QqbotPluginAsset, QqbotPluginConfig, QqbotPluginEventHandler, QqbotPluginInstallation, QqbotPluginOperation, QqbotPluginRuntimeEvent, QqbotPluginVersion, type QqbotPluginInstallStatus, type QqbotPluginRuntimeEventLevel, type QqbotPluginRuntimeStatus, } from '../infrastructure/persistence'; export const QQBOT_PLUGIN_RUNTIME_FACTORY = Symbol( 'QQBOT_PLUGIN_RUNTIME_FACTORY', ); export type QqbotPluginRuntimeFactory = { create( installation: QqbotPluginInstallation, version: QqbotPluginVersion, ): Pick< QqbotPluginWorkerRuntime, | 'activate' | 'deactivate' | 'dispose' | 'drainRuntimeEvents' | 'executeOperation' | 'executeTask' | 'handleEvent' | 'health' | 'load' >; }; type ValidateManifestBody = { manifest?: unknown; }; type InstallLocalBody = { manifest?: unknown; packageHash?: string; packagePath?: string; }; type InstallationActionBody = { id?: string; }; type ListOperationsQuery = { pageNo?: number | string; pageSize?: number | string; pluginId?: string; pluginKey?: string; triggerMode?: QqbotPluginTriggerMode; }; type RuntimeEventQuery = { endTime?: string; eventType?: string; installationId?: string; level?: QqbotPluginRuntimeEventLevel; pluginId?: string; startTime?: string; }; type UpdateConfigBody = { configKey?: string; pluginId?: string; value?: unknown; }; type ActiveWorkerContext = { installationId: string; manifest: QqbotPluginManifest; pluginId: string; pluginKey: string; worker: QqbotPluginWorkerRuntime; }; type PersistedPluginRuntimeState = { enabledInstallationsByPluginKey: Map; inactivePluginKeys: Set; }; @Injectable() export class QqbotPluginPlatformService implements OnModuleInit, QqbotPluginExecutionPort { private readonly activeWorkers = new Map(); private readonly activeWorkerContexts = new Map< string, ActiveWorkerContext >(); private readonly activeWorkersByPluginKey = new Map< string, ActiveWorkerContext >(); private readonly activeWorkerPluginAliases = new Map(); /** * 初始化 QqbotPluginPlatformService 实例。 * @param pluginRepository - 插件主表仓库;维护 package key、名称和安装状态的持久化真相。 * @param versionRepository - 插件版本仓库;保存 manifest 快照和 package hash 供 worker 启动。 * @param installationRepository - 插件安装仓库;记录 installedPath、versionId 和 runtime/status 状态。 * @param operationRepository - 命令能力仓库;写入和启停 manifest 声明的 command operation。 * @param eventHandlerRepository - 事件能力仓库;写入和启停 manifest 声明的 event handler。 * @param accountBindingRepository - 账号绑定仓库;查询插件与账号的绑定关系。 * @param configRepository - 插件配置仓库;保存 Admin 修改的插件配置项。 * @param assetRepository - 插件资产仓库;保存 manifest 声明的资源路径和 hash。 * @param runtimeEventRepository - 运行时事件仓库;持久化 worker lifecycle、命令和任务安全摘要。 * @param argumentParser - 命令参数解析器;在执行 worker operation 前规范化输入。 * @param runtimeFactory - worker runtime 工厂;根据 installation/version 创建隔离执行实例。 * @param pluginRegistry - 命令 registry;在没有 active worker 时作为显式注册插件的查询兜底。 * @param eventPluginRegistry - 事件 registry;接收 worker manifest 元数据并支撑 Admin 绑定页面。 * @param packageReader - 本地安装包读取器;处理 Admin 上传/安装插件包的 manifest 校验。 * @param packageSource - 插件包描述符来源;启动内置包时只读取 manifest 和受控路径。 * @param taskSynchronizer - 定时任务同步器;把 manifest tasks 投影到平台任务表。 * @param taskScheduler - 定时任务调度器;在插件启停时同步或移除 BullMQ cron 调度。 */ constructor( @InjectRepository(QqbotPlugin) private readonly pluginRepository: Repository, @InjectRepository(QqbotPluginVersion) private readonly versionRepository: Repository, @InjectRepository(QqbotPluginInstallation) private readonly installationRepository: Repository, @InjectRepository(QqbotPluginOperation) private readonly operationRepository: Repository, @InjectRepository(QqbotPluginEventHandler) private readonly eventHandlerRepository: Repository, @InjectRepository(QqbotPluginAccountBinding) private readonly accountBindingRepository: Repository, @InjectRepository(QqbotPluginConfig) private readonly configRepository: Repository, @InjectRepository(QqbotPluginAsset) private readonly assetRepository: Repository, @InjectRepository(QqbotPluginRuntimeEvent) private readonly runtimeEventRepository: Repository, @Optional() private readonly argumentParser?: QqbotPluginArgumentParserService, @Optional() @Inject(QQBOT_PLUGIN_RUNTIME_FACTORY) private readonly runtimeFactory?: QqbotPluginRuntimeFactory, @Optional() private readonly pluginRegistry?: QqbotPluginRegistryService, @Optional() private readonly eventPluginRegistry?: QqbotEventPluginRegistryService, @Optional() private readonly packageReader?: QqbotPluginPackageReaderService, @Optional() private readonly packageSource?: QqbotPluginPackageSourceService, @Optional() private readonly taskSynchronizer?: QqbotPluginTaskManifestSynchronizer, @Optional() private readonly taskScheduler?: QqbotPluginTaskSchedulerService, ) {} /** * 处理 QQBot 插件平台事件。 */ async onModuleInit() { await this.startBuiltinWorkers(); } /** * 列出Installations。 */ async listInstallations() { return this.installationRepository.find(); } /** * 列出Capabilities。 * @param pluginId - 插件 ID;定位本次读取、更新、删除或关联的插件。 */ async listCapabilities(pluginId?: string) { const where = pluginId ? { pluginId } : undefined; const [operations, eventHandlers] = await Promise.all([ this.operationRepository.find({ where }), this.eventHandlerRepository.find({ where }), ]); return { eventHandlers, operations, }; } /** * 列出Operations。 * @param pluginId - 插件 ID;定位本次读取、更新、删除或关联的插件。 */ async listOperations(pluginId?: string) { return this.listOperationSummaries({ pluginId }); } /** * Lists command plugin summaries from active descriptor-backed workers. * @param pluginKey - Optional package or legacy key used by Admin to narrow the command plugin list. * @returns Command plugin summaries sourced from active worker manifests, with registry fallback only when no workers are active. */ async listPluginSummaries(pluginKey?: string): Promise { const workerSummaries = this.listActiveWorkerPluginSummaries(pluginKey); if (workerSummaries.length > 0 || this.activeWorkerContexts.size > 0) { return workerSummaries; } return (this.pluginRegistry?.listPlugins() || []).filter( (plugin) => !pluginKey || plugin.key === pluginKey, ); } /** * Reads health for command plugins from active descriptor-backed workers. * @param pluginKey - Optional package or legacy key used by Admin to narrow the health check. * @returns Health entries normalized for Admin command plugin status display. */ async listPluginHealth(pluginKey?: string): Promise { const workerContexts = this.listActiveWorkerCommandContexts(pluginKey); if (workerContexts.length <= 0 && this.activeWorkerContexts.size <= 0) { return this.pluginRegistry?.health(pluginKey) || []; } return Promise.all( workerContexts.map(async (workerContext) => { try { return this.toWorkerPluginHealth( workerContext, await workerContext.worker.health(), ); } finally { await this.flushWorkerRuntimeEvents(workerContext); } }), ); } /** * 执行 QQBot 插件平台流程。 * @param query - 查询参数 DTO;限定 插件平台分页、搜索或详情查询条件。 */ async pageOperations(query: ListOperationsQuery) { return this.pageOperationSummaries(query); } /** * 列出Operation Summaries。 * @param query - 查询参数 DTO;限定 插件平台分页、搜索或详情查询条件。 */ async listOperationSummaries(query: ListOperationsQuery = {}) { const pluginKey = await this.resolveOperationPluginKeyFilter(query); const operations = await this.resolveActiveOperationSummaries(); return operations.filter( (operation) => (!pluginKey || operation.pluginKey === pluginKey) && (!query.triggerMode || operation.triggerMode === query.triggerMode), ); } /** * 执行 QQBot 插件平台流程。 * @param query - 查询参数 DTO;限定 插件平台分页、搜索或详情查询条件。 */ async pageOperationSummaries(query: ListOperationsQuery) { const pageNo = Number(query.pageNo || 1); const pageSize = Number(query.pageSize || 10); const safePageNo = Number.isFinite(pageNo) && pageNo > 0 ? pageNo : 1; const safePageSize = Number.isFinite(pageSize) && pageSize > 0 ? pageSize : 10; const operations = await this.listOperationSummaries(query); const skip = (safePageNo - 1) * safePageSize; return { list: operations.slice(skip, skip + safePageSize), pageNo: safePageNo, pageSize: safePageSize, total: operations.length, }; } /** * 列出Event Handlers。 * @param pluginId - 插件 ID;定位本次读取、更新、删除或关联的插件。 */ async listEventHandlers(pluginId?: string) { return this.eventHandlerRepository.find({ where: pluginId ? { pluginId } : undefined, }); } /** * 判断 QQBot 插件平台条件。 * @param body - 请求体 DTO;承载 插件平台新增、更新、导入或执行字段。 */ validateManifest(body: ValidateManifestBody) { return { manifest: parseQqbotPluginManifest(body.manifest), valid: true, }; } /** * 执行 QQBot 插件平台流程。 * @param body - 请求体 DTO;承载 插件平台新增、更新、导入或执行字段。 */ uploadPackage(body: InstallLocalBody) { return { ...this.requirePackageReader().readPackage(body), valid: true, }; } /** * 执行 QQBot 插件平台流程。 * @param body - 请求体 DTO;承载 插件平台新增、更新、导入或执行字段。 */ async installLocal(body: InstallLocalBody) { const pluginPackage = this.requirePackageReader().readPackage(body); const manifest = pluginPackage.manifest; const plugin = await this.pluginRepository.save({ pluginKey: manifest.pluginKey, pluginName: manifest.name, description: manifest.description || null, status: 'installed', }); const version = await this.versionRepository.save({ manifestJson: manifest, packageHash: pluginPackage.packageHash, pluginId: plugin.id, version: manifest.version, }); await this.persistManifestCapabilities(plugin.id, manifest); const installation = await this.installationRepository.save({ installedPath: pluginPackage.packagePath, pluginId: plugin.id, runtimeStatus: 'stopped', status: 'installed', versionId: version.id, }); await this.syncManifestTasksForInstallation(installation, manifest, false); return installation; } /** * 执行 QQBot 插件平台流程。 * @param body - 请求体 DTO;承载 插件平台新增、更新、导入或执行字段。 */ async enableInstallation(body: InstallationActionBody) { const installation = await this.requireInstallation(body); await this.updateInstallationRuntime(installation, 'enabled', 'starting'); await this.recordRuntimeEvent(installation, 'enable-started'); await this.startWorker(installation); await this.refreshActiveRegistries(installation, true); await this.updateInstallationRuntime(installation, 'enabled', 'healthy'); await this.recordRuntimeEvent(installation, 'enable-finished'); return { id: installation.id, runtimeStatus: 'healthy' as QqbotPluginRuntimeStatus, status: 'enabled' as QqbotPluginInstallStatus, }; } /** * 执行 QQBot 插件平台流程。 * @param body - 请求体 DTO;承载 插件平台新增、更新、导入或执行字段。 */ async disableInstallation(body: InstallationActionBody) { const installation = await this.requireInstallation(body); await this.stopWorkersForInstallation(installation); await this.taskScheduler?.removeSchedulersForInstallation(installation.id); await this.refreshActiveRegistries(installation, false); await this.updateInstallationRuntime(installation, 'disabled', 'stopped'); await this.recordRuntimeEvent(installation, 'disable-finished'); return { id: installation.id, runtimeStatus: 'stopped' as QqbotPluginRuntimeStatus, status: 'disabled' as QqbotPluginInstallStatus, }; } /** * 执行 QQBot 插件平台流程。 * @param body - 请求体 DTO;承载 插件平台新增、更新、导入或执行字段。 */ async upgradeInstallation(body: InstallationActionBody) { const installation = await this.requireInstallation(body); const previousWorker = this.activeWorkers.get(installation.id); await this.updateInstallationRuntime(installation, 'upgrading', 'starting'); await this.recordRuntimeEvent(installation, 'upgrade-started'); try { await this.startWorker(installation); } catch (error) { if (previousWorker) { this.activeWorkers.set(installation.id, previousWorker); await this.updateInstallationRuntime( installation, 'enabled', 'healthy', ); } await this.recordRuntimeEvent(installation, 'upgrade-failed', 'error', { message: error instanceof Error ? error.message : `${error}`, }); throw error; } await this.refreshActiveRegistries(installation, true); await this.updateInstallationRuntime(installation, 'enabled', 'healthy'); await this.recordRuntimeEvent(installation, 'upgrade-finished'); return { id: installation.id, runtimeStatus: 'healthy' as QqbotPluginRuntimeStatus, status: 'enabled' as QqbotPluginInstallStatus, }; } /** * 执行 QQBot 插件平台流程。 * @param body - 请求体 DTO;承载 插件平台新增、更新、导入或执行字段。 */ async uninstallInstallation(body: InstallationActionBody) { const installation = await this.requireInstallation(body); if (installation.status === 'enabled') { throwVbenError('请先禁用插件后再卸载'); } await this.refreshActiveRegistries(installation, false); await this.taskScheduler?.removeSchedulersForInstallation(installation.id); await this.updateInstallationRuntime( installation, 'uninstalled', 'stopped', ); await this.recordRuntimeEvent(installation, 'uninstall-finished'); return { id: installation.id, runtimeStatus: 'stopped' as QqbotPluginRuntimeStatus, status: 'uninstalled' as QqbotPluginInstallStatus, }; } /** * 执行Operation。 * @param input - input 输入;使用 `input`、`pluginKey`、`operationKey`、`context` 字段生成结果。 */ async executeOperation(input: QqbotPluginExecutionInput) { const normalizedInput = (await this.argumentParser?.normalizeInput(input)) || input.input; const workerContext = this.requireActiveWorker(input.pluginKey); const operation = workerContext.manifest.operations.find( (item) => item.key === input.operationKey, ); if (!operation) { throwVbenError( `QQBot 插件能力不存在:${input.pluginKey}.${input.operationKey}`, ); } try { const output = await workerContext.worker.executeOperation({ input: normalizedInput, operationId: operation.key, operationKey: operation.key, timeoutMs: operation.timeoutMs, }); const pluginId = this.getPluginIdFromContext(input.context); if (pluginId) { await this.runtimeEventRepository.save({ eventType: 'command log mapped', installationId: null, level: 'info', pluginId, safeSummary: { operationKey: input.operationKey, outputKeys: output && typeof output === 'object' ? Object.keys(output as Record).sort() : [], pluginKey: input.pluginKey, }, }); } return output; } finally { await this.flushWorkerRuntimeEvents(workerContext); } } /** * 投递 QQBot 插件平台消息或任务。 * @param input - input 输入;使用 `eventKey`、`message` 字段生成结果。 */ async dispatchEvent(input: QqbotPluginEventDispatchInput) { let handled = false; for (const workerContext of this.activeWorkerContexts.values()) { for (const event of workerContext.manifest.events) { if ( event.eventName !== input.eventKey && event.key !== input.eventKey ) { continue; } try { const result = await workerContext.worker.handleEvent({ event: input.message, eventKey: event.eventName || input.eventKey, timeoutMs: workerContext.manifest.runtime.timeoutMs, }); handled = Boolean(result) || handled; } finally { await this.flushWorkerRuntimeEvents(workerContext); } } } return handled; } /** * 执行Task。 * @param input - input 输入;使用 `installationId`、`input`、`taskHandlerName`、`taskId` 字段生成结果。 */ async executeTask(input: { input: Record; installationId: string; pluginId: string; taskHandlerName: string; taskId: string; taskKey: string; timeoutMs: number; triggerType: QqbotPluginTaskTriggerType; }) { const workerContext = this.activeWorkerContexts.get(input.installationId); if (!workerContext) { throwVbenError('插件运行时未启用'); } try { return await workerContext.worker.executeTask({ input: input.input, taskHandlerName: input.taskHandlerName, taskId: input.taskId, taskKey: input.taskKey, timeoutMs: input.timeoutMs, triggerType: input.triggerType, }); } finally { await this.flushWorkerRuntimeEvents(workerContext); } } /** * 列出Active Operations。 */ async listActiveOperations() { const workerOperations = this.listActiveWorkerOperations(); if (workerOperations.length > 0) return workerOperations; return [ ...(this.pluginRegistry?.listOperations() || []), ...(this.eventPluginRegistry?.listOperations() || []), ]; } /** * 查询 QQBot 插件平台数据。 * @param command - command 输入;使用 `pluginKey`、`operationKey` 字段生成结果。 */ async getOperationByCommand(command: QqbotPluginOperationLookup) { if (!command.pluginKey || !command.operationKey) return null; return ( (await this.listActiveOperations()).find( (operation) => operation.pluginKey === this.resolveActivePluginKey(command.pluginKey) && operation.key === command.operationKey, ) || null ); } /** * 更新Config。 * @param body - 请求体 DTO;承载 插件平台新增、更新、导入或执行字段。 */ async updateConfig(body: UpdateConfigBody) { if (!body.pluginId || !body.configKey) { throwVbenError('请选择插件和配置项'); } return this.configRepository.save({ configKey: body.configKey, configValue: body.value === undefined ? null : { value: body.value }, pluginId: body.pluginId, }); } /** * 列出Runtime Events。 * @param query - 查询参数 DTO;限定 插件平台分页、搜索或详情查询条件。 */ async listRuntimeEvents(query?: RuntimeEventQuery | string) { const normalizedQuery = typeof query === 'string' ? { pluginId: query } : query || {}; const where = { ...(normalizedQuery.eventType ? { eventType: normalizedQuery.eventType } : {}), ...(normalizedQuery.installationId ? { installationId: normalizedQuery.installationId } : {}), ...(normalizedQuery.level ? { level: normalizedQuery.level } : {}), ...(normalizedQuery.pluginId ? { pluginId: normalizedQuery.pluginId } : {}), ...this.buildRuntimeEventTimeFilter(normalizedQuery), }; return this.runtimeEventRepository.find({ where: Object.keys(where).length ? (where as any) : undefined, }); } /** * 列出Account Bindings。 * @param pluginId - 插件 ID;定位本次读取、更新、删除或关联的插件。 */ async listAccountBindings(pluginId?: string) { return this.accountBindingRepository.find({ where: pluginId ? { pluginId } : undefined, }); } /** * 保存 QQBot 插件平台数据。 * @param pluginId - 插件 ID;定位本次读取、更新、删除或关联的插件。 * @param manifest - manifest 输入;使用 `operations`、`events`、`assets` 字段生成结果。 */ private async persistManifestCapabilities( pluginId: string, manifest: QqbotPluginManifest, ) { await Promise.all([ ...manifest.operations.map((operation) => this.operationRepository.save({ enabled: true, handlerName: operation.handlerName, operationKey: operation.key, operationName: operation.name, pluginId, }), ), ...manifest.events.map((event) => this.eventHandlerRepository.save({ enabled: true, eventKey: event.key, handlerName: event.handlerName, pluginId, }), ), ...manifest.assets.map((asset) => this.assetRepository.save({ assetKey: asset.key, assetPath: asset.path, contentHash: asset.contentHash || '', pluginId, }), ), ]); } /** * 解析Active Operation Summaries。 */ private async resolveActiveOperationSummaries() { const operations = await this.listActiveOperations(); return operations.map((operation) => this.toPlatformOperationSummary(operation), ); } /** * 执行 QQBot 插件平台流程。 * @param operation - operation 输入;使用 `key`、`name`、`pluginKey` 字段生成结果。 */ private toPlatformOperationSummary(operation: QqbotPluginOperationSummary) { return { ...operation, enabled: true, operationKey: operation.key, operationName: operation.name, pluginId: operation.pluginKey, }; } /** * 解析Operation Plugin Key Filter。 * @param query - 查询参数 DTO;限定 插件平台分页、搜索或详情查询条件。 */ private async resolveOperationPluginKeyFilter(query: ListOperationsQuery) { if (query.pluginKey) return this.resolveActivePluginKey(query.pluginKey); if (!query.pluginId) return undefined; const findOne = this.pluginRepository.findOne?.bind(this.pluginRepository); const plugin = findOne ? await findOne({ where: { id: query.pluginId } }) : null; return plugin?.pluginKey || query.pluginId; } /** * 执行 QQBot 插件平台流程。 * @param body - 请求体 DTO;承载 插件平台新增、更新、导入或执行字段。 */ private async requireInstallation(body: InstallationActionBody) { if (!body.id) throwVbenError('请选择插件安装记录'); const findOne = this.installationRepository.findOne?.bind( this.installationRepository, ); const installation = findOne ? await findOne({ where: { id: body.id } }) : null; return ( installation || ({ id: body.id, installedPath: '', pluginId: body.id, runtimeStatus: 'stopped', status: 'installed', versionId: '', } as QqbotPluginInstallation) ); } /** * 更新Installation Runtime。 * @param installation - installation 输入;使用 `id`、`runtimeStatus`、`status` 字段生成结果。 * @param status - 插件平台列表;写入 插件平台状态。 * @param runtimeStatus - 插件平台列表;写入 插件平台状态。 */ private async updateInstallationRuntime( installation: QqbotPluginInstallation, status: QqbotPluginInstallStatus, runtimeStatus: QqbotPluginRuntimeStatus, ) { await this.installationRepository.update( { id: installation.id }, { runtimeStatus, status }, ); installation.runtimeStatus = runtimeStatus; installation.status = status; } /** * 执行 QQBot 插件平台流程。 * @param installation - installation 输入;使用 `pluginId` 字段生成结果。 * @param enabled - enabled 输入;驱动 `setPluginActive()` 的 插件平台步骤。 */ private async refreshActiveRegistries( installation: QqbotPluginInstallation, enabled: boolean, ) { const activeOperation = enabled; const activeEvent = enabled; const pluginKey = await this.getPluginKey(installation.pluginId); await Promise.all([ this.operationRepository.update( { pluginId: installation.pluginId }, { enabled: activeOperation }, ), this.eventHandlerRepository.update( { pluginId: installation.pluginId }, { enabled: activeEvent }, ), ]); this.pluginRegistry?.setPluginActive(pluginKey, enabled); this.eventPluginRegistry?.setPluginActive(pluginKey, enabled); } /** * Starts built-in plugin packages discovered from controlled package roots. * @returns Count of descriptor-backed workers that reached the active registry. */ async startBuiltinWorkers(): Promise { if (!this.runtimeFactory || !this.packageSource) return 0; const persistedState = await this.resolvePersistedPluginRuntimeState(); const descriptors = await this.packageSource.discoverPackages(); let startedCount = 0; for (const descriptor of descriptors) { if (persistedState.inactivePluginKeys.has(descriptor.pluginKey)) { continue; } if (this.activeWorkersByPluginKey.has(descriptor.pluginKey)) { continue; } const persistedInstallation = persistedState.enabledInstallationsByPluginKey.get( descriptor.pluginKey, ); const { installation, version } = await this.ensureBuiltinRuntimePersistence( descriptor, persistedInstallation, ); try { await this.startWorker(installation, version); startedCount += 1; } catch (error) { await this.recordBuiltinWorkerStartFailure(installation, error); } } return startedCount; } /** * Records one failed built-in worker boot without blocking other built-in plugin packages. * @param installation - Persisted installation whose worker failed during module startup. * @param error - Startup error from worker load, activate, or health checks. */ private async recordBuiltinWorkerStartFailure( installation: QqbotPluginInstallation, error: unknown, ) { const message = error instanceof Error ? error.message : `${error}`; await this.updateInstallationRuntime( installation, 'enabled', 'unhealthy', ); await this.recordRuntimeEvent( installation, 'builtin-start-failed', 'error', { message, }, ); } /** * Ensures descriptor-discovered built-in package state is persisted. * @param descriptor - Manifest descriptor discovered from a controlled plugin package root. * @param persistedInstallation - Existing enabled installation for the same plugin key, when present. * @returns Installation and version rows aligned to the descriptor package root and manifest snapshot. */ private async ensureBuiltinRuntimePersistence( descriptor: QqbotPluginPackageDescriptor, persistedInstallation?: QqbotPluginInstallation, ) { const plugin = await this.ensureBuiltinPlugin(descriptor); const version = await this.ensureBuiltinPluginVersion( plugin.id, descriptor, ); const installation = persistedInstallation ? await this.alignBuiltinInstallation( persistedInstallation, version, descriptor, ) : await this.installationRepository.save({ installedPath: descriptor.packageRoot, pluginId: plugin.id, runtimeStatus: 'stopped', status: 'enabled', versionId: version.id, }); return { installation, version }; } /** * Ensures the plugin row exists for a descriptor-discovered package. * @param descriptor - Package descriptor whose manifest owns the plugin key, name, and description. * @returns Persisted plugin row for the descriptor package. */ private async ensureBuiltinPlugin( descriptor: QqbotPluginPackageDescriptor, ) { const existing = await this.pluginRepository.findOne({ where: { pluginKey: descriptor.pluginKey }, }); if (existing) return existing; return this.pluginRepository.save({ description: descriptor.manifest.description || null, pluginKey: descriptor.pluginKey, pluginName: descriptor.manifest.name, status: 'installed', }); } /** * Ensures a version row exists for the descriptor manifest version. * @param pluginId - Persisted plugin id used for the `pluginId + version` uniqueness boundary. * @param descriptor - Package descriptor whose manifest is stored as the version snapshot. * @returns Persisted version row for this plugin and manifest version. */ private async ensureBuiltinPluginVersion( pluginId: string, descriptor: QqbotPluginPackageDescriptor, ) { const manifestJson = descriptor.manifest as unknown as Record; const packageHash = `${descriptor.pluginKey}:${descriptor.manifest.version}`; const existing = await this.versionRepository.findOne({ where: { pluginId, version: descriptor.manifest.version, }, }); if (existing) { const shouldUpdateSnapshot = existing.packageHash !== packageHash || JSON.stringify(existing.manifestJson) !== JSON.stringify(manifestJson); if (shouldUpdateSnapshot) { await this.versionRepository.update( { id: existing.id }, { manifestJson, packageHash, }, ); existing.manifestJson = manifestJson; existing.packageHash = packageHash; } return existing; } return this.versionRepository.save({ manifestJson, packageHash, pluginId, version: descriptor.manifest.version, }); } /** * Aligns an existing enabled installation with the current descriptor package version. * @param installation - Enabled installation previously persisted for the package plugin key. * @param version - Current descriptor-backed version row that should be launched. * @param descriptor - Package descriptor whose package root must become the installation path. * @returns Installation row updated in-memory for runtime startup. */ private async alignBuiltinInstallation( installation: QqbotPluginInstallation, version: QqbotPluginVersion, descriptor: QqbotPluginPackageDescriptor, ) { const desiredPath = descriptor.packageRoot; const patch: Partial = {}; if (installation.installedPath !== desiredPath) { patch.installedPath = desiredPath; } if (installation.versionId !== version.id) { patch.versionId = version.id; } if (Object.keys(patch).length) { await this.installationRepository.update({ id: installation.id }, patch); Object.assign(installation, patch); } return installation; } /** * 解析Persisted Plugin Runtime State。 * @returns QQBot 插件平台转换后的值。 */ private async resolvePersistedPluginRuntimeState(): Promise { const [plugins, installations] = await Promise.all([ this.pluginRepository.find(), this.installationRepository.find(), ]); const pluginsById = new Map( plugins.map((plugin) => [plugin.id, plugin] as const), ); const enabledInstallationsByPluginKey = new Map< string, QqbotPluginInstallation >(); for (const installation of installations) { if (installation.status !== 'enabled') continue; const plugin = pluginsById.get(installation.pluginId); if (!plugin?.pluginKey) continue; enabledInstallationsByPluginKey.set(plugin.pluginKey, installation); } return { enabledInstallationsByPluginKey, inactivePluginKeys: new Set( resolveInactivePluginKeys(plugins, installations), ), }; } /** * 执行 QQBot 插件平台流程。 * @param pluginKey - pluginKey 输入;驱动 `this.resolveActivePluginKey()` 的 插件平台步骤。 */ private requireActiveWorker(pluginKey: string) { const resolvedPluginKey = this.resolveActivePluginKey(pluginKey); const workerContext = this.activeWorkersByPluginKey.get(resolvedPluginKey); if (!workerContext) { throwVbenError(`QQBot 插件运行时未启用:${pluginKey}`); } return workerContext; } /** * 解析Active Plugin Key。 * @param pluginKey - pluginKey 输入;驱动 `activeWorkerPluginAliases.get()` 的 插件平台步骤。 */ private resolveActivePluginKey(pluginKey: string) { return this.activeWorkerPluginAliases.get(pluginKey) || pluginKey; } /** * Lists active command worker contexts filtered by canonical or legacy plugin key. * @param pluginKey - Optional package or legacy key supplied by Admin or compatibility callers. * @returns Active worker contexts whose manifests expose command operations. */ private listActiveWorkerCommandContexts( pluginKey?: string, ): ActiveWorkerContext[] { const resolvedPluginKey = pluginKey ? this.resolveActivePluginKey(pluginKey) : undefined; return [...this.activeWorkerContexts.values()].filter( (workerContext) => workerContext.manifest.operations.length > 0 && (!resolvedPluginKey || workerContext.pluginKey === resolvedPluginKey), ); } /** * Builds Admin command plugin summaries from active worker manifests. * @param pluginKey - Optional package or legacy key used to filter active worker contexts. * @returns Plugin summaries that do not depend on command registry plugin instances. */ private listActiveWorkerPluginSummaries( pluginKey?: string, ): QqbotPluginSummary[] { return this.listActiveWorkerCommandContexts(pluginKey).map( (workerContext) => ({ description: workerContext.manifest.description, key: workerContext.pluginKey, name: workerContext.manifest.name, operationCount: workerContext.manifest.operations.length, triggerMode: 'command', version: workerContext.manifest.version, }), ); } /** * Normalizes an arbitrary worker health payload into the Admin health contract. * @param workerContext - Active worker context that owns the package key and manifest display data. * @param healthPayload - Worker health response returned by the plugin package runtime. * @returns Command plugin health entry for Admin display. */ private toWorkerPluginHealth( workerContext: ActiveWorkerContext, healthPayload: unknown, ): QqbotPluginHealth { const health = healthPayload && typeof healthPayload === 'object' ? (healthPayload as Record) : {}; return { checkedAt: typeof health.checkedAt === 'string' ? health.checkedAt : formatKtDateTime(new Date()), message: typeof health.message === 'string' ? health.message : undefined, name: typeof health.name === 'string' ? health.name : workerContext.manifest.name, pluginKey: workerContext.pluginKey, status: this.normalizePluginHealthStatus(health.status), triggerMode: 'command', }; } /** * Converts worker health status into the public QQBot plugin health enum. * @param status - Raw status value returned by a plugin health hook. * @returns Supported health status, defaulting to healthy for generic `{ ok: true }` payloads. */ private normalizePluginHealthStatus( status: unknown, ): QqbotPluginHealth['status'] { return status === 'degraded' || status === 'offline' || status === 'healthy' ? status : 'healthy'; } /** * 列出Active Worker Operations。 * @returns QQBot 插件平台查询结果。 */ private listActiveWorkerOperations(): QqbotPluginOperationSummary[] { return [...this.activeWorkerContexts.values()].flatMap((workerContext) => [ ...workerContext.manifest.operations.map((operation) => ({ aliases: operation.aliases, description: operation.description, inputSchema: operation.inputSchema, key: operation.key, name: operation.name, outputSchema: operation.outputSchema, pluginKey: workerContext.pluginKey, timeoutMs: operation.timeoutMs, triggerMode: 'command' as const, })), ...workerContext.manifest.events.map((event) => ({ description: event.description, inputSchema: { triggerType: event.eventName, }, key: event.eventName || event.key, name: event.name, pluginKey: workerContext.pluginKey, triggerMode: 'event' as const, })), ]); } /** * 执行 QQBot 插件平台流程。 * @param installation - installation 输入;使用 `id`、`pluginId` 字段生成结果。 * @param version - version 输入;使用 `manifestJson` 字段生成结果。 * @param worker - worker 输入;驱动 `activeWorkers.set()` 的 插件平台步骤。 */ private async registerActiveWorker( installation: QqbotPluginInstallation, version: QqbotPluginVersion, worker: QqbotPluginWorkerRuntime, ) { const manifest = parseQqbotPluginManifest(version.manifestJson); await this.stopExistingWorkersForManifest(manifest); const workerContext: ActiveWorkerContext = { installationId: installation.id, manifest, pluginId: installation.pluginId, pluginKey: manifest.pluginKey, worker, }; this.activeWorkers.set(installation.id, worker); this.activeWorkerContexts.set(installation.id, workerContext); this.activeWorkersByPluginKey.set(manifest.pluginKey, workerContext); for (const alias of manifest.legacyAliases) { this.activeWorkerPluginAliases.set(alias, manifest.pluginKey); this.activeWorkersByPluginKey.set(alias, workerContext); } this.eventPluginRegistry?.registerRuntimeEvents( manifest.pluginKey, this.buildRuntimeEventDefinitions(manifest), ); await this.syncManifestTasksForInstallation(installation, manifest, true); } /** * Builds Admin-facing event metadata from a worker runtime manifest. * @param manifest - Active worker manifest that owns event handler metadata. * @returns Event plugin definitions grouped under the package plugin key. */ private buildRuntimeEventDefinitions( manifest: QqbotPluginManifest, ): QqbotEventPluginDefinition[] { return manifest.events.map((event) => ({ description: event.description, key: manifest.pluginKey, name: event.name || manifest.name, remark: event.key, triggerType: 'message', version: manifest.version, })); } /** * 更新 QQBot 插件平台状态。 * @param installation - installation 输入;使用 `pluginId`、`id` 字段生成结果。 * @param manifest - manifest 输入;使用 `tasks` 字段生成结果。 * @param scheduleEnabledTasks - 插件任务列表;决定 插件平台条件分支。 */ private async syncManifestTasksForInstallation( installation: QqbotPluginInstallation, manifest: QqbotPluginManifest, scheduleEnabledTasks: boolean, ) { if (!this.taskSynchronizer || !manifest.tasks.length) return []; if ( !this.isPersistablePluginId(installation.pluginId) || !this.isPersistablePluginId(installation.id) ) { return []; } const tasks = await this.taskSynchronizer.syncManifestTasks({ installationId: installation.id, manifestTasks: manifest.tasks, pluginId: installation.pluginId, }); if (scheduleEnabledTasks && this.taskScheduler) { for (const task of tasks) { await this.taskScheduler.syncTaskScheduler(task); } } return tasks; } /** * 停止Existing Workers For Manifest。 * @param manifest - manifest 输入;使用 `pluginKey`、`legacyAliases` 字段生成结果。 */ private async stopExistingWorkersForManifest(manifest: QqbotPluginManifest) { const installationIds = new Set(); for (const pluginKey of [manifest.pluginKey, ...manifest.legacyAliases]) { const workerContext = this.activeWorkersByPluginKey.get(pluginKey); if (!workerContext) { continue; } installationIds.add(workerContext.installationId); } for (const installationId of installationIds) { await this.stopWorker(installationId); } } /** * 执行 QQBot 插件平台流程。 * @param installationId - 插件平台 ID;定位本次读取、更新、删除或关联的插件平台。 */ private unregisterActiveWorker(installationId: string) { const workerContext = this.activeWorkerContexts.get(installationId); this.activeWorkers.delete(installationId); this.activeWorkerContexts.delete(installationId); if (!workerContext) return; this.eventPluginRegistry?.unregisterRuntimeEvents(workerContext.pluginKey); for (const pluginKey of [ workerContext.pluginKey, ...workerContext.manifest.legacyAliases, ]) { if (this.activeWorkersByPluginKey.get(pluginKey) === workerContext) { this.activeWorkersByPluginKey.delete(pluginKey); } } for (const alias of workerContext.manifest.legacyAliases) { if ( this.activeWorkerPluginAliases.get(alias) === workerContext.pluginKey ) { this.activeWorkerPluginAliases.delete(alias); } } } /** * 查询 QQBot 插件平台数据。 * @param pluginId - 插件 ID;定位本次读取、更新、删除或关联的插件。 */ private async getPluginKey(pluginId: string) { const findOne = this.pluginRepository.findOne?.bind(this.pluginRepository); const plugin = findOne ? await findOne({ where: { id: pluginId } }) : null; return plugin?.pluginKey || pluginId; } /** * 停止Worker。 * @param installationId - 插件平台 ID;定位本次读取、更新、删除或关联的插件平台。 */ private async stopWorker(installationId: string) { const worker = this.activeWorkers.get(installationId); if (!worker) return; const workerContext = this.activeWorkerContexts.get(installationId); try { try { await worker.deactivate(); } finally { await this.flushWorkerRuntimeEventsBestEffort(workerContext); } } finally { try { await worker.dispose(); } finally { await this.flushWorkerRuntimeEventsBestEffort(workerContext); this.unregisterActiveWorker(installationId); } } } /** * 停止Workers For Installation。 * @param installation - installation 输入;使用 `pluginId`、`id` 字段生成结果。 */ private async stopWorkersForInstallation( installation: QqbotPluginInstallation, ) { const pluginKey = await this.getPluginKey(installation.pluginId); const installationIds = new Set([installation.id]); const pluginWorkerContext = this.activeWorkersByPluginKey.get(pluginKey); if (pluginWorkerContext) { installationIds.add(pluginWorkerContext.installationId); } for (const installationId of installationIds) { await this.stopWorker(installationId); } } /** * 启动Worker。 * @param installation - installation 输入;使用 `versionId`、`id`、`pluginId` 字段生成结果。 * @param versionOverride - versionOverride 输入;影响 startWorker 的返回值。 */ private async startWorker( installation: QqbotPluginInstallation, versionOverride?: QqbotPluginVersion, ) { if (!this.runtimeFactory) return; const version = versionOverride || (await this.versionRepository.findOne({ where: { id: installation.versionId }, })); if (!version) { throwVbenError('插件版本不存在,无法启动运行时'); } const worker = this.runtimeFactory.create(installation, version); try { await worker.load(version.manifestJson); await worker.activate(); await worker.health(); await this.flushRuntimeEvents( installation.id, installation.pluginId, worker, ); await this.registerActiveWorker( installation, version, worker as QqbotPluginWorkerRuntime, ); } catch (error) { await this.flushRuntimeEvents( installation.id, installation.pluginId, worker, ); try { await worker.dispose(); } finally { await this.flushRuntimeEvents( installation.id, installation.pluginId, worker, ); } throw error; } } /** * 执行 QQBot 插件平台流程。 * @param workerContext - workerContext 输入;使用 `installationId`、`pluginId`、`worker` 字段生成结果。 */ private async flushWorkerRuntimeEvents(workerContext: ActiveWorkerContext) { await this.flushRuntimeEvents( workerContext.installationId, workerContext.pluginId, workerContext.worker, ); } /** * 执行 QQBot 插件平台流程。 * @param workerContext - workerContext 输入;驱动 `this.flushWorkerRuntimeEvents()` 的 插件平台步骤。 */ private async flushWorkerRuntimeEventsBestEffort( workerContext?: ActiveWorkerContext, ) { if (!workerContext) return; try { await this.flushWorkerRuntimeEvents(workerContext); } catch { // Runtime event persistence must not block worker cleanup. } } /** * 执行 QQBot 插件平台流程。 * @param installationId - 插件平台 ID;定位本次读取、更新、删除或关联的插件平台。 * @param pluginId - 插件 ID;定位本次读取、更新、删除或关联的插件。 * @param worker - worker 输入;使用 `drainRuntimeEvents` 字段生成结果。 */ private async flushRuntimeEvents( installationId: string, pluginId: string, worker: Pick, ) { const events = worker.drainRuntimeEvents?.() || []; if (!events.length || !this.isPersistablePluginId(pluginId)) return; await Promise.all( events.map((event: QqbotPluginWorkerRuntimeEvent) => this.runtimeEventRepository.save({ eventType: event.eventType, installationId, level: event.level, pluginId, safeSummary: event.safeSummary, }), ), ); } /** * 判断 QQBot 插件平台条件。 * @param pluginId - 插件 ID;定位本次读取、更新、删除或关联的插件。 */ private isPersistablePluginId(pluginId: string) { return /^\d+$/.test(pluginId); } /** * 执行 QQBot 插件平台流程。 * @param installation - installation 输入;使用 `id`、`pluginId` 字段生成结果。 * @param eventType - eventType 输入;影响 recordRuntimeEvent 的返回值。 * @param level - level 输入;影响 recordRuntimeEvent 的返回值。 * @param safeSummary - safeSummary 输入;影响 recordRuntimeEvent 的返回值。 */ private async recordRuntimeEvent( installation: QqbotPluginInstallation, eventType: string, level: QqbotPluginRuntimeEventLevel = 'info', safeSummary: Record = {}, ) { return this.runtimeEventRepository.save({ eventType, installationId: installation.id, level, pluginId: installation.pluginId, safeSummary, }); } /** * 查询 QQBot 插件平台数据。 * @param context - context 输入;使用 `pluginId` 字段生成结果。 */ private getPluginIdFromContext(context?: Record) { return typeof context?.pluginId === 'string' && context.pluginId ? context.pluginId : null; } /** * 创建 QQBot 插件平台对象或配置。 * @param query - 查询参数 DTO;限定 插件平台分页、搜索或详情查询条件。 */ private buildRuntimeEventTimeFilter(query: RuntimeEventQuery) { if (query.startTime && query.endTime) { return { createTime: Between(query.startTime, query.endTime), }; } if (query.startTime) { return { createTime: MoreThanOrEqual(query.startTime), }; } if (query.endTime) { return { createTime: LessThanOrEqual(query.endTime), }; } return {}; } /** * 执行 QQBot 插件平台流程。 */ private requirePackageReader() { if (!this.packageReader) { throwVbenError('插件包读取器未初始化'); } return this.packageReader; } }