use serde::{Deserialize, Serialize}; use std::collections::HashSet; pub const AGC_MODEL_CATALOG_CONFLICT: &str = "AGC_MODEL_CATALOG_CONFLICT"; #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AgcModel { pub id: String, pub alias: String, pub model_id: String, pub enabled: bool, } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct AgcModelCatalog { pub revision: u64, pub default_model_id: String, pub models: Vec, } impl Default for AgcModelCatalog { fn default() -> Self { Self { revision: 0, default_model_id: "quality".into(), models: vec![ AgcModel { id: "quality".into(), alias: "高质量".into(), model_id: "gpt-6-astra".into(), enabled: true, }, AgcModel { id: "fast".into(), alias: "快速".into(), model_id: "gpt-5.6-luna".into(), enabled: true, }, ], } } } impl AgcModelCatalog { pub fn validate(&self) -> Result<(), String> { if self.models.is_empty() || self.models.len() > 32 { return Err("模型列表必须包含 1 至 32 项".into()); } let mut ids = HashSet::new(); let mut aliases = HashSet::new(); for model in &self.models { if model.id.is_empty() || model.id == "platform-default" || model.id.len() > 64 || !model .id .bytes() .all(|c| c.is_ascii_alphanumeric() || c == b'-' || c == b'_') || !ids.insert(&model.id) { return Err("模型标识须唯一且只含字母、数字、连字符或下划线".into()); } if model.alias.trim().is_empty() || model.alias != model.alias.trim() || model.alias.chars().count() > 40 || model.alias.chars().any(char::is_control) || !aliases.insert(&model.alias) { return Err("模型别名须唯一,长度为 1 至 40 字符".into()); } if model.model_id.trim().is_empty() || model.model_id != model.model_id.trim() || model.model_id.len() > 200 || model.model_id.chars().any(char::is_control) { return Err("实际模型名不能为空,长度不能超过 200 字节".into()); } } self.resolve(&self.default_model_id).map(|_| ()) } pub fn resolve(&self, id: &str) -> Result<&str, String> { self.models .iter() .find(|m| m.enabled && m.id == id) .map(|m| m.model_id.as_str()) .ok_or_else(|| "所选模型不可用,请刷新模型列表".into()) } } #[cfg(test)] mod tests { use super::*; #[test] fn catalog_maps_only_enabled_ids() { let mut catalog = AgcModelCatalog::default(); assert!(catalog.validate().is_ok()); assert_eq!(catalog.resolve("quality").unwrap(), "gpt-6-astra"); assert!(catalog.resolve("gpt-6-astra").is_err()); assert!(catalog.resolve("unknown").is_err()); catalog.models[0].enabled = false; assert!(catalog.resolve("quality").is_err()); assert!(catalog.validate().is_err()); } #[test] fn catalog_rejects_duplicate_aliases_and_ids() { let mut catalog = AgcModelCatalog::default(); catalog.models[1].alias = catalog.models[0].alias.clone(); assert!(catalog.validate().is_err()); catalog.models[1].alias = "快速".into(); catalog.models[1].id = catalog.models[0].id.clone(); assert!(catalog.validate().is_err()); } }