From 02b4b6955d7d06b1c2493da246ed02efe13807e8 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 19:38:12 +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=E7=94=9F=E6=88=90=E6=8C=81=E4=B9=85=E5=8C=96=E7=99=BD?= =?UTF-8?q?=E5=90=8D=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SpacetimeDB生成结果白名单加入独立图标规范任务 新增icon-spec durable result shape回归测试 同步后端数据契约与长期决策记录 --- .../shared-memory/decision-log.md | 2 +- ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 2 +- .../src/editor_project_storage.rs | 33 ++++++++++++++++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index e4ff4202d..2ed0f3e4b 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -6771,7 +6771,7 @@ ## 2026-08-06 图标规范与图标图集使用独立任务及 ID-only 主规范引用 -- 本条取代上一条“不新增图标规范专用 worker job”和“图标图集主规范允许 objectKey”的结论。图标规范生成使用独立 `editor_icon_spec_generation` job kind;队列保存原始强类型业务参数,worker 在统一计费操作预扣成功后执行 ExtraParam LLM、构建最终 prompt,再进入共享图片生成和既有后处理 / 持久化链。余额不足不调用文本 LLM,执行失败沿用统一退款;普通 `editor_image_generation` 的 DTO、payload 与执行行为保持不变。 +- 本条取代上一条“不新增图标规范专用 worker job”和“图标图集主规范允许 objectKey”的结论。图标规范生成使用独立 `editor_icon_spec_generation` job kind;队列保存原始强类型业务参数,worker 在统一计费操作预扣成功后执行 ExtraParam LLM、构建最终 prompt,再进入共享图片生成和既有后处理 / 持久化链。SpacetimeDB 编辑器生成结果 operation kind 白名单必须显式包含该独立 job kind,确保 Provider 成功后可以进入统一 durable receipt 原子提交;新增正式生成 job kind 时必须同步扩展白名单和模块回归。余额不足不调用文本 LLM,执行失败沿用统一退款;普通 `editor_image_generation` 的 DTO、payload 与执行行为保持不变。 - `editor_project_icon.rs` 独立承接图标规范、图标 spritesheet、自动切片和手工拆分。普通图片模块只暴露可复用的 prompt builder 执行入口与强类型 provider 请求分流;nanobanana、无参考图 generation、有参考图 edit 的选择集中在同一个 helper,图标图集复用该 helper,不复制 provider 分支。 - 图标规范生成的可选参考图与图标图集的必选主规范统一使用 `referenceId`,只接受当前 owner 的项目资源 ID 或账号素材 ID,不接受 objectKey、URL 或临时 key。SpacetimeDB `resolve_editor_reference_and_return` 因而只接收 `reference_id` 并按两张表主键查询,删除 `image_src` 二级索引。图标图集的普通附加参考图 `referenceImageSrcs` 仍允许 owned objectKey、项目资源 ID 或素材 ID,并继续走既有 owner 校验与真实图片下载链。 - External v1 图标图集请求与 OpenAPI 同步改为必填 `referenceId`。前端和 Editor Agent 优先传正式 resource ID,其次传 asset ID;本地临时 ID、objectKey 和图片地址不得回退成主规范引用,未登记时明确失败并要求先上传或登记。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 862535314..c9f278922 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -740,7 +740,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 - Rust 结构体:`EditorGenerationOperation` - 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` -- 说明:编辑器生成结果的私有 durable commit receipt,queue 与受控 inline 路径共用。主键 `operation_key` 由 owner 与 operation ID 稳定派生,同 owner 不得跨 kind 复用 operation ID;`operation_fingerprint` 绑定规范用户请求,`commit_sha256` 绑定全部 slot 候选、object/resource/asset/binding、可选 canvas 候选与 job completion。表内另固化 owner/kind/ID、可选 `project_id`、queue 的 `job_id / job_worker_id / job_lease_token / job_result_payload_sha256` 和首次提交时间;重放从已完成 job 回读权威 compact result 并核对摘要,不在 receipt 复制正文。它只证明一笔业务结果已原子提交,不是第二套 job 状态或 resource/asset/canvas read model,不保存大快照。 +- 说明:编辑器生成结果的私有 durable commit receipt,queue 与受控 inline 路径共用。主键 `operation_key` 由 owner 与 operation ID 稳定派生,同 owner 不得跨 kind 复用 operation ID;`operation_fingerprint` 绑定规范用户请求,`commit_sha256` 绑定全部 slot 候选、object/resource/asset/binding、可选 canvas 候选与 job completion。表内另固化 owner/kind/ID、可选 `project_id`、queue 的 `job_id / job_worker_id / job_lease_token / job_result_payload_sha256` 和首次提交时间;重放从已完成 job 回读权威 compact result 并核对摘要,不在 receipt 复制正文。它只证明一笔业务结果已原子提交,不是第二套 job 状态或 resource/asset/canvas read model,不保存大快照。结果提交的 operation kind 白名单必须覆盖所有进入统一持久化的正式 job kind,其中包括独立图标规范任务 `editor_icon_spec_generation`;新增 job kind 时必须在同一变更中同步白名单和模块回归。 - 重放:receipt 存在时必须核对全部绑定和权威记录,完全一致才返回 `AlreadyApplied`;同 operation 的请求或提交摘要漂移、project/job 绑定漂移、receipt 缺失但稳定 resource/asset/binding 等业务记录已存在均失败关闭。事务前单独确认的 asset object 只在全部字段与稳定候选完全一致时允许复用,不能据此补造 receipt。重放不更新 receipt 时间,不重复 job/binding 事件,不推进 canvas revision。 - 时间:`completed_at_micros` 必须为正数并固化为 receipt 完成时间;object/resource/asset/binding/canvas 候选的原时间字段与它一起进入 commit SHA-256,重放必须复用原 prepared commit 而不得重新取时。queue job 完成时间与完成事件仍使用 SpacetimeDB `ctx.timestamp`,不信任调用方时钟。 - 索引:主键 `operation_key`;`by_editor_generation_operation_owner(owner_user_id, operation_id)` 仅用于受控定位和诊断,不允许同 owner 跨 operation kind 复用同一 operation ID。 diff --git a/server-rs/crates/spacetime-module/src/editor_project_storage.rs b/server-rs/crates/spacetime-module/src/editor_project_storage.rs index 41dc759ae..a61eff8c0 100644 --- a/server-rs/crates/spacetime-module/src/editor_project_storage.rs +++ b/server-rs/crates/spacetime-module/src/editor_project_storage.rs @@ -23,8 +23,9 @@ const EDITOR_CHARACTER_ANIMATION_NORMALIZATION_MAX_BATCH_SIZE: u32 = 25; const EDITOR_CHARACTER_ANIMATION_CANVAS_NORMALIZATION_MAX_BATCH_SIZE: u32 = 5; // 透明图集最多产出 64 个切片,另有 provider 原图和透明整图两个正式 item。 const EDITOR_GENERATION_RESULT_MAX_ITEMS: usize = 66; -const EDITOR_GENERATION_OPERATION_KINDS: [&str; 9] = [ +const EDITOR_GENERATION_OPERATION_KINDS: [&str; 10] = [ "editor_image_generation", + "editor_icon_spec_generation", "editor_image_edit", "editor_background_removal", "editor_icon_spritesheet_generation", @@ -13703,6 +13704,36 @@ mod tests { ); } + #[test] + fn editor_generation_result_shape_allows_icon_spec_operation_kind() { + let operation_kind = "editor_icon_spec_generation"; + let operation_id = "icon-spec-job-1"; + let mut input = editor_generation_result_input(); + input.operation_kind = operation_kind.to_string(); + input.operation_id = operation_id.to_string(); + let resource = input.items[0] + .project_resource + .as_mut() + .expect("icon spec resource"); + resource.resource_id = shared_contracts::editor_generation_stable_resource_id( + "user-1", + operation_kind, + operation_id, + "primary", + ); + resource.asset_kind = Some("icon-spec".to_string()); + + assert!( + validate_editor_generation_result_shape( + &input, + "user-1", + operation_kind, + operation_id, + ) + .is_ok() + ); + } + #[test] fn editor_generation_result_rejects_cross_record_object_drift_without_candidate() { let mut input = editor_generation_result_input();