mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 18:59:50 +08:00
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:
+134
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user