4f3f0f24ff
新增后台 AGC 模型目录、别名、启停和默认项管理 客户端设置页恢复原状,对话框右下角按别名选择模型 服务端按稳定模型标识映射并校验实际模型白名单 修复 AGC 配套后端端口漂移、启动等待和 SpacetimeDB 版本检查 补充迁移、文档、启动与模型选择测试
119 lines
3.9 KiB
Rust
119 lines
3.9 KiB
Rust
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<AgcModel>,
|
|
}
|
|
|
|
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());
|
|
}
|
|
}
|