refactor: 接入 BangDream Tsugu 执行入口
This commit is contained in:
parent
47a48fb311
commit
4eec2d6d05
@ -389,6 +389,14 @@ export interface TsuguHook {
|
|||||||
- 每次命令执行能记录 operation、耗时、图片数、错误。
|
- 每次命令执行能记录 operation、耗时、图片数、错误。
|
||||||
- 线上日志能定位 BangDream 命令的具体失败阶段。
|
- 线上日志能定位 BangDream 命令的具体失败阶段。
|
||||||
|
|
||||||
|
当前进度:
|
||||||
|
|
||||||
|
- 已新增 `tsugu/runtime/hook-registry.ts`,定义 `TsuguHookContext`、`TsuguHook`、`TsuguHookRegistry` 和默认 `TsuguLogHook`,命令执行日志包含 `operation`、`stage`、`handler`、`query`、`imageCount`、`durationMs` 和错误字符串。
|
||||||
|
- 已新增 `renderer/tsugu-application.service.ts` 作为 BangDream Tsugu 应用入口,统一执行主数据 ready 等待、operation registry 查找、handler 调度、hook 触发和错误字符串化。
|
||||||
|
- `QqbotBangDreamClientService` 已改为注入 `TsuguApplicationService`,不再直接调用 renderer;`QqbotBangDreamRendererService` 移除 operation key 调度,只保留字典刷新、健康检查、渲染选项解析和 15 个具体 handler。
|
||||||
|
- `scripts/bangdream-render-smoke.ps1` 已切到 `TsuguApplicationService`,本地 smoke 继续走真实应用入口,避免 facade 改造后验证脚本回退到旧链路。
|
||||||
|
- 已新增 `hook-registry.spec.ts` 和 `tsugu-application.service.spec.ts`,覆盖 hook 顺序、错误 hook、应用入口执行、字典刷新和错误字符串化;本地 Phase 7 smoke 已生成 `phase7-hook-event-50.jpg`、`phase7-hook-cutoff-detail-100-50-cn.jpg`、`phase7-hook-cutoff-recent-100-50-cn.jpg`。
|
||||||
|
|
||||||
## 文件迁移优先级
|
## 文件迁移优先级
|
||||||
|
|
||||||
| 优先级 | 文件/目录 | 原因 |
|
| 优先级 | 文件/目录 | 原因 |
|
||||||
|
|||||||
@ -43,10 +43,13 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { ConfigService } = require("@nestjs/config");
|
const { ConfigService } = require("@nestjs/config");
|
||||||
const payload = JSON.parse(Buffer.from("$PayloadBase64", "base64").toString("utf8"));
|
const payload = JSON.parse(Buffer.from("$PayloadBase64", "base64").toString("utf8"));
|
||||||
|
const { ToolsService } = require("./src/common/services/tool.service");
|
||||||
const { QqbotBangDreamRendererService } = require("./src/qqbot/plugins/bangDream/renderer/qqbot-bangdream-renderer.service");
|
const { QqbotBangDreamRendererService } = require("./src/qqbot/plugins/bangDream/renderer/qqbot-bangdream-renderer.service");
|
||||||
|
const { TsuguApplicationService } = require("./src/qqbot/plugins/bangDream/renderer/tsugu-application.service");
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const service = new QqbotBangDreamRendererService(new ConfigService({}), undefined);
|
const renderer = new QqbotBangDreamRendererService(new ConfigService({}), undefined);
|
||||||
|
const service = new TsuguApplicationService(renderer, new ToolsService());
|
||||||
await service.onApplicationBootstrap();
|
await service.onApplicationBootstrap();
|
||||||
const result = await service.execute(payload.operationKey, payload.input);
|
const result = await service.execute(payload.operationKey, payload.input);
|
||||||
const match = result.replyText.match(/base64:\/\/([A-Za-z0-9+/=]+)/);
|
const match = result.replyText.match(/base64:\/\/([A-Za-z0-9+/=]+)/);
|
||||||
|
|||||||
@ -5,28 +5,31 @@ import type {
|
|||||||
QqbotBangDreamSongSearchInput,
|
QqbotBangDreamSongSearchInput,
|
||||||
QqbotBangDreamSongSummary,
|
QqbotBangDreamSongSummary,
|
||||||
} from './qqbot-bangdream.types';
|
} from './qqbot-bangdream.types';
|
||||||
import { QqbotBangDreamRendererService } from './renderer/qqbot-bangdream-renderer.service';
|
import { TsuguApplicationService } from './renderer/tsugu-application.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QqbotBangDreamClientService {
|
export class QqbotBangDreamClientService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly rendererService: QqbotBangDreamRendererService,
|
private readonly tsuguApplicationService: TsuguApplicationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async checkHealth() {
|
async checkHealth() {
|
||||||
return this.rendererService.checkHealth();
|
return this.tsuguApplicationService.checkHealth();
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(
|
async execute(
|
||||||
operationKey: QqbotBangDreamOperationKey,
|
operationKey: QqbotBangDreamOperationKey,
|
||||||
input: QqbotBangDreamCommandInput,
|
input: QqbotBangDreamCommandInput,
|
||||||
) {
|
) {
|
||||||
return await this.rendererService.execute(operationKey, input);
|
return await this.tsuguApplicationService.execute(operationKey, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchSong(
|
async searchSong(
|
||||||
params: QqbotBangDreamSongSearchInput,
|
params: QqbotBangDreamSongSearchInput,
|
||||||
): Promise<QqbotBangDreamSongSummary> {
|
): Promise<QqbotBangDreamSongSummary> {
|
||||||
return await this.rendererService.execute('bangdream.song.search', params);
|
return await this.tsuguApplicationService.execute(
|
||||||
|
'bangdream.song.search',
|
||||||
|
params,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Injectable, OnApplicationBootstrap, Optional } from '@nestjs/common';
|
import { Injectable, Optional } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { DictService } from '@/admin/dict/dict.service';
|
import { DictService } from '@/admin/dict/dict.service';
|
||||||
import { Card } from '../tsugu/models/card';
|
import { Card } from '../tsugu/models/card';
|
||||||
@ -8,7 +8,10 @@ import { Gacha, getPresentGachaList } from '../tsugu/models/gacha';
|
|||||||
import { Server } from '../tsugu/models/server';
|
import { Server } from '../tsugu/models/server';
|
||||||
import { Song } from '../tsugu/models/song';
|
import { Song } from '../tsugu/models/song';
|
||||||
import mainAPI, { waitForMainDataReady } from '../tsugu/models/main-data-store';
|
import mainAPI, { waitForMainDataReady } from '../tsugu/models/main-data-store';
|
||||||
import { fuzzySearch, type FuzzySearchResult } from '../tsugu/search/fuzzy-search';
|
import {
|
||||||
|
fuzzySearch,
|
||||||
|
type FuzzySearchResult,
|
||||||
|
} from '../tsugu/search/fuzzy-search';
|
||||||
import { drawCardDetail } from '../tsugu/command-renderers/card-detail';
|
import { drawCardDetail } from '../tsugu/command-renderers/card-detail';
|
||||||
import { drawCardList } from '../tsugu/command-renderers/card-list';
|
import { drawCardList } from '../tsugu/command-renderers/card-list';
|
||||||
import { drawCharacterDetail } from '../tsugu/command-renderers/character-detail';
|
import { drawCharacterDetail } from '../tsugu/command-renderers/character-detail';
|
||||||
@ -32,7 +35,7 @@ import {
|
|||||||
BangDreamDictionaryLoader,
|
BangDreamDictionaryLoader,
|
||||||
type BangDreamDictionaryItem,
|
type BangDreamDictionaryItem,
|
||||||
} from '../tsugu/runtime/dictionary-loader';
|
} from '../tsugu/runtime/dictionary-loader';
|
||||||
import { getBangDreamOperationDefinition } from '../tsugu/runtime/operation-registry';
|
import type { QqbotBangDreamOperationHandlerName } from '../tsugu/runtime/operation-registry';
|
||||||
import {
|
import {
|
||||||
BANGDREAM_TSUGU_ENV_KEYS,
|
BANGDREAM_TSUGU_ENV_KEYS,
|
||||||
normalizeBangDreamBoolean,
|
normalizeBangDreamBoolean,
|
||||||
@ -47,7 +50,7 @@ import type {
|
|||||||
const SOURCE_NAME = 'Tsugu BangDream Bot 内置源码';
|
const SOURCE_NAME = 'Tsugu BangDream Bot 内置源码';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QqbotBangDreamRendererService implements OnApplicationBootstrap {
|
export class QqbotBangDreamRendererService {
|
||||||
private readonly dictionaryLoader = new BangDreamDictionaryLoader();
|
private readonly dictionaryLoader = new BangDreamDictionaryLoader();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -56,10 +59,6 @@ export class QqbotBangDreamRendererService implements OnApplicationBootstrap {
|
|||||||
private readonly dictService?: DictService,
|
private readonly dictService?: DictService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async onApplicationBootstrap() {
|
|
||||||
await this.refreshDictionaryCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshDictionaryCache() {
|
async refreshDictionaryCache() {
|
||||||
await this.dictionaryLoader.refresh((dictCode) =>
|
await this.dictionaryLoader.refresh((dictCode) =>
|
||||||
this.fetchDictionaryItems(dictCode),
|
this.fetchDictionaryItems(dictCode),
|
||||||
@ -76,18 +75,13 @@ export class QqbotBangDreamRendererService implements OnApplicationBootstrap {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(
|
async executeOperationHandler(
|
||||||
operationKey: QqbotBangDreamOperationKey,
|
handlerName: QqbotBangDreamOperationHandlerName,
|
||||||
input: QqbotBangDreamCommandInput,
|
input: QqbotBangDreamCommandInput,
|
||||||
) {
|
) {
|
||||||
await waitForMainDataReady();
|
const handler = this[handlerName];
|
||||||
const operation = getBangDreamOperationDefinition(operationKey);
|
|
||||||
if (!operation) {
|
|
||||||
throw new Error(`BangDream 插件能力不存在:${operationKey}`);
|
|
||||||
}
|
|
||||||
const handler = this[operation.handlerName];
|
|
||||||
if (typeof handler !== 'function') {
|
if (typeof handler !== 'function') {
|
||||||
throw new Error(`BangDream 插件能力未绑定执行器:${operationKey}`);
|
throw new Error(`BangDream 插件能力未绑定执行器:${handlerName}`);
|
||||||
}
|
}
|
||||||
return await (
|
return await (
|
||||||
handler as (
|
handler as (
|
||||||
@ -434,9 +428,7 @@ export class QqbotBangDreamRendererService implements OnApplicationBootstrap {
|
|||||||
private pickDisplayedServerList(input: QqbotBangDreamCommandInput) {
|
private pickDisplayedServerList(input: QqbotBangDreamCommandInput) {
|
||||||
const source =
|
const source =
|
||||||
input.displayedServerList ||
|
input.displayedServerList ||
|
||||||
this.configService.get<string>(
|
this.configService.get<string>(BANGDREAM_TSUGU_ENV_KEYS.displayedServers);
|
||||||
BANGDREAM_TSUGU_ENV_KEYS.displayedServers,
|
|
||||||
);
|
|
||||||
const defaultServers = this.dictionaryLoader.getDefaultDisplayedServers();
|
const defaultServers = this.dictionaryLoader.getDefaultDisplayedServers();
|
||||||
if (!source) return defaultServers;
|
if (!source) return defaultServers;
|
||||||
const values = splitBangDreamOptionList(source);
|
const values = splitBangDreamOptionList(source);
|
||||||
|
|||||||
@ -0,0 +1,74 @@
|
|||||||
|
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
||||||
|
import { ToolsService } from '@/common';
|
||||||
|
import type {
|
||||||
|
QqbotBangDreamCommandInput,
|
||||||
|
QqbotBangDreamCommandOutput,
|
||||||
|
QqbotBangDreamOperationKey,
|
||||||
|
} from '../qqbot-bangdream.types';
|
||||||
|
import { waitForMainDataReady } from '../tsugu/models/main-data-store';
|
||||||
|
import {
|
||||||
|
createTsuguHookContext,
|
||||||
|
createTsuguLogHook,
|
||||||
|
TsuguHookRegistry,
|
||||||
|
} from '../tsugu/runtime/hook-registry';
|
||||||
|
import { getBangDreamOperationDefinition } from '../tsugu/runtime/operation-registry';
|
||||||
|
import { QqbotBangDreamRendererService } from './qqbot-bangdream-renderer.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TsuguApplicationService implements OnApplicationBootstrap {
|
||||||
|
private readonly hookRegistry = new TsuguHookRegistry([createTsuguLogHook()]);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly rendererService: QqbotBangDreamRendererService,
|
||||||
|
private readonly toolsService: ToolsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onApplicationBootstrap() {
|
||||||
|
await this.rendererService.refreshDictionaryCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkHealth() {
|
||||||
|
return await this.rendererService.checkHealth();
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
operationKey: QqbotBangDreamOperationKey,
|
||||||
|
input: QqbotBangDreamCommandInput,
|
||||||
|
): Promise<QqbotBangDreamCommandOutput> {
|
||||||
|
const context = createTsuguHookContext(operationKey, input);
|
||||||
|
await this.hookRegistry.beforeParse(context);
|
||||||
|
|
||||||
|
try {
|
||||||
|
context.stage = 'mainData';
|
||||||
|
await waitForMainDataReady();
|
||||||
|
|
||||||
|
context.stage = 'operation';
|
||||||
|
const operation = getBangDreamOperationDefinition(operationKey);
|
||||||
|
if (!operation) {
|
||||||
|
throw new Error(`BangDream 插件能力不存在:${operationKey}`);
|
||||||
|
}
|
||||||
|
context.handlerName = operation.handlerName;
|
||||||
|
await this.hookRegistry.afterResolve(context);
|
||||||
|
|
||||||
|
context.stage = 'handler';
|
||||||
|
await this.hookRegistry.beforeRender(context);
|
||||||
|
const output = await this.rendererService.executeOperationHandler(
|
||||||
|
operation.handlerName,
|
||||||
|
input,
|
||||||
|
);
|
||||||
|
|
||||||
|
context.stage = 'output';
|
||||||
|
context.imageCount = output.imageCount;
|
||||||
|
context.query = output.query || context.query;
|
||||||
|
await this.hookRegistry.afterOutput(context);
|
||||||
|
return output;
|
||||||
|
} catch (err) {
|
||||||
|
const message = this.toolsService.getErrorMessage(
|
||||||
|
err,
|
||||||
|
'BangDream 命令执行失败',
|
||||||
|
);
|
||||||
|
await this.hookRegistry.onError(context, message);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
148
src/qqbot/plugins/bangDream/tsugu/runtime/hook-registry.ts
Normal file
148
src/qqbot/plugins/bangDream/tsugu/runtime/hook-registry.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import type {
|
||||||
|
QqbotBangDreamCommandInput,
|
||||||
|
QqbotBangDreamOperationKey,
|
||||||
|
} from '../../qqbot-bangdream.types';
|
||||||
|
import { logger } from './logger';
|
||||||
|
|
||||||
|
export type TsuguExecutionStage =
|
||||||
|
| 'handler'
|
||||||
|
| 'mainData'
|
||||||
|
| 'operation'
|
||||||
|
| 'output'
|
||||||
|
| 'start';
|
||||||
|
|
||||||
|
export type TsuguHookContext = {
|
||||||
|
handlerName?: string;
|
||||||
|
imageCount?: number;
|
||||||
|
input: QqbotBangDreamCommandInput;
|
||||||
|
operationKey: QqbotBangDreamOperationKey;
|
||||||
|
query?: string;
|
||||||
|
stage: TsuguExecutionStage;
|
||||||
|
startedAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TsuguHook = {
|
||||||
|
afterOutput?: (context: TsuguHookContext) => Promise<void> | void;
|
||||||
|
afterResolve?: (context: TsuguHookContext) => Promise<void> | void;
|
||||||
|
beforeParse?: (context: TsuguHookContext) => Promise<void> | void;
|
||||||
|
beforeRender?: (context: TsuguHookContext) => Promise<void> | void;
|
||||||
|
name: string;
|
||||||
|
onError?: (context: TsuguHookContext, error: unknown) => Promise<void> | void;
|
||||||
|
order?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TsuguSimpleHookMethod =
|
||||||
|
| 'afterOutput'
|
||||||
|
| 'afterResolve'
|
||||||
|
| 'beforeParse'
|
||||||
|
| 'beforeRender';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 维护 Tsugu 命令执行生命周期 hook。
|
||||||
|
*/
|
||||||
|
export class TsuguHookRegistry {
|
||||||
|
private readonly hooks: TsuguHook[];
|
||||||
|
|
||||||
|
constructor(hooks: readonly TsuguHook[] = []) {
|
||||||
|
this.hooks = [...hooks].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeParse(context: TsuguHookContext) {
|
||||||
|
await this.emit('beforeParse', context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterResolve(context: TsuguHookContext) {
|
||||||
|
await this.emit('afterResolve', context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async beforeRender(context: TsuguHookContext) {
|
||||||
|
await this.emit('beforeRender', context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async afterOutput(context: TsuguHookContext) {
|
||||||
|
await this.emit('afterOutput', context);
|
||||||
|
}
|
||||||
|
|
||||||
|
async onError(context: TsuguHookContext, error: unknown) {
|
||||||
|
for (const hook of this.hooks) {
|
||||||
|
await hook.onError?.(context, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async emit(method: TsuguSimpleHookMethod, context: TsuguHookContext) {
|
||||||
|
for (const hook of this.hooks) {
|
||||||
|
const handler = hook[method];
|
||||||
|
if (typeof handler === 'function') {
|
||||||
|
await handler(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建命令执行上下文,供 hook 在全链路中共享。
|
||||||
|
*/
|
||||||
|
export function createTsuguHookContext(
|
||||||
|
operationKey: QqbotBangDreamOperationKey,
|
||||||
|
input: QqbotBangDreamCommandInput,
|
||||||
|
): TsuguHookContext {
|
||||||
|
return {
|
||||||
|
input,
|
||||||
|
operationKey,
|
||||||
|
query: extractTsuguInputText(input),
|
||||||
|
stage: 'start',
|
||||||
|
startedAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认日志 hook。
|
||||||
|
*/
|
||||||
|
export function createTsuguLogHook(): TsuguHook {
|
||||||
|
return {
|
||||||
|
afterOutput: (context) => {
|
||||||
|
logger('operation', formatTsuguHookMessage('success', context));
|
||||||
|
},
|
||||||
|
beforeParse: (context) => {
|
||||||
|
logger('operation', formatTsuguHookMessage('start', context));
|
||||||
|
},
|
||||||
|
name: 'TsuguLogHook',
|
||||||
|
onError: (context, error) => {
|
||||||
|
logger(
|
||||||
|
'operation',
|
||||||
|
`${formatTsuguHookMessage('error', context)} error=${getHookErrorMessage(error)}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTsuguHookMessage(
|
||||||
|
status: 'error' | 'start' | 'success',
|
||||||
|
context: TsuguHookContext,
|
||||||
|
) {
|
||||||
|
const durationMs = Date.now() - context.startedAt;
|
||||||
|
return [
|
||||||
|
`status=${status}`,
|
||||||
|
`operation=${context.operationKey}`,
|
||||||
|
`stage=${context.stage}`,
|
||||||
|
context.handlerName ? `handler=${context.handlerName}` : '',
|
||||||
|
context.query ? `query=${context.query}` : '',
|
||||||
|
context.imageCount === undefined ? '' : `imageCount=${context.imageCount}`,
|
||||||
|
`durationMs=${durationMs}`,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTsuguInputText(input: QqbotBangDreamCommandInput) {
|
||||||
|
const direct = `${input.query || input.text || input.raw || ''}`.trim();
|
||||||
|
if (direct) return direct;
|
||||||
|
return Array.isArray(input.args) ? input.args.join(' ').trim() : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHookErrorMessage(error: unknown) {
|
||||||
|
if (error instanceof Error) return error.message;
|
||||||
|
if (typeof error === 'string') return error;
|
||||||
|
if (error === undefined || error === null) return '';
|
||||||
|
return `${error}`;
|
||||||
|
}
|
||||||
@ -41,6 +41,7 @@ import { QqbotPluginRegistryService } from './plugin/qqbot-plugin-registry.servi
|
|||||||
import { QqbotBangDreamClientService } from './plugins/bangDream/qqbot-bangdream-client.service';
|
import { QqbotBangDreamClientService } from './plugins/bangDream/qqbot-bangdream-client.service';
|
||||||
import { QqbotBangDreamPluginService } from './plugins/bangDream/qqbot-bangdream.plugin';
|
import { QqbotBangDreamPluginService } from './plugins/bangDream/qqbot-bangdream.plugin';
|
||||||
import { QqbotBangDreamRendererService } from './plugins/bangDream/renderer/qqbot-bangdream-renderer.service';
|
import { QqbotBangDreamRendererService } from './plugins/bangDream/renderer/qqbot-bangdream-renderer.service';
|
||||||
|
import { TsuguApplicationService } from './plugins/bangDream/renderer/tsugu-application.service';
|
||||||
import { QqbotFf14ClientService } from './plugins/ff14Market/qqbot-ff14-client.service';
|
import { QqbotFf14ClientService } from './plugins/ff14Market/qqbot-ff14-client.service';
|
||||||
import { QqbotFf14MarketPluginService } from './plugins/ff14Market/qqbot-ff14-market.plugin';
|
import { QqbotFf14MarketPluginService } from './plugins/ff14Market/qqbot-ff14-market.plugin';
|
||||||
import { QqbotFflogsClientService } from './plugins/fflogs/qqbot-fflogs-client.service';
|
import { QqbotFflogsClientService } from './plugins/fflogs/qqbot-fflogs-client.service';
|
||||||
@ -100,6 +101,7 @@ import { QqbotSendService } from './send/qqbot-send.service';
|
|||||||
QqbotBangDreamClientService,
|
QqbotBangDreamClientService,
|
||||||
QqbotBangDreamPluginService,
|
QqbotBangDreamPluginService,
|
||||||
QqbotBangDreamRendererService,
|
QqbotBangDreamRendererService,
|
||||||
|
TsuguApplicationService,
|
||||||
QqbotFf14ClientService,
|
QqbotFf14ClientService,
|
||||||
QqbotFf14MarketPluginService,
|
QqbotFf14MarketPluginService,
|
||||||
QqbotFflogsClientService,
|
QqbotFflogsClientService,
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
jest.mock(
|
jest.mock(
|
||||||
'@/qqbot/plugins/bangDream/renderer/qqbot-bangdream-renderer.service',
|
'@/qqbot/plugins/bangDream/renderer/tsugu-application.service',
|
||||||
() => ({
|
() => ({
|
||||||
QqbotBangDreamRendererService: class QqbotBangDreamRendererService {},
|
TsuguApplicationService: class TsuguApplicationService {},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
import { QqbotBangDreamClientService } from '@/qqbot/plugins/bangDream/qqbot-bangdream-client.service';
|
import { QqbotBangDreamClientService } from '@/qqbot/plugins/bangDream/qqbot-bangdream-client.service';
|
||||||
import type { QqbotBangDreamRendererService } from '@/qqbot/plugins/bangDream/renderer/qqbot-bangdream-renderer.service';
|
import type { TsuguApplicationService } from '@/qqbot/plugins/bangDream/renderer/tsugu-application.service';
|
||||||
|
|
||||||
describe('QqbotBangDreamClientService', () => {
|
describe('QqbotBangDreamClientService', () => {
|
||||||
let service: QqbotBangDreamClientService;
|
let service: QqbotBangDreamClientService;
|
||||||
let rendererService: jest.Mocked<QqbotBangDreamRendererService>;
|
let tsuguApplicationService: jest.Mocked<TsuguApplicationService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
rendererService = {
|
tsuguApplicationService = {
|
||||||
checkHealth: jest.fn().mockReturnValue(true),
|
checkHealth: jest.fn().mockReturnValue(true),
|
||||||
execute: jest.fn().mockResolvedValue({
|
execute: jest.fn().mockResolvedValue({
|
||||||
imageCount: 1,
|
imageCount: 1,
|
||||||
@ -23,19 +23,19 @@ describe('QqbotBangDreamClientService', () => {
|
|||||||
source: 'Tsugu BangDream Bot 内置源码',
|
source: 'Tsugu BangDream Bot 内置源码',
|
||||||
}),
|
}),
|
||||||
} as any;
|
} as any;
|
||||||
service = new QqbotBangDreamClientService(rendererService);
|
service = new QqbotBangDreamClientService(tsuguApplicationService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checks embedded Tsugu renderer health', async () => {
|
it('checks embedded Tsugu application health', async () => {
|
||||||
await expect(service.checkHealth()).resolves.toBe(true);
|
await expect(service.checkHealth()).resolves.toBe(true);
|
||||||
|
|
||||||
expect(rendererService.checkHealth).toHaveBeenCalledTimes(1);
|
expect(tsuguApplicationService.checkHealth).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches song through embedded Tsugu renderer', async () => {
|
it('searches song through embedded Tsugu application facade', async () => {
|
||||||
const result = await service.searchSong({ text: '夏祭り' });
|
const result = await service.searchSong({ text: '夏祭り' });
|
||||||
|
|
||||||
expect(rendererService.execute).toHaveBeenCalledWith(
|
expect(tsuguApplicationService.execute).toHaveBeenCalledWith(
|
||||||
'bangdream.song.search',
|
'bangdream.song.search',
|
||||||
{
|
{
|
||||||
text: '夏祭り',
|
text: '夏祭り',
|
||||||
@ -52,7 +52,7 @@ describe('QqbotBangDreamClientService', () => {
|
|||||||
it('executes other Tsugu open commands through the same plugin facade', async () => {
|
it('executes other Tsugu open commands through the same plugin facade', async () => {
|
||||||
await service.execute('bangdream.card.search', { text: '1399' });
|
await service.execute('bangdream.card.search', { text: '1399' });
|
||||||
|
|
||||||
expect(rendererService.execute).toHaveBeenCalledWith(
|
expect(tsuguApplicationService.execute).toHaveBeenCalledWith(
|
||||||
'bangdream.card.search',
|
'bangdream.card.search',
|
||||||
{
|
{
|
||||||
text: '1399',
|
text: '1399',
|
||||||
@ -61,7 +61,7 @@ describe('QqbotBangDreamClientService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('bubbles embedded Tsugu renderer errors as command errors', async () => {
|
it('bubbles embedded Tsugu renderer errors as command errors', async () => {
|
||||||
rendererService.execute.mockRejectedValueOnce(
|
tsuguApplicationService.execute.mockRejectedValueOnce(
|
||||||
new Error('错误: 没有有效的关键词'),
|
new Error('错误: 没有有效的关键词'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,73 @@
|
|||||||
|
jest.mock(
|
||||||
|
'@/qqbot/plugins/bangDream/renderer/qqbot-bangdream-renderer.service',
|
||||||
|
() => ({
|
||||||
|
QqbotBangDreamRendererService: class QqbotBangDreamRendererService {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock('@/qqbot/plugins/bangDream/tsugu/models/main-data-store', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: {},
|
||||||
|
waitForMainDataReady: jest.fn().mockResolvedValue(undefined),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { ToolsService } from '@/common';
|
||||||
|
import { TsuguApplicationService } from '@/qqbot/plugins/bangDream/renderer/tsugu-application.service';
|
||||||
|
import { waitForMainDataReady } from '@/qqbot/plugins/bangDream/tsugu/models/main-data-store';
|
||||||
|
import type { QqbotBangDreamRendererService } from '@/qqbot/plugins/bangDream/renderer/qqbot-bangdream-renderer.service';
|
||||||
|
|
||||||
|
describe('TsuguApplicationService', () => {
|
||||||
|
let rendererService: jest.Mocked<QqbotBangDreamRendererService>;
|
||||||
|
let service: TsuguApplicationService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
rendererService = {
|
||||||
|
checkHealth: jest.fn().mockResolvedValue(true),
|
||||||
|
executeOperationHandler: jest.fn().mockResolvedValue({
|
||||||
|
imageCount: 1,
|
||||||
|
operationKey: 'bangdream.song.search',
|
||||||
|
query: '夏祭り',
|
||||||
|
replyText: '[CQ:image,file=base64://base64-song-card]',
|
||||||
|
source: 'Tsugu BangDream Bot 内置源码',
|
||||||
|
}),
|
||||||
|
refreshDictionaryCache: jest.fn().mockResolvedValue(undefined),
|
||||||
|
} as any;
|
||||||
|
service = new TsuguApplicationService(rendererService, new ToolsService());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refreshes dictionary cache during application bootstrap', async () => {
|
||||||
|
await service.onApplicationBootstrap();
|
||||||
|
|
||||||
|
expect(rendererService.refreshDictionaryCache).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('executes a registry operation through the renderer handler', async () => {
|
||||||
|
const result = await service.execute('bangdream.song.search', {
|
||||||
|
text: '夏祭り',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(waitForMainDataReady).toHaveBeenCalledTimes(1);
|
||||||
|
expect(rendererService.executeOperationHandler).toHaveBeenCalledWith(
|
||||||
|
'searchSong',
|
||||||
|
{
|
||||||
|
text: '夏祭り',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
imageCount: 1,
|
||||||
|
operationKey: 'bangdream.song.search',
|
||||||
|
query: '夏祭り',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes command errors into string messages', async () => {
|
||||||
|
rendererService.executeOperationHandler.mockRejectedValueOnce(
|
||||||
|
'图片渲染失败',
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.execute('bangdream.event.search', { text: '50' }),
|
||||||
|
).rejects.toThrow('图片渲染失败');
|
||||||
|
});
|
||||||
|
});
|
||||||
66
test/qqbot/plugins/bangDream/tsugu/hook-registry.spec.ts
Normal file
66
test/qqbot/plugins/bangDream/tsugu/hook-registry.spec.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
createTsuguHookContext,
|
||||||
|
TsuguHookRegistry,
|
||||||
|
} from '@/qqbot/plugins/bangDream/tsugu/runtime/hook-registry';
|
||||||
|
|
||||||
|
describe('BangDream Tsugu hook registry', () => {
|
||||||
|
it('emits simple lifecycle hooks by configured order', async () => {
|
||||||
|
const events: string[] = [];
|
||||||
|
const registry = new TsuguHookRegistry([
|
||||||
|
{
|
||||||
|
afterOutput: () => {
|
||||||
|
events.push('afterOutput:2');
|
||||||
|
},
|
||||||
|
beforeParse: () => {
|
||||||
|
events.push('beforeParse:2');
|
||||||
|
},
|
||||||
|
name: 'second',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
afterOutput: () => {
|
||||||
|
events.push('afterOutput:1');
|
||||||
|
},
|
||||||
|
beforeParse: () => {
|
||||||
|
events.push('beforeParse:1');
|
||||||
|
},
|
||||||
|
name: 'first',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const context = createTsuguHookContext('bangdream.song.search', {
|
||||||
|
text: '夏祭り',
|
||||||
|
});
|
||||||
|
|
||||||
|
await registry.beforeParse(context);
|
||||||
|
await registry.afterOutput(context);
|
||||||
|
|
||||||
|
expect(events).toEqual([
|
||||||
|
'beforeParse:1',
|
||||||
|
'beforeParse:2',
|
||||||
|
'afterOutput:1',
|
||||||
|
'afterOutput:2',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes shared context and error to error hooks', async () => {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const registry = new TsuguHookRegistry([
|
||||||
|
{
|
||||||
|
name: 'error-recorder',
|
||||||
|
onError: (context, error) => {
|
||||||
|
errors.push(`${context.operationKey}:${context.stage}:${error}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const context = createTsuguHookContext('bangdream.event.search', {
|
||||||
|
args: ['50'],
|
||||||
|
});
|
||||||
|
context.stage = 'handler';
|
||||||
|
|
||||||
|
await registry.onError(context, '活动渲染失败');
|
||||||
|
|
||||||
|
expect(context.query).toBe('50');
|
||||||
|
expect(errors).toEqual(['bangdream.event.search:handler:活动渲染失败']);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user