mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +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).
|
||||
if std::env::var_os("INVOCATION_ID").is_none() && setup::service::is_service_active() {
|
||||
eprintln!("Warning: systemd service is already running.");
|
||||
eprintln!("Use `aether-proxy stop` to stop it first, or manage via subcommands:");
|
||||
eprintln!(" aether-proxy status / logs / restart / stop");
|
||||
eprintln!("Use `./aether-proxy stop` to stop it first, or manage via subcommands:");
|
||||
eprintln!(" ./aether-proxy status / logs / restart / stop");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
|
||||
anyhow::bail!("systemd not available");
|
||||
}
|
||||
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()?;
|
||||
@@ -94,11 +94,11 @@ pub fn install_service(config_path: &Path) -> anyhow::Result<()> {
|
||||
|
||||
eprintln!();
|
||||
eprintln!(" Commands:");
|
||||
eprintln!(" aether-proxy status # service status");
|
||||
eprintln!(" aether-proxy logs # tail logs");
|
||||
eprintln!(" sudo aether-proxy restart # restart");
|
||||
eprintln!(" sudo aether-proxy stop # stop");
|
||||
eprintln!(" sudo aether-proxy uninstall # remove service");
|
||||
eprintln!(" ./aether-proxy status # service status");
|
||||
eprintln!(" ./aether-proxy logs # tail logs");
|
||||
eprintln!(" sudo ./aether-proxy restart # restart");
|
||||
eprintln!(" sudo ./aether-proxy stop # stop");
|
||||
eprintln!(" sudo ./aether-proxy uninstall # remove service");
|
||||
eprintln!();
|
||||
|
||||
Ok(())
|
||||
@@ -166,7 +166,7 @@ pub fn is_service_active() -> bool {
|
||||
|
||||
fn ensure_service_installed() -> anyhow::Result<()> {
|
||||
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(())
|
||||
}
|
||||
@@ -174,7 +174,7 @@ fn ensure_service_installed() -> anyhow::Result<()> {
|
||||
fn ensure_root_and_service() -> anyhow::Result<()> {
|
||||
ensure_service_installed()?;
|
||||
if !is_root() {
|
||||
anyhow::bail!("root required, use: sudo aether-proxy <command>");
|
||||
anyhow::bail!("root required, use: sudo ./aether-proxy <command>");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -31,7 +31,31 @@ def _table_exists(table_name: str) -> bool:
|
||||
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:
|
||||
# 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: 新增错误指标字段
|
||||
if not _column_exists("proxy_nodes", "failed_requests"):
|
||||
op.add_column(
|
||||
@@ -102,6 +126,16 @@ def upgrade() -> 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"):
|
||||
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")
|
||||
|
||||
@@ -124,10 +124,10 @@
|
||||
状态
|
||||
</TableHead>
|
||||
<TableHead class="w-[100px] h-12 font-semibold text-center">
|
||||
连接数
|
||||
总请求
|
||||
</TableHead>
|
||||
<TableHead class="w-[100px] h-12 font-semibold text-center">
|
||||
总请求
|
||||
失败率
|
||||
</TableHead>
|
||||
<TableHead class="w-[100px] h-12 font-semibold text-center">
|
||||
延迟
|
||||
@@ -181,10 +181,13 @@
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<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 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 class="py-4 text-center">
|
||||
<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) }}
|
||||
</Badge>
|
||||
</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>
|
||||
<span class="block text-foreground/60">区域</span>
|
||||
<span>{{ formatRegion(node.region) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="block text-foreground/60">连接</span>
|
||||
<span class="tabular-nums">{{ node.active_connections }}</span>
|
||||
<span class="block text-foreground/60">总请求</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>
|
||||
<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' })
|
||||
}
|
||||
|
||||
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) {
|
||||
if (node.is_manual) return node.proxy_url || `${node.ip}:${node.port}`
|
||||
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("")
|
||||
async def list_proxy_nodes(
|
||||
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),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
db: Session = Depends(get_db),
|
||||
|
||||
@@ -224,7 +224,6 @@ class ProxyNodeStatus(PyEnum):
|
||||
"""代理节点状态"""
|
||||
|
||||
ONLINE = "online"
|
||||
UNHEALTHY = "unhealthy"
|
||||
OFFLINE = "offline"
|
||||
|
||||
|
||||
|
||||
@@ -392,13 +392,13 @@ class ProxyNodeService:
|
||||
query = db.query(ProxyNode)
|
||||
if status:
|
||||
normalized = status.strip().lower()
|
||||
allowed = {"online", "unhealthy", "offline"}
|
||||
allowed = {"online", "offline"}
|
||||
if normalized not in allowed:
|
||||
raise InvalidRequestException(f"status 必须是以下之一: {sorted(allowed)}", "status")
|
||||
query = query.filter(ProxyNode.status == ProxyNodeStatus(normalized))
|
||||
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
|
||||
Reference in New Issue
Block a user