Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b6ed8a4cf | |||
| 16deb0ad61 | |||
| 13f56e644d | |||
| e08b031761 | |||
| 70ad794b28 | |||
| ca761e093c | |||
| a17367112f | |||
| d7a0c3cffd | |||
| fa64819b6e | |||
| f748d36d21 |
@@ -7,10 +7,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: project-ci-${{ github.event.pull_request.number || github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ Let the client derive projections from real disk changes and trusted tool result
|
|||||||
3. Keep read scopes separate: `asset.list` is the current project manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is the authoritative canvas list. The account library is not the complete canvas list.
|
3. Keep read scopes separate: `asset.list` is the current project manifest, `asset.library.list` is the signed-in account library, and the web project's canvas resource read model is the authoritative canvas list. The account library is not the complete canvas list.
|
||||||
4. Use `canvas.asset_import` for safe account/canvas asset IDs or project-relative local paths. The client rechecks ownership and validates bytes; host absolute paths require native UI file-picker authorization.
|
4. Use `canvas.asset_import` for safe account/canvas asset IDs or project-relative local paths. The client rechecks ownership and validates bytes; host absolute paths require native UI file-picker authorization.
|
||||||
5. When the user explicitly asks to create or derive video, character animation, sound effect, or background music, call `agc_create_or_derive_resource`. Use `create` only for video/audio without a source and `derive` with a registered `sourceLocalAssetId`; character animation is always derived from an image.
|
5. When the user explicitly asks to create or derive video, character animation, sound effect, or background music, call `agc_create_or_derive_resource`. Use `create` only for video/audio without a source and `derive` with a registered `sourceLocalAssetId`; character animation is always derived from an image.
|
||||||
6. When the user explicitly asks to remove an image background, call `agc_remove_background` with a registered image `sourceLocalAssetId` and an output name. The client requires the signed-in account, owns canvas/folder context and task identity, and returns only bounded queue state.
|
6. When the user explicitly asks to remove an image background, call `agc_remove_background` with a registered image `sourceLocalAssetId` and `assetName`. Optional `backgroundMode` is `complex` (semantic foreground segmentation; default) or `flat` (solid-colour background removal). Prefer `flat` when the background is known to be solid. Only `flat` accepts optional `screenColor`: `auto`, `#RRGGBB`, or omitted for automatic detection by the service. Do not select a colour on behalf of `auto`. The client requires the signed-in account, owns canvas/folder context and task identity, and returns only bounded queue state.
|
||||||
7. Preserve existing relative paths when a small edit is sufficient so client resource identities remain stable.
|
7. Preserve existing relative paths when a small edit is sufficient so client resource identities remain stable.
|
||||||
8. Do not edit `.agent/manifest.json`, revision counters, version records, resource IDs, canvas identities, source provenance, generation ledgers, or browser receipts by hand.
|
8. Do not edit `.agent/manifest.json`, revision counters, version records, resource IDs, canvas identities, source provenance, generation ledgers, or browser receipts by hand.
|
||||||
9. Do not create a version when no game file changed. The client compares content fingerprints and advances revision only after an actual source change.
|
9. Do not create a version when no game file changed. The client compares content fingerprints and advances revision only after an actual source change.
|
||||||
|
|||||||
+1
-1
@@ -14,4 +14,4 @@ Read scopes remain separate: `asset.list` is the current project's local manifes
|
|||||||
|
|
||||||
`agc_create_or_derive_resource` accepts only semantic intent. The client resolves `sourceLocalAssetId`, creates stable request identities, recovers matching pending operations, serializes paid submissions, writes supported media into the current canvas and same-name asset folder, validates downloaded bytes, commits the local manifest transaction, and returns redacted warnings. A tool error or timeout is not permission to generate again with a new identity.
|
`agc_create_or_derive_resource` accepts only semantic intent. The client resolves `sourceLocalAssetId`, creates stable request identities, recovers matching pending operations, serializes paid submissions, writes supported media into the current canvas and same-name asset folder, validates downloaded bytes, commits the local manifest transaction, and returns redacted warnings. A tool error or timeout is not permission to generate again with a new identity.
|
||||||
|
|
||||||
`agc_remove_background` is the semantic image post-processing path. It accepts only a registered image `sourceLocalAssetId` and output name; the client resolves the formal source resource, canvas/folder context, stable operation identity, idempotency key, and authenticated External v1 `/api/external/v1/editor/images/background-removals` call. Its result is bounded queue state; Codex must not poll internal workers, construct source URLs, or retry with a new identity after an uncertain response.
|
`agc_remove_background` accepts a registered image `sourceLocalAssetId`, `assetName`, and optional `backgroundMode` and `screenColor`. `complex` uses semantic segmentation to identify the foreground; `flat` removes a solid-colour background. Prefer `flat` when the background is known to be solid; omitting the mode selects `complex`. Only `flat` accepts a colour: `auto`, `#RRGGBB`, or omitted for automatic service detection. Never infer a concrete colour for `auto`. Empty or invalid values and colour without `flat` are rejected. The client resolves the formal source resource, canvas/folder context, stable operation identity, idempotency key, and authenticated External v1 `/api/external/v1/editor/images/background-removals` call. Mode and colour are part of request identity. Its result is bounded queue state; Codex must not poll internal workers, construct source URLs, or retry with a new identity after an uncertain response.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schemaVersion": "agc-skill-pack.v1",
|
"schemaVersion": "agc-skill-pack.v1",
|
||||||
"version": "2026-08-26.16",
|
"version": "2026-08-26.17",
|
||||||
"skills": [
|
"skills": [
|
||||||
{
|
{
|
||||||
"name": "agc-game-production-workflow",
|
"name": "agc-game-production-workflow",
|
||||||
@@ -123,7 +123,7 @@
|
|||||||
"agents/openai.yaml",
|
"agents/openai.yaml",
|
||||||
"references/projection-contract.md"
|
"references/projection-contract.md"
|
||||||
],
|
],
|
||||||
"sha256": "96b5bf9e2ed150bbe934a888867c1bb500b214a131f8b36c4830f51ca30267b6"
|
"sha256": "a929c27bc5b2b0bee0b7935e5c7b04ddbab1eb1804fe196f8c2537ad040ca5b1"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1887,7 +1887,7 @@ async fn bridge_create_or_derive_resource(
|
|||||||
|
|
||||||
async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Value) -> Value {
|
async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Value) -> Value {
|
||||||
let result = async {
|
let result = async {
|
||||||
bridge_reject_unknown_fields(arguments, &["sourceLocalAssetId", "assetName"])?;
|
super::direct_tools_mcp::validate_remove_background_arguments(arguments)?;
|
||||||
enforce_project_permission_policy(&state.root, "canvas.asset_generate")?;
|
enforce_project_permission_policy(&state.root, "canvas.asset_generate")?;
|
||||||
enforce_project_permission_policy(&state.root, "asset.register")?;
|
enforce_project_permission_policy(&state.root, "asset.register")?;
|
||||||
let source_asset_id = bridge_bounded_string(arguments, "sourceLocalAssetId", 80)?;
|
let source_asset_id = bridge_bounded_string(arguments, "sourceLocalAssetId", 80)?;
|
||||||
@@ -1896,6 +1896,8 @@ async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Val
|
|||||||
"assetName",
|
"assetName",
|
||||||
DIRECT_TOOL_BRIDGE_MAX_RESOURCE_NAME_CHARS,
|
DIRECT_TOOL_BRIDGE_MAX_RESOURCE_NAME_CHARS,
|
||||||
)?;
|
)?;
|
||||||
|
let background_mode = arguments.get("backgroundMode").and_then(Value::as_str);
|
||||||
|
let screen_color = arguments.get("screenColor").and_then(Value::as_str);
|
||||||
let manifest = read_existing_manifest_for_project(&state.root)?;
|
let manifest = read_existing_manifest_for_project(&state.root)?;
|
||||||
let source_asset = manifest
|
let source_asset = manifest
|
||||||
.assets
|
.assets
|
||||||
@@ -1920,22 +1922,34 @@ async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Val
|
|||||||
.map_err(|_| "创建抠图服务连接失败".to_string())?;
|
.map_err(|_| "创建抠图服务连接失败".to_string())?;
|
||||||
let context =
|
let context =
|
||||||
prepare_external_canvas_generation_context(&state.root, &client, &access).await?;
|
prepare_external_canvas_generation_context(&state.root, &client, &access).await?;
|
||||||
let fingerprint = format!("{}\0{}", source_asset_id, asset_name);
|
let fingerprint = background_removal_request_fingerprint(
|
||||||
|
&source_asset_id,
|
||||||
|
&asset_name,
|
||||||
|
background_mode,
|
||||||
|
screen_color,
|
||||||
|
);
|
||||||
let (_operation_id, idempotency_key) = state.resource_request_ids(&fingerprint)?;
|
let (_operation_id, idempotency_key) = state.resource_request_ids(&fingerprint)?;
|
||||||
let route = "/api/external/v1/editor/images/background-removals";
|
let route = "/api/external/v1/editor/images/background-removals";
|
||||||
|
let mut request_body = json!({
|
||||||
|
"sourceImageSrc": source_resource_id,
|
||||||
|
"projectId": manifest.project_id,
|
||||||
|
"assetKind": source_asset.kind,
|
||||||
|
"assetFolderId": context.asset_folder_id,
|
||||||
|
"assetLabel": asset_name,
|
||||||
|
"sourceResourceId": source_resource_id,
|
||||||
|
});
|
||||||
|
if background_mode == Some("flat") {
|
||||||
|
request_body["backgroundMode"] = json!("flat");
|
||||||
|
}
|
||||||
|
if let Some(color) = screen_color {
|
||||||
|
request_body["screenColor"] = json!(color);
|
||||||
|
}
|
||||||
let response = crate::http_client::with_agc_main_site_marker(
|
let response = crate::http_client::with_agc_main_site_marker(
|
||||||
client
|
client
|
||||||
.post(format!("{}{}", api_base_url, route))
|
.post(format!("{}{}", api_base_url, route))
|
||||||
.bearer_auth(api_key)
|
.bearer_auth(api_key)
|
||||||
.header("Idempotency-Key", idempotency_key)
|
.header("Idempotency-Key", idempotency_key)
|
||||||
.json(&json!({
|
.json(&request_body),
|
||||||
"sourceImageSrc": source_resource_id,
|
|
||||||
"projectId": manifest.project_id,
|
|
||||||
"assetKind": source_asset.kind,
|
|
||||||
"assetFolderId": context.asset_folder_id,
|
|
||||||
"assetLabel": asset_name,
|
|
||||||
"sourceResourceId": source_resource_id,
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
@@ -1972,6 +1986,20 @@ async fn bridge_remove_background(state: &DirectToolBridgeState, arguments: &Val
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn background_removal_request_fingerprint(
|
||||||
|
source: &str,
|
||||||
|
name: &str,
|
||||||
|
mode: Option<&str>,
|
||||||
|
color: Option<&str>,
|
||||||
|
) -> String {
|
||||||
|
let mode = mode.unwrap_or("complex");
|
||||||
|
if mode == "complex" && color.is_none() {
|
||||||
|
format!("{source}\0{name}")
|
||||||
|
} else {
|
||||||
|
format!("{source}\0{name}\0{mode}\0{}", color.unwrap_or(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn bridge_safe_queue_state(value: Value) -> Value {
|
fn bridge_safe_queue_state(value: Value) -> Value {
|
||||||
let object = value.as_object();
|
let object = value.as_object();
|
||||||
json!({
|
json!({
|
||||||
@@ -2703,6 +2731,29 @@ pub(crate) async fn start_direct_tool_bridge(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn remove_background_identity_preserves_default_and_distinguishes_options() {
|
||||||
|
let legacy = "asset-1\0透明图";
|
||||||
|
assert_eq!(
|
||||||
|
background_removal_request_fingerprint("asset-1", "透明图", None, None),
|
||||||
|
legacy
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
background_removal_request_fingerprint("asset-1", "透明图", Some("complex"), None),
|
||||||
|
legacy
|
||||||
|
);
|
||||||
|
let mut identities = std::collections::HashSet::new();
|
||||||
|
identities.insert(legacy.to_string());
|
||||||
|
for color in [None, Some("auto"), Some("#CFEFFF"), Some("#112233")] {
|
||||||
|
let id =
|
||||||
|
background_removal_request_fingerprint("asset-1", "透明图", Some("flat"), color);
|
||||||
|
assert_eq!(
|
||||||
|
id,
|
||||||
|
background_removal_request_fingerprint("asset-1", "透明图", Some("flat"), color)
|
||||||
|
);
|
||||||
|
assert!(identities.insert(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::io::{Cursor, Read, Write};
|
use std::io::{Cursor, Read, Write};
|
||||||
|
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
|
|||||||
}),
|
}),
|
||||||
json!({
|
json!({
|
||||||
"name": "agc_remove_background",
|
"name": "agc_remove_background",
|
||||||
"description": "为当前项目已登记的图片资源去除背景。客户端使用当前登录账号的抠图服务、项目画布和素材目录,模型只能提供已登记资源身份与结果名称;不会返回 Token、内部路由、宿主路径或临时签名 URL。",
|
"description": "为当前项目已登记的图片资源去除背景。complex 通过语义分割识别前景;flat 用于纯色背景抠图,确定背景为纯色时优先选择 flat。提供资源身份、结果名称及可选模式和背景色;客户端管理登录、项目画布和素材目录,不返回 Token、内部路由、宿主路径或临时签名 URL。",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -449,6 +449,16 @@ fn direct_tools_mcp_specs_for(controlled_web_search: bool, _cocos_editor_availab
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"maxLength": DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS
|
"maxLength": DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS
|
||||||
|
},
|
||||||
|
"backgroundMode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["complex", "flat"],
|
||||||
|
"description": "可选抠图模式:complex 用语义分割识别前景,flat 用纯色背景抠图;确定背景为纯色时优先使用 flat。省略时使用 complex"
|
||||||
|
},
|
||||||
|
"screenColor": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^(auto|#[0-9A-Fa-f]{6})$",
|
||||||
|
"description": "flat 模式可选背景色;传 auto 或 #RRGGBB,省略时由服务自动检测"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["sourceLocalAssetId", "assetName"],
|
"required": ["sourceLocalAssetId", "assetName"],
|
||||||
@@ -881,14 +891,46 @@ fn validate_resource_generation_arguments(arguments: &Value) -> Result<(), Strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_remove_background_arguments(arguments: &Value) -> Result<(), String> {
|
pub(super) fn validate_remove_background_arguments(arguments: &Value) -> Result<(), String> {
|
||||||
validate_tool_object_fields(arguments, &["sourceLocalAssetId", "assetName"])?;
|
validate_tool_object_fields(
|
||||||
|
arguments,
|
||||||
|
&[
|
||||||
|
"sourceLocalAssetId",
|
||||||
|
"assetName",
|
||||||
|
"backgroundMode",
|
||||||
|
"screenColor",
|
||||||
|
],
|
||||||
|
)?;
|
||||||
bounded_tool_string(arguments, "sourceLocalAssetId", 80)?;
|
bounded_tool_string(arguments, "sourceLocalAssetId", 80)?;
|
||||||
bounded_tool_string(
|
bounded_tool_string(
|
||||||
arguments,
|
arguments,
|
||||||
"assetName",
|
"assetName",
|
||||||
DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS,
|
DIRECT_TOOLS_MCP_MAX_RESOURCE_NAME_CHARS,
|
||||||
)?;
|
)?;
|
||||||
|
if let Some(mode) = arguments.get("backgroundMode") {
|
||||||
|
let mode = mode
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| "backgroundMode 必须是 complex 或 flat".to_string())?;
|
||||||
|
if mode != "complex" && mode != "flat" {
|
||||||
|
return Err("backgroundMode 必须是 complex 或 flat".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(color) = arguments.get("screenColor") {
|
||||||
|
let color = color
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| "screenColor 必须是 auto 或 #RRGGBB".to_string())?;
|
||||||
|
let valid_hex = color.len() == 7
|
||||||
|
&& color.starts_with('#')
|
||||||
|
&& color[1..]
|
||||||
|
.chars()
|
||||||
|
.all(|character| character.is_ascii_hexdigit());
|
||||||
|
if color != "auto" && !valid_hex {
|
||||||
|
return Err("screenColor 必须是 auto 或 #RRGGBB".to_string());
|
||||||
|
}
|
||||||
|
if arguments.get("backgroundMode").and_then(Value::as_str) != Some("flat") {
|
||||||
|
return Err("complex 模式不能传 screenColor".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1849,6 +1891,51 @@ pub(crate) fn stop_game_creator_external_mcp() -> Result<(), String> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_background_arguments_enforce_mode_color_contract() {
|
||||||
|
for fields in [
|
||||||
|
json!({}),
|
||||||
|
json!({"backgroundMode":"complex"}),
|
||||||
|
json!({"backgroundMode":"flat"}),
|
||||||
|
json!({"backgroundMode":"flat","screenColor":"auto"}),
|
||||||
|
json!({"backgroundMode":"flat","screenColor":"#Ab12EF"}),
|
||||||
|
] {
|
||||||
|
let mut arguments = json!({"sourceLocalAssetId":"asset-1","assetName":"透明图"});
|
||||||
|
arguments
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.extend(fields.as_object().unwrap().clone());
|
||||||
|
assert!(
|
||||||
|
validate_remove_background_arguments(&arguments).is_ok(),
|
||||||
|
"{fields}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for fields in [
|
||||||
|
json!({"screenColor":"auto"}),
|
||||||
|
json!({"backgroundMode":"complex","screenColor":"auto"}),
|
||||||
|
json!({"backgroundMode":"flat","screenColor":""}),
|
||||||
|
json!({"backgroundMode":"flat","screenColor":" auto "}),
|
||||||
|
json!({"backgroundMode":"flat","screenColor":"AUTO"}),
|
||||||
|
json!({"backgroundMode":"flat","screenColor":"#GGGGGG"}),
|
||||||
|
json!({"backgroundMode":"flat","screenColor":null}),
|
||||||
|
json!({"backgroundMode":"flat","screenColor":12}),
|
||||||
|
json!({"backgroundMode":""}),
|
||||||
|
json!({"backgroundMode":"FLAT"}),
|
||||||
|
json!({"backgroundMode":" flat "}),
|
||||||
|
json!({"backgroundMode":null}),
|
||||||
|
] {
|
||||||
|
let mut arguments = json!({"sourceLocalAssetId":"asset-1","assetName":"透明图"});
|
||||||
|
arguments
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.extend(fields.as_object().unwrap().clone());
|
||||||
|
assert!(
|
||||||
|
validate_remove_background_arguments(&arguments).is_err(),
|
||||||
|
"{fields}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
#[cfg(all(windows, feature = "cocos-editor-execute"))]
|
||||||
#[test]
|
#[test]
|
||||||
fn builtin_mcp_process_probe() {
|
fn builtin_mcp_process_probe() {
|
||||||
|
|||||||
@@ -59,6 +59,8 @@
|
|||||||
|
|
||||||
## 图片画布与媒体
|
## 图片画布与媒体
|
||||||
|
|
||||||
|
- [AGC 抠图模式与背景色透传方案](./technical/【技术方案】AGC抠图模式与背景色透传-2026-09-16.md):External v1 与 AGC 客户端扩展 `flat`/`complex` 及 BgFilter `auto` 透传。
|
||||||
|
|
||||||
- [共享基础组件库与展示页](./technical/【前端架构】共享基础组件库与展示页-2026-08-26.md):网站与客户端复用的无业务 UI chrome、样式边界和 `/components` 展示页。
|
- [共享基础组件库与展示页](./technical/【前端架构】共享基础组件库与展示页-2026-08-26.md):网站与客户端复用的无业务 UI chrome、样式边界和 `/components` 展示页。
|
||||||
- [Raw GPT Image 2 图片编辑代理](./technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md):主站客户端调用的同步图片编辑代理、multipart 输入、预检查与计费边界。
|
- [Raw GPT Image 2 图片编辑代理](./technical/【技术方案】Raw GPT Image 2图片编辑代理-2026-09-07.md):主站客户端调用的同步图片编辑代理、multipart 输入、预检查与计费边界。
|
||||||
- [UI 编辑器自动切分素材工作流](./technical/【技术方案】UI编辑器自动切分素材工作流-2026-09-08.md):UI 设计图素材切分、Raw GPT Image 2 调用与结果持久化边界。
|
- [UI 编辑器自动切分素材工作流](./technical/【技术方案】UI编辑器自动切分素材工作流-2026-09-08.md):UI 设计图素材切分、Raw GPT Image 2 调用与结果持久化边界。
|
||||||
|
|||||||
@@ -1122,7 +1122,7 @@
|
|||||||
"tags": ["Editor Images"],
|
"tags": ["Editor Images"],
|
||||||
"operationId": "removeExternalEditorImageBackground",
|
"operationId": "removeExternalEditorImageBackground",
|
||||||
"summary": "去除编辑器图片背景",
|
"summary": "去除编辑器图片背景",
|
||||||
"description": "提交已有静态图片素材的异步去背景任务。sourceImageSrc 只接受当前账号拥有的稳定 objectKey、项目资源 ID 或素材 ID;禁止 Data URL、Blob URL 和临时 signed URL。assetKind 只能表达静态图片,并且存在权威来源记录时必须与其类型一致;视频、音频、动画和图片序列在入队前返回 400。服务端固定使用 complex 去背景模式,不会在失败时切换到其它 provider。需要写入画布时提供 projectId 与 canvasCompletion;仅需原位替换既有图层时提供 projectId 与 targetLayerId,且来源与目标必须指向同一权威对象。",
|
"description": "提交已有静态图片素材的异步去背景任务。sourceImageSrc 只接受当前账号拥有的稳定 objectKey、项目资源 ID 或素材 ID;禁止 Data URL、Blob URL 和临时 signed URL。assetKind 只能表达静态图片,并且存在权威来源记录时必须与其类型一致;视频、音频、动画和图片序列在入队前返回 400。complex 使用语义分割识别前景,flat 用于纯色背景抠图;确定背景为纯色时优先使用 flat。需要写入画布时提供 projectId 与 canvasCompletion;仅需原位替换既有图层时提供 projectId 与 targetLayerId,且来源与目标必须指向同一权威对象。",
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"ExternalApiKey": []
|
"ExternalApiKey": []
|
||||||
@@ -3211,6 +3211,17 @@
|
|||||||
"minLength": 1,
|
"minLength": 1,
|
||||||
"description": "当前账号拥有的稳定 objectKey、项目资源 ID 或素材 ID。禁止 Data URL、Blob URL 和临时 signed URL。"
|
"description": "当前账号拥有的稳定 objectKey、项目资源 ID 或素材 ID。禁止 Data URL、Blob URL 和临时 signed URL。"
|
||||||
},
|
},
|
||||||
|
"backgroundMode": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"enum": ["complex", "flat", null],
|
||||||
|
"default": "complex",
|
||||||
|
"description": "抠图模式。省略或 null 按 complex 处理;complex 使用语义分割识别前景,flat 用于纯色背景抠图。确定背景为纯色时优先使用 flat。"
|
||||||
|
},
|
||||||
|
"screenColor": {
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"pattern": "^(auto|#[0-9A-Fa-f]{6})$",
|
||||||
|
"description": "仅 flat 模式使用。可传 auto、#RRGGBB 或省略;null 等同省略。auto 和省略由服务自动检测背景色。模式省略或 complex 时提供非 null 颜色返回 400;空字符串或非法颜色返回 400。"
|
||||||
|
},
|
||||||
"projectId": {
|
"projectId": {
|
||||||
"type": ["string", "null"],
|
"type": ["string", "null"],
|
||||||
"description": "可选项目上下文。提供 targetLayerId 时必须同时提供非空 projectId,否则在入队前返回 400。"
|
"description": "可选项目上下文。提供 targetLayerId 时必须同时提供非空 projectId,否则在入队前返回 400。"
|
||||||
@@ -3249,6 +3260,14 @@
|
|||||||
"description": "画布生成占位完成指令。提供时优先按生成完成链路写入结果,targetLayerId 不参与原位替换。"
|
"description": "画布生成占位完成指令。提供时优先按生成完成链路写入结果,targetLayerId 不参与原位替换。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"if": {
|
||||||
|
"required": ["screenColor"],
|
||||||
|
"properties": { "screenColor": { "type": "string" } }
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": ["backgroundMode"],
|
||||||
|
"properties": { "backgroundMode": { "const": "flat" } }
|
||||||
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
"EditorImageGenerationResponse": {
|
"EditorImageGenerationResponse": {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 抠图模式联调验收实施计划
|
||||||
|
|
||||||
|
依据:[里程碑](./【里程碑】抠图模式联调验收-2026-09-16.md)。
|
||||||
|
|
||||||
|
1. 主站修复 worker 对 auto/省略的门禁、空值契约和旧指纹;补接口、队列与 worker 定向测试。
|
||||||
|
2. 客户端独立核对 schema、参数校验、幂等意图、Skill 与契约说明。
|
||||||
|
3. 串行请求真实 BgFilter,凭据仅在进程内读取,不输出或落库。
|
||||||
|
4. 运行 cargo test 的 background_removal、bgfilter、OpenAPI 定向过滤,客户端定向测试;运行 doc-index、encoding、diff 检查。
|
||||||
|
5. 尝试 npm run dev:api-server 与 healthz smoke;记录真实登录/全链路未验证项。
|
||||||
|
6. 收敛证据到主规范,删除临时计划。
|
||||||
|
|
||||||
|
检查点:先通过契约测试,再开展运行时核验;不因环境缺失修改生产配置。回滚仅限本次局部补丁。
|
||||||
|
|
||||||
|
当前收口:主站及客户端修复和定向检查已执行,真实 BgFilter 四组成功及自动检测失败分支已验证;本地 API 启动被现有数据库连接配置阻塞。仅完整登录/队列/资源回写运行时证据尚待补齐,测试凭据和临时日志不得提交。
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# 抠图模式联调验收
|
||||||
|
|
||||||
|
Version: 1
|
||||||
|
Status: 自动化与真实上游验收已执行;完整主站链路待本地数据库恢复
|
||||||
|
Date: 2026-09-16
|
||||||
|
Parent Spec: ../../technical/【技术方案】AGC抠图模式与背景色透传-2026-09-16.md
|
||||||
|
|
||||||
|
交付:证明客户端、External v1、队列和 BgFilter 的模式/颜色契约一致;修正范围内缺漏。
|
||||||
|
不做:401 专题、部署、主站前端变化、生成角色与图集自动选色行为变化、数据库 schema 修改。
|
||||||
|
|
||||||
|
验收:旧请求 complex;flat 三种颜色输入贯通;非法组合入队前 400;幂等包含新意图并保留旧请求指纹;真实服务返回可解码透明 PNG;实际证据与未验证环境分开记录。
|
||||||
|
依赖:既有方案及前五步代码。
|
||||||
|
|
||||||
|
剩余门禁:当前本地 SpacetimeDB 连接拒绝,导致 api-server 启动恢复未就绪。环境恢复后,用 npm run dev:api-server 验证 healthz,再以真实登录客户端提交三种 flat 请求及旧 complex 请求,确认队列完成和资源回写。不得用直连 BgFilter 测试替代此门禁;完成后将结论回写主规范并删除本计划和实施计划。
|
||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
|
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
|
||||||
> 当前口径:历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据;如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。
|
> 当前口径:历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据;如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。
|
||||||
|
## 2026-09-16 抠图模式与背景色契约
|
||||||
|
|
||||||
|
- External v1 抠图和 AGC `agc_remove_background` 支持 `complex`(语义分割识别前景)与 `flat`(纯色背景抠图);明确纯色背景优先 flat,模式缺省仍为 complex,主站前端保持现有行为。
|
||||||
|
- flat 的颜色允许 `auto`、`#RRGGBB` 或省略,自动识别完全由 BgFilter 负责。主站只校验、透传,不调用视觉模型选色;complex 携带颜色、非法值和空字符串在入队前拒绝。
|
||||||
|
- 来源、名称、模式与颜色共同区分客户端请求意图;旧参数调用及旧 External 请求的幂等指纹须保持稳定。
|
||||||
|
- 权威合同:[AGC 抠图模式与背景色透传](../../technical/【技术方案】AGC抠图模式与背景色透传-2026-09-16.md)。
|
||||||
|
|
||||||
## 2026-09-16 策划 Agent 工具执行退出项目级写锁并自动接续中断批次
|
## 2026-09-16 策划 Agent 工具执行退出项目级写锁并自动接续中断批次
|
||||||
|
|
||||||
- 背景:策划 Agent 每个 `read_file` / `write_file` / `patch_file` 工具都在执行前竞争全局项目写锁,但同一会话已由 `.agent/design-agent/active.lock` 串行化,工具目标又限定在 `design_artifacts`;项目锁既不覆盖「工具 + 会话 checkpoint」事务,还把进程中断时的 `executing=true` 不确定窗口扩大到等锁与工具执行全程。真机项目出现 `pendingBatch.executing=true`、`function_call` 无配对 output、UI 只显示工作中且无错误的状态。
|
- 背景:策划 Agent 每个 `read_file` / `write_file` / `patch_file` 工具都在执行前竞争全局项目写锁,但同一会话已由 `.agent/design-agent/active.lock` 串行化,工具目标又限定在 `design_artifacts`;项目锁既不覆盖「工具 + 会话 checkpoint」事务,还把进程中断时的 `executing=true` 不确定窗口扩大到等锁与工具执行全程。真机项目出现 `pendingBatch.executing=true`、`function_call` 无配对 output、UI 只显示工作中且无错误的状态。
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ Authorization: Bearer <internal-token>
|
|||||||
- 当前部署只有一个配置内私有 OSS bucket,因此请求只传 `sourceObjectKey`,子 worker 从自身 OSS 配置取 bucket 并生成短期签名 URL。
|
- 当前部署只有一个配置内私有 OSS bucket,因此请求只传 `sourceObjectKey`,子 worker 从自身 OSS 配置取 bucket 并生成短期签名 URL。
|
||||||
- 如果未来确实支持多个 bucket,新增字段也必须由服务端 allowlist 校验;不能接受调用方提供任意下载 URL。
|
- 如果未来确实支持多个 bucket,新增字段也必须由服务端 allowlist 校验;不能接受调用方提供任意下载 URL。
|
||||||
- `backgroundMode` 只允许 `flat / complex`;`segModel` 继续沿用当前 `birefnet / anime-seg` allowlist;complex 固定使用当前参数组合。
|
- `backgroundMode` 只允许 `flat / complex`;`segModel` 继续沿用当前 `birefnet / anime-seg` allowlist;complex 固定使用当前参数组合。
|
||||||
- `screenColor` 只对 flat 必填;complex 不得误接 flat 参数,两种模式的熔断状态必须隔离。
|
- `screenColor` 在 flat 下可省略,也可传 `auto` 或 `#RRGGBB`;省略或 `auto` 由 BgFilter 自动识别。complex 不得携带背景色,两种模式的熔断状态必须隔离。生成角色、图集等既有链路继续传已确定的背景色。
|
||||||
- `maxQueueWaitMs` 与 `callBudgetMs` 都是相对预算,不是跨机器绝对时间。前者从 admission 起约束排队阶段(worker 还会用 §5.2 的动态估计对其取 min);后者从取得 provider permit 起计时,覆盖签名、两次 attempt、结果校验和响应构造。`callBudgetMs` 是父侧按 `N / est` 公式算出的“配置指纹”,仅作核对:worker 始终以自己按同一公式派生的值执行,不一致时不拒绝请求,而是记录 warn 日志并递增漂移指标。发布调优 N / est 时新旧进程共存的瞬态漂移因此不会误伤在途任务;持久性漂移的硬拦截由部署脚本的共享 env 对齐校验承担。
|
- `maxQueueWaitMs` 与 `callBudgetMs` 都是相对预算,不是跨机器绝对时间。前者从 admission 起约束排队阶段(worker 还会用 §5.2 的动态估计对其取 min);后者从取得 provider permit 起计时,覆盖签名、两次 attempt、结果校验和响应构造。`callBudgetMs` 是父侧按 `N / est` 公式算出的“配置指纹”,仅作核对:worker 始终以自己按同一公式派生的值执行,不一致时不拒绝请求,而是记录 warn 日志并递增漂移指标。发布调优 N / est 时新旧进程共存的瞬态漂移因此不会误伤在途任务;持久性漂移的硬拦截由部署脚本的共享 env 对齐校验承担。
|
||||||
- JSON body 设置很小的固定上限;源图字节不进入该 JSON。
|
- JSON body 设置很小的固定上限;源图字节不进入该 JSON。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# AGC 抠图模式与背景色透传方案
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
主站编辑器保持现有前端行为(继续使用 `complex`),同时扩展 External v1 抠图接口和 AGC 客户端,使客户端可以选择 `complex` / `flat`,并把 `screenColor` 原样交给 BgFilter。`complex` 用语义分割识别前景,`flat` 用于纯色背景抠图;确定背景为纯色时优先使用 `flat`。`auto` 的背景色识别完全由下游服务负责,主站不读取图片、不调用模型决策颜色、不生成颜色兜底值。
|
||||||
|
|
||||||
|
## 当前 BgFilter 契约
|
||||||
|
|
||||||
|
已登录生产服务器核对 `/root/BGfilter`,当前代码版本为 `f1a0833`,运行进程为 `python -m uvicorn app:app --host 0.0.0.0 --port 6006 --workers 1 --no-access-log`。服务契约为 `POST /remove-background` multipart:
|
||||||
|
|
||||||
|
- `background_mode`:可选,`flat` 或 `complex`;
|
||||||
|
- `screen_color`:可选,支持 `#RRGGBB`、`auto` 或省略;省略/`auto` 时由 BgFilter 从图片边框自动检测;
|
||||||
|
- `complex` 模式忽略 `screen_color`;
|
||||||
|
- 自动检测失败由 BgFilter 返回 400。
|
||||||
|
- 若配置 `BGFILTER_AUTH_TOKEN`,必须发送 `X-Genarrative-Image-Token`;缺失或错误返回 401;未配置时该接口不在服务层做 token 校验。
|
||||||
|
|
||||||
|
主站向 BgFilter 发送 `#RRGGBB` 时保留 `#`,`auto` 也原样发送,不做所谓的“hex 转换”。
|
||||||
|
|
||||||
|
服务器行为:`screen_color` 在 `auto` 或省略时由服务自动检测;`#RRGGBB` 用作指定背景色。`background_mode` 缺省在 BgFilter 侧为 `flat`,因此主站必须为 External v1 旧请求显式归一化为 `complex`,不能把下游服务的默认值直接当成主站默认值。
|
||||||
|
|
||||||
|
## External v1 请求契约
|
||||||
|
|
||||||
|
接口保持:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST /api/external/v1/editor/images/background-removals
|
||||||
|
```
|
||||||
|
|
||||||
|
新增可选字段:
|
||||||
|
|
||||||
|
| 字段 | 取值 | 缺省/行为 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `backgroundMode` | `complex`、`flat` | 不填按 `complex`,保证旧客户端兼容 |
|
||||||
|
| `screenColor` | `auto` 或 `#RRGGBB` | 不填则不向 BgFilter 发送该字段 |
|
||||||
|
|
||||||
|
组合规则:
|
||||||
|
|
||||||
|
1. 不传新增字段:按 `complex` 执行。
|
||||||
|
2. `complex` 不允许传 `screenColor`,返回 400。
|
||||||
|
3. `flat` 可以传具体 `#RRGGBB`、`auto`,也可以省略颜色。
|
||||||
|
4. 模式和颜色严格按原值校验;非法值、空字符串、前后空格和大写 `AUTO` / `FLAT` 返回 400。十六进制颜色的字母允许大小写。
|
||||||
|
5. 主站只做格式和组合校验;`auto` 不在主站解析,直接转发给 BgFilter。
|
||||||
|
6. HTTP 请求中的 `null` 视同省略;模式省略时提供非 null 颜色同样违反 complex 约束。客户端 MCP 可选参数应省略,不传 null。
|
||||||
|
|
||||||
|
格式或组合错误在入队前返回 400;BgFilter 自动检测失败发生在异步执行阶段,任务通过既有失败状态收口,不把已接受的 202 改成同步 400,不启动其他抠图方式兜底。
|
||||||
|
|
||||||
|
OpenAPI 与客户端工具的对外说明只描述模式用途、参数约束和调用方可见的结果;provider 选择与兜底策略保留在内部技术方案中,不写入对外 description。
|
||||||
|
|
||||||
|
主站前端继续不传新增字段,因此用户行为不变。
|
||||||
|
|
||||||
|
## AGC 客户端改动
|
||||||
|
|
||||||
|
`agc_remove_background` 增加可选参数:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sourceLocalAssetId": "...",
|
||||||
|
"assetName": "...",
|
||||||
|
"backgroundMode": "flat",
|
||||||
|
"screenColor": "auto"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
客户端保留旧参数调用;新字段不填时不改变旧调用语义。客户端不读取图片、不自动选色、不把 `auto` 改写为具体颜色,使用原有 Bearer 认证、幂等键和队列返回模型。
|
||||||
|
|
||||||
|
工具 schema、桥接参数校验和随包 `agc-client-projection` Skill/契约说明必须保持一致。模式与颜色属于请求意图,必须参与客户端幂等指纹;同一图片与名称的不同模式不能复用同一次请求。缺省 complex 且没有颜色时保留既有指纹。主站在默认值归一化之前计算 External 请求指纹,缺失的新字段不序列化,避免旧请求重放发生冲突。
|
||||||
|
|
||||||
|
## 实施任务
|
||||||
|
|
||||||
|
### 任务一:冻结 BgFilter 契约
|
||||||
|
|
||||||
|
记录服务器已支持的模式、颜色格式、自动检测和错误行为。不得把 SSH 地址、服务器凭据写入客户端或公开契约。
|
||||||
|
|
||||||
|
### 任务二:更新主站 DTO 与 OpenAPI
|
||||||
|
|
||||||
|
为 External v1 和内部任务 DTO 增加可选字段,更新 `docs/openapi/genarrative-external-v1.openapi.json`,写明默认值、组合约束和 400 响应。
|
||||||
|
|
||||||
|
### 任务三:更新主站归一化与队列
|
||||||
|
|
||||||
|
缺省模式归一化为 `complex`;`complex + screenColor` 拒绝;`flat` 允许颜色、省略或 `auto`。队列保存字段,worker 始终发送 `background_mode`,仅在调用方提供颜色时发送 `screen_color`,值原样透传。
|
||||||
|
|
||||||
|
### 任务四:更新 AGC 客户端
|
||||||
|
|
||||||
|
增加参数 schema、请求体字段和本地校验,更新 Skill、projection contract 与测试。旧客户端请求必须继续有效。
|
||||||
|
|
||||||
|
### 任务五:联调与验收
|
||||||
|
|
||||||
|
覆盖旧请求、`flat + auto`、`flat + #RRGGBB`、`flat` 不传颜色、`complex`、`complex + screenColor` 和非法值;使用真实 BgFilter 验证 multipart 字段及自动检测错误传播。
|
||||||
|
|
||||||
|
## 依赖、发布与回滚
|
||||||
|
|
||||||
|
先发布兼容的新主站,再发布支持新参数的 AGC 客户端。主站前端无需发布改动。若联调失败,客户端可回退为只传旧字段,主站仍按 `complex` 处理;主站回滚时不改变旧字段语义。
|
||||||
|
|
||||||
|
## 验收证据
|
||||||
|
|
||||||
|
2026-09-16 实测:
|
||||||
|
|
||||||
|
- 主站 `cargo test -p api-server background_removal`:36 项通过,覆盖非法请求入队前拒绝、缺省 complex、队列参数保留、旧请求指纹、父侧内部 RPC 和 provider multipart。
|
||||||
|
- `cargo test -p api-server bgfilter`:52 项通过,包括 flat 的 auto/省略/具体颜色以及既有生成链路。
|
||||||
|
- `exported_openapi_json_contains_external_editor_routes_and_security` 契约测试通过。
|
||||||
|
- 客户端 `agent::direct_tools_mcp::tests` 18 项、`agent::skill_pack::tests` 4 项与抠图幂等指纹测试通过;Skill manifest 内容指纹已同步。主站与客户端 rustfmt、文档索引、编码及 diff 检查通过。
|
||||||
|
- 真实 BgFilter(版本 `f1a0833`):使用进程内凭据串行请求 `flat + auto`、flat 省略颜色、`flat + #CFEFFF`、complex;四组均返回 200、512×512 RGBA PNG,alpha 范围均为 0–255。
|
||||||
|
- 无纯色背景的随机噪声图片使用 flat + auto 返回 400,确认自动识别失败要求调用方提供颜色。测试没有修改服务器代码或配置。
|
||||||
|
- 本地 `npm run dev:api-server` 已尝试,但当前配置指向的 SpacetimeDB 不可连接,服务停留在启动恢复重试,`/healthz` 未通过;已结束本次启动。完整登录客户端 → 主站持久化队列 → 结果回写的运行时验收尚未完成,不能用真实 BgFilter 的独立测试代替。没有部署本次主站或客户端代码。
|
||||||
@@ -849,13 +849,13 @@ fn validate_internal_request(
|
|||||||
}
|
}
|
||||||
match request.background_mode {
|
match request.background_mode {
|
||||||
BgfilterBackgroundMode::Flat => {
|
BgfilterBackgroundMode::Flat => {
|
||||||
let screen_color = request.screen_color.as_deref().ok_or_else(|| {
|
if let Some(screen_color) = request.screen_color.as_deref()
|
||||||
WorkerFailure::new("invalid_request", "flat 请求缺少 screenColor", false)
|
&& screen_color != "auto"
|
||||||
})?;
|
&& !valid_screen_color(screen_color)
|
||||||
if !valid_screen_color(screen_color) {
|
{
|
||||||
return Err(WorkerFailure::new(
|
return Err(WorkerFailure::new(
|
||||||
"invalid_request",
|
"invalid_request",
|
||||||
"screenColor 必须是 #RRGGBB",
|
"screenColor 必须是 auto 或 #RRGGBB",
|
||||||
false,
|
false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -2811,7 +2811,13 @@ mod tests {
|
|||||||
call_budget_ms: 321_000,
|
call_budget_ms: 321_000,
|
||||||
audit_context: None,
|
audit_context: None,
|
||||||
};
|
};
|
||||||
assert!(validate_internal_request(&request, &admission).is_err());
|
assert!(validate_internal_request(&request, &admission).is_ok());
|
||||||
|
request.screen_color = Some("auto".to_string());
|
||||||
|
assert!(validate_internal_request(&request, &admission).is_ok());
|
||||||
|
for invalid in ["", "AUTO", "#12", "#GGGGGG"] {
|
||||||
|
request.screen_color = Some(invalid.to_string());
|
||||||
|
assert!(validate_internal_request(&request, &admission).is_err());
|
||||||
|
}
|
||||||
request.screen_color = Some("#CFEFFF".to_string());
|
request.screen_color = Some("#CFEFFF".to_string());
|
||||||
assert!(validate_internal_request(&request, &admission).is_ok());
|
assert!(validate_internal_request(&request, &admission).is_ok());
|
||||||
// callBudget 只是父侧配置指纹:与 worker 公式值不一致不得拒绝(发布重启窗口
|
// callBudget 只是父侧配置指纹:与 worker 公式值不一致不得拒绝(发布重启窗口
|
||||||
@@ -2854,6 +2860,85 @@ mod tests {
|
|||||||
assert!(validate_internal_request(&request, &admission).is_err());
|
assert!(validate_internal_request(&request, &admission).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn background_removal_provider_multipart_preserves_mode_and_color() {
|
||||||
|
for (mode, color) in [
|
||||||
|
(BgfilterBackgroundMode::Complex, None),
|
||||||
|
(BgfilterBackgroundMode::Flat, None),
|
||||||
|
(BgfilterBackgroundMode::Flat, Some("auto")),
|
||||||
|
(BgfilterBackgroundMode::Flat, Some("#Ab12EF")),
|
||||||
|
] {
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let address = listener.local_addr().unwrap();
|
||||||
|
let (sender, mut receiver) = tokio::sync::mpsc::channel(1);
|
||||||
|
let router = Router::new().route(
|
||||||
|
"/remove-background",
|
||||||
|
post(move |headers: HeaderMap, body: axum::body::Bytes| {
|
||||||
|
let sender = sender.clone();
|
||||||
|
async move {
|
||||||
|
sender.send((headers, body)).await.unwrap();
|
||||||
|
(
|
||||||
|
[(axum::http::header::CONTENT_TYPE, "image/png")],
|
||||||
|
encoded_png(2, 3),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
let server = tokio::spawn(async move { axum::serve(listener, router).await.unwrap() });
|
||||||
|
let state = AppState::new(AppConfig {
|
||||||
|
editor_bgfilter_base_url: format!("http://{address}"),
|
||||||
|
editor_bgfilter_token: Some("provider-test-token".to_string()),
|
||||||
|
..AppConfig::default()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let request = BgfilterInternalRequest {
|
||||||
|
request_id: "multipart-test".to_string(),
|
||||||
|
source_object_key: "editor-upload/source.png".to_string(),
|
||||||
|
background_mode: mode,
|
||||||
|
screen_color: color.map(str::to_string),
|
||||||
|
seg_model: "birefnet".to_string(),
|
||||||
|
cross_check: false,
|
||||||
|
max_queue_wait_ms: 1000,
|
||||||
|
call_budget_ms: 321000,
|
||||||
|
audit_context: None,
|
||||||
|
};
|
||||||
|
let result = request_provider_once(
|
||||||
|
&state,
|
||||||
|
&request,
|
||||||
|
"https://example.invalid/source.png",
|
||||||
|
ProviderAttemptBudget {
|
||||||
|
timeout: Duration::from_secs(5),
|
||||||
|
budget_limited: false,
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
Instant::now() + Duration::from_secs(5),
|
||||||
|
&BgfilterTaskTracker::new(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
server.abort();
|
||||||
|
let result = result.unwrap();
|
||||||
|
assert_eq!((result.width, result.height), (2, 3));
|
||||||
|
let (headers, body) = receiver.recv().await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
headers[BGFILTER_PROVIDER_TOKEN_HEADER],
|
||||||
|
"provider-test-token"
|
||||||
|
);
|
||||||
|
let body = String::from_utf8(body.to_vec()).unwrap();
|
||||||
|
assert!(body.contains(&format!(
|
||||||
|
"name=\"background_mode\"\r\n\r\n{}\r\n",
|
||||||
|
mode.as_str()
|
||||||
|
)));
|
||||||
|
assert!(body.contains("name=\"image_url\""));
|
||||||
|
assert!(!body.contains("name=\"file\""));
|
||||||
|
match color {
|
||||||
|
Some(color) => {
|
||||||
|
assert!(body.contains(&format!("name=\"screen_color\"\r\n\r\n{color}\r\n")))
|
||||||
|
}
|
||||||
|
None => assert!(!body.contains("name=\"screen_color\"")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn provider_timeout_distinguishes_full_attempt_from_budget_truncation() {
|
fn provider_timeout_distinguishes_full_attempt_from_budget_truncation() {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|||||||
@@ -1019,6 +1019,52 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn background_removal_identity_preserves_old_dto_and_distinguishes_options() {
|
||||||
|
let legacy = json!({
|
||||||
|
"sourceImageSrc": "resource-source", "projectId": null,
|
||||||
|
"targetLayerId": null, "assetKind": null, "generationInputs": null,
|
||||||
|
"assetFolderId": null, "assetLabel": null, "sourceResourceId": null,
|
||||||
|
"taskId": null, "canvasCompletion": null,
|
||||||
|
});
|
||||||
|
let old = external_api_editor_generation_request_identity(
|
||||||
|
"user-1",
|
||||||
|
EDITOR_BACKGROUND_REMOVAL_JOB_KIND,
|
||||||
|
&legacy,
|
||||||
|
"stable-key",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let restored: crate::editor_project::EditorBackgroundRemovalRequest =
|
||||||
|
serde_json::from_value(legacy.clone()).unwrap();
|
||||||
|
let current = external_api_editor_generation_request_identity(
|
||||||
|
"user-1",
|
||||||
|
EDITOR_BACKGROUND_REMOVAL_JOB_KIND,
|
||||||
|
&restored,
|
||||||
|
"stable-key",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(old.request_fingerprint, current.request_fingerprint);
|
||||||
|
assert_eq!(old.job_id, current.job_id);
|
||||||
|
let mut fingerprints = std::collections::HashSet::new();
|
||||||
|
fingerprints.insert(current.request_fingerprint);
|
||||||
|
for color in [None, Some("auto"), Some("#CFEFFF"), Some("#112233")] {
|
||||||
|
let mut body = legacy.clone();
|
||||||
|
body["backgroundMode"] = json!("flat");
|
||||||
|
if let Some(color) = color {
|
||||||
|
body["screenColor"] = json!(color);
|
||||||
|
}
|
||||||
|
let changed = external_api_editor_generation_request_identity(
|
||||||
|
"user-1",
|
||||||
|
EDITOR_BACKGROUND_REMOVAL_JOB_KIND,
|
||||||
|
&body,
|
||||||
|
"stable-key",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(old.job_id, changed.job_id);
|
||||||
|
assert!(fingerprints.insert(changed.request_fingerprint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn external_api_dedupe_key_preserves_legacy_hash_bytes() {
|
fn external_api_dedupe_key_preserves_legacy_hash_bytes() {
|
||||||
let dedupe_key = build_editor_generation_dedupe_key(
|
let dedupe_key = build_editor_generation_dedupe_key(
|
||||||
|
|||||||
@@ -452,6 +452,10 @@ pub struct EditorImageEditRequest {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct EditorBackgroundRemovalRequest {
|
pub struct EditorBackgroundRemovalRequest {
|
||||||
pub(crate) source_image_src: String,
|
pub(crate) source_image_src: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub(crate) background_mode: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub(crate) screen_color: Option<String>,
|
||||||
pub(crate) project_id: Option<String>,
|
pub(crate) project_id: Option<String>,
|
||||||
pub(crate) target_layer_id: Option<String>,
|
pub(crate) target_layer_id: Option<String>,
|
||||||
pub(crate) asset_kind: Option<String>,
|
pub(crate) asset_kind: Option<String>,
|
||||||
@@ -6300,6 +6304,7 @@ pub(crate) async fn enqueue_editor_background_removal_for_owner(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
normalize_editor_background_removal_options(&mut payload)?;
|
||||||
ensure_editor_reference_image_source_is_stable(
|
ensure_editor_reference_image_source_is_stable(
|
||||||
payload.source_image_src.as_str(),
|
payload.source_image_src.as_str(),
|
||||||
"editor-background-removal",
|
"editor-background-removal",
|
||||||
@@ -6401,6 +6406,52 @@ pub(crate) async fn enqueue_editor_background_removal_for_owner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_editor_background_removal_options(
|
||||||
|
payload: &mut EditorBackgroundRemovalRequest,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let mode = payload
|
||||||
|
.background_mode
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "complex".to_string());
|
||||||
|
if mode != "complex" && mode != "flat" {
|
||||||
|
return Err(
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "editor-background-removal",
|
||||||
|
"field": "backgroundMode",
|
||||||
|
"message": "backgroundMode 必须是 complex 或 flat",
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let color = payload.screen_color.as_deref();
|
||||||
|
if mode == "complex" && color.is_some() {
|
||||||
|
return Err(
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "editor-background-removal",
|
||||||
|
"field": "screenColor",
|
||||||
|
"message": "只有 flat 模式可以提供 screenColor",
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(color) = color
|
||||||
|
&& color != "auto"
|
||||||
|
&& !(color.len() == 7
|
||||||
|
&& color.starts_with('#')
|
||||||
|
&& color[1..]
|
||||||
|
.chars()
|
||||||
|
.all(|character| character.is_ascii_hexdigit()))
|
||||||
|
{
|
||||||
|
return Err(
|
||||||
|
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||||
|
"provider": "editor-background-removal",
|
||||||
|
"field": "screenColor",
|
||||||
|
"message": "screenColor 必须是 auto 或 #RRGGBB",
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
payload.background_mode = Some(mode);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_editor_background_removal_canvas_target(
|
fn resolve_editor_background_removal_canvas_target(
|
||||||
project_id: Option<&str>,
|
project_id: Option<&str>,
|
||||||
target_layer_id: Option<&str>,
|
target_layer_id: Option<&str>,
|
||||||
@@ -6650,6 +6701,7 @@ pub(crate) async fn remove_editor_image_background_for_owner(
|
|||||||
caller: EditorGenerationCaller,
|
caller: EditorGenerationCaller,
|
||||||
mut payload: EditorBackgroundRemovalRequest,
|
mut payload: EditorBackgroundRemovalRequest,
|
||||||
) -> Result<Json<Value>, AppError> {
|
) -> Result<Json<Value>, AppError> {
|
||||||
|
normalize_editor_background_removal_options(&mut payload)?;
|
||||||
payload.generation_inputs =
|
payload.generation_inputs =
|
||||||
sanitize_editor_client_generation_inputs(payload.generation_inputs.take());
|
sanitize_editor_client_generation_inputs(payload.generation_inputs.take());
|
||||||
let started_at = Instant::now();
|
let started_at = Instant::now();
|
||||||
@@ -6725,6 +6777,8 @@ pub(crate) async fn remove_editor_image_background_for_owner(
|
|||||||
let removed = request_editor_background_removal_image_with_bgfilter_worker(
|
let removed = request_editor_background_removal_image_with_bgfilter_worker(
|
||||||
state,
|
state,
|
||||||
source.object_key.as_str(),
|
source.object_key.as_str(),
|
||||||
|
payload.background_mode.as_deref().unwrap_or("complex"),
|
||||||
|
payload.screen_color.as_deref(),
|
||||||
&matting_audit,
|
&matting_audit,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -7948,15 +8002,21 @@ fn editor_bgfilter_flat_deadline_reserve(aliyun_timeout_ms: u64) -> Duration {
|
|||||||
async fn request_editor_background_removal_image_with_bgfilter_worker(
|
async fn request_editor_background_removal_image_with_bgfilter_worker(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
source_object_key: &str,
|
source_object_key: &str,
|
||||||
|
background_mode: &str,
|
||||||
|
screen_color: Option<&str>,
|
||||||
audit: &crate::external_api_audit::ExternalApiAuditContext,
|
audit: &crate::external_api_audit::ExternalApiAuditContext,
|
||||||
) -> Result<EditorBackgroundRemovalImage, AppError> {
|
) -> Result<EditorBackgroundRemovalImage, AppError> {
|
||||||
// complex 没有 flat fallback,排队上限只预留 2s 传输窗;worker 重启窗口内的
|
// 独立抠图两种模式都不进入生成链路的 fallback,排队上限只预留 2s 传输窗;worker 重启窗口内的
|
||||||
// 连接失败由 client 内部按预算有界重试,避免 max_attempts=1 的队列任务终态失败。
|
// 连接失败由 client 内部按预算有界重试,避免 max_attempts=1 的队列任务终态失败。
|
||||||
|
let mode = match background_mode {
|
||||||
|
"flat" => crate::bgfilter_worker::BgfilterBackgroundMode::Flat,
|
||||||
|
_ => crate::bgfilter_worker::BgfilterBackgroundMode::Complex,
|
||||||
|
};
|
||||||
let removed = crate::bgfilter_worker::request_bgfilter_worker_with_connect_retry(
|
let removed = crate::bgfilter_worker::request_bgfilter_worker_with_connect_retry(
|
||||||
state,
|
state,
|
||||||
source_object_key,
|
source_object_key,
|
||||||
crate::bgfilter_worker::BgfilterBackgroundMode::Complex,
|
mode,
|
||||||
None,
|
screen_color,
|
||||||
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
|
EDITOR_BGFILTER_DEFAULT_SEG_MODEL,
|
||||||
EDITOR_BGFILTER_CROSS_CHECK_DISABLED,
|
EDITOR_BGFILTER_CROSS_CHECK_DISABLED,
|
||||||
EDITOR_BGFILTER_PARENT_TRANSPORT_WINDOW,
|
EDITOR_BGFILTER_PARENT_TRANSPORT_WINDOW,
|
||||||
@@ -13669,6 +13729,111 @@ mod tests {
|
|||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn background_removal_options_preserve_queue_parameters_and_legacy_identity() {
|
||||||
|
for (fields, mode, color) in [
|
||||||
|
(json!({}), "complex", None),
|
||||||
|
(json!({"backgroundMode": "complex"}), "complex", None),
|
||||||
|
(json!({"backgroundMode": "flat"}), "flat", None),
|
||||||
|
(
|
||||||
|
json!({"backgroundMode": "flat", "screenColor": "auto"}),
|
||||||
|
"flat",
|
||||||
|
Some("auto"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
json!({"backgroundMode": "flat", "screenColor": "#Ab12EF"}),
|
||||||
|
"flat",
|
||||||
|
Some("#Ab12EF"),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let mut input = json!({"sourceImageSrc": "resource-source"});
|
||||||
|
input
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.extend(fields.as_object().unwrap().clone());
|
||||||
|
let mut payload: EditorBackgroundRemovalRequest =
|
||||||
|
serde_json::from_value(input).unwrap();
|
||||||
|
if payload.background_mode.is_none() {
|
||||||
|
let raw = serde_json::to_value(&payload).unwrap();
|
||||||
|
assert!(raw.get("backgroundMode").is_none());
|
||||||
|
assert!(raw.get("screenColor").is_none());
|
||||||
|
}
|
||||||
|
normalize_editor_background_removal_options(&mut payload).unwrap();
|
||||||
|
let restored: EditorBackgroundRemovalRequest =
|
||||||
|
serde_json::from_str(&serde_json::to_string(&payload).unwrap()).unwrap();
|
||||||
|
assert_eq!(restored.background_mode.as_deref(), Some(mode));
|
||||||
|
assert_eq!(restored.screen_color.as_deref(), color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn background_removal_options_reject_invalid_values_and_color_without_flat() {
|
||||||
|
for fields in [
|
||||||
|
json!({"backgroundMode": ""}),
|
||||||
|
json!({"backgroundMode": "FLAT"}),
|
||||||
|
json!({"backgroundMode": " flat "}),
|
||||||
|
json!({"backgroundMode": "other"}),
|
||||||
|
json!({"screenColor": "auto"}),
|
||||||
|
json!({"backgroundMode": "complex", "screenColor": "auto"}),
|
||||||
|
json!({"backgroundMode": "complex", "screenColor": ""}),
|
||||||
|
json!({"backgroundMode": "flat", "screenColor": ""}),
|
||||||
|
json!({"backgroundMode": "flat", "screenColor": "AUTO"}),
|
||||||
|
json!({"backgroundMode": "flat", "screenColor": " auto "}),
|
||||||
|
json!({"backgroundMode": "flat", "screenColor": "#GGGGGG"}),
|
||||||
|
json!({"backgroundMode": "flat", "screenColor": "CFEFFF"}),
|
||||||
|
] {
|
||||||
|
let mut input = json!({"sourceImageSrc": "resource-source"});
|
||||||
|
input
|
||||||
|
.as_object_mut()
|
||||||
|
.unwrap()
|
||||||
|
.extend(fields.as_object().unwrap().clone());
|
||||||
|
let mut payload: EditorBackgroundRemovalRequest =
|
||||||
|
serde_json::from_value(input).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
normalize_editor_background_removal_options(&mut payload)
|
||||||
|
.unwrap_err()
|
||||||
|
.status_code(),
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"{fields}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn background_removal_flat_options_reach_internal_worker_unchanged() {
|
||||||
|
for color in [None, Some("auto"), Some("#Ab12EF")] {
|
||||||
|
let response_png = encode_test_png(3, 2);
|
||||||
|
let (base_url, receiver, server) = spawn_bgfilter_worker_png_mock(response_png.clone());
|
||||||
|
let state = AppState::new(AppConfig {
|
||||||
|
bgfilter_worker_base_url: base_url,
|
||||||
|
bgfilter_internal_token: Some("flat-test-token".to_string()),
|
||||||
|
..AppConfig::default()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let audit = crate::external_api_audit::ExternalApiAuditContext {
|
||||||
|
user_id: None,
|
||||||
|
profile_id: None,
|
||||||
|
request_id: None,
|
||||||
|
external_call_deadline: None,
|
||||||
|
};
|
||||||
|
let output = request_editor_background_removal_image_with_bgfilter_worker(
|
||||||
|
&state,
|
||||||
|
"generated-character-drafts/editor/source.png",
|
||||||
|
"flat",
|
||||||
|
color,
|
||||||
|
&audit,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let request = receiver.recv_timeout(Duration::from_secs(1)).unwrap();
|
||||||
|
server.join().unwrap();
|
||||||
|
let payload = parse_mock_http_json_body(&request);
|
||||||
|
assert_eq!(payload["backgroundMode"], json!("flat"));
|
||||||
|
assert_eq!(payload["screenColor"], json!(color));
|
||||||
|
assert_eq!(output.image.bytes, response_png);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ordinary_image_prompt_validation_stays_outside_prompt_builder_execution() {
|
fn ordinary_image_prompt_validation_stays_outside_prompt_builder_execution() {
|
||||||
assert!(validate_editor_image_generation_prompt("生成森林场景").is_ok());
|
assert!(validate_editor_image_generation_prompt("生成森林场景").is_ok());
|
||||||
@@ -22055,6 +22220,8 @@ mod tests {
|
|||||||
let removed = request_editor_background_removal_image_with_bgfilter_worker(
|
let removed = request_editor_background_removal_image_with_bgfilter_worker(
|
||||||
&state,
|
&state,
|
||||||
"generated-character-drafts/editor/manual-source.png",
|
"generated-character-drafts/editor/manual-source.png",
|
||||||
|
"complex",
|
||||||
|
None,
|
||||||
&audit,
|
&audit,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ pub struct ExternalEditorProjectResourceCreateRequest {
|
|||||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||||
pub(crate) struct ExternalEditorBackgroundRemovalRequest {
|
pub(crate) struct ExternalEditorBackgroundRemovalRequest {
|
||||||
source_image_src: String,
|
source_image_src: String,
|
||||||
|
background_mode: Option<String>,
|
||||||
|
screen_color: Option<String>,
|
||||||
project_id: Option<String>,
|
project_id: Option<String>,
|
||||||
target_layer_id: Option<String>,
|
target_layer_id: Option<String>,
|
||||||
asset_kind: Option<String>,
|
asset_kind: Option<String>,
|
||||||
@@ -183,6 +185,8 @@ impl From<ExternalEditorBackgroundRemovalRequest> for EditorBackgroundRemovalReq
|
|||||||
fn from(payload: ExternalEditorBackgroundRemovalRequest) -> Self {
|
fn from(payload: ExternalEditorBackgroundRemovalRequest) -> Self {
|
||||||
Self {
|
Self {
|
||||||
source_image_src: payload.source_image_src,
|
source_image_src: payload.source_image_src,
|
||||||
|
background_mode: payload.background_mode,
|
||||||
|
screen_color: payload.screen_color,
|
||||||
project_id: payload.project_id,
|
project_id: payload.project_id,
|
||||||
target_layer_id: payload.target_layer_id,
|
target_layer_id: payload.target_layer_id,
|
||||||
asset_kind: payload.asset_kind,
|
asset_kind: payload.asset_kind,
|
||||||
@@ -1864,7 +1868,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn external_background_removal_rejects_undocumented_fields_before_queueing() {
|
async fn external_background_removal_rejects_invalid_parameters_before_queueing() {
|
||||||
let state = AppState::new(crate::config::AppConfig::default())
|
let state = AppState::new(crate::config::AppConfig::default())
|
||||||
.expect("external background removal test state should build");
|
.expect("external background removal test state should build");
|
||||||
state.fail_test_editor_generation_enqueue();
|
state.fail_test_editor_generation_enqueue();
|
||||||
@@ -1886,6 +1890,26 @@ mod tests {
|
|||||||
json!({"taskId": "caller-controlled-task"}),
|
json!({"taskId": "caller-controlled-task"}),
|
||||||
),
|
),
|
||||||
("unknown field", json!({"unexpected": true})),
|
("unknown field", json!({"unexpected": true})),
|
||||||
|
("invalid mode", json!({"backgroundMode": "unknown"})),
|
||||||
|
("empty mode", json!({"backgroundMode": ""})),
|
||||||
|
("uppercase mode", json!({"backgroundMode": "FLAT"})),
|
||||||
|
("color without flat", json!({"screenColor": "auto"})),
|
||||||
|
(
|
||||||
|
"complex with color",
|
||||||
|
json!({"backgroundMode": "complex", "screenColor": "#123456"}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"empty color",
|
||||||
|
json!({"backgroundMode": "flat", "screenColor": ""}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"invalid color",
|
||||||
|
json!({"backgroundMode": "flat", "screenColor": "red"}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"non-string color",
|
||||||
|
json!({"backgroundMode": "flat", "screenColor": 123}),
|
||||||
|
),
|
||||||
] {
|
] {
|
||||||
let mut request_body = json!({"sourceImageSrc": "editor-upload/source.png"});
|
let mut request_body = json!({"sourceImageSrc": "editor-upload/source.png"});
|
||||||
request_body
|
request_body
|
||||||
@@ -2844,6 +2868,25 @@ mod tests {
|
|||||||
["EditorBackgroundRemovalRequest"]["properties"]["targetLayerId"]["description"]
|
["EditorBackgroundRemovalRequest"]["properties"]["targetLayerId"]["description"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.expect("background removal targetLayerId should document placement semantics");
|
.expect("background removal targetLayerId should document placement semantics");
|
||||||
|
let background_properties =
|
||||||
|
&parsed["components"]["schemas"]["EditorBackgroundRemovalRequest"]["properties"];
|
||||||
|
assert_eq!(
|
||||||
|
background_properties["backgroundMode"]["enum"],
|
||||||
|
json!(["complex", "flat", null])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
background_properties["backgroundMode"]["default"],
|
||||||
|
"complex"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
background_properties["screenColor"]["pattern"],
|
||||||
|
"^(auto|#[0-9A-Fa-f]{6})$"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parsed["components"]["schemas"]["EditorBackgroundRemovalRequest"]["then"]["properties"]
|
||||||
|
["backgroundMode"]["const"],
|
||||||
|
"flat"
|
||||||
|
);
|
||||||
assert!(background_target_description.contains("projectId"));
|
assert!(background_target_description.contains("projectId"));
|
||||||
assert!(background_target_description.contains("canvasCompletion"));
|
assert!(background_target_description.contains("canvasCompletion"));
|
||||||
assert!(background_target_description.contains("assetObjectId"));
|
assert!(background_target_description.contains("assetObjectId"));
|
||||||
|
|||||||
+1
-1
@@ -68,7 +68,7 @@ export default defineConfig({
|
|||||||
environment: 'node',
|
environment: 'node',
|
||||||
globals: true,
|
globals: true,
|
||||||
minThreads: 1,
|
minThreads: 1,
|
||||||
maxThreads: 1,
|
maxThreads: 8,
|
||||||
server: {
|
server: {
|
||||||
deps: {
|
deps: {
|
||||||
inline: [
|
inline: [
|
||||||
|
|||||||
Reference in New Issue
Block a user