fix: 修复优先级管理重新打开顺序回退

优先级管理弹窗依赖 grouped-by-format 接口回显格式优先级,但该接口此前读取 summary key 行。summary 查询会清空 global_priority_by_format 和 internal_priority 等路由字段,导致保存后的数据库顺序存在,重新打开页面却回退为前端占位顺序。

改为使用完整 key 查询,并增加 summary 字段被清空时仍能回显真实优先级的回归测试。

Fixes #505

Constraint: grouped-by-format 是优先级管理弹窗的数据源,必须返回真实 per-format priority 字段。
Rejected: 修改前端继续猜测顺序 | 无法区分真实数据库优先级与占位回退。
Confidence: high
Scope-risk: narrow
Tested: cargo fmt --check; git diff --check
Not-tested: cargo test on local Windows blocked by missing NASM for boring-sys2
This commit is contained in:
流云
2026-05-20 19:38:27 +08:00
parent d0981c2fd5
commit b47282fe4c
2 changed files with 111 additions and 3 deletions

View File

@@ -46,7 +46,7 @@ pub(crate) async fn build_admin_keys_grouped_by_format_payload(
let (endpoints_result, keys_result) = tokio::join!( let (endpoints_result, keys_result) = tokio::join!(
state.list_provider_catalog_endpoints_by_provider_ids(&provider_ids), state.list_provider_catalog_endpoints_by_provider_ids(&provider_ids),
state.list_provider_catalog_key_summaries_by_provider_ids(&provider_ids), state.list_provider_catalog_keys_by_provider_ids(&provider_ids),
); );
let active_endpoints = endpoints_result let active_endpoints = endpoints_result

View File

@@ -5,7 +5,12 @@ use aether_crypto::{
decrypt_python_fernet_ciphertext, encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY, decrypt_python_fernet_ciphertext, encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY,
}; };
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository; use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
use aether_data_contracts::repository::provider_catalog::ProviderCatalogReadRepository; use aether_data_contracts::repository::provider_catalog::{
ProviderCatalogKeyListQuery, ProviderCatalogReadRepository, StoredProviderCatalogEndpoint,
StoredProviderCatalogKey, StoredProviderCatalogKeyPage, StoredProviderCatalogKeyStats,
StoredProviderCatalogProvider,
};
use aether_data_contracts::DataLayerError;
use axum::body::Body; use axum::body::Body;
use axum::routing::any; use axum::routing::any;
use axum::{extract::Request, Json, Router}; use axum::{extract::Request, Json, Router};
@@ -22,6 +27,103 @@ use crate::constants::{
}; };
use crate::data::GatewayDataState; use crate::data::GatewayDataState;
struct SummaryNullingProviderCatalogReadRepository {
inner: InMemoryProviderCatalogReadRepository,
}
impl SummaryNullingProviderCatalogReadRepository {
fn seed(
providers: Vec<StoredProviderCatalogProvider>,
endpoints: Vec<StoredProviderCatalogEndpoint>,
keys: Vec<StoredProviderCatalogKey>,
) -> Self {
Self {
inner: InMemoryProviderCatalogReadRepository::seed(providers, endpoints, keys),
}
}
}
#[async_trait::async_trait]
impl ProviderCatalogReadRepository for SummaryNullingProviderCatalogReadRepository {
async fn list_providers(
&self,
active_only: bool,
) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
self.inner.list_providers(active_only).await
}
async fn list_providers_by_ids(
&self,
provider_ids: &[String],
) -> Result<Vec<StoredProviderCatalogProvider>, DataLayerError> {
self.inner.list_providers_by_ids(provider_ids).await
}
async fn list_endpoints_by_ids(
&self,
endpoint_ids: &[String],
) -> Result<Vec<StoredProviderCatalogEndpoint>, DataLayerError> {
self.inner.list_endpoints_by_ids(endpoint_ids).await
}
async fn list_endpoints_by_provider_ids(
&self,
provider_ids: &[String],
) -> Result<Vec<StoredProviderCatalogEndpoint>, DataLayerError> {
self.inner
.list_endpoints_by_provider_ids(provider_ids)
.await
}
async fn list_keys_by_ids(
&self,
key_ids: &[String],
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
self.inner.list_keys_by_ids(key_ids).await
}
async fn list_keys_by_provider_ids(
&self,
provider_ids: &[String],
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
self.inner.list_keys_by_provider_ids(provider_ids).await
}
async fn list_key_summaries_by_provider_ids(
&self,
provider_ids: &[String],
) -> Result<Vec<StoredProviderCatalogKey>, DataLayerError> {
let mut keys = self.inner.list_keys_by_provider_ids(provider_ids).await?;
for key in &mut keys {
key.internal_priority = 50;
key.global_priority_by_format = None;
key.rate_multipliers = None;
key.request_count = None;
key.success_count = None;
key.error_count = None;
key.total_response_time_ms = None;
key.circuit_breaker_by_format = None;
}
Ok(keys)
}
async fn list_keys_page(
&self,
query: &ProviderCatalogKeyListQuery,
) -> Result<StoredProviderCatalogKeyPage, DataLayerError> {
self.inner.list_keys_page(query).await
}
async fn list_key_stats_by_provider_ids(
&self,
provider_ids: &[String],
) -> Result<Vec<StoredProviderCatalogKeyStats>, DataLayerError> {
self.inner
.list_key_stats_by_provider_ids(provider_ids)
.await
}
}
#[tokio::test] #[tokio::test]
async fn gateway_handles_admin_provider_keys_locally_with_trusted_admin_principal() { async fn gateway_handles_admin_provider_keys_locally_with_trusted_admin_principal() {
let upstream_hits = Arc::new(Mutex::new(0usize)); let upstream_hits = Arc::new(Mutex::new(0usize));
@@ -2195,7 +2297,7 @@ async fn gateway_handles_admin_keys_grouped_by_format_locally_with_trusted_admin
key_b.created_at_unix_ms = Some(1_711_100_000); key_b.created_at_unix_ms = Some(1_711_100_000);
key_b.updated_at_unix_secs = Some(1_711_100_100); key_b.updated_at_unix_secs = Some(1_711_100_100);
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed( let provider_catalog_repository = Arc::new(SummaryNullingProviderCatalogReadRepository::seed(
vec![ vec![
sample_provider("provider-openai", "openai", 10), sample_provider("provider-openai", "openai", 10),
sample_provider("provider-claude", "claude", 20) sample_provider("provider-claude", "claude", 20)
@@ -2252,6 +2354,12 @@ async fn gateway_handles_admin_keys_grouped_by_format_locally_with_trusted_admin
"https://api.openai.example" "https://api.openai.example"
); );
assert_eq!(payload["openai:chat"][0]["capabilities"], json!(["1h缓存"])); assert_eq!(payload["openai:chat"][0]["capabilities"], json!(["1h缓存"]));
assert_eq!(payload["openai:chat"][0]["format_priority"], 3);
assert_eq!(
payload["openai:chat"][0]["global_priority_by_format"]["openai:chat"],
3
);
assert_eq!(payload["openai:chat"][0]["internal_priority"], 10);
assert_eq!(payload["claude:messages"][0]["provider_active"], false); assert_eq!(payload["claude:messages"][0]["provider_active"], false);
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0); assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);