diff --git a/server-rs/crates/shared-contracts/src/assets.rs b/server-rs/crates/shared-contracts/src/assets.rs index c1dd65e71..f5345a67e 100644 --- a/server-rs/crates/shared-contracts/src/assets.rs +++ b/server-rs/crates/shared-contracts/src/assets.rs @@ -659,8 +659,11 @@ impl<'de> Deserialize<'de> for EditorSoundEffectGenerationMetadataV2 { } } +/// OpenAPI 对该请求体声明了 `additionalProperties: false`,实现必须同样拒绝未知字段: +/// 否则 `loopEnabled` 这类拼错的字段会被静默丢弃,按 `loop=false` 入队并计费, +/// 客户端拿不到 400,只能从生成结果反推自己写错了参数。 #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct EditorSoundEffectGenerateRequest { pub prompt: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1613,6 +1616,20 @@ mod tests { assert!(!parsed.loop_enabled); } + // OpenAPI 声明 additionalProperties: false,实现必须拒绝而不是静默丢弃。 + // loopEnabled 是最容易写错的那个:丢弃后会按 loop=false 入队并照常计费。 + for unknown_field_payload in [ + json!({ "prompt": "按钮确认短促音", "loopEnabled": true }), + json!({ "prompt": "按钮确认短促音", "tempo": 120 }), + json!({ "prompt": "按钮确认短促音", "priceMudPoints": 0 }), + ] { + assert!( + serde_json::from_value::(unknown_field_payload) + .is_err(), + "sound effect requests must reject unknown fields" + ); + } + let music_payload = serde_json::to_value(EditorBackgroundMusicGenerateRequest { gpt_description_prompt: "森林冒险背景音乐".to_string(), make_instrumental: true,