mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor(proxy): 移除 unhealthy 状态、前端展示失败率替换连接数、命令提示修正
- 移除 ProxyNodeStatus.UNHEALTHY 枚举值,仅保留 online/offline - 迁移脚本将已有 unhealthy 数据迁移为 offline,upgrade/downgrade 均有幂等性保护 - 前端代理节点列表用失败率列替换连接数列,超过 5% 高亮显示 - 节点列表排序从 updated_at DESC 改为 name ASC - Rust 端命令行提示从 aether-proxy 改为 ./aether-proxy
This commit is contained in:
@@ -136,8 +136,8 @@ async fn run_proxy(config: Config) -> anyhow::Result<()> {
|
|||||||
// Skip this check when we ARE the systemd service (INVOCATION_ID is set by systemd).
|
// Skip this check when we ARE the systemd service (INVOCATION_ID is set by systemd).
|
||||||
if std::env::var_os("INVOCATION_ID").is_none() && setup::service::is_service_active() {
|
if std::env::var_os("INVOCATION_ID").is_none() && setup::service::is_service_active() {
|
||||||
eprintln!("Warning: systemd service is already running.");
|
eprintln!("Warning: systemd service is already running.");
|
||||||
eprintln!("Use `aether-proxy stop` to stop it first, or manage via subcommands:");
|
eprintln!("Use `./aether-proxy stop` to stop it first, or manage via subcommands:");
|
||||||
eprintln!(" aether-proxy status / logs / restart / stop");
|
eprintln!(" ./aether-proxy status / logs / restart / stop");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
|
|||||||
anyhow::bail!("systemd not available");
|
anyhow::bail!("systemd not available");
|
||||||
}
|
}
|
||||||
if !is_root() {
|
if !is_root() {
|
||||||
anyhow::bail!("root required, use: sudo aether-proxy setup");
|
anyhow::bail!("root required, use: sudo ./aether-proxy setup");
|
||||||
}
|
}
|
||||||
|
|
||||||
let exe_path = std::env::current_exe()?.canonicalize()?;
|
let exe_path = std::env::current_exe()?.canonicalize()?;
|
||||||
@@ -94,11 +94,11 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
eprintln!();
|
eprintln!();
|
||||||
eprintln!(" Commands:");
|
eprintln!(" Commands:");
|
||||||
eprintln!(" aether-proxy status # service status");
|
eprintln!(" ./aether-proxy status # service status");
|
||||||
eprintln!(" aether-proxy logs # tail logs");
|
eprintln!(" ./aether-proxy logs # tail logs");
|
||||||
eprintln!(" sudo aether-proxy restart # restart");
|
eprintln!(" sudo ./aether-proxy restart # restart");
|
||||||
eprintln!(" sudo aether-proxy stop # stop");
|
eprintln!(" sudo ./aether-proxy stop # stop");
|
||||||
eprintln!(" sudo aether-proxy uninstall # remove service");
|
eprintln!(" sudo ./aether-proxy uninstall # remove service");
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -166,7 +166,7 @@ pub fn is_service_active() -> bool {
|
|||||||
|
|
||||||
fn ensure_service_installed() -> anyhow::Result<()> {
|
fn ensure_service_installed() -> anyhow::Result<()> {
|
||||||
if !std::path::Path::new(UNIT_PATH).exists() {
|
if !std::path::Path::new(UNIT_PATH).exists() {
|
||||||
anyhow::bail!("service not installed, run `sudo aether-proxy setup` first");
|
anyhow::bail!("service not installed, run `sudo ./aether-proxy setup` first");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ fn ensure_service_installed() -> anyhow::Result<()> {
|
|||||||
fn ensure_root_and_service() -> anyhow::Result<()> {
|
fn ensure_root_and_service() -> anyhow::Result<()> {
|
||||||
ensure_service_installed()?;
|
ensure_service_installed()?;
|
||||||
if !is_root() {
|
if !is_root() {
|
||||||
anyhow::bail!("root required, use: sudo aether-proxy <command>");
|
anyhow::bail!("root required, use: sudo ./aether-proxy <command>");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,31 @@ def _table_exists(table_name: str) -> bool:
|
|||||||
return table_name in insp.get_table_names()
|
return table_name in insp.get_table_names()
|
||||||
|
|
||||||
|
|
||||||
|
def _enum_has_value(enum_name: str, value: str) -> bool:
|
||||||
|
"""检查 PostgreSQL 枚举类型是否包含指定值"""
|
||||||
|
bind = op.get_bind()
|
||||||
|
result = bind.execute(
|
||||||
|
sa.text(
|
||||||
|
"SELECT 1 FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid"
|
||||||
|
" WHERE t.typname = :enum_name AND e.enumlabel = :value"
|
||||||
|
),
|
||||||
|
{"enum_name": enum_name, "value": value},
|
||||||
|
)
|
||||||
|
return result.fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
|
# proxy_nodes: 将已废弃的 unhealthy 状态迁移为 offline,然后从枚举中移除
|
||||||
|
if _enum_has_value("proxynodestatus", "unhealthy"):
|
||||||
|
op.execute("UPDATE proxy_nodes SET status = 'offline' WHERE status = 'unhealthy'")
|
||||||
|
op.execute("ALTER TYPE proxynodestatus RENAME TO proxynodestatus_old")
|
||||||
|
op.execute("CREATE TYPE proxynodestatus AS ENUM ('online', 'offline')")
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE proxy_nodes ALTER COLUMN status TYPE proxynodestatus"
|
||||||
|
" USING status::text::proxynodestatus"
|
||||||
|
)
|
||||||
|
op.execute("DROP TYPE proxynodestatus_old")
|
||||||
|
|
||||||
# proxy_nodes: 新增错误指标字段
|
# proxy_nodes: 新增错误指标字段
|
||||||
if not _column_exists("proxy_nodes", "failed_requests"):
|
if not _column_exists("proxy_nodes", "failed_requests"):
|
||||||
op.add_column(
|
op.add_column(
|
||||||
@@ -102,6 +126,16 @@ def upgrade() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
# 恢复 proxynodestatus 枚举,加回 unhealthy
|
||||||
|
if not _enum_has_value("proxynodestatus", "unhealthy"):
|
||||||
|
op.execute("ALTER TYPE proxynodestatus RENAME TO proxynodestatus_old")
|
||||||
|
op.execute("CREATE TYPE proxynodestatus AS ENUM ('online', 'unhealthy', 'offline')")
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE proxy_nodes ALTER COLUMN status TYPE proxynodestatus"
|
||||||
|
" USING status::text::proxynodestatus"
|
||||||
|
)
|
||||||
|
op.execute("DROP TYPE proxynodestatus_old")
|
||||||
|
|
||||||
if _table_exists("proxy_node_events"):
|
if _table_exists("proxy_node_events"):
|
||||||
op.drop_index(op.f("ix_proxy_node_events_node_id"), table_name="proxy_node_events")
|
op.drop_index(op.f("ix_proxy_node_events_node_id"), table_name="proxy_node_events")
|
||||||
op.drop_index("idx_proxy_node_events_node_created", table_name="proxy_node_events")
|
op.drop_index("idx_proxy_node_events_node_created", table_name="proxy_node_events")
|
||||||
|
|||||||
@@ -124,10 +124,10 @@
|
|||||||
状态
|
状态
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead class="w-[100px] h-12 font-semibold text-center">
|
<TableHead class="w-[100px] h-12 font-semibold text-center">
|
||||||
连接数
|
总请求
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead class="w-[100px] h-12 font-semibold text-center">
|
<TableHead class="w-[100px] h-12 font-semibold text-center">
|
||||||
总请求
|
失败率
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead class="w-[100px] h-12 font-semibold text-center">
|
<TableHead class="w-[100px] h-12 font-semibold text-center">
|
||||||
延迟
|
延迟
|
||||||
@@ -181,10 +181,13 @@
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-4 text-center">
|
<TableCell class="py-4 text-center">
|
||||||
<span class="text-sm tabular-nums">{{ node.active_connections }}</span>
|
<span class="text-sm tabular-nums">{{ formatNumber(node.total_requests) }}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-4 text-center">
|
<TableCell class="py-4 text-center">
|
||||||
<span class="text-sm tabular-nums">{{ formatNumber(node.total_requests) }}</span>
|
<span
|
||||||
|
class="text-sm tabular-nums"
|
||||||
|
:class="failureRate(node) > 5 ? 'text-destructive font-medium' : ''"
|
||||||
|
>{{ formatFailureRate(node) }}</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-4 text-center">
|
<TableCell class="py-4 text-center">
|
||||||
<span class="text-sm tabular-nums">{{ node.avg_latency_ms != null ? `${node.avg_latency_ms.toFixed(0)}ms` : '-' }}</span>
|
<span class="text-sm tabular-nums">{{ node.avg_latency_ms != null ? `${node.avg_latency_ms.toFixed(0)}ms` : '-' }}</span>
|
||||||
@@ -301,14 +304,21 @@
|
|||||||
{{ statusLabel(node.status) }}
|
{{ statusLabel(node.status) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-2 text-xs text-muted-foreground mb-3">
|
<div class="grid grid-cols-4 gap-2 text-xs text-muted-foreground mb-3">
|
||||||
<div>
|
<div>
|
||||||
<span class="block text-foreground/60">区域</span>
|
<span class="block text-foreground/60">区域</span>
|
||||||
<span>{{ formatRegion(node.region) }}</span>
|
<span>{{ formatRegion(node.region) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="block text-foreground/60">连接</span>
|
<span class="block text-foreground/60">总请求</span>
|
||||||
<span class="tabular-nums">{{ node.active_connections }}</span>
|
<span class="tabular-nums">{{ formatNumber(node.total_requests) }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="block text-foreground/60">失败率</span>
|
||||||
|
<span
|
||||||
|
class="tabular-nums"
|
||||||
|
:class="failureRate(node) > 5 ? 'text-destructive font-medium' : ''"
|
||||||
|
>{{ formatFailureRate(node) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="block text-foreground/60">延迟</span>
|
<span class="block text-foreground/60">延迟</span>
|
||||||
@@ -980,6 +990,20 @@ function formatTime(iso: string | null) {
|
|||||||
return d.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
return d.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function failureRate(node: ProxyNode) {
|
||||||
|
if (!node.total_requests) return 0
|
||||||
|
const failed = (node.failed_requests || 0) + (node.dns_failures || 0) + (node.stream_errors || 0)
|
||||||
|
return (failed / node.total_requests) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFailureRate(node: ProxyNode) {
|
||||||
|
if (!node.total_requests) return '-'
|
||||||
|
const rate = failureRate(node)
|
||||||
|
if (rate === 0) return '0%'
|
||||||
|
if (rate < 0.1) return '<0.1%'
|
||||||
|
return `${rate.toFixed(1)}%`
|
||||||
|
}
|
||||||
|
|
||||||
function nodeAddress(node: ProxyNode) {
|
function nodeAddress(node: ProxyNode) {
|
||||||
if (node.is_manual) return node.proxy_url || `${node.ip}:${node.port}`
|
if (node.is_manual) return node.proxy_url || `${node.ip}:${node.port}`
|
||||||
if (node.tunnel_mode) return node.ip || 'WebSocket Tunnel'
|
if (node.tunnel_mode) return node.ip || 'WebSocket Tunnel'
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ async def unregister_proxy_node(request: Request, db: Session = Depends(get_db))
|
|||||||
@router.get("")
|
@router.get("")
|
||||||
async def list_proxy_nodes(
|
async def list_proxy_nodes(
|
||||||
request: Request,
|
request: Request,
|
||||||
status: str | None = Query(None, description="按状态筛选:online/unhealthy/offline"),
|
status: str | None = Query(None, description="按状态筛选:online/offline"),
|
||||||
skip: int = Query(0, ge=0),
|
skip: int = Query(0, ge=0),
|
||||||
limit: int = Query(100, ge=1, le=1000),
|
limit: int = Query(100, ge=1, le=1000),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|||||||
@@ -224,7 +224,6 @@ class ProxyNodeStatus(PyEnum):
|
|||||||
"""代理节点状态"""
|
"""代理节点状态"""
|
||||||
|
|
||||||
ONLINE = "online"
|
ONLINE = "online"
|
||||||
UNHEALTHY = "unhealthy"
|
|
||||||
OFFLINE = "offline"
|
OFFLINE = "offline"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -392,13 +392,13 @@ class ProxyNodeService:
|
|||||||
query = db.query(ProxyNode)
|
query = db.query(ProxyNode)
|
||||||
if status:
|
if status:
|
||||||
normalized = status.strip().lower()
|
normalized = status.strip().lower()
|
||||||
allowed = {"online", "unhealthy", "offline"}
|
allowed = {"online", "offline"}
|
||||||
if normalized not in allowed:
|
if normalized not in allowed:
|
||||||
raise InvalidRequestException(f"status 必须是以下之一: {sorted(allowed)}", "status")
|
raise InvalidRequestException(f"status 必须是以下之一: {sorted(allowed)}", "status")
|
||||||
query = query.filter(ProxyNode.status == ProxyNodeStatus(normalized))
|
query = query.filter(ProxyNode.status == ProxyNodeStatus(normalized))
|
||||||
|
|
||||||
total = query.count()
|
total = query.count()
|
||||||
nodes = query.order_by(ProxyNode.updated_at.desc()).offset(skip).limit(limit).all()
|
nodes = query.order_by(ProxyNode.name.asc()).offset(skip).limit(limit).all()
|
||||||
return nodes, total
|
return nodes, total
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
Reference in New Issue
Block a user