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:
Entropy.Xu
2026-04-17 12:57:06 +08:00
committed by GitHub
parent 96a25d058b
commit ac1a126756
43 changed files with 2909 additions and 168 deletions

View File

@@ -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,