feat: 增加QQBot插件定时任务持久化接口
This commit is contained in:
parent
250bb654f0
commit
7775fd580d
@ -875,6 +875,53 @@ CREATE TABLE IF NOT EXISTS qqbot_plugin_runtime_event (
|
||||
KEY idx_qqbot_plugin_runtime_event_plugin (plugin_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qqbot_plugin_task (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
plugin_id BIGINT NOT NULL,
|
||||
installation_id BIGINT NOT NULL,
|
||||
task_key VARCHAR(128) NOT NULL,
|
||||
task_name VARCHAR(128) NOT NULL,
|
||||
handler_name VARCHAR(128) NOT NULL,
|
||||
description TEXT NULL,
|
||||
default_cron VARCHAR(64) NOT NULL,
|
||||
cron_expression VARCHAR(64) NOT NULL,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
timeout_ms INT NOT NULL,
|
||||
runtime_status VARCHAR(32) NOT NULL DEFAULT 'idle',
|
||||
last_run_id BIGINT NULL,
|
||||
last_run_at DATETIME NULL,
|
||||
last_status VARCHAR(32) NULL,
|
||||
last_error TEXT NULL,
|
||||
last_duration_ms INT NULL,
|
||||
next_run_at DATETIME NULL,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uk_qqbot_plugin_task (installation_id, task_key),
|
||||
KEY idx_qqbot_plugin_task_plugin (plugin_id),
|
||||
KEY idx_qqbot_plugin_task_enabled (enabled),
|
||||
KEY idx_qqbot_plugin_task_status (runtime_status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS qqbot_plugin_task_run (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
task_id BIGINT NOT NULL,
|
||||
plugin_id BIGINT NOT NULL,
|
||||
installation_id BIGINT NOT NULL,
|
||||
task_key VARCHAR(128) NOT NULL,
|
||||
trigger_type VARCHAR(32) NOT NULL,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
job_id VARCHAR(191) NULL,
|
||||
started_at DATETIME NULL,
|
||||
finished_at DATETIME NULL,
|
||||
duration_ms INT NULL,
|
||||
safe_summary JSON NULL,
|
||||
error_message TEXT NULL,
|
||||
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
KEY idx_qqbot_plugin_task_run_task_time (task_id, create_time),
|
||||
KEY idx_qqbot_plugin_task_run_plugin_time (plugin_id, create_time),
|
||||
KEY idx_qqbot_plugin_task_run_status_time (status, create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS napcat_container (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
account_id BIGINT NULL,
|
||||
|
||||
@ -11,6 +11,8 @@ SELECT 'napcat_account_binding' AS table_name, COUNT(*) AS row_count FROM napcat
|
||||
SELECT 'napcat_login_session' AS table_name, COUNT(*) AS row_count FROM napcat_login_session;
|
||||
SELECT 'napcat_login_challenge' AS table_name, COUNT(*) AS row_count FROM napcat_login_challenge;
|
||||
SELECT 'napcat_runtime_cleanup' AS table_name, COUNT(*) AS row_count FROM napcat_runtime_cleanup;
|
||||
SELECT 'qqbot_plugin_task' AS table_name, COUNT(*) AS row_count FROM qqbot_plugin_task;
|
||||
SELECT 'qqbot_plugin_task_run' AS table_name, COUNT(*) AS row_count FROM qqbot_plugin_task_run;
|
||||
|
||||
SELECT 'seed_admin_user' AS check_name, COUNT(*) AS matched_rows
|
||||
FROM admin_user
|
||||
|
||||
@ -1,2 +1,4 @@
|
||||
export * from './qqbot-plugin-task-cron.validator';
|
||||
export * from './qqbot-plugin-task-manifest.synchronizer';
|
||||
export * from './qqbot-plugin-task.service';
|
||||
export * from './qqbot-plugin-task.types';
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import type { QqbotPluginTaskManifest } from '../../domain/manifest';
|
||||
import { QqbotPluginTask } from '../../infrastructure/persistence';
|
||||
|
||||
export type SyncPluginManifestTasksInput = {
|
||||
installationId: string;
|
||||
manifestTasks: QqbotPluginTaskManifest[];
|
||||
pluginId: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class QqbotPluginTaskManifestSynchronizer {
|
||||
constructor(
|
||||
@InjectRepository(QqbotPluginTask)
|
||||
private readonly taskRepository: Repository<QqbotPluginTask>,
|
||||
) {}
|
||||
|
||||
async syncManifestTasks(input: SyncPluginManifestTasksInput) {
|
||||
const tasks: QqbotPluginTask[] = [];
|
||||
for (const manifestTask of input.manifestTasks) {
|
||||
const existing = await this.taskRepository.findOne({
|
||||
where: {
|
||||
installationId: input.installationId,
|
||||
taskKey: manifestTask.key,
|
||||
},
|
||||
});
|
||||
const task = this.taskRepository.create({
|
||||
...(existing || {}),
|
||||
cronExpression: existing?.cronExpression || manifestTask.defaultCron,
|
||||
defaultCron: manifestTask.defaultCron,
|
||||
description: manifestTask.description || null,
|
||||
enabled: existing?.enabled ?? manifestTask.enabled,
|
||||
handlerName: manifestTask.handlerName,
|
||||
installationId: input.installationId,
|
||||
pluginId: input.pluginId,
|
||||
runtimeStatus:
|
||||
existing?.runtimeStatus ||
|
||||
(manifestTask.enabled ? 'scheduled' : 'disabled'),
|
||||
taskKey: manifestTask.key,
|
||||
taskName: manifestTask.name,
|
||||
timeoutMs: manifestTask.timeoutMs,
|
||||
});
|
||||
tasks.push(await this.taskRepository.save(task));
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,138 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import {
|
||||
Between,
|
||||
FindOptionsWhere,
|
||||
LessThanOrEqual,
|
||||
MoreThanOrEqual,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
import { throwVbenError, ToolsService } from '@/common';
|
||||
import {
|
||||
QqbotPlugin,
|
||||
QqbotPluginTask,
|
||||
QqbotPluginTaskRun,
|
||||
} from '../../infrastructure/persistence';
|
||||
import { requireQqbotPluginTaskCron } from './qqbot-plugin-task-cron.validator';
|
||||
import type {
|
||||
QqbotPluginTaskPageQuery,
|
||||
QqbotPluginTaskRunPageQuery,
|
||||
} from './qqbot-plugin-task.types';
|
||||
|
||||
@Injectable()
|
||||
export class QqbotPluginTaskService {
|
||||
constructor(
|
||||
@InjectRepository(QqbotPluginTask)
|
||||
private readonly taskRepository: Repository<QqbotPluginTask>,
|
||||
@InjectRepository(QqbotPluginTaskRun)
|
||||
private readonly runRepository: Repository<QqbotPluginTaskRun>,
|
||||
@InjectRepository(QqbotPlugin)
|
||||
private readonly pluginRepository: Repository<QqbotPlugin>,
|
||||
private readonly toolsService: ToolsService,
|
||||
) {}
|
||||
|
||||
async pageTasks(query: QqbotPluginTaskPageQuery) {
|
||||
const { pageNo, pageSize, skip } = this.toolsService.getPageParams(query);
|
||||
const where: FindOptionsWhere<QqbotPluginTask> = {};
|
||||
const pluginId = await this.resolvePluginIdFilter(query);
|
||||
if (pluginId) where.pluginId = pluginId;
|
||||
if (query.taskKey) where.taskKey = query.taskKey;
|
||||
if (query.status) where.runtimeStatus = query.status;
|
||||
if (query.enabled !== undefined) {
|
||||
where.enabled = this.toolsService.normalizeBoolean(query.enabled);
|
||||
}
|
||||
const [list, total] = await this.taskRepository.findAndCount({
|
||||
order: { createTime: 'DESC' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
where,
|
||||
});
|
||||
return { list, pageNo, pageSize, total };
|
||||
}
|
||||
|
||||
async getTaskDetail(id: string) {
|
||||
const task = await this.taskRepository.findOne({ where: { id } });
|
||||
if (!task) throwVbenError('插件定时任务不存在');
|
||||
return task;
|
||||
}
|
||||
|
||||
async enableTask(id: string) {
|
||||
await this.taskRepository.update(
|
||||
{ id },
|
||||
{ enabled: true, runtimeStatus: 'scheduled' },
|
||||
);
|
||||
return this.getTaskDetail(id);
|
||||
}
|
||||
|
||||
async disableTask(id: string) {
|
||||
await this.taskRepository.update(
|
||||
{ id },
|
||||
{ enabled: false, runtimeStatus: 'disabled' },
|
||||
);
|
||||
return this.getTaskDetail(id);
|
||||
}
|
||||
|
||||
async updateTaskCron(id: string, body: { cronExpression?: string }) {
|
||||
const cronExpression = requireQqbotPluginTaskCron(body.cronExpression);
|
||||
await this.taskRepository.update({ id }, { cronExpression });
|
||||
return this.getTaskDetail(id);
|
||||
}
|
||||
|
||||
async runTaskOnce(id: string, body: { input?: Record<string, unknown> }) {
|
||||
void body;
|
||||
const task = await this.getTaskDetail(id);
|
||||
const run = await this.runRepository.save({
|
||||
installationId: task.installationId,
|
||||
pluginId: task.pluginId,
|
||||
status: 'running',
|
||||
taskId: task.id,
|
||||
taskKey: task.taskKey,
|
||||
triggerType: 'manual',
|
||||
});
|
||||
return { jobId: run.jobId || `${run.id || ''}`, taskId: task.id };
|
||||
}
|
||||
|
||||
async pageTaskRuns(id: string, query: QqbotPluginTaskRunPageQuery) {
|
||||
const { pageNo, pageSize, skip } = this.toolsService.getPageParams(query);
|
||||
const where: FindOptionsWhere<QqbotPluginTaskRun> = { taskId: id };
|
||||
if (query.status) where.status = query.status;
|
||||
if (query.triggerType) where.triggerType = query.triggerType;
|
||||
Object.assign(where, this.buildRunTimeFilter(query));
|
||||
const [list, total] = await this.runRepository.findAndCount({
|
||||
order: { createTime: 'DESC' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
where,
|
||||
});
|
||||
return { list, pageNo, pageSize, total };
|
||||
}
|
||||
|
||||
private async resolvePluginIdFilter(query: QqbotPluginTaskPageQuery) {
|
||||
if (query.pluginId) return query.pluginId;
|
||||
if (!query.pluginKey) return undefined;
|
||||
|
||||
const plugin = await this.pluginRepository.findOne({
|
||||
where: { pluginKey: query.pluginKey },
|
||||
});
|
||||
return plugin?.id || '__missing_plugin__';
|
||||
}
|
||||
|
||||
private buildRunTimeFilter(query: QqbotPluginTaskRunPageQuery) {
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { vbenSuccess } from '@/common';
|
||||
import { JwtAuthGuard } from '@/modules/admin/identity/auth/jwt-auth.guard';
|
||||
import { QqbotPluginTaskService } from '../application/task';
|
||||
|
||||
@ApiTags('QQBot - 插件定时任务')
|
||||
@Controller('qqbot/plugin-platform/tasks')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class QqbotPluginPlatformTaskController {
|
||||
constructor(private readonly service: QqbotPluginTaskService) {}
|
||||
|
||||
@Get('page')
|
||||
@ApiOperation({ summary: '插件定时任务分页' })
|
||||
async page(@Query() query: Record<string, unknown>) {
|
||||
return vbenSuccess(await this.service.pageTasks(query));
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '插件定时任务详情' })
|
||||
async detail(@Param('id') id: string) {
|
||||
return vbenSuccess(await this.service.getTaskDetail(id));
|
||||
}
|
||||
|
||||
@Post(':id/enable')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '启用插件定时任务' })
|
||||
async enable(@Param('id') id: string) {
|
||||
return vbenSuccess(await this.service.enableTask(id));
|
||||
}
|
||||
|
||||
@Post(':id/disable')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '停用插件定时任务' })
|
||||
async disable(@Param('id') id: string) {
|
||||
return vbenSuccess(await this.service.disableTask(id));
|
||||
}
|
||||
|
||||
@Post(':id/cron')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '更新插件定时任务 cron' })
|
||||
async updateCron(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { cronExpression?: string },
|
||||
) {
|
||||
return vbenSuccess(await this.service.updateTaskCron(id, body));
|
||||
}
|
||||
|
||||
@Post(':id/run')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '手动运行插件定时任务' })
|
||||
async run(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { input?: Record<string, unknown> },
|
||||
) {
|
||||
return vbenSuccess(await this.service.runTaskOnce(id, body));
|
||||
}
|
||||
|
||||
@Get(':id/runs')
|
||||
@ApiOperation({ summary: '插件定时任务运行记录分页' })
|
||||
async runs(@Param('id') id: string, @Query() query: Record<string, unknown>) {
|
||||
return vbenSuccess(await this.service.pageTaskRuns(id, query));
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ export const QQBOT_PLUGIN_PLATFORM_DOMAIN_CONTRACT = {
|
||||
installLocal: '/qqbot/plugin-platform/install-local',
|
||||
installations: '/qqbot/plugin-platform/installations',
|
||||
runtimeEvents: '/qqbot/plugin-platform/runtime-events',
|
||||
tasks: '/qqbot/plugin-platform/tasks',
|
||||
validate: '/qqbot/plugin-platform/validate',
|
||||
},
|
||||
tables: [
|
||||
@ -16,5 +17,7 @@ export const QQBOT_PLUGIN_PLATFORM_DOMAIN_CONTRACT = {
|
||||
'qqbot_plugin_config',
|
||||
'qqbot_plugin_asset',
|
||||
'qqbot_plugin_runtime_event',
|
||||
'qqbot_plugin_task',
|
||||
'qqbot_plugin_task_run',
|
||||
],
|
||||
} as const;
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
ensureSnowflakeId,
|
||||
KtCreateDateColumn,
|
||||
KtDateTime,
|
||||
KtDateTimeColumn,
|
||||
KtUpdateDateColumn,
|
||||
} from '@/common';
|
||||
|
||||
@ -25,6 +26,21 @@ export type QqbotPluginRuntimeStatus =
|
||||
|
||||
export type QqbotPluginRuntimeEventLevel = 'error' | 'info' | 'warn';
|
||||
|
||||
export type QqbotPluginTaskRuntimeStatus =
|
||||
| 'disabled'
|
||||
| 'failed'
|
||||
| 'idle'
|
||||
| 'running'
|
||||
| 'scheduled';
|
||||
|
||||
export type QqbotPluginTaskRunStatus =
|
||||
| 'failed'
|
||||
| 'running'
|
||||
| 'skipped'
|
||||
| 'success';
|
||||
|
||||
export type QqbotPluginTaskTriggerType = 'bootstrap' | 'manual' | 'schedule';
|
||||
|
||||
@Entity('qqbot_plugin')
|
||||
@Index('uk_qqbot_plugin_key', ['pluginKey'], { unique: true })
|
||||
export class QqbotPlugin {
|
||||
@ -291,6 +307,133 @@ export class QqbotPluginRuntimeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
@Entity('qqbot_plugin_task')
|
||||
@Index('uk_qqbot_plugin_task', ['installationId', 'taskKey'], {
|
||||
unique: true,
|
||||
})
|
||||
@Index('idx_qqbot_plugin_task_plugin', ['pluginId'])
|
||||
@Index('idx_qqbot_plugin_task_enabled', ['enabled'])
|
||||
@Index('idx_qqbot_plugin_task_status', ['runtimeStatus'])
|
||||
export class QqbotPluginTask {
|
||||
@PrimaryColumn({ type: 'bigint' })
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'plugin_id', type: 'bigint' })
|
||||
pluginId: string;
|
||||
|
||||
@Column({ name: 'installation_id', type: 'bigint' })
|
||||
installationId: string;
|
||||
|
||||
@Column({ length: 128, name: 'task_key' })
|
||||
taskKey: string;
|
||||
|
||||
@Column({ length: 128, name: 'task_name' })
|
||||
taskName: string;
|
||||
|
||||
@Column({ length: 128, name: 'handler_name' })
|
||||
handlerName: string;
|
||||
|
||||
@Column({ name: 'description', nullable: true, type: 'text' })
|
||||
description: null | string;
|
||||
|
||||
@Column({ length: 64, name: 'default_cron' })
|
||||
defaultCron: string;
|
||||
|
||||
@Column({ length: 64, name: 'cron_expression' })
|
||||
cronExpression: string;
|
||||
|
||||
@Column({ default: true })
|
||||
enabled: boolean;
|
||||
|
||||
@Column({ name: 'timeout_ms', type: 'int' })
|
||||
timeoutMs: number;
|
||||
|
||||
@Column({ length: 32, name: 'runtime_status' })
|
||||
runtimeStatus: QqbotPluginTaskRuntimeStatus;
|
||||
|
||||
@Column({ name: 'last_run_id', nullable: true, type: 'bigint' })
|
||||
lastRunId: null | string;
|
||||
|
||||
@KtDateTimeColumn({ name: 'last_run_at', nullable: true })
|
||||
lastRunAt: null | KtDateTime;
|
||||
|
||||
@Column({ length: 32, name: 'last_status', nullable: true })
|
||||
lastStatus: null | QqbotPluginTaskRunStatus;
|
||||
|
||||
@Column({ name: 'last_error', nullable: true, type: 'text' })
|
||||
lastError: null | string;
|
||||
|
||||
@Column({ name: 'last_duration_ms', nullable: true, type: 'int' })
|
||||
lastDurationMs: null | number;
|
||||
|
||||
@KtDateTimeColumn({ name: 'next_run_at', nullable: true })
|
||||
nextRunAt: null | KtDateTime;
|
||||
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@KtUpdateDateColumn({ name: 'update_time' })
|
||||
updateTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
ensureSnowflakeId(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Entity('qqbot_plugin_task_run')
|
||||
@Index('idx_qqbot_plugin_task_run_task_time', ['taskId', 'createTime'])
|
||||
@Index('idx_qqbot_plugin_task_run_plugin_time', ['pluginId', 'createTime'])
|
||||
@Index('idx_qqbot_plugin_task_run_status_time', ['status', 'createTime'])
|
||||
export class QqbotPluginTaskRun {
|
||||
@PrimaryColumn({ type: 'bigint' })
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'task_id', type: 'bigint' })
|
||||
taskId: string;
|
||||
|
||||
@Column({ name: 'plugin_id', type: 'bigint' })
|
||||
pluginId: string;
|
||||
|
||||
@Column({ name: 'installation_id', type: 'bigint' })
|
||||
installationId: string;
|
||||
|
||||
@Column({ length: 128, name: 'task_key' })
|
||||
taskKey: string;
|
||||
|
||||
@Column({ length: 32, name: 'trigger_type' })
|
||||
triggerType: QqbotPluginTaskTriggerType;
|
||||
|
||||
@Column({ length: 32 })
|
||||
status: QqbotPluginTaskRunStatus;
|
||||
|
||||
@Column({ length: 191, name: 'job_id', nullable: true })
|
||||
jobId: null | string;
|
||||
|
||||
@KtDateTimeColumn({ name: 'started_at', nullable: true })
|
||||
startedAt: null | KtDateTime;
|
||||
|
||||
@KtDateTimeColumn({ name: 'finished_at', nullable: true })
|
||||
finishedAt: null | KtDateTime;
|
||||
|
||||
@Column({ name: 'duration_ms', nullable: true, type: 'int' })
|
||||
durationMs: null | number;
|
||||
|
||||
@Column({ name: 'safe_summary', nullable: true, type: 'simple-json' })
|
||||
safeSummary: null | Record<string, unknown>;
|
||||
|
||||
@Column({ name: 'error_message', nullable: true, type: 'text' })
|
||||
errorMessage: null | string;
|
||||
|
||||
@KtCreateDateColumn({ name: 'create_time' })
|
||||
createTime: KtDateTime;
|
||||
|
||||
@BeforeInsert()
|
||||
createId() {
|
||||
ensureSnowflakeId(this);
|
||||
}
|
||||
}
|
||||
|
||||
export const QQBOT_PLUGIN_PLATFORM_ENTITIES = [
|
||||
QqbotPlugin,
|
||||
QqbotPluginVersion,
|
||||
@ -301,4 +444,6 @@ export const QQBOT_PLUGIN_PLATFORM_ENTITIES = [
|
||||
QqbotPluginConfig,
|
||||
QqbotPluginAsset,
|
||||
QqbotPluginRuntimeEvent,
|
||||
QqbotPluginTask,
|
||||
QqbotPluginTaskRun,
|
||||
] as const;
|
||||
|
||||
@ -11,6 +11,11 @@ import { QqbotEventPluginRegistryService } from './application/registry/qqbot-ev
|
||||
import { QqbotPluginRegistryService } from './application/registry/qqbot-plugin-registry.service';
|
||||
import { QqbotPluginExecutionAdapter } from './application/plugin-execution.adapter';
|
||||
import { QqbotPluginPlatformService } from './application/plugin-platform.service';
|
||||
import {
|
||||
QqbotPluginTaskManifestSynchronizer,
|
||||
QqbotPluginTaskService,
|
||||
} from './application/task';
|
||||
import { QqbotPluginPlatformTaskController } from './contract/plugin-platform-task.controller';
|
||||
import { QqbotPluginPlatformController } from './contract/plugin-platform.controller';
|
||||
import { QqbotPluginController } from './contract/qqbot-plugin.controller';
|
||||
import { QQBOT_PLUGIN_PLATFORM_ENTITIES } from './infrastructure/persistence';
|
||||
@ -25,7 +30,11 @@ import {
|
||||
import { QQBOT_PLUGIN_RUNTIME_FACTORY } from './application/plugin-platform.service';
|
||||
|
||||
@Module({
|
||||
controllers: [QqbotPluginController, QqbotPluginPlatformController],
|
||||
controllers: [
|
||||
QqbotPluginController,
|
||||
QqbotPluginPlatformController,
|
||||
QqbotPluginPlatformTaskController,
|
||||
],
|
||||
exports: [
|
||||
QQBOT_PLUGIN_EXECUTION_PORT,
|
||||
QqbotPluginHttpClientService,
|
||||
@ -53,6 +62,8 @@ import { QQBOT_PLUGIN_RUNTIME_FACTORY } from './application/plugin-platform.serv
|
||||
QqbotBuiltinPluginPackageLoaderService,
|
||||
QqbotBuiltinPluginWorkerRuntimeFactoryService,
|
||||
QqbotPluginPackageReaderService,
|
||||
QqbotPluginTaskManifestSynchronizer,
|
||||
QqbotPluginTaskService,
|
||||
{
|
||||
provide: QQBOT_PLUGIN_EXECUTION_PORT,
|
||||
useExisting: QqbotPluginExecutionAdapter,
|
||||
|
||||
@ -33,6 +33,8 @@ describe('QQBot plugin platform persistence contract', () => {
|
||||
'qqbot_plugin_config',
|
||||
'qqbot_plugin_asset',
|
||||
'qqbot_plugin_runtime_event',
|
||||
'qqbot_plugin_task',
|
||||
'qqbot_plugin_task_run',
|
||||
]);
|
||||
|
||||
for (const table of QQBOT_PLUGIN_PLATFORM_DOMAIN_CONTRACT.tables) {
|
||||
@ -52,6 +54,8 @@ describe('QQBot plugin platform persistence contract', () => {
|
||||
'QqbotPluginConfig',
|
||||
'QqbotPluginAsset',
|
||||
'QqbotPluginRuntimeEvent',
|
||||
'QqbotPluginTask',
|
||||
'QqbotPluginTaskRun',
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -273,7 +273,7 @@ describe('QQBot plugin platform API contract', () => {
|
||||
|
||||
it('keeps TypeORM entity registration aligned with the persistence contract', () => {
|
||||
expect(QqbotPluginPlatformModule).toBeDefined();
|
||||
expect(QQBOT_PLUGIN_PLATFORM_ENTITIES).toHaveLength(9);
|
||||
expect(QQBOT_PLUGIN_PLATFORM_ENTITIES).toHaveLength(11);
|
||||
});
|
||||
|
||||
it('passes runtime-event filters to persistence', async () => {
|
||||
|
||||
@ -0,0 +1,105 @@
|
||||
import { QqbotPluginPlatformTaskController } from '../../../../src/modules/qqbot/plugin-platform/contract/plugin-platform-task.controller';
|
||||
import { ToolsService } from '../../../../src/common';
|
||||
import { QqbotPluginTaskService } from '../../../../src/modules/qqbot/plugin-platform/application/task';
|
||||
import {
|
||||
collectControllerRoutes,
|
||||
routeKey,
|
||||
} from '../../../helpers/controller-route.helper';
|
||||
|
||||
const createRepositoryMock = () => ({
|
||||
findAndCount: jest.fn(async () => [[], 0]),
|
||||
findOne: jest.fn(async () => null),
|
||||
save: jest.fn(async (value) => value),
|
||||
update: jest.fn(async () => ({ affected: 1 })),
|
||||
});
|
||||
|
||||
describe('QQBot plugin task API contract', () => {
|
||||
it('exposes task management routes under plugin-platform ownership', () => {
|
||||
expect(
|
||||
collectControllerRoutes([QqbotPluginPlatformTaskController]).map(
|
||||
routeKey,
|
||||
),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
'GET /qqbot/plugin-platform/tasks/page',
|
||||
'GET /qqbot/plugin-platform/tasks/:id',
|
||||
'POST /qqbot/plugin-platform/tasks/:id/enable',
|
||||
'POST /qqbot/plugin-platform/tasks/:id/disable',
|
||||
'POST /qqbot/plugin-platform/tasks/:id/cron',
|
||||
'POST /qqbot/plugin-platform/tasks/:id/run',
|
||||
'GET /qqbot/plugin-platform/tasks/:id/runs',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('applies task page filters and safe pagination defaults', async () => {
|
||||
const taskRepository = createRepositoryMock();
|
||||
const runRepository = createRepositoryMock();
|
||||
const pluginRepository = createRepositoryMock();
|
||||
pluginRepository.findOne.mockResolvedValue({ id: 'plugin-1' });
|
||||
const service = new QqbotPluginTaskService(
|
||||
taskRepository as any,
|
||||
runRepository as any,
|
||||
pluginRepository as any,
|
||||
new ToolsService(),
|
||||
);
|
||||
|
||||
const page = await service.pageTasks({
|
||||
enabled: 'true',
|
||||
pageNo: 'bad',
|
||||
pageSize: 'also-bad',
|
||||
pluginKey: 'bangdream',
|
||||
status: 'scheduled',
|
||||
taskKey: 'bangdream.bestdori.sync-main-data',
|
||||
});
|
||||
|
||||
expect(pluginRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { pluginKey: 'bangdream' },
|
||||
});
|
||||
expect(taskRepository.findAndCount).toHaveBeenCalledWith({
|
||||
order: { createTime: 'DESC' },
|
||||
skip: 0,
|
||||
take: 10,
|
||||
where: {
|
||||
enabled: true,
|
||||
pluginId: 'plugin-1',
|
||||
runtimeStatus: 'scheduled',
|
||||
taskKey: 'bangdream.bestdori.sync-main-data',
|
||||
},
|
||||
});
|
||||
expect(page).toMatchObject({ pageNo: 1, pageSize: 10, total: 0 });
|
||||
});
|
||||
|
||||
it('applies task run status, trigger, and time-range filters', async () => {
|
||||
const taskRepository = createRepositoryMock();
|
||||
const runRepository = createRepositoryMock();
|
||||
const pluginRepository = createRepositoryMock();
|
||||
const service = new QqbotPluginTaskService(
|
||||
taskRepository as any,
|
||||
runRepository as any,
|
||||
pluginRepository as any,
|
||||
new ToolsService(),
|
||||
);
|
||||
|
||||
await service.pageTaskRuns('task-1', {
|
||||
endTime: '2026-06-17 23:59:59',
|
||||
pageNo: 2,
|
||||
pageSize: 20,
|
||||
startTime: '2026-06-17 00:00:00',
|
||||
status: 'success',
|
||||
triggerType: 'schedule',
|
||||
});
|
||||
|
||||
expect(runRepository.findAndCount).toHaveBeenCalledWith({
|
||||
order: { createTime: 'DESC' },
|
||||
skip: 20,
|
||||
take: 20,
|
||||
where: {
|
||||
createTime: expect.any(Object),
|
||||
status: 'success',
|
||||
taskId: 'task-1',
|
||||
triggerType: 'schedule',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,37 @@
|
||||
import { getMetadataArgsStorage } from 'typeorm';
|
||||
import {
|
||||
QQBOT_PLUGIN_PLATFORM_DOMAIN_CONTRACT,
|
||||
QQBOT_PLUGIN_PLATFORM_ENTITIES,
|
||||
QqbotPluginTask,
|
||||
QqbotPluginTaskRun,
|
||||
} from '../../../../src/modules/qqbot/plugin-platform/infrastructure/persistence';
|
||||
import { readRefactorV3SqlSchema } from '../../../helpers/sql-schema.helper';
|
||||
|
||||
describe('QQBot plugin task persistence contract', () => {
|
||||
const schema = readRefactorV3SqlSchema();
|
||||
|
||||
it('declares task tables in SQL and entity registry', () => {
|
||||
expect(QQBOT_PLUGIN_PLATFORM_DOMAIN_CONTRACT.tables).toEqual(
|
||||
expect.arrayContaining(['qqbot_plugin_task', 'qqbot_plugin_task_run']),
|
||||
);
|
||||
expect(QQBOT_PLUGIN_PLATFORM_ENTITIES).toEqual(
|
||||
expect.arrayContaining([QqbotPluginTask, QqbotPluginTaskRun]),
|
||||
);
|
||||
expect(schema.hasTable('qqbot_plugin_task')).toBe(true);
|
||||
expect(schema.hasTable('qqbot_plugin_task_run')).toBe(true);
|
||||
});
|
||||
|
||||
it('maps task entity columns to SQL schema', () => {
|
||||
for (const entity of [QqbotPluginTask, QqbotPluginTaskRun]) {
|
||||
const tableName = getMetadataArgsStorage().tables.find(
|
||||
(table) => table.target === entity,
|
||||
)?.name;
|
||||
const columns = getMetadataArgsStorage()
|
||||
.columns.filter((column) => column.target === entity)
|
||||
.map((column) => `${column.options.name || column.propertyName}`);
|
||||
|
||||
expect(tableName).toBeTruthy();
|
||||
schema.expectTableColumns(tableName || '', columns);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user