diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index 12885c91a..c5f022286 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -4162,6 +4162,7 @@ version = "0.1.0" dependencies = [ "bytes", "reqwest", + "serde", "serde_json", "shared-contracts", "tokio", diff --git a/server-rs/crates/api-server/src/tripo3d/job.rs b/server-rs/crates/api-server/src/tripo3d/job.rs index f26b9a830..0c63406b1 100644 --- a/server-rs/crates/api-server/src/tripo3d/job.rs +++ b/server-rs/crates/api-server/src/tripo3d/job.rs @@ -78,11 +78,13 @@ impl Model3dJobRequest { } } - pub(crate) fn model_version(&self) -> &'static str { - match self { - Self::TextToModel(request) => request.generation.model.as_str(), - Self::ImageToModel(request) => request.generation.model.as_str(), - } + /// 审计与落库用的模型版本线上取值;取值只由契约枚举的 serde rename 定义。 + pub(crate) fn model_version(&self) -> String { + let model = match self { + Self::TextToModel(request) => request.generation.model, + Self::ImageToModel(request) => request.generation.model, + }; + shared_contracts::model3d::wire_str_lossy(&model) } } diff --git a/server-rs/crates/api-server/src/tripo3d/pricing.rs b/server-rs/crates/api-server/src/tripo3d/pricing.rs index 6b1989bf2..e40a1aa93 100644 --- a/server-rs/crates/api-server/src/tripo3d/pricing.rs +++ b/server-rs/crates/api-server/src/tripo3d/pricing.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use shared_contracts::model3d::common::{ Model3dGeometryQuality, Model3dModelVersion, Model3dTextureQuality, }; +use shared_contracts::model3d::wire_str_lossy; /// 生成端点。批量价键与请求入口一一对应,不做 provider 侧的变形。 #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] @@ -158,7 +159,7 @@ impl std::fmt::Display for Model3dPricingError { f, "Tripo 3D 定价缺少底价:endpoint={} model={}", endpoint.as_str(), - model_version.as_str() + wire_str_lossy(model_version) ), Self::MissingAddOnPrice(add_on) => { write!(f, "Tripo 3D 定价缺少 add-on:{add_on:?}") @@ -182,7 +183,7 @@ impl Model3dPricingConfig { return Err(format!( "缺少 endpoint {} 模型 {} 的底价", endpoint.as_str(), - model_version.as_str() + wire_str_lossy(&model_version) )); } } @@ -191,7 +192,7 @@ impl Model3dPricingConfig { return Err(format!( "endpoint {} 出现了契约不支持的模型版本 {}", endpoint.as_str(), - model_version.as_str() + wire_str_lossy(model_version) )); } } diff --git a/server-rs/crates/platform-tripo/Cargo.toml b/server-rs/crates/platform-tripo/Cargo.toml index 4196df708..086a9fec8 100644 --- a/server-rs/crates/platform-tripo/Cargo.toml +++ b/server-rs/crates/platform-tripo/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true [dependencies] shared-contracts = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } tripo3d-sdk = { workspace = true } bytes = { workspace = true } diff --git a/server-rs/crates/platform-tripo/src/common/extra.rs b/server-rs/crates/platform-tripo/src/common/extra.rs index 14ef2d2cd..569f98007 100644 --- a/server-rs/crates/platform-tripo/src/common/extra.rs +++ b/server-rs/crates/platform-tripo/src/common/extra.rs @@ -9,23 +9,23 @@ use std::collections::HashMap; use serde_json::Value; use shared_contracts::model3d::common::Model3dTextureVersion; +use super::{TripoError, wire}; + const EXTRA_TEXTURE_VERSION: &str = "texture_version"; const EXTRA_DELIGHT: &str = "delight"; -/// 按 provider 需要的键名构造 `extra`;未提供的参数不写入,由 provider 取默认值。 +/// 按 provider 需要的键名构造 `extra`;未提供的参数不写入,由 provider 取默认值; +/// `texture_version` 的取值仍由契约枚举的 serde rename 定义。 pub(crate) fn extra_fields( texture_version: Option, delight: Option, -) -> HashMap { +) -> Result, TripoError> { let mut extra = HashMap::new(); if let Some(value) = texture_version { - extra.insert( - EXTRA_TEXTURE_VERSION.into(), - Value::String(value.as_str().into()), - ); + extra.insert(EXTRA_TEXTURE_VERSION.into(), Value::String(wire(&value)?)); } if let Some(value) = delight { extra.insert(EXTRA_DELIGHT.into(), Value::Bool(value)); } - extra + Ok(extra) } diff --git a/server-rs/crates/platform-tripo/src/common/mod.rs b/server-rs/crates/platform-tripo/src/common/mod.rs index 5529bb3da..0d3b8a4d7 100644 --- a/server-rs/crates/platform-tripo/src/common/mod.rs +++ b/server-rs/crates/platform-tripo/src/common/mod.rs @@ -5,6 +5,7 @@ mod extra; mod mapping; mod types; mod validation; +mod wire; pub use client::TripoProviderClient; pub use config::TripoSettings; @@ -18,3 +19,4 @@ pub use types::{ pub(crate) use validation::{ TripoGenerationOptions, validate_generation_options, validate_task_id, }; +pub(crate) use wire::{wire, wire_option}; diff --git a/server-rs/crates/platform-tripo/src/common/wire.rs b/server-rs/crates/platform-tripo/src/common/wire.rs new file mode 100644 index 000000000..64aa5b30f --- /dev/null +++ b/server-rs/crates/platform-tripo/src/common/wire.rs @@ -0,0 +1,25 @@ +//! 契约枚举 → provider 请求里要发的线上取值。 +//! +//! 取值只由 `shared-contracts` 里的 serde `rename` / `rename_all` 定义:provider 侧 +//! 不再维护第二份字面量,两处各写一份、改一处就静默分叉的问题不会再出现。 + +use serde::Serialize; + +use super::TripoError; + +/// 契约枚举 → provider 线上取值。 +/// +/// 契约枚举都是「序列化成字符串」的形态;真拿到非字符串说明契约被改坏,这里归一成 +/// provider 内部错误,而不是 panic 或静默兜底。 +pub(crate) fn wire(value: &T) -> Result { + shared_contracts::model3d::wire_str(value).map_err(|error| TripoError::Sdk { + message: format!("契约枚举未按字符串序列化:{error}"), + }) +} + +/// [`wire`] 的 `Option` 形态:缺省仍是缺省。 +pub(crate) fn wire_option( + value: Option<&T>, +) -> Result, TripoError> { + value.map(wire).transpose() +} diff --git a/server-rs/crates/platform-tripo/src/image_to_model/client.rs b/server-rs/crates/platform-tripo/src/image_to_model/client.rs index 0c17d84bb..5c7fbf923 100644 --- a/server-rs/crates/platform-tripo/src/image_to_model/client.rs +++ b/server-rs/crates/platform-tripo/src/image_to_model/client.rs @@ -1,13 +1,9 @@ -use shared_contracts::model3d::common::{ - Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dInputOrientation, - Model3dTextureAlignment, Model3dTextureQuality, -}; use shared_contracts::model3d::image_to_model::Model3dImageToModelParams; use tripo3d_sdk::{models::FileInput, params::ImageToModelParams}; use crate::common::{ TripoError, TripoField, TripoProviderClient, TripoTaskHandle, TripoValidationReason, - extra_fields, + extra_fields, wire, wire_option, }; use super::validation::validate_image_to_model_params; @@ -101,53 +97,38 @@ impl TripoProviderClient { let task_id = self .client - .image_to_model(to_sdk_params(input, params)) + .image_to_model(to_sdk_params(input, params)?) .await .map_err(TripoError::from)?; Ok(TripoTaskHandle { task_id }) } } -fn to_sdk_params(input: FileInput, params: &Model3dImageToModelParams) -> ImageToModelParams { - ImageToModelParams { +fn to_sdk_params( + input: FileInput, + params: &Model3dImageToModelParams, +) -> Result { + Ok(ImageToModelParams { input, - model: Some(params.model.as_str().to_owned()), + model: Some(wire(¶ms.model)?), enable_image_autofix: params.enable_image_autofix, model_seed: params.model_seed, texture_seed: params.texture_seed, texture: params.texture, pbr: params.pbr, - texture_quality: params - .texture_quality - .map(Model3dTextureQuality::as_str) - .map(str::to_owned), - extra: extra_fields(params.texture_version, params.delight), - texture_alignment: params - .texture_alignment - .map(Model3dTextureAlignment::as_str) - .map(str::to_owned), - geometry_quality: params - .geometry_quality - .map(Model3dGeometryQuality::as_str) - .map(str::to_owned), + texture_quality: wire_option(params.texture_quality.as_ref())?, + extra: extra_fields(params.texture_version, params.delight)?, + texture_alignment: wire_option(params.texture_alignment.as_ref())?, + geometry_quality: wire_option(params.geometry_quality.as_ref())?, face_limit: params.face_limit, auto_size: params.auto_size, - orientation: params - .orientation - .map(Model3dInputOrientation::as_str) - .map(str::to_owned), + orientation: wire_option(params.orientation.as_ref())?, quad: params.quad, smart_low_poly: params.smart_low_poly, generate_parts: params.generate_parts, - compress: params - .compress - .map(Model3dCompression::as_str) - .map(str::to_owned), + compress: wire_option(params.compress.as_ref())?, export_uv: params.export_uv, - export_orientation: params - .export_orientation - .map(Model3dExportOrientation::as_str) - .map(str::to_owned), + export_orientation: wire_option(params.export_orientation.as_ref())?, style: None, - } + }) } diff --git a/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs b/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs index 7de1e63bf..3d526f244 100644 --- a/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs +++ b/server-rs/crates/platform-tripo/src/multiview_to_model/client.rs @@ -1,7 +1,3 @@ -use shared_contracts::model3d::common::{ - Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dInputOrientation, - Model3dTextureAlignment, Model3dTextureQuality, -}; use shared_contracts::model3d::multiview_to_model::{ Model3dMultiviewInputs, Model3dMultiviewToModelRequest, }; @@ -9,7 +5,8 @@ use tripo3d_sdk::{models::FileInput, params::MultiviewToModelParams}; use crate::common::{ TripoError, TripoField, TripoGenerationOptions, TripoProviderClient, TripoTaskHandle, - TripoValidationReason, extra_fields, validate_generation_options, validate_task_id, + TripoValidationReason, extra_fields, validate_generation_options, validate_task_id, wire, + wire_option, }; impl TripoProviderClient { @@ -66,7 +63,7 @@ impl TripoProviderClient { })?; let task_id = self .client - .multiview_to_model(to_sdk_params(request)) + .multiview_to_model(to_sdk_params(request)?) .await .map_err(TripoError::from)?; Ok(TripoTaskHandle { task_id }) @@ -80,7 +77,9 @@ fn optional_view(value: &Option) -> Option { .map(|value| FileInput::from(value.trim())) } -fn to_sdk_params(request: &Model3dMultiviewToModelRequest) -> MultiviewToModelParams { +fn to_sdk_params( + request: &Model3dMultiviewToModelRequest, +) -> Result { let mut params = match &request.inputs { Model3dMultiviewInputs::Views { front, @@ -97,41 +96,23 @@ fn to_sdk_params(request: &Model3dMultiviewToModelRequest) -> MultiviewToModelPa MultiviewToModelParams::from_task_id(task_id.trim().to_owned()) } }; - params.model = Some(request.model.as_str().to_owned()); + params.model = Some(wire(&request.model)?); params.model_seed = request.model_seed; params.texture_seed = request.texture_seed; params.texture = request.texture; params.pbr = request.pbr; - params.texture_quality = request - .texture_quality - .map(Model3dTextureQuality::as_str) - .map(str::to_owned); - params.extra = extra_fields(request.texture_version, request.delight); - params.geometry_quality = request - .geometry_quality - .map(Model3dGeometryQuality::as_str) - .map(str::to_owned); - params.texture_alignment = request - .texture_alignment - .map(Model3dTextureAlignment::as_str) - .map(str::to_owned); + params.texture_quality = wire_option(request.texture_quality.as_ref())?; + params.extra = extra_fields(request.texture_version, request.delight)?; + params.geometry_quality = wire_option(request.geometry_quality.as_ref())?; + params.texture_alignment = wire_option(request.texture_alignment.as_ref())?; params.face_limit = request.face_limit; params.auto_size = request.auto_size; - params.orientation = request - .orientation - .map(Model3dInputOrientation::as_str) - .map(str::to_owned); + params.orientation = wire_option(request.orientation.as_ref())?; params.quad = request.quad; params.smart_low_poly = request.smart_low_poly; params.generate_parts = request.generate_parts; - params.compress = request - .compress - .map(Model3dCompression::as_str) - .map(str::to_owned); + params.compress = wire_option(request.compress.as_ref())?; params.export_uv = request.export_uv; - params.export_orientation = request - .export_orientation - .map(Model3dExportOrientation::as_str) - .map(str::to_owned); - params + params.export_orientation = wire_option(request.export_orientation.as_ref())?; + Ok(params) } diff --git a/server-rs/crates/platform-tripo/src/text_to_model/client.rs b/server-rs/crates/platform-tripo/src/text_to_model/client.rs index fa11c5755..11b09f7c9 100644 --- a/server-rs/crates/platform-tripo/src/text_to_model/client.rs +++ b/server-rs/crates/platform-tripo/src/text_to_model/client.rs @@ -1,6 +1,5 @@ -use crate::common::{TripoError, TripoProviderClient, TripoTaskHandle, extra_fields}; -use shared_contracts::model3d::common::{ - Model3dCompression, Model3dExportOrientation, Model3dGeometryQuality, Model3dTextureQuality, +use crate::common::{ + TripoError, TripoProviderClient, TripoTaskHandle, extra_fields, wire, wire_option, }; use shared_contracts::model3d::text_to_model::Model3dTextToModelParams; @@ -14,46 +13,36 @@ impl TripoProviderClient { validate_text_to_model_params(params)?; let task_id = self .client - .text_to_model(to_sdk_params(params)) + .text_to_model(to_sdk_params(params)?) .await .map_err(TripoError::from)?; Ok(TripoTaskHandle { task_id }) } } -fn to_sdk_params(params: &Model3dTextToModelParams) -> tripo3d_sdk::params::TextToModelParams { - tripo3d_sdk::params::TextToModelParams { +fn to_sdk_params( + params: &Model3dTextToModelParams, +) -> Result { + Ok(tripo3d_sdk::params::TextToModelParams { prompt: params.prompt.clone(), - model: Some(params.model.as_str().to_owned()), + model: Some(wire(¶ms.model)?), negative_prompt: params.negative_prompt.clone(), image_seed: params.image_seed, model_seed: params.model_seed, texture_seed: params.texture_seed, texture: params.texture, pbr: params.pbr, - texture_quality: params - .texture_quality - .map(Model3dTextureQuality::as_str) - .map(str::to_owned), - extra: extra_fields(params.texture_version, params.delight), - geometry_quality: params - .geometry_quality - .map(Model3dGeometryQuality::as_str) - .map(str::to_owned), + texture_quality: wire_option(params.texture_quality.as_ref())?, + extra: extra_fields(params.texture_version, params.delight)?, + geometry_quality: wire_option(params.geometry_quality.as_ref())?, face_limit: params.face_limit, auto_size: params.auto_size, quad: params.quad, smart_low_poly: params.smart_low_poly, generate_parts: params.generate_parts, - compress: params - .compress - .map(Model3dCompression::as_str) - .map(str::to_owned), + compress: wire_option(params.compress.as_ref())?, export_uv: params.export_uv, - export_orientation: params - .export_orientation - .map(Model3dExportOrientation::as_str) - .map(str::to_owned), + export_orientation: wire_option(params.export_orientation.as_ref())?, style: None, - } + }) } diff --git a/server-rs/crates/shared-contracts/src/model3d/common/compression.rs b/server-rs/crates/shared-contracts/src/model3d/common/compression.rs index 2fda58d5f..f32d2e1f6 100644 --- a/server-rs/crates/shared-contracts/src/model3d/common/compression.rs +++ b/server-rs/crates/shared-contracts/src/model3d/common/compression.rs @@ -8,11 +8,3 @@ pub enum Model3dCompression { #[serde(rename = "geometry")] Geometry, } - -impl Model3dCompression { - pub const fn as_str(self) -> &'static str { - match self { - Self::Geometry => "geometry", - } - } -} diff --git a/server-rs/crates/shared-contracts/src/model3d/common/export_orientation.rs b/server-rs/crates/shared-contracts/src/model3d/common/export_orientation.rs index 1657ef898..7fa8109ca 100644 --- a/server-rs/crates/shared-contracts/src/model3d/common/export_orientation.rs +++ b/server-rs/crates/shared-contracts/src/model3d/common/export_orientation.rs @@ -14,14 +14,3 @@ pub enum Model3dExportOrientation { #[serde(rename = "-y")] MinusY, } - -impl Model3dExportOrientation { - pub const fn as_str(self) -> &'static str { - match self { - Self::PlusX => "+x", - Self::MinusX => "-x", - Self::PlusY => "+y", - Self::MinusY => "-y", - } - } -} diff --git a/server-rs/crates/shared-contracts/src/model3d/common/geometry_quality.rs b/server-rs/crates/shared-contracts/src/model3d/common/geometry_quality.rs index bf2cfda39..a99860a76 100644 --- a/server-rs/crates/shared-contracts/src/model3d/common/geometry_quality.rs +++ b/server-rs/crates/shared-contracts/src/model3d/common/geometry_quality.rs @@ -10,12 +10,3 @@ pub enum Model3dGeometryQuality { Standard, Detailed, } - -impl Model3dGeometryQuality { - pub const fn as_str(self) -> &'static str { - match self { - Self::Standard => "standard", - Self::Detailed => "detailed", - } - } -} diff --git a/server-rs/crates/shared-contracts/src/model3d/common/input_orientation.rs b/server-rs/crates/shared-contracts/src/model3d/common/input_orientation.rs index 96517ea8d..393be2331 100644 --- a/server-rs/crates/shared-contracts/src/model3d/common/input_orientation.rs +++ b/server-rs/crates/shared-contracts/src/model3d/common/input_orientation.rs @@ -10,12 +10,3 @@ pub enum Model3dInputOrientation { Default, AlignImage, } - -impl Model3dInputOrientation { - pub const fn as_str(self) -> &'static str { - match self { - Self::Default => "default", - Self::AlignImage => "align_image", - } - } -} diff --git a/server-rs/crates/shared-contracts/src/model3d/common/model_version.rs b/server-rs/crates/shared-contracts/src/model3d/common/model_version.rs index 4e905abdd..0dc1255df 100644 --- a/server-rs/crates/shared-contracts/src/model3d/common/model_version.rs +++ b/server-rs/crates/shared-contracts/src/model3d/common/model_version.rs @@ -20,14 +20,4 @@ pub enum Model3dModelVersion { impl Model3dModelVersion { /// 契约支持的全部模型版本;定价配置必须为每个版本给出两个端点的底价。 pub const ALL: [Self; 5] = [Self::H31, Self::H30, Self::H25, Self::P1, Self::P2]; - - pub const fn as_str(self) -> &'static str { - match self { - Self::H31 => "v3.1-20260211", - Self::H30 => "v3.0-20250812", - Self::H25 => "v2.5-20250123", - Self::P1 => "P1-20260311", - Self::P2 => "P2-20260801", - } - } } diff --git a/server-rs/crates/shared-contracts/src/model3d/common/texture_alignment.rs b/server-rs/crates/shared-contracts/src/model3d/common/texture_alignment.rs index 1c87c36b8..789d7f9d4 100644 --- a/server-rs/crates/shared-contracts/src/model3d/common/texture_alignment.rs +++ b/server-rs/crates/shared-contracts/src/model3d/common/texture_alignment.rs @@ -10,12 +10,3 @@ pub enum Model3dTextureAlignment { OriginalImage, Geometry, } - -impl Model3dTextureAlignment { - pub const fn as_str(self) -> &'static str { - match self { - Self::OriginalImage => "original_image", - Self::Geometry => "geometry", - } - } -} diff --git a/server-rs/crates/shared-contracts/src/model3d/common/texture_quality.rs b/server-rs/crates/shared-contracts/src/model3d/common/texture_quality.rs index b1f77ac97..bb95dab97 100644 --- a/server-rs/crates/shared-contracts/src/model3d/common/texture_quality.rs +++ b/server-rs/crates/shared-contracts/src/model3d/common/texture_quality.rs @@ -12,14 +12,3 @@ pub enum Model3dTextureQuality { Detailed, Extreme, } - -impl Model3dTextureQuality { - pub const fn as_str(self) -> &'static str { - match self { - Self::Fast => "fast", - Self::Standard => "standard", - Self::Detailed => "detailed", - Self::Extreme => "extreme", - } - } -} diff --git a/server-rs/crates/shared-contracts/src/model3d/common/texture_version.rs b/server-rs/crates/shared-contracts/src/model3d/common/texture_version.rs index 847b94c04..724d00295 100644 --- a/server-rs/crates/shared-contracts/src/model3d/common/texture_version.rs +++ b/server-rs/crates/shared-contracts/src/model3d/common/texture_version.rs @@ -12,13 +12,3 @@ pub enum Model3dTextureVersion { #[serde(rename = "v2.5-20250123")] V25, } - -impl Model3dTextureVersion { - pub const fn as_str(self) -> &'static str { - match self { - Self::V35 => "v3.5-20260815", - Self::V30 => "v3.0-20250812", - Self::V25 => "v2.5-20250123", - } - } -} diff --git a/server-rs/crates/shared-contracts/src/model3d/mod.rs b/server-rs/crates/shared-contracts/src/model3d/mod.rs index 6d6708bf1..657dd6a19 100644 --- a/server-rs/crates/shared-contracts/src/model3d/mod.rs +++ b/server-rs/crates/shared-contracts/src/model3d/mod.rs @@ -24,3 +24,32 @@ pub mod text_to_model; pub use generation_result::Model3dGenerationResult; pub const MODEL3D_CONTRACT_VERSION: &str = "model3d.v1"; + +/// 契约枚举的线上取值。 +/// +/// 取值只由枚举上的 serde `rename` / `rename_all` 定义:调用方需要字符串时走这里, +/// 不要在枚举旁边再手写一份 `as_str` —— 同一份取值出现两处,改一处就会静默分叉。 +/// 契约里的枚举都是「序列化成字符串」的形态;序列化失败或形态不符说明契约被改坏, +/// 因此按错误返回,而不是 panic 或静默兜底。 +pub fn wire_str(value: &T) -> Result +where + T: serde::Serialize + ?Sized, +{ + match serde_json::to_value(value)? { + serde_json::Value::String(text) => Ok(text), + other => Err(serde::ser::Error::custom(format!( + "model3d 契约枚举应序列化成字符串,实际为 {other}" + ))), + } +} + +/// 与 [`wire_str`] 相同,但失败时退回 `Debug` 名称。 +/// +/// 只用于日志与审计文案:这些位置必须给出字符串且不能失败,退回值明显不是线上取值, +/// 不会被误当成正常结果。 +pub fn wire_str_lossy(value: &T) -> String +where + T: serde::Serialize + std::fmt::Debug + ?Sized, +{ + wire_str(value).unwrap_or_else(|_| format!("{value:?}")) +} diff --git a/server-rs/crates/shared-contracts/tests/model3d_multiview_request_contract.rs b/server-rs/crates/shared-contracts/tests/model3d_multiview_request_contract.rs index c67bf35cc..de52020ac 100644 --- a/server-rs/crates/shared-contracts/tests/model3d_multiview_request_contract.rs +++ b/server-rs/crates/shared-contracts/tests/model3d_multiview_request_contract.rs @@ -69,7 +69,7 @@ fn multiview_inputs_reject_unknown_fields_inside_variants() { fn multiview_request_rejects_unknown_top_level_fields() { let error = serde_json::from_value::(json!({ "inputs": { "kind": "taskId", "taskId": "tripo-task-1" }, - "model": Model3dModelVersion::H31.as_str(), + "model": "v3.1-20260211", "unexpected": true })) .expect_err("请求体顶层出现未知字段应被拒绝");