补齐图标规范生成持久化白名单
Project CI / Repository checks (pull_request) Successful in 1m21s
Project CI / Frontend tests (pull_request) Successful in 2m49s
Project CI / Backend tests (pull_request) Successful in 3m46s
Project CI / Native shell tests (pull_request) Successful in 13m17s

SpacetimeDB生成结果白名单加入独立图标规范任务
新增icon-spec durable result shape回归测试
同步后端数据契约与长期决策记录
This commit is contained in:
2026-08-08 19:38:12 +08:00
parent a43c84fd75
commit 02b4b6955d
3 changed files with 34 additions and 3 deletions
@@ -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 和图片地址不得回退成主规范引用,未登记时明确失败并要求先上传或登记。
@@ -740,7 +740,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- Rust 结构体:`EditorGenerationOperation`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:编辑器生成结果的私有 durable commit receiptqueue 与受控 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 receiptqueue 与受控 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。
@@ -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();