feat: 全栈功能增强 - 扩展 provider/pool 管理、完善调度与数据层、重构前端 Pool 页面

后端:
- 扩展 pool_admin payloads 和 provider query models,增强 endpoint key 管理
- 完善 scheduler-core 候选排序与请求候选逻辑
- 增强 usage-runtime 写入、provider-transport 网络层与 OAuth 刷新
- 改进 AI pipeline 响应转换与流式处理
- 扩展 global_models/provider_catalog 数据层查询能力
- 增强 video-tasks-core 多 provider 支持
- 新增大量集成测试覆盖 pool/keys/provider_query/frontdoor

前端:
- 重构 PoolManagement 页面,拆分状态管理/对话框逻辑到独立模块
- 新增 poolAdvancedDialog/poolSchedulingDialog/poolManagementState/poolMobilePresentation 工具函数及测试
- 改进 Dialog 组件与 provider tabs 显示

部署:
- 更新 Rust CI workflow 和 Dockerfile 构建配置

Closes #275
Co-authored-by: AAEE86 <[email protected]>
This commit is contained in:
fawney19
2026-04-09 13:51:50 +08:00
parent 4fc95adfb9
commit b0b40c16ff
97 changed files with 5816 additions and 1881 deletions
+134 -12
View File
@@ -5,8 +5,8 @@ use aether_crypto::warm_python_fernet_secret;
use aether_data::postgres::PostgresPoolConfig;
use aether_data::redis::RedisClientConfig;
use aether_gateway::{
build_router_with_state, AppState, FrontdoorCorsConfig, FrontdoorUserRpmConfig,
GatewayDataConfig, UsageRuntimeConfig, VideoTaskTruthSourceMode,
attach_static_frontend, build_router_with_state, AppState, FrontdoorCorsConfig,
FrontdoorUserRpmConfig, GatewayDataConfig, UsageRuntimeConfig, VideoTaskTruthSourceMode,
};
use aether_runtime::{
init_service_runtime, DistributedConcurrencyGate, FileLoggingConfig, LogDestination, LogFormat,
@@ -494,6 +494,18 @@ struct Args {
#[arg(long, env = "AETHER_GATEWAY_BIND", default_value = "0.0.0.0:80")]
bind: String,
/// 容器内健康检查入口:根据当前 bind 端口探测本地 /health。
#[arg(long, hide = true, default_value_t = false)]
healthcheck: bool,
#[arg(
long,
hide = true,
env = "AETHER_GATEWAY_HEALTHCHECK_TIMEOUT_MS",
default_value_t = 3_000
)]
healthcheck_timeout_ms: u64,
#[arg(
long,
env = "AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY",
@@ -609,6 +621,70 @@ fn resolve_gateway_log_instance_id() -> String {
.unwrap_or_else(|| "local".to_string())
}
fn resolve_healthcheck_url(bind: &str) -> Result<String, std::io::Error> {
let trimmed = bind.trim();
if trimmed.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"AETHER_GATEWAY_BIND cannot be empty when --healthcheck is enabled",
));
}
if let Ok(socket_addr) = trimmed.parse::<std::net::SocketAddr>() {
let host = match socket_addr.ip() {
std::net::IpAddr::V4(ip) if ip.is_unspecified() => "127.0.0.1".to_string(),
std::net::IpAddr::V4(ip) => ip.to_string(),
std::net::IpAddr::V6(ip) if ip.is_unspecified() => "[::1]".to_string(),
std::net::IpAddr::V6(ip) => format!("[{ip}]"),
};
return Ok(format!("http://{host}:{}/health", socket_addr.port()));
}
let (host, port) = trimmed.rsplit_once(':').ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"AETHER_GATEWAY_BIND must include a port when --healthcheck is enabled: {trimmed}"
),
)
})?;
let port = port.parse::<u16>().map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid healthcheck port in AETHER_GATEWAY_BIND={trimmed}: {error}"),
)
})?;
let host = host.trim();
if host.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("invalid host in AETHER_GATEWAY_BIND={trimmed}"),
));
}
let host = if host.contains(':') && !host.starts_with('[') {
format!("[{host}]")
} else {
host.to_string()
};
Ok(format!("http://{host}:{port}/health"))
}
async fn run_healthcheck(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
let url = resolve_healthcheck_url(&args.bind)?;
reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(
args.healthcheck_timeout_ms.max(1),
))
.build()?
.get(url)
.send()
.await?
.error_for_status()?;
Ok(())
}
fn validate_deployment_topology(
args: &Args,
data_postgres_url: Option<&str>,
@@ -685,6 +761,9 @@ fn validate_deployment_topology(
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
if args.healthcheck {
return run_healthcheck(&args).await;
}
init_service_runtime(args.runtime_config()?)?;
let data_postgres_url = args.data.effective_postgres_url();
let data_redis_url = args.data.effective_redis_url();
@@ -776,7 +855,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.or_else(|| data_redis_url.as_deref())
.or(data_redis_url.as_deref())
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
@@ -847,17 +926,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Compose the final router: API routes + optional static file serving + CF header stripping
let router = if let Some(ref static_dir) = args.static_dir {
use tower_http::compression::CompressionLayer;
use tower_http::services::{ServeDir, ServeFile};
let static_path = std::path::PathBuf::from(static_dir);
let index_html = static_path.join("index.html");
info!(static_dir = %static_dir, "serving frontend static files");
// ServeDir with SPA fallback: if no static file matches, serve index.html
let serve_dir = ServeDir::new(&static_path).not_found_service(ServeFile::new(&index_html));
api_router
.fallback_service(serve_dir)
attach_static_frontend(api_router, static_dir)
.layer(CompressionLayer::new())
.layer(axum::middleware::from_fn(
aether_gateway::strip_cf_headers_middleware,
@@ -878,3 +949,54 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::resolve_healthcheck_url;
#[test]
fn resolves_ipv4_healthcheck_url() {
assert_eq!(
resolve_healthcheck_url("0.0.0.0:80").unwrap(),
"http://127.0.0.1:80/health"
);
}
#[test]
fn resolves_ipv6_healthcheck_url() {
assert_eq!(
resolve_healthcheck_url("[::]:8080").unwrap(),
"http://[::1]:8080/health"
);
}
#[test]
fn preserves_explicit_ipv4_bind_for_healthcheck_url() {
assert_eq!(
resolve_healthcheck_url("172.18.0.2:9000").unwrap(),
"http://172.18.0.2:9000/health"
);
}
#[test]
fn preserves_explicit_ipv6_bind_for_healthcheck_url() {
assert_eq!(
resolve_healthcheck_url("[2001:db8::2]:9000").unwrap(),
"http://[2001:db8::2]:9000/health"
);
}
#[test]
fn preserves_hostname_healthcheck_url() {
assert_eq!(
resolve_healthcheck_url("gateway.internal:9000").unwrap(),
"http://gateway.internal:9000/health"
);
}
#[test]
fn rejects_bind_without_port() {
let error = resolve_healthcheck_url("not-a-socket").unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
}
}