diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 98ead284..38250ae3 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -1127,6 +1127,12 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) { c.JSON(409, gin.H{ "error": "mixed_channel_warning", "message": mixedErr.Error(), + "details": gin.H{ + "group_id": mixedErr.GroupID, + "group_name": mixedErr.GroupName, + "current_platform": mixedErr.CurrentPlatform, + "other_platform": mixedErr.OtherPlatform, + }, }) return } diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 63ea1069..5c4862ba 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -1542,27 +1542,16 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp // 预加载账号平台信息(混合渠道检查或 Sora 同步需要)。 platformByID := map[int64]string{} groupAccountsByID := map[int64][]Account{} - groupNameByID := map[int64]string{} if needMixedChannelCheck { accounts, err := s.accountRepo.GetByIDs(ctx, input.AccountIDs) - if err != nil { - if needMixedChannelCheck { - return nil, err - } - } else { - for _, account := range accounts { - if account != nil { - platformByID[account.ID] = account.Platform - } - } - } - - loadedAccounts, loadedNames, err := s.preloadMixedChannelRiskData(ctx, *input.GroupIDs) if err != nil { return nil, err } - groupAccountsByID = loadedAccounts - groupNameByID = loadedNames + for _, account := range accounts { + if account != nil { + platformByID[account.ID] = account.Platform + } + } } // 预检查混合渠道风险:在任何写操作之前,若发现风险立即返回错误。 @@ -2529,6 +2518,6 @@ type MixedChannelError struct { } func (e *MixedChannelError) Error() string { - return fmt.Sprintf("警告:分组 \"%s\" 中同时包含 %s 和 %s 账号。混合使用不同渠道可能导致 thinking block 签名验证问题,请确保 Anthropic 账号是 Antigravity 反代暴露的 api。确定要继续吗?", + 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.", e.GroupName, e.CurrentPlatform, e.OtherPlatform) } diff --git a/backend/internal/service/admin_service_bulk_update_test.go b/backend/internal/service/admin_service_bulk_update_test.go index 647a84a9..4845d87c 100644 --- a/backend/internal/service/admin_service_bulk_update_test.go +++ b/backend/internal/service/admin_service_bulk_update_test.go @@ -139,34 +139,34 @@ func TestAdminService_BulkUpdateAccounts_NilGroupRepoReturnsError(t *testing.T) require.Contains(t, err.Error(), "group repository not configured") } -func TestAdminService_BulkUpdateAccounts_MixedChannelCheckUsesUpdatedSnapshot(t *testing.T) { +// TestAdminService_BulkUpdateAccounts_MixedChannelPreCheckBlocksOnExistingConflict verifies +// that the global pre-check detects a conflict with existing group members and returns an +// error before any DB write is performed. +func TestAdminService_BulkUpdateAccounts_MixedChannelPreCheckBlocksOnExistingConflict(t *testing.T) { repo := &accountRepoStubForBulkUpdate{ getByIDsAccounts: []*Account{ - {ID: 1, Platform: PlatformAnthropic}, - {ID: 2, Platform: PlatformAntigravity}, + {ID: 1, Platform: PlatformAntigravity}, }, + // Group 10 already contains an Anthropic account. listByGroupData: map[int64][]Account{ - 10: {}, + 10: {{ID: 99, Platform: PlatformAnthropic}}, }, } svc := &adminServiceImpl{ accountRepo: repo, - groupRepo: &groupRepoStubForAdmin{getByID: &Group{ID: 10, Name: "目标分组"}}, + groupRepo: &groupRepoStubForAdmin{getByID: &Group{ID: 10, Name: "target-group"}}, } groupIDs := []int64{10} input := &BulkUpdateAccountsInput{ - AccountIDs: []int64{1, 2}, + AccountIDs: []int64{1}, GroupIDs: &groupIDs, } result, err := svc.BulkUpdateAccounts(context.Background(), input) - require.NoError(t, err) - require.Equal(t, 1, result.Success) - require.Equal(t, 1, result.Failed) - require.ElementsMatch(t, []int64{1}, result.SuccessIDs) - require.ElementsMatch(t, []int64{2}, result.FailedIDs) - require.Len(t, result.Results, 2) - require.Contains(t, result.Results[1].Error, "mixed channel") - require.Equal(t, []int64{1}, repo.bindGroupsCalls) + require.Nil(t, result) + require.Error(t, err) + require.Contains(t, err.Error(), "mixed channel") + // No BindGroups should have been called since the check runs before any write. + require.Empty(t, repo.bindGroupsCalls) } diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue index 30c3d739..949400ea 100644 --- a/frontend/src/components/account/BulkEditAccountModal.vue +++ b/frontend/src/components/account/BulkEditAccountModal.vue @@ -1283,7 +1283,11 @@ const preCheckMixedChannelRisk = async (built: Record): Promise if (!result.has_risk) return true pendingUpdatesForConfirm.value = built - mixedChannelWarningMessage.value = result.message || t('admin.accounts.bulkEdit.failed') + mixedChannelWarningMessage.value = t('admin.accounts.mixedChannelWarning', { + groupName: result.details?.group_name, + currentPlatform: result.details?.current_platform, + otherPlatform: result.details?.other_platform + }) showMixedChannelWarning.value = true return false } catch (error: any) { @@ -1358,7 +1362,11 @@ const submitBulkUpdate = async (baseUpdates: Record) => { // 兜底:多平台混合场景下,预检查跳过,由后端 409 触发确认框 if (error.status === 409 && error.error === 'mixed_channel_warning') { pendingUpdatesForConfirm.value = baseUpdates - mixedChannelWarningMessage.value = error.message + mixedChannelWarningMessage.value = t('admin.accounts.mixedChannelWarning', { + groupName: error.details?.group_name, + currentPlatform: error.details?.current_platform, + otherPlatform: error.details?.other_platform + }) showMixedChannelWarning.value = true } else { appStore.showError(error.message || t('admin.accounts.bulkEdit.failed'))