refactor: 拆分调度器为独立子模块,增强 Sub2API 多认证方式支持

调度器重构:
- 将 CacheAwareScheduler 拆分为 candidate_builder、candidate_sorter、
  concurrency_checker、restriction_checker、scheduling_config、schemas、utils 等独立模块
- 删除旧的 _candidate_builder.py 和 _candidate_sorter.py
- 新增调度并发拒绝 Prometheus 指标

Sub2API 架构增强:
- 支持账号密码登录和 Refresh Token 两种认证方式
- 实现 JWT 自动刷新和 Token Rotation 持久化
- 前端 ProviderAuthDialog 支持多认证方式切换和 credentials_schema 动态渲染
- 验证接口返回 updated_credentials 以同步轮换后的 token

其他改进:
- 并发管理器增加 RPM guard 和动态预留逻辑
- RequestCandidate 支持 mark_skipped 附加 extra_data
- TaskService 增强健壮性
- 补充相关单元测试和契约测试
This commit is contained in:
fawney19
2026-02-15 16:32:23 +08:00
parent 8a670f5524
commit 1c16b77a92
27 changed files with 2159 additions and 645 deletions

View File

@@ -25,26 +25,56 @@
v-else
class="space-y-4"
>
<!-- 认证模板选择 -->
<div class="space-y-2">
<Label>认证模板</Label>
<Select
v-model="selectedArchitectureId"
@update:model-value="handleArchitectureChange"
<!-- 认证模板 + 认证方式并排 -->
<div class="flex gap-3">
<div
class="space-y-2"
:style="{ flex: currentAuthTypes.length > 1 ? 1 : 'auto', width: currentAuthTypes.length > 1 ? undefined : '100%' }"
>
<SelectTrigger>
<SelectValue placeholder="选择认证模板" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="arch in architectures"
:key="arch.architecture_id"
:value="arch.architecture_id"
>
{{ arch.display_name }}
</SelectItem>
</SelectContent>
</Select>
<Label>认证模板</Label>
<Select
v-model="selectedArchitectureId"
@update:model-value="handleArchitectureChange"
>
<SelectTrigger>
<SelectValue placeholder="选择认证模板" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="arch in architectures"
:key="arch.architecture_id"
:value="arch.architecture_id"
>
{{ arch.display_name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<div
v-if="currentAuthTypes.length > 1"
class="space-y-2"
style="flex: 1"
>
<Label>认证方式</Label>
<Select
v-model="selectedAuthType"
@update:model-value="handleAuthTypeChange"
>
<SelectTrigger>
<SelectValue placeholder="选择认证方式" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="authType in currentAuthTypes"
:key="authType.type"
:value="authType.type"
>
{{ authType.display_name }}
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<!-- 动态表单字段 -->
@@ -253,7 +283,7 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, nextTick } from 'vue'
import { KeyRound } from 'lucide-vue-next'
import {
Dialog,
@@ -342,11 +372,28 @@ const architecturesLoaded = ref(false)
// 当前选择
const selectedArchitectureId = ref('new_api')
const selectedAuthType = ref('')
const formData = ref<Record<string, any>>({})
// 当前架构的 schema
// 当前架构支持的认证方式
const currentAuthTypes = computed(() => {
const arch = architectures.value.find((a) => a.architecture_id === selectedArchitectureId.value)
return arch?.supported_auth_types ?? []
})
// 当前架构的 schema优先从选中的 auth_type 获取)
const currentSchema = computed<CredentialsSchema | null>(() => {
const arch = architectures.value.find((a) => a.architecture_id === selectedArchitectureId.value)
if (!arch) return null
// 如果有选中的 auth_type 且架构有多个 connector从对应的 auth_type 获取 schema
if (selectedAuthType.value && arch.supported_auth_types.length > 1) {
const authType = arch.supported_auth_types.find((t) => t.type === selectedAuthType.value)
if (authType?.credentials_schema) {
return authType.credentials_schema as CredentialsSchema
}
}
return (arch?.credentials_schema as CredentialsSchema) ?? null
})
@@ -385,6 +432,15 @@ const fieldGroups = computed<AuthTemplateFieldGroup[]>(() => {
// Methods
function handleArchitectureChange() {
// 切换架构时,默认选中第一个认证方式
const authTypes = currentAuthTypes.value
selectedAuthType.value = authTypes.length > 0 ? authTypes[0].type : ''
resetFormData()
verifyStatus.value = null
formChanged.value = true
}
function handleAuthTypeChange() {
resetFormData()
verifyStatus.value = null
formChanged.value = true
@@ -492,6 +548,19 @@ async function handleVerify() {
} else {
verifyStatus.value = 'success'
formChanged.value = false
// Token Rotation: 验证过程中 refresh_token 可能已被轮换,用新值更新表单
// 必须在 formChanged=false 之后执行,避免 watch 将 formChanged 重新设为 true
if (result.updated_credentials) {
for (const [key, value] of Object.entries(result.updated_credentials)) {
formData.value[key] = value
}
// 凭据更新不算用户修改,保持可保存状态
nextTick(() => {
formChanged.value = false
})
}
const displayName = result.data?.display_name || result.data?.username
const extra = result.data?.extra
let balanceText = `余额: ${formatQuota(quota)}`
@@ -502,6 +571,14 @@ async function handleVerify() {
}
} else {
verifyStatus.value = 'error'
// Token Rotation: 即使验证失败refresh_token 可能已被轮换(旧 token 已失效)
if (result.updated_credentials) {
for (const [key, value] of Object.entries(result.updated_credentials)) {
formData.value[key] = value
}
}
showError(result.message || '验证失败')
}
} catch (error: any) {
@@ -582,6 +659,7 @@ async function handleClear() {
verifyStatus.value = null
formChanged.value = false
selectedArchitectureId.value = 'new_api'
selectedAuthType.value = ''
resetFormData()
emit('saved')
emit('update:open', false)
@@ -605,6 +683,15 @@ function loadFromConfig(config: any) {
const archExists = architectures.value.some((a) => a.architecture_id === architectureId)
selectedArchitectureId.value = archExists ? architectureId : 'new_api'
// 从已保存的 connector auth_type 恢复认证方式选择
const savedAuthType = config.connector?.auth_type
const authTypes = currentAuthTypes.value
if (savedAuthType && authTypes.some((t) => t.type === savedAuthType)) {
selectedAuthType.value = savedAuthType
} else {
selectedAuthType.value = authTypes.length > 0 ? authTypes[0].type : ''
}
const schema = currentSchema.value
if (schema) {
const parsedData = parseConfigFromSchema(schema, config)
@@ -666,12 +753,14 @@ watch(
hasExistingConfig.value = false
sensitivePlaceholders.value = {}
selectedArchitectureId.value = 'new_api'
selectedAuthType.value = ''
resetFormData()
}
} catch {
hasExistingConfig.value = false
sensitivePlaceholders.value = {}
selectedArchitectureId.value = 'new_api'
selectedAuthType.value = ''
resetFormData()
} finally {
isLoadingConfig.value = false
@@ -680,6 +769,7 @@ watch(
hasExistingConfig.value = false
sensitivePlaceholders.value = {}
selectedArchitectureId.value = 'new_api'
selectedAuthType.value = ''
resetFormData()
}
}