refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案

将 gateway 内部的 model-fetch、provider-transport、scheduler-core、
usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway
内部模块结构(state/router/cache/data/query 等);移除大量遗留模块
文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关
API 和组件。
This commit is contained in:
fawney19
2026-04-05 20:23:16 +08:00
parent cbc811f6ce
commit 763ff03a7b
777 changed files with 42659 additions and 21469 deletions

View File

@@ -5,7 +5,7 @@ use axum::http::Uri;
use sha2::{Digest, Sha256};
use url::form_urlencoded;
use crate::gateway::headers::{header_value_str, is_json_request};
use crate::headers::{header_value_str, is_json_request};
use super::super::GatewayControlDecision;
use super::types::{
@@ -68,7 +68,7 @@ pub(super) fn extract_request_credentials(
}
fn has_trusted_gateway_marker(headers: &http::HeaderMap) -> bool {
header_value_str(headers, crate::gateway::constants::GATEWAY_HEADER)
header_value_str(headers, crate::constants::GATEWAY_HEADER)
.unwrap_or_default()
.trim()
.to_ascii_lowercase()
@@ -128,23 +128,23 @@ fn extract_trusted_auth_headers(headers: &http::HeaderMap) -> Option<GatewayTrus
}
let user_id = header_value_str(
headers,
crate::gateway::constants::TRUSTED_AUTH_USER_ID_HEADER,
crate::constants::TRUSTED_AUTH_USER_ID_HEADER,
)
.filter(|value| !value.is_empty())?;
let api_key_id = header_value_str(
headers,
crate::gateway::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
crate::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
)
.filter(|value| !value.is_empty())?;
let balance_remaining = header_value_str(
headers,
crate::gateway::constants::TRUSTED_AUTH_BALANCE_HEADER,
crate::constants::TRUSTED_AUTH_BALANCE_HEADER,
)
.as_deref()
.and_then(parse_f64_header);
let access_allowed = header_value_str(
headers,
crate::gateway::constants::TRUSTED_AUTH_ACCESS_ALLOWED_HEADER,
crate::constants::TRUSTED_AUTH_ACCESS_ALLOWED_HEADER,
)
.as_deref()
.and_then(parse_bool_header);
@@ -165,7 +165,7 @@ pub(super) fn extract_trusted_admin_headers(
}
let user_id = header_value_str(
headers,
crate::gateway::constants::TRUSTED_ADMIN_USER_ID_HEADER,
crate::constants::TRUSTED_ADMIN_USER_ID_HEADER,
)?
.trim()
.to_string();
@@ -174,7 +174,7 @@ pub(super) fn extract_trusted_admin_headers(
}
let user_role = header_value_str(
headers,
crate::gateway::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
crate::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
)?
.trim()
.to_string();
@@ -183,13 +183,13 @@ pub(super) fn extract_trusted_admin_headers(
}
let session_id = header_value_str(
headers,
crate::gateway::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
crate::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
)
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
let management_token_id = header_value_str(
headers,
crate::gateway::constants::TRUSTED_ADMIN_MANAGEMENT_TOKEN_ID_HEADER,
crate::constants::TRUSTED_ADMIN_MANAGEMENT_TOKEN_ID_HEADER,
)
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty());
@@ -519,23 +519,23 @@ mod tests {
fn extracts_trusted_auth_headers() {
let mut headers = http::HeaderMap::new();
headers.insert(
crate::gateway::constants::GATEWAY_HEADER,
crate::constants::GATEWAY_HEADER,
"rust-phase3b".parse().unwrap(),
);
headers.insert(
crate::gateway::constants::TRUSTED_AUTH_USER_ID_HEADER,
crate::constants::TRUSTED_AUTH_USER_ID_HEADER,
"user-1".parse().unwrap(),
);
headers.insert(
crate::gateway::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
crate::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
"key-1".parse().unwrap(),
);
headers.insert(
crate::gateway::constants::TRUSTED_AUTH_BALANCE_HEADER,
crate::constants::TRUSTED_AUTH_BALANCE_HEADER,
"1.5".parse().unwrap(),
);
headers.insert(
crate::gateway::constants::TRUSTED_AUTH_ACCESS_ALLOWED_HEADER,
crate::constants::TRUSTED_AUTH_ACCESS_ALLOWED_HEADER,
"true".parse().unwrap(),
);
@@ -557,11 +557,11 @@ mod tests {
fn ignores_trusted_auth_headers_without_gateway_marker() {
let mut headers = http::HeaderMap::new();
headers.insert(
crate::gateway::constants::TRUSTED_AUTH_USER_ID_HEADER,
crate::constants::TRUSTED_AUTH_USER_ID_HEADER,
"user-1".parse().unwrap(),
);
headers.insert(
crate::gateway::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
crate::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
"key-1".parse().unwrap(),
);
@@ -574,19 +574,19 @@ mod tests {
fn extracts_trusted_admin_headers() {
let mut headers = http::HeaderMap::new();
headers.insert(
crate::gateway::constants::GATEWAY_HEADER,
crate::constants::GATEWAY_HEADER,
"rust-phase3b".parse().unwrap(),
);
headers.insert(
crate::gateway::constants::TRUSTED_ADMIN_USER_ID_HEADER,
crate::constants::TRUSTED_ADMIN_USER_ID_HEADER,
"admin-user-1".parse().unwrap(),
);
headers.insert(
crate::gateway::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
crate::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
"admin".parse().unwrap(),
);
headers.insert(
crate::gateway::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
crate::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
"sess-1".parse().unwrap(),
);
@@ -610,15 +610,15 @@ mod tests {
fn ignores_trusted_admin_headers_without_gateway_marker() {
let mut headers = http::HeaderMap::new();
headers.insert(
crate::gateway::constants::TRUSTED_ADMIN_USER_ID_HEADER,
crate::constants::TRUSTED_ADMIN_USER_ID_HEADER,
"admin-user-1".parse().unwrap(),
);
headers.insert(
crate::gateway::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
crate::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
"admin".parse().unwrap(),
);
headers.insert(
crate::gateway::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
crate::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
"sess-1".parse().unwrap(),
);

View File

@@ -39,7 +39,7 @@ pub(crate) fn should_buffer_request_for_local_auth(
};
decision.route_class.as_deref() == Some("ai_public")
&& decision.route_kind.as_deref() != Some("files")
&& crate::gateway::headers::is_json_request(headers)
&& crate::headers::is_json_request(headers)
}
pub(crate) fn request_model_local_rejection(

View File

@@ -35,7 +35,7 @@ pub(super) fn derive_principal_candidate(
#[cfg(test)]
mod tests {
use super::{derive_principal_candidate, GatewayPrincipalCandidate};
use crate::gateway::control::auth::types::{
use crate::control::auth::types::{
GatewayCredentialBundle, GatewayCredentialCarrier, GatewayExtractedCredentials,
GatewayPrimaryCredential, GatewayTrustedAuthHeaders,
};

View File

@@ -5,8 +5,12 @@ use base64::Engine as _;
use hmac::Mac;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tracing::{debug, info};
use crate::gateway::{AppState, GatewayError};
use crate::wallet_runtime::{
local_rejection_from_wallet_access, resolve_wallet_auth_gate,
};
use crate::{AppState, GatewayError};
use super::super::GatewayControlDecision;
use super::credentials::{
@@ -16,8 +20,7 @@ use super::credentials::{
use super::gate::GatewayLocalAuthRejection;
use super::principal::derive_principal_candidate;
use super::types::{GatewayPrincipalCandidate, GatewayTrustedAuthHeaders};
use crate::gateway::headers::header_value_str;
use crate::gateway::{local_rejection_from_wallet_access, resolve_wallet_auth_gate};
use crate::headers::header_value_str;
const AUTH_CONTEXT_CACHE_TTL: Duration = Duration::from_secs(60);
const AUTH_CONTEXT_CACHE_MAX_ENTRIES: usize = 256;
@@ -56,11 +59,13 @@ pub(in super::super) async fn resolve_control_decision_auth(
state: &AppState,
headers: &http::HeaderMap,
uri: &Uri,
trace_id: &str,
mut decision: GatewayControlDecision,
) -> Result<ControlDecisionAuthResolution, GatewayError> {
if let Some(admin_principal) =
resolve_trusted_admin_principal(headers, decision.auth_endpoint_signature.as_deref())
{
log_admin_principal_resolution(trace_id, &decision, "trusted_headers", &admin_principal);
decision.admin_principal = Some(admin_principal);
} else if let Some(admin_principal) = resolve_local_admin_principal(
state,
@@ -70,6 +75,7 @@ pub(in super::super) async fn resolve_control_decision_auth(
)
.await?
{
log_admin_principal_resolution(trace_id, &decision, "local_session", &admin_principal);
decision.admin_principal = Some(admin_principal);
}
@@ -81,6 +87,7 @@ pub(in super::super) async fn resolve_control_decision_auth(
)
.await?
{
log_auth_context_resolution(trace_id, &decision, &auth_context);
decision.local_auth_rejection = auth_context.local_rejection.clone();
if !auth_context.user_id.is_empty() && !auth_context.api_key_id.is_empty() {
if let Some(cache_key) = decision
@@ -95,6 +102,7 @@ pub(in super::super) async fn resolve_control_decision_auth(
}
if decision.local_auth_rejection.is_some() {
log_local_auth_rejection(trace_id, &decision);
return Ok(ControlDecisionAuthResolution::Resolved(decision));
}
@@ -113,6 +121,93 @@ pub(in super::super) async fn resolve_control_decision_auth(
Ok(ControlDecisionAuthResolution::Resolved(decision))
}
fn log_admin_principal_resolution(
trace_id: &str,
decision: &GatewayControlDecision,
resolution: &'static str,
admin_principal: &GatewayAdminPrincipalContext,
) {
debug!(
event_name = "admin_principal_resolved",
log_type = "debug",
debug_context = "control_auth",
trace_id = %trace_id,
route_class = decision.route_class.as_deref().unwrap_or("unknown"),
route_family = decision.route_family.as_deref().unwrap_or("unknown"),
route_kind = decision.route_kind.as_deref().unwrap_or("unknown"),
resolution,
admin_user_id = admin_principal.user_id.as_str(),
admin_user_role = admin_principal.user_role.as_str(),
admin_session_id = admin_principal.session_id.as_deref().unwrap_or("-"),
admin_management_token_id = admin_principal.management_token_id.as_deref().unwrap_or("-"),
"resolved admin principal for control decision"
);
}
fn log_auth_context_resolution(
trace_id: &str,
decision: &GatewayControlDecision,
auth_context: &GatewayControlAuthContext,
) {
info!(
event_name = "auth_context_resolved",
log_type = "event",
status = if auth_context.access_allowed {
"allowed"
} else {
"blocked"
},
trace_id = %trace_id,
route_class = decision.route_class.as_deref().unwrap_or("unknown"),
route_family = decision.route_family.as_deref().unwrap_or("unknown"),
route_kind = decision.route_kind.as_deref().unwrap_or("unknown"),
user_id = auth_context.user_id.as_str(),
api_key_id = auth_context.api_key_id.as_str(),
access_allowed = auth_context.access_allowed,
api_key_is_standalone = auth_context.api_key_is_standalone,
has_local_rejection = auth_context.local_rejection.is_some(),
"resolved data-backed auth context for control decision"
);
}
fn log_local_auth_rejection(trace_id: &str, decision: &GatewayControlDecision) {
let Some(rejection) = decision.local_auth_rejection.as_ref() else {
return;
};
let (rejection_kind, rejection_detail) = match rejection {
GatewayLocalAuthRejection::InvalidApiKey => ("invalid_api_key", "-".to_string()),
GatewayLocalAuthRejection::LockedApiKey => ("locked_api_key", "-".to_string()),
GatewayLocalAuthRejection::WalletUnavailable => ("wallet_unavailable", "-".to_string()),
GatewayLocalAuthRejection::BalanceDenied { remaining } => (
"balance_denied",
remaining
.map(|value| format!("remaining_usd={value:.4}"))
.unwrap_or_else(|| "remaining_usd=unknown".to_string()),
),
GatewayLocalAuthRejection::ProviderNotAllowed { provider } => {
("provider_not_allowed", provider.clone())
}
GatewayLocalAuthRejection::ApiFormatNotAllowed { api_format } => {
("api_format_not_allowed", api_format.clone())
}
GatewayLocalAuthRejection::ModelNotAllowed { model } => {
("model_not_allowed", model.clone())
}
};
info!(
event_name = "local_auth_rejected",
log_type = "event",
status = "rejected",
trace_id = %trace_id,
route_class = decision.route_class.as_deref().unwrap_or("unknown"),
route_family = decision.route_family.as_deref().unwrap_or("unknown"),
route_kind = decision.route_kind.as_deref().unwrap_or("unknown"),
rejection_kind,
rejection_detail = %rejection_detail,
"rejected local control request during auth gate resolution"
);
}
fn allows_missing_data_backed_auth_context(decision: &GatewayControlDecision) -> bool {
matches!(
decision.route_kind.as_deref(),
@@ -463,7 +558,7 @@ async fn resolve_trusted_auth_context(
}
fn build_data_backed_auth_context(
snapshot: crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
snapshot: crate::data::auth::GatewayAuthApiKeySnapshot,
auth_endpoint_signature: &str,
header_access_allowed: Option<bool>,
balance_remaining: Option<f64>,
@@ -551,8 +646,9 @@ mod tests {
use axum::http::{HeaderMap, Uri};
use super::resolve_data_backed_auth_context;
use crate::gateway::control::auth::credentials::hash_api_key;
use crate::gateway::{AppState, GatewayDataState};
use crate::control::auth::credentials::hash_api_key;
use crate::data::GatewayDataState;
use crate::AppState;
fn sample_snapshot(api_key_id: &str, user_id: &str) -> StoredAuthApiKeySnapshot {
StoredAuthApiKeySnapshot::new(

View File

@@ -1,8 +1,9 @@
use axum::body::{Body, Bytes};
use axum::http::{HeaderName, HeaderValue, Response};
use crate::gateway::constants::CONTROL_EXECUTED_HEADER;
use crate::gateway::{AppState, GatewayControlDecision, GatewayError};
use crate::constants::CONTROL_EXECUTED_HEADER;
use crate::control::GatewayControlDecision;
use crate::{AppState, GatewayError};
use super::resolve_execution_runtime_auth_context;
@@ -37,7 +38,7 @@ pub(crate) async fn maybe_execute_via_control(
}
let response = if require_stream {
crate::gateway::maybe_execute_via_execution_runtime_stream(
crate::execution_runtime::maybe_execute_via_execution_runtime_stream(
state,
parts,
&body_bytes,
@@ -46,7 +47,7 @@ pub(crate) async fn maybe_execute_via_control(
)
.await?
} else {
crate::gateway::maybe_execute_via_execution_runtime_sync(
crate::execution_runtime::maybe_execute_via_execution_runtime_sync(
state,
parts,
&body_bytes,

View File

@@ -1,13 +1,9 @@
#[cfg(test)]
use axum::http::Uri;
#[path = "auth/mod.rs"]
mod auth;
#[path = "execute.rs"]
mod execute;
#[path = "public.rs"]
mod public;
#[path = "route.rs"]
mod route;
pub(crate) use auth::{

View File

@@ -1,7 +1,7 @@
use axum::http::Uri;
use crate::gateway::headers::header_value_str;
use crate::gateway::{AppState, GatewayError};
use crate::headers::header_value_str;
use crate::{AppState, GatewayError};
use super::{resolve_control_route, GatewayControlDecision};

View File

@@ -1,5 +1,7 @@
use super::{classified, ClassifiedRoute};
use crate::gateway::{is_tunnel_heartbeat_path, is_tunnel_node_status_path, TUNNEL_ROUTE_FAMILY};
use crate::tunnel::{
is_tunnel_heartbeat_path, is_tunnel_node_status_path, TUNNEL_ROUTE_FAMILY,
};
pub(super) fn classify_internal_route(
method: &http::Method,

View File

@@ -1,17 +1,12 @@
use axum::http::Uri;
use crate::gateway::headers::header_value_str;
use crate::gateway::{AppState, GatewayError};
use crate::headers::header_value_str;
use crate::{AppState, GatewayError};
#[path = "route/admin.rs"]
mod admin;
#[path = "route/ai.rs"]
mod ai;
#[path = "route/internal.rs"]
mod internal;
#[path = "route/oauth.rs"]
mod oauth;
#[path = "route/public_support.rs"]
mod public_support;
use super::auth::{resolve_control_decision_auth, ControlDecisionAuthResolution};
@@ -122,14 +117,14 @@ pub(crate) async fn resolve_control_route(
method: &http::Method,
uri: &Uri,
headers: &http::HeaderMap,
_trace_id: &str,
trace_id: &str,
) -> Result<Option<GatewayControlDecision>, GatewayError> {
let Some(mut decision) = classify_control_route(method, uri, headers) else {
return Ok(None);
};
decision.public_query_string = uri.query().map(ToOwned::to_owned);
match resolve_control_decision_auth(state, headers, uri, decision).await? {
match resolve_control_decision_auth(state, headers, uri, trace_id, decision).await? {
ControlDecisionAuthResolution::Resolved(decision) => Ok(Some(decision)),
}
}

View File

@@ -1,6 +1,7 @@
use http::Uri;
use super::{classify_control_route, headers};
use crate::tunnel::TUNNEL_ROUTE_FAMILY;
#[test]
fn classifies_internal_tunnel_heartbeat_as_internal_proxy_route() {
@@ -12,10 +13,7 @@ fn classifies_internal_tunnel_heartbeat_as_internal_proxy_route() {
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
assert_eq!(decision.route_class.as_deref(), Some("internal_proxy"));
assert_eq!(
decision.route_family.as_deref(),
Some(crate::gateway::TUNNEL_ROUTE_FAMILY)
);
assert_eq!(decision.route_family.as_deref(), Some(TUNNEL_ROUTE_FAMILY));
assert_eq!(decision.route_kind.as_deref(), Some("heartbeat"));
assert_eq!(decision.auth_endpoint_signature.as_deref(), Some(""));
assert!(!decision.is_execution_runtime_candidate());
@@ -47,10 +45,7 @@ fn classifies_internal_tunnel_node_status_as_internal_proxy_route() {
classify_control_route(&http::Method::POST, &uri, &headers).expect("route should classify");
assert_eq!(decision.route_class.as_deref(), Some("internal_proxy"));
assert_eq!(
decision.route_family.as_deref(),
Some(crate::gateway::TUNNEL_ROUTE_FAMILY)
);
assert_eq!(decision.route_family.as_deref(), Some(TUNNEL_ROUTE_FAMILY));
assert_eq!(decision.route_kind.as_deref(), Some("node_status"));
assert_eq!(decision.auth_endpoint_signature.as_deref(), Some(""));
assert!(!decision.is_execution_runtime_candidate());