恢复图标图集目标预检

入队前预检并回写规范化项目与素材目录。

worker 执行前再次预检最终写入目标。

补充默认目录及副作用顺序的回归测试和契约说明。
This commit is contained in:
2026-08-08 17:02:13 +08:00
parent 57c825089b
commit 5379e038f6
3 changed files with 53 additions and 13 deletions
@@ -41,6 +41,7 @@
- 前端提交到 `POST /api/editor/icon-spritesheets/generations`
- 图标规范生成在 inline 模式下也必须先建立带稳定请求指纹的 generation operation,并由编辑器生成 durable billing 边界包住共享执行器;不得在 `operation=None` 时调用 provider 后再进入原子结果持久化。
- 图标 spritesheet 的入队与实际执行路径都必须在引用解析、generation input 重建、定价和 provider / OSS 副作用之前预检 owner、项目和最终素材目录,并将返回的 canonical `projectId + assetFolderId` 回写到后续流程;请求省略目录时按实际写入的 owner 默认目录预检,worker 不得只信任入队时的旧校验结果。
- inline 与持久队列入口共用同一份 `iconDescriptions` prompt 合同:请求数组原始长度先满足 OpenAPI `1..100`,不得通过丢弃空白项绕过 `maxItems`;随后去除空白项仍须至少保留 1 条,单条最多 `200` 个 Unicode 字符,以换行拼接后合计最多 `2000` 个 Unicode 字符且不超过 `6144` 个 UTF-8 字节。请求边界校验成功后生成 `ValidatedEditorIconSpritesheetPrompt`,后续 prompt builder 不接受裸字符串。队列入口必须在引用解析、定价和任务持久化前同步拒绝可预测错误,不能把无效任务留给 worker 延迟失败。
- worker 解析主 `referenceId` 时必须通过 `spacetime-client` 的通用窄查询 `resolve_editor_reference` 在同一事务快照内完成引用解析和 owner 校验:该字段只接受当前 owner 的项目资源 ID 或素材 ID,并只按两张表的主键查询;不接受 `objectKey``image_src`、URL 或临时 key 作为主规范引用。两张表先按 `owner_user_id` 筛选候选,再判断同一 ID 是否在当前 owner 范围内同时命中;其它账号的同名 ID 不得制造歧义或阻断当前账号的合法引用。记录带 `asset_object_id` 时必须按该 ID 读取对象并同时核对 bucket、object key 与 owner,只有明确缺少 `asset_object_id` 的兼容旧行才允许按对象位置查询。procedure 复用既有 `EditorProjectResourceSnapshot``EditorAssetSnapshot` 返回唯一已验证行,不接收图标业务类型参数、不新建图标专属快照,也不得拉取当前用户的完整工程列表或素材库。`assetKind="icon-spec"``genre` 都由图标图集业务代码从返回行校验和提取。入队与 inline 预检只核对引用行和 asset object 元数据,不下载图片正文;最终执行重新解析当前事实并只下载一次实际参考图。解析成功后的 `objectKey` 是服务端内部存储事实,不是该请求的输入协议。引用不存在、owner 不匹配、asset object 不存在或数据库调用失败时 procedure 直接失败;业务类型不符或保存的游戏类型无效时 API 失败;合法规范没有已保存游戏类型时允许 `genre=None`
- 请求字段:
@@ -17368,11 +17368,26 @@ mod tests {
&[
"payload.icon_descriptions =",
"normalize_icon_descriptions(payload.icon_descriptions)?.into_descriptions();",
"preflight_editor_billable_generation_target(",
"resolve_editor_icon_spec_reference(",
"resolve_editor_icon_spritesheet_price(",
"enqueue_editor_generation_job_for_caller(",
],
);
assert_function_contains_in_order(
source,
"pub(crate) async fn generate_editor_icon_spritesheet_for_owner",
"fn resolve_editor_icon_spritesheet_source_layer_asset_kind",
&[
"ensure_editor_reference_image_source_limit(",
"preflight_editor_billable_generation_target(",
"payload.project_id = target.project_id;",
"payload.asset_folder_id = target.asset_folder_id;",
"rebuild_editor_generation_input_references(",
"resolve_editor_icon_spec_reference(",
"execute_billable_asset_operation_with_cost(",
],
);
}
#[test]
@@ -1340,19 +1340,6 @@ pub(crate) async fn enqueue_editor_icon_spritesheet_generation_for_owner(
sanitize_editor_untrusted_generation_inputs(payload.generation_inputs.take());
payload.icon_descriptions =
normalize_icon_descriptions(payload.icon_descriptions)?.into_descriptions();
resolve_editor_icon_spec_reference(
state,
caller.owner_user_id.as_str(),
payload.reference_id.as_str(),
)
.await
.map_err(|error| {
error.with_details(json!({
"provider": "editor-icon-spritesheet",
"field": "referenceId",
"message": "图标素材规范必须是当前用户已登记的 icon-spec 资源或素材。",
}))
})?;
ensure_editor_reference_image_sources_are_stable(
payload.reference_image_srcs.as_deref(),
"editor-icon-spritesheet",
@@ -1373,6 +1360,31 @@ pub(crate) async fn enqueue_editor_icon_spritesheet_generation_for_owner(
"referenceImageSrcs",
"图标素材参考图",
)?;
let target = preflight_editor_billable_generation_target(
state,
caller.owner_user_id.as_str(),
payload.project_id.clone(),
payload
.asset_folder_id
.clone()
.or_else(|| Some("project".to_string())),
)
.await?;
payload.project_id = target.project_id;
payload.asset_folder_id = target.asset_folder_id;
resolve_editor_icon_spec_reference(
state,
caller.owner_user_id.as_str(),
payload.reference_id.as_str(),
)
.await
.map_err(|error| {
error.with_details(json!({
"provider": "editor-icon-spritesheet",
"field": "referenceId",
"message": "图标素材规范必须是当前用户已登记的 icon-spec 资源或素材。",
}))
})?;
let price_mud_points = u64::from(
resolve_editor_icon_spritesheet_price(
state,
@@ -1427,6 +1439,18 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner(
"referenceImageSrcs",
"图标素材参考图",
)?;
let target = preflight_editor_billable_generation_target(
state,
caller.owner_user_id.as_str(),
payload.project_id.clone(),
payload
.asset_folder_id
.clone()
.or_else(|| Some("project".to_string())),
)
.await?;
payload.project_id = target.project_id;
payload.asset_folder_id = target.asset_folder_id;
payload.generation_inputs = rebuild_editor_generation_input_references(
state,
caller.owner_user_id.as_str(),