diff --git a/src/modules/qqbot/plugins/bangdream/src/application/bangdream-command-context.ts b/src/modules/qqbot/plugins/bangdream/src/application/bangdream-command-context.ts index 5e704ca..02b4a1d 100644 --- a/src/modules/qqbot/plugins/bangdream/src/application/bangdream-command-context.ts +++ b/src/modules/qqbot/plugins/bangdream/src/application/bangdream-command-context.ts @@ -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('夏祭り'); diff --git a/src/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache.ts b/src/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache.ts index be4e66c..878e79f 100644 --- a/src/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache.ts +++ b/src/modules/qqbot/plugins/bangdream/src/application/catalog/bangdream-catalog-cache.ts @@ -11,15 +11,16 @@ import { sleepBangDreamRuntime, } from '@/modules/qqbot/plugins/bangdream/src/infrastructure/integration/runtime-io'; -const bangdreamCatalogCache: Record = {}; +export type BangDreamCatalogKey = keyof typeof bestdoriApiPath; + +const bangdreamCatalogCache: Record = 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>(); 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 { * * @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>( - 'cards-cn-fix.json', - ); - for (const key in cardsCnFix) { - bangdreamCatalogCache['cards'][key] = cardsCnFix[key]; - } - const skillsCnFix = - await bangdreamStaticPatchProvider.readJson>( - 'skills-cn-fix.json', - ); - for (const key in skillsCnFix) { - bangdreamCatalogCache['skills'][key] = skillsCnFix[key]; - } - const areaItemFix = - await bangdreamStaticPatchProvider.readJson>( - '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 | 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>( + 'cards-cn-fix.json', + ); + for (const key in cardsCnFix) { + bangdreamCatalogCache['cards'][key] = cardsCnFix[key]; + } + } + if (keySet.has('skills')) { + const skillsCnFix = + await bangdreamStaticPatchProvider.readJson>( + 'skills-cn-fix.json', + ); + for (const key in skillsCnFix) { + bangdreamCatalogCache['skills'][key] = skillsCnFix[key]; + } + } + if (keySet.has('areaItems')) { + const areaItemFix = + await bangdreamStaticPatchProvider.readJson>( + '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 { - if (isCatalogReady()) { +export async function waitForBangDreamCatalogReady( + keys?: readonly BangDreamCatalogKey[], +): Promise { + 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 主数据未完成关键集合加载'); } } diff --git a/src/modules/qqbot/plugins/bangdream/src/index.ts b/src/modules/qqbot/plugins/bangdream/src/index.ts index 72eb2a5..e3b2efe 100644 --- a/src/modules/qqbot/plugins/bangdream/src/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/index.ts @@ -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); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/card-illustration/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/card-illustration/index.ts index c9d9c16..b52c0b0 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/card-illustration/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/card-illustration/index.ts @@ -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( diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/card-search/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/card-search/index.ts index 19327f4..16ba252 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/card-search/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/card-search/index.ts @@ -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'); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/catalog-keys.ts b/src/modules/qqbot/plugins/bangdream/src/operations/catalog-keys.ts new file mode 100644 index 0000000..b0bff87 --- /dev/null +++ b/src/modules/qqbot/plugins/bangdream/src/operations/catalog-keys.ts @@ -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[]; diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/character-search/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/character-search/index.ts index d932311..551a4da 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/character-search/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/character-search/index.ts @@ -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'); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-all/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-all/index.ts index 9d33efc..c908212 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-all/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-all/index.ts @@ -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); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-detail/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-detail/index.ts index 872874c..a25a050 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-detail/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-detail/index.ts @@ -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); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-recent/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-recent/index.ts index 8c7c512..780b6ef 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-recent/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/cutoff-recent/index.ts @@ -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); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/event-search/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/event-search/index.ts index 3fd10cf..b9f31a3 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/event-search/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/event-search/index.ts @@ -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'); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/event-stage/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/event-stage/index.ts index bf2b6a9..f1b730f 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/event-stage/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/event-stage/index.ts @@ -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) => { diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/gacha-search/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/gacha-search/index.ts index dfb940c..c5eca16 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/gacha-search/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/gacha-search/index.ts @@ -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( diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/gacha-simulate/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/gacha-simulate/index.ts index c4dc698..28fd405 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/gacha-simulate/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/gacha-simulate/index.ts @@ -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); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/operation.ts b/src/modules/qqbot/plugins/bangdream/src/operations/operation.ts index 2d9eb84..720449e 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/operation.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/operation.ts @@ -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; export type BangDreamOperationModule = { + catalogKeys?: readonly BangDreamCatalogKey[]; execute: BangDreamOperationExecute; expectedImageCount?: number; handlerName: BangDreamOperationHandlerName; diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/player-search/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/player-search/index.ts index 4709847..cfa0bbe 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/player-search/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/player-search/index.ts @@ -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); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/song-chart/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/song-chart/index.ts index 3ff9edb..b7e9c7c 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/song-chart/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/song-chart/index.ts @@ -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); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/song-meta/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/song-meta/index.ts index 60efe5f..4c6475e 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/song-meta/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/song-meta/index.ts @@ -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)); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/song-random/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/song-random/index.ts index cdb0662..7f11dca 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/song-random/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/song-random/index.ts @@ -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); diff --git a/src/modules/qqbot/plugins/bangdream/src/operations/song-search/index.ts b/src/modules/qqbot/plugins/bangdream/src/operations/song-search/index.ts index ddbc93f..1f27fe8 100644 --- a/src/modules/qqbot/plugins/bangdream/src/operations/song-search/index.ts +++ b/src/modules/qqbot/plugins/bangdream/src/operations/song-search/index.ts @@ -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'); diff --git a/test/qqbot/plugins/bangdream/application/bangdream-operation-catalog-keys.spec.ts b/test/qqbot/plugins/bangdream/application/bangdream-operation-catalog-keys.spec.ts new file mode 100644 index 0000000..5914308 --- /dev/null +++ b/test/qqbot/plugins/bangdream/application/bangdream-operation-catalog-keys.spec.ts @@ -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); +} diff --git a/test/qqbot/plugins/bangdream/application/bangdream-package-entry.spec.ts b/test/qqbot/plugins/bangdream/application/bangdream-package-entry.spec.ts index c6c9706..8e4c205 100644 --- a/test/qqbot/plugins/bangdream/application/bangdream-package-entry.spec.ts +++ b/test/qqbot/plugins/bangdream/application/bangdream-package-entry.spec.ts @@ -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, diff --git a/test/qqbot/plugins/bangdream/catalog/catalog-cache-partial.spec.ts b/test/qqbot/plugins/bangdream/catalog/catalog-cache-partial.spec.ts new file mode 100644 index 0000000..cd732bd --- /dev/null +++ b/test/qqbot/plugins/bangdream/catalog/catalog-cache-partial.spec.ts @@ -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>, + [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); + }); +}); diff --git a/test/qqbot/plugins/bangdream/catalog/catalog-cache.spec.ts b/test/qqbot/plugins/bangdream/catalog/catalog-cache.spec.ts index cd0aac0..8ab0a97 100644 --- a/test/qqbot/plugins/bangdream/catalog/catalog-cache.spec.ts +++ b/test/qqbot/plugins/bangdream/catalog/catalog-cache.spec.ts @@ -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); }); + });