fix: 按BangDream能力懒加载目录数据

This commit is contained in:
sunlei 2026-06-16 07:18:01 +08:00
parent 4e5ed876c3
commit 0fe3f99c36
24 changed files with 488 additions and 84 deletions

View File

@ -58,9 +58,9 @@ export class BangDreamCommandContext {
}
async checkHealth() {
await waitForBangDreamCatalogReady();
const data = bangdreamCatalogCache as { cards?: unknown; songs?: unknown };
if (!data.songs || !data.cards) {
await waitForBangDreamCatalogReady(['songs']);
const data = bangdreamCatalogCache as { songs?: unknown };
if (!data.songs) {
throw new Error('BangDream 数据配置未加载');
}
fuzzySearch('夏祭り');

View File

@ -11,15 +11,16 @@ import {
sleepBangDreamRuntime,
} from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io';
const bangdreamCatalogCache: Record<string, any> = {};
export type BangDreamCatalogKey = keyof typeof bestdoriApiPath;
const bangdreamCatalogCache: Record<string, any> = Object.fromEntries(
Object.keys(bestdoriApiPath).map((key) => [key, {}]),
);
const REQUIRED_CATALOG_KEYS = [
'cards',
'characters',
'events',
'gacha',
'songs',
];
] as const satisfies readonly BangDreamCatalogKey[];
const DEFAULT_CATALOG_READY_TIMEOUT_MS = 15000;
const catalogLoadPromises = new Map<BangDreamCatalogKey, Promise<void>>();
function getCatalogReadyTimeoutMs(): number {
return normalizeBangDreamPositiveInteger(
@ -28,8 +29,22 @@ function getCatalogReadyTimeoutMs(): number {
);
}
function isCatalogReady(): boolean {
return REQUIRED_CATALOG_KEYS.every((key) => {
function normalizeCatalogKeys(
keys: readonly BangDreamCatalogKey[] = REQUIRED_CATALOG_KEYS,
): BangDreamCatalogKey[] {
return [...new Set(keys)].filter((key) => key in bestdoriApiPath);
}
function isCatalogKeyReady(key: BangDreamCatalogKey): boolean {
const collection = bangdreamCatalogCache[key];
if (!collection) return false;
return typeof collection === 'object'
? Object.keys(collection).length > 0
: true;
}
function isCatalogReady(keys?: readonly BangDreamCatalogKey[]): boolean {
return normalizeCatalogKeys(keys).every((key) => {
const collection = bangdreamCatalogCache[key];
return collection && Object.keys(collection).length > 0;
});
@ -45,91 +60,126 @@ async function rejectAfter(ms: number): Promise<never> {
*
* @param useCache - use缓存参数使
*/
async function loadCatalogData(useCache: boolean = false) {
async function loadCatalogData(
keys?: readonly BangDreamCatalogKey[],
useCache: boolean = false,
) {
const catalogKeys = normalizeCatalogKeys(keys);
logger('catalog', 'loading catalog...');
const promiseAll = Object.keys(bestdoriApiPath).map(async (key) => {
if (useCache) {
return (bangdreamCatalogCache[key] =
await bangdreamBestdoriProvider.getJson(bestdoriApiPath[key], {
cacheTime: 1 / 0,
}));
} else {
try {
return (bangdreamCatalogCache[key] =
await bangdreamBestdoriProvider.getJson(bestdoriApiPath[key]));
} catch {
logger('catalog', `load ${key} failed`);
}
}
});
await Promise.all(promiseAll);
for (const key of catalogKeys) {
await loadCatalogKey(key, useCache);
}
const cardsCnFix =
await bangdreamStaticPatchProvider.readJson<Record<string, unknown>>(
'cards-cn-fix.json',
);
for (const key in cardsCnFix) {
bangdreamCatalogCache['cards'][key] = cardsCnFix[key];
}
const skillsCnFix =
await bangdreamStaticPatchProvider.readJson<Record<string, unknown>>(
'skills-cn-fix.json',
);
for (const key in skillsCnFix) {
bangdreamCatalogCache['skills'][key] = skillsCnFix[key];
}
const areaItemFix =
await bangdreamStaticPatchProvider.readJson<Record<string, unknown>>(
'area-item-fix.json',
);
for (const key in areaItemFix) {
if (bangdreamCatalogCache['areaItems'][key] == undefined) {
bangdreamCatalogCache['areaItems'][key] = areaItemFix[key];
}
}
try {
const songNickname = await bangdreamStaticPatchProvider.readExcelRows<{
Id: number;
Nickname: string;
}>('nickname-song.xlsx');
for (let i = 0; i < songNickname.length; i++) {
const element = songNickname[i];
if (bangdreamCatalogCache['songs'][element['Id'].toString()]) {
bangdreamCatalogCache['songs'][element['Id'].toString()]['nickname'] =
element['Nickname'];
}
}
} catch {
logger('catalog', '读取 nickname-song.xlsx 失败');
}
await applyStaticPatches(catalogKeys);
logger('catalog', 'catalog loaded');
}
let initialLoadPromise: Promise<void> | undefined;
async function loadCatalogKey(key: BangDreamCatalogKey, useCache: boolean) {
if (isCatalogKeyReady(key)) return;
const pending = catalogLoadPromises.get(key);
if (pending) return await pending;
function ensureCatalogInitialLoad() {
if (!initialLoadPromise) {
logger('catalog', 'initializing...');
initialLoadPromise = loadCatalogData(true).then(() => {
logger('catalog', 'initializing done');
});
const promise = (async () => {
if (useCache) {
bangdreamCatalogCache[key] = await bangdreamBestdoriProvider.getJson(
bestdoriApiPath[key],
{
cacheTime: 1 / 0,
},
);
return;
}
try {
bangdreamCatalogCache[key] = await bangdreamBestdoriProvider.getJson(
bestdoriApiPath[key],
);
} catch {
logger('catalog', `load ${key} failed`);
}
})();
catalogLoadPromises.set(key, promise);
try {
await promise;
} finally {
catalogLoadPromises.delete(key);
}
return initialLoadPromise;
}
async function applyStaticPatches(keys: readonly BangDreamCatalogKey[]) {
const keySet = new Set(keys);
if (keySet.has('cards')) {
const cardsCnFix =
await bangdreamStaticPatchProvider.readJson<Record<string, unknown>>(
'cards-cn-fix.json',
);
for (const key in cardsCnFix) {
bangdreamCatalogCache['cards'][key] = cardsCnFix[key];
}
}
if (keySet.has('skills')) {
const skillsCnFix =
await bangdreamStaticPatchProvider.readJson<Record<string, unknown>>(
'skills-cn-fix.json',
);
for (const key in skillsCnFix) {
bangdreamCatalogCache['skills'][key] = skillsCnFix[key];
}
}
if (keySet.has('areaItems')) {
const areaItemFix =
await bangdreamStaticPatchProvider.readJson<Record<string, unknown>>(
'area-item-fix.json',
);
for (const key in areaItemFix) {
if (bangdreamCatalogCache['areaItems'][key] == undefined) {
bangdreamCatalogCache['areaItems'][key] = areaItemFix[key];
}
}
}
if (keySet.has('songs')) {
try {
const songNickname = await bangdreamStaticPatchProvider.readExcelRows<{
Id: number;
Nickname: string;
}>('nickname-song.xlsx');
for (let i = 0; i < songNickname.length; i++) {
const element = songNickname[i];
if (bangdreamCatalogCache['songs'][element['Id'].toString()]) {
bangdreamCatalogCache['songs'][element['Id'].toString()]['nickname'] =
element['Nickname'];
}
}
} catch {
logger('catalog', '读取 nickname-song.xlsx 失败');
}
}
}
async function ensureCatalogInitialLoad(keys?: readonly BangDreamCatalogKey[]) {
const catalogKeys = normalizeCatalogKeys(keys);
if (isCatalogReady(catalogKeys)) return;
logger('catalog', 'initializing...');
await loadCatalogData(catalogKeys, true);
logger('catalog', 'initializing done');
}
/**
* BangDream
*/
export async function waitForBangDreamCatalogReady(): Promise<void> {
if (isCatalogReady()) {
export async function waitForBangDreamCatalogReady(
keys?: readonly BangDreamCatalogKey[],
): Promise<void> {
const catalogKeys = normalizeCatalogKeys(keys);
if (isCatalogReady(catalogKeys)) {
return;
}
await Promise.race([
ensureCatalogInitialLoad(),
ensureCatalogInitialLoad(catalogKeys),
rejectAfter(getCatalogReadyTimeoutMs()),
]);
if (!isCatalogReady()) {
if (!isCatalogReady(catalogKeys)) {
throw new Error('BangDream 主数据未完成关键集合加载');
}
}

View File

@ -46,6 +46,7 @@ type BangDreamManifestOperation = {
};
type BangDreamResolvedOperation = BangDreamManifestOperation & {
catalogKeys?: BangDreamOperationModule['catalogKeys'];
execute: BangDreamOperationModule['execute'];
};
@ -131,6 +132,7 @@ function resolveBangDreamOperations(operations: BangDreamManifestOperation[]) {
operation.key,
{
...operation,
catalogKeys: operationModule.catalogKeys,
execute: operationModule.execute,
},
] as const;
@ -153,9 +155,6 @@ async function executeBangDreamOperation(options: {
await options.lifecycle.beforeParse(operationContext);
try {
operationContext.stage = 'catalog';
await waitForBangDreamCatalogReady();
operationContext.stage = 'operation';
const operation = options.operationsByKey.get(options.operationKey);
if (!operation) {
@ -164,6 +163,9 @@ async function executeBangDreamOperation(options: {
operationContext.handlerName = operation.handlerName;
await options.lifecycle.afterResolve(operationContext);
operationContext.stage = 'catalog';
await waitForBangDreamCatalogReady(operation.catalogKeys);
operationContext.stage = 'handler';
await options.lifecycle.beforeRender(operationContext);
const output = await operation.execute(options.input, options.context);

View File

@ -1,7 +1,9 @@
import { Card } from '@/modules/qqbot/plugins/bangdream/src/domain/card/card.model';
import { BANGDREAM_CARD_ILLUSTRATION_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const cardIllustrationOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_CARD_ILLUSTRATION_CATALOG_KEYS,
handlerName: 'getCardIllustration',
execute: async (input, context) => {
const cardId = context.requireNumber(

View File

@ -1,8 +1,10 @@
import { drawCardDetail } from '@/modules/qqbot/plugins/bangdream/src/domain/card/card-detail.renderer';
import { drawCardList } from '@/modules/qqbot/plugins/bangdream/src/domain/card/card-search.renderer';
import { BANGDREAM_CARD_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const cardSearchOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_CARD_CATALOG_KEYS,
handlerName: 'searchCard',
execute: async (input, context) => {
const query = context.requireText(input, '请提供卡牌关键词或卡牌 ID');

View File

@ -0,0 +1,105 @@
import type { BangDreamCatalogKey } from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
export const BANGDREAM_BAND_CATALOG_KEYS = [
'singer',
'bands',
'characters',
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_SONG_CATALOG_KEYS = [
'songs',
'meta',
...BANGDREAM_BAND_CATALOG_KEYS,
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_SONG_SEARCH_CATALOG_KEYS = [
...BANGDREAM_SONG_CATALOG_KEYS,
'events',
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_CARD_ILLUSTRATION_CATALOG_KEYS = [
'cards',
'skills',
'characters',
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_CARD_CATALOG_KEYS = [
...BANGDREAM_CARD_ILLUSTRATION_CATALOG_KEYS,
'singer',
'bands',
'events',
'gacha',
'costumes',
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_CHARACTER_CATALOG_KEYS = [
'characters',
'singer',
'bands',
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_EVENT_CATALOG_KEYS = [
'events',
'characters',
'singer',
'bands',
'cards',
'skills',
'gacha',
'songs',
'meta',
'degrees',
'deco',
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_EVENT_STAGE_CATALOG_KEYS = [
'events',
'characters',
'songs',
'meta',
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_GACHA_CATALOG_KEYS = [
'gacha',
'cards',
'skills',
'characters',
'singer',
'bands',
'events',
'items',
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_GACHA_SIMULATE_CATALOG_KEYS = [
'gacha',
'cards',
'skills',
'characters',
'singer',
'bands',
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_CUTOFF_BASE_CATALOG_KEYS = [
'events',
'characters',
'rates',
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_CUTOFF_DETAIL_CATALOG_KEYS = [
...BANGDREAM_CUTOFF_BASE_CATALOG_KEYS,
'cards',
'skills',
'singer',
'bands',
'degrees',
] as const satisfies readonly BangDreamCatalogKey[];
export const BANGDREAM_PLAYER_CATALOG_KEYS = [
'cards',
'skills',
'characters',
'singer',
'bands',
'degrees',
'areaItems',
] as const satisfies readonly BangDreamCatalogKey[];

View File

@ -1,8 +1,10 @@
import { drawCharacterDetail } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character-detail.renderer';
import { drawCharacterList } from '@/modules/qqbot/plugins/bangdream/src/domain/character/character-search.renderer';
import { BANGDREAM_CHARACTER_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const characterSearchOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_CHARACTER_CATALOG_KEYS,
handlerName: 'searchCharacter',
execute: async (input, context) => {
const query = context.requireText(input, '请提供角色关键词或角色 ID');

View File

@ -1,8 +1,10 @@
import { drawCutoffAll } from '@/modules/qqbot/plugins/bangdream/src/domain/cutoff/cutoff-all.renderer';
import { getPresentEvent } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event.model';
import { BANGDREAM_CUTOFF_BASE_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const cutoffAllOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_CUTOFF_BASE_CATALOG_KEYS,
handlerName: 'getCutoffAll',
execute: async (input, context) => {
const tokens = context.getTokens(input);

View File

@ -1,6 +1,7 @@
import { drawCutoffDetail } from '@/modules/qqbot/plugins/bangdream/src/domain/cutoff/cutoff-detail.renderer';
import { drawCutoffEventTop } from '@/modules/qqbot/plugins/bangdream/src/domain/cutoff/cutoff-event-top.renderer';
import { getPresentEvent } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event.model';
import { BANGDREAM_CUTOFF_DETAIL_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
const CUTOFF_DETAIL_COMMAND_ALIASES = new Set([
@ -12,6 +13,7 @@ const CUTOFF_DETAIL_COMMAND_ALIASES = new Set([
const DEFAULT_CUTOFF_DETAIL_TIER = 1000;
export const cutoffDetailOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_CUTOFF_DETAIL_CATALOG_KEYS,
handlerName: 'getCutoffDetail',
execute: async (input, context) => {
const tokens = context.getTokens(input);

View File

@ -1,8 +1,10 @@
import { drawCutoffListOfRecentEvent } from '@/modules/qqbot/plugins/bangdream/src/domain/cutoff/cutoff-recent.renderer';
import { getPresentEvent } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event.model';
import { BANGDREAM_CUTOFF_BASE_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const cutoffRecentOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_CUTOFF_BASE_CATALOG_KEYS,
handlerName: 'getCutoffRecent',
execute: async (input, context) => {
const tokens = context.getTokens(input);

View File

@ -1,8 +1,10 @@
import { drawEventDetail } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event-detail.renderer';
import { drawEventList } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event-search.renderer';
import { BANGDREAM_EVENT_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const eventSearchOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_EVENT_CATALOG_KEYS,
handlerName: 'searchEvent',
execute: async (input, context) => {
const query = context.requireText(input, '请提供活动关键词或活动 ID');

View File

@ -1,8 +1,10 @@
import { getPresentEvent } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event.model';
import { drawEventStage } from '@/modules/qqbot/plugins/bangdream/src/domain/event/event-stage.renderer';
import { BANGDREAM_EVENT_STAGE_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const eventStageOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_EVENT_STAGE_CATALOG_KEYS,
expectedImageCount: 5,
handlerName: 'getEventStage',
execute: async (input, context) => {

View File

@ -1,7 +1,9 @@
import { drawGachaDetail } from '@/modules/qqbot/plugins/bangdream/src/domain/gacha/gacha-detail.renderer';
import { BANGDREAM_GACHA_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const gachaSearchOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_GACHA_CATALOG_KEYS,
handlerName: 'searchGacha',
execute: async (input, context) => {
const gachaId = context.requireNumber(

View File

@ -7,9 +7,11 @@ import {
BANGDREAM_GACHA_DEFAULT_SPIN_COUNT,
isBirthdayGachaType,
} from '@/modules/qqbot/plugins/bangdream/src/domain/policy/gacha.policy';
import { BANGDREAM_GACHA_SIMULATE_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const gachaSimulateOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_GACHA_SIMULATE_CATALOG_KEYS,
handlerName: 'simulateGacha',
execute: async (input, context) => {
const tokens = context.getTokens(input);

View File

@ -4,6 +4,7 @@ import type {
BangDreamCommandOutput,
BangDreamOperationHandlerName,
} from '@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream.types';
import type { BangDreamCatalogKey } from '@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache';
export type BangDreamOperationExecute = (
input: BangDreamCommandInput,
@ -11,6 +12,7 @@ export type BangDreamOperationExecute = (
) => Promise<BangDreamCommandOutput>;
export type BangDreamOperationModule = {
catalogKeys?: readonly BangDreamCatalogKey[];
execute: BangDreamOperationExecute;
expectedImageCount?: number;
handlerName: BangDreamOperationHandlerName;

View File

@ -1,7 +1,9 @@
import { drawPlayerDetail } from '@/modules/qqbot/plugins/bangdream/src/domain/player/player-detail.renderer';
import { BANGDREAM_PLAYER_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const playerSearchOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_PLAYER_CATALOG_KEYS,
handlerName: 'searchPlayer',
execute: async (input, context) => {
const tokens = context.getTokens(input);

View File

@ -1,7 +1,9 @@
import { drawSongChart } from '@/modules/qqbot/plugins/bangdream/src/domain/song/song-chart.renderer';
import { BANGDREAM_SONG_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const songChartOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_SONG_CATALOG_KEYS,
handlerName: 'getSongChart',
execute: async (input, context) => {
const tokens = context.getTokens(input);

View File

@ -1,8 +1,10 @@
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
import { drawSongMetaList } from '@/modules/qqbot/plugins/bangdream/src/domain/song/song-meta.renderer';
import { BANGDREAM_SONG_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const songMetaOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_SONG_CATALOG_KEYS,
handlerName: 'getSongMeta',
execute: async (input, context) => {
const mainServer = context.pickMainServer(input, context.getTokens(input));

View File

@ -1,9 +1,11 @@
import { Server } from '@/modules/qqbot/plugins/bangdream/src/domain/catalog/server.model';
import { fuzzySearch } from '@/modules/qqbot/plugins/bangdream/src/domain/search/fuzzy-search';
import { drawSongRandom } from '@/modules/qqbot/plugins/bangdream/src/domain/song/song-random.renderer';
import { BANGDREAM_SONG_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const songRandomOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_SONG_CATALOG_KEYS,
handlerName: 'randomSong',
execute: async (input, context) => {
const query = context.pickText(input);

View File

@ -1,9 +1,11 @@
import { Song } from '@/modules/qqbot/plugins/bangdream/src/domain/song/song.model';
import { drawSongDetail } from '@/modules/qqbot/plugins/bangdream/src/domain/song/song-detail.renderer';
import { drawSongList } from '@/modules/qqbot/plugins/bangdream/src/domain/song/song-search.renderer';
import { BANGDREAM_SONG_SEARCH_CATALOG_KEYS } from '@/modules/qqbot/plugins/bangdream/src/operations/catalog-keys';
import type { BangDreamOperationModule } from '@/modules/qqbot/plugins/bangdream/src/operations/operation';
export const songSearchOperation: BangDreamOperationModule = {
catalogKeys: BANGDREAM_SONG_SEARCH_CATALOG_KEYS,
handlerName: 'searchSong',
execute: async (input, context) => {
const query = context.requireText(input, '请提供歌曲名或歌曲 ID');

View File

@ -0,0 +1,129 @@
import { getBangDreamOperationsByHandlerName } from '@/modules/qqbot/plugins/bangdream/src/operations';
describe('BangDream operation catalog keys', () => {
it('declares the catalog keys required by each operation', () => {
const operations = getBangDreamOperationsByHandlerName();
expectCatalogKeys(operations.get('searchSong')?.catalogKeys, [
'songs',
'meta',
'singer',
'bands',
'characters',
'events',
]);
expectCatalogKeys(operations.get('getSongChart')?.catalogKeys, [
'songs',
'meta',
'singer',
'bands',
'characters',
]);
expectCatalogKeys(operations.get('randomSong')?.catalogKeys, [
'songs',
'meta',
'singer',
'bands',
'characters',
]);
expectCatalogKeys(operations.get('getSongMeta')?.catalogKeys, [
'songs',
'meta',
'singer',
'bands',
'characters',
]);
expectCatalogKeys(operations.get('searchCard')?.catalogKeys, [
'cards',
'skills',
'characters',
'singer',
'bands',
'events',
'gacha',
'costumes',
]);
expectCatalogKeys(operations.get('getCardIllustration')?.catalogKeys, [
'cards',
'skills',
'characters',
]);
expectCatalogKeys(operations.get('searchCharacter')?.catalogKeys, [
'characters',
'singer',
'bands',
]);
expectCatalogKeys(operations.get('searchEvent')?.catalogKeys, [
'events',
'characters',
'singer',
'bands',
'cards',
'skills',
'gacha',
'songs',
'meta',
'degrees',
'deco',
]);
expectCatalogKeys(operations.get('getEventStage')?.catalogKeys, [
'events',
'characters',
'songs',
'meta',
]);
expectCatalogKeys(operations.get('searchPlayer')?.catalogKeys, [
'cards',
'skills',
'characters',
'singer',
'bands',
'degrees',
'areaItems',
]);
expectCatalogKeys(operations.get('searchGacha')?.catalogKeys, [
'gacha',
'cards',
'skills',
'characters',
'singer',
'bands',
'events',
'items',
]);
expectCatalogKeys(operations.get('simulateGacha')?.catalogKeys, [
'gacha',
'cards',
'skills',
'characters',
'singer',
'bands',
]);
expectCatalogKeys(operations.get('getCutoffDetail')?.catalogKeys, [
'events',
'characters',
'rates',
'cards',
'skills',
'singer',
'bands',
'degrees',
]);
expectCatalogKeys(operations.get('getCutoffAll')?.catalogKeys, [
'events',
'characters',
'rates',
]);
expectCatalogKeys(operations.get('getCutoffRecent')?.catalogKeys, [
'events',
'characters',
'rates',
]);
expect(operations.size).toBe(15);
});
});
function expectCatalogKeys(actual: readonly string[] | undefined, keys: string[]) {
expect(actual).toEqual(keys);
}

View File

@ -51,6 +51,17 @@ jest.mock('@/modules/qqbot/plugins/bangdream/src/operations', () => ({
return [
handlerName,
{
catalogKeys:
key === 'bangdream.song.search'
? [
'songs',
'meta',
'singer',
'bands',
'characters',
'events',
]
: undefined,
execute,
handlerName,
},
@ -109,7 +120,14 @@ describe('BangDream package entry', () => {
});
expect(mockContext.refreshDictionaryCache).toHaveBeenCalledTimes(1);
expect(waitForBangDreamCatalogReady).toHaveBeenCalledTimes(1);
expect(waitForBangDreamCatalogReady).toHaveBeenCalledWith([
'songs',
'meta',
'singer',
'bands',
'characters',
'events',
]);
expect(songSearch).toHaveBeenCalledWith(
{ text: '夏祭り' },
mockContext,

View File

@ -0,0 +1,63 @@
describe('BangDream catalog cache partial loading', () => {
it('loads only requested catalog keys and their matching static patches', async () => {
const getJson = jest.fn<
Promise<Record<string, { path: string }>>,
[string, { cacheTime?: number }?]
>(async (path) => ({
1: { path },
}));
const readJson = jest.fn(async () => ({}));
const readExcelRows = jest.fn(async () => []);
jest.resetModules();
jest.doMock(
'@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/bestdori.provider',
() => ({
bangdreamBestdoriProvider: {
getJson,
},
}),
);
jest.doMock(
'@/modules/qqbot/plugins/bangdream/src/infrastructure/storage/static-patch.provider',
() => ({
bangdreamStaticPatchProvider: {
readExcelRows,
readJson,
},
}),
);
jest.doMock(
'@/modules/qqbot/plugins/bangdream/src/application/bangdream-logger',
() => ({
logger: jest.fn(),
}),
);
jest.doMock(
'@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io',
() => ({
readBangDreamRuntimeConfig: jest.fn(() => undefined),
sleepBangDreamRuntime: jest.fn(() => new Promise(() => undefined)),
}),
);
const { BANGDREAM_BESTDORI_API_PATHS } = await import(
'@/modules/qqbot/plugins/bangdream/src/domain/common/bangdream-protocol'
);
const { waitForBangDreamCatalogReady } = await import(
'@/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache'
);
await waitForBangDreamCatalogReady(['songs', 'meta']);
expect(getJson).toHaveBeenCalledTimes(2);
expect(getJson.mock.calls.map(([path]) => path).sort()).toEqual(
[
BANGDREAM_BESTDORI_API_PATHS.meta,
BANGDREAM_BESTDORI_API_PATHS.songs,
].sort(),
);
expect(readJson).not.toHaveBeenCalled();
expect(readExcelRows).toHaveBeenCalledTimes(1);
});
});

View File

@ -51,9 +51,13 @@ describe('BangDream catalog cache', () => {
});
it('loads each Bestdori catalog path only once during the initial ready wait', async () => {
await waitForBangDreamCatalogReady!();
const catalogPaths = Object.values(BANGDREAM_BESTDORI_API_PATHS);
await waitForBangDreamCatalogReady(
Object.keys(BANGDREAM_BESTDORI_API_PATHS) as Array<
keyof typeof BANGDREAM_BESTDORI_API_PATHS
>,
);
expect(mockGetJson).toHaveBeenCalledTimes(catalogPaths.length);
expect(mockGetJson.mock.calls.map(([path]) => path).sort()).toEqual(
[...catalogPaths].sort(),
@ -66,4 +70,5 @@ describe('BangDream catalog cache', () => {
expect(mockReadJson).toHaveBeenCalledTimes(3);
expect(mockReadExcelRows).toHaveBeenCalledTimes(1);
});
});