refactor(ui): extract mixed channel warning handler

This commit is contained in:
liuxiongfeng
2026-02-13 00:22:14 +08:00
parent 5715587baf
commit 6218eefd61
3 changed files with 152 additions and 104 deletions

View File

@@ -1964,6 +1964,7 @@ import {
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth' import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
import { useGeminiOAuth } from '@/composables/useGeminiOAuth' import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth' import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
import { useMixedChannelWarning } from '@/composables/useMixedChannelWarning'
import type { Proxy, AdminGroup, AccountPlatform, AccountType } from '@/types' import type { Proxy, AdminGroup, AccountPlatform, AccountType } from '@/types'
import BaseDialog from '@/components/common/BaseDialog.vue' import BaseDialog from '@/components/common/BaseDialog.vue'
import ConfirmDialog from '@/components/common/ConfirmDialog.vue' import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
@@ -2102,10 +2103,9 @@ const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one') const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one')
const geminiAIStudioOAuthEnabled = ref(false) const geminiAIStudioOAuthEnabled = ref(false)
// Mixed channel warning dialog state const mixedChannelWarning = useMixedChannelWarning()
const showMixedChannelWarning = ref(false) const showMixedChannelWarning = mixedChannelWarning.show
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(null) const mixedChannelWarningDetails = mixedChannelWarning.details
const pendingCreatePayload = ref<any>(null)
const showAdvancedOAuth = ref(false) const showAdvancedOAuth = ref(false)
const showGeminiHelpDialog = ref(false) const showGeminiHelpDialog = ref(false)
@@ -2583,6 +2583,7 @@ const resetForm = () => {
geminiOAuth.resetState() geminiOAuth.resetState()
antigravityOAuth.resetState() antigravityOAuth.resetState()
oauthFlowRef.value?.reset() oauthFlowRef.value?.reset()
mixedChannelWarning.cancel()
} }
const handleClose = () => { const handleClose = () => {
@@ -2593,24 +2594,16 @@ const handleClose = () => {
const doCreateAccount = async (payload: any) => { const doCreateAccount = async (payload: any) => {
submitting.value = true submitting.value = true
try { try {
await adminAPI.accounts.create(payload) await mixedChannelWarning.tryRequest(payload, (p) => adminAPI.accounts.create(p), {
appStore.showSuccess(t('admin.accounts.accountCreated')) onSuccess: () => {
emit('created') appStore.showSuccess(t('admin.accounts.accountCreated'))
handleClose() emit('created')
} catch (error: any) { handleClose()
// Handle 409 mixed_channel_warning - show confirmation dialog },
if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning') { onError: (error: any) => {
const details = error.response.data.details || {} appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate'))
mixedChannelWarningDetails.value = {
groupName: details.group_name || 'Unknown',
currentPlatform: details.current_platform || 'Unknown',
otherPlatform: details.other_platform || 'Unknown'
} }
pendingCreatePayload.value = payload })
showMixedChannelWarning.value = true
} else {
appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate'))
}
} finally { } finally {
submitting.value = false submitting.value = false
} }
@@ -2618,28 +2611,16 @@ const doCreateAccount = async (payload: any) => {
// Handle mixed channel warning confirmation // Handle mixed channel warning confirmation
const handleMixedChannelConfirm = async () => { const handleMixedChannelConfirm = async () => {
showMixedChannelWarning.value = false submitting.value = true
if (pendingCreatePayload.value) { try {
pendingCreatePayload.value.confirm_mixed_channel_risk = true await mixedChannelWarning.confirm()
submitting.value = true } finally {
try { submitting.value = false
await adminAPI.accounts.create(pendingCreatePayload.value)
appStore.showSuccess(t('admin.accounts.accountCreated'))
emit('created')
handleClose()
} catch (error: any) {
appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate'))
} finally {
submitting.value = false
pendingCreatePayload.value = null
}
} }
} }
const handleMixedChannelCancel = () => { const handleMixedChannelCancel = () => {
showMixedChannelWarning.value = false mixedChannelWarning.cancel()
pendingCreatePayload.value = null
mixedChannelWarningDetails.value = null
} }
const handleSubmit = async () => { const handleSubmit = async () => {
@@ -2795,7 +2776,7 @@ const createAccountAndFinish = async (
if (!applyTempUnschedConfig(credentials)) { if (!applyTempUnschedConfig(credentials)) {
return return
} }
const payload: any = { await doCreateAccount({
name: form.name, name: form.name,
notes: form.notes, notes: form.notes,
platform, platform,
@@ -2809,31 +2790,7 @@ const createAccountAndFinish = async (
group_ids: form.group_ids, group_ids: form.group_ids,
expires_at: form.expires_at, expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value auto_pause_on_expired: autoPauseOnExpired.value
} })
try {
await adminAPI.accounts.create(payload)
} catch (error: any) {
// Handle 409 mixed_channel_warning - show confirmation dialog
// Note: upstream Antigravity create path uses createAccountAndFinish directly, so mixed warning
// must be handled here as well (otherwise user only sees a generic "failed" toast).
if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning') {
const details = error.response.data.details || {}
mixedChannelWarningDetails.value = {
groupName: details.group_name || 'Unknown',
currentPlatform: details.current_platform || 'Unknown',
otherPlatform: details.other_platform || 'Unknown'
}
pendingCreatePayload.value = payload
showMixedChannelWarning.value = true
return
}
throw error
}
appStore.showSuccess(t('admin.accounts.accountCreated'))
emit('created')
handleClose()
} }
// OpenAI OAuth 授权码兑换 // OpenAI OAuth 授权码兑换

View File

@@ -994,6 +994,7 @@ import ProxySelector from '@/components/common/ProxySelector.vue'
import GroupSelector from '@/components/common/GroupSelector.vue' import GroupSelector from '@/components/common/GroupSelector.vue'
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue' import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format' import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
import { useMixedChannelWarning } from '@/composables/useMixedChannelWarning'
import { import {
getPresetMappingsByPlatform, getPresetMappingsByPlatform,
commonErrorCodes, commonErrorCodes,
@@ -1060,10 +1061,9 @@ const antigravityModelMappings = ref<ModelMapping[]>([])
const tempUnschedEnabled = ref(false) const tempUnschedEnabled = ref(false)
const tempUnschedRules = ref<TempUnschedRuleForm[]>([]) const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
// Mixed channel warning dialog state const mixedChannelWarning = useMixedChannelWarning()
const showMixedChannelWarning = ref(false) const showMixedChannelWarning = mixedChannelWarning.show
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(null) const mixedChannelWarningDetails = mixedChannelWarning.details
const pendingUpdatePayload = ref<Record<string, unknown> | null>(null)
// Quota control state (Anthropic OAuth/SetupToken only) // Quota control state (Anthropic OAuth/SetupToken only)
const windowCostEnabled = ref(false) const windowCostEnabled = ref(false)
@@ -1525,11 +1525,13 @@ const parseDateTimeLocal = parseDateTimeLocalInput
// Methods // Methods
const handleClose = () => { const handleClose = () => {
mixedChannelWarning.cancel()
emit('close') emit('close')
} }
const handleSubmit = async () => { const handleSubmit = async () => {
if (!props.account) return if (!props.account) return
const accountID = props.account.id
submitting.value = true submitting.value = true
const updatePayload: Record<string, unknown> = { ...form } const updatePayload: Record<string, unknown> = { ...form }
@@ -1698,24 +1700,18 @@ const handleSubmit = async () => {
updatePayload.extra = newExtra updatePayload.extra = newExtra
} }
await adminAPI.accounts.update(props.account.id, updatePayload) await mixedChannelWarning.tryRequest(updatePayload, (p) => adminAPI.accounts.update(accountID, p), {
appStore.showSuccess(t('admin.accounts.accountUpdated')) onSuccess: () => {
emit('updated') appStore.showSuccess(t('admin.accounts.accountUpdated'))
handleClose() emit('updated')
} catch (error: any) { handleClose()
// Handle 409 mixed_channel_warning - show confirmation dialog },
if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning') { onError: (error: any) => {
const details = error.response.data.details || {} appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate'))
mixedChannelWarningDetails.value = {
groupName: details.group_name || 'Unknown',
currentPlatform: details.current_platform || 'Unknown',
otherPlatform: details.other_platform || 'Unknown'
} }
pendingUpdatePayload.value = updatePayload })
showMixedChannelWarning.value = true } catch (error: any) {
} else { appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate'))
appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate'))
}
} finally { } finally {
submitting.value = false submitting.value = false
} }
@@ -1723,27 +1719,15 @@ const handleSubmit = async () => {
// Handle mixed channel warning confirmation // Handle mixed channel warning confirmation
const handleMixedChannelConfirm = async () => { const handleMixedChannelConfirm = async () => {
showMixedChannelWarning.value = false submitting.value = true
if (pendingUpdatePayload.value && props.account) { try {
pendingUpdatePayload.value.confirm_mixed_channel_risk = true await mixedChannelWarning.confirm()
submitting.value = true } finally {
try { submitting.value = false
await adminAPI.accounts.update(props.account.id, pendingUpdatePayload.value)
appStore.showSuccess(t('admin.accounts.accountUpdated'))
emit('updated')
handleClose()
} catch (error: any) {
appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate'))
} finally {
submitting.value = false
pendingUpdatePayload.value = null
}
} }
} }
const handleMixedChannelCancel = () => { const handleMixedChannelCancel = () => {
showMixedChannelWarning.value = false mixedChannelWarning.cancel()
pendingUpdatePayload.value = null
mixedChannelWarningDetails.value = null
} }
</script> </script>

View File

@@ -0,0 +1,107 @@
import { ref } from 'vue'
export interface MixedChannelWarningDetails {
groupName: string
currentPlatform: string
otherPlatform: string
}
function isMixedChannelWarningError(error: any): boolean {
return error?.response?.status === 409 && error?.response?.data?.error === 'mixed_channel_warning'
}
function extractMixedChannelWarningDetails(error: any): MixedChannelWarningDetails {
const details = error?.response?.data?.details || {}
return {
groupName: details.group_name || 'Unknown',
currentPlatform: details.current_platform || 'Unknown',
otherPlatform: details.other_platform || 'Unknown'
}
}
export function useMixedChannelWarning() {
const show = ref(false)
const details = ref<MixedChannelWarningDetails | null>(null)
const pendingPayload = ref<any | null>(null)
const pendingRequest = ref<((payload: any) => Promise<any>) | null>(null)
const pendingOnSuccess = ref<(() => void) | null>(null)
const pendingOnError = ref<((error: any) => void) | null>(null)
const clearPending = () => {
pendingPayload.value = null
pendingRequest.value = null
pendingOnSuccess.value = null
pendingOnError.value = null
details.value = null
}
const tryRequest = async (
payload: any,
request: (payload: any) => Promise<any>,
opts?: {
onSuccess?: () => void
onError?: (error: any) => void
}
): Promise<boolean> => {
try {
await request(payload)
opts?.onSuccess?.()
return true
} catch (error: any) {
if (isMixedChannelWarningError(error)) {
details.value = extractMixedChannelWarningDetails(error)
pendingPayload.value = payload
pendingRequest.value = request
pendingOnSuccess.value = opts?.onSuccess || null
pendingOnError.value = opts?.onError || null
show.value = true
return false
}
if (opts?.onError) {
opts.onError(error)
return false
}
throw error
}
}
const confirm = async (): Promise<boolean> => {
show.value = false
if (!pendingPayload.value || !pendingRequest.value) {
clearPending()
return false
}
pendingPayload.value.confirm_mixed_channel_risk = true
try {
await pendingRequest.value(pendingPayload.value)
pendingOnSuccess.value?.()
return true
} catch (error: any) {
if (pendingOnError.value) {
pendingOnError.value(error)
return false
}
throw error
} finally {
clearPending()
}
}
const cancel = () => {
show.value = false
clearPending()
}
return {
show,
details,
tryRequest,
confirm,
cancel
}
}