feat(provider): 支持 Provider 级别代理节点配置,修正配额分组匹配

- Provider 详情抽屉新增代理节点设置入口(Globe 图标),支持选择/清除代理
- 更新 Antigravity 配额分组:修正模型匹配规则以适配当前模型命名
- 简化上游元数据合并逻辑,以上游返回为准,不再保留已下架模型条目
This commit is contained in:
fawney19
2026-02-24 02:44:15 +08:00
parent 7e447fe54e
commit 837bed3d47
2 changed files with 106 additions and 15 deletions

View File

@@ -50,6 +50,52 @@
<Shuffle class="w-4 h-4" />
</Button>
</span>
<Popover
v-if="provider.provider_type !== 'custom'"
:open="providerProxyPopoverOpen"
@update:open="handleProviderProxyPopoverToggle"
>
<PopoverTrigger as-child>
<Button
variant="ghost"
size="icon"
:class="provider.proxy?.node_id ? 'text-blue-500' : ''"
:disabled="savingProviderProxy"
:title="provider.proxy?.node_id ? `代理: ${getProviderProxyNodeName()}` : '设置代理节点'"
>
<Globe class="w-4 h-4" />
</Button>
</PopoverTrigger>
<PopoverContent
class="w-72 p-3"
side="bottom"
align="end"
>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-xs font-medium">代理节点</span>
<Button
v-if="provider.proxy?.node_id"
variant="ghost"
size="sm"
class="h-6 px-2 text-[10px] text-muted-foreground"
:disabled="savingProviderProxy"
@click="clearProviderProxy"
>
清除
</Button>
</div>
<ProxyNodeSelect
:model-value="provider.proxy?.node_id || ''"
trigger-class="h-8"
@update:model-value="setProviderProxy"
/>
<p class="text-[10px] text-muted-foreground">
{{ provider.proxy?.node_id ? '当前使用独立代理' : '未设置代理节点' }}
</p>
</div>
</PopoverContent>
</Popover>
<Button
variant="ghost"
size="icon"
@@ -1145,8 +1191,12 @@ const refreshingQuota = ref(false)
const antigravityQuotaDialogOpen = ref(false)
const antigravityQuotaDialogKey = ref<EndpointAPIKey | null>(null)
// Key 级别代理配置状态
// Provider 级别代理配置状态
const proxyNodesStore = useProxyNodesStore()
const providerProxyPopoverOpen = ref(false)
const savingProviderProxy = ref(false)
// Key 级别代理配置状态
const savingProxyKeyId = ref<string | null>(null)
const proxyPopoverOpenKeyId = ref<string | null>(null)
@@ -1289,6 +1339,55 @@ async function toggleFormatConversion() {
}
}
// Provider 级别代理配置
function handleProviderProxyPopoverToggle(open: boolean) {
providerProxyPopoverOpen.value = open
if (open) {
proxyNodesStore.ensureLoaded()
}
}
function getProviderProxyNodeName(): string {
const nodeId = provider.value?.proxy?.node_id
if (!nodeId) return '未知节点'
const node = proxyNodesStore.nodes.find(n => n.id === nodeId)
return node ? node.name : `${nodeId.slice(0, 8)}...`
}
async function setProviderProxy(nodeId: string) {
if (!provider.value) return
savingProviderProxy.value = true
try {
const updated = await updateProvider(provider.value.id, {
proxy: { node_id: nodeId, enabled: true },
})
provider.value = updated
providerProxyPopoverOpen.value = false
showSuccess('代理节点已设置')
emit('refresh')
} catch (err: unknown) {
showError(parseApiError(err, '设置代理失败'))
} finally {
savingProviderProxy.value = false
}
}
async function clearProviderProxy() {
if (!provider.value) return
savingProviderProxy.value = true
try {
const updated = await updateProvider(provider.value.id, { proxy: null })
provider.value = updated
providerProxyPopoverOpen.value = false
showSuccess('已清除提供商代理')
emit('refresh')
} catch (err: unknown) {
showError(parseApiError(err, '清除代理失败'))
} finally {
savingProviderProxy.value = false
}
}
// 显示端点管理对话框
function showAddEndpointDialog() {
endpointDialogOpen.value = true
@@ -2269,10 +2368,10 @@ interface AntigravityQuotaGroup {
}
const ANTIGRAVITY_QUOTA_GROUPS: AntigravityQuotaGroup[] = [
{ key: 'claude-gpt', label: 'Claude 4.5', match: m => m.includes('claude') },
{ key: 'gemini-3-pro', label: 'Gemini 3 Pro', match: m => m.includes('gemini-3-pro') && !m.includes('image') },
{ key: 'gemini-3-flash', label: 'Gemini 3 Flash', match: m => m.includes('gemini-3-flash') },
{ key: 'gemini-3-pro-image', label: 'Gemini 3 Pro Image', match: m => m.includes('gemini-3-pro-image') },
{ key: 'claude', label: 'Claude', match: m => m.includes('claude') },
{ key: 'gemini-2.5', label: 'Gemini 2.5', match: m => m.includes('gemini-2.5') || m.includes('gemini-2-5') },
{ key: 'gemini-3', label: 'Gemini 3', match: m => m.includes('gemini-3') && !m.includes('image') },
{ key: 'gemini-3-image', label: 'Gemini 3 Image', match: m => m.includes('gemini-3') && m.includes('image') },
]
interface AntigravityQuotaSummaryItem {

View File

@@ -125,9 +125,8 @@ def merge_upstream_metadata(
) -> dict[str, Any]:
"""合并上游元数据,对 quota_by_model 做模型级深度合并。
上游 API 在配额耗尽后可能不再返回该模型的 quotaInfo因此需要
1. 保留旧数据中已有的 reset_time当新数据缺少时
2. 保留旧数据中存在但新数据中缺失的模型条目(标记为 100% 已用)
上游返回为准(全量替换),不再保留上游已下架的模型。
仅对上游返回的模型补充旧数据中的 reset_time当新数据缺少时
"""
merged: dict[str, Any] = dict(current) if isinstance(current, dict) else {}
for ns_key, ns_val in incoming.items():
@@ -152,13 +151,6 @@ def merge_upstream_metadata(
and "reset_time" not in new_info
):
new_info["reset_time"] = old_info["reset_time"]
# 保留新数据中缺失但旧数据中存在的模型(配额耗尽后上游可能不返回)
for model_id, old_info in old_qbm.items():
if model_id not in new_qbm and isinstance(old_info, dict):
exhausted = dict(old_info)
exhausted["remaining_fraction"] = 0.0
exhausted["used_percent"] = 100.0
new_qbm[model_id] = exhausted
merged[ns_key] = ns_val
return merged