mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30: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:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user