feat(failover): 支持 Provider 级别故障转移规则,默认全部转移策略

- 新增 failover_rules 配置:支持 success_failover_patterns(成功响应匹配时转移)
  和 error_stop_patterns(错误响应匹配时终止),支持按 status_code 过滤
- 修改默认转移策略:ErrorClassifier 不再返回 RAISE,所有错误默认继续转移
- TaskService 中客户端错误不再直接抛出,改为 break 继续尝试下一个候选
- 修复 proxy tunnel 连接/断连竞态:引入 per-node 锁和事件时间戳排序
- 优化 ProxyNode 状态判定:OFFLINE 统一由心跳超时判定,兼容多 worker 场景
- has_tunnel 改为纯检查方法,避免在 finally 块中误清理新注册连接
- Redis stream NOGROUP 异常自愈处理
- OAuthAccountDialog 输入框焦点样式补全
This commit is contained in:
fawney19
2026-03-01 00:21:16 +08:00
parent fbcb54a8a5
commit 005cc3e388
18 changed files with 941 additions and 116 deletions
@@ -50,6 +50,16 @@
<Shuffle class="w-4 h-4" />
</Button>
</span>
<span :title="hasFailoverRules ? '已配置故障转移规则(点击编辑)' : '配置故障转移规则'">
<Button
variant="ghost"
size="icon"
:class="hasFailoverRules ? 'text-orange-500 dark:text-orange-400' : ''"
@click="failoverRulesDialogOpen = true"
>
<GitBranch class="w-4 h-4" />
</Button>
</span>
<Popover
:open="providerProxyPopoverOpen"
@update:open="handleProviderProxyPopoverToggle"
@@ -1029,6 +1039,14 @@
:key-id="antigravityQuotaDialogKey.id"
@update:open="antigravityQuotaDialogOpen = $event"
/>
<!-- 故障转移规则弹窗 -->
<FailoverRulesDialog
:open="failoverRulesDialogOpen"
:provider="provider ?? null"
@update:open="failoverRulesDialogOpen = $event"
@saved="loadProvider()"
/>
</template>
<script setup lang="ts">
@@ -1050,6 +1068,7 @@ import {
BarChart3,
ShieldX,
Globe,
GitBranch,
} from 'lucide-vue-next'
import { parseApiError } from '@/utils/errorParser'
import { useEscapeKey } from '@/composables/useEscapeKey'
@@ -1084,6 +1103,7 @@ import EndpointFormDialog from '@/features/providers/components/EndpointFormDial
import ProviderModelFormDialog from '@/features/providers/components/ProviderModelFormDialog.vue'
import AlertDialog from '@/components/common/AlertDialog.vue'
import AntigravityQuotaDialog from '@/features/providers/components/AntigravityQuotaDialog.vue'
import FailoverRulesDialog from '@/features/providers/components/FailoverRulesDialog.vue'
import ProxyNodeSelect from '@/features/providers/components/ProxyNodeSelect.vue'
import { useProxyNodesStore } from '@/stores/proxy-nodes'
import {
@@ -1189,6 +1209,15 @@ const refreshingQuota = ref(false)
const antigravityQuotaDialogOpen = ref(false)
const antigravityQuotaDialogKey = ref<EndpointAPIKey | null>(null)
// 故障转移规则
const failoverRulesDialogOpen = ref(false)
const hasFailoverRules = computed(() => {
const rules = provider.value?.failover_rules
if (!rules) return false
return (rules.success_failover_patterns?.length || 0) > 0
|| (rules.error_stop_patterns?.length || 0) > 0
})
// Provider 级别代理配置状态
const proxyNodesStore = useProxyNodesStore()
const providerProxyPopoverOpen = ref(false)