feat(frontend): integrate kiro account management UI
This commit is contained in:
@@ -12,6 +12,11 @@
|
||||
<span class="text-[11px] text-gray-400 dark:text-gray-500">{{ overloadCountdown }}</span>
|
||||
</div>
|
||||
|
||||
<div v-else-if="kiroQuotaBadgeLabel" class="flex flex-col items-center gap-1">
|
||||
<span :class="['badge text-xs', kiroQuotaBadgeClass]">{{ kiroQuotaBadgeLabel }}</span>
|
||||
<span v-if="kiroQuotaHint" class="text-[11px] text-gray-400 dark:text-gray-500">{{ kiroQuotaHint }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Main Status Badge (shown when not rate limited/overloaded) -->
|
||||
<template v-else>
|
||||
<button
|
||||
@@ -69,7 +74,7 @@
|
||||
<div
|
||||
class="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 w-56 -translate-x-1/2 whitespace-normal rounded bg-gray-900 px-3 py-2 text-center text-xs leading-relaxed text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||
>
|
||||
{{ t('admin.accounts.status.rateLimitedUntil', { time: formatDateTime(account.rate_limit_reset_at) }) }}
|
||||
{{ t('admin.accounts.status.rateLimitedUntil', { time: formatDateTime(activeKiroRuntimeResetAt || account.rate_limit_reset_at) }) }}
|
||||
<div
|
||||
class="absolute left-1/2 top-full -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"
|
||||
></div>
|
||||
@@ -172,11 +177,37 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
// Computed: is rate limited (429)
|
||||
const activeKiroRuntimeResetAt = computed(() => {
|
||||
if (props.account.platform !== 'kiro') return null
|
||||
if (props.account.kiro_runtime_state !== 'cooldown') return null
|
||||
if (!props.account.kiro_runtime_reset_at) return null
|
||||
const resetAt = new Date(props.account.kiro_runtime_reset_at)
|
||||
if (Number.isNaN(resetAt.getTime()) || resetAt <= new Date()) return null
|
||||
return props.account.kiro_runtime_reset_at
|
||||
})
|
||||
|
||||
const isRateLimited = computed(() => {
|
||||
if (activeKiroRuntimeResetAt.value) return true
|
||||
if (!props.account.rate_limit_reset_at) return false
|
||||
return new Date(props.account.rate_limit_reset_at) > new Date()
|
||||
})
|
||||
|
||||
const isKiroRuntimeSuspended = computed(() => {
|
||||
if (props.account.platform !== 'kiro') return false
|
||||
if (props.account.kiro_runtime_state !== 'suspended') return false
|
||||
if (!props.account.kiro_runtime_reset_at) return true
|
||||
const resetAt = new Date(props.account.kiro_runtime_reset_at)
|
||||
return Number.isNaN(resetAt.getTime()) || resetAt > new Date()
|
||||
})
|
||||
|
||||
const activeKiroQuotaResetAt = computed(() => {
|
||||
if (props.account.platform !== 'kiro') return null
|
||||
if (!props.account.kiro_quota_reset_at) return null
|
||||
const resetAt = new Date(props.account.kiro_quota_reset_at)
|
||||
if (Number.isNaN(resetAt.getTime()) || resetAt <= new Date()) return null
|
||||
return props.account.kiro_quota_reset_at
|
||||
})
|
||||
|
||||
type AccountModelStatusItem = {
|
||||
kind: 'rate_limit' | 'credits_exhausted' | 'credits_active'
|
||||
model: string
|
||||
@@ -281,7 +312,7 @@ const isTempUnschedulable = computed(() => {
|
||||
|
||||
// Computed: has error status
|
||||
const hasError = computed(() => {
|
||||
return props.account.status === 'error'
|
||||
return props.account.status === 'error' || isKiroRuntimeSuspended.value
|
||||
})
|
||||
|
||||
const isQuotaExceeded = computed(() => {
|
||||
@@ -296,7 +327,7 @@ const isQuotaExceeded = computed(() => {
|
||||
|
||||
// Computed: countdown text for rate limit (429)
|
||||
const rateLimitCountdown = computed(() => {
|
||||
return formatCountdown(props.account.rate_limit_reset_at)
|
||||
return formatCountdown(activeKiroRuntimeResetAt.value || props.account.rate_limit_reset_at)
|
||||
})
|
||||
|
||||
const rateLimitResumeText = computed(() => {
|
||||
@@ -309,8 +340,45 @@ const overloadCountdown = computed(() => {
|
||||
return formatCountdownWithSuffix(props.account.overload_until)
|
||||
})
|
||||
|
||||
const kiroQuotaBadgeLabel = computed(() => {
|
||||
if (props.account.platform !== 'kiro') return ''
|
||||
switch (props.account.kiro_quota_state) {
|
||||
case 'credits_exhausted':
|
||||
return t('admin.accounts.status.creditsExhausted')
|
||||
case 'overage_exhausted':
|
||||
return t('admin.accounts.status.overageExhausted')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const kiroQuotaBadgeClass = computed(() => {
|
||||
switch (props.account.kiro_quota_state) {
|
||||
case 'credits_exhausted':
|
||||
case 'overage_exhausted':
|
||||
return 'badge-danger'
|
||||
default:
|
||||
return 'badge-gray'
|
||||
}
|
||||
})
|
||||
|
||||
const kiroQuotaHint = computed(() => {
|
||||
if (!activeKiroQuotaResetAt.value) return ''
|
||||
switch (props.account.kiro_quota_state) {
|
||||
case 'credits_exhausted':
|
||||
return t('admin.accounts.status.creditsExhaustedUntil', { time: formatDateTime(activeKiroQuotaResetAt.value) })
|
||||
case 'overage_exhausted':
|
||||
return t('admin.accounts.status.overageExhaustedUntil', { time: formatDateTime(activeKiroQuotaResetAt.value) })
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
// Computed: status badge class
|
||||
const statusClass = computed(() => {
|
||||
if (isKiroRuntimeSuspended.value) {
|
||||
return 'badge-danger'
|
||||
}
|
||||
if (hasError.value) {
|
||||
return 'badge-danger'
|
||||
}
|
||||
@@ -331,6 +399,9 @@ const statusClass = computed(() => {
|
||||
|
||||
// Computed: status text
|
||||
const statusText = computed(() => {
|
||||
if (isKiroRuntimeSuspended.value) {
|
||||
return t('admin.accounts.forbidden')
|
||||
}
|
||||
if (hasError.value) {
|
||||
return t('admin.accounts.status.error')
|
||||
}
|
||||
|
||||
@@ -395,6 +395,72 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Kiro platform: show credits + bonus + overage summary -->
|
||||
<template v-else-if="account.platform === 'kiro' && account.type === 'oauth'">
|
||||
<div v-if="loading" class="space-y-1.5">
|
||||
<div class="h-4 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="space-y-1">
|
||||
<div class="h-1.5 w-32 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
||||
<div class="h-1.5 w-28 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="error" class="text-xs text-red-500">
|
||||
{{ error }}
|
||||
</div>
|
||||
<div v-else-if="kiroUsageAvailable || kiroStatusBadgeLabel" class="space-y-2">
|
||||
<div v-if="kiroStatusBadgeLabel" class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span
|
||||
:class="[
|
||||
'inline-flex items-center gap-1 text-[10px] font-medium',
|
||||
kiroStatusToneClass
|
||||
]"
|
||||
:title="usageInfo?.error || undefined"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-current opacity-80"></span>
|
||||
{{ kiroStatusBadgeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="kiroStatusHint" class="text-[9px] leading-tight text-gray-500 dark:text-gray-400">
|
||||
{{ kiroStatusHint }}
|
||||
</div>
|
||||
|
||||
<div v-if="usageInfo?.kiro_credit" class="space-y-1">
|
||||
<div class="flex items-baseline justify-between gap-2 text-[11px] text-gray-600 dark:text-gray-300">
|
||||
<span class="font-medium tracking-[0.01em]">{{ t('admin.accounts.usageWindow.kiroCredits') }}</span>
|
||||
<span class="font-semibold tabular-nums text-gray-700 dark:text-gray-200">{{ formatKiroAmount(usageInfo.kiro_credit.current_usage) }} / {{ formatKiroAmount(usageInfo.kiro_credit.usage_limit) }}</span>
|
||||
</div>
|
||||
<div class="h-1.5 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div class="h-full rounded-full bg-amber-500 transition-all" :style="{ width: `${kiroCreditPercent}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="usageInfo?.kiro_bonus" class="space-y-1">
|
||||
<div class="flex items-baseline justify-between gap-2 text-[11px] text-gray-600 dark:text-gray-300">
|
||||
<span class="font-medium tracking-[0.01em]">{{ t('admin.accounts.usageWindow.kiroBonus') }}</span>
|
||||
<span class="font-semibold tabular-nums text-gray-700 dark:text-gray-200">{{ formatKiroAmount(usageInfo.kiro_bonus.current_usage) }} / {{ formatKiroAmount(usageInfo.kiro_bonus.usage_limit) }}</span>
|
||||
</div>
|
||||
<div class="h-1.5 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div class="h-full rounded-full bg-emerald-500 transition-all" :style="{ width: `${kiroBonusPercent}%` }"></div>
|
||||
</div>
|
||||
<div v-if="kiroBonusMeta" class="text-[9px] leading-tight text-gray-500 dark:text-gray-400">
|
||||
{{ kiroBonusMeta }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-[10px] text-gray-500 dark:text-gray-400">
|
||||
<span v-if="kiroResetDisplay" class="inline-flex items-center gap-1">
|
||||
<span class="text-gray-400 dark:text-gray-500">{{ t('admin.accounts.usageWindow.kiroReset') }}</span>
|
||||
<span class="font-medium tabular-nums text-gray-600 dark:text-gray-300">{{ kiroResetDisplay }}</span>
|
||||
</span>
|
||||
<span v-if="kiroOverageSummary" class="inline-flex items-center gap-1 font-medium">
|
||||
{{ kiroOverageSummary }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-400">-</div>
|
||||
</template>
|
||||
|
||||
<!-- Other accounts: no usage window -->
|
||||
<template v-else>
|
||||
<div class="text-xs text-gray-400">-</div>
|
||||
@@ -498,6 +564,10 @@ const props = withDefaults(
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
kiroUsageMeta: [meta: { plan_type?: string; kiro_overages_enabled: boolean }]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const desktopViewportQuery = '(min-width: 768px)'
|
||||
|
||||
@@ -510,7 +580,9 @@ const error = ref<string | null>(null)
|
||||
const usageInfo = ref<AccountUsageInfo | null>(null)
|
||||
const rootRef = ref<HTMLElement | null>(null)
|
||||
const isDesktopViewport = ref(
|
||||
typeof window === 'undefined' ? true : window.matchMedia(desktopViewportQuery).matches
|
||||
typeof window === 'undefined' || typeof window.matchMedia !== 'function'
|
||||
? true
|
||||
: window.matchMedia(desktopViewportQuery).matches
|
||||
)
|
||||
const hasEnteredViewport = ref(false)
|
||||
const pendingAutoLoad = ref(false)
|
||||
@@ -531,6 +603,9 @@ const shouldFetchUsage = computed(() => {
|
||||
if (props.account.platform === 'anthropic') {
|
||||
return props.account.type === 'oauth' || props.account.type === 'setup-token'
|
||||
}
|
||||
if (props.account.platform === 'kiro') {
|
||||
return props.account.type === 'oauth'
|
||||
}
|
||||
if (props.account.platform === 'gemini') {
|
||||
return true
|
||||
}
|
||||
@@ -984,6 +1059,179 @@ const isAnthropicOAuthOrSetupToken = computed(() => {
|
||||
return props.account.platform === 'anthropic' && (props.account.type === 'oauth' || props.account.type === 'setup-token')
|
||||
})
|
||||
|
||||
const isKiroOAuth = computed(() => {
|
||||
return props.account.platform === 'kiro' && props.account.type === 'oauth'
|
||||
})
|
||||
|
||||
const defaultUsageSource = computed<'passive' | 'active' | undefined>(() => {
|
||||
if (isAnthropicOAuthOrSetupToken.value || isKiroOAuth.value) {
|
||||
return 'passive'
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const manualRefreshUsageSource = computed<'passive' | 'active' | undefined>(() => {
|
||||
if (isKiroOAuth.value) {
|
||||
return 'active'
|
||||
}
|
||||
return defaultUsageSource.value
|
||||
})
|
||||
|
||||
const kiroUsageAvailable = computed(() => {
|
||||
return !!(
|
||||
usageInfo.value?.kiro_credit ||
|
||||
usageInfo.value?.kiro_bonus ||
|
||||
usageInfo.value?.kiro_overage ||
|
||||
usageInfo.value?.kiro_reset_at ||
|
||||
usageInfo.value?.kiro_overages_enabled
|
||||
)
|
||||
})
|
||||
|
||||
const syncKiroUsageMeta = (info?: AccountUsageInfo | null) => {
|
||||
if (!isKiroOAuth.value) return
|
||||
|
||||
const planType = (
|
||||
info?.kiro_subscription_name ||
|
||||
info?.kiro_subscription_type ||
|
||||
''
|
||||
).trim()
|
||||
|
||||
emit('kiroUsageMeta', {
|
||||
...(planType ? { plan_type: planType } : {}),
|
||||
kiro_overages_enabled: info?.kiro_overages_enabled === true
|
||||
})
|
||||
}
|
||||
|
||||
const clampPercent = (value?: number | null) => {
|
||||
if (value == null || !Number.isFinite(value)) return 0
|
||||
return Math.max(0, Math.min(100, value))
|
||||
}
|
||||
|
||||
const kiroCreditPercent = computed(() => clampPercent(usageInfo.value?.kiro_credit?.percentage_used))
|
||||
const kiroBonusPercent = computed(() => clampPercent(usageInfo.value?.kiro_bonus?.percentage_used))
|
||||
|
||||
const formatKiroAmount = (value?: number | null) => {
|
||||
if (value == null || !Number.isFinite(value)) return '0'
|
||||
if (Math.abs(value) >= 1000 || Number.isInteger(value)) {
|
||||
return formatCompactNumber(value, { allowBillions: false })
|
||||
}
|
||||
return value.toFixed(2).replace(/\.?0+$/, '')
|
||||
}
|
||||
|
||||
const kiroResetDisplay = computed(() => {
|
||||
const raw = usageInfo.value?.kiro_reset_at
|
||||
if (!raw) return ''
|
||||
const parsed = new Date(raw)
|
||||
if (Number.isNaN(parsed.getTime())) return ''
|
||||
return parsed.toLocaleDateString()
|
||||
})
|
||||
|
||||
const kiroBonusMeta = computed(() => {
|
||||
const bonus = usageInfo.value?.kiro_bonus
|
||||
if (!bonus) return ''
|
||||
if ((bonus.days_remaining ?? 0) > 0) {
|
||||
return t('admin.accounts.usageWindow.kiroDaysLeft', { days: bonus.days_remaining })
|
||||
}
|
||||
if (bonus.expiry_date) {
|
||||
const parsed = new Date(bonus.expiry_date)
|
||||
if (!Number.isNaN(parsed.getTime())) {
|
||||
return `${t('admin.accounts.usageWindow.kiroExpires')} ${parsed.toLocaleDateString()}`
|
||||
}
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const kiroRuntimeResetDisplay = computed(() => {
|
||||
const raw = usageInfo.value?.kiro_runtime_reset_at
|
||||
if (!raw) return ''
|
||||
const parsed = new Date(raw)
|
||||
if (Number.isNaN(parsed.getTime())) return ''
|
||||
return parsed.toLocaleString()
|
||||
})
|
||||
|
||||
const kiroQuotaResetDisplay = computed(() => {
|
||||
const raw = usageInfo.value?.kiro_quota_reset_at
|
||||
if (!raw) return ''
|
||||
const parsed = new Date(raw)
|
||||
if (Number.isNaN(parsed.getTime())) return ''
|
||||
return parsed.toLocaleString()
|
||||
})
|
||||
|
||||
const isKiroProfileError = computed(() => {
|
||||
if (!isKiroOAuth.value) return false
|
||||
const err = (usageInfo.value?.error || '').toLowerCase()
|
||||
return err.includes('profilearn is required') ||
|
||||
(err.includes('profile arn') && err.includes('required')) ||
|
||||
err.includes('profilearn') ||
|
||||
err.includes('listavailableprofiles')
|
||||
})
|
||||
|
||||
const isKiroUsageForbidden = computed(() => {
|
||||
if (!isKiroOAuth.value) return false
|
||||
return usageInfo.value?.error_code === 'forbidden' && !usageInfo.value?.needs_reauth && !isKiroProfileError.value
|
||||
})
|
||||
|
||||
const kiroQuotaState = computed(() => usageInfo.value?.kiro_quota_state || '')
|
||||
|
||||
const kiroStatusBadgeLabel = computed(() => {
|
||||
const runtimeState = usageInfo.value?.kiro_runtime_state
|
||||
if (runtimeState === 'suspended') return t('admin.accounts.forbidden')
|
||||
if (runtimeState === 'cooldown') return t('admin.accounts.status.rateLimited')
|
||||
if (usageInfo.value?.needs_reauth) return t('admin.accounts.needsReauth')
|
||||
if (isKiroProfileError.value) return t('admin.accounts.usageError')
|
||||
if (isKiroUsageForbidden.value) return t('admin.accounts.forbidden')
|
||||
if (kiroQuotaState.value === 'overage_active') return t('admin.accounts.status.overageActive')
|
||||
if (kiroQuotaState.value === 'credits_exhausted') return t('admin.accounts.status.creditsExhausted')
|
||||
if (kiroQuotaState.value === 'overage_exhausted') return t('admin.accounts.status.overageExhausted')
|
||||
return ''
|
||||
})
|
||||
|
||||
const kiroStatusToneClass = computed(() => {
|
||||
const runtimeState = usageInfo.value?.kiro_runtime_state
|
||||
if (runtimeState === 'suspended') return 'text-red-700 dark:text-red-300'
|
||||
if (runtimeState === 'cooldown') return 'text-amber-700 dark:text-amber-300'
|
||||
if (usageInfo.value?.needs_reauth) return 'text-orange-700 dark:text-orange-300'
|
||||
if (isKiroProfileError.value) return 'text-yellow-700 dark:text-yellow-300'
|
||||
if (isKiroUsageForbidden.value) return 'text-rose-700 dark:text-rose-300'
|
||||
if (kiroQuotaState.value === 'overage_active') return 'text-amber-700 dark:text-amber-300'
|
||||
if (kiroQuotaState.value === 'credits_exhausted' || kiroQuotaState.value === 'overage_exhausted') {
|
||||
return 'text-red-700 dark:text-red-300'
|
||||
}
|
||||
return 'text-gray-600 dark:text-gray-300'
|
||||
})
|
||||
|
||||
const kiroStatusHint = computed(() => {
|
||||
const runtimeState = usageInfo.value?.kiro_runtime_state
|
||||
if (runtimeState === 'cooldown' && kiroRuntimeResetDisplay.value) {
|
||||
return t('admin.accounts.status.rateLimitedUntil', { time: kiroRuntimeResetDisplay.value })
|
||||
}
|
||||
if (kiroQuotaState.value === 'credits_exhausted' && kiroQuotaResetDisplay.value) {
|
||||
return t('admin.accounts.status.creditsExhaustedUntil', { time: kiroQuotaResetDisplay.value })
|
||||
}
|
||||
if (kiroQuotaState.value === 'overage_exhausted' && kiroQuotaResetDisplay.value) {
|
||||
return t('admin.accounts.status.overageExhaustedUntil', { time: kiroQuotaResetDisplay.value })
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const kiroOverageSummary = computed(() => {
|
||||
const overage = usageInfo.value?.kiro_overage
|
||||
if (!overage) return ''
|
||||
const hasOverageCount = (overage.current_overages ?? 0) > 0
|
||||
const hasCharges = (overage.overage_charges ?? 0) > 0
|
||||
if (!hasOverageCount && !hasCharges) return ''
|
||||
|
||||
const parts: string[] = [t('admin.accounts.usageWindow.kiroOverage')]
|
||||
if (hasOverageCount) {
|
||||
parts.push(formatKiroAmount(overage.current_overages))
|
||||
}
|
||||
if (hasCharges) {
|
||||
const symbol = overage.currency_symbol || overage.currency_code || ''
|
||||
parts.push(`(${symbol}${(overage.overage_charges ?? 0).toFixed(2)})`)
|
||||
}
|
||||
return parts.join(' ')
|
||||
})
|
||||
|
||||
const loadUsage = async (options?: { source?: 'passive' | 'active'; bypassCache?: boolean }) => {
|
||||
if (!shouldFetchUsage.value) return
|
||||
|
||||
@@ -992,6 +1240,7 @@ const loadUsage = async (options?: { source?: 'passive' | 'active'; bypassCache?
|
||||
const cached = _usageCache.get(props.account.id)
|
||||
if (cached && Date.now() - cached.ts < USAGE_CACHE_TTL) {
|
||||
usageInfo.value = cached.data
|
||||
syncKiroUsageMeta(cached.data)
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
@@ -1001,10 +1250,13 @@ const loadUsage = async (options?: { source?: 'passive' | 'active'; bypassCache?
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const fetchFn = () => adminAPI.accounts.getUsage(props.account.id, options?.source)
|
||||
const fetchFn = () => options?.source
|
||||
? adminAPI.accounts.getUsage(props.account.id, options.source)
|
||||
: adminAPI.accounts.getUsage(props.account.id)
|
||||
const result = await enqueueUsageRequest(props.account, fetchFn)
|
||||
if (!unmounted.value) {
|
||||
usageInfo.value = result
|
||||
syncKiroUsageMeta(result)
|
||||
_usageCache.set(props.account.id, { data: result, ts: Date.now() })
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -1070,7 +1322,10 @@ const attachVisibilityObserver = () => {
|
||||
const loadActiveUsage = async () => {
|
||||
activeQueryLoading.value = true
|
||||
try {
|
||||
usageInfo.value = await adminAPI.accounts.getUsage(props.account.id, 'active')
|
||||
const result = await adminAPI.accounts.getUsage(props.account.id, 'active')
|
||||
usageInfo.value = result
|
||||
syncKiroUsageMeta(result)
|
||||
_usageCache.set(props.account.id, { data: result, ts: Date.now() })
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load active usage:', e)
|
||||
} finally {
|
||||
@@ -1166,7 +1421,7 @@ const formatKeyUserCost = computed(() => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
|
||||
desktopViewportMediaQuery = window.matchMedia(desktopViewportQuery)
|
||||
isDesktopViewport.value = desktopViewportMediaQuery.matches
|
||||
desktopViewportListener = (event: MediaQueryListEvent) => {
|
||||
@@ -1180,15 +1435,17 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
if (!shouldAutoLoadUsageOnMount.value) return
|
||||
const source = isAnthropicOAuthOrSetupToken.value ? 'passive' : undefined
|
||||
requestAutoLoad(source)
|
||||
requestAutoLoad(defaultUsageSource.value)
|
||||
})
|
||||
|
||||
watch(openAIUsageRefreshKey, (nextKey, prevKey) => {
|
||||
if (!prevKey || nextKey === prevKey) return
|
||||
if (props.account.platform !== 'openai' || props.account.type !== 'oauth') return
|
||||
|
||||
requestAutoLoad()
|
||||
_usageCache.delete(props.account.id)
|
||||
loadUsage({ bypassCache: true }).catch((e) => {
|
||||
console.error('Failed to reload OpenAI usage after row refresh:', e)
|
||||
})
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -1197,9 +1454,8 @@ watch(
|
||||
if (nextToken === prevToken) return
|
||||
if (!shouldFetchUsage.value) return
|
||||
|
||||
const source = isAnthropicOAuthOrSetupToken.value ? 'passive' : undefined
|
||||
_usageCache.delete(props.account.id)
|
||||
loadUsage({ source, bypassCache: true }).catch((e) => {
|
||||
loadUsage({ source: manualRefreshUsageSource.value, bypassCache: true }).catch((e) => {
|
||||
console.error('Failed to refresh usage after manual refresh:', e)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -147,6 +147,19 @@
|
||||
<Icon name="cloud" size="sm" />
|
||||
Antigravity
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="form.platform = 'kiro'"
|
||||
:class="[
|
||||
'flex flex-1 items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all',
|
||||
form.platform === 'kiro'
|
||||
? 'bg-white text-amber-700 shadow-sm dark:bg-dark-600 dark:text-amber-300'
|
||||
: 'text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-200'
|
||||
]"
|
||||
>
|
||||
<Icon name="sparkles" size="sm" />
|
||||
Kiro
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -774,6 +787,457 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kiro account type selection -->
|
||||
<div v-if="form.platform === 'kiro'">
|
||||
<label class="input-label">{{ t('admin.accounts.accountType') }}</label>
|
||||
<div class="mt-2 grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="accountCategory = 'oauth-based'"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
accountCategory === 'oauth-based'
|
||||
? 'border-amber-500 bg-amber-50 dark:bg-amber-900/20'
|
||||
: 'border-gray-200 hover:border-amber-300 dark:border-dark-600 dark:hover:border-amber-700'
|
||||
]"
|
||||
>
|
||||
<div :class="['flex h-8 w-8 shrink-0 items-center justify-center rounded-lg', accountCategory === 'oauth-based' ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400']">
|
||||
<Icon name="key" size="sm" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.accounts.types.oauth') }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.types.kiroOauth') }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="accountCategory = 'apikey'"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
accountCategory === 'apikey'
|
||||
? 'border-purple-500 bg-purple-50 dark:bg-purple-900/20'
|
||||
: 'border-gray-200 hover:border-purple-300 dark:border-dark-600 dark:hover:border-purple-700'
|
||||
]"
|
||||
>
|
||||
<div :class="['flex h-8 w-8 shrink-0 items-center justify-center rounded-lg', accountCategory === 'apikey' ? 'bg-purple-500 text-white' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400']">
|
||||
<Icon name="cloud" size="sm" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
API Key
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.types.kiroApikey') }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kiro OAuth auth mode selection -->
|
||||
<div v-if="form.platform === 'kiro' && accountCategory === 'oauth-based'">
|
||||
<label class="input-label">{{ t('admin.accounts.oauth.kiro.authModeTitle') }}</label>
|
||||
<div class="mt-2 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="kiroAccountType = 'oauth'"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
kiroAccountType === 'oauth'
|
||||
? 'border-amber-500 bg-amber-50 dark:bg-amber-900/20'
|
||||
: 'border-gray-200 hover:border-amber-300 dark:border-dark-600 dark:hover:border-amber-700'
|
||||
]"
|
||||
>
|
||||
<div :class="['flex h-8 w-8 shrink-0 items-center justify-center rounded-lg', kiroAccountType === 'oauth' ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400']">
|
||||
<Icon name="key" size="sm" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.accounts.oauth.kiro.oauthTitle') }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.oauth.kiro.oauthSubtitle') }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="kiroAccountType = 'idc'"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
kiroAccountType === 'idc'
|
||||
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||
: 'border-gray-200 hover:border-blue-300 dark:border-dark-600 dark:hover:border-blue-700'
|
||||
]"
|
||||
>
|
||||
<div :class="['flex h-8 w-8 shrink-0 items-center justify-center rounded-lg', kiroAccountType === 'idc' ? 'bg-blue-500 text-white' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400']">
|
||||
<Icon name="cloud" size="sm" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.accounts.oauth.kiro.idcTitle') }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.oauth.kiro.idcSubtitle') }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="kiroAccountType = 'import'"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
kiroAccountType === 'import'
|
||||
? 'border-slate-500 bg-slate-50 dark:bg-slate-900/20'
|
||||
: 'border-gray-200 hover:border-slate-300 dark:border-dark-600 dark:hover:border-slate-700'
|
||||
]"
|
||||
>
|
||||
<div :class="['flex h-8 w-8 shrink-0 items-center justify-center rounded-lg', kiroAccountType === 'import' ? 'bg-slate-700 text-white dark:bg-slate-500' : 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400']">
|
||||
<Icon name="download" size="sm" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.accounts.oauth.kiro.importTitle') }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.oauth.kiro.importSubtitle') }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.platform === 'kiro' && accountCategory === 'oauth-based' && kiroAccountType === 'oauth'" class="mt-4 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="input-label">{{ t('admin.accounts.oauth.kiro.oauthProviderTitle') }}</label>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.oauth.kiro.socialSubtitle') }}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
@click="kiroOAuthProvider = 'google'"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
kiroOAuthProvider === 'google'
|
||||
? 'border-amber-500 bg-amber-50 dark:bg-amber-900/20'
|
||||
: 'border-gray-200 hover:border-amber-300 dark:border-dark-600 dark:hover:border-amber-700'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
|
||||
kiroOAuthProvider === 'google'
|
||||
? 'bg-amber-500 text-white'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
|
||||
]"
|
||||
>
|
||||
<Icon name="user" size="sm" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.accounts.oauth.kiro.googleTitle') }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.oauth.kiro.googleDesc') }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@click="kiroOAuthProvider = 'github'"
|
||||
:class="[
|
||||
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||
kiroOAuthProvider === 'github'
|
||||
? 'border-slate-500 bg-slate-50 dark:bg-slate-900/20'
|
||||
: 'border-gray-200 hover:border-slate-300 dark:border-dark-600 dark:hover:border-slate-700'
|
||||
]"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
|
||||
kiroOAuthProvider === 'github'
|
||||
? 'bg-slate-700 text-white dark:bg-slate-500'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-dark-600 dark:text-gray-400'
|
||||
]"
|
||||
>
|
||||
<Icon name="terminal" size="sm" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('admin.accounts.oauth.kiro.githubTitle') }}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.oauth.kiro.githubDesc') }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.platform === 'kiro' && accountCategory === 'oauth-based' && kiroAccountType === 'idc'" class="space-y-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.oauth.kiro.startUrlLabel') }}</label>
|
||||
<input
|
||||
v-model="kiroIDCStartUrl"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="t('admin.accounts.oauth.kiro.startUrlPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.oauth.kiro.regionLabel') }}</label>
|
||||
<input
|
||||
v-model="kiroIDCRegion"
|
||||
type="text"
|
||||
class="input"
|
||||
:placeholder="t('admin.accounts.oauth.kiro.regionPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.platform === 'kiro' && accountCategory === 'apikey'" class="space-y-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.baseUrl') }}</label>
|
||||
<input
|
||||
v-model="apiKeyBaseUrl"
|
||||
type="text"
|
||||
required
|
||||
class="input"
|
||||
placeholder="https://your-kiro-upstream.example.com"
|
||||
/>
|
||||
<p class="input-hint">{{ baseUrlHint }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.apiKeyRequired') }}</label>
|
||||
<input
|
||||
v-model="apiKeyValue"
|
||||
type="password"
|
||||
required
|
||||
class="input font-mono"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
<p class="input-hint">{{ apiKeyHint }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="form.platform === 'kiro' && accountCategory === 'apikey'" class="space-y-4">
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.poolMode') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.poolModeHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="poolModeEnabled = !poolModeEnabled"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||
poolModeEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
poolModeEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="poolModeEnabled" class="rounded-lg bg-blue-50 p-3 dark:bg-blue-900/20">
|
||||
<p class="text-xs text-blue-700 dark:text-blue-400">
|
||||
<Icon name="exclamationCircle" size="sm" class="mr-1 inline" :stroke-width="2" />
|
||||
{{ t('admin.accounts.poolModeInfo') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="poolModeEnabled" class="mt-3">
|
||||
<label class="input-label">{{ t('admin.accounts.poolModeRetryCount') }}</label>
|
||||
<input
|
||||
v-model.number="poolModeRetryCount"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="MAX_POOL_MODE_RETRY_COUNT"
|
||||
step="1"
|
||||
class="input"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
t('admin.accounts.poolModeRetryCountHint', {
|
||||
default: DEFAULT_POOL_MODE_RETRY_COUNT,
|
||||
max: MAX_POOL_MODE_RETRY_COUNT
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<label class="input-label mb-0">{{ t('admin.accounts.customErrorCodes') }}</label>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.accounts.customErrorCodesHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="customErrorCodesEnabled = !customErrorCodesEnabled"
|
||||
:class="[
|
||||
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||
customErrorCodesEnabled ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||
]"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||
customErrorCodesEnabled ? 'translate-x-5' : 'translate-x-0'
|
||||
]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="customErrorCodesEnabled" class="space-y-3">
|
||||
<div class="rounded-lg bg-amber-50 p-3 dark:bg-amber-900/20">
|
||||
<p class="text-xs text-amber-700 dark:text-amber-400">
|
||||
<Icon name="exclamationTriangle" size="sm" class="mr-1 inline" :stroke-width="2" />
|
||||
{{ t('admin.accounts.customErrorCodesWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="code in commonErrorCodes"
|
||||
:key="code.value"
|
||||
type="button"
|
||||
@click="toggleErrorCode(code.value)"
|
||||
:class="[
|
||||
'rounded-lg px-3 py-1.5 text-sm font-medium transition-colors',
|
||||
selectedErrorCodes.includes(code.value)
|
||||
? 'bg-red-100 text-red-700 ring-1 ring-red-500 dark:bg-red-900/30 dark:text-red-400'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500'
|
||||
]"
|
||||
>
|
||||
{{ code.value }} {{ code.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model.number="customErrorCodeInput"
|
||||
type="number"
|
||||
min="100"
|
||||
max="599"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.enterErrorCode')"
|
||||
@keyup.enter="addCustomErrorCode"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="addCustomErrorCode"
|
||||
class="btn btn-secondary shrink-0"
|
||||
>
|
||||
{{ t('admin.accounts.add') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<span
|
||||
v-for="code in selectedErrorCodes.sort((a, b) => a - b)"
|
||||
:key="code"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-red-100 px-2.5 py-0.5 text-sm font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||
>
|
||||
{{ code }}
|
||||
<button
|
||||
type="button"
|
||||
@click="removeErrorCode(code)"
|
||||
class="hover:text-red-900 dark:hover:text-red-300"
|
||||
>
|
||||
<Icon name="x" size="sm" :stroke-width="2" />
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="selectedErrorCodes.length === 0" class="text-xs text-gray-400">
|
||||
{{ t('admin.accounts.noneSelectedUsesDefault') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Kiro 只支持模型映射模式,不支持白名单模式 -->
|
||||
<div v-if="form.platform === 'kiro'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||
<div>
|
||||
<div class="mb-3 rounded-lg bg-purple-50 p-3 dark:bg-purple-900/20">
|
||||
<p class="text-xs text-purple-700 dark:text-purple-400">
|
||||
{{ t('admin.accounts.mapRequestModels') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="kiroModelMappings.length > 0" class="mb-3 space-y-2">
|
||||
<div
|
||||
v-for="(mapping, index) in kiroModelMappings"
|
||||
:key="getKiroModelMappingKey(mapping)"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="mapping.from"
|
||||
type="text"
|
||||
:class="[
|
||||
'input flex-1',
|
||||
!isValidWildcardPattern(mapping.from) ? 'border-red-500 dark:border-red-500' : ''
|
||||
]"
|
||||
:placeholder="t('admin.accounts.requestModel')"
|
||||
/>
|
||||
<svg class="h-4 w-4 flex-shrink-0 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
<input
|
||||
v-model="mapping.to"
|
||||
type="text"
|
||||
:class="[
|
||||
'input flex-1',
|
||||
mapping.to.includes('*') ? 'border-red-500 dark:border-red-500' : ''
|
||||
]"
|
||||
:placeholder="t('admin.accounts.actualModel')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeKiroModelMapping(index)"
|
||||
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
>
|
||||
<Icon name="x" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" @click="addKiroModelMapping" class="mb-3 w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300">
|
||||
<svg class="mr-1 inline h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{{ t('admin.accounts.addMapping') }}
|
||||
</button>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="preset in kiroPresetMappings"
|
||||
:key="preset.label"
|
||||
type="button"
|
||||
@click="addKiroPresetMapping(preset.from, preset.to)"
|
||||
:class="['rounded-lg px-3 py-1 text-xs transition-colors', preset.color]"
|
||||
>
|
||||
+ {{ preset.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upstream config (only for Antigravity upstream type) -->
|
||||
<div v-if="form.platform === 'antigravity' && antigravityAccountType === 'upstream'" class="space-y-4">
|
||||
<div>
|
||||
@@ -1009,7 +1473,7 @@
|
||||
</div>
|
||||
|
||||
<!-- API Key input (only for apikey type, excluding Antigravity which has its own fields) -->
|
||||
<div v-if="form.type === 'apikey' && form.platform !== 'antigravity'" class="space-y-4">
|
||||
<div v-if="form.type === 'apikey' && form.platform !== 'antigravity' && form.platform !== 'kiro'" class="space-y-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.baseUrl') }}</label>
|
||||
<input
|
||||
@@ -2750,7 +3214,23 @@
|
||||
|
||||
<!-- Step 2: OAuth Authorization -->
|
||||
<div v-else class="space-y-5">
|
||||
<div v-if="isKiroImportMode" class="space-y-4 rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-700 dark:bg-amber-900/20">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.oauth.kiro.tokenJsonLabel') }}</label>
|
||||
<textarea v-model="kiroTokenJson" rows="8" class="input font-mono text-xs" placeholder='{"accessToken":"...","refreshToken":"..."}'></textarea>
|
||||
<p class="input-hint">{{ t('admin.accounts.oauth.kiro.tokenJsonHint') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.accounts.oauth.kiro.deviceRegistrationLabel') }}</label>
|
||||
<textarea v-model="kiroDeviceRegistrationJson" rows="6" class="input font-mono text-xs" placeholder='{"clientId":"...","clientSecret":"..."}'></textarea>
|
||||
<p class="input-hint">{{ t('admin.accounts.oauth.kiro.deviceRegistrationHint') }}</p>
|
||||
</div>
|
||||
<div v-if="currentOAuthError" class="rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-700 dark:bg-red-900/30">
|
||||
<p class="whitespace-pre-line text-sm text-red-600 dark:text-red-400">{{ currentOAuthError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<OAuthAuthorizationFlow
|
||||
v-else
|
||||
ref="oauthFlowRef"
|
||||
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
|
||||
:auth-url="currentAuthUrl"
|
||||
@@ -2822,7 +3302,16 @@
|
||||
{{ t('common.back') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="isManualInputMethod"
|
||||
v-if="isKiroImportMode"
|
||||
type="button"
|
||||
:disabled="currentOAuthLoading || !kiroTokenJson.trim()"
|
||||
class="btn btn-primary"
|
||||
@click="handleKiroImport"
|
||||
>
|
||||
{{ currentOAuthLoading ? t('admin.accounts.creating') : t('common.create') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="isManualInputMethod"
|
||||
type="button"
|
||||
:disabled="!canExchangeCode"
|
||||
class="btn btn-primary"
|
||||
@@ -3099,6 +3588,7 @@ import {
|
||||
commonErrorCodes,
|
||||
buildModelMappingObject,
|
||||
fetchAntigravityDefaultMappings,
|
||||
fetchKiroDefaultMappings,
|
||||
isValidWildcardPattern
|
||||
} from '@/composables/useModelWhitelist'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@@ -3112,6 +3602,7 @@ import {
|
||||
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
||||
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
||||
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
||||
import { useKiroOAuth } from '@/composables/useKiroOAuth'
|
||||
import type {
|
||||
Proxy,
|
||||
AdminGroup,
|
||||
@@ -3148,6 +3639,8 @@ import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
||||
interface OAuthFlowExposed {
|
||||
authCode: string
|
||||
oauthState: string
|
||||
oauthCallbackPath: string
|
||||
oauthLoginOption: string
|
||||
projectId: string
|
||||
sessionKey: string
|
||||
refreshToken: string
|
||||
@@ -3163,6 +3656,11 @@ const oauthStepTitle = computed(() => {
|
||||
if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')
|
||||
if (form.platform === 'gemini') return t('admin.accounts.oauth.gemini.title')
|
||||
if (form.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.title')
|
||||
if (form.platform === 'kiro') {
|
||||
return kiroAccountType.value === 'import'
|
||||
? t('admin.accounts.oauth.kiro.importDialogTitle')
|
||||
: t('admin.accounts.oauth.kiro.title')
|
||||
}
|
||||
return t('admin.accounts.oauth.title')
|
||||
})
|
||||
|
||||
@@ -3170,12 +3668,14 @@ const oauthStepTitle = computed(() => {
|
||||
const baseUrlHint = computed(() => {
|
||||
if (form.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
|
||||
if (form.platform === 'gemini') return t('admin.accounts.gemini.baseUrlHint')
|
||||
if (form.platform === 'kiro') return t('admin.accounts.kiro.baseUrlHint')
|
||||
return t('admin.accounts.baseUrlHint')
|
||||
})
|
||||
|
||||
const apiKeyHint = computed(() => {
|
||||
if (form.platform === 'openai') return t('admin.accounts.openai.apiKeyHint')
|
||||
if (form.platform === 'gemini') return t('admin.accounts.gemini.apiKeyHint')
|
||||
if (form.platform === 'kiro') return t('admin.accounts.kiro.apiKeyHint')
|
||||
return t('admin.accounts.apiKeyHint')
|
||||
})
|
||||
|
||||
@@ -3198,12 +3698,14 @@ const oauth = useAccountOAuth() // For Anthropic OAuth
|
||||
const openaiOAuth = useOpenAIOAuth() // For OpenAI OAuth
|
||||
const geminiOAuth = useGeminiOAuth() // For Gemini OAuth
|
||||
const antigravityOAuth = useAntigravityOAuth() // For Antigravity OAuth
|
||||
const kiroOAuth = useKiroOAuth() // For Kiro OAuth / IDC
|
||||
|
||||
// Computed: current OAuth state for template binding
|
||||
const currentAuthUrl = computed(() => {
|
||||
if (form.platform === 'openai') return openaiOAuth.authUrl.value
|
||||
if (form.platform === 'gemini') return geminiOAuth.authUrl.value
|
||||
if (form.platform === 'antigravity') return antigravityOAuth.authUrl.value
|
||||
if (form.platform === 'kiro') return kiroOAuth.authUrl.value
|
||||
return oauth.authUrl.value
|
||||
})
|
||||
|
||||
@@ -3211,6 +3713,7 @@ const currentSessionId = computed(() => {
|
||||
if (form.platform === 'openai') return openaiOAuth.sessionId.value
|
||||
if (form.platform === 'gemini') return geminiOAuth.sessionId.value
|
||||
if (form.platform === 'antigravity') return antigravityOAuth.sessionId.value
|
||||
if (form.platform === 'kiro') return kiroOAuth.sessionId.value
|
||||
return oauth.sessionId.value
|
||||
})
|
||||
|
||||
@@ -3218,6 +3721,7 @@ const currentOAuthLoading = computed(() => {
|
||||
if (form.platform === 'openai') return openaiOAuth.loading.value
|
||||
if (form.platform === 'gemini') return geminiOAuth.loading.value
|
||||
if (form.platform === 'antigravity') return antigravityOAuth.loading.value
|
||||
if (form.platform === 'kiro') return kiroOAuth.loading.value
|
||||
return oauth.loading.value
|
||||
})
|
||||
|
||||
@@ -3225,6 +3729,7 @@ const currentOAuthError = computed(() => {
|
||||
if (form.platform === 'openai') return openaiOAuth.error.value
|
||||
if (form.platform === 'gemini') return geminiOAuth.error.value
|
||||
if (form.platform === 'antigravity') return antigravityOAuth.error.value
|
||||
if (form.platform === 'kiro') return kiroOAuth.error.value
|
||||
return oauth.error.value
|
||||
})
|
||||
|
||||
@@ -3303,6 +3808,14 @@ const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist'
|
||||
const antigravityWhitelistModels = ref<string[]>([])
|
||||
const antigravityModelMappings = ref<ModelMapping[]>([])
|
||||
const antigravityPresetMappings = computed(() => getPresetMappingsByPlatform('antigravity'))
|
||||
const kiroAccountType = ref<'oauth' | 'idc' | 'import'>('oauth')
|
||||
const kiroOAuthProvider = ref<'google' | 'github'>('google')
|
||||
const kiroIDCStartUrl = ref('https://view.awsapps.com/start')
|
||||
const kiroIDCRegion = ref('us-east-1')
|
||||
const kiroTokenJson = ref('')
|
||||
const kiroDeviceRegistrationJson = ref('')
|
||||
const kiroModelMappings = ref<ModelMapping[]>([])
|
||||
const kiroPresetMappings = computed(() => getPresetMappingsByPlatform('kiro'))
|
||||
const bedrockPresets = computed(() => getPresetMappingsByPlatform('bedrock'))
|
||||
|
||||
// Bedrock credentials
|
||||
@@ -3324,6 +3837,7 @@ const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
||||
const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-model-mapping')
|
||||
const getOpenAICompactModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-openai-compact-model-mapping')
|
||||
const getAntigravityModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-antigravity-model-mapping')
|
||||
const getKiroModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-kiro-model-mapping')
|
||||
const getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>('create-temp-unsched-rule')
|
||||
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one')
|
||||
const geminiAIStudioOAuthEnabled = ref(false)
|
||||
@@ -3509,6 +4023,8 @@ const isOAuthFlow = computed(() => {
|
||||
return accountCategory.value === 'oauth-based'
|
||||
})
|
||||
|
||||
const isKiroImportMode = computed(() => form.platform === 'kiro' && kiroAccountType.value === 'import')
|
||||
|
||||
const isManualInputMethod = computed(() => {
|
||||
return oauthFlowRef.value?.inputMethod === 'manual'
|
||||
})
|
||||
@@ -3531,6 +4047,9 @@ const canExchangeCode = computed(() => {
|
||||
if (form.platform === 'antigravity') {
|
||||
return authCode.trim() && antigravityOAuth.sessionId.value && !antigravityOAuth.loading.value
|
||||
}
|
||||
if (form.platform === 'kiro') {
|
||||
return authCode.trim() && kiroOAuth.sessionId.value && !kiroOAuth.loading.value
|
||||
}
|
||||
return authCode.trim() && oauth.sessionId.value && !oauth.loading.value
|
||||
})
|
||||
|
||||
@@ -3552,10 +4071,15 @@ watch(
|
||||
antigravityModelMappings.value = [...mappings]
|
||||
})
|
||||
antigravityWhitelistModels.value = []
|
||||
} else if (form.platform === 'kiro') {
|
||||
fetchKiroDefaultMappings().then(mappings => {
|
||||
kiroModelMappings.value = [...mappings]
|
||||
})
|
||||
} else {
|
||||
antigravityWhitelistModels.value = []
|
||||
antigravityModelMappings.value = []
|
||||
antigravityModelRestrictionMode.value = 'mapping'
|
||||
kiroModelMappings.value = []
|
||||
}
|
||||
} else {
|
||||
resetForm()
|
||||
@@ -3572,6 +4096,10 @@ watch(
|
||||
form.type = 'apikey'
|
||||
return
|
||||
}
|
||||
if (form.platform === 'kiro') {
|
||||
form.type = category === 'oauth-based' ? 'oauth' : 'apikey'
|
||||
return
|
||||
}
|
||||
// Bedrock 类型
|
||||
if (form.platform === 'anthropic' && category === 'bedrock') {
|
||||
form.type = 'bedrock' as AccountType
|
||||
@@ -3598,6 +4126,8 @@ watch(
|
||||
? 'https://api.openai.com'
|
||||
: newPlatform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: newPlatform === 'kiro'
|
||||
? ''
|
||||
: 'https://api.anthropic.com'
|
||||
// Clear model-related settings
|
||||
allowedModels.value = []
|
||||
@@ -3611,11 +4141,21 @@ watch(
|
||||
antigravityWhitelistModels.value = []
|
||||
accountCategory.value = 'oauth-based'
|
||||
antigravityAccountType.value = 'oauth'
|
||||
} else if (newPlatform === 'kiro') {
|
||||
fetchKiroDefaultMappings().then(mappings => {
|
||||
kiroModelMappings.value = [...mappings]
|
||||
})
|
||||
accountCategory.value = 'oauth-based'
|
||||
kiroAccountType.value = 'oauth'
|
||||
kiroOAuthProvider.value = 'google'
|
||||
apiKeyBaseUrl.value = ''
|
||||
apiKeyValue.value = ''
|
||||
} else {
|
||||
allowOverages.value = false
|
||||
antigravityWhitelistModels.value = []
|
||||
antigravityModelMappings.value = []
|
||||
antigravityModelRestrictionMode.value = 'mapping'
|
||||
kiroModelMappings.value = []
|
||||
}
|
||||
if (newPlatform !== 'gemini' && newPlatform !== 'anthropic' && accountCategory.value === 'service_account') {
|
||||
accountCategory.value = 'oauth-based'
|
||||
@@ -3655,6 +4195,7 @@ watch(
|
||||
|
||||
geminiOAuth.resetState()
|
||||
antigravityOAuth.resetState()
|
||||
kiroOAuth.resetState()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3756,6 +4297,22 @@ const addAntigravityPresetMapping = (from: string, to: string) => {
|
||||
antigravityModelMappings.value.push({ from, to })
|
||||
}
|
||||
|
||||
const addKiroModelMapping = () => {
|
||||
kiroModelMappings.value.push({ from: '', to: '' })
|
||||
}
|
||||
|
||||
const removeKiroModelMapping = (index: number) => {
|
||||
kiroModelMappings.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const addKiroPresetMapping = (from: string, to: string) => {
|
||||
if (kiroModelMappings.value.some((m) => m.from === from)) {
|
||||
appStore.showInfo(t('admin.accounts.mappingExists', { model: from }))
|
||||
return
|
||||
}
|
||||
kiroModelMappings.value.push({ from, to })
|
||||
}
|
||||
|
||||
// Error code toggle helper
|
||||
const toggleErrorCode = (code: number) => {
|
||||
const index = selectedErrorCodes.value.indexOf(code)
|
||||
@@ -4029,6 +4586,15 @@ const resetForm = () => {
|
||||
fetchAntigravityDefaultMappings().then(mappings => {
|
||||
antigravityModelMappings.value = [...mappings]
|
||||
})
|
||||
kiroAccountType.value = 'oauth'
|
||||
kiroOAuthProvider.value = 'google'
|
||||
kiroIDCStartUrl.value = 'https://view.awsapps.com/start'
|
||||
kiroIDCRegion.value = 'us-east-1'
|
||||
kiroTokenJson.value = ''
|
||||
kiroDeviceRegistrationJson.value = ''
|
||||
fetchKiroDefaultMappings().then(mappings => {
|
||||
kiroModelMappings.value = [...mappings]
|
||||
})
|
||||
poolModeEnabled.value = false
|
||||
poolModeRetryCount.value = DEFAULT_POOL_MODE_RETRY_COUNT
|
||||
customErrorCodesEnabled.value = false
|
||||
@@ -4080,6 +4646,7 @@ const resetForm = () => {
|
||||
openaiOAuth.resetState()
|
||||
geminiOAuth.resetState()
|
||||
antigravityOAuth.resetState()
|
||||
kiroOAuth.resetState()
|
||||
oauthFlowRef.value?.reset()
|
||||
antigravityMixedChannelConfirmed.value = false
|
||||
clearMixedChannelDialog()
|
||||
@@ -4375,6 +4942,45 @@ const handleSubmit = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// For Kiro API key type, create directly
|
||||
if (form.platform === 'kiro' && accountCategory.value === 'apikey') {
|
||||
if (!form.name.trim()) {
|
||||
appStore.showError(t('admin.accounts.pleaseEnterAccountName'))
|
||||
return
|
||||
}
|
||||
if (!apiKeyBaseUrl.value.trim()) {
|
||||
appStore.showError(t('admin.accounts.upstream.pleaseEnterBaseUrl'))
|
||||
return
|
||||
}
|
||||
if (!apiKeyValue.value.trim()) {
|
||||
appStore.showError(t('admin.accounts.pleaseEnterApiKey'))
|
||||
return
|
||||
}
|
||||
|
||||
const credentials: Record<string, unknown> = {
|
||||
base_url: apiKeyBaseUrl.value.trim(),
|
||||
api_key: apiKeyValue.value.trim()
|
||||
}
|
||||
|
||||
const modelMapping = buildModelMappingObject('mapping', [], kiroModelMappings.value)
|
||||
if (modelMapping) {
|
||||
credentials.model_mapping = modelMapping
|
||||
}
|
||||
|
||||
if (poolModeEnabled.value) {
|
||||
credentials.pool_mode = true
|
||||
credentials.pool_mode_retry_count = normalizePoolModeRetryCount(poolModeRetryCount.value)
|
||||
}
|
||||
|
||||
if (customErrorCodesEnabled.value) {
|
||||
credentials.custom_error_codes_enabled = true
|
||||
credentials.custom_error_codes = [...selectedErrorCodes.value]
|
||||
}
|
||||
|
||||
await createAccountAndFinish('kiro', 'apikey', credentials)
|
||||
return
|
||||
}
|
||||
|
||||
// For apikey type, create directly
|
||||
if (!apiKeyValue.value.trim()) {
|
||||
appStore.showError(t('admin.accounts.pleaseEnterApiKey'))
|
||||
@@ -4446,6 +5052,7 @@ const goBackToBasicInfo = () => {
|
||||
openaiOAuth.resetState()
|
||||
geminiOAuth.resetState()
|
||||
antigravityOAuth.resetState()
|
||||
kiroOAuth.resetState()
|
||||
oauthFlowRef.value?.reset()
|
||||
}
|
||||
|
||||
@@ -4461,6 +5068,19 @@ const handleGenerateUrl = async () => {
|
||||
)
|
||||
} else if (form.platform === 'antigravity') {
|
||||
await antigravityOAuth.generateAuthUrl(form.proxy_id)
|
||||
} else if (form.platform === 'kiro') {
|
||||
if (kiroAccountType.value === 'idc') {
|
||||
await kiroOAuth.generateIDCAuthUrl({
|
||||
proxyId: form.proxy_id,
|
||||
startUrl: kiroIDCStartUrl.value.trim() || undefined,
|
||||
region: kiroIDCRegion.value.trim() || undefined
|
||||
})
|
||||
} else {
|
||||
await kiroOAuth.generateAuthUrl(
|
||||
form.proxy_id,
|
||||
kiroOAuthProvider.value === 'github' ? 'Github' : 'Google'
|
||||
)
|
||||
}
|
||||
} else {
|
||||
await oauth.generateAuthUrl(addMethod.value, form.proxy_id)
|
||||
}
|
||||
@@ -4924,6 +5544,50 @@ const handleAntigravityExchange = async (authCode: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
const buildKiroCredentials = (tokenInfo: Parameters<typeof kiroOAuth.buildCredentials>[0]) => {
|
||||
const credentials = kiroOAuth.buildCredentials(tokenInfo)
|
||||
const modelMapping = buildModelMappingObject('mapping', [], kiroModelMappings.value)
|
||||
if (modelMapping) {
|
||||
credentials.model_mapping = modelMapping
|
||||
}
|
||||
return credentials
|
||||
}
|
||||
|
||||
const handleKiroExchange = async (authCode: string) => {
|
||||
if (!authCode.trim() || !kiroOAuth.sessionId.value) return
|
||||
|
||||
kiroOAuth.loading.value = true
|
||||
kiroOAuth.error.value = ''
|
||||
|
||||
try {
|
||||
const stateFromInput = oauthFlowRef.value?.oauthState || ''
|
||||
const stateToUse = stateFromInput || kiroOAuth.state.value
|
||||
if (!stateToUse) {
|
||||
kiroOAuth.error.value = t('admin.accounts.oauth.authFailed')
|
||||
appStore.showError(kiroOAuth.error.value)
|
||||
return
|
||||
}
|
||||
|
||||
const tokenInfo = await kiroOAuth.exchangeAuthCode({
|
||||
code: authCode.trim(),
|
||||
sessionId: kiroOAuth.sessionId.value,
|
||||
state: stateToUse,
|
||||
callbackPath: oauthFlowRef.value?.oauthCallbackPath || '',
|
||||
loginOption: oauthFlowRef.value?.oauthLoginOption || '',
|
||||
proxyId: form.proxy_id
|
||||
})
|
||||
if (!tokenInfo) return
|
||||
|
||||
const credentials = buildKiroCredentials(tokenInfo)
|
||||
await createAccountAndFinish('kiro', 'oauth', credentials)
|
||||
} catch (error: any) {
|
||||
kiroOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
||||
appStore.showError(kiroOAuth.error.value)
|
||||
} finally {
|
||||
kiroOAuth.loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Anthropic OAuth 授权码兑换
|
||||
const handleAnthropicExchange = async (authCode: string) => {
|
||||
if (!authCode.trim() || !oauth.sessionId.value) return
|
||||
@@ -5022,6 +5686,8 @@ const handleExchangeCode = async () => {
|
||||
return handleOpenAIExchange(authCode)
|
||||
case 'gemini':
|
||||
return handleGeminiExchange(authCode)
|
||||
case 'kiro':
|
||||
return handleKiroExchange(authCode)
|
||||
case 'antigravity':
|
||||
return handleAntigravityExchange(authCode)
|
||||
default:
|
||||
@@ -5029,6 +5695,24 @@ const handleExchangeCode = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleKiroImport = async () => {
|
||||
if (!isKiroImportMode.value || !kiroTokenJson.value.trim()) return
|
||||
|
||||
const tokenInfo = await kiroOAuth.importToken(
|
||||
kiroTokenJson.value,
|
||||
kiroDeviceRegistrationJson.value || undefined
|
||||
)
|
||||
if (!tokenInfo) return
|
||||
|
||||
try {
|
||||
const credentials = buildKiroCredentials(tokenInfo)
|
||||
await createAccountAndFinish('kiro', 'oauth', credentials)
|
||||
} catch (error: any) {
|
||||
kiroOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
||||
appStore.showError(kiroOAuth.error.value)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCookieAuth = async (sessionKey: string) => {
|
||||
oauth.loading.value = true
|
||||
oauth.error.value = ''
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
? 'https://api.openai.com'
|
||||
: account.platform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: account.platform === 'kiro'
|
||||
? 'https://your-kiro-upstream.example.com'
|
||||
: account.platform === 'antigravity'
|
||||
? 'https://cloudcode-pa.googleapis.com'
|
||||
: 'https://api.anthropic.com'
|
||||
@@ -61,6 +63,8 @@
|
||||
? 'sk-proj-...'
|
||||
: account.platform === 'gemini'
|
||||
? 'AIza...'
|
||||
: account.platform === 'kiro'
|
||||
? 'sk-...'
|
||||
: account.platform === 'antigravity'
|
||||
? 'sk-...'
|
||||
: 'sk-ant-...'
|
||||
@@ -69,8 +73,93 @@
|
||||
<p class="input-hint">{{ t('admin.accounts.leaveEmptyToKeep') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Model Restriction Section (不适用于 Antigravity) -->
|
||||
<div v-if="account.platform !== 'antigravity'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<div v-if="account.platform === 'kiro'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||
|
||||
<div class="mb-3 rounded-lg bg-purple-50 p-3 dark:bg-purple-900/20">
|
||||
<p class="text-xs text-purple-700 dark:text-purple-400">
|
||||
{{ t('admin.accounts.mapRequestModels') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="modelMappings.length > 0" class="mb-3 space-y-2">
|
||||
<div
|
||||
v-for="(mapping, index) in modelMappings"
|
||||
:key="getModelMappingKey(mapping)"
|
||||
class="space-y-1"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="mapping.from"
|
||||
type="text"
|
||||
:class="[
|
||||
'input flex-1',
|
||||
!isValidWildcardPattern(mapping.from) ? 'border-red-500 dark:border-red-500' : ''
|
||||
]"
|
||||
:placeholder="t('admin.accounts.requestModel')"
|
||||
/>
|
||||
<svg class="h-4 w-4 flex-shrink-0 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
|
||||
</svg>
|
||||
<input
|
||||
v-model="mapping.to"
|
||||
type="text"
|
||||
:class="[
|
||||
'input flex-1',
|
||||
mapping.to.includes('*') ? 'border-red-500 dark:border-red-500' : ''
|
||||
]"
|
||||
:placeholder="t('admin.accounts.actualModel')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeModelMapping(index)"
|
||||
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!isValidWildcardPattern(mapping.from)" class="text-xs text-red-500">
|
||||
{{ t('admin.accounts.wildcardOnlyAtEnd') }}
|
||||
</p>
|
||||
<p v-if="mapping.to.includes('*')" class="text-xs text-red-500">
|
||||
{{ t('admin.accounts.targetNoWildcard') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="addModelMapping"
|
||||
class="mb-3 w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg class="mr-1 inline h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{{ t('admin.accounts.addMapping') }}
|
||||
</button>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="preset in presetMappings"
|
||||
:key="preset.label"
|
||||
type="button"
|
||||
@click="addPresetMapping(preset.from, preset.to)"
|
||||
:class="['rounded-lg px-3 py-1 text-xs transition-colors', preset.color]"
|
||||
>
|
||||
+ {{ preset.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Model Restriction Section (不适用于 Antigravity / Kiro) -->
|
||||
<div v-else-if="account.platform !== 'antigravity'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||
|
||||
<div
|
||||
@@ -407,9 +496,9 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- OpenAI OAuth Model Mapping (OAuth 类型没有 apikey 容器,需要独立的模型映射区域) -->
|
||||
<!-- OpenAI / Kiro OAuth Model Restriction (OAuth 类型没有 apikey 容器,需要独立区域) -->
|
||||
<div
|
||||
v-if="account.platform === 'openai' && account.type === 'oauth'"
|
||||
v-if="(account.platform === 'openai' || account.platform === 'kiro') && account.type === 'oauth'"
|
||||
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||
>
|
||||
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||
@@ -423,6 +512,82 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-else-if="account.platform === 'kiro'">
|
||||
<div class="mb-3 rounded-lg bg-purple-50 p-3 dark:bg-purple-900/20">
|
||||
<p class="text-xs text-purple-700 dark:text-purple-400">
|
||||
{{ t('admin.accounts.mapRequestModels') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="modelMappings.length > 0" class="mb-3 space-y-2">
|
||||
<div
|
||||
v-for="(mapping, index) in modelMappings"
|
||||
:key="'oauth-' + getModelMappingKey(mapping)"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<input
|
||||
v-model="mapping.from"
|
||||
type="text"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.requestModel')"
|
||||
/>
|
||||
<svg
|
||||
class="h-4 w-4 flex-shrink-0 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M14 5l7 7m0 0l-7 7m7-7H3"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
v-model="mapping.to"
|
||||
type="text"
|
||||
class="input flex-1"
|
||||
:placeholder="t('admin.accounts.actualModel')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="removeModelMapping(index)"
|
||||
class="rounded-lg p-2 text-red-500 transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20"
|
||||
>
|
||||
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@click="addModelMapping"
|
||||
class="mb-3 w-full rounded-lg border-2 border-dashed border-gray-300 px-4 py-2 text-gray-600 transition-colors hover:border-gray-400 hover:text-gray-700 dark:border-dark-500 dark:text-gray-400 dark:hover:border-dark-400 dark:hover:text-gray-300"
|
||||
>
|
||||
+ {{ t('admin.accounts.addMapping') }}
|
||||
</button>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="preset in presetMappings"
|
||||
:key="'oauth-' + preset.label"
|
||||
type="button"
|
||||
@click="addPresetMapping(preset.from, preset.to)"
|
||||
:class="['rounded-lg px-3 py-1 text-xs transition-colors', preset.color]"
|
||||
>
|
||||
+ {{ preset.label }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<!-- Mode Toggle -->
|
||||
<div class="mb-4 flex gap-2">
|
||||
@@ -2145,6 +2310,7 @@ import {
|
||||
resolveOpenAIWSModeFromExtra
|
||||
} from '@/utils/openaiWsMode'
|
||||
import {
|
||||
fetchKiroDefaultMappings,
|
||||
getPresetMappingsByPlatform,
|
||||
commonErrorCodes,
|
||||
buildModelMappingObject,
|
||||
@@ -2173,11 +2339,13 @@ const baseUrlHint = computed(() => {
|
||||
if (!props.account) return t('admin.accounts.baseUrlHint')
|
||||
if (props.account.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
|
||||
if (props.account.platform === 'gemini') return t('admin.accounts.gemini.baseUrlHint')
|
||||
if (props.account.platform === 'kiro') return t('admin.accounts.kiro.baseUrlHint')
|
||||
return t('admin.accounts.baseUrlHint')
|
||||
})
|
||||
|
||||
const antigravityPresetMappings = computed(() => getPresetMappingsByPlatform('antigravity'))
|
||||
const bedrockPresets = computed(() => getPresetMappingsByPlatform('bedrock'))
|
||||
const isKiroOAuthAccount = computed(() => props.account?.platform === 'kiro' && props.account?.type === 'oauth')
|
||||
|
||||
// Model mapping type
|
||||
interface ModelMapping {
|
||||
@@ -2235,6 +2403,21 @@ const getOpenAICompactModelMappingKey = createStableObjectKeyResolver<ModelMappi
|
||||
const getAntigravityModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-antigravity-model-mapping')
|
||||
const getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>('edit-temp-unsched-rule')
|
||||
|
||||
const applyKiroModelMappings = (entries: Array<[string, string]>) => {
|
||||
modelRestrictionMode.value = 'mapping'
|
||||
modelMappings.value = entries.map(([from, to]) => ({ from, to }))
|
||||
allowedModels.value = []
|
||||
}
|
||||
|
||||
const loadDefaultKiroModelMappings = () => {
|
||||
fetchKiroDefaultMappings().then(mappings => {
|
||||
if (!isKiroOAuthAccount.value) return
|
||||
modelRestrictionMode.value = 'mapping'
|
||||
modelMappings.value = mappings.map(({ from, to }) => ({ from, to }))
|
||||
allowedModels.value = []
|
||||
})
|
||||
}
|
||||
|
||||
const showMixedChannelWarning = ref(false)
|
||||
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(
|
||||
null
|
||||
@@ -2383,6 +2566,7 @@ const tempUnschedPresets = computed(() => [
|
||||
const defaultBaseUrl = computed(() => {
|
||||
if (props.account?.platform === 'openai') return 'https://api.openai.com'
|
||||
if (props.account?.platform === 'gemini') return 'https://generativelanguage.googleapis.com'
|
||||
if (props.account?.platform === 'kiro') return ''
|
||||
return 'https://api.anthropic.com'
|
||||
})
|
||||
|
||||
@@ -2597,6 +2781,8 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
? 'https://api.openai.com'
|
||||
: newAccount.platform === 'gemini'
|
||||
? 'https://generativelanguage.googleapis.com'
|
||||
: newAccount.platform === 'kiro'
|
||||
? ''
|
||||
: 'https://api.anthropic.com'
|
||||
editBaseUrl.value = (credentials.base_url as string) || platformDefaultUrl
|
||||
|
||||
@@ -2605,20 +2791,35 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
if (existingMappings && typeof existingMappings === 'object') {
|
||||
const entries = Object.entries(existingMappings)
|
||||
|
||||
// Detect if this is whitelist mode (all from === to) or mapping mode
|
||||
const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to)
|
||||
|
||||
if (isWhitelistMode) {
|
||||
// Whitelist mode: populate allowedModels
|
||||
modelRestrictionMode.value = 'whitelist'
|
||||
allowedModels.value = entries.map(([from]) => from)
|
||||
modelMappings.value = []
|
||||
} else {
|
||||
// Mapping mode: populate modelMappings
|
||||
if (newAccount.platform === 'kiro') {
|
||||
modelRestrictionMode.value = 'mapping'
|
||||
modelMappings.value = entries.map(([from, to]) => ({ from, to }))
|
||||
allowedModels.value = []
|
||||
} else {
|
||||
// Detect if this is whitelist mode (all from === to) or mapping mode
|
||||
const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to)
|
||||
|
||||
if (isWhitelistMode) {
|
||||
// Whitelist mode: populate allowedModels
|
||||
modelRestrictionMode.value = 'whitelist'
|
||||
allowedModels.value = entries.map(([from]) => from)
|
||||
modelMappings.value = []
|
||||
} else {
|
||||
// Mapping mode: populate modelMappings
|
||||
modelRestrictionMode.value = 'mapping'
|
||||
modelMappings.value = entries.map(([from, to]) => ({ from, to }))
|
||||
allowedModels.value = []
|
||||
}
|
||||
}
|
||||
} else if (newAccount.platform === 'kiro') {
|
||||
fetchKiroDefaultMappings().then(mappings => {
|
||||
if (props.account?.id !== newAccount.id || props.account?.type !== 'apikey' || props.account?.platform !== 'kiro') {
|
||||
return
|
||||
}
|
||||
modelRestrictionMode.value = 'mapping'
|
||||
modelMappings.value = mappings.map(({ from, to }) => ({ from, to }))
|
||||
allowedModels.value = []
|
||||
})
|
||||
} else {
|
||||
// No mappings: default to whitelist mode with empty selection (allow all)
|
||||
modelRestrictionMode.value = 'whitelist'
|
||||
@@ -2673,15 +2874,18 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
const entries = Object.entries(existingMappings)
|
||||
const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to)
|
||||
if (isWhitelistMode) {
|
||||
// Whitelist mode: populate allowedModels
|
||||
modelRestrictionMode.value = 'whitelist'
|
||||
allowedModels.value = entries.map(([from]) => from)
|
||||
modelMappings.value = []
|
||||
} else {
|
||||
// Mapping mode: populate modelMappings
|
||||
modelRestrictionMode.value = 'mapping'
|
||||
modelMappings.value = entries.map(([from, to]) => ({ from, to }))
|
||||
allowedModels.value = []
|
||||
}
|
||||
} else {
|
||||
// No mappings: default to whitelist mode with empty selection (allow all)
|
||||
modelRestrictionMode.value = 'whitelist'
|
||||
modelMappings.value = []
|
||||
allowedModels.value = []
|
||||
@@ -2723,8 +2927,16 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
||||
: 'https://api.anthropic.com'
|
||||
editBaseUrl.value = platformDefaultUrl
|
||||
|
||||
// Load model mappings for OpenAI OAuth accounts
|
||||
if (newAccount.platform === 'openai' && newAccount.credentials) {
|
||||
// Load model mappings for OpenAI/Kiro OAuth accounts
|
||||
if (newAccount.platform === 'kiro' && newAccount.credentials) {
|
||||
const oauthCredentials = newAccount.credentials as Record<string, unknown>
|
||||
const existingMappings = oauthCredentials.model_mapping as Record<string, string> | undefined
|
||||
if (existingMappings && typeof existingMappings === 'object' && Object.keys(existingMappings).length > 0) {
|
||||
applyKiroModelMappings(Object.entries(existingMappings))
|
||||
} else {
|
||||
loadDefaultKiroModelMappings()
|
||||
}
|
||||
} else if (newAccount.platform === 'openai' && newAccount.credentials) {
|
||||
const oauthCredentials = newAccount.credentials as Record<string, unknown>
|
||||
const existingMappings = oauthCredentials.model_mapping as Record<string, string> | undefined
|
||||
if (existingMappings && typeof existingMappings === 'object') {
|
||||
@@ -2780,6 +2992,7 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
|
||||
// Model mapping helpers
|
||||
const addModelMapping = () => {
|
||||
modelMappings.value.push({ from: '', to: '' })
|
||||
@@ -3221,9 +3434,16 @@ const handleSubmit = async () => {
|
||||
// For apikey type, handle credentials update
|
||||
if (props.account.type === 'apikey') {
|
||||
const currentCredentials = (props.account.credentials as Record<string, unknown>) || {}
|
||||
const newBaseUrl = editBaseUrl.value.trim() || defaultBaseUrl.value
|
||||
const newBaseUrl = props.account.platform === 'kiro'
|
||||
? editBaseUrl.value.trim()
|
||||
: (editBaseUrl.value.trim() || defaultBaseUrl.value)
|
||||
const shouldApplyModelMapping = !(props.account.platform === 'openai' && openaiPassthroughEnabled.value)
|
||||
|
||||
if (!newBaseUrl) {
|
||||
appStore.showError(t('admin.accounts.upstream.pleaseEnterBaseUrl'))
|
||||
return
|
||||
}
|
||||
|
||||
// Always update credentials for apikey type to handle model mapping changes
|
||||
const newCredentials: Record<string, unknown> = {
|
||||
...currentCredentials,
|
||||
@@ -3244,7 +3464,11 @@ const handleSubmit = async () => {
|
||||
|
||||
// Add model mapping if configured(OpenAI 开启自动透传时保留现有映射,不再编辑)
|
||||
if (shouldApplyModelMapping) {
|
||||
const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
|
||||
const modelMapping = buildModelMappingObject(
|
||||
props.account.platform === 'kiro' ? 'mapping' : modelRestrictionMode.value,
|
||||
props.account.platform === 'kiro' ? [] : allowedModels.value,
|
||||
modelMappings.value
|
||||
)
|
||||
if (modelMapping) {
|
||||
newCredentials.model_mapping = modelMapping
|
||||
} else {
|
||||
@@ -3382,7 +3606,7 @@ const handleSubmit = async () => {
|
||||
}
|
||||
|
||||
// Model mapping
|
||||
const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
|
||||
const modelMapping = buildModelMappingObject('mapping', [], modelMappings.value)
|
||||
if (modelMapping) {
|
||||
newCredentials.model_mapping = modelMapping
|
||||
} else {
|
||||
@@ -3436,6 +3660,22 @@ const handleSubmit = async () => {
|
||||
updatePayload.credentials = newCredentials
|
||||
}
|
||||
|
||||
// Kiro OAuth: persist model mapping to credentials
|
||||
if (props.account.platform === 'kiro' && props.account.type === 'oauth') {
|
||||
const currentCredentials = (updatePayload.credentials as Record<string, unknown>) ||
|
||||
((props.account.credentials as Record<string, unknown>) || {})
|
||||
const newCredentials: Record<string, unknown> = { ...currentCredentials }
|
||||
|
||||
const modelMapping = buildModelMappingObject('mapping', [], modelMappings.value)
|
||||
if (modelMapping) {
|
||||
newCredentials.model_mapping = modelMapping
|
||||
} else {
|
||||
delete newCredentials.model_mapping
|
||||
}
|
||||
|
||||
updatePayload.credentials = newCredentials
|
||||
}
|
||||
|
||||
// Antigravity: persist model mapping to credentials (applies to all antigravity types)
|
||||
// Antigravity 只支持映射模式
|
||||
if (props.account.platform === 'antigravity') {
|
||||
|
||||
@@ -603,6 +603,7 @@ const getOAuthKey = (key: string) => {
|
||||
if (props.platform === 'openai') return `admin.accounts.oauth.openai.${key}`
|
||||
if (props.platform === 'gemini') return `admin.accounts.oauth.gemini.${key}`
|
||||
if (props.platform === 'antigravity') return `admin.accounts.oauth.antigravity.${key}`
|
||||
if (props.platform === 'kiro') return `admin.accounts.oauth.kiro.${key}`
|
||||
return `admin.accounts.oauth.${key}`
|
||||
}
|
||||
|
||||
@@ -632,6 +633,8 @@ const refreshTokenInput = ref('')
|
||||
const sessionTokenInput = ref('')
|
||||
const showHelpDialog = ref(false)
|
||||
const oauthState = ref('')
|
||||
const oauthCallbackPath = ref('')
|
||||
const oauthLoginOption = ref('')
|
||||
const projectId = ref('')
|
||||
|
||||
// Computed: show method selection when either cookie or refresh token option is enabled
|
||||
@@ -661,10 +664,10 @@ watch(inputMethod, (newVal) => {
|
||||
emit('update:inputMethod', newVal)
|
||||
})
|
||||
|
||||
// Auto-extract code from callback URL (OpenAI/Gemini/Antigravity)
|
||||
// Auto-extract code from callback URL (OpenAI/Gemini/Antigravity/Kiro)
|
||||
// e.g., http://localhost:8085/callback?code=xxx...&state=...
|
||||
watch(authCodeInput, (newVal) => {
|
||||
if (props.platform !== 'openai' && props.platform !== 'gemini' && props.platform !== 'antigravity') return
|
||||
if (props.platform !== 'openai' && props.platform !== 'gemini' && props.platform !== 'antigravity' && props.platform !== 'kiro') return
|
||||
|
||||
const trimmed = newVal.trim()
|
||||
// Check if it looks like a URL with code parameter
|
||||
@@ -674,7 +677,11 @@ watch(authCodeInput, (newVal) => {
|
||||
const url = new URL(trimmed)
|
||||
const code = url.searchParams.get('code')
|
||||
const stateParam = url.searchParams.get('state')
|
||||
if ((props.platform === 'openai' || props.platform === 'gemini' || props.platform === 'antigravity') && stateParam) {
|
||||
if (props.platform === 'kiro') {
|
||||
oauthCallbackPath.value = url.pathname || ''
|
||||
oauthLoginOption.value = url.searchParams.get('login_option') || ''
|
||||
}
|
||||
if ((props.platform === 'openai' || props.platform === 'gemini' || props.platform === 'antigravity' || props.platform === 'kiro') && stateParam) {
|
||||
oauthState.value = stateParam
|
||||
}
|
||||
if (code && code !== trimmed) {
|
||||
@@ -685,7 +692,13 @@ watch(authCodeInput, (newVal) => {
|
||||
// If URL parsing fails, try regex extraction
|
||||
const match = trimmed.match(/[?&]code=([^&]+)/)
|
||||
const stateMatch = trimmed.match(/[?&]state=([^&]+)/)
|
||||
if ((props.platform === 'openai' || props.platform === 'gemini' || props.platform === 'antigravity') && stateMatch && stateMatch[1]) {
|
||||
if (props.platform === 'kiro') {
|
||||
const pathMatch = trimmed.match(/^https?:\/\/[^/]+(\/[^?]*)/)
|
||||
oauthCallbackPath.value = pathMatch?.[1] || oauthCallbackPath.value
|
||||
const loginOptionMatch = trimmed.match(/[?&]login_option=([^&]+)/)
|
||||
oauthLoginOption.value = loginOptionMatch?.[1] || oauthLoginOption.value
|
||||
}
|
||||
if ((props.platform === 'openai' || props.platform === 'gemini' || props.platform === 'antigravity' || props.platform === 'kiro') && stateMatch && stateMatch[1]) {
|
||||
oauthState.value = stateMatch[1]
|
||||
}
|
||||
if (match && match[1] && match[1] !== trimmed) {
|
||||
@@ -731,6 +744,8 @@ const handleValidateRefreshToken = () => {
|
||||
defineExpose({
|
||||
authCode: authCodeInput,
|
||||
oauthState,
|
||||
oauthCallbackPath,
|
||||
oauthLoginOption,
|
||||
projectId,
|
||||
sessionKey: sessionKeyInput,
|
||||
refreshToken: refreshTokenInput,
|
||||
@@ -739,6 +754,8 @@ defineExpose({
|
||||
reset: () => {
|
||||
authCodeInput.value = ''
|
||||
oauthState.value = ''
|
||||
oauthCallbackPath.value = ''
|
||||
oauthLoginOption.value = ''
|
||||
projectId.value = ''
|
||||
sessionKeyInput.value = ''
|
||||
refreshTokenInput.value = ''
|
||||
|
||||
Reference in New Issue
Block a user