feat: add mixed-channel precheck API for account-group binding
Add a dedicated CheckMixedChannel endpoint that allows the frontend to pre-validate mixed channel risk before submitting create/update requests. This improves UX by showing warnings earlier in the flow instead of only after form submission. Backend changes: - Add CheckMixedChannelRequest struct and CheckMixedChannel handler - Register POST /check-mixed-channel route - Expose CheckMixedChannelRisk as public method on AdminService - Simplify Create/Update 409 responses (remove details/require_confirmation) - Add comprehensive handler tests and stub methods Frontend changes: - Add checkMixedChannelRisk API function and TypeScript types - Refactor CreateAccountModal to precheck before step transition and submission - Refactor EditAccountModal to precheck before update submission - Replace pendingPayload pattern with action-based dialog flow
This commit is contained in:
@@ -15,7 +15,9 @@ import type {
|
||||
AccountUsageStatsResponse,
|
||||
TempUnschedulableStatus,
|
||||
AdminDataPayload,
|
||||
AdminDataImportResult
|
||||
AdminDataImportResult,
|
||||
CheckMixedChannelRequest,
|
||||
CheckMixedChannelResponse
|
||||
} from '@/types'
|
||||
|
||||
/**
|
||||
@@ -133,6 +135,16 @@ export async function update(id: number, updates: UpdateAccountRequest): Promise
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Check mixed-channel risk for account-group binding.
|
||||
*/
|
||||
export async function checkMixedChannelRisk(
|
||||
payload: CheckMixedChannelRequest
|
||||
): Promise<CheckMixedChannelResponse> {
|
||||
const { data } = await apiClient.post<CheckMixedChannelResponse>('/admin/accounts/check-mixed-channel', payload)
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete account
|
||||
* @param id - Account ID
|
||||
@@ -535,6 +547,7 @@ export const accountsAPI = {
|
||||
getById,
|
||||
create,
|
||||
update,
|
||||
checkMixedChannelRisk,
|
||||
delete: deleteAccount,
|
||||
toggleStatus,
|
||||
testAccount,
|
||||
|
||||
@@ -2157,7 +2157,7 @@
|
||||
<ConfirmDialog
|
||||
:show="showMixedChannelWarning"
|
||||
:title="t('admin.accounts.mixedChannelWarningTitle')"
|
||||
:message="mixedChannelWarningDetails ? t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails) : ''"
|
||||
:message="mixedChannelWarningMessageText"
|
||||
:confirm-text="t('common.confirm')"
|
||||
:cancel-text="t('common.cancel')"
|
||||
:danger="true"
|
||||
@@ -2189,7 +2189,14 @@ import {
|
||||
import { useOpenAIOAuth } from '@/composables/useOpenAIOAuth'
|
||||
import { useGeminiOAuth } from '@/composables/useGeminiOAuth'
|
||||
import { useAntigravityOAuth } from '@/composables/useAntigravityOAuth'
|
||||
import type { Proxy, AdminGroup, AccountPlatform, AccountType } from '@/types'
|
||||
import type {
|
||||
Proxy,
|
||||
AdminGroup,
|
||||
AccountPlatform,
|
||||
AccountType,
|
||||
CheckMixedChannelResponse,
|
||||
CreateAccountRequest
|
||||
} from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
@@ -2338,10 +2345,13 @@ const getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>
|
||||
const geminiOAuthType = ref<'code_assist' | 'google_one' | 'ai_studio'>('google_one')
|
||||
const geminiAIStudioOAuthEnabled = ref(false)
|
||||
|
||||
// Mixed channel warning dialog state
|
||||
const showMixedChannelWarning = ref(false)
|
||||
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(null)
|
||||
const pendingCreatePayload = ref<any>(null)
|
||||
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(
|
||||
null
|
||||
)
|
||||
const mixedChannelWarningRawMessage = ref('')
|
||||
const mixedChannelWarningAction = ref<(() => Promise<void>) | null>(null)
|
||||
const antigravityMixedChannelConfirmed = ref(false)
|
||||
const showAdvancedOAuth = ref(false)
|
||||
const showGeminiHelpDialog = ref(false)
|
||||
|
||||
@@ -2379,6 +2389,13 @@ const isOpenAIModelRestrictionDisabled = computed(() =>
|
||||
form.platform === 'openai' && openaiPassthroughEnabled.value
|
||||
)
|
||||
|
||||
const mixedChannelWarningMessageText = computed(() => {
|
||||
if (mixedChannelWarningDetails.value) {
|
||||
return t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails.value)
|
||||
}
|
||||
return mixedChannelWarningRawMessage.value
|
||||
})
|
||||
|
||||
const geminiQuotaDocs = {
|
||||
codeAssist: 'https://developers.google.com/gemini-code-assist/resources/quotas',
|
||||
aiStudio: 'https://ai.google.dev/pricing',
|
||||
@@ -2795,6 +2812,105 @@ const splitTempUnschedKeywords = (value: string) => {
|
||||
.filter((item) => item.length > 0)
|
||||
}
|
||||
|
||||
const needsMixedChannelCheck = (platform: AccountPlatform) => platform === 'antigravity' || platform === 'anthropic'
|
||||
|
||||
const buildMixedChannelDetails = (resp?: CheckMixedChannelResponse) => {
|
||||
const details = resp?.details
|
||||
if (!details) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
groupName: details.group_name || 'Unknown',
|
||||
currentPlatform: details.current_platform || 'Unknown',
|
||||
otherPlatform: details.other_platform || 'Unknown'
|
||||
}
|
||||
}
|
||||
|
||||
const clearMixedChannelDialog = () => {
|
||||
showMixedChannelWarning.value = false
|
||||
mixedChannelWarningDetails.value = null
|
||||
mixedChannelWarningRawMessage.value = ''
|
||||
mixedChannelWarningAction.value = null
|
||||
}
|
||||
|
||||
const openMixedChannelDialog = (opts: {
|
||||
response?: CheckMixedChannelResponse
|
||||
message?: string
|
||||
onConfirm: () => Promise<void>
|
||||
}) => {
|
||||
mixedChannelWarningDetails.value = buildMixedChannelDetails(opts.response)
|
||||
mixedChannelWarningRawMessage.value =
|
||||
opts.message || opts.response?.message || t('admin.accounts.failedToCreate')
|
||||
mixedChannelWarningAction.value = opts.onConfirm
|
||||
showMixedChannelWarning.value = true
|
||||
}
|
||||
|
||||
const withAntigravityConfirmFlag = (payload: CreateAccountRequest): CreateAccountRequest => {
|
||||
if (needsMixedChannelCheck(payload.platform) && antigravityMixedChannelConfirmed.value) {
|
||||
return {
|
||||
...payload,
|
||||
confirm_mixed_channel_risk: true
|
||||
}
|
||||
}
|
||||
const cloned = { ...payload }
|
||||
delete cloned.confirm_mixed_channel_risk
|
||||
return cloned
|
||||
}
|
||||
|
||||
const ensureAntigravityMixedChannelConfirmed = async (onConfirm: () => Promise<void>): Promise<boolean> => {
|
||||
if (!needsMixedChannelCheck(form.platform)) {
|
||||
return true
|
||||
}
|
||||
if (antigravityMixedChannelConfirmed.value) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await adminAPI.accounts.checkMixedChannelRisk({
|
||||
platform: form.platform,
|
||||
group_ids: form.group_ids
|
||||
})
|
||||
if (!result.has_risk) {
|
||||
return true
|
||||
}
|
||||
openMixedChannelDialog({
|
||||
response: result,
|
||||
onConfirm: async () => {
|
||||
antigravityMixedChannelConfirmed.value = true
|
||||
await onConfirm()
|
||||
}
|
||||
})
|
||||
return false
|
||||
} catch (error: any) {
|
||||
appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToCreate'))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const submitCreateAccount = async (payload: CreateAccountRequest) => {
|
||||
submitting.value = true
|
||||
try {
|
||||
await adminAPI.accounts.create(withAntigravityConfirmFlag(payload))
|
||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
||||
emit('created')
|
||||
handleClose()
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning' && needsMixedChannelCheck(form.platform)) {
|
||||
openMixedChannelDialog({
|
||||
message: error.response?.data?.message,
|
||||
onConfirm: async () => {
|
||||
antigravityMixedChannelConfirmed.value = true
|
||||
await submitCreateAccount(payload)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToCreate'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Methods
|
||||
const resetForm = () => {
|
||||
step.value = 1
|
||||
@@ -2856,9 +2972,13 @@ const resetForm = () => {
|
||||
geminiOAuth.resetState()
|
||||
antigravityOAuth.resetState()
|
||||
oauthFlowRef.value?.reset()
|
||||
antigravityMixedChannelConfirmed.value = false
|
||||
clearMixedChannelDialog()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
antigravityMixedChannelConfirmed.value = false
|
||||
clearMixedChannelDialog()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
@@ -2917,56 +3037,34 @@ const buildSoraExtra = (
|
||||
}
|
||||
|
||||
// Helper function to create account with mixed channel warning handling
|
||||
const doCreateAccount = async (payload: any) => {
|
||||
const doCreateAccount = async (payload: CreateAccountRequest) => {
|
||||
const canContinue = await ensureAntigravityMixedChannelConfirmed(async () => {
|
||||
await submitCreateAccount(payload)
|
||||
})
|
||||
if (!canContinue) {
|
||||
return
|
||||
}
|
||||
await submitCreateAccount(payload)
|
||||
}
|
||||
|
||||
// Handle mixed channel warning confirmation
|
||||
const handleMixedChannelConfirm = async () => {
|
||||
const action = mixedChannelWarningAction.value
|
||||
if (!action) {
|
||||
clearMixedChannelDialog()
|
||||
return
|
||||
}
|
||||
clearMixedChannelDialog()
|
||||
submitting.value = true
|
||||
try {
|
||||
await adminAPI.accounts.create(payload)
|
||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
||||
emit('created')
|
||||
handleClose()
|
||||
} catch (error: any) {
|
||||
// Handle 409 mixed_channel_warning - show confirmation dialog
|
||||
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
|
||||
} else {
|
||||
appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate'))
|
||||
}
|
||||
await action()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Handle mixed channel warning confirmation
|
||||
const handleMixedChannelConfirm = async () => {
|
||||
showMixedChannelWarning.value = false
|
||||
if (pendingCreatePayload.value) {
|
||||
pendingCreatePayload.value.confirm_mixed_channel_risk = true
|
||||
submitting.value = true
|
||||
try {
|
||||
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 = () => {
|
||||
showMixedChannelWarning.value = false
|
||||
pendingCreatePayload.value = null
|
||||
mixedChannelWarningDetails.value = null
|
||||
clearMixedChannelDialog()
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -2976,6 +3074,12 @@ const handleSubmit = async () => {
|
||||
appStore.showError(t('admin.accounts.pleaseEnterAccountName'))
|
||||
return
|
||||
}
|
||||
const canContinue = await ensureAntigravityMixedChannelConfirmed(async () => {
|
||||
step.value = 2
|
||||
})
|
||||
if (!canContinue) {
|
||||
return
|
||||
}
|
||||
step.value = 2
|
||||
return
|
||||
}
|
||||
@@ -3132,7 +3236,7 @@ const createAccountAndFinish = async (
|
||||
if (!applyTempUnschedConfig(credentials)) {
|
||||
return
|
||||
}
|
||||
await adminAPI.accounts.create({
|
||||
await doCreateAccount({
|
||||
name: form.name,
|
||||
notes: form.notes,
|
||||
platform,
|
||||
@@ -3147,9 +3251,6 @@ const createAccountAndFinish = async (
|
||||
expires_at: form.expires_at,
|
||||
auto_pause_on_expired: autoPauseOnExpired.value
|
||||
})
|
||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
||||
emit('created')
|
||||
handleClose()
|
||||
}
|
||||
|
||||
// OpenAI OAuth 授权码兑换
|
||||
@@ -3497,7 +3598,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
|
||||
const accountName = refreshTokens.length > 1 ? `${form.name} #${i + 1}` : form.name
|
||||
|
||||
// Note: Antigravity doesn't have buildExtraInfo, so we pass empty extra or rely on credentials
|
||||
await adminAPI.accounts.create({
|
||||
const createPayload = withAntigravityConfirmFlag({
|
||||
name: accountName,
|
||||
notes: form.notes,
|
||||
platform: 'antigravity',
|
||||
@@ -3512,6 +3613,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
|
||||
expires_at: form.expires_at,
|
||||
auto_pause_on_expired: autoPauseOnExpired.value
|
||||
})
|
||||
await adminAPI.accounts.create(createPayload)
|
||||
successCount++
|
||||
} catch (error: any) {
|
||||
failedCount++
|
||||
|
||||
@@ -1139,7 +1139,7 @@
|
||||
<ConfirmDialog
|
||||
:show="showMixedChannelWarning"
|
||||
:title="t('admin.accounts.mixedChannelWarningTitle')"
|
||||
:message="mixedChannelWarningDetails ? t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails) : ''"
|
||||
:message="mixedChannelWarningMessageText"
|
||||
:confirm-text="t('common.confirm')"
|
||||
:cancel-text="t('common.cancel')"
|
||||
:danger="true"
|
||||
@@ -1154,7 +1154,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { adminAPI } from '@/api/admin'
|
||||
import type { Account, Proxy, AdminGroup } from '@/types'
|
||||
import type { Account, Proxy, AdminGroup, CheckMixedChannelResponse } from '@/types'
|
||||
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||
import Select from '@/components/common/Select.vue'
|
||||
@@ -1234,10 +1234,13 @@ const getModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-mod
|
||||
const getAntigravityModelMappingKey = createStableObjectKeyResolver<ModelMapping>('edit-antigravity-model-mapping')
|
||||
const getTempUnschedRuleKey = createStableObjectKeyResolver<TempUnschedRuleForm>('edit-temp-unsched-rule')
|
||||
|
||||
// Mixed channel warning dialog state
|
||||
const showMixedChannelWarning = ref(false)
|
||||
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(null)
|
||||
const pendingUpdatePayload = ref<Record<string, unknown> | null>(null)
|
||||
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(
|
||||
null
|
||||
)
|
||||
const mixedChannelWarningRawMessage = ref('')
|
||||
const mixedChannelWarningAction = ref<(() => Promise<void>) | null>(null)
|
||||
const antigravityMixedChannelConfirmed = ref(false)
|
||||
|
||||
// Quota control state (Anthropic OAuth/SetupToken only)
|
||||
const windowCostEnabled = ref(false)
|
||||
@@ -1298,6 +1301,13 @@ const defaultBaseUrl = computed(() => {
|
||||
return 'https://api.anthropic.com'
|
||||
})
|
||||
|
||||
const mixedChannelWarningMessageText = computed(() => {
|
||||
if (mixedChannelWarningDetails.value) {
|
||||
return t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails.value)
|
||||
}
|
||||
return mixedChannelWarningRawMessage.value
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
name: '',
|
||||
notes: '',
|
||||
@@ -1327,6 +1337,11 @@ watch(
|
||||
() => props.account,
|
||||
(newAccount) => {
|
||||
if (newAccount) {
|
||||
antigravityMixedChannelConfirmed.value = false
|
||||
showMixedChannelWarning.value = false
|
||||
mixedChannelWarningDetails.value = null
|
||||
mixedChannelWarningRawMessage.value = ''
|
||||
mixedChannelWarningAction.value = null
|
||||
form.name = newAccount.name
|
||||
form.notes = newAccount.notes || ''
|
||||
form.proxy_id = newAccount.proxy_id
|
||||
@@ -1726,18 +1741,123 @@ function toPositiveNumber(value: unknown) {
|
||||
return Math.trunc(num)
|
||||
}
|
||||
|
||||
const needsMixedChannelCheck = () => props.account?.platform === 'antigravity' || props.account?.platform === 'anthropic'
|
||||
|
||||
const buildMixedChannelDetails = (resp?: CheckMixedChannelResponse) => {
|
||||
const details = resp?.details
|
||||
if (!details) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
groupName: details.group_name || 'Unknown',
|
||||
currentPlatform: details.current_platform || 'Unknown',
|
||||
otherPlatform: details.other_platform || 'Unknown'
|
||||
}
|
||||
}
|
||||
|
||||
const clearMixedChannelDialog = () => {
|
||||
showMixedChannelWarning.value = false
|
||||
mixedChannelWarningDetails.value = null
|
||||
mixedChannelWarningRawMessage.value = ''
|
||||
mixedChannelWarningAction.value = null
|
||||
}
|
||||
|
||||
const openMixedChannelDialog = (opts: {
|
||||
response?: CheckMixedChannelResponse
|
||||
message?: string
|
||||
onConfirm: () => Promise<void>
|
||||
}) => {
|
||||
mixedChannelWarningDetails.value = buildMixedChannelDetails(opts.response)
|
||||
mixedChannelWarningRawMessage.value =
|
||||
opts.message || opts.response?.message || t('admin.accounts.failedToUpdate')
|
||||
mixedChannelWarningAction.value = opts.onConfirm
|
||||
showMixedChannelWarning.value = true
|
||||
}
|
||||
|
||||
const withAntigravityConfirmFlag = (payload: Record<string, unknown>) => {
|
||||
if (needsMixedChannelCheck() && antigravityMixedChannelConfirmed.value) {
|
||||
return {
|
||||
...payload,
|
||||
confirm_mixed_channel_risk: true
|
||||
}
|
||||
}
|
||||
const cloned = { ...payload }
|
||||
delete cloned.confirm_mixed_channel_risk
|
||||
return cloned
|
||||
}
|
||||
|
||||
const ensureAntigravityMixedChannelConfirmed = async (onConfirm: () => Promise<void>): Promise<boolean> => {
|
||||
if (!needsMixedChannelCheck()) {
|
||||
return true
|
||||
}
|
||||
if (antigravityMixedChannelConfirmed.value) {
|
||||
return true
|
||||
}
|
||||
if (!props.account) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await adminAPI.accounts.checkMixedChannelRisk({
|
||||
platform: props.account.platform,
|
||||
group_ids: form.group_ids,
|
||||
account_id: props.account.id
|
||||
})
|
||||
if (!result.has_risk) {
|
||||
return true
|
||||
}
|
||||
openMixedChannelDialog({
|
||||
response: result,
|
||||
onConfirm: async () => {
|
||||
antigravityMixedChannelConfirmed.value = true
|
||||
await onConfirm()
|
||||
}
|
||||
})
|
||||
return false
|
||||
} catch (error: any) {
|
||||
appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate'))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const formatDateTimeLocal = formatDateTimeLocalInput
|
||||
const parseDateTimeLocal = parseDateTimeLocalInput
|
||||
|
||||
// Methods
|
||||
const handleClose = () => {
|
||||
antigravityMixedChannelConfirmed.value = false
|
||||
clearMixedChannelDialog()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const submitUpdateAccount = async (accountID: number, updatePayload: Record<string, unknown>) => {
|
||||
submitting.value = true
|
||||
try {
|
||||
const updatedAccount = await adminAPI.accounts.update(accountID, withAntigravityConfirmFlag(updatePayload))
|
||||
appStore.showSuccess(t('admin.accounts.accountUpdated'))
|
||||
emit('updated', updatedAccount)
|
||||
handleClose()
|
||||
} catch (error: any) {
|
||||
if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning' && needsMixedChannelCheck()) {
|
||||
openMixedChannelDialog({
|
||||
message: error.response?.data?.message,
|
||||
onConfirm: async () => {
|
||||
antigravityMixedChannelConfirmed.value = true
|
||||
await submitUpdateAccount(accountID, updatePayload)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!props.account) return
|
||||
const accountID = props.account.id
|
||||
|
||||
submitting.value = true
|
||||
const updatePayload: Record<string, unknown> = { ...form }
|
||||
try {
|
||||
// 后端期望 proxy_id: 0 表示清除代理,而不是 null
|
||||
@@ -1769,7 +1889,6 @@ const handleSubmit = async () => {
|
||||
newCredentials.api_key = currentCredentials.api_key
|
||||
} else {
|
||||
appStore.showError(t('admin.accounts.apiKeyIsRequired'))
|
||||
submitting.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1792,7 +1911,6 @@ const handleSubmit = async () => {
|
||||
// Add intercept warmup requests setting
|
||||
applyInterceptWarmup(newCredentials, interceptWarmupRequests.value, 'edit')
|
||||
if (!applyTempUnschedConfig(newCredentials)) {
|
||||
submitting.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1811,7 +1929,6 @@ const handleSubmit = async () => {
|
||||
applyInterceptWarmup(newCredentials, interceptWarmupRequests.value, 'edit')
|
||||
|
||||
if (!applyTempUnschedConfig(newCredentials)) {
|
||||
submitting.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1823,7 +1940,6 @@ const handleSubmit = async () => {
|
||||
|
||||
applyInterceptWarmup(newCredentials, interceptWarmupRequests.value, 'edit')
|
||||
if (!applyTempUnschedConfig(newCredentials)) {
|
||||
submitting.value = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1953,52 +2069,36 @@ const handleSubmit = async () => {
|
||||
updatePayload.extra = newExtra
|
||||
}
|
||||
|
||||
const updatedAccount = await adminAPI.accounts.update(props.account.id, updatePayload)
|
||||
appStore.showSuccess(t('admin.accounts.accountUpdated'))
|
||||
emit('updated', updatedAccount)
|
||||
handleClose()
|
||||
} catch (error: any) {
|
||||
// Handle 409 mixed_channel_warning - show confirmation dialog
|
||||
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'
|
||||
}
|
||||
pendingUpdatePayload.value = updatePayload
|
||||
showMixedChannelWarning.value = true
|
||||
} else {
|
||||
appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate'))
|
||||
const canContinue = await ensureAntigravityMixedChannelConfirmed(async () => {
|
||||
await submitUpdateAccount(accountID, updatePayload)
|
||||
})
|
||||
if (!canContinue) {
|
||||
return
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
|
||||
await submitUpdateAccount(accountID, updatePayload)
|
||||
} catch (error: any) {
|
||||
appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate'))
|
||||
}
|
||||
}
|
||||
|
||||
// Handle mixed channel warning confirmation
|
||||
const handleMixedChannelConfirm = async () => {
|
||||
showMixedChannelWarning.value = false
|
||||
if (pendingUpdatePayload.value && props.account) {
|
||||
pendingUpdatePayload.value.confirm_mixed_channel_risk = true
|
||||
submitting.value = true
|
||||
try {
|
||||
const updatedAccount = await adminAPI.accounts.update(props.account.id, pendingUpdatePayload.value)
|
||||
appStore.showSuccess(t('admin.accounts.accountUpdated'))
|
||||
emit('updated', updatedAccount)
|
||||
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 action = mixedChannelWarningAction.value
|
||||
if (!action) {
|
||||
clearMixedChannelDialog()
|
||||
return
|
||||
}
|
||||
clearMixedChannelDialog()
|
||||
submitting.value = true
|
||||
try {
|
||||
await action()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleMixedChannelCancel = () => {
|
||||
showMixedChannelWarning.value = false
|
||||
pendingUpdatePayload.value = null
|
||||
mixedChannelWarningDetails.value = null
|
||||
clearMixedChannelDialog()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -766,6 +766,26 @@ export interface UpdateAccountRequest {
|
||||
confirm_mixed_channel_risk?: boolean
|
||||
}
|
||||
|
||||
export interface CheckMixedChannelRequest {
|
||||
platform: AccountPlatform
|
||||
group_ids: number[]
|
||||
account_id?: number
|
||||
}
|
||||
|
||||
export interface MixedChannelWarningDetails {
|
||||
group_id: number
|
||||
group_name: string
|
||||
current_platform: string
|
||||
other_platform: string
|
||||
}
|
||||
|
||||
export interface CheckMixedChannelResponse {
|
||||
has_risk: boolean
|
||||
error?: string
|
||||
message?: string
|
||||
details?: MixedChannelWarningDetails
|
||||
}
|
||||
|
||||
export interface CreateProxyRequest {
|
||||
name: string
|
||||
protocol: ProxyProtocol
|
||||
|
||||
Reference in New Issue
Block a user