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

@@ -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 剩余有效期:')
})
})

View 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]'
}

View File

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