修复匿名精选内部元数据泄露

匿名精选响应省略 provider,并清理 generationInputs 中的内部抠图字段。

保留素材 owner、精选提交响应和后台管理接口的原始审计信息。

补充公开边界回归测试并同步后端契约、前端说明和项目决策。
This commit is contained in:
2026-07-30 13:52:19 +00:00
parent 4b50f879d2
commit 6170a91992
4 changed files with 72 additions and 4 deletions
@@ -19,9 +19,9 @@
## 2026-07-30 抠图实际后端作为 generationInputs 顶层内部元数据保存
- 背景:角色、图标图集和 UI 图集抠图派生资产需要保留最终实际执行的处理后端,供后台诊断 BgFilter、阿里云通用抠图和本地键色的降级结果;把抠图模型写成 `generationInputs.fields` 的“处理模型”会进入图片信息,与用户可见输入快照语义冲突,而覆盖正式资产 `model` 又会丢失源生图模型。
- 决策:继续使用现有 `generation_inputs_json` JSON 包络,不修改 SpacetimeDB schema。`fields` / `references` 只保存用户可见生成输入;仿照顶层 `screenColorHex`,抠图派生资产在顶层写入 `mattingProvider` / `mattingModel`。BgFilter 记录本次实际 `seg_model`;阿里云记录 `Aliyun Matting / segment-common-image`;本地键色记录 `Genarrative Local / screen-color-keying`。三条链路的正式资产 `model` 继续继承源生图模型,图片信息不读取顶层内部字段;这些字段不是保密字段,原始管理接口仍按既有权限读取。角色动作逐帧可能混用多个 fallback,本次不把单帧结果提升为整组动画模型。
- 决策:继续使用现有 `generation_inputs_json` JSON 包络,不修改 SpacetimeDB schema。`fields` / `references` 只保存用户可见生成输入;仿照顶层 `screenColorHex`,抠图派生资产在顶层写入 `mattingProvider` / `mattingModel`。BgFilter 记录本次实际 `seg_model`;阿里云记录 `Aliyun Matting / segment-common-image`;本地键色记录 `Genarrative Local / screen-color-keying`。三条链路的正式资产 `model` 继续继承源生图模型,图片信息不读取顶层内部字段`screenColorHex``mattingProvider``mattingModel` 和素材顶层 `provider` 属于内部执行信息:素材 owner、精选提交响应和后台管理接口仍按既有权限读取原始值,匿名 `GET /api/editor/showcase/resources` 必须在 public payload 边界删除三个 `generationInputs` 顶层内部键并省略 `provider`,历史精选快照不做数据迁移。角色动作逐帧可能混用多个 fallback,本次不把单帧结果提升为整组动画模型。
- 影响范围:`server-rs/crates/api-server/src/editor_project.rs` 的抠图结果和角色 / 图标 / UI 派生资产持久化、图片信息兼容测试、后端数据契约与图片画布 MVP 文档;不修改前端生产展示逻辑、请求 DTO、SpacetimeDB 表、迁移或生成绑定。
- 验证方式:后端单测覆盖三种实际结果映射、顶层元数据不改写 `fields`,结构断言覆盖三条派生资产持久化链路仍保留源生图模型;前端测试覆盖顶层字段不在图片信息出现。运行 `cargo test -p api-server editor_project::tests --manifest-path server-rs/Cargo.toml`、图片信息定向前端测试、`cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run typecheck``npm run check:encoding``git diff --check`
- 验证方式:后端单测覆盖三种实际结果映射、顶层元数据不改写 `fields`,结构断言覆盖三条派生资产持久化链路仍保留源生图模型;public mapper 测试同时证明匿名响应省略 `provider` 和三个内部键、owner 精选快照仍保留原始值;前端测试覆盖顶层字段不在图片信息出现。运行 `cargo test -p api-server editor_project::tests --manifest-path server-rs/Cargo.toml`、图片信息定向前端测试、`cargo check -p api-server --manifest-path server-rs/Cargo.toml``npm run typecheck``npm run check:encoding``git diff --check`
- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md``docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`
---
@@ -172,7 +172,7 @@
- “我的”页项目入口能进入 `/project`;项目页能列出工程、重命名 / 删除单个工程、批量选择和批量删除;点击工程后进入 `/editor/canvas?projectid=<projectId>` 并按 query 加载该工程。
- 用户可见的 `generationInputs.fields` 不得写入或展示“处理模型”;角色抠图、图标 / UI 图集抠图及自动拆分形成的派生资产,其 `Model` 继续继承并展示源生图模型,不得改写为 `birefnet``anime-seg``connected-components`、阿里云抠图或本地键色等内部后处理模型。前端同时过滤历史项目中已持久化的“处理模型”字段;历史资源若只剩内部处理模型而无法恢复源生图模型,保留 `Model` 行并显示 `-`。画布 ZIP 的用户可见导出元数据使用同一过滤口径且保持既有结构。
- `generationInputs` 整体是可扩展 JSON 包络,不等同于全量用户可见快照;图片信息只消费 `fields` / `references`。后端可仿照 `screenColorHex` 在顶层保存 `mattingProvider` / `mattingModel`,记录角色、图标图集和 UI 图集抠图实际成功的 BgFilter、阿里云或本地键色结果;前端规范化和图片信息展示不得把这些顶层内部字段映射成可见字段,正式 `Model` 仍取源生图模型。
- `generationInputs` 整体是可扩展 JSON 包络,不等同于全量用户可见快照;图片信息只消费 `fields` / `references`。后端可仿照 `screenColorHex` 在顶层保存 `mattingProvider` / `mattingModel`,记录角色、图标图集和 UI 图集抠图实际成功的 BgFilter、阿里云或本地键色结果;前端规范化和图片信息展示不得把这些顶层内部字段映射成可见字段,正式 `Model` 仍取源生图模型。匿名精选资源响应不返回素材顶层 `provider`,也不返回 `screenColorHex``mattingProvider``mattingModel`owner 与后台审计读取不受影响。
## 后续扩展点
@@ -618,7 +618,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复
- Rust 结构体:`EditorProjectResource`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind``generation_inputs_json` 和历史 `public_showcase_enabled``asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复。`public_showcase_enabled` 只保留旧接口兼容,不再作为 `/creation``陶泥儿精选` 事实源;精选公开改由账号级生成素材提交 `editor_showcase_asset` 审核决定。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存稳定 `resource_id` 布局引用,不能把同一生成结果再次作为正式业务真相写入。项目封面快照也落在该表,使用 `asset_kind = project-cover-snapshot``source_type = uploaded` 和私有 OSS / asset object 引用,代表画布当前视口栅格化后的静态封面;项目列表和创作主页最近项目只读取最新封面快照资源,不在列表页根据 layout 临时拼画布。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。账号级素材删除不级联删除该表,避免历史画布丢图。结构化 canvas 的几何、层级、分组和资源引用以 `editor_canvas_layer` 为权威,生成器对象以 `editor_canvas_generation_dialog` 为权威;legacy canvas 才在 2 MiB 上限内从 `editor_canvas.layers_json` 兼容读取。新写入不再把素材生成输入快照作为图层布局真相保存。历史普通图层缺资源只能由 migration operator 调用 `repair_editor_canvas_resources_and_return` 定向修复:procedure 每次只处理一个尚无迁移记录的 legacy canvas,校验 owner/project、revision、canvas/project 两份 raw layout SHA-256、精确 layer/resource/sourceResourceId、同工程替换资源与 private asset_object 谱系;图片只替换引用,音频只恢复经核验的 `420x120` 项目资源行。运维入口 `npm run spacetime:editor-canvas-resources:repair` 默认 dry-runapply 必须绑定 plan SHA-256 并在成功后自动复核 already-repaired,禁止手工 SQL 绕过事务 guard。
- `generation_inputs_json` 包络契约:`fields` / `references` 是图片信息读取的用户可见生成输入快照;顶层允许保存后端内部结果扩展。现有 `screenColorHex` 保存实际背景色,角色、图标图集和 UI 图集抠图派生资产使用 `mattingProvider` / `mattingModel` 保存实际成功的处理后端与模型。BgFilter 保存本次 `seg_model`,阿里云通用抠图保存 `Aliyun Matting / segment-common-image`,本地键色保存 `Genarrative Local / screen-color-keying`。这些顶层字段不写入 `fields`、不在图片信息展示,也不具有保密边界,原始管理接口按既有权限仍可读取
- `generation_inputs_json` 包络契约:`fields` / `references` 是图片信息读取的用户可见生成输入快照;顶层允许保存后端内部结果扩展。现有 `screenColorHex` 保存实际背景色,角色、图标图集和 UI 图集抠图派生资产使用 `mattingProvider` / `mattingModel` 保存实际成功的处理后端与模型。BgFilter 保存本次 `seg_model`,阿里云通用抠图保存 `Aliyun Matting / segment-common-image`,本地键色保存 `Genarrative Local / screen-color-keying`。这些顶层字段不写入 `fields`、不在图片信息展示,原始素材 owner、精选提交响应和后台管理接口按既有权限仍可读取;匿名 `GET /api/editor/showcase/resources` 必须在 public payload 边界删除 `screenColorHex``mattingProvider``mattingModel`,同时省略素材顶层 `provider`。过滤只作用于公开响应,不修改素材或精选快照,因此历史数据无需迁移
- 索引:`by_editor_project_resource_project_id``by_editor_project_resource_owner_user_id`
### `editor_asset_folder`
@@ -899,6 +899,7 @@ pub struct EditorProjectResourcePayload {
prompt: Option<String>,
actual_prompt: Option<String>,
model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
provider: Option<String>,
task_id: Option<String>,
source_resource_id: Option<String>,
@@ -6338,9 +6339,22 @@ fn editor_project_resource_public_payload_from_record(
resolve_editor_project_resource_public_author(state, payload.owner_user_id.as_str());
payload.author_display_name = display_name;
payload.author_public_user_code = public_user_code;
payload.provider = None;
payload.generation_inputs = payload
.generation_inputs
.map(sanitize_editor_public_generation_inputs);
payload
}
fn sanitize_editor_public_generation_inputs(mut value: Value) -> Value {
if let Some(object) = value.as_object_mut() {
for field in ["screenColorHex", "mattingProvider", "mattingModel"] {
object.remove(field);
}
}
value
}
fn resolve_editor_project_resource_public_author(
state: &AppState,
owner_user_id: &str,
@@ -9079,6 +9093,60 @@ mod tests {
assert_eq!(value["authorPublicUserCode"], json!("SY-00000042"));
}
#[test]
fn editor_project_resource_public_payload_removes_internal_generation_metadata() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let record = EditorShowcaseAssetRecord {
provider: Some("BgFilter".to_string()),
generation_inputs: Some(json!({
"fields": [{"title": "处理阶段", "value": "角色抠图"}],
"references": [{"refType": "asset", "refId": "asset-source"}],
"screenColorHex": "#CFEFFF",
"mattingProvider": "BgFilter",
"mattingModel": "birefnet"
})),
..test_editor_showcase_asset_record("user-author-1")
};
let owner_payload = serde_json::to_value(
editor_project_resource_payload_from_showcase_record(record.clone()),
)
.expect("owner payload should serialize");
assert_eq!(owner_payload["provider"], json!("BgFilter"));
assert_eq!(
owner_payload["generationInputs"]["screenColorHex"],
json!("#CFEFFF")
);
assert_eq!(
owner_payload["generationInputs"]["mattingProvider"],
json!("BgFilter")
);
assert_eq!(
owner_payload["generationInputs"]["mattingModel"],
json!("birefnet")
);
let public_payload = serde_json::to_value(
editor_project_resource_public_payload_from_record(&state, record),
)
.expect("public payload should serialize");
assert!(public_payload.get("provider").is_none());
let generation_inputs = public_payload["generationInputs"]
.as_object()
.expect("generation inputs should stay object");
assert!(!generation_inputs.contains_key("screenColorHex"));
assert!(!generation_inputs.contains_key("mattingProvider"));
assert!(!generation_inputs.contains_key("mattingModel"));
assert_eq!(
generation_inputs["fields"],
json!([{"title": "处理阶段", "value": "角色抠图"}])
);
assert_eq!(
generation_inputs["references"],
json!([{"refType": "asset", "refId": "asset-source"}])
);
}
#[test]
fn editor_project_resource_public_payload_falls_back_to_public_user_code() {
let state = AppState::new(AppConfig::default()).expect("state should build");