mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
fix(kiro,pool,model): 对齐 Kiro 管理链路并修复全局模型删除行为 (#305)
* feat(pool): 号池支持跳过额度耗尽账号 - 新增 pool_advanced.skip_exhausted_accounts 开关及高级设置 UI, 默认关闭并兼容旧配置 - 为 Codex/Kiro 增加额度耗尽判定, 接入请求侧候选跳过并新增 account_quota_exhausted skip reason - 号池列表将额度耗尽账号标记为 blocked/额度耗尽, 并补充前后端相关测试 * fix(kiro): 对齐账号管理与 provider-query 的 Rust 行为 - 修复 Kiro 单条导入误走 import-refresh-token 的前端分流, 并为误用路径返回明确错误提示 - 为 Kiro 导入与本地请求链补齐 bearer 兼容, 同步放开账号启停等 Key 更新操作的 auth_type 校验 - 实现 Kiro provider-query 本地模型测试与 failover 执行链, 并修复结果弹窗在无 trace 时无法展示 attempts/响应体的问题 * fix(model): 删除全局模型时级联清理关联提供商模型 - 对齐 Python 版本删除逻辑, GlobalModel 删除前先在事务内清理关联的 Provider Model 记录 - 修复已绑定 Provider 的模型在 Rust SQL 仓库下会被外键约束拦住、无法正常删除的问题 - 增加管理端回归测试, 覆盖绑定 Provider Model 的 GlobalModel 删除场景 * fix(kiro,ci): 恢复 Kiro OAuth 持久化并修复 Rust CI * Fix oauth-managed provider key semantics --------- Co-authored-by: fawney19 <elky0401@gmail.com>
This commit is contained in:
@@ -60,7 +60,7 @@ export async function getModelCapabilities(modelName: string): Promise<ModelCapa
|
||||
* 获取完整的 API Key(用于查看和复制)
|
||||
*/
|
||||
export interface RevealKeyResult {
|
||||
auth_type: 'api_key' | 'service_account' | 'oauth'
|
||||
auth_type: 'api_key' | 'service_account' | 'oauth' | 'bearer'
|
||||
api_key?: string
|
||||
refresh_token?: string
|
||||
auth_config?: string | Record<string, unknown>
|
||||
@@ -165,7 +165,7 @@ export async function updateProviderKey(
|
||||
data: Partial<{
|
||||
api_formats: string[] // 支持的 API 格式列表
|
||||
api_key: string
|
||||
auth_type: 'api_key' | 'service_account' | 'oauth' // 认证类型
|
||||
auth_type: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型
|
||||
auth_config: Record<string, unknown> // 认证配置(Vertex AI Service Account JSON)
|
||||
name: string
|
||||
rate_multipliers: Record<string, number> | null // 按 API 格式的成本倍率
|
||||
|
||||
@@ -104,6 +104,12 @@ export interface PoolKeyDetail {
|
||||
key_name: string
|
||||
is_active: boolean
|
||||
auth_type: string
|
||||
credential_kind?: 'raw_secret' | 'oauth_session' | 'service_account' | string | null
|
||||
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'unknown' | string | null
|
||||
oauth_managed?: boolean
|
||||
can_refresh_oauth?: boolean
|
||||
can_export_oauth?: boolean
|
||||
can_edit_oauth?: boolean
|
||||
oauth_expires_at?: number | null
|
||||
oauth_invalid_at?: number | null // 兼容字段;优先使用 status_snapshot.oauth
|
||||
oauth_invalid_reason?: string | null // 兼容字段;优先使用 status_snapshot.oauth
|
||||
@@ -200,6 +206,12 @@ export interface PoolKeySelectionItem {
|
||||
key_id: string
|
||||
key_name: string
|
||||
auth_type: string
|
||||
credential_kind?: 'raw_secret' | 'oauth_session' | 'service_account' | string | null
|
||||
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'unknown' | string | null
|
||||
oauth_managed?: boolean
|
||||
can_refresh_oauth?: boolean
|
||||
can_export_oauth?: boolean
|
||||
can_edit_oauth?: boolean
|
||||
}
|
||||
|
||||
export interface PoolKeySelectionResponse {
|
||||
|
||||
@@ -229,7 +229,13 @@ export interface EndpointAPIKey {
|
||||
api_formats: string[] // 支持的 endpoint signature 列表(如 "openai:chat")
|
||||
api_key_masked: string
|
||||
api_key_plain?: string | null
|
||||
auth_type: 'api_key' | 'service_account' | 'oauth' // 认证类型(必返回)
|
||||
auth_type: 'api_key' | 'service_account' | 'oauth' | 'bearer' // 认证类型(必返回)
|
||||
credential_kind?: 'raw_secret' | 'oauth_session' | 'service_account' | string | null
|
||||
runtime_auth_kind?: 'api_key' | 'bearer' | 'service_account' | 'unknown' | string | null
|
||||
oauth_managed?: boolean
|
||||
can_refresh_oauth?: boolean
|
||||
can_export_oauth?: boolean
|
||||
can_edit_oauth?: boolean
|
||||
name: string // 密钥名称(必填,用于识别)
|
||||
rate_multipliers?: Record<string, number> | null // 按 endpoint signature 的成本倍率
|
||||
internal_priority: number // Key 内部优先级
|
||||
@@ -492,6 +498,7 @@ export interface PoolAdvancedConfig {
|
||||
global_priority?: number | null
|
||||
sticky_session_ttl_seconds?: number | null
|
||||
load_threshold_percent?: number | null
|
||||
skip_exhausted_accounts?: boolean | null
|
||||
// 旧字段(兼容读取)
|
||||
lru_enabled?: boolean
|
||||
scheduling_mode?: 'lru' | 'multi_score' | null
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
<Badge
|
||||
variant="outline"
|
||||
class="text-[10px] px-1 py-0 h-4 shrink-0"
|
||||
>{{ normalizeAuthTypeLabel(key.auth_type) }}</Badge>
|
||||
>{{ normalizeAuthTypeLabel(key) }}</Badge>
|
||||
<Badge
|
||||
v-if="getStatusBadgeLabel(key)"
|
||||
variant="destructive"
|
||||
@@ -289,6 +289,11 @@ import { exportKey, refreshProviderQuota } from '@/api/endpoints/keys'
|
||||
import { refreshProviderOAuth } from '@/api/endpoints/provider_oauth'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
import { getOAuthOrgBadge } from '@/utils/oauthIdentity'
|
||||
import {
|
||||
canExportOAuthCredential,
|
||||
canRefreshOAuthCredential,
|
||||
getProviderAuthLabel,
|
||||
} from '@/utils/providerKeyAuth'
|
||||
import {
|
||||
getAccountStatusDisplay,
|
||||
getAccountStatusTitle,
|
||||
@@ -416,10 +421,6 @@ const isCurrentPageFullySelected = computed(() => {
|
||||
const canClearSelection = computed(() => selectAllFiltered.value || selectedKeyIds.value.length > 0)
|
||||
const activeQuickSelectorSet = computed(() => new Set(activeQuickSelectors.value))
|
||||
|
||||
function normalizeText(value: unknown): string {
|
||||
return String(value || '').trim().toLowerCase()
|
||||
}
|
||||
|
||||
function sanitizeFileNamePart(value: unknown, fallback: string): string {
|
||||
const sanitized = String(value || '')
|
||||
.trim()
|
||||
@@ -452,11 +453,8 @@ function downloadJsonFile(data: unknown, filename: string): void {
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function normalizeAuthTypeLabel(authType: string): string {
|
||||
const text = normalizeText(authType)
|
||||
if (text === 'oauth') return 'OAuth'
|
||||
if (text === 'service_account') return 'Service'
|
||||
return 'API Key'
|
||||
function normalizeAuthTypeLabel(key: PoolKeyDetail | PoolKeySelectionItem): string {
|
||||
return getProviderAuthLabel(key)
|
||||
}
|
||||
|
||||
function getStatusBadgeLabel(key: PoolKeyDetail): string | null {
|
||||
@@ -741,6 +739,12 @@ async function resolveSelectedItems(): Promise<PoolKeySelectionItem[]> {
|
||||
key_id: keyId,
|
||||
key_name: key?.key_name || '',
|
||||
auth_type: key?.auth_type || 'api_key',
|
||||
credential_kind: key?.credential_kind,
|
||||
runtime_auth_kind: key?.runtime_auth_kind,
|
||||
oauth_managed: key?.oauth_managed,
|
||||
can_refresh_oauth: key?.can_refresh_oauth,
|
||||
can_export_oauth: key?.can_export_oauth,
|
||||
can_edit_oauth: key?.can_edit_oauth,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -810,7 +814,7 @@ async function executeAction(actionOverride?: BatchActionValue): Promise<void> {
|
||||
progressDone.value = Math.min(i + BATCH_SIZE, targetIds.length)
|
||||
}
|
||||
} else if (selectedAction.value === 'export') {
|
||||
const exportableKeys = selectedKeys.filter((key) => normalizeText(key.auth_type) === 'oauth')
|
||||
const exportableKeys = selectedKeys.filter((key) => canExportOAuthCredential(key))
|
||||
const exportedEntries: Array<Record<string, unknown> | null> = Array.from({ length: exportableKeys.length }, () => null)
|
||||
|
||||
skippedCount += selectedKeys.length - exportableKeys.length
|
||||
@@ -920,7 +924,7 @@ async function executeAction(actionOverride?: BatchActionValue): Promise<void> {
|
||||
const CONCURRENCY = props.batchConcurrency || 8
|
||||
const tasks: Array<() => Promise<'success' | 'skip'>> = []
|
||||
for (const key of selectedKeys) {
|
||||
if (selectedAction.value === 'refresh_oauth' && normalizeText(key.auth_type) !== 'oauth') {
|
||||
if (selectedAction.value === 'refresh_oauth' && !canRefreshOAuthCredential(key)) {
|
||||
skippedCount += 1
|
||||
progressDone.value += 1
|
||||
continue
|
||||
|
||||
@@ -467,6 +467,7 @@ const form = ref({
|
||||
probing_enabled: false,
|
||||
probing_interval_minutes: null as number | null | undefined,
|
||||
auto_remove_banned_keys: false,
|
||||
skip_exhausted_accounts: false,
|
||||
})
|
||||
|
||||
interface ClaudeFormState {
|
||||
@@ -503,6 +504,8 @@ function getHealthToggleValue(key: PoolHealthToggleKey): boolean {
|
||||
return form.value.probing_enabled
|
||||
case 'auto_remove_banned_keys':
|
||||
return form.value.auto_remove_banned_keys
|
||||
case 'skip_exhausted_accounts':
|
||||
return form.value.skip_exhausted_accounts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,6 +519,9 @@ function updateHealthToggleValue(key: PoolHealthToggleKey, value: boolean): void
|
||||
return
|
||||
case 'auto_remove_banned_keys':
|
||||
form.value.auto_remove_banned_keys = value
|
||||
return
|
||||
case 'skip_exhausted_accounts':
|
||||
form.value.skip_exhausted_accounts = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,6 +542,7 @@ watch(() => props.modelValue, (open) => {
|
||||
probing_enabled: cfg?.probing_enabled ?? false,
|
||||
probing_interval_minutes: cfg?.probing_interval_minutes ?? null,
|
||||
auto_remove_banned_keys: cfg?.auto_remove_banned_keys ?? false,
|
||||
skip_exhausted_accounts: cfg?.skip_exhausted_accounts ?? false,
|
||||
}
|
||||
|
||||
const cc = props.currentClaudeConfig
|
||||
@@ -570,6 +577,7 @@ async function handleSave() {
|
||||
? (form.value.probing_interval_minutes ?? undefined)
|
||||
: undefined,
|
||||
auto_remove_banned_keys: form.value.auto_remove_banned_keys,
|
||||
skip_exhausted_accounts: form.value.skip_exhausted_accounts,
|
||||
}
|
||||
|
||||
const payload: Parameters<typeof updateProvider>[1] = {
|
||||
|
||||
@@ -13,6 +13,7 @@ describe('poolAdvancedDialog', () => {
|
||||
'health_policy_enabled',
|
||||
'probing_enabled',
|
||||
'auto_remove_banned_keys',
|
||||
'skip_exhausted_accounts',
|
||||
])
|
||||
})
|
||||
|
||||
@@ -33,6 +34,11 @@ describe('poolAdvancedDialog', () => {
|
||||
label: '异常自动清除',
|
||||
description: '仅在检测到不可恢复的账号异常时自动从号池移除,不处理纯 Token 失效。',
|
||||
},
|
||||
{
|
||||
key: 'skip_exhausted_accounts',
|
||||
label: '跳过额度耗尽账号',
|
||||
description: '当 Codex / Kiro 账号额度已耗尽时,直接标记为不可调度并在请求侧跳过。',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ export type PoolHealthToggleKey =
|
||||
| 'health_policy_enabled'
|
||||
| 'probing_enabled'
|
||||
| 'auto_remove_banned_keys'
|
||||
| 'skip_exhausted_accounts'
|
||||
|
||||
export interface PoolHealthToggleCard {
|
||||
key: PoolHealthToggleKey
|
||||
@@ -40,6 +41,11 @@ export function buildPoolHealthToggleCards(): PoolHealthToggleCard[] {
|
||||
label: '异常自动清除',
|
||||
description: '仅在检测到不可恢复的账号异常时自动从号池移除,不处理纯 Token 失效。',
|
||||
},
|
||||
{
|
||||
key: 'skip_exhausted_accounts',
|
||||
label: '跳过额度耗尽账号',
|
||||
description: '当 Codex / Kiro 账号额度已耗尽时,直接标记为不可调度并在请求侧跳过。',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -939,8 +939,8 @@ async function handleImport() {
|
||||
let keepImporting = false
|
||||
try {
|
||||
const proxyNodeId = selectedProxyNodeId.value || undefined
|
||||
// 检测是否为批量导入
|
||||
if (isBatchImport(inputText)) {
|
||||
// Kiro 的单条 JSON 凭据也必须走 batch-import 路径,后端需要完整 auth_config。
|
||||
if (isKiroProvider.value || isBatchImport(inputText)) {
|
||||
const task = await startBatchImportOAuthTask(props.providerId, inputText, proxyNodeId)
|
||||
importTask.value = {
|
||||
task_id: task.task_id,
|
||||
|
||||
@@ -325,10 +325,10 @@
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-[11px] font-mono text-muted-foreground">
|
||||
{{ key.auth_type === 'oauth' ? '[Refresh Token]' : (key.auth_type === 'service_account' ? '[Service Account]' : key.api_key_masked) }}
|
||||
{{ isOAuthManagedCredential(key) ? '[Refresh Token]' : (isServiceAccountCredential(key) ? '[Service Account]' : key.api_key_masked) }}
|
||||
</span>
|
||||
<Button
|
||||
v-if="key.auth_type === 'oauth'"
|
||||
v-if="canExportOAuthCredential(key)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
@@ -403,7 +403,7 @@
|
||||
</template>
|
||||
<!-- Antigravity 账号未激活提示 -->
|
||||
<span
|
||||
v-if="provider.provider_type === 'antigravity' && key.is_active && key.auth_type === 'oauth' && (!key.upstream_metadata || !hasAntigravityQuotaData(key.upstream_metadata))"
|
||||
v-if="provider.provider_type === 'antigravity' && key.is_active && isOAuthManagedCredential(key) && (!key.upstream_metadata || !hasAntigravityQuotaData(key.upstream_metadata))"
|
||||
class="text-[10px] text-orange-500 dark:text-orange-400"
|
||||
title="该账号尚未完成 Gemini Code Assist 激活,无法获取配额和使用模型"
|
||||
>
|
||||
@@ -1157,6 +1157,12 @@ import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||
import { isOAuthAccountProviderType, isKeyManagedProviderType } from '../utils/providerTypeUtils'
|
||||
import { getOAuthOrgBadge } from '@/utils/oauthIdentity'
|
||||
import { getOAuthRefreshFeedback } from '@/utils/oauthRefreshFeedback'
|
||||
import {
|
||||
canEditOAuthCredential,
|
||||
canExportOAuthCredential,
|
||||
isOAuthManagedCredential,
|
||||
isServiceAccountCredential,
|
||||
} from '@/utils/providerKeyAuth'
|
||||
import {
|
||||
getAccountStatusDisplay,
|
||||
getAccountStatusTitle,
|
||||
@@ -1579,7 +1585,7 @@ function handleEditKey(endpoint: ProviderEndpoint | undefined, key: EndpointAPIK
|
||||
currentEndpoint.value = endpoint || null
|
||||
editingKey.value = key
|
||||
// OAuth 密钥使用专门的编辑对话框
|
||||
if (key.auth_type === 'oauth') {
|
||||
if (canEditOAuthCredential(key)) {
|
||||
oauthKeyEditDialogOpen.value = true
|
||||
} else {
|
||||
keyFormDialogOpen.value = true
|
||||
|
||||
@@ -715,7 +715,7 @@ const requestBodyDraft = computed(() => props.requestBodyDraft ?? '')
|
||||
const traceCandidates = computed(() => props.trace?.candidates ?? [])
|
||||
const showSetup = computed(() => props.open && !props.testing && !props.result)
|
||||
const showResult = computed(() => !!props.result)
|
||||
const showTraceTimeline = computed(() => Boolean(props.requestId))
|
||||
const showTraceTimeline = computed(() => Boolean(props.requestId) && traceCandidates.value.length > 0)
|
||||
const isDark = computed(() => typeof document !== 'undefined' && document.documentElement.classList.contains('dark'))
|
||||
const { copyToClipboard } = useClipboard()
|
||||
|
||||
|
||||
@@ -104,4 +104,24 @@ describe('providerKeyStatus', () => {
|
||||
},
|
||||
}, 0)).toContain('Token 剩余有效期:')
|
||||
})
|
||||
|
||||
it('treats oauth_managed bearer credentials as oauth for legacy countdown fallback', () => {
|
||||
const future = Math.floor(Date.now() / 1000) + 2 * 24 * 3600
|
||||
const status = getOAuthStatusDisplay(
|
||||
{
|
||||
auth_type: 'bearer',
|
||||
oauth_managed: true,
|
||||
oauth_expires_at: future,
|
||||
},
|
||||
0,
|
||||
)
|
||||
|
||||
expect(status).not.toBeNull()
|
||||
expect(status?.isExpired).toBe(false)
|
||||
expect(getOAuthStatusTitle({
|
||||
auth_type: 'bearer',
|
||||
oauth_managed: true,
|
||||
oauth_expires_at: future,
|
||||
}, 0)).toContain('Token 剩余有效期:')
|
||||
})
|
||||
})
|
||||
|
||||
100
frontend/src/utils/providerKeyAuth.ts
Normal file
100
frontend/src/utils/providerKeyAuth.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
export interface ProviderKeyAuthCarrier {
|
||||
auth_type?: string | null
|
||||
credential_kind?: string | null
|
||||
runtime_auth_kind?: string | null
|
||||
oauth_managed?: boolean | null
|
||||
can_refresh_oauth?: boolean | null
|
||||
can_export_oauth?: boolean | null
|
||||
can_edit_oauth?: boolean | null
|
||||
}
|
||||
|
||||
function normalizeText(value: unknown): string | null {
|
||||
if (typeof value !== 'string') return null
|
||||
const text = value.trim().toLowerCase()
|
||||
return text || null
|
||||
}
|
||||
|
||||
export function getProviderCredentialKind(
|
||||
input: ProviderKeyAuthCarrier,
|
||||
): 'raw_secret' | 'oauth_session' | 'service_account' {
|
||||
const credentialKind = normalizeText(input.credential_kind)
|
||||
if (
|
||||
credentialKind === 'raw_secret'
|
||||
|| credentialKind === 'oauth_session'
|
||||
|| credentialKind === 'service_account'
|
||||
) {
|
||||
return credentialKind
|
||||
}
|
||||
|
||||
if (typeof input.oauth_managed === 'boolean') {
|
||||
return input.oauth_managed ? 'oauth_session' : 'raw_secret'
|
||||
}
|
||||
|
||||
const authType = normalizeText(input.auth_type)
|
||||
if (authType === 'oauth') return 'oauth_session'
|
||||
if (authType === 'service_account' || authType === 'vertex_ai') return 'service_account'
|
||||
return 'raw_secret'
|
||||
}
|
||||
|
||||
export function getProviderRuntimeAuthKind(
|
||||
input: ProviderKeyAuthCarrier,
|
||||
): 'api_key' | 'bearer' | 'service_account' | 'unknown' {
|
||||
const runtimeAuthKind = normalizeText(input.runtime_auth_kind)
|
||||
if (
|
||||
runtimeAuthKind === 'api_key'
|
||||
|| runtimeAuthKind === 'bearer'
|
||||
|| runtimeAuthKind === 'service_account'
|
||||
) {
|
||||
return runtimeAuthKind
|
||||
}
|
||||
|
||||
const authType = normalizeText(input.auth_type)
|
||||
if (authType === 'service_account' || authType === 'vertex_ai') return 'service_account'
|
||||
if (authType === 'bearer') return 'bearer'
|
||||
if (authType === 'api_key') return 'api_key'
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
export function isOAuthManagedCredential(input: ProviderKeyAuthCarrier): boolean {
|
||||
if (typeof input.oauth_managed === 'boolean') {
|
||||
return input.oauth_managed
|
||||
}
|
||||
return getProviderCredentialKind(input) === 'oauth_session'
|
||||
}
|
||||
|
||||
export function isServiceAccountCredential(input: ProviderKeyAuthCarrier): boolean {
|
||||
return getProviderCredentialKind(input) === 'service_account'
|
||||
}
|
||||
|
||||
export function canRefreshOAuthCredential(input: ProviderKeyAuthCarrier): boolean {
|
||||
if (typeof input.can_refresh_oauth === 'boolean') {
|
||||
return input.can_refresh_oauth
|
||||
}
|
||||
return isOAuthManagedCredential(input)
|
||||
}
|
||||
|
||||
export function canExportOAuthCredential(input: ProviderKeyAuthCarrier): boolean {
|
||||
if (typeof input.can_export_oauth === 'boolean') {
|
||||
return input.can_export_oauth
|
||||
}
|
||||
return isOAuthManagedCredential(input)
|
||||
}
|
||||
|
||||
export function canEditOAuthCredential(input: ProviderKeyAuthCarrier): boolean {
|
||||
if (typeof input.can_edit_oauth === 'boolean') {
|
||||
return input.can_edit_oauth
|
||||
}
|
||||
return isOAuthManagedCredential(input)
|
||||
}
|
||||
|
||||
export function getProviderAuthLabel(input: ProviderKeyAuthCarrier): string {
|
||||
if (isOAuthManagedCredential(input)) return 'OAuth'
|
||||
if (isServiceAccountCredential(input)) return '服务账号'
|
||||
return getProviderRuntimeAuthKind(input) === 'bearer' ? 'Bearer' : 'API Key'
|
||||
}
|
||||
|
||||
export function getProviderMaskedSecretLabel(input: ProviderKeyAuthCarrier): string {
|
||||
if (isOAuthManagedCredential(input)) return '[OAuth Token]'
|
||||
if (isServiceAccountCredential(input)) return '[Service Account]'
|
||||
return getProviderRuntimeAuthKind(input) === 'bearer' ? '[Bearer Token]' : '[Key]'
|
||||
}
|
||||
@@ -6,9 +6,12 @@ import {
|
||||
isAccountLevelBlockReason,
|
||||
isRefreshFailedReason,
|
||||
} from './accountBlock'
|
||||
import {
|
||||
isOAuthManagedCredential,
|
||||
type ProviderKeyAuthCarrier,
|
||||
} from './providerKeyAuth'
|
||||
|
||||
export interface ProviderKeyStatusCarrier {
|
||||
auth_type?: string | null
|
||||
export interface ProviderKeyStatusCarrier extends ProviderKeyAuthCarrier {
|
||||
oauth_expires_at?: number | null
|
||||
oauth_invalid_at?: number | null // compatibility only
|
||||
oauth_invalid_reason?: string | null // compatibility only
|
||||
@@ -107,7 +110,7 @@ function getLegacyOAuthState(
|
||||
input: ProviderKeyStatusCarrier,
|
||||
tick: number,
|
||||
): OAuthStatusInfo | null {
|
||||
if (normalizeText(input.auth_type) !== 'oauth') return null
|
||||
if (!isOAuthManagedCredential(input)) return null
|
||||
if (!input.oauth_expires_at && !input.oauth_invalid_at && !input.oauth_invalid_reason) return null
|
||||
|
||||
const rawReason = normalizeText(input.oauth_invalid_reason)
|
||||
|
||||
@@ -441,7 +441,7 @@
|
||||
P{{ key.internal_priority ?? 50 }}
|
||||
</button>
|
||||
<Button
|
||||
v-if="key.auth_type === 'oauth'"
|
||||
v-if="canExportOAuthCredential(key)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-4 w-4 shrink-0"
|
||||
@@ -461,9 +461,9 @@
|
||||
<Copy class="w-2.5 h-2.5" />
|
||||
</Button>
|
||||
<span class="font-mono">
|
||||
{{ key.auth_type === 'oauth' ? '[OAuth Token]' : (key.auth_type === 'service_account' ? '[Service Account]' : '[Key]') }}
|
||||
{{ getProviderMaskedSecretLabel(key) }}
|
||||
</span>
|
||||
<template v-if="key.auth_type === 'oauth'">
|
||||
<template v-if="canRefreshOAuthCredential(key)">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -853,7 +853,7 @@
|
||||
class="min-w-0 flex-1 flex justify-center"
|
||||
>
|
||||
<Button
|
||||
v-if="actionId === 'copy_or_download' && key.auth_type === 'oauth'"
|
||||
v-if="actionId === 'copy_or_download' && canExportOAuthCredential(key)"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 shrink-0"
|
||||
@@ -1207,6 +1207,15 @@ import {
|
||||
} from '@/features/pool/utils/poolManagementState'
|
||||
import { getOAuthOrgBadge } from '@/utils/oauthIdentity'
|
||||
import { getOAuthRefreshFeedback } from '@/utils/oauthRefreshFeedback'
|
||||
import {
|
||||
canEditOAuthCredential,
|
||||
canExportOAuthCredential,
|
||||
canRefreshOAuthCredential,
|
||||
getProviderAuthLabel,
|
||||
getProviderMaskedSecretLabel,
|
||||
isOAuthManagedCredential,
|
||||
isServiceAccountCredential,
|
||||
} from '@/utils/providerKeyAuth'
|
||||
import {
|
||||
getAccountStatusDisplay,
|
||||
getAccountStatusTitle,
|
||||
@@ -1836,8 +1845,9 @@ watch(searchQuery, () => {
|
||||
}, 300)
|
||||
})
|
||||
|
||||
function normalizeAuthTypeForEdit(authType: string): EndpointAPIKey['auth_type'] {
|
||||
if (authType === 'oauth' || authType === 'service_account') return authType
|
||||
function normalizeAuthTypeForEdit(key: PoolKeyDetail): EndpointAPIKey['auth_type'] {
|
||||
if (isOAuthManagedCredential(key)) return 'oauth'
|
||||
if (isServiceAccountCredential(key)) return 'service_account'
|
||||
return 'api_key'
|
||||
}
|
||||
|
||||
@@ -1847,12 +1857,14 @@ function toEndpointApiKey(key: PoolKeyDetail): EndpointAPIKey {
|
||||
id: key.key_id,
|
||||
provider_id: selectedProviderId.value || '',
|
||||
api_formats: key.api_formats || [],
|
||||
api_key_masked: key.auth_type === 'oauth'
|
||||
? '[OAuth Token]'
|
||||
: key.auth_type === 'service_account'
|
||||
? '[Service Account]'
|
||||
: '[Key]',
|
||||
auth_type: normalizeAuthTypeForEdit(key.auth_type),
|
||||
api_key_masked: getProviderMaskedSecretLabel(key),
|
||||
auth_type: normalizeAuthTypeForEdit(key),
|
||||
credential_kind: key.credential_kind ?? null,
|
||||
runtime_auth_kind: key.runtime_auth_kind ?? null,
|
||||
oauth_managed: key.oauth_managed ?? undefined,
|
||||
can_refresh_oauth: key.can_refresh_oauth ?? undefined,
|
||||
can_export_oauth: key.can_export_oauth ?? undefined,
|
||||
can_edit_oauth: key.can_edit_oauth ?? undefined,
|
||||
name: key.key_name || '未命名',
|
||||
rate_multipliers: key.rate_multipliers ?? null,
|
||||
internal_priority: key.internal_priority ?? 50,
|
||||
@@ -1959,7 +1971,7 @@ async function finishEditInternalPriority(
|
||||
|
||||
function handleEditKey(key: PoolKeyDetail) {
|
||||
editingKeyDetail.value = key
|
||||
if (key.auth_type === 'oauth') {
|
||||
if (canEditOAuthCredential(key)) {
|
||||
oauthKeyEditDialogOpen.value = true
|
||||
} else {
|
||||
keyFormDialogOpen.value = true
|
||||
@@ -2457,10 +2469,8 @@ function getRowClass(key: PoolKeyDetail): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
function getAuthTypeChipLabel(authType: string): string {
|
||||
if (authType === 'oauth') return 'OAuth'
|
||||
if (authType === 'service_account') return '服务账号'
|
||||
return 'API Key'
|
||||
function getAuthTypeChipLabel(key: PoolKeyDetail): string {
|
||||
return getProviderAuthLabel(key)
|
||||
}
|
||||
|
||||
function getMobileOAuthTone(key: PoolKeyDetail): PoolMobileTagTone | null {
|
||||
@@ -2482,7 +2492,7 @@ function getMobileTagItems(key: PoolKeyDetail): PoolMobileTagItem[] {
|
||||
oauthStatusLabel: oauthState?.text ?? null,
|
||||
oauthStatusTone: getMobileOAuthTone(key),
|
||||
priorityLabel: `P${key.internal_priority ?? 50}`,
|
||||
authLabel: getAuthTypeChipLabel(key.auth_type),
|
||||
authLabel: getAuthTypeChipLabel(key),
|
||||
planLabel: key.oauth_plan_type ? formatOAuthPlanType(key.oauth_plan_type) : null,
|
||||
orgLabel: orgBadge?.label ?? null,
|
||||
proxyLabel: key.proxy?.node_id ? '独立代理' : null,
|
||||
@@ -2492,7 +2502,7 @@ function getMobileTagItems(key: PoolKeyDetail): PoolMobileTagItem[] {
|
||||
function getMobileActionIds(key: PoolKeyDetail): PoolMobileActionId[] {
|
||||
return splitPoolMobileActions({
|
||||
canDownloadOrCopy: true,
|
||||
canRefreshToken: key.auth_type === 'oauth',
|
||||
canRefreshToken: canRefreshOAuthCredential(key),
|
||||
canClearCooldown: Boolean(key.cooldown_reason),
|
||||
canRecoverHealth: key.circuit_breaker_open || (key.health_score ?? 1) < 0.5,
|
||||
hasProxy: true,
|
||||
|
||||
Reference in New Issue
Block a user