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,8 +5,8 @@ use std::sync::{Arc, RwLock};
use std::time::Duration;
use aether_runtime::{
init_reloadable_tracing, wait_for_shutdown_signal, ConcurrencyGate, DistributedConcurrencyGate,
LogFormat, RedisDistributedConcurrencyConfig,
init_reloadable_service_tracing, wait_for_shutdown_signal, ConcurrencyGate,
DistributedConcurrencyGate, RedisDistributedConcurrencyConfig,
};
use arc_swap::ArcSwap;
use tokio::sync::{watch, Mutex};
@@ -338,14 +338,13 @@ async fn retry_failed_registrations(
}
fn init_tracing(config: &Config) {
let format = if config.log_json {
LogFormat::Json
} else {
LogFormat::Pretty
};
let reloader = init_reloadable_tracing(&config.log_level, format)
.expect("proxy tracing should initialize");
let reloader = init_reloadable_service_tracing(
&config.log_level,
config
.service_runtime_config()
.expect("proxy service runtime config should be valid"),
)
.expect("proxy tracing should initialize");
runtime::set_log_reloader(reloader);
}

View File

@@ -1,5 +1,8 @@
use std::path::Path;
use aether_runtime::{
FileLoggingConfig, LogDestination, LogFormat, LogRotation, ServiceRuntimeConfig,
};
use clap::Parser;
use serde::{Deserialize, Serialize};
@@ -33,6 +36,40 @@ const DELEGATE_TO_UPSTREAM: &[(&str, &str)] = &[
("delegate_tcp_nodelay", "upstream_tcp_nodelay"),
];
#[derive(clap::ValueEnum, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ProxyLogDestinationArg {
Stdout,
File,
Both,
}
impl From<ProxyLogDestinationArg> for LogDestination {
fn from(value: ProxyLogDestinationArg) -> Self {
match value {
ProxyLogDestinationArg::Stdout => LogDestination::Stdout,
ProxyLogDestinationArg::File => LogDestination::File,
ProxyLogDestinationArg::Both => LogDestination::Both,
}
}
}
#[derive(clap::ValueEnum, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ProxyLogRotationArg {
Hourly,
Daily,
}
impl From<ProxyLogRotationArg> for LogRotation {
fn from(value: ProxyLogRotationArg) -> Self {
match value {
ProxyLogRotationArg::Hourly => LogRotation::Hourly,
ProxyLogRotationArg::Daily => LogRotation::Daily,
}
}
}
/// Aether tunnel proxy.
///
/// Deployed on overseas VPS to relay API traffic for Aether instances
@@ -242,6 +279,36 @@ pub struct Config {
#[arg(long, env = "AETHER_PROXY_LOG_JSON", default_value_t = false)]
pub log_json: bool,
/// Log destination (stdout, file, both)
#[arg(
long,
env = "AETHER_PROXY_LOG_DESTINATION",
value_enum,
default_value = "stdout"
)]
pub log_destination: ProxyLogDestinationArg,
/// Log directory when file logging is enabled
#[arg(long, env = "AETHER_PROXY_LOG_DIR")]
pub log_dir: Option<String>,
/// Log rotation schedule for file logging
#[arg(
long,
env = "AETHER_PROXY_LOG_ROTATION",
value_enum,
default_value = "daily"
)]
pub log_rotation: ProxyLogRotationArg,
/// Log file retention days for file logging
#[arg(long, env = "AETHER_PROXY_LOG_RETENTION_DAYS", default_value_t = 7)]
pub log_retention_days: u64,
/// Maximum number of retained rolled log files
#[arg(long, env = "AETHER_PROXY_LOG_MAX_FILES", default_value_t = 30)]
pub log_max_files: usize,
/// Tunnel reconnect base delay in milliseconds (used by exponential backoff)
#[arg(
long,
@@ -356,8 +423,49 @@ impl Config {
if self.distributed_stream_command_timeout_ms == 0 {
anyhow::bail!("distributed_stream_command_timeout_ms must be > 0");
}
if matches!(
self.log_destination,
ProxyLogDestinationArg::File | ProxyLogDestinationArg::Both
) && self
.log_dir
.as_deref()
.map(str::trim)
.is_none_or(|value| value.is_empty())
{
anyhow::bail!("log_dir must be set when AETHER_PROXY_LOG_DESTINATION is file or both");
}
Ok(())
}
pub fn service_runtime_config(&self) -> anyhow::Result<ServiceRuntimeConfig> {
let mut config = ServiceRuntimeConfig::new("aether-proxy", "aether_proxy=info")
.with_log_format(if self.log_json {
LogFormat::Json
} else {
LogFormat::Pretty
})
.with_log_destination(self.log_destination.into())
.with_node_role("proxy")
.with_instance_id(self.node_name.trim().to_string());
if matches!(
self.log_destination,
ProxyLogDestinationArg::File | ProxyLogDestinationArg::Both
) {
let log_dir = self
.log_dir
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| anyhow::anyhow!("log_dir must be configured for file logging"))?;
config = config.with_file_logging(FileLoggingConfig::new(
log_dir,
self.log_rotation.into(),
self.log_retention_days,
self.log_max_files,
));
}
Ok(config)
}
}
/// Per-server connection config (used in multi-server TOML `[[servers]]`).
@@ -432,6 +540,16 @@ pub struct ConfigFile {
#[serde(skip_serializing_if = "Option::is_none")]
pub log_json: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub log_destination: Option<ProxyLogDestinationArg>,
#[serde(skip_serializing_if = "Option::is_none")]
pub log_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub log_rotation: Option<ProxyLogRotationArg>,
#[serde(skip_serializing_if = "Option::is_none")]
pub log_retention_days: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub log_max_files: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tunnel_reconnect_base_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tunnel_reconnect_max_ms: Option<u64>,
@@ -678,6 +796,24 @@ impl ConfigFile {
);
set!("AETHER_PROXY_LOG_LEVEL", self.log_level);
set!("AETHER_PROXY_LOG_JSON", self.log_json);
set!(
"AETHER_PROXY_LOG_DESTINATION",
self.log_destination.map(|v| match v {
ProxyLogDestinationArg::Stdout => "stdout",
ProxyLogDestinationArg::File => "file",
ProxyLogDestinationArg::Both => "both",
})
);
set!("AETHER_PROXY_LOG_DIR", self.log_dir.as_deref());
set!(
"AETHER_PROXY_LOG_ROTATION",
self.log_rotation.map(|v| match v {
ProxyLogRotationArg::Hourly => "hourly",
ProxyLogRotationArg::Daily => "daily",
})
);
set!("AETHER_PROXY_LOG_RETENTION_DAYS", self.log_retention_days);
set!("AETHER_PROXY_LOG_MAX_FILES", self.log_max_files);
set!(
"AETHER_PROXY_TUNNEL_RECONNECT_BASE_MS",
self.tunnel_reconnect_base_ms

View File

@@ -1,4 +1,5 @@
use aether_http::{build_http_client, jittered_delay_for_retry, HttpClientConfig, HttpRetryConfig};
use aether_runtime::summarize_text_payload;
use reqwest::{Client, StatusCode};
use serde::{Deserialize, Serialize};
use tokio::time::sleep;
@@ -132,7 +133,13 @@ impl AetherClient {
let status = resp.status();
if !status.is_success() {
let text = resp.text().await.unwrap_or_default();
anyhow::bail!("register failed (HTTP {}): {}", status, text);
let summary = summarize_text_payload(&text);
anyhow::bail!(
"register failed (HTTP {}): response body redacted (bytes={}, sha256={})",
status,
summary.bytes,
summary.sha256
);
}
let data: RegisterResponse = resp.json().await?;
@@ -167,9 +174,21 @@ impl AetherClient {
Ok(())
}
Ok(r) => {
let status = r.status();
let text = r.text().await.unwrap_or_default();
error!(body = %text, "unregister failed");
anyhow::bail!("unregister failed: {}", text);
let summary = summarize_text_payload(&text);
error!(
status = %status,
body_bytes = summary.bytes,
body_sha256 = %summary.sha256,
"unregister failed"
);
anyhow::bail!(
"unregister failed (HTTP {}): response body redacted (bytes={}, sha256={})",
status,
summary.bytes,
summary.sha256
);
}
Err(e) => {
// Best-effort during shutdown

View File

@@ -63,11 +63,15 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
Type=simple\n\
WorkingDirectory={working_dir}\n\
Environment=AETHER_PROXY_CONFIG={config_str}\n\
Environment=AETHER_PROXY_LOG_DESTINATION=both\n\
Environment=AETHER_PROXY_LOG_DIR=/var/log/aether-proxy\n\
ExecStart={exe_str}\n\
Restart=on-failure\n\
RestartSec=5\n\
LimitNOFILE=65535\n\
UMask=0077\n\
LogsDirectory=aether-proxy\n\
LogsDirectoryMode=0750\n\
\n\
[Install]\n\
WantedBy=multi-user.target\n",

View File

@@ -358,8 +358,7 @@ mod tests {
async fn start_gateway_on_port(
port: u16,
) -> Result<(GatewayAppState, tokio::task::JoinHandle<()>), std::io::Error> {
let state =
GatewayAppState::new("http://127.0.0.1:9").expect("gateway test state should build");
let state = GatewayAppState::new().expect("gateway test state should build");
let router = build_router_with_state(state.clone());
let handle = spawn_router_on_port(port, router).await?;
Ok((state, handle))
@@ -475,6 +474,11 @@ mod tests {
upstream_tcp_nodelay: true,
log_level: "info".to_string(),
log_json: false,
log_destination: crate::config::ProxyLogDestinationArg::Stdout,
log_dir: None,
log_rotation: crate::config::ProxyLogRotationArg::Daily,
log_retention_days: 7,
log_max_files: 30,
tunnel_reconnect_base_ms: 50,
tunnel_reconnect_max_ms: 250,
tunnel_ping_interval_secs: 1,

View File

@@ -690,6 +690,11 @@ mod tests {
upstream_tcp_nodelay: true,
log_level: "info".to_string(),
log_json: false,
log_destination: crate::config::ProxyLogDestinationArg::Stdout,
log_dir: None,
log_rotation: crate::config::ProxyLogRotationArg::Daily,
log_retention_days: 7,
log_max_files: 30,
tunnel_reconnect_base_ms: 500,
tunnel_reconnect_max_ms: 30_000,
tunnel_ping_interval_secs: 15,