mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix(providers): 修复 Key 表单状态同步与 streaming 候选状态码处理
前端: - KeyAllowedModelsEditDialog 同时监听 open 和 apiKey 变化,避免切换 key 时状态未刷新 - KeyFormDialog 新增 api_formats 过滤与默认值逻辑,可用格式变化时自动同步表单 - ProviderDetailDrawer 合并 provider 和 endpoint 的 api_formats 传递给 Key 表单, 数据刷新后同步 currentEndpoint 和 editingKey 引用 后端: - mark_candidate_streaming 移除 status_code 参数,streaming 阶段不再提前写入状态码 - 简化 active_requests 中 streaming 请求的完成判断,不再依赖 status_code 条件
This commit is contained in:
@@ -717,49 +717,44 @@ function parseAllowedModels(allowed: AllowedModels): string[] {
|
||||
return [...allowed]
|
||||
}
|
||||
|
||||
// 监听对话框打开
|
||||
watch(() => props.open, async (open) => {
|
||||
if (open && props.apiKey) {
|
||||
loadingCancelled = false
|
||||
async function initializeDialogState(apiKey: EndpointAPIKey) {
|
||||
loadingCancelled = false
|
||||
|
||||
const parsed = parseAllowedModels(props.apiKey.allowed_models ?? null)
|
||||
selectedModels.value = [...parsed]
|
||||
initialSelectedModels.value = [...parsed]
|
||||
const parsed = parseAllowedModels(apiKey.allowed_models ?? null)
|
||||
selectedModels.value = [...parsed]
|
||||
initialSelectedModels.value = [...parsed]
|
||||
|
||||
// 加载锁定的模型
|
||||
const locked = props.apiKey.locked_models ?? []
|
||||
lockedModels.value = [...locked]
|
||||
initialLockedModels.value = [...locked]
|
||||
const locked = apiKey.locked_models ?? []
|
||||
lockedModels.value = [...locked]
|
||||
initialLockedModels.value = [...locked]
|
||||
|
||||
searchQuery.value = ''
|
||||
upstreamModels.value = []
|
||||
upstreamModelsLoaded.value = false
|
||||
allCustomModels.value = []
|
||||
searchQuery.value = ''
|
||||
upstreamModels.value = []
|
||||
upstreamModelsLoaded.value = false
|
||||
allCustomModels.value = []
|
||||
|
||||
// 自动获取模式下展开上游模型,其他收缩;非自动获取模式下全部展开
|
||||
if (props.apiKey.auto_fetch_models) {
|
||||
collapsedGroups.value = new Set(['global', 'custom'])
|
||||
} else {
|
||||
collapsedGroups.value = new Set()
|
||||
}
|
||||
if (apiKey.auto_fetch_models) {
|
||||
collapsedGroups.value = new Set(['global', 'custom'])
|
||||
} else {
|
||||
collapsedGroups.value = new Set()
|
||||
}
|
||||
|
||||
// 加载该 Provider 已关联模型
|
||||
await loadProviderModels()
|
||||
await loadProviderModels()
|
||||
|
||||
// 自动获取模式下,获取上游模型用于显示(但选中状态使用已保存的 allowed_models)
|
||||
if (props.apiKey.auto_fetch_models) {
|
||||
await fetchUpstreamModels()
|
||||
// 注意:不再将所有上游模型自动标记为选中
|
||||
// 因为后端有过滤规则,实际保存的 allowed_models 是过滤后的结果
|
||||
// selectedModels 已在上面从 props.apiKey.allowed_models 初始化
|
||||
}
|
||||
if (apiKey.auto_fetch_models) {
|
||||
await fetchUpstreamModels()
|
||||
}
|
||||
|
||||
// 提取自定义模型(不在提供商模型和上游模型中的)
|
||||
const upstreamModelIdsSet = new Set(upstreamModels.value.map(m => m.id))
|
||||
// 自定义模型是用户手动添加的、不在已知模型列表中的
|
||||
allCustomModels.value = selectedModels.value.filter(m =>
|
||||
!providerModelNamesSet.value.has(m) && !upstreamModelIdsSet.has(m)
|
||||
)
|
||||
const upstreamModelIdsSet = new Set(upstreamModels.value.map(m => m.id))
|
||||
allCustomModels.value = selectedModels.value.filter(m =>
|
||||
!providerModelNamesSet.value.has(m) && !upstreamModelIdsSet.has(m)
|
||||
)
|
||||
}
|
||||
|
||||
// 监听对话框打开 / 当前 key 变化
|
||||
watch([() => props.open, () => props.apiKey], async ([open, apiKey]) => {
|
||||
if (open && apiKey) {
|
||||
await initializeDialogState(apiKey)
|
||||
} else {
|
||||
loadingCancelled = true
|
||||
}
|
||||
|
||||
@@ -377,6 +377,32 @@ function normalizeApiFormat(format: string): string {
|
||||
return String(format || '').trim().toLowerCase()
|
||||
}
|
||||
|
||||
function getAvailableApiFormatSet(): Set<string> {
|
||||
return new Set(props.availableApiFormats.map(normalizeApiFormat))
|
||||
}
|
||||
|
||||
function filterAvailableApiFormats(formats: string[]): string[] {
|
||||
const availableFormatSet = getAvailableApiFormatSet()
|
||||
if (availableFormatSet.size === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return formats.filter(format => availableFormatSet.has(normalizeApiFormat(format)))
|
||||
}
|
||||
|
||||
function getDefaultApiFormats(): string[] {
|
||||
const endpointFormat = props.endpoint?.api_format
|
||||
if (endpointFormat) {
|
||||
const endpointFormats = filterAvailableApiFormats([endpointFormat])
|
||||
if (endpointFormats.length > 0) {
|
||||
return endpointFormats
|
||||
}
|
||||
}
|
||||
|
||||
const firstAvailableFormat = sortApiFormats(props.availableApiFormats)[0]
|
||||
return firstAvailableFormat ? [firstAvailableFormat] : []
|
||||
}
|
||||
|
||||
// 按 provider/auth_type 过滤后的可用 API 格式列表
|
||||
const visibleApiFormats = computed(() => {
|
||||
const sorted = sortApiFormats(props.availableApiFormats)
|
||||
@@ -486,6 +512,29 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
[() => props.availableApiFormats, () => props.open, () => props.editingKey],
|
||||
([, open, editingKey]) => {
|
||||
if (!open) {
|
||||
return
|
||||
}
|
||||
|
||||
const filtered = filterAvailableApiFormats(form.value.api_formats)
|
||||
if (filtered.length !== form.value.api_formats.length) {
|
||||
form.value.api_formats = [...filtered]
|
||||
return
|
||||
}
|
||||
|
||||
if (!editingKey && form.value.api_formats.length === 0) {
|
||||
const defaults = getDefaultApiFormats()
|
||||
if (defaults.length > 0) {
|
||||
form.value.api_formats = defaults
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
// 加载能力列表
|
||||
async function loadCapabilities() {
|
||||
try {
|
||||
@@ -540,7 +589,7 @@ function resetForm() {
|
||||
api_key: '',
|
||||
auth_type: defaultAuthType,
|
||||
auth_config_text: '',
|
||||
api_formats: [], // 默认不选中任何格式
|
||||
api_formats: getDefaultApiFormats(),
|
||||
rate_multipliers: {},
|
||||
internal_priority: 10,
|
||||
rpm_limit: undefined,
|
||||
@@ -573,7 +622,7 @@ function loadKeyData() {
|
||||
auth_type: props.editingKey.auth_type === 'service_account' ? 'service_account' : 'api_key',
|
||||
auth_config_text: '', // auth_config 不返回给前端,编辑时需要重新输入
|
||||
api_formats: props.editingKey.api_formats?.length > 0
|
||||
? [...props.editingKey.api_formats]
|
||||
? filterAvailableApiFormats(props.editingKey.api_formats)
|
||||
: [], // 编辑模式下保持原有选择,不默认全选
|
||||
rate_multipliers: { ...(props.editingKey.rate_multipliers || {}) },
|
||||
internal_priority: props.editingKey.internal_priority ?? 10,
|
||||
|
||||
@@ -936,7 +936,7 @@
|
||||
:editing-key="editingKey"
|
||||
:provider-id="provider ? provider.id : null"
|
||||
:provider-type="provider?.provider_type || null"
|
||||
:available-api-formats="provider?.api_formats || []"
|
||||
:available-api-formats="availableKeyApiFormats"
|
||||
@close="keyFormDialogOpen = false"
|
||||
@saved="handleKeyChanged"
|
||||
/>
|
||||
@@ -1258,6 +1258,65 @@ const allKeys = computed(() => {
|
||||
return result
|
||||
})
|
||||
|
||||
const availableKeyApiFormats = computed(() => {
|
||||
const formatSet = new Set<string>()
|
||||
|
||||
for (const format of provider.value?.api_formats || []) {
|
||||
if (format) {
|
||||
formatSet.add(format)
|
||||
}
|
||||
}
|
||||
|
||||
for (const endpoint of endpoints.value) {
|
||||
if (endpoint.api_format) {
|
||||
formatSet.add(endpoint.api_format)
|
||||
}
|
||||
}
|
||||
|
||||
return sortApiFormats([...formatSet])
|
||||
})
|
||||
|
||||
function syncCurrentSelections(
|
||||
nextEndpoints: ProviderEndpointWithKeys[] = endpoints.value,
|
||||
nextProviderKeys: EndpointAPIKey[] = providerKeys.value
|
||||
) {
|
||||
if (currentEndpoint.value) {
|
||||
currentEndpoint.value = nextEndpoints.find(endpoint => endpoint.id === currentEndpoint.value?.id) ?? null
|
||||
}
|
||||
|
||||
if (!editingKey.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const latestKeys: EndpointAPIKey[] = []
|
||||
const seenKeyIds = new Set<string>()
|
||||
|
||||
for (const key of nextProviderKeys) {
|
||||
if (!seenKeyIds.has(key.id)) {
|
||||
seenKeyIds.add(key.id)
|
||||
latestKeys.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
for (const endpoint of nextEndpoints) {
|
||||
for (const key of endpoint.keys || []) {
|
||||
if (!seenKeyIds.has(key.id)) {
|
||||
seenKeyIds.add(key.id)
|
||||
latestKeys.push(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const latestEditingKey = latestKeys.find(key => key.id === editingKey.value?.id) || null
|
||||
editingKey.value = latestEditingKey
|
||||
|
||||
if (!latestEditingKey) {
|
||||
keyFormDialogOpen.value = false
|
||||
keyPermissionsDialogOpen.value = false
|
||||
oauthKeyEditDialogOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 账号列表智能分页 =====
|
||||
const keysListRef = ref<HTMLElement | null>(null)
|
||||
const {
|
||||
@@ -1558,6 +1617,7 @@ async function handleRefreshOAuth(key: EndpointAPIKey) {
|
||||
const freshKeys = await getProviderKeys(props.providerId).catch(() => null)
|
||||
if (freshKeys) {
|
||||
providerKeys.value = freshKeys
|
||||
syncCurrentSelections(endpoints.value, freshKeys)
|
||||
}
|
||||
}
|
||||
// Antigravity:token 刷新后可能完成了账号激活,触发配额获取
|
||||
@@ -2640,7 +2700,7 @@ async function loadEndpoints() {
|
||||
providerKeys.value = providerKeysResult
|
||||
providerModels.value = modelsResult
|
||||
// 按 API 格式排序
|
||||
endpoints.value = endpointsList.sort((a, b) => {
|
||||
const sortedEndpoints = endpointsList.sort((a, b) => {
|
||||
const aIdx = API_FORMAT_ORDER.indexOf(a.api_format)
|
||||
const bIdx = API_FORMAT_ORDER.indexOf(b.api_format)
|
||||
if (aIdx === -1 && bIdx === -1) return 0
|
||||
@@ -2648,6 +2708,8 @@ async function loadEndpoints() {
|
||||
if (bIdx === -1) return -1
|
||||
return aIdx - bIdx
|
||||
})
|
||||
endpoints.value = sortedEndpoints
|
||||
syncCurrentSelections(sortedEndpoints, providerKeysResult)
|
||||
} catch (err: unknown) {
|
||||
if (requestId !== endpointsLoadRequestId) return
|
||||
showError(parseApiError(err, '加载端点失败'), '错误')
|
||||
|
||||
Reference in New Issue
Block a user