From 4816354344213d6286e54bd76bf2e86248795e76 Mon Sep 17 00:00:00 2001 From: sunlei Date: Sun, 7 Jun 2026 02:46:32 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E5=8F=A3=20BangDream=20?= =?UTF-8?q?=E6=9C=8D=E8=A3=85=E8=B5=84=E6=BA=90=E4=BB=93=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ot-bangdream-tsugu-global-refactor-plan.md | 1 + .../models/costume-resource-repository.ts | 67 +++++++++++++++++++ .../plugins/bangDream/tsugu/models/costume.ts | 26 +++---- .../tsugu/costume-resource-repository.spec.ts | 49 ++++++++++++++ 4 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 src/qqbot/plugins/bangDream/tsugu/models/costume-resource-repository.ts create mode 100644 test/qqbot/plugins/bangDream/tsugu/costume-resource-repository.spec.ts diff --git a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md index 320b739..ed83964 100644 --- a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md +++ b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md @@ -307,6 +307,7 @@ export interface TsuguHook { - 已新增 `models/attribute-resource-repository.ts`,把 `Attribute` 的属性图标 SVG 资源路径从 `bestdoriUrl + downloadFileCache` 收口到 provider-backed repository;`attribute-resource-repository.spec.ts` 覆盖属性图标路径和下载调用,本地 `/查卡 472` 图片 smoke 保持非空输出。 - 已新增 `models/server-resource-repository.ts`,把 `Server` 的服务器图标 SVG 资源路径从 `bestdoriUrl + downloadFileCache` 收口到 provider-backed repository,并把台服本地图标路径改走 asset manifest;`server-resource-repository.spec.ts` 覆盖服务器图标路径、台服本地图标路径和下载调用,本地 `/查卡 472` 图片 smoke 保持非空输出。 - 已新增 `render-blocks/card-art-resource-repository.ts`,把 `card-art.ts` 的卡牌小图/插画边框资源下载从 `bestdoriUrl + downloadFileCache` 收口到 provider-backed repository,`card-art-spec.ts` 改为只生成 `/res/image/...` 相对资源路径;`card-art-resource-repository.spec.ts` 覆盖边框下载调用,本地 `/查卡 472` 图片 smoke 保持非空输出。 +- 已新增 `models/costume-resource-repository.ts`,把 `Costume` 的服装详情请求和 Live2D 缩略图素材路径从 `bestdoriUrl + callAPIAndCacheResponse/downloadFile` 收口到 provider-backed repository;`costume-resource-repository.spec.ts` 覆盖详情路径、`sdchara.png` 路径和非内存缓存下载调用,本地 `/查卡 472` 图片 smoke 保持演出缩略图正常输出。 - 固化:Windows 下不要用反斜杠测试路径直接调用 Jest pattern,容易出现 `Pattern ... - 0 matches`;指定文件测试统一使用 `pnpm exec jest --runInBand --runTestsByPath test/qqbot/plugins/bangDream/tsugu/.spec.ts ...`,路径用正斜杠。 ### Phase 4:搜索 specification 和 matcher diff --git a/src/qqbot/plugins/bangDream/tsugu/models/costume-resource-repository.ts b/src/qqbot/plugins/bangDream/tsugu/models/costume-resource-repository.ts new file mode 100644 index 0000000..3add34f --- /dev/null +++ b/src/qqbot/plugins/bangDream/tsugu/models/costume-resource-repository.ts @@ -0,0 +1,67 @@ +import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider'; +import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider'; +import { + getServerByPriority, + Server, +} from '@/qqbot/plugins/bangDream/tsugu/models/server'; +import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; + +export interface CostumeSdResourceSource { + publishedAt: Array; + sdResourceName: string; +} + +function toServerCode(server: Server | undefined): string { + return server == null ? 'undefined' : Server[server]; +} + +export class CostumeResourceRepository { + constructor( + private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider, + ) {} + + /** + * 获取服装详情。 + * + * @param costumeId - 服装 ID。 + */ + async getDetail(costumeId: number): Promise> { + return await this.provider.getJson>( + `/api/costumes/${costumeId}.json`, + ); + } + + /** + * 获取 Live2D 缩略图资源路径。 + * + * @param source - 服装资源来源字段。 + * @param displayedServerList - 可展示服务器优先级。 + */ + getSdCharacterPath( + source: CostumeSdResourceSource, + displayedServerList: Server[] = globalDefaultServer, + ): string { + const serverCode = toServerCode( + getServerByPriority(source.publishedAt, displayedServerList), + ); + return `/assets/${serverCode}/characters/livesd/${source.sdResourceName}_rip/sdchara.png`; + } + + /** + * 下载 Live2D 缩略图资源。 + * + * @param source - 服装资源来源字段。 + * @param displayedServerList - 可展示服务器优先级。 + */ + async getSdCharacterBuffer( + source: CostumeSdResourceSource, + displayedServerList: Server[] = globalDefaultServer, + ): Promise { + return await this.provider.getAsset( + this.getSdCharacterPath(source, displayedServerList), + { memoryCache: false }, + ); + } +} + +export const costumeResourceRepository = new CostumeResourceRepository(); diff --git a/src/qqbot/plugins/bangDream/tsugu/models/costume.ts b/src/qqbot/plugins/bangDream/tsugu/models/costume.ts index a0bd311..22fc600 100644 --- a/src/qqbot/plugins/bangDream/tsugu/models/costume.ts +++ b/src/qqbot/plugins/bangDream/tsugu/models/costume.ts @@ -1,16 +1,9 @@ -import { callAPIAndCacheResponse } from '@/qqbot/plugins/bangDream/tsugu/data-clients/api-cache-client'; -import { downloadFile } from '@/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client'; -import { - bestdoriUrl, - globalDefaultServer, -} from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; +import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import mainAPI from '@/qqbot/plugins/bangDream/tsugu/models/main-data-store'; -import { - Server, - getServerByPriority, -} from '@/qqbot/plugins/bangDream/tsugu/models/server'; +import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server'; import { Image, loadImage } from 'skia-canvas'; import { stringToNumberArray } from '@/qqbot/plugins/bangDream/tsugu/models/model-utils'; +import { costumeResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/models/costume-resource-repository'; export class Costume { costumeId: number; @@ -48,8 +41,8 @@ export class Costume { if (this.isInitfull) { return; } - const costumeData = await callAPIAndCacheResponse( - `${bestdoriUrl}/api/costumes/${this.costumeId}.json`, + const costumeData = await costumeResourceRepository.getDetail( + this.costumeId, ); this.data = costumeData; this.isExist = true; @@ -71,10 +64,11 @@ export class Costume { displayedServerList: Server[] = globalDefaultServer, ): Promise { if (!displayedServerList) displayedServerList = globalDefaultServer; - const server = getServerByPriority(this.publishedAt, displayedServerList); - const sdCharacterBuffer = await downloadFile( - `${bestdoriUrl}/assets/${Server[server]}/characters/livesd/${this.sdResourceName}_rip/sdchara.png`, - ); + const sdCharacterBuffer = + await costumeResourceRepository.getSdCharacterBuffer( + this, + displayedServerList, + ); return await loadImage(sdCharacterBuffer); } } diff --git a/test/qqbot/plugins/bangDream/tsugu/costume-resource-repository.spec.ts b/test/qqbot/plugins/bangDream/tsugu/costume-resource-repository.spec.ts new file mode 100644 index 0000000..2633785 --- /dev/null +++ b/test/qqbot/plugins/bangDream/tsugu/costume-resource-repository.spec.ts @@ -0,0 +1,49 @@ +import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider'; +import { CostumeResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/models/costume-resource-repository'; +import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server'; + +function createProviderMock(): jest.Mocked { + return { + getAsset: jest.fn(), + getJson: jest.fn(), + getTracker: jest.fn(), + name: 'MockBestdori', + resolveUrl: jest.fn((pathOrUrl) => `https://bestdori.example${pathOrUrl}`), + }; +} + +describe('BangDream costume resource repository', () => { + it('loads costume details through the provider', async () => { + const provider = createProviderMock(); + const detail = { costumeId: 472, sdResourceName: 'sd_001' }; + provider.getJson.mockResolvedValue(detail); + const repository = new CostumeResourceRepository(provider); + + await expect(repository.getDetail(472)).resolves.toBe(detail); + + expect(provider.getJson).toHaveBeenCalledWith('/api/costumes/472.json'); + }); + + it('builds and downloads sd character assets through the provider', async () => { + const provider = createProviderMock(); + const sdCharacterBuffer = Buffer.from('sd-character'); + provider.getAsset.mockResolvedValue(sdCharacterBuffer); + const repository = new CostumeResourceRepository(provider); + const source = { + publishedAt: [100, null, null, 200, null], + sdResourceName: 'sd_001', + }; + + expect(repository.getSdCharacterPath(source, [Server.cn])).toBe( + '/assets/cn/characters/livesd/sd_001_rip/sdchara.png', + ); + await expect( + repository.getSdCharacterBuffer(source, [Server.cn]), + ).resolves.toBe(sdCharacterBuffer); + + expect(provider.getAsset).toHaveBeenCalledWith( + '/assets/cn/characters/livesd/sd_001_rip/sdchara.png', + { memoryCache: false }, + ); + }); +});