提升图集切片上限并保留原始告警

图标图集自动、手动和网格切片上限统一提高到 256。

同步 SpacetimeDB 批次与生成结果 item 上限,覆盖 256/257 边界。

严格图集提交校验透传 sliceWarning 原始原因,避免通用文案覆盖根因。

补充 Rust 定向测试、文档和共享决策记录。
This commit is contained in:
Git Hooks Test
2026-09-16 01:05:34 +08:00
parent 792530ad24
commit 0ce5eb35b0
10 changed files with 156 additions and 31 deletions
@@ -6519,6 +6519,7 @@ impl PlatformArtSliceContractRollback {
fn validate_strict_platform_art_spritesheet_contract(
slices: &[PreparedPlatformArtAssetSlice],
slice_warning: Option<&str>,
canvas_context: &ExternalCanvasGenerationContext,
canvas_project_id: Option<&str>,
resource_id: Option<&str>,
@@ -6532,7 +6533,11 @@ fn validate_strict_platform_art_spritesheet_contract(
has_visible_pixels: bool,
) -> Result<(), String> {
if slices.is_empty() {
return Err("spritesheet 图集至少需要一个独立切片".to_string());
return Err(slice_warning
.map(str::trim)
.filter(|warning| !warning.is_empty())
.map(|warning| format!("spritesheet 图集至少需要一个独立切片;原始切片告警:{warning}"))
.unwrap_or_else(|| "spritesheet 图集至少需要一个独立切片".to_string()));
}
let resource_id = resource_id
.map(str::trim)
@@ -7337,6 +7342,7 @@ fn commit_prepared_platform_art_asset_with_before_replace_hook(
if require_complete_core_slices {
validate_strict_platform_art_spritesheet_contract(
&slices,
slice_warning.as_deref(),
&canvas_context,
canvas_project_id.as_deref(),
resource_id.as_deref(),
@@ -9798,6 +9804,7 @@ mod canvas_generation_tests {
.collect::<Vec<_>>();
validate_strict_platform_art_spritesheet_contract(
&slices,
None,
&canvas_context,
Some("canvas-project"),
Some("spritesheet-resource"),
@@ -9813,6 +9820,35 @@ mod canvas_generation_tests {
.expect("valid slice identities and pixel evidence do not require a fixed layout");
}
#[test]
fn strict_spritesheet_contract_preserves_slice_warning_when_empty() {
let canvas_context = ExternalCanvasGenerationContext {
project_id: "canvas-project".to_string(),
asset_folder_id: "asset-folder".to_string(),
canvas_name: "empty-slice-warning".to_string(),
};
let error = validate_strict_platform_art_spritesheet_contract(
&[],
Some("识别出的素材数量超过输出上限:86,最多允许 256 个"),
&canvas_context,
None,
None,
None,
None,
"route",
"kind",
None,
&[],
false,
false,
)
.expect_err("empty slices must expose the original platform warning");
assert!(error.contains("至少需要一个独立切片"));
assert!(error.contains("原始切片告警"));
assert!(error.contains("识别出的素材数量超过输出上限:86,最多允许 256 个"));
}
#[test]
fn strict_spritesheet_contract_rejects_an_opaque_slice() {
let canvas_context = ExternalCanvasGenerationContext {
@@ -9850,6 +9886,7 @@ mod canvas_generation_tests {
let error = validate_strict_platform_art_spritesheet_contract(
&slices,
None,
&canvas_context,
Some("canvas-project"),
Some("spritesheet-resource"),
@@ -12449,7 +12486,10 @@ mod canvas_generation_tests {
.expect("init strict slice project");
let path = root.join("assets/art-spritesheet.png");
fs::write(&path, b"old-image").expect("write old spritesheet");
let prepared = prepared_replacement(root, b"new-image");
let mut prepared = prepared_replacement(root, b"new-image");
prepared.slice_warning = Some(
"图标 spritesheet 识别出的素材数量超过输出上限:86,最多允许 256 个。".to_string(),
);
let error = commit_prepared_platform_art_asset_strict_slices_at(
root,
@@ -12460,6 +12500,8 @@ mod canvas_generation_tests {
.expect_err("strict spritesheet commit must require at least one slice");
assert!(error.contains("至少需要一个独立切片"));
assert!(error.contains("原始切片告警"));
assert!(error.contains("识别出的素材数量超过输出上限:86,最多允许 256 个"));
assert_eq!(fs::read(path).expect("read preserved sheet"), b"old-image");
assert!(!root
.join("assets/art-spritesheet-slices/manifest.json")
@@ -2,6 +2,12 @@
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
> 当前口径:历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据;如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。
## 2026-09-16 图标图集自动拆图上限提高到 256
- 背景:AGC 图标图集自动连通域识别在一次生成中识别出 86 个区域,原有 64 片上限在后处理阶段阻断了请求;该上限同时影响 api-server 自动 / 手动切片、SpacetimeDB 批量落库和统一生成结果 item 数量。
- 决策:将可输出独立切片上限统一提高到 `256`;统一生成结果最多 `258` 个 item256 个切片加 provider 原图和透明整图)。保持原始连通域 `4096`、总裁剪像素、CPU / 内存 admission、并发上传和处理时限不变。
- 边界:超过 256 仍按现有 `output-slice-limit-exceeded` / `sliceWarning` 语义失败关闭切片写入;自动路径保留可信整图,手动路径继续在持久化前返回错误。
- 验证:平台切片器、api-server 警告映射与 payload、SpacetimeDB 结果 / 批次校验均覆盖 256 成功边界和 257 溢出边界。
## 2026-09-14 生成进度面收敛为「常驻可折叠任务侧栏」;提交即关面板、阶段文案只归侧栏;定位动作终局化
@@ -4987,7 +4987,7 @@ Cocos Creator 根目录由 `package.json.creator.version` 与普通 `assets/`
- 原因:把“请求已入队”、“某个稳定 ID 已存在”或“job 已 completed”误当成整批业务记录已原子提交的证据。request fingerprint 只证明用户请求,不绑定最终 slot、派生记录、画布候选和 compact result;仅比较资源 ID 也无法发现内容漂移。
- 处理:用 `editor_generation_operation` 记录 durable receipt,分开 request fingerprint 与整笔 commit SHA-256。首次调用在同一 SpacetimeDB 事务中校验 lease 并写 object/resource/asset/binding/canvas/job/receipt;重放先查 receipt,再读回逐 slot 权威事实精确比较。receipt 缺失但 resource/asset/binding 已存在时失败关闭,不得补写 receipt;事务前已确认的 asset object 只能在 ID、bucket/key、owner、策略、媒体、来源和实体字段全部相等时复用。
- 时间与并发:`completed_at_micros` 必须为正数,object/resource/asset/binding/canvas 候选原时间字段与它一起纳入 commit SHA-256,不能在每次重放时重新取时;job 终态和完成事件只用 SpacetimeDB `ctx.timestamp`。canvas CAS 冲突后只刷新 project 并重算布局,不重跑 Provider / OSS。OSS 尚未进入该事务,无引用 object 仍是需另行清理的边界,不要宣称跨 OSS exactly-once。
- queue completion 不能把 inline 完整响应无条件同时复制到 `result``editor-agent-tool-call-result`。图集/UI 最多 64 个切片会重复携带 resource/asset/prompt/generationInputs,容易超过 job payload 512 KiB 上限并让整个原子提交回滚。必须先按普通 UI、Editor Agent、External API 的消费方契约裁剪,再把最终 JSON 交给统一 procedure。
- queue completion 不能把 inline 完整响应无条件同时复制到 `result``editor-agent-tool-call-result`。图集/UI 最多 256 个切片会重复携带 resource/asset/prompt/generationInputs,容易超过 job payload 512 KiB 上限并让整个原子提交回滚。必须先按普通 UI、Editor Agent、External API 的消费方契约裁剪,再把最终 JSON 交给统一 procedure。
- 消费方身份不能在提交前重新读取 summary 兼容快照来判断:该快照按设计清空 dedupe key 并删除 generationInputsEditor Agent / External API 会因此被误判成普通 UI。应在 worker 持有完整 claimed job 时把安全的 consumer kind 与 source identity 固化到调用上下文。
- procedure future 超时或连接断开不能直接映射为业务失败,远端事务可能已经提交。必须有界重放同一 prepared commit;明确 CAS 后才刷新 layout,且刷新 layout 应使用新时间,不能把项目 `updated_at` 回拨。receipt 不复制 queue payload,只存摘要并从 job 权威行回读;跨记录 object/project 一致性必须在事务内验证,不能依赖当前 builder 通常会携带完整 candidate。
- job 的 owner/kind/fingerprint/lease 都正确仍不够:`source_entity_id` 还必须绑定结果项目,来源资源必须另查存在性与 owner/project 归属;否则同 owner 的 job 可以误写别的项目,或伪造跨用户/跨项目血缘。
File diff suppressed because one or more lines are too long
@@ -54,7 +54,7 @@ EditorGenerationResultPersistInput {
### 首次提交顺序
1. 校验调用身份、operation 字段、fingerprint、item 数量上限和 slot 唯一性。统一提交最多接受 66 个 item,用于容纳最多 64 个图集切片以及 provider 原图和透明整图。
1. 校验调用身份、operation 字段、fingerprint、item 数量上限和 slot 唯一性。统一提交最多接受 258 个 item,用于容纳最多 256 个图集切片以及 provider 原图和透明整图。
2. queue 输入必须完整携带 `job_id + worker_id + lease_token + result_payload_json`;inline 输入必须全部省略,禁止半套 guard。
3. queue 路径在同一事务快照内校验 job owner、kind、request fingerprint、running 状态和有效 lease;过期 worker 不得写业务结果。`source_entity_id` 必须精确等于本次唯一结果 `project_id`,不得用同 owner 的 job 向其他项目提交。
4. 对每个 item 校验稳定 object/resource/asset ID、owner、project、folder、object key、source resource、task 审计字段和媒体字段的交叉一致性。project resource 和 account asset 的 `source_resource_id` 均必须单独验证:来源资源必须是本次同事务候选或已登记资源,属于同 owner,且在结果具有项目上下文时属于同 project;不接受 asset-only 分支绕过血缘校验。
@@ -355,7 +355,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- 载荷约束:本次先对 `source_module = editor-canvas``request_payload_json` / `result_payload_json` 实施有限大小合法 JSON、任意层级禁止 `data:` / `blob:` 的双层门禁,只保存 worker 执行必需的普通参数和已登记媒体引用。画布 Agent 来源的任务可在 `result_payload_json.editor-agent-tool-call-result` 中保存有界的轻量结果和已登记媒体引用,供后端按已有 `externalJobId + owner_user_id` 定向懒回填;其它编辑器任务保持元数据结果,并可保存有界的 `warning.code/reason`。其它玩法在完成各自参考图资源化之前不由本次门禁静默改变既有请求契约。该主表只供 worker claim / 执行、受控维护以及画布 Agent 的定向结果回填读取;正式用户任务列表、单任务状态、队列概览与 acknowledge 不得返回或解析这两个 payload。画布 Agent 懒回填必须经对应工具 formatter 归一为有界轻量媒体引用后写入 OSS 会话,不能把原始 payload 直接透传前端。
- 非阻断告警:角色形象、图标图集和 UI 素材提取已保存 provider 原图、但透明背景处理最终失败时,以原图唯一主图完成任务;透明图和切片不写入画布。这个 source-only 降级只包住透明背景处理的最终失败,phase 上报、provider 原图持久化、透明处理图持久化或画布写回失败仍按任务错误传播。图标 / UI 透明图集成功但自动拆分降级时仍保留透明图集;通用 `warning``sliceWarning` 只在「透明背景最终失败」这一条上互斥,风格归一化或像素规整产生的通用 `warning` 可与 `sliceWarning` 并存。两类成功降级都以既有 `completed` 状态收口,不新增状态值:source-only 的 inline / external v1 响应使用结构化 `warning.code/reason`,仅拆分失败的 inline / external v1 响应继续使用既有 `sliceWarning.code/reason`,其 `reason` 保留原始诊断;queue worker 才把两者归一为有界的 `result_payload_json.warning`:只有一条时原样保留完整 `reason`,两条并存时按“通用在前、拆分在后”拼接且 `code` 收敛为 `multiple-generation-warnings`(两条 `code` 相同则沿用原 `code`),不允许任何一条被丢弃;`sliceWarning.reason` 无论是否并存都由 worker 添加“图集已生成,但自动拆分未完成:”前缀,拼接结果最后统一做长度上界收敛。除上述画布 Agent 定向回填的轻量结果外,队列结果不保存图片、切片列表或媒体 URL。
- 2026-07-29 收口补充:上条 source-only 的“透明背景处理最终失败”同时包含 Alpha 比例漂移超过 `5%`、provider 原图修复性回读失败、Alpha 回贴失败和透明图完整解码失败;三条链路共用 helper,只写已保存 provider 原图画布层,图标 / UI 固定 `iconImageSrcs=[]``sliceWarning=null`,不得写透明图、派生资源或切片。provider 原图本身解码失败时在首次持久化前失败,不允许 `512×512` 元数据兜底。图标自动拆分、手动拆分与 UI 提取先在受 2 路 CPU semaphore、30 秒 / 请求 deadline 保护的 blocking prepare 中完成解码、透明化、连通域和 bounds 排序;platform 对全部原始连通域设置 `4096` 硬上限、用空间网格查询邻近辅助候选,并在首片 PNG 编码前同时执行 `maxOutputSlices=64` 与全部 padding crop 总像素预算。prepare 返回共享 RGBA + bounds 计划,api-server 再以容量 `2` 的有界管线按需编码、共享 HTTP client 并发 OSS `PUT + HEAD`OSS 连接 / 单请求超时固定为 `10s / 60s`;手动入口在下载最大 `32 MiB` 来源对象前取得独立内存 admission,同一 admission 覆盖下载、prepare 到最后一片上传结束并在数据库调用前释放,CPU permit 只覆盖实际 CPU 阶段。全部对象上传验证成功后,切片的 `asset_object + editor_project_resource + editor_asset + editor_asset_group_cohort` 由单个受 editor generation runtime service identity 保护的 SpacetimeDB procedure 在一次 `try_with_tx` 中原子写入;resource / asset ID 由 owner + task + 序号稳定派生,已有同 ID 素材仅在内容完全一致时幂等复用,来源资源必须存在且与派生资源同 owner / project;上传中途失败不得写部分资源、素材或 cohort,不确定结果重放不得复制整批素材。自动超限只保留整张可信透明图并返回稳定 `sliceWarning`,不写切片;手动超限在首次持久化前返回 `422`
- 2026-07-29 收口补充:上条 source-only 的“透明背景处理最终失败”同时包含 Alpha 比例漂移超过 `5%`、provider 原图修复性回读失败、Alpha 回贴失败和透明图完整解码失败;三条链路共用 helper,只写已保存 provider 原图画布层,图标 / UI 固定 `iconImageSrcs=[]``sliceWarning=null`,不得写透明图、派生资源或切片。provider 原图本身解码失败时在首次持久化前失败,不允许 `512×512` 元数据兜底。图标自动拆分、手动拆分与 UI 提取先在受 2 路 CPU semaphore、30 秒 / 请求 deadline 保护的 blocking prepare 中完成解码、透明化、连通域和 bounds 排序;platform 对全部原始连通域设置 `4096` 硬上限、用空间网格查询邻近辅助候选,并在首片 PNG 编码前同时执行 `maxOutputSlices=256` 与全部 padding crop 总像素预算。prepare 返回共享 RGBA + bounds 计划,api-server 再以容量 `2` 的有界管线按需编码、共享 HTTP client 并发 OSS `PUT + HEAD`OSS 连接 / 单请求超时固定为 `10s / 60s`;手动入口在下载最大 `32 MiB` 来源对象前取得独立内存 admission,同一 admission 覆盖下载、prepare 到最后一片上传结束并在数据库调用前释放,CPU permit 只覆盖实际 CPU 阶段。全部对象上传验证成功后,切片的 `asset_object + editor_project_resource + editor_asset + editor_asset_group_cohort` 由单个受 editor generation runtime service identity 保护的 SpacetimeDB procedure 在一次 `try_with_tx` 中原子写入;resource / asset ID 由 owner + task + 序号稳定派生,已有同 ID 素材仅在内容完全一致时幂等复用,来源资源必须存在且与派生资源同 owner / project;上传中途失败不得写部分资源、素材或 cohort,不确定结果重放不得复制整批素材。自动超限只保留整张可信透明图并返回稳定 `sliceWarning`,不写切片;手动超限在首次持久化前返回 `422`
- 2026-08-06 原子提交对上述 source-only / slice 条款的修正:“provider 原图已持久化”只能解释为 OSS 对象已上传并验证,不再表示 project resource / account asset 已先行入库。source-only、透明整图和成功切片必须先完成最终选择,再作为一份 prepared commit 连同 canvas/job/receipt 一次提交;未选中或失败分支不得留下正式 resource / asset 部分记录。
@@ -18994,8 +18994,8 @@ mod tests {
),
(
GeneratedAssetSheetError::OutputSliceLimitExceeded {
slice_count: 65,
max_slice_count: 64,
slice_count: 257,
max_slice_count: 256,
},
EDITOR_ICON_SPRITESHEET_SLICE_WARNING_OUTPUT_LIMIT,
),
@@ -20441,9 +20441,9 @@ mod tests {
}
#[test]
fn atomic_spritesheet_job_result_with_64_large_slices_stays_below_payload_limit() {
fn atomic_spritesheet_job_result_with_256_large_slices_stays_below_payload_limit() {
let large_metadata = "x".repeat(32 * 1024);
let icons = (0..64)
let icons = (0..256)
.map(|index| {
json!({
"name": format!("icon-{index}"),
@@ -20496,7 +20496,7 @@ mod tests {
.to_string();
let agent_context = EditorGenerationQueueResultContext::from_job(&agent_job);
let agent_payload = serialize_atomic_editor_generation_job_result(&agent_context, &result)
.expect("64-slice agent payload should remain compact");
.expect("256-slice agent payload should remain compact");
assert!(agent_payload.len() < MAX_EDITOR_GENERATION_JOB_RESULT_PAYLOAD_BYTES);
assert!(!agent_payload.contains("generationInputs"));
assert!(!agent_payload.contains("ownerUserId"));
@@ -20507,7 +20507,7 @@ mod tests {
let external_context = EditorGenerationQueueResultContext::from_job(&external_job);
let external_payload =
serialize_atomic_editor_generation_job_result(&external_context, &result)
.expect("64-slice external payload should remain compact");
.expect("256-slice external payload should remain compact");
assert!(external_payload.len() < MAX_EDITOR_GENERATION_JOB_RESULT_PAYLOAD_BYTES);
assert!(!external_payload.contains("generationInputs"));
assert!(!external_payload.contains("ownerUserId"));
@@ -76,7 +76,7 @@ pub(crate) const EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_CHARS: usize = 2_000;
pub(crate) const EDITOR_ICON_DESCRIPTIONS_MAX_TOTAL_UTF8_BYTES: usize = 6 * 1024;
pub(crate) const EDITOR_ICON_SPRITESHEET_MAX_DIMENSION: u32 = 4096;
pub(crate) const EDITOR_ICON_SPRITESHEET_MAX_PIXELS: u64 = 2048 * 2048;
const EDITOR_ICON_SPRITESHEET_MAX_SLICES: usize = 64;
const EDITOR_ICON_SPRITESHEET_MAX_SLICES: usize = 256;
pub(crate) const EDITOR_ICON_SPRITESHEET_CPU_MAX_CONCURRENCY: usize = 2;
pub(crate) const EDITOR_ICON_SPRITESHEET_MEMORY_MAX_CONCURRENCY: usize = 2;
pub(crate) const EDITOR_ICON_SPRITESHEET_UPLOAD_MAX_CONCURRENCY: usize = 2;
@@ -314,10 +314,29 @@ fn resolve_editor_icon_spritesheet_grid_dimensions(
if !(1..=EDITOR_ICON_SPRITESHEET_MAX_GRID_AXIS).contains(&grid_x)
|| !(1..=EDITOR_ICON_SPRITESHEET_MAX_GRID_AXIS).contains(&grid_y)
{
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"field": "gridX/gridY",
"message": format!("gridXgridY 必须在 1 到 {} 之间。", EDITOR_ICON_SPRITESHEET_MAX_GRID_AXIS),
})));
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"field": "gridX/gridY",
"message": format!(
"gridX 与 gridY 必须在 1 到 {} 之间。",
EDITOR_ICON_SPRITESHEET_MAX_GRID_AXIS
),
})),
);
}
if usize::try_from(grid_x.saturating_mul(grid_y))
.ok()
.is_some_and(|slice_count| slice_count > EDITOR_ICON_SPRITESHEET_MAX_SLICES)
{
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"field": "gridX/gridY",
"message": format!(
"gridX 与 gridY 的切片总数不得超过 {}。",
EDITOR_ICON_SPRITESHEET_MAX_SLICES
),
})),
);
}
Ok((grid_x, grid_y))
}
@@ -3075,6 +3094,29 @@ mod tests {
assert_eq!(grid.grid_y, Some(2));
}
#[test]
fn grid_dimensions_reject_more_than_maximum_output_slices() {
let error = resolve_editor_icon_spritesheet_grid_dimensions(
EditorIconSpritesheetSliceMode::Grid,
Some(17),
Some(16),
)
.expect_err("257 grid cells must exceed the output slice limit");
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
assert_eq!(
error.details().and_then(|details| details.get("field")),
Some(&json!("gridX/gridY"))
);
assert!(
error
.details()
.and_then(|details| details.get("message"))
.and_then(Value::as_str)
.is_some_and(|message| message.contains("256"))
);
}
#[test]
fn spritesheet_genre_requires_exact_game_type_title() {
assert_eq!(
@@ -1288,8 +1288,8 @@ mod tests {
#[test]
fn rejects_output_limit_before_cropping_and_png_encoding() {
let columns = 13u32;
let rows = 5u32;
let columns = 257u32;
let rows = 1u32;
let stride = 10u32;
let mut sheet: image::RgbaImage =
ImageBuffer::from_pixel(columns * stride, rows * stride, Rgba([0, 0, 0, 0]));
@@ -1310,12 +1310,12 @@ mod tests {
mime_type: "image/png".to_string(),
extension: "png".to_string(),
};
let error = slice_generated_icon_spritesheet_all_by_connected_components(&source, 64)
.expect_err("65 output components must fail before slice encoding");
let error = slice_generated_icon_spritesheet_all_by_connected_components(&source, 256)
.expect_err("257 output components must fail before slice encoding");
assert!(error.to_string().contains("素材数量超过输出上限"));
assert!(error.to_string().contains("65"));
assert!(error.to_string().contains("64"));
assert!(error.to_string().contains("257"));
assert!(error.to_string().contains("256"));
}
#[test]
@@ -16,7 +16,7 @@ const EDITOR_CANVAS_LAYOUT_MIGRATION_STATUS_BACKFILLED: &str = "backfilled";
const EDITOR_CANVAS_LAYOUT_MIGRATION_STATUS_ACTIVE: &str = "active";
const EDITOR_CANVAS_LAYOUT_MIGRATION_STATUS_ROLLED_BACK: &str = "rolled_back";
const EDITOR_CANVAS_RESOURCE_REPAIR_MAX_ACTIONS: usize = 16;
const EDITOR_SPRITESHEET_SLICE_BATCH_MAX_ITEMS: usize = 64;
const EDITOR_SPRITESHEET_SLICE_BATCH_MAX_ITEMS: usize = 256;
const EDITOR_CANVAS_AUDIO_RESOURCE_WIDTH: u32 = 420;
const EDITOR_CANVAS_AUDIO_RESOURCE_HEIGHT: u32 = 120;
const EDITOR_CHARACTER_ANIMATION_NORMALIZATION_MAX_BATCH_SIZE: u32 = 25;
@@ -24,8 +24,8 @@ const EDITOR_CHARACTER_ANIMATION_CANVAS_NORMALIZATION_MAX_BATCH_SIZE: u32 = 5;
const EDITOR_IMAGE_ASSET_KIND_CLEANUP_MAX_BATCH_SIZE: u32 = 25;
const EDITOR_IMAGE_ASSET_KIND_CLEANUP_CANVAS_MAX_BATCH_SIZE: u32 = 5;
const EDITOR_LEGACY_IMAGE_ASSET_KIND: &str = "image";
// 透明图集最多产出 64 个切片,另有 provider 原图和透明整图两个正式 item。
const EDITOR_GENERATION_RESULT_MAX_ITEMS: usize = 66;
// 透明图集最多产出 256 个切片,另有 provider 原图和透明整图两个正式 item。
const EDITOR_GENERATION_RESULT_MAX_ITEMS: usize = 258;
const EDITOR_GENERATION_OPERATION_KINDS: [&str; 10] = [
"editor_image_generation",
"editor_icon_spec_generation",
@@ -3872,7 +3872,7 @@ fn validate_editor_generation_result_shape(
return Err("生成结果 completed_at_micros 必须为正数".to_string());
}
if !(1..=EDITOR_GENERATION_RESULT_MAX_ITEMS).contains(&input.items.len()) {
return Err("生成结果 item 数量必须在 1 到 66 之间".to_string());
return Err("生成结果 item 数量必须在 1 到 258 之间".to_string());
}
let result_project_id = editor_generation_result_project_id(input)?;
let mut slots = BTreeSet::new();
@@ -5527,7 +5527,7 @@ fn validate_editor_spritesheet_slice_batch(
) -> Result<(), String> {
let item_count = input.items.len();
if !(1..=EDITOR_SPRITESHEET_SLICE_BATCH_MAX_ITEMS).contains(&item_count) {
return Err("图集切片批次产物数量必须在 1 到 64 之间".to_string());
return Err("图集切片批次产物数量必须在 1 到 256 之间".to_string());
}
if usize::try_from(input.expected_asset_count).ok() != Some(item_count) {
return Err("图集切片批次产物数量与预期不一致".to_string());
@@ -16124,7 +16124,7 @@ mod tests {
}
#[test]
fn editor_generation_result_shape_accepts_sixty_four_slices_plus_two_source_items() {
fn editor_generation_result_shape_accepts_two_hundred_fifty_six_slices_plus_two_source_items() {
let mut input = editor_generation_result_input();
input.items.clear();
for index in 0..EDITOR_GENERATION_RESULT_MAX_ITEMS {
@@ -16179,7 +16179,7 @@ mod tests {
"job-1",
)
.expect_err("more than the full spritesheet bundle must fail")
.contains("1 到 66")
.contains("1 到 258")
);
}
@@ -17781,6 +17781,41 @@ mod tests {
.expect("complete batch should pass before transaction writes");
}
#[test]
fn spritesheet_slice_batch_validation_accepts_256_and_rejects_257_items() {
let mut batch = spritesheet_slice_batch();
batch.items = (0..256)
.map(|index| {
let mut item = spritesheet_slice_item(index);
item.asset
.as_mut()
.expect("slice asset")
.group_task_expected_asset_count = Some(256);
item
})
.collect();
batch.expected_asset_count = 256;
validate_editor_spritesheet_slice_batch(&batch, "user-1", "task-1", Some("group-task-1"))
.expect("256 slices should remain within the batch limit");
let mut overflow_item = spritesheet_slice_item(256);
overflow_item
.asset
.as_mut()
.expect("slice asset")
.group_task_expected_asset_count = Some(257);
batch.items.push(overflow_item);
batch.expected_asset_count = 257;
let error = validate_editor_spritesheet_slice_batch(
&batch,
"user-1",
"task-1",
Some("group-task-1"),
)
.expect_err("257 slices must exceed the batch limit");
assert_eq!(error, "图集切片批次产物数量必须在 1 到 256 之间");
}
#[test]
fn spritesheet_slice_batch_validation_rejects_duplicate_object_key() {
let mut batch = spritesheet_slice_batch();