kt-template-online-api/src/modules/qqbot/plugin-platform/application/plugin-platform.service.ts

1536 lines
52 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, QqbotPluginInstallation>;
inactivePluginKeys: Set<string>;
};
@Injectable()
export class QqbotPluginPlatformService
implements OnModuleInit, QqbotPluginExecutionPort
{
private readonly activeWorkers = new Map<string, QqbotPluginWorkerRuntime>();
private readonly activeWorkerContexts = new Map<
string,
ActiveWorkerContext
>();
private readonly activeWorkersByPluginKey = new Map<
string,
ActiveWorkerContext
>();
private readonly activeWorkerPluginAliases = new Map<string, string>();
/**
* 初始化 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<QqbotPlugin>,
@InjectRepository(QqbotPluginVersion)
private readonly versionRepository: Repository<QqbotPluginVersion>,
@InjectRepository(QqbotPluginInstallation)
private readonly installationRepository: Repository<QqbotPluginInstallation>,
@InjectRepository(QqbotPluginOperation)
private readonly operationRepository: Repository<QqbotPluginOperation>,
@InjectRepository(QqbotPluginEventHandler)
private readonly eventHandlerRepository: Repository<QqbotPluginEventHandler>,
@InjectRepository(QqbotPluginAccountBinding)
private readonly accountBindingRepository: Repository<QqbotPluginAccountBinding>,
@InjectRepository(QqbotPluginConfig)
private readonly configRepository: Repository<QqbotPluginConfig>,
@InjectRepository(QqbotPluginAsset)
private readonly assetRepository: Repository<QqbotPluginAsset>,
@InjectRepository(QqbotPluginRuntimeEvent)
private readonly runtimeEventRepository: Repository<QqbotPluginRuntimeEvent>,
@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<QqbotPluginSummary[]> {
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<QqbotPluginHealth[]> {
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<string, unknown>).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<string, unknown>;
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<number> {
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<string, unknown>;
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<QqbotPluginInstallation> = {};
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<PersistedPluginRuntimeState> {
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<string, unknown>)
: {};
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<string>();
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<QqbotPluginWorkerRuntime, 'drainRuntimeEvents'>,
) {
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<string, unknown> = {},
) {
return this.runtimeEventRepository.save({
eventType,
installationId: installation.id,
level,
pluginId: installation.pluginId,
safeSummary,
});
}
/**
* 查询 QQBot 插件平台数据。
* @param context - context 输入;使用 `pluginId` 字段生成结果。
*/
private getPluginIdFromContext(context?: Record<string, any>) {
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;
}
}