mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-26 01:14:47 +08:00
feat: add mixed-channel precheck flow for antigravity accounts
This commit is contained in:
@@ -133,6 +133,13 @@ type BulkUpdateAccountsRequest struct {
|
|||||||
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckMixedChannelRequest represents check mixed channel risk request
|
||||||
|
type CheckMixedChannelRequest struct {
|
||||||
|
Platform string `json:"platform" binding:"required"`
|
||||||
|
GroupIDs []int64 `json:"group_ids"`
|
||||||
|
AccountID *int64 `json:"account_id"`
|
||||||
|
}
|
||||||
|
|
||||||
// AccountWithConcurrency extends Account with real-time concurrency info
|
// AccountWithConcurrency extends Account with real-time concurrency info
|
||||||
type AccountWithConcurrency struct {
|
type AccountWithConcurrency struct {
|
||||||
*dto.Account
|
*dto.Account
|
||||||
@@ -278,6 +285,50 @@ func (h *AccountHandler) GetByID(c *gin.Context) {
|
|||||||
response.Success(c, dto.AccountFromService(account))
|
response.Success(c, dto.AccountFromService(account))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckMixedChannel handles checking mixed channel risk for account-group binding.
|
||||||
|
// POST /api/v1/admin/accounts/check-mixed-channel
|
||||||
|
func (h *AccountHandler) CheckMixedChannel(c *gin.Context) {
|
||||||
|
var req CheckMixedChannelRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.BadRequest(c, "Invalid request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(req.GroupIDs) == 0 {
|
||||||
|
response.Success(c, gin.H{"has_risk": false})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accountID := int64(0)
|
||||||
|
if req.AccountID != nil {
|
||||||
|
accountID = *req.AccountID
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.adminService.CheckMixedChannelRisk(c.Request.Context(), accountID, req.Platform, req.GroupIDs)
|
||||||
|
if err != nil {
|
||||||
|
var mixedErr *service.MixedChannelError
|
||||||
|
if errors.As(err, &mixedErr) {
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"has_risk": true,
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, gin.H{"has_risk": false})
|
||||||
|
}
|
||||||
|
|
||||||
// Create handles creating a new account
|
// Create handles creating a new account
|
||||||
// POST /api/v1/admin/accounts
|
// POST /api/v1/admin/accounts
|
||||||
func (h *AccountHandler) Create(c *gin.Context) {
|
func (h *AccountHandler) Create(c *gin.Context) {
|
||||||
@@ -314,17 +365,10 @@ func (h *AccountHandler) Create(c *gin.Context) {
|
|||||||
// 检查是否为混合渠道错误
|
// 检查是否为混合渠道错误
|
||||||
var mixedErr *service.MixedChannelError
|
var mixedErr *service.MixedChannelError
|
||||||
if errors.As(err, &mixedErr) {
|
if errors.As(err, &mixedErr) {
|
||||||
// 返回特殊错误码要求确认
|
// 创建接口仅返回最小必要字段,详细信息由专门检查接口提供
|
||||||
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,
|
|
||||||
},
|
|
||||||
"require_confirmation": true,
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -378,17 +422,10 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
|||||||
// 检查是否为混合渠道错误
|
// 检查是否为混合渠道错误
|
||||||
var mixedErr *service.MixedChannelError
|
var mixedErr *service.MixedChannelError
|
||||||
if errors.As(err, &mixedErr) {
|
if errors.As(err, &mixedErr) {
|
||||||
// 返回特殊错误码要求确认
|
// 更新接口仅返回最小必要字段,详细信息由专门检查接口提供
|
||||||
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,
|
|
||||||
},
|
|
||||||
"require_confirmation": true,
|
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupAccountMixedChannelRouter(adminSvc *stubAdminService) *gin.Engine {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
router := gin.New()
|
||||||
|
accountHandler := NewAccountHandler(adminSvc, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
|
||||||
|
router.POST("/api/v1/admin/accounts/check-mixed-channel", accountHandler.CheckMixedChannel)
|
||||||
|
router.POST("/api/v1/admin/accounts", accountHandler.Create)
|
||||||
|
router.PUT("/api/v1/admin/accounts/:id", accountHandler.Update)
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccountHandlerCheckMixedChannelNoRisk(t *testing.T) {
|
||||||
|
adminSvc := newStubAdminService()
|
||||||
|
router := setupAccountMixedChannelRouter(adminSvc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"platform": "antigravity",
|
||||||
|
"group_ids": []int64{27},
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/check-mixed-channel", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
|
require.Equal(t, float64(0), resp["code"])
|
||||||
|
data, ok := resp["data"].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, false, data["has_risk"])
|
||||||
|
require.Equal(t, int64(0), adminSvc.lastMixedCheck.accountID)
|
||||||
|
require.Equal(t, "antigravity", adminSvc.lastMixedCheck.platform)
|
||||||
|
require.Equal(t, []int64{27}, adminSvc.lastMixedCheck.groupIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccountHandlerCheckMixedChannelWithRisk(t *testing.T) {
|
||||||
|
adminSvc := newStubAdminService()
|
||||||
|
adminSvc.checkMixedErr = &service.MixedChannelError{
|
||||||
|
GroupID: 27,
|
||||||
|
GroupName: "claude-max",
|
||||||
|
CurrentPlatform: "Antigravity",
|
||||||
|
OtherPlatform: "Anthropic",
|
||||||
|
}
|
||||||
|
router := setupAccountMixedChannelRouter(adminSvc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"platform": "antigravity",
|
||||||
|
"group_ids": []int64{27},
|
||||||
|
"account_id": 99,
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/check-mixed-channel", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
|
require.Equal(t, float64(0), resp["code"])
|
||||||
|
data, ok := resp["data"].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, true, data["has_risk"])
|
||||||
|
require.Equal(t, "mixed_channel_warning", data["error"])
|
||||||
|
details, ok := data["details"].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, float64(27), details["group_id"])
|
||||||
|
require.Equal(t, "claude-max", details["group_name"])
|
||||||
|
require.Equal(t, "Antigravity", details["current_platform"])
|
||||||
|
require.Equal(t, "Anthropic", details["other_platform"])
|
||||||
|
require.Equal(t, int64(99), adminSvc.lastMixedCheck.accountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccountHandlerCreateMixedChannelConflictSimplifiedResponse(t *testing.T) {
|
||||||
|
adminSvc := newStubAdminService()
|
||||||
|
adminSvc.createAccountErr = &service.MixedChannelError{
|
||||||
|
GroupID: 27,
|
||||||
|
GroupName: "claude-max",
|
||||||
|
CurrentPlatform: "Antigravity",
|
||||||
|
OtherPlatform: "Anthropic",
|
||||||
|
}
|
||||||
|
router := setupAccountMixedChannelRouter(adminSvc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"name": "ag-oauth-1",
|
||||||
|
"platform": "antigravity",
|
||||||
|
"type": "oauth",
|
||||||
|
"credentials": map[string]any{"refresh_token": "rt"},
|
||||||
|
"group_ids": []int64{27},
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusConflict, rec.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
|
require.Equal(t, "mixed_channel_warning", resp["error"])
|
||||||
|
require.Contains(t, resp["message"], "mixed_channel_warning")
|
||||||
|
_, hasDetails := resp["details"]
|
||||||
|
_, hasRequireConfirmation := resp["require_confirmation"]
|
||||||
|
require.False(t, hasDetails)
|
||||||
|
require.False(t, hasRequireConfirmation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAccountHandlerUpdateMixedChannelConflictSimplifiedResponse(t *testing.T) {
|
||||||
|
adminSvc := newStubAdminService()
|
||||||
|
adminSvc.updateAccountErr = &service.MixedChannelError{
|
||||||
|
GroupID: 27,
|
||||||
|
GroupName: "claude-max",
|
||||||
|
CurrentPlatform: "Antigravity",
|
||||||
|
OtherPlatform: "Anthropic",
|
||||||
|
}
|
||||||
|
router := setupAccountMixedChannelRouter(adminSvc)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"group_ids": []int64{27},
|
||||||
|
})
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/v1/admin/accounts/3", bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusConflict, rec.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
|
||||||
|
require.Equal(t, "mixed_channel_warning", resp["error"])
|
||||||
|
require.Contains(t, resp["message"], "mixed_channel_warning")
|
||||||
|
_, hasDetails := resp["details"]
|
||||||
|
_, hasRequireConfirmation := resp["require_confirmation"]
|
||||||
|
require.False(t, hasDetails)
|
||||||
|
require.False(t, hasRequireConfirmation)
|
||||||
|
}
|
||||||
@@ -10,19 +10,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type stubAdminService struct {
|
type stubAdminService struct {
|
||||||
users []service.User
|
users []service.User
|
||||||
apiKeys []service.APIKey
|
apiKeys []service.APIKey
|
||||||
groups []service.Group
|
groups []service.Group
|
||||||
accounts []service.Account
|
accounts []service.Account
|
||||||
proxies []service.Proxy
|
proxies []service.Proxy
|
||||||
proxyCounts []service.ProxyWithAccountCount
|
proxyCounts []service.ProxyWithAccountCount
|
||||||
redeems []service.RedeemCode
|
redeems []service.RedeemCode
|
||||||
createdAccounts []*service.CreateAccountInput
|
createdAccounts []*service.CreateAccountInput
|
||||||
createdProxies []*service.CreateProxyInput
|
createdProxies []*service.CreateProxyInput
|
||||||
updatedProxyIDs []int64
|
updatedProxyIDs []int64
|
||||||
updatedProxies []*service.UpdateProxyInput
|
updatedProxies []*service.UpdateProxyInput
|
||||||
testedProxyIDs []int64
|
testedProxyIDs []int64
|
||||||
mu sync.Mutex
|
createAccountErr error
|
||||||
|
updateAccountErr error
|
||||||
|
checkMixedErr error
|
||||||
|
lastMixedCheck struct {
|
||||||
|
accountID int64
|
||||||
|
platform string
|
||||||
|
groupIDs []int64
|
||||||
|
}
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStubAdminService() *stubAdminService {
|
func newStubAdminService() *stubAdminService {
|
||||||
@@ -188,11 +196,17 @@ func (s *stubAdminService) CreateAccount(ctx context.Context, input *service.Cre
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.createdAccounts = append(s.createdAccounts, input)
|
s.createdAccounts = append(s.createdAccounts, input)
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
if s.createAccountErr != nil {
|
||||||
|
return nil, s.createAccountErr
|
||||||
|
}
|
||||||
account := service.Account{ID: 300, Name: input.Name, Status: service.StatusActive}
|
account := service.Account{ID: 300, Name: input.Name, Status: service.StatusActive}
|
||||||
return &account, nil
|
return &account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stubAdminService) UpdateAccount(ctx context.Context, id int64, input *service.UpdateAccountInput) (*service.Account, error) {
|
func (s *stubAdminService) UpdateAccount(ctx context.Context, id int64, input *service.UpdateAccountInput) (*service.Account, error) {
|
||||||
|
if s.updateAccountErr != nil {
|
||||||
|
return nil, s.updateAccountErr
|
||||||
|
}
|
||||||
account := service.Account{ID: id, Name: input.Name, Status: service.StatusActive}
|
account := service.Account{ID: id, Name: input.Name, Status: service.StatusActive}
|
||||||
return &account, nil
|
return &account, nil
|
||||||
}
|
}
|
||||||
@@ -224,6 +238,13 @@ func (s *stubAdminService) BulkUpdateAccounts(ctx context.Context, input *servic
|
|||||||
return &service.BulkUpdateAccountsResult{Success: 1, Failed: 0, SuccessIDs: []int64{1}}, nil
|
return &service.BulkUpdateAccountsResult{Success: 1, Failed: 0, SuccessIDs: []int64{1}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *stubAdminService) CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error {
|
||||||
|
s.lastMixedCheck.accountID = currentAccountID
|
||||||
|
s.lastMixedCheck.platform = currentAccountPlatform
|
||||||
|
s.lastMixedCheck.groupIDs = append([]int64(nil), groupIDs...)
|
||||||
|
return s.checkMixedErr
|
||||||
|
}
|
||||||
|
|
||||||
func (s *stubAdminService) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]service.Proxy, int64, error) {
|
func (s *stubAdminService) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]service.Proxy, int64, error) {
|
||||||
search = strings.TrimSpace(strings.ToLower(search))
|
search = strings.TrimSpace(strings.ToLower(search))
|
||||||
filtered := make([]service.Proxy, 0, len(s.proxies))
|
filtered := make([]service.Proxy, 0, len(s.proxies))
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|||||||
accounts.GET("", h.Admin.Account.List)
|
accounts.GET("", h.Admin.Account.List)
|
||||||
accounts.GET("/:id", h.Admin.Account.GetByID)
|
accounts.GET("/:id", h.Admin.Account.GetByID)
|
||||||
accounts.POST("", h.Admin.Account.Create)
|
accounts.POST("", h.Admin.Account.Create)
|
||||||
|
accounts.POST("/check-mixed-channel", h.Admin.Account.CheckMixedChannel)
|
||||||
accounts.POST("/sync/crs", h.Admin.Account.SyncFromCRS)
|
accounts.POST("/sync/crs", h.Admin.Account.SyncFromCRS)
|
||||||
accounts.POST("/sync/crs/preview", h.Admin.Account.PreviewFromCRS)
|
accounts.POST("/sync/crs/preview", h.Admin.Account.PreviewFromCRS)
|
||||||
accounts.PUT("/:id", h.Admin.Account.Update)
|
accounts.PUT("/:id", h.Admin.Account.Update)
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ type AdminService interface {
|
|||||||
SetAccountError(ctx context.Context, id int64, errorMsg string) error
|
SetAccountError(ctx context.Context, id int64, errorMsg string) error
|
||||||
SetAccountSchedulable(ctx context.Context, id int64, schedulable bool) (*Account, error)
|
SetAccountSchedulable(ctx context.Context, id int64, schedulable bool) (*Account, error)
|
||||||
BulkUpdateAccounts(ctx context.Context, input *BulkUpdateAccountsInput) (*BulkUpdateAccountsResult, error)
|
BulkUpdateAccounts(ctx context.Context, input *BulkUpdateAccountsInput) (*BulkUpdateAccountsResult, error)
|
||||||
|
CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error
|
||||||
|
|
||||||
// Proxy management
|
// Proxy management
|
||||||
ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]Proxy, int64, error)
|
ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string) ([]Proxy, int64, error)
|
||||||
@@ -1706,6 +1707,11 @@ func (s *adminServiceImpl) checkMixedChannelRisk(ctx context.Context, currentAcc
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckMixedChannelRisk checks whether target groups contain mixed channels for the current account platform.
|
||||||
|
func (s *adminServiceImpl) CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error {
|
||||||
|
return s.checkMixedChannelRisk(ctx, currentAccountID, currentAccountPlatform, groupIDs)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *adminServiceImpl) attachProxyLatency(ctx context.Context, proxies []ProxyWithAccountCount) {
|
func (s *adminServiceImpl) attachProxyLatency(ctx context.Context, proxies []ProxyWithAccountCount) {
|
||||||
if s.proxyLatencyCache == nil || len(proxies) == 0 {
|
if s.proxyLatencyCache == nil || len(proxies) == 0 {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import type {
|
|||||||
AccountUsageStatsResponse,
|
AccountUsageStatsResponse,
|
||||||
TempUnschedulableStatus,
|
TempUnschedulableStatus,
|
||||||
AdminDataPayload,
|
AdminDataPayload,
|
||||||
AdminDataImportResult
|
AdminDataImportResult,
|
||||||
|
CheckMixedChannelRequest,
|
||||||
|
CheckMixedChannelResponse
|
||||||
} from '@/types'
|
} from '@/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,6 +82,16 @@ export async function update(id: number, updates: UpdateAccountRequest): Promise
|
|||||||
return data
|
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
|
* Delete account
|
||||||
* @param id - Account ID
|
* @param id - Account ID
|
||||||
@@ -458,6 +470,7 @@ export const accountsAPI = {
|
|||||||
getById,
|
getById,
|
||||||
create,
|
create,
|
||||||
update,
|
update,
|
||||||
|
checkMixedChannelRisk,
|
||||||
delete: deleteAccount,
|
delete: deleteAccount,
|
||||||
toggleStatus,
|
toggleStatus,
|
||||||
testAccount,
|
testAccount,
|
||||||
|
|||||||
@@ -1932,7 +1932,7 @@
|
|||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
:show="showMixedChannelWarning"
|
:show="showMixedChannelWarning"
|
||||||
:title="t('admin.accounts.mixedChannelWarningTitle')"
|
:title="t('admin.accounts.mixedChannelWarningTitle')"
|
||||||
:message="mixedChannelWarningDetails ? t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails) : ''"
|
:message="mixedChannelWarningMessageText"
|
||||||
:confirm-text="t('common.confirm')"
|
:confirm-text="t('common.confirm')"
|
||||||
:cancel-text="t('common.cancel')"
|
:cancel-text="t('common.cancel')"
|
||||||
:danger="true"
|
:danger="true"
|
||||||
@@ -1964,8 +1964,14 @@ 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 {
|
||||||
import type { Proxy, AdminGroup, AccountPlatform, AccountType } from '@/types'
|
Proxy,
|
||||||
|
AdminGroup,
|
||||||
|
AccountPlatform,
|
||||||
|
AccountType,
|
||||||
|
CheckMixedChannelResponse,
|
||||||
|
CreateAccountRequest
|
||||||
|
} 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'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
@@ -2103,9 +2109,13 @@ 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)
|
||||||
|
|
||||||
const mixedChannelWarning = useMixedChannelWarning()
|
const showMixedChannelWarning = ref(false)
|
||||||
const showMixedChannelWarning = mixedChannelWarning.show
|
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(
|
||||||
const mixedChannelWarningDetails = mixedChannelWarning.details
|
null
|
||||||
|
)
|
||||||
|
const mixedChannelWarningRawMessage = ref('')
|
||||||
|
const mixedChannelWarningAction = ref<(() => Promise<void>) | null>(null)
|
||||||
|
const antigravityMixedChannelConfirmed = ref(false)
|
||||||
const showAdvancedOAuth = ref(false)
|
const showAdvancedOAuth = ref(false)
|
||||||
const showGeminiHelpDialog = ref(false)
|
const showGeminiHelpDialog = ref(false)
|
||||||
|
|
||||||
@@ -2137,6 +2147,13 @@ const geminiSelectedTier = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const mixedChannelWarningMessageText = computed(() => {
|
||||||
|
if (mixedChannelWarningDetails.value) {
|
||||||
|
return t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails.value)
|
||||||
|
}
|
||||||
|
return mixedChannelWarningRawMessage.value
|
||||||
|
})
|
||||||
|
|
||||||
const geminiQuotaDocs = {
|
const geminiQuotaDocs = {
|
||||||
codeAssist: 'https://developers.google.com/gemini-code-assist/resources/quotas',
|
codeAssist: 'https://developers.google.com/gemini-code-assist/resources/quotas',
|
||||||
aiStudio: 'https://ai.google.dev/pricing',
|
aiStudio: 'https://ai.google.dev/pricing',
|
||||||
@@ -2528,6 +2545,105 @@ const splitTempUnschedKeywords = (value: string) => {
|
|||||||
.filter((item) => item.length > 0)
|
.filter((item) => item.length > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isAntigravityAccount = (platform: AccountPlatform) => platform === 'antigravity'
|
||||||
|
|
||||||
|
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 (isAntigravityAccount(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 (!isAntigravityAccount(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' && isAntigravityAccount(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
|
// Methods
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
step.value = 1
|
step.value = 1
|
||||||
@@ -2583,45 +2699,45 @@ const resetForm = () => {
|
|||||||
geminiOAuth.resetState()
|
geminiOAuth.resetState()
|
||||||
antigravityOAuth.resetState()
|
antigravityOAuth.resetState()
|
||||||
oauthFlowRef.value?.reset()
|
oauthFlowRef.value?.reset()
|
||||||
mixedChannelWarning.cancel()
|
antigravityMixedChannelConfirmed.value = false
|
||||||
|
clearMixedChannelDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
mixedChannelWarning.cancel()
|
antigravityMixedChannelConfirmed.value = false
|
||||||
|
clearMixedChannelDialog()
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to create account with mixed channel warning handling
|
// Helper function to create account with mixed channel warning handling
|
||||||
const doCreateAccount = async (payload: any) => {
|
const doCreateAccount = async (payload: CreateAccountRequest) => {
|
||||||
submitting.value = true
|
const canContinue = await ensureAntigravityMixedChannelConfirmed(async () => {
|
||||||
try {
|
await submitCreateAccount(payload)
|
||||||
await mixedChannelWarning.tryRequest(payload, (p) => adminAPI.accounts.create(p), {
|
})
|
||||||
onSuccess: () => {
|
if (!canContinue) {
|
||||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
return
|
||||||
emit('created')
|
|
||||||
handleClose()
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate'))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
}
|
||||||
|
await submitCreateAccount(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle mixed channel warning confirmation
|
// Handle mixed channel warning confirmation
|
||||||
const handleMixedChannelConfirm = async () => {
|
const handleMixedChannelConfirm = async () => {
|
||||||
|
const action = mixedChannelWarningAction.value
|
||||||
|
if (!action) {
|
||||||
|
clearMixedChannelDialog()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clearMixedChannelDialog()
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await mixedChannelWarning.confirm()
|
await action()
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMixedChannelCancel = () => {
|
const handleMixedChannelCancel = () => {
|
||||||
mixedChannelWarning.cancel()
|
clearMixedChannelDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
@@ -2631,6 +2747,12 @@ const handleSubmit = async () => {
|
|||||||
appStore.showError(t('admin.accounts.pleaseEnterAccountName'))
|
appStore.showError(t('admin.accounts.pleaseEnterAccountName'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const canContinue = await ensureAntigravityMixedChannelConfirmed(async () => {
|
||||||
|
step.value = 2
|
||||||
|
})
|
||||||
|
if (!canContinue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
step.value = 2
|
step.value = 2
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2666,15 +2788,8 @@ const handleSubmit = async () => {
|
|||||||
credentials.model_mapping = antigravityModelMapping
|
credentials.model_mapping = antigravityModelMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
submitting.value = true
|
const extra = mixedScheduling.value ? { mixed_scheduling: true } : undefined
|
||||||
try {
|
await createAccountAndFinish(form.platform, 'apikey', credentials, extra)
|
||||||
const extra = mixedScheduling.value ? { mixed_scheduling: true } : undefined
|
|
||||||
await createAccountAndFinish(form.platform, 'apikey', credentials, extra)
|
|
||||||
} catch (error: any) {
|
|
||||||
appStore.showError(error.response?.data?.detail || t('admin.accounts.failedToCreate'))
|
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2951,7 +3066,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
|
|||||||
const accountName = refreshTokens.length > 1 ? `${form.name} #${i + 1}` : form.name
|
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
|
// Note: Antigravity doesn't have buildExtraInfo, so we pass empty extra or rely on credentials
|
||||||
await adminAPI.accounts.create({
|
const createPayload = withAntigravityConfirmFlag({
|
||||||
name: accountName,
|
name: accountName,
|
||||||
notes: form.notes,
|
notes: form.notes,
|
||||||
platform: 'antigravity',
|
platform: 'antigravity',
|
||||||
@@ -2966,6 +3081,7 @@ const handleAntigravityValidateRT = async (refreshTokenInput: string) => {
|
|||||||
expires_at: form.expires_at,
|
expires_at: form.expires_at,
|
||||||
auto_pause_on_expired: autoPauseOnExpired.value
|
auto_pause_on_expired: autoPauseOnExpired.value
|
||||||
})
|
})
|
||||||
|
await adminAPI.accounts.create(createPayload)
|
||||||
successCount++
|
successCount++
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
failedCount++
|
failedCount++
|
||||||
|
|||||||
@@ -970,7 +970,7 @@
|
|||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
:show="showMixedChannelWarning"
|
:show="showMixedChannelWarning"
|
||||||
:title="t('admin.accounts.mixedChannelWarningTitle')"
|
:title="t('admin.accounts.mixedChannelWarningTitle')"
|
||||||
:message="mixedChannelWarningDetails ? t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails) : ''"
|
:message="mixedChannelWarningMessageText"
|
||||||
:confirm-text="t('common.confirm')"
|
:confirm-text="t('common.confirm')"
|
||||||
:cancel-text="t('common.cancel')"
|
:cancel-text="t('common.cancel')"
|
||||||
:danger="true"
|
:danger="true"
|
||||||
@@ -985,7 +985,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { adminAPI } from '@/api/admin'
|
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 BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||||
import Select from '@/components/common/Select.vue'
|
import Select from '@/components/common/Select.vue'
|
||||||
@@ -994,7 +994,6 @@ 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,
|
||||||
@@ -1061,9 +1060,13 @@ const antigravityModelMappings = ref<ModelMapping[]>([])
|
|||||||
const tempUnschedEnabled = ref(false)
|
const tempUnschedEnabled = ref(false)
|
||||||
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
||||||
|
|
||||||
const mixedChannelWarning = useMixedChannelWarning()
|
const showMixedChannelWarning = ref(false)
|
||||||
const showMixedChannelWarning = mixedChannelWarning.show
|
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(
|
||||||
const mixedChannelWarningDetails = mixedChannelWarning.details
|
null
|
||||||
|
)
|
||||||
|
const mixedChannelWarningRawMessage = ref('')
|
||||||
|
const mixedChannelWarningAction = ref<(() => Promise<void>) | null>(null)
|
||||||
|
const antigravityMixedChannelConfirmed = ref(false)
|
||||||
|
|
||||||
// Quota control state (Anthropic OAuth/SetupToken only)
|
// Quota control state (Anthropic OAuth/SetupToken only)
|
||||||
const windowCostEnabled = ref(false)
|
const windowCostEnabled = ref(false)
|
||||||
@@ -1114,6 +1117,13 @@ const defaultBaseUrl = computed(() => {
|
|||||||
return 'https://api.anthropic.com'
|
return 'https://api.anthropic.com'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const mixedChannelWarningMessageText = computed(() => {
|
||||||
|
if (mixedChannelWarningDetails.value) {
|
||||||
|
return t('admin.accounts.mixedChannelWarning', mixedChannelWarningDetails.value)
|
||||||
|
}
|
||||||
|
return mixedChannelWarningRawMessage.value
|
||||||
|
})
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
@@ -1143,6 +1153,11 @@ watch(
|
|||||||
() => props.account,
|
() => props.account,
|
||||||
(newAccount) => {
|
(newAccount) => {
|
||||||
if (newAccount) {
|
if (newAccount) {
|
||||||
|
antigravityMixedChannelConfirmed.value = false
|
||||||
|
showMixedChannelWarning.value = false
|
||||||
|
mixedChannelWarningDetails.value = null
|
||||||
|
mixedChannelWarningRawMessage.value = ''
|
||||||
|
mixedChannelWarningAction.value = null
|
||||||
form.name = newAccount.name
|
form.name = newAccount.name
|
||||||
form.notes = newAccount.notes || ''
|
form.notes = newAccount.notes || ''
|
||||||
form.proxy_id = newAccount.proxy_id
|
form.proxy_id = newAccount.proxy_id
|
||||||
@@ -1520,20 +1535,123 @@ function toPositiveNumber(value: unknown) {
|
|||||||
return Math.trunc(num)
|
return Math.trunc(num)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isAntigravityAccount = () => props.account?.platform === 'antigravity'
|
||||||
|
|
||||||
|
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 (isAntigravityAccount() && 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 (!isAntigravityAccount()) {
|
||||||
|
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 formatDateTimeLocal = formatDateTimeLocalInput
|
||||||
const parseDateTimeLocal = parseDateTimeLocalInput
|
const parseDateTimeLocal = parseDateTimeLocalInput
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
mixedChannelWarning.cancel()
|
antigravityMixedChannelConfirmed.value = false
|
||||||
|
clearMixedChannelDialog()
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const submitUpdateAccount = async (accountID: number, updatePayload: Record<string, unknown>) => {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await adminAPI.accounts.update(accountID, withAntigravityConfirmFlag(updatePayload))
|
||||||
|
appStore.showSuccess(t('admin.accounts.accountUpdated'))
|
||||||
|
emit('updated')
|
||||||
|
handleClose()
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response?.status === 409 && error.response?.data?.error === 'mixed_channel_warning' && isAntigravityAccount()) {
|
||||||
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
if (!props.account) return
|
if (!props.account) return
|
||||||
const accountID = props.account.id
|
const accountID = props.account.id
|
||||||
|
|
||||||
submitting.value = true
|
|
||||||
const updatePayload: Record<string, unknown> = { ...form }
|
const updatePayload: Record<string, unknown> = { ...form }
|
||||||
try {
|
try {
|
||||||
// 后端期望 proxy_id: 0 表示清除代理,而不是 null
|
// 后端期望 proxy_id: 0 表示清除代理,而不是 null
|
||||||
@@ -1565,7 +1683,6 @@ const handleSubmit = async () => {
|
|||||||
newCredentials.api_key = currentCredentials.api_key
|
newCredentials.api_key = currentCredentials.api_key
|
||||||
} else {
|
} else {
|
||||||
appStore.showError(t('admin.accounts.apiKeyIsRequired'))
|
appStore.showError(t('admin.accounts.apiKeyIsRequired'))
|
||||||
submitting.value = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1585,7 +1702,6 @@ const handleSubmit = async () => {
|
|||||||
newCredentials.intercept_warmup_requests = true
|
newCredentials.intercept_warmup_requests = true
|
||||||
}
|
}
|
||||||
if (!applyTempUnschedConfig(newCredentials)) {
|
if (!applyTempUnschedConfig(newCredentials)) {
|
||||||
submitting.value = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1601,7 +1717,6 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!applyTempUnschedConfig(newCredentials)) {
|
if (!applyTempUnschedConfig(newCredentials)) {
|
||||||
submitting.value = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1617,7 +1732,6 @@ const handleSubmit = async () => {
|
|||||||
delete newCredentials.intercept_warmup_requests
|
delete newCredentials.intercept_warmup_requests
|
||||||
}
|
}
|
||||||
if (!applyTempUnschedConfig(newCredentials)) {
|
if (!applyTempUnschedConfig(newCredentials)) {
|
||||||
submitting.value = false
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1700,34 +1814,36 @@ const handleSubmit = async () => {
|
|||||||
updatePayload.extra = newExtra
|
updatePayload.extra = newExtra
|
||||||
}
|
}
|
||||||
|
|
||||||
await mixedChannelWarning.tryRequest(updatePayload, (p) => adminAPI.accounts.update(accountID, p), {
|
const canContinue = await ensureAntigravityMixedChannelConfirmed(async () => {
|
||||||
onSuccess: () => {
|
await submitUpdateAccount(accountID, updatePayload)
|
||||||
appStore.showSuccess(t('admin.accounts.accountUpdated'))
|
|
||||||
emit('updated')
|
|
||||||
handleClose()
|
|
||||||
},
|
|
||||||
onError: (error: any) => {
|
|
||||||
appStore.showError(error.response?.data?.message || error.response?.data?.detail || t('admin.accounts.failedToUpdate'))
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
if (!canContinue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await submitUpdateAccount(accountID, updatePayload)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
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 {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle mixed channel warning confirmation
|
// Handle mixed channel warning confirmation
|
||||||
const handleMixedChannelConfirm = async () => {
|
const handleMixedChannelConfirm = async () => {
|
||||||
|
const action = mixedChannelWarningAction.value
|
||||||
|
if (!action) {
|
||||||
|
clearMixedChannelDialog()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clearMixedChannelDialog()
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await mixedChannelWarning.confirm()
|
await action()
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMixedChannelCancel = () => {
|
const handleMixedChannelCancel = () => {
|
||||||
mixedChannelWarning.cancel()
|
clearMixedChannelDialog()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -716,6 +716,26 @@ export interface UpdateAccountRequest {
|
|||||||
confirm_mixed_channel_risk?: boolean
|
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 {
|
export interface CreateProxyRequest {
|
||||||
name: string
|
name: string
|
||||||
protocol: ProxyProtocol
|
protocol: ProxyProtocol
|
||||||
|
|||||||
Reference in New Issue
Block a user