fix: use i18n for mixed-channel warning messages and improve bulk pre-check
- BulkUpdate handler: add structured details to 409 response - BulkUpdateAccounts: simplify to global pre-check before any DB write; remove per-account snapshot tracking which is no longer needed - MixedChannelError.Error(): restore English message for API compatibility - BulkEditAccountModal: use t() with details for both pre-check and 409 fallback paths instead of displaying raw backend strings - Update test to verify pre-check blocks on existing group conflicts
This commit is contained in:
@@ -1127,6 +1127,12 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
|
|||||||
c.JSON(409, gin.H{
|
c.JSON(409, gin.H{
|
||||||
"error": "mixed_channel_warning",
|
"error": "mixed_channel_warning",
|
||||||
"message": mixedErr.Error(),
|
"message": mixedErr.Error(),
|
||||||
|
"details": gin.H{
|
||||||
|
"group_id": mixedErr.GroupID,
|
||||||
|
"group_name": mixedErr.GroupName,
|
||||||
|
"current_platform": mixedErr.CurrentPlatform,
|
||||||
|
"other_platform": mixedErr.OtherPlatform,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1542,27 +1542,16 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp
|
|||||||
// 预加载账号平台信息(混合渠道检查或 Sora 同步需要)。
|
// 预加载账号平台信息(混合渠道检查或 Sora 同步需要)。
|
||||||
platformByID := map[int64]string{}
|
platformByID := map[int64]string{}
|
||||||
groupAccountsByID := map[int64][]Account{}
|
groupAccountsByID := map[int64][]Account{}
|
||||||
groupNameByID := map[int64]string{}
|
|
||||||
if needMixedChannelCheck {
|
if needMixedChannelCheck {
|
||||||
accounts, err := s.accountRepo.GetByIDs(ctx, input.AccountIDs)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
groupAccountsByID = loadedAccounts
|
for _, account := range accounts {
|
||||||
groupNameByID = loadedNames
|
if account != nil {
|
||||||
|
platformByID[account.ID] = account.Platform
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预检查混合渠道风险:在任何写操作之前,若发现风险立即返回错误。
|
// 预检查混合渠道风险:在任何写操作之前,若发现风险立即返回错误。
|
||||||
@@ -2529,6 +2518,6 @@ type MixedChannelError struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *MixedChannelError) Error() string {
|
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)
|
e.GroupName, e.CurrentPlatform, e.OtherPlatform)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,34 +139,34 @@ func TestAdminService_BulkUpdateAccounts_NilGroupRepoReturnsError(t *testing.T)
|
|||||||
require.Contains(t, err.Error(), "group repository not configured")
|
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{
|
repo := &accountRepoStubForBulkUpdate{
|
||||||
getByIDsAccounts: []*Account{
|
getByIDsAccounts: []*Account{
|
||||||
{ID: 1, Platform: PlatformAnthropic},
|
{ID: 1, Platform: PlatformAntigravity},
|
||||||
{ID: 2, Platform: PlatformAntigravity},
|
|
||||||
},
|
},
|
||||||
|
// Group 10 already contains an Anthropic account.
|
||||||
listByGroupData: map[int64][]Account{
|
listByGroupData: map[int64][]Account{
|
||||||
10: {},
|
10: {{ID: 99, Platform: PlatformAnthropic}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
svc := &adminServiceImpl{
|
svc := &adminServiceImpl{
|
||||||
accountRepo: repo,
|
accountRepo: repo,
|
||||||
groupRepo: &groupRepoStubForAdmin{getByID: &Group{ID: 10, Name: "目标分组"}},
|
groupRepo: &groupRepoStubForAdmin{getByID: &Group{ID: 10, Name: "target-group"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
groupIDs := []int64{10}
|
groupIDs := []int64{10}
|
||||||
input := &BulkUpdateAccountsInput{
|
input := &BulkUpdateAccountsInput{
|
||||||
AccountIDs: []int64{1, 2},
|
AccountIDs: []int64{1},
|
||||||
GroupIDs: &groupIDs,
|
GroupIDs: &groupIDs,
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := svc.BulkUpdateAccounts(context.Background(), input)
|
result, err := svc.BulkUpdateAccounts(context.Background(), input)
|
||||||
require.NoError(t, err)
|
require.Nil(t, result)
|
||||||
require.Equal(t, 1, result.Success)
|
require.Error(t, err)
|
||||||
require.Equal(t, 1, result.Failed)
|
require.Contains(t, err.Error(), "mixed channel")
|
||||||
require.ElementsMatch(t, []int64{1}, result.SuccessIDs)
|
// No BindGroups should have been called since the check runs before any write.
|
||||||
require.ElementsMatch(t, []int64{2}, result.FailedIDs)
|
require.Empty(t, repo.bindGroupsCalls)
|
||||||
require.Len(t, result.Results, 2)
|
|
||||||
require.Contains(t, result.Results[1].Error, "mixed channel")
|
|
||||||
require.Equal(t, []int64{1}, repo.bindGroupsCalls)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1283,7 +1283,11 @@ const preCheckMixedChannelRisk = async (built: Record<string, unknown>): Promise
|
|||||||
if (!result.has_risk) return true
|
if (!result.has_risk) return true
|
||||||
|
|
||||||
pendingUpdatesForConfirm.value = built
|
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
|
showMixedChannelWarning.value = true
|
||||||
return false
|
return false
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -1358,7 +1362,11 @@ const submitBulkUpdate = async (baseUpdates: Record<string, unknown>) => {
|
|||||||
// 兜底:多平台混合场景下,预检查跳过,由后端 409 触发确认框
|
// 兜底:多平台混合场景下,预检查跳过,由后端 409 触发确认框
|
||||||
if (error.status === 409 && error.error === 'mixed_channel_warning') {
|
if (error.status === 409 && error.error === 'mixed_channel_warning') {
|
||||||
pendingUpdatesForConfirm.value = baseUpdates
|
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
|
showMixedChannelWarning.value = true
|
||||||
} else {
|
} else {
|
||||||
appStore.showError(error.message || t('admin.accounts.bulkEdit.failed'))
|
appStore.showError(error.message || t('admin.accounts.bulkEdit.failed'))
|
||||||
|
|||||||
Reference in New Issue
Block a user