mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat(proxy): 实现代理节点批量升级回滚、隧道重定向跟随及远程配置管理
核心功能: - 新增代理节点批量升级回滚工作流,支持分批升级、健康探针、跳过/重试/取消等操作 - proxy 隧道流处理器支持 HTTP 重定向跟随(最多 10 跳),区分 307/308 可重播与不可重播请求体 - proxy 协议新增 follow_redirects / http1_only 字段,网关侧同步支持 - 新增代理节点远端配置变更接口(名称、允许端口、调度状态、升级目标等) - 新增代理节点注册/反注册/心跳的 Admin API,及节点过期清理维护任务 - gateway 隧道 owner-relay 支持流式代理大请求体,新增 5 MiB 默认限制 - 新增 ProxyNodeRegistrationMutation / ProxyNodeRemoteConfigMutation 数据类型 - proxy 配置新增重定向重播预算、心跳间隔等参数,TUI 安装向导同步更新 - 前端 ProxyNodes 页面新增批量升级操作面板及滚动进度展示
This commit is contained in:
@@ -5,9 +5,12 @@ export interface ProxyNodeRemoteConfig {
|
||||
allowed_ports?: number[]
|
||||
log_level?: string
|
||||
heartbeat_interval?: number
|
||||
scheduling_state?: ProxyNodeSchedulingState | null
|
||||
upgrade_to?: string | null
|
||||
}
|
||||
|
||||
export type ProxyNodeSchedulingState = 'active' | 'draining' | 'cordoned'
|
||||
|
||||
export interface ProxyNode {
|
||||
id: string
|
||||
name: string
|
||||
@@ -50,11 +53,50 @@ export interface ProxyNodeEvent {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ProxyNodeUpgradeRolloutProbe {
|
||||
url: string
|
||||
timeout_secs: number
|
||||
}
|
||||
|
||||
export type ProxyNodeUpgradeRolloutTrackedNodeState =
|
||||
| 'awaiting_version'
|
||||
| 'awaiting_traffic'
|
||||
| 'cooling_down'
|
||||
| 'unhealthy'
|
||||
| 'ready_to_finalize'
|
||||
|
||||
export interface ProxyNodeUpgradeRolloutTrackedNode {
|
||||
node_id: string
|
||||
state: ProxyNodeUpgradeRolloutTrackedNodeState
|
||||
dispatched_at: string | null
|
||||
version_confirmed_at: string | null
|
||||
traffic_confirmed_at: string | null
|
||||
cooldown_remaining_secs: number | null
|
||||
}
|
||||
|
||||
export interface ProxyNodeUpgradeRolloutStatus {
|
||||
version: string
|
||||
batch_size: number
|
||||
cooldown_secs: number
|
||||
started_at: string | null
|
||||
last_dispatched_at: string | null
|
||||
updated_at: string | null
|
||||
probe: ProxyNodeUpgradeRolloutProbe | null
|
||||
blocked: boolean
|
||||
online_eligible_total: number
|
||||
completed_node_ids: string[]
|
||||
pending_node_ids: string[]
|
||||
conflict_node_ids: string[]
|
||||
skipped_node_ids: string[]
|
||||
tracked_nodes: ProxyNodeUpgradeRolloutTrackedNode[]
|
||||
}
|
||||
|
||||
export interface ProxyNodeListResponse {
|
||||
items: ProxyNode[]
|
||||
total: number
|
||||
skip: number
|
||||
limit: number
|
||||
rollout: ProxyNodeUpgradeRolloutStatus | null
|
||||
}
|
||||
|
||||
export interface ManualProxyNodeCreateRequest {
|
||||
@@ -80,6 +122,69 @@ export interface ProxyNodeTestResult {
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export interface ProxyNodeBatchUpgradeResult {
|
||||
version: string
|
||||
batch_size: number
|
||||
cooldown_secs: number
|
||||
probe_url?: string | null
|
||||
probe_timeout_secs?: number | null
|
||||
updated: number
|
||||
skipped: number
|
||||
node_ids: string[]
|
||||
blocked: boolean
|
||||
pending_node_ids: string[]
|
||||
rollout_active: boolean
|
||||
completed: number
|
||||
remaining: number
|
||||
}
|
||||
|
||||
export interface ProxyNodeUpgradeRolloutCancelResult {
|
||||
cancelled: boolean
|
||||
rollout_active?: boolean
|
||||
version?: string | null
|
||||
pending_node_ids?: string[]
|
||||
conflict_node_ids?: string[]
|
||||
completed?: number
|
||||
remaining?: number
|
||||
}
|
||||
|
||||
export interface ProxyNodeUpgradeRolloutClearConflictsResult {
|
||||
version: string | null
|
||||
cleared: number
|
||||
node_ids: string[]
|
||||
updated: number
|
||||
blocked: boolean
|
||||
pending_node_ids: string[]
|
||||
rollout_active: boolean
|
||||
completed: number
|
||||
remaining: number
|
||||
}
|
||||
|
||||
export interface ProxyNodeUpgradeRolloutRestoreSkippedResult {
|
||||
version: string | null
|
||||
restored: number
|
||||
node_ids: string[]
|
||||
skipped_node_ids: string[]
|
||||
updated: number
|
||||
blocked: boolean
|
||||
pending_node_ids: string[]
|
||||
rollout_active: boolean
|
||||
completed: number
|
||||
remaining: number
|
||||
}
|
||||
|
||||
export interface ProxyNodeUpgradeRolloutNodeActionResult {
|
||||
version: string | null
|
||||
node_id: string
|
||||
skipped_node_ids: string[]
|
||||
updated: number
|
||||
blocked: boolean
|
||||
pending_node_ids: string[]
|
||||
rollout_active: boolean
|
||||
completed: number
|
||||
remaining: number
|
||||
}
|
||||
|
||||
export const proxyNodesApi = {
|
||||
async listProxyNodes(params?: { status?: string; skip?: number; limit?: number }): Promise<ProxyNodeListResponse> {
|
||||
const response = await apiClient.get<ProxyNodeListResponse>('/api/admin/proxy-nodes', { params })
|
||||
@@ -111,10 +216,53 @@ 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[] }>(
|
||||
async batchUpgrade(
|
||||
version: string,
|
||||
options?: {
|
||||
batch_size?: number
|
||||
cooldown_secs?: number
|
||||
probe_url?: string | null
|
||||
probe_timeout_secs?: number
|
||||
},
|
||||
): Promise<ProxyNodeBatchUpgradeResult> {
|
||||
const response = await apiClient.post<ProxyNodeBatchUpgradeResult>(
|
||||
'/api/admin/proxy-nodes/upgrade',
|
||||
{ version }
|
||||
{ version, ...options }
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async cancelUpgradeRollout(): Promise<ProxyNodeUpgradeRolloutCancelResult> {
|
||||
const response = await apiClient.post<ProxyNodeUpgradeRolloutCancelResult>(
|
||||
'/api/admin/proxy-nodes/upgrade/cancel'
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async clearUpgradeConflicts(): Promise<ProxyNodeUpgradeRolloutClearConflictsResult> {
|
||||
const response = await apiClient.post<ProxyNodeUpgradeRolloutClearConflictsResult>(
|
||||
'/api/admin/proxy-nodes/upgrade/clear-conflicts'
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async restoreSkippedUpgradeNodes(): Promise<ProxyNodeUpgradeRolloutRestoreSkippedResult> {
|
||||
const response = await apiClient.post<ProxyNodeUpgradeRolloutRestoreSkippedResult>(
|
||||
'/api/admin/proxy-nodes/upgrade/restore-skipped'
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async skipUpgradeNode(nodeId: string): Promise<ProxyNodeUpgradeRolloutNodeActionResult> {
|
||||
const response = await apiClient.post<ProxyNodeUpgradeRolloutNodeActionResult>(
|
||||
`/api/admin/proxy-nodes/${nodeId}/upgrade/skip`
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async retryUpgradeNode(nodeId: string): Promise<ProxyNodeUpgradeRolloutNodeActionResult> {
|
||||
const response = await apiClient.post<ProxyNodeUpgradeRolloutNodeActionResult>(
|
||||
`/api/admin/proxy-nodes/${nodeId}/upgrade/retry`
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { proxyNodesApi, type ProxyNode, type ManualProxyNodeCreateRequest } from '@/api/proxy-nodes'
|
||||
import {
|
||||
proxyNodesApi,
|
||||
type ManualProxyNodeCreateRequest,
|
||||
type ProxyNode,
|
||||
type ProxyNodeUpgradeRolloutStatus,
|
||||
} from '@/api/proxy-nodes'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
|
||||
export const useProxyNodesStore = defineStore('proxy-nodes', () => {
|
||||
const nodes = ref<ProxyNode[]>([])
|
||||
const rollout = ref<ProxyNodeUpgradeRolloutStatus | null>(null)
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
@@ -13,7 +19,11 @@ export const useProxyNodesStore = defineStore('proxy-nodes', () => {
|
||||
|
||||
/** 在线节点(可用于代理选择) */
|
||||
const onlineNodes = computed(() =>
|
||||
nodes.value.filter(n => n.status === 'online')
|
||||
nodes.value.filter(n =>
|
||||
n.status === 'online'
|
||||
&& n.remote_config?.scheduling_state !== 'draining'
|
||||
&& n.remote_config?.scheduling_state !== 'cordoned'
|
||||
)
|
||||
)
|
||||
|
||||
async function fetchNodes(params?: { status?: string }) {
|
||||
@@ -23,9 +33,11 @@ export const useProxyNodesStore = defineStore('proxy-nodes', () => {
|
||||
try {
|
||||
const data = await proxyNodesApi.listProxyNodes({ ...params, limit: 1000 })
|
||||
nodes.value = data.items
|
||||
rollout.value = data.rollout
|
||||
total.value = data.total
|
||||
fetched.value = true
|
||||
} catch (err: unknown) {
|
||||
rollout.value = null
|
||||
error.value = parseApiError(err, '获取代理节点列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
@@ -74,6 +86,7 @@ export const useProxyNodesStore = defineStore('proxy-nodes', () => {
|
||||
|
||||
return {
|
||||
nodes,
|
||||
rollout,
|
||||
total,
|
||||
loading,
|
||||
error,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,7 @@
|
||||
id="section-proxy"
|
||||
:proxy-node-id="systemConfig.system_proxy_node_id"
|
||||
:online-nodes="proxyNodesStore.onlineNodes"
|
||||
:all-nodes="proxyNodesStore.nodes"
|
||||
:loading="proxyConfigLoading"
|
||||
:has-changes="hasProxyConfigChanges"
|
||||
@save="saveProxyConfig"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
直连(不使用代理)
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="node in onlineNodes"
|
||||
v-for="node in selectableNodes"
|
||||
:key="node.id"
|
||||
:value="node.id"
|
||||
>
|
||||
@@ -44,6 +44,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import Button from '@/components/ui/button.vue'
|
||||
import Label from '@/components/ui/label.vue'
|
||||
import Select from '@/components/ui/select.vue'
|
||||
@@ -61,9 +62,10 @@ interface ProxyNode {
|
||||
port: number
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
proxyNodeId: string | null
|
||||
onlineNodes: ProxyNode[]
|
||||
allNodes: ProxyNode[]
|
||||
loading: boolean
|
||||
hasChanges: boolean
|
||||
}>()
|
||||
@@ -72,4 +74,16 @@ defineEmits<{
|
||||
save: []
|
||||
'update:proxyNodeId': [value: string | null]
|
||||
}>()
|
||||
|
||||
const selectableNodes = computed(() => {
|
||||
if (!props.proxyNodeId) {
|
||||
return props.onlineNodes
|
||||
}
|
||||
const exists = props.onlineNodes.some(node => node.id === props.proxyNodeId)
|
||||
if (exists) {
|
||||
return props.onlineNodes
|
||||
}
|
||||
const selected = props.allNodes.find(node => node.id === props.proxyNodeId)
|
||||
return selected ? [selected, ...props.onlineNodes] : props.onlineNodes
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user