mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
feat(proxy): 支持远程推送升级与proxy元数据上报
- aether-proxy 注册和心跳时上报 proxy_metadata(含版本号) - 心跳 ACK 支持 upgrade_to 字段,proxy 收到后自动执行升级 - 重构 upgrade 逻辑,新增 perform_upgrade 用于远程触发的自动升级 - stream_handler 延迟统计改为仅记录连接建立延迟(DNS+TCP/TLS+TTFB) - 后端新增 proxy_metadata 数据库字段和批量升级 API - 远程配置支持下发 upgrade_to 版本指令 - 前端展示节点版本号,支持单节点和批量升级操作
This commit is contained in:
@@ -5,6 +5,7 @@ export interface ProxyNodeRemoteConfig {
|
||||
allowed_ports?: number[]
|
||||
log_level?: string
|
||||
heartbeat_interval?: number
|
||||
upgrade_to?: string | null
|
||||
}
|
||||
|
||||
export interface ProxyNode {
|
||||
@@ -37,6 +38,7 @@ export interface ProxyNode {
|
||||
failed_requests: number
|
||||
dns_failures: number
|
||||
stream_errors: number
|
||||
proxy_metadata: Record<string, unknown> | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
@@ -109,6 +111,14 @@ export const proxyNodesApi = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
async batchUpgrade(version: string): Promise<{ version: string; updated: number; skipped: number; node_ids: string[] }> {
|
||||
const response = await apiClient.post<{ version: string; updated: number; skipped: number; node_ids: string[] }>(
|
||||
'/api/admin/proxy-nodes/upgrade',
|
||||
{ version }
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async testProxyUrl(data: { proxy_url: string; username?: string; password?: string }): Promise<ProxyNodeTestResult> {
|
||||
const response = await apiClient.post<ProxyNodeTestResult>('/api/admin/proxy-nodes/test-url', data)
|
||||
return response.data
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
代理节点
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
class="h-7 text-xs"
|
||||
@click="showBatchUpgradeDialog = true"
|
||||
>
|
||||
升级
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
class="h-7 text-xs"
|
||||
@@ -89,6 +97,14 @@
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="h-4 w-px bg-border" />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="h-8 text-xs"
|
||||
@click="showBatchUpgradeDialog = true"
|
||||
>
|
||||
批量升级
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -132,6 +148,9 @@
|
||||
<TableHead class="w-[100px] h-12 font-semibold text-center">
|
||||
延迟
|
||||
</TableHead>
|
||||
<TableHead class="w-[120px] h-12 font-semibold text-center">
|
||||
版本
|
||||
</TableHead>
|
||||
<TableHead class="w-[160px] h-12 font-semibold">
|
||||
最后心跳
|
||||
</TableHead>
|
||||
@@ -192,6 +211,9 @@
|
||||
<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>
|
||||
</TableCell>
|
||||
<TableCell class="py-4 text-center">
|
||||
<span class="text-sm tabular-nums">{{ node.is_manual ? '-' : nodeProxyVersion(node) }}</span>
|
||||
</TableCell>
|
||||
<TableCell class="py-4">
|
||||
<span class="text-xs text-muted-foreground">{{ formatTime(node.last_heartbeat_at) }}</span>
|
||||
</TableCell>
|
||||
@@ -258,7 +280,7 @@
|
||||
</TableRow>
|
||||
<TableRow v-if="paginatedNodes.length === 0">
|
||||
<TableCell
|
||||
colspan="9"
|
||||
colspan="10"
|
||||
class="py-12 text-center text-muted-foreground text-sm"
|
||||
>
|
||||
{{ store.loading ? '加载中...' : '暂无代理节点' }}
|
||||
@@ -296,6 +318,12 @@
|
||||
<HardwareTooltip :node="node" />
|
||||
</div>
|
||||
<code class="text-xs text-muted-foreground">{{ nodeAddress(node) }}</code>
|
||||
<div
|
||||
v-if="!node.is_manual"
|
||||
class="text-[11px] text-muted-foreground mt-1"
|
||||
>
|
||||
版本: {{ nodeProxyVersion(node) }}
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
:variant="statusVariant(node.status)"
|
||||
@@ -540,6 +568,16 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label>升级到版本</Label>
|
||||
<Input
|
||||
v-model="configForm.upgrade_to"
|
||||
placeholder="例如 0.2.3"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
留空可清除已有升级指令
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="configNode"
|
||||
class="text-xs text-muted-foreground"
|
||||
@@ -563,6 +601,43 @@
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- 批量升级对话框 -->
|
||||
<Dialog
|
||||
:model-value="showBatchUpgradeDialog"
|
||||
title="批量升级"
|
||||
description="向所有在线 tunnel 节点下发升级版本指令"
|
||||
:icon="Settings"
|
||||
size="sm"
|
||||
@update:model-value="(open: boolean) => { if (!open) { showBatchUpgradeDialog = false; batchUpgradeVersion = '' } }"
|
||||
>
|
||||
<form
|
||||
class="space-y-4"
|
||||
@submit.prevent="handleBatchUpgrade"
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<Label>目标版本</Label>
|
||||
<Input
|
||||
v-model="batchUpgradeVersion"
|
||||
placeholder="例如 0.2.3"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<template #footer>
|
||||
<Button
|
||||
variant="outline"
|
||||
@click="showBatchUpgradeDialog = false; batchUpgradeVersion = ''"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
:disabled="batchUpgrading || !batchUpgradeVersion.trim()"
|
||||
@click="handleBatchUpgrade"
|
||||
>
|
||||
{{ batchUpgrading ? '下发中...' : '确认下发' }}
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- 连接事件对话框 -->
|
||||
<Dialog
|
||||
:open="showEventsDialog"
|
||||
@@ -699,7 +774,11 @@ const configForm = ref({
|
||||
allowed_ports: '',
|
||||
log_level: 'info',
|
||||
heartbeat_interval: '30',
|
||||
upgrade_to: '',
|
||||
})
|
||||
const showBatchUpgradeDialog = ref(false)
|
||||
const batchUpgradeVersion = ref('')
|
||||
const batchUpgrading = ref(false)
|
||||
|
||||
// 连接事件对话框
|
||||
const showEventsDialog = ref(false)
|
||||
@@ -840,6 +919,7 @@ function handleConfig(node: ProxyNode) {
|
||||
allowed_ports: rc.allowed_ports?.join(', ') || '',
|
||||
log_level: rc.log_level || 'info',
|
||||
heartbeat_interval: String(rc.heartbeat_interval || node.heartbeat_interval || 30),
|
||||
upgrade_to: rc.upgrade_to || '',
|
||||
}
|
||||
showConfigDialog.value = true
|
||||
}
|
||||
@@ -873,6 +953,12 @@ async function handleSaveConfig() {
|
||||
if (!isNaN(hb) && hb >= 5) {
|
||||
data.heartbeat_interval = hb
|
||||
}
|
||||
const targetVersion = configForm.value.upgrade_to.trim()
|
||||
if (targetVersion) {
|
||||
data.upgrade_to = targetVersion
|
||||
} else if (configNode.value.remote_config?.upgrade_to) {
|
||||
data.upgrade_to = null
|
||||
}
|
||||
await proxyNodesApi.updateNodeConfig(configNode.value.id, data)
|
||||
success('远程配置已保存,将在下次心跳时生效')
|
||||
handleConfigDialogClose(false)
|
||||
@@ -884,6 +970,23 @@ async function handleSaveConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchUpgrade() {
|
||||
const version = batchUpgradeVersion.value.trim()
|
||||
if (!version || batchUpgrading.value) return
|
||||
batchUpgrading.value = true
|
||||
try {
|
||||
const result = await proxyNodesApi.batchUpgrade(version)
|
||||
success(`升级指令已下发:${result.updated} 个节点,跳过 ${result.skipped} 个`)
|
||||
showBatchUpgradeDialog.value = false
|
||||
batchUpgradeVersion.value = ''
|
||||
await store.fetchNodes()
|
||||
} catch (err: unknown) {
|
||||
toastError(parseApiError(err, '批量升级下发失败'))
|
||||
} finally {
|
||||
batchUpgrading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(node: ProxyNode) {
|
||||
const confirmed = await confirmDanger(
|
||||
`确定要删除代理节点 "${node.name}" (${node.tunnel_mode ? node.ip : `${node.ip}:${node.port}`}) 吗?`,
|
||||
@@ -1009,4 +1112,13 @@ function nodeAddress(node: ProxyNode) {
|
||||
if (node.tunnel_mode) return node.ip || 'WebSocket Tunnel'
|
||||
return `${node.ip}:${node.port}`
|
||||
}
|
||||
|
||||
function nodeProxyVersion(node: ProxyNode) {
|
||||
const metadata = node.proxy_metadata
|
||||
if (!metadata || typeof metadata !== 'object') return '-'
|
||||
const version = (metadata as Record<string, unknown>).version
|
||||
if (typeof version !== 'string') return '-'
|
||||
const normalized = version.trim()
|
||||
return normalized || '-'
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user