feat(admin): 完善代理节点与 OAuth 授权管理

This commit is contained in:
fawney19
2026-04-14 14:09:24 +08:00
parent 593640ac19
commit 861ae81ff0
44 changed files with 2757 additions and 365 deletions
@@ -601,7 +601,14 @@ impl<'a> AdminAppState<'a> {
client_api_format: "provider_oauth:exchange".to_string(),
provider_api_format: "provider_oauth:exchange".to_string(),
model_name: Some("oauth-exchange".to_string()),
proxy: self.resolve_admin_proxy_node_snapshot(proxy_node_id).await,
proxy: if proxy_node_id
.map(str::trim)
.is_some_and(|value| !value.is_empty())
{
self.resolve_admin_proxy_node_snapshot(proxy_node_id).await
} else {
self.app.resolve_system_proxy_snapshot().await
},
tls_profile: None,
timeouts: Some(ExecutionTimeouts {
connect_ms: Some(ADMIN_PROVIDER_OAUTH_TIMEOUT_MS),
@@ -1,14 +1,8 @@
use super::*;
use aether_contracts::ProxySnapshot;
use aether_data::repository::proxy_nodes::{proxy_node_accepts_new_tunnels, StoredProxyNode};
use aether_provider_transport::TransportTunnelAffinityLookup;
use serde_json::{json, Map, Value};
use serde_json::{Map, Value};
use url::Url;
const TUNNEL_BASE_URL_EXTRA_KEY: &str = "tunnel_base_url";
const TUNNEL_OWNER_INSTANCE_ID_EXTRA_KEY: &str = "tunnel_owner_instance_id";
const TUNNEL_OWNER_OBSERVED_AT_EXTRA_KEY: &str = "tunnel_owner_observed_at_unix_secs";
impl<'a> AdminAppState<'a> {
pub(crate) async fn read_provider_transport_snapshot(
&self,
@@ -83,10 +77,9 @@ impl<'a> AdminAppState<'a> {
&self,
transport: &AdminGatewayProviderTransportSnapshot,
) -> Option<aether_contracts::ProxySnapshot> {
crate::provider_transport::resolve_transport_proxy_snapshot_with_tunnel_affinity(
self.app, transport,
)
.await
self.app
.resolve_transport_proxy_snapshot_with_tunnel_affinity(transport)
.await
}
pub(crate) fn fixed_provider_template(
@@ -123,17 +116,7 @@ impl<'a> AdminAppState<'a> {
}
if explicit_node_id.is_none() {
let system_node_id = self
.read_system_config_json_value("system_proxy_node_id")
.await
.ok()
.flatten()
.and_then(|value| value.as_str().map(str::trim).map(ToOwned::to_owned))
.filter(|value| !value.is_empty());
if let Some(snapshot) = self
.resolve_admin_proxy_node_snapshot(system_node_id.as_deref())
.await
{
if let Some(snapshot) = self.app.resolve_system_proxy_snapshot().await {
return Some(snapshot);
}
}
@@ -147,60 +130,7 @@ impl<'a> AdminAppState<'a> {
&self,
node_id: Option<&str>,
) -> Option<ProxySnapshot> {
let node_id = node_id.map(str::trim).filter(|value| !value.is_empty())?;
let node = self.find_proxy_node(node_id).await.ok().flatten()?;
if node.status.trim() != "online" {
return None;
}
if !proxy_node_accepts_new_tunnels(&node) {
return None;
}
if node.tunnel_mode && node.tunnel_connected {
let mut extra = Map::new();
if let Ok(Some(owner)) = self.app().lookup_tunnel_attachment_owner(node_id).await {
extra.insert(
TUNNEL_BASE_URL_EXTRA_KEY.to_string(),
Value::String(owner.relay_base_url),
);
extra.insert(
TUNNEL_OWNER_INSTANCE_ID_EXTRA_KEY.to_string(),
Value::String(owner.gateway_instance_id),
);
extra.insert(
TUNNEL_OWNER_OBSERVED_AT_EXTRA_KEY.to_string(),
json!(owner.observed_at_unix_secs),
);
}
return Some(ProxySnapshot {
enabled: Some(true),
mode: Some("tunnel".to_string()),
node_id: Some(node_id.to_string()),
label: Some(node.name),
url: None,
extra: if extra.is_empty() {
None
} else {
Some(Value::Object(extra))
},
});
}
if !node.is_manual {
return None;
}
let proxy_url = node
.proxy_url
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())?;
Some(ProxySnapshot {
enabled: Some(true),
mode: admin_provider_transport_proxy_mode(Some(proxy_url)),
node_id: Some(node.id.clone()),
label: Some(node.name.clone()),
url: admin_provider_transport_proxy_url_with_node_auth(&node)
.or_else(|| Some(proxy_url.to_string())),
extra: None,
})
self.app.resolve_proxy_node_snapshot(node_id).await
}
pub(crate) fn supports_local_gemini_transport_with_network(
@@ -387,25 +317,6 @@ fn admin_provider_transport_legacy_proxy_snapshot(value: &Value) -> Option<Proxy
}
}
fn admin_provider_transport_proxy_url_with_node_auth(node: &StoredProxyNode) -> Option<String> {
let proxy_url = node
.proxy_url
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())?;
let username = node
.proxy_username
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
let password = node
.proxy_password
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty());
admin_provider_transport_inject_proxy_auth(proxy_url, username, password)
}
fn admin_provider_transport_inject_proxy_auth(
proxy_url: &str,
username: Option<&str>,
@@ -2113,10 +2113,15 @@ impl<'a> AdminAppState<'a> {
aether_data::repository::auth::UpdateStandaloneApiKeyBasicRecord {
api_key_id: existing_key.api_key_id.clone(),
name: name.clone(),
rate_limit_present: true,
rate_limit: Some(rate_limit),
allowed_providers: Some(allowed_providers.clone()),
allowed_api_formats: Some(allowed_api_formats.clone()),
allowed_models: Some(allowed_models.clone()),
expires_at_present: false,
expires_at_unix_secs: None,
auto_delete_on_expiry_present: false,
auto_delete_on_expiry: false,
},
)
.await?;
@@ -2165,7 +2170,7 @@ impl<'a> AdminAppState<'a> {
allowed_providers,
allowed_api_formats,
allowed_models,
rate_limit,
rate_limit: Some(rate_limit),
concurrent_limit,
force_capabilities,
is_active,
@@ -11,6 +11,20 @@ use aether_admin::system::{
use axum::{body::Body, response::Response};
impl<'a> AdminAppState<'a> {
pub(crate) async fn create_manual_proxy_node(
&self,
mutation: &aether_data::repository::proxy_nodes::ProxyNodeManualCreateMutation,
) -> Result<Option<aether_data::repository::proxy_nodes::StoredProxyNode>, GatewayError> {
self.app.create_manual_proxy_node(mutation).await
}
pub(crate) async fn update_manual_proxy_node(
&self,
mutation: &aether_data::repository::proxy_nodes::ProxyNodeManualUpdateMutation,
) -> Result<Option<aether_data::repository::proxy_nodes::StoredProxyNode>, GatewayError> {
self.app.update_manual_proxy_node(mutation).await
}
pub(crate) async fn register_proxy_node(
&self,
mutation: &aether_data::repository::proxy_nodes::ProxyNodeRegistrationMutation,
@@ -94,6 +108,13 @@ impl<'a> AdminAppState<'a> {
self.app.unregister_proxy_node(node_id).await
}
pub(crate) async fn delete_proxy_node(
&self,
node_id: &str,
) -> Result<Option<aether_data::repository::proxy_nodes::StoredProxyNode>, GatewayError> {
self.app.delete_proxy_node(node_id).await
}
pub(crate) async fn update_proxy_node_remote_config(
&self,
mutation: &aether_data::repository::proxy_nodes::ProxyNodeRemoteConfigMutation,