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>
|
<span class="text-[11px] text-gray-400 dark:text-gray-500">{{ overloadCountdown }}</span>
|
||||||
</div>
|
</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) -->
|
<!-- Main Status Badge (shown when not rate limited/overloaded) -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button
|
<button
|
||||||
@@ -69,7 +74,7 @@
|
|||||||
<div
|
<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"
|
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
|
<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"
|
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>
|
></div>
|
||||||
@@ -172,11 +177,37 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Computed: is rate limited (429)
|
// 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(() => {
|
const isRateLimited = computed(() => {
|
||||||
|
if (activeKiroRuntimeResetAt.value) return true
|
||||||
if (!props.account.rate_limit_reset_at) return false
|
if (!props.account.rate_limit_reset_at) return false
|
||||||
return new Date(props.account.rate_limit_reset_at) > new Date()
|
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 = {
|
type AccountModelStatusItem = {
|
||||||
kind: 'rate_limit' | 'credits_exhausted' | 'credits_active'
|
kind: 'rate_limit' | 'credits_exhausted' | 'credits_active'
|
||||||
model: string
|
model: string
|
||||||
@@ -281,7 +312,7 @@ const isTempUnschedulable = computed(() => {
|
|||||||
|
|
||||||
// Computed: has error status
|
// Computed: has error status
|
||||||
const hasError = computed(() => {
|
const hasError = computed(() => {
|
||||||
return props.account.status === 'error'
|
return props.account.status === 'error' || isKiroRuntimeSuspended.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const isQuotaExceeded = computed(() => {
|
const isQuotaExceeded = computed(() => {
|
||||||
@@ -296,7 +327,7 @@ const isQuotaExceeded = computed(() => {
|
|||||||
|
|
||||||
// Computed: countdown text for rate limit (429)
|
// Computed: countdown text for rate limit (429)
|
||||||
const rateLimitCountdown = computed(() => {
|
const rateLimitCountdown = computed(() => {
|
||||||
return formatCountdown(props.account.rate_limit_reset_at)
|
return formatCountdown(activeKiroRuntimeResetAt.value || props.account.rate_limit_reset_at)
|
||||||
})
|
})
|
||||||
|
|
||||||
const rateLimitResumeText = computed(() => {
|
const rateLimitResumeText = computed(() => {
|
||||||
@@ -309,8 +340,45 @@ const overloadCountdown = computed(() => {
|
|||||||
return formatCountdownWithSuffix(props.account.overload_until)
|
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
|
// Computed: status badge class
|
||||||
const statusClass = computed(() => {
|
const statusClass = computed(() => {
|
||||||
|
if (isKiroRuntimeSuspended.value) {
|
||||||
|
return 'badge-danger'
|
||||||
|
}
|
||||||
if (hasError.value) {
|
if (hasError.value) {
|
||||||
return 'badge-danger'
|
return 'badge-danger'
|
||||||
}
|
}
|
||||||
@@ -331,6 +399,9 @@ const statusClass = computed(() => {
|
|||||||
|
|
||||||
// Computed: status text
|
// Computed: status text
|
||||||
const statusText = computed(() => {
|
const statusText = computed(() => {
|
||||||
|
if (isKiroRuntimeSuspended.value) {
|
||||||
|
return t('admin.accounts.forbidden')
|
||||||
|
}
|
||||||
if (hasError.value) {
|
if (hasError.value) {
|
||||||
return t('admin.accounts.status.error')
|
return t('admin.accounts.status.error')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,6 +395,72 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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 -->
|
<!-- Other accounts: no usage window -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="text-xs text-gray-400">-</div>
|
<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 { t } = useI18n()
|
||||||
const desktopViewportQuery = '(min-width: 768px)'
|
const desktopViewportQuery = '(min-width: 768px)'
|
||||||
|
|
||||||
@@ -510,7 +580,9 @@ const error = ref<string | null>(null)
|
|||||||
const usageInfo = ref<AccountUsageInfo | null>(null)
|
const usageInfo = ref<AccountUsageInfo | null>(null)
|
||||||
const rootRef = ref<HTMLElement | null>(null)
|
const rootRef = ref<HTMLElement | null>(null)
|
||||||
const isDesktopViewport = ref(
|
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 hasEnteredViewport = ref(false)
|
||||||
const pendingAutoLoad = ref(false)
|
const pendingAutoLoad = ref(false)
|
||||||
@@ -531,6 +603,9 @@ const shouldFetchUsage = computed(() => {
|
|||||||
if (props.account.platform === 'anthropic') {
|
if (props.account.platform === 'anthropic') {
|
||||||
return props.account.type === 'oauth' || props.account.type === 'setup-token'
|
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') {
|
if (props.account.platform === 'gemini') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -984,6 +1059,179 @@ const isAnthropicOAuthOrSetupToken = computed(() => {
|
|||||||
return props.account.platform === 'anthropic' && (props.account.type === 'oauth' || props.account.type === 'setup-token')
|
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 }) => {
|
const loadUsage = async (options?: { source?: 'passive' | 'active'; bypassCache?: boolean }) => {
|
||||||
if (!shouldFetchUsage.value) return
|
if (!shouldFetchUsage.value) return
|
||||||
|
|
||||||
@@ -992,6 +1240,7 @@ const loadUsage = async (options?: { source?: 'passive' | 'active'; bypassCache?
|
|||||||
const cached = _usageCache.get(props.account.id)
|
const cached = _usageCache.get(props.account.id)
|
||||||
if (cached && Date.now() - cached.ts < USAGE_CACHE_TTL) {
|
if (cached && Date.now() - cached.ts < USAGE_CACHE_TTL) {
|
||||||
usageInfo.value = cached.data
|
usageInfo.value = cached.data
|
||||||
|
syncKiroUsageMeta(cached.data)
|
||||||
loading.value = false
|
loading.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1001,10 +1250,13 @@ const loadUsage = async (options?: { source?: 'passive' | 'active'; bypassCache?
|
|||||||
error.value = null
|
error.value = null
|
||||||
|
|
||||||
try {
|
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)
|
const result = await enqueueUsageRequest(props.account, fetchFn)
|
||||||
if (!unmounted.value) {
|
if (!unmounted.value) {
|
||||||
usageInfo.value = result
|
usageInfo.value = result
|
||||||
|
syncKiroUsageMeta(result)
|
||||||
_usageCache.set(props.account.id, { data: result, ts: Date.now() })
|
_usageCache.set(props.account.id, { data: result, ts: Date.now() })
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -1070,7 +1322,10 @@ const attachVisibilityObserver = () => {
|
|||||||
const loadActiveUsage = async () => {
|
const loadActiveUsage = async () => {
|
||||||
activeQueryLoading.value = true
|
activeQueryLoading.value = true
|
||||||
try {
|
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) {
|
} catch (e: any) {
|
||||||
console.error('Failed to load active usage:', e)
|
console.error('Failed to load active usage:', e)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1166,7 +1421,7 @@ const formatKeyUserCost = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
|
||||||
desktopViewportMediaQuery = window.matchMedia(desktopViewportQuery)
|
desktopViewportMediaQuery = window.matchMedia(desktopViewportQuery)
|
||||||
isDesktopViewport.value = desktopViewportMediaQuery.matches
|
isDesktopViewport.value = desktopViewportMediaQuery.matches
|
||||||
desktopViewportListener = (event: MediaQueryListEvent) => {
|
desktopViewportListener = (event: MediaQueryListEvent) => {
|
||||||
@@ -1180,15 +1435,17 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldAutoLoadUsageOnMount.value) return
|
if (!shouldAutoLoadUsageOnMount.value) return
|
||||||
const source = isAnthropicOAuthOrSetupToken.value ? 'passive' : undefined
|
requestAutoLoad(defaultUsageSource.value)
|
||||||
requestAutoLoad(source)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(openAIUsageRefreshKey, (nextKey, prevKey) => {
|
watch(openAIUsageRefreshKey, (nextKey, prevKey) => {
|
||||||
if (!prevKey || nextKey === prevKey) return
|
if (!prevKey || nextKey === prevKey) return
|
||||||
if (props.account.platform !== 'openai' || props.account.type !== 'oauth') 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(
|
watch(
|
||||||
@@ -1197,9 +1454,8 @@ watch(
|
|||||||
if (nextToken === prevToken) return
|
if (nextToken === prevToken) return
|
||||||
if (!shouldFetchUsage.value) return
|
if (!shouldFetchUsage.value) return
|
||||||
|
|
||||||
const source = isAnthropicOAuthOrSetupToken.value ? 'passive' : undefined
|
|
||||||
_usageCache.delete(props.account.id)
|
_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)
|
console.error('Failed to refresh usage after manual refresh:', e)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,6 +147,19 @@
|
|||||||
<Icon name="cloud" size="sm" />
|
<Icon name="cloud" size="sm" />
|
||||||
Antigravity
|
Antigravity
|
||||||
</button>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -774,6 +787,457 @@
|
|||||||
</div>
|
</div>
|
||||||
</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) -->
|
<!-- Upstream config (only for Antigravity upstream type) -->
|
||||||
<div v-if="form.platform === 'antigravity' && antigravityAccountType === 'upstream'" class="space-y-4">
|
<div v-if="form.platform === 'antigravity' && antigravityAccountType === 'upstream'" class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -1009,7 +1473,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Key input (only for apikey type, excluding Antigravity which has its own fields) -->
|
<!-- 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>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.accounts.baseUrl') }}</label>
|
<label class="input-label">{{ t('admin.accounts.baseUrl') }}</label>
|
||||||
<input
|
<input
|
||||||
@@ -2750,7 +3214,23 @@
|
|||||||
|
|
||||||
<!-- Step 2: OAuth Authorization -->
|
<!-- Step 2: OAuth Authorization -->
|
||||||
<div v-else class="space-y-5">
|
<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
|
<OAuthAuthorizationFlow
|
||||||
|
v-else
|
||||||
ref="oauthFlowRef"
|
ref="oauthFlowRef"
|
||||||
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
|
:add-method="form.platform === 'anthropic' ? addMethod : 'oauth'"
|
||||||
:auth-url="currentAuthUrl"
|
:auth-url="currentAuthUrl"
|
||||||
@@ -2822,7 +3302,16 @@
|
|||||||
{{ t('common.back') }}
|
{{ t('common.back') }}
|
||||||
</button>
|
</button>
|
||||||
<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"
|
type="button"
|
||||||
:disabled="!canExchangeCode"
|
:disabled="!canExchangeCode"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
@@ -3099,6 +3588,7 @@ import {
|
|||||||
commonErrorCodes,
|
commonErrorCodes,
|
||||||
buildModelMappingObject,
|
buildModelMappingObject,
|
||||||
fetchAntigravityDefaultMappings,
|
fetchAntigravityDefaultMappings,
|
||||||
|
fetchKiroDefaultMappings,
|
||||||
isValidWildcardPattern
|
isValidWildcardPattern
|
||||||
} from '@/composables/useModelWhitelist'
|
} from '@/composables/useModelWhitelist'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
@@ -3112,6 +3602,7 @@ import {
|
|||||||
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
||||||
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
||||||
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
||||||
|
import { useKiroOAuth } from '@/composables/useKiroOAuth'
|
||||||
import type {
|
import type {
|
||||||
Proxy,
|
Proxy,
|
||||||
AdminGroup,
|
AdminGroup,
|
||||||
@@ -3148,6 +3639,8 @@ import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
|||||||
interface OAuthFlowExposed {
|
interface OAuthFlowExposed {
|
||||||
authCode: string
|
authCode: string
|
||||||
oauthState: string
|
oauthState: string
|
||||||
|
oauthCallbackPath: string
|
||||||
|
oauthLoginOption: string
|
||||||
projectId: string
|
projectId: string
|
||||||
sessionKey: string
|
sessionKey: string
|
||||||
refreshToken: string
|
refreshToken: string
|
||||||
@@ -3163,6 +3656,11 @@ const oauthStepTitle = computed(() => {
|
|||||||
if (form.platform === 'openai') return t('admin.accounts.oauth.openai.title')
|
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 === 'gemini') return t('admin.accounts.oauth.gemini.title')
|
||||||
if (form.platform === 'antigravity') return t('admin.accounts.oauth.antigravity.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')
|
return t('admin.accounts.oauth.title')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3170,12 +3668,14 @@ const oauthStepTitle = computed(() => {
|
|||||||
const baseUrlHint = computed(() => {
|
const baseUrlHint = computed(() => {
|
||||||
if (form.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
|
if (form.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
|
||||||
if (form.platform === 'gemini') return t('admin.accounts.gemini.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')
|
return t('admin.accounts.baseUrlHint')
|
||||||
})
|
})
|
||||||
|
|
||||||
const apiKeyHint = computed(() => {
|
const apiKeyHint = computed(() => {
|
||||||
if (form.platform === 'openai') return t('admin.accounts.openai.apiKeyHint')
|
if (form.platform === 'openai') return t('admin.accounts.openai.apiKeyHint')
|
||||||
if (form.platform === 'gemini') return t('admin.accounts.gemini.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')
|
return t('admin.accounts.apiKeyHint')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3198,12 +3698,14 @@ const oauth = useAccountOAuth() // For Anthropic OAuth
|
|||||||
const openaiOAuth = useOpenAIOAuth() // For OpenAI OAuth
|
const openaiOAuth = useOpenAIOAuth() // For OpenAI OAuth
|
||||||
const geminiOAuth = useGeminiOAuth() // For Gemini OAuth
|
const geminiOAuth = useGeminiOAuth() // For Gemini OAuth
|
||||||
const antigravityOAuth = useAntigravityOAuth() // For Antigravity OAuth
|
const antigravityOAuth = useAntigravityOAuth() // For Antigravity OAuth
|
||||||
|
const kiroOAuth = useKiroOAuth() // For Kiro OAuth / IDC
|
||||||
|
|
||||||
// Computed: current OAuth state for template binding
|
// Computed: current OAuth state for template binding
|
||||||
const currentAuthUrl = computed(() => {
|
const currentAuthUrl = computed(() => {
|
||||||
if (form.platform === 'openai') return openaiOAuth.authUrl.value
|
if (form.platform === 'openai') return openaiOAuth.authUrl.value
|
||||||
if (form.platform === 'gemini') return geminiOAuth.authUrl.value
|
if (form.platform === 'gemini') return geminiOAuth.authUrl.value
|
||||||
if (form.platform === 'antigravity') return antigravityOAuth.authUrl.value
|
if (form.platform === 'antigravity') return antigravityOAuth.authUrl.value
|
||||||
|
if (form.platform === 'kiro') return kiroOAuth.authUrl.value
|
||||||
return oauth.authUrl.value
|
return oauth.authUrl.value
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3211,6 +3713,7 @@ const currentSessionId = computed(() => {
|
|||||||
if (form.platform === 'openai') return openaiOAuth.sessionId.value
|
if (form.platform === 'openai') return openaiOAuth.sessionId.value
|
||||||
if (form.platform === 'gemini') return geminiOAuth.sessionId.value
|
if (form.platform === 'gemini') return geminiOAuth.sessionId.value
|
||||||
if (form.platform === 'antigravity') return antigravityOAuth.sessionId.value
|
if (form.platform === 'antigravity') return antigravityOAuth.sessionId.value
|
||||||
|
if (form.platform === 'kiro') return kiroOAuth.sessionId.value
|
||||||
return oauth.sessionId.value
|
return oauth.sessionId.value
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3218,6 +3721,7 @@ const currentOAuthLoading = computed(() => {
|
|||||||
if (form.platform === 'openai') return openaiOAuth.loading.value
|
if (form.platform === 'openai') return openaiOAuth.loading.value
|
||||||
if (form.platform === 'gemini') return geminiOAuth.loading.value
|
if (form.platform === 'gemini') return geminiOAuth.loading.value
|
||||||
if (form.platform === 'antigravity') return antigravityOAuth.loading.value
|
if (form.platform === 'antigravity') return antigravityOAuth.loading.value
|
||||||
|
if (form.platform === 'kiro') return kiroOAuth.loading.value
|
||||||
return oauth.loading.value
|
return oauth.loading.value
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3225,6 +3729,7 @@ const currentOAuthError = computed(() => {
|
|||||||
if (form.platform === 'openai') return openaiOAuth.error.value
|
if (form.platform === 'openai') return openaiOAuth.error.value
|
||||||
if (form.platform === 'gemini') return geminiOAuth.error.value
|
if (form.platform === 'gemini') return geminiOAuth.error.value
|
||||||
if (form.platform === 'antigravity') return antigravityOAuth.error.value
|
if (form.platform === 'antigravity') return antigravityOAuth.error.value
|
||||||
|
if (form.platform === 'kiro') return kiroOAuth.error.value
|
||||||
return oauth.error.value
|
return oauth.error.value
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3303,6 +3808,14 @@ const antigravityModelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist'
|
|||||||
const antigravityWhitelistModels = ref<string[]>([])
|
const antigravityWhitelistModels = ref<string[]>([])
|
||||||
const antigravityModelMappings = ref<ModelMapping[]>([])
|
const antigravityModelMappings = ref<ModelMapping[]>([])
|
||||||
const antigravityPresetMappings = computed(() => getPresetMappingsByPlatform('antigravity'))
|
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'))
|
const bedrockPresets = computed(() => getPresetMappingsByPlatform('bedrock'))
|
||||||
|
|
||||||
// Bedrock credentials
|
// Bedrock credentials
|
||||||
@@ -3324,6 +3837,7 @@ const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
|||||||
const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-model-mapping')
|
const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-model-mapping')
|
||||||
const getOpenAICompactModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-openai-compact-model-mapping')
|
const getOpenAICompactModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-openai-compact-model-mapping')
|
||||||
const getAntigravityModelMappingKey = createStableObjectKeyResolver<ModelMapping>('create-antigravity-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 getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>('create-temp-unsched-rule')
|
||||||
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one')
|
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one')
|
||||||
const geminiAIStudioOAuthEnabled = ref(false)
|
const geminiAIStudioOAuthEnabled = ref(false)
|
||||||
@@ -3509,6 +4023,8 @@ const isOAuthFlow = computed(() => {
|
|||||||
return accountCategory.value === 'oauth-based'
|
return accountCategory.value === 'oauth-based'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isKiroImportMode = computed(() => form.platform === 'kiro' && kiroAccountType.value === 'import')
|
||||||
|
|
||||||
const isManualInputMethod = computed(() => {
|
const isManualInputMethod = computed(() => {
|
||||||
return oauthFlowRef.value?.inputMethod === 'manual'
|
return oauthFlowRef.value?.inputMethod === 'manual'
|
||||||
})
|
})
|
||||||
@@ -3531,6 +4047,9 @@ const canExchangeCode = computed(() => {
|
|||||||
if (form.platform === 'antigravity') {
|
if (form.platform === 'antigravity') {
|
||||||
return authCode.trim() && antigravityOAuth.sessionId.value && !antigravityOAuth.loading.value
|
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
|
return authCode.trim() && oauth.sessionId.value && !oauth.loading.value
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3552,10 +4071,15 @@ watch(
|
|||||||
antigravityModelMappings.value = [...mappings]
|
antigravityModelMappings.value = [...mappings]
|
||||||
})
|
})
|
||||||
antigravityWhitelistModels.value = []
|
antigravityWhitelistModels.value = []
|
||||||
|
} else if (form.platform === 'kiro') {
|
||||||
|
fetchKiroDefaultMappings().then(mappings => {
|
||||||
|
kiroModelMappings.value = [...mappings]
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
antigravityWhitelistModels.value = []
|
antigravityWhitelistModels.value = []
|
||||||
antigravityModelMappings.value = []
|
antigravityModelMappings.value = []
|
||||||
antigravityModelRestrictionMode.value = 'mapping'
|
antigravityModelRestrictionMode.value = 'mapping'
|
||||||
|
kiroModelMappings.value = []
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resetForm()
|
resetForm()
|
||||||
@@ -3572,6 +4096,10 @@ watch(
|
|||||||
form.type = 'apikey'
|
form.type = 'apikey'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (form.platform === 'kiro') {
|
||||||
|
form.type = category === 'oauth-based' ? 'oauth' : 'apikey'
|
||||||
|
return
|
||||||
|
}
|
||||||
// Bedrock 类型
|
// Bedrock 类型
|
||||||
if (form.platform === 'anthropic' && category === 'bedrock') {
|
if (form.platform === 'anthropic' && category === 'bedrock') {
|
||||||
form.type = 'bedrock' as AccountType
|
form.type = 'bedrock' as AccountType
|
||||||
@@ -3598,6 +4126,8 @@ watch(
|
|||||||
? 'https://api.openai.com'
|
? 'https://api.openai.com'
|
||||||
: newPlatform === 'gemini'
|
: newPlatform === 'gemini'
|
||||||
? 'https://generativelanguage.googleapis.com'
|
? 'https://generativelanguage.googleapis.com'
|
||||||
|
: newPlatform === 'kiro'
|
||||||
|
? ''
|
||||||
: 'https://api.anthropic.com'
|
: 'https://api.anthropic.com'
|
||||||
// Clear model-related settings
|
// Clear model-related settings
|
||||||
allowedModels.value = []
|
allowedModels.value = []
|
||||||
@@ -3611,11 +4141,21 @@ watch(
|
|||||||
antigravityWhitelistModels.value = []
|
antigravityWhitelistModels.value = []
|
||||||
accountCategory.value = 'oauth-based'
|
accountCategory.value = 'oauth-based'
|
||||||
antigravityAccountType.value = 'oauth'
|
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 {
|
} else {
|
||||||
allowOverages.value = false
|
allowOverages.value = false
|
||||||
antigravityWhitelistModels.value = []
|
antigravityWhitelistModels.value = []
|
||||||
antigravityModelMappings.value = []
|
antigravityModelMappings.value = []
|
||||||
antigravityModelRestrictionMode.value = 'mapping'
|
antigravityModelRestrictionMode.value = 'mapping'
|
||||||
|
kiroModelMappings.value = []
|
||||||
}
|
}
|
||||||
if (newPlatform !== 'gemini' && newPlatform !== 'anthropic' && accountCategory.value === 'service_account') {
|
if (newPlatform !== 'gemini' && newPlatform !== 'anthropic' && accountCategory.value === 'service_account') {
|
||||||
accountCategory.value = 'oauth-based'
|
accountCategory.value = 'oauth-based'
|
||||||
@@ -3655,6 +4195,7 @@ watch(
|
|||||||
|
|
||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
antigravityOAuth.resetState()
|
antigravityOAuth.resetState()
|
||||||
|
kiroOAuth.resetState()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3756,6 +4297,22 @@ const addAntigravityPresetMapping = (from: string, to: string) => {
|
|||||||
antigravityModelMappings.value.push({ from, to })
|
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
|
// Error code toggle helper
|
||||||
const toggleErrorCode = (code: number) => {
|
const toggleErrorCode = (code: number) => {
|
||||||
const index = selectedErrorCodes.value.indexOf(code)
|
const index = selectedErrorCodes.value.indexOf(code)
|
||||||
@@ -4029,6 +4586,15 @@ const resetForm = () => {
|
|||||||
fetchAntigravityDefaultMappings().then(mappings => {
|
fetchAntigravityDefaultMappings().then(mappings => {
|
||||||
antigravityModelMappings.value = [...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
|
poolModeEnabled.value = false
|
||||||
poolModeRetryCount.value = DEFAULT_POOL_MODE_RETRY_COUNT
|
poolModeRetryCount.value = DEFAULT_POOL_MODE_RETRY_COUNT
|
||||||
customErrorCodesEnabled.value = false
|
customErrorCodesEnabled.value = false
|
||||||
@@ -4080,6 +4646,7 @@ const resetForm = () => {
|
|||||||
openaiOAuth.resetState()
|
openaiOAuth.resetState()
|
||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
antigravityOAuth.resetState()
|
antigravityOAuth.resetState()
|
||||||
|
kiroOAuth.resetState()
|
||||||
oauthFlowRef.value?.reset()
|
oauthFlowRef.value?.reset()
|
||||||
antigravityMixedChannelConfirmed.value = false
|
antigravityMixedChannelConfirmed.value = false
|
||||||
clearMixedChannelDialog()
|
clearMixedChannelDialog()
|
||||||
@@ -4375,6 +4942,45 @@ const handleSubmit = async () => {
|
|||||||
return
|
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
|
// For apikey type, create directly
|
||||||
if (!apiKeyValue.value.trim()) {
|
if (!apiKeyValue.value.trim()) {
|
||||||
appStore.showError(t('admin.accounts.pleaseEnterApiKey'))
|
appStore.showError(t('admin.accounts.pleaseEnterApiKey'))
|
||||||
@@ -4446,6 +5052,7 @@ const goBackToBasicInfo = () => {
|
|||||||
openaiOAuth.resetState()
|
openaiOAuth.resetState()
|
||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
antigravityOAuth.resetState()
|
antigravityOAuth.resetState()
|
||||||
|
kiroOAuth.resetState()
|
||||||
oauthFlowRef.value?.reset()
|
oauthFlowRef.value?.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4461,6 +5068,19 @@ const handleGenerateUrl = async () => {
|
|||||||
)
|
)
|
||||||
} else if (form.platform === 'antigravity') {
|
} else if (form.platform === 'antigravity') {
|
||||||
await antigravityOAuth.generateAuthUrl(form.proxy_id)
|
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 {
|
} else {
|
||||||
await oauth.generateAuthUrl(addMethod.value, form.proxy_id)
|
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 授权码兑换
|
// Anthropic OAuth 授权码兑换
|
||||||
const handleAnthropicExchange = async (authCode: string) => {
|
const handleAnthropicExchange = async (authCode: string) => {
|
||||||
if (!authCode.trim() || !oauth.sessionId.value) return
|
if (!authCode.trim() || !oauth.sessionId.value) return
|
||||||
@@ -5022,6 +5686,8 @@ const handleExchangeCode = async () => {
|
|||||||
return handleOpenAIExchange(authCode)
|
return handleOpenAIExchange(authCode)
|
||||||
case 'gemini':
|
case 'gemini':
|
||||||
return handleGeminiExchange(authCode)
|
return handleGeminiExchange(authCode)
|
||||||
|
case 'kiro':
|
||||||
|
return handleKiroExchange(authCode)
|
||||||
case 'antigravity':
|
case 'antigravity':
|
||||||
return handleAntigravityExchange(authCode)
|
return handleAntigravityExchange(authCode)
|
||||||
default:
|
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) => {
|
const handleCookieAuth = async (sessionKey: string) => {
|
||||||
oauth.loading.value = true
|
oauth.loading.value = true
|
||||||
oauth.error.value = ''
|
oauth.error.value = ''
|
||||||
|
|||||||
@@ -39,6 +39,8 @@
|
|||||||
? 'https://api.openai.com'
|
? 'https://api.openai.com'
|
||||||
: account.platform === 'gemini'
|
: account.platform === 'gemini'
|
||||||
? 'https://generativelanguage.googleapis.com'
|
? 'https://generativelanguage.googleapis.com'
|
||||||
|
: account.platform === 'kiro'
|
||||||
|
? 'https://your-kiro-upstream.example.com'
|
||||||
: account.platform === 'antigravity'
|
: account.platform === 'antigravity'
|
||||||
? 'https://cloudcode-pa.googleapis.com'
|
? 'https://cloudcode-pa.googleapis.com'
|
||||||
: 'https://api.anthropic.com'
|
: 'https://api.anthropic.com'
|
||||||
@@ -61,6 +63,8 @@
|
|||||||
? 'sk-proj-...'
|
? 'sk-proj-...'
|
||||||
: account.platform === 'gemini'
|
: account.platform === 'gemini'
|
||||||
? 'AIza...'
|
? 'AIza...'
|
||||||
|
: account.platform === 'kiro'
|
||||||
|
? 'sk-...'
|
||||||
: account.platform === 'antigravity'
|
: account.platform === 'antigravity'
|
||||||
? 'sk-...'
|
? 'sk-...'
|
||||||
: 'sk-ant-...'
|
: 'sk-ant-...'
|
||||||
@@ -69,8 +73,93 @@
|
|||||||
<p class="input-hint">{{ t('admin.accounts.leaveEmptyToKeep') }}</p>
|
<p class="input-hint">{{ t('admin.accounts.leaveEmptyToKeep') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Model Restriction Section (不适用于 Antigravity) -->
|
<div v-if="account.platform === 'kiro'" class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
<div v-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 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>
|
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -407,9 +496,9 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OpenAI OAuth Model Mapping (OAuth 类型没有 apikey 容器,需要独立的模型映射区域) -->
|
<!-- OpenAI / Kiro OAuth Model Restriction (OAuth 类型没有 apikey 容器,需要独立区域) -->
|
||||||
<div
|
<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"
|
class="border-t border-gray-200 pt-4 dark:border-dark-600"
|
||||||
>
|
>
|
||||||
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
<label class="input-label">{{ t('admin.accounts.modelRestriction') }}</label>
|
||||||
@@ -423,6 +512,82 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
<template v-else>
|
||||||
<!-- Mode Toggle -->
|
<!-- Mode Toggle -->
|
||||||
<div class="mb-4 flex gap-2">
|
<div class="mb-4 flex gap-2">
|
||||||
@@ -2145,6 +2310,7 @@ import {
|
|||||||
resolveOpenAIWSModeFromExtra
|
resolveOpenAIWSModeFromExtra
|
||||||
} from '@/utils/openaiWsMode'
|
} from '@/utils/openaiWsMode'
|
||||||
import {
|
import {
|
||||||
|
fetchKiroDefaultMappings,
|
||||||
getPresetMappingsByPlatform,
|
getPresetMappingsByPlatform,
|
||||||
commonErrorCodes,
|
commonErrorCodes,
|
||||||
buildModelMappingObject,
|
buildModelMappingObject,
|
||||||
@@ -2173,11 +2339,13 @@ const baseUrlHint = computed(() => {
|
|||||||
if (!props.account) return t('admin.accounts.baseUrlHint')
|
if (!props.account) return t('admin.accounts.baseUrlHint')
|
||||||
if (props.account.platform === 'openai') return t('admin.accounts.openai.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 === 'gemini') return t('admin.accounts.gemini.baseUrlHint')
|
||||||
|
if (props.account.platform === 'kiro') return t('admin.accounts.kiro.baseUrlHint')
|
||||||
return t('admin.accounts.baseUrlHint')
|
return t('admin.accounts.baseUrlHint')
|
||||||
})
|
})
|
||||||
|
|
||||||
const antigravityPresetMappings = computed(() => getPresetMappingsByPlatform('antigravity'))
|
const antigravityPresetMappings = computed(() => getPresetMappingsByPlatform('antigravity'))
|
||||||
const bedrockPresets = computed(() => getPresetMappingsByPlatform('bedrock'))
|
const bedrockPresets = computed(() => getPresetMappingsByPlatform('bedrock'))
|
||||||
|
const isKiroOAuthAccount = computed(() => props.account?.platform === 'kiro' && props.account?.type === 'oauth')
|
||||||
|
|
||||||
// Model mapping type
|
// Model mapping type
|
||||||
interface ModelMapping {
|
interface ModelMapping {
|
||||||
@@ -2235,6 +2403,21 @@ const getOpenAICompactModelMappingKey = createStableObjectKeyResolver<ModelMappi
|
|||||||
const getAntigravityModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-antigravity-model-mapping')
|
const getAntigravityModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-antigravity-model-mapping')
|
||||||
const getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>('edit-temp-unsched-rule')
|
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 showMixedChannelWarning = ref(false)
|
||||||
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(
|
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(
|
||||||
null
|
null
|
||||||
@@ -2383,6 +2566,7 @@ const tempUnschedPresets = computed(() => [
|
|||||||
const defaultBaseUrl = computed(() => {
|
const defaultBaseUrl = computed(() => {
|
||||||
if (props.account?.platform === 'openai') return 'https://api.openai.com'
|
if (props.account?.platform === 'openai') return 'https://api.openai.com'
|
||||||
if (props.account?.platform === 'gemini') return 'https://generativelanguage.googleapis.com'
|
if (props.account?.platform === 'gemini') return 'https://generativelanguage.googleapis.com'
|
||||||
|
if (props.account?.platform === 'kiro') return ''
|
||||||
return 'https://api.anthropic.com'
|
return 'https://api.anthropic.com'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2597,6 +2781,8 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
|||||||
? 'https://api.openai.com'
|
? 'https://api.openai.com'
|
||||||
: newAccount.platform === 'gemini'
|
: newAccount.platform === 'gemini'
|
||||||
? 'https://generativelanguage.googleapis.com'
|
? 'https://generativelanguage.googleapis.com'
|
||||||
|
: newAccount.platform === 'kiro'
|
||||||
|
? ''
|
||||||
: 'https://api.anthropic.com'
|
: 'https://api.anthropic.com'
|
||||||
editBaseUrl.value = (credentials.base_url as string) || platformDefaultUrl
|
editBaseUrl.value = (credentials.base_url as string) || platformDefaultUrl
|
||||||
|
|
||||||
@@ -2605,20 +2791,35 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
|||||||
if (existingMappings && typeof existingMappings === 'object') {
|
if (existingMappings && typeof existingMappings === 'object') {
|
||||||
const entries = Object.entries(existingMappings)
|
const entries = Object.entries(existingMappings)
|
||||||
|
|
||||||
// Detect if this is whitelist mode (all from === to) or mapping mode
|
if (newAccount.platform === 'kiro') {
|
||||||
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'
|
modelRestrictionMode.value = 'mapping'
|
||||||
modelMappings.value = entries.map(([from, to]) => ({ from, to }))
|
modelMappings.value = entries.map(([from, to]) => ({ from, to }))
|
||||||
allowedModels.value = []
|
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 {
|
} else {
|
||||||
// No mappings: default to whitelist mode with empty selection (allow all)
|
// No mappings: default to whitelist mode with empty selection (allow all)
|
||||||
modelRestrictionMode.value = 'whitelist'
|
modelRestrictionMode.value = 'whitelist'
|
||||||
@@ -2673,15 +2874,18 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
|||||||
const entries = Object.entries(existingMappings)
|
const entries = Object.entries(existingMappings)
|
||||||
const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to)
|
const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to)
|
||||||
if (isWhitelistMode) {
|
if (isWhitelistMode) {
|
||||||
|
// Whitelist mode: populate allowedModels
|
||||||
modelRestrictionMode.value = 'whitelist'
|
modelRestrictionMode.value = 'whitelist'
|
||||||
allowedModels.value = entries.map(([from]) => from)
|
allowedModels.value = entries.map(([from]) => from)
|
||||||
modelMappings.value = []
|
modelMappings.value = []
|
||||||
} else {
|
} else {
|
||||||
|
// Mapping mode: populate modelMappings
|
||||||
modelRestrictionMode.value = 'mapping'
|
modelRestrictionMode.value = 'mapping'
|
||||||
modelMappings.value = entries.map(([from, to]) => ({ from, to }))
|
modelMappings.value = entries.map(([from, to]) => ({ from, to }))
|
||||||
allowedModels.value = []
|
allowedModels.value = []
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// No mappings: default to whitelist mode with empty selection (allow all)
|
||||||
modelRestrictionMode.value = 'whitelist'
|
modelRestrictionMode.value = 'whitelist'
|
||||||
modelMappings.value = []
|
modelMappings.value = []
|
||||||
allowedModels.value = []
|
allowedModels.value = []
|
||||||
@@ -2723,8 +2927,16 @@ const syncFormFromAccount = (newAccount: Account | null) => {
|
|||||||
: 'https://api.anthropic.com'
|
: 'https://api.anthropic.com'
|
||||||
editBaseUrl.value = platformDefaultUrl
|
editBaseUrl.value = platformDefaultUrl
|
||||||
|
|
||||||
// Load model mappings for OpenAI OAuth accounts
|
// Load model mappings for OpenAI/Kiro OAuth accounts
|
||||||
if (newAccount.platform === 'openai' && newAccount.credentials) {
|
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 oauthCredentials = newAccount.credentials as Record<string, unknown>
|
||||||
const existingMappings = oauthCredentials.model_mapping as Record<string, string> | undefined
|
const existingMappings = oauthCredentials.model_mapping as Record<string, string> | undefined
|
||||||
if (existingMappings && typeof existingMappings === 'object') {
|
if (existingMappings && typeof existingMappings === 'object') {
|
||||||
@@ -2780,6 +2992,7 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// Model mapping helpers
|
// Model mapping helpers
|
||||||
const addModelMapping = () => {
|
const addModelMapping = () => {
|
||||||
modelMappings.value.push({ from: '', to: '' })
|
modelMappings.value.push({ from: '', to: '' })
|
||||||
@@ -3221,9 +3434,16 @@ const handleSubmit = async () => {
|
|||||||
// For apikey type, handle credentials update
|
// For apikey type, handle credentials update
|
||||||
if (props.account.type === 'apikey') {
|
if (props.account.type === 'apikey') {
|
||||||
const currentCredentials = (props.account.credentials as Record<string, unknown>) || {}
|
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)
|
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
|
// Always update credentials for apikey type to handle model mapping changes
|
||||||
const newCredentials: Record<string, unknown> = {
|
const newCredentials: Record<string, unknown> = {
|
||||||
...currentCredentials,
|
...currentCredentials,
|
||||||
@@ -3244,7 +3464,11 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
// Add model mapping if configured(OpenAI 开启自动透传时保留现有映射,不再编辑)
|
// Add model mapping if configured(OpenAI 开启自动透传时保留现有映射,不再编辑)
|
||||||
if (shouldApplyModelMapping) {
|
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) {
|
if (modelMapping) {
|
||||||
newCredentials.model_mapping = modelMapping
|
newCredentials.model_mapping = modelMapping
|
||||||
} else {
|
} else {
|
||||||
@@ -3382,7 +3606,7 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Model mapping
|
// Model mapping
|
||||||
const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
|
const modelMapping = buildModelMappingObject('mapping', [], modelMappings.value)
|
||||||
if (modelMapping) {
|
if (modelMapping) {
|
||||||
newCredentials.model_mapping = modelMapping
|
newCredentials.model_mapping = modelMapping
|
||||||
} else {
|
} else {
|
||||||
@@ -3436,6 +3660,22 @@ const handleSubmit = async () => {
|
|||||||
updatePayload.credentials = newCredentials
|
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: persist model mapping to credentials (applies to all antigravity types)
|
||||||
// Antigravity 只支持映射模式
|
// Antigravity 只支持映射模式
|
||||||
if (props.account.platform === '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 === 'openai') return `admin.accounts.oauth.openai.${key}`
|
||||||
if (props.platform === 'gemini') return `admin.accounts.oauth.gemini.${key}`
|
if (props.platform === 'gemini') return `admin.accounts.oauth.gemini.${key}`
|
||||||
if (props.platform === 'antigravity') return `admin.accounts.oauth.antigravity.${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}`
|
return `admin.accounts.oauth.${key}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,6 +633,8 @@ const refreshTokenInput = ref('')
|
|||||||
const sessionTokenInput = ref('')
|
const sessionTokenInput = ref('')
|
||||||
const showHelpDialog = ref(false)
|
const showHelpDialog = ref(false)
|
||||||
const oauthState = ref('')
|
const oauthState = ref('')
|
||||||
|
const oauthCallbackPath = ref('')
|
||||||
|
const oauthLoginOption = ref('')
|
||||||
const projectId = ref('')
|
const projectId = ref('')
|
||||||
|
|
||||||
// Computed: show method selection when either cookie or refresh token option is enabled
|
// Computed: show method selection when either cookie or refresh token option is enabled
|
||||||
@@ -661,10 +664,10 @@ watch(inputMethod, (newVal) => {
|
|||||||
emit('update: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=...
|
// e.g., http://localhost:8085/callback?code=xxx...&state=...
|
||||||
watch(authCodeInput, (newVal) => {
|
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()
|
const trimmed = newVal.trim()
|
||||||
// Check if it looks like a URL with code parameter
|
// Check if it looks like a URL with code parameter
|
||||||
@@ -674,7 +677,11 @@ watch(authCodeInput, (newVal) => {
|
|||||||
const url = new URL(trimmed)
|
const url = new URL(trimmed)
|
||||||
const code = url.searchParams.get('code')
|
const code = url.searchParams.get('code')
|
||||||
const stateParam = url.searchParams.get('state')
|
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
|
oauthState.value = stateParam
|
||||||
}
|
}
|
||||||
if (code && code !== trimmed) {
|
if (code && code !== trimmed) {
|
||||||
@@ -685,7 +692,13 @@ watch(authCodeInput, (newVal) => {
|
|||||||
// If URL parsing fails, try regex extraction
|
// If URL parsing fails, try regex extraction
|
||||||
const match = trimmed.match(/[?&]code=([^&]+)/)
|
const match = trimmed.match(/[?&]code=([^&]+)/)
|
||||||
const stateMatch = trimmed.match(/[?&]state=([^&]+)/)
|
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]
|
oauthState.value = stateMatch[1]
|
||||||
}
|
}
|
||||||
if (match && match[1] && match[1] !== trimmed) {
|
if (match && match[1] && match[1] !== trimmed) {
|
||||||
@@ -731,6 +744,8 @@ const handleValidateRefreshToken = () => {
|
|||||||
defineExpose({
|
defineExpose({
|
||||||
authCode: authCodeInput,
|
authCode: authCodeInput,
|
||||||
oauthState,
|
oauthState,
|
||||||
|
oauthCallbackPath,
|
||||||
|
oauthLoginOption,
|
||||||
projectId,
|
projectId,
|
||||||
sessionKey: sessionKeyInput,
|
sessionKey: sessionKeyInput,
|
||||||
refreshToken: refreshTokenInput,
|
refreshToken: refreshTokenInput,
|
||||||
@@ -739,6 +754,8 @@ defineExpose({
|
|||||||
reset: () => {
|
reset: () => {
|
||||||
authCodeInput.value = ''
|
authCodeInput.value = ''
|
||||||
oauthState.value = ''
|
oauthState.value = ''
|
||||||
|
oauthCallbackPath.value = ''
|
||||||
|
oauthLoginOption.value = ''
|
||||||
projectId.value = ''
|
projectId.value = ''
|
||||||
sessionKeyInput.value = ''
|
sessionKeyInput.value = ''
|
||||||
refreshTokenInput.value = ''
|
refreshTokenInput.value = ''
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const updateType = (value: string | number | boolean | null) => { emit('update:f
|
|||||||
const updateStatus = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, status: value }) }
|
const updateStatus = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, status: value }) }
|
||||||
const updatePrivacyMode = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, privacy_mode: value }) }
|
const updatePrivacyMode = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, privacy_mode: value }) }
|
||||||
const updateGroup = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, group: value }) }
|
const updateGroup = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, group: value }) }
|
||||||
const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }])
|
const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }, { value: 'kiro', label: 'Kiro' }])
|
||||||
const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }, { value: 'bedrock', label: 'AWS Bedrock' }])
|
const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }, { value: 'bedrock', label: 'AWS Bedrock' }])
|
||||||
const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }, { value: 'rate_limited', label: t('admin.accounts.status.rateLimited') }, { value: 'temp_unschedulable', label: t('admin.accounts.status.tempUnschedulable') }, { value: 'unschedulable', label: t('admin.accounts.status.unschedulable') }])
|
const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }, { value: 'rate_limited', label: t('admin.accounts.status.rateLimited') }, { value: 'temp_unschedulable', label: t('admin.accounts.status.tempUnschedulable') }, { value: 'unschedulable', label: t('admin.accounts.status.unschedulable') }])
|
||||||
const privacyOpts = computed(() => [
|
const privacyOpts = computed(() => [
|
||||||
|
|||||||
@@ -2,14 +2,11 @@
|
|||||||
<BaseDialog
|
<BaseDialog
|
||||||
:show="show"
|
:show="show"
|
||||||
:title="t('admin.accounts.reAuthorizeAccount')"
|
:title="t('admin.accounts.reAuthorizeAccount')"
|
||||||
width="normal"
|
:width="isKiro ? 'wide' : 'normal'"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
<div v-if="account" class="space-y-4">
|
<div v-if="account" class="space-y-4">
|
||||||
<!-- Account Info -->
|
<div class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700">
|
||||||
<div
|
|
||||||
class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
@@ -18,33 +15,34 @@
|
|||||||
? 'from-green-500 to-green-600'
|
? 'from-green-500 to-green-600'
|
||||||
: isGemini
|
: isGemini
|
||||||
? 'from-blue-500 to-blue-600'
|
? 'from-blue-500 to-blue-600'
|
||||||
: isAntigravity
|
: isKiro
|
||||||
? 'from-purple-500 to-purple-600'
|
? 'from-amber-500 to-amber-600'
|
||||||
: 'from-orange-500 to-orange-600'
|
: isAntigravity
|
||||||
|
? 'from-purple-500 to-purple-600'
|
||||||
|
: 'from-orange-500 to-orange-600'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<Icon name="sparkles" size="md" class="text-white" />
|
<Icon name="sparkles" size="md" class="text-white" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="block font-semibold text-gray-900 dark:text-white">{{
|
<span class="block font-semibold text-gray-900 dark:text-white">{{ account.name }}</span>
|
||||||
account.name
|
|
||||||
}}</span>
|
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{
|
{{
|
||||||
isOpenAI
|
isOpenAI
|
||||||
? t('admin.accounts.openaiAccount')
|
? t('admin.accounts.openaiAccount')
|
||||||
: isGemini
|
: isGemini
|
||||||
? t('admin.accounts.geminiAccount')
|
? t('admin.accounts.geminiAccount')
|
||||||
: isAntigravity
|
: isKiro
|
||||||
? t('admin.accounts.antigravityAccount')
|
? t('admin.accounts.kiroAccount')
|
||||||
: t('admin.accounts.claudeCodeAccount')
|
: isAntigravity
|
||||||
|
? t('admin.accounts.antigravityAccount')
|
||||||
|
: t('admin.accounts.claudeCodeAccount')
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Method Selection (Claude only) -->
|
|
||||||
<fieldset v-if="isAnthropic" class="border-0 p-0">
|
<fieldset v-if="isAnthropic" class="border-0 p-0">
|
||||||
<legend class="input-label">{{ t('admin.accounts.oauth.authMethod') }}</legend>
|
<legend class="input-label">{{ t('admin.accounts.oauth.authMethod') }}</legend>
|
||||||
<div class="mt-2 flex gap-4">
|
<div class="mt-2 flex gap-4">
|
||||||
@@ -55,9 +53,7 @@
|
|||||||
value="oauth"
|
value="oauth"
|
||||||
class="mr-2 text-primary-600 focus:ring-primary-500"
|
class="mr-2 text-primary-600 focus:ring-primary-500"
|
||||||
/>
|
/>
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('admin.accounts.types.oauth') }}</span>
|
||||||
t('admin.accounts.types.oauth')
|
|
||||||
}}</span>
|
|
||||||
</label>
|
</label>
|
||||||
<label class="flex cursor-pointer items-center">
|
<label class="flex cursor-pointer items-center">
|
||||||
<input
|
<input
|
||||||
@@ -66,14 +62,11 @@
|
|||||||
value="setup-token"
|
value="setup-token"
|
||||||
class="mr-2 text-primary-600 focus:ring-primary-500"
|
class="mr-2 text-primary-600 focus:ring-primary-500"
|
||||||
/>
|
/>
|
||||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{
|
<span class="text-sm text-gray-700 dark:text-gray-300">{{ t('admin.accounts.setupTokenLongLived') }}</span>
|
||||||
t('admin.accounts.setupTokenLongLived')
|
|
||||||
}}</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- Gemini OAuth Type Display (read-only) -->
|
|
||||||
<div v-if="isGemini" class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700">
|
<div v-if="isGemini" class="rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-dark-600 dark:bg-dark-700">
|
||||||
<div class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
<div class="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
{{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }}
|
{{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }}
|
||||||
@@ -116,7 +109,187 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isKiro" class="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-700/40 dark:bg-amber-900/20">
|
||||||
|
<div class="mb-3 text-sm font-medium text-amber-900 dark:text-amber-100">
|
||||||
|
{{ t('admin.accounts.oauth.kiro.authModeTitle') }}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="kiroAccountType = 'oauth'"
|
||||||
|
:class="kiroModeClass('oauth')"
|
||||||
|
>
|
||||||
|
<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="kiroModeClass('idc')"
|
||||||
|
>
|
||||||
|
<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="kiroModeClass('import')"
|
||||||
|
>
|
||||||
|
<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 v-if="kiroAccountType === 'oauth'" class="mt-3 space-y-3">
|
||||||
|
<div class="text-xs text-amber-800 dark:text-amber-200">
|
||||||
|
{{ t('admin.accounts.oauth.kiro.oauthSubtitle') }}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="kiroOAuthProvider = 'google'"
|
||||||
|
:class="kiroProviderClass('google')"
|
||||||
|
>
|
||||||
|
<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="kiroProviderClass('github')"
|
||||||
|
>
|
||||||
|
<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="kiroAccountType === 'idc'" class="mt-3 grid gap-3 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.accounts.oauth.kiro.idcStartUrlLabel') }}</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="isKiroImportMode" class="mt-3 space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('admin.accounts.oauth.kiro.tokenJsonLabel') }}</label>
|
||||||
|
<textarea
|
||||||
|
v-model="kiroTokenJson"
|
||||||
|
rows="7"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
placeholder='{"accessToken":"...","refreshToken":"..."}'
|
||||||
|
/>
|
||||||
|
<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="4"
|
||||||
|
class="input font-mono text-xs"
|
||||||
|
placeholder='{"clientId":"...","clientSecret":"..."}'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<OAuthAuthorizationFlow
|
<OAuthAuthorizationFlow
|
||||||
|
v-if="!isKiroImportMode"
|
||||||
ref="oauthFlowRef"
|
ref="oauthFlowRef"
|
||||||
:add-method="addMethod"
|
:add-method="addMethod"
|
||||||
:auth-url="currentAuthUrl"
|
:auth-url="currentAuthUrl"
|
||||||
@@ -128,12 +301,11 @@
|
|||||||
:show-cookie-option="isAnthropic"
|
:show-cookie-option="isAnthropic"
|
||||||
:allow-multiple="false"
|
:allow-multiple="false"
|
||||||
:method-label="t('admin.accounts.inputMethod')"
|
:method-label="t('admin.accounts.inputMethod')"
|
||||||
:platform="isOpenAI ? 'openai' : isGemini ? 'gemini' : isAntigravity ? 'antigravity' : 'anthropic'"
|
:platform="oauthPlatform"
|
||||||
:show-project-id="isGemini && geminiOAuthType === 'code_assist'"
|
:show-project-id="isGemini && geminiOAuthType === 'code_assist'"
|
||||||
@generate-url="handleGenerateUrl"
|
@generate-url="handleGenerateUrl"
|
||||||
@cookie-auth="handleCookieAuth"
|
@cookie-auth="handleCookieAuth"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -142,7 +314,16 @@
|
|||||||
{{ t('common.cancel') }}
|
{{ t('common.cancel') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="isManualInputMethod"
|
v-if="isKiroImportMode"
|
||||||
|
type="button"
|
||||||
|
:disabled="currentLoading || !kiroTokenJson.trim()"
|
||||||
|
class="btn btn-primary"
|
||||||
|
@click="handleKiroImport"
|
||||||
|
>
|
||||||
|
{{ currentLoading ? t('admin.accounts.oauth.verifying') : t('admin.accounts.oauth.kiro.importAndUpdate') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="isManualInputMethod"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="!canExchangeCode"
|
:disabled="!canExchangeCode"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
@@ -161,18 +342,14 @@
|
|||||||
r="10"
|
r="10"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="4"
|
stroke-width="4"
|
||||||
></circle>
|
/>
|
||||||
<path
|
<path
|
||||||
class="opacity-75"
|
class="opacity-75"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
></path>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{{
|
{{ currentLoading ? t('admin.accounts.oauth.verifying') : t('admin.accounts.oauth.completeAuth') }}
|
||||||
currentLoading
|
|
||||||
? t('admin.accounts.oauth.verifying')
|
|
||||||
: t('admin.accounts.oauth.completeAuth')
|
|
||||||
}}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -180,28 +357,29 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useAppStore } from '@/stores/app'
|
|
||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import {
|
|
||||||
useAccountOAuth,
|
|
||||||
type AddMethod,
|
|
||||||
type AuthInputMethod
|
|
||||||
} from '@/composables/useAccountOAuth'
|
|
||||||
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
|
||||||
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
|
||||||
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
|
||||||
import type { Account } from '@/types'
|
|
||||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
import OAuthAuthorizationFlow from '@/components/account/OAuthAuthorizationFlow.vue'
|
import OAuthAuthorizationFlow from '@/components/account/OAuthAuthorizationFlow.vue'
|
||||||
|
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
||||||
|
import {
|
||||||
|
type AddMethod,
|
||||||
|
type AuthInputMethod,
|
||||||
|
useAccountOAuth
|
||||||
|
} from '@/composables/useAccountOAuth'
|
||||||
|
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
||||||
|
import { useKiroOAuth } from '@/composables/useKiroOAuth'
|
||||||
|
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import type { Account, AccountPlatform } from '@/types'
|
||||||
|
|
||||||
// Type for exposed OAuthAuthorizationFlow component
|
|
||||||
// Note: defineExpose automatically unwraps refs, so we use the unwrapped types
|
|
||||||
interface OAuthFlowExposed {
|
interface OAuthFlowExposed {
|
||||||
authCode: string
|
authCode: string
|
||||||
oauthState: string
|
oauthState: string
|
||||||
|
oauthCallbackPath: string
|
||||||
|
oauthLoginOption: string
|
||||||
projectId: string
|
projectId: string
|
||||||
sessionKey: string
|
sessionKey: string
|
||||||
inputMethod: AuthInputMethod
|
inputMethod: AuthInputMethod
|
||||||
@@ -222,122 +400,222 @@ const emit = defineEmits<{
|
|||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
// OAuth composables
|
|
||||||
const claudeOAuth = useAccountOAuth()
|
const claudeOAuth = useAccountOAuth()
|
||||||
const openaiOAuth = useOpenAIOAuth()
|
const openaiOAuth = useOpenAIOAuth()
|
||||||
const geminiOAuth = useGeminiOAuth()
|
const geminiOAuth = useGeminiOAuth()
|
||||||
const antigravityOAuth = useAntigravityOAuth()
|
const antigravityOAuth = useAntigravityOAuth()
|
||||||
|
const kiroOAuth = useKiroOAuth()
|
||||||
|
|
||||||
// Refs
|
|
||||||
const oauthFlowRef = ref<OAuthFlowExposed | null>(null)
|
const oauthFlowRef = ref<OAuthFlowExposed | null>(null)
|
||||||
|
|
||||||
// State
|
|
||||||
const addMethod = ref<AddMethod>('oauth')
|
const addMethod = ref<AddMethod>('oauth')
|
||||||
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_assist')
|
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('code_assist')
|
||||||
|
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('')
|
||||||
|
|
||||||
// Computed - check platform
|
|
||||||
const isOpenAI = computed(() => props.account?.platform === 'openai')
|
const isOpenAI = computed(() => props.account?.platform === 'openai')
|
||||||
const isOpenAILike = computed(() => isOpenAI.value)
|
const isOpenAILike = computed(() => isOpenAI.value)
|
||||||
const isGemini = computed(() => props.account?.platform === 'gemini')
|
const isGemini = computed(() => props.account?.platform === 'gemini')
|
||||||
const isAnthropic = computed(() => props.account?.platform === 'anthropic')
|
const isAnthropic = computed(() => props.account?.platform === 'anthropic')
|
||||||
const isAntigravity = computed(() => props.account?.platform === 'antigravity')
|
const isAntigravity = computed(() => props.account?.platform === 'antigravity')
|
||||||
|
const isKiro = computed(() => props.account?.platform === 'kiro')
|
||||||
|
|
||||||
|
const oauthPlatform = computed<AccountPlatform>(() => {
|
||||||
|
if (isOpenAI.value) return 'openai'
|
||||||
|
if (isGemini.value) return 'gemini'
|
||||||
|
if (isKiro.value) return 'kiro'
|
||||||
|
if (isAntigravity.value) return 'antigravity'
|
||||||
|
return 'anthropic'
|
||||||
|
})
|
||||||
|
|
||||||
// Computed - current OAuth state based on platform
|
|
||||||
const currentAuthUrl = computed(() => {
|
const currentAuthUrl = computed(() => {
|
||||||
if (isOpenAILike.value) return openaiOAuth.authUrl.value
|
if (isOpenAILike.value) return openaiOAuth.authUrl.value
|
||||||
if (isGemini.value) return geminiOAuth.authUrl.value
|
if (isGemini.value) return geminiOAuth.authUrl.value
|
||||||
|
if (isKiro.value) return kiroOAuth.authUrl.value
|
||||||
if (isAntigravity.value) return antigravityOAuth.authUrl.value
|
if (isAntigravity.value) return antigravityOAuth.authUrl.value
|
||||||
return claudeOAuth.authUrl.value
|
return claudeOAuth.authUrl.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentSessionId = computed(() => {
|
const currentSessionId = computed(() => {
|
||||||
if (isOpenAILike.value) return openaiOAuth.sessionId.value
|
if (isOpenAILike.value) return openaiOAuth.sessionId.value
|
||||||
if (isGemini.value) return geminiOAuth.sessionId.value
|
if (isGemini.value) return geminiOAuth.sessionId.value
|
||||||
|
if (isKiro.value) return kiroOAuth.sessionId.value
|
||||||
if (isAntigravity.value) return antigravityOAuth.sessionId.value
|
if (isAntigravity.value) return antigravityOAuth.sessionId.value
|
||||||
return claudeOAuth.sessionId.value
|
return claudeOAuth.sessionId.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentLoading = computed(() => {
|
const currentLoading = computed(() => {
|
||||||
if (isOpenAILike.value) return openaiOAuth.loading.value
|
if (isOpenAILike.value) return openaiOAuth.loading.value
|
||||||
if (isGemini.value) return geminiOAuth.loading.value
|
if (isGemini.value) return geminiOAuth.loading.value
|
||||||
|
if (isKiro.value) return kiroOAuth.loading.value
|
||||||
if (isAntigravity.value) return antigravityOAuth.loading.value
|
if (isAntigravity.value) return antigravityOAuth.loading.value
|
||||||
return claudeOAuth.loading.value
|
return claudeOAuth.loading.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentError = computed(() => {
|
const currentError = computed(() => {
|
||||||
if (isOpenAILike.value) return openaiOAuth.error.value
|
if (isOpenAILike.value) return openaiOAuth.error.value
|
||||||
if (isGemini.value) return geminiOAuth.error.value
|
if (isGemini.value) return geminiOAuth.error.value
|
||||||
|
if (isKiro.value) return kiroOAuth.error.value
|
||||||
if (isAntigravity.value) return antigravityOAuth.error.value
|
if (isAntigravity.value) return antigravityOAuth.error.value
|
||||||
return claudeOAuth.error.value
|
return claudeOAuth.error.value
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed
|
const isKiroImportMode = computed(() => isKiro.value && kiroAccountType.value === 'import')
|
||||||
|
|
||||||
const isManualInputMethod = computed(() => {
|
const isManualInputMethod = computed(() => {
|
||||||
// OpenAI/Gemini/Antigravity always use manual input (no cookie auth option)
|
return isOpenAILike.value || isGemini.value || isKiro.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual'
|
||||||
return isOpenAILike.value || isGemini.value || isAntigravity.value || oauthFlowRef.value?.inputMethod === 'manual'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const canExchangeCode = computed(() => {
|
const canExchangeCode = computed(() => {
|
||||||
|
if (isKiroImportMode.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
const authCode = oauthFlowRef.value?.authCode || ''
|
const authCode = oauthFlowRef.value?.authCode || ''
|
||||||
const sessionId = currentSessionId.value
|
return !!(authCode.trim() && currentSessionId.value && !currentLoading.value)
|
||||||
const loading = currentLoading.value
|
|
||||||
return authCode.trim() && sessionId && !loading
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watchers
|
|
||||||
watch(
|
watch(
|
||||||
() => props.show,
|
() => props.show,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
if (newVal && props.account) {
|
if (!newVal || !props.account) {
|
||||||
// Initialize addMethod based on current account type (Claude only)
|
|
||||||
if (
|
|
||||||
isAnthropic.value &&
|
|
||||||
(props.account.type === 'oauth' || props.account.type === 'setup-token')
|
|
||||||
) {
|
|
||||||
addMethod.value = props.account.type as AddMethod
|
|
||||||
}
|
|
||||||
if (isGemini.value) {
|
|
||||||
const creds = (props.account.credentials || {}) as Record<string, unknown>
|
|
||||||
geminiOAuthType.value =
|
|
||||||
creds.oauth_type === 'google_one'
|
|
||||||
? 'google_one'
|
|
||||||
: creds.oauth_type === 'ai_studio'
|
|
||||||
? 'ai_studio'
|
|
||||||
: 'code_assist'
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resetState()
|
resetState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnthropic.value && (props.account.type === 'oauth' || props.account.type === 'setup-token')) {
|
||||||
|
addMethod.value = props.account.type as AddMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGemini.value) {
|
||||||
|
const creds = (props.account.credentials || {}) as Record<string, unknown>
|
||||||
|
geminiOAuthType.value =
|
||||||
|
creds.oauth_type === 'google_one'
|
||||||
|
? 'google_one'
|
||||||
|
: creds.oauth_type === 'ai_studio'
|
||||||
|
? 'ai_studio'
|
||||||
|
: 'code_assist'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isKiro.value) {
|
||||||
|
const creds = (props.account.credentials || {}) as Record<string, unknown>
|
||||||
|
const authMethod = typeof creds.auth_method === 'string' ? creds.auth_method : ''
|
||||||
|
const provider = String(creds.provider || '').toLowerCase()
|
||||||
|
kiroIDCStartUrl.value = typeof creds.start_url === 'string' && creds.start_url ? creds.start_url : 'https://view.awsapps.com/start'
|
||||||
|
kiroIDCRegion.value = typeof creds.region === 'string' && creds.region ? creds.region : 'us-east-1'
|
||||||
|
kiroAccountType.value = authMethod === 'idc' ? 'idc' : 'oauth'
|
||||||
|
kiroOAuthProvider.value = provider === 'github' ? 'github' : 'google'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Methods
|
|
||||||
const resetState = () => {
|
const resetState = () => {
|
||||||
addMethod.value = 'oauth'
|
addMethod.value = 'oauth'
|
||||||
geminiOAuthType.value = 'code_assist'
|
geminiOAuthType.value = 'code_assist'
|
||||||
|
kiroAccountType.value = 'oauth'
|
||||||
|
kiroOAuthProvider.value = 'google'
|
||||||
|
kiroIDCStartUrl.value = 'https://view.awsapps.com/start'
|
||||||
|
kiroIDCRegion.value = 'us-east-1'
|
||||||
|
kiroTokenJson.value = ''
|
||||||
|
kiroDeviceRegistrationJson.value = ''
|
||||||
claudeOAuth.resetState()
|
claudeOAuth.resetState()
|
||||||
openaiOAuth.resetState()
|
openaiOAuth.resetState()
|
||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
antigravityOAuth.resetState()
|
antigravityOAuth.resetState()
|
||||||
|
kiroOAuth.resetState()
|
||||||
oauthFlowRef.value?.reset()
|
oauthFlowRef.value?.reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kiroModeClass = (mode: typeof kiroAccountType.value) => [
|
||||||
|
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||||
|
kiroAccountType.value === mode
|
||||||
|
? mode === 'idc'
|
||||||
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
|
||||||
|
: mode === 'import'
|
||||||
|
? 'border-slate-500 bg-slate-50 dark:bg-slate-900/20'
|
||||||
|
: 'border-amber-500 bg-amber-50 dark:bg-amber-900/20'
|
||||||
|
: mode === 'idc'
|
||||||
|
? 'border-gray-200 hover:border-blue-300 dark:border-dark-600 dark:hover:border-blue-700'
|
||||||
|
: mode === 'import'
|
||||||
|
? 'border-gray-200 hover:border-slate-300 dark:border-dark-600 dark:hover:border-slate-700'
|
||||||
|
: 'border-gray-200 hover:border-amber-300 dark:border-dark-600 dark:hover:border-amber-700'
|
||||||
|
]
|
||||||
|
|
||||||
|
const kiroProviderClass = (provider: typeof kiroOAuthProvider.value) => [
|
||||||
|
'flex items-center gap-3 rounded-lg border-2 p-3 text-left transition-all',
|
||||||
|
kiroOAuthProvider.value === provider
|
||||||
|
? provider === 'github'
|
||||||
|
? 'border-slate-500 bg-slate-50 dark:bg-slate-900/20'
|
||||||
|
: 'border-amber-500 bg-amber-50 dark:bg-amber-900/20'
|
||||||
|
: provider === 'github'
|
||||||
|
? 'border-amber-200 hover:border-slate-300 dark:border-amber-700/40 dark:hover:border-slate-700'
|
||||||
|
: 'border-amber-200 hover:border-amber-300 dark:border-amber-700/40 dark:hover:border-amber-600'
|
||||||
|
]
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildUpdatedCredentials = (next: Record<string, unknown>) => ({
|
||||||
|
...((props.account?.credentials || {}) as Record<string, unknown>),
|
||||||
|
...next
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateAccountCredentials = async (payload: {
|
||||||
|
type: 'oauth' | 'setup-token'
|
||||||
|
credentials: Record<string, unknown>
|
||||||
|
extra?: Record<string, unknown>
|
||||||
|
}) => {
|
||||||
|
if (!props.account) return
|
||||||
|
|
||||||
|
await adminAPI.accounts.update(props.account.id, payload)
|
||||||
|
const updatedAccount = await adminAPI.accounts.clearError(props.account.id)
|
||||||
|
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
||||||
|
emit('reauthorized', updatedAccount)
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
|
||||||
const handleGenerateUrl = async () => {
|
const handleGenerateUrl = async () => {
|
||||||
if (!props.account) return
|
if (!props.account) return
|
||||||
|
|
||||||
if (isOpenAILike.value) {
|
if (isOpenAILike.value) {
|
||||||
await openaiOAuth.generateAuthUrl(props.account.proxy_id)
|
await openaiOAuth.generateAuthUrl(props.account.proxy_id)
|
||||||
} else if (isGemini.value) {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGemini.value) {
|
||||||
const creds = (props.account.credentials || {}) as Record<string, unknown>
|
const creds = (props.account.credentials || {}) as Record<string, unknown>
|
||||||
const tierId = typeof creds.tier_id === 'string' ? creds.tier_id : undefined
|
const tierId = typeof creds.tier_id === 'string' ? creds.tier_id : undefined
|
||||||
const projectId = geminiOAuthType.value === 'code_assist' ? oauthFlowRef.value?.projectId : undefined
|
const projectId = geminiOAuthType.value === 'code_assist' ? oauthFlowRef.value?.projectId : undefined
|
||||||
await geminiOAuth.generateAuthUrl(props.account.proxy_id, projectId, geminiOAuthType.value, tierId)
|
await geminiOAuth.generateAuthUrl(props.account.proxy_id, projectId, geminiOAuthType.value, tierId)
|
||||||
} else if (isAntigravity.value) {
|
return
|
||||||
await antigravityOAuth.generateAuthUrl(props.account.proxy_id)
|
|
||||||
} else {
|
|
||||||
await claudeOAuth.generateAuthUrl(addMethod.value, props.account.proxy_id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isKiro.value) {
|
||||||
|
if (kiroAccountType.value === 'idc') {
|
||||||
|
await kiroOAuth.generateIDCAuthUrl({
|
||||||
|
proxyId: props.account.proxy_id,
|
||||||
|
startUrl: kiroIDCStartUrl.value,
|
||||||
|
region: kiroIDCRegion.value
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await kiroOAuth.generateAuthUrl(
|
||||||
|
props.account.proxy_id,
|
||||||
|
kiroOAuthProvider.value === 'github' ? 'Github' : 'Google'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAntigravity.value) {
|
||||||
|
await antigravityOAuth.generateAuthUrl(props.account.proxy_id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await claudeOAuth.generateAuthUrl(addMethod.value, props.account.proxy_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExchangeCode = async () => {
|
const handleExchangeCode = async () => {
|
||||||
@@ -347,53 +625,37 @@ const handleExchangeCode = async () => {
|
|||||||
if (!authCode.trim()) return
|
if (!authCode.trim()) return
|
||||||
|
|
||||||
if (isOpenAILike.value) {
|
if (isOpenAILike.value) {
|
||||||
// OpenAI OAuth flow
|
const sessionId = openaiOAuth.sessionId.value
|
||||||
const oauthClient = openaiOAuth
|
|
||||||
const sessionId = oauthClient.sessionId.value
|
|
||||||
if (!sessionId) return
|
if (!sessionId) return
|
||||||
const stateToUse = (oauthFlowRef.value?.oauthState || oauthClient.oauthState.value || '').trim()
|
|
||||||
|
const stateToUse = (oauthFlowRef.value?.oauthState || openaiOAuth.oauthState.value || '').trim()
|
||||||
if (!stateToUse) {
|
if (!stateToUse) {
|
||||||
oauthClient.error.value = t('admin.accounts.oauth.authFailed')
|
openaiOAuth.error.value = t('admin.accounts.oauth.authFailed')
|
||||||
appStore.showError(oauthClient.error.value)
|
appStore.showError(openaiOAuth.error.value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenInfo = await oauthClient.exchangeAuthCode(
|
const tokenInfo = await openaiOAuth.exchangeAuthCode(authCode.trim(), sessionId, stateToUse, props.account.proxy_id)
|
||||||
authCode.trim(),
|
|
||||||
sessionId,
|
|
||||||
stateToUse,
|
|
||||||
props.account.proxy_id
|
|
||||||
)
|
|
||||||
if (!tokenInfo) return
|
if (!tokenInfo) return
|
||||||
|
|
||||||
// Build credentials and extra info
|
|
||||||
const credentials = oauthClient.buildCredentials(tokenInfo)
|
|
||||||
const extra = oauthClient.buildExtraInfo(tokenInfo)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update account with new credentials
|
await updateAccountCredentials({
|
||||||
await adminAPI.accounts.update(props.account.id, {
|
type: 'oauth',
|
||||||
type: 'oauth', // OpenAI OAuth is always 'oauth' type
|
credentials: buildUpdatedCredentials(openaiOAuth.buildCredentials(tokenInfo)),
|
||||||
credentials,
|
extra: openaiOAuth.buildExtraInfo(tokenInfo) as Record<string, unknown> | undefined
|
||||||
extra
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Clear error status after successful re-authorization
|
|
||||||
const updatedAccount = await adminAPI.accounts.clearError(props.account.id)
|
|
||||||
|
|
||||||
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
|
||||||
emit('reauthorized', updatedAccount)
|
|
||||||
handleClose()
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
oauthClient.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
openaiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
||||||
appStore.showError(oauthClient.error.value)
|
appStore.showError(openaiOAuth.error.value)
|
||||||
}
|
}
|
||||||
} else if (isGemini.value) {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isGemini.value) {
|
||||||
const sessionId = geminiOAuth.sessionId.value
|
const sessionId = geminiOAuth.sessionId.value
|
||||||
if (!sessionId) return
|
if (!sessionId) return
|
||||||
|
|
||||||
const stateFromInput = oauthFlowRef.value?.oauthState || ''
|
const stateToUse = oauthFlowRef.value?.oauthState || geminiOAuth.state.value
|
||||||
const stateToUse = stateFromInput || geminiOAuth.state.value
|
|
||||||
if (!stateToUse) return
|
if (!stateToUse) return
|
||||||
|
|
||||||
const tokenInfo = await geminiOAuth.exchangeAuthCode({
|
const tokenInfo = await geminiOAuth.exchangeAuthCode({
|
||||||
@@ -402,32 +664,58 @@ const handleExchangeCode = async () => {
|
|||||||
state: stateToUse,
|
state: stateToUse,
|
||||||
proxyId: props.account.proxy_id,
|
proxyId: props.account.proxy_id,
|
||||||
oauthType: geminiOAuthType.value,
|
oauthType: geminiOAuthType.value,
|
||||||
tierId: typeof (props.account.credentials as any)?.tier_id === 'string' ? ((props.account.credentials as any).tier_id as string) : undefined
|
tierId: typeof (props.account.credentials as any)?.tier_id === 'string'
|
||||||
|
? ((props.account.credentials as any).tier_id as string)
|
||||||
|
: undefined
|
||||||
})
|
})
|
||||||
if (!tokenInfo) return
|
if (!tokenInfo) return
|
||||||
|
|
||||||
const credentials = geminiOAuth.buildCredentials(tokenInfo)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await adminAPI.accounts.update(props.account.id, {
|
await updateAccountCredentials({
|
||||||
type: 'oauth',
|
type: 'oauth',
|
||||||
credentials
|
credentials: buildUpdatedCredentials(geminiOAuth.buildCredentials(tokenInfo))
|
||||||
})
|
})
|
||||||
const updatedAccount = await adminAPI.accounts.clearError(props.account.id)
|
|
||||||
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
|
||||||
emit('reauthorized', updatedAccount)
|
|
||||||
handleClose()
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
geminiOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
||||||
appStore.showError(geminiOAuth.error.value)
|
appStore.showError(geminiOAuth.error.value)
|
||||||
}
|
}
|
||||||
} else if (isAntigravity.value) {
|
return
|
||||||
// Antigravity OAuth flow
|
}
|
||||||
|
|
||||||
|
if (isKiro.value) {
|
||||||
|
const sessionId = kiroOAuth.sessionId.value
|
||||||
|
if (!sessionId) return
|
||||||
|
|
||||||
|
const stateToUse = oauthFlowRef.value?.oauthState || kiroOAuth.state.value
|
||||||
|
if (!stateToUse) return
|
||||||
|
|
||||||
|
const tokenInfo = await kiroOAuth.exchangeAuthCode({
|
||||||
|
code: authCode.trim(),
|
||||||
|
sessionId,
|
||||||
|
state: stateToUse,
|
||||||
|
callbackPath: oauthFlowRef.value?.oauthCallbackPath || '',
|
||||||
|
loginOption: oauthFlowRef.value?.oauthLoginOption || '',
|
||||||
|
proxyId: props.account.proxy_id
|
||||||
|
})
|
||||||
|
if (!tokenInfo) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateAccountCredentials({
|
||||||
|
type: 'oauth',
|
||||||
|
credentials: buildUpdatedCredentials(kiroOAuth.buildCredentials(tokenInfo))
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
kiroOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
||||||
|
appStore.showError(kiroOAuth.error.value)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAntigravity.value) {
|
||||||
const sessionId = antigravityOAuth.sessionId.value
|
const sessionId = antigravityOAuth.sessionId.value
|
||||||
if (!sessionId) return
|
if (!sessionId) return
|
||||||
|
|
||||||
const stateFromInput = oauthFlowRef.value?.oauthState || ''
|
const stateToUse = oauthFlowRef.value?.oauthState || antigravityOAuth.state.value
|
||||||
const stateToUse = stateFromInput || antigravityOAuth.state.value
|
|
||||||
if (!stateToUse) return
|
if (!stateToUse) return
|
||||||
|
|
||||||
const tokenInfo = await antigravityOAuth.exchangeAuthCode({
|
const tokenInfo = await antigravityOAuth.exchangeAuthCode({
|
||||||
@@ -438,68 +726,72 @@ const handleExchangeCode = async () => {
|
|||||||
})
|
})
|
||||||
if (!tokenInfo) return
|
if (!tokenInfo) return
|
||||||
|
|
||||||
const credentials = antigravityOAuth.buildCredentials(tokenInfo)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await adminAPI.accounts.update(props.account.id, {
|
await updateAccountCredentials({
|
||||||
type: 'oauth',
|
type: 'oauth',
|
||||||
credentials
|
credentials: buildUpdatedCredentials(antigravityOAuth.buildCredentials(tokenInfo))
|
||||||
})
|
})
|
||||||
const updatedAccount = await adminAPI.accounts.clearError(props.account.id)
|
|
||||||
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
|
||||||
emit('reauthorized', updatedAccount)
|
|
||||||
handleClose()
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
antigravityOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
antigravityOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
||||||
appStore.showError(antigravityOAuth.error.value)
|
appStore.showError(antigravityOAuth.error.value)
|
||||||
}
|
}
|
||||||
} else {
|
return
|
||||||
// Claude OAuth flow
|
}
|
||||||
const sessionId = claudeOAuth.sessionId.value
|
|
||||||
if (!sessionId) return
|
|
||||||
|
|
||||||
claudeOAuth.loading.value = true
|
const sessionId = claudeOAuth.sessionId.value
|
||||||
claudeOAuth.error.value = ''
|
if (!sessionId) return
|
||||||
|
|
||||||
try {
|
claudeOAuth.loading.value = true
|
||||||
const proxyConfig = props.account.proxy_id ? { proxy_id: props.account.proxy_id } : {}
|
claudeOAuth.error.value = ''
|
||||||
const endpoint =
|
|
||||||
addMethod.value === 'oauth'
|
|
||||||
? '/admin/accounts/exchange-code'
|
|
||||||
: '/admin/accounts/exchange-setup-token-code'
|
|
||||||
|
|
||||||
const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, {
|
try {
|
||||||
session_id: sessionId,
|
const proxyConfig = props.account.proxy_id ? { proxy_id: props.account.proxy_id } : {}
|
||||||
code: authCode.trim(),
|
const endpoint =
|
||||||
...proxyConfig
|
addMethod.value === 'oauth'
|
||||||
})
|
? '/admin/accounts/exchange-code'
|
||||||
|
: '/admin/accounts/exchange-setup-token-code'
|
||||||
|
|
||||||
const extra = claudeOAuth.buildExtraInfo(tokenInfo)
|
const tokenInfo = await adminAPI.accounts.exchangeCode(endpoint, {
|
||||||
|
session_id: sessionId,
|
||||||
|
code: authCode.trim(),
|
||||||
|
...proxyConfig
|
||||||
|
})
|
||||||
|
|
||||||
// Update account with new credentials and type
|
await updateAccountCredentials({
|
||||||
await adminAPI.accounts.update(props.account.id, {
|
type: addMethod.value,
|
||||||
type: addMethod.value, // Update type based on selected method
|
credentials: buildUpdatedCredentials(tokenInfo),
|
||||||
credentials: tokenInfo,
|
extra: claudeOAuth.buildExtraInfo(tokenInfo)
|
||||||
extra
|
})
|
||||||
})
|
} catch (error: any) {
|
||||||
|
claudeOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
||||||
|
appStore.showError(claudeOAuth.error.value)
|
||||||
|
} finally {
|
||||||
|
claudeOAuth.loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear error status after successful re-authorization
|
const handleKiroImport = async () => {
|
||||||
const updatedAccount = await adminAPI.accounts.clearError(props.account.id)
|
if (!props.account || !isKiroImportMode.value || !kiroTokenJson.value.trim()) return
|
||||||
|
|
||||||
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
const tokenInfo = await kiroOAuth.importToken(
|
||||||
emit('reauthorized', updatedAccount)
|
kiroTokenJson.value,
|
||||||
handleClose()
|
kiroDeviceRegistrationJson.value || undefined
|
||||||
} catch (error: any) {
|
)
|
||||||
claudeOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.authFailed')
|
if (!tokenInfo) return
|
||||||
appStore.showError(claudeOAuth.error.value)
|
|
||||||
} finally {
|
try {
|
||||||
claudeOAuth.loading.value = false
|
await updateAccountCredentials({
|
||||||
}
|
type: 'oauth',
|
||||||
|
credentials: buildUpdatedCredentials(kiroOAuth.buildCredentials(tokenInfo))
|
||||||
|
})
|
||||||
|
} 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) => {
|
const handleCookieAuth = async (sessionKey: string) => {
|
||||||
if (!props.account || isOpenAILike.value) return
|
if (!props.account || isOpenAILike.value || isKiro.value) return
|
||||||
|
|
||||||
claudeOAuth.loading.value = true
|
claudeOAuth.loading.value = true
|
||||||
claudeOAuth.error.value = ''
|
claudeOAuth.error.value = ''
|
||||||
@@ -517,24 +809,13 @@ const handleCookieAuth = async (sessionKey: string) => {
|
|||||||
...proxyConfig
|
...proxyConfig
|
||||||
})
|
})
|
||||||
|
|
||||||
const extra = claudeOAuth.buildExtraInfo(tokenInfo)
|
await updateAccountCredentials({
|
||||||
|
type: addMethod.value,
|
||||||
// Update account with new credentials and type
|
credentials: buildUpdatedCredentials(tokenInfo),
|
||||||
await adminAPI.accounts.update(props.account.id, {
|
extra: claudeOAuth.buildExtraInfo(tokenInfo)
|
||||||
type: addMethod.value, // Update type based on selected method
|
|
||||||
credentials: tokenInfo,
|
|
||||||
extra
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Clear error status after successful re-authorization
|
|
||||||
const updatedAccount = await adminAPI.accounts.clearError(props.account.id)
|
|
||||||
|
|
||||||
appStore.showSuccess(t('admin.accounts.reAuthorizedSuccess'))
|
|
||||||
emit('reauthorized', updatedAccount)
|
|
||||||
handleClose()
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
claudeOAuth.error.value =
|
claudeOAuth.error.value = error.response?.data?.detail || t('admin.accounts.oauth.cookieAuthFailed')
|
||||||
error.response?.data?.detail || t('admin.accounts.oauth.cookieAuthFailed')
|
|
||||||
} finally {
|
} finally {
|
||||||
claudeOAuth.loading.value = false
|
claudeOAuth.loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Claude/Anthropic logo -->
|
<!-- Claude/Anthropic logo -->
|
||||||
<svg v-if="platform === 'anthropic'" :class="sizeClass" viewBox="0 0 16 16" fill="currentColor">
|
<svg v-if="platform === 'anthropic' || platform === 'kiro'" :class="sizeClass" viewBox="0 0 16 16" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
d="m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z"
|
d="m3.127 10.604 3.135-1.76.053-.153-.053-.085H6.11l-.525-.032-1.791-.048-1.554-.065-1.505-.08-.38-.081L0 7.832l.036-.234.32-.214.455.04 1.009.069 1.513.105 1.097.064 1.626.17h.259l.036-.105-.089-.065-.068-.064-1.566-1.062-1.695-1.121-.887-.646-.48-.327-.243-.306-.104-.67.435-.48.585.04.15.04.593.456 1.267.981 1.654 1.218.242.202.097-.068.012-.049-.109-.181-.9-1.626-.96-1.655-.428-.686-.113-.411a2 2 0 0 1-.068-.484l.496-.674L4.446 0l.662.089.279.242.411.94.666 1.48 1.033 2.014.302.597.162.553.06.17h.105v-.097l.085-1.134.157-1.392.154-1.792.052-.504.25-.605.497-.327.387.186.319.456-.045.294-.19 1.23-.37 1.93-.243 1.29h.142l.161-.16.654-.868 1.097-1.372.484-.545.565-.601.363-.287h.686l.505.751-.226.775-.707.895-.585.759-.839 1.13-.524.904.048.072.125-.012 1.897-.403 1.024-.186 1.223-.21.553.258.06.263-.218.536-1.307.323-1.533.307-2.284.54-.028.02.032.04 1.029.098.44.024h1.077l2.005.15.525.346.315.424-.053.323-.807.411-3.631-.863-.872-.218h-.12v.073l.726.71 1.331 1.202 1.667 1.55.084.383-.214.302-.226-.032-1.464-1.101-.565-.497-1.28-1.077h-.084v.113l.295.432 1.557 2.34.08.718-.112.234-.404.141-.444-.08-.911-1.28-.94-1.44-.759-1.291-.093.053-.448 4.821-.21.246-.484.186-.403-.307-.214-.496.214-.98.258-1.28.21-1.016.19-1.263.112-.42-.008-.028-.092.012-.953 1.307-1.448 1.957-1.146 1.227-.274.109-.477-.247.045-.44.266-.39 1.586-2.018.956-1.25.617-.723-.004-.105h-.036l-4.212 2.736-.75.096-.324-.302.04-.496.154-.162 1.267-.871z"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -31,10 +31,18 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- Row 2: Plan type + Privacy mode (only if either exists) -->
|
<!-- Row 2: Plan type + Privacy mode (only if either exists) -->
|
||||||
<div v-if="planLabel || privacyBadge" class="inline-flex items-center overflow-hidden rounded-md">
|
<div v-if="planLabel || privacyBadge || overagesBadge" class="inline-flex items-center overflow-hidden rounded-md">
|
||||||
<span v-if="planLabel" :class="['inline-flex items-center gap-1 px-1.5 py-1', planBadgeClass]">
|
<span v-if="planLabel" :class="['inline-flex items-center gap-1 px-1.5 py-1', planBadgeClass]">
|
||||||
<span>{{ planLabel }}</span>
|
<span>{{ planLabel }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="overagesBadge"
|
||||||
|
:class="['inline-flex items-center gap-1 px-1.5 py-1', overagesBadge.class]"
|
||||||
|
:title="overagesBadge.title"
|
||||||
|
>
|
||||||
|
<Icon name="sparkles" size="xs" />
|
||||||
|
<span>{{ overagesBadge.label }}</span>
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="privacyBadge"
|
v-if="privacyBadge"
|
||||||
:class="['inline-flex items-center gap-1 px-1.5 py-1', privacyBadge.class]"
|
:class="['inline-flex items-center gap-1 px-1.5 py-1', privacyBadge.class]"
|
||||||
@@ -66,6 +74,7 @@ interface Props {
|
|||||||
platform: AccountPlatform
|
platform: AccountPlatform
|
||||||
type: AccountType
|
type: AccountType
|
||||||
planType?: string
|
planType?: string
|
||||||
|
overagesEnabled?: boolean
|
||||||
privacyMode?: string
|
privacyMode?: string
|
||||||
subscriptionExpiresAt?: string
|
subscriptionExpiresAt?: string
|
||||||
}
|
}
|
||||||
@@ -76,6 +85,7 @@ const platformLabel = computed(() => {
|
|||||||
if (props.platform === 'anthropic') return 'Anthropic'
|
if (props.platform === 'anthropic') return 'Anthropic'
|
||||||
if (props.platform === 'openai') return 'OpenAI'
|
if (props.platform === 'openai') return 'OpenAI'
|
||||||
if (props.platform === 'antigravity') return 'Antigravity'
|
if (props.platform === 'antigravity') return 'Antigravity'
|
||||||
|
if (props.platform === 'kiro') return 'Kiro'
|
||||||
return 'Gemini'
|
return 'Gemini'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -126,6 +136,9 @@ const platformClass = computed(() => {
|
|||||||
if (props.platform === 'antigravity') {
|
if (props.platform === 'antigravity') {
|
||||||
return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
||||||
}
|
}
|
||||||
|
if (props.platform === 'kiro') {
|
||||||
|
return 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||||
|
}
|
||||||
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -139,6 +152,9 @@ const typeClass = computed(() => {
|
|||||||
if (props.platform === 'antigravity') {
|
if (props.platform === 'antigravity') {
|
||||||
return 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400'
|
return 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400'
|
||||||
}
|
}
|
||||||
|
if (props.platform === 'kiro') {
|
||||||
|
return 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400'
|
||||||
|
}
|
||||||
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
return 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -149,6 +165,15 @@ const planBadgeClass = computed(() => {
|
|||||||
return typeClass.value
|
return typeClass.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const overagesBadge = computed(() => {
|
||||||
|
if (props.platform !== 'kiro' || !props.overagesEnabled) return null
|
||||||
|
return {
|
||||||
|
label: t('admin.accounts.status.overageActive'),
|
||||||
|
title: t('admin.accounts.usageWindow.kiroOverage'),
|
||||||
|
class: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Subscription expiration label (non-free only)
|
// Subscription expiration label (non-free only)
|
||||||
const expiresLabel = computed(() => {
|
const expiresLabel = computed(() => {
|
||||||
if (!props.subscriptionExpiresAt || !props.planType) return ''
|
if (!props.subscriptionExpiresAt || !props.planType) return ''
|
||||||
|
|||||||
@@ -1979,6 +1979,7 @@ export default {
|
|||||||
openai: 'OpenAI',
|
openai: 'OpenAI',
|
||||||
gemini: 'Gemini',
|
gemini: 'Gemini',
|
||||||
antigravity: 'Antigravity',
|
antigravity: 'Antigravity',
|
||||||
|
kiro: 'Kiro',
|
||||||
},
|
},
|
||||||
deleteConfirm:
|
deleteConfirm:
|
||||||
"Are you sure you want to delete '{name}'? All associated API keys will no longer belong to any group.",
|
"Are you sure you want to delete '{name}'? All associated API keys will no longer belong to any group.",
|
||||||
@@ -2576,6 +2577,7 @@ export default {
|
|||||||
openai: 'OpenAI',
|
openai: 'OpenAI',
|
||||||
gemini: 'Gemini',
|
gemini: 'Gemini',
|
||||||
antigravity: 'Antigravity',
|
antigravity: 'Antigravity',
|
||||||
|
kiro: 'Kiro',
|
||||||
},
|
},
|
||||||
types: {
|
types: {
|
||||||
oauth: 'OAuth',
|
oauth: 'OAuth',
|
||||||
@@ -2583,6 +2585,8 @@ export default {
|
|||||||
responsesApi: 'Responses API',
|
responsesApi: 'Responses API',
|
||||||
googleOauth: 'Google OAuth',
|
googleOauth: 'Google OAuth',
|
||||||
codeAssist: 'Code Assist',
|
codeAssist: 'Code Assist',
|
||||||
|
kiroOauth: 'Social OAuth / AWS Builder ID / Import',
|
||||||
|
kiroApikey: 'Connect via Base URL + API Key',
|
||||||
antigravityOauth: 'Antigravity OAuth',
|
antigravityOauth: 'Antigravity OAuth',
|
||||||
antigravityApikey: 'Connect via Base URL + API Key',
|
antigravityApikey: 'Connect via Base URL + API Key',
|
||||||
upstream: 'Upstream',
|
upstream: 'Upstream',
|
||||||
@@ -2604,8 +2608,12 @@ export default {
|
|||||||
rateLimitedAutoResume: 'Auto resumes in {time}',
|
rateLimitedAutoResume: 'Auto resumes in {time}',
|
||||||
modelRateLimitedUntil: '{model} rate limited until {time}',
|
modelRateLimitedUntil: '{model} rate limited until {time}',
|
||||||
modelCreditOveragesUntil: '{model} using AI Credits until {time}',
|
modelCreditOveragesUntil: '{model} using AI Credits until {time}',
|
||||||
|
overageActive: 'Overage',
|
||||||
|
overageActiveUntil: 'Using overage until the reset window at {time}',
|
||||||
creditsExhausted: 'Credits Exhausted',
|
creditsExhausted: 'Credits Exhausted',
|
||||||
creditsExhaustedUntil: 'AI Credits exhausted, expected recovery at {time}',
|
creditsExhaustedUntil: 'AI Credits exhausted, expected recovery at {time}',
|
||||||
|
overageExhausted: 'Overage Exhausted',
|
||||||
|
overageExhaustedUntil: 'Overage exhausted, expected recovery at {time}',
|
||||||
overloadedUntil: 'Overloaded until {time}',
|
overloadedUntil: 'Overloaded until {time}',
|
||||||
viewTempUnschedDetails: 'View temp unschedulable details'
|
viewTempUnschedDetails: 'View temp unschedulable details'
|
||||||
},
|
},
|
||||||
@@ -2892,6 +2900,10 @@ export default {
|
|||||||
testModeCompact: 'Compact probe',
|
testModeCompact: 'Compact probe',
|
||||||
modelRestrictionDisabledByPassthrough: 'Automatic passthrough is enabled: model whitelist/mapping will not take effect.',
|
modelRestrictionDisabledByPassthrough: 'Automatic passthrough is enabled: model whitelist/mapping will not take effect.',
|
||||||
},
|
},
|
||||||
|
kiro: {
|
||||||
|
baseUrlHint: 'Enter the Base URL of the Kiro-compatible upstream',
|
||||||
|
apiKeyHint: 'API Key for that Kiro upstream',
|
||||||
|
},
|
||||||
anthropic: {
|
anthropic: {
|
||||||
apiKeyPassthrough: 'Auto passthrough (auth only)',
|
apiKeyPassthrough: 'Auto passthrough (auth only)',
|
||||||
apiKeyPassthroughDesc:
|
apiKeyPassthroughDesc:
|
||||||
@@ -3261,20 +3273,65 @@ export default {
|
|||||||
authCode: 'Authorization URL or Code',
|
authCode: 'Authorization URL or Code',
|
||||||
authCodePlaceholder:
|
authCodePlaceholder:
|
||||||
'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value',
|
'Option 1: Copy the complete URL\n(http://localhost:xxx/auth/callback?code=...)\nOption 2: Copy only the code parameter value',
|
||||||
authCodeHint: 'You can copy the entire URL or just the code parameter value, the system will auto-detect',
|
authCodeHint: 'You can copy the entire URL or just the code parameter value, the system will auto-detect',
|
||||||
failedToGenerateUrl: 'Failed to generate Antigravity auth URL',
|
failedToGenerateUrl: 'Failed to generate Antigravity auth URL',
|
||||||
missingExchangeParams: 'Missing code, session ID, or state',
|
missingExchangeParams: 'Missing code, session ID, or state',
|
||||||
failedToExchangeCode: 'Failed to exchange Antigravity auth code',
|
failedToExchangeCode: 'Failed to exchange Antigravity auth code',
|
||||||
// Refresh Token auth
|
refreshTokenAuth: 'Manual RT',
|
||||||
refreshTokenAuth: 'Manual RT',
|
refreshTokenDesc: 'Enter your existing Antigravity Refresh Token. Supports batch input (one per line). The system will automatically validate and create accounts.',
|
||||||
refreshTokenDesc: 'Enter your existing Antigravity Refresh Token. Supports batch input (one per line). The system will automatically validate and create accounts.',
|
refreshTokenPlaceholder: 'Paste your Antigravity Refresh Token...\nSupports multiple tokens, one per line',
|
||||||
refreshTokenPlaceholder: 'Paste your Antigravity Refresh Token...\nSupports multiple tokens, one per line',
|
validating: 'Validating...',
|
||||||
validating: 'Validating...',
|
validateAndCreate: 'Validate & Create',
|
||||||
validateAndCreate: 'Validate & Create',
|
pleaseEnterRefreshToken: 'Please enter Refresh Token',
|
||||||
pleaseEnterRefreshToken: 'Please enter Refresh Token',
|
failedToValidateRT: 'Failed to validate Refresh Token'
|
||||||
failedToValidateRT: 'Failed to validate Refresh Token'
|
},
|
||||||
}
|
kiro: {
|
||||||
}, // Gemini specific (platform-wide)
|
title: 'Kiro Authorization',
|
||||||
|
followSteps: 'Follow these steps to authorize your Kiro account:',
|
||||||
|
step1GenerateUrl: 'Click the button below to generate the authorization URL',
|
||||||
|
generateAuthUrl: 'Generate Authorization URL',
|
||||||
|
step2OpenUrl: 'Open the URL in your browser and complete authorization',
|
||||||
|
openUrlDesc:
|
||||||
|
'Open the authorization URL in a new tab. The Kiro sign-in page will open at app.kiro.dev; choose Google or GitHub there. After approval, the browser may redirect to http://localhost:49153/oauth/callback and show an unreachable-page error; that is expected.',
|
||||||
|
step3EnterCode: 'Enter Callback URL or Code',
|
||||||
|
authCodeDesc:
|
||||||
|
'After authorization, copy the full callback URL from the browser address bar (recommended), or paste only the code parameter value below.',
|
||||||
|
authCode: 'Callback URL or Code',
|
||||||
|
authCodePlaceholder:
|
||||||
|
'Option 1 (recommended): Paste the full callback URL\n(http://localhost:49153/oauth/callback?code=...&state=...&login_option=github)\nOption 2: Paste only the code value',
|
||||||
|
authCodeHint:
|
||||||
|
'The system will auto-extract code/state and Kiro callback metadata from the URL. If the localhost page cannot be reached, copy the full URL from the address bar.',
|
||||||
|
importDialogTitle: 'Import Kiro Token',
|
||||||
|
authModeTitle: 'Kiro Authorization Method',
|
||||||
|
oauthTitle: 'Social OAuth',
|
||||||
|
oauthSubtitle: 'Browser-based auth with Google or GitHub',
|
||||||
|
oauthProviderTitle: 'Social Sign-In Provider',
|
||||||
|
googleTitle: 'Google',
|
||||||
|
githubTitle: 'GitHub',
|
||||||
|
googleDesc: 'Sign in to Kiro with your Google account',
|
||||||
|
githubDesc: 'Sign in to Kiro with your GitHub account',
|
||||||
|
idcTitle: 'AWS Builder ID / IDC',
|
||||||
|
importTitle: 'Import from Kiro IDE',
|
||||||
|
socialSubtitle: 'Google / GitHub sign-in',
|
||||||
|
idcSubtitle: 'AWS Builder ID or enterprise Identity Center',
|
||||||
|
googleOauth: 'Google OAuth',
|
||||||
|
githubOauth: 'GitHub OAuth',
|
||||||
|
idcLogin: 'Builder ID / IDC Login',
|
||||||
|
importTokenFile: 'Import Token File',
|
||||||
|
importSubtitle: 'Use this if you already signed in via Kiro IDE',
|
||||||
|
startUrlLabel: 'Builder ID / IDC Start URL',
|
||||||
|
idcStartUrlLabel: 'Builder ID / IDC Start URL',
|
||||||
|
startUrlPlaceholder: 'https://view.awsapps.com/start',
|
||||||
|
regionLabel: 'Region',
|
||||||
|
regionPlaceholder: 'us-east-1',
|
||||||
|
tokenJsonLabel: 'Kiro Token JSON',
|
||||||
|
tokenJsonHint: 'Sign in through Kiro IDE first, then paste the contents of `~/.aws/sso/cache/kiro-auth-token.json` here.',
|
||||||
|
deviceRegistrationLabel: 'Device Registration JSON',
|
||||||
|
deviceRegistrationHint: 'Optional. Only needed when the token file does not include full client details and only has `clientIdHash`.',
|
||||||
|
importAndUpdate: 'Import and Update'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Gemini specific (platform-wide)
|
||||||
gemini: {
|
gemini: {
|
||||||
helpButton: 'Help',
|
helpButton: 'Help',
|
||||||
helpDialog: {
|
helpDialog: {
|
||||||
@@ -3416,6 +3473,7 @@ export default {
|
|||||||
claudeCodeAccount: 'Claude Code Account',
|
claudeCodeAccount: 'Claude Code Account',
|
||||||
openaiAccount: 'OpenAI Account',
|
openaiAccount: 'OpenAI Account',
|
||||||
geminiAccount: 'Gemini Account',
|
geminiAccount: 'Gemini Account',
|
||||||
|
kiroAccount: 'Kiro Account',
|
||||||
antigravityAccount: 'Antigravity Account',
|
antigravityAccount: 'Antigravity Account',
|
||||||
inputMethod: 'Input Method',
|
inputMethod: 'Input Method',
|
||||||
reAuthorizedSuccess: 'Account re-authorized successfully',
|
reAuthorizedSuccess: 'Account re-authorized successfully',
|
||||||
@@ -3491,6 +3549,12 @@ export default {
|
|||||||
gemini3Flash: 'G3F',
|
gemini3Flash: 'G3F',
|
||||||
gemini3Image: 'G31FI',
|
gemini3Image: 'G31FI',
|
||||||
claude: 'Claude',
|
claude: 'Claude',
|
||||||
|
kiroCredits: 'Credits',
|
||||||
|
kiroBonus: 'Bonus',
|
||||||
|
kiroReset: 'Reset',
|
||||||
|
kiroOverage: 'Overage',
|
||||||
|
kiroDaysLeft: '{days}d left',
|
||||||
|
kiroExpires: 'Expires',
|
||||||
passiveSampled: 'Passive',
|
passiveSampled: 'Passive',
|
||||||
activeQuery: 'Query'
|
activeQuery: 'Query'
|
||||||
},
|
},
|
||||||
@@ -3513,6 +3577,13 @@ export default {
|
|||||||
copyLink: 'Copy Link',
|
copyLink: 'Copy Link',
|
||||||
linkCopied: 'Link Copied',
|
linkCopied: 'Link Copied',
|
||||||
needsReauth: 'Re-auth Required',
|
needsReauth: 'Re-auth Required',
|
||||||
|
kiroCooldown: 'Kiro Cooldown',
|
||||||
|
kiroSuspended: 'Kiro Suspended',
|
||||||
|
kiroProfileError: 'Profile Error',
|
||||||
|
kiroProfileHint: 'Resolve and save a valid profileArn for this account',
|
||||||
|
kiroUsageForbidden: 'Usage Forbidden',
|
||||||
|
kiroUsageForbiddenHint: 'Usage fetch is blocked for this account, but request forwarding may still work',
|
||||||
|
kiroRuntimeResetsAt: 'Auto resumes at {time}',
|
||||||
rateLimited: 'Rate Limited',
|
rateLimited: 'Rate Limited',
|
||||||
usageError: 'Fetch Error'
|
usageError: 'Fetch Error'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2007,6 +2007,7 @@ export default {
|
|||||||
openai: 'OpenAI',
|
openai: 'OpenAI',
|
||||||
gemini: 'Gemini',
|
gemini: 'Gemini',
|
||||||
antigravity: 'Antigravity',
|
antigravity: 'Antigravity',
|
||||||
|
kiro: 'Kiro',
|
||||||
},
|
},
|
||||||
saving: '保存中...',
|
saving: '保存中...',
|
||||||
noGroups: '暂无分组',
|
noGroups: '暂无分组',
|
||||||
@@ -2761,6 +2762,7 @@ export default {
|
|||||||
anthropic: 'Anthropic',
|
anthropic: 'Anthropic',
|
||||||
gemini: 'Gemini',
|
gemini: 'Gemini',
|
||||||
antigravity: 'Antigravity',
|
antigravity: 'Antigravity',
|
||||||
|
kiro: 'Kiro',
|
||||||
},
|
},
|
||||||
types: {
|
types: {
|
||||||
oauth: 'OAuth',
|
oauth: 'OAuth',
|
||||||
@@ -2768,6 +2770,8 @@ export default {
|
|||||||
responsesApi: 'Responses API',
|
responsesApi: 'Responses API',
|
||||||
googleOauth: 'Google OAuth',
|
googleOauth: 'Google OAuth',
|
||||||
codeAssist: 'Code Assist',
|
codeAssist: 'Code Assist',
|
||||||
|
kiroOauth: '社交 OAuth / AWS Builder ID / 导入',
|
||||||
|
kiroApikey: '通过 Base URL + API Key 连接',
|
||||||
antigravityOauth: 'Antigravity OAuth',
|
antigravityOauth: 'Antigravity OAuth',
|
||||||
antigravityApikey: '通过 Base URL + API Key 连接',
|
antigravityApikey: '通过 Base URL + API Key 连接',
|
||||||
upstream: '对接上游',
|
upstream: '对接上游',
|
||||||
@@ -2791,8 +2795,12 @@ export default {
|
|||||||
rateLimitedAutoResume: '{time} 自动恢复',
|
rateLimitedAutoResume: '{time} 自动恢复',
|
||||||
modelRateLimitedUntil: '{model} 限流至 {time}',
|
modelRateLimitedUntil: '{model} 限流至 {time}',
|
||||||
modelCreditOveragesUntil: '{model} 正在使用 AI Credits,至 {time}',
|
modelCreditOveragesUntil: '{model} 正在使用 AI Credits,至 {time}',
|
||||||
|
overageActive: '超量',
|
||||||
|
overageActiveUntil: '当前正在使用超量额度,重置窗口时间 {time}',
|
||||||
creditsExhausted: '积分已用尽',
|
creditsExhausted: '积分已用尽',
|
||||||
creditsExhaustedUntil: 'AI Credits 已用尽,预计 {time} 恢复',
|
creditsExhaustedUntil: 'AI Credits 已用尽,预计 {time} 恢复',
|
||||||
|
overageExhausted: '超量额度已用尽',
|
||||||
|
overageExhaustedUntil: '超量额度已用尽,预计 {time} 恢复',
|
||||||
overloadedUntil: '负载过重,重置时间:{time}',
|
overloadedUntil: '负载过重,重置时间:{time}',
|
||||||
viewTempUnschedDetails: '查看临时不可调度详情'
|
viewTempUnschedDetails: '查看临时不可调度详情'
|
||||||
},
|
},
|
||||||
@@ -2848,6 +2856,12 @@ export default {
|
|||||||
gemini3Flash: 'G3F',
|
gemini3Flash: 'G3F',
|
||||||
gemini3Image: 'G31FI',
|
gemini3Image: 'G31FI',
|
||||||
claude: 'Claude',
|
claude: 'Claude',
|
||||||
|
kiroCredits: 'Credits',
|
||||||
|
kiroBonus: 'Bonus',
|
||||||
|
kiroReset: '重置',
|
||||||
|
kiroOverage: '超额',
|
||||||
|
kiroDaysLeft: '剩余 {days} 天',
|
||||||
|
kiroExpires: '到期',
|
||||||
passiveSampled: '被动采样',
|
passiveSampled: '被动采样',
|
||||||
activeQuery: '查询'
|
activeQuery: '查询'
|
||||||
},
|
},
|
||||||
@@ -2870,6 +2884,13 @@ export default {
|
|||||||
copyLink: '复制链接',
|
copyLink: '复制链接',
|
||||||
linkCopied: '链接已复制',
|
linkCopied: '链接已复制',
|
||||||
needsReauth: '需要重新授权',
|
needsReauth: '需要重新授权',
|
||||||
|
kiroCooldown: 'Kiro 冷却中',
|
||||||
|
kiroSuspended: 'Kiro 暂停中',
|
||||||
|
kiroProfileError: 'Profile 配置异常',
|
||||||
|
kiroProfileHint: '请重新解析或保存可用的 profileArn',
|
||||||
|
kiroUsageForbidden: 'Usage 无权访问',
|
||||||
|
kiroUsageForbiddenHint: '该账号当前无法读取 Kiro Usage,但不代表转发一定不可用',
|
||||||
|
kiroRuntimeResetsAt: '预计 {time} 自动恢复',
|
||||||
rateLimited: '限流中',
|
rateLimited: '限流中',
|
||||||
usageError: '获取失败',
|
usageError: '获取失败',
|
||||||
form: {
|
form: {
|
||||||
@@ -3037,6 +3058,10 @@ export default {
|
|||||||
testModeCompact: 'Compact 探测',
|
testModeCompact: 'Compact 探测',
|
||||||
modelRestrictionDisabledByPassthrough: '已开启自动透传:模型白名单/映射不会生效。',
|
modelRestrictionDisabledByPassthrough: '已开启自动透传:模型白名单/映射不会生效。',
|
||||||
},
|
},
|
||||||
|
kiro: {
|
||||||
|
baseUrlHint: '请输入 Kiro 兼容上游的 Base URL',
|
||||||
|
apiKeyHint: '用于该 Kiro 上游的 API Key',
|
||||||
|
},
|
||||||
anthropic: {
|
anthropic: {
|
||||||
apiKeyPassthrough: '自动透传(仅替换认证)',
|
apiKeyPassthrough: '自动透传(仅替换认证)',
|
||||||
apiKeyPassthroughDesc:
|
apiKeyPassthroughDesc:
|
||||||
@@ -3404,6 +3429,50 @@ export default {
|
|||||||
validateAndCreate: '验证并创建账号',
|
validateAndCreate: '验证并创建账号',
|
||||||
pleaseEnterRefreshToken: '请输入 Refresh Token',
|
pleaseEnterRefreshToken: '请输入 Refresh Token',
|
||||||
failedToValidateRT: '验证 Refresh Token 失败'
|
failedToValidateRT: '验证 Refresh Token 失败'
|
||||||
|
},
|
||||||
|
kiro: {
|
||||||
|
title: 'Kiro 授权',
|
||||||
|
followSteps: '按照以下步骤授权您的 Kiro 账号:',
|
||||||
|
step1GenerateUrl: '点击下方按钮生成授权 URL',
|
||||||
|
generateAuthUrl: '生成授权 URL',
|
||||||
|
step2OpenUrl: '在浏览器中打开 URL 并完成授权',
|
||||||
|
openUrlDesc:
|
||||||
|
'在新标签页中打开授权 URL。浏览器会先进入 app.kiro.dev 的 Kiro 登录页,请在那里选择 Google 或 GitHub。授权后浏览器可能跳转到 http://localhost:49153/oauth/callback 并提示无法访问,这是正常现象。',
|
||||||
|
step3EnterCode: '输入回调链接或 Code',
|
||||||
|
authCodeDesc:
|
||||||
|
'授权完成后,请复制浏览器地址栏中的完整回调链接(推荐),或仅复制其中的 code 参数值并粘贴到下方。',
|
||||||
|
authCode: '回调链接或 Code',
|
||||||
|
authCodePlaceholder:
|
||||||
|
'方式1(推荐):粘贴完整回调链接\n(http://localhost:49153/oauth/callback?code=...&state=...&login_option=github)\n方式2:仅粘贴 code 参数值',
|
||||||
|
authCodeHint: '系统会自动从链接中解析 code/state 以及 Kiro 回调元数据;若无法访问 localhost 页面,请直接复制地址栏完整链接。',
|
||||||
|
importDialogTitle: '导入 Kiro Token',
|
||||||
|
authModeTitle: 'Kiro 授权方式',
|
||||||
|
oauthTitle: '社交 OAuth',
|
||||||
|
oauthSubtitle: '浏览器授权,支持 Google 或 GitHub',
|
||||||
|
oauthProviderTitle: '社交登录提供商',
|
||||||
|
googleTitle: 'Google',
|
||||||
|
githubTitle: 'GitHub',
|
||||||
|
googleDesc: '使用 Google 账号登录 Kiro',
|
||||||
|
githubDesc: '使用 GitHub 账号登录 Kiro',
|
||||||
|
idcTitle: 'AWS Builder ID / IDC',
|
||||||
|
importTitle: '从 Kiro IDE 导入',
|
||||||
|
socialSubtitle: 'Google / GitHub 登录',
|
||||||
|
idcSubtitle: 'AWS Builder ID 或企业 Identity Center',
|
||||||
|
googleOauth: 'Google OAuth',
|
||||||
|
githubOauth: 'GitHub OAuth',
|
||||||
|
idcLogin: 'Builder ID / IDC 登录',
|
||||||
|
importTokenFile: '导入 Token 文件',
|
||||||
|
importSubtitle: '已在 Kiro IDE 登录时使用',
|
||||||
|
startUrlLabel: 'Builder ID / IDC Start URL',
|
||||||
|
idcStartUrlLabel: 'Builder ID / IDC Start URL',
|
||||||
|
startUrlPlaceholder: 'https://view.awsapps.com/start',
|
||||||
|
regionLabel: 'Region',
|
||||||
|
regionPlaceholder: 'us-east-1',
|
||||||
|
tokenJsonLabel: 'Kiro Token JSON',
|
||||||
|
tokenJsonHint: '先在 Kiro IDE 完成登录,再粘贴 `~/.aws/sso/cache/kiro-auth-token.json` 的内容。',
|
||||||
|
deviceRegistrationLabel: 'Device Registration JSON',
|
||||||
|
deviceRegistrationHint: '可选。只有 token 文件里缺少完整客户端信息、只剩 `clientIdHash` 时才需要补充。',
|
||||||
|
importAndUpdate: '导入并更新'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Gemini specific (platform-wide)
|
// Gemini specific (platform-wide)
|
||||||
@@ -3546,6 +3615,7 @@ export default {
|
|||||||
claudeCodeAccount: 'Claude Code 账号',
|
claudeCodeAccount: 'Claude Code 账号',
|
||||||
openaiAccount: 'OpenAI 账号',
|
openaiAccount: 'OpenAI 账号',
|
||||||
geminiAccount: 'Gemini 账号',
|
geminiAccount: 'Gemini 账号',
|
||||||
|
kiroAccount: 'Kiro 账号',
|
||||||
antigravityAccount: 'Antigravity 账号',
|
antigravityAccount: 'Antigravity 账号',
|
||||||
inputMethod: '输入方式',
|
inputMethod: '输入方式',
|
||||||
reAuthorizedSuccess: '账号重新授权成功',
|
reAuthorizedSuccess: '账号重新授权成功',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* instead of defining their own color mappings.
|
* instead of defining their own color mappings.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type Platform = 'anthropic' | 'openai' | 'antigravity' | 'gemini'
|
export type Platform = 'anthropic' | 'openai' | 'antigravity' | 'gemini' | 'kiro'
|
||||||
|
|
||||||
// ── Badge (bg + text + border, for inline badges with border) ───────
|
// ── Badge (bg + text + border, for inline badges with border) ───────
|
||||||
const BADGE: Record<Platform, string> = {
|
const BADGE: Record<Platform, string> = {
|
||||||
@@ -13,6 +13,7 @@ const BADGE: Record<Platform, string> = {
|
|||||||
openai: 'bg-green-500/10 text-green-600 border-green-500/30 dark:text-green-400',
|
openai: 'bg-green-500/10 text-green-600 border-green-500/30 dark:text-green-400',
|
||||||
antigravity: 'bg-purple-500/10 text-purple-600 border-purple-500/30 dark:text-purple-400',
|
antigravity: 'bg-purple-500/10 text-purple-600 border-purple-500/30 dark:text-purple-400',
|
||||||
gemini: 'bg-blue-500/10 text-blue-600 border-blue-500/30 dark:text-blue-400',
|
gemini: 'bg-blue-500/10 text-blue-600 border-blue-500/30 dark:text-blue-400',
|
||||||
|
kiro: 'bg-orange-500/10 text-orange-600 border-orange-500/30 dark:text-orange-400',
|
||||||
}
|
}
|
||||||
const BADGE_DEFAULT = 'bg-slate-500/10 text-slate-600 border-slate-500/30 dark:text-slate-400'
|
const BADGE_DEFAULT = 'bg-slate-500/10 text-slate-600 border-slate-500/30 dark:text-slate-400'
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ const BADGE_LIGHT: Record<Platform, string> = {
|
|||||||
openai: 'bg-green-500/10 text-green-600 dark:bg-green-500/10 dark:text-green-300',
|
openai: 'bg-green-500/10 text-green-600 dark:bg-green-500/10 dark:text-green-300',
|
||||||
antigravity: 'bg-purple-500/10 text-purple-600 dark:bg-purple-500/10 dark:text-purple-300',
|
antigravity: 'bg-purple-500/10 text-purple-600 dark:bg-purple-500/10 dark:text-purple-300',
|
||||||
gemini: 'bg-blue-500/10 text-blue-600 dark:bg-blue-500/10 dark:text-blue-300',
|
gemini: 'bg-blue-500/10 text-blue-600 dark:bg-blue-500/10 dark:text-blue-300',
|
||||||
|
kiro: 'bg-orange-500/10 text-orange-600 dark:bg-orange-500/10 dark:text-orange-300',
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Border ──────────────────────────────────────────────────────────
|
// ── Border ──────────────────────────────────────────────────────────
|
||||||
@@ -30,6 +32,7 @@ const BORDER: Record<Platform, string> = {
|
|||||||
openai: 'border-green-500/20 dark:border-green-500/20',
|
openai: 'border-green-500/20 dark:border-green-500/20',
|
||||||
antigravity: 'border-purple-500/20 dark:border-purple-500/20',
|
antigravity: 'border-purple-500/20 dark:border-purple-500/20',
|
||||||
gemini: 'border-blue-500/20 dark:border-blue-500/20',
|
gemini: 'border-blue-500/20 dark:border-blue-500/20',
|
||||||
|
kiro: 'border-orange-500/20 dark:border-orange-500/20',
|
||||||
}
|
}
|
||||||
const BORDER_DEFAULT = 'border-gray-200 dark:border-dark-700'
|
const BORDER_DEFAULT = 'border-gray-200 dark:border-dark-700'
|
||||||
|
|
||||||
@@ -39,6 +42,7 @@ const ACCENT_BAR: Record<Platform, string> = {
|
|||||||
openai: 'bg-gradient-to-r from-emerald-400 to-emerald-500',
|
openai: 'bg-gradient-to-r from-emerald-400 to-emerald-500',
|
||||||
antigravity: 'bg-gradient-to-r from-purple-400 to-purple-500',
|
antigravity: 'bg-gradient-to-r from-purple-400 to-purple-500',
|
||||||
gemini: 'bg-gradient-to-r from-blue-400 to-blue-500',
|
gemini: 'bg-gradient-to-r from-blue-400 to-blue-500',
|
||||||
|
kiro: 'bg-gradient-to-r from-orange-400 to-orange-500',
|
||||||
}
|
}
|
||||||
const ACCENT_BAR_DEFAULT = 'bg-gradient-to-r from-primary-400 to-primary-500'
|
const ACCENT_BAR_DEFAULT = 'bg-gradient-to-r from-primary-400 to-primary-500'
|
||||||
|
|
||||||
@@ -48,6 +52,7 @@ const TEXT: Record<Platform, string> = {
|
|||||||
openai: 'text-emerald-600 dark:text-emerald-400',
|
openai: 'text-emerald-600 dark:text-emerald-400',
|
||||||
antigravity: 'text-purple-600 dark:text-purple-400',
|
antigravity: 'text-purple-600 dark:text-purple-400',
|
||||||
gemini: 'text-blue-600 dark:text-blue-400',
|
gemini: 'text-blue-600 dark:text-blue-400',
|
||||||
|
kiro: 'text-orange-600 dark:text-orange-400',
|
||||||
}
|
}
|
||||||
const TEXT_DEFAULT = 'text-primary-600 dark:text-primary-400'
|
const TEXT_DEFAULT = 'text-primary-600 dark:text-primary-400'
|
||||||
|
|
||||||
@@ -57,6 +62,7 @@ const ICON: Record<Platform, string> = {
|
|||||||
openai: 'text-emerald-500 dark:text-emerald-400',
|
openai: 'text-emerald-500 dark:text-emerald-400',
|
||||||
antigravity: 'text-purple-500 dark:text-purple-400',
|
antigravity: 'text-purple-500 dark:text-purple-400',
|
||||||
gemini: 'text-blue-500 dark:text-blue-400',
|
gemini: 'text-blue-500 dark:text-blue-400',
|
||||||
|
kiro: 'text-orange-500 dark:text-orange-400',
|
||||||
}
|
}
|
||||||
const ICON_DEFAULT = 'text-primary-500 dark:text-primary-400'
|
const ICON_DEFAULT = 'text-primary-500 dark:text-primary-400'
|
||||||
|
|
||||||
@@ -66,6 +72,7 @@ const BUTTON: Record<Platform, string> = {
|
|||||||
openai: 'bg-green-600 text-white hover:bg-green-700 active:bg-green-800 dark:bg-green-600/80 dark:hover:bg-green-600',
|
openai: 'bg-green-600 text-white hover:bg-green-700 active:bg-green-800 dark:bg-green-600/80 dark:hover:bg-green-600',
|
||||||
antigravity: 'bg-purple-500 text-white hover:bg-purple-600 active:bg-purple-700 dark:bg-purple-500/80 dark:hover:bg-purple-500',
|
antigravity: 'bg-purple-500 text-white hover:bg-purple-600 active:bg-purple-700 dark:bg-purple-500/80 dark:hover:bg-purple-500',
|
||||||
gemini: 'bg-blue-500 text-white hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-500/80 dark:hover:bg-blue-500',
|
gemini: 'bg-blue-500 text-white hover:bg-blue-600 active:bg-blue-700 dark:bg-blue-500/80 dark:hover:bg-blue-500',
|
||||||
|
kiro: 'bg-orange-500 text-white hover:bg-orange-600 active:bg-orange-700 dark:bg-orange-500/80 dark:hover:bg-orange-500',
|
||||||
}
|
}
|
||||||
const BUTTON_DEFAULT = 'bg-primary-500 text-white hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-500'
|
const BUTTON_DEFAULT = 'bg-primary-500 text-white hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-500'
|
||||||
|
|
||||||
@@ -75,6 +82,7 @@ const DISCOUNT: Record<Platform, string> = {
|
|||||||
openai: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
|
openai: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
|
||||||
antigravity: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
|
antigravity: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
|
||||||
gemini: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
|
gemini: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
|
||||||
|
kiro: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300',
|
||||||
}
|
}
|
||||||
const DISCOUNT_DEFAULT = 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
|
const DISCOUNT_DEFAULT = 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
|
||||||
|
|
||||||
@@ -84,6 +92,7 @@ const GRADIENT: Record<Platform, string> = {
|
|||||||
openai: 'from-emerald-500 to-emerald-600',
|
openai: 'from-emerald-500 to-emerald-600',
|
||||||
antigravity: 'from-purple-500 to-purple-600',
|
antigravity: 'from-purple-500 to-purple-600',
|
||||||
gemini: 'from-blue-500 to-blue-600',
|
gemini: 'from-blue-500 to-blue-600',
|
||||||
|
kiro: 'from-orange-500 to-orange-600',
|
||||||
}
|
}
|
||||||
const GRADIENT_DEFAULT = 'from-primary-500 to-primary-600'
|
const GRADIENT_DEFAULT = 'from-primary-500 to-primary-600'
|
||||||
|
|
||||||
@@ -93,6 +102,7 @@ const GRADIENT_TEXT: Record<Platform, string> = {
|
|||||||
openai: 'text-emerald-100',
|
openai: 'text-emerald-100',
|
||||||
antigravity: 'text-purple-100',
|
antigravity: 'text-purple-100',
|
||||||
gemini: 'text-blue-100',
|
gemini: 'text-blue-100',
|
||||||
|
kiro: 'text-orange-100',
|
||||||
}
|
}
|
||||||
const GRADIENT_TEXT_DEFAULT = 'text-primary-100'
|
const GRADIENT_TEXT_DEFAULT = 'text-primary-100'
|
||||||
|
|
||||||
@@ -101,13 +111,14 @@ const GRADIENT_SUBTEXT: Record<Platform, string> = {
|
|||||||
openai: 'text-emerald-200',
|
openai: 'text-emerald-200',
|
||||||
antigravity: 'text-purple-200',
|
antigravity: 'text-purple-200',
|
||||||
gemini: 'text-blue-200',
|
gemini: 'text-blue-200',
|
||||||
|
kiro: 'text-orange-200',
|
||||||
}
|
}
|
||||||
const GRADIENT_SUBTEXT_DEFAULT = 'text-primary-200'
|
const GRADIENT_SUBTEXT_DEFAULT = 'text-primary-200'
|
||||||
|
|
||||||
// ── Public API ──────────────────────────────────────────────────────
|
// ── Public API ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
function isPlatform(p: string): p is Platform {
|
function isPlatform(p: string): p is Platform {
|
||||||
return p === 'anthropic' || p === 'openai' || p === 'antigravity' || p === 'gemini'
|
return p === 'anthropic' || p === 'openai' || p === 'antigravity' || p === 'gemini' || p === 'kiro'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function platformBadgeClass(p: string): string {
|
export function platformBadgeClass(p: string): string {
|
||||||
@@ -160,6 +171,7 @@ export function platformLabel(p: string): string {
|
|||||||
case 'openai': return 'OpenAI'
|
case 'openai': return 'OpenAI'
|
||||||
case 'antigravity': return 'Antigravity'
|
case 'antigravity': return 'Antigravity'
|
||||||
case 'gemini': return 'Gemini'
|
case 'gemini': return 'Gemini'
|
||||||
|
case 'kiro': return 'Kiro'
|
||||||
default: return p || 'API'
|
default: return p || 'API'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,14 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #cell-platform_type="{ row }">
|
<template #cell-platform_type="{ row }">
|
||||||
<div class="flex flex-wrap items-center gap-1">
|
<div class="flex flex-wrap items-center gap-1">
|
||||||
<PlatformTypeBadge :platform="row.platform" :type="row.type" :plan-type="row.credentials?.plan_type" :privacy-mode="row.extra?.privacy_mode" :subscription-expires-at="row.credentials?.subscription_expires_at" />
|
<PlatformTypeBadge
|
||||||
|
:platform="row.platform"
|
||||||
|
:type="row.type"
|
||||||
|
:plan-type="row.credentials?.plan_type"
|
||||||
|
:overages-enabled="isKiroOveragesEnabled(row)"
|
||||||
|
:privacy-mode="row.extra?.privacy_mode"
|
||||||
|
:subscription-expires-at="row.credentials?.subscription_expires_at"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="getOpenAICompactLabel(row)"
|
v-if="getOpenAICompactLabel(row)"
|
||||||
:class="['inline-block rounded px-1.5 py-0.5 text-[10px] font-medium', getOpenAICompactClass(row)]"
|
:class="['inline-block rounded px-1.5 py-0.5 text-[10px] font-medium', getOpenAICompactClass(row)]"
|
||||||
@@ -242,6 +249,7 @@
|
|||||||
:today-stats="todayStatsByAccountId[String(row.id)] ?? null"
|
:today-stats="todayStatsByAccountId[String(row.id)] ?? null"
|
||||||
:today-stats-loading="todayStatsLoading"
|
:today-stats-loading="todayStatsLoading"
|
||||||
:manual-refresh-token="usageManualRefreshToken"
|
:manual-refresh-token="usageManualRefreshToken"
|
||||||
|
@kiro-usage-meta="handleKiroUsageMeta(row, $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #cell-proxy="{ row }">
|
<template #cell-proxy="{ row }">
|
||||||
@@ -836,12 +844,31 @@ const shouldReplaceAutoRefreshRow = (current: Account, next: Account) => {
|
|||||||
current.schedulable !== next.schedulable ||
|
current.schedulable !== next.schedulable ||
|
||||||
current.status !== next.status ||
|
current.status !== next.status ||
|
||||||
current.rate_limit_reset_at !== next.rate_limit_reset_at ||
|
current.rate_limit_reset_at !== next.rate_limit_reset_at ||
|
||||||
|
current.kiro_quota_state !== next.kiro_quota_state ||
|
||||||
|
current.kiro_quota_reason !== next.kiro_quota_reason ||
|
||||||
|
current.kiro_quota_reset_at !== next.kiro_quota_reset_at ||
|
||||||
|
current.kiro_runtime_state !== next.kiro_runtime_state ||
|
||||||
|
current.kiro_runtime_reason !== next.kiro_runtime_reason ||
|
||||||
|
current.kiro_runtime_reset_at !== next.kiro_runtime_reset_at ||
|
||||||
current.overload_until !== next.overload_until ||
|
current.overload_until !== next.overload_until ||
|
||||||
current.temp_unschedulable_until !== next.temp_unschedulable_until ||
|
current.temp_unschedulable_until !== next.temp_unschedulable_until ||
|
||||||
buildOpenAIUsageRefreshKey(current) !== buildOpenAIUsageRefreshKey(next)
|
buildOpenAIUsageRefreshKey(current) !== buildOpenAIUsageRefreshKey(next)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isKiroOveragesEnabled = (account: Account) => {
|
||||||
|
return account.platform === 'kiro' && account.credentials?.kiro_overages_enabled === true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKiroUsageMeta = (account: Account, meta: { plan_type?: string; kiro_overages_enabled: boolean }) => {
|
||||||
|
if (account.platform !== 'kiro') return
|
||||||
|
account.credentials = {
|
||||||
|
...(account.credentials || {}),
|
||||||
|
...(meta.plan_type ? { plan_type: meta.plan_type } : {}),
|
||||||
|
kiro_overages_enabled: meta.kiro_overages_enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const syncAccountRefs = (nextAccount: Account) => {
|
const syncAccountRefs = (nextAccount: Account) => {
|
||||||
if (edAcc.value?.id === nextAccount.id) edAcc.value = nextAccount
|
if (edAcc.value?.id === nextAccount.id) edAcc.value = nextAccount
|
||||||
if (reAuthAcc.value?.id === nextAccount.id) reAuthAcc.value = nextAccount
|
if (reAuthAcc.value?.id === nextAccount.id) reAuthAcc.value = nextAccount
|
||||||
@@ -1331,7 +1358,13 @@ const accountMatchesCurrentFilters = (account: Account) => {
|
|||||||
if (filters.status) {
|
if (filters.status) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const rateLimitResetAt = account.rate_limit_reset_at ? new Date(account.rate_limit_reset_at).getTime() : Number.NaN
|
const rateLimitResetAt = account.rate_limit_reset_at ? new Date(account.rate_limit_reset_at).getTime() : Number.NaN
|
||||||
const isRateLimited = Number.isFinite(rateLimitResetAt) && rateLimitResetAt > now
|
const kiroRuntimeResetAt = account.kiro_runtime_reset_at ? new Date(account.kiro_runtime_reset_at).getTime() : Number.NaN
|
||||||
|
const isKiroRuntimeLimited =
|
||||||
|
account.platform === 'kiro' &&
|
||||||
|
account.kiro_runtime_state === 'cooldown' &&
|
||||||
|
Number.isFinite(kiroRuntimeResetAt) &&
|
||||||
|
kiroRuntimeResetAt > now
|
||||||
|
const isRateLimited = (Number.isFinite(rateLimitResetAt) && rateLimitResetAt > now) || isKiroRuntimeLimited
|
||||||
const tempUnschedUntil = account.temp_unschedulable_until ? new Date(account.temp_unschedulable_until).getTime() : Number.NaN
|
const tempUnschedUntil = account.temp_unschedulable_until ? new Date(account.temp_unschedulable_until).getTime() : Number.NaN
|
||||||
const isTempUnschedulable = Number.isFinite(tempUnschedUntil) && tempUnschedUntil > now
|
const isTempUnschedulable = Number.isFinite(tempUnschedUntil) && tempUnschedUntil > now
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',
|
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',
|
||||||
value === 'anthropic'
|
value === 'anthropic' || value === 'kiro'
|
||||||
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||||
: value === 'openai'
|
: value === 'openai'
|
||||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||||
@@ -1137,10 +1137,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 账号过滤控制 (OpenAI/Antigravity/Anthropic/Gemini) -->
|
<!-- 账号过滤控制 (OpenAI/Antigravity/Anthropic/Gemini/Kiro) -->
|
||||||
<div
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
['openai', 'antigravity', 'anthropic', 'gemini'].includes(
|
['openai', 'antigravity', 'anthropic', 'gemini', 'kiro'].includes(
|
||||||
createForm.platform,
|
createForm.platform,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@@ -2657,7 +2657,7 @@
|
|||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',
|
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',
|
||||||
group.platform === 'anthropic'
|
group.platform === 'anthropic' || group.platform === 'kiro'
|
||||||
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||||||
: group.platform === 'openai'
|
: group.platform === 'openai'
|
||||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||||
@@ -2825,6 +2825,7 @@ const platformOptions = computed(() => [
|
|||||||
{ value: "openai", label: "OpenAI" },
|
{ value: "openai", label: "OpenAI" },
|
||||||
{ value: "gemini", label: "Gemini" },
|
{ value: "gemini", label: "Gemini" },
|
||||||
{ value: "antigravity", label: "Antigravity" },
|
{ value: "antigravity", label: "Antigravity" },
|
||||||
|
{ value: "kiro", label: "Kiro" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const platformFilterOptions = computed(() => [
|
const platformFilterOptions = computed(() => [
|
||||||
@@ -2833,6 +2834,7 @@ const platformFilterOptions = computed(() => [
|
|||||||
{ value: "openai", label: "OpenAI" },
|
{ value: "openai", label: "OpenAI" },
|
||||||
{ value: "gemini", label: "Gemini" },
|
{ value: "gemini", label: "Gemini" },
|
||||||
{ value: "antigravity", label: "Antigravity" },
|
{ value: "antigravity", label: "Antigravity" },
|
||||||
|
{ value: "kiro", label: "Kiro" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const editStatusOptions = computed(() => [
|
const editStatusOptions = computed(() => [
|
||||||
@@ -3766,7 +3768,7 @@ watch(
|
|||||||
if (newVal !== "openai") {
|
if (newVal !== "openai") {
|
||||||
resetMessagesDispatchFormState(createForm);
|
resetMessagesDispatchFormState(createForm);
|
||||||
}
|
}
|
||||||
if (!["openai", "antigravity", "anthropic", "gemini"].includes(newVal)) {
|
if (!["openai", "antigravity", "anthropic", "gemini", "kiro"].includes(newVal)) {
|
||||||
createForm.require_oauth_only = false;
|
createForm.require_oauth_only = false;
|
||||||
createForm.require_privacy_set = false;
|
createForm.require_privacy_set = false;
|
||||||
}
|
}
|
||||||
@@ -3782,7 +3784,7 @@ watch(
|
|||||||
if (newVal !== "openai") {
|
if (newVal !== "openai") {
|
||||||
resetMessagesDispatchFormState(editForm);
|
resetMessagesDispatchFormState(editForm);
|
||||||
}
|
}
|
||||||
if (!["openai", "antigravity", "anthropic", "gemini"].includes(newVal)) {
|
if (!["openai", "antigravity", "anthropic", "gemini", "kiro"].includes(newVal)) {
|
||||||
editForm.require_oauth_only = false;
|
editForm.require_oauth_only = false;
|
||||||
editForm.require_privacy_set = false;
|
editForm.require_privacy_set = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user