fix: 拆分 BangDream 试炼长图输出
This commit is contained in:
parent
4220efa6b2
commit
9bf2b0792e
@ -363,9 +363,10 @@ export interface TsuguHook {
|
||||
- 已新增 `render-blocks/gacha-simulate-spec.ts`,收口抽卡模拟网格宽度、单抽/汇总混排尺寸、重复卡叠层、数量右对齐和卡池横幅布局;`gacha-simulate.ts` 改为消费 spec,抽卡概率和卡池选择逻辑保持不变。
|
||||
- 已新增 `gacha-simulate-spec.spec.ts`,覆盖 10 抽网格、汇总模式、重复卡叠层、计数字样和横幅尺寸;本地生成 `gacha-simulate-spec-10-259.jpg` 和 `gacha-simulate-spec-50-259-after-cache-fix.jpg`,验证抽卡模拟两种布局模式图片输出正常。
|
||||
- 抽卡模拟 50 抽 smoke 暴露资源下载短时失败会把 URL 写入无过期错误缓存,导致后续重试直接输出 `asset error`;已改为只有 HTTP 404 进入错误缓存,超时/网络抖动不污染 URL,并新增 `file-cache-client.spec.ts` 覆盖瞬时失败后可再次下载、404 缓存缺失资源两种路径。
|
||||
- 已新增 `render-blocks/event-stage-spec.ts`,收口试炼列表最大列高、歌曲单元格/封面/ID/难度条尺寸和试炼类型顶部文字规格;`event-stage.ts` 与 `list-event-stage.ts` 改为消费 spec,活动/歌曲选择和输出结构保持不变。
|
||||
- 已新增 `event-stage-spec.spec.ts`,覆盖试炼歌曲行尺寸、类型顶部文字规格和按列高拆分算法;本地生成 `event-stage-spec-310.jpg`、`event-stage-spec-310-meta.jpg`,验证 `/查试炼 310` 与 `/查试炼 310 -m` 图片输出正常。
|
||||
- smoke/Jenkins/远程调试卡点继续按已固化规则处理:同一卡点第二次尝试前必须改变可验证变量;Jenkins 查询 URL 含方括号时用 `curl -g` 或改查 Jenkins home;线上图片 smoke 必须查询 `commandId`、拉回图片、展示图片、查日志并清理本轮临时目录。
|
||||
- 已新增 `render-blocks/event-stage-spec.ts`,收口试炼列表最大列高、歌曲单元格/封面/ID/难度条尺寸、试炼类型顶部文字规格和换列判断;`event-stage.ts` 与 `list-event-stage.ts` 改为消费 spec,活动/歌曲选择和输出结构保持不变。
|
||||
- 线上 smoke 暴露 `/查试炼 310 -m` 将所有列横向拼成一张巨大 canvas 会触发 Pod `OOMKilled exit=137`;已改为边绘制边按列输出多张 CQ 图片,不再创建最终横向巨图,普通版和 meta 版均拆成 5 张输出。
|
||||
- 已新增 `event-stage-spec.spec.ts`,覆盖试炼歌曲行尺寸、类型顶部文字规格、换列判断和按列高拆分算法;本地生成 `event-stage-split-310*.jpg`、`event-stage-split-310-meta*.jpg`,验证 `/查试炼 310` 与 `/查试炼 310 -m` 图片输出正常。
|
||||
- smoke/Jenkins/远程调试卡点继续按已固化规则处理:同一卡点第二次尝试前必须改变可验证变量;Jenkins 查询 URL 含方括号时用 `curl -g` 或改查 Jenkins home;线上图片 smoke 必须查询 `commandId`、拉回图片、展示图片、查日志并清理本轮临时目录;PowerShell 双引号 here-string 中不要写 JS `${...}` 模板字面量,避免被 PowerShell 提前插值;smoke 没有真实图片落盘时必须失败。
|
||||
|
||||
### Phase 6:策略 policy 和时间/档线规则
|
||||
|
||||
|
||||
@ -25,6 +25,9 @@ $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
|
||||
$OutExtension = [System.IO.Path]::GetExtension($ResolvedOutFile)
|
||||
Get-ChildItem -LiteralPath $OutDir -Filter "$LogName-*${OutExtension}" -ErrorAction SilentlyContinue |
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$Payload = @{
|
||||
input = @{
|
||||
@ -53,12 +56,27 @@ const { TsuguApplicationService } = require("./src/qqbot/plugins/bangDream/rende
|
||||
const service = new TsuguApplicationService(renderer, new ToolsService());
|
||||
await service.onApplicationBootstrap();
|
||||
const result = await service.execute(payload.operationKey, payload.input);
|
||||
const match = result.replyText.match(/base64:\/\/([A-Za-z0-9+/=]+)/);
|
||||
if (!match) throw new Error("No image CQ payload");
|
||||
const matches = [...result.replyText.matchAll(/base64:\/\/([A-Za-z0-9+/=]+)/g)];
|
||||
if (matches.length === 0) throw new Error("No image CQ payload");
|
||||
fs.mkdirSync(path.dirname(payload.outFile), { recursive: true });
|
||||
fs.writeFileSync(payload.outFile, Buffer.from(match[1], "base64"));
|
||||
const parsed = path.parse(payload.outFile);
|
||||
const files = matches.map((match, index) => {
|
||||
const outFile =
|
||||
index === 0
|
||||
? payload.outFile
|
||||
: path.join(
|
||||
parsed.dir,
|
||||
parsed.name + "-" + (index + 1) + (parsed.ext || ".jpg"),
|
||||
);
|
||||
fs.writeFileSync(outFile, Buffer.from(match[1], "base64"));
|
||||
return {
|
||||
bytes: fs.statSync(outFile).size,
|
||||
out: outFile,
|
||||
};
|
||||
});
|
||||
process.stdout.write(JSON.stringify({
|
||||
bytes: fs.statSync(payload.outFile).size,
|
||||
bytes: files[0].bytes,
|
||||
files,
|
||||
imageCount: result.imageCount,
|
||||
out: payload.outFile,
|
||||
}, null, 2) + "\n");
|
||||
@ -118,4 +136,8 @@ if ($CompletedByOutput) {
|
||||
Write-Output "BangDream smoke output completed; lingering process $($Process.Id) was cleaned up."
|
||||
exit 0
|
||||
}
|
||||
if (-not (Test-SmokeCompleted)) {
|
||||
Write-Output "BangDream smoke did not produce an image file."
|
||||
exit 1
|
||||
}
|
||||
exit $Process.ExitCode
|
||||
|
||||
@ -12,11 +12,9 @@ import {
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-event-stage';
|
||||
import { outputEasyImages } from '@/qqbot/plugins/bangDream/tsugu/canvas/output';
|
||||
import { drawDataBlock } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/data-block';
|
||||
import {
|
||||
stackImage,
|
||||
stackImageHorizontal,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/image-stack';
|
||||
import { splitEventStageImagesByColumnHeight } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/event-stage-spec';
|
||||
import { stackImage } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/image-stack';
|
||||
import { shouldStartNewEventStageColumn } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/event-stage-spec';
|
||||
import { Canvas } from 'skia-canvas';
|
||||
|
||||
/**
|
||||
* 在QQBot 图片视图层中绘制活动试炼。
|
||||
@ -52,13 +50,14 @@ export async function drawEventStage(
|
||||
return [`错误: 活动stage数据不足`];
|
||||
}
|
||||
|
||||
const all = [];
|
||||
all.push(drawTitle('查试炼', `国服 ID:${eventId} 活动试炼`));
|
||||
const titleImage = drawTitle('查试炼', `国服 ID:${eventId} 活动试炼`);
|
||||
|
||||
//获得活动stage列表
|
||||
const stageList = eventStage.getStageList();
|
||||
|
||||
const eventStagePromises = [];
|
||||
const outputImages: Array<Buffer | string> = [];
|
||||
let currentColumn: Canvas[] = [];
|
||||
let currentHeight = 0;
|
||||
let pageIndex = 0;
|
||||
|
||||
//绘制活动stage,每个stage一个图片
|
||||
/**
|
||||
@ -73,22 +72,32 @@ export async function drawEventStage(
|
||||
]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < stageList.length; i++) {
|
||||
const stage = stageList[i];
|
||||
eventStagePromises.push(drawStageSong(stage));
|
||||
async function flushColumn() {
|
||||
if (currentColumn.length === 0) return;
|
||||
const columnBlock = drawDataBlock({ list: currentColumn });
|
||||
const pageImages =
|
||||
pageIndex === 0 ? [titleImage, columnBlock] : [columnBlock];
|
||||
outputImages.push(...(await outputEasyImages(pageImages, { compress })));
|
||||
currentColumn = [];
|
||||
currentHeight = 0;
|
||||
pageIndex += 1;
|
||||
}
|
||||
|
||||
const eventStageResults = await Promise.all(eventStagePromises);
|
||||
for (const stage of stageList) {
|
||||
const stageImage = await drawStageSong(stage);
|
||||
if (
|
||||
shouldStartNewEventStageColumn(
|
||||
currentHeight,
|
||||
stageImage.height,
|
||||
currentColumn.length,
|
||||
)
|
||||
) {
|
||||
await flushColumn();
|
||||
}
|
||||
currentColumn.push(stageImage);
|
||||
currentHeight += stageImage.height;
|
||||
}
|
||||
|
||||
//将活动stage图片纵向并横向合并
|
||||
const eventStageImageListHorizontal = splitEventStageImagesByColumnHeight(
|
||||
eventStageResults,
|
||||
).map((list) => drawDataBlock({ list }));
|
||||
const eventStageListImage = stackImageHorizontal(
|
||||
eventStageImageListHorizontal,
|
||||
);
|
||||
|
||||
all.push(eventStageListImage);
|
||||
|
||||
return await outputEasyImages(all, { compress });
|
||||
await flushColumn();
|
||||
return outputImages;
|
||||
}
|
||||
|
||||
@ -66,6 +66,23 @@ export function getEventStageSongRowSize(): { height: number; width: number } {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断下一张试炼 stage 图是否应该换到新列。
|
||||
*
|
||||
* @param currentHeight - 当前列高度。
|
||||
* @param nextImageHeight - 下一张 stage 图高度。
|
||||
* @param currentColumnLength - 当前列已有图片数量。
|
||||
* @param maxHeight - 单列最大高度,未传入时使用默认值。
|
||||
*/
|
||||
export function shouldStartNewEventStageColumn(
|
||||
currentHeight: number,
|
||||
nextImageHeight: number,
|
||||
currentColumnLength: number,
|
||||
maxHeight = BANGDREAM_EVENT_STAGE_SPEC.list.maxColumnHeight,
|
||||
): boolean {
|
||||
return currentColumnLength > 0 && currentHeight + nextImageHeight > maxHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将试炼 stage 图片按最大列高拆成多列。
|
||||
*
|
||||
@ -83,13 +100,20 @@ export function splitEventStageImagesByColumnHeight<
|
||||
let currentHeight = 0;
|
||||
|
||||
for (const image of images) {
|
||||
currentHeight += image.height;
|
||||
if (currentHeight > maxHeight && currentColumn.length > 0) {
|
||||
if (
|
||||
shouldStartNewEventStageColumn(
|
||||
currentHeight,
|
||||
image.height,
|
||||
currentColumn.length,
|
||||
maxHeight,
|
||||
)
|
||||
) {
|
||||
columns.push(currentColumn);
|
||||
currentColumn = [];
|
||||
currentHeight = image.height;
|
||||
currentHeight = 0;
|
||||
}
|
||||
currentColumn.push(image);
|
||||
currentHeight += image.height;
|
||||
}
|
||||
|
||||
if (currentColumn.length > 0) {
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
getEventStageSongCellWidth,
|
||||
getEventStageSongJacketHeight,
|
||||
getEventStageSongRowSize,
|
||||
shouldStartNewEventStageColumn,
|
||||
splitEventStageImagesByColumnHeight,
|
||||
} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/event-stage-spec';
|
||||
|
||||
@ -38,6 +39,12 @@ describe('BangDream event stage spec', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('detects whether the next stage image should start a new column', () => {
|
||||
expect(shouldStartNewEventStageColumn(5000, 1000, 2)).toBe(false);
|
||||
expect(shouldStartNewEventStageColumn(5000, 1001, 2)).toBe(true);
|
||||
expect(shouldStartNewEventStageColumn(0, 7000, 0)).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps oversized first image in its own column', () => {
|
||||
const columns = splitEventStageImagesByColumnHeight(
|
||||
[
|
||||
|
||||
Loading…
Reference in New Issue
Block a user