diff --git a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md index 3c2eb63..41e78dd 100644 --- a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md +++ b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md @@ -347,6 +347,8 @@ export interface TsuguHook { - 已生成查曲、查活动和查谱面 smoke 图片,验证 theme/spec/manifest 第一段迁移后本地图片输出非空,谱面预览资源路径未断。 - 已新增 `render-blocks/detail-block-builder.ts`,活动详情页先接入 `DetailBlockBuilder` 统一 section、spacer 和 data block 拼装,去掉局部 `pushSection` 和手动分割线维护;`detail-block-builder.spec.ts` 覆盖 section 分割线与 spacer/data block 输出。 - 本地验证时发现 Jest 无法解析 `skia-canvas/lib/v6/index.node`,根因是 `moduleFileExtensions` 未包含 `node`;已把 `.node` 原生扩展解析固化进 Jest 配置,避免后续渲染单测重复卡在同类问题。 +- 卡牌详情页已接入 `DetailBlockBuilder`,卡牌标题、插画、基础字段、缩略图和演出缩略图不再手写数组与分割线;`/查卡 472` 本地 smoke 输出非空长图。 +- `scripts/bangdream-render-smoke.ps1` 已固化图片 smoke 完成判定:先删除旧目标图,轮询 stdout 成功 JSON 和新图片文件;如果 Tsugu 后台 handle 导致 Node 未自然退出,则在图片落盘后清理本次进程并返回成功,避免已成功出图仍卡到超时。 - 线上 `/qqbot/command/test` smoke 已固化为先按 `operationKey` 查询启用命令、传 `commandId`,并保留完整命令文本;避免默认 `preview` selfId 未绑定命令时误报“未匹配到命令”。 ### Phase 6:策略 policy 和时间/档线规则 diff --git a/scripts/bangdream-render-smoke.ps1 b/scripts/bangdream-render-smoke.ps1 index d49eddc..62ba6c5 100644 --- a/scripts/bangdream-render-smoke.ps1 +++ b/scripts/bangdream-render-smoke.ps1 @@ -24,6 +24,7 @@ $LogName = [System.IO.Path]::GetFileNameWithoutExtension($ResolvedOutFile) $StdoutLog = Join-Path $LogDir "$LogName.out.log" $StderrLog = Join-Path $LogDir "$LogName.err.log" Remove-Item -LiteralPath $StdoutLog,$StderrLog -Force -ErrorAction SilentlyContinue +Remove-Item -LiteralPath $ResolvedOutFile -Force -ErrorAction SilentlyContinue $Payload = @{ input = @{ @@ -80,7 +81,30 @@ $Process = Start-Process ` -PassThru ` -WindowStyle Hidden -if (-not $Process.WaitForExit($TimeoutSeconds * 1000)) { +function Test-SmokeCompleted { + if (-not (Test-Path -LiteralPath $ResolvedOutFile)) { return $false } + if ((Get-Item -LiteralPath $ResolvedOutFile).Length -le 0) { return $false } + if (-not (Test-Path -LiteralPath $StdoutLog)) { return $false } + return [bool](Select-String -Path $StdoutLog -Pattern '"bytes"' -Quiet) +} + +$Deadline = (Get-Date).AddSeconds($TimeoutSeconds) +$CompletedByOutput = $false +while (-not $Process.HasExited) { + if (Test-SmokeCompleted) { + $CompletedByOutput = $true + Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue + $Process.WaitForExit(5000) | Out-Null + break + } + if ((Get-Date) -ge $Deadline) { + break + } + Start-Sleep -Milliseconds 500 + $Process.Refresh() +} + +if ((-not $Process.HasExited) -and (-not $CompletedByOutput)) { Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue Write-Output "BangDream smoke timed out and process $($Process.Id) was killed." if (Test-Path $StdoutLog) { Get-Content $StdoutLog } @@ -90,4 +114,8 @@ if (-not $Process.WaitForExit($TimeoutSeconds * 1000)) { if (Test-Path $StdoutLog) { Get-Content $StdoutLog } if (Test-Path $StderrLog) { Get-Content $StderrLog } +if ($CompletedByOutput) { + Write-Output "BangDream smoke output completed; lingering process $($Process.Id) was cleaned up." + exit 0 +} exit $Process.ExitCode diff --git a/src/qqbot/plugins/bangDream/tsugu/command-renderers/card-detail.ts b/src/qqbot/plugins/bangDream/tsugu/command-renderers/card-detail.ts index d5ae393..f98928b 100644 --- a/src/qqbot/plugins/bangDream/tsugu/command-renderers/card-detail.ts +++ b/src/qqbot/plugins/bangDream/tsugu/command-renderers/card-detail.ts @@ -2,11 +2,9 @@ import { Card } from '@/qqbot/plugins/bangDream/tsugu/models/card'; import { Skill } from '@/qqbot/plugins/bangDream/tsugu/models/skill'; import { drawList, - line, drawListByServerList, drawListMerge, } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame'; -import { drawDataBlock } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/data-block'; import { drawCardIllustration } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/card-art'; import { drawSkillInList } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-skill'; import { drawTimeInList } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-time'; @@ -26,17 +24,7 @@ import { globalDefaultServer, serverNameFullList, } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; - -/** - * 在QQBot 图片视图层中推入区块。 - * - * @param list - 待处理列表。 - * @param section - 区块参数。 - */ -function pushSection(list: Array, section: Image | Canvas) { - list.push(section); - list.push(line); -} +import { DetailBlockBuilder } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/detail-block-builder'; /** * 在QQBot 图片视图层中判断对象是否包含指定自有属性。 @@ -52,22 +40,22 @@ function hasOwn(source: object, key: string): boolean { /** * 在QQBot 图片视图层中追加卡牌Illustrations。 * - * @param list - 待处理列表。 + * @param builder - 详情区块构建器。 * @param card - 卡牌参数。 */ async function appendCardIllustrations( - list: Array, + builder: DetailBlockBuilder, card: Card, ): Promise { for (const trainingStatus of card.getTrainingStatusList()) { - list.push( + builder.add( await drawCardIllustration({ card, trainingStatus, isList: true, }), ); - list.push(new Canvas(800, 30)); + builder.addSpacer(30); } } @@ -101,20 +89,19 @@ function shouldShowGachaText( /** * 在QQBot 图片视图层中追加卡牌基础区块列表。 * - * @param list - 待处理列表。 + * @param builder - 详情区块构建器。 * @param card - 卡牌参数。 * @param source - 输入来源对象或数据集合。 * @param displayedServerList - 允许展示或下载资源的服务器优先级列表。 */ async function appendCardBaseSections( - list: Array, + builder: DetailBlockBuilder, card: Card, source, displayedServerList: Server[], ): Promise { //类型 / 卡牌ID - pushSection( - list, + builder.addSection( drawListMerge([ drawList({ key: '类型', text: card.getTypeName() }), drawList({ key: 'ID', text: card.cardId.toString() }), @@ -122,12 +109,11 @@ async function appendCardBaseSections( ); //综合力 - pushSection(list, await drawCardStatInList(card)); + builder.addSection(await drawCardStatInList(card)); //技能 const skill = new Skill(card.skillId); - pushSection( - list, + builder.addSection( await drawSkillInList( { key: '技能', card: card, content: skill }, displayedServerList, @@ -135,22 +121,19 @@ async function appendCardBaseSections( ); //标题 - pushSection( - list, + builder.addSection( await drawListByServerList(card.prefix, '标题', displayedServerList), ); //招募语 if (shouldShowGachaText(card, source, displayedServerList)) { - pushSection( - list, + builder.addSection( await drawListByServerList(card.gachaText, '招募语', displayedServerList), ); } //发售日期 - pushSection( - list, + builder.addSection( await drawTimeInList( { key: '发布日期', @@ -161,8 +144,7 @@ async function appendCardBaseSections( ); //缩略图 - pushSection( - list, + builder.addSection( await drawCardListInList({ key: '缩略图', cardList: [card], @@ -307,14 +289,15 @@ async function drawCardDetail( await card.initFull(); const source = card.source; - const list: Array = []; + const builder = new DetailBlockBuilder(); //标题 - list.push(await drawCardPrefixInList(card, displayedServerList)); - list.push(new Canvas(800, 30)); + builder + .add(await drawCardPrefixInList(card, displayedServerList)) + .addSpacer(30); //插画 - await appendCardIllustrations(list, card); + await appendCardIllustrations(builder, card); /* //乐队 @@ -335,13 +318,13 @@ async function drawCardDetail( list.push(await drawRarityInList({ rarity: card.rarity })) list.push(line) */ - await appendCardBaseSections(list, card, source, displayedServerList); + await appendCardBaseSections(builder, card, source, displayedServerList); //演出缩略图 - list.push(await drawSdCharacterInList(card)); + builder.add(await drawSdCharacterInList(card)); //创建最终输出数组 - const listImage = drawDataBlock({ list }); + const listImage = builder.toDataBlock(); const all = []; all.push(drawTitle('查询', '卡牌')); all.push(listImage);