refactor: 收口 BangDream 卡牌详情区块拼装

This commit is contained in:
sunlei 2026-06-06 22:35:11 +08:00
parent 5a02effd9d
commit 0da6d21d26
3 changed files with 53 additions and 40 deletions

View File

@ -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 和时间/档线规则

View File

@ -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

View File

@ -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<Image | Canvas>, 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<Image | Canvas>,
builder: DetailBlockBuilder,
card: Card,
): Promise<void> {
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<Image | Canvas>,
builder: DetailBlockBuilder,
card: Card,
source,
displayedServerList: Server[],
): Promise<void> {
//类型 / 卡牌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<Image | Canvas> = [];
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);