收紧编辑器内部处理元数据边界
Project CI / Native shell tests (pull_request) Successful in 12m45s
Project CI / Frontend tests (pull_request) Successful in 2m58s
Project CI / Repository checks (pull_request) Failing after 9s
Project CI / Backend tests (pull_request) Failing after 9s

普通用户与公开响应统一移除生成 provider、内部处理模型和抠图审计字段。
普通生成请求移除 segModel,由后端固定抠图策略并清理客户端伪造的保留字段。
手动去背景继承可信源模型,角色动作与去背景失败统一使用稳定用户文案。
后台原始审计继续保留真实 provider 与抠图模型,并补齐 Owner、Public、Admin 和紧凑结果测试。
同步更新前端响应契约、外部 OpenAPI、后端架构文档和项目决策记录。
This commit is contained in:
2026-07-31 04:58:07 +00:00
parent 915eb4b739
commit 56fe1cb85d
23 changed files with 993 additions and 207 deletions
@@ -2293,12 +2293,6 @@
"null"
]
},
"provider": {
"type": [
"string",
"null"
]
},
"taskId": {
"type": [
"string",
@@ -2476,12 +2470,6 @@
"null"
]
},
"provider": {
"type": [
"string",
"null"
]
},
"taskId": {
"type": [
"string",
@@ -2852,7 +2840,6 @@
"sourceType",
"prompt",
"model",
"provider",
"taskId"
],
"properties": {
@@ -2895,9 +2882,6 @@
"model": {
"type": "string"
},
"provider": {
"type": "string"
},
"taskId": {
"type": "string"
},
@@ -3231,7 +3215,6 @@
"spritesheetHeight",
"prompt",
"model",
"provider",
"taskId",
"priceMudPoints"
],
@@ -3277,9 +3260,6 @@
"model": {
"type": "string"
},
"provider": {
"type": "string"
},
"taskId": {
"type": "string"
},
@@ -3718,7 +3698,6 @@
"sourceType",
"prompt",
"model",
"provider",
"taskId",
"durationSeconds",
"resolution",
@@ -3767,9 +3746,6 @@
"model": {
"type": "string"
},
"provider": {
"type": "string"
},
"taskId": {
"type": "string"
},
@@ -3955,7 +3931,6 @@
"sourceType",
"prompt",
"model",
"provider",
"taskId",
"priceMudPoints",
"audioKind"
@@ -4003,9 +3978,6 @@
"model": {
"type": "string"
},
"provider": {
"type": "string"
},
"taskId": {
"type": "string"
},
@@ -26,6 +26,17 @@
---
## 2026-07-31 修正抠图内部元数据的普通用户读取边界
- 背景:2026-07-30 的记录误把素材 owner 与后台审计并列为原始抠图执行信息的读取方。owner 是普通用户,前端不展示字段不能阻止其从项目资源、素材库、精选提交 / 点赞回包、画布布局或任务完成响应的网络 payload 读取 BgFilter、阿里云、本地键色、具体分割模型或背景色。
- 决策:本条取代 2026-07-30 决策中“素材 owner、精选提交响应仍可读取原始值”的表述。普通用户(包括素材 owner)和匿名公开读取必须共同过滤素材顶层 `provider`、内部处理 `model`,以及 `generationInputs` 顶层 `screenColorHex``mattingProvider``mattingModel`;正常用户可见生成 `model` 和其他合法功能性顶层字段(例如 `characterAnimation`)保持不变。User/Owner mapper 先完成该清理,public mapper 在其基础上叠加公开字段规则;后台管理与服务端审计继续使用 raw mapper 和持久化原值。历史数据不迁移,统一在读取边界清理。
- 入站与持久化:客户端提交的 `generationInputs` 不得伪造上述内部键,服务端在实际处理完成后才写入可信值。手动去背景的正式素材 `model` 必须继承经服务端验证的正常源生图模型;若来源或祖先链不存在正常模型则为 `null`,不得写入 `BgFilter complex` 等内部处理模型。内部抠图 provider / model 可继续持久化供后台审计,普通用户完成响应和用户可见错误文本均不得暴露它们;手动去背景与角色动作透明化失败在 Owner HTTP / 任务状态边界统一替换为稳定业务文案,原始错误只留在任务记录、tracing 和后台审计。
- 影响范围:项目资源、素材库、精选提交 / 点赞、图片 / 图标 / 视频 / 音频 / 角色动画生成完成、Agent 紧凑结果和识别出的画布资源 / 图层快照的 User/Public mapper;手动去背景持久化和完成响应;External Editor API 创建素材 / 资源时的保留键入站清理;相应 API / OpenAPI 契约、前端搜索 / 详情 / ZIP 过滤测试与后台 raw 审计测试。不修改 SpacetimeDB schema、迁移或 bindings。
- 验证方式:Owner 和匿名响应覆盖无 `provider`、无内部 `model`、无三个内部 `generationInputs` 键,且正常 `model``characterAnimation` 仍保留;Admin raw payload 保持完整。覆盖历史 `BgFilter complex`、三类入站伪造键、手动去背景源模型回溯和用户错误文本过滤;运行 api-server 定向测试、前端定向测试、`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`
---
## 2026-07-30 图片画布搜索不得索引内部模型和 Provider
- 背景:素材和图层详情虽已把内部处理模型显示为 `-`,搜索仍索引原始 `model``provider`,导致 `BgFilter complex``segment-common-image``BgFilter``Aliyun Matting` 等隐藏信息可被查询命中,并出现“命中但无可见匹配字段”的异常体验。
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+41 -4
View File
@@ -4043,10 +4043,11 @@ mod tests {
use super::{
AdminDashboardGranularity, AdminDisplayNameDirectory, EditorShowcaseAssetRecord,
admin_dashboard_user_stats_from_record, admin_dashboard_user_stats_from_result,
admin_editor_asset_group_payload, admin_editor_showcase_asset_payload_from_record,
append_spacetime_sql_response_chunk, apply_admin_database_table_filters,
build_admin_asset_read_url_audit, build_admin_dashboard_chart,
build_admin_database_table_row, build_admin_editor_showcase_campaign_image_confirm_request,
admin_editor_asset_group_payload, admin_editor_asset_payload_from_record,
admin_editor_showcase_asset_payload_from_record, append_spacetime_sql_response_chunk,
apply_admin_database_table_filters, build_admin_asset_read_url_audit,
build_admin_dashboard_chart, build_admin_database_table_row,
build_admin_editor_showcase_campaign_image_confirm_request,
build_admin_tracking_event_keys_sql, build_admin_tracking_events_sql, build_body_preview,
build_debug_base_url, build_spacetime_schema_url, clamp_admin_database_table_limit,
clamp_admin_tracking_event_limit, enforce_admin_request_permission,
@@ -4154,6 +4155,42 @@ mod tests {
}
}
#[test]
fn admin_editor_asset_payload_keeps_internal_matting_audit_metadata() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let mut record = test_admin_editor_asset_record(
"asset-matting",
"task-matting",
"抠图结果",
"generated-character-drafts/editor/matting.png",
0,
);
record.model = Some("BgFilter complex".to_string());
record.provider = Some("BgFilter".to_string());
record.generation_inputs = Some(json!({
"fields": [],
"references": [],
"screenColorHex": "#CFEFFF",
"mattingProvider": "BgFilter",
"mattingModel": "birefnet"
}));
let payload = admin_editor_asset_payload_from_record(&state, record);
let value = serde_json::to_value(payload).expect("admin payload should serialize");
assert_eq!(value["model"], json!("BgFilter complex"));
assert_eq!(value["provider"], json!("BgFilter"));
assert_eq!(
value["generationInputs"]["screenColorHex"],
json!("#CFEFFF")
);
assert_eq!(
value["generationInputs"]["mattingProvider"],
json!("BgFilter")
);
assert_eq!(value["generationInputs"]["mattingModel"], json!("birefnet"));
}
#[test]
fn admin_asset_read_url_audit_keeps_actor_and_target_without_signed_url() {
let draft = build_admin_asset_read_url_audit(
@@ -1164,7 +1164,6 @@ pub(crate) async fn generate_editor_video_for_owner(
prompt: normalized.prompt,
actual_prompt: Some(generated.submitted_prompt),
model: normalized.model,
provider: "VectorEngine".to_string(),
task_id,
duration_seconds: normalized.duration_seconds,
resolution: normalized.resolution,
@@ -5827,7 +5826,22 @@ fn character_animation_error_response(
request_context: &RequestContext,
error: AppError,
) -> Response {
error.into_response_with_context(Some(request_context))
user_visible_character_animation_error(error).into_response_with_context(Some(request_context))
}
fn user_visible_character_animation_error(error: AppError) -> AppError {
if !error.status_code().is_server_error() {
return error;
}
tracing::error!(
status = %error.status_code(),
error = %error,
"editor_character_animation_internal_error_hidden_from_user"
);
AppError::from_status(error.status_code()).with_details(json!({
"message": "角色动作生成失败,请稍后重试。",
}))
}
fn editor_video_error_response(request_context: &RequestContext, error: AppError) -> Response {
@@ -5985,6 +5999,23 @@ mod tests {
use super::*;
use crate::AppConfig;
#[test]
fn character_animation_server_error_hides_internal_matting_details() {
let error = AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
"provider": "BgFilter",
"message": "BgFilter birefnet provider 调用失败",
}));
let sanitized = user_visible_character_animation_error(error);
assert_eq!(sanitized.status_code(), StatusCode::BAD_GATEWAY);
assert_eq!(sanitized.body_text(), "角色动作生成失败,请稍后重试。");
let details = sanitized.details().expect("稳定错误应保留用户文案");
assert!(details.get("provider").is_none());
let lower = details.to_string().to_ascii_lowercase();
assert!(!lower.contains("bgfilter"));
assert!(!lower.contains("birefnet"));
}
#[test]
fn parse_video_data_url_accepts_mp4_payload() {
let parsed =
@@ -333,7 +333,6 @@ impl EditorAgentTool for GenerateImageTool {
style: None,
model: Some(args.model),
screen_color: None,
seg_model: None,
aspect_ratio: Some(args.aspect_ratio),
image_size: Some(args.image_size),
reference_image_srcs: Some(reference_image_srcs),
@@ -441,7 +440,6 @@ impl EditorAgentTool for GenerateCharacterTool {
style: None,
model: Some(args.model),
screen_color: Some("auto".to_string()),
seg_model: Some("birefnet".to_string()),
aspect_ratio: Some(args.aspect_ratio),
image_size: Some(args.image_size),
reference_image_srcs: Some(reference_image_srcs),
@@ -551,7 +549,6 @@ impl EditorAgentTool for GenerateUiDesignTool {
style: None,
model: Some(args.model),
screen_color: None,
seg_model: None,
aspect_ratio: Some(args.aspect_ratio),
image_size: Some(args.image_size),
reference_image_srcs: Some(reference_image_srcs),
@@ -805,7 +802,6 @@ impl EditorAgentTool for GenerateIconSpritesheetTool {
style: None,
model: Some(args.model),
screen_color: Some("auto".to_string()),
seg_model: Some("birefnet".to_string()),
aspect_ratio: Some(args.aspect_ratio),
image_size: Some(args.image_size),
project_id: Some(context.conversation.project_id.clone()),
File diff suppressed because it is too large Load Diff
@@ -32,8 +32,8 @@ use crate::{
editor_project_resource_payload_from_record, extract_editor_ui_design_assets_for_owner,
generate_editor_icon_spritesheet_for_owner, generate_editor_image_for_owner,
map_editor_project_error, normalize_editor_persisted_media_src, normalize_optional_string,
parse_editor_generation_json_payload, save_editor_project_layout_with_revision_and_get,
serialize_editor_asset_metadata,
parse_editor_generation_json_payload, sanitize_editor_client_generation_inputs,
save_editor_project_layout_with_revision_and_get, serialize_editor_asset_metadata,
},
external_api_auth::ExternalApiPrincipal,
http_error::AppError,
@@ -467,7 +467,7 @@ pub async fn create_external_editor_asset(
) -> Result<Json<Value>, AppError> {
require_scope(&principal, SCOPE_EDITOR_ASSET)?;
let generation_inputs_json =
serialize_editor_asset_metadata(payload.generation_inputs.clone())?;
serialize_external_editor_generation_inputs(payload.generation_inputs.clone())?;
let object_key = normalize_optional_string(payload.object_key);
let image_src = normalize_editor_persisted_media_src(payload.image_src, object_key.as_deref())?;
let asset = state
@@ -569,7 +569,7 @@ pub async fn create_external_editor_project_resource(
) -> Result<Json<Value>, AppError> {
require_scope(&principal, SCOPE_EDITOR_ASSET)?;
let generation_inputs_json =
serialize_editor_asset_metadata(payload.generation_inputs.clone())?;
serialize_external_editor_generation_inputs(payload.generation_inputs.clone())?;
let object_key = normalize_optional_string(payload.object_key);
let image_src = normalize_editor_persisted_media_src(payload.image_src, object_key.as_deref())?;
let resource = state
@@ -787,6 +787,12 @@ fn normalize_project_title(title: Option<String>) -> String {
.unwrap_or_else(|| EDITOR_PROJECT_DEFAULT_TITLE.to_string())
}
fn serialize_external_editor_generation_inputs(
generation_inputs: Option<Value>,
) -> Result<Option<String>, AppError> {
serialize_editor_asset_metadata(sanitize_editor_client_generation_inputs(generation_inputs))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -809,6 +815,26 @@ mod tests {
assert_eq!(request.expected_revision, 7);
}
#[test]
fn external_editor_generation_inputs_cannot_forge_internal_audit_fields() {
let serialized = serialize_external_editor_generation_inputs(Some(json!({
"fields": [{"label": "角色设定", "value": "骑士"}],
"screenColorHex": "#00FF00",
"mattingProvider": "forged-provider",
"mattingModel": "forged-model",
"characterAnimation": {"durationSeconds": 4}
})))
.expect("外部编辑器生成输入应可序列化")
.expect("非空生成输入应保留");
let parsed: Value = serde_json::from_str(&serialized).expect("生成输入应为合法 JSON");
assert_eq!(parsed["fields"][0]["value"], "骑士");
assert_eq!(parsed["characterAnimation"]["durationSeconds"], 4);
assert!(parsed.get("screenColorHex").is_none());
assert!(parsed.get("mattingProvider").is_none());
assert!(parsed.get("mattingModel").is_none());
}
#[test]
fn exported_openapi_json_contains_external_editor_routes_and_security() {
let parsed: Value = serde_json::from_str(OPENAPI_JSON).expect("openapi json should parse");
@@ -1025,5 +1051,25 @@ mod tests {
parsed["components"]["securitySchemes"]["ExternalApiKey"]["scheme"],
"bearer"
);
for schema_name in [
"EditorProjectResource",
"EditorAsset",
"EditorImageGenerationResponse",
"EditorIconSpritesheetGenerationResponse",
"EditorVideoGenerationResponse",
"EditorAudioGenerationResponse",
] {
let schema = &parsed["components"]["schemas"][schema_name];
assert!(
schema["properties"].get("provider").is_none(),
"{schema_name} 不应向普通用户公开 provider"
);
assert!(
schema["required"]
.as_array()
.is_some_and(|required| !required.contains(&json!("provider"))),
"{schema_name} 不应要求 provider"
);
}
}
}
@@ -19,6 +19,9 @@ use spacetime_client::{
ExternalGenerationJobSummaryRecord, SpacetimeClientError,
};
use crate::editor_generation_queue::{
EDITOR_BACKGROUND_REMOVAL_JOB_KIND, EDITOR_CHARACTER_ANIMATION_GENERATION_JOB_KIND,
};
use crate::{
api_response::json_success_body, auth::AuthenticatedAccessToken, http_error::AppError,
request_context::RequestContext, state::AppState,
@@ -180,7 +183,10 @@ fn map_external_generation_job_status(
ExternalGenerationJobStatus::Failed,
"生成失败。",
0,
job.last_error_message.clone(),
user_visible_external_generation_error(
job.job_kind.as_str(),
job.last_error_message.clone(),
),
),
_ => (ExternalGenerationJobStatus::Queued, "排队中。", 8, None),
};
@@ -196,6 +202,21 @@ fn map_external_generation_job_status(
}
}
/// 外部任务表保存的失败文本同时服务于 worker 诊断和用户通知。抠图及角色动作透明化任务的
/// 原始失败文本可能包含内部 provider、分割模型或实现细节,因此只能在普通用户读取边界
/// 替换成稳定文案。
/// 原始值仍留在任务记录、tracing 和后台审计路径中。
fn user_visible_external_generation_error(job_kind: &str, error: Option<String>) -> Option<String> {
if job_kind == EDITOR_BACKGROUND_REMOVAL_JOB_KIND && error.is_some() {
return Some("去除背景失败,请稍后重试。".to_string());
}
if job_kind == EDITOR_CHARACTER_ANIMATION_GENERATION_JOB_KIND && error.is_some() {
return Some("角色动作生成失败,请稍后重试。".to_string());
}
error
}
fn map_external_generation_job_status_detail(
job: ExternalGenerationJobSummaryRecord,
) -> ExternalGenerationJobStatusDetailRecord {
@@ -392,4 +413,70 @@ mod tests {
let generating = map_external_generation_job_status(job);
assert_eq!(generating.phase_detail, "正在生成。");
}
#[test]
fn background_removal_failure_hides_internal_processing_details_from_owner() {
let job = ExternalGenerationJobSummaryRecord {
job_id: "task-matting-failed".to_string(),
job_kind: EDITOR_BACKGROUND_REMOVAL_JOB_KIND.to_string(),
owner_user_id: "user-1".to_string(),
source_module: "editor-canvas".to_string(),
source_entity_id: "project-1".to_string(),
request_label: "去除背景".to_string(),
request_prompt: None,
status: "failed".to_string(),
last_error_message: Some(
"BgFilter birefnet provider 返回失败,model=segment-common-image".to_string(),
),
created_at: "2026-07-31T08:00:00Z".to_string(),
started_at: Some("2026-07-31T08:00:01Z".to_string()),
completed_at: Some("2026-07-31T08:00:10Z".to_string()),
updated_at: "2026-07-31T08:00:10Z".to_string(),
updated_at_micros: 1_000,
price_mud_points: 0,
refund_ledger_id: None,
notification_acknowledged_at: None,
notification_acknowledged_at_micros: None,
phase: None,
warning_message: None,
};
let status = map_external_generation_job_status(job.clone());
let task = map_external_generation_task_record(job);
assert_eq!(status.error.as_deref(), Some("去除背景失败,请稍后重试。"));
assert_eq!(task.error.as_deref(), Some("去除背景失败,请稍后重试。"));
for value in [status.error, task.error].into_iter().flatten() {
let lower = value.to_ascii_lowercase();
assert!(!lower.contains("bgfilter"));
assert!(!lower.contains("birefnet"));
assert!(!lower.contains("segment-common-image"));
}
}
#[test]
fn character_animation_failure_hides_internal_matting_details_from_owner() {
let message = user_visible_external_generation_error(
EDITOR_CHARACTER_ANIMATION_GENERATION_JOB_KIND,
Some("解析 BgFilter 输入动作帧失败,seg_model=birefnetprovider=BgFilter".to_string()),
);
assert_eq!(message.as_deref(), Some("角色动作生成失败,请稍后重试。"));
let lower = message
.expect("失败任务应返回稳定文案")
.to_ascii_lowercase();
assert!(!lower.contains("bgfilter"));
assert!(!lower.contains("birefnet"));
assert!(!lower.contains("provider"));
}
#[test]
fn unrelated_generation_failure_keeps_its_user_visible_message() {
let message = user_visible_external_generation_error(
"editor_image_generation",
Some("生成服务暂时不可用。".to_string()),
);
assert_eq!(message.as_deref(), Some("生成服务暂时不可用。"));
}
}
@@ -1237,6 +1237,24 @@ fn compact_editor_generation_result(mut result: Value) -> Value {
let Some(object) = result.as_object_mut() else {
return result;
};
// 紧凑结果会回传给普通用户的 Agent 工具调用卡片,不能把供应商或内部后处理实现
// 当作可见生成信息下发。正常的用户可见模型仍然保留,以便卡片恢复原有展示。
object.remove("provider");
if object
.get("model")
.and_then(Value::as_str)
.is_some_and(is_editor_internal_processing_model)
{
object.remove("model");
}
if let Some(generation_inputs) = object
.get_mut("generationInputs")
.and_then(Value::as_object_mut)
{
for field in ["screenColorHex", "mattingProvider", "mattingModel"] {
generation_inputs.remove(field);
}
}
object.remove("project");
object.remove("asset");
object.remove("spritesheetAsset");
@@ -1271,6 +1289,18 @@ fn compact_editor_generation_result(mut result: Value) -> Value {
result
}
fn is_editor_internal_processing_model(model: &str) -> bool {
matches!(
model.trim().to_ascii_lowercase().as_str(),
"anime-seg"
| "bgfilter complex"
| "birefnet"
| "connected-components"
| "screen-color-keying"
| "segment-common-image"
)
}
fn extract_editor_generation_warning_fields(
warning: Option<&Value>,
is_slice_warning: bool,
@@ -1757,6 +1787,13 @@ mod tests {
"actualPrompt": null,
"model": "gpt-image-2",
"provider": "VectorEngine",
"generationInputs": {
"fields": { "prompt": "castle" },
"characterAnimation": { "frameCount": 8 },
"screenColorHex": "#CFEFFF",
"mattingProvider": "BgFilter",
"mattingModel": "birefnet",
},
"taskId": "provider-task-1",
"resource": {
"resourceId": "resource-1",
@@ -1794,9 +1831,45 @@ mod tests {
.get("project")
.is_none()
);
assert_eq!(
payload["editor-agent-tool-call-result"]["model"],
json!("gpt-image-2")
);
assert!(
payload["editor-agent-tool-call-result"]
.get("provider")
.is_none()
);
assert_eq!(
payload["editor-agent-tool-call-result"]["generationInputs"],
json!({
"fields": { "prompt": "castle" },
"characterAnimation": { "frameCount": 8 },
})
);
assert!(!payload.to_string().contains("data:image"));
}
#[test]
fn compact_editor_generation_result_removes_internal_models_case_insensitively() {
for model in [
"anime-seg",
" BgFilter Complex ",
"BIREFNET",
"connected-components",
"screen-color-keying",
"segment-common-image",
] {
let compact = compact_editor_generation_result(json!({
"model": model,
"provider": "internal-provider",
}));
assert!(compact.get("model").is_none(), "model={model}");
assert!(compact.get("provider").is_none(), "model={model}");
}
}
#[test]
fn editor_agent_spritesheet_result_keeps_all_persisted_slices() {
let mut job = external_generation_job_record_fixture(Some("lease-1"));
@@ -338,7 +338,6 @@ pub(crate) async fn generate_editor_sound_effect_for_owner(
prompt: normalized.prompt.clone(),
actual_prompt: Some(normalized.prompt),
model: normalized.model,
provider: generated.provider,
task_id: generated.task_id,
price_mud_points: normalized.price_mud_points,
audio_kind: "sound-effect".to_string(),
@@ -562,7 +561,6 @@ pub(crate) async fn generate_editor_background_music_for_owner(
prompt: normalized.gpt_description_prompt.clone(),
actual_prompt: Some(normalized.gpt_description_prompt),
model: platform_audio::SUNO_DEFAULT_MODEL.to_string(),
provider: generated.provider,
task_id: generated.task_id,
price_mud_points: normalized.price_mud_points,
audio_kind: "background-music".to_string(),
@@ -424,7 +424,6 @@ pub struct EditorVideoGenerateResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub actual_prompt: Option<String>,
pub model: String,
pub provider: String,
pub task_id: String,
pub duration_seconds: u32,
pub resolution: String,
@@ -503,7 +502,6 @@ pub struct EditorIconSpritesheetGenerateResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub actual_prompt: Option<String>,
pub model: String,
pub provider: String,
pub task_id: String,
pub price_mud_points: u32,
}
@@ -560,7 +558,6 @@ pub struct EditorAudioGenerateResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub actual_prompt: Option<String>,
pub model: String,
pub provider: String,
pub task_id: String,
pub price_mud_points: u32,
pub audio_kind: String,
@@ -1260,7 +1257,6 @@ mod tests {
prompt: "让角色向镜头挥手".to_string(),
actual_prompt: Some("让角色向镜头挥手".to_string()),
model: "kling3.0-omni".to_string(),
provider: "VectorEngine".to_string(),
task_id: "task-1".to_string(),
duration_seconds: 5,
resolution: "480p".to_string(),
@@ -1287,6 +1283,7 @@ mod tests {
assert_eq!(payload["assetObjectId"], json!("assetobj_video_1"));
assert_eq!(payload["sourceType"], json!("generated"));
assert_eq!(payload["durationSeconds"], json!(5));
assert!(payload.get("provider").is_none());
}
#[test]
@@ -1323,7 +1320,6 @@ mod tests {
prompt: "图标 prompt".to_string(),
actual_prompt: Some("图标 prompt".to_string()),
model: "gemini-3.1-flash-image-preview".to_string(),
provider: "VectorEngine".to_string(),
task_id: "task-icons".to_string(),
price_mud_points: 12,
})
@@ -1331,6 +1327,7 @@ mod tests {
assert_eq!(response_payload["priceMudPoints"], json!(12));
assert_eq!(response_payload["iconImageSrcs"], json!([]));
assert!(response_payload.get("provider").is_none());
assert_eq!(
response_payload["sliceWarning"],
json!({
@@ -1410,7 +1407,6 @@ mod tests {
prompt: "金币掉落叮当声".to_string(),
actual_prompt: Some("金币掉落叮当声".to_string()),
model: "audio1.0".to_string(),
provider: "VectorEngine".to_string(),
task_id: "sound-task-1".to_string(),
price_mud_points: 10,
audio_kind: "sound-effect".to_string(),
@@ -1439,6 +1435,7 @@ mod tests {
);
assert_eq!(response_payload["assetObjectId"], json!("assetobj_audio_1"));
assert_eq!(response_payload["audioKind"], json!("sound-effect"));
assert!(response_payload.get("provider").is_none());
assert_eq!(
response_payload["asset"]["assetKind"],
json!("sound-effect")
@@ -3529,7 +3529,6 @@ describe('ImageCanvasEditorView generation integration', () => {
sourceImageSrc:
'generated-character-drafts/editor/generation-references/marked-ui-design.png',
screenColor: 'auto',
segModel: 'birefnet',
aspectRatio: '1:1',
imageSize: '1K',
projectId: 'editor-project-ui-extract',
@@ -341,7 +341,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
prompt: '图标 prompt',
actualPrompt: '图标 actual prompt',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'task-icons',
priceMudPoints: 20,
},
@@ -432,7 +431,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
prompt: '图标 prompt',
actualPrompt: '图标 actual prompt',
model: 'gpt-image-2',
provider: 'VectorEngine',
taskId: 'task-icons',
priceMudPoints: 20,
},
@@ -464,7 +462,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
prompt: '金币掉落叮当声',
actualPrompt: '金币掉落叮当声',
model: 'audio1.0',
provider: 'VectorEngine',
taskId: 'sound-task-1',
priceMudPoints: 10,
audioKind: 'sound-effect',
@@ -525,7 +522,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
sourceType: 'generated',
prompt: '金币掉落叮当声',
model: 'audio1.0',
provider: 'VectorEngine',
taskId: 'sound-task-1',
objectKey: 'generated-character-drafts/editor-audios/sfx.mp3',
assetObjectId: 'assetobj-audio-1',
@@ -535,6 +531,7 @@ describe('ImageCanvasGenerationLayerModel', () => {
assetId: 'asset-audio-bff',
}),
});
expect(layer.provider).toBeUndefined();
});
it('creates video result layers with OSS asset identity', () => {
@@ -549,7 +546,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
prompt: '开场动画',
actualPrompt: '开场动画',
model: 'seedance2.0-fast',
provider: 'VectorEngine',
taskId: 'video-task-1',
priceMudPoints: 40,
durationSeconds: 5,
@@ -586,6 +582,7 @@ describe('ImageCanvasGenerationLayerModel', () => {
durationSeconds: 5,
});
expect(layer.generatedAssetSnapshot?.assetId).toBe('asset-video-bff');
expect(layer.provider).toBeUndefined();
});
it('creates character animation result layers as playable image sequences', () => {
@@ -653,11 +650,11 @@ describe('ImageCanvasGenerationLayerModel', () => {
prompt: '待机动作',
actualPrompt: '待机动作',
model: 'seedance2.0-fast',
provider: 'ark',
taskId: 'character-animation-task-1',
assetKind: 'character-animation',
durationSeconds: 4,
});
expect(layer?.generationInputs?.fields[0]?.value).toBe('待机动作');
expect(layer?.provider).toBeUndefined();
});
});
@@ -114,7 +114,6 @@ function applyGeneratedMetadata(
prompt: generated.prompt,
actualPrompt: generated.actualPrompt ?? generated.prompt,
model: generated.model,
provider: generated.provider,
taskId: generated.taskId,
objectKey: resource?.objectKey ?? generated.objectKey,
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
@@ -333,7 +332,6 @@ export function createIconSpritesheetResultLayers({
prompt: generated.prompt,
actualPrompt: generated.actualPrompt ?? generated.prompt,
model: generated.model,
provider: generated.provider,
taskId: generated.taskId,
assetKind: 'icon-spritesheet',
sourceAssetId: spritesheetAsset?.assetId,
@@ -382,7 +380,6 @@ export function createIconSpritesheetResultLayers({
prompt: generated.prompt,
actualPrompt: generated.actualPrompt ?? generated.prompt,
model: generated.model,
provider: generated.provider,
taskId: generated.taskId,
assetKind: 'icon',
sourceAssetId: asset?.assetId,
@@ -451,7 +448,6 @@ export function createVideoResultLayer({
prompt: generated.prompt,
actualPrompt: generated.actualPrompt ?? generated.prompt,
model: generated.model,
provider: generated.provider,
taskId: generated.taskId,
objectKey: resource?.objectKey ?? generated.objectKey,
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
@@ -513,7 +509,6 @@ export function createAudioResultLayer({
prompt: generated.prompt,
actualPrompt: generated.actualPrompt ?? generated.prompt,
model: generated.model,
provider: generated.provider,
taskId: generated.taskId,
objectKey: resource?.objectKey ?? generated.objectKey,
assetObjectId: resource?.assetObjectId ?? generated.assetObjectId,
@@ -575,7 +570,6 @@ export function createCharacterAnimationResultLayer({
prompt: generated.prompt,
actualPrompt: generated.prompt,
model: generated.model,
provider: 'ark',
taskId: generated.taskId,
assetKind: 'character-animation',
durationSeconds: generated.durationSeconds,
@@ -100,7 +100,6 @@ export const DEFAULT_ICON_DESCRIPTIONS = [
'冻结按钮',
];
export const DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR = 'auto';
export const DEFAULT_EDITOR_BGFILTER_SEG_MODEL = 'birefnet';
export const UI_DESIGN_ASSET_EXTRACTION_PROMPT =
'仅提取被红色框框选的素材并整理成spritesheet,图集背景必须使用所选单一纯色抠图背景。纯色背景必须平整无纹理、无渐变、无阴影、无地面、无环境、无道具,方便后续扣除背景;素材自身不要出现与背景色相同或相近的描边、底板、投影或反光。';
export const QUICK_EDIT_SIZE_PRESETS = [
@@ -343,7 +343,6 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
kind: 'character',
model: 'gpt-image-2',
screenColor: 'auto',
segModel: 'birefnet',
style: 'pixelArt',
aspectRatio: '2:3',
imageSize: '2K',
@@ -717,7 +716,6 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
iconDescriptions: ['返回按钮\n\n设置按钮'],
model: 'gpt-image-2',
screenColor: 'auto',
segModel: 'birefnet',
style: 'pixelArt',
aspectRatio: '3:2',
imageSize: '2K',
@@ -29,7 +29,6 @@ import {
buildUiDesignGenerationInputs,
buildVideoGenerationInputs,
CHARACTER_ANIMATION_MODEL,
DEFAULT_EDITOR_BGFILTER_SEG_MODEL,
DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
DEFAULT_SOUND_EFFECT_DURATION_SECONDS,
DEFAULT_SOUND_EFFECT_MODEL,
@@ -343,7 +342,6 @@ export function buildImageGenerationSubmissionPlan({
].filter((src): src is string => Boolean(src));
const imageModel = normalizeEditorImageModel(dialog.imageModel);
const screenColor = DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR;
const segModel = DEFAULT_EDITOR_BGFILTER_SEG_MODEL;
return {
kind: 'image',
normalizedPrompt,
@@ -352,7 +350,6 @@ export function buildImageGenerationSubmissionPlan({
kind: 'character',
model: imageModel,
screenColor,
segModel,
style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none',
aspectRatio: dialog.aspectRatio ?? '1:1',
imageSize: dialog.imageSize ?? '1K',
@@ -619,7 +616,6 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
const rememberImageModel = normalizeEditorImageModel(dialog.imageModel);
const screenColor = DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR;
const segModel = DEFAULT_EDITOR_BGFILTER_SEG_MODEL;
return {
ok: true,
iconDescriptions,
@@ -637,7 +633,6 @@ export function buildIconSpritesheetGenerationSubmissionPlan(
iconDescriptions,
model: rememberImageModel,
screenColor,
segModel,
style: dialog.style === 'pixelArt' ? 'pixelArt' : 'none',
aspectRatio: dialog.aspectRatio ?? '1:1',
imageSize: dialog.imageSize ?? '1K',
@@ -2573,7 +2573,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
sourceImageSrc: 'generated/ui-design.png',
model: 'gemini-3.1-flash-image-preview',
screenColor: 'auto',
segModel: 'birefnet',
}),
);
});
@@ -2710,7 +2709,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
'generated-character-drafts/editor/generation-references/reference.png',
model: 'gemini-3.1-flash-image-preview',
screenColor: 'auto',
segModel: 'birefnet',
aspectRatio: '1:1',
imageSize: '2K',
canvasCompletion: expect.objectContaining({
@@ -2772,7 +2770,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(extractEditorUiDesignAssetsMock).toHaveBeenCalledWith(
expect.objectContaining({
screenColor: 'auto',
segModel: 'birefnet',
referenceImageSrcs: ['generated/ref-style.png'],
generationInputs: expect.objectContaining({
references: expect.arrayContaining([
@@ -58,7 +58,6 @@ import {
buildQuickEditGenerationInputs,
buildUiDesignAssetExtractionGenerationInputs,
buildVideoGenerationInputs,
DEFAULT_EDITOR_BGFILTER_SEG_MODEL,
DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR,
DEFAULT_IMAGE_MODEL,
DEFAULT_VIDEO_MODEL,
@@ -1086,7 +1085,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
);
}
const screenColor = DEFAULT_EDITOR_GENERATION_BACKGROUND_COLOR;
const segModel = DEFAULT_EDITOR_BGFILTER_SEG_MODEL;
const generationInputs = buildUiDesignAssetExtractionGenerationInputs(
sourceLayer,
options.references,
@@ -1123,7 +1121,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
options.model ?? DEFAULT_IMAGE_MODEL,
),
screenColor,
segModel,
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
projectId,
generationInputs,
@@ -837,7 +837,6 @@ describe('editorProjectClient', () => {
kind: 'character',
model: 'gpt-image-2',
screenColor: '#FFD6C2',
segModel: 'anime-seg',
style: 'pixelArt',
aspectRatio: '2:3',
imageSize: '1K',
@@ -862,7 +861,6 @@ describe('editorProjectClient', () => {
kind: 'character',
model: 'gpt-image-2',
screenColor: '#FFD6C2',
segModel: 'anime-seg',
style: 'pixelArt',
aspectRatio: '2:3',
imageSize: '1K',
@@ -1016,7 +1014,6 @@ describe('editorProjectClient', () => {
iconDescriptions: ['返回按钮'],
model: 'gpt-image-2',
screenColor: '#E6D8FF',
segModel: 'anime-seg',
style: 'none',
aspectRatio: '1:1',
imageSize: '2K',
@@ -1038,7 +1035,6 @@ describe('editorProjectClient', () => {
iconDescriptions: ['返回按钮'],
model: 'gpt-image-2',
screenColor: '#E6D8FF',
segModel: 'anime-seg',
style: 'none',
aspectRatio: '1:1',
imageSize: '2K',
@@ -1157,7 +1153,6 @@ describe('editorProjectClient', () => {
sourceImageSrc: '/generated-images/editor/ui-design.png',
model: 'gpt-image-2',
screenColor: '#7FB3FF',
segModel: 'anime-seg',
referenceImageSrcs: ['/generated-images/editor/ui-ref.png'],
projectId: 'editor-project-1',
assetFolderId: 'project',
@@ -1180,7 +1175,6 @@ describe('editorProjectClient', () => {
sourceImageSrc: '/generated-images/editor/ui-design.png',
model: 'gpt-image-2',
screenColor: '#7FB3FF',
segModel: 'anime-seg',
referenceImageSrcs: ['/generated-images/editor/ui-ref.png'],
projectId: 'editor-project-1',
generationInputs: {
@@ -197,7 +197,6 @@ export type EditorImageGenerationInput = {
| 'publication-material';
model?: string;
screenColor?: string;
segModel?: string;
style?: EditorImageGenerationStyle;
aspectRatio?: string;
imageSize?: string;
@@ -230,7 +229,6 @@ export type EditorIconSpritesheetGenerationInput = {
iconDescriptions: string[];
model?: string;
screenColor?: string;
segModel?: string;
style?: EditorImageGenerationStyle;
aspectRatio?: string;
imageSize?: string;
@@ -252,7 +250,6 @@ export type EditorUiDesignAssetExtractionInput = {
sourceImageSrc: string;
model?: string;
screenColor?: string;
segModel?: string;
referenceImageSrcs?: string[];
aspectRatio?: string;
imageSize?: string;
@@ -304,7 +301,6 @@ export type EditorImageGenerationResult = {
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
resource?: EditorProjectResourceSnapshot | null;
asset?: EditorAssetSnapshot | null;
@@ -342,7 +338,6 @@ export type EditorIconSpritesheetGenerationResult = {
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
priceMudPoints: number;
spritesheetResource?: EditorProjectResourceSnapshot | null;
@@ -465,7 +460,6 @@ export type EditorVideoGenerationResult = {
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
durationSeconds: number;
priceMudPoints?: number;
@@ -506,7 +500,6 @@ export type EditorAudioGenerationResult = {
prompt: string;
actualPrompt?: string | null;
model: string;
provider: string;
taskId: string;
priceMudPoints: number;
audioKind: 'sound-effect' | 'background-music';
@@ -923,7 +916,6 @@ export async function generateEditorImage(input: EditorImageGenerationInput) {
...(input.kind ? { kind: input.kind } : {}),
...(input.model ? { model: input.model } : {}),
...(input.screenColor ? { screenColor: input.screenColor } : {}),
...(input.segModel ? { segModel: input.segModel } : {}),
...(input.style ? { style: input.style } : {}),
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
...(input.imageSize ? { imageSize: input.imageSize } : {}),
@@ -970,7 +962,6 @@ export async function generateEditorIconSpritesheet(
iconDescriptions: input.iconDescriptions,
model: input.model?.trim() || EDITOR_IMAGE_MODEL_NANOBANANA2,
...(input.screenColor ? { screenColor: input.screenColor } : {}),
...(input.segModel ? { segModel: input.segModel } : {}),
...(input.style ? { style: input.style } : {}),
...(input.aspectRatio ? { aspectRatio: input.aspectRatio } : {}),
...(input.imageSize ? { imageSize: input.imageSize } : {}),
@@ -1018,7 +1009,6 @@ export async function extractEditorUiDesignAssets(
sourceImageSrc: input.sourceImageSrc,
...(input.model ? { model: input.model } : {}),
...(input.screenColor ? { screenColor: input.screenColor } : {}),
...(input.segModel ? { segModel: input.segModel } : {}),
...(input.referenceImageSrcs?.length
? { referenceImageSrcs: input.referenceImageSrcs }
: {}),