Fix key API format auth help

This commit is contained in:
fawney19
2026-04-29 17:35:16 +08:00
parent e751289dfb
commit d6de917878
2 changed files with 87 additions and 107 deletions

View File

@@ -201,3 +201,8 @@ export function sortApiFormats(formats: string[]): string[] {
return aIdx - bIdx return aIdx - bIdx
}) })
} }
// openai family 格式只支持 bearerAuthorization header不允许覆盖认证方式
export function formatSupportsAuthOverride(format: string): boolean {
return parseApiFormat(normalizeApiFormatAlias(format)).family !== 'openai'
}

View File

@@ -83,35 +83,36 @@
<!-- API 格式 & 认证方式 --> <!-- API 格式 & 认证方式 -->
<div v-if="visibleApiFormats.length > 0"> <div v-if="visibleApiFormats.length > 0">
<Label class="mb-1.5 block">支持的 API 格式 *</Label> <div class="flex items-center gap-1 mb-1.5">
<!-- 默认认证方式单个格式可在下方覆盖 --> <Label>支持的 API 格式 *</Label>
<div <span
v-if="showAuthTypeSelector" class="relative inline-flex"
class="flex gap-2 mb-2" @mouseenter="apiFormatHelpHovered = true"
> @mouseleave="apiFormatHelpHovered = false"
<button
v-for="option in authTypeOptions"
:key="option.value"
type="button"
class="flex items-center gap-1.5 px-3 py-1.5 rounded-md border text-sm transition-colors"
:class="form.auth_type === option.value
? 'bg-primary/10 border-primary/50 text-primary'
: 'bg-muted/30 border-border text-muted-foreground hover:border-muted-foreground/40'"
@click="form.auth_type = option.value"
> >
<span <button
class="w-3.5 h-3.5 rounded-full border-2 flex items-center justify-center shrink-0" type="button"
:class="form.auth_type === option.value ? 'border-primary' : 'border-muted-foreground/40'" class="inline-flex items-center justify-center rounded-sm p-0.5 text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
title="API 格式说明"
aria-label="API 格式说明"
:aria-expanded="apiFormatHelpVisible"
@click.stop="toggleApiFormatHelp"
@focus="apiFormatHelpHovered = true"
@blur="apiFormatHelpHovered = false"
@keydown.escape.stop.prevent="apiFormatHelpOpen = false"
> >
<span <CircleHelp class="h-3.5 w-3.5" />
v-if="form.auth_type === option.value" </button>
class="w-1.5 h-1.5 rounded-full bg-primary" <span
/> v-if="apiFormatHelpVisible"
role="tooltip"
class="absolute left-0 top-full z-[100] mt-1 w-80 rounded-md border bg-popover px-3 py-2 text-xs font-normal normal-case leading-5 tracking-normal text-popover-foreground shadow-md"
>
选择此密钥支持的 API 格式及对应认证方式OpenAI 格式固定使用 Bearer TokenClaude / Gemini 格式可选 API Key Bearer Token Claude Code 应使用 Bearer Token
</span> </span>
{{ option.label }} </span>
</button>
</div> </div>
<div class="grid grid-cols-2 gap-2"> <div class="flex flex-col gap-1.5">
<div <div
v-for="format in visibleApiFormats" v-for="format in visibleApiFormats"
:key="format" :key="format"
@@ -130,46 +131,55 @@
> >
<span v-if="form.api_formats.includes(format)"></span> <span v-if="form.api_formats.includes(format)"></span>
</span> </span>
<div class="min-w-0"> <span
<span class="text-sm"
class="block text-sm truncate" :class="form.api_formats.includes(format) ? 'text-primary' : 'text-muted-foreground'"
:class="form.api_formats.includes(format) ? 'text-primary' : 'text-muted-foreground'" >{{ formatApiFormat(format) }}</span>
>{{ formatApiFormat(format) }}</span> </div>
<!-- 认证方式已勾选且可覆盖时显示 radio -->
<div class="flex items-center gap-3">
<div
v-if="canOverrideFormatAuth(format)"
class="flex gap-2"
@click.stop
>
<button <button
v-for="opt in authTypeOptions.filter(o => isRawSecretAuthType(o.value))"
:key="opt.value"
type="button" type="button"
class="mt-0.5 inline-flex max-w-full items-center rounded border px-1.5 py-0.5 text-[10px] leading-none transition-colors" class="flex items-center gap-1 text-[10px] leading-none transition-colors"
:class="[ :class="getFormatAuthType(format) === opt.value ? 'text-primary' : 'text-muted-foreground hover:text-foreground'"
form.api_formats.includes(format) @click="setFormatAuthType(format, opt.value as RawSecretAuthType)"
? 'border-primary/20 bg-primary/10 text-primary'
: 'border-border bg-background/60 text-muted-foreground',
canOverrideFormatAuth(format)
? 'cursor-pointer hover:border-primary/40 hover:bg-primary/15'
: 'cursor-default'
]"
:aria-disabled="!canOverrideFormatAuth(format)"
:title="formatAuthMethodTitle(format)"
@click="handleFormatAuthBadgeClick(format, $event)"
> >
{{ formatAuthMethodLabel(format) }} <span
class="w-2.5 h-2.5 rounded-full border flex items-center justify-center shrink-0"
:class="getFormatAuthType(format) === opt.value ? 'border-primary' : 'border-muted-foreground/40'"
>
<span
v-if="getFormatAuthType(format) === opt.value"
class="w-1 h-1 rounded-full bg-primary"
/>
</span>
{{ opt.label }}
</button> </button>
</div> </div>
</div> <div
<div class="flex items-center text-xs text-muted-foreground gap-1"
class="flex items-center shrink-0 ml-2 text-xs text-muted-foreground gap-1" @click.stop
@click.stop
>
<span>×</span>
<input
:value="form.rate_multipliers[format] ?? ''"
type="number"
step="0.01"
min="0.01"
placeholder="1"
class="w-9 bg-transparent text-right outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
:class="form.api_formats.includes(format) ? 'text-primary' : 'text-muted-foreground'"
title="成本倍率"
@input="(e) => updateRateMultiplier(format, (e.target as HTMLInputElement).value)"
> >
<span>×</span>
<input
:value="form.rate_multipliers[format] ?? ''"
type="number"
step="0.01"
min="0.01"
placeholder="1"
class="w-9 bg-transparent text-right outline-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
:class="form.api_formats.includes(format) ? 'text-primary' : 'text-muted-foreground'"
title="成本倍率"
@input="(e) => updateRateMultiplier(format, (e.target as HTMLInputElement).value)"
>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -338,7 +348,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { Dialog, Button, Input, Label, Switch } from '@/components/ui' import { Dialog, Button, Input, Label, Switch } from '@/components/ui'
import { Key, SquarePen } from 'lucide-vue-next' import { Key, SquarePen, CircleHelp } from 'lucide-vue-next'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import { useFormDialog } from '@/composables/useFormDialog' import { useFormDialog } from '@/composables/useFormDialog'
import { parseApiError } from '@/utils/errorParser' import { parseApiError } from '@/utils/errorParser'
@@ -356,7 +366,7 @@ import {
type CapabilityDefinition, type CapabilityDefinition,
type ProviderType type ProviderType
} from '@/api/endpoints' } from '@/api/endpoints'
import { formatApiFormat, normalizeApiFormatAlias } from '@/api/endpoints/types/api-format' import { formatApiFormat, normalizeApiFormatAlias, formatSupportsAuthOverride } from '@/api/endpoints/types/api-format'
type RawSecretAuthType = 'api_key' | 'bearer' type RawSecretAuthType = 'api_key' | 'bearer'
type ProviderKeyFormAuthType = RawSecretAuthType | 'service_account' type ProviderKeyFormAuthType = RawSecretAuthType | 'service_account'
@@ -428,19 +438,6 @@ function normalizeApiFormat(format: string): string {
return normalizeApiFormatAlias(format).trim().toLowerCase() return normalizeApiFormatAlias(format).trim().toLowerCase()
} }
function getAvailableApiFormatSet(): Set<string> {
return new Set(props.availableApiFormats.map(normalizeApiFormat))
}
function filterAvailableApiFormats(formats: string[]): string[] {
const availableFormatSet = getAvailableApiFormatSet()
if (availableFormatSet.size === 0) {
return []
}
return formats.filter(format => availableFormatSet.has(normalizeApiFormat(format)))
}
function getSelectableApiFormats(authType = form.value.auth_type): string[] { function getSelectableApiFormats(authType = form.value.auth_type): string[] {
const sorted = sortApiFormats(props.availableApiFormats) const sorted = sortApiFormats(props.availableApiFormats)
if (props.providerType !== 'vertex_ai') { if (props.providerType !== 'vertex_ai') {
@@ -499,7 +496,16 @@ const visibleApiFormats = computed(() => getSelectableApiFormats())
const authTypeOptions = computed(() => getAuthTypeOptions(props.providerType)) const authTypeOptions = computed(() => getAuthTypeOptions(props.providerType))
const showAuthTypeSelector = computed(() => authTypeOptions.value.length > 1) const apiFormatHelpOpen = ref(false)
const apiFormatHelpHovered = ref(false)
const apiFormatHelpVisible = computed(() => apiFormatHelpOpen.value || apiFormatHelpHovered.value)
function toggleApiFormatHelp() {
apiFormatHelpOpen.value = !apiFormatHelpOpen.value
if (!apiFormatHelpOpen.value) {
apiFormatHelpHovered.value = false
}
}
const authSecretLabel = computed(() => { const authSecretLabel = computed(() => {
if (form.value.auth_type === 'service_account') return 'Service Account JSON' if (form.value.auth_type === 'service_account') return 'Service Account JSON'
@@ -518,28 +524,7 @@ const authSecretRequiredMark = computed(() => {
return '' return ''
}) })
function formatAuthMethodLabel(format: string): string {
const authType = getFormatAuthType(format)
if (authType === 'service_account') return 'Service Account'
if (authType === 'bearer') return 'Authorization'
const { family } = normalizeApiFormat(format).includes(':')
? { family: normalizeApiFormat(format).split(':', 1)[0] }
: { family: normalizeApiFormat(format) }
if (family === 'claude') return 'x-api-key'
if (family === 'gemini') return 'x-goog-api-key'
if (family === 'openai') return 'Authorization'
return 'API Key'
}
function formatAuthMethodTitle(format: string): string {
const label = formatAuthMethodLabel(format)
if (!canOverrideFormatAuth(format)) {
return label
}
const nextAuthType = getFormatAuthType(format) === 'bearer' ? 'API Key' : 'Bearer Token'
return `${label},点击切换为 ${nextAuthType}`
}
function getFormatAuthType(format: string): ProviderKeyFormAuthType { function getFormatAuthType(format: string): ProviderKeyFormAuthType {
if (!isRawSecretAuthType(form.value.auth_type)) { if (!isRawSecretAuthType(form.value.auth_type)) {
@@ -549,7 +534,7 @@ function getFormatAuthType(format: string): ProviderKeyFormAuthType {
} }
function canOverrideFormatAuth(format: string): boolean { function canOverrideFormatAuth(format: string): boolean {
return isRawSecretAuthType(form.value.auth_type) && form.value.api_formats.includes(format) return isRawSecretAuthType(form.value.auth_type) && form.value.api_formats.includes(format) && formatSupportsAuthOverride(format)
} }
function setFormatAuthType(format: string, authType: RawSecretAuthType) { function setFormatAuthType(format: string, authType: RawSecretAuthType) {
@@ -564,16 +549,6 @@ function setFormatAuthType(format: string, authType: RawSecretAuthType) {
form.value.auth_type_by_format = sanitizeAuthTypeByFormat(next) form.value.auth_type_by_format = sanitizeAuthTypeByFormat(next)
} }
function toggleFormatAuthType(format: string) {
if (!canOverrideFormatAuth(format)) return
setFormatAuthType(format, getFormatAuthType(format) === 'bearer' ? 'api_key' : 'bearer')
}
function handleFormatAuthBadgeClick(format: string, event: MouseEvent) {
if (!canOverrideFormatAuth(format)) return
event.stopPropagation()
toggleFormatAuthType(format)
}
function buildAuthTypeByFormatPayload(): Record<string, RawSecretAuthType> | null { function buildAuthTypeByFormatPayload(): Record<string, RawSecretAuthType> | null {
const sanitized = sanitizeAuthTypeByFormat(form.value.auth_type_by_format) const sanitized = sanitizeAuthTypeByFormat(form.value.auth_type_by_format)