diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 94fe87ed5..41601a092 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -14,6 +14,14 @@ - 关联:相关文件、文档、提交或 Issue ``` +## 工具 JSON Schema 的条件约束必须覆盖运行时默认值 + +- 现象:LLM 按工具 schema 生成的参数可以通过结构约束,但参数补默认值后被运行时校验拒绝,白白消耗一次工具修复轮次。例如固定 `gpt-image-2` 的 UI 工具仍暴露 `0.5K`,或视频调用省略 `model` 时 schema 允许 `1080p`,运行时却默认成 `seedance2.0-fast` 后拒绝。 +- 原因:通用枚举 schema 被固定模型工具直接复用;JSON Schema 的 `if` 又用 `required: ["model"]` 排除了字段缺失场景,而 Serde 默认值只在 schema 校验之后生效。description 只能提示 LLM,不能替代 `enum` / `if` / `then` 的结构约束。 +- 处理:固定模型工具使用与该模型能力一致的专用枚举;可切换模型的图片工具在对象层复用共享 `model + image_size` 条件约束。条件字段有运行时默认值时,省略字段必须落入默认模型对应的 schema 分支:默认 nanobanana2 的图片工具只在显式选择 `gpt-image-2` 时收紧尺寸,所以条件保留 `required: ["model"]`;默认 fast 的视频工具则利用字段缺失时 `properties.model.const` 条件成立的语义,不额外要求 `model` 存在。运行时校验仍保留为最终防线。 +- 验证:锁定 `generate-ui-design.image_size = ["1K", "2K"]`,三个可切换图片模型的工具都接入共享 `gpt-image-2 -> image_size = ["1K", "2K"]` 条件,以及视频 fast 条件没有内层 `required`、其 `then.resolution = ["480p", "720p"]`;同时保留运行时拒绝 `gpt-image-2 + 0.5K` 与 `seedance2.0-fast + 1080p` 的测试。 +- 关联:`server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs`、`server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs`、`server-rs/crates/platform-editor-agent/src/agent/tools/generate_video.rs`、`docs/【编辑器】画布Agent对话面板-2026-07-03.md`。 + ## 图片生成的 K 档不能靠回图后缩放实现 - 现象:用户选择 2K 时占位框看起来是 2K,最终资源元数据也显示为 2K,但模型请求实际仍是固定 1K 或竖版回落尺寸;画面只是后端放大后的低分辨率结果。 diff --git a/docs/【编辑器】画布Agent对话面板-2026-07-03.md b/docs/【编辑器】画布Agent对话面板-2026-07-03.md index 6a6a7c131..2526044dd 100644 --- a/docs/【编辑器】画布Agent对话面板-2026-07-03.md +++ b/docs/【编辑器】画布Agent对话面板-2026-07-03.md @@ -28,6 +28,7 @@ - 用户要求“规范图 / 视觉规范图 / 风格规范图 / 素材规范展板”时,规划默认选择 `generate_image`,并在 prompt 中明确要求生成规范展板,包含统一视角、线条粗细、色卡、材质、阴影、圆角、状态层级、尺寸标注等可落地的视觉规范元素。 - 用户要求“角色规范图”且语义是角色的规范展板、风格展板或设定板时,仍走 `generate_image`,不要误分流到 `generate_character`;只有实际生成角色立绘、角色主形象或角色视觉资产时才走 `generate_character`。用户要求多个图标素材、图集或 spritesheet 时才走 `generate_icon_spritesheet`。 - 所有生成必须走 `execute_billable_asset_operation_with_cost` 与模型定价配置,禁止绕过定价收口。 +- function-calling 的 JSON Schema 必须与参数默认值和运行时校验保持一致,不能只在 description 中提示会被运行时拒绝的组合。`generate-ui-design` 固定 `gpt-image-2`,因此 `image_size` 只暴露 `1K / 2K`;其它可切换图片模型的工具通过共享条件 schema 在显式选择 `gpt-image-2` 时同样把 `image_size` 限制为 `1K / 2K`,省略模型时仍按默认 nanobanana2 允许 `0.5K`。`generate-video` 省略 `model` 时按默认 `seedance2.0-fast` 约束 `resolution` 为 `480p / 720p`,显式选择其它模型时仍使用其现有分辨率范围。运行时强类型校验继续作为最终防线。 - 图层操作及其他未注册的画板功能第一期不进入对话工具面,仍走现有面板。 ## 当前分支落地状态 diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/edit_image.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/edit_image.rs index 2d52be188..e72210d94 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/edit_image.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/edit_image.rs @@ -88,6 +88,7 @@ impl Tool for EditImageTool { "type": "string", "description": "编辑提示词,描述希望如何修改图片。例如「把背景换成红色」、「把人物改成坐着」。" }, + // TODO need to introduce size param, but that needs more metadata such as original image size, skip in this version "model": { "type": "string", "enum": [GPT_IMAGE_2_MODEL], diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_character.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_character.rs index 801c2816c..fd331d759 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_character.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_character.rs @@ -6,7 +6,8 @@ use crate::agent::tools::generate_image::{ }; use crate::agent::tools::image_generation_options::{ default_image_aspect_ratio, default_image_model, default_image_size, - image_aspect_ratio_parameter_schema, image_model_parameter_schema, image_size_parameter_schema, + image_aspect_ratio_parameter_schema, image_model_parameter_schema, + image_model_size_constraint_schema, image_size_parameter_schema, validate_image_generation_options, }; use crate::framework::tool::ToolFailureKind; @@ -53,7 +54,8 @@ impl Tool for GenerateCharacterTool { "image_size": image_size_parameter_schema(), }, "required": ["prompt"], - "additionalProperties": false + "additionalProperties": false, + "allOf": [image_model_size_constraint_schema()] }) } diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_icon_spritesheet.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_icon_spritesheet.rs index 180def213..684f39b35 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_icon_spritesheet.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_icon_spritesheet.rs @@ -4,7 +4,8 @@ use crate::agent::tools::context::EditorToolContext; use crate::agent::tools::image_generation_options::{ ImageGenerationOptionsError, default_image_aspect_ratio, default_image_model, default_image_size, image_aspect_ratio_parameter_schema, image_model_parameter_schema, - image_size_parameter_schema, validate_image_generation_options, + image_model_size_constraint_schema, image_size_parameter_schema, + validate_image_generation_options, }; use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind}; use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL}; @@ -133,7 +134,8 @@ impl Tool for GenerateIconSpritesheetTool { "image_size": image_size_parameter_schema() }, "required": ["reference_image_id", "icon_descriptions"], - "additionalProperties": false + "additionalProperties": false, + "allOf": [image_model_size_constraint_schema()] }) } diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_image.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_image.rs index e2700b17a..984cd20ad 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_image.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_image.rs @@ -4,7 +4,8 @@ use crate::agent::tools::context::EditorToolContext; use crate::agent::tools::image_generation_options::{ ImageGenerationOptionsError, default_image_aspect_ratio, default_image_model, default_image_size, image_aspect_ratio_parameter_schema, image_model_parameter_schema, - image_size_parameter_schema, validate_image_generation_options, + image_model_size_constraint_schema, image_size_parameter_schema, + validate_image_generation_options, }; use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind}; use platform_image::{GPT_IMAGE_2_MODEL, NANOBANANA_2_MODEL}; @@ -113,7 +114,8 @@ impl Tool for GenerateImageTool { "image_size": image_size_parameter_schema() }, "required": ["prompt"], - "additionalProperties": false + "additionalProperties": false, + "allOf": [image_model_size_constraint_schema()] }) } diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs index bf12b3192..73bdaea56 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_ui_design.rs @@ -5,8 +5,8 @@ use crate::agent::tools::generate_image::{ EditorImageGenerationResult, GenerateImageError, GenerateImageToolOutput, }; use crate::agent::tools::image_generation_options::{ - default_image_aspect_ratio, default_image_size, image_aspect_ratio_parameter_schema, - image_size_parameter_schema, validate_image_generation_options, + default_image_aspect_ratio, default_image_size, gpt_image_2_size_parameter_schema, + image_aspect_ratio_parameter_schema, validate_image_generation_options, }; use crate::framework::tool::ToolFailureKind; use crate::framework::tool::{Tool, ToolFailure}; @@ -58,7 +58,7 @@ impl Tool for GenerateUiDesignTool { }, "reference_image_ids": { "type": "array", "items": { "type": "string" }, "description": "image_id(s) for desc UI 风格或布局" }, "aspect_ratio": image_aspect_ratio_parameter_schema(), - "image_size": image_size_parameter_schema() + "image_size": gpt_image_2_size_parameter_schema() }, "required": ["prompt", "model"], "additionalProperties": false @@ -169,6 +169,14 @@ mod tests { parameters["properties"]["model"]["default"], GPT_IMAGE_2_MODEL ); + assert_eq!( + parameters["properties"]["image_size"]["enum"], + json!(["1K", "2K"]) + ); + assert_eq!( + parameters["properties"]["image_size"]["default"], + json!("1K") + ); assert!( parameters["required"] .as_array() diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_video.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_video.rs index d3e800839..6d4d5b14d 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/generate_video.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/generate_video.rs @@ -125,9 +125,10 @@ impl Tool for GenerateVideoTool { "additionalProperties": false, "allOf": [ { + // model 省略时运行时会补为 fast;这里故意不要求 model 存在, + // 让 JSON Schema 对省略 model 的调用应用同一分辨率约束。 "if": { - "properties": { "model": { "const": GenerateVideoTool::SEEDANCE_2_FAST_MODEL } }, - "required": ["model"] + "properties": { "model": { "const": GenerateVideoTool::SEEDANCE_2_FAST_MODEL } } }, "then": { "properties": { "resolution": { "enum": GenerateVideoTool::SEEDANCE_2_FAST_RESOLUTIONS } } @@ -360,5 +361,15 @@ mod tests { ); assert_eq!(schema["properties"]["sound"]["default"], json!("on")); assert_eq!(schema["allOf"].as_array().map(Vec::len), Some(2)); + let fast_model_condition = &schema["allOf"][0]; + assert_eq!( + fast_model_condition["if"]["properties"]["model"]["const"], + json!(GenerateVideoTool::DEFAULT_VIDEO_MODEL) + ); + assert!(fast_model_condition["if"].get("required").is_none()); + assert_eq!( + fast_model_condition["then"]["properties"]["resolution"]["enum"], + json!(["480p", "720p"]) + ); } } diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs index a76b2e3d2..c4f2ab218 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/image_generation_options.rs @@ -93,12 +93,36 @@ pub fn image_aspect_ratio_parameter_schema() -> Value { pub fn image_size_parameter_schema() -> Value { json!({ "type": "string", - "enum": ["0.5K", "1K", "2K"], + "enum": NANOBANANA_2_IMAGE_SIZES, "default": DEFAULT_IMAGE_SIZE, "description": "图片尺寸档位。nanobanana2 支持 0.5K、1K、2K;gpt-image-2 仅支持 1K、2K;默认 1K。" }) } +pub fn gpt_image_2_size_parameter_schema() -> Value { + json!({ + "type": "string", + "enum": GPT_IMAGE_2_IMAGE_SIZES, + "default": DEFAULT_IMAGE_SIZE, + "description": "图片尺寸档位。gpt-image-2 仅支持 1K、2K;默认 1K。" + }) +} + +pub fn image_model_size_constraint_schema() -> Value { + json!({ + "if": { + "properties": { "model": { "const": GPT_IMAGE_2_MODEL } }, + // model 省略时运行时默认 nanobanana2,仍允许 0.5K。 + "required": ["model"] + }, + "then": { + "properties": { + "image_size": { "enum": GPT_IMAGE_2_IMAGE_SIZES } + } + } + }) +} + fn supported_image_sizes(model: &str) -> Option<&'static [&'static str]> { match model { NANOBANANA_2_MODEL => Some(NANOBANANA_2_IMAGE_SIZES), @@ -157,5 +181,23 @@ mod tests { json!(["0.5K", "1K", "2K"]) ); assert_eq!(image_size_parameter_schema()["default"], DEFAULT_IMAGE_SIZE); + assert_eq!( + gpt_image_2_size_parameter_schema()["enum"], + json!(["1K", "2K"]) + ); + assert_eq!( + gpt_image_2_size_parameter_schema()["default"], + DEFAULT_IMAGE_SIZE + ); + let model_size_constraint = image_model_size_constraint_schema(); + assert_eq!( + model_size_constraint["if"]["properties"]["model"]["const"], + GPT_IMAGE_2_MODEL + ); + assert_eq!(model_size_constraint["if"]["required"], json!(["model"])); + assert_eq!( + model_size_constraint["then"]["properties"]["image_size"]["enum"], + json!(["1K", "2K"]) + ); } } diff --git a/server-rs/crates/platform-editor-agent/src/agent/tools/mod.rs b/server-rs/crates/platform-editor-agent/src/agent/tools/mod.rs index b925e1fb3..ba4a6e188 100644 --- a/server-rs/crates/platform-editor-agent/src/agent/tools/mod.rs +++ b/server-rs/crates/platform-editor-agent/src/agent/tools/mod.rs @@ -18,7 +18,9 @@ mod tests { GenerateBackgroundMusicTool, GenerateBackgroundMusicToolArgs, }; use super::generate_character::{GenerateCharacterTool, GenerateCharacterToolArgs}; - use super::generate_icon_spritesheet::GenerateIconSpritesheetToolArgs; + use super::generate_icon_spritesheet::{ + GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs, + }; use super::generate_image::{GenerateImageError, GenerateImageTool, GenerateImageToolArgs}; use super::generate_sound_effect::{GenerateSoundEffectTool, GenerateSoundEffectToolArgs}; use super::generate_ui_design::{GenerateUiDesignTool, GenerateUiDesignToolArgs}; @@ -188,6 +190,18 @@ mod tests { #[test] fn confirmation_schemas_expose_all_finite_choices_as_enums() { let context = EditorToolContext::default(); + let image = GenerateImageTool { + context: context.clone(), + } + .parameters(); + let character = GenerateCharacterTool { + context: context.clone(), + } + .parameters(); + let icon = GenerateIconSpritesheetTool { + context: context.clone(), + } + .parameters(); let edit = EditImageTool { context: context.clone(), } @@ -218,5 +232,16 @@ mod tests { assert_eq!(sound["properties"]["duration"]["default"], json!(5)); assert_eq!(video["properties"]["sound"]["default"], json!("on")); assert!(music["properties"].get("make_instrumental").is_none()); + + for schema in [&image, &character, &icon] { + assert_eq!( + schema["allOf"][0]["if"]["properties"]["model"]["const"], + json!(GPT_IMAGE_2_MODEL) + ); + assert_eq!( + schema["allOf"][0]["then"]["properties"]["image_size"]["enum"], + json!(["1K", "2K"]) + ); + } } }