refactor(rules): 规则引擎容错优化,无效规则条目跳过而非中止整个规则集

- header/body rules 的 _are_locally_supported 简化为仅检查是否为数组
- apply 逻辑中遇到格式错误/不支持的规则条目改为 continue 跳过,而非 return false
- 允许非字符串 header value,自动序列化为 JSON 字符串
- 宽松处理无效 regex flag,不再拒绝整条规则

fix(gateway): Claude CLI 路由仅检查 bearer 头,不排斥同时携带 x-api-key 的请求

feat(observability): 监控链路候选展示解密 auth_config 的账号标签和 OAuth 计划类型
This commit is contained in:
fawney19
2026-04-25 19:46:52 +08:00
parent 00744c0ce5
commit 912a92cd1a
7 changed files with 438 additions and 184 deletions
@@ -6,19 +6,26 @@ use aether_admin::observability::monitoring::{
admin_monitoring_bad_request_response, admin_monitoring_trace_not_found_response,
admin_monitoring_trace_provider_id_from_path, admin_monitoring_trace_request_id_from_path,
build_admin_monitoring_trace_provider_stats_payload_response,
build_admin_monitoring_trace_request_payload_response, parse_admin_monitoring_attempted_only,
build_admin_monitoring_trace_request_payload_response_with_key_accounts,
parse_admin_monitoring_attempted_only, AdminMonitoringKeyAccountDisplay,
};
use aether_data_contracts::repository::{
candidates::{DecisionTrace, RequestCandidateStatus},
provider_catalog::StoredProviderCatalogKey,
};
use aether_data_contracts::repository::candidates::{DecisionTrace, RequestCandidateStatus};
use axum::{
body::Body,
response::{IntoResponse, Response},
};
use serde_json::{Map, Value};
use std::collections::BTreeMap;
use tracing::debug;
pub(super) async fn build_admin_monitoring_trace_request_response(
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
) -> Result<Response<Body>, GatewayError> {
let admin_state = state;
let state = state.as_ref();
let Some(request_id) =
admin_monitoring_trace_request_id_from_path(&request_context.request_path)
@@ -56,11 +63,105 @@ pub(super) async fn build_admin_monitoring_trace_request_response(
.read_request_usage_audit(&request_id)
.await
.map_err(|err| GatewayError::Internal(err.to_string()))?;
let key_accounts = build_admin_monitoring_key_account_display_map(admin_state, &trace).await?;
Ok(build_admin_monitoring_trace_request_payload_response(
&trace,
usage.as_ref(),
))
Ok(
build_admin_monitoring_trace_request_payload_response_with_key_accounts(
&trace,
usage.as_ref(),
&key_accounts,
),
)
}
async fn build_admin_monitoring_key_account_display_map(
state: &AdminAppState<'_>,
trace: &DecisionTrace,
) -> Result<BTreeMap<String, AdminMonitoringKeyAccountDisplay>, GatewayError> {
let key_ids = trace
.candidates
.iter()
.filter_map(|item| item.candidate.key_id.as_deref())
.filter(|value| !value.trim().is_empty())
.map(ToOwned::to_owned)
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
if key_ids.is_empty() {
return Ok(BTreeMap::new());
}
let keys = state.read_provider_catalog_keys_by_ids(&key_ids).await?;
Ok(keys
.into_iter()
.filter_map(|key| {
let display = resolve_admin_monitoring_key_account_display(state, &key)?;
Some((key.id, display))
})
.collect())
}
fn resolve_admin_monitoring_key_account_display(
state: &AdminAppState<'_>,
key: &StoredProviderCatalogKey,
) -> Option<AdminMonitoringKeyAccountDisplay> {
let auth_config = parse_admin_monitoring_key_auth_config(state, key);
let label = auth_config
.as_ref()
.and_then(|config| {
first_non_empty_json_string([
config.get("email"),
config.get("account_name"),
config.get("accountName"),
config.get("client_email"),
config.get("account_id"),
config.get("accountId"),
])
})
.or_else(|| {
key.upstream_metadata.as_ref().and_then(|metadata| {
first_non_empty_json_string([
metadata.get("email"),
metadata.get("account_name"),
metadata.get("accountName"),
metadata.get("account_id"),
metadata.get("accountId"),
])
})
});
let oauth_plan_type = auth_config.as_ref().and_then(|config| {
first_non_empty_json_string([config.get("plan_type"), config.get("planType")])
});
if label.is_none() && oauth_plan_type.is_none() {
return None;
}
Some(AdminMonitoringKeyAccountDisplay {
label,
oauth_plan_type,
})
}
fn parse_admin_monitoring_key_auth_config(
state: &AdminAppState<'_>,
key: &StoredProviderCatalogKey,
) -> Option<Map<String, Value>> {
let ciphertext = key.encrypted_auth_config.as_deref()?;
let plaintext = state.decrypt_catalog_secret_with_fallbacks(ciphertext)?;
serde_json::from_str::<Value>(&plaintext)
.ok()?
.as_object()
.cloned()
}
fn first_non_empty_json_string<'a>(
values: impl IntoIterator<Item = Option<&'a Value>>,
) -> Option<String> {
values.into_iter().find_map(|value| {
let text = value?.as_str()?.trim();
(!text.is_empty()).then(|| text.to_string())
})
}
pub(super) async fn build_admin_monitoring_trace_provider_stats_response(