feat: bulk update accounts pre-check mixed channel risk with confirm dialog
- Move mixed channel check before any DB writes in BulkUpdateAccounts - Return 409 from BulkUpdate handler for MixedChannelError - Add ConfirmDialog to BulkEditAccountModal for mixed channel warning - Update mixed channel warning message to Chinese
This commit is contained in:
@@ -1122,6 +1122,14 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
|
|||||||
SkipMixedChannelCheck: skipCheck,
|
SkipMixedChannelCheck: skipCheck,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
var mixedErr *service.MixedChannelError
|
||||||
|
if errors.As(err, &mixedErr) {
|
||||||
|
c.JSON(409, gin.H{
|
||||||
|
"error": "mixed_channel_warning",
|
||||||
|
"message": mixedErr.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1565,6 +1565,19 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp
|
|||||||
groupNameByID = loadedNames
|
groupNameByID = loadedNames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 预检查混合渠道风险:在任何写操作之前,若发现风险立即返回错误。
|
||||||
|
if needMixedChannelCheck {
|
||||||
|
for _, accountID := range input.AccountIDs {
|
||||||
|
platform := platformByID[accountID]
|
||||||
|
if platform == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.checkMixedChannelRisk(ctx, accountID, platform, *input.GroupIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if input.RateMultiplier != nil {
|
if input.RateMultiplier != nil {
|
||||||
if *input.RateMultiplier < 0 {
|
if *input.RateMultiplier < 0 {
|
||||||
return nil, errors.New("rate_multiplier must be >= 0")
|
return nil, errors.New("rate_multiplier must be >= 0")
|
||||||
@@ -1609,31 +1622,6 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp
|
|||||||
platform := ""
|
platform := ""
|
||||||
|
|
||||||
if input.GroupIDs != nil {
|
if input.GroupIDs != nil {
|
||||||
// 检查混合渠道风险(除非用户已确认)
|
|
||||||
if !input.SkipMixedChannelCheck {
|
|
||||||
platform = platformByID[accountID]
|
|
||||||
if platform == "" {
|
|
||||||
account, err := s.accountRepo.GetByID(ctx, accountID)
|
|
||||||
if err != nil {
|
|
||||||
entry.Success = false
|
|
||||||
entry.Error = err.Error()
|
|
||||||
result.Failed++
|
|
||||||
result.FailedIDs = append(result.FailedIDs, accountID)
|
|
||||||
result.Results = append(result.Results, entry)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
platform = account.Platform
|
|
||||||
}
|
|
||||||
if err := s.checkMixedChannelRiskWithPreloaded(accountID, platform, *input.GroupIDs, groupAccountsByID, groupNameByID); err != nil {
|
|
||||||
entry.Success = false
|
|
||||||
entry.Error = err.Error()
|
|
||||||
result.Failed++
|
|
||||||
result.FailedIDs = append(result.FailedIDs, accountID)
|
|
||||||
result.Results = append(result.Results, entry)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.accountRepo.BindGroups(ctx, accountID, *input.GroupIDs); err != nil {
|
if err := s.accountRepo.BindGroups(ctx, accountID, *input.GroupIDs); err != nil {
|
||||||
entry.Success = false
|
entry.Success = false
|
||||||
entry.Error = err.Error()
|
entry.Error = err.Error()
|
||||||
@@ -2541,6 +2529,6 @@ type MixedChannelError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *MixedChannelError) Error() string {
|
func (e *MixedChannelError) Error() string {
|
||||||
return fmt.Sprintf("mixed_channel_warning: Group '%s' contains both %s and %s accounts. Using mixed channels in the same context may cause thinking block signature validation issues, which will fallback to non-thinking mode for historical messages.",
|
return fmt.Sprintf("警告:分组 \"%s\" 中同时包含 %s 和 %s 账号。混合使用不同渠道可能导致 thinking block 签名验证问题,会导致请求报错,请确认混合调用的 Anthropic 账号对应的是反重力反代的 Claude API。确定要继续吗?",
|
||||||
e.GroupName, e.CurrentPlatform, e.OtherPlatform)
|
e.GroupName, e.CurrentPlatform, e.OtherPlatform)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -756,6 +756,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
:show="showMixedChannelWarning"
|
||||||
|
:title="t('admin.accounts.mixedChannelWarningTitle')"
|
||||||
|
:message="mixedChannelWarningMessage"
|
||||||
|
:confirm-text="t('common.confirm')"
|
||||||
|
:cancel-text="t('common.cancel')"
|
||||||
|
:danger="true"
|
||||||
|
@confirm="handleMixedChannelConfirm"
|
||||||
|
@cancel="handleMixedChannelCancel"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -765,6 +776,7 @@ import { useAppStore } from '@/stores/app'
|
|||||||
import { adminAPI } from '@/api/admin'
|
import { adminAPI } from '@/api/admin'
|
||||||
import type { Proxy as ProxyConfig, AdminGroup, AccountPlatform, AccountType } from '@/types'
|
import type { Proxy as ProxyConfig, 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 Select from '@/components/common/Select.vue'
|
import Select from '@/components/common/Select.vue'
|
||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||||
@@ -844,6 +856,9 @@ const enableRpmLimit = ref(false)
|
|||||||
|
|
||||||
// State - field values
|
// State - field values
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
const showMixedChannelWarning = ref(false)
|
||||||
|
const mixedChannelWarningMessage = ref('')
|
||||||
|
const pendingUpdatesForConfirm = ref<Record<string, unknown> | null>(null)
|
||||||
const baseUrl = ref('')
|
const baseUrl = ref('')
|
||||||
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
const modelRestrictionMode = ref<'whitelist' | 'mapping'>('whitelist')
|
||||||
const allowedModels = ref<string[]>([])
|
const allowedModels = ref<string[]>([])
|
||||||
@@ -1238,10 +1253,13 @@ const buildUpdatePayload = (): Record<string, unknown> | null => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
|
showMixedChannelWarning.value = false
|
||||||
|
mixedChannelWarningMessage.value = ''
|
||||||
|
pendingUpdatesForConfirm.value = null
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async (confirmMixedChannel = false) => {
|
||||||
if (props.accountIds.length === 0) {
|
if (props.accountIds.length === 0) {
|
||||||
appStore.showError(t('admin.accounts.bulkEdit.noSelection'))
|
appStore.showError(t('admin.accounts.bulkEdit.noSelection'))
|
||||||
return
|
return
|
||||||
@@ -1265,10 +1283,16 @@ const handleSubmit = async () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const updates = buildUpdatePayload()
|
let updates: Record<string, unknown>
|
||||||
if (!updates) {
|
if (confirmMixedChannel && pendingUpdatesForConfirm.value) {
|
||||||
appStore.showError(t('admin.accounts.bulkEdit.noFieldsSelected'))
|
updates = { ...pendingUpdatesForConfirm.value, confirm_mixed_channel_risk: true }
|
||||||
return
|
} else {
|
||||||
|
const built = buildUpdatePayload()
|
||||||
|
if (!built) {
|
||||||
|
appStore.showError(t('admin.accounts.bulkEdit.noFieldsSelected'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updates = built
|
||||||
}
|
}
|
||||||
|
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
@@ -1287,17 +1311,34 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (success > 0) {
|
if (success > 0) {
|
||||||
|
pendingUpdatesForConfirm.value = null
|
||||||
emit('updated')
|
emit('updated')
|
||||||
handleClose()
|
handleClose()
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
appStore.showError(error.response?.data?.detail || t('admin.accounts.bulkEdit.failed'))
|
if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning') {
|
||||||
console.error('Error bulk updating accounts:', error)
|
pendingUpdatesForConfirm.value = updates
|
||||||
|
mixedChannelWarningMessage.value = error.response.data.message
|
||||||
|
showMixedChannelWarning.value = true
|
||||||
|
} else {
|
||||||
|
appStore.showError(error.response?.data?.detail || t('admin.accounts.bulkEdit.failed'))
|
||||||
|
console.error('Error bulk updating accounts:', error)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleMixedChannelConfirm = async () => {
|
||||||
|
showMixedChannelWarning.value = false
|
||||||
|
await handleSubmit(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMixedChannelCancel = () => {
|
||||||
|
showMixedChannelWarning.value = false
|
||||||
|
pendingUpdatesForConfirm.value = null
|
||||||
|
}
|
||||||
|
|
||||||
// Reset form when modal closes
|
// Reset form when modal closes
|
||||||
watch(
|
watch(
|
||||||
() => props.show,
|
() => props.show,
|
||||||
@@ -1330,10 +1371,11 @@ watch(
|
|||||||
rateMultiplier.value = 1
|
rateMultiplier.value = 1
|
||||||
status.value = 'active'
|
status.value = 'active'
|
||||||
groupIds.value = []
|
groupIds.value = []
|
||||||
rpmLimitEnabled.value = false
|
|
||||||
bulkBaseRpm.value = null
|
// Reset mixed channel warning state
|
||||||
bulkRpmStrategy.value = 'tiered'
|
showMixedChannelWarning.value = false
|
||||||
bulkRpmStickyBuffer.value = null
|
mixedChannelWarningMessage.value = ''
|
||||||
|
pendingUpdatesForConfirm.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user