From 78675326c73f80028a50d7b24971a6628dd3b795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Sat, 8 Aug 2026 17:11:00 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E5=9B=BE=E6=A0=87=E8=A7=84?= =?UTF-8?q?=E8=8C=83=E5=85=A5=E9=98=9F=E7=9B=AE=E6=A0=87=E9=A2=84=E6=A3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在参考资源解析和定价前校验项目与素材目录。 将预检返回的规范化目标写入队列任务。 拆分本地字段与远端引用校验并补充顺序回归测试。 --- ...辑器】画板图标素材生成入口设计-2026-06-15.md | 1 + .../crates/api-server/src/editor_project.rs | 20 ++++++++++++++ .../api-server/src/editor_project_icon.rs | 26 +++++++++++++++---- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md b/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md index ba5a5875f..493a573b7 100644 --- a/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md +++ b/docs/【编辑器】画板图标素材生成入口设计-2026-06-15.md @@ -43,6 +43,7 @@ - 图标规范生成在 inline 模式下也必须先建立带稳定请求指纹的 generation operation,并由编辑器生成 durable billing 边界包住共享执行器;不得在 `operation=None` 时调用 provider 后再进入原子结果持久化。 - 图标 spritesheet 的入队与实际执行路径都必须在引用解析、generation input 重建、定价和 provider / OSS 副作用之前预检 owner、项目和最终素材目录,并将返回的 canonical `projectId + assetFolderId` 回写到后续流程;请求省略目录时按实际写入的 owner 默认目录预检,worker 不得只信任入队时的旧校验结果。 - queued 图标规范生成由共享原子结果持久化使用 worker caller 中的 lease 一并完成任务并清理 lease;共享执行器返回成功后 worker 只能返回 `Ok(())`,不得再次调用 job completion。 +- queued 图标规范生成在本地文本门禁后、参考资源 owner 解析和定价之前执行目标预检,并把 canonical `projectId + assetFolderId` 写入任务 payload;不存在、越权或目录不匹配必须由提交请求同步失败,不得入队后再变成 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`。 - 请求字段: diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index e9e6a110f..27e2b47b2 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -16010,6 +16010,26 @@ mod tests { ); } + #[test] + fn queued_icon_spec_preflights_and_persists_canonical_target_before_remote_work() { + let source = include_str!("editor_project_icon.rs"); + + assert_function_contains_in_order( + source, + "pub(crate) async fn enqueue_icon_spec_generation_for_owner", + "pub(crate) async fn generate_icon_spec_for_owner", + &[ + "validate_icon_spec_request_fields(", + "preflight_editor_billable_generation_target(", + "payload.project_id = target.project_id;", + "payload.asset_folder_id = target.asset_folder_id;", + "validate_icon_spec_reference_for_owner(", + "resolve_icon_spec_price(", + "enqueue_editor_generation_job_for_caller(", + ], + ); + } + #[test] fn image_generation_preflight_checks_references_provider_and_pricing() { let source = concat!( diff --git a/server-rs/crates/api-server/src/editor_project_icon.rs b/server-rs/crates/api-server/src/editor_project_icon.rs index b1d851e68..ee746e8a2 100644 --- a/server-rs/crates/api-server/src/editor_project_icon.rs +++ b/server-rs/crates/api-server/src/editor_project_icon.rs @@ -513,7 +513,17 @@ pub(crate) async fn enqueue_icon_spec_generation_for_owner( mut payload: EditorIconSpecGenerationRequest, external_idempotency_key: Option<&str>, ) -> Result { - validate_icon_spec_request(state, caller, &mut payload).await?; + validate_icon_spec_request_fields(&mut payload)?; + let target = preflight_editor_billable_generation_target( + state, + caller.owner_user_id.as_str(), + payload.project_id.clone(), + payload.asset_folder_id.clone(), + ) + .await?; + payload.project_id = target.project_id; + payload.asset_folder_id = target.asset_folder_id; + validate_icon_spec_reference_for_owner(state, caller, &payload).await?; let price_mud_points = resolve_icon_spec_price(state).await?; enqueue_editor_generation_job_for_caller( state, @@ -538,7 +548,7 @@ pub(crate) async fn generate_icon_spec_for_owner( caller: EditorGenerationCaller, mut payload: EditorIconSpecGenerationRequest, ) -> Result, AppError> { - validate_icon_spec_request(state, &caller, &mut payload).await?; + validate_icon_spec_request_fields(&mut payload)?; let request_param = payload.request_param.clone(); let has_reference_image = payload.reference_id.is_some(); let generation_inputs = payload.generation_inputs.clone(); @@ -565,9 +575,7 @@ pub(crate) async fn generate_icon_spec_for_owner( .await } -async fn validate_icon_spec_request( - state: &AppState, - caller: &EditorGenerationCaller, +fn validate_icon_spec_request_fields( payload: &mut EditorIconSpecGenerationRequest, ) -> Result<(), AppError> { payload.request_param.play_setting = require_icon_spec_generation_text( @@ -580,6 +588,14 @@ async fn validate_icon_spec_request( "artStyle", "美术风格", )?; + Ok(()) +} + +async fn validate_icon_spec_reference_for_owner( + state: &AppState, + caller: &EditorGenerationCaller, + payload: &EditorIconSpecGenerationRequest, +) -> Result<(), AppError> { if let Some(reference_id) = payload.reference_id.as_deref() { validate_editor_reference_id_for_owner(state, caller.owner_user_id.as_str(), reference_id) .await?;