mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(proxy): 节点状态简化、连接事件记录、可靠性指标与批量删除
- 移除 UNHEALTHY 中间状态,节点状态简化为 ONLINE/OFFLINE - 新增 proxy_node_events 表记录 tunnel 连接/断开/错误事件 - 新增 failed_requests/dns_failures/stream_errors 可靠性指标(增量累加) - tunnel 重连改为固定 1s 延迟,移除指数退避逻辑 - resolver/service 改为以 TunnelManager 内存状态判断节点可用性,避免 DB 竞态 - 修正 Claude cache_control 字段格式,使用 ttl 字段控制缓存时长 - 移除前端手动勾选 capability 的 UI,改为从价格配置自动推断 - 新增全局模型批量删除 API,替换前端并行单个删除
This commit is contained in:
@@ -656,6 +656,7 @@ import {
|
||||
getGlobalModel,
|
||||
updateGlobalModel,
|
||||
deleteGlobalModel,
|
||||
batchDeleteGlobalModels,
|
||||
batchAssignToProviders,
|
||||
getGlobalModelProviders,
|
||||
type GlobalModelResponse,
|
||||
@@ -1276,15 +1277,13 @@ async function confirmBatchDeleteModels() {
|
||||
submittingBatchManage.value = true
|
||||
try {
|
||||
const ids = Array.from(selectedBatchManageModelIds.value)
|
||||
const results = await Promise.allSettled(ids.map(id => deleteGlobalModel(id)))
|
||||
const successCount = results.filter(r => r.status === 'fulfilled').length
|
||||
const failCount = results.filter(r => r.status === 'rejected').length
|
||||
const result = await batchDeleteGlobalModels(ids)
|
||||
|
||||
if (successCount > 0) {
|
||||
success(`成功删除 ${successCount} 个模型`)
|
||||
if (result.success_count > 0) {
|
||||
success(`成功删除 ${result.success_count} 个模型`)
|
||||
}
|
||||
if (failCount > 0) {
|
||||
showError(`${failCount} 个模型删除失败`, '部分失败')
|
||||
if (result.failed.length > 0) {
|
||||
showError(`${result.failed.length} 个模型删除失败`, '部分失败')
|
||||
}
|
||||
|
||||
// 清除选中的已删除模型
|
||||
|
||||
@@ -48,9 +48,6 @@
|
||||
<SelectItem value="online">
|
||||
在线
|
||||
</SelectItem>
|
||||
<SelectItem value="unhealthy">
|
||||
异常
|
||||
</SelectItem>
|
||||
<SelectItem value="offline">
|
||||
离线
|
||||
</SelectItem>
|
||||
@@ -86,9 +83,6 @@
|
||||
<SelectItem value="online">
|
||||
在线
|
||||
</SelectItem>
|
||||
<SelectItem value="unhealthy">
|
||||
异常
|
||||
</SelectItem>
|
||||
<SelectItem value="offline">
|
||||
离线
|
||||
</SelectItem>
|
||||
@@ -237,6 +231,16 @@
|
||||
>
|
||||
<Settings class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!node.is_manual"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
title="连接事件"
|
||||
@click="handleViewEvents(node)"
|
||||
>
|
||||
<History class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -548,6 +552,77 @@
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- 连接事件对话框 -->
|
||||
<Dialog
|
||||
:open="showEventsDialog"
|
||||
title="连接事件"
|
||||
:description="eventsNode ? `${eventsNode.name} 的连接历史` : ''"
|
||||
size="lg"
|
||||
@update:open="(v: boolean) => { if (!v) { showEventsDialog = false; eventsNode = null; nodeEvents = [] } }"
|
||||
>
|
||||
<div class="space-y-3">
|
||||
<!-- 可靠性指标摘要 -->
|
||||
<div
|
||||
v-if="eventsNode"
|
||||
class="grid grid-cols-3 gap-3 text-sm"
|
||||
>
|
||||
<div class="bg-muted/40 rounded-lg px-3 py-2 text-center">
|
||||
<span class="block text-foreground/60 text-xs">失败请求</span>
|
||||
<span class="tabular-nums font-medium">{{ formatNumber(eventsNode.failed_requests || 0) }}</span>
|
||||
</div>
|
||||
<div class="bg-muted/40 rounded-lg px-3 py-2 text-center">
|
||||
<span class="block text-foreground/60 text-xs">DNS 失败</span>
|
||||
<span class="tabular-nums font-medium">{{ formatNumber(eventsNode.dns_failures || 0) }}</span>
|
||||
</div>
|
||||
<div class="bg-muted/40 rounded-lg px-3 py-2 text-center">
|
||||
<span class="block text-foreground/60 text-xs">流错误</span>
|
||||
<span class="tabular-nums font-medium">{{ formatNumber(eventsNode.stream_errors || 0) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 事件列表 -->
|
||||
<div
|
||||
v-if="loadingEvents"
|
||||
class="py-8 text-center text-muted-foreground text-sm"
|
||||
>
|
||||
加载中...
|
||||
</div>
|
||||
<div
|
||||
v-else-if="nodeEvents.length === 0"
|
||||
class="py-8 text-center text-muted-foreground text-sm"
|
||||
>
|
||||
暂无连接事件记录
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="max-h-80 overflow-y-auto space-y-1.5"
|
||||
>
|
||||
<div
|
||||
v-for="event in nodeEvents"
|
||||
:key="event.id"
|
||||
class="flex items-center gap-2 px-3 py-2 rounded-lg bg-muted/30 text-sm"
|
||||
>
|
||||
<Badge
|
||||
:variant="eventTypeVariant(event.event_type)"
|
||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||
>
|
||||
{{ eventTypeLabel(event.event_type) }}
|
||||
</Badge>
|
||||
<span class="text-muted-foreground truncate flex-1">{{ event.detail || '-' }}</span>
|
||||
<span class="text-xs text-muted-foreground/70 tabular-nums shrink-0">{{ formatTime(event.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button
|
||||
variant="outline"
|
||||
@click="showEventsDialog = false; eventsNode = null; nodeEvents = []"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -556,7 +631,7 @@ import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { useConfirm } from '@/composables/useConfirm'
|
||||
import { proxyNodesApi, type ProxyNode, type ProxyNodeRemoteConfig } from '@/api/proxy-nodes'
|
||||
import { proxyNodesApi, type ProxyNode, type ProxyNodeRemoteConfig, type ProxyNodeEvent } from '@/api/proxy-nodes'
|
||||
|
||||
import {
|
||||
Card,
|
||||
@@ -580,7 +655,7 @@ import {
|
||||
Dialog,
|
||||
} from '@/components/ui'
|
||||
|
||||
import { Search, Trash2, Plus, SquarePen, Activity, Loader2, Settings } from 'lucide-vue-next'
|
||||
import { Search, Trash2, Plus, SquarePen, Activity, Loader2, Settings, History } from 'lucide-vue-next'
|
||||
import { parseApiError } from '@/utils/errorParser'
|
||||
import { formatRegion } from '@/utils/region'
|
||||
import HardwareTooltip from './components/HardwareTooltip.vue'
|
||||
@@ -616,6 +691,12 @@ const configForm = ref({
|
||||
heartbeat_interval: '30',
|
||||
})
|
||||
|
||||
// 连接事件对话框
|
||||
const showEventsDialog = ref(false)
|
||||
const eventsNode = ref<ProxyNode | null>(null)
|
||||
const nodeEvents = ref<ProxyNodeEvent[]>([])
|
||||
const loadingEvents = ref(false)
|
||||
|
||||
// 测试连通性
|
||||
const testingNodes = ref(new Set<string>())
|
||||
const testingUrl = ref(false)
|
||||
@@ -834,10 +915,41 @@ async function handleTest(node: ProxyNode) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleViewEvents(node: ProxyNode) {
|
||||
eventsNode.value = node
|
||||
showEventsDialog.value = true
|
||||
loadingEvents.value = true
|
||||
try {
|
||||
const res = await proxyNodesApi.listNodeEvents(node.id, 50)
|
||||
nodeEvents.value = res.items
|
||||
} catch (err: unknown) {
|
||||
toastError(parseApiError(err, '加载事件失败'))
|
||||
} finally {
|
||||
loadingEvents.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function eventTypeLabel(type: string) {
|
||||
switch (type) {
|
||||
case 'connected': return '连接'
|
||||
case 'disconnected': return '断开'
|
||||
case 'error': return '错误'
|
||||
default: return type
|
||||
}
|
||||
}
|
||||
|
||||
function eventTypeVariant(type: string) {
|
||||
switch (type) {
|
||||
case 'connected': return 'success' as const
|
||||
case 'disconnected': return 'destructive' as const
|
||||
case 'error': return 'destructive' as const
|
||||
default: return 'secondary' as const
|
||||
}
|
||||
}
|
||||
|
||||
function statusVariant(status: string) {
|
||||
switch (status) {
|
||||
case 'online': return 'success' as const
|
||||
case 'unhealthy': return 'secondary' as const
|
||||
case 'offline': return 'destructive' as const
|
||||
default: return 'secondary' as const
|
||||
}
|
||||
@@ -846,7 +958,6 @@ function statusVariant(status: string) {
|
||||
function statusLabel(status: string) {
|
||||
switch (status) {
|
||||
case 'online': return '在线'
|
||||
case 'unhealthy': return '异常'
|
||||
case 'offline': return '离线'
|
||||
default: return status
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
v-if="getFirst1hCachePrice(model.default_tiered_pricing) !== '-'"
|
||||
class="flex items-center gap-3 p-3 rounded-lg border bg-muted/20"
|
||||
>
|
||||
<Label class="text-xs text-muted-foreground whitespace-nowrap">1h 缓存创建</Label>
|
||||
<Label class="text-xs text-muted-foreground whitespace-nowrap">1h 缓存</Label>
|
||||
<span class="text-sm font-mono">{{ getFirst1hCachePrice(model.default_tiered_pricing) }}</span>
|
||||
</div>
|
||||
<!-- 按次计费 -->
|
||||
|
||||
Reference in New Issue
Block a user